8000 Merge pull request #110 · python-control/python-control@b6b9906 · GitHub
[go: up one dir, main page]

Skip to content

Commit b6b9906

Browse files
committed
Merge pull request #110
#110 Changes are from branch `rory-ss-static-pole` of https://github.com/roryyorke/python-control.git
2 parents c498d3d + c84debb commit b6b9906

File tree

2 files changed

+121
-43
lines changed

2 files changed

+121
-43
lines changed

control/statesp.py

Lines changed: 40 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -122,34 +122,35 @@ def __init__(self, *args):
122122
else:
123123
raise ValueError("Needs 1 or 4 arguments; received %i." % len(args))
124124

125-
# Here we're going to convert inputs to matrices, if the user gave a
126-
# non-matrix type.
127-
#! TODO: [A, B, C, D] = map(matrix, [A, B, C, D])?
128-
matrices = [A, B, C, D]
129-
for i in range(len(matrices)):
130-
# Convert to matrix first, if necessary.
131-
matrices[i] = matrix(matrices[i])
132-
[A, B, C, D] = matrices
133-
134-
LTI.__init__(self, B.shape[1], C.shape[0], dt)
125+
A, B, C, D = [matrix(M) for M in (A, B, C, D)]
126+
127+
# TODO: use super here?
128+
LTI.__init__(self, inputs=D.shape[1], outputs=D.shape[0], dt=dt)
135129
self.A = A
136130
self.B = B
137131
self.C = C
138132
self.D = D
139133

140-
self.states = A.shape[0]
134+
self.states = A.shape[1]
135+
136+
if 0 == self.states:
137+
# static gain
138+
# matrix's default "empty" shape is 1x0
139+
A.shape = (0,0)
140+
B.shape = (0,self.inputs)
141+
C.shape = (self.outputs,0)
141142

142143
# Check that the matrix sizes are consistent.
143-
if self.states != A.shape[1]:
144+
if self.states != A.shape[0]:
144145
raise ValueError("A must be square.")
145146
if self.states != B.shape[0]:
146-
raise ValueError("B must have the same row size as A.")
147+
raise ValueError("A and B must have the same number of rows.")
147148
if self.states != C.shape[1]:
148-
raise ValueError("C must have the same column size as A.")
149-
if self.inputs != D.shape[1]:
150-
raise ValueError("D must have the same column size as B.")
151-
if self.outputs != D.shape[0]:
152-
raise ValueError("D must have the same row size as C.")
149+
raise ValueError("A and C must have the same number of columns.")
150+
if self.inputs != B.shape[1]:
151+
raise ValueError("B and D must have the same number of columns.")
152+
if self.outputs != C.shape[0]:
153+
raise ValueError("C and D must have the same number of rows.")
153154

154155
# Check for states that don't do anything, and remove them.
155156
self._remove_useless_states()
@@ -179,17 +180,10 @@ def _remove_useless_states(self):
179180
useless.append(i)
180181

181182
# Remove the useless states.
182-
if all(useless == range(self.states)):
183-
# All the states were useless.
184-
self.A = zeros((1, 1))
185-
self.B = zeros((1, self.inputs))
186-
self.C = zeros((self.outputs, 1))
187-
else:
188-
# A more typical scenario.
189-
self.A = delete(self.A, useless, 0)
190-
self.A = delete(self.A, useless, 1)
191-
57AE self.B = delete(self.B, useless, 0)
192-
self.C = delete(self.C, useless, 1)
183+
self.A = delete(self.A, useless, 0)
184+
self.A = delete(self.A, useless, 1)
185+
self.B = delete(self.B, useless, 0)
186+
self.C = delete(self.C, useless, 1)
193187

194188
self.states = self.A.shape[0]
195189
self.inputs = self.B.shape[1]
@@ -405,7 +399,7 @@ def freqresp(self, omega):
405399
def pole(self):
406400
"""Compute the poles of a state space system."""
407401

408-
return eigvals(self.A)
402+
return eigvals(self.A) if self.states else np.array([])
409403

410404
def zero(self):
411405
"""Compute the zeros of a state space system."""
@@ -477,18 +471,22 @@ def feedback(self, other=1, sign=-1):
477471
def minreal(self, tol=0.0):
478472
"""Calculate a minimal realization, removes unobservable and
479473
uncontrollable states"""
480-
try:
481-
from slycot import tb01pd
482-
B = empty((self.states, max(self.inputs, self.outputs)))
483-
B[:,:self.inputs] = self.B
484-
C = empty((max(self.outputs, self.inputs), self.states))
485-
C[:self.outputs,:] = self.C
486-
A, B, C, nr = tb01pd(self.states, self.inputs, self.outputs,
487-
self.A, B, C, tol=tol)
488-
return StateSpace(A[:nr,:nr], B[:nr,:self.inputs],
489-
C[:self.outputs,:nr], self.D)
490-
except ImportError:
491-
raise TypeError("minreal requires slycot tb01pd")
474+
if self.states:
475+
try:
476+
from slycot import tb01pd
477+
B = empty((self.states, max(self.inputs, self.outputs)))
478+
B[:,:self.inputs] = self.B
479+
C = empty((max(self.outputs, self.inputs), self.states))
480+
C[:self.outputs,:] = self.C
481+
A, B, C, nr = tb01pd(self.states, self.inputs, self.outputs,
482+
self.A, B, C, tol=tol)
483+
return StateSpace(A[:nr,:nr], B[:nr,:self.inputs],
484+
C[:self.outputs,:nr], self.D)
485+
except ImportError:
486+
raise TypeError("minreal requires slycot tb01pd")
487+
else:
488+
return StateSpace(self)
489+
492490

