lagrange.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477
  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 (
  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. __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. _validate_coordinates(self.q)
  135. mat_build = lambda x: Matrix(x) if x else Matrix()
  136. hol_coneqs = mat_build(hol_coneqs)
  137. nonhol_coneqs = mat_build(nonhol_coneqs)
  138. self.coneqs = Matrix([hol_coneqs.diff(dynamicsymbols._t),
  139. nonhol_coneqs])
  140. self._hol_coneqs = hol_coneqs
  141. def form_lagranges_equations(self):
  142. """Method to form Lagrange's equations of motion.
  143. Returns a vector of equations of motion using Lagrange's equations of
  144. the second kind.
  145. """
  146. qds = self._qdots
  147. qdd_zero = {i: 0 for i in self._qdoubledots}
  148. n = len(self.q)
  149. # Internally we represent the EOM as four terms:
  150. # EOM = term1 - term2 - term3 - term4 = 0
  151. # First term
  152. self._term1 = self._L.jacobian(qds)
  153. self._term1 = self._term1.diff(dynamicsymbols._t).T
  154. # Second term
  155. self._term2 = self._L.jacobian(self.q).T
  156. # Third term
  157. if self.coneqs:
  158. coneqs = self.coneqs
  159. m = len(coneqs)
  160. # Creating the multipliers
  161. self.lam_vec = Matrix(dynamicsymbols('lam1:' + str(m + 1)))
  162. self.lam_coeffs = -coneqs.jacobian(qds)
  163. self._term3 = self.lam_coeffs.T * self.lam_vec
  164. # Extracting the coeffecients of the qdds from the diff coneqs
  165. diffconeqs = coneqs.diff(dynamicsymbols._t)
  166. self._m_cd = diffconeqs.jacobian(self._qdoubledots)
  167. # The remaining terms i.e. the 'forcing' terms in diff coneqs
  168. self._f_cd = -diffconeqs.subs(qdd_zero)
  169. else:
  170. self._term3 = zeros(n, 1)
  171. # Fourth term
  172. if self.forcelist:
  173. N = self.inertial
  174. self._term4 = zeros(n, 1)
  175. for i, qd in enumerate(qds):
  176. flist = zip(*_f_list_parser(self.forcelist, N))
  177. self._term4[i] = sum(v.diff(qd, N) & f for (v, f) in flist)
  178. else:
  179. self._term4 = zeros(n, 1)
  180. # Form the dynamic mass and forcing matrices
  181. without_lam = self._term1 - self._term2 - self._term4
  182. self._m_d = without_lam.jacobian(self._qdoubledots)
  183. self._f_d = -without_lam.subs(qdd_zero)
  184. # Form the EOM
  185. self.eom = without_lam - self._term3
  186. return self.eom
  187. def _form_eoms(self):
  188. return self.form_lagranges_equations()
  189. @property
  190. def mass_matrix(self):
  191. """Returns the mass matrix, which is augmented by the Lagrange
  192. multipliers, if necessary.
  193. Explanation
  194. ===========
  195. If the system is described by 'n' generalized coordinates and there are
  196. no constraint equations then an n X n matrix is returned.
  197. If there are 'n' generalized coordinates and 'm' constraint equations
  198. have been supplied during initialization then an n X (n+m) matrix is
  199. returned. The (n + m - 1)th and (n + m)th columns contain the
  200. coefficients of the Lagrange multipliers.
  201. """
  202. if self.eom is None:
  203. raise ValueError('Need to compute the equations of motion first')
  204. if self.coneqs:
  205. return (self._m_d).row_join(self.lam_coeffs.T)
  206. else:
  207. return self._m_d
  208. @property
  209. def mass_matrix_full(self):
  210. """Augments the coefficients of qdots to the mass_matrix."""
  211. if self.eom is None:
  212. raise ValueError('Need to compute the equations of motion first')
  213. n = len(self.q)
  214. m = len(self.coneqs)
  215. row1 = eye(n).row_join(zeros(n, n + m))
  216. row2 = zeros(n, n).row_join(self.mass_matrix)
  217. if self.coneqs:
  218. row3 = zeros(m, n).row_join(self._m_cd).row_join(zeros(m, m))
  219. return row1.col_join(row2).col_join(row3)
  220. else:
  221. return row1.col_join(row2)
  222. @property
  223. def forcing(self):
  224. """Returns the forcing vector from 'lagranges_equations' method."""
  225. if self.eom is None:
  226. raise ValueError('Need to compute the equations of motion first')
  227. return self._f_d
  228. @property
  229. def forcing_full(self):
  230. """Augments qdots to the forcing vector above."""
  231. if self.eom is None:
  232. raise ValueError('Need to compute the equations of motion first')
  233. if self.coneqs:
  234. return self._qdots.col_join(self.forcing).col_join(self._f_cd)
  235. else:
  236. return self._qdots.col_join(self.forcing)
  237. def to_linearizer(self, q_ind=None, qd_ind=None, q_dep=None, qd_dep=None):
  238. """Returns an instance of the Linearizer class, initiated from the
  239. data in the LagrangesMethod class. This may be more desirable than using
  240. the linearize class method, as the Linearizer object will allow more
  241. efficient recalculation (i.e. about varying operating points).
  242. Parameters
  243. ==========
  244. q_ind, qd_ind : array_like, optional
  245. The independent generalized coordinates and speeds.
  246. q_dep, qd_dep : array_like, optional
  247. The dependent generalized coordinates and speeds.
  248. """
  249. # Compose vectors
  250. t = dynamicsymbols._t
  251. q = self.q
  252. u = self._qdots
  253. ud = u.diff(t)
  254. # Get vector of lagrange multipliers
  255. lams = self.lam_vec
  256. mat_build = lambda x: Matrix(x) if x else Matrix()
  257. q_i = mat_build(q_ind)
  258. q_d = mat_build(q_dep)
  259. u_i = mat_build(qd_ind)
  260. u_d = mat_build(qd_dep)
  261. # Compose general form equations
  262. f_c = self._hol_coneqs
  263. f_v = self.coneqs
  264. f_a = f_v.diff(t)
  265. f_0 = u
  266. f_1 = -u
  267. f_2 = self._term1
  268. f_3 = -(self._term2 + self._term4)
  269. f_4 = -self._term3
  270. # Check that there are an appropriate number of independent and
  271. # dependent coordinates
  272. if len(q_d) != len(f_c) or len(u_d) != len(f_v):
  273. raise ValueError(("Must supply {:} dependent coordinates, and " +
  274. "{:} dependent speeds").format(len(f_c), len(f_v)))
  275. if set(Matrix([q_i, q_d])) != set(q):
  276. raise ValueError("Must partition q into q_ind and q_dep, with " +
  277. "no extra or missing symbols.")
  278. if set(Matrix([u_i, u_d])) != set(u):
  279. raise ValueError("Must partition qd into qd_ind and qd_dep, " +
  280. "with no extra or missing symbols.")
  281. # Find all other dynamic symbols, forming the forcing vector r.
  282. # Sort r to make it canonical.
  283. insyms = set(Matrix([q, u, ud, lams]))
  284. r = list(find_dynamicsymbols(f_3, insyms))
  285. r.sort(key=default_sort_key)
  286. # Check for any derivatives of variables in r that are also found in r.
  287. for i in r:
  288. if diff(i, dynamicsymbols._t) in r:
  289. raise ValueError('Cannot have derivatives of specified \
  290. quantities when linearizing forcing terms.')
  291. return Linearizer(f_0, f_1, f_2, f_3, f_4, f_c, f_v, f_a, q, u, q_i,
  292. q_d, u_i, u_d, r, lams)
  293. def linearize(self, q_ind=None, qd_ind=None, q_dep=None, qd_dep=None,
  294. **kwargs):
  295. """Linearize the equations of motion about a symbolic operating point.
  296. Explanation
  297. ===========
  298. If kwarg A_and_B is False (default), returns M, A, B, r for the
  299. linearized form, M*[q', u']^T = A*[q_ind, u_ind]^T + B*r.
  300. If kwarg A_and_B is True, returns A, B, r for the linearized form
  301. dx = A*x + B*r, where x = [q_ind, u_ind]^T. Note that this is
  302. computationally intensive if there are many symbolic parameters. For
  303. this reason, it may be more desirable to use the default A_and_B=False,
  304. returning M, A, and B. Values may then be substituted in to these
  305. matrices, and the state space form found as
  306. A = P.T*M.inv()*A, B = P.T*M.inv()*B, where P = Linearizer.perm_mat.
  307. In both cases, r is found as all dynamicsymbols in the equations of
  308. motion that are not part of q, u, q', or u'. They are sorted in
  309. canonical form.
  310. The operating points may be also entered using the ``op_point`` kwarg.
  311. This takes a dictionary of {symbol: value}, or a an iterable of such
  312. dictionaries. The values may be numeric or symbolic. The more values
  313. you can specify beforehand, the faster this computation will run.
  314. For more documentation, please see the ``Linearizer`` class."""
  315. linearizer = self.to_linearizer(q_ind, qd_ind, q_dep, qd_dep)
  316. result = linearizer.linearize(**kwargs)
  317. return result + (linearizer.r,)
  318. def solve_multipliers(self, op_point=None, sol_type='dict'):
  319. """Solves for the values of the lagrange multipliers symbolically at
  320. the specified operating point.
  321. Parameters
  322. ==========
  323. op_point : dict or iterable of dicts, optional
  324. Point at which to solve at. The operating point is specified as
  325. a dictionary or iterable of dictionaries of {symbol: value}. The
  326. value may be numeric or symbolic itself.
  327. sol_type : str, optional
  328. Solution return type. Valid options are:
  329. - 'dict': A dict of {symbol : value} (default)
  330. - 'Matrix': An ordered column matrix of the solution
  331. """
  332. # Determine number of multipliers
  333. k = len(self.lam_vec)
  334. if k == 0:
  335. raise ValueError("System has no lagrange multipliers to solve for.")
  336. # Compose dict of operating conditions
  337. if isinstance(op_point, dict):
  338. op_point_dict = op_point
  339. elif iterable(op_point):
  340. op_point_dict = {}
  341. for op in op_point:
  342. op_point_dict.update(op)
  343. elif op_point is None:
  344. op_point_dict = {}
  345. else:
  346. raise TypeError("op_point must be either a dictionary or an "
  347. "iterable of dictionaries.")
  348. # Compose the system to be solved
  349. mass_matrix = self.mass_matrix.col_join(-self.lam_coeffs.row_join(
  350. zeros(k, k)))
  351. force_matrix = self.forcing.col_join(self._f_cd)
  352. # Sub in the operating point
  353. mass_matrix = msubs(mass_matrix, op_point_dict)
  354. force_matrix = msubs(force_matrix, op_point_dict)
  355. # Solve for the multipliers
  356. sol_list = mass_matrix.LUsolve(-force_matrix)[-k:]
  357. if sol_type == 'dict':
  358. return dict(zip(self.lam_vec, sol_list))
  359. elif sol_type == 'Matrix':
  360. return Matrix(sol_list)
  361. else:
  362. raise ValueError("Unknown sol_type {:}.".format(sol_type))
  363. def rhs(self, inv_method=None, **kwargs):
  364. """Returns equations that can be solved numerically.
  365. Parameters
  366. ==========
  367. inv_method : str
  368. The specific sympy inverse matrix calculation method to use. For a
  369. list of valid methods, see
  370. :meth:`~sympy.matrices.matrices.MatrixBase.inv`
  371. """
  372. if inv_method is None:
  373. self._rhs = self.mass_matrix_full.LUsolve(self.forcing_full)
  374. else:
  375. self._rhs = (self.mass_matrix_full.inv(inv_method,
  376. try_block_diag=True) * self.forcing_full)
  377. return self._rhs
  378. @property
  379. def q(self):
  380. return self._q
  381. @property
  382. def u(self):
  383. return self._qdots
  384. @property
  385. def bodies(self):
  386. return self._bodies
  387. @property
  388. def forcelist(self):
  389. return self._forcelist
  390. @property
  391. def loads(self):
  392. return self._forcelist