"""
This module is for EDA through interactive visualisation of the core structures:
``TensorCPD``, ``TensorTKD`` etc.
.. note::
All functionality of this module is designed for the Jupyter Lab.
Thus, it is not guaranteed to work correctly within Jupyter Notebook.
"""
import numpy as np
from collections import OrderedDict
from scipy import signal # for generating testing data and can be removed
from hottbox.core import TensorCPD, TensorTKD # for type hinting
try:
import matplotlib.pyplot as plt
from ipywidgets import IntSlider, VBox, HBox, Dropdown, Output
from IPython.display import display, clear_output
except ModuleNotFoundError as error:
extra_required = ["matplotlib",
"ipywidgets",
"IPython"
]
print("="*50)
print("\n\nThis is experimental module that depends on additional libraries "
"that are not included in the list of main dependencies. "
"Please, make sure that you have install {} if you want to "
"try out this module.\n\n".format(extra_required))
print("=" * 50)
[docs]def gen_test_data(plot=False):
""" Generate factor matrices which components will be easy to differentiate from one another
Parameters
----------
plot : bool
Returns
-------
fmat : list[np.ndarray]
core_values : np.ndarray
"""
t_A = np.linspace(0, 1, 500, endpoint=False).reshape(-1, 1)
t_B = np.linspace(0, 2, 10, endpoint=False).reshape(-1, 1)
t_C = np.linspace(-1, 1, 2 * 100, endpoint=False).reshape(-1, 1)
w_A = np.array([1, 2, 5]).reshape(-1, 1)
w_B = np.roll(w_A, 1)
w_C = np.array([0.3, 2, 0.7]).reshape(-1, 1)
A = np.sin (2 * np.pi * t_A * w_A.T)
B = signal.square (2 * np.pi * t_B * w_B.T)
C, _, _ = signal.gausspulse(t_C * w_C.T, fc=5, retquad=True, retenv=True)
fmat = [A, B, C]
core_values = np.array([1]*A.shape[1])
if plot:
for mode, factor in enumerate(fmat):
print("Mode-{} factor matrix shape = {}".format(mode, factor.shape))
fig, axis = plt.subplots(nrows=3,
ncols=1,
figsize=(8, 8)
)
axis[0].plot(t_A, A)
axis[0].set_title("Factor matrix A")
axis[1].plot(t_B, B)
axis[1].set_title("Factor matrix B")
axis[2].plot(t_C, C)
axis[2].set_title("Factor matrix C")
plt.tight_layout()
return fmat, core_values
[docs]def gen_test_tensor_cpd():
""" Generate ``TensorCPD`` object for testing purposes
Returns
-------
TensorCPD
"""
return TensorCPD(*gen_test_data())
def _line_plot(ax, data):
""" Base function for representing factor vector as line plot
Parameters
----------
ax : matplotlib.axes.Axes
Axis object which is used to illustrate `data`
data : np.ndarray
Array of data to be plotted. Shape of such array is ``(N, 1)``
"""
ax.plot(data)
def _bar_plot(ax, data):
""" Base function for representing factor vector as bar plot
Parameters
----------
ax : matplotlib.axes.Axes
Axis object which is used to illustrate `data`
data : np.ndarray
Array of data to be plotted. Shape of such array is ``(N, 1)``
"""
ax.bar(x=range(data.shape[0]), height=data)
_DEFAULT_1D_PLOTS = OrderedDict([("line", _line_plot),
("bar", _bar_plot)
])
[docs]class BaseComponentPlot(object):
""" Dashboard for interactive visualisation of the factor vectors
Attributes
----------
available_plots : dict[str, callable]
Ordered dictionary with all available plot functions for the factor vectors
out : Output
Widget used as a context manager to display output.
sliders : list[IntSlider]
List of slider widgets each of which allows to select factor vector to be plotted.
dropdown : list[Dropdown]
List of dropdown widgets each of which allows to select type of plot
for the factor vector selected by the corresponding slider.
dashboard : VBox
Dashboard with visualisations of the factor vectors and widgets for selection
of how and what is going to be displayed.
"""
def __init__(self, tensor_rep):
""" Constructor of the interactive dashboard
Parameters
----------
tensor_rep : {TensorCPD, TensorTKD}
"""
self.tensor_rep = tensor_rep
self.available_plots = _DEFAULT_1D_PLOTS.copy()
self.out = Output()
self.sliders = self._create_fmat_sliders()
self.dropdown = self._create_fmat_dropdown()
self.dashboard = VBox([self.out,
HBox(self.sliders),
HBox(self.dropdown)
])
self._start_interacting()
def _create_fmat_sliders(self):
""" Create slider widgets for selecting factor vectors
Returns
-------
slider_list : list[IntSlider]
List of slider widgets for selecting factor vector to be plotted
Notes
-----
This is a dummy slider
"""
default_params = dict(min=0,
max=0,
value=0,
continuous_update=False
)
slider_list = [IntSlider(**default_params)]
return slider_list
def _create_fmat_dropdown(self):
""" Create dropdown widgets for selecting plotting functions
Returns
-------
dropdown_list : list[Dropdown]
List of dropdown widgets for selecting type of plot for the factor matrix
"""
options_list = list(self.available_plots.keys())
default_value = options_list[0] # default_value = "line"
dropdown_default_params = dict(options=options_list,
value=default_value,
description='Plot type:',
disabled=False
)
dropdown_list = [Dropdown(**dropdown_default_params) for _ in self.tensor_rep.fmat]
return dropdown_list
def _start_interacting(self):
""" Display the dashboard and setup callbacks for its widgets """
[slider.observe(self._general_callback, names="value") for slider in self.sliders]
[dropdown.observe(self._general_callback, names="value") for dropdown in self.dropdown]
display(self.dashboard)
def _general_callback(self, change):
""" A callable that is called when values of slider or dropdown widgets have been changed
Notes
-----
The signature of this method should not be changed
"""
with self.out:
fig = self._plot_factor_vectors()
display(fig)
clear_output(wait=True)
def _plot_factor_vectors(self):
""" Plot selected factor vectors using selected plot functions
Values of slider widgets are used to determine which factor vectors to plot.
Values of dropdown widgets are used to determine which plot functions to use.
Returns
-------
fig : matplotlib.figure.Figure
"""
dropdown_values = [dropdown.value for dropdown in self.dropdown]
factor_vectors_list = [slider.value for slider in self.sliders]
if isinstance(self.tensor_rep, TensorCPD):
# Since one slider is used to select factor vectors across all modes
factor_vectors_list *= self.tensor_rep.order
n_rows = 1
n_cols = self.tensor_rep.order
axis_width = 4
axis_height = 4
fig, axis = plt.subplots(nrows=n_rows,
ncols=n_cols,
figsize=(n_cols * axis_width, n_rows * axis_height)
)
for i, fmat in enumerate(self.tensor_rep.fmat):
factor = factor_vectors_list[i]
plot_function = self.available_plots[dropdown_values[i]]
plot_function(ax=axis[i],
data=fmat[:, factor])
axis[i].set_title("Factor matrix: {}".format(self.tensor_rep.mode_names[i]))
plt.tight_layout()
return fig
[docs] def extend_available_plots(self, custom_plots, modes=()):
""" Add custom plot functions available for representing factor vectors
This method can be used either for adding new ways of plotting of the
factor vectors or changing ones that have already been defined.
Despite of chosen option there are two main steps. First the internal dictionary
with plots (`self.available_plots`) is updated, then dropdown menus.
Parameters
----------
custom_plots : dict[str, callable]
Dictionary with plot functions
Keys will be displayed in the dropdown menu.
Values will be used as plotting functions for factor vector
when the corresponding option from the dropdown menu is selected.
modes : list[int]
List of modes for which keys of `custom_plots` will be available in the dropdown.
If not specified then they will be available for all modes.
Notes
-----
1) If key from `custom_plots` is already in ``self.available_plots.keys()``,
then the corresponding value will be updated anyway.
2) When the options of dropdown menu are updated, then index of that dropdown
menu is also getting changed. Looks like it is always set to zero which could
change the value that is selected in the dropdown menu.
3) Signature of plotting functions should contain two variables: `ax` and `data`
::
def my_line_plot(ax, data):
ax.plot(data, 'r+')
"""
self.available_plots.update(custom_plots)
if not modes:
dropdown_update_list = [j for j in range(len(self.dropdown))]
else:
dropdown_update_list = modes
# Update dropdown menus, this will also reset their index
new_options = list(custom_plots.keys())
for j in dropdown_update_list:
old_options = [*self.dropdown[j].options]
unique_options = {*old_options, *new_options}
updated_options = [option for option in self.available_plots.keys() if option in unique_options]
self.dropdown[j].options = tuple(updated_options)
[docs]class ComponentPlotCPD(BaseComponentPlot):
def __init__(self, tensor_rep):
""" Constructor of the interactive dashboard for the `TensorCPD` objects
Parameters
----------
tensor_rep : TensorCPD
Tensor represented in the kruskal form.
Notes
-----
1) There is only one slider for selecting which factor vectors to plot.
This is due to the nature of the kruskal representation, i.e. one to
one relation between the factor vectors from factor matrices of different modes.
"""
super(ComponentPlotCPD, self).__init__(tensor_rep=tensor_rep)
def _create_fmat_sliders(self):
""" Create slider widgets for selecting factor vectors
Returns
-------
slider_list : list[IntSlider]
List of slider widgets for selecting factor vector to be plotted
Notes
-----
Only one slider is required for selecting which factor vectors
will be plotted, i.e. ``len(slider_list) == 1``
"""
default_params = dict(min=0,
max=self.tensor_rep.fmat[0].shape[1] - 1,
value=0,
continuous_update=False
)
slider_list = [IntSlider(**default_params)]
return slider_list
[docs]class ComponentPlotTKD(BaseComponentPlot):
def __init__(self, tensor_rep):
""" Constructor of the interactive dashboard for the `TensorTKD` objects
Parameters
----------
tensor_rep : TensorTKD
Tensor represented in the tucker form.
Notes
-----
1) There is one slider per mode for selecting which factor vector to plot.
This is due to the nature of the tucker representation, i.e. each factor
from one mode vector is related all factor vectors from all other modes.
"""
super(ComponentPlotTKD, self).__init__(tensor_rep=tensor_rep)
def _create_fmat_sliders(self):
""" Create slider widgets for selecting factor vectors
Returns
-------
slider_list : list[IntSlider]
List of slider widgets for selecting factor vector to be plotted
Notes
-----
There is one slider widgets per mode, i.e. ``len(slider_list) == self.tensor_rep.order``
"""
default_params = dict(min=0,
value=0,
continuous_update=False
)
slider_list = [IntSlider(**default_params, max=(fmat.shape[1] - 1)) for fmat in self.tensor_rep.fmat]
return slider_list