Skip to content

Commit 472983b

Browse files
committed
Small fixes based on testing + re-added root locus
* Implemented syntax checks for MATLAB functions * Fixed root locus plots to work with new SF, TF classes * Transfer functions with integer coeffs now convered to floats * More detailed list of changes in ChangeLog
1 parent fcbe78d commit 472983b

10 files changed

Lines changed: 356 additions & 73 deletions

File tree

ChangeLog

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,38 @@
11
2011-04-02 Richard Murray <murray@malabar.local>
22

3+
* src/xferfcn.py (TransferFunction.__add__): fixed bug when adding a
4+
transfer function to state space system; _convertToTransferFunction
5+
was being called with input/output keywords. Picked up in unit test.
6+
7+
* tests/matlab_test.py: added calls to all of the functions that are
8+
currently implemented in the library, to make sure there are no
9+
hidden issues. These calls do *not* test functionality, they just
10+
make sure that MATLAB compatibility functions accept the right types
11+
of arguments.
12+
13+
* examples/secord-matlab.py: added root locus plot to list of
14+
figures that are produced
15+
16+
* src/__init__.py: added rlocus to list of modules that are imported
17+
by control module
18+
19+
* src/exception.py (ControlMIMONotImplemented): added exception for
20+
functions that are not yet implemented for MIMO systems
21+
22+
* src/xferfcn.py (TransferFunction.__init__): convert integer
23+
numerator and denominator objects to floats to eliminate errors when
24+
creating common denominator (also used on pole command). This fixes
25+
and error were tf([1], [1, 2, 1]).pole() would generate an error.
26+
27+
* src/freqplot.py (bode): Tweaked documentation string to remove 'h'
28+
from mag and phase return arguments
29+
30+
* src/rlocus.py (RootLocus): removed commands that set figure number
31+
inside of RootLocus command, for consistency with other frequency
32+
plot routines. Added Plot=True argument and updated documentation.
33+
34+
* src/matlab.py: added rlocus() command (calls RootLocus)
35+
336
* MANIFEST.in: Added MANIFEST.in file to include examples and tests
437
in source distribution
538

Pending

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ OPEN BUGS
1212
* matlab.step() doesn't handle systems with a pole at the origin (use lsim2)
1313
* TF <-> SS transformations are buggy; see tests/convert_test.py
1414
* hsvd returns different value than MATLAB (2010a); see modelsimp_test.py
15-
* MIMO common denominator fails unit test; see convert_test.py
15+
* lsim doesn't work for StateSpace systems (signal.lsim2 bug??)
1616

1717
Transfer code from Roberto Bucher's yottalab to python-control
1818
acker - pole placement using Ackermann method
@@ -44,23 +44,15 @@ Examples and test cases
4444
* tests/freqresp.py needs to be converted to unit test
4545
* Convert examples/test-{response,statefbk}.py to unit tests
4646

47-
TransferFunction class fixes
48-
* evalfr is not working (num, den stored as ndarrays, not poly1ds)
49-
50-
Block diagram algebra fixes
51-
* Implement state space block diagram algebra
52-
* Produce minimal realizations to avoid later errors
47+
Root locus plot improvements
48+
* Make sure that scipy.signal.lti objects still work
49+
* Update calling syntax to be consistent with other plotting commands
5350

5451
State space class fixes
55-
* Convert pvtol to state space systems and rerun example
5652
* Implement pzmap for state space systems
5753

58-
LTI updates
59-
* Implement control.matlab.step (with semantics similar to MATLAB)
60-
6154
Basic functions to be added
6255
* margin - compute gain and phase margin (no plot)
63-
* lqr - compute optimal feedback gains (use SLICOT SB02ND.f)
6456
* lyap - solve Lyapunov equation (use SLICOT SB03MD.f)
6557
* See http://www.slicot.org/shared/libindex.html for list of functions
6658

examples/secord-matlab.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,7 @@
2727
# Nyquist plot for the system
2828
figure(3)
2929
nyquist(sys, logspace(-2, 2))
30+
31+
# Root lcous plut for the system
32+
figure(4)
33+
rlocus(sys)

