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 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..bf3920a02 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,51 @@ 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(actual, desired, rtol=1e-5, atol=1e-8): + """Check if two transfer functions have close coefficients. + + Parameters + ---------- + actual, desired : TransferFunction + Transfer functions to compare. + 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 actual.noutputs == desired.noutputs + assert actual.ninputs == desired.ninputs + # Check timestep + assert actual.dt == desired.dt + # Check coefficient arrays + for i in range(actual.noutputs): + for j in range(actual.ninputs): + np.testing.assert_allclose( + actual.num[i][j], + desired.num[i][j], + rtol=rtol, atol=atol) + np.testing.assert_allclose( + actual.den[i][j], + desired.den[i][j], + rtol=rtol, atol=atol) diff --git a/control/tests/statesp_test.py b/control/tests/statesp_test.py index 6d57a38a9..08a6566bb 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 @@ -7,22 +7,23 @@ convert to pytest """ +import operator + import numpy as np -from numpy.testing import assert_array_almost_equal import pytest -import operator 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, tf2ss, \ - _statesp_defaults, _rss_generate, linfnorm, ss, rss, drss -from control.xferfcn import TransferFunction, ss2tf, _tf_close_coeff - -from .conftest import editsdefaults, slycotonly +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 class TestStateSpace: @@ -384,7 +385,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 +405,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 +480,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,60 +555,53 @@ 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(), ) @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(), - ) - 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) + 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", ["left", "right"]) + @pytest.mark.parametrize("sysname", ["sys121", "sys222", "sys322"]) + def test_pow_inv(self, request, sysname, order): + """Check for identity when multiplying by inverse. + + 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 == "left": + combined = sys**-1 * sys + else: + 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): @@ -616,14 +610,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 +628,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 +643,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: pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy