Skip to content

Commit 7859f8c

Browse files
committed
[statesp.py] Add a function checking consistency between statespace matrices
1 parent 27f83e4 commit 7859f8c

3 files changed

Lines changed: 181 additions & 49 deletions

File tree

‎control/exception.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@ class ControlSlycot(Exception):
4646

4747
class ControlDimension(Exception):
4848
"""Raised when dimensions of system objects are not correct"""
49-
pass
49+
def __init__(self, error):
50+
print("[Control] Dimension error.")
51+
print(error)
5052

5153
class ControlArgument(Exception):
5254
"""Raised when arguments to a function are not correct"""

‎control/statesp.py‎

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
from .lti import LTI, timebase, timebaseEqual, isdtime
6565
from .xferfcn import _convert_to_transfer_function
6666
from copy import deepcopy
67+
from .exception import ControlArgument, ControlDimension
6768

6869
__all__ = ['StateSpace', 'ss', 'rss', 'drss', 'tf2ss', 'ssdata']
6970

@@ -1330,3 +1331,105 @@ def ssdata(sys):
13301331
"""
13311332
ss = _convertToStateSpace(sys)
13321333
return ss.A, ss.B, ss.C, ss.D
1334+
1335+
1336+
def consistent_dimensions(A=None, B=None, C=None, D=None):
1337+
"""
1338+
Return a boolean stating if dimensions of the state-space matrices are consistent.
1339+
1340+
A state space representation should verify:
1341+
- A square matrix [n_states x n_states]
1342+
- B [n_states x n_inputs]
1343+
- C [n_outputs x n_states]
1344+
- D [n_outputs x n_inputs]
1345+
Parameters
1346+
----------
1347+
A: ndarray - optional
1348+
State space system matrix
1349+
B: ndarray - optional
1350+
State space input matrix
1351+
C: ndarray - optional
1352+
State space output matrix
1353+
D: ndarray - optional
1354+
State space feedthrough matrix
1355+
1356+
Returns
1357+
-------
1358+
consistency: boolean
1359+
True if state space input matrices have consistent dimensions
1360+
False otherwise
1361+
"""
1362+
try:
1363+
if A is not None:
1364+
if not isinstance(A, np.ndarray):
1365+
raise ControlArgument("A is not a numpy array.")
1366+
else:
1367+
1368+
n_row_A = A.shape[0]
1369+
n_col_A = A.shape[1]
1370+
if n_row_A != n_col_A:
1371+
raise ControlDimension("System matrix is not square. "
1372+
"(n_row_A: {} != n_col_A: {}).".format(n_row_A, n_col_A))
1373+
else:
1374+
n_states = n_row_A
1375+
1376+
if B is not None:
1377+
if not isinstance(B, np.ndarray):
1378+
raise ControlArgument("B is not a numpy array.")
1379+
1380+
n_row_B = B.shape[0]
1381+
n_col_B = B.shape[1]
1382+
1383+
try:
1384+
if n_row_B != n_states:
1385+
raise ControlDimension("Input matrix has not the right number of states. "
1386+
"(n_row_B: {} != n_states: {}).".format(n_row_B, n_states))
1387+
else:
1388+
n_inputs = n_col_B
1389+
except NameError:
1390+
warn("State space representation has no System matrix.")
1391+
n_states = n_row_B
1392+
1393+
if C is not None:
1394+
if not isinstance(C, np.ndarray):
1395+
raise ControlArgument("C is not a numpy array.")
1396+
1397+
n_row_C = C.shape[0]
1398+
n_col_C = C.shape[1]
1399+
1400+
try:
1401+
if n_col_C != n_states:
1402+
raise ControlDimension("Output matrix has not the right number of states. "
1403+
"(n_row_C: {} != n_states: {}).".format(n_row_C,
1404+
n_states))
1405+
else:
1406+
n_outputs = n_row_C
1407+
except NameError:
1408+
warn("State space representation has no system matrix nor input matrix.")
1409+
1410+
if D is not None:
1411+
if not isinstance(D, np.ndarray):
1412+
raise ControlArgument("D is not a numpy array.")
1413+
1414+
n_row_D = D.shape[0]
1415+
n_col_D = D.shape[1]
1416+
1417+
try:
1418+
if n_row_D != n_outputs:
1419+
raise ControlDimension("Feedthrough matrix has not the right number of "
1420+
"outputs. (n_row_D: {} != n_outputs: "
1421+
"{}).".format(n_row_D, n_outputs))
1422+
except NameError:
1423+
warn("State space representation has no output matrix.")
1424+
1425+
try:
1426+
if n_col_D != n_inputs:
1427+
raise ControlDimension("Feedthrough matrix has not the right number of inputs. "
1428+
"(n_row_D: {} != n_inputs: {}).".format(n_col_D,
1429+
n_inputs))
1430+
except NameError:
1431+
warn("State space representation has no input matrix.")
1432+
except ControlDimension:
1433+
return False
1434+
1435+
return True

‎control/tests/statesp_test.py‎

Lines changed: 75 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from numpy.linalg import solve
99
from scipy.linalg import eigvals, block_diag
1010
from control import matlab
11-
from control.statesp import StateSpace, _convertToStateSpace, tf2ss
11+
from control.statesp import StateSpace, _convertToStateSpace, tf2ss, consistent_dimensions
1212
from control.xferfcn import TransferFunction, ss2tf
1313
from control.lti import evalfr
1414
from control.exception import slycot_check
@@ -21,47 +21,47 @@ def setUp(self):
2121
"""Set up a MIMO system to test operations on."""
2222

2323
# sys1: 3-states square system (2 inputs x 2 outputs)
24-
A322 = [[-3., 4., 2.],
25-
[-1., -3., 0.],
26-
[2., 5., 3.]]
27-
B322 = [[1., 4.],
28-
[-3., -3.],
29-
[-2., 1.]]
30-
C322 = [[4., 2., -3.],
31-
[1., 4., 3.]]
32-
D322 = [[-2., 4.],
33-
[0., 1.]]
34-
self.sys322 = StateSpace(A322, B322, C322, D322)
24+
self.A322 = np.array([[-3., 4., 2.],
25+
[-1., -3., 0.],
26+
[2., 5., 3.]])
27+
self.B322 = np.array([[1., 4.],
28+
[-3., -3.],
29+
[-2., 1.]])
30+
self.C322 = np.array([[4., 2., -3.],
31+
[1., 4., 3.]])
32+
self.D322 = np.array([[-2., 4.],
33+
[0., 1.]])
34+
self.sys322 = StateSpace(self.A322, self.B322, self.C322, self.D322)
3535

3636
# sys1: 2-states square system (2 inputs x 2 outputs)
37-
A222 = [[4., 1.],
38-
[2., -3]]
39-
B222 = [[5., 2.],
40-
[-3., -3.]]
41-
C222 = [[2., -4],
42-
[0., 1.]]
43-
D222 = [[3., 2.],
44-
[1., -1.]]
45-
self.sys222 = StateSpace(A222, B222, C222, D222)
37+
self.A222 = np.array([[4., 1.],
38+
[2., -3]])
39+
self.B222 = np.array([[5., 2.],
40+
[-3., -3.]])
41+
self.C222 = np.array([[2., -4],
42+
[0., 1.]])
43+
self.D222 = np.array([[3., 2.],
44+
[1., -1.]])
45+
self.sys222 = StateSpace(self.A222, self.B222, self.C222, self.D222)
4646

4747
# sys3: 6 states non square system (2 inputs x 3 outputs)
48-
A623 = np.array([[1, 0, 0, 0, 0, 0],
49-
[0, 1, 0, 0, 0, 0],
50-
[0, 0, 3, 0, 0, 0],
51-
[0, 0, 0, -4, 0, 0],
52-
[0, 0, 0, 0, -1, 0],
53-
[0, 0, 0, 0, 0, 3]])
54-
B623 = np.array([[0, -1],
55-
[-1, 0],
56-
[1, -1],
57-
[0, 0],
58-
[0, 1],
59-
[-1, -1]])
60-
C623 = np.array([[1, 0, 0, 1, 0, 0],
61-
[0, 1, 0, 1, 0, 1],
62-
[0, 0, 1, 0, 0, 1]])
63-
D623 = np.zeros((3, 2))
64-
self.sys623 = StateSpace(A623, B623, C623, D623)
48+
self.A623 = np.array([[1, 0, 0, 0, 0, 0],
49+
[0, 1, 0, 0, 0, 0],
50+
[0, 0, 3, 0, 0, 0],
51+
[0, 0, 0, -4, 0, 0],
52+
[0, 0, 0, 0, -1, 0],
53+
[0, 0, 0, 0, 0, 3]])
54+
self.B623 = np.array([[0, -1],
55+
[-1, 0],
56+
[1, -1],
57+
[0, 0],
58+
[0, 1],
59+
[-1, -1]])
60+
self.C623 = np.array([[1, 0, 0, 1, 0, 0],
61+
[0, 1, 0, 1, 0, 1],
62+
[0, 0, 1, 0, 0, 1]])
63+
self.D623 = np.zeros((3, 2))
64+
self.sys623 = StateSpace(self.A623, self.B623, self.C623, self.D623)
6565

6666
def test_pole(self):
6767
"""Evaluate the poles of a MIMO system."""
@@ -107,7 +107,7 @@ def test_zero_mimo_sys222_square(self):
107107
"""Evaluate the zeros of a square MIMO system."""
108108

109109
z = np.sort(self.sys222.zero())
110-
true_z = np.sort([-10.568501, 3.368501])
110+
true_z = np.sort([-10.568501, 3.368501])
111111
np.testing.assert_array_almost_equal(z, true_z)
112112

113113
@unittest.skipIf(not slycot_check(), "slycot not installed")
@@ -207,13 +207,13 @@ def test_freq_resp(self):
207207
sys = StateSpace(A, B, C, D)
208208

209209
true_mag = [[[0.0852992637230322, 0.00103596611395218],
210-
[0.935374692849736, 0.799380720864549]],
211-
[[0.55656854563842, 0.301542699860857],
212-
[0.609178071542849, 0.0382108097985257]]]
210+
[0.935374692849736, 0.799380720864549]],
211+
[[0.55656854563842, 0.301542699860857],
212+
[0.609178071542849, 0.0382108097985257]]]
213213
true_phase = [[[-0.566195599644593, -1.68063565332582],
214-
[3.0465958317514, 3.14141384339534]],
215-
[[2.90457947657161, 3.10601268291914],
216-
[-0.438157380501337, -1.40720969147217]]]
214+
[3.0465958317514, 3.14141384339534]],
215+
[[2.90457947657161, 3.10601268291914],
216+
[-0.438157380501337, -1.40720969147217]]]
217217
true_omega = [0.1, 10.]
218218

219219
mag, phase, omega = sys.freqresp(true_omega)
@@ -241,7 +241,7 @@ def test_minreal(self):
241241
self.assertEqual(sysr.inputs, sys.inputs)
242242
self.assertEqual(sysr.outputs, sys.outputs)
243243
np.testing.assert_array_almost_equal(
244-
eigvals(sysr.A), [-2.136154, -0.1638459])
244+
eigvals(sysr.A), [-2.136154, -0.1638459])
245245

246246
def test_append_ss(self):
247247
"""Test appending two state-space systems."""
@@ -290,7 +290,6 @@ def test_append_tf(self):
290290
np.testing.assert_array_almost_equal(sys3c.A[3:, :3], np.zeros((2, 3)))
291291

292292
def test_array_access_ss(self):
293-
294293
sys1 = StateSpace([[1., 2.], [3., 4.]],
295294
[[5., 6.], [6., 8.]],
296295
[[9., 10.], [11., 12.]],
@@ -392,7 +391,7 @@ def test_matrix_static_gain(self):
392391
np.testing.assert_array_equal(d1 + d2.T, h2.D)
393392
h3 = g1.feedback(g2)
394393
np.testing.assert_array_almost_equal(
395-
solve(np.eye(2) + d1 * d2, d1), h3.D)
394+
solve(np.eye(2) + d1 * d2, d1), h3.D)
396395
h4 = g1.append(g2)
397396
np.testing.assert_array_equal(block_diag(d1, d2), h4.D)
398397

@@ -443,11 +442,39 @@ def empty(shape):
443442
m = np.matrix([])
444443
m.shape = shape
445444
return m
445+
446446
np.testing.assert_array_equal(empty((0, 0)), g.A)
447447
np.testing.assert_array_equal(empty((0, D.shape[1])), g.B)
448448
np.testing.assert_array_equal(empty((D.shape[0], 0)), g.C)
449449
np.testing.assert_array_equal(D, g.D)
450450

451+
def test_consistent_dimensions_322(self):
452+
true = consistent_dimensions(self.A322, self.B322, self.C322, self.D322)
453+
np.testing.assert_(true)
454+
true = consistent_dimensions(B=self.B322, C=self.C322, D=self.D322)
455+
np.testing.assert_(true)
456+
true = consistent_dimensions(A=self.A322, C=self.C322, D=self.D322)
457+
np.testing.assert_(true)
458+
true = consistent_dimensions(A=self.A322, B=self.B322, D=self.D322)
459+
np.testing.assert_(true)
460+
true = consistent_dimensions(A=self.A322, B=self.B322, C=self.C322)
461+
np.testing.assert_(true)
462+
463+
def test_consistent_dimensions_bad_inputs(self):
464+
# Number of states not consistent
465+
true = consistent_dimensions(A=self.A222, B=self.B322, C=self.C322, D=self.D322)
466+
np.testing.assert_(not true)
467+
true = consistent_dimensions(self.A322, self.B222, self.C322, self.D322)
468+
np.testing.assert_(not true)
469+
true = consistent_dimensions(A=self.A322, C=self.C222, D=self.D322)
470+
np.testing.assert_(not true)
471+
# Number of input not consistent
472+
true = consistent_dimensions(A=self.A322, B=self.B322, D=self.A322)
473+
np.testing.assert_(not true)
474+
# Number of output not consistent
475+
true = consistent_dimensions(A=self.A322, C=self.C322, D=self.A322)
476+
np.testing.assert_(not true)
477+
451478

452479
class TestRss(unittest.TestCase):
453480
"""These are tests for the proper functionality of statesp.rss."""

0 commit comments

Comments
 (0)