From e69fc1a091f66cee434b0843129202680794fc65 Mon Sep 17 00:00:00 2001 From: Ben Greiner Date: Sat, 1 Feb 2025 10:37:14 +0100 Subject: [PATCH 1/5] Move _tf_close_coeff back to testing realm and make better use of assertion messages --- control/tests/bdalg_test.py | 35 +++++++++++------------- control/tests/conftest.py | 50 +++++++++++++++++++++++++++++++---- control/tests/statesp_test.py | 40 ++++++++++++++-------------- control/tests/xferfcn_test.py | 24 ++++++++--------- control/xferfcn.py | 47 -------------------------------- 5 files changed, 93 insertions(+), 103 deletions(-) diff --git a/control/tests/bdalg_test.py b/control/tests/bdalg_test.py index f69574d9a..576b91d77 100644 --- a/control/tests/bdalg_test.py +++ b/control/tests/bdalg_test.py @@ -1,24 +1,21 @@ -"""bdalg_test.py - test suite for block diagram algebra +"""bdalg_test.py - test suite for block diagram algebra. RMM, 30 Mar 2011 (based on TestBDAlg from v0.4a) """ +import control as ctrl import numpy as np -from numpy import sort import pytest - -import control as ctrl -from control.xferfcn import TransferFunction, _tf_close_coeff +from control.bdalg import _ensure_tf, append, connect, feedback +from control.lti import poles, zeros from control.statesp import StateSpace -from control.bdalg import feedback, append, connect -from control.lti import zeros, poles -from control.bdalg import _ensure_tf +from control.tests.conftest import assert_tf_close_coeff +from control.xferfcn import TransferFunction +from numpy import sort class TestFeedback: - """These are tests for the feedback function in bdalg.py. Currently, some - of the tests are not implemented, or are not working properly. TODO: these - need to be fixed.""" + """Tests for the feedback function in bdalg.py.""" @pytest.fixture def tsys(self): @@ -180,7 +177,7 @@ def testTFTF(self, tsys): [[[1., 4., 9., 8., 5.]]]) def testLists(self, tsys): - """Make sure that lists of various lengths work for operations""" + """Make sure that lists of various lengths work for operations.""" sys1 = ctrl.tf([1, 1], [1, 2]) sys2 = ctrl.tf([1, 3], [1, 4]) sys3 = ctrl.tf([1, 5], [1, 6]) @@ -237,7 +234,7 @@ def testLists(self, tsys): sort(zeros(sys1 + sys2 + sys3 + sys4 + sys5))) def testMimoSeries(self, tsys): - """regression: bdalg.series reverses order of arguments""" + """regression: bdalg.series reverses order of arguments.""" g1 = ctrl.ss([], [], [], [[1, 2], [0, 3]]) g2 = ctrl.ss([], [], [], [[1, 0], [2, 3]]) ref = g2 * g1 @@ -430,9 +427,9 @@ class TestEnsureTf: ], ) def test_ensure(self, arraylike_or_tf, dt, tf): - """Test nominal cases""" + """Test nominal cases.""" ensured_tf = _ensure_tf(arraylike_or_tf, dt) - assert _tf_close_coeff(tf, ensured_tf) + assert_tf_close_coeff(tf, ensured_tf) @pytest.mark.parametrize( "arraylike_or_tf, dt, exception", @@ -460,7 +457,7 @@ def test_ensure(self, arraylike_or_tf, dt, tf): ], ) def test_error_ensure(self, arraylike_or_tf, dt, exception): - """Test error cases""" + """Test error cases.""" with pytest.raises(exception): _ensure_tf(arraylike_or_tf, dt) @@ -624,7 +621,7 @@ class TestTfCombineSplit: def test_combine_tf(self, tf_array, tf): """Test combining transfer functions.""" tf_combined = ctrl.combine_tf(tf_array) - assert _tf_close_coeff(tf_combined, tf) + assert_tf_close_coeff(tf_combined, tf) @pytest.mark.parametrize( "tf_array, tf", @@ -712,12 +709,12 @@ def test_split_tf(self, tf_array, tf): # Test entry-by-entry for i in range(tf_split.shape[0]): for j in range(tf_split.shape[1]): - assert _tf_close_coeff( + assert_tf_close_coeff( tf_split[i, j], tf_array[i, j], ) # Test combined - assert _tf_close_coeff( + assert_tf_close_coeff( ctrl.combine_tf(tf_split), ctrl.combine_tf(tf_array), ) diff --git a/control/tests/conftest.py b/control/tests/conftest.py index 004b96058..94b9599b4 100644 --- a/control/tests/conftest.py +++ b/control/tests/conftest.py @@ -1,4 +1,4 @@ -"""conftest.py - pytest local plugins and fixtures""" +"""conftest.py - pytest local plugins, fixtures, marks and functions.""" import os from contextlib import contextmanager @@ -9,6 +9,7 @@ import control + # some common pytest marks. These can be used as test decorators or in # pytest.param(marks=) slycotonly = pytest.mark.skipif( @@ -61,7 +62,7 @@ def mplcleanup(): @pytest.fixture(scope="function") def legacy_plot_signature(): - """Turn off warnings for calls to plotting functions with old signatures""" + """Turn off warnings for calls to plotting functions with old signatures.""" import warnings warnings.filterwarnings( 'ignore', message='passing systems .* is deprecated', @@ -75,14 +76,53 @@ def legacy_plot_signature(): @pytest.fixture(scope="function") def ignore_future_warning(): - """Turn off warnings for functions that generate FutureWarning""" + """Turn off warnings for functions that generate FutureWarning.""" import warnings warnings.filterwarnings( 'ignore', message='.*deprecated', category=FutureWarning) yield warnings.resetwarnings() - -# Allow pytest.mark.slow to mark slow tests (skip with pytest -m "not slow") + def pytest_configure(config): + """Allow pytest.mark.slow to mark slow tests. + + skip with pytest -m "not slow" + """ config.addinivalue_line("markers", "slow: mark test as slow to run") + + +def assert_tf_close_coeff(tf_a, tf_b, rtol=1e-5, atol=1e-8): + """Check if two transfer functions have close coefficients. + + Parameters + ---------- + tf_a : TransferFunction + First transfer function. + tf_b : TransferFunction + Second transfer function. + rtol : float + Relative tolerance for ``np.testing.assert_allclose``. + atol : float + Absolute tolerance for ``np.testing.assert_allclose``. + + Raises + ------ + AssertionError + """ + # Check number of outputs and inputs + assert tf_a.noutputs == tf_b.noutputs + assert tf_a.ninputs == tf_b.ninputs + # Check timestep + assert tf_a.dt == tf_b.dt + # Check coefficient arrays + for i in range(tf_a.noutputs): + for j in range(tf_a.ninputs): + np.testing.assert_allclose( + tf_a.num[i][j], + tf_b.num[i][j], + rtol=rtol, atol=atol) + np.testing.assert_allclose( + tf_a.den[i][j], + tf_b.den[i][j], + rtol=rtol, atol=atol) diff --git a/control/tests/statesp_test.py b/control/tests/statesp_test.py index 6d57a38a9..6fe240839 100644 --- a/control/tests/statesp_test.py +++ b/control/tests/statesp_test.py @@ -7,22 +7,22 @@ convert to pytest """ -import numpy as np -from numpy.testing import assert_array_almost_equal -import pytest import operator -from numpy.linalg import solve -from scipy.linalg import block_diag, eigvals import control as ct +import numpy as np +import pytest from control.config import defaults from control.dtime import sample_system from control.lti import evalfr -from control.statesp import StateSpace, _convert_to_statespace, tf2ss, \ - _statesp_defaults, _rss_generate, linfnorm, ss, rss, drss -from control.xferfcn import TransferFunction, ss2tf, _tf_close_coeff +from control.statesp import (StateSpace, _convert_to_statespace, _rss_generate, + _statesp_defaults, drss, linfnorm, rss, ss, tf2ss) +from control.xferfcn import TransferFunction, ss2tf +from numpy.linalg import solve +from numpy.testing import assert_array_almost_equal +from scipy.linalg import block_diag, eigvals -from .conftest import editsdefaults, slycotonly +from .conftest import assert_tf_close_coeff, editsdefaults, slycotonly class TestStateSpace: @@ -384,7 +384,7 @@ def test_add_sub_mimo_siso(self): (StateSpace.__rsub__, -expected_sub), ]: result = op(ss_mimo, ss_siso) - assert _tf_close_coeff( + assert_tf_close_coeff( expected.minreal(), ss2tf(result).minreal(), ) @@ -404,7 +404,7 @@ def test_add_sub_mimo_siso(self): (StateSpace.__rsub__, -expected_sub), ]: result = op(ss_siso, np.eye(2)) - assert _tf_close_coeff( + assert_tf_close_coeff( expected.minreal(), ss2tf(result).minreal(), ) @@ -479,7 +479,7 @@ def test_add_sub_mimo_siso(self): ) def test_mul_mimo_siso(self, left, right, expected): result = tf2ss(left).__mul__(right) - assert _tf_close_coeff( + assert_tf_close_coeff( expected.minreal(), ss2tf(result).minreal(), ) @@ -554,7 +554,7 @@ def test_mul_mimo_siso(self, left, right, expected): ) def test_rmul_mimo_siso(self, left, right, expected): result = tf2ss(right).__rmul__(left) - assert _tf_close_coeff( + assert_tf_close_coeff( expected.minreal(), ss2tf(result).minreal(), ) @@ -584,13 +584,13 @@ def test_pow(self, sys222, sys322): # matrices instead. result = (sys * sys**-1).minreal() expected = StateSpace([], [], [], np.eye(2), dt=0) - assert _tf_close_coeff( + assert_tf_close_coeff( ss2tf(expected).minreal(), ss2tf(result).minreal(), ) result = (sys**-1 * sys).minreal() expected = StateSpace([], [], [], np.eye(2), dt=0) - assert _tf_close_coeff( + assert_tf_close_coeff( ss2tf(expected).minreal(), ss2tf(result).minreal(), ) @@ -616,14 +616,14 @@ def test_truediv(self, sys222, sys322): # Divide by self result = (sys.__truediv__(sys)).minreal() expected = StateSpace([], [], [], np.eye(2), dt=0) - assert _tf_close_coeff( + assert_tf_close_coeff( ss2tf(expected).minreal(), ss2tf(result).minreal(), ) # Divide by TF result = sys.__truediv__(TransferFunction.s) expected = ss2tf(sys) / TransferFunction.s - assert _tf_close_coeff( + assert_tf_close_coeff( expected.minreal(), ss2tf(result).minreal(), ) @@ -634,14 +634,14 @@ def test_rtruediv(self, sys222, sys322): for sys in [sys222, sys322]: result = (sys.__rtruediv__(sys)).minreal() expected = StateSpace([], [], [], np.eye(2), dt=0) - assert _tf_close_coeff( + assert_tf_close_coeff( ss2tf(expected).minreal(), ss2tf(result).minreal(), ) # Divide TF by SS result = sys.__rtruediv__(TransferFunction.s) expected = TransferFunction.s / sys - assert _tf_close_coeff( + assert_tf_close_coeff( expected.minreal(), result.minreal(), ) @@ -649,7 +649,7 @@ def test_rtruediv(self, sys222, sys322): sys = tf2ss(TransferFunction([1, 2], [2, 1])) result = sys.__rtruediv__(np.eye(2)) expected = TransferFunction([2, 1], [1, 2]) * np.eye(2) - assert _tf_close_coeff( + assert_tf_close_coeff( expected.minreal(), ss2tf(result).minreal(), ) diff --git a/control/tests/xferfcn_test.py b/control/tests/xferfcn_test.py index 00000f52c..b7be91187 100644 --- a/control/tests/xferfcn_test.py +++ b/control/tests/xferfcn_test.py @@ -10,12 +10,12 @@ import pytest import control as ct -from control import StateSpace, TransferFunction, defaults, evalfr, isctime, \ - isdtime, reset_defaults, rss, sample_system, set_defaults, ss, ss2tf, tf, \ - tf2ss, zpk +from control import (StateSpace, TransferFunction, defaults, evalfr, isctime, + isdtime, reset_defaults, rss, sample_system, set_defaults, + ss, ss2tf, tf, tf2ss, zpk) from control.statesp import _convert_to_statespace -from control.tests.conftest import slycotonly -from control.xferfcn import _convert_to_transfer_function, _tf_close_coeff +from control.tests.conftest import assert_tf_close_coeff, slycotonly +from control.xferfcn import _convert_to_transfer_function class TestXferFcn: @@ -424,7 +424,7 @@ def test_add_sub_mimo_siso(self): [op(tf_arr[1, 0], tf_siso), op(tf_arr[1, 1], tf_siso)], ]) result = op(tf_mimo, tf_siso) - assert _tf_close_coeff(expected.minreal(), result.minreal()) + assert_tf_close_coeff(expected.minreal(), result.minreal()) @pytest.mark.parametrize( "left, right, expected", @@ -496,7 +496,7 @@ def test_add_sub_mimo_siso(self): def test_mul_mimo_siso(self, left, right, expected): """Test multiplication of a MIMO and a SISO system.""" result = left.__mul__(right) - assert _tf_close_coeff(expected.minreal(), result.minreal()) + assert_tf_close_coeff(expected.minreal(), result.minreal()) @pytest.mark.parametrize( "left, right, expected", @@ -568,7 +568,7 @@ def test_mul_mimo_siso(self, left, right, expected): def test_rmul_mimo_siso(self, left, right, expected): """Test right multiplication of a MIMO and a SISO system.""" result = right.__rmul__(left) - assert _tf_close_coeff(expected.minreal(), result.minreal()) + assert_tf_close_coeff(expected.minreal(), result.minreal()) @pytest.mark.parametrize( "left, right, expected", @@ -605,7 +605,7 @@ def test_rmul_mimo_siso(self, left, right, expected): def test_truediv_mimo_siso(self, left, right, expected): """Test true division of a MIMO and a SISO system.""" result = left.__truediv__(right) - assert _tf_close_coeff(expected.minreal(), result.minreal()) + assert_tf_close_coeff(expected.minreal(), result.minreal()) @pytest.mark.parametrize( "left, right, expected", @@ -631,7 +631,7 @@ def test_truediv_mimo_siso(self, left, right, expected): def test_rtruediv_mimo_siso(self, left, right, expected): """Test right true division of a MIMO and a SISO system.""" result = right.__rtruediv__(left) - assert _tf_close_coeff(expected.minreal(), result.minreal()) + assert_tf_close_coeff(expected.minreal(), result.minreal()) @pytest.mark.parametrize("named", [False, True]) def test_slice(self, named): @@ -925,9 +925,9 @@ def test_append(self): ], ) tf_appended_1 = tf1.append(tf2) - assert _tf_close_coeff(tf_exp_1, tf_appended_1) + assert_tf_close_coeff(tf_exp_1, tf_appended_1) tf_appended_2 = tf1.append(tf2).append(tf3) - assert _tf_close_coeff(tf_exp_2, tf_appended_2) + assert_tf_close_coeff(tf_exp_2, tf_appended_2) def test_convert_to_transfer_function(self): """Test for correct state space to transfer function conversion.""" diff --git a/control/xferfcn.py b/control/xferfcn.py index 4a8fd4a1c..bc98d71e4 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -2007,53 +2007,6 @@ def _float2str(value): return f"{value:{_num_format}}" -def _tf_close_coeff(tf_a, tf_b, rtol=1e-5, atol=1e-8): - """Check if two transfer functions have close coefficients. - - Parameters - ---------- - tf_a : TransferFunction - First transfer function. - tf_b : TransferFunction - Second transfer function. - rtol : float - Relative tolerance for ``np.allclose``. - atol : float - Absolute tolerance for ``np.allclose``. - - Returns - ------- - bool - True if transfer function cofficients are all close. - """ - # Check number of outputs and inputs - if tf_a.noutputs != tf_b.noutputs: - return False - if tf_a.ninputs != tf_b.ninputs: - return False - # Check timestep - if tf_a.dt != tf_b.dt: - return False - # Check coefficient arrays - for i in range(tf_a.noutputs): - for j in range(tf_a.ninputs): - if not np.allclose( - tf_a.num[i][j], - tf_b.num[i][j], - rtol=rtol, - atol=atol, - ): - return False - if not np.allclose( - tf_a.den[i][j], - tf_b.den[i][j], - rtol=rtol, - atol=atol, - ): - return False - return True - - def _create_poly_array(shape, default=None): out = np.empty(shape, dtype=np.ndarray) if default is not None: From 1ceeeea1c855117f9295c3c9d14d883b87ab8c5c Mon Sep 17 00:00:00 2001 From: Ben Greiner Date: Sat, 1 Feb 2025 10:43:50 +0100 Subject: [PATCH 2/5] Fix docstring and imports --- .github/scripts/set-conda-test-matrix.py | 5 +---- .github/scripts/set-pip-test-matrix.py | 4 +--- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/.github/scripts/set-conda-test-matrix.py b/.github/scripts/set-conda-test-matrix.py index 2e0b95688..69ef0a57e 100644 --- a/.github/scripts/set-conda-test-matrix.py +++ b/.github/scripts/set-conda-test-matrix.py @@ -1,8 +1,5 @@ -""" set-conda-test-matrix.py +"""Create test matrix for conda packages in OS/BLAS test matrix workflow.""" -Create test matrix for conda packages -""" -import json, re from pathlib import Path osmap = {'linux': 'ubuntu', diff --git a/.github/scripts/set-pip-test-matrix.py b/.github/scripts/set-pip-test-matrix.py index ed18239d0..a28a63240 100644 --- a/.github/scripts/set-pip-test-matrix.py +++ b/.github/scripts/set-pip-test-matrix.py @@ -1,7 +1,5 @@ -""" set-pip-test-matrix.py +"""Create test matrix for pip wheels in OS/BLAS test matrix workflow.""" -Create test matrix for pip wheels -""" import json from pathlib import Path From 1b6e49be0e2b1cb233e2ede14c03df01cd0fdbfb Mon Sep 17 00:00:00 2001 From: Ben Greiner Date: Sat, 1 Feb 2025 11:25:18 +0100 Subject: [PATCH 3/5] Parametrize pow tests --- control/tests/statesp_test.py | 84 +++++++++++++++-------------------- 1 file changed, 37 insertions(+), 47 deletions(-) diff --git a/control/tests/statesp_test.py b/control/tests/statesp_test.py index 6fe240839..73340121c 100644 --- a/control/tests/statesp_test.py +++ b/control/tests/statesp_test.py @@ -1,4 +1,4 @@ -"""statesp_test.py - test state space class +"""Tests for the StateSpace class. RMM, 30 Mar 2011 based on TestStateSp from v0.4a) RMM, 14 Jun 2019 statesp_array_test.py coverted from statesp_test.py to test @@ -560,54 +560,44 @@ def test_rmul_mimo_siso(self, left, right, expected): ) @slycotonly - def test_pow(self, sys222, sys322): + @pytest.mark.parametrize("power", [0, 1, 3, -3]) + @pytest.mark.parametrize("sysname", ["sys222", "sys322"]) + def test_pow(self, request, sysname, power): """Test state space powers.""" - for sys in [sys222, sys322]: - # Power of 0 - result = sys**0 - expected = StateSpace([], [], [], np.eye(2), dt=0) - np.testing.assert_allclose(expected.A, result.A) - np.testing.assert_allclose(expected.B, result.B) - np.testing.assert_allclose(expected.C, result.C) - np.testing.assert_allclose(expected.D, result.D) - # Power of 1 - result = sys**1 - expected = sys - np.testing.assert_allclose(expected.A, result.A) - np.testing.assert_allclose(expected.B, result.B) - np.testing.assert_allclose(expected.C, result.C) - np.testing.assert_allclose(expected.D, result.D) - # Power of -1 (inverse of biproper system) - # Testing transfer function representations to avoid the - # non-uniqueness of the state-space representation. Once MIMO - # canonical forms are supported, can check canonical state-space - # matrices instead. - result = (sys * sys**-1).minreal() - expected = StateSpace([], [], [], np.eye(2), dt=0) - assert_tf_close_coeff( - ss2tf(expected).minreal(), - ss2tf(result).minreal(), - ) + sys = request.getfixturevalue(sysname) + result = sys**power + if power == 0: + expected = StateSpace([], [], [], np.eye(sys.ninputs), dt=0) + else: + sign = 1 if power > 0 else -1 + expected = sys**sign + for i in range(1,abs(power)): + expected *= sys**sign + np.testing.assert_allclose(expected.A, result.A) + np.testing.assert_allclose(expected.B, result.B) + np.testing.assert_allclose(expected.C, result.C) + np.testing.assert_allclose(expected.D, result.D) + + @slycotonly + @pytest.mark.parametrize("order", ["inv*sys", "sys*inv"]) + @pytest.mark.parametrize("sysname", ["sys222", "sys322"]) + def test_pow_inv(self, request, sysname, order): + """Power of -1 (inverse of biproper system). + + Testing transfer function representations to avoid the + non-uniqueness of the state-space representation. Once MIMO + canonical forms are supported, can check canonical state-space + matrices instead. + """ + sys = request.getfixturevalue(sysname) + if order == "inv*sys": result = (sys**-1 * sys).minreal() - expected = StateSpace([], [], [], np.eye(2), dt=0) - assert_tf_close_coeff( - ss2tf(expected).minreal(), - ss2tf(result).minreal(), - ) - # Power of 3 - result = sys**3 - expected = sys * sys * sys - np.testing.assert_allclose(expected.A, result.A) - np.testing.assert_allclose(expected.B, result.B) - np.testing.assert_allclose(expected.C, result.C) - np.testing.assert_allclose(expected.D, result.D) - # Power of -3 - result = sys**-3 - expected = sys**-1 * sys**-1 * sys**-1 - np.testing.assert_allclose(expected.A, result.A) - np.testing.assert_allclose(expected.B, result.B) - np.testing.assert_allclose(expected.C, result.C) - np.testing.assert_allclose(expected.D, result.D) + else: + result = (sys * sys**-1).minreal() + expected = StateSpace([], [], [], np.eye(sys.ninputs), dt=0) + assert_tf_close_coeff( + ss2tf(expected).minreal(), + ss2tf(result).minreal()) @slycotonly def test_truediv(self, sys222, sys322): From 72aef924c107b40cb266a01845ddb330c8fc5ae9 Mon Sep 17 00:00:00 2001 From: Ben Greiner Date: Sat, 1 Feb 2025 17:10:36 +0100 Subject: [PATCH 4/5] Rename tested tf to actual and desired --- control/tests/conftest.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/control/tests/conftest.py b/control/tests/conftest.py index 94b9599b4..bf3920a02 100644 --- a/control/tests/conftest.py +++ b/control/tests/conftest.py @@ -92,15 +92,13 @@ def pytest_configure(config): config.addinivalue_line("markers", "slow: mark test as slow to run") -def assert_tf_close_coeff(tf_a, tf_b, rtol=1e-5, atol=1e-8): +def assert_tf_close_coeff(actual, desired, rtol=1e-5, atol=1e-8): """Check if two transfer functions have close coefficients. Parameters ---------- - tf_a : TransferFunction - First transfer function. - tf_b : TransferFunction - Second transfer function. + actual, desired : TransferFunction + Transfer functions to compare. rtol : float Relative tolerance for ``np.testing.assert_allclose``. atol : float @@ -111,18 +109,18 @@ def assert_tf_close_coeff(tf_a, tf_b, rtol=1e-5, atol=1e-8): AssertionError """ # Check number of outputs and inputs - assert tf_a.noutputs == tf_b.noutputs - assert tf_a.ninputs == tf_b.ninputs + assert actual.noutputs == desired.noutputs + assert actual.ninputs == desired.ninputs # Check timestep - assert tf_a.dt == tf_b.dt + assert actual.dt == desired.dt # Check coefficient arrays - for i in range(tf_a.noutputs): - for j in range(tf_a.ninputs): + for i in range(actual.noutputs): + for j in range(actual.ninputs): np.testing.assert_allclose( - tf_a.num[i][j], - tf_b.num[i][j], + actual.num[i][j], + desired.num[i][j], rtol=rtol, atol=atol) np.testing.assert_allclose( - tf_a.den[i][j], - tf_b.den[i][j], + actual.den[i][j], + desired.den[i][j], rtol=rtol, atol=atol) From 2359299f34f23147dc9417481d2103cb9e048af5 Mon Sep 17 00:00:00 2001 From: Ben Greiner Date: Sat, 1 Feb 2025 18:44:20 +0100 Subject: [PATCH 5/5] Update test to check for inverse by identity --- control/tests/statesp_test.py | 44 +++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/control/tests/statesp_test.py b/control/tests/statesp_test.py index 73340121c..08a6566bb 100644 --- a/control/tests/statesp_test.py +++ b/control/tests/statesp_test.py @@ -9,20 +9,21 @@ import operator -import control as ct import numpy as np import pytest +from numpy.linalg import solve +from numpy.testing import assert_array_almost_equal +from scipy.linalg import block_diag, eigvals + +import control as ct from control.config import defaults from control.dtime import sample_system from control.lti import evalfr from control.statesp import (StateSpace, _convert_to_statespace, _rss_generate, _statesp_defaults, drss, linfnorm, rss, ss, tf2ss) +from control.tests.conftest import (assert_tf_close_coeff, editsdefaults, + slycotonly) from control.xferfcn import TransferFunction, ss2tf -from numpy.linalg import solve -from numpy.testing import assert_array_almost_equal -from scipy.linalg import block_diag, eigvals - -from .conftest import assert_tf_close_coeff, editsdefaults, slycotonly class TestStateSpace: @@ -579,25 +580,28 @@ def test_pow(self, request, sysname, power): np.testing.assert_allclose(expected.D, result.D) @slycotonly - @pytest.mark.parametrize("order", ["inv*sys", "sys*inv"]) - @pytest.mark.parametrize("sysname", ["sys222", "sys322"]) + @pytest.mark.parametrize("order", ["left", "right"]) + @pytest.mark.parametrize("sysname", ["sys121", "sys222", "sys322"]) def test_pow_inv(self, request, sysname, order): - """Power of -1 (inverse of biproper system). + """Check for identity when multiplying by inverse. - Testing transfer function representations to avoid the - non-uniqueness of the state-space representation. Once MIMO - canonical forms are supported, can check canonical state-space - matrices instead. + This holds approximately true for a few steps but is very + unstable due to numerical precision. Don't assume this in + real life. For testing purposes only! """ sys = request.getfixturevalue(sysname) - if order == "inv*sys": - result = (sys**-1 * sys).minreal() + if order == "left": + combined = sys**-1 * sys else: - result = (sys * sys**-1).minreal() - expected = StateSpace([], [], [], np.eye(sys.ninputs), dt=0) - assert_tf_close_coeff( - ss2tf(expected).minreal(), - ss2tf(result).minreal()) + combined = sys * sys**-1 + combined = combined.minreal() + np.testing.assert_allclose(combined.dcgain(), np.eye(sys.ninputs), + atol=1e-7) + T = np.linspace(0., 0.3, 100) + U = np.random.rand(sys.ninputs, len(T)) + R = combined.forced_response(T=T, U=U, squeeze=False) + # Check that the output is the same as the input + np.testing.assert_allclose(R.outputs, U) @slycotonly def test_truediv(self, sys222, sys322):