Skip to content

Commit 9671fe3

Browse files
committed
Merge pull request #54 from cwrowley/gh-45
Fix #45 - make gain vector optional in root_locus Checked and this looks OK. For posterity, what was the reason for getting rid of some of the **kw arguments: -def tfdata(sys, **kw): +def tfdata(sys):
2 parents b7e7ae9 + 45e0450 commit 9671fe3

File tree

6 files changed

+61
-69
lines changed

6 files changed

+61
-69
lines changed

control/margins.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,11 +86,6 @@ def stability_margins(sysdata, deg=True, returnall=False, epsw=1e-10):
8686
"""Calculate gain, phase and stability margins and associated
8787
crossover frequencies.
8888
89-
Usage
90-
-----
91-
gm, pm, sm, wg, wp, ws = stability_margins(sysdata, deg=True,
92-
returnall=False, epsw=1e-10)
93-
9489
Parameters
9590
----------
9691
sysdata: linsys or (mag, phase, omega) sequence

control/matlab.py

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1110,11 +1110,8 @@ def rlocus(sys, klist = None, **keywords):
11101110
----------
11111111
sys: StateSpace or TransferFunction
11121112
Linear system
1113-
klist:
1113+
klist: iterable, optional
11141114
optional list of gains
1115-
1116-
Keyword parameters
1117-
------------------
11181115
xlim : control of x-axis range, normally with tuple, for
11191116
other options, see matplotlib.axes
11201117
ylim : control of y-axis range
@@ -1132,12 +1129,8 @@ def rlocus(sys, klist = None, **keywords):
11321129
list of gains used to compute roots
11331130
"""
11341131
from .rlocus import root_locus
1135-
#! TODO: update with a smart calculation of the gains using sys poles/zeros
1136-
if klist == None:
1137-
klist = logspace(-3, 3)
11381132

1139-
rlist = root_locus(sys, klist, **keywords)
1140-
return rlist, klist
1133+
return root_locus(sys, klist, **keywords)
11411134

11421135
def margin(*args):
11431136
"""Calculate gain and phase margins and associated crossover frequencies
@@ -1516,7 +1509,7 @@ def ssdata(sys):
15161509
return (ss.A, ss.B, ss.C, ss.D)
15171510

15181511
# Return transfer function data as a tuple
1519-
def tfdata(sys, **kw):
1512+
def tfdata(sys):
15201513
'''
15211514
Return transfer function data objects for a system
15221515
@@ -1525,17 +1518,12 @@ def tfdata(sys, **kw):
15251518
sys: Lti (StateSpace, or TransferFunction)
15261519
LTI system whose data will be returned
15271520
1528-
Keywords
1529-
--------
1530-
inputs = int; outputs = int
1531-
For MIMO transfer function, return num, den for given inputs, outputs
1532-
15331521
Returns
15341522
-------
15351523
(num, den): numerator and denominator arrays
15361524
Transfer function coefficients (SISO only)
15371525
'''
1538-
tf = _convertToTransferFunction(sys, **kw)
1526+
tf = _convertToTransferFunction(sys)
15391527

15401528
return (tf.num, tf.den)
15411529

control/nichols.py

Lines changed: 8 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,6 @@ def nichols_plot(syslist, omega=None, grid=True):
103103
def nichols_grid(cl_mags=None, cl_phases=None):
104104
"""Nichols chart grid
105105
106-
Usage
107-
=====
108-
nichols_grid()
109-
110106
Plots a Nichols chart grid on the current axis, or creates a new chart
111107
if no plot already exists.
112108
@@ -119,8 +115,8 @@ def nichols_grid(cl_mags=None, cl_phases=None):
119115
Array of closed-loop phases defining the iso-phase lines on a custom
120116
Nichols chart. Must be in the range -360 < cl_phases < 0
121117
122-
Return values
123-
-------------
118+
Returns
119+
-------
124120
None
125121
"""
126122
# Default chart size
@@ -211,19 +207,15 @@ def closed_loop_contours(Gcl_mags, Gcl_phases):
211207
Gol is an open-loop transfer function, and Gcl is a corresponding
212208
closed-loop transfer function.
213209
214-
Usage
215-
=====
216-
contours = closed_loop_contours(Gcl_mags, Gcl_phases)
217-
218210
Parameters
219211
----------
220212
Gcl_mags : array-like
221213
Array of magnitudes of the contours
222214
Gcl_phases : array-like
223215
Array of phases in radians of the contours
224216
225-
Return values
226-
-------------
217+
Returns
218+
-------
227219
contours : complex array
228220
Array of complex numbers corresponding to the contours.
229221
"""
@@ -241,10 +233,6 @@ def m_circles(mags, phase_min=-359.75, phase_max=-0.25):
241233
Gol is an open-loop transfer function, and Gcl is a corresponding
242234
closed-loop transfer function.
243235
244-
Usage
245-
=====
246-
contours = m_circles(mags, phase_min, phase_max)
247-
248236
Parameters
249237
----------
250238
mags : array-like
@@ -254,8 +242,8 @@ def m_circles(mags, phase_min=-359.75, phase_max=-0.25):
254242
phase_max : degrees
255243
Maximum phase in degrees of the N-circles
256244
257-
Return values
258-
-------------
245+
Returns
246+
-------
259247
contours : complex array
260248
Array of complex numbers corresponding to the contours.
261249
"""
@@ -271,10 +259,6 @@ def n_circles(phases, mag_min=-40.0, mag_max=12.0):
271259
Gol is an open-loop transfer function, and Gcl is a corresponding
272260
closed-loop transfer function.
273261
274-
Usage
275-
=====
276-
contours = n_circles(phases, mag_min, mag_max)
277-
278262
Parameters
279263
----------
280264
phases : array-like
@@ -284,8 +268,8 @@ def n_circles(phases, mag_min=-40.0, mag_max=12.0):
284268
mag_max : dB
285269
Maximum magnitude in dB of the N-circles
286270
287-
Return values
288-
-------------
271+
Returns
272+
-------
289273
contours : complex array
290274
Array of complex numbers corresponding to the contours.
291275
"""