src/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,4 @@
6464
from statefbk import *
6565
from delay import *
6666
from modelsimp import *
67+
from rlocus import *

src/exception.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ class ControlArgument(Exception):
5252
"""Raised when arguments to a function are not correct"""
5353
pass
5454

55+
class ControlMIMONotImplemented(Exception):
56+
"""Function is not currently implemented for MIMO systems"""
57+
pass
58+
5559
class ControlNotImplemented(Exception):
5660
"""Functionality is not yet implemented"""
5761
pass

src/freqplot.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def bode(syslist, omega=None, dB=False, Hz=False, color=None, Plot=True):
6060
6161
Usage
6262
=====
63-
(magh, phaseh, omega) = bode(syslist, omega=None, dB=False, Hz=False, color=None, Plot=True)
63+
(mag, phase, omega) = bode(syslist, omega=None, dB=False, Hz=False, color=None, Plot=True)
6464
6565
Plots a Bode plot for the system over a (optional) frequency range.
6666
@@ -79,8 +79,8 @@ def bode(syslist, omega=None, dB=False, Hz=False, color=None, Plot=True):
7979
8080
Return values
8181
-------------
82-
magh : magnitude array
83-
phaseh : phase array
82+
mag : magnitude array
83+
phase : phase array
8484
omega : frequency array
8585
8686
Notes

src/matlab.py

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -179,15 +179,15 @@
179179
\* ss/modred - model order reduction
180180
181181
Compensator design
182-
rlocus - evans root locus
182+
\* rlocus - evans root locus
183183
\* place - pole placement
184184
estim - form estimator given estimator gain
185185
reg - form regulator given state-feedback and estimator gains
186186
187187
LQR/LQG design
188188
ss/lqg - single-step LQG design
189189
\* lqr - linear-Quadratic (LQ) state-feedback regulator
190-
\* dlqr - discrete-time LQ state-feedback regulator
190+
dlqr - discrete-time LQ state-feedback regulator
191191
lqry - lq regulator with output weighting
192192
lqrd - discrete LQ regulator for continuous plant
193193
ss/lqi - linear-Quadratic-Integral (LQI) controller
@@ -243,8 +243,8 @@ class - model type ('tf', 'zpk', 'ss', or 'frd')
243243
Overloaded arithmetic operations
244244
\* + and - - add and subtract systems (parallel connection)
245245
\* \* - multiply systems (series connection)
246-
\* / - left divide -- sys1\sys2 means inv(sys1)\*sys2
247-
\* / - right divide -- sys1/sys2 means sys1\*inv(sys2)
246+
/ - left divide -- sys1\sys2 means inv(sys1)\*sys2
247+
- \ - right divide -- sys1/sys2 means sys1\*inv(sys2)
248248
^ - powers of a given system
249249
' - pertransposition
250250
.' - transposition of input/output map
@@ -758,12 +758,35 @@ def ngrid():
758758
from nichols import nichols_grid
759759
nichols_grid()
760760

761+
# Root locus plot
762+
def rlocus(sys, klist = None, **keywords):
763+
"""Root locus plot
764+
765+
Parameters
766+
----------
767+
sys: StateSpace or TransferFunction object
768+
klist: optional list of gains
769+
770+
Returns
771+
-------
772+
rlist: list of roots for each gain
773+
klist: list of gains used to compute roots
774+
"""
775+
from rlocus import RootLocus
776+
if (klist == None):
777+
#! TODO: update with a smart cacluation of the gains
778+
klist = logspace(-3, 3)
779+
780+
rlist = RootLocus(sys, klist, **keywords)
781+
return rlist, klist
782+
783+
761784
#
762785
# Modifications to scipy.signal functions
763786
#
764787

