Module koma.plot
Visualization module
All functions related to the plotting of results from OMA methods.
Expand source code
"""
##############################################
Visualization module
##############################################
All functions related to the plotting of results from OMA methods.
"""
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import colors as mcolors
from matplotlib.backend_bases import MouseButton
from matplotlib import cm
import warnings
from .modal import mpc
from matplotlib.offsetbox import AnnotationBbox, TextArea
class Selector:
def __init__(self, ax, active_settings={}, deactive_settings={}, templine_settings={}, picked_settings={}):
self.ax = ax
self.fig = ax.get_figure()
self.lines = list(ax.lines)
self.picked = []
self.picked_lines = []
self.index = 0
self.templine_settings = dict(ls='-', lw=0.5) | templine_settings
if len(self.lines)>0:
self.x = self.lines[0].get_xdata()
self.active_settings = {'linewidth': 2.0, 'alpha':1.0} | active_settings
self.deactive_settings = {'linewidth': 1.0, 'alpha': 0.5} | deactive_settings
self.picked_settings = dict(ls='--', lw=1) | picked_settings
@property
def n_lines(self):
return len(self.lines)
@property
def current_line(self):
return self.lines[self.index]
def on_click(self, event):
if event.inaxes and event.button is MouseButton.LEFT and self.fig.canvas.manager.toolbar.mode.value == '':
color = self.current_line.get_color()
ix_sel = np.argmin(np.abs(event.xdata - self.x))
self.picked.append([self.index, ix_sel, self.x[ix_sel]])
self.picked_lines.append(self.ax.axvline(self.x[ix_sel], color=color, **self.picked_settings))
elif event.inaxes and event.button is MouseButton.RIGHT:
ix_sel = np.argmin(np.abs(event.xdata - self.x))
picked_mat = np.vstack(self.picked)
valid_x_ix = np.where((picked_mat[:,0] == self.index))[0]
if len(valid_x_ix)>0:
row_ix = np.argmin(np.abs(ix_sel-picked_mat[valid_x_ix, 1]))
ix_remove = valid_x_ix[row_ix]
self.picked.pop(ix_remove)
self.picked_lines[ix_remove].remove()
self.picked_lines.pop(ix_remove)
self.fig.canvas.draw()
def on_scroll(self, event):
increment = 1 if event.button == 'up' else -1
self.index = np.clip(self.index + increment, 0, self.n_lines-1)
self.x = self.lines[self.index].get_xdata()
self.templine.set_color(self.current_line.get_color())
self.update()
def update(self):
for ix,line in enumerate(self.lines):
if ix==self.index:
line.set(**self.active_settings)
else:
line.set(**self.deactive_settings)
self.fig.canvas.draw()
def on_close(self, event):
self.fig.canvas.stop_event_loop()
def on_keyboard(self, event):
if event.key == 'enter':
self.title_format = 'ix = {index}'
self.update()
self.fig.canvas.stop_event_loop()
# Prepare for printing
self.templine.set_xdata([np.nan])
for ix,line in enumerate(self.lines):
line.set(**self.deactive_settings)
line.set(alpha=1.0)
def on_move(self, event):
if event.inaxes:
ix_sel = np.argmin(np.abs(event.xdata-self.x))
self.templine.set_xdata([self.x[ix_sel]])
self.fig.canvas.draw()
def get_fig(self, show=True, block=True):
'''
Get an interactive figure.
Parameters
-----------
show : bool, default=True
whether or not to automatically show figure
block : bool, default=True
whether or not to block before returning to retain interactivity
Returns
-----------
fig : `matplotlib.Figure` object
figure object
'''
self.ax.legend(frameon=False)
self.templine = self.ax.axvline(np.nan, **self.templine_settings)
self.templine.set_color(self.current_line.get_color())
self.fig.canvas.mpl_connect('scroll_event', self.on_scroll)
self.fig.canvas.mpl_connect('button_press_event', self.on_click)
self.fig.canvas.mpl_connect('motion_notify_event', self.on_move)
self.fig.canvas.mpl_connect('key_press_event', self.on_keyboard)
self.fig.canvas.mpl_connect('close_event', self.on_close)
if block:
self.title_format = 'ix = {index} (Press enter when selection is done)'
self.update()
if show:
plt.show()
if block:
self.fig.canvas.start_event_loop()
return self.fig
@property
def all_picked_x(self):
return np.array([p[2] for p in self.picked])
@property
def picked_x(self):
pmat = np.vstack(self.picked)
x = [None]*self.n_lines
for ix in range(self.n_lines):
okix = pmat[:,0] == ix
x[ix] = pmat[okix, 2]
return x
@property
def picked_ix(self):
pmat = np.vstack(self.picked)
x_ix = [None]*self.n_lines
for ix in range(self.n_lines):
okix = pmat[:,0] == ix
x_ix[ix] = pmat[okix, 1].astype(int)
return x_ix
@property
def picked_line_ix(self):
return np.array([p[0] for p in self.picked])
@property
def all_picked_ix(self):
return np.array([p[1] for p in self.picked])
class StabPlotter:
'''
Class to initialize interactive stabilization plot.
Example
-----------
Assuming we have a data matrix `data` sampled at `fs=128.0`. The data is first processed
using the covssi (can also be filtered using the `find_stable_poles` function):
fs = 128.0
i = 30
orders_input = np.arange(2, 100, 2)
lambd, phi, orders = koma.oma.covssi(data, fs, i, orders_input)
The stabilization plot is generated like this:
stab_plotter = koma.plot.StabPlotter(lambd, orders, phi=phi, freq_unit='hz', damped_freq=False, annotate_hover=True)
fig = stab_plotter.get_fig()
The following code extracts the data from the plotter object as variables:
phi_sel, fn_sel, xi_sel = stab_plotter.phi, stab_plotter.fn, stab_plotter.xi
'
'''
def __init__(self, lambd, orders, phi=None, freq_unit='rad/s',
damped_freq=False, psd_freq=None, psd_y=None, log_psd_scale=True,
pole_settings=None, selected_pole_settings=None, hover_pole_settings=None, ax=None, num=None,
sort_by='undamped', annotate_hover=False, psd_color='gray'):
'''
Parameters
------------
lambd : double
array with complex-valued eigenvalues
orders : int
corresponding order for each pole in `lambd`
phi : double, optional
matrix where each column is complex-valued eigenvector corresponding to lambd
freq_unit : str, default='rad/s'
what frequency unit to use; 'Hz' or 'rad/s'
damped_freq : False, optional
whether or not to use damped frequency (or period) values in plot (False enforces undamped freqs)
psd_freq : double, optional
frequency values of plot to overlay, typically spectrum of data
psd_y : double, optional
function values of plot to overlay, typically spectrum of data
log_psd_scale: boolean, default=True
whether or not to plot the overlaid PSD using a logarithmic y-scale
pole_settings : dict
dictionary with settings to pass to the plot settings of the poles
selected_pole_settings : dict
dictionary with settings to pass to the plot settings of the selected poles
hover_pole_settings : dict
dictionary with settings to pass to the plot settings of the pole currently
being hovered
ax : `matplotlib.Axis` object
axis to place plot in; if not given, a new axis in the figure specified
will be created
num : int, optional
figure number used; only used if `ax` = None
sort_by : str, default='undamped'
what quantity to sort output by; either 'undamped', 'damped' or None
psd_color : str, default='gray'
color to use for PSD plot overlayed
Returns
---------------------------
fig : obj
plotly figure object
'''
if pole_settings is None: pole_settings = {}
if selected_pole_settings is None: selected_pole_settings = {}
if hover_pole_settings is None: hover_pole_settings = {}
self._lambd = lambd
self._orders = orders
self._phi = phi
self.sort_by = sort_by
self.damped_freq = damped_freq
# Make list
if psd_y is not None and not isinstance(psd_y, list):
psd_y = [psd_y]
psd_freq = [psd_freq]
self.psd_color = psd_color
self.psd_freq = psd_freq
self.psd_y = psd_y
self.log_psd_scale = log_psd_scale
self.pole_settings = dict(linestyle='none', marker='.', color='k') | pole_settings
self.selected_pole_settings = dict(linestyle='none', marker='o', color='r') | selected_pole_settings
self.hover_pole_settings = dict(linestyle='none', marker='o', color='r', alpha=0.3) | hover_pole_settings
self.annotate_hover = annotate_hover
self._picked = []
self._hoverpos = [np.nan, np.nan]
self._hoverix = None
self._hoverdot = None
self._annotation = None
self.picked_dots = []
if damped_freq:
dampedornot = 'd'
self._f = np.abs(np.imag(lambd))
else:
dampedornot = 'n'
self._f = np.abs(lambd)
if freq_unit.lower() == 'hz':
self._f = self._f/2/np.pi
self.freq_name = fr'$f_{dampedornot}$'
self.freq_unit = 'Hz'
else:
self.freq_name = fr'$\omega_{dampedornot}$'
self.freq_unit = 'rad/s'
if ax is None:
plt.figure(num).clf()
self.fig, self.ax = plt.subplots(num=num, figsize=(18,7))
else:
plt.sca(ax)
self.ax = ax
self.fig = plt.gcf()
@property
def hoverpos(self):
return self._hoverpos
@hoverpos.setter
def hoverpos(self, ix):
self._hoverix = ix
if self._hoverix is None:
x = np.nan
y = np.nan
text = ''
self._annotation.set_visible(False)
else:
x = self._f[ix]
y = self._orders[ix]
text = (f'ix = {self._hoverix}\n' +
f'n = {self._orders[ix]}\n' +
fr'{self.freq_name} = {self._hoverpos[0]:.2f} {self.freq_unit}' + '\n' +
fr'$\xi$ = {self.get_xi(ix)*100:.2f}%')
if self._phi is not None:
this_mpc = mpc(self._phi[:,ix:ix+1])[0]
text = text + f'\nMPC={this_mpc*100:.1f}%'
self._hoverpos = x,y
self._hoverdot.set_xdata([x])
self._hoverdot.set_ydata([y])
if self.annotate_hover:
self._annotation.offsetbox.set(text=text)
self._annotation.xy = self._hoverpos
@property
def picked(self): #sorted picked
if self.sort_by is None:
ix = None
elif self.sort_by == 'undamped':
ix = np.argsort(np.abs(self._lambd[self._picked]))
elif self.sort_by == 'damped':
ix = np.argsort(np.abs(np.imag(self._lambd[self._picked])))
return np.array(self._picked)[ix]
@property
def ix(self): #alias
return self.picked
# Picked orders
@property
def n_picked(self):
if len(self.picked)>0:
return self._orders[self.picked]
else:
return np.empty([0])
# Eigenvalues and eigenvectors
@property
def lambd(self):
if len(self.picked)>0:
return self._lambd[self.picked]
else:
return np.empty([0])
@property
def phi(self):
if len(self.picked)>0:
return self._phi[:, self.picked]
else:
return np.empty([0])
# Damped natural freqs
@property
def wd(self):
return np.abs(np.imag(self.lambd))
@property
def omegad(self):
return self.wd
@property
def fd(self):
return self.wd/2/np.pi
# Undamped natural freqs
@property
def wn(self):
return np.abs(self.lambd)
@property
def omegan(self):
return self.wn
@property
def fn(self):
return self.wn/2/np.pi
# Damping
@property
def xi(self):
return -np.real(self.lambd)/np.abs(self.lambd)
def get_xi(self, ix=None):
xi = -np.real(self._lambd[ix])/np.abs(self._lambd[ix])
return xi
def get_df(self, pars=['ix', 'n_picked', 'wn', 'xi']):
'''
Get pandas dataframe with results.
'''
df = pd.DataFrame(data=np.vstack([getattr(self, par) for par in pars]).T,
columns=pars)
if 'n_picked' in df:
df['n_picked'] = df['n_picked'].astype(int)
if 'picked' in df:
df['picked'] = df['picked'].astype(int)
if 'ix' in df:
df['ix'] = df['ix'].astype(int)
return df
def get_ix(self, event):
dist = (event.xdata - self._f)**2 + (event.ydata-self._orders)**2
ix_sel = np.argmin(dist)
return ix_sel
def get_fig(self, show=True, block=True):
if self.psd_y is not None:
ax2 = self.ax.twinx()
for ix, (psd, fi) in enumerate(zip(self.psd_y, self.psd_freq)):
ax2.plot(fi, psd, self.psd_color)
if self.log_psd_scale:
ax2.set_yscale('log')
self.ax.plot(self._f, self._orders, **self.pole_settings)
self.ax.set_xlabel(f'{self.freq_name} [{self.freq_unit}]')
self.ax.set_ylabel('Order, $n$')
self._hoverdot = self.ax.plot([np.nan], [np.nan], **self.hover_pole_settings)[0]
if self.annotate_hover:
offsetbox = TextArea('')
self._annotation = AnnotationBbox(offsetbox, [np.nan, np.nan],
xybox=(80, 0),
xycoords='data',
boxcoords="offset points",
arrowprops=dict(arrowstyle="->"),
pad=0.3, bboxprops=dict(alpha=0.85))
self.ax.add_artist(self._annotation)
if self.psd_y is not None:
self.ax.set_zorder(ax2.get_zorder() + 1)
self.ax.patch.set_visible(False)
self.fig.canvas.mpl_connect('button_press_event', self.on_click)
self.fig.canvas.mpl_connect('motion_notify_event', self.on_move)
self.fig.canvas.mpl_connect('key_press_event', self.on_keyboard)
self.fig.canvas.mpl_connect('close_event', self.on_close)
if block:
self.title_format = '(Press enter when selection is done)'
self.update()
if show:
plt.show()
if block:
self.fig.canvas.start_event_loop()
return self.fig
# Interaction methods
def on_click(self, event):
if event.inaxes:
ix_sel = self.get_ix(event)
if (event.button is MouseButton.LEFT and self.fig.canvas.manager.toolbar.mode.value == ''):
self._picked.append(ix_sel)
self.picked_dots.append(
self.ax.plot(self._f[ix_sel], self._orders[ix_sel],
**self.selected_pole_settings)[0]
)
elif event.button is MouseButton.RIGHT:
if ix_sel in self._picked:
ix_remove = self._picked.index(ix_sel)
self._picked.pop(ix_remove)
self.picked_dots[ix_remove].remove()
self.picked_dots.pop(ix_remove)
self.fig.canvas.draw()
def on_close(self, event):
self.fig.canvas.stop_event_loop()
def on_keyboard(self, event):
if event.key == 'enter':
# Prepare for printing
self.hoverpos = None
self.update()
self.fig.canvas.stop_event_loop()
def on_move(self, event):
if event.inaxes:
ix_hover = self.get_ix(event)
self.hoverpos = ix_hover
self.fig.canvas.draw()
def update(self):
self.fig.canvas.draw()
class FDDPlotter:
'''
Class to initiate interactive peak picking plot for FDD (or directly on CPSD).
Example
----------
Assuming we have a data matrix `data` sampled at `fs=128.0`.
First, we import the necessary functions and classes:
from koma.signal import xwelch
from koma.oma import freq_svd
from koma.plot import FDDPlotter
Then, we need to create a CPSD matrix:
f, cpsd = xwelch(data, fs=fs, nfft=2048, nperseg=1024)
This is thereafter decomposed and used as input to create the FDDPlotter object:
U, D = koma.oma.freq_svd(cpsd)
fdd_plotter = koma.plot.FDDPlotter(D, U=U, f=f, lines=[0, 1], num=1)
Finally, we get the figure:
fig = fdd_plotter.get_fig()
Scroll with the mouse to change the active line, click right mouse button to
remove point and left mouse button to add point.
'''
def __init__(self, D, U=None, f=None, lines=None, colors=None, ax=None,
num=None, active_settings={}, deactive_settings={}, picked_settings={},
vline_settings= {}, normalize=False, logaritmic=False, label_str='Singular line'):
'''
Parameters
-------------
D : float
numpy array of dimensions N_dofs x N_dofs x N_freq; can either be diagonal
(3d) array from FDD (i.e., SVD) or the CPSD matrix (frequencies along last
axis)
U : float, optional
if FDD is used as a prior step, this is the U matrix defining the orthogonal
vectors for varying frequencies
f : float, optional
frequency axis corresponding to D (and U); if not given indices are used
lines : int, optional
list of integers corresponding to the indices of D; these define which
diagonal terms to plot; if not defined, all components/lines are plotted
colors : list, optional
list of strings or rgb tuples to use for the different line plots;
if not given, default color order is applied
ax : `matplotlib.Axis` object
axis to place plot in; if not given, a new axis in the figure specified
will be created
num : int, optional
figure number used; only used if `ax` = None
active_settings : dict
dictionary with settings to use for lines that are active
deactive_settings : dict
dictionary with settings to use for lines that are inactive
picked_settings : dict
dictionary with settings to use for picked dot
vline_settings : dict
dictionary with settings to use for vertical line
normalize : bool, default=False
whether or not to normalize each line to its max value
logaritmic : bool, default=False
whether or not to use logaritmic y-axis
label_str : str, default='Singular line'
label used in legend
Returns
----------
fdd_plotter : `FDDPlotter` object
'''
self.logaritmic = logaritmic
self.normalize = normalize
if U is None:
U = np.stack([np.eye(D.shape[0])]*D.shape[2],axis=2)
self._U = U
self._D = D
self.line_ixs = np.array(lines)
self.index = 0
self.lines = [None]*len(self.line_ixs)
self.n_lines = len(self.line_ixs)
self.active_settings = {'linewidth': 2.0, 'alpha':1.0} | active_settings
self.deactive_settings = {'linewidth': 1.0, 'alpha': 0.5} | deactive_settings
self._picked = []
self.picked_dots = []
self.vline_settings = dict(ls='-', lw=0.5) | vline_settings
self.title_format = 'ix = {index}'
self.label_str = label_str
if f is None:
self.f = np.arange(self.D.shape[2])
else:
self.f = f
if ax is None:
plt.figure(num).clf()
self.fig, self.ax = plt.subplots(num=num)
else:
plt.sca(ax)
self.ax = ax
self.fig = plt.gcf()
if colors is None:
self.colors = list(mcolors.TABLEAU_COLORS.values())
else:
self.colors = colors
def get_fig(self, show=True, block=True):
'''
Get an interactive figure.
Parameters
-----------
show : bool, default=True
whether or not to automatically show figure
block : bool, default=True
whether or not to block before returning to retain interactivity
Returns
-----------
fig : `matplotlib.Figure` object
figure object
'''
for l in range(self.n_lines):
if self.normalize:
plotted = [self.D[l, l, :]/np.max(self.D[l, l, :]) for l in range(self.D.shape[0])]
else:
plotted = [self.D[l, l, :] for l in range(self.D.shape[0])]
if self.logaritmic:
plotted = [np.log10(plotted_l) for plotted_l in plotted]
self.lines[l], = self.ax.plot(self.f, plotted[l], color=self.colors[l], linewidth=1.0, label=f'{self.label_str} {self.line_ixs[l]}')
self.ax.legend(frameon=False)
self.vline = self.ax.axvline(np.nan, **self.vline_settings)
self.vline.set_color(self.current_line.get_color())
self.fig.canvas.mpl_connect('scroll_event', self.on_scroll)
self.fig.canvas.mpl_connect('button_press_event', self.on_click)
self.fig.canvas.mpl_connect('motion_notify_event', self.on_move)
self.fig.canvas.mpl_connect('key_press_event', self.on_keyboard)
self.fig.canvas.mpl_connect('close_event', self.on_close)
if block:
self.title_format = 'ix = {index} (Press enter when selection is done)'
self.update()
if show:
plt.show()
if block:
self.fig.canvas.start_event_loop()
return self.fig
@property
def picked(self):
if len(self._picked) == 0:
return self._picked
else:
_picked = np.array(self._picked)
sort_ix = np.argsort(_picked[:,1])
return _picked[sort_ix, :]
@property
def current_line(self):
return self.lines[self.index]
def on_click(self, event):
if event.inaxes and event.button is MouseButton.LEFT and self.fig.canvas.manager.toolbar.mode.value == '':
current_line = self.current_line
color = current_line.get_color()
ix_sel = np.argmin(np.abs(event.xdata - self.f))
self._picked.append([self.index, ix_sel])
self.picked_dots.append(self.ax.plot(self.f[ix_sel], current_line.get_ydata()[ix_sel], marker='o',
markersize=7, markerfacecolor=color, markeredgecolor='black')[0])
elif event.inaxes and event.button is MouseButton.RIGHT:
ix_sel = np.argmin(np.abs(event.xdata - self.f))
picked_mat = np.array(self._picked)
valid_ix = np.where((picked_mat[:,0] == self.index))[0]
if len(valid_ix)>0:
row_ix = np.argmin(np.abs(ix_sel-picked_mat[valid_ix, 1]))
ix_remove = valid_ix[row_ix]
self._picked.pop(ix_remove)
self.picked_dots[ix_remove].remove()
self.picked_dots.pop(ix_remove)
self.fig.canvas.draw()
def on_close(self, event):
self.fig.canvas.stop_event_loop()
def on_keyboard(self, event):
if event.key == 'enter':
self.title_format = 'ix = {index}'
self.update()
self.fig.canvas.stop_event_loop()
# Prepare for printing
self.vline.set_xdata([np.nan])
for ix,line in enumerate(self.lines):
line.set(**self.deactive_settings)
line.set(alpha=1.0)
def on_move(self, event):
if event.inaxes:
ix_sel = np.argmin(np.abs(event.xdata-self.f))
self.vline.set_xdata([self.f[ix_sel]])
self.fig.canvas.draw()
def on_scroll(self, event):
increment = 1 if event.button == 'up' else -1
self.index = np.clip(self.index + increment, 0, self.n_lines-1)
self.vline.set_color(self.current_line.get_color())
self.update()
def update(self):
self.ax.set_title(self.title_format.format(index = self.line_ixs[self.index]))
for ix,line in enumerate(self.lines):
if ix==self.index:
line.set(**self.active_settings)
else:
line.set(**self.deactive_settings)
self.fig.canvas.draw()
@property
def D(self):
return self._D[np.ix_(self.line_ixs, self.line_ixs, np.arange(self._D.shape[2]))]
@property
def U(self):
return self._U[np.ix_(np.arange(self._U.shape[0]), self.line_ixs, np.arange(self._U.shape[2]))]
@property
def freq(self):
if len(self.picked)>0:
ixs = self.picked[:, 1]
return self.f[ixs]
@property
def phi(self):
if len(self.picked)>0:
phi = np.zeros([self.U.shape[0], len(self._picked)]).astype(complex)
for ix,pick in enumerate(self.picked):
phi[:, ix] = self.U[:, pick[0], pick[1]]
return phi
def stabplot(lambd, orders, phi=None, freq_range=None, frequency_unit='rad/s', damped_freq=False, psd_freq=None, psd_y=None, psd_plot_scale='log',
renderer=None, pole_settings=None, selected_pole_settings=None, to_clipboard='none', return_ix=False):
"""
(DEPRECATED) Generate plotly-based stabilization plot from output from find_stable_poles.
Arguments
---------------------------
lambd : double
array with complex-valued eigenvalues
orders : int
corresponding order for each pole in `lambd`
phi : optional, double
matrix where each column is complex-valued eigenvector corresponding to lambd
freq_range : double, optional
list of min and max values used for frequency axis
frequency_unit : {'rad/s', 'Hz', 's'}, optional
what frequency unit to use ('s' or 'period' enforces period rather than frequency)
damped_freq : False, optional
whether or not to use damped frequency (or period) values in plot (False enforces undamped freqs)
psd_freq : double, optional
[not yet implemented] frequency values of plot to overlay, typically spectrum of data
psd_y : double, optional
[not yet implemented] function values of plot to overlay, typically spectrum of data
psd_plot_scale: {'log', 'linear'}, optional
how to plot the overlaid PSD (linear or logarithmic y-scale)
renderer : None (render no plot - manually render output object), optional
how to plot figure, refer plotly documentation for details
('svg', 'browser', 'notebook', 'notebook_connected', are examples -
use 'default' to give default and None to avoid plot)
to_clipboard : {'df', 'ix', 'none'}, optional
update clipboard every time a pole is added, keeping selected indices or table
'df' is not operational yet
return_ix : False, optional
whether or not to return second variable with indices - this is updated as more poles are selected
Returns
---------------------------
fig : obj
plotly figure object
Notes
----------------------------
By hovering a point, the following data about the point will be given in tooltip:
* Natural frequency / period in specified unit (damped or undamped)
* Order
* Critical damping ratio in % (xi)
* Index of pole (corresponding to inputs lambda_stab and order_stab)
"""
warnings.warn("deprecated", DeprecationWarning)
# Treat input settings
if pole_settings is None: pole_settings = {}
if selected_pole_settings is None: selected_pole_settings = {}
unsel_settings = {'color':'#a9a9a9', 'size':6, 'opacity':0.6}
unsel_settings.update(**pole_settings)
current_settings = listify_each_dict_entry(unsel_settings, len(lambd))
sel_settings = {'color':'#cd5c5c', 'size':10, 'opacity':1.0, 'line': {'color': '#000000', 'width': 0}}
sel_settings.update(**selected_pole_settings)
select_status = np.zeros(len(lambd), dtype=bool)
# Create suffix and frequency value depending on whether damped freq. is requested or not
if damped_freq:
dampedornot = 'd'
omega = np.abs(np.imag(lambd))
else:
dampedornot = 'n'
omega = np.abs(lambd)
# Create frequency/period axis and corresponding labels
if frequency_unit == 'rad/s':
x = omega
xlabel = fr'$\omega_{dampedornot} [{frequency_unit}]$'
tooltip_name = fr'\omega_{dampedornot}'
frequency_unit = 'rad/s'
elif frequency_unit.lower() == 'hz':
x = omega/(2*np.pi)
xlabel = fr'$f_{dampedornot} [{frequency_unit}]$'
tooltip_name = fr'f_{dampedornot}'
frequency_unit = 'Hz'
elif (frequency_unit.lower() == 's') or (frequency_unit.lower() == 'period'):
x = (2*np.pi)/omega
xlabel = fr'Period, $T_{dampedornot} [{frequency_unit}]$'
tooltip_name = fr'T_{dampedornot}'
frequency_unit = 's'
# Damping ratio and index to hover
xi = -np.real(lambd)/np.abs(lambd)
text = [f'xi = {xi_i*100:.2f}% <br> ix = {ix}' for ix, xi_i in enumerate(xi)] # rewrite xi as %, and make string
htemplate = f'{tooltip_name}' + ' = %{x:.3f} ' + f'{frequency_unit}<br>n =' + ' %{y}' +'<br> %{text}'
# Construct dataframe and create scatter trace
poles = pd.DataFrame({'freq': x, 'order':orders})
scatter_trace = go.Scattergl(
x=poles['freq'], y=poles['order'], mode='markers', name='',
hovertemplate = htemplate, text=text,
marker=current_settings)
scatter_trace['name'] = 'Poles'
# PSD overlay trace
overlay_trace = go.Scatter(x=psd_freq, y=psd_y, mode='lines', name='PSD',
hoverinfo='skip', line={'color':'#ffdab9'})
# Create figure object, add traces and adjust labels and axes
# fig = go.FigureWidget(scatter_trace)
fig = make_subplots(rows=2, cols=1, specs=[[{"type": "xy", "secondary_y": True}],
[{"type": "table"}]])
fig.layout.hovermode = 'closest'
fig.add_trace(scatter_trace, secondary_y=False, row=1, col=1)
if psd_freq is not None:
fig.add_trace(overlay_trace, secondary_y=True, row=1, col=1)
fig.update_yaxes(title_text="PSD", secondary_y=True, type=psd_plot_scale)
fig['layout']['yaxis2']['showgrid'] = False
fig.layout['xaxis']['title'] = xlabel
fig.layout['yaxis']['title'] = '$n$'
if freq_range is not None:
fig.layout['xaxis']['range'] = freq_range
fig['layout']['yaxis']['range'] = [0.05, np.max(orders)*1.1]
# Renderer (refer to plotly documentation for details)
if renderer is 'default':
import plotly.io as pio
renderer = pio.renderers.default
df = pd.DataFrame(columns=['ix','x','xi'])
pd.options.display.float_format = '{:,.2f}'.format
fig.add_trace(
go.Table(header=
dict(values=['Pole index', xlabel, r'$\xi [\%]$'],
fill_color='paleturquoise',
align='left'),
cells=
dict(values=[],fill_color='lavender',
align='left', format=["",".4",".4"])), row=2, col=1)
fig.update_layout(
height=1000,
showlegend=False
)
fig = go.FigureWidget(fig) #convert to widget
# Callback function for selection poles
ix = np.arange(0, len(lambd))
ix_sel = []
def update_table():
df = pd.DataFrame(data={'ix': ix[select_status],
'freq': x[select_status],
'xi':100*xi[select_status]})
df = df.sort_values(by=['freq'])
if len(fig.data)==2:
sel_ix = 1
else:
sel_ix = 2
fig.data[sel_ix].cells.values=[df.ix, df.freq, df.xi]
def toggle_pole_selection(trace, clicked_point, selector):
def export_df():
df.to_clipboard(index=False)
def export_ix_list():
import pyperclip #requires pyperclip
ix_str = '[' + ', '.join(str(i) for i in ix_sel) + ']'
pyperclip.copy(ix_str)
for i in clicked_point.point_inds:
if select_status[i]:
for key in current_settings:
current_settings[key][i] = unsel_settings[key]
else:
for key in current_settings:
current_settings[key][i] = sel_settings[key]
select_status[i] = not select_status[i] #swap status
with fig.batch_update():
trace.marker = current_settings
update_table()
ix_sel = ix[select_status]
if to_clipboard == 'ix':
export_ix_list()
elif to_clipboard == 'df':
export_df()
fig.data[0].on_click(toggle_pole_selection)
if renderer == 'browser_legacy':
from plotly.offline import plot
plot(fig, include_mathjax='cdn')
elif renderer is not None:
fig.show(renderer=renderer, include_mathjax='cdn')
if return_ix:
return fig, ix_sel
else:
return fig
def listify_each_dict_entry(dict_in, n):
dict_out = dict()
for key in dict_in:
dict_out[key] = [dict_in[key]]*n
return dict_out
def plot_argand(phi, ax=None, colors=None, labels=None, **plot_settings):
if ax is None:
ax = plt.gca()
if labels is None:
labels = np.arange(len(phi))
plot_settings = {'width': 0.0, 'linestyle': '-'} | plot_settings
if type(colors) == str:
colors = cm.get_cmap(colors, len(phi))
if hasattr(colors, 'colors'):
colors = colors.colors
for ix,val in enumerate(phi):
if colors is not None:
color = colors[ix]
else:
color = None
ax.arrow(0, 0, np.real(val), np.imag(val), color=color, label=labels[ix], **plot_settings)
ymax = np.max(np.abs(ax.get_ylim()))
xmax = np.max(np.abs(ax.get_xlim()))
xymax = np.max([ymax,xmax])
ax.set_ylim([-xymax, xymax])
ax.set_xlim([-xymax, xymax])
return ax
Functions
def listify_each_dict_entry(dict_in, n)
-
Expand source code
def listify_each_dict_entry(dict_in, n): dict_out = dict() for key in dict_in: dict_out[key] = [dict_in[key]]*n return dict_out
def plot_argand(phi, ax=None, colors=None, labels=None, **plot_settings)
-
Expand source code
def plot_argand(phi, ax=None, colors=None, labels=None, **plot_settings): if ax is None: ax = plt.gca() if labels is None: labels = np.arange(len(phi)) plot_settings = {'width': 0.0, 'linestyle': '-'} | plot_settings if type(colors) == str: colors = cm.get_cmap(colors, len(phi)) if hasattr(colors, 'colors'): colors = colors.colors for ix,val in enumerate(phi): if colors is not None: color = colors[ix] else: color = None ax.arrow(0, 0, np.real(val), np.imag(val), color=color, label=labels[ix], **plot_settings) ymax = np.max(np.abs(ax.get_ylim())) xmax = np.max(np.abs(ax.get_xlim())) xymax = np.max([ymax,xmax]) ax.set_ylim([-xymax, xymax]) ax.set_xlim([-xymax, xymax]) return ax
def stabplot(lambd, orders, phi=None, freq_range=None, frequency_unit='rad/s', damped_freq=False, psd_freq=None, psd_y=None, psd_plot_scale='log', renderer=None, pole_settings=None, selected_pole_settings=None, to_clipboard='none', return_ix=False)
-
(DEPRECATED) Generate plotly-based stabilization plot from output from find_stable_poles.
Arguments
lambd
:double
- array with complex-valued eigenvalues
orders
:int
- corresponding order for each pole in
lambd
phi
:optional, double
- matrix where each column is complex-valued eigenvector corresponding to lambd
freq_range
:double
, optional- list of min and max values used for frequency axis
frequency_unit
:{'rad/s', 'Hz', 's'}
, optional- what frequency unit to use ('s' or 'period' enforces period rather than frequency)
damped_freq
:False
, optional- whether or not to use damped frequency (or period) values in plot (False enforces undamped freqs)
psd_freq
:double
, optional- [not yet implemented] frequency values of plot to overlay, typically spectrum of data
psd_y
:double
, optional- [not yet implemented] function values of plot to overlay, typically spectrum of data
psd_plot_scale
:{'log', 'linear'}
, optional- how to plot the overlaid PSD (linear or logarithmic y-scale)
renderer
:None (render no plot - manually render output object)
, optional- how to plot figure, refer plotly documentation for details ('svg', 'browser', 'notebook', 'notebook_connected', are examples - use 'default' to give default and None to avoid plot)
to_clipboard
:{'df', 'ix', 'none'}
, optional- update clipboard every time a pole is added, keeping selected indices or table 'df' is not operational yet
return_ix
:False
, optional- whether or not to return second variable with indices - this is updated as more poles are selected
Returns
fig
:obj
- plotly figure object
Notes
By hovering a point, the following data about the point will be given in tooltip:
* Natural frequency / period in specified unit (damped or undamped) * Order * Critical damping ratio in % (xi) * Index of pole (corresponding to inputs lambda_stab and order_stab)
Expand source code
def stabplot(lambd, orders, phi=None, freq_range=None, frequency_unit='rad/s', damped_freq=False, psd_freq=None, psd_y=None, psd_plot_scale='log', renderer=None, pole_settings=None, selected_pole_settings=None, to_clipboard='none', return_ix=False): """ (DEPRECATED) Generate plotly-based stabilization plot from output from find_stable_poles. Arguments --------------------------- lambd : double array with complex-valued eigenvalues orders : int corresponding order for each pole in `lambd` phi : optional, double matrix where each column is complex-valued eigenvector corresponding to lambd freq_range : double, optional list of min and max values used for frequency axis frequency_unit : {'rad/s', 'Hz', 's'}, optional what frequency unit to use ('s' or 'period' enforces period rather than frequency) damped_freq : False, optional whether or not to use damped frequency (or period) values in plot (False enforces undamped freqs) psd_freq : double, optional [not yet implemented] frequency values of plot to overlay, typically spectrum of data psd_y : double, optional [not yet implemented] function values of plot to overlay, typically spectrum of data psd_plot_scale: {'log', 'linear'}, optional how to plot the overlaid PSD (linear or logarithmic y-scale) renderer : None (render no plot - manually render output object), optional how to plot figure, refer plotly documentation for details ('svg', 'browser', 'notebook', 'notebook_connected', are examples - use 'default' to give default and None to avoid plot) to_clipboard : {'df', 'ix', 'none'}, optional update clipboard every time a pole is added, keeping selected indices or table 'df' is not operational yet return_ix : False, optional whether or not to return second variable with indices - this is updated as more poles are selected Returns --------------------------- fig : obj plotly figure object Notes ---------------------------- By hovering a point, the following data about the point will be given in tooltip: * Natural frequency / period in specified unit (damped or undamped) * Order * Critical damping ratio in % (xi) * Index of pole (corresponding to inputs lambda_stab and order_stab) """ warnings.warn("deprecated", DeprecationWarning) # Treat input settings if pole_settings is None: pole_settings = {} if selected_pole_settings is None: selected_pole_settings = {} unsel_settings = {'color':'#a9a9a9', 'size':6, 'opacity':0.6} unsel_settings.update(**pole_settings) current_settings = listify_each_dict_entry(unsel_settings, len(lambd)) sel_settings = {'color':'#cd5c5c', 'size':10, 'opacity':1.0, 'line': {'color': '#000000', 'width': 0}} sel_settings.update(**selected_pole_settings) select_status = np.zeros(len(lambd), dtype=bool) # Create suffix and frequency value depending on whether damped freq. is requested or not if damped_freq: dampedornot = 'd' omega = np.abs(np.imag(lambd)) else: dampedornot = 'n' omega = np.abs(lambd) # Create frequency/period axis and corresponding labels if frequency_unit == 'rad/s': x = omega xlabel = fr'$\omega_{dampedornot} [{frequency_unit}]$' tooltip_name = fr'\omega_{dampedornot}' frequency_unit = 'rad/s' elif frequency_unit.lower() == 'hz': x = omega/(2*np.pi) xlabel = fr'$f_{dampedornot} [{frequency_unit}]$' tooltip_name = fr'f_{dampedornot}' frequency_unit = 'Hz' elif (frequency_unit.lower() == 's') or (frequency_unit.lower() == 'period'): x = (2*np.pi)/omega xlabel = fr'Period, $T_{dampedornot} [{frequency_unit}]$' tooltip_name = fr'T_{dampedornot}' frequency_unit = 's' # Damping ratio and index to hover xi = -np.real(lambd)/np.abs(lambd) text = [f'xi = {xi_i*100:.2f}% <br> ix = {ix}' for ix, xi_i in enumerate(xi)] # rewrite xi as %, and make string htemplate = f'{tooltip_name}' + ' = %{x:.3f} ' + f'{frequency_unit}<br>n =' + ' %{y}' +'<br> %{text}' # Construct dataframe and create scatter trace poles = pd.DataFrame({'freq': x, 'order':orders}) scatter_trace = go.Scattergl( x=poles['freq'], y=poles['order'], mode='markers', name='', hovertemplate = htemplate, text=text, marker=current_settings) scatter_trace['name'] = 'Poles' # PSD overlay trace overlay_trace = go.Scatter(x=psd_freq, y=psd_y, mode='lines', name='PSD', hoverinfo='skip', line={'color':'#ffdab9'}) # Create figure object, add traces and adjust labels and axes # fig = go.FigureWidget(scatter_trace) fig = make_subplots(rows=2, cols=1, specs=[[{"type": "xy", "secondary_y": True}], [{"type": "table"}]]) fig.layout.hovermode = 'closest' fig.add_trace(scatter_trace, secondary_y=False, row=1, col=1) if psd_freq is not None: fig.add_trace(overlay_trace, secondary_y=True, row=1, col=1) fig.update_yaxes(title_text="PSD", secondary_y=True, type=psd_plot_scale) fig['layout']['yaxis2']['showgrid'] = False fig.layout['xaxis']['title'] = xlabel fig.layout['yaxis']['title'] = '$n$' if freq_range is not None: fig.layout['xaxis']['range'] = freq_range fig['layout']['yaxis']['range'] = [0.05, np.max(orders)*1.1] # Renderer (refer to plotly documentation for details) if renderer is 'default': import plotly.io as pio renderer = pio.renderers.default df = pd.DataFrame(columns=['ix','x','xi']) pd.options.display.float_format = '{:,.2f}'.format fig.add_trace( go.Table(header= dict(values=['Pole index', xlabel, r'$\xi [\%]$'], fill_color='paleturquoise', align='left'), cells= dict(values=[],fill_color='lavender', align='left', format=["",".4",".4"])), row=2, col=1) fig.update_layout( height=1000, showlegend=False ) fig = go.FigureWidget(fig) #convert to widget # Callback function for selection poles ix = np.arange(0, len(lambd)) ix_sel = [] def update_table(): df = pd.DataFrame(data={'ix': ix[select_status], 'freq': x[select_status], 'xi':100*xi[select_status]}) df = df.sort_values(by=['freq']) if len(fig.data)==2: sel_ix = 1 else: sel_ix = 2 fig.data[sel_ix].cells.values=[df.ix, df.freq, df.xi] def toggle_pole_selection(trace, clicked_point, selector): def export_df(): df.to_clipboard(index=False) def export_ix_list(): import pyperclip #requires pyperclip ix_str = '[' + ', '.join(str(i) for i in ix_sel) + ']' pyperclip.copy(ix_str) for i in clicked_point.point_inds: if select_status[i]: for key in current_settings: current_settings[key][i] = unsel_settings[key] else: for key in current_settings: current_settings[key][i] = sel_settings[key] select_status[i] = not select_status[i] #swap status with fig.batch_update(): trace.marker = current_settings update_table() ix_sel = ix[select_status] if to_clipboard == 'ix': export_ix_list() elif to_clipboard == 'df': export_df() fig.data[0].on_click(toggle_pole_selection) if renderer == 'browser_legacy': from plotly.offline import plot plot(fig, include_mathjax='cdn') elif renderer is not None: fig.show(renderer=renderer, include_mathjax='cdn') if return_ix: return fig, ix_sel else: return fig
Classes
class FDDPlotter (D, U=None, f=None, lines=None, colors=None, ax=None, num=None, active_settings={}, deactive_settings={}, picked_settings={}, vline_settings={}, normalize=False, logaritmic=False, label_str='Singular line')
-
Class to initiate interactive peak picking plot for FDD (or directly on CPSD).
Example
Assuming we have a data matrix
data
sampled atfs=128.0
.First, we import the necessary functions and classes:
from koma.signal import xwelch from koma.oma import freq_svd from koma.plot import FDDPlotter
Then, we need to create a CPSD matrix:
f, cpsd = xwelch(data, fs=fs, nfft=2048, nperseg=1024)
This is thereafter decomposed and used as input to create the FDDPlotter object:
U, D = koma.oma.freq_svd(cpsd) fdd_plotter = koma.plot.FDDPlotter(D, U=U, f=f, lines=[0, 1], num=1)
Finally, we get the figure:
fig = fdd_plotter.get_fig()
Scroll with the mouse to change the active line, click right mouse button to remove point and left mouse button to add point.
Parameters
D
:float
- numpy array of dimensions N_dofs x N_dofs x N_freq; can either be diagonal (3d) array from FDD (i.e., SVD) or the CPSD matrix (frequencies along last axis)
U
:float
, optional- if FDD is used as a prior step, this is the U matrix defining the orthogonal vectors for varying frequencies
f
:float
, optional- frequency axis corresponding to D (and U); if not given indices are used
lines
:int
, optional- list of integers corresponding to the indices of D; these define which diagonal terms to plot; if not defined, all components/lines are plotted
colors
:list
, optional- list of strings or rgb tuples to use for the different line plots; if not given, default color order is applied
ax
:matplotlib.Axis</code> object
- axis to place plot in; if not given, a new axis in the figure specified will be created
num
:int
, optional- figure number used; only used if
ax
= None active_settings
:dict
- dictionary with settings to use for lines that are active
deactive_settings
:dict
- dictionary with settings to use for lines that are inactive
picked_settings
:dict
- dictionary with settings to use for picked dot
vline_settings
:dict
- dictionary with settings to use for vertical line
normalize
:bool
, default=False
- whether or not to normalize each line to its max value
logaritmic
:bool
, default=False
- whether or not to use logaritmic y-axis
label_str
:str
, default='Singular line'
- label used in legend
Returns
fdd_plotter
:<a title="koma.plot.FDDPlotter" href="#koma.plot.FDDPlotter">FDDPlotter</a></code> object
Expand source code
class FDDPlotter: ''' Class to initiate interactive peak picking plot for FDD (or directly on CPSD). Example ---------- Assuming we have a data matrix `data` sampled at `fs=128.0`. First, we import the necessary functions and classes: from koma.signal import xwelch from koma.oma import freq_svd from koma.plot import FDDPlotter Then, we need to create a CPSD matrix: f, cpsd = xwelch(data, fs=fs, nfft=2048, nperseg=1024) This is thereafter decomposed and used as input to create the FDDPlotter object: U, D = koma.oma.freq_svd(cpsd) fdd_plotter = koma.plot.FDDPlotter(D, U=U, f=f, lines=[0, 1], num=1) Finally, we get the figure: fig = fdd_plotter.get_fig() Scroll with the mouse to change the active line, click right mouse button to remove point and left mouse button to add point. ''' def __init__(self, D, U=None, f=None, lines=None, colors=None, ax=None, num=None, active_settings={}, deactive_settings={}, picked_settings={}, vline_settings= {}, normalize=False, logaritmic=False, label_str='Singular line'): ''' Parameters ------------- D : float numpy array of dimensions N_dofs x N_dofs x N_freq; can either be diagonal (3d) array from FDD (i.e., SVD) or the CPSD matrix (frequencies along last axis) U : float, optional if FDD is used as a prior step, this is the U matrix defining the orthogonal vectors for varying frequencies f : float, optional frequency axis corresponding to D (and U); if not given indices are used lines : int, optional list of integers corresponding to the indices of D; these define which diagonal terms to plot; if not defined, all components/lines are plotted colors : list, optional list of strings or rgb tuples to use for the different line plots; if not given, default color order is applied ax : `matplotlib.Axis` object axis to place plot in; if not given, a new axis in the figure specified will be created num : int, optional figure number used; only used if `ax` = None active_settings : dict dictionary with settings to use for lines that are active deactive_settings : dict dictionary with settings to use for lines that are inactive picked_settings : dict dictionary with settings to use for picked dot vline_settings : dict dictionary with settings to use for vertical line normalize : bool, default=False whether or not to normalize each line to its max value logaritmic : bool, default=False whether or not to use logaritmic y-axis label_str : str, default='Singular line' label used in legend Returns ---------- fdd_plotter : `FDDPlotter` object ''' self.logaritmic = logaritmic self.normalize = normalize if U is None: U = np.stack([np.eye(D.shape[0])]*D.shape[2],axis=2) self._U = U self._D = D self.line_ixs = np.array(lines) self.index = 0 self.lines = [None]*len(self.line_ixs) self.n_lines = len(self.line_ixs) self.active_settings = {'linewidth': 2.0, 'alpha':1.0} | active_settings self.deactive_settings = {'linewidth': 1.0, 'alpha': 0.5} | deactive_settings self._picked = [] self.picked_dots = [] self.vline_settings = dict(ls='-', lw=0.5) | vline_settings self.title_format = 'ix = {index}' self.label_str = label_str if f is None: self.f = np.arange(self.D.shape[2]) else: self.f = f if ax is None: plt.figure(num).clf() self.fig, self.ax = plt.subplots(num=num) else: plt.sca(ax) self.ax = ax self.fig = plt.gcf() if colors is None: self.colors = list(mcolors.TABLEAU_COLORS.values()) else: self.colors = colors def get_fig(self, show=True, block=True): ''' Get an interactive figure. Parameters ----------- show : bool, default=True whether or not to automatically show figure block : bool, default=True whether or not to block before returning to retain interactivity Returns ----------- fig : `matplotlib.Figure` object figure object ''' for l in range(self.n_lines): if self.normalize: plotted = [self.D[l, l, :]/np.max(self.D[l, l, :]) for l in range(self.D.shape[0])] else: plotted = [self.D[l, l, :] for l in range(self.D.shape[0])] if self.logaritmic: plotted = [np.log10(plotted_l) for plotted_l in plotted] self.lines[l], = self.ax.plot(self.f, plotted[l], color=self.colors[l], linewidth=1.0, label=f'{self.label_str} {self.line_ixs[l]}') self.ax.legend(frameon=False) self.vline = self.ax.axvline(np.nan, **self.vline_settings) self.vline.set_color(self.current_line.get_color()) self.fig.canvas.mpl_connect('scroll_event', self.on_scroll) self.fig.canvas.mpl_connect('button_press_event', self.on_click) self.fig.canvas.mpl_connect('motion_notify_event', self.on_move) self.fig.canvas.mpl_connect('key_press_event', self.on_keyboard) self.fig.canvas.mpl_connect('close_event', self.on_close) if block: self.title_format = 'ix = {index} (Press enter when selection is done)' self.update() if show: plt.show() if block: self.fig.canvas.start_event_loop() return self.fig @property def picked(self): if len(self._picked) == 0: return self._picked else: _picked = np.array(self._picked) sort_ix = np.argsort(_picked[:,1]) return _picked[sort_ix, :] @property def current_line(self): return self.lines[self.index] def on_click(self, event): if event.inaxes and event.button is MouseButton.LEFT and self.fig.canvas.manager.toolbar.mode.value == '': current_line = self.current_line color = current_line.get_color() ix_sel = np.argmin(np.abs(event.xdata - self.f)) self._picked.append([self.index, ix_sel]) self.picked_dots.append(self.ax.plot(self.f[ix_sel], current_line.get_ydata()[ix_sel], marker='o', markersize=7, markerfacecolor=color, markeredgecolor='black')[0]) elif event.inaxes and event.button is MouseButton.RIGHT: ix_sel = np.argmin(np.abs(event.xdata - self.f)) picked_mat = np.array(self._picked) valid_ix = np.where((picked_mat[:,0] == self.index))[0] if len(valid_ix)>0: row_ix = np.argmin(np.abs(ix_sel-picked_mat[valid_ix, 1])) ix_remove = valid_ix[row_ix] self._picked.pop(ix_remove) self.picked_dots[ix_remove].remove() self.picked_dots.pop(ix_remove) self.fig.canvas.draw() def on_close(self, event): self.fig.canvas.stop_event_loop() def on_keyboard(self, event): if event.key == 'enter': self.title_format = 'ix = {index}' self.update() self.fig.canvas.stop_event_loop() # Prepare for printing self.vline.set_xdata([np.nan]) for ix,line in enumerate(self.lines): line.set(**self.deactive_settings) line.set(alpha=1.0) def on_move(self, event): if event.inaxes: ix_sel = np.argmin(np.abs(event.xdata-self.f)) self.vline.set_xdata([self.f[ix_sel]]) self.fig.canvas.draw() def on_scroll(self, event): increment = 1 if event.button == 'up' else -1 self.index = np.clip(self.index + increment, 0, self.n_lines-1) self.vline.set_color(self.current_line.get_color()) self.update() def update(self): self.ax.set_title(self.title_format.format(index = self.line_ixs[self.index])) for ix,line in enumerate(self.lines): if ix==self.index: line.set(**self.active_settings) else: line.set(**self.deactive_settings) self.fig.canvas.draw() @property def D(self): return self._D[np.ix_(self.line_ixs, self.line_ixs, np.arange(self._D.shape[2]))] @property def U(self): return self._U[np.ix_(np.arange(self._U.shape[0]), self.line_ixs, np.arange(self._U.shape[2]))] @property def freq(self): if len(self.picked)>0: ixs = self.picked[:, 1] return self.f[ixs] @property def phi(self): if len(self.picked)>0: phi = np.zeros([self.U.shape[0], len(self._picked)]).astype(complex) for ix,pick in enumerate(self.picked): phi[:, ix] = self.U[:, pick[0], pick[1]] return phi
Instance variables
var D
-
Expand source code
@property def D(self): return self._D[np.ix_(self.line_ixs, self.line_ixs, np.arange(self._D.shape[2]))]
var U
-
Expand source code
@property def U(self): return self._U[np.ix_(np.arange(self._U.shape[0]), self.line_ixs, np.arange(self._U.shape[2]))]
var current_line
-
Expand source code
@property def current_line(self): return self.lines[self.index]
var freq
-
Expand source code
@property def freq(self): if len(self.picked)>0: ixs = self.picked[:, 1] return self.f[ixs]
var phi
-
Expand source code
@property def phi(self): if len(self.picked)>0: phi = np.zeros([self.U.shape[0], len(self._picked)]).astype(complex) for ix,pick in enumerate(self.picked): phi[:, ix] = self.U[:, pick[0], pick[1]] return phi
var picked
-
Expand source code
@property def picked(self): if len(self._picked) == 0: return self._picked else: _picked = np.array(self._picked) sort_ix = np.argsort(_picked[:,1]) return _picked[sort_ix, :]
Methods
def get_fig(self, show=True, block=True)
-
Get an interactive figure.
Parameters
show
:bool
, default=True
- whether or not to automatically show figure
block
:bool
, default=True
- whether or not to block before returning to retain interactivity
Returns
fig
:matplotlib.Figure</code> object
- figure object
Expand source code
def get_fig(self, show=True, block=True): ''' Get an interactive figure. Parameters ----------- show : bool, default=True whether or not to automatically show figure block : bool, default=True whether or not to block before returning to retain interactivity Returns ----------- fig : `matplotlib.Figure` object figure object ''' for l in range(self.n_lines): if self.normalize: plotted = [self.D[l, l, :]/np.max(self.D[l, l, :]) for l in range(self.D.shape[0])] else: plotted = [self.D[l, l, :] for l in range(self.D.shape[0])] if self.logaritmic: plotted = [np.log10(plotted_l) for plotted_l in plotted] self.lines[l], = self.ax.plot(self.f, plotted[l], color=self.colors[l], linewidth=1.0, label=f'{self.label_str} {self.line_ixs[l]}') self.ax.legend(frameon=False) self.vline = self.ax.axvline(np.nan, **self.vline_settings) self.vline.set_color(self.current_line.get_color()) self.fig.canvas.mpl_connect('scroll_event', self.on_scroll) self.fig.canvas.mpl_connect('button_press_event', self.on_click) self.fig.canvas.mpl_connect('motion_notify_event', self.on_move) self.fig.canvas.mpl_connect('key_press_event', self.on_keyboard) self.fig.canvas.mpl_connect('close_event', self.on_close) if block: self.title_format = 'ix = {index} (Press enter when selection is done)' self.update() if show: plt.show() if block: self.fig.canvas.start_event_loop() return self.fig
def on_click(self, event)
-
Expand source code
def on_click(self, event): if event.inaxes and event.button is MouseButton.LEFT and self.fig.canvas.manager.toolbar.mode.value == '': current_line = self.current_line color = current_line.get_color() ix_sel = np.argmin(np.abs(event.xdata - self.f)) self._picked.append([self.index, ix_sel]) self.picked_dots.append(self.ax.plot(self.f[ix_sel], current_line.get_ydata()[ix_sel], marker='o', markersize=7, markerfacecolor=color, markeredgecolor='black')[0]) elif event.inaxes and event.button is MouseButton.RIGHT: ix_sel = np.argmin(np.abs(event.xdata - self.f)) picked_mat = np.array(self._picked) valid_ix = np.where((picked_mat[:,0] == self.index))[0] if len(valid_ix)>0: row_ix = np.argmin(np.abs(ix_sel-picked_mat[valid_ix, 1])) ix_remove = valid_ix[row_ix] self._picked.pop(ix_remove) self.picked_dots[ix_remove].remove() self.picked_dots.pop(ix_remove) self.fig.canvas.draw()
def on_close(self, event)
-
Expand source code
def on_close(self, event): self.fig.canvas.stop_event_loop()
def on_keyboard(self, event)
-
Expand source code
def on_keyboard(self, event): if event.key == 'enter': self.title_format = 'ix = {index}' self.update() self.fig.canvas.stop_event_loop() # Prepare for printing self.vline.set_xdata([np.nan]) for ix,line in enumerate(self.lines): line.set(**self.deactive_settings) line.set(alpha=1.0)
def on_move(self, event)
-
Expand source code
def on_move(self, event): if event.inaxes: ix_sel = np.argmin(np.abs(event.xdata-self.f)) self.vline.set_xdata([self.f[ix_sel]]) self.fig.canvas.draw()
def on_scroll(self, event)
-
Expand source code
def on_scroll(self, event): increment = 1 if event.button == 'up' else -1 self.index = np.clip(self.index + increment, 0, self.n_lines-1) self.vline.set_color(self.current_line.get_color()) self.update()
def update(self)
-
Expand source code
def update(self): self.ax.set_title(self.title_format.format(index = self.line_ixs[self.index])) for ix,line in enumerate(self.lines): if ix==self.index: line.set(**self.active_settings) else: line.set(**self.deactive_settings) self.fig.canvas.draw()
class Selector (ax, active_settings={}, deactive_settings={}, templine_settings={}, picked_settings={})
-
Expand source code
class Selector: def __init__(self, ax, active_settings={}, deactive_settings={}, templine_settings={}, picked_settings={}): self.ax = ax self.fig = ax.get_figure() self.lines = list(ax.lines) self.picked = [] self.picked_lines = [] self.index = 0 self.templine_settings = dict(ls='-', lw=0.5) | templine_settings if len(self.lines)>0: self.x = self.lines[0].get_xdata() self.active_settings = {'linewidth': 2.0, 'alpha':1.0} | active_settings self.deactive_settings = {'linewidth': 1.0, 'alpha': 0.5} | deactive_settings self.picked_settings = dict(ls='--', lw=1) | picked_settings @property def n_lines(self): return len(self.lines) @property def current_line(self): return self.lines[self.index] def on_click(self, event): if event.inaxes and event.button is MouseButton.LEFT and self.fig.canvas.manager.toolbar.mode.value == '': color = self.current_line.get_color() ix_sel = np.argmin(np.abs(event.xdata - self.x)) self.picked.append([self.index, ix_sel, self.x[ix_sel]]) self.picked_lines.append(self.ax.axvline(self.x[ix_sel], color=color, **self.picked_settings)) elif event.inaxes and event.button is MouseButton.RIGHT: ix_sel = np.argmin(np.abs(event.xdata - self.x)) picked_mat = np.vstack(self.picked) valid_x_ix = np.where((picked_mat[:,0] == self.index))[0] if len(valid_x_ix)>0: row_ix = np.argmin(np.abs(ix_sel-picked_mat[valid_x_ix, 1])) ix_remove = valid_x_ix[row_ix] self.picked.pop(ix_remove) self.picked_lines[ix_remove].remove() self.picked_lines.pop(ix_remove) self.fig.canvas.draw() def on_scroll(self, event): increment = 1 if event.button == 'up' else -1 self.index = np.clip(self.index + increment, 0, self.n_lines-1) self.x = self.lines[self.index].get_xdata() self.templine.set_color(self.current_line.get_color()) self.update() def update(self): for ix,line in enumerate(self.lines): if ix==self.index: line.set(**self.active_settings) else: line.set(**self.deactive_settings) self.fig.canvas.draw() def on_close(self, event): self.fig.canvas.stop_event_loop() def on_keyboard(self, event): if event.key == 'enter': self.title_format = 'ix = {index}' self.update() self.fig.canvas.stop_event_loop() # Prepare for printing self.templine.set_xdata([np.nan]) for ix,line in enumerate(self.lines): line.set(**self.deactive_settings) line.set(alpha=1.0) def on_move(self, event): if event.inaxes: ix_sel = np.argmin(np.abs(event.xdata-self.x)) self.templine.set_xdata([self.x[ix_sel]]) self.fig.canvas.draw() def get_fig(self, show=True, block=True): ''' Get an interactive figure. Parameters ----------- show : bool, default=True whether or not to automatically show figure block : bool, default=True whether or not to block before returning to retain interactivity Returns ----------- fig : `matplotlib.Figure` object figure object ''' self.ax.legend(frameon=False) self.templine = self.ax.axvline(np.nan, **self.templine_settings) self.templine.set_color(self.current_line.get_color()) self.fig.canvas.mpl_connect('scroll_event', self.on_scroll) self.fig.canvas.mpl_connect('button_press_event', self.on_click) self.fig.canvas.mpl_connect('motion_notify_event', self.on_move) self.fig.canvas.mpl_connect('key_press_event', self.on_keyboard) self.fig.canvas.mpl_connect('close_event', self.on_close) if block: self.title_format = 'ix = {index} (Press enter when selection is done)' self.update() if show: plt.show() if block: self.fig.canvas.start_event_loop() return self.fig @property def all_picked_x(self): return np.array([p[2] for p in self.picked]) @property def picked_x(self): pmat = np.vstack(self.picked) x = [None]*self.n_lines for ix in range(self.n_lines): okix = pmat[:,0] == ix x[ix] = pmat[okix, 2] return x @property def picked_ix(self): pmat = np.vstack(self.picked) x_ix = [None]*self.n_lines for ix in range(self.n_lines): okix = pmat[:,0] == ix x_ix[ix] = pmat[okix, 1].astype(int) return x_ix @property def picked_line_ix(self): return np.array([p[0] for p in self.picked]) @property def all_picked_ix(self): return np.array([p[1] for p in self.picked])
Instance variables
var all_picked_ix
-
Expand source code
@property def all_picked_ix(self): return np.array([p[1] for p in self.picked])
var all_picked_x
-
Expand source code
@property def all_picked_x(self): return np.array([p[2] for p in self.picked])
var current_line
-
Expand source code
@property def current_line(self): return self.lines[self.index]
var n_lines
-
Expand source code
@property def n_lines(self): return len(self.lines)
var picked_ix
-
Expand source code
@property def picked_ix(self): pmat = np.vstack(self.picked) x_ix = [None]*self.n_lines for ix in range(self.n_lines): okix = pmat[:,0] == ix x_ix[ix] = pmat[okix, 1].astype(int) return x_ix
var picked_line_ix
-
Expand source code
@property def picked_line_ix(self): return np.array([p[0] for p in self.picked])
var picked_x
-
Expand source code
@property def picked_x(self): pmat = np.vstack(self.picked) x = [None]*self.n_lines for ix in range(self.n_lines): okix = pmat[:,0] == ix x[ix] = pmat[okix, 2] return x
Methods
def get_fig(self, show=True, block=True)
-
Get an interactive figure.
Parameters
show
:bool
, default=True
- whether or not to automatically show figure
block
:bool
, default=True
- whether or not to block before returning to retain interactivity
Returns
fig
:matplotlib.Figure</code> object
- figure object
Expand source code
def get_fig(self, show=True, block=True): ''' Get an interactive figure. Parameters ----------- show : bool, default=True whether or not to automatically show figure block : bool, default=True whether or not to block before returning to retain interactivity Returns ----------- fig : `matplotlib.Figure` object figure object ''' self.ax.legend(frameon=False) self.templine = self.ax.axvline(np.nan, **self.templine_settings) self.templine.set_color(self.current_line.get_color()) self.fig.canvas.mpl_connect('scroll_event', self.on_scroll) self.fig.canvas.mpl_connect('button_press_event', self.on_click) self.fig.canvas.mpl_connect('motion_notify_event', self.on_move) self.fig.canvas.mpl_connect('key_press_event', self.on_keyboard) self.fig.canvas.mpl_connect('close_event', self.on_close) if block: self.title_format = 'ix = {index} (Press enter when selection is done)' self.update() if show: plt.show() if block: self.fig.canvas.start_event_loop() return self.fig
def on_click(self, event)
-
Expand source code
def on_click(self, event): if event.inaxes and event.button is MouseButton.LEFT and self.fig.canvas.manager.toolbar.mode.value == '': color = self.current_line.get_color() ix_sel = np.argmin(np.abs(event.xdata - self.x)) self.picked.append([self.index, ix_sel, self.x[ix_sel]]) self.picked_lines.append(self.ax.axvline(self.x[ix_sel], color=color, **self.picked_settings)) elif event.inaxes and event.button is MouseButton.RIGHT: ix_sel = np.argmin(np.abs(event.xdata - self.x)) picked_mat = np.vstack(self.picked) valid_x_ix = np.where((picked_mat[:,0] == self.index))[0] if len(valid_x_ix)>0: row_ix = np.argmin(np.abs(ix_sel-picked_mat[valid_x_ix, 1])) ix_remove = valid_x_ix[row_ix] self.picked.pop(ix_remove) self.picked_lines[ix_remove].remove() self.picked_lines.pop(ix_remove) self.fig.canvas.draw()
def on_close(self, event)
-
Expand source code
def on_close(self, event): self.fig.canvas.stop_event_loop()
def on_keyboard(self, event)
-
Expand source code
def on_keyboard(self, event): if event.key == 'enter': self.title_format = 'ix = {index}' self.update() self.fig.canvas.stop_event_loop() # Prepare for printing self.templine.set_xdata([np.nan]) for ix,line in enumerate(self.lines): line.set(**self.deactive_settings) line.set(alpha=1.0)
def on_move(self, event)
-
Expand source code
def on_move(self, event): if event.inaxes: ix_sel = np.argmin(np.abs(event.xdata-self.x)) self.templine.set_xdata([self.x[ix_sel]]) self.fig.canvas.draw()
def on_scroll(self, event)
-
Expand source code
def on_scroll(self, event): increment = 1 if event.button == 'up' else -1 self.index = np.clip(self.index + increment, 0, self.n_lines-1) self.x = self.lines[self.index].get_xdata() self.templine.set_color(self.current_line.get_color()) self.update()
def update(self)
-
Expand source code
def update(self): for ix,line in enumerate(self.lines): if ix==self.index: line.set(**self.active_settings) else: line.set(**self.deactive_settings) self.fig.canvas.draw()
class StabPlotter (lambd, orders, phi=None, freq_unit='rad/s', damped_freq=False, psd_freq=None, psd_y=None, log_psd_scale=True, pole_settings=None, selected_pole_settings=None, hover_pole_settings=None, ax=None, num=None, sort_by='undamped', annotate_hover=False, psd_color='gray')
-
Class to initialize interactive stabilization plot.
Example
Assuming we have a data matrix
data
sampled atfs=128.0
. The data is first processed using the covssi (can also be filtered using thefind_stable_poles
function):fs = 128.0 i = 30 orders_input = np.arange(2, 100, 2) lambd, phi, orders = koma.oma.covssi(data, fs, i, orders_input)
The stabilization plot is generated like this:
stab_plotter = koma.plot.StabPlotter(lambd, orders, phi=phi, freq_unit='hz', damped_freq=False, annotate_hover=True) fig = stab_plotter.get_fig()
The following code extracts the data from the plotter object as variables:
phi_sel, fn_sel, xi_sel = stab_plotter.phi, stab_plotter.fn, stab_plotter.xi '
Parameters
lambd
:double
- array with complex-valued eigenvalues
orders
:int
- corresponding order for each pole in
lambd
phi
:double
, optional- matrix where each column is complex-valued eigenvector corresponding to lambd
freq_unit
:str
, default='rad/s'
- what frequency unit to use; 'Hz' or 'rad/s'
damped_freq
:False
, optional- whether or not to use damped frequency (or period) values in plot (False enforces undamped freqs)
psd_freq
:double
, optional- frequency values of plot to overlay, typically spectrum of data
psd_y
:double
, optional- function values of plot to overlay, typically spectrum of data
log_psd_scale
:boolean
, default=True
- whether or not to plot the overlaid PSD using a logarithmic y-scale
pole_settings
:dict
- dictionary with settings to pass to the plot settings of the poles
selected_pole_settings
:dict
- dictionary with settings to pass to the plot settings of the selected poles
hover_pole_settings
:dict
- dictionary with settings to pass to the plot settings of the pole currently being hovered
ax
:matplotlib.Axis</code> object
- axis to place plot in; if not given, a new axis in the figure specified will be created
num
:int
, optional- figure number used; only used if
ax
= None sort_by
:str
, default='undamped'
- what quantity to sort output by; either 'undamped', 'damped' or None
psd_color
:str
, default='gray'
- color to use for PSD plot overlayed
Returns
fig
:obj
- plotly figure object
Expand source code
class StabPlotter: ''' Class to initialize interactive stabilization plot. Example ----------- Assuming we have a data matrix `data` sampled at `fs=128.0`. The data is first processed using the covssi (can also be filtered using the `find_stable_poles` function): fs = 128.0 i = 30 orders_input = np.arange(2, 100, 2) lambd, phi, orders = koma.oma.covssi(data, fs, i, orders_input) The stabilization plot is generated like this: stab_plotter = koma.plot.StabPlotter(lambd, orders, phi=phi, freq_unit='hz', damped_freq=False, annotate_hover=True) fig = stab_plotter.get_fig() The following code extracts the data from the plotter object as variables: phi_sel, fn_sel, xi_sel = stab_plotter.phi, stab_plotter.fn, stab_plotter.xi ' ''' def __init__(self, lambd, orders, phi=None, freq_unit='rad/s', damped_freq=False, psd_freq=None, psd_y=None, log_psd_scale=True, pole_settings=None, selected_pole_settings=None, hover_pole_settings=None, ax=None, num=None, sort_by='undamped', annotate_hover=False, psd_color='gray'): ''' Parameters ------------ lambd : double array with complex-valued eigenvalues orders : int corresponding order for each pole in `lambd` phi : double, optional matrix where each column is complex-valued eigenvector corresponding to lambd freq_unit : str, default='rad/s' what frequency unit to use; 'Hz' or 'rad/s' damped_freq : False, optional whether or not to use damped frequency (or period) values in plot (False enforces undamped freqs) psd_freq : double, optional frequency values of plot to overlay, typically spectrum of data psd_y : double, optional function values of plot to overlay, typically spectrum of data log_psd_scale: boolean, default=True whether or not to plot the overlaid PSD using a logarithmic y-scale pole_settings : dict dictionary with settings to pass to the plot settings of the poles selected_pole_settings : dict dictionary with settings to pass to the plot settings of the selected poles hover_pole_settings : dict dictionary with settings to pass to the plot settings of the pole currently being hovered ax : `matplotlib.Axis` object axis to place plot in; if not given, a new axis in the figure specified will be created num : int, optional figure number used; only used if `ax` = None sort_by : str, default='undamped' what quantity to sort output by; either 'undamped', 'damped' or None psd_color : str, default='gray' color to use for PSD plot overlayed Returns --------------------------- fig : obj plotly figure object ''' if pole_settings is None: pole_settings = {} if selected_pole_settings is None: selected_pole_settings = {} if hover_pole_settings is None: hover_pole_settings = {} self._lambd = lambd self._orders = orders self._phi = phi self.sort_by = sort_by self.damped_freq = damped_freq # Make list if psd_y is not None and not isinstance(psd_y, list): psd_y = [psd_y] psd_freq = [psd_freq] self.psd_color = psd_color self.psd_freq = psd_freq self.psd_y = psd_y self.log_psd_scale = log_psd_scale self.pole_settings = dict(linestyle='none', marker='.', color='k') | pole_settings self.selected_pole_settings = dict(linestyle='none', marker='o', color='r') | selected_pole_settings self.hover_pole_settings = dict(linestyle='none', marker='o', color='r', alpha=0.3) | hover_pole_settings self.annotate_hover = annotate_hover self._picked = [] self._hoverpos = [np.nan, np.nan] self._hoverix = None self._hoverdot = None self._annotation = None self.picked_dots = [] if damped_freq: dampedornot = 'd' self._f = np.abs(np.imag(lambd)) else: dampedornot = 'n' self._f = np.abs(lambd) if freq_unit.lower() == 'hz': self._f = self._f/2/np.pi self.freq_name = fr'$f_{dampedornot}$' self.freq_unit = 'Hz' else: self.freq_name = fr'$\omega_{dampedornot}$' self.freq_unit = 'rad/s' if ax is None: plt.figure(num).clf() self.fig, self.ax = plt.subplots(num=num, figsize=(18,7)) else: plt.sca(ax) self.ax = ax self.fig = plt.gcf() @property def hoverpos(self): return self._hoverpos @hoverpos.setter def hoverpos(self, ix): self._hoverix = ix if self._hoverix is None: x = np.nan y = np.nan text = '' self._annotation.set_visible(False) else: x = self._f[ix] y = self._orders[ix] text = (f'ix = {self._hoverix}\n' + f'n = {self._orders[ix]}\n' + fr'{self.freq_name} = {self._hoverpos[0]:.2f} {self.freq_unit}' + '\n' + fr'$\xi$ = {self.get_xi(ix)*100:.2f}%') if self._phi is not None: this_mpc = mpc(self._phi[:,ix:ix+1])[0] text = text + f'\nMPC={this_mpc*100:.1f}%' self._hoverpos = x,y self._hoverdot.set_xdata([x]) self._hoverdot.set_ydata([y]) if self.annotate_hover: self._annotation.offsetbox.set(text=text) self._annotation.xy = self._hoverpos @property def picked(self): #sorted picked if self.sort_by is None: ix = None elif self.sort_by == 'undamped': ix = np.argsort(np.abs(self._lambd[self._picked])) elif self.sort_by == 'damped': ix = np.argsort(np.abs(np.imag(self._lambd[self._picked]))) return np.array(self._picked)[ix] @property def ix(self): #alias return self.picked # Picked orders @property def n_picked(self): if len(self.picked)>0: return self._orders[self.picked] else: return np.empty([0]) # Eigenvalues and eigenvectors @property def lambd(self): if len(self.picked)>0: return self._lambd[self.picked] else: return np.empty([0]) @property def phi(self): if len(self.picked)>0: return self._phi[:, self.picked] else: return np.empty([0]) # Damped natural freqs @property def wd(self): return np.abs(np.imag(self.lambd)) @property def omegad(self): return self.wd @property def fd(self): return self.wd/2/np.pi # Undamped natural freqs @property def wn(self): return np.abs(self.lambd) @property def omegan(self): return self.wn @property def fn(self): return self.wn/2/np.pi # Damping @property def xi(self): return -np.real(self.lambd)/np.abs(self.lambd) def get_xi(self, ix=None): xi = -np.real(self._lambd[ix])/np.abs(self._lambd[ix]) return xi def get_df(self, pars=['ix', 'n_picked', 'wn', 'xi']): ''' Get pandas dataframe with results. ''' df = pd.DataFrame(data=np.vstack([getattr(self, par) for par in pars]).T, columns=pars) if 'n_picked' in df: df['n_picked'] = df['n_picked'].astype(int) if 'picked' in df: df['picked'] = df['picked'].astype(int) if 'ix' in df: df['ix'] = df['ix'].astype(int) return df def get_ix(self, event): dist = (event.xdata - self._f)**2 + (event.ydata-self._orders)**2 ix_sel = np.argmin(dist) return ix_sel def get_fig(self, show=True, block=True): if self.psd_y is not None: ax2 = self.ax.twinx() for ix, (psd, fi) in enumerate(zip(self.psd_y, self.psd_freq)): ax2.plot(fi, psd, self.psd_color) if self.log_psd_scale: ax2.set_yscale('log') self.ax.plot(self._f, self._orders, **self.pole_settings) self.ax.set_xlabel(f'{self.freq_name} [{self.freq_unit}]') self.ax.set_ylabel('Order, $n$') self._hoverdot = self.ax.plot([np.nan], [np.nan], **self.hover_pole_settings)[0] if self.annotate_hover: offsetbox = TextArea('') self._annotation = AnnotationBbox(offsetbox, [np.nan, np.nan], xybox=(80, 0), xycoords='data', boxcoords="offset points", arrowprops=dict(arrowstyle="->"), pad=0.3, bboxprops=dict(alpha=0.85)) self.ax.add_artist(self._annotation) if self.psd_y is not None: self.ax.set_zorder(ax2.get_zorder() + 1) self.ax.patch.set_visible(False) self.fig.canvas.mpl_connect('button_press_event', self.on_click) self.fig.canvas.mpl_connect('motion_notify_event', self.on_move) self.fig.canvas.mpl_connect('key_press_event', self.on_keyboard) self.fig.canvas.mpl_connect('close_event', self.on_close) if block: self.title_format = '(Press enter when selection is done)' self.update() if show: plt.show() if block: self.fig.canvas.start_event_loop() return self.fig # Interaction methods def on_click(self, event): if event.inaxes: ix_sel = self.get_ix(event) if (event.button is MouseButton.LEFT and self.fig.canvas.manager.toolbar.mode.value == ''): self._picked.append(ix_sel) self.picked_dots.append( self.ax.plot(self._f[ix_sel], self._orders[ix_sel], **self.selected_pole_settings)[0] ) elif event.button is MouseButton.RIGHT: if ix_sel in self._picked: ix_remove = self._picked.index(ix_sel) self._picked.pop(ix_remove) self.picked_dots[ix_remove].remove() self.picked_dots.pop(ix_remove) self.fig.canvas.draw() def on_close(self, event): self.fig.canvas.stop_event_loop() def on_keyboard(self, event): if event.key == 'enter': # Prepare for printing self.hoverpos = None self.update() self.fig.canvas.stop_event_loop() def on_move(self, event): if event.inaxes: ix_hover = self.get_ix(event) self.hoverpos = ix_hover self.fig.canvas.draw() def update(self): self.fig.canvas.draw()
Instance variables
var fd
-
Expand source code
@property def fd(self): return self.wd/2/np.pi
var fn
-
Expand source code
@property def fn(self): return self.wn/2/np.pi
var hoverpos
-
Expand source code
@property def hoverpos(self): return self._hoverpos
var ix
-
Expand source code
@property def ix(self): #alias return self.picked
var lambd
-
Expand source code
@property def lambd(self): if len(self.picked)>0: return self._lambd[self.picked] else: return np.empty([0])
var n_picked
-
Expand source code
@property def n_picked(self): if len(self.picked)>0: return self._orders[self.picked] else: return np.empty([0])
var omegad
-
Expand source code
@property def omegad(self): return self.wd
var omegan
-
Expand source code
@property def omegan(self): return self.wn
var phi
-
Expand source code
@property def phi(self): if len(self.picked)>0: return self._phi[:, self.picked] else: return np.empty([0])
var picked
-
Expand source code
@property def picked(self): #sorted picked if self.sort_by is None: ix = None elif self.sort_by == 'undamped': ix = np.argsort(np.abs(self._lambd[self._picked])) elif self.sort_by == 'damped': ix = np.argsort(np.abs(np.imag(self._lambd[self._picked]))) return np.array(self._picked)[ix]
var wd
-
Expand source code
@property def wd(self): return np.abs(np.imag(self.lambd))
var wn
-
Expand source code
@property def wn(self): return np.abs(self.lambd)
var xi
-
Expand source code
@property def xi(self): return -np.real(self.lambd)/np.abs(self.lambd)
Methods
def get_df(self, pars=['ix', 'n_picked', 'wn', 'xi'])
-
Get pandas dataframe with results.
Expand source code
def get_df(self, pars=['ix', 'n_picked', 'wn', 'xi']): ''' Get pandas dataframe with results. ''' df = pd.DataFrame(data=np.vstack([getattr(self, par) for par in pars]).T, columns=pars) if 'n_picked' in df: df['n_picked'] = df['n_picked'].astype(int) if 'picked' in df: df['picked'] = df['picked'].astype(int) if 'ix' in df: df['ix'] = df['ix'].astype(int) return df
def get_fig(self, show=True, block=True)
-
Expand source code
def get_fig(self, show=True, block=True): if self.psd_y is not None: ax2 = self.ax.twinx() for ix, (psd, fi) in enumerate(zip(self.psd_y, self.psd_freq)): ax2.plot(fi, psd, self.psd_color) if self.log_psd_scale: ax2.set_yscale('log') self.ax.plot(self._f, self._orders, **self.pole_settings) self.ax.set_xlabel(f'{self.freq_name} [{self.freq_unit}]') self.ax.set_ylabel('Order, $n$') self._hoverdot = self.ax.plot([np.nan], [np.nan], **self.hover_pole_settings)[0] if self.annotate_hover: offsetbox = TextArea('') self._annotation = AnnotationBbox(offsetbox, [np.nan, np.nan], xybox=(80, 0), xycoords='data', boxcoords="offset points", arrowprops=dict(arrowstyle="->"), pad=0.3, bboxprops=dict(alpha=0.85)) self.ax.add_artist(self._annotation) if self.psd_y is not None: self.ax.set_zorder(ax2.get_zorder() + 1) self.ax.patch.set_visible(False) self.fig.canvas.mpl_connect('button_press_event', self.on_click) self.fig.canvas.mpl_connect('motion_notify_event', self.on_move) self.fig.canvas.mpl_connect('key_press_event', self.on_keyboard) self.fig.canvas.mpl_connect('close_event', self.on_close) if block: self.title_format = '(Press enter when selection is done)' self.update() if show: plt.show() if block: self.fig.canvas.start_event_loop() return self.fig
def get_ix(self, event)
-
Expand source code
def get_ix(self, event): dist = (event.xdata - self._f)**2 + (event.ydata-self._orders)**2 ix_sel = np.argmin(dist) return ix_sel
def get_xi(self, ix=None)
-
Expand source code
def get_xi(self, ix=None): xi = -np.real(self._lambd[ix])/np.abs(self._lambd[ix]) return xi
def on_click(self, event)
-
Expand source code
def on_click(self, event): if event.inaxes: ix_sel = self.get_ix(event) if (event.button is MouseButton.LEFT and self.fig.canvas.manager.toolbar.mode.value == ''): self._picked.append(ix_sel) self.picked_dots.append( self.ax.plot(self._f[ix_sel], self._orders[ix_sel], **self.selected_pole_settings)[0] ) elif event.button is MouseButton.RIGHT: if ix_sel in self._picked: ix_remove = self._picked.index(ix_sel) self._picked.pop(ix_remove) self.picked_dots[ix_remove].remove() self.picked_dots.pop(ix_remove) self.fig.canvas.draw()
def on_close(self, event)
-
Expand source code
def on_close(self, event): self.fig.canvas.stop_event_loop()
def on_keyboard(self, event)
-
Expand source code
def on_keyboard(self, event): if event.key == 'enter': # Prepare for printing self.hoverpos = None self.update() self.fig.canvas.stop_event_loop()
def on_move(self, event)
-
Expand source code
def on_move(self, event): if event.inaxes: ix_hover = self.get_ix(event) self.hoverpos = ix_hover self.fig.canvas.draw()
def update(self)
-
Expand source code
def update(self): self.fig.canvas.draw()