actuator.py 43 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147
  1. """Implementations of actuators for linked force and torque application."""
  2. from abc import ABC, abstractmethod
  3. from sympy import S, sympify, exp, sign
  4. from sympy.physics.mechanics.joint import PinJoint
  5. from sympy.physics.mechanics.loads import Torque
  6. from sympy.physics.mechanics.pathway import PathwayBase
  7. from sympy.physics.mechanics.rigidbody import RigidBody
  8. from sympy.physics.vector import ReferenceFrame, Vector
  9. __all__ = [
  10. 'ActuatorBase',
  11. 'ForceActuator',
  12. 'LinearDamper',
  13. 'LinearSpring',
  14. 'TorqueActuator',
  15. 'DuffingSpring',
  16. 'CoulombKineticFriction',
  17. ]
  18. class ActuatorBase(ABC):
  19. """Abstract base class for all actuator classes to inherit from.
  20. Notes
  21. =====
  22. Instances of this class cannot be directly instantiated by users. However,
  23. it can be used to created custom actuator types through subclassing.
  24. """
  25. def __init__(self):
  26. """Initializer for ``ActuatorBase``."""
  27. pass
  28. @abstractmethod
  29. def to_loads(self):
  30. """Loads required by the equations of motion method classes.
  31. Explanation
  32. ===========
  33. ``KanesMethod`` requires a list of ``Point``-``Vector`` tuples to be
  34. passed to the ``loads`` parameters of its ``kanes_equations`` method
  35. when constructing the equations of motion. This method acts as a
  36. utility to produce the correctly-structred pairs of points and vectors
  37. required so that these can be easily concatenated with other items in
  38. the list of loads and passed to ``KanesMethod.kanes_equations``. These
  39. loads are also in the correct form to also be passed to the other
  40. equations of motion method classes, e.g. ``LagrangesMethod``.
  41. """
  42. pass
  43. def __repr__(self):
  44. """Default representation of an actuator."""
  45. return f'{self.__class__.__name__}()'
  46. class ForceActuator(ActuatorBase):
  47. """Force-producing actuator.
  48. Explanation
  49. ===========
  50. A ``ForceActuator`` is an actuator that produces a (expansile) force along
  51. its length.
  52. A force actuator uses a pathway instance to determine the direction and
  53. number of forces that it applies to a system. Consider the simplest case
  54. where a ``LinearPathway`` instance is used. This pathway is made up of two
  55. points that can move relative to each other, and results in a pair of equal
  56. and opposite forces acting on the endpoints. If the positive time-varying
  57. Euclidean distance between the two points is defined, then the "extension
  58. velocity" is the time derivative of this distance. The extension velocity
  59. is positive when the two points are moving away from each other and
  60. negative when moving closer to each other. The direction for the force
  61. acting on either point is determined by constructing a unit vector directed
  62. from the other point to this point. This establishes a sign convention such
  63. that a positive force magnitude tends to push the points apart, this is the
  64. meaning of "expansile" in this context. The following diagram shows the
  65. positive force sense and the distance between the points::
  66. P Q
  67. o<--- F --->o
  68. | |
  69. |<--l(t)--->|
  70. Examples
  71. ========
  72. To construct an actuator, an expression (or symbol) must be supplied to
  73. represent the force it can produce, alongside a pathway specifying its line
  74. of action. Let's also create a global reference frame and spatially fix one
  75. of the points in it while setting the other to be positioned such that it
  76. can freely move in the frame's x direction specified by the coordinate
  77. ``q``.
  78. >>> from sympy import symbols
  79. >>> from sympy.physics.mechanics import (ForceActuator, LinearPathway,
  80. ... Point, ReferenceFrame)
  81. >>> from sympy.physics.vector import dynamicsymbols
  82. >>> N = ReferenceFrame('N')
  83. >>> q = dynamicsymbols('q')
  84. >>> force = symbols('F')
  85. >>> pA, pB = Point('pA'), Point('pB')
  86. >>> pA.set_vel(N, 0)
  87. >>> pB.set_pos(pA, q*N.x)
  88. >>> pB.pos_from(pA)
  89. q(t)*N.x
  90. >>> linear_pathway = LinearPathway(pA, pB)
  91. >>> actuator = ForceActuator(force, linear_pathway)
  92. >>> actuator
  93. ForceActuator(F, LinearPathway(pA, pB))
  94. Parameters
  95. ==========
  96. force : Expr
  97. The scalar expression defining the (expansile) force that the actuator
  98. produces.
  99. pathway : PathwayBase
  100. The pathway that the actuator follows. This must be an instance of a
  101. concrete subclass of ``PathwayBase``, e.g. ``LinearPathway``.
  102. """
  103. def __init__(self, force, pathway):
  104. """Initializer for ``ForceActuator``.
  105. Parameters
  106. ==========
  107. force : Expr
  108. The scalar expression defining the (expansile) force that the
  109. actuator produces.
  110. pathway : PathwayBase
  111. The pathway that the actuator follows. This must be an instance of
  112. a concrete subclass of ``PathwayBase``, e.g. ``LinearPathway``.
  113. """
  114. self.force = force
  115. self.pathway = pathway
  116. @property
  117. def force(self):
  118. """The magnitude of the force produced by the actuator."""
  119. return self._force
  120. @force.setter
  121. def force(self, force):
  122. if hasattr(self, '_force'):
  123. msg = (
  124. f'Can\'t set attribute `force` to {repr(force)} as it is '
  125. f'immutable.'
  126. )
  127. raise AttributeError(msg)
  128. self._force = sympify(force, strict=True)
  129. @property
  130. def pathway(self):
  131. """The ``Pathway`` defining the actuator's line of action."""
  132. return self._pathway
  133. @pathway.setter
  134. def pathway(self, pathway):
  135. if hasattr(self, '_pathway'):
  136. msg = (
  137. f'Can\'t set attribute `pathway` to {repr(pathway)} as it is '
  138. f'immutable.'
  139. )
  140. raise AttributeError(msg)
  141. if not isinstance(pathway, PathwayBase):
  142. msg = (
  143. f'Value {repr(pathway)} passed to `pathway` was of type '
  144. f'{type(pathway)}, must be {PathwayBase}.'
  145. )
  146. raise TypeError(msg)
  147. self._pathway = pathway
  148. def to_loads(self):
  149. """Loads required by the equations of motion method classes.
  150. Explanation
  151. ===========
  152. ``KanesMethod`` requires a list of ``Point``-``Vector`` tuples to be
  153. passed to the ``loads`` parameters of its ``kanes_equations`` method
  154. when constructing the equations of motion. This method acts as a
  155. utility to produce the correctly-structred pairs of points and vectors
  156. required so that these can be easily concatenated with other items in
  157. the list of loads and passed to ``KanesMethod.kanes_equations``. These
  158. loads are also in the correct form to also be passed to the other
  159. equations of motion method classes, e.g. ``LagrangesMethod``.
  160. Examples
  161. ========
  162. The below example shows how to generate the loads produced by a force
  163. actuator that follows a linear pathway. In this example we'll assume
  164. that the force actuator is being used to model a simple linear spring.
  165. First, create a linear pathway between two points separated by the
  166. coordinate ``q`` in the ``x`` direction of the global frame ``N``.
  167. >>> from sympy.physics.mechanics import (LinearPathway, Point,
  168. ... ReferenceFrame)
  169. >>> from sympy.physics.vector import dynamicsymbols
  170. >>> q = dynamicsymbols('q')
  171. >>> N = ReferenceFrame('N')
  172. >>> pA, pB = Point('pA'), Point('pB')
  173. >>> pB.set_pos(pA, q*N.x)
  174. >>> pathway = LinearPathway(pA, pB)
  175. Now create a symbol ``k`` to describe the spring's stiffness and
  176. instantiate a force actuator that produces a (contractile) force
  177. proportional to both the spring's stiffness and the pathway's length.
  178. Note that actuator classes use the sign convention that expansile
  179. forces are positive, so for a spring to produce a contractile force the
  180. spring force needs to be calculated as the negative for the stiffness
  181. multiplied by the length.
  182. >>> from sympy import symbols
  183. >>> from sympy.physics.mechanics import ForceActuator
  184. >>> stiffness = symbols('k')
  185. >>> spring_force = -stiffness*pathway.length
  186. >>> spring = ForceActuator(spring_force, pathway)
  187. The forces produced by the spring can be generated in the list of loads
  188. form that ``KanesMethod`` (and other equations of motion methods)
  189. requires by calling the ``to_loads`` method.
  190. >>> spring.to_loads()
  191. [(pA, k*q(t)*N.x), (pB, - k*q(t)*N.x)]
  192. A simple linear damper can be modeled in a similar way. Create another
  193. symbol ``c`` to describe the dampers damping coefficient. This time
  194. instantiate a force actuator that produces a force proportional to both
  195. the damper's damping coefficient and the pathway's extension velocity.
  196. Note that the damping force is negative as it acts in the opposite
  197. direction to which the damper is changing in length.
  198. >>> damping_coefficient = symbols('c')
  199. >>> damping_force = -damping_coefficient*pathway.extension_velocity
  200. >>> damper = ForceActuator(damping_force, pathway)
  201. Again, the forces produces by the damper can be generated by calling
  202. the ``to_loads`` method.
  203. >>> damper.to_loads()
  204. [(pA, c*Derivative(q(t), t)*N.x), (pB, - c*Derivative(q(t), t)*N.x)]
  205. """
  206. return self.pathway.to_loads(self.force)
  207. def __repr__(self):
  208. """Representation of a ``ForceActuator``."""
  209. return f'{self.__class__.__name__}({self.force}, {self.pathway})'
  210. class LinearSpring(ForceActuator):
  211. """A spring with its spring force as a linear function of its length.
  212. Explanation
  213. ===========
  214. Note that the "linear" in the name ``LinearSpring`` refers to the fact that
  215. the spring force is a linear function of the springs length. I.e. for a
  216. linear spring with stiffness ``k``, distance between its ends of ``x``, and
  217. an equilibrium length of ``0``, the spring force will be ``-k*x``, which is
  218. a linear function in ``x``. To create a spring that follows a linear, or
  219. straight, pathway between its two ends, a ``LinearPathway`` instance needs
  220. to be passed to the ``pathway`` parameter.
  221. A ``LinearSpring`` is a subclass of ``ForceActuator`` and so follows the
  222. same sign conventions for length, extension velocity, and the direction of
  223. the forces it applies to its points of attachment on bodies. The sign
  224. convention for the direction of forces is such that, for the case where a
  225. linear spring is instantiated with a ``LinearPathway`` instance as its
  226. pathway, they act to push the two ends of the spring away from one another.
  227. Because springs produces a contractile force and acts to pull the two ends
  228. together towards the equilibrium length when stretched, the scalar portion
  229. of the forces on the endpoint are negative in order to flip the sign of the
  230. forces on the endpoints when converted into vector quantities. The
  231. following diagram shows the positive force sense and the distance between
  232. the points::
  233. P Q
  234. o<--- F --->o
  235. | |
  236. |<--l(t)--->|
  237. Examples
  238. ========
  239. To construct a linear spring, an expression (or symbol) must be supplied to
  240. represent the stiffness (spring constant) of the spring, alongside a
  241. pathway specifying its line of action. Let's also create a global reference
  242. frame and spatially fix one of the points in it while setting the other to
  243. be positioned such that it can freely move in the frame's x direction
  244. specified by the coordinate ``q``.
  245. >>> from sympy import symbols
  246. >>> from sympy.physics.mechanics import (LinearPathway, LinearSpring,
  247. ... Point, ReferenceFrame)
  248. >>> from sympy.physics.vector import dynamicsymbols
  249. >>> N = ReferenceFrame('N')
  250. >>> q = dynamicsymbols('q')
  251. >>> stiffness = symbols('k')
  252. >>> pA, pB = Point('pA'), Point('pB')
  253. >>> pA.set_vel(N, 0)
  254. >>> pB.set_pos(pA, q*N.x)
  255. >>> pB.pos_from(pA)
  256. q(t)*N.x
  257. >>> linear_pathway = LinearPathway(pA, pB)
  258. >>> spring = LinearSpring(stiffness, linear_pathway)
  259. >>> spring
  260. LinearSpring(k, LinearPathway(pA, pB))
  261. This spring will produce a force that is proportional to both its stiffness
  262. and the pathway's length. Note that this force is negative as SymPy's sign
  263. convention for actuators is that negative forces are contractile.
  264. >>> spring.force
  265. -k*sqrt(q(t)**2)
  266. To create a linear spring with a non-zero equilibrium length, an expression
  267. (or symbol) can be passed to the ``equilibrium_length`` parameter on
  268. construction on a ``LinearSpring`` instance. Let's create a symbol ``l``
  269. to denote a non-zero equilibrium length and create another linear spring.
  270. >>> l = symbols('l')
  271. >>> spring = LinearSpring(stiffness, linear_pathway, equilibrium_length=l)
  272. >>> spring
  273. LinearSpring(k, LinearPathway(pA, pB), equilibrium_length=l)
  274. The spring force of this new spring is again proportional to both its
  275. stiffness and the pathway's length. However, the spring will not produce
  276. any force when ``q(t)`` equals ``l``. Note that the force will become
  277. expansile when ``q(t)`` is less than ``l``, as expected.
  278. >>> spring.force
  279. -k*(-l + sqrt(q(t)**2))
  280. Parameters
  281. ==========
  282. stiffness : Expr
  283. The spring constant.
  284. pathway : PathwayBase
  285. The pathway that the actuator follows. This must be an instance of a
  286. concrete subclass of ``PathwayBase``, e.g. ``LinearPathway``.
  287. equilibrium_length : Expr, optional
  288. The length at which the spring is in equilibrium, i.e. it produces no
  289. force. The default value is 0, i.e. the spring force is a linear
  290. function of the pathway's length with no constant offset.
  291. See Also
  292. ========
  293. ForceActuator: force-producing actuator (superclass of ``LinearSpring``).
  294. LinearPathway: straight-line pathway between a pair of points.
  295. """
  296. def __init__(self, stiffness, pathway, equilibrium_length=S.Zero):
  297. """Initializer for ``LinearSpring``.
  298. Parameters
  299. ==========
  300. stiffness : Expr
  301. The spring constant.
  302. pathway : PathwayBase
  303. The pathway that the actuator follows. This must be an instance of
  304. a concrete subclass of ``PathwayBase``, e.g. ``LinearPathway``.
  305. equilibrium_length : Expr, optional
  306. The length at which the spring is in equilibrium, i.e. it produces
  307. no force. The default value is 0, i.e. the spring force is a linear
  308. function of the pathway's length with no constant offset.
  309. """
  310. self.stiffness = stiffness
  311. self.pathway = pathway
  312. self.equilibrium_length = equilibrium_length
  313. @property
  314. def force(self):
  315. """The spring force produced by the linear spring."""
  316. return -self.stiffness*(self.pathway.length - self.equilibrium_length)
  317. @force.setter
  318. def force(self, force):
  319. raise AttributeError('Can\'t set computed attribute `force`.')
  320. @property
  321. def stiffness(self):
  322. """The spring constant for the linear spring."""
  323. return self._stiffness
  324. @stiffness.setter
  325. def stiffness(self, stiffness):
  326. if hasattr(self, '_stiffness'):
  327. msg = (
  328. f'Can\'t set attribute `stiffness` to {repr(stiffness)} as it '
  329. f'is immutable.'
  330. )
  331. raise AttributeError(msg)
  332. self._stiffness = sympify(stiffness, strict=True)
  333. @property
  334. def equilibrium_length(self):
  335. """The length of the spring at which it produces no force."""
  336. return self._equilibrium_length
  337. @equilibrium_length.setter
  338. def equilibrium_length(self, equilibrium_length):
  339. if hasattr(self, '_equilibrium_length'):
  340. msg = (
  341. f'Can\'t set attribute `equilibrium_length` to '
  342. f'{repr(equilibrium_length)} as it is immutable.'
  343. )
  344. raise AttributeError(msg)
  345. self._equilibrium_length = sympify(equilibrium_length, strict=True)
  346. def __repr__(self):
  347. """Representation of a ``LinearSpring``."""
  348. string = f'{self.__class__.__name__}({self.stiffness}, {self.pathway}'
  349. if self.equilibrium_length == S.Zero:
  350. string += ')'
  351. else:
  352. string += f', equilibrium_length={self.equilibrium_length})'
  353. return string
  354. class LinearDamper(ForceActuator):
  355. """A damper whose force is a linear function of its extension velocity.
  356. Explanation
  357. ===========
  358. Note that the "linear" in the name ``LinearDamper`` refers to the fact that
  359. the damping force is a linear function of the damper's rate of change in
  360. its length. I.e. for a linear damper with damping ``c`` and extension
  361. velocity ``v``, the damping force will be ``-c*v``, which is a linear
  362. function in ``v``. To create a damper that follows a linear, or straight,
  363. pathway between its two ends, a ``LinearPathway`` instance needs to be
  364. passed to the ``pathway`` parameter.
  365. A ``LinearDamper`` is a subclass of ``ForceActuator`` and so follows the
  366. same sign conventions for length, extension velocity, and the direction of
  367. the forces it applies to its points of attachment on bodies. The sign
  368. convention for the direction of forces is such that, for the case where a
  369. linear damper is instantiated with a ``LinearPathway`` instance as its
  370. pathway, they act to push the two ends of the damper away from one another.
  371. Because dampers produce a force that opposes the direction of change in
  372. length, when extension velocity is positive the scalar portions of the
  373. forces applied at the two endpoints are negative in order to flip the sign
  374. of the forces on the endpoints wen converted into vector quantities. When
  375. extension velocity is negative (i.e. when the damper is shortening), the
  376. scalar portions of the fofces applied are also negative so that the signs
  377. cancel producing forces on the endpoints that are in the same direction as
  378. the positive sign convention for the forces at the endpoints of the pathway
  379. (i.e. they act to push the endpoints away from one another). The following
  380. diagram shows the positive force sense and the distance between the
  381. points::
  382. P Q
  383. o<--- F --->o
  384. | |
  385. |<--l(t)--->|
  386. Examples
  387. ========
  388. To construct a linear damper, an expression (or symbol) must be supplied to
  389. represent the damping coefficient of the damper (we'll use the symbol
  390. ``c``), alongside a pathway specifying its line of action. Let's also
  391. create a global reference frame and spatially fix one of the points in it
  392. while setting the other to be positioned such that it can freely move in
  393. the frame's x direction specified by the coordinate ``q``. The velocity
  394. that the two points move away from one another can be specified by the
  395. coordinate ``u`` where ``u`` is the first time derivative of ``q``
  396. (i.e., ``u = Derivative(q(t), t)``).
  397. >>> from sympy import symbols
  398. >>> from sympy.physics.mechanics import (LinearDamper, LinearPathway,
  399. ... Point, ReferenceFrame)
  400. >>> from sympy.physics.vector import dynamicsymbols
  401. >>> N = ReferenceFrame('N')
  402. >>> q = dynamicsymbols('q')
  403. >>> damping = symbols('c')
  404. >>> pA, pB = Point('pA'), Point('pB')
  405. >>> pA.set_vel(N, 0)
  406. >>> pB.set_pos(pA, q*N.x)
  407. >>> pB.pos_from(pA)
  408. q(t)*N.x
  409. >>> pB.vel(N)
  410. Derivative(q(t), t)*N.x
  411. >>> linear_pathway = LinearPathway(pA, pB)
  412. >>> damper = LinearDamper(damping, linear_pathway)
  413. >>> damper
  414. LinearDamper(c, LinearPathway(pA, pB))
  415. This damper will produce a force that is proportional to both its damping
  416. coefficient and the pathway's extension length. Note that this force is
  417. negative as SymPy's sign convention for actuators is that negative forces
  418. are contractile and the damping force of the damper will oppose the
  419. direction of length change.
  420. >>> damper.force
  421. -c*sqrt(q(t)**2)*Derivative(q(t), t)/q(t)
  422. Parameters
  423. ==========
  424. damping : Expr
  425. The damping constant.
  426. pathway : PathwayBase
  427. The pathway that the actuator follows. This must be an instance of a
  428. concrete subclass of ``PathwayBase``, e.g. ``LinearPathway``.
  429. See Also
  430. ========
  431. ForceActuator: force-producing actuator (superclass of ``LinearDamper``).
  432. LinearPathway: straight-line pathway between a pair of points.
  433. """
  434. def __init__(self, damping, pathway):
  435. """Initializer for ``LinearDamper``.
  436. Parameters
  437. ==========
  438. damping : Expr
  439. The damping constant.
  440. pathway : PathwayBase
  441. The pathway that the actuator follows. This must be an instance of
  442. a concrete subclass of ``PathwayBase``, e.g. ``LinearPathway``.
  443. """
  444. self.damping = damping
  445. self.pathway = pathway
  446. @property
  447. def force(self):
  448. """The damping force produced by the linear damper."""
  449. return -self.damping*self.pathway.extension_velocity
  450. @force.setter
  451. def force(self, force):
  452. raise AttributeError('Can\'t set computed attribute `force`.')
  453. @property
  454. def damping(self):
  455. """The damping constant for the linear damper."""
  456. return self._damping
  457. @damping.setter
  458. def damping(self, damping):
  459. if hasattr(self, '_damping'):
  460. msg = (
  461. f'Can\'t set attribute `damping` to {repr(damping)} as it is '
  462. f'immutable.'
  463. )
  464. raise AttributeError(msg)
  465. self._damping = sympify(damping, strict=True)
  466. def __repr__(self):
  467. """Representation of a ``LinearDamper``."""
  468. return f'{self.__class__.__name__}({self.damping}, {self.pathway})'
  469. class TorqueActuator(ActuatorBase):
  470. """Torque-producing actuator.
  471. Explanation
  472. ===========
  473. A ``TorqueActuator`` is an actuator that produces a pair of equal and
  474. opposite torques on a pair of bodies.
  475. Examples
  476. ========
  477. To construct a torque actuator, an expression (or symbol) must be supplied
  478. to represent the torque it can produce, alongside a vector specifying the
  479. axis about which the torque will act, and a pair of frames on which the
  480. torque will act.
  481. >>> from sympy import symbols
  482. >>> from sympy.physics.mechanics import (ReferenceFrame, RigidBody,
  483. ... TorqueActuator)
  484. >>> N = ReferenceFrame('N')
  485. >>> A = ReferenceFrame('A')
  486. >>> torque = symbols('T')
  487. >>> axis = N.z
  488. >>> parent = RigidBody('parent', frame=N)
  489. >>> child = RigidBody('child', frame=A)
  490. >>> bodies = (child, parent)
  491. >>> actuator = TorqueActuator(torque, axis, *bodies)
  492. >>> actuator
  493. TorqueActuator(T, axis=N.z, target_frame=A, reaction_frame=N)
  494. Note that because torques actually act on frames, not bodies,
  495. ``TorqueActuator`` will extract the frame associated with a ``RigidBody``
  496. when one is passed instead of a ``ReferenceFrame``.
  497. Parameters
  498. ==========
  499. torque : Expr
  500. The scalar expression defining the torque that the actuator produces.
  501. axis : Vector
  502. The axis about which the actuator applies torques.
  503. target_frame : ReferenceFrame | RigidBody
  504. The primary frame on which the actuator will apply the torque.
  505. reaction_frame : ReferenceFrame | RigidBody | None
  506. The secondary frame on which the actuator will apply the torque. Note
  507. that the (equal and opposite) reaction torque is applied to this frame.
  508. """
  509. def __init__(self, torque, axis, target_frame, reaction_frame=None):
  510. """Initializer for ``TorqueActuator``.
  511. Parameters
  512. ==========
  513. torque : Expr
  514. The scalar expression defining the torque that the actuator
  515. produces.
  516. axis : Vector
  517. The axis about which the actuator applies torques.
  518. target_frame : ReferenceFrame | RigidBody
  519. The primary frame on which the actuator will apply the torque.
  520. reaction_frame : ReferenceFrame | RigidBody | None
  521. The secondary frame on which the actuator will apply the torque.
  522. Note that the (equal and opposite) reaction torque is applied to
  523. this frame.
  524. """
  525. self.torque = torque
  526. self.axis = axis
  527. self.target_frame = target_frame
  528. self.reaction_frame = reaction_frame
  529. @classmethod
  530. def at_pin_joint(cls, torque, pin_joint):
  531. """Alternate constructor to instantiate from a ``PinJoint`` instance.
  532. Examples
  533. ========
  534. To create a pin joint the ``PinJoint`` class requires a name, parent
  535. body, and child body to be passed to its constructor. It is also
  536. possible to control the joint axis using the ``joint_axis`` keyword
  537. argument. In this example let's use the parent body's reference frame's
  538. z-axis as the joint axis.
  539. >>> from sympy.physics.mechanics import (PinJoint, ReferenceFrame,
  540. ... RigidBody, TorqueActuator)
  541. >>> N = ReferenceFrame('N')
  542. >>> A = ReferenceFrame('A')
  543. >>> parent = RigidBody('parent', frame=N)
  544. >>> child = RigidBody('child', frame=A)
  545. >>> pin_joint = PinJoint(
  546. ... 'pin',
  547. ... parent,
  548. ... child,
  549. ... joint_axis=N.z,
  550. ... )
  551. Let's also create a symbol ``T`` that will represent the torque applied
  552. by the torque actuator.
  553. >>> from sympy import symbols
  554. >>> torque = symbols('T')
  555. To create the torque actuator from the ``torque`` and ``pin_joint``
  556. variables previously instantiated, these can be passed to the alternate
  557. constructor class method ``at_pin_joint`` of the ``TorqueActuator``
  558. class. It should be noted that a positive torque will cause a positive
  559. displacement of the joint coordinate or that the torque is applied on
  560. the child body with a reaction torque on the parent.
  561. >>> actuator = TorqueActuator.at_pin_joint(torque, pin_joint)
  562. >>> actuator
  563. TorqueActuator(T, axis=N.z, target_frame=A, reaction_frame=N)
  564. Parameters
  565. ==========
  566. torque : Expr
  567. The scalar expression defining the torque that the actuator
  568. produces.
  569. pin_joint : PinJoint
  570. The pin joint, and by association the parent and child bodies, on
  571. which the torque actuator will act. The pair of bodies acted upon
  572. by the torque actuator are the parent and child bodies of the pin
  573. joint, with the child acting as the reaction body. The pin joint's
  574. axis is used as the axis about which the torque actuator will apply
  575. its torque.
  576. """
  577. if not isinstance(pin_joint, PinJoint):
  578. msg = (
  579. f'Value {repr(pin_joint)} passed to `pin_joint` was of type '
  580. f'{type(pin_joint)}, must be {PinJoint}.'
  581. )
  582. raise TypeError(msg)
  583. return cls(
  584. torque,
  585. pin_joint.joint_axis,
  586. pin_joint.child_interframe,
  587. pin_joint.parent_interframe,
  588. )
  589. @property
  590. def torque(self):
  591. """The magnitude of the torque produced by the actuator."""
  592. return self._torque
  593. @torque.setter
  594. def torque(self, torque):
  595. if hasattr(self, '_torque'):
  596. msg = (
  597. f'Can\'t set attribute `torque` to {repr(torque)} as it is '
  598. f'immutable.'
  599. )
  600. raise AttributeError(msg)
  601. self._torque = sympify(torque, strict=True)
  602. @property
  603. def axis(self):
  604. """The axis about which the torque acts."""
  605. return self._axis
  606. @axis.setter
  607. def axis(self, axis):
  608. if hasattr(self, '_axis'):
  609. msg = (
  610. f'Can\'t set attribute `axis` to {repr(axis)} as it is '
  611. f'immutable.'
  612. )
  613. raise AttributeError(msg)
  614. if not isinstance(axis, Vector):
  615. msg = (
  616. f'Value {repr(axis)} passed to `axis` was of type '
  617. f'{type(axis)}, must be {Vector}.'
  618. )
  619. raise TypeError(msg)
  620. self._axis = axis
  621. @property
  622. def target_frame(self):
  623. """The primary reference frames on which the torque will act."""
  624. return self._target_frame
  625. @target_frame.setter
  626. def target_frame(self, target_frame):
  627. if hasattr(self, '_target_frame'):
  628. msg = (
  629. f'Can\'t set attribute `target_frame` to {repr(target_frame)} '
  630. f'as it is immutable.'
  631. )
  632. raise AttributeError(msg)
  633. if isinstance(target_frame, RigidBody):
  634. target_frame = target_frame.frame
  635. elif not isinstance(target_frame, ReferenceFrame):
  636. msg = (
  637. f'Value {repr(target_frame)} passed to `target_frame` was of '
  638. f'type {type(target_frame)}, must be {ReferenceFrame}.'
  639. )
  640. raise TypeError(msg)
  641. self._target_frame = target_frame
  642. @property
  643. def reaction_frame(self):
  644. """The primary reference frames on which the torque will act."""
  645. return self._reaction_frame
  646. @reaction_frame.setter
  647. def reaction_frame(self, reaction_frame):
  648. if hasattr(self, '_reaction_frame'):
  649. msg = (
  650. f'Can\'t set attribute `reaction_frame` to '
  651. f'{repr(reaction_frame)} as it is immutable.'
  652. )
  653. raise AttributeError(msg)
  654. if isinstance(reaction_frame, RigidBody):
  655. reaction_frame = reaction_frame.frame
  656. elif (
  657. not isinstance(reaction_frame, ReferenceFrame)
  658. and reaction_frame is not None
  659. ):
  660. msg = (
  661. f'Value {repr(reaction_frame)} passed to `reaction_frame` was '
  662. f'of type {type(reaction_frame)}, must be {ReferenceFrame}.'
  663. )
  664. raise TypeError(msg)
  665. self._reaction_frame = reaction_frame
  666. def to_loads(self):
  667. """Loads required by the equations of motion method classes.
  668. Explanation
  669. ===========
  670. ``KanesMethod`` requires a list of ``Point``-``Vector`` tuples to be
  671. passed to the ``loads`` parameters of its ``kanes_equations`` method
  672. when constructing the equations of motion. This method acts as a
  673. utility to produce the correctly-structred pairs of points and vectors
  674. required so that these can be easily concatenated with other items in
  675. the list of loads and passed to ``KanesMethod.kanes_equations``. These
  676. loads are also in the correct form to also be passed to the other
  677. equations of motion method classes, e.g. ``LagrangesMethod``.
  678. Examples
  679. ========
  680. The below example shows how to generate the loads produced by a torque
  681. actuator that acts on a pair of bodies attached by a pin joint.
  682. >>> from sympy import symbols
  683. >>> from sympy.physics.mechanics import (PinJoint, ReferenceFrame,
  684. ... RigidBody, TorqueActuator)
  685. >>> torque = symbols('T')
  686. >>> N = ReferenceFrame('N')
  687. >>> A = ReferenceFrame('A')
  688. >>> parent = RigidBody('parent', frame=N)
  689. >>> child = RigidBody('child', frame=A)
  690. >>> pin_joint = PinJoint(
  691. ... 'pin',
  692. ... parent,
  693. ... child,
  694. ... joint_axis=N.z,
  695. ... )
  696. >>> actuator = TorqueActuator.at_pin_joint(torque, pin_joint)
  697. The forces produces by the damper can be generated by calling the
  698. ``to_loads`` method.
  699. >>> actuator.to_loads()
  700. [(A, T*N.z), (N, - T*N.z)]
  701. Alternatively, if a torque actuator is created without a reaction frame
  702. then the loads returned by the ``to_loads`` method will contain just
  703. the single load acting on the target frame.
  704. >>> actuator = TorqueActuator(torque, N.z, N)
  705. >>> actuator.to_loads()
  706. [(N, T*N.z)]
  707. """
  708. loads = [
  709. Torque(self.target_frame, self.torque*self.axis),
  710. ]
  711. if self.reaction_frame is not None:
  712. loads.append(Torque(self.reaction_frame, -self.torque*self.axis))
  713. return loads
  714. def __repr__(self):
  715. """Representation of a ``TorqueActuator``."""
  716. string = (
  717. f'{self.__class__.__name__}({self.torque}, axis={self.axis}, '
  718. f'target_frame={self.target_frame}'
  719. )
  720. if self.reaction_frame is not None:
  721. string += f', reaction_frame={self.reaction_frame})'
  722. else:
  723. string += ')'
  724. return string
  725. class DuffingSpring(ForceActuator):
  726. """A nonlinear spring based on the Duffing equation.
  727. Explanation
  728. ===========
  729. Here, ``DuffingSpring`` represents the force exerted by a nonlinear spring based on the Duffing equation:
  730. F = -beta*x-alpha*x**3, where x is the displacement from the equilibrium position, beta is the linear spring constant,
  731. and alpha is the coefficient for the nonlinear cubic term.
  732. Parameters
  733. ==========
  734. linear_stiffness : Expr
  735. The linear stiffness coefficient (beta).
  736. nonlinear_stiffness : Expr
  737. The nonlinear stiffness coefficient (alpha).
  738. pathway : PathwayBase
  739. The pathway that the actuator follows.
  740. equilibrium_length : Expr, optional
  741. The length at which the spring is in equilibrium (x).
  742. """
  743. def __init__(self, linear_stiffness, nonlinear_stiffness, pathway, equilibrium_length=S.Zero):
  744. self.linear_stiffness = sympify(linear_stiffness, strict=True)
  745. self.nonlinear_stiffness = sympify(nonlinear_stiffness, strict=True)
  746. self.equilibrium_length = sympify(equilibrium_length, strict=True)
  747. if not isinstance(pathway, PathwayBase):
  748. raise TypeError("pathway must be an instance of PathwayBase.")
  749. self._pathway = pathway
  750. @property
  751. def linear_stiffness(self):
  752. return self._linear_stiffness
  753. @linear_stiffness.setter
  754. def linear_stiffness(self, linear_stiffness):
  755. if hasattr(self, '_linear_stiffness'):
  756. msg = (
  757. f'Can\'t set attribute `linear_stiffness` to '
  758. f'{repr(linear_stiffness)} as it is immutable.'
  759. )
  760. raise AttributeError(msg)
  761. self._linear_stiffness = sympify(linear_stiffness, strict=True)
  762. @property
  763. def nonlinear_stiffness(self):
  764. return self._nonlinear_stiffness
  765. @nonlinear_stiffness.setter
  766. def nonlinear_stiffness(self, nonlinear_stiffness):
  767. if hasattr(self, '_nonlinear_stiffness'):
  768. msg = (
  769. f'Can\'t set attribute `nonlinear_stiffness` to '
  770. f'{repr(nonlinear_stiffness)} as it is immutable.'
  771. )
  772. raise AttributeError(msg)
  773. self._nonlinear_stiffness = sympify(nonlinear_stiffness, strict=True)
  774. @property
  775. def pathway(self):
  776. return self._pathway
  777. @pathway.setter
  778. def pathway(self, pathway):
  779. if hasattr(self, '_pathway'):
  780. msg = (
  781. f'Can\'t set attribute `pathway` to {repr(pathway)} as it is '
  782. f'immutable.'
  783. )
  784. raise AttributeError(msg)
  785. if not isinstance(pathway, PathwayBase):
  786. msg = (
  787. f'Value {repr(pathway)} passed to `pathway` was of type '
  788. f'{type(pathway)}, must be {PathwayBase}.'
  789. )
  790. raise TypeError(msg)
  791. self._pathway = pathway
  792. @property
  793. def equilibrium_length(self):
  794. return self._equilibrium_length
  795. @equilibrium_length.setter
  796. def equilibrium_length(self, equilibrium_length):
  797. if hasattr(self, '_equilibrium_length'):
  798. msg = (
  799. f'Can\'t set attribute `equilibrium_length` to '
  800. f'{repr(equilibrium_length)} as it is immutable.'
  801. )
  802. raise AttributeError(msg)
  803. self._equilibrium_length = sympify(equilibrium_length, strict=True)
  804. @property
  805. def force(self):
  806. """The force produced by the Duffing spring."""
  807. displacement = self.pathway.length - self.equilibrium_length
  808. return -self.linear_stiffness * displacement - self.nonlinear_stiffness * displacement**3
  809. @force.setter
  810. def force(self, force):
  811. if hasattr(self, '_force'):
  812. msg = (
  813. f'Can\'t set attribute `force` to {repr(force)} as it is '
  814. f'immutable.'
  815. )
  816. raise AttributeError(msg)
  817. self._force = sympify(force, strict=True)
  818. def __repr__(self):
  819. return (f"{self.__class__.__name__}("
  820. f"{self.linear_stiffness}, {self.nonlinear_stiffness}, {self.pathway}, "
  821. f"equilibrium_length={self.equilibrium_length})")
  822. class CoulombKineticFriction(ForceActuator):
  823. r"""Coulomb kinetic friction with Stribeck and viscous effects.
  824. Explanation
  825. ===========
  826. This represents a Coulomb kinetic friction with the Stribeck and viscous effect,
  827. described by the function:
  828. .. math::
  829. F = (\mu_k f_n + (\mu_s - \mu_k) f_n e^{-(\frac{v}{v_s})^2}) \text{sign}(v) + \sigma v
  830. where :math:`\mu_k` is the coefficient of kinetic friction, :math:`\mu_s` is the
  831. coefficient of static friction, :math:`f_n` is the normal force, :math:`v` is the
  832. relative velocity, :math:`v_s` is the Stribeck friction coefficient, and
  833. :math:`\sigma` is the viscous friction constant.
  834. The default friction force is :math:`F = \mu_k f_n`.
  835. When specified, the actuator includes:
  836. - Stribeck effect: :math:`(\mu_s - \mu_k) f_n e^{-(\frac{v}{v_s})^2}`
  837. - Viscous effect: :math:`\sigma v`
  838. Notes
  839. =====
  840. The actuator makes the following assumptions:
  841. - The actuator assumes relative motion is non-zero.
  842. - The normal force is assumed to be a non-negative scalar.
  843. - The resultant friction force is opposite to the velocity direction.
  844. - Each point in the pathway is fixed within separate objects that are sliding relative to each other. In other words, these two points are fixed in the mutually sliding objects.
  845. This actuator has been tested for straightforward motions, like a block sliding
  846. on a surface.
  847. The friction force is defined to always oppose the direction of relative velocity :math:`v`.
  848. Specifically:
  849. - The default Coulomb friction force :math:`\mu_k f_n \text{sign}(v)` is opposite to :math:`v`.
  850. - The Stribeck effect :math:`(\mu_s - \mu_k) f_n e^{-(\frac{v}{v_s})^2} \text{sign}(v)` is also opposite to :math:`v`.
  851. - The viscous friction term :math:`\sigma v` is opposite to :math:`v`.
  852. Examples
  853. ========
  854. The below example shows how to generate the loads produced by a Coulomb kinetic
  855. friction actuator in a mass-spring system with friction.
  856. >>> import sympy as sm
  857. >>> from sympy.physics.mechanics import (dynamicsymbols, ReferenceFrame, Point,
  858. ... LinearPathway, CoulombKineticFriction, LinearSpring, KanesMethod, Particle)
  859. >>> x, v = dynamicsymbols('x, v', real=True)
  860. >>> m, g, k, mu_k, mu_s, v_s, sigma = sm.symbols('m, g, k, mu_k, mu_s, v_s, sigma')
  861. >>> N = ReferenceFrame('N')
  862. >>> O, P = Point('O'), Point('P')
  863. >>> O.set_vel(N, 0)
  864. >>> P.set_pos(O, x*N.x)
  865. >>> pathway = LinearPathway(O, P)
  866. >>> friction = CoulombKineticFriction(mu_k, m*g, pathway, v_s=v_s, sigma=sigma, mu_s=mu_k)
  867. >>> spring = LinearSpring(k, pathway)
  868. >>> block = Particle('block', point=P, mass=m)
  869. >>> kane = KanesMethod(N, (x,), (v,), kd_eqs=(x.diff() - v,))
  870. >>> friction.to_loads()
  871. [(O, (g*m*mu_k*sign(sign(x(t))*Derivative(x(t), t)) + sigma*sign(x(t))*Derivative(x(t), t))*x(t)/Abs(x(t))*N.x), (P, (-g*m*mu_k*sign(sign(x(t))*Derivative(x(t), t)) - sigma*sign(x(t))*Derivative(x(t), t))*x(t)/Abs(x(t))*N.x)]
  872. >>> loads = friction.to_loads() + spring.to_loads()
  873. >>> fr, frstar = kane.kanes_equations([block], loads)
  874. >>> eom = fr + frstar
  875. >>> eom
  876. Matrix([[-k*x(t) - m*Derivative(v(t), t) + (-g*m*mu_k*sign(v(t)*sign(x(t))) - sigma*v(t)*sign(x(t)))*x(t)/Abs(x(t))]])
  877. Parameters
  878. ==========
  879. f_n : sympifiable
  880. The normal force between the surfaces. It should always be a non-negative scalar.
  881. mu_k : sympifiable
  882. The coefficient of kinetic friction.
  883. pathway : PathwayBase
  884. The pathway that the actuator follows.
  885. v_s : sympifiable, optional
  886. The Stribeck friction coefficient.
  887. sigma : sympifiable, optional
  888. The viscous friction coefficient.
  889. mu_s : sympifiable, optional
  890. The coefficient of static friction. Defaults to mu_k, meaning the Stribeck effect evaluates to 0 by default.
  891. References
  892. ==========
  893. .. [Moore2022] https://moorepants.github.io/learn-multibody-dynamics/loads.html#friction.
  894. .. [Flores2023] Paulo Flores, Jorge Ambrosio, Hamid M. Lankarani,
  895. "Contact-impact events with friction in multibody dynamics: Back to basics",
  896. Mechanism and Machine Theory, vol. 184, 2023. https://doi.org/10.1016/j.mechmachtheory.2023.105305.
  897. .. [Rogner2017] I. Rogner, "Friction modelling for robotic applications with planar motion",
  898. Chalmers University of Technology, Department of Electrical Engineering, 2017.
  899. """
  900. def __init__(self, mu_k, f_n, pathway, *, v_s=None, sigma=None, mu_s=None):
  901. self._mu_k = sympify(mu_k, strict=True) if mu_k is not None else 1
  902. self._mu_s = sympify(mu_s, strict=True) if mu_s is not None else self._mu_k
  903. self._f_n = sympify(f_n, strict=True)
  904. self._sigma = sympify(sigma, strict=True) if sigma is not None else 0
  905. self._v_s = sympify(v_s, strict=True) if v_s is not None or v_s == 0 else 0.01
  906. self.pathway = pathway
  907. @property
  908. def mu_k(self):
  909. """The coefficient of kinetic friction."""
  910. return self._mu_k
  911. @property
  912. def mu_s(self):
  913. """The coefficient of static friction."""
  914. return self._mu_s
  915. @property
  916. def f_n(self):
  917. """The normal force between the surfaces."""
  918. return self._f_n
  919. @property
  920. def sigma(self):
  921. """The viscous friction coefficient."""
  922. return self._sigma
  923. @property
  924. def v_s(self):
  925. """The Stribeck friction coefficient."""
  926. return self._v_s
  927. @property
  928. def force(self):
  929. v = self.pathway.extension_velocity
  930. f_c = self.mu_k * self.f_n
  931. f_max = self.mu_s * self.f_n
  932. stribeck_term = (f_max - f_c) * exp(-(v / self.v_s)**2) if self.v_s is not None else 0
  933. viscous_term = self.sigma * v if self.sigma is not None else 0
  934. return (f_c + stribeck_term) * -sign(v) - viscous_term
  935. @force.setter
  936. def force(self, force):
  937. raise AttributeError('Can\'t set computed attribute `force`.')
  938. def __repr__(self):
  939. return (f'{self.__class__.__name__}({self._mu_k}, {self._mu_s} '
  940. f'{self._f_n}, {self.pathway}, {self._v_s}, '
  941. f'{self._sigma})')