Esempio n. 1
0
    def test_issiso_mimo(self):
        # MIMO transfer function
        sys = tf([[[-1, 41], [1]], [[1, 2], [3, 4]]],
                 [[[1, 10], [1, 20]], [[1, 30], [1, 40]]])
        assert not issiso(sys)
        assert not issiso(sys, strict=True)

        # MIMO state space system
        sys = tf2ss(sys)
        assert not issiso(sys)
        assert not issiso(sys, strict=True)
Esempio n. 2
0
def reachable_form(xsys):
    """Convert a system into reachable canonical form
    Parameters
    ----------
    xsys : StateSpace object
        System to be transformed, with state `x`
    Returns
    -------
    zsys : StateSpace object
        System in reachable canonical form, with state `z`
    T : matrix
        Coordinate transformation: z = T * x
    """
    # Check to make sure we have a SISO system
    if not issiso(xsys):
        raise ControlNotImplemented(
            "Canonical forms for MIMO systems not yet supported")

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

    # Generate the system matrices for the desired canonical form
    zsys.B = zeros(shape(xsys.B))
    zsys.B[0, 0] = 1.0
    zsys.A = zeros(shape(xsys.A))
    Apoly = poly(xsys.A)                # characteristic polynomial
    for i in range(0, xsys.states):
        zsys.A[0, i] = -Apoly[i+1] / Apoly[0]
        if (i+1 < xsys.states):
            zsys.A[i+1, i] = 1.0

    # Compute the reachability matrices for each set of states
    Wrx = ctrb(xsys.A, xsys.B)
    Wrz = ctrb(zsys.A, zsys.B)

    if matrix_rank(Wrx) != xsys.states:
        raise ValueError("System not controllable to working precision.")

    # Transformation from one form to another
    Tzx = solve(Wrx.T, Wrz.T).T  # matrix right division, Tzx = Wrz * inv(Wrx)

    if matrix_rank(Tzx) != xsys.states:
        raise ValueError("Transformation matrix singular to working precision.")

    # Finally, compute the output matrix
    zsys.C = solve(Tzx.T, xsys.C.T).T  # matrix right division, zsys.C = xsys.C * inv(Tzx)

    return zsys, Tzx
Esempio n. 3
0
    def test_issiso(self):
        assert issiso(1)
        with pytest.raises(ValueError):
            issiso(1, strict=True)

        # SISO transfer function
        sys = tf([-1, 42], [1, 10])
        assert issiso(sys)
        assert issiso(sys, strict=True)

        # SISO state space system
        sys = tf2ss(sys)
        assert issiso(sys)
        assert issiso(sys, strict=True)
Esempio n. 4
0
def observable_form(xsys):
    """Convert a system into observable canonical form
    Parameters
    ----------
    xsys : StateSpace object
        System to be transformed, with state `x`
    Returns
    -------
    zsys : StateSpace object
        System in observable canonical form, with state `z`
    T : matrix
        Coordinate transformation: z = T * x
    """
    # Check to make sure we have a SISO system
    if not issiso(xsys):
        raise ControlNotImplemented(
            "Canonical forms for MIMO systems not yet supported")

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

    # Generate the system matrices for the desired canonical form
    zsys.C = zeros(shape(xsys.C))
    zsys.C[0, 0] = 1
    zsys.A = zeros(shape(xsys.A))
    Apoly = poly(xsys.A)                # characteristic polynomial
    for i in range(0, xsys.states):
        zsys.A[i, 0] = -Apoly[i+1] / Apoly[0]
        if (i+1 < xsys.states):
            zsys.A[i, i+1] = 1

    # Compute the observability matrices for each set of states
    Wrx = obsv(xsys.A, xsys.C)
    Wrz = obsv(zsys.A, zsys.C)

    # Transformation from one form to another
    Tzx = inv(Wrz) * Wrx

    # Finally, compute the output matrix
    zsys.B = Tzx * xsys.B

    return zsys, Tzx
