Source code for myogen.simulator.core.physiological_distribution

from typing import Literal

import numpy as np
from beartype.typing import Iterator

from myogen.utils.decorators import beartowertype
from myogen.utils.types import RECRUITMENT_THRESHOLDS__ARRAY


[docs] @beartowertype class RecruitmentThresholds: """ Motor unit recruitment threshold generator using physiological models. This class computes recruitment thresholds for motor unit pools using different established models from the literature. """
[docs] def __init__( self, N: int, recruitment_range__ratio: float, deluca__slope: float | None = None, konstantin__max_threshold__ratio: float = 1.0, mode: Literal["fuglevand", "deluca", "konstantin", "combined"] = "konstantin", ) -> None: r""" Generate recruitment thresholds for a pool of motor units using different models. This class computes the recruitment thresholds (and zero-based thresholds) for a pool of N motor units according to one of several models from the literature. The distribution of thresholds is controlled by the recruitment range (RR) and, for some models, additional parameters. Following models are available: - Fuglevand et al. (1993) [1]_ - De Luca & Contessa (2012) [2]_ - Konstantin et al. (2020) [3]_ - Combined model Parameters ---------- N : int Number of motor units in the pool. recruitment_range__ratio : float Recruitment range (dimensionless ratio), defined as the ratio of the largest to smallest threshold :math:`(rt(N)/rt(1))`. deluca__slope : float, optional Dimensionless slope parameter for the ``'deluca'`` mode. Required if ``mode='deluca'``. Controls the curvature of the threshold distribution. Typical values range from 0.001-100. konstantin__max_threshold__ratio : float, optional Maximum recruitment threshold (dimensionless ratio) for the ``'konstantin'`` mode. Required if ``mode='konstantin'``. Sets the absolute scale of all thresholds. Default is 1.0. mode : RecruitmentMode, optional Model to use for threshold generation. One of ``'fuglevand'``, ``'deluca'``, ``'konstantin'``, or ``'combined'``. Default is ``'konstantin'``. Attributes ---------- rt : RECRUITMENT_THRESHOLDS__ARRAY Recruitment thresholds for each motor unit (shape: (N,)). Values are monotonically increasing from ``rt[0]`` to ``rt[N-1]``. rtz : RECRUITMENT_THRESHOLDS__ARRAY Zero-based recruitment thresholds where :math:`rtz[0] = 0` (shape: (N,)). Computed as :math:`rtz = rt - rt[0]`, convenient for simulation. Raises ------ ValueError If a required mode-specific parameter is not provided or if an unknown mode is specified. References ---------- .. [1] Fuglevand, A.J., Winter, D.A., Patla, A.E., 1993. Models of recruitment and rate coding organization in motor-unit pools. Journal of Neurophysiology 70, 2470-2488. https://doi.org/10.1152/jn.1993.70.6.2470 .. [2] De Luca, C.J., Contessa, P., 2012. Hierarchical control of motor units in voluntary contractions. Journal of Neurophysiology 107, 178-195. https://doi.org/10.1152/jn.00961.2010 .. [3] Konstantin, A., Yu, T., Le Carpentier, E., Aoustin, Y., Farina, D., 2020. Simulation of Motor Unit Action Potential Recordings From Intramuscular Multichannel Scanning Electrodes. IEEE Transactions on Biomedical Engineering 67, 2005-2014. https://doi.org/10.1109/TBME.2019.2953680 Notes ----- **fuglevand** : Fuglevand et al. (1993) [1]_ exponential model .. math:: rt(i) = \\exp( \\frac{i \\cdot \\ln(RR)}{N} ) / 100 where :math:`i = 1, 2, \\ldots, N` **deluca** : De Luca & Contessa (2012) [2]_ model with slope correction .. math:: rt(i) = \\frac{b \\cdot i}{N} \\cdot \\exp\\left(\\frac{i \\cdot \\ln(RR / b)}{N}\\right) / 100 where :math:`b` = ``deluca__slope``, :math:`i = 1, 2, \\ldots, N` **konstantin** : Konstantin et al. (2020) [3]_ model allowing explicit maximum threshold control .. math:: rt(i) &= \\frac{RT_{max}}{RR} \\cdot \\exp\\left(\\frac{(i - 1) \\cdot \\ln(RR)}{N - 1}\\right) \\\\ rtz(i) &= \\frac{RT_{max}}{RR} \\cdot \\left(\\exp\\left(\\frac{(i - 1) \\cdot \\ln(RR + 1)}{N}\\right) - 1\\right) where :math:`RT_{max}` = ``konstantin__max_threshold__ratio``, :math:`i = 1, 2, \\ldots, N` **combined** : A corrected De Luca model that uses the slope parameter for shape control but properly respects the RR constraint and maximum threshold like the Konstantin model .. math:: rt(i) = \\frac{RT_{max}}{RR} + \\left(\\frac{b \\cdot i}{N} \\cdot \\exp\\left(\\frac{i \\cdot \\ln(RR / b)}{N}\\right) - \\frac{RT_{max}}{RR}\\right) \\cdot \\left(\\frac{RT_{max} - RT_{max}/RR}{b \\cdot N \\cdot \\exp\\left(\\frac{i \\cdot \\ln(RR / b)}{N}\\right) - \\frac{RT_{max}}{RR}}\\right) where :math:`b` = ``deluca__slope``, :math:`RT_{max}` = ``konstantin__max_threshold__ratio``, :math:`i = 1, 2, \\ldots, N` Examples -------- >>> # Generate thresholds using Fuglevand model >>> thresholds = RecruitmentThresholds( ... N=100, recruitment_range__ratio=50.0, mode='fuglevand' ... ) >>> rt, rtz = thresholds # Tuple unpacking works >>> # Or access directly >>> rt = thresholds.rt >>> rtz = thresholds.rtz >>> >>> # Generate thresholds using Konstantin model with explicit max threshold >>> thresholds = RecruitmentThresholds( ... N=100, recruitment_range__ratio=50.0, konstantin__max_threshold__ratio=1.0, mode='konstantin' ... ) >>> rt, rtz = thresholds """ # Store immutable public parameters (for joblib serialization) self.N = N self.recruitment_range__ratio = recruitment_range__ratio self.deluca__slope = deluca__slope self.konstantin__max_threshold__ratio = konstantin__max_threshold__ratio self.mode = mode # Generate and store thresholds self.rt, self.rtz = self._generate_thresholds()
def __iter__(self) -> Iterator[RECRUITMENT_THRESHOLDS__ARRAY]: """Allow tuple unpacking: rt, rtz = thresholds""" return iter((self.rt, self.rtz)) def __getitem__(self, index: int) -> RECRUITMENT_THRESHOLDS__ARRAY: """Allow indexing: thresholds[0] -> rt, thresholds[1] -> rtz""" if index == 0: return self.rt elif index == 1: return self.rtz else: raise IndexError("Index out of range. Use 0 for rt and 1 for rtz.") def _generate_thresholds( self, ) -> tuple[RECRUITMENT_THRESHOLDS__ARRAY, RECRUITMENT_THRESHOLDS__ARRAY]: """Generate recruitment thresholds based on the specified model.""" match self.mode: case "fuglevand": rt, rtz = self._fuglevand_model() case "deluca": rt, rtz = self._deluca_model() case "konstantin": rt, rtz = self._konstantin_model() case "combined": rt, rtz = self._combined_model() case _: raise ValueError(f"Unknown mode: {self.mode}") # Normalize the thresholds to the maximum threshold rtz = rtz * np.max(rt) / np.max(rtz) return rt, rtz def _fuglevand_model( self, ) -> tuple[RECRUITMENT_THRESHOLDS__ARRAY, RECRUITMENT_THRESHOLDS__ARRAY]: i = np.arange(self.N) + 1 rt = np.exp((np.log(self.recruitment_range__ratio) / self.N) * i) / 100 rtz = rt - rt[0] return rt, rtz def _deluca_model( self, ) -> tuple[RECRUITMENT_THRESHOLDS__ARRAY, RECRUITMENT_THRESHOLDS__ARRAY]: if self.deluca__slope is None: raise ValueError("deluca__slope must be provided for 'deluca' mode.") i = np.arange(self.N) + 1 rt = ( (self.deluca__slope * i / self.N) * np.exp((np.log(self.recruitment_range__ratio / self.deluca__slope) / self.N) * i) / 100 ) rtz = rt - rt[0] return rt, rtz def _konstantin_model( self, ) -> tuple[RECRUITMENT_THRESHOLDS__ARRAY, RECRUITMENT_THRESHOLDS__ARRAY]: if self.konstantin__max_threshold__ratio is None: raise ValueError( "konstantin__max_threshold__ratio must be provided for 'konstantin' mode." ) i = np.arange(self.N) + 1 rt = (self.konstantin__max_threshold__ratio / self.recruitment_range__ratio) * np.exp( (i - 1) * np.log(self.recruitment_range__ratio) / (self.N - 1) ) rtz = (self.konstantin__max_threshold__ratio / self.recruitment_range__ratio) * ( np.exp((i - 1) * np.log(self.recruitment_range__ratio + 1) / self.N) - 1 ) return rt, rtz def _combined_model( self, ) -> tuple[RECRUITMENT_THRESHOLDS__ARRAY, RECRUITMENT_THRESHOLDS__ARRAY]: if self.deluca__slope is None or self.konstantin__max_threshold__ratio is None: raise ValueError( "Both deluca__slope and konstantin__max_threshold__ratio must be provided for 'combined' mode." ) i = np.arange(self.N) + 1 # Create a De Luca-style curve with slope parameter controlling curvature # but properly scaled to respect RR and max threshold # Generate base De Luca shape (without the /100 scaling) base_shape = (self.deluca__slope * i / self.N) * np.exp( (np.log(self.recruitment_range__ratio / self.deluca__slope) / self.N) * i ) # Scale to ensure exact RR and max threshold # We want: rt[0] = konstantin__max_threshold__ratio / RR and rt[-1] = konstantin__max_threshold__ratio min_val = base_shape[0] max_val = base_shape[-1] # Scale the shape to achieve the desired RR while respecting max threshold rt = (self.konstantin__max_threshold__ratio / self.recruitment_range__ratio) + ( base_shape - min_val ) * ( self.konstantin__max_threshold__ratio - self.konstantin__max_threshold__ratio / self.recruitment_range__ratio ) / (max_val - min_val) rtz = rt - rt[0] return rt, rtz