chore: 添加虚拟环境到仓库
- 添加 backend_service/venv 虚拟环境 - 包含所有Python依赖包 - 注意:虚拟环境约393MB,包含12655个文件
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',
|
||||
]
|
||||
@@ -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
@@ -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