lagrange.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476
  1. from sympy.core.backend 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 (find_dynamicsymbols, msubs,
  6. _f_list_parser)
  7. from sympy.physics.mechanics.linearize import Linearizer
  8. from sympy.utilities.iterables import iterable
  9. __all__ = ['LagrangesMethod']
  10. class LagrangesMethod(_Methods):
  11. """Lagrange's method object.
  12. Explanation
  13. ===========
  14. This object generates the equations of motion in a two step procedure. The
  15. first step involves the initialization of LagrangesMethod by supplying the
  16. Lagrangian and the generalized coordinates, at the bare minimum. If there
  17. are any constraint equations, they can be supplied as keyword arguments.
  18. The Lagrange multipliers are automatically generated and are equal in
  19. number to the constraint equations. Similarly any non-conservative forces
  20. can be supplied in an iterable (as described below and also shown in the
  21. example) along with a ReferenceFrame. This is also discussed further in the
  22. __init__ method.
  23. Attributes
  24. ==========
  25. q, u : Matrix
  26. Matrices of the generalized coordinates and speeds
  27. loads : iterable
  28. Iterable of (Point, vector) or (ReferenceFrame, vector) tuples
  29. describing the forces on the system.
  30. bodies : iterable
  31. Iterable containing the rigid bodies and particles of the system.
  32. mass_matrix : Matrix
  33. The system's mass matrix
  34. forcing : Matrix
  35. The system's forcing vector
  36. mass_matrix_full : Matrix
  37. The "mass matrix" for the qdot's, qdoubledot's, and the
  38. lagrange multipliers (lam)
  39. forcing_full : Matrix
  40. The forcing vector for the qdot's, qdoubledot's and
  41. lagrange multipliers (lam)
  42. Examples
  43. ========
  44. This is a simple example for a one degree of freedom translational
  45. spring-mass-damper.
  46. In this example, we first need to do the kinematics.
  47. This involves creating generalized coordinates and their derivatives.
  48. Then we create a point and set its velocity in a frame.
  49. >>> from sympy.physics.mechanics import LagrangesMethod, Lagrangian
  50. >>> from sympy.physics.mechanics import ReferenceFrame, Particle, Point
  51. >>> from sympy.physics.mechanics import dynamicsymbols
  52. >>> from sympy import symbols
  53. >>> q = dynamicsymbols('q')
  54. >>> qd = dynamicsymbols('q', 1)
  55. >>> m, k, b = symbols('m k b')
  56. >>> N = ReferenceFrame('N')
  57. >>> P = Point('P')
  58. >>> P.set_vel(N, qd * N.x)
  59. We need to then prepare the information as required by LagrangesMethod to
  60. generate equations of motion.
  61. First we create the Particle, which has a point attached to it.
  62. Following this the lagrangian is created from the kinetic and potential
  63. energies.
  64. Then, an iterable of nonconservative forces/torques must be constructed,
  65. where each item is a (Point, Vector) or (ReferenceFrame, Vector) tuple,
  66. with the Vectors representing the nonconservative forces or torques.
  67. >>> Pa = Particle('Pa', P, m)
  68. >>> Pa.potential_energy = k * q**2 / 2.0
  69. >>> L = Lagrangian(N, Pa)
  70. >>> fl = [(P, -b * qd * N.x)]
  71. Finally we can generate the equations of motion.
  72. First we create the LagrangesMethod object. To do this one must supply
  73. the Lagrangian, and the generalized coordinates. The constraint equations,
  74. the forcelist, and the inertial frame may also be provided, if relevant.
  75. Next we generate Lagrange's equations of motion, such that:
  76. Lagrange's equations of motion = 0.
  77. We have the equations of motion at this point.
  78. >>> l = LagrangesMethod(L, [q], forcelist = fl, frame = N)
  79. >>> print(l.form_lagranges_equations())
  80. Matrix([[b*Derivative(q(t), t) + 1.0*k*q(t) + m*Derivative(q(t), (t, 2))]])
  81. We can also solve for the states using the 'rhs' method.
  82. >>> print(l.rhs())
  83. Matrix([[Derivative(q(t), t)], [(-b*Derivative(q(t), t) - 1.0*k*q(t))/m]])
  84. Please refer to the docstrings on each method for more details.
  85. """
  86. def __init__(self, Lagrangian, qs, forcelist=None, bodies=None, frame=None,
  87. hol_coneqs=None, nonhol_coneqs=None):
  88. """Supply the following for the initialization of LagrangesMethod.
  89. Lagrangian : Sympifyable
  90. qs : array_like
  91. The generalized coordinates
  92. hol_coneqs : array_like, optional
  93. The holonomic constraint equations
  94. nonhol_coneqs : array_like, optional
  95. The nonholonomic constraint equations
  96. forcelist : iterable, optional
  97. Takes an iterable of (Point, Vector) or (ReferenceFrame, Vector)
  98. tuples which represent the force at a point or torque on a frame.
  99. This feature is primarily to account for the nonconservative forces
  100. and/or moments.
  101. bodies : iterable, optional
  102. Takes an iterable containing the rigid bodies and particles of the
  103. system.
  104. frame : ReferenceFrame, optional
  105. Supply the inertial frame. This is used to determine the
  106. generalized forces due to non-conservative forces.
  107. """
  108. self._L = Matrix([sympify(Lagrangian)])
  109. self.eom = None
  110. self._m_cd = Matrix() # Mass Matrix of differentiated coneqs
  111. self._m_d = Matrix() # Mass Matrix of dynamic equations
  112. self._f_cd = Matrix() # Forcing part of the diff coneqs
  113. self._f_d = Matrix() # Forcing part of the dynamic equations
  114. self.lam_coeffs = Matrix() # The coeffecients of the multipliers
  115. forcelist = forcelist if forcelist else []
  116. if not iterable(forcelist):
  117. raise TypeError('Force pairs must be supplied in an iterable.')
  118. self._forcelist = forcelist
  119. if frame and not isinstance(frame, ReferenceFrame):
  120. raise TypeError('frame must be a valid ReferenceFrame')
  121. self._bodies = bodies
  122. self.inertial = frame
  123. self.lam_vec = Matrix()
  124. self._term1 = Matrix()
  125. self._term2 = Matrix()
  126. self._term3 = Matrix()
  127. self._term4 = Matrix()
  128. # Creating the qs, qdots and qdoubledots
  129. if not iterable(qs):
  130. raise TypeError('Generalized coordinates must be an iterable')
  131. self._q = Matrix(qs)
  132. self._qdots = self.q.diff(dynamicsymbols._t)
  133. self._qdoubledots = self._qdots.diff(dynamicsymbols._t)
  134. mat_build = lambda x: Matrix(x) if x else Matrix()
  135. hol_coneqs = mat_build(hol_coneqs)
  136. nonhol_coneqs = mat_build(nonhol_coneqs)
  137. self.coneqs = Matrix([hol_coneqs.diff(dynamicsymbols._t),
  138. nonhol_coneqs])
  139. self._hol_coneqs = hol_coneqs
  140. def form_lagranges_equations(self):
  141. """Method to form Lagrange's equations of motion.
  142. Returns a vector of equations of motion using Lagrange's equations of
  143. the second kind.
  144. """
  145. qds = self._qdots
  146. qdd_zero = {i: 0 for i in self._qdoubledots}
  147. n = len(self.q)
  148. # Internally we represent the EOM as four terms:
  149. # EOM = term1 - term2 - term3 - term4 = 0
  150. # First term
  151. self._term1 = self._L.jacobian(qds)
  152. self._term1 = self._term1.diff(dynamicsymbols._t).T
  153. # Second term
  154. self._term2 = self._L.jacobian(self.q).T
  155. # Third term
  156. if self.coneqs:
  157. coneqs = self.coneqs
  158. m = len(coneqs)
  159. # Creating the multipliers
  160. self.lam_vec = Matrix(dynamicsymbols('lam1:' + str(m + 1)))
  161. self.lam_coeffs = -coneqs.jacobian(qds)
  162. self._term3 = self.lam_coeffs.T * self.lam_vec
  163. # Extracting the coeffecients of the qdds from the diff coneqs
  164. diffconeqs = coneqs.diff(dynamicsymbols._t)
  165. self._m_cd = diffconeqs.jacobian(self._qdoubledots)
  166. # The remaining terms i.e. the 'forcing' terms in diff coneqs
  167. self._f_cd = -diffconeqs.subs(qdd_zero)
  168. else:
  169. self._term3 = zeros(n, 1)
  170. # Fourth term
  171. if self.forcelist:
  172. N = self.inertial
  173. self._term4 = zeros(n, 1)
  174. for i, qd in enumerate(qds):
  175. flist = zip(*_f_list_parser(self.forcelist, N))
  176. self._term4[i] = sum(v.diff(qd, N) & f for (v, f) in flist)
  177. else:
  178. self._term4 = zeros(n, 1)
  179. # Form the dynamic mass and forcing matrices
  180. without_lam = self._term1 - self._term2 - self._term4
  181. self._m_d = without_lam.jacobian(self._qdoubledots)
  182. self._f_d = -without_lam.subs(qdd_zero)
  183. # Form the EOM
  184. self.eom = without_lam - self._term3
  185. return self.eom
  186. def _form_eoms(self):
  187. return self.form_lagranges_equations()
  188. @property
  189. def mass_matrix(self):
  190. """Returns the mass matrix, which is augmented by the Lagrange
  191. multipliers, if necessary.
  192. Explanation
  193. ===========
  194. If the system is described by 'n' generalized coordinates and there are
  195. no constraint equations then an n X n matrix is returned.
  196. If there are 'n' generalized coordinates and 'm' constraint equations
  197. have been supplied during initialization then an n X (n+m) matrix is
  198. returned. The (n + m - 1)th and (n + m)th columns contain the
  199. coefficients of the Lagrange multipliers.
  200. """
  201. if self.eom is None:
  202. raise ValueError('Need to compute the equations of motion first')
  203. if self.coneqs:
  204. return (self._m_d).row_join(self.lam_coeffs.T)
  205. else:
  206. return self._m_d
  207. @property
  208. def mass_matrix_full(self):
  209. """Augments the coefficients of qdots to the mass_matrix."""
  210. if self.eom is None:
  211. raise ValueError('Need to compute the equations of motion first')
  212. n = len(self.q)
  213. m = len(self.coneqs)
  214. row1 = eye(n).row_join(zeros(n, n + m))
  215. row2 = zeros(n, n).row_join(self.mass_matrix)
  216. if self.coneqs:
  217. row3 = zeros(m, n).row_join(self._m_cd).row_join(zeros(m, m))
  218. return row1.col_join(row2).col_join(row3)
  219. else:
  220. return row1.col_join(row2)
  221. @property
  222. def forcing(self):
  223. """Returns the forcing vector from 'lagranges_equations' method."""
  224. if self.eom is None:
  225. raise ValueError('Need to compute the equations of motion first')
  226. return self._f_d
  227. @property
  228. def forcing_full(self):
  229. """Augments qdots to the forcing vector above."""
  230. if self.eom is None:
  231. raise ValueError('Need to compute the equations of motion first')
  232. if self.coneqs:
  233. return self._qdots.col_join(self.forcing).col_join(self._f_cd)
  234. else:
  235. return self._qdots.col_join(self.forcing)
  236. def to_linearizer(self, q_ind=None, qd_ind=None, q_dep=None, qd_dep=None):
  237. """Returns an instance of the Linearizer class, initiated from the
  238. data in the LagrangesMethod class. This may be more desirable than using
  239. the linearize class method, as the Linearizer object will allow more
  240. efficient recalculation (i.e. about varying operating points).
  241. Parameters
  242. ==========
  243. q_ind, qd_ind : array_like, optional
  244. The independent generalized coordinates and speeds.
  245. q_dep, qd_dep : array_like, optional
  246. The dependent generalized coordinates and speeds.
  247. """
  248. # Compose vectors
  249. t = dynamicsymbols._t
  250. q = self.q
  251. u = self._qdots
  252. ud = u.diff(t)
  253. # Get vector of lagrange multipliers
  254. lams = self.lam_vec
  255. mat_build = lambda x: Matrix(x) if x else Matrix()
  256. q_i = mat_build(q_ind)
  257. q_d = mat_build(q_dep)
  258. u_i = mat_build(qd_ind)
  259. u_d = mat_build(qd_dep)
  260. # Compose general form equations
  261. f_c = self._hol_coneqs
  262. f_v = self.coneqs
  263. f_a = f_v.diff(t)
  264. f_0 = u
  265. f_1 = -u
  266. f_2 = self._term1
  267. f_3 = -(self._term2 + self._term4)
  268. f_4 = -self._term3
  269. # Check that there are an appropriate number of independent and
  270. # dependent coordinates
  271. if len(q_d) != len(f_c) or len(u_d) != len(f_v):
  272. raise ValueError(("Must supply {:} dependent coordinates, and " +
  273. "{:} dependent speeds").format(len(f_c), len(f_v)))
  274. if set(Matrix([q_i, q_d])) != set(q):
  275. raise ValueError("Must partition q into q_ind and q_dep, with " +
  276. "no extra or missing symbols.")
  277. if set(Matrix([u_i, u_d])) != set(u):
  278. raise ValueError("Must partition qd into qd_ind and qd_dep, " +
  279. "with no extra or missing symbols.")
  280. # Find all other dynamic symbols, forming the forcing vector r.
  281. # Sort r to make it canonical.
  282. insyms = set(Matrix([q, u, ud, lams]))
  283. r = list(find_dynamicsymbols(f_3, insyms))
  284. r.sort(key=default_sort_key)
  285. # Check for any derivatives of variables in r that are also found in r.
  286. for i in r:
  287. if diff(i, dynamicsymbols._t) in r:
  288. raise ValueError('Cannot have derivatives of specified \
  289. quantities when linearizing forcing terms.')
  290. return Linearizer(f_0, f_1, f_2, f_3, f_4, f_c, f_v, f_a, q, u, q_i,
  291. q_d, u_i, u_d, r, lams)
  292. def linearize(self, q_ind=None, qd_ind=None, q_dep=None, qd_dep=None,
  293. **kwargs):
  294. """Linearize the equations of motion about a symbolic operating point.
  295. Explanation
  296. ===========
  297. If kwarg A_and_B is False (default), returns M, A, B, r for the
  298. linearized form, M*[q', u']^T = A*[q_ind, u_ind]^T + B*r.
  299. If kwarg A_and_B is True, returns A, B, r for the linearized form
  300. dx = A*x + B*r, where x = [q_ind, u_ind]^T. Note that this is
  301. computationally intensive if there are many symbolic parameters. For
  302. this reason, it may be more desirable to use the default A_and_B=False,
  303. returning M, A, and B. Values may then be substituted in to these
  304. matrices, and the state space form found as
  305. A = P.T*M.inv()*A, B = P.T*M.inv()*B, where P = Linearizer.perm_mat.
  306. In both cases, r is found as all dynamicsymbols in the equations of
  307. motion that are not part of q, u, q', or u'. They are sorted in
  308. canonical form.
  309. The operating points may be also entered using the ``op_point`` kwarg.
  310. This takes a dictionary of {symbol: value}, or a an iterable of such
  311. dictionaries. The values may be numeric or symbolic. The more values
  312. you can specify beforehand, the faster this computation will run.
  313. For more documentation, please see the ``Linearizer`` class."""
  314. linearizer = self.to_linearizer(q_ind, qd_ind, q_dep, qd_dep)
  315. result = linearizer.linearize(**kwargs)
  316. return result + (linearizer.r,)
  317. def solve_multipliers(self, op_point=None, sol_type='dict'):
  318. """Solves for the values of the lagrange multipliers symbolically at
  319. the specified operating point.
  320. Parameters
  321. ==========
  322. op_point : dict or iterable of dicts, optional
  323. Point at which to solve at. The operating point is specified as
  324. a dictionary or iterable of dictionaries of {symbol: value}. The
  325. value may be numeric or symbolic itself.
  326. sol_type : str, optional
  327. Solution return type. Valid options are:
  328. - 'dict': A dict of {symbol : value} (default)
  329. - 'Matrix': An ordered column matrix of the solution
  330. """
  331. # Determine number of multipliers
  332. k = len(self.lam_vec)
  333. if k == 0:
  334. raise ValueError("System has no lagrange multipliers to solve for.")
  335. # Compose dict of operating conditions
  336. if isinstance(op_point, dict):
  337. op_point_dict = op_point
  338. elif iterable(op_point):
  339. op_point_dict = {}
  340. for op in op_point:
  341. op_point_dict.update(op)
  342. elif op_point is None:
  343. op_point_dict = {}
  344. else:
  345. raise TypeError("op_point must be either a dictionary or an "
  346. "iterable of dictionaries.")
  347. # Compose the system to be solved
  348. mass_matrix = self.mass_matrix.col_join(-self.lam_coeffs.row_join(
  349. zeros(k, k)))
  350. force_matrix = self.forcing.col_join(self._f_cd)
  351. # Sub in the operating point
  352. mass_matrix = msubs(mass_matrix, op_point_dict)
  353. force_matrix = msubs(force_matrix, op_point_dict)
  354. # Solve for the multipliers
  355. sol_list = mass_matrix.LUsolve(-force_matrix)[-k:]
  356. if sol_type == 'dict':
  357. return dict(zip(self.lam_vec, sol_list))
  358. elif sol_type == 'Matrix':
  359. return Matrix(sol_list)
  360. else:
  361. raise ValueError("Unknown sol_type {:}.".format(sol_type))
  362. def rhs(self, inv_method=None, **kwargs):
  363. """Returns equations that can be solved numerically.
  364. Parameters
  365. ==========
  366. inv_method : str
  367. The specific sympy inverse matrix calculation method to use. For a
  368. list of valid methods, see
  369. :meth:`~sympy.matrices.matrices.MatrixBase.inv`
  370. """
  371. if inv_method is None:
  372. self._rhs = self.mass_matrix_full.LUsolve(self.forcing_full)
  373. else:
  374. self._rhs = (self.mass_matrix_full.inv(inv_method,
  375. try_block_diag=True) * self.forcing_full)
  376. return self._rhs
  377. @property
  378. def q(self):
  379. return self._q
  380. @property
  381. def u(self):
  382. return self._qdots
  383. @property
  384. def bodies(self):
  385. return self._bodies
  386. @property
  387. def forcelist(self):
  388. return self._forcelist
  389. @property
  390. def loads(self):
  391. return self._forcelist