def calc_eqlbr(self, parameter_values=None, ttheta_guess=None, etype='one_ep', display=False, **kwargs): """In the simplest case(RT1 and 2) only one of the equilibrium points of a system is used for linearization and other researches. Such a equilibrium point is calculated by minimizing the target function for a certain input variable. In other case(NT) all of the equilibrium points of a system are needed, which can be calculated by using Slover in Sympy to solve the differential equations for certain input values. :param: uu: certain input value defined by user :param: parameter_values: unknown system parameters in list.(Default: all parameters are known) :param: ttheta_guess: initial values for minimizing the target function.(Default: 0) :param: etype: 'one_ep': one equilibrium point, 'all_ep': all of the equilibrium points. :param: display: display all information of the result of the 'fmin' fucntion :return: equilibrium point(s) in list """ if parameter_values is None: parameter_values = [] if kwargs.get('uu') is None: # if the system doesn't have input assert self.tau is None uu = [] all_vars = st.row_stack(self.tt) uu_para = [] else: uu = kwargs.get('uu') all_vars = st.row_stack(self.tt, self.tau) uu_para = list(zip(self.tau, uu)) class Type_equilibrium(Enum): one_ep = auto() all_ep = auto() if etype == Type_equilibrium.one_ep.name: # set all of the derivatives of the system states to zero state_eqns = self.eqns.subz0(self.ttd, self.ttdd) # target function for minimizing state_eqns_func = st.expr_to_func(all_vars, state_eqns.subs(parameter_values)) if ttheta_guess is None: ttheta_guess = st.to_np(self.tt * 0) def target(ttheta): """target function for the certain global input values uu """ all_values = np.r_[ttheta, uu] # combine arrays rhs = state_eqns_func(*all_values) return np.sum(rhs ** 2) self.eqlbr = fmin(target, ttheta_guess, disp=display) elif etype == Type_equilibrium.all_ep.name: state_eqns = self.eqns.subz0(self.ttd, self.ttdd) all_vars_value = uu_para + parameter_values self.eqlbr = sp.solve(state_eqns.subs(all_vars_value), self.tt)
def feedback(xx_ref): omega_matrix = omega_matrix_func(*xx_ref) # iterate row-wise over the matrix feedback_gain = st.to_np( sum([rho_i * w for (rho_i, w) in zip(clcp_coeffs, omega_matrix)])) return feedback_gain
def Rz(phi, to_numpy=False): """ Rotation Matrix in the xy plane """ c = sp.cos(phi) s = sp.sin(phi) M = sp.Matrix([[c, -s], [s, c]]) if to_numpy: return st.to_np(M) else: return M
def feedback_factory(vf_f, vf_g, xx, clcp_coeffs): n = len(xx) assert len(clcp_coeffs) == n + 1 assert len(vf_f) == n assert len(vf_g) == n assert clcp_coeffs[-1] == 1 # prevent datatype problems: clcp_coeffs = st.to_np(clcp_coeffs) # calculate the relevant covector_fields # 1. extended nonlinear controllability matrix Qext = st.nl_cont_matrix(vf_f, vf_g, xx, n_extra_cols=0) QQinv = Qext.inverse_ADJ() w_i = QQinv[-1, :] omega_symb_list = [w_i] t0 = time.time() for i in range(1, n + 1): w_i = st.lie_deriv_covf(w_i, vf_f, xx) omega_symb_list.append(w_i) print(i, t0 - time.time()) # dieser schritt dauert ca. 1 min # ggf. sinnvoll: Konvertierung in c-code # stack all 1-forms together omega_matrix = sp.Matrix(omega_symb_list) IPS() omega_matrix_func = sp2c.convert_to_c(xx, omega_matrix, cfilepath="omega.c") # noinspection PyPep8Naming def feedback(xx_ref): omega_matrix = omega_matrix_func(*xx_ref) # iterate row-wise over the matrix feedback_gain = st.to_np( sum([rho_i * w for (rho_i, w) in zip(clcp_coeffs, omega_matrix)])) return feedback_gain # now return that fabricated function return feedback
def test_create_simfunction2(self): x1, x2, x3, x4 = xx = sp.Matrix(sp.symbols("x1, x2, x3, x4")) u1, u2 = uu = sp.Matrix(sp.symbols("u1, u2")) # inputs p1, p2, p3, p4 = pp = sp.Matrix( sp.symbols("p1, p2, p3, p4")) # parameter t = sp.Symbol('t') A = A0 = sp.randMatrix(len(xx), len(xx), -10, 10, seed=704) B = B0 = sp.randMatrix(len(xx), len(uu), -10, 10, seed=705) v1 = A[0, 0] A[0, 0] = p1 v2 = A[2, -1] A[2, -1] = p2 v3 = B[3, 0] B[3, 0] = p3 v4 = B[2, 1] B[2, 1] = p4 par_vals = lzip(pp, [v1, v2, v3, v4]) f = A * xx G = B fxu = (f + G * uu).subs(par_vals) # some random initial values x0 = st.to_np(sp.randMatrix(len(xx), 1, -10, 10, seed=706)).squeeze() u0 = st.to_np(sp.randMatrix(len(uu), 1, -10, 10, seed=2257)).squeeze() # create the model and the rhs-function mod = st.SimulationModel(f, G, xx, par_vals) rhs_xx_uu = mod.create_simfunction(free_input_args=True) res0_1 = rhs_xx_uu(x0, u0, 0) dres0_1 = st.to_np(fxu.subs(lzip(xx, x0) + lzip(uu, u0))).squeeze() bin_res01 = np.isclose(res0_1, dres0_1) # binary array self.assertTrue(np.all(bin_res01))
def test_sorted_eigenvectors(self): V1 = st.sorted_eigenvector_matrix(self.M1) ev1 = st.sorted_eigenvalues(self.M1) self.assertEqual(len(ev1), V1.shape[1]) for val, vect in lzip(ev1, st.col_split(V1)): res_vect = self.M1 * vect - val * vect res = (res_vect.T * res_vect)[0] self.assertTrue(res < 1e-15) self.assertAlmostEqual((vect.T * vect)[0] - 1, 0) V2 = st.sorted_eigenvector_matrix(self.M1, numpy=True) V3 = st.sorted_eigenvector_matrix(self.M1, numpy=True, increase=True) # quotients should be +-1 res1 = np.abs(st.to_np(V1) / st.to_np(V2)) - np.ones_like(V1) res2 = np.abs(st.to_np(V1) / st.to_np(V3[:, ::-1])) - np.ones_like(V1) self.assertTrue(np.max(np.abs(res1)) < 1e-5) self.assertTrue(np.max(np.abs(res2)) < 1e-5)
def test_siso_place(self): n = 6 A = sp.randMatrix(n, n, seed=1648, min=-10, max=10) b = sp.randMatrix(n, 1, seed=1649, min=-10, max=10) ev = np.sort(np.random.random(n) * 10) f = st.siso_place(A, b, ev) A2 = st.to_np(A + b * f.T) ev2 = np.sort(np.linalg.eigvals(A2)) diff = np.sum(np.abs((ev - ev2) / ev)) self.assertTrue(diff < 1e-6)
def test_siso_place2(self): n = 4 A = sp.randMatrix(n, n, seed=1648, min=-10, max=10) b = sp.randMatrix(n, 1, seed=1649, min=-10, max=10) omega = np.pi * 2 / 2.0 ev = np.sort([1j * omega, -1j * omega, -2, -3]) f = st.siso_place(A, b, ev) A2 = st.to_np(A + b * f.T) ev2 = np.sort(np.linalg.eigvals(A2)) diff = np.sum(np.abs((ev - ev2) / ev)) self.assertTrue(diff < 1e-6)
def calc_eqlbr_rt1(mod, uu, sys_paras, ttheta_guess=None, display=False, debug=False): """ In the simplest case, only one of the equilibrium points of a nonlinear system is used for linearization.Such a equilibrium point is calculated by minimizing the target function for a certain input variable. :param mod: symbolic_model :param uu: system inputs :param sys_paras: system parameters :param ttheta_guess: initial guess of the equilibrium points :param display: Set to True to print convergence messages. :param debug: output control for debugging in unittest(False:normal output,True: output local variables and normal output) :return: Parameter that minimizes function """ # set all of the derivatives of the system states to zero stat_eqns = mod.eqns.subz0(mod.ttd, mod.ttdd) all_vars = st.row_stack(mod.tt, mod.uu) # target function for minimizing mod.stat_eqns_func = st.expr_to_func(all_vars, stat_eqns.subs(sys_paras)) if ttheta_guess is None: ttheta_guess = st.to_np(mod.tt * 0) def target(ttheta): """target function for the certain global input values uu """ all_vars = np.r_[ttheta, uu] # combine arrays rhs = mod.stat_eqns_func(*all_vars) return np.sum(rhs**2) res = fmin(target, ttheta_guess, disp=display) if debug: C = ipydex.Container(fetch_locals=True) return C, res return res
def test_num_trajectory_compatibility_test(self): x1, x2, x3, x4 = xx = sp.Matrix(sp.symbols("x1, x2, x3, x4")) u1, u2 = uu = sp.Matrix(sp.symbols("u1, u2")) # inputs t = sp.Symbol('t') # we want to create a random but stable matrix np.random.seed(2805) diag = np.diag(np.random.random(len(xx)) * -10) T = sp.randMatrix(len(xx), len(xx), -10, 10, seed=704) Tinv = T.inv() A = Tinv * diag * T B = B0 = sp.randMatrix(len(xx), len(uu), -10, 10, seed=705) x0 = st.to_np(sp.randMatrix(len(xx), 1, -10, 10, seed=706)).squeeze() tt = np.linspace(0, 5, 2000) des_input = st.piece_wise((2 - t, t <= 1), (t, t < 2), (2 * t - 2, t < 3), (4, True)) des_input_func_vec = st.expr_to_func(t, sp.Matrix([des_input, des_input])) mod2 = st.SimulationModel(A * xx, B, xx) rhs3 = mod2.create_simfunction(input_function=des_input_func_vec) XX = sc.integrate.odeint(rhs3, x0, tt) UU = des_input_func_vec(tt) res1 = mod2.num_trajectory_compatibility_test(tt, XX, UU) self.assertTrue(res1) # slightly different input signal -> other results res2 = mod2.num_trajectory_compatibility_test(tt, XX, UU * 1.1) self.assertFalse(res2)
def unimod_inv(M, s=None, t=None, time_dep_symbs=[], simplify_nsm=True, max_deg=None): """ Assumes that M(s) is an unimodular polynomial matrix and calculates its inverse which is again unimodular :param M: Matrix to be inverted :param s: Derivative Symbol :param time_dep_symbs: sequence of time dependent symbols :param max_deg: maximum polynomial degree w.r.t. s of the ansatz :return: Minv """ assert isinstance(M, sp.MatrixBase) assert M.is_square n = M.shape[0] degree_m = nc_degree(M, s) if max_deg is None: # upper bound according to # Levine 2011, On necessary and sufficient conditions for differential flatness, p. 73 max_deg = (n - 1) * degree_m assert int(max_deg) == max_deg assert max_deg >= 0 C = M * 0 free_params = [] for i in range(max_deg + 1): prefix = 'c{0}_'.format(i) c_part = st.symbMatrix(n, n, prefix, commutative=False) C += nc_mul(c_part, s**i) free_params.extend(list(c_part)) P = nc_mul(C, M) - sp.eye(n) P2 = right_shift_all(P, s, t, time_dep_symbs).reshape(n * n, 1) deg_P = nc_degree(P2, s) part_eqns = [] for i in range(deg_P + 1): # omit the highest order (because it behaves like in the commutative case) res = P2.diff(s, i).subs(s, 0) #/sp.factorial(i) part_eqns.append(res) eqns = st.row_stack(*part_eqns) # equations for all degrees of s # now non-commutativity is inferring eqns2, st_c_nc = make_all_symbols_commutative(eqns) free_params_c, st_c_nc_free_params = make_all_symbols_commutative( free_params) # find out which of the equations are (in)homogeneous eqns2_0 = eqns2.subs(st.zip0(free_params_c)) assert eqns2_0.atoms() in ({0, -1}, {-1}, set()) inhom_idcs = st.np.where(st.to_np(eqns2_0) != 0)[0] hom_idcs = st.np.where(st.to_np(eqns2_0) == 0)[0] eqns_hom = sp.Matrix(st.np.array(eqns2)[hom_idcs]) eqns_inh = sp.Matrix(st.np.array(eqns2)[inhom_idcs]) assert len(eqns_inh) == n # find a solution for the homogeneous equations # if this is not possible, M was not unimodular Jh = eqns_hom.jacobian(free_params_c).expand() nsm = st.nullspaceMatrix(Jh, simplify=simplify_nsm, sort_rows=True) na = nsm.shape[1] if na < n: msg = 'Could not determine sufficiently large nullspace. '\ 'Either M is not unimodular or the expressions are to complicated.' # TODO: decide which of the two cases occurs, via substitution of # random numbers and singular value decomposition # (or application of st.generic_rank) raise ValueError(msg) # parameterize the inhomogenous equations with the solution of the homogeneous equations # new free parameters: aa = st.symb_vector('_a1:{0}'.format(na + 1)) nsm_a = nsm * aa eqns_inh2 = eqns_inh.subs(lzip(free_params_c, nsm_a)) # now solve the remaining equations # solve the linear system Jinh = eqns_inh2.jacobian(aa) rhs_inh = -eqns_inh2.subs(st.zip0(aa)) assert rhs_inh == sp.ones(n, 1) sol_vect = Jinh.solve(rhs_inh) sol = lzip(aa, sol_vect) # get the values for all free_params (now they are not free anymore) free_params_sol_c = nsm_a.subs(sol) # replace the commutative symbols with the original non_commutative symbols (of M) free_params_sol = free_params_sol_c.subs(st_c_nc) Minv = C.subs(lzip(free_params, free_params_sol)) return Minv
def testRz(self): Rz1 = mt.Rz(sp.pi * 0.4) Rz2 = mt.Rz(npy.pi * 0.4, to_numpy=True) self.assertTrue(npy.allclose(st.to_np(Rz1), Rz2))
def tv_feedback_factory(ff, gg, xx, uu, clcp_coeffs, use_exisiting_so="smart"): """ :param ff: :param gg: :param xx: :param uu: :param clcp_coeffs: :param use_exisiting_so: :return: """ n = len(ff) assert len(xx) == n clcp_coeffs = st.to_np(clcp_coeffs) assert len(clcp_coeffs) == n + 1 cfilepath = "k_timev.c" # if we reuse the compiled code for the controller, # we can skip the complete caluclation input_data_hash = sp2c.reproducible_fast_hash([ff, gg, xx, uu]) print(input_data_hash) if use_exisiting_so == "smart": try: lib_meta_data = sp2c.get_meta_data(cfilepath, reload_lib=True) except FileNotFoundError as ferr: use_exisiting_so = False else: if lib_meta_data.get("input_data_hash") == input_data_hash: pass use_exisiting_so = True else: use_exisiting_so = False assert use_exisiting_so in (True, False) if use_exisiting_so is True: try: nargs = sp2c.get_meta_data(cfilepath, reload_lib=True)["nargs"] except FileNotFoundError as ferr: print("File not found. Unable to use exisiting shared libray.") # noinspection PyTypeChecker return tv_feedback_factory(ff, gg, xx, uu, clcp_coeffs, use_exisiting_so=False) # now load the existing function sopath = sp2c._get_so_path(cfilepath) L_matrix_func = sp2c.load_func(sopath) nu = nargs - n else: K1 = time_variant_controllability_matrix(ff, gg, xx, uu) kappa = K1.det() # K1_inv = K1.inverse_ADJ() K1_adj = K1.adjugate() lmd = K1_adj[-1, :] diffop = DiffOpTimeVarSys(ff, gg, xx, uu) ll = [ diffop.MA_vect(lmd, order=i, subs_xref=False) for i in range(n + 1) ] # append a vector which contains kappa as first entries and 0s elsewhere kappa_vector = sp.Matrix([kappa] + [0] * (n - 1)).T ll.append(kappa_vector) L_matrix = st.row_stack(*ll) maxorder = max([0] + [symb.difforder for symb in L_matrix.s]) rplm = diffop.get_orig_ref_replm(maxorder) L_matrix_r = L_matrix.subs(rplm) xxuu = list(zip(*rplm))[1] nu = len(xxuu) - len(xx) # additional metadata amd = dict(input_data_hash=input_data_hash, variables=xxuu) L_matrix_func = sp2c.convert_to_c(xxuu, L_matrix_r, cfilepath=cfilepath, use_exisiting_so=False, additional_metadata=amd) # noinspection PyShadowingNames def tv_feedback_gain(xref, uuref=None): if uuref is None: args = list(xref) + [0] * nu else: args = list(xref) + list(uuref) ll_num_ext = L_matrix_func(*args) ll_num = ll_num_ext[:n + 1, :] # extract kappa which we inserted into the L_matrix for convenience kappa = ll_num_ext[n + 1, 0] k = np.dot(clcp_coeffs, ll_num) / kappa return k # ship the information and internal functions to the outside tv_feedback_gain.nu = nu tv_feedback_gain.n = n tv_feedback_gain.L_matrix_func = L_matrix_func # return the fucntion return tv_feedback_gain
def test_create_simfunction(self): x1, x2, x3, x4 = xx = sp.Matrix(sp.symbols("x1, x2, x3, x4")) u1, u2 = uu = sp.Matrix(sp.symbols("u1, u2")) # inputs p1, p2, p3, p4 = pp = sp.Matrix( sp.symbols("p1, p2, p3, p4")) # parameter t = sp.Symbol('t') A = A0 = sp.randMatrix(len(xx), len(xx), -10, 10, seed=704) B = B0 = sp.randMatrix(len(xx), len(uu), -10, 10, seed=705) v1 = A[0, 0] A[0, 0] = p1 v2 = A[2, -1] A[2, -1] = p2 v3 = B[3, 0] B[3, 0] = p3 v4 = B[2, 1] B[2, 1] = p4 par_vals = lzip(pp, [v1, v2, v3, v4]) f = A * xx G = B fxu = (f + G * uu).subs(par_vals) # some random initial values x0 = st.to_np(sp.randMatrix(len(xx), 1, -10, 10, seed=706)).squeeze() # Test handling of unsubstituted parameters mod = st.SimulationModel(f, G, xx, model_parameters=par_vals[1:]) with self.assertRaises(ValueError) as cm: rhs0 = mod.create_simfunction() self.assertTrue("unexpected symbols" in cm.exception.args[0]) # create the model and the rhs-function mod = st.SimulationModel(f, G, xx, par_vals) rhs0 = mod.create_simfunction() self.assertFalse(mod.compiler_called) self.assertFalse(mod.use_sp2c) res0_1 = rhs0(x0, 0) dres0_1 = st.to_np(fxu.subs(lzip(xx, x0) + st.zip0(uu))).squeeze() bin_res01 = np.isclose(res0_1, dres0_1) # binary array self.assertTrue(np.all(bin_res01)) # difference should be [0, 0, ..., 0] self.assertFalse(np.any(rhs0(x0, 0) - rhs0(x0, 3.7))) # simulate tt = np.linspace(0, 0.5, 100) # simulation should be short due to instability res1 = sc.integrate.odeint(rhs0, x0, tt) # create and try sympy_to_c bridge (currently only works on linux # and if sympy_to_c is installed (e.g. with `pip install sympy_to_c`)) # until it is not available for windows we do not want it as a requirement # see also https://stackoverflow.com/a/10572833/333403 try: import sympy_to_c except ImportError: # noinspection PyUnusedLocal sympy_to_c = None sp2c_available = False else: sp2c_available = True if sp2c_available: rhs0_c = mod.create_simfunction(use_sp2c=True) self.assertTrue(mod.compiler_called) res1_c = sc.integrate.odeint(rhs0_c, x0, tt) self.assertTrue(np.all(np.isclose(res1_c, res1))) mod.compiler_called = None rhs0_c = mod.create_simfunction(use_sp2c=True) self.assertTrue(mod.compiler_called is None) # proof calculation # x(t) = x0*exp(A*t) Anum = st.to_np(A.subs(par_vals)) Bnum = st.to_np(G.subs(par_vals)) # noinspection PyUnresolvedReferences xt = [np.dot(sc.linalg.expm(Anum * T), x0) for T in tt] xt = np.array(xt) # test whether numeric results are close within given tolerance bin_res1 = np.isclose(res1, xt, rtol=2e-5) # binary array self.assertTrue(np.all(bin_res1)) # test handling of parameter free models: mod2 = st.SimulationModel(Anum * xx, Bnum, xx) rhs2 = mod2.create_simfunction() res2 = sc.integrate.odeint(rhs2, x0, tt) self.assertTrue(np.allclose(res1, res2)) # test input functions des_input = st.piece_wise((0, t <= 1), (t, t < 2), (0.5, t < 3), (1, True)) des_input_func_scalar = st.expr_to_func(t, des_input) des_input_func_vec = st.expr_to_func(t, sp.Matrix([des_input, des_input])) # noinspection PyUnusedLocal with self.assertRaises(TypeError) as cm: mod2.create_simfunction(input_function=des_input_func_scalar) rhs3 = mod2.create_simfunction(input_function=des_input_func_vec) # noinspection PyUnusedLocal res3_0 = rhs3(x0, 0) rhs4 = mod2.create_simfunction(input_function=des_input_func_vec, time_direction=-1) res4_0 = rhs4(x0, 0) self.assertTrue(np.allclose(res3_0, np.array([119., -18., -36., -51.]))) self.assertTrue(np.allclose(res4_0, -res3_0))
def unimod_inv(M, s=None, t=None, time_dep_symbs=[], simplify_nsm=True, max_deg=None): """ Assumes that M(s) is an unimodular polynomial matrix and calculates its inverse which is again unimodular :param M: Matrix to be inverted :param s: Derivative Symbol :param time_dep_symbs: sequence of time dependent symbols :param max_deg: maximum polynomial degree w.r.t. s of the ansatz :return: Minv """ assert isinstance(M, sp.MatrixBase) assert M.is_square n = M.shape[0] degree_m = nc_degree(M, s) if max_deg is None: # upper bound according to # Levine 2011, On necessary and sufficient conditions for differential flatness, p. 73 max_deg = (n - 1)*degree_m assert int(max_deg) == max_deg assert max_deg >= 0 C = M*0 free_params = [] for i in range(max_deg+1): prefix = 'c{0}_'.format(i) c_part = st.symbMatrix(n, n, prefix, commutative=False) C += nc_mul(c_part, s**i) free_params.extend(list(c_part)) P = nc_mul(C, M) - sp.eye(n) P2 = right_shift_all(P, s, t, time_dep_symbs).reshape(n*n, 1) deg_P = nc_degree(P2, s) part_eqns = [] for i in range(deg_P + 1): # omit the highest order (because it behaves like in the commutative case) res = P2.diff(s, i).subs(s, 0)#/sp.factorial(i) part_eqns.append(res) eqns = st.row_stack(*part_eqns) # equations for all degrees of s # now non-commutativity is inferring eqns2, st_c_nc = make_all_symbols_commutative(eqns) free_params_c, st_c_nc_free_params = make_all_symbols_commutative(free_params) # find out which of the equations are (in)homogeneous eqns2_0 = eqns2.subs(st.zip0(free_params_c)) assert eqns2_0.atoms() in ({0, -1}, {-1}, set()) inhom_idcs = st.np.where(st.to_np(eqns2_0) != 0)[0] hom_idcs = st.np.where(st.to_np(eqns2_0) == 0)[0] eqns_hom = sp.Matrix(st.np.array(eqns2)[hom_idcs]) eqns_inh = sp.Matrix(st.np.array(eqns2)[inhom_idcs]) assert len(eqns_inh) == n # find a solution for the homogeneous equations # if this is not possible, M was not unimodular Jh = eqns_hom.jacobian(free_params_c).expand() nsm = st.nullspaceMatrix(Jh, simplify=simplify_nsm, sort_rows=True) na = nsm.shape[1] if na < n: msg = 'Could not determine sufficiently large nullspace. '\ 'Either M is not unimodular or the expressions are to complicated.' # TODO: decide which of the two cases occurs, via substitution of # random numbers and singular value decomposition # (or application of st.generic_rank) raise ValueError(msg) # parameterize the inhomogenous equations with the solution of the homogeneous equations # new free parameters: aa = st.symb_vector('_a1:{0}'.format(na+1)) nsm_a = nsm*aa eqns_inh2 = eqns_inh.subs(lzip(free_params_c, nsm_a)) # now solve the remaining equations # solve the linear system Jinh = eqns_inh2.jacobian(aa) rhs_inh = -eqns_inh2.subs(st.zip0(aa)) assert rhs_inh == sp.ones(n, 1) sol_vect = Jinh.solve(rhs_inh) sol = lzip(aa, sol_vect) # get the values for all free_params (now they are not free anymore) free_params_sol_c = nsm_a.subs(sol) # replace the commutative symbols with the original non_commutative symbols (of M) free_params_sol = free_params_sol_c.subs(st_c_nc) Minv = C.subs(lzip(free_params, free_params_sol)) return Minv