|
From: <mur...@us...> - 2011-07-16 06:05:52
|
Revision: 168
http://python-control.svn.sourceforge.net/python-control/?rev=168&view=rev
Author: murrayrm
Date: 2011-07-16 06:05:46 +0000 (Sat, 16 Jul 2011)
Log Message:
-----------
* Moved margin command from freqplot.py to margins.py (new file); updated
matlab.py, init.py, etc to reflect changes
* Added PhaseCrossoverFrequencies() function; contributed by Steffen Waldherr
* Added initial unit tests for StabilityMargins(), PhaseCrossoverFrequencies()
Modified Paths:
--------------
trunk/ChangeLog
trunk/src/__init__.py
trunk/src/freqplot.py
trunk/src/matlab.py
trunk/src/statesp.py
trunk/tests/matlab_test.py
Added Paths:
-----------
trunk/src/margins.py
Modified: trunk/ChangeLog
===================================================================
--- trunk/ChangeLog 2011-07-13 16:03:32 UTC (rev 167)
+++ trunk/ChangeLog 2011-07-16 06:05:46 UTC (rev 168)
@@ -1,3 +1,24 @@
+2011-07-15 Richard Murray <mu...@ma...>
+
+ * tests/matlab_test.py (TestMatlab): added unittest for margin()
+ commands (calling format only)
+
+ * src/statesp.py (StateSpace): updated comments
+
+ * tests/margin_test.py: set up unit tests for StabilityMargins() and
+ PhaseCrossoverFrequencies()
+
+ * src/__init__.py: added margins.py to __init__
+
+2011-07-14 Richard Murray <mu...@ma...>
+
+ * src/margins.py (GainPhaseMargin): moved freqplot.MarginPlot to
+ margin.StabilityMargins (MarginPlot didn't actually plot anything)
+
+ * src/margins.py (PhaseCrossoverFrequencies): added new function to
+ compute frequencies that we cross real axis. Contributed by Steffen
+ Waldherr <wal...@is...>
+
2011-07-11 Richard Murray <mu...@ma...>
* src/rlocus.py: added real() and imag() to list of functions
Modified: trunk/src/__init__.py
===================================================================
--- trunk/src/__init__.py 2011-07-13 16:03:32 UTC (rev 167)
+++ trunk/src/__init__.py 2011-07-16 06:05:46 UTC (rev 168)
@@ -57,15 +57,16 @@
# Import functions from within the control system library
#! Should probably only import the exact functions we use...
-from xferfcn import *
-from statesp import *
-from freqplot import *
-from nichols import *
from bdalg import *
-from statefbk import *
from delay import *
+from freqplot import *
+from margins import *
+from mateqn import *
from modelsimp import *
+from nichols import *
from rlocus import *
-from mateqn import *
+from statefbk import *
+from statesp import *
from timeresp import ForcedResponse, InitialResponse, StepResponse, \
ImpulseResponse
+from xferfcn import *
Modified: trunk/src/freqplot.py
===================================================================
--- trunk/src/freqplot.py 2011-07-13 16:03:32 UTC (rev 167)
+++ trunk/src/freqplot.py 2011-07-16 06:05:46 UTC (rev 168)
@@ -299,101 +299,6 @@
phase = np.squeeze(phase_tmp)
plt.subplot(224); plt.loglog(omega, mag);
-
-# gain and phase margins
-# contributed by Sawyer B. Fuller <mi...@ca...>
-def MarginPlot(sysdata, deg=True):
- """Calculate gain and phase margins and associated crossover frequencies
-
- Usage:
-
- gm, pm, sm, wg, wp, ws = margin(sysdata, deg=True)
-
- Parameters
- ----------
- sysdata: linsys or (mag, phase, omega) sequence
- sys : linsys
- Linear SISO system
- mag, phase, omega : sequence of array_like
- Input magnitude, phase, and frequencies (rad/sec) sequence from
- bode frequency response data
- deg=True: boolean
- If true, all input and output phases in degrees, else in radians
-
- Returns
- -------
- gm, pm, sm, wg, wp, ws: float
- Gain margin gm, phase margin pm, stability margin sm, and
- associated crossover
- frequencies wg, wp, and ws of SISO open-loop. If more than
- one crossover frequency is detected, returns the lowest corresponding
- margin.
- """
- #TODO do this precisely without the effects of discretization of frequencies?
- #TODO assumes SISO
- #TODO unit tests, margin plot
-
- if (not getattr(sysdata, '__iter__', False)):
- sys = sysdata
- mag, phase, omega = bode(sys, deg=deg, Plot=False)
- elif len(sysdata) == 3:
- mag, phase, omega = sysdata
- else:
- raise ValueError("Margin sysdata must be either a linear system or a 3-sequence of mag, phase, omega.")
-
- if deg:
- cycle = 360.
- crossover = 180.
- else:
- cycle = 2 * np.pi
- crossover = np.pi
-
- wrapped_phase = -np.mod(phase, cycle)
-
- # phase margin from minimum phase among all gain crossovers
- neg_mag_crossings_i = np.nonzero(np.diff(mag < 1) > 0)[0]
- mag_crossings_p = wrapped_phase[neg_mag_crossings_i]
- if len(neg_mag_crossings_i) == 0:
- if mag[0] < 1: # gain always less than one
- wp = np.nan
- pm = np.inf
- else: # gain always greater than one
- print "margin: no magnitude crossings found"
- wp = np.nan
- pm = np.nan
- else:
- min_mag_crossing_i = neg_mag_crossings_i[np.argmin(mag_crossings_p)]
- wp = omega[min_mag_crossing_i]
- pm = crossover + phase[min_mag_crossing_i]
- if pm < 0:
- print "warning: system unstable: negative phase margin"
-
- # gain margin from minimum gain margin among all phase crossovers
- neg_phase_crossings_i = np.nonzero(np.diff(wrapped_phase < -crossover) > 0)[0]
- neg_phase_crossings_g = mag[neg_phase_crossings_i]
- if len(neg_phase_crossings_i) == 0:
- wg = np.nan
- gm = np.inf
- else:
- min_phase_crossing_i = neg_phase_crossings_i[
- np.argmax(neg_phase_crossings_g)]
- wg = omega[min_phase_crossing_i]
- gm = abs(1/mag[min_phase_crossing_i])
- if gm < 1:
- print "warning: system unstable: gain margin < 1"
-
- # stability margin from minimum abs distance from -1 point
- if deg:
- phase_rad = phase * np.pi / 180.
- else:
- phase_rad = phase
- L = mag * np.exp(1j * phase_rad) # complex loop response to -1 pt
- min_Lplus1_i = np.argmin(np.abs(L + 1))
- sm = np.abs(L[min_Lplus1_i] + 1)
- ws = phase[min_Lplus1_i]
-
- return gm, pm, sm, wg, wp, ws
-
#
# Utility functions
#
@@ -461,4 +366,3 @@
bode = BodePlot
nyquist = NyquistPlot
gangof4 = GangOf4Plot
-margin = MarginPlot
Added: trunk/src/margins.py
===================================================================
--- trunk/src/margins.py (rev 0)
+++ trunk/src/margins.py 2011-07-16 06:05:46 UTC (rev 168)
@@ -0,0 +1,193 @@
+"""margin.py
+
+Functions for computing stability margins and related functions.
+
+Routeins in this module:
+
+margin.StabilityMargins
+margin.PhaseCrossoverFrequencies
+"""
+
+"""Copyright (c) 2011 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.
+
+Author: Richard M. Murray
+Date: 14 July 2011
+
+$Id: xferfcn.py 165 2011-06-26 02:44:09Z murrayrm $
+
+"""
+
+import xferfcn
+from freqplot import bode
+import numpy as np
+
+# gain and phase margins
+# contributed by Sawyer B. Fuller <mi...@ca...>
+#! TODO - need to add unit test functions
+def StabilityMargins(sysdata, deg=True):
+ """Calculate gain, phase and stability margins and associated
+ crossover frequencies.
+
+ Usage:
+
+ gm, pm, sm, wg, wp, ws = StabilityMargins(sysdata, deg=True)
+
+ Parameters
+ ----------
+ sysdata: linsys or (mag, phase, omega) sequence
+ sys : linsys
+ Linear SISO system
+ mag, phase, omega : sequence of array_like
+ Input magnitude, phase, and frequencies (rad/sec) sequence from
+ bode frequency response data
+ deg=True: boolean
+ If true, all input and output phases in degrees, else in radians
+
+ Returns
+ -------
+ gm, pm, sm, wg, wp, ws: float
+ Gain margin gm, phase margin pm, stability margin sm, and
+ associated crossover
+ frequencies wg, wp, and ws of SISO open-loop. If more than
+ one crossover frequency is detected, returns the lowest corresponding
+ margin.
+ """
+ #TODO do this precisely without the effects of discretization of frequencies?
+ #TODO assumes SISO
+ #TODO unit tests, margin plot
+
+ if (not getattr(sysdata, '__iter__', False)):
+ sys = sysdata
+ mag, phase, omega = bode(sys, deg=deg, Plot=False)
+ elif len(sysdata) == 3:
+ mag, phase, omega = sysdata
+ else:
+ raise ValueError("Margin sysdata must be either a linear system or a 3-sequence of mag, phase, omega.")
+
+ if deg:
+ cycle = 360.
+ crossover = 180.
+ else:
+ cycle = 2 * np.pi
+ crossover = np.pi
+
+ wrapped_phase = -np.mod(phase, cycle)
+
+ # phase margin from minimum phase among all gain crossovers
+ neg_mag_crossings_i = np.nonzero(np.diff(mag < 1) > 0)[0]
+ mag_crossings_p = wrapped_phase[neg_mag_crossings_i]
+ if len(neg_mag_crossings_i) == 0:
+ if mag[0] < 1: # gain always less than one
+ wp = np.nan
+ pm = np.inf
+ else: # gain always greater than one
+ print "margin: no magnitude crossings found"
+ wp = np.nan
+ pm = np.nan
+ else:
+ min_mag_crossing_i = neg_mag_crossings_i[np.argmin(mag_crossings_p)]
+ wp = omega[min_mag_crossing_i]
+ pm = crossover + phase[min_mag_crossing_i]
+ if pm < 0:
+ print "warning: system unstable: negative phase margin"
+
+ # gain margin from minimum gain margin among all phase crossovers
+ neg_phase_crossings_i = np.nonzero(np.diff(wrapped_phase < -crossover) > 0)[0]
+ neg_phase_crossings_g = mag[neg_phase_crossings_i]
+ if len(neg_phase_crossings_i) == 0:
+ wg = np.nan
+ gm = np.inf
+ else:
+ min_phase_crossing_i = neg_phase_crossings_i[
+ np.argmax(neg_phase_crossings_g)]
+ wg = omega[min_phase_crossing_i]
+ gm = abs(1/mag[min_phase_crossing_i])
+ if gm < 1:
+ print "warning: system unstable: gain margin < 1"
+
+ # stability margin from minimum abs distance from -1 point
+ if deg:
+ phase_rad = phase * np.pi / 180.
+ else:
+ phase_rad = phase
+ L = mag * np.exp(1j * phase_rad) # complex loop response to -1 pt
+ min_Lplus1_i = np.argmin(np.abs(L + 1))
+ sm = np.abs(L[min_Lplus1_i] + 1)
+ ws = phase[min_Lplus1_i]
+
+ return gm, pm, sm, wg, wp, ws
+
+# Contributed by Steffen Waldherr <wal...@is...>
+#! TODO - need to add test functions
+def PhaseCrossoverFrequencies(sys):
+ """
+ Compute frequencies and gains at intersections with real axis
+ in Nyquist plot.
+
+ Call as:
+ omega, gain = PhaseCrossoverFrequencies()
+
+ Returns
+ -------
+ omega: 1d array of (non-negative) frequencies where Nyquist plot
+ intersects the real axis
+
+ gain: 1d array of corresponding gains
+
+ Examples
+ --------
+ >>> tf = TransferFunction([1], [1, 2, 3, 4])
+ >>> PhaseCrossoverFrequenies(tf)
+ (array([ 1.73205081, 0. ]), array([-0.5 , 0.25]))
+ """
+
+ # Convert to a transfer function
+ tf = xferfcn._convertToTransferFunction(sys)
+
+ # if not siso, fall back to (0,0) element
+ #! TODO: should add a check and warning here
+ num = tf.num[0][0]
+ den = tf.den[0][0]
+
+ # Compute frequencies that we cross over the real axis
+ numj = (1.j)**np.arange(len(num)-1,-1,-1)*num
+ denj = (-1.j)**np.arange(len(den)-1,-1,-1)*den
+ allfreq = np.roots(np.imag(np.polymul(numj,denj)))
+ realfreq = np.real(allfreq[np.isreal(allfreq)])
+ realposfreq = realfreq[realfreq >= 0.]
+
+ # using real() to avoid rounding errors and results like 1+0j
+ # it would be nice to have a vectorized version of self.evalfr here
+ gain = np.real(np.asarray([tf.evalfr(f)[0][0] for f in realposfreq]))
+
+ return realposfreq, gain
Modified: trunk/src/matlab.py
===================================================================
--- trunk/src/matlab.py 2011-07-13 16:03:32 UTC (rev 167)
+++ trunk/src/matlab.py 2011-07-16 06:05:46 UTC (rev 168)
@@ -72,6 +72,7 @@
import ctrlutil
import freqplot
import timeresp
+import margins
from statesp import StateSpace, _rss_generate, _convertToStateSpace
from xferfcn import TransferFunction, _convertToTransferFunction
from lti import Lti #base class of StateSpace, TransferFunction
@@ -1025,7 +1026,6 @@
rlist = RootLocus(sys, klist, **keywords)
return rlist, klist
-
def margin(*args):
"""Calculate gain and phase margins and associated crossover frequencies
@@ -1060,16 +1060,15 @@
"""
if len(args) == 1:
sys = args[0]
- margins = freqplot.margin(sys)
+ margin = margins.StabilityMargins(sys)
elif len(args) == 3:
- margins = freqplot.margin(args)
+ margin = margins.StabilityMargins(args)
else:
raise ValueError("Margin needs 1 or 3 arguments; received %i."
% len(args))
- return margins[0], margins[1], margins[3], margins[4]
+ return margin[0], margin[1], margin[3], margin[4]
-
def dcgain(*args):
'''
Compute the gain of the system in steady state.
Modified: trunk/src/statesp.py
===================================================================
--- trunk/src/statesp.py 2011-07-13 16:03:32 UTC (rev 167)
+++ trunk/src/statesp.py 2011-07-16 06:05:46 UTC (rev 168)
@@ -208,7 +208,7 @@
return StateSpace(self.A, self.B, -self.C, -self.D)
- # Addition of two transfer functions (parallel interconnection)
+ # Addition of two state space systems (parallel interconnection)
def __add__(self, other):
"""Add two LTI systems (parallel connection)."""
@@ -244,7 +244,7 @@
return self + other
- # Subtraction of two transfer functions (parallel interconnection)
+ # Subtraction of two state space systems (parallel interconnection)
def __sub__(self, other):
"""Subtract two LTI systems."""
@@ -255,7 +255,7 @@
return other + (-self)
- # Multiplication of two transfer functions (series interconnection)
+ # Multiplication of two state space systems (series interconnection)
def __mul__(self, other):
"""Multiply two LTI objects (serial connection)."""
@@ -284,7 +284,7 @@
return StateSpace(A, B, C, D)
- # Right multiplication of two transfer functions (series interconnection)
+ # Right multiplication of two state space systems (series interconnection)
# Just need to convert LH argument to a state space object
def __rmul__(self, other):
"""Right multiply two LTI objects (serial connection)."""
Modified: trunk/tests/matlab_test.py
===================================================================
--- trunk/tests/matlab_test.py 2011-07-13 16:03:32 UTC (rev 167)
+++ trunk/tests/matlab_test.py 2011-07-16 06:05:46 UTC (rev 168)
@@ -186,6 +186,13 @@
[-0.1391, 48.9776]])
yout, _t, _xout = lsim(self.mimo_ss1, u, t, x0)
np.testing.assert_array_almost_equal(yout, youttrue, decimal=4)
+
+ def testMargin(self):
+ #! TODO: check results to make sure they are OK
+ gm, pm, wg, wp = margin(self.siso_tf1);
+ gm, pm, wg, wp = margin(self.siso_tf2);
+ gm, pm, wg, wp = margin(self.siso_ss1);
+ gm, pm, wg, wp = margin(self.siso_ss2);
def testDcgain(self):
#Create different forms of a SISO system
This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site.
|