diff --git a/src/dkpy/utilities.py b/src/dkpy/utilities.py index acc154a..d8c8145 100644 --- a/src/dkpy/utilities.py +++ b/src/dkpy/utilities.py @@ -4,7 +4,10 @@ "_ensure_tf", "_tf_close_coeff", "_tf_combine", + "_tf_split", "_tf_eye", + "_tf_zeros", + "_tf_ones", "_auto_lmi_strictness", ] @@ -192,6 +195,55 @@ def _tf_combine( return G_tf +def _tf_split(tf: control.TransferFunction) -> np.ndarray: + """Split MIMO transfer function into NumPy array of SISO tranfer functions. + + Parameters + ---------- + tf : control.TransferFunction + MIMO transfer function to split. + + Returns + ------- + np.ndarray + NumPy array of SISO transfer functions. + + Examples + -------- + Split a MIMO transfer function + + >>> G = control.TransferFunction( + ... [ + ... [[87.8], [-86.4]], + ... [[108.2], [-109.6]], + ... ], + ... [ + ... [[1, 1], [1, 1]], + ... [[1, 1], [1, 1]], + ... ], + ... ) + >>> dkpy._tf_split(G) + array([[TransferFunction(array([87.8]), array([1, 1])), + TransferFunction(array([-86.4]), array([1, 1]))], + [TransferFunction(array([108.2]), array([1, 1])), + TransferFunction(array([-109.6]), array([1, 1]))]], dtype=object) + """ + tf_split_lst = [] + for i_out in range(tf.noutputs): + row = [] + for i_in in range(tf.ninputs): + row.append( + control.TransferFunction( + tf.num[i_out][i_in], + tf.den[i_out][i_in], + dt=tf.dt, + ) + ) + tf_split_lst.append(row) + tf_split = np.array(tf_split_lst, dtype=object) + return tf_split + + def _tf_eye( n: int, dt: Union[None, bool, float] = None, @@ -219,6 +271,66 @@ def _tf_eye( return eye +def _tf_zeros( + m: int, + n: int, + dt: Union[None, bool, float] = None, +) -> control.TransferFunction: + """Transfer function matrix of zeros. + + Parameters + ---------- + m : int + First dimension. + n : int + Second dimension. + dt : Union[None, bool, float] + Timestep (s). Based on the ``control`` package, ``True`` indicates a + discrete-time system with unspecified timestep, ``0`` indicates a + continuous-time system, and ``None`` indicates a continuous- or + discrete-time system with unspecified timestep. + + Returns + ------- + control.TransferFunction + Identity transfer matrix. + """ + num = np.zeros((m, n, 1)) + den = np.ones((m, n, 1)) + zeros = control.TransferFunction(num, den, dt=dt) + return zeros + + +def _tf_ones( + m: int, + n: int, + dt: Union[None, bool, float] = None, +) -> control.TransferFunction: + """Transfer matrix of ones. + + Parameters + ---------- + m : int + First dimension. + n : int + Second dimension. + dt : Union[None, bool, float] + Timestep (s). Based on the ``control`` package, ``True`` indicates a + discrete-time system with unspecified timestep, ``0`` indicates a + continuous-time system, and ``None`` indicates a continuous- or + discrete-time system with unspecified timestep. + + Returns + ------- + control.TransferFunction + Identity transfer matrix. + """ + num = np.ones((m, n, 1)) + den = np.ones((m, n, 1)) + zeros = control.TransferFunction(num, den, dt=dt) + return zeros + + def _auto_lmi_strictness( solver_params: Dict[str, Any], scale: float = 10, diff --git a/tests/test_utilities.py b/tests/test_utilities.py index a43fc0e..2bd3fa4 100644 --- a/tests/test_utilities.py +++ b/tests/test_utilities.py @@ -201,8 +201,8 @@ def test_error_ensure(self, arraylike_or_tf, dt, exception): dkpy._ensure_tf(arraylike_or_tf, dt) -class TestTfCombine: - """Test :func:`_tf_combine`.""" +class TestTfCombineSplit: + """Test :func:`_tf_combine` and :func:`_tf_split`.""" @pytest.mark.parametrize( "tf_array, tf", @@ -362,6 +362,102 @@ def test_combine(self, tf_array, tf): tf_combined = dkpy._tf_combine(tf_array) assert dkpy._tf_close_coeff(tf_combined, tf) + @pytest.mark.parametrize( + "tf_array, tf", + [ + ( + np.array( + [ + [control.TransferFunction([1], [1, 1])], + ], + dtype=object, + ), + control.TransferFunction( + [ + [[1]], + ], + [ + [[1, 1]], + ], + ), + ), + ( + np.array( + [ + [control.TransferFunction([1], [1, 1])], + [control.TransferFunction([2], [1, 0])], + ], + dtype=object, + ), + control.TransferFunction( + [ + [[1]], + [[2]], + ], + [ + [[1, 1]], + [[1, 0]], + ], + ), + ), + ( + np.array( + [ + [control.TransferFunction([1], [1, 1], dt=1)], + [control.TransferFunction([2], [1, 0], dt=1)], + ], + dtype=object, + ), + control.TransferFunction( + [ + [[1]], + [[2]], + ], + [ + [[1, 1]], + [[1, 0]], + ], + dt=1, + ), + ), + ( + np.array( + [ + [control.TransferFunction([2], [1], dt=0.1)], + [control.TransferFunction([2], [1, 0], dt=0.1)], + ], + dtype=object, + ), + control.TransferFunction( + [ + [[2]], + [[2]], + ], + [ + [[1]], + [[1, 0]], + ], + dt=0.1, + ), + ), + ], + ) + def test_split(self, tf_array, tf): + """Test splitting transfer functions.""" + tf_split = dkpy._tf_split(tf) + # Test entry-by-entry + for i in range(tf_split.shape[0]): + for j in range(tf_split.shape[1]): + assert dkpy._tf_close_coeff( + tf_split[i, j], + tf_array[i, j], + ) + # Test combined + assert dkpy._tf_close_coeff( + dkpy._tf_combine(tf_split), + dkpy._tf_combine(tf_array), + ) + @pytest.mark.parametrize( "tf_array, exception", [ @@ -431,7 +527,7 @@ class TestTfEye: ), ), ( - 2, + 3, 1e-3, control.TransferFunction( [ @@ -452,7 +548,79 @@ class TestTfEye: def test_tf_eye(self, n, dt, tf_exp): """Test :func:`_tf_eye`.""" tf = dkpy._tf_eye(n, dt) - assert dkpy._tf_close_coeff(tf, tf) + assert dkpy._tf_close_coeff(tf, tf_exp) + + +class TestTfZeros: + """Test :func:`_tf_zeros`.""" + + @pytest.mark.parametrize( + "m, n, dt, tf_exp", + [ + ( + 1, + 1, + None, + control.TransferFunction([0], [1], dt=None), + ), + ( + 2, + 3, + None, + control.TransferFunction( + [ + [[0], [0], [0]], + [[0], [0], [0]], + ], + [ + [[1], [1], [1]], + [[1], [1], [1]], + ], + dt=None, + ), + ), + ], + ) + def test_tf_zeros(self, m, n, dt, tf_exp): + """Test :func:`_tf_zeros`.""" + tf = dkpy._tf_zeros(m, n, dt) + assert dkpy._tf_close_coeff(tf, tf_exp) + + +class TestTfOnes: + """Test :func:`_tf_ones`.""" + + @pytest.mark.parametrize( + "m, n, dt, tf_exp", + [ + ( + 1, + 1, + None, + control.TransferFunction([1], [1], dt=None), + ), + ( + 2, + 3, + None, + control.TransferFunction( + [ + [[1], [1], [1]], + [[1], [1], [1]], + ], + [ + [[1], [1], [1]], + [[1], [1], [1]], + ], + dt=None, + ), + ), + ], + ) + def test_tf_ones(self, m, n, dt, tf_exp): + """Test :func:`_tf_ones`.""" + tf = dkpy._tf_ones(m, n, dt) + assert dkpy._tf_close_coeff(tf, tf_exp) class TestAutoLmiStrictness: