AI Model Refactoring Guide for Synaptipy

Introduction

This document provides a step-by-step guide for an AI model to refactor the Synaptipy analysis module. The goal is to unify methods, remove significant code duplication across analysis tabs, and improve the overall workflow efficiency.

Follow each phase in order, as they build upon each other.


Phase 1: Unify Data Selection and Plotting in BaseAnalysisTab

Objective: Move all common data selection (channel, trial) and plotting logic from individual tabs into BaseAnalysisTab. Subclasses should no longer manage this boilerplate.

Step 1.1: Modify BaseAnalysisTab (base.py)

  1. Add UI Element Attributes: In BaseAnalysisTab.__init__, add attributes for the shared UI elements.

self.signal_channel_combobox: Optional[QtWidgets.QComboBox] = None
self.data_source_combobox: Optional[QtWidgets.QComboBox] = None
self._current_plot_data: Optional[Dict[str, Any]] = None
  1. Create a Centralized UI Setup Method: Create a new method _setup_data_selection_ui(self, layout) in BaseAnalysisTab. This method will create the QFormLayout and add the analysis_item_combo, signal_channel_combobox, and data_source_combobox to the provided layout.

  2. Enhance _on_analysis_item_selected: Modify this method to handle populating the signal_channel_combobox and data_source_combobox. This logic should be moved from the individual tabs. It should find the first available channel, determine num_trials and has_average, and populate the source combobox accordingly. After populating, it should automatically trigger _plot_selected_data().

  3. Create Centralized Plotting Method: Create a new method _plot_selected_data in BaseAnalysisTab.

  • This method will be connected to the currentIndexChanged signals of signal_channel_combobox and data_source_combobox.

  • It will be responsible for:

  • Getting the selected channel and data source.

  • Fetching the data (time_vec, data_vec, units, sampling_rate) from the _selected_item_recording.

  • Storing this information in self._current_plot_data.

  • Clearing the plot_widget.

  • Plotting the main data trace.

  • Setting plot labels and title.

  • Calling self.set_data_ranges() for zoom sync.

  • Crucially, at the end, it must call a new abstract method: self._on_data_plotted().

  1. Define Hook Method: Add a new (virtual, not strictly abstract) method to BaseAnalysisTab.

def _on_data_plotted(self):
"""
Hook for subclasses, called after new data has been successfully plotted
by the base class. Subclasses should implement this to add their specific
plot items (e.g., interactive regions) or update parameter ranges.
"""
pass # Subclasses will override this.
  1. Connect Signals: In BaseAnalysisTab, add connections for the new comboboxes to _plot_selected_data.

Step 1.2: Refactor Subclass Tabs (e.g., rmp_tab.py, rin_tab.py)

For each analysis tab (rmp_tab.py, rin_tab.py, spike_tab.py, event_detection_tab.py):

  1. Remove UI Attributes: Delete self.signal_channel_combobox and self.data_source_combobox from the subclass __init__.

  2. Simplify _setup_ui:

  • Remove the code that creates and adds the channel and data source comboboxes.

  • Instead, call the new self._setup_data_selection_ui(layout) from the base class.

  1. Delete Redundant Methods:

  • Delete the _plot_selected_channel_trace or _plot_selected_trace method entirely.

  • Delete the slots that were connected to the combobox signals.

  1. Simplify _update_ui_for_selected_item:

  • This method in the subclass is no longer needed, as the base class now handles the entire flow from item selection to plotting. Delete it.

  1. Implement _on_data_plotted:

  • Create the _on_data_plotted(self) method.

  • Move any logic that needs to happen after plotting into this method. This includes adding LinearRegionItems or ScatterPlotItems to the plot.


Phase 2: Unify Analysis Execution with a Template Method

Objective: Create a single, final method in BaseAnalysisTab that orchestrates the analysis execution, while subclasses provide the specific implementation details.

Step 2.1: Modify BaseAnalysisTab (base.py)

  1. Add a run_analysis_button: Add self.run_analysis_button to the __init__ and a _setup_run_button method. Subclasses can call this to add a standardized “Run Analysis” button.

  2. Create the Template Method: Create a new method _trigger_analysis.

  • This method should not be overridden by subclasses. It will contain the generic workflow: set wait cursor, try...finally block for error handling, and calls to new abstract methods.

# In BaseAnalysisTab
@QtCore.Slot()
def _trigger_analysis(self):
if not self._current_plot_data:
# ... show error ...
return

QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor)
self._last_analysis_result = None
try:
# 1. Gather specific parameters from the subclass UI
params = self._gather_analysis_parameters()

