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)
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
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)
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
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)
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)
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)),