Skip to content

Commit ee7c35d

Browse files
committed
Add smart selection of gains in root locus plot. Calculation of breakpoints of real axis.
1 parent 5ab74cf commit ee7c35d

File tree

1 file changed

+156
-44
lines changed

1 file changed

+156
-44
lines changed

control/rlocus.py

Lines changed: 156 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -43,43 +43,52 @@
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+
4956
import numpy as np
57+
import pylab # plotting routines
58+
import scipy.signal # signal processing toolbox
5059
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
5462
from .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
138249
def _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

Comments
 (0)