test_actuator.py 40 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084
  1. """Tests for the ``sympy.physics.mechanics.actuator.py`` module."""
  2. import pytest
  3. from sympy import (
  4. S,
  5. Matrix,
  6. Symbol,
  7. SympifyError,
  8. sqrt,
  9. Abs,
  10. symbols,
  11. exp,
  12. sign,
  13. )
  14. from sympy.physics.mechanics import (
  15. ActuatorBase,
  16. Force,
  17. ForceActuator,
  18. KanesMethod,
  19. LinearDamper,
  20. LinearPathway,
  21. LinearSpring,
  22. Particle,
  23. PinJoint,
  24. Point,
  25. ReferenceFrame,
  26. RigidBody,
  27. TorqueActuator,
  28. Vector,
  29. dynamicsymbols,
  30. DuffingSpring,
  31. CoulombKineticFriction,
  32. )
  33. from sympy.core.expr import Expr as ExprType
  34. target = RigidBody('target')
  35. reaction = RigidBody('reaction')
  36. class TestForceActuator:
  37. @pytest.fixture(autouse=True)
  38. def _linear_pathway_fixture(self):
  39. self.force = Symbol('F')
  40. self.pA = Point('pA')
  41. self.pB = Point('pB')
  42. self.pathway = LinearPathway(self.pA, self.pB)
  43. self.q1 = dynamicsymbols('q1')
  44. self.q2 = dynamicsymbols('q2')
  45. self.q3 = dynamicsymbols('q3')
  46. self.q1d = dynamicsymbols('q1', 1)
  47. self.q2d = dynamicsymbols('q2', 1)
  48. self.q3d = dynamicsymbols('q3', 1)
  49. self.N = ReferenceFrame('N')
  50. def test_is_actuator_base_subclass(self):
  51. assert issubclass(ForceActuator, ActuatorBase)
  52. @pytest.mark.parametrize(
  53. 'force, expected_force',
  54. [
  55. (1, S.One),
  56. (S.One, S.One),
  57. (Symbol('F'), Symbol('F')),
  58. (dynamicsymbols('F'), dynamicsymbols('F')),
  59. (Symbol('F')**2 + Symbol('F'), Symbol('F')**2 + Symbol('F')),
  60. ]
  61. )
  62. def test_valid_constructor_force(self, force, expected_force):
  63. instance = ForceActuator(force, self.pathway)
  64. assert isinstance(instance, ForceActuator)
  65. assert hasattr(instance, 'force')
  66. assert isinstance(instance.force, ExprType)
  67. assert instance.force == expected_force
  68. @pytest.mark.parametrize('force', [None, 'F'])
  69. def test_invalid_constructor_force_not_sympifyable(self, force):
  70. with pytest.raises(SympifyError):
  71. _ = ForceActuator(force, self.pathway)
  72. @pytest.mark.parametrize(
  73. 'pathway',
  74. [
  75. LinearPathway(Point('pA'), Point('pB')),
  76. ]
  77. )
  78. def test_valid_constructor_pathway(self, pathway):
  79. instance = ForceActuator(self.force, pathway)
  80. assert isinstance(instance, ForceActuator)
  81. assert hasattr(instance, 'pathway')
  82. assert isinstance(instance.pathway, LinearPathway)
  83. assert instance.pathway == pathway
  84. def test_invalid_constructor_pathway_not_pathway_base(self):
  85. with pytest.raises(TypeError):
  86. _ = ForceActuator(self.force, None)
  87. @pytest.mark.parametrize(
  88. 'property_name, fixture_attr_name',
  89. [
  90. ('force', 'force'),
  91. ('pathway', 'pathway'),
  92. ]
  93. )
  94. def test_properties_are_immutable(self, property_name, fixture_attr_name):
  95. instance = ForceActuator(self.force, self.pathway)
  96. value = getattr(self, fixture_attr_name)
  97. with pytest.raises(AttributeError):
  98. setattr(instance, property_name, value)
  99. def test_repr(self):
  100. actuator = ForceActuator(self.force, self.pathway)
  101. expected = "ForceActuator(F, LinearPathway(pA, pB))"
  102. assert repr(actuator) == expected
  103. def test_to_loads_static_pathway(self):
  104. self.pB.set_pos(self.pA, 2*self.N.x)
  105. actuator = ForceActuator(self.force, self.pathway)
  106. expected = [
  107. (self.pA, - self.force*self.N.x),
  108. (self.pB, self.force*self.N.x),
  109. ]
  110. assert actuator.to_loads() == expected
  111. def test_to_loads_2D_pathway(self):
  112. self.pB.set_pos(self.pA, 2*self.q1*self.N.x)
  113. actuator = ForceActuator(self.force, self.pathway)
  114. expected = [
  115. (self.pA, - self.force*(self.q1/sqrt(self.q1**2))*self.N.x),
  116. (self.pB, self.force*(self.q1/sqrt(self.q1**2))*self.N.x),
  117. ]
  118. assert actuator.to_loads() == expected
  119. def test_to_loads_3D_pathway(self):
  120. self.pB.set_pos(
  121. self.pA,
  122. self.q1*self.N.x - self.q2*self.N.y + 2*self.q3*self.N.z,
  123. )
  124. actuator = ForceActuator(self.force, self.pathway)
  125. length = sqrt(self.q1**2 + self.q2**2 + 4*self.q3**2)
  126. pO_force = (
  127. - self.force*self.q1*self.N.x/length
  128. + self.force*self.q2*self.N.y/length
  129. - 2*self.force*self.q3*self.N.z/length
  130. )
  131. pI_force = (
  132. self.force*self.q1*self.N.x/length
  133. - self.force*self.q2*self.N.y/length
  134. + 2*self.force*self.q3*self.N.z/length
  135. )
  136. expected = [
  137. (self.pA, pO_force),
  138. (self.pB, pI_force),
  139. ]
  140. assert actuator.to_loads() == expected
  141. class TestLinearSpring:
  142. @pytest.fixture(autouse=True)
  143. def _linear_spring_fixture(self):
  144. self.stiffness = Symbol('k')
  145. self.l = Symbol('l')
  146. self.pA = Point('pA')
  147. self.pB = Point('pB')
  148. self.pathway = LinearPathway(self.pA, self.pB)
  149. self.q = dynamicsymbols('q')
  150. self.N = ReferenceFrame('N')
  151. def test_is_force_actuator_subclass(self):
  152. assert issubclass(LinearSpring, ForceActuator)
  153. def test_is_actuator_base_subclass(self):
  154. assert issubclass(LinearSpring, ActuatorBase)
  155. @pytest.mark.parametrize(
  156. (
  157. 'stiffness, '
  158. 'expected_stiffness, '
  159. 'equilibrium_length, '
  160. 'expected_equilibrium_length, '
  161. 'force'
  162. ),
  163. [
  164. (
  165. 1,
  166. S.One,
  167. 0,
  168. S.Zero,
  169. -sqrt(dynamicsymbols('q')**2),
  170. ),
  171. (
  172. Symbol('k'),
  173. Symbol('k'),
  174. 0,
  175. S.Zero,
  176. -Symbol('k')*sqrt(dynamicsymbols('q')**2),
  177. ),
  178. (
  179. Symbol('k'),
  180. Symbol('k'),
  181. S.Zero,
  182. S.Zero,
  183. -Symbol('k')*sqrt(dynamicsymbols('q')**2),
  184. ),
  185. (
  186. Symbol('k'),
  187. Symbol('k'),
  188. Symbol('l'),
  189. Symbol('l'),
  190. -Symbol('k')*(sqrt(dynamicsymbols('q')**2) - Symbol('l')),
  191. ),
  192. ]
  193. )
  194. def test_valid_constructor(
  195. self,
  196. stiffness,
  197. expected_stiffness,
  198. equilibrium_length,
  199. expected_equilibrium_length,
  200. force,
  201. ):
  202. self.pB.set_pos(self.pA, self.q*self.N.x)
  203. spring = LinearSpring(stiffness, self.pathway, equilibrium_length)
  204. assert isinstance(spring, LinearSpring)
  205. assert hasattr(spring, 'stiffness')
  206. assert isinstance(spring.stiffness, ExprType)
  207. assert spring.stiffness == expected_stiffness
  208. assert hasattr(spring, 'pathway')
  209. assert isinstance(spring.pathway, LinearPathway)
  210. assert spring.pathway == self.pathway
  211. assert hasattr(spring, 'equilibrium_length')
  212. assert isinstance(spring.equilibrium_length, ExprType)
  213. assert spring.equilibrium_length == expected_equilibrium_length
  214. assert hasattr(spring, 'force')
  215. assert isinstance(spring.force, ExprType)
  216. assert spring.force == force
  217. @pytest.mark.parametrize('stiffness', [None, 'k'])
  218. def test_invalid_constructor_stiffness_not_sympifyable(self, stiffness):
  219. with pytest.raises(SympifyError):
  220. _ = LinearSpring(stiffness, self.pathway, self.l)
  221. def test_invalid_constructor_pathway_not_pathway_base(self):
  222. with pytest.raises(TypeError):
  223. _ = LinearSpring(self.stiffness, None, self.l)
  224. @pytest.mark.parametrize('equilibrium_length', [None, 'l'])
  225. def test_invalid_constructor_equilibrium_length_not_sympifyable(
  226. self,
  227. equilibrium_length,
  228. ):
  229. with pytest.raises(SympifyError):
  230. _ = LinearSpring(self.stiffness, self.pathway, equilibrium_length)
  231. @pytest.mark.parametrize(
  232. 'property_name, fixture_attr_name',
  233. [
  234. ('stiffness', 'stiffness'),
  235. ('pathway', 'pathway'),
  236. ('equilibrium_length', 'l'),
  237. ]
  238. )
  239. def test_properties_are_immutable(self, property_name, fixture_attr_name):
  240. spring = LinearSpring(self.stiffness, self.pathway, self.l)
  241. value = getattr(self, fixture_attr_name)
  242. with pytest.raises(AttributeError):
  243. setattr(spring, property_name, value)
  244. @pytest.mark.parametrize(
  245. 'equilibrium_length, expected',
  246. [
  247. (S.Zero, 'LinearSpring(k, LinearPathway(pA, pB))'),
  248. (
  249. Symbol('l'),
  250. 'LinearSpring(k, LinearPathway(pA, pB), equilibrium_length=l)',
  251. ),
  252. ]
  253. )
  254. def test_repr(self, equilibrium_length, expected):
  255. self.pB.set_pos(self.pA, self.q*self.N.x)
  256. spring = LinearSpring(self.stiffness, self.pathway, equilibrium_length)
  257. assert repr(spring) == expected
  258. def test_to_loads(self):
  259. self.pB.set_pos(self.pA, self.q*self.N.x)
  260. spring = LinearSpring(self.stiffness, self.pathway, self.l)
  261. normal = self.q/sqrt(self.q**2)*self.N.x
  262. pA_force = self.stiffness*(sqrt(self.q**2) - self.l)*normal
  263. pB_force = -self.stiffness*(sqrt(self.q**2) - self.l)*normal
  264. expected = [Force(self.pA, pA_force), Force(self.pB, pB_force)]
  265. loads = spring.to_loads()
  266. for load, (point, vector) in zip(loads, expected):
  267. assert isinstance(load, Force)
  268. assert load.point == point
  269. assert (load.vector - vector).simplify() == 0
  270. class TestLinearDamper:
  271. @pytest.fixture(autouse=True)
  272. def _linear_damper_fixture(self):
  273. self.damping = Symbol('c')
  274. self.l = Symbol('l')
  275. self.pA = Point('pA')
  276. self.pB = Point('pB')
  277. self.pathway = LinearPathway(self.pA, self.pB)
  278. self.q = dynamicsymbols('q')
  279. self.dq = dynamicsymbols('q', 1)
  280. self.u = dynamicsymbols('u')
  281. self.N = ReferenceFrame('N')
  282. def test_is_force_actuator_subclass(self):
  283. assert issubclass(LinearDamper, ForceActuator)
  284. def test_is_actuator_base_subclass(self):
  285. assert issubclass(LinearDamper, ActuatorBase)
  286. def test_valid_constructor(self):
  287. self.pB.set_pos(self.pA, self.q*self.N.x)
  288. damper = LinearDamper(self.damping, self.pathway)
  289. assert isinstance(damper, LinearDamper)
  290. assert hasattr(damper, 'damping')
  291. assert isinstance(damper.damping, ExprType)
  292. assert damper.damping == self.damping
  293. assert hasattr(damper, 'pathway')
  294. assert isinstance(damper.pathway, LinearPathway)
  295. assert damper.pathway == self.pathway
  296. def test_valid_constructor_force(self):
  297. self.pB.set_pos(self.pA, self.q*self.N.x)
  298. damper = LinearDamper(self.damping, self.pathway)
  299. expected_force = -self.damping*sqrt(self.q**2)*self.dq/self.q
  300. assert hasattr(damper, 'force')
  301. assert isinstance(damper.force, ExprType)
  302. assert damper.force == expected_force
  303. @pytest.mark.parametrize('damping', [None, 'c'])
  304. def test_invalid_constructor_damping_not_sympifyable(self, damping):
  305. with pytest.raises(SympifyError):
  306. _ = LinearDamper(damping, self.pathway)
  307. def test_invalid_constructor_pathway_not_pathway_base(self):
  308. with pytest.raises(TypeError):
  309. _ = LinearDamper(self.damping, None)
  310. @pytest.mark.parametrize(
  311. 'property_name, fixture_attr_name',
  312. [
  313. ('damping', 'damping'),
  314. ('pathway', 'pathway'),
  315. ]
  316. )
  317. def test_properties_are_immutable(self, property_name, fixture_attr_name):
  318. damper = LinearDamper(self.damping, self.pathway)
  319. value = getattr(self, fixture_attr_name)
  320. with pytest.raises(AttributeError):
  321. setattr(damper, property_name, value)
  322. def test_repr(self):
  323. self.pB.set_pos(self.pA, self.q*self.N.x)
  324. damper = LinearDamper(self.damping, self.pathway)
  325. expected = 'LinearDamper(c, LinearPathway(pA, pB))'
  326. assert repr(damper) == expected
  327. def test_to_loads(self):
  328. self.pB.set_pos(self.pA, self.q*self.N.x)
  329. damper = LinearDamper(self.damping, self.pathway)
  330. direction = self.q**2/self.q**2*self.N.x
  331. pA_force = self.damping*self.dq*direction
  332. pB_force = -self.damping*self.dq*direction
  333. expected = [Force(self.pA, pA_force), Force(self.pB, pB_force)]
  334. assert damper.to_loads() == expected
  335. class TestForcedMassSpringDamperModel():
  336. r"""A single degree of freedom translational forced mass-spring-damper.
  337. Notes
  338. =====
  339. This system is well known to have the governing equation:
  340. .. math::
  341. m \ddot{x} = F - k x - c \dot{x}
  342. where $F$ is an externally applied force, $m$ is the mass of the particle
  343. to which the spring and damper are attached, $k$ is the spring's stiffness,
  344. $c$ is the dampers damping coefficient, and $x$ is the generalized
  345. coordinate representing the system's single (translational) degree of
  346. freedom.
  347. """
  348. @pytest.fixture(autouse=True)
  349. def _force_mass_spring_damper_model_fixture(self):
  350. self.m = Symbol('m')
  351. self.k = Symbol('k')
  352. self.c = Symbol('c')
  353. self.F = Symbol('F')
  354. self.q = dynamicsymbols('q')
  355. self.dq = dynamicsymbols('q', 1)
  356. self.u = dynamicsymbols('u')
  357. self.frame = ReferenceFrame('N')
  358. self.origin = Point('pO')
  359. self.origin.set_vel(self.frame, 0)
  360. self.attachment = Point('pA')
  361. self.attachment.set_pos(self.origin, self.q*self.frame.x)
  362. self.mass = Particle('mass', self.attachment, self.m)
  363. self.pathway = LinearPathway(self.origin, self.attachment)
  364. self.kanes_method = KanesMethod(
  365. self.frame,
  366. q_ind=[self.q],
  367. u_ind=[self.u],
  368. kd_eqs=[self.dq - self.u],
  369. )
  370. self.bodies = [self.mass]
  371. self.mass_matrix = Matrix([[self.m]])
  372. self.forcing = Matrix([[self.F - self.c*self.u - self.k*self.q]])
  373. def test_force_acuator(self):
  374. stiffness = -self.k*self.pathway.length
  375. spring = ForceActuator(stiffness, self.pathway)
  376. damping = -self.c*self.pathway.extension_velocity
  377. damper = ForceActuator(damping, self.pathway)
  378. loads = [
  379. (self.attachment, self.F*self.frame.x),
  380. *spring.to_loads(),
  381. *damper.to_loads(),
  382. ]
  383. self.kanes_method.kanes_equations(self.bodies, loads)
  384. assert self.kanes_method.mass_matrix == self.mass_matrix
  385. assert self.kanes_method.forcing == self.forcing
  386. def test_linear_spring_linear_damper(self):
  387. spring = LinearSpring(self.k, self.pathway)
  388. damper = LinearDamper(self.c, self.pathway)
  389. loads = [
  390. (self.attachment, self.F*self.frame.x),
  391. *spring.to_loads(),
  392. *damper.to_loads(),
  393. ]
  394. self.kanes_method.kanes_equations(self.bodies, loads)
  395. assert self.kanes_method.mass_matrix == self.mass_matrix
  396. assert self.kanes_method.forcing == self.forcing
  397. class TestTorqueActuator:
  398. @pytest.fixture(autouse=True)
  399. def _torque_actuator_fixture(self):
  400. self.torque = Symbol('T')
  401. self.N = ReferenceFrame('N')
  402. self.A = ReferenceFrame('A')
  403. self.axis = self.N.z
  404. self.target = RigidBody('target', frame=self.N)
  405. self.reaction = RigidBody('reaction', frame=self.A)
  406. def test_is_actuator_base_subclass(self):
  407. assert issubclass(TorqueActuator, ActuatorBase)
  408. @pytest.mark.parametrize(
  409. 'torque',
  410. [
  411. Symbol('T'),
  412. dynamicsymbols('T'),
  413. Symbol('T')**2 + Symbol('T'),
  414. ]
  415. )
  416. @pytest.mark.parametrize(
  417. 'target_frame, reaction_frame',
  418. [
  419. (target.frame, reaction.frame),
  420. (target, reaction.frame),
  421. (target.frame, reaction),
  422. (target, reaction),
  423. ]
  424. )
  425. def test_valid_constructor_with_reaction(
  426. self,
  427. torque,
  428. target_frame,
  429. reaction_frame,
  430. ):
  431. instance = TorqueActuator(
  432. torque,
  433. self.axis,
  434. target_frame,
  435. reaction_frame,
  436. )
  437. assert isinstance(instance, TorqueActuator)
  438. assert hasattr(instance, 'torque')
  439. assert isinstance(instance.torque, ExprType)
  440. assert instance.torque == torque
  441. assert hasattr(instance, 'axis')
  442. assert isinstance(instance.axis, Vector)
  443. assert instance.axis == self.axis
  444. assert hasattr(instance, 'target_frame')
  445. assert isinstance(instance.target_frame, ReferenceFrame)
  446. assert instance.target_frame == target.frame
  447. assert hasattr(instance, 'reaction_frame')
  448. assert isinstance(instance.reaction_frame, ReferenceFrame)
  449. assert instance.reaction_frame == reaction.frame
  450. @pytest.mark.parametrize(
  451. 'torque',
  452. [
  453. Symbol('T'),
  454. dynamicsymbols('T'),
  455. Symbol('T')**2 + Symbol('T'),
  456. ]
  457. )
  458. @pytest.mark.parametrize('target_frame', [target.frame, target])
  459. def test_valid_constructor_without_reaction(self, torque, target_frame):
  460. instance = TorqueActuator(torque, self.axis, target_frame)
  461. assert isinstance(instance, TorqueActuator)
  462. assert hasattr(instance, 'torque')
  463. assert isinstance(instance.torque, ExprType)
  464. assert instance.torque == torque
  465. assert hasattr(instance, 'axis')
  466. assert isinstance(instance.axis, Vector)
  467. assert instance.axis == self.axis
  468. assert hasattr(instance, 'target_frame')
  469. assert isinstance(instance.target_frame, ReferenceFrame)
  470. assert instance.target_frame == target.frame
  471. assert hasattr(instance, 'reaction_frame')
  472. assert instance.reaction_frame is None
  473. @pytest.mark.parametrize('torque', [None, 'T'])
  474. def test_invalid_constructor_torque_not_sympifyable(self, torque):
  475. with pytest.raises(SympifyError):
  476. _ = TorqueActuator(torque, self.axis, self.target)
  477. @pytest.mark.parametrize('axis', [Symbol('a'), dynamicsymbols('a')])
  478. def test_invalid_constructor_axis_not_vector(self, axis):
  479. with pytest.raises(TypeError):
  480. _ = TorqueActuator(self.torque, axis, self.target, self.reaction)
  481. @pytest.mark.parametrize(
  482. 'frames',
  483. [
  484. (None, ReferenceFrame('child')),
  485. (ReferenceFrame('parent'), True),
  486. (None, RigidBody('child')),
  487. (RigidBody('parent'), True),
  488. ]
  489. )
  490. def test_invalid_constructor_frames_not_frame(self, frames):
  491. with pytest.raises(TypeError):
  492. _ = TorqueActuator(self.torque, self.axis, *frames)
  493. @pytest.mark.parametrize(
  494. 'property_name, fixture_attr_name',
  495. [
  496. ('torque', 'torque'),
  497. ('axis', 'axis'),
  498. ('target_frame', 'target'),
  499. ('reaction_frame', 'reaction'),
  500. ]
  501. )
  502. def test_properties_are_immutable(self, property_name, fixture_attr_name):
  503. actuator = TorqueActuator(
  504. self.torque,
  505. self.axis,
  506. self.target,
  507. self.reaction,
  508. )
  509. value = getattr(self, fixture_attr_name)
  510. with pytest.raises(AttributeError):
  511. setattr(actuator, property_name, value)
  512. def test_repr_without_reaction(self):
  513. actuator = TorqueActuator(self.torque, self.axis, self.target)
  514. expected = 'TorqueActuator(T, axis=N.z, target_frame=N)'
  515. assert repr(actuator) == expected
  516. def test_repr_with_reaction(self):
  517. actuator = TorqueActuator(
  518. self.torque,
  519. self.axis,
  520. self.target,
  521. self.reaction,
  522. )
  523. expected = 'TorqueActuator(T, axis=N.z, target_frame=N, reaction_frame=A)'
  524. assert repr(actuator) == expected
  525. def test_at_pin_joint_constructor(self):
  526. pin_joint = PinJoint(
  527. 'pin',
  528. self.target,
  529. self.reaction,
  530. coordinates=dynamicsymbols('q'),
  531. speeds=dynamicsymbols('u'),
  532. parent_interframe=self.N,
  533. joint_axis=self.axis,
  534. )
  535. instance = TorqueActuator.at_pin_joint(self.torque, pin_joint)
  536. assert isinstance(instance, TorqueActuator)
  537. assert hasattr(instance, 'torque')
  538. assert isinstance(instance.torque, ExprType)
  539. assert instance.torque == self.torque
  540. assert hasattr(instance, 'axis')
  541. assert isinstance(instance.axis, Vector)
  542. assert instance.axis == self.axis
  543. assert hasattr(instance, 'target_frame')
  544. assert isinstance(instance.target_frame, ReferenceFrame)
  545. assert instance.target_frame == self.A
  546. assert hasattr(instance, 'reaction_frame')
  547. assert isinstance(instance.reaction_frame, ReferenceFrame)
  548. assert instance.reaction_frame == self.N
  549. def test_at_pin_joint_pin_joint_not_pin_joint_invalid(self):
  550. with pytest.raises(TypeError):
  551. _ = TorqueActuator.at_pin_joint(self.torque, Symbol('pin'))
  552. def test_to_loads_without_reaction(self):
  553. actuator = TorqueActuator(self.torque, self.axis, self.target)
  554. expected = [
  555. (self.N, self.torque*self.axis),
  556. ]
  557. assert actuator.to_loads() == expected
  558. def test_to_loads_with_reaction(self):
  559. actuator = TorqueActuator(
  560. self.torque,
  561. self.axis,
  562. self.target,
  563. self.reaction,
  564. )
  565. expected = [
  566. (self.N, self.torque*self.axis),
  567. (self.A, - self.torque*self.axis),
  568. ]
  569. assert actuator.to_loads() == expected
  570. class NonSympifyable:
  571. pass
  572. class TestDuffingSpring:
  573. @pytest.fixture(autouse=True)
  574. # Set up common variables that will be used in multiple tests
  575. def _duffing_spring_fixture(self):
  576. self.linear_stiffness = Symbol('beta')
  577. self.nonlinear_stiffness = Symbol('alpha')
  578. self.equilibrium_length = Symbol('l')
  579. self.pA = Point('pA')
  580. self.pB = Point('pB')
  581. self.pathway = LinearPathway(self.pA, self.pB)
  582. self.q = dynamicsymbols('q')
  583. self.N = ReferenceFrame('N')
  584. # Simples tests to check that DuffingSpring is a subclass of ForceActuator and ActuatorBase
  585. def test_is_force_actuator_subclass(self):
  586. assert issubclass(DuffingSpring, ForceActuator)
  587. def test_is_actuator_base_subclass(self):
  588. assert issubclass(DuffingSpring, ActuatorBase)
  589. @pytest.mark.parametrize(
  590. # Create parametrized tests that allows running the same test function multiple times with different sets of arguments
  591. (
  592. 'linear_stiffness, '
  593. 'expected_linear_stiffness, '
  594. 'nonlinear_stiffness, '
  595. 'expected_nonlinear_stiffness, '
  596. 'equilibrium_length, '
  597. 'expected_equilibrium_length, '
  598. 'force'
  599. ),
  600. [
  601. (
  602. 1,
  603. S.One,
  604. 1,
  605. S.One,
  606. 0,
  607. S.Zero,
  608. -sqrt(dynamicsymbols('q')**2)-(sqrt(dynamicsymbols('q')**2))**3,
  609. ),
  610. (
  611. Symbol('beta'),
  612. Symbol('beta'),
  613. Symbol('alpha'),
  614. Symbol('alpha'),
  615. 0,
  616. S.Zero,
  617. -Symbol('beta')*sqrt(dynamicsymbols('q')**2)-Symbol('alpha')*(sqrt(dynamicsymbols('q')**2))**3,
  618. ),
  619. (
  620. Symbol('beta'),
  621. Symbol('beta'),
  622. Symbol('alpha'),
  623. Symbol('alpha'),
  624. S.Zero,
  625. S.Zero,
  626. -Symbol('beta')*sqrt(dynamicsymbols('q')**2)-Symbol('alpha')*(sqrt(dynamicsymbols('q')**2))**3,
  627. ),
  628. (
  629. Symbol('beta'),
  630. Symbol('beta'),
  631. Symbol('alpha'),
  632. Symbol('alpha'),
  633. Symbol('l'),
  634. Symbol('l'),
  635. -Symbol('beta') * (sqrt(dynamicsymbols('q')**2) - Symbol('l')) - Symbol('alpha') * (sqrt(dynamicsymbols('q')**2) - Symbol('l'))**3,
  636. ),
  637. ]
  638. )
  639. # Check if DuffingSpring correctly initializes its attributes
  640. # It tests various combinations of linear & nonlinear stiffness, equilibriun length, and the resulting force expression
  641. def test_valid_constructor(
  642. self,
  643. linear_stiffness,
  644. expected_linear_stiffness,
  645. nonlinear_stiffness,
  646. expected_nonlinear_stiffness,
  647. equilibrium_length,
  648. expected_equilibrium_length,
  649. force,
  650. ):
  651. self.pB.set_pos(self.pA, self.q*self.N.x)
  652. spring = DuffingSpring(linear_stiffness, nonlinear_stiffness, self.pathway, equilibrium_length)
  653. assert isinstance(spring, DuffingSpring)
  654. assert hasattr(spring, 'linear_stiffness')
  655. assert isinstance(spring.linear_stiffness, ExprType)
  656. assert spring.linear_stiffness == expected_linear_stiffness
  657. assert hasattr(spring, 'nonlinear_stiffness')
  658. assert isinstance(spring.nonlinear_stiffness, ExprType)
  659. assert spring.nonlinear_stiffness == expected_nonlinear_stiffness
  660. assert hasattr(spring, 'pathway')
  661. assert isinstance(spring.pathway, LinearPathway)
  662. assert spring.pathway == self.pathway
  663. assert hasattr(spring, 'equilibrium_length')
  664. assert isinstance(spring.equilibrium_length, ExprType)
  665. assert spring.equilibrium_length == expected_equilibrium_length
  666. assert hasattr(spring, 'force')
  667. assert isinstance(spring.force, ExprType)
  668. assert spring.force == force
  669. @pytest.mark.parametrize('linear_stiffness', [None, NonSympifyable()])
  670. def test_invalid_constructor_linear_stiffness_not_sympifyable(self, linear_stiffness):
  671. with pytest.raises(SympifyError):
  672. _ = DuffingSpring(linear_stiffness, self.nonlinear_stiffness, self.pathway, self.equilibrium_length)
  673. @pytest.mark.parametrize('nonlinear_stiffness', [None, NonSympifyable()])
  674. def test_invalid_constructor_nonlinear_stiffness_not_sympifyable(self, nonlinear_stiffness):
  675. with pytest.raises(SympifyError):
  676. _ = DuffingSpring(self.linear_stiffness, nonlinear_stiffness, self.pathway, self.equilibrium_length)
  677. def test_invalid_constructor_pathway_not_pathway_base(self):
  678. with pytest.raises(TypeError):
  679. _ = DuffingSpring(self.linear_stiffness, self.nonlinear_stiffness, NonSympifyable(), self.equilibrium_length)
  680. @pytest.mark.parametrize('equilibrium_length', [None, NonSympifyable()])
  681. def test_invalid_constructor_equilibrium_length_not_sympifyable(self, equilibrium_length):
  682. with pytest.raises(SympifyError):
  683. _ = DuffingSpring(self.linear_stiffness, self.nonlinear_stiffness, self.pathway, equilibrium_length)
  684. @pytest.mark.parametrize(
  685. 'property_name, fixture_attr_name',
  686. [
  687. ('linear_stiffness', 'linear_stiffness'),
  688. ('nonlinear_stiffness', 'nonlinear_stiffness'),
  689. ('pathway', 'pathway'),
  690. ('equilibrium_length', 'equilibrium_length')
  691. ]
  692. )
  693. # Check if certain properties of DuffingSpring object are immutable after initialization
  694. # Ensure that once DuffingSpring is created, its key properties cannot be changed
  695. def test_properties_are_immutable(self, property_name, fixture_attr_name):
  696. spring = DuffingSpring(self.linear_stiffness, self.nonlinear_stiffness, self.pathway, self.equilibrium_length)
  697. with pytest.raises(AttributeError):
  698. setattr(spring, property_name, getattr(self, fixture_attr_name))
  699. @pytest.mark.parametrize(
  700. 'equilibrium_length, expected',
  701. [
  702. (0, 'DuffingSpring(beta, alpha, LinearPathway(pA, pB), equilibrium_length=0)'),
  703. (Symbol('l'), 'DuffingSpring(beta, alpha, LinearPathway(pA, pB), equilibrium_length=l)'),
  704. ]
  705. )
  706. # Check the __repr__ method of DuffingSpring class
  707. # Check if the actual string representation of DuffingSpring instance matches the expected string for each provided parameter values
  708. def test_repr(self, equilibrium_length, expected):
  709. spring = DuffingSpring(self.linear_stiffness, self.nonlinear_stiffness, self.pathway, equilibrium_length)
  710. assert repr(spring) == expected
  711. def test_to_loads(self):
  712. self.pB.set_pos(self.pA, self.q*self.N.x)
  713. spring = DuffingSpring(self.linear_stiffness, self.nonlinear_stiffness, self.pathway, self.equilibrium_length)
  714. # Calculate the displacement from the equilibrium length
  715. displacement = self.q - self.equilibrium_length
  716. # Make sure this matches the computation in DuffingSpring class
  717. force = -self.linear_stiffness * displacement - self.nonlinear_stiffness * displacement**3
  718. # The expected loads on pA and pB due to the spring
  719. expected_loads = [Force(self.pA, force * self.N.x), Force(self.pB, -force * self.N.x)]
  720. # Compare expected loads to what is returned from DuffingSpring.to_loads()
  721. calculated_loads = spring.to_loads()
  722. for calculated, expected in zip(calculated_loads, expected_loads):
  723. assert calculated.point == expected.point
  724. for dim in self.N: # Assuming self.N is the reference frame
  725. calculated_component = calculated.vector.dot(dim)
  726. expected_component = expected.vector.dot(dim)
  727. # Substitute all symbols with numeric values
  728. substitutions = {self.q: 1, Symbol('l'): 1, Symbol('alpha'): 1, Symbol('beta'): 1} # Add other necessary symbols as needed
  729. diff = (calculated_component - expected_component).subs(substitutions).evalf()
  730. # Check if the absolute value of the difference is below a threshold
  731. assert Abs(diff) < 1e-9, f"The forces do not match. Difference: {diff}"
  732. class TestCoulombKineticFriction:
  733. @pytest.fixture(autouse=True)
  734. def _block_on_surface(self):
  735. """A block sliding on a surface.
  736. Notes
  737. =====
  738. This test validates the correctness of the CoulombKineticFriction by simulating
  739. a block sliding on a surface with the Coulomb kinetic friction force.
  740. The test covers scenarios with both positive and negative velocities.
  741. """
  742. # Mass, gravity constant, friction coefficient, coefficient of Stribeck friction, viscous_coefficient
  743. self.m, self.g, self.mu_k, self.mu_s, self.v_s, self.sigma, self.F = symbols('m g mu_k mu_s v_s sigma F', real=True)
  744. def test_block_on_surface_default(self):
  745. # General Case
  746. q = dynamicsymbols('q')
  747. N = ReferenceFrame('N')
  748. O = Point('O')
  749. P = O.locatenew('P', q * N.x)
  750. O.set_vel(N, 0)
  751. P.set_vel(N, q.diff() * N.x)
  752. pathway = LinearPathway(O, P)
  753. friction = CoulombKineticFriction(self.mu_k, self.m * self.g, pathway)
  754. expected_general = [Force(point=O, force=self.g * self.m * self.mu_k * q * sign(sqrt(q**2) * q.diff()/q)/sqrt(q**2) * N.x),
  755. Force(point=P, force=-self.g * self.m * self.mu_k * q * sign(sqrt(q**2) * q.diff()/q)/sqrt(q**2) * N.x)]
  756. assert friction.to_loads() == expected_general
  757. # Positive
  758. q = dynamicsymbols('q', positive=True)
  759. N = ReferenceFrame('N')
  760. O = Point('O')
  761. P = O.locatenew('P', q * N.x)
  762. O.set_vel(N, 0)
  763. P.set_vel(N, q.diff() * N.x)
  764. pathway = LinearPathway(O, P)
  765. friction = CoulombKineticFriction(self.mu_k, self.m * self.g, pathway)
  766. expected_positive = [Force(point=O, force=self.g * self.m * self.mu_k * sign(q.diff()) * N.x),
  767. Force(point=P, force=-self.g * self.m * self.mu_k * sign(q.diff()) * N.x)]
  768. assert friction.to_loads() == expected_positive
  769. # Negative
  770. q = dynamicsymbols('q', positive=False)
  771. N = ReferenceFrame('N')
  772. O = Point('O')
  773. P = O.locatenew('P', q * N.x)
  774. O.set_vel(N, 0)
  775. P.set_vel(N, q.diff() * N.x)
  776. pathway = LinearPathway(O, P)
  777. friction = CoulombKineticFriction(self.mu_k, self.m * self.g, pathway)
  778. expected_negative = [Force(point=O, force=self.g * self.m * self.mu_k * q * sign(sqrt(q**2) * q.diff()/q)/sqrt(q**2)*N.x),
  779. Force(point=P, force=-self.g * self.m * self.mu_k * q * sign(sqrt(q**2) * q.diff()/q)/sqrt(q**2)*N.x)]
  780. assert friction.to_loads() == expected_negative
  781. def test_block_on_surface_viscous(self):
  782. # General Case
  783. q = dynamicsymbols('q')
  784. N = ReferenceFrame('N')
  785. O = Point('O')
  786. P = O.locatenew('P', q * N.x)
  787. O.set_vel(N, 0)
  788. P.set_vel(N, q.diff() * N.x)
  789. pathway = LinearPathway(O, P)
  790. friction = CoulombKineticFriction(self.mu_k, self.m * self.g, pathway, sigma=self.sigma)
  791. expected_general = [Force(point=O, force=(self.g * self.m * self.mu_k * sign(sqrt(q**2) * q.diff()/q) + self.sigma * sqrt(q**2) * q.diff()/q) * q/sqrt(q**2) * N.x),
  792. Force(point=P, force=(-self.g * self.m * self.mu_k * sign(sqrt(q**2) * q.diff()/q) - self.sigma * sqrt(q**2) * q.diff()/q) * q/sqrt(q**2) * N.x)]
  793. assert friction.to_loads() == expected_general
  794. # Positive
  795. q = dynamicsymbols('q', positive=True)
  796. N = ReferenceFrame('N')
  797. O = Point('O')
  798. P = O.locatenew('P', q * N.x)
  799. O.set_vel(N, 0)
  800. P.set_vel(N, q.diff() * N.x)
  801. pathway = LinearPathway(O, P)
  802. friction = CoulombKineticFriction(self.mu_k, self.m * self.g, pathway, sigma=self.sigma)
  803. expected_positive = [Force(point=O, force=(self.g * self.m * self.mu_k * sign(q.diff()) + self.sigma * q.diff()) * N.x),
  804. Force(point=P, force=(-self.g * self.m * self.mu_k * sign(q.diff()) - self.sigma * q.diff()) * N.x)]
  805. assert friction.to_loads() == expected_positive
  806. # Negative
  807. q = dynamicsymbols('q', positive=False)
  808. N = ReferenceFrame('N')
  809. O = Point('O')
  810. P = O.locatenew('P', q * N.x)
  811. O.set_vel(N, 0)
  812. P.set_vel(N, q.diff() * N.x)
  813. pathway = LinearPathway(O, P)
  814. friction = CoulombKineticFriction(self.mu_k, self.m * self.g, pathway, sigma=self.sigma)
  815. expected_negative = [Force(point=O, force=(self.g * self.m * self.mu_k * sign(sqrt(q**2) * q.diff()/q) + self.sigma * sqrt(q**2) * q.diff()/q) * q/sqrt(q**2) * N.x),
  816. Force(point=P, force=(-self.g * self.m * self.mu_k * sign(sqrt(q**2) * q.diff()/q) - self.sigma * sqrt(q**2) * q.diff()/q) * q/sqrt(q**2) * N.x)]
  817. assert friction.to_loads() == expected_negative
  818. def test_block_on_surface_stribeck(self):
  819. # General Case
  820. q = dynamicsymbols('q')
  821. N = ReferenceFrame('N')
  822. O = Point('O')
  823. P = O.locatenew('P', q * N.x)
  824. O.set_vel(N, 0)
  825. P.set_vel(N, q.diff() * N.x)
  826. pathway = LinearPathway(O, P)
  827. friction = CoulombKineticFriction(self.mu_k, self.m * self.g, pathway, v_s=self.v_s, mu_s=self.mu_s)
  828. expected_general = [Force(point=O, force=(self.g * self.m * self.mu_k + (-self.g * self.m * self.mu_k + self.g * self.m * self.mu_s) * exp(-q.diff()**2/self.v_s**2)) * q * sign(sqrt(q**2) * q.diff()/q)/sqrt(q**2) * N.x),
  829. Force(point=P, force=- (self.g * self.m * self.mu_k + (-self.g * self.m * self.mu_k + self.g * self.m * self.mu_s) * exp(-q.diff()**2/self.v_s**2)) * q * sign(sqrt(q**2) * q.diff()/q)/sqrt(q**2) * N.x)]
  830. assert friction.to_loads() == expected_general
  831. # Positive
  832. q = dynamicsymbols('q', positive=True)
  833. N = ReferenceFrame('N')
  834. O = Point('O')
  835. P = O.locatenew('P', q * N.x)
  836. O.set_vel(N, 0)
  837. P.set_vel(N, q.diff() * N.x)
  838. pathway = LinearPathway(O, P)
  839. friction = CoulombKineticFriction(self.mu_k, self.m * self.g, pathway, v_s=self.v_s, mu_s=self.mu_s)
  840. expected_positive = [Force(point=O, force=(self.g * self.m * self.mu_k + (-self.g * self.m * self.mu_k + self.g * self.m * self.mu_s) * exp(-q.diff()**2/self.v_s**2)) * sign(q.diff()) * N.x),
  841. Force(point=P, force=- (self.g * self.m * self.mu_k + (-self.g * self.m * self.mu_k + self.g * self.m * self.mu_s) * exp(-q.diff()**2/self.v_s**2)) * sign(q.diff()) * N.x)]
  842. assert friction.to_loads() == expected_positive
  843. # Negative
  844. q = dynamicsymbols('q', positive=False)
  845. N = ReferenceFrame('N')
  846. O = Point('O')
  847. P = O.locatenew('P', q * N.x)
  848. O.set_vel(N, 0)
  849. P.set_vel(N, q.diff() * N.x)
  850. pathway = LinearPathway(O, P)
  851. friction = CoulombKineticFriction(self.mu_k, self.m * self.g, pathway, v_s=self.v_s, mu_s=self.mu_s)
  852. expected_negative = [Force(point=O, force=(self.g * self.m * self.mu_k + (-self.g * self.m * self.mu_k + self.g * self.m * self.mu_s) * exp(-q.diff()**2/self.v_s**2)) * q * sign(sqrt(q**2) * q.diff()/q)/sqrt(q**2) * N.x),
  853. Force(point=P, force=- (self.g * self.m * self.mu_k + (-self.g * self.m * self.mu_k + self.g * self.m * self.mu_s) * exp(-q.diff()**2/self.v_s**2)) * q * sign(sqrt(q**2) * q.diff()/q)/sqrt(q**2) * N.x)]
  854. assert friction.to_loads() == expected_negative
  855. def test_block_on_surface_all(self):
  856. # General Case
  857. q = dynamicsymbols('q')
  858. N = ReferenceFrame('N')
  859. O = Point('O')
  860. P = O.locatenew('P', q * N.x)
  861. O.set_vel(N, 0)
  862. P.set_vel(N, q.diff() * N.x)
  863. pathway = LinearPathway(O, P)
  864. friction = CoulombKineticFriction(self.mu_k, self.m * self.g, pathway, v_s=self.v_s, sigma=self.sigma, mu_s=self.mu_s)
  865. expected_general = [Force(point=O, force=(self.sigma * sqrt(q**2) * q.diff()/q + (self.g * self.m * self.mu_k + (-self.g * self.m * self.mu_k + self.g * self.m * self.mu_s) * exp(-q.diff()**2/self.v_s**2)) * sign(sqrt(q**2) * q.diff()/q)) * q/sqrt(q**2) * N.x),
  866. Force(point=P, force=(-self.sigma * sqrt(q**2) * q.diff()/q - (self.g * self.m * self.mu_k + (-self.g * self.m * self.mu_k + self.g * self.m * self.mu_s) * exp(-q.diff()**2/self.v_s**2)) * sign(sqrt(q**2) * q.diff()/q)) * q/sqrt(q**2) * N.x)]
  867. assert friction.to_loads() == expected_general
  868. # Positive
  869. q = dynamicsymbols('q', positive=True)
  870. N = ReferenceFrame('N')
  871. O = Point('O')
  872. P = O.locatenew('P', q * N.x)
  873. O.set_vel(N, 0)
  874. P.set_vel(N, q.diff() * N.x)
  875. pathway = LinearPathway(O, P)
  876. friction = CoulombKineticFriction(self.mu_k, self.m * self.g, pathway, v_s=self.v_s, sigma=self.sigma, mu_s=self.mu_s)
  877. expected_positive = [Force(point=O, force=(self.sigma * q.diff() + (self.g * self.m * self.mu_k + (-self.g * self.m * self.mu_k + self.g * self.m * self.mu_s) * exp(-q.diff()**2/self.v_s**2)) * sign(q.diff())) * N.x),
  878. Force(point=P, force=(-self.sigma * q.diff() - (self.g * self.m * self.mu_k + (-self.g * self.m * self.mu_k + self.g * self.m * self.mu_s) * exp(-q.diff()**2/self.v_s**2)) * sign(q.diff())) * N.x)]
  879. assert friction.to_loads() == expected_positive
  880. # Negative
  881. q = dynamicsymbols('q', positive=False)
  882. N = ReferenceFrame('N')
  883. O = Point('O')
  884. P = O.locatenew('P', q * N.x)
  885. O.set_vel(N, 0)
  886. P.set_vel(N, q.diff() * N.x)
  887. pathway = LinearPathway(O, P)
  888. friction = CoulombKineticFriction(self.mu_k, self.m * self.g, pathway, v_s=self.v_s, sigma=self.sigma, mu_s=self.mu_s)
  889. expected_negative = [Force(point=O, force=(self.sigma * sqrt(q**2) * q.diff()/q + (self.g * self.m * self.mu_k + (-self.g * self.m * self.mu_k + self.g * self.m * self.mu_s) * exp(-q.diff()**2/self.v_s**2)) * sign(sqrt(q**2) * q.diff()/q)) * q/sqrt(q**2) * N.x),
  890. Force(point=P, force=(-self.sigma * sqrt(q**2) * q.diff()/q - (self.g * self.m * self.mu_k + (-self.g * self.m * self.mu_k + self.g * self.m * self.mu_s) * exp(-q.diff()**2/self.v_s**2)) * sign(sqrt(q**2) * q.diff()/q)) * q/sqrt(q**2) * N.x)]
  891. assert friction.to_loads() == expected_negative
  892. def test_normal_force_zero(self):
  893. q = dynamicsymbols('q')
  894. N = ReferenceFrame('N')
  895. O = Point('O')
  896. P = O.locatenew('P', q * N.x)
  897. O.set_vel(N, 0)
  898. P.set_vel(N, q.diff() * N.x)
  899. pathway = LinearPathway(O, P)
  900. friction = CoulombKineticFriction(
  901. self.mu_k,
  902. 0,
  903. pathway
  904. )
  905. assert friction.force == 0