|
__all__ = ['Linearizer'] |
|
|
|
from sympy import Matrix, eye, zeros |
|
from sympy.core.symbol import Dummy |
|
from sympy.utilities.iterables import flatten |
|
from sympy.physics.vector import dynamicsymbols |
|
from sympy.physics.mechanics.functions import msubs, _parse_linear_solver |
|
|
|
from collections import namedtuple |
|
from collections.abc import Iterable |
|
|
|
|
|
class Linearizer: |
|
"""This object holds the general model form for a dynamic system. This |
|
model is used for computing the linearized form of the system, while |
|
properly dealing with constraints leading to dependent coordinates and |
|
speeds. The notation and method is described in [1]_. |
|
|
|
Attributes |
|
========== |
|
|
|
f_0, f_1, f_2, f_3, f_4, f_c, f_v, f_a : Matrix |
|
Matrices holding the general system form. |
|
q, u, r : Matrix |
|
Matrices holding the generalized coordinates, speeds, and |
|
input vectors. |
|
q_i, u_i : Matrix |
|
Matrices of the independent generalized coordinates and speeds. |
|
q_d, u_d : Matrix |
|
Matrices of the dependent generalized coordinates and speeds. |
|
perm_mat : Matrix |
|
Permutation matrix such that [q_ind, u_ind]^T = perm_mat*[q, u]^T |
|
|
|
References |
|
========== |
|
|
|
.. [1] D. L. Peterson, G. Gede, and M. Hubbard, "Symbolic linearization of |
|
equations of motion of constrained multibody systems," Multibody |
|
Syst Dyn, vol. 33, no. 2, pp. 143-161, Feb. 2015, doi: |
|
10.1007/s11044-014-9436-5. |
|
|
|
""" |
|
|
|
def __init__(self, f_0, f_1, f_2, f_3, f_4, f_c, f_v, f_a, q, u, q_i=None, |
|
q_d=None, u_i=None, u_d=None, r=None, lams=None, |
|
linear_solver='LU'): |
|
""" |
|
Parameters |
|
========== |
|
|
|
f_0, f_1, f_2, f_3, f_4, f_c, f_v, f_a : array_like |
|
System of equations holding the general system form. |
|
Supply empty array or Matrix if the parameter |
|
does not exist. |
|
q : array_like |
|
The generalized coordinates. |
|
u : array_like |
|
The generalized speeds |
|
q_i, u_i : array_like, optional |
|
The independent generalized coordinates and speeds. |
|
q_d, u_d : array_like, optional |
|
The dependent generalized coordinates and speeds. |
|
r : array_like, optional |
|
The input variables. |
|
lams : array_like, optional |
|
The lagrange multipliers |
|
linear_solver : str, callable |
|
Method used to solve the several symbolic linear systems of the |
|
form ``A*x=b`` in the linearization process. If a string is |
|
supplied, it should be a valid method that can be used with the |
|
:meth:`sympy.matrices.matrixbase.MatrixBase.solve`. If a callable is |
|
supplied, it should have the format ``x = f(A, b)``, where it |
|
solves the equations and returns the solution. The default is |
|
``'LU'`` which corresponds to SymPy's ``A.LUsolve(b)``. |
|
``LUsolve()`` is fast to compute but will often result in |
|
divide-by-zero and thus ``nan`` results. |
|
|
|
""" |
|
self.linear_solver = _parse_linear_solver(linear_solver) |
|
|
|
|
|
self.f_0 = Matrix(f_0) |
|
self.f_1 = Matrix(f_1) |
|
self.f_2 = Matrix(f_2) |
|
self.f_3 = Matrix(f_3) |
|
self.f_4 = Matrix(f_4) |
|
self.f_c = Matrix(f_c) |
|
self.f_v = Matrix(f_v) |
|
self.f_a = Matrix(f_a) |
|
|
|
|
|
self.q = Matrix(q) |
|
self.u = Matrix(u) |
|
none_handler = lambda x: Matrix(x) if x else Matrix() |
|
self.q_i = none_handler(q_i) |
|
self.q_d = none_handler(q_d) |
|
self.u_i = none_handler(u_i) |
|
self.u_d = none_handler(u_d) |
|
self.r = none_handler(r) |
|
self.lams = none_handler(lams) |
|
|
|
|
|
self._qd = self.q.diff(dynamicsymbols._t) |
|
self._ud = self.u.diff(dynamicsymbols._t) |
|
|
|
|
|
|
|
dup_vars = set(self._qd).intersection(self.u) |
|
self._qd_dup = Matrix([var if var not in dup_vars else Dummy() for var |
|
in self._qd]) |
|
|
|
|
|
l = len(self.f_c) |
|
m = len(self.f_v) |
|
n = len(self.q) |
|
o = len(self.u) |
|
s = len(self.r) |
|
k = len(self.lams) |
|
dims = namedtuple('dims', ['l', 'm', 'n', 'o', 's', 'k']) |
|
self._dims = dims(l, m, n, o, s, k) |
|
|
|
self._Pq = None |
|
self._Pqi = None |
|
self._Pqd = None |
|
self._Pu = None |
|
self._Pui = None |
|
self._Pud = None |
|
self._C_0 = None |
|
self._C_1 = None |
|
self._C_2 = None |
|
self.perm_mat = None |
|
|
|
self._setup_done = False |
|
|
|
def _setup(self): |
|
|
|
|
|
self._form_permutation_matrices() |
|
self._form_block_matrices() |
|
self._form_coefficient_matrices() |
|
self._setup_done = True |
|
|
|
def _form_permutation_matrices(self): |
|
"""Form the permutation matrices Pq and Pu.""" |
|
|
|
|
|
l, m, n, o, s, k = self._dims |
|
|
|
if n != 0: |
|
self._Pq = permutation_matrix(self.q, Matrix([self.q_i, self.q_d])) |
|
if l > 0: |
|
self._Pqi = self._Pq[:, :-l] |
|
self._Pqd = self._Pq[:, -l:] |
|
else: |
|
self._Pqi = self._Pq |
|
self._Pqd = Matrix() |
|
if o != 0: |
|
self._Pu = permutation_matrix(self.u, Matrix([self.u_i, self.u_d])) |
|
if m > 0: |
|
self._Pui = self._Pu[:, :-m] |
|
self._Pud = self._Pu[:, -m:] |
|
else: |
|
self._Pui = self._Pu |
|
self._Pud = Matrix() |
|
|
|
P_col1 = Matrix([self._Pqi, zeros(o + k, n - l)]) |
|
P_col2 = Matrix([zeros(n, o - m), self._Pui, zeros(k, o - m)]) |
|
if P_col1: |
|
if P_col2: |
|
self.perm_mat = P_col1.row_join(P_col2) |
|
else: |
|
self.perm_mat = P_col1 |
|
else: |
|
self.perm_mat = P_col2 |
|
|
|
def _form_coefficient_matrices(self): |
|
"""Form the coefficient matrices C_0, C_1, and C_2.""" |
|
|
|
|
|
l, m, n, o, s, k = self._dims |
|
|
|
|
|
|
|
if l > 0: |
|
f_c_jac_q = self.f_c.jacobian(self.q) |
|
self._C_0 = (eye(n) - self._Pqd * |
|
self.linear_solver(f_c_jac_q*self._Pqd, |
|
f_c_jac_q))*self._Pqi |
|
else: |
|
self._C_0 = eye(n) |
|
|
|
|
|
|
|
if m > 0: |
|
f_v_jac_u = self.f_v.jacobian(self.u) |
|
temp = f_v_jac_u * self._Pud |
|
if n != 0: |
|
f_v_jac_q = self.f_v.jacobian(self.q) |
|
self._C_1 = -self._Pud * self.linear_solver(temp, f_v_jac_q) |
|
else: |
|
self._C_1 = zeros(o, n) |
|
self._C_2 = (eye(o) - self._Pud * |
|
self.linear_solver(temp, f_v_jac_u))*self._Pui |
|
else: |
|
self._C_1 = zeros(o, n) |
|
self._C_2 = eye(o) |
|
|
|
def _form_block_matrices(self): |
|
"""Form the block matrices for composing M, A, and B.""" |
|
|
|
|
|
l, m, n, o, s, k = self._dims |
|
|
|
|
|
if n != 0: |
|
self._M_qq = self.f_0.jacobian(self._qd) |
|
self._A_qq = -(self.f_0 + self.f_1).jacobian(self.q) |
|
else: |
|
self._M_qq = Matrix() |
|
self._A_qq = Matrix() |
|
if n != 0 and m != 0: |
|
self._M_uqc = self.f_a.jacobian(self._qd_dup) |
|
self._A_uqc = -self.f_a.jacobian(self.q) |
|
else: |
|
self._M_uqc = Matrix() |
|
self._A_uqc = Matrix() |
|
if n != 0 and o - m + k != 0: |
|
self._M_uqd = self.f_3.jacobian(self._qd_dup) |
|
self._A_uqd = -(self.f_2 + self.f_3 + self.f_4).jacobian(self.q) |
|
else: |
|
self._M_uqd = Matrix() |
|
self._A_uqd = Matrix() |
|
if o != 0 and m != 0: |
|
self._M_uuc = self.f_a.jacobian(self._ud) |
|
self._A_uuc = -self.f_a.jacobian(self.u) |
|
else: |
|
self._M_uuc = Matrix() |
|
self._A_uuc = Matrix() |
|
if o != 0 and o - m + k != 0: |
|
self._M_uud = self.f_2.jacobian(self._ud) |
|
self._A_uud = -(self.f_2 + self.f_3).jacobian(self.u) |
|
else: |
|
self._M_uud = Matrix() |
|
self._A_uud = Matrix() |
|
if o != 0 and n != 0: |
|
self._A_qu = -self.f_1.jacobian(self.u) |
|
else: |
|
self._A_qu = Matrix() |
|
if k != 0 and o - m + k != 0: |
|
self._M_uld = self.f_4.jacobian(self.lams) |
|
else: |
|
self._M_uld = Matrix() |
|
if s != 0 and o - m + k != 0: |
|
self._B_u = -self.f_3.jacobian(self.r) |
|
else: |
|
self._B_u = Matrix() |
|
|
|
def linearize(self, op_point=None, A_and_B=False, simplify=False): |
|
"""Linearize the system about the operating point. Note that |
|
q_op, u_op, qd_op, ud_op must satisfy the equations of motion. |
|
These may be either symbolic or numeric. |
|
|
|
Parameters |
|
========== |
|
op_point : dict or iterable of dicts, optional |
|
Dictionary or iterable of dictionaries containing the operating |
|
point conditions for all or a subset of the generalized |
|
coordinates, generalized speeds, and time derivatives of the |
|
generalized speeds. These will be substituted into the linearized |
|
system before the linearization is complete. Leave set to ``None`` |
|
if you want the operating point to be an arbitrary set of symbols. |
|
Note that any reduction in symbols (whether substituted for numbers |
|
or expressions with a common parameter) will result in faster |
|
runtime. |
|
A_and_B : bool, optional |
|
If A_and_B=False (default), (M, A, B) is returned and of |
|
A_and_B=True, (A, B) is returned. See below. |
|
simplify : bool, optional |
|
Determines if returned values are simplified before return. |
|
For large expressions this may be time consuming. Default is False. |
|
|
|
Returns |
|
======= |
|
M, A, B : Matrices, ``A_and_B=False`` |
|
Matrices from the implicit form: |
|
``[M]*[q', u']^T = [A]*[q_ind, u_ind]^T + [B]*r`` |
|
A, B : Matrices, ``A_and_B=True`` |
|
Matrices from the explicit form: |
|
``[q_ind', u_ind']^T = [A]*[q_ind, u_ind]^T + [B]*r`` |
|
|
|
Notes |
|
===== |
|
|
|
Note that the process of solving with A_and_B=True is computationally |
|
intensive if there are many symbolic parameters. For this reason, it |
|
may be more desirable to use the default A_and_B=False, returning M, A, |
|
and B. More values may then be substituted in to these matrices later |
|
on. The state space form can then be found as A = P.T*M.LUsolve(A), B = |
|
P.T*M.LUsolve(B), where P = Linearizer.perm_mat. |
|
|
|
""" |
|
|
|
|
|
if not self._setup_done: |
|
self._setup() |
|
|
|
|
|
if isinstance(op_point, dict): |
|
op_point_dict = op_point |
|
elif isinstance(op_point, Iterable): |
|
op_point_dict = {} |
|
for op in op_point: |
|
op_point_dict.update(op) |
|
else: |
|
op_point_dict = {} |
|
|
|
|
|
l, m, n, o, s, k = self._dims |
|
|
|
|
|
M_qq = self._M_qq |
|
M_uqc = self._M_uqc |
|
M_uqd = self._M_uqd |
|
M_uuc = self._M_uuc |
|
M_uud = self._M_uud |
|
M_uld = self._M_uld |
|
A_qq = self._A_qq |
|
A_uqc = self._A_uqc |
|
A_uqd = self._A_uqd |
|
A_qu = self._A_qu |
|
A_uuc = self._A_uuc |
|
A_uud = self._A_uud |
|
B_u = self._B_u |
|
C_0 = self._C_0 |
|
C_1 = self._C_1 |
|
C_2 = self._C_2 |
|
|
|
|
|
|
|
|
|
|
|
if o != 0: |
|
col2 = Matrix([zeros(n, o), M_uuc, M_uud]) |
|
if k != 0: |
|
col3 = Matrix([zeros(n + m, k), M_uld]) |
|
if n != 0: |
|
col1 = Matrix([M_qq, M_uqc, M_uqd]) |
|
if o != 0 and k != 0: |
|
M = col1.row_join(col2).row_join(col3) |
|
elif o != 0: |
|
M = col1.row_join(col2) |
|
else: |
|
M = col1 |
|
elif k != 0: |
|
M = col2.row_join(col3) |
|
else: |
|
M = col2 |
|
M_eq = msubs(M, op_point_dict) |
|
|
|
|
|
|
|
|
|
|
|
|
|
if n != 0: |
|
r1c1 = A_qq |
|
if o != 0: |
|
r1c1 += (A_qu * C_1) |
|
r1c1 = r1c1 * C_0 |
|
if m != 0: |
|
r2c1 = A_uqc |
|
if o != 0: |
|
r2c1 += (A_uuc * C_1) |
|
r2c1 = r2c1 * C_0 |
|
else: |
|
r2c1 = Matrix() |
|
if o - m + k != 0: |
|
r3c1 = A_uqd |
|
if o != 0: |
|
r3c1 += (A_uud * C_1) |
|
r3c1 = r3c1 * C_0 |
|
else: |
|
r3c1 = Matrix() |
|
col1 = Matrix([r1c1, r2c1, r3c1]) |
|
else: |
|
col1 = Matrix() |
|
|
|
if o != 0: |
|
if n != 0: |
|
r1c2 = A_qu * C_2 |
|
else: |
|
r1c2 = Matrix() |
|
if m != 0: |
|
r2c2 = A_uuc * C_2 |
|
else: |
|
r2c2 = Matrix() |
|
if o - m + k != 0: |
|
r3c2 = A_uud * C_2 |
|
else: |
|
r3c2 = Matrix() |
|
col2 = Matrix([r1c2, r2c2, r3c2]) |
|
else: |
|
col2 = Matrix() |
|
if col1: |
|
if col2: |
|
Amat = col1.row_join(col2) |
|
else: |
|
Amat = col1 |
|
else: |
|
Amat = col2 |
|
Amat_eq = msubs(Amat, op_point_dict) |
|
|
|
|
|
|
|
|
|
if s != 0 and o - m + k != 0: |
|
Bmat = zeros(n + m, s).col_join(B_u) |
|
Bmat_eq = msubs(Bmat, op_point_dict) |
|
else: |
|
Bmat_eq = Matrix() |
|
|
|
|
|
|
|
if A_and_B: |
|
A_cont = self.perm_mat.T * self.linear_solver(M_eq, Amat_eq) |
|
if Bmat_eq: |
|
B_cont = self.perm_mat.T * self.linear_solver(M_eq, Bmat_eq) |
|
else: |
|
|
|
B_cont = Bmat_eq |
|
if simplify: |
|
A_cont.simplify() |
|
B_cont.simplify() |
|
return A_cont, B_cont |
|
|
|
|
|
else: |
|
if simplify: |
|
M_eq.simplify() |
|
Amat_eq.simplify() |
|
Bmat_eq.simplify() |
|
return M_eq, Amat_eq, Bmat_eq |
|
|
|
|
|
def permutation_matrix(orig_vec, per_vec): |
|
"""Compute the permutation matrix to change order of |
|
orig_vec into order of per_vec. |
|
|
|
Parameters |
|
========== |
|
|
|
orig_vec : array_like |
|
Symbols in original ordering. |
|
per_vec : array_like |
|
Symbols in new ordering. |
|
|
|
Returns |
|
======= |
|
|
|
p_matrix : Matrix |
|
Permutation matrix such that orig_vec == (p_matrix * per_vec). |
|
""" |
|
if not isinstance(orig_vec, (list, tuple)): |
|
orig_vec = flatten(orig_vec) |
|
if not isinstance(per_vec, (list, tuple)): |
|
per_vec = flatten(per_vec) |
|
if set(orig_vec) != set(per_vec): |
|
raise ValueError("orig_vec and per_vec must be the same length, " |
|
"and contain the same symbols.") |
|
ind_list = [orig_vec.index(i) for i in per_vec] |
|
p_matrix = zeros(len(orig_vec)) |
|
for i, j in enumerate(ind_list): |
|
p_matrix[i, j] = 1 |
|
return p_matrix |
|
|