Tutorial 3: Differential plotter

Tutorial 3: Differential plotter#

Corresponding file for this tutorial: TMSiHelpers/differential_signal_plotter_helper.py

The next example outlines a differential signal viewer for HD-EMG purposes. The viewer displays the difference between successively separated grid electrodes (along the row direction).

To be able to view both the unprocessed data and the single differentials data, two windows are initialized from the Gui. Here, the first window (with unprocessed data) is defined to be the main window. The class’ close event is connected to this window. In the plotter helper, a main plotter is defined using the FilteredSignalPlotterHelper and a secondary plotter is created using the DifferentialSignalPlotterHelper. Both plotters are initialized similarly and receive the same data.

from .filtered_signal_plotter_helper import FilteredSignalPlotterHelper

class DifferentialSignalPlotterHelper(FilteredSignalPlotterHelper):
    def __init__(self, device, grid_type=None, hpf=0, lpf=0, order=1):
        super().__init__(device=device, grid_type=grid_type, hpf=hpf, lpf=lpf, order=order)
        self.plotter2 = SignalPlotter()

The single differentials can be calculated as a matrix multiplication between a pre-defined ‘single differential matrix’ and the data. The single differential matrix and the differential names are determined based on channel names. See the code snippet for details on the implementation.

    def get_single_differential_matrix(self):
        RC_order, RC_matrix = self._get_grid_order()
        
        # Initialize channel names
        ch_names_diff = ['R1C1 - R1C2']
        skip = 0
        # Single differential matrix
        SD_matrix = np.zeros((np.shape(RC_matrix)[0]*(np.shape(RC_matrix)[1]-1), len(self.device.get_device_active_channels())))
        n_C = np.shape(RC_matrix)[1]-1
        for r in range(0, max(RC_order, key=itemgetter(0))[0]):
            for c in range(0, max(RC_order, key=itemgetter(1))[1]-1):
                # If channel RC and R(C+1) are available, 
                # RC - R(C+1) 
                # SD = np.matmul(SD_matrix. samples) 
                if not np.isnan(RC_matrix[r,c]) and not np.isnan(RC_matrix[r,c+1]):
                    SD_matrix[n_C*r + c - skip, int(RC_matrix[r,c])] = 1
                    SD_matrix[n_C*r + c -skip, int(RC_matrix[r,c+1])] = -1
                    # Create channel name
                    ch_names_diff = np.vstack((ch_names_diff, 'R'+str(r+1)+'C'+str(c+1) + ' - ' + 'R'+str(r+1)+'C'+str(c+2)))
                else:
                    # Delete row when not available
                    SD_matrix = np.delete(SD_matrix, n_C*r + c-skip, 0)
                    skip = skip + 1 
        # Remove initial channel name
        ch_names_diff = ch_names_diff[1:,:]
        return SD_matrix, ch_names_diff

Once the channel names are known, the plotters can be constructed. The controls for both plotters are then based on each plotter’s specific channel components. In order to initialize the channel components, a list of channels needs to be passed, not just channel names. Therefore, the type of the channels’ instance is needed. A second instance should be created, where the new channel names should be appended to the channel list:

        SignalType = self.channels[0].__class__
        # Generate channels list with differential channel names
        differential_signals = []
        for i in ch_names_diff:
            sig = SignalType()
            sig.set_channel_name(alternative_channel_name= i)
            differential_signals.append(sig)
        # Initialize channel components
        self.plotter2.initialize_channels_components(differential_signals)

In order to be able to see the action potentials travel across the grid in the single differential mode, a short time range should be used. Therefore, the time span of the differential plotter is set to 0.2s and the refresh rate is limited to 1 Hz, instead of the 10 Hz update frequency of the main plotter.

    def initialize(self):
        super().initialize()
        # Set time span of secondary window to 0.2 s
        self.plotter2.chart.set_time_min_max(0, 0.2)
        self.differential_time_span = np.arange(0,0.2,1.0/self.sampling_frequency)
        # Set refresh rate to update every 10 times (=1Hz) to have readible signals
        self.plotter2_refresh_rate = 10
        self.main_plotter_refresh_counter = 0

Rather than updating the plotted data in a cycling update, the newest data is now appended to the right side of the plot. As this differs for the signal plotter, a change needs to be made on how the processed data should be passed to the window. After updating this, the single differential calculation can be made and data can be provided to the plot.

        # Update plotter2 depending on refresh rate
        if self.main_plotter_refresh_counter % self.plotter2_refresh_rate == 0:
            if size_dataset < response.size_buffer:
                data_to_return = np.empty((n_channels, response.size_buffer))
                data_to_return[:] = np.nan
                data_to_return[:,response.size_buffer - pointer_data_to_plot:] = data_to_plot[:,:pointer_data_to_plot]
            else:
                # Flip data such that the newest data is on the end
                data_to_return = np.ones_like(data_to_plot)
                data_to_return[:,size_dataset - pointer_data_to_plot:] = data_to_plot[:,:pointer_data_to_plot]
                data_to_return[:,:size_dataset - pointer_data_to_plot] = data_to_plot[:,pointer_data_to_plot:]
            
            # Calculate differential data
            SD_data = np.matmul(self.SD_matrix, data_to_return[:,-len(self.differential_time_span):])
            # Send data to plotter and update chart
            self.plotter2.update_chart(data_to_plot = SD_data, time_span=self.differential_time_span)
        self.main_plotter_refresh_counter += 1