Tutorial 2: Heatmap plotter#
Corresponding file for this tutorial: TMSiHelpers/heatmap_plotter_helper.py
The second example on plotter customization is the heatmap plotter. This tutorial focusses on the data acquisition part and providing the data to the heatmap; not on making the heatmap itself.
As the type of plotter is defined in the PlotterHelper-class, we have to make a new class named HeatmapPlotterHelper
.
We will derive this class from the FilteredSignalPlotterHelper
, as we have to filter the data to remove DC offsets and
signal drift before transferring Root Mean Square (RMS) values to a heatmap. Again, inheritance is used to base the new class on the parent class.
The __init__()
method of the FilteredSignalPlotterHelper
can be reused, where the SignalPlotter
should be replaced by the HeatmapPlotter
.
from .filtered_signal_plotter_helper import FilteredSignalPlotterHelper, FilteredConsumerThread
class HeatmapPlotterHelper(FilteredSignalPlotterHelper):
def __init__(self, device, grid_type = None, is_head_layout = False, hpf = 5, lpf = 0, order = 1):
# call super of SignalAcquisitionHelper, initializing acquisition details
super(SignalPlotterHelper, self).__init__(device = device, monitor_class = Monitor, consumer_thread_class = FilteredConsumerThread )
self.main_plotter = HeatmapPlotter(device_type=device.get_device_type(), is_headcap=is_head_layout)
The FilteredSignalPlotterHelper
and SignalPlotterHelper
can be checked to see what changes are required to go from a signal plotter
to a heatmap. Two changes are required:
The callback function, which provides data to the plotter.
The initialization where, instead of the different controls of the plotted channels, the locations of the electrodes have to be defined.
In the initialization a window length, over which the RMS is computed, needs to be defined. In addition, the initialization of the heatmap has to be done here, which consists of setting the positions of the different electrodes. The locations of the different electrodes depend on the selected grid or headcap. Those locations are provided in TMSiFrontend. The ordering of the channels differs between grids and can be read from the HD-grid configuration file (located in TMSiSDK/tmsi_resources). Channel locations, channels displayed in the heatmap and the reordering (if applicable) method have to be provided to the heatmap plotter to obtain the right positions and channel names.
if hasattr(self, 'conversion_list'):
self.main_plotter.set_electrode_position(channels = original_channels, coordinates = coordinates, reordered_indices = self.conversion_list.tolist())
else:
self.main_plotter.set_electrode_position(channels = original_channels, coordinates = coordinates)
The callback function is the function that provides data to the plotter. The callback is called with a callback object. This object consists of a buffer object which consists of two parts. The object has a dataset that contains the data, and a pointer_buffer which is the pointer that controls where new data is added. The newest data is the data just before the pointer_buffer. For the heatmap, we should calculate the RMS-values over a specific time window, the calculation of which is shown in the snippet below. Next, the RMS is calculated and the data of the channels present in the heatmap is sent to the plotter.
def callback(self, callback_object):
response = callback_object["buffer"]
# The function that provides the plotter from data
pointer = response.pointer_buffer
# Wait for data to come in
if response.dataset is None:
return
# Get data in time window
if len(response.dataset[0]) <= self.window_length:
data = response.dataset
elif pointer < self.window_length:
data = np.hstack((response.dataset[:,-(self.window_length-pointer):], response.dataset[:,:pointer]))
else:
data = response.dataset[:,(pointer-self.window_length):pointer]
# Calulate rms
rms_data = np.sqrt(np.mean(data**2, axis = 1))
self.main_plotter.update_chart(rms_data[self.heatmap_channels])