import numpy as np
from collections import defaultdict
import functools
import operator
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib import cm
from matplotlib.text import Text
from mpl_toolkits.mplot3d import Axes3D
# create docs
# sphinx-apidoc -F -o docs rf_linkbudget/
try:
from .rfmath import RFMath
except ImportError:
from rfmath import RFMath
class simResult():
"""
| A class which gets generated by Circuit.simulate(...)
| It hold convenience functions for plotting and accessing the right datsets
Parameters
----------
network : str
circuit network
data : dictionary
circuit data, from a simulation
**kwargs : kwargs
all other kwargs gets saved in a variable with the same name
"""
def __init__(self, network, data, **kwargs):
"""
Parameters
----------
network : str
circuit network
data : dictionary
circuit data, from a simulation
**kwargs : kwargs
all other kwargs gets saved in a variable with the same name
"""
self.network = network
self.nodes = set() # all different nodes in network
self.data = data
self.freq = [key for key in data]
self.power = [key for key in data[self.freq[0]]]
self.graphes = set(functools.reduce(operator.iconcat, [list(network[x][y].nodes) for x in network for y in network[x]], []))
self.nodes = set([g.parent.name for g in self.graphes])
for key, value in kwargs.items():
setattr(self, key, value)
dat = defaultdict(dict) # convert dictionarys to pandas DataFrame
for freq in data:
for pwr in data[freq]:
dat[pwr][freq] = pd.DataFrame(data[freq][pwr])
self.pandas = pd.DataFrame(dat)
@property
def param(self):
"""
| access the parameters as a list
Returns
----------
parameter : list
list of parameters
"""
tmp = self.pandas.applymap(lambda x: list(x.index))
return set(functools.reduce(operator.concat, functools.reduce(operator.concat, tmp.values.tolist())))
def __repr__(self):
out = 'simulation result:\n'
out += '@ frequencies : {}\n'.format(self.freq)
out += '@ input power : {}\n'.format(self.power)
return out
[docs] def setNoiseBandwidth(self, B):
"""
| set the noisebandwidth
| changes the following parameters
- n : noise power
- SNR : signal to noise ratio
- DYN : dynamic
"""
df = self.pandas
def setNoise(x):
for i in range(len(x.loc['n'])):
tn = x.loc['Tn'].iat[i]
p = x.loc['p'].iat[i]
sfdr = x.loc['SFDR'].iat[i]
n = RFMath.convert_T_n(tn, B)
snr = RFMath.calc_SNR(p, n)
dyn = RFMath.calc_Dynamic(snr, sfdr)
x.loc['n'].iat[i] = n
x.loc['SNR'].iat[i] = snr
x.loc['DYN'].iat[i] = dyn
return x
df = df.applymap(lambda x: setNoise(x))
self.pandas = df
[docs] def plot_chain(self, param=None, freq=None, power=None, keys=None):
"""
| plots a window with responsive fields which can be switched between (click or mousewheel)
| plot_chain plots all data from the start to the end, by all ports between
| we can select which ports to show, be defining the keys
| when param, freq or power is not defined, all parameters will be shown
Parameters
----------
param : str or list of str, optional
a single parameter or a list of parameters which shall be plotted
freq : float or list of floats, optional
a single frequency or a list of frequencies which shall be plotted
power : float or list of floats, optional
a single power value or a list of power values which shall be plotted
keys : str or list of str, optional
a single device port or a list of device ports.
only these values are extracted.
Notes
-----
return handler must remain, otherwise the plot will not be responsive anymore
Returns
----------
handler : callback handlers
"""
freq = self.freq if freq is None else freq
power = self.power if power is None else power
param = self.param if param is None else param
freq = freq if type(freq) is list else [freq]
power = power if type(power) is list else [power]
param = param if type(param) is set() else list(param)
param = param if type(param) is list else [param]
fig, ax = plt.subplots()
fig.subplots_adjust(bottom=0.2)
l, = plt.plot([], [])
plt.xticks(rotation=45)
ax.grid(True)
bbox_props = dict(boxstyle="round", fc="w", ec="0.5", alpha=0.9, )
tb = [ax.text(1.015, 0.95, param[0], picker=True, bbox=bbox_props, horizontalalignment='left', rotation=0, transform=ax.transAxes),
ax.text(1.015, 0.85, freq[0], picker=True, bbox=bbox_props, horizontalalignment='left', rotation=0, transform=ax.transAxes),
ax.text(1.015, 0.75, power[0], picker=True, bbox=bbox_props, horizontalalignment='left', rotation=0, transform=ax.transAxes)]
class Index(object):
v = {'param': param, 'freq': freq, 'power': power}
i = dict(zip(['param', 'freq', 'power'], [0, 0, 0]))
t = dict(zip(['param', 'freq', 'power'], tb))
ref = {'fig': fig, 'plot': l, 'axis': ax}
parent = self
def _cb_rotate(self, event, direction, idx):
self.i[idx] = np.mod(self.i[idx] + direction, len(self.v[idx]))
self.redraw()
def redraw(self):
cparam, cfreq, cpower = [self.v['param'][self.i['param']], self.v['freq'][self.i['freq']], self.v['power'][self.i['power']]]
xt, val = self.parent.extractValues(cparam, cfreq, cpower, keys=keys)
self.t['param'].set_text(cparam)
self.t['freq'].set_text('{:.1f}MHz'.format(cfreq/1e6))
self.t['power'].set_text(cpower)
self.ref['plot'].set_data(range(len(xt)), val)
self.ref['axis'].set_xticks(range(len(xt)))
self.ref['axis'].set_xticklabels(xt)
self.ref['axis'].set_ylabel(cparam)
self.ref['axis'].set_title("Performance: freq={:.1f}MHz, Pin={:.1f}dBm".format(cfreq/1e6, cpower), picker=True)
self.ref['axis'].relim()
self.ref['axis'].autoscale_view()
self.ref['fig'].canvas.draw_idle()
callback = Index()
callback.redraw()
def onpick1(event):
if isinstance(event.artist, Text):
text = event.artist
direction = 1 if event.mouseevent.button == 1 or event.mouseevent.button == 'up' else -1
if callback.t['param'] == text:
callback._cb_rotate(event=event, direction=direction, idx='param')
elif callback.t['freq'] == text:
callback._cb_rotate(event=event, direction=direction, idx='freq')
elif callback.t['power'] == text:
callback._cb_rotate(event=event, direction=direction, idx='power')
cid = fig.canvas.mpl_connect('pick_event', onpick1)
handler = (cid, callback)
return handler
[docs] def plot_total(self, param=None, freq=None, power=None):
"""
| plots a window with responsive fields which can be switched between (click or mousewheel)
| plot_total plots all "and" data in respect to frequency or power
| we can select which ports to show, be defining the keys
| when param, freq or power are not defined, all parameters will be shown
Parameters
----------
param : str or list of str, optional
a single parameter or a list of parameters which shall be plotted
freq : float or list of floats, optional
a single frequency or a list of frequencies which shall be plotted
power : float or list of floats, optional
a single power value or a list of power values which shall be plotted
Notes
-----
return handler must remain, otherwise the plot will not be responsive anymore
Returns
----------
handler : callback handlers
"""
freq = self.freq if freq is None else freq
power = self.power if power is None else power
param = self.param if param is None else param
freq = freq if type(freq) is list else [freq]
power = power if type(power) is list else [power]
param = param if type(param) is set() else list(param)
param = param if type(param) is list else [param]
if len(freq) == 1:
varDependend = 'freq'
elif len(power) == 1:
varDependend = 'power'
else:
varDependend = 'freq'
fig, ax = plt.subplots()
fig.subplots_adjust(bottom=0.2)
l, = plt.plot([], [])
ax.grid(True)
bbox_props = dict(boxstyle="round", fc="w", ec="0.5", alpha=0.9, )
tb = [ax.text(1.015, 0.95, param[0], picker=True, bbox=bbox_props, horizontalalignment='left', rotation=0, transform=ax.transAxes),
ax.text(1.015, 0.85, freq[0], picker=True, bbox=bbox_props, horizontalalignment='left', rotation=0, transform=ax.transAxes),
ax.text(1.015, 0.75, power[0], picker=True, bbox=bbox_props, horizontalalignment='left', rotation=0, transform=ax.transAxes)]
class Index(object):
v = {'param': param, 'freq': freq, 'power': power}
i = dict(zip(['param', 'freq', 'power'], [0, 0, 0]))
t = dict(zip(['param', 'freq', 'power'], tb))
ref = {'fig': fig, 'plot': l, 'axis': ax}
varDep = varDependend
parent = self
def _cb_rotate(self, event, direction, idx):
self.i[idx] = np.mod(self.i[idx] + direction, len(self.v[idx]))
self.redraw()
def redraw(self):
cparam, cfreq, cpower = [self.v['param'][self.i['param']], self.v['freq'][self.i['freq']], self.v['power']] if self.varDep == 'freq' else [self.v['param'][self.i['param']], self.v['freq'], self.v['power'][self.i['power']]]
df = self.parent.extractLastValues(cparam, cfreq, cpower)
self.t['param'].set_text(cparam)
self.t['freq'].set_text('{:.1f}MHz'.format(cfreq/1e6)) if self.varDep == 'freq' else self.t['freq'].set_text('freq')
self.t['power'].set_text('power') if self.varDep == 'freq' else self.t['power'].set_text(cpower)
self.ref['plot'].set_data(list(df.index), list(df.values))
self.ref['axis'].set_ylabel(cparam)
self.ref['axis'].set_title("Concat {} plot".format(cparam))
self.ref['axis'].relim()
self.ref['axis'].autoscale_view()
self.ref['fig'].canvas.draw_idle()
def swapDependendVar(self):
if self.varDep == 'freq':
self.varDep = 'power'
else:
self.varDep = 'freq'
self.redraw()
callback = Index()
callback.redraw()
def onpick1(event):
if isinstance(event.artist, Text):
text = event.artist
direction = 1 if event.mouseevent.button == 1 or event.mouseevent.button == 'up' else -1
if event.mouseevent.button == 1:
# clicked on item
if callback.t['freq'] == text or callback.t['power'] == text:
callback.swapDependendVar()
else:
if callback.t['param'] == text:
callback._cb_rotate(event=event, direction=direction, idx='param')
elif callback.t['freq'] == text:
callback._cb_rotate(event=event, direction=direction, idx='freq')
elif callback.t['power'] == text:
callback._cb_rotate(event=event, direction=direction, idx='power')
else:
pass
cid = fig.canvas.mpl_connect('pick_event', onpick1)
handler = (cid, callback)
return handler
[docs] def plot_total_simple(self, param, freq=None, power=None, axis=None):
"""
| plots a window with the defined parameter
| plot_total_simple is a simpler variant of the other plots
| it is not a responvice view, but it is easy to show different parameters at once
Parameters
----------
param : str or list of str
a single parameter or a list of parameters which shall be plotted
freq : float or list of floats, optional
a single frequency or a list of frequencies which shall be plotted
power : float or list of floats, optional
a single power value or a list of power values which shall be plotted
axis : axis object, optional
currently not in use
Notes
-----
return handler must remain, otherwise the plot will not be responsive anymore
Examples
---------
>>> t = sim1.plot_total_simple(['SFDR', 'SNR', 'DYN'], freq=430e6)
>>> plt.show()
Returns
----------
handler : callback handlers
"""
# Todo, use axis parameter to show different plots in one...
fig, axis = plt.subplots()
for p in param:
df = self.extractLastValues(p, freq, power)
if freq is not None and power is None:
plt.plot(df, label=p)
axis.set_xlabel('input frequency [MHz]')
elif freq is None and power is not None:
plt.plot(df.index/1e6, df.values, label=p)
axis.set_xlabel('input power [dBm]')
elif freq is not None and power is not None:
raise ValueError('is a scalar')
else:
raise ValueError('two dimensional plot')
# axis.set_ylabel('Values')
plt.xticks(rotation=45)
plt.xticks(df.index, df.index)
plt.legend()
fig.set_tight_layout({"pad": .0})
plt.grid(True)
[docs] def plot_surface(self, param=None, freq=None, power=None, keys=None):
"""
| plots a window with responsive fields which can be switched between (click or mousewheel)
| plot_surface plots a surface plot with all data in respect to al ports and power or frequency
| we can select which ports to show, be defining the keys
| when param, freq or power are not defined, all parameters will be shown
Parameters
----------
param : str or list of str, optional
a single parameter or a list of parameters which shall be plotted
freq : float or list of floats, optional
a single frequency or a list of frequencies which shall be plotted
power : float or list of floats, optional
a single power value or a list of power values which shall be plotted
keys : str or list of str, optional
a single device port or a list of device ports.
only these values are extracted.
Notes
-----
return handler must remain, otherwise the plot will not be responsive anymore
Returns
----------
handler : callback handlers
"""
freq = self.freq if freq is None else freq
power = self.power if power is None else power
param = self.param if param is None else param
freq = freq if type(freq) is list else [freq]
power = power if type(power) is list else [power]
param = param if type(param) is set() else list(param)
param = param if type(param) is list else [param]
# create figure
fig = plt.figure()
ax = fig.gca(projection='3d')
surf = ax.plot([], [])
fig.tight_layout()
class Index(object):
mesh = np.meshgrid(power, list(range(len(keys))))
v = {'param': param, 'freq': freq, 'power': power}
i = dict(zip(['param', 'freq'], [0, 0]))
t = dict(zip(['param', 'freq'], [0, 0]))
bbox_props = dict(boxstyle="round", fc="w", ec="0.5", alpha=0.9, )
ref = {'fig': fig, 'plot': surf, 'axis': ax}
ports = keys
parent = self
def _cb_rotate(self, event, direction, idx):
self.i[idx] = np.mod(self.i[idx] + direction, len(self.v[idx]))
self.redraw()
def redraw(self):
cparam, cfreq, cpower = [self.v['param'][self.i['param']], self.v['freq'][self.i['freq']], self.v['power']]
(X, Y) = self.mesh
Z = np.zeros(shape=(X.shape[0], X.shape[1]))
for col, c in enumerate(Z):
for row, r in enumerate(c):
xt, val = self.parent.extractValues(cparam, freq=cfreq, power=cpower[row], keys=[self.ports[col]])
Z[col][row] = val.values[0]
self.ref['axis'].cla()
self.t['param'] = self.ref['axis'].text2D(0.05, 0.95, cparam, transform=ax.transAxes, bbox=self.bbox_props, picker=True)
self.t['freq'] = self.ref['axis'].text2D(0.05, 0.90, cfreq, transform=ax.transAxes, bbox=self.bbox_props, picker=True)
self.ref['axis'].plot_surface(X, Y, Z, cmap=cm.coolwarm,
linewidth=1, antialiased=True, alpha=0.75, vmin=-174, vmax=-156, edgecolor='none')
self.ref['axis'].set_title("surface plot : {}".format(cparam))
self.ref['axis'].set_yticks(range(len(self.ports)))
self.ref['axis'].set_yticklabels([str(x) for x in self.ports])
self.ref['axis'].relim()
self.ref['axis'].autoscale_view()
self.ref['fig'].canvas.draw_idle()
pass
callback = Index()
callback.redraw()
def onpick1(event):
if isinstance(event.artist, Text):
text = event.artist
direction = 1 if event.mouseevent.button == 1 or event.mouseevent.button == 'up' else -1
if callback.t['param'] == text:
callback._cb_rotate(event=event, direction=direction, idx='param')
elif callback.t['freq'] == text:
callback._cb_rotate(event=event, direction=direction, idx='freq')
else:
pass
cid = fig.canvas.mpl_connect('pick_event', onpick1)
handler = (cid, callback)
return handler
# ============================================================================
if __name__ == "__main__":
pass