test_kane.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553
  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. def test_invalid_coordinates():
  10. # Simple pendulum, but use symbols instead of dynamicsymbols
  11. l, m, g = symbols('l m g')
  12. q, u = symbols('q u') # Generalized coordinate
  13. kd = [q.diff(dynamicsymbols._t) - u]
  14. N, O = ReferenceFrame('N'), Point('O')
  15. O.set_vel(N, 0)
  16. P = Particle('P', Point('P'), m)
  17. P.point.set_pos(O, l * (sin(q) * N.x - cos(q) * N.y))
  18. F = (P.point, -m * g * N.y)
  19. raises(ValueError, lambda: KanesMethod(N, [q], [u], kd, bodies=[P],
  20. forcelist=[F]))
  21. def test_one_dof():
  22. # This is for a 1 dof spring-mass-damper case.
  23. # It is described in more detail in the KanesMethod docstring.
  24. q, u = dynamicsymbols('q u')
  25. qd, ud = dynamicsymbols('q u', 1)
  26. m, c, k = symbols('m c k')
  27. N = ReferenceFrame('N')
  28. P = Point('P')
  29. P.set_vel(N, u * N.x)
  30. kd = [qd - u]
  31. FL = [(P, (-k * q - c * u) * N.x)]
  32. pa = Particle('pa', P, m)
  33. BL = [pa]
  34. KM = KanesMethod(N, [q], [u], kd)
  35. KM.kanes_equations(BL, FL)
  36. assert KM.bodies == BL
  37. assert KM.loads == FL
  38. MM = KM.mass_matrix
  39. forcing = KM.forcing
  40. rhs = MM.inv() * forcing
  41. assert expand(rhs[0]) == expand(-(q * k + u * c) / m)
  42. assert simplify(KM.rhs() -
  43. KM.mass_matrix_full.LUsolve(KM.forcing_full)) == zeros(2, 1)
  44. assert (KM.linearize(A_and_B=True, )[0] == Matrix([[0, 1], [-k/m, -c/m]]))
  45. def test_two_dof():
  46. # This is for a 2 d.o.f., 2 particle spring-mass-damper.
  47. # The first coordinate is the displacement of the first particle, and the
  48. # second is the relative displacement between the first and second
  49. # particles. Speeds are defined as the time derivatives of the particles.
  50. q1, q2, u1, u2 = dynamicsymbols('q1 q2 u1 u2')
  51. q1d, q2d, u1d, u2d = dynamicsymbols('q1 q2 u1 u2', 1)
  52. m, c1, c2, k1, k2 = symbols('m c1 c2 k1 k2')
  53. N = ReferenceFrame('N')
  54. P1 = Point('P1')
  55. P2 = Point('P2')
  56. P1.set_vel(N, u1 * N.x)
  57. P2.set_vel(N, (u1 + u2) * N.x)
  58. # Note we multiply the kinematic equation by an arbitrary factor
  59. # to test the implicit vs explicit kinematics attribute
  60. kd = [q1d/2 - u1/2, 2*q2d - 2*u2]
  61. # Now we create the list of forces, then assign properties to each
  62. # particle, then create a list of all particles.
  63. FL = [(P1, (-k1 * q1 - c1 * u1 + k2 * q2 + c2 * u2) * N.x), (P2, (-k2 *
  64. q2 - c2 * u2) * N.x)]
  65. pa1 = Particle('pa1', P1, m)
  66. pa2 = Particle('pa2', P2, m)
  67. BL = [pa1, pa2]
  68. # Finally we create the KanesMethod object, specify the inertial frame,
  69. # pass relevant information, and form Fr & Fr*. Then we calculate the mass
  70. # matrix and forcing terms, and finally solve for the udots.
  71. KM = KanesMethod(N, q_ind=[q1, q2], u_ind=[u1, u2], kd_eqs=kd)
  72. KM.kanes_equations(BL, FL)
  73. MM = KM.mass_matrix
  74. forcing = KM.forcing
  75. rhs = MM.inv() * forcing
  76. assert expand(rhs[0]) == expand((-k1 * q1 - c1 * u1 + k2 * q2 + c2 * u2)/m)
  77. assert expand(rhs[1]) == expand((k1 * q1 + c1 * u1 - 2 * k2 * q2 - 2 *
  78. c2 * u2) / m)
  79. # Check that the explicit form is the default and kinematic mass matrix is identity
  80. assert KM.explicit_kinematics
  81. assert KM.mass_matrix_kin == eye(2)
  82. # Check that for the implicit form the mass matrix is not identity
  83. KM.explicit_kinematics = False
  84. assert KM.mass_matrix_kin == Matrix([[S(1)/2, 0], [0, 2]])
  85. # Check that whether using implicit or explicit kinematics the RHS
  86. # equations are consistent with the matrix form
  87. for explicit_kinematics in [False, True]:
  88. KM.explicit_kinematics = explicit_kinematics
  89. assert simplify(KM.rhs() -
  90. KM.mass_matrix_full.LUsolve(KM.forcing_full)) == zeros(4, 1)
  91. # Make sure an error is raised if nonlinear kinematic differential
  92. # equations are supplied.
  93. kd = [q1d - u1**2, sin(q2d) - cos(u2)]
  94. raises(ValueError, lambda: KanesMethod(N, q_ind=[q1, q2],
  95. u_ind=[u1, u2], kd_eqs=kd))
  96. def test_pend():
  97. q, u = dynamicsymbols('q u')
  98. qd, ud = dynamicsymbols('q u', 1)
  99. m, l, g = symbols('m l g')
  100. N = ReferenceFrame('N')
  101. P = Point('P')
  102. P.set_vel(N, -l * u * sin(q) * N.x + l * u * cos(q) * N.y)
  103. kd = [qd - u]
  104. FL = [(P, m * g * N.x)]
  105. pa = Particle('pa', P, m)
  106. BL = [pa]
  107. KM = KanesMethod(N, [q], [u], kd)
  108. KM.kanes_equations(BL, FL)
  109. MM = KM.mass_matrix
  110. forcing = KM.forcing
  111. rhs = MM.inv() * forcing
  112. rhs.simplify()
  113. assert expand(rhs[0]) == expand(-g / l * sin(q))
  114. assert simplify(KM.rhs() -
  115. KM.mass_matrix_full.LUsolve(KM.forcing_full)) == zeros(2, 1)
  116. def test_rolling_disc():
  117. # Rolling Disc Example
  118. # Here the rolling disc is formed from the contact point up, removing the
  119. # need to introduce generalized speeds. Only 3 configuration and three
  120. # speed variables are need to describe this system, along with the disc's
  121. # mass and radius, and the local gravity (note that mass will drop out).
  122. q1, q2, q3, u1, u2, u3 = dynamicsymbols('q1 q2 q3 u1 u2 u3')
  123. q1d, q2d, q3d, u1d, u2d, u3d = dynamicsymbols('q1 q2 q3 u1 u2 u3', 1)
  124. r, m, g = symbols('r m g')
  125. # The kinematics are formed by a series of simple rotations. Each simple
  126. # rotation creates a new frame, and the next rotation is defined by the new
  127. # frame's basis vectors. This example uses a 3-1-2 series of rotations, or
  128. # Z, X, Y series of rotations. Angular velocity for this is defined using
  129. # the second frame's basis (the lean frame).
  130. N = ReferenceFrame('N')
  131. Y = N.orientnew('Y', 'Axis', [q1, N.z])
  132. L = Y.orientnew('L', 'Axis', [q2, Y.x])
  133. R = L.orientnew('R', 'Axis', [q3, L.y])
  134. w_R_N_qd = R.ang_vel_in(N)
  135. R.set_ang_vel(N, u1 * L.x + u2 * L.y + u3 * L.z)
  136. # This is the translational kinematics. We create a point with no velocity
  137. # in N; this is the contact point between the disc and ground. Next we form
  138. # the position vector from the contact point to the disc's center of mass.
  139. # Finally we form the velocity and acceleration of the disc.
  140. C = Point('C')
  141. C.set_vel(N, 0)
  142. Dmc = C.locatenew('Dmc', r * L.z)
  143. Dmc.v2pt_theory(C, N, R)
  144. # This is a simple way to form the inertia dyadic.
  145. I = inertia(L, m / 4 * r**2, m / 2 * r**2, m / 4 * r**2)
  146. # Kinematic differential equations; how the generalized coordinate time
  147. # derivatives relate to generalized speeds.
  148. kd = [dot(R.ang_vel_in(N) - w_R_N_qd, uv) for uv in L]
  149. # Creation of the force list; it is the gravitational force at the mass
  150. # center of the disc. Then we create the disc by assigning a Point to the
  151. # center of mass attribute, a ReferenceFrame to the frame attribute, and mass
  152. # and inertia. Then we form the body list.
  153. ForceList = [(Dmc, - m * g * Y.z)]
  154. BodyD = RigidBody('BodyD', Dmc, R, m, (I, Dmc))
  155. BodyList = [BodyD]
  156. # Finally we form the equations of motion, using the same steps we did
  157. # before. Specify inertial frame, supply generalized speeds, supply
  158. # kinematic differential equation dictionary, compute Fr from the force
  159. # list and Fr* from the body list, compute the mass matrix and forcing
  160. # terms, then solve for the u dots (time derivatives of the generalized
  161. # speeds).
  162. KM = KanesMethod(N, q_ind=[q1, q2, q3], u_ind=[u1, u2, u3], kd_eqs=kd)
  163. KM.kanes_equations(BodyList, ForceList)
  164. MM = KM.mass_matrix
  165. forcing = KM.forcing
  166. rhs = MM.inv() * forcing
  167. kdd = KM.kindiffdict()
  168. rhs = rhs.subs(kdd)
  169. rhs.simplify()
  170. assert rhs.expand() == Matrix([(6*u2*u3*r - u3**2*r*tan(q2) +
  171. 4*g*sin(q2))/(5*r), -2*u1*u3/3, u1*(-2*u2 + u3*tan(q2))]).expand()
  172. assert simplify(KM.rhs() -
  173. KM.mass_matrix_full.LUsolve(KM.forcing_full)) == zeros(6, 1)
  174. # This code tests our output vs. benchmark values. When r=g=m=1, the
  175. # critical speed (where all eigenvalues of the linearized equations are 0)
  176. # is 1 / sqrt(3) for the upright case.
  177. A = KM.linearize(A_and_B=True)[0]
  178. A_upright = A.subs({r: 1, g: 1, m: 1}).subs({q1: 0, q2: 0, q3: 0, u1: 0, u3: 0})
  179. import sympy
  180. assert sympy.sympify(A_upright.subs({u2: 1 / sqrt(3)})).eigenvals() == {S.Zero: 6}
  181. def test_aux():
  182. # Same as above, except we have 2 auxiliary speeds for the ground contact
  183. # point, which is known to be zero. In one case, we go through then
  184. # substitute the aux. speeds in at the end (they are zero, as well as their
  185. # derivative), in the other case, we use the built-in auxiliary speed part
  186. # of KanesMethod. The equations from each should be the same.
  187. q1, q2, q3, u1, u2, u3 = dynamicsymbols('q1 q2 q3 u1 u2 u3')
  188. q1d, q2d, q3d, u1d, u2d, u3d = dynamicsymbols('q1 q2 q3 u1 u2 u3', 1)
  189. u4, u5, f1, f2 = dynamicsymbols('u4, u5, f1, f2')
  190. u4d, u5d = dynamicsymbols('u4, u5', 1)
  191. r, m, g = symbols('r m g')
  192. N = ReferenceFrame('N')
  193. Y = N.orientnew('Y', 'Axis', [q1, N.z])
  194. L = Y.orientnew('L', 'Axis', [q2, Y.x])
  195. R = L.orientnew('R', 'Axis', [q3, L.y])
  196. w_R_N_qd = R.ang_vel_in(N)
  197. R.set_ang_vel(N, u1 * L.x + u2 * L.y + u3 * L.z)
  198. C = Point('C')
  199. C.set_vel(N, u4 * L.x + u5 * (Y.z ^ L.x))
  200. Dmc = C.locatenew('Dmc', r * L.z)
  201. Dmc.v2pt_theory(C, N, R)
  202. Dmc.a2pt_theory(C, N, R)
  203. I = inertia(L, m / 4 * r**2, m / 2 * r**2, m / 4 * r**2)
  204. kd = [dot(R.ang_vel_in(N) - w_R_N_qd, uv) for uv in L]
  205. ForceList = [(Dmc, - m * g * Y.z), (C, f1 * L.x + f2 * (Y.z ^ L.x))]
  206. BodyD = RigidBody('BodyD', Dmc, R, m, (I, Dmc))
  207. BodyList = [BodyD]
  208. KM = KanesMethod(N, q_ind=[q1, q2, q3], u_ind=[u1, u2, u3, u4, u5],
  209. kd_eqs=kd)
  210. (fr, frstar) = KM.kanes_equations(BodyList, ForceList)
  211. fr = fr.subs({u4d: 0, u5d: 0}).subs({u4: 0, u5: 0})
  212. frstar = frstar.subs({u4d: 0, u5d: 0}).subs({u4: 0, u5: 0})
  213. KM2 = KanesMethod(N, q_ind=[q1, q2, q3], u_ind=[u1, u2, u3], kd_eqs=kd,
  214. u_auxiliary=[u4, u5])
  215. (fr2, frstar2) = KM2.kanes_equations(BodyList, ForceList)
  216. fr2 = fr2.subs({u4d: 0, u5d: 0}).subs({u4: 0, u5: 0})
  217. frstar2 = frstar2.subs({u4d: 0, u5d: 0}).subs({u4: 0, u5: 0})
  218. frstar.simplify()
  219. frstar2.simplify()
  220. assert (fr - fr2).expand() == Matrix([0, 0, 0, 0, 0])
  221. assert (frstar - frstar2).expand() == Matrix([0, 0, 0, 0, 0])
  222. def test_parallel_axis():
  223. # This is for a 2 dof inverted pendulum on a cart.
  224. # This tests the parallel axis code in KanesMethod. The inertia of the
  225. # pendulum is defined about the hinge, not about the center of mass.
  226. # Defining the constants and knowns of the system
  227. gravity = symbols('g')
  228. k, ls = symbols('k ls')
  229. a, mA, mC = symbols('a mA mC')
  230. F = dynamicsymbols('F')
  231. Ix, Iy, Iz = symbols('Ix Iy Iz')
  232. # Declaring the Generalized coordinates and speeds
  233. q1, q2 = dynamicsymbols('q1 q2')
  234. q1d, q2d = dynamicsymbols('q1 q2', 1)
  235. u1, u2 = dynamicsymbols('u1 u2')
  236. u1d, u2d = dynamicsymbols('u1 u2', 1)
  237. # Creating reference frames
  238. N = ReferenceFrame('N')
  239. A = ReferenceFrame('A')
  240. A.orient(N, 'Axis', [-q2, N.z])
  241. A.set_ang_vel(N, -u2 * N.z)
  242. # Origin of Newtonian reference frame
  243. O = Point('O')
  244. # Creating and Locating the positions of the cart, C, and the
  245. # center of mass of the pendulum, A
  246. C = O.locatenew('C', q1 * N.x)
  247. Ao = C.locatenew('Ao', a * A.y)
  248. # Defining velocities of the points
  249. O.set_vel(N, 0)
  250. C.set_vel(N, u1 * N.x)
  251. Ao.v2pt_theory(C, N, A)
  252. Cart = Particle('Cart', C, mC)
  253. Pendulum = RigidBody('Pendulum', Ao, A, mA, (inertia(A, Ix, Iy, Iz), C))
  254. # kinematical differential equations
  255. kindiffs = [q1d - u1, q2d - u2]
  256. bodyList = [Cart, Pendulum]
  257. forceList = [(Ao, -N.y * gravity * mA),
  258. (C, -N.y * gravity * mC),
  259. (C, -N.x * k * (q1 - ls)),
  260. (C, N.x * F)]
  261. km = KanesMethod(N, [q1, q2], [u1, u2], kindiffs)
  262. (fr, frstar) = km.kanes_equations(bodyList, forceList)
  263. mm = km.mass_matrix_full
  264. assert mm[3, 3] == Iz
  265. def test_input_format():
  266. # 1 dof problem from test_one_dof
  267. q, u = dynamicsymbols('q u')
  268. qd, ud = dynamicsymbols('q u', 1)
  269. m, c, k = symbols('m c k')
  270. N = ReferenceFrame('N')
  271. P = Point('P')
  272. P.set_vel(N, u * N.x)
  273. kd = [qd - u]
  274. FL = [(P, (-k * q - c * u) * N.x)]
  275. pa = Particle('pa', P, m)
  276. BL = [pa]
  277. KM = KanesMethod(N, [q], [u], kd)
  278. # test for input format kane.kanes_equations((body1, body2, particle1))
  279. assert KM.kanes_equations(BL)[0] == Matrix([0])
  280. # test for input format kane.kanes_equations(bodies=(body1, body 2), loads=(load1,load2))
  281. assert KM.kanes_equations(bodies=BL, loads=None)[0] == Matrix([0])
  282. # test for input format kane.kanes_equations(bodies=(body1, body 2), loads=None)
  283. assert KM.kanes_equations(BL, loads=None)[0] == Matrix([0])
  284. # test for input format kane.kanes_equations(bodies=(body1, body 2))
  285. assert KM.kanes_equations(BL)[0] == Matrix([0])
  286. # test for input format kane.kanes_equations(bodies=(body1, body2), loads=[])
  287. assert KM.kanes_equations(BL, [])[0] == Matrix([0])
  288. # test for error raised when a wrong force list (in this case a string) is provided
  289. raises(ValueError, lambda: KM._form_fr('bad input'))
  290. # 1 dof problem from test_one_dof with FL & BL in instance
  291. KM = KanesMethod(N, [q], [u], kd, bodies=BL, forcelist=FL)
  292. assert KM.kanes_equations()[0] == Matrix([-c*u - k*q])
  293. # 2 dof problem from test_two_dof
  294. q1, q2, u1, u2 = dynamicsymbols('q1 q2 u1 u2')
  295. q1d, q2d, u1d, u2d = dynamicsymbols('q1 q2 u1 u2', 1)
  296. m, c1, c2, k1, k2 = symbols('m c1 c2 k1 k2')
  297. N = ReferenceFrame('N')
  298. P1 = Point('P1')
  299. P2 = Point('P2')
  300. P1.set_vel(N, u1 * N.x)
  301. P2.set_vel(N, (u1 + u2) * N.x)
  302. kd = [q1d - u1, q2d - u2]
  303. FL = ((P1, (-k1 * q1 - c1 * u1 + k2 * q2 + c2 * u2) * N.x), (P2, (-k2 *
  304. q2 - c2 * u2) * N.x))
  305. pa1 = Particle('pa1', P1, m)
  306. pa2 = Particle('pa2', P2, m)
  307. BL = (pa1, pa2)
  308. KM = KanesMethod(N, q_ind=[q1, q2], u_ind=[u1, u2], kd_eqs=kd)
  309. # test for input format
  310. # kane.kanes_equations((body1, body2), (load1, load2))
  311. KM.kanes_equations(BL, FL)
  312. MM = KM.mass_matrix
  313. forcing = KM.forcing
  314. rhs = MM.inv() * forcing
  315. assert expand(rhs[0]) == expand((-k1 * q1 - c1 * u1 + k2 * q2 + c2 * u2)/m)
  316. assert expand(rhs[1]) == expand((k1 * q1 + c1 * u1 - 2 * k2 * q2 - 2 *
  317. c2 * u2) / m)
  318. def test_implicit_kinematics():
  319. # Test that implicit kinematics can handle complicated
  320. # equations that explicit form struggles with
  321. # See https://github.com/sympy/sympy/issues/22626
  322. # Inertial frame
  323. NED = ReferenceFrame('NED')
  324. NED_o = Point('NED_o')
  325. NED_o.set_vel(NED, 0)
  326. # body frame
  327. q_att = dynamicsymbols('lambda_0:4', real=True)
  328. B = NED.orientnew('B', 'Quaternion', q_att)
  329. # Generalized coordinates
  330. q_pos = dynamicsymbols('B_x:z')
  331. B_cm = NED_o.locatenew('B_cm', q_pos[0]*B.x + q_pos[1]*B.y + q_pos[2]*B.z)
  332. q_ind = q_att[1:] + q_pos
  333. q_dep = [q_att[0]]
  334. kinematic_eqs = []
  335. # Generalized velocities
  336. B_ang_vel = B.ang_vel_in(NED)
  337. P, Q, R = dynamicsymbols('P Q R')
  338. B.set_ang_vel(NED, P*B.x + Q*B.y + R*B.z)
  339. B_ang_vel_kd = (B.ang_vel_in(NED) - B_ang_vel).simplify()
  340. # Equating the two gives us the kinematic equation
  341. kinematic_eqs += [
  342. B_ang_vel_kd & B.x,
  343. B_ang_vel_kd & B.y,
  344. B_ang_vel_kd & B.z
  345. ]
  346. B_cm_vel = B_cm.vel(NED)
  347. U, V, W = dynamicsymbols('U V W')
  348. B_cm.set_vel(NED, U*B.x + V*B.y + W*B.z)
  349. # Compute the velocity of the point using the two methods
  350. B_ref_vel_kd = (B_cm.vel(NED) - B_cm_vel)
  351. # taking dot product with unit vectors to get kinematic equations
  352. # relating body coordinates and velocities
  353. # Note, there is a choice to dot with NED.xyz here. That makes
  354. # the implicit form have some bigger terms but is still fine, the
  355. # explicit form still struggles though
  356. kinematic_eqs += [
  357. B_ref_vel_kd & B.x,
  358. B_ref_vel_kd & B.y,
  359. B_ref_vel_kd & B.z,
  360. ]
  361. u_ind = [U, V, W, P, Q, R]
  362. # constraints
  363. q_att_vec = Matrix(q_att)
  364. config_cons = [(q_att_vec.T*q_att_vec)[0] - 1] #unit norm
  365. kinematic_eqs = kinematic_eqs + [(q_att_vec.T * q_att_vec.diff())[0]]
  366. try:
  367. KM = KanesMethod(NED, q_ind, u_ind,
  368. q_dependent= q_dep,
  369. kd_eqs = kinematic_eqs,
  370. configuration_constraints = config_cons,
  371. velocity_constraints= [],
  372. u_dependent= [], #no dependent speeds
  373. u_auxiliary = [], # No auxiliary speeds
  374. explicit_kinematics = False # implicit kinematics
  375. )
  376. except Exception as e:
  377. raise e
  378. # mass and inertia dyadic relative to CM
  379. M_B = symbols('M_B')
  380. J_B = inertia(B, *[S(f'J_B_{ax}')*(1 if ax[0] == ax[1] else -1)
  381. for ax in ['xx', 'yy', 'zz', 'xy', 'yz', 'xz']])
  382. J_B = J_B.subs({S('J_B_xy'): 0, S('J_B_yz'): 0})
  383. RB = RigidBody('RB', B_cm, B, M_B, (J_B, B_cm))
  384. rigid_bodies = [RB]
  385. # Forces
  386. force_list = [
  387. #gravity pointing down
  388. (RB.masscenter, RB.mass*S('g')*NED.z),
  389. #generic forces and torques in body frame(inputs)
  390. (RB.frame, dynamicsymbols('T_z')*B.z),
  391. (RB.masscenter, dynamicsymbols('F_z')*B.z)
  392. ]
  393. KM.kanes_equations(rigid_bodies, force_list)
  394. # Expecting implicit form to be less than 5% of the flops
  395. n_ops_implicit = sum(
  396. [x.count_ops() for x in KM.forcing_full] +
  397. [x.count_ops() for x in KM.mass_matrix_full]
  398. )
  399. # Save implicit kinematic matrices to use later
  400. mass_matrix_kin_implicit = KM.mass_matrix_kin
  401. forcing_kin_implicit = KM.forcing_kin
  402. KM.explicit_kinematics = True
  403. n_ops_explicit = sum(
  404. [x.count_ops() for x in KM.forcing_full] +
  405. [x.count_ops() for x in KM.mass_matrix_full]
  406. )
  407. forcing_kin_explicit = KM.forcing_kin
  408. assert n_ops_implicit / n_ops_explicit < .05
  409. # Ideally we would check that implicit and explicit equations give the same result as done in test_one_dof
  410. # But the whole raison-d'etre of the implicit equations is to deal with problems such
  411. # as this one where the explicit form is too complicated to handle, especially the angular part
  412. # (i.e. tests would be too slow)
  413. # Instead, we check that the kinematic equations are correct using more fundamental tests:
  414. #
  415. # (1) that we recover the kinematic equations we have provided
  416. assert (mass_matrix_kin_implicit * KM.q.diff() - forcing_kin_implicit) == Matrix(kinematic_eqs)
  417. # (2) that rate of quaternions matches what 'textbook' solutions give
  418. # Note that we just use the explicit kinematics for the linear velocities
  419. # as they are not as complicated as the angular ones
  420. qdot_candidate = forcing_kin_explicit
  421. quat_dot_textbook = Matrix([
  422. [0, -P, -Q, -R],
  423. [P, 0, R, -Q],
  424. [Q, -R, 0, P],
  425. [R, Q, -P, 0],
  426. ]) * q_att_vec / 2
  427. # Again, if we don't use this "textbook" solution
  428. # sympy will struggle to deal with the terms related to quaternion rates
  429. # due to the number of operations involved
  430. qdot_candidate[-1] = quat_dot_textbook[0] # lambda_0, note the [-1] as sympy's Kane puts the dependent coordinate last
  431. qdot_candidate[0] = quat_dot_textbook[1] # lambda_1
  432. qdot_candidate[1] = quat_dot_textbook[2] # lambda_2
  433. qdot_candidate[2] = quat_dot_textbook[3] # lambda_3
  434. # sub the config constraint in the candidate solution and compare to the implicit rhs
  435. lambda_0_sol = solve(config_cons[0], q_att_vec[0])[1]
  436. lhs_candidate = simplify(mass_matrix_kin_implicit * qdot_candidate).subs({q_att_vec[0]: lambda_0_sol})
  437. assert lhs_candidate == forcing_kin_implicit
  438. def test_issue_24887():
  439. # Spherical pendulum
  440. g, l, m, c = symbols('g l m c')
  441. q1, q2, q3, u1, u2, u3 = dynamicsymbols('q1:4 u1:4')
  442. N = ReferenceFrame('N')
  443. A = ReferenceFrame('A')
  444. A.orient_body_fixed(N, (q1, q2, q3), 'zxy')
  445. N_w_A = A.ang_vel_in(N)
  446. # A.set_ang_vel(N, u1 * A.x + u2 * A.y + u3 * A.z)
  447. kdes = [N_w_A.dot(A.x) - u1, N_w_A.dot(A.y) - u2, N_w_A.dot(A.z) - u3]
  448. O = Point('O')
  449. O.set_vel(N, 0)
  450. Po = O.locatenew('Po', -l * A.y)
  451. Po.set_vel(A, 0)
  452. P = Particle('P', Po, m)
  453. kane = KanesMethod(N, [q1, q2, q3], [u1, u2, u3], kdes, bodies=[P],
  454. forcelist=[(Po, -m * g * N.y)])
  455. kane.kanes_equations()
  456. expected_md = m * l ** 2 * Matrix([[1, 0, 0], [0, 0, 0], [0, 0, 1]])
  457. expected_fd = Matrix([
  458. [l*m*(g*(sin(q1)*sin(q3) - sin(q2)*cos(q1)*cos(q3)) - l*u2*u3)],
  459. [0], [l*m*(-g*(sin(q1)*cos(q3) + sin(q2)*sin(q3)*cos(q1)) + l*u1*u2)]])
  460. assert find_dynamicsymbols(kane.forcing).issubset({q1, q2, q3, u1, u2, u3})
  461. assert simplify(kane.mass_matrix - expected_md) == zeros(3, 3)
  462. assert simplify(kane.forcing - expected_fd) == zeros(3, 1)