Skip to content

modal canonical form does not work with repeated eigenvalues #318

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
murrayrm opened this issue Jun 23, 2019 · 3 comments · Fixed by #495
Closed

modal canonical form does not work with repeated eigenvalues #318

murrayrm opened this issue Jun 23, 2019 · 3 comments · Fixed by #495

Comments

@murrayrm
Copy link
Member

The modal_form() function appears to assume that all eigenvalues are distinct. The following example gives strange results:

import control as ct
sys_repeat = ct.StateSpace([[-1, 1], [0, -1]], [[0], [1]], [[1, 0]], [[0]])
sys_modal, T = ct.modal_form(sys_repeat)
print(sys_modal)

which yields

A = [[-1.00000000e+00  0.00000000e+00]
 [ 2.22044605e-16 -1.00000000e+00]]

B = [[4.50359963e+15]
 [4.50359963e+15]]

C = [[-1.  1.]]

D = [[0.]]

The problem is that the transformation T as computed in modal_form is singular:

[[-1.00000000e+00  1.00000000e+00]
 [ 2.22044605e-16  0.00000000e+00]]
@rabraker
Copy link
Contributor

Just a couple thoughts on this.

It looks like matlab's strategy to deal with repeated eigenvalues is to reduce the A matrix to block schur form. The MB03RD routine in slicot can do this, given a matrix in already in real schur form.

So maybe the strategy to solve this would be to first wrap MB03RD over in slycot. We can use scipy.linalg.schur to put A into real schur form before the call to MB03RD.

@repagh
Copy link
Member

repagh commented Aug 2, 2019 via email

@murrayrm murrayrm added this to the 0.9.x milestone Dec 26, 2020
@roryyorke
Copy link
Contributor

Some references:

How important is the ordering of the eigenvalues in the result? If it is important, do we have eigenvalue sorting routines (I did a search for "sort" in the code, couldn't spot anything obvious) ?

With this:

def modal_form(xsys, pmax=None):
    """Convert a system into modal canonical form

    Parameters
    ----------
    xsys : StateSpace object
        System to be transformed, with state `x`
    pmax: float
        An upper bound on individual transformations.  Default eps ** -0.25.

    Returns
    -------
    zsys : StateSpace object
        System in modal canonical form, with state `z`
    T : matrix
        Coordinate transformation: z = T * x

    Notes
    -----
    pmax is passed to slycot.mb03rd.  The pmax value allows for a
    trade-off: larger values give smaller modal blocks, but
    worse-conditioned T.
    """

    if pmax is None:
        pmax = finfo(float64).eps ** -0.5

    aschur, tschur = schur(xsys.A)
    _, tmodal, _, _ = mb03rd(aschur.shape[0], aschur, tschur, pmax=pmax)
    return similarity_transform(xsys, tmodal, inverse=True), tmodal


def similarity_transform(xsys, T, timescale=1, inverse=False):
    """Perform a similarity transformation, with option time rescaling.

    Transform a linear state space system to a new state space representation
    z = T x, where T is an invertible matrix.

    Parameters
    ----------
    xsys : StateSpace object
           System to transform
    T : 2D invertible array
        The matrix `T` defines the new set of coordinates z = T x.
    timescale : float
        If present, also rescale the time unit to tau = timescale * t
    inverse: boolean
        If True (default), transform so z = T x.  If False, transform
        so x = T z.

    Returns
    -------
    zsys : StateSpace object
        System in transformed coordinates, with state 'z'

    """
    # Create a new system, starting with a copy of the old one
    zsys = StateSpace(xsys)

    # Define a function to compute the right inverse (solve x M = y)
    def rsolve(M, y):
        return transpose(solve(transpose(M), transpose(y)))

    # Update the system matrices
    if not inverse:
        zsys.A = rsolve(T, dot(T, zsys.A)) / timescale
        zsys.B = dot(T, zsys.B) / timescale
        zsys.C = rsolve(T, zsys.C)
    else:
        zsys.A = solve(T, zsys.A).dot(T) / timescale
        zsys.B = solve(T, zsys.B) / timescale
        zsys.C = zsys.C.dot(T)

    return zsys

I can find a modal decomposition of 1 / (s+1)**3 / (s**2 + s + 100) ** 2
(triply-repeated eigenvalues at -1, and double repeated complex pole pair).

from control import canonical_form, ss
import numpy as np

from scipy.linalg import schur
from slycot import mb03rd
from control import similarity_transform

import control as ct

s = ct.tf([1,0],[1])
g_tf = 1 / (s+1)**3 * 1 / (s**2 + s + 100)**2

g = ct.ss(g_tf)
gmod, tmod = ct.modal_form(g)

with np.printoptions(precision=3, suppress=True):
    print(gmod.A)

print(f'{np.linalg.cond(tmod) = }')
[[-0.5   13.936 49.989 -1.919  0.     0.    -0.   ]
 [-7.158 -0.5    3.282 -7.172  0.     0.     0.   ]
 [-0.    -0.    -0.5   -1.999  0.    -0.    -0.   ]
 [ 0.     0.    49.901 -0.5    0.    -0.     0.   ]
 [-0.    -0.     0.    -0.    -1.     0.758  0.388]
 [ 0.     0.    -0.     0.    -0.    -1.    -1.398]
 [ 0.     0.    -0.     0.    -0.     0.    -1.   ]]
np.linalg.cond(tmod) = 13.060711659854526

on pmax: cond(A B) <= cond(A) cond(B); the mb03rd docs state that the condition numbers on the order of pmax ** 2, so to get an upper bound of on cond(T) of 1/sqrt(eps), I've set pmax to 1/sqrt(sqrt(eps)).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants