lagrange.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512
  1. from sympy 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 = dict.fromkeys(self._qdoubledots, 0)
  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).dot(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. linear_solver='LU'):
  239. """Returns an instance of the Linearizer class, initiated from the data
  240. in the LagrangesMethod class. This may be more desirable than using the
  241. linearize class method, as the Linearizer object will allow more
  242. efficient recalculation (i.e. about varying operating points).
  243. Parameters
  244. ==========
  245. q_ind, qd_ind : array_like, optional
  246. The independent generalized coordinates and speeds.
  247. q_dep, qd_dep : array_like, optional
  248. The dependent generalized coordinates and speeds.
  249. linear_solver : str, callable
  250. Method used to solve the several symbolic linear systems of the
  251. form ``A*x=b`` in the linearization process. If a string is
  252. supplied, it should be a valid method that can be used with the
  253. :meth:`sympy.matrices.matrixbase.MatrixBase.solve`. If a callable is
  254. supplied, it should have the format ``x = f(A, b)``, where it
  255. solves the equations and returns the solution. The default is
  256. ``'LU'`` which corresponds to SymPy's ``A.LUsolve(b)``.
  257. ``LUsolve()`` is fast to compute but will often result in
  258. divide-by-zero and thus ``nan`` results.
  259. Returns
  260. =======
  261. Linearizer
  262. An instantiated
  263. :class:`sympy.physics.mechanics.linearize.Linearizer`.
  264. """
  265. # Compose vectors
  266. t = dynamicsymbols._t
  267. q = self.q
  268. u = self._qdots
  269. ud = u.diff(t)
  270. # Get vector of lagrange multipliers
  271. lams = self.lam_vec
  272. mat_build = lambda x: Matrix(x) if x else Matrix()
  273. q_i = mat_build(q_ind)
  274. q_d = mat_build(q_dep)
  275. u_i = mat_build(qd_ind)
  276. u_d = mat_build(qd_dep)
  277. # Compose general form equations
  278. f_c = self._hol_coneqs
  279. f_v = self.coneqs
  280. f_a = f_v.diff(t)
  281. f_0 = u
  282. f_1 = -u
  283. f_2 = self._term1
  284. f_3 = -(self._term2 + self._term4)
  285. f_4 = -self._term3
  286. # Check that there are an appropriate number of independent and
  287. # dependent coordinates
  288. if len(q_d) != len(f_c) or len(u_d) != len(f_v):
  289. raise ValueError(("Must supply {:} dependent coordinates, and " +
  290. "{:} dependent speeds").format(len(f_c), len(f_v)))
  291. if set(Matrix([q_i, q_d])) != set(q):
  292. raise ValueError("Must partition q into q_ind and q_dep, with " +
  293. "no extra or missing symbols.")
  294. if set(Matrix([u_i, u_d])) != set(u):
  295. raise ValueError("Must partition qd into qd_ind and qd_dep, " +
  296. "with no extra or missing symbols.")
  297. # Find all other dynamic symbols, forming the forcing vector r.
  298. # Sort r to make it canonical.
  299. insyms = set(Matrix([q, u, ud, lams]))
  300. r = list(find_dynamicsymbols(f_3, insyms))
  301. r.sort(key=default_sort_key)
  302. # Check for any derivatives of variables in r that are also found in r.
  303. for i in r:
  304. if diff(i, dynamicsymbols._t) in r:
  305. raise ValueError('Cannot have derivatives of specified \
  306. quantities when linearizing forcing terms.')
  307. return Linearizer(f_0, f_1, f_2, f_3, f_4, f_c, f_v, f_a, q, u, q_i,
  308. q_d, u_i, u_d, r, lams, linear_solver=linear_solver)
  309. def linearize(self, q_ind=None, qd_ind=None, q_dep=None, qd_dep=None,
  310. linear_solver='LU', **kwargs):
  311. """Linearize the equations of motion about a symbolic operating point.
  312. Parameters
  313. ==========
  314. linear_solver : str, callable
  315. Method used to solve the several symbolic linear systems of the
  316. form ``A*x=b`` in the linearization process. If a string is
  317. supplied, it should be a valid method that can be used with the
  318. :meth:`sympy.matrices.matrixbase.MatrixBase.solve`. If a callable is
  319. supplied, it should have the format ``x = f(A, b)``, where it
  320. solves the equations and returns the solution. The default is
  321. ``'LU'`` which corresponds to SymPy's ``A.LUsolve(b)``.
  322. ``LUsolve()`` is fast to compute but will often result in
  323. divide-by-zero and thus ``nan`` results.
  324. **kwargs
  325. Extra keyword arguments are passed to
  326. :meth:`sympy.physics.mechanics.linearize.Linearizer.linearize`.
  327. Explanation
  328. ===========
  329. If kwarg A_and_B is False (default), returns M, A, B, r for the
  330. linearized form, M*[q', u']^T = A*[q_ind, u_ind]^T + B*r.
  331. If kwarg A_and_B is True, returns A, B, r for the linearized form
  332. dx = A*x + B*r, where x = [q_ind, u_ind]^T. Note that this is
  333. computationally intensive if there are many symbolic parameters. For
  334. this reason, it may be more desirable to use the default A_and_B=False,
  335. returning M, A, and B. Values may then be substituted in to these
  336. matrices, and the state space form found as
  337. A = P.T*M.inv()*A, B = P.T*M.inv()*B, where P = Linearizer.perm_mat.
  338. In both cases, r is found as all dynamicsymbols in the equations of
  339. motion that are not part of q, u, q', or u'. They are sorted in
  340. canonical form.
  341. The operating points may be also entered using the ``op_point`` kwarg.
  342. This takes a dictionary of {symbol: value}, or a an iterable of such
  343. dictionaries. The values may be numeric or symbolic. The more values
  344. you can specify beforehand, the faster this computation will run.
  345. For more documentation, please see the ``Linearizer`` class."""
  346. linearizer = self.to_linearizer(q_ind, qd_ind, q_dep, qd_dep,
  347. linear_solver=linear_solver)
  348. result = linearizer.linearize(**kwargs)
  349. return result + (linearizer.r,)
  350. def solve_multipliers(self, op_point=None, sol_type='dict'):
  351. """Solves for the values of the lagrange multipliers symbolically at
  352. the specified operating point.
  353. Parameters
  354. ==========
  355. op_point : dict or iterable of dicts, optional
  356. Point at which to solve at. The operating point is specified as
  357. a dictionary or iterable of dictionaries of {symbol: value}. The
  358. value may be numeric or symbolic itself.
  359. sol_type : str, optional
  360. Solution return type. Valid options are:
  361. - 'dict': A dict of {symbol : value} (default)
  362. - 'Matrix': An ordered column matrix of the solution
  363. """
  364. # Determine number of multipliers
  365. k = len(self.lam_vec)
  366. if k == 0:
  367. raise ValueError("System has no lagrange multipliers to solve for.")
  368. # Compose dict of operating conditions
  369. if isinstance(op_point, dict):
  370. op_point_dict = op_point
  371. elif iterable(op_point):
  372. op_point_dict = {}
  373. for op in op_point:
  374. op_point_dict.update(op)
  375. elif op_point is None:
  376. op_point_dict = {}
  377. else:
  378. raise TypeError("op_point must be either a dictionary or an "
  379. "iterable of dictionaries.")
  380. # Compose the system to be solved
  381. mass_matrix = self.mass_matrix.col_join(-self.lam_coeffs.row_join(
  382. zeros(k, k)))
  383. force_matrix = self.forcing.col_join(self._f_cd)
  384. # Sub in the operating point
  385. mass_matrix = msubs(mass_matrix, op_point_dict)
  386. force_matrix = msubs(force_matrix, op_point_dict)
  387. # Solve for the multipliers
  388. sol_list = mass_matrix.LUsolve(-force_matrix)[-k:]
  389. if sol_type == 'dict':
  390. return dict(zip(self.lam_vec, sol_list))
  391. elif sol_type == 'Matrix':
  392. return Matrix(sol_list)
  393. else:
  394. raise ValueError("Unknown sol_type {:}.".format(sol_type))
  395. def rhs(self, inv_method=None, **kwargs):
  396. """Returns equations that can be solved numerically.
  397. Parameters
  398. ==========
  399. inv_method : str
  400. The specific sympy inverse matrix calculation method to use. For a
  401. list of valid methods, see
  402. :meth:`~sympy.matrices.matrixbase.MatrixBase.inv`
  403. """
  404. if inv_method is None:
  405. self._rhs = self.mass_matrix_full.LUsolve(self.forcing_full)
  406. else:
  407. self._rhs = (self.mass_matrix_full.inv(inv_method,
  408. try_block_diag=True) * self.forcing_full)
  409. return self._rhs
  410. @property
  411. def q(self):
  412. return self._q
  413. @property
  414. def u(self):
  415. return self._qdots
  416. @property
  417. def bodies(self):
  418. return self._bodies
  419. @property
  420. def forcelist(self):
  421. return self._forcelist
  422. @property
  423. def loads(self):
  424. return self._forcelist