add read me
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
"""Biomechanics extension for SymPy.
|
||||
|
||||
Includes biomechanics-related constructs which allows users to extend multibody
|
||||
models created using `sympy.physics.mechanics` into biomechanical or
|
||||
musculoskeletal models involding musculotendons and activation dynamics.
|
||||
|
||||
"""
|
||||
|
||||
from .activation import (
|
||||
ActivationBase,
|
||||
FirstOrderActivationDeGroote2016,
|
||||
ZerothOrderActivation,
|
||||
)
|
||||
from .curve import (
|
||||
CharacteristicCurveCollection,
|
||||
CharacteristicCurveFunction,
|
||||
FiberForceLengthActiveDeGroote2016,
|
||||
FiberForceLengthPassiveDeGroote2016,
|
||||
FiberForceLengthPassiveInverseDeGroote2016,
|
||||
FiberForceVelocityDeGroote2016,
|
||||
FiberForceVelocityInverseDeGroote2016,
|
||||
TendonForceLengthDeGroote2016,
|
||||
TendonForceLengthInverseDeGroote2016,
|
||||
)
|
||||
from .musculotendon import (
|
||||
MusculotendonBase,
|
||||
MusculotendonDeGroote2016,
|
||||
MusculotendonFormulation,
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
# Musculotendon characteristic curve functions
|
||||
'CharacteristicCurveCollection',
|
||||
'CharacteristicCurveFunction',
|
||||
'FiberForceLengthActiveDeGroote2016',
|
||||
'FiberForceLengthPassiveDeGroote2016',
|
||||
'FiberForceLengthPassiveInverseDeGroote2016',
|
||||
'FiberForceVelocityDeGroote2016',
|
||||
'FiberForceVelocityInverseDeGroote2016',
|
||||
'TendonForceLengthDeGroote2016',
|
||||
'TendonForceLengthInverseDeGroote2016',
|
||||
|
||||
# Activation dynamics classes
|
||||
'ActivationBase',
|
||||
'FirstOrderActivationDeGroote2016',
|
||||
'ZerothOrderActivation',
|
||||
|
||||
# Musculotendon classes
|
||||
'MusculotendonBase',
|
||||
'MusculotendonDeGroote2016',
|
||||
'MusculotendonFormulation',
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,53 @@
|
||||
"""Mixin classes for sharing functionality between unrelated classes.
|
||||
|
||||
This module is named with a leading underscore to signify to users that it's
|
||||
"private" and only intended for internal use by the biomechanics module.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
__all__ = ['_NamedMixin']
|
||||
|
||||
|
||||
class _NamedMixin:
|
||||
"""Mixin class for adding `name` properties.
|
||||
|
||||
Valid names, as will typically be used by subclasses as a suffix when
|
||||
naming automatically-instantiated symbol attributes, must be nonzero length
|
||||
strings.
|
||||
|
||||
Attributes
|
||||
==========
|
||||
|
||||
name : str
|
||||
The name identifier associated with the instance. Must be a string of
|
||||
length at least 1.
|
||||
|
||||
"""
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""The name associated with the class instance."""
|
||||
return self._name
|
||||
|
||||
@name.setter
|
||||
def name(self, name: str) -> None:
|
||||
if hasattr(self, '_name'):
|
||||
msg = (
|
||||
f'Can\'t set attribute `name` to {repr(name)} as it is '
|
||||
f'immutable.'
|
||||
)
|
||||
raise AttributeError(msg)
|
||||
if not isinstance(name, str):
|
||||
msg = (
|
||||
f'Name {repr(name)} passed to `name` was of type '
|
||||
f'{type(name)}, must be {str}.'
|
||||
)
|
||||
raise TypeError(msg)
|
||||
if name in {''}:
|
||||
msg = (
|
||||
f'Name {repr(name)} is invalid, must be a nonzero length '
|
||||
f'{type(str)}.'
|
||||
)
|
||||
raise ValueError(msg)
|
||||
self._name = name
|
||||
@@ -0,0 +1,869 @@
|
||||
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})'
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,348 @@
|
||||
"""Tests for the ``sympy.physics.biomechanics.activation.py`` module."""
|
||||
|
||||
import pytest
|
||||
|
||||
from sympy import Symbol
|
||||
from sympy.core.numbers import Float, Integer, Rational
|
||||
from sympy.functions.elementary.hyperbolic import tanh
|
||||
from sympy.matrices import Matrix
|
||||
from sympy.matrices.dense import zeros
|
||||
from sympy.physics.mechanics import dynamicsymbols
|
||||
from sympy.physics.biomechanics import (
|
||||
ActivationBase,
|
||||
FirstOrderActivationDeGroote2016,
|
||||
ZerothOrderActivation,
|
||||
)
|
||||
from sympy.physics.biomechanics._mixin import _NamedMixin
|
||||
from sympy.simplify.simplify import simplify
|
||||
|
||||
|
||||
class TestZerothOrderActivation:
|
||||
|
||||
@staticmethod
|
||||
def test_class():
|
||||
assert issubclass(ZerothOrderActivation, ActivationBase)
|
||||
assert issubclass(ZerothOrderActivation, _NamedMixin)
|
||||
assert ZerothOrderActivation.__name__ == 'ZerothOrderActivation'
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _zeroth_order_activation_fixture(self):
|
||||
self.name = 'name'
|
||||
self.e = dynamicsymbols('e_name')
|
||||
self.instance = ZerothOrderActivation(self.name)
|
||||
|
||||
def test_instance(self):
|
||||
instance = ZerothOrderActivation(self.name)
|
||||
assert isinstance(instance, ZerothOrderActivation)
|
||||
|
||||
def test_with_defaults(self):
|
||||
instance = ZerothOrderActivation.with_defaults(self.name)
|
||||
assert isinstance(instance, ZerothOrderActivation)
|
||||
assert instance == ZerothOrderActivation(self.name)
|
||||
|
||||
def test_name(self):
|
||||
assert hasattr(self.instance, 'name')
|
||||
assert self.instance.name == self.name
|
||||
|
||||
def test_order(self):
|
||||
assert hasattr(self.instance, 'order')
|
||||
assert self.instance.order == 0
|
||||
|
||||
def test_excitation_attribute(self):
|
||||
assert hasattr(self.instance, 'e')
|
||||
assert hasattr(self.instance, 'excitation')
|
||||
e_expected = dynamicsymbols('e_name')
|
||||
assert self.instance.e == e_expected
|
||||
assert self.instance.excitation == e_expected
|
||||
assert self.instance.e is self.instance.excitation
|
||||
|
||||
def test_activation_attribute(self):
|
||||
assert hasattr(self.instance, 'a')
|
||||
assert hasattr(self.instance, 'activation')
|
||||
a_expected = dynamicsymbols('e_name')
|
||||
assert self.instance.a == a_expected
|
||||
assert self.instance.activation == a_expected
|
||||
assert self.instance.a is self.instance.activation is self.instance.e
|
||||
|
||||
def test_state_vars_attribute(self):
|
||||
assert hasattr(self.instance, 'x')
|
||||
assert hasattr(self.instance, 'state_vars')
|
||||
assert self.instance.x == self.instance.state_vars
|
||||
x_expected = zeros(0, 1)
|
||||
assert self.instance.x == x_expected
|
||||
assert self.instance.state_vars == x_expected
|
||||
assert isinstance(self.instance.x, Matrix)
|
||||
assert isinstance(self.instance.state_vars, Matrix)
|
||||
assert self.instance.x.shape == (0, 1)
|
||||
assert self.instance.state_vars.shape == (0, 1)
|
||||
|
||||
def test_input_vars_attribute(self):
|
||||
assert hasattr(self.instance, 'r')
|
||||
assert hasattr(self.instance, 'input_vars')
|
||||
assert self.instance.r == self.instance.input_vars
|
||||
r_expected = Matrix([self.e])
|
||||
assert self.instance.r == r_expected
|
||||
assert self.instance.input_vars == r_expected
|
||||
assert isinstance(self.instance.r, Matrix)
|
||||
assert isinstance(self.instance.input_vars, Matrix)
|
||||
assert self.instance.r.shape == (1, 1)
|
||||
assert self.instance.input_vars.shape == (1, 1)
|
||||
|
||||
def test_constants_attribute(self):
|
||||
assert hasattr(self.instance, 'p')
|
||||
assert hasattr(self.instance, 'constants')
|
||||
assert self.instance.p == self.instance.constants
|
||||
p_expected = zeros(0, 1)
|
||||
assert self.instance.p == p_expected
|
||||
assert self.instance.constants == p_expected
|
||||
assert isinstance(self.instance.p, Matrix)
|
||||
assert isinstance(self.instance.constants, Matrix)
|
||||
assert self.instance.p.shape == (0, 1)
|
||||
assert self.instance.constants.shape == (0, 1)
|
||||
|
||||
def test_M_attribute(self):
|
||||
assert hasattr(self.instance, 'M')
|
||||
M_expected = Matrix([])
|
||||
assert self.instance.M == M_expected
|
||||
assert isinstance(self.instance.M, Matrix)
|
||||
assert self.instance.M.shape == (0, 0)
|
||||
|
||||
def test_F(self):
|
||||
assert hasattr(self.instance, 'F')
|
||||
F_expected = zeros(0, 1)
|
||||
assert self.instance.F == F_expected
|
||||
assert isinstance(self.instance.F, Matrix)
|
||||
assert self.instance.F.shape == (0, 1)
|
||||
|
||||
def test_rhs(self):
|
||||
assert hasattr(self.instance, 'rhs')
|
||||
rhs_expected = zeros(0, 1)
|
||||
rhs = self.instance.rhs()
|
||||
assert rhs == rhs_expected
|
||||
assert isinstance(rhs, Matrix)
|
||||
assert rhs.shape == (0, 1)
|
||||
|
||||
def test_repr(self):
|
||||
expected = 'ZerothOrderActivation(\'name\')'
|
||||
assert repr(self.instance) == expected
|
||||
|
||||
|
||||
class TestFirstOrderActivationDeGroote2016:
|
||||
|
||||
@staticmethod
|
||||
def test_class():
|
||||
assert issubclass(FirstOrderActivationDeGroote2016, ActivationBase)
|
||||
assert issubclass(FirstOrderActivationDeGroote2016, _NamedMixin)
|
||||
assert FirstOrderActivationDeGroote2016.__name__ == 'FirstOrderActivationDeGroote2016'
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _first_order_activation_de_groote_2016_fixture(self):
|
||||
self.name = 'name'
|
||||
self.e = dynamicsymbols('e_name')
|
||||
self.a = dynamicsymbols('a_name')
|
||||
self.tau_a = Symbol('tau_a')
|
||||
self.tau_d = Symbol('tau_d')
|
||||
self.b = Symbol('b')
|
||||
self.instance = FirstOrderActivationDeGroote2016(
|
||||
self.name,
|
||||
self.tau_a,
|
||||
self.tau_d,
|
||||
self.b,
|
||||
)
|
||||
|
||||
def test_instance(self):
|
||||
instance = FirstOrderActivationDeGroote2016(self.name)
|
||||
assert isinstance(instance, FirstOrderActivationDeGroote2016)
|
||||
|
||||
def test_with_defaults(self):
|
||||
instance = FirstOrderActivationDeGroote2016.with_defaults(self.name)
|
||||
assert isinstance(instance, FirstOrderActivationDeGroote2016)
|
||||
assert instance.tau_a == Float('0.015')
|
||||
assert instance.activation_time_constant == Float('0.015')
|
||||
assert instance.tau_d == Float('0.060')
|
||||
assert instance.deactivation_time_constant == Float('0.060')
|
||||
assert instance.b == Float('10.0')
|
||||
assert instance.smoothing_rate == Float('10.0')
|
||||
|
||||
def test_name(self):
|
||||
assert hasattr(self.instance, 'name')
|
||||
assert self.instance.name == self.name
|
||||
|
||||
def test_order(self):
|
||||
assert hasattr(self.instance, 'order')
|
||||
assert self.instance.order == 1
|
||||
|
||||
def test_excitation(self):
|
||||
assert hasattr(self.instance, 'e')
|
||||
assert hasattr(self.instance, 'excitation')
|
||||
e_expected = dynamicsymbols('e_name')
|
||||
assert self.instance.e == e_expected
|
||||
assert self.instance.excitation == e_expected
|
||||
assert self.instance.e is self.instance.excitation
|
||||
|
||||
def test_excitation_is_immutable(self):
|
||||
with pytest.raises(AttributeError):
|
||||
self.instance.e = None
|
||||
with pytest.raises(AttributeError):
|
||||
self.instance.excitation = None
|
||||
|
||||
def test_activation(self):
|
||||
assert hasattr(self.instance, 'a')
|
||||
assert hasattr(self.instance, 'activation')
|
||||
a_expected = dynamicsymbols('a_name')
|
||||
assert self.instance.a == a_expected
|
||||
assert self.instance.activation == a_expected
|
||||
|
||||
def test_activation_is_immutable(self):
|
||||
with pytest.raises(AttributeError):
|
||||
self.instance.a = None
|
||||
with pytest.raises(AttributeError):
|
||||
self.instance.activation = None
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'tau_a, expected',
|
||||
[
|
||||
(None, Symbol('tau_a_name')),
|
||||
(Symbol('tau_a'), Symbol('tau_a')),
|
||||
(Float('0.015'), Float('0.015')),
|
||||
]
|
||||
)
|
||||
def test_activation_time_constant(self, tau_a, expected):
|
||||
instance = FirstOrderActivationDeGroote2016(
|
||||
'name', activation_time_constant=tau_a,
|
||||
)
|
||||
assert instance.tau_a == expected
|
||||
assert instance.activation_time_constant == expected
|
||||
assert instance.tau_a is instance.activation_time_constant
|
||||
|
||||
def test_activation_time_constant_is_immutable(self):
|
||||
with pytest.raises(AttributeError):
|
||||
self.instance.tau_a = None
|
||||
with pytest.raises(AttributeError):
|
||||
self.instance.activation_time_constant = None
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'tau_d, expected',
|
||||
[
|
||||
(None, Symbol('tau_d_name')),
|
||||
(Symbol('tau_d'), Symbol('tau_d')),
|
||||
(Float('0.060'), Float('0.060')),
|
||||
]
|
||||
)
|
||||
def test_deactivation_time_constant(self, tau_d, expected):
|
||||
instance = FirstOrderActivationDeGroote2016(
|
||||
'name', deactivation_time_constant=tau_d,
|
||||
)
|
||||
assert instance.tau_d == expected
|
||||
assert instance.deactivation_time_constant == expected
|
||||
assert instance.tau_d is instance.deactivation_time_constant
|
||||
|
||||
def test_deactivation_time_constant_is_immutable(self):
|
||||
with pytest.raises(AttributeError):
|
||||
self.instance.tau_d = None
|
||||
with pytest.raises(AttributeError):
|
||||
self.instance.deactivation_time_constant = None
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'b, expected',
|
||||
[
|
||||
(None, Symbol('b_name')),
|
||||
(Symbol('b'), Symbol('b')),
|
||||
(Integer('10'), Integer('10')),
|
||||
]
|
||||
)
|
||||
def test_smoothing_rate(self, b, expected):
|
||||
instance = FirstOrderActivationDeGroote2016(
|
||||
'name', smoothing_rate=b,
|
||||
)
|
||||
assert instance.b == expected
|
||||
assert instance.smoothing_rate == expected
|
||||
assert instance.b is instance.smoothing_rate
|
||||
|
||||
def test_smoothing_rate_is_immutable(self):
|
||||
with pytest.raises(AttributeError):
|
||||
self.instance.b = None
|
||||
with pytest.raises(AttributeError):
|
||||
self.instance.smoothing_rate = None
|
||||
|
||||
def test_state_vars(self):
|
||||
assert hasattr(self.instance, 'x')
|
||||
assert hasattr(self.instance, 'state_vars')
|
||||
assert self.instance.x == self.instance.state_vars
|
||||
x_expected = Matrix([self.a])
|
||||
assert self.instance.x == x_expected
|
||||
assert self.instance.state_vars == x_expected
|
||||
assert isinstance(self.instance.x, Matrix)
|
||||
assert isinstance(self.instance.state_vars, Matrix)
|
||||
assert self.instance.x.shape == (1, 1)
|
||||
assert self.instance.state_vars.shape == (1, 1)
|
||||
|
||||
def test_input_vars(self):
|
||||
assert hasattr(self.instance, 'r')
|
||||
assert hasattr(self.instance, 'input_vars')
|
||||
assert self.instance.r == self.instance.input_vars
|
||||
r_expected = Matrix([self.e])
|
||||
assert self.instance.r == r_expected
|
||||
assert self.instance.input_vars == r_expected
|
||||
assert isinstance(self.instance.r, Matrix)
|
||||
assert isinstance(self.instance.input_vars, Matrix)
|
||||
assert self.instance.r.shape == (1, 1)
|
||||
assert self.instance.input_vars.shape == (1, 1)
|
||||
|
||||
def test_constants(self):
|
||||
assert hasattr(self.instance, 'p')
|
||||
assert hasattr(self.instance, 'constants')
|
||||
assert self.instance.p == self.instance.constants
|
||||
p_expected = Matrix([self.tau_a, self.tau_d, self.b])
|
||||
assert self.instance.p == p_expected
|
||||
assert self.instance.constants == p_expected
|
||||
assert isinstance(self.instance.p, Matrix)
|
||||
assert isinstance(self.instance.constants, Matrix)
|
||||
assert self.instance.p.shape == (3, 1)
|
||||
assert self.instance.constants.shape == (3, 1)
|
||||
|
||||
def test_M(self):
|
||||
assert hasattr(self.instance, 'M')
|
||||
M_expected = Matrix([1])
|
||||
assert self.instance.M == M_expected
|
||||
assert isinstance(self.instance.M, Matrix)
|
||||
assert self.instance.M.shape == (1, 1)
|
||||
|
||||
def test_F(self):
|
||||
assert hasattr(self.instance, 'F')
|
||||
da_expr = (
|
||||
((1/(self.tau_a*(Rational(1, 2) + Rational(3, 2)*self.a)))
|
||||
*(Rational(1, 2) + Rational(1, 2)*tanh(self.b*(self.e - self.a)))
|
||||
+ ((Rational(1, 2) + Rational(3, 2)*self.a)/self.tau_d)
|
||||
*(Rational(1, 2) - Rational(1, 2)*tanh(self.b*(self.e - self.a))))
|
||||
*(self.e - self.a)
|
||||
)
|
||||
F_expected = Matrix([da_expr])
|
||||
assert self.instance.F == F_expected
|
||||
assert isinstance(self.instance.F, Matrix)
|
||||
assert self.instance.F.shape == (1, 1)
|
||||
|
||||
def test_rhs(self):
|
||||
assert hasattr(self.instance, 'rhs')
|
||||
da_expr = (
|
||||
((1/(self.tau_a*(Rational(1, 2) + Rational(3, 2)*self.a)))
|
||||
*(Rational(1, 2) + Rational(1, 2)*tanh(self.b*(self.e - self.a)))
|
||||
+ ((Rational(1, 2) + Rational(3, 2)*self.a)/self.tau_d)
|
||||
*(Rational(1, 2) - Rational(1, 2)*tanh(self.b*(self.e - self.a))))
|
||||
*(self.e - self.a)
|
||||
)
|
||||
rhs_expected = Matrix([da_expr])
|
||||
rhs = self.instance.rhs()
|
||||
assert rhs == rhs_expected
|
||||
assert isinstance(rhs, Matrix)
|
||||
assert rhs.shape == (1, 1)
|
||||
assert simplify(self.instance.M.solve(self.instance.F) - rhs) == zeros(1)
|
||||
|
||||
def test_repr(self):
|
||||
expected = (
|
||||
'FirstOrderActivationDeGroote2016(\'name\', '
|
||||
'activation_time_constant=tau_a, '
|
||||
'deactivation_time_constant=tau_d, '
|
||||
'smoothing_rate=b)'
|
||||
)
|
||||
assert repr(self.instance) == expected
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,48 @@
|
||||
"""Tests for the ``sympy.physics.biomechanics._mixin.py`` module."""
|
||||
|
||||
import pytest
|
||||
|
||||
from sympy.physics.biomechanics._mixin import _NamedMixin
|
||||
|
||||
|
||||
class TestNamedMixin:
|
||||
|
||||
@staticmethod
|
||||
def test_subclass():
|
||||
|
||||
class Subclass(_NamedMixin):
|
||||
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
instance = Subclass('name')
|
||||
assert instance.name == 'name'
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _named_mixin_fixture(self):
|
||||
|
||||
class Subclass(_NamedMixin):
|
||||
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
self.Subclass = Subclass
|
||||
|
||||
@pytest.mark.parametrize('name', ['a', 'name', 'long_name'])
|
||||
def test_valid_name_argument(self, name):
|
||||
instance = self.Subclass(name)
|
||||
assert instance.name == name
|
||||
|
||||
@pytest.mark.parametrize('invalid_name', [0, 0.0, None, False])
|
||||
def test_invalid_name_argument_not_str(self, invalid_name):
|
||||
with pytest.raises(TypeError):
|
||||
_ = self.Subclass(invalid_name)
|
||||
|
||||
def test_invalid_name_argument_zero_length_str(self):
|
||||
with pytest.raises(ValueError):
|
||||
_ = self.Subclass('')
|
||||
|
||||
def test_name_attribute_is_immutable(self):
|
||||
instance = self.Subclass('name')
|
||||
with pytest.raises(AttributeError):
|
||||
instance.name = 'new_name'
|
||||
@@ -0,0 +1,837 @@
|
||||
"""Tests for the ``sympy.physics.biomechanics.musculotendon.py`` module."""
|
||||
|
||||
import abc
|
||||
|
||||
import pytest
|
||||
|
||||
from sympy.core.expr import UnevaluatedExpr
|
||||
from sympy.core.numbers import Float, Integer, Rational
|
||||
from sympy.core.symbol import Symbol
|
||||
from sympy.functions.elementary.exponential import exp
|
||||
from sympy.functions.elementary.hyperbolic import tanh
|
||||
from sympy.functions.elementary.miscellaneous import sqrt
|
||||
from sympy.functions.elementary.trigonometric import sin
|
||||
from sympy.matrices.dense import MutableDenseMatrix as Matrix, eye, zeros
|
||||
from sympy.physics.biomechanics.activation import (
|
||||
FirstOrderActivationDeGroote2016
|
||||
)
|
||||
from sympy.physics.biomechanics.curve import (
|
||||
CharacteristicCurveCollection,
|
||||
FiberForceLengthActiveDeGroote2016,
|
||||
FiberForceLengthPassiveDeGroote2016,
|
||||
FiberForceLengthPassiveInverseDeGroote2016,
|
||||
FiberForceVelocityDeGroote2016,
|
||||
FiberForceVelocityInverseDeGroote2016,
|
||||
TendonForceLengthDeGroote2016,
|
||||
TendonForceLengthInverseDeGroote2016,
|
||||
)
|
||||
from sympy.physics.biomechanics.musculotendon import (
|
||||
MusculotendonBase,
|
||||
MusculotendonDeGroote2016,
|
||||
MusculotendonFormulation,
|
||||
)
|
||||
from sympy.physics.biomechanics._mixin import _NamedMixin
|
||||
from sympy.physics.mechanics.actuator import ForceActuator
|
||||
from sympy.physics.mechanics.pathway import LinearPathway
|
||||
from sympy.physics.vector.frame import ReferenceFrame
|
||||
from sympy.physics.vector.functions import dynamicsymbols
|
||||
from sympy.physics.vector.point import Point
|
||||
from sympy.simplify.simplify import simplify
|
||||
|
||||
|
||||
class TestMusculotendonFormulation:
|
||||
@staticmethod
|
||||
def test_rigid_tendon_member():
|
||||
assert MusculotendonFormulation(0) == 0
|
||||
assert MusculotendonFormulation.RIGID_TENDON == 0
|
||||
|
||||
@staticmethod
|
||||
def test_fiber_length_explicit_member():
|
||||
assert MusculotendonFormulation(1) == 1
|
||||
assert MusculotendonFormulation.FIBER_LENGTH_EXPLICIT == 1
|
||||
|
||||
@staticmethod
|
||||
def test_tendon_force_explicit_member():
|
||||
assert MusculotendonFormulation(2) == 2
|
||||
assert MusculotendonFormulation.TENDON_FORCE_EXPLICIT == 2
|
||||
|
||||
@staticmethod
|
||||
def test_fiber_length_implicit_member():
|
||||
assert MusculotendonFormulation(3) == 3
|
||||
assert MusculotendonFormulation.FIBER_LENGTH_IMPLICIT == 3
|
||||
|
||||
@staticmethod
|
||||
def test_tendon_force_implicit_member():
|
||||
assert MusculotendonFormulation(4) == 4
|
||||
assert MusculotendonFormulation.TENDON_FORCE_IMPLICIT == 4
|
||||
|
||||
|
||||
class TestMusculotendonBase:
|
||||
|
||||
@staticmethod
|
||||
def test_is_abstract_base_class():
|
||||
assert issubclass(MusculotendonBase, abc.ABC)
|
||||
|
||||
@staticmethod
|
||||
def test_class():
|
||||
assert issubclass(MusculotendonBase, ForceActuator)
|
||||
assert issubclass(MusculotendonBase, _NamedMixin)
|
||||
assert MusculotendonBase.__name__ == 'MusculotendonBase'
|
||||
|
||||
@staticmethod
|
||||
def test_cannot_instantiate_directly():
|
||||
with pytest.raises(TypeError):
|
||||
_ = MusculotendonBase()
|
||||
|
||||
|
||||
@pytest.mark.parametrize('musculotendon_concrete', [MusculotendonDeGroote2016])
|
||||
class TestMusculotendonRigidTendon:
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _musculotendon_rigid_tendon_fixture(self, musculotendon_concrete):
|
||||
self.name = 'name'
|
||||
self.N = ReferenceFrame('N')
|
||||
self.q = dynamicsymbols('q')
|
||||
self.origin = Point('pO')
|
||||
self.insertion = Point('pI')
|
||||
self.insertion.set_pos(self.origin, self.q*self.N.x)
|
||||
self.pathway = LinearPathway(self.origin, self.insertion)
|
||||
self.activation = FirstOrderActivationDeGroote2016(self.name)
|
||||
self.e = self.activation.excitation
|
||||
self.a = self.activation.activation
|
||||
self.tau_a = self.activation.activation_time_constant
|
||||
self.tau_d = self.activation.deactivation_time_constant
|
||||
self.b = self.activation.smoothing_rate
|
||||
self.formulation = MusculotendonFormulation.RIGID_TENDON
|
||||
self.l_T_slack = Symbol('l_T_slack')
|
||||
self.F_M_max = Symbol('F_M_max')
|
||||
self.l_M_opt = Symbol('l_M_opt')
|
||||
self.v_M_max = Symbol('v_M_max')
|
||||
self.alpha_opt = Symbol('alpha_opt')
|
||||
self.beta = Symbol('beta')
|
||||
self.instance = musculotendon_concrete(
|
||||
self.name,
|
||||
self.pathway,
|
||||
self.activation,
|
||||
musculotendon_dynamics=self.formulation,
|
||||
tendon_slack_length=self.l_T_slack,
|
||||
peak_isometric_force=self.F_M_max,
|
||||
optimal_fiber_length=self.l_M_opt,
|
||||
maximal_fiber_velocity=self.v_M_max,
|
||||
optimal_pennation_angle=self.alpha_opt,
|
||||
fiber_damping_coefficient=self.beta,
|
||||
)
|
||||
self.da_expr = (
|
||||
(1/(self.tau_a*(Rational(1, 2) + Rational(3, 2)*self.a)))
|
||||
*(Rational(1, 2) + Rational(1, 2)*tanh(self.b*(self.e - self.a)))
|
||||
+ ((Rational(1, 2) + Rational(3, 2)*self.a)/self.tau_d)
|
||||
*(Rational(1, 2) - Rational(1, 2)*tanh(self.b*(self.e - self.a)))
|
||||
)*(self.e - self.a)
|
||||
|
||||
def test_state_vars(self):
|
||||
assert hasattr(self.instance, 'x')
|
||||
assert hasattr(self.instance, 'state_vars')
|
||||
assert self.instance.x == self.instance.state_vars
|
||||
x_expected = Matrix([self.a])
|
||||
assert self.instance.x == x_expected
|
||||
assert self.instance.state_vars == x_expected
|
||||
assert isinstance(self.instance.x, Matrix)
|
||||
assert isinstance(self.instance.state_vars, Matrix)
|
||||
assert self.instance.x.shape == (1, 1)
|
||||
assert self.instance.state_vars.shape == (1, 1)
|
||||
|
||||
def test_input_vars(self):
|
||||
assert hasattr(self.instance, 'r')
|
||||
assert hasattr(self.instance, 'input_vars')
|
||||
assert self.instance.r == self.instance.input_vars
|
||||
r_expected = Matrix([self.e])
|
||||
assert self.instance.r == r_expected
|
||||
assert self.instance.input_vars == r_expected
|
||||
assert isinstance(self.instance.r, Matrix)
|
||||
assert isinstance(self.instance.input_vars, Matrix)
|
||||
assert self.instance.r.shape == (1, 1)
|
||||
assert self.instance.input_vars.shape == (1, 1)
|
||||
|
||||
def test_constants(self):
|
||||
assert hasattr(self.instance, 'p')
|
||||
assert hasattr(self.instance, 'constants')
|
||||
assert self.instance.p == self.instance.constants
|
||||
p_expected = Matrix(
|
||||
[
|
||||
self.l_T_slack,
|
||||
self.F_M_max,
|
||||
self.l_M_opt,
|
||||
self.v_M_max,
|
||||
self.alpha_opt,
|
||||
self.beta,
|
||||
self.tau_a,
|
||||
self.tau_d,
|
||||
self.b,
|
||||
Symbol('c_0_fl_T_name'),
|
||||
Symbol('c_1_fl_T_name'),
|
||||
Symbol('c_2_fl_T_name'),
|
||||
Symbol('c_3_fl_T_name'),
|
||||
Symbol('c_0_fl_M_pas_name'),
|
||||
Symbol('c_1_fl_M_pas_name'),
|
||||
Symbol('c_0_fl_M_act_name'),
|
||||
Symbol('c_1_fl_M_act_name'),
|
||||
Symbol('c_2_fl_M_act_name'),
|
||||
Symbol('c_3_fl_M_act_name'),
|
||||
Symbol('c_4_fl_M_act_name'),
|
||||
Symbol('c_5_fl_M_act_name'),
|
||||
Symbol('c_6_fl_M_act_name'),
|
||||
Symbol('c_7_fl_M_act_name'),
|
||||
Symbol('c_8_fl_M_act_name'),
|
||||
Symbol('c_9_fl_M_act_name'),
|
||||
Symbol('c_10_fl_M_act_name'),
|
||||
Symbol('c_11_fl_M_act_name'),
|
||||
Symbol('c_0_fv_M_name'),
|
||||
Symbol('c_1_fv_M_name'),
|
||||
Symbol('c_2_fv_M_name'),
|
||||
Symbol('c_3_fv_M_name'),
|
||||
]
|
||||
)
|
||||
assert self.instance.p == p_expected
|
||||
assert self.instance.constants == p_expected
|
||||
assert isinstance(self.instance.p, Matrix)
|
||||
assert isinstance(self.instance.constants, Matrix)
|
||||
assert self.instance.p.shape == (31, 1)
|
||||
assert self.instance.constants.shape == (31, 1)
|
||||
|
||||
def test_M(self):
|
||||
assert hasattr(self.instance, 'M')
|
||||
M_expected = Matrix([1])
|
||||
assert self.instance.M == M_expected
|
||||
assert isinstance(self.instance.M, Matrix)
|
||||
assert self.instance.M.shape == (1, 1)
|
||||
|
||||
def test_F(self):
|
||||
assert hasattr(self.instance, 'F')
|
||||
F_expected = Matrix([self.da_expr])
|
||||
assert self.instance.F == F_expected
|
||||
assert isinstance(self.instance.F, Matrix)
|
||||
assert self.instance.F.shape == (1, 1)
|
||||
|
||||
def test_rhs(self):
|
||||
assert hasattr(self.instance, 'rhs')
|
||||
rhs_expected = Matrix([self.da_expr])
|
||||
rhs = self.instance.rhs()
|
||||
assert isinstance(rhs, Matrix)
|
||||
assert rhs.shape == (1, 1)
|
||||
assert simplify(rhs - rhs_expected) == zeros(1)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'musculotendon_concrete, curve',
|
||||
[
|
||||
(
|
||||
MusculotendonDeGroote2016,
|
||||
CharacteristicCurveCollection(
|
||||
tendon_force_length=TendonForceLengthDeGroote2016,
|
||||
tendon_force_length_inverse=TendonForceLengthInverseDeGroote2016,
|
||||
fiber_force_length_passive=FiberForceLengthPassiveDeGroote2016,
|
||||
fiber_force_length_passive_inverse=FiberForceLengthPassiveInverseDeGroote2016,
|
||||
fiber_force_length_active=FiberForceLengthActiveDeGroote2016,
|
||||
fiber_force_velocity=FiberForceVelocityDeGroote2016,
|
||||
fiber_force_velocity_inverse=FiberForceVelocityInverseDeGroote2016,
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
class TestFiberLengthExplicit:
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _musculotendon_fiber_length_explicit_fixture(
|
||||
self,
|
||||
musculotendon_concrete,
|
||||
curve,
|
||||
):
|
||||
self.name = 'name'
|
||||
self.N = ReferenceFrame('N')
|
||||
self.q = dynamicsymbols('q')
|
||||
self.origin = Point('pO')
|
||||
self.insertion = Point('pI')
|
||||
self.insertion.set_pos(self.origin, self.q*self.N.x)
|
||||
self.pathway = LinearPathway(self.origin, self.insertion)
|
||||
self.activation = FirstOrderActivationDeGroote2016(self.name)
|
||||
self.e = self.activation.excitation
|
||||
self.a = self.activation.activation
|
||||
self.tau_a = self.activation.activation_time_constant
|
||||
self.tau_d = self.activation.deactivation_time_constant
|
||||
self.b = self.activation.smoothing_rate
|
||||
self.formulation = MusculotendonFormulation.FIBER_LENGTH_EXPLICIT
|
||||
self.l_T_slack = Symbol('l_T_slack')
|
||||
self.F_M_max = Symbol('F_M_max')
|
||||
self.l_M_opt = Symbol('l_M_opt')
|
||||
self.v_M_max = Symbol('v_M_max')
|
||||
self.alpha_opt = Symbol('alpha_opt')
|
||||
self.beta = Symbol('beta')
|
||||
self.instance = musculotendon_concrete(
|
||||
self.name,
|
||||
self.pathway,
|
||||
self.activation,
|
||||
musculotendon_dynamics=self.formulation,
|
||||
tendon_slack_length=self.l_T_slack,
|
||||
peak_isometric_force=self.F_M_max,
|
||||
optimal_fiber_length=self.l_M_opt,
|
||||
maximal_fiber_velocity=self.v_M_max,
|
||||
optimal_pennation_angle=self.alpha_opt,
|
||||
fiber_damping_coefficient=self.beta,
|
||||
with_defaults=True,
|
||||
)
|
||||
self.l_M_tilde = dynamicsymbols('l_M_tilde_name')
|
||||
l_MT = self.pathway.length
|
||||
l_M = self.l_M_tilde*self.l_M_opt
|
||||
l_T = l_MT - sqrt(l_M**2 - (self.l_M_opt*sin(self.alpha_opt))**2)
|
||||
fl_T = curve.tendon_force_length.with_defaults(l_T/self.l_T_slack)
|
||||
fl_M_pas = curve.fiber_force_length_passive.with_defaults(self.l_M_tilde)
|
||||
fl_M_act = curve.fiber_force_length_active.with_defaults(self.l_M_tilde)
|
||||
v_M_tilde = curve.fiber_force_velocity_inverse.with_defaults(
|
||||
((((fl_T*self.F_M_max)/((l_MT - l_T)/l_M))/self.F_M_max) - fl_M_pas)
|
||||
/(self.a*fl_M_act)
|
||||
)
|
||||
self.dl_M_tilde_expr = (self.v_M_max/self.l_M_opt)*v_M_tilde
|
||||
self.da_expr = (
|
||||
(1/(self.tau_a*(Rational(1, 2) + Rational(3, 2)*self.a)))
|
||||
*(Rational(1, 2) + Rational(1, 2)*tanh(self.b*(self.e - self.a)))
|
||||
+ ((Rational(1, 2) + Rational(3, 2)*self.a)/self.tau_d)
|
||||
*(Rational(1, 2) - Rational(1, 2)*tanh(self.b*(self.e - self.a)))
|
||||
)*(self.e - self.a)
|
||||
|
||||
def test_state_vars(self):
|
||||
assert hasattr(self.instance, 'x')
|
||||
assert hasattr(self.instance, 'state_vars')
|
||||
assert self.instance.x == self.instance.state_vars
|
||||
x_expected = Matrix([self.l_M_tilde, self.a])
|
||||
assert self.instance.x == x_expected
|
||||
assert self.instance.state_vars == x_expected
|
||||
assert isinstance(self.instance.x, Matrix)
|
||||
assert isinstance(self.instance.state_vars, Matrix)
|
||||
assert self.instance.x.shape == (2, 1)
|
||||
assert self.instance.state_vars.shape == (2, 1)
|
||||
|
||||
def test_input_vars(self):
|
||||
assert hasattr(self.instance, 'r')
|
||||
assert hasattr(self.instance, 'input_vars')
|
||||
assert self.instance.r == self.instance.input_vars
|
||||
r_expected = Matrix([self.e])
|
||||
assert self.instance.r == r_expected
|
||||
assert self.instance.input_vars == r_expected
|
||||
assert isinstance(self.instance.r, Matrix)
|
||||
assert isinstance(self.instance.input_vars, Matrix)
|
||||
assert self.instance.r.shape == (1, 1)
|
||||
assert self.instance.input_vars.shape == (1, 1)
|
||||
|
||||
def test_constants(self):
|
||||
assert hasattr(self.instance, 'p')
|
||||
assert hasattr(self.instance, 'constants')
|
||||
assert self.instance.p == self.instance.constants
|
||||
p_expected = Matrix(
|
||||
[
|
||||
self.l_T_slack,
|
||||
self.F_M_max,
|
||||
self.l_M_opt,
|
||||
self.v_M_max,
|
||||
self.alpha_opt,
|
||||
self.beta,
|
||||
self.tau_a,
|
||||
self.tau_d,
|
||||
self.b,
|
||||
]
|
||||
)
|
||||
assert self.instance.p == p_expected
|
||||
assert self.instance.constants == p_expected
|
||||
assert isinstance(self.instance.p, Matrix)
|
||||
assert isinstance(self.instance.constants, Matrix)
|
||||
assert self.instance.p.shape == (9, 1)
|
||||
assert self.instance.constants.shape == (9, 1)
|
||||
|
||||
def test_M(self):
|
||||
assert hasattr(self.instance, 'M')
|
||||
M_expected = eye(2)
|
||||
assert self.instance.M == M_expected
|
||||
assert isinstance(self.instance.M, Matrix)
|
||||
assert self.instance.M.shape == (2, 2)
|
||||
|
||||
def test_F(self):
|
||||
assert hasattr(self.instance, 'F')
|
||||
F_expected = Matrix([self.dl_M_tilde_expr, self.da_expr])
|
||||
assert self.instance.F == F_expected
|
||||
assert isinstance(self.instance.F, Matrix)
|
||||
assert self.instance.F.shape == (2, 1)
|
||||
|
||||
def test_rhs(self):
|
||||
assert hasattr(self.instance, 'rhs')
|
||||
rhs_expected = Matrix([self.dl_M_tilde_expr, self.da_expr])
|
||||
rhs = self.instance.rhs()
|
||||
assert isinstance(rhs, Matrix)
|
||||
assert rhs.shape == (2, 1)
|
||||
assert simplify(rhs - rhs_expected) == zeros(2, 1)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'musculotendon_concrete, curve',
|
||||
[
|
||||
(
|
||||
MusculotendonDeGroote2016,
|
||||
CharacteristicCurveCollection(
|
||||
tendon_force_length=TendonForceLengthDeGroote2016,
|
||||
tendon_force_length_inverse=TendonForceLengthInverseDeGroote2016,
|
||||
fiber_force_length_passive=FiberForceLengthPassiveDeGroote2016,
|
||||
fiber_force_length_passive_inverse=FiberForceLengthPassiveInverseDeGroote2016,
|
||||
fiber_force_length_active=FiberForceLengthActiveDeGroote2016,
|
||||
fiber_force_velocity=FiberForceVelocityDeGroote2016,
|
||||
fiber_force_velocity_inverse=FiberForceVelocityInverseDeGroote2016,
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
class TestTendonForceExplicit:
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _musculotendon_tendon_force_explicit_fixture(
|
||||
self,
|
||||
musculotendon_concrete,
|
||||
curve,
|
||||
):
|
||||
self.name = 'name'
|
||||
self.N = ReferenceFrame('N')
|
||||
self.q = dynamicsymbols('q')
|
||||
self.origin = Point('pO')
|
||||
self.insertion = Point('pI')
|
||||
self.insertion.set_pos(self.origin, self.q*self.N.x)
|
||||
self.pathway = LinearPathway(self.origin, self.insertion)
|
||||
self.activation = FirstOrderActivationDeGroote2016(self.name)
|
||||
self.e = self.activation.excitation
|
||||
self.a = self.activation.activation
|
||||
self.tau_a = self.activation.activation_time_constant
|
||||
self.tau_d = self.activation.deactivation_time_constant
|
||||
self.b = self.activation.smoothing_rate
|
||||
self.formulation = MusculotendonFormulation.TENDON_FORCE_EXPLICIT
|
||||
self.l_T_slack = Symbol('l_T_slack')
|
||||
self.F_M_max = Symbol('F_M_max')
|
||||
self.l_M_opt = Symbol('l_M_opt')
|
||||
self.v_M_max = Symbol('v_M_max')
|
||||
self.alpha_opt = Symbol('alpha_opt')
|
||||
self.beta = Symbol('beta')
|
||||
self.instance = musculotendon_concrete(
|
||||
self.name,
|
||||
self.pathway,
|
||||
self.activation,
|
||||
musculotendon_dynamics=self.formulation,
|
||||
tendon_slack_length=self.l_T_slack,
|
||||
peak_isometric_force=self.F_M_max,
|
||||
optimal_fiber_length=self.l_M_opt,
|
||||
maximal_fiber_velocity=self.v_M_max,
|
||||
optimal_pennation_angle=self.alpha_opt,
|
||||
fiber_damping_coefficient=self.beta,
|
||||
with_defaults=True,
|
||||
)
|
||||
self.F_T_tilde = dynamicsymbols('F_T_tilde_name')
|
||||
l_T_tilde = curve.tendon_force_length_inverse.with_defaults(self.F_T_tilde)
|
||||
l_MT = self.pathway.length
|
||||
v_MT = self.pathway.extension_velocity
|
||||
l_T = l_T_tilde*self.l_T_slack
|
||||
l_M = sqrt((l_MT - l_T)**2 + (self.l_M_opt*sin(self.alpha_opt))**2)
|
||||
l_M_tilde = l_M/self.l_M_opt
|
||||
cos_alpha = (l_MT - l_T)/l_M
|
||||
F_T = self.F_T_tilde*self.F_M_max
|
||||
F_M = F_T/cos_alpha
|
||||
F_M_tilde = F_M/self.F_M_max
|
||||
fl_M_pas = curve.fiber_force_length_passive.with_defaults(l_M_tilde)
|
||||
fl_M_act = curve.fiber_force_length_active.with_defaults(l_M_tilde)
|
||||
fv_M = (F_M_tilde - fl_M_pas)/(self.a*fl_M_act)
|
||||
v_M_tilde = curve.fiber_force_velocity_inverse.with_defaults(fv_M)
|
||||
v_M = v_M_tilde*self.v_M_max
|
||||
v_T = v_MT - v_M/cos_alpha
|
||||
v_T_tilde = v_T/self.l_T_slack
|
||||
self.dF_T_tilde_expr = (
|
||||
Float('0.2')*Float('33.93669377311689')*exp(
|
||||
Float('33.93669377311689')*UnevaluatedExpr(l_T_tilde - Float('0.995'))
|
||||
)*v_T_tilde
|
||||
)
|
||||
self.da_expr = (
|
||||
(1/(self.tau_a*(Rational(1, 2) + Rational(3, 2)*self.a)))
|
||||
*(Rational(1, 2) + Rational(1, 2)*tanh(self.b*(self.e - self.a)))
|
||||
+ ((Rational(1, 2) + Rational(3, 2)*self.a)/self.tau_d)
|
||||
*(Rational(1, 2) - Rational(1, 2)*tanh(self.b*(self.e - self.a)))
|
||||
)*(self.e - self.a)
|
||||
|
||||
def test_state_vars(self):
|
||||
assert hasattr(self.instance, 'x')
|
||||
assert hasattr(self.instance, 'state_vars')
|
||||
assert self.instance.x == self.instance.state_vars
|
||||
x_expected = Matrix([self.F_T_tilde, self.a])
|
||||
assert self.instance.x == x_expected
|
||||
assert self.instance.state_vars == x_expected
|
||||
assert isinstance(self.instance.x, Matrix)
|
||||
assert isinstance(self.instance.state_vars, Matrix)
|
||||
assert self.instance.x.shape == (2, 1)
|
||||
assert self.instance.state_vars.shape == (2, 1)
|
||||
|
||||
def test_input_vars(self):
|
||||
assert hasattr(self.instance, 'r')
|
||||
assert hasattr(self.instance, 'input_vars')
|
||||
assert self.instance.r == self.instance.input_vars
|
||||
r_expected = Matrix([self.e])
|
||||
assert self.instance.r == r_expected
|
||||
assert self.instance.input_vars == r_expected
|
||||
assert isinstance(self.instance.r, Matrix)
|
||||
assert isinstance(self.instance.input_vars, Matrix)
|
||||
assert self.instance.r.shape == (1, 1)
|
||||
assert self.instance.input_vars.shape == (1, 1)
|
||||
|
||||
def test_constants(self):
|
||||
assert hasattr(self.instance, 'p')
|
||||
assert hasattr(self.instance, 'constants')
|
||||
assert self.instance.p == self.instance.constants
|
||||
p_expected = Matrix(
|
||||
[
|
||||
self.l_T_slack,
|
||||
self.F_M_max,
|
||||
self.l_M_opt,
|
||||
self.v_M_max,
|
||||
self.alpha_opt,
|
||||
self.beta,
|
||||
self.tau_a,
|
||||
self.tau_d,
|
||||
self.b,
|
||||
]
|
||||
)
|
||||
assert self.instance.p == p_expected
|
||||
assert self.instance.constants == p_expected
|
||||
assert isinstance(self.instance.p, Matrix)
|
||||
assert isinstance(self.instance.constants, Matrix)
|
||||
assert self.instance.p.shape == (9, 1)
|
||||
assert self.instance.constants.shape == (9, 1)
|
||||
|
||||
def test_M(self):
|
||||
assert hasattr(self.instance, 'M')
|
||||
M_expected = eye(2)
|
||||
assert self.instance.M == M_expected
|
||||
assert isinstance(self.instance.M, Matrix)
|
||||
assert self.instance.M.shape == (2, 2)
|
||||
|
||||
def test_F(self):
|
||||
assert hasattr(self.instance, 'F')
|
||||
F_expected = Matrix([self.dF_T_tilde_expr, self.da_expr])
|
||||
assert self.instance.F == F_expected
|
||||
assert isinstance(self.instance.F, Matrix)
|
||||
assert self.instance.F.shape == (2, 1)
|
||||
|
||||
def test_rhs(self):
|
||||
assert hasattr(self.instance, 'rhs')
|
||||
rhs_expected = Matrix([self.dF_T_tilde_expr, self.da_expr])
|
||||
rhs = self.instance.rhs()
|
||||
assert isinstance(rhs, Matrix)
|
||||
assert rhs.shape == (2, 1)
|
||||
assert simplify(rhs - rhs_expected) == zeros(2, 1)
|
||||
|
||||
|
||||
class TestMusculotendonDeGroote2016:
|
||||
|
||||
@staticmethod
|
||||
def test_class():
|
||||
assert issubclass(MusculotendonDeGroote2016, ForceActuator)
|
||||
assert issubclass(MusculotendonDeGroote2016, _NamedMixin)
|
||||
assert MusculotendonDeGroote2016.__name__ == 'MusculotendonDeGroote2016'
|
||||
|
||||
@staticmethod
|
||||
def test_instance():
|
||||
origin = Point('pO')
|
||||
insertion = Point('pI')
|
||||
insertion.set_pos(origin, dynamicsymbols('q')*ReferenceFrame('N').x)
|
||||
pathway = LinearPathway(origin, insertion)
|
||||
activation = FirstOrderActivationDeGroote2016('name')
|
||||
l_T_slack = Symbol('l_T_slack')
|
||||
F_M_max = Symbol('F_M_max')
|
||||
l_M_opt = Symbol('l_M_opt')
|
||||
v_M_max = Symbol('v_M_max')
|
||||
alpha_opt = Symbol('alpha_opt')
|
||||
beta = Symbol('beta')
|
||||
instance = MusculotendonDeGroote2016(
|
||||
'name',
|
||||
pathway,
|
||||
activation,
|
||||
musculotendon_dynamics=MusculotendonFormulation.RIGID_TENDON,
|
||||
tendon_slack_length=l_T_slack,
|
||||
peak_isometric_force=F_M_max,
|
||||
optimal_fiber_length=l_M_opt,
|
||||
maximal_fiber_velocity=v_M_max,
|
||||
optimal_pennation_angle=alpha_opt,
|
||||
fiber_damping_coefficient=beta,
|
||||
)
|
||||
assert isinstance(instance, MusculotendonDeGroote2016)
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _musculotendon_fixture(self):
|
||||
self.name = 'name'
|
||||
self.N = ReferenceFrame('N')
|
||||
self.q = dynamicsymbols('q')
|
||||
self.origin = Point('pO')
|
||||
self.insertion = Point('pI')
|
||||
self.insertion.set_pos(self.origin, self.q*self.N.x)
|
||||
self.pathway = LinearPathway(self.origin, self.insertion)
|
||||
self.activation = FirstOrderActivationDeGroote2016(self.name)
|
||||
self.l_T_slack = Symbol('l_T_slack')
|
||||
self.F_M_max = Symbol('F_M_max')
|
||||
self.l_M_opt = Symbol('l_M_opt')
|
||||
self.v_M_max = Symbol('v_M_max')
|
||||
self.alpha_opt = Symbol('alpha_opt')
|
||||
self.beta = Symbol('beta')
|
||||
|
||||
def test_with_defaults(self):
|
||||
origin = Point('pO')
|
||||
insertion = Point('pI')
|
||||
insertion.set_pos(origin, dynamicsymbols('q')*ReferenceFrame('N').x)
|
||||
pathway = LinearPathway(origin, insertion)
|
||||
activation = FirstOrderActivationDeGroote2016('name')
|
||||
l_T_slack = Symbol('l_T_slack')
|
||||
F_M_max = Symbol('F_M_max')
|
||||
l_M_opt = Symbol('l_M_opt')
|
||||
v_M_max = Float('10.0')
|
||||
alpha_opt = Float('0.0')
|
||||
beta = Float('0.1')
|
||||
instance = MusculotendonDeGroote2016.with_defaults(
|
||||
'name',
|
||||
pathway,
|
||||
activation,
|
||||
musculotendon_dynamics=MusculotendonFormulation.RIGID_TENDON,
|
||||
tendon_slack_length=l_T_slack,
|
||||
peak_isometric_force=F_M_max,
|
||||
optimal_fiber_length=l_M_opt,
|
||||
)
|
||||
assert instance.tendon_slack_length == l_T_slack
|
||||
assert instance.peak_isometric_force == F_M_max
|
||||
assert instance.optimal_fiber_length == l_M_opt
|
||||
assert instance.maximal_fiber_velocity == v_M_max
|
||||
assert instance.optimal_pennation_angle == alpha_opt
|
||||
assert instance.fiber_damping_coefficient == beta
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'l_T_slack, expected',
|
||||
[
|
||||
(None, Symbol('l_T_slack_name')),
|
||||
(Symbol('l_T_slack'), Symbol('l_T_slack')),
|
||||
(Rational(1, 2), Rational(1, 2)),
|
||||
(Float('0.5'), Float('0.5')),
|
||||
],
|
||||
)
|
||||
def test_tendon_slack_length(self, l_T_slack, expected):
|
||||
instance = MusculotendonDeGroote2016(
|
||||
self.name,
|
||||
self.pathway,
|
||||
self.activation,
|
||||
musculotendon_dynamics=MusculotendonFormulation.RIGID_TENDON,
|
||||
tendon_slack_length=l_T_slack,
|
||||
peak_isometric_force=self.F_M_max,
|
||||
optimal_fiber_length=self.l_M_opt,
|
||||
maximal_fiber_velocity=self.v_M_max,
|
||||
optimal_pennation_angle=self.alpha_opt,
|
||||
fiber_damping_coefficient=self.beta,
|
||||
)
|
||||
assert instance.l_T_slack == expected
|
||||
assert instance.tendon_slack_length == expected
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'F_M_max, expected',
|
||||
[
|
||||
(None, Symbol('F_M_max_name')),
|
||||
(Symbol('F_M_max'), Symbol('F_M_max')),
|
||||
(Integer(1000), Integer(1000)),
|
||||
(Float('1000.0'), Float('1000.0')),
|
||||
],
|
||||
)
|
||||
def test_peak_isometric_force(self, F_M_max, expected):
|
||||
instance = MusculotendonDeGroote2016(
|
||||
self.name,
|
||||
self.pathway,
|
||||
self.activation,
|
||||
musculotendon_dynamics=MusculotendonFormulation.RIGID_TENDON,
|
||||
tendon_slack_length=self.l_T_slack,
|
||||
peak_isometric_force=F_M_max,
|
||||
optimal_fiber_length=self.l_M_opt,
|
||||
maximal_fiber_velocity=self.v_M_max,
|
||||
optimal_pennation_angle=self.alpha_opt,
|
||||
fiber_damping_coefficient=self.beta,
|
||||
)
|
||||
assert instance.F_M_max == expected
|
||||
assert instance.peak_isometric_force == expected
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'l_M_opt, expected',
|
||||
[
|
||||
(None, Symbol('l_M_opt_name')),
|
||||
(Symbol('l_M_opt'), Symbol('l_M_opt')),
|
||||
(Rational(1, 2), Rational(1, 2)),
|
||||
(Float('0.5'), Float('0.5')),
|
||||
],
|
||||
)
|
||||
def test_optimal_fiber_length(self, l_M_opt, expected):
|
||||
instance = MusculotendonDeGroote2016(
|
||||
self.name,
|
||||
self.pathway,
|
||||
self.activation,
|
||||
musculotendon_dynamics=MusculotendonFormulation.RIGID_TENDON,
|
||||
tendon_slack_length=self.l_T_slack,
|
||||
peak_isometric_force=self.F_M_max,
|
||||
optimal_fiber_length=l_M_opt,
|
||||
maximal_fiber_velocity=self.v_M_max,
|
||||
optimal_pennation_angle=self.alpha_opt,
|
||||
fiber_damping_coefficient=self.beta,
|
||||
)
|
||||
assert instance.l_M_opt == expected
|
||||
assert instance.optimal_fiber_length == expected
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'v_M_max, expected',
|
||||
[
|
||||
(None, Symbol('v_M_max_name')),
|
||||
(Symbol('v_M_max'), Symbol('v_M_max')),
|
||||
(Integer(10), Integer(10)),
|
||||
(Float('10.0'), Float('10.0')),
|
||||
],
|
||||
)
|
||||
def test_maximal_fiber_velocity(self, v_M_max, expected):
|
||||
instance = MusculotendonDeGroote2016(
|
||||
self.name,
|
||||
self.pathway,
|
||||
self.activation,
|
||||
musculotendon_dynamics=MusculotendonFormulation.RIGID_TENDON,
|
||||
tendon_slack_length=self.l_T_slack,
|
||||
peak_isometric_force=self.F_M_max,
|
||||
optimal_fiber_length=self.l_M_opt,
|
||||
maximal_fiber_velocity=v_M_max,
|
||||
optimal_pennation_angle=self.alpha_opt,
|
||||
fiber_damping_coefficient=self.beta,
|
||||
)
|
||||
assert instance.v_M_max == expected
|
||||
assert instance.maximal_fiber_velocity == expected
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'alpha_opt, expected',
|
||||
[
|
||||
(None, Symbol('alpha_opt_name')),
|
||||
(Symbol('alpha_opt'), Symbol('alpha_opt')),
|
||||
(Integer(0), Integer(0)),
|
||||
(Float('0.1'), Float('0.1')),
|
||||
],
|
||||
)
|
||||
def test_optimal_pennation_angle(self, alpha_opt, expected):
|
||||
instance = MusculotendonDeGroote2016(
|
||||
self.name,
|
||||
self.pathway,
|
||||
self.activation,
|
||||
musculotendon_dynamics=MusculotendonFormulation.RIGID_TENDON,
|
||||
tendon_slack_length=self.l_T_slack,
|
||||
peak_isometric_force=self.F_M_max,
|
||||
optimal_fiber_length=self.l_M_opt,
|
||||
maximal_fiber_velocity=self.v_M_max,
|
||||
optimal_pennation_angle=alpha_opt,
|
||||
fiber_damping_coefficient=self.beta,
|
||||
)
|
||||
assert instance.alpha_opt == expected
|
||||
assert instance.optimal_pennation_angle == expected
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'beta, expected',
|
||||
[
|
||||
(None, Symbol('beta_name')),
|
||||
(Symbol('beta'), Symbol('beta')),
|
||||
(Integer(0), Integer(0)),
|
||||
(Rational(1, 10), Rational(1, 10)),
|
||||
(Float('0.1'), Float('0.1')),
|
||||
],
|
||||
)
|
||||
def test_fiber_damping_coefficient(self, beta, expected):
|
||||
instance = MusculotendonDeGroote2016(
|
||||
self.name,
|
||||
self.pathway,
|
||||
self.activation,
|
||||
musculotendon_dynamics=MusculotendonFormulation.RIGID_TENDON,
|
||||
tendon_slack_length=self.l_T_slack,
|
||||
peak_isometric_force=self.F_M_max,
|
||||
optimal_fiber_length=self.l_M_opt,
|
||||
maximal_fiber_velocity=self.v_M_max,
|
||||
optimal_pennation_angle=self.alpha_opt,
|
||||
fiber_damping_coefficient=beta,
|
||||
)
|
||||
assert instance.beta == expected
|
||||
assert instance.fiber_damping_coefficient == expected
|
||||
|
||||
def test_excitation(self):
|
||||
instance = MusculotendonDeGroote2016(
|
||||
self.name,
|
||||
self.pathway,
|
||||
self.activation,
|
||||
)
|
||||
assert hasattr(instance, 'e')
|
||||
assert hasattr(instance, 'excitation')
|
||||
e_expected = dynamicsymbols('e_name')
|
||||
assert instance.e == e_expected
|
||||
assert instance.excitation == e_expected
|
||||
assert instance.e is instance.excitation
|
||||
|
||||
def test_excitation_is_immutable(self):
|
||||
instance = MusculotendonDeGroote2016(
|
||||
self.name,
|
||||
self.pathway,
|
||||
self.activation,
|
||||
)
|
||||
with pytest.raises(AttributeError):
|
||||
instance.e = None
|
||||
with pytest.raises(AttributeError):
|
||||
instance.excitation = None
|
||||
|
||||
def test_activation(self):
|
||||
instance = MusculotendonDeGroote2016(
|
||||
self.name,
|
||||
self.pathway,
|
||||
self.activation,
|
||||
)
|
||||
assert hasattr(instance, 'a')
|
||||
assert hasattr(instance, 'activation')
|
||||
a_expected = dynamicsymbols('a_name')
|
||||
assert instance.a == a_expected
|
||||
assert instance.activation == a_expected
|
||||
|
||||
def test_activation_is_immutable(self):
|
||||
instance = MusculotendonDeGroote2016(
|
||||
self.name,
|
||||
self.pathway,
|
||||
self.activation,
|
||||
)
|
||||
with pytest.raises(AttributeError):
|
||||
instance.a = None
|
||||
with pytest.raises(AttributeError):
|
||||
instance.activation = None
|
||||
|
||||
def test_repr(self):
|
||||
instance = MusculotendonDeGroote2016(
|
||||
self.name,
|
||||
self.pathway,
|
||||
self.activation,
|
||||
musculotendon_dynamics=MusculotendonFormulation.RIGID_TENDON,
|
||||
tendon_slack_length=self.l_T_slack,
|
||||
peak_isometric_force=self.F_M_max,
|
||||
optimal_fiber_length=self.l_M_opt,
|
||||
maximal_fiber_velocity=self.v_M_max,
|
||||
optimal_pennation_angle=self.alpha_opt,
|
||||
fiber_damping_coefficient=self.beta,
|
||||
)
|
||||
expected = (
|
||||
'MusculotendonDeGroote2016(\'name\', '
|
||||
'pathway=LinearPathway(pO, pI), '
|
||||
'activation_dynamics=FirstOrderActivationDeGroote2016(\'name\', '
|
||||
'activation_time_constant=tau_a_name, '
|
||||
'deactivation_time_constant=tau_d_name, '
|
||||
'smoothing_rate=b_name), '
|
||||
'musculotendon_dynamics=0, '
|
||||
'tendon_slack_length=l_T_slack, '
|
||||
'peak_isometric_force=F_M_max, '
|
||||
'optimal_fiber_length=l_M_opt, '
|
||||
'maximal_fiber_velocity=v_M_max, '
|
||||
'optimal_pennation_angle=alpha_opt, '
|
||||
'fiber_damping_coefficient=beta)'
|
||||
)
|
||||
assert repr(instance) == expected
|
||||
Reference in New Issue
Block a user