From: <mur...@us...> - 2013-06-09 14:58:04
|
Revision: 274 http://sourceforge.net/p/python-control/code/274 Author: murrayrm Date: 2013-06-09 14:57:59 +0000 (Sun, 09 Jun 2013) Log Message: ----------- merging changes from Rane van Paassen - see 2013-05-23 ChangeLog entry for details Modified Paths: -------------- trunk/ChangeLog trunk/src/bdalg.py trunk/src/frdata.py trunk/src/lti.py trunk/src/margins.py trunk/src/matlab.py trunk/src/modelsimp.py trunk/src/rlocus.py trunk/src/statesp.py trunk/src/timeresp.py trunk/src/xferfcn.py trunk/tests/convert_test.py trunk/tests/matlab_test.py trunk/tests/slycot_convert_test.py trunk/tests/statesp_test.py trunk/tests/timeresp_test.py trunk/tests/xferfcn_test.py Removed Paths: ------------- branches/add-frd/ branches/mix-matrices-and-sys/ Property Changed: ---------------- trunk/ trunk/src/ctrlutil.py Index: trunk =================================================================== --- trunk 2013-05-31 11:53:29 UTC (rev 273) +++ trunk 2013-06-09 14:57:59 UTC (rev 274) Property changes on: trunk ___________________________________________________________________ Modified: svn:mergeinfo ## -1 +1,2 ## /branches/add-frd:188-219 +/branches/mix-matrices-and-sys:249-273 \ No newline at end of property Modified: trunk/ChangeLog =================================================================== --- trunk/ChangeLog 2013-05-31 11:53:29 UTC (rev 273) +++ trunk/ChangeLog 2013-06-09 14:57:59 UTC (rev 274) @@ -1,3 +1,24 @@ +2013-05-23 Rene van Paassen <ren...@gm...> + + * src/margin.py: re-vamped stability_margins function. Now + uses analytical method for TF and state-space, works on FRD + objects too, and can optionally return all margins found for + the analytical method. + + * src/xferfcn.py and src/frdata.py: Now return array result + for fresp when called with an iterable frequency vector + + * src/xferfcn.py and src/statesp.py: added minreal functions + + * src/bdalg.py: implemented append and connect functions (old + version, with indexes, not with names) + + * src/matlab.py: added connect, append, minreal + + * src/xferfcn.py and src/statesp.py: added/corrected + __rmul__. In most cases a gain matrix (np.matrix) may now be + used in combination with ss or tf systems. + 2012-11-10 Richard Murray <murray@altura.local> * src/canonical.py: new module implementing conversions to selected Modified: trunk/src/bdalg.py =================================================================== --- trunk/src/bdalg.py 2013-05-31 11:53:29 UTC (rev 273) +++ trunk/src/bdalg.py 2013-06-09 14:57:59 UTC (rev 274) @@ -4,10 +4,12 @@ Routines in this module: +append series parallel negate feedback +connect """ @@ -236,3 +238,97 @@ sys2 = tf._convertToTransferFunction(sys2) return sys1.feedback(sys2, sign) + +def append(*sys): + ''' + Group models by appending their inputs and outputs + + Forms an augmented system model, and appends the inputs and + outputs together. The system type will be the type of the first + system given; if you mix state-space systems and gain matrices, + make sure the gain matrices are not first. + + Parameters. + ----------- + sys1, sys2, ... sysn: StateSpace or Transferfunction + LTI systems to combine + + + Returns + ------- + sys: LTI system + Combined LTI system, with input/output vectors consisting of all + input/output vectors appended + + Examples + -------- + >>> sys1 = ss("1. -2; 3. -4", "5.; 7", "6. 8", "9.") + >>> sys2 = ss("-1.", "1.", "1.", "0.") + >>> sys = append(sys1, sys2) + + .. todo:: + also implement for transfer function, zpk, etc. + ''' + s1 = sys[0] + for s in sys[1:]: + s1 = s1.append(s) + return s1 + +def connect(sys, Q, inputv, outputv): + ''' + Index-base interconnection of system + + The system sys is a system typically constructed with append, with + multiple inputs and outputs. The inputs and outputs are connected + according to the interconnection matrix Q, and then the final + inputs and outputs are trimmed according to the inputs and outputs + listed in inputv and outputv. + + Note: to have this work, inputs and outputs start counting at 1!!!! + + Parameters. + ----------- + sys: StateSpace Transferfunction + System to be connected + Q: 2d array + Interconnection matrix. First column gives the input to be connected + second column gives the output to be fed into this input. Negative + values for the second column mean the feedback is negative, 0 means + no connection is made + inputv: 1d array + list of final external inputs + outputv: 1d array + list of final external outputs + + Returns + ------- + sys: LTI system + Connected and trimmed LTI system + + Examples + -------- + >>> sys1 = ss("1. -2; 3. -4", "5.; 7", "6, 8", "9.") + >>> sys2 = ss("-1.", "1.", "1.", "0.") + >>> sys = append(sys1, sys2) + >>> Q = sp.mat([ [ 1, 2], [2, -1] ]) # basically feedback, output 2 in 1 + >>> sysc = connect(sys, Q, [2], [1, 2]) + ''' + # first connect + K = sp.zeros( (sys.inputs, sys.outputs) ) + for r in sp.array(Q).astype(int): + inp = r[0]-1 + for outp in r[1:]: + if outp > 0 and outp <= sys.outputs: + K[inp,outp-1] = 1. + elif outp < 0 and -outp >= -sys.outputs: + K[inp,-outp-1] = -1. + sys = sys.feedback(sp.matrix(K), sign=1) + + # now trim + Ytrim = sp.zeros( (len(outputv), sys.outputs) ) + Utrim = sp.zeros( (sys.inputs, len(inputv)) ) + for i,u in enumerate(inputv): + Utrim[u-1,i] = 1. + for i,y in enumerate(outputv): + Ytrim[i,y-1] = 1. + return sp.matrix(Ytrim)*sys*sp.matrix(Utrim) Index: trunk/src/ctrlutil.py =================================================================== --- trunk/src/ctrlutil.py 2013-05-31 11:53:29 UTC (rev 273) +++ trunk/src/ctrlutil.py 2013-06-09 14:57:59 UTC (rev 274) Property changes on: trunk/src/ctrlutil.py ___________________________________________________________________ Modified: svn:mergeinfo ## -1,2 +1,3 ## /branches/add-frd/src/ctrlutil.py:188-219 +/branches/mix-matrices-and-sys/src/ctrlutil.py:249-273 /trunk/src/ctrlutil.py:140-142 \ No newline at end of property Modified: trunk/src/frdata.py =================================================================== --- trunk/src/frdata.py 2013-05-31 11:53:29 UTC (rev 273) +++ trunk/src/frdata.py 2013-06-09 14:57:59 UTC (rev 274) @@ -107,7 +107,7 @@ def __init__(self, *args, **kwargs): """Construct a transfer function. - The default constructor is FRD(w, d), where w is an iterable of + The default constructor is FRD(d, w), where w is an iterable of frequency points, and d is the matching frequency data. If d is a single list, 1d array, or tuple, a SISO system description @@ -142,8 +142,8 @@ # The user provided a response and a freq vector self.fresp = array(args[0], dtype=complex) if len(self.fresp.shape) == 1: - self.fresp.reshape(1, 1, len(args[0])) - self.omega = array(args[1]) + self.fresp = self.fresp.reshape(1, 1, len(args[0])) + self.omega = array(args[1], dtype=float) if len(self.fresp.shape) != 3 or \ self.fresp.shape[-1] != self.omega.shape[-1] or \ len(self.omega.shape) != 1: @@ -189,9 +189,9 @@ if mimo: outstr.append("Input %i to output %i:" % (i + 1, j + 1)) outstr.append('Freq [rad/s] Response ') - outstr.append('------------ ------------------------') + outstr.append('------------ ---------------------') outstr.extend( - [ '%12.3f %10.4g + %10.4g' % (w, m, p) + [ '%12.3f %10.4g%+10.4gj' % (w, m, p) for m, p, w in zip(real(self.fresp[j,i,:]), imag(self.fresp[j,i,:]), wt) ]) @@ -340,7 +340,10 @@ """ # Preallocate the output. - out = empty((self.outputs, self.inputs), dtype=complex) + if getattr(omega, '__iter__', False): + out = empty((self.outputs, self.inputs, len(omega)), dtype=complex) + else: + out = empty((self.outputs, self.inputs), dtype=complex) if self.ifunc is None: try: @@ -350,11 +353,18 @@ "Frequency %f not in frequency list, try an interpolating" " FRD if you want additional points") else: - for i in range(self.outputs): - for j in range(self.inputs): - frraw = splev(omega, self.ifunc[i,j], der=0) - out[i,j] = frraw[0] + 1.0j*frraw[1] - + if getattr(omega, '__iter__', False): + for i in range(self.outputs): + for j in range(self.inputs): + for k,w in enumerate(omega): + frraw = splev(w, self.ifunc[i,j], der=0) + out[i,j,k] = frraw[0] + 1.0j*frraw[1] + else: + for i in range(self.outputs): + for j in range(self.inputs): + frraw = splev(omega, self.ifunc[i,j], der=0) + out[i,j] = frraw[0] + 1.0j*frraw[1] + return out # Method for generating the frequency response of the system Modified: trunk/src/lti.py =================================================================== --- trunk/src/lti.py 2013-05-31 11:53:29 UTC (rev 273) +++ trunk/src/lti.py 2013-06-09 14:57:59 UTC (rev 274) @@ -12,6 +12,8 @@ timebaseEqual() """ +from numpy import absolute, real + class Lti: """Lti is a parent class to linear time invariant control (LTI) objects. @@ -64,6 +66,12 @@ self.outputs = outputs self.dt = dt + def damp(self): + poles = self.pole() + wn = absolute(poles) + Z = -real(poles)/wn + return wn, Z, poles + # Test to see if a system is SISO def issiso(sys, strict=False): if isinstance(sys, (int, float, complex)) and not strict: Modified: trunk/src/margins.py =================================================================== --- trunk/src/margins.py 2013-05-31 11:53:29 UTC (rev 273) +++ trunk/src/margins.py 2013-06-09 14:57:59 UTC (rev 274) @@ -53,15 +53,37 @@ import numpy as np import control.xferfcn as xferfcn from control.freqplot import bode -from control.lti import isdtime +from control.lti import isdtime, issiso +import control.frdata as frdata +import scipy as sp -# gain and phase margins -# contributed by Sawyer B. Fuller <mi...@ca...> -#! TODO - need to add unit test functions -def stability_margins(sysdata, deg=True): +# helper functions for stability_margins +def _polyimsplit(pol): + """split a polynomial with (iw) applied into a real and an + imaginary part with w applied""" + rpencil = np.zeros_like(pol) + ipencil = np.zeros_like(pol) + rpencil[-1::-4] = 1. + rpencil[-3::-4] = -1. + ipencil[-2::-4] = 1. + ipencil[-4::-4] = -1. + return pol * rpencil, pol*ipencil + +def _polysqr(pol): + """return a polynomial squared""" + return np.polymul(pol, pol) + +# Took the framework for the old function by +# Sawyer B. Fuller <mi...@ca...>, removed a lot of the innards +# and replaced with analytical polynomial functions for Lti systems. +# +# idea for the frequency data solution copied/adapted from +# https://github.com/alchemyst/Skogestad-Python/blob/master/BODE.py +# Rene van Paassen <ren...@gm...> +def stability_margins(sysdata, deg=True, returnall=False): """Calculate gain, phase and stability margins and associated crossover frequencies. - + Usage ----- gm, pm, sm, wg, wp, ws = stability_margins(sysdata, deg=True) @@ -76,86 +98,117 @@ 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. + Returns ------- - gm, pm, sm, wg, wp, ws: float + 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. - """ - #TODO do this precisely without the effects of discretization of frequencies? - #TODO assumes SISO - #TODO unit tests, margin plot + 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, e: + print (e) + raise ValueError("Margin sysdata must be either a linear system or " + "a 3-sequence of mag, phase, omega.") - if (not getattr(sysdata, '__iter__', False)): - sys = sysdata + # 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]) - # TODO: implement for discrete time systems - if (isdtime(sys, strict=True)): - raise NotImplementedError("Function not implemented in discrete time") + # 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 > 0.)]) + w_180.sort() - mag, phase, omega = bode(sys, deg=deg, Plot=False) - elif len(sysdata) == 3: - # TODO: replace with FRD object type? - mag, phase, omega = sysdata - else: - raise ValueError("Margin sysdata must be either a linear system or a 3-sequence of mag, phase, omega.") - - if deg: - cycle = 360. - crossover = 180. + # 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 > 0.)]) + 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 > 0.) * + (np.abs(wstabplus) > 0.)]) + wstab.sort() + else: - cycle = 2 * np.pi - crossover = np.pi - - wrapped_phase = -np.mod(phase, cycle) - - # phase margin from minimum phase among all gain crossovers - neg_mag_crossings_i = np.nonzero(np.diff(mag < 1) > 0)[0] - mag_crossings_p = wrapped_phase[neg_mag_crossings_i] - if len(neg_mag_crossings_i) == 0: - if mag[0] < 1: # gain always less than one - wp = np.nan - pm = np.inf - else: # gain always greater than one - print("margin: no magnitude crossings found") - wp = np.nan - pm = np.nan - else: - min_mag_crossing_i = neg_mag_crossings_i[np.argmin(mag_crossings_p)] - wp = omega[min_mag_crossing_i] - pm = crossover + phase[min_mag_crossing_i] - if pm < 0: - print("warning: system unstable: negative phase margin") - - # gain margin from minimum gain margin among all phase crossovers - neg_phase_crossings_i = np.nonzero(np.diff(wrapped_phase < -crossover) > 0)[0] - neg_phase_crossings_g = mag[neg_phase_crossings_i] - if len(neg_phase_crossings_i) == 0: - wg = np.nan - gm = np.inf - else: - min_phase_crossing_i = neg_phase_crossings_i[ - np.argmax(neg_phase_crossings_g)] - wg = omega[min_phase_crossing_i] - gm = abs(1/mag[min_phase_crossing_i]) - if gm < 1: - print("warning: system unstable: gain margin < 1") + # 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] - # stability margin from minimum abs distance from -1 point - if deg: - phase_rad = phase * np.pi / 180. + 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: - phase_rad = phase - L = mag * np.exp(1j * phase_rad) # complex loop response to -1 pt - min_Lplus1_i = np.argmin(np.abs(L + 1)) - sm = np.abs(L[min_Lplus1_i] + 1) - ws = phase[min_Lplus1_i] + 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]) - return gm, pm, sm, wg, wp, ws # Contributed by Steffen Waldherr <wal...@is...> #! TODO - need to add test functions Modified: trunk/src/matlab.py =================================================================== --- trunk/src/matlab.py 2013-05-31 11:53:29 UTC (rev 273) +++ trunk/src/matlab.py 2013-06-09 14:57:59 UTC (rev 274) @@ -89,6 +89,7 @@ from control.statesp import StateSpace, _rss_generate, _convertToStateSpace from control.xferfcn import TransferFunction, _convertToTransferFunction from control.lti import Lti #base class of StateSpace, TransferFunction +from control.frdata import FRD from control.dtime import sample_system from control.exception import ControlArgument @@ -96,11 +97,11 @@ from control.ctrlutil import unwrap from control.freqplot import nyquist, gangof4 from control.nichols import nichols -from control.bdalg import series, parallel, negate, feedback +from control.bdalg import series, parallel, negate, feedback, append, connect from control.pzmap import pzmap from control.statefbk import ctrb, obsv, gram, place, lqr from control.delay import pade -from control.modelsimp import hsvd, balred, modred +from control.modelsimp import hsvd, balred, modred, minreal from control.mateqn import lyap, dlyap, dare, care __doc__ += r""" @@ -125,7 +126,7 @@ \* :func:`ss` create state-space (SS) models \ dss create descriptor state-space models \ delayss create state-space models with delayed terms -\ frd create frequency response data (FRD) models +\* :func:`frd` create frequency response data (FRD) models \ lti/exp create pure continuous-time delays (TF and ZPK only) \ filt specify digital filters @@ -156,7 +157,7 @@ \* :func:`tf` conversion to transfer function \ zpk conversion to zero/pole/gain \* :func:`ss` conversion to state space -\ frd conversion to frequency data +\* :func:`frd` conversion to frequency data \ c2d continuous to discrete conversion \ d2c discrete to continuous conversion \ d2d resample discrete-time model @@ -174,7 +175,7 @@ ---------------------------------------------------------------------------- == ========================== ============================================ -\ append group LTI models by appending inputs/outputs +\* :func:`~bdalg.append` group LTI models by appending inputs/outputs \* :func:`~bdalg.parallel` connect LTI models in parallel (see also overloaded ``+``) \* :func:`~bdalg.series` connect LTI models in series @@ -200,7 +201,7 @@ \ lti/order model order (number of states) \* :func:`~pzmap.pzmap` pole-zero map (TF only) \ lti/iopzmap input/output pole-zero map -\ damp natural frequency, damping of system poles +\* :func:`damp` natural frequency, damping of system poles \ esort sort continuous poles by real part \ dsort sort discrete poles by magnitude \ lti/stabsep stable/unstable decomposition @@ -243,7 +244,7 @@ ---------------------------------------------------------------------------- == ========================== ============================================ -\ minreal minimal realization; pole/zero cancellation +\* :func:`~modelsimp.minreal` minimal realization; pole/zero cancellation \ ss/sminreal structurally minimal realization \* :func:`~modelsimp.hsvd` hankel singular values (state contributions) \* :func:`~modelsimp.balred` reduced-order approximations of LTI models @@ -569,7 +570,43 @@ else: raise ValueError("Needs 1 or 2 arguments; received %i." % len(args)) +def frd(*args): + ''' + Construct a Frequency Response Data model, or convert a system + frd models store the (measured) frequency response of a system. + + This function can be called in different ways: + + ``frd(response, freqs)`` + Create an frd model with the given response data, in the form of + complex response vector, at matching frequency freqs [in rad/s] + + ``frd(sys, freqs)`` + Convert an Lti system into an frd model with data at frequencies + freqs. + + Parameters + ---------- + response: array_like, or list + complex vector with the system response + freq: array_lik or lis + vector with frequencies + sys: Lti (StateSpace or TransferFunction) + A linear system + + Returns + ------- + sys: FRD + New frequency response system + + See Also + -------- + ss, tf + ''' + return FRD(*args) + + def ss2tf(*args): """ Transform a state space system to a transfer function. @@ -714,7 +751,7 @@ else: raise ValueError("Needs 1 or 2 arguments; received %i." % len(args)) -def rss(states=1, inputs=1, outputs=1): +def rss(states=1, outputs=1, inputs=1): """ Create a stable **continuous** random state space object. @@ -751,7 +788,7 @@ return _rss_generate(states, inputs, outputs, 'c') -def drss(states=1, inputs=1, outputs=1): +def drss(states=1, outputs=1, inputs=1): """ Create a stable **discrete** random state space object. @@ -852,16 +889,20 @@ return sys.zero() -def evalfr(sys, omega): +def evalfr(sys, x): """ - Evaluate the transfer function of an LTI system at an angular frequency. + 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 - omega: scalar - Frequency + x: scalar + Complex number Returns ------- @@ -880,15 +921,17 @@ Examples -------- >>> sys = ss("1. -2; 3. -4", "5.; 7", "6. 8", "9.") - >>> evalfr(sys, 1.) + >>> 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) + - return sys.evalfr(omega) - def freqresp(sys, omega): """ Frequency response of an LTI system at multiple angular frequencies. @@ -1057,10 +1100,11 @@ Returns ------- - gm, pm, wg, wp : float - Gain margin gm, phase margin pm (in deg), and associated crossover - frequencies wg and wp (in rad/sec) of SISO open-loop. If more than - one crossover frequency is detected, returns the lowest + gm, pm, Wcg, Wcp : float + Gain margin gm, phase margin pm (in deg), gain crossover frequency + (corresponding to phase margin) and phase crossover frequency + (corresponding to gain margin), in rad/sec of SISO open-loop. + If more than one crossover frequency is detected, returns the lowest corresponding margin. Examples @@ -1083,7 +1127,7 @@ raise ValueError("Margin needs 1 or 3 arguments; received %i." % len(args)) - return margin[0], margin[1], margin[3], margin[4] + return margin[0], margin[1], margin[4], margin[3] def dcgain(*args): ''' @@ -1139,10 +1183,48 @@ gain = sys.D - sys.C * sys.A.I * sys.B return gain +def damp(sys, doprint=True): + ''' + Compute natural frequency, damping and poles of a system + + The function takes 1 or 2 parameters + + Parameters + ---------- + sys: Lti (StateSpace or TransferFunction) + A linear system object + doprint: + if true, print table with values + + Returns + ------- + wn: array + Natural frequencies of the poles + damping: array + Damping values + poles: array + Pole locations + + See Also + -------- + pole + ''' + wn, damping, poles = sys.damp() + if doprint: + print('_____Eigenvalue______ Damping___ Frequency_') + for p, d, w in zip(poles, damping, wn) : + if abs(p.imag) < 1e-12: + print("%10.4g %10.4g %10.4g" % + (p.real, 1.0, -p.real)) + else: + print("%10.4g%+10.4gj %10.4g %10.4g" % + (p.real, p.imag, d, w)) + return wn, damping, poles + # Simulation routines # Call corresponding functions in timeresp, with arguments transposed -def step(sys, T=None, X0=0., input=0, output=0, **keywords): +def step(sys, T=None, X0=0., input=0, output=None, **keywords): ''' Step response of a linear system @@ -1192,7 +1274,7 @@ Examples -------- - >>> T, yout = step(sys, T, X0) + >>> yout, T = step(sys, T, X0) ''' T, yout = timeresp.step_response(sys, T, X0, input, output, transpose = True, **keywords) @@ -1405,3 +1487,4 @@ # TODO: add docstring # Call the sample_system() function to do the work return sample_system(sysc, Ts, method) + Modified: trunk/src/modelsimp.py =================================================================== --- trunk/src/modelsimp.py 2013-05-31 11:53:29 UTC (rev 273) +++ trunk/src/modelsimp.py 2013-06-09 14:57:59 UTC (rev 274) @@ -269,6 +269,33 @@ return rsys +def minreal(sys, tol=None, verbose=True): + ''' + Eliminates uncontrollable or unobservable states in state-space + models or cancelling pole-zero pairs in transfer functions. The + output sysr has minimal order and the same response + characteristics as the original model sys. + + Parameters + ---------- + sys: StateSpace or TransferFunction + Original system + tol: real + Tolerance + verbose: bool + Print results if True + + Returns + ------- + rsys: StateSpace or TransferFunction + Cleaned model + ''' + sysr = sys.minreal(tol) + if verbose: + print("{nstates} states have been removed from the model".format( + nstates=len(sys.pole()) - len(sysr.pole()))) + return sysr + def era(YY, m, n, nin, nout, r): """ Calculate an ERA model of order `r` based on the impulse-response data `YY`. Modified: trunk/src/rlocus.py =================================================================== --- trunk/src/rlocus.py 2013-05-31 11:53:29 UTC (rev 273) +++ trunk/src/rlocus.py 2013-06-09 14:57:59 UTC (rev 274) @@ -50,9 +50,11 @@ import scipy.signal # signal processing toolbox import pylab # plotting routines import control.xferfcn as xferfcn +from functools import partial # Main function: compute a root locus diagram -def root_locus(sys, kvect, xlim=None, ylim=None, plotstr='-', Plot=True): +def root_locus(sys, kvect, xlim=None, ylim=None, plotstr='-', Plot=True, + PrintGain=True): """Calculate the root locus by finding the roots of 1+k*TF(s) where TF is self.num(s)/self.den(s) and each k is an element of kvect. @@ -65,7 +67,9 @@ List of gains to use in computing diagram Plot : boolean (default = True) If True, plot magnitude and phase - + PrintGain: boolean (default = True) + If True, report mouse clicks when close to the root-locus branches, + calculate gain, damping and print Return values ------------- rlist : list of computed root locations @@ -80,6 +84,10 @@ # Create the plot if (Plot): + f = pylab.figure() + if PrintGain: + cid = f.canvas.mpl_connect( + 'button_release_event', partial(_RLFeedbackClicks, sys=sys)) ax = pylab.axes(); # plot open loop poles @@ -165,3 +173,12 @@ sorted[n,ind] = elem prevrow = sorted[n,:] return sorted + +def _RLFeedbackClicks(event, sys): + """Print root-locus gain feedback for clicks on the root-locus plot + """ + s = complex(event.xdata, event.ydata) + K = -1./sys.horner(s) + if abs(K.real) > 1e-8 and abs(K.imag/K.real) < 0.04: + print("Clicked at %10.4g%+10.4gj gain %10.4g damp %10.4g" % + (s.real, s.imag, K.real, -1*s.real/abs(s))) Modified: trunk/src/statesp.py =================================================================== --- trunk/src/statesp.py 2013-05-31 11:53:29 UTC (rev 273) +++ trunk/src/statesp.py 2013-06-09 14:57:59 UTC (rev 274) @@ -27,6 +27,7 @@ StateSpace.zero StateSpace.feedback StateSpace.returnScipySignalLti +StateSpace.append _convertToStateSpace _rss_generate @@ -338,10 +339,23 @@ B = self.B * other; D = self.D * other; return StateSpace(A, B, C, D, self.dt) + + # is lti, and convertible? + if isinstance(other, Lti): + return _convertToStateSpace(other) * self - else: - raise TypeError("can't interconnect systems") + # try to treat this as a matrix + try: + X = matrix(other) + C = X * self.C + D = X * self.D + return StateSpace(self.A, self.B, C, D, self.dt) + except Exception, e: + print(e) + pass + raise TypeError("can't interconnect systems") + # TODO: __div__ and __rdiv__ are not written yet. def __div__(self, other): """Divide two LTI systems.""" @@ -370,10 +384,16 @@ else: s = omega * 1.j - fresp = self.C * solve(s * eye(self.states) - self.A, - self.B) + self.D + return self.horner(s) - return array(fresp) + def horner(self, s): + '''Evaluate the systems's transfer function for a complex variable + + Returns a matrix of values evaluated at complex variable s. + ''' + resp = self.C * solve(s * eye(self.states) - self.A, + self.B) + self.D + return array(resp) # Method for generating the frequency response of the system # TODO: add discrete time check @@ -475,6 +495,17 @@ return StateSpace(A, B, C, D, dt) + def minreal(self, tol=None): + """Calculate a minimal realization, removes unobservable and + uncontrollable states""" + try: + from slycot import tb01pd + A, B, C, nr = tb01pd(self.states, self.inputs, self.outputs, + self.A, self.B, self.C, tol=tol) + return StateSpace(A[:nr,:nr], B[:nr,:], C[:,:nr], self.D) + except ImportError: + raise TypeError("minreal requires slycot tb01pd") + # TODO: add discrete time check def returnScipySignalLti(self): """Return a list of a list of scipy.signal.lti objects. @@ -497,6 +528,33 @@ return out + def append(self, other): + """Append a second model to the present model. The second + model is converted to state-space if necessary, inputs and + outputs are appended and their order is preserved""" + if not isinstance(other, StateSpace): + other = _convertToStateSpace(other) + + if self.dt != other.dt: + raise ValueError("Systems must have the same time step") + + n = self.states + other.states + m = self.inputs + other.inputs + p = self.outputs + other.outputs + A = zeros( (n, n) ) + B = zeros( (n, m) ) + C = zeros( (p, n) ) + D = zeros( (p, m) ) + A[:self.states,:self.states] = self.A + A[self.states:,self.states:] = other.A + B[:self.states,:self.inputs] = self.B + B[self.states:,self.inputs:] = other.B + C[:self.outputs,:self.states] = self.C + C[self.outputs:,self.states:] = other.C + D[:self.outputs,:self.inputs] = self.D + D[self.outputs:,self.inputs:] = other.D + return StateSpace(A, B, C, D, self.dt) + # TODO: add discrete time check def _convertToStateSpace(sys, **kw): """Convert a system to state space form (if needed). @@ -757,3 +815,51 @@ return sys +def _mimo2simo(sys, input, warn_conversion=False): + #pylint: disable=W0622 + """ + Convert a MIMO system to a SIMO system. (Convert a system with multiple + inputs and/or outputs, to a system with a single input but possibly + multiple outputs.) + + The input that is used in the SIMO system can be selected with the + parameter ``input``. All other inputs are set to 0, all other + outputs are ignored. + + If ``sys`` is already a SIMO system, it will be returned unaltered. + + Parameters + ---------- + sys: StateSpace + Linear (MIMO) system that should be converted. + input: int + Index of the input that will become the SIMO system's only input. + warn_conversion: bool + If True: print a warning message when sys is a MIMO system. + Warn that a conversion will take place. + + Returns: + -------- + sys: StateSpace + The converted (SIMO) system. + """ + if not (isinstance(input, int)): + raise TypeError("Parameter ``input`` be an integer number.") + if not (0 <= input < sys.inputs): + raise ValueError("Selected input does not exist. " + "Selected input: {sel}, " + "number of system inputs: {ext}." + .format(sel=input, ext=sys.inputs)) + #Convert sys to SISO if necessary + if sys.inputs > 1: + if warn_conversion: + warnings.warn("Converting MIMO system to SIMO system. " + "Only input {i} is used." + .format(i=input)) + # $X = A*X + B*U + # Y = C*X + D*U + new_B = sys.B[:, input] + new_D = sys.D[:, input] + sys = StateSpace(sys.A, new_B, sys.C, new_D, sys.dt) + + return sys Modified: trunk/src/timeresp.py =================================================================== --- trunk/src/timeresp.py 2013-05-31 11:53:29 UTC (rev 273) +++ trunk/src/timeresp.py 2013-06-09 14:57:59 UTC (rev 274) @@ -121,7 +121,7 @@ from copy import deepcopy import warnings from control.lti import Lti # base class of StateSpace, TransferFunction -from control. statesp import StateSpace, _rss_generate, _convertToStateSpace, _mimo2siso +from control. statesp import StateSpace, _rss_generate, _convertToStateSpace, _mimo2simo, _mimo2siso from control.lti import isdtime, isctime # Helper function for checking array-like parameters @@ -386,15 +386,15 @@ return T, yout, xout -def step_response(sys, T=None, X0=0., input=0, output=0, \ - transpose = False, **keywords): +def step_response(sys, T=None, X0=0., input=0, output=None, + transpose = False, **keywords): #pylint: disable=W0622 """Step response of a linear system - If the system has multiple inputs or outputs (MIMO), one input and one - output have to be selected for the simulation. The parameters `input` - and `output` do this. All other inputs are set to 0, all other outputs - are ignored. + If the system has multiple inputs or outputs (MIMO), one input has + to be selected for the simulation. Optionally, one output may be + selected. The parameters `input` and `output` do this. All other + inputs are set to 0, all other outputs are ignored. For information on the **shape** of parameters `T`, `X0` and return values `T`, `yout` see: :ref:`time-series-convention` @@ -416,7 +416,8 @@ Index of the input that will be used in this simulation. output: int - Index of the output that will be used in this simulation. + Index of the output that will be used in this simulation. Set to None + to not trim outputs transpose: bool If True, transpose all input and output arrays (for backward @@ -447,7 +448,10 @@ >>> T, yout = step_response(sys, T, X0) """ sys = _convertToStateSpace(sys) - sys = _mimo2siso(sys, input, output, warn_conversion=True) + if output == None: + sys = _mimo2simo(sys, input, warn_conversion=True) + else: + sys = _mimo2siso(sys, input, output, warn_conversion=True) if T is None: if isctime(sys): T = _default_response_times(sys.A, 100) @@ -459,13 +463,13 @@ U = np.ones_like(T) T, yout, _xout = forced_response(sys, T, U, X0, - transpose=transpose, **keywords) + transpose=transpose, **keywords) return T, yout -def initial_response(sys, T=None, X0=0., input=0, output=0, transpose=False, - **keywords): +def initial_response(sys, T=None, X0=0., input=0, output=None, transpose=False, + **keywords): #pylint: disable=W0622 """Initial condition response of a linear system @@ -494,7 +498,8 @@ Index of the input that will be used in this simulation. output: int - Index of the output that will be used in this simulation. + Index of the output that will be used in this simulation. Set to None + to not trim outputs transpose: bool If True, transpose all input and output arrays (for backward @@ -525,7 +530,10 @@ >>> T, yout = initial_response(sys, T, X0) """ sys = _convertToStateSpace(sys) - sys = _mimo2siso(sys, input, output, warn_conversion=True) + if output == None: + sys = _mimo2simo(sys, input, warn_conversion=False) + else: + sys = _mimo2siso(sys, input, output, warn_conversion=False) # Create time and input vectors; checking is done in forced_response(...) # The initial vector X0 is created in forced_response(...) if necessary @@ -534,11 +542,11 @@ U = np.zeros_like(T) T, yout, _xout = forced_response(sys, T, U, X0, transpose=transpose, - **keywords) + **keywords) return T, yout -def impulse_response(sys, T=None, X0=0., input=0, output=0, +def impulse_response(sys, T=None, X0=0., input=0, output=None, transpose=False, **keywords): #pylint: disable=W0622 """Impulse response of a linear system @@ -568,7 +576,8 @@ Index of the input that will be used in this simulation. output: int - Index of the output that will be used in this simulation. + Index of the output that will be used in this simulation. Set to None + to not trim outputs transpose: bool If True, transpose all input and output arrays (for backward @@ -599,7 +608,10 @@ >>> T, yout = impulse_response(sys, T, X0) """ sys = _convertToStateSpace(sys) - sys = _mimo2siso(sys, input, output, warn_conversion=True) + if output == None: + sys = _mimo2simo(sys, input, warn_conversion=True) + else: + sys = _mimo2siso(sys, input, output, warn_conversion=True) # System has direct feedthrough, can't simulate impulse response numerically if np.any(sys.D != 0) and isctime(sys): Modified: trunk/src/xferfcn.py =================================================================== --- trunk/src/xferfcn.py 2013-05-31 11:53:29 UTC (rev 273) +++ trunk/src/xferfcn.py 2013-06-09 14:57:59 UTC (rev 274) @@ -404,7 +404,8 @@ for k in range(self.inputs): # Multiply & add. num_summand[k] = polymul(self.num[i][k], other.num[k][j]) den_summand[k] = polymul(self.den[i][k], other.den[k][j]) - num[i][j], den[i][j] = _addSISO(num[i][j], den[i][j], + num[i][j], den[i][j] = _addSISO( + num[i][j], den[i][j], num_summand[k], den_summand[k]) return TransferFunction(num, den, dt) @@ -412,8 +413,48 @@ def __rmul__(self, other): """Right multiply two LTI objects (serial connection).""" - return self * other + # Convert the second argument to a transfer function. + if isinstance(other, (int, float, complex)): + other = _convertToTransferFunction(other, inputs=self.inputs, + outputs=self.inputs) + else: + other = _convertToTransferFunction(other) + + # Check that the input-output sizes are consistent. + if other.inputs != self.outputs: + raise ValueError("C = A * B: A has %i column(s) (input(s)), but B \ +has %i row(s)\n(output(s))." % (other.inputs, self.outputs)) + inputs = self.inputs + outputs = other.outputs + + # Figure out the sampling time to use + if (self.dt == None and other.dt != None): + dt = other.dt # use dt from second argument + elif (other.dt == None and self.dt != None) or (self.dt == other.dt): + dt = self.dt # use dt from first argument + else: + raise ValueError("Systems have different sampling times") + + # Preallocate the numerator and denominator of the sum. + num = [[[0] for j in range(inputs)] for i in range(outputs)] + den = [[[1] for j in range(inputs)] for i in range(outputs)] + + # Temporary storage for the summands needed to find the (i, j)th element + # of the product. + num_summand = [[] for k in range(other.inputs)] + den_summand = [[] for k in range(other.inputs)] + + for i in range(outputs): # Iterate through rows of product. + for j in range(inputs): # Iterate through columns of product. + for k in range(other.inputs): # Multiply & add. + num_summand[k] = polymul(other.num[i][k], self.num[k][j]) + den_summand[k] = polymul(other.den[i][k], self.den[k][j]) + num[i][j], den[i][j] = _addSISO(num[i][j], den[i][j], + num_summand[k], den_summand[k]) + + return TransferFunction(num, den, dt) + # TODO: Division of MIMO transfer function objects is not written yet. def __div__(self, other): """Divide two LTI objects.""" @@ -486,9 +527,20 @@ warn("evalfr: frequency evaluation above Nyquist frequency") else: s = 1.j * omega + + return self.horner(s) + def horner(self, s): + '''Evaluate the systems's transfer function for a complex variable + + Returns a matrix of values evaluated at complex variable s. + ''' + # Preallocate the output. - out = empty((self.outputs, self.inputs), dtype=complex) + if getattr(s, '__iter__', False): + out = empty((self.outputs, self.inputs, len(s)), dtype=complex) + else: + out = empty((self.outputs, self.inputs), dtype=complex) for i in range(self.outputs): for j in range(self.inputs): @@ -620,8 +672,12 @@ newzeros.append(z) # keep result - num[i][j] = gain * real(poly(newzeros)) + if len(newzeros): + num[i][j] = gain * real(poly(newzeros)) + else: + num[i][j] = array([gain]) den[i][j] = real(poly(poles)) + # end result return TransferFunction(num, den) @@ -906,6 +962,15 @@ In the latter example, sys's matrix transfer function is [[1., 1., 1.] [1., 1., 1.]]. + + If sys is an array-like type, then it is converted to a constant-gain + transfer function. + + >>> sys = _convertToTransferFunction([[1. 0.], [2. 3.]]) + + In this example, the numerator matrix will be + [[[1.0], [0.0]], [[2.0], [3.0]]] + and the denominator matrix [[[1.0], [1.0]], [[1.0], [1.0]]] """ from control.statesp import StateSpace @@ -966,5 +1031,16 @@ den = [[[1] for j in range(inputs)] for i in range(outputs)] return TransferFunction(num, den) - else: - raise TypeError("Can't convert given type to TransferFunction system.") + + # If this is array-like, try to create a constant feedthrough + try: + D = array(sys) + outputs, inputs = D.shape + num = [[[D[i,j]] for j in range(inputs)] for i in range(outputs)] + den = [[[1] for j in range(inputs)] for i in range(outputs)] + return TransferFunction(num, den) + except Exception, e: + print("Failure to assume argument is matrix-like in" + " _convertToTransferFunction, result %s" % e) + + raise TypeError("Can't convert given type to TransferFunction system.") Modified: trunk/tests/convert_test.py =================================================================== --- trunk/tests/convert_test.py 2013-05-31 11:53:29 UTC (rev 273) +++ trunk/tests/convert_test.py 2013-06-09 14:57:59 UTC (rev 274) @@ -57,7 +57,7 @@ for outputs in range(1, self.maxIO): # start with a random SS system and transform to TF then # back to SS, check that the matrices are the same. - ssOriginal = matlab.rss(states, inputs, outputs) + ssOriginal = matlab.rss(states, outputs, inputs) if (verbose): self.printSys(ssOriginal, 1) Modified: trunk/tests/matlab_test.py =================================================================== --- trunk/tests/matlab_test.py 2013-05-31 11:53:29 UTC (rev 273) +++ trunk/tests/matlab_test.py 2013-06-09 14:57:59 UTC (rev 274) @@ -11,9 +11,48 @@ from __future__ import print_function import unittest import numpy as np +from scipy.linalg import eigvals import scipy as sp from control.matlab import * +from control.frdata import FRD +# for running these through Matlab or Octave +''' +siso_ss1 = ss([1. -2.; 3. -4.], [5.; 7.], [6. 8.], [0]) + +siso_tf1 = tf([1], [1, 2, 1]) +siso_tf2 = tf([1, 1], [1, 2, 3, 1]) + +siso_tf3 = tf(siso_ss1) +siso_ss2 = ss(siso_tf2) +siso_ss3 = ss(siso_tf3) +siso_tf4 = tf(siso_ss2) + +A =[ 1. -2. 0. 0.; + 3. -4. 0. 0.; + 0. 0. 1. -2.; + 0. 0. 3. -4. ] +B = [ 5. 0.; + 7. 0.; + 0. 5.; + 0. 7. ] +C = [ 6. 8. 0. 0.; + 0. 0. 6. 8. ] +D = [ 9. 0.; + 0. 9. ] +mimo_ss1 = ss(A, B, C, D) + +% all boring, since no cross-over +margin(siso_tf1) +margin(siso_tf2) +margin(siso_ss1) +margin(siso_ss2) + +% make a bit better +[gm, pm, gmc, pmc] = margin(siso_ss2*siso_ss2*2) + +''' + class TestMatlab(unittest.TestCase): def setUp(self): """Set up some systems for testing out MATLAB functions""" @@ -194,6 +233,9 @@ gm, pm, wg, wp = margin(self.siso_tf2); gm, pm, wg, wp = margin(self.siso_ss1); gm, pm, wg, wp = margin(self.siso_ss2); + gm, pm, wg, wp = margin(self.siso_ss2*self.siso_ss2*2); + np.testing.assert_array_almost_equal( + [gm, pm, wg, wp], [1.5451, 75.9933, 1.2720, 0.6559], decimal=3) def testDcgain(self): #Create different forms of a SISO system @@ -274,13 +316,16 @@ freqresp(self.siso_tf3, w) def testEvalfr(self): - w = 1 - evalfr(self.siso_ss1, w) + w = 1j + self.assertEqual(evalfr(self.siso_ss1, w), 44.8-21.4j) evalfr(self.siso_ss2, w) evalfr(self.siso_ss3, w) evalfr(self.siso_tf1, w) evalfr(self.siso_tf2, w) evalfr(self.siso_tf3, w) + np.testing.assert_array_almost_equal( + evalfr(self.mimo_ss1, w), + np.array( [[44.8-21.4j, 0.], [0., 44.8-21.4j]])) def testHsvd(self): hsvd(self.siso_ss1) @@ -309,12 +354,12 @@ def testRss(self): rss(1) rss(2) - rss(2, 3, 1) + rss(2, 1, 3) def testDrss(self): drss(1) drss(2) - drss(2, 3, 1) + drss(2, 1, 3) def testCtrb(self): ctrb(self.siso_ss1.A, self.siso_ss1.B) @@ -372,6 +417,102 @@ for i in range(len(tfdata_1)): np.testing.assert_array_almost_equal(tfdata_1[i], tfdata_2[i]) + def testDamp(self): + A = np.mat('''-0.2 0.06 0 -1; + 0 0 1 0; + -17 0 -3.8 1; + 9.4 0 -0.4 -0.6''') + B = np.mat('''-0.01 0.06; + 0 0; + -32 5.4; + 2.6 -7''') + C = np.eye(4) + D = np.zeros((4,2)) + sys = ss(A, B, C, D) + wn, Z, p = damp(sys, False) + print (wn) + np.testing.assert_array_almost_equal( + wn, np.array([4.07381994, 3.28874827, 3.28874827, + 1.08937685e-03])) + np.testing.assert_array_almost_equal( + Z, np.array([1.0, 0.07983139, 0.07983139, 1.0])) + + def testConnect(self): + sys1 = ss("1. -2; 3. -4", "5.; 7", "6, 8", "9.") + sys2 = ss("-1.", "1.", "1.", "0.") + sys = append(sys1, sys2) + Q= np.mat([ [ 1, 2], [2, -1] ]) # basically feedback, output 2 in 1 + sysc = connect(sys, Q, [2], [1, 2]) + print(sysc) + np.testing.assert_array_almost_equal( + sysc.A, np.mat('1 -2 5; 3 -4 7; -6 -8 -10')) + np.testing.assert_array_almost_equal( + sysc.B, np.mat('0; 0; 1')) + np.testing.assert_array_almost_equal( + sysc.C, np.mat('6 8 9; 0 0 1')) + np.testing.assert_array_almost_equal( + sysc.D, np.mat('0; 0')) + + def testConnect2(self): + sys = append(ss([[-5, -2.25], [4, 0]], [[2], [0]], + [[0, 1.125]], [[0]]), + ss([[-1.6667, 0], [1, 0]], [[2], [0]], + [[0, 3.3333]], [[0]]), + 1) + Q = [ [ 1, 3], [2, 1], [3, -2]] + sysc = connect(sys, Q, [3], [3, 1, 2]) + np.testing.assert_array_almost_equal( + sysc.A, np.mat([[-5, -2.25, 0, -6.6666], + [4, 0, 0, 0], + [0, 2.25, -1.6667, 0], + [0, 0, 1, 0]])) + np.testing.assert_array_almost_equal( + sysc.B, np.mat([[2], [0], [0], [0]])) + np.testing.assert_array_almost_equal( + sysc.C, np.mat([[0, 0, 0, -3.3333], + [0, 1.125, 0, 0], + [0, 0, 0, 3.3333]])) + np.testing.assert_array_almost_equal( + sysc.D, np.mat([[1], [0], [0]])) + + + + def testFRD(self): + h = tf([1], [1, 2, 2]) + omega = np.logspace(-1, 2, 10) + frd1 = frd(h, omega) + assert isinstance(frd1, FRD) + frd2 = frd(frd1.fresp[0,0,:], omega) + assert isinstance(frd2, FRD) + + def testMinreal(self, verbose=False): + """Test a minreal model reduction""" + #A = [-2, 0.5, 0; 0.5, -0.3, 0; 0, 0, -0.1] + A = [[-2, 0.5, 0], [0.5, -0.3, 0], [0, 0, -0.1]] + #B = [0.3, -1.3; 0.1, 0; 1, 0] + B = [[0.3, -1.3], [0.1, 0.], [1.0, 0.0]] + #C = [0, 0.1, 0; -0.3, -0.2, 0] + C = [[0., 0.1, 0.0], [-0.3, -0.2, 0.0]] + #D = [0 -0.8; -0.3 0] + D = [[0., -0.8], [-0.3, 0.]] + # sys = ss(A, B, C, D) + + sys = ss(A, B, C, D) + sysr = minreal(sys) + self.assertEqual(sysr.states, 2) + self.assertEqual(sysr.inputs, sys.inputs) + self.assertEqual(sysr.outputs, sys.outputs) + np.testing.assert_array_almost_equal( + eigvals(sysr.A), [-2.136154, -0.1638459]) + + s = tf([1, 0], [1]) + h = (s+1)*(s+2.00000000001)/(s+2)/(s**2+s+1) + hm = minreal(h) + hr = (s+1)/(s**2+s+1) + np.testing.assert_array_almost_equal(hm.num[0][0], hr.num[0][0]) + np.testing.assert_array_almost_equal(hm.den[0][0], hr.den[0][0]) + + #! TODO: not yet implemented # def testMIMOtfdata(self): # sisotf = ss2tf(self.siso_ss1) Modified: trunk/tests/slycot_convert_test.py =================================================================== --- trunk/tests/slycot_convert_test.py 2013-05-31 11:53:29 UTC (rev 273) +++ trunk/tests/slycot_convert_test.py 2013-06-09 14:57:59 UTC (rev 274) @@ -37,7 +37,7 @@ for inputs in range(1, self.maxI+1): for outputs in range(1, self.maxO+1): for testNum in range(self.numTests): - ssOriginal = matlab.rss(states, inputs, outputs) + ssOriginal = matlab.rss(states, outputs, inputs) if (verbose): print('====== Original SS ==========') print(ssOriginal) @@ -82,7 +82,7 @@ for testNum in range(self.numTests): for inputs in range(1,1): for outputs in range(1,1): - ssOriginal = matlab.rss(states, inputs, outputs) + ssOriginal = matlab.rss(states, outputs, inputs) tfOriginal_Actrb, tfOriginal_Bctrb, tfOriginal_Cctrb, tfOrigingal_nctrb, tfOriginal_index,\ tfOriginal_dcoeff, tfOriginal_ucoeff = tb04ad(states,inputs,outputs,\ Modified: trunk/tests/statesp_test.py =================================================================== --- trunk/tests/statesp_test.py 2013-05-31 11:53:29 UTC (rev 273) +++ trunk/tests/statesp_test.py 2013-06-09 14:57:59 UTC (rev 274) @@ -5,8 +5,10 @@ import unittest import numpy as np +from scipy.linalg import eigvals import control.matlab as matlab -from control.statesp import StateSpace +from control.statesp import StateSpace, _convertToStateSpace +from control.xferfcn import TransferFunction class TestStateSpace(unittest.TestCase): """Tests for the StateSpace class.""" @@ -134,7 +136,74 @@ np.testing.assert_almost_equal(mag, truemag) np.testing.assert_almost_equal(phase, truephase) np.testing.assert_equal(omega, trueomega) + + def testMinreal(self): + """Test a minreal model reduction""" + #A = [-2, 0.5, 0; 0.5, -0.3, 0; 0, 0, -0.1] + A = [[-2, 0.5, 0], [0.5, -0.3, 0], [0, 0, -0.1]] + #B = [0.3, -1.3; 0.1, 0; 1, 0] + B = [[0.3, -1.3], [0.1, 0.], [1.0, 0.0]] + #C = [0, 0.1, 0; -0.3, -0.2, 0] + C = [[0., 0.1, 0.0], [-0.3, -0.2, 0.0]] + #D = [0 -0.8; -0.3 0] + D = [[0., -0.8], [-0.3, 0.]] + # sys = ss(A, B, C, D) + + sys = StateSpace(A, B, C, D) + sysr = sys.minreal() + self.assertEqual(sysr.states, 2) + self.assertEqual(sysr.inputs, sys.inputs) + self.assertEqual(sysr.outputs, sys.outputs) + np.testing.assert_array_almost_equal( + eigvals(sysr.A), [-2.136154, -0.1638459]) + + def testAppendSS(self): + """Test appending two state-space systems""" + A1 = [[-2, 0.5, 0], [0.5, -0.3, 0], [0, 0, -0.1]] + B1 = [[0.3, -1.3], [0.1, 0.], [1.0, 0.0]] + C1 = [[0., 0.1, 0.0], [-0.3, -0.2, 0.0]] + D1 = [[0., -0.8], [-0.3, 0.]] + A2 = [[-1.]] + B2 = [[1.2]] + C2 = [[0.5]] + D2 = [[0.4]] + A3 = [[-2, 0.5, 0, 0], [0.5, -0.3, 0, 0], [0, 0, -0.1, 0], + [0, 0, 0., -1.]] + B3 = [[0.3, -1.3, 0], [0.1, 0., 0], [1.0, 0.0, 0], [0., 0, 1.2]] + C3 = [[0., 0.1, 0.0, 0.0], [-0.3, -0.2, 0.0, 0.0], [0., 0., 0., 0.5]] + D3 = [[0., -0.8, 0.], [-0.3, 0., 0.], [0., 0., 0.4]] + sys1 = StateSpace(A1, B1, C1, D1) + sys2 = StateSpace(A2, B2, C2, D2) + sys3 = StateSpace(A3, B3, C3, D3) + sys3c = sys1.append(sys2) + np.testing.assert_array_almost_equal(sys3.A, sys3c.A) + np.testing.assert_array_almost_equal(sys3.B, sys3c.B) + np.testing.assert_array_almost_equal(sys3.C, sys3c.C) + np.testing.assert_array_almost_equal(sys3.D, sys3c.D) + + def testAppendTF(self): + """Test appending a state-space system with a tf""" + A1 = [[-2, 0.5, 0], [0.5, -0.3, 0], [0, 0, -0.1]] + B1 = [[0.3... [truncated message content] |