From: <mur...@us...> - 2011-04-02 15:52:46
|
Revision: 144 http://python-control.svn.sourceforge.net/python-control/?rev=144&view=rev Author: murrayrm Date: 2011-04-02 15:52:38 +0000 (Sat, 02 Apr 2011) Log Message: ----------- * Moved nichols() code to separate file, added unit test; add'l updates pending * Moved slycot imports inside functions that need them * Updated pvtol-nested example to work with v0.4 * See ChangeLog for detailed listing of changes Modified Paths: -------------- branches/control-0.4b/ChangeLog branches/control-0.4b/examples/pvtol-nested.py branches/control-0.4b/src/__init__.py branches/control-0.4b/src/freqplot.py branches/control-0.4b/src/matlab.py branches/control-0.4b/src/statesp.py branches/control-0.4b/src/xferfcn.py Added Paths: ----------- branches/control-0.4b/src/nichols.py branches/control-0.4b/tests/nichols_test.py Modified: branches/control-0.4b/ChangeLog =================================================================== --- branches/control-0.4b/ChangeLog 2011-04-01 04:46:30 UTC (rev 143) +++ branches/control-0.4b/ChangeLog 2011-04-02 15:52:38 UTC (rev 144) @@ -1,3 +1,27 @@ +2011-03-31 Richard Murray <murray@malabar.local> + + * examples/pvtol-nested.py: updated stability margin plot to use + proper calling format for bode(). + + * src/statesp.py (_convertToStateSpace): moved slycot import + to the location where it is actually needed (allows running some + commands without slycot installed) + + * src/xferfcn.py (_convertToTransferFunction): moved slycot import + to the location where it is actually needed (allows running some + commands without slycot installed) + + * src/nichols.py: new file for Nichols plot routines; move + nichols(), nichols_grid(), closed_loop_contours(), m_circles(), + n_circles() + + * src/__init__.py, src/freqresp.py, src/matlab.py: updated to match + new file structure for Nichols charts + + * src/nichols.py (nichols): updated processing of freqresp to take + into account the fact that return arguments are now a matrix of + results (even for a SISO system) + 2011-03-30 Richard Murray <murray@malabar.local> * tests/: added top level subdirectory, to be used for unit tests. Modified: branches/control-0.4b/examples/pvtol-nested.py =================================================================== --- branches/control-0.4b/examples/pvtol-nested.py 2011-04-01 04:46:30 UTC (rev 143) +++ branches/control-0.4b/examples/pvtol-nested.py 2011-04-02 15:52:38 UTC (rev 144) @@ -84,22 +84,22 @@ # (gm, pm, wgc, wpc) = margin(L); #! TODO: this figure has something wrong; axis limits mismatch -figure(6); clf; subplot(221); -(magh, phaseh) = bode(L); +figure(6); clf; +bode(L); # Add crossover line -subplot(magh); hold(True); -loglog([10^-4, 10^3], [1, 1], 'k-') +subplot(211); hold(True); +loglog([1e-4, 1e3], [1, 1], 'k-') # Replot phase starting at -90 degrees bode(L, logspace(-4, 3)); (mag, phase, w) = freqresp(L, logspace(-4, 3)); phase = phase - 360; -subplot(phaseh); -semilogx([10^-4, 10^3], [-180, -180], 'k-') +subplot(212); +semilogx([1e-4, 1e3], [-180, -180], 'k-') hold(True); semilogx(w, np.squeeze(phase), 'b-') -axis([10^-4, 10^3, -360, 0]); +axis([1e-4, 1e3, -360, 0]); xlabel('Frequency [deg]'); ylabel('Phase [deg]'); # set(gca, 'YTick', [-360, -270, -180, -90, 0]); # set(gca, 'XTick', [10^-4, 10^-2, 1, 100]); Modified: branches/control-0.4b/src/__init__.py =================================================================== --- branches/control-0.4b/src/__init__.py 2011-04-01 04:46:30 UTC (rev 143) +++ branches/control-0.4b/src/__init__.py 2011-04-02 15:52:38 UTC (rev 144) @@ -59,6 +59,7 @@ from xferfcn import * from statesp import * from freqplot import * +from nichols import * from bdalg import * from statefbk import * from delay import * Modified: branches/control-0.4b/src/freqplot.py =================================================================== --- branches/control-0.4b/src/freqplot.py 2011-04-01 04:46:30 UTC (rev 143) +++ branches/control-0.4b/src/freqplot.py 2011-04-02 15:52:38 UTC (rev 144) @@ -212,139 +212,7 @@ plt.plot([-1], [0], 'r+') return x, y, omega -# Nichols plot -# Contributed by Allan McInnes <All...@ca...> -#! TODO: need unit test code -def nichols(syslist, omega=None): - """Nichols plot for a system - Usage - ===== - magh = nichols(sys, omega=None) - - Plots a Nichols plot for the system over a (optional) frequency range. - - Parameters - ---------- - syslist : linsys - List of linear input/output systems (single system is OK) - omega : freq_range - Range of frequencies (list or bounds) in rad/sec - - Return values - ------------- - None - """ - - # If argument was a singleton, turn it into a list - if (not getattr(syslist, '__iter__', False)): - syslist = (syslist,) - - # Select a default range if none is provided - if (omega == None): - omega = default_frequency_range(syslist) - - for sys in syslist: - # Get the magnitude and phase of the system - mag, phase, omega = sys.freqresp(omega) - - # Convert to Nichols-plot format (phase in degrees, - # and magnitude in dB) - x = unwrap(sp.degrees(phase), 360) - y = 20*sp.log10(mag) - - # Generate the plot - plt.plot(x, y) - - plt.xlabel('Phase (deg)') - plt.ylabel('Magnitude (dB)') - plt.title('Nichols Plot') - - # Mark the -180 point - plt.plot([-180], [0], 'r+') - -# Nichols grid -def nichols_grid(): - """Nichols chart grid - - Usage - ===== - nichols_grid() - - Plots a Nichols chart grid on the current axis. - - Parameters - ---------- - None - - Return values - ------------- - None - """ - mag_min_default = -40.0 # dB - mag_step = 20.0 # dB - - # Chart defaults - phase_min, phase_max, mag_min, mag_max = -360.0, 0.0, mag_min_default, 40.0 - - # Set actual chart bounds based on current plot - if plt.gcf().gca().has_data(): - phase_min, phase_max, mag_min, mag_max = plt.axis() - - # M-circle magnitudes. - # The "fixed" set are always generated, since this guarantees a recognizable - # Nichols chart grid. - mags_fixed = np.array([-40.0, -20.0, -12.0, -6.0, -3.0, -1.0, -0.5, 0.0, - 0.25, 0.5, 1.0, 3.0, 6.0, 12.0]) - - if mag_min < mag_min_default: - # Outside the "fixed" set of magnitudes, the generated M-circles - # are extended in steps of 'mag_step' dB to cover anything made - # visible by the range of the existing plot - mags_adjust = np.arange(mag_step*np.floor(mag_min/mag_step), - mag_min_default, mag_step) - mags = np.concatenate((mags_adjust, mags_fixed)) - else: - mags = mags_fixed - - # N-circle phases (should be in the range -360 to 0) - phases = np.array([-0.25, -10.0, -20.0, -30.0, -45.0, -60.0, -90.0, - -120.0, -150.0, -180.0, -210.0, -240.0, -270.0, - -310.0, -325.0, -340.0, -350.0, -359.75]) - - # Find the M-contours - m = m_circles(mags, phase_min=np.min(phases), phase_max=np.max(phases)) - m_mag = 20*sp.log10(np.abs(m)) - m_phase = sp.mod(sp.degrees(sp.angle(m)), -360.0) # Unwrap - - # Find the N-contours - n = n_circles(phases, mag_min=np.min(mags), mag_max=np.max(mags)) - n_mag = 20*sp.log10(np.abs(n)) - n_phase = sp.mod(sp.degrees(sp.angle(n)), -360.0) # Unwrap - - # Plot the contours behind other plot elements. - # The "phase offset" is used to produce copies of the chart that cover - # the entire range of the plotted data, starting from a base chart computed - # over the range -360 < phase < 0 (see above). Given the range - # the base chart is computed over, the phase offset should be 0 - # for -360 < phase_min < 0. - phase_offset_min = 360.0*np.ceil(phase_min/360.0) - phase_offset_max = 360.0*np.ceil(phase_max/360.0) + 360.0 - phase_offsets = np.arange(phase_offset_min, phase_offset_max, 360.0) - for phase_offset in phase_offsets: - plt.plot(m_phase + phase_offset, m_mag, color='gray', - linestyle='dashed', zorder=0) - plt.plot(n_phase + phase_offset, n_mag, color='gray', - linestyle='dashed', zorder=0) - - # Add magnitude labels - for x, y, m in zip(m_phase[:][-1], m_mag[:][-1], mags): - align = 'right' if m < 0.0 else 'left' - plt.text(x, y, str(m) + ' dB', size='small', ha=align) - - # Make sure axes conform to any pre-existing plot. - plt.axis([phase_min, phase_max, mag_min, mag_max]) - # Gang of Four #! TODO: think about how (and whether) to handle lists of systems def gangof4(P, C, omega=None): @@ -462,84 +330,3 @@ return omega -# Compute contours of a closed-loop transfer function -def closed_loop_contours(Hmag, Hphase): - """Contours of the function H = G/(1+G). - - Usage - ===== - contours = closed_loop_contours(mags, phases) - - Parameters - ---------- - mags : array-like - Meshgrid array of magnitudes of the contours - phases : array-like - Meshgrid array of phases in radians of the contours - - Return values - ------------- - contours : complex array - Array of complex numbers corresponding to the contours. - """ - # Compute the contours in H-space - H = Hmag*sp.exp(1.j*Hphase) - - # Invert H = G/(1+G) to get an expression for the contours in G-space - return H/(1.0 - H) - -# M-circle -def m_circles(mags, phase_min=-359.75, phase_max=-0.25): - """Constant-magnitude contours of the function H = G/(1+G). - - Usage - ===== - contours = m_circles(mags) - - Parameters - ---------- - mags : array-like - Array of magnitudes in dB of the M-circles - phase_min : degrees - Minimum phase in degrees of the N-circles - phase_max : degrees - Maximum phase in degrees of the N-circles - - Return values - ------------- - contours : complex array - Array of complex numbers corresponding to the contours. - """ - # Convert magnitudes and phase range into a grid suitable for - # building contours - phases = sp.radians(sp.linspace(phase_min, phase_max, 500)) - Hmag, Hphase = sp.meshgrid(10.0**(mags/20.0), phases) - return closed_loop_contours(Hmag, Hphase) - -# N-circle -def n_circles(phases, mag_min=-40.0, mag_max=12.0): - """Constant-phase contours of the function H = G/(1+G). - - Usage - ===== - contour = n_circles(angles) - - Parameters - ---------- - phases : array-like - Array of phases in degrees of the N-circles - mag_min : dB - Minimum magnitude in dB of the N-circles - mag_max : dB - Maximum magnitude in dB of the N-circles - - Return values - ------------- - contours : complex array - Array of complex numbers corresponding to the contours. - """ - # Convert phases and magnitude range into a grid suitable for - # building contours - mags = sp.linspace(10**(mag_min/20.0), 10**(mag_max/20.0), 2000) - Hphase, Hmag = sp.meshgrid(sp.radians(phases), mags) - return closed_loop_contours(Hmag, Hphase) Modified: branches/control-0.4b/src/matlab.py =================================================================== --- branches/control-0.4b/src/matlab.py 2011-04-01 04:46:30 UTC (rev 143) +++ branches/control-0.4b/src/matlab.py 2011-04-02 15:52:38 UTC (rev 144) @@ -74,7 +74,8 @@ # Import MATLAB-like functions that can be used as-is from ctrlutil import unwrap -from freqplot import nyquist, nichols, gangof4 +from freqplot import nyquist, gangof4 +from nichols import nichols, nichols_grid from bdalg import series, parallel, negate, feedback from pzmap import pzmap from statefbk import ctrb, obsv, gram, place, lqr @@ -752,7 +753,7 @@ ===== ngrid() """ - freqplot.nichols_grid() + nichols_grid() # # Modifications to scipy.signal functions Added: branches/control-0.4b/src/nichols.py =================================================================== --- branches/control-0.4b/src/nichols.py (rev 0) +++ branches/control-0.4b/src/nichols.py 2011-04-02 15:52:38 UTC (rev 144) @@ -0,0 +1,267 @@ +# nichols.py - Nichols plot +# +# Contributed by Allan McInnes <All...@ca...> +# +# This file contains some standard control system plots: Bode plots, +# Nyquist plots, Nichols plots and pole-zero diagrams +# +# Copyright (c) 2010 by California Institute of Technology +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# 3. Neither the name of the California Institute of Technology nor +# the names of its contributors may be used to endorse or promote +# products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL CALTECH +# OR THE CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF +# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. +# +# $Id: freqplot.py 139 2011-03-30 16:19:59Z murrayrm $ + +import matplotlib.pyplot as plt +import scipy as sp +import numpy as np +from ctrlutil import unwrap +from freqplot import default_frequency_range + +def nichols(syslist, omega=None): + """Nichols plot for a system + + Usage + ===== + magh = nichols(sys, omega=None) + + Plots a Nichols plot for the system over a (optional) frequency range. + + Parameters + ---------- + syslist : linsys + List of linear input/output systems (single system is OK) + omega : freq_range + Range of frequencies (list or bounds) in rad/sec + + Return values + ------------- + None + """ + + # If argument was a singleton, turn it into a list + if (not getattr(syslist, '__iter__', False)): + syslist = (syslist,) + + # Select a default range if none is provided + if (omega == None): + omega = default_frequency_range(syslist) + + for sys in syslist: + # Get the magnitude and phase of the system + mag_tmp, phase_tmp, omega = sys.freqresp(omega) + mag = np.squeeze(mag_tmp) + phase = np.squeeze(phase_tmp) + + # Convert to Nichols-plot format (phase in degrees, + # and magnitude in dB) + x = unwrap(sp.degrees(phase), 360) + y = 20*sp.log10(mag) + + # Generate the plot + plt.plot(x, y) + + plt.xlabel('Phase (deg)') + plt.ylabel('Magnitude (dB)') + plt.title('Nichols Plot') + + # Mark the -180 point + plt.plot([-180], [0], 'r+') + +# Nichols grid +def nichols_grid(): + """Nichols chart grid + + Usage + ===== + nichols_grid() + + Plots a Nichols chart grid on the current axis. + + Parameters + ---------- + None + + Return values + ------------- + None + """ + mag_min_default = -40.0 # dB + mag_step = 20.0 # dB + + # Chart defaults + phase_min, phase_max, mag_min, mag_max = -360.0, 0.0, mag_min_default, 40.0 + + # Set actual chart bounds based on current plot + if plt.gcf().gca().has_data(): + phase_min, phase_max, mag_min, mag_max = plt.axis() + + # M-circle magnitudes. + # The "fixed" set are always generated, since this guarantees a recognizable + # Nichols chart grid. + mags_fixed = np.array([-40.0, -20.0, -12.0, -6.0, -3.0, -1.0, -0.5, 0.0, + 0.25, 0.5, 1.0, 3.0, 6.0, 12.0]) + + if mag_min < mag_min_default: + # Outside the "fixed" set of magnitudes, the generated M-circles + # are extended in steps of 'mag_step' dB to cover anything made + # visible by the range of the existing plot + mags_adjust = np.arange(mag_step*np.floor(mag_min/mag_step), + mag_min_default, mag_step) + mags = np.concatenate((mags_adjust, mags_fixed)) + else: + mags = mags_fixed + + # N-circle phases (should be in the range -360 to 0) + phases = np.array([-0.25, -10.0, -20.0, -30.0, -45.0, -60.0, -90.0, + -120.0, -150.0, -180.0, -210.0, -240.0, -270.0, + -310.0, -325.0, -340.0, -350.0, -359.75]) + + # Find the M-contours + m = m_circles(mags, phase_min=np.min(phases), phase_max=np.max(phases)) + m_mag = 20*sp.log10(np.abs(m)) + m_phase = sp.mod(sp.degrees(sp.angle(m)), -360.0) # Unwrap + + # Find the N-contours + n = n_circles(phases, mag_min=np.min(mags), mag_max=np.max(mags)) + n_mag = 20*sp.log10(np.abs(n)) + n_phase = sp.mod(sp.degrees(sp.angle(n)), -360.0) # Unwrap + + # Plot the contours behind other plot elements. + # The "phase offset" is used to produce copies of the chart that cover + # the entire range of the plotted data, starting from a base chart computed + # over the range -360 < phase < 0 (see above). Given the range + # the base chart is computed over, the phase offset should be 0 + # for -360 < phase_min < 0. + phase_offset_min = 360.0*np.ceil(phase_min/360.0) + phase_offset_max = 360.0*np.ceil(phase_max/360.0) + 360.0 + phase_offsets = np.arange(phase_offset_min, phase_offset_max, 360.0) + for phase_offset in phase_offsets: + plt.plot(m_phase + phase_offset, m_mag, color='gray', + linestyle='dashed', zorder=0) + plt.plot(n_phase + phase_offset, n_mag, color='gray', + linestyle='dashed', zorder=0) + + # Add magnitude labels + for x, y, m in zip(m_phase[:][-1], m_mag[:][-1], mags): + align = 'right' if m < 0.0 else 'left' + plt.text(x, y, str(m) + ' dB', size='small', ha=align) + + # Make sure axes conform to any pre-existing plot. + plt.axis([phase_min, phase_max, mag_min, mag_max]) + +# +# Utility functions +# +# This section of the code contains some utility functions for +# generating Nichols plots +# + +# Compute contours of a closed-loop transfer function +def closed_loop_contours(Hmag, Hphase): + """Contours of the function H = G/(1+G). + + Usage + ===== + contours = closed_loop_contours(mags, phases) + + Parameters + ---------- + mags : array-like + Meshgrid array of magnitudes of the contours + phases : array-like + Meshgrid array of phases in radians of the contours + + Return values + ------------- + contours : complex array + Array of complex numbers corresponding to the contours. + """ + # Compute the contours in H-space + H = Hmag*sp.exp(1.j*Hphase) + + # Invert H = G/(1+G) to get an expression for the contours in G-space + return H/(1.0 - H) + +# M-circle +def m_circles(mags, phase_min=-359.75, phase_max=-0.25): + """Constant-magnitude contours of the function H = G/(1+G). + + Usage + ===== + contours = m_circles(mags) + + Parameters + ---------- + mags : array-like + Array of magnitudes in dB of the M-circles + phase_min : degrees + Minimum phase in degrees of the N-circles + phase_max : degrees + Maximum phase in degrees of the N-circles + + Return values + ------------- + contours : complex array + Array of complex numbers corresponding to the contours. + """ + # Convert magnitudes and phase range into a grid suitable for + # building contours + phases = sp.radians(sp.linspace(phase_min, phase_max, 500)) + Hmag, Hphase = sp.meshgrid(10.0**(mags/20.0), phases) + return closed_loop_contours(Hmag, Hphase) + +# N-circle +def n_circles(phases, mag_min=-40.0, mag_max=12.0): + """Constant-phase contours of the function H = G/(1+G). + + Usage + ===== + contour = n_circles(angles) + + Parameters + ---------- + phases : array-like + Array of phases in degrees of the N-circles + mag_min : dB + Minimum magnitude in dB of the N-circles + mag_max : dB + Maximum magnitude in dB of the N-circles + + Return values + ------------- + contours : complex array + Array of complex numbers corresponding to the contours. + """ + # Convert phases and magnitude range into a grid suitable for + # building contours + mags = sp.linspace(10**(mag_min/20.0), 10**(mag_max/20.0), 2000) + Hphase, Hmag = sp.meshgrid(sp.radians(phases), mags) + return closed_loop_contours(Hmag, Hphase) Modified: branches/control-0.4b/src/statesp.py =================================================================== --- branches/control-0.4b/src/statesp.py 2011-04-01 04:46:30 UTC (rev 143) +++ branches/control-0.4b/src/statesp.py 2011-04-02 15:52:38 UTC (rev 144) @@ -78,7 +78,6 @@ from numpy.linalg import inv, det, solve from numpy.linalg.linalg import LinAlgError from scipy.signal import lti -from slycot import td04ad from lti import Lti import xferfcn @@ -457,6 +456,7 @@ # Already a state space system; just return it return sys elif isinstance(sys, xferfcn.TransferFunction): + from slycot import td04ad if len(kw): raise TypeError("If sys is a TransferFunction, _convertToStateSpace \ cannot take keywords.") Modified: branches/control-0.4b/src/xferfcn.py =================================================================== --- branches/control-0.4b/src/xferfcn.py 2011-04-01 04:46:30 UTC (rev 143) +++ branches/control-0.4b/src/xferfcn.py 2011-04-02 15:52:38 UTC (rev 144) @@ -79,7 +79,6 @@ polyadd, polymul, polyval, roots, sort, sqrt, zeros from scipy.signal import lti from copy import deepcopy -from slycot import tb04ad from lti import Lti import statesp @@ -707,6 +706,7 @@ return sys elif isinstance(sys, statesp.StateSpace): + from slycot import tb04ad if len(kw): raise TypeError("If sys is a StateSpace, _convertToTransferFunction \ cannot take keywords.") Added: branches/control-0.4b/tests/nichols_test.py =================================================================== --- branches/control-0.4b/tests/nichols_test.py (rev 0) +++ branches/control-0.4b/tests/nichols_test.py 2011-04-02 15:52:38 UTC (rev 144) @@ -0,0 +1,37 @@ +#!/usr/bin/env python +# +# nichols_test.py - test Nichols plot +# RMM, 31 Mar 2011 + +import unittest +import numpy as np +from control.matlab import * + +class TestStateSpace(unittest.TestCase): + """Tests for the Nichols plots.""" + + def setUp(self): + """Set up a system to test operations on.""" + + A = [[-3., 4., 2.], [-1., -3., 0.], [2., 5., 3.]] + B = [[1.], [-3.], [-2.]] + C = [[4., 2., -3.]] + D = [[0.]] + + self.sys = StateSpace(A, B, C, D) + + def testNichols(self): + """Generate a Nichols plot.""" + nichols(self.sys) + + def testNgrid(self): + """Generate a Nichols plot.""" + nichols(self.sys) + ngrid() + +def suite(): + return unittest.TestLoader().loadTestsFromTestCase(TestStateSpace) + + +if __name__ == "__main__": + unittest.main() This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |