43
43
# RMM, 2 April 2011: modified to work with new LTI structure (see ChangeLog)
44
44
# * Not tested: should still work on signal.ltisys objects
45
45
#
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
+ #
46
51
# $Id$
47
52
48
53
# Packages used by this module
54
+ from functools import partial
55
+
49
56
import numpy as np
57
+ import pylab # plotting routines
58
+ import scipy .signal # signal processing toolbox
50
59
from 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
54
62
from .exception import ControlMIMONotImplemented
55
- from functools import partial
56
63
57
64
__all__ = ['root_locus' ,'rlocus' ]
58
65
66
+
59
67
# 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 ):
62
70
"""Root locus plot
63
71
64
72
Calculate the root locus by finding the roots of 1+k*TF(s)
65
73
where TF is self.num(s)/self.den(s) and each k is an element
66
- ofkvect .
74
+ ofgvect .
67
75
68
76
Parameters
69
77
----------
70
- sys : LTI object
78
+ dinsys : LTI object
71
79
Linear input/output systems (SISO only, for now)
72
80
kvect : list or ndarray, optional
73
81
List of gains to use in computing diagram
74
82
xlim : tuple or list, optional
75
83
control of x-axis range, normally with tuple (see matplotlib.axes)
76
84
ylim : tuple or list, optional
77
85
control of y-axis range
78
- Plot : boolean, optional (default = True)
86
+ plot : boolean, optional (default = True)
79
87
If True, plot magnitude and phase
80
- PrintGain : boolean (default = True)
88
+ print_gain : boolean (default = True)
81
89
If True, report mouse clicks when close to the root-locus branches,
82
90
calculate gain, damping and print
91
+ plotstr: string that declare of the rlocus (see matplotlib)
83
92
84
93
Returns
85
94
-------
@@ -90,21 +99,74 @@ def root_locus(sys, kvect=None, xlim=None, ylim=None, plotstr='-', Plot=True,
90
99
"""
91
100
92
101
# Convert numerator and denominator to polynomials if they aren't
93
- (nump ,denp )= _systopoly1d (sys )
102
+ (nump ,denp )= _systopoly1d (dinsys )
94
103
95
104
if kvect is None :
96
- kvect = _default_gains (sys )
105
+ gvect = _default_gains (nump ,denp )
106
+ else :
107
+ gvect = np .asarray (kvect )
97
108
98
109
# 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
101
163
102
164
# Create the plot
103
- if ( Plot ) :
165
+ if plot :
104
166
f = pylab .figure ()
105
- if PrintGain :
167
+ if print_gain :
106
168
f .canvas .mpl_connect (
107
- 'button_release_event' ,partial (_RLFeedbackClicks ,sys = sys ))
169
+ 'button_release_event' ,partial (_feedback_clicks ,sys = dinsys ))
108
170
ax = pylab .axes ()
109
171
110
172
# plot open loop poles
@@ -121,49 +183,98 @@ def root_locus(sys, kvect=None, xlim=None, ylim=None, plotstr='-', Plot=True,
121
183
ax .plot (real (col ),imag (col ),plotstr )
122
184
123
185
# 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 )
128
188
ax .set_xlabel ('Real' )
129
189
ax .set_ylabel ('Imaginary' )
130
190
131
191
return mymat ,kvect
132
192
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
+
136
247
137
248
# Utility function to extract numerator and denominator polynomials
138
249
def _systopoly1d (sys ):
139
250
"""Extract numerator and denominator polynomails for a system"""
140
251
# Allow inputs from the signal processing toolbox
141
- if ( isinstance (sys ,scipy .signal .lti ) ):
252
+ if isinstance (sys ,scipy .signal .lti ):
142
253
nump = sys .num
143
254
denp = sys .den
144
255
145
256
else :
146
257
# Convert to a transfer function, if needed
147
- sys = _convertToTransferFunction (sys )
258
+ sys = xferfcn . convertToTransferFunction (sys )
148
259
149
260
# 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. :
151
262
raise ControlMIMONotImplemented ()
152
263
153
264
# Start by extracting the numerator and denominator from system object
154
265
nump = sys .num [0 ][0 ]
155
266
denp = sys .den [0 ][0 ]
156
267
157
268
# Check to see if num, den are already polynomials; otherwise convert
158
- if ( not isinstance (nump ,poly1d ) ):
269
+ if not isinstance (nump ,poly1d ):
159
270
nump = poly1d (nump )
160
- if ( not isinstance (denp ,poly1d ) ):
271
+ if not isinstance (denp ,poly1d ):
161
272
denp = poly1d (denp )
162
273
163
- return ( nump ,denp )
274
+ return nump ,denp
164
275
165
276
166
- def _RLFindRoots (sys ,kvect ):
277
+ def _find_roots (sys ,kvect ):
167
278
"""Find the roots for the root locus."""
168
279
169
280
# Convert numerator and denominator to polynomials if they aren't
@@ -179,36 +290,37 @@ def _RLFindRoots(sys, kvect):
179
290
return mymat
180
291
181
292
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
184
295
locus doesn't show weird pseudo-branches as roots jump from
185
296
one branch to another."""
186
297
187
- sorted = zeros_like (mymat )
298
+ sorted_roots = zeros_like (mymat )
188
299
for n ,row in enumerate (mymat ):
189
300
if n == 0 :
190
- sorted [n , :]= row
301
+ sorted_roots [n , :]= row
191
302
else :
192
303
# sort the current row by finding the element with the
193
304
# smallest absolute distance to each root in the
194
305
# previous row
195
306
available = list (range (len (prevrow )))
196
307
for elem in row :
197
- evect = elem - prevrow [available ]
308
+ evect = elem - prevrow [available ]
198
309
ind1 = abs (evect ).argmin ()
199
310
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
203
314
204
315
205
- def _RLFeedbackClicks (event ,sys ):
316
+ def _feedback_clicks (event ,sys ):
206
317
"""Print root-locus gain feedback for clicks on the root-locus plot
207
318
"""
208
319
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 :
211
322
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
+
213
325
214
- rlocus = root_locus
326
+ rlocus = root_locus