diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..507c6603 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,86 @@ +name: Docs + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + # NEURON + MPI system libraries: the build imports myogen for the + # mkdocstrings API reference (and supports opt-in gallery execution via + # MKDOCS_GALLERY_PLOT=true). Same set the old Sphinx docs workflow used. + - name: Install NEURON/MPI system dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + libegl1-mesa-dev libgtk2.0-dev \ + openmpi-bin openmpi-common libopenmpi-dev \ + infiniband-diags ibverbs-utils libibverbs-dev \ + libfabric1 libfabric-dev libpsm2-dev librdmacm-dev + + - name: Set up uv + uses: astral-sh/setup-uv@v8.1.0 + with: + enable-cache: true + + - name: Sync project + docs/nwb extras + run: uv sync --locked --group docs --extra nwb + + - name: Compile NMODL mechanisms + run: uv run poe setup_myogen + + - name: Build docs site + # Execute the example gallery so figures render inline in the deployed + # site (parity with the old Sphinx docs). scripts/build_docs.py wraps + # `properdocs build` with bounded retries: NEURON's native runtime state + # accumulates across mkdocs-gallery's single-process execution and + # eventually segfaults, but each executed example is md5-cached, so + # retrying resumes past the crash. The wrapper aborts if a retry makes no + # progress and validates gallery completeness before the site can deploy. + # (gallery_conf.py additionally forces joblib to n_jobs=1 so no loky + # worker processes are spawned mid-build.) + env: + # Execute the gallery (inline plots) only where the site is actually + # deployed — the push to main — plus manual dispatch for pre-merge + # testing. On pull requests the gallery is source-only so review builds + # stay fast (~10s); the slow executed build (NEURON sims + retries to + # survive state-accumulation segfaults) runs once per merge. + MKDOCS_GALLERY_PLOT: ${{ (github.event_name == 'push' && github.ref == 'refs/heads/main') || github.event_name == 'workflow_dispatch' }} + run: uv run --group docs python scripts/build_docs.py + + - name: Upload Pages artifact + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + uses: actions/upload-pages-artifact@v5 + with: + path: site + + deploy: + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + needs: build + runs-on: ubuntu-latest + # Serialize only the Pages deployment — scoping this to the deploy job (not + # the whole workflow) keeps PR "Docs" checks from queueing behind a long + # main-branch executed build. + concurrency: + group: pages + cancel-in-progress: false + environment: + name: github-pages + url: ${{ steps.deploy.outputs.page_url }} + steps: + - name: Configure Pages + uses: actions/configure-pages@v6 + - name: Deploy to GitHub Pages + id: deploy + uses: actions/deploy-pages@v5 diff --git a/.github/workflows/sphinx.yml b/.github/workflows/sphinx.yml deleted file mode 100644 index 8daf0d1e..00000000 --- a/.github/workflows/sphinx.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: publish-docs - -on: - release: - types: [published] - workflow_dispatch: - -jobs: - deploy-docs: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - - - name: Set up Python 3.12 - uses: actions/setup-python@v6 - with: - python-version: 3.12.8 - - - name: Install dependencies - run: | - sudo apt update && sudo apt install -y libegl1-mesa-dev - sudo apt-get update && sudo apt-get install infiniband-diags ibverbs-utils libibverbs-dev libfabric1 libfabric-dev libpsm2-dev -y - sudo apt-get install openmpi-bin openmpi-common libopenmpi-dev libgtk2.0-dev - sudo apt-get install librdmacm-dev libpsm2-dev - python -m pip install --upgrade pip - python -m pip install uv - uv sync --group docs - uv run poe setup_myogen - - - name: Build the docs - run: | - source .venv/bin/activate - cd docs - make clean - make html || make html # Retry once if first build fails (NEURON state issue) - cd .. - - - name: Deploy - uses: peaceiris/actions-gh-pages@v4 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: docs/build/html diff --git a/.gitignore b/.gitignore index 374db174..2add1799 100644 --- a/.gitignore +++ b/.gitignore @@ -200,3 +200,8 @@ docs/serve_docs.sh # Local scratch notes (kept in nexus, not the repo) notes/ + +# mkdocs / properdocs build output +/site/ +# mkdocs-gallery generated pages +/docs/auto_examples/ diff --git a/README.md b/README.md index c7448ccf..2e0b14d0 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@

Welcome to - MyoGen Logo + MyoGen Logo

The modular and extensible simulation toolkit for neurophysiology

@@ -10,9 +10,9 @@ [![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/) [![Version](https://img.shields.io/badge/version-0.10.1-orange.svg)](https://github.com/NsquaredLab/MyoGen) - [Installation](https://nsquaredlab.github.io/MyoGen/#installation) • + [Installation](https://nsquaredlab.github.io/MyoGen/getting-started/#install) • [Documentation](https://nsquaredlab.github.io/MyoGen/) • - [Examples](https://nsquaredlab.github.io/MyoGen/examples.html) • + [Examples](https://nsquaredlab.github.io/MyoGen/auto_examples/01_basic/) • [How to Cite](https://nsquaredlab.github.io/MyoGen/#how-to-cite)
@@ -161,7 +161,7 @@ uv add cupy-cuda12x 📖 **[Read the full documentation](https://nsquaredlab.github.io/MyoGen/)** -- [User Guide](https://nsquaredlab.github.io/MyoGen/neo_blocks_guide.html) — Working with simulation outputs +- [User Guide](https://nsquaredlab.github.io/MyoGen/neo-blocks/) — Working with simulation outputs - [API Reference](https://nsquaredlab.github.io/MyoGen/api/) — Complete class documentation - [Examples](examples/) — Step-by-step tutorials from recruitment to EMG diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index 65946377..00000000 --- a/docs/Makefile +++ /dev/null @@ -1,26 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line, and also -# from the environment for the first two. -SPHINXOPTS ?= -SPHINXBUILD ?= sphinx-build -SOURCEDIR = source -BUILDDIR = build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -# Serve documentation locally with HTTP server (required for hoverxref tooltips to work) -serve: - @echo "Serving documentation at http://localhost:8000" - @echo "Press Ctrl+C to stop" - @cd $(BUILDDIR)/html && python -m http.server 8000 - -.PHONY: help serve Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 8373338e..00000000 --- a/docs/README.md +++ /dev/null @@ -1,60 +0,0 @@ -# MyoGen Documentation - -This directory contains the source files for MyoGen's Sphinx documentation. - -## Building the Documentation - -```bash -# From the docs directory -make html -``` - -The built documentation will be in `build/html/`. - -## Viewing the Documentation - -**Important:** Hover tooltips (hoverxref) require viewing the documentation via HTTP server, not by opening `file://` URLs directly. - -### Option 1: Using the serve script (recommended) - -```bash -cd docs -./serve_docs.sh -``` - -Then open http://localhost:8000 in your browser. - -### Option 2: Manual Python server - -```bash -cd docs/build/html -python -m http.server 8000 -``` - -Then open http://localhost:8000 in your browser. - -### Why is this necessary? - -The hoverxref extension uses AJAX to fetch tooltip content. Browsers block AJAX requests from `file://` URLs for security reasons, causing tooltips to show "Loading..." indefinitely. Serving via HTTP resolves this issue. - -## Documentation Structure - -- `source/` - RST source files - - `api/` - API reference documentation - - `auto_examples/` - Generated example galleries (auto-generated) - - `_static/` - Static files (CSS, JS, images) - - `templates/` - Custom Sphinx templates - - `generated/` - Auto-generated API docs (auto-generated) -- `build/` - Built documentation (generated, not tracked in git) - -## Making Changes - -1. Edit RST files in `source/` -2. Rebuild: `make html` -3. View changes: `./serve_docs.sh` - -## Cleaning Build - -```bash -make clean -``` diff --git a/docs/api/currents.md b/docs/api/currents.md new file mode 100644 index 00000000..95a68c41 --- /dev/null +++ b/docs/api/currents.md @@ -0,0 +1,7 @@ +# Currents + +::: myogen.utils.currents.create_ramp_current +::: myogen.utils.currents.create_step_current +::: myogen.utils.currents.create_sinusoidal_current +::: myogen.utils.currents.create_sawtooth_current +::: myogen.utils.currents.create_trapezoid_current diff --git a/docs/api/index.md b/docs/api/index.md new file mode 100644 index 00000000..16d4d1e4 --- /dev/null +++ b/docs/api/index.md @@ -0,0 +1,8 @@ +# API Reference + +- [Top-level (`myogen`)](myogen.md) +- [Simulator](simulator.md) +- [Currents](currents.md) +- [Neuron injection & I/O](io-and-neuron.md) +- [Plotting](plotting.md) +- [Types](types.md) diff --git a/docs/api/io-and-neuron.md b/docs/api/io-and-neuron.md new file mode 100644 index 00000000..18930382 --- /dev/null +++ b/docs/api/io-and-neuron.md @@ -0,0 +1,17 @@ +# Neuron injection & I/O + +## Current injection +::: myogen.utils.neuron.inject_currents_into_populations.inject_currents_into_populations +::: myogen.utils.neuron.inject_currents_into_populations.inject_currents_and_simulate_spike_trains + +## Persistence +::: myogen.utils.continuous_saver.ContinuousSaver +::: myogen.utils.continuous_saver.convert_chunks_to_neo + +## NWB export +!!! note + NWB export requires optional dependencies: `pip install myogen[nwb]`. + +::: myogen.utils.nwb.export_to_nwb +::: myogen.utils.nwb.export_simulation_to_nwb +::: myogen.utils.nwb.validate_nwb diff --git a/docs/api/myogen.md b/docs/api/myogen.md new file mode 100644 index 00000000..5ca04131 --- /dev/null +++ b/docs/api/myogen.md @@ -0,0 +1,17 @@ +# Top-level API (`myogen`) + +## Reproducibility + +Module-level state: + +- **`myogen.RANDOM_GENERATOR`** — deprecated compatibility attribute; use `get_random_generator()` instead. +- **`myogen.SEED`** — deprecated compatibility attribute; use `get_random_seed()` instead. + +::: myogen.set_random_seed +::: myogen.get_random_generator +::: myogen.get_random_seed +::: myogen.derive_subseed +::: myogen.load_nmodl_mechanisms +::: myogen.get_mechanism_parameters +::: myogen.validate_mechanism_parameter +::: myogen.set_mechanism_param diff --git a/docs/api/plotting.md b/docs/api/plotting.md new file mode 100644 index 00000000..098c0b11 --- /dev/null +++ b/docs/api/plotting.md @@ -0,0 +1,8 @@ +# Plotting + +::: myogen.utils.plotting.plot_raster_spikes +::: myogen.utils.plotting.plot_membrane_potentials +::: myogen.utils.plotting.plot_muscle_dynamics +::: myogen.utils.plotting.plot_antagonist_muscle_comparison +::: myogen.utils.plotting.plot_spindle_dynamics +::: myogen.utils.plotting.plot_gto_dynamics diff --git a/docs/api/simulator.md b/docs/api/simulator.md new file mode 100644 index 00000000..4ed8d539 --- /dev/null +++ b/docs/api/simulator.md @@ -0,0 +1,53 @@ +# Simulator + +## Recruitment +::: myogen.simulator.RecruitmentThresholds + +## Neuron populations +::: myogen.simulator.neuron.populations.AlphaMN__Pool +::: myogen.simulator.neuron.populations.DescendingDrive__Pool + +`DescendingDrive_Gamma__Pool` is a backward-compatibility alias. New code should +prefer `DescendingDrive__Pool(process_type="gamma", shape=...)`. + +::: myogen.simulator.neuron.populations.DescendingDrive_Gamma__Pool +::: myogen.simulator.neuron.populations.AffIa__Pool +::: myogen.simulator.neuron.populations.AffII__Pool +::: myogen.simulator.neuron.populations.AffIb__Pool +::: myogen.simulator.neuron.populations.GII__Pool +::: myogen.simulator.neuron.populations.GIb__Pool + +## Network & runner +::: myogen.simulator.Network +::: myogen.simulator.SimulationRunner + +## Muscle & force +::: myogen.simulator.Muscle +::: myogen.simulator.HillModel +::: myogen.simulator.ForceModel +::: myogen.simulator.ForceModelVectorized + +## EMG +::: myogen.simulator.SurfaceEMG +::: myogen.simulator.IntramuscularEMG +::: myogen.simulator.SurfaceElectrodeArray +::: myogen.simulator.IntramuscularElectrodeArray + +## Proprioception +::: myogen.simulator.SpindleModel +::: myogen.simulator.GolgiTendonOrganModel +::: myogen.simulator.JointDynamics + +## Geometry & biomechanics +::: myogen.simulator.MuscleGeometry +::: myogen.simulator.JointGeometry +::: myogen.simulator.JointBiomechanics + +## Grid / Neo utilities +::: myogen.simulator.create_grid_signal +::: myogen.simulator.signal_to_grid +::: myogen.simulator.get_electrode +::: myogen.simulator.get_row +::: myogen.simulator.get_column + +The deprecated `GridAnalogSignal` compatibility class is intentionally excluded. diff --git a/docs/api/types.md b/docs/api/types.md new file mode 100644 index 00000000..9536b6e8 --- /dev/null +++ b/docs/api/types.md @@ -0,0 +1,33 @@ +# Types + +::: myogen.utils.types + options: + members: + - Quantity__s + - Quantity__ms + - Quantity__rad + - Quantity__deg + - Quantity__mV + - Quantity__uV + - Quantity__nA + - Quantity__uS + - Quantity__S_per_m + - Quantity__Hz + - Quantity__pps + - Quantity__mm + - Quantity__m + - Quantity__mm2 + - Quantity__per_mm2 + - Quantity__m_per_s + - Quantity__mm_per_s + - CURRENT__AnalogSignal + - FORCE__AnalogSignal + - SPIKE_TRAIN__Block + - SURFACE_MUAP__Block + - SURFACE_EMG__Block + - INTRAMUSCULAR_MUAP__Block + - INTRAMUSCULAR_EMG__Block + - CORTICAL_INPUT__MATRIX + - RECRUITMENT_THRESHOLDS__ARRAY + - JOINT_ANGLE__ARRAY + - MOMENT_ARM__MATRIX diff --git a/docs/api/utils.md b/docs/api/utils.md new file mode 100644 index 00000000..cf9813c6 --- /dev/null +++ b/docs/api/utils.md @@ -0,0 +1,12 @@ +# Utilities + +## Noise generation & calibration +::: myogen.utils.emg_noise.add_realistic_noise +::: myogen.utils.emg_noise.generate_realistic_noise +::: myogen.utils.emg_noise.estimate_noise_rms_from_recording +::: myogen.utils.emg_noise.calibrate_realistic_noise_profile +::: myogen.utils.emg_noise.calibrate_baseline_drift_profile +::: myogen.utils.emg_noise.tune_noise_profile_with_optuna + +## Binning +::: myogen.utils.binning.bin_spike_trains diff --git a/docs/gallery_conf.py b/docs/gallery_conf.py new file mode 100644 index 00000000..2ed7dd37 --- /dev/null +++ b/docs/gallery_conf.py @@ -0,0 +1,124 @@ +"""mkdocs-gallery base configuration for the MyoGen example gallery. + +Referenced from ``properdocs.yml`` via the ``gallery`` plugin's ``conf_script``. +The module-level ``conf`` dict is loaded as the gallery's base configuration; +``plot_gallery`` is overridden from ``properdocs.yml`` (env-controlled). +""" + +from pathlib import Path + +import matplotlib +from mkdocs_gallery.sorting import FileNameSortKey + +# Headless build: render figures off-screen and capture them, never try to +# display. mkdocs-gallery's matplotlib scraper collects figures via +# ``plt.get_fignums()`` (see mkdocs_gallery/scrapers.py), independently of +# ``plt.show()``. Under the Agg backend ``plt.show()`` therefore does nothing +# useful and only emits a "FigureCanvasAgg is non-interactive, and thus cannot +# be shown" UserWarning that leaks (with the absolute source path) into every +# example's Out block. sphinx-gallery no-ops ``show`` for exactly this reason; +# mkdocs-gallery does not, so we do it here. +matplotlib.use("Agg") +import matplotlib.pyplot as plt # noqa: E402 (must follow matplotlib.use) + +plt.show = lambda *args, **kwargs: None + +# Progress bars are terminal UX with no place in rendered docs. mkdocs-gallery +# scrapes stderr, so a live tqdm bar would be captured into an example's Out +# block. tqdm reads TQDM_DISABLE only at its own import (too early to set +# reliably from here), so instead force every tqdm instance created during the +# build to be disabled. +import tqdm as _tqdm # noqa: E402 + +_tqdm_init = _tqdm.std.tqdm.__init__ + + +def _disabled_tqdm_init(self, *args, **kwargs): + kwargs["disable"] = True + _tqdm_init(self, *args, **kwargs) + + +_tqdm.std.tqdm.__init__ = _disabled_tqdm_init + +# NEURON initialises native runtime state that reset_neuron() below cannot +# unwind. mkdocs-gallery executes every example sequentially in one long-lived +# process; once that native state has accumulated, MyoGen launching joblib's +# default `loky` worker processes (parallel muscle-fibre / MUAP generation) +# segfaults the build on both Linux CI and macOS. Force every joblib.Parallel +# to run in-process (n_jobs=1) for the duration of the build. As a bonus this +# keeps the tqdm patch above effective (no fresh worker imports re-enable bars). +import joblib # noqa: E402 + +_joblib_parallel_init = joblib.Parallel.__init__ + + +def _sequential_parallel_init(self, *args, **kwargs): + # n_jobs is Parallel.__init__'s first positional parameter. Override it in + # place so callers that pass it positionally (e.g. scikit-learn's + # ``Parallel(n_jobs, prefer="threads")``) don't raise "multiple values for + # argument 'n_jobs'". + if args: + args = (1, *args[1:]) + else: + kwargs["n_jobs"] = 1 + _joblib_parallel_init(self, *args, **kwargs) + + +joblib.Parallel.__init__ = _sequential_parallel_init + +# mkdocs-gallery requires examples_dirs / gallery_dirs as absolute paths under +# the project root (it calls Path(...).relative_to(project_root)). This file +# lives at docs/gallery_conf.py, so its grandparent is the repo root. +_ROOT = Path(__file__).resolve().parent.parent + + +def reset_neuron(gallery_conf, fname): + """Reset NEURON global HOC state between gallery examples. + + NEURON's interpreter keeps process-global state across examples run in one + process; clearing sections + time between examples keeps them independent. + """ + try: + import myogen # noqa: F401 (auto-loads mechanisms, sets up NEURON) + from neuron import h + + for sec in list(h.allsec()): + h.delete_section(sec=sec) + h.load_file("stdrun.hoc") + h.t = 0 + h.tstop = 0 + except (ImportError, RuntimeError, LookupError, AttributeError): + pass + + +conf = { + "examples_dirs": [ + str(_ROOT / "examples" / "01_basic"), + str(_ROOT / "examples" / "02_finetune"), + str(_ROOT / "examples" / "03_papers" / "watanabe"), + str(_ROOT / "examples" / "04_clinical"), + ], + "gallery_dirs": [ + str(_ROOT / "docs" / "auto_examples" / "01_basic"), + str(_ROOT / "docs" / "auto_examples" / "02_finetune"), + str(_ROOT / "docs" / "auto_examples" / "03_papers" / "watanabe"), + str(_ROOT / "docs" / "auto_examples" / "04_clinical"), + ], + # Execute only the self-contained tutorial + fine-tuning galleries. The + # paper (03_papers/watanabe) and clinical (04_clinical) examples rely on + # ``__file__`` (undefined under mkdocs-gallery's exec()) to locate a results + # directory and load each other's ``.pkl`` outputs, so they cannot execute + # cleanly in the gallery — they render source-only. (When MKDOCS_GALLERY_PLOT + # is false, everything is source-only regardless.) + "filename_pattern": r"/(01_basic|02_finetune)/", + "ignore_pattern": r"(14_calibrate_noise_from_real|_oscillating_dc_helpers|_optimize_dc_worker|_pic_protocols)\.py", + "within_subsection_order": FileNameSortKey, + "image_scrapers": ("matplotlib",), + # strip the `# mkdocs_gallery_thumbnail_path = ...` directives from rendered source + "remove_config_comments": True, + # fallback thumbnail for examples without a captured figure (logo) + "default_thumb_file": str(_ROOT / "docs" / "images" / "myogen_logo.png"), + "reset_modules": (reset_neuron,), + # Source-only by default; overridden from properdocs.yml via MKDOCS_GALLERY_PLOT. + "plot_gallery": False, +} diff --git a/docs/gallery_thumbs/01_compute_baseline_force.png b/docs/gallery_thumbs/01_compute_baseline_force.png new file mode 100644 index 00000000..42eb0d77 Binary files /dev/null and b/docs/gallery_thumbs/01_compute_baseline_force.png differ diff --git a/docs/gallery_thumbs/01_optimize_dd_for_target_firing_rate.png b/docs/gallery_thumbs/01_optimize_dd_for_target_firing_rate.png new file mode 100644 index 00000000..d8e8be6c Binary files /dev/null and b/docs/gallery_thumbs/01_optimize_dd_for_target_firing_rate.png differ diff --git a/docs/gallery_thumbs/01_simulate_recruitment_thresholds.png b/docs/gallery_thumbs/01_simulate_recruitment_thresholds.png new file mode 100644 index 00000000..4e916d8c Binary files /dev/null and b/docs/gallery_thumbs/01_simulate_recruitment_thresholds.png differ diff --git a/docs/gallery_thumbs/02_compute_force_from_optimized_dd.png b/docs/gallery_thumbs/02_compute_force_from_optimized_dd.png new file mode 100644 index 00000000..16100c6d Binary files /dev/null and b/docs/gallery_thumbs/02_compute_force_from_optimized_dd.png differ diff --git a/docs/gallery_thumbs/02_optimize_oscillating_dc.png b/docs/gallery_thumbs/02_optimize_oscillating_dc.png new file mode 100644 index 00000000..ed25b863 Binary files /dev/null and b/docs/gallery_thumbs/02_optimize_oscillating_dc.png differ diff --git a/docs/gallery_thumbs/02_simulate_spike_trains_current_injection.png b/docs/gallery_thumbs/02_simulate_spike_trains_current_injection.png new file mode 100644 index 00000000..aa896229 Binary files /dev/null and b/docs/gallery_thumbs/02_simulate_spike_trains_current_injection.png differ diff --git a/docs/gallery_thumbs/03_10pct_mvc_simulation.png b/docs/gallery_thumbs/03_10pct_mvc_simulation.png new file mode 100644 index 00000000..13438407 Binary files /dev/null and b/docs/gallery_thumbs/03_10pct_mvc_simulation.png differ diff --git a/docs/gallery_thumbs/03_optimize_dd_for_target_force.png b/docs/gallery_thumbs/03_optimize_dd_for_target_force.png new file mode 100644 index 00000000..f9bbee8a Binary files /dev/null and b/docs/gallery_thumbs/03_optimize_dd_for_target_force.png differ diff --git a/docs/gallery_thumbs/03_simulate_spike_trains_descending_drive.png b/docs/gallery_thumbs/03_simulate_spike_trains_descending_drive.png new file mode 100644 index 00000000..31562e5a Binary files /dev/null and b/docs/gallery_thumbs/03_simulate_spike_trains_descending_drive.png differ diff --git a/docs/gallery_thumbs/04_extract_isi_and_cv_per_ramps.png b/docs/gallery_thumbs/04_extract_isi_and_cv_per_ramps.png new file mode 100644 index 00000000..2b6aa8a9 Binary files /dev/null and b/docs/gallery_thumbs/04_extract_isi_and_cv_per_ramps.png differ diff --git a/docs/gallery_thumbs/04_load_and_analyze_results.png b/docs/gallery_thumbs/04_load_and_analyze_results.png new file mode 100644 index 00000000..d30f596c Binary files /dev/null and b/docs/gallery_thumbs/04_load_and_analyze_results.png differ diff --git a/docs/gallery_thumbs/04_simulate_muscle.png b/docs/gallery_thumbs/04_simulate_muscle.png new file mode 100644 index 00000000..3d87f48f Binary files /dev/null and b/docs/gallery_thumbs/04_simulate_muscle.png differ diff --git a/docs/gallery_thumbs/05_plot_isi_cv_multi_muscle_comparison.png b/docs/gallery_thumbs/05_plot_isi_cv_multi_muscle_comparison.png new file mode 100644 index 00000000..e32986a7 Binary files /dev/null and b/docs/gallery_thumbs/05_plot_isi_cv_multi_muscle_comparison.png differ diff --git a/docs/gallery_thumbs/05_simulate_surface_muaps.png b/docs/gallery_thumbs/05_simulate_surface_muaps.png new file mode 100644 index 00000000..26aa10a5 Binary files /dev/null and b/docs/gallery_thumbs/05_simulate_surface_muaps.png differ diff --git a/docs/gallery_thumbs/06_simulate_surface_emg.png b/docs/gallery_thumbs/06_simulate_surface_emg.png new file mode 100644 index 00000000..83bb1cf5 Binary files /dev/null and b/docs/gallery_thumbs/06_simulate_surface_emg.png differ diff --git a/docs/gallery_thumbs/06_visualize.png b/docs/gallery_thumbs/06_visualize.png new file mode 100644 index 00000000..88126853 Binary files /dev/null and b/docs/gallery_thumbs/06_visualize.png differ diff --git a/docs/gallery_thumbs/07_simulate_currents.png b/docs/gallery_thumbs/07_simulate_currents.png new file mode 100644 index 00000000..c9a91245 Binary files /dev/null and b/docs/gallery_thumbs/07_simulate_currents.png differ diff --git a/docs/gallery_thumbs/08_simulate_force.png b/docs/gallery_thumbs/08_simulate_force.png new file mode 100644 index 00000000..147f0b56 Binary files /dev/null and b/docs/gallery_thumbs/08_simulate_force.png differ diff --git a/docs/gallery_thumbs/09_simulate_intramuscular_emg.png b/docs/gallery_thumbs/09_simulate_intramuscular_emg.png new file mode 100644 index 00000000..6acf12d5 Binary files /dev/null and b/docs/gallery_thumbs/09_simulate_intramuscular_emg.png differ diff --git a/docs/gallery_thumbs/11_simulate_spinal_network.png b/docs/gallery_thumbs/11_simulate_spinal_network.png new file mode 100644 index 00000000..7ca54965 Binary files /dev/null and b/docs/gallery_thumbs/11_simulate_spinal_network.png differ diff --git a/docs/gallery_thumbs/12_extract_data_from_neo_blocks.png b/docs/gallery_thumbs/12_extract_data_from_neo_blocks.png new file mode 100644 index 00000000..8e415fa4 Binary files /dev/null and b/docs/gallery_thumbs/12_extract_data_from_neo_blocks.png differ diff --git a/docs/gallery_thumbs/13_load_and_inspect_nwb_data.png b/docs/gallery_thumbs/13_load_and_inspect_nwb_data.png new file mode 100644 index 00000000..68bb5e33 Binary files /dev/null and b/docs/gallery_thumbs/13_load_and_inspect_nwb_data.png differ diff --git a/docs/gallery_thumbs/15_noise_parameter_sweep.png b/docs/gallery_thumbs/15_noise_parameter_sweep.png new file mode 100644 index 00000000..152629f3 Binary files /dev/null and b/docs/gallery_thumbs/15_noise_parameter_sweep.png differ diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 00000000..8b43c144 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,108 @@ +# Getting Started + +!!! info "Requires Python 3.12+" + Check your version with `python --version`. + +## System requirements + +NEURON and an MPI library must be available before installing MyoGen. + +| Platform | Before installing MyoGen | +|----------|--------------------------| +| **Windows** | [NEURON 8.2.7](https://github.com/neuronsimulator/nrn/releases/download/8.2.7/nrn-8.2.7.w64-mingw-py-39-310-311-312-313-setup.exe) — download, run the installer, select "Add to PATH". | +| **Linux** | `sudo apt install libopenmpi-dev` (Ubuntu/Debian) or `sudo dnf install openmpi-devel` (Fedora). | +| **macOS** | `brew install open-mpi`. | + +!!! warning "Windows prerequisites" + You **must** install the following before installing MyoGen on Windows: + + 1. **Visual C++ Build Tools** — install [Visual C++ Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/) with: MSVC Build Tools for x64/x86 (latest), MSVC v143 (VS 2022 C++ x64/x86), Windows 11 SDK (latest), and C++ core desktop features. + 2. **NEURON 8.2.7** — run the [installer](https://github.com/neuronsimulator/nrn/releases/download/8.2.7/nrn-8.2.7.w64-mingw-py-39-310-311-312-313-setup.exe), select **"Add to PATH"**, then restart your terminal. + +## Install + +We recommend [uv](https://docs.astral.sh/uv/), a fast Python package manager. + +=== "uv (recommended)" + + ```bash + # install uv (Linux/macOS) + curl -LsSf https://astral.sh/uv/install.sh | sh + # ...or Windows PowerShell: + # powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" + + # in a fresh project + uv init my_emg_project && cd my_emg_project + uv add myogen + ``` + +=== "pip" + + ```bash + pip install myogen + ``` + +=== "from source (developers)" + + ```bash + git clone https://github.com/NsquaredLab/MyoGen.git + cd MyoGen + uv sync + uv run poe setup_myogen + ``` + +Verify the install: + +```bash +uv run python -c "from myogen import simulator; print('MyoGen installed successfully!')" +``` + +!!! tip "Optional: GPU acceleration" + For 5–10× faster convolutions on an NVIDIA GPU: `uv add cupy-cuda12x`. + +## Quick start + +Generate motor-unit action potentials and surface EMG: + +```python +from myogen import simulator +import quantities as pq + +# 1. Recruitment thresholds for 100 motor units +thresholds, _ = simulator.RecruitmentThresholds( + N=100, recruitment_range__ratio=50, mode="fuglevand" +) + +# 2. Muscle model with fiber distribution +muscle = simulator.Muscle( + recruitment_thresholds=thresholds, + radius_bone__mm=1.0 * pq.mm, + fiber_density__fibers_per_mm2=400 * pq.mm**-2, + fat_thickness__mm=10 * pq.mm, + autorun=True, +) + +# 3. Surface electrode array +electrode_array = simulator.SurfaceElectrodeArray( + num_rows=5, + num_cols=5, + inter_electrode_distance__mm=5 * pq.mm, + electrode_radius__mm=5 * pq.mm, + bending_radius__mm=muscle.radius__mm + muscle.skin_thickness__mm + muscle.fat_thickness__mm, +) + +# 4. Surface EMG simulator +surface_emg = simulator.SurfaceEMG( + muscle_model=muscle, + electrode_arrays=[electrode_array], + sampling_frequency__Hz=2048.0, + MUs_to_simulate=[0, 1, 2, 3, 4], +) + +# 5. Simulate MUAPs and access the results +muaps = surface_emg.simulate_muaps() # neo.Block of MUAP templates +first_mu = muaps.segments[0].analogsignals[0] # everything is inspectable +``` + +For the full pipeline — spike trains, force, intramuscular EMG, spinal networks +— browse the [Examples](auto_examples/01_basic/index.md) gallery. diff --git a/docs/hooks/no_notebooks.py b/docs/hooks/no_notebooks.py new file mode 100644 index 00000000..099bfc0a --- /dev/null +++ b/docs/hooks/no_notebooks.py @@ -0,0 +1,46 @@ +"""mkdocs hook: never ship Jupyter notebooks from the example gallery. + +mkdocs-gallery hard-codes a "Download Jupyter notebook" button into every +rendered example and writes a sibling ``.ipynb`` next to each example's source. +There is no upstream toggle to disable this, so we enforce it here in two +complementary ways: + +1. ``on_page_content`` strips the notebook download button from the rendered + HTML, so it never appears in the UI. +2. ``on_post_build`` deletes every ``.ipynb`` from the built site, so the files + cannot be reached by direct URL either. + +Registered via ``hooks:`` in ``properdocs.yml``. +""" + +import os +import re + +# The notebook button is a markdown link rendered to a

wrapping an whose +# href ends in .ipynb. Match that paragraph (and only that one) for removal. +_NOTEBOOK_BUTTON = re.compile( + r"

\s*]*href=\"[^\"]*\.ipynb\"[^>]*>.*?\s*

", + re.DOTALL | re.IGNORECASE, +) + + +def on_page_content(html, page, config, files): + """Remove the 'Download Jupyter notebook' button from gallery pages.""" + if ".ipynb" in html: + return _NOTEBOOK_BUTTON.sub("", html) + return html + + +def on_post_build(config): + """Delete every generated .ipynb from the built site.""" + site_dir = config["site_dir"] + removed = 0 + for root, _dirs, names in os.walk(site_dir): + for name in names: + if name.endswith(".ipynb"): + os.remove(os.path.join(root, name)) + removed += 1 + if removed: + from mkdocs import utils + + utils.log.info("no_notebooks hook: removed %d .ipynb file(s) from site", removed) diff --git a/docs/source/_static/myogen_logo.png b/docs/images/myogen_logo.png similarity index 100% rename from docs/source/_static/myogen_logo.png rename to docs/images/myogen_logo.png diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..a01fd920 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,63 @@ +
+

MyoGen

+ +**The modular and extensible simulation toolkit for neurophysiology** + +[Getting Started](getting-started.md){ .md-button .md-button--primary } · +[Examples](auto_examples/01_basic/index.md){ .md-button } · +[API Reference](api/index.md){ .md-button } +
+ +## Overview + +MyoGen is a **modular and extensible neuromuscular simulation framework** for +generating physiologically grounded motor-unit activity, muscle force, and +surface EMG signals. + +It supports end-to-end modeling of the neuromuscular pathway — from descending +neural drive and spinal motor-neuron dynamics to muscle activation and +bioelectric signal formation at the electrode level. MyoGen is designed for +algorithm validation, hypothesis-driven research, and education, providing +configurable building blocks that can be combined and extended independently. + +## Highlights + +- 🧬 **Biophysically inspired neuron models** — NEURON-based motor neurons with validated calcium dynamics and membrane properties. +- 🎯 **Everything is inspectable** — complete access to every motor unit, spike time, and fiber location for rigorous algorithm testing. +- ⚡️ **Vectorized & parallel** — multi-core CPU processing with NumPy/Numba vectorization for fast computation. +- 🔬 **End-to-end simulation** — from motor-unit recruitment to high-density surface EMG in a single framework. +- 📊 **Reproducible science** — deterministic random seeds and standardized Neo Block outputs for exact replication. +- 📦 **NWB export** — optional export to [Neurodata Without Borders](https://www.nwb.org/) for data sharing via DANDI. +- 🧰 **Comprehensive toolkit** — surface EMG, intramuscular EMG, force generation, and spinal-network modeling. + +Head to [Getting Started](getting-started.md) to install MyoGen and run your +first simulation, browse the [Examples](auto_examples/01_basic/index.md) +gallery, or dive into the [API Reference](api/index.md). + +## How to cite + +If you use MyoGen in your research, please cite: + +```bibtex +@article{simpetru_molinari_2026_myogen, + title = {MyoGen: Unified Biophysical Modeling of Human Neuromotor Activity and Resulting Signals}, + author = {S{\^i}mpetru, Raul C. and Molinari, Ricardo G. and Rohlf, Devon R. and + Batichotti, Rebeka L. and Watanabe, Renato N. and + Elias, Leonardo A. and Del Vecchio, Alessandro}, + journal = {bioRxiv}, + note = {preprint}, + year = {2026}, + doi = {10.64898/2026.01.01.697284}, + url = {https://www.biorxiv.org/content/10.64898/2026.01.01.697284} +} +``` + +## Contributing + +Contributions are welcome — see the +[issue tracker](https://github.com/NsquaredLab/MyoGen/issues) to report a bug or +propose a feature. + +## License + +MyoGen is released under the **AGPL-3.0-or-later** license. diff --git a/docs/javascripts/mathjax.js b/docs/javascripts/mathjax.js new file mode 100644 index 00000000..c7b2ed27 --- /dev/null +++ b/docs/javascripts/mathjax.js @@ -0,0 +1,21 @@ +// MathJax 3 configuration for pymdownx.arithmatex (generic mode). +// Re-typesets after mkdocs-material instant navigation swaps the page body. +window.MathJax = { + tex: { + inlineMath: [["\\(", "\\)"]], + displayMath: [["\\[", "\\]"]], + processEscapes: true, + processEnvironments: true, + }, + options: { + ignoreHtmlClass: ".*|", + processHtmlClass: "arithmatex", + }, +}; + +document$.subscribe(() => { + MathJax.startup.output.clearCache(); + MathJax.typesetClear(); + MathJax.texReset(); + MathJax.typesetPromise(); +}); diff --git a/docs/make.bat b/docs/make.bat deleted file mode 100644 index dc1312ab..00000000 --- a/docs/make.bat +++ /dev/null @@ -1,35 +0,0 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=source -set BUILDDIR=build - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.https://www.sphinx-doc.org/ - exit /b 1 -) - -if "%1" == "" goto help - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% - -:end -popd diff --git a/docs/neo-blocks.md b/docs/neo-blocks.md new file mode 100644 index 00000000..f04c62aa --- /dev/null +++ b/docs/neo-blocks.md @@ -0,0 +1,329 @@ +# Working with Neo Block Data Structures + +MyoGen uses **Neo Block** objects as the primary data containers for storing simulation results. This guide explains how to extract and work with data from the different Block types. + +## Overview + +MyoGen uses five different Neo Block types to store simulation data: + +| Block Type | Purpose | Dimensions | +|---|---|---| +| `SPIKE_TRAIN__Block` | Neural firing times | 1D spike times | +| `SURFACE_MUAP__Block` | Surface MUAP templates | 3D electrode grid | +| `SURFACE_EMG__Block` | Surface EMG signals | 3D electrode grid | +| `INTRAMUSCULAR_MUAP__Block` | Intramuscular MUAP templates | 2D electrode array | +| `INTRAMUSCULAR_EMG__Block` | Intramuscular EMG signals | 2D electrode array | + +Neo Blocks provide standardized containers with: + +- Automatic unit tracking (mV, µV, seconds, etc.) +- Metadata storage (sampling rate, duration, etc.) +- Compatibility with analysis tools like Elephant +- Integration with neuroscience workflows + +## Quick Start + +### Basic Access Patterns + +```python +import joblib + +# Load spike trains +spike_trains = joblib.load("spike_trains.pkl") +spiketrain = spike_trains.segments[0].spiketrains[0] +spike_times = spiketrain.magnitude # NumPy array + +# Load surface EMG +surface_emg = joblib.load("surface_emg.pkl") +emg_signal = surface_emg.groups[0].segments[0].analogsignals[0] +electrode_data = emg_signal[:, row, col] # Time series at electrode + +# Load intramuscular EMG +im_emg = joblib.load("intramuscular_emg.pkl") +emg_signal = im_emg.segments[0].analogsignals[0] +electrode_data = emg_signal[:, electrode_idx] # Time series +``` + +## Block Structure Details + +### SPIKE_TRAIN__Block + +**Structure**: Block → Segments (motor pools) → SpikeTrain objects + +The `SPIKE_TRAIN__Block` stores neural firing patterns from motor neuron pools. Each segment represents a motor pool, and each spiketrain contains the firing times of an individual neuron. + +```python +# Access spike trains +motor_pool = spike_train__Block.segments[pool_idx] +spiketrain = motor_pool.spiketrains[neuron_idx] + +# Extract data +spike_times__s = spiketrain.magnitude # NumPy array +n_spikes = len(spiketrain) +duration = spiketrain.t_stop +sampling_rate = spiketrain.sampling_rate +``` + +**Example: Calculate firing rate** + +```python +motor_pool = spike_train__Block.segments[0] +firing_rates = [] + +for spiketrain in motor_pool.spiketrains: + if len(spiketrain) > 0: + rate = (len(spiketrain) / (spiketrain.t_stop - spiketrain.t_start)).rescale("Hz") + firing_rates.append(rate.magnitude) + +print(f"Mean firing rate: {np.mean(firing_rates):.2f} Hz") +``` + +### SURFACE_MUAP__Block + +**Structure**: Block → Groups (electrode arrays) → Segments (MUAP indices) → AnalogSignals (3D) + +Stores Motor Unit Action Potential (MUAP) templates for surface electrode arrays. Each group represents an electrode array, each segment represents a MUAP, and signals are 3D arrays (time × rows × columns). + +```python +# Access MUAPs +electrode_array = muaps__Block.groups[array_idx] +muap_segment = electrode_array.segments[muap_idx] +muap_signal = muap_segment.analogsignals[0] + +# Extract data +muap_array = muap_signal.magnitude # Shape: (time, rows, cols) +time__s = muap_signal.times.magnitude +electrode_muap = muap_signal[:, row, col] # Specific electrode +``` + +**Example: Analyze MUAP amplitude** + +```python +# Get MUAP from motor unit 5, electrode at row 2, column 3 +muap_signal = muaps__Block.groups[0].segments[5].analogsignals[0] + +row, col = 2, 3 +electrode_muap = muap_signal[:, row, col] + +print(f"Peak-to-peak: {np.ptp(electrode_muap.magnitude):.2f} {electrode_muap.units}") +print(f"Max amplitude: {np.max(np.abs(electrode_muap.magnitude)):.2f} {electrode_muap.units}") +``` + +### SURFACE_EMG__Block + +**Structure**: Block → Groups (electrode arrays) → Segments (motor pools) → AnalogSignals (3D) + +Stores synthesized surface EMG signals. Similar structure to `SURFACE_MUAP__Block`, but contains the full EMG signal (longer duration) rather than templates. + +```python +# Access surface EMG +electrode_array = surface_emg__Block.groups[array_idx] +motor_pool = electrode_array.segments[pool_idx] +emg_signal = motor_pool.analogsignals[0] + +# Extract data +emg_array = emg_signal.magnitude # Shape: (time, rows, cols) +electrode_emg = emg_signal[:, row, col] # Specific electrode +``` + +**Example: Calculate RMS amplitude** + +```python +emg_signal = surface_emg__Block.groups[0].segments[0].analogsignals[0] +electrode_emg = emg_signal[:, 2, 2].magnitude + +rms = np.sqrt(np.mean(electrode_emg ** 2)) +print(f"RMS amplitude: {rms:.2f} {emg_signal.units}") +``` + +### INTRAMUSCULAR_MUAP__Block + +**Structure**: Block → Segments (MUAP indices) → AnalogSignals (2D) + +Stores MUAP templates for intramuscular electrodes. Each segment represents a MUAP, and signals are 2D arrays (time × electrodes). + +```python +# Access intramuscular MUAPs +muap_segment = im_muaps__Block.segments[muap_idx] +muap_signal = muap_segment.analogsignals[0] + +# Extract data +muap_array = muap_signal.magnitude # Shape: (time, electrodes) +electrode_muap = muap_signal[:, electrode_idx] +``` + +### INTRAMUSCULAR_EMG__Block + +**Structure**: Block → Segments (motor pools) → AnalogSignals (2D) + +Stores synthesized intramuscular EMG signals. Each segment represents a motor pool, and signals are 2D arrays (time × electrodes). + +```python +# Access intramuscular EMG +motor_pool = im_emg__Block.segments[pool_idx] +emg_signal = motor_pool.analogsignals[0] + +# Extract data +emg_array = emg_signal.magnitude # Shape: (time, electrodes) +electrode_emg = emg_signal[:, electrode_idx] +``` + +## Common Operations + +### Accessing Metadata + +All Neo signals have metadata properties: + +```python +signal.magnitude # NumPy array of values +signal.times.magnitude # Time axis (in seconds) +signal.sampling_rate # Sampling frequency +signal.sampling_period # Time between samples +signal.t_start # Start time +signal.t_stop # Stop time +signal.duration # Total duration +signal.units # Physical units (mV, uV, etc.) +signal.shape # Array dimensions +``` + +### Extracting Time Windows + +```python +# For spike trains +windowed_train = spiketrain.time_slice(t_start, t_stop) + +# For analog signals +start_idx = int(t_start * emg_signal.sampling_rate.magnitude) +end_idx = int(t_stop * emg_signal.sampling_rate.magnitude) +windowed_signal = emg_signal[start_idx:end_idx] +``` + +### Converting Units + +```python +import quantities as pq + +# Check current units +print(signal.units) # e.g., mV + +# Convert to different units +signal_uV = signal.rescale(pq.uV) +``` + +### Saving and Loading + +```python +import joblib + +# Save blocks +joblib.dump(spike_train__Block, "spike_trains.pkl") +joblib.dump(surface_emg__Block, "surface_emg.pkl") + +# Load blocks +spike_trains = joblib.load("spike_trains.pkl") +surface_emg = joblib.load("surface_emg.pkl") +``` + +## Shape Reference + +| Block Type | Dimensionality | Shape Format | +|---|---|---| +| SPIKE_TRAIN | 1D | `(n_spikes,)` | +| SURFACE_MUAP | 3D | `(time, rows, cols)` | +| SURFACE_EMG | 3D | `(time, rows, cols)` | +| INTRAMUSCULAR_MUAP | 2D | `(time, electrodes)` | +| INTRAMUSCULAR_EMG | 2D | `(time, electrodes)` | + +## Data Flow Pipeline + +The simulation pipeline follows this flow: + +1. **Generate Spike Trains** → `SPIKE_TRAIN__Block` + + - When neurons fire + +2. **Generate MUAPs** → `SURFACE_MUAP__Block` or `INTRAMUSCULAR_MUAP__Block` + + - What individual motor units look like + +3. **Convolve MUAPs with Spike Trains** → `SURFACE_EMG__Block` or `INTRAMUSCULAR_EMG__Block` + + - Final synthesized EMG signals + +## Complete Example + +```python +import joblib +import numpy as np +import matplotlib.pyplot as plt +from myogen.utils.types import SPIKE_TRAIN__Block, SURFACE_EMG__Block + +# 1. Load data +spike_trains = joblib.load("spike_trains.pkl") +surface_emg = joblib.load("surface_emg.pkl") + +# 2. Extract spike times +motor_pool = spike_trains.segments[0] +neuron_5 = motor_pool.spiketrains[5] +spike_times = neuron_5.magnitude + +# 3. Extract EMG from center electrode +emg_signal = surface_emg.groups[0].segments[0].analogsignals[0] +center_row = emg_signal.shape[1] // 2 +center_col = emg_signal.shape[2] // 2 +electrode_emg = emg_signal[:, center_row, center_col] + +# 4. Plot +fig, (ax1, ax2) = plt.subplots(2, 1, sharex=True) + +# Spike raster +ax1.scatter(spike_times, [0]*len(spike_times), s=10, c='black') +ax1.set_ylabel('Neuron 5') +ax1.set_ylim(-0.5, 0.5) + +# EMG signal +time = emg_signal.times.magnitude +ax2.plot(time, electrode_emg.magnitude) +ax2.set_xlabel('Time (s)') +ax2.set_ylabel(f'EMG ({electrode_emg.units})') + +plt.tight_layout() +plt.show() +``` + +## Troubleshooting + +### Missing 'groups' attribute + +**Issue**: `AttributeError: 'Block' object has no attribute 'groups'` + +**Solution**: This is likely a spike train or intramuscular block. Those use `segments` not `groups`. + +### IndexError when accessing electrodes + +**Issue**: `IndexError` when accessing electrodes + +**Solution**: Check the shape first: + +```python +print(signal.shape) # e.g., (150000, 13, 5) +# Then access within bounds: signal[:, 0:13, 0:5] +``` + +### Wrong units + +**Issue**: Values seem wrong (very large or very small) + +**Solution**: Check the units: + +```python +print(signal.units) # e.g., mV or µV +signal.rescale(pq.mV) # Convert to desired unit +``` + +## Additional Resources + +- Example: `examples/01_basic/12_extract_data_from_neo_blocks.py` +- [Neo Documentation](https://neo.readthedocs.io/) +- [Elephant (Electrophysiology Analysis)](https://elephant.readthedocs.io/) +- [Quantities (Unit Handling)](https://python-quantities.readthedocs.io/) diff --git a/docs/neo_blocks.md b/docs/neo_blocks.md deleted file mode 100644 index 5b6bbdaa..00000000 --- a/docs/neo_blocks.md +++ /dev/null @@ -1,127 +0,0 @@ -# Neo Blocks in MyoGen - -MyoGen uses Neo Block objects to store simulation results with automatic unit tracking and metadata. - -## Block Types - -| Block Type | What it stores | Shape | Access Pattern | -|------------|---------------|-------|----------------| -| SPIKE_TRAIN__Block | Spike times | `(n_spikes,)` | `block.segments[pool].spiketrains[neuron]` | -| SURFACE_MUAP__Block | MUAP templates (surface grid) | `(time, rows, cols)` | `block.groups[array].segments[mu].analogsignals[0]` | -| SURFACE_EMG__Block | EMG signals (surface grid) | `(time, rows, cols)` | `block.groups[array].segments[pool].analogsignals[0]` | -| INTRAMUSCULAR_MUAP__Block | MUAP templates (needle) | `(time, electrodes)` | `block.segments[mu].analogsignals[0]` | -| INTRAMUSCULAR_EMG__Block | EMG signals (needle) | `(time, electrodes)` | `block.segments[pool].analogsignals[0]` | - -## Structure Diagrams - -``` -SPIKE_TRAIN__Block SURFACE_EMG__Block / SURFACE_MUAP__Block -────────────────── ──────────────────────────────────────── -Block Block -└── Segment[pool_idx] └── Group[array_idx] - └── SpikeTrain[neuron_idx] └── Segment[pool/muap_idx] - → [t1, t2, t3, ...] └── AnalogSignal[0] - → 3D array (time × rows × cols) - -INTRAMUSCULAR_EMG__Block / INTRAMUSCULAR_MUAP__Block -──────────────────────────────────────────────────── -Block -└── Segment[pool/muap_idx] - └── AnalogSignal[0] - → 2D array (time × electrodes) -``` - -## Quick Start - -```python -import joblib - -# Load -data = joblib.load("results.pkl") - -# Spike trains -spike_times = data.segments[0].spiketrains[0].magnitude # numpy array - -# Surface EMG/MUAP (3D grid) -signal = data.groups[0].segments[0].analogsignals[0] -electrode_trace = signal[:, row, col].magnitude # single electrode -time = signal.times.magnitude - -# Intramuscular EMG/MUAP (2D array) -signal = data.segments[0].analogsignals[0] -electrode_trace = signal[:, electrode_idx].magnitude -``` - -## Common Operations - -```python -# Metadata -signal.sampling_rate # e.g., 2048 Hz -signal.t_start / t_stop # Start/stop time -signal.units # e.g., mV, µV -signal.shape # Array dimensions - -# Convert to numpy -values = signal.magnitude -time = signal.times.magnitude - -# Slice time window -windowed = spiketrain.time_slice(t_start, t_stop) - -# Calculate RMS -rms = np.sqrt(np.mean(signal.magnitude ** 2)) - -# Mean firing rate (spikes over the active duration) -rate = (len(spiketrain) / (spiketrain.t_stop - spiketrain.t_start)).rescale("Hz") -``` - -## Iteration Examples - -```python -# All spike trains -for segment in block.segments: - for st in segment.spiketrains: - print(f"{st.name}: {len(st)} spikes") - -# All surface MUAPs -for group in block.groups: - for segment in group.segments: - muap = segment.analogsignals[0] - print(f"{segment.name}: shape {muap.shape}") - -# All intramuscular signals -for segment in block.segments: - signal = segment.analogsignals[0] - print(f"{segment.name}: shape {signal.shape}") -``` - -## Key Differences - -| | Surface | Intramuscular | -|--|---------|---------------| -| **Electrodes** | 2D grid (rows × cols) | 1D array | -| **Signal shape** | 3D: `(time, rows, cols)` | 2D: `(time, electrodes)` | -| **Has Groups?** | Yes (multiple arrays) | No | - -| | MUAP | EMG | -|--|------|-----| -| **Duration** | Short (~10-50ms) | Long (full simulation) | -| **Content** | Template per motor unit | Full signal (MUAPs convolved with spikes) | -| **Segments named** | `MUAP_0`, `MUAP_1`... | `Pool_0`, `Pool_1`... | - -## Troubleshooting - -**`AttributeError: 'Block' object has no attribute 'groups'`** -→ This is a spike train or intramuscular block. Use `block.segments` instead. - -**`IndexError` when accessing electrodes** -→ Check shape first: `print(signal.shape)` - -**Values seem wrong** -→ Check units: `print(signal.units)` and use `signal.rescale(pq.mV)` if needed. - -## See Also - -- Example: `examples/01_basic/12_extract_data_from_neo_blocks.py` -- Neo docs: https://neo.readthedocs.io/ -- Elephant docs: https://elephant.readthedocs.io/ diff --git a/docs/source/README.md b/docs/source/README.md deleted file mode 100644 index 25a89c8b..00000000 --- a/docs/source/README.md +++ /dev/null @@ -1,149 +0,0 @@ -
- - -
- - [![Documentation](https://img.shields.io/badge/docs-latest-blue.svg)](https://nsquaredlab.github.io/MyoGen/) - [![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/) - [![Version](https://img.shields.io/badge/version-0.5.0-orange.svg)](https://github.com/NsquaredLab/MyoGen) - - [Installation](#installation) • - [Documentation](https://nsquaredlab.github.io/MyoGen/) • - [Examples](https://nsquaredlab.github.io/MyoGen/examples/) • - [Paper](#citation) -
- -# MyoGen - The modular and extensible simulation toolkit for neurophysiology - -MyoGen is a **modular and extensible neuromuscular simulation framework** for generating physiologically grounded motor-unit activity, muscle force, and surface EMG signals. - -It supports end-to-end modeling of the neuromuscular pathway, from descending neural drive and spinal motor neuron dynamics to muscle activation and bioelectric signal formation at the electrode level. -MyoGen is designed for algorithm validation, hypothesis-driven research, and education, providing configurable building blocks that can be independently combined and extended. - -## Highlights - -🧬 **Biophysically inspired neuron models** — NEURON-based motor neurons with validated calcium dynamics and membrane properties - -🎯 **Everything is inspectable** — Complete access to every motor unit, spike time, fiber location etc. for rigorous algorithm testing - -⚡️ **Vectorized & parallel** — Multi-core CPU processing with NumPy/Numba vectorization for fast computation - -🔬 **End-to-end simulation** — From motor unit recruitment to high-density surface EMG in a single framework - -📊 **Reproducible science** — Deterministic random seeds and standardized Neo Block outputs for exact replication - -🧰 **Comprehensive toolkit** — Surface EMG, intramuscular EMG, force generation, and spinal network modeling - -## Installation - -**Prerequisites**: Python ≥3.12, Linux/Windows/macOS - -> [!IMPORTANT] -> **Windows users**: Install [NEURON 8.2.7](https://github.com/neuronsimulator/nrn/releases/download/8.2.7/nrn-8.2.7.w64-mingw-py-39-310-311-312-313-setup.exe) before running `uv sync` - -```bash -# Clone and install -git clone https://github.com/NsquaredLab/MyoGen.git -cd MyoGen -uv sync - -# Activate environment -source .venv/bin/activate # Linux/macOS -.venv\Scripts\activate # Windows - -# Compile NEURON mechanisms (required) -uv run poe setup_myogen -``` - -> [!TIP] -> Install [uv](https://docs.astral.sh/uv/) first - -**GPU acceleration of convoutions** (optional): -```bash -uv pip install cupy-cuda12x # 5-10× speedup -``` - -## Quick Start - -Generate motor unit action potentials (MUAPs): - -```python -from myogen import simulator -import quantities as pq - -# 1. Generate recruitment thresholds (100 motor units) -thresholds, _ = simulator.RecruitmentThresholds( - N=100, - recruitment_range__ratio=50, - mode="fuglevand" -) - -# 2. Create muscle model with fiber distribution -muscle = simulator.Muscle( - recruitment_thresholds=thresholds, - radius_bone__mm=1.0 * pq.mm, - fiber_density__fibers_per_mm2=400 * pq.mm**-2, - fat_thickness__mm=10 * pq.mm, - autorun=True -) - -# 3. Set up surface electrode array -electrode_array = simulator.SurfaceElectrodeArray( - num_rows=5, - num_cols=5, - inter_electrode_distance__mm=5 * pq.mm, - electrode_radius__mm=5 * pq.mm, - bending_radius__mm=muscle.radius__mm + muscle.skin_thickness__mm + muscle.fat_thickness__mm, -) - -# 4. Create surface EMG simulator -surface_emg = simulator.SurfaceEMG( - muscle_model=muscle, - electrode_arrays=[electrode_array], - sampling_frequency__Hz=2048.0, - MUs_to_simulate=[0, 1, 2, 3, 4] # First 5 motor units -) - -# 5. Simulate MUAPs (parallel processing) -muaps = surface_emg.simulate_muaps(n_jobs=-2) -``` - -**Access MUAP data**: - -```python -import numpy as np - -# Get MUAP from motor unit 0 -muap_signal = muaps.groups[0].segments[0].analogsignals[0] -print(f"MUAP shape: {muap_signal.shape}") # (time, rows, cols) - -# Extract from specific electrode (row 2, col 2) -electrode_muap = muap_signal[:, 2, 2] -peak_amplitude = np.max(np.abs(electrode_muap.magnitude)) -print(f"Peak amplitude: {peak_amplitude:.3f} {electrode_muap.units}") -``` - -**For full EMG simulation** with spike trains, see [examples](https://nsquaredlab.github.io/MyoGen/examples/) - -## Documentation - -📖 **[Read the full documentation](https://nsquaredlab.github.io/MyoGen/)** - -- [User Guide](https://nsquaredlab.github.io/MyoGen/neo_blocks_guide.html) — Working with simulation outputs -- [API Reference](https://nsquaredlab.github.io/MyoGen/api/) — Complete class documentation -- [Examples](examples/) — 9 step-by-step tutorials from recruitment to EMG - -## Citation - -If you use MyoGen in your research, please cite: - -TBD - -## Contributing - -Contributions welcome! See [issues](https://github.com/NsquaredLab/MyoGen/issues) if you want to add a feature or fix a bug. - -## License - -MyoGen is AGPL licensed. See [LICENSE](https://github.com/NsquaredLab/MyoGen/LICENSE.md) for details. - diff --git a/docs/source/_static/custom.css b/docs/source/_static/custom.css deleted file mode 100644 index 63f0e7a2..00000000 --- a/docs/source/_static/custom.css +++ /dev/null @@ -1,277 +0,0 @@ -html[data-theme="dark"] img { - filter: none; -} -html[data-theme="dark"] .bd-content img:not(.only-dark):not(.dark-light) { - background: unset; -} - -/* Add bottom margin to the logo image */ -.top-logo-spacing { - margin-bottom: 2em; /* Adjust value as needed */ -} - -/* Increase main article width */ -/* https://pydata-sphinx-theme.readthedocs.io/en/stable/user_guide/layout.html */ -.bd-main .bd-content .bd-article-container { - max-width: 80em; /* default is 60em */ -} - -.bd-page-width { - max-width: 100em; /* default is 88rem */ -} - -/* Hide Sphinx gallery download link notes */ -.sphx-glr-download-link-note.admonition.note { - display: none; -} - -.sphx-glr-footer.sphx-glr-footer-example.docutils.container { - display: none; -} - -.mermaid { - background-color: #ffffff; -} - -/* Improved API documentation styling for vertical parameter display */ -/* Force function/method signatures to display parameters vertically */ -.sig.sig-object { - font-family: 'SFMono-Regular', Monaco, 'Inconsolata', 'Liberation Mono', 'Courier New', monospace !important; - font-size: 0.9em; - line-height: 1.6; - padding: 1rem; - background: var(--color-code-background); - border: 1px solid var(--color-background-border); - border-radius: 0.375rem; - margin: 1rem 0; - overflow-x: auto; -} - -.sig-name { - font-weight: 600; - color: var(--color-api-name); -} - -.sig-paren { - font-weight: normal; -} - -/* Force parameters to display vertically */ -.sig-param { - display: block !important; - padding-left: 2rem; - margin: 0.25rem 0; - text-indent: 0; -} - -/* Improve parameter display */ -.sig .o { - color: var(--color-api-pre-name); -} - -.sig .n { - color: var(--color-api-name); -} - -.sig .p { - color: var(--color-inline-code-links); -} - -/* Better spacing for method signatures */ -dl.py.method > dt, -dl.py.function > dt, -dl.py.class > dt { - background: var(--color-api-background); - border-left: 3px solid var(--color-api-background-hover); - padding: 0.75rem 1rem; - margin-bottom: 0.5rem; -} - -/* Improve autosummary tables */ -.autosummary { - width: 100%; - border-collapse: collapse; - margin: 1rem 0; -} - -.autosummary th, -.autosummary td { - padding: 0.75rem; - border-bottom: 1px solid var(--color-background-border); - text-align: left; - vertical-align: top; -} - -.autosummary th { - background: var(--color-background-secondary); - font-weight: 600; -} - -/* Improve parameter lists in docstrings */ -.field-list { - margin: 1rem 0; -} - -.field-list .field-name { - font-weight: 600; - color: var(--color-api-pre-name); - min-width: 8rem; -} - -.field-list .field-body { - margin-left: 1rem; -} - -/* Better spacing for class documentation */ -.py.class .sig { - margin-bottom: 1.5rem; -} - -/* Improve code blocks in docstrings */ -.docstring .highlight pre { - padding: 1rem; - border-radius: 0.375rem; - background: var(--color-code-background); - border: 1px solid var(--color-background-border); -} - -/* Better rubric styling */ -.rubric { - font-size: 1.1em; - font-weight: 600; - color: var(--color-api-name); - margin: 2rem 0 1rem 0; - padding-bottom: 0.5rem; - border-bottom: 2px solid var(--color-background-border); -} - -/* Improve note and warning admonitions */ -.admonition.note, -.admonition.warning, -.admonition.tip { - border-radius: 0.375rem; - border-left-width: 4px; - margin: 1rem 0; -} - -/* Improve cross-references */ -.reference.internal { - color: var(--color-link); - text-decoration: none; -} - -.reference.internal:hover { - color: var(--color-link-hover); - text-decoration: underline; -} - -/* Make internal Quantity type links in signatures match external Python doc links */ -.sig-param .reference.internal, -.sig .reference.internal { - color: var(--color-link) !important; -} - -.sig-param .reference.internal:hover, -.sig .reference.internal:hover { - color: var(--color-link-hover) !important; - text-decoration: underline; -} - -/* Ensure hoverxref tooltip links have consistent styling */ -a.hxr-hoverxref.reference.internal { - color: var(--color-link) !important; -} - -a.hxr-hoverxref.reference.internal:hover { - color: var(--color-link-hover) !important; -} - -/* Custom spacing for MyoGen API cards */ -.myogen-card { - padding: 0.5rem 0.5rem !important; - background-color: transparent !important; -} - - -.sig-param .o { - margin-left: 0.3em; - margin-right: 0.3em; -} - -/* Ensure stretched link text is hidden */ -.sd-hide-link-text { - font-size: 0 !important; - line-height: 0 !important; - overflow: hidden !important; - clip: rect(0, 0, 0, 0) !important; - white-space: nowrap !important; -} - -/* Type alias display uses standard Sphinx highlight classes */ -dl.py.data dd p .highlight-python { - margin: 0.5rem 0; -} - -dl.py.data dd p .highlight-python pre { - line-height: 1.8; -} - -/* Preserve link styling inside formatted type aliases */ -dl.py.data dd .highlight pre code .xref, -dl.py.data dd .highlight pre code a, -dl.py.data dd .highlight pre code .reference { - color: #2980b9 !important; - text-decoration: none; -} - -dl.py.data dd .highlight pre code .xref:hover, -dl.py.data dd .highlight pre code a:hover, -dl.py.data dd .highlight pre code .reference:hover { - color: #3091d1 !important; - text-decoration: underline; -} - -/* Ensure Pygments syntax highlighting colors work */ -/* But links should always be blue, even if they have other classes */ -dl.py.data dd .highlight pre code a .nc, -dl.py.data dd .highlight pre code a.nc { - color: #2980b9 !important; - font-weight: bold; -} - -dl.py.data dd .highlight pre code .nc { - color: #458588; - font-weight: bold; -} - -dl.py.data dd .highlight pre code a .nn, -dl.py.data dd .highlight pre code a.nn { - color: #2980b9 !important; -} - -dl.py.data dd .highlight pre code .nn { - color: #b16286; -} - -dl.py.data dd .highlight pre code a .n, -dl.py.data dd .highlight pre code a.n { - color: #2980b9 !important; -} - -dl.py.data dd .highlight pre code .n { - color: #83a598; -} - -dl.py.data dd .highlight pre code .s1 { - color: #b8bb26; -} - -dl.py.data dd .highlight pre code .p { - color: #ebdbb2; -} - -/* Style for "alias of" text */ -dl.py.data dd p strong { - font-weight: 600; - color: var(--color-api-name); -} diff --git a/docs/source/_static/custom.js b/docs/source/_static/custom.js deleted file mode 100644 index 18c05916..00000000 --- a/docs/source/_static/custom.js +++ /dev/null @@ -1,217 +0,0 @@ -document.addEventListener('DOMContentLoaded', () => { - // Remove trailing commas from dd elements - document.querySelectorAll('dd').forEach(dd => { - // Check if last child is a text node containing comma - const lastChild = dd.lastChild; - if (lastChild && lastChild.nodeType === Node.TEXT_NODE) { - // Remove the comma (and trim surrounding spaces) - lastChild.textContent = lastChild.textContent.replace(/,/, ''); - } - }); - - // Format type alias annotations with proper indentation - document.querySelectorAll('dl.py.data dd p').forEach(elem => { - const text = elem.textContent; - - // Only process if it contains "alias of Annotated" - if (text.includes('alias of') && text.includes('Annotated')) { - // Clone the element to work with - const clone = elem.cloneNode(true); - - // Get the HTML content - const html = clone.innerHTML; - - // Check if it starts with "alias of" - if (html.includes('alias of')) { - // Format the HTML while preserving links - const formatted = formatTypeAnnotationHTML(clone); - - // Replace content - elem.innerHTML = 'alias of' + formatted; - } - } - }); -}); - -function formatTypeAnnotationHTML(elem) { - // Remove "alias of" text if present - const walker = document.createTreeWalker( - elem, - NodeFilter.SHOW_TEXT, - null, - false - ); - - let node; - while (node = walker.nextNode()) { - node.textContent = node.textContent.replace('alias of', '').trim(); - } - - // Get the HTML content after removing "alias of" - let html = elem.innerHTML.trim(); - - // Format the HTML with proper indentation - let result = '\n
';
-  let indentLevel = 0;
-  const indentSize = 4;
-  let insideTag = false;
-  let currentTag = '';
-  let i = 0;
-
-  while (i < html.length) {
-    const char = html[i];
-
-    if (char === '<') {
-      insideTag = true;
-      currentTag = '';
-      result += char;
-    } else if (char === '>') {
-      insideTag = false;
-      result += char;
-      currentTag = '';
-    } else if (insideTag) {
-      currentTag += char;
-      result += char;
-    } else if (char === '[') {
-      result += char + '\n';
-      indentLevel++;
-      result += ' '.repeat(indentLevel * indentSize);
-    } else if (char === ']') {
-      result += '\n';
-      indentLevel--;
-      result += ' '.repeat(indentLevel * indentSize) + char;
-    } else if (char === ',' && !insideTag) {
-      result += char + '\n';
-      // Skip following space
-      if (i + 1 < html.length && html[i + 1] === ' ') {
-        i++;
-      }
-      result += ' '.repeat(indentLevel * indentSize);
-    } else {
-      result += char;
-    }
-
-    i++;
-  }
-
-  result += '
'; - - // Add links for common types - result = addTypeLinks(result); - - // Wrap in proper Sphinx highlight div for consistent styling - return '
' + result + '
'; -} - -function addTypeLinks(html) { - // Define link mappings with Pygments classes for syntax highlighting - const links = { - 'Annotated': { - url: 'https://docs.python.org/3/library/typing.html#typing.Annotated', - class: 'nc' // class name - }, - 'Quantity': { - url: 'https://python-quantities.readthedocs.io/en/latest/user/tutorial.html#quantity-basics', - class: 'nc' // class name - }, - 'beartype.vale.IsAttr': { - url: 'https://beartype.readthedocs.io/en/latest/api_vale/#beartype.vale.IsAttr', - class: 'n' // name - }, - 'beartype.vale.Is': { - url: 'https://beartype.readthedocs.io/en/latest/api_vale/#beartype.vale.Is', - class: 'n' // name - }, - 'beartype.vale.IsEqual': { - url: 'https://beartype.readthedocs.io/en/latest/api_vale/#beartype.vale.IsEqual', - class: 'n' // name - }, - 'beartype': { - url: 'https://beartype.readthedocs.io/', - class: 'nn' // module name - }, - // Quantities units - 'pq.nA': { - url: 'https://python-quantities.readthedocs.io/en/latest/user/tutorial.html', - class: 'n' // name - }, - 'pq.s': { - url: 'https://python-quantities.readthedocs.io/en/latest/user/tutorial.html', - class: 'n' // name - }, - 'pq.ms': { - url: 'https://python-quantities.readthedocs.io/en/latest/user/tutorial.html', - class: 'n' // name - }, - 'pq.mV': { - url: 'https://python-quantities.readthedocs.io/en/latest/user/tutorial.html', - class: 'n' // name - }, - 'pq.uV': { - url: 'https://python-quantities.readthedocs.io/en/latest/user/tutorial.html', - class: 'n' // name - }, - 'pq.Hz': { - url: 'https://python-quantities.readthedocs.io/en/latest/user/tutorial.html', - class: 'n' // name - }, - 'pq.mm': { - url: 'https://python-quantities.readthedocs.io/en/latest/user/tutorial.html', - class: 'n' // name - }, - 'pq.uS': { - url: 'https://python-quantities.readthedocs.io/en/latest/user/tutorial.html', - class: 'n' // name - }, - 'pq.rad': { - url: 'https://python-quantities.readthedocs.io/en/latest/user/tutorial.html', - class: 'n' // name - }, - 'pq.deg': { - url: 'https://python-quantities.readthedocs.io/en/latest/user/tutorial.html', - class: 'n' // name - }, - 'pq.m': { - url: 'https://python-quantities.readthedocs.io/en/latest/user/tutorial.html', - class: 'n' // name - }, - 'pq.S': { - url: 'https://python-quantities.readthedocs.io/en/latest/user/tutorial.html', - class: 'n' // name - }, - 'pq.N': { - url: 'https://python-quantities.readthedocs.io/en/latest/user/tutorial.html', - class: 'n' // name - }, - }; - - // Sort by length (longest first) to avoid replacing parts of longer names - const sortedKeys = Object.keys(links).sort((a, b) => b.length - a.length); - - for (const term of sortedKeys) { - const {url, class: pygmentsClass} = links[term]; - // Escape dots in term for regex - const escapedTerm = term.replace(/\./g, '\\.'); - - // Split by < to work with text nodes only, avoiding already-linked content - const parts = html.split(/(<[^>]+>)/); - for (let i = 0; i < parts.length; i++) { - // Only process text parts (not HTML tags) - if (!parts[i].startsWith('<')) { - const regex = new RegExp(`\\b(${escapedTerm})\\b`, 'g'); - parts[i] = parts[i].replace(regex, - `$1` - ); - } - } - html = parts.join(''); - } - - // Add syntax highlighting for strings (anything in quotes) - html = html.replace(/'([^']*)'/g, '\'$1\''); - - // Add syntax highlighting for operators - html = html.replace(/(\[|\]|,)/g, '$1'); - - return html; -} diff --git a/docs/source/api/currents_api.rst b/docs/source/api/currents_api.rst deleted file mode 100644 index d3c6aadf..00000000 --- a/docs/source/api/currents_api.rst +++ /dev/null @@ -1,18 +0,0 @@ -Current Generation -================== - -This module contains functions for generating various input current waveforms (ramp, step, sinusoidal, etc.). - - -.. currentmodule:: myogen.utils.currents - -.. autosummary:: - :toctree: ../generated/ - :template: autosummary/function.rst - :recursive: - - create_ramp_current - create_step_current - create_sinusoidal_current - create_sawtooth_current - create_trapezoid_current \ No newline at end of file diff --git a/docs/source/api/index.rst b/docs/source/api/index.rst deleted file mode 100644 index 9815fbee..00000000 --- a/docs/source/api/index.rst +++ /dev/null @@ -1,67 +0,0 @@ -API Documentation -================= - -Welcome to the MyoGen API reference! -This section provides a complete overview of all modules, classes, and functions available in MyoGen for neuromuscular simulation and analysis. - -MyoGen is organized into the following modules: - -.. grid:: 2 - :gutter: 0 - :margin: 0 - - .. card:: MyoGen Core - :link: myogen_api.html - :class-card: sd-shadow-xs sd-bg-light myogen-card - - Package-level functions and global objects for random number generation and setup. - - .. card:: Simulator - :link: simulator_api.html - :class-card: sd-shadow-xs sd-bg-light myogen-card - - Core functionality for neuromuscular simulation, including motor unit recruitment, muscle modeling, and EMG generation. - - .. card:: Utils - :link: utils_api.html - :class-card: sd-shadow-xs sd-bg-light myogen-card - - Utility functions for setup, NMODL file handling, current generation, plotting, and type definitions. - - .. card:: Currents - :link: currents_api.html - :class-card: sd-shadow-xs sd-bg-light myogen-card - - Functions for generating various input current waveforms (ramp, step, sinusoidal, etc.). *(Submodule of Utils)* - - .. card:: Plotting - :link: plotting_api.html - :class-card: sd-shadow-xs sd-bg-light myogen-card - - Visualization tools for simulation results and analysis. *(Submodule of Utils)* - - .. card:: Types - :link: types_api.html - :class-card: sd-shadow-xs sd-bg-light myogen-card - - Type definitions for structured data and type safety. *(Submodule of Utils)* - -**How to use this documentation:** - -- Click on a module above to see all its classes and functions. -- Each API page provides autosummary tables with links to detailed docstrings and usage examples. -- For a practical introduction, see the `examples` section in the documentation sidebar. - -If you are new to MyoGen, start with the Simulator module to understand the core simulation workflow, then explore the utility and plotting modules as needed. - -If you have questions or need further help, please refer to the `README <../../README.md>`_ or open an issue on `GitHub `_. - - -.. toctree:: - :maxdepth: 2 - :caption: API Modules - :hidden: - - myogen_api - simulator_api - utils_api \ No newline at end of file diff --git a/docs/source/api/myogen_api.rst b/docs/source/api/myogen_api.rst deleted file mode 100644 index a59c3ce8..00000000 --- a/docs/source/api/myogen_api.rst +++ /dev/null @@ -1,31 +0,0 @@ -MyoGen Core -=========== - -This module contains the core MyoGen package-level functions and objects for random number generation and setup. - - -.. currentmodule:: myogen - -.. autosummary:: - :toctree: ../generated/ - :template: autosummary/function.rst - :recursive: - - set_random_seed - load_nmodl_mechanisms - -Global Objects --------------- - -.. data:: RANDOM_GENERATOR - :type: numpy.random.Generator - - Global random number generator for reproducibility across MyoGen simulations. - - This is a :class:`numpy.random.Generator` instance initialized with the default seed. - Use :func:`set_random_seed` to change the seed for reproducible simulations. - -.. data:: SEED - :type: int - - Default random seed value (180319) used to initialize :data:`RANDOM_GENERATOR`. diff --git a/docs/source/api/neuron_api.rst b/docs/source/api/neuron_api.rst deleted file mode 100644 index 4a720947..00000000 --- a/docs/source/api/neuron_api.rst +++ /dev/null @@ -1,15 +0,0 @@ -NEURON Utilities -================ - -This module contains utilities for working with NEURON simulations, including current injection and spike train recording. - - -.. currentmodule:: myogen.utils.neuron.inject_currents_into_populations - -.. autosummary:: - :toctree: ../generated/ - :template: autosummary/function.rst - :recursive: - - inject_currents_into_populations - inject_currents_and_simulate_spike_trains diff --git a/docs/source/api/plotting_api.rst b/docs/source/api/plotting_api.rst deleted file mode 100644 index cd729daa..00000000 --- a/docs/source/api/plotting_api.rst +++ /dev/null @@ -1,19 +0,0 @@ -Plotting & Visualization -======================== - -This module contains functions for visualizing simulation results and analysis. -All functions will return an Axes or a list of Axes for use in matplotlib. - -.. currentmodule:: myogen.utils.plotting - -.. autosummary:: - :toctree: ../generated/ - :template: autosummary/function.rst - :recursive: - - plot_raster_spikes - plot_membrane_potentials - plot_muscle_dynamics - plot_antagonist_muscle_comparison - plot_spindle_dynamics - plot_gto_dynamics \ No newline at end of file diff --git a/docs/source/api/simulator_api.rst b/docs/source/api/simulator_api.rst deleted file mode 100644 index b0f5672f..00000000 --- a/docs/source/api/simulator_api.rst +++ /dev/null @@ -1,168 +0,0 @@ -Simulator Module -================ - -.. currentmodule:: myogen.simulator - -Motor Unit Recruitment Thresholds -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. autosummary:: - :toctree: ../generated/ - :template: autosummary/class.rst - :recursive: - - RecruitmentThresholds - -Neuron Populations -^^^^^^^^^^^^^^^^^^ - -.. currentmodule:: myogen.simulator.neuron.populations - -Base Class ----------- - -.. autosummary:: - :toctree: ../generated/ - :template: autosummary/class.rst - :recursive: - - _Pool - -Population Classes ------------------- - -.. autosummary:: - :toctree: ../generated/ - :template: autosummary/class.rst - :recursive: - - AlphaMN__Pool - DescendingDrive__Pool - AffIa__Pool - AffII__Pool - AffIb__Pool - GII__Pool - GIb__Pool - -Network -------- - -.. currentmodule:: myogen.simulator.neuron.network - -.. autosummary:: - :toctree: ../generated/ - :template: autosummary/class.rst - :recursive: - - Network - -Simulation Runner ------------------ - -.. currentmodule:: myogen.simulator.neuron.simulation_runner - -.. autosummary:: - :toctree: ../generated/ - :template: autosummary/class.rst - :recursive: - - SimulationRunner - -.. currentmodule:: myogen.simulator - -Muscle Model -^^^^^^^^^^^^ - -.. autosummary:: - :toctree: ../generated/ - :template: autosummary/class.rst - :recursive: - - Muscle - -Hill Muscle Model -^^^^^^^^^^^^^^^^^ - -.. currentmodule:: myogen.simulator.neuron.muscle - -.. autosummary:: - :toctree: ../generated/ - :template: autosummary/class.rst - :recursive: - - HillModel - -Proprioception Models -^^^^^^^^^^^^^^^^^^^^^ - -.. currentmodule:: myogen.simulator.neuron.proprioception - -.. autosummary:: - :toctree: ../generated/ - :template: autosummary/class.rst - :recursive: - - SpindleModel - GolgiTendonOrganModel - -Joint Dynamics -^^^^^^^^^^^^^^ - -.. currentmodule:: myogen.simulator.neuron.joint_dynamics - -.. autosummary:: - :toctree: ../generated/ - :template: autosummary/class.rst - :recursive: - - JointDynamics - -.. currentmodule:: myogen.simulator - -Force Model -^^^^^^^^^^^ - -.. autosummary:: - :toctree: ../generated/ - :template: autosummary/class.rst - :recursive: - - ForceModel - -Biomechanics -^^^^^^^^^^^^ - -.. currentmodule:: myogen.simulator.core.force - -.. autosummary:: - :toctree: ../generated/ - :template: autosummary/class.rst - :recursive: - - MuscleGeometry - JointGeometry - JointBiomechanics - -.. currentmodule:: myogen.simulator - -Electrodes -^^^^^^^^^^ - -.. autosummary:: - :toctree: ../generated/ - :template: autosummary/class.rst - :recursive: - - SurfaceElectrodeArray - IntramuscularElectrodeArray - -EMG Generation -^^^^^^^^^^^^^^ - -.. autosummary:: - :toctree: ../generated/ - :template: autosummary/class.rst - :recursive: - - SurfaceEMG - IntramuscularEMG diff --git a/docs/source/api/types_api.rst b/docs/source/api/types_api.rst deleted file mode 100644 index 790a68eb..00000000 --- a/docs/source/api/types_api.rst +++ /dev/null @@ -1,117 +0,0 @@ -Type Definitions -================ - -This module contains type definitions for structured data and type safety with Beartype validation. - -.. currentmodule:: myogen.utils.types - -Physical Quantity Types -^^^^^^^^^^^^^^^^^^^^^^^ - -Time Units ----------- - -.. autosummary:: - :toctree: ../generated/ - :template: autosummary/data.rst - - Quantity__s - Quantity__ms - -Angles ------- - -.. autosummary:: - :toctree: ../generated/ - :template: autosummary/data.rst - - Quantity__rad - Quantity__deg - -Electrical Potential --------------------- - -.. autosummary:: - :toctree: ../generated/ - :template: autosummary/data.rst - - Quantity__mV - Quantity__uV - -Electrical Current ------------------- - -.. autosummary:: - :toctree: ../generated/ - :template: autosummary/data.rst - - Quantity__nA - -Electrical Conductance ----------------------- - -.. autosummary:: - :toctree: ../generated/ - :template: autosummary/data.rst - - Quantity__uS - Quantity__S_per_m - -Frequency ---------- - -.. autosummary:: - :toctree: ../generated/ - :template: autosummary/data.rst - - Quantity__Hz - Quantity__pps - -Length & Areas --------------- - -.. autosummary:: - :toctree: ../generated/ - :template: autosummary/data.rst - - Quantity__mm - Quantity__m - Quantity__mm2 - Quantity__per_mm2 - -Velocity --------- - -.. autosummary:: - :toctree: ../generated/ - :template: autosummary/data.rst - - Quantity__m_per_s - Quantity__mm_per_s - -Signal Types (Neo) -^^^^^^^^^^^^^^^^^^ - -.. autosummary:: - :toctree: ../generated/ - :template: autosummary/data.rst - - CURRENT__AnalogSignal - FORCE__AnalogSignal - SPIKE_TRAIN__Block - SURFACE_MUAP__Block - SURFACE_EMG__Block - INTRAMUSCULAR_MUAP__Block - INTRAMUSCULAR_EMG__Block - -Array Types -^^^^^^^^^^^ - -.. autosummary:: - :toctree: ../generated/ - :template: autosummary/data.rst - - CORTICAL_INPUT__MATRIX - RECRUITMENT_THRESHOLDS__ARRAY - JOINT_ANGLE__ARRAY - MOMENT_ARM__MATRIX diff --git a/docs/source/api/utils_api.rst b/docs/source/api/utils_api.rst deleted file mode 100644 index 2bb9275b..00000000 --- a/docs/source/api/utils_api.rst +++ /dev/null @@ -1,146 +0,0 @@ -Utils Module -============ - -This module contains utility functions for setup, NMODL file handling, current generation, plotting, and type definitions. - - -Current Generation -^^^^^^^^^^^^^^^^^^ - -.. currentmodule:: myogen.utils.currents - -.. autosummary:: - :toctree: ../generated/ - :template: autosummary/function.rst - - create_ramp_current - create_step_current - create_sinusoidal_current - create_sawtooth_current - create_trapezoid_current - - -NEURON Utilities -^^^^^^^^^^^^^^^^ - -.. currentmodule:: myogen.utils.neuron.inject_currents_into_populations - -.. autosummary:: - :toctree: ../generated/ - :template: autosummary/function.rst - - inject_currents_into_populations - inject_currents_and_simulate_spike_trains - - -Continuous Saver -^^^^^^^^^^^^^^^^ - -.. currentmodule:: myogen.utils.continuous_saver - -.. autosummary:: - :toctree: ../generated/ - :template: autosummary/class.rst - - ContinuousSaver - -.. autosummary:: - :toctree: ../generated/ - :template: autosummary/function.rst - - convert_chunks_to_neo - - -NWB Export -^^^^^^^^^^ - -Export simulation data to Neurodata Without Borders (NWB) format for sharing -and interoperability with the broader neuroscience ecosystem. - -.. note:: - NWB export requires optional dependencies. Install with: ``pip install myogen[nwb]`` - -.. currentmodule:: myogen.utils.nwb - -.. autosummary:: - :toctree: ../generated/ - :template: autosummary/function.rst - - export_to_nwb - export_simulation_to_nwb - validate_nwb - - -Plotting & Visualization -^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. currentmodule:: myogen.utils.plotting - -.. autosummary:: - :toctree: ../generated/ - :template: autosummary/function.rst - - plot_raster_spikes - plot_membrane_potentials - plot_muscle_dynamics - plot_antagonist_muscle_comparison - plot_spindle_dynamics - plot_gto_dynamics - - -Type Definitions -^^^^^^^^^^^^^^^^ - -.. currentmodule:: myogen.utils.types - -Quantity Types -"""""""""""""" - -.. autosummary:: - :toctree: ../generated/ - :template: autosummary/data.rst - - Quantity__s - Quantity__ms - Quantity__rad - Quantity__deg - Quantity__mV - Quantity__uV - Quantity__nA - Quantity__uS - Quantity__S_per_m - Quantity__Hz - Quantity__pps - Quantity__mm - Quantity__m - Quantity__mm2 - Quantity__per_mm2 - Quantity__m_per_s - Quantity__mm_per_s - -Neo Data Types -"""""""""""""" - -.. autosummary:: - :toctree: ../generated/ - :template: autosummary/data.rst - - CURRENT__AnalogSignal - FORCE__AnalogSignal - SPIKE_TRAIN__Block - SURFACE_MUAP__Block - SURFACE_EMG__Block - INTRAMUSCULAR_MUAP__Block - INTRAMUSCULAR_EMG__Block - -Matrix & Array Types -"""""""""""""""""""" - -.. autosummary:: - :toctree: ../generated/ - :template: autosummary/data.rst - - CORTICAL_INPUT__MATRIX - RECRUITMENT_THRESHOLDS__ARRAY - JOINT_ANGLE__ARRAY - MOMENT_ARM__MATRIX \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py deleted file mode 100644 index 1611c821..00000000 --- a/docs/source/conf.py +++ /dev/null @@ -1,741 +0,0 @@ -import sys -from datetime import datetime -from pathlib import Path - -import toml -from sphinx_gallery.sorting import ExplicitOrder, FileNameSortKey - - -def reset_neuron(gallery_conf, fname): - """ - Reset NEURON state between Sphinx Gallery examples. - - NEURON's HOC interpreter maintains global state that persists across - examples when they run in the same Python process. This causes examples - to fail on first run but succeed on subsequent runs. - """ - try: - # Import myogen to ensure NEURON is properly initialized - # (myogen auto-loads mechanisms and sets up NEURON on import) - import myogen # noqa: F401 - from neuron import h - - # Delete all sections to start fresh - for sec in list(h.allsec()): - h.delete_section(sec=sec) - - # Load stdrun.hoc and reset time variables - h.load_file("stdrun.hoc") - h.t = 0 - h.tstop = 0 - - except (ImportError, RuntimeError, LookupError, AttributeError): - pass # NEURON not available or not initialized, skip reset - -# Setup paths -base_dir = Path(__file__).parent.parent.parent -sys.path.insert(0, str(base_dir)) - -# Project Information from pyproject.toml -pyproject_data = toml.load(base_dir / "pyproject.toml") -project_info = pyproject_data["project"] - -project = project_info["name"] -author = ", ".join( - [f"{a.get('name', '')} ({a.get('email', '')})" for a in project_info.get("authors", [])] -) -release = version = project_info["version"] -copyright = f"2025 - {datetime.now().year}, n-squared lab, FAU Erlangen-Nürnberg, Germany" - -# Import the main package -import myogen - - -# Copy README.md from root to docs/source with path fixes -def copy_and_prepare_readme(): - """Copy README.md to docs/source and append RST sections.""" - import re - - readme_src = base_dir / "README.md" - index_rst = Path(__file__).parent / "index.rst" - - if readme_src.exists(): - content = readme_src.read_text(encoding="utf-8") - - # Fix image paths for docs context - content = content.replace('src="./docs/source/_static/', 'src="_static/') - content = content.replace('src="docs/source/_static/', 'src="_static/') - - # Fix documentation links - content = content.replace("](docs/", "](") - - # Convert GitHub-style alerts to MyST admonitions - # Pattern: > [!TYPE]\n> content - def convert_gh_alert(match): - alert_type = match.group(1).lower() - alert_content = match.group(2).strip() - # Remove leading > from subsequent lines - alert_content = re.sub(r"^> ", "", alert_content, flags=re.MULTILINE) - return f":::{{{alert_type}}}\n{alert_content}\n:::\n" - - # Match GitHub alert blocks: > [!TYPE] followed by lines starting with > - content = re.sub( - r"> \[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\n((?:> .+\n?)+)", - convert_gh_alert, - content, - flags=re.IGNORECASE, - ) - - # Append package structure and toctrees as eval-rst block - rst_appendix = """ - -```{eval-rst} ----- - -Package Structure ------------------ - -.. code-block:: text - - MyoGen/ - ├── myogen/ # Main package source code - │ ├── simulator/ # Core simulation functionality - │ │ ├── core/ # Core simulation components - │ │ │ ├── emg/ # EMG signal generation - │ │ │ ├── muscle/ # Muscle modeling - │ │ │ └── spike_train/ # Motor neuron simulation - │ │ └── ... - │ ├── utils/ # Utility functions and tools - │ │ ├── plotting/ # Visualization utilities - │ │ ├── currents.py # Current generation - │ │ └── nmodl.py # NMODL file handling - │ └── ... - ├── examples/ # Example scripts and tutorials - ├── docs/ # Documentation source - ├── pyproject.toml # Project metadata and dependencies - └── uv.lock # Pinned versions of dependencies - - - -.. toctree:: - :maxdepth: 2 - :hidden: - :caption: API Documentation - - api/index - -.. toctree:: - :maxdepth: 2 - :caption: User Guide - :hidden: - - neo_blocks_guide - -.. toctree:: - :maxdepth: 2 - :caption: Examples & Tutorials - :hidden: - - examples -``` -""" - content += rst_appendix - - # Save as index.md (which MyST will parse as the root document) - index_md = Path(__file__).parent / "index.md" - index_md.write_text(content, encoding="utf-8") - print("✓ README.md integrated into index.md") - else: - print(f"⚠️ WARNING: README.md not found at {readme_src}") - - -# Run during module load -copy_and_prepare_readme() - -# Sphinx Configuration -extensions = [ - "sphinx.ext.autodoc", - "sphinx.ext.napoleon", - "sphinx.ext.viewcode", - "sphinx.ext.autosummary", - "sphinx.ext.intersphinx", - "sphinx_gallery.gen_gallery", - "sphinx.ext.doctest", - "myst_parser", - "sphinxcontrib.mermaid", - "sphinx_design", - "hoverxref.extension", - "sphinx_autodoc_typehints", # Automatic type hint formatting and linking -] - -mermaid_version = "11.9.0" - -napoleon_use_admonition_for_examples = False -napoleon_use_admonition_for_references = False -napoleon_numpy_docstring = True -napoleon_use_param = True -napoleon_use_rtype = True -napoleon_preprocess_types = False # Let sphinx-autodoc-typehints handle types -napoleon_include_init_with_doc = False -napoleon_type_aliases = { - # Standard typing aliases for cleaner docstrings - "optional": "Optional[Any]", - "array_like": ":term:`array_like `", - "dict_like": ":term:`dict-like `", - # NumPy and scientific computing types - "ndarray": ":class:`numpy.ndarray`", - "np.ndarray": ":class:`numpy.ndarray`", - "float_array": ":class:`numpy.ndarray`\\[float]", - "bool_array": ":class:`numpy.ndarray`\\[bool]", - "int_array": ":class:`numpy.ndarray`\\[int]", - "list[np.ndarray]": ":class:`list`\\[:class:`numpy.ndarray`]", - "list[ndarray]": ":class:`list`\\[:class:`numpy.ndarray`]", - "dtype[bool]": ":class:`numpy.dtype`\\[bool]", - "tuple[int, ...]": ":class:`tuple`\\[int, ...]", - "ndarray[tuple[int, ...], dtype[bool]]": ":class:`numpy.ndarray`\\[bool]", - "float | list[float]": ":class:`float` | :class:`list`\\[:class:`float`]", - "int | list[int]": ":class:`int` | :class:`list`\\[:class:`int`]", - "str | list[str]": ":class:`str` | :class:`list`\\[:class:`str`]", - "list[int] | None": ":class:`list`\\[:class:`int`] | :class:`None`", - "list[float] | None": ":class:`list`\\[:class:`float`] | :class:`None`", - "list[str] | None": ":class:`list`\\[:class:`str`] | :class:`None`", - "tuple[int, int]": ":class:`tuple`\\[:class:`int`, :class:`int`]", - "tuple[float, float]": ":class:`tuple`\\[:class:`float`, :class:`float`]", - "list[int]": ":class:`list`\\[:class:`int`]", - "list[float]": ":class:`list`\\[:class:`float`]", - "list[str]": ":class:`list`\\[:class:`str`]", - # MyoGen Quantity types - link to type documentation - "Quantity__s": ":data:`~myogen.utils.types.Quantity__s`", - "Quantity__ms": ":data:`~myogen.utils.types.Quantity__ms`", - "Quantity__rad": ":data:`~myogen.utils.types.Quantity__rad`", - "Quantity__deg": ":data:`~myogen.utils.types.Quantity__deg`", - "Quantity__mV": ":data:`~myogen.utils.types.Quantity__mV`", - "Quantity__uV": ":data:`~myogen.utils.types.Quantity__uV`", - "Quantity__nA": ":data:`~myogen.utils.types.Quantity__nA`", - "Quantity__uS": ":data:`~myogen.utils.types.Quantity__uS`", - "Quantity__S_per_m": ":data:`~myogen.utils.types.Quantity__S_per_m`", - "Quantity__Hz": ":data:`~myogen.utils.types.Quantity__Hz`", - "Quantity__pps": ":data:`~myogen.utils.types.Quantity__pps`", - "Quantity__mm": ":data:`~myogen.utils.types.Quantity__mm`", - "Quantity__m": ":data:`~myogen.utils.types.Quantity__m`", - "Quantity__mm2": ":data:`~myogen.utils.types.Quantity__mm2`", - "Quantity__per_mm2": ":data:`~myogen.utils.types.Quantity__per_mm2`", - "Quantity__m_per_s": ":data:`~myogen.utils.types.Quantity__m_per_s`", - "Quantity__mm_per_s": ":data:`~myogen.utils.types.Quantity__mm_per_s`", - # Quantity tuple types - "tuple[Quantity__m_per_s, Quantity__m_per_s]": ":class:`tuple`\\[:data:`~myogen.utils.types.Quantity__m_per_s`, :data:`~myogen.utils.types.Quantity__m_per_s`]", - # AnalogSignal types - "CURRENT__AnalogSignal": ":data:`~myogen.utils.types.CURRENT__AnalogSignal`", - "INPUT_CURRENT__AnalogSignal": ":data:`~myogen.utils.types.CURRENT__AnalogSignal`", # Alias to CURRENT__AnalogSignal - "FORCE__AnalogSignal": ":data:`~myogen.utils.types.FORCE__AnalogSignal`", - # MyoGen custom types - link to documentation - "RECRUITMENT_THRESHOLDS__ARRAY": ":data:`~myogen.utils.types.RECRUITMENT_THRESHOLDS__ARRAY`", - "INPUT_CURRENT__MATRIX": ":data:`~myogen.utils.types.INPUT_CURRENT__MATRIX`", - "SPIKE_TRAIN__MATRIX": ":data:`~myogen.utils.types.SPIKE_TRAIN__MATRIX`", - "MUAP_SHAPE__TENSOR": ":data:`~myogen.utils.types.MUAP_SHAPE__TENSOR`", - "SURFACE_EMG__TENSOR": ":data:`~myogen.utils.types.SURFACE_EMG__TENSOR`", - "INTRAMUSCULAR_EMG__TENSOR": ":data:`~myogen.utils.types.INTRAMUSCULAR_EMG__TENSOR`", - "CORTICAL_INPUT__MATRIX": ":data:`~myogen.utils.types.CORTICAL_INPUT__MATRIX`", - "SURFACE_MUAP_SHAPE__TENSOR": ":data:`~myogen.utils.types.SURFACE_MUAP_SHAPE__TENSOR`", - "INTRAMUSCULAR_MUAP_SHAPE__TENSOR": ":data:`~myogen.utils.types.INTRAMUSCULAR_MUAP_SHAPE__TENSOR`", - "INPUT_CURRENT__MATRIX | None": ":class:`~myogen.utils.types.INPUT_CURRENT__MATRIX` | :class:`None`", - # Beartype and Annotated type patterns - map to clean aliases - "Annotated[ndarray[tuple[int, ...], dtype[bool]], beartype.vale.Is[lambda x: x.ndim == 3]]": ":data:`~myogen.utils.types.SPIKE_TRAIN__MATRIX`", - "Annotated[npt.NDArray[np.bool_], Is[lambda x: x.ndim == 3]]": ":data:`~myogen.utils.types.SPIKE_TRAIN__MATRIX`", - "Annotated[npt.NDArray[np.floating], Is[lambda x: x.ndim == 2]]": ":data:`~myogen.utils.types.INPUT_CURRENT__MATRIX`", - "Annotated[npt.NDArray[np.floating], Is[lambda x: x.ndim == 5]]": ":data:`~myogen.utils.types.SURFACE_EMG__TENSOR`", - # Matplotlib types - "Axes": ":class:`matplotlib.axes.Axes`", - "Figure": ":class:`matplotlib.figure.Figure`", - "Axes3D": ":class:`mpl_toolkits.mplot3d.axes3d.Axes3D`", - "IterableType[Axes]": ":class:`beartype.cave.IterableType`\\[:class:`matplotlib.axes.Axes`]", - # Beartype types - "IterableType": ":class:`beartype.cave.IterableType`", - # NeuroML types - "Segment": ":class:`neuroml.Segment`", - "list[Segment]": ":class:`list`\\[:class:`neuroml.Segment`]", - "list[neo.core.segment.Segment]": ":class:`list`\\[:class:`neo.core.segment.Segment`]", - # Common union types - "str_or_path": "str | :class:`pathlib.Path`", - "float_or_list": "float | list[float]", - "int_or_list": "int | list[int]", - "str_or_list": "str | list[str]", - # Motor unit recruitment model literals - "fuglevand": "``'fuglevand'``", - "deluca": "``'deluca'``", - "konstantin": "``'konstantin'``", - "combined": "``'combined'``", - "RecruitmentMode": "``'fuglevand'`` | ``'deluca'`` | ``'konstantin'`` | ``'combined'``", - "WhatToRecord": ":class:`list`\\[:class:`dict`\\[``'variables'``, ``'to_file'``, ``'sampling_interval'``, ``'locations'``\\], :class:`Any`\\]", - "ElectrodeGridDimensions": ":class:`tuple`\\[:class:`int`, :class:`int`]", - "ElectrodeGridCenterPosition": ":class:`tuple`\\[:class:`float` | :class:`int`, :class:`float` | :class:`int`]", - "list[ElectrodeGridCenterPosition]": ":class:`list`\\[:class:`tuple`\\[:class:`float` | :class:`int`, :class:`float` | :class:`int`\\]]", - # Simulation and modeling types - "Muscle": ":class:`~myogen.simulator.Muscle`", - "MotorNeuronPool": ":class:`~myogen.simulator.MotorNeuronPool`", - "SurfaceEMG": ":class:`~myogen.simulator.SurfaceEMG`", -} - - -# Custom type annotation formatters -def simplify_quantity_annotations(annotation): - """Simplify Annotated[Quantity, ...] types to clean Quantity__* aliases.""" - import re - - annotation_str = str(annotation) - - # Map of unit patterns to type alias names - quantity_patterns = { - r"IsEqual\['s'\]": "Quantity__s", - r"IsEqual\['ms'\]": "Quantity__ms", - r"IsEqual\['rad'\]": "Quantity__rad", - r"IsEqual\['deg'\]": "Quantity__deg", - r"IsEqual\['mV'\]": "Quantity__mV", - r"IsEqual\['uV'\]": "Quantity__uV", - r"IsEqual\['nA'\]": "Quantity__nA", - r"IsEqual\['uS'\]": "Quantity__uS", - r"IsEqual\['S/m'\]": "Quantity__S_per_m", - r"IsEqual\['Hz'\]": "Quantity__Hz", - r"IsEqual\['pps'\]": "Quantity__pps", - r"IsEqual\['mm'\]": "Quantity__mm", - r"IsEqual\['m'\]": "Quantity__m", - r"IsEqual\['mm\*\*2'\]": "Quantity__mm2", - r"IsEqual\['1/mm\*\*2'\]": "Quantity__per_mm2", - r"IsEqual\['m/s'\]": "Quantity__m_per_s", - r"IsEqual\['mm/s'\]": "Quantity__mm_per_s", - } - - # Try to match typing.Annotated[quantities.quantity.Quantity, beartype.vale.IsAttr[...]] - # Pattern matches with or without module prefixes - for unit_pattern, type_alias in quantity_patterns.items(): - pattern = rf"Annotated\[.*?Quantity,\s*.*?IsAttr\['dimensionality',\s*.*?IsAttr\['unicode',\s*.*?{unit_pattern}\]\]\]" - if re.search(pattern, annotation_str): - return type_alias - - return None - - -def format_annotation(annotation, config=None): - """Custom formatter to simplify Quantity annotations. - - Parameters - ---------- - annotation : Any - The type annotation to format - config : sphinx.config.Config, optional - Sphinx configuration object (may not be provided in all contexts) - - Returns - ------- - str or None - Formatted reStructuredText for the annotation, or None to use default formatting - """ - import typing - - # Try to simplify Quantity annotations - simplified = simplify_quantity_annotations(annotation) - if simplified: - # Try importing the type alias and returning it directly - try: - from myogen.utils import types - - return getattr(types, simplified) - except Exception: - # If that fails, return RST reference - return f":data:`~myogen.utils.types.{simplified}`" - - # Return None to let sphinx-autodoc-typehints handle it normally - return None - - -# MyST-Parser configuration -myst_enable_extensions = [ - "attrs_inline", - "attrs_block", - "colon_fence", - "deflist", - "dollarmath", - "fieldlist", - "html_admonition", - "html_image", - "linkify", - "replacements", - "smartquotes", - "substitution", - "tasklist", -] - -myst_heading_anchors = 3 -myst_admonition_enable = True -myst_url_schemes = ["http", "https", "mailto", "ftp"] -myst_ref_domains = ["std"] - -# Autodoc configuration -autodoc_default_options = { - "members": True, - "inherited-members": False, - "show-inheritance": True, - "undoc-members": True, - "exclude-members": "__weakref__", -} -autodoc_inherit_docstrings = True -autoclass_content = "both" -autodoc_typehints = "description" -autodoc_member_order = "groupwise" -autodoc_preserve_defaults = True -autodoc_typehints_format = "short" -autodoc_type_aliases = napoleon_type_aliases - -# sphinx-autodoc-typehints configuration -typehints_use_rtype = True # Show return types in :rtype: field -typehints_document_rtype = True # Document return types -typehints_defaults = "comma" # Show default values with commas -typehints_use_signature = True # Put types in signature for better rendering -typehints_use_signature_return = True # Put return type in signature -typehints_fully_qualified = False # Use short names (not myogen.utils.types.Foo) -always_use_bars_union = True # Use | instead of Union in docs (Python 3.10+ style) - -# Better signature formatting -maximum_signature_line_length = 80 -python_use_unqualified_type_names = True - -# Advanced autodoc signature formatting -add_function_parentheses = True -add_module_names = False -show_authors = True - -# Improved signature display -autodoc_signature_formatting = "multiline" -python_maximum_signature_line_length = 88 - -# Autosummary configuration -autosummary_generate = True -autosummary_generate_overwrite = True -autosummary_imported_members = False -autosummary_ignore_module_all = False - -# General configuration -templates_path = ["templates"] -exclude_patterns = ["Thumbs.db", ".DS_Store"] - -# Syntax highlighting with Pygments -pygments_style = "monokai" # Default/fallback style -pygments_dark_style = "monokai" # Dark mode - -# HTML theme configuration -html_theme = "pydata_sphinx_theme" -html_theme_options = { - "github_url": f"https://github.com/NSquaredLab/{project}", - "navbar_start": ["navbar-logo", "navbar-version.html", "header-text.html"], - "show_prev_next": False, - "navbar_align": "left", - "navbar_persistent": ["search-button"], - "footer_start": ["copyright"], - "footer_end": ["sphinx-version"], - # Enhanced navigation - "use_edit_page_button": False, - "navigation_with_keys": True, - "show_toc_level": 2, - "navigation_depth": 4, - # Search improvements - "search_bar_text": "Search MyoGen docs...", - # API documentation improvements - "show_nav_level": 2, - "collapse_navigation": False, - # Pygments (syntax highlighting) configuration - "pygments_light_style": "friendly", # Clean light theme that pairs well with Monokai - "pygments_dark_style": "monokai", - # Header and footer customization - "header_links_before_dropdown": 4, - # Announcement bar - "announcement": "MyoGen is under active development. API may change.", -} - -html_static_path = ["_static"] -html_logo = "_static/myogen_logo.png" -html_css_files = ["custom.css"] -html_title = f"{project} {version} Documentation" -html_show_sourcelink = False - -# HTML context -html_context = { - "AUTHOR": author, - "VERSION": version, - "DESCRIPTION": project_info.get("description", ""), - "github_user": "NSquaredLab", # Update with your GitHub username - "github_repo": project, - "github_version": "main", - "doc_path": "docs", -} - -# Intersphinx mapping -intersphinx_mapping = { - "python": ("https://docs.python.org/3", None), - "numpy": ("https://numpy.org/doc/stable/", None), - "scipy": ("https://docs.scipy.org/doc/scipy/reference/", None), - "matplotlib": ("https://matplotlib.org/stable/", None), - "pandas": ("https://pandas.pydata.org/docs/", None), - "sklearn": ("https://scikit-learn.org/stable/", None), - "neo": ("https://neo.readthedocs.io/en/latest/", None), - "beartype": ("https://beartype.readthedocs.io/en/latest/", None), - "quantities": ("https://python-quantities.readthedocs.io/en/latest/", None), -} - -# Sphinx-hoverxref configuration for hover previews -hoverxref_auto_ref = True # Automatically add tooltips to all references -hoverxref_domains = ["py"] # Enable for Python domain -hoverxref_roles = [ - "class", - "func", - "meth", - "attr", - "data", - "mod", - "obj", -] # Roles to enable hover preview for -hoverxref_role_types = { - "class": "tooltip", # Use tooltip style for classes - "func": "tooltip", # Use tooltip style for functions - "meth": "tooltip", # Use tooltip style for methods - "attr": "tooltip", # Use tooltip style for attributes - "data": "tooltip", # Use tooltip style for data - "mod": "modal", # Use modal style for modules (larger preview) - "ref": "tooltip", # Use tooltip for references - "obj": "tooltip", # Use tooltip style for generic objects -} -hoverxref_tooltip_maxwidth = 600 # Maximum width of tooltip in pixels -hoverxref_tooltip_animation_duration = 200 # Animation duration in milliseconds -hoverxref_intersphinx = [ - "python", - "numpy", - "scipy", - "matplotlib", -] # Enable hover previews for intersphinx references - -# Embed tooltip content for local viewing (fixes "Loading..." issue with file:// URLs) -hoverxref_tooltip_lazy = False # Disable lazy loading to embed content directly in HTML - -# Sphinx Gallery configuration -sphinx_gallery_conf = { - "examples_dirs": [ - str(base_dir / "examples" / "01_basic"), - str(base_dir / "examples" / "02_finetune"), - str(base_dir / "examples" / "03_papers" / "watanabe"), - str(base_dir / "examples" / "04_clinical"), - ], - "gallery_dirs": [ - "auto_examples/01_basic", - "auto_examples/02_finetune", - "auto_examples/03_papers/watanabe", - "auto_examples/04_clinical", - ], - "subsection_order": ExplicitOrder( - [ - str(base_dir / "examples" / "01_basic"), - str(base_dir / "examples" / "02_finetune"), - str(base_dir / "examples" / "03_papers" / "watanabe"), - str(base_dir / "examples" / "04_clinical"), - ] - ), - "filename_pattern": r"\.py", - # 14_calibrate_noise_from_real.py needs a private iEMG .mat recording that - # isn't available in CI, so it can't be executed by the gallery. - # _pic_protocols.py is a shared helper module, not a standalone example, so - # it must not be parsed/executed as a gallery item (it has no example title). - "ignore_pattern": r"(14_calibrate_noise_from_real|_pic_protocols)\.py", - "remove_config_comments": True, - "within_subsection_order": FileNameSortKey, - "show_memory": False, - "plot_gallery": True, - "download_all_examples": False, - "first_notebook_cell": "%matplotlib inline", - "reset_modules": (reset_neuron,), # Reset NEURON state between examples -} - -# Warning suppressions -suppress_warnings = [ - "config.cache", - "ref.citation", -] - - -def prettify_type_alias(_app, what, _name, _obj, _options, lines): - """Pretty-print type alias beartype annotations.""" - import re - - # Only process data (type aliases) - if what != "data": - return - - # Check for beartype Annotated types and format them better - for i, line in enumerate(lines): - if "alias of" in line and "Annotated[" in line: - # Extract the annotation content - match = re.search(r"alias of (.+)", line) - if match: - annotation = match.group(1) - # Format as a code block for better readability - lines[i] = "**Type Alias:**\n\n.. code-block:: python\n\n " + annotation.replace( - ", ", ",\n " - ) - lines.insert(i + 1, "") - - -def post_process_html(_app, exception): - """Post-process HTML files to replace Annotated[Quantity, ...] with Quantity__* links.""" - if exception: - return - - import re - from pathlib import Path - - build_dir = Path(_app.outdir) - - # HTML replacements for Quantity types - # Match specific Annotated[Quantity, IsAttr[...IsEqual['X']...]] patterns - # Base pattern for all Quantity types - base_pattern = r']*>Annotated\[Quantity, ]*>IsAttr\[\'dimensionality\', ]*>IsAttr\[\'unicode\', ]*>IsEqual\[\'UNIT\'\]\]\]\]' - - # Map of unit strings to type alias names - quantity_units = { - "s": "Quantity__s", - "ms": "Quantity__ms", - "rad": "Quantity__rad", - "deg": "Quantity__deg", - "mV": "Quantity__mV", - "uV": "Quantity__uV", - "nA": "Quantity__nA", - "uS": "Quantity__uS", - "S/m": "Quantity__S_per_m", - "Hz": "Quantity__Hz", - "pps": "Quantity__pps", - "mm": "Quantity__mm", - "m": "Quantity__m", - "mm\\*\\*2": "Quantity__mm2", - "1/mm\\*\\*2": "Quantity__per_mm2", - "m/s": "Quantity__m_per_s", - "mm/s": "Quantity__mm_per_s", - } - - # Build replacement patterns for all units - replacements = [] - for unit, alias_name in quantity_units.items(): - pattern = base_pattern.replace("UNIT", unit) - # Use full module path in filename: myogen.utils.types.Quantity__X.html - replacement = f'{alias_name}' - replacements.append((pattern, replacement)) - - # Add replacements for Block types (simpler pattern - just plain text in

tags) - block_types = [ - "SPIKE_TRAIN__Block", - "SURFACE_EMG__Block", - "SURFACE_MUAP__Block", - "INTRAMUSCULAR_EMG__Block", - "INTRAMUSCULAR_MUAP__Block", - ] - - for block_type in block_types: - # Pattern:

BLOCK_TYPE

or BLOCK_TYPE in parameter lists - pattern = f"

{block_type}

" - replacement = f'

{block_type}

' - replacements.append((pattern, replacement)) - - # Also handle Block types in parameter descriptions - pattern_em = f"{block_type}" - replacement_em = f'{block_type}' - replacements.append((pattern_em, replacement_em)) - - # Fix truncated Block type signatures (sphinx-autodoc-typehints rendering issue) - # Pattern: parameter name followed by truncated type hint ]] - truncated_sig_patterns = [ - # spike_train__Block parameter with truncated ]] type - ( - r'(spike_train__Block: )]]', - r'\1SPIKE_TRAIN__Block', - ), - ] - - replacements.extend(truncated_sig_patterns) - - # Process all HTML files with context-aware replacements - total_replacements = 0 - for html_file in build_dir.rglob("*.html"): - try: - content = html_file.read_text(encoding="utf-8") - original_content = content - - # Apply all replacement patterns - for pattern, replacement in replacements: - content = re.sub(pattern, replacement, content) - - # Context-aware return type replacements for truncated Block types - # SurfaceEMG methods return SURFACE_EMG__Block or SURFACE_MUAP__Block - if "SurfaceEMG" in html_file.name: - if "simulate_surface_emg" in content or "add_noise" in content: - # These return SURFACE_EMG__Block - content = re.sub( - r'( )segments\)\)]]', - r'\1SURFACE_EMG__Block', - content, - ) - elif "simulate_muaps" in content: - # This returns SURFACE_MUAP__Block - content = re.sub( - r'( )segments\)\)]]', - r'\1SURFACE_MUAP__Block', - content, - ) - - # IntramuscularEMG methods return INTRAMUSCULAR_EMG__Block or INTRAMUSCULAR_MUAP__Block - if "IntramuscularEMG" in html_file.name: - if "simulate_intramuscular_emg" in content or "add_noise" in content: - # These return INTRAMUSCULAR_EMG__Block - content = re.sub( - r'( )segments\)\)]]', - r'\1INTRAMUSCULAR_EMG__Block', - content, - ) - elif "simulate_muaps" in content: - # This returns INTRAMUSCULAR_MUAP__Block - content = re.sub( - r'( )segments\)\)]]', - r'\1INTRAMUSCULAR_MUAP__Block', - content, - ) - - # ForceModel.generate_force returns with truncated "N]]" (likely FORCE__AnalogSignal but truncated) - if "ForceModel" in html_file.name and "generate_force" in content: - content = re.sub( - r'( )N]]', - r'\1FORCE__AnalogSignal', - content, - ) - - # inject_currents_and_simulate_spike_trains returns SPIKE_TRAIN__Block - if "inject_currents_and_simulate_spike_trains" in html_file.name: - content = re.sub( - r'( )segments\)\)]]', - r'\1SPIKE_TRAIN__Block', - content, - ) - - # Check if anything was modified - if content != original_content: - html_file.write_text(content, encoding="utf-8") - total_replacements += 1 - print(f"Processed {html_file.name}") - except Exception as e: - print(f"Warning: Could not process {html_file}: {e}") - - if total_replacements > 0: - print(f"Total: Modified {total_replacements} HTML files") - - -def setup(app): - """Setup function for custom configurations.""" - app.add_css_file("custom.css") - app.add_js_file("custom.js") - - # Add custom autodoc processors - app.connect("autodoc-process-docstring", prettify_type_alias) - app.connect("build-finished", post_process_html) diff --git a/docs/source/examples.rst b/docs/source/examples.rst deleted file mode 100644 index 897bcef2..00000000 --- a/docs/source/examples.rst +++ /dev/null @@ -1,73 +0,0 @@ -.. _examples-index: - -================== -Examples -================== - -| MyoGen has a variety of examples demonstrating key functionalities, workflows, and research applications. -| Each section contains detailed python examples that guide you through the implementation and usage of MyoGen components. -| -.. grid:: 2 - :gutter: 3 - - .. grid-item-card:: - :link: basic-examples - :link-type: ref - :text-align: center - - Basics - ^^^ - - Self-contained examples covering core MyoGen components and workflows. - - +++ - For first-time users - - .. grid-item-card:: - :link: finetune-examples - :link-type: ref - :text-align: center - - Finetuning - ^^^ - - Workflow for matching MU and cortical population activity to match MVC % targets. - - +++ - Requires Optuna - - .. grid-item-card:: - :link: clinical-examples - :link-type: ref - :text-align: center - - Clinical & Pathology - ^^^ - - Pathological motor-control signals such as SCI-like iEMG. - - +++ - Self-contained - -Literature Reproductions -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. grid:: 1 - :gutter: 3 - - .. grid-item-card:: - :link: watanabe-reproduction - :link-type: ref - :text-align: center - - Watanabe and Kohn (2015, J. Neurosci.) - - -.. toctree:: - :hidden: - :maxdepth: 2 - - auto_examples/01_basic/index - auto_examples/02_finetune/index - auto_examples/03_papers/watanabe/index - auto_examples/04_clinical/index diff --git a/docs/source/index.md b/docs/source/index.md deleted file mode 100644 index f4e4b284..00000000 --- a/docs/source/index.md +++ /dev/null @@ -1,293 +0,0 @@ -
-

- Welcome to - MyoGen Logo -

- -

The modular and extensible simulation toolkit for neurophysiology

- - [![Documentation](https://img.shields.io/badge/docs-latest-blue.svg)](https://nsquaredlab.github.io/MyoGen/) - [![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/) - [![Version](https://img.shields.io/badge/version-0.10.1-orange.svg)](https://github.com/NsquaredLab/MyoGen) - - [Installation](https://nsquaredlab.github.io/MyoGen/#installation) • - [Documentation](https://nsquaredlab.github.io/MyoGen/) • - [Examples](https://nsquaredlab.github.io/MyoGen/examples.html) • - [How to Cite](https://nsquaredlab.github.io/MyoGen/#how-to-cite) -
- -# Overview - -MyoGen is a **modular and extensible neuromuscular simulation framework** for generating physiologically grounded motor-unit activity, muscle force, and surface EMG signals. - -It supports end-to-end modeling of the neuromuscular pathway, from descending neural drive and spinal motor neuron dynamics to muscle activation and bioelectric signal formation at the electrode level. -MyoGen is designed for algorithm validation, hypothesis-driven research, and education, providing configurable building blocks that can be independently combined and extended. - -# Highlights - -🧬 **Biophysically inspired neuron models** — NEURON-based motor neurons with validated calcium dynamics and membrane properties - -🎯 **Everything is inspectable** — Complete access to every motor unit, spike time, fiber location etc. for rigorous algorithm testing - -⚡️ **Vectorized & parallel** — Multi-core CPU processing with NumPy/Numba vectorization for fast computation - -🔬 **End-to-end simulation** — From motor unit recruitment to high-density surface EMG in a single framework - -📊 **Reproducible science** — Deterministic random seeds and standardized Neo Block outputs for exact replication - -📦 **NWB export** — Optional export to [Neurodata Without Borders](https://www.nwb.org/) format for data sharing via DANDI - -🧰 **Comprehensive toolkit** — Surface EMG, intramuscular EMG, force generation, and spinal network modeling - -# Installation - -> **Requires Python 3.12+** — Check your version with `python --version` - -## System Requirements - -| Platform | Before Installing MyoGen | -|----------|--------------------------| -| **Windows** | [NEURON 8.2.7](https://github.com/neuronsimulator/nrn/releases/download/8.2.7/nrn-8.2.7.w64-mingw-py-39-310-311-312-313-setup.exe) - Download, run installer, select "Add to PATH" | -| **Linux** | `sudo apt install libopenmpi-dev` (Ubuntu/Debian) or `sudo dnf install openmpi-devel` (Fedora) | -| **macOS** | `brew install open-mpi` | - -> [!CAUTION] -> -> ## Windows Users: Prerequisites -> -> **You MUST install the following before installing MyoGen on Windows:** -> -> ### 1. Visual C++ Build Tools -> -> Download and install [Visual C++ Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/). -> ->During installation, select these components: -> -> - MSVC Build Tools for x64/x86 (Latest) -> - MSVC v143 – VS 2022 C++ x64/x86 build tools -> - Windows 11 SDK (latest) -> - C++ core desktop features -> -> ### 2. NEURON Simulator -> -> 1. **Download**: [NEURON 8.2.7 Installer](https://github.com/neuronsimulator/nrn/releases/download/8.2.7/nrn-8.2.7.w64-mingw-py-39-310-311-312-313-setup.exe) -> 2. **Run the installer** and select **"Add to PATH"** when prompted -> 3. **Restart your terminal** (close and reopen) -> 4. Then continue with the installation below - ---- - -## Step 1: Install uv (Package Manager) - -We use [uv](https://docs.astral.sh/uv/) - a fast Python package manager. Install it first: - -**Windows** (open PowerShell): - -```powershell -powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" -``` - -**Linux/macOS**: - -```bash -curl -LsSf https://astral.sh/uv/install.sh | sh -``` - -After installing, **restart your terminal** (close and reopen it). - ---- - -## Step 2: Create a New Project - -Open a terminal and navigate to where you want your project: - -```bash -# Create a new folder for your project -mkdir my_emg_project -cd my_emg_project - -# Initialize a Python project -uv init - -# Add MyoGen to your project -uv add myogen -``` - -That's it! MyoGen is now installed and ready to use. - ---- - -## Step 3: Verify Installation - -Create a test file to make sure everything works: - -```bash -# Create a test script -uv run python -c "from myogen import simulator; print('MyoGen installed successfully!')" -``` - -If you see `MyoGen installed successfully!` - you're all set! - ---- - -## Alternative: pip install - -If you prefer pip over uv: - -```bash -pip install myogen -``` - ---- - -## For Developers (From Source) - -```bash -git clone https://github.com/NsquaredLab/MyoGen.git -cd MyoGen -uv sync -uv run poe setup_myogen -``` - -## Optional: GPU Acceleration - -For 5-10× faster convolutions (requires NVIDIA GPU): - -```bash -uv add cupy-cuda12x -``` - -# Quick Start - -Generate motor unit action potentials (MUAPs): - -```python -from myogen import simulator -import quantities as pq - -# 1. Generate recruitment thresholds (100 motor units) -thresholds, _ = simulator.RecruitmentThresholds( - N=100, - recruitment_range__ratio=50, - mode="fuglevand" -) - -# 2. Create muscle model with fiber distribution -muscle = simulator.Muscle( - recruitment_thresholds=thresholds, - radius_bone__mm=1.0 * pq.mm, - fiber_density__fibers_per_mm2=400 * pq.mm**-2, - fat_thickness__mm=10 * pq.mm, - autorun=True -) - -# 3. Set up surface electrode array -electrode_array = simulator.SurfaceElectrodeArray( - num_rows=5, - num_cols=5, - inter_electrode_distance__mm=5 * pq.mm, - electrode_radius__mm=5 * pq.mm, - bending_radius__mm=muscle.radius__mm + muscle.skin_thickness__mm + muscle.fat_thickness__mm, -) - -# 4. Create surface EMG simulator -surface_emg = simulator.SurfaceEMG( - muscle_model=muscle, - electrode_arrays=[electrode_array], - sampling_frequency__Hz=2048.0, - MUs_to_simulate=[0, 1, 2, 3, 4] # First 5 motor units -) - -# 5. Simulate MUAPs (parallel processing) -muaps = surface_emg.simulate_muaps(n_jobs=-2) -``` - -**Access MUAP data**: - -```python -import numpy as np - -# Get MUAP from motor unit 0 -muap_signal = muaps.groups[0].segments[0].analogsignals[0] -print(f"MUAP shape: {muap_signal.shape}") # (time, rows, cols) - -# Extract from specific electrode (row 2, col 2) -electrode_muap = muap_signal[:, 2, 2] -peak_amplitude = np.max(np.abs(electrode_muap.magnitude)) -print(f"Peak amplitude: {peak_amplitude:.3f} {electrode_muap.units}") -``` - -**For full EMG simulation** with spike trains, see [examples](https://nsquaredlab.github.io/MyoGen/examples.html) - -# Documentation - -📖 **[Read the full documentation](https://nsquaredlab.github.io/MyoGen/)** - -- [User Guide](https://nsquaredlab.github.io/MyoGen/neo_blocks_guide.html) — Working with simulation outputs -- [API Reference](https://nsquaredlab.github.io/MyoGen/api/) — Complete class documentation -- [Examples](examples/) — Step-by-step tutorials from recruitment to EMG - -# How to Cite - -If you use MyoGen in your research, please cite: - -TBD - -# Contributing - -Contributions welcome! See [issues](https://github.com/NsquaredLab/MyoGen/issues) if you want to add a feature or fix a bug. - -# License - -MyoGen is AGPL licensed. See [LICENSE](https://github.com/NsquaredLab/MyoGen/LICENSE.md) for details. - - -```{eval-rst} ----- - -Package Structure ------------------ - -.. code-block:: text - - MyoGen/ - ├── myogen/ # Main package source code - │ ├── simulator/ # Core simulation functionality - │ │ ├── core/ # Core simulation components - │ │ │ ├── emg/ # EMG signal generation - │ │ │ ├── muscle/ # Muscle modeling - │ │ │ └── spike_train/ # Motor neuron simulation - │ │ └── ... - │ ├── utils/ # Utility functions and tools - │ │ ├── plotting/ # Visualization utilities - │ │ ├── currents.py # Current generation - │ │ └── nmodl.py # NMODL file handling - │ └── ... - ├── examples/ # Example scripts and tutorials - ├── docs/ # Documentation source - ├── pyproject.toml # Project metadata and dependencies - └── uv.lock # Pinned versions of dependencies - - - -.. toctree:: - :maxdepth: 2 - :hidden: - :caption: API Documentation - - api/index - -.. toctree:: - :maxdepth: 2 - :caption: User Guide - :hidden: - - neo_blocks_guide - -.. toctree:: - :maxdepth: 2 - :caption: Examples & Tutorials - :hidden: - - examples -``` diff --git a/docs/source/index.rst.backup b/docs/source/index.rst.backup deleted file mode 100644 index fa1185e7..00000000 --- a/docs/source/index.rst.backup +++ /dev/null @@ -1,53 +0,0 @@ -.. MyoGen documentation master file - -.. toctree:: - :maxdepth: 2 - :hidden: - - readme_content - -Package Structure ------------------ - -.. code-block:: text - - MyoGen/ - ├── myogen/ # Main package source code - │ ├── simulator/ # Core simulation functionality - │ │ ├── core/ # Core simulation components - │ │ │ ├── emg/ # EMG signal generation - │ │ │ ├── muscle/ # Muscle modeling - │ │ │ └── spike_train/ # Motor neuron simulation - │ │ └── ... - │ ├── utils/ # Utility functions and tools - │ │ ├── plotting/ # Visualization utilities - │ │ ├── currents.py # Current generation - │ │ └── nmodl.py # NMODL file handling - │ └── ... - ├── examples/ # Example scripts and tutorials - ├── docs/ # Documentation source - ├── pyproject.toml # Project metadata and dependencies - └── uv.lock # Pinned versions of dependencies - - - -.. toctree:: - :maxdepth: 2 - :hidden: - :caption: API Documentation - - api/index - -.. toctree:: - :maxdepth: 2 - :caption: User Guide - :hidden: - - neo_blocks_guide - -.. toctree:: - :maxdepth: 2 - :caption: Examples & Tutorials - :hidden: - - examples diff --git a/docs/source/neo_blocks_guide.rst b/docs/source/neo_blocks_guide.rst deleted file mode 100644 index 4b56220a..00000000 --- a/docs/source/neo_blocks_guide.rst +++ /dev/null @@ -1,386 +0,0 @@ -Working with Neo Block Data Structures -======================================= - -MyoGen uses **Neo Block** objects as the primary data containers for storing simulation results. This guide explains how to extract and work with data from the different Block types. - -.. contents:: Table of Contents - :local: - :depth: 2 - -Overview --------- - -MyoGen uses five different Neo Block types to store simulation data: - -.. list-table:: - :header-rows: 1 - :widths: 30 40 30 - - * - Block Type - - Purpose - - Dimensions - * - ``SPIKE_TRAIN__Block`` - - Neural firing times - - 1D spike times - * - ``SURFACE_MUAP__Block`` - - Surface MUAP templates - - 3D electrode grid - * - ``SURFACE_EMG__Block`` - - Surface EMG signals - - 3D electrode grid - * - ``INTRAMUSCULAR_MUAP__Block`` - - Intramuscular MUAP templates - - 2D electrode array - * - ``INTRAMUSCULAR_EMG__Block`` - - Intramuscular EMG signals - - 2D electrode array - -Neo Blocks provide standardized containers with: - -- Automatic unit tracking (mV, µV, seconds, etc.) -- Metadata storage (sampling rate, duration, etc.) -- Compatibility with analysis tools like Elephant -- Integration with neuroscience workflows - -Quick Start ------------ - -Basic Access Patterns -~~~~~~~~~~~~~~~~~~~~~~ - -.. code-block:: python - - import joblib - - # Load spike trains - spike_trains = joblib.load("spike_trains.pkl") - spiketrain = spike_trains.segments[0].spiketrains[0] - spike_times = spiketrain.magnitude # NumPy array - - # Load surface EMG - surface_emg = joblib.load("surface_emg.pkl") - emg_signal = surface_emg.groups[0].segments[0].analogsignals[0] - electrode_data = emg_signal[:, row, col] # Time series at electrode - - # Load intramuscular EMG - im_emg = joblib.load("intramuscular_emg.pkl") - emg_signal = im_emg.segments[0].analogsignals[0] - electrode_data = emg_signal[:, electrode_idx] # Time series - -Block Structure Details ------------------------- - -SPIKE_TRAIN__Block -~~~~~~~~~~~~~~~~~~~ - -**Structure**: Block → Segments (motor pools) → SpikeTrain objects - -The ``SPIKE_TRAIN__Block`` stores neural firing patterns from motor neuron pools. Each segment represents a motor pool, and each spiketrain contains the firing times of an individual neuron. - -.. code-block:: python - - # Access spike trains - motor_pool = spike_train__Block.segments[pool_idx] - spiketrain = motor_pool.spiketrains[neuron_idx] - - # Extract data - spike_times__s = spiketrain.magnitude # NumPy array - n_spikes = len(spiketrain) - duration = spiketrain.t_stop - sampling_rate = spiketrain.sampling_rate - -**Example: Calculate firing rate** - -.. code-block:: python - - motor_pool = spike_train__Block.segments[0] - firing_rates = [] - - for spiketrain in motor_pool.spiketrains: - if len(spiketrain) > 0: - rate = (len(spiketrain) / (spiketrain.t_stop - spiketrain.t_start)).rescale("Hz") - firing_rates.append(rate.magnitude) - - print(f"Mean firing rate: {np.mean(firing_rates):.2f} Hz") - -SURFACE_MUAP__Block -~~~~~~~~~~~~~~~~~~~~ - -**Structure**: Block → Groups (electrode arrays) → Segments (MUAP indices) → AnalogSignals (3D) - -Stores Motor Unit Action Potential (MUAP) templates for surface electrode arrays. Each group represents an electrode array, each segment represents a MUAP, and signals are 3D arrays (time × rows × columns). - -.. code-block:: python - - # Access MUAPs - electrode_array = muaps__Block.groups[array_idx] - muap_segment = electrode_array.segments[muap_idx] - muap_signal = muap_segment.analogsignals[0] - - # Extract data - muap_array = muap_signal.magnitude # Shape: (time, rows, cols) - time__s = muap_signal.times.magnitude - electrode_muap = muap_signal[:, row, col] # Specific electrode - -**Example: Analyze MUAP amplitude** - -.. code-block:: python - - # Get MUAP from motor unit 5, electrode at row 2, column 3 - muap_signal = muaps__Block.groups[0].segments[5].analogsignals[0] - - row, col = 2, 3 - electrode_muap = muap_signal[:, row, col] - - print(f"Peak-to-peak: {np.ptp(electrode_muap.magnitude):.2f} {electrode_muap.units}") - print(f"Max amplitude: {np.max(np.abs(electrode_muap.magnitude)):.2f} {electrode_muap.units}") - -SURFACE_EMG__Block -~~~~~~~~~~~~~~~~~~~ - -**Structure**: Block → Groups (electrode arrays) → Segments (motor pools) → AnalogSignals (3D) - -Stores synthesized surface EMG signals. Similar structure to SURFACE_MUAP__Block, but contains the full EMG signal (longer duration) rather than templates. - -.. code-block:: python - - # Access surface EMG - electrode_array = surface_emg__Block.groups[array_idx] - motor_pool = electrode_array.segments[pool_idx] - emg_signal = motor_pool.analogsignals[0] - - # Extract data - emg_array = emg_signal.magnitude # Shape: (time, rows, cols) - electrode_emg = emg_signal[:, row, col] # Specific electrode - -**Example: Calculate RMS amplitude** - -.. code-block:: python - - emg_signal = surface_emg__Block.groups[0].segments[0].analogsignals[0] - electrode_emg = emg_signal[:, 2, 2].magnitude - - rms = np.sqrt(np.mean(electrode_emg ** 2)) - print(f"RMS amplitude: {rms:.2f} {emg_signal.units}") - -INTRAMUSCULAR_MUAP__Block -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Structure**: Block → Segments (MUAP indices) → AnalogSignals (2D) - -Stores MUAP templates for intramuscular electrodes. Each segment represents a MUAP, and signals are 2D arrays (time × electrodes). - -.. code-block:: python - - # Access intramuscular MUAPs - muap_segment = im_muaps__Block.segments[muap_idx] - muap_signal = muap_segment.analogsignals[0] - - # Extract data - muap_array = muap_signal.magnitude # Shape: (time, electrodes) - electrode_muap = muap_signal[:, electrode_idx] - -INTRAMUSCULAR_EMG__Block -~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Structure**: Block → Segments (motor pools) → AnalogSignals (2D) - -Stores synthesized intramuscular EMG signals. Each segment represents a motor pool, and signals are 2D arrays (time × electrodes). - -.. code-block:: python - - # Access intramuscular EMG - motor_pool = im_emg__Block.segments[pool_idx] - emg_signal = motor_pool.analogsignals[0] - - # Extract data - emg_array = emg_signal.magnitude # Shape: (time, electrodes) - electrode_emg = emg_signal[:, electrode_idx] - -Common Operations ------------------ - -Accessing Metadata -~~~~~~~~~~~~~~~~~~~ - -All Neo signals have metadata properties: - -.. code-block:: python - - signal.magnitude # NumPy array of values - signal.times.magnitude # Time axis (in seconds) - signal.sampling_rate # Sampling frequency - signal.sampling_period # Time between samples - signal.t_start # Start time - signal.t_stop # Stop time - signal.duration # Total duration - signal.units # Physical units (mV, uV, etc.) - signal.shape # Array dimensions - -Extracting Time Windows -~~~~~~~~~~~~~~~~~~~~~~~~ - -.. code-block:: python - - # For spike trains - windowed_train = spiketrain.time_slice(t_start, t_stop) - - # For analog signals - start_idx = int(t_start * emg_signal.sampling_rate.magnitude) - end_idx = int(t_stop * emg_signal.sampling_rate.magnitude) - windowed_signal = emg_signal[start_idx:end_idx] - -Converting Units -~~~~~~~~~~~~~~~~ - -.. code-block:: python - - import quantities as pq - - # Check current units - print(signal.units) # e.g., mV - - # Convert to different units - signal_uV = signal.rescale(pq.uV) - -Saving and Loading -~~~~~~~~~~~~~~~~~~ - -.. code-block:: python - - import joblib - - # Save blocks - joblib.dump(spike_train__Block, "spike_trains.pkl") - joblib.dump(surface_emg__Block, "surface_emg.pkl") - - # Load blocks - spike_trains = joblib.load("spike_trains.pkl") - surface_emg = joblib.load("surface_emg.pkl") - -Shape Reference ---------------- - -.. list-table:: - :header-rows: 1 - :widths: 40 20 40 - - * - Block Type - - Dimensionality - - Shape Format - * - SPIKE_TRAIN - - 1D - - ``(n_spikes,)`` - * - SURFACE_MUAP - - 3D - - ``(time, rows, cols)`` - * - SURFACE_EMG - - 3D - - ``(time, rows, cols)`` - * - INTRAMUSCULAR_MUAP - - 2D - - ``(time, electrodes)`` - * - INTRAMUSCULAR_EMG - - 2D - - ``(time, electrodes)`` - -Data Flow Pipeline ------------------- - -The simulation pipeline follows this flow: - -1. **Generate Spike Trains** → ``SPIKE_TRAIN__Block`` - - - When neurons fire - -2. **Generate MUAPs** → ``SURFACE_MUAP__Block`` or ``INTRAMUSCULAR_MUAP__Block`` - - - What individual motor units look like - -3. **Convolve MUAPs with Spike Trains** → ``SURFACE_EMG__Block`` or ``INTRAMUSCULAR_EMG__Block`` - - - Final synthesized EMG signals - -Complete Example ----------------- - -.. code-block:: python - - import joblib - import numpy as np - import matplotlib.pyplot as plt - from myogen.utils.types import SPIKE_TRAIN__Block, SURFACE_EMG__Block - - # 1. Load data - spike_trains = joblib.load("spike_trains.pkl") - surface_emg = joblib.load("surface_emg.pkl") - - # 2. Extract spike times - motor_pool = spike_trains.segments[0] - neuron_5 = motor_pool.spiketrains[5] - spike_times = neuron_5.magnitude - - # 3. Extract EMG from center electrode - emg_signal = surface_emg.groups[0].segments[0].analogsignals[0] - center_row = emg_signal.shape[1] // 2 - center_col = emg_signal.shape[2] // 2 - electrode_emg = emg_signal[:, center_row, center_col] - - # 4. Plot - fig, (ax1, ax2) = plt.subplots(2, 1, sharex=True) - - # Spike raster - ax1.scatter(spike_times, [0]*len(spike_times), s=10, c='black') - ax1.set_ylabel('Neuron 5') - ax1.set_ylim(-0.5, 0.5) - - # EMG signal - time = emg_signal.times.magnitude - ax2.plot(time, electrode_emg.magnitude) - ax2.set_xlabel('Time (s)') - ax2.set_ylabel(f'EMG ({electrode_emg.units})') - - plt.tight_layout() - plt.show() - -Troubleshooting ---------------- - -Missing 'groups' attribute -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Issue**: ``AttributeError: 'Block' object has no attribute 'groups'`` - -**Solution**: This is likely a spike train or intramuscular block. Those use ``segments`` not ``groups``. - -IndexError when accessing electrodes -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Issue**: ``IndexError`` when accessing electrodes - -**Solution**: Check the shape first: - -.. code-block:: python - - print(signal.shape) # e.g., (150000, 13, 5) - # Then access within bounds: signal[:, 0:13, 0:5] - -Wrong units -~~~~~~~~~~~ - -**Issue**: Values seem wrong (very large or very small) - -**Solution**: Check the units: - -.. code-block:: python - - print(signal.units) # e.g., mV or µV - signal.rescale(pq.mV) # Convert to desired unit - -Additional Resources --------------------- - -- Example: ``examples/01_basic/12_extract_data_from_neo_blocks.py`` -- `Neo Documentation `_ -- `Elephant (Electrophysiology Analysis) `_ -- `Quantities (Unit Handling) `_ diff --git a/docs/source/templates/autosummary/class.rst b/docs/source/templates/autosummary/class.rst deleted file mode 100644 index 3fb3a40b..00000000 --- a/docs/source/templates/autosummary/class.rst +++ /dev/null @@ -1,34 +0,0 @@ -{{ objname | escape | underline}} - -.. currentmodule:: {{ module }} - -.. autoclass:: {{ objname }} - :members: - :show-inheritance: - :member-order: bysource - - {% block methods %} - {% if methods %} - .. rubric:: Methods - - .. autosummary:: - :nosignatures: - :toctree: - {% for item in methods %} - ~{{ objname }}.{{ item }} - {%- endfor %} - {% endif %} - {% endblock %} - - {% block attributes %} - {% if attributes %} - .. rubric:: Attributes - - .. autosummary:: - :nosignatures: - :toctree: - {% for item in attributes %} - ~{{ objname }}.{{ item }} - {%- endfor %} - {% endif %} - {% endblock %} \ No newline at end of file diff --git a/docs/source/templates/autosummary/data.rst b/docs/source/templates/autosummary/data.rst deleted file mode 100644 index e5eb894b..00000000 --- a/docs/source/templates/autosummary/data.rst +++ /dev/null @@ -1,6 +0,0 @@ -{{ objname | escape | underline}} - -.. currentmodule:: {{ module }} - -.. autodata:: {{ objname }} - :annotation: diff --git a/docs/source/templates/autosummary/function.rst b/docs/source/templates/autosummary/function.rst deleted file mode 100644 index 59888209..00000000 --- a/docs/source/templates/autosummary/function.rst +++ /dev/null @@ -1,5 +0,0 @@ -{{ objname | escape | underline}} - -.. currentmodule:: {{ module }} - -.. autofunction:: {{ objname }} \ No newline at end of file diff --git a/docs/source/templates/header-text.html b/docs/source/templates/header-text.html deleted file mode 100644 index 40de2ab9..00000000 --- a/docs/source/templates/header-text.html +++ /dev/null @@ -1,5 +0,0 @@ - \ No newline at end of file diff --git a/docs/source/templates/navbar-version.html b/docs/source/templates/navbar-version.html deleted file mode 100644 index ea972fcc..00000000 --- a/docs/source/templates/navbar-version.html +++ /dev/null @@ -1,15 +0,0 @@ -{# templates/navbar-version.html #} -{%- set nav_version = version %} -{%- if READTHEDOCS and current_version %} - {%- set nav_version = current_version %} -{%- endif %} - \ No newline at end of file diff --git a/docs/stylesheets/gallery.css b/docs/stylesheets/gallery.css new file mode 100644 index 00000000..2ca912e4 --- /dev/null +++ b/docs/stylesheets/gallery.css @@ -0,0 +1,24 @@ +/* Soften the example-gallery cards: rounder corners, gentle shadow, clipped image. */ +.mkd-glr-thumbcontainer { + border: none; + border-radius: 14px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.12); + overflow: hidden; + transition: box-shadow 0.2s ease, transform 0.2s ease; +} + +.mkd-glr-thumbcontainer:hover { + border: none; + box-shadow: 0 6px 18px rgba(0, 0, 0, 0.18); + transform: translateY(-2px); +} + +.mkd-glr-thumbcontainer img { + border-radius: 8px; +} + +/* center the card titles */ +.mkd-glr-thumbcontainer a.internal, +.mkd-glr-thumbcontainer p { + text-align: center; +} diff --git a/docs/superpowers/plans/2026-06-23-mkdocs-properdocs-migration.md b/docs/superpowers/plans/2026-06-23-mkdocs-properdocs-migration.md new file mode 100644 index 00000000..e293b8a7 --- /dev/null +++ b/docs/superpowers/plans/2026-06-23-mkdocs-properdocs-migration.md @@ -0,0 +1,696 @@ +# MyoGen Docs → properdocs (MkDocs) Migration Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Replace MyoGen's Sphinx docs with properdocs (mkdocs-material + mkdocstrings) while keeping the executed example gallery via mkdocs-gallery; CI deploys to GitHub Pages. + +**Architecture:** A repo-root `properdocs.yml` (mkdocs config) drives a `material` theme, `mkdocstrings` API pages, and a `gallery` plugin that executes the `examples/` scripts. Content is flat markdown under `docs/`; `docs/superpowers/` is excluded from the build. A `docs.yml` GitHub workflow runs `properdocs build` (executing the gallery) and deploys. + +**Tech Stack:** properdocs 1.6.x, mkdocs-material, mkdocstrings[python], mkdocs-section-index, mkdocs-gallery; uv for env management. + +**Reference:** mirror `/Users/oj98yqyk/code/MyoGestic-main/properdocs.yml` and `.github/workflows/docs.yml`. Source spec: `docs/superpowers/specs/2026-06-23-mkdocs-properdocs-migration-design.md`. + +**Verification model:** there are no pytest tests here — each task's "test" is a build/inspection. The canonical fast build (no example execution) is: + +```bash +cd /Users/oj98yqyk/code/MyoGen +MKDOCS_GALLERY_PLOT=false uv run --extra docs --extra nwb properdocs build 2>&1 | tail -30 +``` + +(Until the gallery plugin exists, drop the env var.) "Open the site" means `open site/index.html`. + +--- + +### Task 1: Swap the `docs` extra in pyproject.toml + +**Files:** Modify `pyproject.toml` (the `docs = [...]` block, ~lines 82-100); Modify `uv.lock`. + +- [ ] **Step 1: Replace the docs extra** + +In `pyproject.toml`, replace the entire `docs = [ ... ]` list under `[project.optional-dependencies]` with: + +```toml +docs = [ + "properdocs>=1.6.7", + "mkdocs-material>=9.5", + "mkdocstrings[python]>=0.27", + "mkdocs-section-index>=0.3", + "mkdocs-gallery>=0.10", +] +``` + +(The `nwb` extra already carries `pynwb`/`nwbinspector`/`h5py`; example execution syncs `--extra docs --extra nwb` together. Sphinx deps are removed.) + +- [ ] **Step 2: Lock and verify resolution** + +Run: +```bash +uv lock 2>&1 | tail -3 +uv sync --extra docs --extra nwb 2>&1 | tail -5 +``` +Expected: `uv lock` resolves; `uv sync` installs properdocs + mkdocs-material + mkdocstrings + mkdocs-section-index + mkdocs-gallery with no error. Confirm the binary exists: +```bash +uv run properdocs --version +``` +Expected: prints a version (e.g. `properdocs, version 1.6.7`). + +- [ ] **Step 3: Commit** + +```bash +git add pyproject.toml uv.lock +git commit -m "build(docs): swap Sphinx stack for properdocs + mkdocs-gallery" +``` + +--- + +### Task 2: properdocs.yml skeleton + home page (theme/nav, no gallery yet) + +**Files:** Create `properdocs.yml`; Create `docs/index.md` (temporary minimal home, replaced in Task 4); Modify `.gitignore`. + +- [ ] **Step 1: Create `properdocs.yml`** + +Create `properdocs.yml` at the repo root (adapted from MyoGestic's; gallery plugin added in Task 5): + +```yaml +site_name: MyoGen +site_description: Modular neuromuscular simulation framework for motor-unit activity, force, and EMG. +site_url: https://nsquaredlab.github.io/MyoGen/ +repo_url: https://github.com/NsquaredLab/MyoGen +repo_name: MyoGen +edit_uri: edit/main/docs/ +copyright: Copyright © 2025-2026 n-squared lab, FAU Erlangen-Nürnberg + +# superpowers specs/plans live under docs/ but are not site pages +exclude_docs: | + superpowers/ + +theme: + name: material + features: + - navigation.instant + - navigation.tracking + - navigation.tabs + - navigation.tabs.sticky + - navigation.sections + - navigation.indexes + - navigation.top + - search.suggest + - search.highlight + - content.code.copy + - content.code.annotate + - toc.follow + palette: + - media: "(prefers-color-scheme: light)" + scheme: default + primary: white + accent: indigo + toggle: + icon: material/brightness-7 + name: Switch to dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: black + accent: indigo + toggle: + icon: material/brightness-4 + name: Switch to light mode + font: + text: Inter + code: JetBrains Mono + icon: + repo: fontawesome/brands/github + +plugins: + - search + - section-index + - mkdocstrings: + handlers: + python: + paths: [.] + inventories: + - https://docs.python.org/3/objects.inv + - https://numpy.org/doc/stable/objects.inv + - https://docs.scipy.org/doc/scipy/objects.inv + options: + docstring_style: numpy + show_source: true + show_root_heading: true + show_root_full_path: false + members_order: source + separate_signature: true + show_signature_annotations: true + signature_crossrefs: true + merge_init_into_class: true + heading_level: 2 + filters: + - "!^_" + +markdown_extensions: + - admonition + - attr_list + - def_list + - md_in_html + - tables + - toc: + permalink: true + - pymdownx.details + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format + - pymdownx.tabbed: + alternate_style: true + - pymdownx.highlight: + anchor_linenums: true + pygments_lang_class: true + - pymdownx.inlinehilite + - pymdownx.magiclink + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + +nav: + - Home: index.md +``` + +- [ ] **Step 2: Create a minimal home page** + +Create `docs/index.md`: + +```markdown +# MyoGen + +Modular, extensible neuromuscular simulation framework for generating +physiologically grounded motor-unit activity, muscle force, and EMG +(surface and intramuscular). + +This site is built with [properdocs](https://pypi.org/project/properdocs/). +``` + +- [ ] **Step 3: Ignore build outputs** + +Append to `.gitignore`: + +```gitignore +# mkdocs / properdocs build output +/site/ +# mkdocs-gallery generated pages +/docs/auto_examples/ +``` + +- [ ] **Step 4: Build and verify** + +Run: +```bash +uv run --extra docs --extra nwb properdocs build 2>&1 | tail -20 +``` +Expected: build succeeds, writes `site/`. Then: +```bash +test -f site/index.html && echo "OK site built" && open site/index.html +``` +Expected: `OK site built`; the page opens showing the MyoGen home with the material theme (light/dark toggle, MyoGen title). + +- [ ] **Step 5: Commit** + +```bash +git add properdocs.yml docs/index.md .gitignore +git commit -m "docs: add properdocs.yml skeleton with material theme + home page" +``` + +--- + +### Task 3: API reference pages via mkdocstrings + +**Files:** Create `docs/api/index.md`, `docs/api/simulator.md`, `docs/api/neuron.md`, `docs/api/currents.md`, `docs/api/io-and-neuron.md`, `docs/api/plotting.md`, `docs/api/types.md`, `docs/api/myogen.md`; Modify `properdocs.yml` (nav). + +The conversion rule: each old rst `autosummary` entry under a `.. currentmodule:: X` becomes a mkdocstrings block `::: X.Symbol`. Keep the section headings as markdown `##`/`###`. + +- [ ] **Step 1: Create `docs/api/index.md`** + +```markdown +# API Reference + +- [Top-level (`myogen`)](myogen.md) +- [Simulator](simulator.md) +- [Currents](currents.md) +- [Neuron injection & I/O](io-and-neuron.md) +- [Plotting](plotting.md) +- [Types](types.md) +``` + +- [ ] **Step 2: Create `docs/api/myogen.md`** (from `myogen_api.rst`) + +```markdown +# Top-level API (`myogen`) + +::: myogen.set_random_seed +::: myogen.get_random_generator +::: myogen.get_random_seed +::: myogen.derive_subseed +::: myogen.load_nmodl_mechanisms +::: myogen.get_mechanism_parameters +::: myogen.validate_mechanism_parameter +::: myogen.set_mechanism_param +``` + +- [ ] **Step 3: Create `docs/api/simulator.md`** (from `simulator_api.rst`) + +```markdown +# Simulator + +## Recruitment +::: myogen.simulator.RecruitmentThresholds + +## Neuron populations +::: myogen.simulator.neuron.populations.AlphaMN__Pool +::: myogen.simulator.neuron.populations.DescendingDrive__Pool +::: myogen.simulator.neuron.populations.AffIa__Pool +::: myogen.simulator.neuron.populations.AffII__Pool +::: myogen.simulator.neuron.populations.AffIb__Pool +::: myogen.simulator.neuron.populations.GII__Pool +::: myogen.simulator.neuron.populations.GIb__Pool + +## Network & runner +::: myogen.simulator.neuron.network.Network +::: myogen.simulator.neuron.simulation_runner.SimulationRunner + +## Muscle & force +::: myogen.simulator.Muscle +::: myogen.simulator.neuron.muscle.HillModel +::: myogen.simulator.ForceModel +::: myogen.simulator.ForceModelVectorized + +## EMG +::: myogen.simulator.SurfaceEMG +::: myogen.simulator.IntramuscularEMG +::: myogen.simulator.SurfaceElectrodeArray +::: myogen.simulator.IntramuscularElectrodeArray + +## Proprioception +::: myogen.simulator.neuron.proprioception.SpindleModel +::: myogen.simulator.neuron.proprioception.GolgiTendonOrganModel +::: myogen.simulator.neuron.joint_dynamics.JointDynamics +``` + +- [ ] **Step 4: Create `docs/api/currents.md`** (from `currents_api.rst`) + +```markdown +# Currents + +::: myogen.utils.currents.create_ramp_current +::: myogen.utils.currents.create_step_current +::: myogen.utils.currents.create_sinusoidal_current +::: myogen.utils.currents.create_sawtooth_current +::: myogen.utils.currents.create_trapezoid_current +``` + +- [ ] **Step 5: Create `docs/api/io-and-neuron.md`** (from `utils_api.rst` injection + saver + nwb) + +```markdown +# Neuron injection & I/O + +## Current injection +::: myogen.utils.neuron.inject_currents_into_populations.inject_currents_into_populations +::: myogen.utils.neuron.inject_currents_into_populations.inject_currents_and_simulate_spike_trains + +## Persistence +::: myogen.utils.continuous_saver.ContinuousSaver +::: myogen.utils.continuous_saver.convert_chunks_to_neo + +## NWB export +!!! note + NWB export requires optional dependencies: `pip install myogen[nwb]`. + +::: myogen.utils.nwb.export_to_nwb +::: myogen.utils.nwb.export_simulation_to_nwb +::: myogen.utils.nwb.validate_nwb +``` + +- [ ] **Step 6: Create `docs/api/plotting.md`** (from `plotting_api.rst`) + +```markdown +# Plotting + +::: myogen.utils.plotting.plot_raster_spikes +::: myogen.utils.plotting.plot_membrane_potentials +::: myogen.utils.plotting.plot_muscle_dynamics +::: myogen.utils.plotting.plot_antagonist_muscle_comparison +::: myogen.utils.plotting.plot_spindle_dynamics +::: myogen.utils.plotting.plot_gto_dynamics +``` + +- [ ] **Step 7: Create `docs/api/types.md`** (from `types_api.rst`) + +```markdown +# Types + +::: myogen.utils.types + options: + members: + - Quantity__s + - Quantity__ms + - Quantity__rad + - Quantity__deg + - Quantity__mV + - Quantity__uV + - Quantity__nA + - Quantity__uS + - Quantity__S_per_m + - Quantity__Hz + - Quantity__pps + - Quantity__mm + - Quantity__m + - Quantity__mm2 + - Quantity__per_mm2 + - Quantity__m_per_s + - Quantity__mm_per_s + - CURRENT__AnalogSignal + - FORCE__AnalogSignal + - SPIKE_TRAIN__Block + - SURFACE_MUAP__Block + - SURFACE_EMG__Block + - INTRAMUSCULAR_MUAP__Block + - INTRAMUSCULAR_EMG__Block +``` + +- [ ] **Step 8: Add API to nav** + +In `properdocs.yml`, extend `nav:` to: + +```yaml +nav: + - Home: index.md + - API reference: + - api/index.md + - Top-level: api/myogen.md + - Simulator: api/simulator.md + - Currents: api/currents.md + - Injection & I/O: api/io-and-neuron.md + - Plotting: api/plotting.md + - Types: api/types.md +``` + +- [ ] **Step 9: Build and verify mkdocstrings resolves every symbol** + +Run: +```bash +uv run --extra docs --extra nwb properdocs build --strict 2>&1 | tail -40 +``` +Expected: build succeeds with NO "Could not collect" / griffe import warnings. If any symbol path is wrong (e.g. a class lives in a different module), fix the `:::` path against the real location in `myogen/` and rebuild. Then: +```bash +open site/api/simulator/index.html +``` +Expected: rendered API docs with signatures + numpy docstrings for `AlphaMN__Pool`, `SurfaceEMG`, etc. + +- [ ] **Step 10: Commit** + +```bash +git add docs/api properdocs.yml +git commit -m "docs: add mkdocstrings API reference pages" +``` + +--- + +### Task 4: Prose pages (home, getting-started, neo blocks) + assets + +**Files:** Rewrite `docs/index.md`; Create `docs/getting-started.md`, `docs/neo-blocks.md`; Create `docs/stylesheets/` and `docs/images/` as needed; Modify `properdocs.yml` (nav, logo, extra_css). + +- [ ] **Step 1: Real home page** + +Replace `docs/index.md` with a landing page adapted from the existing `docs/source/index.md` and the project README: a one-paragraph intro, an install snippet (`uv add myogen` / `pip install myogen`), and cards/links to Getting Started, the Examples gallery, and the API reference. Use the existing `docs/source/index.md` content as the source of truth — read it and port the prose to markdown (drop Sphinx directives; convert `{toctree}`/`grid` to a plain markdown list or `attr_list` cards). + +- [ ] **Step 2: Getting Started** + +Create `docs/getting-started.md` from the install + quick-start prose in the project `README.md` (root). Port the minimal end-to-end snippet (create pool → inject current → simulate → force/EMG) as a fenced `python` block. + +- [ ] **Step 3: Neo blocks guide** + +Create `docs/neo-blocks.md` by converting `docs/source/neo_blocks_guide.rst` to markdown (headings, code-blocks, lists). Read the rst and translate directives: `.. code-block:: python` → ```` ```python ````, `.. note::` → `!!! note`, cross-refs → plain text or markdown links. + +- [ ] **Step 4: Assets** + +If `docs/source/_static/` contains a logo or custom CSS still wanted, copy the logo to `docs/images/` and any CSS to `docs/stylesheets/`, and reference them in `properdocs.yml` (`theme.logo`, `theme.favicon`, `extra_css`). If there are no assets worth keeping, skip and note it in the commit. + +- [ ] **Step 5: Nav** + +Update `properdocs.yml` `nav:` to: + +```yaml +nav: + - Home: index.md + - Getting Started: getting-started.md + - Neo blocks: neo-blocks.md + - API reference: + - api/index.md + - Top-level: api/myogen.md + - Simulator: api/simulator.md + - Currents: api/currents.md + - Injection & I/O: api/io-and-neuron.md + - Plotting: api/plotting.md + - Types: api/types.md +``` + +- [ ] **Step 6: Build and verify** + +Run: +```bash +uv run --extra docs --extra nwb properdocs build --strict 2>&1 | tail -20 +open site/index.html +``` +Expected: build clean; home, getting-started, and neo-blocks render with working nav. + +- [ ] **Step 7: Commit** + +```bash +git add docs/index.md docs/getting-started.md docs/neo-blocks.md docs/images docs/stylesheets properdocs.yml +git commit -m "docs: migrate home, getting-started, and neo-blocks prose to markdown" +``` + +--- + +### Task 5: mkdocs-gallery — executed example gallery + +**Files:** Create `docs/gallery_conf.py`; Modify `properdocs.yml` (add `gallery` plugin + nav entry). + +- [ ] **Step 1: Gallery reset hook** + +Create `docs/gallery_conf.py` (ported from the old `conf.py` `reset_neuron`): + +```python +"""mkdocs-gallery configuration helpers for MyoGen examples.""" + + +def reset_neuron(gallery_conf, fname): + """Reset NEURON global HOC state between gallery examples. + + NEURON's interpreter keeps process-global state across examples run in one + process; clearing sections + time between examples keeps them independent. + """ + try: + import myogen # noqa: F401 (auto-loads mechanisms, sets up NEURON) + from neuron import h + + for sec in list(h.allsec()): + h.delete_section(sec=sec) + h.load_file("stdrun.hoc") + h.t = 0 + h.tstop = 0 + except (ImportError, RuntimeError, LookupError, AttributeError): + pass +``` + +- [ ] **Step 2: Add the gallery plugin to `properdocs.yml`** + +Insert into the `plugins:` list (after `mkdocstrings`): + +```yaml + - gallery: + examples_dirs: + - examples/01_basic + - examples/02_finetune + - examples/03_papers/watanabe + gallery_dirs: + - docs/auto_examples/01_basic + - docs/auto_examples/02_finetune + - docs/auto_examples/03_papers/watanabe + filename_pattern: "\\.py" + ignore_pattern: "(14_calibrate_noise_from_real|_oscillating_dc_helpers|_optimize_dc_worker)\\.py" + within_subsection_order: mkdocs_gallery.sorting.FileNameSortKey + image_scrapers: matplotlib + reset_modules: + - !!python/name:gallery_conf.reset_neuron + plot_gallery: !ENV [MKDOCS_GALLERY_PLOT, true] +``` + +Note: `paths: [.]` already on mkdocstrings puts repo root on `sys.path`; mkdocs-gallery resolves `!!python/name:gallery_conf.reset_neuron` because `docs/` is added to the path by the gallery plugin. If the name fails to resolve at build time, change the reference to `docs.gallery_conf.reset_neuron` and add an empty `docs/__init__.py`, OR move `gallery_conf.py` to the repo root and keep `gallery_conf.reset_neuron`. Verify by build (next step). The `!ENV` makes `MKDOCS_GALLERY_PLOT=false` skip execution for fast iteration. + +- [ ] **Step 3: Add the gallery to nav** + +Add under `nav:` (after Neo blocks): + +```yaml + - Examples: + - Basics: auto_examples/01_basic/index.md + - Fine-tuning: auto_examples/02_finetune/index.md + - Watanabe (paper): auto_examples/03_papers/watanabe/index.md +``` + +- [ ] **Step 4: Fast build (no execution) to validate gallery wiring** + +Run: +```bash +MKDOCS_GALLERY_PLOT=false uv run --extra docs --extra nwb properdocs build 2>&1 | tail -40 +``` +Expected: build succeeds; `docs/auto_examples/01_basic/index.md` and per-example pages are generated from the `# %%` blocks (source shown, no executed output). If `!!python/name:gallery_conf...` fails to resolve, apply the fallback in Step 2. Then: +```bash +open site/auto_examples/01_basic/index.html +``` +Expected: a gallery index listing the basic examples with their headers. + +- [ ] **Step 5: Full build (executes examples) — the real verification** + +Run (this executes all gallery examples; watanabe/02 is parallelized so ~minutes, not tens of minutes): +```bash +uv run --extra docs --extra nwb properdocs build 2>&1 | tail -40 +``` +Expected: build succeeds; example pages now contain rendered plots. Spot-check: +```bash +open site/auto_examples/01_basic/index.html +``` +Expected: thumbnails/plots rendered for the basic examples. If a specific example errors during execution, fix the example or extend `ignore_pattern` (and note why), then rebuild. + +- [ ] **Step 6: Commit** + +```bash +git add docs/gallery_conf.py properdocs.yml +git commit -m "docs: add executed example gallery via mkdocs-gallery" +``` + +--- + +### Task 6: GitHub Pages CI workflow + +**Files:** Create `.github/workflows/docs.yml`; Delete `.github/workflows/sphinx.yml`. + +- [ ] **Step 1: Create `.github/workflows/docs.yml`** (mirroring MyoGestic's) + +```yaml +name: Docs + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Set up uv + uses: astral-sh/setup-uv@v8.1.0 + with: + enable-cache: true + - name: Sync project + docs extras + run: uv sync --locked --extra docs --extra nwb + - name: Build docs site (executes the example gallery) + run: uv run --locked --extra docs --extra nwb properdocs build + - name: Upload Pages artifact + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + uses: actions/upload-pages-artifact@v5 + with: + path: site + + deploy: + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deploy.outputs.page_url }} + steps: + - name: Configure Pages + uses: actions/configure-pages@v6 + - name: Deploy to GitHub Pages + id: deploy + uses: actions/deploy-pages@v5 +``` + +- [ ] **Step 2: Remove the Sphinx workflow** + +```bash +git rm .github/workflows/sphinx.yml +``` + +- [ ] **Step 3: Validate the workflow YAML** + +Run: +```bash +uv run python -c "import yaml; yaml.safe_load(open('.github/workflows/docs.yml')); print('docs.yml valid YAML')" +``` +Expected: `docs.yml valid YAML`. (Note: the NEURON-in-CI build path mirrors the old sphinx.yml, which already executed the gallery; if `sphinx.yml` installed extra system deps for NEURON, port those `apt`/setup steps into `docs.yml`'s build job — read `sphinx.yml` before deleting and carry over any NEURON system setup.) + +- [ ] **Step 4: Commit** + +```bash +git add .github/workflows/docs.yml +git commit -m "ci(docs): build + deploy properdocs site to GitHub Pages; drop sphinx workflow" +``` + +--- + +### Task 7: Remove the Sphinx scaffolding + final build + +**Files:** Delete `docs/source/` (conf.py, examples.rst, api/*.rst, index.md, neo_blocks_guide.rst, index.rst.backup, templates/, _static/ leftovers, README.md), `docs/Makefile`, `docs/make.bat`, `docs/README.md`/`docs/neo_blocks.md` if superseded. + +- [ ] **Step 1: Confirm nothing still references `docs/source/`** + +Run: +```bash +grep -rIl "docs/source\|sphinx_gallery\|conf.py" --include="*.py" --include="*.toml" --include="*.yml" --include="*.cfg" . | grep -v site/ | grep -v .venv/ +``` +Expected: no references outside the files about to be deleted. If `pyproject.toml`/`README.md` link to the old docs paths, update them to the new site URL/paths. + +- [ ] **Step 2: Delete the Sphinx tree** + +```bash +git rm -r docs/source docs/Makefile docs/make.bat +git rm docs/neo_blocks.md docs/README.md 2>/dev/null || true # only if superseded by docs/neo-blocks.md / docs/index.md +``` +(Keep `docs/superpowers/` — it's excluded from the build, not part of the site.) + +- [ ] **Step 3: Final clean full build** + +```bash +uv run --extra docs --extra nwb properdocs build --strict 2>&1 | tail -30 +test -f site/index.html && echo "FINAL SITE OK" && open site/index.html +``` +Expected: `--strict` build succeeds end-to-end (home, getting-started, neo-blocks, all API pages, executed gallery), no broken nav/links, `FINAL SITE OK`. + +- [ ] **Step 4: Commit** + +```bash +git add -A +git commit -m "docs: remove Sphinx scaffolding after properdocs migration" +``` + +--- + +## Final verification + +```bash +uv run --extra docs --extra nwb properdocs build --strict +open site/index.html +``` +The site builds strictly, the gallery executes and renders plots, and `docs/superpowers/` is absent from the site. The branch is ready to push (CI will reproduce the build + deploy on merge to `main`). diff --git a/docs/superpowers/specs/2026-06-23-mkdocs-properdocs-migration-design.md b/docs/superpowers/specs/2026-06-23-mkdocs-properdocs-migration-design.md new file mode 100644 index 00000000..c10c2929 --- /dev/null +++ b/docs/superpowers/specs/2026-06-23-mkdocs-properdocs-migration-design.md @@ -0,0 +1,62 @@ +# MyoGen docs migration: Sphinx → properdocs (MkDocs) + mkdocs-gallery + +**Status:** Design proposal (approved). Lands on `feat/puffer-api`. +**Date:** 2026-06-23 + +## Goal + +Replace MyoGen's Sphinx documentation stack with **properdocs** (NsquaredLab's mkdocs-material + mkdocstrings wrapper, as used by MyoGestic and Virtual-Hand-Interface), while **keeping the executed example gallery** via the **mkdocs-gallery** plugin. End state: `properdocs build` produces the complete site, examples still auto-execute and render their plots, and CI deploys to GitHub Pages. + +## Why + +Consistency with the other NsquaredLab projects (MyoGestic, VHI) which all use properdocs. properdocs is a thin mkdocs fork (`config.load_config` + `cfg.plugins`), on PyPI (1.6.7), and supports arbitrary mkdocs plugins — so mkdocs-gallery can be added to keep the executed gallery that sphinx-gallery currently provides. + +## Non-goals + +- Rewriting example *content* — the `# %%`-delimited scripts are mkdocs-gallery-compatible and stay as-is (only the gallery engine changes). +- Changing the public API or any `myogen/` code. + +## Architecture + +### Tooling +- New **`properdocs.yml`** at repo root (mkdocs config format), mirroring MyoGestic's: `theme: material` (custom palette, Inter/JetBrains Mono fonts, navigation features), `plugins: [search, section-index, mkdocstrings, gallery]`, and the same `markdown_extensions` (admonition, pymdownx.*, toc permalink, mermaid superfence). +- `pyproject.toml` `docs` extra: **remove** `sphinx`, `sphinx-gallery`, `pydata-sphinx-theme`, `sphinx-design`, `sphinx-hoverxref`, `sphinxcontrib-mermaid`, `sphinx-autodoc-typehints`, `enum-tools[sphinx]`, `rinohtype`, `roman`, `toml`, `linkify-it-py`, `memory-profiler`. **Add** `properdocs`, `mkdocs-material>=9.5`, `mkdocstrings[python]>=0.27`, `mkdocs-section-index>=0.3`, `mkdocs-gallery`. Keep `pynwb`/`nwbinspector` only if still referenced (NWB lives in the `nwb` extra; example execution syncs both extras in CI). + +### Content layout (flat `docs/`, no `source/`) +- `docs/index.md` — home (from existing `docs/source/index.md`). +- `docs/getting-started.md` — quick start. +- `docs/neo-blocks.md` — from `docs/source/neo_blocks_guide.rst`. +- `docs/api/index.md` + `docs/api/{core,simulator,neuron,currents,utils,plotting,types}.md` — converted from the 8 rst `autosummary` pages to **mkdocstrings `:::` blocks**, preserving the section headings. mkdocstrings options mirror MyoGestic (numpy docstrings, `show_root_heading`, `merge_init_into_class`, `filters: ["!^_"]`). +- `nav:` in `properdocs.yml` lists Home / Getting Started / Neo blocks / Examples (gallery) / API reference. +- **`exclude_docs:`** in the config globs out `superpowers/` so the brainstorming specs/plans under `docs/superpowers/` are not built as site pages. +- Static assets (logo, any custom CSS) migrated from `docs/source/_static/` to `docs/` (e.g. `docs/stylesheets/`, `docs/images/`). + +### Executed gallery (mkdocs-gallery) +- The `gallery` plugin block in `properdocs.yml` ports the current `sphinx_gallery_conf` ~1:1: + - `examples_dirs: [examples/01_basic, examples/02_finetune, examples/03_papers/watanabe]` + - `gallery_dirs: [docs/auto_examples/01_basic, docs/auto_examples/02_finetune, docs/auto_examples/03_papers/watanabe]` (generated; gitignored) + - `filename_pattern: "\\.py"`, `ignore_pattern: "(14_calibrate_noise_from_real|_oscillating_dc_helpers|_optimize_dc_worker)\\.py"` + - `within_subsection_order: FileNameSortKey`, explicit subsection order, `plot_gallery: True`, `image_scrapers: matplotlib`. + - `reset_modules`: the existing `reset_neuron(gallery_conf, fname)` (resets NEURON `h` state between examples), moved to **`docs/gallery_conf.py`** and referenced from the plugin config. +- The generated `auto_examples/` tree is added to `.gitignore` and to the nav (or surfaced via section-index). + +### CI +- Delete `.github/workflows/sphinx.yml`; add `.github/workflows/docs.yml` mirroring MyoGestic's: build job runs `uv sync --locked --extra docs --extra nwb` (so NEURON + example deps are present and the gallery can execute), then `uv run --locked --extra docs --extra nwb properdocs build`, uploads the `site/` Pages artifact; deploy job publishes to GitHub Pages on push to `main` (PRs build-only). Concurrency group `pages`. + +### Removal +Delete `docs/source/conf.py`, `docs/source/examples.rst`, `docs/source/api/*.rst`, `docs/source/index.rst.backup`, `docs/source/neo_blocks_guide.rst` (after conversion), `docs/source/templates/`, `docs/Makefile`, `docs/make.bat`, and the Sphinx `_static`/`source/` scaffolding once content is migrated. + +## Verification + +1. `uv sync --extra docs --extra nwb` resolves. +2. `properdocs build` with the gallery temporarily set to `plot_gallery: False` → confirms config/theme/nav/mkdocstrings all resolve fast (no example execution). Open `site/index.html`. +3. Full `properdocs build` (gallery executes; watanabe/02 now ~139s) → confirms examples render with plots. Open the built gallery pages. +4. mkdocstrings resolves every API symbol (requires a full `import myogen`, which needs NEURON — present via the synced extras). +5. Show the rendered site to the user at each checkpoint. + +## Risks / mitigations + +- **mkdocs-gallery ↔ properdocs**: properdocs is mkdocs-based and infers deps from the `plugins:` list, so the `gallery` plugin should load; verified at build time in step 2/3. +- **CI build time**: executing the gallery is the same cost as today's sphinx CI; the slowest example (watanabe/02) is now 5× faster. The `14`/helper/worker ignores keep non-executable scripts out. +- **`docs/superpowers/` leaking into the site**: handled by `exclude_docs`. +- **mkdocstrings import failures**: any optional dep referenced by an API symbol must be installed in the build env — covered by syncing `--extra docs --extra nwb`. diff --git a/examples/01_basic/01_simulate_recruitment_thresholds.py b/examples/01_basic/01_simulate_recruitment_thresholds.py index 93e0a86e..9d67536b 100644 --- a/examples/01_basic/01_simulate_recruitment_thresholds.py +++ b/examples/01_basic/01_simulate_recruitment_thresholds.py @@ -4,10 +4,10 @@ The first step in using **MyoGen** is to generate the **recruitment thresholds** of the **motor units** (MUs). -.. note:: +!!! note A **recruitment threshold** is the minimum force required to activate a MU. - In **MyoGen**, the threshold is defined between ``0`` and ``1``, where ``0`` is the minimum force required to activate a MU and ``1`` is the maximum force required to activate a MU. + In **MyoGen**, the threshold is defined between `0` and `1`, where `0` is the minimum force required to activate a MU and `1` is the maximum force required to activate a MU. **MyoGen** offers **4 different models** to generate the recruitment thresholds: @@ -38,9 +38,9 @@ # - ``N``: Number of motor units in the pool # - ``recruitment_range``: Recruitment range (max_threshold / min_threshold) # -# .. note:: -# The **recruitment_range** is defined as the ratio between the ``maximum`` and ``minimum`` recruitment thresholds. -# For example, if the **recruitment_range** is ``50``, the biggest MU will have a **recruitment threshold** ``50`` times bigger than the smallest MU. +# !!! note +# The **recruitment_range** is defined as the ratio between the `maximum` and `minimum` recruitment thresholds. +# For example, if the **recruitment_range** is `50`, the biggest MU will have a **recruitment threshold** `50` times bigger than the smallest MU. n_motor_units = 100 # Number of motor units in the pool recruitment_range = 100 # Recruitment range (max_threshold / min_threshold) @@ -58,12 +58,12 @@ # # **No additional parameters needed** - only requires the common parameters. # -# .. important:: -# **MyoGen** is intended to be used with the following API: +# !!! important +# **MyoGen** is intended to be used with the following API: # -# .. code-block:: python -# -# from myogen import simulator +# ```python +# from myogen import simulator +# ``` rt_fuglevand, _ = simulator.RecruitmentThresholds( N=n_motor_units, recruitment_range__ratio=recruitment_range, mode="fuglevand" @@ -167,7 +167,9 @@ # Save Recruitment Thresholds # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ # -# .. note:: -# All **MyoGen** objects can be saved to a file using ``joblib``. This is useful to **avoid re-running expensive simulations** if you need to use the same parameters. +# !!! note +# All **MyoGen** objects can be saved to a file using `joblib`. This is useful to **avoid re-running expensive simulations** if you need to use the same parameters. joblib.dump(combined_results[5], save_path / "thresholds.pkl") + +# mkdocs_gallery_thumbnail_path = "gallery_thumbs/01_simulate_recruitment_thresholds.png" diff --git a/examples/01_basic/02_simulate_spike_trains_current_injection.py b/examples/01_basic/02_simulate_spike_trains_current_injection.py index fdbbb545..c1a41175 100644 --- a/examples/01_basic/02_simulate_spike_trains_current_injection.py +++ b/examples/01_basic/02_simulate_spike_trains_current_injection.py @@ -8,7 +8,7 @@ 1. Manual step-by-step workflow — explicitly walks through each stage of the NEURON simulation pipeline. This workflow is intended to clarify the underlying mechanisms. -2. Utility-function workflow — uses the high-level :func:`~myogen.utils.neuron.inject_currents_into_populations.inject_currents_and_simulate_spike_trains` function for routine simulations. +2. Utility-function workflow — uses the high-level `inject_currents_and_simulate_spike_trains` function for routine simulations. Both workflows yield identical results; the manual version is provided purely for explanatory purposes. """ @@ -19,28 +19,27 @@ # Import Libraries # ---------------- # -# .. important:: -# In **MyoGen** all **random number generation** is handled by the RNG returned from -# :func:`~myogen.get_random_generator`, a thin wrapper around :mod:`numpy.random`. +# !!! important +# In **MyoGen** all **random number generation** is handled by the RNG returned from +# [`get_random_generator`][myogen.get_random_generator], a thin wrapper around `numpy.random`. # -# Always fetch the generator at the call site so the current seed is honored: +# Always fetch the generator at the call site so the current seed is honored: # -# .. code-block:: python +# ```python +# from myogen import simulator, get_random_generator +# get_random_generator().normal(0, 1) +# ``` # -# from myogen import simulator, get_random_generator -# get_random_generator().normal(0, 1) +# To change the seed, use [`set_random_seed`][myogen.set_random_seed]: # -# To change the seed, use :func:`~myogen.set_random_seed`: -# -# .. code-block:: python -# -# from myogen import set_random_seed -# set_random_seed(42) +# ```python +# from myogen import set_random_seed +# set_random_seed(42) +# ``` from pathlib import Path import joblib -import neuron import numpy as np import quantities as pq import seaborn as sns @@ -104,12 +103,12 @@ def rasterplot_rates(spiketrains, filter_function=None): # # A population can easily be created by specifying the number of cells. Plausible default parameters are already set. # -# For a motor neuron population (referred to as **motor pool**), we can use the :class:`~myogen.simulator.neuron.populations.AlphaMN__Pool` class. +# For a motor neuron population (referred to as **motor pool**), we can use the [`AlphaMN__Pool`][myogen.simulator.neuron.populations.AlphaMN__Pool] class. # This class can also use the recruitment thresholds generated in the previous example to distribute the motor units properties in a physiologically plausible manner. # -# .. important:: -# These **Population** classes are custom-built and use therefore custom NMODL mechanisms. -# To use them, the NMODL mechanisms need to be loaded first using the :func:`~myogen.utils.nmodl.load_nmodl_mechanisms` function. +# !!! important +# These **Population** classes are custom-built and use therefore custom NMODL mechanisms. +# To use them, the NMODL mechanisms need to be loaded first using the [`load_nmodl_mechanisms`][myogen.load_nmodl_mechanisms] function. # # To showcase MyoGen's capabilities, we will create two different motor neuron pools with identical properties but different input currents. load_nmodl_mechanisms() @@ -130,13 +129,13 @@ def rasterplot_rates(spiketrains, filter_function=None): # # To drive the motor units, we use a **common input current profile**. # -# In this example, we use a **trapezoid-shaped input current** which is generated using the :func:`~myogen.utils.currents.create_trapezoid_current` function. +# In this example, we use a **trapezoid-shaped input current** which is generated using the [`create_trapezoid_current`][myogen.utils.currents.create_trapezoid_current] function. # -# .. note:: -# More convenient functions for generating input current profiles are available in the :mod:`myogen.utils.currents` module. +# !!! note +# More convenient functions for generating input current profiles are available in the `myogen.utils.currents` module. # -# .. note:: -# The generated input current is an instance of the :class:`neo.core.AnalogSignal` class from the :mod:`neo` package. +# !!! note +# The generated input current is an instance of the `neo.core.AnalogSignal` class from the `neo` package. timestep = 0.05 * pq.ms simulation_time = 4000 * pq.ms @@ -174,13 +173,13 @@ def rasterplot_rates(spiketrains, filter_function=None): # Step 1: Set up current injection manually # ========================================= # We need to inject time-varying currents into each motor neuron. -# This uses NEURON's :class:`neuron.h.IClamp` (current clamp) mechanism with :meth:`neuron.h.Vector.play`. +# This uses NEURON's `neuron.h.IClamp` (current clamp) mechanism with `neuron.h.Vector.play`. inject_currents_into_populations(motor_neuron_pools, input_current__AnalogSignal) # Step 2: Set up spike recording manually # ======================================= -# For each neuron, we create a :class:`neuron.h.NetCon` (network connection) object that detects +# For each neuron, we create a `neuron.h.NetCon` (network connection) object that detects # spikes when the membrane voltage crosses a threshold, and records spike times. spike_detection_threshold__mV = 50.0 * pq.mV @@ -211,7 +210,8 @@ def rasterplot_rates(spiketrains, filter_function=None): # ============================================== # Before running, we need to initialize membrane voltages to physiological values. # -# .. note:: For this MyoGen populations provide the ``get_initialization_data()`` method. +# !!! note +# For this MyoGen populations provide the `get_initialization_data()` method. # This returns the sections and their initial voltages. # Initialize each neuron's membrane voltage to its resting potential @@ -221,12 +221,16 @@ def rasterplot_rates(spiketrains, filter_function=None): # Initialize NEURON's internal state and run the simulation h.finitialize() # Initialize all mechanisms and variables -neuron.run(simulation_time__ms) +# Advance the solver to the end of the simulation (replaces the deprecated +# neuron.run(); finitialize() above already set the initial state). +h.tstop = float(simulation_time__ms) +while h.t < h.tstop: + h.fadvance() -# Step 4: Convert recorded data to :class:`neo.core.Block` format +# Step 4: Convert recorded data to `neo.core.Block` format # ================================================================== # The spike times are now stored in NEURON vectors. We convert them to -# the standardized :class:`neo.core.Block` format for analysis and compatibility. +# the standardized `neo.core.Block` format for analysis and compatibility. spike_train__Block_manual = Block(name="Manual Simulation Results") @@ -234,13 +238,13 @@ def rasterplot_rates(spiketrains, filter_function=None): # Create a segment for this motor unit pool segment = Segment(name=f"Pool {pool_idx}") - # Convert each neuron's spike times to a :class:`neo.core.SpikeTrain` object + # Convert each neuron's spike times to a `neo.core.SpikeTrain` object segment.spiketrains = [] for neuron_idx, spike_recorder in enumerate(pool_spike_recorders): # Convert NEURON vector to numpy array and add units spike_times = (spike_recorder.as_numpy() * pq.ms).rescale(pq.s) - # Create :class:`neo.core.SpikeTrain` object with metadata + # Create `neo.core.SpikeTrain` object with metadata spiketrain = SpikeTrain( spike_times, t_stop=simulation_time__ms.rescale(pq.s), @@ -260,7 +264,7 @@ def rasterplot_rates(spiketrains, filter_function=None): # ------------------------------------ # # The manual approach above shows you exactly what happens during simulation. -# However, since this is a common task, MyoGen provides the :func:`~myogen.utils.neuron.inject_currents_into_populations.inject_currents_and_simulate_spike_trains` +# However, since this is a common task, MyoGen provides the [`inject_currents_and_simulate_spike_trains`][myogen.utils.neuron.inject_currents_into_populations.inject_currents_and_simulate_spike_trains] # utility function that encapsulates all these steps in a single call. # # This is the recommended approach for routine simulations, while the manual @@ -295,8 +299,8 @@ def rasterplot_rates(spiketrains, filter_function=None): # # It might be of interest to calculate the **firing rates** of the motor units. # -# .. note:: -# The **firing rates** are calculated as the number of spikes divided by the time in which each MU was active. +# !!! note +# The **firing rates** are calculated as the number of spikes divided by the time in which each MU was active. firing_rates = [ np.array( @@ -403,3 +407,5 @@ def rasterplot_rates(spiketrains, filter_function=None): ) plt.show() + +# mkdocs_gallery_thumbnail_path = "gallery_thumbs/02_simulate_spike_trains_current_injection.png" diff --git a/examples/01_basic/03_simulate_spike_trains_descending_drive.py b/examples/01_basic/03_simulate_spike_trains_descending_drive.py index c93ae017..dfdf398e 100644 --- a/examples/01_basic/03_simulate_spike_trains_descending_drive.py +++ b/examples/01_basic/03_simulate_spike_trains_descending_drive.py @@ -6,7 +6,7 @@ instead of direct current injection. This approach provides more physiologically accurate motor control patterns by modeling cortical input through descending drive populations. -.. note:: +!!! note This example bridges the gap between simple current injection (example 02) and full spinal network simulation (11_simulate_spinal_network.py). It uses: @@ -15,7 +15,7 @@ - **Network**: Synaptic connections between DD and motor neuron populations - **Sinusoidal patterns**: Smooth, physiologically relevant input at 0.5-2 Hz -.. important:: +!!! important **Descending Drive (DD)** refers to the cortical and subcortical neural pathways that provide voluntary motor commands to spinal motor neurons. This is more realistic than direct current injection because it models the actual synaptic input patterns from upper motor neurons. @@ -27,23 +27,23 @@ # Import Libraries # ---------------- # -# .. important:: -# In **MyoGen** all **random number generation** is handled by the RNG returned from -# ``get_random_generator()``, a thin wrapper around ``numpy.random``. +# !!! important +# In **MyoGen** all **random number generation** is handled by the RNG returned from +# [`get_random_generator`][myogen.get_random_generator], a thin wrapper around `numpy.random`. # -# Always fetch the generator at the call site so the current seed is honored: +# Always fetch the generator at the call site so the current seed is honored: # -# .. code-block:: python +# ```python +# from myogen import simulator, get_random_generator +# get_random_generator().normal(0, 1) +# ``` # -# from myogen import simulator, get_random_generator -# get_random_generator().normal(0, 1) +# To change the seed, use [`set_random_seed`][myogen.set_random_seed]: # -# To change the seed, use ``set_random_seed``: -# -# .. code-block:: python -# -# from myogen import set_random_seed -# set_random_seed(42) +# ```python +# from myogen import set_random_seed +# set_random_seed(42) +# ``` # %% @@ -91,12 +91,13 @@ def population_psth(spiketrains, bin_size): # Create Populations # ------------------------ # -# Like the previous example, we create a **motor neuron pool** using the **AlphaMN__Pool** class. +# Like the previous example, we create a **motor neuron pool** using the [`AlphaMN__Pool`][myogen.simulator.neuron.populations.AlphaMN__Pool] class. # -# We also create a **DescendingDrive__Pool** to represent the cortical input. +# We also create a [`DescendingDrive__Pool`][myogen.simulator.neuron.populations.DescendingDrive__Pool] to represent the cortical input. # -# .. note:: These neurons are modeled as Poisson point processes to convert the smooth input signal into realistic -# spike patterns that represent cortical input to the spinal cord. +# !!! note +# These neurons are modeled as Poisson point processes to convert the smooth input signal into realistic +# spike patterns that represent cortical input to the spinal cord. # load_nmodl_mechanisms() @@ -113,7 +114,7 @@ def population_psth(spiketrains, bin_size): timestep = 0.1 * pq.ms h.secondorder = 2 # Crank-Nicolson method (second-order accurate) -descending_drive_pool = DescendingDrive__Pool(n=100, poisson_batch_size=5, timestep__ms=timestep) +descending_drive_pool = DescendingDrive__Pool(n=100, process_type="poisson", timestep__ms=timestep) ############################################################################## # Generate Trapezoidal Drive Pattern # ----------------------------------- @@ -205,7 +206,7 @@ def population_psth(spiketrains, bin_size): # Create Network and Connections # ------------------------------- # -# In MyoGen, populations can be connected using the **Network** class from the +# In MyoGen, populations can be connected using the [`Network`][myogen.simulator.Network] class from the # `myogen.simulator.neuron` module. # # The **Network** class provides a high-level interface for creating and managing @@ -344,7 +345,7 @@ def population_psth(spiketrains, bin_size): [ mean_firing_rate(st__s.time_slice(st__s.min(), st__s.max())) for st__s in dd_segment.spiketrains - if len(st__s) > 0 + if len(st__s) > 1 # need >=2 spikes for a rate over the spike span ] ) @@ -353,7 +354,7 @@ def population_psth(spiketrains, bin_size): [ mean_firing_rate(st__s.time_slice(st__s.min(), st__s.max())) for st__s in mn_segment.spiketrains - if len(st__s) > 0 + if len(st__s) > 1 # need >=2 spikes for a rate over the spike span ] ) @@ -392,7 +393,7 @@ def population_psth(spiketrains, bin_size): axes[0].grid(True, alpha=0.3) # 2. DD population raster plot -dd_colors = plt.cm.get_cmap("Blues")(np.linspace(0.3, 0.8, len(dd_segment.spiketrains))) +dd_colors = plt.get_cmap("Blues")(np.linspace(0.3, 0.8, len(dd_segment.spiketrains))) for i, (spiketrain, color) in enumerate(zip(dd_segment.spiketrains, dd_colors)): if len(spiketrain) > 0: axes[1].scatter(spiketrain.magnitude, [i] * len(spiketrain), c=[color], s=0.8, alpha=0.8) @@ -403,7 +404,7 @@ def population_psth(spiketrains, bin_size): axes[1].grid(True, alpha=0.3) # 3. Motor neuron raster plot (recruitment ordered) -mn_colors = plt.cm.get_cmap("Reds")(np.linspace(0.3, 0.9, len(mn_segment.spiketrains))) +mn_colors = plt.get_cmap("Reds")(np.linspace(0.3, 0.9, len(mn_segment.spiketrains))) active_mn_count = 0 for i, (spiketrain, color) in enumerate(zip(mn_segment.spiketrains, mn_colors)): if len(spiketrain) > 0: @@ -540,7 +541,7 @@ def population_psth(spiketrains, bin_size): n_to_plot = len(active_neuron_ids) # Use colormap for lines (gradient from blue to red showing recruitment order) - colors = plt.cm.get_cmap("rainbow")(np.linspace(0, 1, n_to_plot)) + colors = plt.get_cmap("rainbow")(np.linspace(0, 1, n_to_plot)) for neuron_idx in range(n_to_plot): axes2[1].plot( @@ -567,3 +568,5 @@ def population_psth(spiketrains, bin_size): plt.show() print("\n[DONE] Simulation complete with individual neuron noise and discharge rate analysis!") + +# mkdocs_gallery_thumbnail_path = "gallery_thumbs/03_simulate_spike_trains_descending_drive.png" diff --git a/examples/01_basic/04_simulate_muscle.py b/examples/01_basic/04_simulate_muscle.py index 77ea92f8..920708f0 100644 --- a/examples/01_basic/04_simulate_muscle.py +++ b/examples/01_basic/04_simulate_muscle.py @@ -29,7 +29,7 @@ # Define Parameters # ----------------- # -# The **muscle model** is created using the **Muscle** object. +# The **muscle model** is created using the [`Muscle`][myogen.simulator.Muscle] object. # # The **Muscle** object takes the following parameters: # @@ -60,10 +60,10 @@ # Create Muscle Model # ------------------- # -# .. note:: -# Depending on the parameters, the simulation can take a few minutes to run. +# !!! note +# Depending on the parameters, the simulation can take a few minutes to run. # -# To **avoid running the simulation every time**, we can save the muscle model using ``joblib``. +# To **avoid running the simulation every time**, we can save the muscle model using `joblib`. # Create muscle model muscle = simulator.Muscle( @@ -91,9 +91,9 @@ # # The **fiber centers** are the centers of the fibers that are innervated by the motor units. # -# .. note:: -# The **fiber centers** have been precalculated from a Voronoi tessellation of the muscle volume. -# Then depending on the **fiber density**, the **fiber centers** are distributed within the muscle volume. +# !!! note +# The **fiber centers** have been precalculated from a Voronoi tessellation of the muscle volume. +# Then depending on the **fiber density**, the **fiber centers** are distributed within the muscle volume. plt.figure(figsize=(6, 6)) @@ -112,9 +112,9 @@ # # Display the spatial organization of motor units and their innervation areas. # -# .. note:: -# The **innervation areas** are the areas where the motor units are innervated. -# They are calculated using the **recruitment thresholds** and the **fiber density**. +# !!! note +# The **innervation areas** are the areas where the motor units are innervated. +# They are calculated using the **recruitment thresholds** and the **fiber density**. plt.figure(figsize=(6, 6)) @@ -126,3 +126,5 @@ plt.tight_layout() plt.show() + +# mkdocs_gallery_thumbnail_path = "gallery_thumbs/04_simulate_muscle.png" diff --git a/examples/01_basic/05_simulate_surface_muaps.py b/examples/01_basic/05_simulate_surface_muaps.py index 0362a13e..d8702291 100644 --- a/examples/01_basic/05_simulate_surface_muaps.py +++ b/examples/01_basic/05_simulate_surface_muaps.py @@ -26,7 +26,7 @@ # Define Parameters # ----------------- # -# The **surface EMG** is created using the **SurfaceEMG** object. +# The **surface EMG** is created using the [`SurfaceEMG`][myogen.simulator.SurfaceEMG] object. # # The **SurfaceEMG** object takes the following parameters: # @@ -53,13 +53,13 @@ # Create Surface EMG Model # ------------------------- # -# The **SurfaceEMG** object is initialized with the **muscle model**, the **electrode array**, and the **simulation parameters**. +# The **SurfaceEMG** object is initialized with the **muscle model**, the [`SurfaceElectrodeArray`][myogen.simulator.SurfaceElectrodeArray], and the **simulation parameters**. # -# .. note:: -# For simplicity, we only simulate the first motor unit. -# This can be changed by modifying the ``MUs_to_simulate`` parameter. +# !!! note +# For simplicity, we only simulate the first motor unit. +# This can be changed by modifying the `MUs_to_simulate` parameter. # -# This is to simulate the **surface EMG** from two different directions. +# This is to simulate the **surface EMG** from two different directions. # electrode_array_monopolar = simulator.SurfaceElectrodeArray( @@ -167,3 +167,5 @@ plt.tight_layout() plt.show() + +# mkdocs_gallery_thumbnail_path = "gallery_thumbs/05_simulate_surface_muaps.png" diff --git a/examples/01_basic/06_simulate_surface_emg.py b/examples/01_basic/06_simulate_surface_emg.py index 2124528d..9507a578 100644 --- a/examples/01_basic/06_simulate_surface_emg.py +++ b/examples/01_basic/06_simulate_surface_emg.py @@ -4,7 +4,7 @@ After having created the **MUAPs**, we can finally simulate the **surface EMG** by creating a **surface EMG model**. -.. note:: +!!! note The **surface EMG** signals are the **summation** of the **MUAPs** at the surface of the skin. In **Myogen**, we can simulate the **surface EMG** by convolving the **MUAPs** with the **spike trains** of the **motor units**. @@ -66,12 +66,12 @@ # Visualize Surface EMG Results # ----------------------------- # -# .. note:: -# Since **MyoGen** is a simulator, the results will have no real-world noise. +# !!! note +# Since **MyoGen** is a simulator, the results will have no real-world noise. # -# We can add noise to the **surface EMG** signals to make them more realistic. +# We can add noise to the **surface EMG** signals to make them more realistic. # -# For this the method ``add_noise`` is used. +# For this the method `add_noise` is used. noisy_surface_emg__Block = surface_emg.add_noise(snr__dB=5.0) @@ -120,3 +120,5 @@ plt.tight_layout() plt.show() + +# mkdocs_gallery_thumbnail_path = "gallery_thumbs/06_simulate_surface_emg.png" diff --git a/examples/01_basic/07_simulate_currents.py b/examples/01_basic/07_simulate_currents.py index d6736231..6ee19d5b 100644 --- a/examples/01_basic/07_simulate_currents.py +++ b/examples/01_basic/07_simulate_currents.py @@ -5,7 +5,7 @@ **MyoGen** provides various functions to generate different types of input currents for motor neuron pool simulations. These currents can be used to drive motor unit recruitment and firing patterns. -.. note:: +!!! note **Input currents** are the electrical stimuli applied to motor neuron pools to simulate muscle activation. Different current shapes can produce different recruitment and firing patterns. @@ -222,3 +222,5 @@ fig.suptitle("Trapezoid Currents") plt.tight_layout() plt.show() + +# mkdocs_gallery_thumbnail_path = "gallery_thumbs/07_simulate_currents.png" diff --git a/examples/01_basic/08_simulate_force.py b/examples/01_basic/08_simulate_force.py index 5fc73a8e..d538b0b0 100644 --- a/examples/01_basic/08_simulate_force.py +++ b/examples/01_basic/08_simulate_force.py @@ -6,7 +6,7 @@ produced by the muscle. **MyoGen** provides a comprehensive force model based on the classic Fuglevand et al. (1993) approach. -.. note:: +!!! note The **force model** converts spike trains into force by simulating individual motor unit twitches and their summation. Each motor unit has unique twitch characteristics (amplitude, duration) that depend on its recruitment threshold. @@ -20,9 +20,8 @@ References ---------- -.. [1] Fuglevand, A. J., Winter, D. A., & Patla, A. E. (1993). - Models of recruitment and rate coding in motor-unit pools. - Journal of Neurophysiology, 70(2), 782-797. + +[1] Fuglevand, A. J., Winter, D. A., & Patla, A. E. (1993). Models of recruitment and rate coding in motor-unit pools. Journal of Neurophysiology, 70(2), 782-797. """ ############################################################################## @@ -244,3 +243,5 @@ plt.tight_layout() plt.show() + +# mkdocs_gallery_thumbnail_path = "gallery_thumbs/08_simulate_force.png" diff --git a/examples/01_basic/09_simulate_intramuscular_emg.py b/examples/01_basic/09_simulate_intramuscular_emg.py index 12f81ed6..17182c91 100644 --- a/examples/01_basic/09_simulate_intramuscular_emg.py +++ b/examples/01_basic/09_simulate_intramuscular_emg.py @@ -6,7 +6,7 @@ needle electrodes. It shows the complete pipeline from muscle model creation to EMG signal generation with realistic noise and motor unit detectability. -.. note:: +!!! note **Intramuscular EMG** (iEMG) is recorded using needle electrodes inserted directly into the muscle tissue. This provides high spatial resolution and allows for the detection of individual motor unit action potentials @@ -185,10 +185,10 @@ # # Compare the **intramuscular EMG** signal with the input current. # -# .. note:: -# In this example, only **three motor units** (small, medium, large) contribute -# to the EMG signal. This demonstrates how individual MUAPs sum to create the -# composite EMG signal, and how different motor unit sizes affect signal amplitude. +# !!! note +# In this example, only **three motor units** (small, medium, large) contribute +# to the EMG signal. This demonstrates how individual MUAPs sum to create the +# composite EMG signal, and how different motor unit sizes affect signal amplitude. plt.figure(figsize=(12, 6)) @@ -216,3 +216,5 @@ plt.tight_layout() plt.show() + +# mkdocs_gallery_thumbnail_path = "gallery_thumbs/09_simulate_intramuscular_emg.png" diff --git a/examples/01_basic/11_simulate_spinal_network.py b/examples/01_basic/11_simulate_spinal_network.py index b8c5b49a..c903a6ef 100644 --- a/examples/01_basic/11_simulate_spinal_network.py +++ b/examples/01_basic/11_simulate_spinal_network.py @@ -1,6 +1,6 @@ """ -Spinal Network Simulation with Systematic Tendon Tap Protocol -============================================================== +Spinal Network & Tendon Tap +=========================== This example demonstrates **complete spinal reflex network modeling** with a **comprehensive tendon tap protocol** that systematically varies mechanical perturbations, fusimotor drive, and cortical activity. @@ -46,7 +46,7 @@ - Repeat gamma drive pattern (0→25→50→75→100 pps) - Sinusoidal cortical drive (38±1 Hz at 1 Hz) recruiting ~75% of motor neurons -.. note:: +!!! note This example builds upon all previous examples and demonstrates how the complete neuromuscular system functions as an integrated network: @@ -57,7 +57,7 @@ - **Joint dynamics**: Closed-loop biomechanical control with realistic inertia and damping - **Descending drive**: Cortical control signals for voluntary movement (from example 01) -.. important:: +!!! important **Spinal Reflex Networks** are the fundamental control circuits that coordinate muscle activity. Key physiological concepts demonstrated: @@ -196,7 +196,7 @@ # Descending drive (cortical input) nDD = 400 # Total descending drive neurons (increased for more drive) DDorder = ( - 5 # Poisson process order for realistic spike patterns (higher order = more regular spiking) + 5 # Gamma shape parameter for realistic spike patterns (higher shape = more regular spiking, CV=1/sqrt(shape)) ) print("Neural population sizes:") @@ -327,7 +327,7 @@ aMN = AlphaMN__Pool(recruitment_thresholds__array=recruitment_thresholds) # α-motoneurons # Create descending drive population -DD = DescendingDrive__Pool(n=nDD, poisson_batch_size=DDorder, timestep__ms=dt) +DD = DescendingDrive__Pool(n=nDD, process_type="gamma", shape=DDorder, timestep__ms=dt) # Create afferent populations Ia = AffIa__Pool(n=nIa, timestep__ms=dt) # Primary spindle afferents @@ -970,3 +970,5 @@ def step_callback(step_counter): plt.tight_layout() plt.savefig(save_path / "gto_dynamics.png", dpi=150, bbox_inches="tight") plt.show() + +# mkdocs_gallery_thumbnail_path = "gallery_thumbs/11_simulate_spinal_network.png" diff --git a/examples/01_basic/12_extract_data_from_neo_blocks.py b/examples/01_basic/12_extract_data_from_neo_blocks.py index 8212c0cc..e2b24376 100644 --- a/examples/01_basic/12_extract_data_from_neo_blocks.py +++ b/examples/01_basic/12_extract_data_from_neo_blocks.py @@ -5,11 +5,11 @@ This example demonstrates how to extract and work with data from the different Neo Block types used in MyoGen: -- SPIKE_TRAIN__Block -- SURFACE_MUAP__Block -- SURFACE_EMG__Block -- INTRAMUSCULAR_MUAP__Block -- INTRAMUSCULAR_EMG__Block +- [`SPIKE_TRAIN__Block`][myogen.utils.types.SPIKE_TRAIN__Block] +- [`SURFACE_MUAP__Block`][myogen.utils.types.SURFACE_MUAP__Block] +- [`SURFACE_EMG__Block`][myogen.utils.types.SURFACE_EMG__Block] +- [`INTRAMUSCULAR_MUAP__Block`][myogen.utils.types.INTRAMUSCULAR_MUAP__Block] +- [`INTRAMUSCULAR_EMG__Block`][myogen.utils.types.INTRAMUSCULAR_EMG__Block] Each section shows practical code for accessing and analyzing the data. """ @@ -433,3 +433,5 @@ def _mean_firing_rate(spiketrain): plt.tight_layout() plt.savefig(save_path / "intramuscular_emg_analysis.png", dpi=150) print(f"\nSaved EMG analysis to: {save_path / 'intramuscular_emg_analysis.png'}") + +# mkdocs_gallery_thumbnail_path = "gallery_thumbs/12_extract_data_from_neo_blocks.png" diff --git a/examples/01_basic/13_load_and_inspect_nwb_data.py b/examples/01_basic/13_load_and_inspect_nwb_data.py index 618ccfcf..2ef9c606 100644 --- a/examples/01_basic/13_load_and_inspect_nwb_data.py +++ b/examples/01_basic/13_load_and_inspect_nwb_data.py @@ -25,7 +25,7 @@ This example requires an NWB file from example 11 (``spinal_network_results.nwb``). -.. note:: +!!! note If you haven't run example 11, this example will create a minimal NWB file for demonstration purposes. """ @@ -486,3 +486,5 @@ def get_timestamps(data): print(" 1. Run example 11 to generate full simulation NWB file") print(" 2. Explore your data with NWB Widgets: pip install nwbwidgets") print(" 3. Upload to DANDI Archive for sharing: https://dandiarchive.org/") + +# mkdocs_gallery_thumbnail_path = "gallery_thumbs/13_load_and_inspect_nwb_data.png" diff --git a/examples/01_basic/14_calibrate_noise_from_real.py b/examples/01_basic/14_calibrate_noise_from_real.py index 88a49690..47079140 100644 --- a/examples/01_basic/14_calibrate_noise_from_real.py +++ b/examples/01_basic/14_calibrate_noise_from_real.py @@ -3,7 +3,7 @@ ================================================= This example takes a real iEMG recording, derives a device-matched -noise profile with :func:`myogen.utils.calibrate_realistic_noise_profile`, +noise profile with `myogen.utils.calibrate_realistic_noise_profile`, then generates simulated noise with the derived parameters and overlays the real and simulated PSDs in a single figure so the match is visually verifiable. @@ -21,10 +21,10 @@ iemg.add_noise(snr__dB=20.0, noise_type="realistic", **profile) -.. note:: - This example expects a MAT v7.3 file with an ``EMGSIGNAL`` struct - (``data`` shape ``(n_channels, n_samples)``, ``rate`` in Hz) and a - ``target_signal`` (drive envelope, ``0`` = rest). Adjust the +!!! note + This example expects a MAT v7.3 file with an `EMGSIGNAL` struct + (`data` shape `(n_channels, n_samples)`, `rate` in Hz) and a + `target_signal` (drive envelope, `0` = rest). Adjust the loader for other formats — only the µV array + sample rate + optional rest mask actually feed the calibration. """ diff --git a/examples/01_basic/15_noise_parameter_sweep.py b/examples/01_basic/15_noise_parameter_sweep.py index e4d4e69c..f9dba279 100644 --- a/examples/01_basic/15_noise_parameter_sweep.py +++ b/examples/01_basic/15_noise_parameter_sweep.py @@ -744,3 +744,5 @@ def _xlabel(sax): fig.savefig(save_path / "noise_parameter_sweep.svg", transparent=True) fig.savefig(save_path / "noise_parameter_sweep.pdf", transparent=True) plt.show() + +# mkdocs_gallery_thumbnail_path = "gallery_thumbs/15_noise_parameter_sweep.png" diff --git a/examples/01_basic/README.md b/examples/01_basic/README.md new file mode 100644 index 00000000..2b269a6f --- /dev/null +++ b/examples/01_basic/README.md @@ -0,0 +1,16 @@ +# Basics + +This example gallery contains tutorials demonstrating core functionality and +workflow patterns. These examples cover the essential building blocks, from +motor-unit recruitment to EMG signal generation. + +!!! note + The gallery is source-only by default: examples are rendered from source but + are not executed during a normal documentation build. Thumbnails and + screenshots may reflect a prior verified run, so run an example locally to + reproduce its outputs. Maintainers can opt into execution with + `MKDOCS_GALLERY_PLOT=true`. + +The examples are self-contained and can be run independently, though they may +depend on each other for context. Following the numbered order provides the +best learning experience. diff --git a/examples/01_basic/README.rst b/examples/01_basic/README.rst deleted file mode 100644 index 40008168..00000000 --- a/examples/01_basic/README.rst +++ /dev/null @@ -1,11 +0,0 @@ -.. _basic-examples: - -================== -Basics -================== - -| This example gallery contains tutorials demonstrating core functionality and workflow patterns. -| These examples cover the essential building blocks, from motor unit recruitment to EMG signal generation. -| -| The examples are self-contained and can be run independently, though they may depend on each other for context. -| Following the numbered order provides the best learning experience. diff --git a/examples/02_finetune/01_optimize_dd_for_target_firing_rate.py b/examples/02_finetune/01_optimize_dd_for_target_firing_rate.py index 8bd36b8a..0a921090 100644 --- a/examples/02_finetune/01_optimize_dd_for_target_firing_rate.py +++ b/examples/02_finetune/01_optimize_dd_for_target_firing_rate.py @@ -6,7 +6,7 @@ The goal is to find network parameters (number of DD neurons, connection probability, drive frequency) that produce motor neuron firing patterns matching target physiological characteristics. -.. note:: +!!! note **Multi-objective optimization** is essential for tuning complex neural network models because: - Multiple parameters interact non-linearly (DD neurons, connectivity, synaptic weights) @@ -14,13 +14,13 @@ - Manual parameter tuning is time-consuming and may miss optimal combinations - Systematic search ensures reproducible, well-documented parameter choices -.. important:: +!!! important **Descending Drive (DD) Networks** represent cortical input to spinal motor neurons. Key parameters: - - ``dd_neurons``: Population size (affects input diversity and convergence) - - ``conn_probability``: Synaptic connectivity (affects drive strength and correlation) - - ``dd_drive__Hz``: Input frequency (controls baseline excitation level) - - ``gamma_shape``: Variability of Poisson processes (affects temporal patterns) + - `dd_neurons`: Population size (affects input diversity and convergence) + - `conn_probability`: Synaptic connectivity (affects drive strength and correlation) + - `dd_drive__Hz`: Input frequency (controls baseline excitation level) + - `gamma_shape`: Variability of Poisson processes (affects temporal patterns) **Optimization Objective**: Match target motor neuron firing rate statistics while maintaining biologically plausible network parameters. @@ -547,27 +547,29 @@ def trial_to_dict(t): # Using Optimized Parameters # --------------------------- # -# .. important:: -# The optimized parameters can be loaded and used in production simulations: +# !!! important +# The optimized parameters can be loaded and used in production simulations: # -# .. code-block:: python +# ```python +# import json +# from pathlib import Path # -# import json -# from pathlib import Path +# # Load optimized parameters +# with open("results/dd_optimization/dd_optimized_params.json") as f: +# params = json.load(f) # -# # Load optimized parameters -# with open("results/dd_optimization/dd_optimized_params.json") as f: -# params = json.load(f) +# # Extract best values +# dd_neurons = int(params["best_trial"]["dd_neurons"]) +# conn_prob = params["best_trial"]["conn_probability"] +# dd_drive_Hz = params["best_trial"]["dd_drive__Hz"] # -# # Extract best values -# dd_neurons = int(params["best_trial"]["dd_neurons"]) -# conn_prob = params["best_trial"]["conn_probability"] -# dd_drive_Hz = params["best_trial"]["dd_drive__Hz"] -# -# # Use in your simulation -# descending_drive_pool = DescendingDrive__Pool(n=dd_neurons, ...) -# network.connect("DD", "aMN", probability=conn_prob, ...) +# # Use in your simulation +# descending_drive_pool = DescendingDrive__Pool(n=dd_neurons, ...) +# network.connect("DD", "aMN", probability=conn_prob, ...) +# ``` print("\n[DONE] Optimization complete!") print(f"Best parameters saved to: {json_path}") print(f"Full study saved to: {RESULTS_DIR / 'study.pkl'}") + +# mkdocs_gallery_thumbnail_path = "gallery_thumbs/01_optimize_dd_for_target_firing_rate.png" diff --git a/examples/02_finetune/02_compute_force_from_optimized_dd.py b/examples/02_finetune/02_compute_force_from_optimized_dd.py index 2afaf31c..435810e7 100644 --- a/examples/02_finetune/02_compute_force_from_optimized_dd.py +++ b/examples/02_finetune/02_compute_force_from_optimized_dd.py @@ -6,7 +6,7 @@ previous optimization study. It shows how to load saved parameters, run a simulation with those settings, and generate realistic force output from motor neuron spike trains. -.. note:: +!!! note **Force generation workflow**: 1. Load optimized DD parameters (neurons, connectivity, drive frequency) @@ -15,11 +15,11 @@ 4. Convert spike trains to muscle force using twitch dynamics 5. Validate force characteristics (mean, variability, smoothness) -.. important:: +!!! important **Prerequisites**: This example requires results from the optimization study: - - Run ``01_optimize_dd_for_target_firing_rate.py`` first - - Generates ``dd_optimized_params.json`` in ``results/dd_optimization/`` + - Run `01_optimize_dd_for_target_firing_rate.py` first + - Generates `dd_optimized_params.json` in `results/dd_optimization/` - Contains best network parameters for target firing rate **Force Model**: Uses physiologically accurate motor unit twitch dynamics with: @@ -294,13 +294,13 @@ # # Convert spike trains to muscle force using motor unit twitch dynamics. # -# .. note:: -# The **ForceModel** implements: +# !!! note +# The **ForceModel** implements: # -# - Size-dependent twitch rise times (Henneman's principle) -# - Nonlinear force summation during repetitive activation -# - Realistic contraction-relaxation dynamics -# - Physiologically validated parameter ranges +# - Size-dependent twitch rise times (Henneman's principle) +# - Nonlinear force summation during repetitive activation +# - Realistic contraction-relaxation dynamics +# - Physiologically validated parameter ranges force_model = ForceModel( recruitment_thresholds=recruitment_thresholds, @@ -437,3 +437,5 @@ plt.tight_layout() plt.savefig(OUTPUT_DIR / "force_validation.png", dpi=150, bbox_inches="tight") plt.show() + +# mkdocs_gallery_thumbnail_path = "gallery_thumbs/02_compute_force_from_optimized_dd.png" diff --git a/examples/02_finetune/03_optimize_dd_for_target_force.py b/examples/02_finetune/03_optimize_dd_for_target_force.py index 587a5e97..0b02e200 100644 --- a/examples/02_finetune/03_optimize_dd_for_target_force.py +++ b/examples/02_finetune/03_optimize_dd_for_target_force.py @@ -6,7 +6,7 @@ to achieve specific muscle force outputs. Unlike firing rate optimization (example 01), this approach targets biomechanically relevant force production. -.. note:: +!!! note **Force-based optimization workflow**: 1. Load baseline force from previous optimization (example 01) @@ -14,11 +14,11 @@ 3. Optimize DD drive frequency to match target force 4. Keep network structure fixed (neurons, connectivity from baseline) -.. important:: +!!! important **Prerequisites**: This example requires results from previous examples: - - Run ``01_optimize_dd_for_target_firing_rate.py`` first - - Run ``02_compute_force_from_optimized_dd.py`` second + - Run `01_optimize_dd_for_target_firing_rate.py` first + - Run `02_compute_force_from_optimized_dd.py` second - Generates baseline force measurements needed here **Optimization Strategy**: We optimize only the DD drive frequency while keeping the network @@ -513,3 +513,5 @@ def objective(trial): bbox_inches="tight", ) plt.show() + +# mkdocs_gallery_thumbnail_path = "gallery_thumbs/03_optimize_dd_for_target_force.png" diff --git a/examples/02_finetune/04_extract_isi_and_cv_per_ramps.py b/examples/02_finetune/04_extract_isi_and_cv_per_ramps.py index 7807b325..9a0d51f6 100644 --- a/examples/02_finetune/04_extract_isi_and_cv_per_ramps.py +++ b/examples/02_finetune/04_extract_isi_and_cv_per_ramps.py @@ -6,7 +6,7 @@ from previous optimization studies. It extracts inter-spike interval (ISI) and coefficient of variation (CV) statistics during realistic trapezoidal contraction patterns. -.. note:: +!!! note **Analysis workflow**: 1. Load optimized DD parameters from baseline or force-specific optimization @@ -15,11 +15,11 @@ 4. Extract ISI/CV statistics during plateau phase 5. Visualize firing patterns and instantaneous discharge rates -.. important:: +!!! important **Prerequisites**: This example requires results from previous optimizations: - - **Baseline mode**: Run ``01_optimize_dd_for_target_firing_rate.py`` first - - **Force mode**: Run ``02_optimize_dd_for_target_force.py`` for specific MVC level + - **Baseline mode**: Run `01_optimize_dd_for_target_firing_rate.py` first + - **Force mode**: Run `02_optimize_dd_for_target_force.py` for specific MVC level - Loads network structure and drive parameters from saved results **Trapezoidal Contraction**: Simulates realistic voluntary isometric contractions with: @@ -466,7 +466,7 @@ def _mean_firing_rate(spiketrain): [ _mean_firing_rate(st__s.time_slice(st__s.min(), st__s.max())) for st__s in dd_segment.spiketrains - if len(st__s) > 0 + if len(st__s) > 1 # need >=2 spikes for a rate over the spike span ] ) @@ -475,7 +475,7 @@ def _mean_firing_rate(spiketrain): [ _mean_firing_rate(st__s.time_slice(st__s.min(), st__s.max())) for st__s in mn_segment.spiketrains - if len(st__s) > 0 + if len(st__s) > 1 # need >=2 spikes for a rate over the spike span ] ) @@ -545,7 +545,7 @@ def _mean_firing_rate(spiketrain): axes[0].grid(True, alpha=0.3) # 2. Motor neuron raster plot (recruitment ordered) -mn_colors = plt.cm.get_cmap("Reds")(np.linspace(0.3, 0.9, len(mn_segment.spiketrains))) +mn_colors = plt.get_cmap("Reds")(np.linspace(0.3, 0.9, len(mn_segment.spiketrains))) active_mn_count = 0 for i, (spiketrain, color) in enumerate(zip(mn_segment.spiketrains, mn_colors)): if len(spiketrain) > 0: @@ -642,7 +642,7 @@ def _mean_firing_rate(spiketrain): n_to_plot = len(active_neuron_ids) # Use colormap for lines (gradient showing recruitment order) - colors = plt.cm.get_cmap("rainbow")(np.linspace(0, 1, n_to_plot)) + colors = plt.get_cmap("rainbow")(np.linspace(0, 1, n_to_plot)) for neuron_idx in range(n_to_plot): axes2[1].plot( @@ -671,3 +671,5 @@ def _mean_firing_rate(spiketrain): plt.show() print(f"\nSaved discharge rate plot to: {discharge_plot_path}") + +# mkdocs_gallery_thumbnail_path = "gallery_thumbs/04_extract_isi_and_cv_per_ramps.png" diff --git a/examples/02_finetune/05_plot_isi_cv_multi_muscle_comparison.py b/examples/02_finetune/05_plot_isi_cv_multi_muscle_comparison.py index b1c8c128..b62efde4 100644 --- a/examples/02_finetune/05_plot_isi_cv_multi_muscle_comparison.py +++ b/examples/02_finetune/05_plot_isi_cv_multi_muscle_comparison.py @@ -7,7 +7,7 @@ publication-quality visualizations comparing simulated motor unit firing patterns against experimental data. -.. note:: +!!! note **Analysis workflow**: 1. Auto-detect available force levels for each muscle type @@ -16,12 +16,12 @@ 4. Create multi-muscle comparison plot with color-coded muscles and force levels 5. Generate summary statistics for all conditions -.. important:: +!!! important **Prerequisites**: This example requires ISI/CV data files: - - Run ``03_extract_isi_and_cv_per_ramps.py`` for each muscle/force combination - - Generates CSV files: ``{prefix}isi_cv_data_{muscle}_{force}.csv`` - - Optional: ``ISI_statistics.csv`` for experimental data overlay + - Run `03_extract_isi_and_cv_per_ramps.py` for each muscle/force combination + - Generates CSV files: `{prefix}isi_cv_data_{muscle}_{force}.csv` + - Optional: `ISI_statistics.csv` for experimental data overlay **Visualization Strategy**: Creates a comprehensive comparison showing: @@ -807,3 +807,5 @@ def plot_cv_vs_fr_per_muscle(all_muscle_data, exp_data, muscles=("VL", "VM", "FD print(f"Total motor units plotted: {total_motor_units}") print(f"Total muscle types: {len(all_muscle_data)}") print("=" * 80) + +# mkdocs_gallery_thumbnail_path = "gallery_thumbs/05_plot_isi_cv_multi_muscle_comparison.png" diff --git a/examples/02_finetune/README.md b/examples/02_finetune/README.md new file mode 100644 index 00000000..a232c963 --- /dev/null +++ b/examples/02_finetune/README.md @@ -0,0 +1,11 @@ +# Step-by-step reproduction + +This example gallery contains tutorials for fine-tuning descending-drive +parameters to match target firing rates or force levels. Examples 01–04 form a +sequential chain and must be run in numbered order, since each example loads +results saved by its predecessor. + +- **01** optimises the descending drive to reproduce a target motor-unit firing rate. +- **02** uses those optimised parameters to compute the resulting muscle force. +- **03** re-optimises the drive to hit a specific force target (e.g. 30 % MVC). +- **04** extracts inter-spike intervals and their coefficient of variation across contraction ramps. diff --git a/examples/02_finetune/README.rst b/examples/02_finetune/README.rst deleted file mode 100644 index 72a03e15..00000000 --- a/examples/02_finetune/README.rst +++ /dev/null @@ -1,13 +0,0 @@ -.. _finetune-examples: - -Step-by-step reproduction -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -| This example gallery contains tutorials for fine-tuning descending drive parameters to match -| target firing rates or force levels. Examples 01–04 form a sequential chain and must be run -| in numbered order, since each example loads results saved by its predecessor. -| -| **01** optimises the descending drive to reproduce a target motor-unit firing rate. -| **02** uses those optimised parameters to compute the resulting muscle force. -| **03** re-optimises the drive to hit a specific force target (e.g. 30 % MVC). -| **04** extracts inter-spike intervals and their coefficient of variation across contraction ramps. diff --git a/examples/03_papers/README.rst b/examples/03_papers/README.rst deleted file mode 100644 index 9fdab0a9..00000000 --- a/examples/03_papers/README.rst +++ /dev/null @@ -1,57 +0,0 @@ -.. _paper-reproductions: - -==================== -Paper Reproductions -==================== - -This gallery contains complete reproductions of published neuromuscular modeling studies using MyoGen. - -Each reproduction demonstrates MyoGen's ability to replicate established research findings and provides validated reference implementations for advanced modeling techniques. - -Purpose -------- - -These examples serve multiple purposes: - -- **Validation**: Demonstrate that MyoGen can reproduce published results -- **Reference**: Provide working implementations of established methods -- **Education**: Show complete workflows from simulation to publication-quality figures -- **Benchmarking**: Establish performance baselines for comparison - -Structure ---------- - -Each paper reproduction is organized into separate scripts: - -1. **Simulation** - Generate spike trains and neural activity -2. **Analysis** - Compute derived signals (force, coherence, etc.) -3. **Visualization** - Reproduce paper figures - -This modular structure allows you to: - -- Re-run expensive simulations independently -- Quickly iterate on visualizations -- Modify analysis parameters without full re-simulation - -Available Reproductions ------------------------ - -Currently available paper reproductions: - -.. card:: Watanabe & Kohn (2015) - :link: watanabe-reproduction - :link-type: ref - - **Fast Oscillatory Commands from the Motor Cortex Can Be Decoded by the Spinal Cord for Force Control** - - J. Neurosci. 35(40):13687-13697. `DOI: 10.1523/JNEUROSCI.1950-15.2015 `_ - - Demonstrates how spinal motor neuron pools decode frequency oscillations from descending cortical drive for force modulation. - - **MyoGen Components**: ``AlphaMN__Pool``, ``DescendingDrive__Pool``, ``Network``, ``ForceModel``, ``SimulationRunner``, ``ContinuousSaver`` - - **Workflow**: 6-step pipeline from baseline force → optimization → simulation → analysis → force computation → visualization - - See :ref:`watanabe-reproduction` for complete documentation. - -More reproductions will be added over time as MyoGen capabilities expand. diff --git a/examples/03_papers/watanabe/01_compute_baseline_force.py b/examples/03_papers/watanabe/01_compute_baseline_force.py index 3fe2e72a..11886c41 100644 --- a/examples/03_papers/watanabe/01_compute_baseline_force.py +++ b/examples/03_papers/watanabe/01_compute_baseline_force.py @@ -5,7 +5,7 @@ This example computes the reference muscle force using constant descending drive. This force is used as the target for optimizing the oscillating drive DC offset in Phase 3. -.. note:: +!!! note **Purpose**: Establish force baseline using Watanabe network parameters - 800 motor neurons @@ -20,24 +20,24 @@ **MyoGen Components Used**: -- :class:`~myogen.simulator.neuron.populations.AlphaMN__Pool`: +- [`AlphaMN__Pool`][myogen.simulator.neuron.populations.AlphaMN__Pool]: Pool of 800 alpha motor neurons with exponentially distributed recruitment thresholds. Each neuron is a biophysically detailed NEURON model with soma, dendrites, and ion channels. -- :class:`~myogen.simulator.neuron.populations.DescendingDrive__Pool`: +- [`DescendingDrive__Pool`][myogen.simulator.neuron.populations.DescendingDrive__Pool]: Pool of 400 "descending drive" neurons that generate Poisson spike trains. These represent cortical/brainstem input to motor neurons. - ``poisson_batch_size=1`` creates order-1 (renewal) Poisson processes. + ``process_type="poisson"`` generates true Poisson processes (exponential ISIs, CV=1). -- :class:`~myogen.simulator.Network`: +- [`Network`][myogen.simulator.Network]: Container that manages populations and synaptic connections between them. Supports probabilistic connectivity (``connect()``) and external input (``connect_from_external()``). -- :class:`~myogen.simulator.RecruitmentThresholds`: +- [`RecruitmentThresholds`][myogen.simulator.RecruitmentThresholds]: Generates recruitment threshold distribution following Fuglevand/DeLuca models. The ``mode="combined"`` blends exponential and linear distributions. -- :class:`~myogen.simulator.core.force.force_model.ForceModel`: +- [`ForceModel`][myogen.simulator.ForceModel]: Converts motor neuron spike trains to muscle force using Fuglevand's twitch model. Each motor unit has amplitude and contraction time determined by recruitment threshold. @@ -172,14 +172,12 @@ # at a specified rate. They don't have membrane dynamics - just Poisson processes. # # Parameters: -# - process_type="poisson": Renewal Poisson process -# - poisson_batch_size=1: Order-1 (memoryless) - Watanabe specification -# Higher values create more regular spike trains (order-k gamma process) +# - process_type="poisson": true Poisson process (exponential ISIs, CV=1) - Watanabe specification +# For regular, low-CV firing use process_type="gamma", shape=k instead. descending_drive_pool = DescendingDrive__Pool( n=N_DD_NEURONS, timestep__ms=TIMESTEP_MS * pq.ms, process_type="poisson", - poisson_batch_size=1, # Order 1 Poisson (Watanabe specification) ) ############################################################################## @@ -230,9 +228,13 @@ # ---------------------------------------- time_points = int(SIMULATION_TIME_MS / TIMESTEP_MS) -drive_signal = np.ones(time_points) * DD_DRIVE_HZ + np.clip( - get_random_generator().normal(0, 1.0, size=time_points), 0, None +# Zero-mean noise, then clip the total to non-negative firing rates. (One-sided +# clipped noise adds a systematic ~0.4 Hz bias, and must match the optimization +# trials in _oscillating_dc_helpers.py so the reference and trials are comparable.) +drive_signal = np.ones(time_points) * DD_DRIVE_HZ + get_random_generator().normal( + 0, 1.0, size=time_points ) +drive_signal = np.clip(drive_signal, 0, None) ############################################################################## # Run Simulation @@ -324,10 +326,12 @@ force_raw = force_output.magnitude[:, 0] # Arbitrary units (sum of MU twitches) force_time = force_output.times.rescale(pq.s).magnitude -# Normalize force to 0-1 range, then scale to Newtons -# (ForceModel outputs sum of MU twitch forces, not normalized values) +# %MVC normalization (Watanabe methodology): normalize to the peak, then scale to +# MAX_FORCE_N. This is the reference the optimization (script 02) matches, and it +# normalizes trials the same way, so the comparison is of normalized force +# *profiles*, not absolute Newtons. force_max_raw = np.max(force_raw) -force_signal = (force_raw / force_max_raw) * MAX_FORCE_N # Scale to Newtons +force_signal = (force_raw / force_max_raw) * MAX_FORCE_N # peak-normalized (%MVC), not absolute N ############################################################################## # Analyze Force Characteristics @@ -360,7 +364,6 @@ "dd_drive__Hz": DD_DRIVE_HZ, "synaptic_weight__uS": SYNAPTIC_WEIGHT, "process_type": "poisson", - "poisson_batch_size": 1, }, "force_scaling": { "max_force__N": MAX_FORCE_N, @@ -458,3 +461,5 @@ plt.tight_layout() plt.savefig(RESULTS_DIR / "force_reference.png", dpi=150, bbox_inches="tight") plt.show() + +# mkdocs_gallery_thumbnail_path = "gallery_thumbs/01_compute_baseline_force.png" diff --git a/examples/03_papers/watanabe/02_optimize_oscillating_dc.py b/examples/03_papers/watanabe/02_optimize_oscillating_dc.py index 6197a600..66930bfa 100644 --- a/examples/03_papers/watanabe/02_optimize_oscillating_dc.py +++ b/examples/03_papers/watanabe/02_optimize_oscillating_dc.py @@ -9,32 +9,32 @@ This finds the equivalent of the original 58 pps value (Phase 3) that produces the same force as constant drive (Phase 1) when combined with 20 Hz oscillation. -.. note:: +!!! note **Purpose**: Find DC offset for Phase 3 that matches Phase 1 force - Reference: Constant drive force from script 01 (configurable, default 40 Hz) - Optimize: DC offset + 20 Hz oscillation to match that force - Result: DC offset < constant drive (oscillation adds activation) -.. important:: - **Prerequisites**: Run ``01_compute_baseline_force.py`` first +!!! important + **Prerequisites**: Run `01_compute_baseline_force.py` first **Output**: ``results/watanabe_optimization/dc_offset_optimized.json`` **MyoGen Components Used**: -- :class:`~myogen.simulator.neuron.populations.AlphaMN__Pool`: +- [`AlphaMN__Pool`][myogen.simulator.neuron.populations.AlphaMN__Pool]: Same motor neuron pool as script 01. Created fresh for each optimization trial to ensure independent network realizations. -- :class:`~myogen.simulator.neuron.populations.DescendingDrive__Pool`: +- [`DescendingDrive__Pool`][myogen.simulator.neuron.populations.DescendingDrive__Pool]: Poisson spike generators driven by time-varying rate: ``DC_offset + amplitude*sin(2πft)``. The ``integrate()`` method advances the internal Poisson process by one timestep. -- :class:`~myogen.simulator.Network`: +- [`Network`][myogen.simulator.Network]: Rebuilds the network for each trial with same connectivity parameters (30%). -- :class:`~myogen.simulator.core.force.force_model.ForceModel`: +- [`ForceModel`][myogen.simulator.ForceModel]: Computes steady-state force from spike trains to evaluate objective function. - **External dependency**: ``optuna`` for Bayesian optimization (TPE sampler). @@ -56,92 +56,41 @@ import json import os - -os.environ["MPLBACKEND"] = "Agg" -if "DISPLAY" in os.environ: - del os.environ["DISPLAY"] - -import warnings +import subprocess +import sys from pathlib import Path -import joblib import matplotlib.pyplot as plt -import numpy as np import optuna -import quantities as pq -from neo import Block, Segment, SpikeTrain -from neuron import h - -from myogen import get_random_generator, set_random_seed -from myogen.simulator import RecruitmentThresholds -from myogen.simulator.core.force.force_model import ForceModel -from myogen.simulator.neuron import Network -from myogen.simulator.neuron.populations import AlphaMN__Pool, DescendingDrive__Pool -from myogen.utils.helper import calculate_firing_rate_statistics -from myogen.utils.nmodl import load_nmodl_mechanisms - -warnings.filterwarnings("ignore") -plt.style.use("fivethirtyeight") - -############################################################################## -# Configuration -# ------------- - -# Simulation parameters -SIMULATION_TIME_MS = 5000.0 # Shorter simulation for efficient optimization -TIMESTEP_MS = 0.1 -N_MOTOR_UNITS = 800 # Watanabe specification - -# Force model parameters -RECORDING_FREQUENCY__HZ = 2048 -LONGEST_DURATION_RISE_TIME__MS = 90.0 -CONTRACTION_TIME_RANGE = 3 - -# Oscillation parameters (Watanabe specification) -OSC_FREQUENCY__HZ = 20.0 # 20 Hz physiological tremor -OSC_AMPLITUDE__HZ = 20.0 # Amplitude of oscillation -# Optimization settings -N_TRIALS = 25 # Increase for production -TIMEOUT_SECONDS = 3600 - -# Network parameters (Watanabe specification) -N_DD_NEURONS = 400 -DD_CONNECTIVITY = 0.3 -SYNAPTIC_WEIGHT = 0.05 +sys.path.insert(0, str(Path(__file__).parent)) +from _oscillating_dc_helpers import ( # noqa: E402 + DD_CONNECTIVITY, + MAX_FORCE_N, + N_DD_NEURONS, + N_MOTOR_UNITS, + N_TRIALS, + OSC_AMPLITUDE__HZ, + OSC_FREQUENCY__HZ, + REFERENCE_DRIVE__HZ, + REFERENCE_FORCE__N, + RESULTS_DIR, + STUDY_NAME, + SYNAPTIC_WEIGHT, + TARGET_FORCE__N, + make_storage, + objective, + recruitment_thresholds, +) -# Directories -try: - _script_dir = Path(__file__).parent -except NameError: - _script_dir = Path.cwd() -BASELINE_DIR = _script_dir / "results" / "watanabe_optimization" -RESULTS_DIR = _script_dir / "results" / "watanabe_optimization" -RESULTS_DIR.mkdir(exist_ok=True, parents=True) +plt.style.use("fivethirtyeight") ############################################################################## -# Load Reference Force Data -# -------------------------- +# Configuration Banner +# -------------------- print("\nLoading Reference Force (Constant Drive)") print("=" * 50) - -reference_file = BASELINE_DIR / "force_reference.json" - -if not reference_file.exists(): - raise FileNotFoundError( - f"Reference force not found: {reference_file}\nRun 01_compute_baseline_force.py first!" - ) - -with open(reference_file, "r") as f: - reference_results = json.load(f) - -# Extract reference force and scaling -REFERENCE_FORCE__N = reference_results["force"]["mean__N"] -REFERENCE_DRIVE__HZ = reference_results["network_parameters"]["dd_drive__Hz"] -TARGET_FORCE__N = REFERENCE_FORCE__N # Match the reference force -MAX_FORCE_N = reference_results["force_scaling"]["max_force__N"] - print(f"Reference force: {REFERENCE_FORCE__N:.2f} N ({REFERENCE_DRIVE__HZ:.1f} Hz constant)") print(f"Target force: {TARGET_FORCE__N:.2f} N (match with oscillation)") print(f"Force scaling: {MAX_FORCE_N:.0f} N maximum") @@ -151,223 +100,18 @@ print("=" * 50 + "\n") ############################################################################## -# Initialize NEURON -# ----------------- - -set_random_seed(42) -load_nmodl_mechanisms() -h.secondorder = 2 - -############################################################################## -# Define Simulation Function -# --------------------------- - - -def run_simulation_with_oscillating_drive(dc_offset, recruitment_thresholds): - """ - Run network simulation with oscillating drive and compute resulting force. - - Drive pattern: dc_offset + OSC_AMPLITUDE * sin(2*pi*OSC_FREQUENCY*t) - - Parameters - ---------- - dc_offset : float - DC offset component of oscillating drive (Hz) - recruitment_thresholds : np.ndarray - Motor unit recruitment thresholds - - Returns - ------- - tuple - (force_mean, n_active, fr_mean, fr_std) - """ - # Create motor neuron pool - motor_neuron_pool = AlphaMN__Pool( - recruitment_thresholds__array=recruitment_thresholds, - config_file="alpha_mn_default.yaml", - ) - - # Create descending drive pool (Poisson - Watanabe specification) - descending_drive_pool = DescendingDrive__Pool( - n=N_DD_NEURONS, - timestep__ms=TIMESTEP_MS * pq.ms, - process_type="poisson", - poisson_batch_size=1, # Order 1 Poisson (Watanabe) - ) - - # Build network - network = Network({"DD": descending_drive_pool, "aMN": motor_neuron_pool}) - network.connect( - source="DD", - target="aMN", - probability=DD_CONNECTIVITY, - weight__uS=SYNAPTIC_WEIGHT * pq.uS, - ) - network.connect_from_external(source="cortical_input", target="DD", weight__uS=1.0 * pq.uS) - dd_netcons = network.get_netcons("cortical_input", "DD") - - # Setup spike recording - mn_spike_recorders = [] - for cell in motor_neuron_pool: - spike_recorder = h.Vector() - nc = h.NetCon(cell.soma(0.5)._ref_v, None, sec=cell.soma) - nc.threshold = 50 - nc.record(spike_recorder) - mn_spike_recorders.append(spike_recorder) - - # Create oscillating drive signal: DC + amplitude * sin(2*pi*freq*t) - time_points = int(SIMULATION_TIME_MS / TIMESTEP_MS) - time_s = np.arange(time_points) * TIMESTEP_MS / 1000.0 - - # Oscillating component - drive_signal = dc_offset + OSC_AMPLITUDE__HZ * np.sin(2 * np.pi * OSC_FREQUENCY__HZ * time_s) - - # Clip to prevent negative firing rates - drive_signal = np.clip(drive_signal, 0, None) - - # Add small noise - drive_signal += np.clip(get_random_generator().normal(0, 1.0, size=time_points), 0, None) - - # Initialize simulation - h.load_file("stdrun.hoc") - h.dt = TIMESTEP_MS - h.tstop = SIMULATION_TIME_MS - - for section, voltage in zip(*motor_neuron_pool.get_initialization_data()): - section.v = voltage - for section, voltage in zip(*descending_drive_pool.get_initialization_data()): - section.v = voltage - - h.finitialize() - - # Run simulation - step_counter = 0 - while h.t < h.tstop: - current_drive = drive_signal[min(step_counter, len(drive_signal) - 1)] - for dd_cell in descending_drive_pool: - if dd_cell.integrate(current_drive): - if h.t < h.tstop: - dd_netcons[dd_cell.pool__ID].event(h.t + 1) - h.fadvance() - step_counter += 1 - - # Convert to Neo format - dt_s = h.dt / 1000.0 - mn_segment = Segment(name="Motor Neurons") - mn_segment.spiketrains = [ - SpikeTrain( - recorder.as_numpy() / 1000 * pq.s, - t_stop=SIMULATION_TIME_MS / 1000 * pq.s, - sampling_rate=(1 / dt_s * pq.Hz), - sampling_period=dt_s * pq.s, - name=f"MN_{i}", - ) - for i, recorder in enumerate(mn_spike_recorders) - ] - - spike_train__Block = Block(name="Motor Unit Pool") - spike_train__Block.segments = [mn_segment] - - # Calculate statistics - n_active = sum(1 for st in mn_segment.spiketrains if len(st) > 1) - stats = calculate_firing_rate_statistics(mn_segment.spiketrains) - fr_mean = float(stats["FR_mean"]) - fr_std = float(stats["FR_std"]) - - # Generate force - force_model = ForceModel( - recruitment_thresholds=recruitment_thresholds, - recording_frequency__Hz=RECORDING_FREQUENCY__HZ * pq.Hz, - longest_duration_rise_time__ms=LONGEST_DURATION_RISE_TIME__MS * pq.ms, - contraction_time_range_factor=CONTRACTION_TIME_RANGE, - ) - - force_output = force_model.generate_force(spike_train__Block=spike_train__Block) - force_raw = force_output.magnitude[:, 0] # Arbitrary units (sum of MU twitches) - - # Normalize force to 0-1 range, then scale to Newtons - # (ForceModel outputs sum of MU twitch forces, not normalized values) - force_max_raw = np.max(force_raw) - force_signal = (force_raw / force_max_raw) * MAX_FORCE_N # Scale to Newtons - - # Calculate steady-state force - steady_idx = len(force_signal) // 2 - force_mean = np.mean(force_signal[steady_idx:]) - - return force_mean, n_active, fr_mean, fr_std - - -############################################################################## -# Define Objective Function -# -------------------------- - -# Generate recruitment thresholds -recruitment_thresholds, _ = RecruitmentThresholds( - N=N_MOTOR_UNITS, - recruitment_range__ratio=100, - deluca__slope=5, - konstantin__max_threshold__ratio=1.0, - mode="combined", -) - - -def objective(trial): - """ - Optimize DC offset to match reference force with oscillation. - - Parameters - ---------- - trial : optuna.Trial - Optimization trial - - Returns - ------- - float - Relative force error (minimize) - """ - try: - # Optimize DC offset (will be lower than constant drive) - dc_offset = trial.suggest_float("dc_offset", 1.0, 100.0) - - # Run simulation with oscillating drive - force_mean, n_active, fr_mean, fr_std = run_simulation_with_oscillating_drive( - dc_offset, recruitment_thresholds - ) - - # Check minimum recruitment - if n_active < 10: # At least 1.25% of neurons - return 1000.0 + (10 - n_active) * 100.0 - - # Calculate relative error - force_error = abs(force_mean - TARGET_FORCE__N) / TARGET_FORCE__N - - # Store metadata - trial.set_user_attr("force_achieved", float(force_mean)) - trial.set_user_attr("force_error", float(force_error)) - trial.set_user_attr("n_active", n_active) - trial.set_user_attr("dc_offset__Hz", float(dc_offset)) - trial.set_user_attr("FR_mean", float(fr_mean)) - trial.set_user_attr("FR_std", float(fr_std)) - - if trial.number % 5 == 0: - print( - f"Trial {trial.number}: " - f"Force={force_mean:.2f}N (target={TARGET_FORCE__N:.2f}N), " - f"Error={force_error:.1%}, " - f"DC={dc_offset:.1f}Hz, " - f"Active={n_active}/{N_MOTOR_UNITS}" - ) - - return force_error - - except Exception as e: - print(f"Trial {trial.number} failed: {e}") - return 1000.0 - - -############################################################################## -# Run Optimization -# ---------------- +# Run Optimization (Parallel Workers) +# ------------------------------------ +# +# Each trial is a fully independent NEURON simulation (~80 s), so we fan out +# across worker *processes*. NEURON's ``h`` object is a process-global singleton +# and cannot be shared across threads. Workers are separate interpreter +# processes (``_optimize_dc_worker.py``) — they do not re-import this gallery +# file, avoiding the macOS ``spawn`` fork-bomb problem. All workers write to a +# single shared SQLite Optuna study. +# +# Override the worker count via ``MYOGEN_OPTUNA_WORKERS`` (e.g. ``=1`` for +# strictly sequential, or a small number on memory-constrained CI). print(f"\nOptimizing DC Offset to Match Reference Force (Oscillating Drive)") print("=" * 50) @@ -378,16 +122,40 @@ def objective(trial): colors = plt.rcParams["axes.prop_cycle"].by_key()["color"] -storage_name = f"sqlite:///{RESULTS_DIR}/optuna_oscillating_dc.db" +# Start each pipeline run from a clean study so a leftover SQLite DB from a +# previous run doesn't accumulate stale trials into best_trial. +try: + optuna.delete_study(study_name=STUDY_NAME, storage=make_storage()) +except KeyError: + pass + study = optuna.create_study( direction="minimize", - sampler=optuna.samplers.TPESampler(seed=42), - study_name="dc_offset_oscillating_match", - storage=storage_name, + sampler=optuna.samplers.TPESampler(seed=42), # workers set their own seeded samplers + study_name=STUDY_NAME, + storage=make_storage(), load_if_exists=True, ) -study.optimize(objective, n_trials=N_TRIALS, timeout=TIMEOUT_SECONDS, show_progress_bar=True) +# Conservative default: each worker builds a memory-heavy 800-MN + 400-DD NEURON +# network, so 8 concurrent workers can OOM a laptop/CI runner. Raise it via +# MYOGEN_OPTUNA_WORKERS on a machine with enough RAM for the full speedup. +n_workers = int(os.environ.get("MYOGEN_OPTUNA_WORKERS", min(4, N_TRIALS))) +n_workers = max(1, min(n_workers, N_TRIALS)) + +per_worker = [N_TRIALS // n_workers + (1 if i < N_TRIALS % n_workers else 0) for i in range(n_workers)] +worker_script = str(Path(__file__).parent / "_optimize_dc_worker.py") +print(f"Launching {n_workers} worker process(es): trial split {per_worker}") +procs = [ + subprocess.Popen([sys.executable, worker_script, "--n-trials", str(n), "--seed", str(42 + i)]) + for i, n in enumerate(per_worker) if n > 0 +] +exit_codes = [p.wait() for p in procs] +if any(code != 0 for code in exit_codes): + raise RuntimeError(f"Optuna worker(s) failed with exit codes {exit_codes}") + +# Reload to see all trials written by the workers +study = optuna.load_study(study_name=STUDY_NAME, storage=make_storage()) ############################################################################## # Analyze Results @@ -421,7 +189,6 @@ def objective(trial): "synaptic_weight__uS": SYNAPTIC_WEIGHT, "dc_offset__Hz": best_trial.user_attrs["dc_offset__Hz"], "process_type": "poisson", - "poisson_batch_size": 1, } results = { @@ -444,6 +211,7 @@ def objective(trial): with open(json_path, "w") as f: json.dump(results, f, indent=2) +import joblib joblib.dump(study, RESULTS_DIR / "study_oscillating_dc.pkl") print(f"\nSaved results: {json_path}") @@ -533,3 +301,5 @@ def objective(trial): print( f"This implementation: {REFERENCE_DRIVE__HZ:.1f} Hz constant → {best_trial.user_attrs['dc_offset__Hz']:.1f} Hz + oscillation (ratio: {best_trial.user_attrs['dc_offset__Hz'] / REFERENCE_DRIVE__HZ:.3f})" ) + +# mkdocs_gallery_thumbnail_path = "gallery_thumbs/02_optimize_oscillating_dc.png" diff --git a/examples/03_papers/watanabe/03_10pct_mvc_simulation.py b/examples/03_papers/watanabe/03_10pct_mvc_simulation.py index 534bd9e5..d2f151d4 100644 --- a/examples/03_papers/watanabe/03_10pct_mvc_simulation.py +++ b/examples/03_papers/watanabe/03_10pct_mvc_simulation.py @@ -7,18 +7,18 @@ - Constant + oscillation (Phase 2) - oscillation added to baseline - Optimized DC + oscillation (Phase 3) - DC offset matches Phase 1 force -.. note:: +!!! note **Simulation Phases** (5 seconds each): - **Phase 1**: Constant drive (baseline, e.g., 40-65 Hz) - **Phase 2**: Constant + 20*sin(20Hz) (oscillation added to baseline) - **Phase 3**: Optimized DC + 20*sin(20Hz) (DC offset matches Phase 1 force) -.. important:: +!!! important **Prerequisites**: - 1. ``01_compute_baseline_force.py`` - Compute reference force - 2. ``02_optimize_oscillating_dc.py`` - Optimize DC offset + 1. `01_compute_baseline_force.py` - Compute reference force + 2. `02_optimize_oscillating_dc.py` - Optimize DC offset **Scientific Context**: Demonstrates how shared descending drive creates motor unit synchronization, while independent noise reduces it. Phase 3 uses lower DC offset than @@ -26,11 +26,11 @@ **MyoGen Components Used**: -- :class:`~myogen.simulator.neuron.populations.AlphaMN__Pool`: +- [`AlphaMN__Pool`][myogen.simulator.neuron.populations.AlphaMN__Pool]: Pool of 800 motor neurons. Created from recruitment thresholds that determine each unit's excitability and force contribution. -- :class:`~myogen.simulator.neuron.populations.DescendingDrive__Pool`: +- [`DescendingDrive__Pool`][myogen.simulator.neuron.populations.DescendingDrive__Pool]: Two separate pools are used here: 1. **DD (Descending Drive)**: 400 neurons with *shared* input to motor neurons @@ -38,18 +38,18 @@ 2. **IN (Independent Noise)**: 800 neurons with *one-to-one* connectivity. This decorrelates motor neuron activity. Driven at constant 125 Hz. -- :class:`~myogen.simulator.neuron.Network`: +- [`Network`][myogen.simulator.Network]: Manages multiple populations and different connection patterns: - ``connect(prob=0.3)``: Random connectivity for shared drive - ``connect_one_to_one()``: Private inputs for independent noise - ``connect_from_external()``: External command signals to drive populations -- :class:`~myogen.simulator.neuron.simulation_runner.SimulationRunner`: +- [`SimulationRunner`][myogen.simulator.SimulationRunner]: High-level interface for running NEURON simulations with callbacks. The ``step_callback`` is called every timestep to update drives and record data. -- :class:`~myogen.utils.continuous_saver.ContinuousSaver`: +- [`ContinuousSaver`][myogen.utils.continuous_saver.ContinuousSaver]: Memory-efficient recording that saves data in chunks during long simulations. Essential for 15-second simulations with 800+ neurons. @@ -245,11 +245,11 @@ # Descending drive (DD) - SHARED input to motor neurons # These represent corticospinal neurons that project to multiple motor neurons. -# poisson_batch_size=1 creates renewal (order-1) Poisson processes. -# Higher batch sizes would create more regular spike trains (order-k gamma). +# process_type="poisson" generates true Poisson processes (exponential ISIs, CV=1). +# For regular, low-CV firing use process_type="gamma", shape=k instead. DD = DescendingDrive__Pool( n=nDD, - poisson_batch_size=1, # Order 1 Poisson (Watanabe specification) + process_type="poisson", timestep__ms=dt * pq.ms, ) @@ -258,7 +258,7 @@ # one motor neuron (one-to-one), providing uncorrelated background input. IN = DescendingDrive__Pool( n=nIN, - poisson_batch_size=1, # Order 1 Poisson (Watanabe specification) + process_type="poisson", timestep__ms=dt * pq.ms, ) @@ -516,3 +516,5 @@ def step_callback(step_counter): print(f"\nPhase 1 constant drive: {DD_DRIVE_CONSTANT:.2f} Hz") print(f"Phase 3 DC offset: {DC_OFFSET_OPTIMIZED:.2f} Hz (optimized to match Phase 1 force)") print(f"Ratio: {DC_OFFSET_OPTIMIZED / DD_DRIVE_CONSTANT:.3f} (Watanabe: {58 / 65:.3f})") + +# mkdocs_gallery_thumbnail_path = "gallery_thumbs/03_10pct_mvc_simulation.png" diff --git a/examples/03_papers/watanabe/04_load_and_analyze_results.py b/examples/03_papers/watanabe/04_load_and_analyze_results.py index b67fd970..a1e29a85 100644 --- a/examples/03_papers/watanabe/04_load_and_analyze_results.py +++ b/examples/03_papers/watanabe/04_load_and_analyze_results.py @@ -6,7 +6,7 @@ spinal network model. It shows how to load chunked simulation data, convert it to NEO format, and analyze membrane potentials, spike trains, and population dynamics. -.. note:: +!!! note **Analysis workflow**: 1. Load simulation parameters from previous run @@ -16,12 +16,12 @@ 5. Compute population firing rates across simulation phases 6. Save visualizations for publication -.. important:: +!!! important **Prerequisites**: This example requires simulation output from: - - Run ``03_10pct_mvc_simulation.py`` first - - Generates chunked data in ``results/watanabe_chunks/`` - - Creates simulation parameters in ``results/watanabe_simulation_params.pkl`` + - Run `03_10pct_mvc_simulation.py` first + - Generates chunked data in `results/watanabe_chunks/` + - Creates simulation parameters in `results/watanabe_simulation_params.pkl` **Key Features**: @@ -32,7 +32,7 @@ **MyoGen Components Used**: -- :func:`~myogen.utils.continuous_saver.convert_chunks_to_neo`: +- [`convert_chunks_to_neo`][myogen.utils.continuous_saver.convert_chunks_to_neo]: Converts chunked simulation data back into a standard NEO Block. This is the recommended way to load large simulation results. @@ -353,3 +353,5 @@ neo_output_path = chunks_path.parent / "watanabe_results_neo.pkl" joblib.dump(results, neo_output_path, compress=0) # No compression = faster print(f"(OK) NEO Block saved to: {neo_output_path}") + +# mkdocs_gallery_thumbnail_path = "gallery_thumbs/04_load_and_analyze_results.png" diff --git a/examples/03_papers/watanabe/05_compute_force_from_spinal_network.py b/examples/03_papers/watanabe/05_compute_force_from_spinal_network.py index 9a4689e3..e7750cf5 100644 --- a/examples/03_papers/watanabe/05_compute_force_from_spinal_network.py +++ b/examples/03_papers/watanabe/05_compute_force_from_spinal_network.py @@ -6,7 +6,7 @@ simulation. It loads pre-computed spike trains and converts them to realistic muscle force output using the Fuglevand force model with batched processing for memory efficiency. -.. note:: +!!! note **Force computation pipeline**: 1. Load spike train data from previous analysis (NEO Block format) @@ -16,11 +16,11 @@ 5. Accumulate force contributions from all motor units 6. Save force output as NEO Block with comprehensive metadata -.. important:: +!!! important **Prerequisites**: This example requires outputs from: - - ``03_10pct_mvc_simulation.py``: Simulation parameters - - ``04_load_and_analyze_results.py``: NEO-formatted spike train data (``watanabe_results_neo.pkl``) + - `03_10pct_mvc_simulation.py`: Simulation parameters + - `04_load_and_analyze_results.py`: NEO-formatted spike train data (`watanabe_results_neo.pkl`) **Key Features**: @@ -33,7 +33,7 @@ **MyoGen Components Used**: -- :class:`~myogen.simulator.core.force.force_model.ForceModel`: +- [`ForceModel`][myogen.simulator.ForceModel]: The Fuglevand force model converts spike trains to muscle force. Key parameters: - ``recruitment_thresholds``: Determines each motor unit's twitch amplitude @@ -45,7 +45,7 @@ The model generates a normalized twitch waveform for each motor unit, then convolves it with the spike train to produce force contributions. -- :class:`~myogen.simulator.RecruitmentThresholds`: +- [`RecruitmentThresholds`][myogen.simulator.RecruitmentThresholds]: Same thresholds used during simulation - ensures force model matches network. **How Force Generation Works**: diff --git a/examples/03_papers/watanabe/06_visualize.py b/examples/03_papers/watanabe/06_visualize.py index 69d581e9..87ae6e51 100644 --- a/examples/03_papers/watanabe/06_visualize.py +++ b/examples/03_papers/watanabe/06_visualize.py @@ -6,7 +6,7 @@ spinal network model reproduction. It creates comprehensive visualizations including coherence analysis, force timeseries, and detailed raster plots. -.. note:: +!!! note **Visualization pipeline**: 1. Load simulation parameters and results from previous scripts @@ -15,12 +15,12 @@ 4. Create full-duration raster plot with adaptive zoom insets 5. Save all figures in PDF format for publication -.. important:: +!!! important **Prerequisites**: This example requires outputs from: - - ``03_10pct_mvc_simulation.py``: Simulation parameters and results - - ``04_load_and_analyze_results.py``: NEO-formatted data blocks - - ``05_compute_force_from_spinal_network.py``: Force model output (``watanabe__force_results.pkl``) + - `03_10pct_mvc_simulation.py`: Simulation parameters and results + - `04_load_and_analyze_results.py`: NEO-formatted data blocks + - `05_compute_force_from_spinal_network.py`: Force model output (`watanabe__force_results.pkl`) **Key Features**: @@ -556,3 +556,5 @@ def compute_single_coherence(conv_sig): plt.tight_layout() plt.savefig(watanabe_plots_dir / "watanabe_raster_full_duration.pdf", dpi=300, bbox_inches="tight") plt.show() + +# mkdocs_gallery_thumbnail_path = "gallery_thumbs/06_visualize.png" diff --git a/examples/03_papers/watanabe/README.md b/examples/03_papers/watanabe/README.md new file mode 100644 index 00000000..3d60173c --- /dev/null +++ b/examples/03_papers/watanabe/README.md @@ -0,0 +1,7 @@ +# Watanabe and Kohn (2015) + +Reproduces findings from: + +> Watanabe, R.N., Kohn, A.F. (2015) *Fast Oscillatory Commands from the Motor +> Cortex Can Be Decoded by the Spinal Cord for Force Control.* J. Neurosci. +> 35:13687–13697. [DOI](https://doi.org/10.1523/JNEUROSCI.1950-15.2015) diff --git a/examples/03_papers/watanabe/README.rst b/examples/03_papers/watanabe/README.rst deleted file mode 100644 index 574376b2..00000000 --- a/examples/03_papers/watanabe/README.rst +++ /dev/null @@ -1,8 +0,0 @@ -.. _watanabe-reproduction: - -Watanabe and Kohn (2015) -====================== - -Reproduces findings from: - - Watanabe, R.N., Kohn, A.F. (2015) *Fast Oscillatory Commands from the Motor Cortex Can Be Decoded by the Spinal Cord for Force Control.* J. Neurosci. 35:13687–13697. `DOI `_ diff --git a/examples/03_papers/watanabe/_optimize_dc_worker.py b/examples/03_papers/watanabe/_optimize_dc_worker.py new file mode 100644 index 00000000..94a6e7ba --- /dev/null +++ b/examples/03_papers/watanabe/_optimize_dc_worker.py @@ -0,0 +1,45 @@ +"""Internal worker: run N Optuna trials against the shared study. Not a gallery example.""" +import argparse +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent)) + +import optuna # noqa: E402 + +from _oscillating_dc_helpers import ( # noqa: E402 + STUDY_NAME, + TIMEOUT_SECONDS, + make_storage, + objective, +) +from myogen import set_random_seed # noqa: E402 + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--n-trials", type=int, required=True) + parser.add_argument("--seed", type=int, default=42) + args = parser.parse_args() + # Decorrelate per-worker membrane/drive noise; thresholds were already built at + # import under seed 42 so they stay identical across workers. + set_random_seed(args.seed) + # Seed this worker's TPE sampler explicitly. Optuna does NOT persist samplers + # to storage, so a bare load_study() would build a fresh *unseeded* sampler in + # every worker, making the dc_offset suggestions non-reproducible despite the + # study being created with a seed. (Bit-reproducibility across parallel workers + # sharing one study is still not achievable — trial interleaving is + # nondeterministic — but this removes the unseeded-sampler source of drift.) + study = optuna.load_study( + study_name=STUDY_NAME, + storage=make_storage(), + sampler=optuna.samplers.TPESampler(seed=args.seed), + ) + # Keep the wall-clock guard that existed before the parallelization: each + # worker stops at n_trials OR TIMEOUT_SECONDS, whichever comes first (workers + # run concurrently, so this bounds the whole optimization to ~TIMEOUT_SECONDS). + study.optimize(objective, n_trials=args.n_trials, timeout=TIMEOUT_SECONDS) + + +if __name__ == "__main__": + main() diff --git a/examples/03_papers/watanabe/_oscillating_dc_helpers.py b/examples/03_papers/watanabe/_oscillating_dc_helpers.py new file mode 100644 index 00000000..0d314453 --- /dev/null +++ b/examples/03_papers/watanabe/_oscillating_dc_helpers.py @@ -0,0 +1,348 @@ +""" +Internal helpers for 02_optimize_oscillating_dc.py. + +Not a gallery example — imported by 02_optimize_oscillating_dc.py and +_optimize_dc_worker.py. Contains all simulation logic, configuration +constants, and Optuna storage helpers so that both the gallery script and +the worker subprocesses share exactly one copy of the code. +""" + +import json +import os + +os.environ["MPLBACKEND"] = "Agg" +if "DISPLAY" in os.environ: + del os.environ["DISPLAY"] + +import warnings +from pathlib import Path + +import numpy as np +import optuna +import quantities as pq +from neo import Block, Segment, SpikeTrain +from neuron import h + +from myogen import get_random_generator, set_random_seed +from myogen.simulator import RecruitmentThresholds +from myogen.simulator.core.force.force_model import ForceModel +from myogen.simulator.neuron import Network +from myogen.simulator.neuron.populations import AlphaMN__Pool, DescendingDrive__Pool +from myogen.utils.helper import calculate_firing_rate_statistics +from myogen.utils.nmodl import load_nmodl_mechanisms + +warnings.filterwarnings("ignore") + +############################################################################## +# Configuration +# ------------- + +# Simulation parameters +SIMULATION_TIME_MS = 5000.0 # Shorter simulation for efficient optimization +TIMESTEP_MS = 0.1 +N_MOTOR_UNITS = 800 # Watanabe specification + +# Force model parameters +RECORDING_FREQUENCY__HZ = 2048 +LONGEST_DURATION_RISE_TIME__MS = 90.0 +CONTRACTION_TIME_RANGE = 3 + +# Oscillation parameters (Watanabe specification) +OSC_FREQUENCY__HZ = 20.0 # 20 Hz physiological tremor +OSC_AMPLITUDE__HZ = 20.0 # Amplitude of oscillation + +# Optimization settings +N_TRIALS = 25 # Increase for production +TIMEOUT_SECONDS = 3600 + +# Network parameters (Watanabe specification) +N_DD_NEURONS = 400 +DD_CONNECTIVITY = 0.3 +SYNAPTIC_WEIGHT = 0.05 + +# Directories +RESULTS_DIR = Path(__file__).parent / "results" / "watanabe_optimization" +RESULTS_DIR.mkdir(exist_ok=True, parents=True) + +############################################################################## +# Load Reference Force Data +# -------------------------- + +_reference_file = RESULTS_DIR / "force_reference.json" + +if not _reference_file.exists(): + raise FileNotFoundError( + f"Reference force not found: {_reference_file}\nRun 01_compute_baseline_force.py first!" + ) + +with open(_reference_file, "r") as _f: + _reference_results = json.load(_f) + +REFERENCE_FORCE__N = _reference_results["force"]["mean__N"] +REFERENCE_DRIVE__HZ = _reference_results["network_parameters"]["dd_drive__Hz"] +TARGET_FORCE__N = REFERENCE_FORCE__N # Match the reference force +MAX_FORCE_N = _reference_results["force_scaling"]["max_force__N"] + +############################################################################## +# Initialize NEURON +# ----------------- + +set_random_seed(42) +load_nmodl_mechanisms() +h.secondorder = 2 + +############################################################################## +# Simulation Function +# ------------------- + + +def run_simulation_with_oscillating_drive(dc_offset, recruitment_thresholds): + """ + Run network simulation with oscillating drive and compute resulting force. + + Drive pattern: dc_offset + OSC_AMPLITUDE * sin(2*pi*OSC_FREQUENCY*t) + + Parameters + ---------- + dc_offset : float + DC offset component of oscillating drive (Hz) + recruitment_thresholds : np.ndarray + Motor unit recruitment thresholds + + Returns + ------- + tuple + (force_mean, n_active, fr_mean, fr_std) + """ + # Create motor neuron pool + motor_neuron_pool = AlphaMN__Pool( + recruitment_thresholds__array=recruitment_thresholds, + config_file="alpha_mn_default.yaml", + ) + + # Create descending drive pool (Poisson - Watanabe specification) + descending_drive_pool = DescendingDrive__Pool( + n=N_DD_NEURONS, + timestep__ms=TIMESTEP_MS * pq.ms, + process_type="poisson", + ) + + # Build network + network = Network({"DD": descending_drive_pool, "aMN": motor_neuron_pool}) + network.connect( + source="DD", + target="aMN", + probability=DD_CONNECTIVITY, + weight__uS=SYNAPTIC_WEIGHT * pq.uS, + ) + network.connect_from_external(source="cortical_input", target="DD", weight__uS=1.0 * pq.uS) + dd_netcons = network.get_netcons("cortical_input", "DD") + + # Setup spike recording + mn_spike_recorders = [] + for cell in motor_neuron_pool: + spike_recorder = h.Vector() + nc = h.NetCon(cell.soma(0.5)._ref_v, None, sec=cell.soma) + nc.threshold = 50 + nc.record(spike_recorder) + mn_spike_recorders.append(spike_recorder) + + # Create oscillating drive signal: DC + amplitude * sin(2*pi*freq*t) + time_points = int(SIMULATION_TIME_MS / TIMESTEP_MS) + time_s = np.arange(time_points) * TIMESTEP_MS / 1000.0 + + # Oscillating component + drive_signal = dc_offset + OSC_AMPLITUDE__HZ * np.sin(2 * np.pi * OSC_FREQUENCY__HZ * time_s) + + # Add small zero-mean noise, then clip the total to non-negative firing rates. + # (Clipping the noise itself to [0, inf) makes it one-sided with mean ~0.4 Hz, + # injecting a systematic DC bias into every trial's drive and thus into the + # reported dc_offset.) + drive_signal += get_random_generator().normal(0, 1.0, size=time_points) + drive_signal = np.clip(drive_signal, 0, None) + + # Initialize simulation + h.load_file("stdrun.hoc") + h.dt = TIMESTEP_MS + h.tstop = SIMULATION_TIME_MS + + for section, voltage in zip(*motor_neuron_pool.get_initialization_data()): + section.v = voltage + for section, voltage in zip(*descending_drive_pool.get_initialization_data()): + section.v = voltage + + h.finitialize() + + # Run simulation + step_counter = 0 + while h.t < h.tstop: + current_drive = drive_signal[min(step_counter, len(drive_signal) - 1)] + for dd_cell in descending_drive_pool: + if dd_cell.integrate(current_drive): + if h.t < h.tstop: + dd_netcons[dd_cell.pool__ID].event(h.t + 1) + h.fadvance() + step_counter += 1 + + # Convert to Neo format + dt_s = h.dt / 1000.0 + mn_segment = Segment(name="Motor Neurons") + mn_segment.spiketrains = [ + SpikeTrain( + recorder.as_numpy() / 1000 * pq.s, + t_stop=SIMULATION_TIME_MS / 1000 * pq.s, + sampling_rate=(1 / dt_s * pq.Hz), + sampling_period=dt_s * pq.s, + name=f"MN_{i}", + ) + for i, recorder in enumerate(mn_spike_recorders) + ] + + spike_train__Block = Block(name="Motor Unit Pool") + spike_train__Block.segments = [mn_segment] + + # Calculate statistics + n_active = sum(1 for st in mn_segment.spiketrains if len(st) > 1) + stats = calculate_firing_rate_statistics(mn_segment.spiketrains) + fr_mean = float(stats["FR_mean"]) + fr_std = float(stats["FR_std"]) + + # Generate force + force_model = ForceModel( + recruitment_thresholds=recruitment_thresholds, + recording_frequency__Hz=RECORDING_FREQUENCY__HZ * pq.Hz, + longest_duration_rise_time__ms=LONGEST_DURATION_RISE_TIME__MS * pq.ms, + contraction_time_range_factor=CONTRACTION_TIME_RANGE, + ) + + force_output = force_model.generate_force(spike_train__Block=spike_train__Block) + force_raw = force_output.magnitude[:, 0] # Arbitrary units (sum of MU twitches) + + # %MVC normalization (Watanabe methodology): normalize each simulation to its + # OWN peak, then scale to MAX_FORCE_N. This deliberately matches the *shape* of + # the normalized force profile, not absolute force in Newtons — the baseline + # (script 01) is normalized the same way, so the two are directly comparable. + force_max_raw = np.max(force_raw) + force_signal = (force_raw / force_max_raw) * MAX_FORCE_N # peak-normalized (%MVC), not absolute N + + # Calculate steady-state force + steady_idx = len(force_signal) // 2 + force_mean = np.mean(force_signal[steady_idx:]) + + return force_mean, n_active, fr_mean, fr_std + + +############################################################################## +# Recruitment Thresholds (module global — built once under seed 42) +# ----------------------------------------------------------------- + +recruitment_thresholds, _ = RecruitmentThresholds( + N=N_MOTOR_UNITS, + recruitment_range__ratio=100, + deluca__slope=5, + konstantin__max_threshold__ratio=1.0, + mode="combined", +) + +############################################################################## +# Objective Function +# ------------------ + + +def objective(trial): + """ + Optimize the DC offset so the oscillating drive reproduces the reference's + NORMALIZED steady-state force. + + Both this trial and the baseline (script 01) are peak-normalized to + ``MAX_FORCE_N`` (%MVC methodology), so the objective matches the normalized + force *profile*, not absolute force in Newtons. + + Parameters + ---------- + trial : optuna.Trial + Optimization trial + + Returns + ------- + float + Relative error of the normalized steady-state force (minimize) + """ + try: + # Optimize DC offset (will be lower than constant drive) + dc_offset = trial.suggest_float("dc_offset", 1.0, 100.0) + + # Run simulation with oscillating drive + force_mean, n_active, fr_mean, fr_std = run_simulation_with_oscillating_drive( + dc_offset, recruitment_thresholds + ) + + # Check minimum recruitment + if n_active < 10: # At least 1.25% of neurons + return 1000.0 + (10 - n_active) * 100.0 + + # Calculate relative error + force_error = abs(force_mean - TARGET_FORCE__N) / TARGET_FORCE__N + + # Store metadata + trial.set_user_attr("force_achieved", float(force_mean)) + trial.set_user_attr("force_error", float(force_error)) + trial.set_user_attr("n_active", n_active) + trial.set_user_attr("dc_offset__Hz", float(dc_offset)) + trial.set_user_attr("FR_mean", float(fr_mean)) + trial.set_user_attr("FR_std", float(fr_std)) + + if trial.number % 5 == 0: + print( + f"Trial {trial.number}: " + f"Force={force_mean:.2f}N (target={TARGET_FORCE__N:.2f}N), " + f"Error={force_error:.1%}, " + f"DC={dc_offset:.1f}Hz, " + f"Active={n_active}/{N_MOTOR_UNITS}" + ) + + return force_error + + except Exception as e: + print(f"Trial {trial.number} failed: {e}") + return 1000.0 + + +############################################################################## +# Optuna Study Config + Storage +# ------------------------------ + +STUDY_NAME = "dc_offset_oscillating_match" +STORAGE_URL = f"sqlite:///{RESULTS_DIR}/optuna_oscillating_dc.db" + + +def make_storage(): + """Return an RDBStorage with a generous lock timeout for concurrent workers.""" + # connect timeout reduces "database is locked" under concurrent workers + return optuna.storages.RDBStorage(STORAGE_URL, engine_kwargs={"connect_args": {"timeout": 60}}) + + +############################################################################## +# Public API +# ---------- + +__all__ = [ + "objective", + "recruitment_thresholds", + "STUDY_NAME", + "STORAGE_URL", + "make_storage", + "RESULTS_DIR", + "N_TRIALS", + "TIMEOUT_SECONDS", + "REFERENCE_FORCE__N", + "REFERENCE_DRIVE__HZ", + "TARGET_FORCE__N", + "MAX_FORCE_N", + "N_MOTOR_UNITS", + "OSC_FREQUENCY__HZ", + "OSC_AMPLITUDE__HZ", + "N_DD_NEURONS", + "DD_CONNECTIVITY", + "SYNAPTIC_WEIGHT", +] diff --git a/examples/04_clinical/README.md b/examples/04_clinical/README.md new file mode 100644 index 00000000..c3f4c67c --- /dev/null +++ b/examples/04_clinical/README.md @@ -0,0 +1,15 @@ +# Clinical & Pathology + +This gallery contains examples that model pathological motor-control signals, +such as those seen after spinal cord injury (SCI). Each example reuses MyoGen's +standard simulation pipeline with a fixed peripheral model (muscle, electrode, +motor-unit pool) and reproduces clinically recognizable EMG signatures by +varying motoneuron excitability — the persistent inward current (PIC) — and the +descending drive. + +!!! note + The gallery is source-only by default: examples are rendered from source but + are not executed during a normal documentation build. Thumbnails and + screenshots may reflect a prior verified run, so run an example locally to + reproduce its outputs. Maintainers can opt into execution with + `MKDOCS_GALLERY_PLOT=true`. diff --git a/examples/04_clinical/README.rst b/examples/04_clinical/README.rst deleted file mode 100644 index 2f84d5a0..00000000 --- a/examples/04_clinical/README.rst +++ /dev/null @@ -1,12 +0,0 @@ -.. _clinical-examples: - -======================== -Clinical & Pathology -======================== - -| This gallery contains examples that model pathological motor-control signals, -| such as those seen after spinal cord injury (SCI). Each example reuses MyoGen's -| standard simulation pipeline with a fixed peripheral model (muscle, electrode, -| motor-unit pool) and reproduces clinically recognizable EMG signatures by -| varying motoneuron excitability -- the persistent inward current (PIC) -- and -| the descending drive. diff --git a/examples/04_clinical/_pic_protocols.py b/examples/04_clinical/_pic_protocols.py index 3ab1b6c8..d6173f49 100644 --- a/examples/04_clinical/_pic_protocols.py +++ b/examples/04_clinical/_pic_protocols.py @@ -325,7 +325,7 @@ def run_pool(command, n_mu, gamma, nap_factor=1.0, nap_ceiling=0.00215, iv.play(ic._ref_amp, tvec_n, True) _noise_keep.append((ic, iv)) _noise_keep.append(tvec_n) - dd_pool = DescendingDrive__Pool(n=dd_n, poisson_batch_size=5, + dd_pool = DescendingDrive__Pool(n=dd_n, process_type="gamma", shape=5, timestep__ms=_POOL_TIMESTEP) net = Network({"DD": dd_pool, "aMN": mn_pool}) net.connect(source="DD", target="aMN", probability=0.5, diff --git a/myogen/__init__.py b/myogen/__init__.py index 2aa6fb35..5e892784 100644 --- a/myogen/__init__.py +++ b/myogen/__init__.py @@ -34,17 +34,17 @@ def derive_subseed(*labels: int) -> int: Intended for seeding non-NumPy generators (Cython Mersenne spike generators, sklearn ``random_state``, etc.) so that a call to - :func:`set_random_seed` propagates to them. Label order matters: + ``set_random_seed`` propagates to them. Label order matters: ``derive_subseed(a, b)`` and ``derive_subseed(b, a)`` yield different sub-seeds. Each label must be a **non-negative** integer; callers with signed identifiers should offset them beforehand (NumPy's - :class:`~numpy.random.SeedSequence`, which backs this helper, rejects + ``numpy.random.SeedSequence``, which backs this helper, rejects negatives). This replaces the pre-existing ``SEED + (class_id+1)*(global_id+1)`` derivation, which collided on swapped factors — e.g. ``(0, 5)`` and ``(1, 2)`` both produced ``+6``. The present mixing function uses - :class:`numpy.random.SeedSequence` to fold the inputs into a 32-bit + ``numpy.random.SeedSequence`` to fold the inputs into a 32-bit integer; collisions remain possible in principle (birthday-paradox probability ≈ ``N² / 2³³``) but are negligible for realistic motor-unit pool sizes (≲ 10⁻⁷ at 1000 cells). @@ -64,7 +64,7 @@ def set_random_seed(seed: int = _DEFAULT_SEED) -> None: Set the random seed for reproducibility. Rebuilds the global NumPy ``Generator``. All modules that read the RNG - through :func:`get_random_generator` will observe the new state on their + through ``get_random_generator`` will observe the new state on their next draw; this includes seeds derived for non-NumPy RNGs (e.g. sklearn ``random_state`` arguments or Cython Mersenne generators), which are now drawn from the global RNG rather than read from a frozen module constant. diff --git a/myogen/simulator/core/emg/fiber_simulation.py b/myogen/simulator/core/emg/fiber_simulation.py index 9d64941b..8096eace 100644 --- a/myogen/simulator/core/emg/fiber_simulation.py +++ b/myogen/simulator/core/emg/fiber_simulation.py @@ -7,10 +7,8 @@ References ---------- -.. [1] Rosenfalck, P., 1969. Intra- and extracellular potential fields of active - nerve and muscle fibres. Acta Physiol. Scand. Suppl. 321, 1-168. -.. [2] Farina, D. et al., 2004. A surface EMG generation model with multilayer - cylindrical description of the volume conductor. IEEE TBME 51(3), 415-426. +[1] Rosenfalck, P., 1969. Intra- and extracellular potential fields of active nerve and muscle fibres. Acta Physiol. Scand. Suppl. 321, 1-168.
+[2] Farina, D. et al., 2004. A surface EMG generation model with multilayer cylindrical description of the volume conductor. IEEE TBME 51(3), 415-426. """ from __future__ import annotations diff --git a/myogen/simulator/core/emg/intramuscular/bioelectric.py b/myogen/simulator/core/emg/intramuscular/bioelectric.py index b9974471..8a86e0e3 100644 --- a/myogen/simulator/core/emg/intramuscular/bioelectric.py +++ b/myogen/simulator/core/emg/intramuscular/bioelectric.py @@ -9,13 +9,9 @@ References ---------- -.. [1] Farina, D., Merletti, R., 2001. A novel approach for precise simulation of - the EMG signal detected by surface electrodes. IEEE Transactions on - Biomedical Engineering 48, 637–646. -.. [2] Rosenfalck, P., 1969. Intra- and extracellular potential fields of active - nerve and muscle fibres. Acta Physiologica Scandinavica Supplementum 321, 1–168. -.. [3] Nandedkar, S.D., Stålberg, E., 1983. Simulation of single muscle fibre - action potentials. Medical & Biological Engineering & Computing 21, 158–165. +[1] Farina, D., Merletti, R., 2001. A novel approach for precise simulation of the EMG signal detected by surface electrodes. IEEE Transactions on Biomedical Engineering 48, 637–646.
+[2] Rosenfalck, P., 1969. Intra- and extracellular potential fields of active nerve and muscle fibres. Acta Physiologica Scandinavica Supplementum 321, 1–168.
+[3] Nandedkar, S.D., Stålberg, E., 1983. Simulation of single muscle fibre action potentials. Medical & Biological Engineering & Computing 21, 158–165. """ import numpy as np diff --git a/myogen/simulator/core/emg/intramuscular/intramuscular_emg.py b/myogen/simulator/core/emg/intramuscular/intramuscular_emg.py index 7b6baeab..ef563620 100644 --- a/myogen/simulator/core/emg/intramuscular/intramuscular_emg.py +++ b/myogen/simulator/core/emg/intramuscular/intramuscular_emg.py @@ -52,9 +52,9 @@ class IntramuscularEMG: Parameters ---------- muscle_model : Muscle - Pre-computed muscle model (see :class:`myogen.simulator.Muscle`). + Pre-computed muscle model (see `myogen.simulator.Muscle`). electrode_array : IntramuscularElectrodeArray - Intramuscular electrode array configuration to use for simulation (see :class:`myogen.simulator.IntramuscularElectrodeArray`). + Intramuscular electrode array configuration to use for simulation (see `myogen.simulator.IntramuscularElectrodeArray`). sampling_frequency__Hz : Quantity__Hz, default=10240.0 * pq.Hz Sampling frequency in Hz for EMG simulation. Default is set to 10240 Hz as used by the Quattrocento (OT Bioelettronica, Turin, Italy) system. @@ -66,14 +66,13 @@ class IntramuscularEMG: By default, the endplate is located at the center of the muscle (50% of the muscle length). nmj_jitter__s : Quantity__s, default=35e-6 * pq.s Standard deviation of neuromuscular junction jitter in seconds. - Default is set to 35e-6 s as determined by Konstantin et al. 2020 [1]_. + Default is set to 35e-6 s as determined by Konstantin et al. 2020 [1]. branch_cvs__m_per_s : tuple[Quantity__m_per_s, Quantity__m_per_s], default=(5.0 * pq.m / pq.s, 2.0 * pq.m / pq.s) Conduction velocities for the two-layer model of the neuromuscular junction in m/s. - Default is set to (5.0, 2.0) m/s as determined by Konstantin et al. 2020 [1]_. + Default is set to (5.0, 2.0) m/s as determined by Konstantin et al. 2020 [1]. - .. note:: - The two-layer model is a simplification of the actual arborization pattern, but it is a good approximation for the purposes of this simulation. - Follows the implementation of Konstantin et al. 2020 [1]_. + Note: The two-layer model is a simplification of the actual arborization pattern, but it is a good approximation for the purposes of this simulation. + Follows the implementation of Konstantin et al. 2020 [1]. MUs_to_simulate : list[int], optional Indices of motor units to simulate. If None, all motor units are simulated. Default is None. For computational efficiency, consider @@ -93,7 +92,7 @@ class IntramuscularEMG: References ---------- - .. [1] Konstantin, A., Yu, T., Le Carpentier, E., Aoustin, Y., Farina, D., 2020. Simulation of Motor Unit Action Potential Recordings From Intramuscular Multichannel Scanning Electrodes. IEEE Transactions on Biomedical Engineering 67, 2005–2014. https://doi.org/10.1109/TBME.2019.2953680 + [1] Konstantin, A., Yu, T., Le Carpentier, E., Aoustin, Y., Farina, D., 2020. Simulation of Motor Unit Action Potential Recordings From Intramuscular Multichannel Scanning Electrodes. IEEE Transactions on Biomedical Engineering 67, 2005–2014. https://doi.org/10.1109/TBME.2019.2953680 """ def __init__( @@ -762,7 +761,7 @@ def add_noise( 1/f-like base, mid-band spectral emphasis from electrode–amplifier bandwidth, heavy tails from cross-talk artifacts, and additive 50/60 Hz powerline interference with - harmonics. See :mod:`myogen.utils.emg_noise` for the math. + harmonics. See `myogen.utils.emg_noise` for the math. Per-channel SNR is preserved across both modes: each electrode's noise RMS is computed from that channel's own signal RMS so @@ -823,7 +822,7 @@ def add_noise( The spectral form is paper-constrained; the amplitude defaults are *not* validated for intramuscular EMG — calibrate against real recordings via - :func:`myogen.utils.calibrate_baseline_drift_profile`. + `myogen.utils.calibrate_baseline_drift_profile`. Broadband movement artifacts (0–20 Hz, De Luca 2010) are a separate phenomenon out of scope here. Ignored when ``noise_type="gaussian"``. @@ -852,7 +851,7 @@ def add_noise( ------ ValueError If intramuscular EMG has not been simulated (call - :meth:`simulate_intramuscular_emg` first) or ``noise_type`` + `simulate_intramuscular_emg` first) or ``noise_type`` is unrecognized. """ if self._intramuscular_emg__Block is None: diff --git a/myogen/simulator/core/emg/surface/surface_emg.py b/myogen/simulator/core/emg/surface/surface_emg.py index 1ca397da..fa479171 100644 --- a/myogen/simulator/core/emg/surface/surface_emg.py +++ b/myogen/simulator/core/emg/surface/surface_emg.py @@ -40,14 +40,14 @@ class SurfaceEMG: This class provides a simulation framework for generating surface electromyography signals from the muscle. It implements the - multi-layered cylindrical volume conductor model from Farina et al. 2004 [1]_. + multi-layered cylindrical volume conductor model from Farina et al. 2004 [1]. Parameters ---------- muscle_model : Muscle - Pre-computed muscle model (see :class:`myogen.simulator.Muscle`). + Pre-computed muscle model (see `myogen.simulator.Muscle`). electrode_arrays : list[SurfaceElectrodeArray] - List of electrode arrays to use for simulation (see :class:`myogen.simulator.SurfaceElectrodeArray`). + List of electrode arrays to use for simulation (see `myogen.simulator.SurfaceElectrodeArray`). sampling_frequency__Hz : float, default=2048.0 Sampling frequency in Hz. Default is set to 2048 Hz as used by the Quattrocento (OT Bioelettronica, Turin, Italy) system. sampling_points_in_t_and_z_domains : int, default=256 @@ -98,7 +98,7 @@ class SurfaceEMG: References ---------- - .. [1] Farina, D., Mesin, L., Martina, S., Merletti, R., 2004. A surface EMG generation model with multilayer cylindrical description of the volume conductor. IEEE Transactions on Biomedical Engineering 51, 415–426. https://doi.org/10.1109/TBME.2003.820998 + [1] Farina, D., Mesin, L., Martina, S., Merletti, R., 2004. A surface EMG generation model with multilayer cylindrical description of the volume conductor. IEEE Transactions on Biomedical Engineering 51, 415–426. https://doi.org/10.1109/TBME.2003.820998 """ def __init__( @@ -228,7 +228,7 @@ def simulate_muaps( If True, display progress bars. Set to False to disable. use_gpu : bool or None, default=None GPU acceleration control for the per-fiber volume-conductor solve - (mirrors :meth:`myogen.simulator.MotorUnitSim.calc_sfaps`): + (mirrors `myogen.simulator.MotorUnitSim.calc_sfaps`): - ``None`` → auto: use GPU if CuPy is available and ``MYOGEN_DISABLE_GPU`` is not set in the environment; CPU otherwise. @@ -262,7 +262,7 @@ def simulate_muaps( # Calculate innervation zone variance innervation_zone_variance = ( self._mean_fiber_length__mm * 0.1 - ) # 10% of the mean fiber length (see Botelho et al. 2019 [6]_) + ) # 10% of the mean fiber length (see Botelho et al. 2019 [6]) # Extract fiber counts number_of_fibers_per_MUs = self._muscle_model.resulting_number_of_innervated_fibers diff --git a/myogen/simulator/core/force/force_model.py b/myogen/simulator/core/force/force_model.py index af6c6692..0f108987 100644 --- a/myogen/simulator/core/force/force_model.py +++ b/myogen/simulator/core/force/force_model.py @@ -22,7 +22,7 @@ @beartowertype class ForceModel: """ - Force model based on Fuglevand et al. (1993) [1]_. + Force model based on Fuglevand et al. (1993) [1]. This class implements the Fuglevand force generation model for motor unit pools, computing individual motor unit twitch responses and their nonlinear gain modulation @@ -70,9 +70,7 @@ class ForceModel: References ---------- - .. [1] Fuglevand, A. J., Winter, D. A., & Patla, A. E. (1993). - Models of recruitment and rate coding in motor-unit pools. - Journal of Neurophysiology, 70(2), 782-797. + [1] Fuglevand, A. J., Winter, D. A., & Patla, A. E. (1993). Models of recruitment and rate coding in motor-unit pools. Journal of Neurophysiology, 70(2), 782-797. Examples -------- diff --git a/myogen/simulator/core/force/force_model_vectorized.py b/myogen/simulator/core/force/force_model_vectorized.py index bcd93c71..c6154657 100644 --- a/myogen/simulator/core/force/force_model_vectorized.py +++ b/myogen/simulator/core/force/force_model_vectorized.py @@ -23,9 +23,9 @@ @beartowertype class ForceModelVectorized: """ - Vectorized force model based on Fuglevand et al. (1993) [1]_. + Vectorized force model based on Fuglevand et al. (1993) [1]. - This is an optimized version of :class:`ForceModel` that uses numpy + This is an optimized version of `ForceModel` that uses numpy vectorization for significantly better performance, especially for long simulations. It shares the IPI/gain/twitch pipeline with the reference implementation via ``force_utils`` so the output is guaranteed to match @@ -46,9 +46,7 @@ class ForceModelVectorized: References ---------- - .. [1] Fuglevand, A. J., Winter, D. A., & Patla, A. E. (1993). - Models of recruitment and rate coding in motor-unit pools. - Journal of Neurophysiology, 70(2), 782-797. + [1] Fuglevand, A. J., Winter, D. A., & Patla, A. E. (1993). Models of recruitment and rate coding in motor-unit pools. Journal of Neurophysiology, 70(2), 782-797. """ def __init__( @@ -184,7 +182,7 @@ def generate_force( """ Generate force output from motor unit spike trains. - The body mirrors :meth:`ForceModel.generate_force` so that the two + The body mirrors `ForceModel.generate_force` so that the two implementations cannot drift apart silently. Only the per-spike accumulation differs (vectorized vs. per-spike loop). """ @@ -242,9 +240,9 @@ def _generate_force_vectorized( ) -> np.ndarray: """Generate force using vectorized per-time-step accumulation. - Mirrors :meth:`ForceModel._generate_force` byte-for-byte except for + Mirrors `ForceModel._generate_force` byte-for-byte except for the inner per-spike accumulation, which is delegated to - :func:`generate_force_vectorized`. + `generate_force_vectorized`. """ # Convert sparse to dense once at the start (mirrors ForceModel) if sp.issparse(spikes): diff --git a/myogen/simulator/core/muscle/muscle.py b/myogen/simulator/core/muscle/muscle.py index 9e04140f..f7c5f507 100644 --- a/myogen/simulator/core/muscle/muscle.py +++ b/myogen/simulator/core/muscle/muscle.py @@ -88,22 +88,22 @@ def _perform_fast_marching(speed_map: np.ndarray, seed_points: np.ndarray) -> np @beartowertype class Muscle: """ - A muscle model based on the cylindrical description of the volume conductor by Farina et al. 2004 [1]_ and the motor unit distribution by Konstantin et al. 2020 [2]_. + A muscle model based on the cylindrical description of the volume conductor by Farina et al. 2004 [1] and the motor unit distribution by Konstantin et al. 2020 [2]. - .. note:: - All default values are set to simulate the First Dorsal Interosseous (FDI) muscle. Values are pulled from the literature. + All default values are set to simulate the First Dorsal Interosseous (FDI) muscle. + Values are pulled from the literature. Parameters ---------- recruitment_thresholds : RECRUITMENT_THRESHOLDS__ARRAY - Array of recruitment thresholds for each motor unit (see `myogen.simulator.generate_mu_recruitment_thresholds`). + Array of recruitment thresholds for each motor unit (see `myogen.simulator.RecruitmentThresholds`). Values range from 0 to 1 with the largest motor units having thresholds near 1. radius__mm : float, default=6.91 - Radius of the muscle cross-section in millimeters. Default is set to 6.91 mm as determined by Jacobson et al. 1992 [3]_. + Radius of the muscle cross-section in millimeters. Default is set to 6.91 mm as determined by Jacobson et al. 1992 [3]. length__mm : float, default=30.0 Length of the muscle in millimeters. Default is 30.0 mm, a nominal value chosen to match the order of magnitude of the FDI muscle; adjust to match the muscle under study. fiber_density__fibers_per_mm2 : float, default=350 - Density of muscle fibers per square millimeter. Default is set to 350 fibers/mm² as determined by Bettelho et al. 2019 [7]_. + Density of muscle fibers per square millimeter. Default is set to 350 fibers/mm² as determined by Bettelho et al. 2019 [7]. max_innervation_area_to_total_muscle_area__ratio : float, default=0.25 Ratio defining the maximum territory size relative to total muscle area. Default is 0.25 as a pragmatic upper bound for the FDI, with no single published source; revisit for larger muscles. @@ -111,26 +111,26 @@ class Muscle: of the total muscle cross-sectional area. Must be in range (0, 1]. mean_conduction_velocity__m_per_s : float, default=4.2 - Mean conduction velocity in m/s. Default is set to 4.2 m/s as determined by Nishizono et al. 1990 [4]_. - Experimental range determined by Nishizono et al. 1990 [4]_ is between 3.2 and 5.0 m/s. + Mean conduction velocity in m/s. Default is set to 4.2 m/s as determined by Nishizono et al. 1990 [4]. + Experimental range determined by Nishizono et al. 1990 [4] is between 3.2 and 5.0 m/s. mean_fiber_length__mm : float, default=31.7 - Mean fiber length in mm. Default is set to 31.7 mm as determined by Jacobson et al. 1992 [3]_ (Table 1). + Mean fiber length in mm. Default is set to 31.7 mm as determined by Jacobson et al. 1992 [3] (Table 1). var_fiber_length__mm : float, default=2.8 - Fiber length variance in mm. Default is set to 2.8 mm as determined by Jacobson et al. 1992 [3]_ (Table 1). + Fiber length variance in mm. Default is set to 2.8 mm as determined by Jacobson et al. 1992 [3] (Table 1). radius_bone__mm : float, default=1 Bone radius in mm. Default is set to 1 mm. fat_thickness__mm : float, default=0.3 - Fat thickness in mm. Default is set to 0.3 mm as determined by Störchle et al. 2018 [5]_. + Fat thickness in mm. Default is set to 0.3 mm as determined by Störchle et al. 2018 [5]. skin_thickness__mm : float, default=1.29 - Skin thickness in mm. Default is set to the male skin thickness average of 1.29 mm as determined by Brodar 1960 [6]_. - muscle_conductivity_radial__S_m : float, default=0.09 - Muscle conductivity in radial direction. Default is set to 0.09 S/m as determined by Botelho et al. 2019 [7]_ (Table 1). - muscle_conductivity_longitudinal__S_m : float, default=0.4 - Muscle conductivity in longitudinal direction. Default is set to 0.4 S/m as determined by Botelho et al. 2019 [7]_ (Table 1). + Skin thickness in mm. Default is set to the male skin thickness average of 1.29 mm as determined by Brodar 1960 [6]. + muscle_conductivity_radial__S_per_m : float, default=0.09 + Muscle conductivity in radial direction. Default is set to 0.09 S/m as determined by Botelho et al. 2019 [7] (Table 1). + muscle_conductivity_longitudinal__S_per_m : float, default=0.4 + Muscle conductivity in longitudinal direction. Default is set to 0.4 S/m as determined by Botelho et al. 2019 [7] (Table 1). fat_conductivity__S_per_m : float, default=4.07E-2 - Fat conductivity. Default is set to 4.07E-2 S/m as determined by Botelho et al. 2019 [7]_ (Table 1). + Fat conductivity. Default is set to 4.07E-2 S/m as determined by Botelho et al. 2019 [7] (Table 1). skin_conductivity__S_per_m : float, default=4.88E-4 - Skin conductivity. Default is set to 4.88E-4 S/m as determined by Botelho et al. 2019 [7]_ (Table 1). + Skin conductivity. Default is set to 4.88E-4 S/m as determined by Botelho et al. 2019 [7] (Table 1). grid_resolution : int, default=256 Resolution of the computational grid used for innervation the muscle. Higher values provide more accurate spatial distribution but increase computational cost. @@ -167,19 +167,13 @@ class Muscle: References ---------- - .. [1] Farina, D., Mesin, L., Martina, S., Merletti, R., 2004. A surface EMG generation model with multilayer cylindrical description of the volume conductor. IEEE Transactions on Biomedical Engineering 51, 415–426. https://doi.org/10.1109/TBME.2003.820998 - - .. [2] Konstantin, A., Yu, T., Le Carpentier, E., Aoustin, Y., Farina, D., 2020. Simulation of Motor Unit Action Potential Recordings From Intramuscular Multichannel Scanning Electrodes. IEEE Transactions on Biomedical Engineering 67, 2005–2014. https://doi.org/10.1109/TBME.2019.2953680 - - .. [3] Jacobson, M.D., Raab, R., Fazeli, B.M., Abrams, R.A., Botte, M.J., Lieber, R.L., 1992. Architectural design of the human intrinsic hand muscles. The Journal of Hand Surgery 17, 804–809. https://doi.org/10.1016/0363-5023(92)90446-V - - .. [4] Nishizono, H., Fujimoto, T., Ohtake, H., Miyashita, M., 1990. Muscle fiber conduction velocity and contractile properties estimated from surface electrode arrays. Electroencephalography and Clinical Neurophysiology 75, 75–81. https://doi.org/10.1016/0013-4694(90)90154-C - - .. [5] Störchle, P., Müller, W., Sengeis, M., Lackner, S., Holasek, S., Fürhapter-Rieger, A., 2018. Measurement of mean subcutaneous fat thickness: eight standardised ultrasound sites compared to 216 randomly selected sites. Sci Rep 8, 16268. https://doi.org/10.1038/s41598-018-34213-0 - - .. [6] Brodar, V., 1960. Observations on skin thickness and subcutaneous tissue in man. Zeitschrift für Morphologie und Anthropologie 50, 386–395. - - .. [7] Botelho, D.P., Curran, K., Lowery, M.M., 2019. Anatomically accurate model of EMG during index finger flexion and abduction derived from diffusion tensor imaging. PLOS Computational Biology 15, e1007267. https://doi.org/10.1371/journal.pcbi.1007267 + [1] Farina, D., Mesin, L., Martina, S., Merletti, R., 2004. A surface EMG generation model with multilayer cylindrical description of the volume conductor. IEEE Transactions on Biomedical Engineering 51, 415–426. https://doi.org/10.1109/TBME.2003.820998
+ [2] Konstantin, A., Yu, T., Le Carpentier, E., Aoustin, Y., Farina, D., 2020. Simulation of Motor Unit Action Potential Recordings From Intramuscular Multichannel Scanning Electrodes. IEEE Transactions on Biomedical Engineering 67, 2005–2014. https://doi.org/10.1109/TBME.2019.2953680
+ [3] Jacobson, M.D., Raab, R., Fazeli, B.M., Abrams, R.A., Botte, M.J., Lieber, R.L., 1992. Architectural design of the human intrinsic hand muscles. The Journal of Hand Surgery 17, 804–809. https://doi.org/10.1016/0363-5023(92)90446-V
+ [4] Nishizono, H., Fujimoto, T., Ohtake, H., Miyashita, M., 1990. Muscle fiber conduction velocity and contractile properties estimated from surface electrode arrays. Electroencephalography and Clinical Neurophysiology 75, 75–81. https://doi.org/10.1016/0013-4694(90)90154-C
+ [5] Störchle, P., Müller, W., Sengeis, M., Lackner, S., Holasek, S., Fürhapter-Rieger, A., 2018. Measurement of mean subcutaneous fat thickness: eight standardised ultrasound sites compared to 216 randomly selected sites. Sci Rep 8, 16268. https://doi.org/10.1038/s41598-018-34213-0
+ [6] Brodar, V., 1960. Observations on skin thickness and subcutaneous tissue in man. Zeitschrift für Morphologie und Anthropologie 50, 386–395.
+ [7] Botelho, D.P., Curran, K., Lowery, M.M., 2019. Anatomically accurate model of EMG during index finger flexion and abduction derived from diffusion tensor imaging. PLOS Computational Biology 15, e1007267. https://doi.org/10.1371/journal.pcbi.1007267 """ def __init__( @@ -427,14 +421,14 @@ def generate_muscle_fiber_centers(self, verbose: bool = True) -> None: verbose : bool, default=True If True, display status messages. Set to False to disable. + Notes + ----- Results are stored in the following properties after execution: - - `mf_centers`: Array of shape (n_fibers, 2) with fiber positions [x, y] in mm - - `number_of_muscle_fibers`: Total number of muscle fibers - - `muscle_border`: Array of border points for visualization + - `mf_centers`: Array of shape (n_fibers, 2) with fiber positions [x, y] in mm + - `number_of_muscle_fibers`: Total number of muscle fibers + - `muscle_border`: Array of border points for visualization - Notes - ----- This method should be called after distribute_innervation_centers() and before assign_mfs2mns(). The Voronoi-based distribution provides more realistic fiber spacing compared to regular grids or purely random distributions. @@ -547,8 +541,6 @@ def assign_mfs2mns(self, n_neighbours: int = 3, conf: float = 0.999, n_jobs: int verbose : bool, default=True If True, display progress bars and status messages. Set to False to disable. - Results are stored in the `assignment` property after execution. - Raises ------ ValueError @@ -557,6 +549,8 @@ def assign_mfs2mns(self, n_neighbours: int = 3, conf: float = 0.999, n_jobs: int Notes ----- + Results are stored in the `assignment` property after execution. + The algorithm compensates for out-of-muscle effects by calculating how much of each motor unit's Gaussian distribution falls outside the circular muscle boundary and adjusting the in-muscle probabilities accordingly. diff --git a/myogen/simulator/core/physiological_distribution.py b/myogen/simulator/core/physiological_distribution.py index 82133ecb..c51dd7bd 100644 --- a/myogen/simulator/core/physiological_distribution.py +++ b/myogen/simulator/core/physiological_distribution.py @@ -32,9 +32,9 @@ def __init__( recruitment range (RR) and, for some models, additional parameters. Following models are available: - - Fuglevand et al. (1993) [1]_ - - De Luca & Contessa (2012) [2]_ - - Konstantin et al. (2020) [3]_ + - Fuglevand et al. (1993) [1] + - De Luca & Contessa (2012) [2] + - Konstantin et al. (2020) [3] - Combined model Parameters @@ -43,7 +43,7 @@ def __init__( Number of motor units in the pool. recruitment_range__ratio : float Recruitment range (dimensionless ratio), defined as the ratio of the largest to smallest threshold - :math:`(rt(N)/rt(1))`. + $(rt(N)/rt(1))$. deluca__slope : float, optional Dimensionless slope parameter for the ``'deluca'`` mode. Required if ``mode='deluca'``. Controls the curvature of the threshold distribution. Typical values range from 0.001-100. @@ -60,8 +60,8 @@ def __init__( Recruitment thresholds for each motor unit (shape: (N,)). Values are monotonically increasing from ``rt[0]`` to ``rt[N-1]``. rtz : RECRUITMENT_THRESHOLDS__ARRAY - Zero-based recruitment thresholds where :math:`rtz[0] = 0` (shape: (N,)). - Computed as :math:`rtz = rt - rt[0]`, convenient for simulation. + Zero-based recruitment thresholds where $rtz[0] = 0$ (shape: (N,)). + Computed as $rtz = rt - rt[0]$, convenient for simulation. Raises ------ @@ -70,41 +70,40 @@ def __init__( References ---------- - .. [1] Fuglevand, A.J., Winter, D.A., Patla, A.E., 1993. - Models of recruitment and rate coding organization in motor-unit pools. - Journal of Neurophysiology 70, 2470-2488. https://doi.org/10.1152/jn.1993.70.6.2470 - .. [2] De Luca, C.J., Contessa, P., 2012. - Hierarchical control of motor units in voluntary contractions. - Journal of Neurophysiology 107, 178-195. https://doi.org/10.1152/jn.00961.2010 - .. [3] Konstantin, A., Yu, T., Le Carpentier, E., Aoustin, Y., Farina, D., 2020. - Simulation of Motor Unit Action Potential Recordings From Intramuscular Multichannel Scanning Electrodes. - IEEE Transactions on Biomedical Engineering 67, 2005-2014. https://doi.org/10.1109/TBME.2019.2953680 + [1] Fuglevand, A.J., Winter, D.A., Patla, A.E., 1993. Models of recruitment and rate coding organization in motor-unit pools. Journal of Neurophysiology 70, 2470-2488. https://doi.org/10.1152/jn.1993.70.6.2470
+ [2] De Luca, C.J., Contessa, P., 2012. Hierarchical control of motor units in voluntary contractions. Journal of Neurophysiology 107, 178-195. https://doi.org/10.1152/jn.00961.2010
+ [3] Konstantin, A., Yu, T., Le Carpentier, E., Aoustin, Y., Farina, D., 2020. Simulation of Motor Unit Action Potential Recordings From Intramuscular Multichannel Scanning Electrodes. IEEE Transactions on Biomedical Engineering 67, 2005-2014. https://doi.org/10.1109/TBME.2019.2953680 Notes ----- - **fuglevand** : Fuglevand et al. (1993) [1]_ exponential model - .. math:: rt(i) = \\exp( \\frac{i \\cdot \\ln(RR)}{N} ) / 100 + **fuglevand** : Fuglevand et al. (1993) [1] exponential model - where :math:`i = 1, 2, \\ldots, N` + $$rt(i) = \exp\left( \frac{i \cdot \ln(RR)}{N} \right) / 100$$ - **deluca** : De Luca & Contessa (2012) [2]_ model with slope correction - .. math:: - rt(i) = \\frac{b \\cdot i}{N} \\cdot \\exp\\left(\\frac{i \\cdot \\ln(RR / b)}{N}\\right) / 100 + where $i = 1, 2, \ldots, N$ - where :math:`b` = ``deluca__slope``, :math:`i = 1, 2, \\ldots, N` + **deluca** : De Luca & Contessa (2012) [2] model with slope correction - **konstantin** : Konstantin et al. (2020) [3]_ model allowing explicit maximum threshold control - .. math:: - rt(i) &= \\frac{RT_{max}}{RR} \\cdot \\exp\\left(\\frac{(i - 1) \\cdot \\ln(RR)}{N - 1}\\right) \\\\ - rtz(i) &= \\frac{RT_{max}}{RR} \\cdot \\left(\\exp\\left(\\frac{(i - 1) \\cdot \\ln(RR + 1)}{N}\\right) - 1\\right) + $$rt(i) = \frac{b \cdot i}{N} \cdot \exp\left(\frac{i \cdot \ln(RR / b)}{N}\right) / 100$$ - where :math:`RT_{max}` = ``konstantin__max_threshold__ratio``, :math:`i = 1, 2, \\ldots, N` + where $b$ = ``deluca__slope``, $i = 1, 2, \ldots, N$ + + **konstantin** : Konstantin et al. (2020) [3] model allowing explicit maximum threshold control + + $$ + \begin{aligned} + rt(i) &= \frac{RT_{max}}{RR} \cdot \exp\left(\frac{(i - 1) \cdot \ln(RR)}{N - 1}\right) \\ + rtz(i) &= \frac{RT_{max}}{RR} \cdot \left(\exp\left(\frac{(i - 1) \cdot \ln(RR + 1)}{N}\right) - 1\right) + \end{aligned} + $$ + + where $RT_{max}$ = ``konstantin__max_threshold__ratio``, $i = 1, 2, \ldots, N$ **combined** : A corrected De Luca model that uses the slope parameter for shape control but properly respects the RR constraint and maximum threshold like the Konstantin model - .. math:: - rt(i) = \\frac{RT_{max}}{RR} + \\left(\\frac{b \\cdot i}{N} \\cdot \\exp\\left(\\frac{i \\cdot \\ln(RR / b)}{N}\\right) - \\frac{RT_{max}}{RR}\\right) \\cdot \\left(\\frac{RT_{max} - RT_{max}/RR}{b \\cdot N \\cdot \\exp\\left(\\frac{i \\cdot \\ln(RR / b)}{N}\\right) - \\frac{RT_{max}}{RR}}\\right) - where :math:`b` = ``deluca__slope``, :math:`RT_{max}` = ``konstantin__max_threshold__ratio``, :math:`i = 1, 2, \\ldots, N` + $$rt(i) = \frac{RT_{max}}{RR} + \left(\frac{b \cdot i}{N} \cdot \exp\left(\frac{i \cdot \ln(RR / b)}{N}\right) - \frac{RT_{max}}{RR}\right) \cdot \left(\frac{RT_{max} - RT_{max}/RR}{b \cdot N \cdot \exp\left(\frac{i \cdot \ln(RR / b)}{N}\right) - \frac{RT_{max}}{RR}}\right)$$ + + where $b$ = ``deluca__slope``, $RT_{max}$ = ``konstantin__max_threshold__ratio``, $i = 1, 2, \ldots, N$ Examples -------- diff --git a/myogen/simulator/neuron/_cython/_poisson_process_generator.pyx b/myogen/simulator/neuron/_cython/_poisson_process_generator.pyx index e75ca844..19057351 100644 --- a/myogen/simulator/neuron/_cython/_poisson_process_generator.pyx +++ b/myogen/simulator/neuron/_cython/_poisson_process_generator.pyx @@ -1,87 +1,87 @@ # cython: language_level=3, boundscheck=False, wraparound=False -from libc.math cimport log +from libc.math cimport log, log1p from libc.stdint cimport uint64_t cdef class _PoissonProcessGenerator__Cython: """ - High-performance Cython implementation of a Poisson process generator for neural spike train simulation. - - This class implements an efficient algorithm for generating spike events from a time-varying - Poisson process, commonly used to model neural firing in response to continuous input currents. - The generator uses exponential inter-arrival times with adaptive thresholding to determine - spike timing based on accumulated input intensity. - - The implementation uses a custom xorshift64* random number generator for high performance - and reproducible results across different platforms. The algorithm accumulates input intensity - over time and compares against exponentially distributed thresholds to determine spike events. - + High-performance Cython implementation of a time-varying Poisson process + generator for neural spike train simulation. + + This class generates spike events from an inhomogeneous Poisson process, + commonly used to model neural firing in response to a continuous input + intensity (rate). It uses the integrated-intensity (time-rescaling) method: + the running integral of the input rate is accumulated over time, and a + spike is emitted whenever that integral crosses an exponentially + distributed threshold. After each spike the accumulator and the threshold + are reset. + + Because the inter-spike threshold is a single ``Exp(1)`` draw, the emitted + process is a discrete-time Poisson process (exact in the ``dt -> 0`` limit): + at a constant input rate the inter-spike intervals are exponentially + distributed with coefficient of variation ``CV = 1``. (At finite ``dt`` the + ISIs are quantised to multiples of ``dt`` and at most one spike is emitted + per step, so the fit is approximate for ``rate * dt`` not << 1.) For + deliberately regular, low-CV firing (e.g. muscle + afferents or cortical drive) use the Gamma generator instead — see + ``_GammaProcessGenerator__Cython`` / ``DD_Gamma``. + + The implementation uses a custom xorshift64* random number generator for + high performance and reproducible results across different platforms. + Parameters ---------- seed : uint64_t Random number generator seed for reproducible results. If 0, uses default seed. - N : int - Batch size for threshold generation. Higher values increase computational cost - but may improve statistical properties of the generated process. dt : double Time step in milliseconds for numerical integration of input intensity. Ninit : int, optional - Number of random numbers to pre-consume from generator, useful for - decorrelating parallel generators, by default 0. - + Number of random numbers to pre-consume from the generator before the + first threshold is drawn, by default 0. Useful for decorrelating + parallel generators seeded from nearby values. This only advances the + RNG state and does not change the Poisson statistics. + Attributes ---------- dt : double Time step in milliseconds for numerical integration. - N : int - Batch size for threshold generation, affecting statistical properties. yi : double - Accumulated input intensity since last spike event. - aux : double - Auxiliary variable for exponential threshold computation. + Accumulated input intensity since the last spike event. thres : double - Current exponential threshold for spike generation. + Current ``Exp(1)`` threshold for the next spike. spk : int Binary spike output (1 for spike, 0 for no spike). state : uint64_t Internal state of the xorshift64* random number generator. - + """ cdef double dt - cdef int N cdef double yi - cdef double aux cdef double thres cdef int spk cdef uint64_t state # C RNG state - def __init__(self, uint64_t seed, int N, double dt, int Ninit=0): + def __init__(self, uint64_t seed, double dt, int Ninit=0): self.dt = dt - self.N = N self.yi = 0.0 - self.aux = 1.0 - self.thres = 0.0 self.spk = 0 self.state = seed if seed != 0 else 0xDEADBEEFCAFEBABE - # pre-consume Ninit uniforms + # pre-consume Ninit uniforms to decorrelate parallel generators for _ in range(Ninit): self._rand_uniform() - # generate first exponential threshold - for _ in range(self.N): - self.aux *= self._rand_uniform() - - self.thres = -(1.0 / self.N) * log(self.aux) + # first inter-spike threshold ~ Exp(1) + self.thres = self._next_threshold() cdef double _rand_uniform(self): """ Generate uniform random number using xorshift64* algorithm. - + High-performance pseudo-random number generator that produces uniformly distributed values in the range [0, 1). The algorithm uses bitwise XOR and shift operations for excellent performance and statistical properties. - + Returns ------- double @@ -97,35 +97,54 @@ cdef class _PoissonProcessGenerator__Cython: return ((x * 2685821657736338717 & 0xFFFFFFFFFFFFFFFF)) / 18446744073709551616.0 + cdef double _next_threshold(self): + """ + Draw the next inter-spike integrated-intensity threshold. + + For a Poisson process the integrated intensity accumulated between two + consecutive spikes is ``Exp(1)``-distributed, sampled here by inverse + transform. ``_rand_uniform`` returns values in ``[0, 1)``, so ``-log1p(-U)`` + (i.e. ``-log(1 - U)``, evaluated more accurately for small ``U``) is a + finite ``Exp(1)`` draw; ``U = 0`` yields a zero threshold (an immediate + spike), which is harmless. + + Returns + ------- + double + A single exponentially distributed threshold with unit mean. + """ + return -log1p(-self._rand_uniform()) + cpdef int compute(self, double y): """ Compute spike output for given input intensity at current time step. - + Integrates the input intensity over the time step and compares the accumulated intensity against the current exponential threshold to determine if a spike should be generated. If a spike occurs, resets the accumulator and generates a new exponential threshold for the next inter-spike interval. - + Parameters ---------- y : double Input intensity (rate) in Hz at the current time step. This represents the instantaneous firing probability density function. - + Returns ------- int Binary spike output: 1 if spike occurs, 0 otherwise. - + Notes ----- The input intensity is integrated over the time step using Euler's method: yi += y * dt * 1e-3, where dt is in milliseconds and y is in Hz. - + When yi exceeds the current threshold, a spike is generated and both - yi and the threshold are reset. The new threshold is drawn from an - exponential distribution using the batch method for improved efficiency. - + yi and the threshold are reset. The new threshold is a single ``Exp(1)`` + draw, which makes the inter-spike intervals exponentially distributed + (CV = 1) — a discrete-time Poisson process (exact as ``dt -> 0``). + This method can be called repeatedly with time-varying input intensities to generate realistic Poisson spike trains that capture the temporal dynamics of neural firing patterns. @@ -136,11 +155,6 @@ cdef class _PoissonProcessGenerator__Cython: if self.yi >= self.thres: self.spk = 1 self.yi = 0.0 - self.aux = 1.0 - - for _ in range(self.N): - self.aux *= self._rand_uniform() - - self.thres = -(1.0 / self.N) * log(self.aux) + self.thres = self._next_threshold() return self.spk diff --git a/myogen/simulator/neuron/cells.py b/myogen/simulator/neuron/cells.py index e0e79c6b..f9629522 100644 --- a/myogen/simulator/neuron/cells.py +++ b/myogen/simulator/neuron/cells.py @@ -379,11 +379,13 @@ class DD(_Cell, _PoissonProcessGenerator__Cython): simulation of voluntary motor commands, reflex modulation, and other descending influences on spinal motor circuits. + The process is a discrete-time Poisson process (exponential inter-spike + intervals, CV = 1, exact as dt -> 0); the instantaneous rate is set by the + drive signal passed to `integrate`, not by a constructor argument. For regular, + low-CV firing use `DD_Gamma` instead. + Parameters ---------- - N : int - Maximum firing rate in Hz when input drive is at maximum. Determines - the scaling factor for converting drive signals to spike rates. dt : float Simulation time step in milliseconds. Must match the integration time step used in the main simulation loop. @@ -405,11 +407,11 @@ class DD(_Cell, _PoissonProcessGenerator__Cython): _ids2 = itertools.count(0) - def __init__(self, N, dt, pool__ID: int | None = None): + def __init__(self, dt, pool__ID: int | None = None): self.ns = h.DUMMY() # Dummy cell _Cell.__init__(self, next(self._ids2), pool__ID) _PoissonProcessGenerator__Cython.__init__( - self, derive_subseed(self.class__ID, self.global__ID), N, dt + self, derive_subseed(self.class__ID, self.global__ID), dt ) def __repr__(self) -> str: @@ -547,9 +549,10 @@ class AffIa(_Cell, _GammaProcessGenerator__Cython): RT : float Recruitment threshold - minimum input level required for activation. Represents the stretch sensitivity of the particular spindle ending. - N : int - Maximum firing rate in Hz when fully activated. Determines the - gain of the length-to-frequency transduction. + shape : int + Gamma shape parameter controlling ISI regularity: CV = 1/sqrt(shape). + Larger values give more regular (clock-like) firing. It does not set the + firing rate — the rate follows the input drive relative to RT. timestep__ms : Quantity__ms Simulation time step as a Quantity with units of milliseconds. initN : int, optional @@ -581,7 +584,7 @@ class AffIa(_Cell, _GammaProcessGenerator__Cython): def __init__( self, RT, - N, + shape, timestep__ms: Quantity__ms, initN=0, class__ID: Optional[int] = None, @@ -596,7 +599,7 @@ def __init__( _GammaProcessGenerator__Cython.__init__( self, seed=derive_subseed(self.class__ID, self.global__ID), - shape=N, # Shape parameter controls ISI CV = 1/sqrt(N) + shape=shape, # controls ISI CV = 1/sqrt(shape) dt=timestep__ms.magnitude, ) @@ -612,7 +615,7 @@ def integrate(self, y): Returns ------- int - Number of spikes generated (0 or 1) based on Poisson process + Number of spikes generated (0 or 1) based on a Gamma renewal process with rate determined by activation level above threshold. """ # NumPy 2.0 no longer auto-casts a single-element array (e.g. a neo @@ -642,8 +645,8 @@ class AffII(AffIa): ---------- RT : float Recruitment threshold for activation. - N : int - Maximum firing rate in Hz. + shape : int + Gamma shape parameter controlling ISI regularity: CV = 1/sqrt(shape). pool__ID : int, optional Pool identifier for muscle-specific grouping. *args, **kwargs @@ -660,8 +663,8 @@ class AffII(AffIa): _ids2 = itertools.count(0) - def __init__(self, RT, N, pool__ID: int | None = None, *args, **kwargs): - super().__init__(RT, N, class__ID=next(self._ids2), pool__ID=pool__ID, *args, **kwargs) + def __init__(self, RT, shape, pool__ID: int | None = None, *args, **kwargs): + super().__init__(RT, shape, class__ID=next(self._ids2), pool__ID=pool__ID, *args, **kwargs) @beartowertype @@ -684,8 +687,8 @@ class AffIb(AffIa): ---------- RT : float Force recruitment threshold for activation. - N : int - Maximum firing rate in Hz at full force. + shape : int + Gamma shape parameter controlling ISI regularity: CV = 1/sqrt(shape). pool__ID : int, optional Pool identifier for muscle-specific grouping. *args, **kwargs @@ -702,8 +705,8 @@ class AffIb(AffIa): _ids2 = itertools.count(0) - def __init__(self, RT, N, pool__ID: int | None = None, *args, **kwargs): - super().__init__(RT, N, class__ID=next(self._ids2), pool__ID=pool__ID, *args, **kwargs) + def __init__(self, RT, shape, pool__ID: int | None = None, *args, **kwargs): + super().__init__(RT, shape, class__ID=next(self._ids2), pool__ID=pool__ID, *args, **kwargs) # MOTORNEURON @@ -1012,8 +1015,8 @@ def reset_cell_id_counters() -> None: """Reset every per-class ``itertools.count`` counter to zero. Cell classes assign ``global__ID`` / ``class__ID`` from module-level - :class:`itertools.count` instances. Those counters live for the - interpreter's lifetime, so a second call to :func:`myogen.set_random_seed` + `itertools.count` instances. Those counters live for the + interpreter's lifetime, so a second call to `myogen.set_random_seed` would deterministically reseed the RNG but still hand out monotonically increasing IDs from where the previous run stopped -- breaking determinism across consecutive runs in the same process. diff --git a/myogen/simulator/neuron/network.py b/myogen/simulator/neuron/network.py index b9a447fa..666d7af3 100644 --- a/myogen/simulator/neuron/network.py +++ b/myogen/simulator/neuron/network.py @@ -42,7 +42,7 @@ def _to_float(value, unit: pq.Quantity) -> float: NEURON's NetCon attributes (``delay``, ``weight[0]``, ``threshold``) accept plain Python floats interpreted in NEURON's native units (ms, uS, mV). - Passing a :class:`pq.Quantity` directly invokes ``__float__``, which drops + Passing a `pq.Quantity` directly invokes ``__float__``, which drops the unit and returns the raw magnitude -- so a delay of ``1 * pq.s`` would be silently stored as ``1.0`` ms instead of ``1000.0`` ms. @@ -167,7 +167,7 @@ def _apply_default_synaptic_params(netcon: h.NetCon, source_neuron, synaptic_del Sets default weight, threshold, and delay, with optional axonal delay addition. All values are rescaled to NEURON's native units (ms, uS, mV) so the - function is safe to call with :class:`pq.Quantity` inputs in any unit. + function is safe to call with `pq.Quantity` inputs in any unit. Parameters ---------- @@ -1300,7 +1300,7 @@ def connect_one_to_one( Examples -------- >>> # Create independent noise for each motor neuron - >>> noise_pool = DescendingDrive__Pool(n=10, poisson_batch_size=16, timestep__ms=0.05) + >>> noise_pool = DescendingDrive__Pool(n=10, timestep__ms=0.05) >>> mn_pool = AlphaMN__Pool(n=10) >>> network = Network({"noise": noise_pool, "mn": mn_pool}) >>> network.connect_one_to_one("noise", "mn", weight__uS=0.5) @@ -1426,7 +1426,7 @@ def print_network(self): timestep__ms = 0.05 - dd__pool = DescendingDrive__Pool(n=2, poisson_batch_size=16, timestep__ms=timestep__ms) + dd__pool = DescendingDrive__Pool(n=2, timestep__ms=timestep__ms) n_type1 = 2 n_type2 = 2 diff --git a/myogen/simulator/neuron/populations/__init__.py b/myogen/simulator/neuron/populations/__init__.py index 68d814b8..849864fc 100644 --- a/myogen/simulator/neuron/populations/__init__.py +++ b/myogen/simulator/neuron/populations/__init__.py @@ -40,8 +40,7 @@ >>> motor_pool = AlphaMN__Pool(n=10, model="Powers2017", mode="active") >>> >>> # Create descending drive population with Poisson process (default) ->>> drive_pool = DescendingDrive__Pool(n=5, poisson_batch_size=16, -... timestep__ms=0.05) +>>> drive_pool = DescendingDrive__Pool(n=5, timestep__ms=0.05) >>> >>> # Create descending drive with Gamma process for more regular firing >>> drive_gamma = DescendingDrive__Pool(n=5, timestep__ms=0.05, process_type='gamma', diff --git a/myogen/simulator/neuron/populations/afferents.py b/myogen/simulator/neuron/populations/afferents.py index ef813b61..54b1ca35 100644 --- a/myogen/simulator/neuron/populations/afferents.py +++ b/myogen/simulator/neuron/populations/afferents.py @@ -34,8 +34,9 @@ class AffIa__Pool(_Pool): Min and max axon conduction velocities (m/s). axon_length__m : Quantity__m Length of the axon (m). - poisson_batch_size : int - Batch size for exponential threshold generation algorithm. + shape : int + Gamma shape parameter controlling ISI regularity: CV = 1/sqrt(shape). + Larger values give more regular firing. Does not set the firing rate. timestep__ms : Quantity__ms Time step for simulation (ms). init_order : int @@ -52,14 +53,14 @@ def __init__( 75 * pq.m / pq.s, ), axon_length__m: Quantity__m = 0.6 * pq.m, - poisson_batch_size: int = 145, # Shape param for Gamma process: CV = 1/sqrt(145) = 8.3% + shape: int = 145, # Gamma shape: CV = 1/sqrt(145) = 8.3% init_order: int = 0, ): self.n = n self.recruitment_thresholds = recruitment_thresholds self.axon_velocities = axon_velocities__m_per_s self.axon_length = axon_length__m - self.poisson_batch_size = poisson_batch_size + self.shape = shape self.timestep__ms = timestep__ms self.init_order = init_order @@ -70,7 +71,7 @@ def __init__( for i, (rt_i, vcon_i) in enumerate(zip(rt, vcon)): ia = cells.AffIa( RT=rt_i, - N=poisson_batch_size, + shape=shape, timestep__ms=timestep__ms, initN=init_order, pool__ID=i, @@ -99,8 +100,9 @@ class AffII__Pool(_Pool): Min and max axon conduction velocities (m/s). axon_length__m : Quantity__m Length of the axon (m). - poisson_batch_size : int - Batch size for exponential threshold generation algorithm. + shape : int + Gamma shape parameter controlling ISI regularity: CV = 1/sqrt(shape). + Larger values give more regular firing. Does not set the firing rate. timestep__ms : Quantity__ms Time step for simulation (ms). init_order : int @@ -117,14 +119,14 @@ def __init__( 50 * pq.m / pq.s, ), axon_length__m: Quantity__m = 0.6 * pq.m, - poisson_batch_size: int = 772, # Shape param for Gamma process: CV = 1/sqrt(772) = 3.6% + shape: int = 772, # Gamma shape: CV = 1/sqrt(772) = 3.6% init_order: int = 0, ): self.n = n self.recruitment_thresholds = recruitment_thresholds self.axon_velocities = axon_velocities__m_per_s self.axon_length = axon_length__m - self.poisson_batch_size = poisson_batch_size + self.shape = shape self.timestep__ms = timestep__ms self.init_order = init_order @@ -135,7 +137,7 @@ def __init__( for i, (rt_i, vcon_i) in enumerate(zip(rt, vcon)): ii = cells.AffII( RT=rt_i, - N=poisson_batch_size, + shape=shape, timestep__ms=timestep__ms, initN=init_order, pool__ID=i, @@ -160,13 +162,14 @@ class AffIb__Pool(_Pool): Number of type Ib afferent neurons to create. recruitment_thresholds : tuple[float, float] Min and max recruitment thresholds (Hz). - axon_velocities : tuple[float, float] + axon_velocities__m_per_s : tuple[Quantity__m_per_s, Quantity__m_per_s] Min and max axon conduction velocities (m/s). - axon_length : float + axon_length__mm : Quantity__mm Length of the axon (mm). - poisson_batch_size : int - Batch size for exponential threshold generation algorithm. - timestep__ms : float + shape : int + Gamma shape parameter controlling ISI regularity: CV = 1/sqrt(shape). + Larger values give more regular firing. Does not set the firing rate. + timestep__ms : Quantity__ms Time step for simulation (ms). init_order : int Initial order parameter for afferent initialization. @@ -182,14 +185,14 @@ def __init__( 72 * pq.m / pq.s, ), axon_length__mm: Quantity__mm = 0.6 * pq.mm, - poisson_batch_size: int = 145, # Shape param for Gamma process: CV = 1/sqrt(145) = 8.3% + shape: int = 145, # Gamma shape: CV = 1/sqrt(145) = 8.3% init_order: int = 0, ): self.n = n self.recruitment_thresholds = recruitment_thresholds self.axon_velocities = axon_velocities__m_per_s self.axon_length = axon_length__mm - self.poisson_batch_size = poisson_batch_size + self.shape = shape self.timestep__ms = timestep__ms self.init_order = init_order @@ -200,7 +203,7 @@ def __init__( for i, (rt_i, vcon_i) in enumerate(zip(rt, vcon)): ib = cells.AffIb( RT=rt_i, - N=poisson_batch_size, + shape=shape, timestep__ms=timestep__ms, initN=init_order, pool__ID=i, diff --git a/myogen/simulator/neuron/populations/base.py b/myogen/simulator/neuron/populations/base.py index a0aa3ad8..f8fc466e 100644 --- a/myogen/simulator/neuron/populations/base.py +++ b/myogen/simulator/neuron/populations/base.py @@ -61,7 +61,7 @@ def _exp_interp(first: float, last: float, n: int, curv: float = 0.33, negative: def _get_interneuron_diameter_range__um() -> tuple[float, float]: - """Estimate interneuron soma diameter range based on Biu et al. 2003 [1]_. + """Estimate interneuron soma diameter range based on Biu et al. 2003 [1]. Returns ------- @@ -70,7 +70,7 @@ def _get_interneuron_diameter_range__um() -> tuple[float, float]: References ---------- - .. [1] Bui, T.V., Cushing, S., Dewey, D., Fyffe, R.E., Rose, P.K., 2003. Comparison of the Morphological and Electrotonic Properties of Renshaw Cells, Ia Inhibitory Interneurons, and Motoneurons in the Cat. Journal of Neurophysiology 90, 2900–2918. https://doi.org/10.1152/jn.00533.2003 + [1] Bui, T.V., Cushing, S., Dewey, D., Fyffe, R.E., Rose, P.K., 2003. Comparison of the Morphological and Electrotonic Properties of Renshaw Cells, Ia Inhibitory Interneurons, and Motoneurons in the Cat. Journal of Neurophysiology 90, 2900–2918. https://doi.org/10.1152/jn.00533.2003 """ A_cell = 81390 + 3113 diff --git a/myogen/simulator/neuron/populations/descending_drive.py b/myogen/simulator/neuron/populations/descending_drive.py index c5c6422e..7f692780 100644 --- a/myogen/simulator/neuron/populations/descending_drive.py +++ b/myogen/simulator/neuron/populations/descending_drive.py @@ -24,16 +24,11 @@ class DescendingDrive__Pool(_Pool): ---------- n : int Number of descending drive neurons to create. - poisson_batch_size : int, optional - Batch size for exponential threshold generation algorithm (only used when - process_type="poisson"). Higher values improve statistical accuracy but - increase computation. Typical values: 16-50. - Required if process_type="poisson", ignored if process_type="gamma". - timestep__ms : float - Time step for simulation (ms). + timestep__ms : Quantity__ms + Time step for simulation as a Quantity with units of milliseconds (required). process_type : str, optional Type of point process: "poisson" or "gamma", by default "poisson". - - "poisson": Irregular firing (CV=1.0) + - "poisson": discrete-time Poisson process, irregular firing (CV=1.0, exact as dt->0) - "gamma": More regular firing with CV controlled by shape parameter shape : float, optional Shape parameter for Gamma process (only used when process_type="gamma"), @@ -46,7 +41,6 @@ class DescendingDrive__Pool(_Pool): def __init__( self, n: int, - poisson_batch_size: int | None = None, timestep__ms: Quantity__ms | None = None, process_type: str = "poisson", shape: float = 3.0, @@ -69,10 +63,7 @@ def __init__( for i in range(n) ] elif process_type.lower() == "poisson": - if poisson_batch_size is None: - raise ValueError("poisson_batch_size is required when process_type='poisson'") - self.poisson_batch_size = poisson_batch_size - _cells = [cells.DD(N=poisson_batch_size, dt=timestep__ms, pool__ID=i) for i in range(n)] + _cells = [cells.DD(dt=timestep__ms, pool__ID=i) for i in range(n)] else: raise ValueError( f"Invalid process_type '{process_type}'. Must be 'poisson' or 'gamma'." diff --git a/myogen/simulator/neuron/populations/interneurons.py b/myogen/simulator/neuron/populations/interneurons.py index 67a0ade4..76397edb 100644 --- a/myogen/simulator/neuron/populations/interneurons.py +++ b/myogen/simulator/neuron/populations/interneurons.py @@ -28,9 +28,9 @@ class GII__Pool(_Pool): n : int Number of group II interneurons to create. soma_length_range__um : tuple[float, float] - Min and max soma length (um). By default, it is set to the estimated range for interneurons from Bui et al. 2003 [1]_. + Min and max soma length (um). By default, it is set to the estimated range for interneurons from Bui et al. 2003 [1]. soma_diameter_range : tuple[float, float] - Min and max soma diameter (um). By default, it is set to the estimated range for interneurons from Bui et al. 2003 [1]_. + Min and max soma diameter (um). By default, it is set to the estimated range for interneurons from Bui et al. 2003 [1]. passive_conductance_range : tuple[float, float] Min and max passive membrane conductance (S/cm²). na3rp_conductance_range : tuple[float, float] @@ -54,7 +54,7 @@ class GII__Pool(_Pool): References ---------- - .. [1] Bui, T.V., Cushing, S., Dewey, D., Fyffe, R.E., Rose, P.K., 2003. Comparison of the Morphological and Electrotonic Properties of Renshaw Cells, Ia Inhibitory Interneurons, and Motoneurons in the Cat. Journal of Neurophysiology 90, 2900–2918. https://doi.org/10.1152/jn.00533.2003 + [1] Bui, T.V., Cushing, S., Dewey, D., Fyffe, R.E., Rose, P.K., 2003. Comparison of the Morphological and Electrotonic Properties of Renshaw Cells, Ia Inhibitory Interneurons, and Motoneurons in the Cat. Journal of Neurophysiology 90, 2900–2918. https://doi.org/10.1152/jn.00533.2003 """ diff --git a/myogen/simulator/neuron/populations/motor_neurons.py b/myogen/simulator/neuron/populations/motor_neurons.py index 561983f1..8b28aa1c 100644 --- a/myogen/simulator/neuron/populations/motor_neurons.py +++ b/myogen/simulator/neuron/populations/motor_neurons.py @@ -41,6 +41,8 @@ class AlphaMN__Pool(_Pool): By default uses "alpha_mn_default.yaml". model : str, optional Motor neuron model type ("NERLab" or "Powers2017"), by default "NERLab". + When ``model="Powers2017"``, the soma/dendrite parameters listed under + **Other Parameters** below become available. mode : str, optional Simulation mode ("active" or "passive"), by default "active". axon_velocities : tuple[float, float], optional @@ -59,10 +61,10 @@ class AlphaMN__Pool(_Pool): Spike detection threshold for recording motor neuron spikes, by default 50.0. Motor neurons have large action potentials (80-100 mV) requiring higher thresholds. - Powers2017 Model Parameters (required when model="Powers2017") - -------------------------------------------------------- + Other Parameters + ---------------- soma_length_range : tuple[float, float, float], optional - Soma length [min, max, curve] (um). + Soma length [min, max, curve] (um). Only used when ``model="Powers2017"``. soma_diameter_range : tuple[float, float, float], optional Soma diameter [min, max, curve] (um). soma_capacitance_range : tuple[float, float, float], optional diff --git a/myogen/utils/emg_noise.py b/myogen/utils/emg_noise.py index d5303055..0cbcff1b 100644 --- a/myogen/utils/emg_noise.py +++ b/myogen/utils/emg_noise.py @@ -297,7 +297,7 @@ def _emg_band_shape( Notes ----- The analog HPF that strips infra-low drift is handled in a separate - explicit stage inside :func:`generate_realistic_noise` + explicit stage inside `generate_realistic_noise` (``analog_hpf_hz``), so this function performs only the band emphasis. Stacking two HPFs here would double the rolloff order and cut the PSD below the cutoff far steeper than any real device. @@ -350,7 +350,7 @@ def _baseline_drift( sees the same amplitude they requested. A downstream user-applied HPF will attenuate it further (realistic). - Public input validation lives in :func:`generate_realistic_noise`; + Public input validation lives in `generate_realistic_noise`; this helper assumes its arguments have already been checked. """ if target_rms == 0 or n_samples < 4: @@ -450,27 +450,25 @@ def generate_realistic_noise( ``high_hz=1.0`` are the midpoint and upper bound of the reported electrode-noise regime, not validated physiological constants for intramuscular EMG. Calibrate against real - recordings via :func:`calibrate_baseline_drift_profile` if + recordings via `calibrate_baseline_drift_profile` if you need amplitude-accurate simulation. Movement artifacts (broadband, 0–20 Hz per De Luca et al. 2010, DOI 10.1016/j.jbiomech.2010.01.027) are a separate phenomenon and would need their own contaminant model. - .. note:: - **SNR contract**: drift is **additive on top** of the - broadband noise. ``noise_rms`` still controls the - broadband floor exactly, but enabling drift makes the - total noise RMS slightly higher - (``sqrt(noise_rms**2 + drift_rms**2)``). If you are - using ``snr__dB`` upstream to hit a target SNR, factor - this in or leave drift at 0. - - .. note:: - When called from a multi-channel context (e.g. - :func:`add_realistic_noise`), each channel receives an - **independent** drift realisation. Real electrode-array - drift is often partially common-mode across nearby - electrodes; this model does not capture that correlation. + Note: **SNR contract**: drift is **additive on top** of the + broadband noise. ``noise_rms`` still controls the + broadband floor exactly, but enabling drift makes the + total noise RMS slightly higher + (``sqrt(noise_rms**2 + drift_rms**2)``). If you are + using ``snr__dB`` upstream to hit a target SNR, factor + this in or leave drift at 0. + + Note: When called from a multi-channel context (e.g. + `add_realistic_noise`), each channel receives an + **independent** drift realisation. Real electrode-array + drift is often partially common-mode across nearby + electrodes; this model does not capture that correlation. baseline_drift_alpha : float, default 1.75 PSD slope α (positive number) for ``PSD ∝ 1/f^α``. Must be > 0. The literature regime is α ∈ [1.5, 2.0]; the default @@ -662,17 +660,17 @@ def add_realistic_noise( Per-harmonic amplitude ratios relative to the fundamental. powerline_frequency_drift_hz : float, default 0.3 STD of the slow drift of the mains instantaneous frequency. - See :func:`generate_realistic_noise`. + See `generate_realistic_noise`. powerline_amplitude_modulation_depth : float, default 0.15 Fractional AM depth on the powerline carriers. See - :func:`generate_realistic_noise`. + `generate_realistic_noise`. peak_hz : float, default 750.0 Center of EMG-band spectral emphasis. analog_hpf_hz : float, default 10.0 Analog HPF cutoff applied before powerline is mixed in. baseline_drift_rms_uv : float, default 0.0 Target RMS of the band-limited 1/f^α drift (off by default). - See :func:`generate_realistic_noise` for the paper-constrained + See `generate_realistic_noise` for the paper-constrained spectral form and calibration guidance. Each channel receives an independent drift realisation; common-mode array drift is not modelled. @@ -832,8 +830,8 @@ def calibrate_realistic_noise_profile( post-notch RMS in a ±1 Hz band around each harmonic. The output dict is shaped to be unpacked straight into - :func:`generate_realistic_noise` / - :meth:`myogen.simulator.IntramuscularEMG.add_noise` so the simulator + `generate_realistic_noise` / + `myogen.simulator.IntramuscularEMG.add_noise` so the simulator will reproduce the same noise statistics as the real recording. Parameters @@ -861,7 +859,7 @@ def calibrate_realistic_noise_profile( Returns ------- dict - Keys ready to splat into :func:`generate_realistic_noise`: + Keys ready to splat into `generate_realistic_noise`: * ``noise_floor_uv`` — RMS of the noise residual (µV). * ``spectral_slope`` — PSD slope in log-log space above the peak. @@ -1110,10 +1108,10 @@ def calibrate_baseline_drift_profile( """Estimate baseline-drift parameters from a real recording. Fits the band-limited 1/f^α drift model - (see :func:`generate_realistic_noise`) to the low-frequency PSD + (see `generate_realistic_noise`) to the low-frequency PSD of ``real_signal_uv`` over ``band = (low_hz, high_hz)``. Returns a dict with the four ``baseline_drift_*`` kwargs that - :func:`generate_realistic_noise` and downstream APIs accept, so + `generate_realistic_noise` and downstream APIs accept, so the user can splat it directly: >>> profile = calibrate_baseline_drift_profile(real_uv, fs_hz) @@ -1123,12 +1121,11 @@ def calibrate_baseline_drift_profile( signal** (µV when the input is in µV); the function does *not* standardise the channels because that would erase the scale. - .. note:: - The model's spectral form (PSD ∝ 1/f^α over a bounded band) - is paper-constrained for electrode/interface noise (Huigen - 2002, Gondran 1996). The fitted parameters become validated - only when this calibration is run on real recordings — they - are not pre-baked physiological constants. + Note: The model's spectral form (PSD ∝ 1/f^α over a bounded band) + is paper-constrained for electrode/interface noise (Huigen + 2002, Gondran 1996). The fitted parameters become validated + only when this calibration is run on real recordings — they + are not pre-baked physiological constants. Parameters ---------- @@ -1260,7 +1257,7 @@ def tune_noise_profile_with_optuna( ) -> dict: """Refine a noise profile with Optuna to minimise PSD distance vs real. - Closed-form calibration (:func:`calibrate_realistic_noise_profile`) + Closed-form calibration (`calibrate_realistic_noise_profile`) matches the first few moments and spectral summary statistics but can leave residual shape mismatch — most visibly the low-frequency rolloff curvature and the line-peak shape. This function takes the @@ -1279,7 +1276,7 @@ def tune_noise_profile_with_optuna( fs_hz : float Sampling rate. initial_profile : dict - Output of :func:`calibrate_realistic_noise_profile`. Used as + Output of `calibrate_realistic_noise_profile`. Used as starting point + to define the search bounds. rest_mask : ndarray of bool, optional Per-sample rest mask. If provided, the reference PSD is built diff --git a/myogen/utils/neuron/inject_currents_into_populations.py b/myogen/utils/neuron/inject_currents_into_populations.py index e3158fe2..a68e82f9 100644 --- a/myogen/utils/neuron/inject_currents_into_populations.py +++ b/myogen/utils/neuron/inject_currents_into_populations.py @@ -1,4 +1,3 @@ -import neuron import numpy as np import quantities as pq from beartype.typing import Sequence @@ -166,7 +165,11 @@ def inject_currents_and_simulate_spike_trains( # Initialize and run the NEURON simulation h.finitialize() # Use default initialization, voltages already set above - neuron.run(simulation_time__ms) + # Advance the solver to the end of the simulation (replaces the deprecated + # neuron.run(); finitialize() above already set the initial state). + h.tstop = float(simulation_time__ms) + while h.t < h.tstop: + h.fadvance() # Convert spike data to neo.Block format block = Block() diff --git a/properdocs.yml b/properdocs.yml new file mode 100644 index 00000000..3e881a3f --- /dev/null +++ b/properdocs.yml @@ -0,0 +1,138 @@ +site_name: MyoGen +site_description: Modular neuromuscular simulation framework for motor-unit activity, force, and EMG. +site_url: https://nsquaredlab.github.io/MyoGen/ +repo_url: https://github.com/NsquaredLab/MyoGen +repo_name: MyoGen +edit_uri: edit/main/docs/ +copyright: Copyright © 2025-2026 n-squared lab, FAU Erlangen-Nürnberg + +# superpowers specs/plans live under docs/ but are not site pages +exclude_docs: | + superpowers/ + +# Never ship Jupyter notebooks: strip the gallery's "Download Jupyter notebook" +# button and delete every generated .ipynb from the built site. +hooks: + - docs/hooks/no_notebooks.py + +extra_css: + - stylesheets/gallery.css + +extra_javascript: + - javascripts/mathjax.js + - https://unpkg.com/mathjax@3/es5/tex-mml-chtml.js + +theme: + name: material + features: + - navigation.instant + - navigation.tracking + - navigation.tabs + - navigation.tabs.sticky + - navigation.sections + - navigation.indexes + - navigation.top + - search.suggest + - search.highlight + - content.code.copy + - content.code.annotate + - toc.follow + palette: + - media: "(prefers-color-scheme: light)" + scheme: default + primary: white + accent: indigo + toggle: + icon: material/brightness-7 + name: Switch to dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: black + accent: indigo + toggle: + icon: material/brightness-4 + name: Switch to light mode + font: + text: Inter + code: JetBrains Mono + icon: + repo: fontawesome/brands/github + +plugins: + - search + - section-index + - gallery: + conf_script: docs/gallery_conf.py + # Source-only by default: examples render as markdown (prose + code) with + # "download full example code" links, but are NOT executed — many MyoGen + # examples use multiprocessing/joblib (which breaks under gallery exec) or + # form slow inter-dependent chains. Opt in to execution locally with + # MKDOCS_GALLERY_PLOT=true. + plot_gallery: !ENV [MKDOCS_GALLERY_PLOT, false] + - mkdocstrings: + handlers: + python: + paths: [.] + inventories: + - https://docs.python.org/3/objects.inv + - https://numpy.org/doc/stable/objects.inv + - https://docs.scipy.org/doc/scipy/objects.inv + options: + docstring_style: numpy + show_source: true + show_root_heading: true + show_root_full_path: false + members_order: source + separate_signature: true + show_signature_annotations: true + signature_crossrefs: true + merge_init_into_class: true + heading_level: 2 + filters: + - "!^_" + +markdown_extensions: + - admonition + - attr_list + - pymdownx.arithmatex: + generic: true + - def_list + - md_in_html + - tables + - toc: + permalink: true + - pymdownx.details + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format + - pymdownx.tabbed: + alternate_style: true + - pymdownx.highlight: + anchor_linenums: true + pygments_lang_class: true + - pymdownx.inlinehilite + - pymdownx.magiclink + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + +nav: + - Home: index.md + - Getting Started: getting-started.md + - Neo blocks: neo-blocks.md + - Examples: + - Basics: auto_examples/01_basic/index.md + - Fine-tuning: auto_examples/02_finetune/index.md + - Watanabe (paper): auto_examples/03_papers/watanabe/index.md + - Clinical: auto_examples/04_clinical/index.md + - API reference: + - api/index.md + - Top-level: api/myogen.md + - Simulator: api/simulator.md + - Currents: api/currents.md + - Injection & I/O: api/io-and-neuron.md + - Plotting: api/plotting.md + - Utilities: api/utils.md + - Types: api/types.md diff --git a/pyproject.toml b/pyproject.toml index 7ca3ac90..1f43e1fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,7 @@ dependencies = [ nwb = [ "pynwb>=2.8.0", "nwbinspector>=0.5.0", + "h5py>=3.0", ] gpu = [ "cupy-cuda12x>=13.0,<14", @@ -79,23 +80,11 @@ dev = [ "scipy-stubs>=1.16.1.0", ] docs = [ - "enum-tools[sphinx]>=0.12.0", - "linkify-it-py>=2.0.3", - "memory-profiler>=0.61.0", - "pydata-sphinx-theme>=0.16.1", - "pygments>=2.19.2", - "rinohtype>=0.5.5", - "roman>=5.2", - "sphinx>=8.1.3,<9", - "sphinx-autodoc-typehints>=2.5.0", - "sphinx-design>=0.6.1", - "sphinx-gallery>=0.19.0", - "sphinx-hoverxref>=1.4.1", - "sphinxcontrib-mermaid>=1.0.0", - "toml>=0.10.2", - # NWB support for examples - "pynwb>=2.8.0", - "nwbinspector>=0.5.0", + "properdocs>=1.6.7", + "mkdocs-material>=9.5", + "mkdocstrings[python]>=0.27", + "mkdocs-section-index>=0.3", + "mkdocs-gallery>=0.10", ] [tool.pytest.ini_options] diff --git a/scripts/build_docs.py b/scripts/build_docs.py new file mode 100644 index 00000000..5c2e5ecc --- /dev/null +++ b/scripts/build_docs.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +"""Build the MyoGen documentation, resuming the example gallery past segfaults. + +mkdocs-gallery executes every example sequentially in one long-lived process. +MyoGen's examples drive NEURON, whose native runtime state accumulates and +eventually segfaults the build (there is no per-example process isolation in +mkdocs-gallery). mkdocs-gallery md5-caches each *successfully executed* example +into ``docs/auto_examples``, which persists between invocations, so re-running +resumes past the crash in a fresh process with less accumulated state. This is +the same reason the old Sphinx workflow ran ``make html || make html``. + +This wrapper therefore retries the build a bounded number of times, but with +guards so it can never (a) loop forever, or (b) deploy an incomplete gallery: + +* After every attempt the cache is validated. A NEURON SIGSEGV happens *during* + execution, before mkdocs-gallery writes the ``.py.md5`` stamp, so a crashing + example is never stamped and simply re-runs. But mkdocs-gallery writes the + stamp just before the markdown/figures, so — defensively — any stamp whose + rendered outputs are missing is deleted so that example re-runs. +* If a *failed* attempt produced no new complete examples, we abort loudly + rather than spin. +* A build that exits 0 but reports an example that "failed to execute" (a plain + Python error, which retrying will not fix) is a hard failure — we do not ship + a broken page. +* On success every executable example must be present with all of its figures. + +Run it from inside the docs environment so ``properdocs`` is on PATH:: + + MKDOCS_GALLERY_PLOT=true uv run --group docs python scripts/build_docs.py + +Without ``MKDOCS_GALLERY_PLOT`` the gallery is source-only and one attempt +suffices. +""" + +from __future__ import annotations + +import os +import re +import subprocess +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +EXAMPLES = ROOT / "examples" +GALLERY_OUT = ROOT / "docs" / "auto_examples" +SUBDIRS = ["01_basic", "02_finetune", "03_papers/watanabe", "04_clinical"] +# Only these subdirs are executed (must match filename_pattern in +# docs/gallery_conf.py); watanabe/clinical render source-only, so they are not +# expected to produce figures and are not part of the completeness target. +EXECUTE_SUBDIRS = ["01_basic", "02_finetune"] +# keep in sync with ignore_pattern in docs/gallery_conf.py +IGNORE = re.compile( + r"(14_calibrate_noise_from_real|_oscillating_dc_helpers|_optimize_dc_worker|_pic_protocols)\.py" +) +MAX_ATTEMPTS = 12 # heavy watanabe/clinical tail clears ~1-2 examples per fresh process +# markdown image sources look like: ![alt](./images/mkd_glr__001.png){...} +_IMG_SRC = re.compile(r"!\[[^\]]*\]\((\.?/?images/[^)]+\.(?:png|svg))\)") +# mkdocs-gallery logs: "/.py failed to execute correctly: Traceback" +_FAILED = re.compile(r"([^\s/\\]+)\.py failed to execute correctly") + + +def executable_examples() -> set[str]: + """Names of example .py files that the gallery is expected to execute.""" + names: set[str] = set() + for sub in EXECUTE_SUBDIRS: + for py in sorted((EXAMPLES / sub).glob("*.py")): + if not IGNORE.search(py.name): + names.add(py.stem) + return names + + +def validate_cache() -> tuple[set[str], list[str]]: + """Return (complete example names, deleted-as-incomplete names). + + An example is complete when its ``.py.md5`` stamp, its rendered ``.md`` and + every image the ``.md`` references all exist and are non-empty. Incomplete + stamps are deleted so the example re-runs on the next attempt. + """ + complete: set[str] = set() + dropped: list[str] = [] + for sub in SUBDIRS: + gdir = GALLERY_OUT / sub + if not gdir.exists(): + continue + for stamp in gdir.rglob("*.py.md5"): + name = stamp.name[: -len(".py.md5")] + md = stamp.parent / f"{name}.md" + ok = md.exists() and md.stat().st_size > 0 + if ok: + for ref in _IMG_SRC.findall(md.read_text(errors="replace")): + img = stamp.parent / ref.lstrip("./") + if not img.exists() or img.stat().st_size == 0: + ok = False + break + if ok: + complete.add(name) + else: + stamp.unlink(missing_ok=True) + dropped.append(name) + return complete, dropped + + +def drop_stamps(names: set[str]) -> None: + """Delete the .py.md5 stamps for the given examples so they re-run.""" + for sub in SUBDIRS: + for name in names: + for stamp in (GALLERY_OUT / sub).rglob(f"{name}.py.md5"): + stamp.unlink(missing_ok=True) + + +def run_build() -> tuple[int, set[str]]: + """Run one ``properdocs build``; return (exit code, names of failed examples).""" + proc = subprocess.run( + ["properdocs", "build"], + cwd=ROOT, + env={**os.environ}, + text=True, + capture_output=True, + ) + # stream a trimmed tail so CI logs stay useful without the tqdm flood + sys.stdout.write(proc.stdout[-6000:]) + sys.stderr.write(proc.stderr[-3000:]) + sys.stdout.flush() + failed = set(_FAILED.findall(proc.stdout + proc.stderr)) + return proc.returncode, failed + + +def main() -> int: + target = executable_examples() + executing = os.environ.get("MKDOCS_GALLERY_PLOT", "").lower() in {"1", "true", "yes"} + print(f"docs build: {len(target)} executable examples; " + f"gallery execution {'ON' if executing else 'OFF (source-only)'}") + + prev_complete = -1 + for attempt in range(1, MAX_ATTEMPTS + 1): + print(f"::group::docs build attempt {attempt}", flush=True) + code, failed = run_build() + print("::endgroup::", flush=True) + + # A failed example is untrustworthy even if it got a stamp (mkdocs-gallery + # renders a broken page): drop its stamp so it re-executes, and don't + # count it complete. A deterministically-failing example then shows up as + # "no progress" below rather than a silent broken page in the deploy. + drop_stamps(failed) + complete, dropped = validate_cache() + rerun = sorted(failed | set(dropped)) + if rerun: + print(f" will re-run {len(rerun)} example(s): {', '.join(rerun)}") + print(f"attempt {attempt}: exit={code} failed={sorted(failed) or 'none'} " + f"complete={len(complete)}/{len(target)}", flush=True) + + if code == 0 and not failed: + missing = target - complete if executing else set() + if missing: + print(f"::error::build reported success but these examples are " + f"missing figures/pages: {', '.join(sorted(missing))}") + return 1 + print(f"docs build complete on attempt {attempt}.") + return 0 + + # crashed (segfault) or an example failed: retry only if making progress + if not executing: + print("::error::source-only build failed (not a gallery segfault).") + return code + if len(complete) <= prev_complete: + print(f"::error::no progress on attempt {attempt} " + f"({len(complete)} <= {prev_complete} complete). A deterministically " + f"failing example won't be fixed by retrying. Aborting.") + return 1 + prev_complete = len(complete) + + print(f"::error::gallery did not complete after {MAX_ATTEMPTS} attempts " + f"({prev_complete}/{len(target)} examples).") + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_poisson_process.py b/tests/test_poisson_process.py new file mode 100644 index 00000000..82ffa0bf --- /dev/null +++ b/tests/test_poisson_process.py @@ -0,0 +1,249 @@ +"""Statistical validation that the descending drive emits a *true* Poisson process. + +Regression guard for the bug where ``poisson_batch_size`` / ``N > 1`` silently turned the +"Poisson" generator into a ``Gamma(N, N)`` renewal process (ISI CV = ``1/sqrt(N)`` instead +of 1). The generator now draws a single ``Exp(1)`` inter-spike threshold, so at a constant +drive the inter-spike intervals (ISIs) must be exponentially distributed with CV = 1. + +The tests exercise the process end-to-end through the public API — a real +``DescendingDrive__Pool`` whose cells are driven with ``cell.integrate(rate)`` exactly as the +simulation loop does — and check every defining property of a homogeneous Poisson process: + +* the ISIs are exponentially distributed (KS goodness-of-fit), +* CV(ISI) = 1, +* spike counts in fixed windows are Poisson-distributed (Fano factor = 1), +* the ISIs are serially independent (memoryless / renewal), +* the mean rate matches the requested drive. + +The Gamma process (``process_type="gamma"``) is validated symmetrically: its ISIs are shown to +follow a *genuine* Gamma(shape) distribution (shape recovered, KS against Gamma not rejected, KS +against a wrong shape rejected) and to be rejected as Exponential — both as a check in its own +right and to prove the Poisson tests have discriminating power. A kernel-level layer localises any +failure to the public path vs the Cython generator. +""" + +from __future__ import annotations + +import numpy as np +import pytest +import quantities as pq +from scipy import stats + +import myogen +from myogen.simulator.neuron import cells +from myogen.simulator.neuron._cython._poisson_process_generator import ( + _PoissonProcessGenerator__Cython, +) +from myogen.simulator.neuron.populations.descending_drive import DescendingDrive__Pool + +DT_MS = 0.1 +SEED = 42 +RATE_HZ = 25.0 +DURATION_MS = 30_000.0 +N_CELLS = 20 + + +def _drive_pool(pool, rate_hz, duration_ms): + """Drive every cell in ``pool`` at a constant rate, as the simulation loop does. + + Returns per-cell lists of spike step-indices. ``cell.integrate`` advances the cell's own + generator by its internal ``dt``, so a plain Python loop reproduces the real spike-generation + path without needing NEURON's ``fadvance`` (which only steps the postsynaptic dynamics). + """ + n = len(list(pool)) + n_steps = int(duration_ms / DT_MS) + spike_steps = [[] for _ in range(n)] + for step in range(n_steps): + for cell in pool: + if cell.integrate(rate_hz): + spike_steps[cell.pool__ID].append(step) + return spike_steps + + +def _pooled_isis(spike_steps): + """Concatenate per-cell ISIs (seconds); each cell's ISIs are i.i.d. so pooling is valid.""" + dt_s = DT_MS * 1e-3 + per_cell = [np.diff(np.asarray(s, dtype=float) * dt_s) for s in spike_steps] + return np.concatenate([d for d in per_cell if d.size > 2]) + + +# -------------------------------------------------------------------------------------------- +# End-to-end: a real DescendingDrive__Pool with process_type="poisson" +# -------------------------------------------------------------------------------------------- + + +@pytest.fixture(scope="module") +def poisson_pool_spikes(): + myogen.set_random_seed(SEED) + pool = DescendingDrive__Pool( + n=N_CELLS, timestep__ms=DT_MS * pq.ms, process_type="poisson" + ) + return _drive_pool(pool, RATE_HZ, DURATION_MS) + + +def test_pool_mean_rate_matches_drive(poisson_pool_spikes): + isis = _pooled_isis(poisson_pool_spikes) + assert 1.0 / isis.mean() == pytest.approx(RATE_HZ, rel=0.05) + + +def test_pool_isi_cv_is_one(poisson_pool_spikes): + """Exponential ISIs have CV = 1. The old Gamma(N, N) bug gave CV = 1/sqrt(N) (0.45 at N=5).""" + isis = _pooled_isis(poisson_pool_spikes) + assert isis.std() / isis.mean() == pytest.approx(1.0, abs=0.05) + + +def test_pool_isis_are_exponential(poisson_pool_spikes): + """KS goodness-of-fit: pooled DD ISIs must not be distinguishable from Exponential(mean).""" + isis = _pooled_isis(poisson_pool_spikes) + p_value = stats.kstest(isis, "expon", args=(0.0, isis.mean())).pvalue + assert p_value > 0.01, f"pooled DD ISIs reject the Exponential hypothesis (KS p={p_value:.4f})" + + +def test_pool_spike_counts_are_poisson(poisson_pool_spikes): + """Counts in fixed 100 ms windows are Poisson-distributed: mean Fano factor (var/mean) ~ 1.""" + window = int(100.0 / DT_MS) + n_steps = int(DURATION_MS / DT_MS) + fanos = [] + for steps in poisson_pool_spikes: + if len(steps) < 50: + continue + train = np.zeros(n_steps, dtype=np.int64) + train[np.asarray(steps, dtype=int)] = 1 + counts = train[: (n_steps // window) * window].reshape(-1, window).sum(axis=1) + fanos.append(counts.var() / counts.mean()) + assert np.mean(fanos) == pytest.approx(1.0, abs=0.1) + + +def test_pool_spike_counts_follow_poisson_distribution(poisson_pool_spikes): + """Chi-square goodness-of-fit: the full distribution of spike counts in 100 ms windows must + not be distinguishable from a Poisson(lambda) distribution (lambda = mean count). This is the + count-domain counterpart to the KS-on-ISIs test and the direct definition of "Poisson". + """ + window = int(100.0 / DT_MS) + n_steps = int(DURATION_MS / DT_MS) + n_windows = n_steps // window + per_cell_counts = [] + for steps in poisson_pool_spikes: + train = np.zeros(n_steps, dtype=np.int64) + if steps: + train[np.asarray(steps, dtype=int)] = 1 + per_cell_counts.append(train[: n_windows * window].reshape(-1, window).sum(axis=1)) + counts = np.concatenate(per_cell_counts) + + lam = counts.mean() + kmax = int(counts.max()) + observed = np.bincount(counts, minlength=kmax + 1).astype(float) + # Expected Poisson(lambda) frequencies. The final (k = kmax) bin carries the ENTIRE upper + # tail P(X > kmax) via the survival function, so the expected probabilities sum to 1 — an + # unconditional goodness-of-fit, not one conditioned away above the observed maximum. + expected = stats.poisson.pmf(np.arange(kmax + 1), lam) + expected[-1] += stats.poisson.sf(kmax, lam) + expected = expected * counts.size + + # Merge bins from the bottom so every expected frequency >= 5 (chi-square validity condition). + obs_binned, exp_binned, acc_o, acc_e = [], [], 0.0, 0.0 + for o, e in zip(observed, expected): + acc_o += o + acc_e += e + if acc_e >= 5: + obs_binned.append(acc_o) + exp_binned.append(acc_e) + acc_o = acc_e = 0.0 + if acc_e > 0: # fold any remainder into the last bin + obs_binned[-1] += acc_o + exp_binned[-1] += acc_e + obs_binned = np.asarray(obs_binned) + exp_binned = np.asarray(exp_binned) + exp_binned *= obs_binned.sum() / exp_binned.sum() # float-safety only; totals already match + + # ddof=1 because lambda was estimated from the data. + p_value = stats.chisquare(obs_binned, exp_binned, ddof=1).pvalue + assert p_value > 0.01, f"spike counts reject the Poisson distribution (chi2 p={p_value:.4f})" + + +def test_pool_isis_are_serially_independent(poisson_pool_spikes): + """A memoryless (renewal) Poisson process has independent ISIs: lag-1 autocorrelation ~ 0.""" + isis = _pooled_isis(poisson_pool_spikes) + x = isis - isis.mean() + lag1 = float((x[:-1] * x[1:]).sum() / (x * x).sum()) + assert abs(lag1) < 0.05 + + +def test_gamma_pool_follows_gamma_distribution(): + """End-to-end Gamma validation through the public pool: process_type="gamma", shape=5 must + produce *genuine* Gamma(5) ISIs — CV = 1/sqrt(5) and a KS test against Gamma(5) that does not + reject — while being emphatically rejected as Exponential (i.e. not Poisson). This is exactly + the old poisson_batch_size=5 behaviour, now only reachable explicitly, and it doubles as proof + that the Poisson checks above have discriminating power. + """ + myogen.set_random_seed(SEED) + shape = 5.0 + pool = DescendingDrive__Pool( + n=N_CELLS, timestep__ms=DT_MS * pq.ms, process_type="gamma", shape=shape + ) + isis = _pooled_isis(_drive_pool(pool, RATE_HZ, DURATION_MS)) + mean = isis.mean() + assert isis.std() / mean == pytest.approx(1.0 / np.sqrt(shape), abs=0.05) + assert stats.kstest(isis, "gamma", args=(shape, 0.0, mean / shape)).pvalue > 0.01 # fits Gamma(5) + assert stats.kstest(isis, "expon", args=(0.0, mean)).pvalue < 0.01 # not Exponential/Poisson + + +# -------------------------------------------------------------------------------------------- +# Afferents: the renamed `shape` parameter must set the Gamma shape (CV = 1/sqrt(shape)) +# -------------------------------------------------------------------------------------------- + + +@pytest.mark.parametrize("shape", [4, 9]) +def test_afferent_shape_produces_gamma_isis(shape): + """The renamed `shape` param on afferents must set the Gamma shape: an AffIa cell's ISIs + follow Gamma(shape) (KS not rejected) with CV = 1/sqrt(shape).""" + myogen.set_random_seed(SEED) + cell = cells.AffIa(RT=0.0, shape=shape, timestep__ms=DT_MS * pq.ms) + n_steps = int(DURATION_MS / DT_MS) + steps = [i for i in range(n_steps) if cell.integrate(RATE_HZ)] + isis = np.diff(np.asarray(steps, dtype=float) * DT_MS * 1e-3) + mean = isis.mean() + assert isis.std() / mean == pytest.approx(1.0 / np.sqrt(shape), abs=0.05) + assert stats.kstest(isis, "gamma", args=(shape, 0.0, mean / shape)).pvalue > 0.01 + + +# -------------------------------------------------------------------------------------------- +# Kernel-level precision checks on the raw Cython generator +# -------------------------------------------------------------------------------------------- + + +def _generator_isis(generator, rate_hz, duration_ms=200_000.0): + n_steps = int(duration_ms / DT_MS) + spikes = np.fromiter( + (generator.compute(rate_hz) for _ in range(n_steps)), dtype=np.int8, count=n_steps + ) + return np.diff(np.flatnonzero(spikes) * DT_MS * 1e-3) + + +@pytest.mark.parametrize("rate_hz", [10.0, 50.0]) +def test_generator_is_true_poisson(rate_hz): + isis = _generator_isis(_PoissonProcessGenerator__Cython(20260710, DT_MS), rate_hz) + assert 1.0 / isis.mean() == pytest.approx(rate_hz, rel=0.02) + assert isis.std() / isis.mean() == pytest.approx(1.0, abs=0.03) + assert stats.kstest(isis, "expon", args=(0.0, isis.mean())).pvalue > 0.01 + + +@pytest.mark.parametrize("shape", [2.0, 5.0, 10.0]) +def test_gamma_generator_follows_gamma_distribution(shape): + """Positive goodness-of-fit that the Gamma generator produces *genuine* Gamma(shape) ISIs: + the mean rate is preserved, the shape is recovered (method of moments, shape = 1/CV^2), a KS + test against Gamma(shape) does NOT reject, a KS test against a wrong shape DOES, and it is not + Exponential. This is the Gamma counterpart of ``test_generator_is_true_poisson``. + """ + from myogen.simulator.neuron._cython._gamma_process_generator import ( + _GammaProcessGenerator__Cython, + ) + + isis = _generator_isis(_GammaProcessGenerator__Cython(20260710, shape, DT_MS), RATE_HZ) + mean = isis.mean() + + assert 1.0 / mean == pytest.approx(RATE_HZ, rel=0.02) # mean rate preserved + assert 1.0 / (isis.std() / mean) ** 2 == pytest.approx(shape, rel=0.1) # shape recovered + assert stats.kstest(isis, "gamma", args=(shape, 0.0, mean / shape)).pvalue > 0.01 # fits Gamma(shape) + assert stats.kstest(isis, "gamma", args=(1.5 * shape, 0.0, mean / (1.5 * shape))).pvalue < 1e-3 # not a wrong shape + assert stats.kstest(isis, "expon", args=(0.0, mean)).pvalue < 1e-3 # not Exponential diff --git a/uv.lock b/uv.lock index 940b4c42..0040fcde 100644 --- a/uv.lock +++ b/uv.lock @@ -10,18 +10,6 @@ resolution-markers = [ "python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] -[[package]] -name = "accessible-pygments" -version = "0.0.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pygments" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/bc/c1/bbac6a50d02774f91572938964c582fff4270eee73ab822a4aeea4d8b11b/accessible_pygments-0.0.5.tar.gz", hash = "sha256:40918d3e6a2b619ad424cb91e556bd3bd8865443d9f22f1dcdf79e33c8046872", size = 1377899, upload-time = "2024-05-10T11:23:10.216Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/3f/95338030883d8c8b91223b4e21744b04d11b161a3ef117295d8241f50ab4/accessible_pygments-0.0.5-py3-none-any.whl", hash = "sha256:88ae3211e68a1d0b011504b2ffc1691feafce124b845bd072ab6f9f66f34d4b7", size = 1395903, upload-time = "2024-05-10T11:23:08.421Z" }, -] - [[package]] name = "aiohappyeyeballs" version = "2.6.2" @@ -95,15 +83,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, ] -[[package]] -name = "alabaster" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, -] - [[package]] name = "alembic" version = "1.18.4" @@ -118,43 +97,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/29/6533c317b74f707ea28f8d633734dbda2119bbadfc61b2f3640ba835d0f7/alembic-1.18.4-py3-none-any.whl", hash = "sha256:a5ed4adcf6d8a4cb575f3d759f071b03cd6e5c7618eb796cb52497be25bfe19a", size = 263893, upload-time = "2026-02-10T16:00:49.997Z" }, ] -[[package]] -name = "apeye" -version = "1.4.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "apeye-core" }, - { name = "domdf-python-tools" }, - { name = "platformdirs" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/4f/6b/cc65e31843d7bfda8313a9dc0c77a21e8580b782adca53c7cb3e511fe023/apeye-1.4.1.tar.gz", hash = "sha256:14ea542fad689e3bfdbda2189a354a4908e90aee4bf84c15ab75d68453d76a36", size = 99219, upload-time = "2023-08-14T15:32:41.381Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/89/7b/2d63664777b3e831ac1b1d8df5bbf0b7c8bee48e57115896080890527b1b/apeye-1.4.1-py3-none-any.whl", hash = "sha256:44e58a9104ec189bf42e76b3a7fe91e2b2879d96d48e9a77e5e32ff699c9204e", size = 107989, upload-time = "2023-08-14T15:32:40.064Z" }, -] - -[[package]] -name = "apeye-core" -version = "1.1.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "domdf-python-tools" }, - { name = "idna" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e5/4c/4f108cfd06923bd897bf992a6ecb6fb122646ee7af94d7f9a64abd071d4c/apeye_core-1.1.5.tar.gz", hash = "sha256:5de72ed3d00cc9b20fea55e54b7ab8f5ef8500eb33a5368bc162a5585e238a55", size = 96511, upload-time = "2024-01-30T17:45:48.727Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/77/9f/fa9971d2a0c6fef64c87ba362a493a4f230eff4ea8dfb9f4c7cbdf71892e/apeye_core-1.1.5-py3-none-any.whl", hash = "sha256:dc27a93f8c9e246b3b238c5ea51edf6115ab2618ef029b9f2d9a190ec8228fbf", size = 99286, upload-time = "2024-01-30T17:45:46.764Z" }, -] - -[[package]] -name = "appdirs" -version = "1.4.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/d8/05696357e0311f5b5c316d7b95f46c669dd9c15aaeecbb48c7d0aeb88c40/appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", size = 13470, upload-time = "2020-05-11T07:59:51.037Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/00/2344469e2084fb287c2e0b57b72910309874c3245463acd6cf5e3db69324/appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128", size = 9566, upload-time = "2020-05-11T07:59:49.499Z" }, -] - [[package]] name = "attrs" version = "26.1.0" @@ -164,18 +106,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, ] -[[package]] -name = "autodocsumm" -version = "0.2.15" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "sphinx" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/46/b7/f28dea12fae1d1ad1e706f5cf6d16e8d735f305ebee86fd9390e099bd27d/autodocsumm-0.2.15.tar.gz", hash = "sha256:eaf431e7a5a39e41a215311173c8b95e83859059df1ccf3b79c64bf3d5582b3c", size = 46674, upload-time = "2026-03-26T20:44:07.074Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ca/3d/4357a0f685c0a2ae7132ac91905bec565e64f9ba63b079f7ec5da46e3597/autodocsumm-0.2.15-py3-none-any.whl", hash = "sha256:dbe6fabcaeae4540748ea9b3443eb76c2692e063d44f004f67c424610a5aca9a", size = 14852, upload-time = "2026-03-26T20:44:05.273Z" }, -] - [[package]] name = "babel" version = "2.18.0" @@ -186,43 +116,24 @@ wheels = [ ] [[package]] -name = "beartype" -version = "0.22.9" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/94/1009e248bbfbab11397abca7193bea6626806be9a327d399810d523a07cb/beartype-0.22.9.tar.gz", hash = "sha256:8f82b54aa723a2848a56008d18875f91c1db02c32ef6a62319a002e3e25a975f", size = 1608866, upload-time = "2025-12-13T06:50:30.72Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/71/cc/18245721fa7747065ab478316c7fea7c74777d07f37ae60db2e84f8172e8/beartype-0.22.9-py3-none-any.whl", hash = "sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2", size = 1333658, upload-time = "2025-12-13T06:50:28.266Z" }, -] - -[[package]] -name = "beautifulsoup4" -version = "4.14.3" +name = "backrefs" +version = "7.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "soupsieve" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/a7dd63622beef68cc0d3c3c36d472e143dd95443d5ebf14cd1a5b4dfbf11/backrefs-7.0.tar.gz", hash = "sha256:4989bb9e1e99eb23647c7160ed51fb21d0b41b5d200f2d3017da41e023097e82", size = 7012453, upload-time = "2026-04-28T16:28:04.215Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, + { url = "https://files.pythonhosted.org/packages/d4/39/39a31d7eae729ea14ed10c3ccef79371197177b9355a86cb3525709e8502/backrefs-7.0-py310-none-any.whl", hash = "sha256:b57cd227ea556b0aed3dc9b8da4628db4eabc0402c6d7fcfc69283a93955f7e9", size = 380824, upload-time = "2026-04-28T16:27:55.647Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b5/9302644225ba7dfa934a2ff2b9c7bb85701313a90dddb3dfaf693fa5bae2/backrefs-7.0-py311-none-any.whl", hash = "sha256:a0fa7360c63509e9e077e174ef4e6d3c21c8db94189b9d957289ae6d794b9475", size = 392626, upload-time = "2026-04-28T16:27:57.42Z" }, + { url = "https://files.pythonhosted.org/packages/36/da/87912ddec6e06feffbaa3d7aa18fc6352bee2e8f1fee185d7d1690f8f4e8/backrefs-7.0-py312-none-any.whl", hash = "sha256:ca42ce6a49ace3d75684dfa9937f3373902a63284ecb385ce36d15e5dcb41c12", size = 398537, upload-time = "2026-04-28T16:27:58.913Z" }, + { url = "https://files.pythonhosted.org/packages/00/bb/90ba423612b6aa0adccc6b1874bcd4a9b44b660c0c16f346611e00f64ac3/backrefs-7.0-py313-none-any.whl", hash = "sha256:f2c52955d631b9e1ac4cd56209f0a3a946d592b98e7790e77699339ae01c102a", size = 400491, upload-time = "2026-04-28T16:28:00.928Z" }, ] [[package]] -name = "cachecontrol" -version = "0.14.4" +name = "beartype" +version = "0.22.9" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "msgpack" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2d/f6/c972b32d80760fb79d6b9eeb0b3010a46b89c0b23cf6329417ff7886cd22/cachecontrol-0.14.4.tar.gz", hash = "sha256:e6220afafa4c22a47dd0badb319f84475d79108100d04e26e8542ef7d3ab05a1", size = 16150, upload-time = "2025-11-14T04:32:13.138Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/94/1009e248bbfbab11397abca7193bea6626806be9a327d399810d523a07cb/beartype-0.22.9.tar.gz", hash = "sha256:8f82b54aa723a2848a56008d18875f91c1db02c32ef6a62319a002e3e25a975f", size = 1608866, upload-time = "2025-12-13T06:50:30.72Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/79/c45f2d53efe6ada1110cf6f9fca095e4ff47a0454444aefdde6ac4789179/cachecontrol-0.14.4-py3-none-any.whl", hash = "sha256:b7ac014ff72ee199b5f8af1de29d60239954f223e948196fa3d84adaffc71d2b", size = 22247, upload-time = "2025-11-14T04:32:11.733Z" }, -] - -[package.optional-dependencies] -filecache = [ - { name = "filelock" }, + { url = "https://files.pythonhosted.org/packages/71/cc/18245721fa7747065ab478316c7fea7c74777d07f37ae60db2e84f8172e8/beartype-0.22.9-py3-none-any.whl", hash = "sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2", size = 1333658, upload-time = "2025-12-13T06:50:28.266Z" }, ] [[package]] @@ -378,61 +289,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, ] -[[package]] -name = "dict2css" -version = "0.6.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "domdf-python-tools" }, - { name = "tinycss2" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/83/ae/242596e550f79aa85ab6b5310caadd0b592063dc0c20c397d25707981f65/dict2css-0.6.0.tar.gz", hash = "sha256:143e55cb71c98a88c79f2c41e08a5fa4d875659275756f794e31ccd69936ce88", size = 9268, upload-time = "2026-05-21T08:34:29.598Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/68/0fbc6124cdd4f5a92599d18345bd24c67977988d0bb277f3ea284321d836/dict2css-0.6.0-py3-none-any.whl", hash = "sha256:5251f1df1c78ffdf09313657a7f88add0ad219127d9aeb18fb343b052d6bfbbe", size = 11874, upload-time = "2026-05-21T08:34:28.548Z" }, -] - -[[package]] -name = "docutils" -version = "0.21.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444, upload-time = "2024-04-23T18:57:18.24Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" }, -] - -[[package]] -name = "domdf-python-tools" -version = "3.10.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "natsort" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/36/8b/ab2d8a292bba8fe3135cacc8bfd3576710a14b8f2d0a8cde19130d5c9d21/domdf_python_tools-3.10.0.tar.gz", hash = "sha256:2ae308d2f4f1e9145f5f4ba57f840fbfd1c2983ee26e4824347789649d3ae298", size = 100458, upload-time = "2025-02-12T17:34:05.747Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/11/208f72084084d3f6a2ed5ebfdfc846692c3f7ad6dce65e400194924f7eed/domdf_python_tools-3.10.0-py3-none-any.whl", hash = "sha256:5e71c1be71bbcc1f881d690c8984b60e64298ec256903b3147f068bc33090c36", size = 126860, upload-time = "2025-02-12T17:34:04.093Z" }, -] - -[[package]] -name = "enum-tools" -version = "0.13.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pygments" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6d/87/50091e20c2765aa495b24521844a7d8f7041d48e4f9b47dd928cd38c8606/enum_tools-0.13.0.tar.gz", hash = "sha256:0d13335e361d300dc0f8fd82c8cf9951417246f9676144f5ee1761eb690228eb", size = 18904, upload-time = "2025-04-17T15:26:59.412Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/84/45/cf8a8df3ebe78db691ab54525d552085b67658877f0334f4b0c08c43b518/enum_tools-0.13.0-py3-none-any.whl", hash = "sha256:e0112b16767dd08cb94105844b52770eae67ece6f026916a06db4a3d330d2a95", size = 22366, upload-time = "2025-04-17T15:26:58.34Z" }, -] - -[package.optional-dependencies] -sphinx = [ - { name = "sphinx" }, - { name = "sphinx-jinja2-compat" }, - { name = "sphinx-toolbox" }, -] - [[package]] name = "fastrlock" version = "0.8.3" @@ -453,15 +309,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/28/a3/2ad0a0a69662fd4cf556ab8074f0de978ee9b56bff6ddb4e656df4aa9e8e/fastrlock-0.8.3-cp313-cp313-win_amd64.whl", hash = "sha256:8d1d6a28291b4ace2a66bd7b49a9ed9c762467617febdd9ab356b867ed901af8", size = 30472, upload-time = "2024-12-17T11:02:37.983Z" }, ] -[[package]] -name = "filelock" -version = "3.29.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b5/fe/997687a931ab51049acce6fa1f23e8f01216374ea81374ddee763c493db5/filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90", size = 57571, upload-time = "2026-04-19T15:39:10.068Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" }, -] - [[package]] name = "find-libpython" version = "0.5.1" @@ -562,6 +409,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl", hash = "sha256:11ef7bb35dab8a394fde6e608221d5cf3e8499401c249bebaeaad760a1a8dec2", size = 203402, upload-time = "2026-04-29T20:42:36.842Z" }, ] +[[package]] +name = "ghp-import" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, +] + [[package]] name = "greenlet" version = "3.5.1" @@ -586,6 +445,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6d/5c/a485a36e87df8d8fd0632ee01511244f5156a20ed3746cc6599340326395/greenlet-3.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:f16ba1efc0715b680a18b8123d90dad887c6112ae3555b4b5c32c149540c6b4e", size = 235499, upload-time = "2026-05-20T13:12:42.028Z" }, ] +[[package]] +name = "griffelib" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/33/e4/8d187ea29c2e30b3a09505c567513077d6117861bde1fbd997a167f262ec/griffelib-2.1.0.tar.gz", hash = "sha256:762a186d2c6fd6794d4ea20d428d597ffb857cb56b66421651cbba15bdd5e813", size = 216234, upload-time = "2026-06-19T12:05:42.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/d3/5268aeabf2ad82658c4e2ff3a060648d0f02f3926cb53247c0e4d0dab49e/griffelib-2.1.0-py3-none-any.whl", hash = "sha256:cc7b3d2d2865ad0b909fcc38086e3f554b5ea7acbaa7bbb7ecaa3f5dfb7d9f00", size = 142560, upload-time = "2026-06-19T12:05:38.742Z" }, +] + [[package]] name = "h5py" version = "3.16.0" @@ -629,19 +497,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/c4/2200240c6318b2057162335f77697011c07e85275ec64f11212fdeb7fd38/hdmf-4.2.0-py3-none-any.whl", hash = "sha256:d2856ebdd6058ff50d09ec226d65d72617919dd9e7ebcd6912c7e0917600f319", size = 340400, upload-time = "2025-12-18T18:49:16.507Z" }, ] -[[package]] -name = "html5lib" -version = "1.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, - { name = "webencodings" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ac/b6/b55c3f49042f1df3dcd422b7f224f939892ee94f22abcf503a9b7339eaf2/html5lib-1.1.tar.gz", hash = "sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f", size = 272215, upload-time = "2020-06-22T23:32:38.834Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6c/dd/a834df6482147d48e225a49515aabc28974ad5a4ca3215c18a882565b028/html5lib-1.1-py2.py3-none-any.whl", hash = "sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d", size = 112173, upload-time = "2020-06-22T23:32:36.781Z" }, -] - [[package]] name = "idna" version = "3.17" @@ -651,15 +506,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/de/a7/f76514cc40ad6234098ecdebda08732d75964776c51a42845b7da10649e2/idna-3.17-py3-none-any.whl", hash = "sha256:466e48829084efe2548012b855df21540b96f2e20e51bd124c851536556a592c", size = 65316, upload-time = "2026-05-28T14:32:37.035Z" }, ] -[[package]] -name = "imagesize" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/e6/7bf14eeb8f8b7251141944835abd42eb20a658d89084b7e1f3e5fe394090/imagesize-2.0.0.tar.gz", hash = "sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3", size = 1773045, upload-time = "2026-03-03T14:18:29.941Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl", hash = "sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96", size = 9441, upload-time = "2026-03-03T14:18:27.892Z" }, -] - [[package]] name = "iniconfig" version = "2.3.0" @@ -782,18 +628,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b5/91/53255615acd2a1eaca307ede3c90eb550bae9c94581f8c00081b6b1c8f44/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:1f1489f769582498610e015a8ef2d36f28f505ab3096d0e16b4858a9ec214f57", size = 75987, upload-time = "2026-03-09T13:15:39.65Z" }, ] -[[package]] -name = "linkify-it-py" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "uc-micro-py" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2e/c9/06ea13676ef354f0af6169587ae292d3e2406e212876a413bf9eece4eb23/linkify_it_py-2.1.0.tar.gz", hash = "sha256:43360231720999c10e9328dc3691160e27a718e280673d444c38d7d3aaa3b98b", size = 29158, upload-time = "2026-03-01T07:48:47.683Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/de/88b3be5c31b22333b3ca2f6ff1de4e863d8fe45aaea7485f591970ec1d3e/linkify_it_py-2.1.0-py3-none-any.whl", hash = "sha256:0d252c1594ecba2ecedc444053db5d3a9b7ec1b0dd929c8f1d74dce89f86c05e", size = 19878, upload-time = "2026-03-01T07:48:46.098Z" }, -] - [[package]] name = "llvmlite" version = "0.47.0" @@ -823,15 +657,12 @@ wheels = [ ] [[package]] -name = "markdown-it-py" -version = "4.2.0" +name = "markdown" +version = "3.10.2" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mdurl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, + { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, ] [[package]] @@ -916,62 +747,159 @@ wheels = [ ] [[package]] -name = "mdit-py-plugins" -version = "0.6.1" +name = "mergedeep" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "ghp-import" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mergedeep" }, + { name = "mkdocs-get-deps" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, +] + +[[package]] +name = "mkdocs-autorefs" +version = "1.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/c0/f641843de3f612a6b48253f39244165acff36657a91cc903633d456ae1ac/mkdocs_autorefs-1.4.4.tar.gz", hash = "sha256:d54a284f27a7346b9c38f1f852177940c222da508e66edc816a0fa55fc6da197", size = 56588, upload-time = "2026-02-10T15:23:55.105Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl", hash = "sha256:834ef5408d827071ad1bc69e0f39704fa34c7fc05bc8e1c72b227dfdc5c76089", size = 25530, upload-time = "2026-02-10T15:23:53.817Z" }, +] + +[[package]] +name = "mkdocs-gallery" +version = "0.10.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mkdocs" }, + { name = "mkdocs-material" }, + { name = "packaging" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/de/ceec460a00f47fc06676c9c2cbc9c95e12c9f85bafa9edbf5b309d392b3c/mkdocs_gallery-0.10.4.tar.gz", hash = "sha256:469f84a0c842ea87aa59e8679bd6237607f2a578788b50a50132abd9937d14d4", size = 423267, upload-time = "2024-09-30T08:31:06.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/b3/f6aa253503b3147238747672322503b2b529f390861f6184838d18abd51a/mkdocs_gallery-0.10.4-py2.py3-none-any.whl", hash = "sha256:8669d162b412714c52792f2959d4d211bf92bf5f820f5916c0686ff1ccd89806", size = 137833, upload-time = "2024-09-30T08:31:04.959Z" }, +] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mergedeep" }, + { name = "platformdirs" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/25/b3cccb187655b9393572bde9b09261d267c3bf2f2cdabe347673be5976a6/mkdocs_get_deps-0.2.2.tar.gz", hash = "sha256:8ee8d5f316cdbbb2834bc1df6e69c08fe769a83e040060de26d3c19fad3599a1", size = 11047, upload-time = "2026-03-10T02:46:33.632Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl", hash = "sha256:e7878cbeac04860b8b5e0ca31d3abad3df9411a75a32cde82f8e44b6c16ff650", size = 9555, upload-time = "2026-03-10T02:46:32.256Z" }, +] + +[[package]] +name = "mkdocs-material" +version = "9.7.6" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "markdown-it-py" }, + { name = "babel" }, + { name = "backrefs" }, + { name = "colorama" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "mkdocs" }, + { name = "mkdocs-material-extensions" }, + { name = "paginate" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/59/fc/f8d0863f8862f25602c0404d75568e89fb6b4109804645e5cdfb1be5cf56/mdit_py_plugins-0.6.1.tar.gz", hash = "sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0", size = 56114, upload-time = "2026-05-13T09:03:38.91Z" } +sdist = { url = "https://files.pythonhosted.org/packages/45/29/6d2bcf41ae40802c4beda2432396fff97b8456fb496371d1bc7aad6512ec/mkdocs_material-9.7.6.tar.gz", hash = "sha256:00bdde50574f776d328b1862fe65daeaf581ec309bd150f7bff345a098c64a69", size = 4097959, upload-time = "2026-03-19T15:41:58.161Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/01/bc663630c510822c95c47a66af9fa7a443c295b47d5f041e5e6ae62ef659/mkdocs_material-9.7.6-py3-none-any.whl", hash = "sha256:71b84353921b8ea1ba84fe11c50912cc512da8fe0881038fcc9a0761c0e635ba", size = 9305470, upload-time = "2026-03-19T15:41:55.217Z" }, +] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl", hash = "sha256:214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d", size = 66663, upload-time = "2026-05-13T09:03:37.76Z" }, + { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, ] [[package]] -name = "mdurl" -version = "0.1.2" +name = "mkdocs-section-index" +version = "0.3.12" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +dependencies = [ + { name = "mkdocs" }, + { name = "properdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/e2/64d0f3f054ca8efe61e706006ff5f0d49ad99620c62c2e04818573391c33/mkdocs_section_index-0.3.12.tar.gz", hash = "sha256:285635bf86c643b0fc7a343053d7a818049817bff4408f52b80c4367bd5e7268", size = 14946, upload-time = "2026-04-16T19:20:00.953Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, + { url = "https://files.pythonhosted.org/packages/b0/4d/a330cab5e055d45e924cec69da54a3d8ed37643964f8d1fa1a772b496273/mkdocs_section_index-0.3.12-py3-none-any.whl", hash = "sha256:a1100039546beb4ebef63ce6fc91f3195fb9c0c3763105d4d3d7cd31e0a046eb", size = 8932, upload-time = "2026-04-16T19:19:59.741Z" }, ] [[package]] -name = "memory-profiler" -version = "0.61.0" +name = "mkdocstrings" +version = "1.0.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "psutil" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, + { name = "mkdocs-autorefs" }, + { name = "pymdown-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b2/88/e1907e1ca3488f2d9507ca8b0ae1add7b1cd5d3ca2bc8e5b329382ea2c7b/memory_profiler-0.61.0.tar.gz", hash = "sha256:4e5b73d7864a1d1292fb76a03e82a3e78ef934d06828a698d9dada76da2067b0", size = 35935, upload-time = "2022-11-15T17:57:28.994Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/5d/f888d4d3eb31359b327bc9b17a212d6ef03fe0b0682fbb3fc2cb849fb12b/mkdocstrings-1.0.4.tar.gz", hash = "sha256:3969a6515b77db65fd097b53c1b7aa4ae840bd71a2ee62a6a3e89503446d7172", size = 100088, upload-time = "2026-04-15T09:16:53.376Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/49/26/aaca612a0634ceede20682e692a6c55e35a94c21ba36b807cc40fe910ae1/memory_profiler-0.61.0-py3-none-any.whl", hash = "sha256:400348e61031e3942ad4d4109d18753b2fb08c2f6fb8290671c5513a34182d84", size = 31803, upload-time = "2022-11-15T17:57:27.031Z" }, + { url = "https://files.pythonhosted.org/packages/6e/94/be70f8ee9c45f2f62b39a1f0e9303bc20e138a8f3b8e50ffd89498e177e1/mkdocstrings-1.0.4-py3-none-any.whl", hash = "sha256:63464b4b29053514f32a1dbbf604e52876d5e638111b0c295ab7ed3cac73ca9b", size = 35560, upload-time = "2026-04-15T09:16:51.436Z" }, +] + +[package.optional-dependencies] +python = [ + { name = "mkdocstrings-python" }, ] [[package]] -name = "msgpack" -version = "1.1.2" +name = "mkdocstrings-python" +version = "2.0.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4d/f2/bfb55a6236ed8725a96b0aa3acbd0ec17588e6a2c3b62a93eb513ed8783f/msgpack-1.1.2.tar.gz", hash = "sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e", size = 173581, upload-time = "2025-10-08T09:15:56.596Z" } +dependencies = [ + { name = "griffelib" }, + { name = "mkdocs-autorefs" }, + { name = "mkdocstrings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/b6/e858701499d57eee8b3fd8e78168083956c6683ddbe727b46758b19e1119/mkdocstrings_python-2.0.5.tar.gz", hash = "sha256:3a4d92556ad39637e88af94a5374213af9a8e3040c3824ceaed04b486c017594", size = 199578, upload-time = "2026-06-19T10:41:08.868Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/bd/8b0d01c756203fbab65d265859749860682ccd2a59594609aeec3a144efa/msgpack-1.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa", size = 81939, upload-time = "2025-10-08T09:15:01.472Z" }, - { url = "https://files.pythonhosted.org/packages/34/68/ba4f155f793a74c1483d4bdef136e1023f7bcba557f0db4ef3db3c665cf1/msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb", size = 85064, upload-time = "2025-10-08T09:15:03.764Z" }, - { url = "https://files.pythonhosted.org/packages/f2/60/a064b0345fc36c4c3d2c743c82d9100c40388d77f0b48b2f04d6041dbec1/msgpack-1.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f", size = 417131, upload-time = "2025-10-08T09:15:05.136Z" }, - { url = "https://files.pythonhosted.org/packages/65/92/a5100f7185a800a5d29f8d14041f61475b9de465ffcc0f3b9fba606e4505/msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42", size = 427556, upload-time = "2025-10-08T09:15:06.837Z" }, - { url = "https://files.pythonhosted.org/packages/f5/87/ffe21d1bf7d9991354ad93949286f643b2bb6ddbeab66373922b44c3b8cc/msgpack-1.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9", size = 404920, upload-time = "2025-10-08T09:15:08.179Z" }, - { url = "https://files.pythonhosted.org/packages/ff/41/8543ed2b8604f7c0d89ce066f42007faac1eaa7d79a81555f206a5cdb889/msgpack-1.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620", size = 415013, upload-time = "2025-10-08T09:15:09.83Z" }, - { url = "https://files.pythonhosted.org/packages/41/0d/2ddfaa8b7e1cee6c490d46cb0a39742b19e2481600a7a0e96537e9c22f43/msgpack-1.1.2-cp312-cp312-win32.whl", hash = "sha256:1fff3d825d7859ac888b0fbda39a42d59193543920eda9d9bea44d958a878029", size = 65096, upload-time = "2025-10-08T09:15:11.11Z" }, - { url = "https://files.pythonhosted.org/packages/8c/ec/d431eb7941fb55a31dd6ca3404d41fbb52d99172df2e7707754488390910/msgpack-1.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b", size = 72708, upload-time = "2025-10-08T09:15:12.554Z" }, - { url = "https://files.pythonhosted.org/packages/c5/31/5b1a1f70eb0e87d1678e9624908f86317787b536060641d6798e3cf70ace/msgpack-1.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69", size = 64119, upload-time = "2025-10-08T09:15:13.589Z" }, - { url = "https://files.pythonhosted.org/packages/6b/31/b46518ecc604d7edf3a4f94cb3bf021fc62aa301f0cb849936968164ef23/msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf", size = 81212, upload-time = "2025-10-08T09:15:14.552Z" }, - { url = "https://files.pythonhosted.org/packages/92/dc/c385f38f2c2433333345a82926c6bfa5ecfff3ef787201614317b58dd8be/msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7", size = 84315, upload-time = "2025-10-08T09:15:15.543Z" }, - { url = "https://files.pythonhosted.org/packages/d3/68/93180dce57f684a61a88a45ed13047558ded2be46f03acb8dec6d7c513af/msgpack-1.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fdf7d83102bf09e7ce3357de96c59b627395352a4024f6e2458501f158bf999", size = 412721, upload-time = "2025-10-08T09:15:16.567Z" }, - { url = "https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e", size = 424657, upload-time = "2025-10-08T09:15:17.825Z" }, - { url = "https://files.pythonhosted.org/packages/38/f8/4398c46863b093252fe67368b44edc6c13b17f4e6b0e4929dbf0bdb13f23/msgpack-1.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fffee09044073e69f2bad787071aeec727183e7580443dfeb8556cbf1978d162", size = 402668, upload-time = "2025-10-08T09:15:19.003Z" }, - { url = "https://files.pythonhosted.org/packages/28/ce/698c1eff75626e4124b4d78e21cca0b4cc90043afb80a507626ea354ab52/msgpack-1.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5928604de9b032bc17f5099496417f113c45bc6bc21b5c6920caf34b3c428794", size = 419040, upload-time = "2025-10-08T09:15:20.183Z" }, - { url = "https://files.pythonhosted.org/packages/67/32/f3cd1667028424fa7001d82e10ee35386eea1408b93d399b09fb0aa7875f/msgpack-1.1.2-cp313-cp313-win32.whl", hash = "sha256:a7787d353595c7c7e145e2331abf8b7ff1e6673a6b974ded96e6d4ec09f00c8c", size = 65037, upload-time = "2025-10-08T09:15:21.416Z" }, - { url = "https://files.pythonhosted.org/packages/74/07/1ed8277f8653c40ebc65985180b007879f6a836c525b3885dcc6448ae6cb/msgpack-1.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:a465f0dceb8e13a487e54c07d04ae3ba131c7c5b95e2612596eafde1dccf64a9", size = 72631, upload-time = "2025-10-08T09:15:22.431Z" }, - { url = "https://files.pythonhosted.org/packages/e5/db/0314e4e2db56ebcf450f277904ffd84a7988b9e5da8d0d61ab2d057df2b6/msgpack-1.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:e69b39f8c0aa5ec24b57737ebee40be647035158f14ed4b40e6f150077e21a84", size = 64118, upload-time = "2025-10-08T09:15:23.402Z" }, + { url = "https://files.pythonhosted.org/packages/d1/fc/10ab7e80650a9c9e8f4f1105f8c8e73567f88ed0c06ada589ab81d38687c/mkdocstrings_python-2.0.5-py3-none-any.whl", hash = "sha256:30c837bbff016549f659fcba6539ac351303f0fd7e713c89a040611072236e9d", size = 104951, upload-time = "2026-06-19T10:41:07.378Z" }, ] [[package]] @@ -1066,6 +994,7 @@ gpu = [ { name = "cupy-cuda12x" }, ] nwb = [ + { name = "h5py" }, { name = "nwbinspector" }, { name = "pynwb" }, ] @@ -1078,28 +1007,18 @@ dev = [ { name = "scipy-stubs" }, ] docs = [ - { name = "enum-tools", extra = ["sphinx"] }, - { name = "linkify-it-py" }, - { name = "memory-profiler" }, - { name = "nwbinspector" }, - { name = "pydata-sphinx-theme" }, - { name = "pygments" }, - { name = "pynwb" }, - { name = "rinohtype" }, - { name = "roman" }, - { name = "sphinx" }, - { name = "sphinx-autodoc-typehints" }, - { name = "sphinx-design" }, - { name = "sphinx-gallery" }, - { name = "sphinx-hoverxref" }, - { name = "sphinxcontrib-mermaid" }, - { name = "toml" }, + { name = "mkdocs-gallery" }, + { name = "mkdocs-material" }, + { name = "mkdocs-section-index" }, + { name = "mkdocstrings", extra = ["python"] }, + { name = "properdocs" }, ] [package.metadata] requires-dist = [ { name = "beartype", specifier = ">=0.21.0" }, { name = "cupy-cuda12x", marker = "extra == 'gpu'", specifier = ">=13.0,<14" }, + { name = "h5py", marker = "extra == 'nwb'", specifier = ">=3.0" }, { name = "joblib", specifier = ">=1.3" }, { name = "matplotlib", specifier = ">=3.10.1" }, { name = "neo", specifier = ">=0.14.0" }, @@ -1129,39 +1048,11 @@ dev = [ { name = "scipy-stubs", specifier = ">=1.16.1.0" }, ] docs = [ - { name = "enum-tools", extras = ["sphinx"], specifier = ">=0.12.0" }, - { name = "linkify-it-py", specifier = ">=2.0.3" }, - { name = "memory-profiler", specifier = ">=0.61.0" }, - { name = "nwbinspector", specifier = ">=0.5.0" }, - { name = "pydata-sphinx-theme", specifier = ">=0.16.1" }, - { name = "pygments", specifier = ">=2.19.2" }, - { name = "pynwb", specifier = ">=2.8.0" }, - { name = "rinohtype", specifier = ">=0.5.5" }, - { name = "roman", specifier = ">=5.2" }, - { name = "sphinx", specifier = ">=8.1.3,<9" }, - { name = "sphinx-autodoc-typehints", specifier = ">=2.5.0" }, - { name = "sphinx-design", specifier = ">=0.6.1" }, - { name = "sphinx-gallery", specifier = ">=0.19.0" }, - { name = "sphinx-hoverxref", specifier = ">=1.4.1" }, - { name = "sphinxcontrib-mermaid", specifier = ">=1.0.0" }, - { name = "toml", specifier = ">=0.10.2" }, -] - -[[package]] -name = "myst-parser" -version = "5.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "docutils" }, - { name = "jinja2" }, - { name = "markdown-it-py" }, - { name = "mdit-py-plugins" }, - { name = "pyyaml" }, - { name = "sphinx" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/21/dc/603751677fff302f34396e206b610f556a59d7fe58b9a2145f54e96b48e8/myst_parser-5.1.0.tar.gz", hash = "sha256:ab69322dc6719dcc7f296479dbb70181b66df6ed315064f92dbc85c0e1bf2f02", size = 101182, upload-time = "2026-05-13T09:38:19.361Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/09/dc/f3dfb7488b770f3f67e6545085bf2abea5172e88f57b8ad25ef860ca704c/myst_parser-5.1.0-py3-none-any.whl", hash = "sha256:9c91c52b3cdb4d94a6506e4fab4e2f296c7623a0da0dcbe6de1565c3dad67a8a", size = 85817, upload-time = "2026-05-13T09:38:17.904Z" }, + { name = "mkdocs-gallery", specifier = ">=0.10" }, + { name = "mkdocs-material", specifier = ">=9.5" }, + { name = "mkdocs-section-index", specifier = ">=0.3" }, + { name = "mkdocstrings", extras = ["python"], specifier = ">=0.27" }, + { name = "properdocs", specifier = ">=1.6.7" }, ] [[package]] @@ -1345,6 +1236,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, ] +[[package]] +name = "paginate" +version = "0.5.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, +] + [[package]] name = "pandas" version = "3.0.3" @@ -1402,6 +1302,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/aa/18/a8444036c6dd65ba3624c63b734d3ba95ba63ace513078e1580590075d21/pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364", size = 5955, upload-time = "2020-09-16T19:21:11.409Z" }, ] +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + [[package]] name = "pillow" version = "12.2.0" @@ -1538,43 +1447,26 @@ wheels = [ ] [[package]] -name = "psutil" -version = "7.2.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, - { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, - { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, - { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, - { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, - { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, - { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, - { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, - { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, - { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, - { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, - { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, - { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, - { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, -] - -[[package]] -name = "pydata-sphinx-theme" -version = "0.18.0" +name = "properdocs" +version = "1.6.7" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "accessible-pygments" }, - { name = "babel" }, - { name = "beautifulsoup4" }, - { name = "docutils" }, - { name = "pygments" }, - { name = "sphinx" }, - { name = "typing-extensions" }, + { name = "click" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "ghp-import" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag" }, + { name = "watchdog" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ad/81/b3fdc8b74d0cfed9e623a0fef9932376800da5daa1a85d1224cac4c131a3/pydata_sphinx_theme-0.18.0.tar.gz", hash = "sha256:b4abc95ab02600872e060db07c79e056e87b7ea653ab1ffd0e0b1fa75a3003d4", size = 5004260, upload-time = "2026-05-20T08:32:28.897Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/29/f27a4e1eddf72ed3db6e47818fbafe6debbf09fd7051f9c1a007239b46ef/properdocs-1.6.7.tar.gz", hash = "sha256:adc7b16e562890af0e098a7e5b02e3a81c20894a87d6a28d345c9300de73c26e", size = 276141, upload-time = "2026-03-20T20:07:48.167Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/cd/e0eda602060f9dc99068f8e54490812d9d34ebb134043ff0ae594cf721a4/pydata_sphinx_theme-0.18.0-py3-none-any.whl", hash = "sha256:fbe5401f26642d487e3c5b6dfcbf69b3b1d579e80dcc479a429632abe0a13929", size = 6200747, upload-time = "2026-05-20T08:32:26.646Z" }, + { url = "https://files.pythonhosted.org/packages/bd/4d/fc923f5c85318ee8cc903566dc4e0ebe41b2dfc1d2ecf5546db232397ed6/properdocs-1.6.7-py3-none-any.whl", hash = "sha256:6fa0cfa2e01bf338f684892c8a506cf70ea88ae7f3479c933b6fa20168101cbd", size = 225406, upload-time = "2026-03-20T20:07:46.875Z" }, ] [[package]] @@ -1586,6 +1478,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] +[[package]] +name = "pymdown-extensions" +version = "11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/47/67/f1e79672a5f91985577c7984c9709ca110e4fd37fe7fd167b60422e6ccc2/pymdown_extensions-11.0.tar.gz", hash = "sha256:8269cef0247f9e2d0a62fcea10860aba05c1cbab5470fd4b63230b96434dc589", size = 857049, upload-time = "2026-06-23T02:27:45.146Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/b6/1ae53367e28b9cffa3be7574e13fbe4589694272fd47710fbdbafd3d63c6/pymdown_extensions-11.0-py3-none-any.whl", hash = "sha256:fbc4acb641814fa9d17521bbd21a5240ef739a662f11c06330c4b78c93e954d6", size = 269415, upload-time = "2026-06-23T02:27:43.826Z" }, +] + [[package]] name = "pynwb" version = "3.1.3" @@ -1668,6 +1573,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, ] +[[package]] +name = "pyyaml-env-tag" +version = "1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, +] + [[package]] name = "quantities" version = "0.16.4" @@ -1709,103 +1626,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, ] -[[package]] -name = "rinoh-typeface-dejavuserif" -version = "0.1.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "rinohtype" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ca/d1/e221568b41d6205356ffa6340dcee02acf86d92dec5e5b04798c141b3472/rinoh-typeface-dejavuserif-0.1.3.tar.gz", hash = "sha256:8be129230ac98ab2ebfbf5b570575052ba7069e4087ce36a2d4c1d85182833ce", size = 1666226, upload-time = "2020-12-07T15:22:02.19Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/75/a7/c3df4e93137f390d28849ee1fd7578adee2b6322dca33082c7173ac2135f/rinoh_typeface_dejavuserif-0.1.3-py3-none-any.whl", hash = "sha256:35ba67bf25e526b4b8180dc31d8fa2ff9b594d924e8790e8074699cd2f0e7da8", size = 1667463, upload-time = "2020-12-07T15:21:58.999Z" }, -] - -[[package]] -name = "rinoh-typeface-texgyrecursor" -version = "0.1.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "rinohtype" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/68/70/b0a661118e7a93d1abf4375543644b054bb74b258924d62dd898b9860a71/rinoh-typeface-texgyrecursor-0.1.1.tar.gz", hash = "sha256:076f7dbbd0201b0d44f4c77e4f7070474e1365152dc8509b5cded04c1648308a", size = 239925, upload-time = "2016-07-18T13:31:16.791Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/21/1b46f0a50dfea435f90f29595d2e0b3d3c15a31b35bdde725942c83f2644/rinoh_typeface_texgyrecursor-0.1.1-py3-none-any.whl", hash = "sha256:98080e6af2271e67cff1d69033cb6c3a6baa58de41a66dce1a1102b4dd41de72", size = 242415, upload-time = "2016-07-18T13:31:20.398Z" }, -] - -[[package]] -name = "rinoh-typeface-texgyreheros" -version = "0.1.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "rinohtype" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8e/62/3b28c30558fd53590797c455bb46ab47599c2b9145a09e7ae162dd625426/rinoh-typeface-texgyreheros-0.1.1.tar.gz", hash = "sha256:fd7082e917bccc292894447f11d15a3efce9f1386056118b57b66cc2f5a36bbd", size = 522029, upload-time = "2016-07-18T13:32:49.1Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/98/0d/b428d4cf5ad3bd6848b617570d5dcb061b3f66a68b72e96bf2a7909a2300/rinoh_typeface_texgyreheros-0.1.1-py3-none-any.whl", hash = "sha256:0c921a040a84b0af031e4a36d196d46b65fb18929d88df5ed26533b57100e6c9", size = 523725, upload-time = "2016-07-18T13:32:53.338Z" }, -] - -[[package]] -name = "rinoh-typeface-texgyrepagella" -version = "0.1.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "rinohtype" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6a/2a/86db888c244f11bb0f6fed72c75b09dba9dbf8aa72e7b69c27cf7975340f/rinoh-typeface-texgyrepagella-0.1.1.tar.gz", hash = "sha256:53f4dba338c6b1df758888f23ce1ed728e5be45746f161488ad3b944e5e79fd2", size = 319023, upload-time = "2016-07-18T13:33:36.348Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/84/44/c27695119e6ad918358d44a87f8543cab6240f1f768068dd32ba1b65d92e/rinoh_typeface_texgyrepagella-0.1.1-py3-none-any.whl", hash = "sha256:0144e3b828a31b405ab9be1dec67f48be360d9f86d109578924fd1d7e0e1ded6", size = 321426, upload-time = "2016-07-18T13:33:39.855Z" }, -] - -[[package]] -name = "rinohtype" -version = "0.5.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "appdirs" }, - { name = "docutils" }, - { name = "myst-parser" }, - { name = "packaging" }, - { name = "rinoh-typeface-dejavuserif" }, - { name = "rinoh-typeface-texgyrecursor" }, - { name = "rinoh-typeface-texgyreheros" }, - { name = "rinoh-typeface-texgyrepagella" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/10/59/53c88ae5af164e03dceace7b86ac67f4f45cd019f2f58fd6aaddae79cb46/rinohtype-0.5.6.tar.gz", hash = "sha256:e9392bd4e9117f5bef1285b81d6420183df5393aed289b176f7a81b6c9646d15", size = 6731068, upload-time = "2026-05-15T14:24:10.085Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/39/221faa2cfd10bdf00be83a9e7643ba9d4ab8fdcb9373d7a1563aed8eb980/rinohtype-0.5.6-py3-none-any.whl", hash = "sha256:fa137d7f8f805b6725b8100e59ef873a56767f255951be7826596c4f28cbfc5d", size = 616489, upload-time = "2026-05-15T14:24:07.956Z" }, -] - -[[package]] -name = "roman" -version = "5.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9c/7c/3901b35ed856329bf98e84da8e5e0b4d899ea0027eee222f1be42a24ff3f/roman-5.2.tar.gz", hash = "sha256:275fe9f46290f7d0ffaea1c33251b92b8e463ace23660508ceef522e7587cb6f", size = 8185, upload-time = "2025-11-11T08:03:57.025Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/10/14/ea3cdd7276fcd731a9003fe4abeb6b395a38110ddff6a6a509f4ee00f741/roman-5.2-py3-none-any.whl", hash = "sha256:89d3b47400388806d06ff77ea77c79ab080bc127820dea6bf34e1f1c1b8e676e", size = 6041, upload-time = "2025-11-11T08:03:56.051Z" }, -] - -[[package]] -name = "roman-numerals" -version = "4.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ae/f9/41dc953bbeb056c17d5f7a519f50fdf010bd0553be2d630bc69d1e022703/roman_numerals-4.1.0.tar.gz", hash = "sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2", size = 9077, upload-time = "2025-12-17T18:25:34.381Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7", size = 7676, upload-time = "2025-12-17T18:25:33.098Z" }, -] - -[[package]] -name = "roman-numerals-py" -version = "4.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "roman-numerals" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cb/b5/de96fca640f4f656eb79bbee0e79aeec52e3e0e359f8a3e6a0d366378b64/roman_numerals_py-4.1.0.tar.gz", hash = "sha256:f5d7b2b4ca52dd855ef7ab8eb3590f428c0b1ea480736ce32b01fef2a5f8daf9", size = 4274, upload-time = "2025-12-17T18:25:41.153Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/27/2c/daca29684cbe9fd4bc711f8246da3c10adca1ccc4d24436b17572eb2590e/roman_numerals_py-4.1.0-py3-none-any.whl", hash = "sha256:553114c1167141c1283a51743759723ecd05604a1b6b507225e91dc1a6df0780", size = 4547, upload-time = "2025-12-17T18:25:40.136Z" }, -] - [[package]] name = "rpds-py" version = "2026.5.1" @@ -2006,258 +1826,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] -[[package]] -name = "snowballstemmer" -version = "3.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/63/ee/67eef9600338e245ad7838230969a34c823ddbdbccc5e1fc43cd75b55bc9/snowballstemmer-3.1.0.tar.gz", hash = "sha256:fd9e34526b23340cd23ffea6c9f9760974ecc2c2ac9e1d81401443ccdb2a801f", size = 122523, upload-time = "2026-05-24T19:04:19.691Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/49/83/ddbf4533c62dd32667ef1238952abef155f3d3391f5be69a352ad1638a42/snowballstemmer-3.1.0-py3-none-any.whl", hash = "sha256:17e6d1da216aa07db6dad37139ea70cf13c4b2e9a096f6e64a9648fc657d3154", size = 104550, upload-time = "2026-05-24T19:04:18.026Z" }, -] - -[[package]] -name = "soupsieve" -version = "2.8.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/47/2c/0a5f6f8ee0d5589e48c7640213ed5175d52cf540a06725b628cc1a45d6ce/soupsieve-2.8.4.tar.gz", hash = "sha256:e121fd02e975c695e4e9e8774a5ee35d74714b59307868dcc5319ad2d9e3328e", size = 121110, upload-time = "2026-05-24T13:55:57.154Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl", hash = "sha256:e7e6b0769c8f51ed59acab6e994b00621096cfb1c640a7509295987388fbaf65", size = 37304, upload-time = "2026-05-24T13:55:55.406Z" }, -] - -[[package]] -name = "sphinx" -version = "8.2.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "alabaster" }, - { name = "babel" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "docutils" }, - { name = "imagesize" }, - { name = "jinja2" }, - { name = "packaging" }, - { name = "pygments" }, - { name = "requests" }, - { name = "roman-numerals-py" }, - { name = "snowballstemmer" }, - { name = "sphinxcontrib-applehelp" }, - { name = "sphinxcontrib-devhelp" }, - { name = "sphinxcontrib-htmlhelp" }, - { name = "sphinxcontrib-jsmath" }, - { name = "sphinxcontrib-qthelp" }, - { name = "sphinxcontrib-serializinghtml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/38/ad/4360e50ed56cb483667b8e6dadf2d3fda62359593faabbe749a27c4eaca6/sphinx-8.2.3.tar.gz", hash = "sha256:398ad29dee7f63a75888314e9424d40f52ce5a6a87ae88e7071e80af296ec348", size = 8321876, upload-time = "2025-03-02T22:31:59.658Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/31/53/136e9eca6e0b9dc0e1962e2c908fbea2e5ac000c2a2fbd9a35797958c48b/sphinx-8.2.3-py3-none-any.whl", hash = "sha256:4405915165f13521d875a8c29c8970800a0141c14cc5416a38feca4ea5d9b9c3", size = 3589741, upload-time = "2025-03-02T22:31:56.836Z" }, -] - -[[package]] -name = "sphinx-autodoc-typehints" -version = "3.5.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "sphinx" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/34/4f/4fd5583678bb7dc8afa69e9b309e6a99ee8d79ad3a4728f4e52fd7cb37c7/sphinx_autodoc_typehints-3.5.2.tar.gz", hash = "sha256:5fcd4a3eb7aa89424c1e2e32bedca66edc38367569c9169a80f4b3e934171fdb", size = 37839, upload-time = "2025-10-16T00:50:15.743Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/05/f2/9657c98a66973b7c35bfd48ba65d1922860de9598fbb535cd96e3f58a908/sphinx_autodoc_typehints-3.5.2-py3-none-any.whl", hash = "sha256:0accd043619f53c86705958e323b419e41667917045ac9215d7be1b493648d8c", size = 21184, upload-time = "2025-10-16T00:50:13.973Z" }, -] - -[[package]] -name = "sphinx-design" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "sphinx" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/13/7b/804f311da4663a4aecc6cf7abd83443f3d4ded970826d0c958edc77d4527/sphinx_design-0.7.0.tar.gz", hash = "sha256:d2a3f5b19c24b916adb52f97c5f00efab4009ca337812001109084a740ec9b7a", size = 2203582, upload-time = "2026-01-19T13:12:53.297Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/30/cf/45dd359f6ca0c3762ce0490f681da242f0530c49c81050c035c016bfdd3a/sphinx_design-0.7.0-py3-none-any.whl", hash = "sha256:f82bf179951d58f55dca78ab3706aeafa496b741a91b1911d371441127d64282", size = 2220350, upload-time = "2026-01-19T13:12:51.077Z" }, -] - -[[package]] -name = "sphinx-gallery" -version = "0.21.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pillow" }, - { name = "sphinx" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fb/9d/91334ba9370de74564c8a1e0c54ce1bc638b35e00177cc02cb25c9c14348/sphinx_gallery-0.21.0.tar.gz", hash = "sha256:72a7734ad9100878345b8b65c249148cc0f1cd0e274adf3e3900214e4c2c5bee", size = 483616, upload-time = "2026-04-24T03:09:28.173Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/dd/d95843947392524418aa9fc4e8d205e82b4261ab2d2fab4abce7a14ee7c0/sphinx_gallery-0.21.0-py3-none-any.whl", hash = "sha256:f37bea4012f1cd7439c7782081e4259945207cf179e79b81330a6db3b18bca8b", size = 466808, upload-time = "2026-04-24T03:09:25.992Z" }, -] - -[[package]] -name = "sphinx-hoverxref" -version = "1.4.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "sphinx" }, - { name = "sphinxcontrib-jquery" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d6/6d/89764d3c719ed172d94778a74661cca647b2475165ae6cde1f73c9524d63/sphinx_hoverxref-1.4.2.tar.gz", hash = "sha256:74fab961b1b8c0e9c2cf22fa195c0e783d635e78686d39dd06ff157c196ca0c9", size = 1715764, upload-time = "2024-11-18T18:01:34.965Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/19/3b/d981175d7b9bbbcf6894faf640d15198394efc7dedd6182332c4a791a5a6/sphinx_hoverxref-1.4.2-py2.py3-none-any.whl", hash = "sha256:4fc2e283e908d9df61ea9196589934944a7e2e6e3cb753a420b938cd6d14e220", size = 32198, upload-time = "2024-11-18T18:01:31.473Z" }, -] - -[[package]] -name = "sphinx-jinja2-compat" -version = "0.4.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jinja2" }, - { name = "markupsafe" }, - { name = "standard-imghdr", marker = "python_full_version >= '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/43/98/43313781f29e8c6c46fec907430310172d6f207e95e4fbea9289990fbbfe/sphinx_jinja2_compat-0.4.1.tar.gz", hash = "sha256:0188f0802d42c3da72997533b55a00815659a78d3f81d4b4747b1fb15a5728e6", size = 5222, upload-time = "2025-08-06T20:06:25.824Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/c8/4fd58c1000d7f8f5572c507f4550d2e2d9741e500c68eb2e3da17cbe5a85/sphinx_jinja2_compat-0.4.1-py3-none-any.whl", hash = "sha256:64ca0d46f0d8029fbe69ea612793a55e6ef0113e1bba4a85d402158c09f17a14", size = 8123, upload-time = "2025-08-06T20:06:24.947Z" }, -] - -[[package]] -name = "sphinx-prompt" -version = "1.10.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "docutils" }, - { name = "idna" }, - { name = "jinja2" }, - { name = "pygments" }, - { name = "requests" }, - { name = "sphinx" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d0/a3/91293c0e0f0b76d0697ba7a41541929ca3f5457671d008bd84a9bde17e21/sphinx_prompt-1.10.2.tar.gz", hash = "sha256:47b592ba75caebd044b0eddf7a5a1b6e0aef6df587b034377cd101a999b686ba", size = 5566, upload-time = "2025-11-28T09:23:18.057Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/f4/44ce4d0179fb4e9cfe181a8aa281bba23e40158a609fb3680774529acaaa/sphinx_prompt-1.10.2-py3-none-any.whl", hash = "sha256:6594337962c4b1498602e6984634bed4a0dc7955852e3cfc255eb0af766ed859", size = 7474, upload-time = "2025-11-28T09:23:17.154Z" }, -] - -[[package]] -name = "sphinx-tabs" -version = "3.5.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "docutils" }, - { name = "pygments" }, - { name = "sphinx" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ce/30/ca5b0de830f369968d8e3483dd45a8908fd10169c05cd9837f0bd075982e/sphinx_tabs-3.5.0.tar.gz", hash = "sha256:91dba1187e4c35fd37380a56ac228bbd54c6c649b2351829f3bf033718277537", size = 17006, upload-time = "2026-03-03T23:00:30.404Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/45/6adc5efeb19fd5fed4027e520b5c668ce58236a2b271ade5533c4c116276/sphinx_tabs-3.5.0-py3-none-any.whl", hash = "sha256:154be49de4d5c8249ea08c5d9bf88ca8f9c31e00a178305a93cbc33e000339e5", size = 9871, upload-time = "2026-03-03T23:00:28.89Z" }, -] - -[[package]] -name = "sphinx-toolbox" -version = "4.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "apeye" }, - { name = "autodocsumm" }, - { name = "beautifulsoup4" }, - { name = "cachecontrol", extra = ["filecache"] }, - { name = "dict2css" }, - { name = "docutils" }, - { name = "domdf-python-tools" }, - { name = "filelock" }, - { name = "html5lib" }, - { name = "roman" }, - { name = "ruamel-yaml" }, - { name = "sphinx" }, - { name = "sphinx-autodoc-typehints" }, - { name = "sphinx-jinja2-compat" }, - { name = "sphinx-prompt" }, - { name = "sphinx-tabs" }, - { name = "tabulate" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a6/fe/45526091eb845625e5feed9685152682b6bb6b792f193533bc091c9a9277/sphinx_toolbox-4.2.0.tar.gz", hash = "sha256:f4dac92fc70e09e9af15d6b8ff003bde1f2aa3573118910f81cf486ced68d4ea", size = 116034, upload-time = "2026-05-21T20:20:01.105Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/c2/055e2fa4329d6514befbdc01f646a78e32645be1033d56713c3e1c3be2c9/sphinx_toolbox-4.2.0-py3-none-any.whl", hash = "sha256:09f7bef48ee8936ecd890a298b3e1908fda11fe9e6a21fc623495ac34e42c962", size = 198012, upload-time = "2026-05-21T20:19:59.071Z" }, -] - -[[package]] -name = "sphinxcontrib-applehelp" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" }, -] - -[[package]] -name = "sphinxcontrib-devhelp" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, -] - -[[package]] -name = "sphinxcontrib-htmlhelp" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, -] - -[[package]] -name = "sphinxcontrib-jquery" -version = "4.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "sphinx" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/de/f3/aa67467e051df70a6330fe7770894b3e4f09436dea6881ae0b4f3d87cad8/sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a", size = 122331, upload-time = "2023-03-14T15:01:01.944Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/76/85/749bd22d1a68db7291c89e2ebca53f4306c3f205853cf31e9de279034c3c/sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae", size = 121104, upload-time = "2023-03-14T15:01:00.356Z" }, -] - -[[package]] -name = "sphinxcontrib-jsmath" -version = "1.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" }, -] - -[[package]] -name = "sphinxcontrib-mermaid" -version = "2.0.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jinja2" }, - { name = "pyyaml" }, - { name = "sphinx" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/19/75/3a1cc926da8c563c58ddc124a7b3fe5ccadcae96c96e3a6f8ac3653a210a/sphinxcontrib_mermaid-2.0.2.tar.gz", hash = "sha256:f09576c78ca93fa0e3034fd9c45aaffa7c44ab449de9c43b8b8d262afe52bc66", size = 19265, upload-time = "2026-05-05T13:59:02.959Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/16/8d/93be7e0f7fa915a576859b3bfac7a7baa3303181c44d7db7eefbd3e8a69f/sphinxcontrib_mermaid-2.0.2-py3-none-any.whl", hash = "sha256:d862e514991279fb4816302c5cfe167d2557bf3ce7125ae0cb47dac80a0f46ce", size = 14094, upload-time = "2026-05-05T13:59:01.585Z" }, -] - -[[package]] -name = "sphinxcontrib-qthelp" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" }, -] - -[[package]] -name = "sphinxcontrib-serializinghtml" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, -] - [[package]] name = "sqlalchemy" version = "2.0.50" @@ -2285,24 +1853,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d0/10/f7220e9b784d295d241c86ed99aeb537f92afcd469a64861f2717e9bb077/sqlalchemy-2.0.50-py3-none-any.whl", hash = "sha256:92064363517a3ff8212b5a93b8c62876579d8dfd1ca5b561335f30152d884fa9", size = 1943861, upload-time = "2026-05-24T19:59:01.119Z" }, ] -[[package]] -name = "standard-imghdr" -version = "3.10.14" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/09/d2/2eb5521072c9598886035c65c023f39f7384bcb73eed70794f469e34efac/standard_imghdr-3.10.14.tar.gz", hash = "sha256:2598fe2e7c540dbda34b233295e10957ab8dc8ac6f3bd9eaa8d38be167232e52", size = 5474, upload-time = "2024-04-21T18:55:10.859Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/d0/9852f70eb01f814843530c053542b72d30e9fbf74da7abb0107e71938389/standard_imghdr-3.10.14-py3-none-any.whl", hash = "sha256:cdf6883163349624dee9a81d2853a20260337c4cd41c04e99c082e01833a08e2", size = 5598, upload-time = "2024-04-21T18:54:48.587Z" }, -] - -[[package]] -name = "tabulate" -version = "0.10.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/46/58/8c37dea7bbf769b20d58e7ace7e5edfe65b849442b00ffcdd56be88697c6/tabulate-0.10.0.tar.gz", hash = "sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d", size = 91754, upload-time = "2026-03-04T18:55:34.402Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl", hash = "sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3", size = 39814, upload-time = "2026-03-04T18:55:31.284Z" }, -] - [[package]] name = "threadpoolctl" version = "3.6.0" @@ -2312,27 +1862,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, ] -[[package]] -name = "tinycss2" -version = "1.5.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "webencodings" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a3/ae/2ca4913e5c0f09781d75482874c3a95db9105462a92ddd303c7d285d3df2/tinycss2-1.5.1.tar.gz", hash = "sha256:d339d2b616ba90ccce58da8495a78f46e55d4d25f9fd71dfd526f07e7d53f957", size = 88195, upload-time = "2025-11-23T10:29:10.082Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/60/45/c7b5c3168458db837e8ceab06dc77824e18202679d0463f0e8f002143a97/tinycss2-1.5.1-py3-none-any.whl", hash = "sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661", size = 28404, upload-time = "2025-11-23T10:29:08.676Z" }, -] - -[[package]] -name = "toml" -version = "0.10.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253, upload-time = "2020-11-01T01:40:22.204Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" }, -] - [[package]] name = "tqdm" version = "4.67.3" @@ -2363,15 +1892,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" }, ] -[[package]] -name = "uc-micro-py" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/78/67/9a363818028526e2d4579334460df777115bdec1bb77c08f9db88f6389f2/uc_micro_py-2.0.0.tar.gz", hash = "sha256:c53691e495c8db60e16ffc4861a35469b0ba0821fe409a8a7a0a71864d33a811", size = 6611, upload-time = "2026-03-01T06:31:27.526Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/61/73/d21edf5b204d1467e06500080a50f79d49ef2b997c79123a536d4a17d97c/uc_micro_py-2.0.0-py3-none-any.whl", hash = "sha256:3603a3859af53e5a39bc7677713c78ea6589ff188d70f4fee165db88e22b242c", size = 6383, upload-time = "2026-03-01T06:31:26.257Z" }, -] - [[package]] name = "urllib3" version = "2.7.0" @@ -2382,12 +1902,27 @@ wheels = [ ] [[package]] -name = "webencodings" -version = "0.5.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload-time = "2017-04-05T20:21:34.189Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" }, +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, ] [[package]]