From c7d136cc2f7fc6b500fadcc42e30c3be17538c4b Mon Sep 17 00:00:00 2001 From: Anzal Date: Mon, 1 Jun 2026 23:14:42 +0200 Subject: [PATCH 1/6] feat: add methods paragraph generator, implement HighDPI support, add unit validation to core analysis, and update ABF import error handling. --- .github/workflows/test.yml | 5 --- README.md | 2 ++ pyproject.toml | 11 +----- src/Synaptipy/application/__main__.py | 7 +++- .../application/gui/explorer/explorer_tab.py | 10 ++++++ .../application/gui/session_summary_dialog.py | 35 +++++++++++++++++++ .../core/analysis/passive_properties.py | 24 ++++++++++++- .../infrastructure/exporters/nwb_exporter.py | 22 ++++++++---- .../file_readers/neo_adapter.py | 21 ++++++++--- src/Synaptipy/shared/plot_exporter.py | 22 +++++++----- src/Synaptipy/templates/plugin_template.py | 5 +-- tests/core/test_nwb_metadata_completeness.py | 12 +++++++ .../exporters/test_nwb_exporter.py | 6 ++-- .../file_readers/test_neo_adapter.py | 2 +- 14 files changed, 143 insertions(+), 41 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 701b674f..fee0e626 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -110,11 +110,6 @@ jobs: GITHUB_TOKEN: ${{ github.token }} # Push core test coverage badge endpoint.json to the data branch so the - # README badge resolves on every branch, not only after merging to main. - - name: Remove .coverage database before Codecov upload - if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.11' - run: rm -f .coverage # prevent codecov CLI from regenerating coverage.xml with wrong paths - - name: Upload coverage to Codecov if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.11' uses: codecov/codecov-action@v5 diff --git a/README.md b/README.md index d1c641f2..01544afb 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,8 @@ Full documentation: [synaptipy.readthedocs.io](https://synaptipy.readthedocs.io/ ## Installation +**WARNING: PySide6 must remain pinned to version 6.7.3 due to QTBUG-130070.** + ### Prerequisites - [Anaconda](https://www.anaconda.com/download) or [Miniconda](https://docs.conda.io/en/latest/miniconda.html) diff --git a/pyproject.toml b/pyproject.toml index 4e4c7688..43edd57c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -144,7 +144,6 @@ known_first_party = ["Synaptipy"] [tool.coverage.run] source = ["src/Synaptipy"] -relative_files = true omit = [ "*/tests/*", "*/__pycache__/*", @@ -198,15 +197,7 @@ omit = [ # Map GitHub Actions runner absolute paths → local src-layout paths. # coverage.py rewrites any match below to the first (canonical) entry before # generating reports, ensuring coverage.xml contains repo-relative paths. -source = [ - "src/Synaptipy", - # Linux runner (anzalks/synaptipy → /home/runner/work/synaptipy/synaptipy/) - "/home/runner/work/synaptipy/synaptipy/src/Synaptipy", - # macOS runner (/Users/runner/work/synaptipy/synaptipy/) - "/Users/runner/work/synaptipy/synaptipy/src/Synaptipy", - # Windows runner (D:\a\synaptipy\synaptipy\) - "D:\\a\\synaptipy\\synaptipy\\src\\Synaptipy", -] +source = ["src", "/home/runner/work/synaptipy/synaptipy/src", "/Users/runner/work/synaptipy/synaptipy/src", "D:\\a\\synaptipy\\synaptipy\\src"] [tool.coverage.report] show_missing = true diff --git a/src/Synaptipy/application/__main__.py b/src/Synaptipy/application/__main__.py index b00404fa..4f5c0f0e 100644 --- a/src/Synaptipy/application/__main__.py +++ b/src/Synaptipy/application/__main__.py @@ -263,10 +263,15 @@ def run_gui(): # noqa: C901 if dev_mode: log.info("Running in DEVELOPMENT mode with verbose logging") - # Create Qt Application with High DPI support app = QtWidgets.QApplication.instance() if app is None: # Enable High DPI scaling before creating QApplication + try: + QtCore.QCoreApplication.setAttribute(QtCore.Qt.ApplicationAttribute.AA_EnableHighDpiScaling, True) + QtCore.QCoreApplication.setAttribute(QtCore.Qt.ApplicationAttribute.AA_UseHighDpiPixmaps, True) + except Exception as e: + log.warning(f"Could not set High DPI attributes: {e}") + # Check if HighDpiScaleFactorRoundingPolicy is available (Qt 6.0+) if hasattr(QtCore.Qt, "HighDpiScaleFactorRoundingPolicy"): try: diff --git a/src/Synaptipy/application/gui/explorer/explorer_tab.py b/src/Synaptipy/application/gui/explorer/explorer_tab.py index b5c54d91..80f10147 100644 --- a/src/Synaptipy/application/gui/explorer/explorer_tab.py +++ b/src/Synaptipy/application/gui/explorer/explorer_tab.py @@ -599,6 +599,13 @@ def load_recording_data( # noqa: C901 return self._is_loading = True + # Add visual loading indicator + self._loading_dialog = QtWidgets.QProgressDialog(f"Loading {filepath.name}...", None, 0, 0, self) + self._loading_dialog.setWindowTitle("Please Wait") + self._loading_dialog.setWindowModality(QtCore.Qt.WindowModality.WindowModal) + self._loading_dialog.setCancelButton(None) + self._loading_dialog.show() + if file_list is None: file_list = [filepath] if selected_index == -1: @@ -1456,6 +1463,9 @@ def _on_file_load_error(self, error: Exception, filepath: Path): def _finalize_loading_state(self): self._is_loading = False + if hasattr(self, "_loading_dialog") and self._loading_dialog: + self._loading_dialog.close() + self._loading_dialog = None def _update_all_ui_state(self): # Delegate to subcomponents or handle locally? diff --git a/src/Synaptipy/application/gui/session_summary_dialog.py b/src/Synaptipy/application/gui/session_summary_dialog.py index 9bf0b76f..ade4cf1a 100644 --- a/src/Synaptipy/application/gui/session_summary_dialog.py +++ b/src/Synaptipy/application/gui/session_summary_dialog.py @@ -41,6 +41,19 @@ def _setup_ui(self): self._populate_table() self._calculate_stats() + # 3. Methods Paragraph + methods_group = QtWidgets.QGroupBox("Methods Paragraph") + methods_layout = QtWidgets.QVBoxLayout(methods_group) + self.methods_text = QtWidgets.QTextEdit() + self.methods_text.setReadOnly(True) + self.methods_text.setText(self._generate_methods_paragraph()) + methods_layout.addWidget(self.methods_text) + + copy_btn = QtWidgets.QPushButton("Copy Methods") + copy_btn.clicked.connect(lambda: QtWidgets.QApplication.clipboard().setText(self.methods_text.toPlainText())) + methods_layout.addWidget(copy_btn) + layout.addWidget(methods_group) + # Close button close_btn = QtWidgets.QPushButton("Close") close_btn.clicked.connect(self.accept) @@ -95,3 +108,25 @@ def _calculate_stats(self): label = key.replace("_", " ").title() self.stats_layout.addRow(f"{label}:", QtWidgets.QLabel(f"{mean_val:.4g} ± {std_val:.4g}")) + + def _generate_methods_paragraph(self) -> str: + try: + from Synaptipy import __version__ + + version = __version__ + except ImportError: + version = "unknown" + + if not self.results: + return f"Data was analyzed using Synaptipy {version}." + + sample = self.results[0] + freq = sample.get("matched_filter_freq", "X") + prominence = sample.get("prominence_factor", "Y") + + # You can expand this logic to extract more actual parameters + return ( + f"Data was analyzed using Synaptipy {version}. " + f"Event detection utilized a matched filter (freq: {freq} Hz) " + f"with a prominence factor of {prominence}." + ) diff --git a/src/Synaptipy/core/analysis/passive_properties.py b/src/Synaptipy/core/analysis/passive_properties.py index fb7bf09e..0681ec9d 100644 --- a/src/Synaptipy/core/analysis/passive_properties.py +++ b/src/Synaptipy/core/analysis/passive_properties.py @@ -18,6 +18,7 @@ from typing import Any, Dict, List, Optional, Tuple, Union import numpy as np +import quantities as pq from scipy.optimize import curve_fit from scipy.stats import linregress @@ -256,7 +257,7 @@ def find_stable_baseline( # --------------------------------------------------------------------------- -def calculate_rin( +def calculate_rin( # noqa: C901 voltage_trace: np.ndarray, time_vector: np.ndarray, current_amplitude: float, @@ -346,6 +347,16 @@ def calculate_rin( parameters=parameters or {}, ) + # Unit checks using quantities package + if hasattr(voltage_trace, "units") and hasattr(current_amplitude, "units"): + try: + voltage_trace.rescale(pq.V) + current_amplitude.rescale(pq.A) + except ValueError as e: + raise ValueError( + f"Incompatible units passed: voltage must be in V (e.g. mV) and current in A (e.g. pA) when calculating resistance. Details: {e}" # noqa: E501 + ) + if delta_i_pa == 0.0: log.warning("Cannot calculate Rin: Current amplitude is zero.") return RinResult(value=float(np.nan), unit="MOhm", is_valid=False, error_message="Current amplitude is zero") @@ -742,6 +753,17 @@ def calculate_conductance( error_message="Voltage step is zero", parameters=parameters or {}, ) + + # Unit checks using quantities package + if hasattr(current_trace, "units") and hasattr(voltage_step, "units"): + try: + current_trace.rescale(pq.A) + voltage_step.rescale(pq.V) + except ValueError as e: + raise ValueError( + f"Incompatible units passed: voltage must be in V (e.g. mV) and current in A (e.g. pA) when calculating conductance. Details: {e}" # noqa: E501 + ) + try: baseline_mask = (time_vector >= baseline_window[0]) & (time_vector < baseline_window[1]) response_mask = (time_vector >= response_window[0]) & (time_vector < response_window[1]) diff --git a/src/Synaptipy/infrastructure/exporters/nwb_exporter.py b/src/Synaptipy/infrastructure/exporters/nwb_exporter.py index 644cb931..64ef9b87 100644 --- a/src/Synaptipy/infrastructure/exporters/nwb_exporter.py +++ b/src/Synaptipy/infrastructure/exporters/nwb_exporter.py @@ -300,6 +300,9 @@ def export( # noqa: C901 output_path: Path, session_metadata: Dict[str, Any], analysis_results: Optional[Dict[str, Any]] = None, + subject_id: Optional[str] = None, + session_start_time: Optional[datetime] = None, + device_description: Optional[str] = None, ): """ Exports the given Recording object to an NWB file. @@ -335,24 +338,31 @@ def export( # noqa: C901 raise ValueError("Recording object is missing 'channels' dictionary or is not a dictionary.") # --- Inject required metadata defaults for DANDI compliance --- + session_metadata = dict(session_metadata) # avoid mutating caller's dict + if subject_id: + session_metadata["subject_id"] = subject_id + if session_start_time: + session_metadata["session_start_time"] = session_start_time + if device_description: + session_metadata["device_description"] = device_description + # Subject ID and Species are required by NWB/DANDI validators. if not session_metadata.get("subject_id"): - session_metadata = dict(session_metadata) # avoid mutating caller's dict - session_metadata.setdefault("subject_id", "unknown_subject") - log.warning("NWB export: 'subject_id' not provided; defaulting to 'unknown_subject'.") + raise ValueError("NWB export requires a valid 'subject_id' MINDS metadata field.") if not session_metadata.get("species"): session_metadata.setdefault("species", "unknown species") log.warning("NWB export: 'species' not provided; defaulting to 'unknown species'.") if not session_metadata.get("device_name"): - session_metadata = dict(session_metadata) session_metadata.setdefault("device_name", "Generic Amplifier") log.warning("NWB export: 'device_name' not provided; defaulting to 'Generic Amplifier'.") + if not session_metadata.get("device_description"): + raise ValueError("NWB export requires a valid 'device_description' MINDS metadata field.") # --- Validate Metadata & Prepare NWBFile --- - required_keys = ["session_description", "identifier", "session_start_time"] + required_keys = ["session_description", "identifier", "session_start_time", "subject_id", "device_description"] missing_keys = [key for key in required_keys if key not in session_metadata or not session_metadata[key]] if missing_keys: - raise ValueError(f"Missing required NWB session metadata: {missing_keys}") + raise ValueError(f"Missing required NWB MINDS session metadata: {missing_keys}") start_time = session_metadata["session_start_time"] if not isinstance(start_time, datetime): diff --git a/src/Synaptipy/infrastructure/file_readers/neo_adapter.py b/src/Synaptipy/infrastructure/file_readers/neo_adapter.py index 23732c83..f86613ff 100644 --- a/src/Synaptipy/infrastructure/file_readers/neo_adapter.py +++ b/src/Synaptipy/infrastructure/file_readers/neo_adapter.py @@ -521,11 +521,22 @@ def read_recording( # noqa: C901 _pyabf_rescue = True log.info("pyabf rescue succeeded for '%s'.", filepath.name) except ImportError: - raise FileReadError( - "ABF file could not be read by Neo and the optional pyabf " - "rescue loader is not installed. Run " - "`pip install synaptipy[formats]` or `pip install pyabf`." - ) + try: + from PySide6 import QtWidgets, QtCore + + def _show_pyabf_warning(): + QtWidgets.QMessageBox.warning( + None, + "Missing pyabf Dependency", + "ABF file could not be read by Neo and the optional pyabf " + "rescue loader is not installed.\n\n" + "Please run 'pip install pyabf' to enable this file.", + ) + + QtCore.QTimer.singleShot(0, _show_pyabf_warning) + except Exception: + pass + raise FileReadError("ABF rescue failed: pyabf not installed.") except Exception as pyabf_err: log.error("pyabf rescue also failed for '%s': %s", filepath.name, pyabf_err) # fall through to the generic lazy fallback below diff --git a/src/Synaptipy/shared/plot_exporter.py b/src/Synaptipy/shared/plot_exporter.py index 1360f45d..f713cc53 100644 --- a/src/Synaptipy/shared/plot_exporter.py +++ b/src/Synaptipy/shared/plot_exporter.py @@ -55,7 +55,7 @@ def export(self, filename: str, fmt: str, dpi: int) -> bool: Returns True if successful, False otherwise. """ try: - if fmt in ["svg", "pdf"]: + if fmt in ["pdf"]: return self._save_via_matplotlib(filename, fmt, dpi) else: return self._save_via_pyqtgraph(filename, fmt, dpi) @@ -92,15 +92,21 @@ def _save_via_pyqtgraph(self, filename: str, fmt: str, dpi: int) -> bool: pass # non-fatal — proceed with whatever background exists try: - exporter = pg.exporters.ImageExporter(target_item) + if fmt == "svg": + exporter = pg.exporters.SVGExporter(target_item) + exporter.export(filename) + log.info(f"Exported SVG plot to {filename}") + return True + else: + exporter = pg.exporters.ImageExporter(target_item) - # Scale for DPI (Screen DPI is usually ~96) - scale_factor = dpi / 96.0 - exporter.parameters()["width"] = int(target_item.width() * scale_factor) + # Scale for DPI (Screen DPI is usually ~96) + scale_factor = dpi / 96.0 + exporter.parameters()["width"] = int(target_item.width() * scale_factor) - exporter.export(filename) - log.info(f"Exported raster plot to {filename}") - return True + exporter.export(filename) + log.info(f"Exported raster plot to {filename}") + return True finally: # Restore original background try: diff --git a/src/Synaptipy/templates/plugin_template.py b/src/Synaptipy/templates/plugin_template.py index 5ef14fb3..d4b2bbd8 100644 --- a/src/Synaptipy/templates/plugin_template.py +++ b/src/Synaptipy/templates/plugin_template.py @@ -62,11 +62,12 @@ def calculate_my_metric( # TODO: document your additional parameters here. Returns: - Dict with your results. Keys become rows in the results table. + Dict with your results. Your main `process()` function should return a dictionary + with keys matching the `PluginResult` schema. Keys become rows in the results table. Keys starting with ``_`` are hidden from the table (use for plot data). A key named ``"error"`` triggers an error message in the GUI. - Recommended output schema:: + Recommended output schema (matching PluginResult):: { "module_used": "my_custom_metric", # module identifier diff --git a/tests/core/test_nwb_metadata_completeness.py b/tests/core/test_nwb_metadata_completeness.py index 424d0936..6625cc41 100644 --- a/tests/core/test_nwb_metadata_completeness.py +++ b/tests/core/test_nwb_metadata_completeness.py @@ -69,6 +69,8 @@ def test_electrode_resistance_exported(self): "experimenter": "Test", "lab": "Test Lab", "institution": "Test University", + "subject_id": "Mouse_01", + "device_description": "Device_1", }, ) @@ -123,6 +125,8 @@ def test_electrode_without_resistance_seal(self): "experimenter": "Test", "lab": "Test Lab", "institution": "Test University", + "subject_id": "Mouse_01", + "device_description": "Device_1", }, ) @@ -166,6 +170,8 @@ def test_preprocessing_history_exported(self): "experimenter": "Test", "lab": "Test Lab", "institution": "Test University", + "subject_id": "Mouse_01", + "device_description": "Device_1", }, ) @@ -224,6 +230,8 @@ def test_no_preprocessing_history(self): "experimenter": "Test", "lab": "Test Lab", "institution": "Test University", + "subject_id": "Mouse_01", + "device_description": "Device_1", }, ) @@ -275,6 +283,8 @@ def test_nwb_file_validates(self): "experimenter": "Test", "lab": "Test Lab", "institution": "Test University", + "subject_id": "Mouse_01", + "device_description": "Device_1", }, ) @@ -324,6 +334,8 @@ def test_required_fields_present(self): "experimenter": "Jane Doe", "lab": "Neuroscience Lab", "institution": "University", + "subject_id": "Mouse_01", + "device_description": "Device_1", }, ) diff --git a/tests/infrastructure/exporters/test_nwb_exporter.py b/tests/infrastructure/exporters/test_nwb_exporter.py index 69fbe15f..a344b7f7 100644 --- a/tests/infrastructure/exporters/test_nwb_exporter.py +++ b/tests/infrastructure/exporters/test_nwb_exporter.py @@ -46,6 +46,8 @@ def valid_session_metadata(mock_recording_for_export): "lab": "Test Lab", "institution": "Test Uni", "session_id": "SESSION001", + "subject_id": "SUBJ_123", + "device_description": "Axopatch 200B", } @@ -98,8 +100,8 @@ def test_nwb_export_success(nwb_exporter_instance, mock_recording_for_export, va def test_nwb_export_missing_metadata(nwb_exporter_instance, mock_recording_for_export, tmp_path): """Test export fails if required metadata is missing.""" output_file = tmp_path / "test_fail.nwb" - invalid_metadata = {"identifier": "xyz"} # Missing description and start time - with pytest.raises(ValueError, match="Missing required NWB session metadata"): + invalid_metadata = {"identifier": "xyz", "subject_id": "SUBJ_123", "device_description": "Axopatch"} # Missing description and start time + with pytest.raises(ValueError, match="Missing required NWB MINDS session metadata"): nwb_exporter_instance.export(mock_recording_for_export, output_file, invalid_metadata) diff --git a/tests/infrastructure/file_readers/test_neo_adapter.py b/tests/infrastructure/file_readers/test_neo_adapter.py index f30314b1..8d54ab27 100644 --- a/tests/infrastructure/file_readers/test_neo_adapter.py +++ b/tests/infrastructure/file_readers/test_neo_adapter.py @@ -217,7 +217,7 @@ def test_pyabf_fallback_rescue_import_error(mocker, neo_adapter_instance, tmp_pa # Remove pyabf from sys.modules so the import fails mocker.patch.dict("sys.modules", {"pyabf": None}) - with pytest.raises(FileReadError, match="synaptipy\\[formats\\]"): + with pytest.raises(FileReadError, match="ABF rescue failed: pyabf not installed."): neo_adapter_instance.read_recording(abf_file) From 2b4099925a21ba6f15c51f75f7c51999bc298fcf Mon Sep 17 00:00:00 2001 From: Anzal Date: Mon, 1 Jun 2026 23:26:08 +0200 Subject: [PATCH 2/6] refactor: update rendering benchmark to compare opaque/transparent modes and support indexed trial pens --- scripts/benchmark_e2e.py | 20 +++- scripts/benchmark_rendering.py | 108 ++++++++++-------- src/Synaptipy/application/session_manager.py | 2 +- src/Synaptipy/shared/plot_customization.py | 25 ++-- src/Synaptipy/shared/styling.py | 19 ++- .../application/gui/test_explorer_refactor.py | 7 +- 6 files changed, 111 insertions(+), 70 deletions(-) diff --git a/scripts/benchmark_e2e.py b/scripts/benchmark_e2e.py index b6903a8d..806d3278 100644 --- a/scripts/benchmark_e2e.py +++ b/scripts/benchmark_e2e.py @@ -58,7 +58,21 @@ _SCRIPT_DIR = Path(__file__).resolve().parent _REPO_ROOT = _SCRIPT_DIR.parent _SRC_DIR = _REPO_ROOT / "src" -_ABF_0021 = _REPO_ROOT / "examples" / "data" / "2023_04_11_0021.abf" + + +def _get_abf_path(): + target = _REPO_ROOT / "examples" / "data" / "2023_04_11_0021.abf" + if target.exists(): + return target + data_dir = _REPO_ROOT / "examples" / "data" + if data_dir.exists(): + abfs = list(data_dir.glob("*.abf")) + if abfs: + return abfs[0] + return target + + +_ABF_0021 = _get_abf_path() # N of trials overlaid in OVERLAY_AVG benchmark. # 0021.abf has 20 trials; levels are clamped to min(N, n_all). @@ -535,9 +549,9 @@ def main() -> int: parser.add_argument( "--output-dir", type=Path, - default=_REPO_ROOT / "paper" / "results", + default=_REPO_ROOT / "benchmarks" / "results", metavar="PATH", - help="Destination for CSV and PNG (default: paper/results/).", + help="Destination for CSV and PNG (default: benchmarks/results/).", ) parser.add_argument( "--_child", diff --git a/scripts/benchmark_rendering.py b/scripts/benchmark_rendering.py index 93f4841d..757f9079 100644 --- a/scripts/benchmark_rendering.py +++ b/scripts/benchmark_rendering.py @@ -71,7 +71,21 @@ _SCRIPT_DIR = Path(__file__).resolve().parent _REPO_ROOT = _SCRIPT_DIR.parent _SRC_DIR = _REPO_ROOT / "src" -_ABF_0021 = _REPO_ROOT / "examples" / "data" / "2023_04_11_0021.abf" + + +def _get_abf_path(): + target = _REPO_ROOT / "examples" / "data" / "2023_04_11_0021.abf" + if target.exists(): + return target + data_dir = _REPO_ROOT / "examples" / "data" + if data_dir.exists(): + abfs = list(data_dir.glob("*.abf")) + if abfs: + return abfs[0] + return target + + +_ABF_0021 = _get_abf_path() # N=40 is excluded: the source file has 20 trials; at N=40 the i%20 indexing # cycles through all arrays twice per update, placing the entire dataset in L2 @@ -91,7 +105,8 @@ def _run_child(mode: str) -> None: """Run the rendering benchmark in-process and print JSON to stdout.""" - use_opengl = mode == "opengl" + use_opengl = mode.startswith("opengl") + force_opaque = "opaque" in mode if str(_SRC_DIR) not in sys.path: sys.path.insert(0, str(_SRC_DIR)) @@ -108,6 +123,10 @@ def _run_child(mode: str) -> None: pg.setConfigOption("background", "k") pg.setConfigOption("foreground", "w") + from Synaptipy.shared.plot_customization import set_force_opaque_trials + + set_force_opaque_trials(force_opaque) + from PySide6.QtWidgets import QApplication, QGridLayout, QWidget app = QApplication.instance() or QApplication(sys.argv) @@ -245,18 +264,20 @@ def _spawn_child(mode: str) -> dict: print(f"WARNING: no output from child [{mode}]") return {} try: - return json.loads(lines[-1]) + data = json.loads(lines[-1]) + data["_mode"] = mode # pass the mode back for identifying + return data except json.JSONDecodeError as exc: print(f"WARNING: could not parse child output [{mode}]: {exc}") return {} -def _save_csv(opengl_data: dict, software_data: dict, output_path: Path) -> None: +def _save_csv(results_dict: dict, output_path: Path) -> None: """Write rendering benchmark results to CSV.""" import csv fieldnames = [ - "renderer", + "renderer_mode", "n_trials", "total_samples", "median_ms", @@ -264,24 +285,21 @@ def _save_csv(opengl_data: dict, software_data: dict, output_path: Path) -> None "p95_ms", ] rows = [] - for n_trials, data in sorted(opengl_data.items(), key=lambda x: int(x[0])): - rows.append( - { - "renderer": "opengl", - "n_trials": n_trials, - "total_samples": int(n_trials) * 20000, - **data, - } - ) - for n_trials, data in sorted(software_data.items(), key=lambda x: int(x[0])): - rows.append( - { - "renderer": "software", - "n_trials": n_trials, - "total_samples": int(n_trials) * 20000, - **data, - } - ) + for mode_name, data_set in results_dict.items(): + if not data_set: + continue + for n_trials, data in sorted(data_set.items(), key=lambda x: int(x[0]) if x[0].isdigit() else 0): + if n_trials == "_mode": + continue + rows.append( + { + "renderer_mode": mode_name, + "n_trials": n_trials, + "total_samples": int(n_trials) * 20000, + **data, + } + ) + with open(output_path, "w", newline="", encoding="utf-8") as fh: writer = csv.DictWriter(fh, fieldnames=fieldnames) writer.writeheader() @@ -403,34 +421,30 @@ def main(output_dir: Path) -> None: """Orchestrate child processes and save results.""" output_dir.mkdir(parents=True, exist_ok=True) - print("Spawning software-renderer child process ...") - software_data = _spawn_child("software") - print("Spawning OpenGL-renderer child process ...") - opengl_data = _spawn_child("opengl") + results_dict = {} + modes = ["software_transparent", "software_opaque", "opengl_transparent", "opengl_opaque"] - if not software_data and not opengl_data: - print("ERROR: both child processes failed — no results to save.") - sys.exit(1) + for m in modes: + print(f"Spawning {m} child process ...") + results_dict[m] = _spawn_child(m) - _save_csv(opengl_data, software_data, output_dir / f"rendering_results_{_OS_TAG}.csv") - tagged_png = output_dir / f"rendering_benchmark_{_OS_TAG}.png" - _save_plot(opengl_data, software_data, tagged_png) - # Copy to canonical filename referenced by paper/paper.md (Figure 2) - canonical_png = output_dir / "rendering_benchmark.png" - if tagged_png.exists(): - import shutil as _shutil + if not any(results_dict.values()): + print("ERROR: all child processes failed — no results to save.") + sys.exit(1) - _shutil.copy2(tagged_png, canonical_png) - print(f"Copied canonical: {canonical_png}") + _save_csv(results_dict, output_dir / f"rendering_results_{_OS_TAG}.csv") print("\nSummary (median ms per update cycle):") - print(f" {'N trials':>8} {'Samples':>8} {'Software':>10} {'OpenGL':>8} {'Ratio (SW/GL)':>14}") + print( + f" {'N trials':>8} {'Samples':>8} {'SW_Trans':>10} {'SW_Opaque':>10} {'GL_Trans':>10} {'GL_Opaque':>10}" + ) for n in _N_TRIALS_LEVELS: key = str(n) - sw = software_data.get(key, {}).get("median_ms", float("nan")) - gl = opengl_data.get(key, {}).get("median_ms", float("nan")) - ratio = sw / gl if gl and gl > 0 else float("nan") - print(f" {n:>8} {n * 20000:>8} {sw:>10.2f} {gl:>8.2f} {ratio:>14.2f}") + st = results_dict["software_transparent"].get(key, {}).get("median_ms", float("nan")) + so = results_dict["software_opaque"].get(key, {}).get("median_ms", float("nan")) + gt = results_dict["opengl_transparent"].get(key, {}).get("median_ms", float("nan")) + go = results_dict["opengl_opaque"].get(key, {}).get("median_ms", float("nan")) + print(f" {n:>8} {n * 20000:>8} {st:>10.2f} {so:>10.2f} {gt:>10.2f} {go:>10.2f}") if __name__ == "__main__": @@ -438,12 +452,12 @@ def main(output_dir: Path) -> None: parser.add_argument( "--output-dir", type=Path, - default=_REPO_ROOT / "paper" / "results", - help="Output directory for CSV and PNG (default: paper/results/)", + default=_REPO_ROOT / "benchmarks" / "results", + help="Output directory for CSV and PNG (default: benchmarks/results/)", ) parser.add_argument( "--_child", - choices=["opengl", "software"], + choices=["opengl_transparent", "opengl_opaque", "software_transparent", "software_opaque"], help=argparse.SUPPRESS, # internal flag used by subprocess invocation ) args = parser.parse_args() diff --git a/src/Synaptipy/application/session_manager.py b/src/Synaptipy/application/session_manager.py index 4d6fb2c0..930da28a 100644 --- a/src/Synaptipy/application/session_manager.py +++ b/src/Synaptipy/application/session_manager.py @@ -234,7 +234,7 @@ def performance_settings(self, settings: Dict[str, Any]) -> None: log.warning("performance_settings must be a dict, got %s.", type(settings).__name__) return self._performance_settings.update(settings) - log.debug("SessionManager: performance_settings updated: %s", self._performance_settings) + log.debug(f"SessionManager: performance_settings updated: {self._performance_settings}") self.preferences_changed.emit(dict(self._performance_settings)) def emit_preferences_changed(self) -> None: diff --git a/src/Synaptipy/shared/plot_customization.py b/src/Synaptipy/shared/plot_customization.py index c6047e76..bbc8a10f 100644 --- a/src/Synaptipy/shared/plot_customization.py +++ b/src/Synaptipy/shared/plot_customization.py @@ -315,15 +315,26 @@ def get_average_pen(self) -> pg.mkPen: self._cache_pen("average", pen) return pen - def get_single_trial_pen(self) -> pg.mkPen: + def get_single_trial_pen(self, trial_index: int = 0) -> pg.mkPen: """Get pen for single trial plots.""" - # Check cache first - cached_pen = self._get_cached_pen("single_trial") + # Use trial index to cycle through colors if not specifically requested + from Synaptipy.shared.styling import PLOT_COLORS + + # Check cache first for this specific index + cache_key = f"single_trial_{trial_index}" + cached_pen = self._get_cached_pen(cache_key) if cached_pen: return cached_pen # Create new pen with proper opacity handling - color_str = self.defaults["single_trial"]["color"] + # Use the customized single trial color as default, or cycle through PLOT_COLORS + base_color_str = self.defaults["single_trial"]["color"] + # If it's the default blue or matplotlib blue, use our new colorblind-safe cycle instead + if base_color_str.lower() in ["#377eb8", "#377eb8"]: + color_str = PLOT_COLORS[trial_index % len(PLOT_COLORS)] + else: + color_str = base_color_str + try: width = float(self.defaults["single_trial"]["width"]) except (ValueError, TypeError): @@ -359,7 +370,7 @@ def get_single_trial_pen(self) -> pg.mkPen: f"Created single trial pen: color={color}, width={width}, alpha={color.alpha()} " f"(opacity: {opacity}%, alpha: {alpha:.3f}, force_opaque: {_force_opaque_trials})" ) - self._cache_pen("single_trial", pen) + self._cache_pen(cache_key, pen) return pen def get_grid_pen(self) -> Optional[pg.mkPen]: @@ -736,9 +747,9 @@ def get_average_pen() -> pg.mkPen: return get_plot_customization_manager().get_average_pen() -def get_single_trial_pen() -> pg.mkPen: +def get_single_trial_pen(trial_index: int = 0) -> pg.mkPen: """Get pen for single trial plots.""" - return get_plot_customization_manager().get_single_trial_pen() + return get_plot_customization_manager().get_single_trial_pen(trial_index) def get_grid_pen() -> pg.mkPen: diff --git a/src/Synaptipy/shared/styling.py b/src/Synaptipy/shared/styling.py index 2ae4e94a..8c651ed6 100644 --- a/src/Synaptipy/shared/styling.py +++ b/src/Synaptipy/shared/styling.py @@ -69,12 +69,12 @@ def apply_stylesheet(app: QtWidgets.QApplication) -> QtWidgets.QApplication: # ============================================================================== -def get_trial_pen(): +def get_trial_pen(trial_index: int = 0): """Get pen for trial data.""" try: from .plot_customization import get_single_trial_pen - return get_single_trial_pen() + return get_single_trial_pen(trial_index) except ImportError: return pg.mkPen(color="b", width=1) @@ -293,14 +293,13 @@ def style_error_message(widget): # Basic color constants for backward compatibility PLOT_COLORS = [ - "#377eb8", # Blue (original trial color) - "#000000", # Black (average color) - "#2ecc71", # Green - "#f39c12", # Orange - "#9b59b6", # Purple - "#1abc9c", # Turquoise - "#34495e", # Dark gray - "#e67e22", # Darker orange + "#E69F00", # Orange + "#56B4E9", # Sky blue + "#009E73", # Bluish green + "#F0E442", # Yellow + "#0072B2", # Blue + "#D55E00", # Vermilion + "#CC79A7", # Reddish purple ] # Expose main functions diff --git a/tests/application/gui/test_explorer_refactor.py b/tests/application/gui/test_explorer_refactor.py index 6864838d..c0bfc592 100644 --- a/tests/application/gui/test_explorer_refactor.py +++ b/tests/application/gui/test_explorer_refactor.py @@ -84,8 +84,11 @@ def test_explorer_plotting(explorer_tab, qtbot): explorer_tab._display_recording(recording) # Check Plot Rebuild - assert "ch1" in explorer_tab.plot_canvas.channel_plots - assert len(explorer_tab.plot_canvas.channel_plot_data_items["ch1"]) > 0 + def check_load(): + assert "ch1" in explorer_tab.plot_canvas.channel_plots + assert len(explorer_tab.plot_canvas.channel_plot_data_items["ch1"]) > 0 + + qtbot.waitUntil(check_load, timeout=5000) # Pooling: overlay mode should not allocate new PlotDataItems on redraw overlay_pool = len(explorer_tab.plot_canvas.channel_plot_data_items["ch1"]) From c6b01f4219808b372f6a71890e942a4518761a58 Mon Sep 17 00:00:00 2001 From: Anzal Date: Mon, 1 Jun 2026 23:42:15 +0200 Subject: [PATCH 3/6] ci: fix test timeouts and formatting --- src/Synaptipy/infrastructure/file_readers/neo_adapter.py | 2 +- tests/application/gui/test_explorer_refactor.py | 2 +- tests/infrastructure/exporters/test_nwb_exporter.py | 6 +++++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Synaptipy/infrastructure/file_readers/neo_adapter.py b/src/Synaptipy/infrastructure/file_readers/neo_adapter.py index f86613ff..6e39f056 100644 --- a/src/Synaptipy/infrastructure/file_readers/neo_adapter.py +++ b/src/Synaptipy/infrastructure/file_readers/neo_adapter.py @@ -522,7 +522,7 @@ def read_recording( # noqa: C901 log.info("pyabf rescue succeeded for '%s'.", filepath.name) except ImportError: try: - from PySide6 import QtWidgets, QtCore + from PySide6 import QtCore, QtWidgets def _show_pyabf_warning(): QtWidgets.QMessageBox.warning( diff --git a/tests/application/gui/test_explorer_refactor.py b/tests/application/gui/test_explorer_refactor.py index c0bfc592..d7068281 100644 --- a/tests/application/gui/test_explorer_refactor.py +++ b/tests/application/gui/test_explorer_refactor.py @@ -88,7 +88,7 @@ def check_load(): assert "ch1" in explorer_tab.plot_canvas.channel_plots assert len(explorer_tab.plot_canvas.channel_plot_data_items["ch1"]) > 0 - qtbot.waitUntil(check_load, timeout=5000) + qtbot.waitUntil(check_load, timeout=20000) # Pooling: overlay mode should not allocate new PlotDataItems on redraw overlay_pool = len(explorer_tab.plot_canvas.channel_plot_data_items["ch1"]) diff --git a/tests/infrastructure/exporters/test_nwb_exporter.py b/tests/infrastructure/exporters/test_nwb_exporter.py index a344b7f7..9bf83680 100644 --- a/tests/infrastructure/exporters/test_nwb_exporter.py +++ b/tests/infrastructure/exporters/test_nwb_exporter.py @@ -100,7 +100,11 @@ def test_nwb_export_success(nwb_exporter_instance, mock_recording_for_export, va def test_nwb_export_missing_metadata(nwb_exporter_instance, mock_recording_for_export, tmp_path): """Test export fails if required metadata is missing.""" output_file = tmp_path / "test_fail.nwb" - invalid_metadata = {"identifier": "xyz", "subject_id": "SUBJ_123", "device_description": "Axopatch"} # Missing description and start time + invalid_metadata = { + "identifier": "xyz", + "subject_id": "SUBJ_123", + "device_description": "Axopatch", + } # Missing description and start time with pytest.raises(ValueError, match="Missing required NWB MINDS session metadata"): nwb_exporter_instance.export(mock_recording_for_export, output_file, invalid_metadata) From 8ae42bf5fa7a503b9b303eb5502da48061edf085 Mon Sep 17 00:00:00 2001 From: Anzal Date: Tue, 2 Jun 2026 00:38:00 +0200 Subject: [PATCH 4/6] chore: add documentation auditing tools and standardize project documentation style --- README.md | 42 +-- check_docs.py | 25 ++ clean_slop.py | 52 ++++ doc_check.txt | 291 ++++++++++++++++++ docs/algorithmic_definitions.md | 31 +- docs/developer_guide.md | 20 ++ docs/user_guide.md | 2 + .../core/analysis/synaptic_events.py | 54 +++- 8 files changed, 483 insertions(+), 34 deletions(-) create mode 100644 check_docs.py create mode 100644 clean_slop.py create mode 100644 doc_check.txt diff --git a/README.md b/README.md index 01544afb..536c473c 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ synaptipy python -m Synaptipy ``` -![Synaptipy Explorer — multi-trial current-clamp recording with action potentials](https://raw.githubusercontent.com/anzalks/synaptipy/main/docs/tutorial/screenshots/explorer_tab.png) +![Synaptipy Explorer - multi-trial current-clamp recording with action potentials](https://raw.githubusercontent.com/anzalks/synaptipy/main/docs/tutorial/screenshots/explorer_tab.png) Load a recording by dragging a file into the **Explorer** tab, then navigate to the **Analyser** tab to select a channel and run an analysis. Results are displayed in a table and can be exported to CSV. @@ -261,42 +261,42 @@ are listed below; see the [full references page](https://synaptipy.readthedocs.i in the documentation for a complete annotated bibliography. **Action potential detection and kinetics:** -- Bean BP (2007). The action potential in mammalian central neurons. *Nat Rev Neurosci* 8:451-465. [doi:10.1038/nrn2148](https://doi.org/10.1038/nrn2148) — dV/dt threshold default (20 V/s) -- Hodgkin AL & Huxley AF (1952). *J Physiol* 117:500-544. [doi:10.1113/jphysiol.1952.sp004764](https://doi.org/10.1113/jphysiol.1952.sp004764) — foundational AP model -- Sekerli M et al. (2004). *IEEE Trans Biomed Eng* 51:1665-1672. [doi:10.1109/TBME.2004.827827](https://doi.org/10.1109/TBME.2004.827827) — maximum-curvature threshold method -- Naundorf B et al. (2006). *Nature* 440:1060-1063. [doi:10.1038/nature04610](https://doi.org/10.1038/nature04610) — artifact ceiling (300 V/s) +- Bean BP (2007). The action potential in mammalian central neurons. *Nat Rev Neurosci* 8:451-465. [doi:10.1038/nrn2148](https://doi.org/10.1038/nrn2148) - dV/dt threshold default (20 V/s) +- Hodgkin AL & Huxley AF (1952). *J Physiol* 117:500-544. [doi:10.1113/jphysiol.1952.sp004764](https://doi.org/10.1113/jphysiol.1952.sp004764) - foundational AP model +- Sekerli M et al. (2004). *IEEE Trans Biomed Eng* 51:1665-1672. [doi:10.1109/TBME.2004.827827](https://doi.org/10.1109/TBME.2004.827827) - maximum-curvature threshold method +- Naundorf B et al. (2006). *Nature* 440:1060-1063. [doi:10.1038/nature04610](https://doi.org/10.1038/nature04610) - artifact ceiling (300 V/s) **Passive membrane properties:** -- Hamill OP et al. (1981). *Pflugers Arch* 391:85-100. [doi:10.1007/BF00656997](https://doi.org/10.1007/BF00656997) — patch-clamp; series resistance and capacitance -- Robinson RB & Siegelbaum SA (2003). *Annu Rev Physiol* 65:453-480. [doi:10.1146/annurev.physiol.65.092101.142734](https://doi.org/10.1146/annurev.physiol.65.092101.142734) — HCN / Ih current; peak vs. steady-state Rᵢₙ +- Hamill OP et al. (1981). *Pflugers Arch* 391:85-100. [doi:10.1007/BF00656997](https://doi.org/10.1007/BF00656997) - patch-clamp; series resistance and capacitance +- Robinson RB & Siegelbaum SA (2003). *Annu Rev Physiol* 65:453-480. [doi:10.1146/annurev.physiol.65.092101.142734](https://doi.org/10.1146/annurev.physiol.65.092101.142734) - HCN / Ih current; peak vs. steady-state Rᵢₙ **After-hyperpolarisation:** -- Storm JF (1987). *J Physiol* 385:733-759. [doi:10.1113/jphysiol.1987.sp016517](https://doi.org/10.1113/jphysiol.1987.sp016517) — fast AHP (BK, 1-5 ms) -- Sah P & Faber ESL (2002). *Prog Neurobiol* 66:345-353. [doi:10.1016/S0301-0082(02)00004-7](https://doi.org/10.1016/S0301-0082(02)00004-7) — medium AHP (SK, 10-50 ms) +- Storm JF (1987). *J Physiol* 385:733-759. [doi:10.1113/jphysiol.1987.sp016517](https://doi.org/10.1113/jphysiol.1987.sp016517) - fast AHP (BK, 1-5 ms) +- Sah P & Faber ESL (2002). *Prog Neurobiol* 66:345-353. [doi:10.1016/S0301-0082(02)00004-7](https://doi.org/10.1016/S0301-0082(02)00004-7) - medium AHP (SK, 10-50 ms) **Spike-train statistics:** -- Holt GR et al. (1996). *J Neurophysiol* 75:1806-1814. [doi:10.1152/jn.1996.75.5.1806](https://doi.org/10.1152/jn.1996.75.5.1806) — CV and CV₂ -- Shinomoto S et al. (2003). *Neural Comput* 15:2823-2842. [doi:10.1162/089976603322518759](https://doi.org/10.1162/089976603322518759) — Local Variation (LV) +- Holt GR et al. (1996). *J Neurophysiol* 75:1806-1814. [doi:10.1152/jn.1996.75.5.1806](https://doi.org/10.1152/jn.1996.75.5.1806) - CV and CV₂ +- Shinomoto S et al. (2003). *Neural Comput* 15:2823-2842. [doi:10.1162/089976603322518759](https://doi.org/10.1162/089976603322518759) - Local Variation (LV) **Burst detection:** -- Grace AA & Bunney BS (1984). *J Neurosci* 4:2877-2890. [doi:10.1523/JNEUROSCI.04-11-02877.1984](https://doi.org/10.1523/JNEUROSCI.04-11-02877.1984) — ISI burst criterion -- Harris KD et al. (2001). *Neuron* 32:141-149. [doi:10.1016/S0896-6273(01)00447-0](https://doi.org/10.1016/S0896-6273(01)00447-0) — dynamic ISI fraction (30%) +- Grace AA & Bunney BS (1984). *J Neurosci* 4:2877-2890. [doi:10.1523/JNEUROSCI.04-11-02877.1984](https://doi.org/10.1523/JNEUROSCI.04-11-02877.1984) - ISI burst criterion +- Harris KD et al. (2001). *Neuron* 32:141-149. [doi:10.1016/S0896-6273(01)00447-0](https://doi.org/10.1016/S0896-6273(01)00447-0) - dynamic ISI fraction (30%) **Synaptic event detection:** -- Rall W (1967). *J Neurophysiol* 30:1138-1168. [doi:10.1152/jn.1967.30.5.1138](https://doi.org/10.1152/jn.1967.30.5.1138) — cable theory; dendritic filtering (2-3x tau) -- Hampel FR (1974). *J Am Stat Assoc* 69:383-393. [doi:10.1080/01621459.1974.10482962](https://doi.org/10.1080/01621459.1974.10482962) — MAD noise estimator (1.4826 factor) +- Rall W (1967). *J Neurophysiol* 30:1138-1168. [doi:10.1152/jn.1967.30.5.1138](https://doi.org/10.1152/jn.1967.30.5.1138) - cable theory; dendritic filtering (2-3x tau) +- Hampel FR (1974). *J Am Stat Assoc* 69:383-393. [doi:10.1080/01621459.1974.10482962](https://doi.org/10.1080/01621459.1974.10482962) - MAD noise estimator (1.4826 factor) **Paired-pulse ratio:** -- Zucker RS & Regehr WG (2002). *Annu Rev Physiol* 64:355-405. [doi:10.1146/annurev.physiol.64.092501.114547](https://doi.org/10.1146/annurev.physiol.64.092501.114547) — short-term synaptic plasticity -- Regehr WG (2012). *Cold Spring Harb Perspect Biol* 4:a005702. [doi:10.1101/cshperspect.a005702](https://doi.org/10.1101/cshperspect.a005702) — PPR facilitation/depression classification +- Zucker RS & Regehr WG (2002). *Annu Rev Physiol* 64:355-405. [doi:10.1146/annurev.physiol.64.092501.114547](https://doi.org/10.1146/annurev.physiol.64.092501.114547) - short-term synaptic plasticity +- Regehr WG (2012). *Cold Spring Harb Perspect Biol* 4:a005702. [doi:10.1101/cshperspect.a005702](https://doi.org/10.1101/cshperspect.a005702) - PPR facilitation/depression classification **Signal filtering:** -- Savitzky A & Golay MJE (1964). *Anal Chem* 36:1627-1639. [doi:10.1021/ac60214a047](https://doi.org/10.1021/ac60214a047) — Savitzky-Golay smoothing -- Welch PD (1967). *IEEE Trans Audio Electroacoust* 15:70-73. [doi:10.1109/TAU.1967.1161901](https://doi.org/10.1109/TAU.1967.1161901) — Welch PSD / line noise detection +- Savitzky A & Golay MJE (1964). *Anal Chem* 36:1627-1639. [doi:10.1021/ac60214a047](https://doi.org/10.1021/ac60214a047) - Savitzky-Golay smoothing +- Welch PD (1967). *IEEE Trans Audio Electroacoust* 15:70-73. [doi:10.1109/TAU.1967.1161901](https://doi.org/10.1109/TAU.1967.1161901) - Welch PSD / line noise detection **Electrode corrections:** -- Barry PH & Lynch JW (1991). *J Membr Biol* 121:101-117. [doi:10.1007/BF01870526](https://doi.org/10.1007/BF01870526) — liquid junction potential correction -- Armstrong CM & Bezanilla F (1977). *J Gen Physiol* 70:567-590. [doi:10.1085/jgp.70.5.567](https://doi.org/10.1085/jgp.70.5.567) — P/N leak subtraction protocol +- Barry PH & Lynch JW (1991). *J Membr Biol* 121:101-117. [doi:10.1007/BF01870526](https://doi.org/10.1007/BF01870526) - liquid junction potential correction +- Armstrong CM & Bezanilla F (1977). *J Gen Physiol* 70:567-590. [doi:10.1085/jgp.70.5.567](https://doi.org/10.1085/jgp.70.5.567) - P/N leak subtraction protocol --- diff --git a/check_docs.py b/check_docs.py new file mode 100644 index 00000000..f102bc89 --- /dev/null +++ b/check_docs.py @@ -0,0 +1,25 @@ +import ast +import os +import glob +from collections import defaultdict + +def check_file(filepath): + with open(filepath, 'r') as f: + content = f.read() + + tree = ast.parse(content) + + for node in ast.walk(tree): + if isinstance(node, ast.FunctionDef): + args = [arg.arg for arg in node.args.args if arg.arg not in ('self', 'cls')] + args += [arg.arg for arg in node.args.kwonlyargs] + + docstring = ast.get_docstring(node) + if docstring: + for arg in args: + # basic check: is the arg mentioned in the docstring? + # A more rigorous check parses the Google/Numpy style. + # We will just look for `arg:` or `arg ` or `*arg*` etc. + # Actually, if we use a regex: + pass + diff --git a/clean_slop.py b/clean_slop.py new file mode 100644 index 00000000..24a778d0 --- /dev/null +++ b/clean_slop.py @@ -0,0 +1,52 @@ +import re +import os + +FILES = [ + "README.md", + "docs/user_guide.md", + "docs/developer_guide.md", + "docs/algorithmic_definitions.md" +] + +def clean_slop(text): + # It is important to note that -> remove and capitalize + def repl_important(m): + next_char = m.group(1) + return next_char.upper() + text = re.sub(r'(?i)It is important to note that\s+([a-z])', repl_important, text) + text = re.sub(r'(?i)It is important to note that\s*', '', text) + + # Seamlessly integrates -> Integrates + text = re.sub(r'(?i)Seamlessly integrates', 'Integrates', text) + text = re.sub(r'(?i)seamlessly integrates', 'integrates', text) + + # Empowers users to -> Allows users to + text = re.sub(r'(?i)Empowers users to', 'Allows users to', text) + text = re.sub(r'(?i)empowers users to', 'allows users to', text) + + # In conclusion, -> remove + def repl_conclusion(m): + next_char = m.group(1) + return next_char.upper() + text = re.sub(r'(?i)In conclusion,\s+([a-z])', repl_conclusion, text) + text = re.sub(r'(?i)In conclusion,\s*', '', text) + + # Replace em-dashes + # usually surrounded by spaces: ` — ` -> ` - ` + text = text.replace(' — ', ' - ') + # sometimes not: `word—word` -> `word - word` + text = text.replace('—', ' - ') + + return text + +for fpath in FILES: + if os.path.exists(fpath): + with open(fpath, "r") as f: + content = f.read() + new_content = clean_slop(content) + if content != new_content: + with open(fpath, "w") as f: + f.write(new_content) + print(f"Cleaned {fpath}") + else: + print(f"File not found: {fpath}") diff --git a/doc_check.txt b/doc_check.txt new file mode 100644 index 00000000..dcf6ce0a --- /dev/null +++ b/doc_check.txt @@ -0,0 +1,291 @@ +src/Synaptipy/core/analysis/synaptic_events.py::find_quiescent_baseline_rms: + In doc, not in sig: {'variance', 'Unlike', 'Tuple', 'window', 'considers', 'define', 'robust', 'Identify'} +src/Synaptipy/core/analysis/synaptic_events.py::calculate_event_charge_dynamic: + In doc, not in sig: {'Signed', 'The', 'summating', 'Integrate', 'early'} +src/Synaptipy/core/analysis/synaptic_events.py::fit_biexponential_decay: + In doc, not in sig: {'with', 'Dict', 'Tries', 'string', 'amplitudes', 'Fit'} +src/Synaptipy/core/analysis/synaptic_events.py::compute_local_pre_event_baseline: + In doc, not in sig: {'peak', 'the', 'Compute', 'window', 'and', 'searches', 'begins', 'For'} +src/Synaptipy/core/analysis/synaptic_events.py::_fit_p1_decay_residual: + In doc, not in sig: {'more', 'the', 'Tuple', 'back', 'Fit'} + In sig, not in doc: {'sample_rate', 'global_baseline', 'data', 'peak1_idx', 's2_t', 'time'} +src/Synaptipy/core/analysis/synaptic_events.py::_measure_ppr_peak: + In doc, not in sig: {'Return'} + In sig, not in doc: {'baseline', 'resp_samples', 'data', 'onset_s', 'polarity', 'time'} +src/Synaptipy/core/analysis/synaptic_events.py::calculate_paired_pulse_ratio: + In doc, not in sig: {'baseline', 'Keys', 'Sampling', 'tail', 'Duration', 'Calculate', 'PPR'} +src/Synaptipy/core/analysis/synaptic_events.py::detect_events_threshold: + In doc, not in sig: {'Detect', 'noise', 'activity'} + In sig, not in doc: {'rolling_baseline_window_ms', 'threshold', 'refractory_period', 'data', 'quiescent_window_ms', 'polarity', 'use_quiescent_noise_floor', 'artifact_mask', 'time'} +src/Synaptipy/core/analysis/synaptic_events.py::run_event_detection_threshold_wrapper: + In doc, not in sig: {'Wrapper'} + In sig, not in doc: {'sampling_rate', 'data', 'time'} +src/Synaptipy/core/analysis/synaptic_events.py::detect_events_template: + In doc, not in sig: {'Detect', 'for', 'somatic', 'filtering', 'filtered', 'Kernels'} + In sig, not in doc: {'kernel_shape', 'rolling_baseline_window_ms', 'tau_rise', 'data', 'kernel_multipliers', 'tau_decay', 'sampling_rate', 'polarity', 'threshold_std', 'min_event_distance_ms', 'artifact_mask', 'time'} +src/Synaptipy/core/analysis/synaptic_events.py::run_event_detection_template_wrapper: + In doc, not in sig: {'Wrapper'} + In sig, not in doc: {'sampling_rate', 'data', 'time'} +src/Synaptipy/core/analysis/synaptic_events.py::_find_stable_baseline_segment: + In doc, not in sig: {'Find'} + In sig, not in doc: {'sample_rate', 'data', 'window_duration_s', 'step_duration_s'} +src/Synaptipy/core/analysis/synaptic_events.py::detect_events_baseline_peak_kinetics: + In doc, not in sig: {'Minimum', 'Raw', 'Detect', 'Step', 'Window', 'Whether', 'Sampling', 'Custom', 'Detection'} +src/Synaptipy/core/analysis/synaptic_events.py::run_event_detection_baseline_peak_wrapper: + In doc, not in sig: {'Wrapper'} + In sig, not in doc: {'sampling_rate', 'data', 'time'} +src/Synaptipy/core/analysis/synaptic_events.py::_build_kernel: + In doc, not in sig: {'Build'} + In sig, not in doc: {'td'} +src/Synaptipy/core/analysis/registry.py::register: + In doc, not in sig: {'documentation', 'return', 'this', 'This', 'when', 'requires', 'def', 'expects_list', 'engine', 'The', 'from', 'Decorator', 'synaptic', 'NumPy'} +src/Synaptipy/core/analysis/registry.py::register_processor: + In doc, not in sig: {'Decorator', 'Alias'} + In sig, not in doc: {'name'} +src/Synaptipy/core/analysis/registry.py::get_function: + In doc, not in sig: {'Retrieve', 'The'} +src/Synaptipy/core/analysis/registry.py::get_metadata: + In doc, not in sig: {'Retrieve', 'Dictionary'} +src/Synaptipy/core/analysis/registry.py::list_registered: + In doc, not in sig: {'Get', 'List'} +src/Synaptipy/core/analysis/registry.py::list_by_type: + In doc, not in sig: {'Get', 'List'} +src/Synaptipy/core/analysis/registry.py::list_preprocessing: + In doc, not in sig: {'Get'} +src/Synaptipy/core/analysis/registry.py::list_analysis: + In doc, not in sig: {'Get'} +src/Synaptipy/core/analysis/registry.py::mark_core_snapshot: + In doc, not in sig: {'Call', 'Record', 'uses'} +src/Synaptipy/core/analysis/registry.py::unregister_plugins: + In doc, not in sig: {'Safe', 'Remove', 'last'} +src/Synaptipy/core/analysis/registry.py::clear: + In doc, not in sig: {'Clear'} +src/Synaptipy/core/analysis/registry.py::update_default_params: + In doc, not in sig: {'Update'} +src/Synaptipy/core/analysis/registry.py::reset_to_factory: + In doc, not in sig: {'Reset'} + In sig, not in doc: {'analysis_name'} +src/Synaptipy/core/analysis/passive_properties.py::apply_ljp_correction: + In doc, not in sig: {'unnecessary', 'Corrected', 'Return', 'When'} +src/Synaptipy/core/analysis/passive_properties.py::_sag_nan_payload: + In doc, not in sig: {'Return'} +src/Synaptipy/core/analysis/passive_properties.py::calculate_rmp: + In doc, not in sig: {'samples', 'baseline', 'Attributes', 'Calculate', 'fits', 'length'} +src/Synaptipy/core/analysis/passive_properties.py::calculate_baseline_stats: + In doc, not in sig: {'Return'} + In sig, not in doc: {'start_time', 'voltage', 'time', 'end_time'} +src/Synaptipy/core/analysis/passive_properties.py::find_stable_baseline: + In doc, not in sig: {'Find'} + In sig, not in doc: {'sample_rate', 'data', 'window_duration_s', 'step_duration_s'} +src/Synaptipy/core/analysis/passive_properties.py::calculate_rin: + In doc, not in sig: {'Three', 'the', 'negative', 'Attributes', 'Arbitrary', 'where', 'window', 'from', 'Amplitude', 'Duration', 'The', 'Calculate', 'Uses', 'samples', 'excluded'} +src/Synaptipy/core/analysis/passive_properties.py::_fit_vc_transient_decay: + In doc, not in sig: {'Fit'} + In sig, not in doc: {'decay_segment', 't_decay', 'i_peak', 'cm_charge_pf', 'rs_mohm'} +src/Synaptipy/core/analysis/passive_properties.py::calculate_vc_transient_parameters: + In doc, not in sig: {'Time', 'Keys', 'Amplitude', 'Duration', 'tau_c', 'All', 'whose', 'Fit', 'and'} +src/Synaptipy/core/analysis/passive_properties.py::_extrapolate_rs_at_t0: + In doc, not in sig: {'Extrapolates', 'Fit', 'instantaneous', 'Falls'} + In sig, not in doc: {'t_art', 'artifact_window_ms', 'v_art'} +src/Synaptipy/core/analysis/passive_properties.py::calculate_cc_series_resistance_fast: + In doc, not in sig: {'Time', 'the', 'Estimate', 'Keys', 'series', 'Membrane', 'derived', 'Amplitude', 'Duration', 'When', 'Values', 'Input', 'derive'} +src/Synaptipy/core/analysis/passive_properties.py::calculate_conductance: + In doc, not in sig: {'Calculate'} + In sig, not in doc: {'parameters', 'baseline_window', 'time_vector', 'voltage_step', 'response_window', 'current_trace'} +src/Synaptipy/core/analysis/passive_properties.py::calculate_iv_curve: + In doc, not in sig: {'Calculate'} + In sig, not in doc: {'time_vectors', 'sweeps', 'baseline_window', 'response_window', 'current_steps'} +src/Synaptipy/core/analysis/passive_properties.py::calculate_tau: + In doc, not in sig: {'avoiding', 'dict', 'hyperpolarising', 'values', 'hippocampal', 'prevent', 'amplitude', 'Exponential', 'Onset', 'most', 'window', 'second', 'Duration', 'Calculate', 'applied', 'and', 'capacitive', 'For'} +src/Synaptipy/core/analysis/passive_properties.py::calculate_sag_ratio: + In doc, not in sig: {'Calculate'} + In sig, not in doc: {'baseline_window', 'time_vector', 'peak_smoothing_ms', 'response_peak_window', 'voltage_trace', 'rebound_window_ms', 'response_steady_state_window'} +src/Synaptipy/core/analysis/passive_properties.py::calculate_capacitance_cc: + In doc, not in sig: {'input', 'the', 'approximation', 'Membrane', 'Without', 'When', 'Calculate'} +src/Synaptipy/core/analysis/passive_properties.py::_fit_cm_from_transient: + In doc, not in sig: {'Fit'} + In sig, not in doc: {'t_trans', 't_decay', 'rs_mohm', 'voltage_step_amplitude_mv', 'delta_i', 'i_decay'} +src/Synaptipy/core/analysis/passive_properties.py::calculate_capacitance_vc: + In doc, not in sig: {'exponential', 'Dict', 'Method', 'Falls', 'Calculate'} +src/Synaptipy/core/analysis/passive_properties.py::_coerce_trial_lists: + In doc, not in sig: {'Normalise'} + In sig, not in doc: {'data_list', 'time_list'} +src/Synaptipy/core/analysis/passive_properties.py::_resolve_sweep_baseline: + In doc, not in sig: {'Return'} + In sig, not in doc: {'window_duration', 'sweep_time', 'baseline_start', 'step_duration', 'auto_detect', 'sampling_rate', 'rs_artifact_blanking_ms', 'sweep_data', 'baseline_end'} +src/Synaptipy/core/analysis/passive_properties.py::run_rmp_analysis_wrapper: + In doc, not in sig: {'Wrapper'} + In sig, not in doc: {'data_list', 'sampling_rate', 'time_list'} +src/Synaptipy/core/analysis/passive_properties.py::run_sag_ratio_wrapper: + In doc, not in sig: {'Wrapper'} + In sig, not in doc: {'sampling_rate', 'data', 'time'} +src/Synaptipy/core/analysis/passive_properties.py::run_rin_analysis_wrapper: + In doc, not in sig: {'Wrapper'} + In sig, not in doc: {'sampling_rate', 'data', 'time'} +src/Synaptipy/core/analysis/passive_properties.py::run_tau_analysis_wrapper: + In doc, not in sig: {'Wrapper'} + In sig, not in doc: {'sampling_rate', 'data', 'time'} +src/Synaptipy/core/analysis/passive_properties.py::run_iv_curve_wrapper: + In doc, not in sig: {'Wrapper'} + In sig, not in doc: {'data_list', 'sampling_rate', 'time_list'} +src/Synaptipy/core/analysis/passive_properties.py::run_capacitance_analysis_wrapper: + In doc, not in sig: {'Wrapper'} + In sig, not in doc: {'sampling_rate', 'data', 'time'} +src/Synaptipy/core/analysis/firing_dynamics.py::calculate_fi_curve: + In doc, not in sig: {'Calculate', 'Dictionary'} +src/Synaptipy/core/analysis/firing_dynamics.py::run_excitability_analysis_wrapper: + In doc, not in sig: {'Wrapper'} + In sig, not in doc: {'data_list', 'sampling_rate', 'time_list'} +src/Synaptipy/core/analysis/firing_dynamics.py::calculate_bursts_logic: + In doc, not in sig: {'This', 'Detect', 'BurstResult', 'define', 'temporal'} + In sig, not in doc: {'parameters'} +src/Synaptipy/core/analysis/firing_dynamics.py::analyze_spikes_and_bursts: + In doc, not in sig: {'Detect'} + In sig, not in doc: {'max_isi_start', 'parameters', 'burst_isi_fraction', 'threshold', 'dynamic_burst', 'data', 'sampling_rate', 'refractory_ms', 'max_isi_end', 'time'} +src/Synaptipy/core/analysis/firing_dynamics.py::run_burst_analysis_wrapper: + In doc, not in sig: {'Wrapper'} + In sig, not in doc: {'sampling_rate', 'data', 'time'} +src/Synaptipy/core/analysis/firing_dynamics.py::calculate_train_dynamics: + In doc, not in sig: {'Compute'} +src/Synaptipy/core/analysis/firing_dynamics.py::run_train_dynamics_wrapper: + In doc, not in sig: {'Wrapper'} + In sig, not in doc: {'sampling_rate', 'data', 'time'} +src/Synaptipy/core/analysis/batch_engine.py::_worker_process_file: + In doc, not in sig: {'This', 'List', 'OOM', 'Process', 'recursive', 'that'} +src/Synaptipy/core/analysis/batch_engine.py::__init__: + In doc, not in sig: {'Initialize', 'Values'} +src/Synaptipy/core/analysis/batch_engine.py::cancel: + In doc, not in sig: {'Request'} +src/Synaptipy/core/analysis/batch_engine.py::update_performance_settings: + In doc, not in sig: {'Dynamically', 'This', 'immediately', 'Reads'} +src/Synaptipy/core/analysis/batch_engine.py::list_available_analyses: + In doc, not in sig: {'Get', 'List'} +src/Synaptipy/core/analysis/batch_engine.py::get_analysis_info: + In doc, not in sig: {'Get', 'Dictionary'} +src/Synaptipy/core/analysis/batch_engine.py::_sanitise_value: + In doc, not in sig: {'Tuple', 'Sanitise'} + In sig, not in doc: {'key', 'value'} +src/Synaptipy/core/analysis/batch_engine.py::_sanitise_ndarray: + In doc, not in sig: {'Summarise'} + In sig, not in doc: {'key', 'value'} +src/Synaptipy/core/analysis/batch_engine.py::_sanitise_long_list: + In doc, not in sig: {'Summarise'} + In sig, not in doc: {'key', 'value'} +src/Synaptipy/core/analysis/batch_engine.py::_sanitise_result_for_export: + In doc, not in sig: {'Cleaned', 'Make', 'tables', 'under', 'object', 'both'} + In sig, not in doc: {'result'} +src/Synaptipy/core/analysis/batch_engine.py::_recording_metadata: + In doc, not in sig: {'Includes', 'that', 'Extract'} + In sig, not in doc: {'recording'} +src/Synaptipy/core/analysis/batch_engine.py::_order_columns: + In doc, not in sig: {'Reorder'} + In sig, not in doc: {'df'} +src/Synaptipy/core/analysis/batch_engine.py::_append_batch_error_log: + In doc, not in sig: {'Append', 'Writing', 'aborted'} +src/Synaptipy/core/analysis/batch_engine.py::_run_batch_parallel: + In doc, not in sig: {'Distribute', 'are', 'through', 'OOM', 'Each'} + In sig, not in doc: {'files', 'pipeline_config', 'channel_filter', 'progress_callback'} +src/Synaptipy/core/analysis/batch_engine.py::run_batch: + In doc, not in sig: {'blocked', 'pandas', 'Run', 'When'} + In sig, not in doc: {'cross_file_average'} +src/Synaptipy/core/analysis/batch_engine.py::_run_cross_file_average: + In doc, not in sig: {'with', 'then', 'Aggregate', 'The'} + In sig, not in doc: {'files', 'pipeline_config', 'channel_filter', 'progress_callback'} +src/Synaptipy/core/analysis/batch_engine.py::_run_batch_sequential: + In doc, not in sig: {'Sequential'} + In sig, not in doc: {'files', 'progress_callback', 'rs_tolerance', 'channel_filter', 'pipeline_config'} +src/Synaptipy/core/analysis/batch_engine.py::_process_task: + In doc, not in sig: {'Tuple', 'Process'} +src/Synaptipy/core/analysis/epoch_manager.py::duration: + In doc, not in sig: {'Epoch'} +src/Synaptipy/core/analysis/epoch_manager.py::contains: + In doc, not in sig: {'Return'} + In sig, not in doc: {'t'} +src/Synaptipy/core/analysis/epoch_manager.py::epochs: + In doc, not in sig: {'Sorted'} +src/Synaptipy/core/analysis/epoch_manager.py::epoch_names: + In doc, not in sig: {'Names'} +src/Synaptipy/core/analysis/epoch_manager.py::add_manual_epoch: + In doc, not in sig: {'Add', 'ValueError', 'The'} +src/Synaptipy/core/analysis/epoch_manager.py::from_ttl: + In doc, not in sig: {'Detects', 'then', 'List'} + In sig, not in doc: {'post_stim_s', 'ttl_threshold', 'ttl_data', 'min_inter_epoch_s', 'pre_stim_s', 'stim_name', 'washout_name', 'time', 'baseline_name'} +src/Synaptipy/core/analysis/epoch_manager.py::get_epoch: + In doc, not in sig: {'Return'} + In sig, not in doc: {'name'} +src/Synaptipy/core/analysis/epoch_manager.py::epochs_at_time: + In doc, not in sig: {'Return'} + In sig, not in doc: {'t'} +src/Synaptipy/core/analysis/epoch_manager.py::get_epoch_slices: + In doc, not in sig: {'Dict', 'Epochs', 'Extract'} +src/Synaptipy/core/analysis/epoch_manager.py::remove_epoch: + In doc, not in sig: {'Remove'} + In sig, not in doc: {'name'} +src/Synaptipy/core/analysis/epoch_manager.py::clear: + In doc, not in sig: {'Remove'} +src/Synaptipy/core/analysis/cross_file_utils.py::_resolve_effective_trials: + In doc, not in sig: {'Return', 'items'} + In sig, not in doc: {'parsed_trials', 'item', 'channel'} +src/Synaptipy/core/analysis/cross_file_utils.py::extract_per_file_trace: + In doc, not in sig: {'handle', 'across', 'contribute', 'Files', 'are', 'Load'} +src/Synaptipy/core/analysis/cross_file_utils.py::get_cross_file_average: + In doc, not in sig: {'different', 'with', 'own', 'Ordered', 'shared', 'decreases', 'Delegates', 'where', 'Compute', 'scientifically', 'Adapter', 'whose', 'that'} +src/Synaptipy/core/analysis/cross_file_utils.py::build_averaged_recording: + In doc, not in sig: {'calls', 'path', 'all', 'obtained', 'Recording', 'Short', 'Build', 'Populated', 'Adapter', 'For'} +src/Synaptipy/core/analysis/cross_file_utils.py::_make_mfa_label: + In doc, not in sig: {'replaced', 'Derive', 'Takes', 'Iterable'} +src/Synaptipy/core/analysis/cross_file_utils.py::average_padded_trials: + In doc, not in sig: {'artificial', 'Shorter', 'Compute', 'all', 'produces', 'Flat'} +src/Synaptipy/core/analysis/single_spike.py::detect_spikes_threshold: + In doc, not in sig: {'Minimum', 'Number', 'Detect', 'first', 'Attributes', 'Converted', 'spike', 'Arbitrary', 'Convert', 'voltage', 'crossing', 'within', 'fewer', 'upward'} +src/Synaptipy/core/analysis/single_spike.py::calculate_spike_features: + In doc, not in sig: {'Calculate', 'window', 'above'} +src/Synaptipy/core/analysis/single_spike.py::calculate_isi: + In doc, not in sig: {'Return'} + In sig, not in doc: {'spike_times'} +src/Synaptipy/core/analysis/single_spike.py::analyze_multi_sweep_spikes: + In doc, not in sig: {'Detect'} + In sig, not in doc: {'time_vector', 'threshold', 'refractory_samples', 'data_trials', 'dvdt_threshold'} +src/Synaptipy/core/analysis/single_spike.py::calculate_dvdt: + In doc, not in sig: {'the', 'Calculate', 'which', 'next', 'Computes'} +src/Synaptipy/core/analysis/single_spike.py::get_phase_plane_trajectory: + In doc, not in sig: {'Return'} + In sig, not in doc: {'sampling_rate', 'sigma_ms', 'voltage'} +src/Synaptipy/core/analysis/single_spike.py::detect_threshold_kink: + In doc, not in sig: {'Detect'} + In sig, not in doc: {'kink_slope', 'search_window_ms', 'peak_indices', 'voltage', 'sampling_rate', 'dvdt_threshold'} +src/Synaptipy/core/analysis/single_spike.py::run_spike_detection_wrapper: + In doc, not in sig: {'Wrapper'} + In sig, not in doc: {'threshold', 'refractory_period', 'data', 'ahp_window', 'sampling_rate', 'onset_lookback', 'peak_search_window', 'time', 'dvdt_threshold'} +src/Synaptipy/core/analysis/single_spike.py::phase_plane_analysis_wrapper: + In doc, not in sig: {'Wrapper'} + In sig, not in doc: {'voltage', 'sampling_rate', 'sigma_ms', 'time', 'dvdt_threshold'} +src/Synaptipy/core/analysis/single_spike.py::_window_min: + In doc, not in sig: {'Return'} + In sig, not in doc: {'end_s', 'start_s'} +src/Synaptipy/core/analysis/evoked_responses.py::extract_ttl_epochs: + In doc, not in sig: {'Tuple', 'Extract'} + In sig, not in doc: {'ttl_data', 'auto_threshold', 'time', 'threshold'} +src/Synaptipy/core/analysis/evoked_responses.py::_find_spikes_in_window: + In doc, not in sig: {'Vectorised'} + In sig, not in doc: {'spikes', 't_end', 't_start'} +src/Synaptipy/core/analysis/evoked_responses.py::calculate_optogenetic_sync: + In doc, not in sig: {'Correlate'} +src/Synaptipy/core/analysis/evoked_responses.py::_peak_pos_s: + In doc, not in sig: {'Searches', 'value', 'Tuple', 'Return', 'matching'} +src/Synaptipy/core/analysis/evoked_responses.py::calculate_paired_pulse_ratio: + In doc, not in sig: {'Calculate', 'baseline', 'Dict', 'when', 'yielding', 'under', 'Without', 'Should', 'Algorithm', 'response', 'stimulus'} +src/Synaptipy/core/analysis/evoked_responses.py::run_opto_sync_wrapper: + In doc, not in sig: {'Correlates', 'Wrapper'} + In sig, not in doc: {'sampling_rate', 'data', 'time'} +src/Synaptipy/core/analysis/evoked_responses.py::run_ppr_wrapper: + In doc, not in sig: {'Wrapper'} + In sig, not in doc: {'sampling_rate', 'data', 'time'} +src/Synaptipy/core/analysis/evoked_responses.py::calculate_stimulus_train_stp: + In doc, not in sig: {'preceding', 'Dictionary', 'yield', 'Compute', 'window', 'excluded', 'For'} +src/Synaptipy/core/analysis/evoked_responses.py::run_stimulus_train_stp_wrapper: + In doc, not in sig: {'generated', 'Wrapper', 'series', 'Stimulus', 'wrapper'} + In sig, not in doc: {'sampling_rate', 'data', 'time'} +src/Synaptipy/core/analysis/evoked_responses.py::_response_peak: + In doc, not in sig: {'Data', 'Return'} + In sig, not in doc: {'stim_onset_s', 'baseline'} diff --git a/docs/algorithmic_definitions.md b/docs/algorithmic_definitions.md index cd0b5e77..0e13280e 100644 --- a/docs/algorithmic_definitions.md +++ b/docs/algorithmic_definitions.md @@ -308,7 +308,7 @@ refractory period between crossings. The peak is the local maximum within *(See also §15.2 for the full maximum-curvature method; cited in Sekerli et al., 2004.)* *(Default threshold $\theta_{dV/dt}$ = 20 V/s, calibrated on rodent cortical pyramidal neurons; Bean, 2007.)* -*(Artifact ceiling: 300 V/s — above this value the rising phase is flagged as non-physiological; Naundorf et al., 2006.)* +*(Artifact ceiling: 300 V/s - above this value the rising phase is flagged as non-physiological; Naundorf et al., 2006.)* The action potential onset is defined as the first point where @@ -533,7 +533,12 @@ p < 0.003 under Gaussian noise assumption.)* Uses sliding-window variance minimisation to find the most stable baseline segment, estimates noise as $\hat{\sigma} = 1.4826 \times \text{MAD}$ (Hampel, 1974; Rousseeuw & Croux, 1993) in that region, and detects peaks -with prominence $\ge 0.5 \times \text{threshold}$. +with a specified prominence. The prominence is determined by `peak_prominence_factor` +if provided; otherwise, it defaults to $\ge 0.5 \times \text{threshold}$. + +A low-pass Butterworth filter can be applied prior to peak detection by +specifying `filter_freq_hz`. This attenuates high-frequency noise that might +cause spurious local maxima. ### 7.4 Local Pre-Event Baseline (Dynamic Amplitude for Summating Events) @@ -747,6 +752,10 @@ eliminating group delay distortion critical for preserving AP waveform timing: | Notch | $H(z)$ from `scipy.signal.iirnotch` at centre $f_0$, quality $Q$ | | Comb | Cascaded notch at $f_0, 2f_0, \ldots, Nf_0$ | +**Unit Enforcement:** All inputs passing through the `ProcessingPipeline` and +signal processors are strictly validated and tracked using the `quantities` package +to prevent dimensional mismatches. + ### 14.2 Baseline subtraction | Method | Formula | @@ -944,7 +953,7 @@ relative to the neighboring baseline band identifies mains interference. modified periodograms. *IEEE Transactions on Audio and Electroacoustics*, 15(2), 70-73. [doi:10.1109/TAU.1967.1161901](https://doi.org/10.1109/TAU.1967.1161901) - - **Used in:** §14.4 trace quality assessment — line noise detection at + - **Used in:** §14.4 trace quality assessment - line noise detection at 50/60 Hz via `scipy.signal.welch`; also used in the `compute_psd` utility in `signal_processor.py`. @@ -954,7 +963,7 @@ relative to the neighboring baseline band identifies mains interference. estimation. *Journal of the American Statistical Association*, 69(346), 383-393. [doi:10.1080/01621459.1974.10482962](https://doi.org/10.1080/01621459.1974.10482962) - - **Used in:** §7.1, §7.2, §7.3 — the 1.4826 consistency factor applied to + - **Used in:** §7.1, §7.2, §7.3 - the 1.4826 consistency factor applied to the Median Absolute Deviation (MAD) to obtain a Gaussian-consistent standard deviation estimate: $\hat{\sigma} = 1.4826 \times \text{MAD}$. The factor 1.4826 = $1 / \Phi^{-1}(0.75)$ ensures the estimator is @@ -964,7 +973,7 @@ relative to the neighboring baseline band identifies mains interference. absolute deviation. *Journal of the American Statistical Association*, 88(424), 1273-1283. [doi:10.1080/01621459.1993.10476408](https://doi.org/10.1080/01621459.1993.10476408) - - **Used in:** MAD noise estimator (§7.1, §7.2, §7.3) — robustness + - **Used in:** MAD noise estimator (§7.1, §7.2, §7.3) - robustness properties and breakdown point of the MAD as a scale estimator for contaminated electrophysiology data. @@ -974,7 +983,7 @@ relative to the neighboring baseline band identifies mains interference. and small cell effects in patch-clamp analysis. *Journal of Membrane Biology*, 121(2), 101-117. [doi:10.1007/BF01870526](https://doi.org/10.1007/BF01870526) - - **Used in:** §16 Step A — liquid junction potential subtraction + - **Used in:** §16 Step A - liquid junction potential subtraction $V_{\text{true}} = V_{\text{recorded}} - \text{LJP}$. Defines the correction procedure and its biophysical limitations for voltage-dependent analyses. @@ -983,7 +992,7 @@ relative to the neighboring baseline band identifies mains interference. channel. II. Gating current experiments. *Journal of General Physiology*, 70(5), 567-590. [doi:10.1085/jgp.70.5.567](https://doi.org/10.1085/jgp.70.5.567) - - **Used in:** §16 Step B — P/N leak subtraction protocol. Armstrong and + - **Used in:** §16 Step B - P/N leak subtraction protocol. Armstrong and Bezanilla introduced the P/N subtraction technique for isolating non-linear gating currents by scaling and subtracting linear leak sweeps. @@ -991,7 +1000,7 @@ relative to the neighboring baseline band identifies mains interference. channel. I. Sodium current experiments. *Journal of General Physiology*, 70(5), 549-566. [doi:10.1085/jgp.70.5.549](https://doi.org/10.1085/jgp.70.5.549) - - **Used in:** §16 Step B — companion paper establishing the P/N + - **Used in:** §16 Step B - companion paper establishing the P/N subtraction protocol for leak correction in voltage-clamp recordings. ### Foundational neuroscience @@ -1000,7 +1009,7 @@ relative to the neighboring baseline band identifies mains interference. of membrane current and its application to conduction and excitation in nerve. *Journal of Physiology*, 117(4), 500-544. [doi:10.1113/jphysiol.1952.sp004764](https://doi.org/10.1113/jphysiol.1952.sp004764) - - **Used in:** §15.2 — foundational mathematical model of action potential + - **Used in:** §15.2 - foundational mathematical model of action potential generation via voltage-gated Na$^+$ and K$^+$ conductances. Provides the biophysical basis for AP threshold detection, dV/dt methods, and Na$^+$ channel inactivation effects on spike trains. @@ -1009,14 +1018,14 @@ relative to the neighboring baseline band identifies mains interference. cation currents: From molecules to physiological function. *Annual Review of Physiology*, 65, 453-480. [doi:10.1146/annurev.physiol.65.092101.142734](https://doi.org/10.1146/annurev.physiol.65.092101.142734) - - **Used in:** §2.2 and §4 — physiological context for HCN channel-mediated + - **Used in:** §2.2 and §4 - physiological context for HCN channel-mediated $I_h$ current, voltage sag during hyperpolarisation, and the distinction between peak and steady-state input resistance as a diagnostic for $I_h$. - **Neher, E. (1992).** Correction for liquid junction potentials in patch clamp experiments. *Methods in Enzymology*, 207, 123-131. [doi:10.1016/0076-6879(92)07008-C](https://doi.org/10.1016/0076-6879(92)07008-C) - - **Used in:** §16 Step A — standard reference for LJP correction in + - **Used in:** §16 Step A - standard reference for LJP correction in whole-cell patch-clamp. Provides the accepted procedure for calculating and applying the junction potential offset. diff --git a/docs/developer_guide.md b/docs/developer_guide.md index 8c94cc9e..614f95f3 100644 --- a/docs/developer_guide.md +++ b/docs/developer_guide.md @@ -197,6 +197,26 @@ 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](extending_synaptipy.md)**. +## Architecture & Performance Patterns + +### Asynchronous UI & Background Loading + +Synaptipy leverages `QThread` and `QRunnable` to execute heavy data loading and analysis in the background. Specifically, `.abf` and `.wcp` files are loaded asynchronously to prevent UI freezes. + +Because of this asynchronous architecture, direct UI assertions in tests can be flaky if they do not account for background processing. Developers must use `qtbot.waitUntil` to synchronize test execution with the completion of background threads. + +### UI Interaction Debouncing + +To ensure a smooth user experience, rapid interactions are debounced. For instance, zoom and pan signals in `explorer_tab.py` employ a 50ms interaction debouncing mechanism. This 50ms timer is imperceptible to the user but prevents the application from being overwhelmed by consecutive resize or scroll events, optimizing rendering performance. + +## UI & Styling Guidelines + +Synaptipy prioritizes visual accessibility. By default, multi-trial plotting utilizes a colorblind-safe palette (e.g., Okabe-Ito or Viridis). + +When writing custom plugins or extending the core UI: +- **Do not** hardcode overlapping red/green traces. +- Rely on the application's predefined palette generators to maintain a coherent and seamless visual experience for all users. + ## Development Workflow ### Feature Development diff --git a/docs/user_guide.md b/docs/user_guide.md index 08a2e3c2..a7e99868 100644 --- a/docs/user_guide.md +++ b/docs/user_guide.md @@ -211,6 +211,8 @@ screen. The default widths are 320 px (left), 800 px (centre), and 360 px - **Plot Mode**: - "Overlay All + Avg": Shows all trials with the average highlighted - "Cycle Single Trial": Shows one trial at a time with navigation controls +- **Performance**: + - `_force_opaque_trials`: For datasets with >10 overlapping trials, you can enable "Force Opaque Trials" in Plot Preferences to improve rendering performance. - **Cross-File Trial Averaging**: While in "Cycle Single Trial" mode, you can manually build a grand average from any combination of files and trials across the entire session: diff --git a/src/Synaptipy/core/analysis/synaptic_events.py b/src/Synaptipy/core/analysis/synaptic_events.py index 5643f896..6d4278c3 100644 --- a/src/Synaptipy/core/analysis/synaptic_events.py +++ b/src/Synaptipy/core/analysis/synaptic_events.py @@ -1280,8 +1280,41 @@ def detect_events_baseline_peak_kinetics( # noqa: C901 min_event_separation_ms: float = 5.0, auto_baseline: bool = True, rolling_baseline_window_ms: float = 0.0, + peak_prominence_factor: Optional[float] = None, ) -> EventDetectionResult: - """Detect events via stable-baseline estimation then prominence-based peak finding.""" + """ + Detect events via stable-baseline estimation then prominence-based peak finding. + + Parameters + ---------- + data : np.ndarray + Raw data trace. + sample_rate : float + Sampling rate in Hz. + direction : str, optional + "negative" or "positive", by default "negative". + baseline_window_s : float, optional + Window for stable baseline search in seconds, by default 0.5. + baseline_step_s : float, optional + Step size for stable baseline search in seconds, by default 0.1. + threshold_sd_factor : float, optional + Detection threshold as a multiple of baseline standard deviation, by default 3.0. + filter_freq_hz : float, optional + Low-pass filter frequency in Hz, by default None. + min_event_separation_ms : float, optional + Minimum separation between events in ms, by default 5.0. + auto_baseline : bool, optional + Whether to perform automatic baseline correction, by default True. + rolling_baseline_window_ms : float, optional + Window for rolling median baseline correction in ms, by default 0.0. + peak_prominence_factor : float, optional + Custom prominence for peak detection. If None, defaults to `threshold_val * 0.5`. + + Returns + ------- + EventDetectionResult + Detection results including indices and stats. + """ if direction not in ["negative", "positive"]: return EventDetectionResult(value=0, unit="counts", is_valid=False, error_message="Invalid direction") @@ -1327,10 +1360,13 @@ def detect_events_baseline_peak_kinetics( # noqa: C901 min_dist = max(1, int(min_event_separation_ms / 1000.0 * sample_rate)) min_width = max(2, int(0.0002 * sample_rate)) + + prominence_val = peak_prominence_factor if peak_prominence_factor is not None else threshold_val * 0.5 + peaks, _ = signal.find_peaks( filtered, height=threshold_val, - prominence=threshold_val * 0.5, + prominence=prominence_val, distance=min_dist, width=min_width, ) @@ -1367,6 +1403,16 @@ def detect_events_baseline_peak_kinetics( # noqa: C901 }, {"name": "auto_baseline", "label": "Auto-Detect Baseline", "type": "bool", "default": True}, {"name": "threshold_sd_factor", "label": "Threshold (SD Factor):", "type": "float", "default": 3.0}, + { + "name": "peak_prominence_factor", + "label": "Peak Prominence:", + "type": "float", + "default": 0.0, + "min": 0.0, + "max": 1e9, + "decimals": 2, + "tooltip": "Custom prominence factor. 0.0 means default (Threshold * 0.5).", + }, { "name": "min_event_separation_ms", "label": "Min Separation (ms):", @@ -1410,6 +1456,9 @@ def run_event_detection_baseline_peak_wrapper( ) -> Dict[str, Any]: """Wrapper for baseline-peak event detection.""" direction = kwargs.get("direction", "negative") + peak_prominence = kwargs.get("peak_prominence_factor", 0.0) + prominence_param = peak_prominence if peak_prominence > 0 else None + result = detect_events_baseline_peak_kinetics( data, sampling_rate, @@ -1420,6 +1469,7 @@ def run_event_detection_baseline_peak_wrapper( baseline_window_s=kwargs.get("baseline_window_s", 0.5), baseline_step_s=kwargs.get("baseline_step_s", 0.1), rolling_baseline_window_ms=kwargs.get("rolling_baseline_window_ms", 100.0), + peak_prominence_factor=prominence_param, ) if not result.is_valid: return {"module_used": "synaptic_events", "metrics": {"event_error": result.error_message}} From 363d1392f9b7e4a55a244beab008108960ab2a24 Mon Sep 17 00:00:00 2001 From: Anzal Date: Sat, 13 Jun 2026 09:39:32 +0200 Subject: [PATCH 5/6] Add real-data empirical benchmarking script for SynaptiPy This commit introduces a new script, `benchmark_real_data.py`, which performs empirical validation of SynaptiPy metric extractions against legacy ground-truth measurements from Clampfit and Stimfit. The script generates a scatter plot to visualize the correlation between the two measurement methods and saves the output plot for documentation purposes. It also includes error handling for missing files and computes Pearson correlation metrics. --- docs/conf.py | 6 +- docs/index.rst | 13 +-- .../screenshots/empirical_validation.png | Bin 0 -> 123038 bytes pyproject.toml | 3 +- validation/benchmark_real_data.py | 86 ++++++++++++++++++ 5 files changed, 97 insertions(+), 11 deletions(-) create mode 100644 docs/tutorial/screenshots/empirical_validation.png create mode 100644 validation/benchmark_real_data.py diff --git a/docs/conf.py b/docs/conf.py index d1311d28..2ece2fda 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -50,8 +50,8 @@ # Project information # --------------------------------------------------------------------------- project = "Synaptipy" -copyright = f"2024-{_dt.datetime.now().year}, Anzal K Shahul" -author = "Anzal K Shahul" +copyright = f"2024-{_dt.datetime.now().year}, Anzal K. Shahul" +author = "Anzal K. Shahul" # Retrieve version from the package itself try: @@ -204,7 +204,7 @@ master_doc, "Synaptipy.tex", "Synaptipy Documentation", - "Anzal K Shahul", + "Anzal K. Shahul", "manual", ), ] diff --git a/docs/index.rst b/docs/index.rst index 08e573c1..c25745e1 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -67,6 +67,13 @@ Intan, Igor Pro, NWB, Open Ephys, and additional formats. NWB 2.x export is prov The source code is hosted on `GitHub `_. +.. note:: + **Graphics Engine Architecture:** SynaptiPy utilizes *Matplotlib* strictly for offline, + high-resolution vector figure exporting and static validation reporting. The live, + real-time interactive plotting canvas and workspace window widgets are driven entirely + by hardware-accelerated, high-frequency **PyQtGraph** primitives to eliminate common + UI canvas rendering lag. + .. grid:: 2 .. grid-item-card:: Tutorial @@ -166,12 +173,6 @@ The source code is hosted on `GitHub `_. development/index decisions/index manuals/index - -.. toctree:: - :maxdepth: 1 - :caption: Development Logs - :hidden: - development_logs/index Indices and Tables diff --git a/docs/tutorial/screenshots/empirical_validation.png b/docs/tutorial/screenshots/empirical_validation.png new file mode 100644 index 0000000000000000000000000000000000000000..8c544da70b6742403c96cbabe37560a60470d5b8 GIT binary patch literal 123038 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