# 2. Execute the core logic, passing in current data
results = self._execute_core_analysis(params, self._current_plot_data)
self._last_analysis_result = results # Store for saving

# 3. Display results in UI
self._display_analysis_results(results)

# 4. Update plot with visualizations
self._plot_analysis_visualizations(results)

self._set_save_button_enabled(True)
except Exception as e:
log.error(f"Analysis failed in {self.get_display_name()}: {e}", exc_info=True)
# ... show error message to user ...
self._set_save_button_enabled(False)
finally:
QtWidgets.QApplication.restoreOverrideCursor()
  1. Define Abstract Helper Methods: Add the following new abstract methods to BaseAnalysisTab using the abc module.

from abc import ABC, abstractmethod

class BaseAnalysisTab(QtWidgets.QWidget, ABC):
# ... existing stuff ...

@abstractmethod
def _gather_analysis_parameters(self) -> Dict[str, Any]:
"""Subclass must implement this to read parameters from its UI widgets."""
pass

@abstractmethod
def _execute_core_analysis(self, params: Dict[str, Any], data: Dict[str, Any]) -> Dict[str, Any]:
"""Subclass must implement this to call its core analysis function."""
pass

@abstractmethod
def _display_analysis_results(self, results: Dict[str, Any]):
"""Subclass must implement this to update its results labels/text edits."""
pass

@abstractmethod
def _plot_analysis_visualizations(self, results: Dict[str, Any]):
"""Subclass must implement this to add markers/lines to the plot."""
pass

Step 2.2: Refactor Subclass Tabs

For each analysis tab:

  1. Remove Old Execution Logic: Delete the old _run_..._analysis method.

  2. Implement Abstract Methods: Implement all four new abstract methods:

  • _gather_analysis_parameters: Read values from self.threshold_edit, spinboxes, etc., and return them in a dictionary.

  • _execute_core_analysis: Take the params dict and the data dict. Call the specific function from core.analysis (e.g., spike_analysis.detect_spikes_threshold). Return the results in a structured dictionary.

  • _display_analysis_results: Take the results dict and format a string to display in self.results_textedit.

  • _plot_analysis_visualizations: Take the results dict and use it to call self.spike_markers_item.setData(...) or self.baseline_line.setValue(...).


Phase 3: Implement Real-Time Parameter Tuning

Objective: Make the UI more interactive by automatically re-running analyses when parameters are changed.

Step 3.1: Add a Debounce Timer to BaseAnalysisTab

  1. In BaseAnalysisTab.__init__, add a QTimer.

self.debounce_timer = QTimer(self)
self.debounce_timer.setSingleShot(True)
self.debounce_timer.timeout.connect(self._trigger_analysis)

Step 3.2: Modify Subclass Tabs

  1. In each subclass’s _connect_signals method, connect the relevant parameter widgets to a new debounce handler.

# In SpikeAnalysisTab._connect_signals
self.threshold_edit.textChanged.connect(self._on_parameter_changed)
self.refractory_edit.textChanged.connect(self._on_parameter_changed)

# In RinAnalysisTab._connect_signals (for interactive mode)
self.manual_delta_i_spinbox.valueChanged.connect(self._on_parameter_changed)
  1. Add the _on_parameter_changed handler to each subclass.

# In SpikeAnalysisTab
def _on_parameter_changed(self):
"""Called when a parameter widget's value changes."""
# Optional: Add a check for interactive mode if desired
# Start or restart the timer. The analysis will run after 250ms of inactivity.
self.debounce_timer.start(250)

This will make the workflow much smoother for tuning parameters. The “Run” button will now primarily be for manual mode in tabs that have it.


Phase 4: Unify Results Management (Future Direction)

Objective: Centralize all saved analysis results into a single, powerful view instead of just a simple list.

This is a larger architectural change and should be tackled after the above refactoring is complete.

  1. Modify ExporterTab: Rename ExporterTab to ResultsTab.

  2. Replace QListWidget: Change the results list from a simple list to a QTableWidget.

  3. Define Table Columns: Create columns for common data points: Analysis Type, Source File, Channel, Trial, Result Value, Units, etc.

  4. Update MainWindow.add_saved_result: Modify this method to take the result dictionary and populate a new row in the ResultsTab’s table instead of adding a string to a list.

  5. Add Functionality: Add buttons to the ResultsTab for Export Selected to CSV, Export All, Clear Results, and Group By....

This phase will provide a much more professional and useful way for users to manage and export their analysis data.