Writing Custom Analysis Plugins for Synaptipy
This guide explains how to add your own analysis function to Synaptipy as a new tab in the Analyser - without modifying any Synaptipy source code. You write a single Python file, drop it in a folder, and your analysis appears in the GUI and batch engine the next time the application starts.
Table of Contents
1. Overview - How the Plugin System Works
Synaptipy has a central AnalysisRegistry - a Python class that maps named
analysis functions to the GUI and batch engine. You register a function by
decorating it with @AnalysisRegistry.register(...). The decorator stores the
function and its metadata (parameter definitions, plot overlays, label, etc.).
At startup, Synaptipy:
Loads all built-in analyses from
src/Synaptipy/core/analysis/.Scans two plugin directories in order:
examples/plugins/inside the Synaptipy installation - shipped example plugins that work out-of-the-box without any extra setup.~/.synaptipy/plugins/- your personal or third-party additions. If a file with the same stem name exists in both directories, the user copy takes precedence and a warning is written to the log.
Builds the Analyser GUI. For every registered analysis that does not already have a hand-coded tab class, a metadata-driven tab is created automatically - complete with parameter widgets, a Run button, a results table, and plot overlays. Your function appears as a new sub-tab.
┌──────────────────────────────────────────────────────────┐
│ ~/.synaptipy/plugins/synaptic_charge.py │
│ │
│ @AnalysisRegistry.register( │
│ name="synaptic_charge", │
│ label="Synaptic Charge (AUC)", │
│ ui_params=[...], │
│ plots=[...] │
│ ) │
│ def run_auc(data, time, sampling_rate, **kwargs): │
│ ... │
│ return {"module_used": "synaptic_charge", │
│ "metrics": {"Charge_pC": 1.23, │
│ "Baseline_pA": -42.0}} │
└──────────────────────────────────────────────────────────┘
│
▼ startup → PluginManager.load_plugins()
┌──────────────────────────────────────────────────────────┐
│ AnalysisRegistry │
│ ├── rmp_analysis (built-in) │
│ ├── spike_detection (built-in) │
│ ├── ... │
│ └── synaptic_charge ← YOUR PLUGIN │
└──────────────────────────────────────────────────────────┘
│
▼ GUI build → auto-generated MetadataDrivenAnalysisTab
┌──────────────────────────────────────────────────────────┐
│ Analyser Tab: ... | Baseline | Spikes | │
│ Synaptic Charge (AUC) ◄─────────── │
│ ┌────────────────────────────────────────────┐ │
│ │ Baseline Start (s): [0.0 ] │ │
│ │ Baseline End (s): [0.05 ] │ │
│ │ Window Start (s): [0.05 ] │ │
│ │ Window End (s): [0.3 ] │ │
│ │ [ ▶ Run Analysis ] │ │
│ │ ────────────────────────────────────── │ │
│ │ Results: Charge = 1.23 pC │ │
│ └────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
You do not need to write any GUI code. The ui_params list generates the
parameter widgets, and the plots list generates the plot overlays - all from
metadata.
2. Quick Start - Your First Plugin in 5 Minutes
Copy the template:
# macOS / Linux cp src/Synaptipy/templates/plugin_template.py ~/.synaptipy/plugins/my_analysis.py # Windows (PowerShell) Copy-Item src\Synaptipy\templates\plugin_template.py ~\.synaptipy\plugins\my_analysis.py
The plugin directory is created automatically the first time Synaptipy runs. If it does not exist yet, create it manually:
macOS / Linux:
mkdir -p ~/.synaptipy/pluginsWindows (PowerShell):
New-Item -ItemType Directory -Force "$HOME\.synaptipy\plugins"
Where is the plugins folder?
Platform
Path
macOS / Linux
~/.synaptipy/plugins/Windows
C:\Users\<YourUsername>\.synaptipy\plugins\On Windows you can open it by typing
%USERPROFILE%\.synaptipy\pluginsin the File Explorer address bar.Open the copied file in any editor.
Rename the function, change the
name=andlabel=, adjust theui_paramsandplotslists, and write your analysis logic.(Re)start Synaptipy. Your analysis appears as a new tab in the Analyser.
No pip install, no editing __init__.py, no rebuilding - just save and restart.
3. The Plugin File - Anatomy of a Custom Analysis
A plugin file has two parts:
3.1 Part 1: Pure Analysis Logic
A regular Python function with explicit typed arguments and a return value. No GUI dependencies (no PySide6, no pyqtgraph).
import numpy as np
def calculate_area_under_curve(
data: np.ndarray,
time: np.ndarray,
sampling_rate: float,
baseline_start: float,
baseline_end: float,
window_start: float,
window_end: float,
) -> dict:
"""Integrate baseline-subtracted trace to obtain synaptic charge (pC)."""
bl0 = int(np.searchsorted(time, baseline_start, side="left"))
bl1 = int(np.searchsorted(time, baseline_end, side="right"))
baseline = float(np.mean(data[bl0:bl1])) if bl1 > bl0 else 0.0
wi0 = int(np.searchsorted(time, window_start, side="left"))
wi1 = int(np.searchsorted(time, window_end, side="right"))
win_t = time[wi0:wi1]
win_d = data[wi0:wi1] - baseline
if win_t.size < 2:
return {"error": "Integration window too narrow"}
charge_pc = float(np.trapezoid(win_d, win_t))
return {
"Charge_pC": round(charge_pc, 6),
"Baseline_pA": round(baseline, 4),
"_int_x": win_t,
"_int_y": data[wi0:wi1],
"_base": baseline,
}
Rules for the logic function:
Takes explicit, typed arguments - no
**kwargs.Returns a typed result object or a plain dict.
Must handle edge cases (empty data, bad windows) gracefully.
Must not import any GUI modules.
3.2 Part 2: Registry Wrapper
A thin wrapper decorated with @AnalysisRegistry.register(...). It extracts
parameters from kwargs, calls your logic function, and returns a dict that
follows the nested output schema.
from Synaptipy.core.analysis.registry import AnalysisRegistry
@AnalysisRegistry.register(
name="synaptic_charge", # unique internal name
label="Synaptic Charge (AUC)", # display name in the tab
ui_params=[...], # parameter widgets (see §4)
plots=[...], # plot overlays (see §5)
)
def run_auc_wrapper(
data: np.ndarray,
time: np.ndarray,
sampling_rate: float,
**kwargs,
) -> dict:
"""Registry wrapper for AUC / synaptic charge analysis."""
return calculate_area_under_curve(
data=data,
time=time,
sampling_rate=sampling_rate,
baseline_start=kwargs.get("baseline_start", 0.0),
baseline_end=kwargs.get("baseline_end", 0.05),
window_start=kwargs.get("window_start", 0.05),
window_end=kwargs.get("window_end", 0.3),
)
The wrapper signature is fixed:
def wrapper(data: np.ndarray, time: np.ndarray, sampling_rate: float, **kwargs) -> Dict[str, Any]
Parameter |
Type |
Description |
|---|---|---|
|
|
1-D voltage/current trace for the selected sweep |
|
|
1-D time array in seconds, same length as |
|
|
Sampling rate in Hz |
|
All |
3.3 Return Dict Conventions
Wrappers must return a Dict[str, Any] using the nested output schema:
{
"module_used": "my_plugin_name", # string identifying the source module
"metrics": { # all scalar results go here
"MyMetric1": 1.0,
"MyMetric2": 42,
},
# optional private keys for plot overlays (hidden from results table):
"_fit_curve": np.array(...),
"_event_indices": [...],
}
The metrics dict drives the results table and batch CSV columns. Any key
in metrics appears as a column header; any value that is a number is
written to the CSV.
Convention |
Behaviour |
|---|---|
|
Identifies the source; used by the batch engine to route results to the correct CSV file. |
Keys inside |
Displayed in the results table and exported to CSV. Use plain, human-readable names. |
Keys starting with |
Hidden from the results table. Use for arrays passed to plot overlays (e.g. |
Key named |
If present, the GUI shows an error message instead of results. |
Numeric values in |
Displayed as-is in the results table. |
|
Displayed as shape summary (e.g. |
|
Displayed as |
4. Defining GUI Parameters (ui_params)
The ui_params list in @AnalysisRegistry.register(...) defines the parameter
widgets that appear in your tab. Each entry is a dict describing one widget.
4.1 float Parameter
Creates a double-precision spin box.
{
"name": "threshold", # kwarg name passed to your wrapper
"label": "Threshold (mV):", # label text shown in the GUI
"type": "float",
"default": -20.0, # initial value (default: 0.0)
"min": -200.0, # minimum allowed (default: -1e9)
"max": 200.0, # maximum allowed (default: 1e9)
"decimals": 2, # decimal places (default: 4)
"step": 0.5, # step increment (optional)
}
4.2 int Parameter
Creates an integer spin box.
{
"name": "min_spikes",
"label": "Min Spikes:",
"type": "int",
"default": 3,
"min": 1,
"max": 10000,
}
4.3 choice / combo Parameter
Creates a drop-down combo box.
{
"name": "direction",
"label": "Detection Direction:",
"type": "choice", # "combo" also works
"choices": ["negative", "positive", "both"],
"default": "negative", # pre-selected option
}
Note: You can use
"options"instead of"choices"- both keys are accepted.
4.4 bool Parameter
Creates a check box.
{
"name": "auto_detect",
"label": "Auto-Detect Baseline",
"type": "bool",
"default": False,
}
4.5 Common Optional Fields
These fields work on any parameter type:
Field |
Type |
Description |
|---|---|---|
|
|
Tooltip text shown on hover. |
|
|
If |
|
|
Conditional visibility - see below. |
4.6 Conditional Visibility (visible_when)
Show or hide a parameter widget based on the current value of another widget.
Example: Show "spike_threshold" only when "event_type" is set to
"Spikes":
ui_params=[
{"name": "event_type", "type": "choice", "choices": ["Spikes", "EPSCs"],
"label": "Event Type:", "default": "Spikes"},
{"name": "spike_threshold", "type": "float", "default": -20.0,
"label": "Spike Threshold (mV):",
"visible_when": {"param": "event_type", "value": "Spikes"}},
{"name": "epsc_threshold", "type": "float", "default": -5.0,
"label": "EPSC Threshold (pA):",
"visible_when": {"param": "event_type", "value": "EPSCs"}},
]
The "param" key names the sibling widget; "value" is the value that makes
this widget visible. When the controlling widget changes, visibility updates
automatically.
There is also a "context" form for clamp-mode-aware parameters:
"visible_when": {"context": "clamp_mode", "value": "current_clamp"}
5. Defining Plot Overlays (plots)
The plots list in @AnalysisRegistry.register(...) defines visual overlays
rendered on the data trace after analysis completes. Each entry is a dict.
5.1 hlines - Horizontal Lines
Draw horizontal lines at y-positions taken from the result dict.
{"type": "hlines", "data": ["threshold"], "color": "r", "styles": ["dash"]}
{"type": "hlines", "data": ["mean", "mean_plus_sd", "mean_minus_sd"],
"color": "b", "styles": ["solid", "dash", "dash"]}
Field |
Description |
|---|---|
|
List of result-dict keys. Each key’s value → one horizontal line. |
|
Line colour (default |
|
List of |
5.2 vlines - Vertical Lines
Draw vertical lines at x-positions.
{"type": "vlines", "data": "stimulus_times", "color": "c"}
Field |
Description |
|---|---|
|
Result-dict key holding a scalar or array of x-positions. |
|
Line colour (default |
5.3 markers - Scatter Points
Draw scatter points at (x, y) positions from result arrays.
{"type": "markers", "x": "peak_times", "y": "peak_values", "color": "r"}
5.4 interactive_region - Draggable Region
A shaded region linked to two float spinboxes. Dragging the region updates the spinbox values, and changing a spinbox moves the region.
{"type": "interactive_region", "data": ["window_start", "window_end"], "color": "g"}
"data" must be a 2-element list of ui_params names (not result keys).
5.5 threshold_line - Draggable Threshold
A horizontal line synced to a float parameter widget.
{"type": "threshold_line", "param": "threshold"}
5.6 overlay_fit - Curve Overlay
Overlay a fitted curve on the trace.
{
"type": "overlay_fit",
"x": "_fit_time", # result key (use _ prefix to hide from table)
"y": "_fit_values",
"color": "r",
"width": 2,
"label": "Exponential Fit",
}
5.7 popup_xy - Popup Scatter/Line Plot
Open a separate window showing e.g. an I-V or F-I curve.
{
"type": "popup_xy",
"title": "F-I Curve",
"x": "current_steps_pa",
"y": "firing_rates_hz",
"x_label": "Current (pA)",
"y_label": "Firing Rate (Hz)",
}
Optionally add "slope_key" and "intercept_key" for a regression line.
5.8 brackets - Burst/Event Brackets
Draw bracket bars above burst groups.
{"type": "brackets", "data": "bursts", "color": "r"}
"data" key should hold a list of arrays (each array = spike times within one
burst).
5.9 event_markers - Interactive Event Points
Scatter plot with click-to-remove and Ctrl+click-to-add.
{"type": "event_markers"}
Reads result["event_indices"] automatically.
5.10 trace - Base Trace with Overlay
Plot the trace with optional spike/event markers.
{"type": "trace", "show_spikes": True}
5.11 fill_between - Shaded Region Between Two Curves
Draw a translucent filled area between a primary curve (y1) and a baseline
curve or constant (y2). This is ideal for visualising integrated areas such
as synaptic charge transfer.
{
"type": "fill_between",
"x": "_int_x", # key for the shared time array
"y1": "_int_y", # key for the upper/primary curve (required)
"y2": "_base", # key for the lower curve or scalar baseline (default: 0.0)
"brush": (0, 100, 255, 100), # RGBA fill colour (optional)
}
The named keys are looked up first in the top-level result dict and then inside
the nested result["metrics"] dict, so both a flat schema and the standard
{"module_used": ..., "metrics": {...}} schema are supported transparently.
y2 may be:
a key pointing to an array of the same length as
y1(arbitrary curve),a key pointing to a scalar (constant horizontal baseline), or
omitted entirely (defaults to zero).
Example - Synaptic Charge Transfer with shaded integral:
plots=[
{"type": "interactive_region", "data": ["window_start", "window_end"], "color": "g"},
{
"type": "fill_between",
"x": "_int_x",
"y1": "_int_y",
"y2": "_base",
"brush": (0, 100, 255, 100),
},
]
The corresponding return dict must include _int_x, _int_y, and _base as
private (hidden) keys:
return {
"module_used": "synaptic_charge",
"metrics": {"Total_Charge_pC": charge},
"Total_Charge_pC": charge,
"_int_x": win_time.tolist(),
"_int_y": win_data.tolist(),
"_base": baseline_mean,
}
5.12 trace_overlay - Highlight a Region on the Raw Trace
Draw a semi-transparent coloured segment directly on top of the raw trace to highlight an analysed time window (e.g. a baseline region or a response integration window).
{
"type": "trace_overlay",
"start_time": "_baseline_start_s", # result key for start time (s)
"end_time": "_baseline_end_s", # result key for end time (s)
"color": "#00cfff", # hex or name (default from preferences)
"width": 3, # pen width in pixels
"opacity": 60, # 0-100%; 100 = fully opaque
}
Field |
Type |
Description |
|---|---|---|
|
str or float |
Result key or literal float giving the region start (s). |
|
str or float |
Result key or literal float giving the region end (s). |
|
str |
Colour string or hex code. Falls back to the Trace Overlay preference if omitted. |
|
int |
Pen width (pixels, default 3). |
|
int |
0-100 (default 60). Can be overridden by the user via Plot Preferences > Trace Overlay. |
The overlay only renders when the time region lies within the currently plotted
time axis, and uses self._current_plot_data to obtain the raw trace samples.
5.13 event_fit_overlay - Overlay Fitted Event Decay Curves
Plot fitted curves (e.g. bi-exponential EPSP decays) hugging each detected event on the raw trace. Supports both single-event (1-D arrays) and multi-event (list of arrays) data.
{
"type": "event_fit_overlay",
"times_key": "_event_fit_times", # result key for fit time array(s)
"values_key": "_event_fit_values", # result key for fit value array(s)
"color": "#ff9900", # amber by default
"width": 2,
"opacity": 80,
}
Field |
Type |
Description |
|---|---|---|
|
str |
Result key for fit time array (1-D |
|
str |
Result key for fit value array or list of arrays. |
|
str |
Colour (falls back to Event Fit Overlay preference). |
|
int |
Pen width (default 2). |
|
int |
0-100 (default 80). |
Example return dict for a PPR decay fit:
return {
"module_used": "evoked_responses",
"metrics": {
"decay_tau_ms": tau_ms,
},
# Private keys at the TOP LEVEL (not inside "metrics") are fed to plot
# overlays and hidden from the results table.
"_ppr_fit_times": fit_time_array.tolist(), # absolute time (s)
"_ppr_fit_values": fit_value_array.tolist(), # fitted current/voltage
}
The user can customise both overlay types via Edit > Plot Preferences > Trace Overlay and Event Fit Overlay tabs.
6. Where to Put Your Plugin File
Prerequisite - Enable Custom Plugins: Before your plugin will load you must ensure the “Enable Custom Plugins” checkbox is checked in Edit > Preferences > Extensions (or Synaptipy > Preferences on macOS). This setting is on by default. After changing it, restart Synaptipy for the change to take effect.
Option A: Built-in Examples Directory
Synaptipy ships ready-to-run example plugins in examples/plugins/. These are
loaded automatically at startup so you can try them immediately and use them as
templates. Enable them via Edit > Preferences (or Synaptipy > Preferences
on macOS) by checking Enable Custom Plugins, then restart Synaptipy.
Included Example Plugins
File |
Label in GUI |
Purpose |
|---|---|---|
|
Synaptic Charge (AUC) |
Integrates a postsynaptic current trace over a user-defined window to compute total charge (pC) via the trapezoidal rule; highlights the integrated area with a shaded fill overlay and marks the peak amplitude with a star symbol. |
|
Opto Latency Jitter |
Detects the first spike in each sweep following a TTL pulse and reports trial-to-trial latency variability (jitter) for optogenetic monosynaptic verification. Requires a secondary digital/TTL channel. |
|
AP Repolarization Rate |
Finds the steepest falling slope (dV/dt minimum) of the first action potential in a window, quantifying maximum repolarization rate in V/s as a proxy for potassium-channel dynamics. |
To use these plugins:
Open Edit > Preferences (or Synaptipy > Preferences on macOS).
Check Enable Custom Plugins.
Restart Synaptipy. Each plugin appears as a new sub-tab in the Analyser.
To customise one, copy the file to ~/.synaptipy/plugins/ and edit your copy. Synaptipy prefers the user copy over the bundled example, so your changes take effect immediately on the next restart.
Option B: User Plugin Directory (recommended for personal additions)
Platform |
Full path |
|---|---|
macOS / Linux |
|
Windows |
|
No Synaptipy source changes needed.
File is auto-discovered at startup.
Works for any number of
.pyfiles.Will not be overwritten by upgrades.
On Windows, open the folder with
%USERPROFILE%\.synaptipy\pluginsin Explorer.
Option C: Built-in Module (for core contributors)
If you are contributing to the Synaptipy repository itself:
Create your module in
src/Synaptipy/core/analysis/my_analysis.py.Add the import to
src/Synaptipy/core/analysis/__init__.py:from . import my_analysis # noqa: F401 - registers: my_analysis_name
(Optional) Create a custom tab class in
src/Synaptipy/application/gui/analysis_tabs/if you need GUI behaviour beyond what the metadata-driven tab provides.Add a test in
tests/core/test_my_analysis.py.
Important: The
__init__.pyimport is required. Without it, the@AnalysisRegistry.registerdecorator never executes and your analysis will not appear (see the developer guide’s Registry import rule).
7. For Core Contributors - Adding a Built-in Analysis
Step-by-step:
Step |
File |
What to do |
|---|---|---|
1 |
|
Write pure logic + registry wrapper (see §3) |
2 |
|
Add |
3 |
|
Write pytest tests for the pure logic function |
4 |
|
Add your |
Do not create a custom tab class unless you need interactive GUI features (e.g. click-to-add events, drag-to-select spikes) that the metadata-driven tab cannot provide.
8. Testing Your Plugin
Unit-testing the logic function
Since the logic function is pure Python + NumPy, test it directly with pytest:
# test_my_auc.py
import numpy as np
from my_analysis import calculate_area_under_curve
def test_auc_basic():
fs = 10000.0
t = np.arange(0, 0.5, 1 / fs)
# Flat baseline followed by a rectangular current pulse
data = np.zeros_like(t)
data[(t >= 0.1) & (t < 0.3)] = -100.0 # 100 pA inward current, 200 ms
result = calculate_area_under_curve(data, t, fs,
baseline_start=0.0, baseline_end=0.05,
window_start=0.1, window_end=0.3)
# Expected charge: -100 pA * 0.2 s = -20 pC (trapezoid should be close)
assert abs(result["Charge_pC"] - (-20.0)) < 0.01
assert result["Baseline_pA"] == 0.0
def test_auc_error_on_narrow_window():
fs = 1000.0
t = np.array([0.0, 0.001])
data = np.array([0.0, -1.0])
result = calculate_area_under_curve(data, t, fs,
baseline_start=0.0, baseline_end=0.001,
window_start=0.5, window_end=0.6)
assert "error" in result
Integration-testing the registry wrapper
import numpy as np
from Synaptipy.core.analysis.registry import AnalysisRegistry
def test_auc_registered():
# Ensure the plugin is loaded
from Synaptipy.application.plugin_manager import PluginManager
PluginManager.load_plugins()
func = AnalysisRegistry.get_function("synaptic_charge")
assert func is not None
meta = AnalysisRegistry.get_metadata("synaptic_charge")
assert "ui_params" in meta
assert meta.get("label") == "Synaptic Charge (AUC)"
Testing the template shipped with Synaptipy
The repository includes a test that validates the plugin template itself:
conda run -n synaptipy python -m pytest tests/core/test_plugin_template.py -v
9. Full Annotated Example - Synaptic Charge Transfer
This example measures the total synaptic charge (\(Q\), in picocoulombs) delivered during a postsynaptic current by integrating the current trace inside a user-defined time window using the trapezoidal rule.
For a current trace in pA and time in seconds the result is in pA·s = pC.
Save this as ~/.synaptipy/plugins/synaptic_charge.py (or copy it from
examples/plugins/):
"""
Custom Synaptipy Plugin: Synaptic Charge Transfer (Area Under Curve).
Drop this file in ~/.synaptipy/plugins/ and restart Synaptipy.
A new "Synaptic Charge Transfer" tab will appear in the Analyser.
"""
import logging
from typing import Any, Dict
import numpy as np
from Synaptipy.core.analysis.registry import AnalysisRegistry
log = logging.getLogger(__name__)
# ── Part 1: Pure logic ─────────────────────────────────────────────
def calculate_synaptic_charge(
data: np.ndarray,
time: np.ndarray,
sampling_rate: float,
window_start: float,
window_end: float,
baseline_start: float,
baseline_end: float,
) -> Dict[str, Any]:
"""
Integrate the baseline-subtracted current trace to obtain total charge.
Args:
data: 1-D current trace in pA.
time: 1-D time array in seconds, same length as data.
sampling_rate: Sampling rate in Hz.
window_start: Start of the integration window (seconds).
window_end: End of the integration window (seconds).
baseline_start: Start of the baseline window (seconds).
baseline_end: End of the baseline window (seconds).
Returns:
Dict with 'Total_Charge_pC' and hidden keys for plot overlays.
Returns {'error': ...} on invalid input.
"""
if data.size == 0:
return {"error": "Empty data array"}
# Baseline window
bl_i0 = int(np.searchsorted(time, baseline_start, side="left"))
bl_i1 = int(np.searchsorted(time, baseline_end, side="right"))
baseline_seg = data[bl_i0:bl_i1]
if baseline_seg.size < 2:
return {"error": "Baseline window too narrow (need >= 2 samples)"}
baseline_mean = float(np.mean(baseline_seg))
# Integration window
win_i0 = int(np.searchsorted(time, window_start, side="left"))
win_i1 = int(np.searchsorted(time, window_end, side="right"))
win_time = time[win_i0:win_i1]
win_data = data[win_i0:win_i1] - baseline_mean
if win_data.size < 2:
return {"error": "Integration window too narrow (need >= 2 samples)"}
# np.trapz integrates pA * s = pC
charge_pC = float(np.trapz(win_data, win_time))
return {
"module_used": "synaptic_charge",
"metrics": {
"Total_Charge_pC": round(charge_pC, 4),
},
# Scalar top-level key for the results table
"Total_Charge_pC": round(charge_pC, 4),
"Baseline_pA": round(baseline_mean, 4),
# Private keys for plot overlays
"_baseline_level": baseline_mean,
"_int_x": win_time.tolist(),
"_int_y": (data[win_i0:win_i1]).tolist(),
"_base": baseline_mean,
}
# ── Part 2: Registry wrapper ──────────────────────────────────────
@AnalysisRegistry.register(
name="synaptic_charge",
label="Synaptic Charge Transfer",
ui_params=[
{
"name": "baseline_start",
"label": "Baseline Start (s):",
"type": "float",
"default": 0.0,
"min": 0.0,
"max": 1e9,
"decimals": 4,
},
{
"name": "baseline_end",
"label": "Baseline End (s):",
"type": "float",
"default": 0.05,
"min": 0.0,
"max": 1e9,
"decimals": 4,
},
{
"name": "window_start",
"label": "Integration Start (s):",
"type": "float",
"default": 0.05,
"min": 0.0,
"max": 1e9,
"decimals": 4,
},
{
"name": "window_end",
"label": "Integration End (s):",
"type": "float",
"default": 0.3,
"min": 0.0,
"max": 1e9,
"decimals": 4,
},
],
plots=[
# Draggable region over the baseline window
{"type": "interactive_region", "data": ["baseline_start", "baseline_end"], "color": "b"},
# Draggable region over the integration window
{"type": "interactive_region", "data": ["window_start", "window_end"], "color": "g"},
# Horizontal line at the baseline level
{"type": "hlines", "data": ["_baseline_level"], "color": "b", "styles": ["dash"]},
# Shaded fill between the raw current and the baseline level
{
"type": "fill_between",
"x": "_int_x",
"y1": "_int_y",
"y2": "_base",
"brush": (0, 100, 255, 100),
},
],
)
def run_synaptic_charge_wrapper(
data: np.ndarray,
time: np.ndarray,
sampling_rate: float,
**kwargs,
) -> Dict[str, Any]:
"""Registry wrapper - extracts kwargs and calls pure logic."""
return calculate_synaptic_charge(
data=data,
time=time,
sampling_rate=sampling_rate,
window_start=kwargs.get("window_start", 0.05),
window_end=kwargs.get("window_end", 0.3),
baseline_start=kwargs.get("baseline_start", 0.0),
baseline_end=kwargs.get("baseline_end", 0.05),
)
The key design points illustrated here:
A dedicated baseline window is used to subtract the holding current before integration, keeping the result physically meaningful.
np.trapzis used for trapezoidal integration (not a simple sum), which is exact for linear interpolations between sample points.The return dict follows the nested output schema
{"module_used": ..., "metrics": {...}}alongside flat scalar keys for the results table.Two
interactive_regionoverlays let the user drag both windows directly on the trace without typing numbers.A
fill_betweenoverlay shades the integrated area, making the charge visually obvious on the trace._baseline_level,_int_x,_int_y, and_base(private keys) feed the plot overlays without appearing in the results table.
10. Applying Global Themes to Plugins
Synaptipy ships a global plot-customisation system (shared/plot_customization.py)
that lets users change trace colours, line widths, scatter sizes, and region
fills from Edit > Preferences > Plot Customisation. When a user saves new
preferences, a preferences_updated signal fires and all built-in canvases
instantly re-style their existing plot items - no file reload required.
How the theme reaches your plugin
The registry wrapper receives all GUI state through **kwargs. Synaptipy
injects a theme_config dict whenever the current plot-customisation settings
are relevant to the analysis call. Your wrapper can read it like this:
@AnalysisRegistry.register(name="my_metric", ...)
def run_my_metric_wrapper(data, time, sampling_rate, **kwargs):
theme_config = kwargs.get("theme_config", {})
# Retrieve individual style properties with sensible defaults
trace_color = theme_config.get("single_trial_color", (200, 200, 200))
avg_color = theme_config.get("average_color", (255, 255, 0))
scatter_color = theme_config.get("scatter_color", (255, 0, 0))
line_width = theme_config.get("line_width", 1.5)
return my_metric_logic(data, time, sampling_rate,
trace_color=trace_color,
avg_color=avg_color)
Available keys in theme_config:
Key |
Type |
Description |
|---|---|---|
|
|
Colour of individual trial traces |
|
|
Colour of the average overlay |
|
|
Colour of scatter / event markers |
|
|
Fill colour of |
|
|
Colour of threshold |
|
|
Stroke width for all trace pens |
|
|
Symbol size for scatter plots |
Always guard with .get("key", default) so your plugin works even when
theme_config is absent (e.g. in batch-only usage or unit tests).
Returning styled plot data
If your analysis produces overlay arrays (stored under private _-prefixed
keys), you can store the chosen colour alongside them so the canvas can apply
it verbatim:
return {
"My_Value": 42.0,
"_plot_x": time_slice,
"_plot_y": data_slice,
"_plot_color": theme_config.get("average_color", (255, 255, 0)),
}
The canvas reads _plot_color when drawing the overlay curve and passes it
directly to pg.mkPen().
Listening to live theme changes in a popup window
If your plugin opens a secondary pg.PlotWidget popup (via the popup_xy
plot type), connect to the global signal so it re-styles on the fly:
from Synaptipy.shared.plot_customization import get_plot_customization_signals
signals = get_plot_customization_signals()
signals.preferences_updated.connect(my_popup_widget.update_pens)
11. Troubleshooting
Symptom |
Cause |
Fix |
|---|---|---|
Tab does not appear |
Plugin file has a syntax error |
Check the Synaptipy log ( |
Tab does not appear |
File not in the plugins folder |
Verify the path: macOS/Linux: |
Tab does not appear |
Missing |
The file must contain a decorated function. |
|
Plugin imports a package not installed in your environment |
Install the dependency: |
|
Two plugins use the same |
Change one plugin’s |
Parameters don’t show up |
|
Must be one of: |
Plot overlay missing |
Result dict key doesn’t match |
The key in |
Results table shows |
Keys must start with underscore |
Prefix with |
Built-in contrib: 0 tabs on Windows |
Forgot to add |
See §7, step 2. |