test_linearize.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  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. @slow
  10. def test_linearize_rolling_disc_kane():
  11. # Symbols for time and constant parameters
  12. t, r, m, g, v = symbols('t r m g v')
  13. # Configuration variables and their time derivatives
  14. q1, q2, q3, q4, q5, q6 = q = dynamicsymbols('q1:7')
  15. q1d, q2d, q3d, q4d, q5d, q6d = qd = [qi.diff(t) for qi in q]
  16. # Generalized speeds and their time derivatives
  17. u = dynamicsymbols('u:6')
  18. u1, u2, u3, u4, u5, u6 = u = dynamicsymbols('u1:7')
  19. u1d, u2d, u3d, u4d, u5d, u6d = [ui.diff(t) for ui in u]
  20. # Reference frames
  21. N = ReferenceFrame('N') # Inertial frame
  22. NO = Point('NO') # Inertial origin
  23. A = N.orientnew('A', 'Axis', [q1, N.z]) # Yaw intermediate frame
  24. B = A.orientnew('B', 'Axis', [q2, A.x]) # Lean intermediate frame
  25. C = B.orientnew('C', 'Axis', [q3, B.y]) # Disc fixed frame
  26. CO = NO.locatenew('CO', q4*N.x + q5*N.y + q6*N.z) # Disc center
  27. # Disc angular velocity in N expressed using time derivatives of coordinates
  28. w_c_n_qd = C.ang_vel_in(N)
  29. w_b_n_qd = B.ang_vel_in(N)
  30. # Inertial angular velocity and angular acceleration of disc fixed frame
  31. C.set_ang_vel(N, u1*B.x + u2*B.y + u3*B.z)
  32. # Disc center velocity in N expressed using time derivatives of coordinates
  33. v_co_n_qd = CO.pos_from(NO).dt(N)
  34. # Disc center velocity in N expressed using generalized speeds
  35. CO.set_vel(N, u4*C.x + u5*C.y + u6*C.z)
  36. # Disc Ground Contact Point
  37. P = CO.locatenew('P', r*B.z)
  38. P.v2pt_theory(CO, N, C)
  39. # Configuration constraint
  40. f_c = Matrix([q6 - dot(CO.pos_from(P), N.z)])
  41. # Velocity level constraints
  42. f_v = Matrix([dot(P.vel(N), uv) for uv in C])
  43. # Kinematic differential equations
  44. kindiffs = Matrix([dot(w_c_n_qd - C.ang_vel_in(N), uv) for uv in B] +
  45. [dot(v_co_n_qd - CO.vel(N), uv) for uv in N])
  46. qdots = solve(kindiffs, qd)
  47. # Set angular velocity of remaining frames
  48. B.set_ang_vel(N, w_b_n_qd.subs(qdots))
  49. C.set_ang_acc(N, C.ang_vel_in(N).dt(B) + cross(B.ang_vel_in(N), C.ang_vel_in(N)))
  50. # Active forces
  51. F_CO = m*g*A.z
  52. # Create inertia dyadic of disc C about point CO
  53. I = (m * r**2) / 4
  54. J = (m * r**2) / 2
  55. I_C_CO = inertia(C, I, J, I)
  56. Disc = RigidBody('Disc', CO, C, m, (I_C_CO, CO))
  57. BL = [Disc]
  58. FL = [(CO, F_CO)]
  59. KM = KanesMethod(N, [q1, q2, q3, q4, q5], [u1, u2, u3], kd_eqs=kindiffs,
  60. q_dependent=[q6], configuration_constraints=f_c,
  61. u_dependent=[u4, u5, u6], velocity_constraints=f_v)
  62. (fr, fr_star) = KM.kanes_equations(BL, FL)
  63. # Test generalized form equations
  64. linearizer = KM.to_linearizer()
  65. assert linearizer.f_c == f_c
  66. assert linearizer.f_v == f_v
  67. assert linearizer.f_a == f_v.diff(t).subs(KM.kindiffdict())
  68. sol = solve(linearizer.f_0 + linearizer.f_1, qd)
  69. for qi in qdots.keys():
  70. assert sol[qi] == qdots[qi]
  71. assert simplify(linearizer.f_2 + linearizer.f_3 - fr - fr_star) == Matrix([0, 0, 0])
  72. # Perform the linearization
  73. # Precomputed operating point
  74. q_op = {q6: -r*cos(q2)}
  75. u_op = {u1: 0,
  76. u2: sin(q2)*q1d + q3d,
  77. u3: cos(q2)*q1d,
  78. u4: -r*(sin(q2)*q1d + q3d)*cos(q3),
  79. u5: 0,
  80. u6: -r*(sin(q2)*q1d + q3d)*sin(q3)}
  81. qd_op = {q2d: 0,
  82. q4d: -r*(sin(q2)*q1d + q3d)*cos(q1),
  83. q5d: -r*(sin(q2)*q1d + q3d)*sin(q1),
  84. q6d: 0}
  85. ud_op = {u1d: 4*g*sin(q2)/(5*r) + sin(2*q2)*q1d**2/2 + 6*cos(q2)*q1d*q3d/5,
  86. u2d: 0,
  87. u3d: 0,
  88. u4d: r*(sin(q2)*sin(q3)*q1d*q3d + sin(q3)*q3d**2),
  89. u5d: r*(4*g*sin(q2)/(5*r) + sin(2*q2)*q1d**2/2 + 6*cos(q2)*q1d*q3d/5),
  90. u6d: -r*(sin(q2)*cos(q3)*q1d*q3d + cos(q3)*q3d**2)}
  91. A, B = linearizer.linearize(op_point=[q_op, u_op, qd_op, ud_op], A_and_B=True, simplify=True)
  92. upright_nominal = {q1d: 0, q2: 0, m: 1, r: 1, g: 1}
  93. # Precomputed solution
  94. A_sol = Matrix([[0, 0, 0, 0, 0, 0, 0, 1],
  95. [0, 0, 0, 0, 0, 1, 0, 0],
  96. [0, 0, 0, 0, 0, 0, 1, 0],
  97. [sin(q1)*q3d, 0, 0, 0, 0, -sin(q1), -cos(q1), 0],
  98. [-cos(q1)*q3d, 0, 0, 0, 0, cos(q1), -sin(q1), 0],
  99. [0, Rational(4, 5), 0, 0, 0, 0, 0, 6*q3d/5],
  100. [0, 0, 0, 0, 0, 0, 0, 0],
  101. [0, 0, 0, 0, 0, -2*q3d, 0, 0]])
  102. B_sol = Matrix([])
  103. # Check that linearization is correct
  104. assert A.subs(upright_nominal) == A_sol
  105. assert B.subs(upright_nominal) == B_sol
  106. # Check eigenvalues at critical speed are all zero:
  107. assert sympify(A.subs(upright_nominal).subs(q3d, 1/sqrt(3))).eigenvals() == {0: 8}
  108. # Check whether alternative solvers work
  109. # symengine doesn't support method='GJ'
  110. linearizer = KM.to_linearizer(linear_solver='GJ')
  111. A, B = linearizer.linearize(op_point=[q_op, u_op, qd_op, ud_op],
  112. A_and_B=True, simplify=True)
  113. assert A.subs(upright_nominal) == A_sol
  114. assert B.subs(upright_nominal) == B_sol
  115. def test_linearize_pendulum_kane_minimal():
  116. q1 = dynamicsymbols('q1') # angle of pendulum
  117. u1 = dynamicsymbols('u1') # Angular velocity
  118. q1d = dynamicsymbols('q1', 1) # Angular velocity
  119. L, m, t = symbols('L, m, t')
  120. g = 9.8
  121. # Compose world frame
  122. N = ReferenceFrame('N')
  123. pN = Point('N*')
  124. pN.set_vel(N, 0)
  125. # A.x is along the pendulum
  126. A = N.orientnew('A', 'axis', [q1, N.z])
  127. A.set_ang_vel(N, u1*N.z)
  128. # Locate point P relative to the origin N*
  129. P = pN.locatenew('P', L*A.x)
  130. P.v2pt_theory(pN, N, A)
  131. pP = Particle('pP', P, m)
  132. # Create Kinematic Differential Equations
  133. kde = Matrix([q1d - u1])
  134. # Input the force resultant at P
  135. R = m*g*N.x
  136. # Solve for eom with kanes method
  137. KM = KanesMethod(N, q_ind=[q1], u_ind=[u1], kd_eqs=kde)
  138. (fr, frstar) = KM.kanes_equations([pP], [(P, R)])
  139. # Linearize
  140. A, B, inp_vec = KM.linearize(A_and_B=True, simplify=True)
  141. assert A == Matrix([[0, 1], [-9.8*cos(q1)/L, 0]])
  142. assert B == Matrix([])
  143. def test_linearize_pendulum_kane_nonminimal():
  144. # Create generalized coordinates and speeds for this non-minimal realization
  145. # q1, q2 = N.x and N.y coordinates of pendulum
  146. # u1, u2 = N.x and N.y velocities of pendulum
  147. q1, q2 = dynamicsymbols('q1:3')
  148. q1d, q2d = dynamicsymbols('q1:3', level=1)
  149. u1, u2 = dynamicsymbols('u1:3')
  150. u1d, u2d = dynamicsymbols('u1:3', level=1)
  151. L, m, t = symbols('L, m, t')
  152. g = 9.8
  153. # Compose world frame
  154. N = ReferenceFrame('N')
  155. pN = Point('N*')
  156. pN.set_vel(N, 0)
  157. # A.x is along the pendulum
  158. theta1 = atan(q2/q1)
  159. A = N.orientnew('A', 'axis', [theta1, N.z])
  160. # Locate the pendulum mass
  161. P = pN.locatenew('P1', q1*N.x + q2*N.y)
  162. pP = Particle('pP', P, m)
  163. # Calculate the kinematic differential equations
  164. kde = Matrix([q1d - u1,
  165. q2d - u2])
  166. dq_dict = solve(kde, [q1d, q2d])
  167. # Set velocity of point P
  168. P.set_vel(N, P.pos_from(pN).dt(N).subs(dq_dict))
  169. # Configuration constraint is length of pendulum
  170. f_c = Matrix([P.pos_from(pN).magnitude() - L])
  171. # Velocity constraint is that the velocity in the A.x direction is
  172. # always zero (the pendulum is never getting longer).
  173. f_v = Matrix([P.vel(N).express(A).dot(A.x)])
  174. f_v.simplify()
  175. # Acceleration constraints is the time derivative of the velocity constraint
  176. f_a = f_v.diff(t)
  177. f_a.simplify()
  178. # Input the force resultant at P
  179. R = m*g*N.x
  180. # Derive the equations of motion using the KanesMethod class.
  181. KM = KanesMethod(N, q_ind=[q2], u_ind=[u2], q_dependent=[q1],
  182. u_dependent=[u1], configuration_constraints=f_c,
  183. velocity_constraints=f_v, acceleration_constraints=f_a, kd_eqs=kde)
  184. (fr, frstar) = KM.kanes_equations([pP], [(P, R)])
  185. # Set the operating point to be straight down, and non-moving
  186. q_op = {q1: L, q2: 0}
  187. u_op = {u1: 0, u2: 0}
  188. ud_op = {u1d: 0, u2d: 0}
  189. A, B, inp_vec = KM.linearize(op_point=[q_op, u_op, ud_op], A_and_B=True,
  190. simplify=True)
  191. assert A.expand() == Matrix([[0, 1], [-9.8/L, 0]])
  192. assert B == Matrix([])
  193. # symengine doesn't support method='GJ'
  194. A, B, inp_vec = KM.linearize(op_point=[q_op, u_op, ud_op], A_and_B=True,
  195. simplify=True, linear_solver='GJ')
  196. assert A.expand() == Matrix([[0, 1], [-9.8/L, 0]])
  197. assert B == Matrix([])
  198. A, B, inp_vec = KM.linearize(op_point=[q_op, u_op, ud_op],
  199. A_and_B=True,
  200. simplify=True,
  201. linear_solver=lambda A, b: A.LUsolve(b))
  202. assert A.expand() == Matrix([[0, 1], [-9.8/L, 0]])
  203. assert B == Matrix([])
  204. def test_linearize_pendulum_lagrange_minimal():
  205. q1 = dynamicsymbols('q1') # angle of pendulum
  206. q1d = dynamicsymbols('q1', 1) # Angular velocity
  207. L, m, t = symbols('L, m, t')
  208. g = 9.8
  209. # Compose world frame
  210. N = ReferenceFrame('N')
  211. pN = Point('N*')
  212. pN.set_vel(N, 0)
  213. # A.x is along the pendulum
  214. A = N.orientnew('A', 'axis', [q1, N.z])
  215. A.set_ang_vel(N, q1d*N.z)
  216. # Locate point P relative to the origin N*
  217. P = pN.locatenew('P', L*A.x)
  218. P.v2pt_theory(pN, N, A)
  219. pP = Particle('pP', P, m)
  220. # Solve for eom with Lagranges method
  221. Lag = Lagrangian(N, pP)
  222. LM = LagrangesMethod(Lag, [q1], forcelist=[(P, m*g*N.x)], frame=N)
  223. LM.form_lagranges_equations()
  224. # Linearize
  225. A, B, inp_vec = LM.linearize([q1], [q1d], A_and_B=True)
  226. assert simplify(A) == Matrix([[0, 1], [-9.8*cos(q1)/L, 0]])
  227. assert B == Matrix([])
  228. # Check an alternative solver
  229. A, B, inp_vec = LM.linearize([q1], [q1d], A_and_B=True, linear_solver='GJ')
  230. assert simplify(A) == Matrix([[0, 1], [-9.8*cos(q1)/L, 0]])
  231. assert B == Matrix([])
  232. def test_linearize_pendulum_lagrange_nonminimal():
  233. q1, q2 = dynamicsymbols('q1:3')
  234. q1d, q2d = dynamicsymbols('q1:3', level=1)
  235. L, m, t = symbols('L, m, t')
  236. g = 9.8
  237. # Compose World Frame
  238. N = ReferenceFrame('N')
  239. pN = Point('N*')
  240. pN.set_vel(N, 0)
  241. # A.x is along the pendulum
  242. theta1 = atan(q2/q1)
  243. A = N.orientnew('A', 'axis', [theta1, N.z])
  244. # Create point P, the pendulum mass
  245. P = pN.locatenew('P1', q1*N.x + q2*N.y)
  246. P.set_vel(N, P.pos_from(pN).dt(N))
  247. pP = Particle('pP', P, m)
  248. # Constraint Equations
  249. f_c = Matrix([q1**2 + q2**2 - L**2])
  250. # Calculate the lagrangian, and form the equations of motion
  251. Lag = Lagrangian(N, pP)
  252. LM = LagrangesMethod(Lag, [q1, q2], hol_coneqs=f_c, forcelist=[(P, m*g*N.x)], frame=N)
  253. LM.form_lagranges_equations()
  254. # Compose operating point
  255. op_point = {q1: L, q2: 0, q1d: 0, q2d: 0, q1d.diff(t): 0, q2d.diff(t): 0}
  256. # Solve for multiplier operating point
  257. lam_op = LM.solve_multipliers(op_point=op_point)
  258. op_point.update(lam_op)
  259. # Perform the Linearization
  260. A, B, inp_vec = LM.linearize([q2], [q2d], [q1], [q1d],
  261. op_point=op_point, A_and_B=True)
  262. assert simplify(A) == Matrix([[0, 1], [-9.8/L, 0]])
  263. assert B == Matrix([])
  264. # Check if passing a function to linear_solver works
  265. A, B, inp_vec = LM.linearize([q2], [q2d], [q1], [q1d], op_point=op_point,
  266. A_and_B=True, linear_solver=lambda A, b:
  267. A.LUsolve(b))
  268. assert simplify(A) == Matrix([[0, 1], [-9.8/L, 0]])
  269. assert B == Matrix([])
  270. def test_linearize_rolling_disc_lagrange():
  271. q1, q2, q3 = q = dynamicsymbols('q1 q2 q3')
  272. q1d, q2d, q3d = qd = dynamicsymbols('q1 q2 q3', 1)
  273. r, m, g = symbols('r m g')
  274. N = ReferenceFrame('N')
  275. Y = N.orientnew('Y', 'Axis', [q1, N.z])
  276. L = Y.orientnew('L', 'Axis', [q2, Y.x])
  277. R = L.orientnew('R', 'Axis', [q3, L.y])
  278. C = Point('C')
  279. C.set_vel(N, 0)
  280. Dmc = C.locatenew('Dmc', r * L.z)
  281. Dmc.v2pt_theory(C, N, R)
  282. I = inertia(L, m / 4 * r**2, m / 2 * r**2, m / 4 * r**2)
  283. BodyD = RigidBody('BodyD', Dmc, R, m, (I, Dmc))
  284. BodyD.potential_energy = - m * g * r * cos(q2)
  285. Lag = Lagrangian(N, BodyD)
  286. l = LagrangesMethod(Lag, q)
  287. l.form_lagranges_equations()
  288. # Linearize about steady-state upright rolling
  289. op_point = {q1: 0, q2: 0, q3: 0,
  290. q1d: 0, q2d: 0,
  291. q1d.diff(): 0, q2d.diff(): 0, q3d.diff(): 0}
  292. A = l.linearize(q_ind=q, qd_ind=qd, op_point=op_point, A_and_B=True)[0]
  293. sol = Matrix([[0, 0, 0, 1, 0, 0],
  294. [0, 0, 0, 0, 1, 0],
  295. [0, 0, 0, 0, 0, 1],
  296. [0, 0, 0, 0, -6*q3d, 0],
  297. [0, -4*g/(5*r), 0, 6*q3d/5, 0, 0],
  298. [0, 0, 0, 0, 0, 0]])
  299. assert A == sol