4343# RMM, 2 April 2011: modified to work with new LTI structure (see ChangeLog)
4444# * Not tested: should still work on signal.ltisys objects
4545#
46+ # GDM, 16 February 2017: add smart selection of gains based on axis.
47+ # * Add gains up to a tolerance is achieved
48+ # * Change some variables and functions names ir order to improve "code style"
49+ #
50+ #
4651# $Id$
4752
4853# Packages used by this module
54+ from functools import partial
55+
4956import numpy as np
57+ import pylab # plotting routines
58+ import scipy .signal # signal processing toolbox
5059from scipy import array , poly1d , row_stack , zeros_like , real , imag
51- import scipy .signal # signal processing toolbox
52- import pylab # plotting routines
53- from .xferfcn import _convertToTransferFunction
60+
61+ from . import xferfcn
5462from .exception import ControlMIMONotImplemented
55- from functools import partial
5663
5764__all__ = ['root_locus' , 'rlocus' ]
5865
66+
5967# Main function: compute a root locus diagram
60- def root_locus (sys , kvect = None , xlim = None , ylim = None , plotstr = '-' , Plot = True ,
61- PrintGain = True ):
68+ def root_locus (dinsys , kvect = None , xlim = None , ylim = None , plotstr = '-' , plot = True ,
69+ print_gain = True ):
6270 """Root locus plot
6371
6472 Calculate the root locus by finding the roots of 1+k*TF(s)
6573 where TF is self.num(s)/self.den(s) and each k is an element
66- of kvect .
74+ of gvect .
6775
6876 Parameters
6977 ----------
70- sys : LTI object
78+ dinsys : LTI object
7179 Linear input/output systems (SISO only, for now)
7280 kvect : list or ndarray, optional
7381 List of gains to use in computing diagram
7482 xlim : tuple or list, optional
7583 control of x-axis range, normally with tuple (see matplotlib.axes)
7684 ylim : tuple or list, optional
7785 control of y-axis range
78- Plot : boolean, optional (default = True)
86+ plot : boolean, optional (default = True)
7987 If True, plot magnitude and phase
80- PrintGain : boolean (default = True)
88+ print_gain : boolean (default = True)
8189 If True, report mouse clicks when close to the root-locus branches,
8290 calculate gain, damping and print
91+ plotstr: string that declare of the rlocus (see matplotlib)
8392
8493 Returns
8594 -------
@@ -90,21 +99,74 @@ def root_locus(sys, kvect=None, xlim=None, ylim=None, plotstr='-', Plot=True,
9099 """
91100
92101 # Convert numerator and denominator to polynomials if they aren't
93- (nump , denp ) = _systopoly1d (sys )
102+ (nump , denp ) = _systopoly1d (dinsys )
94103
95104 if kvect is None :
96- kvect = _default_gains (sys )
105+ gvect = _default_gains (nump , denp )
106+ else :
107+ gvect = np .asarray (kvect )
97108
98109 # Compute out the loci
99- mymat = _RLFindRoots (sys , kvect )
100- mymat = _RLSortRoots (sys , mymat )
110+ mymat = _find_roots (dinsys , gvect )
111+ mymat = _sort_roots (mymat )
112+
113+ # set smoothing tolerance
114+ smtolx = 0.01 * (np .max (np .max (np .real (mymat ))) - np .min (np .min (np .real (mymat ))))
115+ smtoly = 0.01 * (np .max (np .max (np .imag (mymat ))) - np .min (np .min (np .imag (mymat ))))
116+ smtol = np .max (smtolx , smtoly )
117+
118+ if ~ (xlim is None ):
119+ xmin = np .min (np .min (np .real (mymat )))
120+ xmax = np .max (np .max (np .real (mymat )))
121+ deltax = (xmax - xmin ) * 0.02
122+ xlim = [xmin - deltax , xmax + deltax ]
123+
124+ if ~ (ylim is None ):
125+ ymin = np .min (np .min (np .imag (mymat )))
126+ ymax = np .max (np .max (np .imag (mymat )))
127+ deltay = (ymax - ymin ) * 0.02
128+ ylim = [ymin - deltay , ymax + deltay ]
129+
130+ done = False
131+ ngain = gvect .size
132+
133+ while ~ done & (ngain < 2000 ) & (kvect is None ):
134+ done = True
135+ dp = np .abs (np .diff (mymat , axis = 0 ))
136+ dp = np .max (dp , axis = 1 )
137+ idx = np .where (dp > smtol )
138+
139+ for ii in np .arange (0 , idx [0 ].size ):
140+ i1 = idx [0 ][ii ]
141+ g1 = gvect [i1 ]
142+ p1 = mymat [i1 ]
143+
144+ i2 = idx [0 ][ii ] + 1
145+ g2 = gvect [i2 ]
146+ p2 = mymat [i2 ]
147+ # isolate poles in p1, p2
148+ if np .max (np .abs (p2 - p1 )) > smtol :
149+ newg = np .linspace (g1 , g2 , 5 )
150+ newmymat = _find_roots (dinsys , newg )
151+ gvect = np .insert (gvect , i1 + 1 , newg [1 :4 ])
152+ mymat = np .insert (mymat , i1 + 1 , newmymat [1 :4 ], axis = 0 )
153+ mymat = _sort_roots (mymat )
154+ done = False # need to process new gains
155+ ngain = gvect .size
156+ if kvect is None :
157+ newg = np .linspace (gvect [- 1 ], gvect [- 1 ] * 200 , 5 )
158+ newmymat = _find_roots (dinsys , newg )
159+ gvect = np .append (gvect , newg [1 :5 ])
160+ mymat = np .concatenate ((mymat , newmymat [1 :5 ]), axis = 0 )
161+ mymat = _sort_roots (mymat )
162+ kvect = gvect
101163
102164 # Create the plot
103- if ( Plot ) :
165+ if plot :
104166 f = pylab .figure ()
105- if PrintGain :
167+ if print_gain :
106168 f .canvas .mpl_connect (
107- 'button_release_event' , partial (_RLFeedbackClicks , sys = sys ))
169+ 'button_release_event' , partial (_feedback_clicks , sys = dinsys ))
108170 ax = pylab .axes ()
109171
110172 # plot open loop poles
@@ -121,49 +183,98 @@ def root_locus(sys, kvect=None, xlim=None, ylim=None, plotstr='-', Plot=True,
121183 ax .plot (real (col ), imag (col ), plotstr )
122184
123185 # Set up plot axes and labels
124- if xlim :
125- ax .set_xlim (xlim )
126- if ylim :
127- ax .set_ylim (ylim )
186+ ax .set_xlim (xlim )
187+ ax .set_ylim (ylim )
128188 ax .set_xlabel ('Real' )
129189 ax .set_ylabel ('Imaginary' )
130190
131191 return mymat , kvect
132192
133- def _default_gains (sys ):
134- # TODO: update with a smart calculation of the gains using sys poles/zeros
135- return np .logspace (- 3 , 3 )
193+
194+ def _default_gains (num , den ):
195+ """Insert gains up to a tolerance is achieved. This tolerance is a function of figure axis """
196+ nas = den .order - num .order # number of asymptotes
197+ maxk = 0
198+ olpol = den .roots
199+ olzer = num .roots
200+ if nas > 0 :
201+ cas = (sum (den .roots ) - sum (num .roots )) / nas
202+ angles = (2 * np .arange (1 , nas + 1 ) - 1 ) * np .pi / nas
203+ # print("rlocus: there are %d asymptotes centered at %f\n", nas, cas)
204+ else :
205+ cas = []
206+ angles = []
207+ maxk = 100 * den (1 ) / num (1 )
208+ k_break , real_ax_pts = _break_points (num , den )
209+ if nas == 0 :
210+ maxk = np .max ([1 , 2 * maxk ]) # get at least some root locus
211+ else :
212+ # get distance from breakpoints, poles, and zeros to center of asymptotes
213+ dmax = 3 * np .max (np .abs (np .concatenate ((np .concatenate ((olzer , olpol ), axis = 0 ),
214+ real_ax_pts ), axis = 0 ) - cas ))
215+ if dmax == 0 :
216+ dmax = 1
217+ # get gain for dmax along each asymptote, adjust maxk if necessary
218+ svals = cas + dmax * np .exp (angles * 1j )
219+ kvals = - den (svals ) / num (svals )
220+
221+ if k_break .size > 0 :
222+ maxk = np .max (np .max (k_break ), maxk )
223+ maxk = np .max ([maxk , np .max (np .real (kvals ))])
224+ mink = 0
225+ ngain = 30
226+ gvec = np .linspace (mink , maxk , ngain )
227+ gvec = np .concatenate ((gvec , k_break ), axis = 0 )
228+ gvec .sort ()
229+ return gvec
230+
231+
232+ def _break_points (num , den ):
233+ """Extract the break points over real axis and the gains that give these location"""
234+ # type: (np.poly1d, np.poly1d) -> (np.array, np.array)
235+ dnum = num .deriv (m = 1 )
236+ dden = den .deriv (m = 1 )
237+ brkp = np .poly1d (np .convolve (den , dnum ) - np .convolve (num , dden ))
238+ real_ax_pts = brkp .r
239+ real_ax_pts = real_ax_pts [np .imag (real_ax_pts ) == 0 ]
240+ real_ax_pts = real_ax_pts [num (real_ax_pts ) != 0 ] # avoid dividing by zero
241+ k_break = - den (real_ax_pts ) / num (real_ax_pts )
242+ idx = k_break >= 0
243+ k_break = k_break [idx ]
244+ real_ax_pts = real_ax_pts [idx ]
245+ return k_break , real_ax_pts
246+
136247
137248# Utility function to extract numerator and denominator polynomials
138249def _systopoly1d (sys ):
139250 """Extract numerator and denominator polynomails for a system"""
140251 # Allow inputs from the signal processing toolbox
141- if ( isinstance (sys , scipy .signal .lti ) ):
252+ if isinstance (sys , scipy .signal .lti ):
142253 nump = sys .num
143254 denp = sys .den
144255
145256 else :
146257 # Convert to a transfer function, if needed
147- sys = _convertToTransferFunction (sys )
258+ sys = xferfcn . convertToTransferFunction (sys )
148259
149260 # Make sure we have a SISO system
150- if ( sys .inputs > 1 or sys .outputs > 1 ) :
261+ if sys .inputs > 1. or sys .outputs > 1. :
151262 raise ControlMIMONotImplemented ()
152263
153264 # Start by extracting the numerator and denominator from system object
154265 nump = sys .num [0 ][0 ]
155266 denp = sys .den [0 ][0 ]
156267
157268 # Check to see if num, den are already polynomials; otherwise convert
158- if ( not isinstance (nump , poly1d ) ):
269+ if not isinstance (nump , poly1d ):
159270 nump = poly1d (nump )
160- if ( not isinstance (denp , poly1d ) ):
271+ if not isinstance (denp , poly1d ):
161272 denp = poly1d (denp )
162273
163- return ( nump , denp )
274+ return nump , denp
164275
165276
166- def _RLFindRoots (sys , kvect ):
277+ def _find_roots (sys , kvect ):
167278 """Find the roots for the root locus."""
168279
169280 # Convert numerator and denominator to polynomials if they aren't
@@ -179,36 +290,37 @@ def _RLFindRoots(sys, kvect):
179290 return mymat
180291
181292
182- def _RLSortRoots ( sys , mymat ):
183- """Sort the roots from sys._RLFindRoots , so that the root
293+ def _sort_roots ( mymat ):
294+ """Sort the roots from sys._sort_roots , so that the root
184295 locus doesn't show weird pseudo-branches as roots jump from
185296 one branch to another."""
186297
187- sorted = zeros_like (mymat )
298+ sorted_roots = zeros_like (mymat )
188299 for n , row in enumerate (mymat ):
189300 if n == 0 :
190- sorted [n , :] = row
301+ sorted_roots [n , :] = row
191302 else :
192303 # sort the current row by finding the element with the
193304 # smallest absolute distance to each root in the
194305 # previous row
195306 available = list (range (len (prevrow )))
196307 for elem in row :
197- evect = elem - prevrow [available ]
308+ evect = elem - prevrow [available ]
198309 ind1 = abs (evect ).argmin ()
199310 ind = available .pop (ind1 )
200- sorted [n , ind ] = elem
201- prevrow = sorted [n , :]
202- return sorted
311+ sorted_roots [n , ind ] = elem
312+ prevrow = sorted_roots [n , :]
313+ return sorted_roots
203314
204315
205- def _RLFeedbackClicks (event , sys ):
316+ def _feedback_clicks (event , sys ):
206317 """Print root-locus gain feedback for clicks on the root-locus plot
207318 """
208319 s = complex (event .xdata , event .ydata )
209- K = - 1. / sys .horner (s )
210- if abs (K .real ) > 1e-8 and abs (K .imag / K .real ) < 0.04 :
320+ k = - 1. / sys .horner (s )
321+ if abs (k .real ) > 1e-8 and abs (k .imag / k .real ) < 0.04 :
211322 print ("Clicked at %10.4g%+10.4gj gain %10.4g damp %10.4g" %
212- (s .real , s .imag , K .real , - 1 * s .real / abs (s )))
323+ (s .real , s .imag , k .real , - 1 * s .real / abs (s )))
324+
213325
214- rlocus = root_locus
326+ rlocus = root_locus
0 commit comments