765788
# Redefine lsim to use lsim2
766-
def lsim(*args, **keywords):
789+
def lsim(sys, U=None, T=None, X0=None, **keywords):
767790
"""Simulate the output of a linear system
768791
769792
Examples
@@ -784,11 +807,12 @@ def lsim(*args, **keywords):
784807
yout: response of the system
785808
xout: time evolution of the state vector
786809
"""
787-
sys = args[0]
810+
# Convert the system to an signal.lti for simulation
811+
#! This should send a warning for MIMO systems
788812
ltiobjs = sys.returnScipySignalLti()
789813
ltiobj = ltiobjs[0][0]
790-
791-
return sp.signal.lsim2(ltiobj, **keywords)
814+
815+
return sp.signal.lsim2(ltiobj, U, T, X0, **keywords)
792816

793817
#! Redefine step to use lsim2
794818
#! Not yet implemented

src/rlocus.py

Lines changed: 62 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -37,64 +37,92 @@
3737
# * Added BSD copyright info to file (per Ryan)
3838
# * Added code to convert (num, den) to poly1d's if they aren't already.
3939
# This allows Ryan's code to run on a standard signal.ltisys object
40-
# or a controls.TransferFunction object.
40+
# or a control.TransferFunction object.
4141
# * Added some comments to make sure I understand the code
42+
#
43+
# RMM, 2 April 2011: modified to work with new Lti structure (see ChangeLog)
44+
# * Not tested: should still work on signal.ltisys objects
4245
#
4346
# $Id$
4447

4548
# Packages used by this module
4649
from scipy import *
50+
import scipy.signal # signal processing toolbox
51+
import pylab # plotting routines
52+
import xferfcn # transfer function manipulation
4753

