test_kane3.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. from sympy.core.evalf import evalf
  2. from sympy.core.numbers import pi
  3. from sympy.core.symbol import symbols
  4. from sympy.functions.elementary.miscellaneous import sqrt
  5. from sympy.functions.elementary.trigonometric import acos, sin, cos
  6. from sympy.matrices.dense import Matrix
  7. from sympy.physics.mechanics import (ReferenceFrame, dynamicsymbols,
  8. KanesMethod, inertia, msubs, Point, RigidBody, dot)
  9. from sympy.testing.pytest import slow, ON_CI, skip
  10. @slow
  11. def test_bicycle():
  12. if ON_CI:
  13. skip("Too slow for CI.")
  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. # Note that this code has been crudely ported from Autolev, which is the
  20. # reason for some of the unusual naming conventions. It was purposefully as
  21. # similar as possible in order to aide debugging.
  22. # Declare Coordinates & Speeds
  23. # Simple definitions for qdots - qd = u
  24. # Speeds are: yaw frame ang. rate, roll frame ang. rate, rear wheel frame
  25. # ang. rate (spinning motion), frame ang. rate (pitching motion), steering
  26. # frame ang. rate, and front wheel ang. rate (spinning motion).
  27. # Wheel positions are ignorable coordinates, so they are not introduced.
  28. q1, q2, q4, q5 = dynamicsymbols('q1 q2 q4 q5')
  29. q1d, q2d, q4d, q5d = dynamicsymbols('q1 q2 q4 q5', 1)
  30. u1, u2, u3, u4, u5, u6 = dynamicsymbols('u1 u2 u3 u4 u5 u6')
  31. u1d, u2d, u3d, u4d, u5d, u6d = dynamicsymbols('u1 u2 u3 u4 u5 u6', 1)
  32. # Declare System's Parameters
  33. WFrad, WRrad, htangle, forkoffset = symbols('WFrad WRrad htangle forkoffset')
  34. forklength, framelength, forkcg1 = symbols('forklength framelength forkcg1')
  35. forkcg3, framecg1, framecg3, Iwr11 = symbols('forkcg3 framecg1 framecg3 Iwr11')
  36. Iwr22, Iwf11, Iwf22, Iframe11 = symbols('Iwr22 Iwf11 Iwf22 Iframe11')
  37. Iframe22, Iframe33, Iframe31, Ifork11 = symbols('Iframe22 Iframe33 Iframe31 Ifork11')
  38. Ifork22, Ifork33, Ifork31, g = symbols('Ifork22 Ifork33 Ifork31 g')
  39. mframe, mfork, mwf, mwr = symbols('mframe mfork mwf mwr')
  40. # Set up reference frames for the system
  41. # N - inertial
  42. # Y - yaw
  43. # R - roll
  44. # WR - rear wheel, rotation angle is ignorable coordinate so not oriented
  45. # Frame - bicycle frame
  46. # TempFrame - statically rotated frame for easier reference inertia definition
  47. # Fork - bicycle fork
  48. # TempFork - statically rotated frame for easier reference inertia definition
  49. # WF - front wheel, again posses a ignorable coordinate
  50. N = ReferenceFrame('N')
  51. Y = N.orientnew('Y', 'Axis', [q1, N.z])
  52. R = Y.orientnew('R', 'Axis', [q2, Y.x])
  53. Frame = R.orientnew('Frame', 'Axis', [q4 + htangle, R.y])
  54. WR = ReferenceFrame('WR')
  55. TempFrame = Frame.orientnew('TempFrame', 'Axis', [-htangle, Frame.y])
  56. Fork = Frame.orientnew('Fork', 'Axis', [q5, Frame.x])
  57. TempFork = Fork.orientnew('TempFork', 'Axis', [-htangle, Fork.y])
  58. WF = ReferenceFrame('WF')
  59. # Kinematics of the Bicycle First block of code is forming the positions of
  60. # the relevant points
  61. # rear wheel contact -> rear wheel mass center -> frame mass center +
  62. # frame/fork connection -> fork mass center + front wheel mass center ->
  63. # front wheel contact point
  64. WR_cont = Point('WR_cont')
  65. WR_mc = WR_cont.locatenew('WR_mc', WRrad * R.z)
  66. Steer = WR_mc.locatenew('Steer', framelength * Frame.z)
  67. Frame_mc = WR_mc.locatenew('Frame_mc', - framecg1 * Frame.x
  68. + framecg3 * Frame.z)
  69. Fork_mc = Steer.locatenew('Fork_mc', - forkcg1 * Fork.x
  70. + forkcg3 * Fork.z)
  71. WF_mc = Steer.locatenew('WF_mc', forklength * Fork.x + forkoffset * Fork.z)
  72. WF_cont = WF_mc.locatenew('WF_cont', WFrad * (dot(Fork.y, Y.z) * Fork.y -
  73. Y.z).normalize())
  74. # Set the angular velocity of each frame.
  75. # Angular accelerations end up being calculated automatically by
  76. # differentiating the angular velocities when first needed.
  77. # u1 is yaw rate
  78. # u2 is roll rate
  79. # u3 is rear wheel rate
  80. # u4 is frame pitch rate
  81. # u5 is fork steer rate
  82. # u6 is front wheel rate
  83. Y.set_ang_vel(N, u1 * Y.z)
  84. R.set_ang_vel(Y, u2 * R.x)
  85. WR.set_ang_vel(Frame, u3 * Frame.y)
  86. Frame.set_ang_vel(R, u4 * Frame.y)
  87. Fork.set_ang_vel(Frame, u5 * Fork.x)
  88. WF.set_ang_vel(Fork, u6 * Fork.y)
  89. # Form the velocities of the previously defined points, using the 2 - point
  90. # theorem (written out by hand here). Accelerations again are calculated
  91. # automatically when first needed.
  92. WR_cont.set_vel(N, 0)
  93. WR_mc.v2pt_theory(WR_cont, N, WR)
  94. Steer.v2pt_theory(WR_mc, N, Frame)
  95. Frame_mc.v2pt_theory(WR_mc, N, Frame)
  96. Fork_mc.v2pt_theory(Steer, N, Fork)
  97. WF_mc.v2pt_theory(Steer, N, Fork)
  98. WF_cont.v2pt_theory(WF_mc, N, WF)
  99. # Sets the inertias of each body. Uses the inertia frame to construct the
  100. # inertia dyadics. Wheel inertias are only defined by principle moments of
  101. # inertia, and are in fact constant in the frame and fork reference frames;
  102. # it is for this reason that the orientations of the wheels does not need
  103. # to be defined. The frame and fork inertias are defined in the 'Temp'
  104. # frames which are fixed to the appropriate body frames; this is to allow
  105. # easier input of the reference values of the benchmark paper. Note that
  106. # due to slightly different orientations, the products of inertia need to
  107. # have their signs flipped; this is done later when entering the numerical
  108. # value.
  109. Frame_I = (inertia(TempFrame, Iframe11, Iframe22, Iframe33, 0, 0, Iframe31), Frame_mc)
  110. Fork_I = (inertia(TempFork, Ifork11, Ifork22, Ifork33, 0, 0, Ifork31), Fork_mc)
  111. WR_I = (inertia(Frame, Iwr11, Iwr22, Iwr11), WR_mc)
  112. WF_I = (inertia(Fork, Iwf11, Iwf22, Iwf11), WF_mc)
  113. # Declaration of the RigidBody containers. ::
  114. BodyFrame = RigidBody('BodyFrame', Frame_mc, Frame, mframe, Frame_I)
  115. BodyFork = RigidBody('BodyFork', Fork_mc, Fork, mfork, Fork_I)
  116. BodyWR = RigidBody('BodyWR', WR_mc, WR, mwr, WR_I)
  117. BodyWF = RigidBody('BodyWF', WF_mc, WF, mwf, WF_I)
  118. # The kinematic differential equations; they are defined quite simply. Each
  119. # entry in this list is equal to zero.
  120. kd = [q1d - u1, q2d - u2, q4d - u4, q5d - u5]
  121. # The nonholonomic constraints are the velocity of the front wheel contact
  122. # point dotted into the X, Y, and Z directions; the yaw frame is used as it
  123. # is "closer" to the front wheel (1 less DCM connecting them). These
  124. # constraints force the velocity of the front wheel contact point to be 0
  125. # in the inertial frame; the X and Y direction constraints enforce a
  126. # "no-slip" condition, and the Z direction constraint forces the front
  127. # wheel contact point to not move away from the ground frame, essentially
  128. # replicating the holonomic constraint which does not allow the frame pitch
  129. # to change in an invalid fashion.
  130. conlist_speed = [WF_cont.vel(N) & Y.x, WF_cont.vel(N) & Y.y, WF_cont.vel(N) & Y.z]
  131. # The holonomic constraint is that the position from the rear wheel contact
  132. # point to the front wheel contact point when dotted into the
  133. # normal-to-ground plane direction must be zero; effectively that the front
  134. # and rear wheel contact points are always touching the ground plane. This
  135. # is actually not part of the dynamic equations, but instead is necessary
  136. # for the lineraization process.
  137. conlist_coord = [WF_cont.pos_from(WR_cont) & Y.z]
  138. # The force list; each body has the appropriate gravitational force applied
  139. # at its mass center.
  140. FL = [(Frame_mc, -mframe * g * Y.z),
  141. (Fork_mc, -mfork * g * Y.z),
  142. (WF_mc, -mwf * g * Y.z),
  143. (WR_mc, -mwr * g * Y.z)]
  144. BL = [BodyFrame, BodyFork, BodyWR, BodyWF]
  145. # The N frame is the inertial frame, coordinates are supplied in the order
  146. # of independent, dependent coordinates, as are the speeds. The kinematic
  147. # differential equation are also entered here. Here the dependent speeds
  148. # are specified, in the same order they were provided in earlier, along
  149. # with the non-holonomic constraints. The dependent coordinate is also
  150. # provided, with the holonomic constraint. Again, this is only provided
  151. # for the linearization process.
  152. KM = KanesMethod(N, q_ind=[q1, q2, q5],
  153. q_dependent=[q4], configuration_constraints=conlist_coord,
  154. u_ind=[u2, u3, u5],
  155. u_dependent=[u1, u4, u6], velocity_constraints=conlist_speed,
  156. kd_eqs=kd)
  157. (fr, frstar) = KM.kanes_equations(BL, FL)
  158. # This is the start of entering in the numerical values from the benchmark
  159. # paper to validate the eigen values of the linearized equations from this
  160. # model to the reference eigen values. Look at the aforementioned paper for
  161. # more information. Some of these are intermediate values, used to
  162. # transform values from the paper into the coordinate systems used in this
  163. # model.
  164. PaperRadRear = 0.3
  165. PaperRadFront = 0.35
  166. HTA = evalf.N(pi / 2 - pi / 10)
  167. TrailPaper = 0.08
  168. rake = evalf.N(-(TrailPaper*sin(HTA)-(PaperRadFront*cos(HTA))))
  169. PaperWb = 1.02
  170. PaperFrameCgX = 0.3
  171. PaperFrameCgZ = 0.9
  172. PaperForkCgX = 0.9
  173. PaperForkCgZ = 0.7
  174. FrameLength = evalf.N(PaperWb*sin(HTA)-(rake-(PaperRadFront-PaperRadRear)*cos(HTA)))
  175. FrameCGNorm = evalf.N((PaperFrameCgZ - PaperRadRear-(PaperFrameCgX/sin(HTA))*cos(HTA))*sin(HTA))
  176. FrameCGPar = evalf.N(PaperFrameCgX / sin(HTA) + (PaperFrameCgZ - PaperRadRear - PaperFrameCgX / sin(HTA) * cos(HTA)) * cos(HTA))
  177. tempa = evalf.N(PaperForkCgZ - PaperRadFront)
  178. tempb = evalf.N(PaperWb-PaperForkCgX)
  179. tempc = evalf.N(sqrt(tempa**2+tempb**2))
  180. PaperForkL = evalf.N(PaperWb*cos(HTA)-(PaperRadFront-PaperRadRear)*sin(HTA))
  181. ForkCGNorm = evalf.N(rake+(tempc * sin(pi/2-HTA-acos(tempa/tempc))))
  182. ForkCGPar = evalf.N(tempc * cos((pi/2-HTA)-acos(tempa/tempc))-PaperForkL)
  183. # Here is the final assembly of the numerical values. The symbol 'v' is the
  184. # forward speed of the bicycle (a concept which only makes sense in the
  185. # upright, static equilibrium case?). These are in a dictionary which will
  186. # later be substituted in. Again the sign on the *product* of inertia
  187. # values is flipped here, due to different orientations of coordinate
  188. # systems.
  189. v = symbols('v')
  190. val_dict = {WFrad: PaperRadFront,
  191. WRrad: PaperRadRear,
  192. htangle: HTA,
  193. forkoffset: rake,
  194. forklength: PaperForkL,
  195. framelength: FrameLength,
  196. forkcg1: ForkCGPar,
  197. forkcg3: ForkCGNorm,
  198. framecg1: FrameCGNorm,
  199. framecg3: FrameCGPar,
  200. Iwr11: 0.0603,
  201. Iwr22: 0.12,
  202. Iwf11: 0.1405,
  203. Iwf22: 0.28,
  204. Ifork11: 0.05892,
  205. Ifork22: 0.06,
  206. Ifork33: 0.00708,
  207. Ifork31: 0.00756,
  208. Iframe11: 9.2,
  209. Iframe22: 11,
  210. Iframe33: 2.8,
  211. Iframe31: -2.4,
  212. mfork: 4,
  213. mframe: 85,
  214. mwf: 3,
  215. mwr: 2,
  216. g: 9.81,
  217. q1: 0,
  218. q2: 0,
  219. q4: 0,
  220. q5: 0,
  221. u1: 0,
  222. u2: 0,
  223. u3: v / PaperRadRear,
  224. u4: 0,
  225. u5: 0,
  226. u6: v / PaperRadFront}
  227. # Linearizes the forcing vector; the equations are set up as MM udot =
  228. # forcing, where MM is the mass matrix, udot is the vector representing the
  229. # time derivatives of the generalized speeds, and forcing is a vector which
  230. # contains both external forcing terms and internal forcing terms, such as
  231. # centripital or coriolis forces. This actually returns a matrix with as
  232. # many rows as *total* coordinates and speeds, but only as many columns as
  233. # independent coordinates and speeds.
  234. forcing_lin = KM.linearize()[0]
  235. # As mentioned above, the size of the linearized forcing terms is expanded
  236. # to include both q's and u's, so the mass matrix must have this done as
  237. # well. This will likely be changed to be part of the linearized process,
  238. # for future reference.
  239. MM_full = KM.mass_matrix_full
  240. MM_full_s = msubs(MM_full, val_dict)
  241. forcing_lin_s = msubs(forcing_lin, KM.kindiffdict(), val_dict)
  242. MM_full_s = MM_full_s.evalf()
  243. forcing_lin_s = forcing_lin_s.evalf()
  244. # Finally, we construct an "A" matrix for the form xdot = A x (x being the
  245. # state vector, although in this case, the sizes are a little off). The
  246. # following line extracts only the minimum entries required for eigenvalue
  247. # analysis, which correspond to rows and columns for lean, steer, lean
  248. # rate, and steer rate.
  249. Amat = MM_full_s.inv() * forcing_lin_s
  250. A = Amat.extract([1, 2, 4, 6], [1, 2, 3, 5])
  251. # Precomputed for comparison
  252. Res = Matrix([[ 0, 0, 1.0, 0],
  253. [ 0, 0, 0, 1.0],
  254. [9.48977444677355, -0.891197738059089*v**2 - 0.571523173729245, -0.105522449805691*v, -0.330515398992311*v],
  255. [11.7194768719633, -1.97171508499972*v**2 + 30.9087533932407, 3.67680523332152*v, -3.08486552743311*v]])
  256. # Actual eigenvalue comparison
  257. eps = 1.e-12
  258. for i in range(6):
  259. error = Res.subs(v, i) - A.subs(v, i)
  260. assert all(abs(x) < eps for x in error)