Synaptipy Developer Guide
This guide is intended for developers who want to understand, modify, or contribute to Synaptipy.
Table of Contents
Development Environment Setup
Prerequisites
Python 3.10+
Git
pip or conda package manager
Setting Up Your Development Environment
Clone the repository:
git clone https://github.com/anzalks/synaptipy.git
cd synaptipy
Create and activate the conda environment:
conda env create -f environment.yml
conda activate synaptipy
Install in development mode:
pip install -e ".[dev]"
This will install Synaptipy in development mode with all development dependencies.
Project Structure
The Synaptipy codebase is organized as follows:
src/Synaptipy/ # Main package
├── __init__.py # Package initialization
├── __main__.py # Entry point for `python -m Synaptipy`
├── application/ # GUI application layer
│ ├── __main__.py # GUI entry point (run_gui)
│ ├── cli/ # Command-line interface
│ ├── controllers/ # MVC controllers
│ │ ├── analysis_formatter.py # Format analysis results for display
│ │ ├── analysis_plot_manager.py # Manage plot overlays for analysis
│ │ ├── file_io_controller.py # File open/save coordination
│ │ ├── live_analysis_controller.py # Real-time spike detection
│ │ └── shortcut_manager.py # Keyboard shortcut bindings
│ ├── data_loader.py # Background file loading (QThread)
│ ├── gui/ # Main GUI components
│ │ ├── main_window.py # MainWindow (3 tabs)
│ │ ├── analyser_tab.py # Dynamic analysis tab manager
│ │ ├── explorer/ # Data explorer tab
│ │ │ └── explorer_tab.py
│ │ ├── exporter_tab.py # NWB/CSV export tab
│ │ ├── analysis_tabs/ # Analysis tab framework
│ │ │ ├── base.py # BaseAnalysisTab (ABC)
│ │ │ └── metadata_driven.py # MetadataDrivenAnalysisTab (auto-generated)
│ │ ├── analysis_worker.py # Worker thread for analysis
│ │ ├── dialogs/ # Modal dialogs (batch, NWB, preferences)
│ │ ├── widgets/ # Reusable GUI widgets
│ │ └── ui_generator.py # Dynamic UI generation from metadata
│ ├── plugin_manager.py # Plugin discovery and loading
│ ├── session_manager.py # Session state persistence
│ └── startup_manager.py # Application startup sequence
├── core/ # Core analysis and data models
│ ├── data_model.py # Recording and Channel classes
│ ├── error_handler.py # Crash reporter and install_excepthook
│ ├── results.py # Typed result dataclasses
│ ├── processing_pipeline.py # Signal processing pipeline
│ │ # (includes apply_trace_corrections - immutable A→B→C→D)
│ ├── signal_processor.py # Low-level signal processing
│ ├── source_interfaces.py # Data source abstraction
│ └── analysis/ # Analysis algorithms (registry pattern)
│ ├── registry.py # AnalysisRegistry (decorator pattern)
│ ├── batch_engine.py # Batch processing engine
│ ├── epoch_manager.py # EpochManager for TTL and manual epochs
│ ├── cross_file_utils.py # Cross-file trial averaging utilities
│ ├── passive_properties.py # Pillar 1: RMP, Rin, Tau, Sag, I-V, Capacitance
│ ├── single_spike.py # Pillar 2: Spike detection and phase plane
│ ├── firing_dynamics.py # Pillar 3: Excitability, burst, train dynamics
│ ├── synaptic_events.py # Pillar 4: Event detection (3 methods)
│ └── evoked_responses.py # Pillar 5: Evoked Sync, PPR, Stimulus Train (STP)
├── infrastructure/ # I/O and external integrations
│ ├── file_readers/ # Neo-based file readers (NeoAdapter)
│ ├── exporters/ # NWB export (NWBExporter)
│ └── neo_patches.py # Neo compatibility patches
├── shared/ # Utilities and styling
│ ├── constants.py # Application-wide constants
│ ├── data_cache.py # Data caching layer
│ ├── error_handling.py # Custom error classes
│ ├── logging_config.py # Logging configuration
│ ├── plot_exporter.py # Plot export logic (PNG, PDF, SVG)
│ ├── scroll_settings.py # Scroll behaviour settings
│ ├── styling.py # Qt theming (light/dark)
│ ├── theme_manager.py # Theme state management
│ ├── utils.py # General shared utilities
│ ├── plot_factory.py # Reusable plot components
│ ├── plot_zoom_sync.py # Cross-tab zoom synchronization
│ ├── plot_customization.py # Plot appearance options
│ ├── zoom_theme.py # Zoom state theming utilities
│ └── viewbox.py # Custom ViewBox subclass
├── application/
│ └── services/ # Application services layer
│ └── data_loader_service.py # Thin service wrapper around data loading
├── resources/ # Icons and assets
└── templates/ # Plugin templates
├── analysis_template.py # Annotated analysis template
├── plugin_template.py # Quick-start plugin template
├── tab_template.py # Custom tab template
└── test_template.py # Pytest test template for new analyses
tests/ # Test suite
├── conftest.py # Pytest fixtures (platform-specific)
├── application/ # Application and GUI tests
├── core/ # Core module tests
├── infrastructure/ # Infrastructure tests
├── gui/ # GUI-specific tests
└── shared/ # Shared utility tests
scripts/ # Utility scripts
└── run_tests.py # Test runner script
docs/ # Sphinx documentation (ReadTheDocs)
examples/ # Example scripts, notebooks, plugins
5-Pillar Analyser Architecture
The Synaptipy Analyser UI is organised around five primary tabs (pillars).
Each pillar corresponds to a single module-level aggregator entry in the
AnalysisRegistry. Sub-analyses are exposed via method_selector drop-downs
inside each pillar tab and are never shown as independent top-level tabs.
Pillar |
Module file |
Registry key |
Sub-analyses |
|---|---|---|---|
1 - Intrinsic Properties |
|
|
Baseline (RMP), Input Resistance, Tau, Sag Ratio, I-V Curve, Capacitance |
2 - Spike Analysis |
|
|
Spike Detection, Phase Plane |
3 - Excitability |
|
|
Excitability, Burst Analysis, Spike Train Dynamics |
4 - Synaptic Events |
|
|
Threshold, Deconvolution, Baseline+Peak+Kinetics |
5 - Evoked Responses |
|
|
Evoked Sync, Paired-Pulse Ratio, Stimulus Train (STP) |
Custom plugin analyses are appended after the five core pillars.
Immutable Trace Correction Pipeline
All backend analysis must obtain its input trace from
apply_trace_corrections() in core/processing_pipeline.py. This function
enforces the following correction order regardless of GUI state:
Step A - LJP:
V_true = V_recorded - LJP_mvStep B - P/N Leak: subtract scaled mean of sub-threshold sweeps
Step C - Noise Floor: subtract median of pre-event window
Step D - Filtering: apply any digital filters
See Algorithmic Definitions - Section 16 for the full mathematical specification.
Writing Custom Analysis Plugins
Synaptipy supports two ways to add new analysis functions:
User plugins (no source edits): Drop a
.pyfile in~/.synaptipy/plugins/. The file is auto-discovered at startup and your analysis appears as a new Analyser tab. This is the recommended approach for end users.Built-in modules (core contributors): Add a module to
src/Synaptipy/core/analysis/, register the import in__init__.py, and add tests.
Three bundled example plugins ship in examples/plugins/ and are loaded
automatically when Enable Custom Plugins is checked in Preferences:
Plugin file |
Tab label |
Purpose |
|---|---|---|
|
Synaptic Charge (AUC) |
Integrates postsynaptic current to compute total charge (pC) |
|
Opto Latency Jitter |
Trial-to-trial spike latency variability after TTL pulse |
|
AP Repolarization Rate |
Maximum repolarization rate (dV/dt minimum) of the first AP |
A ready-to-copy template is at src/Synaptipy/templates/plugin_template.py.
For the complete reference - including all ui_params types, plots types,
return-dict conventions, visible_when rules, and a fully annotated example -
see the dedicated guide: Writing Custom Analysis Plugins.
Development Workflow
Feature Development
Create a branch: For new features or bug fixes, create a branch:
git checkout -b feature/your-feature-name
Implement changes: Make your changes in the relevant files
Add tests: Write tests for your new code
Run tests: Ensure all tests pass:
python scripts/run_tests.py
Submit a pull request: Push your branch and create a pull request
Code Review Process
All contributions go through code review. Maintainers will review your code for:
Functionality
Test coverage
Code style
Documentation
Testing
Running Tests
Run the full test suite:
python scripts/run_tests.py
Run specific tests:
python scripts/run_tests.py --test test_main_window
Run with coverage reporting:
python scripts/run_tests.py --coverage
CI Test Matrix
CI runs on all three platforms across Python 3.10, 3.11, and 3.12:
Platform |
Python Versions |
|---|---|
Ubuntu (latest) |
3.10, 3.11, 3.12 |
Windows (latest) |
3.10, 3.11, 3.12 |
macOS (latest) |
3.10, 3.11, 3.12 |
CI enforces black --check, isort --check, and flake8. PRs that fail
any of these checks are rejected.
Two additional jobs run automatically:
minimum-viable: Python 3.10 with exact lower-bound versions (numpy==2.0.0,scipy==1.14.0,neo==0.14.0,pyqtgraph==0.13.3,pyside6==6.7.3). Confirms the stated minimum requirements actually work.bleeding-edge: Python 3.12 with all upgradable deps set to latest (PySide6 excluded). Runs withcontinue-on-error: trueto give early warning of upcoming breakage without blocking the PR.
Golden Master Tests
tests/core/test_golden_master.py freezes exact floating-point outputs from
the passive-properties algorithms against known ABF data files in
examples/data/. If a library upgrade changes a result by more than 0.001 %,
these tests fail immediately and pinpoint which value drifted.
When to update golden master values:
If you intentionally change an algorithm (e.g. improve the exponential fit
initialisation in calculate_tau), the golden master values will need
updating. Do this with a dedicated commit:
# Re-run the probe script to get new values
conda run -n synaptipy python -c "
from Synaptipy.infrastructure.file_readers.neo_adapter import NeoAdapter
from Synaptipy.core.analysis.passive_properties import calculate_rmp, calculate_rin
adapter = NeoAdapter()
rec = adapter.load_recording('examples/data/24o18002.abf')
ch = list(rec.channels.values())[0]
v = ch.get_trial_data(0)
t = ch.get_relative_time_vector(0)
print(calculate_rmp(v, t, baseline_window=(0.0, 0.5)))
"
# Then edit tests/core/test_golden_master.py to use the new expected value
# with a clear commit message: 'test: update golden masters for improved tau fit'
The tolerances are set to rel=1e-5 (0.001 %) for most values and rel=1e-2
(1 %) for tau (exponential fits are inherently less numerically stable).
Do not tighten these tolerances without a specific reason.
Writing Tests
Place tests in the appropriate subdirectory of the
tests/folderName test files with
test_prefixUse pytest fixtures for setup and teardown
Mock external dependencies where appropriate
Every new analysis function needs a test in
tests/core/Every new GUI behaviour needs a test in
tests/gui/
CI Behaviour and Platform-Specific Test Rules
The test suite involves PySide6 and pyqtgraph widgets running under
QT_QPA_PLATFORM=offscreen. Several platform-specific crash patterns have been
resolved; the rules below must not be reverted or the CI will break again.
Analysis Registry import rule - DO NOT import only registry.py
To populate the AnalysisRegistry, always import the full package:
import Synaptipy.core.analysis # triggers __init__.py → from . import basic_features, etc.
Never rely on from Synaptipy.core.analysis.registry import AnalysisRegistry
alone - that only imports the registry class and does NOT execute the analysis
sub-modules’ @AnalysisRegistry.register decorators.
This was the root cause of a Windows-only bug where the Analyser tab showed 0 tabs while macOS showed 15: on macOS the batch engine happened to import the full package earlier via a different path (masking the issue), but on Windows no other code path triggered the import and the registry remained empty.
The fix is in two places:
startup_manager._begin_loading()- imports the full package before building the GUI so the registry is pre-populated.analyser_tab._load_analysis_tabs()- imports the full package immediately before callingAnalysisRegistry.list_registered()as a safety net.
Editable install must point to the active workspace
pip install -e . stores the editable project location. If the repo is cloned
to a new directory, the old editable link still points to the previous path.
Run pip install -e . from the new workspace to update.
Symptom: modules visible on disk (e.g. capacitance.py, optogenetics.py,
train_dynamics.py) throw ModuleNotFoundError because Python resolves the
package from the stale path. Verify with:
pip show Synaptipy | grep "Editable project location"
Why local macOS tests always exit non-zero
pytest_sessionfinish in tests/conftest.py calls os._exit(exitstatus) when
QT_QPA_PLATFORM=offscreen is set. This causes the macOS process to terminate
with a QThread: Destroyed while thread is still running message and an
Abort trap: 6 printed by the shell - even when every test passed. The
exit code written to the OS is the real pytest exit code (0 = all passed). The
shell may still report exit code 1 because conda intercepts the abnormal
termination.
Rule: Do not judge local macOS test runs by the shell exit code or the
Abort trap message. Always check the pytest output lines (N passed, no
FAILED) or use:
conda run -n synaptipy python -m pytest tests/ 2>&1 | grep -c PASSED
conda run -n synaptipy python -m pytest tests/ 2>&1 | grep "FAILED\|ERROR "
GC must be disabled in offscreen mode
pytest_configure disables Python’s cyclic GC when QT_QPA_PLATFORM=offscreen
(see tests/conftest.py). Do not remove this. With GC enabled, Python can
trigger tp_dealloc on PySide6 wrapper objects while Qt’s C++ destructor chain
is still running, causing SIGBUS on macOS and access violations on Windows.
processEvents() before addPlot() in offscreen mode
SynaptipyPlotCanvas.add_plot() calls
QCoreApplication.processEvents() before widget.addPlot() when
QT_QPA_PLATFORM=offscreen. This must not be removed. On Windows +
PySide6 ≥ 6.9, deferred callbacks queued by a prior widget.clear() or widget
construction fire inside PlotItem.__init__() if they are still pending,
dereferences freed C++ pointers, and causes an access violation that silently
kills the test worker process (all remaining tests never run).
Using processEvents() (execute callbacks) rather than removePostedEvents()
(discard callbacks) is intentional:
removePostedEventson macOS discards events that pyqtgraph needs to maintain itsAllViewsregistry and internal geometry caches; discarding them corrupts session-scoped widget state and causes segfaults inwidget.clear()on the next test.processEventsis safe on all platforms.
removePostedEvents() must skip macOS
The global _drain_qt_events_after_test fixture in tests/conftest.py and the
per-file drain in tests/application/gui/test_explorer_refactor.py both guard
the removePostedEvents(None, 0) call with if sys.platform != 'darwin': return.
Do not remove this guard. On macOS, draining the global event queue between
tests discards pyqtgraph’s internal range/layout events and corrupts
ViewBox geometry caches, causing later widget.clear() calls to segfault.
Plot teardown order
SynaptipyPlotCanvas.clear_plots() must follow this exact sequence:
_unlink_all_plots()- breaksetXLink/setYLinkconnections before teardown._close_all_plots()- disconnect ctrl signals and callPlotItem.close()while the scene is still valid._cancel_pending_qt_events()- discard stale events (Win/Linux only).widget.clear()- destroy C++ layout children via Qt’s scene graph.plot_items.clear()- drop Python references after C++ teardown._flush_qt_registry()- discard any events posted bywidget.clear().
Dropping Python references (step 5) before widget.clear() (step 4) causes
PySide6 ≥ 6.7 to segfault on macOS when the C++ destructor tries to reach the
Python side.
Coding Standards
PEP 8: Follow Python style guidelines (max line length 120)
Formatting: All code is auto-formatted with black (line-length 120, target Python 3.10)
Import sorting: Imports are sorted by isort with the
blackprofileLinting: flake8 enforces style rules (max-complexity 10)
Docstrings: All public functions, classes, and methods should have docstrings
Type Hints: Use type hints for function parameters and return values
Error Handling: Use custom error classes and handle exceptions appropriately
Formatting your code
Before committing, run the formatters and linter:
# Sort imports
isort src/ tests/
# Format code
black src/ tests/
# Check for lint errors
flake8 src/ tests/
You can also verify without modifying files (as CI does):
black --check --diff src/ tests/
isort --check --diff src/ tests/
CI will reject any pull request that does not pass black --check, isort --check, and flake8.
License Compliance
Synaptipy is licensed under the GNU Affero General Public License Version 3 (AGPL-3.0).
License Requirements
As a developer, you should:
Include license notice: All source files should include a reference to the AGPL-3.0 license
Preserve copyright notices: Keep all copyright notices intact
Document changes: Note significant modifications in the code and CHANGELOG
Share modifications: If you distribute modified versions, you must release the source code
Adding New Files
When adding new files to the project, include this header:
#!/usr/bin/env python3
"""
Brief description of the file
Detailed description of the file's purpose and functionality.
This file is part of Synaptipy, licensed under the GNU Affero General Public License v3.0.
See the LICENSE file in the root of the repository for full license details.
"""
Third-Party Dependencies
When adding new dependencies, ensure they have licenses compatible with AGPL-3.0. Generally, this means:
GPL-3.0 and AGPL-3.0 are fully compatible
LGPL, MIT, BSD, and Apache 2.0 licenses can be used alongside AGPL-3.0
Proprietary licenses are typically incompatible
If you’re unsure about compatibility, discuss with project maintainers before adding the dependency.