control/rlocus.py

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -46,43 +46,51 @@
4646
# $Id$
4747

4848
# Packages used by this module
49+
import numpy as np
4950
from scipy import array, poly1d, row_stack, zeros_like, real, imag
5051
import scipy.signal # signal processing toolbox
5152
import pylab # plotting routines
5253
from . import xferfcn
5354
from .exception import ControlMIMONotImplemented
5455
from functools import partial
5556

56-
5757
# Main function: compute a root locus diagram
58-
def root_locus(sys, kvect, xlim=None, ylim=None, plotstr='-', Plot=True,
58+
def root_locus(sys, kvect=None, xlim=None, ylim=None, plotstr='-', Plot=True,
5959
PrintGain=True):
6060
"""Calculate the root locus by finding the roots of 1+k*TF(s)
6161
where TF is self.num(s)/self.den(s) and each k is an element
6262
of kvect.
6363
6464
Parameters
6565
----------
66-
sys : linsys
66+
sys : LTI object
6767
Linear input/output systems (SISO only, for now)
68-
kvect : gain_range (default = None)
68+
kvect : list or ndarray, optional
6969
List of gains to use in computing diagram
70-
xlim : control of x-axis range, normally with tuple, for
71-
other options, see matplotlib.axes
72-
ylim : control of y-axis range
73-
Plot : boolean (default = True)
70+
xlim : tuple or list, optional
71+
control of x-axis range, normally with tuple (see matplotlib.axes)
72+
ylim : tuple or list, optional
73+
control of y-axis range
74+
Plot : boolean, optional (default = True)
7475
If True, plot magnitude and phase
7576
PrintGain: boolean (default = True)
7677
If True, report mouse clicks when close to the root-locus branches,
7778
calculate gain, damping and print
78-
Return values
79-
-------------
80-
rlist : list of computed root locations
79+
80+
Returns
81+
-------
82+
rlist : ndarray
83+
Computed root locations, given as a 2d array
84+
klist : ndarray or list
85+
Gains used. Same as klist keyword argument if provided.
8186
"""
8287

8388
# Convert numerator and denominator to polynomials if they aren't
8489
(nump, denp) = _systopoly1d(sys)
8590

91+
if kvect is None:
92+
kvect = _default_gains(sys)
93+
8694
# Compute out the loci
8795
mymat = _RLFindRoots(sys, kvect)
8896
mymat = _RLSortRoots(sys, mymat)
@@ -116,8 +124,11 @@ def root_locus(sys, kvect, xlim=None, ylim=None, plotstr='-', Plot=True,
116124
ax.set_xlabel('Real')
117125
ax.set_ylabel('Imaginary')
118126

119-
return mymat
127+
return mymat, kvect
120128

129+
def _default_gains(sys):
130+
# TODO: update with a smart calculation of the gains using sys poles/zeros
131+
return np.logspace(-3, 3)
121132

122133
# Utility function to extract numerator and denominator polynomials
123134
def _systopoly1d(sys):

control/tests/matlab_test.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,10 @@ def testRlocus(self):
309309
rlocus(self.siso_ss1)
310310
rlocus(self.siso_tf1)
311311
rlocus(self.siso_tf2)
312-
rlist, klist = rlocus(self.siso_tf2, klist=[1, 10, 100], Plot=False)
312+
klist = [1, 10, 100]
313+
rlist, klist_out = rlocus(self.siso_tf2, klist=klist, Plot=False)
314+
np.testing.assert_equal(len(rlist), len(klist))
315+
np.testing.assert_array_equal(klist, klist_out)
313316

314317
def testNyquist(self):
315318
nyquist(self.siso_ss1)
@@ -618,7 +621,7 @@ def testCombi01(self):
618621
# for i in range(len(tfdata)):
619622
# np.testing.assert_array_almost_equal(tfdata_1[i], tfdata_2[i])
620623

621-
def suite():
624+
def test_suite():
622625
return unittest.TestLoader().loadTestsFromTestCase(TestMatlab)
623626

624627
if __name__ == '__main__':

control/tests/rlocus_test.py

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,32 @@ def setUp(self):
1717
"""This contains some random LTI systems and scalars for testing."""
1818

1919
# Two random SISO systems.
20-
self.sys1 = TransferFunction([1, 2], [1, 2, 3])
21-
self.sys2 = StateSpace([[1., 4.], [3., 2.]], [[1.], [-4.]],
20+
sys1 = TransferFunction([1, 2], [1, 2, 3])
21+
sys2 = StateSpace([[1., 4.], [3., 2.]], [[1.], [-4.]],
2222
[[1., 0.]], [[0.]])
23+
self.systems = (sys1, sys2)
24+
25+
def check_cl_poles(self, sys, pole_list, k_list):
26+
for k, poles in zip(k_list, pole_list):
27+
poles_expected = np.sort(feedback(sys, k).pole())
28+
poles = np.sort(poles)
29+
np.testing.assert_array_almost_equal(poles, poles_expected)
2330

2431
def testRootLocus(self):
2532
"""Basic root locus plot"""
2633
klist = [-1, 0, 1]
27-
rlist = root_locus(self.sys1, [-1, 0, 1], Plot=False)
28-
29-
for k in klist:
30-
np.testing.assert_array_almost_equal(
31-
np.sort(rlist[k]),
32-
np.sort(feedback(self.sys1, klist[k]).pole()))
33-
34-
def suite():
34+
for sys in self.systems:
35+
roots, k_out = root_locus(sys, klist, Plot=False)
36+
np.testing.assert_equal(len(roots), len(klist))
37+
np.testing.assert_array_equal(klist, k_out)
38+
self.check_cl_poles(sys, roots, klist)
39+
40+
def test_without_gains(self):
41+
for sys in self.systems:
42+
roots, kvect = root_locus(sys, Plot=False)
43+
self.check_cl_poles(sys, roots, kvect)
44+
45+
def test_suite():
3546
return unittest.TestLoader().loadTestsFromTestCase(TestRootLocus)
3647

3748
if __name__ == "__main__":

0 commit comments

Comments
 (0)