Esempio n. 5
0
def evalfr(sys, x):
    """
    Evaluate the transfer function of an LTI system for a single complex 
    number x.
    
    To evaluate at a frequency, enter x = omega*j, where omega is the 
    frequency in radians

    Parameters
    ----------
    sys: StateSpace or TransferFunction
        Linear system
    x: scalar
        Complex number 

    Returns
    -------
    fresp: ndarray

    See Also
    --------
    freqresp
    bode

    Notes
    -----
    This function is a wrapper for StateSpace.evalfr and
    TransferFunction.evalfr.

    Examples
    --------
    >>> sys = ss("1. -2; 3. -4", "5.; 7", "6. 8", "9.")
    >>> evalfr(sys, 1j)
    array([[ 44.8-21.4j]])
    >>> # This is the transfer function matrix evaluated at s = i.

    .. todo:: Add example with MIMO system
    """
    if issiso(sys):
        return sys.horner(x)[0][0]
    return sys.horner(x)
Esempio n. 6
0
def evalfr(sys, x):
    """
    Evaluate the transfer function of an LTI system for a single complex 
    number x.
    
    To evaluate at a frequency, enter x = omega*j, where omega is the 
    frequency in radians

    Parameters
    ----------
    sys: StateSpace or TransferFunction
        Linear system
    x: scalar
        Complex number 

    Returns
    -------
    fresp: ndarray

    See Also
    --------
    freqresp
    bode

    Notes
    -----
    This function is a wrapper for StateSpace.evalfr and
    TransferFunction.evalfr.

    Examples
    --------
    >>> sys = ss("1. -2; 3. -4", "5.; 7", "6. 8", "9.")
    >>> evalfr(sys, 1j)
    array([[ 44.8-21.4j]])
    >>> # This is the transfer function matrix evaluated at s = i.

    .. todo:: Add example with MIMO system
    """
    if issiso(sys):
        return sys.horner(x)[0][0]
    return sys.horner(x)
