From 564b1dcc9e856e2e8958d1b108d04af5d97f70c0 Mon Sep 17 00:00:00 2001 From: Rory Yorke Date: Thu, 11 Aug 2016 21:28:03 +0200 Subject: [PATCH 1/4] BugFix: allow straightforward creation of static gain StateSpace objects. Allows StateSpace([],[],[],D), which failed previously. Static gains have sizes enforced as follows: A 0-by-0, B 0-by-ninputs, C noutputs-by-0. Tests added for instantiation, and sum, product, feedback, and appending, of 1x1, 2x3, and 3x2 static gains StateSpace objects. --- control/statesp.py | 37 ++++++++++++++------------- control/tests/statesp_test.py | 48 ++++++++++++++++++++++++++++++++++- 2 files changed, 66 insertions(+), 19 deletions(-) diff --git a/control/statesp.py b/control/statesp.py index 80da2a5f8..40811a065 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -122,34 +122,35 @@ def __init__(self, *args): else: raise ValueError("Needs 1 or 4 arguments; received %i." % len(args)) - # Here we're going to convert inputs to matrices, if the user gave a - # non-matrix type. - #! TODO: [A, B, C, D] = map(matrix, [A, B, C, D])? - matrices = [A, B, C, D] - for i in range(len(matrices)): - # Convert to matrix first, if necessary. - matrices[i] = matrix(matrices[i]) - [A, B, C, D] = matrices - - LTI.__init__(self, B.shape[1], C.shape[0], dt) + A, B, C, D = [matrix(M) for M in (A, B, C, D)] + + # TODO: use super here? + LTI.__init__(self, inputs=D.shape[1], outputs=D.shape[0], dt=dt) self.A = A self.B = B self.C = C self.D = D - self.states = A.shape[0] + self.states = A.shape[1] + + if 0 == self.states: + # static gain + # matrix's default "empty" shape is 1x0 + A.shape = (0,0) + B.shape = (0,self.inputs) + C.shape = (self.outputs,0) # Check that the matrix sizes are consistent. - if self.states != A.shape[1]: + if self.states != A.shape[0]: raise ValueError("A must be square.") if self.states != B.shape[0]: - raise ValueError("B must have the same row size as A.") + raise ValueError("A and B must have the same number of rows.") if self.states != C.shape[1]: - raise ValueError("C must have the same column size as A.") - if self.inputs != D.shape[1]: - raise ValueError("D must have the same column size as B.") - if self.outputs != D.shape[0]: - raise ValueError("D must have the same row size as C.") + raise ValueError("A and C C must have the same number of columns.") + if self.inputs != B.shape[1]: + raise ValueError("B and D must have the same number of columns.") + if self.outputs != C.shape[0]: + raise ValueError("C and D must have the same number of rows.") # Check for states that don't do anything, and remove them. self._remove_useless_states() diff --git a/control/tests/statesp_test.py b/control/tests/statesp_test.py index c19f4f38b..5afe5c5e9 100644 --- a/control/tests/statesp_test.py +++ b/control/tests/statesp_test.py @@ -5,7 +5,8 @@ import unittest import numpy as np -from scipy.linalg import eigvals +from numpy.linalg import solve +from scipy.linalg import eigvals, block_diag from control import matlab from control.statesp import StateSpace, _convertToStateSpace from control.xferfcn import TransferFunction @@ -235,6 +236,51 @@ def test_dcgain(self): sys3 = StateSpace(0., 1., 1., 0.) np.testing.assert_equal(sys3.dcgain(), np.nan) + + def test_scalarStaticGain(self): + """Regression: can we create a scalar static gain?""" + g1=StateSpace([],[],[],[2]) + g2=StateSpace([],[],[],[3]) + + # make sure StateSpace internals, specifically ABC matrix + # sizes, are OK for LTI operations + g3 = g1*g2 + self.assertEqual(6, g3.D[0,0]) + g4 = g1+g2 + self.assertEqual(5, g4.D[0,0]) + g5 = g1.feedback(g2) + self.assertAlmostEqual(2./7, g5.D[0,0]) + g6 = g1.append(g2) + np.testing.assert_array_equal(np.diag([2,3]),g6.D) + + def test_matrixStaticGain(self): + """Regression: can we create a scalar static gain?""" + d1 = np.matrix([[1,2,3],[4,5,6]]) + d2 = np.matrix([[7,8],[9,10],[11,12]]) + g1=StateSpace([],[],[],d1) + g2=StateSpace([],[],[],d2) + g3=StateSpace([],[],[],d2.T) + + h1 = g1*g2 + np.testing.assert_array_equal(d1*d2, h1.D) + h2 = g1+g3 + np.testing.assert_array_equal(d1+d2.T, h2.D) + h3 = g1.feedback(g2) + np.testing.assert_array_almost_equal(solve(np.eye(2)+d1*d2,d1), h3.D) + h4 = g1.append(g2) + np.testing.assert_array_equal(block_diag(d1,d2),h4.D) + + + def test_BadEmptyMatrices(self): + """Mismatched ABCD matrices when some are empty""" + self.assertRaises(ValueError,StateSpace, [1], [], [], [1]) + self.assertRaises(ValueError,StateSpace, [1], [1], [], [1]) + self.assertRaises(ValueError,StateSpace, [1], [], [1], [1]) + self.assertRaises(ValueError,StateSpace, [], [1], [], [1]) + self.assertRaises(ValueError,StateSpace, [], [1], [1], [1]) + self.assertRaises(ValueError,StateSpace, [], [], [1], [1]) + self.assertRaises(ValueError,StateSpace, [1], [1], [1], []) + class TestRss(unittest.TestCase): """These are tests for the proper functionality of statesp.rss.""" From 4a74cc559468256d179b9f72f0886208ff6b0771 Mon Sep 17 00:00:00 2001 From: Rory Yorke Date: Thu, 11 Aug 2016 22:01:59 +0200 Subject: [PATCH 2/4] BugFix: fix Python 2.7 failure. On Python 2.7, the special case "all states useless" in _remove_useless_states resulted in A=[[0]] (and similarly for B and C). The special case is no longer needed, since empty A, B, C matrices can be handled. numpy.delete does the right thing w.r.t. matrix sizes (e.g., deleting all columns of a nxm matrix gives an nx0 matrix). Added test for this. --- control/statesp.py | 17 +++++------------ control/tests/statesp_test.py | 19 ++++++++++++++++++- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/control/statesp.py b/control/statesp.py index 40811a065..b74c5cb71 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -146,7 +146,7 @@ def __init__(self, *args): if self.states != B.shape[0]: raise ValueError("A and B must have the same number of rows.") if self.states != C.shape[1]: - raise ValueError("A and C C must have the same number of columns.") + raise ValueError("A and C must have the same number of columns.") if self.inputs != B.shape[1]: raise ValueError("B and D must have the same number of columns.") if self.outputs != C.shape[0]: @@ -180,17 +180,10 @@ def _remove_useless_states(self): useless.append(i) # Remove the useless states. - if all(useless == range(self.states)): - # All the states were useless. - self.A = zeros((1, 1)) - self.B = zeros((1, self.inputs)) - self.C = zeros((self.outputs, 1)) - else: - # A more typical scenario. - self.A = delete(self.A, useless, 0) - self.A = delete(self.A, useless, 1) - self.B = delete(self.B, useless, 0) - self.C = delete(self.C, useless, 1) + self.A = delete(self.A, useless, 0) + self.A = delete(self.A, useless, 1) + self.B = delete(self.B, useless, 0) + self.C = delete(self.C, useless, 1) self.states = self.A.shape[0] self.inputs = self.B.shape[1] diff --git a/control/tests/statesp_test.py b/control/tests/statesp_test.py index 5afe5c5e9..39f4a965d 100644 --- a/control/tests/statesp_test.py +++ b/control/tests/statesp_test.py @@ -254,10 +254,14 @@ def test_scalarStaticGain(self): np.testing.assert_array_equal(np.diag([2,3]),g6.D) def test_matrixStaticGain(self): - """Regression: can we create a scalar static gain?""" + """Regression: can we create matrix static gains?""" d1 = np.matrix([[1,2,3],[4,5,6]]) d2 = np.matrix([[7,8],[9,10],[11,12]]) g1=StateSpace([],[],[],d1) + + # _remove_useless_states was making A = [[0]] + self.assertEqual((0,0), g1.A.shape) + g2=StateSpace([],[],[],d2) g3=StateSpace([],[],[],d2.T) @@ -271,6 +275,19 @@ def test_matrixStaticGain(self): np.testing.assert_array_equal(block_diag(d1,d2),h4.D) + def test_remove_useless_states(self): + """Regression: _remove_useless_states gives correct ABC sizes""" + g1 = StateSpace(np.zeros((3,3)), + np.zeros((3,4)), + np.zeros((5,3)), + np.zeros((5,4))) + self.assertEqual((0,0), g1.A.shape) + self.assertEqual((0,4), g1.B.shape) + self.assertEqual((5,0), g1.C.shape) + self.assertEqual((5,4), g1.D.shape) + self.assertEqual(0, g1.states) + + def test_BadEmptyMatrices(self): """Mismatched ABCD matrices when some are empty""" self.assertRaises(ValueError,StateSpace, [1], [], [], [1]) From 09f9bab4e5d984dffc9215d2e8e80f2958873759 Mon Sep 17 00:00:00 2001 From: Rory Yorke Date: Fri, 12 Aug 2016 18:54:53 +0200 Subject: [PATCH 3/4] BugFix: allow minreal on static gain StateSpace objects Do this by only calling Slycot's tb01pd for non-static systems. --- control/statesp.py | 28 ++++++++++++++++------------ control/tests/statesp_test.py | 11 +++++++++++ 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/control/statesp.py b/control/statesp.py index b74c5cb71..0aa568e38 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -471,18 +471,22 @@ def feedback(self, other=1, sign=-1): def minreal(self, tol=0.0): """Calculate a minimal realization, removes unobservable and uncontrollable states""" - try: - from slycot import tb01pd - B = empty((self.states, max(self.inputs, self.outputs))) - B[:,:self.inputs] = self.B - C = empty((max(self.outputs, self.inputs), self.states)) - C[:self.outputs,:] = self.C - A, B, C, nr = tb01pd(self.states, self.inputs, self.outputs, - self.A, B, C, tol=tol) - return StateSpace(A[:nr,:nr], B[:nr,:self.inputs], - C[:self.outputs,:nr], self.D) - except ImportError: - raise TypeError("minreal requires slycot tb01pd") + if self.states: + try: + from slycot import tb01pd + B = empty((self.states, max(self.inputs, self.outputs))) + B[:,:self.inputs] = self.B + C = empty((max(self.outputs, self.inputs), self.states)) + C[:self.outputs,:] = self.C + A, B, C, nr = tb01pd(self.states, self.inputs, self.outputs, + self.A, B, C, tol=tol) + return StateSpace(A[:nr,:nr], B[:nr,:self.inputs], + C[:self.outputs,:nr], self.D) + except ImportError: + raise TypeError("minreal requires slycot tb01pd") + else: + return StateSpace(self) + # TODO: add discrete time check def returnScipySignalLTI(self): diff --git a/control/tests/statesp_test.py b/control/tests/statesp_test.py index 39f4a965d..f33d24a26 100644 --- a/control/tests/statesp_test.py +++ b/control/tests/statesp_test.py @@ -298,6 +298,17 @@ def test_BadEmptyMatrices(self): self.assertRaises(ValueError,StateSpace, [], [], [1], [1]) self.assertRaises(ValueError,StateSpace, [1], [1], [1], []) + + def test_minrealStaticGain(self): + """Regression: minreal on static gain was failing""" + g1 = StateSpace([],[],[],[1]) + g2 = g1.minreal() + np.testing.assert_array_equal(g1.A, g2.A) + np.testing.assert_array_equal(g1.B, g2.B) + np.testing.assert_array_equal(g1.C, g2.C) + np.testing.assert_array_equal(g1.D, g2.D) + + class TestRss(unittest.TestCase): """These are tests for the proper functionality of statesp.rss.""" From c84debb49c14abe1df4c537ad42cc5e5a9fd75a3 Mon Sep 17 00:00:00 2001 From: Rory Yorke Date: Wed, 31 Aug 2016 21:22:38 +0200 Subject: [PATCH 4/4] BugFix: pole of stateless StateSpace object is empty array --- control/statesp.py | 2 +- control/tests/statesp_test.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/control/statesp.py b/control/statesp.py index 0aa568e38..1c220630a 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -399,7 +399,7 @@ def freqresp(self, omega): def pole(self): """Compute the poles of a state space system.""" - return eigvals(self.A) + return eigvals(self.A) if self.states else np.array([]) def zero(self): """Compute the zeros of a state space system.""" diff --git a/control/tests/statesp_test.py b/control/tests/statesp_test.py index f33d24a26..bcb411026 100644 --- a/control/tests/statesp_test.py +++ b/control/tests/statesp_test.py @@ -378,6 +378,12 @@ def testPole(self): self.assertTrue(abs(z) < 1) + def testPoleStatic(self): + """Regression: pole() of static gain is empty array""" + np.testing.assert_array_equal(np.array([]), + StateSpace([],[],[],[[1]]).pole()) + + def suite(): return unittest.TestLoader().loadTestsFromTestCase(TestStateSpace)