diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 00000000..6260b94e
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,39 @@
+name: tests
+
+on:
+ pull_request:
+ branches: [main, improved-semg]
+ push:
+ branches: [main, improved-semg]
+ workflow_dispatch:
+
+jobs:
+ pytest:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python 3.12
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.12.8"
+
+ - name: System packages for NEURON / MPI
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y libopenmpi-dev openmpi-bin libgtk2.0-dev
+
+ - name: Install uv
+ run: |
+ python -m pip install --upgrade pip
+ python -m pip install uv
+
+ - name: Install project with dev group
+ run: |
+ uv sync --group dev
+
+ - name: Compile NEURON mechanisms
+ run: uv run poe setup_myogen
+
+ - name: Run pytest
+ run: uv run pytest tests/ -v
diff --git a/.gitignore b/.gitignore
index 805e3729..5e58b558 100644
--- a/.gitignore
+++ b/.gitignore
@@ -175,10 +175,14 @@ test_*.png
myogen/simulator/neuron/_cython/*.c
myogen/simulator/nmodl_files/x86_64/
+myogen/simulator/nmodl_files/arm64/
myogen/simulator/x86_64/
+myogen/simulator/arm64/
myogen/simulator/nmodl_files/*.c
myogen/simulator/nmodl_files/*.o
myogen/simulator/nmodl_files/*.dll
+myogen/simulator/nmodl_files/*.dylib
+myogen/simulator/nmodl_files/*.so
data/
# Note: examples/data/ is NOT ignored - it contains pre-generated static data for docs
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8ac7f419..ef062604 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+### Added
+- **RNG accessors**: new public API for reproducible random draws
+ - `myogen.get_random_generator()` — always returns the current global RNG (tracks the latest `set_random_seed` call)
+ - `myogen.get_random_seed()` — returns the seed currently in effect
+ - `myogen.derive_subseed(*labels)` — deterministic sub-seed helper for seeding non-NumPy generators (Cython spike generators, sklearn `random_state`, etc.) so they also track `set_random_seed`
+- **Optional `elephant` extra**: install via `pip install myogen[elephant]`. The extra bundles both `elephant>=1.1.1` and `viziphant>=0.4.0` (viziphant was moved out of core dependencies because its only transitive import chain in core came from `elephant`). Core install is now genuinely elephant-free — verified with `importlib.util.find_spec('elephant') is None` on a fresh `uv sync`. The five modules that import elephant (`utils/helper.py`, `surface_emg.py`, `intramuscular_emg.py`, `force_model.py`, `force_model_vectorized.py`) were already guarded with `try/except ImportError` and now emit an accurate install hint when the extra is missing. `neo>=0.14.0` is now an explicit core dependency — it was previously only reached transitively through `elephant`/`viziphant`, so removing those would otherwise break `import myogen`
+- **Test suite** under `tests/`
+ - `test_determinism.py` — 10 regressions covering seed propagation, sub-seed collision avoidance, and deprecation warnings for the legacy RNG names
+ - `test_firing_rate_statistics.py` — 4 regressions covering `FR_std` and `CV_ISI` NaN edge cases
+ - Added `pytest>=8.0` to the `dev` dependency-group and a `[tool.pytest.ini_options]` section
+- **Per-muscle ISI/CV figure**: new `plot_cv_vs_fr_per_muscle()` in `examples/02_finetune/05_plot_isi_cv_multi_muscle_comparison.py` emits a 3-panel VL / VM / FDI breakdown alongside the existing pooled plot
+
+### Changed
+- **RNG architecture** (internal refactor): the six internal modules and eight example scripts that previously did `from myogen import RANDOM_GENERATOR, SEED` at module scope now call the accessor at each site, eliminating the stale-reference bug where `set_random_seed` didn't reach downstream modules or seed-derived generators
+- **Per-cell seed derivation**: the three sites in `myogen/simulator/neuron/cells.py` that built a per-cell seed as `SEED + (class_id+1)*(global_id+1)` — a formula that collided on swapped factors, e.g. `(0, 5)` and `(1, 2)` both resolved to `+6` — now use `derive_subseed(class_id, global_id)` (collision-free with respect to label order, built on `numpy.random.SeedSequence`). The `motor_unit_sim.py` `KMeans` `random_state` uses the same helper. **Consequence**: for a given global seed, RNG output differs from earlier releases; byte-identical reproduction of pre-`Unreleased` outputs is not possible without matching code
+- **Example figures**: bar graphs in `02_finetune/01_optimize_dd_for_target_firing_rate.py`, `02_finetune/02_compute_force_from_optimized_dd.py` and `03_papers/watanabe/01_compute_baseline_force.py` replaced with dumbbell / violin + box + jittered-scatter plots that show the underlying distribution (all points when n<10, violin + box when n≥10)
+- **NEURON version alignment**: `README.md`, `docs/source/README.md`, `docs/source/index.md`, and `setup.py` Windows-install messaging now reference NEURON **8.2.7** (matching the Linux/macOS pip pin in `pyproject.toml` and the CI workflow) with the correct installer filename (`py-39-310-311-312-313`)
+- **Docs build**: pinned `sphinx<9` in the `docs` dependency-group to work around a `sphinx-hoverxref` regression on Sphinx 9, and moved `[tool.pytest.ini_options]` in `pyproject.toml` so it no longer re-parents the `docs` dependency-group
+
+### Deprecated
+- **`myogen.RANDOM_GENERATOR`** and **`myogen.SEED`** module attributes. They remain accessible via a module-level `__getattr__` that emits a `DeprecationWarning` and returns the current RNG / current seed. External code should migrate to `get_random_generator()` / `get_random_seed()`; module-level `from myogen import RANDOM_GENERATOR` captures a stale reference and does not reflect later `set_random_seed` calls
+
+### Fixed
+- **`FR_std = NaN` with a single active unit**: `myogen/utils/helper.py::calculate_firing_rate_statistics` now returns `FR_std = 0.0` when fewer than two units pass the firing-rate filter, instead of propagating the `np.std(..., ddof=1)` NaN into downstream ensemble statistics
+- **`CV_ISI = NaN` for n=1 ISI**: same function's per-neuron branch now returns `0.0` when `min_spikes_for_cv` is set to 2 and a neuron has exactly two spikes
+- **`set_random_seed` did not propagate**: module-level `from myogen import RANDOM_GENERATOR, SEED` imports in the internal modules captured a stale reference at import time; reseeding the package rebinding did not affect them. The accessor refactor above is the fix
+- **Typos / placeholder docstrings**: `extandable` → `extensible` in README and docs; `"as determined by no one"` docstring comments in `myogen/simulator/core/muscle/muscle.py` replaced with concrete physiological rationale
+
## [0.8.5] - 2026-01-15
### Fixed
diff --git a/README.md b/README.md
index 1762a49c..c77a8a50 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
-
The modular and extandable simulation toolkit for neurophysiology
+ The modular and extensible simulation toolkit for neurophysiology
[](https://nsquaredlab.github.io/MyoGen/)
[](https://www.python.org/downloads/)
@@ -47,7 +47,7 @@ MyoGen is designed for algorithm validation, hypothesis-driven research, and edu
| Platform | Before Installing MyoGen |
|----------|--------------------------|
-| **Windows** | [NEURON 8.2.6](https://github.com/neuronsimulator/nrn/releases/download/8.2.6/nrn-8.2.6.w64-mingw-py-38-39-310-311-312-setup.exe) - Download, run installer, select "Add to PATH" |
+| **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` |
@@ -70,7 +70,7 @@ MyoGen is designed for algorithm validation, hypothesis-driven research, and edu
>
> ### 2. NEURON Simulator
>
-> 1. **Download**: [NEURON 8.2.6 Installer](https://github.com/neuronsimulator/nrn/releases/download/8.2.6/nrn-8.2.6.w64-mingw-py-38-39-310-311-312-setup.exe)
+> 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
diff --git a/docs/source/README.md b/docs/source/README.md
index c9bfb3d2..3460ae3f 100644
--- a/docs/source/README.md
+++ b/docs/source/README.md
@@ -13,7 +13,7 @@
[Paper](#citation)
-# MyoGen - The modular and extandable simulation toolkit for neurophysiology
+# 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.
@@ -39,7 +39,7 @@ MyoGen is designed for algorithm validation, hypothesis-driven research, and edu
**Prerequisites**: Python ≥3.12, Linux/Windows/macOS
> [!IMPORTANT]
-> **Windows users**: Install [NEURON 8.2.6](https://github.com/neuronsimulator/nrn/releases/download/8.2.6/nrn-8.2.6.w64-mingw-py-38-39-310-311-312-setup.exe) before running `uv sync`
+> **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
diff --git a/docs/source/index.md b/docs/source/index.md
index 45dea341..2096d77b 100644
--- a/docs/source/index.md
+++ b/docs/source/index.md
@@ -4,7 +4,7 @@
- The modular and extandable simulation toolkit for neurophysiology
+ The modular and extensible simulation toolkit for neurophysiology
[](https://nsquaredlab.github.io/MyoGen/)
[](https://www.python.org/downloads/)
@@ -47,7 +47,7 @@ MyoGen is designed for algorithm validation, hypothesis-driven research, and edu
| Platform | Before Installing MyoGen |
|----------|--------------------------|
-| **Windows** | [NEURON 8.2.6](https://github.com/neuronsimulator/nrn/releases/download/8.2.6/nrn-8.2.6.w64-mingw-py-38-39-310-311-312-setup.exe) - Download, run installer, select "Add to PATH" |
+| **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` |
@@ -70,7 +70,7 @@ MyoGen is designed for algorithm validation, hypothesis-driven research, and edu
>
> ### 2. NEURON Simulator
>
-> 1. **Download**: [NEURON 8.2.6 Installer](https://github.com/neuronsimulator/nrn/releases/download/8.2.6/nrn-8.2.6.w64-mingw-py-38-39-310-311-312-setup.exe)
+> 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
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 a03e0dd8..32560daa 100644
--- a/examples/01_basic/02_simulate_spike_trains_current_injection.py
+++ b/examples/01_basic/02_simulate_spike_trains_current_injection.py
@@ -20,17 +20,17 @@
# ----------------
#
# .. important::
-# In **MyoGen** all **random number generation** is handled by the :data:`~myogen.RANDOM_GENERATOR` object.
+# 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`.
#
-# This object is a wrapper around the :mod:`numpy.random` module and is used to generate random numbers.
-#
-# It is intended to be used with the following API:
+# Always fetch the generator at the call site so the current seed is honored:
#
# .. code-block:: python
#
-# from myogen import simulator, RANDOM_GENERATOR
+# from myogen import simulator, get_random_generator
+# get_random_generator().normal(0, 1)
#
-# To change the default seed, use :func:`~myogen.set_random_seed`:
+# To change the seed, use :func:`~myogen.set_random_seed`:
#
# .. code-block:: python
#
@@ -50,7 +50,7 @@
from neuron import h
from viziphant.rasterplot import rasterplot_rates
-from myogen import RANDOM_GENERATOR
+from myogen import get_random_generator
from myogen.simulator.neuron.populations import AlphaMN__Pool
from myogen.utils.currents import create_trapezoid_current
from myogen.utils.neuron.inject_currents_into_populations import (
@@ -107,9 +107,9 @@
timestep = 0.05 * pq.ms
simulation_time = 4000 * pq.ms
-rise_time_ms = list(RANDOM_GENERATOR.uniform(100, 500, size=n_pools)) * pq.ms
-plateau_time_ms = list(RANDOM_GENERATOR.uniform(1000, 2000, size=n_pools)) * pq.ms
-fall_time_ms = list(RANDOM_GENERATOR.uniform(1000, 2000, size=n_pools)) * pq.ms
+rise_time_ms = list(get_random_generator().uniform(100, 500, size=n_pools)) * pq.ms
+plateau_time_ms = list(get_random_generator().uniform(1000, 2000, size=n_pools)) * pq.ms
+fall_time_ms = list(get_random_generator().uniform(1000, 2000, size=n_pools)) * pq.ms
input_current__AnalogSignal = create_trapezoid_current(
n_pools,
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 49b0a6a3..5ab2a9a4 100644
--- a/examples/01_basic/03_simulate_spike_trains_descending_drive.py
+++ b/examples/01_basic/03_simulate_spike_trains_descending_drive.py
@@ -28,17 +28,17 @@
# ----------------
#
# .. important::
-# In **MyoGen** all **random number generation** is handled by the ``RANDOM_GENERATOR`` object.
+# In **MyoGen** all **random number generation** is handled by the RNG returned from
+# ``get_random_generator()``, a thin wrapper around ``numpy.random``.
#
-# This object is a wrapper around the ``numpy.random`` module and is used to generate random numbers.
-#
-# It is intended to be used with the following API:
+# Always fetch the generator at the call site so the current seed is honored:
#
# .. code-block:: python
#
-# from myogen import simulator, RANDOM_GENERATOR
+# from myogen import simulator, get_random_generator
+# get_random_generator().normal(0, 1)
#
-# To change the default seed, use ``set_random_seed``:
+# To change the seed, use ``set_random_seed``:
#
# .. code-block:: python
#
@@ -59,7 +59,7 @@
from neuron import h
from tqdm import tqdm
-from myogen import RANDOM_GENERATOR
+from myogen import get_random_generator
from myogen.simulator.neuron import Network
from myogen.simulator.neuron.populations import AlphaMN__Pool, DescendingDrive__Pool
from myogen.utils.nmodl import load_nmodl_mechanisms
@@ -161,7 +161,7 @@
# Add small noise for realism
trapezoid_drive = (
- trapezoid_drive + np.clip(RANDOM_GENERATOR.normal(0, 1.0, size=time_points), 0, None) * pps
+ trapezoid_drive + np.clip(get_random_generator().normal(0, 1.0, size=time_points), 0, None) * pps
)
# Create AnalogSignal
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 2a9ffbbe..8bd36b8a 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
@@ -56,7 +56,7 @@
from neo import Segment, SpikeTrain
from neuron import h
-from myogen import RANDOM_GENERATOR
+from myogen import get_random_generator
from myogen.simulator import RecruitmentThresholds
from myogen.simulator.neuron import Network
from myogen.simulator.neuron.populations import AlphaMN__Pool, DescendingDrive__Pool
@@ -224,7 +224,7 @@ def objective(trial):
# Generate constant drive signal with small noise
time_points = int(SIMULATION_TIME_MS / TIMESTEP_MS)
drive_signal = np.ones(time_points) * dd_drive__Hz + np.clip(
- RANDOM_GENERATOR.normal(0, 1.0, size=time_points), 0, None
+ get_random_generator().normal(0, 1.0, size=time_points), 0, None
)
# Run NEURON simulation
@@ -504,7 +504,9 @@ def trial_to_dict(t):
fig, ax = plt.subplots(1, 1, figsize=(10, 6))
-# Bar plot comparing target vs achieved
+# Dumbbell plot comparing target vs achieved (two scalar endpoints per
+# category — not a distribution, so Editor 8's "show all data points"
+# rule is honoured by marking each value as an explicit point).
categories = ["Mean FR (Hz)", "Std FR (Hz)"]
targets = [TARGET_FR_MEAN__HZ, TARGET_FR_STD__HZ]
achieved = [
@@ -513,22 +515,16 @@ def trial_to_dict(t):
]
x = np.arange(len(categories))
-width = 0.35
-
-bars1 = ax.bar(x - width / 2, targets, width, label="Target")
-bars2 = ax.bar(x + width / 2, achieved, width, label="Achieved")
-
-# Add value labels on bars
-for bars in [bars1, bars2]:
- for bar in bars:
- height = bar.get_height()
- ax.text(
- bar.get_x() + bar.get_width() / 2.0,
- height,
- f"{height:.2f}",
- ha="center",
- va="bottom",
- )
+
+for xi, t, a in zip(x, targets, achieved):
+ ax.plot([xi, xi], [t, a], color="gray", linestyle="--", alpha=0.6, zorder=1)
+
+ax.scatter(x, targets, s=120, marker="o", label="Target", zorder=3)
+ax.scatter(x, achieved, s=120, marker="D", label="Achieved", zorder=3)
+
+for xi, t, a in zip(x, targets, achieved):
+ ax.text(xi + 0.05, t, f"{t:.2f}", va="center", ha="left")
+ ax.text(xi + 0.05, a, f"{a:.2f}", va="center", ha="left")
# Calculate percent errors
mean_error = abs(achieved[0] - targets[0]) / targets[0] * 100
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 254878ba..e2473825 100644
--- a/examples/02_finetune/02_compute_force_from_optimized_dd.py
+++ b/examples/02_finetune/02_compute_force_from_optimized_dd.py
@@ -58,7 +58,7 @@
from neo import Block, Segment, SpikeTrain
from neuron import h
-from myogen import RANDOM_GENERATOR
+from myogen import get_random_generator
from myogen.simulator import RecruitmentThresholds
from myogen.simulator.core.force.force_model import ForceModel
from myogen.simulator.neuron import Network
@@ -215,7 +215,7 @@
time_points = int(SIMULATION_TIME_MS / TIMESTEP_MS)
drive_signal = np.ones(time_points) * dd_drive__Hz + np.clip(
- RANDOM_GENERATOR.normal(0, 1.0, size=time_points), 0, None
+ get_random_generator().normal(0, 1.0, size=time_points), 0, None
)
##############################################################################
@@ -405,15 +405,33 @@
mu_ids.append(i)
if firing_rates:
- axes[2].bar(mu_ids, firing_rates, alpha=0.7)
- axes[2].axhline(fr_mean, linestyle="--", label=f"Mean = {fr_mean:.1f} Hz")
- axes[2].set_xlabel("Motor Unit ID")
+ # Distribution plot with all data points (Editor 8): violin shell for
+ # the density, box-and-whisker for the quartiles, jittered points for
+ # every active motor unit.
+ fr_array = np.asarray(firing_rates)
+ fr_sd = float(np.std(fr_array, ddof=1)) if fr_array.size > 1 else 0.0
+ if fr_array.size >= 10:
+ axes[2].violinplot(
+ fr_array, positions=[0], widths=0.7, showmeans=False, showmedians=False
+ )
+ axes[2].boxplot(
+ fr_array, positions=[0], widths=0.3, showfliers=False
+ )
+ jitter = np.random.default_rng(0).uniform(-0.15, 0.15, size=fr_array.size)
+ axes[2].scatter(jitter, fr_array, alpha=0.6, s=18, zorder=3)
+ axes[2].axhline(
+ fr_mean, linestyle="--", label=f"Mean = {fr_mean:.1f} Hz (SD = {fr_sd:.1f} Hz)"
+ )
+ axes[2].set_xticks([0])
+ axes[2].set_xticklabels([f"n = {fr_array.size}"])
+ axes[2].set_xlabel("Motor Units")
axes[2].set_ylabel("Firing Rate (Hz)")
axes[2].set_title("Firing Rate Distribution")
axes[2].legend(framealpha=1.0, edgecolor="none")
-# Format all axes
-for ax in axes:
+# Format time-series axes (subplot 2 is the MU firing-rate distribution,
+# whose x-axis is categorical, not time)
+for ax in axes[:2]:
ax.set_xlim(0, SIMULATION_TIME_MS / 1000)
plt.tight_layout()
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 e53e96ea..5b57273c 100644
--- a/examples/02_finetune/03_optimize_dd_for_target_force.py
+++ b/examples/02_finetune/03_optimize_dd_for_target_force.py
@@ -50,7 +50,7 @@
from neo import Block, Segment, SpikeTrain
from neuron import h
-from myogen import RANDOM_GENERATOR, set_random_seed
+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
@@ -212,7 +212,7 @@ def run_simulation_and_compute_force(dd_drive__Hz, gamma_shape, recruitment_thre
# Create constant drive signal
time_points = int(SIMULATION_TIME_MS / TIMESTEP_MS)
drive_signal = np.ones(time_points) * dd_drive__Hz + np.clip(
- RANDOM_GENERATOR.normal(0, 1.0, size=time_points), 0, None
+ get_random_generator().normal(0, 1.0, size=time_points), 0, None
)
# Initialize simulation
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 f7c605b5..b1c8c128 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
@@ -526,6 +526,161 @@ def plot_cv_vs_fr_multi_muscle(all_muscle_data, exp_data):
return fig, ax
+def plot_cv_vs_fr_per_muscle(all_muscle_data, exp_data, muscles=("VL", "VM", "FDI")):
+ """
+ Per-muscle supplementary figure in response to R3 Major 4.
+
+ The main Figure 4B pools experimental data across VL, VM and FDI into
+ a single axis so the simulation overlay can be compared against the
+ envelope of all three muscles at once. The reviewer asked for a
+ companion view that separates the three muscles, enabling a fair
+ per-muscle comparison against the simulation. Each panel therefore
+ shows a single muscle's experimental envelope (convex hull + scatter)
+ and repeats the full simulation overlay so readers can judge how well
+ the simulation matches each individual muscle.
+
+ Parameters
+ ----------
+ all_muscle_data : dict
+ Nested ``{simulated_muscle_type: {force_level: DataFrame}}`` — the
+ same structure consumed by :func:`plot_cv_vs_fr_multi_muscle`.
+ exp_data : pd.DataFrame
+ Experimental ISI statistics with at least ``Muscle``, ``ISI CV``
+ and ``FR mean`` columns.
+ muscles : sequence of str, optional
+ Experimental muscles to plot, one per panel.
+
+ Returns
+ -------
+ tuple
+ ``(fig, axes)`` where ``axes`` is a 1-D array of Axes, one per muscle.
+ """
+ n_panels = len(muscles)
+ fig, axes = plt.subplots(
+ 1, n_panels, figsize=(5.5 * n_panels, 6), sharex=True, sharey=True
+ )
+ if n_panels == 1:
+ axes = np.array([axes])
+
+ # Same force markers across all panels so they can be compared directly.
+ all_force_levels = set()
+ for muscle_data in all_muscle_data.values():
+ all_force_levels.update(muscle_data.keys())
+ force_markers = generate_force_markers(all_force_levels)
+
+ for ax, muscle in zip(axes, muscles):
+ # 1. Experimental envelope for this muscle only.
+ if exp_data is not None:
+ muscle_rows = exp_data[exp_data["Muscle"] == muscle]
+ cv_data = muscle_rows["ISI CV"].values
+ fr_data = muscle_rows["FR mean"].values
+
+ if len(cv_data) > 2:
+ points = np.column_stack([cv_data, fr_data])
+ try:
+ hull = ConvexHull(points)
+ polygon = Polygon(
+ points[hull.vertices],
+ facecolor=EXP_COLORS.get(muscle, "#808080"),
+ alpha=0.25,
+ edgecolor=EXP_COLORS.get(muscle, "#808080"),
+ linewidth=1.5,
+ linestyle="-",
+ zorder=0,
+ )
+ ax.add_patch(polygon)
+ except Exception:
+ pass
+
+ ax.scatter(
+ cv_data,
+ fr_data,
+ s=20,
+ alpha=1.0,
+ color=EXP_COLORS.get(muscle, "#808080"),
+ edgecolors="white",
+ linewidth=0.5,
+ marker="x",
+ zorder=1,
+ label=f"Experimental {muscle}",
+ )
+
+ # 2. Full simulation overlay repeated on every panel so the
+ # simulation-vs-muscle comparison is panel-local.
+ for muscle_type in sorted(all_muscle_data.keys()):
+ muscle_data = all_muscle_data[muscle_type]
+ short_muscle = muscle_type.split("_")[0]
+ colormap_name = MUSCLE_COLORMAPS.get(short_muscle, "Greys")
+
+ for force_level in sorted(muscle_data.keys()):
+ df = muscle_data[force_level]
+ recruitment_order = (
+ df["MU_ID"].values if "MU_ID" in df.columns else np.arange(len(df))
+ )
+ colors = get_muscle_colors(recruitment_order, colormap_name)
+ marker = force_markers.get(force_level, "o")
+ ax.scatter(
+ df["CV_ISI"],
+ df["mean_firing_rate_Hz"],
+ s=40,
+ alpha=0.8,
+ c=colors,
+ edgecolors="black",
+ linewidth=0.5,
+ marker=marker,
+ zorder=2,
+ )
+
+ ax.set_xlabel("Coefficient of Variation (CV)", fontsize=12)
+ ax.set_title(muscle, fontsize=14)
+ ax.set_xlim(0, 0.5)
+ ax.set_ylim(4, 25)
+ ax.tick_params(axis="both", labelsize=10)
+
+ axes[0].set_ylabel("Mean Firing Rate (pps)", fontsize=12)
+
+ # Shared legend at the figure level so the force-level markers are
+ # decoded for every panel without repeating a per-axis legend.
+ legend_handles = [
+ Patch(
+ facecolor=MUSCLE_LEGEND_COLORS.get(m.split("_")[0], "#000000"),
+ edgecolor="black",
+ label=m.split("_")[0],
+ )
+ for m in sorted(all_muscle_data.keys())
+ ]
+ for force in sorted(all_force_levels):
+ legend_handles.append(
+ Line2D(
+ [0],
+ [0],
+ marker=force_markers[force],
+ color="none",
+ markerfacecolor="gray",
+ markeredgecolor="black",
+ markersize=8,
+ label=f"{force}% MVC",
+ linewidth=0,
+ )
+ )
+ fig.legend(
+ handles=legend_handles,
+ loc="lower center",
+ ncol=min(len(legend_handles), 6),
+ frameon=False,
+ fontsize=10,
+ bbox_to_anchor=(0.5, -0.02),
+ )
+
+ fig.suptitle(
+ "ISI Statistics Comparison — per-muscle split (supplementary to Fig 4B)",
+ fontsize=14,
+ )
+ fig.tight_layout(rect=(0, 0.05, 1, 1))
+
+ return fig, axes
+
+
##############################################################################
# Load Simulation Data
# ---------------------
@@ -596,6 +751,26 @@ def plot_cv_vs_fr_multi_muscle(all_muscle_data, exp_data):
plt.show()
print(f"\nPlot saved to: {output_file}")
+##############################################################################
+# Per-Muscle Supplementary Figure (R3 Major 4)
+# --------------------------------------------
+#
+# Companion to the pooled Fig 4B above: splits the experimental envelope
+# into per-muscle panels (VL, VM, FDI) so the simulation overlay can be
+# compared against each muscle individually.
+
+print("\nCreating per-muscle supplementary figure...")
+supp_fig, _ = plot_cv_vs_fr_per_muscle(all_muscle_data, exp_data)
+supp_output_file = RESULTS_PATH / f"isi_cv_per_muscle_supplement.{OUTPUT_FORMAT}"
+if OUTPUT_FORMAT in ["jpg", "jpeg"]:
+ supp_fig.savefig(
+ supp_output_file, dpi=300, bbox_inches="tight", pil_kwargs={"quality": 95}
+ )
+else:
+ supp_fig.savefig(supp_output_file, dpi=300, bbox_inches="tight", transparent=True)
+plt.show()
+print(f"Supplementary plot saved to: {supp_output_file}")
+
##############################################################################
# Summary Statistics
# ------------------
diff --git a/examples/03_papers/watanabe/01_compute_baseline_force.py b/examples/03_papers/watanabe/01_compute_baseline_force.py
index 4fadb4da..3fe2e72a 100644
--- a/examples/03_papers/watanabe/01_compute_baseline_force.py
+++ b/examples/03_papers/watanabe/01_compute_baseline_force.py
@@ -68,7 +68,7 @@
from neo import Block, Segment, SpikeTrain
from neuron import h
-from myogen import RANDOM_GENERATOR
+from myogen import get_random_generator
from myogen.simulator import RecruitmentThresholds
from myogen.simulator.core.force.force_model import ForceModel
from myogen.simulator import Network
@@ -231,7 +231,7 @@
time_points = int(SIMULATION_TIME_MS / TIMESTEP_MS)
drive_signal = np.ones(time_points) * DD_DRIVE_HZ + np.clip(
- RANDOM_GENERATOR.normal(0, 1.0, size=time_points), 0, None
+ get_random_generator().normal(0, 1.0, size=time_points), 0, None
)
##############################################################################
@@ -428,14 +428,31 @@
mu_ids.append(i)
if firing_rates:
- axes[2].bar(mu_ids, firing_rates, alpha=0.7)
- axes[2].axhline(fr_mean, linestyle="--", label=f"Mean = {fr_mean:.1f} Hz")
- axes[2].set_xlabel("Motor Unit ID")
+ # Distribution plot with all data points (Editor 8): violin shell for
+ # the density, box-and-whisker for the quartiles, jittered points for
+ # every active motor unit.
+ fr_array = np.asarray(firing_rates)
+ fr_sd = float(np.std(fr_array, ddof=1)) if fr_array.size > 1 else 0.0
+ if fr_array.size >= 10:
+ axes[2].violinplot(
+ fr_array, positions=[0], widths=0.7, showmeans=False, showmedians=False
+ )
+ axes[2].boxplot(
+ fr_array, positions=[0], widths=0.3, showfliers=False
+ )
+ jitter = np.random.default_rng(0).uniform(-0.15, 0.15, size=fr_array.size)
+ axes[2].scatter(jitter, fr_array, alpha=0.6, s=18, zorder=3)
+ axes[2].axhline(
+ fr_mean, linestyle="--", label=f"Mean = {fr_mean:.1f} Hz (SD = {fr_sd:.1f} Hz)"
+ )
+ axes[2].set_xticks([0])
+ axes[2].set_xticklabels([f"n = {fr_array.size}"])
+ axes[2].set_xlabel("Motor Units")
axes[2].set_ylabel("Firing Rate (Hz)")
axes[2].set_title("Firing Rate Distribution")
axes[2].legend(framealpha=1.0, edgecolor="none")
-for ax in axes:
+for ax in axes[:2]:
ax.set_xlim(0, SIMULATION_TIME_MS / 1000)
plt.tight_layout()
diff --git a/examples/03_papers/watanabe/02_optimize_oscillating_dc.py b/examples/03_papers/watanabe/02_optimize_oscillating_dc.py
index b58be0fa..6197a600 100644
--- a/examples/03_papers/watanabe/02_optimize_oscillating_dc.py
+++ b/examples/03_papers/watanabe/02_optimize_oscillating_dc.py
@@ -72,7 +72,7 @@
from neo import Block, Segment, SpikeTrain
from neuron import h
-from myogen import RANDOM_GENERATOR, set_random_seed
+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
@@ -226,7 +226,7 @@ def run_simulation_with_oscillating_drive(dc_offset, recruitment_thresholds):
drive_signal = np.clip(drive_signal, 0, None)
# Add small noise
- drive_signal += np.clip(RANDOM_GENERATOR.normal(0, 1.0, size=time_points), 0, None)
+ drive_signal += np.clip(get_random_generator().normal(0, 1.0, size=time_points), 0, None)
# Initialize simulation
h.load_file("stdrun.hoc")
diff --git a/examples/03_papers/watanabe/03_10pct_mvc_simulation.py b/examples/03_papers/watanabe/03_10pct_mvc_simulation.py
index 7348ec6c..534bd9e5 100644
--- a/examples/03_papers/watanabe/03_10pct_mvc_simulation.py
+++ b/examples/03_papers/watanabe/03_10pct_mvc_simulation.py
@@ -77,7 +77,7 @@
import quantities as pq
from neuron import h
-from myogen import RANDOM_GENERATOR, load_nmodl_mechanisms
+from myogen import get_random_generator, load_nmodl_mechanisms
from myogen.simulator import RecruitmentThresholds
from myogen.simulator.neuron.network import Network
from myogen.simulator.neuron.populations import AlphaMN__Pool, DescendingDrive__Pool
@@ -204,7 +204,7 @@
DDdrive[phase3_mask] = DC_OFFSET_OPTIMIZED + 20 * np.sin(2 * np.pi * 20 * time_s[phase3_mask])
# Independent noise (IN) - 125 Hz constant (Watanabe specification)
-INdrive = 125.0 + RANDOM_GENERATOR.normal(0, 5.0, len(time))
+INdrive = 125.0 + get_random_generator().normal(0, 5.0, len(time))
plt.figure(figsize=(12, 6))
plt.subplot(2, 1, 1)
diff --git a/myogen/__init__.py b/myogen/__init__.py
index d0954f24..c71d3369 100644
--- a/myogen/__init__.py
+++ b/myogen/__init__.py
@@ -1,24 +1,104 @@
+import warnings
+
import numpy as np
from numpy.random import Generator
-SEED: int = 180319 # Seed for reproducibility
-RANDOM_GENERATOR: Generator = np.random.default_rng(SEED)
+_DEFAULT_SEED: int = 180319
+_current_seed: int = _DEFAULT_SEED
+_random_generator: Generator = np.random.default_rng(_DEFAULT_SEED)
+
+
+def get_random_generator() -> Generator:
+ """
+ Return the current global RNG.
+
+ Always reflects the most recent ``set_random_seed`` call. Prefer this
+ accessor over importing ``RANDOM_GENERATOR`` directly — a direct import
+ captures a stale reference that will not update when the seed changes.
+ """
+ return _random_generator
+
+
+def get_random_seed() -> int:
+ """Return the seed currently in effect."""
+ return _current_seed
+
+def derive_subseed(*labels: int) -> int:
+ """
+ Derive a deterministic, seed-tracking sub-seed from the current seed and a tuple of integer labels.
+
+ 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:
+ ``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
+ 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
+ integer; collisions remain possible in principle (birthday-paradox
+ probability ≈ ``N² / 2³³``) but are negligible for realistic motor-unit
+ pool sizes (≲ 10⁻⁷ at 1000 cells).
-def set_random_seed(seed: int = SEED) -> None:
+ Returns
+ -------
+ int
+ A non-negative 32-bit integer suitable for passing as a seed to
+ NumPy, sklearn, or the bundled Cython RNG wrappers.
+ """
+ seq = np.random.SeedSequence(entropy=(_current_seed, *labels))
+ return int(seq.generate_state(1, dtype=np.uint32)[0])
+
+
+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
+ 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.
+
Parameters
----------
seed : int, optional
- Seed value to set, by default SEED
+ Seed value to set, by default 180319.
"""
- global RANDOM_GENERATOR
- RANDOM_GENERATOR = np.random.default_rng(seed)
+ global _random_generator, _current_seed
+ _current_seed = seed
+ _random_generator = np.random.default_rng(seed)
print(f"Random seed set to {seed}.")
+def __getattr__(name: str):
+ """Backwards-compatible access for the deprecated ``RANDOM_GENERATOR`` and ``SEED`` module attributes."""
+ if name == "RANDOM_GENERATOR":
+ warnings.warn(
+ "myogen.RANDOM_GENERATOR is deprecated; use myogen.get_random_generator() "
+ "to always retrieve the current RNG. Module-level imports of "
+ "RANDOM_GENERATOR capture a stale reference that does not update when "
+ "set_random_seed() is called.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return _random_generator
+ if name == "SEED":
+ warnings.warn(
+ "myogen.SEED is deprecated; use myogen.get_random_seed() to retrieve "
+ "the seed currently in effect.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return _current_seed
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
+
+
class MyoGenSetupError(Exception):
"""Exception raised when MyoGen setup fails."""
@@ -217,8 +297,9 @@ def error(msg):
_nmodl_loaded = load_nmodl_mechanisms(quiet=True, strict=False)
__all__ = [
- "RANDOM_GENERATOR",
- "SEED",
+ "get_random_generator",
+ "get_random_seed",
+ "derive_subseed",
"set_random_seed",
"load_nmodl_mechanisms",
"NMODLLoadError",
diff --git a/myogen/simulator/core/emg/intramuscular/intramuscular_emg.py b/myogen/simulator/core/emg/intramuscular/intramuscular_emg.py
index 456c3b55..44a1afac 100644
--- a/myogen/simulator/core/emg/intramuscular/intramuscular_emg.py
+++ b/myogen/simulator/core/emg/intramuscular/intramuscular_emg.py
@@ -26,7 +26,7 @@
from neo import AnalogSignal, Block, Segment
from tqdm import tqdm
-from myogen import RANDOM_GENERATOR
+from myogen import get_random_generator
from myogen.simulator.core.emg.electrodes import IntramuscularElectrodeArray
from myogen.simulator.core.muscle import Muscle
from myogen.utils.decorators import beartowertype
@@ -825,7 +825,7 @@ def add_noise(self, snr__dB: float, noise_type: str = "gaussian") -> INTRAMUSCUL
# Generate noise
if noise_type.lower() == "gaussian":
# Generate standard normal noise, then scale per channel
- noise = RANDOM_GENERATOR.normal(loc=0.0, scale=1.0, size=emg_array.shape)
+ noise = get_random_generator().normal(loc=0.0, scale=1.0, size=emg_array.shape)
# Broadcast noise_std_per_channel along time axis
# noise shape: (time, n_electrodes)
# noise_std_per_channel shape: (n_electrodes,)
diff --git a/myogen/simulator/core/emg/intramuscular/motor_unit_sim.py b/myogen/simulator/core/emg/intramuscular/motor_unit_sim.py
index bc65b54d..387fa851 100644
--- a/myogen/simulator/core/emg/intramuscular/motor_unit_sim.py
+++ b/myogen/simulator/core/emg/intramuscular/motor_unit_sim.py
@@ -15,7 +15,7 @@
from sklearn.cluster import KMeans
from tqdm import tqdm
-from myogen import RANDOM_GENERATOR, SEED
+from myogen import derive_subseed, get_random_generator
from myogen.utils.decorators import beartowertype
from .bioelectric import (
get_current_density,
@@ -131,14 +131,14 @@ def sim_nmj_branches_two_layers(
arborization_z_std : float
Standard deviation of secondary arborization in mm
"""
- rng = RANDOM_GENERATOR
+ rng = get_random_generator()
self.nerve_paths = np.zeros(
(self._number_of_muscle_fibers, 2)
) # Point coordinates
kmeans = KMeans(
- n_clusters=n_branches, init="k-means++", max_iter=100, random_state=SEED
+ n_clusters=n_branches, init="k-means++", max_iter=100, random_state=derive_subseed(n_branches)
)
idx = kmeans.fit_predict(self.muscle_fiber_centers__mm)
c = kmeans.cluster_centers_
@@ -197,7 +197,7 @@ def sim_nmj_branches_gaussian(self, endplate_center: float, branches_z_std: floa
branches_z_std : float
Standard deviation of NMJ distribution in mm
"""
- rng = RANDOM_GENERATOR
+ rng = get_random_generator()
self.nmj_z = rng.normal(endplate_center, branches_z_std, self.Nmf)
# Simplified nerve paths (single segment)
@@ -408,7 +408,7 @@ def calc_muap(self, jitter_std: float = 0.0) -> np.ndarray:
raise ValueError("_dt not set - call calc_sfaps() first")
if jitter_std != 0:
- delays = jitter_std * RANDOM_GENERATOR.standard_normal(
+ delays = jitter_std * get_random_generator().standard_normal(
size=(self._number_of_muscle_fibers, 1)
)
jittered_sfaps = np.zeros_like(self.sfaps)
diff --git a/myogen/simulator/core/emg/surface/surface_emg.py b/myogen/simulator/core/emg/surface/surface_emg.py
index 3d6084b8..e9cf7efe 100644
--- a/myogen/simulator/core/emg/surface/surface_emg.py
+++ b/myogen/simulator/core/emg/surface/surface_emg.py
@@ -28,7 +28,7 @@
from scipy.signal import resample
from tqdm import tqdm
-from myogen import RANDOM_GENERATOR
+from myogen import get_random_generator
from myogen.simulator.core.emg.electrodes import SurfaceElectrodeArray
from myogen.simulator.core.emg.surface.simulate_fiber import simulate_fiber_v2, _simulate_fiber_v2_python
from myogen.simulator.core.muscle import Muscle
@@ -267,7 +267,7 @@ def simulate_muaps(self, n_jobs: int = -2, verbose: bool = True) -> SURFACE_MUAP
n_motor_units = len(number_of_fibers_per_MUs)
# Pre-calculate innervation zones for all MUs
- innervation_zones = RANDOM_GENERATOR.uniform(
+ innervation_zones = get_random_generator().uniform(
low=-innervation_zone_variance / 2,
high=innervation_zone_variance / 2,
size=n_motor_units,
@@ -338,7 +338,7 @@ def _process_single_mu(
innervation_zone = innervation_zones[MU_index]
# Batch generate random fiber lengths (optimization: single RNG call)
- fiber_length_variations = RANDOM_GENERATOR.uniform(
+ fiber_length_variations = get_random_generator().uniform(
low=-self._var_fiber_length__mm,
high=self._var_fiber_length__mm,
size=number_of_fibers,
@@ -859,7 +859,7 @@ def add_noise(self, snr__dB: float, noise_type: str = "gaussian") -> SURFACE_EMG
# Generate noise
if noise_type.lower() == "gaussian":
# Generate standard normal noise, then scale per channel
- noise = RANDOM_GENERATOR.normal(loc=0.0, scale=1.0, size=emg_array.shape)
+ noise = get_random_generator().normal(loc=0.0, scale=1.0, size=emg_array.shape)
# Broadcast noise_std_per_channel along time axis
# noise shape: (time, rows, cols)
# noise_std_per_channel shape: (rows, cols)
diff --git a/myogen/simulator/core/muscle/muscle.py b/myogen/simulator/core/muscle/muscle.py
index b821200f..9e04140f 100644
--- a/myogen/simulator/core/muscle/muscle.py
+++ b/myogen/simulator/core/muscle/muscle.py
@@ -12,7 +12,7 @@
from tqdm import tqdm
import quantities as pq
-from myogen import RANDOM_GENERATOR
+from myogen import get_random_generator
from myogen.utils.types import (
RECRUITMENT_THRESHOLDS__ARRAY,
Quantity__S_per_m,
@@ -101,12 +101,12 @@ class Muscle:
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]_.
length__mm : float, default=30.0
- Length of the muscle in millimeters. Default is set to 30.0 mm as determined by no one.
+ 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]_.
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 set to 0.25 as determined by no one but it is a good starting point.
+ Default is 0.25 as a pragmatic upper bound for the FDI, with no single published source; revisit for larger muscles.
A value of 0.25 means the largest motor unit can innervate up to 25%
of the total muscle cross-sectional area.
Must be in range (0, 1].
@@ -317,7 +317,7 @@ def _generate_fiber_properties(self) -> None:
std_diameter__mm = 15e-3 # mm (15 um)
self._muscle_fiber_diameters__mm = (
- RANDOM_GENERATOR.lognormal(mean=np.log(mean_diameter__mm), sigma=0.3, size=n_fibers)
+ get_random_generator().lognormal(mean=np.log(mean_diameter__mm), sigma=0.3, size=n_fibers)
* pq.mm
)
@@ -334,7 +334,7 @@ def _generate_fiber_properties(self) -> None:
# Add some biological variability
cv_base = k * self._muscle_fiber_diameters__mm + c
- cv_noise = RANDOM_GENERATOR.normal(0, 0.2, n_fibers) * pq.m / pq.s # 20% CV variation
+ cv_noise = get_random_generator().normal(0, 0.2, n_fibers) * pq.m / pq.s # 20% CV variation
self._muscle_fiber_conduction_velocities__mm_per_s = cv_base + cv_noise
@@ -717,7 +717,7 @@ def probfun(y, x):
# Assignment procedure
self._assignment = np.full(self._number_of_muscle_fibers, np.nan)
- randomized_mf = RANDOM_GENERATOR.permutation(self._number_of_muscle_fibers)
+ randomized_mf = get_random_generator().permutation(self._number_of_muscle_fibers)
for mf in tqdm(randomized_mf, desc="Assigning muscle fibers to motor neurons", unit="MF", disable=not verbose):
# Vectorized computation of likelihoods for all motor units
@@ -753,7 +753,7 @@ def probfun(y, x):
probs = np.ones(self._number_of_neurons) / self._number_of_neurons
# Sample from the probability distribution (equivalent to MATLAB's randsample)
- self._assignment[mf] = RANDOM_GENERATOR.choice(self._number_of_neurons, p=probs)
+ self._assignment[mf] = get_random_generator().choice(self._number_of_neurons, p=probs)
if verbose:
print(f"Assignment completed. {self._number_of_muscle_fibers} muscle fibers assigned.")
diff --git a/myogen/simulator/neuron/cells.py b/myogen/simulator/neuron/cells.py
index 11d2117b..1b84dfc3 100644
--- a/myogen/simulator/neuron/cells.py
+++ b/myogen/simulator/neuron/cells.py
@@ -9,7 +9,7 @@
import quantities as pq
from neuron import h
-from myogen import RANDOM_GENERATOR, SEED
+from myogen import derive_subseed, get_random_generator
from myogen.simulator.neuron._cython._gamma_process_generator import (
_GammaProcessGenerator__Cython,
)
@@ -409,7 +409,7 @@ def __init__(self, N, dt, pool__ID: int | None = None):
self.ns = h.DUMMY() # Dummy cell
_Cell.__init__(self, next(self._ids2), pool__ID)
_PoissonProcessGenerator__Cython.__init__(
- self, SEED + (self.class__ID + 1) * (self.global__ID + 1), N, dt
+ self, derive_subseed(self.class__ID, self.global__ID), N, dt
)
def __repr__(self) -> str:
@@ -494,7 +494,7 @@ def __init__(
_Cell.__init__(self, next(self._ids2), pool__ID)
_GammaProcessGenerator__Cython.__init__(
self,
- SEED + (self.class__ID + 1) * (self.global__ID + 1),
+ derive_subseed(self.class__ID, self.global__ID),
shape,
timestep__ms.magnitude,
)
@@ -584,12 +584,12 @@ def __init__(
self.ns = h.DUMMY() # Dummy cell
self.RT = RT # Recruitment Threshold
- self.IFR = RANDOM_GENERATOR.normal(5, 2.5) # Individual variability
+ self.IFR = get_random_generator().normal(5, 2.5) # Individual variability
_Cell.__init__(self, class__ID if class__ID is not None else next(self._ids2), pool__ID)
_GammaProcessGenerator__Cython.__init__(
self,
- seed=SEED + (self.class__ID + 1) * (self.global__ID + 1),
+ seed=derive_subseed(self.class__ID, self.global__ID),
shape=N, # Shape parameter controls ISI CV = 1/sqrt(N)
dt=timestep__ms.magnitude,
)
diff --git a/myogen/simulator/neuron/network.py b/myogen/simulator/neuron/network.py
index 59afa3e1..c7a111c1 100644
--- a/myogen/simulator/neuron/network.py
+++ b/myogen/simulator/neuron/network.py
@@ -10,7 +10,7 @@
import quantities as pq
from neuron import h
-from myogen import RANDOM_GENERATOR
+from myogen import get_random_generator
from myogen.simulator.neuron.populations import (
AffIa__Pool,
AffII__Pool,
@@ -79,10 +79,10 @@ def _select_synapse(target_neuron, inhibitory: bool = False):
]
if matching_synapses:
- return RANDOM_GENERATOR.choice(matching_synapses)
+ return get_random_generator().choice(matching_synapses)
else:
# Fallback to random selection if no matching synapses found
- return RANDOM_GENERATOR.choice(synapse_list)
+ return get_random_generator().choice(synapse_list)
# Helper functions for create_netcon
@@ -263,7 +263,7 @@ def _connect_population_to_population(
for source_neuron in populations[source_pop]:
# Randomly select exactly n_connections target neurons
- selected_targets = RANDOM_GENERATOR.choice(
+ selected_targets = get_random_generator().choice(
target_neurons, size=n_connections, replace=False
)
@@ -289,7 +289,7 @@ def _connect_population_to_population(
# Probabilistic connectivity: each pair has probability of connecting
for source_neuron in populations[source_pop]:
for target_neuron in target_neurons:
- if RANDOM_GENERATOR.uniform() < connection_probability:
+ if get_random_generator().uniform() < connection_probability:
target_synapse = _select_synapse(target_neuron, inhibitory=inhibitory)
netcon = _create_netcon(
source_neuron,
@@ -431,7 +431,7 @@ def _connect_one_to_one(
connections = []
for source_neuron, target_neuron in zip(source_neurons, target_neurons):
# Check if this pair should be connected
- if RANDOM_GENERATOR.uniform() < connection_probability:
+ if get_random_generator().uniform() < connection_probability:
target_synapse = _select_synapse(target_neuron, inhibitory=inhibitory)
netcon = _create_netcon(
source_neuron,
diff --git a/myogen/utils/helper.py b/myogen/utils/helper.py
index 83f29e99..47389ba0 100644
--- a/myogen/utils/helper.py
+++ b/myogen/utils/helper.py
@@ -144,8 +144,14 @@ def calculate_firing_rate_statistics(
firing_rates.append(neuron_fr)
if return_per_neuron:
- # Compute CV of inter-spike intervals
- cv = np.std(isis_array, ddof=1) / np.mean(isis_array)
+ # Compute CV of inter-spike intervals. Guard against
+ # the n=1 NaN case when a caller lowers
+ # min_spikes_for_cv to 2 (only one ISI is observable).
+ cv = (
+ np.std(isis_array, ddof=1) / np.mean(isis_array)
+ if len(isis_array) > 1
+ else 0.0
+ )
per_neuron_results.append(
{
"MU_ID": mu_id,
@@ -169,10 +175,13 @@ def calculate_firing_rate_statistics(
"firing_rates": np.array([]),
}
- # Ensemble statistics: mean across neurons for both FR and SD_FR
+ # Ensemble statistics: mean across neurons for both FR and SD_FR.
+ # np.std with ddof=1 is undefined for n<2, so report 0.0 rather than
+ # NaN when only one unit is active.
+ fr_std = np.std(firing_rates, ddof=1) if len(firing_rates) > 1 else 0.0
return {
"FR_mean": np.mean(firing_rates),
- "FR_std": np.std(firing_rates, ddof=1),
+ "FR_std": fr_std,
"n_active": len(firing_rates),
"firing_rates": np.array(firing_rates),
}
diff --git a/pyproject.toml b/pyproject.toml
index 14ac2fa4..38c15e41 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -29,13 +29,13 @@ classifiers = [
dependencies = [
"beartype>=0.21.0",
"cython>=3.1.3",
- "elephant>=1.1.1",
"impi-rt>=2021.15.0 ; sys_platform == 'win32'",
"matplotlib>=3.10.1",
"mpi4py>=4.0.3",
"neuron==8.2.7 ; sys_platform == 'linux' or sys_platform == 'darwin'",
#"neuron-gpu-nightly>=9.0a0",
#"neuron-nightly>=9.0a1.dev1492",
+ "neo>=0.14.0",
"numba>=0.61.2",
"numpy>=1.26,<2.0",
"optuna>=4.5.0",
@@ -47,10 +47,13 @@ dependencies = [
"seaborn>=0.13.2",
"setuptools>=80.9.0",
"tqdm>=4.67.1",
- "viziphant>=0.4.0",
]
[project.optional-dependencies]
+elephant = [
+ "elephant>=1.1.1",
+ "viziphant>=0.4.0",
+]
nwb = [
"pynwb>=2.8.0",
"nwbinspector>=0.5.0",
@@ -71,6 +74,7 @@ possibly-unbound-import = "ignore"
dev = [
"pandas-stubs>=2.3.0.250703",
"poethepoet>=0.37.0",
+ "pytest>=8.0",
"scipy-stubs>=1.16.1.0",
]
docs = [
@@ -81,7 +85,7 @@ docs = [
"pygments>=2.19.2",
"rinohtype>=0.5.5",
"roman>=5.2",
- "sphinx>=8.1.3",
+ "sphinx>=8.1.3,<9",
"sphinx-autodoc-typehints>=2.5.0",
"sphinx-design>=0.6.1",
"sphinx-gallery>=0.19.0",
@@ -91,8 +95,16 @@ docs = [
# NWB support for examples
"pynwb>=2.8.0",
"nwbinspector>=0.5.0",
+ # Spike-train analysis + visualization needed by several examples
+ # (elephant is also declared as an optional extra; the docs build
+ # needs both for sphinx-gallery to render those examples).
+ "elephant>=1.1.1",
+ "viziphant>=0.4.0",
]
+[tool.pytest.ini_options]
+testpaths = ["tests"]
+
[tool.poe.tasks]
setup_myogen = "python -c 'from myogen import _setup_myogen; _setup_myogen()'"
diff --git a/setup.py b/setup.py
index 5ca35d25..1bb0932f 100644
--- a/setup.py
+++ b/setup.py
@@ -201,11 +201,11 @@ def compile_nmodl(self):
MyoGen Installation Failed - NEURON Required
================================================================================
-MyoGen requires NEURON 8.2.6 to be installed BEFORE installing MyoGen.
+MyoGen requires NEURON 8.2.7 to be installed BEFORE installing MyoGen.
STEP 1: Download and install NEURON
-----------------------------------
- https://github.com/neuronsimulator/nrn/releases/download/8.2.6/nrn-8.2.6.w64-mingw-py-38-39-310-311-312-setup.exe
+ https://github.com/neuronsimulator/nrn/releases/download/8.2.7/nrn-8.2.7.w64-mingw-py-39-310-311-312-313-setup.exe
During installation, select "Add to PATH"
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/test_determinism.py b/tests/test_determinism.py
new file mode 100644
index 00000000..ca4967b6
--- /dev/null
+++ b/tests/test_determinism.py
@@ -0,0 +1,139 @@
+"""Regression tests for the RNG architecture (R1 Code Review 1 and 2).
+
+These tests verify that ``set_random_seed`` propagates correctly through the
+accessor pattern and that no module captures a stale reference to the global
+RNG or to the deprecated module-level ``SEED`` constant.
+"""
+
+from __future__ import annotations
+
+import warnings
+
+import numpy as np
+import pytest
+
+import myogen
+from myogen import get_random_generator, get_random_seed, set_random_seed
+
+
+def _draw(n: int = 8) -> np.ndarray:
+ return get_random_generator().integers(0, 10**9, n)
+
+
+def test_same_seed_yields_same_draws():
+ set_random_seed(12345)
+ first = _draw()
+ set_random_seed(12345)
+ second = _draw()
+ assert np.array_equal(first, second)
+
+
+def test_different_seed_yields_different_draws():
+ set_random_seed(1)
+ first = _draw()
+ set_random_seed(2)
+ second = _draw()
+ assert not np.array_equal(first, second)
+
+
+def test_get_random_seed_reflects_latest_set_call():
+ set_random_seed(777)
+ assert get_random_seed() == 777
+ set_random_seed(999)
+ assert get_random_seed() == 999
+
+
+def test_accessor_returns_current_generator_after_rebind():
+ """A caller that holds ``get_random_generator`` (the function, not the generator) must see the new RNG after each ``set_random_seed`` call — this is the regression for CR1."""
+ set_random_seed(100)
+ before = _draw()
+ set_random_seed(200)
+ after = _draw()
+ assert not np.array_equal(before, after)
+
+ # Restoring the original seed must reproduce the first sequence.
+ set_random_seed(100)
+ restored = _draw()
+ assert np.array_equal(before, restored)
+
+
+def test_derived_seed_pattern_respects_set_random_seed():
+ """Regression for CR2: code that derives sub-seeds via ``get_random_seed() + offset`` must observe new values after ``set_random_seed``."""
+ set_random_seed(10)
+ derived_a = get_random_seed() + 3 * 7
+ set_random_seed(20)
+ derived_b = get_random_seed() + 3 * 7
+ assert derived_a != derived_b
+
+
+def test_deprecated_RANDOM_GENERATOR_emits_warning_and_returns_current():
+ set_random_seed(42)
+ with warnings.catch_warnings(record=True) as caught:
+ warnings.simplefilter("always")
+ gen_via_deprecated = myogen.RANDOM_GENERATOR
+ assert any(
+ issubclass(w.category, DeprecationWarning) for w in caught
+ ), "accessing myogen.RANDOM_GENERATOR must emit a DeprecationWarning"
+ assert gen_via_deprecated is get_random_generator(), (
+ "deprecated attribute must return the current global generator"
+ )
+
+ # Confirm it reflects later ``set_random_seed`` calls.
+ set_random_seed(43)
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore", DeprecationWarning)
+ gen_again = myogen.RANDOM_GENERATOR
+ assert gen_again is get_random_generator()
+ assert gen_again is not gen_via_deprecated
+
+
+def test_deprecated_SEED_emits_warning_and_returns_current_seed():
+ set_random_seed(5150)
+ with warnings.catch_warnings(record=True) as caught:
+ warnings.simplefilter("always")
+ seed_via_deprecated = myogen.SEED
+ assert any(
+ issubclass(w.category, DeprecationWarning) for w in caught
+ ), "accessing myogen.SEED must emit a DeprecationWarning"
+ assert seed_via_deprecated == 5150
+
+
+def test_derive_subseed_avoids_old_collision_pattern():
+ """Regression for the earlier ``SEED + (a+1)*(b+1)`` collision: (0, 5) and (1, 2) both produced +6. The new SeedSequence-based mixing is uint32 and therefore not literally collision-free, but it must not reproduce the swapped-factor collision, and a 16x16 grid must be collision-free in practice."""
+ set_random_seed(42)
+ assert myogen.derive_subseed(0, 5) != myogen.derive_subseed(1, 2)
+ assert myogen.derive_subseed(1, 2) != myogen.derive_subseed(2, 1)
+
+ subseeds = {
+ (a, b): myogen.derive_subseed(a, b)
+ for a in range(16)
+ for b in range(16)
+ }
+ assert len(set(subseeds.values())) == len(subseeds), (
+ "derive_subseed must yield a unique value across a 16x16 label grid"
+ )
+
+
+def test_derive_subseed_tracks_set_random_seed():
+ set_random_seed(1)
+ sub_a = myogen.derive_subseed(3, 7)
+ set_random_seed(2)
+ sub_b = myogen.derive_subseed(3, 7)
+ assert sub_a != sub_b, (
+ "derive_subseed must change when the global seed changes"
+ )
+
+
+def test_modules_that_use_accessor_are_not_stale_after_seed_change():
+ """End-to-end: reimport a module that calls ``get_random_generator()`` and confirm its RNG draws track the current seed."""
+ from myogen.simulator.core.muscle import muscle as muscle_module
+
+ # The muscle module imports ``get_random_generator`` at top level; after
+ # ``set_random_seed``, any call it makes to ``get_random_generator()`` must
+ # return the new generator. Verify by calling the function directly.
+ set_random_seed(11)
+ gen_before = muscle_module.get_random_generator()
+ set_random_seed(22)
+ gen_after = muscle_module.get_random_generator()
+ assert gen_before is not gen_after
+ assert gen_after is get_random_generator()
diff --git a/tests/test_firing_rate_statistics.py b/tests/test_firing_rate_statistics.py
new file mode 100644
index 00000000..f95aac98
--- /dev/null
+++ b/tests/test_firing_rate_statistics.py
@@ -0,0 +1,79 @@
+"""Regression test for R1 Code Review 3: FR_std must not be NaN when only a single unit is active."""
+
+from __future__ import annotations
+
+import math
+
+import neo
+import numpy as np
+import pytest
+import quantities as pq
+
+from myogen.utils.helper import calculate_firing_rate_statistics
+
+
+def _build_spiketrain(times_ms, t_stop_ms=2000.0):
+ """Construct a ``neo.SpikeTrain`` in milliseconds."""
+ return neo.SpikeTrain(np.asarray(times_ms) * pq.ms, t_stop=t_stop_ms * pq.ms)
+
+
+def test_fr_std_is_zero_not_nan_for_single_active_unit():
+ """When only one unit passes the firing-rate filter, ``FR_std`` must be 0.0, never NaN — otherwise downstream analyses silently corrupt (CR3)."""
+ active = _build_spiketrain(np.linspace(100.0, 1900.0, num=30))
+ silent = _build_spiketrain([])
+
+ stats = calculate_firing_rate_statistics(
+ [active, silent],
+ plateau_start_ms=100.0,
+ plateau_end_ms=1900.0,
+ )
+
+ assert stats["n_active"] == 1
+ assert not math.isnan(stats["FR_std"]), "FR_std must not be NaN when n_active == 1"
+ assert stats["FR_std"] == 0.0
+
+
+def test_fr_std_is_zero_for_empty_population():
+ """Empty-population branch already returned 0.0 and must keep doing so."""
+ silent = _build_spiketrain([])
+ stats = calculate_firing_rate_statistics(
+ [silent, silent],
+ plateau_start_ms=0.0,
+ plateau_end_ms=2000.0,
+ )
+ assert stats["n_active"] == 0
+ assert stats["FR_std"] == 0.0
+
+
+def test_per_neuron_cv_is_finite_with_min_spikes_for_cv_2():
+ """When a caller lowers ``min_spikes_for_cv`` to 2, a neuron with exactly two spikes produces one ISI — ``np.std(..., ddof=1)`` would otherwise be NaN. The guard at the sample-std call site must return a finite CV."""
+ two_spikes = _build_spiketrain([200.0, 1000.0])
+ silent = _build_spiketrain([])
+
+ df = calculate_firing_rate_statistics(
+ [two_spikes, silent],
+ plateau_start_ms=100.0,
+ plateau_end_ms=1900.0,
+ return_per_neuron=True,
+ min_spikes_for_cv=2,
+ )
+
+ assert len(df) == 1
+ cv = df["CV_ISI"].iloc[0]
+ assert math.isfinite(cv), "CV_ISI must not be NaN for n=1 ISI"
+
+
+def test_fr_std_is_finite_and_positive_for_multiple_active_units():
+ """Two active units with distinct rates must produce a positive, finite FR_std."""
+ fast = _build_spiketrain(np.linspace(100.0, 1900.0, num=40)) # ~22 Hz
+ slow = _build_spiketrain(np.linspace(100.0, 1900.0, num=15)) # ~7.5 Hz
+
+ stats = calculate_firing_rate_statistics(
+ [fast, slow],
+ plateau_start_ms=100.0,
+ plateau_end_ms=1900.0,
+ )
+
+ assert stats["n_active"] == 2
+ assert math.isfinite(stats["FR_std"])
+ assert stats["FR_std"] > 0.0
diff --git a/uv.lock b/uv.lock
index 2472378e..bb0cbc26 100644
--- a/uv.lock
+++ b/uv.lock
@@ -202,23 +202,23 @@ wheels = [
[[package]]
name = "autodocsumm"
-version = "0.2.14"
+version = "0.2.15"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "sphinx" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/03/96/92afe8a7912b327c01f0a8b6408c9556ee13b1aba5b98d587ac7327ff32d/autodocsumm-0.2.14.tar.gz", hash = "sha256:2839a9d4facc3c4eccd306c08695540911042b46eeafcdc3203e6d0bab40bc77", size = 46357, upload-time = "2024-10-23T18:51:47.369Z" }
+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/87/bc/3f66af9beb683728e06ca08797e4e9d3e44f432f339718cae3ba856a9cad/autodocsumm-0.2.14-py3-none-any.whl", hash = "sha256:3bad8717fc5190802c60392a7ab04b9f3c97aa9efa8b3780b3d81d615bfe5dc0", size = 14640, upload-time = "2024-10-23T18:51:45.115Z" },
+ { 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.17.0"
+version = "2.18.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" },
+ { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" },
]
[[package]]
@@ -616,11 +616,11 @@ wheels = [
[[package]]
name = "filelock"
-version = "3.20.1"
+version = "3.28.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/a7/23/ce7a1126827cedeb958fc043d61745754464eb56c5937c35bbf2b8e26f34/filelock-3.20.1.tar.gz", hash = "sha256:b8360948b351b80f420878d8516519a2204b07aefcdcfd24912a5d33127f188c", size = 19476, upload-time = "2025-12-15T23:54:28.027Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/d6/17/6e8890271880903e3538660a21d63a6c1fea969ac71d0d6b608b78727fa9/filelock-3.28.0.tar.gz", hash = "sha256:4ed1010aae813c4ee8d9c660e4792475ee60c4a0ba76073ceaf862bd317e3ca6", size = 56474, upload-time = "2026-04-14T22:54:33.625Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/e3/7f/a1a97644e39e7316d850784c642093c99df1290a460df4ede27659056834/filelock-3.20.1-py3-none-any.whl", hash = "sha256:15d9e9a67306188a44baa72f569d2bfd803076269365fdea0934385da4dc361a", size = 16666, upload-time = "2025-12-15T23:54:26.874Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/21/2f728888c45033d34a417bfcd248ea2564c9e08ab1bfd301377cf05d5586/filelock-3.28.0-py3-none-any.whl", hash = "sha256:de9af6712788e7171df1b28b15eba2446c69721433fa427a9bee07b17820a9db", size = 39189, upload-time = "2026-04-14T22:54:32.037Z" },
]
[[package]]
@@ -780,7 +780,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f8/0a/a3871375c7b9727edaeeea994bfff7c63ff7804c9829c19309ba2e058807/greenlet-3.3.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:b01548f6e0b9e9784a2c99c5651e5dc89ffcbe870bc5fb2e5ef864e9cc6b5dcb", size = 276379, upload-time = "2025-12-04T14:23:30.498Z" },
{ url = "https://files.pythonhosted.org/packages/43/ab/7ebfe34dce8b87be0d11dae91acbf76f7b8246bf9d6b319c741f99fa59c6/greenlet-3.3.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349345b770dc88f81506c6861d22a6ccd422207829d2c854ae2af8025af303e3", size = 597294, upload-time = "2025-12-04T14:50:06.847Z" },
{ url = "https://files.pythonhosted.org/packages/a4/39/f1c8da50024feecd0793dbd5e08f526809b8ab5609224a2da40aad3a7641/greenlet-3.3.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e8e18ed6995e9e2c0b4ed264d2cf89260ab3ac7e13555b8032b25a74c6d18655", size = 607742, upload-time = "2025-12-04T14:57:42.349Z" },
- { url = "https://files.pythonhosted.org/packages/77/cb/43692bcd5f7a0da6ec0ec6d58ee7cddb606d055ce94a62ac9b1aa481e969/greenlet-3.3.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c024b1e5696626890038e34f76140ed1daf858e37496d33f2af57f06189e70d7", size = 622297, upload-time = "2025-12-04T15:07:13.552Z" },
{ url = "https://files.pythonhosted.org/packages/75/b0/6bde0b1011a60782108c01de5913c588cf51a839174538d266de15e4bf4d/greenlet-3.3.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:047ab3df20ede6a57c35c14bf5200fcf04039d50f908270d3f9a7a82064f543b", size = 609885, upload-time = "2025-12-04T14:26:02.368Z" },
{ url = "https://files.pythonhosted.org/packages/49/0e/49b46ac39f931f59f987b7cd9f34bfec8ef81d2a1e6e00682f55be5de9f4/greenlet-3.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2d9ad37fc657b1102ec880e637cccf20191581f75c64087a549e66c57e1ceb53", size = 1567424, upload-time = "2025-12-04T15:04:23.757Z" },
{ url = "https://files.pythonhosted.org/packages/05/f5/49a9ac2dff7f10091935def9165c90236d8f175afb27cbed38fb1d61ab6b/greenlet-3.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83cd0e36932e0e7f36a64b732a6f60c2fc2df28c351bae79fbaf4f8092fe7614", size = 1636017, upload-time = "2025-12-04T14:27:29.688Z" },
@@ -788,7 +787,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/02/2f/28592176381b9ab2cafa12829ba7b472d177f3acc35d8fbcf3673d966fff/greenlet-3.3.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:a1e41a81c7e2825822f4e068c48cb2196002362619e2d70b148f20a831c00739", size = 275140, upload-time = "2025-12-04T14:23:01.282Z" },
{ url = "https://files.pythonhosted.org/packages/2c/80/fbe937bf81e9fca98c981fe499e59a3f45df2a04da0baa5c2be0dca0d329/greenlet-3.3.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f515a47d02da4d30caaa85b69474cec77b7929b2e936ff7fb853d42f4bf8808", size = 599219, upload-time = "2025-12-04T14:50:08.309Z" },
{ url = "https://files.pythonhosted.org/packages/c2/ff/7c985128f0514271b8268476af89aee6866df5eec04ac17dcfbc676213df/greenlet-3.3.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d2d9fd66bfadf230b385fdc90426fcd6eb64db54b40c495b72ac0feb5766c54", size = 610211, upload-time = "2025-12-04T14:57:43.968Z" },
- { url = "https://files.pythonhosted.org/packages/79/07/c47a82d881319ec18a4510bb30463ed6891f2ad2c1901ed5ec23d3de351f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30a6e28487a790417d036088b3bcb3f3ac7d8babaa7d0139edbaddebf3af9492", size = 624311, upload-time = "2025-12-04T15:07:14.697Z" },
{ url = "https://files.pythonhosted.org/packages/fd/8e/424b8c6e78bd9837d14ff7df01a9829fc883ba2ab4ea787d4f848435f23f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:087ea5e004437321508a8d6f20efc4cfec5e3c30118e1417ea96ed1d93950527", size = 612833, upload-time = "2025-12-04T14:26:03.669Z" },
{ url = "https://files.pythonhosted.org/packages/b5/ba/56699ff9b7c76ca12f1cdc27a886d0f81f2189c3455ff9f65246780f713d/greenlet-3.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab97cf74045343f6c60a39913fa59710e4bd26a536ce7ab2397adf8b27e67c39", size = 1567256, upload-time = "2025-12-04T15:04:25.276Z" },
{ url = "https://files.pythonhosted.org/packages/1e/37/f31136132967982d698c71a281a8901daf1a8fbab935dce7c0cf15f942cc/greenlet-3.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5375d2e23184629112ca1ea89a53389dddbffcf417dad40125713d88eb5f96e8", size = 1636483, upload-time = "2025-12-04T14:27:30.804Z" },
@@ -796,7 +794,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d7/7c/f0a6d0ede2c7bf092d00bc83ad5bafb7e6ec9b4aab2fbdfa6f134dc73327/greenlet-3.3.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:60c2ef0f578afb3c8d92ea07ad327f9a062547137afe91f38408f08aacab667f", size = 275671, upload-time = "2025-12-04T14:23:05.267Z" },
{ url = "https://files.pythonhosted.org/packages/44/06/dac639ae1a50f5969d82d2e3dd9767d30d6dbdbab0e1a54010c8fe90263c/greenlet-3.3.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a5d554d0712ba1de0a6c94c640f7aeba3f85b3a6e1f2899c11c2c0428da9365", size = 646360, upload-time = "2025-12-04T14:50:10.026Z" },
{ url = "https://files.pythonhosted.org/packages/e0/94/0fb76fe6c5369fba9bf98529ada6f4c3a1adf19e406a47332245ef0eb357/greenlet-3.3.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3a898b1e9c5f7307ebbde4102908e6cbfcb9ea16284a3abe15cab996bee8b9b3", size = 658160, upload-time = "2025-12-04T14:57:45.41Z" },
- { url = "https://files.pythonhosted.org/packages/93/79/d2c70cae6e823fac36c3bbc9077962105052b7ef81db2f01ec3b9bf17e2b/greenlet-3.3.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dcd2bdbd444ff340e8d6bdf54d2f206ccddbb3ccfdcd3c25bf4afaa7b8f0cf45", size = 671388, upload-time = "2025-12-04T15:07:15.789Z" },
{ url = "https://files.pythonhosted.org/packages/b8/14/bab308fc2c1b5228c3224ec2bf928ce2e4d21d8046c161e44a2012b5203e/greenlet-3.3.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5773edda4dc00e173820722711d043799d3adb4f01731f40619e07ea2750b955", size = 660166, upload-time = "2025-12-04T14:26:05.099Z" },
{ url = "https://files.pythonhosted.org/packages/4b/d2/91465d39164eaa0085177f61983d80ffe746c5a1860f009811d498e7259c/greenlet-3.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ac0549373982b36d5fd5d30beb8a7a33ee541ff98d2b502714a09f1169f31b55", size = 1615193, upload-time = "2025-12-04T15:04:27.041Z" },
{ url = "https://files.pythonhosted.org/packages/42/1b/83d110a37044b92423084d52d5d5a3b3a73cafb51b547e6d7366ff62eff1/greenlet-3.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d198d2d977460358c3b3a4dc844f875d1adb33817f0613f663a656f463764ccc", size = 1683653, upload-time = "2025-12-04T14:27:32.366Z" },
@@ -804,7 +801,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/66/bd6317bc5932accf351fc19f177ffba53712a202f9df10587da8df257c7e/greenlet-3.3.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:d6ed6f85fae6cdfdb9ce04c9bf7a08d666cfcfb914e7d006f44f840b46741931", size = 282638, upload-time = "2025-12-04T14:25:20.941Z" },
{ url = "https://files.pythonhosted.org/packages/30/cf/cc81cb030b40e738d6e69502ccbd0dd1bced0588e958f9e757945de24404/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9125050fcf24554e69c4cacb086b87b3b55dc395a8b3ebe6487b045b2614388", size = 651145, upload-time = "2025-12-04T14:50:11.039Z" },
{ url = "https://files.pythonhosted.org/packages/9c/ea/1020037b5ecfe95ca7df8d8549959baceb8186031da83d5ecceff8b08cd2/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:87e63ccfa13c0a0f6234ed0add552af24cc67dd886731f2261e46e241608bee3", size = 654236, upload-time = "2025-12-04T14:57:47.007Z" },
- { url = "https://files.pythonhosted.org/packages/69/cc/1e4bae2e45ca2fa55299f4e85854606a78ecc37fead20d69322f96000504/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2662433acbca297c9153a4023fe2161c8dcfdcc91f10433171cf7e7d94ba2221", size = 662506, upload-time = "2025-12-04T15:07:16.906Z" },
{ url = "https://files.pythonhosted.org/packages/57/b9/f8025d71a6085c441a7eaff0fd928bbb275a6633773667023d19179fe815/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c6e9b9c1527a78520357de498b0e709fb9e2f49c3a513afd5a249007261911b", size = 653783, upload-time = "2025-12-04T14:26:06.225Z" },
{ url = "https://files.pythonhosted.org/packages/f6/c7/876a8c7a7485d5d6b5c6821201d542ef28be645aa024cfe1145b35c120c1/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:286d093f95ec98fdd92fcb955003b8a3d054b4e2cab3e2707a5039e7b50520fd", size = 1614857, upload-time = "2025-12-04T15:04:28.484Z" },
{ url = "https://files.pythonhosted.org/packages/4f/dc/041be1dff9f23dac5f48a43323cd0789cb798342011c19a248d9c9335536/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c10513330af5b8ae16f023e8ddbfb486ab355d04467c4679c5cfe4659975dd9", size = 1676034, upload-time = "2025-12-04T14:27:33.531Z" },
@@ -923,11 +919,11 @@ wheels = [
[[package]]
name = "imagesize"
-version = "1.4.1"
+version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026, upload-time = "2022-07-01T12:21:05.687Z" }
+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/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769, upload-time = "2022-07-01T12:21:02.467Z" },
+ { 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]]
@@ -938,6 +934,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/22/a8/31b67dfecd27656045900ad63292d62a6679bdeb2fdf9b6d16dcd6f90e67/impi_rt-2021.17.1-py2.py3-none-win_amd64.whl", hash = "sha256:614fdd114caf4323eb9d7b5e68c7101b8af1e0d8fb4e546fd0c2a4787f142f93", size = 17439430, upload-time = "2025-12-04T09:51:31.144Z" },
]
+[[package]]
+name = "iniconfig"
+version = "2.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
+]
+
[[package]]
name = "isodate"
version = "0.7.2"
@@ -1122,14 +1127,14 @@ wheels = [
[[package]]
name = "markdown-it-py"
-version = "3.0.0"
+version = "4.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mdurl" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" },
+ { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
]
[[package]]
@@ -1284,11 +1289,11 @@ wheels = [
[[package]]
name = "more-itertools"
-version = "10.8.0"
+version = "11.0.2"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/a2/f7/139d22fef48ac78127d18e01d80cf1be40236ae489769d17f35c3d425293/more_itertools-11.0.2.tar.gz", hash = "sha256:392a9e1e362cbc106a2457d37cabf9b36e5e12efd4ebff1654630e76597df804", size = 144659, upload-time = "2026-04-09T15:01:33.297Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/98/6af411189d9413534c3eb691182bff1f5c6d44ed2f93f2edfe52a1bbceb8/more_itertools-11.0.2-py3-none-any.whl", hash = "sha256:6e35b35f818b01f691643c6c611bc0902f2e92b46c18fffa77ae1e7c46e912e4", size = 71939, upload-time = "2026-04-09T15:01:32.21Z" },
]
[[package]]
@@ -1474,15 +1479,15 @@ wheels = [
[[package]]
name = "myogen"
-version = "0.8.3"
+version = "0.8.5"
source = { editable = "." }
dependencies = [
{ name = "beartype" },
{ name = "cython" },
- { name = "elephant" },
{ name = "impi-rt", marker = "sys_platform == 'win32'" },
{ name = "matplotlib" },
{ name = "mpi4py" },
+ { name = "neo" },
{ name = "neuron", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "numba" },
{ name = "numpy" },
@@ -1495,10 +1500,13 @@ dependencies = [
{ name = "seaborn" },
{ name = "setuptools" },
{ name = "tqdm" },
- { name = "viziphant" },
]
[package.optional-dependencies]
+elephant = [
+ { name = "elephant" },
+ { name = "viziphant" },
+]
nwb = [
{ name = "nwbinspector" },
{ name = "pynwb" },
@@ -1508,9 +1516,11 @@ nwb = [
dev = [
{ name = "pandas-stubs" },
{ name = "poethepoet" },
+ { name = "pytest" },
{ name = "scipy-stubs" },
]
docs = [
+ { name = "elephant" },
{ name = "enum-tools", extra = ["sphinx"] },
{ name = "linkify-it-py" },
{ name = "memory-profiler" },
@@ -1527,16 +1537,18 @@ docs = [
{ name = "sphinx-hoverxref" },
{ name = "sphinxcontrib-mermaid" },
{ name = "toml" },
+ { name = "viziphant" },
]
[package.metadata]
requires-dist = [
{ name = "beartype", specifier = ">=0.21.0" },
{ name = "cython", specifier = ">=3.1.3" },
- { name = "elephant", specifier = ">=1.1.1" },
+ { name = "elephant", marker = "extra == 'elephant'", specifier = ">=1.1.1" },
{ name = "impi-rt", marker = "sys_platform == 'win32'", specifier = ">=2021.15.0" },
{ name = "matplotlib", specifier = ">=3.10.1" },
{ name = "mpi4py", specifier = ">=4.0.3" },
+ { name = "neo", specifier = ">=0.14.0" },
{ name = "neuron", marker = "sys_platform == 'darwin' or sys_platform == 'linux'", specifier = "==8.2.7" },
{ name = "numba", specifier = ">=0.61.2" },
{ name = "numpy", specifier = ">=1.26,<2.0" },
@@ -1551,17 +1563,19 @@ requires-dist = [
{ name = "seaborn", specifier = ">=0.13.2" },
{ name = "setuptools", specifier = ">=80.9.0" },
{ name = "tqdm", specifier = ">=4.67.1" },
- { name = "viziphant", specifier = ">=0.4.0" },
+ { name = "viziphant", marker = "extra == 'elephant'", specifier = ">=0.4.0" },
]
-provides-extras = ["nwb"]
+provides-extras = ["elephant", "nwb"]
[package.metadata.requires-dev]
dev = [
{ name = "pandas-stubs", specifier = ">=2.3.0.250703" },
{ name = "poethepoet", specifier = ">=0.37.0" },
+ { name = "pytest", specifier = ">=8.0" },
{ name = "scipy-stubs", specifier = ">=1.16.1.0" },
]
docs = [
+ { name = "elephant", specifier = ">=1.1.1" },
{ name = "enum-tools", extras = ["sphinx"], specifier = ">=0.12.0" },
{ name = "linkify-it-py", specifier = ">=2.0.3" },
{ name = "memory-profiler", specifier = ">=0.61.0" },
@@ -1571,18 +1585,19 @@ docs = [
{ name = "pynwb", specifier = ">=2.8.0" },
{ name = "rinohtype", specifier = ">=0.5.5" },
{ name = "roman", specifier = ">=5.2" },
- { name = "sphinx", specifier = ">=8.1.3" },
+ { 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" },
+ { name = "viziphant", specifier = ">=0.4.0" },
]
[[package]]
name = "myst-parser"
-version = "4.0.1"
+version = "5.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "docutils" },
@@ -1592,9 +1607,9 @@ dependencies = [
{ name = "pyyaml" },
{ name = "sphinx" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/66/a5/9626ba4f73555b3735ad86247a8077d4603aa8628537687c839ab08bfe44/myst_parser-4.0.1.tar.gz", hash = "sha256:5cfea715e4f3574138aecbf7d54132296bfd72bb614d31168f48c477a830a7c4", size = 93985, upload-time = "2025-02-12T10:53:03.833Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/33/fa/7b45eef11b7971f0beb29d27b7bfe0d747d063aa29e170d9edd004733c8a/myst_parser-5.0.0.tar.gz", hash = "sha256:f6f231452c56e8baa662cc352c548158f6a16fcbd6e3800fc594978002b94f3a", size = 98535, upload-time = "2026-01-15T09:08:18.036Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/5f/df/76d0321c3797b54b60fef9ec3bd6f4cfd124b9e422182156a1dd418722cf/myst_parser-4.0.1-py3-none-any.whl", hash = "sha256:9134e88959ec3b5780aedf8a99680ea242869d012e8821db3126d427edc9c95d", size = 84579, upload-time = "2025-02-12T10:53:02.078Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/ac/686789b9145413f1a61878c407210e41bfdb097976864e0913078b24098c/myst_parser-5.0.0-py3-none-any.whl", hash = "sha256:ab31e516024918296e169139072b81592336f2fef55b8986aa31c9f04b5f7211", size = 84533, upload-time = "2026-01-15T09:08:16.788Z" },
]
[[package]]
@@ -1978,6 +1993,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" },
]
+[[package]]
+name = "pluggy"
+version = "1.6.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
+]
+
[[package]]
name = "poethepoet"
version = "0.39.0"
@@ -2077,35 +2101,35 @@ wheels = [
[[package]]
name = "psutil"
-version = "7.2.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/73/cb/09e5184fb5fc0358d110fc3ca7f6b1d033800734d34cac10f4136cfac10e/psutil-7.2.1.tar.gz", hash = "sha256:f7583aec590485b43ca601dd9cea0dcd65bd7bb21d30ef4ddbf4ea6b5ed1bdd3", size = 490253, upload-time = "2025-12-29T08:26:00.169Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/77/8e/f0c242053a368c2aa89584ecd1b054a18683f13d6e5a318fc9ec36582c94/psutil-7.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ba9f33bb525b14c3ea563b2fd521a84d2fa214ec59e3e6a2858f78d0844dd60d", size = 129624, upload-time = "2025-12-29T08:26:04.255Z" },
- { url = "https://files.pythonhosted.org/packages/26/97/a58a4968f8990617decee234258a2b4fc7cd9e35668387646c1963e69f26/psutil-7.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:81442dac7abfc2f4f4385ea9e12ddf5a796721c0f6133260687fec5c3780fa49", size = 130132, upload-time = "2025-12-29T08:26:06.228Z" },
- { url = "https://files.pythonhosted.org/packages/db/6d/ed44901e830739af5f72a85fa7ec5ff1edea7f81bfbf4875e409007149bd/psutil-7.2.1-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ea46c0d060491051d39f0d2cff4f98d5c72b288289f57a21556cc7d504db37fc", size = 180612, upload-time = "2025-12-29T08:26:08.276Z" },
- { url = "https://files.pythonhosted.org/packages/c7/65/b628f8459bca4efbfae50d4bf3feaab803de9a160b9d5f3bd9295a33f0c2/psutil-7.2.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35630d5af80d5d0d49cfc4d64c1c13838baf6717a13effb35869a5919b854cdf", size = 183201, upload-time = "2025-12-29T08:26:10.622Z" },
- { url = "https://files.pythonhosted.org/packages/fb/23/851cadc9764edcc18f0effe7d0bf69f727d4cf2442deb4a9f78d4e4f30f2/psutil-7.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:923f8653416604e356073e6e0bccbe7c09990acef442def2f5640dd0faa9689f", size = 139081, upload-time = "2025-12-29T08:26:12.483Z" },
- { url = "https://files.pythonhosted.org/packages/59/82/d63e8494ec5758029f31c6cb06d7d161175d8281e91d011a4a441c8a43b5/psutil-7.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cfbe6b40ca48019a51827f20d830887b3107a74a79b01ceb8cc8de4ccb17b672", size = 134767, upload-time = "2025-12-29T08:26:14.528Z" },
- { url = "https://files.pythonhosted.org/packages/05/c2/5fb764bd61e40e1fe756a44bd4c21827228394c17414ade348e28f83cd79/psutil-7.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:494c513ccc53225ae23eec7fe6e1482f1b8a44674241b54561f755a898650679", size = 129716, upload-time = "2025-12-29T08:26:16.017Z" },
- { url = "https://files.pythonhosted.org/packages/c9/d2/935039c20e06f615d9ca6ca0ab756cf8408a19d298ffaa08666bc18dc805/psutil-7.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3fce5f92c22b00cdefd1645aa58ab4877a01679e901555067b1bd77039aa589f", size = 130133, upload-time = "2025-12-29T08:26:18.009Z" },
- { url = "https://files.pythonhosted.org/packages/77/69/19f1eb0e01d24c2b3eacbc2f78d3b5add8a89bf0bb69465bc8d563cc33de/psutil-7.2.1-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93f3f7b0bb07711b49626e7940d6fe52aa9940ad86e8f7e74842e73189712129", size = 181518, upload-time = "2025-12-29T08:26:20.241Z" },
- { url = "https://files.pythonhosted.org/packages/e1/6d/7e18b1b4fa13ad370787626c95887b027656ad4829c156bb6569d02f3262/psutil-7.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d34d2ca888208eea2b5c68186841336a7f5e0b990edec929be909353a202768a", size = 184348, upload-time = "2025-12-29T08:26:22.215Z" },
- { url = "https://files.pythonhosted.org/packages/98/60/1672114392dd879586d60dd97896325df47d9a130ac7401318005aab28ec/psutil-7.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2ceae842a78d1603753561132d5ad1b2f8a7979cb0c283f5b52fb4e6e14b1a79", size = 140400, upload-time = "2025-12-29T08:26:23.993Z" },
- { url = "https://files.pythonhosted.org/packages/fb/7b/d0e9d4513c46e46897b46bcfc410d51fc65735837ea57a25170f298326e6/psutil-7.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:08a2f175e48a898c8eb8eace45ce01777f4785bc744c90aa2cc7f2fa5462a266", size = 135430, upload-time = "2025-12-29T08:26:25.999Z" },
- { url = "https://files.pythonhosted.org/packages/c5/cf/5180eb8c8bdf6a503c6919f1da28328bd1e6b3b1b5b9d5b01ae64f019616/psutil-7.2.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b2e953fcfaedcfbc952b44744f22d16575d3aa78eb4f51ae74165b4e96e55f42", size = 128137, upload-time = "2025-12-29T08:26:27.759Z" },
- { url = "https://files.pythonhosted.org/packages/c5/2c/78e4a789306a92ade5000da4f5de3255202c534acdadc3aac7b5458fadef/psutil-7.2.1-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:05cc68dbb8c174828624062e73078e7e35406f4ca2d0866c272c2410d8ef06d1", size = 128947, upload-time = "2025-12-29T08:26:29.548Z" },
- { url = "https://files.pythonhosted.org/packages/29/f8/40e01c350ad9a2b3cb4e6adbcc8a83b17ee50dd5792102b6142385937db5/psutil-7.2.1-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e38404ca2bb30ed7267a46c02f06ff842e92da3bb8c5bfdadbd35a5722314d8", size = 154694, upload-time = "2025-12-29T08:26:32.147Z" },
- { url = "https://files.pythonhosted.org/packages/06/e4/b751cdf839c011a9714a783f120e6a86b7494eb70044d7d81a25a5cd295f/psutil-7.2.1-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab2b98c9fc19f13f59628d94df5cc4cc4844bc572467d113a8b517d634e362c6", size = 156136, upload-time = "2025-12-29T08:26:34.079Z" },
- { url = "https://files.pythonhosted.org/packages/44/ad/bbf6595a8134ee1e94a4487af3f132cef7fce43aef4a93b49912a48c3af7/psutil-7.2.1-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f78baafb38436d5a128f837fab2d92c276dfb48af01a240b861ae02b2413ada8", size = 148108, upload-time = "2025-12-29T08:26:36.225Z" },
- { url = "https://files.pythonhosted.org/packages/1c/15/dd6fd869753ce82ff64dcbc18356093471a5a5adf4f77ed1f805d473d859/psutil-7.2.1-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:99a4cd17a5fdd1f3d014396502daa70b5ec21bf4ffe38393e152f8e449757d67", size = 147402, upload-time = "2025-12-29T08:26:39.21Z" },
- { url = "https://files.pythonhosted.org/packages/34/68/d9317542e3f2b180c4306e3f45d3c922d7e86d8ce39f941bb9e2e9d8599e/psutil-7.2.1-cp37-abi3-win_amd64.whl", hash = "sha256:b1b0671619343aa71c20ff9767eced0483e4fc9e1f489d50923738caf6a03c17", size = 136938, upload-time = "2025-12-29T08:26:41.036Z" },
- { url = "https://files.pythonhosted.org/packages/3e/73/2ce007f4198c80fcf2cb24c169884f833fe93fbc03d55d302627b094ee91/psutil-7.2.1-cp37-abi3-win_arm64.whl", hash = "sha256:0d67c1822c355aa6f7314d92018fb4268a76668a536f133599b91edd48759442", size = 133836, upload-time = "2025-12-29T08:26:43.086Z" },
+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/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" },
+ { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" },
+ { 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.16.1"
+version = "0.17.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "accessible-pygments" },
@@ -2116,9 +2140,9 @@ dependencies = [
{ name = "sphinx" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/00/20/bb50f9de3a6de69e6abd6b087b52fa2418a0418b19597601605f855ad044/pydata_sphinx_theme-0.16.1.tar.gz", hash = "sha256:a08b7f0b7f70387219dc659bff0893a7554d5eb39b59d3b8ef37b8401b7642d7", size = 2412693, upload-time = "2024-12-17T10:53:39.537Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/de/bb/4a97aaa840b26601d6d04deca1389c35025336428706a4a732051187fbd3/pydata_sphinx_theme-0.17.0.tar.gz", hash = "sha256:529c5631582cb3328cf4814fb9eb80611d1704c854406d282a75c9c86e3a1955", size = 4990605, upload-time = "2026-04-03T13:02:20.091Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/e2/0d/8ba33fa83a7dcde13eb3c1c2a0c1cc29950a048bfed6d9b0d8b6bd710b4c/pydata_sphinx_theme-0.16.1-py3-none-any.whl", hash = "sha256:225331e8ac4b32682c18fcac5a57a6f717c4e632cea5dd0e247b55155faeccde", size = 6723264, upload-time = "2024-12-17T10:53:35.645Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/b7/a2bae25aae3568fe9f17040b31f9c190b4c5d86856d8869d0a30364a2567/pydata_sphinx_theme-0.17.0-py3-none-any.whl", hash = "sha256:cec5c92f41f4a11541b6df8210c446b4aa9c3badb7fcf2db7893405b786d5c99", size = 6820685, upload-time = "2026-04-03T13:02:18.09Z" },
]
[[package]]
@@ -2156,6 +2180,22 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/8b/40/2614036cdd416452f5bf98ec037f38a1afb17f327cb8e6b652d4729e0af8/pyparsing-3.3.1-py3-none-any.whl", hash = "sha256:023b5e7e5520ad96642e2c6db4cb683d3970bd640cdf7115049a6e9c3682df82", size = 121793, upload-time = "2025-12-23T03:14:02.103Z" },
]
+[[package]]
+name = "pytest"
+version = "9.0.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "iniconfig" },
+ { name = "packaging" },
+ { name = "pluggy" },
+ { name = "pygments" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
+]
+
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
@@ -2456,14 +2496,14 @@ wheels = [
[[package]]
name = "ruamel-yaml"
-version = "0.18.17"
+version = "0.18.16"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "ruamel-yaml-clib", marker = "python_full_version < '3.15' and platform_python_implementation == 'CPython'" },
+ { name = "ruamel-yaml-clib", marker = "python_full_version < '3.14' and platform_python_implementation == 'CPython'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/3a/2b/7a1f1ebcd6b3f14febdc003e658778d81e76b40df2267904ee6b13f0c5c6/ruamel_yaml-0.18.17.tar.gz", hash = "sha256:9091cd6e2d93a3a4b157ddb8fabf348c3de7f1fb1381346d985b6b247dcd8d3c", size = 149602, upload-time = "2025-12-17T20:02:55.757Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/9f/c7/ee630b29e04a672ecfc9b63227c87fd7a37eb67c1bf30fe95376437f897c/ruamel.yaml-0.18.16.tar.gz", hash = "sha256:a6e587512f3c998b2225d68aa1f35111c29fad14aed561a26e73fab729ec5e5a", size = 147269, upload-time = "2025-10-22T17:54:02.346Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/af/fe/b6045c782f1fd1ae317d2a6ca1884857ce5c20f59befe6ab25a8603c43a7/ruamel_yaml-0.18.17-py3-none-any.whl", hash = "sha256:9c8ba9eb3e793efdf924b60d521820869d5bf0cb9c6f1b82d82de8295e290b9d", size = 121594, upload-time = "2025-12-17T20:02:07.657Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/73/bb1bc2529f852e7bf64a2dec885e89ff9f5cc7bbf6c9340eed30ff2c69c5/ruamel.yaml-0.18.16-py3-none-any.whl", hash = "sha256:048f26d64245bae57a4f9ef6feb5b552a386830ef7a826f235ffb804c59efbba", size = 119858, upload-time = "2025-10-22T17:53:59.012Z" },
]
[[package]]
@@ -2686,11 +2726,11 @@ wheels = [
[[package]]
name = "soupsieve"
-version = "2.8.1"
+version = "2.8.3"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/89/23/adf3796d740536d63a6fbda113d07e60c734b6ed5d3058d1e47fc0495e47/soupsieve-2.8.1.tar.gz", hash = "sha256:4cf733bc50fa805f5df4b8ef4740fc0e0fa6218cf3006269afd3f9d6d80fd350", size = 117856, upload-time = "2025-12-18T13:50:34.655Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/48/f3/b67d6ea49ca9154453b6d70b34ea22f3996b9fa55da105a79d8732227adc/soupsieve-2.8.1-py3-none-any.whl", hash = "sha256:a11fe2a6f3d76ab3cf2de04eb339c1be5b506a8a47f2ceb6d139803177f85434", size = 36710, upload-time = "2025-12-18T13:50:33.267Z" },
+ { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" },
]
[[package]]
@@ -2735,14 +2775,14 @@ wheels = [
[[package]]
name = "sphinx-design"
-version = "0.6.1"
+version = "0.7.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "sphinx" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/2b/69/b34e0cb5336f09c6866d53b4a19d76c227cdec1bbc7ac4de63ca7d58c9c7/sphinx_design-0.6.1.tar.gz", hash = "sha256:b44eea3719386d04d765c1a8257caca2b3e6f8421d7b3a5e742c0fd45f84e632", size = 2193689, upload-time = "2024-08-02T13:48:44.277Z" }
+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/c6/43/65c0acbd8cc6f50195a3a1fc195c404988b15c67090e73c7a41a9f57d6bd/sphinx_design-0.6.1-py3-none-any.whl", hash = "sha256:b11f37db1a802a183d61b159d9a202314d4d2fe29c163437001324fe2f19549c", size = 2215338, upload-time = "2024-08-02T13:48:42.106Z" },
+ { 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]]
@@ -2820,7 +2860,7 @@ wheels = [
[[package]]
name = "sphinx-toolbox"
-version = "4.1.0"
+version = "4.1.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "apeye" },
@@ -2842,9 +2882,9 @@ dependencies = [
{ name = "tabulate" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/33/57/d9f2cfa638864eac5565c263e994d117d577898a9eca91800f806a225b71/sphinx_toolbox-4.1.0.tar.gz", hash = "sha256:5da890f4bb0cacea4f1cf6cef182c5be480340d0ead43c905f51f7e5aacfc19c", size = 113632, upload-time = "2025-12-05T23:23:53.23Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/ba/16/769454374719569f6165149a16d06b8da27cda8330fe5f932c2c65a77a04/sphinx_toolbox-4.1.2.tar.gz", hash = "sha256:c30a4f86c4c29e97adb0eb9337d35f5093cb96a44f49caffcf7d5bc58a88b781", size = 114911, upload-time = "2026-01-14T17:33:02.156Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/fd/6e/e6a9f56972c971eb1556b604c82bb957c2286c68d50f39f64a7526907216/sphinx_toolbox-4.1.0-py3-none-any.whl", hash = "sha256:9024a7482b92ecf4572f83940c87ae26c2eca3ca49ff3df5f59806e88da958f6", size = 196115, upload-time = "2025-12-05T23:23:51.713Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/38/6763a5a981770a21a18b66d3593a8bbb801884d9828c126866efdd01d41a/sphinx_toolbox-4.1.2-py3-none-any.whl", hash = "sha256:0438f8342ba1c6c0d6e47207f4eac167adba61742e8c2b1dc9624ff955b7bc89", size = 196812, upload-time = "2026-01-14T17:33:00.51Z" },
]
[[package]]
@@ -2897,15 +2937,16 @@ wheels = [
[[package]]
name = "sphinxcontrib-mermaid"
-version = "1.2.3"
+version = "2.0.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
+ { name = "jinja2" },
{ name = "pyyaml" },
{ name = "sphinx" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/f5/49/c6ddfe709a4ab76ac6e5a00e696f73626b2c189dc1e1965a361ec102e6cc/sphinxcontrib_mermaid-1.2.3.tar.gz", hash = "sha256:358699d0ec924ef679b41873d9edd97d0773446daf9760c75e18dc0adfd91371", size = 18885, upload-time = "2025-11-26T04:18:32.43Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/2b/ae/999891de292919b66ea34f2c22fc22c9be90ab3536fbc0fca95716277351/sphinxcontrib_mermaid-2.0.1.tar.gz", hash = "sha256:a21a385a059a6cafd192aa3a586b14bf5c42721e229db67b459dc825d7f0a497", size = 19839, upload-time = "2026-03-05T14:10:41.901Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/d1/39/8b54299ffa00e597d3b0b4d042241a0a0b22cb429ad007ccfb9c1745b4d1/sphinxcontrib_mermaid-1.2.3-py3-none-any.whl", hash = "sha256:5be782b27026bef97bfb15ccb2f7868b674a1afc0982b54cb149702cfc25aa02", size = 13413, upload-time = "2025-11-26T04:18:31.269Z" },
+ { url = "https://files.pythonhosted.org/packages/03/46/25d64bcd7821c8d6f1080e1c43d5fcdfc442a18f759a230b5ccdc891093e/sphinxcontrib_mermaid-2.0.1-py3-none-any.whl", hash = "sha256:9dca7fbe827bad5e7e2b97c4047682cfd26e3e07398cfdc96c7a8842ae7f06e7", size = 14064, upload-time = "2026-03-05T14:10:40.533Z" },
]
[[package]]
@@ -2972,11 +3013,11 @@ wheels = [
[[package]]
name = "tabulate"
-version = "0.9.0"
+version = "0.10.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090, upload-time = "2022-10-06T17:21:48.54Z" }
+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/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252, upload-time = "2022-10-06T17:21:44.262Z" },
+ { 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]]