Skip to content

Commit 1d0afdf

Browse files
committed
Copied over latest changes from v0.3d:
* Added in latest nichols() code from Allan McInnes (including 13 Feb commits) * Turned svn:keyword Id back on for source code files * Set version number to 0.4a
1 parent 061e1e6 commit 1d0afdf

File tree

14 files changed

+277
-42
lines changed

14 files changed

+277
-42
lines changed

ChangeLog

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
1+
2011-02-13 Richard Murray <murray@sumatra.local>
2+
3+
* src/*.py: added svn:keywords Id properly
4+
5+
* src/matlab.py (ngrid): added ngrid() from v0.3d
6+
7+
* src/freqplot.py (nichols_grid, closed_loop_contours, m_circles,
8+
n_circles): copied over changes from Allan McInnes in v0.3d; ngrid()
9+
functiality + split out some of the nichols chart code into separate
10+
functions
11+
12+
2011-02-12 Richard Murray <murray@sumatra.local>
13+
14+
* setup.py: updated version number to 0.4a
15+
116
2010-11-05 Richard Murray <murray@sumatra.local>
217

318
* external/yottalab.py: New file containing Roberto Bucher's control

examples/bdalg-matlab.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# bdalg-matlab.py - demonstrate some MATLAB commands for block diagram altebra
2+
# RMM, 29 May 09
3+
4+
from control.matlab import * # MATLAB-like functions
5+
6+
# System matrices
7+
A1 = [[0, 1.], [-4, -1]]
8+
B1 = [[0], [1.]]
9+
C1 = [[1., 0]]
10+
sys1ss = ss(A1, B1, C1, 0)
11+
sys1tf = ss2tf(sys1ss)
12+
13+
sys2tf = tf([1, 0.5], [1, 5]);
14+
sys2ss = tf2ss(sys2tf);
15+
16+
# Series composition
17+
series1 = sys1ss + sys2ss;

examples/test-response.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# test-response.py - Unit tests for system response functions
2+
# RMM, 11 Sep 2010
3+
4+
from matplotlib.pyplot import * # Grab MATLAB plotting functions
5+
from control.matlab import * # Load the controls systems library
6+
from scipy import arange # function to create range of numbers
7+
8+
# Create several systems for testing
9+
sys1 = tf([1], [1, 2, 1])
10+
sys2 = tf([1, 1], [1, 1, 0])
11+
12+
# Generate step responses
13+
(T1a, y1a) = step(sys1)
14+
(T1b, y1b) = step(sys1, T = arange(0, 10, 0.1))
15+
(T1c, y1c) = step(sys1, X0 = [1, 0])
16+
(T2a, y2a) = step(sys2, T = arange(0, 10, 0.1))
17+
18+
plot(T1a, y1a, T1b, y1b, T1c, y1c, T2a, y2a)

examples/test-statefbk.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# test-statefbk.py - Unit tests for state feedback code
2+
# RMM, 6 Sep 2010
3+
4+
import numpy as np # Numerical library
5+
from scipy import * # Load the scipy functions
6+
from control.matlab import * # Load the controls systems library
7+
8+
# Parameters defining the system
9+
m = 250.0 # system mass
10+
k = 40.0 # spring constant
11+
b = 60.0 # damping constant
12+
13+
# System matrices
14+
A = matrix([[1, -1, 1.], [1, -k/m, -b/m], [1, 1, 1]])
15+
B = matrix([[0], [1/m], [1]])
16+
C = matrix([[1., 0, 1.]])
17+
sys = ss(A, B, C, 0);
18+
19+
# Controllability
20+
Wc = ctrb(A, B)
21+
print "Wc = ", Wc
22+
23+
# Observability
24+
Wo = obsv(A, C)
25+
print "Wo = ", Wo
26+
27+

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from distutils.core import setup
44

55
setup(name='control',
6-
version='0.3c',
6+
version='0.4a',
77
description='Python Control Systems Library',
88
author='Richard Murray',
99
author_email='murray@cds.caltech.edu',

src/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
3838
# SUCH DAMAGE.
3939
#
40-
# $Id: __init__.py 29 2010-11-06 13:03:55Z murrayrm $
40+
# $Id$
4141

4242
"""Control System Library
4343

src/bdalg.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
Date: 24 May 09
4848
Revised: Kevin K. Chen, Dec 10
4949
50-
$Id: bdalg.py 17 2010-05-29 23:50:52Z murrayrm $
50+
$Id$
5151
5252
"""
5353

src/ctrlutil.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
3939
# SUCH DAMAGE.
4040
#
41-
# $Id: ctrlutil.py 21 2010-06-06 17:29:42Z murrayrm $
41+
# $Id$
4242

4343
# Packages that we need access to
4444
import scipy as sp

src/delay.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
3939
# SUCH DAMAGE.
4040
#
41-
# $Id: pade.py 17 2010-05-29 23:50:52Z murrayrm $
41+
# $Id$
4242

4343
def pade(T, n=1):
4444
"""

src/freqplot.py

Lines changed: 181 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
3939
# SUCH DAMAGE.
4040
#
41-
# $Id: freqplot.py 33 2010-11-26 21:59:57Z murrayrm $
41+
# $Id$
4242

4343
import matplotlib.pyplot as plt
4444
import scipy as sp
@@ -212,16 +212,15 @@ def nyquist(syslist, omega=None, Plot=True):
212212
plt.plot([-1], [0], 'r+')
213213

214214
return x, y, omega
215-
216215
# Nichols plot
217216
# Contributed by Allan McInnes <Allan.McInnes@canterbury.ac.nz>
218217
#! TODO: need unit test code
219-
def nichols(syslist, omega=None, Plot=True):
218+
def nichols(syslist, omega=None):
220219
"""Nichols plot for a system
221220
222221
Usage
223222
=====
224-
mag, phase, freq = nichols(sys, omega=None, Plot=True)
223+
magh = nichols(sys, omega=None)
225224
226225
Plots a Nichols plot for the system over a (optional) frequency range.
227226
@@ -231,15 +230,10 @@ def nichols(syslist, omega=None, Plot=True):
231230
List of linear input/output systems (single system is OK)
232231
omega : freq_range
233232
Range of frequencies (list or bounds) in rad/sec
234-
Plot : boolean
235-
if True, plot the Nichols frequency response
236233
237234
Return values
238235
-------------
239-
mag : array of magnitudes
240-
phase : array of phases
241-
freq : array of frequencies
242-
236+
None
243237
"""
244238

245239
# If argument was a singleton, turn it into a list
@@ -250,34 +244,106 @@ def nichols(syslist, omega=None, Plot=True):
250244
if (omega == None):
251245
omega = default_frequency_range(syslist)
252246

253-
254247
for sys in syslist:
255-
if (sys.inputs > 1 or sys.outputs > 1):
256-
#TODO: Add MIMO nichols plots.
257-
raise NotImplementedError("Nichols is currently only implemented for SISO systems.")
258-
else:
259-
# Get the magnitude and phase of the system
260-
mag_tmp, phase_tmp, omega = sys.freqresp(omega)
261-
mag = np.squeeze(mag_tmp)
262-
phase = np.squeeze(phase_tmp)
263-
264-
# Convert to Nichols-plot format (phase in degrees,
265-
# and magnitude in dB)
266-
x = unwrap(sp.degrees(phase), 360)
267-
y = 20*sp.log10(mag)
248+
# Get the magnitude and phase of the system
249+
mag, phase, omega = sys.freqresp(omega)
268250

269-
if (Plot):
270-
# Generate the plot
271-
plt.plot(x, y)
251+
# Convert to Nichols-plot format (phase in degrees,
252+
# and magnitude in dB)
253+
x = unwrap(sp.degrees(phase), 360)
254+
y = 20*sp.log10(mag)
255+
256+
# Generate the plot
257+
plt.plot(x, y)
272258

273-
plt.xlabel('Phase (deg)')
274-
plt.ylabel('Magnitude (dB)')
275-
plt.title('Nichols Plot')
259+
plt.xlabel('Phase (deg)')
260+
plt.ylabel('Magnitude (dB)')
261+
plt.title('Nichols Plot')
276262

277-
# Mark the -180 point
278-
plt.plot([-180], [0], 'r+')
263+
# Mark the -180 point
264+
plt.plot([-180], [0], 'r+')
279265

280-
return mag, phase, omega
266+
# Nichols grid
267+
def nichols_grid():
268+
"""Nichols chart grid
269+
270+
Usage
271+
=====
272+
nichols_grid()
273+
274+
Plots a Nichols chart grid on the current axis.
275+
276+
Parameters
277+
----------
278+
None
279+
280+
Return values
281+
-------------
282+
None
283+
"""
284+
mag_min_default = -40.0 # dB
285+
mag_step = 20.0 # dB
286+
287+
# Chart defaults
288+
phase_min, phase_max, mag_min, mag_max = -360.0, 0.0, mag_min_default, 40.0
289+
290+
# Set actual chart bounds based on current plot
291+
if plt.gcf().gca().has_data():
292+
phase_min, phase_max, mag_min, mag_max = plt.axis()
293+
294+
# M-circle magnitudes.
295+
# The "fixed" set are always generated, since this guarantees a recognizable
296+
# Nichols chart grid.
297+
mags_fixed = np.array([-40.0, -20.0, -12.0, -6.0, -3.0, -1.0, -0.5, 0.0,
298+
0.25, 0.5, 1.0, 3.0, 6.0, 12.0])
299+
300+
if mag_min < mag_min_default:
301+
# Outside the "fixed" set of magnitudes, the generated M-circles
302+
# are extended in steps of 'mag_step' dB to cover anything made
303+
# visible by the range of the existing plot
304+
mags_adjust = np.arange(mag_step*np.floor(mag_min/mag_step),
305+
mag_min_default, mag_step)
306+
mags = np.concatenate((mags_adjust, mags_fixed))
307+
else:
308+
mags = mags_fixed
309+
310+
# N-circle phases (should be in the range -360 to 0)
311+
phases = np.array([-0.25, -10.0, -20.0, -30.0, -45.0, -60.0, -90.0,
312+
-120.0, -150.0, -180.0, -210.0, -240.0, -270.0,
313+
-310.0, -325.0, -340.0, -350.0, -359.75])
314+
315+
# Find the M-contours
316+
m = m_circles(mags, phase_min=np.min(phases), phase_max=np.max(phases))
317+
m_mag = 20*sp.log10(np.abs(m))
318+
m_phase = sp.mod(sp.degrees(sp.angle(m)), -360.0) # Unwrap
319+
320+
# Find the N-contours
321+
n = n_circles(phases, mag_min=np.min(mags), mag_max=np.max(mags))
322+
n_mag = 20*sp.log10(np.abs(n))
323+
n_phase = sp.mod(sp.degrees(sp.angle(n)), -360.0) # Unwrap
324+
325+
# Plot the contours behind other plot elements.
326+
# The "phase offset" is used to produce copies of the chart that cover
327+
# the entire range of the plotted data, starting from a base chart computed
328+
# over the range -360 < phase < 0 (see above). Given the range
329+
# the base chart is computed over, the phase offset should be 0
330+
# for -360 < phase_min < 0.
331+
phase_offset_min = 360.0*np.ceil(phase_min/360.0)
332+
phase_offset_max = 360.0*np.ceil(phase_max/360.0) + 360.0
333+
phase_offsets = np.arange(phase_offset_min, phase_offset_max, 360.0)
334+
for phase_offset in phase_offsets:
335+
plt.plot(m_phase + phase_offset, m_mag, color='gray',
336+
linestyle='dashed', zorder=0)
337+
plt.plot(n_phase + phase_offset, n_mag, color='gray',
338+
linestyle='dashed', zorder=0)
339+
340+
# Add magnitude labels
341+
for x, y, m in zip(m_phase[:][-1], m_mag[:][-1], mags):
342+
align = 'right' if m < 0.0 else 'left'
343+
plt.text(x, y, str(m) + ' dB', size='small', ha=align)
344+
345+
# Make sure axes conform to any pre-existing plot.
346+
plt.axis([phase_min, phase_max, mag_min, mag_max])
281347

282348
# Gang of Four
283349
#! TODO: think about how (and whether) to handle lists of systems
@@ -395,3 +461,85 @@ def default_frequency_range(syslist):
395461
np.ceil(np.max(features))+1)
396462

397463
return omega
464+
465+
# Compute contours of a closed-loop transfer function
466+
def closed_loop_contours(Hmag, Hphase):
467+
"""Contours of the function H = G/(1+G).
468+
469+
Usage
470+
=====
471+
contours = closed_loop_contours(mags, phases)
472+
473+
Parameters
474+
----------
475+
mags : array-like
476+
Meshgrid array of magnitudes of the contours
477+
phases : array-like
478+
Meshgrid array of phases in radians of the contours
479+
480+
Return values
481+
-------------
482+
contours : complex array
483+
Array of complex numbers corresponding to the contours.
484+
"""
485+
# Compute the contours in H-space
486+
H = Hmag*sp.exp(1.j*Hphase)
487+
488+
# Invert H = G/(1+G) to get an expression for the contours in G-space
489+
return H/(1.0 - H)
490+
491+
# M-circle
492+
def m_circles(mags, phase_min=-359.75, phase_max=-0.25):
493+
"""Constant-magnitude contours of the function H = G/(1+G).
494+
495+
Usage
496+
=====
497+
contours = m_circles(mags)
498+
499+
Parameters
500+
----------
501+
mags : array-like
502+
Array of magnitudes in dB of the M-circles
503+
phase_min : degrees
504+
Minimum phase in degrees of the N-circles
505+
phase_max : degrees
506+
Maximum phase in degrees of the N-circles
507+
508+
Return values
509+
-------------
510+
contours : complex array
511+
Array of complex numbers corresponding to the contours.
512+
"""
513+
# Convert magnitudes and phase range into a grid suitable for
514+
# building contours
515+
phases = sp.radians(sp.linspace(phase_min, phase_max, 500))
516+
Hmag, Hphase = sp.meshgrid(10.0**(mags/20.0), phases)
517+
return closed_loop_contours(Hmag, Hphase)
518+
519+
# N-circle
520+
def n_circles(phases, mag_min=-40.0, mag_max=12.0):
521+
"""Constant-phase contours of the function H = G/(1+G).
522+
523+
Usage
524+
=====
525+
contour = n_circles(angles)
526+
527+
Parameters
528+
----------
529+
phases : array-like
530+
Array of phases in degrees of the N-circles
531+
mag_min : dB
532+
Minimum magnitude in dB of the N-circles
533+
mag_max : dB
534+
Maximum magnitude in dB of the N-circles
535+
536+
Return values
537+
-------------
538+
contours : complex array
539+
Array of complex numbers corresponding to the contours.
540+
"""
541+
# Convert phases and magnitude range into a grid suitable for
542+
# building contours
543+
mags = sp.linspace(10**(mag_min/20.0), 10**(mag_max/20.0), 2000)
544+
Hphase, Hmag = sp.meshgrid(sp.radians(phases), mags)
545+
return closed_loop_contours(Hmag, Hphase)

0 commit comments

Comments
 (0)