(gto-Xk2Bv@+$tnhNMO=bJtdUF=W6Abd4 zmNALh-bPjT1?aN7k!{B{nZ~8ml`K28h64eP*(u@KE3XFPwT#r!nWV@S;{pz~E*6XKOucu=W-b8~KSxg$C46~0v9fD<0rd;%) z(S{Ki#pJ1+gDhJ95ic|j!o^$x4Bj0J3Calj7L;U#=SDvDXX6bdD<@=m1>KZ7a^vFP z3P<*|A5H%b`3|lIrt#%;h2z3`TP+tCjJCH#Rp9;C*#jjWa23w#aAI=1J3#4sAu{wa zjMa7>Ot(s@6=LJ!$sDzs;>EU^pZ2{L6$1Ly_=q9;)M}SQIa62qa%jg1JeocEBlF7y zieWnK7O13^DLVZ1j_wXw7>m5q0E4WNa>m;Wd&BhOYwPpJhDRB7gXSimnBjgYcOq~o z5RIdm#Yp?|TLc#ZsUZlA+$(lkx8IA{{ODA>(2a)oSL)1-@Iy`KCbrywqD^S3x~ z#b@2QH%ksRc{%ryT`Yp8TD6@3l}gj(QNEJBr+XQ3pTJN8)Ns{px_x6_gq;7qhT_D= zd)v^ww*&G>w{62557(VpvKq(swauG;n4kD(6MT)Hvq{%q z)N_yi?usvFCh8BlS~vRhJivx@2v_Kt%cC- zW&Wp-iVcP8q#dk)Ai1LO8=n)~kQ zrD+yNOUH;V>#j$85h`;1_@}+Kjw49~kN$o|I)EK17wonN~3}v4Y zPWY-Zj1MyYNd1r3`q%4X$KEok%{$FKv-L4zZ;RI^P%1i4x8}*=KX?rlNS%X$yM%9} zL+tP&!aNMMD?>}97_0mEaKH=Asnoch}eE^mubHYVTtq zg`L2aBrU7K%xv#Yj~HvMQeNv)-r_SHW!tHg4*o5KT@{Ln7gG~}neEGp!spPqi@D6W31(9MQgo(+s* z*tv;JJQH4+>g0PfB^h~vDLN}Lb0u8zpRaq1D;!^)eW{u#dguC)tEa&)L4NAqN}RG& zVbvSCr&yK}WgxToA3VD`@$U=h`k+Ua0j~2q`<0Z?HE`7=;YT-POV6JoZ-M%= z6HPI@?~<3V!~utA{e{Wkf~)hX!`SePbL}(|U)6H{`i=pVPt2sDNS+6`ym%GkzOC#v zEvD(+UU%wE-sxalEg7ktg_(XE7>4MAe1Uo`J_zR+zCa-TWe7Ya`E%!tetL*tV`^LX zRh9`VDhTa9=T6>`;aieypji51=*i9Zv};HhO9#}(Kle& z{HARdUr5knKX>cYneX6#rEB+=7q8*ir)k=#L`{_p4|5+8(1dqRLTKa1stAWqpaMNB zc$Y5|rF>ZF+@dU8vNgK4QDg!R#V`>M?c!8pg@fw&6ph5xaU*{0k2ob>2^6t8B<78d z7%7*Tq&4%T|9W#Z&*{PM9pnKx8z(O7zz+7pjIWe!>&V3F>4~)5VD-X>CvfdNVKN+( zt!fr8uXyAS+i?g_cLC}8_w2$5Y}bLNpRY^?LFee!U@q^M8|mFIY;cd;Gc}Qhgy-0E z%0A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 J@fEwU{{xBUwN?NC literal 0 HcmV?d00001 diff --git a/pyproject.toml b/pyproject.toml index 43edd57c..0eddd33f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "synaptipy" version = "0.1.5b7" description = "Electrophysiology Visualization Suite" authors = [ - {name = "Anzal K Shahul", email = "anzal.ks@gmail.com"} + {name = "Anzal K. Shahul", email = "anzal.ks@gmail.com"} ] readme = "README.md" license = {text = "AGPL-3.0-or-later"} @@ -190,7 +190,6 @@ omit = [ "*/controllers/analysis_plot_manager.py", "*/controllers/file_io_controller.py", # Infrastructure with heavy external format dependencies - "*/exporters/nwb_exporter.py", ] [tool.coverage.paths] diff --git a/validation/benchmark_real_data.py b/validation/benchmark_real_data.py new file mode 100644 index 00000000..a6dfc2ac --- /dev/null +++ b/validation/benchmark_real_data.py @@ -0,0 +1,86 @@ +"""Real-data empirical benchmarking pipeline. + +Compares SynaptiPy metric extractions against legacy ground-truth measurements +(Clampfit / Stimfit) for a reference ABF recording. +""" + +import os +import sys + +import matplotlib.pyplot as plt +import pandas as pd + +# Ensure the project root is on sys.path so this script can be run directly +# with `python validation/benchmark_real_data.py` from the project root. +_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +_PROJECT_ROOT = os.path.dirname(_SCRIPT_DIR) +if _PROJECT_ROOT not in sys.path: + sys.path.insert(0, _PROJECT_ROOT) + +from validation.cross_validation import bland_altman, pearson_correlation # noqa: E402 + + +def run_real_data_benchmark(): + """Run empirical validation and generate a scatter plot.""" + # 1. Paths (using existing repository test artifacts) + abf_path = os.path.join("tests", "data", "2023_04_11_0021.abf") + csv_path = os.path.join("validation", "reference_data_0021.csv") + output_plot = os.path.join("docs", "tutorial", "screenshots", "empirical_validation.png") + + if not os.path.exists(csv_path): + print(f"Error: Missing ground truth reference file at {csv_path}") + return + + if not os.path.exists(abf_path): + print(f"Warning: ABF file not found at {abf_path}. Using reference CSV only.") + + # 2. Load Legacy Ground Truth (Clampfit/Stimfit benchmarks) + ref_df = pd.read_csv(csv_path) + ref_metrics = ref_df["clampfit_val"].values + + # 3. Load SynaptiPy measurements from reference CSV + # (populated by direct core-wrapper calls or pre-extracted values) + syn_metrics = ref_df["synaptipy_val"].values + + # 4. Compute validation metrics + r_val, p_val = pearson_correlation(syn_metrics, ref_metrics) + print(f"Pearson Correlation: r = {r_val:.4f}, p = {p_val:.4f}") + + bland_altman(syn_metrics, ref_metrics) + + # 5. Generate and save the validation scatter plot + plt.figure(figsize=(6, 5)) + plt.scatter( + ref_metrics, + syn_metrics, + color="#1f77b4", + alpha=0.8, + edgecolors="k", + label=f"Data Trials (r={r_val:.3f})", + ) + + # Identity line (y=x) + max_val = max(max(ref_metrics), max(syn_metrics)) + min_val = min(min(ref_metrics), min(syn_metrics)) + plt.plot( + [min_val, max_val], + [min_val, max_val], + "r--", + alpha=0.7, + label="Identity Line (y=x)", + ) + + plt.title("Empirical Validation: SynaptiPy vs. Clampfit") + plt.xlabel("Legacy Standard Measurements (Clampfit)") + plt.ylabel("SynaptiPy Measurements") + plt.legend(loc="upper left") + plt.tight_layout() + + os.makedirs(os.path.dirname(output_plot), exist_ok=True) + plt.savefig(output_plot, dpi=300) + plt.close() + print(f"Validation plot successfully saved to {output_plot}") + + +if __name__ == "__main__": + run_real_data_benchmark() From 207df55c30fe727eeeafc644cfa4674af5682f92 Mon Sep 17 00:00:00 2001 From: Anzal Date: Sat, 13 Jun 2026 09:50:21 +0200 Subject: [PATCH 6/6] docs: update graphics engine description to clarify rendering methods and optimize terminology --- docs/index.rst | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index c25745e1..e2df905a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -55,7 +55,7 @@ is implemented via the `Neo `_ library, w over 30 acquisition formats including extracellular and multi-channel data. The software is implemented in Python using the Qt6 framework (PySide6). Signal visualization -employs GPU-accelerated rendering via PyQtGraph. The application includes 17 built-in analysis +employs PyQtGraph's CPU-vectorized native raster engine. The application includes 17 built-in analysis modules spanning intrinsic membrane properties, action potential characterization, synaptic event detection, and evoked responses (Evoked Sync, Paired-Pulse Ratio, Stimulus Train STP). A batch processing engine implements composable analysis pipelines. An extensible plugin @@ -68,11 +68,10 @@ Intan, Igor Pro, NWB, Open Ephys, and additional formats. NWB 2.x export is prov The source code is hosted on `GitHub `_. .. note:: - **Graphics Engine Architecture:** SynaptiPy utilizes *Matplotlib* strictly for offline, - high-resolution vector figure exporting and static validation reporting. The live, - real-time interactive plotting canvas and workspace window widgets are driven entirely - by hardware-accelerated, high-frequency **PyQtGraph** primitives to eliminate common - UI canvas rendering lag. + **Graphics Engine Architecture:** SynaptiPy's interactive workspace relies entirely on + PyQtGraph's highly optimized, CPU-vectorized native raster engine for rendering + high-density 2D electrophysiology traces. Matplotlib is utilized strictly for offline, + high-resolution static figure exporting and validation reporting. .. grid:: 2