4854
# Main function: compute a root locus diagram
49-
def RootLocus(sys, kvect, fig=None, fignum=1, \
50-
clear=True, xlim=None, ylim=None, plotstr='-'):
55+
def RootLocus(sys, kvect, xlim=None, ylim=None, plotstr='-', Plot=True):
5156
"""Calculate the root locus by finding the roots of 1+k*TF(s)
5257
where TF is self.num(s)/self.den(s) and each k is an element
53-
of kvect."""
58+
of kvect.
59+
60+
Parameters
61+
----------
62+
sys : linsys
63+
Linear input/output systems (SISO only, for now)
64+
klist : gain_range (default = None)
65+
List of gains to use in computing diagram
66+
Plot : boolean (default = True)
67+
If True, plot magnitude and phase
68+
69+
Return values
70+
-------------
71+
rlist : list of computed root locations
72+
klist : list of gains
73+
"""
5474

5575
# Convert numerator and denominator to polynomials if they aren't
5676
(nump, denp) = _systopoly1d(sys);
5777

58-
# Set up the figure
59-
if fig is None:
60-
import pylab
61-
fig = pylab.figure(fignum)
62-
if clear:
63-
fig.clf()
64-
ax = fig.add_subplot(111)
65-
6678
# Compute out the loci
6779
mymat = _RLFindRoots(sys, kvect)
6880
mymat = _RLSortRoots(sys, mymat)
6981

70-
# plot open loop poles
71-
poles = array(denp.r)
72-
ax.plot(real(poles), imag(poles), 'x')
73-
74-
# plot open loop zeros
75-
zeros = array(nump.r)
76-
if zeros.any():
77-
ax.plot(real(zeros), imag(zeros), 'o')
78-
79-
# Now plot the loci
80-
for col in mymat.T:
81-
ax.plot(real(col), imag(col), plotstr)
82-
83-
# Set up plot axes and labels
84-
if xlim:
85-
ax.set_xlim(xlim)
86-
if ylim:
87-
ax.set_ylim(ylim)
88-
ax.set_xlabel('Real')
89-
ax.set_ylabel('Imaginary')
82+
# Create the plot
83+
if (Plot):
84+
ax = pylab.axes();
85+
86+
# plot open loop poles
87+
poles = array(denp.r)
88+
ax.plot(real(poles), imag(poles), 'x')
89+
90+
# plot open loop zeros
91+
zeros = array(nump.r)
92+
if zeros.any():
93+
ax.plot(real(zeros), imag(zeros), 'o')
94+
95+
# Now plot the loci
96+
for col in mymat.T:
97+
ax.plot(real(col), imag(col), plotstr)
98+
99+
# Set up plot axes and labels
100+
if xlim:
101+
ax.set_xlim(xlim)
102+
if ylim:
103+
ax.set_ylim(ylim)
104+
ax.set_xlabel('Real')
105+
ax.set_ylabel('Imaginary')
106+
90107
return mymat
91108

92109
# Utility function to extract numerator and denominator polynomials
93110
def _systopoly1d(sys):
94111
"""Extract numerator and denominator polynomails for a system"""
112+
# Allow inputs from the signal processing toolbox
113+
if (isinstance(sys, scipy.signal.lti)):
114+
nump = sys.num; denp = sys.den;
115+
116+
else:
117+
# Convert to a transfer function, if needed
118+
sys = xferfcn._convertToTransferFunction(sys)
119+
120+
# Make sure we have a SISO system
121+
if (sys.inputs > 1 or sys.outputs > 1):
122+
raise ControlMIMONotImplemented()
95123

96-
# Start by extracting the numerator and denominator from system object
97-
nump = sys.num; denp = sys.den;
124+
# Start by extracting the numerator and denominator from system object
125+
nump = sys.num[0][0]; denp = sys.den[0][0];
98126

99127
# Check to see if num, den are already polynomials; otherwise convert
100128
if (not isinstance(nump, poly1d)): nump = poly1d(nump)

src/xferfcn.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -129,11 +129,20 @@ def __init__(self, *args):
129129
for i in range(len(data)):
130130
if isinstance(data[i], (int, float, long, complex)):
131131
# Convert scalar to list of list of array.
132-
data[i] = [[array([data[i]])]]
132+
if (isinstance(data[i], int)):
133+
# Convert integers to floats at this point
134+
data[i] = [[array([data[i]], dtype=float)]]
135+
else:
136+
data[i] = [[array([data[i]])]]
133137
elif (isinstance(data[i], (list, tuple, ndarray)) and
134138
isinstance(data[i][0], (int, float, long, complex))):
135139
# Convert array to list of list of array.
136-
data[i] = [[array(data[i])]]
140+
if (isinstance(data[i][0], int)):
141+
# Convert integers to floats at this point
142+
#! Not sure this covers all cases correctly
143+
data[i] = [[array(data[i], dtype=float)]]
144+
else:
145+
data[i] = [[array(data[i])]]
137146
elif (isinstance(data[i], list) and
138147
isinstance(data[i][0], list) and
139148
isinstance(data[i][0][0], (list, tuple, ndarray)) and
@@ -142,7 +151,10 @@ def __init__(self, *args):
142151
# coefficient vectors to arrays, if necessary.
143152
for j in range(len(data[i])):
144153
for k in range(len(data[i][j])):
145-
data[i][j][k] = array(data[i][j][k])
154+
if (isinstance(data[i][j][k], int)):
155+
data[i][j][k] = array(data[i][j][k], dtype=float)
156+
else:
157+
data[i][j][k] = array(data[i][j][k])
146158
else:
147159
# If the user passed in anything else, then it's unclear what
148160
# the meaning is.
@@ -274,7 +286,9 @@ def __add__(self, other):
274286
"""Add two LTI objects (parallel connection)."""
275287

276288
# Convert the second argument to a transfer function.
277-
if not isinstance(other, TransferFunction):
289+
if (isinstance(other, statesp.StateSpace)):
290+
other = _convertToTransferFunction(other)
291+
elif not isinstance(other, TransferFunction):
278292
other = _convertToTransferFunction(other, inputs=self.inputs,
279293
outputs=self.outputs)
280294

@@ -513,6 +527,7 @@ def _common_den(self):
513527
# A sorted list to keep track of cumulative poles found as we scan
514528
# self.den.
515529
poles = []
530+
516531
# A 3-D list to keep track of common denominator poles not present in
517532
# the self.den[i][j].
518533
missingpoles = [[[] for j in range(self.inputs)] for i in

0 commit comments

Comments
 (0)