Beispiel #1
0
    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
Beispiel #3
0
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
Beispiel #5
0
    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))
Beispiel #6
0
    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)
Beispiel #7
0
    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)
Beispiel #8
0
    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)
Beispiel #9
0
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
Beispiel #10
0
    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)
Beispiel #11
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
    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
Beispiel #14
0
    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