From 6b61ed06a4ca77a046d4996d916ba18464592aca Mon Sep 17 00:00:00 2001 From: Vaibhav Gupta Date: Mon, 17 Jun 2024 14:35:30 +0200 Subject: [PATCH 1/4] Add slicing access for state-space models with tests --- control/statesp.py | 12 ++++++------ control/tests/statesp_test.py | 22 ++++++++++++++++++---- control/xferfcn.py | 7 +++++++ 3 files changed, 31 insertions(+), 10 deletions(-) diff --git a/control/statesp.py b/control/statesp.py index 0c2856b15..6e4e5d43d 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -50,6 +50,7 @@ import math from copy import deepcopy from warnings import warn +from collections.abc import Iterable import numpy as np import scipy as sp @@ -1215,17 +1216,16 @@ def append(self, other): def __getitem__(self, indices): """Array style access""" - if len(indices) != 2: + if not isinstance(indices, Iterable) or len(indices) != 2: raise IOError('must provide indices of length 2 for state space') - outdx = indices[0] if isinstance(indices[0], list) else [indices[0]] - inpdx = indices[1] if isinstance(indices[1], list) else [indices[1]] + outdx, inpdx = indices + if not isinstance(outdx, (int, slice)) or not isinstance(inpdx, (int, slice)): + raise TypeError(f"system indices must be integers or slices") sysname = config.defaults['iosys.indexed_system_name_prefix'] + \ self.name + config.defaults['iosys.indexed_system_name_suffix'] return StateSpace( self.A, self.B[:, inpdx], self.C[outdx, :], self.D[outdx, inpdx], - self.dt, name=sysname, - inputs=[self.input_labels[i] for i in list(inpdx)], - outputs=[self.output_labels[i] for i in list(outdx)]) + self.dt, name=sysname, inputs=self.input_labels[inpdx], outputs=self.output_labels[outdx]) def sample(self, Ts, method='zoh', alpha=None, prewarp_frequency=None, name=None, copy_names=True, **kwargs): diff --git a/control/tests/statesp_test.py b/control/tests/statesp_test.py index 59f441456..0fc43ce11 100644 --- a/control/tests/statesp_test.py +++ b/control/tests/statesp_test.py @@ -463,8 +463,22 @@ def test_append_tf(self): np.testing.assert_array_almost_equal(sys3c.A[:3, 3:], np.zeros((3, 2))) np.testing.assert_array_almost_equal(sys3c.A[3:, :3], np.zeros((2, 3))) - def test_array_access_ss(self): - + def test_array_access_ss_failure(self): + sys1 = StateSpace( + [[1., 2.], [3., 4.]], + [[5., 6.], [6., 8.]], + [[9., 10.], [11., 12.]], + [[13., 14.], [15., 16.]], 1, + inputs=['u0', 'u1'], outputs=['y0', 'y1']) + with pytest.raises(IOError): + sys1[0] + + @pytest.mark.parametrize("outdx, inpdx", + [(0, 1), + (slice(0, 1, 1), 1), + (0, slice(1, 2, 1)), + (slice(0, 1, 1), slice(1, 2, 1))]) + def test_array_access_ss(self, outdx, inpdx): sys1 = StateSpace( [[1., 2.], [3., 4.]], [[5., 6.], [6., 8.]], @@ -472,7 +486,7 @@ def test_array_access_ss(self): [[13., 14.], [15., 16.]], 1, inputs=['u0', 'u1'], outputs=['y0', 'y1']) - sys1_01 = sys1[0, 1] + sys1_01 = sys1[outdx, inpdx] np.testing.assert_array_almost_equal(sys1_01.A, sys1.A) np.testing.assert_array_almost_equal(sys1_01.B, @@ -480,7 +494,7 @@ def test_array_access_ss(self): np.testing.assert_array_almost_equal(sys1_01.C, sys1.C[0:1, :]) np.testing.assert_array_almost_equal(sys1_01.D, - sys1.D[0, 1]) + sys1.D[0:1, 1:2]) assert sys1.dt == sys1_01.dt assert sys1_01.input_labels == ['u1'] diff --git a/control/xferfcn.py b/control/xferfcn.py index 63aeff8f9..d0295194f 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -47,6 +47,8 @@ """ +from collections.abc import Iterable + # External function declarations import numpy as np from numpy import angle, array, empty, finfo, ndarray, ones, \ @@ -758,7 +760,12 @@ def __pow__(self, other): return (TransferFunction([1], [1]) / self) * (self**(other + 1)) def __getitem__(self, key): + if not isinstance(key, Iterable) or len(key) != 2: + raise IOError('must provide indices of length 2 for state space') + key1, key2 = key + if not isinstance(key1, (int, slice)) or not isinstance(key2, (int, slice)): + raise TypeError(f"system indices must be integers or slices") # pre-process if isinstance(key1, int): From 4dff6495121c923f150bf600c73bd68265215297 Mon Sep 17 00:00:00 2001 From: Vaibhav Gupta Date: Thu, 27 Jun 2024 12:15:02 +0200 Subject: [PATCH 2/4] Correct typos and column length issue --- control/statesp.py | 6 ++++-- control/xferfcn.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/control/statesp.py b/control/statesp.py index 6e4e5d43d..d775263f4 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -1219,13 +1219,15 @@ def __getitem__(self, indices): if not isinstance(indices, Iterable) or len(indices) != 2: raise IOError('must provide indices of length 2 for state space') outdx, inpdx = indices - if not isinstance(outdx, (int, slice)) or not isinstance(inpdx, (int, slice)): + if not isinstance(outdx, (int, slice)) \ + or not isinstance(inpdx, (int, slice)): raise TypeError(f"system indices must be integers or slices") sysname = config.defaults['iosys.indexed_system_name_prefix'] + \ self.name + config.defaults['iosys.indexed_system_name_suffix'] return StateSpace( self.A, self.B[:, inpdx], self.C[outdx, :], self.D[outdx, inpdx], - self.dt, name=sysname, inputs=self.input_labels[inpdx], outputs=self.output_labels[outdx]) + self.dt, name=sysname, + inputs=self.input_labels[inpdx], outputs=self.output_labels[outdx]) def sample(self, Ts, method='zoh', alpha=None, prewarp_frequency=None, name=None, copy_names=True, **kwargs): diff --git a/control/xferfcn.py b/control/xferfcn.py index d0295194f..ba9af3913 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -761,7 +761,7 @@ def __pow__(self, other): def __getitem__(self, key): if not isinstance(key, Iterable) or len(key) != 2: - raise IOError('must provide indices of length 2 for state space') + raise IOError('must provide indices of length 2 for transfer functions') key1, key2 = key if not isinstance(key1, (int, slice)) or not isinstance(key2, (int, slice)): From c6ef9b494d91e6f55704af04f0c409086e67a8e8 Mon Sep 17 00:00:00 2001 From: Vaibhav Gupta Date: Thu, 27 Jun 2024 14:27:24 +0200 Subject: [PATCH 3/4] Fixed bug in statespace initilisation --- control/statesp.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/control/statesp.py b/control/statesp.py index d775263f4..717fc9a73 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -290,9 +290,9 @@ def __init__(self, *args, **kwargs): raise ValueError("A and B must have the same number of rows.") if self.nstates != C.shape[1]: raise ValueError("A and C must have the same number of columns.") - if self.ninputs != B.shape[1]: + if self.ninputs != B.shape[1] or self.ninputs != D.shape[1]: raise ValueError("B and D must have the same number of columns.") - if self.noutputs != C.shape[0]: + if self.noutputs != C.shape[0] or self.noutputs != D.shape[0]: raise ValueError("C and D must have the same number of rows.") # @@ -1219,9 +1219,14 @@ def __getitem__(self, indices): if not isinstance(indices, Iterable) or len(indices) != 2: raise IOError('must provide indices of length 2 for state space') outdx, inpdx = indices - if not isinstance(outdx, (int, slice)) \ - or not isinstance(inpdx, (int, slice)): + + # Convert int to slice to ensure that numpy doesn't drop the dimension + if isinstance(outdx, int): outdx = slice(outdx, outdx+1, 1) + if isinstance(inpdx, int): inpdx = slice(inpdx, inpdx+1, 1) + + if not isinstance(outdx, slice) or not isinstance(inpdx, slice): raise TypeError(f"system indices must be integers or slices") + sysname = config.defaults['iosys.indexed_system_name_prefix'] + \ self.name + config.defaults['iosys.indexed_system_name_suffix'] return StateSpace( From a0fc6bcdcf0857998fb3efb1d9f945610817799c Mon Sep 17 00:00:00 2001 From: Vaibhav Gupta Date: Thu, 27 Jun 2024 14:27:36 +0200 Subject: [PATCH 4/4] Added more test cases for slicing of statespace model --- control/tests/statesp_test.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/control/tests/statesp_test.py b/control/tests/statesp_test.py index 0fc43ce11..6ddf9933e 100644 --- a/control/tests/statesp_test.py +++ b/control/tests/statesp_test.py @@ -477,28 +477,39 @@ def test_array_access_ss_failure(self): [(0, 1), (slice(0, 1, 1), 1), (0, slice(1, 2, 1)), + (slice(0, 1, 1), slice(1, 2, 1)), + (slice(None, None, -1), 1), + (0, slice(None, None, -1)), + (slice(None, 2, None), 1), + (slice(None, None, 1), slice(None, None, 2)), + (0, slice(1, 2, 1)), (slice(0, 1, 1), slice(1, 2, 1))]) def test_array_access_ss(self, outdx, inpdx): sys1 = StateSpace( [[1., 2.], [3., 4.]], - [[5., 6.], [6., 8.]], + [[5., 6.], [7., 8.]], [[9., 10.], [11., 12.]], [[13., 14.], [15., 16.]], 1, inputs=['u0', 'u1'], outputs=['y0', 'y1']) sys1_01 = sys1[outdx, inpdx] + + # Convert int to slice to ensure that numpy doesn't drop the dimension + if isinstance(outdx, int): outdx = slice(outdx, outdx+1, 1) + if isinstance(inpdx, int): inpdx = slice(inpdx, inpdx+1, 1) + np.testing.assert_array_almost_equal(sys1_01.A, sys1.A) np.testing.assert_array_almost_equal(sys1_01.B, - sys1.B[:, 1:2]) + sys1.B[:, inpdx]) np.testing.assert_array_almost_equal(sys1_01.C, - sys1.C[0:1, :]) + sys1.C[outdx, :]) np.testing.assert_array_almost_equal(sys1_01.D, - sys1.D[0:1, 1:2]) + sys1.D[outdx, inpdx]) assert sys1.dt == sys1_01.dt - assert sys1_01.input_labels == ['u1'] - assert sys1_01.output_labels == ['y0'] + assert sys1_01.input_labels == sys1.input_labels[inpdx] + assert sys1_01.output_labels == sys1.output_labels[outdx] assert sys1_01.name == sys1.name + "$indexed" def test_dc_gain_cont(self):