linearize.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  1. __all__ = ['Linearizer']
  2. from sympy import Matrix, eye, zeros
  3. from sympy.core.symbol import Dummy
  4. from sympy.utilities.iterables import flatten
  5. from sympy.physics.vector import dynamicsymbols
  6. from sympy.physics.mechanics.functions import msubs, _parse_linear_solver
  7. from collections import namedtuple
  8. from collections.abc import Iterable
  9. class Linearizer:
  10. """This object holds the general model form for a dynamic system. This
  11. model is used for computing the linearized form of the system, while
  12. properly dealing with constraints leading to dependent coordinates and
  13. speeds. The notation and method is described in [1]_.
  14. Attributes
  15. ==========
  16. f_0, f_1, f_2, f_3, f_4, f_c, f_v, f_a : Matrix
  17. Matrices holding the general system form.
  18. q, u, r : Matrix
  19. Matrices holding the generalized coordinates, speeds, and
  20. input vectors.
  21. q_i, u_i : Matrix
  22. Matrices of the independent generalized coordinates and speeds.
  23. q_d, u_d : Matrix
  24. Matrices of the dependent generalized coordinates and speeds.
  25. perm_mat : Matrix
  26. Permutation matrix such that [q_ind, u_ind]^T = perm_mat*[q, u]^T
  27. References
  28. ==========
  29. .. [1] D. L. Peterson, G. Gede, and M. Hubbard, "Symbolic linearization of
  30. equations of motion of constrained multibody systems," Multibody
  31. Syst Dyn, vol. 33, no. 2, pp. 143-161, Feb. 2015, doi:
  32. 10.1007/s11044-014-9436-5.
  33. """
  34. def __init__(self, f_0, f_1, f_2, f_3, f_4, f_c, f_v, f_a, q, u, q_i=None,
  35. q_d=None, u_i=None, u_d=None, r=None, lams=None,
  36. linear_solver='LU'):
  37. """
  38. Parameters
  39. ==========
  40. f_0, f_1, f_2, f_3, f_4, f_c, f_v, f_a : array_like
  41. System of equations holding the general system form.
  42. Supply empty array or Matrix if the parameter
  43. does not exist.
  44. q : array_like
  45. The generalized coordinates.
  46. u : array_like
  47. The generalized speeds
  48. q_i, u_i : array_like, optional
  49. The independent generalized coordinates and speeds.
  50. q_d, u_d : array_like, optional
  51. The dependent generalized coordinates and speeds.
  52. r : array_like, optional
  53. The input variables.
  54. lams : array_like, optional
  55. The lagrange multipliers
  56. linear_solver : str, callable
  57. Method used to solve the several symbolic linear systems of the
  58. form ``A*x=b`` in the linearization process. If a string is
  59. supplied, it should be a valid method that can be used with the
  60. :meth:`sympy.matrices.matrixbase.MatrixBase.solve`. If a callable is
  61. supplied, it should have the format ``x = f(A, b)``, where it
  62. solves the equations and returns the solution. The default is
  63. ``'LU'`` which corresponds to SymPy's ``A.LUsolve(b)``.
  64. ``LUsolve()`` is fast to compute but will often result in
  65. divide-by-zero and thus ``nan`` results.
  66. """
  67. self.linear_solver = _parse_linear_solver(linear_solver)
  68. # Generalized equation form
  69. self.f_0 = Matrix(f_0)
  70. self.f_1 = Matrix(f_1)
  71. self.f_2 = Matrix(f_2)
  72. self.f_3 = Matrix(f_3)
  73. self.f_4 = Matrix(f_4)
  74. self.f_c = Matrix(f_c)
  75. self.f_v = Matrix(f_v)
  76. self.f_a = Matrix(f_a)
  77. # Generalized equation variables
  78. self.q = Matrix(q)
  79. self.u = Matrix(u)
  80. none_handler = lambda x: Matrix(x) if x else Matrix()
  81. self.q_i = none_handler(q_i)
  82. self.q_d = none_handler(q_d)
  83. self.u_i = none_handler(u_i)
  84. self.u_d = none_handler(u_d)
  85. self.r = none_handler(r)
  86. self.lams = none_handler(lams)
  87. # Derivatives of generalized equation variables
  88. self._qd = self.q.diff(dynamicsymbols._t)
  89. self._ud = self.u.diff(dynamicsymbols._t)
  90. # If the user doesn't actually use generalized variables, and the
  91. # qd and u vectors have any intersecting variables, this can cause
  92. # problems. We'll fix this with some hackery, and Dummy variables
  93. dup_vars = set(self._qd).intersection(self.u)
  94. self._qd_dup = Matrix([var if var not in dup_vars else Dummy() for var
  95. in self._qd])
  96. # Derive dimension terms
  97. l = len(self.f_c)
  98. m = len(self.f_v)
  99. n = len(self.q)
  100. o = len(self.u)
  101. s = len(self.r)
  102. k = len(self.lams)
  103. dims = namedtuple('dims', ['l', 'm', 'n', 'o', 's', 'k'])
  104. self._dims = dims(l, m, n, o, s, k)
  105. self._Pq = None
  106. self._Pqi = None
  107. self._Pqd = None
  108. self._Pu = None
  109. self._Pui = None
  110. self._Pud = None
  111. self._C_0 = None
  112. self._C_1 = None
  113. self._C_2 = None
  114. self.perm_mat = None
  115. self._setup_done = False
  116. def _setup(self):
  117. # Calculations here only need to be run once. They are moved out of
  118. # the __init__ method to increase the speed of Linearizer creation.
  119. self._form_permutation_matrices()
  120. self._form_block_matrices()
  121. self._form_coefficient_matrices()
  122. self._setup_done = True
  123. def _form_permutation_matrices(self):
  124. """Form the permutation matrices Pq and Pu."""
  125. # Extract dimension variables
  126. l, m, n, o, s, k = self._dims
  127. # Compute permutation matrices
  128. if n != 0:
  129. self._Pq = permutation_matrix(self.q, Matrix([self.q_i, self.q_d]))
  130. if l > 0:
  131. self._Pqi = self._Pq[:, :-l]
  132. self._Pqd = self._Pq[:, -l:]
  133. else:
  134. self._Pqi = self._Pq
  135. self._Pqd = Matrix()
  136. if o != 0:
  137. self._Pu = permutation_matrix(self.u, Matrix([self.u_i, self.u_d]))
  138. if m > 0:
  139. self._Pui = self._Pu[:, :-m]
  140. self._Pud = self._Pu[:, -m:]
  141. else:
  142. self._Pui = self._Pu
  143. self._Pud = Matrix()
  144. # Compute combination permutation matrix for computing A and B
  145. P_col1 = Matrix([self._Pqi, zeros(o + k, n - l)])
  146. P_col2 = Matrix([zeros(n, o - m), self._Pui, zeros(k, o - m)])
  147. if P_col1:
  148. if P_col2:
  149. self.perm_mat = P_col1.row_join(P_col2)
  150. else:
  151. self.perm_mat = P_col1
  152. else:
  153. self.perm_mat = P_col2
  154. def _form_coefficient_matrices(self):
  155. """Form the coefficient matrices C_0, C_1, and C_2."""
  156. # Extract dimension variables
  157. l, m, n, o, s, k = self._dims
  158. # Build up the coefficient matrices C_0, C_1, and C_2
  159. # If there are configuration constraints (l > 0), form C_0 as normal.
  160. # If not, C_0 is I_(nxn). Note that this works even if n=0
  161. if l > 0:
  162. f_c_jac_q = self.f_c.jacobian(self.q)
  163. self._C_0 = (eye(n) - self._Pqd *
  164. self.linear_solver(f_c_jac_q*self._Pqd,
  165. f_c_jac_q))*self._Pqi
  166. else:
  167. self._C_0 = eye(n)
  168. # If there are motion constraints (m > 0), form C_1 and C_2 as normal.
  169. # If not, C_1 is 0, and C_2 is I_(oxo). Note that this works even if
  170. # o = 0.
  171. if m > 0:
  172. f_v_jac_u = self.f_v.jacobian(self.u)
  173. temp = f_v_jac_u * self._Pud
  174. if n != 0:
  175. f_v_jac_q = self.f_v.jacobian(self.q)
  176. self._C_1 = -self._Pud * self.linear_solver(temp, f_v_jac_q)
  177. else:
  178. self._C_1 = zeros(o, n)
  179. self._C_2 = (eye(o) - self._Pud *
  180. self.linear_solver(temp, f_v_jac_u))*self._Pui
  181. else:
  182. self._C_1 = zeros(o, n)
  183. self._C_2 = eye(o)
  184. def _form_block_matrices(self):
  185. """Form the block matrices for composing M, A, and B."""
  186. # Extract dimension variables
  187. l, m, n, o, s, k = self._dims
  188. # Block Matrix Definitions. These are only defined if under certain
  189. # conditions. If undefined, an empty matrix is used instead
  190. if n != 0:
  191. self._M_qq = self.f_0.jacobian(self._qd)
  192. self._A_qq = -(self.f_0 + self.f_1).jacobian(self.q)
  193. else:
  194. self._M_qq = Matrix()
  195. self._A_qq = Matrix()
  196. if n != 0 and m != 0:
  197. self._M_uqc = self.f_a.jacobian(self._qd_dup)
  198. self._A_uqc = -self.f_a.jacobian(self.q)
  199. else:
  200. self._M_uqc = Matrix()
  201. self._A_uqc = Matrix()
  202. if n != 0 and o - m + k != 0:
  203. self._M_uqd = self.f_3.jacobian(self._qd_dup)
  204. self._A_uqd = -(self.f_2 + self.f_3 + self.f_4).jacobian(self.q)
  205. else:
  206. self._M_uqd = Matrix()
  207. self._A_uqd = Matrix()
  208. if o != 0 and m != 0:
  209. self._M_uuc = self.f_a.jacobian(self._ud)
  210. self._A_uuc = -self.f_a.jacobian(self.u)
  211. else:
  212. self._M_uuc = Matrix()
  213. self._A_uuc = Matrix()
  214. if o != 0 and o - m + k != 0:
  215. self._M_uud = self.f_2.jacobian(self._ud)
  216. self._A_uud = -(self.f_2 + self.f_3).jacobian(self.u)
  217. else:
  218. self._M_uud = Matrix()
  219. self._A_uud = Matrix()
  220. if o != 0 and n != 0:
  221. self._A_qu = -self.f_1.jacobian(self.u)
  222. else:
  223. self._A_qu = Matrix()
  224. if k != 0 and o - m + k != 0:
  225. self._M_uld = self.f_4.jacobian(self.lams)
  226. else:
  227. self._M_uld = Matrix()
  228. if s != 0 and o - m + k != 0:
  229. self._B_u = -self.f_3.jacobian(self.r)
  230. else:
  231. self._B_u = Matrix()
  232. def linearize(self, op_point=None, A_and_B=False, simplify=False):
  233. """Linearize the system about the operating point. Note that
  234. q_op, u_op, qd_op, ud_op must satisfy the equations of motion.
  235. These may be either symbolic or numeric.
  236. Parameters
  237. ==========
  238. op_point : dict or iterable of dicts, optional
  239. Dictionary or iterable of dictionaries containing the operating
  240. point conditions for all or a subset of the generalized
  241. coordinates, generalized speeds, and time derivatives of the
  242. generalized speeds. These will be substituted into the linearized
  243. system before the linearization is complete. Leave set to ``None``
  244. if you want the operating point to be an arbitrary set of symbols.
  245. Note that any reduction in symbols (whether substituted for numbers
  246. or expressions with a common parameter) will result in faster
  247. runtime.
  248. A_and_B : bool, optional
  249. If A_and_B=False (default), (M, A, B) is returned and of
  250. A_and_B=True, (A, B) is returned. See below.
  251. simplify : bool, optional
  252. Determines if returned values are simplified before return.
  253. For large expressions this may be time consuming. Default is False.
  254. Returns
  255. =======
  256. M, A, B : Matrices, ``A_and_B=False``
  257. Matrices from the implicit form:
  258. ``[M]*[q', u']^T = [A]*[q_ind, u_ind]^T + [B]*r``
  259. A, B : Matrices, ``A_and_B=True``
  260. Matrices from the explicit form:
  261. ``[q_ind', u_ind']^T = [A]*[q_ind, u_ind]^T + [B]*r``
  262. Notes
  263. =====
  264. Note that the process of solving with A_and_B=True is computationally
  265. intensive if there are many symbolic parameters. For this reason, it
  266. may be more desirable to use the default A_and_B=False, returning M, A,
  267. and B. More values may then be substituted in to these matrices later
  268. on. The state space form can then be found as A = P.T*M.LUsolve(A), B =
  269. P.T*M.LUsolve(B), where P = Linearizer.perm_mat.
  270. """
  271. # Run the setup if needed:
  272. if not self._setup_done:
  273. self._setup()
  274. # Compose dict of operating conditions
  275. if isinstance(op_point, dict):
  276. op_point_dict = op_point
  277. elif isinstance(op_point, Iterable):
  278. op_point_dict = {}
  279. for op in op_point:
  280. op_point_dict.update(op)
  281. else:
  282. op_point_dict = {}
  283. # Extract dimension variables
  284. l, m, n, o, s, k = self._dims
  285. # Rename terms to shorten expressions
  286. M_qq = self._M_qq
  287. M_uqc = self._M_uqc
  288. M_uqd = self._M_uqd
  289. M_uuc = self._M_uuc
  290. M_uud = self._M_uud
  291. M_uld = self._M_uld
  292. A_qq = self._A_qq
  293. A_uqc = self._A_uqc
  294. A_uqd = self._A_uqd
  295. A_qu = self._A_qu
  296. A_uuc = self._A_uuc
  297. A_uud = self._A_uud
  298. B_u = self._B_u
  299. C_0 = self._C_0
  300. C_1 = self._C_1
  301. C_2 = self._C_2
  302. # Build up Mass Matrix
  303. # |M_qq 0_nxo 0_nxk|
  304. # M = |M_uqc M_uuc 0_mxk|
  305. # |M_uqd M_uud M_uld|
  306. if o != 0:
  307. col2 = Matrix([zeros(n, o), M_uuc, M_uud])
  308. if k != 0:
  309. col3 = Matrix([zeros(n + m, k), M_uld])
  310. if n != 0:
  311. col1 = Matrix([M_qq, M_uqc, M_uqd])
  312. if o != 0 and k != 0:
  313. M = col1.row_join(col2).row_join(col3)
  314. elif o != 0:
  315. M = col1.row_join(col2)
  316. else:
  317. M = col1
  318. elif k != 0:
  319. M = col2.row_join(col3)
  320. else:
  321. M = col2
  322. M_eq = msubs(M, op_point_dict)
  323. # Build up state coefficient matrix A
  324. # |(A_qq + A_qu*C_1)*C_0 A_qu*C_2|
  325. # A = |(A_uqc + A_uuc*C_1)*C_0 A_uuc*C_2|
  326. # |(A_uqd + A_uud*C_1)*C_0 A_uud*C_2|
  327. # Col 1 is only defined if n != 0
  328. if n != 0:
  329. r1c1 = A_qq
  330. if o != 0:
  331. r1c1 += (A_qu * C_1)
  332. r1c1 = r1c1 * C_0
  333. if m != 0:
  334. r2c1 = A_uqc
  335. if o != 0:
  336. r2c1 += (A_uuc * C_1)
  337. r2c1 = r2c1 * C_0
  338. else:
  339. r2c1 = Matrix()
  340. if o - m + k != 0:
  341. r3c1 = A_uqd
  342. if o != 0:
  343. r3c1 += (A_uud * C_1)
  344. r3c1 = r3c1 * C_0
  345. else:
  346. r3c1 = Matrix()
  347. col1 = Matrix([r1c1, r2c1, r3c1])
  348. else:
  349. col1 = Matrix()
  350. # Col 2 is only defined if o != 0
  351. if o != 0:
  352. if n != 0:
  353. r1c2 = A_qu * C_2
  354. else:
  355. r1c2 = Matrix()
  356. if m != 0:
  357. r2c2 = A_uuc * C_2
  358. else:
  359. r2c2 = Matrix()
  360. if o - m + k != 0:
  361. r3c2 = A_uud * C_2
  362. else:
  363. r3c2 = Matrix()
  364. col2 = Matrix([r1c2, r2c2, r3c2])
  365. else:
  366. col2 = Matrix()
  367. if col1:
  368. if col2:
  369. Amat = col1.row_join(col2)
  370. else:
  371. Amat = col1
  372. else:
  373. Amat = col2
  374. Amat_eq = msubs(Amat, op_point_dict)
  375. # Build up the B matrix if there are forcing variables
  376. # |0_(n + m)xs|
  377. # B = |B_u |
  378. if s != 0 and o - m + k != 0:
  379. Bmat = zeros(n + m, s).col_join(B_u)
  380. Bmat_eq = msubs(Bmat, op_point_dict)
  381. else:
  382. Bmat_eq = Matrix()
  383. # kwarg A_and_B indicates to return A, B for forming the equation
  384. # dx = [A]x + [B]r, where x = [q_indnd, u_indnd]^T,
  385. if A_and_B:
  386. A_cont = self.perm_mat.T * self.linear_solver(M_eq, Amat_eq)
  387. if Bmat_eq:
  388. B_cont = self.perm_mat.T * self.linear_solver(M_eq, Bmat_eq)
  389. else:
  390. # Bmat = Matrix([]), so no need to sub
  391. B_cont = Bmat_eq
  392. if simplify:
  393. A_cont.simplify()
  394. B_cont.simplify()
  395. return A_cont, B_cont
  396. # Otherwise return M, A, B for forming the equation
  397. # [M]dx = [A]x + [B]r, where x = [q, u]^T
  398. else:
  399. if simplify:
  400. M_eq.simplify()
  401. Amat_eq.simplify()
  402. Bmat_eq.simplify()
  403. return M_eq, Amat_eq, Bmat_eq
  404. def permutation_matrix(orig_vec, per_vec):
  405. """Compute the permutation matrix to change order of
  406. orig_vec into order of per_vec.
  407. Parameters
  408. ==========
  409. orig_vec : array_like
  410. Symbols in original ordering.
  411. per_vec : array_like
  412. Symbols in new ordering.
  413. Returns
  414. =======
  415. p_matrix : Matrix
  416. Permutation matrix such that orig_vec == (p_matrix * per_vec).
  417. """
  418. if not isinstance(orig_vec, (list, tuple)):
  419. orig_vec = flatten(orig_vec)
  420. if not isinstance(per_vec, (list, tuple)):
  421. per_vec = flatten(per_vec)
  422. if set(orig_vec) != set(per_vec):
  423. raise ValueError("orig_vec and per_vec must be the same length, "
  424. "and contain the same symbols.")
  425. ind_list = [orig_vec.index(i) for i in per_vec]
  426. p_matrix = zeros(len(orig_vec))
  427. for i, j in enumerate(ind_list):
  428. p_matrix[i, j] = 1
  429. return p_matrix