From e8ee43f5766d831a6ae34a73c8f9e0feffd436f9 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 | 50 +++++++++++++++++++++++++++++++++-- 2 files changed, 67 insertions(+), 20 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..f64e0ba76 100644 --- a/control/tests/statesp_test.py +++ b/control/tests/statesp_test.py @@ -5,9 +5,10 @@ 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.statesp import StateSpace, _convertToStateSpace,tf2ss from control.xferfcn import TransferFunction class TestStateSpace(unittest.TestCase): @@ -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 738264455e0fa71f9c4f5c6162b014c7350ed45f 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 | 15 ++++----------- control/tests/statesp_test.py | 18 +++++++++++++++++- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/control/statesp.py b/control/statesp.py index 40811a065..cc77eae2b 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -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 f64e0ba76..bc7cc524e 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,18 @@ 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) + + def test_BadEmptyMatrices(self): """Mismatched ABCD matrices when some are empty""" self.assertRaises(ValueError,StateSpace, [1], [], [], [1]) From 27942600f44afc75c540f252dd21dcb85170a775 Mon Sep 17 00:00:00 2001 From: Rory Yorke Date: Fri, 12 Aug 2016 18:45:01 +0200 Subject: [PATCH 3/4] MessageFix: remove extra 'C' --- control/statesp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/control/statesp.py b/control/statesp.py index cc77eae2b..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]: From e640b1785739a81254ccde069a777cfcb574ca13 Mon Sep 17 00:00:00 2001 From: Rory Yorke Date: Sun, 18 Sep 2016 21:30:08 +0200 Subject: [PATCH 4/4] BugFix: allow empty (no input or output) StateSpace objects. Conflicts: control/tests/statesp_test.py --- control/statesp.py | 28 ++++++++++++++++++++-------- control/tests/statesp_test.py | 24 ++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/control/statesp.py b/control/statesp.py index b74c5cb71..4e5a3ed3f 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -52,7 +52,7 @@ import numpy as np from numpy import all, angle, any, array, asarray, concatenate, cos, delete, \ - dot, empty, exp, eye, matrix, ones, pi, poly, poly1d, roots, shape, sin, \ + dot, empty, exp, eye, ones, pi, poly, poly1d, roots, shape, sin, \ zeros, squeeze from numpy.random import rand, randn from numpy.linalg import solve, eigvals, matrix_rank @@ -66,6 +66,19 @@ __all__ = ['StateSpace', 'ss', 'rss', 'drss', 'tf2ss', 'ssdata'] + +def _matrix(a): + """_matrix(a) -> numpy.matrix + a - passed to numpy.matrix + Wrapper around numpy.matrix; unlike that function, _matrix([]) will be 0x0 + """ + from numpy import matrix + am = matrix(a) + if (1,0) == am.shape: + am.shape = (0,0) + return am + + class StateSpace(LTI): """A class for representing state-space models @@ -122,7 +135,7 @@ def __init__(self, *args): else: raise ValueError("Needs 1 or 4 arguments; received %i." % len(args)) - A, B, C, D = [matrix(M) for M in (A, B, C, D)] + 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) @@ -327,8 +340,9 @@ def __rmul__(self, other): return _convertToStateSpace(other) * self # try to treat this as a matrix + # TODO: doesn't _convertToStateSpace do this anyway? try: - X = matrix(other) + X = _matrix(other) C = X * self.C D = X * self.D return StateSpace(self.A, self.B, C, D, self.dt) @@ -686,11 +700,9 @@ def _convertToStateSpace(sys, **kw): # If this is a matrix, try to create a constant feedthrough try: - D = matrix(sys) - outputs, inputs = D.shape - - return StateSpace(0., zeros((1, inputs)), zeros((outputs, 1)), D) - except Exception(e): + D = _matrix(sys) + return StateSpace([], [], [], D) + except Exception as e: print("Failure to assume argument is matrix-like in" \ " _convertToStateSpace, result %s" % e) diff --git a/control/tests/statesp_test.py b/control/tests/statesp_test.py index bc7cc524e..5948bf727 100644 --- a/control/tests/statesp_test.py +++ b/control/tests/statesp_test.py @@ -253,6 +253,7 @@ def test_scalarStaticGain(self): g6 = g1.append(g2) np.testing.assert_array_equal(np.diag([2,3]),g6.D) + def test_matrixStaticGain(self): """Regression: can we create matrix static gains?""" d1 = np.matrix([[1,2,3],[4,5,6]]) @@ -297,6 +298,29 @@ def test_BadEmptyMatrices(self): self.assertRaises(ValueError,StateSpace, [], [], [1], [1]) self.assertRaises(ValueError,StateSpace, [1], [1], [1], []) + + def test_Empty(self): + """Regression: can we create an empty StateSpace object?""" + g1=StateSpace([],[],[],[]) + self.assertEqual(0,g1.states) + self.assertEqual(0,g1.inputs) + self.assertEqual(0,g1.outputs) + + + def test_MatrixToStateSpace(self): + """_convertToStateSpace(matrix) gives ss([],[],[],D)""" + D = np.matrix([[1,2,3],[4,5,6]]) + g = _convertToStateSpace(D) + def empty(shape): + m = np.matrix([]) + m.shape = shape + return m + np.testing.assert_array_equal(empty((0,0)), g.A) + np.testing.assert_array_equal(empty((0,D.shape[1])), g.B) + np.testing.assert_array_equal(empty((D.shape[0],0)), g.C) + np.testing.assert_array_equal(D,g.D) + + class TestRss(unittest.TestCase): """These are tests for the proper functionality of statesp.rss."""