Spaces:
Sleeping
Sleeping
File size: 25,521 Bytes
6a86ad5 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 |
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 involed.
"""
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})'
)
|