493491
# TODO: add discrete time check
494492
def returnScipySignalLTI(self):

control/tests/statesp_test.py

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55

66
import unittest
77
import numpy as np
8-
from scipy.linalg import eigvals
8+
from numpy.linalg import solve
9+
from scipy.linalg import eigvals, block_diag
910
from control import matlab
1011
from control.statesp import StateSpace, _convertToStateSpace
1112
from control.xferfcn import TransferFunction
@@ -235,6 +236,79 @@ def test_dcgain(self):
235236
sys3 = StateSpace(0., 1., 1., 0.)
236237
np.testing.assert_equal(sys3.dcgain(), np.nan)
237238

239+
240+
def test_scalarStaticGain(self):
241+
"""Regression: can we create a scalar static gain?"""
242+
g1=StateSpace([],[],[],[2])
243+
g2=StateSpace([],[],[],[3])
244+
245+
# make sure StateSpace internals, specifically ABC matrix
246+
# sizes, are OK for LTI operations
247+
g3 = g1*g2
248+
self.assertEqual(6, g3.D[0,0])
249+
g4 = g1+g2
250+
self.assertEqual(5, g4.D[0,0])
251+
g5 = g1.feedback(g2)
252+
self.assertAlmostEqual(2./7, g5.D[0,0])
253+
g6 = g1.append(g2)
254+
np.testing.assert_array_equal(np.diag([2,3]),g6.D)
255+
256+
def test_matrixStaticGain(self):
257+
"""Regression: can we create matrix static gains?"""
258+
d1 = np.matrix([[1,2,3],[4,5,6]])
259+
d2 = np.matrix([[7,8],[9,10],[11,12]])
260+
g1=StateSpace([],[],[],d1)
261+
262+
# _remove_useless_states was making A = [[0]]
263+
self.assertEqual((0,0), g1.A.shape)
264+
265+
g2=StateSpace([],[],[],d2)
266+
g3=StateSpace([],[],[],d2.T)
267+
268+
h1 = g1*g2
269+
np.testing.assert_array_equal(d1*d2, h1.D)
270+
h2 = g1+g3
271+
np.testing.assert_array_equal(d1+d2.T, h2.D)
272+
h3 = g1.feedback(g2)
273+
np.testing.assert_array_almost_equal(solve(np.eye(2)+d1*d2,d1), h3.D)
274+
h4 = g1.append(g2)
275+
np.testing.assert_array_equal(block_diag(d1,d2),h4.D)
276+
277+
278+
def test_remove_useless_states(self):
279+
"""Regression: _remove_useless_states gives correct ABC sizes"""
280+
g1 = StateSpace(np.zeros((3,3)),
281+
np.zeros((3,4)),
282+
np.zeros((5,3)),
283+
np.zeros((5,4)))
284+
self.assertEqual((0,0), g1.A.shape)
285+
self.assertEqual((0,4), g1.B.shape)
286+
self.assertEqual((5,0), g1.C.shape)
287+
self.assertEqual((5,4), g1.D.shape)
288+
self.assertEqual(0, g1.states)
289+
290+
291+
def test_BadEmptyMatrices(self):
292+
"""Mismatched ABCD matrices when some are empty"""
293+
self.assertRaises(ValueError,StateSpace, [1], [], [], [1])
294+
self.assertRaises(ValueError,StateSpace, [1], [1], [], [1])
295+
self.assertRaises(ValueError,StateSpace, [1], [], [1], [1])
296+
self.assertRaises(ValueError,StateSpace, [], [1], [], [1])
297+
self.assertRaises(ValueError,StateSpace, [], [1], [1], [1])
298+
self.assertRaises(ValueError,StateSpace, [], [], [1], [1])
299+
self.assertRaises(ValueError,StateSpace, [1], [1], [1], [])
300+
301+
302+
def test_minrealStaticGain(self):
303+
"""Regression: minreal on static gain was failing"""
304+
g1 = StateSpace([],[],[],[1])
305+
g2 = g1.minreal()
306+
np.testing.assert_array_equal(g1.A, g2.A)
307+
np.testing.assert_array_equal(g1.B, g2.B)
308+
np.testing.assert_array_equal(g1.C, g2.C)
309+
np.testing.assert_array_equal(g1.D, g2.D)
310+
311+
238312
class TestRss(unittest.TestCase):
239313
"""These are tests for the proper functionality of statesp.rss."""
240314

@@ -304,6 +378,12 @@ def testPole(self):
304378
self.assertTrue(abs(z) < 1)
305379

306380

381+
def testPoleStatic(self):
382+
"""Regression: pole() of static gain is empty array"""
383+
np.testing.assert_array_equal(np.array([]),
384+
StateSpace([],[],[],[[1]]).pole())
385+
386+
307387
def suite():
308388
return unittest.TestLoader().loadTestsFromTestCase(TestStateSpace)
309389

0 commit comments

Comments
 (0)
0