diff --git a/control/statesp.py b/control/statesp.py index 80da2a5f8..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,34 +135,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 + A, B, C, D = [_matrix(M) for M in (A, B, C, D)] - LTI.__init__(self, B.shape[1], C.shape[0], dt) + # 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 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() @@ -179,17 +193,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] @@ -333,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) @@ -692,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 c19f4f38b..5948bf727 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,91 @@ 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 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) + + 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_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]) + 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], []) + + + 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.""" pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy