| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869 |
- r"""Activation dynamics for musclotendon models.
- Musculotendon models are able to produce active force when they are activated,
- which is when a chemical process has taken place within the muscle fibers
- causing them to voluntarily contract. Biologically this chemical process (the
- diffusion of :math:`\textrm{Ca}^{2+}` ions) is not the input in the system,
- electrical signals from the nervous system are. These are termed excitations.
- Activation dynamics, which relates the normalized excitation level to the
- normalized activation level, can be modeled by the models present in this
- module.
- """
- from abc import ABC, abstractmethod
- from functools import cached_property
- from sympy.core.symbol import Symbol
- from sympy.core.numbers import Float, Integer, Rational
- from sympy.functions.elementary.hyperbolic import tanh
- from sympy.matrices.dense import MutableDenseMatrix as Matrix, zeros
- from sympy.physics.biomechanics._mixin import _NamedMixin
- from sympy.physics.mechanics import dynamicsymbols
- __all__ = [
- 'ActivationBase',
- 'FirstOrderActivationDeGroote2016',
- 'ZerothOrderActivation',
- ]
- class ActivationBase(ABC, _NamedMixin):
- """Abstract base class for all activation dynamics classes to inherit from.
- Notes
- =====
- Instances of this class cannot be directly instantiated by users. However,
- it can be used to created custom activation dynamics types through
- subclassing.
- """
- def __init__(self, name):
- """Initializer for ``ActivationBase``."""
- self.name = str(name)
- # Symbols
- self._e = dynamicsymbols(f"e_{name}")
- self._a = dynamicsymbols(f"a_{name}")
- @classmethod
- @abstractmethod
- def with_defaults(cls, name):
- """Alternate constructor that provides recommended defaults for
- constants."""
- pass
- @property
- def excitation(self):
- """Dynamic symbol representing excitation.
- Explanation
- ===========
- The alias ``e`` can also be used to access the same attribute.
- """
- return self._e
- @property
- def e(self):
- """Dynamic symbol representing excitation.
- Explanation
- ===========
- The alias ``excitation`` can also be used to access the same attribute.
- """
- return self._e
- @property
- def activation(self):
- """Dynamic symbol representing activation.
- Explanation
- ===========
- The alias ``a`` can also be used to access the same attribute.
- """
- return self._a
- @property
- def a(self):
- """Dynamic symbol representing activation.
- Explanation
- ===========
- The alias ``activation`` can also be used to access the same attribute.
- """
- return self._a
- @property
- @abstractmethod
- def order(self):
- """Order of the (differential) equation governing activation."""
- pass
- @property
- @abstractmethod
- def state_vars(self):
- """Ordered column matrix of functions of time that represent the state
- variables.
- Explanation
- ===========
- The alias ``x`` can also be used to access the same attribute.
- """
- pass
- @property
- @abstractmethod
- def x(self):
- """Ordered column matrix of functions of time that represent the state
- variables.
- Explanation
- ===========
- The alias ``state_vars`` can also be used to access the same attribute.
- """
- pass
- @property
- @abstractmethod
- def input_vars(self):
- """Ordered column matrix of functions of time that represent the input
- variables.
- Explanation
- ===========
- The alias ``r`` can also be used to access the same attribute.
- """
- pass
- @property
- @abstractmethod
- def r(self):
- """Ordered column matrix of functions of time that represent the input
- variables.
- Explanation
- ===========
- The alias ``input_vars`` can also be used to access the same attribute.
- """
- pass
- @property
- @abstractmethod
- def constants(self):
- """Ordered column matrix of non-time varying symbols present in ``M``
- and ``F``.
- Only symbolic constants are returned. If a numeric type (e.g. ``Float``)
- has been used instead of ``Symbol`` for a constant then that attribute
- will not be included in the matrix returned by this property. This is
- because the primary use of this property attribute is to provide an
- ordered sequence of the still-free symbols that require numeric values
- during code generation.
- Explanation
- ===========
- The alias ``p`` can also be used to access the same attribute.
- """
- pass
- @property
- @abstractmethod
- def p(self):
- """Ordered column matrix of non-time varying symbols present in ``M``
- and ``F``.
- Only symbolic constants are returned. If a numeric type (e.g. ``Float``)
- has been used instead of ``Symbol`` for a constant then that attribute
- will not be included in the matrix returned by this property. This is
- because the primary use of this property attribute is to provide an
- ordered sequence of the still-free symbols that require numeric values
- during code generation.
- Explanation
- ===========
- The alias ``constants`` can also be used to access the same attribute.
- """
- pass
- @property
- @abstractmethod
- def M(self):
- """Ordered square matrix of coefficients on the LHS of ``M x' = F``.
- Explanation
- ===========
- The square matrix that forms part of the LHS of the linear system of
- ordinary differential equations governing the activation dynamics:
- ``M(x, r, t, p) x' = F(x, r, t, p)``.
- """
- pass
- @property
- @abstractmethod
- def F(self):
- """Ordered column matrix of equations on the RHS of ``M x' = F``.
- Explanation
- ===========
- The column matrix that forms the RHS of the linear system of ordinary
- differential equations governing the activation dynamics:
- ``M(x, r, t, p) x' = F(x, r, t, p)``.
- """
- pass
- @abstractmethod
- def rhs(self):
- """
- Explanation
- ===========
- The solution to the linear system of ordinary differential equations
- governing the activation dynamics:
- ``M(x, r, t, p) x' = F(x, r, t, p)``.
- """
- pass
- def __eq__(self, other):
- """Equality check for activation dynamics."""
- if type(self) != type(other):
- return False
- if self.name != other.name:
- return False
- return True
- def __repr__(self):
- """Default representation of activation dynamics."""
- return f'{self.__class__.__name__}({self.name!r})'
- class ZerothOrderActivation(ActivationBase):
- """Simple zeroth-order activation dynamics mapping excitation to
- activation.
- Explanation
- ===========
- Zeroth-order activation dynamics are useful in instances where you want to
- reduce the complexity of your musculotendon dynamics as they simple map
- exictation to activation. As a result, no additional state equations are
- introduced to your system. They also remove a potential source of delay
- between the input and dynamics of your system as no (ordinary) differential
- equations are involved.
- """
- def __init__(self, name):
- """Initializer for ``ZerothOrderActivation``.
- Parameters
- ==========
- name : str
- The name identifier associated with the instance. Must be a string
- of length at least 1.
- """
- super().__init__(name)
- # Zeroth-order activation dynamics has activation equal excitation so
- # overwrite the symbol for activation with the excitation symbol.
- self._a = self._e
- @classmethod
- def with_defaults(cls, name):
- """Alternate constructor that provides recommended defaults for
- constants.
- Explanation
- ===========
- As this concrete class doesn't implement any constants associated with
- its dynamics, this ``classmethod`` simply creates a standard instance
- of ``ZerothOrderActivation``. An implementation is provided to ensure
- a consistent interface between all ``ActivationBase`` concrete classes.
- """
- return cls(name)
- @property
- def order(self):
- """Order of the (differential) equation governing activation."""
- return 0
- @property
- def state_vars(self):
- """Ordered column matrix of functions of time that represent the state
- variables.
- Explanation
- ===========
- As zeroth-order activation dynamics simply maps excitation to
- activation, this class has no associated state variables and so this
- property return an empty column ``Matrix`` with shape (0, 1).
- The alias ``x`` can also be used to access the same attribute.
- """
- return zeros(0, 1)
- @property
- def x(self):
- """Ordered column matrix of functions of time that represent the state
- variables.
- Explanation
- ===========
- As zeroth-order activation dynamics simply maps excitation to
- activation, this class has no associated state variables and so this
- property return an empty column ``Matrix`` with shape (0, 1).
- The alias ``state_vars`` can also be used to access the same attribute.
- """
- return zeros(0, 1)
- @property
- def input_vars(self):
- """Ordered column matrix of functions of time that represent the input
- variables.
- Explanation
- ===========
- Excitation is the only input in zeroth-order activation dynamics and so
- this property returns a column ``Matrix`` with one entry, ``e``, and
- shape (1, 1).
- The alias ``r`` can also be used to access the same attribute.
- """
- return Matrix([self._e])
- @property
- def r(self):
- """Ordered column matrix of functions of time that represent the input
- variables.
- Explanation
- ===========
- Excitation is the only input in zeroth-order activation dynamics and so
- this property returns a column ``Matrix`` with one entry, ``e``, and
- shape (1, 1).
- The alias ``input_vars`` can also be used to access the same attribute.
- """
- return Matrix([self._e])
- @property
- def constants(self):
- """Ordered column matrix of non-time varying symbols present in ``M``
- and ``F``.
- Only symbolic constants are returned. If a numeric type (e.g. ``Float``)
- has been used instead of ``Symbol`` for a constant then that attribute
- will not be included in the matrix returned by this property. This is
- because the primary use of this property attribute is to provide an
- ordered sequence of the still-free symbols that require numeric values
- during code generation.
- Explanation
- ===========
- As zeroth-order activation dynamics simply maps excitation to
- activation, this class has no associated constants and so this property
- return an empty column ``Matrix`` with shape (0, 1).
- The alias ``p`` can also be used to access the same attribute.
- """
- return zeros(0, 1)
- @property
- def p(self):
- """Ordered column matrix of non-time varying symbols present in ``M``
- and ``F``.
- Only symbolic constants are returned. If a numeric type (e.g. ``Float``)
- has been used instead of ``Symbol`` for a constant then that attribute
- will not be included in the matrix returned by this property. This is
- because the primary use of this property attribute is to provide an
- ordered sequence of the still-free symbols that require numeric values
- during code generation.
- Explanation
- ===========
- As zeroth-order activation dynamics simply maps excitation to
- activation, this class has no associated constants and so this property
- return an empty column ``Matrix`` with shape (0, 1).
- The alias ``constants`` can also be used to access the same attribute.
- """
- return zeros(0, 1)
- @property
- def M(self):
- """Ordered square matrix of coefficients on the LHS of ``M x' = F``.
- Explanation
- ===========
- The square matrix that forms part of the LHS of the linear system of
- ordinary differential equations governing the activation dynamics:
- ``M(x, r, t, p) x' = F(x, r, t, p)``.
- As zeroth-order activation dynamics have no state variables, this
- linear system has dimension 0 and therefore ``M`` is an empty square
- ``Matrix`` with shape (0, 0).
- """
- return Matrix([])
- @property
- def F(self):
- """Ordered column matrix of equations on the RHS of ``M x' = F``.
- Explanation
- ===========
- The column matrix that forms the RHS of the linear system of ordinary
- differential equations governing the activation dynamics:
- ``M(x, r, t, p) x' = F(x, r, t, p)``.
- As zeroth-order activation dynamics have no state variables, this
- linear system has dimension 0 and therefore ``F`` is an empty column
- ``Matrix`` with shape (0, 1).
- """
- return zeros(0, 1)
- def rhs(self):
- """Ordered column matrix of equations for the solution of ``M x' = F``.
- Explanation
- ===========
- The solution to the linear system of ordinary differential equations
- governing the activation dynamics:
- ``M(x, r, t, p) x' = F(x, r, t, p)``.
- As zeroth-order activation dynamics have no state variables, this
- linear has dimension 0 and therefore this method returns an empty
- column ``Matrix`` with shape (0, 1).
- """
- return zeros(0, 1)
- class FirstOrderActivationDeGroote2016(ActivationBase):
- r"""First-order activation dynamics based on De Groote et al., 2016 [1]_.
- Explanation
- ===========
- Gives the first-order activation dynamics equation for the rate of change
- of activation with respect to time as a function of excitation and
- activation.
- The function is defined by the equation:
- .. math::
- \frac{da}{dt} = \left(\frac{\frac{1}{2} + a0}{\tau_a \left(\frac{1}{2}
- + \frac{3a}{2}\right)} + \frac{\left(\frac{1}{2}
- + \frac{3a}{2}\right) \left(\frac{1}{2} - a0\right)}{\tau_d}\right)
- \left(e - a\right)
- where
- .. math::
- a0 = \frac{\tanh{\left(b \left(e - a\right) \right)}}{2}
- with constant values of :math:`tau_a = 0.015`, :math:`tau_d = 0.060`, and
- :math:`b = 10`.
- References
- ==========
- .. [1] De Groote, F., Kinney, A. L., Rao, A. V., & Fregly, B. J., Evaluation
- of direct collocation optimal control problem formulations for
- solving the muscle redundancy problem, Annals of biomedical
- engineering, 44(10), (2016) pp. 2922-2936
- """
- def __init__(self,
- name,
- activation_time_constant=None,
- deactivation_time_constant=None,
- smoothing_rate=None,
- ):
- """Initializer for ``FirstOrderActivationDeGroote2016``.
- Parameters
- ==========
- activation time constant : Symbol | Number | None
- The value of the activation time constant governing the delay
- between excitation and activation when excitation exceeds
- activation.
- deactivation time constant : Symbol | Number | None
- The value of the deactivation time constant governing the delay
- between excitation and activation when activation exceeds
- excitation.
- smoothing_rate : Symbol | Number | None
- The slope of the hyperbolic tangent function used to smooth between
- the switching of the equations where excitation exceed activation
- and where activation exceeds excitation. The recommended value to
- use is ``10``, but values between ``0.1`` and ``100`` can be used.
- """
- super().__init__(name)
- # Symbols
- self.activation_time_constant = activation_time_constant
- self.deactivation_time_constant = deactivation_time_constant
- self.smoothing_rate = smoothing_rate
- @classmethod
- def with_defaults(cls, name):
- r"""Alternate constructor that will use the published constants.
- Explanation
- ===========
- Returns an instance of ``FirstOrderActivationDeGroote2016`` using the
- three constant values specified in the original publication.
- These have the values:
- :math:`tau_a = 0.015`
- :math:`tau_d = 0.060`
- :math:`b = 10`
- """
- tau_a = Float('0.015')
- tau_d = Float('0.060')
- b = Float('10.0')
- return cls(name, tau_a, tau_d, b)
- @property
- def activation_time_constant(self):
- """Delay constant for activation.
- Explanation
- ===========
- The alias ```tau_a`` can also be used to access the same attribute.
- """
- return self._tau_a
- @activation_time_constant.setter
- def activation_time_constant(self, tau_a):
- if hasattr(self, '_tau_a'):
- msg = (
- f'Can\'t set attribute `activation_time_constant` to '
- f'{repr(tau_a)} as it is immutable and already has value '
- f'{self._tau_a}.'
- )
- raise AttributeError(msg)
- self._tau_a = Symbol(f'tau_a_{self.name}') if tau_a is None else tau_a
- @property
- def tau_a(self):
- """Delay constant for activation.
- Explanation
- ===========
- The alias ``activation_time_constant`` can also be used to access the
- same attribute.
- """
- return self._tau_a
- @property
- def deactivation_time_constant(self):
- """Delay constant for deactivation.
- Explanation
- ===========
- The alias ``tau_d`` can also be used to access the same attribute.
- """
- return self._tau_d
- @deactivation_time_constant.setter
- def deactivation_time_constant(self, tau_d):
- if hasattr(self, '_tau_d'):
- msg = (
- f'Can\'t set attribute `deactivation_time_constant` to '
- f'{repr(tau_d)} as it is immutable and already has value '
- f'{self._tau_d}.'
- )
- raise AttributeError(msg)
- self._tau_d = Symbol(f'tau_d_{self.name}') if tau_d is None else tau_d
- @property
- def tau_d(self):
- """Delay constant for deactivation.
- Explanation
- ===========
- The alias ``deactivation_time_constant`` can also be used to access the
- same attribute.
- """
- return self._tau_d
- @property
- def smoothing_rate(self):
- """Smoothing constant for the hyperbolic tangent term.
- Explanation
- ===========
- The alias ``b`` can also be used to access the same attribute.
- """
- return self._b
- @smoothing_rate.setter
- def smoothing_rate(self, b):
- if hasattr(self, '_b'):
- msg = (
- f'Can\'t set attribute `smoothing_rate` to {b!r} as it is '
- f'immutable and already has value {self._b!r}.'
- )
- raise AttributeError(msg)
- self._b = Symbol(f'b_{self.name}') if b is None else b
- @property
- def b(self):
- """Smoothing constant for the hyperbolic tangent term.
- Explanation
- ===========
- The alias ``smoothing_rate`` can also be used to access the same
- attribute.
- """
- return self._b
- @property
- def order(self):
- """Order of the (differential) equation governing activation."""
- return 1
- @property
- def state_vars(self):
- """Ordered column matrix of functions of time that represent the state
- variables.
- Explanation
- ===========
- The alias ``x`` can also be used to access the same attribute.
- """
- return Matrix([self._a])
- @property
- def x(self):
- """Ordered column matrix of functions of time that represent the state
- variables.
- Explanation
- ===========
- The alias ``state_vars`` can also be used to access the same attribute.
- """
- return Matrix([self._a])
- @property
- def input_vars(self):
- """Ordered column matrix of functions of time that represent the input
- variables.
- Explanation
- ===========
- The alias ``r`` can also be used to access the same attribute.
- """
- return Matrix([self._e])
- @property
- def r(self):
- """Ordered column matrix of functions of time that represent the input
- variables.
- Explanation
- ===========
- The alias ``input_vars`` can also be used to access the same attribute.
- """
- return Matrix([self._e])
- @property
- def constants(self):
- """Ordered column matrix of non-time varying symbols present in ``M``
- and ``F``.
- Only symbolic constants are returned. If a numeric type (e.g. ``Float``)
- has been used instead of ``Symbol`` for a constant then that attribute
- will not be included in the matrix returned by this property. This is
- because the primary use of this property attribute is to provide an
- ordered sequence of the still-free symbols that require numeric values
- during code generation.
- Explanation
- ===========
- The alias ``p`` can also be used to access the same attribute.
- """
- constants = [self._tau_a, self._tau_d, self._b]
- symbolic_constants = [c for c in constants if not c.is_number]
- return Matrix(symbolic_constants) if symbolic_constants else zeros(0, 1)
- @property
- def p(self):
- """Ordered column matrix of non-time varying symbols present in ``M``
- and ``F``.
- Explanation
- ===========
- Only symbolic constants are returned. If a numeric type (e.g. ``Float``)
- has been used instead of ``Symbol`` for a constant then that attribute
- will not be included in the matrix returned by this property. This is
- because the primary use of this property attribute is to provide an
- ordered sequence of the still-free symbols that require numeric values
- during code generation.
- The alias ``constants`` can also be used to access the same attribute.
- """
- constants = [self._tau_a, self._tau_d, self._b]
- symbolic_constants = [c for c in constants if not c.is_number]
- return Matrix(symbolic_constants) if symbolic_constants else zeros(0, 1)
- @property
- def M(self):
- """Ordered square matrix of coefficients on the LHS of ``M x' = F``.
- Explanation
- ===========
- The square matrix that forms part of the LHS of the linear system of
- ordinary differential equations governing the activation dynamics:
- ``M(x, r, t, p) x' = F(x, r, t, p)``.
- """
- return Matrix([Integer(1)])
- @property
- def F(self):
- """Ordered column matrix of equations on the RHS of ``M x' = F``.
- Explanation
- ===========
- The column matrix that forms the RHS of the linear system of ordinary
- differential equations governing the activation dynamics:
- ``M(x, r, t, p) x' = F(x, r, t, p)``.
- """
- return Matrix([self._da_eqn])
- def rhs(self):
- """Ordered column matrix of equations for the solution of ``M x' = F``.
- Explanation
- ===========
- The solution to the linear system of ordinary differential equations
- governing the activation dynamics:
- ``M(x, r, t, p) x' = F(x, r, t, p)``.
- """
- return Matrix([self._da_eqn])
- @cached_property
- def _da_eqn(self):
- HALF = Rational(1, 2)
- a0 = HALF * tanh(self._b * (self._e - self._a))
- a1 = (HALF + Rational(3, 2) * self._a)
- a2 = (HALF + a0) / (self._tau_a * a1)
- a3 = a1 * (HALF - a0) / self._tau_d
- activation_dynamics_equation = (a2 + a3) * (self._e - self._a)
- return activation_dynamics_equation
- def __eq__(self, other):
- """Equality check for ``FirstOrderActivationDeGroote2016``."""
- if type(self) != type(other):
- return False
- self_attrs = (self.name, self.tau_a, self.tau_d, self.b)
- other_attrs = (other.name, other.tau_a, other.tau_d, other.b)
- if self_attrs == other_attrs:
- return True
- return False
- def __repr__(self):
- """Representation of ``FirstOrderActivationDeGroote2016``."""
- return (
- f'{self.__class__.__name__}({self.name!r}, '
- f'activation_time_constant={self.tau_a!r}, '
- f'deactivation_time_constant={self.tau_d!r}, '
- f'smoothing_rate={self.b!r})'
- )
|