Skip to content

Allow static gain StateSpace objects to be straightforwardly created. #107

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 41 additions & 35 deletions control/statesp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)

Expand Down
90 changes: 88 additions & 2 deletions control/tests/statesp_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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."""

Expand Down