Source code for pymuscle.potvin_2017_muscle_fibers

import numpy as np
import math # noqa
from numpy import ndarray
from copy import copy

from .model import Model


[docs]class Potvin2017MuscleFibers(Model): """ Encapsulates the muscle fibers portions of the motor unit model. The name of each parameter as it appears in Potvin, 2017 is in parentheses. If a parameter does not appear in the paper but does appear in the Matlab code, the variable name from the Matlab code is in parentheses. :param motor_unit_count: Number of motor units in the muscle (n) :param max_twitch_amplitude: Max twitch force within the pool (RP) :param max_contraction_time: [milliseconds] Maximum contraction time for a motor unit (tL) :param contraction_time_range: The scale between the fastest contraction time and the slowest (rt) :fatigue_factor_first_unit: The nominal fatigability of the first motor unit in percent / second :fatigability_range: The scale between the fatigability of the first motor unit and the last :contraction_time_change_ratio: For each percent of force lost during fatigue, what percentage should contraction increase? Based on Shields et al (1997) .. todo:: The argument naming isn't consistent. Sometimes we use 'max' and other times we use 'last unit'. Can these be made consistent? Usage:: from pymuscle import Potvin2017MuscleFibers as Fibers motor_unit_count = 60 fibers = Fibers(motor_unit_count) motor_neuron_firing_rates = np.rand(motor_unit_count) * 10.0 force = fibers.step(motor_neuron_firing_rates) """ def __init__( self, motor_unit_count: int, max_twitch_amplitude: int = 100, max_contraction_time: int = 90, contraction_time_range: int = 3, max_recruitment_threshold: int = 50, fatigue_factor_first_unit: float = 0.0125, max_fatigue_rate: float = 0.0225, fatigability_range: int = 180, contraction_time_change_ratio: float = 0.379, apply_fatigue: bool = True ): self._peak_twitch_forces = self._calc_peak_twitch_forces( motor_unit_count, max_twitch_amplitude ) # These will change with fatigue. self._current_peak_forces = copy(self._peak_twitch_forces) self._contraction_times = self._calc_contraction_times( max_twitch_amplitude, max_contraction_time, contraction_time_range, self._peak_twitch_forces ) # These will change with fatigue self._current_contraction_times = copy(self._contraction_times) # The maximum rates at which motor units will fatigue self._nominal_fatigabilities = self._calc_nominal_fatigabilities( motor_unit_count, fatigability_range, max_fatigue_rate, self._peak_twitch_forces ) # Assing other non-public attributes self._contraction_time_change_ratio = contraction_time_change_ratio self._apply_fatigue = apply_fatigue # Assign public attributes self.motor_unit_count = motor_unit_count self.current_forces = None def _update_fatigue( self, normalized_forces: ndarray, step_size: float ) -> None: """ Updates current twitch forces and contraction times. :param normalized_forces: Array of scaled forces. Used to weight how much fatigue will be generated in this step. :param step_size: How far time has advanced in this step. """ # Instantaneous fatigue rate fatigues = (self._nominal_fatigabilities * normalized_forces) * step_size self._current_peak_forces -= fatigues # Zero out negative values self._current_peak_forces[self._current_peak_forces < 0] = 0.0 self._update_contraction_times() def _apply_recovery(self): """ Updates twitch forces and contraction times. """ raise NotImplementedError def _update_contraction_times(self) -> None: """ Update our current contraction times as a function of our current force capacity relative to our peak force capacity. From Eq. (11) """ force_loss_pcts = 1 - (self._current_peak_forces / self._peak_twitch_forces) inc_pcts = 1 + self._contraction_time_change_ratio * force_loss_pcts self._current_contraction_times = self._contraction_times * inc_pcts @staticmethod def _calc_contraction_times( max_twitch_amplitude: int, max_contraction_time: int, contraction_time_range: int, peak_twitch_forces: ndarray ) -> ndarray: """ Calculate the contraction times for each motor unit Results in a smooth range from max_contraction_time at the first motor unit down to max_contraction_time / contraction_time range for the last motor unit :param max_twitch_amplitude: Largest force a motor unit in this muscle can produce. :param max_contraction_time: Slowest contraction time for a motor unit in this muscle. :param contraction_time_range: The ratio between the slowest and the fastest contraction times in this muscle. :param peak_twitch_forces: An array of all the largest forces that each motor unit can produce. """ # Fuglevand 93 version - very slightly different values # twitch_force_range = peak_twitch_forces[-1] / peak_twitch_forces[0] # scale = math.log(twitch_force_range, contraction_time_range) # Potvin 2017 version scale = np.log(max_twitch_amplitude) / np.log(contraction_time_range) mantissa = 1 / peak_twitch_forces exponent = 1 / scale return max_contraction_time * np.power(mantissa, exponent) @staticmethod def _calc_peak_twitch_forces( motor_unit_count: int, max_twitch_amplitude: int ) -> ndarray: """ Pure function to calculate the peak twitch force for each motor unit. :param motor_unit_count: The number of motor units in the pool. :param max_twitch_amplitude: Largest force a motor unit in this muscle can produce. """ motor_unit_indices = np.arange(1, motor_unit_count + 1) t_log = np.log(max_twitch_amplitude) t_exponent = (t_log * (motor_unit_indices - 1)) / (motor_unit_count - 1) return np.exp(t_exponent) @staticmethod def _calc_nominal_fatigabilities( motor_unit_count: int, fatigability_range: int, max_fatigue_rate: float, peak_twitch_forces: ndarray ) -> ndarray: """ Pure function to calculate nominal fatigue factors for each motor unit. Taken more from the matlab code than the paper. :param motor_unit_count: The number of motor units in this muscle. :param fatigability_range: The ratio between the maximum fatigue rate of the strongest motor unit (which fatigues the fastest) and the fatigue rate of the weakest motor unit. :param max_fatigue_rate: Largest percentage drop per unit time of twitch strength for the strongest motor unit. :param peak_twitch_forces: An array of all the largest forces that each motor unit can produce. """ motor_unit_indices = np.arange(1, motor_unit_count + 1) f_log = np.log(fatigability_range) motor_unit_fatigue_curve = np.exp((f_log / (motor_unit_count - 1)) * (motor_unit_indices - 1)) fatigue_rates = motor_unit_fatigue_curve * (max_fatigue_rate / fatigability_range) * peak_twitch_forces return fatigue_rates def _normalize_firing_rates(self, firing_rates: ndarray) -> ndarray: """ Calculate the effective impact of a given set of firing rates on muscle fibers which have diverse contraction times and may be fatigued. :param firing_rates: Should be the result of pool._calc_adapted_firing_rates() """ # Divide by 1000 here as firing rates are per second where contraction # times are in milliseconds. return self._current_contraction_times * (firing_rates / 1000) @staticmethod def _calc_normalized_forces(normalized_firing_rates: ndarray) -> ndarray: """ Calculate motor unit force, relative to its peak force. Force grows in a linear fashion up to 0.4 normalized firing rate and then in a sigmoid curve afterward. :param normalized_firing_rates: An array of firing rates scaled by the current contraction times for each motor unit. """ normalized_forces = copy(normalized_firing_rates) linear_threshold = 0.4 # Values are non-linear above this value below_thresh_indices = normalized_forces <= linear_threshold above_thresh_indices = normalized_forces > linear_threshold # The next two lines are strange and magical # In the paper they are simplified to *= 0.3 # This is the equivalent of the Matlab code normalized_forces[below_thresh_indices] /= 0.4 normalized_forces[below_thresh_indices] *= 1 - np.exp(-2 * (0.4 ** 3)) exponent = -2 * np.power( normalized_forces[above_thresh_indices], 3 ) normalized_forces[above_thresh_indices] = 1 - np.exp(exponent) return normalized_forces def _calc_current_forces(self, normalized_forces: ndarray) -> ndarray: """ Scales the normalized forces for each motor unit by their current remaining twitch force capacity. This method also updates the public Muscle.current_forces array. :param normalized_forces: An array of forces scaled between 0 and 1 """ self.current_forces = normalized_forces * self._current_peak_forces return self.current_forces def _calc_total_fiber_force( self, firing_rates: ndarray, step_size: float ) -> ndarray: """ Calculates the total instantaneous force produced by all fibers for the given instantaneous firing rates. :param firing_rates: An array of firing rates calculated by a compatible Pool class. :param step_size: How far time has advanced in this step. """ normalized_firing_rates = self._normalize_firing_rates(firing_rates) normalized_forces = self._calc_normalized_forces(normalized_firing_rates) current_forces = self._calc_current_forces(normalized_forces) # Apply fatigue as last step if self._apply_fatigue: self._update_fatigue(normalized_forces, step_size) total_force = np.sum(current_forces) return total_force
[docs] def step( self, motor_pool_output: ndarray, step_size: float = 0.1 ) -> float: """ Advance the muscle fibers simulation one step. Returns the total instantaneous force produced by all fibers for the given input from the motor neuron pool. :param motor_pool_output: An array of firing rates calculated by a compatible Pool class. :param step_size: How far time has advanced in this step. """ assert (len(motor_pool_output) == self.motor_unit_count) return self._calc_total_fiber_force(motor_pool_output, step_size)