Esempio n. 7
0
def stability_margins(sysdata, deg=True, returnall=False, epsw=1e-12):
    """Calculate gain, phase and stability margins and associated
    crossover frequencies.
    
    Usage
    -----
    gm, pm, sm, wg, wp, ws = stability_margins(sysdata, deg=True)
    
    Parameters
    ----------
    sysdata: linsys or (mag, phase, omega) sequence 
        sys : linsys
            Linear SISO system
        mag, phase, omega : sequence of array_like
            Input magnitude, phase, and frequencies (rad/sec) sequence from 
            bode frequency response data 
    deg=True: boolean  
        If true, all input and output phases in degrees, else in radians
    returnall=False: boolean
        If true, return all margins found. Note that for frequency data or
        FRD systems, only one margin is found and returned. 
    epsw=1e-12: float
        frequencies below this value are considered static gain, and not
        returned as margin.
       
    Returns
    -------
    gm, pm, sm, wg, wp, ws: float or array_like
        Gain margin gm, phase margin pm, stability margin sm, and 
        associated crossover
        frequencies wg, wp, and ws of SISO open-loop. If more than
        one crossover frequency is detected, returns the lowest corresponding
        margin. 
        When requesting all margins, the return values are array_like, 
        and all margins are returns for linear systems not equal to FRD
        """

    try:
        if isinstance(sysdata, frdata.FRD):
            sys = frdata.FRD(sysdata, smooth=True) 
        elif isinstance(sysdata, xferfcn.TransferFunction):
            sys = sysdata
        elif getattr(sysdata, '__iter__', False) and len(sysdata) == 3:
            mag, phase, omega = sysdata
            sys = frdata.FRD(mag*np.exp((1j/360.)*phase), omega, smooth=True)
        else:
            sys = xferfcn._convertToTransferFunction(sysdata)
    except Exception as e:
        print (e)
        raise ValueError("Margin sysdata must be either a linear system or "
                         "a 3-sequence of mag, phase, omega.")

    # calculate gain of system
    if isinstance(sys, xferfcn.TransferFunction):
        
        # check for siso
        if not issiso(sys):
            raise ValueError("Can only do margins for SISO system")
        
        # real and imaginary part polynomials in omega:
        rnum, inum = _polyimsplit(sys.num[0][0])
        rden, iden = _polyimsplit(sys.den[0][0])

        # test imaginary part of tf == 0, for phase crossover/gain margins
        test_w_180 = np.polyadd(np.polymul(inum, rden), np.polymul(rnum, -iden))
        w_180 = np.roots(test_w_180)
        w_180 = np.real(w_180[(np.imag(w_180) == 0) * (w_180 > epsw)])
        w_180.sort()

        # test magnitude is 1 for gain crossover/phase margins
        test_wc = np.polysub(np.polyadd(_polysqr(rnum), _polysqr(inum)), 
                             np.polyadd(_polysqr(rden), _polysqr(iden)))
        wc = np.roots(test_wc)
        wc = np.real(wc[(np.imag(wc) == 0) * (wc > epsw)])
        wc.sort()

        # stability margin was a bitch to elaborate, relies on magnitude to
        # point -1, then take the derivative. Second derivative needs to be >0
        # to have a minimum
        test_wstabn = np.polyadd(_polysqr(rnum), _polysqr(inum))
        test_wstabd = np.polyadd(_polysqr(np.polyadd(rnum,rden)), 
                                 _polysqr(np.polyadd(inum,iden)))
        test_wstab = np.polysub(
            np.polymul(np.polyder(test_wstabn),test_wstabd), 
            np.polymul(np.polyder(test_wstabd),test_wstabn))

        # find the solutions
        wstab = np.roots(test_wstab)

        # and find the value of the 2nd derivative there, needs to be positive
        wstabplus = np.polyval(np.polyder(test_wstab), wstab)
        wstab = np.real(wstab[(np.imag(wstab) == 0) * (wstab > epsw) *
                              (np.abs(wstabplus) > 0.)])
        wstab.sort()

    else:
        # a bit coarse, have the interpolated frd evaluated again
        def mod(w):
            """to give the function to calculate |G(jw)| = 1"""
            return [np.abs(sys.evalfr(w[0])[0][0]) - 1]

        def arg(w):
            """function to calculate the phase angle at -180 deg"""
            return [np.angle(sys.evalfr(w[0])[0][0]) + np.pi]

        def dstab(w):
            """function to calculate the distance from -1 point"""
            return np.abs(sys.evalfr(w[0])[0][0] + 1.)

        # how to calculate the frequency at which |G(jw)| = 1
        wc = np.array([sp.optimize.fsolve(mod, sys.omega[0])])[0]
        w_180 = np.array([sp.optimize.fsolve(arg, sys.omega[0])])[0]
        wstab = np.real(
            np.array([sp.optimize.fmin(dstab, sys.omega[0], disp=0)])[0])

    # margins, as iterables, converted frdata and xferfcn calculations to
    # vector for this
    PM = np.angle(sys.evalfr(wc)[0][0], deg=True) + 180
    GM = 1/(np.abs(sys.evalfr(w_180)[0][0]))
    SM = np.abs(sys.evalfr(wstab)[0][0]+1)

    if returnall:
        return GM, PM, SM, wc, w_180, wstab
    else:
        return (
            (GM.shape[0] or None) and GM[0], 
            (PM.shape[0] or None) and PM[0], 
            (SM.shape[0] or None) and SM[0], 
            (wc.shape[0] or None) and wc[0],
            (w_180.shape[0] or None) and w_180[0],
            (wstab.shape[0] or None) and wstab[0])
            mag, phase, omega = sysdata
            sys = frdata.FRD(mag * np.exp((1j / 360.) * phase),
                             omega,
                             smooth=True)
        else:
            sys = xferfcn._convertToTransferFunction(sysdata)
    except Exception, e:
        print(e)
        raise ValueError("Margin sysdata must be either a linear system or "
                         "a 3-sequence of mag, phase, omega.")

    # calculate gain of system
    if isinstance(sys, xferfcn.TransferFunction):

        # check for siso
        if not issiso(sys):
            raise ValueError("Can only do margins for SISO system")

        # real and imaginary part polynomials in omega:
        rnum, inum = _polyimsplit(sys.num[0][0])
        rden, iden = _polyimsplit(sys.den[0][0])

        # test imaginary part of tf == 0, for phase crossover/gain margins
        test_w_180 = np.polyadd(np.polymul(inum, rden),
                                np.polymul(rnum, -iden))
        w_180 = np.roots(test_w_180)
        w_180 = np.real(w_180[(np.imag(w_180) == 0) * (w_180 > epsw)])
        w_180.sort()

        # test magnitude is 1 for gain crossover/phase margins
        test_wc = np.polysub(np.polyadd(_polysqr(rnum), _polysqr(inum)),