From: <mur...@us...> - 2010-06-17 21:29:29
|
Revision: 26 http://python-control.svn.sourceforge.net/python-control/?rev=26&view=rev Author: murrayrm Date: 2010-06-17 21:29:23 +0000 (Thu, 17 Jun 2010) Log Message: ----------- added Ryan Krauss's controls.py module for easy access (and version control) Added Paths: ----------- trunk/external/ trunk/external/controls.py Added: trunk/external/controls.py =================================================================== --- trunk/external/controls.py (rev 0) +++ trunk/external/controls.py 2010-06-17 21:29:23 UTC (rev 26) @@ -0,0 +1,1386 @@ +# controls.py - Ryan Krauss's control module +# $Id$ + +"""This module is for analyzing linear, time-invariant dynamic systems +and feedback control systems using the Laplace transform. The heart +of the module is the TransferFunction class, which represents a +transfer function as a ratio of numerator and denominator polynomials +in s. TransferFunction is derived from scipy.signal.lti.""" + +import glob, pdb +from math import atan2, log10 + +from scipy import * +from scipy.linalg import inv as inverse +from scipy.optimize import newton, fmin, fminbound +from scipy.io import read_array, save, loadmat, write_array +from scipy import signal + +from IPython.Debugger import Pdb + +import sys, os, copy, time + +from matplotlib.ticker import LogFormatterMathtext + +version = '1.1.0' + +class MyFormatter(LogFormatterMathtext): + def __call__(self, x, pos=None): + if pos==0: return '' # pos=0 is the first tick + else: return LogFormatterMathtext.__call__(self, x, pos) + + +def shift(vectin, new): + N = len(vectin)-1 + for n in range(N,0,-1): + vectin[n]=vectin[n-1] + vectin[0]=new + return vectin + +def myeq(p1,p2): + """Test the equality of the of two polynomials based on + coeffiecents.""" + if hasattr(p1, 'coeffs') and hasattr(p2, 'coeffs'): + c1=p1.coeffs + c2=p2.coeffs + else: + return False + if len(c1)!=len(c2): + return False + else: + testvect=c1==c2 + if hasattr(testvect,'all'): + return testvect.all() + else: + return testvect + +def build_fit_matrix(output_vect, input_vect, numorder, denorder): + """Build the [A] matrix used in least squares curve fitting + according to + + output_vect = [A]c + + as described in fit_discrete_response.""" + A = zeros((len(output_vect),numorder+denorder+1))#the +1 accounts + #for the fact that both the numerator and the denominator + #have zero-order terms (which would give +2), but the + #zero order denominator term is actually not used in the fit + #(that is the output vector) + curin = input_vect + A[:,0] = curin + for n in range(1, numorder+1): + curin = r_[[0.0], curin[0:-1]]#prepend a 0 to curin and drop its + #last element + A[:,n] = curin + curout = -output_vect#this is the first output column, but it not + #actually used + firstden = numorder+1 + for n in range(0, denorder): + curout = r_[[0.0], curout[0:-1]] + A[:,firstden+n] = curout + return A + + +def fit_discrete_response(output_vect, input_vect, numorder, denorder): + """Find the coefficients of a digital transfer function that give + the best fit to output_vect in a least squares sense. output_vect + is the output of the system and input_vect is the input. The + input and output vectors are shifted backward in time a maximum of + numorder and denorder steps respectively. Each shifted vector + becomes a column in the matrix for the least squares curve fit of + the form + + output_vect = [A]c + + where [A] is the matrix whose columns are shifted versions of + input_vect and output_vect and c is composed of the numerator and + denominator coefficients of the transfer function. numorder and + denorder are the highest power of z in the numerator or + denominator respectively. + + In essence, the approach is to find the coefficients that best fit + related the input_vect and output_vect according to the difference + equation + + y(k) = b_0 x(k) + b_1 x(k-1) + b_2 x(k-2) + ... + b_m x(k-m) + - a_1 y(k-1) - a_2 y(k-2) - ... - a_n y(k-n) + + where x = input_vect, y = output_vect, m = numorder, and + n = denorder. The unknown coefficient vector is then + + c = [b_0, b_1, b_2, ... , b_m, a_1, a_2, ..., a_n] + + Note that a_0 is forced to be 1. + + The matrix [A] is then composed of [A] = [X(k) X(k-1) X(k-2) + ... Y(k-1) Y(k-2) ...] where X(k-2) represents the input_vect + shifted 2 elements and Y(k-2) represents the output_vect shifted + two elements.""" + A = build_fit_matrix(output_vect, input_vect, numorder, denorder) + fitres = linalg.lstsq(A, output_vect) + x = fitres[0] + numz = x[0:numorder+1] + denz = x[numorder+1:] + denz = r_[[1.0],denz] + return numz, denz + +def prependzeros(num, den): + nd = len(den) + nn = len(num) + if nn < nd: + zvect = zeros(nd-nn) + numout = r_[zvect, num] + else: + numout = num + return numout, den + +def in_with_tol(elem, searchlist, rtol=1e-5, atol=1e-10): + """Determine whether or not elem+/-tol matches an element of + searchlist.""" + for n, item in enumerate(searchlist): + if allclose(item, elem, rtol=rtol, atol=atol): + return n + return -1 + + + +def PolyToLatex(polyin, var='s', fmt='%0.5g', eps=1e-12): + N = polyin.order + clist = polyin.coeffs + outstr = '' + for i, c in enumerate(clist): + curexp = N-i + curcoeff = fmt%c + if curexp > 0: + if curexp == 1: + curs = var + else: + curs = var+'^%i'%curexp + #Handle coeffs of +/- 1 in a special way: + if 1-eps < c < 1+eps: + curcoeff = '' + elif -1-eps < c < -1+eps: + curcoeff = '-' + else: + curs='' + curstr = curcoeff+curs + if c > 0 and outstr: + curcoeff = '+'+curcoeff + if abs(c) > eps: + outstr+=curcoeff+curs + return outstr + + +def polyfactor(num, den, prepend=True, rtol=1e-5, atol=1e-10): + """Factor out any common roots from the polynomials represented by + the vectors num and den and return new coefficient vectors with + any common roots cancelled. + + Because poly1d does not think in terms of z^-1, z^-2, etc. it may + be necessary to add zeros to the beginning of the numpoly coeffs + to represent multiplying through be z^-n where n is the order of + the denominator. If prependzeros is Trus, the numerator and + denominator coefficient vectors will have the same length.""" + numpoly = poly1d(num) + denpoly = poly1d(den) + nroots = roots(numpoly).tolist() + droots = roots(denpoly).tolist() + n = 0 + while n < len(nroots): + curn = nroots[n] + ind = in_with_tol(curn, droots, rtol=rtol, atol=atol) + if ind > -1: + nroots.pop(n) + droots.pop(ind) + #numpoly, rn = polydiv(numpoly, poly(curn)) + #denpoly, rd = polydiv(denpoly, poly(curn)) + else: + n += 1 + numpoly = poly(nroots) + denpoly = poly(droots) + nvect = numpoly + dvect = denpoly + if prepend: + nout, dout = prependzeros(nvect, dvect) + else: + nout = nvect + dout = dvect + return nout, dout + + +def polysubstitute(polyin, numsub, densub): + """Substitute one polynomial into another to support Tustin and + other c2d algorithms of a similar approach. The idea is to make + it easy to substitute + + a z-1 + s = - ----- + T z+1 + + or other forms involving ratios of polynomials for s in a + polynomial of s such as the numerator or denominator of a transfer + function. + + For the tustin example above, numsub=a*(z-1) and densub=T*(z+1), + where numsub and densub are scipy.poly1d instances. + + Note that this approach seems to have substantial floating point + problems.""" + mys = TransferFunction(numsub, densub) + out = 0.0 + no = polyin.order + for n, coeff in enumerate(polyin.coeffs): + curterm = coeff*mys**(no-n) + out = out+curterm + return out + + +def tustin_sub(polyin, T, a=2.0): + numsub = a*poly1d([1.0,-1.0]) + densub = T*poly1d([1.0,1.0]) + out = polysubstitute(polyin, numsub, densub) + out.myvar = 'z' + return out + + +def create_swept_sine_input(maxt, dt, maxf, minf=0.0, deadtime=2.0): + t = arange(0, maxt, dt) + u = sweptsine(t, minf=minf, maxf=maxf) + if deadtime: + deadt = arange(0,deadtime, dt) + zv = zeros_like(deadt) + u = r_[zv, u, zv] + return u + +def create_swept_sine_t(maxt, dt, deadtime=2.0): + t = arange(0, maxt, dt) + if deadtime: + deadt = arange(0,deadtime, dt) + t = t+max(deadt)+dt + tpost = deadt+max(t)+dt + return r_[deadt, t, tpost] + else: + return t + +def ADC(vectin, bits=9, vmax=2.5, vmin=-2.5): + """Simulate the sampling portion of an analog-to-digital + conversion by outputing an integer number of counts associate with + each voltage in vectin.""" + dv = (vmax-vmin)/2**bits + vect2 = clip(vectin, vmin, vmax) + counts = vect2/dv + return counts.astype(int) + + +def CountsToFloat(counts, bits=9, vmax=2.5, vmin=-2.5): + """Convert the integer output of ADC to a floating point number by + mulitplying by dv.""" + dv = (vmax-vmin)/2**bits + return dv*counts + + +def epslist(listin, eps=1.0e-12): + """Make a copy of listin and then check each element of the copy + to see if its absolute value is greater than eps. Set to zero all + elements in the copied list whose absolute values are less than + eps. Return the copied list.""" + listout = copy.deepcopy(listin) + for i in range(len(listout)): + if abs(listout[i])<eps: + listout[i] = 0.0 + return listout + + +def _PlotMatrixvsF(freqvect,matin,linetype='',linewidth=None, semilogx=True, allsolid=False, axis=None): + mykwargs={} + usepylab = False + if axis is None: + import pylab + axis = pylab.gca() + usepylab = True + if len(shape(matin))==1: + myargs=[freqvect,matin] + if linetype: + myargs.append(linetype) + else: + mykwargs.update(_getlinetype(axis)) + if linewidth: + mykwargs['linewidth']=linewidth + if semilogx: + curline,=axis.semilogx(*myargs,**mykwargs) + else: + curline,=axis.plot(*myargs,**mykwargs) + mylines=[curline] +# _inccount() + else: + mylines=[] + for q in range(shape(matin)[1]): + myargs=[freqvect,matin[:,q]] + if linetype: + myargs.append(linetype) + else: + mykwargs.update(_getlinetype(axis)) + if linewidth: + mykwargs['linewidth']=linewidth + if semilogx: + curline,=axis.semilogx(*myargs,**mykwargs) + else: + curline,=axis.plot(*myargs,**mykwargs) + mylines.append(curline) +# _inccount() + return mylines + + +def _PlotMag(freqvect, bodein, linetype='-', linewidth=0, axis=None): + if callable(bodein.dBmag): + myvect=bodein.dBmag() + else: + myvect=bodein.dBmag + return _PlotMatrixvsF(freqvect, myvect, linetype=linetype, linewidth=linewidth, axis=axis) + + +def _PlotPhase(freqvect, bodein, linetype='-', linewidth=0, axis=None): + return _PlotMatrixvsF(freqvect,bodein.phase,linetype=linetype,linewidth=linewidth, axis=axis) + + +def _k_poles(TF,poleloc): + L = TF.num(poleloc)/TF.den(poleloc) + k = 1.0/abs(L) + poles = TF._RLFindRoots([k]) + poles = TF._RLSortRoots(poles) + return k,poles + +def _checkpoles(poleloc,pnew): + evect = abs(poleloc-array(pnew)) + ind = evect.argmin() + pout = pnew[ind] + return pout + + +def _realizable(num, den): + realizable = False + if not isscalar(den): + if isscalar(num): + realizable = True + elif len(den) >= len(num): + realizable = True + return realizable + + +def shape_u(uvect, slope): + u_shaped = zeros_like(uvect) + u_shaped[0] = uvect[0] + + N = len(uvect) + + for n in range(1, N): + diff = uvect[n] - u_shaped[n-1] + if diff > slope: + u_shaped[n] = u_shaped[n-1] + slope + elif diff < -1*slope: + u_shaped[n] = u_shaped[n-1] - slope + else: + u_shaped[n] = uvect[n] + return u_shaped + + +class TransferFunction(signal.lti): + def __setattr__(self, attr, val): + realizable = False + if hasattr(self, 'den') and hasattr(self, 'num'): + realizable = _realizable(self.num, self.den) + if realizable: + signal.lti.__setattr__(self, attr, val) + else: + self.__dict__[attr] = val + + + def __init__(self, num, den, dt=0.01, maxt=5.0, myvar='s'): + """num and den are either scalar constants or lists that are + passed to scipy.poly1d to create a list of coefficients.""" + #print('in TransferFunction.__init__, dt=%s' % dt) + if _realizable(num, den): + signal.lti.__init__(self, num, den) + self.num = poly1d(num) + self.den = poly1d(den) + self.dt = dt + self.myvar = myvar + self.maxt = maxt + + + def __repr__(self, labelstr='controls.TransferFunction'): + nstr=str(self.num)#.strip() + dstr=str(self.den)#.strip() + nstr=nstr.replace('x',self.myvar) + dstr=dstr.replace('x',self.myvar) + n=len(dstr) + m=len(nstr) + shift=(n-m)/2*' ' + nstr=nstr.replace('\n','\n'+shift) + tempstr=labelstr+'\n'+shift+nstr+'\n'+'-'*n+'\n '+dstr + return tempstr + + + def __call__(self,s,optargs=()): + return self.num(s)/self.den(s) + + + def __add__(self,other): + if hasattr(other,'num') and hasattr(other,'den'): + if len(self.den.coeffs)==len(other.den.coeffs) and \ + (self.den.coeffs==other.den.coeffs).all(): + return TransferFunction(self.num+other.num,self.den) + else: + return TransferFunction(self.num*other.den+other.num*self.den,self.den*other.den) + elif isinstance(other, int) or isinstance(other, float): + return TransferFunction(other*self.den+self.num,self.den) + else: + raise ValueError, 'do not know how to add TransferFunction and '+str(other) +' which is of type '+str(type(other)) + + def __radd__(self,other): + return self.__add__(other) + + + def __mul__(self,other): + if isinstance(other, Digital_P_Control): + return self.__class__(other.kp*self.num, self.den) + elif hasattr(other,'num') and hasattr(other,'den'): + if myeq(self.num,other.den) and myeq(self.den,other.num): + return 1 + elif myeq(self.num,other.den): + return self.__class__(other.num,self.den) + elif myeq(self.den,other.num): + return self.__class__(self.num,other.den) + else: + gain = self.gain*other.gain + new_num, new_den = polyfactor(self.num*other.num, \ + self.den*other.den) + newtf = self.__class__(new_num*gain, new_den) + return newtf + elif isinstance(other, int) or isinstance(other, float): + return self.__class__(other*self.num,self.den) + + + def __pow__(self, expon): + """Basically, go self*self*self as many times as necessary. I + haven't thought about negative exponents. I don't think this + would be hard, you would just need to keep dividing by self + until you got the right answer.""" + assert expon >= 0, 'TransferFunction.__pow__ does not yet support negative exponents.' + out = 1.0 + for n in range(expon): + out *= self + return out + + + def __rmul__(self,other): + return self.__mul__(other) + + + def __div__(self,other): + if hasattr(other,'num') and hasattr(other,'den'): + if myeq(self.den,other.den): + return TransferFunction(self.num,other.num) + else: + return TransferFunction(self.num*other.den,self.den*other.num) + elif isinstance(other, int) or isinstance(other, float): + return TransferFunction(self.num,other*self.den) + + + def __rdiv__(self, other): + print('calling TransferFunction.__rdiv__') + return self.__div__(other) + + + def __truediv__(self,other): + return self.__div__(other) + + + def _get_set_dt(self, dt=None): + if dt is not None: + self.dt = float(dt) + return self.dt + + + def ToLatex(self, eps=1e-12, fmt='%0.5g', ds=True): + mynum = self.num + myden = self.den + npart = PolyToLatex(mynum) + dpart = PolyToLatex(myden) + outstr = '\\frac{'+npart+'}{'+dpart+'}' + if ds: + outstr = '\\displaystyle '+outstr + return outstr + + + def RootLocus(self, kvect, fig=None, fignum=1, \ + clear=True, xlim=None, ylim=None, plotstr='-'): + """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.""" + if fig is None: + import pylab + fig = pylab.figure(fignum) + if clear: + fig.clf() + ax = fig.add_subplot(111) + mymat = self._RLFindRoots(kvect) + mymat = self._RLSortRoots(mymat) + #plot open loop poles + poles = array(self.den.r) + ax.plot(real(poles), imag(poles), 'x') + #plot open loop zeros + zeros = array(self.num.r) + if zeros.any(): + ax.plot(real(zeros), imag(zeros), 'o') + for col in mymat.T: + ax.plot(real(col), imag(col), plotstr) + if xlim: + ax.set_xlim(xlim) + if ylim: + ax.set_ylim(ylim) + ax.set_xlabel('Real') + ax.set_ylabel('Imaginary') + return mymat + + + def _RLFindRoots(self, kvect): + """Find the roots for the root locus.""" + roots = [] + for k in kvect: + curpoly = self.den+k*self.num + curroots = curpoly.r + curroots.sort() + roots.append(curroots) + mymat = row_stack(roots) + return mymat + + + def _RLSortRoots(self, mymat): + """Sort the roots from self._RLFindRoots, so that the root + locus doesn't show weird pseudo-branches as roots jump from + one branch to another.""" + sorted = zeros_like(mymat) + for n, row in enumerate(mymat): + if n==0: + sorted[n,:] = row + else: + #sort the current row by finding the element with the + #smallest absolute distance to each root in the + #previous row + available = range(len(prevrow)) + for elem in row: + evect = elem-prevrow[available] + ind1 = abs(evect).argmin() + ind = available.pop(ind1) + sorted[n,ind] = elem + prevrow = sorted[n,:] + return sorted + + + def opt(self, kguess): + pnew = self._RLFindRoots(kguess) + pnew = self._RLSortRoots(pnew)[0] + if len(pnew)>1: + pnew = _checkpoles(self.poleloc,pnew) + e = abs(pnew-self.poleloc)**2 + return sum(e) + + + def rlocfind(self, poleloc): + self.poleloc = poleloc + kinit,pinit = _k_poles(self,poleloc) + k = optimize.fmin(self.opt,[kinit])[0] + poles = self._RLFindRoots([k]) + poles = self._RLSortRoots(poles) + return k, poles + + + def PlotTimeResp(self, u, t, fig, clear=True, label='model', mysub=111): + ax = fig.add_subplot(mysub) + if clear: + ax.cla() + try: + y = self.lsim(u, t) + except: + y = self.lsim2(u, t) + ax.plot(t, y, label=label) + return ax + + +## def BodePlot(self, f, fig, clear=False): +## mtf = self.FreqResp( +## ax1 = fig.axes[0] +## ax1.semilogx(modelf,20*log10(abs(mtf))) +## mphase = angle(mtf, deg=1) +## ax2 = fig.axes[1] +## ax2.semilogx(modelf, mphase) + + + def SimpleFactor(self): + mynum=self.num + myden=self.den + dsf=myden[myden.order] + nsf=mynum[mynum.order] + sden=myden/dsf + snum=mynum/nsf + poles=sden.r + residues=zeros(shape(sden.r),'D') + factors=[] + for x,p in enumerate(poles): + polearray=poles.copy() + polelist=polearray.tolist() + mypole=polelist.pop(x) + tempden=1.0 + for cp in polelist: + tempden=tempden*(poly1d([1,-cp])) + tempTF=TransferFunction(snum,tempden) + curres=tempTF(mypole) + residues[x]=curres + curTF=TransferFunction(curres,poly1d([1,-mypole])) + factors.append(curTF) + return factors,nsf,dsf + + def factor_constant(self, const): + """Divide numerator and denominator coefficients by const""" + self.num = self.num/const + self.den = self.den/const + + def lsim(self, u, t, interp=0, returnall=False, X0=None): + """Find the response of the TransferFunction to the input u + with time vector t. Uses signal.lsim. + + return y the response of the system.""" + if returnall:#most users will just want the system output y, + #but some will need the (t, y, x) tuple that + #signal.lsim returns + return signal.lsim(self, u, t, interp=interp, X0=X0) + else: + return signal.lsim(self, u, t, interp=interp, X0=X0)[1] + + def lsim2(self, u, t, returnall=False, X0=None): + #tempsys=signal.lti(self.num,self.den) + if returnall: + return signal.lsim2(self, u, t, X0=X0) + else: + return signal.lsim2(self, u, t, X0=X0)[1] + + + def residue(self, tol=1e-3, verbose=0): + """from scipy.signal.residue: + + Compute residues/partial-fraction expansion of b(s) / a(s). + + If M = len(b) and N = len(a) + + b(s) b[0] s**(M-1) + b[1] s**(M-2) + ... + b[M-1] + H(s) = ------ = ---------------------------------------------- + a(s) a[0] s**(N-1) + a[1] s**(N-2) + ... + a[N-1] + + r[0] r[1] r[-1] + = -------- + -------- + ... + --------- + k(s) + (s-p[0]) (s-p[1]) (s-p[-1]) + + If there are any repeated roots (closer than tol), then the + partial fraction expansion has terms like + + r[i] r[i+1] r[i+n-1] + -------- + ----------- + ... + ----------- + (s-p[i]) (s-p[i])**2 (s-p[i])**n + + returns r, p, k + """ + r,p,k = signal.residue(self.num, self.den, tol=tol) + if verbose>0: + print('r='+str(r)) + print('') + print('p='+str(p)) + print('') + print('k='+str(k)) + + return r, p, k + + + def PartFrac(self, eps=1.0e-12): + """Compute the partial fraction expansion based on the residue + command. In the final polynomials, coefficients whose + absolute values are less than eps are set to zero.""" + r,p,k = self.residue() + + rlist = r.tolist() + plist = p.tolist() + + N = len(rlist) + + tflist = [] + eps = 1e-12 + + while N > 0: + curr = rlist.pop(0) + curp = plist.pop(0) + if abs(curp.imag) < eps: + #This is a purely real pole. The portion of the partial + #fraction expansion corresponding to this pole is curr/(s-curp) + curtf = TransferFunction(curr,[1,-curp]) + else: + #this is a complex pole and we need to find its conjugate and + #handle them together + cind = plist.index(curp.conjugate()) + rconj = rlist.pop(cind) + pconj = plist.pop(cind) + p1 = poly1d([1,-curp]) + p2 = poly1d([1,-pconj]) + #num = curr*p2+rconj*p1 + Nr = curr.real + Ni = curr.imag + Pr = curp.real + Pi = curp.imag + numlist = [2.0*Nr,-2.0*(Nr*Pr+Ni*Pi)] + numlist = epslist(numlist, eps) + num = poly1d(numlist) + denlist = [1, -2.0*Pr,Pr**2+Pi**2] + denlist = epslist(denlist, eps) + den = poly1d(denlist) + curtf = TransferFunction(num,den) + tflist.append(curtf) + N = len(rlist) + return tflist + + + def FreqResp(self, f, fignum=1, fig=None, clear=True, \ + grid=True, legend=None, legloc=1, legsub=1, **kwargs): + """Compute the frequency response of the transfer function + using the frequency vector f, returning a complex vector. + + The frequency response (Bode plot) will be plotted on + figure(fignum) unless fignum=None. + + legend should be a list of legend entries if a legend is + desired. If legend is not None, the legend will be placed on + the top half of the plot (magnitude portion) if legsub=1, or + on the bottom half with legsub=2. legloc follows the same + rules as the pylab legend command (1 is top right and goes + counter-clockwise from there.)""" + testvect=real(f)==0 + if testvect.all(): + s=f#then you really sent me s and not f + else: + s=2.0j*pi*f + self.comp = self.num(s)/self.den(s) + self.dBmag = 20*log10(abs(self.comp)) + self.phase = angle(self.comp,1) + + if fig is None: + if fignum is not None: + import pylab + fig = pylab.figure(fignum) + + if fig is not None: + if clear: + fig.clf() + + if fig is not None: + myargs=['linetype','colors','linewidth'] + subkwargs={} + for key in myargs: + if kwargs.has_key(key): + subkwargs[key]=kwargs[key] + if clear: + fig.clf() + ax1 = fig.add_subplot(2,1,1) + #if clear: + # ax1.cla() + myind=ax1._get_lines.count + mylines=_PlotMag(f, self, axis=ax1, **subkwargs) + ax1.set_ylabel('Mag. Ratio (dB)') + ax1.xaxis.set_major_formatter(MyFormatter()) + if grid: + ax1.grid(1) + if legend is not None and legsub==1: + ax1.legend(legend, legloc) + ax2 = fig.add_subplot(2,1,2, sharex=ax1) + #if clear: + # ax2.cla() + mylines=_PlotPhase(f, self, axis=ax2, **subkwargs) + ax2.set_ylabel('Phase (deg.)') + ax2.set_xlabel('Freq. (Hz)') + ax2.xaxis.set_major_formatter(MyFormatter()) + if grid: + ax2.grid(1) + if legend is not None and legsub==2: + ax2.legend(legend, legloc) + return self.comp + + + def CrossoverFreq(self, f): + if not hasattr(self, 'dBmag'): + self.FreqResp(f, fignum=None) + t1 = squeeze(self.dBmag > 0.0) + t2 = r_[t1[1:],t1[0]] + t3 = (t1 & -t2) + myinds = where(t3)[0] + if not myinds.any(): + return None, [] + maxind = max(myinds) + return f[maxind], maxind + + + def PhaseMargin(self,f): + fc,ind=self.CrossoverFreq(f) + if not fc: + return 180.0 + return 180.0+squeeze(self.phase[ind]) + + + def create_tvect(self, dt=None, maxt=None): + if dt is None: + dt = self.dt + else: + self.dt = dt + assert dt is not None, "You must either pass in a dt or call create_tvect on an instance with a self.dt already defined." + if maxt is None: + if hasattr(self,'maxt'): + maxt = self.maxt + else: + maxt = 100*dt + else: + self.maxt = maxt + tvect = arange(0,maxt+dt/2.0,dt) + self.t = tvect + return tvect + + + def create_impulse(self, dt=None, maxt=None, imp_time=0.5): + """Create the input impulse vector to be used in least squares + curve fitting of the c2d function.""" + if dt is None: + dt = self.dt + indon = int(imp_time/dt) + tvect = self.create_tvect(dt=dt, maxt=maxt) + imp = zeros_like(tvect) + imp[indon] = 1.0 + return imp + + + def create_step_input(self, dt=None, maxt=None, indon=5): + """Create the input impulse vector to be used in least squares + curve fitting of the c2d function.""" + tvect = self.create_tvect(dt=dt, maxt=maxt) + mystep = zeros_like(tvect) + mystep[indon:] = 1.0 + return mystep + + + def step_response(self, t=None, dt=None, maxt=None, \ + step_time=None, fignum=1, clear=True, \ + plotu=False, amp=1.0, interp=0, fig=None, \ + fmts=['-','-'], legloc=0, returnall=0, \ + legend=None, **kwargs): + """Find the response of the system to a step input. If t is + not given, then the time vector will go from 0 to maxt in + steps of dt i.e. t=arange(0,maxt,dt). If dt and maxt are not + given, the parameters from the TransferFunction instance will + be used. + + step_time is the time when the step input turns on. If not + given, it will default to 0. + + If clear is True, the figure will be cleared first. + clear=False could be used to overlay the step responses of + multiple TransferFunction's. + + plotu=True means that the step input will also be shown on the + graph. + + amp is the amplitude of the step input. + + return y unless returnall is set then return y, t, u + + where y is the response of the transfer function, t is the + time vector, and u is the step input vector.""" + if t is not None: + tvect = t + else: + tvect = self.create_tvect(dt=dt, maxt=maxt) + u = zeros_like(tvect) + if dt is None: + dt = self.dt + if step_time is None: + step_time = 0.0 + #step_time = 0.1*tvect.max() + if kwargs.has_key('indon'): + indon = kwargs['indon'] + else: + indon = int(step_time/dt) + u[indon:] = amp + try: + ystep = self.lsim(u, tvect, interp=interp)#[1]#the outputs of lsim are (t, y,x) + except: + ystep = self.lsim2(u, tvect)#[1] + + if fig is None: + if fignum is not None: + import pylab + fig = pylab.figure(fignum) + + if fig is not None: + if clear: + fig.clf() + ax = fig.add_subplot(111) + if plotu: + leglist =['Input','Output'] + ax.plot(tvect, u, fmts[0], linestyle='steps', **kwargs)#assume step input wants 'steps' linestyle + ofmt = fmts[1] + else: + ofmt = fmts[0] + ax.plot(tvect, ystep, ofmt, **kwargs) + ax.set_ylabel('Step Response') + ax.set_xlabel('Time (sec)') + if legend is not None: + ax.legend(legend, loc=legloc) + elif plotu: + ax.legend(leglist, loc=legloc) + #return ystep, ax + #else: + #return ystep + if returnall: + return ystep, tvect, u + else: + return ystep + + + + def impulse_response(self, dt=None, maxt=None, fignum=1, \ + clear=True, amp=1.0, fig=None, \ + fmt='-', **kwargs): + """Find the impulse response of the system using + scipy.signal.impulse. + + The time vector will go from 0 to maxt in steps of dt + i.e. t=arange(0,maxt,dt). If dt and maxt are not given, the + parameters from the TransferFunction instance will be used. + + If clear is True, the figure will be cleared first. + clear=False could be used to overlay the impulse responses of + multiple TransferFunction's. + + amp is the amplitude of the impulse input. + + return y, t + + where y is the impulse response of the transfer function and t + is the time vector.""" + + tvect = self.create_tvect(dt=dt, maxt=maxt) + temptf = amp*self + tout, yout = temptf.impulse(T=tvect) + + if fig is None: + if fignum is not None: + import pylab + fig = pylab.figure(fignum) + + if fig is not None: + if clear: + fig.clf() + ax = fig.add_subplot(111) + ax.plot(tvect, yout, fmt, **kwargs) + ax.set_ylabel('Impulse Response') + ax.set_xlabel('Time (sec)') + + return yout, tout + + + def swept_sine_response(self, maxf, minf=0.0, dt=None, maxt=None, deadtime=2.0, interp=0): + u = create_swept_sine_input(maxt, dt, maxf, minf=minf, deadtime=deadtime) + t = create_swept_sine_t(maxt, dt, deadtime=deadtime) + ysweep = self.lsim(u, t, interp=interp) + return t, u, ysweep + + + def _c2d_sub(self, numsub, densub, scale): + """This method performs substitutions for continuous to + digital conversions using the form: + + numsub + s = scale* -------- + densub + + where scale is a floating point number and numsub and densub + are poly1d instances. + + For example, scale = 2.0/T, numsub = poly1d([1,-1]), and + densub = poly1d([1,1]) for a Tustin c2d transformation.""" + m = self.num.order + n = self.den.order + mynum = 0.0 + for p, coeff in enumerate(self.num.coeffs): + mynum += poly1d(coeff*(scale**(m-p))*((numsub**(m-p))*(densub**(n-(m-p))))) + myden = 0.0 + for p, coeff in enumerate(self.den.coeffs): + myden += poly1d(coeff*(scale**(n-p))*((numsub**(n-p))*(densub**(n-(n-p))))) + return mynum.coeffs, myden.coeffs + + + def c2d_tustin(self, dt=None, a=2.0): + """Convert a continuous time transfer function into a digital + one by substituting + + a z-1 + s = - ----- + T z+1 + + into the compensator, where a is typically 2.0""" + #print('in TransferFunction.c2d_tustin, dt=%s' % dt) + dt = self._get_set_dt(dt) + #print('in TransferFunction.c2d_tustin after _get_set_dt, dt=%s' % dt) + scale = a/dt + numsub = poly1d([1.0,-1.0]) + densub = poly1d([1.0,1.0]) + mynum, myden = self._c2d_sub(numsub, densub, scale) + mynum = mynum/myden[0] + myden = myden/myden[0] + return mynum, myden + + + + def c2d(self, dt=None, maxt=None, method='zoh', step_time=0.5, a=2.0): + """Find a numeric approximation of the discrete transfer + function of the system. + + The general approach is to find the response of the system + using lsim and fit a discrete transfer function to that + response as a least squares problem. + + dt is the time between discrete time intervals (i.e. the + sample time). + + maxt is the length of time for which to calculate the system + respnose. An attempt is made to guess an appropriate stopping + time if maxt is None. For now, this defaults to 100*dt, + assuming that dt is appropriate for the system poles. + + method is a string describing the c2d conversion algorithm. + method = 'zoh refers to a zero-order hold for a sampled-data + system and follows the approach outlined by Dorsey in section + 14.17 of + "Continuous and Discrete Control Systems" summarized on page + 472 of the 2002 edition. + + Other supported options for method include 'tustin' + + indon gives the index of when the step input should switch on + for zoh or when the impulse should happen otherwise. There + should probably be enough zero entries before the input occurs + to accomidate the order of the discrete transfer function. + + a is used only if method = 'tustin' and it is substituted in the form + + a z-1 + s = - ----- + T z+1 + + a is almost always equal to 2. + """ + if method.lower() == 'zoh': + ystep = self.step_response(dt=dt, maxt=maxt, step_time=step_time)[0] + myimp = self.create_impulse(dt=dt, maxt=maxt, imp_time=step_time) + #Pdb().set_trace() + print('You called c2d with "zoh". This is most likely bad.') + nz, dz = fit_discrete_response(ystep, myimp, self.den.order, self.den.order+1)#we want the numerator order to be one less than the denominator order - the denominator order +1 is the order of the denominator during a step response + #multiply by (1-z^-1) + nz2 = r_[nz, [0.0]] + nzs = r_[[0.0],nz] + nz3 = nz2 - nzs + nzout, dzout = polyfactor(nz3, dz) + return nzout, dzout + #return nz3, dz + elif method.lower() == 'tustin': + #The basic approach for tustin is to create a transfer + #function that represents s mapped into z and then + #substitute this s(z)=a/T*(z-1)/(z+1) into the continuous + #transfer function + return self.c2d_tustin(dt=dt, a=a) + else: + raise ValueError, 'c2d method not understood:'+str(method) + + + + def DigitalSim(self, u, method='zoh', bits=9, vmin=-2.5, vmax=2.5, dt=None, maxt=None, digitize=True): + """Simulate the digital reponse of the transfer to input u. u + is assumed to be an input signal that has been sampled with + frequency 1/dt. u is further assumed to be a floating point + number with precision much higher than bits. u will be + digitized over the range [min, max], which is broken up into + 2**bits number of bins. + + The A and B vectors from c2d conversion will be found using + method, dt, and maxt. Note that maxt is only used for + method='zoh'. + + Once A and B have been found, the digital reponse of the + system to the digitized input u will be found.""" + B, A = self.c2d(dt=dt, maxt=maxt, method=method) + assert A[0]==1.0, "A[0]!=1 in c2d result, A="+str(A) + uvect = zeros(len(B), dtype='d') + yvect = zeros(len(A)-1, dtype='d') + if digitize: + udig = ADC(u, bits, vmax=vmax, vmin=vmin) + dv = (vmax-vmin)/(2**bits-1) + else: + udig = u + dv = 1.0 + Ydig = zeros(len(u), dtype='d') + for n, u0 in enumerate(udig): + uvect = shift(uvect, u0) + curY = dot(uvect,B) + negpart = dot(yvect,A[1:]) + curY -= negpart + if digitize: + curY = int(curY) + Ydig[n] = curY + yvect = shift(yvect, curY) + return Ydig*dv + + + +class Input(TransferFunction): + def __repr__(self): + return TransferFunction.__repr__(self, labelstr='controls.Input') + + +class Compensator(TransferFunction): + def __init__(self, num, den, *args, **kwargs): + #print('in Compensator.__init__') + #Pdb().set_trace() + TransferFunction.__init__(self, num, den, *args, **kwargs) + + + def c2d(self, dt=None, a=2.0): + """Compensators should use Tustin for c2d conversion. This + method is just and alias for TransferFunction.c2d_tustin""" + #print('in Compensators.c2d, dt=%s' % dt) + #Pdb().set_trace() + return TransferFunction.c2d_tustin(self, dt=dt, a=a) + + def __repr__(self): + return TransferFunction.__repr__(self, labelstr='controls.Compensator') + + + +class Digital_Compensator(object): + def __init__(self, num, den, input_vect=None, output_vect=None): + self.num = num + self.den = den + self.input = input_vect + self.output = output_vect + self.Nnum = len(self.num) + self.Nden = len(self.den) + + + def calc_out(self, i): + out = 0.0 + for n, bn in enumerate(self.num): + out += self.input[i-n]*bn + + for n in range(1, self.Nden): + out -= self.output[i-n]*self.den[n] + out = out/self.den[0] + return out + + +class Digital_PI(object): + def __init__(self, kp, ki, input_vect=None, output_vect=None): + self.kp = kp + self.ki = ki + self.input = input_vect + self.output = output_vect + self.esum = 0.0 + + + def prep(self): + self.esum = zeros_like(self.input) + + + def calc_out(self, i): + self.esum[i] = self.esum[i-1]+self.input[i] + out = self.input[i]*self.kp+self.esum[i]*self.ki + return out + + +class Digital_P_Control(Digital_Compensator): + def __init__(self, kp, input_vect=None, output_vect=None): + self.kp = kp + self.input = input_vect + self.output = output_vect + self.num = poly1d([kp]) + self.den = poly1d([1]) + self.gain = 1 + + def calc_out(self, i): + self.output[i] = self.kp*self.input[i] + return self.output[i] + + +def dig_comp_from_c_comp(c_comp, dt): + """Convert a continuous compensator into a digital one using Tustin + and sampling time dt.""" + b, a = c_comp.c2d_tustin(dt=dt) + return Digital_Compensator(b, a) + + +class FirstOrderCompensator(Compensator): + def __init__(self, K, z, p, dt=0.004): + """Create a first order compensator whose transfer function is + + K*(s+z) + D(s) = ----------- + (s+p) """ + Compensator.__init__(self, K*poly1d([1,z]), [1,p]) + + + def __repr__(self): + return TransferFunction.__repr__(self, labelstr='controls.FirstOrderCompensator') + + + def ToPSoC(self, dt=0.004): + b, a = self.c2d(dt=dt) + outstr = 'v = %f*e%+f*ep%+f*vp;'%(b[0],b[1],-a[1]) + print('PSoC str:') + print(outstr) + return outstr + + +def sat(vin, vmax=2.0): + if vin > vmax: + return vmax + elif vin < -1*vmax: + return -1*vmax + else: + return vin + + + +class Closed_Loop_System_with_Sat(object): + def __init__(self, plant_tf, Kp, sat): + self.plant_tf = plant_tf + self.Kp = Kp + self.sat = sat + + + def lsim(self, u, t, X0=None, include_sat=True, \ + returnall=0, lsim2=0, verbosity=0): + dt = t[1]-t[0] + if X0 is None: + X0 = zeros((2,len(self.plant_tf.den.coeffs)-1)) + N = len(t) + y = zeros(N) + v = zeros(N) + x_n = X0 + for n in range(1,N): + t_n = t[n] + if verbosity > 0: + print('t_n='+str(t_n)) + e = u[n]-y[n-1] + v_n = self.Kp*e + if include_sat: + v_n = sat(v_n, vmax=self.sat) + #simulate for one dt using ZOH + if lsim2: + t_nn, y_n, x_n = self.plant_tf.lsim2([v_n,v_n], [t_n, t_n+dt], X0=x_n[-1], returnall=1) + else: + t_nn, y_n, x_n = self.plant_tf.lsim([v_n,v_n], [t_n, t_n+dt], X0=x_n[-1], returnall=1) + + y[n] = y_n[-1] + v[n] = v_n + self.y = y + self.v = v + self.u = u + if returnall: + return y, v + else: + return y + + + + + +def step_input(): + return Input(1,[1,0]) + + +def feedback(olsys,H=1): + """Calculate the closed-loop transfer function + + olsys + cltf = -------------- + 1+H*olsys + + where olsys is the transfer function of the open loop + system (Gc*Gp) and H is the transfer function in the feedback + loop (H=1 for unity feedback).""" + clsys=olsys/(1.0+H*olsys) + return clsys + + + +def Usweep(ti,maxt,minf=0.0,maxf=10.0): + """Return the current value (scalar) of a swept sine signal - must be used + with list comprehension to generate a vector. + + ti - current time (scalar) + minf - lowest frequency in the sweep + maxf - highest frequency in the sweep + maxt - T or the highest value in the time vector""" + if ti<0.0: + return 0.0 + else: + curf=(maxf-minf)*ti/maxt+minf + if ti<(maxt*0.95): + return sin(2*pi*curf*ti) + else: + return 0.0 + + +def sweptsine(t,minf=0.0, maxf=10.0): + """Generate a sweptsine vector by calling Usweep for each ti in t.""" + T=max(t)-min(t) + Us = [Usweep(ti,T,minf,maxf) for ti in t] + return array(Us) + + +mytypes=['-','--',':','-.'] +colors=['b','y','r','g','c','k']#['y','b','r','g','c','k'] + +def _getlinetype(ax=None): + if ax is None: + import pylab + ax = pylab.gca() + myind=ax._get_lines.count + return {'color':colors[myind % len(colors)],'linestyle':mytypes[myind % len(mytypes)]} + + +def create_step_vector(t, step_time=0.0, amp=1.0): + u = zeros_like(t) + dt = t[1]-t[0] + indon = int(step_time/dt) + u[indon:] = amp + return u + + +def rate_limiter(uin, du): + uout = zeros_like(uin) + N = len(uin) + for n in range(1,N): + curchange = uin[n]-uout[n-1] + if curchange > du: + uout[n] = uout[n-1]+du + elif curchange < -du: + uout[n] = uout[n-1]-du + else: + uout[n] = uin[n] + return uout + + + + This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |