diff --git a/.vscode/settings.json b/.vscode/settings.json
index e80b296f..10e279bc 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -15,7 +15,6 @@
"C:/nrn/lib/python"
],
"python.analysis.autoImportCompletions": true,
- "editor.fontFamily": "Fira Code",
"editor.fontSize": 16,
"editor.fontLigatures": true,
"svg.preview.background": "dark-transparent",
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4008903e..4086b284 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
+## [0.3.0] - 2025-08-08
+
+### Added
+- **Intramuscular EMG Framework**: Enhanced intramuscular EMG simulation capabilities with improved framework
+- Optional CUDA/CuPy support for accelerated computations in intramuscular EMG
+- New type definitions for EMG simulation components:
+ - `CORTICAL_INPUT__MATRIX` type for cortical input data
+ - `INTRAMUSCULAR_MUAP_SHAPE__TENSOR` for intramuscular electrode arrays
+
+### Changed
+- **Major Refactor**: Improved intramuscular EMG simulation framework with better type annotations and imports
+- Enhanced `IntramuscularEMG` class with better parameter handling
+- Optimized bioelectric field calculations for needle electrodes
+- Updated surface EMG components for consistency with intramuscular framework
+- Enhanced muscle model integration with electrode positioning
+- Improved motor unit simulation with better parameter handling
+- Enhanced recruitment thresholds plotting utilities
+- Updated project dependencies and compatibility with latest package versions
+- Enhanced Sphinx documentation configuration with better type aliases and extensions
+- Renamed `MUAP_SHAPE__TENSOR` to `SURFACE_MUAP_SHAPE__TENSOR` for better clarity
+
+### Fixed
+- Ensured Mermaid diagrams have consistent white background in documentation
+- Improved readability and visual consistency in documentation
+- Enhanced development experience with updated VSCode settings
+
## [0.2.0] - 2025-07-26
### Added
diff --git a/docs/source/_static/custom.css b/docs/source/_static/custom.css
index 9ddbe844..68e38182 100644
--- a/docs/source/_static/custom.css
+++ b/docs/source/_static/custom.css
@@ -27,4 +27,8 @@ html[data-theme="dark"] .bd-content img:not(.only-dark):not(.dark-light) {
.sphx-glr-footer.sphx-glr-footer-example.docutils.container {
display: none;
-}
\ No newline at end of file
+}
+
+.mermaid {
+ background-color: #ffffff;
+}
diff --git a/docs/source/api/simulator_api.rst b/docs/source/api/simulator_api.rst
index a5a4ddd6..97aadeee 100644
--- a/docs/source/api/simulator_api.rst
+++ b/docs/source/api/simulator_api.rst
@@ -33,6 +33,16 @@ Muscle Model
Muscle
+Force Model
+^^^^^^^^^^^
+
+.. autosummary::
+ :toctree: ../generated/
+ :template: autosummary/class.rst
+ :recursive:
+
+ ForceModel
+
EMG Generation
^^^^^^^^^^^^^^
@@ -41,4 +51,17 @@ EMG Generation
:template: autosummary/class.rst
:recursive:
- SurfaceEMG
\ No newline at end of file
+ SurfaceEMG
+ IntramuscularEMG
+
+Electrodes
+^^^^^^^^^^^^^^
+
+
+.. autosummary::
+ :toctree: ../generated/
+ :template: autosummary/class.rst
+ :recursive:
+
+ SurfaceElectrodeArray
+ IntramuscularElectrodeArray
diff --git a/docs/source/api/types_api.rst b/docs/source/api/types_api.rst
index 60220d38..a7136ab0 100644
--- a/docs/source/api/types_api.rst
+++ b/docs/source/api/types_api.rst
@@ -11,6 +11,9 @@ This module contains type definitions for structured data and type safety.
:recursive:
INPUT_CURRENT__MATRIX
+ CORTICAL_INPUT__MATRIX
SPIKE_TRAIN__MATRIX
- MUAP_SHAPE__TENSOR
- SURFACE_EMG__TENSOR
\ No newline at end of file
+ SURFACE_MUAP_SHAPE__TENSOR
+ INTRAMUSCULAR_MUAP_SHAPE__TENSOR
+ SURFACE_EMG__TENSOR
+ INTRAMUSCULAR_EMG__TENSOR
\ No newline at end of file
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 4a164d41..82556428 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -38,8 +38,11 @@
"sphinx_gallery.gen_gallery",
"sphinx.ext.doctest",
"myst_parser",
+ "sphinxcontrib.mermaid",
]
+mermaid_version = "11.9.0"
+
napoleon_use_admonition_for_examples = False
napoleon_use_admonition_for_references = False
napoleon_numpy_docstring = True
@@ -70,6 +73,7 @@
"list[float] | None": ":class:`list`\\[:class:`float`] | :class:`None`",
"list[str] | None": ":class:`list`\\[:class:`str`] | :class:`None`",
"tuple[int, int]": ":class:`tuple`\\[:class:`int`, :class:`int`]",
+ "tuple[float, float]": ":class:`tuple`\\[:class:`float`, :class:`float`]",
"list[int]": ":class:`list`\\[:class:`int`]",
"list[float]": ":class:`list`\\[:class:`float`]",
"list[str]": ":class:`list`\\[:class:`str`]",
@@ -78,6 +82,10 @@
"SPIKE_TRAIN__MATRIX": ":data:`~myogen.utils.types.SPIKE_TRAIN__MATRIX`",
"MUAP_SHAPE__TENSOR": ":data:`~myogen.utils.types.MUAP_SHAPE__TENSOR`",
"SURFACE_EMG__TENSOR": ":data:`~myogen.utils.types.SURFACE_EMG__TENSOR`",
+ "INTRAMUSCULAR_EMG__TENSOR": ":data:`~myogen.utils.types.INTRAMUSCULAR_EMG__TENSOR`",
+ "CORTICAL_INPUT__MATRIX": ":data:`~myogen.utils.types.CORTICAL_INPUT__MATRIX`",
+ "SURFACE_MUAP_SHAPE__TENSOR": ":data:`~myogen.utils.types.SURFACE_MUAP_SHAPE__TENSOR`",
+ "INTRAMUSCULAR_MUAP_SHAPE__TENSOR": ":data:`~myogen.utils.types.INTRAMUSCULAR_MUAP_SHAPE__TENSOR`",
"INPUT_CURRENT__MATRIX | None": ":class:`~myogen.utils.types.INPUT_CURRENT__MATRIX` | :class:`None`",
# Beartype and Annotated type patterns - map to clean aliases
"Annotated[ndarray[tuple[int, ...], dtype[bool]], beartype.vale.Is[lambda x: x.ndim == 3]]": ":data:`~myogen.utils.types.SPIKE_TRAIN__MATRIX`",
diff --git a/examples/08_simulate_intramuscular_emg.py b/examples/08_simulate_intramuscular_emg.py
new file mode 100644
index 00000000..e714f31d
--- /dev/null
+++ b/examples/08_simulate_intramuscular_emg.py
@@ -0,0 +1,277 @@
+"""
+Intramuscular EMG Signals
+========================
+
+This example demonstrates how to simulate **intramuscular EMG signals** using
+needle electrodes. It shows the complete pipeline from muscle model creation
+to EMG signal generation with realistic noise and motor unit detectability.
+
+.. note::
+ **Intramuscular EMG** (iEMG) is recorded using needle electrodes inserted
+ directly into the muscle tissue. This provides high spatial resolution
+ and allows for the detection of individual motor unit action potentials
+ (MUAPs). Unlike surface EMG, intramuscular recordings can capture the
+ activity of deeper motor units and provide better selectivity.
+
+Key Features:
+ - **High spatial resolution**: Needle electrodes can detect individual MUAPs
+ - **Deep muscle access**: Can record from muscles not accessible by surface electrodes
+ - **Motor unit discrimination**: Individual motor units can be identified and tracked
+ - **Realistic noise modeling**: Includes physiological noise and recording artifacts
+"""
+# sphinx_gallery_thumbnail_number = -1
+
+##############################################################################
+# Import Libraries
+# ----------------
+
+import joblib
+import matplotlib
+import matplotlib.pyplot as plt
+import numpy as np
+import seaborn as sns
+
+from myogen import simulator
+from myogen.utils.currents import create_sinusoidal_current
+from myogen.utils.plotting.spikes import plot_spike_trains
+
+##############################################################################
+# Define Parameters
+# -----------------
+#
+
+# Simulation parameters
+N_motor_units = 25 # Smaller number for faster computation
+recruitment_range = 50.0
+
+# Electrode parameters
+inter_electrode_distance = 0.5 # mm
+electrode_position = (0.0, 0.0, 0.0) # mm (start of muscle)
+
+##############################################################################
+# Generate Motor Unit Recruitment Thresholds
+# -------------------------------------------
+#
+# First, we generate the **recruitment thresholds** for the motor unit pool.
+
+thresholds, _ = simulator.generate_mu_recruitment_thresholds(
+ N=N_motor_units,
+ recruitment_range=recruitment_range,
+ deluca__slope=5,
+ mode="combined"
+)
+
+plt.figure()
+plt.plot(thresholds, "o")
+plt.tight_layout()
+plt.show()
+
+##############################################################################
+# Load Muscle Model
+# -------------------
+#
+# Load the **muscle model** with the generated recruitment thresholds.
+
+muscle = joblib.load("results/muscle_model.pkl")
+muscle.length__mm = 30
+
+##############################################################################
+# Create Intramuscular Electrode Array
+# ------------------------------------
+#
+# Set up a **differential needle electrode** for intramuscular recordings.
+
+electrode = simulator.IntramuscularElectrodeArray(
+ num_electrodes=4,
+ inter_electrode_distance__mm=inter_electrode_distance,
+ differentiation_mode="consecutive",
+ position__mm=electrode_position,
+ orientation__rad=(-np.pi / 2, 0, -np.pi / 2), # perpendicular to muscle
+ trajectory_distance__mm=0.125, # mm
+ trajectory_steps=1, # number of steps
+)
+
+##############################################################################
+# Initialize Intramuscular EMG Simulator
+# --------------------------------------
+#
+# Create the **intramuscular EMG simulator** with the muscle model and electrode.
+
+print("Initializing iEMG simulator...")
+iemg_sim = simulator.IntramuscularEMG(
+ muscle_model=muscle,
+ electrode_array=electrode,
+ MUs_to_simulate=list(range(0, N_motor_units, 3)),
+)
+
+##############################################################################
+# Calculate Motor Unit Action Potentials
+# --------------------------------------
+#
+# Compute the **MUAPs** for each motor unit at the electrode positions.
+
+print("Computing motor unit action potentials...")
+iemg_sim.simulate_muaps()
+print(f" - Generated MUAPs for {N_motor_units} motor units")
+print(f" - Electrode array with {electrode.num_electrodes} electrodes")
+
+
+##############################################################################
+# Create Motor Neuron Pool
+# ------------------------
+#
+# Set up the **motor neuron pool** for spike train generation.
+
+mn_pool = simulator.MotorNeuronPool(recruitment_thresholds=thresholds)
+mvc_current = mn_pool.mvc_current_threshold
+
+
+##############################################################################
+# Generate Input Currents
+# -----------------------
+#
+# Create a **sinusoidal current profile** for the contraction simulation.
+
+n_pools = 2 # Number of distinct motor neuron pools
+
+timestep = 0.01 # Simulation timestep in ms (high resolution)
+simulation_time = 500 # Total simulation duration in ms
+
+# Calculate number of time points
+t_points = int(simulation_time / timestep)
+
+# Create the input current matrix
+sin_amplitudes = [
+ mvc_current * 1,
+ mvc_current * 1,
+] # Same amplitude for both pools
+sin_frequencies = [1.0, 1.0] # Same frequency for both pools (1 Hz)
+sin_offsets = [
+ 0.0,
+ 0.0,
+]
+sin_phases = [0.0, np.pi] # 0 and 90 degrees (in radians)
+
+sinusoidal_currents = create_sinusoidal_current(
+ n_pools=n_pools,
+ t_points=t_points,
+ timestep__ms=timestep,
+ amplitudes__muV=sin_amplitudes,
+ frequencies__Hz=sin_frequencies,
+ offsets__muV=sin_offsets,
+ phases__rad=sin_phases,
+)
+
+##############################################################################
+# Generate Spike Trains
+# ---------------------
+#
+# Simulate the **neural spike trains** using the motor neuron pool.
+
+print("Generating spike trains...")
+spike_trains, active_indices, _ = mn_pool.generate_spike_trains(
+ input_current__matrix=sinusoidal_currents, timestep__ms=timestep
+)
+print(f" - Generated spike trains for {len(active_indices)} active motor units")
+print(f" - Total simulation time: {simulation_time} ms")
+
+##############################################################################
+# Visualize Spike Trains
+# ----------------------
+#
+# Display the **spike trains** generated by the motor neuron pool in response
+# to the sinusoidal input current. This shows the neural firing patterns
+# that will be convolved with the MUAPs to generate the final EMG signal.
+
+spike_fig, spike_ax = plt.subplots(figsize=(10, 6))
+plot_spike_trains(
+ spike_trains__matrix=spike_trains,
+ timestep__ms=timestep,
+ axs=[spike_ax],
+ pool_current__matrix=sinusoidal_currents,
+ pool_to_plot=[0] # Show only the first pool
+)
+plt.tight_layout()
+plt.show()
+
+##############################################################################
+# Simulate Intramuscular EMG
+# -------------------------
+#
+# Generate the final **intramuscular EMG signals** by convolving spike trains with MUAPs.
+
+print("Simulating intramuscular EMG signals...")
+emg_signals = iemg_sim.simulate_intramuscular_emg(
+ motor_neuron_pool=mn_pool,
+)
+
+print("Intramuscular EMG simulation completed!")
+print(f" - EMG signal shape: {emg_signals.shape}")
+print(f" - Signal RMS (before noise): {np.sqrt(np.mean(emg_signals**2)):.3f} nA")
+
+print("Adding realistic noise (SNR = 20 dB)...")
+noisy_emg_signals = iemg_sim.add_noise(snr_db=20)
+print(f" - Signal RMS (after noise): {np.sqrt(np.mean(noisy_emg_signals**2)):.3f} nA")
+
+##############################################################################
+# Visualize Intramuscular EMG Results
+# ----------------------------------
+#
+# Create an **xkcd-style plot** comparing the **intramuscular EMG** signal
+# with the input current, similar to the surface EMG example.
+#
+# .. note::
+# The intramuscular EMG provides **high spatial resolution** and can detect
+# individual motor unit action potentials (MUAPs) with excellent signal quality.
+
+# Clear matplotlib cache and set up xkcd style
+matplotlib.get_cachedir()
+with plt.xkcd():
+ plt.rcParams.update({"font.size": 24})
+
+ # Create single plot with normalized signals
+ fig, ax = plt.subplots(figsize=(12, 6))
+
+ # Get the signals - use first electrode's differential recording
+ iemg_signal = noisy_emg_signals[0, 0, :] # First electrode differential pair
+ current_signal = sinusoidal_currents[0] # First current pool
+
+ # Create time axes
+ emg_time = np.arange(len(iemg_signal)) / iemg_sim.sampling_frequency__Hz # Convert to seconds
+ current_time = mn_pool.times / 1000
+
+ # Normalize iEMG signal by dividing by maximum absolute value
+ iemg_normalized = iemg_signal / np.max(np.abs(iemg_signal))
+
+ # Normalize current between 0 and 1
+ current_normalized = (current_signal - np.min(current_signal)) / (
+ np.max(current_signal) - np.min(current_signal)
+ )
+
+# Plot both normalized signals
+ax.plot(
+ emg_time,
+ iemg_normalized,
+ linewidth=2,
+ label="Intramuscular EMG",
+)
+
+with plt.xkcd():
+ ax.plot(
+ current_time,
+ current_normalized,
+ linewidth=2,
+ label="Input Current",
+ )
+
+ ax.set_xlabel("Time (s)")
+ ax.set_ylabel("Normalized Amplitude")
+ ax.grid(True, alpha=0.3)
+ ax.legend()
+
+ sns.despine(trim=True, left=False, bottom=False, right=True, top=True, offset=5)
+
+ plt.title("Normalized Intramuscular EMG and Input Current")
+
+plt.tight_layout()
+plt.show()
\ No newline at end of file
diff --git a/examples/README.rst b/examples/README.rst
index 2133186f..bac5db1b 100644
--- a/examples/README.rst
+++ b/examples/README.rst
@@ -3,3 +3,125 @@
==================
Examples
==================
+
+This collection of examples demonstrates the complete MyoGen simulation pipeline, from basic motor unit recruitment to advanced EMG signal generation.
+
+Workflow
+-----------------------------
+
+.. mermaid::
+ :caption: Click any box to navigate to that example
+
+ %%{init: {
+ "theme": "base",
+ "themeVariables": {
+ "fontSize": "1.1em",
+ "fontFamily": "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
+ "primaryColor": "#ffffff",
+ "primaryTextColor": "#1f2937",
+ "primaryBorderColor": "#e5e7eb",
+ "lineColor": "#6b7280",
+ "background": "#ffffff",
+ "mainBkg": "#ffffff",
+ "secondaryColor": "#f9fafb",
+ "tertiaryColor": "#f3f4f6",
+ "clusterBkg": "#f8fafc",
+ "clusterBorder": "#cbd5e1",
+ "edgeLabelBackground": "#ffffff",
+ "cScale0": "#1f2937",
+ "cScale1": "#1f2937",
+ "cScale2": "#1f2937",
+ "clusterTextSize": "1.5em"
+ },
+ "flowchart": {
+ "curve": "linear",
+ "nodeSpacing": 50,
+ "rankSpacing": 80
+ }
+ }}%%
+
+ flowchart TD
+ S((Start))
+ A["Recruitment
Thresholds"]
+
+ B["Injected Current"]
+ C["Cortical Input"]
+
+ D["Muscle Model"]
+
+ E["Surface MUAPs"]
+ F["Surface EMG"]
+ G["Intramuscular EMG"]
+ J["Intramuscular MUAPs"]
+
+ %% Column 4: Independent Utilities
+ H["Current Generation"]
+ I["Force Model"]
+
+ subgraph neural_drive ["⚡ Neural Drive"]
+ B
+ C
+ end
+
+ subgraph utilities ["🔧 Utilities"]
+ H
+ end
+
+ subgraph physiology ["💪 Anatomy Model"]
+ S --> A --> D & I
+ end
+
+ subgraph emg ["📊 EMG Generation"]
+ direction TB
+ E --> F
+ J --> G
+ end
+
+
+ H --> neural_drive
+ D --> emg
+
+ neural_drive --> emg
+
+ %% Modern Light Mode Styling
+ classDef start fill:#f0f9ff,stroke:#0369a1,stroke-width:3px,color:#0c4a6e
+ classDef foundation fill:#dbeafe,stroke:#1d4ed8,stroke-width:2px,color:#1e3a8a
+ classDef neural fill:#ede9fe,stroke:#7c3aed,stroke-width:2px,color:#5b21b6
+ classDef physiology fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#92400e
+ classDef emg fill:#d1fae5,stroke:#059669,stroke-width:2px,color:#064e3b
+ classDef utility fill:#fee2e2,stroke:#dc2626,stroke-width:2px,color:#991b1b
+ classDef default fill:#f3f4f6,stroke:#6b7280,stroke-width:2px,color:#374151
+
+ class S start
+ class A foundation
+ class B,C neural
+ class D,I physiology
+ class E,F,G,J emg
+ class H utility
+
+ %% Subgraph styling
+ style neural_drive fill:#f3f0ff,stroke:#8b5cf6,stroke-width:2px,stroke-dasharray: 5 5
+ style utilities fill:#fef2f2,stroke:#ef4444,stroke-width:2px,stroke-dasharray: 5 5
+ style physiology fill:#fffbeb,stroke:#f59e0b,stroke-width:2px,stroke-dasharray: 5 5
+ style emg fill:#ecfdf5,stroke:#10b981,stroke-width:2px,stroke-dasharray: 5 5
+
+ %% Clickable links
+ click A "00_simulate_recruitment_thresholds.html"
+ click B "01_simulate_spike_trains.html"
+ click C "07_simulate_cortical_input.html"
+ click D "02_simulate_muscle.html"
+ click E "03_simulate_surface_muaps.html"
+ click F "04_simulate_surface_emg.html"
+ click J "08_simulate_intramuscular_emg.html"
+ click G "08_simulate_intramuscular_emg.html"
+ click H "05_simulate_currents.html"
+ click I "06_simulate_force.html"
+
+
+Recommended Entry Path
+---------------------------
+
+1. :ref:`sphx_glr_auto_examples_00_simulate_recruitment_thresholds.py` - Create recruitment thresholds
+2. :ref:`sphx_glr_auto_examples_01_simulate_spike_trains.py` - Neural drive
+3. :ref:`sphx_glr_auto_examples_02_simulate_muscle.py` - Muscle structure
+4. :ref:`sphx_glr_auto_examples_04_simulate_surface_emg.py` - Surface EMG
diff --git a/myogen/simulator/__init__.py b/myogen/simulator/__init__.py
index a9c9aea1..bc620159 100644
--- a/myogen/simulator/__init__.py
+++ b/myogen/simulator/__init__.py
@@ -13,9 +13,9 @@
)
from myogen.simulator.core.emg import (
SurfaceEMG,
- # IntramuscularEMG,
+ IntramuscularEMG,
SurfaceElectrodeArray,
- # IntramuscularElectrodeArray,
+ IntramuscularElectrodeArray,
)
from myogen.simulator.core.muscle import Muscle
from myogen.simulator.core.spike_train import MotorNeuronPool
@@ -26,8 +26,8 @@
"Muscle",
"MotorNeuronPool",
"SurfaceEMG",
- # "IntramuscularEMG",
+ "IntramuscularEMG",
"SurfaceElectrodeArray",
- # "IntramuscularElectrodeArray",
+ "IntramuscularElectrodeArray",
"ForceModel",
]
diff --git a/myogen/simulator/core/emg/__init__.py b/myogen/simulator/core/emg/__init__.py
index 79837232..8d39df94 100644
--- a/myogen/simulator/core/emg/__init__.py
+++ b/myogen/simulator/core/emg/__init__.py
@@ -5,8 +5,7 @@
"""
from myogen.simulator.core.emg.surface.surface_emg import SurfaceEMG
-
-# from myogen.simulator.core.emg.intramuscular.intramuscular_emg import IntramuscularEMG
+from myogen.simulator.core.emg.intramuscular.intramuscular_emg import IntramuscularEMG
from myogen.simulator.core.emg.electrodes import (
SurfaceElectrodeArray,
IntramuscularElectrodeArray,
@@ -14,7 +13,7 @@
__all__ = [
"SurfaceEMG",
- # "IntramuscularEMG",
+ "IntramuscularEMG",
"SurfaceElectrodeArray",
- # "IntramuscularElectrodeArray",
+ "IntramuscularElectrodeArray",
]
diff --git a/myogen/simulator/core/emg/electrodes.py b/myogen/simulator/core/emg/electrodes.py
index e6132f5a..ee651105 100644
--- a/myogen/simulator/core/emg/electrodes.py
+++ b/myogen/simulator/core/emg/electrodes.py
@@ -2,10 +2,15 @@
Electrode configuration framework for EMG simulation.
"""
-from typing import Tuple, Literal, Optional, Dict, Any
+from typing import Literal
+
import numpy as np
+from scipy.spatial.transform import Rotation as R
+
+from myogen.utils.types import beartowertype
+@beartowertype
class SurfaceElectrodeArray:
"""
Surface electrode array for EMG recording.
@@ -23,7 +28,7 @@ class SurfaceElectrodeArray:
Inter-electrode distances in mm.
electrode_radius__mm : float, optional
Radius of the electrodes in mm
- center_point__mm_deg : Tuple[float, float]
+ center_point__mm_deg : tuple[float, float]
Position along z in mm and rotation around the muscle theta in degrees.
bending_radius__mm : float, optional
Bending radius around which the electrode grid is bent. Usually this is equal to the radius of the muscle.
@@ -39,7 +44,7 @@ def __init__(
num_cols: int,
inter_electrode_distances__mm: float,
electrode_radius__mm: float,
- center_point__mm_deg: Tuple[float, float] = (0.0, 0.0),
+ center_point__mm_deg: tuple[float, float] = (0.0, 0.0),
bending_radius__mm: float = 0.0,
rotation_angle__deg: float = 0.0,
differentiation_mode: Literal[
@@ -224,6 +229,7 @@ def get_H_sf(
return H_sf
+@beartowertype
class IntramuscularElectrodeArray:
"""
Intramuscular electrode array for EMG recording.
@@ -237,13 +243,11 @@ class IntramuscularElectrodeArray:
Number of electrodes in the array
inter_electrode_distance__mm : float, default=0.5
Inter-electrode distance in mm
- position__mm : Tuple[float, float, float], default=(0.0, 0.0, 0.0)
+ position__mm : tuple[float, float, float], default=(0.0, 0.0, 0.0)
Position of the electrode array center in mm (x, y, z coordinates)
- orientation__rad : Tuple[float, float, float], default=(0.0, 0.0, 0.0)
+ orientation__rad : tuple[float, float, float], default=(0.0, 0.0, 0.0)
Orientation of the electrode array in radians (roll, pitch, yaw)
- arrangement : Literal["linear", "grid"], default="linear"
- Arrangement type of electrodes
- differentiation_mode : Literal["monopolar", "consecutive", "reference"], default="consecutive"
+ differentiation_mode : Literal["consecutive", "reference"], default="consecutive"
Differentiation mode for recording
trajectory_distance__mm : float, default=0.0
Distance for trajectory movement in mm
@@ -255,12 +259,9 @@ def __init__(
self,
num_electrodes: int,
inter_electrode_distance__mm: float = 0.5,
- position__mm: Tuple[float, float, float] = (0.0, 0.0, 0.0),
- orientation__rad: Tuple[float, float, float] = (0.0, 0.0, 0.0),
- arrangement: Literal["linear", "grid"] = "linear",
- differentiation_mode: Literal[
- "monopolar", "consecutive", "reference"
- ] = "consecutive",
+ position__mm: tuple[float, float, float] = (0.0, 0.0, 0.0),
+ orientation__rad: tuple[float, float, float] = (0.0, 0.0, 0.0),
+ differentiation_mode: Literal["consecutive", "reference"] = "consecutive",
trajectory_distance__mm: float = 0.0,
trajectory_steps: int = 1,
):
@@ -268,7 +269,6 @@ def __init__(
self.inter_electrode_distance__mm = inter_electrode_distance__mm
self.position__mm = position__mm
self.orientation__rad = orientation__rad
- self.arrangement = arrangement
self.differentiation_mode = differentiation_mode
self.trajectory_distance__mm = trajectory_distance__mm
self.trajectory_steps = trajectory_steps
@@ -276,516 +276,336 @@ def __init__(
self.num_points = num_electrodes # Alias for compatibility
self.n_nodes = trajectory_steps
- # Set electrode type description
- if self.num_electrodes == 1:
- self.type = "single point"
- self.type_short = "1p"
- elif self.num_electrodes == 2:
- self.type = "2-point differential"
- self.type_short = "2p_diff"
- else:
- self.type = f"intramuscular_{self.num_electrodes}p_array"
- self.type_short = f"{self.num_electrodes}p_array"
-
- # Set up channel configuration based on differentiation mode
- if differentiation_mode == "monopolar":
- self.num_channels = self.num_electrodes
- elif differentiation_mode == "consecutive":
- # Consecutive differential: adjacent electrode pairs
- self.num_channels = max(1, self.num_electrodes - 1)
- elif differentiation_mode == "reference":
- # Reference differential: all electrodes vs reference
- self.num_channels = max(1, self.num_electrodes - 1)
- else:
- self.num_channels = self.num_electrodes
+ self._pts_origin = np.concatenate(
+ [
+ np.zeros((self.num_electrodes, 2)),
+ np.arange(self.num_electrodes)[..., None]
+ * self.inter_electrode_distance__mm,
+ ],
+ axis=-1,
+ )
- # Create electrode positions and differential matrix
- self._create_electrode_positions()
- self._create_differential_matrix()
-
- def _create_electrode_positions(self) -> None:
- """Create electrode positions in 3D space."""
- if self.arrangement == "linear":
- # Linear arrangement along z-axis (typical for needle electrodes)
- self.pts_origin = np.column_stack(
- [
- np.zeros(self.num_electrodes),
- np.zeros(self.num_electrodes),
- np.linspace(
- 0,
- (self.num_electrodes - 1) * self.inter_electrode_distance__mm,
- self.num_electrodes,
- ),
- ]
- )
- elif self.arrangement == "grid":
- # Grid arrangement (for specialized multi-channel arrays)
- n_per_side = int(np.ceil(np.sqrt(self.num_electrodes)))
- x_pos, y_pos = np.meshgrid(
- np.linspace(
- 0, (n_per_side - 1) * self.inter_electrode_distance__mm, n_per_side
- ),
- np.linspace(
- 0, (n_per_side - 1) * self.inter_electrode_distance__mm, n_per_side
- ),
- )
- # Take only the required number of points
- x_flat = x_pos.flatten()[: self.num_electrodes]
- y_flat = y_pos.flatten()[: self.num_electrodes]
- self.pts_origin = np.column_stack(
- [
- x_flat,
- y_flat,
- np.zeros(self.num_electrodes), # z = 0 for grid
- ]
- )
- else:
- raise ValueError(f"Unknown arrangement: {self.arrangement}")
+ self._normal_origin = []
+ self._normals_init = []
+ self._normals = []
- # Apply rotation and translation
- self._apply_transformation()
+ match differentiation_mode:
+ case "consecutive":
+ eye_mat = np.eye(
+ self._pts_origin.shape[0] - 1, self._pts_origin.shape[0]
+ )
+ self._diff_mat = eye_mat - np.roll(eye_mat, shift=1, axis=1)
+ case "reference":
+ self._diff_mat = np.roll(
+ np.eye(self._pts_origin.shape[0] - 1, self._pts_origin.shape[0]),
+ shift=1,
+ axis=1,
+ )
+ self._diff_mat[:, 0] = -1
- # Calculate trajectory points if needed
- if self.trajectory_steps > 1:
- self._calculate_trajectory_points()
- else:
- self.pts = self.pts_init.copy()
-
- def _apply_transformation(self) -> None:
- """Apply rotation and translation to electrode positions."""
- # Apply rotation matrices for each axis
- # Rotation around x-axis
- rx = self.orientation__rad[0]
- Rx = np.array(
- [[1, 0, 0], [0, np.cos(rx), -np.sin(rx)], [0, np.sin(rx), np.cos(rx)]]
- )
+ self._n_channels = self._diff_mat.shape[0]
- # Rotation around y-axis
- ry = self.orientation__rad[1]
- Ry = np.array(
- [[np.cos(ry), 0, np.sin(ry)], [0, 1, 0], [-np.sin(ry), 0, np.cos(ry)]]
+ self.set_position(
+ position__mm=self.position__mm, orientation__rad=self.orientation__rad
)
-
- # Rotation around z-axis
- rz = self.orientation__rad[2]
- Rz = np.array(
- [[np.cos(rz), -np.sin(rz), 0], [np.sin(rz), np.cos(rz), 0], [0, 0, 1]]
+ self.set_linear_trajectory(
+ distance__mm=self.trajectory_distance__mm, n_nodes=self.trajectory_steps
)
- # Combined rotation matrix
- R = Rz @ Ry @ Rx
-
- # Apply rotation and translation
- self.pts_init = (R @ self.pts_origin.T).T + np.array(self.position__mm)
-
- def _calculate_trajectory_points(self) -> None:
- """Calculate all observation points along the trajectory."""
- all_pts = []
- for i in range(self.n_nodes):
- # Linear movement along x-axis by default
- if self.n_nodes > 1:
- t = i / (self.n_nodes - 1) # Normalized parameter [0, 1]
- offset = np.array([t * self.trajectory_distance__mm, 0, 0])
- else:
- offset = np.array([0, 0, 0])
-
- # Translate all electrode points by the offset
- trajectory_pts = self.pts_init + offset
- all_pts.append(trajectory_pts)
-
- # Concatenate all trajectory positions
- self.pts = np.vstack(all_pts)
-
- # Extend the differential matrix to cover all trajectory nodes
- if hasattr(self, "diff_mat") and self.diff_mat is not None:
- self.diff_mat = np.tile(self.diff_mat, (1, self.n_nodes))
-
- def _create_differential_matrix(self) -> None:
- """Create differential recording matrix based on differentiation mode."""
- if self.differentiation_mode == "monopolar":
- # Monopolar recording: each electrode is a separate channel
- self.diff_mat = np.eye(self.num_electrodes)
- elif self.differentiation_mode == "consecutive":
- # Consecutive differential: adjacent electrode pairs
- if self.num_electrodes >= 2:
- self.diff_mat = np.zeros((self.num_channels, self.num_electrodes))
- for i in range(self.num_channels):
- self.diff_mat[i, i] = 1 # Positive electrode
- self.diff_mat[i, i + 1] = -1 # Negative electrode
- else:
- self.diff_mat = np.ones((1, self.num_electrodes))
- elif self.differentiation_mode == "reference":
- # Reference differential: all electrodes vs reference (last electrode)
- if self.num_electrodes >= 2:
- self.diff_mat = np.zeros((self.num_channels, self.num_electrodes))
- for i in range(self.num_channels):
- self.diff_mat[i, i + 1] = 1 # Signal electrode
- self.diff_mat[i, 0] = -1 # Reference electrode (first)
- else:
- self.diff_mat = np.ones((1, self.num_electrodes))
- else:
- # Default to monopolar
- self.diff_mat = np.eye(self.num_electrodes)
-
- def get_electrode_positions_for_simulation(
- self, trajectory_node: int = 0
- ) -> tuple[np.ndarray, np.ndarray]:
- """
- Get electrode positions formatted for simulation.
-
- Parameters
- ----------
- trajectory_node : int, default=0
- Trajectory node index to get positions for
+ # Create electrode positions and differential matrix
+ # self._create_electrode_positions()
+ # self._create_differential_matrix()
- Returns
- -------
- tuple
- Tuple containing:
- - electrode_positions: (num_electrodes, 3) array of electrode positions
- - differential_matrix: (num_channels, num_electrodes) differential matrix
+ def set_position(
+ self,
+ position__mm: tuple[float, float, float],
+ orientation__rad: tuple[float, float, float],
+ ) -> None:
"""
- if self.trajectory_steps > 1 and trajectory_node < self.n_nodes:
- # Get positions for specific trajectory node
- start_idx = trajectory_node * self.num_electrodes
- end_idx = start_idx + self.num_electrodes
- electrode_positions = self.pts[start_idx:end_idx, :]
- else:
- # Use initial positions
- electrode_positions = self.pts_init
+ Set the position and orientation of the intramuscular electrode array.
- return electrode_positions, self.diff_mat
+ This method defines the spatial placement and angular orientation of the
+ electrode array within the muscle volume. The array is first oriented
+ according to the specified rotations and then translated to the target position.
- def get_differential_matrix(self) -> np.ndarray:
- """
- Get the differential recording matrix.
-
- Returns
- -------
- np.ndarray
- Differential matrix with shape (num_channels, num_electrodes)
- """
- return self.diff_mat.copy()
+ **Coordinate System:**
+ - x-axis: radial direction (outward from muscle center)
+ - y-axis: circumferential direction (around muscle)
+ - z-axis: longitudinal direction (along muscle fibers)
- def set_trajectory(self, distance__mm: float, steps: int) -> None:
- """
- Set linear trajectory for electrode movement.
+ **Rotation Order:**
+ Applied as: Roll (x) → Pitch (y) → Yaw (z) using Rodrigues rotation
Parameters
----------
- distance__mm : float
- Total distance to move in mm
- steps : int
- Number of steps in the trajectory
+ position__mm : tuple[float, float, float]
+ Center position of the electrode array in mm (x, y, z coordinates).
+ This defines where the array center is placed within the muscle.
+ orientation__rad : tuple[float, float, float]
+ Orientation angles in radians (roll, pitch, yaw).
+ - Roll: rotation around x-axis (radial tilt)
+ - Pitch: rotation around y-axis (circumferential tilt)
+ - Yaw: rotation around z-axis (longitudinal rotation)
+
+ Notes
+ -----
+ Position and orientation changes affect all subsequent trajectory calculations.
+ The electrode positions are recalculated based on the new transformation.
+
+ Examples
+ --------
+ >>> # Place array at muscle center with 45° yaw rotation
+ >>> array.set_position(
+ ... position__mm=(0.0, 0.0, 10.0),
+ ... orientation__rad=(0.0, 0.0, np.pi/4)
+ ... )
+
+ See Also
+ --------
+ set_linear_trajectory : Define trajectory movement parameters
+ rodrigues_rot : Rodrigues rotation implementation
"""
- self.trajectory_distance__mm = distance__mm
- self.trajectory_steps = steps
- self.n_nodes = steps
+ self._pts_init = np.copy(self._pts_origin)
- # Recalculate trajectory points
- if steps > 1:
- self._calculate_trajectory_points()
- else:
- self.pts = self.pts_init.copy()
-
- def traj_mixing_fun(self, t: float, n_nodes: int, node: int) -> float:
- """
- Trajectory mixing function for interpolating between trajectory nodes.
+ self._pts_init = self.rodrigues_rot(
+ self._pts_init, [1, 0, 0], orientation__rad[0]
+ )
+ self._pts_init = self.rodrigues_rot(
+ self._pts_init, [0, 1, 0], orientation__rad[1]
+ )
+ self._pts_init = self.rodrigues_rot(
+ self._pts_init, [0, 0, 1], orientation__rad[2]
+ )
- Parameters
- ----------
- t : float
- Normalized parameter (between 0 and 1)
- n_nodes : int
- Number of trajectory nodes
- node : int
- Current node (1-indexed to match MATLAB)
+ self._pts_init += np.matlib.repmat(
+ np.array(position__mm)[None], self._pts_init.shape[0], 1
+ )
+ self._pts = np.copy(self._pts_init)
- Returns
- -------
- float
- Mixing weight for this node
+ def rodrigues_rot(self, v, k, theta):
"""
- # Convert to 0-indexed
- node_idx = node - 1
-
- # Normalized node position
- node_pos = node_idx / (n_nodes - 1 + np.finfo(float).eps)
+ Apply Rodrigues rotation to vectors around an arbitrary axis.
- # Linear interpolation weight (triangular function)
- return max(0, 1 - (n_nodes - 1) * abs(t - node_pos))
+ This method implements 3D rotation of points or vectors around an arbitrary
+ axis using the Rodrigues rotation formula. It is used internally for
+ electrode array positioning and trajectory calculations.
- def traj_mixing_mat(self, t: float, n_nodes: int, n_channels: int) -> np.ndarray:
- """
- Generate trajectory mixing matrix for smooth interpolation between nodes.
+ **Mathematical Foundation:**
+ Based on Rodrigues' rotation formula for rotating a vector v around
+ axis k by angle theta: v_rot = v*cos(θ) + (k×v)*sin(θ) + k*(k·v)*(1-cos(θ))
Parameters
----------
- t : float
- Normalized parameter (between 0 and 1)
- n_nodes : int
- Number of trajectory nodes
- n_channels : int
- Number of channels
+ v : array_like
+ Vector(s) to rotate. Can be single vector (3,) or array of vectors (N, 3).
+ k : array_like
+ Rotation axis vector (3,). Will be normalized internally.
+ theta : float
+ Rotation angle in radians. Positive angles follow right-hand rule.
Returns
-------
np.ndarray
- Diagonal mixing matrix for trajectory interpolation
+ Rotated vector(s) with same shape as input v.
+
+ Notes
+ -----
+ Uses scipy.spatial.transform.Rotation for numerical stability and efficiency.
+ The rotation axis k is automatically normalized to unit length.
+
+ Examples
+ --------
+ >>> # Rotate point 90° around z-axis
+ >>> point = np.array([1.0, 0.0, 0.0])
+ >>> rotated = array.rodrigues_rot(point, [0, 0, 1], np.pi/2)
+ >>> # Result: approximately [0, 1, 0]
"""
- # Calculate mixing weights for all nodes
- weights = []
- for node in range(1, n_nodes + 1): # 1-indexed to match MATLAB
- weight = self.traj_mixing_fun(t, n_nodes, node)
- weights.append(weight)
-
- # Repeat each weight for each electrode point
- weights_array = np.array(weights)
- repeated_weights = np.tile(weights_array, (self.num_points, 1))
- full_weights = repeated_weights.flatten("F") # Fortran order to match MATLAB
-
- # Create diagonal matrix
- return np.diag(full_weights)
-
- def __str__(self) -> str:
- """String representation of the electrode array."""
- return (
- f"IntramuscularElectrodeArray({self.num_electrodes} electrodes, "
- f"{self.inter_electrode_distance__mm}mm spacing, {self.arrangement} arrangement, "
- f"{self.differentiation_mode} mode)"
- )
+ v = np.array(v.copy(), dtype=float)
+ k = np.array(k.copy(), dtype=float)
+ k = k / np.linalg.norm(k) # normalize axis
- def __repr__(self) -> str:
- """Detailed representation of the electrode array."""
- return self.__str__()
+ r = R.from_rotvec(k * theta) # Create rotation from axis-angle
+ return r.apply(v) # Rotates v (works with (3,), (N, 3))
- @classmethod
- def create_single_differential(
- cls,
- inter_electrode_distance__mm: float = 0.5,
- position__mm: Tuple[float, float, float] = (0.0, 0.0, 20.0),
- orientation__rad: Tuple[float, float, float] = (-np.pi / 2, 0, -np.pi / 2),
- trajectory_distance__mm: float = 0.125,
- trajectory_steps: int = 4,
- ) -> "IntramuscularElectrodeArray":
+ def set_linear_trajectory(
+ self, distance__mm: float, n_nodes: int | None = None
+ ) -> None:
"""
- Create a single-channel differential electrode (equivalent to MATLAB s07 example).
-
- Parameters
- ----------
- inter_electrode_distance__mm : float, default=0.5
- Distance between electrode contacts in mm
- position__mm : Tuple[float, float, float], default=(0.0, 0.0, 20.0)
- Initial position of electrode array center
- orientation__rad : Tuple[float, float, float], default=(-π/2, 0, -π/2)
- Electrode orientation (matches MATLAB default)
- trajectory_distance__mm : float, default=0.125
- Distance for scanning trajectory
- trajectory_steps : int, default=4
- Number of trajectory steps
+ Configure linear trajectory movement for the electrode array.
- Returns
- -------
- IntramuscularElectrodeArray
- Configured electrode array
- """
- return cls(
- num_electrodes=2,
- inter_electrode_distance__mm=inter_electrode_distance__mm,
- position__mm=position__mm,
- orientation__rad=orientation__rad,
- arrangement="linear",
- differentiation_mode="consecutive",
- trajectory_distance__mm=trajectory_distance__mm,
- trajectory_steps=trajectory_steps,
- )
+ This method sets up a linear movement path for the electrode array,
+ simulating needle insertion or withdrawal. The trajectory is discretized
+ into nodes for temporal interpolation during EMG simulation.
- @classmethod
- def create_scanning_array(
- cls,
- num_electrodes: int = 16,
- inter_electrode_distance__mm: float = 1.0,
- position__mm: Tuple[float, float, float] = (-5.0, 0.0, 15.0),
- orientation__rad: Tuple[float, float, float] = (-np.pi / 6, 0, -np.pi / 2),
- trajectory_distance__mm: float = 1.0,
- trajectory_steps: int = 19,
- ) -> "IntramuscularElectrodeArray":
- """
- Create a scanning electrode array (equivalent to MATLAB s07 example).
+ **Trajectory Properties:**
+ - Direction: Along the array's longitudinal axis (z-direction in local coordinates)
+ - Movement: Linear progression from start to end position
+ - Discretization: Evenly spaced nodes for smooth interpolation
+ - Default step size: 0.5mm if n_nodes not specified
Parameters
----------
- num_electrodes : int, default=16
- Number of electrode contacts
- inter_electrode_distance__mm : float, default=1.0
- Distance between contacts in mm
- position__mm : Tuple[float, float, float], default=(-5.0, 0.0, 15.0)
- Initial position
- orientation__rad : Tuple[float, float, float], default=(-π/6, 0, -π/2)
- Electrode orientation
- trajectory_distance__mm : float, default=1.0
- Scanning distance
- trajectory_steps : int, default=19
- Number of scanning steps
-
- Returns
- -------
- IntramuscularElectrodeArray
- Configured scanning array
+ distance__mm : float
+ Total trajectory distance in mm. Positive values move in the
+ positive z-direction of the array's local coordinate system.
+ n_nodes : int, optional
+ Number of discrete trajectory nodes. If None, automatically
+ calculated as max(ceil(distance/0.5), 1) for 0.5mm steps.
+
+ Notes
+ -----
+ The trajectory is applied after position and orientation transformations.
+ All trajectory transforms are calculated in the array's oriented coordinate system.
+
+ Examples
+ --------
+ >>> # Set up 10mm insertion with default step size (~0.5mm)
+ >>> array.set_linear_trajectory(distance__mm=10.0)
+
+ >>> # Set up 5mm trajectory with specific number of nodes
+ >>> array.set_linear_trajectory(distance__mm=5.0, n_nodes=20)
+
+ See Also
+ --------
+ calc_observation_points : Calculate electrode positions along trajectory
+ traj_mixing_mat : Generate mixing matrices for trajectory interpolation
"""
- return cls(
- num_electrodes=num_electrodes,
- inter_electrode_distance__mm=inter_electrode_distance__mm,
- position__mm=position__mm,
- orientation__rad=orientation__rad,
- arrangement="linear",
- differentiation_mode="consecutive",
- trajectory_distance__mm=trajectory_distance__mm,
- trajectory_steps=trajectory_steps,
+ if n_nodes is None:
+ n_nodes = max(np.ceil(distance__mm / 0.5), 1)
+
+ self.n_nodes = n_nodes
+ self._trajectory_step = distance__mm / self.n_nodes
+
+ self.traj_transforms = np.linspace(start=0, stop=distance__mm, num=self.n_nodes)
+ self.traj_transforms = np.hstack(
+ [
+ np.zeros((max(self.traj_transforms.shape), 2)),
+ self.traj_transforms.reshape(-1, 1),
+ np.zeros((max(self.traj_transforms.shape), 3)),
+ ]
)
- @classmethod
- def create_end_to_end_array(
- cls,
- num_electrodes: int = 11,
- inter_electrode_distance__mm: float = 1.0,
- position__mm: Tuple[float, float, float] = (-5.0, 0.0, 20.0),
- orientation__rad: Tuple[float, float, float] = (-np.pi / 2, 0, -np.pi / 2),
- static: bool = True,
- ) -> "IntramuscularElectrodeArray":
- """
- Create an end-to-end electrode array (equivalent to MATLAB s07 example).
+ self.traj_transforms[:, :3] = self.rodrigues_rot(
+ self.traj_transforms[:, :3], [1, 0, 0], self.orientation__rad[0]
+ )
+ self.traj_transforms[:, :3] = self.rodrigues_rot(
+ self.traj_transforms[:, :3], [0, 1, 0], self.orientation__rad[1]
+ )
+ self.traj_transforms[:, :3] = self.rodrigues_rot(
+ self.traj_transforms[:, :3], [0, 0, 1], self.orientation__rad[2]
+ )
- Parameters
- ----------
- num_electrodes : int, default=11
- Number of electrode contacts
- inter_electrode_distance__mm : float, default=1.0
- Distance between contacts in mm
- position__mm : Tuple[float, float, float], default=(-5.0, 0.0, 20.0)
- Array position
- orientation__rad : Tuple[float, float, float], default=(-π/2, 0, -π/2)
- Array orientation
- static : bool, default=True
- Whether electrode is static (no trajectory)
+ self.calc_observation_points()
- Returns
- -------
- IntramuscularElectrodeArray
- Configured end-to-end array
- """
- trajectory_distance = 0.0 if static else 1.0
- trajectory_steps = 1 if static else 2
-
- return cls(
- num_electrodes=num_electrodes,
- inter_electrode_distance__mm=inter_electrode_distance__mm,
- position__mm=position__mm,
- orientation__rad=orientation__rad,
- arrangement="linear",
- differentiation_mode="consecutive",
- trajectory_distance__mm=trajectory_distance,
- trajectory_steps=trajectory_steps,
+ def calc_observation_points(self) -> None:
+ self.pts = np.concatenate(
+ [
+ self.rotate_and_translate(
+ self._pts_init,
+ self.traj_transforms[i, 3:],
+ self.traj_transforms[i, :3],
+ )
+ for i in range(self.n_nodes)
+ ]
)
- def get_electrode_normals(self) -> Optional[np.ndarray]:
- """
- Get electrode normal vectors (for point electrodes, returns None).
+ self._diff_mat = np.matlib.repmat(self._diff_mat, 1, self.n_nodes)
- Returns
- -------
- Optional[np.ndarray]
- Normal vectors or None for point electrodes
- """
- # Point electrodes don't have specific orientations
- return None
+ def rotate_and_translate(self, pt, rpy, d):
+ # pt: (N, 3) matrix
+ # rpy: (3,) vector [roll, pitch, yaw]
+ # d: (3,) vector translation
- def get_visible_area(self, fiber_positions: np.ndarray) -> np.ndarray:
+ pt = self.rodrigues_rot(pt, np.array([1, 0, 0]), rpy[0]) # roll
+ pt = self.rodrigues_rot(pt, np.array([0, 1, 0]), rpy[1]) # pitch
+ pt = self.rodrigues_rot(pt, np.array([0, 0, 1]), rpy[2]) # yaw
+
+ return pt + d.reshape(1, 3) # translation (broadcasted)
+
+ def traj_mixing_fun(self, t, n_nodes, node) -> float:
"""
- Calculate visible area/detectability for each fiber position.
+ Compute mixing weight for a specific trajectory node at given time.
- This is a simplified version of the MATLAB get_visible_area function.
+ This function calculates the interpolation weight for a trajectory node based
+ on the current time/position along the trajectory. Uses triangular weighting
+ where nodes closer to the current time get higher weights.
Parameters
----------
- fiber_positions : np.ndarray
- Fiber positions (N × 3) in mm
+ t : float
+ Current normalized time or position in trajectory (0.0 to 1.0).
+ n_nodes : int
+ Total number of nodes in the trajectory.
+ node : int or array_like
+ Node index(es) for which to calculate mixing weights.
+ Can be scalar or array of node indices.
Returns
-------
- np.ndarray
- Visibility weights for each fiber
+ float or np.ndarray
+ Mixing weight(s) for the specified node(s) at time t.
+ Returns 0 for distant nodes, max weight 1 for closest node.
"""
- # Get current electrode positions
- electrode_positions, _ = self.get_electrode_positions_for_simulation()
-
- # Calculate distances from each fiber to closest electrode
- min_distances = np.inf * np.ones(len(fiber_positions))
-
- for electrode_pos in electrode_positions:
- distances = np.sqrt(np.sum((fiber_positions - electrode_pos) ** 2, axis=1))
- min_distances = np.minimum(min_distances, distances)
-
- # Convert distance to visibility (closer = more visible)
- # Use exponential decay with characteristic length scale
- visibility_length_scale = 2.0 # mm
- visibility = np.exp(-min_distances / visibility_length_scale)
-
- return visibility
+ eps = np.finfo(float).eps
+ return np.maximum(
+ 0,
+ 1 - (n_nodes - 1) * np.abs(t - (node - 1) / (n_nodes - 1 + eps)),
+ )
- def set_position(self, position__mm: Tuple[float, float, float]) -> None:
+ def traj_mixing_mat(self, t, n_nodes, n_channels) -> np.ndarray:
"""
- Update electrode array position.
-
- Parameters
- ----------
- position__mm : Tuple[float, float, float]
- New position in mm
- """
- self.position__mm = position__mm
- self._apply_transformation()
+ Generate mixing matrix for trajectory interpolation during EMG simulation.
- if self.trajectory_steps > 1:
- self._calculate_trajectory_points()
- else:
- self.pts = self.pts_init.copy()
+ This method creates a diagonal mixing matrix that weights the contribution of
+ different trajectory nodes during temporal interpolation. The matrix enables
+ smooth transitions between electrode positions as the array moves along its
+ trajectory during needle insertion or withdrawal.
- def set_orientation(self, orientation__rad: Tuple[float, float, float]) -> None:
- """
- Update electrode array orientation.
+ **Interpolation Strategy:**
+ - Linear interpolation between adjacent trajectory nodes
+ - Weights based on distance from current time/position to node positions
+ - Diagonal matrix structure for efficient computation
+ - Smooth transitions avoid discontinuities in EMG signals
Parameters
----------
- orientation__rad : Tuple[float, float, float]
- New orientation in radians (roll, pitch, yaw)
- """
- self.orientation__rad = orientation__rad
- self._apply_transformation()
-
- if self.trajectory_steps > 1:
- self._calculate_trajectory_points()
- else:
- self.pts = self.pts_init.copy()
-
- def get_electrode_type_info(self) -> Dict[str, Any]:
- """
- Get electrode type information for simulation logging.
+ t : float
+ Current normalized time or position in trajectory (0.0 to 1.0).
+ 0.0 corresponds to trajectory start, 1.0 to trajectory end.
+ n_nodes : int
+ Total number of trajectory nodes for interpolation.
+ n_channels : int
+ Number of recording channels in the electrode array.
+ Depends on differentiation mode and electrode count.
Returns
-------
- Dict[str, Any]
- Dictionary with electrode configuration details
+ np.ndarray
+ Diagonal mixing matrix with shape (n_nodes * n_channels, n_nodes * n_channels).
+ Diagonal elements contain repeated mixing weights for each trajectory node,
+ with each node's weight repeated n_channels times.
+
+ Notes
+ -----
+ The mixing matrix enables temporal interpolation of EMG signals recorded
+ at different trajectory positions. Higher weights are given to nodes
+ closer to the current time/position parameter.
+
+ Examples
+ --------
+ >>> # Get mixing weights for mid-trajectory position
+ >>> mix_mat = array.traj_mixing_mat(t=0.5, n_nodes=10, n_channels=4)
+ >>> # Matrix will weight middle nodes more heavily
+
+ See Also
+ --------
+ traj_mixing_fun : Individual node mixing function
+ set_linear_trajectory : Configure trajectory parameters
"""
- return {
- "type": self.type,
- "type_short": self.type_short,
- "num_electrodes": self.num_electrodes,
- "num_channels": self.num_channels,
- "inter_electrode_distance_mm": self.inter_electrode_distance__mm,
- "differentiation_mode": self.differentiation_mode,
- "arrangement": self.arrangement,
- "trajectory_steps": self.trajectory_steps,
- "trajectory_distance_mm": self.trajectory_distance__mm,
- "position_mm": self.position__mm,
- "orientation_rad": self.orientation__rad,
- }
+
+ return np.diag(
+ np.repeat(
+ self.traj_mixing_fun(t, n_nodes, np.arange(1, n_nodes + 1)),
+ n_channels,
+ )
+ )
diff --git a/myogen/simulator/core/emg/intramuscular/__init__.py b/myogen/simulator/core/emg/intramuscular/__init__.py
new file mode 100644
index 00000000..06c4addf
--- /dev/null
+++ b/myogen/simulator/core/emg/intramuscular/__init__.py
@@ -0,0 +1,10 @@
+"""
+Intramuscular EMG simulation components.
+
+This module contains classes and functions for simulating intramuscular EMG signals.
+"""
+
+from myogen.simulator.core.emg.intramuscular.intramuscular_emg import IntramuscularEMG
+from myogen.simulator.core.emg.intramuscular.motor_unit_sim import MotorUnitSim
+
+__all__ = ["IntramuscularEMG", "MotorUnitSim"]
diff --git a/myogen/simulator/core/emg/intramuscular/bioelectric.py b/myogen/simulator/core/emg/intramuscular/bioelectric.py
new file mode 100644
index 00000000..14142e71
--- /dev/null
+++ b/myogen/simulator/core/emg/intramuscular/bioelectric.py
@@ -0,0 +1,432 @@
+"""
+Bioelectric functions for intramuscular EMG simulation.
+
+This module contains the core bioelectric modeling functions for simulating
+single fiber action potentials (SFAPs) and motor unit action potentials (MUAPs)
+in intramuscular EMG. The functions implement the volume conductor models
+from Farina et al. 2004 and the transmembrane current models from
+Rosenfalck 1969.
+
+References
+----------
+.. [1] Farina, D., Merletti, R., 2001. A novel approach for precise simulation of
+ the EMG signal detected by surface electrodes. IEEE Transactions on
+ Biomedical Engineering 48, 637–646.
+.. [2] Rosenfalck, P., 1969. Intra- and extracellular potential fields of active
+ nerve and muscle fibres. Acta Physiologica Scandinavica Supplementum 321, 1–168.
+.. [3] Nandedkar, S.D., Stålberg, E., 1983. Simulation of single muscle fibre
+ action potentials. Medical & Biological Engineering & Computing 21, 158–165.
+"""
+
+import numpy as np
+
+
+def get_tm_current(z: np.ndarray, D1: float = 96.0, D2: float = -90.0) -> np.ndarray:
+ """
+ Calculate transmembrane current using Rosenfalck's model.
+
+ This function implements the transmembrane current model from:
+ P. Rosenfalck "Intra and extracellular fields of active nerve and muscle fibers" (1969)
+
+ Parameters
+ ----------
+ z : np.ndarray
+ Spatial coordinates along fiber in mm
+ D1 : float, default=96.0
+ Current amplitude parameter in mV/mm³
+ D2 : float, default=-90.0
+ Baseline potential in mV
+
+ Returns
+ -------
+ np.ndarray
+ Transmembrane potential in mV
+ """
+ Vm = np.full(z.shape, D2, dtype=np.float64)
+ Vm[z > 0] = D1 * (z[z > 0] ** 3) * np.exp(-z[z > 0]) + D2
+ return Vm
+
+
+def get_tm_current_dz(z: np.ndarray, D1: float = 96.0) -> np.ndarray:
+ """
+ Calculate first derivative of transmembrane current (Rosenfalck model).
+
+ This is the spatial derivative of the transmembrane current model used
+ for action potential propagation simulation.
+
+ Parameters
+ ----------
+ z : np.ndarray
+ Spatial coordinates along fiber in mm
+ D1 : float, default=96.0
+ Current amplitude parameter in mV/mm³
+
+ Returns
+ -------
+ np.ndarray
+ First derivative of transmembrane current
+ """
+ Vm = np.zeros_like(z, dtype=np.float64)
+ pos_mask = z > 0
+ z_pos = z[pos_mask]
+ Vm[pos_mask] = D1 * (3 * z_pos**2 - z_pos**3) * np.exp(-z_pos)
+ return Vm
+
+
+def get_tm_current_ddz(z: np.ndarray, D1: float = 96.0) -> np.ndarray:
+ """
+ Calculate second derivative of transmembrane current (Rosenfalck model).
+
+ Parameters
+ ----------
+ z : np.ndarray
+ Spatial coordinates along fiber in mm
+ D1 : float, default=96.0
+ Current amplitude parameter in mV/mm³
+
+ Returns
+ -------
+ np.ndarray
+ Second derivative of transmembrane current
+ """
+ Vm = np.zeros_like(z, dtype=np.float64)
+ pos_mask = z > 0
+ z_pos = z[pos_mask]
+ Vm[pos_mask] = (
+ D1 * ((6 * z_pos - 3 * z_pos**2) - (3 * z_pos**2 - z_pos**3)) * np.exp(-z_pos)
+ )
+ return Vm
+
+
+def get_elementary_current_response(
+ z: np.ndarray,
+ z_electrode: float,
+ r: np.ndarray,
+ sigma_r: float = 63.0, # S/m
+ sigma_z: float = 330.0, # S/m
+) -> np.ndarray:
+ """
+ Calculate elementary current response for volume conductor.
+
+ This function calculates the potential response at electrode location
+ due to a unit current source at different positions along the muscle fiber.
+ Based on Nandedkar & Stålberg 1983.
+
+ Parameters
+ ----------
+ z : np.ndarray
+ Longitudinal coordinates along fiber in mm
+ z_electrode : float
+ Electrode position along z-axis in mm
+ r : np.ndarray
+ Radial distance from fiber to electrode in mm
+ sigma_r : float, default=63.0
+ Radial conductivity in S/m (from Andreassen & Rosenfalck 1980)
+ sigma_z : float, default=330.0
+ Longitudinal conductivity in S/m (from Andreassen & Rosenfalck 1980)
+
+ Returns
+ -------
+ np.ndarray
+ Elementary current response (transfer function)
+ """
+ return np.divide(
+ 1 / 4 / np.pi / sigma_r,
+ np.sqrt(sigma_z / sigma_r * r**2 + (z - z_electrode) ** 2),
+ )
+
+
+def shift_padding(vec, sh, axis):
+ """
+ Circularly shifts 'vec' by 'sh' positions along the specified 'axis'
+ and then pads the shifted region with zeros.
+
+ Parameters
+ ----------
+ vec : ndarray
+ Input array to shift.
+ sh : int
+ Shift amount (positive means downward/rightward like MATLAB).
+ axis : int
+ Axis along which to shift.
+
+ Returns
+ -------
+ ndarray
+ Shifted and zero-padded array.
+ """
+ vec = np.roll(vec, sh, axis=axis)
+
+ n = len(vec)
+
+ # Equivalent of vec(1:sh) = 0
+ if sh > 0:
+ vec[:sh] = 0
+
+ # Equivalent of vec(end+sh+1:end) = 0
+ if sh < 0:
+ start = n + sh # because end+sh+1 in MATLAB is 1-based
+ if start < n:
+ vec[start:] = 0
+ elif sh > 0:
+ vec[-sh:] = 0
+
+ return vec
+
+
+def hr_shift_template(x, delay):
+ """
+ Shifts waveform x by a subsample step 'delay' using FFT-based phase shifting.
+
+ Parameters:
+ x (array-like): Input signal.
+ delay (float): Fraction of the sampling period to delay (e.g. 0.1 means 1/10th).
+
+ Returns:
+ shifted (np.ndarray): Fractionally shifted signal.
+ """
+ x = np.asarray(x).flatten()
+
+ # Pad if even length
+ padded = False
+ if len(x) % 2 == 0:
+ x = np.append(x, 0)
+ padded = True
+
+ N = len(x)
+
+ X = np.fft.fft(x)
+ X0 = X[0]
+ Xk = X[1 : int(np.ceil(N / 2))]
+
+ k = np.arange(1, len(Xk) + 1)
+ Sk = Xk * np.exp(1j * (2 * np.pi * delay) * k / N)
+ S = np.concatenate(([X0], Sk, np.conj(Sk[::-1])))
+
+ shifted = np.fft.ifft(S).real # same as MATLAB, assumes real signal
+
+ if padded:
+ shifted = shifted[:-1]
+
+ return shifted
+
+
+def get_current_density(
+ t, z, zi, L1, L2, v, d=55e-6, suppress_endplate_density=True, endplate_width=0.5
+):
+ """
+ Model the individual action potential (IAP) or single fiber action potential (SFAP) in space and time.
+ Translated from Farina & Merletti (2001) and Nandedkar & Stålberg (1998).
+
+ Parameters
+ ----------
+ t : array
+ Time vector
+ z : array
+ Spatial coordinates along the muscle fiber (in mm)
+ zi : float
+ Position of endplate (in mm)
+ L1 : float
+ Length of fiber from zi to positive end (mm)
+ L2 : float
+ Length of fiber from zi to negative end (mm)
+ v : float
+ Conduction speed in mm/s
+ d : float, optional
+ Fiber diameter in mm (default: 55 µm)
+ suppress_endplate_density : bool, optional
+ Whether to suppress density at endplate region (default: True)
+ endplate_width : float, optional
+ Width around endplate where density is suppressed (mm)
+ """
+
+ dz = np.mean(np.diff(z, axis=0))
+ z = np.concatenate([z, z[[-1]] + dz], axis=0)
+
+ T, Z = np.meshgrid(t, z)
+
+ # Tendon terminator function
+ def tendon_terminator(z_inline, L_inline):
+ return (z_inline <= L_inline / 2) & (z_inline >= -L_inline / 2)
+
+ # Compute psi (transmembrane current derivative)
+ if L1 >= L2:
+ psi = -4 * get_tm_current_dz(-2 * (Z - zi - v * T))
+ longest_wave = np.diff(psi, axis=0) / dz
+ longest_wave *= tendon_terminator(Z[:-1, :] - zi - L1 / 2, L1)
+ longest_wave *= (Z[:-1, :] - zi) / v > 0 # negative time suppression
+ else:
+ psi = 4 * get_tm_current_dz(-2 * (-Z + zi - v * T))
+ longest_wave = np.diff(psi, axis=0) / dz
+ longest_wave *= tendon_terminator(Z[:-1, :] - zi + L2 / 2, L2)
+ longest_wave *= (-Z[:-1, :] + zi) / v > 0
+
+ # Shortest wave (reversed)
+ shortest_wave = longest_wave[::-1].copy()
+ shift_amount = int(np.round((L1 + L2 - max(z) + L2 - L1) / dz))
+ shortest_wave = shift_padding(shortest_wave, shift_amount, 0)
+
+ if L1 >= L2:
+ shortest_wave *= tendon_terminator(Z[:-1, :] - zi + L2 / 2, L2)
+ iap = longest_wave - shortest_wave
+ else:
+ shortest_wave *= tendon_terminator(Z[:-1, :] - zi - L1 / 2, L1)
+ iap = shortest_wave - longest_wave
+
+ # Suppress endplate density if required
+ if suppress_endplate_density:
+
+ def endplate_terminator(z_inline):
+ return (z_inline <= (zi - endplate_width)) | (
+ z_inline >= (zi + endplate_width)
+ )
+
+ iap *= endplate_terminator(Z[:-1, :])
+
+ # Scale using intracellular conductivity
+ sigma_i = 1.01 * 1000 # [S/m] converted to S/mm
+ d *= 1000 # convert fiber diameter from mm to µm for consistency with original scaling
+ iap *= sigma_i * np.pi * ((d / 2) ** 2) / 4
+
+ return iap
+
+
+def get_current_density_fast(
+ precalculated: np.ndarray,
+ t: np.ndarray,
+ z: np.ndarray,
+ zi: float,
+ L1: float,
+ L2: float,
+ v: float,
+ d: float = 55e-6,
+ suppress_endplate_density: bool = True,
+ endplate_width: float = 0.5,
+) -> np.ndarray:
+ """
+ Fast version of current density calculation using precalculated lookup table.
+
+ This is an optimized version that uses a precalculated transmembrane current
+ derivative lookup table to speed up computation for multiple fibers.
+
+ Parameters
+ ----------
+ precalculated : np.ndarray
+ Precalculated lookup table for get_tm_current_dz
+ t : np.ndarray
+ Time vector in seconds
+ z : np.ndarray
+ Spatial coordinates along muscle fiber in mm
+ zi : float
+ Position of endplate in mm
+ L1 : float
+ Length from endplate to positive tendon in mm
+ L2 : float
+ Length from endplate to negative tendon in mm
+ v : float
+ Conduction velocity in mm/s
+ d : float, default=55e-6
+ Fiber diameter in mm
+ suppress_endplate_density : bool, default=True
+ Whether to suppress endplate region
+ endplate_width : float, default=0.5
+ Endplate suppression width in mm
+
+ Returns
+ -------
+ np.ndarray
+ Current density matrix (space × time)
+ """
+ # This is a simplified version - full implementation would require
+ # proper lookup table indexing and bounds checking
+ # For now, fall back to the regular version
+ return get_current_density(
+ t, z, zi, L1, L2, v, d, suppress_endplate_density, endplate_width
+ )
+
+
+def calculate_sfap(
+ electrode_position: np.ndarray,
+ fiber_positions: np.ndarray,
+ fiber_lengths: tuple[float, float],
+ endplate_position: float,
+ conduction_velocity: float,
+ fiber_diameter: float,
+ time_vector: np.ndarray,
+ spatial_resolution: float = 0.5,
+) -> np.ndarray:
+ """
+ Calculate Single Fiber Action Potential (SFAP) at electrode location.
+
+ This is a high-level function that combines current density calculation
+ with volume conductor modeling to compute the SFAP detected by an electrode.
+
+ Parameters
+ ----------
+ electrode_position : np.ndarray
+ 3D position of electrode [x, y, z] in mm
+ fiber_positions : np.ndarray
+ 3D positions along fiber [x, y, z] in mm (N × 3)
+ fiber_lengths : tuple[float, float]
+ Lengths (L1, L2) from endplate to each tendon in mm
+ endplate_position : float
+ Z-coordinate of endplate in mm
+ conduction_velocity : float
+ Fiber conduction velocity in mm/s
+ fiber_diameter : float
+ Fiber diameter in mm
+ time_vector : np.ndarray
+ Time points for simulation in seconds
+ spatial_resolution : float, default=0.5
+ Spatial sampling resolution in mm
+
+ Returns
+ -------
+ np.ndarray
+ SFAP signal at electrode location
+ """
+ L1, L2 = fiber_lengths
+
+ # Create spatial grid along fiber
+ z_min = min(fiber_positions[:, 2])
+ z_max = max(fiber_positions[:, 2])
+ z_fiber = np.arange(z_min, z_max + spatial_resolution, spatial_resolution)
+
+ # Calculate current density along fiber
+ current_density = get_current_density(
+ time_vector,
+ z_fiber,
+ endplate_position,
+ L1,
+ L2,
+ conduction_velocity,
+ fiber_diameter,
+ )
+
+ # Calculate volume conductor response for each point along fiber
+ sfap_signal = np.zeros(len(time_vector))
+
+ for i, z_point in enumerate(z_fiber):
+ # Find closest fiber position point
+ distances = np.sqrt(
+ np.sum(
+ (
+ fiber_positions
+ - [electrode_position[0], electrode_position[1], z_point]
+ )
+ ** 2,
+ axis=1,
+ )
+ )
+ min_idx = np.argmin(distances)
+ r_distance = distances[min_idx]
+
+ # Calculate elementary response
+ h_response = get_elementary_current_response(
+ np.array([z_point]), electrode_position[2], np.array([r_distance])
+ )
+
+ # Convolve current with volume conductor response
+ sfap_signal += current_density[i, :] * h_response[0] * spatial_resolution
+
+ return sfap_signal
diff --git a/myogen/simulator/core/emg/intramuscular/intramuscular_emg.py b/myogen/simulator/core/emg/intramuscular/intramuscular_emg.py
new file mode 100644
index 00000000..6aa357ea
--- /dev/null
+++ b/myogen/simulator/core/emg/intramuscular/intramuscular_emg.py
@@ -0,0 +1,531 @@
+"""
+Intramuscular Electromyography (iEMG) Simulation.
+
+This module provides the main simulation framework for generating intramuscular
+electromyography signals using needle electrodes. It integrates motor unit
+simulation, electrode modeling, and signal generation with realistic noise.
+"""
+
+import warnings
+from typing import Optional
+
+import numpy as np
+from tqdm import tqdm
+
+from myogen import RANDOM_GENERATOR
+from myogen.simulator.core.emg.electrodes import IntramuscularElectrodeArray
+from myogen.simulator.core.muscle import Muscle
+from myogen.simulator.core.spike_train import MotorNeuronPool
+from myogen.utils.types import (
+ INTRAMUSCULAR_EMG__TENSOR,
+ INTRAMUSCULAR_MUAP_SHAPE__TENSOR,
+ beartowertype,
+)
+
+try:
+ import cupy as cp
+
+ HAS_CUPY = True
+except ImportError:
+ HAS_CUPY = False
+
+from .motor_unit_sim import MotorUnitSim
+
+# Suppress warnings for cleaner output
+warnings.filterwarnings("ignore")
+
+
+@beartowertype
+class IntramuscularEMG:
+ """
+ Intramuscular Electromyography (iEMG) Simulation.
+
+ This class provides a comprehensive simulation framework for generating
+ intramuscular EMG signals detected by needle electrodes.
+
+ Parameters
+ ----------
+ muscle_model : Muscle
+ Pre-computed muscle model (see :class:`myogen.simulator.Muscle`).
+ electrode_array : IntramuscularElectrodeArray
+ Intramuscular electrode array configuration to use for simulation (see :class:`myogen.simulator.IntramuscularElectrodeArray`).
+ sampling_frequency__Hz : float, default=10240.0
+ Sampling frequency in Hz for EMG simulation.
+ Default is set to 10240 Hz as used by the Quattrocento (OT Bioelettronica, Turin, Italy) system.
+ spatial_resolution__mm : float, default=0.01
+ Spatial resolution for fiber action potential calculation in mm.
+ Default is set to 0.01 mm.
+ endplate_center__percent : float, default=50
+ Percentage of muscle length where the endplate is located.
+ By default, the endplate is located at the center of the muscle (50% of the muscle length).
+ nmj_jitter__s : float, default=35e-6
+ Standard deviation of neuromuscular junction jitter in seconds.
+ Default is set to 35e-6 s as determined by Konstantin et al. 2020 [1]_.
+ branch_cvs__m_per_s : tuple[float, float], default=(5.0, 2.0)
+ Conduction velocities for the two-layer model of the neuromuscular junction in m/s.
+ Default is set to (5.0, 2.0) m/s as determined by Konstantin et al. 2020 [1]_.
+
+ .. note::
+ The two-layer model is a simplification of the actual arborization pattern, but it is a good approximation for the purposes of this simulation.
+ Follows the implementation of Kontos et al. 2020 [1]_.
+ MUs_to_simulate : list[int], optional
+ Indices of motor units to simulate. If None, all motor units are simulated.
+ Default is None. For computational efficiency, consider
+ simulating subsets for initial analysis.
+ Indices correspond to the recruitment order (0 is recruited first).
+
+ References
+ ----------
+ .. [1] Konstantin, A., Yu, T., Le Carpentier, E., Aoustin, Y., Farina, D., 2020. Simulation of Motor Unit Action Potential Recordings From Intramuscular Multichannel Scanning Electrodes. IEEE Transactions on Biomedical Engineering 67, 2005–2014. https://doi.org/10.1109/TBME.2019.2953680
+ """
+
+ def __init__(
+ self,
+ muscle_model: Muscle,
+ electrode_array: IntramuscularElectrodeArray,
+ sampling_frequency__Hz: float = 10240.0,
+ spatial_resolution__mm: float = 0.01,
+ endplate_center__percent: float = 50,
+ nmj_jitter__s: float = 35e-6,
+ branch_cvs__m_per_s: tuple[float, float] = (5.0, 2.0),
+ MUs_to_simulate: list[int] | None = None,
+ ):
+ self.muscle_model = muscle_model
+ self.electrode_array = electrode_array
+ self.sampling_frequency__Hz = sampling_frequency__Hz
+ self.spatial_resolution__mm = spatial_resolution__mm
+ self.endplate_center__percent = endplate_center__percent
+ self.nmj_jitter__s = nmj_jitter__s
+ self.branch_cvs__m_per_s = branch_cvs__m_per_s
+ self.MUs_to_simulate = MUs_to_simulate
+
+ self._branch_cvs__mm_per_s: list[float] = list(
+ (self.branch_cvs__m_per_s[0] * 1000.0, self.branch_cvs__m_per_s[1] * 1000.0)
+ )
+ self._endplate_center__mm = muscle_model.length__mm * (
+ endplate_center__percent / 100.0
+ )
+
+ # Derived parameters
+ self._dt = 1.0 / sampling_frequency__Hz
+ self._dz = spatial_resolution__mm
+ self._n_motor_units = len(muscle_model.recruitment_thresholds)
+
+ # Motor unit selection
+ if MUs_to_simulate is None:
+ self._MUs_to_simulate = list(range(self._n_motor_units))
+ else:
+ self._MUs_to_simulate = self.MUs_to_simulate
+
+ # Motor unit simulations
+ self._motor_units: list[MotorUnitSim] = [] # List of motor unit simulators
+ self._muaps: Optional[INTRAMUSCULAR_MUAP_SHAPE__TENSOR] = None
+ self._max_muap_length: int = 0
+
+ # Simulation results
+ self.intramuscular_emg__tensor: INTRAMUSCULAR_EMG__TENSOR | None = None
+ self.noisy_intramuscular_emg__tensor: INTRAMUSCULAR_EMG__TENSOR | None = None
+
+ def simulate_muaps(self) -> INTRAMUSCULAR_MUAP_SHAPE__TENSOR:
+ """
+ Simulate MUAPs for all electrode arrays using the provided muscle model.
+
+ Returns
+ -------
+ INTRAMUSCULAR_MUAP_SHAPE__TENSOR
+ Intramuscular MUAP shapes for all electrode arrays.
+ """
+ self._initialize_motor_units()
+ self._simulate_neuromuscular_junctions()
+ return self._calculate_muaps()
+
+ def _initialize_motor_units(self) -> None:
+ """
+ Initialize individual motor unit simulators.
+
+ This method creates MotorUnitSim objects for each motor unit based on
+ the muscle model fiber assignments and properties.
+ """
+ if (
+ not hasattr(self.muscle_model, "assignment")
+ or self.muscle_model.assignment is None
+ ):
+ raise ValueError(
+ "Muscle model must have fiber assignments. Call muscle.assign_mfs2mns() first."
+ )
+
+ self._motor_units = []
+
+ for mu_idx in tqdm(
+ self._MUs_to_simulate,
+ desc="Creating motor unit simulators",
+ unit="Simulator",
+ ):
+ # Get fibers assigned to this motor unit
+ fiber_mask = self.muscle_model.assignment == mu_idx
+ if not np.any(fiber_mask):
+ continue
+
+ # Create motor unit simulator
+ self._motor_units.append(
+ MotorUnitSim(
+ muscle_fiber_centers__mm=self.muscle_model.mf_centers[fiber_mask],
+ muscle_length__mm=self.muscle_model.length__mm,
+ muscle_fiber_diameters__mm=self.muscle_model.mf_diameters[
+ fiber_mask
+ ],
+ muscle_fiber_conduction_velocity__mm_per_s=self.muscle_model.mf_cv[
+ fiber_mask
+ ],
+ neuromuscular_junction_conduction_velocities__mm_per_s=self._branch_cvs__mm_per_s,
+ nominal_center__mm=self.muscle_model.innervation_center_positions[
+ mu_idx
+ ],
+ )
+ )
+
+ def _simulate_neuromuscular_junctions(self) -> None:
+ """
+ Simulate neuromuscular junction distributions for all motor units.
+
+ This implements the logic from s08_cl_init_muaps.m for generating
+ realistic NMJ branch patterns with size-dependent complexity.
+ """
+ if not self._motor_units:
+ raise ValueError("Must call _initialize_motor_units() first")
+
+ n_branches = 1 + np.round(
+ np.log(
+ self.muscle_model.recruitment_thresholds
+ / self.muscle_model.recruitment_thresholds[0]
+ )
+ )
+
+ for mu_idx, mu_sim in enumerate(
+ tqdm(
+ self._motor_units, desc="Setting up NMJ distributions", unit="Simulator"
+ )
+ ):
+ spread_factor = np.sum(
+ self.muscle_model.recruitment_thresholds[:mu_idx]
+ ) / np.sum(self.muscle_model.recruitment_thresholds)
+
+ # Create NMJ distribution
+ # Branch spread increases with motor unit size
+ mu_sim.sim_nmj_branches_two_layers(
+ n_branches=int(n_branches[mu_idx]),
+ endplate_center=self._endplate_center__mm,
+ branches_z_std=1.5 + spread_factor * 4.0,
+ arborization_z_std=0.5 + spread_factor * 1.5,
+ )
+
+ def _calculate_muaps(self) -> INTRAMUSCULAR_MUAP_SHAPE__TENSOR:
+ """
+ Pre-calculate motor unit action potentials (MUAPs).
+
+ Returns
+ -------
+ INTRAMUSCULAR_MUAP_SHAPE__TENSOR
+ Intramuscular MUAP shapes for all electrode arrays.
+ """
+ if not self._motor_units:
+ raise ValueError("Must call _initialize_motor_units() first")
+
+ # Calculate SFAPs for each motor unit
+ for i, mu_sim in enumerate(self._motor_units):
+ mu_sim.calc_sfaps(
+ index=i,
+ dt=self._dt,
+ dz=self._dz,
+ electrode_positions=self.electrode_array.pts,
+ )
+
+ # Calculate MUAPs (no jitter for templates)
+ muaps_list: list[np.ndarray] = []
+ max_length = 0
+
+ for mu_sim in tqdm(self._motor_units, desc="Computing MUAPs", unit="MU"):
+ muap = mu_sim.calc_muap(jitter_std=0.0) # No jitter for templates
+ muaps_list.append(muap)
+ max_length = max(max_length, muap.shape[0])
+
+ self._muaps: INTRAMUSCULAR_MUAP_SHAPE__TENSOR = np.zeros(
+ (len(self._motor_units), self.electrode_array.pts.shape[0], max_length)
+ )
+
+ for i, muap in enumerate(muaps_list):
+ self._muaps[i, :, : muap.shape[0]] = muap.T
+
+ return self._muaps
+
+ def _analyze_detectable_motor_units(self) -> tuple[np.ndarray, list[int]]:
+ """
+ Analyze which motor units are detectable by the electrode.
+
+ This implements s11_cl_get_detectable_mus.m logic for determining
+ motor unit visibility based on signal-to-noise ratio and contribution.
+
+ Returns
+ -------
+ tuple[np.ndarray, List[int]]
+ Boolean array of detectable motor units and their indices
+ """
+ if self._muaps is None:
+ raise ValueError("Must call simulate_muaps() first")
+
+ print("Analyzing motor unit detectability...")
+
+ detectable = np.zeros(len(self._motor_units), dtype=bool)
+
+ # Prominence criterion: MUAP amplitude vs noise
+ over_noise_threshold = 2.0 # 2x noise level
+
+ for i, mu_sim in enumerate(self._motor_units):
+ # Get peak MUAP amplitude across all channels
+ muap_amplitudes = np.max(np.abs(self._muaps[i]), axis=0)
+ max_amplitude = np.max(muap_amplitudes)
+
+ # Check if MUAP is prominent enough above noise
+ # Use a simple noise threshold estimate
+ noise_estimate = np.std(self._muaps) * 0.1 # Simple noise estimate
+ is_prominent = max_amplitude > over_noise_threshold * noise_estimate
+
+ # Additional criterion: contribution to total signal variance
+ relative_size = (i + 1) / len(self._motor_units)
+ min_contribution = 0.05 # Minimum 5% contribution
+ contributes_enough = relative_size > min_contribution
+
+ detectable[i] = is_prominent and contributes_enough
+
+ detectable_indices = [i for i, det in enumerate(detectable) if det]
+
+ print(
+ f"Found {np.sum(detectable)} detectable motor units out of {len(self._motor_units)}"
+ )
+
+ return detectable, detectable_indices
+
+ def simulate_intramuscular_emg(
+ self,
+ motor_neuron_pool: MotorNeuronPool,
+ ) -> INTRAMUSCULAR_EMG__TENSOR:
+ """
+ Generate intramuscular EMG signals for all electrode arrays using the provided motor neuron pool.
+
+ Parameters
+ ----------
+ motor_neuron_pool : MotorNeuronPool
+ Motor neuron pool with spike trains computed (see :class:`myogen.simulator.MotorNeuronPool`).
+
+ Returns
+ -------
+ INTRAMUSCULAR_EMG__TENSOR
+ Intramuscular EMG signals for all electrode arrays.
+
+ Raises
+ ------
+ AttributeError
+ If MUAP templates have not been generated. Call simulate_muaps() first.
+ """
+ if self._muaps is None:
+ raise AttributeError(
+ "MUAP templates have not been generated. Call simulate_muaps() first."
+ )
+
+ self.motor_neuron_pool = motor_neuron_pool
+
+ # Handle MUs to simulate
+ if self._MUs_to_simulate is None:
+ MUs_to_simulate = set(
+ range(len(self.muscle_model.resulting_number_of_innervated_fibers))
+ )
+ else:
+ MUs_to_simulate = set(self._MUs_to_simulate)
+
+ muap_array = self._muaps.copy()
+
+ target_length = int(
+ np.round(
+ muap_array.shape[2]
+ / self.sampling_frequency__Hz
+ * 1
+ / self.motor_neuron_pool.timestep__ms
+ * 1000
+ )
+ )
+ muap_shapes = np.zeros(
+ (muap_array.shape[0], muap_array.shape[1], target_length)
+ )
+ for muap_nr in range(muap_shapes.shape[0]):
+ for electrode_nr in range(muap_shapes.shape[1]):
+ muap_shapes[muap_nr, electrode_nr] = np.interp(
+ np.linspace(
+ 0,
+ muap_array.shape[-1] / self.sampling_frequency__Hz,
+ target_length,
+ endpoint=False,
+ ),
+ np.arange(
+ 0,
+ muap_array.shape[-1] / self.sampling_frequency__Hz,
+ 1 / self.sampling_frequency__Hz,
+ ),
+ muap_array[muap_nr, electrode_nr],
+ )
+
+ n_pools = motor_neuron_pool.spike_trains.shape[0]
+ n_electrodes = muap_shapes.shape[1]
+
+ # Initialize result array
+ sample_conv = np.convolve(
+ motor_neuron_pool.spike_trains[0, 0],
+ muap_shapes[0, 0],
+ mode="same",
+ )
+ intramuscular_emg = np.zeros((n_pools, n_electrodes, len(sample_conv)))
+
+ muap_shapes /= np.max(np.abs(muap_shapes)) # Normalize MUAP shapes
+
+ # Perform convolution for each pool using GPU acceleration if available
+ if HAS_CUPY:
+ # Use GPU acceleration with CuPy
+ spike_gpu = cp.asarray(motor_neuron_pool.spike_trains)
+ muap_gpu = cp.asarray(muap_shapes)
+ intramuscular_emg_gpu = cp.zeros((n_pools, n_electrodes, len(sample_conv)))
+
+ for pool_idx in tqdm(
+ range(n_pools),
+ desc=f"Intramuscular EMG (GPU)",
+ unit="pool",
+ ):
+ active_neuron_indices = set(
+ motor_neuron_pool.active_neuron_indices[pool_idx]
+ )
+
+ for e_idx in range(n_electrodes):
+ # Process all active MUs on GPU
+ convolutions = cp.array(
+ [
+ cp.correlate(
+ spike_gpu[pool_idx, mu_idx],
+ muap_gpu[i, e_idx],
+ mode="same",
+ )
+ for i, mu_idx in enumerate(
+ MUs_to_simulate.intersection(active_neuron_indices)
+ )
+ ]
+ )
+ # Sum across MUAPs on GPU
+ if len(convolutions) > 0:
+ intramuscular_emg_gpu[pool_idx, e_idx] = cp.sum(
+ convolutions, axis=0
+ )
+
+ # Transfer results back to CPU
+ intramuscular_emg = cp.asnumpy(intramuscular_emg_gpu)
+ else:
+ # Fallback to CPU computation with NumPy
+ for pool_idx in tqdm(
+ range(n_pools),
+ desc=f"Intramuscular EMG (CPU)",
+ unit="pool",
+ ):
+ active_neuron_indices = set(
+ motor_neuron_pool.active_neuron_indices[pool_idx]
+ )
+
+ for e_idx in range(n_electrodes):
+ # Process all active MUs
+ convolutions = []
+ for i, mu_idx in enumerate(
+ MUs_to_simulate.intersection(active_neuron_indices)
+ ):
+ conv = np.correlate(
+ motor_neuron_pool.spike_trains[pool_idx, mu_idx],
+ muap_shapes[i, e_idx],
+ mode="same",
+ )
+ convolutions.append(conv)
+
+ if convolutions:
+ intramuscular_emg[pool_idx, e_idx] = np.sum(
+ convolutions, axis=0
+ )
+
+ intramuscular_emg_resampled = np.zeros(
+ (
+ n_pools,
+ n_electrodes,
+ int(
+ intramuscular_emg.shape[-1]
+ / (1 / self.motor_neuron_pool.timestep__ms * 1000)
+ * self.sampling_frequency__Hz
+ ),
+ )
+ )
+ for pool_idx in range(n_pools):
+ for e_idx in range(n_electrodes):
+ intramuscular_emg_resampled[pool_idx, e_idx] = np.interp(
+ np.arange(
+ 0,
+ intramuscular_emg.shape[-1]
+ * (self.motor_neuron_pool.timestep__ms / 1000),
+ 1 / self.sampling_frequency__Hz,
+ ),
+ np.arange(
+ 0,
+ intramuscular_emg.shape[-1]
+ * (self.motor_neuron_pool.timestep__ms / 1000),
+ self.motor_neuron_pool.timestep__ms / 1000,
+ ),
+ intramuscular_emg[pool_idx, e_idx],
+ )
+
+ self.intramuscular_emg__tensor = intramuscular_emg_resampled
+ return intramuscular_emg_resampled
+
+ def add_noise(
+ self, snr_db: float, noise_type: str = "gaussian"
+ ) -> INTRAMUSCULAR_EMG__TENSOR:
+ """
+ Add noise to all electrode arrays.
+
+ Parameters
+ ----------
+ snr_db : float
+ Signal-to-noise ratio in dB
+ noise_type : str, default="gaussian"
+ Type of noise to add
+
+ Returns
+ -------
+ INTRAMUSCULAR_EMG__TENSOR
+ Noisy intramuscular EMG signals for all electrode arrays.
+ """
+ if self.intramuscular_emg__tensor is None:
+ raise ValueError(
+ "Intramuscular EMG has not been simulated. Call simulate_intramuscular_emg() first."
+ )
+
+ # Calculate signal power
+ signal_power = np.mean(self.intramuscular_emg__tensor**2)
+
+ # Calculate noise power
+ snr_linear = 10 ** (snr_db / 10)
+ noise_power = signal_power / snr_linear
+
+ # Generate noise
+ if noise_type.lower() == "gaussian":
+ noise_std = np.sqrt(noise_power)
+ noise = RANDOM_GENERATOR.normal(
+ loc=0.0, scale=noise_std, size=self.intramuscular_emg__tensor.shape
+ )
+ else:
+ raise ValueError(f"Unsupported noise type: {noise_type}")
+
+ # Add noise
+ noisy_emg = self.intramuscular_emg__tensor + noise
+
+ self.noisy_intramuscular_emg__tensor = noisy_emg
+ return noisy_emg
diff --git a/myogen/simulator/core/emg/intramuscular/motor_unit_sim.py b/myogen/simulator/core/emg/intramuscular/motor_unit_sim.py
new file mode 100644
index 00000000..1d28de57
--- /dev/null
+++ b/myogen/simulator/core/emg/intramuscular/motor_unit_sim.py
@@ -0,0 +1,490 @@
+"""
+Motor Unit Simulation for Intramuscular EMG.
+
+This module implements individual motor unit simulation including neuromuscular
+junction modeling, single fiber action potential (SFAP) calculation, and
+motor unit action potential (MUAP) generation with realistic jitter.
+
+Based on the MU_Sim class from the MATLAB iemg_simulator.
+"""
+
+from typing import Optional, List
+
+import numpy as np
+from scipy.spatial.distance import cdist
+from sklearn.cluster import KMeans
+from tqdm import tqdm
+
+from myogen import RANDOM_GENERATOR, SEED
+from myogen.utils.types import beartowertype
+from .bioelectric import (
+ get_current_density,
+ get_elementary_current_response,
+ shift_padding,
+ hr_shift_template,
+)
+
+
+@beartowertype
+class MotorUnitSim:
+ """
+ Simulation of individual motor unit for intramuscular EMG.
+
+ This class handles the simulation of a single motor unit including:
+ - Muscle fiber spatial distribution
+ - Neuromuscular junction positioning and timing
+ - Single fiber action potential (SFAP) calculation
+ - Motor unit action potential (MUAP) generation with jitter
+
+ Parameters
+ ----------
+ muscle_fiber_centers__mm : np.ndarray
+ Muscle fiber center positions (N × 3) in mm [x, y, z].
+ muscle_length__mm : float
+ Total muscle length in mm.
+ muscle_fiber_diameters__mm : np.ndarray
+ Muscle fiber diameters in mm (N,).
+ muscle_fiber_conduction_velocity__mm_per_s : np.ndarray
+ Muscle fiber conduction velocities in mm/s (N,).
+ neuromuscular_junction_conduction_velocities__mm_per_s : List[float]
+ Neuromuscular junction branch conduction velocities in mm/s.
+ nominal_center__mm : np.ndarray, optional
+ Nominal center of the motor unit in mm [x, y]. This is the target center of the motor unit,
+ which is used to calculate the nerve paths.
+ """
+
+ def __init__(
+ self,
+ muscle_fiber_centers__mm: np.ndarray,
+ muscle_length__mm: float,
+ muscle_fiber_diameters__mm: np.ndarray,
+ muscle_fiber_conduction_velocity__mm_per_s: np.ndarray,
+ nominal_center__mm: np.ndarray,
+ neuromuscular_junction_conduction_velocities__mm_per_s: List[float] = [
+ 5000.0,
+ 2000.0,
+ ],
+ ):
+ self.muscle_fiber_centers__mm = muscle_fiber_centers__mm
+ self.muscle_length__mm = muscle_length__mm
+ self.muscle_fiber_diameters__mm = muscle_fiber_diameters__mm[..., None]
+ self.muscle_fiber_conduction_velocity__mm_per_s = (
+ muscle_fiber_conduction_velocity__mm_per_s[..., None]
+ )
+ self.neuromuscular_junction_conduction_velocities__mm_per_s = (
+ neuromuscular_junction_conduction_velocities__mm_per_s
+ )
+ self.nominal_center__mm = nominal_center__mm
+
+ self._number_of_muscle_fibers = len(muscle_fiber_centers__mm)
+
+ # Initialize fiber end positions
+ self._muscle_fiber_left_ends__mm = np.zeros(
+ shape=(self._number_of_muscle_fibers, 1)
+ ) # coordinates of muscle fibers left ends
+ self._muscle_fiber_right_ends__mm = np.full(
+ fill_value=muscle_length__mm, shape=(self._number_of_muscle_fibers, 1)
+ ) # coordinates of muscle fibers right ends
+
+ # Neuromuscular junction properties
+ self._neuromuscular_z_coordinates__mm: Optional[np.ndarray] = None
+ self._neuromuscular_delays: Optional[np.ndarray] = None
+ self._branch_points_xy__mm: Optional[List] = None
+ self._branch_points_z__mm: Optional[List] = None
+ self._nerve_paths: Optional[np.ndarray] = None
+
+ # Simulation results
+ self._sfaps: Optional[np.ndarray] = None # Single fiber action potentials
+ self._muap: Optional[np.ndarray] = None # Motor unit action potential
+
+ # Simulation parameters
+ self._dt: Optional[float] = None
+ self._dz: Optional[float] = None
+ self._number_of_electrode_points: Optional[int] = (
+ None # Number of electrode points
+ )
+
+ # Centers
+ self._actual_center = np.mean(muscle_fiber_centers__mm, axis=0)
+
+ def sim_nmj_branches_two_layers(
+ self,
+ n_branches: int,
+ endplate_center: float,
+ branches_z_std: float,
+ arborization_z_std: float,
+ ):
+ """
+ Simulate neuromuscular junction branches using two-layer model.
+
+ This creates a realistic distribution of neuromuscular junctions
+ with primary branches and secondary arborizations.
+
+ Parameters
+ ----------
+ n_branches : int
+ Number of primary branches
+ endplate_center : float
+ Center position of endplate zone in mm
+ branches_z_std : float
+ Standard deviation of primary branch distribution in mm
+ arborization_z_std : float
+ Standard deviation of secondary arborization in mm
+ """
+ rng = 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
+ )
+ idx = kmeans.fit_predict(self.muscle_fiber_centers__mm)
+ c = kmeans.cluster_centers_
+
+ self.branch_points_xy = c
+ self.branch_points_z = endplate_center + branches_z_std * rng.standard_normal(
+ size=(n_branches, 1)
+ )
+
+ self._neuromuscular_z_coordinates__mm = np.array(
+ [
+ self.branch_points_z[idx[i]]
+ + arborization_z_std * rng.standard_normal()
+ for i in range(self._number_of_muscle_fibers)
+ ]
+ )
+
+ self._actual_center = np.concatenate(
+ [
+ np.mean(self.muscle_fiber_centers__mm, axis=0),
+ np.mean(self._neuromuscular_z_coordinates__mm, axis=0),
+ ]
+ )[None]
+ for i in range(self._number_of_muscle_fibers):
+ cluster_center = np.concatenate(
+ [
+ self.branch_points_xy[idx[i]],
+ self._neuromuscular_z_coordinates__mm[idx[i]],
+ ]
+ )[None]
+ nmj_coordinates = np.concatenate(
+ [
+ self.muscle_fiber_centers__mm[i],
+ self._neuromuscular_z_coordinates__mm[i],
+ ]
+ )[None]
+
+ self.nerve_paths[i, 0] = np.linalg.norm(
+ self._actual_center - cluster_center, axis=-1
+ )
+ self.nerve_paths[i, 1] = np.linalg.norm(
+ nmj_coordinates - cluster_center, axis=-1
+ )
+
+ # Calculate delays
+ # self._calculate_nmj_delays()
+
+ def sim_nmj_branches_gaussian(self, endplate_center: float, branches_z_std: float):
+ """
+ Simulate neuromuscular junctions with simple Gaussian distribution.
+
+ Parameters
+ ----------
+ endplate_center : float
+ Center of endplate zone in mm
+ branches_z_std : float
+ Standard deviation of NMJ distribution in mm
+ """
+ rng = RANDOM_GENERATOR
+ self.nmj_z = rng.normal(endplate_center, branches_z_std, self.Nmf)
+
+ # Simplified nerve paths (single segment)
+ self.nerve_paths = np.zeros((self.Nmf, 1))
+ for i in range(self.Nmf):
+ distance = np.sqrt(
+ (self.mf_centers[i, 0] - self.actual_center[0]) ** 2
+ + (self.mf_centers[i, 1] - self.actual_center[1]) ** 2
+ + (self.nmj_z[i] - endplate_center) ** 2
+ )
+ self.nerve_paths[i, 0] = distance
+
+ self._calculate_nmj_delays()
+
+ def _calculate_nmj_delays(self):
+ """Calculate neuromuscular junction propagation delays."""
+ if self.nerve_paths is None:
+ return
+
+ self.nmj_delays = np.zeros(self.Nmf)
+
+ for i in range(self.Nmf):
+ total_delay = 0.0
+ for segment_idx in range(self.nerve_paths.shape[1]):
+ path_length = self.nerve_paths[i, segment_idx]
+ if segment_idx < len(self.nmj_cv):
+ cv = self.nmj_cv[segment_idx]
+ else:
+ cv = self.nmj_cv[-1] # Use last velocity for additional segments
+ total_delay += path_length / cv
+
+ self.nmj_delays[i] = total_delay
+
+ def calc_sfaps(
+ self,
+ index: int,
+ dt: float,
+ dz: float,
+ electrode_positions: np.ndarray,
+ electrode_normals: Optional[np.ndarray] = None,
+ min_radial_dist: Optional[float] = None,
+ ):
+ """
+ Calculate single fiber action potentials (SFAPs) for all fibers.
+
+ Parameters
+ ----------
+ dt : float
+ Time step in seconds
+ dz : float
+ Spatial step in mm
+ electrode_positions : np.ndarray
+ Electrode positions (N_electrodes × 3) in mm
+ electrode_normals : np.ndarray, optional
+ Electrode normal vectors (not used for point electrodes)
+ min_radial_dist : float, optional
+ Minimum radial distance for stability (default: mean diameter * 1000)
+ """
+ self.dt = dt
+ self.dz = dz
+ self.Npt = electrode_positions.shape[0]
+
+ if min_radial_dist is None:
+ min_radial_dist = float(
+ np.mean(self.muscle_fiber_diameters__mm) * 1000
+ ) # Convert to micrometers
+
+ if self._neuromuscular_z_coordinates__mm is None:
+ raise ValueError(
+ "Must call sim_nmj_branches_* method first to set neuromuscular junction positions"
+ )
+
+ t = np.arange(
+ start=0,
+ stop=2
+ * np.max(
+ [
+ np.divide(
+ self._neuromuscular_z_coordinates__mm
+ - self._muscle_fiber_left_ends__mm,
+ self.muscle_fiber_conduction_velocity__mm_per_s,
+ ),
+ np.divide(
+ self._muscle_fiber_right_ends__mm
+ - self._neuromuscular_z_coordinates__mm,
+ self.muscle_fiber_conduction_velocity__mm_per_s,
+ ),
+ ]
+ )
+ + dt,
+ step=dt,
+ )[..., None]
+ self.sfaps = np.zeros((len(t), self.Npt, self._number_of_muscle_fibers))
+
+ for fiber_idx in tqdm(
+ range(self._number_of_muscle_fibers),
+ desc=f"MU {index}: Calculating SFAPs",
+ unit="fiber",
+ ):
+ z_left = np.arange(
+ start=self._neuromuscular_z_coordinates__mm[fiber_idx],
+ step=-dz,
+ stop=self._muscle_fiber_left_ends__mm[fiber_idx] - dz,
+ )
+ z_right = np.arange(
+ start=self._neuromuscular_z_coordinates__mm[fiber_idx],
+ step=dz,
+ stop=self._muscle_fiber_right_ends__mm[fiber_idx] + dz,
+ )
+ z = np.concatenate((z_left[::-1], z_right[1:]))[:, None]
+ mf_coord_3d = np.concatenate(
+ [
+ np.matlib.repmat(
+ a=self.muscle_fiber_centers__mm[fiber_idx], m=len(z), n=1
+ ),
+ z,
+ ],
+ axis=1,
+ )
+
+ current_density = get_current_density(
+ t,
+ z,
+ self._neuromuscular_z_coordinates__mm[fiber_idx],
+ self._muscle_fiber_right_ends__mm[fiber_idx]
+ - self._neuromuscular_z_coordinates__mm[fiber_idx],
+ self._neuromuscular_z_coordinates__mm[fiber_idx]
+ - self._muscle_fiber_left_ends__mm[fiber_idx],
+ self.muscle_fiber_conduction_velocity__mm_per_s[fiber_idx],
+ self.muscle_fiber_diameters__mm[fiber_idx],
+ )
+
+ for electrode_idx in range(self.Npt):
+ # Calculate radial distance from fiber to electrode
+ radial_distance = np.sqrt(
+ np.sum(
+ (
+ electrode_positions[electrode_idx, :2]
+ - self.muscle_fiber_centers__mm[fiber_idx]
+ )
+ ** 2,
+ keepdims=True,
+ )
+ )
+ if radial_distance < min_radial_dist:
+ radial_distance = min_radial_dist
+
+ response_to_elem_current = get_elementary_current_response(
+ z,
+ electrode_positions[electrode_idx, 2],
+ radial_distance,
+ )
+
+ self.sfaps[:, electrode_idx, fiber_idx] = (
+ current_density.T @ response_to_elem_current
+ )[:, 0]
+
+ self.shift_sfaps(dt)
+
+ def calc_mnap_delays(self):
+ self.mnap_delays = np.divide(
+ self.nerve_paths,
+ np.matlib.repmat(
+ np.array(self.neuromuscular_junction_conduction_velocities__mm_per_s)[
+ None
+ ],
+ self._number_of_muscle_fibers,
+ 1,
+ ),
+ ).sum(axis=1, keepdims=True)
+
+ def shift_sfaps(self, dt):
+ self.calc_mnap_delays()
+
+ for fb in range(self._number_of_muscle_fibers):
+ for pt in range(self.Npt):
+ self.sfaps[:, pt, fb] = shift_padding(
+ self.sfaps[:, pt, fb],
+ int(np.floor(self.mnap_delays[fb] / dt)),
+ axis=0,
+ )
+ self.sfaps[:, pt, fb] = hr_shift_template(
+ self.sfaps[:, pt, fb], int(np.mod(self.mnap_delays[fb], dt))
+ )
+
+ def calc_muap(self, jitter_std: float = 0.0) -> np.ndarray:
+ """
+ Calculate motor unit action potential (MUAP) with optional jitter.
+
+ Parameters
+ ----------
+ jitter_std : float, default=0.0
+ Standard deviation of neuromuscular junction jitter in seconds
+
+ Returns
+ -------
+ np.ndarray
+ MUAP signal (time × electrodes)
+ """
+ if self.sfaps is None:
+ raise ValueError("Must call calc_sfaps() first")
+
+ if self.dt is None:
+ raise ValueError("_dt not set - call calc_sfaps() first")
+
+ if jitter_std != 0:
+ delays = jitter_std * RANDOM_GENERATOR.standard_normal(
+ size=(self._number_of_muscle_fibers, 1)
+ )
+ jittered_sfaps = np.zeros_like(self.sfaps)
+ for fiber_idx in range(self._number_of_muscle_fibers):
+ for electrode_idx in range(self.Npt):
+ jittered_sfaps[:, electrode_idx, fiber_idx] = hr_shift_template(
+ self.sfaps[:, electrode_idx, fiber_idx],
+ delays[fiber_idx] / self.dt,
+ )
+
+ self.muap = np.sum(jittered_sfaps, axis=2)
+ else:
+ self.muap = np.sum(self.sfaps, axis=2)
+
+ return self.muap
+
+ def get_muap_duration(self, threshold_fraction: float = 0.1) -> float:
+ """
+ Get MUAP duration based on threshold crossing.
+
+ Parameters
+ ----------
+ threshold_fraction : float, default=0.1
+ Fraction of peak amplitude to use as threshold
+
+ Returns
+ -------
+ float
+ MUAP duration in seconds
+ """
+ if self.muap is None or self.dt is None:
+ return 0.0
+
+ # Use first electrode channel
+ signal = self.muap[:, 0]
+ peak_amplitude = np.max(np.abs(signal))
+ threshold = threshold_fraction * peak_amplitude
+
+ # Find first and last threshold crossings
+ above_threshold = np.abs(signal) > threshold
+ if not np.any(above_threshold):
+ return 0.0
+
+ start_idx = np.where(above_threshold)[0][0]
+ end_idx = np.where(above_threshold)[0][-1]
+
+ return (end_idx - start_idx) * self.dt
+
+ def get_muap_amplitude(self, electrode_idx: int = 0) -> float:
+ """
+ Get peak-to-peak MUAP amplitude.
+
+ Parameters
+ ----------
+ electrode_idx : int, default=0
+ Electrode index to analyze
+
+ Returns
+ -------
+ float
+ Peak-to-peak amplitude
+ """
+ if self.muap is None:
+ return 0.0
+
+ signal = self.muap[:, electrode_idx]
+ return float(np.max(signal) - np.min(signal))
+
+ @property
+ def fiber_count(self) -> int:
+ """Number of muscle fibers in this motor unit."""
+ return self.Nmf
+
+ @property
+ def territory_center(self) -> np.ndarray:
+ """Center of motor unit territory."""
+ return self.actual_center
+
+ @property
+ def territory_radius(self) -> float:
+ """Approximate radius of motor unit territory."""
+ distances = cdist([self.actual_center[:2]], self.mf_centers[:, :2])
+ return float(np.mean(distances))
diff --git a/myogen/simulator/core/emg/surface/surface_emg.py b/myogen/simulator/core/emg/surface/surface_emg.py
index 10f70c76..36d46c58 100644
--- a/myogen/simulator/core/emg/surface/surface_emg.py
+++ b/myogen/simulator/core/emg/surface/surface_emg.py
@@ -17,7 +17,11 @@
from myogen.simulator.core.spike_train import MotorNeuronPool
from myogen.simulator.core.emg.surface.simulate_fiber import simulate_fiber_v2
from myogen.simulator.core.emg.electrodes import SurfaceElectrodeArray
-from myogen.utils.types import MUAP_SHAPE__TENSOR, SURFACE_EMG__TENSOR, beartowertype
+from myogen.utils.types import (
+ SURFACE_MUAP_SHAPE__TENSOR,
+ SURFACE_EMG__TENSOR,
+ beartowertype,
+)
# Suppress warnings for cleaner output during batch simulations
warnings.filterwarnings("ignore")
@@ -32,10 +36,6 @@ class SurfaceEMG:
surface electromyography signals from the muscle. It implements the
multi-layered cylindrical volume conductor model from Farina et al. 2004 [1]_.
- .. note::
- All default values are set to simulate the first dorsal interosseous muscle (FDI) of the hand.
- Change all physiological parameters to simulate other muscles.
-
Parameters
----------
muscle_model : Muscle
@@ -105,13 +105,13 @@ def __init__(
+ self.skin_thickness__mm
)
- def simulate_muaps(self) -> list[MUAP_SHAPE__TENSOR]:
+ def simulate_muaps(self) -> list[SURFACE_MUAP_SHAPE__TENSOR]:
"""
Simulate MUAPs for all electrode arrays using the provided muscle model.
Returns
-------
- list[MUAP_SHAPE__TENSOR]
+ list[SURFACE_MUAP_SHAPE__TENSOR]
List of generated MUAP templates for each electrode array.
"""
# Set default MUs to simulate
@@ -365,11 +365,13 @@ def simulate_surface_emg(
[
cp.correlate(
spike_gpu[pool_idx, mu_idx],
- muap_gpu[mu_idx, row_idx, col_idx],
+ muap_gpu[i, row_idx, col_idx],
mode="same",
)
- for mu_idx in MUs_to_simulate.intersection(
- active_neuron_indices
+ for i, mu_idx in enumerate(
+ MUs_to_simulate.intersection(
+ active_neuron_indices
+ )
)
]
)
@@ -396,12 +398,12 @@ def simulate_surface_emg(
for col_idx in range(n_cols):
# Process all active MUs
convolutions = []
- for mu_idx in MUs_to_simulate.intersection(
- active_neuron_indices
+ for i, mu_idx in enumerate(
+ MUs_to_simulate.intersection(active_neuron_indices)
):
conv = np.correlate(
motor_neuron_pool.spike_trains[pool_idx, mu_idx],
- muap_shapes[mu_idx, row_idx, col_idx],
+ muap_shapes[i, row_idx, col_idx],
mode="same",
)
convolutions.append(conv)
diff --git a/myogen/simulator/core/force/force_model.py b/myogen/simulator/core/force/force_model.py
index 6daff69b..a542c48d 100644
--- a/myogen/simulator/core/force/force_model.py
+++ b/myogen/simulator/core/force/force_model.py
@@ -4,10 +4,10 @@
import matplotlib.pyplot as plt
from tqdm import tqdm
-from myogen.utils.types import SPIKE_TRAIN__MATRIX
+from myogen.utils.types import SPIKE_TRAIN__MATRIX, beartowertype
-@beartype
+@beartowertype
class ForceModel:
"""
Force model based on Fuglevand et al. (1993) [1]_.
@@ -98,6 +98,25 @@ def _initialize_twitches(self):
]
def generate_force(self, spike_train__matrix: SPIKE_TRAIN__MATRIX) -> np.ndarray:
+ """
+ Generate force output from motor unit spike trains using the Fuglevand model.
+
+ This method simulates muscle force by converting spike trains into force output
+ through individual motor unit twitches with nonlinear gain modulation based on
+ discharge rate.
+
+ Parameters
+ ----------
+ spike_train__matrix : SPIKE_TRAIN__MATRIX
+ Spike train matrix with shape (n_pools, n_neurons, n_time_points).
+ Binary array where 1 indicates a spike occurrence.
+
+ Returns
+ -------
+ np.ndarray
+ Force output array with shape (n_pools, n_time_points).
+ Force values in arbitrary units representing muscle force over time.
+ """
return np.array(
[self._generate_force(spike_train.T) for spike_train in spike_train__matrix]
)
diff --git a/myogen/simulator/core/muscle/muscle.py b/myogen/simulator/core/muscle/muscle.py
index ba7e7288..912428c4 100644
--- a/myogen/simulator/core/muscle/muscle.py
+++ b/myogen/simulator/core/muscle/muscle.py
@@ -88,6 +88,8 @@ class Muscle:
Values range from 0 to 1 with the largest motor units having thresholds near 1.
radius__mm : float, default=6.91
Radius of the muscle cross-section in millimeters. Default is set to 6.91 mm as determined by Jacobson et al. 1992 [3]_.
+ length__mm : float, default=30.0
+ Length of the muscle in millimeters. Default is set to 30.0 mm as determined by no one.
fiber_density__fibers_per_mm2 : float, default=400
Density of muscle fibers per square millimeter. Default is set to 400 fibers/mm² as determined by no one.
max_innervation_area_to_total_muscle_area__ratio : float, default=0.25
@@ -151,6 +153,7 @@ def __init__(
self,
recruitment_thresholds: np.ndarray,
radius__mm: float = 6.91,
+ length__mm: float = 30.0,
fiber_density__fibers_per_mm2: float = 400,
max_innervation_area_to_total_muscle_area__ratio: float = 1 / 4,
mean_conduction_velocity__m_s: float = 4.2,
@@ -168,6 +171,7 @@ def __init__(
):
# Muscle properties
self.radius__mm = radius__mm
+ self.length__mm = length__mm
self.fiber_density__fibers_per_mm2 = fiber_density__fibers_per_mm2
self.max_innervation_area_to_total_muscle_area__ratio = (
max_innervation_area_to_total_muscle_area__ratio
diff --git a/myogen/utils/plotting/recruitment_thresholds.py b/myogen/utils/plotting/recruitment_thresholds.py
index 843d7c04..36695ac6 100644
--- a/myogen/utils/plotting/recruitment_thresholds.py
+++ b/myogen/utils/plotting/recruitment_thresholds.py
@@ -8,7 +8,7 @@
from beartype.cave import IterableType
from matplotlib.axes import Axes
from matplotlib import pyplot as plt
-from typing import Any, Union, Dict, Tuple, Optional
+from typing import Any, Union, Dict, Optional
# Configure multiple sources to suppress font warnings
logging.getLogger("matplotlib.font_manager").setLevel(logging.ERROR)
diff --git a/myogen/utils/types.py b/myogen/utils/types.py
index 96e76d3a..e5b0f7f6 100644
--- a/myogen/utils/types.py
+++ b/myogen/utils/types.py
@@ -17,32 +17,38 @@
Is[lambda x: x.ndim == 2],
]
+# Cortical input matrix: (mu_pools, time_points)
+CORTICAL_INPUT__MATRIX = Annotated[
+ npt.NDArray[np.floating],
+ Is[lambda x: x.ndim == 2],
+]
+
# Spike train matrix: (pools, neurons_per_pool, time_points)
SPIKE_TRAIN__MATRIX = Annotated[
npt.NDArray[np.bool_],
Is[lambda x: x.ndim == 3],
]
-# MUAP shape tensor: (muap_index, electrode_grid_rows, electrode_grid_columns, muap_samples)
-MUAP_SHAPE__TENSOR = Annotated[
+# Surface MUAP shape tensor: (muap_index, electrode_grid_rows, electrode_grid_columns, muap_samples)
+SURFACE_MUAP_SHAPE__TENSOR = Annotated[
npt.NDArray[np.floating],
Is[lambda x: x.ndim == 4],
]
-# Surface EMG tensor: (mu_pools, electrode_grid_rows, electrode_grid_columns, time)
+# Intramuscular MUAP shape tensor: (muap_index, n_electrodes, muap_samples)
+INTRAMUSCULAR_MUAP_SHAPE__TENSOR = Annotated[
+ npt.NDArray[np.floating],
+ Is[lambda x: x.ndim == 3],
+]
+
+# Surface EMG tensor: (mu_pools, electrode_grid_rows, electrode_grid_columns, time_points)
SURFACE_EMG__TENSOR = Annotated[
npt.NDArray[np.floating],
Is[lambda x: x.ndim == 4],
]
-# Intramuscular EMG tensor: (mu_pools, n_electrodes, time)
+# Intramuscular EMG tensor: (mu_pools, n_electrodes, time_points)
INTRAMUSCULAR_EMG__TENSOR = Annotated[
npt.NDArray[np.floating],
Is[lambda x: x.ndim == 3],
]
-
-# Cortical input matrix: (mu_pools, time_points)
-CORTICAL_INPUT__MATRIX = Annotated[
- npt.NDArray[np.floating],
- Is[lambda x: x.ndim == 2],
-]
diff --git a/pyproject.toml b/pyproject.toml
index 70a9ea47..b159fc23 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "MyoGen"
-version = "0.2.0"
+version = "0.3.0"
description = "Surface and Intramuscular EMG simulation toolkit"
readme = "README.md"
requires-python = ">=3.12"
@@ -18,6 +18,7 @@ dependencies = [
"scikit-fmm>=2025.1.29",
"scikit-learn>=1.6.1",
"scipy>=1.15.3",
+ "scipy-stubs==1.16.1.0",
"seaborn>=0.13.2",
"tqdm>=4.67.1",
]
@@ -36,6 +37,7 @@ docs = [
"rinohtype>=0.5.5",
"sphinx>=8.1.3",
"sphinx-gallery>=0.19.0",
+ "sphinxcontrib-mermaid>=1.0.0",
"toml>=0.10.2",
]
diff --git a/uv.lock b/uv.lock
index 965c01ac..d9f2f70d 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1,5 +1,5 @@
version = 1
-revision = 2
+revision = 3
requires-python = ">=3.12"
resolution-markers = [
"python_full_version >= '3.13'",
@@ -128,7 +128,7 @@ wheels = [
[[package]]
name = "blosc2"
-version = "3.5.1"
+version = "3.6.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "msgpack" },
@@ -139,18 +139,18 @@ dependencies = [
{ name = "py-cpuinfo", marker = "platform_machine != 'wasm32'" },
{ name = "requests" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/10/a0/1abec67127972fabf07bc1b7208324399fb25576ba3a2104738dbd40fc8a/blosc2-3.5.1.tar.gz", hash = "sha256:5d72f7a9a8b3b523c588be9d66e9e7f2463483716c4c01e5056c1f7e37167f85", size = 3653626, upload-time = "2025-07-02T11:47:16.4Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/82/cb/ed9ee34a3835dcdee67927bcdc55ec3e912a9d08500612db05aebb885dd1/blosc2-3.6.1.tar.gz", hash = "sha256:0b6f05311fbee9e9dc23bd7f53a8690af3b60eef640a059f1eb624ca6699cc59", size = 3657993, upload-time = "2025-07-17T16:22:58.999Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/d4/ab/bbc1771dc66815277c3d3e316d0f2864209bc15421608ee13ce955b9de3b/blosc2-3.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:780213c917d9ac28b52a09ac82baa761262e8421688937b67f6516987a96fc58", size = 4012189, upload-time = "2025-07-02T11:47:03.343Z" },
- { url = "https://files.pythonhosted.org/packages/20/84/dafbb363539313c5d5e2f4c082c42a652c5767831e7e1fe77eb509aed73e/blosc2-3.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6e3d1d0885e955e184efae81168448106644d5a0a60c0911b98e94fe9d756fa9", size = 3372462, upload-time = "2025-07-02T11:47:05.053Z" },
- { url = "https://files.pythonhosted.org/packages/07/7f/8e77b054ce2eff7b7c9810378cfcc78f73a4e45c2bf122833395f5dce61d/blosc2-3.5.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d7c23e48414283dcfa5676abc6a4be9fb92a7b372a20f7acf90438abc69a746e", size = 4278243, upload-time = "2025-07-02T11:47:06.757Z" },
- { url = "https://files.pythonhosted.org/packages/a6/fd/df0d0c7aec2d135b9c8df9d3d8136c84d614d04595f051b1f5b8aeab238d/blosc2-3.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5df464652ddcae9ef8d8b3354ad224b1e147187dde5b6470e0e9f95bec328756", size = 4420536, upload-time = "2025-07-02T11:47:07.974Z" },
- { url = "https://files.pythonhosted.org/packages/6b/64/a7497c8ba3aceed462d9cbe8227aeee20fbf77415702a0ced11a03fd0d5a/blosc2-3.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:530965c444186bfb92a4406ab4c2d801939df35a5c58658131000f68ce1a37ec", size = 2212429, upload-time = "2025-07-02T11:47:09.045Z" },
- { url = "https://files.pythonhosted.org/packages/c1/17/38adc448a44c8d0e6f3700de4c072d5cbf3cdd7dffdfe62ccf179c985641/blosc2-3.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:44b4e51fd0b5a7e6ccae8053e133037f5984f6990a1e9b787376f4ec42aaf5bf", size = 4010715, upload-time = "2025-07-02T11:47:10.49Z" },
- { url = "https://files.pythonhosted.org/packages/0c/3b/bd2803d030c204984f8b14ce9919bcb67b6582141898a28887ae1fa58d80/blosc2-3.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b59805aa56ff1f9e6e4ba2f293e54cdf356b64db983bb019dbdbac72f88f2e38", size = 3371665, upload-time = "2025-07-02T11:47:11.651Z" },
- { url = "https://files.pythonhosted.org/packages/2b/c8/b86f3ecfecff76cab9c3c1f54fcbb05ab654b6d0a940b9b614b896f8105f/blosc2-3.5.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:485ca015db7352fa0e3b78b912c66ec495d60b94cab36a82abfb9f87715e6abd", size = 4280046, upload-time = "2025-07-02T11:47:12.913Z" },
- { url = "https://files.pythonhosted.org/packages/c9/38/8815054e054e45fda3c9d1e25ac77e6387d10d8e33c64bcf79f432b3c4fa/blosc2-3.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f88cfa1f1d64d77278c7fae0ab8c42291b7b2e8f36fcee154a0c55388e697fe9", size = 4422773, upload-time = "2025-07-02T11:47:14.184Z" },
- { url = "https://files.pythonhosted.org/packages/b1/f0/55b5090c8fb6e9a12810cf141cd59875718148a78f62a4da818045f7e4b6/blosc2-3.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:9d36c4a8489c0f8719040a7543918e2701821340b8643d508c450d0d012259bf", size = 2213487, upload-time = "2025-07-02T11:47:15.297Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/08/b42e6f3babe94ffc19b84a05039f6e62134bf6426ae3ebbe325c670f482d/blosc2-3.6.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c68aac3dad63ea229ad09ea8a3595129abde493e5df90622ae005457795686a6", size = 4018049, upload-time = "2025-07-17T16:22:43.399Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/30/78649ca5699be9d234f3310ee2d0608d80120cf5c1fc1bdc6d79bb43804b/blosc2-3.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1bde827e1660a6fa9c6974923e56a3bd8db45b0eb90bc87cbb73c5b245ca6ef5", size = 3375727, upload-time = "2025-07-17T16:22:45.278Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/89/26f515c2d1d0fcdb262e640f2f60dafee249d15523d93f6af4358c19ece5/blosc2-3.6.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:89e6e25a2cc1e8ba715bf4bd97bd75b2af9c7209799ffc2c4465acef05d1c8d5", size = 4286933, upload-time = "2025-07-17T16:22:46.774Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/73/d03c34900400d4c8e1bea1c7f8750e17b83f98ac6c940b029e45ee8a9d00/blosc2-3.6.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6f2379d75f1b29727655ff9f9a392431e15e997bb6e927605a83946f293b67c7", size = 4425921, upload-time = "2025-07-17T16:22:48.548Z" },
+ { url = "https://files.pythonhosted.org/packages/48/55/2945d05f88d94ec11e9432fee3014b1cdbd16a13990ab304320c482c37ab/blosc2-3.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:0449067307f707139d57f91675e1d389cdea9d4c527aa443b88dfa18993b88b6", size = 2217651, upload-time = "2025-07-17T16:22:49.873Z" },
+ { url = "https://files.pythonhosted.org/packages/96/6a/cb3c693bd13050d9f68e180e9c5f2fa22060c1fcd04164eae4dd6a97c831/blosc2-3.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:47c4b5878795a4bd63f1c93c2bf286939a216e740227bcb18708654196972346", size = 4016932, upload-time = "2025-07-17T16:22:51.212Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/a8/0ba60e4810af3d9daee1cc7f8b2a5f93da6b76e65e3e195b0a34a576bf06/blosc2-3.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c32b8ec2f878e77476c457cc57af57cb66e87a026850378d16659f543e1db2a", size = 3374697, upload-time = "2025-07-17T16:22:52.923Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/2b/6df9bf29d698dab1f6ee63e96bcf689546e6875af3d0431b90ad2b491888/blosc2-3.6.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9fc209348cbbedce1779ea4d7ce91b349e9298bfd32b92c274c3b5eb444dc206", size = 4287893, upload-time = "2025-07-17T16:22:54.345Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/a6/6af387f01b3442e5c14f02cd05ce67e0232984cb4f34dab31e6e319c3ad8/blosc2-3.6.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2332c14034a9f9f5739ec976af24f208677fe964fe1a196c9ae7603ba80ed886", size = 4426379, upload-time = "2025-07-17T16:22:55.692Z" },
+ { url = "https://files.pythonhosted.org/packages/87/64/34c1e5c3cd4ada2bebc13880715647cab660f8db85a57210dc4932021167/blosc2-3.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:e440a600017592e37747f48592bfbc74baa848a74cf41513adf53287fd213015", size = 2218905, upload-time = "2025-07-17T16:22:57.169Z" },
]
[[package]]
@@ -195,11 +195,11 @@ wheels = [
[[package]]
name = "certifi"
-version = "2025.6.15"
+version = "2025.8.3"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/73/f7/f14b46d4bcd21092d7d3ccef689615220d8a08fb25e564b65d20738e672e/certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b", size = 158753, upload-time = "2025-06-15T02:45:51.329Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/84/ae/320161bd181fc06471eed047ecce67b693fd7515b16d495d8932db763426/certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057", size = 157650, upload-time = "2025-06-15T02:45:49.977Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" },
]
[[package]]
@@ -248,43 +248,68 @@ wheels = [
[[package]]
name = "contourpy"
-version = "1.3.2"
+version = "1.3.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/34/f7/44785876384eff370c251d58fd65f6ad7f39adce4a093c934d4a67a7c6b6/contourpy-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2", size = 271580, upload-time = "2025-04-15T17:37:03.105Z" },
- { url = "https://files.pythonhosted.org/packages/93/3b/0004767622a9826ea3d95f0e9d98cd8729015768075d61f9fea8eeca42a8/contourpy-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15", size = 255530, upload-time = "2025-04-15T17:37:07.026Z" },
- { url = "https://files.pythonhosted.org/packages/e7/bb/7bd49e1f4fa805772d9fd130e0d375554ebc771ed7172f48dfcd4ca61549/contourpy-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92", size = 307688, upload-time = "2025-04-15T17:37:11.481Z" },
- { url = "https://files.pythonhosted.org/packages/fc/97/e1d5dbbfa170725ef78357a9a0edc996b09ae4af170927ba8ce977e60a5f/contourpy-1.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87", size = 347331, upload-time = "2025-04-15T17:37:18.212Z" },
- { url = "https://files.pythonhosted.org/packages/6f/66/e69e6e904f5ecf6901be3dd16e7e54d41b6ec6ae3405a535286d4418ffb4/contourpy-1.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415", size = 318963, upload-time = "2025-04-15T17:37:22.76Z" },
- { url = "https://files.pythonhosted.org/packages/a8/32/b8a1c8965e4f72482ff2d1ac2cd670ce0b542f203c8e1d34e7c3e6925da7/contourpy-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe", size = 323681, upload-time = "2025-04-15T17:37:33.001Z" },
- { url = "https://files.pythonhosted.org/packages/30/c6/12a7e6811d08757c7162a541ca4c5c6a34c0f4e98ef2b338791093518e40/contourpy-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441", size = 1308674, upload-time = "2025-04-15T17:37:48.64Z" },
- { url = "https://files.pythonhosted.org/packages/2a/8a/bebe5a3f68b484d3a2b8ffaf84704b3e343ef1addea528132ef148e22b3b/contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e", size = 1380480, upload-time = "2025-04-15T17:38:06.7Z" },
- { url = "https://files.pythonhosted.org/packages/34/db/fcd325f19b5978fb509a7d55e06d99f5f856294c1991097534360b307cf1/contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912", size = 178489, upload-time = "2025-04-15T17:38:10.338Z" },
- { url = "https://files.pythonhosted.org/packages/01/c8/fadd0b92ffa7b5eb5949bf340a63a4a496a6930a6c37a7ba0f12acb076d6/contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73", size = 223042, upload-time = "2025-04-15T17:38:14.239Z" },
- { url = "https://files.pythonhosted.org/packages/2e/61/5673f7e364b31e4e7ef6f61a4b5121c5f170f941895912f773d95270f3a2/contourpy-1.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb", size = 271630, upload-time = "2025-04-15T17:38:19.142Z" },
- { url = "https://files.pythonhosted.org/packages/ff/66/a40badddd1223822c95798c55292844b7e871e50f6bfd9f158cb25e0bd39/contourpy-1.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08", size = 255670, upload-time = "2025-04-15T17:38:23.688Z" },
- { url = "https://files.pythonhosted.org/packages/1e/c7/cf9fdee8200805c9bc3b148f49cb9482a4e3ea2719e772602a425c9b09f8/contourpy-1.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c", size = 306694, upload-time = "2025-04-15T17:38:28.238Z" },
- { url = "https://files.pythonhosted.org/packages/dd/e7/ccb9bec80e1ba121efbffad7f38021021cda5be87532ec16fd96533bb2e0/contourpy-1.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f", size = 345986, upload-time = "2025-04-15T17:38:33.502Z" },
- { url = "https://files.pythonhosted.org/packages/dc/49/ca13bb2da90391fa4219fdb23b078d6065ada886658ac7818e5441448b78/contourpy-1.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85", size = 318060, upload-time = "2025-04-15T17:38:38.672Z" },
- { url = "https://files.pythonhosted.org/packages/c8/65/5245ce8c548a8422236c13ffcdcdada6a2a812c361e9e0c70548bb40b661/contourpy-1.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841", size = 322747, upload-time = "2025-04-15T17:38:43.712Z" },
- { url = "https://files.pythonhosted.org/packages/72/30/669b8eb48e0a01c660ead3752a25b44fdb2e5ebc13a55782f639170772f9/contourpy-1.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422", size = 1308895, upload-time = "2025-04-15T17:39:00.224Z" },
- { url = "https://files.pythonhosted.org/packages/05/5a/b569f4250decee6e8d54498be7bdf29021a4c256e77fe8138c8319ef8eb3/contourpy-1.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef", size = 1379098, upload-time = "2025-04-15T17:43:29.649Z" },
- { url = "https://files.pythonhosted.org/packages/19/ba/b227c3886d120e60e41b28740ac3617b2f2b971b9f601c835661194579f1/contourpy-1.3.2-cp313-cp313-win32.whl", hash = "sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f", size = 178535, upload-time = "2025-04-15T17:44:44.532Z" },
- { url = "https://files.pythonhosted.org/packages/12/6e/2fed56cd47ca739b43e892707ae9a13790a486a3173be063681ca67d2262/contourpy-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9", size = 223096, upload-time = "2025-04-15T17:44:48.194Z" },
- { url = "https://files.pythonhosted.org/packages/54/4c/e76fe2a03014a7c767d79ea35c86a747e9325537a8b7627e0e5b3ba266b4/contourpy-1.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f", size = 285090, upload-time = "2025-04-15T17:43:34.084Z" },
- { url = "https://files.pythonhosted.org/packages/7b/e2/5aba47debd55d668e00baf9651b721e7733975dc9fc27264a62b0dd26eb8/contourpy-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739", size = 268643, upload-time = "2025-04-15T17:43:38.626Z" },
- { url = "https://files.pythonhosted.org/packages/a1/37/cd45f1f051fe6230f751cc5cdd2728bb3a203f5619510ef11e732109593c/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823", size = 310443, upload-time = "2025-04-15T17:43:44.522Z" },
- { url = "https://files.pythonhosted.org/packages/8b/a2/36ea6140c306c9ff6dd38e3bcec80b3b018474ef4d17eb68ceecd26675f4/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5", size = 349865, upload-time = "2025-04-15T17:43:49.545Z" },
- { url = "https://files.pythonhosted.org/packages/95/b7/2fc76bc539693180488f7b6cc518da7acbbb9e3b931fd9280504128bf956/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532", size = 321162, upload-time = "2025-04-15T17:43:54.203Z" },
- { url = "https://files.pythonhosted.org/packages/f4/10/76d4f778458b0aa83f96e59d65ece72a060bacb20cfbee46cf6cd5ceba41/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b", size = 327355, upload-time = "2025-04-15T17:44:01.025Z" },
- { url = "https://files.pythonhosted.org/packages/43/a3/10cf483ea683f9f8ab096c24bad3cce20e0d1dd9a4baa0e2093c1c962d9d/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52", size = 1307935, upload-time = "2025-04-15T17:44:17.322Z" },
- { url = "https://files.pythonhosted.org/packages/78/73/69dd9a024444489e22d86108e7b913f3528f56cfc312b5c5727a44188471/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd", size = 1372168, upload-time = "2025-04-15T17:44:33.43Z" },
- { url = "https://files.pythonhosted.org/packages/0f/1b/96d586ccf1b1a9d2004dd519b25fbf104a11589abfd05484ff12199cca21/contourpy-1.3.2-cp313-cp313t-win32.whl", hash = "sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1", size = 189550, upload-time = "2025-04-15T17:44:37.092Z" },
- { url = "https://files.pythonhosted.org/packages/b0/e6/6000d0094e8a5e32ad62591c8609e269febb6e4db83a1c75ff8868b42731/contourpy-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69", size = 238214, upload-time = "2025-04-15T17:44:40.827Z" },
+sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" },
+ { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" },
+ { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" },
+ { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" },
+ { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" },
+ { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" },
+ { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" },
+ { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" },
+ { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" },
+ { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" },
+ { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" },
+ { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" },
+ { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" },
+ { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" },
+ { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" },
+ { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" },
+ { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" },
+ { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" },
+ { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" },
+ { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" },
+ { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" },
+ { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" },
+ { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" },
+ { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" },
+ { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" },
+ { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" },
]
[[package]]
@@ -341,11 +366,11 @@ wheels = [
[[package]]
name = "docstring-parser"
-version = "0.16"
+version = "0.17.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/08/12/9c22a58c0b1e29271051222d8906257616da84135af9ed167c9e28f85cb3/docstring_parser-0.16.tar.gz", hash = "sha256:538beabd0af1e2db0146b6bd3caa526c35a34d61af9fd2887f3a8a27a739aa6e", size = 26565, upload-time = "2024-03-15T10:39:44.419Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/d5/7c/e9fcff7623954d86bdc17782036cbf715ecab1bec4847c008557affe1ca8/docstring_parser-0.16-py3-none-any.whl", hash = "sha256:bf0a1387354d3691d102edef7ec124f219ef639982d096e26e3b60aeffa90637", size = 36533, upload-time = "2024-03-15T10:39:41.527Z" },
+ { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" },
]
[[package]]
@@ -410,27 +435,27 @@ wheels = [
[[package]]
name = "fonttools"
-version = "4.58.4"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/2e/5a/1124b2c8cb3a8015faf552e92714040bcdbc145dfa29928891b02d147a18/fonttools-4.58.4.tar.gz", hash = "sha256:928a8009b9884ed3aae17724b960987575155ca23c6f0b8146e400cc9e0d44ba", size = 3525026, upload-time = "2025-06-13T17:25:15.426Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/04/3c/1d1792bfe91ef46f22a3d23b4deb514c325e73c17d4f196b385b5e2faf1c/fonttools-4.58.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:462211c0f37a278494e74267a994f6be9a2023d0557aaa9ecbcbfce0f403b5a6", size = 2754082, upload-time = "2025-06-13T17:24:24.862Z" },
- { url = "https://files.pythonhosted.org/packages/2a/1f/2b261689c901a1c3bc57a6690b0b9fc21a9a93a8b0c83aae911d3149f34e/fonttools-4.58.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0c7a12fb6f769165547f00fcaa8d0df9517603ae7e04b625e5acb8639809b82d", size = 2321677, upload-time = "2025-06-13T17:24:26.815Z" },
- { url = "https://files.pythonhosted.org/packages/fe/6b/4607add1755a1e6581ae1fc0c9a640648e0d9cdd6591cc2d581c2e07b8c3/fonttools-4.58.4-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2d42c63020a922154add0a326388a60a55504629edc3274bc273cd3806b4659f", size = 4896354, upload-time = "2025-06-13T17:24:28.428Z" },
- { url = "https://files.pythonhosted.org/packages/cd/95/34b4f483643d0cb11a1f830b72c03fdd18dbd3792d77a2eb2e130a96fada/fonttools-4.58.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f2b4e6fd45edc6805f5f2c355590b092ffc7e10a945bd6a569fc66c1d2ae7aa", size = 4941633, upload-time = "2025-06-13T17:24:30.568Z" },
- { url = "https://files.pythonhosted.org/packages/81/ac/9bafbdb7694059c960de523e643fa5a61dd2f698f3f72c0ca18ae99257c7/fonttools-4.58.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f155b927f6efb1213a79334e4cb9904d1e18973376ffc17a0d7cd43d31981f1e", size = 4886170, upload-time = "2025-06-13T17:24:32.724Z" },
- { url = "https://files.pythonhosted.org/packages/ae/44/a3a3b70d5709405f7525bb7cb497b4e46151e0c02e3c8a0e40e5e9fe030b/fonttools-4.58.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e38f687d5de97c7fb7da3e58169fb5ba349e464e141f83c3c2e2beb91d317816", size = 5037851, upload-time = "2025-06-13T17:24:35.034Z" },
- { url = "https://files.pythonhosted.org/packages/21/cb/e8923d197c78969454eb876a4a55a07b59c9c4c46598f02b02411dc3b45c/fonttools-4.58.4-cp312-cp312-win32.whl", hash = "sha256:636c073b4da9db053aa683db99580cac0f7c213a953b678f69acbca3443c12cc", size = 2187428, upload-time = "2025-06-13T17:24:36.996Z" },
- { url = "https://files.pythonhosted.org/packages/46/e6/fe50183b1a0e1018e7487ee740fa8bb127b9f5075a41e20d017201e8ab14/fonttools-4.58.4-cp312-cp312-win_amd64.whl", hash = "sha256:82e8470535743409b30913ba2822e20077acf9ea70acec40b10fcf5671dceb58", size = 2236649, upload-time = "2025-06-13T17:24:38.985Z" },
- { url = "https://files.pythonhosted.org/packages/d4/4f/c05cab5fc1a4293e6bc535c6cb272607155a0517700f5418a4165b7f9ec8/fonttools-4.58.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5f4a64846495c543796fa59b90b7a7a9dff6839bd852741ab35a71994d685c6d", size = 2745197, upload-time = "2025-06-13T17:24:40.645Z" },
- { url = "https://files.pythonhosted.org/packages/3e/d3/49211b1f96ae49308f4f78ca7664742377a6867f00f704cdb31b57e4b432/fonttools-4.58.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e80661793a5d4d7ad132a2aa1eae2e160fbdbb50831a0edf37c7c63b2ed36574", size = 2317272, upload-time = "2025-06-13T17:24:43.428Z" },
- { url = "https://files.pythonhosted.org/packages/b2/11/c9972e46a6abd752a40a46960e431c795ad1f306775fc1f9e8c3081a1274/fonttools-4.58.4-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fe5807fc64e4ba5130f1974c045a6e8d795f3b7fb6debfa511d1773290dbb76b", size = 4877184, upload-time = "2025-06-13T17:24:45.527Z" },
- { url = "https://files.pythonhosted.org/packages/ea/24/5017c01c9ef8df572cc9eaf9f12be83ad8ed722ff6dc67991d3d752956e4/fonttools-4.58.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b610b9bef841cb8f4b50472494158b1e347d15cad56eac414c722eda695a6cfd", size = 4939445, upload-time = "2025-06-13T17:24:47.647Z" },
- { url = "https://files.pythonhosted.org/packages/79/b0/538cc4d0284b5a8826b4abed93a69db52e358525d4b55c47c8cef3669767/fonttools-4.58.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2daa7f0e213c38f05f054eb5e1730bd0424aebddbeac094489ea1585807dd187", size = 4878800, upload-time = "2025-06-13T17:24:49.766Z" },
- { url = "https://files.pythonhosted.org/packages/5a/9b/a891446b7a8250e65bffceb248508587958a94db467ffd33972723ab86c9/fonttools-4.58.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:66cccb6c0b944496b7f26450e9a66e997739c513ffaac728d24930df2fd9d35b", size = 5021259, upload-time = "2025-06-13T17:24:51.754Z" },
- { url = "https://files.pythonhosted.org/packages/17/b2/c4d2872cff3ace3ddd1388bf15b76a1d8d5313f0a61f234e9aed287e674d/fonttools-4.58.4-cp313-cp313-win32.whl", hash = "sha256:94d2aebb5ca59a5107825520fde596e344652c1f18170ef01dacbe48fa60c889", size = 2185824, upload-time = "2025-06-13T17:24:54.324Z" },
- { url = "https://files.pythonhosted.org/packages/98/57/cddf8bcc911d4f47dfca1956c1e3aeeb9f7c9b8e88b2a312fe8c22714e0b/fonttools-4.58.4-cp313-cp313-win_amd64.whl", hash = "sha256:b554bd6e80bba582fd326ddab296e563c20c64dca816d5e30489760e0c41529f", size = 2236382, upload-time = "2025-06-13T17:24:56.291Z" },
- { url = "https://files.pythonhosted.org/packages/0b/2f/c536b5b9bb3c071e91d536a4d11f969e911dbb6b227939f4c5b0bca090df/fonttools-4.58.4-py3-none-any.whl", hash = "sha256:a10ce13a13f26cbb9f37512a4346bb437ad7e002ff6fa966a7ce7ff5ac3528bd", size = 1114660, upload-time = "2025-06-13T17:25:13.321Z" },
+version = "4.59.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/8a/27/ec3c723bfdf86f34c5c82bf6305df3e0f0d8ea798d2d3a7cb0c0a866d286/fonttools-4.59.0.tar.gz", hash = "sha256:be392ec3529e2f57faa28709d60723a763904f71a2b63aabe14fee6648fe3b14", size = 3532521, upload-time = "2025-07-16T12:04:54.613Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e2/77/b1c8af22f4265e951cd2e5535dbef8859efcef4fb8dee742d368c967cddb/fonttools-4.59.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f9b3a78f69dcbd803cf2fb3f972779875b244c1115481dfbdd567b2c22b31f6b", size = 2767562, upload-time = "2025-07-16T12:04:06.895Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/5a/aeb975699588176bb357e8b398dfd27e5d3a2230d92b81ab8cbb6187358d/fonttools-4.59.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:57bb7e26928573ee7c6504f54c05860d867fd35e675769f3ce01b52af38d48e2", size = 2335168, upload-time = "2025-07-16T12:04:08.695Z" },
+ { url = "https://files.pythonhosted.org/packages/54/97/c6101a7e60ae138c4ef75b22434373a0da50a707dad523dd19a4889315bf/fonttools-4.59.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4536f2695fe5c1ffb528d84a35a7d3967e5558d2af58b4775e7ab1449d65767b", size = 4909850, upload-time = "2025-07-16T12:04:10.761Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/6c/fa4d18d641054f7bff878cbea14aa9433f292b9057cb1700d8e91a4d5f4f/fonttools-4.59.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:885bde7d26e5b40e15c47bd5def48b38cbd50830a65f98122a8fb90962af7cd1", size = 4955131, upload-time = "2025-07-16T12:04:12.846Z" },
+ { url = "https://files.pythonhosted.org/packages/20/5c/331947fc1377deb928a69bde49f9003364f5115e5cbe351eea99e39412a2/fonttools-4.59.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6801aeddb6acb2c42eafa45bc1cb98ba236871ae6f33f31e984670b749a8e58e", size = 4899667, upload-time = "2025-07-16T12:04:14.558Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/46/b66469dfa26b8ff0baa7654b2cc7851206c6d57fe3abdabbaab22079a119/fonttools-4.59.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:31003b6a10f70742a63126b80863ab48175fb8272a18ca0846c0482968f0588e", size = 5051349, upload-time = "2025-07-16T12:04:16.388Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/05/ebfb6b1f3a4328ab69787d106a7d92ccde77ce66e98659df0f9e3f28d93d/fonttools-4.59.0-cp312-cp312-win32.whl", hash = "sha256:fbce6dae41b692a5973d0f2158f782b9ad05babc2c2019a970a1094a23909b1b", size = 2201315, upload-time = "2025-07-16T12:04:18.557Z" },
+ { url = "https://files.pythonhosted.org/packages/09/45/d2bdc9ea20bbadec1016fd0db45696d573d7a26d95ab5174ffcb6d74340b/fonttools-4.59.0-cp312-cp312-win_amd64.whl", hash = "sha256:332bfe685d1ac58ca8d62b8d6c71c2e52a6c64bc218dc8f7825c9ea51385aa01", size = 2249408, upload-time = "2025-07-16T12:04:20.489Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/bb/390990e7c457d377b00890d9f96a3ca13ae2517efafb6609c1756e213ba4/fonttools-4.59.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:78813b49d749e1bb4db1c57f2d4d7e6db22c253cb0a86ad819f5dc197710d4b2", size = 2758704, upload-time = "2025-07-16T12:04:22.217Z" },
+ { url = "https://files.pythonhosted.org/packages/df/6f/d730d9fcc9b410a11597092bd2eb9ca53e5438c6cb90e4b3047ce1b723e9/fonttools-4.59.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:401b1941ce37e78b8fd119b419b617277c65ae9417742a63282257434fd68ea2", size = 2330764, upload-time = "2025-07-16T12:04:23.985Z" },
+ { url = "https://files.pythonhosted.org/packages/75/b4/b96bb66f6f8cc4669de44a158099b249c8159231d254ab6b092909388be5/fonttools-4.59.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:efd7e6660674e234e29937bc1481dceb7e0336bfae75b856b4fb272b5093c5d4", size = 4890699, upload-time = "2025-07-16T12:04:25.664Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/57/7969af50b26408be12baa317c6147588db5b38af2759e6df94554dbc5fdb/fonttools-4.59.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51ab1ff33c19e336c02dee1e9fd1abd974a4ca3d8f7eef2a104d0816a241ce97", size = 4952934, upload-time = "2025-07-16T12:04:27.733Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/e2/dd968053b6cf1f46c904f5bd409b22341477c017d8201619a265e50762d3/fonttools-4.59.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a9bf8adc9e1f3012edc8f09b08336272aec0c55bc677422273e21280db748f7c", size = 4892319, upload-time = "2025-07-16T12:04:30.074Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/95/a59810d8eda09129f83467a4e58f84205dc6994ebaeb9815406363e07250/fonttools-4.59.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:37e01c6ec0c98599778c2e688350d624fa4770fbd6144551bd5e032f1199171c", size = 5034753, upload-time = "2025-07-16T12:04:32.292Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/84/51a69ee89ff8d1fea0c6997e946657e25a3f08513de8435fe124929f3eef/fonttools-4.59.0-cp313-cp313-win32.whl", hash = "sha256:70d6b3ceaa9cc5a6ac52884f3b3d9544e8e231e95b23f138bdb78e6d4dc0eae3", size = 2199688, upload-time = "2025-07-16T12:04:34.444Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/ee/f626cd372932d828508137a79b85167fdcf3adab2e3bed433f295c596c6a/fonttools-4.59.0-cp313-cp313-win_amd64.whl", hash = "sha256:26731739daa23b872643f0e4072d5939960237d540c35c14e6a06d47d71ca8fe", size = 2248560, upload-time = "2025-07-16T12:04:36.034Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/9c/df0ef2c51845a13043e5088f7bb988ca6cd5bb82d5d4203d6a158aa58cf2/fonttools-4.59.0-py3-none-any.whl", hash = "sha256:241313683afd3baacb32a6bd124d0bce7404bc5280e12e291bae1b9bba28711d", size = 1128050, upload-time = "2025-07-16T12:04:52.687Z" },
]
[[package]]
@@ -724,7 +749,7 @@ wheels = [
[[package]]
name = "matplotlib"
-version = "3.10.3"
+version = "3.10.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "contourpy" },
@@ -737,26 +762,43 @@ dependencies = [
{ name = "pyparsing" },
{ name = "python-dateutil" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/26/91/d49359a21893183ed2a5b6c76bec40e0b1dcbf8ca148f864d134897cfc75/matplotlib-3.10.3.tar.gz", hash = "sha256:2f82d2c5bb7ae93aaaa4cd42aca65d76ce6376f83304fa3a630b569aca274df0", size = 34799811, upload-time = "2025-05-08T19:10:54.39Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/eb/43/6b80eb47d1071f234ef0c96ca370c2ca621f91c12045f1401b5c9b28a639/matplotlib-3.10.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0ab1affc11d1f495ab9e6362b8174a25afc19c081ba5b0775ef00533a4236eea", size = 8179689, upload-time = "2025-05-08T19:10:07.602Z" },
- { url = "https://files.pythonhosted.org/packages/0f/70/d61a591958325c357204870b5e7b164f93f2a8cca1dc6ce940f563909a13/matplotlib-3.10.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2a818d8bdcafa7ed2eed74487fdb071c09c1ae24152d403952adad11fa3c65b4", size = 8050466, upload-time = "2025-05-08T19:10:09.383Z" },
- { url = "https://files.pythonhosted.org/packages/e7/75/70c9d2306203148cc7902a961240c5927dd8728afedf35e6a77e105a2985/matplotlib-3.10.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:748ebc3470c253e770b17d8b0557f0aa85cf8c63fd52f1a61af5b27ec0b7ffee", size = 8456252, upload-time = "2025-05-08T19:10:11.958Z" },
- { url = "https://files.pythonhosted.org/packages/c4/91/ba0ae1ff4b3f30972ad01cd4a8029e70a0ec3b8ea5be04764b128b66f763/matplotlib-3.10.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed70453fd99733293ace1aec568255bc51c6361cb0da94fa5ebf0649fdb2150a", size = 8601321, upload-time = "2025-05-08T19:10:14.47Z" },
- { url = "https://files.pythonhosted.org/packages/d2/88/d636041eb54a84b889e11872d91f7cbf036b3b0e194a70fa064eb8b04f7a/matplotlib-3.10.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dbed9917b44070e55640bd13419de83b4c918e52d97561544814ba463811cbc7", size = 9406972, upload-time = "2025-05-08T19:10:16.569Z" },
- { url = "https://files.pythonhosted.org/packages/b1/79/0d1c165eac44405a86478082e225fce87874f7198300bbebc55faaf6d28d/matplotlib-3.10.3-cp312-cp312-win_amd64.whl", hash = "sha256:cf37d8c6ef1a48829443e8ba5227b44236d7fcaf7647caa3178a4ff9f7a5be05", size = 8067954, upload-time = "2025-05-08T19:10:18.663Z" },
- { url = "https://files.pythonhosted.org/packages/3b/c1/23cfb566a74c696a3b338d8955c549900d18fe2b898b6e94d682ca21e7c2/matplotlib-3.10.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9f2efccc8dcf2b86fc4ee849eea5dcaecedd0773b30f47980dc0cbeabf26ec84", size = 8180318, upload-time = "2025-05-08T19:10:20.426Z" },
- { url = "https://files.pythonhosted.org/packages/6c/0c/02f1c3b66b30da9ee343c343acbb6251bef5b01d34fad732446eaadcd108/matplotlib-3.10.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3ddbba06a6c126e3301c3d272a99dcbe7f6c24c14024e80307ff03791a5f294e", size = 8051132, upload-time = "2025-05-08T19:10:22.569Z" },
- { url = "https://files.pythonhosted.org/packages/b4/ab/8db1a5ac9b3a7352fb914133001dae889f9fcecb3146541be46bed41339c/matplotlib-3.10.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:748302b33ae9326995b238f606e9ed840bf5886ebafcb233775d946aa8107a15", size = 8457633, upload-time = "2025-05-08T19:10:24.749Z" },
- { url = "https://files.pythonhosted.org/packages/f5/64/41c4367bcaecbc03ef0d2a3ecee58a7065d0a36ae1aa817fe573a2da66d4/matplotlib-3.10.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a80fcccbef63302c0efd78042ea3c2436104c5b1a4d3ae20f864593696364ac7", size = 8601031, upload-time = "2025-05-08T19:10:27.03Z" },
- { url = "https://files.pythonhosted.org/packages/12/6f/6cc79e9e5ab89d13ed64da28898e40fe5b105a9ab9c98f83abd24e46d7d7/matplotlib-3.10.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:55e46cbfe1f8586adb34f7587c3e4f7dedc59d5226719faf6cb54fc24f2fd52d", size = 9406988, upload-time = "2025-05-08T19:10:29.056Z" },
- { url = "https://files.pythonhosted.org/packages/b1/0f/eed564407bd4d935ffabf561ed31099ed609e19287409a27b6d336848653/matplotlib-3.10.3-cp313-cp313-win_amd64.whl", hash = "sha256:151d89cb8d33cb23345cd12490c76fd5d18a56581a16d950b48c6ff19bb2ab93", size = 8068034, upload-time = "2025-05-08T19:10:31.221Z" },
- { url = "https://files.pythonhosted.org/packages/3e/e5/2f14791ff69b12b09e9975e1d116d9578ac684460860ce542c2588cb7a1c/matplotlib-3.10.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c26dd9834e74d164d06433dc7be5d75a1e9890b926b3e57e74fa446e1a62c3e2", size = 8218223, upload-time = "2025-05-08T19:10:33.114Z" },
- { url = "https://files.pythonhosted.org/packages/5c/08/30a94afd828b6e02d0a52cae4a29d6e9ccfcf4c8b56cc28b021d3588873e/matplotlib-3.10.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:24853dad5b8c84c8c2390fc31ce4858b6df504156893292ce8092d190ef8151d", size = 8094985, upload-time = "2025-05-08T19:10:35.337Z" },
- { url = "https://files.pythonhosted.org/packages/89/44/f3bc6b53066c889d7a1a3ea8094c13af6a667c5ca6220ec60ecceec2dabe/matplotlib-3.10.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68f7878214d369d7d4215e2a9075fef743be38fa401d32e6020bab2dfabaa566", size = 8483109, upload-time = "2025-05-08T19:10:37.611Z" },
- { url = "https://files.pythonhosted.org/packages/ba/c7/473bc559beec08ebee9f86ca77a844b65747e1a6c2691e8c92e40b9f42a8/matplotlib-3.10.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6929fc618cb6db9cb75086f73b3219bbb25920cb24cee2ea7a12b04971a4158", size = 8618082, upload-time = "2025-05-08T19:10:39.892Z" },
- { url = "https://files.pythonhosted.org/packages/d8/e9/6ce8edd264c8819e37bbed8172e0ccdc7107fe86999b76ab5752276357a4/matplotlib-3.10.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6c7818292a5cc372a2dc4c795e5c356942eb8350b98ef913f7fda51fe175ac5d", size = 9413699, upload-time = "2025-05-08T19:10:42.376Z" },
- { url = "https://files.pythonhosted.org/packages/1b/92/9a45c91089c3cf690b5badd4be81e392ff086ccca8a1d4e3a08463d8a966/matplotlib-3.10.3-cp313-cp313t-win_amd64.whl", hash = "sha256:4f23ffe95c5667ef8a2b56eea9b53db7f43910fa4a2d5472ae0f72b64deab4d5", size = 8139044, upload-time = "2025-05-08T19:10:44.551Z" },
+sdist = { url = "https://files.pythonhosted.org/packages/43/91/f2939bb60b7ebf12478b030e0d7f340247390f402b3b189616aad790c366/matplotlib-3.10.5.tar.gz", hash = "sha256:352ed6ccfb7998a00881692f38b4ca083c691d3e275b4145423704c34c909076", size = 34804044, upload-time = "2025-07-31T18:09:33.805Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/66/1e/c6f6bcd882d589410b475ca1fc22e34e34c82adff519caf18f3e6dd9d682/matplotlib-3.10.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:00b6feadc28a08bd3c65b2894f56cf3c94fc8f7adcbc6ab4516ae1e8ed8f62e2", size = 8253056, upload-time = "2025-07-31T18:08:05.385Z" },
+ { url = "https://files.pythonhosted.org/packages/53/e6/d6f7d1b59413f233793dda14419776f5f443bcccb2dfc84b09f09fe05dbe/matplotlib-3.10.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee98a5c5344dc7f48dc261b6ba5d9900c008fc12beb3fa6ebda81273602cc389", size = 8110131, upload-time = "2025-07-31T18:08:07.293Z" },
+ { url = "https://files.pythonhosted.org/packages/66/2b/bed8a45e74957549197a2ac2e1259671cd80b55ed9e1fe2b5c94d88a9202/matplotlib-3.10.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a17e57e33de901d221a07af32c08870ed4528db0b6059dce7d7e65c1122d4bea", size = 8669603, upload-time = "2025-07-31T18:08:09.064Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/a7/315e9435b10d057f5e52dfc603cd353167ae28bb1a4e033d41540c0067a4/matplotlib-3.10.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97b9d6443419085950ee4a5b1ee08c363e5c43d7176e55513479e53669e88468", size = 9508127, upload-time = "2025-07-31T18:08:10.845Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/d9/edcbb1f02ca99165365d2768d517898c22c6040187e2ae2ce7294437c413/matplotlib-3.10.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ceefe5d40807d29a66ae916c6a3915d60ef9f028ce1927b84e727be91d884369", size = 9566926, upload-time = "2025-07-31T18:08:13.186Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/d9/6dd924ad5616c97b7308e6320cf392c466237a82a2040381163b7500510a/matplotlib-3.10.5-cp312-cp312-win_amd64.whl", hash = "sha256:c04cba0f93d40e45b3c187c6c52c17f24535b27d545f757a2fffebc06c12b98b", size = 8107599, upload-time = "2025-07-31T18:08:15.116Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/f3/522dc319a50f7b0279fbe74f86f7a3506ce414bc23172098e8d2bdf21894/matplotlib-3.10.5-cp312-cp312-win_arm64.whl", hash = "sha256:a41bcb6e2c8e79dc99c5511ae6f7787d2fb52efd3d805fff06d5d4f667db16b2", size = 7978173, upload-time = "2025-07-31T18:08:21.518Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/05/4f3c1f396075f108515e45cb8d334aff011a922350e502a7472e24c52d77/matplotlib-3.10.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:354204db3f7d5caaa10e5de74549ef6a05a4550fdd1c8f831ab9bca81efd39ed", size = 8253586, upload-time = "2025-07-31T18:08:23.107Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/2c/e084415775aac7016c3719fe7006cdb462582c6c99ac142f27303c56e243/matplotlib-3.10.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b072aac0c3ad563a2b3318124756cb6112157017f7431626600ecbe890df57a1", size = 8110715, upload-time = "2025-07-31T18:08:24.675Z" },
+ { url = "https://files.pythonhosted.org/packages/52/1b/233e3094b749df16e3e6cd5a44849fd33852e692ad009cf7de00cf58ddf6/matplotlib-3.10.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d52fd5b684d541b5a51fb276b2b97b010c75bee9aa392f96b4a07aeb491e33c7", size = 8669397, upload-time = "2025-07-31T18:08:26.778Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/ec/03f9e003a798f907d9f772eed9b7c6a9775d5bd00648b643ebfb88e25414/matplotlib-3.10.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee7a09ae2f4676276f5a65bd9f2bd91b4f9fbaedf49f40267ce3f9b448de501f", size = 9508646, upload-time = "2025-07-31T18:08:28.848Z" },
+ { url = "https://files.pythonhosted.org/packages/91/e7/c051a7a386680c28487bca27d23b02d84f63e3d2a9b4d2fc478e6a42e37e/matplotlib-3.10.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ba6c3c9c067b83481d647af88b4e441d532acdb5ef22178a14935b0b881188f4", size = 9567424, upload-time = "2025-07-31T18:08:30.726Z" },
+ { url = "https://files.pythonhosted.org/packages/36/c2/24302e93ff431b8f4173ee1dd88976c8d80483cadbc5d3d777cef47b3a1c/matplotlib-3.10.5-cp313-cp313-win_amd64.whl", hash = "sha256:07442d2692c9bd1cceaa4afb4bbe5b57b98a7599de4dabfcca92d3eea70f9ebe", size = 8107809, upload-time = "2025-07-31T18:08:33.928Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/33/423ec6a668d375dad825197557ed8fbdb74d62b432c1ed8235465945475f/matplotlib-3.10.5-cp313-cp313-win_arm64.whl", hash = "sha256:48fe6d47380b68a37ccfcc94f009530e84d41f71f5dae7eda7c4a5a84aa0a674", size = 7978078, upload-time = "2025-07-31T18:08:36.764Z" },
+ { url = "https://files.pythonhosted.org/packages/51/17/521fc16ec766455c7bb52cc046550cf7652f6765ca8650ff120aa2d197b6/matplotlib-3.10.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b80eb8621331449fc519541a7461987f10afa4f9cfd91afcd2276ebe19bd56c", size = 8295590, upload-time = "2025-07-31T18:08:38.521Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/12/23c28b2c21114c63999bae129fce7fd34515641c517ae48ce7b7dcd33458/matplotlib-3.10.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:47a388908e469d6ca2a6015858fa924e0e8a2345a37125948d8e93a91c47933e", size = 8158518, upload-time = "2025-07-31T18:08:40.195Z" },
+ { url = "https://files.pythonhosted.org/packages/81/f8/aae4eb25e8e7190759f3cb91cbeaa344128159ac92bb6b409e24f8711f78/matplotlib-3.10.5-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8b6b49167d208358983ce26e43aa4196073b4702858670f2eb111f9a10652b4b", size = 8691815, upload-time = "2025-07-31T18:08:42.238Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/ba/450c39ebdd486bd33a359fc17365ade46c6a96bf637bbb0df7824de2886c/matplotlib-3.10.5-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a8da0453a7fd8e3da114234ba70c5ba9ef0e98f190309ddfde0f089accd46ea", size = 9522814, upload-time = "2025-07-31T18:08:44.914Z" },
+ { url = "https://files.pythonhosted.org/packages/89/11/9c66f6a990e27bb9aa023f7988d2d5809cb98aa39c09cbf20fba75a542ef/matplotlib-3.10.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:52c6573dfcb7726a9907b482cd5b92e6b5499b284ffacb04ffbfe06b3e568124", size = 9573917, upload-time = "2025-07-31T18:08:47.038Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/69/8b49394de92569419e5e05e82e83df9b749a0ff550d07631ea96ed2eb35a/matplotlib-3.10.5-cp313-cp313t-win_amd64.whl", hash = "sha256:a23193db2e9d64ece69cac0c8231849db7dd77ce59c7b89948cf9d0ce655a3ce", size = 8181034, upload-time = "2025-07-31T18:08:48.943Z" },
+ { url = "https://files.pythonhosted.org/packages/47/23/82dc435bb98a2fc5c20dffcac8f0b083935ac28286413ed8835df40d0baa/matplotlib-3.10.5-cp313-cp313t-win_arm64.whl", hash = "sha256:56da3b102cf6da2776fef3e71cd96fcf22103a13594a18ac9a9b31314e0be154", size = 8023337, upload-time = "2025-07-31T18:08:50.791Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/e0/26b6cfde31f5383503ee45dcb7e691d45dadf0b3f54639332b59316a97f8/matplotlib-3.10.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:96ef8f5a3696f20f55597ffa91c28e2e73088df25c555f8d4754931515512715", size = 8253591, upload-time = "2025-07-31T18:08:53.254Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/89/98488c7ef7ea20ea659af7499628c240a608b337af4be2066d644cfd0a0f/matplotlib-3.10.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:77fab633e94b9da60512d4fa0213daeb76d5a7b05156840c4fd0399b4b818837", size = 8112566, upload-time = "2025-07-31T18:08:55.116Z" },
+ { url = "https://files.pythonhosted.org/packages/52/67/42294dfedc82aea55e1a767daf3263aacfb5a125f44ba189e685bab41b6f/matplotlib-3.10.5-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27f52634315e96b1debbfdc5c416592edcd9c4221bc2f520fd39c33db5d9f202", size = 9513281, upload-time = "2025-07-31T18:08:56.885Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/68/f258239e0cf34c2cbc816781c7ab6fca768452e6bf1119aedd2bd4a882a3/matplotlib-3.10.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:525f6e28c485c769d1f07935b660c864de41c37fd716bfa64158ea646f7084bb", size = 9780873, upload-time = "2025-07-31T18:08:59.241Z" },
+ { url = "https://files.pythonhosted.org/packages/89/64/f4881554006bd12e4558bd66778bdd15d47b00a1f6c6e8b50f6208eda4b3/matplotlib-3.10.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1f5f3ec4c191253c5f2b7c07096a142c6a1c024d9f738247bfc8e3f9643fc975", size = 9568954, upload-time = "2025-07-31T18:09:01.244Z" },
+ { url = "https://files.pythonhosted.org/packages/06/f8/42779d39c3f757e1f012f2dda3319a89fb602bd2ef98ce8faf0281f4febd/matplotlib-3.10.5-cp314-cp314-win_amd64.whl", hash = "sha256:707f9c292c4cd4716f19ab8a1f93f26598222cd931e0cd98fbbb1c5994bf7667", size = 8237465, upload-time = "2025-07-31T18:09:03.206Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/f8/153fd06b5160f0cd27c8b9dd797fcc9fb56ac6a0ebf3c1f765b6b68d3c8a/matplotlib-3.10.5-cp314-cp314-win_arm64.whl", hash = "sha256:21a95b9bf408178d372814de7baacd61c712a62cae560b5e6f35d791776f6516", size = 8108898, upload-time = "2025-07-31T18:09:05.231Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/ee/c4b082a382a225fe0d2a73f1f57cf6f6f132308805b493a54c8641006238/matplotlib-3.10.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a6b310f95e1102a8c7c817ef17b60ee5d1851b8c71b63d9286b66b177963039e", size = 8295636, upload-time = "2025-07-31T18:09:07.306Z" },
+ { url = "https://files.pythonhosted.org/packages/30/73/2195fa2099718b21a20da82dfc753bf2af58d596b51aefe93e359dd5915a/matplotlib-3.10.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:94986a242747a0605cb3ff1cb98691c736f28a59f8ffe5175acaeb7397c49a5a", size = 8158575, upload-time = "2025-07-31T18:09:09.083Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/e9/a08cdb34618a91fa08f75e6738541da5cacde7c307cea18ff10f0d03fcff/matplotlib-3.10.5-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ff10ea43288f0c8bab608a305dc6c918cc729d429c31dcbbecde3b9f4d5b569", size = 9522815, upload-time = "2025-07-31T18:09:11.191Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/bb/34d8b7e0d1bb6d06ef45db01dfa560d5a67b1c40c0b998ce9ccde934bb09/matplotlib-3.10.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f6adb644c9d040ffb0d3434e440490a66cf73dbfa118a6f79cd7568431f7a012", size = 9783514, upload-time = "2025-07-31T18:09:13.307Z" },
+ { url = "https://files.pythonhosted.org/packages/12/09/d330d1e55dcca2e11b4d304cc5227f52e2512e46828d6249b88e0694176e/matplotlib-3.10.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4fa40a8f98428f789a9dcacd625f59b7bc4e3ef6c8c7c80187a7a709475cf592", size = 9573932, upload-time = "2025-07-31T18:09:15.335Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/3b/f70258ac729aa004aca673800a53a2b0a26d49ca1df2eaa03289a1c40f81/matplotlib-3.10.5-cp314-cp314t-win_amd64.whl", hash = "sha256:95672a5d628b44207aab91ec20bf59c26da99de12b88f7e0b1fb0a84a86ff959", size = 8322003, upload-time = "2025-07-31T18:09:17.416Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/60/3601f8ce6d76a7c81c7f25a0e15fde0d6b66226dd187aa6d2838e6374161/matplotlib-3.10.5-cp314-cp314t-win_arm64.whl", hash = "sha256:2efaf97d72629e74252e0b5e3c46813e9eeaa94e011ecf8084a971a31a97f40b", size = 8153849, upload-time = "2025-07-31T18:09:19.673Z" },
]
[[package]]
@@ -919,7 +961,7 @@ wheels = [
[[package]]
name = "myogen"
-version = "0.1.0"
+version = "0.2.0"
source = { editable = "." }
dependencies = [
{ name = "beartype" },
@@ -935,6 +977,7 @@ dependencies = [
{ name = "scikit-fmm" },
{ name = "scikit-learn" },
{ name = "scipy" },
+ { name = "scipy-stubs" },
{ name = "seaborn" },
{ name = "tqdm" },
]
@@ -949,6 +992,7 @@ docs = [
{ name = "rinohtype" },
{ name = "sphinx" },
{ name = "sphinx-gallery" },
+ { name = "sphinxcontrib-mermaid" },
{ name = "toml" },
]
@@ -967,6 +1011,7 @@ requires-dist = [
{ name = "scikit-fmm", specifier = ">=2025.1.29" },
{ name = "scikit-learn", specifier = ">=1.6.1" },
{ name = "scipy", specifier = ">=1.15.3" },
+ { name = "scipy-stubs", specifier = "==1.16.1.0" },
{ name = "seaborn", specifier = ">=0.13.2" },
{ name = "tqdm", specifier = ">=4.67.1" },
]
@@ -981,6 +1026,7 @@ docs = [
{ name = "rinohtype", specifier = ">=0.5.5" },
{ name = "sphinx", specifier = ">=8.1.3" },
{ name = "sphinx-gallery", specifier = ">=0.19.0" },
+ { name = "sphinxcontrib-mermaid", specifier = ">=1.0.0" },
{ name = "toml", specifier = ">=0.10.2" },
]
@@ -1042,16 +1088,16 @@ wheels = [
[[package]]
name = "neo"
-version = "0.14.1"
+version = "0.14.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
{ name = "packaging" },
{ name = "quantities" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/f1/81/618538a70b0a7db16a174da9a7b5e8aec65462ebcd8f1120cd41b583a8dd/neo-0.14.1.tar.gz", hash = "sha256:4d89c649948499227de36aa59c8f6565aacf376f1b8a7e4cafe4fb336caa7d20", size = 5062513, upload-time = "2025-04-14T13:17:50.704Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/0e/47/ebc266619ea7f1e8025ba057ab446c14423b1265ec9c6810fa3a3fb02de4/neo-0.14.2.tar.gz", hash = "sha256:76517503e114fffcc38aa3600c04dd8be48b1ee5e2c8162bc0db8fb92476811f", size = 5067423, upload-time = "2025-07-08T13:12:54.508Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/80/01/5104bbfa51b3cddb204888d3f6eba61daf7157f47d051c95fed2a61d0170/neo-0.14.1-py3-none-any.whl", hash = "sha256:a013b3654e22cf70948e15fa0dfcafff05a8ce8b51055d649c257698d13978c7", size = 675571, upload-time = "2025-04-14T13:17:48.901Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/e1/16f4f07a65bb952eb99897a4ad6fedbec9e3d8a394e5baa2bc3a97713e0e/neo-0.14.2-py3-none-any.whl", hash = "sha256:ca4e605ffdba944e53cd035e43c0dbeff45efbb1ed00ae0c61c0d49f08ef2b94", size = 679538, upload-time = "2025-07-08T13:12:52.062Z" },
]
[[package]]
@@ -1185,6 +1231,36 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" },
]
+[[package]]
+name = "numpy-typing-compat"
+version = "2.2.20250730"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c5/0e/639afa67a8f6296ba0f2aa16530faf5c7fc4606bffda612b66aff26d898a/numpy_typing_compat-2.2.20250730.tar.gz", hash = "sha256:f6b9a08193c6291c3d6c4717d1ee1b0a8bf5050765165175affde7afaab1639f", size = 4711, upload-time = "2025-07-30T01:36:11.717Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/eb/f3/43a2cf394d3a710b4fb21dc0e65b165080581d7b5352269d93e54a726c4a/numpy_typing_compat-2.2.20250730-py3-none-any.whl", hash = "sha256:7d7639eafb5bc399650ae6d4baf441dfa0a40eb06965fd85e7f6d4dfe2e34ff7", size = 6059, upload-time = "2025-07-30T01:36:04.763Z" },
+]
+
+[[package]]
+name = "optype"
+version = "0.13.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/4b/b5/c295280c848a622e402d1f4c329d0f4d8055e281a140d3ca52aa8eda462d/optype-0.13.1.tar.gz", hash = "sha256:9b1d590597b0c093faf5253e38238c5a5e1a1f9afb04530836c38ac94fdd5cf6", size = 99030, upload-time = "2025-07-30T12:52:49.941Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/da/0f/35fd26222a48cb4601c62616f992586b39fdcb963bd362bbeec435f81355/optype-0.13.1-py3-none-any.whl", hash = "sha256:811c6b4c3d40e10b6602e33a546b30b5e595e9ec7c59e050b5e328332ed2659c", size = 87632, upload-time = "2025-07-30T12:52:48.334Z" },
+]
+
+[package.optional-dependencies]
+numpy = [
+ { name = "numpy" },
+ { name = "numpy-typing-compat" },
+]
+
[[package]]
name = "packaging"
version = "25.0"
@@ -1196,7 +1272,7 @@ wheels = [
[[package]]
name = "pandas"
-version = "2.3.0"
+version = "2.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
@@ -1204,28 +1280,28 @@ dependencies = [
{ name = "pytz" },
{ name = "tzdata" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/72/51/48f713c4c728d7c55ef7444ba5ea027c26998d96d1a40953b346438602fc/pandas-2.3.0.tar.gz", hash = "sha256:34600ab34ebf1131a7613a260a61dbe8b62c188ec0ea4c296da7c9a06b004133", size = 4484490, upload-time = "2025-06-05T03:27:54.133Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/94/46/24192607058dd607dbfacdd060a2370f6afb19c2ccb617406469b9aeb8e7/pandas-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2eb4728a18dcd2908c7fccf74a982e241b467d178724545a48d0caf534b38ebf", size = 11573865, upload-time = "2025-06-05T03:26:46.774Z" },
- { url = "https://files.pythonhosted.org/packages/9f/cc/ae8ea3b800757a70c9fdccc68b67dc0280a6e814efcf74e4211fd5dea1ca/pandas-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9d8c3187be7479ea5c3d30c32a5d73d62a621166675063b2edd21bc47614027", size = 10702154, upload-time = "2025-06-05T16:50:14.439Z" },
- { url = "https://files.pythonhosted.org/packages/d8/ba/a7883d7aab3d24c6540a2768f679e7414582cc389876d469b40ec749d78b/pandas-2.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ff730713d4c4f2f1c860e36c005c7cefc1c7c80c21c0688fd605aa43c9fcf09", size = 11262180, upload-time = "2025-06-05T16:50:17.453Z" },
- { url = "https://files.pythonhosted.org/packages/01/a5/931fc3ad333d9d87b10107d948d757d67ebcfc33b1988d5faccc39c6845c/pandas-2.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba24af48643b12ffe49b27065d3babd52702d95ab70f50e1b34f71ca703e2c0d", size = 11991493, upload-time = "2025-06-05T03:26:51.813Z" },
- { url = "https://files.pythonhosted.org/packages/d7/bf/0213986830a92d44d55153c1d69b509431a972eb73f204242988c4e66e86/pandas-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:404d681c698e3c8a40a61d0cd9412cc7364ab9a9cc6e144ae2992e11a2e77a20", size = 12470733, upload-time = "2025-06-06T00:00:18.651Z" },
- { url = "https://files.pythonhosted.org/packages/a4/0e/21eb48a3a34a7d4bac982afc2c4eb5ab09f2d988bdf29d92ba9ae8e90a79/pandas-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6021910b086b3ca756755e86ddc64e0ddafd5e58e076c72cb1585162e5ad259b", size = 13212406, upload-time = "2025-06-05T03:26:55.992Z" },
- { url = "https://files.pythonhosted.org/packages/1f/d9/74017c4eec7a28892d8d6e31ae9de3baef71f5a5286e74e6b7aad7f8c837/pandas-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:094e271a15b579650ebf4c5155c05dcd2a14fd4fdd72cf4854b2f7ad31ea30be", size = 10976199, upload-time = "2025-06-05T03:26:59.594Z" },
- { url = "https://files.pythonhosted.org/packages/d3/57/5cb75a56a4842bbd0511c3d1c79186d8315b82dac802118322b2de1194fe/pandas-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c7e2fc25f89a49a11599ec1e76821322439d90820108309bf42130d2f36c983", size = 11518913, upload-time = "2025-06-05T03:27:02.757Z" },
- { url = "https://files.pythonhosted.org/packages/05/01/0c8785610e465e4948a01a059562176e4c8088aa257e2e074db868f86d4e/pandas-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c6da97aeb6a6d233fb6b17986234cc723b396b50a3c6804776351994f2a658fd", size = 10655249, upload-time = "2025-06-05T16:50:20.17Z" },
- { url = "https://files.pythonhosted.org/packages/e8/6a/47fd7517cd8abe72a58706aab2b99e9438360d36dcdb052cf917b7bf3bdc/pandas-2.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb32dc743b52467d488e7a7c8039b821da2826a9ba4f85b89ea95274f863280f", size = 11328359, upload-time = "2025-06-05T03:27:06.431Z" },
- { url = "https://files.pythonhosted.org/packages/2a/b3/463bfe819ed60fb7e7ddffb4ae2ee04b887b3444feee6c19437b8f834837/pandas-2.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:213cd63c43263dbb522c1f8a7c9d072e25900f6975596f883f4bebd77295d4f3", size = 12024789, upload-time = "2025-06-05T03:27:09.875Z" },
- { url = "https://files.pythonhosted.org/packages/04/0c/e0704ccdb0ac40aeb3434d1c641c43d05f75c92e67525df39575ace35468/pandas-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1d2b33e68d0ce64e26a4acc2e72d747292084f4e8db4c847c6f5f6cbe56ed6d8", size = 12480734, upload-time = "2025-06-06T00:00:22.246Z" },
- { url = "https://files.pythonhosted.org/packages/e9/df/815d6583967001153bb27f5cf075653d69d51ad887ebbf4cfe1173a1ac58/pandas-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:430a63bae10b5086995db1b02694996336e5a8ac9a96b4200572b413dfdfccb9", size = 13223381, upload-time = "2025-06-05T03:27:15.641Z" },
- { url = "https://files.pythonhosted.org/packages/79/88/ca5973ed07b7f484c493e941dbff990861ca55291ff7ac67c815ce347395/pandas-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:4930255e28ff5545e2ca404637bcc56f031893142773b3468dc021c6c32a1390", size = 10970135, upload-time = "2025-06-05T03:27:24.131Z" },
- { url = "https://files.pythonhosted.org/packages/24/fb/0994c14d1f7909ce83f0b1fb27958135513c4f3f2528bde216180aa73bfc/pandas-2.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f925f1ef673b4bd0271b1809b72b3270384f2b7d9d14a189b12b7fc02574d575", size = 12141356, upload-time = "2025-06-05T03:27:34.547Z" },
- { url = "https://files.pythonhosted.org/packages/9d/a2/9b903e5962134497ac4f8a96f862ee3081cb2506f69f8e4778ce3d9c9d82/pandas-2.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78ad363ddb873a631e92a3c063ade1ecfb34cae71e9a2be6ad100f875ac1042", size = 11474674, upload-time = "2025-06-05T03:27:39.448Z" },
- { url = "https://files.pythonhosted.org/packages/81/3a/3806d041bce032f8de44380f866059437fb79e36d6b22c82c187e65f765b/pandas-2.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:951805d146922aed8357e4cc5671b8b0b9be1027f0619cea132a9f3f65f2f09c", size = 11439876, upload-time = "2025-06-05T03:27:43.652Z" },
- { url = "https://files.pythonhosted.org/packages/15/aa/3fc3181d12b95da71f5c2537c3e3b3af6ab3a8c392ab41ebb766e0929bc6/pandas-2.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a881bc1309f3fce34696d07b00f13335c41f5f5a8770a33b09ebe23261cfc67", size = 11966182, upload-time = "2025-06-05T03:27:47.652Z" },
- { url = "https://files.pythonhosted.org/packages/37/e7/e12f2d9b0a2c4a2cc86e2aabff7ccfd24f03e597d770abfa2acd313ee46b/pandas-2.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e1991bbb96f4050b09b5f811253c4f3cf05ee89a589379aa36cd623f21a31d6f", size = 12547686, upload-time = "2025-06-06T00:00:26.142Z" },
- { url = "https://files.pythonhosted.org/packages/39/c2/646d2e93e0af70f4e5359d870a63584dacbc324b54d73e6b3267920ff117/pandas-2.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:bb3be958022198531eb7ec2008cfc78c5b1eed51af8600c6c5d9160d89d8d249", size = 13231847, upload-time = "2025-06-05T03:27:51.465Z" },
+sdist = { url = "https://files.pythonhosted.org/packages/d1/6f/75aa71f8a14267117adeeed5d21b204770189c0a0025acbdc03c337b28fc/pandas-2.3.1.tar.gz", hash = "sha256:0a95b9ac964fe83ce317827f80304d37388ea77616b1425f0ae41c9d2d0d7bb2", size = 4487493, upload-time = "2025-07-07T19:20:04.079Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/46/de/b8445e0f5d217a99fe0eeb2f4988070908979bec3587c0633e5428ab596c/pandas-2.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:689968e841136f9e542020698ee1c4fbe9caa2ed2213ae2388dc7b81721510d3", size = 11588172, upload-time = "2025-07-07T19:18:52.054Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/e0/801cdb3564e65a5ac041ab99ea6f1d802a6c325bb6e58c79c06a3f1cd010/pandas-2.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:025e92411c16cbe5bb2a4abc99732a6b132f439b8aab23a59fa593eb00704232", size = 10717365, upload-time = "2025-07-07T19:18:54.785Z" },
+ { url = "https://files.pythonhosted.org/packages/51/a5/c76a8311833c24ae61a376dbf360eb1b1c9247a5d9c1e8b356563b31b80c/pandas-2.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b7ff55f31c4fcb3e316e8f7fa194566b286d6ac430afec0d461163312c5841e", size = 11280411, upload-time = "2025-07-07T19:18:57.045Z" },
+ { url = "https://files.pythonhosted.org/packages/da/01/e383018feba0a1ead6cf5fe8728e5d767fee02f06a3d800e82c489e5daaf/pandas-2.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7dcb79bf373a47d2a40cf7232928eb7540155abbc460925c2c96d2d30b006eb4", size = 11988013, upload-time = "2025-07-07T19:18:59.771Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/14/cec7760d7c9507f11c97d64f29022e12a6cc4fc03ac694535e89f88ad2ec/pandas-2.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:56a342b231e8862c96bdb6ab97170e203ce511f4d0429589c8ede1ee8ece48b8", size = 12767210, upload-time = "2025-07-07T19:19:02.944Z" },
+ { url = "https://files.pythonhosted.org/packages/50/b9/6e2d2c6728ed29fb3d4d4d302504fb66f1a543e37eb2e43f352a86365cdf/pandas-2.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ca7ed14832bce68baef331f4d7f294411bed8efd032f8109d690df45e00c4679", size = 13440571, upload-time = "2025-07-07T19:19:06.82Z" },
+ { url = "https://files.pythonhosted.org/packages/80/a5/3a92893e7399a691bad7664d977cb5e7c81cf666c81f89ea76ba2bff483d/pandas-2.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:ac942bfd0aca577bef61f2bc8da8147c4ef6879965ef883d8e8d5d2dc3e744b8", size = 10987601, upload-time = "2025-07-07T19:19:09.589Z" },
+ { url = "https://files.pythonhosted.org/packages/32/ed/ff0a67a2c5505e1854e6715586ac6693dd860fbf52ef9f81edee200266e7/pandas-2.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9026bd4a80108fac2239294a15ef9003c4ee191a0f64b90f170b40cfb7cf2d22", size = 11531393, upload-time = "2025-07-07T19:19:12.245Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/db/d8f24a7cc9fb0972adab0cc80b6817e8bef888cfd0024eeb5a21c0bb5c4a/pandas-2.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6de8547d4fdb12421e2d047a2c446c623ff4c11f47fddb6b9169eb98ffba485a", size = 10668750, upload-time = "2025-07-07T19:19:14.612Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/b0/80f6ec783313f1e2356b28b4fd8d2148c378370045da918c73145e6aab50/pandas-2.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:782647ddc63c83133b2506912cc6b108140a38a37292102aaa19c81c83db2928", size = 11342004, upload-time = "2025-07-07T19:19:16.857Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/e2/20a317688435470872885e7fc8f95109ae9683dec7c50be29b56911515a5/pandas-2.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ba6aff74075311fc88504b1db890187a3cd0f887a5b10f5525f8e2ef55bfdb9", size = 12050869, upload-time = "2025-07-07T19:19:19.265Z" },
+ { url = "https://files.pythonhosted.org/packages/55/79/20d746b0a96c67203a5bee5fb4e00ac49c3e8009a39e1f78de264ecc5729/pandas-2.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e5635178b387bd2ba4ac040f82bc2ef6e6b500483975c4ebacd34bec945fda12", size = 12750218, upload-time = "2025-07-07T19:19:21.547Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/0f/145c8b41e48dbf03dd18fdd7f24f8ba95b8254a97a3379048378f33e7838/pandas-2.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f3bf5ec947526106399a9e1d26d40ee2b259c66422efdf4de63c848492d91bb", size = 13416763, upload-time = "2025-07-07T19:19:23.939Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/c0/54415af59db5cdd86a3d3bf79863e8cc3fa9ed265f0745254061ac09d5f2/pandas-2.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:1c78cf43c8fde236342a1cb2c34bcff89564a7bfed7e474ed2fffa6aed03a956", size = 10987482, upload-time = "2025-07-07T19:19:42.699Z" },
+ { url = "https://files.pythonhosted.org/packages/48/64/2fd2e400073a1230e13b8cd604c9bc95d9e3b962e5d44088ead2e8f0cfec/pandas-2.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8dfc17328e8da77be3cf9f47509e5637ba8f137148ed0e9b5241e1baf526e20a", size = 12029159, upload-time = "2025-07-07T19:19:26.362Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/0a/d84fd79b0293b7ef88c760d7dca69828d867c89b6d9bc52d6a27e4d87316/pandas-2.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ec6c851509364c59a5344458ab935e6451b31b818be467eb24b0fe89bd05b6b9", size = 11393287, upload-time = "2025-07-07T19:19:29.157Z" },
+ { url = "https://files.pythonhosted.org/packages/50/ae/ff885d2b6e88f3c7520bb74ba319268b42f05d7e583b5dded9837da2723f/pandas-2.3.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:911580460fc4884d9b05254b38a6bfadddfcc6aaef856fb5859e7ca202e45275", size = 11309381, upload-time = "2025-07-07T19:19:31.436Z" },
+ { url = "https://files.pythonhosted.org/packages/85/86/1fa345fc17caf5d7780d2699985c03dbe186c68fee00b526813939062bb0/pandas-2.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f4d6feeba91744872a600e6edbbd5b033005b431d5ae8379abee5bcfa479fab", size = 11883998, upload-time = "2025-07-07T19:19:34.267Z" },
+ { url = "https://files.pythonhosted.org/packages/81/aa/e58541a49b5e6310d89474333e994ee57fea97c8aaa8fc7f00b873059bbf/pandas-2.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fe37e757f462d31a9cd7580236a82f353f5713a80e059a29753cf938c6775d96", size = 12704705, upload-time = "2025-07-07T19:19:36.856Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/f9/07086f5b0f2a19872554abeea7658200824f5835c58a106fa8f2ae96a46c/pandas-2.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5db9637dbc24b631ff3707269ae4559bce4b7fd75c1c4d7e13f40edc42df4444", size = 13189044, upload-time = "2025-07-07T19:19:39.999Z" },
]
[[package]]
@@ -1433,7 +1509,7 @@ wheels = [
[[package]]
name = "pyneuroml"
-version = "1.3.19"
+version = "1.3.21"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "airspeed" },
@@ -1449,9 +1525,9 @@ dependencies = [
{ name = "pylems" },
{ name = "sympy" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/35/79/f6a07b998e4ca966a56f28b93382f3d72e00bd05c97f2bda86435989bd95/pyneuroml-1.3.19.tar.gz", hash = "sha256:5a291a64692338806f1d8b056394759711f4bc878914ee57576ec42c3c1fdd16", size = 28821887, upload-time = "2025-07-02T17:12:40.269Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/7f/c8/0aeb890056b5876296d2b87caa85a572bef722c565d61a5aac9fc573351a/pyneuroml-1.3.21.tar.gz", hash = "sha256:c693c19fd9bb6d623bfc9c707b9380ba4e0f22eaf3846d7f112bd3f55b788101", size = 28822088, upload-time = "2025-07-22T15:59:52.181Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/61/26/7d63e24112f4b15377420a0074d3a5de10416cb4bf56d47faf0121e8137d/pyneuroml-1.3.19-py3-none-any.whl", hash = "sha256:20331d6c71e82b0734deb3d3dd784769ca870c9d0f32e65e8128616877bee244", size = 27338580, upload-time = "2025-07-02T17:12:37.455Z" },
+ { url = "https://files.pythonhosted.org/packages/98/9d/19eb704df7e46273da43c1b3f474ac6929e087c66027e4971a2a5a125ede/pyneuroml-1.3.21-py3-none-any.whl", hash = "sha256:ef248b159fd3e8524fe8c0011d5114fd5edba9db04c05e1a3805831e6ebcc819", size = 27338815, upload-time = "2025-07-22T15:59:49.611Z" },
]
[[package]]
@@ -1737,7 +1813,7 @@ wheels = [
[[package]]
name = "scikit-learn"
-version = "1.7.0"
+version = "1.7.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "joblib" },
@@ -1745,60 +1821,91 @@ dependencies = [
{ name = "scipy" },
{ name = "threadpoolctl" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/df/3b/29fa87e76b1d7b3b77cc1fcbe82e6e6b8cd704410705b008822de530277c/scikit_learn-1.7.0.tar.gz", hash = "sha256:c01e869b15aec88e2cdb73d27f15bdbe03bce8e2fb43afbe77c45d399e73a5a3", size = 7178217, upload-time = "2025-06-05T22:02:46.703Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/41/84/5f4af978fff619706b8961accac84780a6d298d82a8873446f72edb4ead0/scikit_learn-1.7.1.tar.gz", hash = "sha256:24b3f1e976a4665aa74ee0fcaac2b8fccc6ae77c8e07ab25da3ba6d3292b9802", size = 7190445, upload-time = "2025-07-18T08:01:54.5Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/70/3a/bffab14e974a665a3ee2d79766e7389572ffcaad941a246931c824afcdb2/scikit_learn-1.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c2c7243d34aaede0efca7a5a96d67fddaebb4ad7e14a70991b9abee9dc5c0379", size = 11646758, upload-time = "2025-06-05T22:02:09.51Z" },
- { url = "https://files.pythonhosted.org/packages/58/d8/f3249232fa79a70cb40595282813e61453c1e76da3e1a44b77a63dd8d0cb/scikit_learn-1.7.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:9f39f6a811bf3f15177b66c82cbe0d7b1ebad9f190737dcdef77cfca1ea3c19c", size = 10673971, upload-time = "2025-06-05T22:02:12.217Z" },
- { url = "https://files.pythonhosted.org/packages/67/93/eb14c50533bea2f77758abe7d60a10057e5f2e2cdcf0a75a14c6bc19c734/scikit_learn-1.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63017a5f9a74963d24aac7590287149a8d0f1a0799bbe7173c0d8ba1523293c0", size = 11818428, upload-time = "2025-06-05T22:02:14.947Z" },
- { url = "https://files.pythonhosted.org/packages/08/17/804cc13b22a8663564bb0b55fb89e661a577e4e88a61a39740d58b909efe/scikit_learn-1.7.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b2f8a0b1e73e9a08b7cc498bb2aeab36cdc1f571f8ab2b35c6e5d1c7115d97d", size = 12505887, upload-time = "2025-06-05T22:02:17.824Z" },
- { url = "https://files.pythonhosted.org/packages/68/c7/4e956281a077f4835458c3f9656c666300282d5199039f26d9de1dabd9be/scikit_learn-1.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:34cc8d9d010d29fb2b7cbcd5ccc24ffdd80515f65fe9f1e4894ace36b267ce19", size = 10668129, upload-time = "2025-06-05T22:02:20.536Z" },
- { url = "https://files.pythonhosted.org/packages/9a/c3/a85dcccdaf1e807e6f067fa95788a6485b0491d9ea44fd4c812050d04f45/scikit_learn-1.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5b7974f1f32bc586c90145df51130e02267e4b7e77cab76165c76cf43faca0d9", size = 11559841, upload-time = "2025-06-05T22:02:23.308Z" },
- { url = "https://files.pythonhosted.org/packages/d8/57/eea0de1562cc52d3196eae51a68c5736a31949a465f0b6bb3579b2d80282/scikit_learn-1.7.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:014e07a23fe02e65f9392898143c542a50b6001dbe89cb867e19688e468d049b", size = 10616463, upload-time = "2025-06-05T22:02:26.068Z" },
- { url = "https://files.pythonhosted.org/packages/10/a4/39717ca669296dfc3a62928393168da88ac9d8cbec88b6321ffa62c6776f/scikit_learn-1.7.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7e7ced20582d3a5516fb6f405fd1d254e1f5ce712bfef2589f51326af6346e8", size = 11766512, upload-time = "2025-06-05T22:02:28.689Z" },
- { url = "https://files.pythonhosted.org/packages/d5/cd/a19722241d5f7b51e08351e1e82453e0057aeb7621b17805f31fcb57bb6c/scikit_learn-1.7.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1babf2511e6ffd695da7a983b4e4d6de45dce39577b26b721610711081850906", size = 12461075, upload-time = "2025-06-05T22:02:31.233Z" },
- { url = "https://files.pythonhosted.org/packages/f3/bc/282514272815c827a9acacbe5b99f4f1a4bc5961053719d319480aee0812/scikit_learn-1.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:5abd2acff939d5bd4701283f009b01496832d50ddafa83c90125a4e41c33e314", size = 10652517, upload-time = "2025-06-05T22:02:34.139Z" },
- { url = "https://files.pythonhosted.org/packages/ea/78/7357d12b2e4c6674175f9a09a3ba10498cde8340e622715bcc71e532981d/scikit_learn-1.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e39d95a929b112047c25b775035c8c234c5ca67e681ce60d12413afb501129f7", size = 12111822, upload-time = "2025-06-05T22:02:36.904Z" },
- { url = "https://files.pythonhosted.org/packages/d0/0c/9c3715393343f04232f9d81fe540eb3831d0b4ec351135a145855295110f/scikit_learn-1.7.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:0521cb460426c56fee7e07f9365b0f45ec8ca7b2d696534ac98bfb85e7ae4775", size = 11325286, upload-time = "2025-06-05T22:02:39.739Z" },
- { url = "https://files.pythonhosted.org/packages/64/e0/42282ad3dd70b7c1a5f65c412ac3841f6543502a8d6263cae7b466612dc9/scikit_learn-1.7.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:317ca9f83acbde2883bd6bb27116a741bfcb371369706b4f9973cf30e9a03b0d", size = 12380865, upload-time = "2025-06-05T22:02:42.137Z" },
- { url = "https://files.pythonhosted.org/packages/4e/d0/3ef4ab2c6be4aa910445cd09c5ef0b44512e3de2cfb2112a88bb647d2cf7/scikit_learn-1.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:126c09740a6f016e815ab985b21e3a0656835414521c81fc1a8da78b679bdb75", size = 11549609, upload-time = "2025-06-05T22:02:44.483Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/16/57f176585b35ed865f51b04117947fe20f130f78940c6477b6d66279c9c2/scikit_learn-1.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3cee419b49b5bbae8796ecd690f97aa412ef1674410c23fc3257c6b8b85b8087", size = 9260431, upload-time = "2025-07-18T08:01:22.77Z" },
+ { url = "https://files.pythonhosted.org/packages/67/4e/899317092f5efcab0e9bc929e3391341cec8fb0e816c4789686770024580/scikit_learn-1.7.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:2fd8b8d35817b0d9ebf0b576f7d5ffbbabdb55536b0655a8aaae629d7ffd2e1f", size = 8637191, upload-time = "2025-07-18T08:01:24.731Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/1b/998312db6d361ded1dd56b457ada371a8d8d77ca2195a7d18fd8a1736f21/scikit_learn-1.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:588410fa19a96a69763202f1d6b7b91d5d7a5d73be36e189bc6396bfb355bd87", size = 9486346, upload-time = "2025-07-18T08:01:26.713Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/09/a2aa0b4e644e5c4ede7006748f24e72863ba2ae71897fecfd832afea01b4/scikit_learn-1.7.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e3142f0abe1ad1d1c31a2ae987621e41f6b578144a911ff4ac94781a583adad7", size = 9290988, upload-time = "2025-07-18T08:01:28.938Z" },
+ { url = "https://files.pythonhosted.org/packages/15/fa/c61a787e35f05f17fc10523f567677ec4eeee5f95aa4798dbbbcd9625617/scikit_learn-1.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3ddd9092c1bd469acab337d87930067c87eac6bd544f8d5027430983f1e1ae88", size = 8735568, upload-time = "2025-07-18T08:01:30.936Z" },
+ { url = "https://files.pythonhosted.org/packages/52/f8/e0533303f318a0f37b88300d21f79b6ac067188d4824f1047a37214ab718/scikit_learn-1.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b7839687fa46d02e01035ad775982f2470be2668e13ddd151f0f55a5bf123bae", size = 9213143, upload-time = "2025-07-18T08:01:32.942Z" },
+ { url = "https://files.pythonhosted.org/packages/71/f3/f1df377d1bdfc3e3e2adc9c119c238b182293e6740df4cbeac6de2cc3e23/scikit_learn-1.7.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:a10f276639195a96c86aa572ee0698ad64ee939a7b042060b98bd1930c261d10", size = 8591977, upload-time = "2025-07-18T08:01:34.967Z" },
+ { url = "https://files.pythonhosted.org/packages/99/72/c86a4cd867816350fe8dee13f30222340b9cd6b96173955819a5561810c5/scikit_learn-1.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:13679981fdaebc10cc4c13c43344416a86fcbc61449cb3e6517e1df9d12c8309", size = 9436142, upload-time = "2025-07-18T08:01:37.397Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/66/277967b29bd297538dc7a6ecfb1a7dce751beabd0d7f7a2233be7a4f7832/scikit_learn-1.7.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f1262883c6a63f067a980a8cdd2d2e7f2513dddcef6a9eaada6416a7a7cbe43", size = 9282996, upload-time = "2025-07-18T08:01:39.721Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/47/9291cfa1db1dae9880420d1e07dbc7e8dd4a7cdbc42eaba22512e6bde958/scikit_learn-1.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:ca6d31fb10e04d50bfd2b50d66744729dbb512d4efd0223b864e2fdbfc4cee11", size = 8707418, upload-time = "2025-07-18T08:01:42.124Z" },
+ { url = "https://files.pythonhosted.org/packages/61/95/45726819beccdaa34d3362ea9b2ff9f2b5d3b8bf721bd632675870308ceb/scikit_learn-1.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:781674d096303cfe3d351ae6963ff7c958db61cde3421cd490e3a5a58f2a94ae", size = 9561466, upload-time = "2025-07-18T08:01:44.195Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/1c/6f4b3344805de783d20a51eb24d4c9ad4b11a7f75c1801e6ec6d777361fd/scikit_learn-1.7.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:10679f7f125fe7ecd5fad37dd1aa2daae7e3ad8df7f3eefa08901b8254b3e12c", size = 9040467, upload-time = "2025-07-18T08:01:46.671Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/80/abe18fe471af9f1d181904203d62697998b27d9b62124cd281d740ded2f9/scikit_learn-1.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1f812729e38c8cb37f760dce71a9b83ccfb04f59b3dca7c6079dcdc60544fa9e", size = 9532052, upload-time = "2025-07-18T08:01:48.676Z" },
+ { url = "https://files.pythonhosted.org/packages/14/82/b21aa1e0c4cee7e74864d3a5a721ab8fcae5ca55033cb6263dca297ed35b/scikit_learn-1.7.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88e1a20131cf741b84b89567e1717f27a2ced228e0f29103426102bc2e3b8ef7", size = 9361575, upload-time = "2025-07-18T08:01:50.639Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/20/f4777fcd5627dc6695fa6b92179d0edb7a3ac1b91bcd9a1c7f64fa7ade23/scikit_learn-1.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b1bd1d919210b6a10b7554b717c9000b5485aa95a1d0f177ae0d7ee8ec750da5", size = 9277310, upload-time = "2025-07-18T08:01:52.547Z" },
]
[[package]]
name = "scipy"
-version = "1.16.0"
+version = "1.16.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/81/18/b06a83f0c5ee8cddbde5e3f3d0bb9b702abfa5136ef6d4620ff67df7eee5/scipy-1.16.0.tar.gz", hash = "sha256:b5ef54021e832869c8cfb03bc3bf20366cbcd426e02a58e8a58d7584dfbb8f62", size = 30581216, upload-time = "2025-06-22T16:27:55.782Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/01/c0/c943bc8d2bbd28123ad0f4f1eef62525fa1723e84d136b32965dcb6bad3a/scipy-1.16.0-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:7eb6bd33cef4afb9fa5f1fb25df8feeb1e52d94f21a44f1d17805b41b1da3180", size = 36459071, upload-time = "2025-06-22T16:19:06.605Z" },
- { url = "https://files.pythonhosted.org/packages/99/0d/270e2e9f1a4db6ffbf84c9a0b648499842046e4e0d9b2275d150711b3aba/scipy-1.16.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:1dbc8fdba23e4d80394ddfab7a56808e3e6489176d559c6c71935b11a2d59db1", size = 28490500, upload-time = "2025-06-22T16:19:11.775Z" },
- { url = "https://files.pythonhosted.org/packages/1c/22/01d7ddb07cff937d4326198ec8d10831367a708c3da72dfd9b7ceaf13028/scipy-1.16.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:7dcf42c380e1e3737b343dec21095c9a9ad3f9cbe06f9c05830b44b1786c9e90", size = 20762345, upload-time = "2025-06-22T16:19:15.813Z" },
- { url = "https://files.pythonhosted.org/packages/34/7f/87fd69856569ccdd2a5873fe5d7b5bbf2ad9289d7311d6a3605ebde3a94b/scipy-1.16.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:26ec28675f4a9d41587266084c626b02899db373717d9312fa96ab17ca1ae94d", size = 23418563, upload-time = "2025-06-22T16:19:20.746Z" },
- { url = "https://files.pythonhosted.org/packages/f6/f1/e4f4324fef7f54160ab749efbab6a4bf43678a9eb2e9817ed71a0a2fd8de/scipy-1.16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:952358b7e58bd3197cfbd2f2f2ba829f258404bdf5db59514b515a8fe7a36c52", size = 33203951, upload-time = "2025-06-22T16:19:25.813Z" },
- { url = "https://files.pythonhosted.org/packages/6d/f0/b6ac354a956384fd8abee2debbb624648125b298f2c4a7b4f0d6248048a5/scipy-1.16.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03931b4e870c6fef5b5c0970d52c9f6ddd8c8d3e934a98f09308377eba6f3824", size = 35070225, upload-time = "2025-06-22T16:19:31.416Z" },
- { url = "https://files.pythonhosted.org/packages/e5/73/5cbe4a3fd4bc3e2d67ffad02c88b83edc88f381b73ab982f48f3df1a7790/scipy-1.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:512c4f4f85912767c351a0306824ccca6fd91307a9f4318efe8fdbd9d30562ef", size = 35389070, upload-time = "2025-06-22T16:19:37.387Z" },
- { url = "https://files.pythonhosted.org/packages/86/e8/a60da80ab9ed68b31ea5a9c6dfd3c2f199347429f229bf7f939a90d96383/scipy-1.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e69f798847e9add03d512eaf5081a9a5c9a98757d12e52e6186ed9681247a1ac", size = 37825287, upload-time = "2025-06-22T16:19:43.375Z" },
- { url = "https://files.pythonhosted.org/packages/ea/b5/29fece1a74c6a94247f8a6fb93f5b28b533338e9c34fdcc9cfe7a939a767/scipy-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:adf9b1999323ba335adc5d1dc7add4781cb5a4b0ef1e98b79768c05c796c4e49", size = 38431929, upload-time = "2025-06-22T16:19:49.385Z" },
- { url = "https://files.pythonhosted.org/packages/46/95/0746417bc24be0c2a7b7563946d61f670a3b491b76adede420e9d173841f/scipy-1.16.0-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:e9f414cbe9ca289a73e0cc92e33a6a791469b6619c240aa32ee18abdce8ab451", size = 36418162, upload-time = "2025-06-22T16:19:56.3Z" },
- { url = "https://files.pythonhosted.org/packages/19/5a/914355a74481b8e4bbccf67259bbde171348a3f160b67b4945fbc5f5c1e5/scipy-1.16.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:bbba55fb97ba3cdef9b1ee973f06b09d518c0c7c66a009c729c7d1592be1935e", size = 28465985, upload-time = "2025-06-22T16:20:01.238Z" },
- { url = "https://files.pythonhosted.org/packages/58/46/63477fc1246063855969cbefdcee8c648ba4b17f67370bd542ba56368d0b/scipy-1.16.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:58e0d4354eacb6004e7aa1cd350e5514bd0270acaa8d5b36c0627bb3bb486974", size = 20737961, upload-time = "2025-06-22T16:20:05.913Z" },
- { url = "https://files.pythonhosted.org/packages/93/86/0fbb5588b73555e40f9d3d6dde24ee6fac7d8e301a27f6f0cab9d8f66ff2/scipy-1.16.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:75b2094ec975c80efc273567436e16bb794660509c12c6a31eb5c195cbf4b6dc", size = 23377941, upload-time = "2025-06-22T16:20:10.668Z" },
- { url = "https://files.pythonhosted.org/packages/ca/80/a561f2bf4c2da89fa631b3cbf31d120e21ea95db71fd9ec00cb0247c7a93/scipy-1.16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6b65d232157a380fdd11a560e7e21cde34fdb69d65c09cb87f6cc024ee376351", size = 33196703, upload-time = "2025-06-22T16:20:16.097Z" },
- { url = "https://files.pythonhosted.org/packages/11/6b/3443abcd0707d52e48eb315e33cc669a95e29fc102229919646f5a501171/scipy-1.16.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1d8747f7736accd39289943f7fe53a8333be7f15a82eea08e4afe47d79568c32", size = 35083410, upload-time = "2025-06-22T16:20:21.734Z" },
- { url = "https://files.pythonhosted.org/packages/20/ab/eb0fc00e1e48961f1bd69b7ad7e7266896fe5bad4ead91b5fc6b3561bba4/scipy-1.16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eb9f147a1b8529bb7fec2a85cf4cf42bdfadf9e83535c309a11fdae598c88e8b", size = 35387829, upload-time = "2025-06-22T16:20:27.548Z" },
- { url = "https://files.pythonhosted.org/packages/57/9e/d6fc64e41fad5d481c029ee5a49eefc17f0b8071d636a02ceee44d4a0de2/scipy-1.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d2b83c37edbfa837a8923d19c749c1935ad3d41cf196006a24ed44dba2ec4358", size = 37841356, upload-time = "2025-06-22T16:20:35.112Z" },
- { url = "https://files.pythonhosted.org/packages/7c/a7/4c94bbe91f12126b8bf6709b2471900577b7373a4fd1f431f28ba6f81115/scipy-1.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:79a3c13d43c95aa80b87328a46031cf52508cf5f4df2767602c984ed1d3c6bbe", size = 38403710, upload-time = "2025-06-22T16:21:54.473Z" },
- { url = "https://files.pythonhosted.org/packages/47/20/965da8497f6226e8fa90ad3447b82ed0e28d942532e92dd8b91b43f100d4/scipy-1.16.0-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:f91b87e1689f0370690e8470916fe1b2308e5b2061317ff76977c8f836452a47", size = 36813833, upload-time = "2025-06-22T16:20:43.925Z" },
- { url = "https://files.pythonhosted.org/packages/28/f4/197580c3dac2d234e948806e164601c2df6f0078ed9f5ad4a62685b7c331/scipy-1.16.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:88a6ca658fb94640079e7a50b2ad3b67e33ef0f40e70bdb7dc22017dae73ac08", size = 28974431, upload-time = "2025-06-22T16:20:51.302Z" },
- { url = "https://files.pythonhosted.org/packages/8a/fc/e18b8550048d9224426e76906694c60028dbdb65d28b1372b5503914b89d/scipy-1.16.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:ae902626972f1bd7e4e86f58fd72322d7f4ec7b0cfc17b15d4b7006efc385176", size = 21246454, upload-time = "2025-06-22T16:20:57.276Z" },
- { url = "https://files.pythonhosted.org/packages/8c/48/07b97d167e0d6a324bfd7484cd0c209cc27338b67e5deadae578cf48e809/scipy-1.16.0-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:8cb824c1fc75ef29893bc32b3ddd7b11cf9ab13c1127fe26413a05953b8c32ed", size = 23772979, upload-time = "2025-06-22T16:21:03.363Z" },
- { url = "https://files.pythonhosted.org/packages/4c/4f/9efbd3f70baf9582edf271db3002b7882c875ddd37dc97f0f675ad68679f/scipy-1.16.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:de2db7250ff6514366a9709c2cba35cb6d08498e961cba20d7cff98a7ee88938", size = 33341972, upload-time = "2025-06-22T16:21:11.14Z" },
- { url = "https://files.pythonhosted.org/packages/3f/dc/9e496a3c5dbe24e76ee24525155ab7f659c20180bab058ef2c5fa7d9119c/scipy-1.16.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e85800274edf4db8dd2e4e93034f92d1b05c9421220e7ded9988b16976f849c1", size = 35185476, upload-time = "2025-06-22T16:21:19.156Z" },
- { url = "https://files.pythonhosted.org/packages/ce/b3/21001cff985a122ba434c33f2c9d7d1dc3b669827e94f4fc4e1fe8b9dfd8/scipy-1.16.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4f720300a3024c237ace1cb11f9a84c38beb19616ba7c4cdcd771047a10a1706", size = 35570990, upload-time = "2025-06-22T16:21:27.797Z" },
- { url = "https://files.pythonhosted.org/packages/e5/d3/7ba42647d6709251cdf97043d0c107e0317e152fa2f76873b656b509ff55/scipy-1.16.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:aad603e9339ddb676409b104c48a027e9916ce0d2838830691f39552b38a352e", size = 37950262, upload-time = "2025-06-22T16:21:36.976Z" },
- { url = "https://files.pythonhosted.org/packages/eb/c4/231cac7a8385394ebbbb4f1ca662203e9d8c332825ab4f36ffc3ead09a42/scipy-1.16.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f56296fefca67ba605fd74d12f7bd23636267731a72cb3947963e76b8c0a25db", size = 38515076, upload-time = "2025-06-22T16:21:45.694Z" },
+sdist = { url = "https://files.pythonhosted.org/packages/f5/4a/b927028464795439faec8eaf0b03b011005c487bb2d07409f28bf30879c4/scipy-1.16.1.tar.gz", hash = "sha256:44c76f9e8b6e8e488a586190ab38016e4ed2f8a038af7cd3defa903c0a2238b3", size = 30580861, upload-time = "2025-07-27T16:33:30.834Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f8/d9/ec4864f5896232133f51382b54a08de91a9d1af7a76dfa372894026dfee2/scipy-1.16.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:81b433bbeaf35728dad619afc002db9b189e45eebe2cd676effe1fb93fef2b9c", size = 36575194, upload-time = "2025-07-27T16:27:41.321Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/6d/40e81ecfb688e9d25d34a847dca361982a6addf8e31f0957b1a54fbfa994/scipy-1.16.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:886cc81fdb4c6903a3bb0464047c25a6d1016fef77bb97949817d0c0d79f9e04", size = 28594590, upload-time = "2025-07-27T16:27:49.204Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/37/9f65178edfcc629377ce9a64fc09baebea18c80a9e57ae09a52edf84880b/scipy-1.16.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:15240c3aac087a522b4eaedb09f0ad061753c5eebf1ea430859e5bf8640d5919", size = 20866458, upload-time = "2025-07-27T16:27:54.98Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/7b/749a66766871ea4cb1d1ea10f27004db63023074c22abed51f22f09770e0/scipy-1.16.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:65f81a25805f3659b48126b5053d9e823d3215e4a63730b5e1671852a1705921", size = 23539318, upload-time = "2025-07-27T16:28:01.604Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/db/8d4afec60eb833a666434d4541a3151eedbf2494ea6d4d468cbe877f00cd/scipy-1.16.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6c62eea7f607f122069b9bad3f99489ddca1a5173bef8a0c75555d7488b6f725", size = 33292899, upload-time = "2025-07-27T16:28:09.147Z" },
+ { url = "https://files.pythonhosted.org/packages/51/1e/79023ca3bbb13a015d7d2757ecca3b81293c663694c35d6541b4dca53e98/scipy-1.16.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f965bbf3235b01c776115ab18f092a95aa74c271a52577bcb0563e85738fd618", size = 35162637, upload-time = "2025-07-27T16:28:17.535Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/49/0648665f9c29fdaca4c679182eb972935b3b4f5ace41d323c32352f29816/scipy-1.16.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f006e323874ffd0b0b816d8c6a8e7f9a73d55ab3b8c3f72b752b226d0e3ac83d", size = 35490507, upload-time = "2025-07-27T16:28:25.705Z" },
+ { url = "https://files.pythonhosted.org/packages/62/8f/66cbb9d6bbb18d8c658f774904f42a92078707a7c71e5347e8bf2f52bb89/scipy-1.16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8fd15fc5085ab4cca74cb91fe0a4263b1f32e4420761ddae531ad60934c2119", size = 37923998, upload-time = "2025-07-27T16:28:34.339Z" },
+ { url = "https://files.pythonhosted.org/packages/14/c3/61f273ae550fbf1667675701112e380881905e28448c080b23b5a181df7c/scipy-1.16.1-cp312-cp312-win_amd64.whl", hash = "sha256:f7b8013c6c066609577d910d1a2a077021727af07b6fab0ee22c2f901f22352a", size = 38508060, upload-time = "2025-07-27T16:28:43.242Z" },
+ { url = "https://files.pythonhosted.org/packages/93/0b/b5c99382b839854a71ca9482c684e3472badc62620287cbbdab499b75ce6/scipy-1.16.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5451606823a5e73dfa621a89948096c6528e2896e40b39248295d3a0138d594f", size = 36533717, upload-time = "2025-07-27T16:28:51.706Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/e5/69ab2771062c91e23e07c12e7d5033a6b9b80b0903ee709c3c36b3eb520c/scipy-1.16.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:89728678c5ca5abd610aee148c199ac1afb16e19844401ca97d43dc548a354eb", size = 28570009, upload-time = "2025-07-27T16:28:57.017Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/69/bd75dbfdd3cf524f4d753484d723594aed62cfaac510123e91a6686d520b/scipy-1.16.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e756d688cb03fd07de0fffad475649b03cb89bee696c98ce508b17c11a03f95c", size = 20841942, upload-time = "2025-07-27T16:29:01.152Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/74/add181c87663f178ba7d6144b370243a87af8476664d5435e57d599e6874/scipy-1.16.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:5aa2687b9935da3ed89c5dbed5234576589dd28d0bf7cd237501ccfbdf1ad608", size = 23498507, upload-time = "2025-07-27T16:29:05.202Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/74/ece2e582a0d9550cee33e2e416cc96737dce423a994d12bbe59716f47ff1/scipy-1.16.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0851f6a1e537fe9399f35986897e395a1aa61c574b178c0d456be5b1a0f5ca1f", size = 33286040, upload-time = "2025-07-27T16:29:10.201Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/82/08e4076df538fb56caa1d489588d880ec7c52d8273a606bb54d660528f7c/scipy-1.16.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fedc2cbd1baed37474b1924c331b97bdff611d762c196fac1a9b71e67b813b1b", size = 35176096, upload-time = "2025-07-27T16:29:17.091Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/79/cd710aab8c921375711a8321c6be696e705a120e3011a643efbbcdeeabcc/scipy-1.16.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2ef500e72f9623a6735769e4b93e9dcb158d40752cdbb077f305487e3e2d1f45", size = 35490328, upload-time = "2025-07-27T16:29:22.928Z" },
+ { url = "https://files.pythonhosted.org/packages/71/73/e9cc3d35ee4526d784520d4494a3e1ca969b071fb5ae5910c036a375ceec/scipy-1.16.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:978d8311674b05a8f7ff2ea6c6bce5d8b45a0cb09d4c5793e0318f448613ea65", size = 37939921, upload-time = "2025-07-27T16:29:29.108Z" },
+ { url = "https://files.pythonhosted.org/packages/21/12/c0efd2941f01940119b5305c375ae5c0fcb7ec193f806bd8f158b73a1782/scipy-1.16.1-cp313-cp313-win_amd64.whl", hash = "sha256:81929ed0fa7a5713fcdd8b2e6f73697d3b4c4816d090dd34ff937c20fa90e8ab", size = 38479462, upload-time = "2025-07-27T16:30:24.078Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/19/c3d08b675260046a991040e1ea5d65f91f40c7df1045fffff412dcfc6765/scipy-1.16.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:bcc12db731858abda693cecdb3bdc9e6d4bd200213f49d224fe22df82687bdd6", size = 36938832, upload-time = "2025-07-27T16:29:35.057Z" },
+ { url = "https://files.pythonhosted.org/packages/81/f2/ce53db652c033a414a5b34598dba6b95f3d38153a2417c5a3883da429029/scipy-1.16.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:744d977daa4becb9fc59135e75c069f8d301a87d64f88f1e602a9ecf51e77b27", size = 29093084, upload-time = "2025-07-27T16:29:40.201Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/ae/7a10ff04a7dc15f9057d05b33737ade244e4bd195caa3f7cc04d77b9e214/scipy-1.16.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:dc54f76ac18073bcecffb98d93f03ed6b81a92ef91b5d3b135dcc81d55a724c7", size = 21365098, upload-time = "2025-07-27T16:29:44.295Z" },
+ { url = "https://files.pythonhosted.org/packages/36/ac/029ff710959932ad3c2a98721b20b405f05f752f07344622fd61a47c5197/scipy-1.16.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:367d567ee9fc1e9e2047d31f39d9d6a7a04e0710c86e701e053f237d14a9b4f6", size = 23896858, upload-time = "2025-07-27T16:29:48.784Z" },
+ { url = "https://files.pythonhosted.org/packages/71/13/d1ef77b6bd7898720e1f0b6b3743cb945f6c3cafa7718eaac8841035ab60/scipy-1.16.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4cf5785e44e19dcd32a0e4807555e1e9a9b8d475c6afff3d21c3c543a6aa84f4", size = 33438311, upload-time = "2025-07-27T16:29:54.164Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/e0/e64a6821ffbb00b4c5b05169f1c1fddb4800e9307efe3db3788995a82a2c/scipy-1.16.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3d0b80fb26d3e13a794c71d4b837e2a589d839fd574a6bbb4ee1288c213ad4a3", size = 35279542, upload-time = "2025-07-27T16:30:00.249Z" },
+ { url = "https://files.pythonhosted.org/packages/57/59/0dc3c8b43e118f1e4ee2b798dcc96ac21bb20014e5f1f7a8e85cc0653bdb/scipy-1.16.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8503517c44c18d1030d666cb70aaac1cc8913608816e06742498833b128488b7", size = 35667665, upload-time = "2025-07-27T16:30:05.916Z" },
+ { url = "https://files.pythonhosted.org/packages/45/5f/844ee26e34e2f3f9f8febb9343748e72daeaec64fe0c70e9bf1ff84ec955/scipy-1.16.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:30cc4bb81c41831ecfd6dc450baf48ffd80ef5aed0f5cf3ea775740e80f16ecc", size = 38045210, upload-time = "2025-07-27T16:30:11.655Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/d7/210f2b45290f444f1de64bc7353aa598ece9f0e90c384b4a156f9b1a5063/scipy-1.16.1-cp313-cp313t-win_amd64.whl", hash = "sha256:c24fa02f7ed23ae514460a22c57eca8f530dbfa50b1cfdbf4f37c05b5309cc39", size = 38593661, upload-time = "2025-07-27T16:30:17.825Z" },
+ { url = "https://files.pythonhosted.org/packages/81/ea/84d481a5237ed223bd3d32d6e82d7a6a96e34756492666c260cef16011d1/scipy-1.16.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:796a5a9ad36fa3a782375db8f4241ab02a091308eb079746bc0f874c9b998318", size = 36525921, upload-time = "2025-07-27T16:30:30.081Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/9f/d9edbdeff9f3a664807ae3aea383e10afaa247e8e6255e6d2aa4515e8863/scipy-1.16.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:3ea0733a2ff73fd6fdc5fecca54ee9b459f4d74f00b99aced7d9a3adb43fb1cc", size = 28564152, upload-time = "2025-07-27T16:30:35.336Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/95/8125bcb1fe04bc267d103e76516243e8d5e11229e6b306bda1024a5423d1/scipy-1.16.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:85764fb15a2ad994e708258bb4ed8290d1305c62a4e1ef07c414356a24fcfbf8", size = 20836028, upload-time = "2025-07-27T16:30:39.421Z" },
+ { url = "https://files.pythonhosted.org/packages/77/9c/bf92e215701fc70bbcd3d14d86337cf56a9b912a804b9c776a269524a9e9/scipy-1.16.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:ca66d980469cb623b1759bdd6e9fd97d4e33a9fad5b33771ced24d0cb24df67e", size = 23489666, upload-time = "2025-07-27T16:30:43.663Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/00/5e941d397d9adac41b02839011594620d54d99488d1be5be755c00cde9ee/scipy-1.16.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e7cc1ffcc230f568549fc56670bcf3df1884c30bd652c5da8138199c8c76dae0", size = 33358318, upload-time = "2025-07-27T16:30:48.982Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/87/8db3aa10dde6e3e8e7eb0133f24baa011377d543f5b19c71469cf2648026/scipy-1.16.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3ddfb1e8d0b540cb4ee9c53fc3dea3186f97711248fb94b4142a1b27178d8b4b", size = 35185724, upload-time = "2025-07-27T16:30:54.26Z" },
+ { url = "https://files.pythonhosted.org/packages/89/b4/6ab9ae443216807622bcff02690262d8184078ea467efee2f8c93288a3b1/scipy-1.16.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4dc0e7be79e95d8ba3435d193e0d8ce372f47f774cffd882f88ea4e1e1ddc731", size = 35554335, upload-time = "2025-07-27T16:30:59.765Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/9a/d0e9dc03c5269a1afb60661118296a32ed5d2c24298af61b676c11e05e56/scipy-1.16.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f23634f9e5adb51b2a77766dac217063e764337fbc816aa8ad9aaebcd4397fd3", size = 37960310, upload-time = "2025-07-27T16:31:06.151Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/00/c8f3130a50521a7977874817ca89e0599b1b4ee8e938bad8ae798a0e1f0d/scipy-1.16.1-cp314-cp314-win_amd64.whl", hash = "sha256:57d75524cb1c5a374958a2eae3d84e1929bb971204cc9d52213fb8589183fc19", size = 39319239, upload-time = "2025-07-27T16:31:59.942Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/f2/1ca3eda54c3a7e4c92f6acef7db7b3a057deb135540d23aa6343ef8ad333/scipy-1.16.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:d8da7c3dd67bcd93f15618938f43ed0995982eb38973023d46d4646c4283ad65", size = 36939460, upload-time = "2025-07-27T16:31:11.865Z" },
+ { url = "https://files.pythonhosted.org/packages/80/30/98c2840b293a132400c0940bb9e140171dcb8189588619048f42b2ce7b4f/scipy-1.16.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:cc1d2f2fd48ba1e0620554fe5bc44d3e8f5d4185c8c109c7fbdf5af2792cfad2", size = 29093322, upload-time = "2025-07-27T16:31:17.045Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/e6/1e6e006e850622cf2a039b62d1a6ddc4497d4851e58b68008526f04a9a00/scipy-1.16.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:21a611ced9275cb861bacadbada0b8c0623bc00b05b09eb97f23b370fc2ae56d", size = 21365329, upload-time = "2025-07-27T16:31:21.188Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/02/72a5aa5b820589dda9a25e329ca752842bfbbaf635e36bc7065a9b42216e/scipy-1.16.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:8dfbb25dffc4c3dd9371d8ab456ca81beeaf6f9e1c2119f179392f0dc1ab7695", size = 23897544, upload-time = "2025-07-27T16:31:25.408Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/dc/7122d806a6f9eb8a33532982234bed91f90272e990f414f2830cfe656e0b/scipy-1.16.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f0ebb7204f063fad87fc0a0e4ff4a2ff40b2a226e4ba1b7e34bf4b79bf97cd86", size = 33442112, upload-time = "2025-07-27T16:31:30.62Z" },
+ { url = "https://files.pythonhosted.org/packages/24/39/e383af23564daa1021a5b3afbe0d8d6a68ec639b943661841f44ac92de85/scipy-1.16.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f1b9e5962656f2734c2b285a8745358ecb4e4efbadd00208c80a389227ec61ff", size = 35286594, upload-time = "2025-07-27T16:31:36.112Z" },
+ { url = "https://files.pythonhosted.org/packages/95/47/1a0b0aff40c3056d955f38b0df5d178350c3d74734ec54f9c68d23910be5/scipy-1.16.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e1a106f8c023d57a2a903e771228bf5c5b27b5d692088f457acacd3b54511e4", size = 35665080, upload-time = "2025-07-27T16:31:42.025Z" },
+ { url = "https://files.pythonhosted.org/packages/64/df/ce88803e9ed6e27fe9b9abefa157cf2c80e4fa527cf17ee14be41f790ad4/scipy-1.16.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:709559a1db68a9abc3b2c8672c4badf1614f3b440b3ab326d86a5c0491eafae3", size = 38050306, upload-time = "2025-07-27T16:31:48.109Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/6c/a76329897a7cae4937d403e623aa6aaea616a0bb5b36588f0b9d1c9a3739/scipy-1.16.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c0c804d60492a0aad7f5b2bb1862f4548b990049e27e828391ff2bf6f7199998", size = 39427705, upload-time = "2025-07-27T16:31:53.96Z" },
+]
+
+[[package]]
+name = "scipy-stubs"
+version = "1.16.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "optype", extra = ["numpy"] },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e5/27/bc9a81cabc1cb24aca77d221d70e9d62677b984da9a20292511b80183895/scipy_stubs-1.16.1.0.tar.gz", hash = "sha256:4251c10d0a0a47b54916c49f62cc7b411ab0f7b78ea3946a156990110982d6d9", size = 344964, upload-time = "2025-08-01T02:41:57.245Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/46/1b/9afd7b859c3067845631c65bff8a1ffa59de122e0af121ff17c476f625cd/scipy_stubs-1.16.1.0-py3-none-any.whl", hash = "sha256:6ce677a485391012cbcc0e28897b5dd6031a1b5dda40b2ceecbbc5d4f9cc03d5", size = 551168, upload-time = "2025-08-01T02:41:55.387Z" },
]
[[package]]
@@ -1906,16 +2013,16 @@ wheels = [
[[package]]
name = "sphinx-jinja2-compat"
-version = "0.3.0"
+version = "0.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jinja2" },
{ name = "markupsafe" },
{ name = "standard-imghdr", marker = "python_full_version >= '3.13'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/26/df/27282da6f8c549f765beca9de1a5fc56f9651ed87711a5cac1e914137753/sphinx_jinja2_compat-0.3.0.tar.gz", hash = "sha256:f3c1590b275f42e7a654e081db5e3e5fb97f515608422bde94015ddf795dfe7c", size = 4998, upload-time = "2024-06-19T10:27:00.781Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/e5/9c/94183ee39fff37685c316e2bdb443e173fd83fd58f430aa3d7c484639b14/sphinx_jinja2_compat-0.4.0.tar.gz", hash = "sha256:61b9cd006ea05aa17db68c5f0d34823c6d08ffc8df60983a632c4c3f46df2f08", size = 5223, upload-time = "2025-08-05T19:02:08.717Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/6f/42/2fd09d672eaaa937d6893d8b747d07943f97a6e5e30653aee6ebd339b704/sphinx_jinja2_compat-0.3.0-py3-none-any.whl", hash = "sha256:b1e4006d8e1ea31013fa9946d1b075b0c8d2a42c6e3425e63542c1e9f8be9084", size = 7883, upload-time = "2024-06-19T10:26:59.121Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/ca/adb63a7b3404e4fc16659e532df2958ca4eea4e3cf881be30d839246ab26/sphinx_jinja2_compat-0.4.0-py3-none-any.whl", hash = "sha256:52cf2626d97aab1eaf005604a3d98edbe2b0f419de57e5fb7f9bb44036eb4f3e", size = 8100, upload-time = "2025-08-05T19:02:07.885Z" },
]
[[package]]
@@ -2015,6 +2122,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" },
]
+[[package]]
+name = "sphinxcontrib-mermaid"
+version = "1.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pyyaml" },
+ { name = "sphinx" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/97/69/bf039237ad260073e8c02f820b3e00dc34f3a2de20aff7861e6b19d2f8c5/sphinxcontrib_mermaid-1.0.0.tar.gz", hash = "sha256:2e8ab67d3e1e2816663f9347d026a8dee4a858acdd4ad32dd1c808893db88146", size = 15153, upload-time = "2024-10-12T16:33:03.863Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cd/c8/784b9ac6ea08aa594c1a4becbd0dbe77186785362e31fd633b8c6ae0197a/sphinxcontrib_mermaid-1.0.0-py3-none-any.whl", hash = "sha256:60b72710ea02087f212028feb09711225fbc2e343a10d34822fe787510e1caa3", size = 9597, upload-time = "2024-10-12T16:33:02.303Z" },
+]
+
[[package]]
name = "sphinxcontrib-qthelp"
version = "2.0.0"
@@ -2121,11 +2241,11 @@ wheels = [
[[package]]
name = "typing-extensions"
-version = "4.14.0"
+version = "4.14.1"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423, upload-time = "2025-06-02T14:52:11.399Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839, upload-time = "2025-06-02T14:52:10.026Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" },
]
[[package]]