curve.py 62 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763
  1. """Implementations of characteristic curves for musculotendon models."""
  2. from dataclasses import dataclass
  3. from sympy.core.expr import UnevaluatedExpr
  4. from sympy.core.function import ArgumentIndexError, Function
  5. from sympy.core.numbers import Float, Integer
  6. from sympy.functions.elementary.exponential import exp, log
  7. from sympy.functions.elementary.hyperbolic import cosh, sinh
  8. from sympy.functions.elementary.miscellaneous import sqrt
  9. from sympy.printing.precedence import PRECEDENCE
  10. __all__ = [
  11. 'CharacteristicCurveCollection',
  12. 'CharacteristicCurveFunction',
  13. 'FiberForceLengthActiveDeGroote2016',
  14. 'FiberForceLengthPassiveDeGroote2016',
  15. 'FiberForceLengthPassiveInverseDeGroote2016',
  16. 'FiberForceVelocityDeGroote2016',
  17. 'FiberForceVelocityInverseDeGroote2016',
  18. 'TendonForceLengthDeGroote2016',
  19. 'TendonForceLengthInverseDeGroote2016',
  20. ]
  21. class CharacteristicCurveFunction(Function):
  22. """Base class for all musculotendon characteristic curve functions."""
  23. @classmethod
  24. def eval(cls):
  25. msg = (
  26. f'Cannot directly instantiate {cls.__name__!r}, instances of '
  27. f'characteristic curves must be of a concrete subclass.'
  28. )
  29. raise TypeError(msg)
  30. def _print_code(self, printer):
  31. """Print code for the function defining the curve using a printer.
  32. Explanation
  33. ===========
  34. The order of operations may need to be controlled as constant folding
  35. the numeric terms within the equations of a musculotendon
  36. characteristic curve can sometimes results in a numerically-unstable
  37. expression.
  38. Parameters
  39. ==========
  40. printer : Printer
  41. The printer to be used to print a string representation of the
  42. characteristic curve as valid code in the target language.
  43. """
  44. return printer._print(printer.parenthesize(
  45. self.doit(deep=False, evaluate=False), PRECEDENCE['Atom'],
  46. ))
  47. _ccode = _print_code
  48. _cupycode = _print_code
  49. _cxxcode = _print_code
  50. _fcode = _print_code
  51. _jaxcode = _print_code
  52. _lambdacode = _print_code
  53. _mpmathcode = _print_code
  54. _octave = _print_code
  55. _pythoncode = _print_code
  56. _numpycode = _print_code
  57. _scipycode = _print_code
  58. class TendonForceLengthDeGroote2016(CharacteristicCurveFunction):
  59. r"""Tendon force-length curve based on De Groote et al., 2016 [1]_.
  60. Explanation
  61. ===========
  62. Gives the normalized tendon force produced as a function of normalized
  63. tendon length.
  64. The function is defined by the equation:
  65. $fl^T = c_0 \exp{c_3 \left( \tilde{l}^T - c_1 \right)} - c_2$
  66. with constant values of $c_0 = 0.2$, $c_1 = 0.995$, $c_2 = 0.25$, and
  67. $c_3 = 33.93669377311689$.
  68. While it is possible to change the constant values, these were carefully
  69. selected in the original publication to give the characteristic curve
  70. specific and required properties. For example, the function produces no
  71. force when the tendon is in an unstrained state. It also produces a force
  72. of 1 normalized unit when the tendon is under a 5% strain.
  73. Examples
  74. ========
  75. The preferred way to instantiate :class:`TendonForceLengthDeGroote2016` is using
  76. the :meth:`~.with_defaults` constructor because this will automatically
  77. populate the constants within the characteristic curve equation with the
  78. floating point values from the original publication. This constructor takes
  79. a single argument corresponding to normalized tendon length. We'll create a
  80. :class:`~.Symbol` called ``l_T_tilde`` to represent this.
  81. >>> from sympy import Symbol
  82. >>> from sympy.physics.biomechanics import TendonForceLengthDeGroote2016
  83. >>> l_T_tilde = Symbol('l_T_tilde')
  84. >>> fl_T = TendonForceLengthDeGroote2016.with_defaults(l_T_tilde)
  85. >>> fl_T
  86. TendonForceLengthDeGroote2016(l_T_tilde, 0.2, 0.995, 0.25,
  87. 33.93669377311689)
  88. It's also possible to populate the four constants with your own values too.
  89. >>> from sympy import symbols
  90. >>> c0, c1, c2, c3 = symbols('c0 c1 c2 c3')
  91. >>> fl_T = TendonForceLengthDeGroote2016(l_T_tilde, c0, c1, c2, c3)
  92. >>> fl_T
  93. TendonForceLengthDeGroote2016(l_T_tilde, c0, c1, c2, c3)
  94. You don't just have to use symbols as the arguments, it's also possible to
  95. use expressions. Let's create a new pair of symbols, ``l_T`` and
  96. ``l_T_slack``, representing tendon length and tendon slack length
  97. respectively. We can then represent ``l_T_tilde`` as an expression, the
  98. ratio of these.
  99. >>> l_T, l_T_slack = symbols('l_T l_T_slack')
  100. >>> l_T_tilde = l_T/l_T_slack
  101. >>> fl_T = TendonForceLengthDeGroote2016.with_defaults(l_T_tilde)
  102. >>> fl_T
  103. TendonForceLengthDeGroote2016(l_T/l_T_slack, 0.2, 0.995, 0.25,
  104. 33.93669377311689)
  105. To inspect the actual symbolic expression that this function represents,
  106. we can call the :meth:`~.doit` method on an instance. We'll use the keyword
  107. argument ``evaluate=False`` as this will keep the expression in its
  108. canonical form and won't simplify any constants.
  109. >>> fl_T.doit(evaluate=False)
  110. -0.25 + 0.2*exp(33.93669377311689*(l_T/l_T_slack - 0.995))
  111. The function can also be differentiated. We'll differentiate with respect
  112. to l_T using the ``diff`` method on an instance with the single positional
  113. argument ``l_T``.
  114. >>> fl_T.diff(l_T)
  115. 6.787338754623378*exp(33.93669377311689*(l_T/l_T_slack - 0.995))/l_T_slack
  116. References
  117. ==========
  118. .. [1] De Groote, F., Kinney, A. L., Rao, A. V., & Fregly, B. J., Evaluation
  119. of direct collocation optimal control problem formulations for
  120. solving the muscle redundancy problem, Annals of biomedical
  121. engineering, 44(10), (2016) pp. 2922-2936
  122. """
  123. @classmethod
  124. def with_defaults(cls, l_T_tilde):
  125. r"""Recommended constructor that will use the published constants.
  126. Explanation
  127. ===========
  128. Returns a new instance of the tendon force-length function using the
  129. four constant values specified in the original publication.
  130. These have the values:
  131. $c_0 = 0.2$
  132. $c_1 = 0.995$
  133. $c_2 = 0.25$
  134. $c_3 = 33.93669377311689$
  135. Parameters
  136. ==========
  137. l_T_tilde : Any (sympifiable)
  138. Normalized tendon length.
  139. """
  140. c0 = Float('0.2')
  141. c1 = Float('0.995')
  142. c2 = Float('0.25')
  143. c3 = Float('33.93669377311689')
  144. return cls(l_T_tilde, c0, c1, c2, c3)
  145. @classmethod
  146. def eval(cls, l_T_tilde, c0, c1, c2, c3):
  147. """Evaluation of basic inputs.
  148. Parameters
  149. ==========
  150. l_T_tilde : Any (sympifiable)
  151. Normalized tendon length.
  152. c0 : Any (sympifiable)
  153. The first constant in the characteristic equation. The published
  154. value is ``0.2``.
  155. c1 : Any (sympifiable)
  156. The second constant in the characteristic equation. The published
  157. value is ``0.995``.
  158. c2 : Any (sympifiable)
  159. The third constant in the characteristic equation. The published
  160. value is ``0.25``.
  161. c3 : Any (sympifiable)
  162. The fourth constant in the characteristic equation. The published
  163. value is ``33.93669377311689``.
  164. """
  165. pass
  166. def _eval_evalf(self, prec):
  167. """Evaluate the expression numerically using ``evalf``."""
  168. return self.doit(deep=False, evaluate=False)._eval_evalf(prec)
  169. def doit(self, deep=True, evaluate=True, **hints):
  170. """Evaluate the expression defining the function.
  171. Parameters
  172. ==========
  173. deep : bool
  174. Whether ``doit`` should be recursively called. Default is ``True``.
  175. evaluate : bool.
  176. Whether the SymPy expression should be evaluated as it is
  177. constructed. If ``False``, then no constant folding will be
  178. conducted which will leave the expression in a more numerically-
  179. stable for values of ``l_T_tilde`` that correspond to a sensible
  180. operating range for a musculotendon. Default is ``True``.
  181. **kwargs : dict[str, Any]
  182. Additional keyword argument pairs to be recursively passed to
  183. ``doit``.
  184. """
  185. l_T_tilde, *constants = self.args
  186. if deep:
  187. hints['evaluate'] = evaluate
  188. l_T_tilde = l_T_tilde.doit(deep=deep, **hints)
  189. c0, c1, c2, c3 = [c.doit(deep=deep, **hints) for c in constants]
  190. else:
  191. c0, c1, c2, c3 = constants
  192. if evaluate:
  193. return c0*exp(c3*(l_T_tilde - c1)) - c2
  194. return c0*exp(c3*UnevaluatedExpr(l_T_tilde - c1)) - c2
  195. def fdiff(self, argindex=1):
  196. """Derivative of the function with respect to a single argument.
  197. Parameters
  198. ==========
  199. argindex : int
  200. The index of the function's arguments with respect to which the
  201. derivative should be taken. Argument indexes start at ``1``.
  202. Default is ``1``.
  203. """
  204. l_T_tilde, c0, c1, c2, c3 = self.args
  205. if argindex == 1:
  206. return c0*c3*exp(c3*UnevaluatedExpr(l_T_tilde - c1))
  207. elif argindex == 2:
  208. return exp(c3*UnevaluatedExpr(l_T_tilde - c1))
  209. elif argindex == 3:
  210. return -c0*c3*exp(c3*UnevaluatedExpr(l_T_tilde - c1))
  211. elif argindex == 4:
  212. return Integer(-1)
  213. elif argindex == 5:
  214. return c0*(l_T_tilde - c1)*exp(c3*UnevaluatedExpr(l_T_tilde - c1))
  215. raise ArgumentIndexError(self, argindex)
  216. def inverse(self, argindex=1):
  217. """Inverse function.
  218. Parameters
  219. ==========
  220. argindex : int
  221. Value to start indexing the arguments at. Default is ``1``.
  222. """
  223. return TendonForceLengthInverseDeGroote2016
  224. def _latex(self, printer):
  225. """Print a LaTeX representation of the function defining the curve.
  226. Parameters
  227. ==========
  228. printer : Printer
  229. The printer to be used to print the LaTeX string representation.
  230. """
  231. l_T_tilde = self.args[0]
  232. _l_T_tilde = printer._print(l_T_tilde)
  233. return r'\operatorname{fl}^T \left( %s \right)' % _l_T_tilde
  234. class TendonForceLengthInverseDeGroote2016(CharacteristicCurveFunction):
  235. r"""Inverse tendon force-length curve based on De Groote et al., 2016 [1]_.
  236. Explanation
  237. ===========
  238. Gives the normalized tendon length that produces a specific normalized
  239. tendon force.
  240. The function is defined by the equation:
  241. ${fl^T}^{-1} = frac{\log{\frac{fl^T + c_2}{c_0}}}{c_3} + c_1$
  242. with constant values of $c_0 = 0.2$, $c_1 = 0.995$, $c_2 = 0.25$, and
  243. $c_3 = 33.93669377311689$. This function is the exact analytical inverse
  244. of the related tendon force-length curve ``TendonForceLengthDeGroote2016``.
  245. While it is possible to change the constant values, these were carefully
  246. selected in the original publication to give the characteristic curve
  247. specific and required properties. For example, the function produces no
  248. force when the tendon is in an unstrained state. It also produces a force
  249. of 1 normalized unit when the tendon is under a 5% strain.
  250. Examples
  251. ========
  252. The preferred way to instantiate :class:`TendonForceLengthInverseDeGroote2016` is
  253. using the :meth:`~.with_defaults` constructor because this will automatically
  254. populate the constants within the characteristic curve equation with the
  255. floating point values from the original publication. This constructor takes
  256. a single argument corresponding to normalized tendon force-length, which is
  257. equal to the tendon force. We'll create a :class:`~.Symbol` called ``fl_T`` to
  258. represent this.
  259. >>> from sympy import Symbol
  260. >>> from sympy.physics.biomechanics import TendonForceLengthInverseDeGroote2016
  261. >>> fl_T = Symbol('fl_T')
  262. >>> l_T_tilde = TendonForceLengthInverseDeGroote2016.with_defaults(fl_T)
  263. >>> l_T_tilde
  264. TendonForceLengthInverseDeGroote2016(fl_T, 0.2, 0.995, 0.25,
  265. 33.93669377311689)
  266. It's also possible to populate the four constants with your own values too.
  267. >>> from sympy import symbols
  268. >>> c0, c1, c2, c3 = symbols('c0 c1 c2 c3')
  269. >>> l_T_tilde = TendonForceLengthInverseDeGroote2016(fl_T, c0, c1, c2, c3)
  270. >>> l_T_tilde
  271. TendonForceLengthInverseDeGroote2016(fl_T, c0, c1, c2, c3)
  272. To inspect the actual symbolic expression that this function represents,
  273. we can call the :meth:`~.doit` method on an instance. We'll use the keyword
  274. argument ``evaluate=False`` as this will keep the expression in its
  275. canonical form and won't simplify any constants.
  276. >>> l_T_tilde.doit(evaluate=False)
  277. c1 + log((c2 + fl_T)/c0)/c3
  278. The function can also be differentiated. We'll differentiate with respect
  279. to l_T using the ``diff`` method on an instance with the single positional
  280. argument ``l_T``.
  281. >>> l_T_tilde.diff(fl_T)
  282. 1/(c3*(c2 + fl_T))
  283. References
  284. ==========
  285. .. [1] De Groote, F., Kinney, A. L., Rao, A. V., & Fregly, B. J., Evaluation
  286. of direct collocation optimal control problem formulations for
  287. solving the muscle redundancy problem, Annals of biomedical
  288. engineering, 44(10), (2016) pp. 2922-2936
  289. """
  290. @classmethod
  291. def with_defaults(cls, fl_T):
  292. r"""Recommended constructor that will use the published constants.
  293. Explanation
  294. ===========
  295. Returns a new instance of the inverse tendon force-length function
  296. using the four constant values specified in the original publication.
  297. These have the values:
  298. $c_0 = 0.2$
  299. $c_1 = 0.995$
  300. $c_2 = 0.25$
  301. $c_3 = 33.93669377311689$
  302. Parameters
  303. ==========
  304. fl_T : Any (sympifiable)
  305. Normalized tendon force as a function of tendon length.
  306. """
  307. c0 = Float('0.2')
  308. c1 = Float('0.995')
  309. c2 = Float('0.25')
  310. c3 = Float('33.93669377311689')
  311. return cls(fl_T, c0, c1, c2, c3)
  312. @classmethod
  313. def eval(cls, fl_T, c0, c1, c2, c3):
  314. """Evaluation of basic inputs.
  315. Parameters
  316. ==========
  317. fl_T : Any (sympifiable)
  318. Normalized tendon force as a function of tendon length.
  319. c0 : Any (sympifiable)
  320. The first constant in the characteristic equation. The published
  321. value is ``0.2``.
  322. c1 : Any (sympifiable)
  323. The second constant in the characteristic equation. The published
  324. value is ``0.995``.
  325. c2 : Any (sympifiable)
  326. The third constant in the characteristic equation. The published
  327. value is ``0.25``.
  328. c3 : Any (sympifiable)
  329. The fourth constant in the characteristic equation. The published
  330. value is ``33.93669377311689``.
  331. """
  332. pass
  333. def _eval_evalf(self, prec):
  334. """Evaluate the expression numerically using ``evalf``."""
  335. return self.doit(deep=False, evaluate=False)._eval_evalf(prec)
  336. def doit(self, deep=True, evaluate=True, **hints):
  337. """Evaluate the expression defining the function.
  338. Parameters
  339. ==========
  340. deep : bool
  341. Whether ``doit`` should be recursively called. Default is ``True``.
  342. evaluate : bool.
  343. Whether the SymPy expression should be evaluated as it is
  344. constructed. If ``False``, then no constant folding will be
  345. conducted which will leave the expression in a more numerically-
  346. stable for values of ``l_T_tilde`` that correspond to a sensible
  347. operating range for a musculotendon. Default is ``True``.
  348. **kwargs : dict[str, Any]
  349. Additional keyword argument pairs to be recursively passed to
  350. ``doit``.
  351. """
  352. fl_T, *constants = self.args
  353. if deep:
  354. hints['evaluate'] = evaluate
  355. fl_T = fl_T.doit(deep=deep, **hints)
  356. c0, c1, c2, c3 = [c.doit(deep=deep, **hints) for c in constants]
  357. else:
  358. c0, c1, c2, c3 = constants
  359. if evaluate:
  360. return log((fl_T + c2)/c0)/c3 + c1
  361. return log(UnevaluatedExpr((fl_T + c2)/c0))/c3 + c1
  362. def fdiff(self, argindex=1):
  363. """Derivative of the function with respect to a single argument.
  364. Parameters
  365. ==========
  366. argindex : int
  367. The index of the function's arguments with respect to which the
  368. derivative should be taken. Argument indexes start at ``1``.
  369. Default is ``1``.
  370. """
  371. fl_T, c0, c1, c2, c3 = self.args
  372. if argindex == 1:
  373. return 1/(c3*(fl_T + c2))
  374. elif argindex == 2:
  375. return -1/(c0*c3)
  376. elif argindex == 3:
  377. return Integer(1)
  378. elif argindex == 4:
  379. return 1/(c3*(fl_T + c2))
  380. elif argindex == 5:
  381. return -log(UnevaluatedExpr((fl_T + c2)/c0))/c3**2
  382. raise ArgumentIndexError(self, argindex)
  383. def inverse(self, argindex=1):
  384. """Inverse function.
  385. Parameters
  386. ==========
  387. argindex : int
  388. Value to start indexing the arguments at. Default is ``1``.
  389. """
  390. return TendonForceLengthDeGroote2016
  391. def _latex(self, printer):
  392. """Print a LaTeX representation of the function defining the curve.
  393. Parameters
  394. ==========
  395. printer : Printer
  396. The printer to be used to print the LaTeX string representation.
  397. """
  398. fl_T = self.args[0]
  399. _fl_T = printer._print(fl_T)
  400. return r'\left( \operatorname{fl}^T \right)^{-1} \left( %s \right)' % _fl_T
  401. class FiberForceLengthPassiveDeGroote2016(CharacteristicCurveFunction):
  402. r"""Passive muscle fiber force-length curve based on De Groote et al., 2016
  403. [1]_.
  404. Explanation
  405. ===========
  406. The function is defined by the equation:
  407. $fl^M_{pas} = \frac{\frac{\exp{c_1 \left(\tilde{l^M} - 1\right)}}{c_0} - 1}{\exp{c_1} - 1}$
  408. with constant values of $c_0 = 0.6$ and $c_1 = 4.0$.
  409. While it is possible to change the constant values, these were carefully
  410. selected in the original publication to give the characteristic curve
  411. specific and required properties. For example, the function produces a
  412. passive fiber force very close to 0 for all normalized fiber lengths
  413. between 0 and 1.
  414. Examples
  415. ========
  416. The preferred way to instantiate :class:`FiberForceLengthPassiveDeGroote2016` is
  417. using the :meth:`~.with_defaults` constructor because this will automatically
  418. populate the constants within the characteristic curve equation with the
  419. floating point values from the original publication. This constructor takes
  420. a single argument corresponding to normalized muscle fiber length. We'll
  421. create a :class:`~.Symbol` called ``l_M_tilde`` to represent this.
  422. >>> from sympy import Symbol
  423. >>> from sympy.physics.biomechanics import FiberForceLengthPassiveDeGroote2016
  424. >>> l_M_tilde = Symbol('l_M_tilde')
  425. >>> fl_M = FiberForceLengthPassiveDeGroote2016.with_defaults(l_M_tilde)
  426. >>> fl_M
  427. FiberForceLengthPassiveDeGroote2016(l_M_tilde, 0.6, 4.0)
  428. It's also possible to populate the two constants with your own values too.
  429. >>> from sympy import symbols
  430. >>> c0, c1 = symbols('c0 c1')
  431. >>> fl_M = FiberForceLengthPassiveDeGroote2016(l_M_tilde, c0, c1)
  432. >>> fl_M
  433. FiberForceLengthPassiveDeGroote2016(l_M_tilde, c0, c1)
  434. You don't just have to use symbols as the arguments, it's also possible to
  435. use expressions. Let's create a new pair of symbols, ``l_M`` and
  436. ``l_M_opt``, representing muscle fiber length and optimal muscle fiber
  437. length respectively. We can then represent ``l_M_tilde`` as an expression,
  438. the ratio of these.
  439. >>> l_M, l_M_opt = symbols('l_M l_M_opt')
  440. >>> l_M_tilde = l_M/l_M_opt
  441. >>> fl_M = FiberForceLengthPassiveDeGroote2016.with_defaults(l_M_tilde)
  442. >>> fl_M
  443. FiberForceLengthPassiveDeGroote2016(l_M/l_M_opt, 0.6, 4.0)
  444. To inspect the actual symbolic expression that this function represents,
  445. we can call the :meth:`~.doit` method on an instance. We'll use the keyword
  446. argument ``evaluate=False`` as this will keep the expression in its
  447. canonical form and won't simplify any constants.
  448. >>> fl_M.doit(evaluate=False)
  449. 0.0186573603637741*(-1 + exp(6.66666666666667*(l_M/l_M_opt - 1)))
  450. The function can also be differentiated. We'll differentiate with respect
  451. to l_M using the ``diff`` method on an instance with the single positional
  452. argument ``l_M``.
  453. >>> fl_M.diff(l_M)
  454. 0.12438240242516*exp(6.66666666666667*(l_M/l_M_opt - 1))/l_M_opt
  455. References
  456. ==========
  457. .. [1] De Groote, F., Kinney, A. L., Rao, A. V., & Fregly, B. J., Evaluation
  458. of direct collocation optimal control problem formulations for
  459. solving the muscle redundancy problem, Annals of biomedical
  460. engineering, 44(10), (2016) pp. 2922-2936
  461. """
  462. @classmethod
  463. def with_defaults(cls, l_M_tilde):
  464. r"""Recommended constructor that will use the published constants.
  465. Explanation
  466. ===========
  467. Returns a new instance of the muscle fiber passive force-length
  468. function using the four constant values specified in the original
  469. publication.
  470. These have the values:
  471. $c_0 = 0.6$
  472. $c_1 = 4.0$
  473. Parameters
  474. ==========
  475. l_M_tilde : Any (sympifiable)
  476. Normalized muscle fiber length.
  477. """
  478. c0 = Float('0.6')
  479. c1 = Float('4.0')
  480. return cls(l_M_tilde, c0, c1)
  481. @classmethod
  482. def eval(cls, l_M_tilde, c0, c1):
  483. """Evaluation of basic inputs.
  484. Parameters
  485. ==========
  486. l_M_tilde : Any (sympifiable)
  487. Normalized muscle fiber length.
  488. c0 : Any (sympifiable)
  489. The first constant in the characteristic equation. The published
  490. value is ``0.6``.
  491. c1 : Any (sympifiable)
  492. The second constant in the characteristic equation. The published
  493. value is ``4.0``.
  494. """
  495. pass
  496. def _eval_evalf(self, prec):
  497. """Evaluate the expression numerically using ``evalf``."""
  498. return self.doit(deep=False, evaluate=False)._eval_evalf(prec)
  499. def doit(self, deep=True, evaluate=True, **hints):
  500. """Evaluate the expression defining the function.
  501. Parameters
  502. ==========
  503. deep : bool
  504. Whether ``doit`` should be recursively called. Default is ``True``.
  505. evaluate : bool.
  506. Whether the SymPy expression should be evaluated as it is
  507. constructed. If ``False``, then no constant folding will be
  508. conducted which will leave the expression in a more numerically-
  509. stable for values of ``l_T_tilde`` that correspond to a sensible
  510. operating range for a musculotendon. Default is ``True``.
  511. **kwargs : dict[str, Any]
  512. Additional keyword argument pairs to be recursively passed to
  513. ``doit``.
  514. """
  515. l_M_tilde, *constants = self.args
  516. if deep:
  517. hints['evaluate'] = evaluate
  518. l_M_tilde = l_M_tilde.doit(deep=deep, **hints)
  519. c0, c1 = [c.doit(deep=deep, **hints) for c in constants]
  520. else:
  521. c0, c1 = constants
  522. if evaluate:
  523. return (exp((c1*(l_M_tilde - 1))/c0) - 1)/(exp(c1) - 1)
  524. return (exp((c1*UnevaluatedExpr(l_M_tilde - 1))/c0) - 1)/(exp(c1) - 1)
  525. def fdiff(self, argindex=1):
  526. """Derivative of the function with respect to a single argument.
  527. Parameters
  528. ==========
  529. argindex : int
  530. The index of the function's arguments with respect to which the
  531. derivative should be taken. Argument indexes start at ``1``.
  532. Default is ``1``.
  533. """
  534. l_M_tilde, c0, c1 = self.args
  535. if argindex == 1:
  536. return c1*exp(c1*UnevaluatedExpr(l_M_tilde - 1)/c0)/(c0*(exp(c1) - 1))
  537. elif argindex == 2:
  538. return (
  539. -c1*exp(c1*UnevaluatedExpr(l_M_tilde - 1)/c0)
  540. *UnevaluatedExpr(l_M_tilde - 1)/(c0**2*(exp(c1) - 1))
  541. )
  542. elif argindex == 3:
  543. return (
  544. -exp(c1)*(-1 + exp(c1*UnevaluatedExpr(l_M_tilde - 1)/c0))/(exp(c1) - 1)**2
  545. + exp(c1*UnevaluatedExpr(l_M_tilde - 1)/c0)*(l_M_tilde - 1)/(c0*(exp(c1) - 1))
  546. )
  547. raise ArgumentIndexError(self, argindex)
  548. def inverse(self, argindex=1):
  549. """Inverse function.
  550. Parameters
  551. ==========
  552. argindex : int
  553. Value to start indexing the arguments at. Default is ``1``.
  554. """
  555. return FiberForceLengthPassiveInverseDeGroote2016
  556. def _latex(self, printer):
  557. """Print a LaTeX representation of the function defining the curve.
  558. Parameters
  559. ==========
  560. printer : Printer
  561. The printer to be used to print the LaTeX string representation.
  562. """
  563. l_M_tilde = self.args[0]
  564. _l_M_tilde = printer._print(l_M_tilde)
  565. return r'\operatorname{fl}^M_{pas} \left( %s \right)' % _l_M_tilde
  566. class FiberForceLengthPassiveInverseDeGroote2016(CharacteristicCurveFunction):
  567. r"""Inverse passive muscle fiber force-length curve based on De Groote et
  568. al., 2016 [1]_.
  569. Explanation
  570. ===========
  571. Gives the normalized muscle fiber length that produces a specific normalized
  572. passive muscle fiber force.
  573. The function is defined by the equation:
  574. ${fl^M_{pas}}^{-1} = \frac{c_0 \log{\left(\exp{c_1} - 1\right)fl^M_pas + 1}}{c_1} + 1$
  575. with constant values of $c_0 = 0.6$ and $c_1 = 4.0$. This function is the
  576. exact analytical inverse of the related tendon force-length curve
  577. ``FiberForceLengthPassiveDeGroote2016``.
  578. While it is possible to change the constant values, these were carefully
  579. selected in the original publication to give the characteristic curve
  580. specific and required properties. For example, the function produces a
  581. passive fiber force very close to 0 for all normalized fiber lengths
  582. between 0 and 1.
  583. Examples
  584. ========
  585. The preferred way to instantiate
  586. :class:`FiberForceLengthPassiveInverseDeGroote2016` is using the
  587. :meth:`~.with_defaults` constructor because this will automatically populate the
  588. constants within the characteristic curve equation with the floating point
  589. values from the original publication. This constructor takes a single
  590. argument corresponding to the normalized passive muscle fiber length-force
  591. component of the muscle fiber force. We'll create a :class:`~.Symbol` called
  592. ``fl_M_pas`` to represent this.
  593. >>> from sympy import Symbol
  594. >>> from sympy.physics.biomechanics import FiberForceLengthPassiveInverseDeGroote2016
  595. >>> fl_M_pas = Symbol('fl_M_pas')
  596. >>> l_M_tilde = FiberForceLengthPassiveInverseDeGroote2016.with_defaults(fl_M_pas)
  597. >>> l_M_tilde
  598. FiberForceLengthPassiveInverseDeGroote2016(fl_M_pas, 0.6, 4.0)
  599. It's also possible to populate the two constants with your own values too.
  600. >>> from sympy import symbols
  601. >>> c0, c1 = symbols('c0 c1')
  602. >>> l_M_tilde = FiberForceLengthPassiveInverseDeGroote2016(fl_M_pas, c0, c1)
  603. >>> l_M_tilde
  604. FiberForceLengthPassiveInverseDeGroote2016(fl_M_pas, c0, c1)
  605. To inspect the actual symbolic expression that this function represents,
  606. we can call the :meth:`~.doit` method on an instance. We'll use the keyword
  607. argument ``evaluate=False`` as this will keep the expression in its
  608. canonical form and won't simplify any constants.
  609. >>> l_M_tilde.doit(evaluate=False)
  610. c0*log(1 + fl_M_pas*(exp(c1) - 1))/c1 + 1
  611. The function can also be differentiated. We'll differentiate with respect
  612. to fl_M_pas using the ``diff`` method on an instance with the single positional
  613. argument ``fl_M_pas``.
  614. >>> l_M_tilde.diff(fl_M_pas)
  615. c0*(exp(c1) - 1)/(c1*(fl_M_pas*(exp(c1) - 1) + 1))
  616. References
  617. ==========
  618. .. [1] De Groote, F., Kinney, A. L., Rao, A. V., & Fregly, B. J., Evaluation
  619. of direct collocation optimal control problem formulations for
  620. solving the muscle redundancy problem, Annals of biomedical
  621. engineering, 44(10), (2016) pp. 2922-2936
  622. """
  623. @classmethod
  624. def with_defaults(cls, fl_M_pas):
  625. r"""Recommended constructor that will use the published constants.
  626. Explanation
  627. ===========
  628. Returns a new instance of the inverse muscle fiber passive force-length
  629. function using the four constant values specified in the original
  630. publication.
  631. These have the values:
  632. $c_0 = 0.6$
  633. $c_1 = 4.0$
  634. Parameters
  635. ==========
  636. fl_M_pas : Any (sympifiable)
  637. Normalized passive muscle fiber force as a function of muscle fiber
  638. length.
  639. """
  640. c0 = Float('0.6')
  641. c1 = Float('4.0')
  642. return cls(fl_M_pas, c0, c1)
  643. @classmethod
  644. def eval(cls, fl_M_pas, c0, c1):
  645. """Evaluation of basic inputs.
  646. Parameters
  647. ==========
  648. fl_M_pas : Any (sympifiable)
  649. Normalized passive muscle fiber force.
  650. c0 : Any (sympifiable)
  651. The first constant in the characteristic equation. The published
  652. value is ``0.6``.
  653. c1 : Any (sympifiable)
  654. The second constant in the characteristic equation. The published
  655. value is ``4.0``.
  656. """
  657. pass
  658. def _eval_evalf(self, prec):
  659. """Evaluate the expression numerically using ``evalf``."""
  660. return self.doit(deep=False, evaluate=False)._eval_evalf(prec)
  661. def doit(self, deep=True, evaluate=True, **hints):
  662. """Evaluate the expression defining the function.
  663. Parameters
  664. ==========
  665. deep : bool
  666. Whether ``doit`` should be recursively called. Default is ``True``.
  667. evaluate : bool.
  668. Whether the SymPy expression should be evaluated as it is
  669. constructed. If ``False``, then no constant folding will be
  670. conducted which will leave the expression in a more numerically-
  671. stable for values of ``l_T_tilde`` that correspond to a sensible
  672. operating range for a musculotendon. Default is ``True``.
  673. **kwargs : dict[str, Any]
  674. Additional keyword argument pairs to be recursively passed to
  675. ``doit``.
  676. """
  677. fl_M_pas, *constants = self.args
  678. if deep:
  679. hints['evaluate'] = evaluate
  680. fl_M_pas = fl_M_pas.doit(deep=deep, **hints)
  681. c0, c1 = [c.doit(deep=deep, **hints) for c in constants]
  682. else:
  683. c0, c1 = constants
  684. if evaluate:
  685. return c0*log(fl_M_pas*(exp(c1) - 1) + 1)/c1 + 1
  686. return c0*log(UnevaluatedExpr(fl_M_pas*(exp(c1) - 1)) + 1)/c1 + 1
  687. def fdiff(self, argindex=1):
  688. """Derivative of the function with respect to a single argument.
  689. Parameters
  690. ==========
  691. argindex : int
  692. The index of the function's arguments with respect to which the
  693. derivative should be taken. Argument indexes start at ``1``.
  694. Default is ``1``.
  695. """
  696. fl_M_pas, c0, c1 = self.args
  697. if argindex == 1:
  698. return c0*(exp(c1) - 1)/(c1*(fl_M_pas*(exp(c1) - 1) + 1))
  699. elif argindex == 2:
  700. return log(fl_M_pas*(exp(c1) - 1) + 1)/c1
  701. elif argindex == 3:
  702. return (
  703. c0*fl_M_pas*exp(c1)/(c1*(fl_M_pas*(exp(c1) - 1) + 1))
  704. - c0*log(fl_M_pas*(exp(c1) - 1) + 1)/c1**2
  705. )
  706. raise ArgumentIndexError(self, argindex)
  707. def inverse(self, argindex=1):
  708. """Inverse function.
  709. Parameters
  710. ==========
  711. argindex : int
  712. Value to start indexing the arguments at. Default is ``1``.
  713. """
  714. return FiberForceLengthPassiveDeGroote2016
  715. def _latex(self, printer):
  716. """Print a LaTeX representation of the function defining the curve.
  717. Parameters
  718. ==========
  719. printer : Printer
  720. The printer to be used to print the LaTeX string representation.
  721. """
  722. fl_M_pas = self.args[0]
  723. _fl_M_pas = printer._print(fl_M_pas)
  724. return r'\left( \operatorname{fl}^M_{pas} \right)^{-1} \left( %s \right)' % _fl_M_pas
  725. class FiberForceLengthActiveDeGroote2016(CharacteristicCurveFunction):
  726. r"""Active muscle fiber force-length curve based on De Groote et al., 2016
  727. [1]_.
  728. Explanation
  729. ===========
  730. The function is defined by the equation:
  731. $fl_{\text{act}}^M = c_0 \exp\left(-\frac{1}{2}\left(\frac{\tilde{l}^M - c_1}{c_2 + c_3 \tilde{l}^M}\right)^2\right)
  732. + c_4 \exp\left(-\frac{1}{2}\left(\frac{\tilde{l}^M - c_5}{c_6 + c_7 \tilde{l}^M}\right)^2\right)
  733. + c_8 \exp\left(-\frac{1}{2}\left(\frac{\tilde{l}^M - c_9}{c_{10} + c_{11} \tilde{l}^M}\right)^2\right)$
  734. with constant values of $c0 = 0.814$, $c1 = 1.06$, $c2 = 0.162$,
  735. $c3 = 0.0633$, $c4 = 0.433$, $c5 = 0.717$, $c6 = -0.0299$, $c7 = 0.2$,
  736. $c8 = 0.1$, $c9 = 1.0$, $c10 = 0.354$, and $c11 = 0.0$.
  737. While it is possible to change the constant values, these were carefully
  738. selected in the original publication to give the characteristic curve
  739. specific and required properties. For example, the function produces a
  740. active fiber force of 1 at a normalized fiber length of 1, and an active
  741. fiber force of 0 at normalized fiber lengths of 0 and 2.
  742. Examples
  743. ========
  744. The preferred way to instantiate :class:`FiberForceLengthActiveDeGroote2016` is
  745. using the :meth:`~.with_defaults` constructor because this will automatically
  746. populate the constants within the characteristic curve equation with the
  747. floating point values from the original publication. This constructor takes
  748. a single argument corresponding to normalized muscle fiber length. We'll
  749. create a :class:`~.Symbol` called ``l_M_tilde`` to represent this.
  750. >>> from sympy import Symbol
  751. >>> from sympy.physics.biomechanics import FiberForceLengthActiveDeGroote2016
  752. >>> l_M_tilde = Symbol('l_M_tilde')
  753. >>> fl_M = FiberForceLengthActiveDeGroote2016.with_defaults(l_M_tilde)
  754. >>> fl_M
  755. FiberForceLengthActiveDeGroote2016(l_M_tilde, 0.814, 1.06, 0.162, 0.0633,
  756. 0.433, 0.717, -0.0299, 0.2, 0.1, 1.0, 0.354, 0.0)
  757. It's also possible to populate the two constants with your own values too.
  758. >>> from sympy import symbols
  759. >>> c0, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11 = symbols('c0:12')
  760. >>> fl_M = FiberForceLengthActiveDeGroote2016(l_M_tilde, c0, c1, c2, c3,
  761. ... c4, c5, c6, c7, c8, c9, c10, c11)
  762. >>> fl_M
  763. FiberForceLengthActiveDeGroote2016(l_M_tilde, c0, c1, c2, c3, c4, c5, c6,
  764. c7, c8, c9, c10, c11)
  765. You don't just have to use symbols as the arguments, it's also possible to
  766. use expressions. Let's create a new pair of symbols, ``l_M`` and
  767. ``l_M_opt``, representing muscle fiber length and optimal muscle fiber
  768. length respectively. We can then represent ``l_M_tilde`` as an expression,
  769. the ratio of these.
  770. >>> l_M, l_M_opt = symbols('l_M l_M_opt')
  771. >>> l_M_tilde = l_M/l_M_opt
  772. >>> fl_M = FiberForceLengthActiveDeGroote2016.with_defaults(l_M_tilde)
  773. >>> fl_M
  774. FiberForceLengthActiveDeGroote2016(l_M/l_M_opt, 0.814, 1.06, 0.162, 0.0633,
  775. 0.433, 0.717, -0.0299, 0.2, 0.1, 1.0, 0.354, 0.0)
  776. To inspect the actual symbolic expression that this function represents,
  777. we can call the :meth:`~.doit` method on an instance. We'll use the keyword
  778. argument ``evaluate=False`` as this will keep the expression in its
  779. canonical form and won't simplify any constants.
  780. >>> fl_M.doit(evaluate=False)
  781. 0.814*exp(-(l_M/l_M_opt
  782. - 1.06)**2/(2*(0.0633*l_M/l_M_opt + 0.162)**2))
  783. + 0.433*exp(-(l_M/l_M_opt - 0.717)**2/(2*(0.2*l_M/l_M_opt - 0.0299)**2))
  784. + 0.1*exp(-3.98991349867535*(l_M/l_M_opt - 1.0)**2)
  785. The function can also be differentiated. We'll differentiate with respect
  786. to l_M using the ``diff`` method on an instance with the single positional
  787. argument ``l_M``.
  788. >>> fl_M.diff(l_M)
  789. ((-0.79798269973507*l_M/l_M_opt
  790. + 0.79798269973507)*exp(-3.98991349867535*(l_M/l_M_opt - 1.0)**2)
  791. + (0.433*(-l_M/l_M_opt + 0.717)/(0.2*l_M/l_M_opt - 0.0299)**2
  792. + 0.0866*(l_M/l_M_opt - 0.717)**2/(0.2*l_M/l_M_opt
  793. - 0.0299)**3)*exp(-(l_M/l_M_opt - 0.717)**2/(2*(0.2*l_M/l_M_opt - 0.0299)**2))
  794. + (0.814*(-l_M/l_M_opt + 1.06)/(0.0633*l_M/l_M_opt
  795. + 0.162)**2 + 0.0515262*(l_M/l_M_opt
  796. - 1.06)**2/(0.0633*l_M/l_M_opt
  797. + 0.162)**3)*exp(-(l_M/l_M_opt
  798. - 1.06)**2/(2*(0.0633*l_M/l_M_opt + 0.162)**2)))/l_M_opt
  799. References
  800. ==========
  801. .. [1] De Groote, F., Kinney, A. L., Rao, A. V., & Fregly, B. J., Evaluation
  802. of direct collocation optimal control problem formulations for
  803. solving the muscle redundancy problem, Annals of biomedical
  804. engineering, 44(10), (2016) pp. 2922-2936
  805. """
  806. @classmethod
  807. def with_defaults(cls, l_M_tilde):
  808. r"""Recommended constructor that will use the published constants.
  809. Explanation
  810. ===========
  811. Returns a new instance of the inverse muscle fiber act force-length
  812. function using the four constant values specified in the original
  813. publication.
  814. These have the values:
  815. $c0 = 0.814$
  816. $c1 = 1.06$
  817. $c2 = 0.162$
  818. $c3 = 0.0633$
  819. $c4 = 0.433$
  820. $c5 = 0.717$
  821. $c6 = -0.0299$
  822. $c7 = 0.2$
  823. $c8 = 0.1$
  824. $c9 = 1.0$
  825. $c10 = 0.354$
  826. $c11 = 0.0$
  827. Parameters
  828. ==========
  829. fl_M_act : Any (sympifiable)
  830. Normalized passive muscle fiber force as a function of muscle fiber
  831. length.
  832. """
  833. c0 = Float('0.814')
  834. c1 = Float('1.06')
  835. c2 = Float('0.162')
  836. c3 = Float('0.0633')
  837. c4 = Float('0.433')
  838. c5 = Float('0.717')
  839. c6 = Float('-0.0299')
  840. c7 = Float('0.2')
  841. c8 = Float('0.1')
  842. c9 = Float('1.0')
  843. c10 = Float('0.354')
  844. c11 = Float('0.0')
  845. return cls(l_M_tilde, c0, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11)
  846. @classmethod
  847. def eval(cls, l_M_tilde, c0, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11):
  848. """Evaluation of basic inputs.
  849. Parameters
  850. ==========
  851. l_M_tilde : Any (sympifiable)
  852. Normalized muscle fiber length.
  853. c0 : Any (sympifiable)
  854. The first constant in the characteristic equation. The published
  855. value is ``0.814``.
  856. c1 : Any (sympifiable)
  857. The second constant in the characteristic equation. The published
  858. value is ``1.06``.
  859. c2 : Any (sympifiable)
  860. The third constant in the characteristic equation. The published
  861. value is ``0.162``.
  862. c3 : Any (sympifiable)
  863. The fourth constant in the characteristic equation. The published
  864. value is ``0.0633``.
  865. c4 : Any (sympifiable)
  866. The fifth constant in the characteristic equation. The published
  867. value is ``0.433``.
  868. c5 : Any (sympifiable)
  869. The sixth constant in the characteristic equation. The published
  870. value is ``0.717``.
  871. c6 : Any (sympifiable)
  872. The seventh constant in the characteristic equation. The published
  873. value is ``-0.0299``.
  874. c7 : Any (sympifiable)
  875. The eighth constant in the characteristic equation. The published
  876. value is ``0.2``.
  877. c8 : Any (sympifiable)
  878. The ninth constant in the characteristic equation. The published
  879. value is ``0.1``.
  880. c9 : Any (sympifiable)
  881. The tenth constant in the characteristic equation. The published
  882. value is ``1.0``.
  883. c10 : Any (sympifiable)
  884. The eleventh constant in the characteristic equation. The published
  885. value is ``0.354``.
  886. c11 : Any (sympifiable)
  887. The tweflth constant in the characteristic equation. The published
  888. value is ``0.0``.
  889. """
  890. pass
  891. def _eval_evalf(self, prec):
  892. """Evaluate the expression numerically using ``evalf``."""
  893. return self.doit(deep=False, evaluate=False)._eval_evalf(prec)
  894. def doit(self, deep=True, evaluate=True, **hints):
  895. """Evaluate the expression defining the function.
  896. Parameters
  897. ==========
  898. deep : bool
  899. Whether ``doit`` should be recursively called. Default is ``True``.
  900. evaluate : bool.
  901. Whether the SymPy expression should be evaluated as it is
  902. constructed. If ``False``, then no constant folding will be
  903. conducted which will leave the expression in a more numerically-
  904. stable for values of ``l_M_tilde`` that correspond to a sensible
  905. operating range for a musculotendon. Default is ``True``.
  906. **kwargs : dict[str, Any]
  907. Additional keyword argument pairs to be recursively passed to
  908. ``doit``.
  909. """
  910. l_M_tilde, *constants = self.args
  911. if deep:
  912. hints['evaluate'] = evaluate
  913. l_M_tilde = l_M_tilde.doit(deep=deep, **hints)
  914. constants = [c.doit(deep=deep, **hints) for c in constants]
  915. c0, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11 = constants
  916. if evaluate:
  917. return (
  918. c0*exp(-(((l_M_tilde - c1)/(c2 + c3*l_M_tilde))**2)/2)
  919. + c4*exp(-(((l_M_tilde - c5)/(c6 + c7*l_M_tilde))**2)/2)
  920. + c8*exp(-(((l_M_tilde - c9)/(c10 + c11*l_M_tilde))**2)/2)
  921. )
  922. return (
  923. c0*exp(-((UnevaluatedExpr(l_M_tilde - c1)/(c2 + c3*l_M_tilde))**2)/2)
  924. + c4*exp(-((UnevaluatedExpr(l_M_tilde - c5)/(c6 + c7*l_M_tilde))**2)/2)
  925. + c8*exp(-((UnevaluatedExpr(l_M_tilde - c9)/(c10 + c11*l_M_tilde))**2)/2)
  926. )
  927. def fdiff(self, argindex=1):
  928. """Derivative of the function with respect to a single argument.
  929. Parameters
  930. ==========
  931. argindex : int
  932. The index of the function's arguments with respect to which the
  933. derivative should be taken. Argument indexes start at ``1``.
  934. Default is ``1``.
  935. """
  936. l_M_tilde, c0, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11 = self.args
  937. if argindex == 1:
  938. return (
  939. c0*(
  940. c3*(l_M_tilde - c1)**2/(c2 + c3*l_M_tilde)**3
  941. + (c1 - l_M_tilde)/((c2 + c3*l_M_tilde)**2)
  942. )*exp(-(l_M_tilde - c1)**2/(2*(c2 + c3*l_M_tilde)**2))
  943. + c4*(
  944. c7*(l_M_tilde - c5)**2/(c6 + c7*l_M_tilde)**3
  945. + (c5 - l_M_tilde)/((c6 + c7*l_M_tilde)**2)
  946. )*exp(-(l_M_tilde - c5)**2/(2*(c6 + c7*l_M_tilde)**2))
  947. + c8*(
  948. c11*(l_M_tilde - c9)**2/(c10 + c11*l_M_tilde)**3
  949. + (c9 - l_M_tilde)/((c10 + c11*l_M_tilde)**2)
  950. )*exp(-(l_M_tilde - c9)**2/(2*(c10 + c11*l_M_tilde)**2))
  951. )
  952. elif argindex == 2:
  953. return exp(-(l_M_tilde - c1)**2/(2*(c2 + c3*l_M_tilde)**2))
  954. elif argindex == 3:
  955. return (
  956. c0*(l_M_tilde - c1)/(c2 + c3*l_M_tilde)**2
  957. *exp(-(l_M_tilde - c1)**2 /(2*(c2 + c3*l_M_tilde)**2))
  958. )
  959. elif argindex == 4:
  960. return (
  961. c0*(l_M_tilde - c1)**2/(c2 + c3*l_M_tilde)**3
  962. *exp(-(l_M_tilde - c1)**2/(2*(c2 + c3*l_M_tilde)**2))
  963. )
  964. elif argindex == 5:
  965. return (
  966. c0*l_M_tilde*(l_M_tilde - c1)**2/(c2 + c3*l_M_tilde)**3
  967. *exp(-(l_M_tilde - c1)**2/(2*(c2 + c3*l_M_tilde)**2))
  968. )
  969. elif argindex == 6:
  970. return exp(-(l_M_tilde - c5)**2/(2*(c6 + c7*l_M_tilde)**2))
  971. elif argindex == 7:
  972. return (
  973. c4*(l_M_tilde - c5)/(c6 + c7*l_M_tilde)**2
  974. *exp(-(l_M_tilde - c5)**2 /(2*(c6 + c7*l_M_tilde)**2))
  975. )
  976. elif argindex == 8:
  977. return (
  978. c4*(l_M_tilde - c5)**2/(c6 + c7*l_M_tilde)**3
  979. *exp(-(l_M_tilde - c5)**2/(2*(c6 + c7*l_M_tilde)**2))
  980. )
  981. elif argindex == 9:
  982. return (
  983. c4*l_M_tilde*(l_M_tilde - c5)**2/(c6 + c7*l_M_tilde)**3
  984. *exp(-(l_M_tilde - c5)**2/(2*(c6 + c7*l_M_tilde)**2))
  985. )
  986. elif argindex == 10:
  987. return exp(-(l_M_tilde - c9)**2/(2*(c10 + c11*l_M_tilde)**2))
  988. elif argindex == 11:
  989. return (
  990. c8*(l_M_tilde - c9)/(c10 + c11*l_M_tilde)**2
  991. *exp(-(l_M_tilde - c9)**2 /(2*(c10 + c11*l_M_tilde)**2))
  992. )
  993. elif argindex == 12:
  994. return (
  995. c8*(l_M_tilde - c9)**2/(c10 + c11*l_M_tilde)**3
  996. *exp(-(l_M_tilde - c9)**2/(2*(c10 + c11*l_M_tilde)**2))
  997. )
  998. elif argindex == 13:
  999. return (
  1000. c8*l_M_tilde*(l_M_tilde - c9)**2/(c10 + c11*l_M_tilde)**3
  1001. *exp(-(l_M_tilde - c9)**2/(2*(c10 + c11*l_M_tilde)**2))
  1002. )
  1003. raise ArgumentIndexError(self, argindex)
  1004. def _latex(self, printer):
  1005. """Print a LaTeX representation of the function defining the curve.
  1006. Parameters
  1007. ==========
  1008. printer : Printer
  1009. The printer to be used to print the LaTeX string representation.
  1010. """
  1011. l_M_tilde = self.args[0]
  1012. _l_M_tilde = printer._print(l_M_tilde)
  1013. return r'\operatorname{fl}^M_{act} \left( %s \right)' % _l_M_tilde
  1014. class FiberForceVelocityDeGroote2016(CharacteristicCurveFunction):
  1015. r"""Muscle fiber force-velocity curve based on De Groote et al., 2016 [1]_.
  1016. Explanation
  1017. ===========
  1018. Gives the normalized muscle fiber force produced as a function of
  1019. normalized tendon velocity.
  1020. The function is defined by the equation:
  1021. $fv^M = c_0 \log{\left(c_1 \tilde{v}_m + c_2\right) + \sqrt{\left(c_1 \tilde{v}_m + c_2\right)^2 + 1}} + c_3$
  1022. with constant values of $c_0 = -0.318$, $c_1 = -8.149$, $c_2 = -0.374$, and
  1023. $c_3 = 0.886$.
  1024. While it is possible to change the constant values, these were carefully
  1025. selected in the original publication to give the characteristic curve
  1026. specific and required properties. For example, the function produces a
  1027. normalized muscle fiber force of 1 when the muscle fibers are contracting
  1028. isometrically (they have an extension rate of 0).
  1029. Examples
  1030. ========
  1031. The preferred way to instantiate :class:`FiberForceVelocityDeGroote2016` is using
  1032. the :meth:`~.with_defaults` constructor because this will automatically populate
  1033. the constants within the characteristic curve equation with the floating
  1034. point values from the original publication. This constructor takes a single
  1035. argument corresponding to normalized muscle fiber extension velocity. We'll
  1036. create a :class:`~.Symbol` called ``v_M_tilde`` to represent this.
  1037. >>> from sympy import Symbol
  1038. >>> from sympy.physics.biomechanics import FiberForceVelocityDeGroote2016
  1039. >>> v_M_tilde = Symbol('v_M_tilde')
  1040. >>> fv_M = FiberForceVelocityDeGroote2016.with_defaults(v_M_tilde)
  1041. >>> fv_M
  1042. FiberForceVelocityDeGroote2016(v_M_tilde, -0.318, -8.149, -0.374, 0.886)
  1043. It's also possible to populate the four constants with your own values too.
  1044. >>> from sympy import symbols
  1045. >>> c0, c1, c2, c3 = symbols('c0 c1 c2 c3')
  1046. >>> fv_M = FiberForceVelocityDeGroote2016(v_M_tilde, c0, c1, c2, c3)
  1047. >>> fv_M
  1048. FiberForceVelocityDeGroote2016(v_M_tilde, c0, c1, c2, c3)
  1049. You don't just have to use symbols as the arguments, it's also possible to
  1050. use expressions. Let's create a new pair of symbols, ``v_M`` and
  1051. ``v_M_max``, representing muscle fiber extension velocity and maximum
  1052. muscle fiber extension velocity respectively. We can then represent
  1053. ``v_M_tilde`` as an expression, the ratio of these.
  1054. >>> v_M, v_M_max = symbols('v_M v_M_max')
  1055. >>> v_M_tilde = v_M/v_M_max
  1056. >>> fv_M = FiberForceVelocityDeGroote2016.with_defaults(v_M_tilde)
  1057. >>> fv_M
  1058. FiberForceVelocityDeGroote2016(v_M/v_M_max, -0.318, -8.149, -0.374, 0.886)
  1059. To inspect the actual symbolic expression that this function represents,
  1060. we can call the :meth:`~.doit` method on an instance. We'll use the keyword
  1061. argument ``evaluate=False`` as this will keep the expression in its
  1062. canonical form and won't simplify any constants.
  1063. >>> fv_M.doit(evaluate=False)
  1064. 0.886 - 0.318*log(-8.149*v_M/v_M_max - 0.374 + sqrt(1 + (-8.149*v_M/v_M_max
  1065. - 0.374)**2))
  1066. The function can also be differentiated. We'll differentiate with respect
  1067. to v_M using the ``diff`` method on an instance with the single positional
  1068. argument ``v_M``.
  1069. >>> fv_M.diff(v_M)
  1070. 2.591382*(1 + (-8.149*v_M/v_M_max - 0.374)**2)**(-1/2)/v_M_max
  1071. References
  1072. ==========
  1073. .. [1] De Groote, F., Kinney, A. L., Rao, A. V., & Fregly, B. J., Evaluation
  1074. of direct collocation optimal control problem formulations for
  1075. solving the muscle redundancy problem, Annals of biomedical
  1076. engineering, 44(10), (2016) pp. 2922-2936
  1077. """
  1078. @classmethod
  1079. def with_defaults(cls, v_M_tilde):
  1080. r"""Recommended constructor that will use the published constants.
  1081. Explanation
  1082. ===========
  1083. Returns a new instance of the muscle fiber force-velocity function
  1084. using the four constant values specified in the original publication.
  1085. These have the values:
  1086. $c_0 = -0.318$
  1087. $c_1 = -8.149$
  1088. $c_2 = -0.374$
  1089. $c_3 = 0.886$
  1090. Parameters
  1091. ==========
  1092. v_M_tilde : Any (sympifiable)
  1093. Normalized muscle fiber extension velocity.
  1094. """
  1095. c0 = Float('-0.318')
  1096. c1 = Float('-8.149')
  1097. c2 = Float('-0.374')
  1098. c3 = Float('0.886')
  1099. return cls(v_M_tilde, c0, c1, c2, c3)
  1100. @classmethod
  1101. def eval(cls, v_M_tilde, c0, c1, c2, c3):
  1102. """Evaluation of basic inputs.
  1103. Parameters
  1104. ==========
  1105. v_M_tilde : Any (sympifiable)
  1106. Normalized muscle fiber extension velocity.
  1107. c0 : Any (sympifiable)
  1108. The first constant in the characteristic equation. The published
  1109. value is ``-0.318``.
  1110. c1 : Any (sympifiable)
  1111. The second constant in the characteristic equation. The published
  1112. value is ``-8.149``.
  1113. c2 : Any (sympifiable)
  1114. The third constant in the characteristic equation. The published
  1115. value is ``-0.374``.
  1116. c3 : Any (sympifiable)
  1117. The fourth constant in the characteristic equation. The published
  1118. value is ``0.886``.
  1119. """
  1120. pass
  1121. def _eval_evalf(self, prec):
  1122. """Evaluate the expression numerically using ``evalf``."""
  1123. return self.doit(deep=False, evaluate=False)._eval_evalf(prec)
  1124. def doit(self, deep=True, evaluate=True, **hints):
  1125. """Evaluate the expression defining the function.
  1126. Parameters
  1127. ==========
  1128. deep : bool
  1129. Whether ``doit`` should be recursively called. Default is ``True``.
  1130. evaluate : bool.
  1131. Whether the SymPy expression should be evaluated as it is
  1132. constructed. If ``False``, then no constant folding will be
  1133. conducted which will leave the expression in a more numerically-
  1134. stable for values of ``v_M_tilde`` that correspond to a sensible
  1135. operating range for a musculotendon. Default is ``True``.
  1136. **kwargs : dict[str, Any]
  1137. Additional keyword argument pairs to be recursively passed to
  1138. ``doit``.
  1139. """
  1140. v_M_tilde, *constants = self.args
  1141. if deep:
  1142. hints['evaluate'] = evaluate
  1143. v_M_tilde = v_M_tilde.doit(deep=deep, **hints)
  1144. c0, c1, c2, c3 = [c.doit(deep=deep, **hints) for c in constants]
  1145. else:
  1146. c0, c1, c2, c3 = constants
  1147. if evaluate:
  1148. return c0*log(c1*v_M_tilde + c2 + sqrt((c1*v_M_tilde + c2)**2 + 1)) + c3
  1149. return c0*log(c1*v_M_tilde + c2 + sqrt(UnevaluatedExpr(c1*v_M_tilde + c2)**2 + 1)) + c3
  1150. def fdiff(self, argindex=1):
  1151. """Derivative of the function with respect to a single argument.
  1152. Parameters
  1153. ==========
  1154. argindex : int
  1155. The index of the function's arguments with respect to which the
  1156. derivative should be taken. Argument indexes start at ``1``.
  1157. Default is ``1``.
  1158. """
  1159. v_M_tilde, c0, c1, c2, c3 = self.args
  1160. if argindex == 1:
  1161. return c0*c1/sqrt(UnevaluatedExpr(c1*v_M_tilde + c2)**2 + 1)
  1162. elif argindex == 2:
  1163. return log(
  1164. c1*v_M_tilde + c2
  1165. + sqrt(UnevaluatedExpr(c1*v_M_tilde + c2)**2 + 1)
  1166. )
  1167. elif argindex == 3:
  1168. return c0*v_M_tilde/sqrt(UnevaluatedExpr(c1*v_M_tilde + c2)**2 + 1)
  1169. elif argindex == 4:
  1170. return c0/sqrt(UnevaluatedExpr(c1*v_M_tilde + c2)**2 + 1)
  1171. elif argindex == 5:
  1172. return Integer(1)
  1173. raise ArgumentIndexError(self, argindex)
  1174. def inverse(self, argindex=1):
  1175. """Inverse function.
  1176. Parameters
  1177. ==========
  1178. argindex : int
  1179. Value to start indexing the arguments at. Default is ``1``.
  1180. """
  1181. return FiberForceVelocityInverseDeGroote2016
  1182. def _latex(self, printer):
  1183. """Print a LaTeX representation of the function defining the curve.
  1184. Parameters
  1185. ==========
  1186. printer : Printer
  1187. The printer to be used to print the LaTeX string representation.
  1188. """
  1189. v_M_tilde = self.args[0]
  1190. _v_M_tilde = printer._print(v_M_tilde)
  1191. return r'\operatorname{fv}^M \left( %s \right)' % _v_M_tilde
  1192. class FiberForceVelocityInverseDeGroote2016(CharacteristicCurveFunction):
  1193. r"""Inverse muscle fiber force-velocity curve based on De Groote et al.,
  1194. 2016 [1]_.
  1195. Explanation
  1196. ===========
  1197. Gives the normalized muscle fiber velocity that produces a specific
  1198. normalized muscle fiber force.
  1199. The function is defined by the equation:
  1200. ${fv^M}^{-1} = \frac{\sinh{\frac{fv^M - c_3}{c_0}} - c_2}{c_1}$
  1201. with constant values of $c_0 = -0.318$, $c_1 = -8.149$, $c_2 = -0.374$, and
  1202. $c_3 = 0.886$. This function is the exact analytical inverse of the related
  1203. muscle fiber force-velocity curve ``FiberForceVelocityDeGroote2016``.
  1204. While it is possible to change the constant values, these were carefully
  1205. selected in the original publication to give the characteristic curve
  1206. specific and required properties. For example, the function produces a
  1207. normalized muscle fiber force of 1 when the muscle fibers are contracting
  1208. isometrically (they have an extension rate of 0).
  1209. Examples
  1210. ========
  1211. The preferred way to instantiate :class:`FiberForceVelocityInverseDeGroote2016`
  1212. is using the :meth:`~.with_defaults` constructor because this will automatically
  1213. populate the constants within the characteristic curve equation with the
  1214. floating point values from the original publication. This constructor takes
  1215. a single argument corresponding to normalized muscle fiber force-velocity
  1216. component of the muscle fiber force. We'll create a :class:`~.Symbol` called
  1217. ``fv_M`` to represent this.
  1218. >>> from sympy import Symbol
  1219. >>> from sympy.physics.biomechanics import FiberForceVelocityInverseDeGroote2016
  1220. >>> fv_M = Symbol('fv_M')
  1221. >>> v_M_tilde = FiberForceVelocityInverseDeGroote2016.with_defaults(fv_M)
  1222. >>> v_M_tilde
  1223. FiberForceVelocityInverseDeGroote2016(fv_M, -0.318, -8.149, -0.374, 0.886)
  1224. It's also possible to populate the four constants with your own values too.
  1225. >>> from sympy import symbols
  1226. >>> c0, c1, c2, c3 = symbols('c0 c1 c2 c3')
  1227. >>> v_M_tilde = FiberForceVelocityInverseDeGroote2016(fv_M, c0, c1, c2, c3)
  1228. >>> v_M_tilde
  1229. FiberForceVelocityInverseDeGroote2016(fv_M, c0, c1, c2, c3)
  1230. To inspect the actual symbolic expression that this function represents,
  1231. we can call the :meth:`~.doit` method on an instance. We'll use the keyword
  1232. argument ``evaluate=False`` as this will keep the expression in its
  1233. canonical form and won't simplify any constants.
  1234. >>> v_M_tilde.doit(evaluate=False)
  1235. (-c2 + sinh((-c3 + fv_M)/c0))/c1
  1236. The function can also be differentiated. We'll differentiate with respect
  1237. to fv_M using the ``diff`` method on an instance with the single positional
  1238. argument ``fv_M``.
  1239. >>> v_M_tilde.diff(fv_M)
  1240. cosh((-c3 + fv_M)/c0)/(c0*c1)
  1241. References
  1242. ==========
  1243. .. [1] De Groote, F., Kinney, A. L., Rao, A. V., & Fregly, B. J., Evaluation
  1244. of direct collocation optimal control problem formulations for
  1245. solving the muscle redundancy problem, Annals of biomedical
  1246. engineering, 44(10), (2016) pp. 2922-2936
  1247. """
  1248. @classmethod
  1249. def with_defaults(cls, fv_M):
  1250. r"""Recommended constructor that will use the published constants.
  1251. Explanation
  1252. ===========
  1253. Returns a new instance of the inverse muscle fiber force-velocity
  1254. function using the four constant values specified in the original
  1255. publication.
  1256. These have the values:
  1257. $c_0 = -0.318$
  1258. $c_1 = -8.149$
  1259. $c_2 = -0.374$
  1260. $c_3 = 0.886$
  1261. Parameters
  1262. ==========
  1263. fv_M : Any (sympifiable)
  1264. Normalized muscle fiber extension velocity.
  1265. """
  1266. c0 = Float('-0.318')
  1267. c1 = Float('-8.149')
  1268. c2 = Float('-0.374')
  1269. c3 = Float('0.886')
  1270. return cls(fv_M, c0, c1, c2, c3)
  1271. @classmethod
  1272. def eval(cls, fv_M, c0, c1, c2, c3):
  1273. """Evaluation of basic inputs.
  1274. Parameters
  1275. ==========
  1276. fv_M : Any (sympifiable)
  1277. Normalized muscle fiber force as a function of muscle fiber
  1278. extension velocity.
  1279. c0 : Any (sympifiable)
  1280. The first constant in the characteristic equation. The published
  1281. value is ``-0.318``.
  1282. c1 : Any (sympifiable)
  1283. The second constant in the characteristic equation. The published
  1284. value is ``-8.149``.
  1285. c2 : Any (sympifiable)
  1286. The third constant in the characteristic equation. The published
  1287. value is ``-0.374``.
  1288. c3 : Any (sympifiable)
  1289. The fourth constant in the characteristic equation. The published
  1290. value is ``0.886``.
  1291. """
  1292. pass
  1293. def _eval_evalf(self, prec):
  1294. """Evaluate the expression numerically using ``evalf``."""
  1295. return self.doit(deep=False, evaluate=False)._eval_evalf(prec)
  1296. def doit(self, deep=True, evaluate=True, **hints):
  1297. """Evaluate the expression defining the function.
  1298. Parameters
  1299. ==========
  1300. deep : bool
  1301. Whether ``doit`` should be recursively called. Default is ``True``.
  1302. evaluate : bool.
  1303. Whether the SymPy expression should be evaluated as it is
  1304. constructed. If ``False``, then no constant folding will be
  1305. conducted which will leave the expression in a more numerically-
  1306. stable for values of ``fv_M`` that correspond to a sensible
  1307. operating range for a musculotendon. Default is ``True``.
  1308. **kwargs : dict[str, Any]
  1309. Additional keyword argument pairs to be recursively passed to
  1310. ``doit``.
  1311. """
  1312. fv_M, *constants = self.args
  1313. if deep:
  1314. hints['evaluate'] = evaluate
  1315. fv_M = fv_M.doit(deep=deep, **hints)
  1316. c0, c1, c2, c3 = [c.doit(deep=deep, **hints) for c in constants]
  1317. else:
  1318. c0, c1, c2, c3 = constants
  1319. if evaluate:
  1320. return (sinh((fv_M - c3)/c0) - c2)/c1
  1321. return (sinh(UnevaluatedExpr(fv_M - c3)/c0) - c2)/c1
  1322. def fdiff(self, argindex=1):
  1323. """Derivative of the function with respect to a single argument.
  1324. Parameters
  1325. ==========
  1326. argindex : int
  1327. The index of the function's arguments with respect to which the
  1328. derivative should be taken. Argument indexes start at ``1``.
  1329. Default is ``1``.
  1330. """
  1331. fv_M, c0, c1, c2, c3 = self.args
  1332. if argindex == 1:
  1333. return cosh((fv_M - c3)/c0)/(c0*c1)
  1334. elif argindex == 2:
  1335. return (c3 - fv_M)*cosh((fv_M - c3)/c0)/(c0**2*c1)
  1336. elif argindex == 3:
  1337. return (c2 - sinh((fv_M - c3)/c0))/c1**2
  1338. elif argindex == 4:
  1339. return -1/c1
  1340. elif argindex == 5:
  1341. return -cosh((fv_M - c3)/c0)/(c0*c1)
  1342. raise ArgumentIndexError(self, argindex)
  1343. def inverse(self, argindex=1):
  1344. """Inverse function.
  1345. Parameters
  1346. ==========
  1347. argindex : int
  1348. Value to start indexing the arguments at. Default is ``1``.
  1349. """
  1350. return FiberForceVelocityDeGroote2016
  1351. def _latex(self, printer):
  1352. """Print a LaTeX representation of the function defining the curve.
  1353. Parameters
  1354. ==========
  1355. printer : Printer
  1356. The printer to be used to print the LaTeX string representation.
  1357. """
  1358. fv_M = self.args[0]
  1359. _fv_M = printer._print(fv_M)
  1360. return r'\left( \operatorname{fv}^M \right)^{-1} \left( %s \right)' % _fv_M
  1361. @dataclass(frozen=True)
  1362. class CharacteristicCurveCollection:
  1363. """Simple data container to group together related characteristic curves."""
  1364. tendon_force_length: CharacteristicCurveFunction
  1365. tendon_force_length_inverse: CharacteristicCurveFunction
  1366. fiber_force_length_passive: CharacteristicCurveFunction
  1367. fiber_force_length_passive_inverse: CharacteristicCurveFunction
  1368. fiber_force_length_active: CharacteristicCurveFunction
  1369. fiber_force_velocity: CharacteristicCurveFunction
  1370. fiber_force_velocity_inverse: CharacteristicCurveFunction
  1371. def __iter__(self):
  1372. """Iterator support for ``CharacteristicCurveCollection``."""
  1373. yield self.tendon_force_length
  1374. yield self.tendon_force_length_inverse
  1375. yield self.fiber_force_length_passive
  1376. yield self.fiber_force_length_passive_inverse
  1377. yield self.fiber_force_length_active
  1378. yield self.fiber_force_velocity
  1379. yield self.fiber_force_velocity_inverse