|
From: <mur...@us...> - 2012-08-30 05:44:39
|
Revision: 185
http://python-control.svn.sourceforge.net/python-control/?rev=185&view=rev
Author: murrayrm
Date: 2012-08-30 05:44:32 +0000 (Thu, 30 Aug 2012)
Log Message:
-----------
* Fixed bug in statespace -> transfer function conversation in the way that complex conjugate pairs of poles with multiplicity > 1 were handled
* Fixed bugs in tests/convert_test in the way that state space and transfer function conversions were tested; added tf to ss comparison
* Replace nd.size() with len() in place() to fix bug reported by R. Bucher
* See ChangeLog for more detailed descriptions
Modified Paths:
--------------
trunk/ChangeLog
trunk/src/__init__.py
trunk/src/matlab.py
trunk/src/statefbk.py
trunk/src/statesp.py
trunk/src/xferfcn.py
trunk/tests/convert_test.py
trunk/tests/matlab_test.py
Modified: trunk/ChangeLog
===================================================================
--- trunk/ChangeLog 2012-08-25 17:45:56 UTC (rev 184)
+++ trunk/ChangeLog 2012-08-30 05:44:32 UTC (rev 185)
@@ -1,5 +1,36 @@
+2012-08-29 Richard Murray <mu...@al...>
+
+ * src/xferfcn.py (TransferFunction._common_den): fixed up code for
+ case where poles have multiplicity > 1. Set end limits of check to
+ avoid overruning the list + combine complex conjugate pole pairs
+ from the outside in to prevent small errors from growing.
+
+2012-08-26 Richard Murray <mu...@al...>
+
+ * tests/convert_test.py (TestConvert.testConvert): replaced previous
+ test of transfer function coefficients with a frequency response
+ test. This is necessary because various degenerate conditions will
+ generate a situation where the transfer functions are of different
+ forms but still equal. Eg 0/1 = 1e-17 s + 1e-16 / (s^2 + s + 1).
+ Still getting errors, but looks like actual problem in conversion.
+
+ * src/statesp.py (_mimo2siso): set default value for warn_conversion
+ to False
+
+ * tests/convert_test.py (TestConvert.testConvert): Added check to
+ make sure that we don't create problems with uncontrollable or
+ unobservable systems.
+
2012-08-25 Richard Murray <mu...@al...>
+ * src/xferfcn.py (TransferFunction._common_den): identified bug in
+ the way that common denominators are computed; see comments in code
+ regarding complex conjugate pairs.
+
+ * src/statefbk.py (place): repalced nd.size(placed_eigs) with
+ len(placed_eigs) to fix intermittent problems pointed out by Roberto
+ Bucher in control-0.3c.
+
* tests/rlocus_test.py (TestRootLocus.testRootLocus): added sort()
to test to get rid of problems in which ordering was generating an
error.
Modified: trunk/src/__init__.py
===================================================================
--- trunk/src/__init__.py 2012-08-25 17:45:56 UTC (rev 184)
+++ trunk/src/__init__.py 2012-08-30 05:44:32 UTC (rev 185)
@@ -74,9 +74,9 @@
from xferfcn import TransferFunction
# Import some of the more common (and benign) MATLAB shortcuts
+# By default, don't import conflicting commands here
from matlab import ss, tf, ss2tf, tf2ss, drss
from matlab import pole, zero, evalfr, freqresp, dcgain
from matlab import nichols, rlocus, margin
# bode and nyquist come directly from freqplot.py
from matlab import step, impulse, initial, lsim
-
Modified: trunk/src/matlab.py
===================================================================
--- trunk/src/matlab.py 2012-08-25 17:45:56 UTC (rev 184)
+++ trunk/src/matlab.py 2012-08-30 05:44:32 UTC (rev 185)
@@ -984,6 +984,7 @@
# Warn about unimplemented plotstyles
#! TODO: remove this when plot styles are implemented in bode()
+ #! TODO: uncomment unit test code that tests this out
if (len(plotstyle) != 0):
print("Warning (matabl.bode): plot styles not implemented");
Modified: trunk/src/statefbk.py
===================================================================
--- trunk/src/statefbk.py 2012-08-25 17:45:56 UTC (rev 184)
+++ trunk/src/statefbk.py 2012-08-30 05:44:32 UTC (rev 185)
@@ -93,7 +93,7 @@
# Call SLICOT routine to place the eigenvalues
A_z,w,nfp,nap,nup,F,Z = \
- sb01bd(B_mat.shape[0], B_mat.shape[1], np.size(placed_eigs), alpha,
+ sb01bd(B_mat.shape[0], B_mat.shape[1], len(placed_eigs), alpha,
A_mat, B_mat, placed_eigs, 'C');
# Return the gain matrix, with MATLAB gain convention
Modified: trunk/src/statesp.py
===================================================================
--- trunk/src/statesp.py 2012-08-25 17:45:56 UTC (rev 184)
+++ trunk/src/statesp.py 2012-08-30 05:44:32 UTC (rev 185)
@@ -472,7 +472,7 @@
index = [len(den) - 1 for i in range(sys.outputs)]
# Repeat the common denominator along the rows.
den = array([den for i in range(sys.outputs)])
- # TODO: transfer function to state space conversion is still buggy!
+ #! TODO: transfer function to state space conversion is still buggy!
#print num
#print shape(num)
ssout = td04ad('R',sys.inputs, sys.outputs, index, den, num,tol=0.0)
@@ -624,7 +624,7 @@
return StateSpace(A, B, C, D)
# Convert a MIMO system to a SISO system
-def _mimo2siso(sys, input, output, warn_conversion):
+def _mimo2siso(sys, input, output, warn_conversion=False):
#pylint: disable=W0622
"""
Convert a MIMO system to a SISO system. (Convert a system with multiple
Modified: trunk/src/xferfcn.py
===================================================================
--- trunk/src/xferfcn.py 2012-08-25 17:45:56 UTC (rev 184)
+++ trunk/src/xferfcn.py 2012-08-30 05:44:32 UTC (rev 185)
@@ -638,14 +638,34 @@
if abs(poles[n].imag) > 10 * eps:
# To prevent buildup of imaginary part error, handle complex
# pole pairs together.
- quad = polymul([1., -poles[n]], [1., -poles[n+1]])
- assert all(quad.imag < 10 * eps), \
- "The quadratic has a nontrivial imaginary part: %g" \
- % quad.imag.max()
- quad = quad.real
+ #
+ # Because we might have repeated real parts of poles
+ # and the fact that we are using lexigraphical
+ # ordering, we can't just combine adjacent poles.
+ # Instead, we have to figure out the multiplicity
+ # first, then multiple the pairs from the outside in.
- den = polymul(den, quad)
- n += 2
+ # Figure out the multiplicity
+ m = 1; # multiplicity count
+ while (n+m < len(poles) and
+ poles[n].real == poles[n+m].real and
+ poles[n].imag * poles[n+m].imag > 0):
+ m += 1
+
+ if (m > 1):
+ print "Found pole with multiplicity %d" % m
+ # print "Poles = ", poles
+
+ # Multiple pairs from the outside in
+ for i in range(m):
+ quad = polymul([1., -poles[n]], [1., -poles[n+2*(m-i)-1]])
+ assert all(quad.imag < 10 * eps), \
+ "Quadratic has a nontrivial imaginary part: %g" \
+ % quad.imag.max()
+
+ den = polymul(den, quad.real)
+ n += 1 # move to next pair
+ n += m # skip past conjugate pairs
else:
den = polymul(den, [1., -poles[n].real])
n += 1
@@ -654,6 +674,7 @@
num = deepcopy(self.num)
if isinstance(den,float):
den = array([den])
+
for i in range(self.outputs):
for j in range(self.inputs):
# The common denominator has leading coefficient 1. Scale out
@@ -661,9 +682,11 @@
assert self.den[i][j][0], "The i = %i, j = %i denominator has \
a zero leading coefficient." % (i, j)
num[i][j] = num[i][j] / self.den[i][j][0]
+
# Multiply in the missing poles.
for p in missingpoles[i][j]:
num[i][j] = polymul(num[i][j], [1., -p])
+
# Pad all numerator polynomials with zeros so that the numerator arrays
# are the same size as the denominator.
for i in range(self.outputs):
@@ -672,11 +695,12 @@
if(pad>0):
num[i][j] = insert(num[i][j], zeros(pad),
zeros(pad))
+
# Finally, convert the numerator to a 3-D array.
num = array(num)
# Remove trivial imaginary parts. Check for nontrivial imaginary parts.
if any(abs(num.imag) > sqrt(eps)):
- print ("Warning: The numerator has a nontrivial nontrivial part: %g"
+ print ("Warning: The numerator has a nontrivial imaginary part: %g"
% abs(num.imag).max())
num = num.real
Modified: trunk/tests/convert_test.py
===================================================================
--- trunk/tests/convert_test.py 2012-08-25 17:45:56 UTC (rev 184)
+++ trunk/tests/convert_test.py 2012-08-30 05:44:32 UTC (rev 185)
@@ -16,6 +16,7 @@
import unittest
import numpy as np
+import control
import control.matlab as matlab
class TestConvert(unittest.TestCase):
@@ -27,9 +28,9 @@
# Number of times to run each of the randomized tests.
self.numTests = 1 #almost guarantees failure
# Maximum number of states to test + 1
- self.maxStates = 20
+ self.maxStates = 4
# Maximum number of inputs and outputs to test + 1
- self.maxIO = 10
+ self.maxIO = 5
# Set to True to print systems to the output.
self.debug = False
@@ -42,20 +43,35 @@
def testConvert(self):
"""Test state space to transfer function conversion."""
- #Currently it only tests that a TF->SS->TF generates an unchanged TF
verbose = self.debug
+ from control.statesp import _mimo2siso
#print __doc__
+ # Machine precision for floats.
+ eps = np.finfo(float).eps
+
for states in range(1, self.maxStates):
for inputs in range(1, self.maxIO):
for outputs in range(1, self.maxIO):
- #start with a random SS system and transform to TF
- #then back to SS, check that the matrices are the same.
+ # start with a random SS system and transform to TF then
+ # back to SS, check that the matrices are the same.
ssOriginal = matlab.rss(states, inputs, outputs)
if (verbose):
self.printSys(ssOriginal, 1)
+ # Make sure the system is not degenerate
+ Cmat = control.ctrb(ssOriginal.A, ssOriginal.B)
+ if (np.linalg.matrix_rank(Cmat) != states):
+ if (verbose):
+ print " skipping (not reachable)"
+ continue
+ Omat = control.obsv(ssOriginal.A, ssOriginal.C)
+ if (np.linalg.matrix_rank(Omat) != states):
+ if (verbose):
+ print " skipping (not observable)"
+ continue
+
tfOriginal = matlab.tf(ssOriginal)
if (verbose):
self.printSys(tfOriginal, 2)
@@ -67,27 +83,77 @@
tfTransformed = matlab.tf(ssTransformed)
if (verbose):
self.printSys(tfTransformed, 4)
-
+
+ # Check to see if the state space systems have same dim
+ if (ssOriginal.states != ssTransformed.states):
+ print "WARNING: state space dimension mismatch: " + \
+ "%d versus %d" % \
+ (ssOriginal.states, ssTransformed.states)
+
+ # Now make sure the frequency responses match
+ # Since bode() only handles SISO, go through each I/O pair
+ # For phase, take sine and cosine to avoid +/- 360 offset
for inputNum in range(inputs):
for outputNum in range(outputs):
- np.testing.assert_array_almost_equal(\
- tfOriginal.num[outputNum][inputNum], \
- tfTransformed.num[outputNum][inputNum], \
- err_msg='numerator mismatch')
+ if (verbose):
+ print "Checking input %d, output %d" \
+ % (inputNum, outputNum)
+ ssorig_mag, ssorig_phase, ssorig_omega = \
+ control.bode(_mimo2siso(ssOriginal, \
+ inputNum, outputNum), \
+ deg=False, Plot=False)
+ ssorig_real = ssorig_mag * np.cos(ssorig_phase)
+ ssorig_imag = ssorig_mag * np.sin(ssorig_phase)
+
+ #
+ # Make sure TF has same frequency response
+ #
+ num = tfOriginal.num[outputNum][inputNum]
+ den = tfOriginal.den[outputNum][inputNum]
+ tforig = control.tf(num, den)
+
+ tforig_mag, tforig_phase, tforig_omega = \
+ control.bode(tforig, ssorig_omega, \
+ deg=False, Plot=False)
+
+ tforig_real = tforig_mag * np.cos(tforig_phase)
+ tforig_imag = tforig_mag * np.sin(tforig_phase)
+ np.testing.assert_array_almost_equal( \
+ ssorig_real, tforig_real)
+ np.testing.assert_array_almost_equal( \
+ ssorig_imag, tforig_imag)
+
+ #
+ # Make sure xform'd SS has same frequency response
+ #
+ ssxfrm_mag, ssxfrm_phase, ssxfrm_omega = \
+ control.bode(_mimo2siso(ssTransformed, \
+ inputNum, outputNum), \
+ ssorig_omega, \
+ deg=False, Plot=False)
+ ssxfrm_real = ssxfrm_mag * np.cos(ssxfrm_phase)
+ ssxfrm_imag = ssxfrm_mag * np.sin(ssxfrm_phase)
+ np.testing.assert_array_almost_equal( \
+ ssorig_real, ssxfrm_real)
+ np.testing.assert_array_almost_equal( \
+ ssorig_imag, ssxfrm_imag)
+
+ #
+ # Make sure xform'd TF has same frequency response
+ #
+ num = tfTransformed.num[outputNum][inputNum]
+ den = tfTransformed.den[outputNum][inputNum]
+ tfxfrm = control.tf(num, den)
+ tfxfrm_mag, tfxfrm_phase, tfxfrm_omega = \
+ control.bode(tfxfrm, ssorig_omega, \
+ deg=False, Plot=False)
- np.testing.assert_array_almost_equal(\
- tfOriginal.den[outputNum][inputNum], \
- tfTransformed.den[outputNum][inputNum],
- err_msg='denominator mismatch')
-
- #To test the ss systems is harder because they aren't the same
- #realization. This could be done with checking that they have the
- #same freq response with bode, but apparently it doesn't work
- #the way it should right now:
- ## Bode should work like this:
- #[mag,phase,freq]=bode(sys)
- #it doesn't seem to......
- #This should be added.
+ tfxfrm_real = tfxfrm_mag * np.cos(tfxfrm_phase)
+ tfxfrm_imag = tfxfrm_mag * np.sin(tfxfrm_phase)
+ np.testing.assert_array_almost_equal( \
+ ssorig_real, tfxfrm_real)
+ np.testing.assert_array_almost_equal( \
+ ssorig_imag, tfxfrm_imag)
def suite():
return unittest.TestLoader().loadTestsFromTestCase(TestConvert)
Modified: trunk/tests/matlab_test.py
===================================================================
--- trunk/tests/matlab_test.py 2012-08-25 17:45:56 UTC (rev 184)
+++ trunk/tests/matlab_test.py 2012-08-30 05:44:32 UTC (rev 185)
@@ -238,7 +238,8 @@
w = logspace(-3, 3);
bode(self.siso_ss1, w)
bode(self.siso_ss1, self.siso_tf2, w)
- bode(self.siso_ss1, '-', self.siso_tf1, 'b--', self.siso_tf2, 'k.')
+# Not yet implemented
+# bode(self.siso_ss1, '-', self.siso_tf1, 'b--', self.siso_tf2, 'k.')
def testRlocus(self):
rlocus(self.siso_ss1)
This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site.
|