ZhengyangZhang commited on
Commit
dd72226
·
verified ·
1 Parent(s): 44c9281

Add files using upload-large-folder tool

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. lib/python3.12/site-packages/sympy/physics/mechanics/functions.py +735 -0
  2. lib/python3.12/site-packages/sympy/physics/mechanics/inertia.py +197 -0
  3. lib/python3.12/site-packages/sympy/physics/mechanics/lagrange.py +512 -0
  4. lib/python3.12/site-packages/sympy/physics/mechanics/loads.py +177 -0
  5. lib/python3.12/site-packages/sympy/physics/mechanics/method.py +39 -0
  6. lib/python3.12/site-packages/sympy/physics/mechanics/pathway.py +688 -0
  7. lib/python3.12/site-packages/sympy/physics/mechanics/tests/__init__.py +0 -0
  8. lib/python3.12/site-packages/sympy/physics/mechanics/tests/__pycache__/test_inertia.cpython-312.pyc +0 -0
  9. lib/python3.12/site-packages/sympy/physics/mechanics/tests/__pycache__/test_kane5.cpython-312.pyc +0 -0
  10. lib/python3.12/site-packages/sympy/physics/mechanics/tests/__pycache__/test_method.cpython-312.pyc +0 -0
  11. lib/python3.12/site-packages/sympy/physics/mechanics/tests/__pycache__/test_rigidbody.cpython-312.pyc +0 -0
  12. lib/python3.12/site-packages/sympy/physics/mechanics/tests/__pycache__/test_system.cpython-312.pyc +0 -0
  13. lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_actuator.py +851 -0
  14. lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_body.py +340 -0
  15. lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_functions.py +262 -0
  16. lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_inertia.py +71 -0
  17. lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_joint.py +1240 -0
  18. lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_jointsmethod.py +249 -0
  19. lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_kane.py +553 -0
  20. lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_kane2.py +464 -0
  21. lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_kane3.py +315 -0
  22. lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_kane4.py +115 -0
  23. lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_kane5.py +128 -0
  24. lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_lagrange.py +247 -0
  25. lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_lagrange2.py +46 -0
  26. lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_linearize.py +372 -0
  27. lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_loads.py +86 -0
  28. lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_method.py +5 -0
  29. lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_models.py +117 -0
  30. lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_particle.py +78 -0
  31. lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_pathway.py +691 -0
  32. lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_rigidbody.py +184 -0
  33. lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_system.py +245 -0
  34. lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_system_class.py +831 -0
  35. lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_wrapping_geometry.py +363 -0
  36. lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/__init__.cpython-312.pyc +0 -0
  37. lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/anticommutator.cpython-312.pyc +0 -0
  38. lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/boson.cpython-312.pyc +0 -0
  39. lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/cartesian.cpython-312.pyc +0 -0
  40. lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/cg.cpython-312.pyc +0 -0
  41. lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/circuitplot.cpython-312.pyc +0 -0
  42. lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/circuitutils.cpython-312.pyc +0 -0
  43. lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/commutator.cpython-312.pyc +0 -0
  44. lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/constants.cpython-312.pyc +0 -0
  45. lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/dagger.cpython-312.pyc +0 -0
  46. lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/density.cpython-312.pyc +0 -0
  47. lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/fermion.cpython-312.pyc +0 -0
  48. lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/gate.cpython-312.pyc +0 -0
  49. lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/grover.cpython-312.pyc +0 -0
  50. lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/hilbert.cpython-312.pyc +0 -0
lib/python3.12/site-packages/sympy/physics/mechanics/functions.py ADDED
@@ -0,0 +1,735 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sympy.utilities import dict_merge
2
+ from sympy.utilities.iterables import iterable
3
+ from sympy.physics.vector import (Dyadic, Vector, ReferenceFrame,
4
+ Point, dynamicsymbols)
5
+ from sympy.physics.vector.printing import (vprint, vsprint, vpprint, vlatex,
6
+ init_vprinting)
7
+ from sympy.physics.mechanics.particle import Particle
8
+ from sympy.physics.mechanics.rigidbody import RigidBody
9
+ from sympy.simplify.simplify import simplify
10
+ from sympy import Matrix, Mul, Derivative, sin, cos, tan, S
11
+ from sympy.core.function import AppliedUndef
12
+ from sympy.physics.mechanics.inertia import (inertia as _inertia,
13
+ inertia_of_point_mass as _inertia_of_point_mass)
14
+ from sympy.utilities.exceptions import sympy_deprecation_warning
15
+
16
+ __all__ = ['linear_momentum',
17
+ 'angular_momentum',
18
+ 'kinetic_energy',
19
+ 'potential_energy',
20
+ 'Lagrangian',
21
+ 'mechanics_printing',
22
+ 'mprint',
23
+ 'msprint',
24
+ 'mpprint',
25
+ 'mlatex',
26
+ 'msubs',
27
+ 'find_dynamicsymbols']
28
+
29
+ # These are functions that we've moved and renamed during extracting the
30
+ # basic vector calculus code from the mechanics packages.
31
+
32
+ mprint = vprint
33
+ msprint = vsprint
34
+ mpprint = vpprint
35
+ mlatex = vlatex
36
+
37
+
38
+ def mechanics_printing(**kwargs):
39
+ """
40
+ Initializes time derivative printing for all SymPy objects in
41
+ mechanics module.
42
+ """
43
+
44
+ init_vprinting(**kwargs)
45
+
46
+ mechanics_printing.__doc__ = init_vprinting.__doc__
47
+
48
+
49
+ def inertia(frame, ixx, iyy, izz, ixy=0, iyz=0, izx=0):
50
+ sympy_deprecation_warning(
51
+ """
52
+ The inertia function has been moved.
53
+ Import it from "sympy.physics.mechanics".
54
+ """,
55
+ deprecated_since_version="1.13",
56
+ active_deprecations_target="moved-mechanics-functions"
57
+ )
58
+ return _inertia(frame, ixx, iyy, izz, ixy, iyz, izx)
59
+
60
+
61
+ def inertia_of_point_mass(mass, pos_vec, frame):
62
+ sympy_deprecation_warning(
63
+ """
64
+ The inertia_of_point_mass function has been moved.
65
+ Import it from "sympy.physics.mechanics".
66
+ """,
67
+ deprecated_since_version="1.13",
68
+ active_deprecations_target="moved-mechanics-functions"
69
+ )
70
+ return _inertia_of_point_mass(mass, pos_vec, frame)
71
+
72
+
73
+ def linear_momentum(frame, *body):
74
+ """Linear momentum of the system.
75
+
76
+ Explanation
77
+ ===========
78
+
79
+ This function returns the linear momentum of a system of Particle's and/or
80
+ RigidBody's. The linear momentum of a system is equal to the vector sum of
81
+ the linear momentum of its constituents. Consider a system, S, comprised of
82
+ a rigid body, A, and a particle, P. The linear momentum of the system, L,
83
+ is equal to the vector sum of the linear momentum of the particle, L1, and
84
+ the linear momentum of the rigid body, L2, i.e.
85
+
86
+ L = L1 + L2
87
+
88
+ Parameters
89
+ ==========
90
+
91
+ frame : ReferenceFrame
92
+ The frame in which linear momentum is desired.
93
+ body1, body2, body3... : Particle and/or RigidBody
94
+ The body (or bodies) whose linear momentum is required.
95
+
96
+ Examples
97
+ ========
98
+
99
+ >>> from sympy.physics.mechanics import Point, Particle, ReferenceFrame
100
+ >>> from sympy.physics.mechanics import RigidBody, outer, linear_momentum
101
+ >>> N = ReferenceFrame('N')
102
+ >>> P = Point('P')
103
+ >>> P.set_vel(N, 10 * N.x)
104
+ >>> Pa = Particle('Pa', P, 1)
105
+ >>> Ac = Point('Ac')
106
+ >>> Ac.set_vel(N, 25 * N.y)
107
+ >>> I = outer(N.x, N.x)
108
+ >>> A = RigidBody('A', Ac, N, 20, (I, Ac))
109
+ >>> linear_momentum(N, A, Pa)
110
+ 10*N.x + 500*N.y
111
+
112
+ """
113
+
114
+ if not isinstance(frame, ReferenceFrame):
115
+ raise TypeError('Please specify a valid ReferenceFrame')
116
+ else:
117
+ linear_momentum_sys = Vector(0)
118
+ for e in body:
119
+ if isinstance(e, (RigidBody, Particle)):
120
+ linear_momentum_sys += e.linear_momentum(frame)
121
+ else:
122
+ raise TypeError('*body must have only Particle or RigidBody')
123
+ return linear_momentum_sys
124
+
125
+
126
+ def angular_momentum(point, frame, *body):
127
+ """Angular momentum of a system.
128
+
129
+ Explanation
130
+ ===========
131
+
132
+ This function returns the angular momentum of a system of Particle's and/or
133
+ RigidBody's. The angular momentum of such a system is equal to the vector
134
+ sum of the angular momentum of its constituents. Consider a system, S,
135
+ comprised of a rigid body, A, and a particle, P. The angular momentum of
136
+ the system, H, is equal to the vector sum of the angular momentum of the
137
+ particle, H1, and the angular momentum of the rigid body, H2, i.e.
138
+
139
+ H = H1 + H2
140
+
141
+ Parameters
142
+ ==========
143
+
144
+ point : Point
145
+ The point about which angular momentum of the system is desired.
146
+ frame : ReferenceFrame
147
+ The frame in which angular momentum is desired.
148
+ body1, body2, body3... : Particle and/or RigidBody
149
+ The body (or bodies) whose angular momentum is required.
150
+
151
+ Examples
152
+ ========
153
+
154
+ >>> from sympy.physics.mechanics import Point, Particle, ReferenceFrame
155
+ >>> from sympy.physics.mechanics import RigidBody, outer, angular_momentum
156
+ >>> N = ReferenceFrame('N')
157
+ >>> O = Point('O')
158
+ >>> O.set_vel(N, 0 * N.x)
159
+ >>> P = O.locatenew('P', 1 * N.x)
160
+ >>> P.set_vel(N, 10 * N.x)
161
+ >>> Pa = Particle('Pa', P, 1)
162
+ >>> Ac = O.locatenew('Ac', 2 * N.y)
163
+ >>> Ac.set_vel(N, 5 * N.y)
164
+ >>> a = ReferenceFrame('a')
165
+ >>> a.set_ang_vel(N, 10 * N.z)
166
+ >>> I = outer(N.z, N.z)
167
+ >>> A = RigidBody('A', Ac, a, 20, (I, Ac))
168
+ >>> angular_momentum(O, N, Pa, A)
169
+ 10*N.z
170
+
171
+ """
172
+
173
+ if not isinstance(frame, ReferenceFrame):
174
+ raise TypeError('Please enter a valid ReferenceFrame')
175
+ if not isinstance(point, Point):
176
+ raise TypeError('Please specify a valid Point')
177
+ else:
178
+ angular_momentum_sys = Vector(0)
179
+ for e in body:
180
+ if isinstance(e, (RigidBody, Particle)):
181
+ angular_momentum_sys += e.angular_momentum(point, frame)
182
+ else:
183
+ raise TypeError('*body must have only Particle or RigidBody')
184
+ return angular_momentum_sys
185
+
186
+
187
+ def kinetic_energy(frame, *body):
188
+ """Kinetic energy of a multibody system.
189
+
190
+ Explanation
191
+ ===========
192
+
193
+ This function returns the kinetic energy of a system of Particle's and/or
194
+ RigidBody's. The kinetic energy of such a system is equal to the sum of
195
+ the kinetic energies of its constituents. Consider a system, S, comprising
196
+ a rigid body, A, and a particle, P. The kinetic energy of the system, T,
197
+ is equal to the vector sum of the kinetic energy of the particle, T1, and
198
+ the kinetic energy of the rigid body, T2, i.e.
199
+
200
+ T = T1 + T2
201
+
202
+ Kinetic energy is a scalar.
203
+
204
+ Parameters
205
+ ==========
206
+
207
+ frame : ReferenceFrame
208
+ The frame in which the velocity or angular velocity of the body is
209
+ defined.
210
+ body1, body2, body3... : Particle and/or RigidBody
211
+ The body (or bodies) whose kinetic energy is required.
212
+
213
+ Examples
214
+ ========
215
+
216
+ >>> from sympy.physics.mechanics import Point, Particle, ReferenceFrame
217
+ >>> from sympy.physics.mechanics import RigidBody, outer, kinetic_energy
218
+ >>> N = ReferenceFrame('N')
219
+ >>> O = Point('O')
220
+ >>> O.set_vel(N, 0 * N.x)
221
+ >>> P = O.locatenew('P', 1 * N.x)
222
+ >>> P.set_vel(N, 10 * N.x)
223
+ >>> Pa = Particle('Pa', P, 1)
224
+ >>> Ac = O.locatenew('Ac', 2 * N.y)
225
+ >>> Ac.set_vel(N, 5 * N.y)
226
+ >>> a = ReferenceFrame('a')
227
+ >>> a.set_ang_vel(N, 10 * N.z)
228
+ >>> I = outer(N.z, N.z)
229
+ >>> A = RigidBody('A', Ac, a, 20, (I, Ac))
230
+ >>> kinetic_energy(N, Pa, A)
231
+ 350
232
+
233
+ """
234
+
235
+ if not isinstance(frame, ReferenceFrame):
236
+ raise TypeError('Please enter a valid ReferenceFrame')
237
+ ke_sys = S.Zero
238
+ for e in body:
239
+ if isinstance(e, (RigidBody, Particle)):
240
+ ke_sys += e.kinetic_energy(frame)
241
+ else:
242
+ raise TypeError('*body must have only Particle or RigidBody')
243
+ return ke_sys
244
+
245
+
246
+ def potential_energy(*body):
247
+ """Potential energy of a multibody system.
248
+
249
+ Explanation
250
+ ===========
251
+
252
+ This function returns the potential energy of a system of Particle's and/or
253
+ RigidBody's. The potential energy of such a system is equal to the sum of
254
+ the potential energy of its constituents. Consider a system, S, comprising
255
+ a rigid body, A, and a particle, P. The potential energy of the system, V,
256
+ is equal to the vector sum of the potential energy of the particle, V1, and
257
+ the potential energy of the rigid body, V2, i.e.
258
+
259
+ V = V1 + V2
260
+
261
+ Potential energy is a scalar.
262
+
263
+ Parameters
264
+ ==========
265
+
266
+ body1, body2, body3... : Particle and/or RigidBody
267
+ The body (or bodies) whose potential energy is required.
268
+
269
+ Examples
270
+ ========
271
+
272
+ >>> from sympy.physics.mechanics import Point, Particle, ReferenceFrame
273
+ >>> from sympy.physics.mechanics import RigidBody, outer, potential_energy
274
+ >>> from sympy import symbols
275
+ >>> M, m, g, h = symbols('M m g h')
276
+ >>> N = ReferenceFrame('N')
277
+ >>> O = Point('O')
278
+ >>> O.set_vel(N, 0 * N.x)
279
+ >>> P = O.locatenew('P', 1 * N.x)
280
+ >>> Pa = Particle('Pa', P, m)
281
+ >>> Ac = O.locatenew('Ac', 2 * N.y)
282
+ >>> a = ReferenceFrame('a')
283
+ >>> I = outer(N.z, N.z)
284
+ >>> A = RigidBody('A', Ac, a, M, (I, Ac))
285
+ >>> Pa.potential_energy = m * g * h
286
+ >>> A.potential_energy = M * g * h
287
+ >>> potential_energy(Pa, A)
288
+ M*g*h + g*h*m
289
+
290
+ """
291
+
292
+ pe_sys = S.Zero
293
+ for e in body:
294
+ if isinstance(e, (RigidBody, Particle)):
295
+ pe_sys += e.potential_energy
296
+ else:
297
+ raise TypeError('*body must have only Particle or RigidBody')
298
+ return pe_sys
299
+
300
+
301
+ def gravity(acceleration, *bodies):
302
+ from sympy.physics.mechanics.loads import gravity as _gravity
303
+ sympy_deprecation_warning(
304
+ """
305
+ The gravity function has been moved.
306
+ Import it from "sympy.physics.mechanics.loads".
307
+ """,
308
+ deprecated_since_version="1.13",
309
+ active_deprecations_target="moved-mechanics-functions"
310
+ )
311
+ return _gravity(acceleration, *bodies)
312
+
313
+
314
+ def center_of_mass(point, *bodies):
315
+ """
316
+ Returns the position vector from the given point to the center of mass
317
+ of the given bodies(particles or rigidbodies).
318
+
319
+ Example
320
+ =======
321
+
322
+ >>> from sympy import symbols, S
323
+ >>> from sympy.physics.vector import Point
324
+ >>> from sympy.physics.mechanics import Particle, ReferenceFrame, RigidBody, outer
325
+ >>> from sympy.physics.mechanics.functions import center_of_mass
326
+ >>> a = ReferenceFrame('a')
327
+ >>> m = symbols('m', real=True)
328
+ >>> p1 = Particle('p1', Point('p1_pt'), S(1))
329
+ >>> p2 = Particle('p2', Point('p2_pt'), S(2))
330
+ >>> p3 = Particle('p3', Point('p3_pt'), S(3))
331
+ >>> p4 = Particle('p4', Point('p4_pt'), m)
332
+ >>> b_f = ReferenceFrame('b_f')
333
+ >>> b_cm = Point('b_cm')
334
+ >>> mb = symbols('mb')
335
+ >>> b = RigidBody('b', b_cm, b_f, mb, (outer(b_f.x, b_f.x), b_cm))
336
+ >>> p2.point.set_pos(p1.point, a.x)
337
+ >>> p3.point.set_pos(p1.point, a.x + a.y)
338
+ >>> p4.point.set_pos(p1.point, a.y)
339
+ >>> b.masscenter.set_pos(p1.point, a.y + a.z)
340
+ >>> point_o=Point('o')
341
+ >>> point_o.set_pos(p1.point, center_of_mass(p1.point, p1, p2, p3, p4, b))
342
+ >>> expr = 5/(m + mb + 6)*a.x + (m + mb + 3)/(m + mb + 6)*a.y + mb/(m + mb + 6)*a.z
343
+ >>> point_o.pos_from(p1.point)
344
+ 5/(m + mb + 6)*a.x + (m + mb + 3)/(m + mb + 6)*a.y + mb/(m + mb + 6)*a.z
345
+
346
+ """
347
+ if not bodies:
348
+ raise TypeError("No bodies(instances of Particle or Rigidbody) were passed.")
349
+
350
+ total_mass = 0
351
+ vec = Vector(0)
352
+ for i in bodies:
353
+ total_mass += i.mass
354
+
355
+ masscenter = getattr(i, 'masscenter', None)
356
+ if masscenter is None:
357
+ masscenter = i.point
358
+ vec += i.mass*masscenter.pos_from(point)
359
+
360
+ return vec/total_mass
361
+
362
+
363
+ def Lagrangian(frame, *body):
364
+ """Lagrangian of a multibody system.
365
+
366
+ Explanation
367
+ ===========
368
+
369
+ This function returns the Lagrangian of a system of Particle's and/or
370
+ RigidBody's. The Lagrangian of such a system is equal to the difference
371
+ between the kinetic energies and potential energies of its constituents. If
372
+ T and V are the kinetic and potential energies of a system then it's
373
+ Lagrangian, L, is defined as
374
+
375
+ L = T - V
376
+
377
+ The Lagrangian is a scalar.
378
+
379
+ Parameters
380
+ ==========
381
+
382
+ frame : ReferenceFrame
383
+ The frame in which the velocity or angular velocity of the body is
384
+ defined to determine the kinetic energy.
385
+
386
+ body1, body2, body3... : Particle and/or RigidBody
387
+ The body (or bodies) whose Lagrangian is required.
388
+
389
+ Examples
390
+ ========
391
+
392
+ >>> from sympy.physics.mechanics import Point, Particle, ReferenceFrame
393
+ >>> from sympy.physics.mechanics import RigidBody, outer, Lagrangian
394
+ >>> from sympy import symbols
395
+ >>> M, m, g, h = symbols('M m g h')
396
+ >>> N = ReferenceFrame('N')
397
+ >>> O = Point('O')
398
+ >>> O.set_vel(N, 0 * N.x)
399
+ >>> P = O.locatenew('P', 1 * N.x)
400
+ >>> P.set_vel(N, 10 * N.x)
401
+ >>> Pa = Particle('Pa', P, 1)
402
+ >>> Ac = O.locatenew('Ac', 2 * N.y)
403
+ >>> Ac.set_vel(N, 5 * N.y)
404
+ >>> a = ReferenceFrame('a')
405
+ >>> a.set_ang_vel(N, 10 * N.z)
406
+ >>> I = outer(N.z, N.z)
407
+ >>> A = RigidBody('A', Ac, a, 20, (I, Ac))
408
+ >>> Pa.potential_energy = m * g * h
409
+ >>> A.potential_energy = M * g * h
410
+ >>> Lagrangian(N, Pa, A)
411
+ -M*g*h - g*h*m + 350
412
+
413
+ """
414
+
415
+ if not isinstance(frame, ReferenceFrame):
416
+ raise TypeError('Please supply a valid ReferenceFrame')
417
+ for e in body:
418
+ if not isinstance(e, (RigidBody, Particle)):
419
+ raise TypeError('*body must have only Particle or RigidBody')
420
+ return kinetic_energy(frame, *body) - potential_energy(*body)
421
+
422
+
423
+ def find_dynamicsymbols(expression, exclude=None, reference_frame=None):
424
+ """Find all dynamicsymbols in expression.
425
+
426
+ Explanation
427
+ ===========
428
+
429
+ If the optional ``exclude`` kwarg is used, only dynamicsymbols
430
+ not in the iterable ``exclude`` are returned.
431
+ If we intend to apply this function on a vector, the optional
432
+ ``reference_frame`` is also used to inform about the corresponding frame
433
+ with respect to which the dynamic symbols of the given vector is to be
434
+ determined.
435
+
436
+ Parameters
437
+ ==========
438
+
439
+ expression : SymPy expression
440
+
441
+ exclude : iterable of dynamicsymbols, optional
442
+
443
+ reference_frame : ReferenceFrame, optional
444
+ The frame with respect to which the dynamic symbols of the
445
+ given vector is to be determined.
446
+
447
+ Examples
448
+ ========
449
+
450
+ >>> from sympy.physics.mechanics import dynamicsymbols, find_dynamicsymbols
451
+ >>> from sympy.physics.mechanics import ReferenceFrame
452
+ >>> x, y = dynamicsymbols('x, y')
453
+ >>> expr = x + x.diff()*y
454
+ >>> find_dynamicsymbols(expr)
455
+ {x(t), y(t), Derivative(x(t), t)}
456
+ >>> find_dynamicsymbols(expr, exclude=[x, y])
457
+ {Derivative(x(t), t)}
458
+ >>> a, b, c = dynamicsymbols('a, b, c')
459
+ >>> A = ReferenceFrame('A')
460
+ >>> v = a * A.x + b * A.y + c * A.z
461
+ >>> find_dynamicsymbols(v, reference_frame=A)
462
+ {a(t), b(t), c(t)}
463
+
464
+ """
465
+ t_set = {dynamicsymbols._t}
466
+ if exclude:
467
+ if iterable(exclude):
468
+ exclude_set = set(exclude)
469
+ else:
470
+ raise TypeError("exclude kwarg must be iterable")
471
+ else:
472
+ exclude_set = set()
473
+ if isinstance(expression, Vector):
474
+ if reference_frame is None:
475
+ raise ValueError("You must provide reference_frame when passing a "
476
+ "vector expression, got %s." % reference_frame)
477
+ else:
478
+ expression = expression.to_matrix(reference_frame)
479
+ return {i for i in expression.atoms(AppliedUndef, Derivative) if
480
+ i.free_symbols == t_set} - exclude_set
481
+
482
+
483
+ def msubs(expr, *sub_dicts, smart=False, **kwargs):
484
+ """A custom subs for use on expressions derived in physics.mechanics.
485
+
486
+ Traverses the expression tree once, performing the subs found in sub_dicts.
487
+ Terms inside ``Derivative`` expressions are ignored:
488
+
489
+ Examples
490
+ ========
491
+
492
+ >>> from sympy.physics.mechanics import dynamicsymbols, msubs
493
+ >>> x = dynamicsymbols('x')
494
+ >>> msubs(x.diff() + x, {x: 1})
495
+ Derivative(x(t), t) + 1
496
+
497
+ Note that sub_dicts can be a single dictionary, or several dictionaries:
498
+
499
+ >>> x, y, z = dynamicsymbols('x, y, z')
500
+ >>> sub1 = {x: 1, y: 2}
501
+ >>> sub2 = {z: 3, x.diff(): 4}
502
+ >>> msubs(x.diff() + x + y + z, sub1, sub2)
503
+ 10
504
+
505
+ If smart=True (default False), also checks for conditions that may result
506
+ in ``nan``, but if simplified would yield a valid expression. For example:
507
+
508
+ >>> from sympy import sin, tan
509
+ >>> (sin(x)/tan(x)).subs(x, 0)
510
+ nan
511
+ >>> msubs(sin(x)/tan(x), {x: 0}, smart=True)
512
+ 1
513
+
514
+ It does this by first replacing all ``tan`` with ``sin/cos``. Then each
515
+ node is traversed. If the node is a fraction, subs is first evaluated on
516
+ the denominator. If this results in 0, simplification of the entire
517
+ fraction is attempted. Using this selective simplification, only
518
+ subexpressions that result in 1/0 are targeted, resulting in faster
519
+ performance.
520
+
521
+ """
522
+
523
+ sub_dict = dict_merge(*sub_dicts)
524
+ if smart:
525
+ func = _smart_subs
526
+ elif hasattr(expr, 'msubs'):
527
+ return expr.msubs(sub_dict)
528
+ else:
529
+ func = lambda expr, sub_dict: _crawl(expr, _sub_func, sub_dict)
530
+ if isinstance(expr, (Matrix, Vector, Dyadic)):
531
+ return expr.applyfunc(lambda x: func(x, sub_dict))
532
+ else:
533
+ return func(expr, sub_dict)
534
+
535
+
536
+ def _crawl(expr, func, *args, **kwargs):
537
+ """Crawl the expression tree, and apply func to every node."""
538
+ val = func(expr, *args, **kwargs)
539
+ if val is not None:
540
+ return val
541
+ new_args = (_crawl(arg, func, *args, **kwargs) for arg in expr.args)
542
+ return expr.func(*new_args)
543
+
544
+
545
+ def _sub_func(expr, sub_dict):
546
+ """Perform direct matching substitution, ignoring derivatives."""
547
+ if expr in sub_dict:
548
+ return sub_dict[expr]
549
+ elif not expr.args or expr.is_Derivative:
550
+ return expr
551
+
552
+
553
+ def _tan_repl_func(expr):
554
+ """Replace tan with sin/cos."""
555
+ if isinstance(expr, tan):
556
+ return sin(*expr.args) / cos(*expr.args)
557
+ elif not expr.args or expr.is_Derivative:
558
+ return expr
559
+
560
+
561
+ def _smart_subs(expr, sub_dict):
562
+ """Performs subs, checking for conditions that may result in `nan` or
563
+ `oo`, and attempts to simplify them out.
564
+
565
+ The expression tree is traversed twice, and the following steps are
566
+ performed on each expression node:
567
+ - First traverse:
568
+ Replace all `tan` with `sin/cos`.
569
+ - Second traverse:
570
+ If node is a fraction, check if the denominator evaluates to 0.
571
+ If so, attempt to simplify it out. Then if node is in sub_dict,
572
+ sub in the corresponding value.
573
+
574
+ """
575
+ expr = _crawl(expr, _tan_repl_func)
576
+
577
+ def _recurser(expr, sub_dict):
578
+ # Decompose the expression into num, den
579
+ num, den = _fraction_decomp(expr)
580
+ if den != 1:
581
+ # If there is a non trivial denominator, we need to handle it
582
+ denom_subbed = _recurser(den, sub_dict)
583
+ if denom_subbed.evalf() == 0:
584
+ # If denom is 0 after this, attempt to simplify the bad expr
585
+ expr = simplify(expr)
586
+ else:
587
+ # Expression won't result in nan, find numerator
588
+ num_subbed = _recurser(num, sub_dict)
589
+ return num_subbed / denom_subbed
590
+ # We have to crawl the tree manually, because `expr` may have been
591
+ # modified in the simplify step. First, perform subs as normal:
592
+ val = _sub_func(expr, sub_dict)
593
+ if val is not None:
594
+ return val
595
+ new_args = (_recurser(arg, sub_dict) for arg in expr.args)
596
+ return expr.func(*new_args)
597
+ return _recurser(expr, sub_dict)
598
+
599
+
600
+ def _fraction_decomp(expr):
601
+ """Return num, den such that expr = num/den."""
602
+ if not isinstance(expr, Mul):
603
+ return expr, 1
604
+ num = []
605
+ den = []
606
+ for a in expr.args:
607
+ if a.is_Pow and a.args[1] < 0:
608
+ den.append(1 / a)
609
+ else:
610
+ num.append(a)
611
+ if not den:
612
+ return expr, 1
613
+ num = Mul(*num)
614
+ den = Mul(*den)
615
+ return num, den
616
+
617
+
618
+ def _f_list_parser(fl, ref_frame):
619
+ """Parses the provided forcelist composed of items
620
+ of the form (obj, force).
621
+ Returns a tuple containing:
622
+ vel_list: The velocity (ang_vel for Frames, vel for Points) in
623
+ the provided reference frame.
624
+ f_list: The forces.
625
+
626
+ Used internally in the KanesMethod and LagrangesMethod classes.
627
+
628
+ """
629
+ def flist_iter():
630
+ for pair in fl:
631
+ obj, force = pair
632
+ if isinstance(obj, ReferenceFrame):
633
+ yield obj.ang_vel_in(ref_frame), force
634
+ elif isinstance(obj, Point):
635
+ yield obj.vel(ref_frame), force
636
+ else:
637
+ raise TypeError('First entry in each forcelist pair must '
638
+ 'be a point or frame.')
639
+
640
+ if not fl:
641
+ vel_list, f_list = (), ()
642
+ else:
643
+ unzip = lambda l: list(zip(*l)) if l[0] else [(), ()]
644
+ vel_list, f_list = unzip(list(flist_iter()))
645
+ return vel_list, f_list
646
+
647
+
648
+ def _validate_coordinates(coordinates=None, speeds=None, check_duplicates=True,
649
+ is_dynamicsymbols=True, u_auxiliary=None):
650
+ """Validate the generalized coordinates and generalized speeds.
651
+
652
+ Parameters
653
+ ==========
654
+ coordinates : iterable, optional
655
+ Generalized coordinates to be validated.
656
+ speeds : iterable, optional
657
+ Generalized speeds to be validated.
658
+ check_duplicates : bool, optional
659
+ Checks if there are duplicates in the generalized coordinates and
660
+ generalized speeds. If so it will raise a ValueError. The default is
661
+ True.
662
+ is_dynamicsymbols : iterable, optional
663
+ Checks if all the generalized coordinates and generalized speeds are
664
+ dynamicsymbols. If any is not a dynamicsymbol, a ValueError will be
665
+ raised. The default is True.
666
+ u_auxiliary : iterable, optional
667
+ Auxiliary generalized speeds to be validated.
668
+
669
+ """
670
+ t_set = {dynamicsymbols._t}
671
+ # Convert input to iterables
672
+ if coordinates is None:
673
+ coordinates = []
674
+ elif not iterable(coordinates):
675
+ coordinates = [coordinates]
676
+ if speeds is None:
677
+ speeds = []
678
+ elif not iterable(speeds):
679
+ speeds = [speeds]
680
+ if u_auxiliary is None:
681
+ u_auxiliary = []
682
+ elif not iterable(u_auxiliary):
683
+ u_auxiliary = [u_auxiliary]
684
+
685
+ msgs = []
686
+ if check_duplicates: # Check for duplicates
687
+ seen = set()
688
+ coord_duplicates = {x for x in coordinates if x in seen or seen.add(x)}
689
+ seen = set()
690
+ speed_duplicates = {x for x in speeds if x in seen or seen.add(x)}
691
+ seen = set()
692
+ aux_duplicates = {x for x in u_auxiliary if x in seen or seen.add(x)}
693
+ overlap_coords = set(coordinates).intersection(speeds)
694
+ overlap_aux = set(coordinates).union(speeds).intersection(u_auxiliary)
695
+ if coord_duplicates:
696
+ msgs.append(f'The generalized coordinates {coord_duplicates} are '
697
+ f'duplicated, all generalized coordinates should be '
698
+ f'unique.')
699
+ if speed_duplicates:
700
+ msgs.append(f'The generalized speeds {speed_duplicates} are '
701
+ f'duplicated, all generalized speeds should be unique.')
702
+ if aux_duplicates:
703
+ msgs.append(f'The auxiliary speeds {aux_duplicates} are duplicated,'
704
+ f' all auxiliary speeds should be unique.')
705
+ if overlap_coords:
706
+ msgs.append(f'{overlap_coords} are defined as both generalized '
707
+ f'coordinates and generalized speeds.')
708
+ if overlap_aux:
709
+ msgs.append(f'The auxiliary speeds {overlap_aux} are also defined '
710
+ f'as generalized coordinates or generalized speeds.')
711
+ if is_dynamicsymbols: # Check whether all coordinates are dynamicsymbols
712
+ for coordinate in coordinates:
713
+ if not (isinstance(coordinate, (AppliedUndef, Derivative)) and
714
+ coordinate.free_symbols == t_set):
715
+ msgs.append(f'Generalized coordinate "{coordinate}" is not a '
716
+ f'dynamicsymbol.')
717
+ for speed in speeds:
718
+ if not (isinstance(speed, (AppliedUndef, Derivative)) and
719
+ speed.free_symbols == t_set):
720
+ msgs.append(
721
+ f'Generalized speed "{speed}" is not a dynamicsymbol.')
722
+ for aux in u_auxiliary:
723
+ if not (isinstance(aux, (AppliedUndef, Derivative)) and
724
+ aux.free_symbols == t_set):
725
+ msgs.append(
726
+ f'Auxiliary speed "{aux}" is not a dynamicsymbol.')
727
+ if msgs:
728
+ raise ValueError('\n'.join(msgs))
729
+
730
+
731
+ def _parse_linear_solver(linear_solver):
732
+ """Helper function to retrieve a specified linear solver."""
733
+ if callable(linear_solver):
734
+ return linear_solver
735
+ return lambda A, b: Matrix.solve(A, b, method=linear_solver)
lib/python3.12/site-packages/sympy/physics/mechanics/inertia.py ADDED
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sympy import sympify
2
+ from sympy.physics.vector import Point, Dyadic, ReferenceFrame, outer
3
+ from collections import namedtuple
4
+
5
+ __all__ = ['inertia', 'inertia_of_point_mass', 'Inertia']
6
+
7
+
8
+ def inertia(frame, ixx, iyy, izz, ixy=0, iyz=0, izx=0):
9
+ """Simple way to create inertia Dyadic object.
10
+
11
+ Explanation
12
+ ===========
13
+
14
+ Creates an inertia Dyadic based on the given tensor values and a body-fixed
15
+ reference frame.
16
+
17
+ Parameters
18
+ ==========
19
+
20
+ frame : ReferenceFrame
21
+ The frame the inertia is defined in.
22
+ ixx : Sympifyable
23
+ The xx element in the inertia dyadic.
24
+ iyy : Sympifyable
25
+ The yy element in the inertia dyadic.
26
+ izz : Sympifyable
27
+ The zz element in the inertia dyadic.
28
+ ixy : Sympifyable
29
+ The xy element in the inertia dyadic.
30
+ iyz : Sympifyable
31
+ The yz element in the inertia dyadic.
32
+ izx : Sympifyable
33
+ The zx element in the inertia dyadic.
34
+
35
+ Examples
36
+ ========
37
+
38
+ >>> from sympy.physics.mechanics import ReferenceFrame, inertia
39
+ >>> N = ReferenceFrame('N')
40
+ >>> inertia(N, 1, 2, 3)
41
+ (N.x|N.x) + 2*(N.y|N.y) + 3*(N.z|N.z)
42
+
43
+ """
44
+
45
+ if not isinstance(frame, ReferenceFrame):
46
+ raise TypeError('Need to define the inertia in a frame')
47
+ ixx, iyy, izz = sympify(ixx), sympify(iyy), sympify(izz)
48
+ ixy, iyz, izx = sympify(ixy), sympify(iyz), sympify(izx)
49
+ return (ixx*outer(frame.x, frame.x) + ixy*outer(frame.x, frame.y) +
50
+ izx*outer(frame.x, frame.z) + ixy*outer(frame.y, frame.x) +
51
+ iyy*outer(frame.y, frame.y) + iyz*outer(frame.y, frame.z) +
52
+ izx*outer(frame.z, frame.x) + iyz*outer(frame.z, frame.y) +
53
+ izz*outer(frame.z, frame.z))
54
+
55
+
56
+ def inertia_of_point_mass(mass, pos_vec, frame):
57
+ """Inertia dyadic of a point mass relative to point O.
58
+
59
+ Parameters
60
+ ==========
61
+
62
+ mass : Sympifyable
63
+ Mass of the point mass
64
+ pos_vec : Vector
65
+ Position from point O to point mass
66
+ frame : ReferenceFrame
67
+ Reference frame to express the dyadic in
68
+
69
+ Examples
70
+ ========
71
+
72
+ >>> from sympy import symbols
73
+ >>> from sympy.physics.mechanics import ReferenceFrame, inertia_of_point_mass
74
+ >>> N = ReferenceFrame('N')
75
+ >>> r, m = symbols('r m')
76
+ >>> px = r * N.x
77
+ >>> inertia_of_point_mass(m, px, N)
78
+ m*r**2*(N.y|N.y) + m*r**2*(N.z|N.z)
79
+
80
+ """
81
+
82
+ return mass*(
83
+ (outer(frame.x, frame.x) +
84
+ outer(frame.y, frame.y) +
85
+ outer(frame.z, frame.z)) *
86
+ (pos_vec.dot(pos_vec)) - outer(pos_vec, pos_vec))
87
+
88
+
89
+ class Inertia(namedtuple('Inertia', ['dyadic', 'point'])):
90
+ """Inertia object consisting of a Dyadic and a Point of reference.
91
+
92
+ Explanation
93
+ ===========
94
+
95
+ This is a simple class to store the Point and Dyadic, belonging to an
96
+ inertia.
97
+
98
+ Attributes
99
+ ==========
100
+
101
+ dyadic : Dyadic
102
+ The dyadic of the inertia.
103
+ point : Point
104
+ The reference point of the inertia.
105
+
106
+ Examples
107
+ ========
108
+
109
+ >>> from sympy.physics.mechanics import ReferenceFrame, Point, Inertia
110
+ >>> N = ReferenceFrame('N')
111
+ >>> Po = Point('Po')
112
+ >>> Inertia(N.x.outer(N.x) + N.y.outer(N.y) + N.z.outer(N.z), Po)
113
+ ((N.x|N.x) + (N.y|N.y) + (N.z|N.z), Po)
114
+
115
+ In the example above the Dyadic was created manually, one can however also
116
+ use the ``inertia`` function for this or the class method ``from_tensor`` as
117
+ shown below.
118
+
119
+ >>> Inertia.from_inertia_scalars(Po, N, 1, 1, 1)
120
+ ((N.x|N.x) + (N.y|N.y) + (N.z|N.z), Po)
121
+
122
+ """
123
+ def __new__(cls, dyadic, point):
124
+ # Switch order if given in the wrong order
125
+ if isinstance(dyadic, Point) and isinstance(point, Dyadic):
126
+ point, dyadic = dyadic, point
127
+ if not isinstance(point, Point):
128
+ raise TypeError('Reference point should be of type Point')
129
+ if not isinstance(dyadic, Dyadic):
130
+ raise TypeError('Inertia value should be expressed as a Dyadic')
131
+ return super().__new__(cls, dyadic, point)
132
+
133
+ @classmethod
134
+ def from_inertia_scalars(cls, point, frame, ixx, iyy, izz, ixy=0, iyz=0,
135
+ izx=0):
136
+ """Simple way to create an Inertia object based on the tensor values.
137
+
138
+ Explanation
139
+ ===========
140
+
141
+ This class method uses the :func`~.inertia` to create the Dyadic based
142
+ on the tensor values.
143
+
144
+ Parameters
145
+ ==========
146
+
147
+ point : Point
148
+ The reference point of the inertia.
149
+ frame : ReferenceFrame
150
+ The frame the inertia is defined in.
151
+ ixx : Sympifyable
152
+ The xx element in the inertia dyadic.
153
+ iyy : Sympifyable
154
+ The yy element in the inertia dyadic.
155
+ izz : Sympifyable
156
+ The zz element in the inertia dyadic.
157
+ ixy : Sympifyable
158
+ The xy element in the inertia dyadic.
159
+ iyz : Sympifyable
160
+ The yz element in the inertia dyadic.
161
+ izx : Sympifyable
162
+ The zx element in the inertia dyadic.
163
+
164
+ Examples
165
+ ========
166
+
167
+ >>> from sympy import symbols
168
+ >>> from sympy.physics.mechanics import ReferenceFrame, Point, Inertia
169
+ >>> ixx, iyy, izz, ixy, iyz, izx = symbols('ixx iyy izz ixy iyz izx')
170
+ >>> N = ReferenceFrame('N')
171
+ >>> P = Point('P')
172
+ >>> I = Inertia.from_inertia_scalars(P, N, ixx, iyy, izz, ixy, iyz, izx)
173
+
174
+ The tensor values can easily be seen when converting the dyadic to a
175
+ matrix.
176
+
177
+ >>> I.dyadic.to_matrix(N)
178
+ Matrix([
179
+ [ixx, ixy, izx],
180
+ [ixy, iyy, iyz],
181
+ [izx, iyz, izz]])
182
+
183
+ """
184
+ return cls(inertia(frame, ixx, iyy, izz, ixy, iyz, izx), point)
185
+
186
+ def __add__(self, other):
187
+ raise TypeError(f"unsupported operand type(s) for +: "
188
+ f"'{self.__class__.__name__}' and "
189
+ f"'{other.__class__.__name__}'")
190
+
191
+ def __mul__(self, other):
192
+ raise TypeError(f"unsupported operand type(s) for *: "
193
+ f"'{self.__class__.__name__}' and "
194
+ f"'{other.__class__.__name__}'")
195
+
196
+ __radd__ = __add__
197
+ __rmul__ = __mul__
lib/python3.12/site-packages/sympy/physics/mechanics/lagrange.py ADDED
@@ -0,0 +1,512 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sympy import diff, zeros, Matrix, eye, sympify
2
+ from sympy.core.sorting import default_sort_key
3
+ from sympy.physics.vector import dynamicsymbols, ReferenceFrame
4
+ from sympy.physics.mechanics.method import _Methods
5
+ from sympy.physics.mechanics.functions import (
6
+ find_dynamicsymbols, msubs, _f_list_parser, _validate_coordinates)
7
+ from sympy.physics.mechanics.linearize import Linearizer
8
+ from sympy.utilities.iterables import iterable
9
+
10
+ __all__ = ['LagrangesMethod']
11
+
12
+
13
+ class LagrangesMethod(_Methods):
14
+ """Lagrange's method object.
15
+
16
+ Explanation
17
+ ===========
18
+
19
+ This object generates the equations of motion in a two step procedure. The
20
+ first step involves the initialization of LagrangesMethod by supplying the
21
+ Lagrangian and the generalized coordinates, at the bare minimum. If there
22
+ are any constraint equations, they can be supplied as keyword arguments.
23
+ The Lagrange multipliers are automatically generated and are equal in
24
+ number to the constraint equations. Similarly any non-conservative forces
25
+ can be supplied in an iterable (as described below and also shown in the
26
+ example) along with a ReferenceFrame. This is also discussed further in the
27
+ __init__ method.
28
+
29
+ Attributes
30
+ ==========
31
+
32
+ q, u : Matrix
33
+ Matrices of the generalized coordinates and speeds
34
+ loads : iterable
35
+ Iterable of (Point, vector) or (ReferenceFrame, vector) tuples
36
+ describing the forces on the system.
37
+ bodies : iterable
38
+ Iterable containing the rigid bodies and particles of the system.
39
+ mass_matrix : Matrix
40
+ The system's mass matrix
41
+ forcing : Matrix
42
+ The system's forcing vector
43
+ mass_matrix_full : Matrix
44
+ The "mass matrix" for the qdot's, qdoubledot's, and the
45
+ lagrange multipliers (lam)
46
+ forcing_full : Matrix
47
+ The forcing vector for the qdot's, qdoubledot's and
48
+ lagrange multipliers (lam)
49
+
50
+ Examples
51
+ ========
52
+
53
+ This is a simple example for a one degree of freedom translational
54
+ spring-mass-damper.
55
+
56
+ In this example, we first need to do the kinematics.
57
+ This involves creating generalized coordinates and their derivatives.
58
+ Then we create a point and set its velocity in a frame.
59
+
60
+ >>> from sympy.physics.mechanics import LagrangesMethod, Lagrangian
61
+ >>> from sympy.physics.mechanics import ReferenceFrame, Particle, Point
62
+ >>> from sympy.physics.mechanics import dynamicsymbols
63
+ >>> from sympy import symbols
64
+ >>> q = dynamicsymbols('q')
65
+ >>> qd = dynamicsymbols('q', 1)
66
+ >>> m, k, b = symbols('m k b')
67
+ >>> N = ReferenceFrame('N')
68
+ >>> P = Point('P')
69
+ >>> P.set_vel(N, qd * N.x)
70
+
71
+ We need to then prepare the information as required by LagrangesMethod to
72
+ generate equations of motion.
73
+ First we create the Particle, which has a point attached to it.
74
+ Following this the lagrangian is created from the kinetic and potential
75
+ energies.
76
+ Then, an iterable of nonconservative forces/torques must be constructed,
77
+ where each item is a (Point, Vector) or (ReferenceFrame, Vector) tuple,
78
+ with the Vectors representing the nonconservative forces or torques.
79
+
80
+ >>> Pa = Particle('Pa', P, m)
81
+ >>> Pa.potential_energy = k * q**2 / 2.0
82
+ >>> L = Lagrangian(N, Pa)
83
+ >>> fl = [(P, -b * qd * N.x)]
84
+
85
+ Finally we can generate the equations of motion.
86
+ First we create the LagrangesMethod object. To do this one must supply
87
+ the Lagrangian, and the generalized coordinates. The constraint equations,
88
+ the forcelist, and the inertial frame may also be provided, if relevant.
89
+ Next we generate Lagrange's equations of motion, such that:
90
+ Lagrange's equations of motion = 0.
91
+ We have the equations of motion at this point.
92
+
93
+ >>> l = LagrangesMethod(L, [q], forcelist = fl, frame = N)
94
+ >>> print(l.form_lagranges_equations())
95
+ Matrix([[b*Derivative(q(t), t) + 1.0*k*q(t) + m*Derivative(q(t), (t, 2))]])
96
+
97
+ We can also solve for the states using the 'rhs' method.
98
+
99
+ >>> print(l.rhs())
100
+ Matrix([[Derivative(q(t), t)], [(-b*Derivative(q(t), t) - 1.0*k*q(t))/m]])
101
+
102
+ Please refer to the docstrings on each method for more details.
103
+ """
104
+
105
+ def __init__(self, Lagrangian, qs, forcelist=None, bodies=None, frame=None,
106
+ hol_coneqs=None, nonhol_coneqs=None):
107
+ """Supply the following for the initialization of LagrangesMethod.
108
+
109
+ Lagrangian : Sympifyable
110
+
111
+ qs : array_like
112
+ The generalized coordinates
113
+
114
+ hol_coneqs : array_like, optional
115
+ The holonomic constraint equations
116
+
117
+ nonhol_coneqs : array_like, optional
118
+ The nonholonomic constraint equations
119
+
120
+ forcelist : iterable, optional
121
+ Takes an iterable of (Point, Vector) or (ReferenceFrame, Vector)
122
+ tuples which represent the force at a point or torque on a frame.
123
+ This feature is primarily to account for the nonconservative forces
124
+ and/or moments.
125
+
126
+ bodies : iterable, optional
127
+ Takes an iterable containing the rigid bodies and particles of the
128
+ system.
129
+
130
+ frame : ReferenceFrame, optional
131
+ Supply the inertial frame. This is used to determine the
132
+ generalized forces due to non-conservative forces.
133
+ """
134
+
135
+ self._L = Matrix([sympify(Lagrangian)])
136
+ self.eom = None
137
+ self._m_cd = Matrix() # Mass Matrix of differentiated coneqs
138
+ self._m_d = Matrix() # Mass Matrix of dynamic equations
139
+ self._f_cd = Matrix() # Forcing part of the diff coneqs
140
+ self._f_d = Matrix() # Forcing part of the dynamic equations
141
+ self.lam_coeffs = Matrix() # The coeffecients of the multipliers
142
+
143
+ forcelist = forcelist if forcelist else []
144
+ if not iterable(forcelist):
145
+ raise TypeError('Force pairs must be supplied in an iterable.')
146
+ self._forcelist = forcelist
147
+ if frame and not isinstance(frame, ReferenceFrame):
148
+ raise TypeError('frame must be a valid ReferenceFrame')
149
+ self._bodies = bodies
150
+ self.inertial = frame
151
+
152
+ self.lam_vec = Matrix()
153
+
154
+ self._term1 = Matrix()
155
+ self._term2 = Matrix()
156
+ self._term3 = Matrix()
157
+ self._term4 = Matrix()
158
+
159
+ # Creating the qs, qdots and qdoubledots
160
+ if not iterable(qs):
161
+ raise TypeError('Generalized coordinates must be an iterable')
162
+ self._q = Matrix(qs)
163
+ self._qdots = self.q.diff(dynamicsymbols._t)
164
+ self._qdoubledots = self._qdots.diff(dynamicsymbols._t)
165
+ _validate_coordinates(self.q)
166
+
167
+ mat_build = lambda x: Matrix(x) if x else Matrix()
168
+ hol_coneqs = mat_build(hol_coneqs)
169
+ nonhol_coneqs = mat_build(nonhol_coneqs)
170
+ self.coneqs = Matrix([hol_coneqs.diff(dynamicsymbols._t),
171
+ nonhol_coneqs])
172
+ self._hol_coneqs = hol_coneqs
173
+
174
+ def form_lagranges_equations(self):
175
+ """Method to form Lagrange's equations of motion.
176
+
177
+ Returns a vector of equations of motion using Lagrange's equations of
178
+ the second kind.
179
+ """
180
+
181
+ qds = self._qdots
182
+ qdd_zero = dict.fromkeys(self._qdoubledots, 0)
183
+ n = len(self.q)
184
+
185
+ # Internally we represent the EOM as four terms:
186
+ # EOM = term1 - term2 - term3 - term4 = 0
187
+
188
+ # First term
189
+ self._term1 = self._L.jacobian(qds)
190
+ self._term1 = self._term1.diff(dynamicsymbols._t).T
191
+
192
+ # Second term
193
+ self._term2 = self._L.jacobian(self.q).T
194
+
195
+ # Third term
196
+ if self.coneqs:
197
+ coneqs = self.coneqs
198
+ m = len(coneqs)
199
+ # Creating the multipliers
200
+ self.lam_vec = Matrix(dynamicsymbols('lam1:' + str(m + 1)))
201
+ self.lam_coeffs = -coneqs.jacobian(qds)
202
+ self._term3 = self.lam_coeffs.T * self.lam_vec
203
+ # Extracting the coeffecients of the qdds from the diff coneqs
204
+ diffconeqs = coneqs.diff(dynamicsymbols._t)
205
+ self._m_cd = diffconeqs.jacobian(self._qdoubledots)
206
+ # The remaining terms i.e. the 'forcing' terms in diff coneqs
207
+ self._f_cd = -diffconeqs.subs(qdd_zero)
208
+ else:
209
+ self._term3 = zeros(n, 1)
210
+
211
+ # Fourth term
212
+ if self.forcelist:
213
+ N = self.inertial
214
+ self._term4 = zeros(n, 1)
215
+ for i, qd in enumerate(qds):
216
+ flist = zip(*_f_list_parser(self.forcelist, N))
217
+ self._term4[i] = sum(v.diff(qd, N).dot(f) for (v, f) in flist)
218
+ else:
219
+ self._term4 = zeros(n, 1)
220
+
221
+ # Form the dynamic mass and forcing matrices
222
+ without_lam = self._term1 - self._term2 - self._term4
223
+ self._m_d = without_lam.jacobian(self._qdoubledots)
224
+ self._f_d = -without_lam.subs(qdd_zero)
225
+
226
+ # Form the EOM
227
+ self.eom = without_lam - self._term3
228
+ return self.eom
229
+
230
+ def _form_eoms(self):
231
+ return self.form_lagranges_equations()
232
+
233
+ @property
234
+ def mass_matrix(self):
235
+ """Returns the mass matrix, which is augmented by the Lagrange
236
+ multipliers, if necessary.
237
+
238
+ Explanation
239
+ ===========
240
+
241
+ If the system is described by 'n' generalized coordinates and there are
242
+ no constraint equations then an n X n matrix is returned.
243
+
244
+ If there are 'n' generalized coordinates and 'm' constraint equations
245
+ have been supplied during initialization then an n X (n+m) matrix is
246
+ returned. The (n + m - 1)th and (n + m)th columns contain the
247
+ coefficients of the Lagrange multipliers.
248
+ """
249
+
250
+ if self.eom is None:
251
+ raise ValueError('Need to compute the equations of motion first')
252
+ if self.coneqs:
253
+ return (self._m_d).row_join(self.lam_coeffs.T)
254
+ else:
255
+ return self._m_d
256
+
257
+ @property
258
+ def mass_matrix_full(self):
259
+ """Augments the coefficients of qdots to the mass_matrix."""
260
+
261
+ if self.eom is None:
262
+ raise ValueError('Need to compute the equations of motion first')
263
+ n = len(self.q)
264
+ m = len(self.coneqs)
265
+ row1 = eye(n).row_join(zeros(n, n + m))
266
+ row2 = zeros(n, n).row_join(self.mass_matrix)
267
+ if self.coneqs:
268
+ row3 = zeros(m, n).row_join(self._m_cd).row_join(zeros(m, m))
269
+ return row1.col_join(row2).col_join(row3)
270
+ else:
271
+ return row1.col_join(row2)
272
+
273
+ @property
274
+ def forcing(self):
275
+ """Returns the forcing vector from 'lagranges_equations' method."""
276
+
277
+ if self.eom is None:
278
+ raise ValueError('Need to compute the equations of motion first')
279
+ return self._f_d
280
+
281
+ @property
282
+ def forcing_full(self):
283
+ """Augments qdots to the forcing vector above."""
284
+
285
+ if self.eom is None:
286
+ raise ValueError('Need to compute the equations of motion first')
287
+ if self.coneqs:
288
+ return self._qdots.col_join(self.forcing).col_join(self._f_cd)
289
+ else:
290
+ return self._qdots.col_join(self.forcing)
291
+
292
+ def to_linearizer(self, q_ind=None, qd_ind=None, q_dep=None, qd_dep=None,
293
+ linear_solver='LU'):
294
+ """Returns an instance of the Linearizer class, initiated from the data
295
+ in the LagrangesMethod class. This may be more desirable than using the
296
+ linearize class method, as the Linearizer object will allow more
297
+ efficient recalculation (i.e. about varying operating points).
298
+
299
+ Parameters
300
+ ==========
301
+
302
+ q_ind, qd_ind : array_like, optional
303
+ The independent generalized coordinates and speeds.
304
+ q_dep, qd_dep : array_like, optional
305
+ The dependent generalized coordinates and speeds.
306
+ linear_solver : str, callable
307
+ Method used to solve the several symbolic linear systems of the
308
+ form ``A*x=b`` in the linearization process. If a string is
309
+ supplied, it should be a valid method that can be used with the
310
+ :meth:`sympy.matrices.matrixbase.MatrixBase.solve`. If a callable is
311
+ supplied, it should have the format ``x = f(A, b)``, where it
312
+ solves the equations and returns the solution. The default is
313
+ ``'LU'`` which corresponds to SymPy's ``A.LUsolve(b)``.
314
+ ``LUsolve()`` is fast to compute but will often result in
315
+ divide-by-zero and thus ``nan`` results.
316
+
317
+ Returns
318
+ =======
319
+ Linearizer
320
+ An instantiated
321
+ :class:`sympy.physics.mechanics.linearize.Linearizer`.
322
+
323
+ """
324
+
325
+ # Compose vectors
326
+ t = dynamicsymbols._t
327
+ q = self.q
328
+ u = self._qdots
329
+ ud = u.diff(t)
330
+ # Get vector of lagrange multipliers
331
+ lams = self.lam_vec
332
+
333
+ mat_build = lambda x: Matrix(x) if x else Matrix()
334
+ q_i = mat_build(q_ind)
335
+ q_d = mat_build(q_dep)
336
+ u_i = mat_build(qd_ind)
337
+ u_d = mat_build(qd_dep)
338
+
339
+ # Compose general form equations
340
+ f_c = self._hol_coneqs
341
+ f_v = self.coneqs
342
+ f_a = f_v.diff(t)
343
+ f_0 = u
344
+ f_1 = -u
345
+ f_2 = self._term1
346
+ f_3 = -(self._term2 + self._term4)
347
+ f_4 = -self._term3
348
+
349
+ # Check that there are an appropriate number of independent and
350
+ # dependent coordinates
351
+ if len(q_d) != len(f_c) or len(u_d) != len(f_v):
352
+ raise ValueError(("Must supply {:} dependent coordinates, and " +
353
+ "{:} dependent speeds").format(len(f_c), len(f_v)))
354
+ if set(Matrix([q_i, q_d])) != set(q):
355
+ raise ValueError("Must partition q into q_ind and q_dep, with " +
356
+ "no extra or missing symbols.")
357
+ if set(Matrix([u_i, u_d])) != set(u):
358
+ raise ValueError("Must partition qd into qd_ind and qd_dep, " +
359
+ "with no extra or missing symbols.")
360
+
361
+ # Find all other dynamic symbols, forming the forcing vector r.
362
+ # Sort r to make it canonical.
363
+ insyms = set(Matrix([q, u, ud, lams]))
364
+ r = list(find_dynamicsymbols(f_3, insyms))
365
+ r.sort(key=default_sort_key)
366
+ # Check for any derivatives of variables in r that are also found in r.
367
+ for i in r:
368
+ if diff(i, dynamicsymbols._t) in r:
369
+ raise ValueError('Cannot have derivatives of specified \
370
+ quantities when linearizing forcing terms.')
371
+
372
+ return Linearizer(f_0, f_1, f_2, f_3, f_4, f_c, f_v, f_a, q, u, q_i,
373
+ q_d, u_i, u_d, r, lams, linear_solver=linear_solver)
374
+
375
+ def linearize(self, q_ind=None, qd_ind=None, q_dep=None, qd_dep=None,
376
+ linear_solver='LU', **kwargs):
377
+ """Linearize the equations of motion about a symbolic operating point.
378
+
379
+ Parameters
380
+ ==========
381
+ linear_solver : str, callable
382
+ Method used to solve the several symbolic linear systems of the
383
+ form ``A*x=b`` in the linearization process. If a string is
384
+ supplied, it should be a valid method that can be used with the
385
+ :meth:`sympy.matrices.matrixbase.MatrixBase.solve`. If a callable is
386
+ supplied, it should have the format ``x = f(A, b)``, where it
387
+ solves the equations and returns the solution. The default is
388
+ ``'LU'`` which corresponds to SymPy's ``A.LUsolve(b)``.
389
+ ``LUsolve()`` is fast to compute but will often result in
390
+ divide-by-zero and thus ``nan`` results.
391
+ **kwargs
392
+ Extra keyword arguments are passed to
393
+ :meth:`sympy.physics.mechanics.linearize.Linearizer.linearize`.
394
+
395
+ Explanation
396
+ ===========
397
+
398
+ If kwarg A_and_B is False (default), returns M, A, B, r for the
399
+ linearized form, M*[q', u']^T = A*[q_ind, u_ind]^T + B*r.
400
+
401
+ If kwarg A_and_B is True, returns A, B, r for the linearized form
402
+ dx = A*x + B*r, where x = [q_ind, u_ind]^T. Note that this is
403
+ computationally intensive if there are many symbolic parameters. For
404
+ this reason, it may be more desirable to use the default A_and_B=False,
405
+ returning M, A, and B. Values may then be substituted in to these
406
+ matrices, and the state space form found as
407
+ A = P.T*M.inv()*A, B = P.T*M.inv()*B, where P = Linearizer.perm_mat.
408
+
409
+ In both cases, r is found as all dynamicsymbols in the equations of
410
+ motion that are not part of q, u, q', or u'. They are sorted in
411
+ canonical form.
412
+
413
+ The operating points may be also entered using the ``op_point`` kwarg.
414
+ This takes a dictionary of {symbol: value}, or a an iterable of such
415
+ dictionaries. The values may be numeric or symbolic. The more values
416
+ you can specify beforehand, the faster this computation will run.
417
+
418
+ For more documentation, please see the ``Linearizer`` class."""
419
+
420
+ linearizer = self.to_linearizer(q_ind, qd_ind, q_dep, qd_dep,
421
+ linear_solver=linear_solver)
422
+ result = linearizer.linearize(**kwargs)
423
+ return result + (linearizer.r,)
424
+
425
+ def solve_multipliers(self, op_point=None, sol_type='dict'):
426
+ """Solves for the values of the lagrange multipliers symbolically at
427
+ the specified operating point.
428
+
429
+ Parameters
430
+ ==========
431
+
432
+ op_point : dict or iterable of dicts, optional
433
+ Point at which to solve at. The operating point is specified as
434
+ a dictionary or iterable of dictionaries of {symbol: value}. The
435
+ value may be numeric or symbolic itself.
436
+
437
+ sol_type : str, optional
438
+ Solution return type. Valid options are:
439
+ - 'dict': A dict of {symbol : value} (default)
440
+ - 'Matrix': An ordered column matrix of the solution
441
+ """
442
+
443
+ # Determine number of multipliers
444
+ k = len(self.lam_vec)
445
+ if k == 0:
446
+ raise ValueError("System has no lagrange multipliers to solve for.")
447
+ # Compose dict of operating conditions
448
+ if isinstance(op_point, dict):
449
+ op_point_dict = op_point
450
+ elif iterable(op_point):
451
+ op_point_dict = {}
452
+ for op in op_point:
453
+ op_point_dict.update(op)
454
+ elif op_point is None:
455
+ op_point_dict = {}
456
+ else:
457
+ raise TypeError("op_point must be either a dictionary or an "
458
+ "iterable of dictionaries.")
459
+ # Compose the system to be solved
460
+ mass_matrix = self.mass_matrix.col_join(-self.lam_coeffs.row_join(
461
+ zeros(k, k)))
462
+ force_matrix = self.forcing.col_join(self._f_cd)
463
+ # Sub in the operating point
464
+ mass_matrix = msubs(mass_matrix, op_point_dict)
465
+ force_matrix = msubs(force_matrix, op_point_dict)
466
+ # Solve for the multipliers
467
+ sol_list = mass_matrix.LUsolve(-force_matrix)[-k:]
468
+ if sol_type == 'dict':
469
+ return dict(zip(self.lam_vec, sol_list))
470
+ elif sol_type == 'Matrix':
471
+ return Matrix(sol_list)
472
+ else:
473
+ raise ValueError("Unknown sol_type {:}.".format(sol_type))
474
+
475
+ def rhs(self, inv_method=None, **kwargs):
476
+ """Returns equations that can be solved numerically.
477
+
478
+ Parameters
479
+ ==========
480
+
481
+ inv_method : str
482
+ The specific sympy inverse matrix calculation method to use. For a
483
+ list of valid methods, see
484
+ :meth:`~sympy.matrices.matrixbase.MatrixBase.inv`
485
+ """
486
+
487
+ if inv_method is None:
488
+ self._rhs = self.mass_matrix_full.LUsolve(self.forcing_full)
489
+ else:
490
+ self._rhs = (self.mass_matrix_full.inv(inv_method,
491
+ try_block_diag=True) * self.forcing_full)
492
+ return self._rhs
493
+
494
+ @property
495
+ def q(self):
496
+ return self._q
497
+
498
+ @property
499
+ def u(self):
500
+ return self._qdots
501
+
502
+ @property
503
+ def bodies(self):
504
+ return self._bodies
505
+
506
+ @property
507
+ def forcelist(self):
508
+ return self._forcelist
509
+
510
+ @property
511
+ def loads(self):
512
+ return self._forcelist
lib/python3.12/site-packages/sympy/physics/mechanics/loads.py ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from abc import ABC
2
+ from collections import namedtuple
3
+ from sympy.physics.mechanics.body_base import BodyBase
4
+ from sympy.physics.vector import Vector, ReferenceFrame, Point
5
+
6
+ __all__ = ['LoadBase', 'Force', 'Torque']
7
+
8
+
9
+ class LoadBase(ABC, namedtuple('LoadBase', ['location', 'vector'])):
10
+ """Abstract base class for the various loading types."""
11
+
12
+ def __add__(self, other):
13
+ raise TypeError(f"unsupported operand type(s) for +: "
14
+ f"'{self.__class__.__name__}' and "
15
+ f"'{other.__class__.__name__}'")
16
+
17
+ def __mul__(self, other):
18
+ raise TypeError(f"unsupported operand type(s) for *: "
19
+ f"'{self.__class__.__name__}' and "
20
+ f"'{other.__class__.__name__}'")
21
+
22
+ __radd__ = __add__
23
+ __rmul__ = __mul__
24
+
25
+
26
+ class Force(LoadBase):
27
+ """Force acting upon a point.
28
+
29
+ Explanation
30
+ ===========
31
+
32
+ A force is a vector that is bound to a line of action. This class stores
33
+ both a point, which lies on the line of action, and the vector. A tuple can
34
+ also be used, with the location as the first entry and the vector as second
35
+ entry.
36
+
37
+ Examples
38
+ ========
39
+
40
+ A force of magnitude 2 along N.x acting on a point Po can be created as
41
+ follows:
42
+
43
+ >>> from sympy.physics.mechanics import Point, ReferenceFrame, Force
44
+ >>> N = ReferenceFrame('N')
45
+ >>> Po = Point('Po')
46
+ >>> Force(Po, 2 * N.x)
47
+ (Po, 2*N.x)
48
+
49
+ If a body is supplied, then the center of mass of that body is used.
50
+
51
+ >>> from sympy.physics.mechanics import Particle
52
+ >>> P = Particle('P', point=Po)
53
+ >>> Force(P, 2 * N.x)
54
+ (Po, 2*N.x)
55
+
56
+ """
57
+
58
+ def __new__(cls, point, force):
59
+ if isinstance(point, BodyBase):
60
+ point = point.masscenter
61
+ if not isinstance(point, Point):
62
+ raise TypeError('Force location should be a Point.')
63
+ if not isinstance(force, Vector):
64
+ raise TypeError('Force vector should be a Vector.')
65
+ return super().__new__(cls, point, force)
66
+
67
+ def __repr__(self):
68
+ return (f'{self.__class__.__name__}(point={self.point}, '
69
+ f'force={self.force})')
70
+
71
+ @property
72
+ def point(self):
73
+ return self.location
74
+
75
+ @property
76
+ def force(self):
77
+ return self.vector
78
+
79
+
80
+ class Torque(LoadBase):
81
+ """Torque acting upon a frame.
82
+
83
+ Explanation
84
+ ===========
85
+
86
+ A torque is a free vector that is acting on a reference frame, which is
87
+ associated with a rigid body. This class stores both the frame and the
88
+ vector. A tuple can also be used, with the location as the first item and
89
+ the vector as second item.
90
+
91
+ Examples
92
+ ========
93
+
94
+ A torque of magnitude 2 about N.x acting on a frame N can be created as
95
+ follows:
96
+
97
+ >>> from sympy.physics.mechanics import ReferenceFrame, Torque
98
+ >>> N = ReferenceFrame('N')
99
+ >>> Torque(N, 2 * N.x)
100
+ (N, 2*N.x)
101
+
102
+ If a body is supplied, then the frame fixed to that body is used.
103
+
104
+ >>> from sympy.physics.mechanics import RigidBody
105
+ >>> rb = RigidBody('rb', frame=N)
106
+ >>> Torque(rb, 2 * N.x)
107
+ (N, 2*N.x)
108
+
109
+ """
110
+
111
+ def __new__(cls, frame, torque):
112
+ if isinstance(frame, BodyBase):
113
+ frame = frame.frame
114
+ if not isinstance(frame, ReferenceFrame):
115
+ raise TypeError('Torque location should be a ReferenceFrame.')
116
+ if not isinstance(torque, Vector):
117
+ raise TypeError('Torque vector should be a Vector.')
118
+ return super().__new__(cls, frame, torque)
119
+
120
+ def __repr__(self):
121
+ return (f'{self.__class__.__name__}(frame={self.frame}, '
122
+ f'torque={self.torque})')
123
+
124
+ @property
125
+ def frame(self):
126
+ return self.location
127
+
128
+ @property
129
+ def torque(self):
130
+ return self.vector
131
+
132
+
133
+ def gravity(acceleration, *bodies):
134
+ """
135
+ Returns a list of gravity forces given the acceleration
136
+ due to gravity and any number of particles or rigidbodies.
137
+
138
+ Example
139
+ =======
140
+
141
+ >>> from sympy.physics.mechanics import ReferenceFrame, Particle, RigidBody
142
+ >>> from sympy.physics.mechanics.loads import gravity
143
+ >>> from sympy import symbols
144
+ >>> N = ReferenceFrame('N')
145
+ >>> g = symbols('g')
146
+ >>> P = Particle('P')
147
+ >>> B = RigidBody('B')
148
+ >>> gravity(g*N.y, P, B)
149
+ [(P_masscenter, P_mass*g*N.y),
150
+ (B_masscenter, B_mass*g*N.y)]
151
+
152
+ """
153
+
154
+ gravity_force = []
155
+ for body in bodies:
156
+ if not isinstance(body, BodyBase):
157
+ raise TypeError(f'{type(body)} is not a body type')
158
+ gravity_force.append(Force(body.masscenter, body.mass * acceleration))
159
+ return gravity_force
160
+
161
+
162
+ def _parse_load(load):
163
+ """Helper function to parse loads and convert tuples to load objects."""
164
+ if isinstance(load, LoadBase):
165
+ return load
166
+ elif isinstance(load, tuple):
167
+ if len(load) != 2:
168
+ raise ValueError(f'Load {load} should have a length of 2.')
169
+ if isinstance(load[0], Point):
170
+ return Force(load[0], load[1])
171
+ elif isinstance(load[0], ReferenceFrame):
172
+ return Torque(load[0], load[1])
173
+ else:
174
+ raise ValueError(f'Load not recognized. The load location {load[0]}'
175
+ f' should either be a Point or a ReferenceFrame.')
176
+ raise TypeError(f'Load type {type(load)} not recognized as a load. It '
177
+ f'should be a Force, Torque or tuple.')
lib/python3.12/site-packages/sympy/physics/mechanics/method.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from abc import ABC, abstractmethod
2
+
3
+ class _Methods(ABC):
4
+ """Abstract Base Class for all methods."""
5
+
6
+ @abstractmethod
7
+ def q(self):
8
+ pass
9
+
10
+ @abstractmethod
11
+ def u(self):
12
+ pass
13
+
14
+ @abstractmethod
15
+ def bodies(self):
16
+ pass
17
+
18
+ @abstractmethod
19
+ def loads(self):
20
+ pass
21
+
22
+ @abstractmethod
23
+ def mass_matrix(self):
24
+ pass
25
+
26
+ @abstractmethod
27
+ def forcing(self):
28
+ pass
29
+
30
+ @abstractmethod
31
+ def mass_matrix_full(self):
32
+ pass
33
+
34
+ @abstractmethod
35
+ def forcing_full(self):
36
+ pass
37
+
38
+ def _form_eoms(self):
39
+ raise NotImplementedError("Subclasses must implement this.")
lib/python3.12/site-packages/sympy/physics/mechanics/pathway.py ADDED
@@ -0,0 +1,688 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Implementations of pathways for use by actuators."""
2
+
3
+ from abc import ABC, abstractmethod
4
+
5
+ from sympy.core.singleton import S
6
+ from sympy.physics.mechanics.loads import Force
7
+ from sympy.physics.mechanics.wrapping_geometry import WrappingGeometryBase
8
+ from sympy.physics.vector import Point, dynamicsymbols
9
+
10
+
11
+ __all__ = ['PathwayBase', 'LinearPathway', 'ObstacleSetPathway',
12
+ 'WrappingPathway']
13
+
14
+
15
+ class PathwayBase(ABC):
16
+ """Abstract base class for all pathway classes to inherit from.
17
+
18
+ Notes
19
+ =====
20
+
21
+ Instances of this class cannot be directly instantiated by users. However,
22
+ it can be used to created custom pathway types through subclassing.
23
+
24
+ """
25
+
26
+ def __init__(self, *attachments):
27
+ """Initializer for ``PathwayBase``."""
28
+ self.attachments = attachments
29
+
30
+ @property
31
+ def attachments(self):
32
+ """The pair of points defining a pathway's ends."""
33
+ return self._attachments
34
+
35
+ @attachments.setter
36
+ def attachments(self, attachments):
37
+ if hasattr(self, '_attachments'):
38
+ msg = (
39
+ f'Can\'t set attribute `attachments` to {repr(attachments)} '
40
+ f'as it is immutable.'
41
+ )
42
+ raise AttributeError(msg)
43
+ if len(attachments) != 2:
44
+ msg = (
45
+ f'Value {repr(attachments)} passed to `attachments` was an '
46
+ f'iterable of length {len(attachments)}, must be an iterable '
47
+ f'of length 2.'
48
+ )
49
+ raise ValueError(msg)
50
+ for i, point in enumerate(attachments):
51
+ if not isinstance(point, Point):
52
+ msg = (
53
+ f'Value {repr(point)} passed to `attachments` at index '
54
+ f'{i} was of type {type(point)}, must be {Point}.'
55
+ )
56
+ raise TypeError(msg)
57
+ self._attachments = tuple(attachments)
58
+
59
+ @property
60
+ @abstractmethod
61
+ def length(self):
62
+ """An expression representing the pathway's length."""
63
+ pass
64
+
65
+ @property
66
+ @abstractmethod
67
+ def extension_velocity(self):
68
+ """An expression representing the pathway's extension velocity."""
69
+ pass
70
+
71
+ @abstractmethod
72
+ def to_loads(self, force):
73
+ """Loads required by the equations of motion method classes.
74
+
75
+ Explanation
76
+ ===========
77
+
78
+ ``KanesMethod`` requires a list of ``Point``-``Vector`` tuples to be
79
+ passed to the ``loads`` parameters of its ``kanes_equations`` method
80
+ when constructing the equations of motion. This method acts as a
81
+ utility to produce the correctly-structred pairs of points and vectors
82
+ required so that these can be easily concatenated with other items in
83
+ the list of loads and passed to ``KanesMethod.kanes_equations``. These
84
+ loads are also in the correct form to also be passed to the other
85
+ equations of motion method classes, e.g. ``LagrangesMethod``.
86
+
87
+ """
88
+ pass
89
+
90
+ def __repr__(self):
91
+ """Default representation of a pathway."""
92
+ attachments = ', '.join(str(a) for a in self.attachments)
93
+ return f'{self.__class__.__name__}({attachments})'
94
+
95
+
96
+ class LinearPathway(PathwayBase):
97
+ """Linear pathway between a pair of attachment points.
98
+
99
+ Explanation
100
+ ===========
101
+
102
+ A linear pathway forms a straight-line segment between two points and is
103
+ the simplest pathway that can be formed. It will not interact with any
104
+ other objects in the system, i.e. a ``LinearPathway`` will intersect other
105
+ objects to ensure that the path between its two ends (its attachments) is
106
+ the shortest possible.
107
+
108
+ A linear pathway is made up of two points that can move relative to each
109
+ other, and a pair of equal and opposite forces acting on the points. If the
110
+ positive time-varying Euclidean distance between the two points is defined,
111
+ then the "extension velocity" is the time derivative of this distance. The
112
+ extension velocity is positive when the two points are moving away from
113
+ each other and negative when moving closer to each other. The direction for
114
+ the force acting on either point is determined by constructing a unit
115
+ vector directed from the other point to this point. This establishes a sign
116
+ convention such that a positive force magnitude tends to push the points
117
+ apart. The following diagram shows the positive force sense and the
118
+ distance between the points::
119
+
120
+ P Q
121
+ o<--- F --->o
122
+ | |
123
+ |<--l(t)--->|
124
+
125
+ Examples
126
+ ========
127
+
128
+ >>> from sympy.physics.mechanics import LinearPathway
129
+
130
+ To construct a pathway, two points are required to be passed to the
131
+ ``attachments`` parameter as a ``tuple``.
132
+
133
+ >>> from sympy.physics.mechanics import Point
134
+ >>> pA, pB = Point('pA'), Point('pB')
135
+ >>> linear_pathway = LinearPathway(pA, pB)
136
+ >>> linear_pathway
137
+ LinearPathway(pA, pB)
138
+
139
+ The pathway created above isn't very interesting without the positions and
140
+ velocities of its attachment points being described. Without this its not
141
+ possible to describe how the pathway moves, i.e. its length or its
142
+ extension velocity.
143
+
144
+ >>> from sympy.physics.mechanics import ReferenceFrame
145
+ >>> from sympy.physics.vector import dynamicsymbols
146
+ >>> N = ReferenceFrame('N')
147
+ >>> q = dynamicsymbols('q')
148
+ >>> pB.set_pos(pA, q*N.x)
149
+ >>> pB.pos_from(pA)
150
+ q(t)*N.x
151
+
152
+ A pathway's length can be accessed via its ``length`` attribute.
153
+
154
+ >>> linear_pathway.length
155
+ sqrt(q(t)**2)
156
+
157
+ Note how what appears to be an overly-complex expression is returned. This
158
+ is actually required as it ensures that a pathway's length is always
159
+ positive.
160
+
161
+ A pathway's extension velocity can be accessed similarly via its
162
+ ``extension_velocity`` attribute.
163
+
164
+ >>> linear_pathway.extension_velocity
165
+ sqrt(q(t)**2)*Derivative(q(t), t)/q(t)
166
+
167
+ Parameters
168
+ ==========
169
+
170
+ attachments : tuple[Point, Point]
171
+ Pair of ``Point`` objects between which the linear pathway spans.
172
+ Constructor expects two points to be passed, e.g.
173
+ ``LinearPathway(Point('pA'), Point('pB'))``. More or fewer points will
174
+ cause an error to be thrown.
175
+
176
+ """
177
+
178
+ def __init__(self, *attachments):
179
+ """Initializer for ``LinearPathway``.
180
+
181
+ Parameters
182
+ ==========
183
+
184
+ attachments : Point
185
+ Pair of ``Point`` objects between which the linear pathway spans.
186
+ Constructor expects two points to be passed, e.g.
187
+ ``LinearPathway(Point('pA'), Point('pB'))``. More or fewer points
188
+ will cause an error to be thrown.
189
+
190
+ """
191
+ super().__init__(*attachments)
192
+
193
+ @property
194
+ def length(self):
195
+ """Exact analytical expression for the pathway's length."""
196
+ return _point_pair_length(*self.attachments)
197
+
198
+ @property
199
+ def extension_velocity(self):
200
+ """Exact analytical expression for the pathway's extension velocity."""
201
+ return _point_pair_extension_velocity(*self.attachments)
202
+
203
+ def to_loads(self, force):
204
+ """Loads required by the equations of motion method classes.
205
+
206
+ Explanation
207
+ ===========
208
+
209
+ ``KanesMethod`` requires a list of ``Point``-``Vector`` tuples to be
210
+ passed to the ``loads`` parameters of its ``kanes_equations`` method
211
+ when constructing the equations of motion. This method acts as a
212
+ utility to produce the correctly-structred pairs of points and vectors
213
+ required so that these can be easily concatenated with other items in
214
+ the list of loads and passed to ``KanesMethod.kanes_equations``. These
215
+ loads are also in the correct form to also be passed to the other
216
+ equations of motion method classes, e.g. ``LagrangesMethod``.
217
+
218
+ Examples
219
+ ========
220
+
221
+ The below example shows how to generate the loads produced in a linear
222
+ actuator that produces an expansile force ``F``. First, create a linear
223
+ actuator between two points separated by the coordinate ``q`` in the
224
+ ``x`` direction of the global frame ``N``.
225
+
226
+ >>> from sympy.physics.mechanics import (LinearPathway, Point,
227
+ ... ReferenceFrame)
228
+ >>> from sympy.physics.vector import dynamicsymbols
229
+ >>> q = dynamicsymbols('q')
230
+ >>> N = ReferenceFrame('N')
231
+ >>> pA, pB = Point('pA'), Point('pB')
232
+ >>> pB.set_pos(pA, q*N.x)
233
+ >>> linear_pathway = LinearPathway(pA, pB)
234
+
235
+ Now create a symbol ``F`` to describe the magnitude of the (expansile)
236
+ force that will be produced along the pathway. The list of loads that
237
+ ``KanesMethod`` requires can be produced by calling the pathway's
238
+ ``to_loads`` method with ``F`` passed as the only argument.
239
+
240
+ >>> from sympy import symbols
241
+ >>> F = symbols('F')
242
+ >>> linear_pathway.to_loads(F)
243
+ [(pA, - F*q(t)/sqrt(q(t)**2)*N.x), (pB, F*q(t)/sqrt(q(t)**2)*N.x)]
244
+
245
+ Parameters
246
+ ==========
247
+
248
+ force : Expr
249
+ Magnitude of the force acting along the length of the pathway. As
250
+ per the sign conventions for the pathway length, pathway extension
251
+ velocity, and pair of point forces, if this ``Expr`` is positive
252
+ then the force will act to push the pair of points away from one
253
+ another (it is expansile).
254
+
255
+ """
256
+ relative_position = _point_pair_relative_position(*self.attachments)
257
+ loads = [
258
+ Force(self.attachments[0], -force*relative_position/self.length),
259
+ Force(self.attachments[-1], force*relative_position/self.length),
260
+ ]
261
+ return loads
262
+
263
+
264
+ class ObstacleSetPathway(PathwayBase):
265
+ """Obstacle-set pathway between a set of attachment points.
266
+
267
+ Explanation
268
+ ===========
269
+
270
+ An obstacle-set pathway forms a series of straight-line segment between
271
+ pairs of consecutive points in a set of points. It is similiar to multiple
272
+ linear pathways joined end-to-end. It will not interact with any other
273
+ objects in the system, i.e. an ``ObstacleSetPathway`` will intersect other
274
+ objects to ensure that the path between its pairs of points (its
275
+ attachments) is the shortest possible.
276
+
277
+ Examples
278
+ ========
279
+
280
+ To construct an obstacle-set pathway, three or more points are required to
281
+ be passed to the ``attachments`` parameter as a ``tuple``.
282
+
283
+ >>> from sympy.physics.mechanics import ObstacleSetPathway, Point
284
+ >>> pA, pB, pC, pD = Point('pA'), Point('pB'), Point('pC'), Point('pD')
285
+ >>> obstacle_set_pathway = ObstacleSetPathway(pA, pB, pC, pD)
286
+ >>> obstacle_set_pathway
287
+ ObstacleSetPathway(pA, pB, pC, pD)
288
+
289
+ The pathway created above isn't very interesting without the positions and
290
+ velocities of its attachment points being described. Without this its not
291
+ possible to describe how the pathway moves, i.e. its length or its
292
+ extension velocity.
293
+
294
+ >>> from sympy import cos, sin
295
+ >>> from sympy.physics.mechanics import ReferenceFrame
296
+ >>> from sympy.physics.vector import dynamicsymbols
297
+ >>> N = ReferenceFrame('N')
298
+ >>> q = dynamicsymbols('q')
299
+ >>> pO = Point('pO')
300
+ >>> pA.set_pos(pO, N.y)
301
+ >>> pB.set_pos(pO, -N.x)
302
+ >>> pC.set_pos(pA, cos(q) * N.x - (sin(q) + 1) * N.y)
303
+ >>> pD.set_pos(pA, sin(q) * N.x + (cos(q) - 1) * N.y)
304
+ >>> pB.pos_from(pA)
305
+ - N.x - N.y
306
+ >>> pC.pos_from(pA)
307
+ cos(q(t))*N.x + (-sin(q(t)) - 1)*N.y
308
+ >>> pD.pos_from(pA)
309
+ sin(q(t))*N.x + (cos(q(t)) - 1)*N.y
310
+
311
+ A pathway's length can be accessed via its ``length`` attribute.
312
+
313
+ >>> obstacle_set_pathway.length.simplify()
314
+ sqrt(2)*(sqrt(cos(q(t)) + 1) + 2)
315
+
316
+ A pathway's extension velocity can be accessed similarly via its
317
+ ``extension_velocity`` attribute.
318
+
319
+ >>> obstacle_set_pathway.extension_velocity.simplify()
320
+ -sqrt(2)*sin(q(t))*Derivative(q(t), t)/(2*sqrt(cos(q(t)) + 1))
321
+
322
+ Parameters
323
+ ==========
324
+
325
+ attachments : tuple[Point, Point]
326
+ The set of ``Point`` objects that define the segmented obstacle-set
327
+ pathway.
328
+
329
+ """
330
+
331
+ def __init__(self, *attachments):
332
+ """Initializer for ``ObstacleSetPathway``.
333
+
334
+ Parameters
335
+ ==========
336
+
337
+ attachments : tuple[Point, ...]
338
+ The set of ``Point`` objects that define the segmented obstacle-set
339
+ pathway.
340
+
341
+ """
342
+ super().__init__(*attachments)
343
+
344
+ @property
345
+ def attachments(self):
346
+ """The set of points defining a pathway's segmented path."""
347
+ return self._attachments
348
+
349
+ @attachments.setter
350
+ def attachments(self, attachments):
351
+ if hasattr(self, '_attachments'):
352
+ msg = (
353
+ f'Can\'t set attribute `attachments` to {repr(attachments)} '
354
+ f'as it is immutable.'
355
+ )
356
+ raise AttributeError(msg)
357
+ if len(attachments) <= 2:
358
+ msg = (
359
+ f'Value {repr(attachments)} passed to `attachments` was an '
360
+ f'iterable of length {len(attachments)}, must be an iterable '
361
+ f'of length 3 or greater.'
362
+ )
363
+ raise ValueError(msg)
364
+ for i, point in enumerate(attachments):
365
+ if not isinstance(point, Point):
366
+ msg = (
367
+ f'Value {repr(point)} passed to `attachments` at index '
368
+ f'{i} was of type {type(point)}, must be {Point}.'
369
+ )
370
+ raise TypeError(msg)
371
+ self._attachments = tuple(attachments)
372
+
373
+ @property
374
+ def length(self):
375
+ """Exact analytical expression for the pathway's length."""
376
+ length = S.Zero
377
+ attachment_pairs = zip(self.attachments[:-1], self.attachments[1:])
378
+ for attachment_pair in attachment_pairs:
379
+ length += _point_pair_length(*attachment_pair)
380
+ return length
381
+
382
+ @property
383
+ def extension_velocity(self):
384
+ """Exact analytical expression for the pathway's extension velocity."""
385
+ extension_velocity = S.Zero
386
+ attachment_pairs = zip(self.attachments[:-1], self.attachments[1:])
387
+ for attachment_pair in attachment_pairs:
388
+ extension_velocity += _point_pair_extension_velocity(*attachment_pair)
389
+ return extension_velocity
390
+
391
+ def to_loads(self, force):
392
+ """Loads required by the equations of motion method classes.
393
+
394
+ Explanation
395
+ ===========
396
+
397
+ ``KanesMethod`` requires a list of ``Point``-``Vector`` tuples to be
398
+ passed to the ``loads`` parameters of its ``kanes_equations`` method
399
+ when constructing the equations of motion. This method acts as a
400
+ utility to produce the correctly-structred pairs of points and vectors
401
+ required so that these can be easily concatenated with other items in
402
+ the list of loads and passed to ``KanesMethod.kanes_equations``. These
403
+ loads are also in the correct form to also be passed to the other
404
+ equations of motion method classes, e.g. ``LagrangesMethod``.
405
+
406
+ Examples
407
+ ========
408
+
409
+ The below example shows how to generate the loads produced in an
410
+ actuator that follows an obstacle-set pathway between four points and
411
+ produces an expansile force ``F``. First, create a pair of reference
412
+ frames, ``A`` and ``B``, in which the four points ``pA``, ``pB``,
413
+ ``pC``, and ``pD`` will be located. The first two points in frame ``A``
414
+ and the second two in frame ``B``. Frame ``B`` will also be oriented
415
+ such that it relates to ``A`` via a rotation of ``q`` about an axis
416
+ ``N.z`` in a global frame (``N.z``, ``A.z``, and ``B.z`` are parallel).
417
+
418
+ >>> from sympy.physics.mechanics import (ObstacleSetPathway, Point,
419
+ ... ReferenceFrame)
420
+ >>> from sympy.physics.vector import dynamicsymbols
421
+ >>> q = dynamicsymbols('q')
422
+ >>> N = ReferenceFrame('N')
423
+ >>> N = ReferenceFrame('N')
424
+ >>> A = N.orientnew('A', 'axis', (0, N.x))
425
+ >>> B = A.orientnew('B', 'axis', (q, N.z))
426
+ >>> pO = Point('pO')
427
+ >>> pA, pB, pC, pD = Point('pA'), Point('pB'), Point('pC'), Point('pD')
428
+ >>> pA.set_pos(pO, A.x)
429
+ >>> pB.set_pos(pO, -A.y)
430
+ >>> pC.set_pos(pO, B.y)
431
+ >>> pD.set_pos(pO, B.x)
432
+ >>> obstacle_set_pathway = ObstacleSetPathway(pA, pB, pC, pD)
433
+
434
+ Now create a symbol ``F`` to describe the magnitude of the (expansile)
435
+ force that will be produced along the pathway. The list of loads that
436
+ ``KanesMethod`` requires can be produced by calling the pathway's
437
+ ``to_loads`` method with ``F`` passed as the only argument.
438
+
439
+ >>> from sympy import Symbol
440
+ >>> F = Symbol('F')
441
+ >>> obstacle_set_pathway.to_loads(F)
442
+ [(pA, sqrt(2)*F/2*A.x + sqrt(2)*F/2*A.y),
443
+ (pB, - sqrt(2)*F/2*A.x - sqrt(2)*F/2*A.y),
444
+ (pB, - F/sqrt(2*cos(q(t)) + 2)*A.y - F/sqrt(2*cos(q(t)) + 2)*B.y),
445
+ (pC, F/sqrt(2*cos(q(t)) + 2)*A.y + F/sqrt(2*cos(q(t)) + 2)*B.y),
446
+ (pC, - sqrt(2)*F/2*B.x + sqrt(2)*F/2*B.y),
447
+ (pD, sqrt(2)*F/2*B.x - sqrt(2)*F/2*B.y)]
448
+
449
+ Parameters
450
+ ==========
451
+
452
+ force : Expr
453
+ The force acting along the length of the pathway. It is assumed
454
+ that this ``Expr`` represents an expansile force.
455
+
456
+ """
457
+ loads = []
458
+ attachment_pairs = zip(self.attachments[:-1], self.attachments[1:])
459
+ for attachment_pair in attachment_pairs:
460
+ relative_position = _point_pair_relative_position(*attachment_pair)
461
+ length = _point_pair_length(*attachment_pair)
462
+ loads.extend([
463
+ Force(attachment_pair[0], -force*relative_position/length),
464
+ Force(attachment_pair[1], force*relative_position/length),
465
+ ])
466
+ return loads
467
+
468
+
469
+ class WrappingPathway(PathwayBase):
470
+ """Pathway that wraps a geometry object.
471
+
472
+ Explanation
473
+ ===========
474
+
475
+ A wrapping pathway interacts with a geometry object and forms a path that
476
+ wraps smoothly along its surface. The wrapping pathway along the geometry
477
+ object will be the geodesic that the geometry object defines based on the
478
+ two points. It will not interact with any other objects in the system, i.e.
479
+ a ``WrappingPathway`` will intersect other objects to ensure that the path
480
+ between its two ends (its attachments) is the shortest possible.
481
+
482
+ To explain the sign conventions used for pathway length, extension
483
+ velocity, and direction of applied forces, we can ignore the geometry with
484
+ which the wrapping pathway interacts. A wrapping pathway is made up of two
485
+ points that can move relative to each other, and a pair of equal and
486
+ opposite forces acting on the points. If the positive time-varying
487
+ Euclidean distance between the two points is defined, then the "extension
488
+ velocity" is the time derivative of this distance. The extension velocity
489
+ is positive when the two points are moving away from each other and
490
+ negative when moving closer to each other. The direction for the force
491
+ acting on either point is determined by constructing a unit vector directed
492
+ from the other point to this point. This establishes a sign convention such
493
+ that a positive force magnitude tends to push the points apart. The
494
+ following diagram shows the positive force sense and the distance between
495
+ the points::
496
+
497
+ P Q
498
+ o<--- F --->o
499
+ | |
500
+ |<--l(t)--->|
501
+
502
+ Examples
503
+ ========
504
+
505
+ >>> from sympy.physics.mechanics import WrappingPathway
506
+
507
+ To construct a wrapping pathway, like other pathways, a pair of points must
508
+ be passed, followed by an instance of a wrapping geometry class as a
509
+ keyword argument. We'll use a cylinder with radius ``r`` and its axis
510
+ parallel to ``N.x`` passing through a point ``pO``.
511
+
512
+ >>> from sympy import symbols
513
+ >>> from sympy.physics.mechanics import Point, ReferenceFrame, WrappingCylinder
514
+ >>> r = symbols('r')
515
+ >>> N = ReferenceFrame('N')
516
+ >>> pA, pB, pO = Point('pA'), Point('pB'), Point('pO')
517
+ >>> cylinder = WrappingCylinder(r, pO, N.x)
518
+ >>> wrapping_pathway = WrappingPathway(pA, pB, cylinder)
519
+ >>> wrapping_pathway
520
+ WrappingPathway(pA, pB, geometry=WrappingCylinder(radius=r, point=pO,
521
+ axis=N.x))
522
+
523
+ Parameters
524
+ ==========
525
+
526
+ attachment_1 : Point
527
+ First of the pair of ``Point`` objects between which the wrapping
528
+ pathway spans.
529
+ attachment_2 : Point
530
+ Second of the pair of ``Point`` objects between which the wrapping
531
+ pathway spans.
532
+ geometry : WrappingGeometryBase
533
+ Geometry about which the pathway wraps.
534
+
535
+ """
536
+
537
+ def __init__(self, attachment_1, attachment_2, geometry):
538
+ """Initializer for ``WrappingPathway``.
539
+
540
+ Parameters
541
+ ==========
542
+
543
+ attachment_1 : Point
544
+ First of the pair of ``Point`` objects between which the wrapping
545
+ pathway spans.
546
+ attachment_2 : Point
547
+ Second of the pair of ``Point`` objects between which the wrapping
548
+ pathway spans.
549
+ geometry : WrappingGeometryBase
550
+ Geometry about which the pathway wraps.
551
+ The geometry about which the pathway wraps.
552
+
553
+ """
554
+ super().__init__(attachment_1, attachment_2)
555
+ self.geometry = geometry
556
+
557
+ @property
558
+ def geometry(self):
559
+ """Geometry around which the pathway wraps."""
560
+ return self._geometry
561
+
562
+ @geometry.setter
563
+ def geometry(self, geometry):
564
+ if hasattr(self, '_geometry'):
565
+ msg = (
566
+ f'Can\'t set attribute `geometry` to {repr(geometry)} as it '
567
+ f'is immutable.'
568
+ )
569
+ raise AttributeError(msg)
570
+ if not isinstance(geometry, WrappingGeometryBase):
571
+ msg = (
572
+ f'Value {repr(geometry)} passed to `geometry` was of type '
573
+ f'{type(geometry)}, must be {WrappingGeometryBase}.'
574
+ )
575
+ raise TypeError(msg)
576
+ self._geometry = geometry
577
+
578
+ @property
579
+ def length(self):
580
+ """Exact analytical expression for the pathway's length."""
581
+ return self.geometry.geodesic_length(*self.attachments)
582
+
583
+ @property
584
+ def extension_velocity(self):
585
+ """Exact analytical expression for the pathway's extension velocity."""
586
+ return self.length.diff(dynamicsymbols._t)
587
+
588
+ def to_loads(self, force):
589
+ """Loads required by the equations of motion method classes.
590
+
591
+ Explanation
592
+ ===========
593
+
594
+ ``KanesMethod`` requires a list of ``Point``-``Vector`` tuples to be
595
+ passed to the ``loads`` parameters of its ``kanes_equations`` method
596
+ when constructing the equations of motion. This method acts as a
597
+ utility to produce the correctly-structred pairs of points and vectors
598
+ required so that these can be easily concatenated with other items in
599
+ the list of loads and passed to ``KanesMethod.kanes_equations``. These
600
+ loads are also in the correct form to also be passed to the other
601
+ equations of motion method classes, e.g. ``LagrangesMethod``.
602
+
603
+ Examples
604
+ ========
605
+
606
+ The below example shows how to generate the loads produced in an
607
+ actuator that produces an expansile force ``F`` while wrapping around a
608
+ cylinder. First, create a cylinder with radius ``r`` and an axis
609
+ parallel to the ``N.z`` direction of the global frame ``N`` that also
610
+ passes through a point ``pO``.
611
+
612
+ >>> from sympy import symbols
613
+ >>> from sympy.physics.mechanics import (Point, ReferenceFrame,
614
+ ... WrappingCylinder)
615
+ >>> N = ReferenceFrame('N')
616
+ >>> r = symbols('r', positive=True)
617
+ >>> pO = Point('pO')
618
+ >>> cylinder = WrappingCylinder(r, pO, N.z)
619
+
620
+ Create the pathway of the actuator using the ``WrappingPathway`` class,
621
+ defined to span between two points ``pA`` and ``pB``. Both points lie
622
+ on the surface of the cylinder and the location of ``pB`` is defined
623
+ relative to ``pA`` by the dynamics symbol ``q``.
624
+
625
+ >>> from sympy import cos, sin
626
+ >>> from sympy.physics.mechanics import WrappingPathway, dynamicsymbols
627
+ >>> q = dynamicsymbols('q')
628
+ >>> pA = Point('pA')
629
+ >>> pB = Point('pB')
630
+ >>> pA.set_pos(pO, r*N.x)
631
+ >>> pB.set_pos(pO, r*(cos(q)*N.x + sin(q)*N.y))
632
+ >>> pB.pos_from(pA)
633
+ (r*cos(q(t)) - r)*N.x + r*sin(q(t))*N.y
634
+ >>> pathway = WrappingPathway(pA, pB, cylinder)
635
+
636
+ Now create a symbol ``F`` to describe the magnitude of the (expansile)
637
+ force that will be produced along the pathway. The list of loads that
638
+ ``KanesMethod`` requires can be produced by calling the pathway's
639
+ ``to_loads`` method with ``F`` passed as the only argument.
640
+
641
+ >>> F = symbols('F')
642
+ >>> loads = pathway.to_loads(F)
643
+ >>> [load.__class__(load.location, load.vector.simplify()) for load in loads]
644
+ [(pA, F*N.y), (pB, F*sin(q(t))*N.x - F*cos(q(t))*N.y),
645
+ (pO, - F*sin(q(t))*N.x + F*(cos(q(t)) - 1)*N.y)]
646
+
647
+ Parameters
648
+ ==========
649
+
650
+ force : Expr
651
+ Magnitude of the force acting along the length of the pathway. It
652
+ is assumed that this ``Expr`` represents an expansile force.
653
+
654
+ """
655
+ pA, pB = self.attachments
656
+ pO = self.geometry.point
657
+ pA_force, pB_force = self.geometry.geodesic_end_vectors(pA, pB)
658
+ pO_force = -(pA_force + pB_force)
659
+
660
+ loads = [
661
+ Force(pA, force * pA_force),
662
+ Force(pB, force * pB_force),
663
+ Force(pO, force * pO_force),
664
+ ]
665
+ return loads
666
+
667
+ def __repr__(self):
668
+ """Representation of a ``WrappingPathway``."""
669
+ attachments = ', '.join(str(a) for a in self.attachments)
670
+ return (
671
+ f'{self.__class__.__name__}({attachments}, '
672
+ f'geometry={self.geometry})'
673
+ )
674
+
675
+
676
+ def _point_pair_relative_position(point_1, point_2):
677
+ """The relative position between a pair of points."""
678
+ return point_2.pos_from(point_1)
679
+
680
+
681
+ def _point_pair_length(point_1, point_2):
682
+ """The length of the direct linear path between two points."""
683
+ return _point_pair_relative_position(point_1, point_2).magnitude()
684
+
685
+
686
+ def _point_pair_extension_velocity(point_1, point_2):
687
+ """The extension velocity of the direct linear path between two points."""
688
+ return _point_pair_length(point_1, point_2).diff(dynamicsymbols._t)
lib/python3.12/site-packages/sympy/physics/mechanics/tests/__init__.py ADDED
File without changes
lib/python3.12/site-packages/sympy/physics/mechanics/tests/__pycache__/test_inertia.cpython-312.pyc ADDED
Binary file (6.77 kB). View file
 
lib/python3.12/site-packages/sympy/physics/mechanics/tests/__pycache__/test_kane5.cpython-312.pyc ADDED
Binary file (9.71 kB). View file
 
lib/python3.12/site-packages/sympy/physics/mechanics/tests/__pycache__/test_method.cpython-312.pyc ADDED
Binary file (622 Bytes). View file
 
lib/python3.12/site-packages/sympy/physics/mechanics/tests/__pycache__/test_rigidbody.cpython-312.pyc ADDED
Binary file (12.6 kB). View file
 
lib/python3.12/site-packages/sympy/physics/mechanics/tests/__pycache__/test_system.cpython-312.pyc ADDED
Binary file (13 kB). View file
 
lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_actuator.py ADDED
@@ -0,0 +1,851 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for the ``sympy.physics.mechanics.actuator.py`` module."""
2
+
3
+ import pytest
4
+
5
+ from sympy import (
6
+ S,
7
+ Matrix,
8
+ Symbol,
9
+ SympifyError,
10
+ sqrt,
11
+ Abs
12
+ )
13
+ from sympy.physics.mechanics import (
14
+ ActuatorBase,
15
+ Force,
16
+ ForceActuator,
17
+ KanesMethod,
18
+ LinearDamper,
19
+ LinearPathway,
20
+ LinearSpring,
21
+ Particle,
22
+ PinJoint,
23
+ Point,
24
+ ReferenceFrame,
25
+ RigidBody,
26
+ TorqueActuator,
27
+ Vector,
28
+ dynamicsymbols,
29
+ DuffingSpring,
30
+ )
31
+
32
+ from sympy.core.expr import Expr as ExprType
33
+
34
+ target = RigidBody('target')
35
+ reaction = RigidBody('reaction')
36
+
37
+
38
+ class TestForceActuator:
39
+
40
+ @pytest.fixture(autouse=True)
41
+ def _linear_pathway_fixture(self):
42
+ self.force = Symbol('F')
43
+ self.pA = Point('pA')
44
+ self.pB = Point('pB')
45
+ self.pathway = LinearPathway(self.pA, self.pB)
46
+ self.q1 = dynamicsymbols('q1')
47
+ self.q2 = dynamicsymbols('q2')
48
+ self.q3 = dynamicsymbols('q3')
49
+ self.q1d = dynamicsymbols('q1', 1)
50
+ self.q2d = dynamicsymbols('q2', 1)
51
+ self.q3d = dynamicsymbols('q3', 1)
52
+ self.N = ReferenceFrame('N')
53
+
54
+ def test_is_actuator_base_subclass(self):
55
+ assert issubclass(ForceActuator, ActuatorBase)
56
+
57
+ @pytest.mark.parametrize(
58
+ 'force, expected_force',
59
+ [
60
+ (1, S.One),
61
+ (S.One, S.One),
62
+ (Symbol('F'), Symbol('F')),
63
+ (dynamicsymbols('F'), dynamicsymbols('F')),
64
+ (Symbol('F')**2 + Symbol('F'), Symbol('F')**2 + Symbol('F')),
65
+ ]
66
+ )
67
+ def test_valid_constructor_force(self, force, expected_force):
68
+ instance = ForceActuator(force, self.pathway)
69
+ assert isinstance(instance, ForceActuator)
70
+ assert hasattr(instance, 'force')
71
+ assert isinstance(instance.force, ExprType)
72
+ assert instance.force == expected_force
73
+
74
+ @pytest.mark.parametrize('force', [None, 'F'])
75
+ def test_invalid_constructor_force_not_sympifyable(self, force):
76
+ with pytest.raises(SympifyError):
77
+ _ = ForceActuator(force, self.pathway)
78
+
79
+ @pytest.mark.parametrize(
80
+ 'pathway',
81
+ [
82
+ LinearPathway(Point('pA'), Point('pB')),
83
+ ]
84
+ )
85
+ def test_valid_constructor_pathway(self, pathway):
86
+ instance = ForceActuator(self.force, pathway)
87
+ assert isinstance(instance, ForceActuator)
88
+ assert hasattr(instance, 'pathway')
89
+ assert isinstance(instance.pathway, LinearPathway)
90
+ assert instance.pathway == pathway
91
+
92
+ def test_invalid_constructor_pathway_not_pathway_base(self):
93
+ with pytest.raises(TypeError):
94
+ _ = ForceActuator(self.force, None)
95
+
96
+ @pytest.mark.parametrize(
97
+ 'property_name, fixture_attr_name',
98
+ [
99
+ ('force', 'force'),
100
+ ('pathway', 'pathway'),
101
+ ]
102
+ )
103
+ def test_properties_are_immutable(self, property_name, fixture_attr_name):
104
+ instance = ForceActuator(self.force, self.pathway)
105
+ value = getattr(self, fixture_attr_name)
106
+ with pytest.raises(AttributeError):
107
+ setattr(instance, property_name, value)
108
+
109
+ def test_repr(self):
110
+ actuator = ForceActuator(self.force, self.pathway)
111
+ expected = "ForceActuator(F, LinearPathway(pA, pB))"
112
+ assert repr(actuator) == expected
113
+
114
+ def test_to_loads_static_pathway(self):
115
+ self.pB.set_pos(self.pA, 2*self.N.x)
116
+ actuator = ForceActuator(self.force, self.pathway)
117
+ expected = [
118
+ (self.pA, - self.force*self.N.x),
119
+ (self.pB, self.force*self.N.x),
120
+ ]
121
+ assert actuator.to_loads() == expected
122
+
123
+ def test_to_loads_2D_pathway(self):
124
+ self.pB.set_pos(self.pA, 2*self.q1*self.N.x)
125
+ actuator = ForceActuator(self.force, self.pathway)
126
+ expected = [
127
+ (self.pA, - self.force*(self.q1/sqrt(self.q1**2))*self.N.x),
128
+ (self.pB, self.force*(self.q1/sqrt(self.q1**2))*self.N.x),
129
+ ]
130
+ assert actuator.to_loads() == expected
131
+
132
+ def test_to_loads_3D_pathway(self):
133
+ self.pB.set_pos(
134
+ self.pA,
135
+ self.q1*self.N.x - self.q2*self.N.y + 2*self.q3*self.N.z,
136
+ )
137
+ actuator = ForceActuator(self.force, self.pathway)
138
+ length = sqrt(self.q1**2 + self.q2**2 + 4*self.q3**2)
139
+ pO_force = (
140
+ - self.force*self.q1*self.N.x/length
141
+ + self.force*self.q2*self.N.y/length
142
+ - 2*self.force*self.q3*self.N.z/length
143
+ )
144
+ pI_force = (
145
+ self.force*self.q1*self.N.x/length
146
+ - self.force*self.q2*self.N.y/length
147
+ + 2*self.force*self.q3*self.N.z/length
148
+ )
149
+ expected = [
150
+ (self.pA, pO_force),
151
+ (self.pB, pI_force),
152
+ ]
153
+ assert actuator.to_loads() == expected
154
+
155
+
156
+ class TestLinearSpring:
157
+
158
+ @pytest.fixture(autouse=True)
159
+ def _linear_spring_fixture(self):
160
+ self.stiffness = Symbol('k')
161
+ self.l = Symbol('l')
162
+ self.pA = Point('pA')
163
+ self.pB = Point('pB')
164
+ self.pathway = LinearPathway(self.pA, self.pB)
165
+ self.q = dynamicsymbols('q')
166
+ self.N = ReferenceFrame('N')
167
+
168
+ def test_is_force_actuator_subclass(self):
169
+ assert issubclass(LinearSpring, ForceActuator)
170
+
171
+ def test_is_actuator_base_subclass(self):
172
+ assert issubclass(LinearSpring, ActuatorBase)
173
+
174
+ @pytest.mark.parametrize(
175
+ (
176
+ 'stiffness, '
177
+ 'expected_stiffness, '
178
+ 'equilibrium_length, '
179
+ 'expected_equilibrium_length, '
180
+ 'force'
181
+ ),
182
+ [
183
+ (
184
+ 1,
185
+ S.One,
186
+ 0,
187
+ S.Zero,
188
+ -sqrt(dynamicsymbols('q')**2),
189
+ ),
190
+ (
191
+ Symbol('k'),
192
+ Symbol('k'),
193
+ 0,
194
+ S.Zero,
195
+ -Symbol('k')*sqrt(dynamicsymbols('q')**2),
196
+ ),
197
+ (
198
+ Symbol('k'),
199
+ Symbol('k'),
200
+ S.Zero,
201
+ S.Zero,
202
+ -Symbol('k')*sqrt(dynamicsymbols('q')**2),
203
+ ),
204
+ (
205
+ Symbol('k'),
206
+ Symbol('k'),
207
+ Symbol('l'),
208
+ Symbol('l'),
209
+ -Symbol('k')*(sqrt(dynamicsymbols('q')**2) - Symbol('l')),
210
+ ),
211
+ ]
212
+ )
213
+ def test_valid_constructor(
214
+ self,
215
+ stiffness,
216
+ expected_stiffness,
217
+ equilibrium_length,
218
+ expected_equilibrium_length,
219
+ force,
220
+ ):
221
+ self.pB.set_pos(self.pA, self.q*self.N.x)
222
+ spring = LinearSpring(stiffness, self.pathway, equilibrium_length)
223
+
224
+ assert isinstance(spring, LinearSpring)
225
+
226
+ assert hasattr(spring, 'stiffness')
227
+ assert isinstance(spring.stiffness, ExprType)
228
+ assert spring.stiffness == expected_stiffness
229
+
230
+ assert hasattr(spring, 'pathway')
231
+ assert isinstance(spring.pathway, LinearPathway)
232
+ assert spring.pathway == self.pathway
233
+
234
+ assert hasattr(spring, 'equilibrium_length')
235
+ assert isinstance(spring.equilibrium_length, ExprType)
236
+ assert spring.equilibrium_length == expected_equilibrium_length
237
+
238
+ assert hasattr(spring, 'force')
239
+ assert isinstance(spring.force, ExprType)
240
+ assert spring.force == force
241
+
242
+ @pytest.mark.parametrize('stiffness', [None, 'k'])
243
+ def test_invalid_constructor_stiffness_not_sympifyable(self, stiffness):
244
+ with pytest.raises(SympifyError):
245
+ _ = LinearSpring(stiffness, self.pathway, self.l)
246
+
247
+ def test_invalid_constructor_pathway_not_pathway_base(self):
248
+ with pytest.raises(TypeError):
249
+ _ = LinearSpring(self.stiffness, None, self.l)
250
+
251
+ @pytest.mark.parametrize('equilibrium_length', [None, 'l'])
252
+ def test_invalid_constructor_equilibrium_length_not_sympifyable(
253
+ self,
254
+ equilibrium_length,
255
+ ):
256
+ with pytest.raises(SympifyError):
257
+ _ = LinearSpring(self.stiffness, self.pathway, equilibrium_length)
258
+
259
+ @pytest.mark.parametrize(
260
+ 'property_name, fixture_attr_name',
261
+ [
262
+ ('stiffness', 'stiffness'),
263
+ ('pathway', 'pathway'),
264
+ ('equilibrium_length', 'l'),
265
+ ]
266
+ )
267
+ def test_properties_are_immutable(self, property_name, fixture_attr_name):
268
+ spring = LinearSpring(self.stiffness, self.pathway, self.l)
269
+ value = getattr(self, fixture_attr_name)
270
+ with pytest.raises(AttributeError):
271
+ setattr(spring, property_name, value)
272
+
273
+ @pytest.mark.parametrize(
274
+ 'equilibrium_length, expected',
275
+ [
276
+ (S.Zero, 'LinearSpring(k, LinearPathway(pA, pB))'),
277
+ (
278
+ Symbol('l'),
279
+ 'LinearSpring(k, LinearPathway(pA, pB), equilibrium_length=l)',
280
+ ),
281
+ ]
282
+ )
283
+ def test_repr(self, equilibrium_length, expected):
284
+ self.pB.set_pos(self.pA, self.q*self.N.x)
285
+ spring = LinearSpring(self.stiffness, self.pathway, equilibrium_length)
286
+ assert repr(spring) == expected
287
+
288
+ def test_to_loads(self):
289
+ self.pB.set_pos(self.pA, self.q*self.N.x)
290
+ spring = LinearSpring(self.stiffness, self.pathway, self.l)
291
+ normal = self.q/sqrt(self.q**2)*self.N.x
292
+ pA_force = self.stiffness*(sqrt(self.q**2) - self.l)*normal
293
+ pB_force = -self.stiffness*(sqrt(self.q**2) - self.l)*normal
294
+ expected = [Force(self.pA, pA_force), Force(self.pB, pB_force)]
295
+ loads = spring.to_loads()
296
+
297
+ for load, (point, vector) in zip(loads, expected):
298
+ assert isinstance(load, Force)
299
+ assert load.point == point
300
+ assert (load.vector - vector).simplify() == 0
301
+
302
+
303
+ class TestLinearDamper:
304
+
305
+ @pytest.fixture(autouse=True)
306
+ def _linear_damper_fixture(self):
307
+ self.damping = Symbol('c')
308
+ self.l = Symbol('l')
309
+ self.pA = Point('pA')
310
+ self.pB = Point('pB')
311
+ self.pathway = LinearPathway(self.pA, self.pB)
312
+ self.q = dynamicsymbols('q')
313
+ self.dq = dynamicsymbols('q', 1)
314
+ self.u = dynamicsymbols('u')
315
+ self.N = ReferenceFrame('N')
316
+
317
+ def test_is_force_actuator_subclass(self):
318
+ assert issubclass(LinearDamper, ForceActuator)
319
+
320
+ def test_is_actuator_base_subclass(self):
321
+ assert issubclass(LinearDamper, ActuatorBase)
322
+
323
+ def test_valid_constructor(self):
324
+ self.pB.set_pos(self.pA, self.q*self.N.x)
325
+ damper = LinearDamper(self.damping, self.pathway)
326
+
327
+ assert isinstance(damper, LinearDamper)
328
+
329
+ assert hasattr(damper, 'damping')
330
+ assert isinstance(damper.damping, ExprType)
331
+ assert damper.damping == self.damping
332
+
333
+ assert hasattr(damper, 'pathway')
334
+ assert isinstance(damper.pathway, LinearPathway)
335
+ assert damper.pathway == self.pathway
336
+
337
+ def test_valid_constructor_force(self):
338
+ self.pB.set_pos(self.pA, self.q*self.N.x)
339
+ damper = LinearDamper(self.damping, self.pathway)
340
+
341
+ expected_force = -self.damping*sqrt(self.q**2)*self.dq/self.q
342
+ assert hasattr(damper, 'force')
343
+ assert isinstance(damper.force, ExprType)
344
+ assert damper.force == expected_force
345
+
346
+ @pytest.mark.parametrize('damping', [None, 'c'])
347
+ def test_invalid_constructor_damping_not_sympifyable(self, damping):
348
+ with pytest.raises(SympifyError):
349
+ _ = LinearDamper(damping, self.pathway)
350
+
351
+ def test_invalid_constructor_pathway_not_pathway_base(self):
352
+ with pytest.raises(TypeError):
353
+ _ = LinearDamper(self.damping, None)
354
+
355
+ @pytest.mark.parametrize(
356
+ 'property_name, fixture_attr_name',
357
+ [
358
+ ('damping', 'damping'),
359
+ ('pathway', 'pathway'),
360
+ ]
361
+ )
362
+ def test_properties_are_immutable(self, property_name, fixture_attr_name):
363
+ damper = LinearDamper(self.damping, self.pathway)
364
+ value = getattr(self, fixture_attr_name)
365
+ with pytest.raises(AttributeError):
366
+ setattr(damper, property_name, value)
367
+
368
+ def test_repr(self):
369
+ self.pB.set_pos(self.pA, self.q*self.N.x)
370
+ damper = LinearDamper(self.damping, self.pathway)
371
+ expected = 'LinearDamper(c, LinearPathway(pA, pB))'
372
+ assert repr(damper) == expected
373
+
374
+ def test_to_loads(self):
375
+ self.pB.set_pos(self.pA, self.q*self.N.x)
376
+ damper = LinearDamper(self.damping, self.pathway)
377
+ direction = self.q**2/self.q**2*self.N.x
378
+ pA_force = self.damping*self.dq*direction
379
+ pB_force = -self.damping*self.dq*direction
380
+ expected = [Force(self.pA, pA_force), Force(self.pB, pB_force)]
381
+ assert damper.to_loads() == expected
382
+
383
+
384
+ class TestForcedMassSpringDamperModel():
385
+ r"""A single degree of freedom translational forced mass-spring-damper.
386
+
387
+ Notes
388
+ =====
389
+
390
+ This system is well known to have the governing equation:
391
+
392
+ .. math::
393
+ m \ddot{x} = F - k x - c \dot{x}
394
+
395
+ where $F$ is an externally applied force, $m$ is the mass of the particle
396
+ to which the spring and damper are attached, $k$ is the spring's stiffness,
397
+ $c$ is the dampers damping coefficient, and $x$ is the generalized
398
+ coordinate representing the system's single (translational) degree of
399
+ freedom.
400
+
401
+ """
402
+
403
+ @pytest.fixture(autouse=True)
404
+ def _force_mass_spring_damper_model_fixture(self):
405
+ self.m = Symbol('m')
406
+ self.k = Symbol('k')
407
+ self.c = Symbol('c')
408
+ self.F = Symbol('F')
409
+
410
+ self.q = dynamicsymbols('q')
411
+ self.dq = dynamicsymbols('q', 1)
412
+ self.u = dynamicsymbols('u')
413
+
414
+ self.frame = ReferenceFrame('N')
415
+ self.origin = Point('pO')
416
+ self.origin.set_vel(self.frame, 0)
417
+
418
+ self.attachment = Point('pA')
419
+ self.attachment.set_pos(self.origin, self.q*self.frame.x)
420
+
421
+ self.mass = Particle('mass', self.attachment, self.m)
422
+ self.pathway = LinearPathway(self.origin, self.attachment)
423
+
424
+ self.kanes_method = KanesMethod(
425
+ self.frame,
426
+ q_ind=[self.q],
427
+ u_ind=[self.u],
428
+ kd_eqs=[self.dq - self.u],
429
+ )
430
+ self.bodies = [self.mass]
431
+
432
+ self.mass_matrix = Matrix([[self.m]])
433
+ self.forcing = Matrix([[self.F - self.c*self.u - self.k*self.q]])
434
+
435
+ def test_force_acuator(self):
436
+ stiffness = -self.k*self.pathway.length
437
+ spring = ForceActuator(stiffness, self.pathway)
438
+ damping = -self.c*self.pathway.extension_velocity
439
+ damper = ForceActuator(damping, self.pathway)
440
+
441
+ loads = [
442
+ (self.attachment, self.F*self.frame.x),
443
+ *spring.to_loads(),
444
+ *damper.to_loads(),
445
+ ]
446
+ self.kanes_method.kanes_equations(self.bodies, loads)
447
+
448
+ assert self.kanes_method.mass_matrix == self.mass_matrix
449
+ assert self.kanes_method.forcing == self.forcing
450
+
451
+ def test_linear_spring_linear_damper(self):
452
+ spring = LinearSpring(self.k, self.pathway)
453
+ damper = LinearDamper(self.c, self.pathway)
454
+
455
+ loads = [
456
+ (self.attachment, self.F*self.frame.x),
457
+ *spring.to_loads(),
458
+ *damper.to_loads(),
459
+ ]
460
+ self.kanes_method.kanes_equations(self.bodies, loads)
461
+
462
+ assert self.kanes_method.mass_matrix == self.mass_matrix
463
+ assert self.kanes_method.forcing == self.forcing
464
+
465
+
466
+ class TestTorqueActuator:
467
+
468
+ @pytest.fixture(autouse=True)
469
+ def _torque_actuator_fixture(self):
470
+ self.torque = Symbol('T')
471
+ self.N = ReferenceFrame('N')
472
+ self.A = ReferenceFrame('A')
473
+ self.axis = self.N.z
474
+ self.target = RigidBody('target', frame=self.N)
475
+ self.reaction = RigidBody('reaction', frame=self.A)
476
+
477
+ def test_is_actuator_base_subclass(self):
478
+ assert issubclass(TorqueActuator, ActuatorBase)
479
+
480
+ @pytest.mark.parametrize(
481
+ 'torque',
482
+ [
483
+ Symbol('T'),
484
+ dynamicsymbols('T'),
485
+ Symbol('T')**2 + Symbol('T'),
486
+ ]
487
+ )
488
+ @pytest.mark.parametrize(
489
+ 'target_frame, reaction_frame',
490
+ [
491
+ (target.frame, reaction.frame),
492
+ (target, reaction.frame),
493
+ (target.frame, reaction),
494
+ (target, reaction),
495
+ ]
496
+ )
497
+ def test_valid_constructor_with_reaction(
498
+ self,
499
+ torque,
500
+ target_frame,
501
+ reaction_frame,
502
+ ):
503
+ instance = TorqueActuator(
504
+ torque,
505
+ self.axis,
506
+ target_frame,
507
+ reaction_frame,
508
+ )
509
+ assert isinstance(instance, TorqueActuator)
510
+
511
+ assert hasattr(instance, 'torque')
512
+ assert isinstance(instance.torque, ExprType)
513
+ assert instance.torque == torque
514
+
515
+ assert hasattr(instance, 'axis')
516
+ assert isinstance(instance.axis, Vector)
517
+ assert instance.axis == self.axis
518
+
519
+ assert hasattr(instance, 'target_frame')
520
+ assert isinstance(instance.target_frame, ReferenceFrame)
521
+ assert instance.target_frame == target.frame
522
+
523
+ assert hasattr(instance, 'reaction_frame')
524
+ assert isinstance(instance.reaction_frame, ReferenceFrame)
525
+ assert instance.reaction_frame == reaction.frame
526
+
527
+ @pytest.mark.parametrize(
528
+ 'torque',
529
+ [
530
+ Symbol('T'),
531
+ dynamicsymbols('T'),
532
+ Symbol('T')**2 + Symbol('T'),
533
+ ]
534
+ )
535
+ @pytest.mark.parametrize('target_frame', [target.frame, target])
536
+ def test_valid_constructor_without_reaction(self, torque, target_frame):
537
+ instance = TorqueActuator(torque, self.axis, target_frame)
538
+ assert isinstance(instance, TorqueActuator)
539
+
540
+ assert hasattr(instance, 'torque')
541
+ assert isinstance(instance.torque, ExprType)
542
+ assert instance.torque == torque
543
+
544
+ assert hasattr(instance, 'axis')
545
+ assert isinstance(instance.axis, Vector)
546
+ assert instance.axis == self.axis
547
+
548
+ assert hasattr(instance, 'target_frame')
549
+ assert isinstance(instance.target_frame, ReferenceFrame)
550
+ assert instance.target_frame == target.frame
551
+
552
+ assert hasattr(instance, 'reaction_frame')
553
+ assert instance.reaction_frame is None
554
+
555
+ @pytest.mark.parametrize('torque', [None, 'T'])
556
+ def test_invalid_constructor_torque_not_sympifyable(self, torque):
557
+ with pytest.raises(SympifyError):
558
+ _ = TorqueActuator(torque, self.axis, self.target)
559
+
560
+ @pytest.mark.parametrize('axis', [Symbol('a'), dynamicsymbols('a')])
561
+ def test_invalid_constructor_axis_not_vector(self, axis):
562
+ with pytest.raises(TypeError):
563
+ _ = TorqueActuator(self.torque, axis, self.target, self.reaction)
564
+
565
+ @pytest.mark.parametrize(
566
+ 'frames',
567
+ [
568
+ (None, ReferenceFrame('child')),
569
+ (ReferenceFrame('parent'), True),
570
+ (None, RigidBody('child')),
571
+ (RigidBody('parent'), True),
572
+ ]
573
+ )
574
+ def test_invalid_constructor_frames_not_frame(self, frames):
575
+ with pytest.raises(TypeError):
576
+ _ = TorqueActuator(self.torque, self.axis, *frames)
577
+
578
+ @pytest.mark.parametrize(
579
+ 'property_name, fixture_attr_name',
580
+ [
581
+ ('torque', 'torque'),
582
+ ('axis', 'axis'),
583
+ ('target_frame', 'target'),
584
+ ('reaction_frame', 'reaction'),
585
+ ]
586
+ )
587
+ def test_properties_are_immutable(self, property_name, fixture_attr_name):
588
+ actuator = TorqueActuator(
589
+ self.torque,
590
+ self.axis,
591
+ self.target,
592
+ self.reaction,
593
+ )
594
+ value = getattr(self, fixture_attr_name)
595
+ with pytest.raises(AttributeError):
596
+ setattr(actuator, property_name, value)
597
+
598
+ def test_repr_without_reaction(self):
599
+ actuator = TorqueActuator(self.torque, self.axis, self.target)
600
+ expected = 'TorqueActuator(T, axis=N.z, target_frame=N)'
601
+ assert repr(actuator) == expected
602
+
603
+ def test_repr_with_reaction(self):
604
+ actuator = TorqueActuator(
605
+ self.torque,
606
+ self.axis,
607
+ self.target,
608
+ self.reaction,
609
+ )
610
+ expected = 'TorqueActuator(T, axis=N.z, target_frame=N, reaction_frame=A)'
611
+ assert repr(actuator) == expected
612
+
613
+ def test_at_pin_joint_constructor(self):
614
+ pin_joint = PinJoint(
615
+ 'pin',
616
+ self.target,
617
+ self.reaction,
618
+ coordinates=dynamicsymbols('q'),
619
+ speeds=dynamicsymbols('u'),
620
+ parent_interframe=self.N,
621
+ joint_axis=self.axis,
622
+ )
623
+ instance = TorqueActuator.at_pin_joint(self.torque, pin_joint)
624
+ assert isinstance(instance, TorqueActuator)
625
+
626
+ assert hasattr(instance, 'torque')
627
+ assert isinstance(instance.torque, ExprType)
628
+ assert instance.torque == self.torque
629
+
630
+ assert hasattr(instance, 'axis')
631
+ assert isinstance(instance.axis, Vector)
632
+ assert instance.axis == self.axis
633
+
634
+ assert hasattr(instance, 'target_frame')
635
+ assert isinstance(instance.target_frame, ReferenceFrame)
636
+ assert instance.target_frame == self.A
637
+
638
+ assert hasattr(instance, 'reaction_frame')
639
+ assert isinstance(instance.reaction_frame, ReferenceFrame)
640
+ assert instance.reaction_frame == self.N
641
+
642
+ def test_at_pin_joint_pin_joint_not_pin_joint_invalid(self):
643
+ with pytest.raises(TypeError):
644
+ _ = TorqueActuator.at_pin_joint(self.torque, Symbol('pin'))
645
+
646
+ def test_to_loads_without_reaction(self):
647
+ actuator = TorqueActuator(self.torque, self.axis, self.target)
648
+ expected = [
649
+ (self.N, self.torque*self.axis),
650
+ ]
651
+ assert actuator.to_loads() == expected
652
+
653
+ def test_to_loads_with_reaction(self):
654
+ actuator = TorqueActuator(
655
+ self.torque,
656
+ self.axis,
657
+ self.target,
658
+ self.reaction,
659
+ )
660
+ expected = [
661
+ (self.N, self.torque*self.axis),
662
+ (self.A, - self.torque*self.axis),
663
+ ]
664
+ assert actuator.to_loads() == expected
665
+
666
+
667
+ class NonSympifyable:
668
+ pass
669
+
670
+
671
+ class TestDuffingSpring:
672
+ @pytest.fixture(autouse=True)
673
+ # Set up common vairables that will be used in multiple tests
674
+ def _duffing_spring_fixture(self):
675
+ self.linear_stiffness = Symbol('beta')
676
+ self.nonlinear_stiffness = Symbol('alpha')
677
+ self.equilibrium_length = Symbol('l')
678
+ self.pA = Point('pA')
679
+ self.pB = Point('pB')
680
+ self.pathway = LinearPathway(self.pA, self.pB)
681
+ self.q = dynamicsymbols('q')
682
+ self.N = ReferenceFrame('N')
683
+
684
+ # Simples tests to check that DuffingSpring is a subclass of ForceActuator and ActuatorBase
685
+ def test_is_force_actuator_subclass(self):
686
+ assert issubclass(DuffingSpring, ForceActuator)
687
+
688
+ def test_is_actuator_base_subclass(self):
689
+ assert issubclass(DuffingSpring, ActuatorBase)
690
+
691
+ @pytest.mark.parametrize(
692
+ # Create parametrized tests that allows running the same test function multiple times with different sets of arguments
693
+ (
694
+ 'linear_stiffness, '
695
+ 'expected_linear_stiffness, '
696
+ 'nonlinear_stiffness, '
697
+ 'expected_nonlinear_stiffness, '
698
+ 'equilibrium_length, '
699
+ 'expected_equilibrium_length, '
700
+ 'force'
701
+ ),
702
+ [
703
+ (
704
+ 1,
705
+ S.One,
706
+ 1,
707
+ S.One,
708
+ 0,
709
+ S.Zero,
710
+ -sqrt(dynamicsymbols('q')**2)-(sqrt(dynamicsymbols('q')**2))**3,
711
+ ),
712
+ (
713
+ Symbol('beta'),
714
+ Symbol('beta'),
715
+ Symbol('alpha'),
716
+ Symbol('alpha'),
717
+ 0,
718
+ S.Zero,
719
+ -Symbol('beta')*sqrt(dynamicsymbols('q')**2)-Symbol('alpha')*(sqrt(dynamicsymbols('q')**2))**3,
720
+ ),
721
+ (
722
+ Symbol('beta'),
723
+ Symbol('beta'),
724
+ Symbol('alpha'),
725
+ Symbol('alpha'),
726
+ S.Zero,
727
+ S.Zero,
728
+ -Symbol('beta')*sqrt(dynamicsymbols('q')**2)-Symbol('alpha')*(sqrt(dynamicsymbols('q')**2))**3,
729
+ ),
730
+ (
731
+ Symbol('beta'),
732
+ Symbol('beta'),
733
+ Symbol('alpha'),
734
+ Symbol('alpha'),
735
+ Symbol('l'),
736
+ Symbol('l'),
737
+ -Symbol('beta') * (sqrt(dynamicsymbols('q')**2) - Symbol('l')) - Symbol('alpha') * (sqrt(dynamicsymbols('q')**2) - Symbol('l'))**3,
738
+ ),
739
+ ]
740
+ )
741
+
742
+ # Check if DuffingSpring correctly inializes its attributes
743
+ # It tests various combinations of linear & nonlinear stiffness, equilibriun length, and the resulting force expression
744
+ def test_valid_constructor(
745
+ self,
746
+ linear_stiffness,
747
+ expected_linear_stiffness,
748
+ nonlinear_stiffness,
749
+ expected_nonlinear_stiffness,
750
+ equilibrium_length,
751
+ expected_equilibrium_length,
752
+ force,
753
+ ):
754
+ self.pB.set_pos(self.pA, self.q*self.N.x)
755
+ spring = DuffingSpring(linear_stiffness, nonlinear_stiffness, self.pathway, equilibrium_length)
756
+
757
+ assert isinstance(spring, DuffingSpring)
758
+
759
+ assert hasattr(spring, 'linear_stiffness')
760
+ assert isinstance(spring.linear_stiffness, ExprType)
761
+ assert spring.linear_stiffness == expected_linear_stiffness
762
+
763
+ assert hasattr(spring, 'nonlinear_stiffness')
764
+ assert isinstance(spring.nonlinear_stiffness, ExprType)
765
+ assert spring.nonlinear_stiffness == expected_nonlinear_stiffness
766
+
767
+ assert hasattr(spring, 'pathway')
768
+ assert isinstance(spring.pathway, LinearPathway)
769
+ assert spring.pathway == self.pathway
770
+
771
+ assert hasattr(spring, 'equilibrium_length')
772
+ assert isinstance(spring.equilibrium_length, ExprType)
773
+ assert spring.equilibrium_length == expected_equilibrium_length
774
+
775
+ assert hasattr(spring, 'force')
776
+ assert isinstance(spring.force, ExprType)
777
+ assert spring.force == force
778
+
779
+ @pytest.mark.parametrize('linear_stiffness', [None, NonSympifyable()])
780
+ def test_invalid_constructor_linear_stiffness_not_sympifyable(self, linear_stiffness):
781
+ with pytest.raises(SympifyError):
782
+ _ = DuffingSpring(linear_stiffness, self.nonlinear_stiffness, self.pathway, self.equilibrium_length)
783
+
784
+ @pytest.mark.parametrize('nonlinear_stiffness', [None, NonSympifyable()])
785
+ def test_invalid_constructor_nonlinear_stiffness_not_sympifyable(self, nonlinear_stiffness):
786
+ with pytest.raises(SympifyError):
787
+ _ = DuffingSpring(self.linear_stiffness, nonlinear_stiffness, self.pathway, self.equilibrium_length)
788
+
789
+ def test_invalid_constructor_pathway_not_pathway_base(self):
790
+ with pytest.raises(TypeError):
791
+ _ = DuffingSpring(self.linear_stiffness, self.nonlinear_stiffness, NonSympifyable(), self.equilibrium_length)
792
+
793
+ @pytest.mark.parametrize('equilibrium_length', [None, NonSympifyable()])
794
+ def test_invalid_constructor_equilibrium_length_not_sympifyable(self, equilibrium_length):
795
+ with pytest.raises(SympifyError):
796
+ _ = DuffingSpring(self.linear_stiffness, self.nonlinear_stiffness, self.pathway, equilibrium_length)
797
+
798
+ @pytest.mark.parametrize(
799
+ 'property_name, fixture_attr_name',
800
+ [
801
+ ('linear_stiffness', 'linear_stiffness'),
802
+ ('nonlinear_stiffness', 'nonlinear_stiffness'),
803
+ ('pathway', 'pathway'),
804
+ ('equilibrium_length', 'equilibrium_length')
805
+ ]
806
+ )
807
+ # Check if certain properties of DuffingSpring object are immutable after initialization
808
+ # Ensure that once DuffingSpring is created, its key properties cannot be changed
809
+ def test_properties_are_immutable(self, property_name, fixture_attr_name):
810
+ spring = DuffingSpring(self.linear_stiffness, self.nonlinear_stiffness, self.pathway, self.equilibrium_length)
811
+ with pytest.raises(AttributeError):
812
+ setattr(spring, property_name, getattr(self, fixture_attr_name))
813
+
814
+ @pytest.mark.parametrize(
815
+ 'equilibrium_length, expected',
816
+ [
817
+ (0, 'DuffingSpring(beta, alpha, LinearPathway(pA, pB), equilibrium_length=0)'),
818
+ (Symbol('l'), 'DuffingSpring(beta, alpha, LinearPathway(pA, pB), equilibrium_length=l)'),
819
+ ]
820
+ )
821
+ # Check the __repr__ method of DuffingSpring class
822
+ # Check if the actual string representation of DuffingSpring instance matches the expected string for each provided parameter values
823
+ def test_repr(self, equilibrium_length, expected):
824
+ spring = DuffingSpring(self.linear_stiffness, self.nonlinear_stiffness, self.pathway, equilibrium_length)
825
+ assert repr(spring) == expected
826
+
827
+ def test_to_loads(self):
828
+ self.pB.set_pos(self.pA, self.q*self.N.x)
829
+ spring = DuffingSpring(self.linear_stiffness, self.nonlinear_stiffness, self.pathway, self.equilibrium_length)
830
+
831
+ # Calculate the displacement from the equilibrium length
832
+ displacement = self.q - self.equilibrium_length
833
+
834
+ # Make sure this matches the computation in DuffingSpring class
835
+ force = -self.linear_stiffness * displacement - self.nonlinear_stiffness * displacement**3
836
+
837
+ # The expected loads on pA and pB due to the spring
838
+ expected_loads = [Force(self.pA, force * self.N.x), Force(self.pB, -force * self.N.x)]
839
+
840
+ # Compare expected loads to what is returned from DuffingSpring.to_loads()
841
+ calculated_loads = spring.to_loads()
842
+ for calculated, expected in zip(calculated_loads, expected_loads):
843
+ assert calculated.point == expected.point
844
+ for dim in self.N: # Assuming self.N is the reference frame
845
+ calculated_component = calculated.vector.dot(dim)
846
+ expected_component = expected.vector.dot(dim)
847
+ # Substitute all symbols with numeric values
848
+ substitutions = {self.q: 1, Symbol('l'): 1, Symbol('alpha'): 1, Symbol('beta'): 1} # Add other necessary symbols as needed
849
+ diff = (calculated_component - expected_component).subs(substitutions).evalf()
850
+ # Check if the absolute value of the difference is below a threshold
851
+ assert Abs(diff) < 1e-9, f"The forces do not match. Difference: {diff}"
lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_body.py ADDED
@@ -0,0 +1,340 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sympy import (Symbol, symbols, sin, cos, Matrix, zeros,
2
+ simplify)
3
+ from sympy.physics.vector import Point, ReferenceFrame, dynamicsymbols, Dyadic
4
+ from sympy.physics.mechanics import inertia, Body
5
+ from sympy.testing.pytest import raises, warns_deprecated_sympy
6
+
7
+
8
+ def test_default():
9
+ with warns_deprecated_sympy():
10
+ body = Body('body')
11
+ assert body.name == 'body'
12
+ assert body.loads == []
13
+ point = Point('body_masscenter')
14
+ point.set_vel(body.frame, 0)
15
+ com = body.masscenter
16
+ frame = body.frame
17
+ assert com.vel(frame) == point.vel(frame)
18
+ assert body.mass == Symbol('body_mass')
19
+ ixx, iyy, izz = symbols('body_ixx body_iyy body_izz')
20
+ ixy, iyz, izx = symbols('body_ixy body_iyz body_izx')
21
+ assert body.inertia == (inertia(body.frame, ixx, iyy, izz, ixy, iyz, izx),
22
+ body.masscenter)
23
+
24
+
25
+ def test_custom_rigid_body():
26
+ # Body with RigidBody.
27
+ rigidbody_masscenter = Point('rigidbody_masscenter')
28
+ rigidbody_mass = Symbol('rigidbody_mass')
29
+ rigidbody_frame = ReferenceFrame('rigidbody_frame')
30
+ body_inertia = inertia(rigidbody_frame, 1, 0, 0)
31
+ with warns_deprecated_sympy():
32
+ rigid_body = Body('rigidbody_body', rigidbody_masscenter,
33
+ rigidbody_mass, rigidbody_frame, body_inertia)
34
+ com = rigid_body.masscenter
35
+ frame = rigid_body.frame
36
+ rigidbody_masscenter.set_vel(rigidbody_frame, 0)
37
+ assert com.vel(frame) == rigidbody_masscenter.vel(frame)
38
+ assert com.pos_from(com) == rigidbody_masscenter.pos_from(com)
39
+
40
+ assert rigid_body.mass == rigidbody_mass
41
+ assert rigid_body.inertia == (body_inertia, rigidbody_masscenter)
42
+
43
+ assert rigid_body.is_rigidbody
44
+
45
+ assert hasattr(rigid_body, 'masscenter')
46
+ assert hasattr(rigid_body, 'mass')
47
+ assert hasattr(rigid_body, 'frame')
48
+ assert hasattr(rigid_body, 'inertia')
49
+
50
+
51
+ def test_particle_body():
52
+ # Body with Particle
53
+ particle_masscenter = Point('particle_masscenter')
54
+ particle_mass = Symbol('particle_mass')
55
+ particle_frame = ReferenceFrame('particle_frame')
56
+ with warns_deprecated_sympy():
57
+ particle_body = Body('particle_body', particle_masscenter,
58
+ particle_mass, particle_frame)
59
+ com = particle_body.masscenter
60
+ frame = particle_body.frame
61
+ particle_masscenter.set_vel(particle_frame, 0)
62
+ assert com.vel(frame) == particle_masscenter.vel(frame)
63
+ assert com.pos_from(com) == particle_masscenter.pos_from(com)
64
+
65
+ assert particle_body.mass == particle_mass
66
+ assert not hasattr(particle_body, "_inertia")
67
+ assert hasattr(particle_body, 'frame')
68
+ assert hasattr(particle_body, 'masscenter')
69
+ assert hasattr(particle_body, 'mass')
70
+ assert particle_body.inertia == (Dyadic(0), particle_body.masscenter)
71
+ assert particle_body.central_inertia == Dyadic(0)
72
+ assert not particle_body.is_rigidbody
73
+
74
+ particle_body.central_inertia = inertia(particle_frame, 1, 1, 1)
75
+ assert particle_body.central_inertia == inertia(particle_frame, 1, 1, 1)
76
+ assert particle_body.is_rigidbody
77
+
78
+ with warns_deprecated_sympy():
79
+ particle_body = Body('particle_body', mass=particle_mass)
80
+ assert not particle_body.is_rigidbody
81
+ point = particle_body.masscenter.locatenew('point', particle_body.x)
82
+ point_inertia = particle_mass * inertia(particle_body.frame, 0, 1, 1)
83
+ particle_body.inertia = (point_inertia, point)
84
+ assert particle_body.inertia == (point_inertia, point)
85
+ assert particle_body.central_inertia == Dyadic(0)
86
+ assert particle_body.is_rigidbody
87
+
88
+
89
+ def test_particle_body_add_force():
90
+ # Body with Particle
91
+ particle_masscenter = Point('particle_masscenter')
92
+ particle_mass = Symbol('particle_mass')
93
+ particle_frame = ReferenceFrame('particle_frame')
94
+ with warns_deprecated_sympy():
95
+ particle_body = Body('particle_body', particle_masscenter,
96
+ particle_mass, particle_frame)
97
+
98
+ a = Symbol('a')
99
+ force_vector = a * particle_body.frame.x
100
+ particle_body.apply_force(force_vector, particle_body.masscenter)
101
+ assert len(particle_body.loads) == 1
102
+ point = particle_body.masscenter.locatenew(
103
+ particle_body._name + '_point0', 0)
104
+ point.set_vel(particle_body.frame, 0)
105
+ force_point = particle_body.loads[0][0]
106
+
107
+ frame = particle_body.frame
108
+ assert force_point.vel(frame) == point.vel(frame)
109
+ assert force_point.pos_from(force_point) == point.pos_from(force_point)
110
+
111
+ assert particle_body.loads[0][1] == force_vector
112
+
113
+
114
+ def test_body_add_force():
115
+ # Body with RigidBody.
116
+ rigidbody_masscenter = Point('rigidbody_masscenter')
117
+ rigidbody_mass = Symbol('rigidbody_mass')
118
+ rigidbody_frame = ReferenceFrame('rigidbody_frame')
119
+ body_inertia = inertia(rigidbody_frame, 1, 0, 0)
120
+ with warns_deprecated_sympy():
121
+ rigid_body = Body('rigidbody_body', rigidbody_masscenter,
122
+ rigidbody_mass, rigidbody_frame, body_inertia)
123
+
124
+ l = Symbol('l')
125
+ Fa = Symbol('Fa')
126
+ point = rigid_body.masscenter.locatenew(
127
+ 'rigidbody_body_point0',
128
+ l * rigid_body.frame.x)
129
+ point.set_vel(rigid_body.frame, 0)
130
+ force_vector = Fa * rigid_body.frame.z
131
+ # apply_force with point
132
+ rigid_body.apply_force(force_vector, point)
133
+ assert len(rigid_body.loads) == 1
134
+ force_point = rigid_body.loads[0][0]
135
+ frame = rigid_body.frame
136
+ assert force_point.vel(frame) == point.vel(frame)
137
+ assert force_point.pos_from(force_point) == point.pos_from(force_point)
138
+ assert rigid_body.loads[0][1] == force_vector
139
+ # apply_force without point
140
+ rigid_body.apply_force(force_vector)
141
+ assert len(rigid_body.loads) == 2
142
+ assert rigid_body.loads[1][1] == force_vector
143
+ # passing something else than point
144
+ raises(TypeError, lambda: rigid_body.apply_force(force_vector, 0))
145
+ raises(TypeError, lambda: rigid_body.apply_force(0))
146
+
147
+ def test_body_add_torque():
148
+ with warns_deprecated_sympy():
149
+ body = Body('body')
150
+ torque_vector = body.frame.x
151
+ body.apply_torque(torque_vector)
152
+
153
+ assert len(body.loads) == 1
154
+ assert body.loads[0] == (body.frame, torque_vector)
155
+ raises(TypeError, lambda: body.apply_torque(0))
156
+
157
+ def test_body_masscenter_vel():
158
+ with warns_deprecated_sympy():
159
+ A = Body('A')
160
+ N = ReferenceFrame('N')
161
+ with warns_deprecated_sympy():
162
+ B = Body('B', frame=N)
163
+ A.masscenter.set_vel(N, N.z)
164
+ assert A.masscenter_vel(B) == N.z
165
+ assert A.masscenter_vel(N) == N.z
166
+
167
+ def test_body_ang_vel():
168
+ with warns_deprecated_sympy():
169
+ A = Body('A')
170
+ N = ReferenceFrame('N')
171
+ with warns_deprecated_sympy():
172
+ B = Body('B', frame=N)
173
+ A.frame.set_ang_vel(N, N.y)
174
+ assert A.ang_vel_in(B) == N.y
175
+ assert B.ang_vel_in(A) == -N.y
176
+ assert A.ang_vel_in(N) == N.y
177
+
178
+ def test_body_dcm():
179
+ with warns_deprecated_sympy():
180
+ A = Body('A')
181
+ B = Body('B')
182
+ A.frame.orient_axis(B.frame, B.frame.z, 10)
183
+ assert A.dcm(B) == Matrix([[cos(10), sin(10), 0], [-sin(10), cos(10), 0], [0, 0, 1]])
184
+ assert A.dcm(B.frame) == Matrix([[cos(10), sin(10), 0], [-sin(10), cos(10), 0], [0, 0, 1]])
185
+
186
+ def test_body_axis():
187
+ N = ReferenceFrame('N')
188
+ with warns_deprecated_sympy():
189
+ B = Body('B', frame=N)
190
+ assert B.x == N.x
191
+ assert B.y == N.y
192
+ assert B.z == N.z
193
+
194
+ def test_apply_force_multiple_one_point():
195
+ a, b = symbols('a b')
196
+ P = Point('P')
197
+ with warns_deprecated_sympy():
198
+ B = Body('B')
199
+ f1 = a*B.x
200
+ f2 = b*B.y
201
+ B.apply_force(f1, P)
202
+ assert B.loads == [(P, f1)]
203
+ B.apply_force(f2, P)
204
+ assert B.loads == [(P, f1+f2)]
205
+
206
+ def test_apply_force():
207
+ f, g = symbols('f g')
208
+ q, x, v1, v2 = dynamicsymbols('q x v1 v2')
209
+ P1 = Point('P1')
210
+ P2 = Point('P2')
211
+ with warns_deprecated_sympy():
212
+ B1 = Body('B1')
213
+ B2 = Body('B2')
214
+ N = ReferenceFrame('N')
215
+
216
+ P1.set_vel(B1.frame, v1*B1.x)
217
+ P2.set_vel(B2.frame, v2*B2.x)
218
+ force = f*q*N.z # time varying force
219
+
220
+ B1.apply_force(force, P1, B2, P2) #applying equal and opposite force on moving points
221
+ assert B1.loads == [(P1, force)]
222
+ assert B2.loads == [(P2, -force)]
223
+
224
+ g1 = B1.mass*g*N.y
225
+ g2 = B2.mass*g*N.y
226
+
227
+ B1.apply_force(g1) #applying gravity on B1 masscenter
228
+ B2.apply_force(g2) #applying gravity on B2 masscenter
229
+
230
+ assert B1.loads == [(P1,force), (B1.masscenter, g1)]
231
+ assert B2.loads == [(P2, -force), (B2.masscenter, g2)]
232
+
233
+ force2 = x*N.x
234
+
235
+ B1.apply_force(force2, reaction_body=B2) #Applying time varying force on masscenter
236
+
237
+ assert B1.loads == [(P1, force), (B1.masscenter, force2+g1)]
238
+ assert B2.loads == [(P2, -force), (B2.masscenter, -force2+g2)]
239
+
240
+ def test_apply_torque():
241
+ t = symbols('t')
242
+ q = dynamicsymbols('q')
243
+ with warns_deprecated_sympy():
244
+ B1 = Body('B1')
245
+ B2 = Body('B2')
246
+ N = ReferenceFrame('N')
247
+ torque = t*q*N.x
248
+
249
+ B1.apply_torque(torque, B2) #Applying equal and opposite torque
250
+ assert B1.loads == [(B1.frame, torque)]
251
+ assert B2.loads == [(B2.frame, -torque)]
252
+
253
+ torque2 = t*N.y
254
+ B1.apply_torque(torque2)
255
+ assert B1.loads == [(B1.frame, torque+torque2)]
256
+
257
+ def test_clear_load():
258
+ a = symbols('a')
259
+ P = Point('P')
260
+ with warns_deprecated_sympy():
261
+ B = Body('B')
262
+ force = a*B.z
263
+ B.apply_force(force, P)
264
+ assert B.loads == [(P, force)]
265
+ B.clear_loads()
266
+ assert B.loads == []
267
+
268
+ def test_remove_load():
269
+ P1 = Point('P1')
270
+ P2 = Point('P2')
271
+ with warns_deprecated_sympy():
272
+ B = Body('B')
273
+ f1 = B.x
274
+ f2 = B.y
275
+ B.apply_force(f1, P1)
276
+ B.apply_force(f2, P2)
277
+ assert B.loads == [(P1, f1), (P2, f2)]
278
+ B.remove_load(P2)
279
+ assert B.loads == [(P1, f1)]
280
+ B.apply_torque(f1.cross(f2))
281
+ assert B.loads == [(P1, f1), (B.frame, f1.cross(f2))]
282
+ B.remove_load()
283
+ assert B.loads == [(P1, f1)]
284
+
285
+ def test_apply_loads_on_multi_degree_freedom_holonomic_system():
286
+ """Example based on: https://pydy.readthedocs.io/en/latest/examples/multidof-holonomic.html"""
287
+ with warns_deprecated_sympy():
288
+ W = Body('W') #Wall
289
+ B = Body('B') #Block
290
+ P = Body('P') #Pendulum
291
+ b = Body('b') #bob
292
+ q1, q2 = dynamicsymbols('q1 q2') #generalized coordinates
293
+ k, c, g, kT = symbols('k c g kT') #constants
294
+ F, T = dynamicsymbols('F T') #Specified forces
295
+
296
+ #Applying forces
297
+ B.apply_force(F*W.x)
298
+ W.apply_force(k*q1*W.x, reaction_body=B) #Spring force
299
+ W.apply_force(c*q1.diff()*W.x, reaction_body=B) #dampner
300
+ P.apply_force(P.mass*g*W.y)
301
+ b.apply_force(b.mass*g*W.y)
302
+
303
+ #Applying torques
304
+ P.apply_torque(kT*q2*W.z, reaction_body=b)
305
+ P.apply_torque(T*W.z)
306
+
307
+ assert B.loads == [(B.masscenter, (F - k*q1 - c*q1.diff())*W.x)]
308
+ assert P.loads == [(P.masscenter, P.mass*g*W.y), (P.frame, (T + kT*q2)*W.z)]
309
+ assert b.loads == [(b.masscenter, b.mass*g*W.y), (b.frame, -kT*q2*W.z)]
310
+ assert W.loads == [(W.masscenter, (c*q1.diff() + k*q1)*W.x)]
311
+
312
+
313
+ def test_parallel_axis():
314
+ N = ReferenceFrame('N')
315
+ m, Ix, Iy, Iz, a, b = symbols('m, I_x, I_y, I_z, a, b')
316
+ Io = inertia(N, Ix, Iy, Iz)
317
+ # Test RigidBody
318
+ o = Point('o')
319
+ p = o.locatenew('p', a * N.x + b * N.y)
320
+ with warns_deprecated_sympy():
321
+ R = Body('R', masscenter=o, frame=N, mass=m, central_inertia=Io)
322
+ Ip = R.parallel_axis(p)
323
+ Ip_expected = inertia(N, Ix + m * b**2, Iy + m * a**2,
324
+ Iz + m * (a**2 + b**2), ixy=-m * a * b)
325
+ assert Ip == Ip_expected
326
+ # Reference frame from which the parallel axis is viewed should not matter
327
+ A = ReferenceFrame('A')
328
+ A.orient_axis(N, N.z, 1)
329
+ assert simplify(
330
+ (R.parallel_axis(p, A) - Ip_expected).to_matrix(A)) == zeros(3, 3)
331
+ # Test Particle
332
+ o = Point('o')
333
+ p = o.locatenew('p', a * N.x + b * N.y)
334
+ with warns_deprecated_sympy():
335
+ P = Body('P', masscenter=o, mass=m, frame=N)
336
+ Ip = P.parallel_axis(p, N)
337
+ Ip_expected = inertia(N, m * b ** 2, m * a ** 2, m * (a ** 2 + b ** 2),
338
+ ixy=-m * a * b)
339
+ assert not P.is_rigidbody
340
+ assert Ip == Ip_expected
lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_functions.py ADDED
@@ -0,0 +1,262 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sympy import sin, cos, tan, pi, symbols, Matrix, S, Function
2
+ from sympy.physics.mechanics import (Particle, Point, ReferenceFrame,
3
+ RigidBody)
4
+ from sympy.physics.mechanics import (angular_momentum, dynamicsymbols,
5
+ kinetic_energy, linear_momentum,
6
+ outer, potential_energy, msubs,
7
+ find_dynamicsymbols, Lagrangian)
8
+
9
+ from sympy.physics.mechanics.functions import (
10
+ center_of_mass, _validate_coordinates, _parse_linear_solver)
11
+ from sympy.testing.pytest import raises, warns_deprecated_sympy
12
+
13
+
14
+ q1, q2, q3, q4, q5 = symbols('q1 q2 q3 q4 q5')
15
+ N = ReferenceFrame('N')
16
+ A = N.orientnew('A', 'Axis', [q1, N.z])
17
+ B = A.orientnew('B', 'Axis', [q2, A.x])
18
+ C = B.orientnew('C', 'Axis', [q3, B.y])
19
+
20
+
21
+ def test_linear_momentum():
22
+ N = ReferenceFrame('N')
23
+ Ac = Point('Ac')
24
+ Ac.set_vel(N, 25 * N.y)
25
+ I = outer(N.x, N.x)
26
+ A = RigidBody('A', Ac, N, 20, (I, Ac))
27
+ P = Point('P')
28
+ Pa = Particle('Pa', P, 1)
29
+ Pa.point.set_vel(N, 10 * N.x)
30
+ raises(TypeError, lambda: linear_momentum(A, A, Pa))
31
+ raises(TypeError, lambda: linear_momentum(N, N, Pa))
32
+ assert linear_momentum(N, A, Pa) == 10 * N.x + 500 * N.y
33
+
34
+
35
+ def test_angular_momentum_and_linear_momentum():
36
+ """A rod with length 2l, centroidal inertia I, and mass M along with a
37
+ particle of mass m fixed to the end of the rod rotate with an angular rate
38
+ of omega about point O which is fixed to the non-particle end of the rod.
39
+ The rod's reference frame is A and the inertial frame is N."""
40
+ m, M, l, I = symbols('m, M, l, I')
41
+ omega = dynamicsymbols('omega')
42
+ N = ReferenceFrame('N')
43
+ a = ReferenceFrame('a')
44
+ O = Point('O')
45
+ Ac = O.locatenew('Ac', l * N.x)
46
+ P = Ac.locatenew('P', l * N.x)
47
+ O.set_vel(N, 0 * N.x)
48
+ a.set_ang_vel(N, omega * N.z)
49
+ Ac.v2pt_theory(O, N, a)
50
+ P.v2pt_theory(O, N, a)
51
+ Pa = Particle('Pa', P, m)
52
+ A = RigidBody('A', Ac, a, M, (I * outer(N.z, N.z), Ac))
53
+ expected = 2 * m * omega * l * N.y + M * l * omega * N.y
54
+ assert linear_momentum(N, A, Pa) == expected
55
+ raises(TypeError, lambda: angular_momentum(N, N, A, Pa))
56
+ raises(TypeError, lambda: angular_momentum(O, O, A, Pa))
57
+ raises(TypeError, lambda: angular_momentum(O, N, O, Pa))
58
+ expected = (I + M * l**2 + 4 * m * l**2) * omega * N.z
59
+ assert angular_momentum(O, N, A, Pa) == expected
60
+
61
+
62
+ def test_kinetic_energy():
63
+ m, M, l1 = symbols('m M l1')
64
+ omega = dynamicsymbols('omega')
65
+ N = ReferenceFrame('N')
66
+ O = Point('O')
67
+ O.set_vel(N, 0 * N.x)
68
+ Ac = O.locatenew('Ac', l1 * N.x)
69
+ P = Ac.locatenew('P', l1 * N.x)
70
+ a = ReferenceFrame('a')
71
+ a.set_ang_vel(N, omega * N.z)
72
+ Ac.v2pt_theory(O, N, a)
73
+ P.v2pt_theory(O, N, a)
74
+ Pa = Particle('Pa', P, m)
75
+ I = outer(N.z, N.z)
76
+ A = RigidBody('A', Ac, a, M, (I, Ac))
77
+ raises(TypeError, lambda: kinetic_energy(Pa, Pa, A))
78
+ raises(TypeError, lambda: kinetic_energy(N, N, A))
79
+ assert 0 == (kinetic_energy(N, Pa, A) - (M*l1**2*omega**2/2
80
+ + 2*l1**2*m*omega**2 + omega**2/2)).expand()
81
+
82
+
83
+ def test_potential_energy():
84
+ m, M, l1, g, h, H = symbols('m M l1 g h H')
85
+ omega = dynamicsymbols('omega')
86
+ N = ReferenceFrame('N')
87
+ O = Point('O')
88
+ O.set_vel(N, 0 * N.x)
89
+ Ac = O.locatenew('Ac', l1 * N.x)
90
+ P = Ac.locatenew('P', l1 * N.x)
91
+ a = ReferenceFrame('a')
92
+ a.set_ang_vel(N, omega * N.z)
93
+ Ac.v2pt_theory(O, N, a)
94
+ P.v2pt_theory(O, N, a)
95
+ Pa = Particle('Pa', P, m)
96
+ I = outer(N.z, N.z)
97
+ A = RigidBody('A', Ac, a, M, (I, Ac))
98
+ Pa.potential_energy = m * g * h
99
+ A.potential_energy = M * g * H
100
+ assert potential_energy(A, Pa) == m * g * h + M * g * H
101
+
102
+
103
+ def test_Lagrangian():
104
+ M, m, g, h = symbols('M m g h')
105
+ N = ReferenceFrame('N')
106
+ O = Point('O')
107
+ O.set_vel(N, 0 * N.x)
108
+ P = O.locatenew('P', 1 * N.x)
109
+ P.set_vel(N, 10 * N.x)
110
+ Pa = Particle('Pa', P, 1)
111
+ Ac = O.locatenew('Ac', 2 * N.y)
112
+ Ac.set_vel(N, 5 * N.y)
113
+ a = ReferenceFrame('a')
114
+ a.set_ang_vel(N, 10 * N.z)
115
+ I = outer(N.z, N.z)
116
+ A = RigidBody('A', Ac, a, 20, (I, Ac))
117
+ Pa.potential_energy = m * g * h
118
+ A.potential_energy = M * g * h
119
+ raises(TypeError, lambda: Lagrangian(A, A, Pa))
120
+ raises(TypeError, lambda: Lagrangian(N, N, Pa))
121
+
122
+
123
+ def test_msubs():
124
+ a, b = symbols('a, b')
125
+ x, y, z = dynamicsymbols('x, y, z')
126
+ # Test simple substitution
127
+ expr = Matrix([[a*x + b, x*y.diff() + y],
128
+ [x.diff().diff(), z + sin(z.diff())]])
129
+ sol = Matrix([[a + b, y],
130
+ [x.diff().diff(), 1]])
131
+ sd = {x: 1, z: 1, z.diff(): 0, y.diff(): 0}
132
+ assert msubs(expr, sd) == sol
133
+ # Test smart substitution
134
+ expr = cos(x + y)*tan(x + y) + b*x.diff()
135
+ sd = {x: 0, y: pi/2, x.diff(): 1}
136
+ assert msubs(expr, sd, smart=True) == b + 1
137
+ N = ReferenceFrame('N')
138
+ v = x*N.x + y*N.y
139
+ d = x*(N.x|N.x) + y*(N.y|N.y)
140
+ v_sol = 1*N.y
141
+ d_sol = 1*(N.y|N.y)
142
+ sd = {x: 0, y: 1}
143
+ assert msubs(v, sd) == v_sol
144
+ assert msubs(d, sd) == d_sol
145
+
146
+
147
+ def test_find_dynamicsymbols():
148
+ a, b = symbols('a, b')
149
+ x, y, z = dynamicsymbols('x, y, z')
150
+ expr = Matrix([[a*x + b, x*y.diff() + y],
151
+ [x.diff().diff(), z + sin(z.diff())]])
152
+ # Test finding all dynamicsymbols
153
+ sol = {x, y.diff(), y, x.diff().diff(), z, z.diff()}
154
+ assert find_dynamicsymbols(expr) == sol
155
+ # Test finding all but those in sym_list
156
+ exclude_list = [x, y, z]
157
+ sol = {y.diff(), x.diff().diff(), z.diff()}
158
+ assert find_dynamicsymbols(expr, exclude=exclude_list) == sol
159
+ # Test finding all dynamicsymbols in a vector with a given reference frame
160
+ d, e, f = dynamicsymbols('d, e, f')
161
+ A = ReferenceFrame('A')
162
+ v = d * A.x + e * A.y + f * A.z
163
+ sol = {d, e, f}
164
+ assert find_dynamicsymbols(v, reference_frame=A) == sol
165
+ # Test if a ValueError is raised on supplying only a vector as input
166
+ raises(ValueError, lambda: find_dynamicsymbols(v))
167
+
168
+
169
+ # This function tests the center_of_mass() function
170
+ # that was added in PR #14758 to compute the center of
171
+ # mass of a system of bodies.
172
+ def test_center_of_mass():
173
+ a = ReferenceFrame('a')
174
+ m = symbols('m', real=True)
175
+ p1 = Particle('p1', Point('p1_pt'), S.One)
176
+ p2 = Particle('p2', Point('p2_pt'), S(2))
177
+ p3 = Particle('p3', Point('p3_pt'), S(3))
178
+ p4 = Particle('p4', Point('p4_pt'), m)
179
+ b_f = ReferenceFrame('b_f')
180
+ b_cm = Point('b_cm')
181
+ mb = symbols('mb')
182
+ b = RigidBody('b', b_cm, b_f, mb, (outer(b_f.x, b_f.x), b_cm))
183
+ p2.point.set_pos(p1.point, a.x)
184
+ p3.point.set_pos(p1.point, a.x + a.y)
185
+ p4.point.set_pos(p1.point, a.y)
186
+ b.masscenter.set_pos(p1.point, a.y + a.z)
187
+ point_o=Point('o')
188
+ point_o.set_pos(p1.point, center_of_mass(p1.point, p1, p2, p3, p4, b))
189
+ expr = 5/(m + mb + 6)*a.x + (m + mb + 3)/(m + mb + 6)*a.y + mb/(m + mb + 6)*a.z
190
+ assert point_o.pos_from(p1.point)-expr == 0
191
+
192
+
193
+ def test_validate_coordinates():
194
+ q1, q2, q3, u1, u2, u3, ua1, ua2, ua3 = dynamicsymbols('q1:4 u1:4 ua1:4')
195
+ s1, s2, s3 = symbols('s1:4')
196
+ # Test normal
197
+ _validate_coordinates([q1, q2, q3], [u1, u2, u3],
198
+ u_auxiliary=[ua1, ua2, ua3])
199
+ # Test not equal number of coordinates and speeds
200
+ _validate_coordinates([q1, q2])
201
+ _validate_coordinates([q1, q2], [u1])
202
+ _validate_coordinates(speeds=[u1, u2])
203
+ # Test duplicate
204
+ _validate_coordinates([q1, q2, q2], [u1, u2, u3], check_duplicates=False)
205
+ raises(ValueError, lambda: _validate_coordinates(
206
+ [q1, q2, q2], [u1, u2, u3]))
207
+ _validate_coordinates([q1, q2, q3], [u1, u2, u2], check_duplicates=False)
208
+ raises(ValueError, lambda: _validate_coordinates(
209
+ [q1, q2, q3], [u1, u2, u2], check_duplicates=True))
210
+ raises(ValueError, lambda: _validate_coordinates(
211
+ [q1, q2, q3], [q1, u2, u3], check_duplicates=True))
212
+ _validate_coordinates([q1, q2, q3], [u1, u2, u3], check_duplicates=False,
213
+ u_auxiliary=[u1, ua2, ua2])
214
+ raises(ValueError, lambda: _validate_coordinates(
215
+ [q1, q2, q3], [u1, u2, u3], u_auxiliary=[u1, ua2, ua3]))
216
+ raises(ValueError, lambda: _validate_coordinates(
217
+ [q1, q2, q3], [u1, u2, u3], u_auxiliary=[q1, ua2, ua3]))
218
+ raises(ValueError, lambda: _validate_coordinates(
219
+ [q1, q2, q3], [u1, u2, u3], u_auxiliary=[ua1, ua2, ua2]))
220
+ # Test is_dynamicsymbols
221
+ _validate_coordinates([q1 + q2, q3], is_dynamicsymbols=False)
222
+ raises(ValueError, lambda: _validate_coordinates([q1 + q2, q3]))
223
+ _validate_coordinates([s1, q1, q2], [0, u1, u2], is_dynamicsymbols=False)
224
+ raises(ValueError, lambda: _validate_coordinates(
225
+ [s1, q1, q2], [0, u1, u2], is_dynamicsymbols=True))
226
+ _validate_coordinates([s1 + s2 + s3, q1], [0, u1], is_dynamicsymbols=False)
227
+ raises(ValueError, lambda: _validate_coordinates(
228
+ [s1 + s2 + s3, q1], [0, u1], is_dynamicsymbols=True))
229
+ _validate_coordinates(u_auxiliary=[s1, ua1], is_dynamicsymbols=False)
230
+ raises(ValueError, lambda: _validate_coordinates(u_auxiliary=[s1, ua1]))
231
+ # Test normal function
232
+ t = dynamicsymbols._t
233
+ a = symbols('a')
234
+ f1, f2 = symbols('f1:3', cls=Function)
235
+ _validate_coordinates([f1(a), f2(a)], is_dynamicsymbols=False)
236
+ raises(ValueError, lambda: _validate_coordinates([f1(a), f2(a)]))
237
+ raises(ValueError, lambda: _validate_coordinates(speeds=[f1(a), f2(a)]))
238
+ dynamicsymbols._t = a
239
+ _validate_coordinates([f1(a), f2(a)])
240
+ raises(ValueError, lambda: _validate_coordinates([f1(t), f2(t)]))
241
+ dynamicsymbols._t = t
242
+
243
+
244
+ def test_parse_linear_solver():
245
+ A, b = Matrix(3, 3, symbols('a:9')), Matrix(3, 2, symbols('b:6'))
246
+ assert _parse_linear_solver(Matrix.LUsolve) == Matrix.LUsolve # Test callable
247
+ assert _parse_linear_solver('LU')(A, b) == Matrix.LUsolve(A, b)
248
+
249
+
250
+ def test_deprecated_moved_functions():
251
+ from sympy.physics.mechanics.functions import (
252
+ inertia, inertia_of_point_mass, gravity)
253
+ N = ReferenceFrame('N')
254
+ with warns_deprecated_sympy():
255
+ assert inertia(N, 0, 1, 0, 1) == (N.x | N.y) + (N.y | N.x) + (N.y | N.y)
256
+ with warns_deprecated_sympy():
257
+ assert inertia_of_point_mass(1, N.x + N.y, N) == (
258
+ (N.x | N.x) + (N.y | N.y) + 2 * (N.z | N.z) -
259
+ (N.x | N.y) - (N.y | N.x))
260
+ p = Particle('P')
261
+ with warns_deprecated_sympy():
262
+ assert gravity(-2 * N.z, p) == [(p.masscenter, -2 * p.mass * N.z)]
lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_inertia.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sympy import symbols
2
+ from sympy.testing.pytest import raises
3
+ from sympy.physics.mechanics import (inertia, inertia_of_point_mass,
4
+ Inertia, ReferenceFrame, Point)
5
+
6
+
7
+ def test_inertia_dyadic():
8
+ N = ReferenceFrame('N')
9
+ ixx, iyy, izz = symbols('ixx iyy izz')
10
+ ixy, iyz, izx = symbols('ixy iyz izx')
11
+ assert inertia(N, ixx, iyy, izz) == (ixx * (N.x | N.x) + iyy *
12
+ (N.y | N.y) + izz * (N.z | N.z))
13
+ assert inertia(N, 0, 0, 0) == 0 * (N.x | N.x)
14
+ raises(TypeError, lambda: inertia(0, 0, 0, 0))
15
+ assert inertia(N, ixx, iyy, izz, ixy, iyz, izx) == (ixx * (N.x | N.x) +
16
+ ixy * (N.x | N.y) + izx * (N.x | N.z) + ixy * (N.y | N.x) + iyy *
17
+ (N.y | N.y) + iyz * (N.y | N.z) + izx * (N.z | N.x) + iyz * (N.z |
18
+ N.y) + izz * (N.z | N.z))
19
+
20
+
21
+ def test_inertia_of_point_mass():
22
+ r, s, t, m = symbols('r s t m')
23
+ N = ReferenceFrame('N')
24
+
25
+ px = r * N.x
26
+ I = inertia_of_point_mass(m, px, N)
27
+ assert I == m * r**2 * (N.y | N.y) + m * r**2 * (N.z | N.z)
28
+
29
+ py = s * N.y
30
+ I = inertia_of_point_mass(m, py, N)
31
+ assert I == m * s**2 * (N.x | N.x) + m * s**2 * (N.z | N.z)
32
+
33
+ pz = t * N.z
34
+ I = inertia_of_point_mass(m, pz, N)
35
+ assert I == m * t**2 * (N.x | N.x) + m * t**2 * (N.y | N.y)
36
+
37
+ p = px + py + pz
38
+ I = inertia_of_point_mass(m, p, N)
39
+ assert I == (m * (s**2 + t**2) * (N.x | N.x) -
40
+ m * r * s * (N.x | N.y) -
41
+ m * r * t * (N.x | N.z) -
42
+ m * r * s * (N.y | N.x) +
43
+ m * (r**2 + t**2) * (N.y | N.y) -
44
+ m * s * t * (N.y | N.z) -
45
+ m * r * t * (N.z | N.x) -
46
+ m * s * t * (N.z | N.y) +
47
+ m * (r**2 + s**2) * (N.z | N.z))
48
+
49
+
50
+ def test_inertia_object():
51
+ N = ReferenceFrame('N')
52
+ O = Point('O')
53
+ ixx, iyy, izz = symbols('ixx iyy izz')
54
+ I_dyadic = ixx * (N.x | N.x) + iyy * (N.y | N.y) + izz * (N.z | N.z)
55
+ I = Inertia(inertia(N, ixx, iyy, izz), O)
56
+ assert isinstance(I, tuple)
57
+ assert I.__repr__() == ('Inertia(dyadic=ixx*(N.x|N.x) + iyy*(N.y|N.y) + '
58
+ 'izz*(N.z|N.z), point=O)')
59
+ assert I.dyadic == I_dyadic
60
+ assert I.point == O
61
+ assert I[0] == I_dyadic
62
+ assert I[1] == O
63
+ assert I == (I_dyadic, O) # Test tuple equal
64
+ raises(TypeError, lambda: I != (O, I_dyadic)) # Incorrect tuple order
65
+ assert I == Inertia(O, I_dyadic) # Parse changed argument order
66
+ assert I == Inertia.from_inertia_scalars(O, N, ixx, iyy, izz)
67
+ # Test invalid tuple operations
68
+ raises(TypeError, lambda: I + (1, 2))
69
+ raises(TypeError, lambda: (1, 2) + I)
70
+ raises(TypeError, lambda: I * 2)
71
+ raises(TypeError, lambda: 2 * I)
lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_joint.py ADDED
@@ -0,0 +1,1240 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sympy.core.function import expand_mul
2
+ from sympy.core.numbers import pi
3
+ from sympy.core.singleton import S
4
+ from sympy.functions.elementary.miscellaneous import sqrt
5
+ from sympy.functions.elementary.trigonometric import (cos, sin)
6
+ from sympy import Matrix, simplify, eye, zeros
7
+ from sympy.core.symbol import symbols
8
+ from sympy.physics.mechanics import (
9
+ dynamicsymbols, RigidBody, Particle, JointsMethod, PinJoint, PrismaticJoint,
10
+ CylindricalJoint, PlanarJoint, SphericalJoint, WeldJoint, Body)
11
+ from sympy.physics.mechanics.joint import Joint
12
+ from sympy.physics.vector import Vector, ReferenceFrame, Point
13
+ from sympy.testing.pytest import raises, warns_deprecated_sympy
14
+
15
+
16
+ t = dynamicsymbols._t # type: ignore
17
+
18
+
19
+ def _generate_body(interframe=False):
20
+ N = ReferenceFrame('N')
21
+ A = ReferenceFrame('A')
22
+ P = RigidBody('P', frame=N)
23
+ C = RigidBody('C', frame=A)
24
+ if interframe:
25
+ Pint, Cint = ReferenceFrame('P_int'), ReferenceFrame('C_int')
26
+ Pint.orient_axis(N, N.x, pi)
27
+ Cint.orient_axis(A, A.y, -pi / 2)
28
+ return N, A, P, C, Pint, Cint
29
+ return N, A, P, C
30
+
31
+
32
+ def test_Joint():
33
+ parent = RigidBody('parent')
34
+ child = RigidBody('child')
35
+ raises(TypeError, lambda: Joint('J', parent, child))
36
+
37
+
38
+ def test_coordinate_generation():
39
+ q, u, qj, uj = dynamicsymbols('q u q_J u_J')
40
+ q0j, q1j, q2j, q3j, u0j, u1j, u2j, u3j = dynamicsymbols('q0:4_J u0:4_J')
41
+ q0, q1, q2, q3, u0, u1, u2, u3 = dynamicsymbols('q0:4 u0:4')
42
+ _, _, P, C = _generate_body()
43
+ # Using PinJoint to access Joint's coordinate generation method
44
+ J = PinJoint('J', P, C)
45
+ # Test single given
46
+ assert J._fill_coordinate_list(q, 1) == Matrix([q])
47
+ assert J._fill_coordinate_list([u], 1) == Matrix([u])
48
+ assert J._fill_coordinate_list([u], 1, offset=2) == Matrix([u])
49
+ # Test None
50
+ assert J._fill_coordinate_list(None, 1) == Matrix([qj])
51
+ assert J._fill_coordinate_list([None], 1) == Matrix([qj])
52
+ assert J._fill_coordinate_list([q0, None, None], 3) == Matrix(
53
+ [q0, q1j, q2j])
54
+ # Test autofill
55
+ assert J._fill_coordinate_list(None, 3) == Matrix([q0j, q1j, q2j])
56
+ assert J._fill_coordinate_list([], 3) == Matrix([q0j, q1j, q2j])
57
+ # Test offset
58
+ assert J._fill_coordinate_list([], 3, offset=1) == Matrix([q1j, q2j, q3j])
59
+ assert J._fill_coordinate_list([q1, None, q3], 3, offset=1) == Matrix(
60
+ [q1, q2j, q3])
61
+ assert J._fill_coordinate_list(None, 2, offset=2) == Matrix([q2j, q3j])
62
+ # Test label
63
+ assert J._fill_coordinate_list(None, 1, 'u') == Matrix([uj])
64
+ assert J._fill_coordinate_list([], 3, 'u') == Matrix([u0j, u1j, u2j])
65
+ # Test single numbering
66
+ assert J._fill_coordinate_list(None, 1, number_single=True) == Matrix([q0j])
67
+ assert J._fill_coordinate_list([], 1, 'u', 2, True) == Matrix([u2j])
68
+ assert J._fill_coordinate_list([], 3, 'q') == Matrix([q0j, q1j, q2j])
69
+ # Test invalid number of coordinates supplied
70
+ raises(ValueError, lambda: J._fill_coordinate_list([q0, q1], 1))
71
+ raises(ValueError, lambda: J._fill_coordinate_list([u0, u1, None], 2, 'u'))
72
+ raises(ValueError, lambda: J._fill_coordinate_list([q0, q1], 3))
73
+ # Test incorrect coordinate type
74
+ raises(TypeError, lambda: J._fill_coordinate_list([q0, symbols('q1')], 2))
75
+ raises(TypeError, lambda: J._fill_coordinate_list([q0 + q1, q1], 2))
76
+ # Test if derivative as generalized speed is allowed
77
+ _, _, P, C = _generate_body()
78
+ PinJoint('J', P, C, q1, q1.diff(t))
79
+ # Test duplicate coordinates
80
+ _, _, P, C = _generate_body()
81
+ raises(ValueError, lambda: SphericalJoint('J', P, C, [q1j, None, None]))
82
+ raises(ValueError, lambda: SphericalJoint('J', P, C, speeds=[u0, u0, u1]))
83
+
84
+
85
+ def test_pin_joint():
86
+ P = RigidBody('P')
87
+ C = RigidBody('C')
88
+ l, m = symbols('l m')
89
+ q, u = dynamicsymbols('q_J, u_J')
90
+ Pj = PinJoint('J', P, C)
91
+ assert Pj.name == 'J'
92
+ assert Pj.parent == P
93
+ assert Pj.child == C
94
+ assert Pj.coordinates == Matrix([q])
95
+ assert Pj.speeds == Matrix([u])
96
+ assert Pj.kdes == Matrix([u - q.diff(t)])
97
+ assert Pj.joint_axis == P.frame.x
98
+ assert Pj.child_point.pos_from(C.masscenter) == Vector(0)
99
+ assert Pj.parent_point.pos_from(P.masscenter) == Vector(0)
100
+ assert Pj.parent_point.pos_from(Pj._child_point) == Vector(0)
101
+ assert C.masscenter.pos_from(P.masscenter) == Vector(0)
102
+ assert Pj.parent_interframe == P.frame
103
+ assert Pj.child_interframe == C.frame
104
+ assert Pj.__str__() == 'PinJoint: J parent: P child: C'
105
+
106
+ P1 = RigidBody('P1')
107
+ C1 = RigidBody('C1')
108
+ Pint = ReferenceFrame('P_int')
109
+ Pint.orient_axis(P1.frame, P1.y, pi / 2)
110
+ J1 = PinJoint('J1', P1, C1, parent_point=l*P1.frame.x,
111
+ child_point=m*C1.frame.y, joint_axis=P1.frame.z,
112
+ parent_interframe=Pint)
113
+ assert J1._joint_axis == P1.frame.z
114
+ assert J1._child_point.pos_from(C1.masscenter) == m * C1.frame.y
115
+ assert J1._parent_point.pos_from(P1.masscenter) == l * P1.frame.x
116
+ assert J1._parent_point.pos_from(J1._child_point) == Vector(0)
117
+ assert (P1.masscenter.pos_from(C1.masscenter) ==
118
+ -l*P1.frame.x + m*C1.frame.y)
119
+ assert J1.parent_interframe == Pint
120
+ assert J1.child_interframe == C1.frame
121
+
122
+ q, u = dynamicsymbols('q, u')
123
+ N, A, P, C, Pint, Cint = _generate_body(True)
124
+ parent_point = P.masscenter.locatenew('parent_point', N.x + N.y)
125
+ child_point = C.masscenter.locatenew('child_point', C.y + C.z)
126
+ J = PinJoint('J', P, C, q, u, parent_point=parent_point,
127
+ child_point=child_point, parent_interframe=Pint,
128
+ child_interframe=Cint, joint_axis=N.z)
129
+ assert J.joint_axis == N.z
130
+ assert J.parent_point.vel(N) == 0
131
+ assert J.parent_point == parent_point
132
+ assert J.child_point == child_point
133
+ assert J.child_point.pos_from(P.masscenter) == N.x + N.y
134
+ assert J.parent_point.pos_from(C.masscenter) == C.y + C.z
135
+ assert C.masscenter.pos_from(P.masscenter) == N.x + N.y - C.y - C.z
136
+ assert C.masscenter.vel(N).express(N) == (u * sin(q) - u * cos(q)) * N.x + (
137
+ -u * sin(q) - u * cos(q)) * N.y
138
+ assert J.parent_interframe == Pint
139
+ assert J.child_interframe == Cint
140
+
141
+
142
+ def test_particle_compatibility():
143
+ m, l = symbols('m l')
144
+ C_frame = ReferenceFrame('C')
145
+ P = Particle('P')
146
+ C = Particle('C', mass=m)
147
+ q, u = dynamicsymbols('q, u')
148
+ J = PinJoint('J', P, C, q, u, child_interframe=C_frame,
149
+ child_point=l * C_frame.y)
150
+ assert J.child_interframe == C_frame
151
+ assert J.parent_interframe.name == 'J_P_frame'
152
+ assert C.masscenter.pos_from(P.masscenter) == -l * C_frame.y
153
+ assert C_frame.dcm(J.parent_interframe) == Matrix([[1, 0, 0],
154
+ [0, cos(q), sin(q)],
155
+ [0, -sin(q), cos(q)]])
156
+ assert C.masscenter.vel(J.parent_interframe) == -l * u * C_frame.z
157
+ # Test with specified joint axis
158
+ P_frame = ReferenceFrame('P')
159
+ C_frame = ReferenceFrame('C')
160
+ P = Particle('P')
161
+ C = Particle('C', mass=m)
162
+ q, u = dynamicsymbols('q, u')
163
+ J = PinJoint('J', P, C, q, u, parent_interframe=P_frame,
164
+ child_interframe=C_frame, child_point=l * C_frame.y,
165
+ joint_axis=P_frame.z)
166
+ assert J.joint_axis == J.parent_interframe.z
167
+ assert C_frame.dcm(J.parent_interframe) == Matrix([[cos(q), sin(q), 0],
168
+ [-sin(q), cos(q), 0],
169
+ [0, 0, 1]])
170
+ assert P.masscenter.vel(J.parent_interframe) == 0
171
+ assert C.masscenter.vel(J.parent_interframe) == l * u * C_frame.x
172
+ q1, q2, q3, u1, u2, u3 = dynamicsymbols('q1:4 u1:4')
173
+ qdot_to_u = {qi.diff(t): ui for qi, ui in ((q1, u1), (q2, u2), (q3, u3))}
174
+ # Test compatibility for prismatic joint
175
+ P, C = Particle('P'), Particle('C')
176
+ J = PrismaticJoint('J', P, C, q, u)
177
+ assert J.parent_interframe.dcm(J.child_interframe) == eye(3)
178
+ assert C.masscenter.pos_from(P.masscenter) == q * J.parent_interframe.x
179
+ assert P.masscenter.vel(J.parent_interframe) == 0
180
+ assert C.masscenter.vel(J.parent_interframe) == u * J.parent_interframe.x
181
+ # Test compatibility for cylindrical joint
182
+ P, C = Particle('P'), Particle('C')
183
+ P_frame = ReferenceFrame('P_frame')
184
+ J = CylindricalJoint('J', P, C, q1, q2, u1, u2, parent_interframe=P_frame,
185
+ parent_point=l * P_frame.x, joint_axis=P_frame.y)
186
+ assert J.parent_interframe.dcm(J.child_interframe) == Matrix([
187
+ [cos(q1), 0, sin(q1)], [0, 1, 0], [-sin(q1), 0, cos(q1)]])
188
+ assert C.masscenter.pos_from(P.masscenter) == l * P_frame.x + q2 * P_frame.y
189
+ assert C.masscenter.vel(J.parent_interframe) == u2 * P_frame.y
190
+ assert P.masscenter.vel(J.child_interframe).xreplace(qdot_to_u) == (
191
+ -u2 * P_frame.y - l * u1 * P_frame.z)
192
+ # Test compatibility for planar joint
193
+ P, C = Particle('P'), Particle('C')
194
+ C_frame = ReferenceFrame('C_frame')
195
+ J = PlanarJoint('J', P, C, q1, [q2, q3], u1, [u2, u3],
196
+ child_interframe=C_frame, child_point=l * C_frame.z)
197
+ P_frame = J.parent_interframe
198
+ assert J.parent_interframe.dcm(J.child_interframe) == Matrix([
199
+ [1, 0, 0], [0, cos(q1), -sin(q1)], [0, sin(q1), cos(q1)]])
200
+ assert C.masscenter.pos_from(P.masscenter) == (
201
+ -l * C_frame.z + q2 * P_frame.y + q3 * P_frame.z)
202
+ assert C.masscenter.vel(J.parent_interframe) == (
203
+ l * u1 * C_frame.y + u2 * P_frame.y + u3 * P_frame.z)
204
+ # Test compatibility for weld joint
205
+ P, C = Particle('P'), Particle('C')
206
+ C_frame, P_frame = ReferenceFrame('C_frame'), ReferenceFrame('P_frame')
207
+ J = WeldJoint('J', P, C, parent_interframe=P_frame,
208
+ child_interframe=C_frame, parent_point=l * P_frame.x,
209
+ child_point=l * C_frame.y)
210
+ assert P_frame.dcm(C_frame) == eye(3)
211
+ assert C.masscenter.pos_from(P.masscenter) == l * P_frame.x - l * C_frame.y
212
+ assert C.masscenter.vel(J.parent_interframe) == 0
213
+
214
+
215
+ def test_body_compatibility():
216
+ m, l = symbols('m l')
217
+ C_frame = ReferenceFrame('C')
218
+ with warns_deprecated_sympy():
219
+ P = Body('P')
220
+ C = Body('C', mass=m, frame=C_frame)
221
+ q, u = dynamicsymbols('q, u')
222
+ PinJoint('J', P, C, q, u, child_point=l * C_frame.y)
223
+ assert C.frame == C_frame
224
+ assert P.frame.name == 'P_frame'
225
+ assert C.masscenter.pos_from(P.masscenter) == -l * C.y
226
+ assert C.frame.dcm(P.frame) == Matrix([[1, 0, 0],
227
+ [0, cos(q), sin(q)],
228
+ [0, -sin(q), cos(q)]])
229
+ assert C.masscenter.vel(P.frame) == -l * u * C.z
230
+
231
+
232
+ def test_pin_joint_double_pendulum():
233
+ q1, q2 = dynamicsymbols('q1 q2')
234
+ u1, u2 = dynamicsymbols('u1 u2')
235
+ m, l = symbols('m l')
236
+ N = ReferenceFrame('N')
237
+ A = ReferenceFrame('A')
238
+ B = ReferenceFrame('B')
239
+ C = RigidBody('C', frame=N) # ceiling
240
+ PartP = RigidBody('P', frame=A, mass=m)
241
+ PartR = RigidBody('R', frame=B, mass=m)
242
+
243
+ J1 = PinJoint('J1', C, PartP, speeds=u1, coordinates=q1,
244
+ child_point=-l*A.x, joint_axis=C.frame.z)
245
+ J2 = PinJoint('J2', PartP, PartR, speeds=u2, coordinates=q2,
246
+ child_point=-l*B.x, joint_axis=PartP.frame.z)
247
+
248
+ # Check orientation
249
+ assert N.dcm(A) == Matrix([[cos(q1), -sin(q1), 0],
250
+ [sin(q1), cos(q1), 0], [0, 0, 1]])
251
+ assert A.dcm(B) == Matrix([[cos(q2), -sin(q2), 0],
252
+ [sin(q2), cos(q2), 0], [0, 0, 1]])
253
+ assert simplify(N.dcm(B)) == Matrix([[cos(q1 + q2), -sin(q1 + q2), 0],
254
+ [sin(q1 + q2), cos(q1 + q2), 0],
255
+ [0, 0, 1]])
256
+
257
+ # Check Angular Velocity
258
+ assert A.ang_vel_in(N) == u1 * N.z
259
+ assert B.ang_vel_in(A) == u2 * A.z
260
+ assert B.ang_vel_in(N) == u1 * N.z + u2 * A.z
261
+
262
+ # Check kde
263
+ assert J1.kdes == Matrix([u1 - q1.diff(t)])
264
+ assert J2.kdes == Matrix([u2 - q2.diff(t)])
265
+
266
+ # Check Linear Velocity
267
+ assert PartP.masscenter.vel(N) == l*u1*A.y
268
+ assert PartR.masscenter.vel(A) == l*u2*B.y
269
+ assert PartR.masscenter.vel(N) == l*u1*A.y + l*(u1 + u2)*B.y
270
+
271
+
272
+ def test_pin_joint_chaos_pendulum():
273
+ mA, mB, lA, lB, h = symbols('mA, mB, lA, lB, h')
274
+ theta, phi, omega, alpha = dynamicsymbols('theta phi omega alpha')
275
+ N = ReferenceFrame('N')
276
+ A = ReferenceFrame('A')
277
+ B = ReferenceFrame('B')
278
+ lA = (lB - h / 2) / 2
279
+ lC = (lB/2 + h/4)
280
+ rod = RigidBody('rod', frame=A, mass=mA)
281
+ plate = RigidBody('plate', mass=mB, frame=B)
282
+ C = RigidBody('C', frame=N)
283
+ J1 = PinJoint('J1', C, rod, coordinates=theta, speeds=omega,
284
+ child_point=lA*A.z, joint_axis=N.y)
285
+ J2 = PinJoint('J2', rod, plate, coordinates=phi, speeds=alpha,
286
+ parent_point=lC*A.z, joint_axis=A.z)
287
+
288
+ # Check orientation
289
+ assert A.dcm(N) == Matrix([[cos(theta), 0, -sin(theta)],
290
+ [0, 1, 0],
291
+ [sin(theta), 0, cos(theta)]])
292
+ assert A.dcm(B) == Matrix([[cos(phi), -sin(phi), 0],
293
+ [sin(phi), cos(phi), 0],
294
+ [0, 0, 1]])
295
+ assert B.dcm(N) == Matrix([
296
+ [cos(phi)*cos(theta), sin(phi), -sin(theta)*cos(phi)],
297
+ [-sin(phi)*cos(theta), cos(phi), sin(phi)*sin(theta)],
298
+ [sin(theta), 0, cos(theta)]])
299
+
300
+ # Check Angular Velocity
301
+ assert A.ang_vel_in(N) == omega*N.y
302
+ assert A.ang_vel_in(B) == -alpha*A.z
303
+ assert N.ang_vel_in(B) == -omega*N.y - alpha*A.z
304
+
305
+ # Check kde
306
+ assert J1.kdes == Matrix([omega - theta.diff(t)])
307
+ assert J2.kdes == Matrix([alpha - phi.diff(t)])
308
+
309
+ # Check pos of masscenters
310
+ assert C.masscenter.pos_from(rod.masscenter) == lA*A.z
311
+ assert rod.masscenter.pos_from(plate.masscenter) == - lC * A.z
312
+
313
+ # Check Linear Velocities
314
+ assert rod.masscenter.vel(N) == (h/4 - lB/2)*omega*A.x
315
+ assert plate.masscenter.vel(N) == ((h/4 - lB/2)*omega +
316
+ (h/4 + lB/2)*omega)*A.x
317
+
318
+
319
+ def test_pin_joint_interframe():
320
+ q, u = dynamicsymbols('q, u')
321
+ # Check not connected
322
+ N, A, P, C = _generate_body()
323
+ Pint, Cint = ReferenceFrame('Pint'), ReferenceFrame('Cint')
324
+ raises(ValueError, lambda: PinJoint('J', P, C, parent_interframe=Pint))
325
+ raises(ValueError, lambda: PinJoint('J', P, C, child_interframe=Cint))
326
+ # Check not fixed interframe
327
+ Pint.orient_axis(N, N.z, q)
328
+ Cint.orient_axis(A, A.z, q)
329
+ raises(ValueError, lambda: PinJoint('J', P, C, parent_interframe=Pint))
330
+ raises(ValueError, lambda: PinJoint('J', P, C, child_interframe=Cint))
331
+ # Check only parent_interframe
332
+ N, A, P, C = _generate_body()
333
+ Pint = ReferenceFrame('Pint')
334
+ Pint.orient_body_fixed(N, (pi / 4, pi, pi / 3), 'xyz')
335
+ PinJoint('J', P, C, q, u, parent_point=N.x, child_point=-C.y,
336
+ parent_interframe=Pint, joint_axis=Pint.x)
337
+ assert simplify(N.dcm(A)) - Matrix([
338
+ [-1 / 2, sqrt(3) * cos(q) / 2, -sqrt(3) * sin(q) / 2],
339
+ [sqrt(6) / 4, sqrt(2) * (2 * sin(q) + cos(q)) / 4,
340
+ sqrt(2) * (-sin(q) + 2 * cos(q)) / 4],
341
+ [sqrt(6) / 4, sqrt(2) * (-2 * sin(q) + cos(q)) / 4,
342
+ -sqrt(2) * (sin(q) + 2 * cos(q)) / 4]]) == zeros(3)
343
+ assert A.ang_vel_in(N) == u * Pint.x
344
+ assert C.masscenter.pos_from(P.masscenter) == N.x + A.y
345
+ assert C.masscenter.vel(N) == u * A.z
346
+ assert P.masscenter.vel(Pint) == Vector(0)
347
+ assert C.masscenter.vel(Pint) == u * A.z
348
+ # Check only child_interframe
349
+ N, A, P, C = _generate_body()
350
+ Cint = ReferenceFrame('Cint')
351
+ Cint.orient_body_fixed(A, (2 * pi / 3, -pi, pi / 2), 'xyz')
352
+ PinJoint('J', P, C, q, u, parent_point=-N.z, child_point=C.x,
353
+ child_interframe=Cint, joint_axis=P.x + P.z)
354
+ assert simplify(N.dcm(A)) == Matrix([
355
+ [-sqrt(2) * sin(q) / 2,
356
+ -sqrt(3) * (cos(q) - 1) / 4 - cos(q) / 4 - S(1) / 4,
357
+ sqrt(3) * (cos(q) + 1) / 4 - cos(q) / 4 + S(1) / 4],
358
+ [cos(q), (sqrt(2) + sqrt(6)) * -sin(q) / 4,
359
+ (-sqrt(2) + sqrt(6)) * sin(q) / 4],
360
+ [sqrt(2) * sin(q) / 2,
361
+ sqrt(3) * (cos(q) + 1) / 4 + cos(q) / 4 - S(1) / 4,
362
+ sqrt(3) * (1 - cos(q)) / 4 + cos(q) / 4 + S(1) / 4]])
363
+ assert A.ang_vel_in(N) == sqrt(2) * u / 2 * N.x + sqrt(2) * u / 2 * N.z
364
+ assert C.masscenter.pos_from(P.masscenter) == - N.z - A.x
365
+ assert C.masscenter.vel(N).simplify() == (
366
+ -sqrt(6) - sqrt(2)) * u / 4 * A.y + (
367
+ -sqrt(2) + sqrt(6)) * u / 4 * A.z
368
+ assert C.masscenter.vel(Cint) == Vector(0)
369
+ # Check combination
370
+ N, A, P, C = _generate_body()
371
+ Pint, Cint = ReferenceFrame('Pint'), ReferenceFrame('Cint')
372
+ Pint.orient_body_fixed(N, (-pi / 2, pi, pi / 2), 'xyz')
373
+ Cint.orient_body_fixed(A, (2 * pi / 3, -pi, pi / 2), 'xyz')
374
+ PinJoint('J', P, C, q, u, parent_point=N.x - N.y, child_point=-C.z,
375
+ parent_interframe=Pint, child_interframe=Cint,
376
+ joint_axis=Pint.x + Pint.z)
377
+ assert simplify(N.dcm(A)) == Matrix([
378
+ [cos(q), (sqrt(2) + sqrt(6)) * -sin(q) / 4,
379
+ (-sqrt(2) + sqrt(6)) * sin(q) / 4],
380
+ [-sqrt(2) * sin(q) / 2,
381
+ -sqrt(3) * (cos(q) + 1) / 4 - cos(q) / 4 + S(1) / 4,
382
+ sqrt(3) * (cos(q) - 1) / 4 - cos(q) / 4 - S(1) / 4],
383
+ [sqrt(2) * sin(q) / 2,
384
+ sqrt(3) * (cos(q) - 1) / 4 + cos(q) / 4 + S(1) / 4,
385
+ -sqrt(3) * (cos(q) + 1) / 4 + cos(q) / 4 - S(1) / 4]])
386
+ assert A.ang_vel_in(N) == sqrt(2) * u / 2 * Pint.x + sqrt(
387
+ 2) * u / 2 * Pint.z
388
+ assert C.masscenter.pos_from(P.masscenter) == N.x - N.y + A.z
389
+ N_v_C = (-sqrt(2) + sqrt(6)) * u / 4 * A.x
390
+ assert C.masscenter.vel(N).simplify() == N_v_C
391
+ assert C.masscenter.vel(Pint).simplify() == N_v_C
392
+ assert C.masscenter.vel(Cint) == Vector(0)
393
+
394
+
395
+ def test_pin_joint_joint_axis():
396
+ q, u = dynamicsymbols('q, u')
397
+ # Check parent as reference
398
+ N, A, P, C, Pint, Cint = _generate_body(True)
399
+ pin = PinJoint('J', P, C, q, u, parent_interframe=Pint,
400
+ child_interframe=Cint, joint_axis=P.y)
401
+ assert pin.joint_axis == P.y
402
+ assert N.dcm(A) == Matrix([[sin(q), 0, cos(q)], [0, -1, 0],
403
+ [cos(q), 0, -sin(q)]])
404
+ # Check parent_interframe as reference
405
+ N, A, P, C, Pint, Cint = _generate_body(True)
406
+ pin = PinJoint('J', P, C, q, u, parent_interframe=Pint,
407
+ child_interframe=Cint, joint_axis=Pint.y)
408
+ assert pin.joint_axis == Pint.y
409
+ assert N.dcm(A) == Matrix([[-sin(q), 0, cos(q)], [0, -1, 0],
410
+ [cos(q), 0, sin(q)]])
411
+ # Check combination of joint_axis with interframes supplied as vectors (2x)
412
+ N, A, P, C = _generate_body()
413
+ pin = PinJoint('J', P, C, q, u, parent_interframe=N.z,
414
+ child_interframe=-C.z, joint_axis=N.z)
415
+ assert pin.joint_axis == N.z
416
+ assert N.dcm(A) == Matrix([[-cos(q), -sin(q), 0], [-sin(q), cos(q), 0],
417
+ [0, 0, -1]])
418
+ N, A, P, C = _generate_body()
419
+ pin = PinJoint('J', P, C, q, u, parent_interframe=N.z,
420
+ child_interframe=-C.z, joint_axis=N.x)
421
+ assert pin.joint_axis == N.x
422
+ assert N.dcm(A) == Matrix([[-1, 0, 0], [0, cos(q), sin(q)],
423
+ [0, sin(q), -cos(q)]])
424
+ # Check time varying axis
425
+ N, A, P, C, Pint, Cint = _generate_body(True)
426
+ raises(ValueError, lambda: PinJoint('J', P, C,
427
+ joint_axis=cos(q) * N.x + sin(q) * N.y))
428
+ # Check joint_axis provided in child frame
429
+ raises(ValueError, lambda: PinJoint('J', P, C, joint_axis=C.x))
430
+ # Check some invalid combinations
431
+ raises(ValueError, lambda: PinJoint('J', P, C, joint_axis=P.x + C.y))
432
+ raises(ValueError, lambda: PinJoint(
433
+ 'J', P, C, parent_interframe=Pint, child_interframe=Cint,
434
+ joint_axis=Pint.x + C.y))
435
+ raises(ValueError, lambda: PinJoint(
436
+ 'J', P, C, parent_interframe=Pint, child_interframe=Cint,
437
+ joint_axis=P.x + Cint.y))
438
+ # Check valid special combination
439
+ N, A, P, C, Pint, Cint = _generate_body(True)
440
+ PinJoint('J', P, C, parent_interframe=Pint, child_interframe=Cint,
441
+ joint_axis=Pint.x + P.y)
442
+ # Check invalid zero vector
443
+ raises(Exception, lambda: PinJoint(
444
+ 'J', P, C, parent_interframe=Pint, child_interframe=Cint,
445
+ joint_axis=Vector(0)))
446
+ raises(Exception, lambda: PinJoint(
447
+ 'J', P, C, parent_interframe=Pint, child_interframe=Cint,
448
+ joint_axis=P.y + Pint.y))
449
+
450
+
451
+ def test_pin_joint_arbitrary_axis():
452
+ q, u = dynamicsymbols('q_J, u_J')
453
+
454
+ # When the bodies are attached though masscenters but axes are opposite.
455
+ N, A, P, C = _generate_body()
456
+ PinJoint('J', P, C, child_interframe=-A.x)
457
+
458
+ assert (-A.x).angle_between(N.x) == 0
459
+ assert -A.x.express(N) == N.x
460
+ assert A.dcm(N) == Matrix([[-1, 0, 0],
461
+ [0, -cos(q), -sin(q)],
462
+ [0, -sin(q), cos(q)]])
463
+ assert A.ang_vel_in(N) == u*N.x
464
+ assert A.ang_vel_in(N).magnitude() == sqrt(u**2)
465
+ assert C.masscenter.pos_from(P.masscenter) == 0
466
+ assert C.masscenter.pos_from(P.masscenter).express(N).simplify() == 0
467
+ assert C.masscenter.vel(N) == 0
468
+
469
+ # When axes are different and parent joint is at masscenter but child joint
470
+ # is at a unit vector from child masscenter.
471
+ N, A, P, C = _generate_body()
472
+ PinJoint('J', P, C, child_interframe=A.y, child_point=A.x)
473
+
474
+ assert A.y.angle_between(N.x) == 0 # Axis are aligned
475
+ assert A.y.express(N) == N.x
476
+ assert A.dcm(N) == Matrix([[0, -cos(q), -sin(q)],
477
+ [1, 0, 0],
478
+ [0, -sin(q), cos(q)]])
479
+ assert A.ang_vel_in(N) == u*N.x
480
+ assert A.ang_vel_in(N).express(A) == u * A.y
481
+ assert A.ang_vel_in(N).magnitude() == sqrt(u**2)
482
+ assert A.ang_vel_in(N).cross(A.y) == 0
483
+ assert C.masscenter.vel(N) == u*A.z
484
+ assert C.masscenter.pos_from(P.masscenter) == -A.x
485
+ assert (C.masscenter.pos_from(P.masscenter).express(N).simplify() ==
486
+ cos(q)*N.y + sin(q)*N.z)
487
+ assert C.masscenter.vel(N).angle_between(A.x) == pi/2
488
+
489
+ # Similar to previous case but wrt parent body
490
+ N, A, P, C = _generate_body()
491
+ PinJoint('J', P, C, parent_interframe=N.y, parent_point=N.x)
492
+
493
+ assert N.y.angle_between(A.x) == 0 # Axis are aligned
494
+ assert N.y.express(A) == A.x
495
+ assert A.dcm(N) == Matrix([[0, 1, 0],
496
+ [-cos(q), 0, sin(q)],
497
+ [sin(q), 0, cos(q)]])
498
+ assert A.ang_vel_in(N) == u*N.y
499
+ assert A.ang_vel_in(N).express(A) == u*A.x
500
+ assert A.ang_vel_in(N).magnitude() == sqrt(u**2)
501
+ angle = A.ang_vel_in(N).angle_between(A.x)
502
+ assert angle.xreplace({u: 1}) == 0
503
+ assert C.masscenter.vel(N) == 0
504
+ assert C.masscenter.pos_from(P.masscenter) == N.x
505
+
506
+ # Both joint pos id defined but different axes
507
+ N, A, P, C = _generate_body()
508
+ PinJoint('J', P, C, parent_point=N.x, child_point=A.x,
509
+ child_interframe=A.x + A.y)
510
+ assert expand_mul(N.x.angle_between(A.x + A.y)) == 0 # Axis are aligned
511
+ assert (A.x + A.y).express(N).simplify() == sqrt(2)*N.x
512
+ assert simplify(A.dcm(N)) == Matrix([
513
+ [sqrt(2)/2, -sqrt(2)*cos(q)/2, -sqrt(2)*sin(q)/2],
514
+ [sqrt(2)/2, sqrt(2)*cos(q)/2, sqrt(2)*sin(q)/2],
515
+ [0, -sin(q), cos(q)]])
516
+ assert A.ang_vel_in(N) == u*N.x
517
+ assert (A.ang_vel_in(N).express(A).simplify() ==
518
+ (u*A.x + u*A.y)/sqrt(2))
519
+ assert A.ang_vel_in(N).magnitude() == sqrt(u**2)
520
+ angle = A.ang_vel_in(N).angle_between(A.x + A.y)
521
+ assert angle.xreplace({u: 1}) == 0
522
+ assert C.masscenter.vel(N).simplify() == (u * A.z)/sqrt(2)
523
+ assert C.masscenter.pos_from(P.masscenter) == N.x - A.x
524
+ assert (C.masscenter.pos_from(P.masscenter).express(N).simplify() ==
525
+ (1 - sqrt(2)/2)*N.x + sqrt(2)*cos(q)/2*N.y +
526
+ sqrt(2)*sin(q)/2*N.z)
527
+ assert (C.masscenter.vel(N).express(N).simplify() ==
528
+ -sqrt(2)*u*sin(q)/2*N.y + sqrt(2)*u*cos(q)/2*N.z)
529
+ assert C.masscenter.vel(N).angle_between(A.x) == pi/2
530
+
531
+ N, A, P, C = _generate_body()
532
+ PinJoint('J', P, C, parent_point=N.x, child_point=A.x,
533
+ child_interframe=A.x + A.y - A.z)
534
+ assert expand_mul(N.x.angle_between(A.x + A.y - A.z)) == 0 # Axis aligned
535
+ assert (A.x + A.y - A.z).express(N).simplify() == sqrt(3)*N.x
536
+ assert simplify(A.dcm(N)) == Matrix([
537
+ [sqrt(3)/3, -sqrt(6)*sin(q + pi/4)/3,
538
+ sqrt(6)*cos(q + pi/4)/3],
539
+ [sqrt(3)/3, sqrt(6)*cos(q + pi/12)/3,
540
+ sqrt(6)*sin(q + pi/12)/3],
541
+ [-sqrt(3)/3, sqrt(6)*cos(q + 5*pi/12)/3,
542
+ sqrt(6)*sin(q + 5*pi/12)/3]])
543
+ assert A.ang_vel_in(N) == u*N.x
544
+ assert A.ang_vel_in(N).express(A).simplify() == (u*A.x + u*A.y -
545
+ u*A.z)/sqrt(3)
546
+ assert A.ang_vel_in(N).magnitude() == sqrt(u**2)
547
+ angle = A.ang_vel_in(N).angle_between(A.x + A.y-A.z)
548
+ assert angle.xreplace({u: 1}).simplify() == 0
549
+ assert C.masscenter.vel(N).simplify() == (u*A.y + u*A.z)/sqrt(3)
550
+ assert C.masscenter.pos_from(P.masscenter) == N.x - A.x
551
+ assert (C.masscenter.pos_from(P.masscenter).express(N).simplify() ==
552
+ (1 - sqrt(3)/3)*N.x + sqrt(6)*sin(q + pi/4)/3*N.y -
553
+ sqrt(6)*cos(q + pi/4)/3*N.z)
554
+ assert (C.masscenter.vel(N).express(N).simplify() ==
555
+ sqrt(6)*u*cos(q + pi/4)/3*N.y +
556
+ sqrt(6)*u*sin(q + pi/4)/3*N.z)
557
+ assert C.masscenter.vel(N).angle_between(A.x) == pi/2
558
+
559
+ N, A, P, C = _generate_body()
560
+ m, n = symbols('m n')
561
+ PinJoint('J', P, C, parent_point=m * N.x, child_point=n * A.x,
562
+ child_interframe=A.x + A.y - A.z,
563
+ parent_interframe=N.x - N.y + N.z)
564
+ angle = (N.x - N.y + N.z).angle_between(A.x + A.y - A.z)
565
+ assert expand_mul(angle) == 0 # Axis are aligned
566
+ assert ((A.x-A.y+A.z).express(N).simplify() ==
567
+ (-4*cos(q)/3 - S(1)/3)*N.x + (S(1)/3 - 4*sin(q + pi/6)/3)*N.y +
568
+ (4*cos(q + pi/3)/3 - S(1)/3)*N.z)
569
+ assert simplify(A.dcm(N)) == Matrix([
570
+ [S(1)/3 - 2*cos(q)/3, -2*sin(q + pi/6)/3 - S(1)/3,
571
+ 2*cos(q + pi/3)/3 + S(1)/3],
572
+ [2*cos(q + pi/3)/3 + S(1)/3, 2*cos(q)/3 - S(1)/3,
573
+ 2*sin(q + pi/6)/3 + S(1)/3],
574
+ [-2*sin(q + pi/6)/3 - S(1)/3, 2*cos(q + pi/3)/3 + S(1)/3,
575
+ 2*cos(q)/3 - S(1)/3]])
576
+ assert (A.ang_vel_in(N) - (u*N.x - u*N.y + u*N.z)/sqrt(3)).simplify()
577
+ assert A.ang_vel_in(N).express(A).simplify() == (u*A.x + u*A.y -
578
+ u*A.z)/sqrt(3)
579
+ assert A.ang_vel_in(N).magnitude() == sqrt(u**2)
580
+ angle = A.ang_vel_in(N).angle_between(A.x+A.y-A.z)
581
+ assert angle.xreplace({u: 1}).simplify() == 0
582
+ assert (C.masscenter.vel(N).simplify() ==
583
+ sqrt(3)*n*u/3*A.y + sqrt(3)*n*u/3*A.z)
584
+ assert C.masscenter.pos_from(P.masscenter) == m*N.x - n*A.x
585
+ assert (C.masscenter.pos_from(P.masscenter).express(N).simplify() ==
586
+ (m + n*(2*cos(q) - 1)/3)*N.x + n*(2*sin(q + pi/6) +
587
+ 1)/3*N.y - n*(2*cos(q + pi/3) + 1)/3*N.z)
588
+ assert (C.masscenter.vel(N).express(N).simplify() ==
589
+ - 2*n*u*sin(q)/3*N.x + 2*n*u*cos(q + pi/6)/3*N.y +
590
+ 2*n*u*sin(q + pi/3)/3*N.z)
591
+ assert C.masscenter.vel(N).dot(N.x - N.y + N.z).simplify() == 0
592
+
593
+
594
+ def test_create_aligned_frame_pi():
595
+ N, A, P, C = _generate_body()
596
+ f = Joint._create_aligned_interframe(P, -P.x, P.x)
597
+ assert f.z == P.z
598
+ f = Joint._create_aligned_interframe(P, -P.y, P.y)
599
+ assert f.x == P.x
600
+ f = Joint._create_aligned_interframe(P, -P.z, P.z)
601
+ assert f.y == P.y
602
+ f = Joint._create_aligned_interframe(P, -P.x - P.y, P.x + P.y)
603
+ assert f.z == P.z
604
+ f = Joint._create_aligned_interframe(P, -P.y - P.z, P.y + P.z)
605
+ assert f.x == P.x
606
+ f = Joint._create_aligned_interframe(P, -P.x - P.z, P.x + P.z)
607
+ assert f.y == P.y
608
+ f = Joint._create_aligned_interframe(P, -P.x - P.y - P.z, P.x + P.y + P.z)
609
+ assert f.y - f.z == P.y - P.z
610
+
611
+
612
+ def test_pin_joint_axis():
613
+ q, u = dynamicsymbols('q u')
614
+ # Test default joint axis
615
+ N, A, P, C, Pint, Cint = _generate_body(True)
616
+ J = PinJoint('J', P, C, q, u, parent_interframe=Pint, child_interframe=Cint)
617
+ assert J.joint_axis == Pint.x
618
+ # Test for the same joint axis expressed in different frames
619
+ N_R_A = Matrix([[0, sin(q), cos(q)],
620
+ [0, -cos(q), sin(q)],
621
+ [1, 0, 0]])
622
+ N, A, P, C, Pint, Cint = _generate_body(True)
623
+ PinJoint('J', P, C, q, u, parent_interframe=Pint, child_interframe=Cint,
624
+ joint_axis=N.z)
625
+ assert N.dcm(A) == N_R_A
626
+ N, A, P, C, Pint, Cint = _generate_body(True)
627
+ PinJoint('J', P, C, q, u, parent_interframe=Pint, child_interframe=Cint,
628
+ joint_axis=-Pint.z)
629
+ assert N.dcm(A) == N_R_A
630
+ # Test time varying joint axis
631
+ N, A, P, C, Pint, Cint = _generate_body(True)
632
+ raises(ValueError, lambda: PinJoint('J', P, C, joint_axis=q * N.z))
633
+
634
+
635
+ def test_locate_joint_pos():
636
+ # Test Vector and default
637
+ N, A, P, C = _generate_body()
638
+ joint = PinJoint('J', P, C, parent_point=N.y + N.z)
639
+ assert joint.parent_point.name == 'J_P_joint'
640
+ assert joint.parent_point.pos_from(P.masscenter) == N.y + N.z
641
+ assert joint.child_point == C.masscenter
642
+ # Test Point objects
643
+ N, A, P, C = _generate_body()
644
+ parent_point = P.masscenter.locatenew('p', N.y + N.z)
645
+ joint = PinJoint('J', P, C, parent_point=parent_point,
646
+ child_point=C.masscenter)
647
+ assert joint.parent_point == parent_point
648
+ assert joint.child_point == C.masscenter
649
+ # Check invalid type
650
+ N, A, P, C = _generate_body()
651
+ raises(TypeError,
652
+ lambda: PinJoint('J', P, C, parent_point=N.x.to_matrix(N)))
653
+ # Test time varying positions
654
+ q = dynamicsymbols('q')
655
+ N, A, P, C = _generate_body()
656
+ raises(ValueError, lambda: PinJoint('J', P, C, parent_point=q * N.x))
657
+ N, A, P, C = _generate_body()
658
+ child_point = C.masscenter.locatenew('p', q * A.y)
659
+ raises(ValueError, lambda: PinJoint('J', P, C, child_point=child_point))
660
+ # Test undefined position
661
+ child_point = Point('p')
662
+ raises(ValueError, lambda: PinJoint('J', P, C, child_point=child_point))
663
+
664
+
665
+ def test_locate_joint_frame():
666
+ # Test rotated frame and default
667
+ N, A, P, C = _generate_body()
668
+ parent_interframe = ReferenceFrame('int_frame')
669
+ parent_interframe.orient_axis(N, N.z, 1)
670
+ joint = PinJoint('J', P, C, parent_interframe=parent_interframe)
671
+ assert joint.parent_interframe == parent_interframe
672
+ assert joint.parent_interframe.ang_vel_in(N) == 0
673
+ assert joint.child_interframe == A
674
+ # Test time varying orientations
675
+ q = dynamicsymbols('q')
676
+ N, A, P, C = _generate_body()
677
+ parent_interframe = ReferenceFrame('int_frame')
678
+ parent_interframe.orient_axis(N, N.z, q)
679
+ raises(ValueError,
680
+ lambda: PinJoint('J', P, C, parent_interframe=parent_interframe))
681
+ # Test undefined frame
682
+ N, A, P, C = _generate_body()
683
+ child_interframe = ReferenceFrame('int_frame')
684
+ child_interframe.orient_axis(N, N.z, 1) # Defined with respect to parent
685
+ raises(ValueError,
686
+ lambda: PinJoint('J', P, C, child_interframe=child_interframe))
687
+
688
+
689
+ def test_prismatic_joint():
690
+ _, _, P, C = _generate_body()
691
+ q, u = dynamicsymbols('q_S, u_S')
692
+ S = PrismaticJoint('S', P, C)
693
+ assert S.name == 'S'
694
+ assert S.parent == P
695
+ assert S.child == C
696
+ assert S.coordinates == Matrix([q])
697
+ assert S.speeds == Matrix([u])
698
+ assert S.kdes == Matrix([u - q.diff(t)])
699
+ assert S.joint_axis == P.frame.x
700
+ assert S.child_point.pos_from(C.masscenter) == Vector(0)
701
+ assert S.parent_point.pos_from(P.masscenter) == Vector(0)
702
+ assert S.parent_point.pos_from(S.child_point) == - q * P.frame.x
703
+ assert P.masscenter.pos_from(C.masscenter) == - q * P.frame.x
704
+ assert C.masscenter.vel(P.frame) == u * P.frame.x
705
+ assert P.frame.ang_vel_in(C.frame) == 0
706
+ assert C.frame.ang_vel_in(P.frame) == 0
707
+ assert S.__str__() == 'PrismaticJoint: S parent: P child: C'
708
+
709
+ N, A, P, C = _generate_body()
710
+ l, m = symbols('l m')
711
+ Pint = ReferenceFrame('P_int')
712
+ Pint.orient_axis(P.frame, P.y, pi / 2)
713
+ S = PrismaticJoint('S', P, C, parent_point=l * P.frame.x,
714
+ child_point=m * C.frame.y, joint_axis=P.frame.z,
715
+ parent_interframe=Pint)
716
+
717
+ assert S.joint_axis == P.frame.z
718
+ assert S.child_point.pos_from(C.masscenter) == m * C.frame.y
719
+ assert S.parent_point.pos_from(P.masscenter) == l * P.frame.x
720
+ assert S.parent_point.pos_from(S.child_point) == - q * P.frame.z
721
+ assert P.masscenter.pos_from(C.masscenter) == - l * N.x - q * N.z + m * A.y
722
+ assert C.masscenter.vel(P.frame) == u * P.frame.z
723
+ assert P.masscenter.vel(Pint) == Vector(0)
724
+ assert C.frame.ang_vel_in(P.frame) == 0
725
+ assert P.frame.ang_vel_in(C.frame) == 0
726
+
727
+ _, _, P, C = _generate_body()
728
+ Pint = ReferenceFrame('P_int')
729
+ Pint.orient_axis(P.frame, P.y, pi / 2)
730
+ S = PrismaticJoint('S', P, C, parent_point=l * P.frame.z,
731
+ child_point=m * C.frame.x, joint_axis=P.frame.z,
732
+ parent_interframe=Pint)
733
+ assert S.joint_axis == P.frame.z
734
+ assert S.child_point.pos_from(C.masscenter) == m * C.frame.x
735
+ assert S.parent_point.pos_from(P.masscenter) == l * P.frame.z
736
+ assert S.parent_point.pos_from(S.child_point) == - q * P.frame.z
737
+ assert P.masscenter.pos_from(C.masscenter) == (-l - q)*P.frame.z + m*C.frame.x
738
+ assert C.masscenter.vel(P.frame) == u * P.frame.z
739
+ assert C.frame.ang_vel_in(P.frame) == 0
740
+ assert P.frame.ang_vel_in(C.frame) == 0
741
+
742
+
743
+ def test_prismatic_joint_arbitrary_axis():
744
+ q, u = dynamicsymbols('q_S, u_S')
745
+
746
+ N, A, P, C = _generate_body()
747
+ PrismaticJoint('S', P, C, child_interframe=-A.x)
748
+
749
+ assert (-A.x).angle_between(N.x) == 0
750
+ assert -A.x.express(N) == N.x
751
+ assert A.dcm(N) == Matrix([[-1, 0, 0], [0, -1, 0], [0, 0, 1]])
752
+ assert C.masscenter.pos_from(P.masscenter) == q * N.x
753
+ assert C.masscenter.pos_from(P.masscenter).express(A).simplify() == -q * A.x
754
+ assert C.masscenter.vel(N) == u * N.x
755
+ assert C.masscenter.vel(N).express(A) == -u * A.x
756
+ assert A.ang_vel_in(N) == 0
757
+ assert N.ang_vel_in(A) == 0
758
+
759
+ #When axes are different and parent joint is at masscenter but child joint is at a unit vector from
760
+ #child masscenter.
761
+ N, A, P, C = _generate_body()
762
+ PrismaticJoint('S', P, C, child_interframe=A.y, child_point=A.x)
763
+
764
+ assert A.y.angle_between(N.x) == 0 #Axis are aligned
765
+ assert A.y.express(N) == N.x
766
+ assert A.dcm(N) == Matrix([[0, -1, 0], [1, 0, 0], [0, 0, 1]])
767
+ assert C.masscenter.vel(N) == u * N.x
768
+ assert C.masscenter.vel(N).express(A) == u * A.y
769
+ assert C.masscenter.pos_from(P.masscenter) == q*N.x - A.x
770
+ assert C.masscenter.pos_from(P.masscenter).express(N).simplify() == q*N.x + N.y
771
+ assert A.ang_vel_in(N) == 0
772
+ assert N.ang_vel_in(A) == 0
773
+
774
+ #Similar to previous case but wrt parent body
775
+ N, A, P, C = _generate_body()
776
+ PrismaticJoint('S', P, C, parent_interframe=N.y, parent_point=N.x)
777
+
778
+ assert N.y.angle_between(A.x) == 0 #Axis are aligned
779
+ assert N.y.express(A) == A.x
780
+ assert A.dcm(N) == Matrix([[0, 1, 0], [-1, 0, 0], [0, 0, 1]])
781
+ assert C.masscenter.vel(N) == u * N.y
782
+ assert C.masscenter.vel(N).express(A) == u * A.x
783
+ assert C.masscenter.pos_from(P.masscenter) == N.x + q*N.y
784
+ assert A.ang_vel_in(N) == 0
785
+ assert N.ang_vel_in(A) == 0
786
+
787
+ #Both joint pos is defined but different axes
788
+ N, A, P, C = _generate_body()
789
+ PrismaticJoint('S', P, C, parent_point=N.x, child_point=A.x,
790
+ child_interframe=A.x + A.y)
791
+ assert N.x.angle_between(A.x + A.y) == 0 #Axis are aligned
792
+ assert (A.x + A.y).express(N) == sqrt(2)*N.x
793
+ assert A.dcm(N) == Matrix([[sqrt(2)/2, -sqrt(2)/2, 0], [sqrt(2)/2, sqrt(2)/2, 0], [0, 0, 1]])
794
+ assert C.masscenter.pos_from(P.masscenter) == (q + 1)*N.x - A.x
795
+ assert C.masscenter.pos_from(P.masscenter).express(N) == (q - sqrt(2)/2 + 1)*N.x + sqrt(2)/2*N.y
796
+ assert C.masscenter.vel(N).express(A) == u * (A.x + A.y)/sqrt(2)
797
+ assert C.masscenter.vel(N) == u*N.x
798
+ assert A.ang_vel_in(N) == 0
799
+ assert N.ang_vel_in(A) == 0
800
+
801
+ N, A, P, C = _generate_body()
802
+ PrismaticJoint('S', P, C, parent_point=N.x, child_point=A.x,
803
+ child_interframe=A.x + A.y - A.z)
804
+ assert N.x.angle_between(A.x + A.y - A.z).simplify() == 0 #Axis are aligned
805
+ assert ((A.x + A.y - A.z).express(N) - sqrt(3)*N.x).simplify() == 0
806
+ assert simplify(A.dcm(N)) == Matrix([[sqrt(3)/3, -sqrt(3)/3, sqrt(3)/3],
807
+ [sqrt(3)/3, sqrt(3)/6 + S(1)/2, S(1)/2 - sqrt(3)/6],
808
+ [-sqrt(3)/3, S(1)/2 - sqrt(3)/6, sqrt(3)/6 + S(1)/2]])
809
+ assert C.masscenter.pos_from(P.masscenter) == (q + 1)*N.x - A.x
810
+ assert (C.masscenter.pos_from(P.masscenter).express(N) -
811
+ ((q - sqrt(3)/3 + 1)*N.x + sqrt(3)/3*N.y - sqrt(3)/3*N.z)).simplify() == 0
812
+ assert C.masscenter.vel(N) == u*N.x
813
+ assert (C.masscenter.vel(N).express(A) - (
814
+ sqrt(3)*u/3*A.x + sqrt(3)*u/3*A.y - sqrt(3)*u/3*A.z)).simplify()
815
+ assert A.ang_vel_in(N) == 0
816
+ assert N.ang_vel_in(A) == 0
817
+
818
+ N, A, P, C = _generate_body()
819
+ m, n = symbols('m n')
820
+ PrismaticJoint('S', P, C, parent_point=m*N.x, child_point=n*A.x,
821
+ child_interframe=A.x + A.y - A.z,
822
+ parent_interframe=N.x - N.y + N.z)
823
+ # 0 angle means that the axis are aligned
824
+ assert (N.x-N.y+N.z).angle_between(A.x+A.y-A.z).simplify() == 0
825
+ assert ((A.x+A.y-A.z).express(N) - (N.x - N.y + N.z)).simplify() == 0
826
+ assert simplify(A.dcm(N)) == Matrix([[-S(1)/3, -S(2)/3, S(2)/3],
827
+ [S(2)/3, S(1)/3, S(2)/3],
828
+ [-S(2)/3, S(2)/3, S(1)/3]])
829
+ assert (C.masscenter.pos_from(P.masscenter) - (
830
+ (m + sqrt(3)*q/3)*N.x - sqrt(3)*q/3*N.y + sqrt(3)*q/3*N.z - n*A.x)
831
+ ).express(N).simplify() == 0
832
+ assert (C.masscenter.pos_from(P.masscenter).express(N) - (
833
+ (m + n/3 + sqrt(3)*q/3)*N.x + (2*n/3 - sqrt(3)*q/3)*N.y +
834
+ (-2*n/3 + sqrt(3)*q/3)*N.z)).simplify() == 0
835
+ assert (C.masscenter.vel(N).express(N) - (
836
+ sqrt(3)*u/3*N.x - sqrt(3)*u/3*N.y + sqrt(3)*u/3*N.z)).simplify() == 0
837
+ assert (C.masscenter.vel(N).express(A) -
838
+ (sqrt(3)*u/3*A.x + sqrt(3)*u/3*A.y - sqrt(3)*u/3*A.z)).simplify() == 0
839
+ assert A.ang_vel_in(N) == 0
840
+ assert N.ang_vel_in(A) == 0
841
+
842
+
843
+ def test_cylindrical_joint():
844
+ N, A, P, C = _generate_body()
845
+ q0_def, q1_def, u0_def, u1_def = dynamicsymbols('q0:2_J, u0:2_J')
846
+ Cj = CylindricalJoint('J', P, C)
847
+ assert Cj.name == 'J'
848
+ assert Cj.parent == P
849
+ assert Cj.child == C
850
+ assert Cj.coordinates == Matrix([q0_def, q1_def])
851
+ assert Cj.speeds == Matrix([u0_def, u1_def])
852
+ assert Cj.rotation_coordinate == q0_def
853
+ assert Cj.translation_coordinate == q1_def
854
+ assert Cj.rotation_speed == u0_def
855
+ assert Cj.translation_speed == u1_def
856
+ assert Cj.kdes == Matrix([u0_def - q0_def.diff(t), u1_def - q1_def.diff(t)])
857
+ assert Cj.joint_axis == N.x
858
+ assert Cj.child_point.pos_from(C.masscenter) == Vector(0)
859
+ assert Cj.parent_point.pos_from(P.masscenter) == Vector(0)
860
+ assert Cj.parent_point.pos_from(Cj._child_point) == -q1_def * N.x
861
+ assert C.masscenter.pos_from(P.masscenter) == q1_def * N.x
862
+ assert Cj.child_point.vel(N) == u1_def * N.x
863
+ assert A.ang_vel_in(N) == u0_def * N.x
864
+ assert Cj.parent_interframe == N
865
+ assert Cj.child_interframe == A
866
+ assert Cj.__str__() == 'CylindricalJoint: J parent: P child: C'
867
+
868
+ q0, q1, u0, u1 = dynamicsymbols('q0:2, u0:2')
869
+ l, m = symbols('l, m')
870
+ N, A, P, C, Pint, Cint = _generate_body(True)
871
+ Cj = CylindricalJoint('J', P, C, rotation_coordinate=q0, rotation_speed=u0,
872
+ translation_speed=u1, parent_point=m * N.x,
873
+ child_point=l * A.y, parent_interframe=Pint,
874
+ child_interframe=Cint, joint_axis=2 * N.z)
875
+ assert Cj.coordinates == Matrix([q0, q1_def])
876
+ assert Cj.speeds == Matrix([u0, u1])
877
+ assert Cj.rotation_coordinate == q0
878
+ assert Cj.translation_coordinate == q1_def
879
+ assert Cj.rotation_speed == u0
880
+ assert Cj.translation_speed == u1
881
+ assert Cj.kdes == Matrix([u0 - q0.diff(t), u1 - q1_def.diff(t)])
882
+ assert Cj.joint_axis == 2 * N.z
883
+ assert Cj.child_point.pos_from(C.masscenter) == l * A.y
884
+ assert Cj.parent_point.pos_from(P.masscenter) == m * N.x
885
+ assert Cj.parent_point.pos_from(Cj._child_point) == -q1_def * N.z
886
+ assert C.masscenter.pos_from(
887
+ P.masscenter) == m * N.x + q1_def * N.z - l * A.y
888
+ assert C.masscenter.vel(N) == u1 * N.z - u0 * l * A.z
889
+ assert A.ang_vel_in(N) == u0 * N.z
890
+
891
+
892
+ def test_planar_joint():
893
+ N, A, P, C = _generate_body()
894
+ q0_def, q1_def, q2_def = dynamicsymbols('q0:3_J')
895
+ u0_def, u1_def, u2_def = dynamicsymbols('u0:3_J')
896
+ Cj = PlanarJoint('J', P, C)
897
+ assert Cj.name == 'J'
898
+ assert Cj.parent == P
899
+ assert Cj.child == C
900
+ assert Cj.coordinates == Matrix([q0_def, q1_def, q2_def])
901
+ assert Cj.speeds == Matrix([u0_def, u1_def, u2_def])
902
+ assert Cj.rotation_coordinate == q0_def
903
+ assert Cj.planar_coordinates == Matrix([q1_def, q2_def])
904
+ assert Cj.rotation_speed == u0_def
905
+ assert Cj.planar_speeds == Matrix([u1_def, u2_def])
906
+ assert Cj.kdes == Matrix([u0_def - q0_def.diff(t), u1_def - q1_def.diff(t),
907
+ u2_def - q2_def.diff(t)])
908
+ assert Cj.rotation_axis == N.x
909
+ assert Cj.planar_vectors == [N.y, N.z]
910
+ assert Cj.child_point.pos_from(C.masscenter) == Vector(0)
911
+ assert Cj.parent_point.pos_from(P.masscenter) == Vector(0)
912
+ r_P_C = q1_def * N.y + q2_def * N.z
913
+ assert Cj.parent_point.pos_from(Cj.child_point) == -r_P_C
914
+ assert C.masscenter.pos_from(P.masscenter) == r_P_C
915
+ assert Cj.child_point.vel(N) == u1_def * N.y + u2_def * N.z
916
+ assert A.ang_vel_in(N) == u0_def * N.x
917
+ assert Cj.parent_interframe == N
918
+ assert Cj.child_interframe == A
919
+ assert Cj.__str__() == 'PlanarJoint: J parent: P child: C'
920
+
921
+ q0, q1, q2, u0, u1, u2 = dynamicsymbols('q0:3, u0:3')
922
+ l, m = symbols('l, m')
923
+ N, A, P, C, Pint, Cint = _generate_body(True)
924
+ Cj = PlanarJoint('J', P, C, rotation_coordinate=q0,
925
+ planar_coordinates=[q1, q2], planar_speeds=[u1, u2],
926
+ parent_point=m * N.x, child_point=l * A.y,
927
+ parent_interframe=Pint, child_interframe=Cint)
928
+ assert Cj.coordinates == Matrix([q0, q1, q2])
929
+ assert Cj.speeds == Matrix([u0_def, u1, u2])
930
+ assert Cj.rotation_coordinate == q0
931
+ assert Cj.planar_coordinates == Matrix([q1, q2])
932
+ assert Cj.rotation_speed == u0_def
933
+ assert Cj.planar_speeds == Matrix([u1, u2])
934
+ assert Cj.kdes == Matrix([u0_def - q0.diff(t), u1 - q1.diff(t),
935
+ u2 - q2.diff(t)])
936
+ assert Cj.rotation_axis == Pint.x
937
+ assert Cj.planar_vectors == [Pint.y, Pint.z]
938
+ assert Cj.child_point.pos_from(C.masscenter) == l * A.y
939
+ assert Cj.parent_point.pos_from(P.masscenter) == m * N.x
940
+ assert Cj.parent_point.pos_from(Cj.child_point) == q1 * N.y + q2 * N.z
941
+ assert C.masscenter.pos_from(
942
+ P.masscenter) == m * N.x - q1 * N.y - q2 * N.z - l * A.y
943
+ assert C.masscenter.vel(N) == -u1 * N.y - u2 * N.z + u0_def * l * A.x
944
+ assert A.ang_vel_in(N) == u0_def * N.x
945
+
946
+
947
+ def test_planar_joint_advanced():
948
+ # Tests whether someone is able to just specify two normals, which will form
949
+ # the rotation axis seen from the parent and child body.
950
+ # This specific example is a block on a slope, which has that same slope of
951
+ # 30 degrees, so in the zero configuration the frames of the parent and
952
+ # child are actually aligned.
953
+ q0, q1, q2, u0, u1, u2 = dynamicsymbols('q0:3, u0:3')
954
+ l1, l2 = symbols('l1:3')
955
+ N, A, P, C = _generate_body()
956
+ J = PlanarJoint('J', P, C, q0, [q1, q2], u0, [u1, u2],
957
+ parent_point=l1 * N.z,
958
+ child_point=-l2 * C.z,
959
+ parent_interframe=N.z + N.y / sqrt(3),
960
+ child_interframe=A.z + A.y / sqrt(3))
961
+ assert J.rotation_axis.express(N) == (N.z + N.y / sqrt(3)).normalize()
962
+ assert J.rotation_axis.express(A) == (A.z + A.y / sqrt(3)).normalize()
963
+ assert J.rotation_axis.angle_between(N.z) == pi / 6
964
+ assert N.dcm(A).xreplace({q0: 0, q1: 0, q2: 0}) == eye(3)
965
+ N_R_A = Matrix([
966
+ [cos(q0), -sqrt(3) * sin(q0) / 2, sin(q0) / 2],
967
+ [sqrt(3) * sin(q0) / 2, 3 * cos(q0) / 4 + 1 / 4,
968
+ sqrt(3) * (1 - cos(q0)) / 4],
969
+ [-sin(q0) / 2, sqrt(3) * (1 - cos(q0)) / 4, cos(q0) / 4 + 3 / 4]])
970
+ # N.dcm(A) == N_R_A did not work
971
+ assert simplify(N.dcm(A) - N_R_A) == zeros(3)
972
+
973
+
974
+ def test_spherical_joint():
975
+ N, A, P, C = _generate_body()
976
+ q0, q1, q2, u0, u1, u2 = dynamicsymbols('q0:3_S, u0:3_S')
977
+ S = SphericalJoint('S', P, C)
978
+ assert S.name == 'S'
979
+ assert S.parent == P
980
+ assert S.child == C
981
+ assert S.coordinates == Matrix([q0, q1, q2])
982
+ assert S.speeds == Matrix([u0, u1, u2])
983
+ assert S.kdes == Matrix([u0 - q0.diff(t), u1 - q1.diff(t), u2 - q2.diff(t)])
984
+ assert S.child_point.pos_from(C.masscenter) == Vector(0)
985
+ assert S.parent_point.pos_from(P.masscenter) == Vector(0)
986
+ assert S.parent_point.pos_from(S.child_point) == Vector(0)
987
+ assert P.masscenter.pos_from(C.masscenter) == Vector(0)
988
+ assert C.masscenter.vel(N) == Vector(0)
989
+ assert N.ang_vel_in(A) == (-u0 * cos(q1) * cos(q2) - u1 * sin(q2)) * A.x + (
990
+ u0 * sin(q2) * cos(q1) - u1 * cos(q2)) * A.y + (
991
+ -u0 * sin(q1) - u2) * A.z
992
+ assert A.ang_vel_in(N) == (u0 * cos(q1) * cos(q2) + u1 * sin(q2)) * A.x + (
993
+ -u0 * sin(q2) * cos(q1) + u1 * cos(q2)) * A.y + (
994
+ u0 * sin(q1) + u2) * A.z
995
+ assert S.__str__() == 'SphericalJoint: S parent: P child: C'
996
+ assert S._rot_type == 'BODY'
997
+ assert S._rot_order == 123
998
+ assert S._amounts is None
999
+
1000
+
1001
+ def test_spherical_joint_speeds_as_derivative_terms():
1002
+ # This tests checks whether the system remains valid if the user chooses to
1003
+ # pass the derivative of the generalized coordinates as generalized speeds
1004
+ q0, q1, q2 = dynamicsymbols('q0:3')
1005
+ u0, u1, u2 = dynamicsymbols('q0:3', 1)
1006
+ N, A, P, C = _generate_body()
1007
+ S = SphericalJoint('S', P, C, coordinates=[q0, q1, q2], speeds=[u0, u1, u2])
1008
+ assert S.coordinates == Matrix([q0, q1, q2])
1009
+ assert S.speeds == Matrix([u0, u1, u2])
1010
+ assert S.kdes == Matrix([0, 0, 0])
1011
+ assert N.ang_vel_in(A) == (-u0 * cos(q1) * cos(q2) - u1 * sin(q2)) * A.x + (
1012
+ u0 * sin(q2) * cos(q1) - u1 * cos(q2)) * A.y + (
1013
+ -u0 * sin(q1) - u2) * A.z
1014
+
1015
+
1016
+ def test_spherical_joint_coords():
1017
+ q0s, q1s, q2s, u0s, u1s, u2s = dynamicsymbols('q0:3_S, u0:3_S')
1018
+ q0, q1, q2, q3, u0, u1, u2, u4 = dynamicsymbols('q0:4, u0:4')
1019
+ # Test coordinates as list
1020
+ N, A, P, C = _generate_body()
1021
+ S = SphericalJoint('S', P, C, [q0, q1, q2], [u0, u1, u2])
1022
+ assert S.coordinates == Matrix([q0, q1, q2])
1023
+ assert S.speeds == Matrix([u0, u1, u2])
1024
+ # Test coordinates as Matrix
1025
+ N, A, P, C = _generate_body()
1026
+ S = SphericalJoint('S', P, C, Matrix([q0, q1, q2]),
1027
+ Matrix([u0, u1, u2]))
1028
+ assert S.coordinates == Matrix([q0, q1, q2])
1029
+ assert S.speeds == Matrix([u0, u1, u2])
1030
+ # Test too few generalized coordinates
1031
+ N, A, P, C = _generate_body()
1032
+ raises(ValueError,
1033
+ lambda: SphericalJoint('S', P, C, Matrix([q0, q1]), Matrix([u0])))
1034
+ # Test too many generalized coordinates
1035
+ raises(ValueError, lambda: SphericalJoint(
1036
+ 'S', P, C, Matrix([q0, q1, q2, q3]), Matrix([u0, u1, u2])))
1037
+ raises(ValueError, lambda: SphericalJoint(
1038
+ 'S', P, C, Matrix([q0, q1, q2]), Matrix([u0, u1, u2, u4])))
1039
+
1040
+
1041
+ def test_spherical_joint_orient_body():
1042
+ q0, q1, q2, u0, u1, u2 = dynamicsymbols('q0:3, u0:3')
1043
+ N_R_A = Matrix([
1044
+ [-sin(q1), -sin(q2) * cos(q1), cos(q1) * cos(q2)],
1045
+ [-sin(q0) * cos(q1), sin(q0) * sin(q1) * sin(q2) - cos(q0) * cos(q2),
1046
+ -sin(q0) * sin(q1) * cos(q2) - sin(q2) * cos(q0)],
1047
+ [cos(q0) * cos(q1), -sin(q0) * cos(q2) - sin(q1) * sin(q2) * cos(q0),
1048
+ -sin(q0) * sin(q2) + sin(q1) * cos(q0) * cos(q2)]])
1049
+ N_w_A = Matrix([[-u0 * sin(q1) - u2],
1050
+ [-u0 * sin(q2) * cos(q1) + u1 * cos(q2)],
1051
+ [u0 * cos(q1) * cos(q2) + u1 * sin(q2)]])
1052
+ N_v_Co = Matrix([
1053
+ [-sqrt(2) * (u0 * cos(q2 + pi / 4) * cos(q1) + u1 * sin(q2 + pi / 4))],
1054
+ [-u0 * sin(q1) - u2], [-u0 * sin(q1) - u2]])
1055
+ # Test default rot_type='BODY', rot_order=123
1056
+ N, A, P, C, Pint, Cint = _generate_body(True)
1057
+ S = SphericalJoint('S', P, C, coordinates=[q0, q1, q2], speeds=[u0, u1, u2],
1058
+ parent_point=N.x + N.y, child_point=-A.y + A.z,
1059
+ parent_interframe=Pint, child_interframe=Cint,
1060
+ rot_type='body', rot_order=123)
1061
+ assert S._rot_type.upper() == 'BODY'
1062
+ assert S._rot_order == 123
1063
+ assert simplify(N.dcm(A) - N_R_A) == zeros(3)
1064
+ assert simplify(A.ang_vel_in(N).to_matrix(A) - N_w_A) == zeros(3, 1)
1065
+ assert simplify(C.masscenter.vel(N).to_matrix(A)) == N_v_Co
1066
+ # Test change of amounts
1067
+ N, A, P, C, Pint, Cint = _generate_body(True)
1068
+ S = SphericalJoint('S', P, C, coordinates=[q0, q1, q2], speeds=[u0, u1, u2],
1069
+ parent_point=N.x + N.y, child_point=-A.y + A.z,
1070
+ parent_interframe=Pint, child_interframe=Cint,
1071
+ rot_type='BODY', amounts=(q1, q0, q2), rot_order=123)
1072
+ switch_order = lambda expr: expr.xreplace(
1073
+ {q0: q1, q1: q0, q2: q2, u0: u1, u1: u0, u2: u2})
1074
+ assert S._rot_type.upper() == 'BODY'
1075
+ assert S._rot_order == 123
1076
+ assert simplify(N.dcm(A) - switch_order(N_R_A)) == zeros(3)
1077
+ assert simplify(A.ang_vel_in(N).to_matrix(A) - switch_order(N_w_A)
1078
+ ) == zeros(3, 1)
1079
+ assert simplify(C.masscenter.vel(N).to_matrix(A)) == switch_order(N_v_Co)
1080
+ # Test different rot_order
1081
+ N, A, P, C, Pint, Cint = _generate_body(True)
1082
+ S = SphericalJoint('S', P, C, coordinates=[q0, q1, q2], speeds=[u0, u1, u2],
1083
+ parent_point=N.x + N.y, child_point=-A.y + A.z,
1084
+ parent_interframe=Pint, child_interframe=Cint,
1085
+ rot_type='BodY', rot_order='yxz')
1086
+ assert S._rot_type.upper() == 'BODY'
1087
+ assert S._rot_order == 'yxz'
1088
+ assert simplify(N.dcm(A) - Matrix([
1089
+ [-sin(q0) * cos(q1), sin(q0) * sin(q1) * cos(q2) - sin(q2) * cos(q0),
1090
+ sin(q0) * sin(q1) * sin(q2) + cos(q0) * cos(q2)],
1091
+ [-sin(q1), -cos(q1) * cos(q2), -sin(q2) * cos(q1)],
1092
+ [cos(q0) * cos(q1), -sin(q0) * sin(q2) - sin(q1) * cos(q0) * cos(q2),
1093
+ sin(q0) * cos(q2) - sin(q1) * sin(q2) * cos(q0)]])) == zeros(3)
1094
+ assert simplify(A.ang_vel_in(N).to_matrix(A) - Matrix([
1095
+ [u0 * sin(q1) - u2], [u0 * cos(q1) * cos(q2) - u1 * sin(q2)],
1096
+ [u0 * sin(q2) * cos(q1) + u1 * cos(q2)]])) == zeros(3, 1)
1097
+ assert simplify(C.masscenter.vel(N).to_matrix(A)) == Matrix([
1098
+ [-sqrt(2) * (u0 * sin(q2 + pi / 4) * cos(q1) + u1 * cos(q2 + pi / 4))],
1099
+ [u0 * sin(q1) - u2], [u0 * sin(q1) - u2]])
1100
+
1101
+
1102
+ def test_spherical_joint_orient_space():
1103
+ q0, q1, q2, u0, u1, u2 = dynamicsymbols('q0:3, u0:3')
1104
+ N_R_A = Matrix([
1105
+ [-sin(q0) * sin(q2) - sin(q1) * cos(q0) * cos(q2),
1106
+ sin(q0) * sin(q1) * cos(q2) - sin(q2) * cos(q0), cos(q1) * cos(q2)],
1107
+ [-sin(q0) * cos(q2) + sin(q1) * sin(q2) * cos(q0),
1108
+ -sin(q0) * sin(q1) * sin(q2) - cos(q0) * cos(q2), -sin(q2) * cos(q1)],
1109
+ [cos(q0) * cos(q1), -sin(q0) * cos(q1), sin(q1)]])
1110
+ N_w_A = Matrix([
1111
+ [u1 * sin(q0) - u2 * cos(q0) * cos(q1)],
1112
+ [u1 * cos(q0) + u2 * sin(q0) * cos(q1)], [u0 - u2 * sin(q1)]])
1113
+ N_v_Co = Matrix([
1114
+ [u0 - u2 * sin(q1)], [u0 - u2 * sin(q1)],
1115
+ [sqrt(2) * (-u1 * sin(q0 + pi / 4) + u2 * cos(q0 + pi / 4) * cos(q1))]])
1116
+ # Test default rot_type='BODY', rot_order=123
1117
+ N, A, P, C, Pint, Cint = _generate_body(True)
1118
+ S = SphericalJoint('S', P, C, coordinates=[q0, q1, q2], speeds=[u0, u1, u2],
1119
+ parent_point=N.x + N.z, child_point=-A.x + A.y,
1120
+ parent_interframe=Pint, child_interframe=Cint,
1121
+ rot_type='space', rot_order=123)
1122
+ assert S._rot_type.upper() == 'SPACE'
1123
+ assert S._rot_order == 123
1124
+ assert simplify(N.dcm(A) - N_R_A) == zeros(3)
1125
+ assert simplify(A.ang_vel_in(N).to_matrix(A)) == N_w_A
1126
+ assert simplify(C.masscenter.vel(N).to_matrix(A)) == N_v_Co
1127
+ # Test change of amounts
1128
+ switch_order = lambda expr: expr.xreplace(
1129
+ {q0: q1, q1: q0, q2: q2, u0: u1, u1: u0, u2: u2})
1130
+ N, A, P, C, Pint, Cint = _generate_body(True)
1131
+ S = SphericalJoint('S', P, C, coordinates=[q0, q1, q2], speeds=[u0, u1, u2],
1132
+ parent_point=N.x + N.z, child_point=-A.x + A.y,
1133
+ parent_interframe=Pint, child_interframe=Cint,
1134
+ rot_type='SPACE', amounts=(q1, q0, q2), rot_order=123)
1135
+ assert S._rot_type.upper() == 'SPACE'
1136
+ assert S._rot_order == 123
1137
+ assert simplify(N.dcm(A) - switch_order(N_R_A)) == zeros(3)
1138
+ assert simplify(A.ang_vel_in(N).to_matrix(A)) == switch_order(N_w_A)
1139
+ assert simplify(C.masscenter.vel(N).to_matrix(A)) == switch_order(N_v_Co)
1140
+ # Test different rot_order
1141
+ N, A, P, C, Pint, Cint = _generate_body(True)
1142
+ S = SphericalJoint('S', P, C, coordinates=[q0, q1, q2], speeds=[u0, u1, u2],
1143
+ parent_point=N.x + N.z, child_point=-A.x + A.y,
1144
+ parent_interframe=Pint, child_interframe=Cint,
1145
+ rot_type='SPaCe', rot_order='zxy')
1146
+ assert S._rot_type.upper() == 'SPACE'
1147
+ assert S._rot_order == 'zxy'
1148
+ assert simplify(N.dcm(A) - Matrix([
1149
+ [-sin(q2) * cos(q1), -sin(q0) * cos(q2) + sin(q1) * sin(q2) * cos(q0),
1150
+ sin(q0) * sin(q1) * sin(q2) + cos(q0) * cos(q2)],
1151
+ [-sin(q1), -cos(q0) * cos(q1), -sin(q0) * cos(q1)],
1152
+ [cos(q1) * cos(q2), -sin(q0) * sin(q2) - sin(q1) * cos(q0) * cos(q2),
1153
+ -sin(q0) * sin(q1) * cos(q2) + sin(q2) * cos(q0)]]))
1154
+ assert simplify(A.ang_vel_in(N).to_matrix(A) - Matrix([
1155
+ [-u0 + u2 * sin(q1)], [-u1 * sin(q0) + u2 * cos(q0) * cos(q1)],
1156
+ [u1 * cos(q0) + u2 * sin(q0) * cos(q1)]])) == zeros(3, 1)
1157
+ assert simplify(C.masscenter.vel(N).to_matrix(A) - Matrix([
1158
+ [u1 * cos(q0) + u2 * sin(q0) * cos(q1)],
1159
+ [u1 * cos(q0) + u2 * sin(q0) * cos(q1)],
1160
+ [u0 + u1 * sin(q0) - u2 * sin(q1) -
1161
+ u2 * cos(q0) * cos(q1)]])) == zeros(3, 1)
1162
+
1163
+
1164
+ def test_weld_joint():
1165
+ _, _, P, C = _generate_body()
1166
+ W = WeldJoint('W', P, C)
1167
+ assert W.name == 'W'
1168
+ assert W.parent == P
1169
+ assert W.child == C
1170
+ assert W.coordinates == Matrix()
1171
+ assert W.speeds == Matrix()
1172
+ assert W.kdes == Matrix(1, 0, []).T
1173
+ assert P.frame.dcm(C.frame) == eye(3)
1174
+ assert W.child_point.pos_from(C.masscenter) == Vector(0)
1175
+ assert W.parent_point.pos_from(P.masscenter) == Vector(0)
1176
+ assert W.parent_point.pos_from(W.child_point) == Vector(0)
1177
+ assert P.masscenter.pos_from(C.masscenter) == Vector(0)
1178
+ assert C.masscenter.vel(P.frame) == Vector(0)
1179
+ assert P.frame.ang_vel_in(C.frame) == 0
1180
+ assert C.frame.ang_vel_in(P.frame) == 0
1181
+ assert W.__str__() == 'WeldJoint: W parent: P child: C'
1182
+
1183
+ N, A, P, C = _generate_body()
1184
+ l, m = symbols('l m')
1185
+ Pint = ReferenceFrame('P_int')
1186
+ Pint.orient_axis(P.frame, P.y, pi / 2)
1187
+ W = WeldJoint('W', P, C, parent_point=l * P.frame.x,
1188
+ child_point=m * C.frame.y, parent_interframe=Pint)
1189
+
1190
+ assert W.child_point.pos_from(C.masscenter) == m * C.frame.y
1191
+ assert W.parent_point.pos_from(P.masscenter) == l * P.frame.x
1192
+ assert W.parent_point.pos_from(W.child_point) == Vector(0)
1193
+ assert P.masscenter.pos_from(C.masscenter) == - l * N.x + m * A.y
1194
+ assert C.masscenter.vel(P.frame) == Vector(0)
1195
+ assert P.masscenter.vel(Pint) == Vector(0)
1196
+ assert C.frame.ang_vel_in(P.frame) == 0
1197
+ assert P.frame.ang_vel_in(C.frame) == 0
1198
+ assert P.x == A.z
1199
+
1200
+ with warns_deprecated_sympy():
1201
+ JointsMethod(P, W) # Tests #10770
1202
+
1203
+
1204
+ def test_deprecated_parent_child_axis():
1205
+ q, u = dynamicsymbols('q_J, u_J')
1206
+ N, A, P, C = _generate_body()
1207
+ with warns_deprecated_sympy():
1208
+ PinJoint('J', P, C, child_axis=-A.x)
1209
+ assert (-A.x).angle_between(N.x) == 0
1210
+ assert -A.x.express(N) == N.x
1211
+ assert A.dcm(N) == Matrix([[-1, 0, 0],
1212
+ [0, -cos(q), -sin(q)],
1213
+ [0, -sin(q), cos(q)]])
1214
+ assert A.ang_vel_in(N) == u * N.x
1215
+ assert A.ang_vel_in(N).magnitude() == sqrt(u ** 2)
1216
+
1217
+ N, A, P, C = _generate_body()
1218
+ with warns_deprecated_sympy():
1219
+ PrismaticJoint('J', P, C, parent_axis=P.x + P.y)
1220
+ assert (A.x).angle_between(N.x + N.y) == 0
1221
+ assert A.x.express(N) == (N.x + N.y) / sqrt(2)
1222
+ assert A.dcm(N) == Matrix([[sqrt(2) / 2, sqrt(2) / 2, 0],
1223
+ [-sqrt(2) / 2, sqrt(2) / 2, 0], [0, 0, 1]])
1224
+ assert A.ang_vel_in(N) == Vector(0)
1225
+
1226
+
1227
+ def test_deprecated_joint_pos():
1228
+ N, A, P, C = _generate_body()
1229
+ with warns_deprecated_sympy():
1230
+ pin = PinJoint('J', P, C, parent_joint_pos=N.x + N.y,
1231
+ child_joint_pos=C.y - C.z)
1232
+ assert pin.parent_point.pos_from(P.masscenter) == N.x + N.y
1233
+ assert pin.child_point.pos_from(C.masscenter) == C.y - C.z
1234
+
1235
+ N, A, P, C = _generate_body()
1236
+ with warns_deprecated_sympy():
1237
+ slider = PrismaticJoint('J', P, C, parent_joint_pos=N.z + N.y,
1238
+ child_joint_pos=C.y - C.x)
1239
+ assert slider.parent_point.pos_from(P.masscenter) == N.z + N.y
1240
+ assert slider.child_point.pos_from(C.masscenter) == C.y - C.x
lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_jointsmethod.py ADDED
@@ -0,0 +1,249 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sympy.core.function import expand
2
+ from sympy.core.symbol import symbols
3
+ from sympy.functions.elementary.trigonometric import (cos, sin)
4
+ from sympy.matrices.dense import Matrix
5
+ from sympy.simplify.trigsimp import trigsimp
6
+ from sympy.physics.mechanics import (
7
+ PinJoint, JointsMethod, RigidBody, Particle, Body, KanesMethod,
8
+ PrismaticJoint, LagrangesMethod, inertia)
9
+ from sympy.physics.vector import dynamicsymbols, ReferenceFrame
10
+ from sympy.testing.pytest import raises, warns_deprecated_sympy
11
+ from sympy import zeros
12
+ from sympy.utilities.lambdify import lambdify
13
+ from sympy.solvers.solvers import solve
14
+
15
+
16
+ t = dynamicsymbols._t # type: ignore
17
+
18
+
19
+ def test_jointsmethod():
20
+ with warns_deprecated_sympy():
21
+ P = Body('P')
22
+ C = Body('C')
23
+ Pin = PinJoint('P1', P, C)
24
+ C_ixx, g = symbols('C_ixx g')
25
+ q, u = dynamicsymbols('q_P1, u_P1')
26
+ P.apply_force(g*P.y)
27
+ with warns_deprecated_sympy():
28
+ method = JointsMethod(P, Pin)
29
+ assert method.frame == P.frame
30
+ assert method.bodies == [C, P]
31
+ assert method.loads == [(P.masscenter, g*P.frame.y)]
32
+ assert method.q == Matrix([q])
33
+ assert method.u == Matrix([u])
34
+ assert method.kdes == Matrix([u - q.diff()])
35
+ soln = method.form_eoms()
36
+ assert soln == Matrix([[-C_ixx*u.diff()]])
37
+ assert method.forcing_full == Matrix([[u], [0]])
38
+ assert method.mass_matrix_full == Matrix([[1, 0], [0, C_ixx]])
39
+ assert isinstance(method.method, KanesMethod)
40
+
41
+
42
+ def test_rigid_body_particle_compatibility():
43
+ l, m, g = symbols('l m g')
44
+ C = RigidBody('C')
45
+ b = Particle('b', mass=m)
46
+ b_frame = ReferenceFrame('b_frame')
47
+ q, u = dynamicsymbols('q u')
48
+ P = PinJoint('P', C, b, coordinates=q, speeds=u, child_interframe=b_frame,
49
+ child_point=-l * b_frame.x, joint_axis=C.z)
50
+ with warns_deprecated_sympy():
51
+ method = JointsMethod(C, P)
52
+ method.loads.append((b.masscenter, m * g * C.x))
53
+ method.form_eoms()
54
+ rhs = method.rhs()
55
+ assert rhs[1] == -g*sin(q)/l
56
+
57
+
58
+ def test_jointmethod_duplicate_coordinates_speeds():
59
+ with warns_deprecated_sympy():
60
+ P = Body('P')
61
+ C = Body('C')
62
+ T = Body('T')
63
+ q, u = dynamicsymbols('q u')
64
+ P1 = PinJoint('P1', P, C, q)
65
+ P2 = PrismaticJoint('P2', C, T, q)
66
+ with warns_deprecated_sympy():
67
+ raises(ValueError, lambda: JointsMethod(P, P1, P2))
68
+
69
+ P1 = PinJoint('P1', P, C, speeds=u)
70
+ P2 = PrismaticJoint('P2', C, T, speeds=u)
71
+ with warns_deprecated_sympy():
72
+ raises(ValueError, lambda: JointsMethod(P, P1, P2))
73
+
74
+ P1 = PinJoint('P1', P, C, q, u)
75
+ P2 = PrismaticJoint('P2', C, T, q, u)
76
+ with warns_deprecated_sympy():
77
+ raises(ValueError, lambda: JointsMethod(P, P1, P2))
78
+
79
+ def test_complete_simple_double_pendulum():
80
+ q1, q2 = dynamicsymbols('q1 q2')
81
+ u1, u2 = dynamicsymbols('u1 u2')
82
+ m, l, g = symbols('m l g')
83
+ with warns_deprecated_sympy():
84
+ C = Body('C') # ceiling
85
+ PartP = Body('P', mass=m)
86
+ PartR = Body('R', mass=m)
87
+ J1 = PinJoint('J1', C, PartP, speeds=u1, coordinates=q1,
88
+ child_point=-l*PartP.x, joint_axis=C.z)
89
+ J2 = PinJoint('J2', PartP, PartR, speeds=u2, coordinates=q2,
90
+ child_point=-l*PartR.x, joint_axis=PartP.z)
91
+
92
+ PartP.apply_force(m*g*C.x)
93
+ PartR.apply_force(m*g*C.x)
94
+
95
+ with warns_deprecated_sympy():
96
+ method = JointsMethod(C, J1, J2)
97
+ method.form_eoms()
98
+
99
+ assert expand(method.mass_matrix_full) == Matrix([[1, 0, 0, 0],
100
+ [0, 1, 0, 0],
101
+ [0, 0, 2*l**2*m*cos(q2) + 3*l**2*m, l**2*m*cos(q2) + l**2*m],
102
+ [0, 0, l**2*m*cos(q2) + l**2*m, l**2*m]])
103
+ assert trigsimp(method.forcing_full) == trigsimp(Matrix([[u1], [u2], [-g*l*m*(sin(q1 + q2) + sin(q1)) -
104
+ g*l*m*sin(q1) + l**2*m*(2*u1 + u2)*u2*sin(q2)],
105
+ [-g*l*m*sin(q1 + q2) - l**2*m*u1**2*sin(q2)]]))
106
+
107
+ def test_two_dof_joints():
108
+ q1, q2, u1, u2 = dynamicsymbols('q1 q2 u1 u2')
109
+ m, c1, c2, k1, k2 = symbols('m c1 c2 k1 k2')
110
+ with warns_deprecated_sympy():
111
+ W = Body('W')
112
+ B1 = Body('B1', mass=m)
113
+ B2 = Body('B2', mass=m)
114
+ J1 = PrismaticJoint('J1', W, B1, coordinates=q1, speeds=u1)
115
+ J2 = PrismaticJoint('J2', B1, B2, coordinates=q2, speeds=u2)
116
+ W.apply_force(k1*q1*W.x, reaction_body=B1)
117
+ W.apply_force(c1*u1*W.x, reaction_body=B1)
118
+ B1.apply_force(k2*q2*W.x, reaction_body=B2)
119
+ B1.apply_force(c2*u2*W.x, reaction_body=B2)
120
+ with warns_deprecated_sympy():
121
+ method = JointsMethod(W, J1, J2)
122
+ method.form_eoms()
123
+ MM = method.mass_matrix
124
+ forcing = method.forcing
125
+ rhs = MM.LUsolve(forcing)
126
+ assert expand(rhs[0]) == expand((-k1 * q1 - c1 * u1 + k2 * q2 + c2 * u2)/m)
127
+ assert expand(rhs[1]) == expand((k1 * q1 + c1 * u1 - 2 * k2 * q2 - 2 *
128
+ c2 * u2) / m)
129
+
130
+ def test_simple_pedulum():
131
+ l, m, g = symbols('l m g')
132
+ with warns_deprecated_sympy():
133
+ C = Body('C')
134
+ b = Body('b', mass=m)
135
+ q = dynamicsymbols('q')
136
+ P = PinJoint('P', C, b, speeds=q.diff(t), coordinates=q,
137
+ child_point=-l * b.x, joint_axis=C.z)
138
+ b.potential_energy = - m * g * l * cos(q)
139
+ with warns_deprecated_sympy():
140
+ method = JointsMethod(C, P)
141
+ method.form_eoms(LagrangesMethod)
142
+ rhs = method.rhs()
143
+ assert rhs[1] == -g*sin(q)/l
144
+
145
+ def test_chaos_pendulum():
146
+ #https://www.pydy.org/examples/chaos_pendulum.html
147
+ mA, mB, lA, lB, IAxx, IBxx, IByy, IBzz, g = symbols('mA, mB, lA, lB, IAxx, IBxx, IByy, IBzz, g')
148
+ theta, phi, omega, alpha = dynamicsymbols('theta phi omega alpha')
149
+
150
+ A = ReferenceFrame('A')
151
+ B = ReferenceFrame('B')
152
+
153
+ with warns_deprecated_sympy():
154
+ rod = Body('rod', mass=mA, frame=A,
155
+ central_inertia=inertia(A, IAxx, IAxx, 0))
156
+ plate = Body('plate', mass=mB, frame=B,
157
+ central_inertia=inertia(B, IBxx, IByy, IBzz))
158
+ C = Body('C')
159
+ J1 = PinJoint('J1', C, rod, coordinates=theta, speeds=omega,
160
+ child_point=-lA * rod.z, joint_axis=C.y)
161
+ J2 = PinJoint('J2', rod, plate, coordinates=phi, speeds=alpha,
162
+ parent_point=(lB - lA) * rod.z, joint_axis=rod.z)
163
+
164
+ rod.apply_force(mA*g*C.z)
165
+ plate.apply_force(mB*g*C.z)
166
+
167
+ with warns_deprecated_sympy():
168
+ method = JointsMethod(C, J1, J2)
169
+ method.form_eoms()
170
+
171
+ MM = method.mass_matrix
172
+ forcing = method.forcing
173
+ rhs = MM.LUsolve(forcing)
174
+ xd = (-2 * IBxx * alpha * omega * sin(phi) * cos(phi) + 2 * IByy * alpha * omega * sin(phi) *
175
+ cos(phi) - g * lA * mA * sin(theta) - g * lB * mB * sin(theta)) / (IAxx + IBxx *
176
+ sin(phi)**2 + IByy * cos(phi)**2 + lA**2 * mA + lB**2 * mB)
177
+ assert (rhs[0] - xd).simplify() == 0
178
+ xd = (IBxx - IByy) * omega**2 * sin(phi) * cos(phi) / IBzz
179
+ assert (rhs[1] - xd).simplify() == 0
180
+
181
+ def test_four_bar_linkage_with_manual_constraints():
182
+ q1, q2, q3, u1, u2, u3 = dynamicsymbols('q1:4, u1:4')
183
+ l1, l2, l3, l4, rho = symbols('l1:5, rho')
184
+
185
+ N = ReferenceFrame('N')
186
+ inertias = [inertia(N, 0, 0, rho * l ** 3 / 12) for l in (l1, l2, l3, l4)]
187
+ with warns_deprecated_sympy():
188
+ link1 = Body('Link1', frame=N, mass=rho * l1,
189
+ central_inertia=inertias[0])
190
+ link2 = Body('Link2', mass=rho * l2, central_inertia=inertias[1])
191
+ link3 = Body('Link3', mass=rho * l3, central_inertia=inertias[2])
192
+ link4 = Body('Link4', mass=rho * l4, central_inertia=inertias[3])
193
+
194
+ joint1 = PinJoint(
195
+ 'J1', link1, link2, coordinates=q1, speeds=u1, joint_axis=link1.z,
196
+ parent_point=l1 / 2 * link1.x, child_point=-l2 / 2 * link2.x)
197
+ joint2 = PinJoint(
198
+ 'J2', link2, link3, coordinates=q2, speeds=u2, joint_axis=link2.z,
199
+ parent_point=l2 / 2 * link2.x, child_point=-l3 / 2 * link3.x)
200
+ joint3 = PinJoint(
201
+ 'J3', link3, link4, coordinates=q3, speeds=u3, joint_axis=link3.z,
202
+ parent_point=l3 / 2 * link3.x, child_point=-l4 / 2 * link4.x)
203
+
204
+ loop = link4.masscenter.pos_from(link1.masscenter) \
205
+ + l1 / 2 * link1.x + l4 / 2 * link4.x
206
+
207
+ fh = Matrix([loop.dot(link1.x), loop.dot(link1.y)])
208
+
209
+ with warns_deprecated_sympy():
210
+ method = JointsMethod(link1, joint1, joint2, joint3)
211
+
212
+ t = dynamicsymbols._t
213
+ qdots = solve(method.kdes, [q1.diff(t), q2.diff(t), q3.diff(t)])
214
+ fhd = fh.diff(t).subs(qdots)
215
+
216
+ kane = KanesMethod(method.frame, q_ind=[q1], u_ind=[u1],
217
+ q_dependent=[q2, q3], u_dependent=[u2, u3],
218
+ kd_eqs=method.kdes, configuration_constraints=fh,
219
+ velocity_constraints=fhd, forcelist=method.loads,
220
+ bodies=method.bodies)
221
+ fr, frs = kane.kanes_equations()
222
+ assert fr == zeros(1)
223
+
224
+ # Numerically check the mass- and forcing-matrix
225
+ p = Matrix([l1, l2, l3, l4, rho])
226
+ q = Matrix([q1, q2, q3])
227
+ u = Matrix([u1, u2, u3])
228
+ eval_m = lambdify((q, p), kane.mass_matrix)
229
+ eval_f = lambdify((q, u, p), kane.forcing)
230
+ eval_fhd = lambdify((q, u, p), fhd)
231
+
232
+ p_vals = [0.13, 0.24, 0.21, 0.34, 997]
233
+ q_vals = [2.1, 0.6655470375077588, 2.527408138024188] # Satisfies fh
234
+ u_vals = [0.2, -0.17963733938852067, 0.1309060540601612] # Satisfies fhd
235
+ mass_check = Matrix([[3.452709815256506e+01, 7.003948798374735e+00,
236
+ -4.939690970641498e+00],
237
+ [-2.203792703880936e-14, 2.071702479957077e-01,
238
+ 2.842917573033711e-01],
239
+ [-1.300000000000123e-01, -8.836934896046506e-03,
240
+ 1.864891330060847e-01]])
241
+ forcing_check = Matrix([[-0.031211821321648],
242
+ [-0.00066022608181],
243
+ [0.001813559741243]])
244
+ eps = 1e-10
245
+ assert all(abs(x) < eps for x in eval_fhd(q_vals, u_vals, p_vals))
246
+ assert all(abs(x) < eps for x in
247
+ (Matrix(eval_m(q_vals, p_vals)) - mass_check))
248
+ assert all(abs(x) < eps for x in
249
+ (Matrix(eval_f(q_vals, u_vals, p_vals)) - forcing_check))
lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_kane.py ADDED
@@ -0,0 +1,553 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sympy import solve
2
+ from sympy import (cos, expand, Matrix, sin, symbols, tan, sqrt, S,
3
+ zeros, eye)
4
+ from sympy.simplify.simplify import simplify
5
+ from sympy.physics.mechanics import (dynamicsymbols, ReferenceFrame, Point,
6
+ RigidBody, KanesMethod, inertia, Particle,
7
+ dot, find_dynamicsymbols)
8
+ from sympy.testing.pytest import raises
9
+
10
+
11
+ def test_invalid_coordinates():
12
+ # Simple pendulum, but use symbols instead of dynamicsymbols
13
+ l, m, g = symbols('l m g')
14
+ q, u = symbols('q u') # Generalized coordinate
15
+ kd = [q.diff(dynamicsymbols._t) - u]
16
+ N, O = ReferenceFrame('N'), Point('O')
17
+ O.set_vel(N, 0)
18
+ P = Particle('P', Point('P'), m)
19
+ P.point.set_pos(O, l * (sin(q) * N.x - cos(q) * N.y))
20
+ F = (P.point, -m * g * N.y)
21
+ raises(ValueError, lambda: KanesMethod(N, [q], [u], kd, bodies=[P],
22
+ forcelist=[F]))
23
+
24
+
25
+ def test_one_dof():
26
+ # This is for a 1 dof spring-mass-damper case.
27
+ # It is described in more detail in the KanesMethod docstring.
28
+ q, u = dynamicsymbols('q u')
29
+ qd, ud = dynamicsymbols('q u', 1)
30
+ m, c, k = symbols('m c k')
31
+ N = ReferenceFrame('N')
32
+ P = Point('P')
33
+ P.set_vel(N, u * N.x)
34
+
35
+ kd = [qd - u]
36
+ FL = [(P, (-k * q - c * u) * N.x)]
37
+ pa = Particle('pa', P, m)
38
+ BL = [pa]
39
+
40
+ KM = KanesMethod(N, [q], [u], kd)
41
+ KM.kanes_equations(BL, FL)
42
+
43
+ assert KM.bodies == BL
44
+ assert KM.loads == FL
45
+
46
+ MM = KM.mass_matrix
47
+ forcing = KM.forcing
48
+ rhs = MM.inv() * forcing
49
+ assert expand(rhs[0]) == expand(-(q * k + u * c) / m)
50
+
51
+ assert simplify(KM.rhs() -
52
+ KM.mass_matrix_full.LUsolve(KM.forcing_full)) == zeros(2, 1)
53
+
54
+ assert (KM.linearize(A_and_B=True, )[0] == Matrix([[0, 1], [-k/m, -c/m]]))
55
+
56
+
57
+ def test_two_dof():
58
+ # This is for a 2 d.o.f., 2 particle spring-mass-damper.
59
+ # The first coordinate is the displacement of the first particle, and the
60
+ # second is the relative displacement between the first and second
61
+ # particles. Speeds are defined as the time derivatives of the particles.
62
+ q1, q2, u1, u2 = dynamicsymbols('q1 q2 u1 u2')
63
+ q1d, q2d, u1d, u2d = dynamicsymbols('q1 q2 u1 u2', 1)
64
+ m, c1, c2, k1, k2 = symbols('m c1 c2 k1 k2')
65
+ N = ReferenceFrame('N')
66
+ P1 = Point('P1')
67
+ P2 = Point('P2')
68
+ P1.set_vel(N, u1 * N.x)
69
+ P2.set_vel(N, (u1 + u2) * N.x)
70
+ # Note we multiply the kinematic equation by an arbitrary factor
71
+ # to test the implicit vs explicit kinematics attribute
72
+ kd = [q1d/2 - u1/2, 2*q2d - 2*u2]
73
+
74
+ # Now we create the list of forces, then assign properties to each
75
+ # particle, then create a list of all particles.
76
+ FL = [(P1, (-k1 * q1 - c1 * u1 + k2 * q2 + c2 * u2) * N.x), (P2, (-k2 *
77
+ q2 - c2 * u2) * N.x)]
78
+ pa1 = Particle('pa1', P1, m)
79
+ pa2 = Particle('pa2', P2, m)
80
+ BL = [pa1, pa2]
81
+
82
+ # Finally we create the KanesMethod object, specify the inertial frame,
83
+ # pass relevant information, and form Fr & Fr*. Then we calculate the mass
84
+ # matrix and forcing terms, and finally solve for the udots.
85
+ KM = KanesMethod(N, q_ind=[q1, q2], u_ind=[u1, u2], kd_eqs=kd)
86
+ KM.kanes_equations(BL, FL)
87
+ MM = KM.mass_matrix
88
+ forcing = KM.forcing
89
+ rhs = MM.inv() * forcing
90
+ assert expand(rhs[0]) == expand((-k1 * q1 - c1 * u1 + k2 * q2 + c2 * u2)/m)
91
+ assert expand(rhs[1]) == expand((k1 * q1 + c1 * u1 - 2 * k2 * q2 - 2 *
92
+ c2 * u2) / m)
93
+
94
+ # Check that the explicit form is the default and kinematic mass matrix is identity
95
+ assert KM.explicit_kinematics
96
+ assert KM.mass_matrix_kin == eye(2)
97
+
98
+ # Check that for the implicit form the mass matrix is not identity
99
+ KM.explicit_kinematics = False
100
+ assert KM.mass_matrix_kin == Matrix([[S(1)/2, 0], [0, 2]])
101
+
102
+ # Check that whether using implicit or explicit kinematics the RHS
103
+ # equations are consistent with the matrix form
104
+ for explicit_kinematics in [False, True]:
105
+ KM.explicit_kinematics = explicit_kinematics
106
+ assert simplify(KM.rhs() -
107
+ KM.mass_matrix_full.LUsolve(KM.forcing_full)) == zeros(4, 1)
108
+
109
+ # Make sure an error is raised if nonlinear kinematic differential
110
+ # equations are supplied.
111
+ kd = [q1d - u1**2, sin(q2d) - cos(u2)]
112
+ raises(ValueError, lambda: KanesMethod(N, q_ind=[q1, q2],
113
+ u_ind=[u1, u2], kd_eqs=kd))
114
+
115
+ def test_pend():
116
+ q, u = dynamicsymbols('q u')
117
+ qd, ud = dynamicsymbols('q u', 1)
118
+ m, l, g = symbols('m l g')
119
+ N = ReferenceFrame('N')
120
+ P = Point('P')
121
+ P.set_vel(N, -l * u * sin(q) * N.x + l * u * cos(q) * N.y)
122
+ kd = [qd - u]
123
+
124
+ FL = [(P, m * g * N.x)]
125
+ pa = Particle('pa', P, m)
126
+ BL = [pa]
127
+
128
+ KM = KanesMethod(N, [q], [u], kd)
129
+ KM.kanes_equations(BL, FL)
130
+ MM = KM.mass_matrix
131
+ forcing = KM.forcing
132
+ rhs = MM.inv() * forcing
133
+ rhs.simplify()
134
+ assert expand(rhs[0]) == expand(-g / l * sin(q))
135
+ assert simplify(KM.rhs() -
136
+ KM.mass_matrix_full.LUsolve(KM.forcing_full)) == zeros(2, 1)
137
+
138
+
139
+ def test_rolling_disc():
140
+ # Rolling Disc Example
141
+ # Here the rolling disc is formed from the contact point up, removing the
142
+ # need to introduce generalized speeds. Only 3 configuration and three
143
+ # speed variables are need to describe this system, along with the disc's
144
+ # mass and radius, and the local gravity (note that mass will drop out).
145
+ q1, q2, q3, u1, u2, u3 = dynamicsymbols('q1 q2 q3 u1 u2 u3')
146
+ q1d, q2d, q3d, u1d, u2d, u3d = dynamicsymbols('q1 q2 q3 u1 u2 u3', 1)
147
+ r, m, g = symbols('r m g')
148
+
149
+ # The kinematics are formed by a series of simple rotations. Each simple
150
+ # rotation creates a new frame, and the next rotation is defined by the new
151
+ # frame's basis vectors. This example uses a 3-1-2 series of rotations, or
152
+ # Z, X, Y series of rotations. Angular velocity for this is defined using
153
+ # the second frame's basis (the lean frame).
154
+ N = ReferenceFrame('N')
155
+ Y = N.orientnew('Y', 'Axis', [q1, N.z])
156
+ L = Y.orientnew('L', 'Axis', [q2, Y.x])
157
+ R = L.orientnew('R', 'Axis', [q3, L.y])
158
+ w_R_N_qd = R.ang_vel_in(N)
159
+ R.set_ang_vel(N, u1 * L.x + u2 * L.y + u3 * L.z)
160
+
161
+ # This is the translational kinematics. We create a point with no velocity
162
+ # in N; this is the contact point between the disc and ground. Next we form
163
+ # the position vector from the contact point to the disc's center of mass.
164
+ # Finally we form the velocity and acceleration of the disc.
165
+ C = Point('C')
166
+ C.set_vel(N, 0)
167
+ Dmc = C.locatenew('Dmc', r * L.z)
168
+ Dmc.v2pt_theory(C, N, R)
169
+
170
+ # This is a simple way to form the inertia dyadic.
171
+ I = inertia(L, m / 4 * r**2, m / 2 * r**2, m / 4 * r**2)
172
+
173
+ # Kinematic differential equations; how the generalized coordinate time
174
+ # derivatives relate to generalized speeds.
175
+ kd = [dot(R.ang_vel_in(N) - w_R_N_qd, uv) for uv in L]
176
+
177
+ # Creation of the force list; it is the gravitational force at the mass
178
+ # center of the disc. Then we create the disc by assigning a Point to the
179
+ # center of mass attribute, a ReferenceFrame to the frame attribute, and mass
180
+ # and inertia. Then we form the body list.
181
+ ForceList = [(Dmc, - m * g * Y.z)]
182
+ BodyD = RigidBody('BodyD', Dmc, R, m, (I, Dmc))
183
+ BodyList = [BodyD]
184
+
185
+ # Finally we form the equations of motion, using the same steps we did
186
+ # before. Specify inertial frame, supply generalized speeds, supply
187
+ # kinematic differential equation dictionary, compute Fr from the force
188
+ # list and Fr* from the body list, compute the mass matrix and forcing
189
+ # terms, then solve for the u dots (time derivatives of the generalized
190
+ # speeds).
191
+ KM = KanesMethod(N, q_ind=[q1, q2, q3], u_ind=[u1, u2, u3], kd_eqs=kd)
192
+ KM.kanes_equations(BodyList, ForceList)
193
+ MM = KM.mass_matrix
194
+ forcing = KM.forcing
195
+ rhs = MM.inv() * forcing
196
+ kdd = KM.kindiffdict()
197
+ rhs = rhs.subs(kdd)
198
+ rhs.simplify()
199
+ assert rhs.expand() == Matrix([(6*u2*u3*r - u3**2*r*tan(q2) +
200
+ 4*g*sin(q2))/(5*r), -2*u1*u3/3, u1*(-2*u2 + u3*tan(q2))]).expand()
201
+ assert simplify(KM.rhs() -
202
+ KM.mass_matrix_full.LUsolve(KM.forcing_full)) == zeros(6, 1)
203
+
204
+ # This code tests our output vs. benchmark values. When r=g=m=1, the
205
+ # critical speed (where all eigenvalues of the linearized equations are 0)
206
+ # is 1 / sqrt(3) for the upright case.
207
+ A = KM.linearize(A_and_B=True)[0]
208
+ A_upright = A.subs({r: 1, g: 1, m: 1}).subs({q1: 0, q2: 0, q3: 0, u1: 0, u3: 0})
209
+ import sympy
210
+ assert sympy.sympify(A_upright.subs({u2: 1 / sqrt(3)})).eigenvals() == {S.Zero: 6}
211
+
212
+
213
+ def test_aux():
214
+ # Same as above, except we have 2 auxiliary speeds for the ground contact
215
+ # point, which is known to be zero. In one case, we go through then
216
+ # substitute the aux. speeds in at the end (they are zero, as well as their
217
+ # derivative), in the other case, we use the built-in auxiliary speed part
218
+ # of KanesMethod. The equations from each should be the same.
219
+ q1, q2, q3, u1, u2, u3 = dynamicsymbols('q1 q2 q3 u1 u2 u3')
220
+ q1d, q2d, q3d, u1d, u2d, u3d = dynamicsymbols('q1 q2 q3 u1 u2 u3', 1)
221
+ u4, u5, f1, f2 = dynamicsymbols('u4, u5, f1, f2')
222
+ u4d, u5d = dynamicsymbols('u4, u5', 1)
223
+ r, m, g = symbols('r m g')
224
+
225
+ N = ReferenceFrame('N')
226
+ Y = N.orientnew('Y', 'Axis', [q1, N.z])
227
+ L = Y.orientnew('L', 'Axis', [q2, Y.x])
228
+ R = L.orientnew('R', 'Axis', [q3, L.y])
229
+ w_R_N_qd = R.ang_vel_in(N)
230
+ R.set_ang_vel(N, u1 * L.x + u2 * L.y + u3 * L.z)
231
+
232
+ C = Point('C')
233
+ C.set_vel(N, u4 * L.x + u5 * (Y.z ^ L.x))
234
+ Dmc = C.locatenew('Dmc', r * L.z)
235
+ Dmc.v2pt_theory(C, N, R)
236
+ Dmc.a2pt_theory(C, N, R)
237
+
238
+ I = inertia(L, m / 4 * r**2, m / 2 * r**2, m / 4 * r**2)
239
+
240
+ kd = [dot(R.ang_vel_in(N) - w_R_N_qd, uv) for uv in L]
241
+
242
+ ForceList = [(Dmc, - m * g * Y.z), (C, f1 * L.x + f2 * (Y.z ^ L.x))]
243
+ BodyD = RigidBody('BodyD', Dmc, R, m, (I, Dmc))
244
+ BodyList = [BodyD]
245
+
246
+ KM = KanesMethod(N, q_ind=[q1, q2, q3], u_ind=[u1, u2, u3, u4, u5],
247
+ kd_eqs=kd)
248
+ (fr, frstar) = KM.kanes_equations(BodyList, ForceList)
249
+ fr = fr.subs({u4d: 0, u5d: 0}).subs({u4: 0, u5: 0})
250
+ frstar = frstar.subs({u4d: 0, u5d: 0}).subs({u4: 0, u5: 0})
251
+
252
+ KM2 = KanesMethod(N, q_ind=[q1, q2, q3], u_ind=[u1, u2, u3], kd_eqs=kd,
253
+ u_auxiliary=[u4, u5])
254
+ (fr2, frstar2) = KM2.kanes_equations(BodyList, ForceList)
255
+ fr2 = fr2.subs({u4d: 0, u5d: 0}).subs({u4: 0, u5: 0})
256
+ frstar2 = frstar2.subs({u4d: 0, u5d: 0}).subs({u4: 0, u5: 0})
257
+
258
+ frstar.simplify()
259
+ frstar2.simplify()
260
+
261
+ assert (fr - fr2).expand() == Matrix([0, 0, 0, 0, 0])
262
+ assert (frstar - frstar2).expand() == Matrix([0, 0, 0, 0, 0])
263
+
264
+
265
+ def test_parallel_axis():
266
+ # This is for a 2 dof inverted pendulum on a cart.
267
+ # This tests the parallel axis code in KanesMethod. The inertia of the
268
+ # pendulum is defined about the hinge, not about the center of mass.
269
+
270
+ # Defining the constants and knowns of the system
271
+ gravity = symbols('g')
272
+ k, ls = symbols('k ls')
273
+ a, mA, mC = symbols('a mA mC')
274
+ F = dynamicsymbols('F')
275
+ Ix, Iy, Iz = symbols('Ix Iy Iz')
276
+
277
+ # Declaring the Generalized coordinates and speeds
278
+ q1, q2 = dynamicsymbols('q1 q2')
279
+ q1d, q2d = dynamicsymbols('q1 q2', 1)
280
+ u1, u2 = dynamicsymbols('u1 u2')
281
+ u1d, u2d = dynamicsymbols('u1 u2', 1)
282
+
283
+ # Creating reference frames
284
+ N = ReferenceFrame('N')
285
+ A = ReferenceFrame('A')
286
+
287
+ A.orient(N, 'Axis', [-q2, N.z])
288
+ A.set_ang_vel(N, -u2 * N.z)
289
+
290
+ # Origin of Newtonian reference frame
291
+ O = Point('O')
292
+
293
+ # Creating and Locating the positions of the cart, C, and the
294
+ # center of mass of the pendulum, A
295
+ C = O.locatenew('C', q1 * N.x)
296
+ Ao = C.locatenew('Ao', a * A.y)
297
+
298
+ # Defining velocities of the points
299
+ O.set_vel(N, 0)
300
+ C.set_vel(N, u1 * N.x)
301
+ Ao.v2pt_theory(C, N, A)
302
+ Cart = Particle('Cart', C, mC)
303
+ Pendulum = RigidBody('Pendulum', Ao, A, mA, (inertia(A, Ix, Iy, Iz), C))
304
+
305
+ # kinematical differential equations
306
+
307
+ kindiffs = [q1d - u1, q2d - u2]
308
+
309
+ bodyList = [Cart, Pendulum]
310
+
311
+ forceList = [(Ao, -N.y * gravity * mA),
312
+ (C, -N.y * gravity * mC),
313
+ (C, -N.x * k * (q1 - ls)),
314
+ (C, N.x * F)]
315
+
316
+ km = KanesMethod(N, [q1, q2], [u1, u2], kindiffs)
317
+ (fr, frstar) = km.kanes_equations(bodyList, forceList)
318
+ mm = km.mass_matrix_full
319
+ assert mm[3, 3] == Iz
320
+
321
+ def test_input_format():
322
+ # 1 dof problem from test_one_dof
323
+ q, u = dynamicsymbols('q u')
324
+ qd, ud = dynamicsymbols('q u', 1)
325
+ m, c, k = symbols('m c k')
326
+ N = ReferenceFrame('N')
327
+ P = Point('P')
328
+ P.set_vel(N, u * N.x)
329
+
330
+ kd = [qd - u]
331
+ FL = [(P, (-k * q - c * u) * N.x)]
332
+ pa = Particle('pa', P, m)
333
+ BL = [pa]
334
+
335
+ KM = KanesMethod(N, [q], [u], kd)
336
+ # test for input format kane.kanes_equations((body1, body2, particle1))
337
+ assert KM.kanes_equations(BL)[0] == Matrix([0])
338
+ # test for input format kane.kanes_equations(bodies=(body1, body 2), loads=(load1,load2))
339
+ assert KM.kanes_equations(bodies=BL, loads=None)[0] == Matrix([0])
340
+ # test for input format kane.kanes_equations(bodies=(body1, body 2), loads=None)
341
+ assert KM.kanes_equations(BL, loads=None)[0] == Matrix([0])
342
+ # test for input format kane.kanes_equations(bodies=(body1, body 2))
343
+ assert KM.kanes_equations(BL)[0] == Matrix([0])
344
+ # test for input format kane.kanes_equations(bodies=(body1, body2), loads=[])
345
+ assert KM.kanes_equations(BL, [])[0] == Matrix([0])
346
+ # test for error raised when a wrong force list (in this case a string) is provided
347
+ raises(ValueError, lambda: KM._form_fr('bad input'))
348
+
349
+ # 1 dof problem from test_one_dof with FL & BL in instance
350
+ KM = KanesMethod(N, [q], [u], kd, bodies=BL, forcelist=FL)
351
+ assert KM.kanes_equations()[0] == Matrix([-c*u - k*q])
352
+
353
+ # 2 dof problem from test_two_dof
354
+ q1, q2, u1, u2 = dynamicsymbols('q1 q2 u1 u2')
355
+ q1d, q2d, u1d, u2d = dynamicsymbols('q1 q2 u1 u2', 1)
356
+ m, c1, c2, k1, k2 = symbols('m c1 c2 k1 k2')
357
+ N = ReferenceFrame('N')
358
+ P1 = Point('P1')
359
+ P2 = Point('P2')
360
+ P1.set_vel(N, u1 * N.x)
361
+ P2.set_vel(N, (u1 + u2) * N.x)
362
+ kd = [q1d - u1, q2d - u2]
363
+
364
+ FL = ((P1, (-k1 * q1 - c1 * u1 + k2 * q2 + c2 * u2) * N.x), (P2, (-k2 *
365
+ q2 - c2 * u2) * N.x))
366
+ pa1 = Particle('pa1', P1, m)
367
+ pa2 = Particle('pa2', P2, m)
368
+ BL = (pa1, pa2)
369
+
370
+ KM = KanesMethod(N, q_ind=[q1, q2], u_ind=[u1, u2], kd_eqs=kd)
371
+ # test for input format
372
+ # kane.kanes_equations((body1, body2), (load1, load2))
373
+ KM.kanes_equations(BL, FL)
374
+ MM = KM.mass_matrix
375
+ forcing = KM.forcing
376
+ rhs = MM.inv() * forcing
377
+ assert expand(rhs[0]) == expand((-k1 * q1 - c1 * u1 + k2 * q2 + c2 * u2)/m)
378
+ assert expand(rhs[1]) == expand((k1 * q1 + c1 * u1 - 2 * k2 * q2 - 2 *
379
+ c2 * u2) / m)
380
+
381
+
382
+ def test_implicit_kinematics():
383
+ # Test that implicit kinematics can handle complicated
384
+ # equations that explicit form struggles with
385
+ # See https://github.com/sympy/sympy/issues/22626
386
+
387
+ # Inertial frame
388
+ NED = ReferenceFrame('NED')
389
+ NED_o = Point('NED_o')
390
+ NED_o.set_vel(NED, 0)
391
+
392
+ # body frame
393
+ q_att = dynamicsymbols('lambda_0:4', real=True)
394
+ B = NED.orientnew('B', 'Quaternion', q_att)
395
+
396
+ # Generalized coordinates
397
+ q_pos = dynamicsymbols('B_x:z')
398
+ B_cm = NED_o.locatenew('B_cm', q_pos[0]*B.x + q_pos[1]*B.y + q_pos[2]*B.z)
399
+
400
+ q_ind = q_att[1:] + q_pos
401
+ q_dep = [q_att[0]]
402
+
403
+ kinematic_eqs = []
404
+
405
+ # Generalized velocities
406
+ B_ang_vel = B.ang_vel_in(NED)
407
+ P, Q, R = dynamicsymbols('P Q R')
408
+ B.set_ang_vel(NED, P*B.x + Q*B.y + R*B.z)
409
+
410
+ B_ang_vel_kd = (B.ang_vel_in(NED) - B_ang_vel).simplify()
411
+
412
+ # Equating the two gives us the kinematic equation
413
+ kinematic_eqs += [
414
+ B_ang_vel_kd & B.x,
415
+ B_ang_vel_kd & B.y,
416
+ B_ang_vel_kd & B.z
417
+ ]
418
+
419
+ B_cm_vel = B_cm.vel(NED)
420
+ U, V, W = dynamicsymbols('U V W')
421
+ B_cm.set_vel(NED, U*B.x + V*B.y + W*B.z)
422
+
423
+ # Compute the velocity of the point using the two methods
424
+ B_ref_vel_kd = (B_cm.vel(NED) - B_cm_vel)
425
+
426
+ # taking dot product with unit vectors to get kinematic equations
427
+ # relating body coordinates and velocities
428
+
429
+ # Note, there is a choice to dot with NED.xyz here. That makes
430
+ # the implicit form have some bigger terms but is still fine, the
431
+ # explicit form still struggles though
432
+ kinematic_eqs += [
433
+ B_ref_vel_kd & B.x,
434
+ B_ref_vel_kd & B.y,
435
+ B_ref_vel_kd & B.z,
436
+ ]
437
+
438
+ u_ind = [U, V, W, P, Q, R]
439
+
440
+ # constraints
441
+ q_att_vec = Matrix(q_att)
442
+ config_cons = [(q_att_vec.T*q_att_vec)[0] - 1] #unit norm
443
+ kinematic_eqs = kinematic_eqs + [(q_att_vec.T * q_att_vec.diff())[0]]
444
+
445
+ try:
446
+ KM = KanesMethod(NED, q_ind, u_ind,
447
+ q_dependent= q_dep,
448
+ kd_eqs = kinematic_eqs,
449
+ configuration_constraints = config_cons,
450
+ velocity_constraints= [],
451
+ u_dependent= [], #no dependent speeds
452
+ u_auxiliary = [], # No auxiliary speeds
453
+ explicit_kinematics = False # implicit kinematics
454
+ )
455
+ except Exception as e:
456
+ raise e
457
+
458
+ # mass and inertia dyadic relative to CM
459
+ M_B = symbols('M_B')
460
+ J_B = inertia(B, *[S(f'J_B_{ax}')*(1 if ax[0] == ax[1] else -1)
461
+ for ax in ['xx', 'yy', 'zz', 'xy', 'yz', 'xz']])
462
+ J_B = J_B.subs({S('J_B_xy'): 0, S('J_B_yz'): 0})
463
+ RB = RigidBody('RB', B_cm, B, M_B, (J_B, B_cm))
464
+
465
+ rigid_bodies = [RB]
466
+ # Forces
467
+ force_list = [
468
+ #gravity pointing down
469
+ (RB.masscenter, RB.mass*S('g')*NED.z),
470
+ #generic forces and torques in body frame(inputs)
471
+ (RB.frame, dynamicsymbols('T_z')*B.z),
472
+ (RB.masscenter, dynamicsymbols('F_z')*B.z)
473
+ ]
474
+
475
+ KM.kanes_equations(rigid_bodies, force_list)
476
+
477
+ # Expecting implicit form to be less than 5% of the flops
478
+ n_ops_implicit = sum(
479
+ [x.count_ops() for x in KM.forcing_full] +
480
+ [x.count_ops() for x in KM.mass_matrix_full]
481
+ )
482
+ # Save implicit kinematic matrices to use later
483
+ mass_matrix_kin_implicit = KM.mass_matrix_kin
484
+ forcing_kin_implicit = KM.forcing_kin
485
+
486
+ KM.explicit_kinematics = True
487
+ n_ops_explicit = sum(
488
+ [x.count_ops() for x in KM.forcing_full] +
489
+ [x.count_ops() for x in KM.mass_matrix_full]
490
+ )
491
+ forcing_kin_explicit = KM.forcing_kin
492
+
493
+ assert n_ops_implicit / n_ops_explicit < .05
494
+
495
+ # Ideally we would check that implicit and explicit equations give the same result as done in test_one_dof
496
+ # But the whole raison-d'etre of the implicit equations is to deal with problems such
497
+ # as this one where the explicit form is too complicated to handle, especially the angular part
498
+ # (i.e. tests would be too slow)
499
+ # Instead, we check that the kinematic equations are correct using more fundamental tests:
500
+ #
501
+ # (1) that we recover the kinematic equations we have provided
502
+ assert (mass_matrix_kin_implicit * KM.q.diff() - forcing_kin_implicit) == Matrix(kinematic_eqs)
503
+
504
+ # (2) that rate of quaternions matches what 'textbook' solutions give
505
+ # Note that we just use the explicit kinematics for the linear velocities
506
+ # as they are not as complicated as the angular ones
507
+ qdot_candidate = forcing_kin_explicit
508
+
509
+ quat_dot_textbook = Matrix([
510
+ [0, -P, -Q, -R],
511
+ [P, 0, R, -Q],
512
+ [Q, -R, 0, P],
513
+ [R, Q, -P, 0],
514
+ ]) * q_att_vec / 2
515
+
516
+ # Again, if we don't use this "textbook" solution
517
+ # sympy will struggle to deal with the terms related to quaternion rates
518
+ # due to the number of operations involved
519
+ qdot_candidate[-1] = quat_dot_textbook[0] # lambda_0, note the [-1] as sympy's Kane puts the dependent coordinate last
520
+ qdot_candidate[0] = quat_dot_textbook[1] # lambda_1
521
+ qdot_candidate[1] = quat_dot_textbook[2] # lambda_2
522
+ qdot_candidate[2] = quat_dot_textbook[3] # lambda_3
523
+
524
+ # sub the config constraint in the candidate solution and compare to the implicit rhs
525
+ lambda_0_sol = solve(config_cons[0], q_att_vec[0])[1]
526
+ lhs_candidate = simplify(mass_matrix_kin_implicit * qdot_candidate).subs({q_att_vec[0]: lambda_0_sol})
527
+ assert lhs_candidate == forcing_kin_implicit
528
+
529
+ def test_issue_24887():
530
+ # Spherical pendulum
531
+ g, l, m, c = symbols('g l m c')
532
+ q1, q2, q3, u1, u2, u3 = dynamicsymbols('q1:4 u1:4')
533
+ N = ReferenceFrame('N')
534
+ A = ReferenceFrame('A')
535
+ A.orient_body_fixed(N, (q1, q2, q3), 'zxy')
536
+ N_w_A = A.ang_vel_in(N)
537
+ # A.set_ang_vel(N, u1 * A.x + u2 * A.y + u3 * A.z)
538
+ kdes = [N_w_A.dot(A.x) - u1, N_w_A.dot(A.y) - u2, N_w_A.dot(A.z) - u3]
539
+ O = Point('O')
540
+ O.set_vel(N, 0)
541
+ Po = O.locatenew('Po', -l * A.y)
542
+ Po.set_vel(A, 0)
543
+ P = Particle('P', Po, m)
544
+ kane = KanesMethod(N, [q1, q2, q3], [u1, u2, u3], kdes, bodies=[P],
545
+ forcelist=[(Po, -m * g * N.y)])
546
+ kane.kanes_equations()
547
+ expected_md = m * l ** 2 * Matrix([[1, 0, 0], [0, 0, 0], [0, 0, 1]])
548
+ expected_fd = Matrix([
549
+ [l*m*(g*(sin(q1)*sin(q3) - sin(q2)*cos(q1)*cos(q3)) - l*u2*u3)],
550
+ [0], [l*m*(-g*(sin(q1)*cos(q3) + sin(q2)*sin(q3)*cos(q1)) + l*u1*u2)]])
551
+ assert find_dynamicsymbols(kane.forcing).issubset({q1, q2, q3, u1, u2, u3})
552
+ assert simplify(kane.mass_matrix - expected_md) == zeros(3, 3)
553
+ assert simplify(kane.forcing - expected_fd) == zeros(3, 1)
lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_kane2.py ADDED
@@ -0,0 +1,464 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sympy import cos, Matrix, sin, zeros, tan, pi, symbols
2
+ from sympy.simplify.simplify import simplify
3
+ from sympy.simplify.trigsimp import trigsimp
4
+ from sympy.solvers.solvers import solve
5
+ from sympy.physics.mechanics import (cross, dot, dynamicsymbols,
6
+ find_dynamicsymbols, KanesMethod, inertia,
7
+ inertia_of_point_mass, Point,
8
+ ReferenceFrame, RigidBody)
9
+
10
+
11
+ def test_aux_dep():
12
+ # This test is about rolling disc dynamics, comparing the results found
13
+ # with KanesMethod to those found when deriving the equations "manually"
14
+ # with SymPy.
15
+ # The terms Fr, Fr*, and Fr*_steady are all compared between the two
16
+ # methods. Here, Fr*_steady refers to the generalized inertia forces for an
17
+ # equilibrium configuration.
18
+ # Note: comparing to the test of test_rolling_disc() in test_kane.py, this
19
+ # test also tests auxiliary speeds and configuration and motion constraints
20
+ #, seen in the generalized dependent coordinates q[3], and depend speeds
21
+ # u[3], u[4] and u[5].
22
+
23
+
24
+ # First, manual derivation of Fr, Fr_star, Fr_star_steady.
25
+
26
+ # Symbols for time and constant parameters.
27
+ # Symbols for contact forces: Fx, Fy, Fz.
28
+ t, r, m, g, I, J = symbols('t r m g I J')
29
+ Fx, Fy, Fz = symbols('Fx Fy Fz')
30
+
31
+ # Configuration variables and their time derivatives:
32
+ # q[0] -- yaw
33
+ # q[1] -- lean
34
+ # q[2] -- spin
35
+ # q[3] -- dot(-r*B.z, A.z) -- distance from ground plane to disc center in
36
+ # A.z direction
37
+ # Generalized speeds and their time derivatives:
38
+ # u[0] -- disc angular velocity component, disc fixed x direction
39
+ # u[1] -- disc angular velocity component, disc fixed y direction
40
+ # u[2] -- disc angular velocity component, disc fixed z direction
41
+ # u[3] -- disc velocity component, A.x direction
42
+ # u[4] -- disc velocity component, A.y direction
43
+ # u[5] -- disc velocity component, A.z direction
44
+ # Auxiliary generalized speeds:
45
+ # ua[0] -- contact point auxiliary generalized speed, A.x direction
46
+ # ua[1] -- contact point auxiliary generalized speed, A.y direction
47
+ # ua[2] -- contact point auxiliary generalized speed, A.z direction
48
+ q = dynamicsymbols('q:4')
49
+ qd = [qi.diff(t) for qi in q]
50
+ u = dynamicsymbols('u:6')
51
+ ud = [ui.diff(t) for ui in u]
52
+ ud_zero = dict(zip(ud, [0.]*len(ud)))
53
+ ua = dynamicsymbols('ua:3')
54
+ ua_zero = dict(zip(ua, [0.]*len(ua))) # noqa:F841
55
+
56
+ # Reference frames:
57
+ # Yaw intermediate frame: A.
58
+ # Lean intermediate frame: B.
59
+ # Disc fixed frame: C.
60
+ N = ReferenceFrame('N')
61
+ A = N.orientnew('A', 'Axis', [q[0], N.z])
62
+ B = A.orientnew('B', 'Axis', [q[1], A.x])
63
+ C = B.orientnew('C', 'Axis', [q[2], B.y])
64
+
65
+ # Angular velocity and angular acceleration of disc fixed frame
66
+ # u[0], u[1] and u[2] are generalized independent speeds.
67
+ C.set_ang_vel(N, u[0]*B.x + u[1]*B.y + u[2]*B.z)
68
+ C.set_ang_acc(N, C.ang_vel_in(N).diff(t, B)
69
+ + cross(B.ang_vel_in(N), C.ang_vel_in(N)))
70
+
71
+ # Velocity and acceleration of points:
72
+ # Disc-ground contact point: P.
73
+ # Center of disc: O, defined from point P with depend coordinate: q[3]
74
+ # u[3], u[4] and u[5] are generalized dependent speeds.
75
+ P = Point('P')
76
+ P.set_vel(N, ua[0]*A.x + ua[1]*A.y + ua[2]*A.z)
77
+ O = P.locatenew('O', q[3]*A.z + r*sin(q[1])*A.y)
78
+ O.set_vel(N, u[3]*A.x + u[4]*A.y + u[5]*A.z)
79
+ O.set_acc(N, O.vel(N).diff(t, A) + cross(A.ang_vel_in(N), O.vel(N)))
80
+
81
+ # Kinematic differential equations:
82
+ # Two equalities: one is w_c_n_qd = C.ang_vel_in(N) in three coordinates
83
+ # directions of B, for qd0, qd1 and qd2.
84
+ # the other is v_o_n_qd = O.vel(N) in A.z direction for qd3.
85
+ # Then, solve for dq/dt's in terms of u's: qd_kd.
86
+ w_c_n_qd = qd[0]*A.z + qd[1]*B.x + qd[2]*B.y
87
+ v_o_n_qd = O.pos_from(P).diff(t, A) + cross(A.ang_vel_in(N), O.pos_from(P))
88
+ kindiffs = Matrix([dot(w_c_n_qd - C.ang_vel_in(N), uv) for uv in B] +
89
+ [dot(v_o_n_qd - O.vel(N), A.z)])
90
+ qd_kd = solve(kindiffs, qd) # noqa:F841
91
+
92
+ # Values of generalized speeds during a steady turn for later substitution
93
+ # into the Fr_star_steady.
94
+ steady_conditions = solve(kindiffs.subs({qd[1] : 0, qd[3] : 0}), u)
95
+ steady_conditions.update({qd[1] : 0, qd[3] : 0})
96
+
97
+ # Partial angular velocities and velocities.
98
+ partial_w_C = [C.ang_vel_in(N).diff(ui, N) for ui in u + ua]
99
+ partial_v_O = [O.vel(N).diff(ui, N) for ui in u + ua]
100
+ partial_v_P = [P.vel(N).diff(ui, N) for ui in u + ua]
101
+
102
+ # Configuration constraint: f_c, the projection of radius r in A.z direction
103
+ # is q[3].
104
+ # Velocity constraints: f_v, for u3, u4 and u5.
105
+ # Acceleration constraints: f_a.
106
+ f_c = Matrix([dot(-r*B.z, A.z) - q[3]])
107
+ f_v = Matrix([dot(O.vel(N) - (P.vel(N) + cross(C.ang_vel_in(N),
108
+ O.pos_from(P))), ai).expand() for ai in A])
109
+ v_o_n = cross(C.ang_vel_in(N), O.pos_from(P))
110
+ a_o_n = v_o_n.diff(t, A) + cross(A.ang_vel_in(N), v_o_n)
111
+ f_a = Matrix([dot(O.acc(N) - a_o_n, ai) for ai in A]) # noqa:F841
112
+
113
+ # Solve for constraint equations in the form of
114
+ # u_dependent = A_rs * [u_i; u_aux].
115
+ # First, obtain constraint coefficient matrix: M_v * [u; ua] = 0;
116
+ # Second, taking u[0], u[1], u[2] as independent,
117
+ # taking u[3], u[4], u[5] as dependent,
118
+ # rearranging the matrix of M_v to be A_rs for u_dependent.
119
+ # Third, u_aux ==0 for u_dep, and resulting dictionary of u_dep_dict.
120
+ M_v = zeros(3, 9)
121
+ for i in range(3):
122
+ for j, ui in enumerate(u + ua):
123
+ M_v[i, j] = f_v[i].diff(ui)
124
+
125
+ M_v_i = M_v[:, :3]
126
+ M_v_d = M_v[:, 3:6]
127
+ M_v_aux = M_v[:, 6:]
128
+ M_v_i_aux = M_v_i.row_join(M_v_aux)
129
+ A_rs = - M_v_d.inv() * M_v_i_aux
130
+
131
+ u_dep = A_rs[:, :3] * Matrix(u[:3])
132
+ u_dep_dict = dict(zip(u[3:], u_dep))
133
+
134
+ # Active forces: F_O acting on point O; F_P acting on point P.
135
+ # Generalized active forces (unconstrained): Fr_u = F_point * pv_point.
136
+ F_O = m*g*A.z
137
+ F_P = Fx * A.x + Fy * A.y + Fz * A.z
138
+ Fr_u = Matrix([dot(F_O, pv_o) + dot(F_P, pv_p) for pv_o, pv_p in
139
+ zip(partial_v_O, partial_v_P)])
140
+
141
+ # Inertia force: R_star_O.
142
+ # Inertia of disc: I_C_O, where J is a inertia component about principal axis.
143
+ # Inertia torque: T_star_C.
144
+ # Generalized inertia forces (unconstrained): Fr_star_u.
145
+ R_star_O = -m*O.acc(N)
146
+ I_C_O = inertia(B, I, J, I)
147
+ T_star_C = -(dot(I_C_O, C.ang_acc_in(N)) \
148
+ + cross(C.ang_vel_in(N), dot(I_C_O, C.ang_vel_in(N))))
149
+ Fr_star_u = Matrix([dot(R_star_O, pv) + dot(T_star_C, pav) for pv, pav in
150
+ zip(partial_v_O, partial_w_C)])
151
+
152
+ # Form nonholonomic Fr: Fr_c, and nonholonomic Fr_star: Fr_star_c.
153
+ # Also, nonholonomic Fr_star in steady turning condition: Fr_star_steady.
154
+ Fr_c = Fr_u[:3, :].col_join(Fr_u[6:, :]) + A_rs.T * Fr_u[3:6, :]
155
+ Fr_star_c = Fr_star_u[:3, :].col_join(Fr_star_u[6:, :])\
156
+ + A_rs.T * Fr_star_u[3:6, :]
157
+ Fr_star_steady = Fr_star_c.subs(ud_zero).subs(u_dep_dict)\
158
+ .subs(steady_conditions).subs({q[3]: -r*cos(q[1])}).expand()
159
+
160
+
161
+ # Second, using KaneMethod in mechanics for fr, frstar and frstar_steady.
162
+
163
+ # Rigid Bodies: disc, with inertia I_C_O.
164
+ iner_tuple = (I_C_O, O)
165
+ disc = RigidBody('disc', O, C, m, iner_tuple)
166
+ bodyList = [disc]
167
+
168
+ # Generalized forces: Gravity: F_o; Auxiliary forces: F_p.
169
+ F_o = (O, F_O)
170
+ F_p = (P, F_P)
171
+ forceList = [F_o, F_p]
172
+
173
+ # KanesMethod.
174
+ kane = KanesMethod(
175
+ N, q_ind= q[:3], u_ind= u[:3], kd_eqs=kindiffs,
176
+ q_dependent=q[3:], configuration_constraints = f_c,
177
+ u_dependent=u[3:], velocity_constraints= f_v,
178
+ u_auxiliary=ua
179
+ )
180
+
181
+ # fr, frstar, frstar_steady and kdd(kinematic differential equations).
182
+ (fr, frstar)= kane.kanes_equations(bodyList, forceList)
183
+ frstar_steady = frstar.subs(ud_zero).subs(u_dep_dict).subs(steady_conditions)\
184
+ .subs({q[3]: -r*cos(q[1])}).expand()
185
+ kdd = kane.kindiffdict()
186
+
187
+ assert Matrix(Fr_c).expand() == fr.expand()
188
+ assert Matrix(Fr_star_c.subs(kdd)).expand() == frstar.expand()
189
+ # These Matrices have some Integer(0) and some Float(0). Running under
190
+ # SymEngine gives different types of zero.
191
+ assert (simplify(Matrix(Fr_star_steady).expand()).xreplace({0:0.0}) ==
192
+ simplify(frstar_steady.expand()).xreplace({0:0.0}))
193
+
194
+ syms_in_forcing = find_dynamicsymbols(kane.forcing)
195
+ for qdi in qd:
196
+ assert qdi not in syms_in_forcing
197
+
198
+
199
+ def test_non_central_inertia():
200
+ # This tests that the calculation of Fr* does not depend the point
201
+ # about which the inertia of a rigid body is defined. This test solves
202
+ # exercises 8.12, 8.17 from Kane 1985.
203
+
204
+ # Declare symbols
205
+ q1, q2, q3 = dynamicsymbols('q1:4')
206
+ q1d, q2d, q3d = dynamicsymbols('q1:4', level=1)
207
+ u1, u2, u3, u4, u5 = dynamicsymbols('u1:6')
208
+ u_prime, R, M, g, e, f, theta = symbols('u\' R, M, g, e, f, theta')
209
+ a, b, mA, mB, IA, J, K, t = symbols('a b mA mB IA J K t')
210
+ Q1, Q2, Q3 = symbols('Q1, Q2 Q3')
211
+ IA22, IA23, IA33 = symbols('IA22 IA23 IA33')
212
+
213
+ # Reference Frames
214
+ F = ReferenceFrame('F')
215
+ P = F.orientnew('P', 'axis', [-theta, F.y])
216
+ A = P.orientnew('A', 'axis', [q1, P.x])
217
+ A.set_ang_vel(F, u1*A.x + u3*A.z)
218
+ # define frames for wheels
219
+ B = A.orientnew('B', 'axis', [q2, A.z])
220
+ C = A.orientnew('C', 'axis', [q3, A.z])
221
+ B.set_ang_vel(A, u4 * A.z)
222
+ C.set_ang_vel(A, u5 * A.z)
223
+
224
+ # define points D, S*, Q on frame A and their velocities
225
+ pD = Point('D')
226
+ pD.set_vel(A, 0)
227
+ # u3 will not change v_D_F since wheels are still assumed to roll without slip.
228
+ pD.set_vel(F, u2 * A.y)
229
+
230
+ pS_star = pD.locatenew('S*', e*A.y)
231
+ pQ = pD.locatenew('Q', f*A.y - R*A.x)
232
+ for p in [pS_star, pQ]:
233
+ p.v2pt_theory(pD, F, A)
234
+
235
+ # masscenters of bodies A, B, C
236
+ pA_star = pD.locatenew('A*', a*A.y)
237
+ pB_star = pD.locatenew('B*', b*A.z)
238
+ pC_star = pD.locatenew('C*', -b*A.z)
239
+ for p in [pA_star, pB_star, pC_star]:
240
+ p.v2pt_theory(pD, F, A)
241
+
242
+ # points of B, C touching the plane P
243
+ pB_hat = pB_star.locatenew('B^', -R*A.x)
244
+ pC_hat = pC_star.locatenew('C^', -R*A.x)
245
+ pB_hat.v2pt_theory(pB_star, F, B)
246
+ pC_hat.v2pt_theory(pC_star, F, C)
247
+
248
+ # the velocities of B^, C^ are zero since B, C are assumed to roll without slip
249
+ kde = [q1d - u1, q2d - u4, q3d - u5]
250
+ vc = [dot(p.vel(F), A.y) for p in [pB_hat, pC_hat]]
251
+
252
+ # inertias of bodies A, B, C
253
+ # IA22, IA23, IA33 are not specified in the problem statement, but are
254
+ # necessary to define an inertia object. Although the values of
255
+ # IA22, IA23, IA33 are not known in terms of the variables given in the
256
+ # problem statement, they do not appear in the general inertia terms.
257
+ inertia_A = inertia(A, IA, IA22, IA33, 0, IA23, 0)
258
+ inertia_B = inertia(B, K, K, J)
259
+ inertia_C = inertia(C, K, K, J)
260
+
261
+ # define the rigid bodies A, B, C
262
+ rbA = RigidBody('rbA', pA_star, A, mA, (inertia_A, pA_star))
263
+ rbB = RigidBody('rbB', pB_star, B, mB, (inertia_B, pB_star))
264
+ rbC = RigidBody('rbC', pC_star, C, mB, (inertia_C, pC_star))
265
+
266
+ km = KanesMethod(F, q_ind=[q1, q2, q3], u_ind=[u1, u2], kd_eqs=kde,
267
+ u_dependent=[u4, u5], velocity_constraints=vc,
268
+ u_auxiliary=[u3])
269
+
270
+ forces = [(pS_star, -M*g*F.x), (pQ, Q1*A.x + Q2*A.y + Q3*A.z)]
271
+ bodies = [rbA, rbB, rbC]
272
+ fr, fr_star = km.kanes_equations(bodies, forces)
273
+ vc_map = solve(vc, [u4, u5])
274
+
275
+ # KanesMethod returns the negative of Fr, Fr* as defined in Kane1985.
276
+ fr_star_expected = Matrix([
277
+ -(IA + 2*J*b**2/R**2 + 2*K +
278
+ mA*a**2 + 2*mB*b**2) * u1.diff(t) - mA*a*u1*u2,
279
+ -(mA + 2*mB +2*J/R**2) * u2.diff(t) + mA*a*u1**2,
280
+ 0])
281
+ t = trigsimp(fr_star.subs(vc_map).subs({u3: 0})).doit().expand()
282
+ assert ((fr_star_expected - t).expand() == zeros(3, 1))
283
+
284
+ # define inertias of rigid bodies A, B, C about point D
285
+ # I_S/O = I_S/S* + I_S*/O
286
+ bodies2 = []
287
+ for rb, I_star in zip([rbA, rbB, rbC], [inertia_A, inertia_B, inertia_C]):
288
+ I = I_star + inertia_of_point_mass(rb.mass,
289
+ rb.masscenter.pos_from(pD),
290
+ rb.frame)
291
+ bodies2.append(RigidBody('', rb.masscenter, rb.frame, rb.mass,
292
+ (I, pD)))
293
+ fr2, fr_star2 = km.kanes_equations(bodies2, forces)
294
+
295
+ t = trigsimp(fr_star2.subs(vc_map).subs({u3: 0})).doit()
296
+ assert (fr_star_expected - t).expand() == zeros(3, 1)
297
+
298
+ def test_sub_qdot():
299
+ # This test solves exercises 8.12, 8.17 from Kane 1985 and defines
300
+ # some velocities in terms of q, qdot.
301
+
302
+ ## --- Declare symbols ---
303
+ q1, q2, q3 = dynamicsymbols('q1:4')
304
+ q1d, q2d, q3d = dynamicsymbols('q1:4', level=1)
305
+ u1, u2, u3 = dynamicsymbols('u1:4')
306
+ u_prime, R, M, g, e, f, theta = symbols('u\' R, M, g, e, f, theta')
307
+ a, b, mA, mB, IA, J, K, t = symbols('a b mA mB IA J K t')
308
+ IA22, IA23, IA33 = symbols('IA22 IA23 IA33')
309
+ Q1, Q2, Q3 = symbols('Q1 Q2 Q3')
310
+
311
+ # --- Reference Frames ---
312
+ F = ReferenceFrame('F')
313
+ P = F.orientnew('P', 'axis', [-theta, F.y])
314
+ A = P.orientnew('A', 'axis', [q1, P.x])
315
+ A.set_ang_vel(F, u1*A.x + u3*A.z)
316
+ # define frames for wheels
317
+ B = A.orientnew('B', 'axis', [q2, A.z])
318
+ C = A.orientnew('C', 'axis', [q3, A.z])
319
+
320
+ ## --- define points D, S*, Q on frame A and their velocities ---
321
+ pD = Point('D')
322
+ pD.set_vel(A, 0)
323
+ # u3 will not change v_D_F since wheels are still assumed to roll w/o slip
324
+ pD.set_vel(F, u2 * A.y)
325
+
326
+ pS_star = pD.locatenew('S*', e*A.y)
327
+ pQ = pD.locatenew('Q', f*A.y - R*A.x)
328
+ # masscenters of bodies A, B, C
329
+ pA_star = pD.locatenew('A*', a*A.y)
330
+ pB_star = pD.locatenew('B*', b*A.z)
331
+ pC_star = pD.locatenew('C*', -b*A.z)
332
+ for p in [pS_star, pQ, pA_star, pB_star, pC_star]:
333
+ p.v2pt_theory(pD, F, A)
334
+
335
+ # points of B, C touching the plane P
336
+ pB_hat = pB_star.locatenew('B^', -R*A.x)
337
+ pC_hat = pC_star.locatenew('C^', -R*A.x)
338
+ pB_hat.v2pt_theory(pB_star, F, B)
339
+ pC_hat.v2pt_theory(pC_star, F, C)
340
+
341
+ # --- relate qdot, u ---
342
+ # the velocities of B^, C^ are zero since B, C are assumed to roll w/o slip
343
+ kde = [dot(p.vel(F), A.y) for p in [pB_hat, pC_hat]]
344
+ kde += [u1 - q1d]
345
+ kde_map = solve(kde, [q1d, q2d, q3d])
346
+ for k, v in list(kde_map.items()):
347
+ kde_map[k.diff(t)] = v.diff(t)
348
+
349
+ # inertias of bodies A, B, C
350
+ # IA22, IA23, IA33 are not specified in the problem statement, but are
351
+ # necessary to define an inertia object. Although the values of
352
+ # IA22, IA23, IA33 are not known in terms of the variables given in the
353
+ # problem statement, they do not appear in the general inertia terms.
354
+ inertia_A = inertia(A, IA, IA22, IA33, 0, IA23, 0)
355
+ inertia_B = inertia(B, K, K, J)
356
+ inertia_C = inertia(C, K, K, J)
357
+
358
+ # define the rigid bodies A, B, C
359
+ rbA = RigidBody('rbA', pA_star, A, mA, (inertia_A, pA_star))
360
+ rbB = RigidBody('rbB', pB_star, B, mB, (inertia_B, pB_star))
361
+ rbC = RigidBody('rbC', pC_star, C, mB, (inertia_C, pC_star))
362
+
363
+ ## --- use kanes method ---
364
+ km = KanesMethod(F, [q1, q2, q3], [u1, u2], kd_eqs=kde, u_auxiliary=[u3])
365
+
366
+ forces = [(pS_star, -M*g*F.x), (pQ, Q1*A.x + Q2*A.y + Q3*A.z)]
367
+ bodies = [rbA, rbB, rbC]
368
+
369
+ # Q2 = -u_prime * u2 * Q1 / sqrt(u2**2 + f**2 * u1**2)
370
+ # -u_prime * R * u2 / sqrt(u2**2 + f**2 * u1**2) = R / Q1 * Q2
371
+ fr_expected = Matrix([
372
+ f*Q3 + M*g*e*sin(theta)*cos(q1),
373
+ Q2 + M*g*sin(theta)*sin(q1),
374
+ e*M*g*cos(theta) - Q1*f - Q2*R])
375
+ #Q1 * (f - u_prime * R * u2 / sqrt(u2**2 + f**2 * u1**2)))])
376
+ fr_star_expected = Matrix([
377
+ -(IA + 2*J*b**2/R**2 + 2*K +
378
+ mA*a**2 + 2*mB*b**2) * u1.diff(t) - mA*a*u1*u2,
379
+ -(mA + 2*mB +2*J/R**2) * u2.diff(t) + mA*a*u1**2,
380
+ 0])
381
+
382
+ fr, fr_star = km.kanes_equations(bodies, forces)
383
+ assert (fr.expand() == fr_expected.expand())
384
+ assert ((fr_star_expected - trigsimp(fr_star)).expand() == zeros(3, 1))
385
+
386
+ def test_sub_qdot2():
387
+ # This test solves exercises 8.3 from Kane 1985 and defines
388
+ # all velocities in terms of q, qdot. We check that the generalized active
389
+ # forces are correctly computed if u terms are only defined in the
390
+ # kinematic differential equations.
391
+ #
392
+ # This functionality was added in PR 8948. Without qdot/u substitution, the
393
+ # KanesMethod constructor will fail during the constraint initialization as
394
+ # the B matrix will be poorly formed and inversion of the dependent part
395
+ # will fail.
396
+
397
+ g, m, Px, Py, Pz, R, t = symbols('g m Px Py Pz R t')
398
+ q = dynamicsymbols('q:5')
399
+ qd = dynamicsymbols('q:5', level=1)
400
+ u = dynamicsymbols('u:5')
401
+
402
+ ## Define inertial, intermediate, and rigid body reference frames
403
+ A = ReferenceFrame('A')
404
+ B_prime = A.orientnew('B_prime', 'Axis', [q[0], A.z])
405
+ B = B_prime.orientnew('B', 'Axis', [pi/2 - q[1], B_prime.x])
406
+ C = B.orientnew('C', 'Axis', [q[2], B.z])
407
+
408
+ ## Define points of interest and their velocities
409
+ pO = Point('O')
410
+ pO.set_vel(A, 0)
411
+
412
+ # R is the point in plane H that comes into contact with disk C.
413
+ pR = pO.locatenew('R', q[3]*A.x + q[4]*A.y)
414
+ pR.set_vel(A, pR.pos_from(pO).diff(t, A))
415
+ pR.set_vel(B, 0)
416
+
417
+ # C^ is the point in disk C that comes into contact with plane H.
418
+ pC_hat = pR.locatenew('C^', 0)
419
+ pC_hat.set_vel(C, 0)
420
+
421
+ # C* is the point at the center of disk C.
422
+ pCs = pC_hat.locatenew('C*', R*B.y)
423
+ pCs.set_vel(C, 0)
424
+ pCs.set_vel(B, 0)
425
+
426
+ # calculate velocites of points C* and C^ in frame A
427
+ pCs.v2pt_theory(pR, A, B) # points C* and R are fixed in frame B
428
+ pC_hat.v2pt_theory(pCs, A, C) # points C* and C^ are fixed in frame C
429
+
430
+ ## Define forces on each point of the system
431
+ R_C_hat = Px*A.x + Py*A.y + Pz*A.z
432
+ R_Cs = -m*g*A.z
433
+ forces = [(pC_hat, R_C_hat), (pCs, R_Cs)]
434
+
435
+ ## Define kinematic differential equations
436
+ # let ui = omega_C_A & bi (i = 1, 2, 3)
437
+ # u4 = qd4, u5 = qd5
438
+ u_expr = [C.ang_vel_in(A) & uv for uv in B]
439
+ u_expr += qd[3:]
440
+ kde = [ui - e for ui, e in zip(u, u_expr)]
441
+ km1 = KanesMethod(A, q, u, kde)
442
+ fr1, _ = km1.kanes_equations([], forces)
443
+
444
+ ## Calculate generalized active forces if we impose the condition that the
445
+ # disk C is rolling without slipping
446
+ u_indep = u[:3]
447
+ u_dep = list(set(u) - set(u_indep))
448
+ vc = [pC_hat.vel(A) & uv for uv in [A.x, A.y]]
449
+ km2 = KanesMethod(A, q, u_indep, kde,
450
+ u_dependent=u_dep, velocity_constraints=vc)
451
+ fr2, _ = km2.kanes_equations([], forces)
452
+
453
+ fr1_expected = Matrix([
454
+ -R*g*m*sin(q[1]),
455
+ -R*(Px*cos(q[0]) + Py*sin(q[0]))*tan(q[1]),
456
+ R*(Px*cos(q[0]) + Py*sin(q[0])),
457
+ Px,
458
+ Py])
459
+ fr2_expected = Matrix([
460
+ -R*g*m*sin(q[1]),
461
+ 0,
462
+ 0])
463
+ assert (trigsimp(fr1.expand()) == trigsimp(fr1_expected.expand()))
464
+ assert (trigsimp(fr2.expand()) == trigsimp(fr2_expected.expand()))
lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_kane3.py ADDED
@@ -0,0 +1,315 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sympy.core.numbers import pi
2
+ from sympy.core.symbol import symbols
3
+ from sympy.functions.elementary.miscellaneous import sqrt
4
+ from sympy.functions.elementary.trigonometric import acos, sin, cos
5
+ from sympy.matrices.dense import Matrix
6
+ from sympy.physics.mechanics import (ReferenceFrame, dynamicsymbols,
7
+ KanesMethod, inertia, Point, RigidBody,
8
+ dot)
9
+ from sympy.testing.pytest import slow
10
+
11
+
12
+ @slow
13
+ def test_bicycle():
14
+ # Code to get equations of motion for a bicycle modeled as in:
15
+ # J.P Meijaard, Jim M Papadopoulos, Andy Ruina and A.L Schwab. Linearized
16
+ # dynamics equations for the balance and steer of a bicycle: a benchmark
17
+ # and review. Proceedings of The Royal Society (2007) 463, 1955-1982
18
+ # doi: 10.1098/rspa.2007.1857
19
+
20
+ # Note that this code has been crudely ported from Autolev, which is the
21
+ # reason for some of the unusual naming conventions. It was purposefully as
22
+ # similar as possible in order to aide debugging.
23
+
24
+ # Declare Coordinates & Speeds
25
+ # Simple definitions for qdots - qd = u
26
+ # Speeds are:
27
+ # - u1: yaw frame ang. rate
28
+ # - u2: roll frame ang. rate
29
+ # - u3: rear wheel frame ang. rate (spinning motion)
30
+ # - u4: frame ang. rate (pitching motion)
31
+ # - u5: steering frame ang. rate
32
+ # - u6: front wheel ang. rate (spinning motion)
33
+ # Wheel positions are ignorable coordinates, so they are not introduced.
34
+ q1, q2, q4, q5 = dynamicsymbols('q1 q2 q4 q5')
35
+ q1d, q2d, q4d, q5d = dynamicsymbols('q1 q2 q4 q5', 1)
36
+ u1, u2, u3, u4, u5, u6 = dynamicsymbols('u1 u2 u3 u4 u5 u6')
37
+ u1d, u2d, u3d, u4d, u5d, u6d = dynamicsymbols('u1 u2 u3 u4 u5 u6', 1)
38
+
39
+ # Declare System's Parameters
40
+ WFrad, WRrad, htangle, forkoffset = symbols('WFrad WRrad htangle forkoffset')
41
+ forklength, framelength, forkcg1 = symbols('forklength framelength forkcg1')
42
+ forkcg3, framecg1, framecg3, Iwr11 = symbols('forkcg3 framecg1 framecg3 Iwr11')
43
+ Iwr22, Iwf11, Iwf22, Iframe11 = symbols('Iwr22 Iwf11 Iwf22 Iframe11')
44
+ Iframe22, Iframe33, Iframe31, Ifork11 = symbols('Iframe22 Iframe33 Iframe31 Ifork11')
45
+ Ifork22, Ifork33, Ifork31, g = symbols('Ifork22 Ifork33 Ifork31 g')
46
+ mframe, mfork, mwf, mwr = symbols('mframe mfork mwf mwr')
47
+
48
+ # Set up reference frames for the system
49
+ # N - inertial
50
+ # Y - yaw
51
+ # R - roll
52
+ # WR - rear wheel, rotation angle is ignorable coordinate so not oriented
53
+ # Frame - bicycle frame
54
+ # TempFrame - statically rotated frame for easier reference inertia definition
55
+ # Fork - bicycle fork
56
+ # TempFork - statically rotated frame for easier reference inertia definition
57
+ # WF - front wheel, again posses a ignorable coordinate
58
+ N = ReferenceFrame('N')
59
+ Y = N.orientnew('Y', 'Axis', [q1, N.z])
60
+ R = Y.orientnew('R', 'Axis', [q2, Y.x])
61
+ Frame = R.orientnew('Frame', 'Axis', [q4 + htangle, R.y])
62
+ WR = ReferenceFrame('WR')
63
+ TempFrame = Frame.orientnew('TempFrame', 'Axis', [-htangle, Frame.y])
64
+ Fork = Frame.orientnew('Fork', 'Axis', [q5, Frame.x])
65
+ TempFork = Fork.orientnew('TempFork', 'Axis', [-htangle, Fork.y])
66
+ WF = ReferenceFrame('WF')
67
+
68
+ # Kinematics of the Bicycle First block of code is forming the positions of
69
+ # the relevant points
70
+ # rear wheel contact -> rear wheel mass center -> frame mass center +
71
+ # frame/fork connection -> fork mass center + front wheel mass center ->
72
+ # front wheel contact point
73
+ WR_cont = Point('WR_cont')
74
+ WR_mc = WR_cont.locatenew('WR_mc', WRrad * R.z)
75
+ Steer = WR_mc.locatenew('Steer', framelength * Frame.z)
76
+ Frame_mc = WR_mc.locatenew('Frame_mc', - framecg1 * Frame.x
77
+ + framecg3 * Frame.z)
78
+ Fork_mc = Steer.locatenew('Fork_mc', - forkcg1 * Fork.x
79
+ + forkcg3 * Fork.z)
80
+ WF_mc = Steer.locatenew('WF_mc', forklength * Fork.x + forkoffset * Fork.z)
81
+ WF_cont = WF_mc.locatenew('WF_cont', WFrad * (dot(Fork.y, Y.z) * Fork.y -
82
+ Y.z).normalize())
83
+
84
+ # Set the angular velocity of each frame.
85
+ # Angular accelerations end up being calculated automatically by
86
+ # differentiating the angular velocities when first needed.
87
+ # u1 is yaw rate
88
+ # u2 is roll rate
89
+ # u3 is rear wheel rate
90
+ # u4 is frame pitch rate
91
+ # u5 is fork steer rate
92
+ # u6 is front wheel rate
93
+ Y.set_ang_vel(N, u1 * Y.z)
94
+ R.set_ang_vel(Y, u2 * R.x)
95
+ WR.set_ang_vel(Frame, u3 * Frame.y)
96
+ Frame.set_ang_vel(R, u4 * Frame.y)
97
+ Fork.set_ang_vel(Frame, u5 * Fork.x)
98
+ WF.set_ang_vel(Fork, u6 * Fork.y)
99
+
100
+ # Form the velocities of the previously defined points, using the 2 - point
101
+ # theorem (written out by hand here). Accelerations again are calculated
102
+ # automatically when first needed.
103
+ WR_cont.set_vel(N, 0)
104
+ WR_mc.v2pt_theory(WR_cont, N, WR)
105
+ Steer.v2pt_theory(WR_mc, N, Frame)
106
+ Frame_mc.v2pt_theory(WR_mc, N, Frame)
107
+ Fork_mc.v2pt_theory(Steer, N, Fork)
108
+ WF_mc.v2pt_theory(Steer, N, Fork)
109
+ WF_cont.v2pt_theory(WF_mc, N, WF)
110
+
111
+ # Sets the inertias of each body. Uses the inertia frame to construct the
112
+ # inertia dyadics. Wheel inertias are only defined by principle moments of
113
+ # inertia, and are in fact constant in the frame and fork reference frames;
114
+ # it is for this reason that the orientations of the wheels does not need
115
+ # to be defined. The frame and fork inertias are defined in the 'Temp'
116
+ # frames which are fixed to the appropriate body frames; this is to allow
117
+ # easier input of the reference values of the benchmark paper. Note that
118
+ # due to slightly different orientations, the products of inertia need to
119
+ # have their signs flipped; this is done later when entering the numerical
120
+ # value.
121
+
122
+ Frame_I = (inertia(TempFrame, Iframe11, Iframe22, Iframe33, 0, 0, Iframe31), Frame_mc)
123
+ Fork_I = (inertia(TempFork, Ifork11, Ifork22, Ifork33, 0, 0, Ifork31), Fork_mc)
124
+ WR_I = (inertia(Frame, Iwr11, Iwr22, Iwr11), WR_mc)
125
+ WF_I = (inertia(Fork, Iwf11, Iwf22, Iwf11), WF_mc)
126
+
127
+ # Declaration of the RigidBody containers. ::
128
+
129
+ BodyFrame = RigidBody('BodyFrame', Frame_mc, Frame, mframe, Frame_I)
130
+ BodyFork = RigidBody('BodyFork', Fork_mc, Fork, mfork, Fork_I)
131
+ BodyWR = RigidBody('BodyWR', WR_mc, WR, mwr, WR_I)
132
+ BodyWF = RigidBody('BodyWF', WF_mc, WF, mwf, WF_I)
133
+
134
+ # The kinematic differential equations; they are defined quite simply. Each
135
+ # entry in this list is equal to zero.
136
+ kd = [q1d - u1, q2d - u2, q4d - u4, q5d - u5]
137
+
138
+ # The nonholonomic constraints are the velocity of the front wheel contact
139
+ # point dotted into the X, Y, and Z directions; the yaw frame is used as it
140
+ # is "closer" to the front wheel (1 less DCM connecting them). These
141
+ # constraints force the velocity of the front wheel contact point to be 0
142
+ # in the inertial frame; the X and Y direction constraints enforce a
143
+ # "no-slip" condition, and the Z direction constraint forces the front
144
+ # wheel contact point to not move away from the ground frame, essentially
145
+ # replicating the holonomic constraint which does not allow the frame pitch
146
+ # to change in an invalid fashion.
147
+
148
+ conlist_speed = [WF_cont.vel(N) & Y.x, WF_cont.vel(N) & Y.y, WF_cont.vel(N) & Y.z]
149
+
150
+ # The holonomic constraint is that the position from the rear wheel contact
151
+ # point to the front wheel contact point when dotted into the
152
+ # normal-to-ground plane direction must be zero; effectively that the front
153
+ # and rear wheel contact points are always touching the ground plane. This
154
+ # is actually not part of the dynamic equations, but instead is necessary
155
+ # for the lineraization process.
156
+
157
+ conlist_coord = [WF_cont.pos_from(WR_cont) & Y.z]
158
+
159
+ # The force list; each body has the appropriate gravitational force applied
160
+ # at its mass center.
161
+ FL = [(Frame_mc, -mframe * g * Y.z),
162
+ (Fork_mc, -mfork * g * Y.z),
163
+ (WF_mc, -mwf * g * Y.z),
164
+ (WR_mc, -mwr * g * Y.z)]
165
+ BL = [BodyFrame, BodyFork, BodyWR, BodyWF]
166
+
167
+
168
+ # The N frame is the inertial frame, coordinates are supplied in the order
169
+ # of independent, dependent coordinates, as are the speeds. The kinematic
170
+ # differential equation are also entered here. Here the dependent speeds
171
+ # are specified, in the same order they were provided in earlier, along
172
+ # with the non-holonomic constraints. The dependent coordinate is also
173
+ # provided, with the holonomic constraint. Again, this is only provided
174
+ # for the linearization process.
175
+
176
+ KM = KanesMethod(N, q_ind=[q1, q2, q5],
177
+ q_dependent=[q4], configuration_constraints=conlist_coord,
178
+ u_ind=[u2, u3, u5],
179
+ u_dependent=[u1, u4, u6], velocity_constraints=conlist_speed,
180
+ kd_eqs=kd,
181
+ constraint_solver="CRAMER")
182
+ (fr, frstar) = KM.kanes_equations(BL, FL)
183
+
184
+ # This is the start of entering in the numerical values from the benchmark
185
+ # paper to validate the eigen values of the linearized equations from this
186
+ # model to the reference eigen values. Look at the aforementioned paper for
187
+ # more information. Some of these are intermediate values, used to
188
+ # transform values from the paper into the coordinate systems used in this
189
+ # model.
190
+ PaperRadRear = 0.3
191
+ PaperRadFront = 0.35
192
+ HTA = (pi / 2 - pi / 10).evalf()
193
+ TrailPaper = 0.08
194
+ rake = (-(TrailPaper*sin(HTA)-(PaperRadFront*cos(HTA)))).evalf()
195
+ PaperWb = 1.02
196
+ PaperFrameCgX = 0.3
197
+ PaperFrameCgZ = 0.9
198
+ PaperForkCgX = 0.9
199
+ PaperForkCgZ = 0.7
200
+ FrameLength = (PaperWb*sin(HTA)-(rake-(PaperRadFront-PaperRadRear)*cos(HTA))).evalf()
201
+ FrameCGNorm = ((PaperFrameCgZ - PaperRadRear-(PaperFrameCgX/sin(HTA))*cos(HTA))*sin(HTA)).evalf()
202
+ FrameCGPar = (PaperFrameCgX / sin(HTA) + (PaperFrameCgZ - PaperRadRear - PaperFrameCgX / sin(HTA) * cos(HTA)) * cos(HTA)).evalf()
203
+ tempa = (PaperForkCgZ - PaperRadFront)
204
+ tempb = (PaperWb-PaperForkCgX)
205
+ tempc = (sqrt(tempa**2+tempb**2)).evalf()
206
+ PaperForkL = (PaperWb*cos(HTA)-(PaperRadFront-PaperRadRear)*sin(HTA)).evalf()
207
+ ForkCGNorm = (rake+(tempc * sin(pi/2-HTA-acos(tempa/tempc)))).evalf()
208
+ ForkCGPar = (tempc * cos((pi/2-HTA)-acos(tempa/tempc))-PaperForkL).evalf()
209
+
210
+ # Here is the final assembly of the numerical values. The symbol 'v' is the
211
+ # forward speed of the bicycle (a concept which only makes sense in the
212
+ # upright, static equilibrium case?). These are in a dictionary which will
213
+ # later be substituted in. Again the sign on the *product* of inertia
214
+ # values is flipped here, due to different orientations of coordinate
215
+ # systems.
216
+ v = symbols('v')
217
+ val_dict = {WFrad: PaperRadFront,
218
+ WRrad: PaperRadRear,
219
+ htangle: HTA,
220
+ forkoffset: rake,
221
+ forklength: PaperForkL,
222
+ framelength: FrameLength,
223
+ forkcg1: ForkCGPar,
224
+ forkcg3: ForkCGNorm,
225
+ framecg1: FrameCGNorm,
226
+ framecg3: FrameCGPar,
227
+ Iwr11: 0.0603,
228
+ Iwr22: 0.12,
229
+ Iwf11: 0.1405,
230
+ Iwf22: 0.28,
231
+ Ifork11: 0.05892,
232
+ Ifork22: 0.06,
233
+ Ifork33: 0.00708,
234
+ Ifork31: 0.00756,
235
+ Iframe11: 9.2,
236
+ Iframe22: 11,
237
+ Iframe33: 2.8,
238
+ Iframe31: -2.4,
239
+ mfork: 4,
240
+ mframe: 85,
241
+ mwf: 3,
242
+ mwr: 2,
243
+ g: 9.81,
244
+ q1: 0,
245
+ q2: 0,
246
+ q4: 0,
247
+ q5: 0,
248
+ u1: 0,
249
+ u2: 0,
250
+ u3: v / PaperRadRear,
251
+ u4: 0,
252
+ u5: 0,
253
+ u6: v / PaperRadFront}
254
+
255
+ # Linearizes the forcing vector; the equations are set up as MM udot =
256
+ # forcing, where MM is the mass matrix, udot is the vector representing the
257
+ # time derivatives of the generalized speeds, and forcing is a vector which
258
+ # contains both external forcing terms and internal forcing terms, such as
259
+ # centripital or coriolis forces. This actually returns a matrix with as
260
+ # many rows as *total* coordinates and speeds, but only as many columns as
261
+ # independent coordinates and speeds.
262
+
263
+ A, B, _ = KM.linearize(
264
+ A_and_B=True,
265
+ op_point={
266
+ # Operating points for the accelerations are required for the
267
+ # linearizer to eliminate u' terms showing up in the coefficient
268
+ # matrices.
269
+ u1.diff(): 0,
270
+ u2.diff(): 0,
271
+ u3.diff(): 0,
272
+ u4.diff(): 0,
273
+ u5.diff(): 0,
274
+ u6.diff(): 0,
275
+ u1: 0,
276
+ u2: 0,
277
+ u3: v / PaperRadRear,
278
+ u4: 0,
279
+ u5: 0,
280
+ u6: v / PaperRadFront,
281
+ q1: 0,
282
+ q2: 0,
283
+ q4: 0,
284
+ q5: 0,
285
+ },
286
+ linear_solver="CRAMER",
287
+ )
288
+ # As mentioned above, the size of the linearized forcing terms is expanded
289
+ # to include both q's and u's, so the mass matrix must have this done as
290
+ # well. This will likely be changed to be part of the linearized process,
291
+ # for future reference.
292
+ A_s = A.xreplace(val_dict)
293
+ B_s = B.xreplace(val_dict)
294
+
295
+ A_s = A_s.evalf()
296
+ B_s = B_s.evalf()
297
+
298
+ # Finally, we construct an "A" matrix for the form xdot = A x (x being the
299
+ # state vector, although in this case, the sizes are a little off). The
300
+ # following line extracts only the minimum entries required for eigenvalue
301
+ # analysis, which correspond to rows and columns for lean, steer, lean
302
+ # rate, and steer rate.
303
+ A = A_s.extract([1, 2, 3, 5], [1, 2, 3, 5])
304
+
305
+ # Precomputed for comparison
306
+ Res = Matrix([[ 0, 0, 1.0, 0],
307
+ [ 0, 0, 0, 1.0],
308
+ [9.48977444677355, -0.891197738059089*v**2 - 0.571523173729245, -0.105522449805691*v, -0.330515398992311*v],
309
+ [11.7194768719633, -1.97171508499972*v**2 + 30.9087533932407, 3.67680523332152*v, -3.08486552743311*v]])
310
+
311
+ # Actual eigenvalue comparison
312
+ eps = 1.e-12
313
+ for i in range(6):
314
+ error = Res.subs(v, i) - A.subs(v, i)
315
+ assert all(abs(x) < eps for x in error)
lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_kane4.py ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sympy import (cos, sin, Matrix, symbols)
2
+ from sympy.physics.mechanics import (dynamicsymbols, ReferenceFrame, Point,
3
+ KanesMethod, Particle)
4
+
5
+ def test_replace_qdots_in_force():
6
+ # Test PR 16700 "Replaces qdots with us in force-list in kanes.py"
7
+ # The new functionality allows one to specify forces in qdots which will
8
+ # automatically be replaced with u:s which are defined by the kde supplied
9
+ # to KanesMethod. The test case is the double pendulum with interacting
10
+ # forces in the example of chapter 4.7 "CONTRIBUTING INTERACTION FORCES"
11
+ # in Ref. [1]. Reference list at end test function.
12
+
13
+ q1, q2 = dynamicsymbols('q1, q2')
14
+ qd1, qd2 = dynamicsymbols('q1, q2', level=1)
15
+ u1, u2 = dynamicsymbols('u1, u2')
16
+
17
+ l, m = symbols('l, m')
18
+
19
+ N = ReferenceFrame('N') # Inertial frame
20
+ A = N.orientnew('A', 'Axis', (q1, N.z)) # Rod A frame
21
+ B = A.orientnew('B', 'Axis', (q2, N.z)) # Rod B frame
22
+
23
+ O = Point('O') # Origo
24
+ O.set_vel(N, 0)
25
+
26
+ P = O.locatenew('P', ( l * A.x )) # Point @ end of rod A
27
+ P.v2pt_theory(O, N, A)
28
+
29
+ Q = P.locatenew('Q', ( l * B.x )) # Point @ end of rod B
30
+ Q.v2pt_theory(P, N, B)
31
+
32
+ Ap = Particle('Ap', P, m)
33
+ Bp = Particle('Bp', Q, m)
34
+
35
+ # The forces are specified below. sigma is the torsional spring stiffness
36
+ # and delta is the viscous damping coefficient acting between the two
37
+ # bodies. Here, we specify the viscous damper as function of qdots prior
38
+ # forming the kde. In more complex systems it not might be obvious which
39
+ # kde is most efficient, why it is convenient to specify viscous forces in
40
+ # qdots independently of the kde.
41
+ sig, delta = symbols('sigma, delta')
42
+ Ta = (sig * q2 + delta * qd2) * N.z
43
+ forces = [(A, Ta), (B, -Ta)]
44
+
45
+ # Try different kdes.
46
+ kde1 = [u1 - qd1, u2 - qd2]
47
+ kde2 = [u1 - qd1, u2 - (qd1 + qd2)]
48
+
49
+ KM1 = KanesMethod(N, [q1, q2], [u1, u2], kd_eqs=kde1)
50
+ fr1, fstar1 = KM1.kanes_equations([Ap, Bp], forces)
51
+
52
+ KM2 = KanesMethod(N, [q1, q2], [u1, u2], kd_eqs=kde2)
53
+ fr2, fstar2 = KM2.kanes_equations([Ap, Bp], forces)
54
+
55
+ # Check EOM for KM2:
56
+ # Mass and force matrix from p.6 in Ref. [2] with added forces from
57
+ # example of chapter 4.7 in [1] and without gravity.
58
+ forcing_matrix_expected = Matrix( [ [ m * l**2 * sin(q2) * u2**2 + sig * q2
59
+ + delta * (u2 - u1)],
60
+ [ m * l**2 * sin(q2) * -u1**2 - sig * q2
61
+ - delta * (u2 - u1)] ] )
62
+ mass_matrix_expected = Matrix( [ [ 2 * m * l**2, m * l**2 * cos(q2) ],
63
+ [ m * l**2 * cos(q2), m * l**2 ] ] )
64
+
65
+ assert (KM2.mass_matrix.expand() == mass_matrix_expected.expand())
66
+ assert (KM2.forcing.expand() == forcing_matrix_expected.expand())
67
+
68
+ # Check fr1 with reference fr_expected from [1] with u:s instead of qdots.
69
+ fr1_expected = Matrix([ 0, -(sig*q2 + delta * u2) ])
70
+ assert fr1.expand() == fr1_expected.expand()
71
+
72
+ # Check fr2
73
+ fr2_expected = Matrix([sig * q2 + delta * (u2 - u1),
74
+ - sig * q2 - delta * (u2 - u1)])
75
+ assert fr2.expand() == fr2_expected.expand()
76
+
77
+ # Specifying forces in u:s should stay the same:
78
+ Ta = (sig * q2 + delta * u2) * N.z
79
+ forces = [(A, Ta), (B, -Ta)]
80
+ KM1 = KanesMethod(N, [q1, q2], [u1, u2], kd_eqs=kde1)
81
+ fr1, fstar1 = KM1.kanes_equations([Ap, Bp], forces)
82
+
83
+ assert fr1.expand() == fr1_expected.expand()
84
+
85
+ Ta = (sig * q2 + delta * (u2-u1)) * N.z
86
+ forces = [(A, Ta), (B, -Ta)]
87
+ KM2 = KanesMethod(N, [q1, q2], [u1, u2], kd_eqs=kde2)
88
+ fr2, fstar2 = KM2.kanes_equations([Ap, Bp], forces)
89
+
90
+ assert fr2.expand() == fr2_expected.expand()
91
+
92
+ # Test if we have a qubic qdot force:
93
+ Ta = (sig * q2 + delta * qd2**3) * N.z
94
+ forces = [(A, Ta), (B, -Ta)]
95
+
96
+ KM1 = KanesMethod(N, [q1, q2], [u1, u2], kd_eqs=kde1)
97
+ fr1, fstar1 = KM1.kanes_equations([Ap, Bp], forces)
98
+
99
+ fr1_cubic_expected = Matrix([ 0, -(sig*q2 + delta * u2**3) ])
100
+
101
+ assert fr1.expand() == fr1_cubic_expected.expand()
102
+
103
+ KM2 = KanesMethod(N, [q1, q2], [u1, u2], kd_eqs=kde2)
104
+ fr2, fstar2 = KM2.kanes_equations([Ap, Bp], forces)
105
+
106
+ fr2_cubic_expected = Matrix([sig * q2 + delta * (u2 - u1)**3,
107
+ - sig * q2 - delta * (u2 - u1)**3])
108
+
109
+ assert fr2.expand() == fr2_cubic_expected.expand()
110
+
111
+ # References:
112
+ # [1] T.R. Kane, D. a Levinson, Dynamics Theory and Applications, 2005.
113
+ # [2] Arun K Banerjee, Flexible Multibody Dynamics:Efficient Formulations
114
+ # and Applications, John Wiley and Sons, Ltd, 2016.
115
+ # doi:http://dx.doi.org/10.1002/9781119015635.
lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_kane5.py ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sympy import (zeros, Matrix, symbols, lambdify, sqrt, pi,
2
+ simplify)
3
+ from sympy.physics.mechanics import (dynamicsymbols, cross, inertia, RigidBody,
4
+ ReferenceFrame, KanesMethod)
5
+
6
+
7
+ def _create_rolling_disc():
8
+ # Define symbols and coordinates
9
+ t = dynamicsymbols._t
10
+ q1, q2, q3, q4, q5, u1, u2, u3, u4, u5 = dynamicsymbols('q1:6 u1:6')
11
+ g, r, m = symbols('g r m')
12
+ # Define bodies and frames
13
+ ground = RigidBody('ground')
14
+ disc = RigidBody('disk', mass=m)
15
+ disc.inertia = (m * r ** 2 / 4 * inertia(disc.frame, 1, 2, 1),
16
+ disc.masscenter)
17
+ ground.masscenter.set_vel(ground.frame, 0)
18
+ disc.masscenter.set_vel(disc.frame, 0)
19
+ int_frame = ReferenceFrame('int_frame')
20
+ # Orient frames
21
+ int_frame.orient_body_fixed(ground.frame, (q1, q2, 0), 'zxy')
22
+ disc.frame.orient_axis(int_frame, int_frame.y, q3)
23
+ g_w_d = disc.frame.ang_vel_in(ground.frame)
24
+ disc.frame.set_ang_vel(ground.frame,
25
+ u1 * disc.x + u2 * disc.y + u3 * disc.z)
26
+ # Define points
27
+ cp = ground.masscenter.locatenew('contact_point',
28
+ q4 * ground.x + q5 * ground.y)
29
+ cp.set_vel(ground.frame, u4 * ground.x + u5 * ground.y)
30
+ disc.masscenter.set_pos(cp, r * int_frame.z)
31
+ disc.masscenter.set_vel(ground.frame, cross(
32
+ disc.frame.ang_vel_in(ground.frame), disc.masscenter.pos_from(cp)))
33
+ # Define kinematic differential equations
34
+ kdes = [g_w_d.dot(disc.x) - u1, g_w_d.dot(disc.y) - u2,
35
+ g_w_d.dot(disc.z) - u3, q4.diff(t) - u4, q5.diff(t) - u5]
36
+ # Define nonholonomic constraints
37
+ v0 = cp.vel(ground.frame) + cross(
38
+ disc.frame.ang_vel_in(int_frame), cp.pos_from(disc.masscenter))
39
+ fnh = [v0.dot(ground.x), v0.dot(ground.y)]
40
+ # Define loads
41
+ loads = [(disc.masscenter, -disc.mass * g * ground.z)]
42
+ bodies = [disc]
43
+ return {
44
+ 'frame': ground.frame,
45
+ 'q_ind': [q1, q2, q3, q4, q5],
46
+ 'u_ind': [u1, u2, u3],
47
+ 'u_dep': [u4, u5],
48
+ 'kdes': kdes,
49
+ 'fnh': fnh,
50
+ 'bodies': bodies,
51
+ 'loads': loads
52
+ }
53
+
54
+
55
+ def _verify_rolling_disc_numerically(kane, all_zero=False):
56
+ q, u, p = dynamicsymbols('q1:6'), dynamicsymbols('u1:6'), symbols('g r m')
57
+ eval_sys = lambdify((q, u, p), (kane.mass_matrix_full, kane.forcing_full),
58
+ cse=True)
59
+ solve_sys = lambda q, u, p: Matrix.LUsolve(
60
+ *(Matrix(mat) for mat in eval_sys(q, u, p)))
61
+ solve_u_dep = lambdify((q, u[:3], p), kane._Ars * Matrix(u[:3]), cse=True)
62
+ eps = 1e-10
63
+ p_vals = (9.81, 0.26, 3.43)
64
+ # First numeric test
65
+ q_vals = (0.3, 0.1, 1.97, -0.35, 2.27)
66
+ u_vals = [-0.2, 1.3, 0.15]
67
+ u_vals.extend(solve_u_dep(q_vals, u_vals, p_vals)[:2, 0])
68
+ expected = Matrix([
69
+ 0.126603940595934, 0.215942571601660, 1.28736069604936,
70
+ 0.319764288376543, 0.0989146857254898, -0.925848952664489,
71
+ -0.0181350656532944, 2.91695398184589, -0.00992793421754526,
72
+ 0.0412861634829171])
73
+ assert all(abs(x) < eps for x in
74
+ (solve_sys(q_vals, u_vals, p_vals) - expected))
75
+ # Second numeric test
76
+ q_vals = (3.97, -0.28, 8.2, -0.35, 2.27)
77
+ u_vals = [-0.25, -2.2, 0.62]
78
+ u_vals.extend(solve_u_dep(q_vals, u_vals, p_vals)[:2, 0])
79
+ expected = Matrix([
80
+ 0.0259159090798597, 0.668041660387416, -2.19283799213811,
81
+ 0.385441810852219, 0.420109283790573, 1.45030568179066,
82
+ -0.0110924422400793, -8.35617840186040, -0.154098542632173,
83
+ -0.146102664410010])
84
+ assert all(abs(x) < eps for x in
85
+ (solve_sys(q_vals, u_vals, p_vals) - expected))
86
+ if all_zero:
87
+ q_vals = (0, 0, 0, 0, 0)
88
+ u_vals = (0, 0, 0, 0, 0)
89
+ assert solve_sys(q_vals, u_vals, p_vals) == zeros(10, 1)
90
+
91
+
92
+ def test_kane_rolling_disc_lu():
93
+ props = _create_rolling_disc()
94
+ kane = KanesMethod(props['frame'], props['q_ind'], props['u_ind'],
95
+ props['kdes'], u_dependent=props['u_dep'],
96
+ velocity_constraints=props['fnh'],
97
+ bodies=props['bodies'], forcelist=props['loads'],
98
+ explicit_kinematics=False, constraint_solver='LU')
99
+ kane.kanes_equations()
100
+ _verify_rolling_disc_numerically(kane)
101
+
102
+
103
+ def test_kane_rolling_disc_kdes_callable():
104
+ props = _create_rolling_disc()
105
+ kane = KanesMethod(
106
+ props['frame'], props['q_ind'], props['u_ind'], props['kdes'],
107
+ u_dependent=props['u_dep'], velocity_constraints=props['fnh'],
108
+ bodies=props['bodies'], forcelist=props['loads'],
109
+ explicit_kinematics=False,
110
+ kd_eqs_solver=lambda A, b: simplify(A.LUsolve(b)))
111
+ q, u, p = dynamicsymbols('q1:6'), dynamicsymbols('u1:6'), symbols('g r m')
112
+ qd = dynamicsymbols('q1:6', 1)
113
+ eval_kdes = lambdify((q, qd, u, p), tuple(kane.kindiffdict().items()))
114
+ eps = 1e-10
115
+ # Test with only zeros. If 'LU' would be used this would result in nan.
116
+ p_vals = (9.81, 0.25, 3.5)
117
+ zero_vals = (0, 0, 0, 0, 0)
118
+ assert all(abs(qdi - fui) < eps for qdi, fui in
119
+ eval_kdes(zero_vals, zero_vals, zero_vals, p_vals))
120
+ # Test with some arbitrary values
121
+ q_vals = tuple(map(float, (pi / 6, pi / 3, pi / 2, 0.42, 0.62)))
122
+ qd_vals = tuple(map(float, (4, 1 / 3, 4 - 2 * sqrt(3),
123
+ 0.25 * (2 * sqrt(3) - 3),
124
+ 0.25 * (2 - sqrt(3)))))
125
+ u_vals = tuple(map(float, (-2, 4, 1 / 3, 0.25 * (-3 + 2 * sqrt(3)),
126
+ 0.25 * (-sqrt(3) + 2))))
127
+ assert all(abs(qdi - fui) < eps for qdi, fui in
128
+ eval_kdes(q_vals, qd_vals, u_vals, p_vals))
lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_lagrange.py ADDED
@@ -0,0 +1,247 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sympy.physics.mechanics import (dynamicsymbols, ReferenceFrame, Point,
2
+ RigidBody, LagrangesMethod, Particle,
3
+ inertia, Lagrangian)
4
+ from sympy.core.function import (Derivative, Function)
5
+ from sympy.core.numbers import pi
6
+ from sympy.core.symbol import symbols
7
+ from sympy.functions.elementary.trigonometric import (cos, sin, tan)
8
+ from sympy.matrices.dense import Matrix
9
+ from sympy.simplify.simplify import simplify
10
+ from sympy.testing.pytest import raises
11
+
12
+
13
+ def test_invalid_coordinates():
14
+ # Simple pendulum, but use symbol instead of dynamicsymbol
15
+ l, m, g = symbols('l m g')
16
+ q = symbols('q') # Generalized coordinate
17
+ N, O = ReferenceFrame('N'), Point('O')
18
+ O.set_vel(N, 0)
19
+ P = Particle('P', Point('P'), m)
20
+ P.point.set_pos(O, l * (sin(q) * N.x - cos(q) * N.y))
21
+ P.potential_energy = m * g * P.point.pos_from(O).dot(N.y)
22
+ L = Lagrangian(N, P)
23
+ raises(ValueError, lambda: LagrangesMethod(L, [q], bodies=P))
24
+
25
+
26
+ def test_disc_on_an_incline_plane():
27
+ # Disc rolling on an inclined plane
28
+ # First the generalized coordinates are created. The mass center of the
29
+ # disc is located from top vertex of the inclined plane by the generalized
30
+ # coordinate 'y'. The orientation of the disc is defined by the angle
31
+ # 'theta'. The mass of the disc is 'm' and its radius is 'R'. The length of
32
+ # the inclined path is 'l', the angle of inclination is 'alpha'. 'g' is the
33
+ # gravitational constant.
34
+ y, theta = dynamicsymbols('y theta')
35
+ yd, thetad = dynamicsymbols('y theta', 1)
36
+ m, g, R, l, alpha = symbols('m g R l alpha')
37
+
38
+ # Next, we create the inertial reference frame 'N'. A reference frame 'A'
39
+ # is attached to the inclined plane. Finally a frame is created which is attached to the disk.
40
+ N = ReferenceFrame('N')
41
+ A = N.orientnew('A', 'Axis', [pi/2 - alpha, N.z])
42
+ B = A.orientnew('B', 'Axis', [-theta, A.z])
43
+
44
+ # Creating the disc 'D'; we create the point that represents the mass
45
+ # center of the disc and set its velocity. The inertia dyadic of the disc
46
+ # is created. Finally, we create the disc.
47
+ Do = Point('Do')
48
+ Do.set_vel(N, yd * A.x)
49
+ I = m * R**2/2 * B.z | B.z
50
+ D = RigidBody('D', Do, B, m, (I, Do))
51
+
52
+ # To construct the Lagrangian, 'L', of the disc, we determine its kinetic
53
+ # and potential energies, T and U, respectively. L is defined as the
54
+ # difference between T and U.
55
+ D.potential_energy = m * g * (l - y) * sin(alpha)
56
+ L = Lagrangian(N, D)
57
+
58
+ # We then create the list of generalized coordinates and constraint
59
+ # equations. The constraint arises due to the disc rolling without slip on
60
+ # on the inclined path. We then invoke the 'LagrangesMethod' class and
61
+ # supply it the necessary arguments and generate the equations of motion.
62
+ # The'rhs' method solves for the q_double_dots (i.e. the second derivative
63
+ # with respect to time of the generalized coordinates and the lagrange
64
+ # multipliers.
65
+ q = [y, theta]
66
+ hol_coneqs = [y - R * theta]
67
+ m = LagrangesMethod(L, q, hol_coneqs=hol_coneqs)
68
+ m.form_lagranges_equations()
69
+ rhs = m.rhs()
70
+ rhs.simplify()
71
+ assert rhs[2] == 2*g*sin(alpha)/3
72
+
73
+
74
+ def test_simp_pen():
75
+ # This tests that the equations generated by LagrangesMethod are identical
76
+ # to those obtained by hand calculations. The system under consideration is
77
+ # the simple pendulum.
78
+ # We begin by creating the generalized coordinates as per the requirements
79
+ # of LagrangesMethod. Also we created the associate symbols
80
+ # that characterize the system: 'm' is the mass of the bob, l is the length
81
+ # of the massless rigid rod connecting the bob to a point O fixed in the
82
+ # inertial frame.
83
+ q, u = dynamicsymbols('q u')
84
+ qd, ud = dynamicsymbols('q u ', 1)
85
+ l, m, g = symbols('l m g')
86
+
87
+ # We then create the inertial frame and a frame attached to the massless
88
+ # string following which we define the inertial angular velocity of the
89
+ # string.
90
+ N = ReferenceFrame('N')
91
+ A = N.orientnew('A', 'Axis', [q, N.z])
92
+ A.set_ang_vel(N, qd * N.z)
93
+
94
+ # Next, we create the point O and fix it in the inertial frame. We then
95
+ # locate the point P to which the bob is attached. Its corresponding
96
+ # velocity is then determined by the 'two point formula'.
97
+ O = Point('O')
98
+ O.set_vel(N, 0)
99
+ P = O.locatenew('P', l * A.x)
100
+ P.v2pt_theory(O, N, A)
101
+
102
+ # The 'Particle' which represents the bob is then created and its
103
+ # Lagrangian generated.
104
+ Pa = Particle('Pa', P, m)
105
+ Pa.potential_energy = - m * g * l * cos(q)
106
+ L = Lagrangian(N, Pa)
107
+
108
+ # The 'LagrangesMethod' class is invoked to obtain equations of motion.
109
+ lm = LagrangesMethod(L, [q])
110
+ lm.form_lagranges_equations()
111
+ RHS = lm.rhs()
112
+ assert RHS[1] == -g*sin(q)/l
113
+
114
+
115
+ def test_nonminimal_pendulum():
116
+ q1, q2 = dynamicsymbols('q1:3')
117
+ q1d, q2d = dynamicsymbols('q1:3', level=1)
118
+ L, m, t = symbols('L, m, t')
119
+ g = 9.8
120
+ # Compose World Frame
121
+ N = ReferenceFrame('N')
122
+ pN = Point('N*')
123
+ pN.set_vel(N, 0)
124
+ # Create point P, the pendulum mass
125
+ P = pN.locatenew('P1', q1*N.x + q2*N.y)
126
+ P.set_vel(N, P.pos_from(pN).dt(N))
127
+ pP = Particle('pP', P, m)
128
+ # Constraint Equations
129
+ f_c = Matrix([q1**2 + q2**2 - L**2])
130
+ # Calculate the lagrangian, and form the equations of motion
131
+ Lag = Lagrangian(N, pP)
132
+ LM = LagrangesMethod(Lag, [q1, q2], hol_coneqs=f_c,
133
+ forcelist=[(P, m*g*N.x)], frame=N)
134
+ LM.form_lagranges_equations()
135
+ # Check solution
136
+ lam1 = LM.lam_vec[0, 0]
137
+ eom_sol = Matrix([[m*Derivative(q1, t, t) - 9.8*m + 2*lam1*q1],
138
+ [m*Derivative(q2, t, t) + 2*lam1*q2]])
139
+ assert LM.eom == eom_sol
140
+ # Check multiplier solution
141
+ lam_sol = Matrix([(19.6*q1 + 2*q1d**2 + 2*q2d**2)/(4*q1**2/m + 4*q2**2/m)])
142
+ assert simplify(LM.solve_multipliers(sol_type='Matrix')) == simplify(lam_sol)
143
+
144
+
145
+ def test_dub_pen():
146
+
147
+ # The system considered is the double pendulum. Like in the
148
+ # test of the simple pendulum above, we begin by creating the generalized
149
+ # coordinates and the simple generalized speeds and accelerations which
150
+ # will be used later. Following this we create frames and points necessary
151
+ # for the kinematics. The procedure isn't explicitly explained as this is
152
+ # similar to the simple pendulum. Also this is documented on the pydy.org
153
+ # website.
154
+ q1, q2 = dynamicsymbols('q1 q2')
155
+ q1d, q2d = dynamicsymbols('q1 q2', 1)
156
+ q1dd, q2dd = dynamicsymbols('q1 q2', 2)
157
+ u1, u2 = dynamicsymbols('u1 u2')
158
+ u1d, u2d = dynamicsymbols('u1 u2', 1)
159
+ l, m, g = symbols('l m g')
160
+
161
+ N = ReferenceFrame('N')
162
+ A = N.orientnew('A', 'Axis', [q1, N.z])
163
+ B = N.orientnew('B', 'Axis', [q2, N.z])
164
+
165
+ A.set_ang_vel(N, q1d * A.z)
166
+ B.set_ang_vel(N, q2d * A.z)
167
+
168
+ O = Point('O')
169
+ P = O.locatenew('P', l * A.x)
170
+ R = P.locatenew('R', l * B.x)
171
+
172
+ O.set_vel(N, 0)
173
+ P.v2pt_theory(O, N, A)
174
+ R.v2pt_theory(P, N, B)
175
+
176
+ ParP = Particle('ParP', P, m)
177
+ ParR = Particle('ParR', R, m)
178
+
179
+ ParP.potential_energy = - m * g * l * cos(q1)
180
+ ParR.potential_energy = - m * g * l * cos(q1) - m * g * l * cos(q2)
181
+ L = Lagrangian(N, ParP, ParR)
182
+ lm = LagrangesMethod(L, [q1, q2], bodies=[ParP, ParR])
183
+ lm.form_lagranges_equations()
184
+
185
+ assert simplify(l*m*(2*g*sin(q1) + l*sin(q1)*sin(q2)*q2dd
186
+ + l*sin(q1)*cos(q2)*q2d**2 - l*sin(q2)*cos(q1)*q2d**2
187
+ + l*cos(q1)*cos(q2)*q2dd + 2*l*q1dd) - lm.eom[0]) == 0
188
+ assert simplify(l*m*(g*sin(q2) + l*sin(q1)*sin(q2)*q1dd
189
+ - l*sin(q1)*cos(q2)*q1d**2 + l*sin(q2)*cos(q1)*q1d**2
190
+ + l*cos(q1)*cos(q2)*q1dd + l*q2dd) - lm.eom[1]) == 0
191
+ assert lm.bodies == [ParP, ParR]
192
+
193
+
194
+ def test_rolling_disc():
195
+ # Rolling Disc Example
196
+ # Here the rolling disc is formed from the contact point up, removing the
197
+ # need to introduce generalized speeds. Only 3 configuration and 3
198
+ # speed variables are need to describe this system, along with the
199
+ # disc's mass and radius, and the local gravity.
200
+ q1, q2, q3 = dynamicsymbols('q1 q2 q3')
201
+ q1d, q2d, q3d = dynamicsymbols('q1 q2 q3', 1)
202
+ r, m, g = symbols('r m g')
203
+
204
+ # The kinematics are formed by a series of simple rotations. Each simple
205
+ # rotation creates a new frame, and the next rotation is defined by the new
206
+ # frame's basis vectors. This example uses a 3-1-2 series of rotations, or
207
+ # Z, X, Y series of rotations. Angular velocity for this is defined using
208
+ # the second frame's basis (the lean frame).
209
+ N = ReferenceFrame('N')
210
+ Y = N.orientnew('Y', 'Axis', [q1, N.z])
211
+ L = Y.orientnew('L', 'Axis', [q2, Y.x])
212
+ R = L.orientnew('R', 'Axis', [q3, L.y])
213
+
214
+ # This is the translational kinematics. We create a point with no velocity
215
+ # in N; this is the contact point between the disc and ground. Next we form
216
+ # the position vector from the contact point to the disc's center of mass.
217
+ # Finally we form the velocity and acceleration of the disc.
218
+ C = Point('C')
219
+ C.set_vel(N, 0)
220
+ Dmc = C.locatenew('Dmc', r * L.z)
221
+ Dmc.v2pt_theory(C, N, R)
222
+
223
+ # Forming the inertia dyadic.
224
+ I = inertia(L, m/4 * r**2, m/2 * r**2, m/4 * r**2)
225
+ BodyD = RigidBody('BodyD', Dmc, R, m, (I, Dmc))
226
+
227
+ # Finally we form the equations of motion, using the same steps we did
228
+ # before. Supply the Lagrangian, the generalized speeds.
229
+ BodyD.potential_energy = - m * g * r * cos(q2)
230
+ Lag = Lagrangian(N, BodyD)
231
+ q = [q1, q2, q3]
232
+ q1 = Function('q1')
233
+ q2 = Function('q2')
234
+ q3 = Function('q3')
235
+ l = LagrangesMethod(Lag, q)
236
+ l.form_lagranges_equations()
237
+ RHS = l.rhs()
238
+ RHS.simplify()
239
+ t = symbols('t')
240
+
241
+ assert (l.mass_matrix[3:6] == [0, 5*m*r**2/4, 0])
242
+ assert RHS[4].simplify() == (
243
+ (-8*g*sin(q2(t)) + r*(5*sin(2*q2(t))*Derivative(q1(t), t) +
244
+ 12*cos(q2(t))*Derivative(q3(t), t))*Derivative(q1(t), t))/(10*r))
245
+ assert RHS[5] == (-5*cos(q2(t))*Derivative(q1(t), t) + 6*tan(q2(t)
246
+ )*Derivative(q3(t), t) + 4*Derivative(q1(t), t)/cos(q2(t))
247
+ )*Derivative(q2(t), t)
lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_lagrange2.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sympy import symbols
2
+ from sympy.physics.mechanics import dynamicsymbols
3
+ from sympy.physics.mechanics import ReferenceFrame, Point, Particle
4
+ from sympy.physics.mechanics import LagrangesMethod, Lagrangian
5
+
6
+ ### This test asserts that a system with more than one external forces
7
+ ### is acurately formed with Lagrange method (see issue #8626)
8
+
9
+ def test_lagrange_2forces():
10
+ ### Equations for two damped springs in serie with two forces
11
+
12
+ ### generalized coordinates
13
+ q1, q2 = dynamicsymbols('q1, q2')
14
+ ### generalized speeds
15
+ q1d, q2d = dynamicsymbols('q1, q2', 1)
16
+
17
+ ### Mass, spring strength, friction coefficient
18
+ m, k, nu = symbols('m, k, nu')
19
+
20
+ N = ReferenceFrame('N')
21
+ O = Point('O')
22
+
23
+ ### Two points
24
+ P1 = O.locatenew('P1', q1 * N.x)
25
+ P1.set_vel(N, q1d * N.x)
26
+ P2 = O.locatenew('P1', q2 * N.x)
27
+ P2.set_vel(N, q2d * N.x)
28
+
29
+ pP1 = Particle('pP1', P1, m)
30
+ pP1.potential_energy = k * q1**2 / 2
31
+
32
+ pP2 = Particle('pP2', P2, m)
33
+ pP2.potential_energy = k * (q1 - q2)**2 / 2
34
+
35
+ #### Friction forces
36
+ forcelist = [(P1, - nu * q1d * N.x),
37
+ (P2, - nu * q2d * N.x)]
38
+ lag = Lagrangian(N, pP1, pP2)
39
+
40
+ l_method = LagrangesMethod(lag, (q1, q2), forcelist=forcelist, frame=N)
41
+ l_method.form_lagranges_equations()
42
+
43
+ eq1 = l_method.eom[0]
44
+ assert eq1.diff(q1d) == nu
45
+ eq2 = l_method.eom[1]
46
+ assert eq2.diff(q2d) == nu
lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_linearize.py ADDED
@@ -0,0 +1,372 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sympy import symbols, Matrix, cos, sin, atan, sqrt, Rational
2
+ from sympy.core.sympify import sympify
3
+ from sympy.simplify.simplify import simplify
4
+ from sympy.solvers.solvers import solve
5
+ from sympy.physics.mechanics import dynamicsymbols, ReferenceFrame, Point,\
6
+ dot, cross, inertia, KanesMethod, Particle, RigidBody, Lagrangian,\
7
+ LagrangesMethod
8
+ from sympy.testing.pytest import slow
9
+
10
+
11
+ @slow
12
+ def test_linearize_rolling_disc_kane():
13
+ # Symbols for time and constant parameters
14
+ t, r, m, g, v = symbols('t r m g v')
15
+
16
+ # Configuration variables and their time derivatives
17
+ q1, q2, q3, q4, q5, q6 = q = dynamicsymbols('q1:7')
18
+ q1d, q2d, q3d, q4d, q5d, q6d = qd = [qi.diff(t) for qi in q]
19
+
20
+ # Generalized speeds and their time derivatives
21
+ u = dynamicsymbols('u:6')
22
+ u1, u2, u3, u4, u5, u6 = u = dynamicsymbols('u1:7')
23
+ u1d, u2d, u3d, u4d, u5d, u6d = [ui.diff(t) for ui in u]
24
+
25
+ # Reference frames
26
+ N = ReferenceFrame('N') # Inertial frame
27
+ NO = Point('NO') # Inertial origin
28
+ A = N.orientnew('A', 'Axis', [q1, N.z]) # Yaw intermediate frame
29
+ B = A.orientnew('B', 'Axis', [q2, A.x]) # Lean intermediate frame
30
+ C = B.orientnew('C', 'Axis', [q3, B.y]) # Disc fixed frame
31
+ CO = NO.locatenew('CO', q4*N.x + q5*N.y + q6*N.z) # Disc center
32
+
33
+ # Disc angular velocity in N expressed using time derivatives of coordinates
34
+ w_c_n_qd = C.ang_vel_in(N)
35
+ w_b_n_qd = B.ang_vel_in(N)
36
+
37
+ # Inertial angular velocity and angular acceleration of disc fixed frame
38
+ C.set_ang_vel(N, u1*B.x + u2*B.y + u3*B.z)
39
+
40
+ # Disc center velocity in N expressed using time derivatives of coordinates
41
+ v_co_n_qd = CO.pos_from(NO).dt(N)
42
+
43
+ # Disc center velocity in N expressed using generalized speeds
44
+ CO.set_vel(N, u4*C.x + u5*C.y + u6*C.z)
45
+
46
+ # Disc Ground Contact Point
47
+ P = CO.locatenew('P', r*B.z)
48
+ P.v2pt_theory(CO, N, C)
49
+
50
+ # Configuration constraint
51
+ f_c = Matrix([q6 - dot(CO.pos_from(P), N.z)])
52
+
53
+ # Velocity level constraints
54
+ f_v = Matrix([dot(P.vel(N), uv) for uv in C])
55
+
56
+ # Kinematic differential equations
57
+ kindiffs = Matrix([dot(w_c_n_qd - C.ang_vel_in(N), uv) for uv in B] +
58
+ [dot(v_co_n_qd - CO.vel(N), uv) for uv in N])
59
+ qdots = solve(kindiffs, qd)
60
+
61
+ # Set angular velocity of remaining frames
62
+ B.set_ang_vel(N, w_b_n_qd.subs(qdots))
63
+ C.set_ang_acc(N, C.ang_vel_in(N).dt(B) + cross(B.ang_vel_in(N), C.ang_vel_in(N)))
64
+
65
+ # Active forces
66
+ F_CO = m*g*A.z
67
+
68
+ # Create inertia dyadic of disc C about point CO
69
+ I = (m * r**2) / 4
70
+ J = (m * r**2) / 2
71
+ I_C_CO = inertia(C, I, J, I)
72
+
73
+ Disc = RigidBody('Disc', CO, C, m, (I_C_CO, CO))
74
+ BL = [Disc]
75
+ FL = [(CO, F_CO)]
76
+ KM = KanesMethod(N, [q1, q2, q3, q4, q5], [u1, u2, u3], kd_eqs=kindiffs,
77
+ q_dependent=[q6], configuration_constraints=f_c,
78
+ u_dependent=[u4, u5, u6], velocity_constraints=f_v)
79
+ (fr, fr_star) = KM.kanes_equations(BL, FL)
80
+
81
+ # Test generalized form equations
82
+ linearizer = KM.to_linearizer()
83
+ assert linearizer.f_c == f_c
84
+ assert linearizer.f_v == f_v
85
+ assert linearizer.f_a == f_v.diff(t).subs(KM.kindiffdict())
86
+ sol = solve(linearizer.f_0 + linearizer.f_1, qd)
87
+ for qi in qdots.keys():
88
+ assert sol[qi] == qdots[qi]
89
+ assert simplify(linearizer.f_2 + linearizer.f_3 - fr - fr_star) == Matrix([0, 0, 0])
90
+
91
+ # Perform the linearization
92
+ # Precomputed operating point
93
+ q_op = {q6: -r*cos(q2)}
94
+ u_op = {u1: 0,
95
+ u2: sin(q2)*q1d + q3d,
96
+ u3: cos(q2)*q1d,
97
+ u4: -r*(sin(q2)*q1d + q3d)*cos(q3),
98
+ u5: 0,
99
+ u6: -r*(sin(q2)*q1d + q3d)*sin(q3)}
100
+ qd_op = {q2d: 0,
101
+ q4d: -r*(sin(q2)*q1d + q3d)*cos(q1),
102
+ q5d: -r*(sin(q2)*q1d + q3d)*sin(q1),
103
+ q6d: 0}
104
+ ud_op = {u1d: 4*g*sin(q2)/(5*r) + sin(2*q2)*q1d**2/2 + 6*cos(q2)*q1d*q3d/5,
105
+ u2d: 0,
106
+ u3d: 0,
107
+ u4d: r*(sin(q2)*sin(q3)*q1d*q3d + sin(q3)*q3d**2),
108
+ u5d: r*(4*g*sin(q2)/(5*r) + sin(2*q2)*q1d**2/2 + 6*cos(q2)*q1d*q3d/5),
109
+ u6d: -r*(sin(q2)*cos(q3)*q1d*q3d + cos(q3)*q3d**2)}
110
+
111
+ A, B = linearizer.linearize(op_point=[q_op, u_op, qd_op, ud_op], A_and_B=True, simplify=True)
112
+
113
+ upright_nominal = {q1d: 0, q2: 0, m: 1, r: 1, g: 1}
114
+
115
+ # Precomputed solution
116
+ A_sol = Matrix([[0, 0, 0, 0, 0, 0, 0, 1],
117
+ [0, 0, 0, 0, 0, 1, 0, 0],
118
+ [0, 0, 0, 0, 0, 0, 1, 0],
119
+ [sin(q1)*q3d, 0, 0, 0, 0, -sin(q1), -cos(q1), 0],
120
+ [-cos(q1)*q3d, 0, 0, 0, 0, cos(q1), -sin(q1), 0],
121
+ [0, Rational(4, 5), 0, 0, 0, 0, 0, 6*q3d/5],
122
+ [0, 0, 0, 0, 0, 0, 0, 0],
123
+ [0, 0, 0, 0, 0, -2*q3d, 0, 0]])
124
+ B_sol = Matrix([])
125
+
126
+ # Check that linearization is correct
127
+ assert A.subs(upright_nominal) == A_sol
128
+ assert B.subs(upright_nominal) == B_sol
129
+
130
+ # Check eigenvalues at critical speed are all zero:
131
+ assert sympify(A.subs(upright_nominal).subs(q3d, 1/sqrt(3))).eigenvals() == {0: 8}
132
+
133
+ # Check whether alternative solvers work
134
+ # symengine doesn't support method='GJ'
135
+ linearizer = KM.to_linearizer(linear_solver='GJ')
136
+ A, B = linearizer.linearize(op_point=[q_op, u_op, qd_op, ud_op],
137
+ A_and_B=True, simplify=True)
138
+ assert A.subs(upright_nominal) == A_sol
139
+ assert B.subs(upright_nominal) == B_sol
140
+
141
+ def test_linearize_pendulum_kane_minimal():
142
+ q1 = dynamicsymbols('q1') # angle of pendulum
143
+ u1 = dynamicsymbols('u1') # Angular velocity
144
+ q1d = dynamicsymbols('q1', 1) # Angular velocity
145
+ L, m, t = symbols('L, m, t')
146
+ g = 9.8
147
+
148
+ # Compose world frame
149
+ N = ReferenceFrame('N')
150
+ pN = Point('N*')
151
+ pN.set_vel(N, 0)
152
+
153
+ # A.x is along the pendulum
154
+ A = N.orientnew('A', 'axis', [q1, N.z])
155
+ A.set_ang_vel(N, u1*N.z)
156
+
157
+ # Locate point P relative to the origin N*
158
+ P = pN.locatenew('P', L*A.x)
159
+ P.v2pt_theory(pN, N, A)
160
+ pP = Particle('pP', P, m)
161
+
162
+ # Create Kinematic Differential Equations
163
+ kde = Matrix([q1d - u1])
164
+
165
+ # Input the force resultant at P
166
+ R = m*g*N.x
167
+
168
+ # Solve for eom with kanes method
169
+ KM = KanesMethod(N, q_ind=[q1], u_ind=[u1], kd_eqs=kde)
170
+ (fr, frstar) = KM.kanes_equations([pP], [(P, R)])
171
+
172
+ # Linearize
173
+ A, B, inp_vec = KM.linearize(A_and_B=True, simplify=True)
174
+
175
+ assert A == Matrix([[0, 1], [-9.8*cos(q1)/L, 0]])
176
+ assert B == Matrix([])
177
+
178
+ def test_linearize_pendulum_kane_nonminimal():
179
+ # Create generalized coordinates and speeds for this non-minimal realization
180
+ # q1, q2 = N.x and N.y coordinates of pendulum
181
+ # u1, u2 = N.x and N.y velocities of pendulum
182
+ q1, q2 = dynamicsymbols('q1:3')
183
+ q1d, q2d = dynamicsymbols('q1:3', level=1)
184
+ u1, u2 = dynamicsymbols('u1:3')
185
+ u1d, u2d = dynamicsymbols('u1:3', level=1)
186
+ L, m, t = symbols('L, m, t')
187
+ g = 9.8
188
+
189
+ # Compose world frame
190
+ N = ReferenceFrame('N')
191
+ pN = Point('N*')
192
+ pN.set_vel(N, 0)
193
+
194
+ # A.x is along the pendulum
195
+ theta1 = atan(q2/q1)
196
+ A = N.orientnew('A', 'axis', [theta1, N.z])
197
+
198
+ # Locate the pendulum mass
199
+ P = pN.locatenew('P1', q1*N.x + q2*N.y)
200
+ pP = Particle('pP', P, m)
201
+
202
+ # Calculate the kinematic differential equations
203
+ kde = Matrix([q1d - u1,
204
+ q2d - u2])
205
+ dq_dict = solve(kde, [q1d, q2d])
206
+
207
+ # Set velocity of point P
208
+ P.set_vel(N, P.pos_from(pN).dt(N).subs(dq_dict))
209
+
210
+ # Configuration constraint is length of pendulum
211
+ f_c = Matrix([P.pos_from(pN).magnitude() - L])
212
+
213
+ # Velocity constraint is that the velocity in the A.x direction is
214
+ # always zero (the pendulum is never getting longer).
215
+ f_v = Matrix([P.vel(N).express(A).dot(A.x)])
216
+ f_v.simplify()
217
+
218
+ # Acceleration constraints is the time derivative of the velocity constraint
219
+ f_a = f_v.diff(t)
220
+ f_a.simplify()
221
+
222
+ # Input the force resultant at P
223
+ R = m*g*N.x
224
+
225
+ # Derive the equations of motion using the KanesMethod class.
226
+ KM = KanesMethod(N, q_ind=[q2], u_ind=[u2], q_dependent=[q1],
227
+ u_dependent=[u1], configuration_constraints=f_c,
228
+ velocity_constraints=f_v, acceleration_constraints=f_a, kd_eqs=kde)
229
+ (fr, frstar) = KM.kanes_equations([pP], [(P, R)])
230
+
231
+ # Set the operating point to be straight down, and non-moving
232
+ q_op = {q1: L, q2: 0}
233
+ u_op = {u1: 0, u2: 0}
234
+ ud_op = {u1d: 0, u2d: 0}
235
+
236
+ A, B, inp_vec = KM.linearize(op_point=[q_op, u_op, ud_op], A_and_B=True,
237
+ simplify=True)
238
+
239
+ assert A.expand() == Matrix([[0, 1], [-9.8/L, 0]])
240
+ assert B == Matrix([])
241
+
242
+
243
+ # symengine doesn't support method='GJ'
244
+ A, B, inp_vec = KM.linearize(op_point=[q_op, u_op, ud_op], A_and_B=True,
245
+ simplify=True, linear_solver='GJ')
246
+
247
+ assert A.expand() == Matrix([[0, 1], [-9.8/L, 0]])
248
+ assert B == Matrix([])
249
+
250
+ A, B, inp_vec = KM.linearize(op_point=[q_op, u_op, ud_op],
251
+ A_and_B=True,
252
+ simplify=True,
253
+ linear_solver=lambda A, b: A.LUsolve(b))
254
+
255
+ assert A.expand() == Matrix([[0, 1], [-9.8/L, 0]])
256
+ assert B == Matrix([])
257
+
258
+
259
+ def test_linearize_pendulum_lagrange_minimal():
260
+ q1 = dynamicsymbols('q1') # angle of pendulum
261
+ q1d = dynamicsymbols('q1', 1) # Angular velocity
262
+ L, m, t = symbols('L, m, t')
263
+ g = 9.8
264
+
265
+ # Compose world frame
266
+ N = ReferenceFrame('N')
267
+ pN = Point('N*')
268
+ pN.set_vel(N, 0)
269
+
270
+ # A.x is along the pendulum
271
+ A = N.orientnew('A', 'axis', [q1, N.z])
272
+ A.set_ang_vel(N, q1d*N.z)
273
+
274
+ # Locate point P relative to the origin N*
275
+ P = pN.locatenew('P', L*A.x)
276
+ P.v2pt_theory(pN, N, A)
277
+ pP = Particle('pP', P, m)
278
+
279
+ # Solve for eom with Lagranges method
280
+ Lag = Lagrangian(N, pP)
281
+ LM = LagrangesMethod(Lag, [q1], forcelist=[(P, m*g*N.x)], frame=N)
282
+ LM.form_lagranges_equations()
283
+
284
+ # Linearize
285
+ A, B, inp_vec = LM.linearize([q1], [q1d], A_and_B=True)
286
+
287
+ assert simplify(A) == Matrix([[0, 1], [-9.8*cos(q1)/L, 0]])
288
+ assert B == Matrix([])
289
+
290
+ # Check an alternative solver
291
+ A, B, inp_vec = LM.linearize([q1], [q1d], A_and_B=True, linear_solver='GJ')
292
+
293
+ assert simplify(A) == Matrix([[0, 1], [-9.8*cos(q1)/L, 0]])
294
+ assert B == Matrix([])
295
+
296
+
297
+ def test_linearize_pendulum_lagrange_nonminimal():
298
+ q1, q2 = dynamicsymbols('q1:3')
299
+ q1d, q2d = dynamicsymbols('q1:3', level=1)
300
+ L, m, t = symbols('L, m, t')
301
+ g = 9.8
302
+ # Compose World Frame
303
+ N = ReferenceFrame('N')
304
+ pN = Point('N*')
305
+ pN.set_vel(N, 0)
306
+ # A.x is along the pendulum
307
+ theta1 = atan(q2/q1)
308
+ A = N.orientnew('A', 'axis', [theta1, N.z])
309
+ # Create point P, the pendulum mass
310
+ P = pN.locatenew('P1', q1*N.x + q2*N.y)
311
+ P.set_vel(N, P.pos_from(pN).dt(N))
312
+ pP = Particle('pP', P, m)
313
+ # Constraint Equations
314
+ f_c = Matrix([q1**2 + q2**2 - L**2])
315
+ # Calculate the lagrangian, and form the equations of motion
316
+ Lag = Lagrangian(N, pP)
317
+ LM = LagrangesMethod(Lag, [q1, q2], hol_coneqs=f_c, forcelist=[(P, m*g*N.x)], frame=N)
318
+ LM.form_lagranges_equations()
319
+ # Compose operating point
320
+ op_point = {q1: L, q2: 0, q1d: 0, q2d: 0, q1d.diff(t): 0, q2d.diff(t): 0}
321
+ # Solve for multiplier operating point
322
+ lam_op = LM.solve_multipliers(op_point=op_point)
323
+ op_point.update(lam_op)
324
+ # Perform the Linearization
325
+ A, B, inp_vec = LM.linearize([q2], [q2d], [q1], [q1d],
326
+ op_point=op_point, A_and_B=True)
327
+ assert simplify(A) == Matrix([[0, 1], [-9.8/L, 0]])
328
+ assert B == Matrix([])
329
+
330
+ # Check if passing a function to linear_solver works
331
+ A, B, inp_vec = LM.linearize([q2], [q2d], [q1], [q1d], op_point=op_point,
332
+ A_and_B=True, linear_solver=lambda A, b:
333
+ A.LUsolve(b))
334
+ assert simplify(A) == Matrix([[0, 1], [-9.8/L, 0]])
335
+ assert B == Matrix([])
336
+
337
+ def test_linearize_rolling_disc_lagrange():
338
+ q1, q2, q3 = q = dynamicsymbols('q1 q2 q3')
339
+ q1d, q2d, q3d = qd = dynamicsymbols('q1 q2 q3', 1)
340
+ r, m, g = symbols('r m g')
341
+
342
+ N = ReferenceFrame('N')
343
+ Y = N.orientnew('Y', 'Axis', [q1, N.z])
344
+ L = Y.orientnew('L', 'Axis', [q2, Y.x])
345
+ R = L.orientnew('R', 'Axis', [q3, L.y])
346
+
347
+ C = Point('C')
348
+ C.set_vel(N, 0)
349
+ Dmc = C.locatenew('Dmc', r * L.z)
350
+ Dmc.v2pt_theory(C, N, R)
351
+
352
+ I = inertia(L, m / 4 * r**2, m / 2 * r**2, m / 4 * r**2)
353
+ BodyD = RigidBody('BodyD', Dmc, R, m, (I, Dmc))
354
+ BodyD.potential_energy = - m * g * r * cos(q2)
355
+
356
+ Lag = Lagrangian(N, BodyD)
357
+ l = LagrangesMethod(Lag, q)
358
+ l.form_lagranges_equations()
359
+
360
+ # Linearize about steady-state upright rolling
361
+ op_point = {q1: 0, q2: 0, q3: 0,
362
+ q1d: 0, q2d: 0,
363
+ q1d.diff(): 0, q2d.diff(): 0, q3d.diff(): 0}
364
+ A = l.linearize(q_ind=q, qd_ind=qd, op_point=op_point, A_and_B=True)[0]
365
+ sol = Matrix([[0, 0, 0, 1, 0, 0],
366
+ [0, 0, 0, 0, 1, 0],
367
+ [0, 0, 0, 0, 0, 1],
368
+ [0, 0, 0, 0, -6*q3d, 0],
369
+ [0, -4*g/(5*r), 0, 6*q3d/5, 0, 0],
370
+ [0, 0, 0, 0, 0, 0]])
371
+
372
+ assert A == sol
lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_loads.py ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pytest import raises
2
+
3
+ from sympy import symbols
4
+ from sympy.physics.mechanics import (RigidBody, Particle, ReferenceFrame, Point,
5
+ outer, dynamicsymbols, Force, Torque)
6
+ from sympy.physics.mechanics.loads import gravity, _parse_load
7
+
8
+
9
+ def test_force_default():
10
+ N = ReferenceFrame('N')
11
+ Po = Point('Po')
12
+ f1 = Force(Po, N.x)
13
+ assert f1.point == Po
14
+ assert f1.force == N.x
15
+ assert f1.__repr__() == 'Force(point=Po, force=N.x)'
16
+ # Test tuple behaviour
17
+ assert isinstance(f1, tuple)
18
+ assert f1[0] == Po
19
+ assert f1[1] == N.x
20
+ assert f1 == (Po, N.x)
21
+ assert f1 != (N.x, Po)
22
+ assert f1 != (Po, N.x + N.y)
23
+ assert f1 != (Point('Co'), N.x)
24
+ # Test body as input
25
+ P = Particle('P', Po)
26
+ f2 = Force(P, N.x)
27
+ assert f1 == f2
28
+
29
+
30
+ def test_torque_default():
31
+ N = ReferenceFrame('N')
32
+ f1 = Torque(N, N.x)
33
+ assert f1.frame == N
34
+ assert f1.torque == N.x
35
+ assert f1.__repr__() == 'Torque(frame=N, torque=N.x)'
36
+ # Test tuple behaviour
37
+ assert isinstance(f1, tuple)
38
+ assert f1[0] == N
39
+ assert f1[1] == N.x
40
+ assert f1 == (N, N.x)
41
+ assert f1 != (N.x, N)
42
+ assert f1 != (N, N.x + N.y)
43
+ assert f1 != (ReferenceFrame('A'), N.x)
44
+ # Test body as input
45
+ rb = RigidBody('P', frame=N)
46
+ f2 = Torque(rb, N.x)
47
+ assert f1 == f2
48
+
49
+
50
+ def test_gravity():
51
+ N = ReferenceFrame('N')
52
+ m, M, g = symbols('m M g')
53
+ F1, F2 = dynamicsymbols('F1 F2')
54
+ po = Point('po')
55
+ pa = Particle('pa', po, m)
56
+ A = ReferenceFrame('A')
57
+ P = Point('P')
58
+ I = outer(A.x, A.x)
59
+ B = RigidBody('B', P, A, M, (I, P))
60
+ forceList = [(po, F1), (P, F2)]
61
+ forceList.extend(gravity(g * N.y, pa, B))
62
+ l = [(po, F1), (P, F2), (po, g * m * N.y), (P, g * M * N.y)]
63
+
64
+ for i in range(len(l)):
65
+ for j in range(len(l[i])):
66
+ assert forceList[i][j] == l[i][j]
67
+
68
+
69
+ def test_parse_loads():
70
+ N = ReferenceFrame('N')
71
+ po = Point('po')
72
+ assert _parse_load(Force(po, N.z)) == (po, N.z)
73
+ assert _parse_load(Torque(N, N.x)) == (N, N.x)
74
+ f1 = _parse_load((po, N.x)) # Test whether a force is recognized
75
+ assert isinstance(f1, Force)
76
+ assert f1 == Force(po, N.x)
77
+ t1 = _parse_load((N, N.y)) # Test whether a torque is recognized
78
+ assert isinstance(t1, Torque)
79
+ assert t1 == Torque(N, N.y)
80
+ # Bodies should be undetermined (even in case of a Particle)
81
+ raises(ValueError, lambda: _parse_load((Particle('pa', po), N.x)))
82
+ raises(ValueError, lambda: _parse_load((RigidBody('pa', po, N), N.x)))
83
+ # Invalid tuple length
84
+ raises(ValueError, lambda: _parse_load((po, N.x, po, N.x)))
85
+ # Invalid type
86
+ raises(TypeError, lambda: _parse_load([po, N.x]))
lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_method.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ from sympy.physics.mechanics.method import _Methods
2
+ from sympy.testing.pytest import raises
3
+
4
+ def test_method():
5
+ raises(TypeError, lambda: _Methods())
lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_models.py ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sympy.physics.mechanics.models as models
2
+ from sympy import (cos, sin, Matrix, symbols, zeros)
3
+ from sympy.simplify.simplify import simplify
4
+ from sympy.physics.mechanics import (dynamicsymbols)
5
+
6
+
7
+ def test_multi_mass_spring_damper_inputs():
8
+
9
+ c0, k0, m0 = symbols("c0 k0 m0")
10
+ g = symbols("g")
11
+ v0, x0, f0 = dynamicsymbols("v0 x0 f0")
12
+
13
+ kane1 = models.multi_mass_spring_damper(1)
14
+ massmatrix1 = Matrix([[m0]])
15
+ forcing1 = Matrix([[-c0*v0 - k0*x0]])
16
+ assert simplify(massmatrix1 - kane1.mass_matrix) == Matrix([0])
17
+ assert simplify(forcing1 - kane1.forcing) == Matrix([0])
18
+
19
+ kane2 = models.multi_mass_spring_damper(1, True)
20
+ massmatrix2 = Matrix([[m0]])
21
+ forcing2 = Matrix([[-c0*v0 + g*m0 - k0*x0]])
22
+ assert simplify(massmatrix2 - kane2.mass_matrix) == Matrix([0])
23
+ assert simplify(forcing2 - kane2.forcing) == Matrix([0])
24
+
25
+ kane3 = models.multi_mass_spring_damper(1, True, True)
26
+ massmatrix3 = Matrix([[m0]])
27
+ forcing3 = Matrix([[-c0*v0 + g*m0 - k0*x0 + f0]])
28
+ assert simplify(massmatrix3 - kane3.mass_matrix) == Matrix([0])
29
+ assert simplify(forcing3 - kane3.forcing) == Matrix([0])
30
+
31
+ kane4 = models.multi_mass_spring_damper(1, False, True)
32
+ massmatrix4 = Matrix([[m0]])
33
+ forcing4 = Matrix([[-c0*v0 - k0*x0 + f0]])
34
+ assert simplify(massmatrix4 - kane4.mass_matrix) == Matrix([0])
35
+ assert simplify(forcing4 - kane4.forcing) == Matrix([0])
36
+
37
+
38
+ def test_multi_mass_spring_damper_higher_order():
39
+ c0, k0, m0 = symbols("c0 k0 m0")
40
+ c1, k1, m1 = symbols("c1 k1 m1")
41
+ c2, k2, m2 = symbols("c2 k2 m2")
42
+ v0, x0 = dynamicsymbols("v0 x0")
43
+ v1, x1 = dynamicsymbols("v1 x1")
44
+ v2, x2 = dynamicsymbols("v2 x2")
45
+
46
+ kane1 = models.multi_mass_spring_damper(3)
47
+ massmatrix1 = Matrix([[m0 + m1 + m2, m1 + m2, m2],
48
+ [m1 + m2, m1 + m2, m2],
49
+ [m2, m2, m2]])
50
+ forcing1 = Matrix([[-c0*v0 - k0*x0],
51
+ [-c1*v1 - k1*x1],
52
+ [-c2*v2 - k2*x2]])
53
+ assert simplify(massmatrix1 - kane1.mass_matrix) == zeros(3)
54
+ assert simplify(forcing1 - kane1.forcing) == Matrix([0, 0, 0])
55
+
56
+
57
+ def test_n_link_pendulum_on_cart_inputs():
58
+ l0, m0 = symbols("l0 m0")
59
+ m1 = symbols("m1")
60
+ g = symbols("g")
61
+ q0, q1, F, T1 = dynamicsymbols("q0 q1 F T1")
62
+ u0, u1 = dynamicsymbols("u0 u1")
63
+
64
+ kane1 = models.n_link_pendulum_on_cart(1)
65
+ massmatrix1 = Matrix([[m0 + m1, -l0*m1*cos(q1)],
66
+ [-l0*m1*cos(q1), l0**2*m1]])
67
+ forcing1 = Matrix([[-l0*m1*u1**2*sin(q1) + F], [g*l0*m1*sin(q1)]])
68
+ assert simplify(massmatrix1 - kane1.mass_matrix) == zeros(2)
69
+ assert simplify(forcing1 - kane1.forcing) == Matrix([0, 0])
70
+
71
+ kane2 = models.n_link_pendulum_on_cart(1, False)
72
+ massmatrix2 = Matrix([[m0 + m1, -l0*m1*cos(q1)],
73
+ [-l0*m1*cos(q1), l0**2*m1]])
74
+ forcing2 = Matrix([[-l0*m1*u1**2*sin(q1)], [g*l0*m1*sin(q1)]])
75
+ assert simplify(massmatrix2 - kane2.mass_matrix) == zeros(2)
76
+ assert simplify(forcing2 - kane2.forcing) == Matrix([0, 0])
77
+
78
+ kane3 = models.n_link_pendulum_on_cart(1, False, True)
79
+ massmatrix3 = Matrix([[m0 + m1, -l0*m1*cos(q1)],
80
+ [-l0*m1*cos(q1), l0**2*m1]])
81
+ forcing3 = Matrix([[-l0*m1*u1**2*sin(q1)], [g*l0*m1*sin(q1) + T1]])
82
+ assert simplify(massmatrix3 - kane3.mass_matrix) == zeros(2)
83
+ assert simplify(forcing3 - kane3.forcing) == Matrix([0, 0])
84
+
85
+ kane4 = models.n_link_pendulum_on_cart(1, True, False)
86
+ massmatrix4 = Matrix([[m0 + m1, -l0*m1*cos(q1)],
87
+ [-l0*m1*cos(q1), l0**2*m1]])
88
+ forcing4 = Matrix([[-l0*m1*u1**2*sin(q1) + F], [g*l0*m1*sin(q1)]])
89
+ assert simplify(massmatrix4 - kane4.mass_matrix) == zeros(2)
90
+ assert simplify(forcing4 - kane4.forcing) == Matrix([0, 0])
91
+
92
+
93
+ def test_n_link_pendulum_on_cart_higher_order():
94
+ l0, m0 = symbols("l0 m0")
95
+ l1, m1 = symbols("l1 m1")
96
+ m2 = symbols("m2")
97
+ g = symbols("g")
98
+ q0, q1, q2 = dynamicsymbols("q0 q1 q2")
99
+ u0, u1, u2 = dynamicsymbols("u0 u1 u2")
100
+ F, T1 = dynamicsymbols("F T1")
101
+
102
+ kane1 = models.n_link_pendulum_on_cart(2)
103
+ massmatrix1 = Matrix([[m0 + m1 + m2, -l0*m1*cos(q1) - l0*m2*cos(q1),
104
+ -l1*m2*cos(q2)],
105
+ [-l0*m1*cos(q1) - l0*m2*cos(q1), l0**2*m1 + l0**2*m2,
106
+ l0*l1*m2*(sin(q1)*sin(q2) + cos(q1)*cos(q2))],
107
+ [-l1*m2*cos(q2),
108
+ l0*l1*m2*(sin(q1)*sin(q2) + cos(q1)*cos(q2)),
109
+ l1**2*m2]])
110
+ forcing1 = Matrix([[-l0*m1*u1**2*sin(q1) - l0*m2*u1**2*sin(q1) -
111
+ l1*m2*u2**2*sin(q2) + F],
112
+ [g*l0*m1*sin(q1) + g*l0*m2*sin(q1) -
113
+ l0*l1*m2*(sin(q1)*cos(q2) - sin(q2)*cos(q1))*u2**2],
114
+ [g*l1*m2*sin(q2) - l0*l1*m2*(-sin(q1)*cos(q2) +
115
+ sin(q2)*cos(q1))*u1**2]])
116
+ assert simplify(massmatrix1 - kane1.mass_matrix) == zeros(3)
117
+ assert simplify(forcing1 - kane1.forcing) == Matrix([0, 0, 0])
lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_particle.py ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sympy import symbols
2
+ from sympy.physics.mechanics import Point, Particle, ReferenceFrame, inertia
3
+ from sympy.physics.mechanics.body_base import BodyBase
4
+ from sympy.testing.pytest import raises, warns_deprecated_sympy
5
+
6
+
7
+ def test_particle_default():
8
+ # Test default
9
+ p = Particle('P')
10
+ assert p.name == 'P'
11
+ assert p.mass == symbols('P_mass')
12
+ assert p.masscenter.name == 'P_masscenter'
13
+ assert p.potential_energy == 0
14
+ assert p.__str__() == 'P'
15
+ assert p.__repr__() == ("Particle('P', masscenter=P_masscenter, "
16
+ "mass=P_mass)")
17
+ raises(AttributeError, lambda: p.frame)
18
+
19
+
20
+ def test_particle():
21
+ # Test initializing with parameters
22
+ m, m2, v1, v2, v3, r, g, h = symbols('m m2 v1 v2 v3 r g h')
23
+ P = Point('P')
24
+ P2 = Point('P2')
25
+ p = Particle('pa', P, m)
26
+ assert isinstance(p, BodyBase)
27
+ assert p.mass == m
28
+ assert p.point == P
29
+ # Test the mass setter
30
+ p.mass = m2
31
+ assert p.mass == m2
32
+ # Test the point setter
33
+ p.point = P2
34
+ assert p.point == P2
35
+ # Test the linear momentum function
36
+ N = ReferenceFrame('N')
37
+ O = Point('O')
38
+ P2.set_pos(O, r * N.y)
39
+ P2.set_vel(N, v1 * N.x)
40
+ raises(TypeError, lambda: Particle(P, P, m))
41
+ raises(TypeError, lambda: Particle('pa', m, m))
42
+ assert p.linear_momentum(N) == m2 * v1 * N.x
43
+ assert p.angular_momentum(O, N) == -m2 * r * v1 * N.z
44
+ P2.set_vel(N, v2 * N.y)
45
+ assert p.linear_momentum(N) == m2 * v2 * N.y
46
+ assert p.angular_momentum(O, N) == 0
47
+ P2.set_vel(N, v3 * N.z)
48
+ assert p.linear_momentum(N) == m2 * v3 * N.z
49
+ assert p.angular_momentum(O, N) == m2 * r * v3 * N.x
50
+ P2.set_vel(N, v1 * N.x + v2 * N.y + v3 * N.z)
51
+ assert p.linear_momentum(N) == m2 * (v1 * N.x + v2 * N.y + v3 * N.z)
52
+ assert p.angular_momentum(O, N) == m2 * r * (v3 * N.x - v1 * N.z)
53
+ p.potential_energy = m * g * h
54
+ assert p.potential_energy == m * g * h
55
+ # TODO make the result not be system-dependent
56
+ assert p.kinetic_energy(
57
+ N) in [m2 * (v1 ** 2 + v2 ** 2 + v3 ** 2) / 2,
58
+ m2 * v1 ** 2 / 2 + m2 * v2 ** 2 / 2 + m2 * v3 ** 2 / 2]
59
+
60
+
61
+ def test_parallel_axis():
62
+ N = ReferenceFrame('N')
63
+ m, a, b = symbols('m, a, b')
64
+ o = Point('o')
65
+ p = o.locatenew('p', a * N.x + b * N.y)
66
+ P = Particle('P', o, m)
67
+ Ip = P.parallel_axis(p, N)
68
+ Ip_expected = inertia(N, m * b ** 2, m * a ** 2, m * (a ** 2 + b ** 2),
69
+ ixy=-m * a * b)
70
+ assert Ip == Ip_expected
71
+
72
+
73
+ def test_deprecated_set_potential_energy():
74
+ m, g, h = symbols('m g h')
75
+ P = Point('P')
76
+ p = Particle('pa', P, m)
77
+ with warns_deprecated_sympy():
78
+ p.set_potential_energy(m * g * h)
lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_pathway.py ADDED
@@ -0,0 +1,691 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for the ``sympy.physics.mechanics.pathway.py`` module."""
2
+
3
+ import pytest
4
+
5
+ from sympy import (
6
+ Rational,
7
+ Symbol,
8
+ cos,
9
+ pi,
10
+ sin,
11
+ sqrt,
12
+ )
13
+ from sympy.physics.mechanics import (
14
+ Force,
15
+ LinearPathway,
16
+ ObstacleSetPathway,
17
+ PathwayBase,
18
+ Point,
19
+ ReferenceFrame,
20
+ WrappingCylinder,
21
+ WrappingGeometryBase,
22
+ WrappingPathway,
23
+ WrappingSphere,
24
+ dynamicsymbols,
25
+ )
26
+ from sympy.simplify.simplify import simplify
27
+
28
+
29
+ def _simplify_loads(loads):
30
+ return [
31
+ load.__class__(load.location, load.vector.simplify())
32
+ for load in loads
33
+ ]
34
+
35
+
36
+ class TestLinearPathway:
37
+
38
+ def test_is_pathway_base_subclass(self):
39
+ assert issubclass(LinearPathway, PathwayBase)
40
+
41
+ @staticmethod
42
+ @pytest.mark.parametrize(
43
+ 'args, kwargs',
44
+ [
45
+ ((Point('pA'), Point('pB')), {}),
46
+ ]
47
+ )
48
+ def test_valid_constructor(args, kwargs):
49
+ pointA, pointB = args
50
+ instance = LinearPathway(*args, **kwargs)
51
+ assert isinstance(instance, LinearPathway)
52
+ assert hasattr(instance, 'attachments')
53
+ assert len(instance.attachments) == 2
54
+ assert instance.attachments[0] is pointA
55
+ assert instance.attachments[1] is pointB
56
+ assert isinstance(instance.attachments[0], Point)
57
+ assert instance.attachments[0].name == 'pA'
58
+ assert isinstance(instance.attachments[1], Point)
59
+ assert instance.attachments[1].name == 'pB'
60
+
61
+ @staticmethod
62
+ @pytest.mark.parametrize(
63
+ 'attachments',
64
+ [
65
+ (Point('pA'), ),
66
+ (Point('pA'), Point('pB'), Point('pZ')),
67
+ ]
68
+ )
69
+ def test_invalid_attachments_incorrect_number(attachments):
70
+ with pytest.raises(ValueError):
71
+ _ = LinearPathway(*attachments)
72
+
73
+ @staticmethod
74
+ @pytest.mark.parametrize(
75
+ 'attachments',
76
+ [
77
+ (None, Point('pB')),
78
+ (Point('pA'), None),
79
+ ]
80
+ )
81
+ def test_invalid_attachments_not_point(attachments):
82
+ with pytest.raises(TypeError):
83
+ _ = LinearPathway(*attachments)
84
+
85
+ @pytest.fixture(autouse=True)
86
+ def _linear_pathway_fixture(self):
87
+ self.N = ReferenceFrame('N')
88
+ self.pA = Point('pA')
89
+ self.pB = Point('pB')
90
+ self.pathway = LinearPathway(self.pA, self.pB)
91
+ self.q1 = dynamicsymbols('q1')
92
+ self.q2 = dynamicsymbols('q2')
93
+ self.q3 = dynamicsymbols('q3')
94
+ self.q1d = dynamicsymbols('q1', 1)
95
+ self.q2d = dynamicsymbols('q2', 1)
96
+ self.q3d = dynamicsymbols('q3', 1)
97
+ self.F = Symbol('F')
98
+
99
+ def test_properties_are_immutable(self):
100
+ instance = LinearPathway(self.pA, self.pB)
101
+ with pytest.raises(AttributeError):
102
+ instance.attachments = None
103
+ with pytest.raises(TypeError):
104
+ instance.attachments[0] = None
105
+ with pytest.raises(TypeError):
106
+ instance.attachments[1] = None
107
+
108
+ def test_repr(self):
109
+ pathway = LinearPathway(self.pA, self.pB)
110
+ expected = 'LinearPathway(pA, pB)'
111
+ assert repr(pathway) == expected
112
+
113
+ def test_static_pathway_length(self):
114
+ self.pB.set_pos(self.pA, 2*self.N.x)
115
+ assert self.pathway.length == 2
116
+
117
+ def test_static_pathway_extension_velocity(self):
118
+ self.pB.set_pos(self.pA, 2*self.N.x)
119
+ assert self.pathway.extension_velocity == 0
120
+
121
+ def test_static_pathway_to_loads(self):
122
+ self.pB.set_pos(self.pA, 2*self.N.x)
123
+ expected = [
124
+ (self.pA, - self.F*self.N.x),
125
+ (self.pB, self.F*self.N.x),
126
+ ]
127
+ assert self.pathway.to_loads(self.F) == expected
128
+
129
+ def test_2D_pathway_length(self):
130
+ self.pB.set_pos(self.pA, 2*self.q1*self.N.x)
131
+ expected = 2*sqrt(self.q1**2)
132
+ assert self.pathway.length == expected
133
+
134
+ def test_2D_pathway_extension_velocity(self):
135
+ self.pB.set_pos(self.pA, 2*self.q1*self.N.x)
136
+ expected = 2*sqrt(self.q1**2)*self.q1d/self.q1
137
+ assert self.pathway.extension_velocity == expected
138
+
139
+ def test_2D_pathway_to_loads(self):
140
+ self.pB.set_pos(self.pA, 2*self.q1*self.N.x)
141
+ expected = [
142
+ (self.pA, - self.F*(self.q1 / sqrt(self.q1**2))*self.N.x),
143
+ (self.pB, self.F*(self.q1 / sqrt(self.q1**2))*self.N.x),
144
+ ]
145
+ assert self.pathway.to_loads(self.F) == expected
146
+
147
+ def test_3D_pathway_length(self):
148
+ self.pB.set_pos(
149
+ self.pA,
150
+ self.q1*self.N.x - self.q2*self.N.y + 2*self.q3*self.N.z,
151
+ )
152
+ expected = sqrt(self.q1**2 + self.q2**2 + 4*self.q3**2)
153
+ assert simplify(self.pathway.length - expected) == 0
154
+
155
+ def test_3D_pathway_extension_velocity(self):
156
+ self.pB.set_pos(
157
+ self.pA,
158
+ self.q1*self.N.x - self.q2*self.N.y + 2*self.q3*self.N.z,
159
+ )
160
+ length = sqrt(self.q1**2 + self.q2**2 + 4*self.q3**2)
161
+ expected = (
162
+ self.q1*self.q1d/length
163
+ + self.q2*self.q2d/length
164
+ + 4*self.q3*self.q3d/length
165
+ )
166
+ assert simplify(self.pathway.extension_velocity - expected) == 0
167
+
168
+ def test_3D_pathway_to_loads(self):
169
+ self.pB.set_pos(
170
+ self.pA,
171
+ self.q1*self.N.x - self.q2*self.N.y + 2*self.q3*self.N.z,
172
+ )
173
+ length = sqrt(self.q1**2 + self.q2**2 + 4*self.q3**2)
174
+ pO_force = (
175
+ - self.F*self.q1*self.N.x/length
176
+ + self.F*self.q2*self.N.y/length
177
+ - 2*self.F*self.q3*self.N.z/length
178
+ )
179
+ pI_force = (
180
+ self.F*self.q1*self.N.x/length
181
+ - self.F*self.q2*self.N.y/length
182
+ + 2*self.F*self.q3*self.N.z/length
183
+ )
184
+ expected = [
185
+ (self.pA, pO_force),
186
+ (self.pB, pI_force),
187
+ ]
188
+ assert self.pathway.to_loads(self.F) == expected
189
+
190
+
191
+ class TestObstacleSetPathway:
192
+
193
+ def test_is_pathway_base_subclass(self):
194
+ assert issubclass(ObstacleSetPathway, PathwayBase)
195
+
196
+ @staticmethod
197
+ @pytest.mark.parametrize(
198
+ 'num_attachments, attachments',
199
+ [
200
+ (3, [Point(name) for name in ('pO', 'pA', 'pI')]),
201
+ (4, [Point(name) for name in ('pO', 'pA', 'pB', 'pI')]),
202
+ (5, [Point(name) for name in ('pO', 'pA', 'pB', 'pC', 'pI')]),
203
+ (6, [Point(name) for name in ('pO', 'pA', 'pB', 'pC', 'pD', 'pI')]),
204
+ ]
205
+ )
206
+ def test_valid_constructor(num_attachments, attachments):
207
+ instance = ObstacleSetPathway(*attachments)
208
+ assert isinstance(instance, ObstacleSetPathway)
209
+ assert hasattr(instance, 'attachments')
210
+ assert len(instance.attachments) == num_attachments
211
+ for attachment in instance.attachments:
212
+ assert isinstance(attachment, Point)
213
+
214
+ @staticmethod
215
+ @pytest.mark.parametrize(
216
+ 'attachments',
217
+ [[Point('pO')], [Point('pO'), Point('pI')]],
218
+ )
219
+ def test_invalid_constructor_attachments_incorrect_number(attachments):
220
+ with pytest.raises(ValueError):
221
+ _ = ObstacleSetPathway(*attachments)
222
+
223
+ @staticmethod
224
+ @pytest.mark.parametrize(
225
+ 'attachments',
226
+ [
227
+ (None, Point('pA'), Point('pI')),
228
+ (Point('pO'), None, Point('pI')),
229
+ (Point('pO'), Point('pA'), None),
230
+ ]
231
+ )
232
+ def test_invalid_constructor_attachments_not_point(attachments):
233
+ with pytest.raises(TypeError):
234
+ _ = WrappingPathway(*attachments) # type: ignore
235
+
236
+ def test_properties_are_immutable(self):
237
+ pathway = ObstacleSetPathway(Point('pO'), Point('pA'), Point('pI'))
238
+ with pytest.raises(AttributeError):
239
+ pathway.attachments = None # type: ignore
240
+ with pytest.raises(TypeError):
241
+ pathway.attachments[0] = None # type: ignore
242
+ with pytest.raises(TypeError):
243
+ pathway.attachments[1] = None # type: ignore
244
+ with pytest.raises(TypeError):
245
+ pathway.attachments[-1] = None # type: ignore
246
+
247
+ @staticmethod
248
+ @pytest.mark.parametrize(
249
+ 'attachments, expected',
250
+ [
251
+ (
252
+ [Point(name) for name in ('pO', 'pA', 'pI')],
253
+ 'ObstacleSetPathway(pO, pA, pI)'
254
+ ),
255
+ (
256
+ [Point(name) for name in ('pO', 'pA', 'pB', 'pI')],
257
+ 'ObstacleSetPathway(pO, pA, pB, pI)'
258
+ ),
259
+ (
260
+ [Point(name) for name in ('pO', 'pA', 'pB', 'pC', 'pI')],
261
+ 'ObstacleSetPathway(pO, pA, pB, pC, pI)'
262
+ ),
263
+ ]
264
+ )
265
+ def test_repr(attachments, expected):
266
+ pathway = ObstacleSetPathway(*attachments)
267
+ assert repr(pathway) == expected
268
+
269
+ @pytest.fixture(autouse=True)
270
+ def _obstacle_set_pathway_fixture(self):
271
+ self.N = ReferenceFrame('N')
272
+ self.pO = Point('pO')
273
+ self.pI = Point('pI')
274
+ self.pA = Point('pA')
275
+ self.pB = Point('pB')
276
+ self.q = dynamicsymbols('q')
277
+ self.qd = dynamicsymbols('q', 1)
278
+ self.F = Symbol('F')
279
+
280
+ def test_static_pathway_length(self):
281
+ self.pA.set_pos(self.pO, self.N.x)
282
+ self.pB.set_pos(self.pO, self.N.y)
283
+ self.pI.set_pos(self.pO, self.N.z)
284
+ pathway = ObstacleSetPathway(self.pO, self.pA, self.pB, self.pI)
285
+ assert pathway.length == 1 + 2 * sqrt(2)
286
+
287
+ def test_static_pathway_extension_velocity(self):
288
+ self.pA.set_pos(self.pO, self.N.x)
289
+ self.pB.set_pos(self.pO, self.N.y)
290
+ self.pI.set_pos(self.pO, self.N.z)
291
+ pathway = ObstacleSetPathway(self.pO, self.pA, self.pB, self.pI)
292
+ assert pathway.extension_velocity == 0
293
+
294
+ def test_static_pathway_to_loads(self):
295
+ self.pA.set_pos(self.pO, self.N.x)
296
+ self.pB.set_pos(self.pO, self.N.y)
297
+ self.pI.set_pos(self.pO, self.N.z)
298
+ pathway = ObstacleSetPathway(self.pO, self.pA, self.pB, self.pI)
299
+ expected = [
300
+ Force(self.pO, -self.F * self.N.x),
301
+ Force(self.pA, self.F * self.N.x),
302
+ Force(self.pA, self.F * sqrt(2) / 2 * (self.N.x - self.N.y)),
303
+ Force(self.pB, self.F * sqrt(2) / 2 * (self.N.y - self.N.x)),
304
+ Force(self.pB, self.F * sqrt(2) / 2 * (self.N.y - self.N.z)),
305
+ Force(self.pI, self.F * sqrt(2) / 2 * (self.N.z - self.N.y)),
306
+ ]
307
+ assert pathway.to_loads(self.F) == expected
308
+
309
+ def test_2D_pathway_length(self):
310
+ self.pA.set_pos(self.pO, -(self.N.x + self.N.y))
311
+ self.pB.set_pos(
312
+ self.pO, cos(self.q) * self.N.x - (sin(self.q) + 1) * self.N.y
313
+ )
314
+ self.pI.set_pos(
315
+ self.pO, sin(self.q) * self.N.x + (cos(self.q) - 1) * self.N.y
316
+ )
317
+ pathway = ObstacleSetPathway(self.pO, self.pA, self.pB, self.pI)
318
+ expected = 2 * sqrt(2) + sqrt(2 + 2*cos(self.q))
319
+ assert (pathway.length - expected).simplify() == 0
320
+
321
+ def test_2D_pathway_extension_velocity(self):
322
+ self.pA.set_pos(self.pO, -(self.N.x + self.N.y))
323
+ self.pB.set_pos(
324
+ self.pO, cos(self.q) * self.N.x - (sin(self.q) + 1) * self.N.y
325
+ )
326
+ self.pI.set_pos(
327
+ self.pO, sin(self.q) * self.N.x + (cos(self.q) - 1) * self.N.y
328
+ )
329
+ pathway = ObstacleSetPathway(self.pO, self.pA, self.pB, self.pI)
330
+ expected = - (sqrt(2) * sin(self.q) * self.qd) / (2 * sqrt(cos(self.q) + 1))
331
+ assert (pathway.extension_velocity - expected).simplify() == 0
332
+
333
+ def test_2D_pathway_to_loads(self):
334
+ self.pA.set_pos(self.pO, -(self.N.x + self.N.y))
335
+ self.pB.set_pos(
336
+ self.pO, cos(self.q) * self.N.x - (sin(self.q) + 1) * self.N.y
337
+ )
338
+ self.pI.set_pos(
339
+ self.pO, sin(self.q) * self.N.x + (cos(self.q) - 1) * self.N.y
340
+ )
341
+ pathway = ObstacleSetPathway(self.pO, self.pA, self.pB, self.pI)
342
+ pO_pA_force_vec = sqrt(2) / 2 * (self.N.x + self.N.y)
343
+ pA_pB_force_vec = (
344
+ - sqrt(2 * cos(self.q) + 2) / 2 * self.N.x
345
+ + sqrt(2) * sin(self.q) / (2 * sqrt(cos(self.q) + 1)) * self.N.y
346
+ )
347
+ pB_pI_force_vec = cos(self.q + pi/4) * self.N.x - sin(self.q + pi/4) * self.N.y
348
+ expected = [
349
+ Force(self.pO, self.F * pO_pA_force_vec),
350
+ Force(self.pA, -self.F * pO_pA_force_vec),
351
+ Force(self.pA, self.F * pA_pB_force_vec),
352
+ Force(self.pB, -self.F * pA_pB_force_vec),
353
+ Force(self.pB, self.F * pB_pI_force_vec),
354
+ Force(self.pI, -self.F * pB_pI_force_vec),
355
+ ]
356
+ assert _simplify_loads(pathway.to_loads(self.F)) == expected
357
+
358
+
359
+ class TestWrappingPathway:
360
+
361
+ def test_is_pathway_base_subclass(self):
362
+ assert issubclass(WrappingPathway, PathwayBase)
363
+
364
+ @pytest.fixture(autouse=True)
365
+ def _wrapping_pathway_fixture(self):
366
+ self.pA = Point('pA')
367
+ self.pB = Point('pB')
368
+ self.r = Symbol('r', positive=True)
369
+ self.pO = Point('pO')
370
+ self.N = ReferenceFrame('N')
371
+ self.ax = self.N.z
372
+ self.sphere = WrappingSphere(self.r, self.pO)
373
+ self.cylinder = WrappingCylinder(self.r, self.pO, self.ax)
374
+ self.pathway = WrappingPathway(self.pA, self.pB, self.cylinder)
375
+ self.F = Symbol('F')
376
+
377
+ def test_valid_constructor(self):
378
+ instance = WrappingPathway(self.pA, self.pB, self.cylinder)
379
+ assert isinstance(instance, WrappingPathway)
380
+ assert hasattr(instance, 'attachments')
381
+ assert len(instance.attachments) == 2
382
+ assert isinstance(instance.attachments[0], Point)
383
+ assert instance.attachments[0] == self.pA
384
+ assert isinstance(instance.attachments[1], Point)
385
+ assert instance.attachments[1] == self.pB
386
+ assert hasattr(instance, 'geometry')
387
+ assert isinstance(instance.geometry, WrappingGeometryBase)
388
+ assert instance.geometry == self.cylinder
389
+
390
+ @pytest.mark.parametrize(
391
+ 'attachments',
392
+ [
393
+ (Point('pA'), ),
394
+ (Point('pA'), Point('pB'), Point('pZ')),
395
+ ]
396
+ )
397
+ def test_invalid_constructor_attachments_incorrect_number(self, attachments):
398
+ with pytest.raises(TypeError):
399
+ _ = WrappingPathway(*attachments, self.cylinder)
400
+
401
+ @staticmethod
402
+ @pytest.mark.parametrize(
403
+ 'attachments',
404
+ [
405
+ (None, Point('pB')),
406
+ (Point('pA'), None),
407
+ ]
408
+ )
409
+ def test_invalid_constructor_attachments_not_point(attachments):
410
+ with pytest.raises(TypeError):
411
+ _ = WrappingPathway(*attachments)
412
+
413
+ def test_invalid_constructor_geometry_is_not_supplied(self):
414
+ with pytest.raises(TypeError):
415
+ _ = WrappingPathway(self.pA, self.pB)
416
+
417
+ @pytest.mark.parametrize(
418
+ 'geometry',
419
+ [
420
+ Symbol('r'),
421
+ dynamicsymbols('q'),
422
+ ReferenceFrame('N'),
423
+ ReferenceFrame('N').x,
424
+ ]
425
+ )
426
+ def test_invalid_geometry_not_geometry(self, geometry):
427
+ with pytest.raises(TypeError):
428
+ _ = WrappingPathway(self.pA, self.pB, geometry)
429
+
430
+ def test_attachments_property_is_immutable(self):
431
+ with pytest.raises(TypeError):
432
+ self.pathway.attachments[0] = self.pB
433
+ with pytest.raises(TypeError):
434
+ self.pathway.attachments[1] = self.pA
435
+
436
+ def test_geometry_property_is_immutable(self):
437
+ with pytest.raises(AttributeError):
438
+ self.pathway.geometry = None
439
+
440
+ def test_repr(self):
441
+ expected = (
442
+ f'WrappingPathway(pA, pB, '
443
+ f'geometry={self.cylinder!r})'
444
+ )
445
+ assert repr(self.pathway) == expected
446
+
447
+ @staticmethod
448
+ def _expand_pos_to_vec(pos, frame):
449
+ return sum(mag*unit for (mag, unit) in zip(pos, frame))
450
+
451
+ @pytest.mark.parametrize(
452
+ 'pA_vec, pB_vec, factor',
453
+ [
454
+ ((1, 0, 0), (0, 1, 0), pi/2),
455
+ ((0, 1, 0), (sqrt(2)/2, -sqrt(2)/2, 0), 3*pi/4),
456
+ ((1, 0, 0), (Rational(1, 2), sqrt(3)/2, 0), pi/3),
457
+ ]
458
+ )
459
+ def test_static_pathway_on_sphere_length(self, pA_vec, pB_vec, factor):
460
+ pA_vec = self._expand_pos_to_vec(pA_vec, self.N)
461
+ pB_vec = self._expand_pos_to_vec(pB_vec, self.N)
462
+ self.pA.set_pos(self.pO, self.r*pA_vec)
463
+ self.pB.set_pos(self.pO, self.r*pB_vec)
464
+ pathway = WrappingPathway(self.pA, self.pB, self.sphere)
465
+ expected = factor*self.r
466
+ assert simplify(pathway.length - expected) == 0
467
+
468
+ @pytest.mark.parametrize(
469
+ 'pA_vec, pB_vec, factor',
470
+ [
471
+ ((1, 0, 0), (0, 1, 0), Rational(1, 2)*pi),
472
+ ((1, 0, 0), (-1, 0, 0), pi),
473
+ ((-1, 0, 0), (1, 0, 0), pi),
474
+ ((0, 1, 0), (sqrt(2)/2, -sqrt(2)/2, 0), 5*pi/4),
475
+ ((1, 0, 0), (Rational(1, 2), sqrt(3)/2, 0), pi/3),
476
+ (
477
+ (0, 1, 0),
478
+ (sqrt(2)*Rational(1, 2), -sqrt(2)*Rational(1, 2), 1),
479
+ sqrt(1 + (Rational(5, 4)*pi)**2),
480
+ ),
481
+ (
482
+ (1, 0, 0),
483
+ (Rational(1, 2), sqrt(3)*Rational(1, 2), 1),
484
+ sqrt(1 + (Rational(1, 3)*pi)**2),
485
+ ),
486
+ ]
487
+ )
488
+ def test_static_pathway_on_cylinder_length(self, pA_vec, pB_vec, factor):
489
+ pA_vec = self._expand_pos_to_vec(pA_vec, self.N)
490
+ pB_vec = self._expand_pos_to_vec(pB_vec, self.N)
491
+ self.pA.set_pos(self.pO, self.r*pA_vec)
492
+ self.pB.set_pos(self.pO, self.r*pB_vec)
493
+ pathway = WrappingPathway(self.pA, self.pB, self.cylinder)
494
+ expected = factor*sqrt(self.r**2)
495
+ assert simplify(pathway.length - expected) == 0
496
+
497
+ @pytest.mark.parametrize(
498
+ 'pA_vec, pB_vec',
499
+ [
500
+ ((1, 0, 0), (0, 1, 0)),
501
+ ((0, 1, 0), (sqrt(2)*Rational(1, 2), -sqrt(2)*Rational(1, 2), 0)),
502
+ ((1, 0, 0), (Rational(1, 2), sqrt(3)*Rational(1, 2), 0)),
503
+ ]
504
+ )
505
+ def test_static_pathway_on_sphere_extension_velocity(self, pA_vec, pB_vec):
506
+ pA_vec = self._expand_pos_to_vec(pA_vec, self.N)
507
+ pB_vec = self._expand_pos_to_vec(pB_vec, self.N)
508
+ self.pA.set_pos(self.pO, self.r*pA_vec)
509
+ self.pB.set_pos(self.pO, self.r*pB_vec)
510
+ pathway = WrappingPathway(self.pA, self.pB, self.sphere)
511
+ assert pathway.extension_velocity == 0
512
+
513
+ @pytest.mark.parametrize(
514
+ 'pA_vec, pB_vec',
515
+ [
516
+ ((1, 0, 0), (0, 1, 0)),
517
+ ((1, 0, 0), (-1, 0, 0)),
518
+ ((-1, 0, 0), (1, 0, 0)),
519
+ ((0, 1, 0), (sqrt(2)/2, -sqrt(2)/2, 0)),
520
+ ((1, 0, 0), (Rational(1, 2), sqrt(3)/2, 0)),
521
+ ((0, 1, 0), (sqrt(2)*Rational(1, 2), -sqrt(2)/2, 1)),
522
+ ((1, 0, 0), (Rational(1, 2), sqrt(3)/2, 1)),
523
+ ]
524
+ )
525
+ def test_static_pathway_on_cylinder_extension_velocity(self, pA_vec, pB_vec):
526
+ pA_vec = self._expand_pos_to_vec(pA_vec, self.N)
527
+ pB_vec = self._expand_pos_to_vec(pB_vec, self.N)
528
+ self.pA.set_pos(self.pO, self.r*pA_vec)
529
+ self.pB.set_pos(self.pO, self.r*pB_vec)
530
+ pathway = WrappingPathway(self.pA, self.pB, self.cylinder)
531
+ assert pathway.extension_velocity == 0
532
+
533
+ @pytest.mark.parametrize(
534
+ 'pA_vec, pB_vec, pA_vec_expected, pB_vec_expected, pO_vec_expected',
535
+ (
536
+ ((1, 0, 0), (0, 1, 0), (0, 1, 0), (1, 0, 0), (-1, -1, 0)),
537
+ (
538
+ (0, 1, 0),
539
+ (sqrt(2)/2, -sqrt(2)/2, 0),
540
+ (1, 0, 0),
541
+ (sqrt(2)/2, sqrt(2)/2, 0),
542
+ (-1 - sqrt(2)/2, -sqrt(2)/2, 0)
543
+ ),
544
+ (
545
+ (1, 0, 0),
546
+ (Rational(1, 2), sqrt(3)/2, 0),
547
+ (0, 1, 0),
548
+ (sqrt(3)/2, -Rational(1, 2), 0),
549
+ (-sqrt(3)/2, Rational(1, 2) - 1, 0),
550
+ ),
551
+ )
552
+ )
553
+ def test_static_pathway_on_sphere_to_loads(
554
+ self,
555
+ pA_vec,
556
+ pB_vec,
557
+ pA_vec_expected,
558
+ pB_vec_expected,
559
+ pO_vec_expected,
560
+ ):
561
+ pA_vec = self._expand_pos_to_vec(pA_vec, self.N)
562
+ pB_vec = self._expand_pos_to_vec(pB_vec, self.N)
563
+ self.pA.set_pos(self.pO, self.r*pA_vec)
564
+ self.pB.set_pos(self.pO, self.r*pB_vec)
565
+ pathway = WrappingPathway(self.pA, self.pB, self.sphere)
566
+
567
+ pA_vec_expected = sum(
568
+ mag*unit for (mag, unit) in zip(pA_vec_expected, self.N)
569
+ )
570
+ pB_vec_expected = sum(
571
+ mag*unit for (mag, unit) in zip(pB_vec_expected, self.N)
572
+ )
573
+ pO_vec_expected = sum(
574
+ mag*unit for (mag, unit) in zip(pO_vec_expected, self.N)
575
+ )
576
+ expected = [
577
+ Force(self.pA, self.F*(self.r**3/sqrt(self.r**6))*pA_vec_expected),
578
+ Force(self.pB, self.F*(self.r**3/sqrt(self.r**6))*pB_vec_expected),
579
+ Force(self.pO, self.F*(self.r**3/sqrt(self.r**6))*pO_vec_expected),
580
+ ]
581
+ assert pathway.to_loads(self.F) == expected
582
+
583
+ @pytest.mark.parametrize(
584
+ 'pA_vec, pB_vec, pA_vec_expected, pB_vec_expected, pO_vec_expected',
585
+ (
586
+ ((1, 0, 0), (0, 1, 0), (0, 1, 0), (1, 0, 0), (-1, -1, 0)),
587
+ ((1, 0, 0), (-1, 0, 0), (0, 1, 0), (0, 1, 0), (0, -2, 0)),
588
+ ((-1, 0, 0), (1, 0, 0), (0, -1, 0), (0, -1, 0), (0, 2, 0)),
589
+ (
590
+ (0, 1, 0),
591
+ (sqrt(2)/2, -sqrt(2)/2, 0),
592
+ (-1, 0, 0),
593
+ (-sqrt(2)/2, -sqrt(2)/2, 0),
594
+ (1 + sqrt(2)/2, sqrt(2)/2, 0)
595
+ ),
596
+ (
597
+ (1, 0, 0),
598
+ (Rational(1, 2), sqrt(3)/2, 0),
599
+ (0, 1, 0),
600
+ (sqrt(3)/2, -Rational(1, 2), 0),
601
+ (-sqrt(3)/2, Rational(1, 2) - 1, 0),
602
+ ),
603
+ (
604
+ (1, 0, 0),
605
+ (sqrt(2)/2, sqrt(2)/2, 0),
606
+ (0, 1, 0),
607
+ (sqrt(2)/2, -sqrt(2)/2, 0),
608
+ (-sqrt(2)/2, sqrt(2)/2 - 1, 0),
609
+ ),
610
+ ((0, 1, 0), (0, 1, 1), (0, 0, 1), (0, 0, -1), (0, 0, 0)),
611
+ (
612
+ (0, 1, 0),
613
+ (sqrt(2)/2, -sqrt(2)/2, 1),
614
+ (-5*pi/sqrt(16 + 25*pi**2), 0, 4/sqrt(16 + 25*pi**2)),
615
+ (
616
+ -5*sqrt(2)*pi/(2*sqrt(16 + 25*pi**2)),
617
+ -5*sqrt(2)*pi/(2*sqrt(16 + 25*pi**2)),
618
+ -4/sqrt(16 + 25*pi**2),
619
+ ),
620
+ (
621
+ 5*(sqrt(2) + 2)*pi/(2*sqrt(16 + 25*pi**2)),
622
+ 5*sqrt(2)*pi/(2*sqrt(16 + 25*pi**2)),
623
+ 0,
624
+ ),
625
+ ),
626
+ )
627
+ )
628
+ def test_static_pathway_on_cylinder_to_loads(
629
+ self,
630
+ pA_vec,
631
+ pB_vec,
632
+ pA_vec_expected,
633
+ pB_vec_expected,
634
+ pO_vec_expected,
635
+ ):
636
+ pA_vec = self._expand_pos_to_vec(pA_vec, self.N)
637
+ pB_vec = self._expand_pos_to_vec(pB_vec, self.N)
638
+ self.pA.set_pos(self.pO, self.r*pA_vec)
639
+ self.pB.set_pos(self.pO, self.r*pB_vec)
640
+ pathway = WrappingPathway(self.pA, self.pB, self.cylinder)
641
+
642
+ pA_force_expected = self.F*self._expand_pos_to_vec(pA_vec_expected,
643
+ self.N)
644
+ pB_force_expected = self.F*self._expand_pos_to_vec(pB_vec_expected,
645
+ self.N)
646
+ pO_force_expected = self.F*self._expand_pos_to_vec(pO_vec_expected,
647
+ self.N)
648
+ expected = [
649
+ Force(self.pA, pA_force_expected),
650
+ Force(self.pB, pB_force_expected),
651
+ Force(self.pO, pO_force_expected),
652
+ ]
653
+ assert _simplify_loads(pathway.to_loads(self.F)) == expected
654
+
655
+ def test_2D_pathway_on_cylinder_length(self):
656
+ q = dynamicsymbols('q')
657
+ pA_pos = self.r*self.N.x
658
+ pB_pos = self.r*(cos(q)*self.N.x + sin(q)*self.N.y)
659
+ self.pA.set_pos(self.pO, pA_pos)
660
+ self.pB.set_pos(self.pO, pB_pos)
661
+ expected = self.r*sqrt(q**2)
662
+ assert simplify(self.pathway.length - expected) == 0
663
+
664
+ def test_2D_pathway_on_cylinder_extension_velocity(self):
665
+ q = dynamicsymbols('q')
666
+ qd = dynamicsymbols('q', 1)
667
+ pA_pos = self.r*self.N.x
668
+ pB_pos = self.r*(cos(q)*self.N.x + sin(q)*self.N.y)
669
+ self.pA.set_pos(self.pO, pA_pos)
670
+ self.pB.set_pos(self.pO, pB_pos)
671
+ expected = self.r*(sqrt(q**2)/q)*qd
672
+ assert simplify(self.pathway.extension_velocity - expected) == 0
673
+
674
+ def test_2D_pathway_on_cylinder_to_loads(self):
675
+ q = dynamicsymbols('q')
676
+ pA_pos = self.r*self.N.x
677
+ pB_pos = self.r*(cos(q)*self.N.x + sin(q)*self.N.y)
678
+ self.pA.set_pos(self.pO, pA_pos)
679
+ self.pB.set_pos(self.pO, pB_pos)
680
+
681
+ pA_force = self.F*self.N.y
682
+ pB_force = self.F*(sin(q)*self.N.x - cos(q)*self.N.y)
683
+ pO_force = self.F*(-sin(q)*self.N.x + (cos(q) - 1)*self.N.y)
684
+ expected = [
685
+ Force(self.pA, pA_force),
686
+ Force(self.pB, pB_force),
687
+ Force(self.pO, pO_force),
688
+ ]
689
+
690
+ loads = _simplify_loads(self.pathway.to_loads(self.F))
691
+ assert loads == expected
lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_rigidbody.py ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sympy.physics.mechanics import Point, ReferenceFrame, Dyadic, RigidBody
2
+ from sympy.physics.mechanics import dynamicsymbols, outer, inertia, Inertia
3
+ from sympy.physics.mechanics import inertia_of_point_mass
4
+ from sympy import expand, zeros, simplify, symbols
5
+ from sympy.testing.pytest import raises, warns_deprecated_sympy
6
+
7
+
8
+ def test_rigidbody_default():
9
+ # Test default
10
+ b = RigidBody('B')
11
+ I = inertia(b.frame, *symbols('B_ixx B_iyy B_izz B_ixy B_iyz B_izx'))
12
+ assert b.name == 'B'
13
+ assert b.mass == symbols('B_mass')
14
+ assert b.masscenter.name == 'B_masscenter'
15
+ assert b.inertia == (I, b.masscenter)
16
+ assert b.central_inertia == I
17
+ assert b.frame.name == 'B_frame'
18
+ assert b.__str__() == 'B'
19
+ assert b.__repr__() == (
20
+ "RigidBody('B', masscenter=B_masscenter, frame=B_frame, mass=B_mass, "
21
+ "inertia=Inertia(dyadic=B_ixx*(B_frame.x|B_frame.x) + "
22
+ "B_ixy*(B_frame.x|B_frame.y) + B_izx*(B_frame.x|B_frame.z) + "
23
+ "B_ixy*(B_frame.y|B_frame.x) + B_iyy*(B_frame.y|B_frame.y) + "
24
+ "B_iyz*(B_frame.y|B_frame.z) + B_izx*(B_frame.z|B_frame.x) + "
25
+ "B_iyz*(B_frame.z|B_frame.y) + B_izz*(B_frame.z|B_frame.z), "
26
+ "point=B_masscenter))")
27
+
28
+
29
+ def test_rigidbody():
30
+ m, m2, v1, v2, v3, omega = symbols('m m2 v1 v2 v3 omega')
31
+ A = ReferenceFrame('A')
32
+ A2 = ReferenceFrame('A2')
33
+ P = Point('P')
34
+ P2 = Point('P2')
35
+ I = Dyadic(0)
36
+ I2 = Dyadic(0)
37
+ B = RigidBody('B', P, A, m, (I, P))
38
+ assert B.mass == m
39
+ assert B.frame == A
40
+ assert B.masscenter == P
41
+ assert B.inertia == (I, B.masscenter)
42
+
43
+ B.mass = m2
44
+ B.frame = A2
45
+ B.masscenter = P2
46
+ B.inertia = (I2, B.masscenter)
47
+ raises(TypeError, lambda: RigidBody(P, P, A, m, (I, P)))
48
+ raises(TypeError, lambda: RigidBody('B', P, P, m, (I, P)))
49
+ raises(TypeError, lambda: RigidBody('B', P, A, m, (P, P)))
50
+ raises(TypeError, lambda: RigidBody('B', P, A, m, (I, I)))
51
+ assert B.__str__() == 'B'
52
+ assert B.mass == m2
53
+ assert B.frame == A2
54
+ assert B.masscenter == P2
55
+ assert B.inertia == (I2, B.masscenter)
56
+ assert isinstance(B.inertia, Inertia)
57
+
58
+ # Testing linear momentum function assuming A2 is the inertial frame
59
+ N = ReferenceFrame('N')
60
+ P2.set_vel(N, v1 * N.x + v2 * N.y + v3 * N.z)
61
+ assert B.linear_momentum(N) == m2 * (v1 * N.x + v2 * N.y + v3 * N.z)
62
+
63
+
64
+ def test_rigidbody2():
65
+ M, v, r, omega, g, h = dynamicsymbols('M v r omega g h')
66
+ N = ReferenceFrame('N')
67
+ b = ReferenceFrame('b')
68
+ b.set_ang_vel(N, omega * b.x)
69
+ P = Point('P')
70
+ I = outer(b.x, b.x)
71
+ Inertia_tuple = (I, P)
72
+ B = RigidBody('B', P, b, M, Inertia_tuple)
73
+ P.set_vel(N, v * b.x)
74
+ assert B.angular_momentum(P, N) == omega * b.x
75
+ O = Point('O')
76
+ O.set_vel(N, v * b.x)
77
+ P.set_pos(O, r * b.y)
78
+ assert B.angular_momentum(O, N) == omega * b.x - M*v*r*b.z
79
+ B.potential_energy = M * g * h
80
+ assert B.potential_energy == M * g * h
81
+ assert expand(2 * B.kinetic_energy(N)) == omega**2 + M * v**2
82
+
83
+
84
+ def test_rigidbody3():
85
+ q1, q2, q3, q4 = dynamicsymbols('q1:5')
86
+ p1, p2, p3 = symbols('p1:4')
87
+ m = symbols('m')
88
+
89
+ A = ReferenceFrame('A')
90
+ B = A.orientnew('B', 'axis', [q1, A.x])
91
+ O = Point('O')
92
+ O.set_vel(A, q2*A.x + q3*A.y + q4*A.z)
93
+ P = O.locatenew('P', p1*B.x + p2*B.y + p3*B.z)
94
+ P.v2pt_theory(O, A, B)
95
+ I = outer(B.x, B.x)
96
+
97
+ rb1 = RigidBody('rb1', P, B, m, (I, P))
98
+ # I_S/O = I_S/S* + I_S*/O
99
+ rb2 = RigidBody('rb2', P, B, m,
100
+ (I + inertia_of_point_mass(m, P.pos_from(O), B), O))
101
+
102
+ assert rb1.central_inertia == rb2.central_inertia
103
+ assert rb1.angular_momentum(O, A) == rb2.angular_momentum(O, A)
104
+
105
+
106
+ def test_pendulum_angular_momentum():
107
+ """Consider a pendulum of length OA = 2a, of mass m as a rigid body of
108
+ center of mass G (OG = a) which turn around (O,z). The angle between the
109
+ reference frame R and the rod is q. The inertia of the body is I =
110
+ (G,0,ma^2/3,ma^2/3). """
111
+
112
+ m, a = symbols('m, a')
113
+ q = dynamicsymbols('q')
114
+
115
+ R = ReferenceFrame('R')
116
+ R1 = R.orientnew('R1', 'Axis', [q, R.z])
117
+ R1.set_ang_vel(R, q.diff() * R.z)
118
+
119
+ I = inertia(R1, 0, m * a**2 / 3, m * a**2 / 3)
120
+
121
+ O = Point('O')
122
+
123
+ A = O.locatenew('A', 2*a * R1.x)
124
+ G = O.locatenew('G', a * R1.x)
125
+
126
+ S = RigidBody('S', G, R1, m, (I, G))
127
+
128
+ O.set_vel(R, 0)
129
+ A.v2pt_theory(O, R, R1)
130
+ G.v2pt_theory(O, R, R1)
131
+
132
+ assert (4 * m * a**2 / 3 * q.diff() * R.z -
133
+ S.angular_momentum(O, R).express(R)) == 0
134
+
135
+
136
+ def test_rigidbody_inertia():
137
+ N = ReferenceFrame('N')
138
+ m, Ix, Iy, Iz, a, b = symbols('m, I_x, I_y, I_z, a, b')
139
+ Io = inertia(N, Ix, Iy, Iz)
140
+ o = Point('o')
141
+ p = o.locatenew('p', a * N.x + b * N.y)
142
+ R = RigidBody('R', o, N, m, (Io, p))
143
+ I_check = inertia(N, Ix - b ** 2 * m, Iy - a ** 2 * m,
144
+ Iz - m * (a ** 2 + b ** 2), m * a * b)
145
+ assert isinstance(R.inertia, Inertia)
146
+ assert R.inertia == (Io, p)
147
+ assert R.central_inertia == I_check
148
+ R.central_inertia = Io
149
+ assert R.inertia == (Io, o)
150
+ assert R.central_inertia == Io
151
+ R.inertia = (Io, p)
152
+ assert R.inertia == (Io, p)
153
+ assert R.central_inertia == I_check
154
+ # parse Inertia object
155
+ R.inertia = Inertia(Io, o)
156
+ assert R.inertia == (Io, o)
157
+
158
+
159
+ def test_parallel_axis():
160
+ N = ReferenceFrame('N')
161
+ m, Ix, Iy, Iz, a, b = symbols('m, I_x, I_y, I_z, a, b')
162
+ Io = inertia(N, Ix, Iy, Iz)
163
+ o = Point('o')
164
+ p = o.locatenew('p', a * N.x + b * N.y)
165
+ R = RigidBody('R', o, N, m, (Io, o))
166
+ Ip = R.parallel_axis(p)
167
+ Ip_expected = inertia(N, Ix + m * b**2, Iy + m * a**2,
168
+ Iz + m * (a**2 + b**2), ixy=-m * a * b)
169
+ assert Ip == Ip_expected
170
+ # Reference frame from which the parallel axis is viewed should not matter
171
+ A = ReferenceFrame('A')
172
+ A.orient_axis(N, N.z, 1)
173
+ assert simplify(
174
+ (R.parallel_axis(p, A) - Ip_expected).to_matrix(A)) == zeros(3, 3)
175
+
176
+
177
+ def test_deprecated_set_potential_energy():
178
+ m, g, h = symbols('m g h')
179
+ A = ReferenceFrame('A')
180
+ P = Point('P')
181
+ I = Dyadic(0)
182
+ B = RigidBody('B', P, A, m, (I, P))
183
+ with warns_deprecated_sympy():
184
+ B.set_potential_energy(m*g*h)
lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_system.py ADDED
@@ -0,0 +1,245 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sympy import symbols, Matrix, atan, zeros
2
+ from sympy.simplify.simplify import simplify
3
+ from sympy.physics.mechanics import (dynamicsymbols, Particle, Point,
4
+ ReferenceFrame, SymbolicSystem)
5
+ from sympy.testing.pytest import raises
6
+
7
+ # This class is going to be tested using a simple pendulum set up in x and y
8
+ # coordinates
9
+ x, y, u, v, lam = dynamicsymbols('x y u v lambda')
10
+ m, l, g = symbols('m l g')
11
+
12
+ # Set up the different forms the equations can take
13
+ # [1] Explicit form where the kinematics and dynamics are combined
14
+ # x' = F(x, t, r, p)
15
+ #
16
+ # [2] Implicit form where the kinematics and dynamics are combined
17
+ # M(x, p) x' = F(x, t, r, p)
18
+ #
19
+ # [3] Implicit form where the kinematics and dynamics are separate
20
+ # M(q, p) u' = F(q, u, t, r, p)
21
+ # q' = G(q, u, t, r, p)
22
+ dyn_implicit_mat = Matrix([[1, 0, -x/m],
23
+ [0, 1, -y/m],
24
+ [0, 0, l**2/m]])
25
+
26
+ dyn_implicit_rhs = Matrix([0, 0, u**2 + v**2 - g*y])
27
+
28
+ comb_implicit_mat = Matrix([[1, 0, 0, 0, 0],
29
+ [0, 1, 0, 0, 0],
30
+ [0, 0, 1, 0, -x/m],
31
+ [0, 0, 0, 1, -y/m],
32
+ [0, 0, 0, 0, l**2/m]])
33
+
34
+ comb_implicit_rhs = Matrix([u, v, 0, 0, u**2 + v**2 - g*y])
35
+
36
+ kin_explicit_rhs = Matrix([u, v])
37
+
38
+ comb_explicit_rhs = comb_implicit_mat.LUsolve(comb_implicit_rhs)
39
+
40
+ # Set up a body and load to pass into the system
41
+ theta = atan(x/y)
42
+ N = ReferenceFrame('N')
43
+ A = N.orientnew('A', 'Axis', [theta, N.z])
44
+ O = Point('O')
45
+ P = O.locatenew('P', l * A.x)
46
+
47
+ Pa = Particle('Pa', P, m)
48
+
49
+ bodies = [Pa]
50
+ loads = [(P, g * m * N.x)]
51
+
52
+ # Set up some output equations to be given to SymbolicSystem
53
+ # Change to make these fit the pendulum
54
+ PE = symbols("PE")
55
+ out_eqns = {PE: m*g*(l+y)}
56
+
57
+ # Set up remaining arguments that can be passed to SymbolicSystem
58
+ alg_con = [2]
59
+ alg_con_full = [4]
60
+ coordinates = (x, y, lam)
61
+ speeds = (u, v)
62
+ states = (x, y, u, v, lam)
63
+ coord_idxs = (0, 1)
64
+ speed_idxs = (2, 3)
65
+
66
+
67
+ def test_form_1():
68
+ symsystem1 = SymbolicSystem(states, comb_explicit_rhs,
69
+ alg_con=alg_con_full, output_eqns=out_eqns,
70
+ coord_idxs=coord_idxs, speed_idxs=speed_idxs,
71
+ bodies=bodies, loads=loads)
72
+
73
+ assert symsystem1.coordinates == Matrix([x, y])
74
+ assert symsystem1.speeds == Matrix([u, v])
75
+ assert symsystem1.states == Matrix([x, y, u, v, lam])
76
+
77
+ assert symsystem1.alg_con == [4]
78
+
79
+ inter = comb_explicit_rhs
80
+ assert simplify(symsystem1.comb_explicit_rhs - inter) == zeros(5, 1)
81
+
82
+ assert set(symsystem1.dynamic_symbols()) == {y, v, lam, u, x}
83
+ assert type(symsystem1.dynamic_symbols()) == tuple
84
+ assert set(symsystem1.constant_symbols()) == {l, g, m}
85
+ assert type(symsystem1.constant_symbols()) == tuple
86
+
87
+ assert symsystem1.output_eqns == out_eqns
88
+
89
+ assert symsystem1.bodies == (Pa,)
90
+ assert symsystem1.loads == ((P, g * m * N.x),)
91
+
92
+
93
+ def test_form_2():
94
+ symsystem2 = SymbolicSystem(coordinates, comb_implicit_rhs, speeds=speeds,
95
+ mass_matrix=comb_implicit_mat,
96
+ alg_con=alg_con_full, output_eqns=out_eqns,
97
+ bodies=bodies, loads=loads)
98
+
99
+ assert symsystem2.coordinates == Matrix([x, y, lam])
100
+ assert symsystem2.speeds == Matrix([u, v])
101
+ assert symsystem2.states == Matrix([x, y, lam, u, v])
102
+
103
+ assert symsystem2.alg_con == [4]
104
+
105
+ inter = comb_implicit_rhs
106
+ assert simplify(symsystem2.comb_implicit_rhs - inter) == zeros(5, 1)
107
+ assert simplify(symsystem2.comb_implicit_mat-comb_implicit_mat) == zeros(5)
108
+
109
+ assert set(symsystem2.dynamic_symbols()) == {y, v, lam, u, x}
110
+ assert type(symsystem2.dynamic_symbols()) == tuple
111
+ assert set(symsystem2.constant_symbols()) == {l, g, m}
112
+ assert type(symsystem2.constant_symbols()) == tuple
113
+
114
+ inter = comb_explicit_rhs
115
+ symsystem2.compute_explicit_form()
116
+ assert simplify(symsystem2.comb_explicit_rhs - inter) == zeros(5, 1)
117
+
118
+
119
+ assert symsystem2.output_eqns == out_eqns
120
+
121
+ assert symsystem2.bodies == (Pa,)
122
+ assert symsystem2.loads == ((P, g * m * N.x),)
123
+
124
+
125
+ def test_form_3():
126
+ symsystem3 = SymbolicSystem(states, dyn_implicit_rhs,
127
+ mass_matrix=dyn_implicit_mat,
128
+ coordinate_derivatives=kin_explicit_rhs,
129
+ alg_con=alg_con, coord_idxs=coord_idxs,
130
+ speed_idxs=speed_idxs, bodies=bodies,
131
+ loads=loads)
132
+
133
+ assert symsystem3.coordinates == Matrix([x, y])
134
+ assert symsystem3.speeds == Matrix([u, v])
135
+ assert symsystem3.states == Matrix([x, y, u, v, lam])
136
+
137
+ assert symsystem3.alg_con == [4]
138
+
139
+ inter1 = kin_explicit_rhs
140
+ inter2 = dyn_implicit_rhs
141
+ assert simplify(symsystem3.kin_explicit_rhs - inter1) == zeros(2, 1)
142
+ assert simplify(symsystem3.dyn_implicit_mat - dyn_implicit_mat) == zeros(3)
143
+ assert simplify(symsystem3.dyn_implicit_rhs - inter2) == zeros(3, 1)
144
+
145
+ inter = comb_implicit_rhs
146
+ assert simplify(symsystem3.comb_implicit_rhs - inter) == zeros(5, 1)
147
+ assert simplify(symsystem3.comb_implicit_mat-comb_implicit_mat) == zeros(5)
148
+
149
+ inter = comb_explicit_rhs
150
+ symsystem3.compute_explicit_form()
151
+ assert simplify(symsystem3.comb_explicit_rhs - inter) == zeros(5, 1)
152
+
153
+ assert set(symsystem3.dynamic_symbols()) == {y, v, lam, u, x}
154
+ assert type(symsystem3.dynamic_symbols()) == tuple
155
+ assert set(symsystem3.constant_symbols()) == {l, g, m}
156
+ assert type(symsystem3.constant_symbols()) == tuple
157
+
158
+ assert symsystem3.output_eqns == {}
159
+
160
+ assert symsystem3.bodies == (Pa,)
161
+ assert symsystem3.loads == ((P, g * m * N.x),)
162
+
163
+
164
+ def test_property_attributes():
165
+ symsystem = SymbolicSystem(states, comb_explicit_rhs,
166
+ alg_con=alg_con_full, output_eqns=out_eqns,
167
+ coord_idxs=coord_idxs, speed_idxs=speed_idxs,
168
+ bodies=bodies, loads=loads)
169
+
170
+ with raises(AttributeError):
171
+ symsystem.bodies = 42
172
+ with raises(AttributeError):
173
+ symsystem.coordinates = 42
174
+ with raises(AttributeError):
175
+ symsystem.dyn_implicit_rhs = 42
176
+ with raises(AttributeError):
177
+ symsystem.comb_implicit_rhs = 42
178
+ with raises(AttributeError):
179
+ symsystem.loads = 42
180
+ with raises(AttributeError):
181
+ symsystem.dyn_implicit_mat = 42
182
+ with raises(AttributeError):
183
+ symsystem.comb_implicit_mat = 42
184
+ with raises(AttributeError):
185
+ symsystem.kin_explicit_rhs = 42
186
+ with raises(AttributeError):
187
+ symsystem.comb_explicit_rhs = 42
188
+ with raises(AttributeError):
189
+ symsystem.speeds = 42
190
+ with raises(AttributeError):
191
+ symsystem.states = 42
192
+ with raises(AttributeError):
193
+ symsystem.alg_con = 42
194
+
195
+
196
+ def test_not_specified_errors():
197
+ """This test will cover errors that arise from trying to access attributes
198
+ that were not specified upon object creation or were specified on creation
199
+ and the user tries to recalculate them."""
200
+ # Trying to access form 2 when form 1 given
201
+ # Trying to access form 3 when form 2 given
202
+
203
+ symsystem1 = SymbolicSystem(states, comb_explicit_rhs)
204
+
205
+ with raises(AttributeError):
206
+ symsystem1.comb_implicit_mat
207
+ with raises(AttributeError):
208
+ symsystem1.comb_implicit_rhs
209
+ with raises(AttributeError):
210
+ symsystem1.dyn_implicit_mat
211
+ with raises(AttributeError):
212
+ symsystem1.dyn_implicit_rhs
213
+ with raises(AttributeError):
214
+ symsystem1.kin_explicit_rhs
215
+ with raises(AttributeError):
216
+ symsystem1.compute_explicit_form()
217
+
218
+ symsystem2 = SymbolicSystem(coordinates, comb_implicit_rhs, speeds=speeds,
219
+ mass_matrix=comb_implicit_mat)
220
+
221
+ with raises(AttributeError):
222
+ symsystem2.dyn_implicit_mat
223
+ with raises(AttributeError):
224
+ symsystem2.dyn_implicit_rhs
225
+ with raises(AttributeError):
226
+ symsystem2.kin_explicit_rhs
227
+
228
+ # Attribute error when trying to access coordinates and speeds when only the
229
+ # states were given.
230
+ with raises(AttributeError):
231
+ symsystem1.coordinates
232
+ with raises(AttributeError):
233
+ symsystem1.speeds
234
+
235
+ # Attribute error when trying to access bodies and loads when they are not
236
+ # given
237
+ with raises(AttributeError):
238
+ symsystem1.bodies
239
+ with raises(AttributeError):
240
+ symsystem1.loads
241
+
242
+ # Attribute error when trying to access comb_explicit_rhs before it was
243
+ # calculated
244
+ with raises(AttributeError):
245
+ symsystem2.comb_explicit_rhs
lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_system_class.py ADDED
@@ -0,0 +1,831 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+
3
+ from sympy.core.symbol import symbols
4
+ from sympy.core.sympify import sympify
5
+ from sympy.functions.elementary.trigonometric import cos, sin
6
+ from sympy.matrices.dense import eye, zeros
7
+ from sympy.matrices.immutable import ImmutableMatrix
8
+ from sympy.physics.mechanics import (
9
+ Force, KanesMethod, LagrangesMethod, Particle, PinJoint, Point,
10
+ PrismaticJoint, ReferenceFrame, RigidBody, Torque, TorqueActuator, System,
11
+ dynamicsymbols)
12
+ from sympy.simplify.simplify import simplify
13
+ from sympy.solvers.solvers import solve
14
+
15
+ t = dynamicsymbols._t # type: ignore
16
+ q = dynamicsymbols('q:6') # type: ignore
17
+ qd = dynamicsymbols('q:6', 1) # type: ignore
18
+ u = dynamicsymbols('u:6') # type: ignore
19
+ ua = dynamicsymbols('ua:3') # type: ignore
20
+
21
+
22
+ class TestSystemBase:
23
+ @pytest.fixture()
24
+ def _empty_system_setup(self):
25
+ self.system = System(ReferenceFrame('frame'), Point('fixed_point'))
26
+
27
+ def _empty_system_check(self, exclude=()):
28
+ matrices = ('q_ind', 'q_dep', 'q', 'u_ind', 'u_dep', 'u', 'u_aux',
29
+ 'kdes', 'holonomic_constraints', 'nonholonomic_constraints')
30
+ tuples = ('loads', 'bodies', 'joints', 'actuators')
31
+ for attr in matrices:
32
+ if attr not in exclude:
33
+ assert getattr(self.system, attr)[:] == []
34
+ for attr in tuples:
35
+ if attr not in exclude:
36
+ assert getattr(self.system, attr) == ()
37
+ if 'eom_method' not in exclude:
38
+ assert self.system.eom_method is None
39
+
40
+ def _create_filled_system(self, with_speeds=True):
41
+ self.system = System(ReferenceFrame('frame'), Point('fixed_point'))
42
+ u = dynamicsymbols('u:6') if with_speeds else qd
43
+ self.bodies = symbols('rb1:5', cls=RigidBody)
44
+ self.joints = (
45
+ PinJoint('J1', self.bodies[0], self.bodies[1], q[0], u[0]),
46
+ PrismaticJoint('J2', self.bodies[1], self.bodies[2], q[1], u[1]),
47
+ PinJoint('J3', self.bodies[2], self.bodies[3], q[2], u[2])
48
+ )
49
+ self.system.add_joints(*self.joints)
50
+ self.system.add_coordinates(q[3], independent=[False])
51
+ self.system.add_speeds(u[3], independent=False)
52
+ if with_speeds:
53
+ self.system.add_kdes(u[3] - qd[3])
54
+ self.system.add_auxiliary_speeds(ua[0], ua[1])
55
+ self.system.add_holonomic_constraints(q[2] - q[0] + q[1])
56
+ self.system.add_nonholonomic_constraints(u[3] - qd[1] + u[2])
57
+ self.system.u_ind = u[:2]
58
+ self.system.u_dep = u[2:4]
59
+ self.q_ind, self.q_dep = self.system.q_ind[:], self.system.q_dep[:]
60
+ self.u_ind, self.u_dep = self.system.u_ind[:], self.system.u_dep[:]
61
+ self.kdes = self.system.kdes[:]
62
+ self.hc = self.system.holonomic_constraints[:]
63
+ self.vc = self.system.velocity_constraints[:]
64
+ self.nhc = self.system.nonholonomic_constraints[:]
65
+
66
+ @pytest.fixture()
67
+ def _filled_system_setup(self):
68
+ self._create_filled_system(with_speeds=True)
69
+
70
+ @pytest.fixture()
71
+ def _filled_system_setup_no_speeds(self):
72
+ self._create_filled_system(with_speeds=False)
73
+
74
+ def _filled_system_check(self, exclude=()):
75
+ assert 'q_ind' in exclude or self.system.q_ind[:] == q[:3]
76
+ assert 'q_dep' in exclude or self.system.q_dep[:] == [q[3]]
77
+ assert 'q' in exclude or self.system.q[:] == q[:4]
78
+ assert 'u_ind' in exclude or self.system.u_ind[:] == u[:2]
79
+ assert 'u_dep' in exclude or self.system.u_dep[:] == u[2:4]
80
+ assert 'u' in exclude or self.system.u[:] == u[:4]
81
+ assert 'u_aux' in exclude or self.system.u_aux[:] == ua[:2]
82
+ assert 'kdes' in exclude or self.system.kdes[:] == [
83
+ ui - qdi for ui, qdi in zip(u[:4], qd[:4])]
84
+ assert ('holonomic_constraints' in exclude or
85
+ self.system.holonomic_constraints[:] == [q[2] - q[0] + q[1]])
86
+ assert ('nonholonomic_constraints' in exclude or
87
+ self.system.nonholonomic_constraints[:] == [u[3] - qd[1] + u[2]]
88
+ )
89
+ assert ('velocity_constraints' in exclude or
90
+ self.system.velocity_constraints[:] == [
91
+ qd[2] - qd[0] + qd[1], u[3] - qd[1] + u[2]])
92
+ assert ('bodies' in exclude or
93
+ self.system.bodies == tuple(self.bodies))
94
+ assert ('joints' in exclude or
95
+ self.system.joints == tuple(self.joints))
96
+
97
+ @pytest.fixture()
98
+ def _moving_point_mass(self, _empty_system_setup):
99
+ self.system.q_ind = q[0]
100
+ self.system.u_ind = u[0]
101
+ self.system.kdes = u[0] - q[0].diff(t)
102
+ p = Particle('p', mass=symbols('m'))
103
+ self.system.add_bodies(p)
104
+ p.masscenter.set_pos(self.system.fixed_point, q[0] * self.system.x)
105
+
106
+
107
+ class TestSystem(TestSystemBase):
108
+ def test_empty_system(self, _empty_system_setup):
109
+ self._empty_system_check()
110
+ self.system.validate_system()
111
+
112
+ def test_filled_system(self, _filled_system_setup):
113
+ self._filled_system_check()
114
+ self.system.validate_system()
115
+
116
+ @pytest.mark.parametrize('frame', [None, ReferenceFrame('frame')])
117
+ @pytest.mark.parametrize('fixed_point', [None, Point('fixed_point')])
118
+ def test_init(self, frame, fixed_point):
119
+ if fixed_point is None and frame is None:
120
+ self.system = System()
121
+ else:
122
+ self.system = System(frame, fixed_point)
123
+ if fixed_point is None:
124
+ assert self.system.fixed_point.name == 'inertial_point'
125
+ else:
126
+ assert self.system.fixed_point == fixed_point
127
+ if frame is None:
128
+ assert self.system.frame.name == 'inertial_frame'
129
+ else:
130
+ assert self.system.frame == frame
131
+ self._empty_system_check()
132
+ assert isinstance(self.system.q_ind, ImmutableMatrix)
133
+ assert isinstance(self.system.q_dep, ImmutableMatrix)
134
+ assert isinstance(self.system.q, ImmutableMatrix)
135
+ assert isinstance(self.system.u_ind, ImmutableMatrix)
136
+ assert isinstance(self.system.u_dep, ImmutableMatrix)
137
+ assert isinstance(self.system.u, ImmutableMatrix)
138
+ assert isinstance(self.system.kdes, ImmutableMatrix)
139
+ assert isinstance(self.system.holonomic_constraints, ImmutableMatrix)
140
+ assert isinstance(self.system.nonholonomic_constraints, ImmutableMatrix)
141
+
142
+ def test_from_newtonian_rigid_body(self):
143
+ rb = RigidBody('body')
144
+ self.system = System.from_newtonian(rb)
145
+ assert self.system.fixed_point == rb.masscenter
146
+ assert self.system.frame == rb.frame
147
+ self._empty_system_check(exclude=('bodies',))
148
+ self.system.bodies = (rb,)
149
+
150
+ def test_from_newtonian_particle(self):
151
+ pt = Particle('particle')
152
+ with pytest.raises(TypeError):
153
+ System.from_newtonian(pt)
154
+
155
+ @pytest.mark.parametrize('args, kwargs, exp_q_ind, exp_q_dep, exp_q', [
156
+ (q[:3], {}, q[:3], [], q[:3]),
157
+ (q[:3], {'independent': True}, q[:3], [], q[:3]),
158
+ (q[:3], {'independent': False}, [], q[:3], q[:3]),
159
+ (q[:3], {'independent': [True, False, True]}, [q[0], q[2]], [q[1]],
160
+ [q[0], q[2], q[1]]),
161
+ ])
162
+ def test_coordinates(self, _empty_system_setup, args, kwargs,
163
+ exp_q_ind, exp_q_dep, exp_q):
164
+ # Test add_coordinates
165
+ self.system.add_coordinates(*args, **kwargs)
166
+ assert self.system.q_ind[:] == exp_q_ind
167
+ assert self.system.q_dep[:] == exp_q_dep
168
+ assert self.system.q[:] == exp_q
169
+ self._empty_system_check(exclude=('q_ind', 'q_dep', 'q'))
170
+ # Test setter for q_ind and q_dep
171
+ self.system.q_ind = exp_q_ind
172
+ self.system.q_dep = exp_q_dep
173
+ assert self.system.q_ind[:] == exp_q_ind
174
+ assert self.system.q_dep[:] == exp_q_dep
175
+ assert self.system.q[:] == exp_q
176
+ self._empty_system_check(exclude=('q_ind', 'q_dep', 'q'))
177
+
178
+ @pytest.mark.parametrize('func', ['add_coordinates', 'add_speeds'])
179
+ @pytest.mark.parametrize('args, kwargs', [
180
+ ((q[0], q[5]), {}),
181
+ ((u[0], u[5]), {}),
182
+ ((q[0],), {'independent': False}),
183
+ ((u[0],), {'independent': False}),
184
+ ((u[0], q[5]), {}),
185
+ ((symbols('a'), q[5]), {}),
186
+ ])
187
+ def test_coordinates_speeds_invalid(self, _filled_system_setup, func, args,
188
+ kwargs):
189
+ with pytest.raises(ValueError):
190
+ getattr(self.system, func)(*args, **kwargs)
191
+ self._filled_system_check()
192
+
193
+ @pytest.mark.parametrize('args, kwargs, exp_u_ind, exp_u_dep, exp_u', [
194
+ (u[:3], {}, u[:3], [], u[:3]),
195
+ (u[:3], {'independent': True}, u[:3], [], u[:3]),
196
+ (u[:3], {'independent': False}, [], u[:3], u[:3]),
197
+ (u[:3], {'independent': [True, False, True]}, [u[0], u[2]], [u[1]],
198
+ [u[0], u[2], u[1]]),
199
+ ])
200
+ def test_speeds(self, _empty_system_setup, args, kwargs, exp_u_ind,
201
+ exp_u_dep, exp_u):
202
+ # Test add_speeds
203
+ self.system.add_speeds(*args, **kwargs)
204
+ assert self.system.u_ind[:] == exp_u_ind
205
+ assert self.system.u_dep[:] == exp_u_dep
206
+ assert self.system.u[:] == exp_u
207
+ self._empty_system_check(exclude=('u_ind', 'u_dep', 'u'))
208
+ # Test setter for u_ind and u_dep
209
+ self.system.u_ind = exp_u_ind
210
+ self.system.u_dep = exp_u_dep
211
+ assert self.system.u_ind[:] == exp_u_ind
212
+ assert self.system.u_dep[:] == exp_u_dep
213
+ assert self.system.u[:] == exp_u
214
+ self._empty_system_check(exclude=('u_ind', 'u_dep', 'u'))
215
+
216
+ @pytest.mark.parametrize('args, kwargs, exp_u_aux', [
217
+ (ua[:3], {}, ua[:3]),
218
+ ])
219
+ def test_auxiliary_speeds(self, _empty_system_setup, args, kwargs,
220
+ exp_u_aux):
221
+ # Test add_speeds
222
+ self.system.add_auxiliary_speeds(*args, **kwargs)
223
+ assert self.system.u_aux[:] == exp_u_aux
224
+ self._empty_system_check(exclude=('u_aux',))
225
+ # Test setter for u_ind and u_dep
226
+ self.system.u_aux = exp_u_aux
227
+ assert self.system.u_aux[:] == exp_u_aux
228
+ self._empty_system_check(exclude=('u_aux',))
229
+
230
+ @pytest.mark.parametrize('args, kwargs', [
231
+ ((ua[2], q[0]), {}),
232
+ ((ua[2], u[1]), {}),
233
+ ((ua[0], ua[2]), {}),
234
+ ((symbols('a'), ua[2]), {}),
235
+ ])
236
+ def test_auxiliary_invalid(self, _filled_system_setup, args, kwargs):
237
+ with pytest.raises(ValueError):
238
+ self.system.add_auxiliary_speeds(*args, **kwargs)
239
+ self._filled_system_check()
240
+
241
+ @pytest.mark.parametrize('prop, add_func, args, kwargs', [
242
+ ('q_ind', 'add_coordinates', (q[0],), {}),
243
+ ('q_dep', 'add_coordinates', (q[3],), {'independent': False}),
244
+ ('u_ind', 'add_speeds', (u[0],), {}),
245
+ ('u_dep', 'add_speeds', (u[3],), {'independent': False}),
246
+ ('u_aux', 'add_auxiliary_speeds', (ua[2],), {}),
247
+ ('kdes', 'add_kdes', (qd[0] - u[0],), {}),
248
+ ('holonomic_constraints', 'add_holonomic_constraints',
249
+ (q[0] - q[1],), {}),
250
+ ('nonholonomic_constraints', 'add_nonholonomic_constraints',
251
+ (u[0] - u[1],), {}),
252
+ ('bodies', 'add_bodies', (RigidBody('body'),), {}),
253
+ ('loads', 'add_loads', (Force(Point('P'), ReferenceFrame('N').x),), {}),
254
+ ('actuators', 'add_actuators', (TorqueActuator(
255
+ symbols('T'), ReferenceFrame('N').x, ReferenceFrame('A')),), {}),
256
+ ])
257
+ def test_add_after_reset(self, _filled_system_setup, prop, add_func, args,
258
+ kwargs):
259
+ setattr(self.system, prop, ())
260
+ exclude = (prop, 'q', 'u')
261
+ if prop in ('holonomic_constraints', 'nonholonomic_constraints'):
262
+ exclude += ('velocity_constraints',)
263
+ self._filled_system_check(exclude=exclude)
264
+ assert list(getattr(self.system, prop)[:]) == []
265
+ getattr(self.system, add_func)(*args, **kwargs)
266
+ assert list(getattr(self.system, prop)[:]) == list(args)
267
+
268
+ @pytest.mark.parametrize('prop, add_func, value, error', [
269
+ ('q_ind', 'add_coordinates', symbols('a'), ValueError),
270
+ ('q_dep', 'add_coordinates', symbols('a'), ValueError),
271
+ ('u_ind', 'add_speeds', symbols('a'), ValueError),
272
+ ('u_dep', 'add_speeds', symbols('a'), ValueError),
273
+ ('u_aux', 'add_auxiliary_speeds', symbols('a'), ValueError),
274
+ ('kdes', 'add_kdes', 7, TypeError),
275
+ ('holonomic_constraints', 'add_holonomic_constraints', 7, TypeError),
276
+ ('nonholonomic_constraints', 'add_nonholonomic_constraints', 7,
277
+ TypeError),
278
+ ('bodies', 'add_bodies', symbols('a'), TypeError),
279
+ ('loads', 'add_loads', symbols('a'), TypeError),
280
+ ('actuators', 'add_actuators', symbols('a'), TypeError),
281
+ ])
282
+ def test_type_error(self, _filled_system_setup, prop, add_func, value,
283
+ error):
284
+ with pytest.raises(error):
285
+ getattr(self.system, add_func)(value)
286
+ with pytest.raises(error):
287
+ setattr(self.system, prop, value)
288
+ self._filled_system_check()
289
+
290
+ @pytest.mark.parametrize('args, kwargs, exp_kdes', [
291
+ ((), {}, [ui - qdi for ui, qdi in zip(u[:4], qd[:4])]),
292
+ ((u[4] - qd[4], u[5] - qd[5]), {},
293
+ [ui - qdi for ui, qdi in zip(u[:6], qd[:6])]),
294
+ ])
295
+ def test_kdes(self, _filled_system_setup, args, kwargs, exp_kdes):
296
+ # Test add_speeds
297
+ self.system.add_kdes(*args, **kwargs)
298
+ self._filled_system_check(exclude=('kdes',))
299
+ assert self.system.kdes[:] == exp_kdes
300
+ # Test setter for kdes
301
+ self.system.kdes = exp_kdes
302
+ self._filled_system_check(exclude=('kdes',))
303
+ assert self.system.kdes[:] == exp_kdes
304
+
305
+ @pytest.mark.parametrize('args, kwargs', [
306
+ ((u[0] - qd[0], u[4] - qd[4]), {}),
307
+ ((-(u[0] - qd[0]), u[4] - qd[4]), {}),
308
+ (([u[0] - u[0], u[4] - qd[4]]), {}),
309
+ ])
310
+ def test_kdes_invalid(self, _filled_system_setup, args, kwargs):
311
+ with pytest.raises(ValueError):
312
+ self.system.add_kdes(*args, **kwargs)
313
+ self._filled_system_check()
314
+
315
+ @pytest.mark.parametrize('args, kwargs, exp_con', [
316
+ ((), {}, [q[2] - q[0] + q[1]]),
317
+ ((q[4] - q[5], q[5] + q[3]), {},
318
+ [q[2] - q[0] + q[1], q[4] - q[5], q[5] + q[3]]),
319
+ ])
320
+ def test_holonomic_constraints(self, _filled_system_setup, args, kwargs,
321
+ exp_con):
322
+ exclude = ('holonomic_constraints', 'velocity_constraints')
323
+ exp_vel_con = [c.diff(t) for c in exp_con] + self.nhc
324
+ # Test add_holonomic_constraints
325
+ self.system.add_holonomic_constraints(*args, **kwargs)
326
+ self._filled_system_check(exclude=exclude)
327
+ assert self.system.holonomic_constraints[:] == exp_con
328
+ assert self.system.velocity_constraints[:] == exp_vel_con
329
+ # Test setter for holonomic_constraints
330
+ self.system.holonomic_constraints = exp_con
331
+ self._filled_system_check(exclude=exclude)
332
+ assert self.system.holonomic_constraints[:] == exp_con
333
+ assert self.system.velocity_constraints[:] == exp_vel_con
334
+
335
+ @pytest.mark.parametrize('args, kwargs', [
336
+ ((q[2] - q[0] + q[1], q[4] - q[3]), {}),
337
+ ((-(q[2] - q[0] + q[1]), q[4] - q[3]), {}),
338
+ ((q[0] - q[0], q[4] - q[3]), {}),
339
+ ])
340
+ def test_holonomic_constraints_invalid(self, _filled_system_setup, args,
341
+ kwargs):
342
+ with pytest.raises(ValueError):
343
+ self.system.add_holonomic_constraints(*args, **kwargs)
344
+ self._filled_system_check()
345
+
346
+ @pytest.mark.parametrize('args, kwargs, exp_con', [
347
+ ((), {}, [u[3] - qd[1] + u[2]]),
348
+ ((u[4] - u[5], u[5] + u[3]), {},
349
+ [u[3] - qd[1] + u[2], u[4] - u[5], u[5] + u[3]]),
350
+ ])
351
+ def test_nonholonomic_constraints(self, _filled_system_setup, args, kwargs,
352
+ exp_con):
353
+ exclude = ('nonholonomic_constraints', 'velocity_constraints')
354
+ exp_vel_con = self.vc[:len(self.hc)] + exp_con
355
+ # Test add_nonholonomic_constraints
356
+ self.system.add_nonholonomic_constraints(*args, **kwargs)
357
+ self._filled_system_check(exclude=exclude)
358
+ assert self.system.nonholonomic_constraints[:] == exp_con
359
+ assert self.system.velocity_constraints[:] == exp_vel_con
360
+ # Test setter for nonholonomic_constraints
361
+ self.system.nonholonomic_constraints = exp_con
362
+ self._filled_system_check(exclude=exclude)
363
+ assert self.system.nonholonomic_constraints[:] == exp_con
364
+ assert self.system.velocity_constraints[:] == exp_vel_con
365
+
366
+ @pytest.mark.parametrize('args, kwargs', [
367
+ ((u[3] - qd[1] + u[2], u[4] - u[3]), {}),
368
+ ((-(u[3] - qd[1] + u[2]), u[4] - u[3]), {}),
369
+ ((u[0] - u[0], u[4] - u[3]), {}),
370
+ (([u[0] - u[0], u[4] - u[3]]), {}),
371
+ ])
372
+ def test_nonholonomic_constraints_invalid(self, _filled_system_setup, args,
373
+ kwargs):
374
+ with pytest.raises(ValueError):
375
+ self.system.add_nonholonomic_constraints(*args, **kwargs)
376
+ self._filled_system_check()
377
+
378
+ @pytest.mark.parametrize('constraints, expected', [
379
+ ([], []),
380
+ (qd[2] - qd[0] + qd[1], [qd[2] - qd[0] + qd[1]]),
381
+ ([qd[2] + qd[1], u[2] - u[1]], [qd[2] + qd[1], u[2] - u[1]]),
382
+ ])
383
+ def test_velocity_constraints_overwrite(self, _filled_system_setup,
384
+ constraints, expected):
385
+ self.system.velocity_constraints = constraints
386
+ self._filled_system_check(exclude=('velocity_constraints',))
387
+ assert self.system.velocity_constraints[:] == expected
388
+
389
+ def test_velocity_constraints_back_to_auto(self, _filled_system_setup):
390
+ self.system.velocity_constraints = qd[3] - qd[2]
391
+ self._filled_system_check(exclude=('velocity_constraints',))
392
+ assert self.system.velocity_constraints[:] == [qd[3] - qd[2]]
393
+ self.system.velocity_constraints = None
394
+ self._filled_system_check()
395
+
396
+ def test_bodies(self, _filled_system_setup):
397
+ rb1, rb2 = RigidBody('rb1'), RigidBody('rb2')
398
+ p1, p2 = Particle('p1'), Particle('p2')
399
+ self.system.add_bodies(rb1, p1)
400
+ assert self.system.bodies == (*self.bodies, rb1, p1)
401
+ self.system.add_bodies(p2)
402
+ assert self.system.bodies == (*self.bodies, rb1, p1, p2)
403
+ self.system.bodies = []
404
+ assert self.system.bodies == ()
405
+ self.system.bodies = p2
406
+ assert self.system.bodies == (p2,)
407
+ symb = symbols('symb')
408
+ pytest.raises(TypeError, lambda: self.system.add_bodies(symb))
409
+ pytest.raises(ValueError, lambda: self.system.add_bodies(p2))
410
+ with pytest.raises(TypeError):
411
+ self.system.bodies = (rb1, rb2, p1, p2, symb)
412
+ assert self.system.bodies == (p2,)
413
+
414
+ def test_add_loads(self):
415
+ system = System()
416
+ N, A = ReferenceFrame('N'), ReferenceFrame('A')
417
+ rb1 = RigidBody('rb1', frame=N)
418
+ mc1 = Point('mc1')
419
+ p1 = Particle('p1', mc1)
420
+ system.add_loads(Torque(rb1, N.x), (mc1, A.x), Force(p1, A.x))
421
+ assert system.loads == ((N, N.x), (mc1, A.x), (mc1, A.x))
422
+ system.loads = [(A, A.x)]
423
+ assert system.loads == ((A, A.x),)
424
+ pytest.raises(ValueError, lambda: system.add_loads((N, N.x, N.y)))
425
+ with pytest.raises(TypeError):
426
+ system.loads = (N, N.x)
427
+ assert system.loads == ((A, A.x),)
428
+
429
+ def test_add_actuators(self):
430
+ system = System()
431
+ N, A = ReferenceFrame('N'), ReferenceFrame('A')
432
+ act1 = TorqueActuator(symbols('T1'), N.x, N)
433
+ act2 = TorqueActuator(symbols('T2'), N.y, N, A)
434
+ system.add_actuators(act1)
435
+ assert system.actuators == (act1,)
436
+ assert system.loads == ()
437
+ system.actuators = (act2,)
438
+ assert system.actuators == (act2,)
439
+
440
+ def test_add_joints(self):
441
+ q1, q2, q3, q4, u1, u2, u3 = dynamicsymbols('q1:5 u1:4')
442
+ rb1, rb2, rb3, rb4, rb5 = symbols('rb1:6', cls=RigidBody)
443
+ J1 = PinJoint('J1', rb1, rb2, q1, u1)
444
+ J2 = PrismaticJoint('J2', rb2, rb3, q2, u2)
445
+ J3 = PinJoint('J3', rb3, rb4, q3, u3)
446
+ J_lag = PinJoint('J_lag', rb4, rb5, q4, q4.diff(t))
447
+ system = System()
448
+ system.add_joints(J1)
449
+ assert system.joints == (J1,)
450
+ assert system.bodies == (rb1, rb2)
451
+ assert system.q_ind == ImmutableMatrix([q1])
452
+ assert system.u_ind == ImmutableMatrix([u1])
453
+ assert system.kdes == ImmutableMatrix([u1 - q1.diff(t)])
454
+ system.add_bodies(rb4)
455
+ system.add_coordinates(q3)
456
+ system.add_kdes(u3 - q3.diff(t))
457
+ system.add_joints(J3)
458
+ assert system.joints == (J1, J3)
459
+ assert system.bodies == (rb1, rb2, rb4, rb3)
460
+ assert system.q_ind == ImmutableMatrix([q1, q3])
461
+ assert system.u_ind == ImmutableMatrix([u1, u3])
462
+ assert system.kdes == ImmutableMatrix(
463
+ [u1 - q1.diff(t), u3 - q3.diff(t)])
464
+ system.add_kdes(-(u2 - q2.diff(t)))
465
+ system.add_joints(J2)
466
+ assert system.joints == (J1, J3, J2)
467
+ assert system.bodies == (rb1, rb2, rb4, rb3)
468
+ assert system.q_ind == ImmutableMatrix([q1, q3, q2])
469
+ assert system.u_ind == ImmutableMatrix([u1, u3, u2])
470
+ assert system.kdes == ImmutableMatrix([u1 - q1.diff(t), u3 - q3.diff(t),
471
+ -(u2 - q2.diff(t))])
472
+ system.add_joints(J_lag)
473
+ assert system.joints == (J1, J3, J2, J_lag)
474
+ assert system.bodies == (rb1, rb2, rb4, rb3, rb5)
475
+ assert system.q_ind == ImmutableMatrix([q1, q3, q2, q4])
476
+ assert system.u_ind == ImmutableMatrix([u1, u3, u2, q4.diff(t)])
477
+ assert system.kdes == ImmutableMatrix([u1 - q1.diff(t), u3 - q3.diff(t),
478
+ -(u2 - q2.diff(t))])
479
+ assert system.q_dep[:] == []
480
+ assert system.u_dep[:] == []
481
+ pytest.raises(ValueError, lambda: system.add_joints(J2))
482
+ pytest.raises(TypeError, lambda: system.add_joints(rb1))
483
+
484
+ def test_joints_setter(self, _filled_system_setup):
485
+ self.system.joints = self.joints[1:]
486
+ assert self.system.joints == self.joints[1:]
487
+ self._filled_system_check(exclude=('joints',))
488
+ self.system.q_ind = ()
489
+ self.system.u_ind = ()
490
+ self.system.joints = self.joints
491
+ self._filled_system_check()
492
+
493
+ @pytest.mark.parametrize('name, joint_index', [
494
+ ('J1', 0),
495
+ ('J2', 1),
496
+ ('not_existing', None),
497
+ ])
498
+ def test_get_joint(self, _filled_system_setup, name, joint_index):
499
+ joint = self.system.get_joint(name)
500
+ if joint_index is None:
501
+ assert joint is None
502
+ else:
503
+ assert joint == self.joints[joint_index]
504
+
505
+ @pytest.mark.parametrize('name, body_index', [
506
+ ('rb1', 0),
507
+ ('rb3', 2),
508
+ ('not_existing', None),
509
+ ])
510
+ def test_get_body(self, _filled_system_setup, name, body_index):
511
+ body = self.system.get_body(name)
512
+ if body_index is None:
513
+ assert body is None
514
+ else:
515
+ assert body == self.bodies[body_index]
516
+
517
+ @pytest.mark.parametrize('eom_method', [KanesMethod, LagrangesMethod])
518
+ def test_form_eoms_calls_subclass(self, _moving_point_mass, eom_method):
519
+ class MyMethod(eom_method):
520
+ pass
521
+
522
+ self.system.form_eoms(eom_method=MyMethod)
523
+ assert isinstance(self.system.eom_method, MyMethod)
524
+
525
+ @pytest.mark.parametrize('kwargs, expected', [
526
+ ({}, ImmutableMatrix([[-1, 0], [0, symbols('m')]])),
527
+ ({'explicit_kinematics': True}, ImmutableMatrix([[1, 0],
528
+ [0, symbols('m')]])),
529
+ ])
530
+ def test_system_kane_form_eoms_kwargs(self, _moving_point_mass, kwargs,
531
+ expected):
532
+ self.system.form_eoms(**kwargs)
533
+ assert self.system.mass_matrix_full == expected
534
+
535
+ @pytest.mark.parametrize('kwargs, mm, gm', [
536
+ ({}, ImmutableMatrix([[1, 0], [0, symbols('m')]]),
537
+ ImmutableMatrix([q[0].diff(t), 0])),
538
+ ])
539
+ def test_system_lagrange_form_eoms_kwargs(self, _moving_point_mass, kwargs,
540
+ mm, gm):
541
+ self.system.form_eoms(eom_method=LagrangesMethod, **kwargs)
542
+ assert self.system.mass_matrix_full == mm
543
+ assert self.system.forcing_full == gm
544
+
545
+ @pytest.mark.parametrize('eom_method, kwargs, error', [
546
+ (KanesMethod, {'non_existing_kwarg': 1}, TypeError),
547
+ (LagrangesMethod, {'non_existing_kwarg': 1}, TypeError),
548
+ (KanesMethod, {'bodies': []}, ValueError),
549
+ (KanesMethod, {'kd_eqs': []}, ValueError),
550
+ (LagrangesMethod, {'bodies': []}, ValueError),
551
+ (LagrangesMethod, {'Lagrangian': 1}, ValueError),
552
+ ])
553
+ def test_form_eoms_kwargs_errors(self, _empty_system_setup, eom_method,
554
+ kwargs, error):
555
+ self.system.q_ind = q[0]
556
+ p = Particle('p', mass=symbols('m'))
557
+ self.system.add_bodies(p)
558
+ p.masscenter.set_pos(self.system.fixed_point, q[0] * self.system.x)
559
+ with pytest.raises(error):
560
+ self.system.form_eoms(eom_method=eom_method, **kwargs)
561
+
562
+
563
+ class TestValidateSystem(TestSystemBase):
564
+ @pytest.mark.parametrize('valid_method, invalid_method, with_speeds', [
565
+ (KanesMethod, LagrangesMethod, True),
566
+ (LagrangesMethod, KanesMethod, False)
567
+ ])
568
+ def test_only_valid(self, valid_method, invalid_method, with_speeds):
569
+ self._create_filled_system(with_speeds=with_speeds)
570
+ self.system.validate_system(valid_method)
571
+ # Test Lagrange should fail due to the usage of generalized speeds
572
+ with pytest.raises(ValueError):
573
+ self.system.validate_system(invalid_method)
574
+
575
+ @pytest.mark.parametrize('method, with_speeds', [
576
+ (KanesMethod, True), (LagrangesMethod, False)])
577
+ def test_missing_joint_coordinate(self, method, with_speeds):
578
+ self._create_filled_system(with_speeds=with_speeds)
579
+ self.system.q_ind = self.q_ind[1:]
580
+ self.system.u_ind = self.u_ind[:-1]
581
+ self.system.kdes = self.kdes[:-1]
582
+ pytest.raises(ValueError, lambda: self.system.validate_system(method))
583
+
584
+ def test_missing_joint_speed(self, _filled_system_setup):
585
+ self.system.q_ind = self.q_ind[:-1]
586
+ self.system.u_ind = self.u_ind[1:]
587
+ self.system.kdes = self.kdes[:-1]
588
+ pytest.raises(ValueError, lambda: self.system.validate_system())
589
+
590
+ def test_missing_joint_kdes(self, _filled_system_setup):
591
+ self.system.kdes = self.kdes[1:]
592
+ pytest.raises(ValueError, lambda: self.system.validate_system())
593
+
594
+ def test_negative_joint_kdes(self, _filled_system_setup):
595
+ self.system.kdes = [-self.kdes[0]] + self.kdes[1:]
596
+ self.system.validate_system()
597
+
598
+ @pytest.mark.parametrize('method, with_speeds', [
599
+ (KanesMethod, True), (LagrangesMethod, False)])
600
+ def test_missing_holonomic_constraint(self, method, with_speeds):
601
+ self._create_filled_system(with_speeds=with_speeds)
602
+ self.system.holonomic_constraints = []
603
+ self.system.nonholonomic_constraints = self.nhc + [
604
+ self.u_ind[1] - self.u_dep[0] + self.u_ind[0]]
605
+ pytest.raises(ValueError, lambda: self.system.validate_system(method))
606
+ self.system.q_dep = []
607
+ self.system.q_ind = self.q_ind + self.q_dep
608
+ self.system.validate_system(method)
609
+
610
+ def test_missing_nonholonomic_constraint(self, _filled_system_setup):
611
+ self.system.nonholonomic_constraints = []
612
+ pytest.raises(ValueError, lambda: self.system.validate_system())
613
+ self.system.u_dep = self.u_dep[1]
614
+ self.system.u_ind = self.u_ind + [self.u_dep[0]]
615
+ self.system.validate_system()
616
+
617
+ def test_number_of_coordinates_speeds(self, _filled_system_setup):
618
+ # Test more speeds than coordinates
619
+ self.system.u_ind = self.u_ind + [u[5]]
620
+ self.system.kdes = self.kdes + [u[5] - qd[5]]
621
+ self.system.validate_system()
622
+ # Test more coordinates than speeds
623
+ self.system.q_ind = self.q_ind
624
+ self.system.u_ind = self.u_ind[:-1]
625
+ self.system.kdes = self.kdes[:-1]
626
+ pytest.raises(ValueError, lambda: self.system.validate_system())
627
+
628
+ def test_number_of_kdes(self, _filled_system_setup):
629
+ # Test wrong number of kdes
630
+ self.system.kdes = self.kdes[:-1]
631
+ pytest.raises(ValueError, lambda: self.system.validate_system())
632
+ self.system.kdes = self.kdes + [u[2] + u[1] - qd[2]]
633
+ pytest.raises(ValueError, lambda: self.system.validate_system())
634
+
635
+ def test_duplicates(self, _filled_system_setup):
636
+ # This is basically a redundant feature, which should never fail
637
+ self.system.validate_system(check_duplicates=True)
638
+
639
+ def test_speeds_in_lagrange(self, _filled_system_setup_no_speeds):
640
+ self.system.u_ind = u[:len(self.u_ind)]
641
+ with pytest.raises(ValueError):
642
+ self.system.validate_system(LagrangesMethod)
643
+ self.system.u_ind = []
644
+ self.system.validate_system(LagrangesMethod)
645
+ self.system.u_aux = ua
646
+ with pytest.raises(ValueError):
647
+ self.system.validate_system(LagrangesMethod)
648
+ self.system.u_aux = []
649
+ self.system.validate_system(LagrangesMethod)
650
+ self.system.add_joints(
651
+ PinJoint('Ju', RigidBody('rbu1'), RigidBody('rbu2')))
652
+ self.system.u_ind = []
653
+ with pytest.raises(ValueError):
654
+ self.system.validate_system(LagrangesMethod)
655
+
656
+
657
+ class TestSystemExamples:
658
+ def test_cart_pendulum_kanes(self):
659
+ # This example is the same as in the top documentation of System
660
+ # Added a spring to the cart
661
+ g, l, mc, mp, k = symbols('g l mc mp k')
662
+ F, qp, qc, up, uc = dynamicsymbols('F qp qc up uc')
663
+ rail = RigidBody('rail')
664
+ cart = RigidBody('cart', mass=mc)
665
+ bob = Particle('bob', mass=mp)
666
+ bob_frame = ReferenceFrame('bob_frame')
667
+ system = System.from_newtonian(rail)
668
+ assert system.bodies == (rail,)
669
+ assert system.frame == rail.frame
670
+ assert system.fixed_point == rail.masscenter
671
+ slider = PrismaticJoint('slider', rail, cart, qc, uc, joint_axis=rail.x)
672
+ pin = PinJoint('pin', cart, bob, qp, up, joint_axis=cart.z,
673
+ child_interframe=bob_frame, child_point=l * bob_frame.y)
674
+ system.add_joints(slider, pin)
675
+ assert system.joints == (slider, pin)
676
+ assert system.get_joint('slider') == slider
677
+ assert system.get_body('bob') == bob
678
+ system.apply_uniform_gravity(-g * system.y)
679
+ system.add_loads((cart.masscenter, F * rail.x))
680
+ system.add_actuators(TorqueActuator(k * qp, cart.z, bob_frame, cart))
681
+ system.validate_system()
682
+ system.form_eoms()
683
+ assert isinstance(system.eom_method, KanesMethod)
684
+ assert (simplify(system.mass_matrix - ImmutableMatrix(
685
+ [[mp + mc, mp * l * cos(qp)], [mp * l * cos(qp), mp * l ** 2]]))
686
+ == zeros(2, 2))
687
+ assert (simplify(system.forcing - ImmutableMatrix([
688
+ [mp * l * up ** 2 * sin(qp) + F],
689
+ [-mp * g * l * sin(qp) + k * qp]])) == zeros(2, 1))
690
+
691
+ system.add_holonomic_constraints(
692
+ sympify(bob.masscenter.pos_from(rail.masscenter).dot(system.x)))
693
+ assert system.eom_method is None
694
+ system.q_ind, system.q_dep = qp, qc
695
+ system.u_ind, system.u_dep = up, uc
696
+ system.validate_system()
697
+
698
+ # Computed solution based on manually solving the constraints
699
+ subs = {qc: -l * sin(qp),
700
+ uc: -l * cos(qp) * up,
701
+ uc.diff(t): l * (up ** 2 * sin(qp) - up.diff(t) * cos(qp))}
702
+ upd_expected = (
703
+ (-g * mp * sin(qp) + k * qp / l + l * mc * sin(2 * qp) * up ** 2 / 2
704
+ - l * mp * sin(2 * qp) * up ** 2 / 2 - F * cos(qp)) /
705
+ (l * (mc * cos(qp) ** 2 + mp * sin(qp) ** 2)))
706
+ upd_sol = tuple(solve(system.form_eoms().xreplace(subs),
707
+ up.diff(t)).values())[0]
708
+ assert simplify(upd_sol - upd_expected) == 0
709
+ assert isinstance(system.eom_method, KanesMethod)
710
+
711
+ # Test other output
712
+ Mk = -ImmutableMatrix([[0, 1], [1, 0]])
713
+ gk = -ImmutableMatrix([uc, up])
714
+ Md = ImmutableMatrix([[-l ** 2 * mp * cos(qp) ** 2 + l ** 2 * mp,
715
+ l * mp * cos(qp) - l * (mc + mp) * cos(qp)],
716
+ [l * cos(qp), 1]])
717
+ gd = ImmutableMatrix(
718
+ [[-g * l * mp * sin(qp) + k * qp - l ** 2 * mp * up ** 2 * sin(qp) *
719
+ cos(qp) - l * F * cos(qp)], [l * up ** 2 * sin(qp)]])
720
+ Mm = (Mk.row_join(zeros(2, 2))).col_join(zeros(2, 2).row_join(Md))
721
+ gm = gk.col_join(gd)
722
+ assert simplify(system.mass_matrix - Md) == zeros(2, 2)
723
+ assert simplify(system.forcing - gd) == zeros(2, 1)
724
+ assert simplify(system.mass_matrix_full - Mm) == zeros(4, 4)
725
+ assert simplify(system.forcing_full - gm) == zeros(4, 1)
726
+
727
+ def test_cart_pendulum_lagrange(self):
728
+ # Lagrange version of test_cart_pendulus_kanes
729
+ # Added a spring to the cart
730
+ g, l, mc, mp, k = symbols('g l mc mp k')
731
+ F, qp, qc = dynamicsymbols('F qp qc')
732
+ qpd, qcd = dynamicsymbols('qp qc', 1)
733
+ rail = RigidBody('rail')
734
+ cart = RigidBody('cart', mass=mc)
735
+ bob = Particle('bob', mass=mp)
736
+ bob_frame = ReferenceFrame('bob_frame')
737
+ system = System.from_newtonian(rail)
738
+ assert system.bodies == (rail,)
739
+ assert system.frame == rail.frame
740
+ assert system.fixed_point == rail.masscenter
741
+ slider = PrismaticJoint('slider', rail, cart, qc, qcd,
742
+ joint_axis=rail.x)
743
+ pin = PinJoint('pin', cart, bob, qp, qpd, joint_axis=cart.z,
744
+ child_interframe=bob_frame, child_point=l * bob_frame.y)
745
+ system.add_joints(slider, pin)
746
+ assert system.joints == (slider, pin)
747
+ assert system.get_joint('slider') == slider
748
+ assert system.get_body('bob') == bob
749
+ for body in system.bodies:
750
+ body.potential_energy = body.mass * g * body.masscenter.pos_from(
751
+ system.fixed_point).dot(system.y)
752
+ system.add_loads((cart.masscenter, F * rail.x))
753
+ system.add_actuators(TorqueActuator(k * qp, cart.z, bob_frame, cart))
754
+ system.validate_system(LagrangesMethod)
755
+ system.form_eoms(LagrangesMethod)
756
+ assert (simplify(system.mass_matrix - ImmutableMatrix(
757
+ [[mp + mc, mp * l * cos(qp)], [mp * l * cos(qp), mp * l ** 2]]))
758
+ == zeros(2, 2))
759
+ assert (simplify(system.forcing - ImmutableMatrix([
760
+ [mp * l * qpd ** 2 * sin(qp) + F], [-mp * g * l * sin(qp) + k * qp]]
761
+ )) == zeros(2, 1))
762
+
763
+ system.add_holonomic_constraints(
764
+ sympify(bob.masscenter.pos_from(rail.masscenter).dot(system.x)))
765
+ assert system.eom_method is None
766
+ system.q_ind, system.q_dep = qp, qc
767
+
768
+ # Computed solution based on manually solving the constraints
769
+ subs = {qc: -l * sin(qp),
770
+ qcd: -l * cos(qp) * qpd,
771
+ qcd.diff(t): l * (qpd ** 2 * sin(qp) - qpd.diff(t) * cos(qp))}
772
+ qpdd_expected = (
773
+ (-g * mp * sin(qp) + k * qp / l + l * mc * sin(2 * qp) * qpd ** 2 /
774
+ 2 - l * mp * sin(2 * qp) * qpd ** 2 / 2 - F * cos(qp)) /
775
+ (l * (mc * cos(qp) ** 2 + mp * sin(qp) ** 2)))
776
+ eoms = system.form_eoms(LagrangesMethod)
777
+ lam1 = system.eom_method.lam_vec[0]
778
+ lam1_sol = system.eom_method.solve_multipliers()[lam1]
779
+ qpdd_sol = solve(eoms[0].xreplace({lam1: lam1_sol}).xreplace(subs),
780
+ qpd.diff(t))[0]
781
+ assert simplify(qpdd_sol - qpdd_expected) == 0
782
+ assert isinstance(system.eom_method, LagrangesMethod)
783
+
784
+ # Test other output
785
+ Md = ImmutableMatrix([[l ** 2 * mp, l * mp * cos(qp), -l * cos(qp)],
786
+ [l * mp * cos(qp), mc + mp, -1]])
787
+ gd = ImmutableMatrix(
788
+ [[-g * l * mp * sin(qp) + k * qp],
789
+ [l * mp * sin(qp) * qpd ** 2 + F]])
790
+ Mm = (eye(2).row_join(zeros(2, 3))).col_join(zeros(3, 2).row_join(
791
+ Md.col_join(ImmutableMatrix([l * cos(qp), 1, 0]).T)))
792
+ gm = ImmutableMatrix([qpd, qcd] + gd[:] + [l * sin(qp) * qpd ** 2])
793
+ assert simplify(system.mass_matrix - Md) == zeros(2, 3)
794
+ assert simplify(system.forcing - gd) == zeros(2, 1)
795
+ assert simplify(system.mass_matrix_full - Mm) == zeros(5, 5)
796
+ assert simplify(system.forcing_full - gm) == zeros(5, 1)
797
+
798
+ def test_box_on_ground(self):
799
+ # Particle sliding on ground with friction. The applied force is assumed
800
+ # to be positive and to be higher than the friction force.
801
+ g, m, mu = symbols('g m mu')
802
+ q, u, ua = dynamicsymbols('q u ua')
803
+ N, F = dynamicsymbols('N F', positive=True)
804
+ P = Particle("P", mass=m)
805
+ system = System()
806
+ system.add_bodies(P)
807
+ P.masscenter.set_pos(system.fixed_point, q * system.x)
808
+ P.masscenter.set_vel(system.frame, u * system.x + ua * system.y)
809
+ system.q_ind, system.u_ind, system.u_aux = [q], [u], [ua]
810
+ system.kdes = [q.diff(t) - u]
811
+ system.apply_uniform_gravity(-g * system.y)
812
+ system.add_loads(
813
+ Force(P, N * system.y),
814
+ Force(P, F * system.x - mu * N * system.x))
815
+ system.validate_system()
816
+ system.form_eoms()
817
+
818
+ # Test other output
819
+ Mk = ImmutableMatrix([1])
820
+ gk = ImmutableMatrix([u])
821
+ Md = ImmutableMatrix([m])
822
+ gd = ImmutableMatrix([F - mu * N])
823
+ Mm = (Mk.row_join(zeros(1, 1))).col_join(zeros(1, 1).row_join(Md))
824
+ gm = gk.col_join(gd)
825
+ aux_eqs = ImmutableMatrix([N - m * g])
826
+ assert simplify(system.mass_matrix - Md) == zeros(1, 1)
827
+ assert simplify(system.forcing - gd) == zeros(1, 1)
828
+ assert simplify(system.mass_matrix_full - Mm) == zeros(2, 2)
829
+ assert simplify(system.forcing_full - gm) == zeros(2, 1)
830
+ assert simplify(system.eom_method.auxiliary_eqs - aux_eqs
831
+ ) == zeros(1, 1)
lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_wrapping_geometry.py ADDED
@@ -0,0 +1,363 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for the ``sympy.physics.mechanics.wrapping_geometry.py`` module."""
2
+
3
+ import pytest
4
+
5
+ from sympy import (
6
+ Integer,
7
+ Rational,
8
+ S,
9
+ Symbol,
10
+ acos,
11
+ cos,
12
+ pi,
13
+ sin,
14
+ sqrt,
15
+ )
16
+ from sympy.core.relational import Eq
17
+ from sympy.physics.mechanics import (
18
+ Point,
19
+ ReferenceFrame,
20
+ WrappingCylinder,
21
+ WrappingSphere,
22
+ dynamicsymbols,
23
+ )
24
+ from sympy.simplify.simplify import simplify
25
+
26
+
27
+ r = Symbol('r', positive=True)
28
+ x = Symbol('x')
29
+ q = dynamicsymbols('q')
30
+ N = ReferenceFrame('N')
31
+
32
+
33
+ class TestWrappingSphere:
34
+
35
+ @staticmethod
36
+ def test_valid_constructor():
37
+ r = Symbol('r', positive=True)
38
+ pO = Point('pO')
39
+ sphere = WrappingSphere(r, pO)
40
+ assert isinstance(sphere, WrappingSphere)
41
+ assert hasattr(sphere, 'radius')
42
+ assert sphere.radius == r
43
+ assert hasattr(sphere, 'point')
44
+ assert sphere.point == pO
45
+
46
+ @staticmethod
47
+ @pytest.mark.parametrize('position', [S.Zero, Integer(2)*r*N.x])
48
+ def test_geodesic_length_point_not_on_surface_invalid(position):
49
+ r = Symbol('r', positive=True)
50
+ pO = Point('pO')
51
+ sphere = WrappingSphere(r, pO)
52
+
53
+ p1 = Point('p1')
54
+ p1.set_pos(pO, position)
55
+ p2 = Point('p2')
56
+ p2.set_pos(pO, position)
57
+
58
+ error_msg = r'point .* does not lie on the surface of'
59
+ with pytest.raises(ValueError, match=error_msg):
60
+ sphere.geodesic_length(p1, p2)
61
+
62
+ @staticmethod
63
+ @pytest.mark.parametrize(
64
+ 'position_1, position_2, expected',
65
+ [
66
+ (r*N.x, r*N.x, S.Zero),
67
+ (r*N.x, r*N.y, S.Half*pi*r),
68
+ (r*N.x, r*-N.x, pi*r),
69
+ (r*-N.x, r*N.x, pi*r),
70
+ (r*N.x, r*sqrt(2)*S.Half*(N.x + N.y), Rational(1, 4)*pi*r),
71
+ (
72
+ r*sqrt(2)*S.Half*(N.x + N.y),
73
+ r*sqrt(3)*Rational(1, 3)*(N.x + N.y + N.z),
74
+ r*acos(sqrt(6)*Rational(1, 3)),
75
+ ),
76
+ ]
77
+ )
78
+ def test_geodesic_length(position_1, position_2, expected):
79
+ r = Symbol('r', positive=True)
80
+ pO = Point('pO')
81
+ sphere = WrappingSphere(r, pO)
82
+
83
+ p1 = Point('p1')
84
+ p1.set_pos(pO, position_1)
85
+ p2 = Point('p2')
86
+ p2.set_pos(pO, position_2)
87
+
88
+ assert simplify(Eq(sphere.geodesic_length(p1, p2), expected))
89
+
90
+ @staticmethod
91
+ @pytest.mark.parametrize(
92
+ 'position_1, position_2, vector_1, vector_2',
93
+ [
94
+ (r * N.x, r * N.y, N.y, N.x),
95
+ (r * N.x, -r * N.y, -N.y, N.x),
96
+ (
97
+ r * N.y,
98
+ sqrt(2)/2 * r * N.x - sqrt(2)/2 * r * N.y,
99
+ N.x,
100
+ sqrt(2)/2 * N.x + sqrt(2)/2 * N.y,
101
+ ),
102
+ (
103
+ r * N.x,
104
+ r / 2 * N.x + sqrt(3)/2 * r * N.y,
105
+ N.y,
106
+ sqrt(3)/2 * N.x - 1/2 * N.y,
107
+ ),
108
+ (
109
+ r * N.x,
110
+ sqrt(2)/2 * r * N.x + sqrt(2)/2 * r * N.y,
111
+ N.y,
112
+ sqrt(2)/2 * N.x - sqrt(2)/2 * N.y,
113
+ ),
114
+ ]
115
+ )
116
+ def test_geodesic_end_vectors(position_1, position_2, vector_1, vector_2):
117
+ r = Symbol('r', positive=True)
118
+ pO = Point('pO')
119
+ sphere = WrappingSphere(r, pO)
120
+
121
+ p1 = Point('p1')
122
+ p1.set_pos(pO, position_1)
123
+ p2 = Point('p2')
124
+ p2.set_pos(pO, position_2)
125
+
126
+ expected = (vector_1, vector_2)
127
+
128
+ assert sphere.geodesic_end_vectors(p1, p2) == expected
129
+
130
+ @staticmethod
131
+ @pytest.mark.parametrize(
132
+ 'position',
133
+ [r * N.x, r * cos(q) * N.x + r * sin(q) * N.y]
134
+ )
135
+ def test_geodesic_end_vectors_invalid_coincident(position):
136
+ r = Symbol('r', positive=True)
137
+ pO = Point('pO')
138
+ sphere = WrappingSphere(r, pO)
139
+
140
+ p1 = Point('p1')
141
+ p1.set_pos(pO, position)
142
+ p2 = Point('p2')
143
+ p2.set_pos(pO, position)
144
+
145
+ with pytest.raises(ValueError):
146
+ _ = sphere.geodesic_end_vectors(p1, p2)
147
+
148
+ @staticmethod
149
+ @pytest.mark.parametrize(
150
+ 'position_1, position_2',
151
+ [
152
+ (r * N.x, -r * N.x),
153
+ (-r * N.y, r * N.y),
154
+ (
155
+ r * cos(q) * N.x + r * sin(q) * N.y,
156
+ -r * cos(q) * N.x - r * sin(q) * N.y,
157
+ )
158
+ ]
159
+ )
160
+ def test_geodesic_end_vectors_invalid_diametrically_opposite(
161
+ position_1,
162
+ position_2,
163
+ ):
164
+ r = Symbol('r', positive=True)
165
+ pO = Point('pO')
166
+ sphere = WrappingSphere(r, pO)
167
+
168
+ p1 = Point('p1')
169
+ p1.set_pos(pO, position_1)
170
+ p2 = Point('p2')
171
+ p2.set_pos(pO, position_2)
172
+
173
+ with pytest.raises(ValueError):
174
+ _ = sphere.geodesic_end_vectors(p1, p2)
175
+
176
+
177
+ class TestWrappingCylinder:
178
+
179
+ @staticmethod
180
+ def test_valid_constructor():
181
+ N = ReferenceFrame('N')
182
+ r = Symbol('r', positive=True)
183
+ pO = Point('pO')
184
+ cylinder = WrappingCylinder(r, pO, N.x)
185
+ assert isinstance(cylinder, WrappingCylinder)
186
+ assert hasattr(cylinder, 'radius')
187
+ assert cylinder.radius == r
188
+ assert hasattr(cylinder, 'point')
189
+ assert cylinder.point == pO
190
+ assert hasattr(cylinder, 'axis')
191
+ assert cylinder.axis == N.x
192
+
193
+ @staticmethod
194
+ @pytest.mark.parametrize(
195
+ 'position, expected',
196
+ [
197
+ (S.Zero, False),
198
+ (r*N.y, True),
199
+ (r*N.z, True),
200
+ (r*(N.y + N.z).normalize(), True),
201
+ (Integer(2)*r*N.y, False),
202
+ (r*(N.x + N.y), True),
203
+ (r*(Integer(2)*N.x + N.y), True),
204
+ (Integer(2)*N.x + r*(Integer(2)*N.y + N.z).normalize(), True),
205
+ (r*(cos(q)*N.y + sin(q)*N.z), True)
206
+ ]
207
+ )
208
+ def test_point_is_on_surface(position, expected):
209
+ r = Symbol('r', positive=True)
210
+ pO = Point('pO')
211
+ cylinder = WrappingCylinder(r, pO, N.x)
212
+
213
+ p1 = Point('p1')
214
+ p1.set_pos(pO, position)
215
+
216
+ assert cylinder.point_on_surface(p1) is expected
217
+
218
+ @staticmethod
219
+ @pytest.mark.parametrize('position', [S.Zero, Integer(2)*r*N.y])
220
+ def test_geodesic_length_point_not_on_surface_invalid(position):
221
+ r = Symbol('r', positive=True)
222
+ pO = Point('pO')
223
+ cylinder = WrappingCylinder(r, pO, N.x)
224
+
225
+ p1 = Point('p1')
226
+ p1.set_pos(pO, position)
227
+ p2 = Point('p2')
228
+ p2.set_pos(pO, position)
229
+
230
+ error_msg = r'point .* does not lie on the surface of'
231
+ with pytest.raises(ValueError, match=error_msg):
232
+ cylinder.geodesic_length(p1, p2)
233
+
234
+ @staticmethod
235
+ @pytest.mark.parametrize(
236
+ 'axis, position_1, position_2, expected',
237
+ [
238
+ (N.x, r*N.y, r*N.y, S.Zero),
239
+ (N.x, r*N.y, N.x + r*N.y, S.One),
240
+ (N.x, r*N.y, -x*N.x + r*N.y, sqrt(x**2)),
241
+ (-N.x, r*N.y, x*N.x + r*N.y, sqrt(x**2)),
242
+ (N.x, r*N.y, r*N.z, S.Half*pi*sqrt(r**2)),
243
+ (-N.x, r*N.y, r*N.z, Integer(3)*S.Half*pi*sqrt(r**2)),
244
+ (N.x, r*N.z, r*N.y, Integer(3)*S.Half*pi*sqrt(r**2)),
245
+ (-N.x, r*N.z, r*N.y, S.Half*pi*sqrt(r**2)),
246
+ (N.x, r*N.y, r*(cos(q)*N.y + sin(q)*N.z), sqrt(r**2*q**2)),
247
+ (
248
+ -N.x, r*N.y,
249
+ r*(cos(q)*N.y + sin(q)*N.z),
250
+ sqrt(r**2*(Integer(2)*pi - q)**2),
251
+ ),
252
+ ]
253
+ )
254
+ def test_geodesic_length(axis, position_1, position_2, expected):
255
+ r = Symbol('r', positive=True)
256
+ pO = Point('pO')
257
+ cylinder = WrappingCylinder(r, pO, axis)
258
+
259
+ p1 = Point('p1')
260
+ p1.set_pos(pO, position_1)
261
+ p2 = Point('p2')
262
+ p2.set_pos(pO, position_2)
263
+
264
+ assert simplify(Eq(cylinder.geodesic_length(p1, p2), expected))
265
+
266
+ @staticmethod
267
+ @pytest.mark.parametrize(
268
+ 'axis, position_1, position_2, vector_1, vector_2',
269
+ [
270
+ (N.z, r * N.x, r * N.y, N.y, N.x),
271
+ (N.z, r * N.x, -r * N.x, N.y, N.y),
272
+ (N.z, -r * N.x, r * N.x, -N.y, -N.y),
273
+ (-N.z, r * N.x, -r * N.x, -N.y, -N.y),
274
+ (-N.z, -r * N.x, r * N.x, N.y, N.y),
275
+ (N.z, r * N.x, -r * N.y, N.y, -N.x),
276
+ (
277
+ N.z,
278
+ r * N.y,
279
+ sqrt(2)/2 * r * N.x - sqrt(2)/2 * r * N.y,
280
+ - N.x,
281
+ - sqrt(2)/2 * N.x - sqrt(2)/2 * N.y,
282
+ ),
283
+ (
284
+ N.z,
285
+ r * N.x,
286
+ r / 2 * N.x + sqrt(3)/2 * r * N.y,
287
+ N.y,
288
+ sqrt(3)/2 * N.x - 1/2 * N.y,
289
+ ),
290
+ (
291
+ N.z,
292
+ r * N.x,
293
+ sqrt(2)/2 * r * N.x + sqrt(2)/2 * r * N.y,
294
+ N.y,
295
+ sqrt(2)/2 * N.x - sqrt(2)/2 * N.y,
296
+ ),
297
+ (
298
+ N.z,
299
+ r * N.x,
300
+ r * N.x + N.z,
301
+ N.z,
302
+ -N.z,
303
+ ),
304
+ (
305
+ N.z,
306
+ r * N.x,
307
+ r * N.y + pi/2 * r * N.z,
308
+ sqrt(2)/2 * N.y + sqrt(2)/2 * N.z,
309
+ sqrt(2)/2 * N.x - sqrt(2)/2 * N.z,
310
+ ),
311
+ (
312
+ N.z,
313
+ r * N.x,
314
+ r * cos(q) * N.x + r * sin(q) * N.y,
315
+ N.y,
316
+ sin(q) * N.x - cos(q) * N.y,
317
+ ),
318
+ ]
319
+ )
320
+ def test_geodesic_end_vectors(
321
+ axis,
322
+ position_1,
323
+ position_2,
324
+ vector_1,
325
+ vector_2,
326
+ ):
327
+ r = Symbol('r', positive=True)
328
+ pO = Point('pO')
329
+ cylinder = WrappingCylinder(r, pO, axis)
330
+
331
+ p1 = Point('p1')
332
+ p1.set_pos(pO, position_1)
333
+ p2 = Point('p2')
334
+ p2.set_pos(pO, position_2)
335
+
336
+ expected = (vector_1, vector_2)
337
+ end_vectors = tuple(
338
+ end_vector.simplify()
339
+ for end_vector in cylinder.geodesic_end_vectors(p1, p2)
340
+ )
341
+
342
+ assert end_vectors == expected
343
+
344
+ @staticmethod
345
+ @pytest.mark.parametrize(
346
+ 'axis, position',
347
+ [
348
+ (N.z, r * N.x),
349
+ (N.z, r * cos(q) * N.x + r * sin(q) * N.y + N.z),
350
+ ]
351
+ )
352
+ def test_geodesic_end_vectors_invalid_coincident(axis, position):
353
+ r = Symbol('r', positive=True)
354
+ pO = Point('pO')
355
+ cylinder = WrappingCylinder(r, pO, axis)
356
+
357
+ p1 = Point('p1')
358
+ p1.set_pos(pO, position)
359
+ p2 = Point('p2')
360
+ p2.set_pos(pO, position)
361
+
362
+ with pytest.raises(ValueError):
363
+ _ = cylinder.geodesic_end_vectors(p1, p2)
lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (1.69 kB). View file
 
lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/anticommutator.cpython-312.pyc ADDED
Binary file (6.55 kB). View file
 
lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/boson.cpython-312.pyc ADDED
Binary file (11.7 kB). View file
 
lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/cartesian.cpython-312.pyc ADDED
Binary file (15.5 kB). View file
 
lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/cg.cpython-312.pyc ADDED
Binary file (36.3 kB). View file
 
lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/circuitplot.cpython-312.pyc ADDED
Binary file (16.2 kB). View file
 
lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/circuitutils.cpython-312.pyc ADDED
Binary file (14 kB). View file
 
lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/commutator.cpython-312.pyc ADDED
Binary file (9.93 kB). View file
 
lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/constants.cpython-312.pyc ADDED
Binary file (2.01 kB). View file
 
lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/dagger.cpython-312.pyc ADDED
Binary file (3.42 kB). View file
 
lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/density.cpython-312.pyc ADDED
Binary file (12.7 kB). View file
 
lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/fermion.cpython-312.pyc ADDED
Binary file (8.3 kB). View file
 
lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/gate.cpython-312.pyc ADDED
Binary file (50.3 kB). View file
 
lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/grover.cpython-312.pyc ADDED
Binary file (11.9 kB). View file
 
lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/hilbert.cpython-312.pyc ADDED
Binary file (27.2 kB). View file