From 5e16344bbccfb89bc9d15419845a658b8f48bf8c Mon Sep 17 00:00:00 2001 From: bnavigator Date: Tue, 29 Dec 2020 22:57:32 +0100 Subject: [PATCH 01/30] reorganize travis matrix, extend conftest.py --- .travis.yml | 68 ++++++++---------------------- control/tests/conftest.py | 82 +++++++++++++++++++++++++++++++++--- control/tests/margin_test.py | 0 control/tests/pzmap_test.py | 0 setup.cfg | 1 - setup.py | 2 +- 6 files changed, 94 insertions(+), 59 deletions(-) mode change 100755 => 100644 control/tests/conftest.py mode change 100755 => 100644 control/tests/margin_test.py mode change 100755 => 100644 control/tests/pzmap_test.py diff --git a/.travis.yml b/.travis.yml index ec615501d..3e700c485 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,72 +13,38 @@ cache: - $HOME/.local python: + - "3.9" + - "3.8" - "3.7" - "3.6" - - "2.7" -# Test against multiple version of SciPy, with and without slycot -# -# Because there were significant changes in SciPy between v0 and v1, we -# test against both of these using the Travis CI environment capability -# -# We also want to test with and without slycot env: - SCIPY=scipy SLYCOT=conda # default, with slycot via conda - SCIPY=scipy SLYCOT= # default, w/out slycot - - SCIPY="scipy==0.19.1" SLYCOT= # legacy support, w/out slycot # Add optional builds that test against latest version of slycot, python jobs: include: - - name: "linux, Python 2.7, slycot=source" - os: linux - dist: xenial - services: xvfb - python: "2.7" - env: SCIPY=scipy SLYCOT=source - - name: "linux, Python 3.7, slycot=source" - os: linux - dist: xenial - services: xvfb - python: "3.7" - env: SCIPY=scipy SLYCOT=source - - name: "linux, Python 3.8, slycot=source" - os: linux - dist: xenial - services: xvfb + - name: "Python 3.8, slycot=source" python: "3.8" env: SCIPY=scipy SLYCOT=source - - name: "use numpy matrix" - dist: xenial - services: xvfb - python: "3.8" - env: SCIPY=scipy SLYCOT=source PYTHON_CONTROL_STATESPACE_ARRAY=1 - - # Exclude combinations that are very unlikely (and don't work) - exclude: - - python: "3.7" # python3.7 should use latest scipy + - name: "Python 3.9, slycot=source, array and matrix" + python: "3.9" + env: SCIPY=scipy SLYCOT=source PYTHON_CONTROL_ARRAY_AND_MATRIX=1 + # Because there were significant changes in SciPy between v0 and v1, we + # also test against the latest v0 (without Slycot) for old pythons. + # newer pythons should always use newer SciPy. + - name: "Python 2.7, Scipy 0.19.1" + python: "2.7" + env: SCIPY="scipy==0.19.1" SLYCOT= + - name: "Python 3.6, Scipy 0.19.1" + python: "3.6" env: SCIPY="scipy==0.19.1" SLYCOT= allow_failures: - - name: "linux, Python 2.7, slycot=source" - os: linux - dist: xenial - services: xvfb - python: "2.7" - env: SCIPY=scipy SLYCOT=source - - name: "linux, Python 3.7, slycot=source" - os: linux - dist: xenial - services: xvfb - python: "3.7" - env: SCIPY=scipy SLYCOT=source - - name: "linux, Python 3.8, slycot=source" - os: linux - dist: xenial - services: xvfb - python: "3.8" - env: SCIPY=scipy SLYCOT=source + - env: SCIPY=scipy SLYCOT=source + - env: SCIPY=scipy SLYCOT=source PYTHON_CONTROL_ARRAY_AND_MATRIX=1 + # install required system libraries before_install: diff --git a/control/tests/conftest.py b/control/tests/conftest.py old mode 100755 new mode 100644 index 60c3d0de1..7204c8f14 --- a/control/tests/conftest.py +++ b/control/tests/conftest.py @@ -1,18 +1,88 @@ -# contest.py - pytest local plugins and fixtures +"""conftest.py - pytest local plugins and fixtures""" +from contextlib import contextmanager +from distutils.version import StrictVersion import os +import sys import matplotlib as mpl +import numpy as np +import scipy as sp import pytest import control +TEST_MATRIX_AND_ARRAY = os.getenv("PYTHON_CONTROL_ARRAY_AND_MATRIX") == "1" -@pytest.fixture(scope="session", autouse=True) -def use_numpy_ndarray(): - """Switch the config to use ndarray instead of matrix""" - if os.getenv("PYTHON_CONTROL_STATESPACE_ARRAY") == "1": - control.config.defaults['statesp.use_numpy_matrix'] = False +# some common pytest marks. These can be used as test decorators or in +# pytest.param(marks=) +slycotonly = pytest.mark.skipif(not control.exception.slycot_check(), + reason="slycot not installed") +noscipy0 = pytest.mark.skipif(StrictVersion(sp.__version__) < "1.0", + reason="requires SciPy 1.0 or greater") +nopython2 = pytest.mark.skipif(sys.version_info < (3, 0), + reason="requires Python 3+") +matrixfilter = pytest.mark.filterwarnings("ignore:.*matrix subclass:" + "PendingDeprecationWarning") +matrixerrorfilter = pytest.mark.filterwarnings("error:.*matrix subclass:" + "PendingDeprecationWarning") + + +@pytest.fixture(scope="session", autouse=TEST_MATRIX_AND_ARRAY, + params=[pytest.param("arrayout", marks=matrixerrorfilter), + pytest.param("matrixout", marks=matrixfilter)]) +def matarrayout(request): + """Switch the config to use np.ndarray and np.matrix as returns""" + restore = control.config.defaults['statesp.use_numpy_matrix'] + control.use_numpy_matrix(request.param == "matrixout", warn=False) + yield + control.use_numpy_matrix(restore, warn=False) + + +def ismatarrayout(obj): + """Test if the returned object has the correct type as configured + + note that isinstance(np.matrix(obj), np.ndarray) is True + """ + use_matrix = control.config.defaults['statesp.use_numpy_matrix'] + return (isinstance(obj, np.ndarray) + and isinstance(obj, np.matrix) == use_matrix) + + +def asmatarrayout(obj): + """Return a object according to the configured default""" + use_matrix = control.config.defaults['statesp.use_numpy_matrix'] + matarray = np.asmatrix if use_matrix else np.asarray + return matarray(obj) + + +@contextmanager +def check_deprecated_matrix(): + """Check that a call produces a deprecation warning because of np.matrix""" + use_matrix = control.config.defaults['statesp.use_numpy_matrix'] + if use_matrix: + with pytest.deprecated_call(): + try: + yield + finally: + pass + else: + yield + + +@pytest.fixture(scope="session", + params=[p for p, usebydefault in + [(pytest.param(np.array, + id="arrayin"), + True), + (pytest.param(np.matrix, + id="matrixin", + marks=matrixfilter), + False)] + if usebydefault or TEST_MATRIX_AND_ARRAY]) +def matarrayin(request): + """Use array and matrix to construct input data in tests""" + return request.param @pytest.fixture(scope="function") diff --git a/control/tests/margin_test.py b/control/tests/margin_test.py old mode 100755 new mode 100644 diff --git a/control/tests/pzmap_test.py b/control/tests/pzmap_test.py old mode 100755 new mode 100644 diff --git a/setup.cfg b/setup.cfg index ac4f92c75..38ca3b912 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,5 +4,4 @@ universal=1 [tool:pytest] filterwarnings = ignore:.*matrix subclass:PendingDeprecationWarning - ignore:.*scipy:DeprecationWarning diff --git a/setup.py b/setup.py index ec16d7135..fcf2d740b 100644 --- a/setup.py +++ b/setup.py @@ -19,10 +19,10 @@ Programming Language :: Python :: 2 Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 -Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 +Programming Language :: Python :: 3.9 Topic :: Software Development Topic :: Scientific/Engineering Operating System :: Microsoft :: Windows From c220a98e7fd4445c10b96ab3c779ac3fce9eb91b Mon Sep 17 00:00:00 2001 From: bnavigator Date: Tue, 29 Dec 2020 01:33:23 +0100 Subject: [PATCH 02/30] pytestify bdalg_test --- control/tests/bdalg_test.py | 214 +++++++++++++++++------------------- 1 file changed, 102 insertions(+), 112 deletions(-) diff --git a/control/tests/bdalg_test.py b/control/tests/bdalg_test.py index a7ec6c14b..fc5f78f91 100644 --- a/control/tests/bdalg_test.py +++ b/control/tests/bdalg_test.py @@ -1,50 +1,53 @@ -#!/usr/bin/env python -# -# bdalg_test.py - test suite for block diagram algebra -# RMM, 30 Mar 2011 (based on TestBDAlg from v0.4a) +"""bdalg_test.py - test suite for block diagram algebra + +RMM, 30 Mar 2011 (based on TestBDAlg from v0.4a) +""" -import unittest import numpy as np from numpy import sort +import pytest + import control as ctrl from control.xferfcn import TransferFunction from control.statesp import StateSpace from control.bdalg import feedback, append, connect from control.lti import zero, pole -class TestFeedback(unittest.TestCase): + +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.""" - def setUp(self): - """This contains some random LTI systems and scalars for testing.""" - - # Two random SISO systems. - self.sys1 = TransferFunction([1, 2], [1, 2, 3]) - self.sys2 = StateSpace([[1., 4.], [3., 2.]], [[1.], [-4.]], - [[1., 0.]], [[0.]]) # 2 states, SISO - self.sys3 = StateSpace([[-1.]], [[1.]], [[1.]], [[0.]]) # 1 state, SISO + @pytest.fixture + def tsys(self): + class T: + pass + # Three SISO systems. + T.sys1 = TransferFunction([1, 2], [1, 2, 3]) + T.sys2 = StateSpace([[1., 4.], [3., 2.]], [[1.], [-4.]], + [[1., 0.]], [[0.]]) + T.sys3 = StateSpace([[-1.]], [[1.]], [[1.]], [[0.]]) # 1 state, SISO # Two random scalars. - self.x1 = 2.5 - self.x2 = -3. + T.x1 = 2.5 + T.x2 = -3. + return T - def testScalarScalar(self): + def testScalarScalar(self, tsys): """Scalar system with scalar feedback block.""" + ans1 = feedback(tsys.x1, tsys.x2) + ans2 = feedback(tsys.x1, tsys.x2, 1.) - ans1 = feedback(self.x1, self.x2) - ans2 = feedback(self.x1, self.x2, 1.) - - self.assertAlmostEqual(ans1.num[0][0][0] / ans1.den[0][0][0], - -2.5 / 6.5) - self.assertAlmostEqual(ans2.num[0][0][0] / ans2.den[0][0][0], 2.5 / 8.5) + np.testing.assert_almost_equal( + ans1.num[0][0][0] / ans1.den[0][0][0], -2.5 / 6.5) + np.testing.assert_almost_equal( + ans2.num[0][0][0] / ans2.den[0][0][0], 2.5 / 8.5) - def testScalarSS(self): + def testScalarSS(self, tsys): """Scalar system with state space feedback block.""" - - ans1 = feedback(self.x1, self.sys2) - ans2 = feedback(self.x1, self.sys2, 1.) + ans1 = feedback(tsys.x1, tsys.sys2) + ans2 = feedback(tsys.x1, tsys.sys2, 1.) np.testing.assert_array_almost_equal(ans1.A, [[-1.5, 4.], [13., 2.]]) np.testing.assert_array_almost_equal(ans1.B, [[2.5], [-10.]]) @@ -56,18 +59,17 @@ def testScalarSS(self): np.testing.assert_array_almost_equal(ans2.D, [[2.5]]) # Make sure default arugments work as well - ans3 = feedback(self.sys2, 1) - ans4 = feedback(self.sys2) + ans3 = feedback(tsys.sys2, 1) + ans4 = feedback(tsys.sys2) np.testing.assert_array_almost_equal(ans3.A, ans4.A) np.testing.assert_array_almost_equal(ans3.B, ans4.B) np.testing.assert_array_almost_equal(ans3.C, ans4.C) np.testing.assert_array_almost_equal(ans3.D, ans4.D) - def testScalarTF(self): + def testScalarTF(self, tsys): """Scalar system with transfer function feedback block.""" - - ans1 = feedback(self.x1, self.sys1) - ans2 = feedback(self.x1, self.sys1, 1.) + ans1 = feedback(tsys.x1, tsys.sys1) + ans2 = feedback(tsys.x1, tsys.sys1, 1.) np.testing.assert_array_almost_equal(ans1.num, [[[2.5, 5., 7.5]]]) np.testing.assert_array_almost_equal(ans1.den, [[[1., 4.5, 8.]]]) @@ -75,16 +77,15 @@ def testScalarTF(self): np.testing.assert_array_almost_equal(ans2.den, [[[1., -0.5, -2.]]]) # Make sure default arugments work as well - ans3 = feedback(self.sys1, 1) - ans4 = feedback(self.sys1) + ans3 = feedback(tsys.sys1, 1) + ans4 = feedback(tsys.sys1) np.testing.assert_array_almost_equal(ans3.num, ans4.num) np.testing.assert_array_almost_equal(ans3.den, ans4.den) - def testSSScalar(self): + def testSSScalar(self, tsys): """State space system with scalar feedback block.""" - - ans1 = feedback(self.sys2, self.x1) - ans2 = feedback(self.sys2, self.x1, 1.) + ans1 = feedback(tsys.sys2, tsys.x1) + ans2 = feedback(tsys.sys2, tsys.x1, 1.) np.testing.assert_array_almost_equal(ans1.A, [[-1.5, 4.], [13., 2.]]) np.testing.assert_array_almost_equal(ans1.B, [[1.], [-4.]]) @@ -95,11 +96,10 @@ def testSSScalar(self): np.testing.assert_array_almost_equal(ans2.C, [[1., 0.]]) np.testing.assert_array_almost_equal(ans2.D, [[0.]]) - def testSSSS1(self): + def testSSSS1(self, tsys): """State space system with state space feedback block.""" - - ans1 = feedback(self.sys2, self.sys2) - ans2 = feedback(self.sys2, self.sys2, 1.) + ans1 = feedback(tsys.sys2, tsys.sys2) + ans2 = feedback(tsys.sys2, tsys.sys2, 1.) np.testing.assert_array_almost_equal(ans1.A, [[1., 4., -1., 0.], [3., 2., 4., 0.], [1., 0., 1., 4.], [-4., 0., 3., 2]]) @@ -112,10 +112,9 @@ def testSSSS1(self): np.testing.assert_array_almost_equal(ans2.C, [[1., 0., 0., 0.]]) np.testing.assert_array_almost_equal(ans2.D, [[0.]]) - def testSSSS2(self): + def testSSSS2(self, tsys): """State space system with state space feedback block, including a direct feedthrough term.""" - sys3 = StateSpace([[-1., 4.], [2., -3]], [[2.], [3.]], [[-3., 1.]], [[-2.]]) sys4 = StateSpace([[-3., -2.], [1., 4.]], [[-2.], [-6.]], [[2., -3.]], @@ -147,42 +146,39 @@ def testSSSS2(self): np.testing.assert_array_almost_equal(ans2.D, [[-0.285714285714286]]) - def testSSTF(self): + def testSSTF(self, tsys): """State space system with transfer function feedback block.""" - # This functionality is not implemented yet. pass - def testTFScalar(self): + def testTFScalar(self, tsys): """Transfer function system with scalar feedback block.""" - - ans1 = feedback(self.sys1, self.x1) - ans2 = feedback(self.sys1, self.x1, 1.) + ans1 = feedback(tsys.sys1, tsys.x1) + ans2 = feedback(tsys.sys1, tsys.x1, 1.) np.testing.assert_array_almost_equal(ans1.num, [[[1., 2.]]]) np.testing.assert_array_almost_equal(ans1.den, [[[1., 4.5, 8.]]]) np.testing.assert_array_almost_equal(ans2.num, [[[1., 2.]]]) np.testing.assert_array_almost_equal(ans2.den, [[[1., -0.5, -2.]]]) - def testTFSS(self): + def testTFSS(self, tsys): """Transfer function system with state space feedback block.""" - # This functionality is not implemented yet. pass - def testTFTF(self): + def testTFTF(self, tsys): """Transfer function system with transfer function feedback block.""" - - ans1 = feedback(self.sys1, self.sys1) - ans2 = feedback(self.sys1, self.sys1, 1.) + ans1 = feedback(tsys.sys1, tsys.sys1) + ans2 = feedback(tsys.sys1, tsys.sys1, 1.) np.testing.assert_array_almost_equal(ans1.num, [[[1., 4., 7., 6.]]]) np.testing.assert_array_almost_equal(ans1.den, - [[[1., 4., 11., 16., 13.]]]) + [[[1., 4., 11., 16., 13.]]]) np.testing.assert_array_almost_equal(ans2.num, [[[1., 4., 7., 6.]]]) - np.testing.assert_array_almost_equal(ans2.den, [[[1., 4., 9., 8., 5.]]]) + np.testing.assert_array_almost_equal(ans2.den, + [[[1., 4., 9., 8., 5.]]]) - def testLists(self): + def testLists(self, tsys): """Make sure that lists of various lengths work for operations""" sys1 = ctrl.tf([1, 1], [1, 2]) sys2 = ctrl.tf([1, 3], [1, 4]) @@ -195,19 +191,19 @@ def testLists(self): np.testing.assert_array_almost_equal(sort(pole(sys1_2)), [-4., -2.]) np.testing.assert_array_almost_equal(sort(zero(sys1_2)), [-3., -1.]) - sys1_3 = ctrl.series(sys1, sys2, sys3); + sys1_3 = ctrl.series(sys1, sys2, sys3) np.testing.assert_array_almost_equal(sort(pole(sys1_3)), [-6., -4., -2.]) np.testing.assert_array_almost_equal(sort(zero(sys1_3)), [-5., -3., -1.]) - sys1_4 = ctrl.series(sys1, sys2, sys3, sys4); + sys1_4 = ctrl.series(sys1, sys2, sys3, sys4) np.testing.assert_array_almost_equal(sort(pole(sys1_4)), [-8., -6., -4., -2.]) np.testing.assert_array_almost_equal(sort(zero(sys1_4)), [-7., -5., -3., -1.]) - sys1_5 = ctrl.series(sys1, sys2, sys3, sys4, sys5); + sys1_5 = ctrl.series(sys1, sys2, sys3, sys4, sys5) np.testing.assert_array_almost_equal(sort(pole(sys1_5)), [-8., -6., -4., -2., -0.]) np.testing.assert_array_almost_equal(sort(zero(sys1_5)), @@ -219,109 +215,103 @@ def testLists(self): np.testing.assert_array_almost_equal(sort(zero(sys1_2)), sort(zero(sys1 + sys2))) - sys1_3 = ctrl.parallel(sys1, sys2, sys3); + sys1_3 = ctrl.parallel(sys1, sys2, sys3) np.testing.assert_array_almost_equal(sort(pole(sys1_3)), [-6., -4., -2.]) np.testing.assert_array_almost_equal(sort(zero(sys1_3)), sort(zero(sys1 + sys2 + sys3))) - sys1_4 = ctrl.parallel(sys1, sys2, sys3, sys4); + sys1_4 = ctrl.parallel(sys1, sys2, sys3, sys4) np.testing.assert_array_almost_equal(sort(pole(sys1_4)), [-8., -6., -4., -2.]) - np.testing.assert_array_almost_equal(sort(zero(sys1_4)), - sort(zero(sys1 + sys2 + - sys3 + sys4))) - + np.testing.assert_array_almost_equal( + sort(zero(sys1_4)), + sort(zero(sys1 + sys2 + sys3 + sys4))) - sys1_5 = ctrl.parallel(sys1, sys2, sys3, sys4, sys5); + sys1_5 = ctrl.parallel(sys1, sys2, sys3, sys4, sys5) np.testing.assert_array_almost_equal(sort(pole(sys1_5)), [-8., -6., -4., -2., -0.]) - np.testing.assert_array_almost_equal(sort(zero(sys1_5)), - sort(zero(sys1 + sys2 + - sys3 + sys4 + sys5))) - def testMimoSeries(self): + np.testing.assert_array_almost_equal( + sort(zero(sys1_5)), + sort(zero(sys1 + sys2 + sys3 + sys4 + sys5))) + + def testMimoSeries(self, tsys): """regression: bdalg.series reverses order of arguments""" - g1 = ctrl.ss([],[],[],[[1,2],[0,3]]) - g2 = ctrl.ss([],[],[],[[1,0],[2,3]]) - ref = g2*g1 - tst = ctrl.series(g1,g2) - # assert_array_equal on mismatched matrices gives - # "repr failed for : ..." - def assert_equal(x,y): - np.testing.assert_array_equal(np.asarray(x), - np.asarray(y)) - assert_equal(ref.A, tst.A) - assert_equal(ref.B, tst.B) - assert_equal(ref.C, tst.C) - assert_equal(ref.D, tst.D) - - def test_feedback_args(self): + g1 = ctrl.ss([], [], [], [[1, 2], [0, 3]]) + g2 = ctrl.ss([], [], [], [[1, 0], [2, 3]]) + ref = g2 * g1 + tst = ctrl.series(g1, g2) + + np.testing.assert_array_equal(ref.A, tst.A) + np.testing.assert_array_equal(ref.B, tst.B) + np.testing.assert_array_equal(ref.C, tst.C) + np.testing.assert_array_equal(ref.D, tst.D) + + def test_feedback_args(self, tsys): # Added 25 May 2019 to cover missing exception handling in feedback() # If first argument is not LTI or convertable, generate an exception - args = ([1], self.sys2) - self.assertRaises(TypeError, ctrl.feedback, *args) + args = ([1], tsys.sys2) + with pytest.raises(TypeError): + ctrl.feedback(*args) # If second argument is not LTI or convertable, generate an exception - args = (self.sys1, np.array([1])) - self.assertRaises(TypeError, ctrl.feedback, *args) + args = (tsys.sys1, np.array([1])) + with pytest.raises(TypeError): + ctrl.feedback(*args) # Convert first argument to FRD, if needed h = TransferFunction([1], [1, 2, 2]) omega = np.logspace(-1, 2, 10) frd = ctrl.FRD(h, omega) sys = ctrl.feedback(1, frd) - self.assertTrue(isinstance(sys, ctrl.FRD)) + assert isinstance(sys, ctrl.FRD) - def testConnect(self): - sys = append(self.sys2, self.sys3) # two siso systems + def testConnect(self, tsys): + sys = append(tsys.sys2, tsys.sys3) # two siso systems # should not raise error connect(sys, [[1, 2], [2, -2]], [2], [1, 2]) connect(sys, [[1, 2], [2, 0]], [2], [1, 2]) connect(sys, [[1, 2, 0], [2, -2, 1]], [2], [1, 2]) connect(sys, [[1, 2], [2, -2]], [2, 1], [1]) - sys3x3 = append(sys, self.sys3) # 3x3 mimo + sys3x3 = append(sys, tsys.sys3) # 3x3 mimo connect(sys3x3, [[1, 2, 0], [2, -2, 1], [3, -3, 0]], [2], [1, 2]) connect(sys3x3, [[1, 2, 0], [2, -2, 1], [3, -3, 0]], [1, 2, 3], [3]) connect(sys3x3, [[1, 2, 0], [2, -2, 1], [3, -3, 0]], [2, 3], [2, 1]) # feedback interconnection out of bounds: input too high Q = [[1, 3], [2, -2]] - with self.assertRaises(IndexError): + with pytest.raises(IndexError): connect(sys, Q, [2], [1, 2]) # feedback interconnection out of bounds: input too low Q = [[0, 2], [2, -2]] - with self.assertRaises(IndexError): + with pytest.raises(IndexError): connect(sys, Q, [2], [1, 2]) # feedback interconnection out of bounds: output too high Q = [[1, 2], [2, -3]] - with self.assertRaises(IndexError): + with pytest.raises(IndexError): connect(sys, Q, [2], [1, 2]) Q = [[1, 2], [2, 4]] - with self.assertRaises(IndexError): + with pytest.raises(IndexError): connect(sys, Q, [2], [1, 2]) # input/output index testing - Q = [[1, 2], [2, -2]] # OK interconnection + Q = [[1, 2], [2, -2]] # OK interconnection # input index is out of bounds: too high - with self.assertRaises(IndexError): + with pytest.raises(IndexError): connect(sys, Q, [3], [1, 2]) # input index is out of bounds: too low - with self.assertRaises(IndexError): + with pytest.raises(IndexError): connect(sys, Q, [0], [1, 2]) - with self.assertRaises(IndexError): + with pytest.raises(IndexError): connect(sys, Q, [-2], [1, 2]) # output index is out of bounds: too high - with self.assertRaises(IndexError): + with pytest.raises(IndexError): connect(sys, Q, [2], [1, 3]) # output index is out of bounds: too low - with self.assertRaises(IndexError): + with pytest.raises(IndexError): connect(sys, Q, [2], [1, 0]) - with self.assertRaises(IndexError): + with pytest.raises(IndexError): connect(sys, Q, [2], [1, -1]) - - -if __name__ == "__main__": - unittest.main() From 90a0059aba62278dcc25214007d5fa090847ac74 Mon Sep 17 00:00:00 2001 From: bnavigator Date: Sun, 26 Jul 2020 13:31:26 +0200 Subject: [PATCH 03/30] pytestify canonical_test --- control/tests/canonical_test.py | 167 +++++++++++++------------------- 1 file changed, 67 insertions(+), 100 deletions(-) diff --git a/control/tests/canonical_test.py b/control/tests/canonical_test.py index 7d4ae4e27..f88f1af56 100644 --- a/control/tests/canonical_test.py +++ b/control/tests/canonical_test.py @@ -1,24 +1,24 @@ -#!/usr/bin/env python +"""canonical_test.py""" -import unittest import numpy as np -from control import ss, tf, tf2ss, ss2tf +import pytest + +from control import ss, tf, tf2ss from control.canonical import canonical_form, reachable_form, \ observable_form, modal_form, similarity_transform from control.exception import ControlNotImplemented -class TestCanonical(unittest.TestCase): +class TestCanonical: """Tests for the canonical forms class""" def test_reachable_form(self): """Test the reachable canonical form""" - # Create a system in the reachable canonical form coeffs = [1.0, 2.0, 3.0, 4.0, 1.0] A_true = np.polynomial.polynomial.polycompanion(coeffs) A_true = np.fliplr(np.rot90(A_true)) - B_true = np.matrix("1.0 0.0 0.0 0.0").T - C_true = np.matrix("1.0 1.0 1.0 1.0") + B_true = np.array([[1.0, 0.0, 0.0, 0.0]]).T + C_true = np.array([[1.0, 1.0, 1.0, 1.0]]) D_true = 42.0 # Perform a coordinate transform with a random invertible matrix @@ -44,30 +44,46 @@ def test_reachable_form(self): # Reachable form only supports SISO sys = tf([[ [1], [1] ]], [[ [1, 2, 1], [1, 2, 1] ]]) np.testing.assert_raises(ControlNotImplemented, reachable_form, sys) - def test_unreachable_system(self): """Test reachable canonical form with an unreachable system""" - # Create an unreachable system - A = np.matrix("1.0 2.0 2.0; 4.0 5.0 5.0; 7.0 8.0 8.0") - B = np.matrix("1.0 1.0 1.0").T - C = np.matrix("1.0 1.0 1.0") - D = 42.0 + A = np.array([[1., 2., 2.], + [4., 5., 5.], + [7., 8., 8.]]) + B = np.array([[1.], [1.],[1.]]) + C = np.array([[1., 1.,1.]]) + D = np.array([[42.0]]) sys = ss(A, B, C, D) # Check if an exception is raised np.testing.assert_raises(ValueError, canonical_form, sys, "reachable") - def test_modal_form(self): + @pytest.mark.parametrize( + "A_true, B_true, C_true, D_true", + [(np.diag([4.0, 3.0, 2.0, 1.0]), # order from largest to smallest + np.array([[1.1, 2.2, 3.3, 4.4]]).T, + np.array([[1.3, 1.4, 1.5, 1.6]]), + np.array([[42.0]])), + (np.array([[-1, 1, 0, 0], + [-1, -1, 0, 0], + [ 0, 0, -2, 0], + [ 0, 0, 0, -3]]), + np.array([[0, 1, 0, 1]]).T, + np.array([[1, 0, 0, 1]]), + np.array([[0]])), + # Reorder rows to get complete coverage (real eigenvalue cxrtvfirst) + (np.array([[-1, 0, 0, 0], + [ 0, -2, 1, 0], + [ 0, -1, -2, 0], + [ 0, 0, 0, -3]]), + np.array([[0, 0, 1, 1]]).T, + np.array([[0, 1, 0, 1]]), + np.array([[0]])), + ], + ids=["sys1", "sys2", "sys3"]) + def test_modal_form(self, A_true, B_true, C_true, D_true): """Test the modal canonical form""" - - # Create a system in the modal canonical form - A_true = np.diag([4.0, 3.0, 2.0, 1.0]) # order from the largest to the smallest - B_true = np.matrix("1.1 2.2 3.3 4.4").T - C_true = np.matrix("1.3 1.4 1.5 1.6") - D_true = 42.0 - # Perform a coordinate transform with a random invertible matrix T_true = np.array([[-0.27144004, -0.39933167, 0.75634684, 0.44135471], [-0.74855725, -0.39136285, -0.18142339, -0.50356997], @@ -75,41 +91,24 @@ def test_modal_form(self): [-0.44769516, 0.15654653, -0.50060858, 0.72419146]]) A = np.linalg.solve(T_true, A_true).dot(T_true) B = np.linalg.solve(T_true, B_true) - C = C_true*T_true + C = C_true.dot(T_true) D = D_true - # Create a state space system and convert it to the modal canonical form + # Create a state space system and convert it to modal canonical form sys_check, T_check = canonical_form(ss(A, B, C, D), "modal") # Check against the true values - # TODO: Test in respect to ambiguous transformation (system characteristics?) + # TODO: Test in respect to ambiguous transformation + # (system characteristics?) np.testing.assert_array_almost_equal(sys_check.A, A_true) #np.testing.assert_array_almost_equal(sys_check.B, B_true) #np.testing.assert_array_almost_equal(sys_check.C, C_true) np.testing.assert_array_almost_equal(sys_check.D, D_true) #np.testing.assert_array_almost_equal(T_check, T_true) - # Check conversion when there are complex eigenvalues - A_true = np.array([[-1, 1, 0, 0], - [-1, -1, 0, 0], - [ 0, 0, -2, 0], - [ 0, 0, 0, -3]]) - B_true = np.array([[0], [1], [0], [1]]) - C_true = np.array([[1, 0, 0, 1]]) - D_true = np.array([[0]]) - - A = np.linalg.solve(T_true, A_true).dot(T_true) - B = np.linalg.solve(T_true, B_true) - C = C_true.dot(T_true) - D = D_true - # Create state space system and convert to modal canonical form sys_check, T_check = canonical_form(ss(A, B, C, D), 'modal') - # Check A and D matrix, which are uniquely defined - np.testing.assert_array_almost_equal(sys_check.A, A_true) - np.testing.assert_array_almost_equal(sys_check.D, D_true) - # B matrix should be all ones (or zero if not controllable) # TODO: need to update modal_form() to implement this if np.allclose(T_check, T_true): @@ -117,59 +116,26 @@ def test_modal_form(self): np.testing.assert_array_almost_equal(sys_check.C, C_true) # Make sure Hankel coefficients are OK - from numpy.linalg import matrix_power for i in range(A.shape[0]): np.testing.assert_almost_equal( - np.dot(np.dot(C_true, matrix_power(A_true, i)), B_true), - np.dot(np.dot(C, matrix_power(A, i)), B)) - - # Reorder rows to get complete coverage (real eigenvalue cxrtvfirst) - A_true = np.array([[-1, 0, 0, 0], - [ 0, -2, 1, 0], - [ 0, -1, -2, 0], - [ 0, 0, 0, -3]]) - B_true = np.array([[0], [0], [1], [1]]) - C_true = np.array([[0, 1, 0, 1]]) - D_true = np.array([[0]]) - - A = np.linalg.solve(T_true, A_true).dot(T_true) - B = np.linalg.solve(T_true, B_true) - C = C_true.dot(T_true) - D = D_true - - # Create state space system and convert to modal canonical form - sys_check, T_check = canonical_form(ss(A, B, C, D), 'modal') + np.dot(np.dot(C_true, np.linalg.matrix_power(A_true, i)), + B_true), + np.dot(np.dot(C, np.linalg.matrix_power(A, i)), B)) - # Check A and D matrix, which are uniquely defined - np.testing.assert_array_almost_equal(sys_check.A, A_true) - np.testing.assert_array_almost_equal(sys_check.D, D_true) + def test_modal_form_MIMO(self): + """Test error because modal form only supports SISO""" + sys = tf([[[1], [1]]], [[[1, 2, 1], [1, 2, 1]]]) + with pytest.raises(ControlNotImplemented): + modal_form(sys) - # B matrix should be all ones (or zero if not controllable) - # TODO: need to update modal_form() to implement this - if np.allclose(T_check, T_true): - np.testing.assert_array_almost_equal(sys_check.B, B_true) - np.testing.assert_array_almost_equal(sys_check.C, C_true) - - # Make sure Hankel coefficients are OK - from numpy.linalg import matrix_power - for i in range(A.shape[0]): - np.testing.assert_almost_equal( - np.dot(np.dot(C_true, matrix_power(A_true, i)), B_true), - np.dot(np.dot(C, matrix_power(A, i)), B)) - - # Modal form only supports SISO - sys = tf([[ [1], [1] ]], [[ [1, 2, 1], [1, 2, 1] ]]) - np.testing.assert_raises(ControlNotImplemented, modal_form, sys) - def test_observable_form(self): """Test the observable canonical form""" - # Create a system in the observable canonical form coeffs = [1.0, 2.0, 3.0, 4.0, 1.0] A_true = np.polynomial.polynomial.polycompanion(coeffs) A_true = np.fliplr(np.flipud(A_true)) - B_true = np.matrix("1.0 1.0 1.0 1.0").T - C_true = np.matrix("1.0 0.0 0.0 0.0") + B_true = np.array([[1.0, 1.0, 1.0, 1.0]]).T + C_true = np.array([[1.0, 0.0, 0.0, 0.0]]) D_true = 42.0 # Perform a coordinate transform with a random invertible matrix @@ -192,31 +158,35 @@ def test_observable_form(self): np.testing.assert_array_almost_equal(sys_check.D, D_true) np.testing.assert_array_almost_equal(T_check, T_true) - # Observable form only supports SISO - sys = tf([[ [1], [1] ]], [[ [1, 2, 1], [1, 2, 1] ]]) - np.testing.assert_raises(ControlNotImplemented, observable_form, sys) - + def test_observable_form_MIMO(self): + """Test error as Observable form only supports SISO""" + sys = tf([[[1], [1] ]], [[[1, 2, 1], [1, 2, 1]]]) + with pytest.raises(ControlNotImplemented): + observable_form(sys) def test_unobservable_system(self): """Test observable canonical form with an unobservable system""" - # Create an unobservable system - A = np.matrix("1.0 2.0 2.0; 4.0 5.0 5.0; 7.0 8.0 8.0") - B = np.matrix("1.0 1.0 1.0").T - C = np.matrix("1.0 1.0 1.0") + A = np.array([[1., 2., 2.], + [4., 5., 5.], + [7., 8., 8.]]) + + B = np.array([[1.], [1.], [1.]]) + C = np.array([[1., 1., 1.]]) D = 42.0 sys = ss(A, B, C, D) # Check if an exception is raised - np.testing.assert_raises(ValueError, canonical_form, sys, "observable") + with pytest.raises(ValueError): + canonical_form(sys, "observable") def test_arguments(self): # Additional unit tests added on 25 May 2019 to increase coverage # Unknown canonical forms should generate exception sys = tf([1], [1, 2, 1]) - np.testing.assert_raises( - ControlNotImplemented, canonical_form, sys, 'unknown') + with pytest.raises(ControlNotImplemented): + canonical_form(sys, 'unknown') def test_similarity(self): """Test similarty transform""" @@ -261,7 +231,7 @@ def test_similarity(self): np.testing.assert_array_almost_equal(mimo_new.B, mimo_ini.B) np.testing.assert_array_almost_equal(mimo_new.C, mimo_ini.C) np.testing.assert_array_almost_equal(mimo_new.D, mimo_ini.D) - + # Time rescaling mimo_tim = similarity_transform(mimo_ini, np.eye(4), timescale=0.3) mimo_new = similarity_transform(mimo_tim, np.eye(4), timescale=1/0.3) @@ -287,7 +257,4 @@ def test_similarity(self): np.testing.assert_array_almost_equal(mimo_new.B, mimo_ini.B) np.testing.assert_array_almost_equal(mimo_new.C, mimo_ini.C) np.testing.assert_array_almost_equal(mimo_new.D, mimo_ini.D) - -if __name__ == "__main__": - unittest.main() From 79b5bfe53da7e042f38f7c14a95c588f47618a94 Mon Sep 17 00:00:00 2001 From: bnavigator Date: Tue, 29 Dec 2020 02:02:10 +0100 Subject: [PATCH 04/30] pytestify config_test --- control/tests/config_test.py | 178 +++++++++++++++++------------------ 1 file changed, 85 insertions(+), 93 deletions(-) diff --git a/control/tests/config_test.py b/control/tests/config_test.py index a8e86d1ff..ede683fe1 100644 --- a/control/tests/config_test.py +++ b/control/tests/config_test.py @@ -1,52 +1,55 @@ -#!/usr/bin/env python -# -# config_test.py - test config module -# RMM, 25 may 2019 -# -# This test suite checks the functionality of the config module - -import unittest +"""config_test.py - test config module + +RMM, 25 may 2019 + +This test suite checks the functionality of the config module +""" + +from math import pi, log10 + +import matplotlib.pyplot as plt +from matplotlib.testing.decorators import cleanup as mplcleanup import numpy as np +import pytest + import control as ct -import matplotlib.pyplot as plt -from math import pi, log10 -class TestConfig(unittest.TestCase): - def setUp(self): - # Create a simple second order system to use for testing - self.sys = ct.tf([10], [1, 2, 1]) +@pytest.mark.usefixtures("editsdefaults") # makes sure to reset the defaults + # to the test configuration +class TestConfig: + + # Create a simple second order system to use for testing + sys = ct.tf([10], [1, 2, 1]) def test_set_defaults(self): ct.config.set_defaults('config', test1=1, test2=2, test3=None) - self.assertEqual(ct.config.defaults['config.test1'], 1) - self.assertEqual(ct.config.defaults['config.test2'], 2) - self.assertEqual(ct.config.defaults['config.test3'], None) + assert ct.config.defaults['config.test1'] == 1 + assert ct.config.defaults['config.test2'] == 2 + assert ct.config.defaults['config.test3'] is None + @mplcleanup def test_get_param(self): - self.assertEqual( - ct.config._get_param('bode', 'dB'), - ct.config.defaults['bode.dB']) - self.assertEqual(ct.config._get_param('bode', 'dB', 1), 1) + assert ct.config._get_param('bode', 'dB')\ + == ct.config.defaults['bode.dB'] + assert ct.config._get_param('bode', 'dB', 1) == 1 ct.config.defaults['config.test1'] = 1 - self.assertEqual(ct.config._get_param('config', 'test1', None), 1) - self.assertEqual(ct.config._get_param('config', 'test1', None, 1), 1) - - ct.config.defaults['config.test3'] = None - self.assertEqual(ct.config._get_param('config', 'test3'), None) - self.assertEqual(ct.config._get_param('config', 'test3', 1), 1) - self.assertEqual( - ct.config._get_param('config', 'test3', None, 1), None) - - self.assertEqual(ct.config._get_param('config', 'test4'), None) - self.assertEqual(ct.config._get_param('config', 'test4', 1), 1) - self.assertEqual(ct.config._get_param('config', 'test4', 2, 1), 2) - self.assertEqual(ct.config._get_param('config', 'test4', None, 3), 3) - - self.assertEqual( - ct.config._get_param('config', 'test4', {'test4':1}, None), 1) + assert ct.config._get_param('config', 'test1', None) == 1 + assert ct.config._get_param('config', 'test1', None, 1) == 1 + + ct.config.defaults['config.test3'] is None + assert ct.config._get_param('config', 'test3') is None + assert ct.config._get_param('config', 'test3', 1) == 1 + assert ct.config._get_param('config', 'test3', None, 1) is None + + assert ct.config._get_param('config', 'test4') is None + assert ct.config._get_param('config', 'test4', 1) == 1 + assert ct.config._get_param('config', 'test4', 2, 1) == 2 + assert ct.config._get_param('config', 'test4', None, 3) == 3 + assert ct.config._get_param('config', 'test4', {'test4': 1}, None) == 1 + @mplcleanup def test_fbs_bode(self): ct.use_fbs_defaults() @@ -91,8 +94,7 @@ def test_fbs_bode(self): phase_x, phase_y = (((plt.gcf().axes[1]).get_lines())[0]).get_data() np.testing.assert_almost_equal(phase_y[-1], -pi, decimal=2) - ct.reset_defaults() - + @mplcleanup def test_matlab_bode(self): ct.use_matlab_defaults() @@ -120,7 +122,7 @@ def test_matlab_bode(self): # Make sure the x-axis is in rad/sec and y-axis is in degrees np.testing.assert_almost_equal(phase_x[-1], 1000, decimal=1) np.testing.assert_almost_equal(phase_y[-1], -180, decimal=0) - + # Override the defaults and make sure that works as well plt.figure() ct.bode_plot(self.sys, omega, dB=True) @@ -137,8 +139,7 @@ def test_matlab_bode(self): phase_x, phase_y = (((plt.gcf().axes[1]).get_lines())[0]).get_data() np.testing.assert_almost_equal(phase_y[-1], -pi, decimal=2) - ct.reset_defaults() - + @mplcleanup def test_custom_bode_default(self): ct.config.defaults['bode.dB'] = True ct.config.defaults['bode.deg'] = True @@ -160,24 +161,22 @@ def test_custom_bode_default(self): np.testing.assert_almost_equal(mag_y[0], 20*log10(10), decimal=3) np.testing.assert_almost_equal(phase_y[-1], -pi, decimal=2) - ct.reset_defaults() - + @mplcleanup def test_bode_number_of_samples(self): # Set the number of samples (default is 50, from np.logspace) mag_ret, phase_ret, omega_ret = ct.bode_plot(self.sys, omega_num=87) - self.assertEqual(len(mag_ret), 87) + assert len(mag_ret) == 87 # Change the default number of samples ct.config.defaults['freqplot.number_of_samples'] = 76 mag_ret, phase_ret, omega_ret = ct.bode_plot(self.sys) - self.assertEqual(len(mag_ret), 76) - + assert len(mag_ret) == 76 + # Override the default number of samples mag_ret, phase_ret, omega_ret = ct.bode_plot(self.sys, omega_num=87) - self.assertEqual(len(mag_ret), 87) - - ct.reset_defaults() + assert len(mag_ret) == 87 + @mplcleanup def test_bode_feature_periphery_decade(self): # Generate a sample Bode plot to figure out the range it uses ct.reset_defaults() # Make sure starting state is correct @@ -198,30 +197,26 @@ def test_bode_feature_periphery_decade(self): np.testing.assert_almost_equal(omega_ret[0], omega_min*10) np.testing.assert_almost_equal(omega_ret[-1], omega_max/10) - ct.reset_defaults() - def test_reset_defaults(self): ct.use_matlab_defaults() ct.reset_defaults() - self.assertEqual(ct.config.defaults['bode.dB'], False) - self.assertEqual(ct.config.defaults['bode.deg'], True) - self.assertEqual(ct.config.defaults['bode.Hz'], False) - self.assertEqual( - ct.config.defaults['freqplot.number_of_samples'], None) - self.assertEqual( - ct.config.defaults['freqplot.feature_periphery_decades'], 1.0) + assert not ct.config.defaults['bode.dB'] + assert ct.config.defaults['bode.deg'] + assert not ct.config.defaults['bode.Hz'] + assert ct.config.defaults['freqplot.number_of_samples'] is None + assert ct.config.defaults['freqplot.feature_periphery_decades'] == 1.0 def test_legacy_defaults(self): - ct.use_legacy_defaults('0.8.3') - assert(isinstance(ct.ss(0,0,0,1).D, np.matrix)) - + with pytest.deprecated_call(): + ct.use_legacy_defaults('0.8.3') + assert(isinstance(ct.ss(0, 0, 0, 1).D, np.matrix)) ct.reset_defaults() - assert(isinstance(ct.ss(0,0,0,1).D, np.ndarray)) - assert(not isinstance(ct.ss(0,0,0,1).D, np.matrix)) + assert(isinstance(ct.ss(0, 0, 0, 1).D, np.ndarray)) + assert(not isinstance(ct.ss(0, 0, 0, 1).D, np.matrix)) ct.use_legacy_defaults('0.9.0') - assert(isinstance(ct.ss(0,0,0,1).D, np.ndarray)) - assert(not isinstance(ct.ss(0,0,0,1).D, np.matrix)) + assert(isinstance(ct.ss(0, 0, 0, 1).D, np.ndarray)) + assert(not isinstance(ct.ss(0, 0, 0, 1).D, np.matrix)) # test that old versions don't raise a problem ct.use_legacy_defaults('REL-0.1') @@ -231,30 +226,27 @@ def test_legacy_defaults(self): ct.use_legacy_defaults('0.1') # Make sure that nonsense versions generate an error - self.assertRaises(ValueError, ct.use_legacy_defaults, "a.b.c") - self.assertRaises(ValueError, ct.use_legacy_defaults, "1.x.3") - - # Leave everything like we found it - ct.config.reset_defaults() - - def test_change_default_dt(self): - ct.set_defaults('statesp', default_dt=0) - self.assertEqual(ct.ss(0,0,0,1).dt, 0) - ct.set_defaults('statesp', default_dt=None) - self.assertEqual(ct.ss(0,0,0,1).dt, None) - ct.set_defaults('xferfcn', default_dt=0) - self.assertEqual(ct.tf(1, 1).dt, 0) - ct.set_defaults('xferfcn', default_dt=None) - self.assertEqual(ct.tf(1, 1).dt, None) - - - def tearDown(self): - # Get rid of any figures that we created - plt.close('all') - - # Reset the configuration defaults - ct.config.reset_defaults() - - -if __name__ == '__main__': - unittest.main() + with pytest.raises(ValueError): + ct.use_legacy_defaults("a.b.c") + with pytest.raises(ValueError): + ct.use_legacy_defaults("1.x.3") + + @pytest.mark.parametrize("dt", [0, None]) + def test_change_default_dt(self, dt): + """Test that system with dynamics uses correct default dt""" + ct.set_defaults('statesp', default_dt=dt) + assert ct.ss(1, 0, 0, 1).dt == dt + ct.set_defaults('xferfcn', default_dt=dt) + assert ct.tf(1, [1, 1]).dt == dt + + # nlsys = ct.iosys.NonlinearIOSystem( + # lambda t, x, u: u * x * x, + # lambda t, x, u: x, inputs=1, outputs=1) + # assert nlsys.dt == dt + + def test_change_default_dt_static(self): + """Test that static gain systems always have dt=None""" + ct.set_defaults('control', default_dt=0) + assert ct.tf(1, 1).dt is None + assert ct.ss(0, 0, 0, 1).dt is None + # TODO: add in test for static gain iosys From 74d563c5f6d37f9bf61c6a3a11ab59e4c726edf6 Mon Sep 17 00:00:00 2001 From: bnavigator Date: Sat, 25 Jul 2020 00:11:58 +0200 Subject: [PATCH 05/30] pytestify convert_test.py --- control/tests/convert_test.py | 379 ++++++++++++++++------------------ 1 file changed, 179 insertions(+), 200 deletions(-) diff --git a/control/tests/convert_test.py b/control/tests/convert_test.py index e0b0e0364..de1cf01d1 100644 --- a/control/tests/convert_test.py +++ b/control/tests/convert_test.py @@ -15,252 +15,234 @@ """ from __future__ import print_function -import unittest +from warnings import warn + import numpy as np -from control import matlab +import pytest + +from control import rss, ss, ss2tf, tf, tf2ss from control.statesp import _mimo2siso from control.statefbk import ctrb, obsv from control.freqplot import bode -from control.matlab import tf from control.exception import slycot_check +from control.tests.conftest import slycotonly -class TestConvert(unittest.TestCase): - """Test state space and transfer function conversions.""" - def setUp(self): - """Set up testing parameters.""" - - # Number of times to run each of the randomized tests. - self.numTests = 1 # almost guarantees failure - # Maximum number of states to test + 1 - self.maxStates = 4 - # Maximum number of inputs and outputs to test + 1 - # If slycot is not installed, just check SISO - self.maxIO = 5 if slycot_check() else 2 - # Set to True to print systems to the output. - self.debug = False - # get consistent results - np.random.seed(7) +# Set to True to print systems to the output. +verbose = False +# Maximum number of states to test + 1 +maxStates = 4 +# Maximum number of inputs and outputs to test + 1 +# If slycot is not installed, just check SISO +maxIO = 5 if slycot_check() else 2 + + +@pytest.fixture +def fixedseed(scope='module'): + """Get consistent results""" + np.random.seed(7) + + +class TestConvert: + """Test state space and transfer function conversions.""" def printSys(self, sys, ind): """Print system to the standard output.""" + print("sys%i:\n" % ind) + print(sys) - if self.debug: - print("sys%i:\n" % ind) - print(sys) - - def testConvert(self): - """Test state space to transfer function conversion.""" - verbose = self.debug - - # print __doc__ - - # Machine precision for floats. - # eps = np.finfo(float).eps - - for states in range(1, self.maxStates): - for inputs in range(1, self.maxIO): - for outputs in range(1, self.maxIO): - # start with a random SS system and transform to TF then - # back to SS, check that the matrices are the same. - ssOriginal = matlab.rss(states, outputs, inputs) - if (verbose): - self.printSys(ssOriginal, 1) - - # Make sure the system is not degenerate - Cmat = ctrb(ssOriginal.A, ssOriginal.B) - if (np.linalg.matrix_rank(Cmat) != states): - if (verbose): - print(" skipping (not reachable)") - continue - Omat = obsv(ssOriginal.A, ssOriginal.C) - if (np.linalg.matrix_rank(Omat) != states): - if (verbose): - print(" skipping (not observable)") - continue - - tfOriginal = matlab.tf(ssOriginal) - if (verbose): - self.printSys(tfOriginal, 2) - - ssTransformed = matlab.ss(tfOriginal) - if (verbose): - self.printSys(ssTransformed, 3) - - tfTransformed = matlab.tf(ssTransformed) - if (verbose): - self.printSys(tfTransformed, 4) - - # Check to see if the state space systems have same dim - if (ssOriginal.states != ssTransformed.states): - print("WARNING: state space dimension mismatch: " + \ - "%d versus %d" % \ - (ssOriginal.states, ssTransformed.states)) - - # Now make sure the frequency responses match - # Since bode() only handles SISO, go through each I/O pair - # For phase, take sine and cosine to avoid +/- 360 offset - for inputNum in range(inputs): - for outputNum in range(outputs): - if (verbose): - print("Checking input %d, output %d" \ - % (inputNum, outputNum)) - ssorig_mag, ssorig_phase, ssorig_omega = \ - bode(_mimo2siso(ssOriginal, \ - inputNum, outputNum), \ - deg=False, plot=False) - ssorig_real = ssorig_mag * np.cos(ssorig_phase) - ssorig_imag = ssorig_mag * np.sin(ssorig_phase) - - # - # Make sure TF has same frequency response - # - num = tfOriginal.num[outputNum][inputNum] - den = tfOriginal.den[outputNum][inputNum] - tforig = tf(num, den) - - tforig_mag, tforig_phase, tforig_omega = \ - bode(tforig, ssorig_omega, \ - deg=False, plot=False) - - tforig_real = tforig_mag * np.cos(tforig_phase) - tforig_imag = tforig_mag * np.sin(tforig_phase) - np.testing.assert_array_almost_equal( \ - ssorig_real, tforig_real) - np.testing.assert_array_almost_equal( \ - ssorig_imag, tforig_imag) - - # - # Make sure xform'd SS has same frequency response - # - ssxfrm_mag, ssxfrm_phase, ssxfrm_omega = \ - bode(_mimo2siso(ssTransformed, \ - inputNum, outputNum), \ - ssorig_omega, \ - deg=False, plot=False) - ssxfrm_real = ssxfrm_mag * np.cos(ssxfrm_phase) - ssxfrm_imag = ssxfrm_mag * np.sin(ssxfrm_phase) - np.testing.assert_array_almost_equal( \ - ssorig_real, ssxfrm_real) - np.testing.assert_array_almost_equal( \ - ssorig_imag, ssxfrm_imag) - # - # Make sure xform'd TF has same frequency response - # - num = tfTransformed.num[outputNum][inputNum] - den = tfTransformed.den[outputNum][inputNum] - tfxfrm = tf(num, den) - tfxfrm_mag, tfxfrm_phase, tfxfrm_omega = \ - bode(tfxfrm, ssorig_omega, \ - deg=False, plot=False) - - tfxfrm_real = tfxfrm_mag * np.cos(tfxfrm_phase) - tfxfrm_imag = tfxfrm_mag * np.sin(tfxfrm_phase) - np.testing.assert_array_almost_equal( \ - ssorig_real, tfxfrm_real) - np.testing.assert_array_almost_equal( \ - ssorig_imag, tfxfrm_imag) + @pytest.mark.parametrize("states", range(1, maxStates)) + @pytest.mark.parametrize("inputs", range(1, maxIO)) + @pytest.mark.parametrize("outputs", range(1, maxIO)) + def testConvert(self, fixedseed, states, inputs, outputs): + """Test state space to transfer function conversion. + + start with a random SS system and transform to TF then + back to SS, check that the matrices are the same. + """ + ssOriginal = rss(states, outputs, inputs) + if verbose: + self.printSys(ssOriginal, 1) + + # Make sure the system is not degenerate + Cmat = ctrb(ssOriginal.A, ssOriginal.B) + if (np.linalg.matrix_rank(Cmat) != states): + pytest.skip("not reachable") + Omat = obsv(ssOriginal.A, ssOriginal.C) + if (np.linalg.matrix_rank(Omat) != states): + pytest.skip("not observable") + + tfOriginal = tf(ssOriginal) + if (verbose): + self.printSys(tfOriginal, 2) + + ssTransformed = ss(tfOriginal) + if (verbose): + self.printSys(ssTransformed, 3) + + tfTransformed = tf(ssTransformed) + if (verbose): + self.printSys(tfTransformed, 4) + + # Check to see if the state space systems have same dim + if (ssOriginal.states != ssTransformed.states) and verbose: + print("WARNING: state space dimension mismatch: %d versus %d" % + (ssOriginal.states, ssTransformed.states)) + + # Now make sure the frequency responses match + # Since bode() only handles SISO, go through each I/O pair + # For phase, take sine and cosine to avoid +/- 360 offset + for inputNum in range(inputs): + for outputNum in range(outputs): + if (verbose): + print("Checking input %d, output %d" + % (inputNum, outputNum)) + ssorig_mag, ssorig_phase, ssorig_omega = \ + bode(_mimo2siso(ssOriginal, inputNum, outputNum), + deg=False, plot=False) + ssorig_real = ssorig_mag * np.cos(ssorig_phase) + ssorig_imag = ssorig_mag * np.sin(ssorig_phase) + + # + # Make sure TF has same frequency response + # + num = tfOriginal.num[outputNum][inputNum] + den = tfOriginal.den[outputNum][inputNum] + tforig = tf(num, den) + + tforig_mag, tforig_phase, tforig_omega = \ + bode(tforig, ssorig_omega, + deg=False, plot=False) + + tforig_real = tforig_mag * np.cos(tforig_phase) + tforig_imag = tforig_mag * np.sin(tforig_phase) + np.testing.assert_array_almost_equal( + ssorig_real, tforig_real) + np.testing.assert_array_almost_equal( + ssorig_imag, tforig_imag) + + # + # Make sure xform'd SS has same frequency response + # + ssxfrm_mag, ssxfrm_phase, ssxfrm_omega = \ + bode(_mimo2siso(ssTransformed, + inputNum, outputNum), + ssorig_omega, + deg=False, plot=False) + ssxfrm_real = ssxfrm_mag * np.cos(ssxfrm_phase) + ssxfrm_imag = ssxfrm_mag * np.sin(ssxfrm_phase) + np.testing.assert_array_almost_equal( + ssorig_real, ssxfrm_real, decimal=5) + np.testing.assert_array_almost_equal( + ssorig_imag, ssxfrm_imag, decimal=5) + + # Make sure xform'd TF has same frequency response + # + num = tfTransformed.num[outputNum][inputNum] + den = tfTransformed.den[outputNum][inputNum] + tfxfrm = tf(num, den) + tfxfrm_mag, tfxfrm_phase, tfxfrm_omega = \ + bode(tfxfrm, ssorig_omega, + deg=False, plot=False) + + tfxfrm_real = tfxfrm_mag * np.cos(tfxfrm_phase) + tfxfrm_imag = tfxfrm_mag * np.sin(tfxfrm_phase) + np.testing.assert_array_almost_equal( + ssorig_real, tfxfrm_real, decimal=5) + np.testing.assert_array_almost_equal( + ssorig_imag, tfxfrm_imag, decimal=5) def testConvertMIMO(self): - """Test state space to transfer function conversion.""" - verbose = self.debug - - # Do a MIMO conversation and make sure that it is processed - # correctly both with and without slycot - # - # Example from issue #120, jgoppert - import control - - # Set up a transfer function (should always work) - tfcn = control.tf([[[-235, 1.146e4], - [-235, 1.146E4], - [-235, 1.146E4, 0]]], - [[[1, 48.78, 0], - [1, 48.78, 0, 0], - [0.008, 1.39, 48.78]]]) + """Test state space to transfer function conversion. + + Do a MIMO conversation and make sure that it is processed + correctly both with and without slycot + + Example from issue gh-120, jgoppert + """ + + # Set up a 1x3 transfer function (should always work) + tsys = tf([[[-235, 1.146e4], + [-235, 1.146E4], + [-235, 1.146E4, 0]]], + [[[1, 48.78, 0], + [1, 48.78, 0, 0], + [0.008, 1.39, 48.78]]]) # Convert to state space and look for an error if (not slycot_check()): - self.assertRaises(TypeError, control.tf2ss, tfcn) + with pytest.raises(TypeError): + tf2ss(tsys) + else: + ssys = tf2ss(tsys) + assert ssys.B.shape[1] == 3 + assert ssys.C.shape[0] == 1 def testTf2ssStaticSiso(self): """Regression: tf2ss for SISO static gain""" - import control - gsiso = control.tf2ss(control.tf(23, 46)) - self.assertEqual(0, gsiso.states) - self.assertEqual(1, gsiso.inputs) - self.assertEqual(1, gsiso.outputs) - # in all cases ratios are exactly representable, so assert_array_equal is fine + gsiso = tf2ss(tf(23, 46)) + assert 0 == gsiso.states + assert 1 == gsiso.inputs + assert 1 == gsiso.outputs + # in all cases ratios are exactly representable, so assert_array_equal + # is fine np.testing.assert_array_equal([[0.5]], gsiso.D) def testTf2ssStaticMimo(self): """Regression: tf2ss for MIMO static gain""" - import control # 2x3 TFM - gmimo = control.tf2ss(control.tf( + gmimo = tf2ss(tf( [[ [23], [3], [5] ], [ [-1], [0.125], [101.3] ]], [[ [46], [0.1], [80] ], [ [2], [-0.1], [1] ]])) - self.assertEqual(0, gmimo.states) - self.assertEqual(3, gmimo.inputs) - self.assertEqual(2, gmimo.outputs) - d = np.matrix([[0.5, 30, 0.0625], [-0.5, -1.25, 101.3]]) + assert 0 == gmimo.states + assert 3 == gmimo.inputs + assert 2 == gmimo.outputs + d = np.array([[0.5, 30, 0.0625], [-0.5, -1.25, 101.3]]) np.testing.assert_array_equal(d, gmimo.D) def testSs2tfStaticSiso(self): """Regression: ss2tf for SISO static gain""" - import control - gsiso = control.ss2tf(control.ss([], [], [], 0.5)) + gsiso = ss2tf(ss([], [], [], 0.5)) np.testing.assert_array_equal([[[0.5]]], gsiso.num) np.testing.assert_array_equal([[[1.]]], gsiso.den) def testSs2tfStaticMimo(self): """Regression: ss2tf for MIMO static gain""" - import control # 2x3 TFM a = [] b = [] c = [] - d = np.matrix([[0.5, 30, 0.0625], [-0.5, -1.25, 101.3]]) - gtf = control.ss2tf(control.ss(a,b,c,d)) + d = np.array([[0.5, 30, 0.0625], [-0.5, -1.25, 101.3]]) + gtf = ss2tf(ss(a, b, c, d)) # we need a 3x2x1 array to compare with gtf.num - # np.testing.assert_array_equal doesn't seem to like a matrices - # with an extra dimension, so convert to ndarray - numref = np.asarray(d)[...,np.newaxis] - np.testing.assert_array_equal(numref, np.array(gtf.num) / np.array(gtf.den)) + numref = d[..., np.newaxis] + np.testing.assert_array_equal(numref, + np.array(gtf.num) / np.array(gtf.den)) + @slycotonly def testTf2SsDuplicatePoles(self): - """Tests for "too few poles for MIMO tf #111" """ - import control - try: - import slycot - num = [ [ [1], [0] ], - [ [0], [1] ] ] - - den = [ [ [1,0], [1] ], - [ [1], [1,0] ] ] - g = control.tf(num, den) - s = control.ss(g) - np.testing.assert_array_equal(g.pole(), s.pole()) - except ImportError: - print("Slycot not present, skipping") - - @unittest.skipIf(not slycot_check(), "slycot not installed") + """Tests for 'too few poles for MIMO tf gh-111'""" + num = [[[1], [0]], + [[0], [1]]] + den = [[[1, 0], [1]], + [[1], [1, 0]]] + g = tf(num, den) + s = ss(g) + np.testing.assert_array_equal(g.pole(), s.pole()) + + @slycotonly def test_tf2ss_robustness(self): - """Unit test to make sure that tf2ss is working correctly. - Source: https://github.com/python-control/python-control/issues/240 - """ - import control - + """Unit test to make sure that tf2ss is working correctly. gh-240""" num = [ [[0], [1]], [[1], [0]] ] den1 = [ [[1], [1,1]], [[1,4], [1]] ] - sys1tf = control.tf(num, den1) - sys1ss = control.tf2ss(sys1tf) + sys1tf = tf(num, den1) + sys1ss = tf2ss(sys1tf) # slight perturbation den2 = [ [[1], [1e-10, 1, 1]], [[1,4], [1]] ] - sys2tf = control.tf(num, den2) - sys2ss = control.tf2ss(sys2tf) + sys2tf = tf(num, den2) + sys2ss = tf2ss(sys2tf) # Make sure that the poles match for StateSpace and TransferFunction np.testing.assert_array_almost_equal(np.sort(sys1tf.pole()), @@ -268,6 +250,3 @@ def test_tf2ss_robustness(self): np.testing.assert_array_almost_equal(np.sort(sys2tf.pole()), np.sort(sys2ss.pole())) - -if __name__ == "__main__": - unittest.main() From c7cc828cbb2b5bb06de3ea329529bc73370fbd64 Mon Sep 17 00:00:00 2001 From: bnavigator Date: Sat, 25 Jul 2020 00:16:28 +0200 Subject: [PATCH 06/30] pytestify ctrlutil_test --- control/tests/ctrlutil_test.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/control/tests/ctrlutil_test.py b/control/tests/ctrlutil_test.py index 03a347154..460ff601c 100644 --- a/control/tests/ctrlutil_test.py +++ b/control/tests/ctrlutil_test.py @@ -1,11 +1,13 @@ -import unittest +"""ctrlutil_test.py""" + import numpy as np -from control.ctrlutil import * -class TestUtils(unittest.TestCase): - def setUp(self): - self.mag = np.array([1, 10, 100, 2, 0.1, 0.01]) - self.db = np.array([0, 20, 40, 6.0205999, -20, -40]) +from control.ctrlutil import db2mag, mag2db, unwrap + +class TestUtils: + + mag = np.array([1, 10, 100, 2, 0.1, 0.01]) + db = np.array([0, 20, 40, 6.0205999, -20, -40]) def check_unwrap_array(self, angle, period=None): if period is None: @@ -56,7 +58,3 @@ def test_mag2db(self): def test_mag2db_array(self): db_array = mag2db(self.mag) np.testing.assert_array_almost_equal(db_array, self.db) - - -if __name__ == "__main__": - unittest.main() From d899140931b5911f7c2451eec02a3fee3ed8e025 Mon Sep 17 00:00:00 2001 From: bnavigator Date: Sat, 25 Jul 2020 00:27:12 +0200 Subject: [PATCH 07/30] pytestify delay_test.py --- control/tests/delay_test.py | 89 ++++++++++++++++++++----------------- 1 file changed, 47 insertions(+), 42 deletions(-) diff --git a/control/tests/delay_test.py b/control/tests/delay_test.py index 17c049d24..533eb4a72 100644 --- a/control/tests/delay_test.py +++ b/control/tests/delay_test.py @@ -1,25 +1,25 @@ -#!/usr/bin/env python -*-coding: utf-8-*- -# -# Test Pade approx -# -# Primitive; ideally test to numerical limits +# -*- coding: utf-8 -*- +"""Test Pade approx -from __future__ import division +Primitive; ideally test to numerical limits +""" -import unittest +from __future__ import division import numpy as np +import pytest from control.delay import pade -class TestPade(unittest.TestCase): - - # Reference data from Miklos Vajta's paper "Some remarks on - # Padé-approximations", Table 1, with corrections. The - # corrections are to highest power coeff in numerator for - # (ddeg,ndeg)=(4,3) and (5,4); use Eq (12) in the paper to verify +class TestPade: + """Test Pade approx + Reference data from Miklos Vajta's paper "Some remarks on + Padé-approximations", Table 1, with corrections. The + corrections are to highest power coeff in numerator for + (ddeg,ndeg)=(4,3) and (5,4); use Eq (12) in the paper to verify + """ # all for T = 1 ref = [ # dendeg numdeg den num @@ -33,35 +33,40 @@ class TestPade(unittest.TestCase): ( 4, 3, [1,16,120,480,840], [-4,60,-360,840]), ( 5, 5, [1,30,420,3360,15120,30240], [-1,30,-420,3360,-15120,30240]), ( 5, 4, [1,25,300,2100,8400,15120,], [5,-120,1260,-6720,15120]), - ] + ] - def testRefs(self): + @pytest.mark.parametrize("dendeg, numdeg, refden, refnum", ref) + def testRefs(self, dendeg, numdeg, refden, refnum): "test reference cases for T=1" T = 1 - for dendeg, numdeg, refden, refnum in self.ref: - num, den = pade(T, dendeg, numdeg) - np.testing.assert_array_almost_equal_nulp(np.array(refden), den, nulp=2) - np.testing.assert_array_almost_equal_nulp(np.array(refnum), num, nulp=2) + num, den = pade(T, dendeg, numdeg) + np.testing.assert_array_almost_equal_nulp( + np.array(refden), den, nulp=2) + np.testing.assert_array_almost_equal_nulp( + np.array(refnum), num, nulp=2) - def testTvalues(self): + @pytest.mark.parametrize("dendeg, numdeg, baseden, basenum", ref) + @pytest.mark.parametrize("T", [1/53, 21.95]) + def testTvalues(self, T, dendeg, numdeg, baseden, basenum): "test reference cases for T!=1" - Ts = [1/53, 21.95] - for dendeg, numdeg, baseden, basenum in self.ref: - for T in Ts: - refden = T**np.arange(dendeg, -1, -1)*baseden - refnum = T**np.arange(numdeg, -1, -1)*basenum - refnum /= refden[0] - refden /= refden[0] - num, den = pade(T, dendeg, numdeg) - np.testing.assert_array_almost_equal_nulp(refden, den, nulp=2) - np.testing.assert_array_almost_equal_nulp(refnum, num, nulp=2) + refden = T**np.arange(dendeg, -1, -1)*baseden + refnum = T**np.arange(numdeg, -1, -1)*basenum + refnum /= refden[0] + refden /= refden[0] + num, den = pade(T, dendeg, numdeg) + np.testing.assert_array_almost_equal_nulp(refden, den, nulp=2) + np.testing.assert_array_almost_equal_nulp(refnum, num, nulp=2) def testErrors(self): "ValueError raised for invalid arguments" - self.assertRaises(ValueError,pade,-1,1) # T<0 - self.assertRaises(ValueError,pade,1,-1) # dendeg < 0 - self.assertRaises(ValueError,pade,1,2,-3) # numdeg < 0 - self.assertRaises(ValueError,pade,1,2,3) # numdeg > dendeg + with pytest.raises(ValueError): + pade(-1, 1) # T<0 + with pytest.raises(ValueError): + pade(1, -1) # dendeg < 0 + with pytest.raises(ValueError): + pade(1, 2, -3) # numdeg < 0 + with pytest.raises(ValueError): + pade(1, 2, 3) # numdeg > dendeg def testNumdeg(self): "numdeg argument follows docs" @@ -72,10 +77,10 @@ def testNumdeg(self): for numdeg in range(0,dendeg+1)] testneg = [pade(T,dendeg,numdeg) for numdeg in range(-dendeg,0)] - self.assertEqual(ref[:-1],testneg) - self.assertEqual(ref[-1], pade(T,dendeg,dendeg)) - self.assertEqual(ref[-1], pade(T,dendeg,None)) - self.assertEqual(ref[-1], pade(T,dendeg)) + assert ref[:-1] == testneg + assert ref[-1] == pade(T,dendeg,dendeg) + assert ref[-1] == pade(T,dendeg,None) + assert ref[-1] == pade(T,dendeg) def testT0(self): "T=0 always returns [1],[1]" @@ -85,8 +90,8 @@ def testT0(self): for dendeg in range(1, 6): for numdeg in range(0, dendeg+1): num, den = pade(T, dendeg, numdeg) - np.testing.assert_array_almost_equal_nulp(np.array(refnum), np.array(num)) - np.testing.assert_array_almost_equal_nulp(np.array(refden), np.array(den)) + np.testing.assert_array_almost_equal_nulp( + np.array(refnum), np.array(num)) + np.testing.assert_array_almost_equal_nulp( + np.array(refden), np.array(den)) -if __name__ == '__main__': - unittest.main() From 53520e9e3de120a8ad5aa34b00a88fbb415051f1 Mon Sep 17 00:00:00 2001 From: bnavigator Date: Thu, 20 Aug 2020 00:00:56 +0200 Subject: [PATCH 08/30] pytestify discrete_test --- control/tests/discrete_test.py | 655 ++++++++++++++++++--------------- 1 file changed, 357 insertions(+), 298 deletions(-) diff --git a/control/tests/discrete_test.py b/control/tests/discrete_test.py index 9c1928dab..ffdd1aeb4 100644 --- a/control/tests/discrete_test.py +++ b/control/tests/discrete_test.py @@ -1,351 +1,414 @@ -#!/usr/bin/env python -# -# discrete_test.py - test discrete time classes -# RMM, 9 Sep 2012 +"""discrete_test.py - test discrete time classes + +RMM, 9 Sep 2012 +""" -import unittest import numpy as np -from control import StateSpace, TransferFunction, feedback, step_response, \ - isdtime, timebase, isctime, sample_system, bode, impulse_response, \ - timebaseEqual, forced_response -from control import matlab +import pytest + +from control import (StateSpace, TransferFunction, bode, common_timebase, + evalfr, feedback, forced_response, impulse_response, + isctime, isdtime, rss, sample_system, step_response, + timebase) -class TestDiscrete(unittest.TestCase): - """Tests for the DiscreteStateSpace class.""" - def setUp(self): - """Set up a SISO and MIMO system to test operations on.""" +class TestDiscrete: + """Tests for the system classes with discrete timebase.""" + @pytest.fixture + def tsys(self): + """Create some systems for testing""" + class Tsys: + pass + T = Tsys() # Single input, single output continuous and discrete time systems - sys = matlab.rss(3, 1, 1) - self.siso_ss1 = StateSpace(sys.A, sys.B, sys.C, sys.D) - self.siso_ss1c = StateSpace(sys.A, sys.B, sys.C, sys.D, 0.0) - self.siso_ss1d = StateSpace(sys.A, sys.B, sys.C, sys.D, 0.1) - self.siso_ss2d = StateSpace(sys.A, sys.B, sys.C, sys.D, 0.2) - self.siso_ss3d = StateSpace(sys.A, sys.B, sys.C, sys.D, True) + sys = rss(3, 1, 1) + T.siso_ss1 = StateSpace(sys.A, sys.B, sys.C, sys.D, None) + T.siso_ss1c = StateSpace(sys.A, sys.B, sys.C, sys.D, 0.0) + T.siso_ss1d = StateSpace(sys.A, sys.B, sys.C, sys.D, 0.1) + T.siso_ss2d = StateSpace(sys.A, sys.B, sys.C, sys.D, 0.2) + T.siso_ss3d = StateSpace(sys.A, sys.B, sys.C, sys.D, True) # Two input, two output continuous time system A = [[-3., 4., 2.], [-1., -3., 0.], [2., 5., 3.]] B = [[1., 4.], [-3., -3.], [-2., 1.]] C = [[4., 2., -3.], [1., 4., 3.]] D = [[-2., 4.], [0., 1.]] - self.mimo_ss1 = StateSpace(A, B, C, D) - self.mimo_ss1c = StateSpace(A, B, C, D, 0) + T.mimo_ss1 = StateSpace(A, B, C, D, None) + T.mimo_ss1c = StateSpace(A, B, C, D, 0) # Two input, two output discrete time system - self.mimo_ss1d = StateSpace(A, B, C, D, 0.1) + T.mimo_ss1d = StateSpace(A, B, C, D, 0.1) # Same system, but with a different sampling time - self.mimo_ss2d = StateSpace(A, B, C, D, 0.2) + T.mimo_ss2d = StateSpace(A, B, C, D, 0.2) # Single input, single output continuus and discrete transfer function - self.siso_tf1 = TransferFunction([1, 1], [1, 2, 1]) - self.siso_tf1c = TransferFunction([1, 1], [1, 2, 1], 0) - self.siso_tf1d = TransferFunction([1, 1], [1, 2, 1], 0.1) - self.siso_tf2d = TransferFunction([1, 1], [1, 2, 1], 0.2) - self.siso_tf3d = TransferFunction([1, 1], [1, 2, 1], True) - - def testTimebaseEqual(self): - self.assertEqual(timebaseEqual(self.siso_ss1, self.siso_tf1), True) - self.assertEqual(timebaseEqual(self.siso_ss1, self.siso_ss1c), True) - self.assertEqual(timebaseEqual(self.siso_ss1, self.siso_ss1d), True) - self.assertEqual(timebaseEqual(self.siso_ss1d, self.siso_ss1c), False) - self.assertEqual(timebaseEqual(self.siso_ss1d, self.siso_ss2d), False) - self.assertEqual(timebaseEqual(self.siso_ss1d, self.siso_ss3d), False) - - def testSystemInitialization(self): + T.siso_tf1 = TransferFunction([1, 1], [1, 2, 1], None) + T.siso_tf1c = TransferFunction([1, 1], [1, 2, 1], 0) + T.siso_tf1d = TransferFunction([1, 1], [1, 2, 1], 0.1) + T.siso_tf2d = TransferFunction([1, 1], [1, 2, 1], 0.2) + T.siso_tf3d = TransferFunction([1, 1], [1, 2, 1], True) + + return T + + def testCompatibleTimebases(self, tsys): + """test that compatible timebases don't throw errors and vice versa""" + common_timebase(tsys.siso_ss1.dt, tsys.siso_tf1.dt) + common_timebase(tsys.siso_ss1.dt, tsys.siso_ss1c.dt) + common_timebase(tsys.siso_ss1d.dt, tsys.siso_ss1.dt) + common_timebase(tsys.siso_ss1.dt, tsys.siso_ss1d.dt) + common_timebase(tsys.siso_ss1.dt, tsys.siso_ss1d.dt) + common_timebase(tsys.siso_ss1d.dt, tsys.siso_ss3d.dt) + common_timebase(tsys.siso_ss3d.dt, tsys.siso_ss1d.dt) + with pytest.raises(ValueError): + # cont + discrete + common_timebase(tsys.siso_ss1d.dt, tsys.siso_ss1c.dt) + with pytest.raises(ValueError): + # incompatible discrete + common_timebase(tsys.siso_ss1d.dt, tsys.siso_ss2d.dt) + + def testSystemInitialization(self, tsys): # Check to make sure systems are discrete time with proper variables - self.assertEqual(self.siso_ss1.dt, None) - self.assertEqual(self.siso_ss1c.dt, 0) - self.assertEqual(self.siso_ss1d.dt, 0.1) - self.assertEqual(self.siso_ss2d.dt, 0.2) - self.assertEqual(self.siso_ss3d.dt, True) - self.assertEqual(self.mimo_ss1c.dt, 0) - self.assertEqual(self.mimo_ss1d.dt, 0.1) - self.assertEqual(self.mimo_ss2d.dt, 0.2) - self.assertEqual(self.siso_tf1.dt, None) - self.assertEqual(self.siso_tf1c.dt, 0) - self.assertEqual(self.siso_tf1d.dt, 0.1) - self.assertEqual(self.siso_tf2d.dt, 0.2) - self.assertEqual(self.siso_tf3d.dt, True) - - def testCopyConstructor(self): - for sys in (self.siso_ss1, self.siso_ss1c, self.siso_ss1d): - newsys = StateSpace(sys); - self.assertEqual(sys.dt, newsys.dt) - for sys in (self.siso_tf1, self.siso_tf1c, self.siso_tf1d): - newsys = TransferFunction(sys); - self.assertEqual(sys.dt, newsys.dt) - - def test_timebase(self): - self.assertEqual(timebase(1), None); - self.assertRaises(ValueError, timebase, [1, 2]) - self.assertEqual(timebase(self.siso_ss1, strict=False), None); - self.assertEqual(timebase(self.siso_ss1, strict=True), None); - self.assertEqual(timebase(self.siso_ss1c), 0); - self.assertEqual(timebase(self.siso_ss1d), 0.1); - self.assertEqual(timebase(self.siso_ss2d), 0.2); - self.assertEqual(timebase(self.siso_ss3d), True); - self.assertEqual(timebase(self.siso_ss3d, strict=False), 1); - self.assertEqual(timebase(self.siso_tf1, strict=False), None); - self.assertEqual(timebase(self.siso_tf1, strict=True), None); - self.assertEqual(timebase(self.siso_tf1c), 0); - self.assertEqual(timebase(self.siso_tf1d), 0.1); - self.assertEqual(timebase(self.siso_tf2d), 0.2); - self.assertEqual(timebase(self.siso_tf3d), True); - self.assertEqual(timebase(self.siso_tf3d, strict=False), 1); - - def test_timebase_conversions(self): + assert tsys.siso_ss1.dt is None + assert tsys.siso_ss1c.dt == 0 + assert tsys.siso_ss1d.dt == 0.1 + assert tsys.siso_ss2d.dt == 0.2 + assert tsys.siso_ss3d.dt is True + assert tsys.mimo_ss1c.dt == 0 + assert tsys.mimo_ss1d.dt == 0.1 + assert tsys.mimo_ss2d.dt == 0.2 + assert tsys.siso_tf1.dt is None + assert tsys.siso_tf1c.dt == 0 + assert tsys.siso_tf1d.dt == 0.1 + assert tsys.siso_tf2d.dt == 0.2 + assert tsys.siso_tf3d.dt is True + + # keyword argument check + # dynamic systems + assert TransferFunction(1, [1, 1], dt=0.1).dt == 0.1 + assert TransferFunction(1, [1, 1], 0.1).dt == 0.1 + assert StateSpace(1,1,1,1, dt=0.1).dt == 0.1 + assert StateSpace(1,1,1,1, 0.1).dt == 0.1 + # static gain system, dt argument should still override default dt + assert TransferFunction(1, [1,], dt=0.1).dt == 0.1 + assert TransferFunction(1, [1,], 0.1).dt == 0.1 + assert StateSpace(0,0,1,1, dt=0.1).dt == 0.1 + assert StateSpace(0,0,1,1, 0.1).dt == 0.1 + + def testCopyConstructor(self, tsys): + for sys in (tsys.siso_ss1, tsys.siso_ss1c, tsys.siso_ss1d): + newsys = StateSpace(sys) + assert sys.dt == newsys.dt + for sys in (tsys.siso_tf1, tsys.siso_tf1c, tsys.siso_tf1d): + newsys = TransferFunction(sys) + assert sys.dt == newsys.dt + + def test_timebase(self, tsys): + assert timebase(1) is None + with pytest.raises(ValueError): + timebase([1, 2]) + assert timebase(tsys.siso_ss1, strict=False) is None + assert timebase(tsys.siso_ss1, strict=True) is None + assert timebase(tsys.siso_ss1c) == 0 + assert timebase(tsys.siso_ss1d) == 0.1 + assert timebase(tsys.siso_ss2d) == 0.2 + assert timebase(tsys.siso_ss3d) + assert timebase(tsys.siso_ss3d, strict=False) == 1 + assert timebase(tsys.siso_tf1, strict=False) is None + assert timebase(tsys.siso_tf1, strict=True) is None + assert timebase(tsys.siso_tf1c) == 0 + assert timebase(tsys.siso_tf1d) == 0.1 + assert timebase(tsys.siso_tf2d) == 0.2 + assert timebase(tsys.siso_tf3d) + assert timebase(tsys.siso_tf3d, strict=False) == 1 + + def test_timebase_conversions(self, tsys): '''Check to make sure timebases transfer properly''' - tf1 = TransferFunction([1,1],[1,2,3]) # unspecified - tf2 = TransferFunction([1,1],[1,2,3], 0) # cont time - tf3 = TransferFunction([1,1],[1,2,3], True) # dtime, unspec - tf4 = TransferFunction([1,1],[1,2,3], 1) # dtime, dt=1 + tf1 = TransferFunction([1, 1], [1, 2, 3], None) # unspecified + tf2 = TransferFunction([1, 1], [1, 2, 3], 0) # cont time + tf3 = TransferFunction([1, 1], [1, 2, 3], True) # dtime, unspec + tf4 = TransferFunction([1, 1], [1, 2, 3], .1) # dtime, dt=.1 # Make sure unspecified timebase is converted correctly - self.assertEqual(timebase(tf1*tf1), timebase(tf1)) - self.assertEqual(timebase(tf1*tf2), timebase(tf2)) - self.assertEqual(timebase(tf1*tf3), timebase(tf3)) - self.assertEqual(timebase(tf1*tf4), timebase(tf4)) - self.assertEqual(timebase(tf2*tf1), timebase(tf2)) - self.assertEqual(timebase(tf3*tf1), timebase(tf3)) - self.assertEqual(timebase(tf4*tf1), timebase(tf4)) - self.assertEqual(timebase(tf1+tf1), timebase(tf1)) - self.assertEqual(timebase(tf1+tf2), timebase(tf2)) - self.assertEqual(timebase(tf1+tf3), timebase(tf3)) - self.assertEqual(timebase(tf1+tf4), timebase(tf4)) - self.assertEqual(timebase(feedback(tf1, tf1)), timebase(tf1)) - self.assertEqual(timebase(feedback(tf1, tf2)), timebase(tf2)) - self.assertEqual(timebase(feedback(tf1, tf3)), timebase(tf3)) - self.assertEqual(timebase(feedback(tf1, tf4)), timebase(tf4)) + assert timebase(tf1*tf1) == timebase(tf1) + assert timebase(tf1*tf2) == timebase(tf2) + assert timebase(tf1*tf3) == timebase(tf3) + assert timebase(tf1*tf4) == timebase(tf4) + assert timebase(tf3*tf4) == timebase(tf4) + assert timebase(tf2*tf1) == timebase(tf2) + assert timebase(tf3*tf1) == timebase(tf3) + assert timebase(tf4*tf1) == timebase(tf4) + assert timebase(tf1+tf1) == timebase(tf1) + assert timebase(tf1+tf2) == timebase(tf2) + assert timebase(tf1+tf3) == timebase(tf3) + assert timebase(tf1+tf4) == timebase(tf4) + assert timebase(feedback(tf1, tf1)) == timebase(tf1) + assert timebase(feedback(tf1, tf2)) == timebase(tf2) + assert timebase(feedback(tf1, tf3)) == timebase(tf3) + assert timebase(feedback(tf1, tf4)) == timebase(tf4) # Make sure discrete time without sampling is converted correctly - self.assertEqual(timebase(tf3*tf3), timebase(tf3)) - self.assertEqual(timebase(tf3*tf4), timebase(tf4)) - self.assertEqual(timebase(tf3+tf3), timebase(tf3)) - self.assertEqual(timebase(tf3+tf3), timebase(tf4)) - self.assertEqual(timebase(feedback(tf3, tf3)), timebase(tf3)) - self.assertEqual(timebase(feedback(tf3, tf4)), timebase(tf4)) + assert timebase(tf3*tf3) == timebase(tf3) + assert timebase(tf3*tf4) == timebase(tf4) + assert timebase(tf3+tf3) == timebase(tf3) + assert timebase(tf3+tf4) == timebase(tf4) + assert timebase(feedback(tf3, tf3)) == timebase(tf3) + assert timebase(feedback(tf3, tf4)) == timebase(tf4) # Make sure all other combinations are errors - try: - tf2*tf3 # Error; incompatible timebases - raise ValueError("incompatible operation allowed") - except ValueError: - pass - try: - tf2*tf4 # Error; incompatible timebases - raise ValueError("incompatible operation allowed") - except ValueError: - pass - try: - tf2+tf3 # Error; incompatible timebases - raise ValueError("incompatible operation allowed") - except ValueError: - pass - try: - tf2+tf4 # Error; incompatible timebases - raise ValueError("incompatible operation allowed") - except ValueError: - pass - try: - feedback(tf2, tf3) # Error; incompatible timebases - raise ValueError("incompatible operation allowed") - except ValueError: - pass - try: - feedback(tf2, tf4) # Error; incompatible timebases - raise ValueError("incompatible operation allowed") - except ValueError: - pass - - def testisdtime(self): + with pytest.raises(ValueError, match="incompatible timebases"): + tf2 * tf3 + with pytest.raises(ValueError, match="incompatible timebases"): + tf3 * tf2 + with pytest.raises(ValueError, match="incompatible timebases"): + tf2 * tf4 + with pytest.raises(ValueError, match="incompatible timebases"): + tf4 * tf2 + with pytest.raises(ValueError, match="incompatible timebases"): + tf2 + tf3 + with pytest.raises(ValueError, match="incompatible timebases"): + tf3 + tf2 + with pytest.raises(ValueError, match="incompatible timebases"): + tf2 + tf4 + with pytest.raises(ValueError, match="incompatible timebases"): + tf4 + tf2 + with pytest.raises(ValueError, match="incompatible timebases"): + feedback(tf2, tf3) + with pytest.raises(ValueError, match="incompatible timebases"): + feedback(tf3, tf2) + with pytest.raises(ValueError, match="incompatible timebases"): + feedback(tf2, tf4) + with pytest.raises(ValueError, match="incompatible timebases"): + feedback(tf4, tf2) + + def testisdtime(self, tsys): # Constant - self.assertEqual(isdtime(1), True); - self.assertEqual(isdtime(1, strict=True), False); + assert isdtime(1) + assert not isdtime(1, strict=True) # State space - self.assertEqual(isdtime(self.siso_ss1), True); - self.assertEqual(isdtime(self.siso_ss1, strict=True), False); - self.assertEqual(isdtime(self.siso_ss1c), False); - self.assertEqual(isdtime(self.siso_ss1c, strict=True), False); - self.assertEqual(isdtime(self.siso_ss1d), True); - self.assertEqual(isdtime(self.siso_ss1d, strict=True), True); - self.assertEqual(isdtime(self.siso_ss3d, strict=True), True); + assert isdtime(tsys.siso_ss1) + assert not isdtime(tsys.siso_ss1, strict=True) + assert not isdtime(tsys.siso_ss1c) + assert not isdtime(tsys.siso_ss1c, strict=True) + assert isdtime(tsys.siso_ss1d) + assert isdtime(tsys.siso_ss1d, strict=True) + assert isdtime(tsys.siso_ss3d, strict=True) # Transfer function - self.assertEqual(isdtime(self.siso_tf1), True); - self.assertEqual(isdtime(self.siso_tf1, strict=True), False); - self.assertEqual(isdtime(self.siso_tf1c), False); - self.assertEqual(isdtime(self.siso_tf1c, strict=True), False); - self.assertEqual(isdtime(self.siso_tf1d), True); - self.assertEqual(isdtime(self.siso_tf1d, strict=True), True); - self.assertEqual(isdtime(self.siso_tf3d, strict=True), True); - - def testisctime(self): + assert isdtime(tsys.siso_tf1) + assert not isdtime(tsys.siso_tf1, strict=True) + assert not isdtime(tsys.siso_tf1c) + assert not isdtime(tsys.siso_tf1c, strict=True) + assert isdtime(tsys.siso_tf1d) + assert isdtime(tsys.siso_tf1d, strict=True) + assert isdtime(tsys.siso_tf3d, strict=True) + + def testisctime(self, tsys): # Constant - self.assertEqual(isctime(1), True); - self.assertEqual(isctime(1, strict=True), False); + assert isctime(1) + assert not isctime(1, strict=True) # State Space - self.assertEqual(isctime(self.siso_ss1), True); - self.assertEqual(isctime(self.siso_ss1, strict=True), False); - self.assertEqual(isctime(self.siso_ss1c), True); - self.assertEqual(isctime(self.siso_ss1c, strict=True), True); - self.assertEqual(isctime(self.siso_ss1d), False); - self.assertEqual(isctime(self.siso_ss1d, strict=True), False); - self.assertEqual(isctime(self.siso_ss3d, strict=True), False); + assert isctime(tsys.siso_ss1) + assert not isctime(tsys.siso_ss1, strict=True) + assert isctime(tsys.siso_ss1c) + assert isctime(tsys.siso_ss1c, strict=True) + assert not isctime(tsys.siso_ss1d) + assert not isctime(tsys.siso_ss1d, strict=True) + assert not isctime(tsys.siso_ss3d, strict=True) # Transfer Function - self.assertEqual(isctime(self.siso_tf1), True); - self.assertEqual(isctime(self.siso_tf1, strict=True), False); - self.assertEqual(isctime(self.siso_tf1c), True); - self.assertEqual(isctime(self.siso_tf1c, strict=True), True); - self.assertEqual(isctime(self.siso_tf1d), False); - self.assertEqual(isctime(self.siso_tf1d, strict=True), False); - self.assertEqual(isctime(self.siso_tf3d, strict=True), False); - - def testAddition(self): + assert isctime(tsys.siso_tf1) + assert not isctime(tsys.siso_tf1, strict=True) + assert isctime(tsys.siso_tf1c) + assert isctime(tsys.siso_tf1c, strict=True) + assert not isctime(tsys.siso_tf1d) + assert not isctime(tsys.siso_tf1d, strict=True) + assert not isctime(tsys.siso_tf3d, strict=True) + + def testAddition(self, tsys): # State space addition - sys = self.siso_ss1 + self.siso_ss1d - sys = self.siso_ss1 + self.siso_ss1c - sys = self.siso_ss1c + self.siso_ss1 - sys = self.siso_ss1d + self.siso_ss1 - sys = self.siso_ss1c + self.siso_ss1c - sys = self.siso_ss1d + self.siso_ss1d - sys = self.siso_ss3d + self.siso_ss3d - self.assertRaises(ValueError, StateSpace.__add__, self.mimo_ss1c, - self.mimo_ss1d) - self.assertRaises(ValueError, StateSpace.__add__, self.mimo_ss1d, - self.mimo_ss2d) - self.assertRaises(ValueError, StateSpace.__add__, self.siso_ss1d, - self.siso_ss3d) + sys = tsys.siso_ss1 + tsys.siso_ss1d + sys = tsys.siso_ss1 + tsys.siso_ss1c + sys = tsys.siso_ss1c + tsys.siso_ss1 + sys = tsys.siso_ss1d + tsys.siso_ss1 + sys = tsys.siso_ss1c + tsys.siso_ss1c + sys = tsys.siso_ss1d + tsys.siso_ss1d + sys = tsys.siso_ss3d + tsys.siso_ss3d + sys = tsys.siso_ss1d + tsys.siso_ss3d + + with pytest.raises(ValueError): + StateSpace.__add__(tsys.mimo_ss1c, tsys.mimo_ss1d) + with pytest.raises(ValueError): + StateSpace.__add__(tsys.mimo_ss1d, tsys.mimo_ss2d) + with pytest.raises(ValueError): + StateSpace.__add__(tsys.siso_ss1d, tsys.siso_ss3d) # Transfer function addition - sys = self.siso_tf1 + self.siso_tf1d - sys = self.siso_tf1 + self.siso_tf1c - sys = self.siso_tf1c + self.siso_tf1 - sys = self.siso_tf1d + self.siso_tf1 - sys = self.siso_tf1c + self.siso_tf1c - sys = self.siso_tf1d + self.siso_tf1d - sys = self.siso_tf2d + self.siso_tf2d - self.assertRaises(ValueError, TransferFunction.__add__, self.siso_tf1c, - self.siso_tf1d) - self.assertRaises(ValueError, TransferFunction.__add__, self.siso_tf1d, - self.siso_tf2d) - self.assertRaises(ValueError, TransferFunction.__add__, self.siso_tf1d, - self.siso_tf3d) + sys = tsys.siso_tf1 + tsys.siso_tf1d + sys = tsys.siso_tf1 + tsys.siso_tf1c + sys = tsys.siso_tf1c + tsys.siso_tf1 + sys = tsys.siso_tf1d + tsys.siso_tf1 + sys = tsys.siso_tf1c + tsys.siso_tf1c + sys = tsys.siso_tf1d + tsys.siso_tf1d + sys = tsys.siso_tf2d + tsys.siso_tf2d + sys = tsys.siso_tf1d + tsys.siso_tf3d + + with pytest.raises(ValueError): + TransferFunction.__add__(tsys.siso_tf1c, tsys.siso_tf1d) + with pytest.raises(ValueError): + TransferFunction.__add__(tsys.siso_tf1d, tsys.siso_tf2d) + with pytest.raises(ValueError): + TransferFunction.__add__(tsys.siso_tf1d, tsys.siso_tf3d) # State space + transfer function - sys = self.siso_ss1c + self.siso_tf1c - sys = self.siso_tf1c + self.siso_ss1c - sys = self.siso_ss1d + self.siso_tf1d - sys = self.siso_tf1d + self.siso_ss1d - self.assertRaises(ValueError, TransferFunction.__add__, self.siso_tf1c, - self.siso_ss1d) - - def testMultiplication(self): - # State space addition - sys = self.siso_ss1 * self.siso_ss1d - sys = self.siso_ss1 * self.siso_ss1c - sys = self.siso_ss1c * self.siso_ss1 - sys = self.siso_ss1d * self.siso_ss1 - sys = self.siso_ss1c * self.siso_ss1c - sys = self.siso_ss1d * self.siso_ss1d - self.assertRaises(ValueError, StateSpace.__mul__, self.mimo_ss1c, - self.mimo_ss1d) - self.assertRaises(ValueError, StateSpace.__mul__, self.mimo_ss1d, - self.mimo_ss2d) - self.assertRaises(ValueError, StateSpace.__mul__, self.siso_ss1d, - self.siso_ss3d) - - # Transfer function addition - sys = self.siso_tf1 * self.siso_tf1d - sys = self.siso_tf1 * self.siso_tf1c - sys = self.siso_tf1c * self.siso_tf1 - sys = self.siso_tf1d * self.siso_tf1 - sys = self.siso_tf1c * self.siso_tf1c - sys = self.siso_tf1d * self.siso_tf1d - self.assertRaises(ValueError, TransferFunction.__mul__, self.siso_tf1c, - self.siso_tf1d) - self.assertRaises(ValueError, TransferFunction.__mul__, self.siso_tf1d, - self.siso_tf2d) - self.assertRaises(ValueError, TransferFunction.__mul__, self.siso_tf1d, - self.siso_tf3d) + sys = tsys.siso_ss1c + tsys.siso_tf1c + sys = tsys.siso_tf1c + tsys.siso_ss1c + sys = tsys.siso_ss1d + tsys.siso_tf1d + sys = tsys.siso_tf1d + tsys.siso_ss1d + with pytest.raises(ValueError): + TransferFunction.__add__(tsys.siso_tf1c, tsys.siso_ss1d) + + def testMultiplication(self, tsys): + # State space multiplication + sys = tsys.siso_ss1 * tsys.siso_ss1d + sys = tsys.siso_ss1 * tsys.siso_ss1c + sys = tsys.siso_ss1c * tsys.siso_ss1 + sys = tsys.siso_ss1d * tsys.siso_ss1 + sys = tsys.siso_ss1c * tsys.siso_ss1c + sys = tsys.siso_ss1d * tsys.siso_ss1d + sys = tsys.siso_ss1d * tsys.siso_ss3d + + with pytest.raises(ValueError): + StateSpace.__mul__(tsys.mimo_ss1c, tsys.mimo_ss1d) + with pytest.raises(ValueError): + StateSpace.__mul__(tsys.mimo_ss1d, tsys.mimo_ss2d) + with pytest.raises(ValueError): + StateSpace.__mul__(tsys.siso_ss1d, tsys.siso_ss3d) + + # Transfer function multiplication + sys = tsys.siso_tf1 * tsys.siso_tf1d + sys = tsys.siso_tf1 * tsys.siso_tf1c + sys = tsys.siso_tf1c * tsys.siso_tf1 + sys = tsys.siso_tf1d * tsys.siso_tf1 + sys = tsys.siso_tf1c * tsys.siso_tf1c + sys = tsys.siso_tf1d * tsys.siso_tf1d + sys = tsys.siso_tf1d * tsys.siso_tf3d + + with pytest.raises(ValueError): + TransferFunction.__mul__(tsys.siso_tf1c, tsys.siso_tf1d) + with pytest.raises(ValueError): + TransferFunction.__mul__(tsys.siso_tf1d, tsys.siso_tf2d) + with pytest.raises(ValueError): + TransferFunction.__mul__(tsys.siso_tf1d, tsys.siso_tf3d) # State space * transfer function - sys = self.siso_ss1c * self.siso_tf1c - sys = self.siso_tf1c * self.siso_ss1c - sys = self.siso_ss1d * self.siso_tf1d - sys = self.siso_tf1d * self.siso_ss1d - self.assertRaises(ValueError, TransferFunction.__mul__, self.siso_tf1c, - self.siso_ss1d) - - - def testFeedback(self): - # State space addition - sys = feedback(self.siso_ss1, self.siso_ss1d) - sys = feedback(self.siso_ss1, self.siso_ss1c) - sys = feedback(self.siso_ss1c, self.siso_ss1) - sys = feedback(self.siso_ss1d, self.siso_ss1) - sys = feedback(self.siso_ss1c, self.siso_ss1c) - sys = feedback(self.siso_ss1d, self.siso_ss1d) - self.assertRaises(ValueError, feedback, self.mimo_ss1c, self.mimo_ss1d) - self.assertRaises(ValueError, feedback, self.mimo_ss1d, self.mimo_ss2d) - self.assertRaises(ValueError, feedback, self.siso_ss1d, self.siso_ss3d) - - # Transfer function addition - sys = feedback(self.siso_tf1, self.siso_tf1d) - sys = feedback(self.siso_tf1, self.siso_tf1c) - sys = feedback(self.siso_tf1c, self.siso_tf1) - sys = feedback(self.siso_tf1d, self.siso_tf1) - sys = feedback(self.siso_tf1c, self.siso_tf1c) - sys = feedback(self.siso_tf1d, self.siso_tf1d) - self.assertRaises(ValueError, feedback, self.siso_tf1c, self.siso_tf1d) - self.assertRaises(ValueError, feedback, self.siso_tf1d, self.siso_tf2d) - self.assertRaises(ValueError, feedback, self.siso_tf1d, self.siso_tf3d) + sys = tsys.siso_ss1c * tsys.siso_tf1c + sys = tsys.siso_tf1c * tsys.siso_ss1c + sys = tsys.siso_ss1d * tsys.siso_tf1d + sys = tsys.siso_tf1d * tsys.siso_ss1d + with pytest.raises(ValueError): + TransferFunction.__mul__(tsys.siso_tf1c, + tsys.siso_ss1d) + + + def testFeedback(self, tsys): + # State space feedback + sys = feedback(tsys.siso_ss1, tsys.siso_ss1d) + sys = feedback(tsys.siso_ss1, tsys.siso_ss1c) + sys = feedback(tsys.siso_ss1c, tsys.siso_ss1) + sys = feedback(tsys.siso_ss1d, tsys.siso_ss1) + sys = feedback(tsys.siso_ss1c, tsys.siso_ss1c) + sys = feedback(tsys.siso_ss1d, tsys.siso_ss1d) + sys = feedback(tsys.siso_ss1d, tsys.siso_ss3d) + + with pytest.raises(ValueError): + feedback(tsys.mimo_ss1c, tsys.mimo_ss1d) + with pytest.raises(ValueError): + feedback(tsys.mimo_ss1d, tsys.mimo_ss2d) + with pytest.raises(ValueError): + feedback(tsys.siso_ss1d, tsys.siso_ss3d) + + # Transfer function feedback + sys = feedback(tsys.siso_tf1, tsys.siso_tf1d) + sys = feedback(tsys.siso_tf1, tsys.siso_tf1c) + sys = feedback(tsys.siso_tf1c, tsys.siso_tf1) + sys = feedback(tsys.siso_tf1d, tsys.siso_tf1) + sys = feedback(tsys.siso_tf1c, tsys.siso_tf1c) + sys = feedback(tsys.siso_tf1d, tsys.siso_tf1d) + sys = feedback(tsys.siso_tf1d, tsys.siso_tf3d) + + with pytest.raises(ValueError): + feedback(tsys.siso_tf1c, tsys.siso_tf1d) + with pytest.raises(ValueError): + feedback(tsys.siso_tf1d, tsys.siso_tf2d) + with pytest.raises(ValueError): + feedback(tsys.siso_tf1d, tsys.siso_tf3d) # State space, transfer function - sys = feedback(self.siso_ss1c, self.siso_tf1c) - sys = feedback(self.siso_tf1c, self.siso_ss1c) - sys = feedback(self.siso_ss1d, self.siso_tf1d) - sys = feedback(self.siso_tf1d, self.siso_ss1d) - self.assertRaises(ValueError, feedback, self.siso_tf1c, self.siso_ss1d) - - def testSimulation(self): + sys = feedback(tsys.siso_ss1c, tsys.siso_tf1c) + sys = feedback(tsys.siso_tf1c, tsys.siso_ss1c) + sys = feedback(tsys.siso_ss1d, tsys.siso_tf1d) + sys = feedback(tsys.siso_tf1d, tsys.siso_ss1d) + with pytest.raises(ValueError): + feedback(tsys.siso_tf1c, tsys.siso_ss1d) + + def testSimulation(self, tsys): T = range(100) U = np.sin(T) # For now, just check calling syntax # TODO: add checks on output of simulations - tout, yout = step_response(self.siso_ss1d) - tout, yout = step_response(self.siso_ss1d, T) - tout, yout = impulse_response(self.siso_ss1d, T) - tout, yout = impulse_response(self.siso_ss1d) - tout, yout, xout = forced_response(self.siso_ss1d, T, U, 0) - tout, yout, xout = forced_response(self.siso_ss2d, T, U, 0) - tout, yout, xout = forced_response(self.siso_ss3d, T, U, 0) - - def test_sample_system(self): + tout, yout = step_response(tsys.siso_ss1d) + tout, yout = step_response(tsys.siso_ss1d, T) + tout, yout = impulse_response(tsys.siso_ss1d) + tout, yout = impulse_response(tsys.siso_ss1d, T) + tout, yout, xout = forced_response(tsys.siso_ss1d, T, U, 0) + tout, yout, xout = forced_response(tsys.siso_ss2d, T, U, 0) + tout, yout, xout = forced_response(tsys.siso_ss3d, T, U, 0) + + def test_sample_system(self, tsys): # Make sure we can convert various types of systems - for sysc in (self.siso_tf1, self.siso_tf1c, - self.siso_ss1, self.siso_ss1c, - self.mimo_ss1, self.mimo_ss1c): + for sysc in (tsys.siso_tf1, tsys.siso_tf1c, + tsys.siso_ss1, tsys.siso_ss1c, + tsys.mimo_ss1, tsys.mimo_ss1c): for method in ("zoh", "bilinear", "euler", "backward_diff"): sysd = sample_system(sysc, 1, method=method) - self.assertEqual(sysd.dt, 1) + assert sysd.dt == 1 # Check "matched", defined only for SISO transfer functions - for sysc in (self.siso_tf1, self.siso_tf1c): + for sysc in (tsys.siso_tf1, tsys.siso_tf1c): sysd = sample_system(sysc, 1, method="matched") - self.assertEqual(sysd.dt, 1) - + assert sysd.dt == 1 + + @pytest.mark.parametrize("plantname", + ["siso_ss1c", + "siso_tf1c"]) + def test_sample_system_prewarp(self, tsys, plantname): + """bilinear approximation with prewarping test""" + wwarp = 50 + Ts = 0.025 + # test state space version + plant = getattr(tsys, plantname) + plant_d_warped = plant.sample(Ts, 'bilinear', prewarp_frequency=wwarp) + plant_fr = evalfr(plant, wwarp * 1j) + dt = plant_d_warped.dt + plant_d_fr = evalfr(plant_d_warped, np.exp(wwarp * 1.j * dt)) + np.testing.assert_array_almost_equal(plant_fr, plant_d_fr) + + def test_sample_system_errors(self, tsys): # Check errors - self.assertRaises(ValueError, sample_system, self.siso_ss1d, 1) - self.assertRaises(ValueError, sample_system, self.siso_tf1d, 1) - self.assertRaises(ValueError, sample_system, self.siso_ss1, 1, 'unknown') + with pytest.raises(ValueError): + sample_system(tsys.siso_ss1d, 1) + with pytest.raises(ValueError): + sample_system(tsys.siso_tf1d, 1) + with pytest.raises(ValueError): + sample_system(tsys.siso_ss1, 1, 'unknown') + - def test_sample_ss(self): + def test_sample_ss(self, tsys): # double integrators, two different ways sys1 = StateSpace([[0.,1.],[0.,0.]], [[0.],[1.]], [[1.,0.]], 0.) sys2 = StateSpace([[0.,0.],[1.,0.]], [[1.],[0.]], [[0.,1.]], 0.) @@ -359,22 +422,22 @@ def test_sample_ss(self): np.testing.assert_array_almost_equal(sysd.B, Bd) np.testing.assert_array_almost_equal(sysd.C, sys.C) np.testing.assert_array_almost_equal(sysd.D, sys.D) - self.assertEqual(sysd.dt, h) + assert sysd.dt == h - def test_sample_tf(self): + def test_sample_tf(self, tsys): # double integrator sys = TransferFunction(1, [1,0,0]) for h in (0.1, 0.5, 1, 2): numd_expected = 0.5 * h**2 * np.array([1.,1.]) dend_expected = np.array([1.,-2.,1.]) sysd = sample_system(sys, h, method='zoh') - self.assertEqual(sysd.dt, h) + assert sysd.dt == h numd = sysd.num[0][0] dend = sysd.den[0][0] np.testing.assert_array_almost_equal(numd, numd_expected) np.testing.assert_array_almost_equal(dend, dend_expected) - def test_discrete_bode(self): + def test_discrete_bode(self, tsys): # Create a simple discrete time system and check the calculation sys = TransferFunction([1], [1, 0.5], 1) omega = [1, 2, 3] @@ -383,7 +446,3 @@ def test_discrete_bode(self): np.testing.assert_array_almost_equal(omega, omega_out) np.testing.assert_array_almost_equal(mag_out, np.absolute(H_z)) np.testing.assert_array_almost_equal(phase_out, np.angle(H_z)) - - -if __name__ == "__main__": - unittest.main() From 659aa2dbfc08eaf2a6b76bb85b3e2a9b37e34818 Mon Sep 17 00:00:00 2001 From: bnavigator Date: Tue, 29 Dec 2020 22:59:24 +0100 Subject: [PATCH 09/30] pytestify flatsys_test --- control/tests/flatsys_test.py | 78 ++++++++++++++++------------------- 1 file changed, 35 insertions(+), 43 deletions(-) diff --git a/control/tests/flatsys_test.py b/control/tests/flatsys_test.py index 0c1d0c92c..1281c519a 100644 --- a/control/tests/flatsys_test.py +++ b/control/tests/flatsys_test.py @@ -1,57 +1,55 @@ -#!/usr/bin/env python -# -# flatsys_test.py - test flat system module -# RMM, 29 Jun 2019 -# -# This test suite checks to make sure that the basic functions supporting -# differential flat systetms are functioning. It doesn't do exhaustive -# testing of operations on flat systems. Separate unit tests should be -# created for that purpose. - -import unittest +"""flatsys_test.py - test flat system module + +RMM, 29 Jun 2019 + +This test suite checks to make sure that the basic functions supporting +differential flat systetms are functioning. It doesn't do exhaustive +testing of operations on flat systems. Separate unit tests should be +created for that purpose. +""" + +from distutils.version import StrictVersion + import numpy as np +import pytest import scipy as sp + import control as ct import control.flatsys as fs -from distutils.version import StrictVersion -class TestFlatSys(unittest.TestCase): - def setUp(self): - ct.use_numpy_matrix(False) +class TestFlatSys: + """Test differential flat systems""" - def test_double_integrator(self): + @pytest.mark.parametrize( + "xf, uf, Tf", + [([1, 0], [0], 2), + ([0, 1], [0], 3), + ([1, 1], [1], 4)]) + def test_double_integrator(self, xf, uf, Tf): # Define a second order integrator sys = ct.StateSpace([[-1, 1], [0, -2]], [[0], [1]], [[1, 0]], 0) flatsys = fs.LinearFlatSystem(sys) - # Define the endpoints of a trajectory - x1 = [0, 0]; u1 = [0]; T1 = 1 - x2 = [1, 0]; u2 = [0]; T2 = 2 - x3 = [0, 1]; u3 = [0]; T3 = 3 - x4 = [1, 1]; u4 = [1]; T4 = 4 - # Define the basis set poly = fs.PolyFamily(6) - # Plan trajectories for various combinations - for x0, u0, xf, uf, Tf in [ - (x1, u1, x2, u2, T2), (x1, u1, x3, u3, T3), (x1, u1, x4, u4, T4)]: - traj = fs.point_to_point(flatsys, x0, u0, xf, uf, Tf, basis=poly) + x1, u1, = [0, 0], [0] + traj = fs.point_to_point(flatsys, x1, u1, xf, uf, Tf, basis=poly) - # Verify that the trajectory computation is correct - x, u = traj.eval([0, Tf]) - np.testing.assert_array_almost_equal(x0, x[:, 0]) - np.testing.assert_array_almost_equal(u0, u[:, 0]) - np.testing.assert_array_almost_equal(xf, x[:, 1]) - np.testing.assert_array_almost_equal(uf, u[:, 1]) + # Verify that the trajectory computation is correct + x, u = traj.eval([0, Tf]) + np.testing.assert_array_almost_equal(x1, x[:, 0]) + np.testing.assert_array_almost_equal(u1, u[:, 0]) + np.testing.assert_array_almost_equal(xf, x[:, 1]) + np.testing.assert_array_almost_equal(uf, u[:, 1]) - # Simulate the system and make sure we stay close to desired traj - T = np.linspace(0, Tf, 100) - xd, ud = traj.eval(T) + # Simulate the system and make sure we stay close to desired traj + T = np.linspace(0, Tf, 100) + xd, ud = traj.eval(T) - t, y, x = ct.forced_response(sys, T, ud, x0) - np.testing.assert_array_almost_equal(x, xd, decimal=3) + t, y, x = ct.forced_response(sys, T, ud, x1) + np.testing.assert_array_almost_equal(x, xd, decimal=3) def test_kinematic_car(self): """Differential flatness for a kinematic car""" @@ -123,9 +121,3 @@ def vehicle_output(t, x, u, params): return x vehicle_flat, T, ud, x0, return_x=True) np.testing.assert_allclose(x, xd, atol=0.01, rtol=0.01) - def tearDown(self): - ct.reset_defaults() - - -if __name__ == '__main__': - unittest.main() From 1da4c5c113d3627f7a73e67c6edf8ddfb2473151 Mon Sep 17 00:00:00 2001 From: bnavigator Date: Sat, 25 Jul 2020 01:33:44 +0200 Subject: [PATCH 10/30] pytestify frd_test --- control/tests/frd_test.py | 140 +++++++++++++++++++------------------- 1 file changed, 69 insertions(+), 71 deletions(-) diff --git a/control/tests/frd_test.py b/control/tests/frd_test.py index fcbc10263..18f2f17b1 100644 --- a/control/tests/frd_test.py +++ b/control/tests/frd_test.py @@ -1,33 +1,36 @@ -#!/usr/bin/env python -# -# frd_test.py - test FRD class -# RvP, 4 Oct 2012 +"""frd_test.py - test FRD class +RvP, 4 Oct 2012 +""" -import unittest import sys as pysys + import numpy as np +import matplotlib.pyplot as plt +import pytest + import control as ct from control.statesp import StateSpace from control.xferfcn import TransferFunction from control.frdata import FRD, _convertToFRD, FrequencyResponseData -from control import bdalg -from control import freqplot -from control.exception import slycot_check -import matplotlib.pyplot as plt +from control import bdalg, evalfr, freqplot +from control.tests.conftest import slycotonly -class TestFRD(unittest.TestCase): +class TestFRD: """These are tests for functionality and correct reporting of the frequency response data class.""" def testBadInputType(self): """Give the constructor invalid input types.""" - self.assertRaises(ValueError, FRD) - self.assertRaises(TypeError, FRD, [1]) + with pytest.raises(ValueError): + FRD() + with pytest.raises(TypeError): + FRD([1]) def testInconsistentDimension(self): - self.assertRaises(TypeError, FRD, [1, 1], [1, 2, 3]) + with pytest.raises(TypeError): + FRD([1, 1], [1, 2, 3]) def testSISOtf(self): # get a SISO transfer function @@ -36,8 +39,11 @@ def testSISOtf(self): frd = FRD(h, omega) assert isinstance(frd, FRD) - np.testing.assert_array_almost_equal( - frd.freqresp([1.0]), h.freqresp([1.0])) + mag1, phase1, omega1 = frd.freqresp([1.0]) + mag2, phase2, omega2 = h.freqresp([1.0]) + np.testing.assert_array_almost_equal(mag1, mag2) + np.testing.assert_array_almost_equal(phase1, phase2) + np.testing.assert_array_almost_equal(omega1, omega2) def testOperators(self): # get two SISO transfer functions @@ -169,7 +175,7 @@ def testFeedback2(self): def testAuto(self): omega = np.logspace(-1, 2, 10) f1 = _convertToFRD(1, omega) - f2 = _convertToFRD(np.matrix([[1, 0], [0.1, -1]]), omega) + f2 = _convertToFRD(np.array([[1, 0], [0.1, -1]]), omega) f2 = _convertToFRD([[1, 0], [0.1, -1]], omega) f1, f2 # reference to avoid pyflakes error @@ -183,7 +189,7 @@ def testNyquist(self): freqplot.nyquist(f1, f1.omega) # plt.savefig('/dev/null', format='svg') - @unittest.skipIf(not slycot_check(), "slycot not installed") + @slycotonly def testMIMO(self): sys = StateSpace([[-0.5, 0.0], [0.0, -1.0]], [[1.0, 0.0], [0.0, 1.0]], @@ -198,7 +204,7 @@ def testMIMO(self): sys.freqresp([0.1, 1.0, 10])[1], f1.freqresp([0.1, 1.0, 10])[1]) - @unittest.skipIf(not slycot_check(), "slycot not installed") + @slycotonly def testMIMOfb(self): sys = StateSpace([[-0.5, 0.0], [0.0, -1.0]], [[1.0, 0.0], [0.0, 1.0]], @@ -214,13 +220,15 @@ def testMIMOfb(self): f1.freqresp([0.1, 1.0, 10])[1], f2.freqresp([0.1, 1.0, 10])[1]) - @unittest.skipIf(not slycot_check(), "slycot not installed") + @slycotonly def testMIMOfb2(self): - sys = StateSpace(np.matrix('-2.0 0 0; 0 -1 1; 0 0 -3'), - np.matrix('1.0 0; 0 0; 0 1'), + sys = StateSpace(np.array([[-2.0, 0, 0], + [0, -1, 1], + [0, 0, -3]]), + np.array([[1.0, 0], [0, 0], [0, 1]]), np.eye(3), np.zeros((3, 2))) omega = np.logspace(-1, 2, 10) - K = np.matrix('1 0.3 0; 0.1 0 0') + K = np.array([[1, 0.3, 0], [0.1, 0, 0]]) f1 = FRD(sys, omega).feedback(K) f2 = FRD(sys.feedback(K), omega) np.testing.assert_array_almost_equal( @@ -230,7 +238,7 @@ def testMIMOfb2(self): f1.freqresp([0.1, 1.0, 10])[1], f2.freqresp([0.1, 1.0, 10])[1]) - @unittest.skipIf(not slycot_check(), "slycot not installed") + @slycotonly def testMIMOMult(self): sys = StateSpace([[-0.5, 0.0], [0.0, -1.0]], [[1.0, 0.0], [0.0, 1.0]], @@ -246,13 +254,13 @@ def testMIMOMult(self): (f1*f2).freqresp([0.1, 1.0, 10])[1], (sys*sys).freqresp([0.1, 1.0, 10])[1]) - @unittest.skipIf(not slycot_check(), "slycot not installed") + @slycotonly def testMIMOSmooth(self): sys = StateSpace([[-0.5, 0.0], [0.0, -1.0]], [[1.0, 0.0], [0.0, 1.0]], [[1.0, 0.0], [0.0, 1.0], [1.0, 1.0]], [[0.0, 0.0], [0.0, 0.0], [0.0, 0.0]]) - sys2 = np.matrix([[1, 0, 0], [0, 1, 0]]) * sys + sys2 = np.array([[1, 0, 0], [0, 1, 0]]) * sys omega = np.logspace(-1, 2, 10) f1 = FRD(sys, omega, smooth=True) f2 = FRD(sys2, omega, smooth=True) @@ -271,47 +279,54 @@ def testAgainstOctave(self): # sys = ss([-2 0 0; 0 -1 1; 0 0 -3], # [1 0; 0 0; 0 1], eye(3), zeros(3,2)) # bfr = frd(bsys, [1]) - sys = StateSpace(np.matrix('-2.0 0 0; 0 -1 1; 0 0 -3'), - np.matrix('1.0 0; 0 0; 0 1'), + sys = StateSpace(np.array([[-2.0, 0, 0], [0, -1, 1], [0, 0, -3]]), + np.array([[1.0, 0], [0, 0], [0, 1]]), np.eye(3), np.zeros((3, 2))) omega = np.logspace(-1, 2, 10) f1 = FRD(sys, omega) np.testing.assert_array_almost_equal( (f1.freqresp([1.0])[0] * - np.exp(1j*f1.freqresp([1.0])[1])).reshape(3, 2), - np.matrix('0.4-0.2j 0; 0 0.1-0.2j; 0 0.3-0.1j')) + np.exp(1j * f1.freqresp([1.0])[1])).reshape(3, 2), + np.array([[0.4 - 0.2j, 0], [0, 0.1 - 0.2j], [0, 0.3 - 0.1j]])) - def test_string_representation(self): + def test_string_representation(self, capsys): sys = FRD([1, 2, 3], [4, 5, 6]) print(sys) # Just print without checking - def test_frequency_mismatch(self): + def test_frequency_mismatch(self, recwarn): + # recwarn: there may be a warning before the error! # Overlapping but non-equal frequency ranges sys1 = FRD([1, 2, 3], [4, 5, 6]) sys2 = FRD([2, 3, 4], [5, 6, 7]) - self.assertRaises(NotImplementedError, FRD.__add__, sys1, sys2) + with pytest.raises(NotImplementedError): + FRD.__add__(sys1, sys2) # One frequency range is a subset of another sys1 = FRD([1, 2, 3], [4, 5, 6]) sys2 = FRD([2, 3], [4, 5]) - self.assertRaises(NotImplementedError, FRD.__add__, sys1, sys2) + with pytest.raises(NotImplementedError): + FRD.__add__(sys1, sys2) def test_size_mismatch(self): sys1 = FRD(ct.rss(2, 2, 2), np.logspace(-1, 1, 10)) # Different number of inputs sys2 = FRD(ct.rss(3, 1, 2), np.logspace(-1, 1, 10)) - self.assertRaises(ValueError, FRD.__add__, sys1, sys2) + with pytest.raises(ValueError): + FRD.__add__(sys1, sys2) # Different number of outputs sys2 = FRD(ct.rss(3, 2, 1), np.logspace(-1, 1, 10)) - self.assertRaises(ValueError, FRD.__add__, sys1, sys2) + with pytest.raises(ValueError): + FRD.__add__(sys1, sys2) # Inputs and outputs don't match - self.assertRaises(ValueError, FRD.__mul__, sys2, sys1) + with pytest.raises(ValueError): + FRD.__mul__(sys2, sys1) # Feedback mismatch - self.assertRaises(ValueError, FRD.feedback, sys2, sys1) + with pytest.raises(ValueError): + FRD.feedback(sys2, sys1) def test_operator_conversion(self): sys_tf = ct.tf([1], [1, 2, 1]) @@ -365,7 +380,8 @@ def test_operator_conversion(self): np.testing.assert_array_almost_equal(sys_pow.fresp, chk_pow.fresp) # Assertion error if we try to raise to a non-integer power - self.assertRaises(ValueError, FRD.__pow__, frd_tf, 0.5) + with pytest.raises(ValueError): + FRD.__pow__(frd_tf, 0.5) # Selected testing on transfer function conversion sys_add = frd_2 + sys_tf @@ -375,44 +391,29 @@ def test_operator_conversion(self): # Input/output mismatch size mismatch in rmul sys1 = FRD(ct.rss(2, 2, 2), np.logspace(-1, 1, 10)) - self.assertRaises(ValueError, FRD.__rmul__, frd_2, sys1) + with pytest.raises(ValueError): + FRD.__rmul__(frd_2, sys1) # Make sure conversion of something random generates exception - self.assertRaises(TypeError, FRD.__add__, frd_tf, 'string') + with pytest.raises(TypeError): + FRD.__add__(frd_tf, 'string') def test_eval(self): sys_tf = ct.tf([1], [1, 2, 1]) frd_tf = FRD(sys_tf, np.logspace(-1, 1, 3)) - np.testing.assert_almost_equal(sys_tf.evalfr(1), frd_tf.eval(1)) + np.testing.assert_almost_equal(evalfr(sys_tf, 1J), frd_tf.eval(1)) # Should get an error if we evaluate at an unknown frequency - self.assertRaises(ValueError, frd_tf.eval, 2) + with pytest.raises(ValueError): + frd_tf.eval(2) + - # This test only works in Python 3 due to a conflict with the same - # warning type in other test modules (frd_test.py). See - # https://bugs.python.org/issue4180 for more details - @unittest.skipIf(pysys.version_info < (3, 0), "test requires Python 3+") def test_evalfr_deprecated(self): sys_tf = ct.tf([1], [1, 2, 1]) frd_tf = FRD(sys_tf, np.logspace(-1, 1, 3)) - # Deprecated version of the call (should generate warning) - import warnings - with warnings.catch_warnings(): - # Make warnings generate an exception - warnings.simplefilter('error') - - # Make sure that we get a pending deprecation warning - self.assertRaises(PendingDeprecationWarning, frd_tf.evalfr, 1.) - - # FRD.evalfr() is being deprecated - import warnings - with warnings.catch_warnings(): - # Make warnings generate an exception - warnings.simplefilter('error') - - # Make sure that we get a pending deprecation warning - self.assertRaises(PendingDeprecationWarning, frd_tf.evalfr, 1.) + with pytest.deprecated_call(): + frd_tf.evalfr(1.) def test_repr_str(self): # repr printing @@ -427,8 +428,8 @@ def test_repr_str(self): sysm = FrequencyResponseData( np.matmul(array([[1],[2]]), sys0.fresp), sys0.omega) - self.assertEqual(repr(sys0), ref0) - self.assertEqual(repr(sys1), ref1) + assert repr(sys0) == ref0 + assert repr(sys1) == ref1 sys0r = eval(repr(sys0)) np.testing.assert_array_almost_equal(sys0r.fresp, sys0.fresp) np.testing.assert_array_almost_equal(sys0r.omega, sys0.omega) @@ -444,8 +445,8 @@ def test_repr_str(self): 1.000 0.9 +0.1j 10.000 0.1 +2j 100.000 0.05 +3j""" - self.assertEqual(str(sys0), refs) - self.assertEqual(str(sys1), refs) + assert str(sys0) == refs + assert str(sys1) == refs # print multi-input system refm = """Frequency response data @@ -463,7 +464,4 @@ def test_repr_str(self): 1.000 1.8 +0.2j 10.000 0.2 +4j 100.000 0.1 +6j""" - self.assertEqual(str(sysm), refm) - -if __name__ == "__main__": - unittest.main() + assert str(sysm) == refm From 96f091139b288ffd9d3bf90411bf6e29611bf6a0 Mon Sep 17 00:00:00 2001 From: bnavigator Date: Tue, 29 Dec 2020 23:02:09 +0100 Subject: [PATCH 11/30] pytestify freqresp_test.py --- control/tests/freqresp_test.py | 475 +++++++++++++++++---------------- 1 file changed, 242 insertions(+), 233 deletions(-) diff --git a/control/tests/freqresp_test.py b/control/tests/freqresp_test.py index 9d59a1972..1ecc88129 100644 --- a/control/tests/freqresp_test.py +++ b/control/tests/freqresp_test.py @@ -1,241 +1,250 @@ -#!/usr/bin/env python -# -# freqresp_test.py - test frequency response functions -# RMM, 30 May 2016 (based on timeresp_test.py) -# -# This is a rudimentary set of tests for frequency response functions, -# including bode plots. - -import unittest +"""freqresp_test.py - test frequency response functions + +RMM, 30 May 2016 (based on timeresp_test.py) + +This is a rudimentary set of tests for frequency response functions, +including bode plots. +""" + import matplotlib.pyplot as plt import numpy as np -from numpy.testing import assert_array_almost_equal +from numpy.testing import assert_allclose +import pytest import control as ctrl from control.statesp import StateSpace from control.xferfcn import TransferFunction from control.matlab import ss, tf, bode, rss -from control.exception import slycot_check - - -class TestFreqresp(unittest.TestCase): - def setUp(self): - self.A = np.matrix('1,1;0,1') - self.C = np.matrix('1,0') - self.omega = np.linspace(10e-2,10e2,1000) - - def test_siso(self): - B = np.matrix('0;1') - D = 0 - sys = StateSpace(self.A,B,self.C,D) - - # test frequency response - frq=sys.freqresp(self.omega) - - # test bode plot - bode(sys) - - # Convert to transfer function and test bode - systf = tf(sys) - bode(systf) - - def test_superimpose(self): - # Test to make sure that multiple calls to plots superimpose their - # data on the same axes unless told to do otherwise - - # Generate two plots in a row; should be on the same axes - plt.figure(1); plt.clf() - ctrl.bode_plot(ctrl.tf([1], [1,2,1])) - ctrl.bode_plot(ctrl.tf([5], [1, 1])) - - # Check to make sure there are two axes and that each axes has two lines - self.assertEqual(len(plt.gcf().axes), 2) - for ax in plt.gcf().axes: - # Make sure there are 2 lines in each subplot - assert len(ax.get_lines()) == 2 - - # Generate two plots as a list; should be on the same axes - plt.figure(2); plt.clf(); - ctrl.bode_plot([ctrl.tf([1], [1,2,1]), ctrl.tf([5], [1, 1])]) - - # Check to make sure there are two axes and that each axes has two lines - self.assertEqual(len(plt.gcf().axes), 2) - for ax in plt.gcf().axes: - # Make sure there are 2 lines in each subplot - assert len(ax.get_lines()) == 2 - - # Generate two separate plots; only the second should appear - plt.figure(3); plt.clf(); - ctrl.bode_plot(ctrl.tf([1], [1,2,1])) - plt.clf() - ctrl.bode_plot(ctrl.tf([5], [1, 1])) - - # Check to make sure there are two axes and that each axes has one line - self.assertEqual(len(plt.gcf().axes), 2) - for ax in plt.gcf().axes: - # Make sure there is only 1 line in the subplot - assert len(ax.get_lines()) == 1 - - # Now add a line to the magnitude plot and make sure if is there - for ax in plt.gcf().axes: - if ax.get_label() == 'control-bode-magnitude': +from control.tests.conftest import slycotonly + + +pytestmark = pytest.mark.usefixtures("mplcleanup") + + +def test_siso(): + """Test SISO frequency response""" + A = np.array([[1, 1], [0, 1]]) + B = np.array([[0], [1]]) + C = np.array([[1, 0]]) + D = 0 + sys = StateSpace(A, B, C, D) + omega = np.linspace(10e-2, 10e2, 1000) + + # test frequency response + sys.freqresp(omega) + + # test bode plot + bode(sys) + + # Convert to transfer function and test bode + systf = tf(sys) + bode(systf) + + +@pytest.mark.filterwarnings("ignore:.*non-positive left xlim:UserWarning") +def test_superimpose(): + """Test superimpose multiple calls. + + Test to make sure that multiple calls to plots superimpose their + data on the same axes unless told to do otherwise + """ + # Generate two plots in a row; should be on the same axes + plt.figure(1) + plt.clf() + ctrl.bode_plot(ctrl.tf([1], [1, 2, 1])) + ctrl.bode_plot(ctrl.tf([5], [1, 1])) + + # Check that there are two axes and that each axes has two lines + len(plt.gcf().axes) == 2 + for ax in plt.gcf().axes: + # Make sure there are 2 lines in each subplot + assert len(ax.get_lines()) == 2 + + # Generate two plots as a list; should be on the same axes + plt.figure(2) + plt.clf() + ctrl.bode_plot([ctrl.tf([1], [1, 2, 1]), ctrl.tf([5], [1, 1])]) + + # Check that there are two axes and that each axes has two lines + assert len(plt.gcf().axes) == 2 + for ax in plt.gcf().axes: + # Make sure there are 2 lines in each subplot + assert len(ax.get_lines()) == 2 + + # Generate two separate plots; only the second should appear + plt.figure(3) + plt.clf() + ctrl.bode_plot(ctrl.tf([1], [1, 2, 1])) + plt.clf() + ctrl.bode_plot(ctrl.tf([5], [1, 1])) + + # Check to make sure there are two axes and that each axes has one line + assert len(plt.gcf().axes) == 2 + for ax in plt.gcf().axes: + # Make sure there is only 1 line in the subplot + assert len(ax.get_lines()) == 1 + + # Now add a line to the magnitude plot and make sure if is there + for ax in plt.gcf().axes: + if ax.get_label() == 'control-bode-magnitude': break - ax.semilogx([1e-2, 1e1], 20 * np.log10([1, 1]), 'k-') - self.assertEqual(len(ax.get_lines()), 2) - - def test_doubleint(self): - # 30 May 2016, RMM: added to replicate typecast bug in freqresp.py - A = np.matrix('0, 1; 0, 0'); - B = np.matrix('0; 1'); - C = np.matrix('1, 0'); - D = 0; - sys = ss(A, B, C, D); - bode(sys); - - @unittest.skipIf(not slycot_check(), "slycot not installed") - def test_mimo(self): - # MIMO - B = np.matrix('1,0;0,1') - D = np.matrix('0,0') - sysMIMO = ss(self.A,B,self.C,D) - - frqMIMO = sysMIMO.freqresp(self.omega) - tfMIMO = tf(sysMIMO) - - #bode(sysMIMO) # - should throw not implemented exception - #bode(tfMIMO) # - should throw not implemented exception - - #plt.figure(3) - #plt.semilogx(self.omega,20*np.log10(np.squeeze(frq[0]))) - - #plt.figure(4) - #bode(sysMIMO,self.omega) - - def test_bode_margin(self): - num = [1000] - den = [1, 25, 100, 0] - sys = ctrl.tf(num, den) - plt.figure() - ctrl.bode_plot(sys, margins=True,dB=False,deg = True, Hz=False) - fig = plt.gcf() - allaxes = fig.get_axes() - - mag_to_infinity = (np.array([6.07828691, 6.07828691]), - np.array([1.00000000e+00, 1.00000000e-08])) - assert_array_almost_equal(mag_to_infinity, allaxes[0].lines[2].get_data()) - - gm_to_infinty = (np.array([10., 10.]), np.array([4.00000000e-01, 1.00000000e-08])) - assert_array_almost_equal(gm_to_infinty, allaxes[0].lines[3].get_data()) - - one_to_gm = (np.array([10., 10.]), np.array([1., 0.4])) - assert_array_almost_equal(one_to_gm, allaxes[0].lines[4].get_data()) - - pm_to_infinity = (np.array([6.07828691, 6.07828691]), - np.array([100000., -157.46405841])) - assert_array_almost_equal(pm_to_infinity, allaxes[1].lines[2].get_data()) - - pm_to_phase = (np.array([6.07828691, 6.07828691]), np.array([-157.46405841, -180.])) - assert_array_almost_equal(pm_to_phase, allaxes[1].lines[3].get_data()) - - phase_to_infinity = (np.array([10., 10.]), np.array([1.00000000e-08, -1.80000000e+02])) - assert_array_almost_equal(phase_to_infinity, allaxes[1].lines[4].get_data()) - - def test_discrete(self): - # Test discrete time frequency response - - # SISO state space systems with either fixed or unspecified sampling times - sys = rss(3, 1, 1) - siso_ss1d = StateSpace(sys.A, sys.B, sys.C, sys.D, 0.1) - siso_ss2d = StateSpace(sys.A, sys.B, sys.C, sys.D, True) - - # MIMO state space systems with either fixed or unspecified sampling times - A = [[-3., 4., 2.], [-1., -3., 0.], [2., 5., 3.]] - B = [[1., 4.], [-3., -3.], [-2., 1.]] - C = [[4., 2., -3.], [1., 4., 3.]] - D = [[-2., 4.], [0., 1.]] - mimo_ss1d = StateSpace(A, B, C, D, 0.1) - mimo_ss2d = StateSpace(A, B, C, D, True) - - # SISO transfer functions - siso_tf1d = TransferFunction([1, 1], [1, 2, 1], 0.1) - siso_tf2d = TransferFunction([1, 1], [1, 2, 1], True) - - # Go through each system and call the code, checking return types - for sys in (siso_ss1d, siso_ss2d, mimo_ss1d, mimo_ss2d, - siso_tf1d, siso_tf2d): - # Set frequency range to just below Nyquist freq (for Bode) - omega_ok = np.linspace(10e-4,0.99,100) * np.pi/sys.dt - - # Test frequency response - ret = sys.freqresp(omega_ok) - - # Check for warning if frequency is out of range - import warnings - warnings.simplefilter('always', UserWarning) # don't supress - with warnings.catch_warnings(record=True) as w: - # Set up warnings filter to only show warnings in control module - warnings.filterwarnings("ignore") - warnings.filterwarnings("always", module="control") - - # Look for a warning about sampling above Nyquist frequency - omega_bad = np.linspace(10e-4,1.1,10) * np.pi/sys.dt - ret = sys.freqresp(omega_bad) - print("len(w) =", len(w)) - self.assertEqual(len(w), 1) - self.assertIn("above", str(w[-1].message)) - self.assertIn("Nyquist", str(w[-1].message)) - - # Test bode plots (currently only implemented for SISO) - if (sys.inputs == 1 and sys.outputs == 1): - # Generic call (frequency range calculated automatically) - ret_ss = bode(sys) - - # Convert to transfer function and test bode again - systf = tf(sys); - ret_tf = bode(systf) - - # Make sure we can pass a frequency range - bode(sys, omega_ok) - - else: - # Calling bode should generate a not implemented error - self.assertRaises(NotImplementedError, bode, (sys,)) - - def test_options(self): - """Test ability to set parameter values""" - # Generate a Bode plot of a transfer function - sys = ctrl.tf([1000], [1, 25, 100, 0]) - fig1 = plt.figure() - ctrl.bode_plot(sys, dB=False, deg = True, Hz=False) - - # Save the parameter values - left1, right1 = fig1.axes[0].xaxis.get_data_interval() - numpoints1 = len(fig1.axes[0].lines[0].get_data()[0]) - - # Same transfer function, but add a decade on each end - ctrl.config.set_defaults('freqplot', feature_periphery_decades=2) - fig2 = plt.figure() - ctrl.bode_plot(sys, dB=False, deg = True, Hz=False) - left2, right2 = fig2.axes[0].xaxis.get_data_interval() - - # Make sure we got an extra decade on each end - self.assertAlmostEqual(left2, 0.1 * left1) - self.assertAlmostEqual(right2, 10 * right1) - - # Same transfer function, but add more points to the plot - ctrl.config.set_defaults( - 'freqplot', feature_periphery_decades=2, number_of_samples=13) - fig3 = plt.figure() - ctrl.bode_plot(sys, dB=False, deg = True, Hz=False) - numpoints3 = len(fig3.axes[0].lines[0].get_data()[0]) - - # Make sure we got the right number of points - self.assertNotEqual(numpoints1, numpoints3) - self.assertEqual(numpoints3, 13) - - # Reset default parameters to avoid contamination - ctrl.config.reset_defaults() - - -if __name__ == '__main__': - unittest.main() + + ax.semilogx([1e-2, 1e1], 20 * np.log10([1, 1]), 'k-') + assert len(ax.get_lines()) == 2 + + +def test_doubleint(): + """Test typcast bug with double int + + 30 May 2016, RMM: added to replicate typecast bug in freqresp.py + """ + A = np.array([[0, 1], [0, 0]]) + B = np.array([[0], [1]]) + C = np.array([[1, 0]]) + D = 0 + sys = ss(A, B, C, D) + bode(sys) + + +@slycotonly +def test_mimo(): + """Test MIMO frequency response calls""" + A = np.array([[1, 1], [0, 1]]) + B = np.array([[1, 0], [0, 1]]) + C = np.array([[1, 0]]) + D = np.array([[0, 0]]) + omega = np.linspace(10e-2, 10e2, 1000) + sysMIMO = ss(A, B, C, D) + + sysMIMO.freqresp(omega) + tf(sysMIMO) + + +def test_bode_margin(): + """Test bode margins""" + num = [1000] + den = [1, 25, 100, 0] + sys = ctrl.tf(num, den) + plt.figure() + ctrl.bode_plot(sys, margins=True, dB=False, deg=True, Hz=False) + fig = plt.gcf() + allaxes = fig.get_axes() + + mag_to_infinity = (np.array([6.07828691, 6.07828691]), + np.array([1., 1e-8])) + assert_allclose(mag_to_infinity, allaxes[0].lines[2].get_data()) + + gm_to_infinty = (np.array([10., 10.]), + np.array([4e-1, 1e-8])) + assert_allclose(gm_to_infinty, allaxes[0].lines[3].get_data()) + + one_to_gm = (np.array([10., 10.]), + np.array([1., 0.4])) + assert_allclose(one_to_gm, allaxes[0].lines[4].get_data()) + + pm_to_infinity = (np.array([6.07828691, 6.07828691]), + np.array([100000., -157.46405841])) + assert_allclose(pm_to_infinity, allaxes[1].lines[2].get_data()) + + pm_to_phase = (np.array([6.07828691, 6.07828691]), + np.array([-157.46405841, -180.])) + assert_allclose(pm_to_phase, allaxes[1].lines[3].get_data()) + + phase_to_infinity = (np.array([10., 10.]), + np.array([1e-8, -1.8e2])) + assert_allclose(phase_to_infinity, allaxes[1].lines[4].get_data()) + + +@pytest.fixture +def dsystem_dt(request): + """Test systems for test_discrete""" + # SISO state space systems with either fixed or unspecified sampling times + sys = rss(3, 1, 1) + + # MIMO state space systems with either fixed or unspecified sampling times + A = [[-3., 4., 2.], [-1., -3., 0.], [2., 5., 3.]] + B = [[1., 4.], [-3., -3.], [-2., 1.]] + C = [[4., 2., -3.], [1., 4., 3.]] + D = [[-2., 4.], [0., 1.]] + + dt = request.param + systems = {'sssiso': StateSpace(sys.A, sys.B, sys.C, sys.D, dt), + 'ssmimo': StateSpace(A, B, C, D, dt), + 'tf': TransferFunction([1, 1], [1, 2, 1], dt)} + return systems + + +@pytest.fixture +def dsystem_type(request, dsystem_dt): + """Return system by typekey""" + systype = request.param + return dsystem_dt[systype] + + +@pytest.mark.parametrize("dsystem_dt", [0.1, True], indirect=True) +@pytest.mark.parametrize("dsystem_type", ['sssiso', 'ssmimo', 'tf'], + indirect=True) +def test_discrete(dsystem_type): + """Test discrete time frequency response""" + dsys = dsystem_type + # Set frequency range to just below Nyquist freq (for Bode) + omega_ok = np.linspace(10e-4, 0.99, 100) * np.pi / dsys.dt + + # Test frequency response + dsys.freqresp(omega_ok) + + # Check for warning if frequency is out of range + with pytest.warns(UserWarning, match="above.*Nyquist"): + # Look for a warning about sampling above Nyquist frequency + omega_bad = np.linspace(10e-4, 1.1, 10) * np.pi / dsys.dt + dsys.freqresp(omega_bad) + + # Test bode plots (currently only implemented for SISO) + if (dsys.inputs == 1 and dsys.outputs == 1): + # Generic call (frequency range calculated automatically) + bode(dsys) + + # Convert to transfer function and test bode again + systf = tf(dsys) + bode(systf) + + # Make sure we can pass a frequency range + bode(dsys, omega_ok) + + else: + # Calling bode should generate a not implemented error + with pytest.raises(NotImplementedError): + bode((dsys,)) + + +def test_options(editsdefaults): + """Test ability to set parameter values""" + # Generate a Bode plot of a transfer function + sys = ctrl.tf([1000], [1, 25, 100, 0]) + fig1 = plt.figure() + ctrl.bode_plot(sys, dB=False, deg=True, Hz=False) + + # Save the parameter values + left1, right1 = fig1.axes[0].xaxis.get_data_interval() + numpoints1 = len(fig1.axes[0].lines[0].get_data()[0]) + + # Same transfer function, but add a decade on each end + ctrl.config.set_defaults('freqplot', feature_periphery_decades=2) + fig2 = plt.figure() + ctrl.bode_plot(sys, dB=False, deg=True, Hz=False) + left2, right2 = fig2.axes[0].xaxis.get_data_interval() + + # Make sure we got an extra decade on each end + assert_allclose(left2, 0.1 * left1) + assert_allclose(right2, 10 * right1) + + # Same transfer function, but add more points to the plot + ctrl.config.set_defaults( + 'freqplot', feature_periphery_decades=2, number_of_samples=13) + fig3 = plt.figure() + ctrl.bode_plot(sys, dB=False, deg=True, Hz=False) + numpoints3 = len(fig3.axes[0].lines[0].get_data()[0]) + + # Make sure we got the right number of points + assert numpoints1 != numpoints3 + assert numpoints3 == 13 From caed7128e0ebfe86448e2dfc42f93b9a46870925 Mon Sep 17 00:00:00 2001 From: bnavigator Date: Sun, 26 Jul 2020 14:42:38 +0200 Subject: [PATCH 12/30] pytestify input_element_int_test --- control/tests/input_element_int_test.py | 74 ++++++++++++++----------- 1 file changed, 43 insertions(+), 31 deletions(-) diff --git a/control/tests/input_element_int_test.py b/control/tests/input_element_int_test.py index c6a6f64a3..ecfaab834 100644 --- a/control/tests/input_element_int_test.py +++ b/control/tests/input_element_int_test.py @@ -1,54 +1,66 @@ -# input_element_int_test.py -# -# Author: Kangwon Lee (kangwonlee) -# Date: 22 Oct 2017 -# -# Unit tests contributed as part of PR #158, "SISO tf() may not work -# with numpy arrays with numpy.int elements" -# -# Modified: -# * 29 Dec 2017, RMM - updated file name and added header - -import unittest +"""input_element_int_test.py + +Author: Kangwon Lee (kangwonlee) +Date: 22 Oct 2017 + +Modified: +* 29 Dec 2017, RMM - updated file name and added header +""" + import numpy as np -import control as ctl +from control import dcgain, ss, tf + +class TestTfInputIntElement: + """input_element_int_test + + Unit tests contributed as part of PR gh-158, "SISO tf() may not work + with numpy arrays with numpy.int elements + """ -class TestTfInputIntElement(unittest.TestCase): - # currently these do not pass def test_tf_den_with_numpy_int_element(self): num = 1 den = np.convolve([1, 2, 1], [1, 1, 1]) - sys = ctl.tf(num, den) + sys = tf(num, den) - self.assertAlmostEqual(1.0, ctl.dcgain(sys)) + np.testing.assert_array_max_ulp(1., dcgain(sys)) def test_tf_num_with_numpy_int_element(self): num = np.convolve([1], [1, 1]) den = np.convolve([1, 2, 1], [1, 1, 1]) - sys = ctl.tf(num, den) + sys = tf(num, den) - self.assertAlmostEqual(1.0, ctl.dcgain(sys)) + np.testing.assert_array_max_ulp(1., dcgain(sys)) # currently these pass - def test_tf_input_with_int_element_works(self): + def test_tf_input_with_int_element(self): num = 1 den = np.convolve([1.0, 2, 1], [1, 1, 1]) - sys = ctl.tf(num, den) + sys = tf(num, den) - self.assertAlmostEqual(1.0, ctl.dcgain(sys)) + np.testing.assert_array_max_ulp(1., dcgain(sys)) def test_ss_input_with_int_element(self): - ident = np.matrix(np.identity(2), dtype=int) - a = np.matrix([[0, 1], - [-1, -2]], dtype=int) * ident - b = np.matrix([[0], + a = np.array([[0, 1], + [-1, -2]], dtype=int) + b = np.array([[0], [1]], dtype=int) - c = np.matrix([[0, 1]], dtype=int) - d = 0 + c = np.array([[0, 1]], dtype=int) + d = np.array([[1]], dtype=int) + + sys = ss(a, b, c, d) + sys2 = tf(sys) + np.testing.assert_array_max_ulp(dcgain(sys), dcgain(sys2)) - sys = ctl.ss(a, b, c, d) - sys2 = ctl.ss2tf(sys) - self.assertAlmostEqual(ctl.dcgain(sys), ctl.dcgain(sys2)) + def test_ss_input_with_0int_dcgain(self): + a = np.array([[0, 1], + [-1, -2]], dtype=int) + b = np.array([[0], + [1]], dtype=int) + c = np.array([[0, 1]], dtype=int) + d = 0 + sys = ss(a, b, c, d) + np.testing.assert_allclose(dcgain(sys), 0, + atol=np.finfo(np.float).epsneg) \ No newline at end of file From 55a61197fb870502355ef01e4c98b16dc5ed27e6 Mon Sep 17 00:00:00 2001 From: bnavigator Date: Tue, 29 Dec 2020 02:07:54 +0100 Subject: [PATCH 13/30] pytestify iosys_test --- control/tests/iosys_test.py | 589 +++++++++++++++++------------------- 1 file changed, 283 insertions(+), 306 deletions(-) diff --git a/control/tests/iosys_test.py b/control/tests/iosys_test.py index 20f289d8c..2bb6f066c 100644 --- a/control/tests/iosys_test.py +++ b/control/tests/iosys_test.py @@ -1,85 +1,89 @@ -#!/usr/bin/env python -# -# iosys_test.py - test input/output system oeprations -# RMM, 17 Apr 2019 -# -# This test suite checks to make sure that basic input/output class -# operations are working. It doesn't do exhaustive testing of -# operations on input/output systems. Separate unit tests should be -# created for that purpose. +"""iosys_test.py - test input/output system oeprations + +RMM, 17 Apr 2019 + +This test suite checks to make sure that basic input/output class +operations are working. It doesn't do exhaustive testing of +operations on input/output systems. Separate unit tests should be +created for that purpose. +""" from __future__ import print_function -import unittest -import warnings + import numpy as np +import pytest import scipy as sp + import control as ct -import control.iosys as ios -from distutils.version import StrictVersion +from control import iosys as ios +from control.tests.conftest import noscipy0 -class TestIOSys(unittest.TestCase): - def setUp(self): - # Turn off numpy matrix warnings - import warnings - warnings.simplefilter('ignore', category=PendingDeprecationWarning) +class TestIOSys: + + @pytest.fixture + def tsys(self): + class TSys: + pass + T = TSys() + """Return some test systems""" # Create a single input/single output linear system - self.siso_linsys = ct.StateSpace( + T.siso_linsys = ct.StateSpace( [[-1, 1], [0, -2]], [[0], [1]], [[1, 0]], [[0]]) # Create a multi input/multi output linear system - self.mimo_linsys1 = ct.StateSpace( + T.mimo_linsys1 = ct.StateSpace( [[-1, 1], [0, -2]], [[1, 0], [0, 1]], - [[1, 0], [0, 1]], np.zeros((2,2))) + [[1, 0], [0, 1]], np.zeros((2, 2))) # Create a multi input/multi output linear system - self.mimo_linsys2 = ct.StateSpace( + T.mimo_linsys2 = ct.StateSpace( [[-1, 1], [0, -2]], [[0, 1], [1, 0]], - [[1, 0], [0, 1]], np.zeros((2,2))) + [[1, 0], [0, 1]], np.zeros((2, 2))) # Create simulation parameters - self.T = np.linspace(0, 10, 100) - self.U = np.sin(self.T) - self.X0 = [0, 0] + T.T = np.linspace(0, 10, 100) + T.U = np.sin(T.T) + T.X0 = [0, 0] + + return T - @unittest.skipIf(StrictVersion(sp.__version__) < "1.0", - "requires SciPy 1.0 or greater") - def test_linear_iosys(self): + @noscipy0 + def test_linear_iosys(self, tsys): # Create an input/output system from the linear system - linsys = self.siso_linsys + linsys = tsys.siso_linsys iosys = ios.LinearIOSystem(linsys) # Make sure that the right hand side matches linear system for x, u in (([0, 0], 0), ([1, 0], 0), ([0, 1], 0), ([0, 0], 1)): np.testing.assert_array_almost_equal( - np.reshape(iosys._rhs(0, x, u), (-1,1)), + np.reshape(iosys._rhs(0, x, u), (-1, 1)), np.dot(linsys.A, np.reshape(x, (-1, 1))) + np.dot(linsys.B, u)) # Make sure that simulations also line up - T, U, X0 = self.T, self.U, self.X0 + T, U, X0 = tsys.T, tsys.U, tsys.X0 lti_t, lti_y, lti_x = ct.forced_response(linsys, T, U, X0) ios_t, ios_y = ios.input_output_response(iosys, T, U, X0) np.testing.assert_array_almost_equal(lti_t, ios_t) - np.testing.assert_allclose(lti_y, ios_y,atol=0.002,rtol=0.) + np.testing.assert_allclose(lti_y, ios_y, atol=0.002, rtol=0.) - @unittest.skipIf(StrictVersion(sp.__version__) < "1.0", - "requires SciPy 1.0 or greater") - def test_tf2io(self): + @noscipy0 + def test_tf2io(self, tsys): # Create a transfer function from the state space system - linsys = self.siso_linsys + linsys = tsys.siso_linsys tfsys = ct.ss2tf(linsys) iosys = ct.tf2io(tfsys) # Verify correctness via simulation - T, U, X0 = self.T, self.U, self.X0 + T, U, X0 = tsys.T, tsys.U, tsys.X0 lti_t, lti_y, lti_x = ct.forced_response(linsys, T, U, X0) ios_t, ios_y = ios.input_output_response(iosys, T, U, X0) np.testing.assert_array_almost_equal(lti_t, ios_t) - np.testing.assert_allclose(lti_y, ios_y,atol=0.002,rtol=0.) + np.testing.assert_allclose(lti_y, ios_y, atol=0.002, rtol=0.) - def test_ss2io(self): + def test_ss2io(self, tsys): # Create an input/output system from the linear system - linsys = self.siso_linsys + linsys = tsys.siso_linsys iosys = ct.ss2io(linsys) np.testing.assert_array_equal(linsys.A, iosys.A) np.testing.assert_array_equal(linsys.B, iosys.B) @@ -89,50 +93,44 @@ def test_ss2io(self): # Try adding names to things iosys_named = ct.ss2io(linsys, inputs='u', outputs='y', states=['x1', 'x2'], name='iosys_named') - self.assertEqual(iosys_named.find_input('u'), 0) - self.assertEqual(iosys_named.find_input('x'), None) - self.assertEqual(iosys_named.find_output('y'), 0) - self.assertEqual(iosys_named.find_output('u'), None) - self.assertEqual(iosys_named.find_state('x0'), None) - self.assertEqual(iosys_named.find_state('x1'), 0) - self.assertEqual(iosys_named.find_state('x2'), 1) + assert iosys_named.find_input('u') == 0 + assert iosys_named.find_input('x') is None + assert iosys_named.find_output('y') == 0 + assert iosys_named.find_output('u') is None + assert iosys_named.find_state('x0') is None + assert iosys_named.find_state('x1') == 0 + assert iosys_named.find_state('x2') == 1 np.testing.assert_array_equal(linsys.A, iosys_named.A) np.testing.assert_array_equal(linsys.B, iosys_named.B) np.testing.assert_array_equal(linsys.C, iosys_named.C) np.testing.assert_array_equal(linsys.D, iosys_named.D) - # Make sure unspecified inputs/outputs/states are handled properly - def test_iosys_unspecified(self): - # System with unspecified inputs and outputs + def test_iosys_unspecified(self, tsys): + """System with unspecified inputs and outputs""" sys = ios.NonlinearIOSystem(secord_update, secord_output) np.testing.assert_raises(TypeError, sys.__mul__, sys) - # Make sure we can print various types of I/O systems - def test_iosys_print(self): + def test_iosys_print(self, tsys, capsys): + """Make sure we can print various types of I/O systems""" # Send the output to /dev/null - import os - f = open(os.devnull,"w") # Simple I/O system - iosys = ct.ss2io(self.siso_linsys) - print(iosys, file=f) + iosys = ct.ss2io(tsys.siso_linsys) + print(iosys) # I/O system without ninputs, noutputs ios_unspecified = ios.NonlinearIOSystem(secord_update, secord_output) - print(ios_unspecified, file=f) + print(ios_unspecified) # I/O system with derived inputs and outputs ios_linearized = ios.linearize(ios_unspecified, [0, 0], [0]) - print(ios_linearized, file=f) - - f.close() + print(ios_linearized) - @unittest.skipIf(StrictVersion(sp.__version__) < "1.0", - "requires SciPy 1.0 or greater") - def test_nonlinear_iosys(self): + @noscipy0 + def test_nonlinear_iosys(self, tsys): # Create a simple nonlinear I/O system nlsys = ios.NonlinearIOSystem(predprey) - T = self.T + T = tsys.T # Start by simulating from an equilibrium point X0 = [0, 0] @@ -147,25 +145,27 @@ def test_nonlinear_iosys(self): # Simulate a linear function as a nonlinear function and compare # # Create a single input/single output linear system - linsys = self.siso_linsys + linsys = tsys.siso_linsys # Create a nonlinear system with the same dynamics nlupd = lambda t, x, u, params: \ - np.reshape(np.dot(linsys.A, np.reshape(x, (-1, 1))) + np.dot(linsys.B, u), (-1,)) + np.reshape(np.dot(linsys.A, np.reshape(x, (-1, 1))) + + np.dot(linsys.B, u), (-1,)) nlout = lambda t, x, u, params: \ - np.reshape(np.dot(linsys.C, np.reshape(x, (-1, 1))) + np.dot(linsys.D, u), (-1,)) + np.reshape(np.dot(linsys.C, np.reshape(x, (-1, 1))) + + np.dot(linsys.D, u), (-1,)) nlsys = ios.NonlinearIOSystem(nlupd, nlout) # Make sure that simulations also line up - T, U, X0 = self.T, self.U, self.X0 + T, U, X0 = tsys.T, tsys.U, tsys.X0 lti_t, lti_y, lti_x = ct.forced_response(linsys, T, U, X0) ios_t, ios_y = ios.input_output_response(nlsys, T, U, X0) np.testing.assert_array_almost_equal(lti_t, ios_t) np.testing.assert_allclose(lti_y, ios_y,atol=0.002,rtol=0.) - def test_linearize(self): + def test_linearize(self, tsys): # Create a single input/single output linear system - linsys = self.siso_linsys + linsys = tsys.siso_linsys iosys = ios.LinearIOSystem(linsys) # Linearize it and make sure we get back what we started with @@ -178,8 +178,10 @@ def test_linearize(self): # Create a simple nonlinear system to check (kinematic car) def kincar_update(t, x, u, params): return np.array([np.cos(x[2]) * u[0], np.sin(x[2]) * u[0], u[1]]) + def kincar_output(t, x, u, params): return np.array([x[0], x[1]]) + iosys = ios.NonlinearIOSystem(kincar_update, kincar_output) linearized = iosys.linearize([0, 0, 0], [0, 0]) np.testing.assert_array_almost_equal(linearized.A, np.zeros((3,3))) @@ -189,13 +191,13 @@ def kincar_output(t, x, u, params): linearized.C, [[1, 0, 0], [0, 1, 0]]) np.testing.assert_array_almost_equal(linearized.D, np.zeros((2,2))) - @unittest.skipIf(StrictVersion(sp.__version__) < "1.0", - "requires SciPy 1.0 or greater") - def test_connect(self): + + @noscipy0 + def test_connect(self, tsys): # Define a couple of (linear) systems to interconnection - linsys1 = self.siso_linsys + linsys1 = tsys.siso_linsys iosys1 = ios.LinearIOSystem(linsys1) - linsys2 = self.siso_linsys + linsys2 = tsys.siso_linsys iosys2 = ios.LinearIOSystem(linsys2) # Connect systems in different ways and compare to StateSpace @@ -208,8 +210,8 @@ def test_connect(self): ) # Run a simulation and compare to linear response - T, U = self.T, self.U - X0 = np.concatenate((self.X0, self.X0)) + T, U = tsys.T, tsys.U + X0 = np.concatenate((tsys.X0, tsys.X0)) ios_t, ios_y, ios_x = ios.input_output_response( iosys_series, T, U, X0, return_x=True) lti_t, lti_y, lti_x = ct.forced_response(linsys_series, T, U, X0) @@ -217,7 +219,7 @@ def test_connect(self): np.testing.assert_allclose(lti_y, ios_y,atol=0.002,rtol=0.) # Connect systems with different timebases - linsys2c = self.siso_linsys + linsys2c = tsys.siso_linsys linsys2c.dt = 0 # Reset the timebase iosys2c = ios.LinearIOSystem(linsys2c) iosys_series = ios.InterconnectedSystem( @@ -226,7 +228,7 @@ def test_connect(self): 0, # input = first system 1 # output = second system ) - self.assertTrue(ct.isctime(iosys_series, strict=True)) + assert ct.isctime(iosys_series, strict=True) ios_t, ios_y, ios_x = ios.input_output_response( iosys_series, T, U, X0, return_x=True) lti_t, lti_y, lti_x = ct.forced_response(linsys_series, T, U, X0) @@ -248,11 +250,10 @@ def test_connect(self): np.testing.assert_array_almost_equal(lti_t, ios_t) np.testing.assert_allclose(lti_y, ios_y,atol=0.002,rtol=0.) - @unittest.skipIf(StrictVersion(sp.__version__) < "1.0", - "requires SciPy 1.0 or greater") - def test_static_nonlinearity(self): + @noscipy0 + def test_static_nonlinearity(self, tsys): # Linear dynamical system - linsys = self.siso_linsys + linsys = tsys.siso_linsys ioslin = ios.LinearIOSystem(linsys) # Nonlinear saturation @@ -261,7 +262,7 @@ def test_static_nonlinearity(self): nlsat = ios.NonlinearIOSystem(None, sat_output, inputs=1, outputs=1) # Set up parameters for simulation - T, U, X0 = self.T, 2 * self.U, self.X0 + T, U, X0 = tsys.T, 2 * tsys.U, tsys.X0 Usat = np.vectorize(sat)(U) # Make sure saturation works properly by comparing linear system with @@ -272,11 +273,12 @@ def test_static_nonlinearity(self): np.testing.assert_array_almost_equal(lti_t, ios_t) np.testing.assert_array_almost_equal(lti_y, ios_y, decimal=2) - @unittest.skipIf(StrictVersion(sp.__version__) < "1.0", - "requires SciPy 1.0 or greater") - def test_algebraic_loop(self): + + @noscipy0 + @pytest.mark.filterwarnings("ignore:Duplicate name::control.iosys") + def test_algebraic_loop(self, tsys): # Create some linear and nonlinear systems to play with - linsys = self.siso_linsys + linsys = tsys.siso_linsys lnios = ios.LinearIOSystem(linsys) nlios = ios.NonlinearIOSystem(None, \ lambda t, x, u, params: u*u, inputs=1, outputs=1) @@ -284,7 +286,7 @@ def test_algebraic_loop(self): nlios2 = nlios.copy() # Set up parameters for simulation - T, U, X0 = self.T, self.U, self.X0 + T, U, X0 = tsys.T, tsys.U, tsys.X0 # Single nonlinear system - no states ios_t, ios_y = ios.input_output_response(nlios, T, U) @@ -324,7 +326,8 @@ def test_algebraic_loop(self): 0, 0 ) args = (iosys, T, U) - self.assertRaises(RuntimeError, ios.input_output_response, *args) + with pytest.raises(RuntimeError): + ios.input_output_response(*args) # Algebraic loop due to feedthrough term linsys = ct.StateSpace( @@ -338,20 +341,20 @@ def test_algebraic_loop(self): ) args = (iosys, T, U, X0) # ios_t, ios_y = ios.input_output_response(iosys, T, U, X0) - self.assertRaises(RuntimeError, ios.input_output_response, *args) + with pytest.raises(RuntimeError): + ios.input_output_response(*args) - @unittest.skipIf(StrictVersion(sp.__version__) < "1.0", - "requires SciPy 1.0 or greater") - def test_summer(self): + @noscipy0 + def test_summer(self, tsys): # Construct a MIMO system for testing - linsys = self.mimo_linsys1 + linsys = tsys.mimo_linsys1 linio = ios.LinearIOSystem(linsys) linsys_parallel = linsys + linsys iosys_parallel = linio + linio # Set up parameters for simulation - T = self.T + T = tsys.T U = [np.sin(T), np.cos(T)] X0 = 0 @@ -359,18 +362,17 @@ def test_summer(self): ios_t, ios_y = ios.input_output_response(iosys_parallel, T, U, X0) np.testing.assert_allclose(ios_y, lin_y,atol=0.002,rtol=0.) - @unittest.skipIf(StrictVersion(sp.__version__) < "1.0", - "requires SciPy 1.0 or greater") - def test_rmul(self): + @noscipy0 + def test_rmul(self, tsys): # Test right multiplication # TODO: replace with better tests when conversions are implemented # Set up parameters for simulation - T, U, X0 = self.T, self.U, self.X0 + T, U, X0 = tsys.T, tsys.U, tsys.X0 # Linear system with input and output nonlinearities # Also creates a nested interconnected system - ioslin = ios.LinearIOSystem(self.siso_linsys) + ioslin = ios.LinearIOSystem(tsys.siso_linsys) nlios = ios.NonlinearIOSystem(None, \ lambda t, x, u, params: u*u, inputs=1, outputs=1) sys1 = nlios * ioslin @@ -381,13 +383,12 @@ def test_rmul(self): lti_t, lti_y, lti_x = ct.forced_response(ioslin, T, U*U, X0) np.testing.assert_array_almost_equal(ios_y, lti_y*lti_y, decimal=3) - @unittest.skipIf(StrictVersion(sp.__version__) < "1.0", - "requires SciPy 1.0 or greater") - def test_neg(self): + @noscipy0 + def test_neg(self, tsys): """Test negation of a system""" # Set up parameters for simulation - T, U, X0 = self.T, self.U, self.X0 + T, U, X0 = tsys.T, tsys.U, tsys.X0 # Static nonlinear system nlios = ios.NonlinearIOSystem(None, \ @@ -397,7 +398,7 @@ def test_neg(self): # Linear system with input nonlinearity # Also creates a nested interconnected system - ioslin = ios.LinearIOSystem(self.siso_linsys) + ioslin = ios.LinearIOSystem(tsys.siso_linsys) sys = (ioslin) * (-nlios) # Make sure we got the right thing (via simulation comparison) @@ -405,36 +406,34 @@ def test_neg(self): lti_t, lti_y, lti_x = ct.forced_response(ioslin, T, U*U, X0) np.testing.assert_array_almost_equal(ios_y, -lti_y, decimal=3) - @unittest.skipIf(StrictVersion(sp.__version__) < "1.0", - "requires SciPy 1.0 or greater") - def test_feedback(self): + @noscipy0 + def test_feedback(self, tsys): # Set up parameters for simulation - T, U, X0 = self.T, self.U, self.X0 + T, U, X0 = tsys.T, tsys.U, tsys.X0 # Linear system with constant feedback (via "nonlinear" mapping) - ioslin = ios.LinearIOSystem(self.siso_linsys) + ioslin = ios.LinearIOSystem(tsys.siso_linsys) nlios = ios.NonlinearIOSystem(None, \ lambda t, x, u, params: u, inputs=1, outputs=1) iosys = ct.feedback(ioslin, nlios) - linsys = ct.feedback(self.siso_linsys, 1) + linsys = ct.feedback(tsys.siso_linsys, 1) ios_t, ios_y = ios.input_output_response(iosys, T, U, X0) lti_t, lti_y, lti_x = ct.forced_response(linsys, T, U, X0) np.testing.assert_allclose(ios_y, lti_y,atol=0.002,rtol=0.) - @unittest.skipIf(StrictVersion(sp.__version__) < "1.0", - "requires SciPy 1.0 or greater") - def test_bdalg_functions(self): + @noscipy0 + def test_bdalg_functions(self, tsys): """Test block diagram functions algebra on I/O systems""" # Set up parameters for simulation - T = self.T + T = tsys.T U = [np.sin(T), np.cos(T)] X0 = 0 # Set up systems to be composed - linsys1 = self.mimo_linsys1 + linsys1 = tsys.mimo_linsys1 linio1 = ios.LinearIOSystem(linsys1) - linsys2 = self.mimo_linsys2 + linsys2 = tsys.mimo_linsys2 linio2 = ios.LinearIOSystem(linsys2) # Series interconnection @@ -447,7 +446,7 @@ def test_bdalg_functions(self): # Make sure that systems don't commute linsys_series = ct.series(linsys2, linsys1) lin_t, lin_y, lin_x = ct.forced_response(linsys_series, T, U, X0) - self.assertFalse((np.abs(lin_y - ios_y) < 1e-3).all()) + assert not (np.abs(lin_y - ios_y) < 1e-3).all() # Parallel interconnection linsys_parallel = ct.parallel(linsys1, linsys2) @@ -470,11 +469,10 @@ def test_bdalg_functions(self): ios_t, ios_y = ios.input_output_response(iosys_feedback, T, U, X0) np.testing.assert_allclose(ios_y, lin_y,atol=0.002,rtol=0.) - @unittest.skipIf(StrictVersion(sp.__version__) < "1.0", - "requires SciPy 1.0 or greater") - def test_nonsquare_bdalg(self): + @noscipy0 + def test_nonsquare_bdalg(self, tsys): # Set up parameters for simulation - T = self.T + T = tsys.T U2 = [np.sin(T), np.cos(T)] U3 = [np.sin(T), np.cos(T), T] X0 = 0 @@ -519,11 +517,11 @@ def test_nonsquare_bdalg(self): # Mismatch should generate exception args = (iosys_3i2o, iosys_3i2o) - self.assertRaises(ValueError, ct.series, *args) + with pytest.raises(ValueError): + ct.series(*args) - @unittest.skipIf(StrictVersion(sp.__version__) < "1.0", - "requires SciPy 1.0 or greater") - def test_discrete(self): + @noscipy0 + def test_discrete(self, tsys): """Test discrete time functionality""" # Create some linear and nonlinear systems to play with linsys = ct.StateSpace( @@ -531,7 +529,7 @@ def test_discrete(self): lnios = ios.LinearIOSystem(linsys) # Set up parameters for simulation - T, U, X0 = self.T, self.U, self.X0 + T, U, X0 = tsys.T, tsys.U, tsys.X0 # Simulate and compare to LTI output ios_t, ios_y = ios.input_output_response(lnios, T, U, X0) @@ -540,12 +538,12 @@ def test_discrete(self): np.testing.assert_allclose(ios_y, lin_y,atol=0.002,rtol=0.) # Test MIMO system, converted to discrete time - linsys = ct.StateSpace(self.mimo_linsys1) - linsys.dt = self.T[1] - self.T[0] + linsys = ct.StateSpace(tsys.mimo_linsys1) + linsys.dt = tsys.T[1] - tsys.T[0] lnios = ios.LinearIOSystem(linsys) # Set up parameters for simulation - T = self.T + T = tsys.T U = [np.sin(T), np.cos(T)] X0 = 0 @@ -555,13 +553,13 @@ def test_discrete(self): np.testing.assert_allclose(ios_t, lin_t,atol=0.002,rtol=0.) np.testing.assert_allclose(ios_y, lin_y,atol=0.002,rtol=0.) - def test_find_eqpts(self): + def test_find_eqpts(self, tsys): """Test find_eqpt function""" # Simple equilibrium point with no inputs nlsys = ios.NonlinearIOSystem(predprey) xeq, ueq, result = ios.find_eqpt( nlsys, [1.6, 1.2], None, return_result=True) - self.assertTrue(result.success) + assert result.success np.testing.assert_array_almost_equal(xeq, [1.64705879, 1.17923874]) np.testing.assert_array_almost_equal( nlsys._rhs(0, xeq, ueq), np.zeros((2,))) @@ -572,7 +570,7 @@ def test_find_eqpts(self): # Make sure the origin is a fixed point xeq, ueq, result = ios.find_eqpt( nlsys, [0, 0, 0, 0], [0, 4*9.8], return_result=True) - self.assertTrue(result.success) + assert result.success np.testing.assert_array_almost_equal( nlsys._rhs(0, xeq, ueq), np.zeros((4,))) np.testing.assert_array_almost_equal(xeq, [0, 0, 0, 0]) @@ -580,7 +578,7 @@ def test_find_eqpts(self): # Use a small lateral force to cause motion xeq, ueq, result = ios.find_eqpt( nlsys, [0, 0, 0, 0], [0.01, 4*9.8], return_result=True) - self.assertTrue(result.success) + assert result.success np.testing.assert_array_almost_equal( nlsys._rhs(0, xeq, ueq), np.zeros((4,)), decimal=5) @@ -588,7 +586,7 @@ def test_find_eqpts(self): xeq, ueq, result = ios.find_eqpt( nlsys, [0, 0, 0, 0], [0.01, 4*9.8], y0=[0.1, 0.1], return_result=True) - self.assertTrue(result.success) + assert result.success np.testing.assert_array_almost_equal( nlsys._out(0, xeq, ueq), [0.1, 0.1], decimal=5) np.testing.assert_array_almost_equal( @@ -598,7 +596,7 @@ def test_find_eqpts(self): xeq, ueq, result = ios.find_eqpt( nlsys, [0, 0, 0, 0], [0.01, 4*9.8], y0=[0.1, 0.1], iy = [0, 1], return_result=True) - self.assertTrue(result.success) + assert result.success np.testing.assert_array_almost_equal( nlsys._out(0, xeq, ueq), [0.1, 0.1], decimal=5) np.testing.assert_array_almost_equal( @@ -619,7 +617,7 @@ def test_find_eqpts(self): nlsys_full, [0, 0, 0, 0, 0, 0], [0.01, 4*9.8], y0=[0, 0, 0.1, 0.1, 0, 0], iy = [2, 3], idx=[2, 3, 4, 5], ix=[0, 1], return_result=True) - self.assertTrue(result.success) + assert result.success np.testing.assert_array_almost_equal( nlsys_full._out(0, xeq, ueq)[[2, 3]], [0.1, 0.1], decimal=5) np.testing.assert_array_almost_equal( @@ -631,7 +629,7 @@ def test_find_eqpts(self): nlsys_full, [0, 0, 0, 0, 0, 0], [0.01, 4*9.8], y0=[0, 0, 0.1, 0.1, 0, 0], iy=[3], iu=[1], idx=[2, 3, 4, 5], ix=[0, 1], return_result=True) - self.assertTrue(result.success) + assert result.success np.testing.assert_almost_equal(ueq[1], 4*9.8, decimal=5) np.testing.assert_array_almost_equal( nlsys_full._out(0, xeq, ueq)[[3]], [0.1], decimal=5) @@ -644,7 +642,7 @@ def test_find_eqpts(self): y0=[0, 0, 0, 0.1, 0, 0], iy=[3], dx0=[0.1, 0, 0, 0, 0, 0], idx=[1, 2, 3, 4, 5], ix=[0, 1], return_result=True) - self.assertTrue(result.success) + assert result.success np.testing.assert_array_almost_equal( nlsys_full._out(0, xeq, ueq)[-3:], [0.1, 0, 0], decimal=5) np.testing.assert_array_almost_equal( @@ -658,16 +656,15 @@ def test_find_eqpts(self): # If result is returned, user has to check xeq, ueq, result = ios.find_eqpt( lnios, [0, 0], [0], y0=[1], return_result=True) - self.assertFalse(result.success) + assert not result.success # If result is not returned, find_eqpt should return None xeq, ueq = ios.find_eqpt(lnios, [0, 0], [0], y0=[1]) - self.assertEqual(xeq, None) - self.assertEqual(ueq, None) + assert xeq is None + assert ueq is None - @unittest.skipIf(StrictVersion(sp.__version__) < "1.0", - "requires SciPy 1.0 or greater") - def test_params(self): + @noscipy0 + def test_params(self, tsys): # Start with the default set of parameters ios_secord_default = ios.NonlinearIOSystem( secord_update, secord_output, inputs=1, outputs=1, states=2) @@ -717,51 +714,41 @@ def test_params(self): np.testing.assert_array_almost_equal(w, [4j, -4j, 4j, -4j]) # Check for warning if we try to set params for LinearIOSystem - linsys = self.siso_linsys + linsys = tsys.siso_linsys iosys = ios.LinearIOSystem(linsys) - T, U, X0 = self.T, self.U, self.X0 + T, U, X0 = tsys.T, tsys.U, tsys.X0 lti_t, lti_y, lti_x = ct.forced_response(linsys, T, U, X0) - with warnings.catch_warnings(record=True) as warnval: - # Turn off deprecation warnings - warnings.simplefilter("ignore", category=DeprecationWarning) - warnings.simplefilter("ignore", category=PendingDeprecationWarning) - - # Trigger a warning + with pytest.warns(UserWarning, match="LinearIOSystem.*ignored"): ios_t, ios_y = ios.input_output_response( iosys, T, U, X0, params={'something':0}) - # Verify that we got a warning - self.assertEqual(len(warnval), 1) - self.assertTrue(issubclass(warnval[-1].category, UserWarning)) - self.assertTrue("LinearIOSystem" in str(warnval[-1].message)) - self.assertTrue("ignored" in str(warnval[-1].message)) # Check to make sure results are OK np.testing.assert_array_almost_equal(lti_t, ios_t) np.testing.assert_allclose(lti_y, ios_y,atol=0.002,rtol=0.) - def test_named_signals(self): + def test_named_signals(self, tsys): sys1 = ios.NonlinearIOSystem( updfcn = lambda t, x, u, params: np.array( - np.dot(self.mimo_linsys1.A, np.reshape(x, (-1, 1))) \ - + np.dot(self.mimo_linsys1.B, np.reshape(u, (-1, 1))) + np.dot(tsys.mimo_linsys1.A, np.reshape(x, (-1, 1))) \ + + np.dot(tsys.mimo_linsys1.B, np.reshape(u, (-1, 1))) ).reshape(-1,), outfcn = lambda t, x, u, params: np.array( - np.dot(self.mimo_linsys1.C, np.reshape(x, (-1, 1))) \ - + np.dot(self.mimo_linsys1.D, np.reshape(u, (-1, 1))) + np.dot(tsys.mimo_linsys1.C, np.reshape(x, (-1, 1))) \ + + np.dot(tsys.mimo_linsys1.D, np.reshape(u, (-1, 1))) ).reshape(-1,), inputs = ('u[0]', 'u[1]'), outputs = ('y[0]', 'y[1]'), - states = self.mimo_linsys1.states, + states = tsys.mimo_linsys1.states, name = 'sys1') - sys2 = ios.LinearIOSystem(self.mimo_linsys2, + sys2 = ios.LinearIOSystem(tsys.mimo_linsys2, inputs = ('u[0]', 'u[1]'), outputs = ('y[0]', 'y[1]'), name = 'sys2') # Series interconnection (sys1 * sys2) using __mul__ ios_mul = sys1 * sys2 - ss_series = self.mimo_linsys1 * self.mimo_linsys2 + ss_series = tsys.mimo_linsys1 * tsys.mimo_linsys2 lin_series = ct.linearize(ios_mul, 0, 0) np.testing.assert_array_almost_equal(ss_series.A, lin_series.A) np.testing.assert_array_almost_equal(ss_series.B, lin_series.B) @@ -770,7 +757,7 @@ def test_named_signals(self): # Series interconnection (sys1 * sys2) using series ios_series = ct.series(sys2, sys1) - ss_series = ct.series(self.mimo_linsys2, self.mimo_linsys1) + ss_series = ct.series(tsys.mimo_linsys2, tsys.mimo_linsys1) lin_series = ct.linearize(ios_series, 0, 0) np.testing.assert_array_almost_equal(ss_series.A, lin_series.A) np.testing.assert_array_almost_equal(ss_series.B, lin_series.B) @@ -803,36 +790,37 @@ def test_named_signals(self): inplist=('sys1.u[0]', 'sys1.u[1]'), outlist=('sys2.u[0]', 'sys2.u[1]') # = sys1.y[0], sys1.y[1] ) - ss_feedback = ct.feedback(self.mimo_linsys1, self.mimo_linsys2) + ss_feedback = ct.feedback(tsys.mimo_linsys1, tsys.mimo_linsys2) lin_feedback = ct.linearize(ios_connect, 0, 0) np.testing.assert_array_almost_equal(ss_feedback.A, lin_feedback.A) np.testing.assert_array_almost_equal(ss_feedback.B, lin_feedback.B) np.testing.assert_array_almost_equal(ss_feedback.C, lin_feedback.C) np.testing.assert_array_almost_equal(ss_feedback.D, lin_feedback.D) - def test_sys_naming_convention(self): - """Enforce generic system names 'sys[i]' to be present when systems are created - without explicit names.""" + def test_sys_naming_convention(self, tsys): + """Enforce generic system names 'sys[i]' to be present when systems are + created without explicit names.""" ct.InputOutputSystem.idCounter = 0 - sys = ct.LinearIOSystem(self.mimo_linsys1) - self.assertEquals(sys.name, "sys[0]") - self.assertEquals(sys.copy().name, "copy of sys[0]") - + sys = ct.LinearIOSystem(tsys.mimo_linsys1) + + assert sys.name == "sys[0]" + assert sys.copy().name == "copy of sys[0]" + namedsys = ios.NonlinearIOSystem( - updfcn = lambda t, x, u, params: x, - outfcn = lambda t, x, u, params: u, - inputs = ('u[0]', 'u[1]'), - outputs = ('y[0]', 'y[1]'), - states = self.mimo_linsys1.states, - name = 'namedsys') + updfcn=lambda t, x, u, params: x, + outfcn=lambda t, x, u, params: u, + inputs=('u[0]', 'u[1]'), + outputs=('y[0]', 'y[1]'), + states=tsys.mimo_linsys1.states, + name='namedsys') unnamedsys1 = ct.NonlinearIOSystem( - lambda t,x,u,params: x, inputs=2, outputs=2, states=2 + lambda t, x, u, params: x, inputs=2, outputs=2, states=2 ) unnamedsys2 = ct.NonlinearIOSystem( - None, lambda t,x,u,params: u, inputs=2, outputs=2 + None, lambda t, x, u, params: u, inputs=2, outputs=2 ) - self.assertEquals(unnamedsys2.name, "sys[2]") + assert unnamedsys2.name == "sys[2]" # Unnamed/unnamed connections uu_series = unnamedsys1 * unnamedsys2 @@ -840,34 +828,33 @@ def test_sys_naming_convention(self): u_neg = - unnamedsys1 uu_feedback = unnamedsys2.feedback(unnamedsys1) uu_dup = unnamedsys1 * unnamedsys1.copy() - uu_hierarchical = uu_series*unnamedsys1 + uu_hierarchical = uu_series * unnamedsys1 - self.assertEquals(uu_series.name, "sys[3]") - self.assertEquals(uu_parallel.name, "sys[4]") - self.assertEquals(u_neg.name, "sys[5]") - self.assertEquals(uu_feedback.name, "sys[6]") - self.assertEquals(uu_dup.name, "sys[7]") - self.assertEquals(uu_hierarchical.name, "sys[8]") + assert uu_series.name == "sys[3]" + assert uu_parallel.name == "sys[4]" + assert u_neg.name == "sys[5]" + assert uu_feedback.name == "sys[6]" + assert uu_dup.name == "sys[7]" + assert uu_hierarchical.name == "sys[8]" # Unnamed/named connections un_series = unnamedsys1 * namedsys un_parallel = unnamedsys1 + namedsys un_feedback = unnamedsys2.feedback(namedsys) un_dup = unnamedsys1 * namedsys.copy() - un_hierarchical = uu_series*unnamedsys1 + un_hierarchical = uu_series * unnamedsys1 - self.assertEquals(un_series.name, "sys[9]") - self.assertEquals(un_parallel.name, "sys[10]") - self.assertEquals(un_feedback.name, "sys[11]") - self.assertEquals(un_dup.name, "sys[12]") - self.assertEquals(un_hierarchical.name, "sys[13]") + assert un_series.name == "sys[9]" + assert un_parallel.name == "sys[10]" + assert un_feedback.name == "sys[11]" + assert un_dup.name == "sys[12]" + assert un_hierarchical.name == "sys[13]" # Same system conflict - with warnings.catch_warnings(record=True) as warnval: + with pytest.warns(UserWarning): unnamedsys1 * unnamedsys1 - self.assertEqual(len(warnval), 1) - def test_signals_naming_convention(self): + def test_signals_naming_convention(self, tsys): """Enforce generic names to be present when systems are created without explicit signal names: input: 'u[i]' @@ -875,30 +862,30 @@ def test_signals_naming_convention(self): output: 'y[i]' """ ct.InputOutputSystem.idCounter = 0 - sys = ct.LinearIOSystem(self.mimo_linsys1) + sys = ct.LinearIOSystem(tsys.mimo_linsys1) for statename in ["x[0]", "x[1]"]: - self.assertTrue(statename in sys.state_index) + assert statename in sys.state_index for inputname in ["u[0]", "u[1]"]: - self.assertTrue(inputname in sys.input_index) + assert inputname in sys.input_index for outputname in ["y[0]", "y[1]"]: - self.assertTrue(outputname in sys.output_index) - self.assertEqual(len(sys.state_index), sys.nstates) - self.assertEqual(len(sys.input_index), sys.ninputs) - self.assertEqual(len(sys.output_index), sys.noutputs) + assert outputname in sys.output_index + assert len(sys.state_index) == sys.nstates + assert len(sys.input_index) == sys.ninputs + assert len(sys.output_index) == sys.noutputs namedsys = ios.NonlinearIOSystem( - updfcn = lambda t, x, u, params: x, - outfcn = lambda t, x, u, params: u, - inputs = ('u0'), - outputs = ('y0'), - states = ('x0'), - name = 'namedsys') + updfcn=lambda t, x, u, params: x, + outfcn=lambda t, x, u, params: u, + inputs=('u0'), + outputs=('y0'), + states=('x0'), + name='namedsys') unnamedsys = ct.NonlinearIOSystem( - lambda t,x,u,params: x, inputs=1, outputs=1, states=1 + lambda t, x, u, params: x, inputs=1, outputs=1, states=1 ) - self.assertTrue('u0' in namedsys.input_index) - self.assertTrue('y0' in namedsys.output_index) - self.assertTrue('x0' in namedsys.state_index) + assert 'u0' in namedsys.input_index + assert 'y0' in namedsys.output_index + assert 'x0' in namedsys.state_index # Unnamed/named connections un_series = unnamedsys * namedsys @@ -908,26 +895,25 @@ def test_signals_naming_convention(self): un_hierarchical = un_series*unnamedsys u_neg = - unnamedsys - self.assertTrue("sys[1].x[0]" in un_series.state_index) - self.assertTrue("namedsys.x0" in un_series.state_index) - self.assertTrue("sys[1].x[0]" in un_parallel.state_index) - self.assertTrue("namedsys.x0" in un_series.state_index) - self.assertTrue("sys[1].x[0]" in un_feedback.state_index) - self.assertTrue("namedsys.x0" in un_feedback.state_index) - self.assertTrue("sys[1].x[0]" in un_dup.state_index) - self.assertTrue("copy of namedsys.x0" in un_dup.state_index) - self.assertTrue("sys[1].x[0]" in un_hierarchical.state_index) - self.assertTrue("sys[2].sys[1].x[0]" in un_hierarchical.state_index) - self.assertTrue("sys[1].x[0]" in u_neg.state_index) + assert "sys[1].x[0]" in un_series.state_index + assert "namedsys.x0" in un_series.state_index + assert "sys[1].x[0]" in un_parallel.state_index + assert "namedsys.x0" in un_series.state_index + assert "sys[1].x[0]" in un_feedback.state_index + assert "namedsys.x0" in un_feedback.state_index + assert "sys[1].x[0]" in un_dup.state_index + assert "copy of namedsys.x0" in un_dup.state_index + assert "sys[1].x[0]" in un_hierarchical.state_index + assert "sys[2].sys[1].x[0]" in un_hierarchical.state_index + assert "sys[1].x[0]" in u_neg.state_index # Same system conflict - with warnings.catch_warnings(record=True) as warnval: + with pytest.warns(UserWarning): same_name_series = unnamedsys * unnamedsys - self.assertEquals(len(warnval), 1) - self.assertTrue("sys[1].x[0]" in same_name_series.state_index) - self.assertTrue("copy of sys[1].x[0]" in same_name_series.state_index) + assert "sys[1].x[0]" in same_name_series.state_index + assert "copy of sys[1].x[0]" in same_name_series.state_index - def test_named_signals_linearize_inconsistent(self): + def test_named_signals_linearize_inconsistent(self, tsys): """Mare sure that providing inputs or outputs not consistent with updfcn or outfcn fail """ @@ -935,15 +921,15 @@ def test_named_signals_linearize_inconsistent(self): def updfcn(t, x, u, params): """2 inputs, 2 states""" return np.array( - np.dot(self.mimo_linsys1.A, np.reshape(x, (-1, 1))) - + np.dot(self.mimo_linsys1.B, np.reshape(u, (-1, 1))) + np.dot(tsys.mimo_linsys1.A, np.reshape(x, (-1, 1))) + + np.dot(tsys.mimo_linsys1.B, np.reshape(u, (-1, 1))) ).reshape(-1,) def outfcn(t, x, u, params): """2 states, 2 outputs""" return np.array( - self.mimo_linsys1.C * np.reshape(x, (-1, 1)) - + self.mimo_linsys1.D * np.reshape(u, (-1, 1)) + tsys.mimo_linsys1.C * np.reshape(x, (-1, 1)) + + tsys.mimo_linsys1.D * np.reshape(u, (-1, 1)) ).reshape(-1,) for inputs, outputs in [ @@ -955,46 +941,48 @@ def outfcn(t, x, u, params): outfcn=outfcn, inputs=inputs, outputs=outputs, - states=self.mimo_linsys1.states, + states=tsys.mimo_linsys1.states, name='sys1') - self.assertRaises(ValueError, sys1.linearize, [0, 0], [0, 0]) + with pytest.raises(ValueError): + sys1.linearize([0, 0], [0, 0]) sys2 = ios.NonlinearIOSystem(updfcn=updfcn, outfcn=outfcn, inputs=('u[0]', 'u[1]'), outputs=('y[0]', 'y[1]'), - states=self.mimo_linsys1.states, + states=tsys.mimo_linsys1.states, name='sys1') for x0, u0 in [([0], [0, 0]), ([0, 0, 0], [0, 0]), ([0, 0], [0]), ([0, 0], [0, 0, 0])]: - self.assertRaises(ValueError, sys2.linearize, x0, u0) + with pytest.raises(ValueError): + sys2.linearize(x0, u0) - def test_lineariosys_statespace(self): + def test_lineariosys_statespace(self, tsys): """Make sure that a LinearIOSystem is also a StateSpace object""" - iosys_siso = ct.LinearIOSystem(self.siso_linsys) - self.assertTrue(isinstance(iosys_siso, ct.StateSpace)) + iosys_siso = ct.LinearIOSystem(tsys.siso_linsys) + assert isinstance(iosys_siso, ct.StateSpace) # Make sure that state space functions work for LinearIOSystems np.testing.assert_array_equal( - iosys_siso.pole(), self.siso_linsys.pole()) + iosys_siso.pole(), tsys.siso_linsys.pole()) omega = np.logspace(.1, 10, 100) mag_io, phase_io, omega_io = iosys_siso.freqresp(omega) - mag_ss, phase_ss, omega_ss = self.siso_linsys.freqresp(omega) + mag_ss, phase_ss, omega_ss = tsys.siso_linsys.freqresp(omega) np.testing.assert_array_equal(mag_io, mag_ss) np.testing.assert_array_equal(phase_io, phase_ss) np.testing.assert_array_equal(omega_io, omega_ss) # LinearIOSystem methods should override StateSpace methods io_mul = iosys_siso * iosys_siso - self.assertTrue(isinstance(io_mul, ct.InputOutputSystem)) + assert isinstance(io_mul, ct.InputOutputSystem) # But also retain linear structure - self.assertTrue(isinstance(io_mul, ct.StateSpace)) + assert isinstance(io_mul, ct.StateSpace) # And make sure the systems match - ss_series = self.siso_linsys * self.siso_linsys + ss_series = tsys.siso_linsys * tsys.siso_linsys np.testing.assert_array_equal(io_mul.A, ss_series.A) np.testing.assert_array_equal(io_mul.B, ss_series.B) np.testing.assert_array_equal(io_mul.C, ss_series.C) @@ -1002,8 +990,8 @@ def test_lineariosys_statespace(self): # Make sure that series does the same thing io_series = ct.series(iosys_siso, iosys_siso) - self.assertTrue(isinstance(io_series, ct.InputOutputSystem)) - self.assertTrue(isinstance(io_series, ct.StateSpace)) + assert isinstance(io_series, ct.InputOutputSystem) + assert isinstance(io_series, ct.StateSpace) np.testing.assert_array_equal(io_series.A, ss_series.A) np.testing.assert_array_equal(io_series.B, ss_series.B) np.testing.assert_array_equal(io_series.C, ss_series.C) @@ -1011,77 +999,65 @@ def test_lineariosys_statespace(self): # Test out feedback as well io_feedback = ct.feedback(iosys_siso, iosys_siso) - self.assertTrue(isinstance(io_series, ct.InputOutputSystem)) + assert isinstance(io_series, ct.InputOutputSystem) # But also retain linear structure - self.assertTrue(isinstance(io_series, ct.StateSpace)) + assert isinstance(io_series, ct.StateSpace) # And make sure the systems match - ss_feedback = ct.feedback(self.siso_linsys, self.siso_linsys) + ss_feedback = ct.feedback(tsys.siso_linsys, tsys.siso_linsys) np.testing.assert_array_equal(io_feedback.A, ss_feedback.A) np.testing.assert_array_equal(io_feedback.B, ss_feedback.B) np.testing.assert_array_equal(io_feedback.C, ss_feedback.C) np.testing.assert_array_equal(io_feedback.D, ss_feedback.D) - def test_duplicates(self): - nlios = ios.NonlinearIOSystem(lambda t,x,u,params: x, \ - lambda t, x, u, params: u*u, \ - inputs=1, outputs=1, states=1, name="sys") - - # Turn off deprecation warnings - warnings.simplefilter("ignore", category=DeprecationWarning) - warnings.simplefilter("ignore", category=PendingDeprecationWarning) + def test_duplicates(self, tsys): + nlios = ios.NonlinearIOSystem(lambda t, x, u, params: x, + lambda t, x, u, params: u * u, + inputs=1, outputs=1, states=1, + name="sys") # Duplicate objects - with warnings.catch_warnings(record=True) as warnval: - # Trigger a warning + with pytest.warns(UserWarning, match="Duplicate object"): ios_series = nlios * nlios - # Verify that we got a warning - self.assertEqual(len(warnval), 1) - self.assertTrue(issubclass(warnval[-1].category, UserWarning)) - self.assertTrue("Duplicate object" in str(warnval[-1].message)) - # Nonduplicate objects nlios1 = nlios.copy() nlios2 = nlios.copy() - with warnings.catch_warnings(record=True) as warnval: + with pytest.warns(UserWarning, match="copy of sys") as record: ios_series = nlios1 * nlios2 - self.assertEquals(len(warnval), 1) - # when subsystems have the same name, duplicates are - # renamed - self.assertTrue("copy of sys_1.x[0]" in ios_series.state_index.keys()) - self.assertTrue("copy of sys.x[0]" in ios_series.state_index.keys()) + assert "copy of sys_1.x[0]" in ios_series.state_index.keys() + assert "copy of sys.x[0]" in ios_series.state_index.keys() # Duplicate names - iosys_siso = ct.LinearIOSystem(self.siso_linsys) - nlios1 = ios.NonlinearIOSystem(None, \ - lambda t, x, u, params: u*u, inputs=1, outputs=1, name="sys") - nlios2 = ios.NonlinearIOSystem(None, \ - lambda t, x, u, params: u*u, inputs=1, outputs=1, name="sys") - with warnings.catch_warnings(record=True) as warnval: - # Trigger a warning - iosys = ct.InterconnectedSystem( - (nlios1, iosys_siso, nlios2), inputs=0, outputs=0, states=0) - - # Verify that we got a warning - self.assertEqual(len(warnval), 1) - self.assertTrue(issubclass(warnval[-1].category, UserWarning)) - self.assertTrue("Duplicate name" in str(warnval[-1].message)) + iosys_siso = ct.LinearIOSystem(tsys.siso_linsys) + nlios1 = ios.NonlinearIOSystem(None, + lambda t, x, u, params: u * u, + inputs=1, outputs=1, name="sys") + nlios2 = ios.NonlinearIOSystem(None, + lambda t, x, u, params: u * u, + inputs=1, outputs=1, name="sys") + + with pytest.warns(UserWarning, match="Duplicate name"): + ct.InterconnectedSystem((nlios1, iosys_siso, nlios2), + inputs=0, outputs=0, states=0) # Same system, different names => everything should be OK - nlios1 = ios.NonlinearIOSystem(None, \ - lambda t, x, u, params: u*u, inputs=1, outputs=1, name="nlios1") - nlios2 = ios.NonlinearIOSystem(None, \ - lambda t, x, u, params: u*u, inputs=1, outputs=1, name="nlios2") - with warnings.catch_warnings(record=True) as warnval: - iosys = ct.InterconnectedSystem( - (nlios1, iosys_siso, nlios2), inputs=0, outputs=0, states=0) - self.assertEqual(len(warnval), 0) + nlios1 = ios.NonlinearIOSystem(None, + lambda t, x, u, params: u * u, + inputs=1, outputs=1, name="nlios1") + nlios2 = ios.NonlinearIOSystem(None, + lambda t, x, u, params: u * u, + inputs=1, outputs=1, name="nlios2") + with pytest.warns(None) as record: + ct.InterconnectedSystem((nlios1, iosys_siso, nlios2), + inputs=0, outputs=0, states=0) + if record: + pytest.fail("Warning not expected: " + record[0].message) -# Predator prey dynamics def predprey(t, x, u, params={}): + """Predator prey dynamics""" r = params.get('r', 2) d = params.get('d', 0.7) b = params.get('b', 0.3) @@ -1096,8 +1072,8 @@ def predprey(t, x, u, params={}): return np.array([dx0, dx1]) -# Reduced planar vertical takeoff and landing dynamics def pvtol(t, x, u, params={}): + """Reduced planar vertical takeoff and landing dynamics""" from math import sin, cos m = params.get('m', 4.) # kg, system mass J = params.get('J', 0.0475) # kg m^2, system inertia @@ -1112,6 +1088,7 @@ def pvtol(t, x, u, params={}): -l/J * sin(x[0]) + r/J * u[0] ]) + def pvtol_full(t, x, u, params={}): from math import sin, cos m = params.get('m', 4.) # kg, system mass @@ -1128,8 +1105,9 @@ def pvtol_full(t, x, u, params={}): ]) -# Second order system dynamics + def secord_update(t, x, u, params={}): + """Second order system dynamics""" omega0 = params.get('omega0', 1.) zeta = params.get('zeta', 0.5) u = np.array(u, ndmin=1) @@ -1137,9 +1115,8 @@ def secord_update(t, x, u, params={}): x[1], -2 * zeta * omega0 * x[1] - omega0*omega0 * x[0] + u[0] ]) -def secord_output(t, x, u, params={}): - return np.array([x[0]]) -if __name__ == '__main__': - unittest.main() +def secord_output(t, x, u, params={}): + """Second order system dynamics output""" + return np.array([x[0]]) From 521e927b73114fa77edd7ada0face6a779f23e09 Mon Sep 17 00:00:00 2001 From: bnavigator Date: Sun, 26 Jul 2020 19:31:28 +0200 Subject: [PATCH 14/30] pytestify lti_test.py --- control/tests/lti_test.py | 122 +++++++++++++++++++++++++++++++------- 1 file changed, 101 insertions(+), 21 deletions(-) diff --git a/control/tests/lti_test.py b/control/tests/lti_test.py index ed832fb05..ee9d95a09 100644 --- a/control/tests/lti_test.py +++ b/control/tests/lti_test.py @@ -1,14 +1,16 @@ -#!/usr/bin/env python +"""lti_test.py""" -import unittest import numpy as np -from control.lti import * -from control.xferfcn import tf -from control import c2d -from control.matlab import tf2ss -from control.exception import slycot_check +import pytest + +from control import c2d, tf, tf2ss, NonlinearIOSystem +from control.lti import (LTI, common_timebase, damp, dcgain, isctime, isdtime, + issiso, pole, timebaseEqual, zero) +from control.tests.conftest import slycotonly + + +class TestLTI: -class TestUtils(unittest.TestCase): def test_pole(self): sys = tf(126, [-1, 42]) np.testing.assert_equal(sys.pole(), 42) @@ -20,31 +22,32 @@ def test_zero(self): np.testing.assert_equal(zero(sys), 42) def test_issiso(self): - self.assertEqual(issiso(1), True) - self.assertRaises(ValueError, issiso, 1, strict=True) + assert issiso(1) + with pytest.raises(ValueError): + issiso(1, strict=True) # SISO transfer function sys = tf([-1, 42], [1, 10]) - self.assertEqual(issiso(sys), True) - self.assertEqual(issiso(sys, strict=True), True) + assert issiso(sys) + assert issiso(sys, strict=True) # SISO state space system sys = tf2ss(sys) - self.assertEqual(issiso(sys), True) - self.assertEqual(issiso(sys, strict=True), True) + assert issiso(sys) + assert issiso(sys, strict=True) - @unittest.skipIf(not slycot_check(), "slycot not installed") + @slycotonly def test_issiso_mimo(self): # MIMO transfer function sys = tf([[[-1, 41], [1]], [[1, 2], [3, 4]]], [[[1, 10], [1, 20]], [[1, 30], [1, 40]]]); - self.assertEqual(issiso(sys), False) - self.assertEqual(issiso(sys, strict=True), False) + assert not issiso(sys) + assert not issiso(sys, strict=True) # MIMO state space system sys = tf2ss(sys) - self.assertEqual(issiso(sys), False) - self.assertEqual(issiso(sys, strict=True), False) + assert not issiso(sys) + assert not issiso(sys, strict=True) def test_damp(self): # Test the continuous time case. @@ -70,6 +73,83 @@ def test_dcgain(self): np.testing.assert_equal(sys.dcgain(), 42) np.testing.assert_equal(dcgain(sys), 42) + @pytest.mark.parametrize("dt1, dt2, expected", + [(None, None, True), + (None, 0, True), + (None, 1, True), + pytest.param(None, True, True, + marks=pytest.mark.xfail( + reason="returns false")), + (0, 0, True), + (0, 1, False), + (0, True, False), + (1, 1, True), + (1, 2, False), + (1, True, False), + (True, True, True)]) + def test_timebaseEqual_deprecated(self, dt1, dt2, expected): + """Test that timbaseEqual throws a warning and returns as documented""" + sys1 = tf([1], [1, 2, 3], dt1) + sys2 = tf([1], [1, 4, 5], dt2) + + print(sys1.dt) + print(sys2.dt) + + with pytest.deprecated_call(): + assert timebaseEqual(sys1, sys2) is expected + # Make sure behaviour is symmetric + with pytest.deprecated_call(): + assert timebaseEqual(sys2, sys1) is expected + + @pytest.mark.parametrize("dt1, dt2, expected", + [(None, None, None), + (None, 0, 0), + (None, 1, 1), + (None, True, True), + (True, True, True), + (True, 1, 1), + (1, 1, 1), + (0, 0, 0), + ]) + @pytest.mark.parametrize("sys1", [True, False]) + @pytest.mark.parametrize("sys2", [True, False]) + def test_common_timebase(self, dt1, dt2, expected, sys1, sys2): + """Test that common_timbase adheres to :ref:`conventions-ref`""" + i1 = tf([1], [1, 2, 3], dt1) if sys1 else dt1 + i2 = tf([1], [1, 4, 5], dt2) if sys2 else dt2 + assert common_timebase(i1, i2) == expected + # Make sure behaviour is symmetric + assert common_timebase(i2, i1) == expected + + @pytest.mark.parametrize("i1, i2", + [(True, 0), + (0, 1), + (1, 2)]) + def test_common_timebase_errors(self, i1, i2): + """Test that common_timbase throws errors on invalid combinations""" + with pytest.raises(ValueError): + common_timebase(i1, i2) + # Make sure behaviour is symmetric + with pytest.raises(ValueError): + common_timebase(i2, i1) + + @pytest.mark.parametrize("dt, ref, strictref", + [(None, True, False), + (0, False, False), + (1, True, True), + (True, True, True)]) + @pytest.mark.parametrize("objfun, arg", + [(LTI, ()), + (NonlinearIOSystem, (lambda x: x, ))]) + def test_isdtime(self, objfun, arg, dt, ref, strictref): + """Test isdtime and isctime functions to follow convention""" + obj = objfun(*arg, dt=dt) + + assert isdtime(obj) == ref + assert isdtime(obj, strict=True) == strictref -if __name__ == "__main__": - unittest.main() + if dt is not None: + ref = not ref + strictref = not strictref + assert isctime(obj) == ref + assert isctime(obj, strict=True) == strictref From 2c31029f32414683c3798bb74a44cac5370ea68c Mon Sep 17 00:00:00 2001 From: bnavigator Date: Tue, 29 Dec 2020 02:17:28 +0100 Subject: [PATCH 15/30] pytestify mateqn_test --- control/tests/mateqn_test.py | 166 ++++++++++++++++++++--------------- 1 file changed, 93 insertions(+), 73 deletions(-) diff --git a/control/tests/mateqn_test.py b/control/tests/mateqn_test.py index 29f31c853..facb1ce08 100644 --- a/control/tests/mateqn_test.py +++ b/control/tests/mateqn_test.py @@ -1,15 +1,6 @@ -#!/usr/bin/env python -from __future__ import print_function -# -# mateqn_test.py - test wuit for matrix equation solvers -# -#! Currently uses numpy.testing framework; will dump you out of unittest -#! if an error occurs. Should figure out the right way to fix this. +"""mateqn_test.py - test suite for matrix equation solvers -""" Test cases for lyap, dlyap, care and dare functions in the file -pyctrl_lin_alg.py. """ - -"""Copyright (c) 2011, All rights reserved. +Copyright (c) 2020, All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions @@ -42,18 +33,18 @@ Author: Bjorn Olofsson """ -import unittest -from numpy import array -from numpy.testing import assert_array_almost_equal, assert_array_less, \ - assert_raises -# need scipy version of eigvals for generalized eigenvalue problem +from numpy import array, zeros +from numpy.testing import assert_array_almost_equal, assert_array_less +import pytest from scipy.linalg import eigvals, solve -from scipy import zeros,dot -from control.mateqn import lyap,dlyap,care,dare -from control.exception import slycot_check, ControlArgument -@unittest.skipIf(not slycot_check(), "slycot not installed") -class TestMatrixEquations(unittest.TestCase): +from control.mateqn import lyap, dlyap, care, dare +from control.exception import ControlArgument +from control.tests.conftest import slycotonly + + +@slycotonly +class TestMatrixEquations: """These are tests for the matrix equation solvers in mateqn.py""" def test_lyap(self): @@ -90,7 +81,8 @@ def test_lyap_g(self): E = array([[1,2],[2,1]]) X = lyap(A,Q,None,E) # print("The solution obtained is ", X) - assert_array_almost_equal(A.dot(X).dot(E.T) + E.dot(X).dot(A.T) + Q, zeros((2,2))) + assert_array_almost_equal(A.dot(X).dot(E.T) + E.dot(X).dot(A.T) + Q, + zeros((2,2))) def test_dlyap(self): A = array([[-0.6, 0],[-0.1, -0.4]]) @@ -111,7 +103,8 @@ def test_dlyap_g(self): E = array([[1, 1],[2, 1]]) X = dlyap(A,Q,None,E) # print("The solution obtained is ", X) - assert_array_almost_equal(A.dot(X).dot(A.T) - E.dot(X).dot(E.T) + Q, zeros((2,2))) + assert_array_almost_equal(A.dot(X).dot(A.T) - E.dot(X).dot(E.T) + Q, + zeros((2,2))) def test_dlyap_sylvester(self): A = 5 @@ -135,7 +128,8 @@ def test_care(self): X,L,G = care(A,B,Q) # print("The solution obtained is", X) - assert_array_almost_equal(A.T.dot(X) + X.dot(A) - X.dot(B).dot(B.T).dot(X) + Q, + M = A.T.dot(X) + X.dot(A) - X.dot(B).dot(B.T).dot(X) + Q + assert_array_almost_equal(M, zeros((2,2))) assert_array_almost_equal(B.T.dot(X), G) @@ -156,6 +150,7 @@ def test_care_g(self): - (E.T.dot(X).dot(B) + S).dot(Gref) + Q, zeros((2,2))) + def test_care_g2(self): A = array([[-2, -1],[-1, -1]]) Q = array([[0, 0],[0, 1]]) B = array([[1],[0]]) @@ -183,9 +178,7 @@ def test_dare(self): Gref = solve(B.T.dot(X).dot(B) + R, B.T.dot(X).dot(A)) assert_array_almost_equal(Gref, G) assert_array_almost_equal( - A.T.dot(X).dot(A) - X - - A.T.dot(X).dot(B).dot(Gref) + Q, - zeros((2,2))) + X, A.T.dot(X).dot(A) - A.T.dot(X).dot(B).dot(Gref) + Q) # check for stable closed loop lam = eigvals(A - B.dot(G)) assert_array_less(abs(lam), 1.0) @@ -197,10 +190,13 @@ def test_dare(self): X,L,G = dare(A,B,Q,R) # print("The solution obtained is", X) + AtXA = A.T.dot(X).dot(A) + AtXB = A.T.dot(X).dot(B) + BtXA = B.T.dot(X).dot(A) + BtXB = B.T.dot(X).dot(B) assert_array_almost_equal( - A.T.dot(X).dot(A) - X - - A.T.dot(X).dot(B) * solve(B.T.dot(X).dot(B) + R, B.T.dot(X).dot(A)) + Q, zeros((2,2))) - assert_array_almost_equal(B.T.dot(X).dot(A) / (B.T.dot(X).dot(B) + R), G) + X, AtXA - AtXB.dot(solve(BtXB + R, BtXA)) + Q) + assert_array_almost_equal(BtXA / (BtXB + R), G) # check for stable closed loop lam = eigvals(A - B.dot(G)) assert_array_less(abs(lam), 1.0) @@ -216,29 +212,32 @@ def test_dare_g(self): X,L,G = dare(A,B,Q,R,S,E) # print("The solution obtained is", X) Gref = solve(B.T.dot(X).dot(B) + R, B.T.dot(X).dot(A) + S.T) - assert_array_almost_equal(Gref,G) + assert_array_almost_equal(Gref, G) assert_array_almost_equal( - A.T.dot(X).dot(A) - E.T.dot(X).dot(E) - - (A.T.dot(X).dot(B) + S).dot(Gref) + Q, - zeros((2,2)) ) + E.T.dot(X).dot(E), + A.T.dot(X).dot(A) - (A.T.dot(X).dot(B) + S).dot(Gref) + Q) # check for stable closed loop lam = eigvals(A - B.dot(G), E) assert_array_less(abs(lam), 1.0) - A = array([[-0.6, 0],[-0.1, -0.4]]) - Q = array([[2, 1],[1, 3]]) - B = array([[1],[2]]) + def test_dare_g2(self): + A = array([[-0.6, 0], [-0.1, -0.4]]) + Q = array([[2, 1], [1, 3]]) + B = array([[1], [2]]) R = 1 - S = array([[1],[2]]) - E = array([[2, 1],[1, 2]]) + S = array([[1], [2]]) + E = array([[2, 1], [1, 2]]) - X,L,G = dare(A,B,Q,R,S,E) + X, L, G = dare(A, B, Q, R, S, E) # print("The solution obtained is", X) + AtXA = A.T.dot(X).dot(A) + AtXB = A.T.dot(X).dot(B) + BtXA = B.T.dot(X).dot(A) + BtXB = B.T.dot(X).dot(B) + EtXE = E.T.dot(X).dot(E) assert_array_almost_equal( - A.T.dot(X).dot(A) - E.T.dot(X).dot(E) - - (A.T.dot(X).dot(B) + S).dot(solve(B.T.dot(X).dot(B) + R, B.T.dot(X).dot(A) + S.T)) + Q, - zeros((2,2)) ) - assert_array_almost_equal((B.T.dot(X).dot(A) + S.T) / (B.T.dot(X).dot(B) + R), G) + EtXE, AtXA - (AtXB + S).dot(solve(BtXB + R, BtXA + S.T)) + Q) + assert_array_almost_equal((BtXA + S.T) / (BtXB + R), G) # check for stable closed loop lam = eigvals(A - B.dot(G), E) assert_array_less(abs(lam), 1.0) @@ -260,16 +259,26 @@ def test_raise(self): Efq = array([[2, 1, 0], [1, 2, 0]]) for cdlyap in [lyap, dlyap]: - assert_raises(ControlArgument, cdlyap, Afq, Q) - assert_raises(ControlArgument, cdlyap, A, Qfq) - assert_raises(ControlArgument, cdlyap, A, Qfs) - assert_raises(ControlArgument, cdlyap, Afq, Q, C) - assert_raises(ControlArgument, cdlyap, A, Qfq, C) - assert_raises(ControlArgument, cdlyap, A, Q, Cfd) - assert_raises(ControlArgument, cdlyap, A, Qfq, None, E) - assert_raises(ControlArgument, cdlyap, A, Q, None, Efq) - assert_raises(ControlArgument, cdlyap, A, Qfs, None, E) - assert_raises(ControlArgument, cdlyap, A, Q, C, E) + with pytest.raises(ControlArgument): + cdlyap(Afq, Q) + with pytest.raises(ControlArgument): + cdlyap(A, Qfq) + with pytest.raises(ControlArgument): + cdlyap(A, Qfs) + with pytest.raises(ControlArgument): + cdlyap(Afq, Q, C) + with pytest.raises(ControlArgument): + cdlyap(A, Qfq, C) + with pytest.raises(ControlArgument): + cdlyap(A, Q, Cfd) + with pytest.raises(ControlArgument): + cdlyap(A, Qfq, None, E) + with pytest.raises(ControlArgument): + cdlyap(A, Q, None, Efq) + with pytest.raises(ControlArgument): + cdlyap(A, Qfs, None, E) + with pytest.raises(ControlArgument): + cdlyap(A, Q, C, E) B = array([[1, 0], [0, 1]]) Bf = array([[1, 0], [0, 1], [1, 1]]) @@ -281,23 +290,34 @@ def test_raise(self): E = array([[2, 1], [1, 2]]) Ef = array([[2, 1], [1, 2], [1, 2]]) - assert_raises(ControlArgument, care, Afq, B, Q) - assert_raises(ControlArgument, care, A, B, Qfq) - assert_raises(ControlArgument, care, A, Bf, Q) - assert_raises(ControlArgument, care, 1, B, 1) - assert_raises(ControlArgument, care, A, B, Qfs) - assert_raises(ValueError, dare, A, B, Q, Rfs) + with pytest.raises(ControlArgument): + care(Afq, B, Q) + with pytest.raises(ControlArgument): + care(A, B, Qfq) + with pytest.raises(ControlArgument): + care(A, Bf, Q) + with pytest.raises(ControlArgument): + care(1, B, 1) + with pytest.raises(ControlArgument): + care(A, B, Qfs) + with pytest.raises(ValueError): + dare(A, B, Q, Rfs) for cdare in [care, dare]: - assert_raises(ControlArgument, cdare, Afq, B, Q, R, S, E) - assert_raises(ControlArgument, cdare, A, B, Qfq, R, S, E) - assert_raises(ControlArgument, cdare, A, Bf, Q, R, S, E) - assert_raises(ControlArgument, cdare, A, B, Q, R, S, Ef) - assert_raises(ControlArgument, cdare, A, B, Q, Rfq, S, E) - assert_raises(ControlArgument, cdare, A, B, Q, R, Sf, E) - assert_raises(ControlArgument, cdare, A, B, Qfs, R, S, E) - assert_raises(ControlArgument, cdare, A, B, Q, Rfs, S, E) - assert_raises(ControlArgument, cdare, A, B, Q, R, S) - - -if __name__ == "__main__": - unittest.main() + with pytest.raises(ControlArgument): + cdare(Afq, B, Q, R, S, E) + with pytest.raises(ControlArgument): + cdare(A, B, Qfq, R, S, E) + with pytest.raises(ControlArgument): + cdare(A, Bf, Q, R, S, E) + with pytest.raises(ControlArgument): + cdare(A, B, Q, R, S, Ef) + with pytest.raises(ControlArgument): + cdare(A, B, Q, Rfq, S, E) + with pytest.raises(ControlArgument): + cdare(A, B, Q, R, Sf, E) + with pytest.raises(ControlArgument): + cdare(A, B, Qfs, R, S, E) + with pytest.raises(ControlArgument): + cdare(A, B, Q, Rfs, S, E) + with pytest.raises(ControlArgument): + cdare(A, B, Q, R, S) From fc2f7707dfd8eb93329deb7524a021c43d5a8ba1 Mon Sep 17 00:00:00 2001 From: bnavigator Date: Sun, 26 Jul 2020 21:45:56 +0200 Subject: [PATCH 16/30] pytestify matlab tests --- ...test_control_matlab.py => matlab2_test.py} | 151 ++- control/tests/matlab_test.py | 874 ++++++++++-------- 2 files changed, 562 insertions(+), 463 deletions(-) rename control/tests/{test_control_matlab.py => matlab2_test.py} (81%) diff --git a/control/tests/test_control_matlab.py b/control/tests/matlab2_test.py similarity index 81% rename from control/tests/test_control_matlab.py rename to control/tests/matlab2_test.py index aa8633e7c..5db4156df 100644 --- a/control/tests/test_control_matlab.py +++ b/control/tests/matlab2_test.py @@ -1,30 +1,30 @@ -''' -Copyright (C) 2011 by Eike Welk. +"""matlab2_test.py Test the control.matlab toolbox. -''' -import unittest +Copyright (C) 2011 by Eike Welk. +""" + +from matplotlib.pyplot import figure, plot, legend, subplot2grid import numpy as np -import scipy.signal +from numpy import array, matrix, zeros, linspace, r_ from numpy.testing import assert_array_almost_equal -from numpy import array, asarray, matrix, asmatrix, zeros, ones, linspace,\ - all, hstack, vstack, c_, r_ -from matplotlib.pyplot import show, figure, plot, legend, subplot2grid -from control.matlab import ss, step, impulse, initial, lsim, dcgain, \ - ss2tf + +import pytest +import scipy.signal + +from control.matlab import ss, step, impulse, initial, lsim, dcgain, ss2tf from control.statesp import _mimo2siso from control.timeresp import _check_convert_array -from control.exception import slycot_check -import warnings +from control.tests.conftest import slycotonly -class TestControlMatlab(unittest.TestCase): - def setUp(self): - pass +class TestControlMatlab: + """Test the control.matlab toolbox.""" - def make_SISO_mats(self): + @pytest.fixture + def SISO_mats(self): """Return matrices for a SISO system""" A = array([[-81.82, -45.45], [ 10., -1. ]]) @@ -34,7 +34,8 @@ def make_SISO_mats(self): D = zeros((1, 1)) return A, B, C, D - def make_MIMO_mats(self): + @pytest.fixture + def MIMO_mats(self): """Return matrices for a MIMO system""" A = array([[-81.82, -45.45, 0, 0 ], [ 10, -1, 0, 0 ], @@ -49,39 +50,40 @@ def make_MIMO_mats(self): D = zeros((2, 2)) return A, B, C, D - def test_dcgain(self): - """Test function dcgain with different systems""" - if slycot_check(): - #Test MIMO systems - A, B, C, D = self.make_MIMO_mats() - - gain1 = dcgain(ss(A, B, C, D)) - gain2 = dcgain(A, B, C, D) - sys_tf = ss2tf(A, B, C, D) - gain3 = dcgain(sys_tf) - gain4 = dcgain(sys_tf.num, sys_tf.den) - #print("gain1:", gain1) - - assert_array_almost_equal(gain1, - array([[0.0269, 0. ], - [0. , 0.0269]]), - decimal=4) - assert_array_almost_equal(gain1, gain2) - assert_array_almost_equal(gain3, gain4) - assert_array_almost_equal(gain1, gain4) - - #Test SISO systems - A, B, C, D = self.make_SISO_mats() + @slycotonly + def test_dcgain_mimo(self, MIMO_mats): + """Test function dcgain with MIMO systems""" + #Test MIMO systems + A, B, C, D = MIMO_mats + + gain1 = dcgain(ss(A, B, C, D)) + gain2 = dcgain(A, B, C, D) + sys_tf = ss2tf(A, B, C, D) + gain3 = dcgain(sys_tf) + gain4 = dcgain(sys_tf.num, sys_tf.den) + #print("gain1:", gain1) + + assert_array_almost_equal(gain1, + array([[0.0269, 0. ], + [0. , 0.0269]]), + decimal=4) + assert_array_almost_equal(gain1, gain2) + assert_array_almost_equal(gain3, gain4) + assert_array_almost_equal(gain1, gain4) + + def test_dcgain_siso(self, SISO_mats): + """Test function dcgain with SISO systems""" + A, B, C, D = SISO_mats gain1 = dcgain(ss(A, B, C, D)) assert_array_almost_equal(gain1, array([[0.0269]]), decimal=4) - def test_dcgain_2(self): + def test_dcgain_2(self, SISO_mats): """Test function dcgain with different systems""" #Create different forms of a SISO system - A, B, C, D = self.make_SISO_mats() + A, B, C, D = SISO_mats num, den = scipy.signal.ss2tf(A, B, C, D) # numerator is only a constant here; pick it out to avoid numpy warning Z, P, k = scipy.signal.tf2zpk(num[0][-1], den) @@ -108,12 +110,12 @@ def test_dcgain_2(self): 0.026948], decimal=6) - def test_step(self): + def test_step(self, SISO_mats, MIMO_mats, mplcleanup): """Test function ``step``.""" figure(); plot_shape = (1, 3) #Test SISO system - A, B, C, D = self.make_SISO_mats() + A, B, C, D = SISO_mats sys = ss(A, B, C, D) #print(sys) #print("gain:", dcgain(sys)) @@ -132,15 +134,15 @@ def test_step(self): t, y, x = step(sys, return_x=True) #Test MIMO system - A, B, C, D = self.make_MIMO_mats() + A, B, C, D = MIMO_mats sys = ss(A, B, C, D) subplot2grid(plot_shape, (0, 2)) t, y = step(sys) plot(t, y) - def test_impulse(self): - A, B, C, D = self.make_SISO_mats() + def test_impulse(self, SISO_mats, mplcleanup): + A, B, C, D = SISO_mats sys = ss(A, B, C, D) figure() @@ -158,14 +160,14 @@ def test_impulse(self): #Test system with direct feed-though, the function should print a warning. D = [[0.5]] sys_ft = ss(A, B, C, D) - with warnings.catch_warnings(): - warnings.simplefilter("ignore") + with pytest.warns(UserWarning, match="has direct feedthrough"): t, y = impulse(sys_ft) plot(t, y, label='Direct feedthrough D=[[0.5]]') + def test_impulse_mimo(self, MIMO_mats, mplcleanup): #Test MIMO system - A, B, C, D = self.make_MIMO_mats() - sys = ss(A, B, C, D) + A, B, C, D = MIMO_mats + sys = ss(A, B, C, D) t, y = impulse(sys) plot(t, y, label='MIMO System') @@ -173,8 +175,8 @@ def test_impulse(self): #show() - def test_initial(self): - A, B, C, D = self.make_SISO_mats() + def test_initial(self, SISO_mats, MIMO_mats, mplcleanup): + A, B, C, D = SISO_mats sys = ss(A, B, C, D) figure(); plot_shape = (1, 3) @@ -186,11 +188,10 @@ def test_initial(self): #X0=[1,1] : produces a spike subplot2grid(plot_shape, (0, 1)) - t, y = initial(sys, X0=array(matrix("1; 1"))) + t, y = initial(sys, X0=array([[1], [1]])) plot(t, y) - #Test MIMO system - A, B, C, D = self.make_MIMO_mats() + A, B, C, D = MIMO_mats sys = ss(A, B, C, D) #X0=[1,1] : produces same spike as above spike subplot2grid(plot_shape, (0, 2)) @@ -200,7 +201,8 @@ def test_initial(self): #show() #! Old test; no longer functional?? (RMM, 3 Nov 2012) - @unittest.skip("skipping test_check_convert_shape, need to update test") + @pytest.mark.skip( + reason="skipping test_check_convert_shape, need to update test") def test_check_convert_shape(self): #TODO: check if shape is correct everywhere. #Correct input --------------------------------------------- @@ -270,9 +272,9 @@ def test_check_convert_shape(self): self.assertRaises(ValueError, _check_convert_array(array([1., 2, 3, 4]), [(3,), (1,3)], 'Test: ')) - @unittest.skip("skipping test_lsim, need to update test") - def test_lsim(self): - A, B, C, D = self.make_SISO_mats() + @pytest.mark.skip(reason="need to update test") + def test_lsim(self, SISO_mats, MIMO_mats): + A, B, C, D = SISO_mats sys = ss(A, B, C, D) figure(); plot_shape = (2, 2) @@ -304,7 +306,7 @@ def test_lsim(self): #Test with MIMO system subplot2grid(plot_shape, (1, 1)) - A, B, C, D = self.make_MIMO_mats() + A, B, C, D = MIMO_mats sys = ss(A, B, C, D) t = array(linspace(0, 1, 100)) u = array([r_[1:1:50j, 0:0:50j], @@ -350,14 +352,14 @@ def assert_systems_behave_equal(self, sys1, sys2): y2, t2 = step(sys2, t1) assert_array_almost_equal(y1, y2) - def test_convert_MIMO_to_SISO(self): + def test_convert_MIMO_to_SISO(self, SISO_mats, MIMO_mats): '''Convert mimo to siso systems''' #Test with our usual systems -------------------------------------------- #SISO PT2 system - As, Bs, Cs, Ds = self.make_SISO_mats() + As, Bs, Cs, Ds = SISO_mats sys_siso = ss(As, Bs, Cs, Ds) #MIMO system that contains two independent copies of the SISO system above - Am, Bm, Cm, Dm = self.make_MIMO_mats() + Am, Bm, Cm, Dm = MIMO_mats sys_mimo = ss(Am, Bm, Cm, Dm) # t, y = step(sys_siso) # plot(t, y, label='sys_siso d=0') @@ -420,24 +422,3 @@ def test_convert_MIMO_to_SISO(self): self.assert_systems_behave_equal(sys_siso, sys_siso_01) self.assert_systems_behave_equal(sys_siso, sys_siso_10) - def debug_nasty_import_problem(): - ''' - ``*.egg`` files have precedence over ``PYTHONPATH``. Therefore packages - that were installed with ``easy_install``, can not be easily developed with - Eclipse. - - See also: - http://bugs.python.org/setuptools/issue53 - - Use this function to debug the issue. - ''' - #print the directories where python searches for modules and packages. - import sys - print('sys.path: -----------------------------------') - for name in sys.path: - print(name) - - -if __name__ == '__main__': - unittest.main() -# vi:ts=4:sw=4:expandtab diff --git a/control/tests/matlab_test.py b/control/tests/matlab_test.py index 7d81288e4..6c7f6f14f 100644 --- a/control/tests/matlab_test.py +++ b/control/tests/matlab_test.py @@ -1,22 +1,36 @@ -#!/usr/bin/env python -# -# matlab_test.py - test MATLAB compatibility -# RMM, 30 Mar 2011 (based on TestMatlab from v0.4a) -# -# This test suite just goes through and calls all of the MATLAB -# functions using different systems and arguments to make sure that -# nothing crashes. It doesn't test actual functionality; the module -# specific unit tests will do that. - -from __future__ import print_function -import unittest +"""matlab_test.py - test MATLAB compatibility + +RMM, 30 Mar 2011 (based on TestMatlab from v0.4a) + +This test suite just goes through and calls all of the MATLAB +functions using different systems and arguments to make sure that +nothing crashes. Many test don't test actual functionality; the module +specific unit tests will do that. +""" + import numpy as np -from scipy.linalg import eigvals +import pytest import scipy as sp -from control.matlab import * +from scipy.linalg import eigvals + +from control.matlab import ss, ss2tf, ssdata, tf, tf2ss, tfdata, rss, drss, frd +from control.matlab import parallel, series, feedback +from control.matlab import pole, zero, damp +from control.matlab import step, stepinfo, impulse, initial, lsim +from control.matlab import margin, dcgain +from control.matlab import linspace, logspace +from control.matlab import bode, rlocus, nyquist, nichols, ngrid, pzmap +from control.matlab import freqresp, evalfr +from control.matlab import hsvd, balred, modred, minreal +from control.matlab import place, place_varga, acker +from control.matlab import lqr, ctrb, obsv, gram +from control.matlab import pade +from control.matlab import unwrap, c2d, isctime, isdtime +from control.matlab import connect, append + + from control.frdata import FRD -from control.exception import slycot_check -import warnings +from control.tests.conftest import slycotonly # for running these through Matlab or Octave ''' @@ -55,96 +69,124 @@ ''' -class TestMatlab(unittest.TestCase): - def setUp(self): + +@pytest.fixture(scope="class") +def fixedseed(): + """Get consistent test results""" + np.random.seed(0) + + +class tsystems: + """struct for test systems""" + + pass + + +@pytest.mark.usefixtures("fixedseed") +class TestMatlab: + """Test matlab style functions""" + + @pytest.fixture + def siso(self): """Set up some systems for testing out MATLAB functions""" - A = np.matrix("1. -2.; 3. -4.") - B = np.matrix("5.; 7.") - C = np.matrix("6. 8.") - D = np.matrix("9.") - self.siso_ss1 = ss(A,B,C,D) + s = tsystems() + + A = np.array([[1., -2.], [3., -4.]]) + B = np.array([[5.], [7.]]) + C = np.array([[6., 8.]]) + D = np.array([[9.]]) + s.ss1 = ss(A, B, C, D) # Create some transfer functions - self.siso_tf1 = tf([1], [1, 2, 1]); - self.siso_tf2 = tf([1, 1], [1, 2, 3, 1]); + s.tf1 = tf([1], [1, 2, 1]) + s.tf2 = tf([1, 1], [1, 2, 3, 1]) # Conversions - self.siso_tf3 = tf(self.siso_ss1); - self.siso_ss2 = ss(self.siso_tf2); - self.siso_ss3 = tf2ss(self.siso_tf3); - self.siso_tf4 = ss2tf(self.siso_ss2); - - #Create MIMO system, contains ``siso_ss1`` twice - A = np.matrix("1. -2. 0. 0.;" - "3. -4. 0. 0.;" - "0. 0. 1. -2.;" - "0. 0. 3. -4. ") - B = np.matrix("5. 0.;" - "7. 0.;" - "0. 5.;" - "0. 7. ") - C = np.matrix("6. 8. 0. 0.;" - "0. 0. 6. 8. ") - D = np.matrix("9. 0.;" - "0. 9. ") - self.mimo_ss1 = ss(A, B, C, D) - - # get consistent test results - np.random.seed(0) - - def testParallel(self): - sys1 = parallel(self.siso_ss1, self.siso_ss2) - sys1 = parallel(self.siso_ss1, self.siso_tf2) - sys1 = parallel(self.siso_tf1, self.siso_ss2) - sys1 = parallel(1, self.siso_ss2) - sys1 = parallel(1, self.siso_tf2) - sys1 = parallel(self.siso_ss1, 1) - sys1 = parallel(self.siso_tf1, 1) - - def testSeries(self): - sys1 = series(self.siso_ss1, self.siso_ss2) - sys1 = series(self.siso_ss1, self.siso_tf2) - sys1 = series(self.siso_tf1, self.siso_ss2) - sys1 = series(1, self.siso_ss2) - sys1 = series(1, self.siso_tf2) - sys1 = series(self.siso_ss1, 1) - sys1 = series(self.siso_tf1, 1) - - def testFeedback(self): - sys1 = feedback(self.siso_ss1, self.siso_ss2) - sys1 = feedback(self.siso_ss1, self.siso_tf2) - sys1 = feedback(self.siso_tf1, self.siso_ss2) - sys1 = feedback(1, self.siso_ss2) - sys1 = feedback(1, self.siso_tf2) - sys1 = feedback(self.siso_ss1, 1) - sys1 = feedback(self.siso_tf1, 1) - - def testPoleZero(self): - pole(self.siso_ss1); - pole(self.siso_tf1); - pole(self.siso_tf2); - zero(self.siso_ss1); - zero(self.siso_tf1); - zero(self.siso_tf2); - - def testPZmap(self): - # pzmap(self.siso_ss1); not implemented - # pzmap(self.siso_ss2); not implemented - pzmap(self.siso_tf1); - pzmap(self.siso_tf2); - pzmap(self.siso_tf2, plot=False); - - def testStep(self): + s.tf3 = tf(s.ss1) + s.ss2 = ss(s.tf2) + s.ss3 = tf2ss(s.tf3) + s.tf4 = ss2tf(s.ss2) + return s + + @pytest.fixture + def mimo(self): + """Create MIMO system, contains ``siso_ss1`` twice""" + m = tsystems() + A = np.array([[1., -2., 0., 0.], + [3., -4., 0., 0.], + [0., 0., 1., -2.], + [0., 0., 3., -4.]]) + B = np.array([[5., 0.], + [7., 0.], + [0., 5.], + [0., 7.]]) + C = np.array([[6., 8., 0., 0.], + [0., 0., 6., 8.]]) + D = np.array([[9., 0.], + [0., 9.]]) + m.ss1 = ss(A, B, C, D) + return m + + def testParallel(self, siso): + """Call parallel()""" + sys1 = parallel(siso.ss1, siso.ss2) + sys1 = parallel(siso.ss1, siso.tf2) + sys1 = parallel(siso.tf1, siso.ss2) + sys1 = parallel(1, siso.ss2) + sys1 = parallel(1, siso.tf2) + sys1 = parallel(siso.ss1, 1) + sys1 = parallel(siso.tf1, 1) + + def testSeries(self, siso): + """Call series()""" + sys1 = series(siso.ss1, siso.ss2) + sys1 = series(siso.ss1, siso.tf2) + sys1 = series(siso.tf1, siso.ss2) + sys1 = series(1, siso.ss2) + sys1 = series(1, siso.tf2) + sys1 = series(siso.ss1, 1) + sys1 = series(siso.tf1, 1) + + def testFeedback(self, siso): + """Call feedback()""" + sys1 = feedback(siso.ss1, siso.ss2) + sys1 = feedback(siso.ss1, siso.tf2) + sys1 = feedback(siso.tf1, siso.ss2) + sys1 = feedback(1, siso.ss2) + sys1 = feedback(1, siso.tf2) + sys1 = feedback(siso.ss1, 1) + sys1 = feedback(siso.tf1, 1) + + def testPoleZero(self, siso): + """Call pole() and zero()""" + pole(siso.ss1) + pole(siso.tf1) + pole(siso.tf2) + zero(siso.ss1) + zero(siso.tf1) + zero(siso.tf2) + + @pytest.mark.parametrize( + "subsys", ["tf1", "tf2"]) + def testPZmap(self, siso, subsys, mplcleanup): + """Call pzmap()""" + # pzmap(siso.ss1); not implemented + # pzmap(siso.ss2); not implemented + pzmap(getattr(siso, subsys)) + pzmap(getattr(siso, subsys), plot=False) + + def testStep(self, siso): + """Test step()""" t = np.linspace(0, 1, 10) # Test transfer function - yout, tout = step(self.siso_tf1, T=t) + yout, tout = step(siso.tf1, T=t) youttrue = np.array([0, 0.0057, 0.0213, 0.0446, 0.0739, 0.1075, 0.1443, 0.1832, 0.2235, 0.2642]) np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) np.testing.assert_array_almost_equal(tout, t) # Test SISO system with direct feedthrough - sys = self.siso_ss1 + sys = siso.ss1 youttrue = np.array([9., 17.6457, 24.7072, 30.4855, 35.2234, 39.1165, 42.3227, 44.9694, 47.1599, 48.9776]) @@ -157,7 +199,7 @@ def testStep(self): np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) np.testing.assert_array_almost_equal(tout, t) - X0 = np.array([0, 0]); + X0 = np.array([0, 0]) yout, tout = step(sys, T=t, X0=X0) np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) np.testing.assert_array_almost_equal(tout, t) @@ -166,63 +208,85 @@ def testStep(self): np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) np.testing.assert_array_almost_equal(tout, t) - if slycot_check(): - # Test MIMO system, which contains ``siso_ss1`` twice - sys = self.mimo_ss1 - y_00, _t = step(sys, T=t, input=0, output=0) - y_11, _t = step(sys, T=t, input=1, output=1) - np.testing.assert_array_almost_equal(y_00, youttrue, decimal=4) - np.testing.assert_array_almost_equal(y_11, youttrue, decimal=4) + @slycotonly + def testStep_mimo(self, mimo): + """Test step for MIMO system""" + sys = mimo.ss1 + t = np.linspace(0, 1, 10) + youttrue = np.array([9., 17.6457, 24.7072, 30.4855, 35.2234, 39.1165, + 42.3227, 44.9694, 47.1599, 48.9776]) + + y_00, _t = step(sys, T=t, input=0, output=0) + y_11, _t = step(sys, T=t, input=1, output=1) + np.testing.assert_array_almost_equal(y_00, youttrue, decimal=4) + np.testing.assert_array_almost_equal(y_11, youttrue, decimal=4) + + def testStepinfo(self, siso): + """Test the stepinfo function (no return value check)""" + infodict = stepinfo(siso.ss1) + assert isinstance(infodict, dict) + assert len(infodict) == 9 - def testImpulse(self): + def testImpulse(self, siso): + """Test impulse()""" t = np.linspace(0, 1, 10) # test transfer function - yout, tout = impulse(self.siso_tf1, T=t) + yout, tout = impulse(siso.tf1, T=t) youttrue = np.array([0., 0.0994, 0.1779, 0.2388, 0.2850, 0.3188, 0.3423, 0.3573, 0.3654, 0.3679]) np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) np.testing.assert_array_almost_equal(tout, t) + sys = siso.ss1 + youttrue = np.array([86., 70.1808, 57.3753, 46.9975, 38.5766, 31.7344, + 26.1668, 21.6292, 17.9245, 14.8945]) # produce a warning for a system with direct feedthrough - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - #Test SISO system - sys = self.siso_ss1 - youttrue = np.array([86., 70.1808, 57.3753, 46.9975, 38.5766, 31.7344, - 26.1668, 21.6292, 17.9245, 14.8945]) + with pytest.warns(UserWarning, match="System has direct feedthrough"): + # Test SISO system yout, tout = impulse(sys, T=t) np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) np.testing.assert_array_almost_equal(tout, t) + # produce a warning for a system with direct feedthrough + with pytest.warns(UserWarning, match="System has direct feedthrough"): # Play with arguments yout, tout = impulse(sys, T=t, X0=0) np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) np.testing.assert_array_almost_equal(tout, t) - X0 = np.array([0, 0]); + # produce a warning for a system with direct feedthrough + with pytest.warns(UserWarning, match="System has direct feedthrough"): + X0 = np.array([0, 0]) yout, tout = impulse(sys, T=t, X0=X0) np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) np.testing.assert_array_almost_equal(tout, t) + # produce a warning for a system with direct feedthrough + with pytest.warns(UserWarning, match="System has direct feedthrough"): yout, tout, xout = impulse(sys, T=t, X0=0, return_x=True) np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) np.testing.assert_array_almost_equal(tout, t) - if slycot_check(): - #Test MIMO system, which contains ``siso_ss1`` twice - sys = self.mimo_ss1 - y_00, _t = impulse(sys, T=t, input=0, output=0) - y_11, _t = impulse(sys, T=t, input=1, output=1) - np.testing.assert_array_almost_equal(y_00, youttrue, decimal=4) - np.testing.assert_array_almost_equal(y_11, youttrue, decimal=4) - - def testInitial(self): - #Test SISO system - sys = self.siso_ss1 + @slycotonly + def testImpulse_mimo(self, mimo): + """Test impulse() for MIMO system""" t = np.linspace(0, 1, 10) - x0 = np.matrix(".5; 1.") + youttrue = np.array([86., 70.1808, 57.3753, 46.9975, 38.5766, 31.7344, + 26.1668, 21.6292, 17.9245, 14.8945]) + sys = mimo.ss1 + with pytest.warns(UserWarning, match="System has direct feedthrough"): + y_00, _t = impulse(sys, T=t, input=0, output=0) + y_11, _t = impulse(sys, T=t, input=1, output=1) + np.testing.assert_array_almost_equal(y_00, youttrue, decimal=4) + np.testing.assert_array_almost_equal(y_11, youttrue, decimal=4) + + def testInitial(self, siso): + """Test initial() for SISO system""" + t = np.linspace(0, 1, 10) + x0 = np.array([[.5], [1.]]) youttrue = np.array([11., 8.1494, 5.9361, 4.2258, 2.9118, 1.9092, 1.1508, 0.5833, 0.1645, -0.1391]) + sys = siso.ss1 yout, tout = initial(sys, T=t, X0=x0) np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) np.testing.assert_array_almost_equal(tout, t) @@ -232,70 +296,81 @@ def testInitial(self): np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) np.testing.assert_array_almost_equal(tout, t) - if slycot_check(): - #Test MIMO system, which contains ``siso_ss1`` twice - sys = self.mimo_ss1 - x0 = np.matrix(".5; 1.; .5; 1.") - y_00, _t = initial(sys, T=t, X0=x0, input=0, output=0) - y_11, _t = initial(sys, T=t, X0=x0, input=1, output=1) - np.testing.assert_array_almost_equal(y_00, youttrue, decimal=4) - np.testing.assert_array_almost_equal(y_11, youttrue, decimal=4) - - def testLsim(self): + @slycotonly + def testInitial_mimo(self, mimo): + """Test initial() for MIMO system""" + t = np.linspace(0, 1, 10) + x0 = np.array([[.5], [1.], [.5], [1.]]) + youttrue = np.array([11., 8.1494, 5.9361, 4.2258, 2.9118, 1.9092, + 1.1508, 0.5833, 0.1645, -0.1391]) + sys = mimo.ss1 + y_00, _t = initial(sys, T=t, X0=x0, input=0, output=0) + y_11, _t = initial(sys, T=t, X0=x0, input=1, output=1) + np.testing.assert_array_almost_equal(y_00, youttrue, decimal=4) + np.testing.assert_array_almost_equal(y_11, youttrue, decimal=4) + + def testLsim(self, siso): + """Test lsim() for SISO system""" t = np.linspace(0, 1, 10) - #compute step response - test with state space, and transfer function - #objects + # compute step response - test with state space, and transfer function + # objects u = np.array([1., 1, 1, 1, 1, 1, 1, 1, 1, 1]) youttrue = np.array([9., 17.6457, 24.7072, 30.4855, 35.2234, 39.1165, 42.3227, 44.9694, 47.1599, 48.9776]) - yout, tout, _xout = lsim(self.siso_ss1, u, t) + yout, tout, _xout = lsim(siso.ss1, u, t) np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) np.testing.assert_array_almost_equal(tout, t) - yout, _t, _xout = lsim(self.siso_tf3, u, t) + yout, _t, _xout = lsim(siso.tf3, u, t) np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) - #test with initial value and special algorithm for ``U=0`` - u=0 - x0 = np.matrix(".5; 1.") + # test with initial value and special algorithm for ``U=0`` + u = 0 + x0 = np.array([[.5], [1.]]) youttrue = np.array([11., 8.1494, 5.9361, 4.2258, 2.9118, 1.9092, 1.1508, 0.5833, 0.1645, -0.1391]) - yout, _t, _xout = lsim(self.siso_ss1, u, t, x0) + yout, _t, _xout = lsim(siso.ss1, u, t, x0) np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) - if slycot_check(): - #Test MIMO system, which contains ``siso_ss1`` twice - #first system: initial value, second system: step response - u = np.array([[0., 1.], [0, 1], [0, 1], [0, 1], [0, 1], - [0, 1], [0, 1], [0, 1], [0, 1], [0, 1]]) - x0 = np.matrix(".5; 1; 0; 0") - youttrue = np.array([[11., 9.], [8.1494, 17.6457], - [5.9361, 24.7072], [4.2258, 30.4855], - [2.9118, 35.2234], [1.9092, 39.1165], - [1.1508, 42.3227], [0.5833, 44.9694], - [0.1645, 47.1599], [-0.1391, 48.9776]]) - yout, _t, _xout = lsim(self.mimo_ss1, u, t, x0) - np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) + @slycotonly + def testLsim_mimo(self, mimo): + """Test lsim() for MIMO system. - def testMargin(self): + first system: initial value, second system: step response + """ + t = np.linspace(0, 1, 10) + + u = np.array([[0., 1.], [0, 1], [0, 1], [0, 1], [0, 1], + [0, 1], [0, 1], [0, 1], [0, 1], [0, 1]]) + x0 = np.array([[.5], [1], [0], [0]]) + youttrue = np.array([[11., 9.], [8.1494, 17.6457], + [5.9361, 24.7072], [4.2258, 30.4855], + [2.9118, 35.2234], [1.9092, 39.1165], + [1.1508, 42.3227], [0.5833, 44.9694], + [0.1645, 47.1599], [-0.1391, 48.9776]]) + yout, _t, _xout = lsim(mimo.ss1, u, t, x0) + np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) + + def testMargin(self, siso): + """Test margin()""" #! TODO: check results to make sure they are OK - gm, pm, wg, wp = margin(self.siso_tf1); - gm, pm, wg, wp = margin(self.siso_tf2); - gm, pm, wg, wp = margin(self.siso_ss1); - gm, pm, wg, wp = margin(self.siso_ss2); - gm, pm, wg, wp = margin(self.siso_ss2*self.siso_ss2*2); + gm, pm, wg, wp = margin(siso.tf1) + gm, pm, wg, wp = margin(siso.tf2) + gm, pm, wg, wp = margin(siso.ss1) + gm, pm, wg, wp = margin(siso.ss2) + gm, pm, wg, wp = margin(siso.ss2 * siso.ss2 * 2) np.testing.assert_array_almost_equal( [gm, pm, wg, wp], [1.5451, 75.9933, 1.2720, 0.6559], decimal=3) - def testDcgain(self): - #Create different forms of a SISO system - A, B, C, D = self.siso_ss1.A, self.siso_ss1.B, self.siso_ss1.C, \ - self.siso_ss1.D + def testDcgain(self, siso): + """Test dcgain() for SISO system""" + # Create different forms of a SISO system using scipy.signal + A, B, C, D = siso.ss1.A, siso.ss1.B, siso.ss1.C, siso.ss1.D Z, P, k = sp.signal.ss2zpk(A, B, C, D) num, den = sp.signal.ss2tf(A, B, C, D) - sys_ss = self.siso_ss1 + sys_ss = siso.ss1 - #Compute the gain with ``dcgain`` + # Compute the gain with ``dcgain`` gain_abcd = dcgain(A, B, C, D) gain_zpk = dcgain(Z, P, k) gain_numden = dcgain(np.squeeze(num), den) @@ -303,282 +378,327 @@ def testDcgain(self): # print('\ngain_abcd:', gain_abcd, 'gain_zpk:', gain_zpk) # print('gain_numden:', gain_numden, 'gain_sys_ss:', gain_sys_ss) - #Compute the gain with a long simulation + # Compute the gain with a long simulation t = linspace(0, 1000, 1000) y, _t = step(sys_ss, t) gain_sim = y[-1] # print('gain_sim:', gain_sim) - #All gain values must be approximately equal to the known gain + # All gain values must be approximately equal to the known gain np.testing.assert_array_almost_equal( - [gain_abcd, gain_zpk, gain_numden, gain_sys_ss, - gain_sim], + [gain_abcd, gain_zpk, gain_numden, gain_sys_ss, gain_sim], [59, 59, 59, 59, 59]) - if slycot_check(): - # Test with MIMO system, which contains ``siso_ss1`` twice - gain_mimo = dcgain(self.mimo_ss1) - # print('gain_mimo: \n', gain_mimo) - np.testing.assert_array_almost_equal(gain_mimo, [[59., 0 ], - [0, 59.]]) - - def testBode(self): - bode(self.siso_ss1) - bode(self.siso_tf1) - bode(self.siso_tf2) - (mag, phase, freq) = bode(self.siso_tf2, plot=False) - bode(self.siso_tf1, self.siso_tf2) - w = logspace(-3, 3); - bode(self.siso_ss1, w) - bode(self.siso_ss1, self.siso_tf2, w) -# Not yet implemented -# bode(self.siso_ss1, '-', self.siso_tf1, 'b--', self.siso_tf2, 'k.') - - def testRlocus(self): - rlocus(self.siso_ss1) - rlocus(self.siso_tf1) - rlocus(self.siso_tf2) + def testDcgain_mimo(self, mimo): + """Test dcgain() for MIMO system""" + gain_mimo = dcgain(mimo.ss1) + # print('gain_mimo: \n', gain_mimo) + np.testing.assert_array_almost_equal(gain_mimo, [[59., 0], + [0, 59.]]) + + def testBode(self, siso, mplcleanup): + """Call bode()""" + bode(siso.ss1) + bode(siso.tf1) + bode(siso.tf2) + (mag, phase, freq) = bode(siso.tf2, plot=False) + bode(siso.tf1, siso.tf2) + w = logspace(-3, 3) + bode(siso.ss1, w) + bode(siso.ss1, siso.tf2, w) + # Not yet implemented + # bode(siso.ss1, '-', siso.tf1, 'b--', siso.tf2, 'k.') + + @pytest.mark.parametrize("subsys", ["ss1", "tf1", "tf2"]) + def testRlocus(self, siso, subsys, mplcleanup): + """Call rlocus()""" + rlocus(getattr(siso, subsys)) + + def testRlocus_list(self, siso, mplcleanup): + """Test rlocus() with list""" klist = [1, 10, 100] - rlist, klist_out = rlocus(self.siso_tf2, klist, plot=False) + rlist, klist_out = rlocus(siso.tf2, klist, plot=False) np.testing.assert_equal(len(rlist), len(klist)) np.testing.assert_array_equal(klist, klist_out) - def testNyquist(self): - nyquist(self.siso_ss1) - nyquist(self.siso_tf1) - nyquist(self.siso_tf2) - w = logspace(-3, 3); - nyquist(self.siso_tf2, w) - (real, imag, freq) = nyquist(self.siso_tf2, w, plot=False) - - def testNichols(self): - nichols(self.siso_ss1) - nichols(self.siso_tf1) - nichols(self.siso_tf2) - w = logspace(-3, 3); - nichols(self.siso_tf2, w) - nichols(self.siso_tf2, grid=False) - - def testFreqresp(self): + def testNyquist(self, siso): + """Call nyquist()""" + nyquist(siso.ss1) + nyquist(siso.tf1) + nyquist(siso.tf2) w = logspace(-3, 3) - freqresp(self.siso_ss1, w) - freqresp(self.siso_ss2, w) - freqresp(self.siso_ss3, w) - freqresp(self.siso_tf1, w) - freqresp(self.siso_tf2, w) - freqresp(self.siso_tf3, w) - - def testEvalfr(self): + nyquist(siso.tf2, w) + (real, imag, freq) = nyquist(siso.tf2, w, plot=False) + + @pytest.mark.parametrize("subsys", ["ss1", "tf1", "tf2"]) + def testNichols(self, siso, subsys, mplcleanup): + """Call nichols()""" + nichols(getattr(siso, subsys)) + + def testNichols_logspace(self, siso, mplcleanup): + """Call nichols() with logspace w""" + w = logspace(-3, 3) + nichols(siso.tf2, w) + + def testNichols_ngrid(self, siso, mplcleanup): + """Call nichols() and ngrid()""" + nichols(siso.tf2, grid=False) + ngrid() + + def testFreqresp(self, siso): + """Call freqresp()""" + w = logspace(-3, 3) + freqresp(siso.ss1, w) + freqresp(siso.ss2, w) + freqresp(siso.ss3, w) + freqresp(siso.tf1, w) + freqresp(siso.tf2, w) + freqresp(siso.tf3, w) + + def testEvalfr(self, siso): + """Call evalfr()""" w = 1j - np.testing.assert_almost_equal(evalfr(self.siso_ss1, w), 44.8-21.4j) - evalfr(self.siso_ss2, w) - evalfr(self.siso_ss3, w) - evalfr(self.siso_tf1, w) - evalfr(self.siso_tf2, w) - evalfr(self.siso_tf3, w) - if slycot_check(): - np.testing.assert_array_almost_equal( - evalfr(self.mimo_ss1, w), - np.array( [[44.8-21.4j, 0.], [0., 44.8-21.4j]])) - - @unittest.skipIf(not slycot_check(), "slycot not installed") - def testHsvd(self): - hsvd(self.siso_ss1) - hsvd(self.siso_ss2) - hsvd(self.siso_ss3) - - @unittest.skipIf(not slycot_check(), "slycot not installed") - def testBalred(self): - balred(self.siso_ss1, 1) - balred(self.siso_ss2, 2) - balred(self.siso_ss3, [2, 2]) - - @unittest.skipIf(not slycot_check(), "slycot not installed") - def testModred(self): - modred(self.siso_ss1, [1]) - modred(self.siso_ss2 * self.siso_ss1, [0, 1]) - modred(self.siso_ss1, [1], 'matchdc') - modred(self.siso_ss1, [1], 'truncate') - - @unittest.skipIf(not slycot_check(), "slycot not installed") - def testPlace_varga(self): - place_varga(self.siso_ss1.A, self.siso_ss1.B, [-2, -2]) - - def testPlace(self): - place(self.siso_ss1.A, self.siso_ss1.B, [-2, -2.5]) - - def testAcker(self): - acker(self.siso_ss1.A, self.siso_ss1.B, [-2, -2.5]) - - - @unittest.skipIf(not slycot_check(), "slycot not installed") - def testLQR(self): - (K, S, E) = lqr(self.siso_ss1.A, self.siso_ss1.B, np.eye(2), np.eye(1)) + np.testing.assert_almost_equal(evalfr(siso.ss1, w), 44.8 - 21.4j) + evalfr(siso.ss2, w) + evalfr(siso.ss3, w) + evalfr(siso.tf1, w) + evalfr(siso.tf2, w) + evalfr(siso.tf3, w) + + def testEvalfr_mimo(self, mimo): + """Test evalfr() MIMO""" + fr = evalfr(mimo.ss1, 1j) + ref = np.array([[44.8 - 21.4j, 0.], [0., 44.8 - 21.4j]]) + np.testing.assert_array_almost_equal(fr, ref) + + @slycotonly + def testHsvd(self, siso): + """Call hsvd()""" + hsvd(siso.ss1) + hsvd(siso.ss2) + hsvd(siso.ss3) + + @slycotonly + def testBalred(self, siso): + """Call balred()""" + balred(siso.ss1, 1) + balred(siso.ss2, 2) + balred(siso.ss3, [2, 2]) + + @slycotonly + def testModred(self, siso): + """Call modred()""" + modred(siso.ss1, [1]) + modred(siso.ss2 * siso.ss1, [0, 1]) + modred(siso.ss1, [1], 'matchdc') + modred(siso.ss1, [1], 'truncate') + + @slycotonly + def testPlace_varga(self, siso): + """Call place_varga()""" + place_varga(siso.ss1.A, siso.ss1.B, [-2, -2]) + + def testPlace(self, siso): + """Call place()""" + place(siso.ss1.A, siso.ss1.B, [-2, -2.5]) + + def testAcker(self, siso): + """Call acker()""" + acker(siso.ss1.A, siso.ss1.B, [-2, -2.5]) + + @slycotonly + def testLQR(self, siso): + """Call lqr()""" + (K, S, E) = lqr(siso.ss1.A, siso.ss1.B, np.eye(2), np.eye(1)) # Should work if [Q N;N' R] is positive semi-definite - (K, S, E) = lqr(self.siso_ss2.A, self.siso_ss2.B, 10*np.eye(3), \ - np.eye(1), [[1], [1], [2]]) - - @unittest.skip("check not yet implemented") - def testLQR_checks(self): - # Make sure we get a warning if [Q N;N' R] is not positive semi-definite - (K, S, E) = lqr(self.siso_ss2.A, self.siso_ss2.B, np.eye(3), \ - np.eye(1), [[1], [1], [2]]) + (K, S, E) = lqr(siso.ss2.A, siso.ss2.B, 10 * np.eye(3), np.eye(1), + [[1], [1], [2]]) def testRss(self): + """Call rss()""" rss(1) rss(2) rss(2, 1, 3) def testDrss(self): + """Call drss()""" drss(1) drss(2) drss(2, 1, 3) - def testCtrb(self): - ctrb(self.siso_ss1.A, self.siso_ss1.B) - ctrb(self.siso_ss2.A, self.siso_ss2.B) + def testCtrb(self, siso): + """Call ctrb()""" + ctrb(siso.ss1.A, siso.ss1.B) + ctrb(siso.ss2.A, siso.ss2.B) - def testObsv(self): - obsv(self.siso_ss1.A, self.siso_ss1.C) - obsv(self.siso_ss2.A, self.siso_ss2.C) + def testObsv(self, siso): + """Call obsv()""" + obsv(siso.ss1.A, siso.ss1.C) + obsv(siso.ss2.A, siso.ss2.C) - @unittest.skipIf(not slycot_check(), "slycot not installed") - def testGram(self): - gram(self.siso_ss1, 'c') - gram(self.siso_ss2, 'c') - gram(self.siso_ss1, 'o') - gram(self.siso_ss2, 'o') + @slycotonly + def testGram(self, siso): + """Call gram()""" + gram(siso.ss1, 'c') + gram(siso.ss2, 'c') + gram(siso.ss1, 'o') + gram(siso.ss2, 'o') def testPade(self): + """Call pade()""" pade(1, 1) pade(1, 2) pade(5, 4) - def testOpers(self): - self.siso_ss1 + self.siso_ss2 - self.siso_tf1 + self.siso_tf2 - self.siso_ss1 + self.siso_tf2 - self.siso_tf1 + self.siso_ss2 - self.siso_ss1 * self.siso_ss2 - self.siso_tf1 * self.siso_tf2 - self.siso_ss1 * self.siso_tf2 - self.siso_tf1 * self.siso_ss2 - # self.siso_ss1 / self.siso_ss2 not implemented yet - # self.siso_tf1 / self.siso_tf2 - # self.siso_ss1 / self.siso_tf2 - # self.siso_tf1 / self.siso_ss2 + def testOpers(self, siso): + """Use arithmetic operators""" + siso.ss1 + siso.ss2 + siso.tf1 + siso.tf2 + siso.ss1 + siso.tf2 + siso.tf1 + siso.ss2 + siso.ss1 * siso.ss2 + siso.tf1 * siso.tf2 + siso.ss1 * siso.tf2 + siso.tf1 * siso.ss2 + # siso.ss1 / siso.ss2 not implemented yet + # siso.tf1 / siso.tf2 + # siso.ss1 / siso.tf2 + # siso.tf1 / siso.ss2 def testUnwrap(self): - phase = np.array(range(1, 100)) / 10.; + """Call unwrap()""" + phase = np.array(range(1, 100)) / 10. wrapped = phase % (2 * np.pi) unwrapped = unwrap(wrapped) - def testSISOssdata(self): - ssdata_1 = ssdata(self.siso_ss2); - ssdata_2 = ssdata(self.siso_tf2); + def testSISOssdata(self, siso): + """Call ssdata() + + At least test for consistency between ss and tf + """ + ssdata_1 = ssdata(siso.ss2) + ssdata_2 = ssdata(siso.tf2) for i in range(len(ssdata_1)): np.testing.assert_array_almost_equal(ssdata_1[i], ssdata_2[i]) - @unittest.skipIf(not slycot_check(), "slycot not installed") - def testMIMOssdata(self): - m = (self.mimo_ss1.A, self.mimo_ss1.B, self.mimo_ss1.C, self.mimo_ss1.D) - ssdata_1 = ssdata(self.mimo_ss1); + @slycotonly + def testMIMOssdata(self, mimo): + """Test ssdata() MIMO""" + m = (mimo.ss1.A, mimo.ss1.B, mimo.ss1.C, mimo.ss1.D) + ssdata_1 = ssdata(mimo.ss1) for i in range(len(ssdata_1)): np.testing.assert_array_almost_equal(ssdata_1[i], m[i]) - def testSISOtfdata(self): - tfdata_1 = tfdata(self.siso_tf2); - tfdata_2 = tfdata(self.siso_tf2); + def testSISOtfdata(self, siso): + """Call tfdata()""" + tfdata_1 = tfdata(siso.tf2) + tfdata_2 = tfdata(siso.tf2) for i in range(len(tfdata_1)): np.testing.assert_array_almost_equal(tfdata_1[i], tfdata_2[i]) def testDamp(self): - A = np.mat('''-0.2 0.06 0 -1; - 0 0 1 0; - -17 0 -3.8 1; - 9.4 0 -0.4 -0.6''') - B = np.mat('''-0.01 0.06; - 0 0; - -32 5.4; - 2.6 -7''') + """Test damp()""" + A = np.array([[-0.2, 0.06, 0, -1], + [0, 0, 1, 0], + [-17, 0, -3.8, 1], + [9.4, 0, -0.4, -0.6]]) + B = np.array([[-0.01, 0.06], + [0, 0], + [-32, 5.4], + [2.6, -7]]) C = np.eye(4) - D = np.zeros((4,2)) + D = np.zeros((4, 2)) sys = ss(A, B, C, D) wn, Z, p = damp(sys, False) # print (wn) np.testing.assert_array_almost_equal( - wn, np.array([4.07381994, 3.28874827, 3.28874827, + wn, np.array([4.07381994, 3.28874827, 3.28874827, 1.08937685e-03])) np.testing.assert_array_almost_equal( - Z, np.array([1.0, 0.07983139, 0.07983139, 1.0])) + Z, np.array([1.0, 0.07983139, 0.07983139, 1.0])) def testConnect(self): - sys1 = ss("1. -2; 3. -4", "5.; 7", "6, 8", "9.") - sys2 = ss("-1.", "1.", "1.", "0.") + """Test append() and connect()""" + sys1 = ss([[1., -2], + [3., -4]], + [[5.], + [7]], + [[6, 8]], + [[9.]]) + sys2 = ss(-1., 1., 1., 0.) sys = append(sys1, sys2) - Q= np.mat([ [ 1, 2], [2, -1] ]) # basically feedback, output 2 in 1 + Q = np.array([[1, 2], # basically feedback, output 2 in 1 + [2, -1]]) sysc = connect(sys, Q, [2], [1, 2]) # print(sysc) np.testing.assert_array_almost_equal( - sysc.A, np.mat('1 -2 5; 3 -4 7; -6 -8 -10')) + sysc.A, np.array([[1, -2, 5], [3, -4, 7], [-6, -8, -10]])) np.testing.assert_array_almost_equal( - sysc.B, np.mat('0; 0; 1')) + sysc.B, np.array([[0], [0], [1]])) np.testing.assert_array_almost_equal( - sysc.C, np.mat('6 8 9; 0 0 1')) + sysc.C, np.array([[6, 8, 9], [0, 0, 1]])) np.testing.assert_array_almost_equal( - sysc.D, np.mat('0; 0')) + sysc.D, np.array([[0], [0]])) def testConnect2(self): - sys = append(ss([[-5, -2.25], [4, 0]], [[2], [0]], - [[0, 1.125]], [[0]]), - ss([[-1.6667, 0], [1, 0]], [[2], [0]], - [[0, 3.3333]], [[0]]), - 1) - Q = [ [ 1, 3], [2, 1], [3, -2]] + """Test append and connect() case 2""" + sys = append(ss([[-5, -2.25], + [4, 0]], + [[2], + [0]], + [[0, 1.125]], + [[0]]), + ss([[-1.6667, 0], + [1, 0]], + [[2], [0]], + [[0, 3.3333]], [[0]]), + 1) + Q = [[1, 3], + [2, 1], + [3, -2]] sysc = connect(sys, Q, [3], [3, 1, 2]) np.testing.assert_array_almost_equal( - sysc.A, np.mat([[-5, -2.25, 0, -6.6666], - [4, 0, 0, 0], - [0, 2.25, -1.6667, 0], - [0, 0, 1, 0]])) + sysc.A, np.array([[-5, -2.25, 0, -6.6666], + [4, 0, 0, 0], + [0, 2.25, -1.6667, 0], + [0, 0, 1, 0]])) np.testing.assert_array_almost_equal( - sysc.B, np.mat([[2], [0], [0], [0]])) + sysc.B, np.array([[2], [0], [0], [0]])) np.testing.assert_array_almost_equal( - sysc.C, np.mat([[0, 0, 0, -3.3333], - [0, 1.125, 0, 0], - [0, 0, 0, 3.3333]])) + sysc.C, np.array([[0, 0, 0, -3.3333], + [0, 1.125, 0, 0], + [0, 0, 0, 3.3333]])) np.testing.assert_array_almost_equal( - sysc.D, np.mat([[1], [0], [0]])) - - + sysc.D, np.array([[1], [0], [0]])) def testFRD(self): + """Test frd()""" h = tf([1], [1, 2, 2]) omega = np.logspace(-1, 2, 10) frd1 = frd(h, omega) assert isinstance(frd1, FRD) - frd2 = frd(frd1.fresp[0,0,:], omega) + frd2 = frd(frd1.fresp[0, 0, :], omega) assert isinstance(frd2, FRD) - @unittest.skipIf(not slycot_check(), "slycot not installed") + @slycotonly def testMinreal(self, verbose=False): """Test a minreal model reduction""" - #A = [-2, 0.5, 0; 0.5, -0.3, 0; 0, 0, -0.1] + # A = [-2, 0.5, 0; 0.5, -0.3, 0; 0, 0, -0.1] A = [[-2, 0.5, 0], [0.5, -0.3, 0], [0, 0, -0.1]] - #B = [0.3, -1.3; 0.1, 0; 1, 0] + # B = [0.3, -1.3; 0.1, 0; 1, 0] B = [[0.3, -1.3], [0.1, 0.], [1.0, 0.0]] - #C = [0, 0.1, 0; -0.3, -0.2, 0] + # C = [0, 0.1, 0; -0.3, -0.2, 0] C = [[0., 0.1, 0.0], [-0.3, -0.2, 0.0]] - #D = [0 -0.8; -0.3 0] + # D = [0 -0.8; -0.3 0] D = [[0., -0.8], [-0.3, 0.]] # sys = ss(A, B, C, D) sys = ss(A, B, C, D) sysr = minreal(sys, verbose=verbose) - self.assertEqual(sysr.states, 2) - self.assertEqual(sysr.inputs, sys.inputs) - self.assertEqual(sysr.outputs, sys.outputs) + assert sysr.states == 2 + assert sysr.inputs == sys.inputs + assert sysr.outputs == sys.outputs np.testing.assert_array_almost_equal( eigvals(sysr.A), [-2.136154, -0.1638459]) @@ -590,28 +710,31 @@ def testMinreal(self, verbose=False): np.testing.assert_array_almost_equal(hm.den[0][0], hr.den[0][0]) def testSS2cont(self): + """Test c2d()""" sys = ss( - np.mat("-3 4 2; -1 -3 0; 2 5 3"), - np.mat("1 4 ; -3 -3; -2 1"), - np.mat("4 2 -3; 1 4 3"), - np.mat("-2 4; 0 1")) + np.array([[-3, 4, 2], [-1, -3, 0], [2, 5, 3]]), + np.array([[1, 4], [-3, -3], [-2, 1]]), + np.array([[4, 2, -3], [1, 4, 3]]), + np.array([[-2, 4], [0, 1]])) sysd = c2d(sys, 0.1) np.testing.assert_array_almost_equal( - np.mat( - """0.742840837331905 0.342242024293711 0.203124211149560; - -0.074130792143890 0.724553295044645 -0.009143771143630; - 0.180264783290485 0.544385612448419 1.370501013067845"""), + np.array( + [[ 0.742840837331905, 0.342242024293711, 0.203124211149560], + [-0.074130792143890, 0.724553295044645, -0.009143771143630], + [ 0.180264783290485, 0.544385612448419, 1.370501013067845]]), sysd.A) np.testing.assert_array_almost_equal( - np.mat(""" 0.012362066084719 0.301932197918268; - -0.260952977031384 -0.274201791021713; - -0.304617775734327 0.075182622718853"""), sysd.B) + np.array([[ 0.012362066084719, 0.301932197918268], + [-0.260952977031384, -0.274201791021713], + [-0.304617775734327, 0.075182622718853]]), + sysd.B) def testCombi01(self): - # test from a "real" case, combines tf, ss, connect and margin - # this is a type 2 system, with phase starting at -180. The - # margin command should remove the solution for w = nearly zero + """Test from a "real" case, combines tf, ss, connect and margin. + This is a type 2 system, with phase starting at -180. The + margin command should remove the solution for w = nearly zero. + """ # Example is a concocted two-body satellite with flexible link Jb = 400; Jp = 1000; @@ -659,35 +782,30 @@ def testCombi01(self): gm, pm, wg, wp = margin(Hol) # print("%f %f %f %f" % (gm, pm, wg, wp)) - self.assertAlmostEqual(gm, 3.32065569155) - self.assertAlmostEqual(pm, 46.9740430224) - self.assertAlmostEqual(wg, 0.176469728448) - self.assertAlmostEqual(wp, 0.0616288455466) + np.testing.assert_allclose(gm, 3.32065569155) + np.testing.assert_allclose(pm, 46.9740430224) + np.testing.assert_allclose(wg, 0.176469728448) + np.testing.assert_allclose(wp, 0.0616288455466) def test_tf_string_args(self): - # Make sure that the 's' variable is defined properly + """Make sure s and z are defined properly""" s = tf('s') G = (s + 1)/(s**2 + 2*s + 1) np.testing.assert_array_almost_equal(G.num, [[[1, 1]]]) np.testing.assert_array_almost_equal(G.den, [[[1, 2, 1]]]) - self.assertTrue(isctime(G, strict=True)) + assert isctime(G, strict=True) - # Make sure that the 'z' variable is defined properly z = tf('z') G = (z + 1)/(z**2 + 2*z + 1) np.testing.assert_array_almost_equal(G.num, [[[1, 1]]]) np.testing.assert_array_almost_equal(G.den, [[[1, 2, 1]]]) - self.assertTrue(isdtime(G, strict=True)) + assert isdtime(G, strict=True) #! TODO: not yet implemented # def testMIMOtfdata(self): -# sisotf = ss2tf(self.siso_ss1) +# sisotf = ss2tf(siso.ss1) # tfdata_1 = tfdata(sisotf) -# tfdata_2 = tfdata(self.mimo_ss1, input=0, output=0) +# tfdata_2 = tfdata(mimo.ss1, input=0, output=0) # for i in range(len(tfdata)): # np.testing.assert_array_almost_equal(tfdata_1[i], tfdata_2[i]) - - -if __name__ == '__main__': - unittest.main() From 5880473475ee9946261d08e4a2e0a3650163e32a Mon Sep 17 00:00:00 2001 From: bnavigator Date: Sun, 26 Jul 2020 20:17:34 +0200 Subject: [PATCH 17/30] pytestify minreal_test --- control/tests/minreal_test.py | 75 ++++++++++++++++++----------------- 1 file changed, 39 insertions(+), 36 deletions(-) diff --git a/control/tests/minreal_test.py b/control/tests/minreal_test.py index 595bb08b0..b2d166d5a 100644 --- a/control/tests/minreal_test.py +++ b/control/tests/minreal_test.py @@ -1,27 +1,28 @@ -#!/usr/bin/env python -# -# minreal_test.py - test state space class -# Rvp, 13 Jun 2013 +"""minreal_test.py - test state space class + +Rvp, 13 Jun 2013 +""" -import unittest import numpy as np from scipy.linalg import eigvals -from control import matlab +import pytest + +from control import rss, ss, zero from control.statesp import StateSpace from control.xferfcn import TransferFunction from itertools import permutations -from control.exception import slycot_check +from control.tests.conftest import slycotonly -@unittest.skipIf(not slycot_check(), "slycot not installed") -class TestMinreal(unittest.TestCase): - """Tests for the StateSpace class.""" - def setUp(self): - np.random.seed(5) - # depending on the seed and minreal performance, a number of - # reductions is produced. If random gen or minreal change, this - # will be likely to fail - self.nreductions = 0 +@pytest.fixture +def fixedseed(scope="class"): + np.random.seed(5) + + +@slycotonly +@pytest.mark.usefixtures("fixedseed") +class TestMinreal: + """Tests for the StateSpace class.""" def assert_numden_almost_equal(self, n1, n2, d1, d2): n1[np.abs(n1) < 1e-10] = 0. @@ -35,13 +36,18 @@ def assert_numden_almost_equal(self, n1, n2, d1, d2): np.testing.assert_array_almost_equal(n1, n2) np.testing.assert_array_almost_equal(d2, d2) - def testMinrealBrute(self): + + # depending on the seed and minreal performance, a number of + # reductions is produced. If random gen or minreal change, this + # will be likely to fail + nreductions = 0 + for n, m, p in permutations(range(1,6), 3): - s = matlab.rss(n, p, m) + s = rss(n, p, m) sr = s.minreal() if s.states > sr.states: - self.nreductions += 1 + nreductions += 1 else: # Check to make sure that poles and zeros match @@ -53,30 +59,30 @@ def testMinrealBrute(self): for i in range(m): for j in range(p): # Extract SISO dynamixs from input i to output j - s1 = matlab.ss(s.A, s.B[:,i], s.C[j,:], s.D[j,i]) - s2 = matlab.ss(sr.A, sr.B[:,i], sr.C[j,:], sr.D[j,i]) + s1 = ss(s.A, s.B[:,i], s.C[j,:], s.D[j,i]) + s2 = ss(sr.A, sr.B[:,i], sr.C[j,:], sr.D[j,i]) # Check that the zeros match # Note: sorting doesn't work => have to do the hard way - z1 = matlab.zero(s1) - z2 = matlab.zero(s2) + z1 = zero(s1) + z2 = zero(s2) # Start by making sure we have the same # of zeros - self.assertEqual(len(z1), len(z2)) + assert len(z1) == len(z2) # Make sure all zeros in s1 are in s2 - for zero in z1: - # Find the closest zero - self.assertAlmostEqual(min(abs(z2 - zero)), 0.) + for z in z1: + # Find the closest zero TODO: find proper bounds + assert min(abs(z2 - z)) <= 1e-7 # Make sure all zeros in s2 are in s1 - for zero in z2: + for z in z2: # Find the closest zero - self.assertAlmostEqual(min(abs(z1 - zero)), 0.) + assert min(abs(z1 - z)) <= 1e-7 # Make sure that the number of systems reduced is as expected # (Need to update this number if you change the seed at top of file) - self.assertEqual(self.nreductions, 2) + assert nreductions == 2 def testMinrealSS(self): """Test a minreal model reduction""" @@ -92,9 +98,9 @@ def testMinrealSS(self): sys = StateSpace(A, B, C, D) sysr = sys.minreal() - self.assertEqual(sysr.states, 2) - self.assertEqual(sysr.inputs, sys.inputs) - self.assertEqual(sysr.outputs, sys.outputs) + assert sysr.states == 2 + assert sysr.inputs == sys.inputs + assert sysr.outputs == sys.outputs np.testing.assert_array_almost_equal( eigvals(sysr.A), [-2.136154, -0.1638459]) @@ -108,6 +114,3 @@ def testMinrealtf(self): np.testing.assert_array_almost_equal(hm.num[0][0], hr.num[0][0]) np.testing.assert_array_almost_equal(hm.den[0][0], hr.den[0][0]) - -if __name__ == "__main__": - unittest.main() From 97eedafab6007d1b09175ed2841b1dd4002f2a60 Mon Sep 17 00:00:00 2001 From: bnavigator Date: Tue, 29 Dec 2020 22:59:47 +0100 Subject: [PATCH 18/30] pytestify modelsimp --- control/tests/modelsimp_array_test.py | 251 ------------------------- control/tests/modelsimp_matrix_test.py | 135 ------------- control/tests/modelsimp_test.py | 225 ++++++++++++++++++++++ 3 files changed, 225 insertions(+), 386 deletions(-) delete mode 100644 control/tests/modelsimp_array_test.py delete mode 100644 control/tests/modelsimp_matrix_test.py create mode 100644 control/tests/modelsimp_test.py diff --git a/control/tests/modelsimp_array_test.py b/control/tests/modelsimp_array_test.py deleted file mode 100644 index dbd6a5796..000000000 --- a/control/tests/modelsimp_array_test.py +++ /dev/null @@ -1,251 +0,0 @@ -#!/usr/bin/env python -# -# modelsimp_test.py - test model reduction functions -# RMM, 30 Mar 2011 (based on TestModelSimp from v0.4a) - -import unittest -import numpy as np -import warnings -import control -from control.modelsimp import * -from control.matlab import * -from control.exception import slycot_check, ControlMIMONotImplemented - -class TestModelsimp(unittest.TestCase): - def setUp(self): - # Use array instead of matrix (and save old value to restore at end) - control.use_numpy_matrix(False) - - @unittest.skipIf(not slycot_check(), "slycot not installed") - def testHSVD(self): - A = np.array([[1., -2.], [3., -4.]]) - B = np.array([[5.], [7.]]) - C = np.array([[6., 8.]]) - D = np.array([[9.]]) - sys = ss(A,B,C,D) - hsv = hsvd(sys) - hsvtrue = np.array([24.42686, 0.5731395]) # from MATLAB - np.testing.assert_array_almost_equal(hsv, hsvtrue) - - # Make sure default type values are correct - self.assertTrue(isinstance(hsv, np.ndarray)) - self.assertFalse(isinstance(hsv, np.matrix)) - - # Check that using numpy.matrix does *not* affect answer - with warnings.catch_warnings(record=True) as w: - control.use_numpy_matrix(True) - self.assertTrue(issubclass(w[-1].category, UserWarning)) - - # Redefine the system (using np.matrix for storage) - sys = ss(A, B, C, D) - - # Compute the Hankel singular value decomposition - hsv = hsvd(sys) - - # Make sure that return type is correct - self.assertTrue(isinstance(hsv, np.ndarray)) - self.assertFalse(isinstance(hsv, np.matrix)) - - # Go back to using the normal np.array representation - control.use_numpy_matrix(False) - - def testMarkovSignature(self): - U = np.array([[1., 1., 1., 1., 1.]]) - Y = U - m = 3 - H = markov(Y, U, m, transpose=False) - Htrue = np.array([[1., 0., 0.]]) - np.testing.assert_array_almost_equal( H, Htrue ) - - # Make sure that transposed data also works - H = markov(np.transpose(Y), np.transpose(U), m, transpose=True) - np.testing.assert_array_almost_equal( H, np.transpose(Htrue) ) - - # Default (in v0.8.4 and below) should be transpose=True (w/ warning) - import warnings - warnings.simplefilter('always', UserWarning) # don't supress - with warnings.catch_warnings(record=True) as w: - # Set up warnings filter to only show warnings in control module - warnings.filterwarnings("ignore") - warnings.filterwarnings("always", module="control") - - # Generate Markov parameters without any arguments - H = markov(np.transpose(Y), np.transpose(U), m) - np.testing.assert_array_almost_equal( H, np.transpose(Htrue) ) - - # Make sure we got a warning - self.assertEqual(len(w), 1) - self.assertIn("assumed to be in rows", str(w[-1].message)) - self.assertIn("change in a future release", str(w[-1].message)) - - # Test example from docstring - T = np.linspace(0, 10, 100) - U = np.ones((1, 100)) - T, Y, _ = control.forced_response( - control.tf([1], [1, 0.5], True), T, U) - H = markov(Y, U, 3, transpose=False) - - # Test example from issue #395 - inp = np.array([1, 2]) - outp = np.array([2, 4]) - mrk = markov(outp, inp, 1, transpose=False) - - # Make sure MIMO generates an error - U = np.ones((2, 100)) # 2 inputs (Y unchanged, with 1 output) - np.testing.assert_raises(ControlMIMONotImplemented, markov, Y, U, m) - - # Make sure markov() returns the right answer - def testMarkovResults(self): - # - # Test over a range of parameters - # - # k = order of the system - # m = number of Markov parameters - # n = size of the data vector - # - # Values should match exactly for n = m, otherewise you get a - # close match but errors due to the assumption that C A^k B = - # 0 for k > m-2 (see modelsimp.py). - # - for k, m, n in \ - ((2, 2, 2), (2, 5, 5), (5, 2, 2), (5, 5, 5), (5, 10, 10)): - - # Generate stable continuous time system - Hc = control.rss(k, 1, 1) - - # Choose sampling time based on fastest time constant / 10 - w, _ = np.linalg.eig(Hc.A) - Ts = np.min(-np.real(w)) / 10. - - # Convert to a discrete time system via sampling - Hd = control.c2d(Hc, Ts, 'zoh') - - # Compute the Markov parameters from state space - Mtrue = np.hstack([Hd.D] + [np.dot( - Hd.C, np.dot(np.linalg.matrix_power(Hd.A, i), - Hd.B)) for i in range(m-1)]) - - # Generate input/output data - T = np.array(range(n)) * Ts - U = np.cos(T) + np.sin(T/np.pi) - _, Y, _ = control.forced_response(Hd, T, U, squeeze=True) - Mcomp = markov(Y, U, m, transpose=False) - - # Compare to results from markov() - np.testing.assert_array_almost_equal(Mtrue, Mcomp) - - def testModredMatchDC(self): - #balanced realization computed in matlab for the transfer function: - # num = [1 11 45 32], den = [1 15 60 200 60] - A = np.array( - [[-1.958, -1.194, 1.824, -1.464], - [-1.194, -0.8344, 2.563, -1.351], - [-1.824, -2.563, -1.124, 2.704], - [-1.464, -1.351, -2.704, -11.08]]) - B = np.array([[-0.9057], [-0.4068], [-0.3263], [-0.3474]]) - C = np.array([[-0.9057, -0.4068, 0.3263, -0.3474]]) - D = np.array([[0.]]) - sys = ss(A,B,C,D) - rsys = modred(sys,[2, 3],'matchdc') - Artrue = np.array([[-4.431, -4.552], [-4.552, -5.361]]) - Brtrue = np.array([[-1.362], [-1.031]]) - Crtrue = np.array([[-1.362, -1.031]]) - Drtrue = np.array([[-0.08384]]) - np.testing.assert_array_almost_equal(rsys.A, Artrue,decimal=3) - np.testing.assert_array_almost_equal(rsys.B, Brtrue,decimal=3) - np.testing.assert_array_almost_equal(rsys.C, Crtrue,decimal=3) - np.testing.assert_array_almost_equal(rsys.D, Drtrue,decimal=2) - - def testModredUnstable(self): - # Check if an error is thrown when an unstable system is given - A = np.array( - [[4.5418, 3.3999, 5.0342, 4.3808], - [0.3890, 0.3599, 0.4195, 0.1760], - [-4.2117, -3.2395, -4.6760, -4.2180], - [0.0052, 0.0429, 0.0155, 0.2743]]) - B = np.array([[1.0, 1.0], [2.0, 2.0], [3.0, 3.0], [4.0, 4.0]]) - C = np.array([[1.0, 2.0, 3.0, 4.0], [1.0, 2.0, 3.0, 4.0]]) - D = np.array([[0.0, 0.0], [0.0, 0.0]]) - sys = ss(A,B,C,D) - np.testing.assert_raises(ValueError, modred, sys, [2, 3]) - - def testModredTruncate(self): - #balanced realization computed in matlab for the transfer function: - # num = [1 11 45 32], den = [1 15 60 200 60] - A = np.array( - [[-1.958, -1.194, 1.824, -1.464], - [-1.194, -0.8344, 2.563, -1.351], - [-1.824, -2.563, -1.124, 2.704], - [-1.464, -1.351, -2.704, -11.08]]) - B = np.array([[-0.9057], [-0.4068], [-0.3263], [-0.3474]]) - C = np.array([[-0.9057, -0.4068, 0.3263, -0.3474]]) - D = np.array([[0.]]) - sys = ss(A,B,C,D) - rsys = modred(sys,[2, 3],'truncate') - Artrue = np.array([[-1.958, -1.194], [-1.194, -0.8344]]) - Brtrue = np.array([[-0.9057], [-0.4068]]) - Crtrue = np.array([[-0.9057, -0.4068]]) - Drtrue = np.array([[0.]]) - np.testing.assert_array_almost_equal(rsys.A, Artrue) - np.testing.assert_array_almost_equal(rsys.B, Brtrue) - np.testing.assert_array_almost_equal(rsys.C, Crtrue) - np.testing.assert_array_almost_equal(rsys.D, Drtrue) - - - @unittest.skipIf(not slycot_check(), "slycot not installed") - def testBalredTruncate(self): - #controlable canonical realization computed in matlab for the transfer function: - # num = [1 11 45 32], den = [1 15 60 200 60] - A = np.array( - [[-15., -7.5, -6.25, -1.875], - [8., 0., 0., 0.], - [0., 4., 0., 0.], - [0., 0., 1., 0.]]) - B = np.array([[2.], [0.], [0.], [0.]]) - C = np.array([[0.5, 0.6875, 0.7031, 0.5]]) - D = np.array([[0.]]) - sys = ss(A,B,C,D) - orders = 2 - rsys = balred(sys,orders,method='truncate') - Artrue = np.array([[-1.958, -1.194], [-1.194, -0.8344]]) - Brtrue = np.array([[0.9057], [0.4068]]) - Crtrue = np.array([[0.9057, 0.4068]]) - Drtrue = np.array([[0.]]) - np.testing.assert_array_almost_equal(rsys.A, Artrue,decimal=2) - np.testing.assert_array_almost_equal(rsys.B, Brtrue,decimal=4) - np.testing.assert_array_almost_equal(rsys.C, Crtrue,decimal=4) - np.testing.assert_array_almost_equal(rsys.D, Drtrue,decimal=4) - - @unittest.skipIf(not slycot_check(), "slycot not installed") - def testBalredMatchDC(self): - #controlable canonical realization computed in matlab for the transfer function: - # num = [1 11 45 32], den = [1 15 60 200 60] - A = np.array( - [[-15., -7.5, -6.25, -1.875], - [8., 0., 0., 0.], - [0., 4., 0., 0.], - [0., 0., 1., 0.]]) - B = np.array([[2.], [0.], [0.], [0.]]) - C = np.array([[0.5, 0.6875, 0.7031, 0.5]]) - D = np.array([[0.]]) - sys = ss(A,B,C,D) - orders = 2 - rsys = balred(sys,orders,method='matchdc') - Artrue = np.array( - [[-4.43094773, -4.55232904], - [-4.55232904, -5.36195206]]) - Brtrue = np.array([[1.36235673], [1.03114388]]) - Crtrue = np.array([[1.36235673, 1.03114388]]) - Drtrue = np.array([[-0.08383902]]) - np.testing.assert_array_almost_equal(rsys.A, Artrue,decimal=2) - np.testing.assert_array_almost_equal(rsys.B, Brtrue,decimal=4) - np.testing.assert_array_almost_equal(rsys.C, Crtrue,decimal=4) - np.testing.assert_array_almost_equal(rsys.D, Drtrue,decimal=4) - - def tearDown(self): - # Reset configuration variables to their original settings - control.config.reset_defaults() - - -if __name__ == '__main__': - unittest.main() diff --git a/control/tests/modelsimp_matrix_test.py b/control/tests/modelsimp_matrix_test.py deleted file mode 100644 index c0ba72a3b..000000000 --- a/control/tests/modelsimp_matrix_test.py +++ /dev/null @@ -1,135 +0,0 @@ -#!/usr/bin/env python -# -# modelsimp_test.py - test model reduction functions -# RMM, 30 Mar 2011 (based on TestModelSimp from v0.4a) - -import unittest -import numpy as np -from control.modelsimp import * -from control.matlab import * -from control.exception import slycot_check - -class TestModelsimp(unittest.TestCase): - @unittest.skipIf(not slycot_check(), "slycot not installed") - def testHSVD(self): - A = np.matrix("1. -2.; 3. -4.") - B = np.matrix("5.; 7.") - C = np.matrix("6. 8.") - D = np.matrix("9.") - sys = ss(A,B,C,D) - hsv = hsvd(sys) - hsvtrue = [24.42686, 0.5731395] # from MATLAB - np.testing.assert_array_almost_equal(hsv, hsvtrue) - - def testMarkov(self): - U = np.matrix("1.; 1.; 1.; 1.; 1.") - Y = U - M = 3 - H = markov(Y, U, M) - Htrue = np.matrix("1.; 0.; 0.") - np.testing.assert_array_almost_equal( H, Htrue ) - - def testModredMatchDC(self): - #balanced realization computed in matlab for the transfer function: - # num = [1 11 45 32], den = [1 15 60 200 60] - A = np.matrix('-1.958, -1.194, 1.824, -1.464; \ - -1.194, -0.8344, 2.563, -1.351; \ - -1.824, -2.563, -1.124, 2.704; \ - -1.464, -1.351, -2.704, -11.08') - B = np.matrix('-0.9057; -0.4068; -0.3263; -0.3474') - C = np.matrix('-0.9057, -0.4068, 0.3263, -0.3474') - D = np.matrix('0.') - sys = ss(A,B,C,D) - rsys = modred(sys,[2, 3],'matchdc') - Artrue = np.matrix('-4.431, -4.552; -4.552, -5.361') - Brtrue = np.matrix('-1.362; -1.031') - Crtrue = np.matrix('-1.362, -1.031') - Drtrue = np.matrix('-0.08384') - np.testing.assert_array_almost_equal(rsys.A, Artrue,decimal=3) - np.testing.assert_array_almost_equal(rsys.B, Brtrue,decimal=3) - np.testing.assert_array_almost_equal(rsys.C, Crtrue,decimal=3) - np.testing.assert_array_almost_equal(rsys.D, Drtrue,decimal=2) - - def testModredUnstable(self): - # Check if an error is thrown when an unstable system is given - A = np.matrix('4.5418, 3.3999, 5.0342, 4.3808; \ - 0.3890, 0.3599, 0.4195, 0.1760; \ - -4.2117, -3.2395, -4.6760, -4.2180; \ - 0.0052, 0.0429, 0.0155, 0.2743') - B = np.matrix('1.0, 1.0; 2.0, 2.0; 3.0, 3.0; 4.0, 4.0') - C = np.matrix('1.0, 2.0, 3.0, 4.0; 1.0, 2.0, 3.0, 4.0') - D = np.matrix('0.0, 0.0; 0.0, 0.0') - sys = ss(A,B,C,D) - np.testing.assert_raises(ValueError, modred, sys, [2, 3]) - - def testModredTruncate(self): - #balanced realization computed in matlab for the transfer function: - # num = [1 11 45 32], den = [1 15 60 200 60] - A = np.matrix('-1.958, -1.194, 1.824, -1.464; \ - -1.194, -0.8344, 2.563, -1.351; \ - -1.824, -2.563, -1.124, 2.704; \ - -1.464, -1.351, -2.704, -11.08') - B = np.matrix('-0.9057; -0.4068; -0.3263; -0.3474') - C = np.matrix('-0.9057, -0.4068, 0.3263, -0.3474') - D = np.matrix('0.') - sys = ss(A,B,C,D) - rsys = modred(sys,[2, 3],'truncate') - Artrue = np.matrix('-1.958, -1.194; -1.194, -0.8344') - Brtrue = np.matrix('-0.9057; -0.4068') - Crtrue = np.matrix('-0.9057, -0.4068') - Drtrue = np.matrix('0.') - np.testing.assert_array_almost_equal(rsys.A, Artrue) - np.testing.assert_array_almost_equal(rsys.B, Brtrue) - np.testing.assert_array_almost_equal(rsys.C, Crtrue) - np.testing.assert_array_almost_equal(rsys.D, Drtrue) - - - @unittest.skipIf(not slycot_check(), "slycot not installed") - def testBalredTruncate(self): - #controlable canonical realization computed in matlab for the transfer function: - # num = [1 11 45 32], den = [1 15 60 200 60] - A = np.matrix('-15., -7.5, -6.25, -1.875; \ - 8., 0., 0., 0.; \ - 0., 4., 0., 0.; \ - 0., 0., 1., 0.') - B = np.matrix('2.; 0.; 0.; 0.') - C = np.matrix('0.5, 0.6875, 0.7031, 0.5') - D = np.matrix('0.') - sys = ss(A,B,C,D) - orders = 2 - rsys = balred(sys,orders,method='truncate') - Artrue = np.matrix('-1.958, -1.194; -1.194, -0.8344') - Brtrue = np.matrix('0.9057; 0.4068') - Crtrue = np.matrix('0.9057, 0.4068') - Drtrue = np.matrix('0.') - np.testing.assert_array_almost_equal(rsys.A, Artrue,decimal=2) - np.testing.assert_array_almost_equal(rsys.B, Brtrue,decimal=4) - np.testing.assert_array_almost_equal(rsys.C, Crtrue,decimal=4) - np.testing.assert_array_almost_equal(rsys.D, Drtrue,decimal=4) - - @unittest.skipIf(not slycot_check(), "slycot not installed") - def testBalredMatchDC(self): - #controlable canonical realization computed in matlab for the transfer function: - # num = [1 11 45 32], den = [1 15 60 200 60] - A = np.matrix('-15., -7.5, -6.25, -1.875; \ - 8., 0., 0., 0.; \ - 0., 4., 0., 0.; \ - 0., 0., 1., 0.') - B = np.matrix('2.; 0.; 0.; 0.') - C = np.matrix('0.5, 0.6875, 0.7031, 0.5') - D = np.matrix('0.') - sys = ss(A,B,C,D) - orders = 2 - rsys = balred(sys,orders,method='matchdc') - Artrue = np.matrix('-4.43094773, -4.55232904; -4.55232904, -5.36195206') - Brtrue = np.matrix('1.36235673; 1.03114388') - Crtrue = np.matrix('1.36235673, 1.03114388') - Drtrue = np.matrix('-0.08383902') - np.testing.assert_array_almost_equal(rsys.A, Artrue,decimal=2) - np.testing.assert_array_almost_equal(rsys.B, Brtrue,decimal=4) - np.testing.assert_array_almost_equal(rsys.C, Crtrue,decimal=4) - np.testing.assert_array_almost_equal(rsys.D, Drtrue,decimal=4) - - -if __name__ == '__main__': - unittest.main() diff --git a/control/tests/modelsimp_test.py b/control/tests/modelsimp_test.py new file mode 100644 index 000000000..1e06cb4b7 --- /dev/null +++ b/control/tests/modelsimp_test.py @@ -0,0 +1,225 @@ +"""modelsimp_array_test.py - test model reduction functions + +RMM, 30 Mar 2011 (based on TestModelSimp from v0.4a) +""" + +import numpy as np +import pytest + + +from control import StateSpace, forced_response, tf, rss, c2d +from control.exception import ControlMIMONotImplemented +from control.tests.conftest import slycotonly, matarrayin +from control.modelsimp import balred, hsvd, markov, modred + + +class TestModelsimp: + """Test model reduction functions""" + + @slycotonly + def testHSVD(self, matarrayout, matarrayin): + A = matarrayin([[1., -2.], [3., -4.]]) + B = matarrayin([[5.], [7.]]) + C = matarrayin([[6., 8.]]) + D = matarrayin([[9.]]) + sys = StateSpace(A, B, C, D) + hsv = hsvd(sys) + hsvtrue = np.array([24.42686, 0.5731395]) # from MATLAB + np.testing.assert_array_almost_equal(hsv, hsvtrue) + + # test for correct return type: ALWAYS return ndarray, even when + # use_numpy_matrix(True) was used + assert isinstance(hsv, np.ndarray) + assert not isinstance(hsv, np.matrix) + + def testMarkovSignature(self, matarrayout, matarrayin): + U = matarrayin([[1., 1., 1., 1., 1.]]) + Y = U + m = 3 + H = markov(Y, U, m, transpose=False) + Htrue = np.array([[1., 0., 0.]]) + np.testing.assert_array_almost_equal(H, Htrue) + + # Make sure that transposed data also works + H = markov(np.transpose(Y), np.transpose(U), m, transpose=True) + np.testing.assert_array_almost_equal(H, np.transpose(Htrue)) + + # Default (in v0.8.4 and below) should be transpose=True (w/ warning) + with pytest.warns(UserWarning, match="assumed to be in rows.*" + "change in a future release"): + # Generate Markov parameters without any arguments + H = markov(np.transpose(Y), np.transpose(U), m) + np.testing.assert_array_almost_equal(H, np.transpose(Htrue)) + + + # Test example from docstring + T = np.linspace(0, 10, 100) + U = np.ones((1, 100)) + T, Y, _ = forced_response(tf([1], [1, 0.5], True), T, U) + H = markov(Y, U, 3, transpose=False) + + # Test example from issue #395 + inp = np.array([1, 2]) + outp = np.array([2, 4]) + mrk = markov(outp, inp, 1, transpose=False) + + # Make sure MIMO generates an error + U = np.ones((2, 100)) # 2 inputs (Y unchanged, with 1 output) + with pytest.warns(UserWarning): + with pytest.raises(ControlMIMONotImplemented): + markov(Y, U, m) + + # Make sure markov() returns the right answer + @pytest.mark.parametrize("k, m, n", + [(2, 2, 2), + (2, 5, 5), + (5, 2, 2), + (5, 5, 5), + (5, 10, 10)]) + def testMarkovResults(self, k, m, n): + # + # Test over a range of parameters + # + # k = order of the system + # m = number of Markov parameters + # n = size of the data vector + # + # Values should match exactly for n = m, otherewise you get a + # close match but errors due to the assumption that C A^k B = + # 0 for k > m-2 (see modelsimp.py). + # + + # Generate stable continuous time system + Hc = rss(k, 1, 1) + + # Choose sampling time based on fastest time constant / 10 + w, _ = np.linalg.eig(Hc.A) + Ts = np.min(-np.real(w)) / 10. + + # Convert to a discrete time system via sampling + Hd = c2d(Hc, Ts, 'zoh') + + # Compute the Markov parameters from state space + Mtrue = np.hstack([Hd.D] + [np.dot( + Hd.C, np.dot(np.linalg.matrix_power(Hd.A, i), + Hd.B)) for i in range(m-1)]) + + # Generate input/output data + T = np.array(range(n)) * Ts + U = np.cos(T) + np.sin(T/np.pi) + _, Y, _ = forced_response(Hd, T, U, squeeze=True) + Mcomp = markov(Y, U, m, transpose=False) + + # Compare to results from markov() + np.testing.assert_array_almost_equal(Mtrue, Mcomp) + + def testModredMatchDC(self, matarrayin): + #balanced realization computed in matlab for the transfer function: + # num = [1 11 45 32], den = [1 15 60 200 60] + A = matarrayin( + [[-1.958, -1.194, 1.824, -1.464], + [-1.194, -0.8344, 2.563, -1.351], + [-1.824, -2.563, -1.124, 2.704], + [-1.464, -1.351, -2.704, -11.08]]) + B = matarrayin([[-0.9057], [-0.4068], [-0.3263], [-0.3474]]) + C = matarrayin([[-0.9057, -0.4068, 0.3263, -0.3474]]) + D = matarrayin([[0.]]) + sys = StateSpace(A, B, C, D) + rsys = modred(sys,[2, 3],'matchdc') + Artrue = np.array([[-4.431, -4.552], [-4.552, -5.361]]) + Brtrue = np.array([[-1.362], [-1.031]]) + Crtrue = np.array([[-1.362, -1.031]]) + Drtrue = np.array([[-0.08384]]) + np.testing.assert_array_almost_equal(rsys.A, Artrue, decimal=3) + np.testing.assert_array_almost_equal(rsys.B, Brtrue, decimal=3) + np.testing.assert_array_almost_equal(rsys.C, Crtrue, decimal=3) + np.testing.assert_array_almost_equal(rsys.D, Drtrue, decimal=2) + + def testModredUnstable(self, matarrayin): + """Check if an error is thrown when an unstable system is given""" + A = matarrayin( + [[4.5418, 3.3999, 5.0342, 4.3808], + [0.3890, 0.3599, 0.4195, 0.1760], + [-4.2117, -3.2395, -4.6760, -4.2180], + [0.0052, 0.0429, 0.0155, 0.2743]]) + B = matarrayin([[1.0, 1.0], [2.0, 2.0], [3.0, 3.0], [4.0, 4.0]]) + C = matarrayin([[1.0, 2.0, 3.0, 4.0], [1.0, 2.0, 3.0, 4.0]]) + D = matarrayin([[0.0, 0.0], [0.0, 0.0]]) + sys = StateSpace(A, B, C, D) + np.testing.assert_raises(ValueError, modred, sys, [2, 3]) + + def testModredTruncate(self, matarrayin): + #balanced realization computed in matlab for the transfer function: + # num = [1 11 45 32], den = [1 15 60 200 60] + A = matarrayin( + [[-1.958, -1.194, 1.824, -1.464], + [-1.194, -0.8344, 2.563, -1.351], + [-1.824, -2.563, -1.124, 2.704], + [-1.464, -1.351, -2.704, -11.08]]) + B = matarrayin([[-0.9057], [-0.4068], [-0.3263], [-0.3474]]) + C = matarrayin([[-0.9057, -0.4068, 0.3263, -0.3474]]) + D = matarrayin([[0.]]) + sys = StateSpace(A, B, C, D) + rsys = modred(sys,[2, 3],'truncate') + Artrue = np.array([[-1.958, -1.194], [-1.194, -0.8344]]) + Brtrue = np.array([[-0.9057], [-0.4068]]) + Crtrue = np.array([[-0.9057, -0.4068]]) + Drtrue = np.array([[0.]]) + np.testing.assert_array_almost_equal(rsys.A, Artrue) + np.testing.assert_array_almost_equal(rsys.B, Brtrue) + np.testing.assert_array_almost_equal(rsys.C, Crtrue) + np.testing.assert_array_almost_equal(rsys.D, Drtrue) + + + @slycotonly + def testBalredTruncate(self, matarrayin): + # controlable canonical realization computed in matlab for the transfer + # function: + # num = [1 11 45 32], den = [1 15 60 200 60] + A = matarrayin( + [[-15., -7.5, -6.25, -1.875], + [8., 0., 0., 0.], + [0., 4., 0., 0.], + [0., 0., 1., 0.]]) + B = matarrayin([[2.], [0.], [0.], [0.]]) + C = matarrayin([[0.5, 0.6875, 0.7031, 0.5]]) + D = matarrayin([[0.]]) + sys = StateSpace(A, B, C, D) + orders = 2 + rsys = balred(sys, orders, method='truncate') + Artrue = np.array([[-1.958, -1.194], [-1.194, -0.8344]]) + Brtrue = np.array([[0.9057], [0.4068]]) + Crtrue = np.array([[0.9057, 0.4068]]) + Drtrue = np.array([[0.]]) + np.testing.assert_array_almost_equal(rsys.A, Artrue, decimal=2) + np.testing.assert_array_almost_equal(rsys.B, Brtrue, decimal=4) + np.testing.assert_array_almost_equal(rsys.C, Crtrue, decimal=4) + np.testing.assert_array_almost_equal(rsys.D, Drtrue, decimal=4) + + @slycotonly + def testBalredMatchDC(self, matarrayin): + # controlable canonical realization computed in matlab for the transfer + # function: + # num = [1 11 45 32], den = [1 15 60 200 60] + A = matarrayin( + [[-15., -7.5, -6.25, -1.875], + [8., 0., 0., 0.], + [0., 4., 0., 0.], + [0., 0., 1., 0.]]) + B = matarrayin([[2.], [0.], [0.], [0.]]) + C = matarrayin([[0.5, 0.6875, 0.7031, 0.5]]) + D = matarrayin([[0.]]) + sys = StateSpace(A, B, C, D) + orders = 2 + rsys = balred(sys,orders,method='matchdc') + Artrue = np.array( + [[-4.43094773, -4.55232904], + [-4.55232904, -5.36195206]]) + Brtrue = np.array([[1.36235673], [1.03114388]]) + Crtrue = np.array([[1.36235673, 1.03114388]]) + Drtrue = np.array([[-0.08383902]]) + np.testing.assert_array_almost_equal(rsys.A, Artrue, decimal=2) + np.testing.assert_array_almost_equal(rsys.B, Brtrue, decimal=4) + np.testing.assert_array_almost_equal(rsys.C, Crtrue, decimal=4) + np.testing.assert_array_almost_equal(rsys.D, Drtrue, decimal=4) + From 3eb0a9260d4bbb17bd70661bf9f3e15f027f1a2b Mon Sep 17 00:00:00 2001 From: bnavigator Date: Sun, 26 Jul 2020 21:00:51 +0200 Subject: [PATCH 19/30] pytestify nichols_test --- control/tests/nichols_test.py | 44 +++++++++++++++-------------------- 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/control/tests/nichols_test.py b/control/tests/nichols_test.py index 9cf15ae44..4cdfcaa65 100644 --- a/control/tests/nichols_test.py +++ b/control/tests/nichols_test.py @@ -1,34 +1,28 @@ -#!/usr/bin/env python -# -# nichols_test.py - test Nichols plot -# RMM, 31 Mar 2011 +"""nichols_test.py - test Nichols plot -import unittest -import numpy as np -from control.matlab import * +RMM, 31 Mar 2011 +""" -class TestStateSpace(unittest.TestCase): - """Tests for the Nichols plots.""" +import pytest - def setUp(self): - """Set up a system to test operations on.""" +from control import StateSpace, nichols_plot, nichols - A = [[-3., 4., 2.], [-1., -3., 0.], [2., 5., 3.]] - B = [[1.], [-3.], [-2.]] - C = [[4., 2., -3.]] - D = [[0.]] - self.sys = StateSpace(A, B, C, D) +@pytest.fixture() +def tsys(): + """Set up a system to test operations on.""" + A = [[-3., 4., 2.], [-1., -3., 0.], [2., 5., 3.]] + B = [[1.], [-3.], [-2.]] + C = [[4., 2., -3.]] + D = [[0.]] + return StateSpace(A, B, C, D) - def testNicholsPlain(self): - """Generate a Nichols plot.""" - nichols(self.sys) - def testNgrid(self): - """Generate a Nichols plot.""" - nichols(self.sys, grid=False) - ngrid() +def test_nichols(tsys, mplcleanup): + """Generate a Nichols plot.""" + nichols_plot(tsys) -if __name__ == "__main__": - unittest.main() +def test_nichols_alias(tsys, mplcleanup): + """Test the control.nichols alias and the grid=False parameter""" + nichols(tsys, grid=False) From 000998f6b173a2f961bf47f35c4b322660043a5e Mon Sep 17 00:00:00 2001 From: bnavigator Date: Wed, 29 Jul 2020 00:29:32 +0200 Subject: [PATCH 20/30] pytestify phaseplot_test --- control/tests/phaseplot_test.py | 45 +++++++++++++++------------------ 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/control/tests/phaseplot_test.py b/control/tests/phaseplot_test.py index 5b41615d7..8336ae975 100644 --- a/control/tests/phaseplot_test.py +++ b/control/tests/phaseplot_test.py @@ -1,27 +1,28 @@ -#!/usr/bin/env python -# -# phaseplot_test.py - test phase plot functions -# RMM, 17 24 2011 (based on TestMatlab from v0.4c) -# -# This test suite calls various phaseplot functions. Since the plots -# themselves can't be verified, this is mainly here to make sure all -# of the function arguments are handled correctly. If you run an -# individual test by itself and then type show(), it should pop open -# the figures so that you can check them visually. - -import unittest -import numpy as np -import scipy as sp +"""phaseplot_test.py - test phase plot functions + +RMM, 17 24 2011 (based on TestMatlab from v0.4c) + +This test suite calls various phaseplot functions. Since the plots +themselves can't be verified, this is mainly here to make sure all +of the function arguments are handled correctly. If you run an +individual test by itself and then type show(), it should pop open +the figures so that you can check them visually. +""" + + import matplotlib.pyplot as mpl -from control import phase_plot +import numpy as np from numpy import pi +import pytest +from control import phase_plot + -class TestPhasePlot(unittest.TestCase): - def setUp(self): - pass + +@pytest.mark.usefixtures("mplcleanup") +class TestPhasePlot: def testInvPendNoSims(self): - phase_plot(self.invpend_ode, (-6,6,10), (-6,6,10)) + phase_plot(self.invpend_ode, (-6,6,10), (-6,6,10)); def testInvPendSims(self): phase_plot(self.invpend_ode, (-6,6,10), (-6,6,10), @@ -74,12 +75,8 @@ def d1(x1x2,t): # Sample dynamical systems - inverted pendulum def invpend_ode(self, x, t, m=1., l=1., b=0, g=9.8): import numpy as np - return (x[1], -b/m*x[1] + (g*l/m)*np.sin(x[0])) + return (x[1], -b/m*x[1] + (g*l/m) * np.sin(x[0])) # Sample dynamical systems - oscillator def oscillator_ode(self, x, t, m=1., b=1, k=1, extra=None): return (x[1], -k/m*x[0] - b/m*x[1]) - - -if __name__ == '__main__': - unittest.main() From 3177796e6bb7edbbb49ed394afc65e05fecc32fa Mon Sep 17 00:00:00 2001 From: bnavigator Date: Wed, 29 Jul 2020 00:45:01 +0200 Subject: [PATCH 21/30] pytestify rlocus_test.py --- control/tests/rlocus_test.py | 67 ++++++++++++++++++------------------ 1 file changed, 34 insertions(+), 33 deletions(-) diff --git a/control/tests/rlocus_test.py b/control/tests/rlocus_test.py index d4c03307d..cf2b72cd3 100644 --- a/control/tests/rlocus_test.py +++ b/control/tests/rlocus_test.py @@ -1,12 +1,12 @@ -#!/usr/bin/env python -# -# rlocus_test.py - unit test for root locus diagrams -# RMM, 1 Jul 2011 +"""rlocus_test.py - unit test for root locus diagrams + +RMM, 1 Jul 2011 +""" -import unittest import matplotlib.pyplot as plt import numpy as np from numpy.testing import assert_array_almost_equal +import pytest from control.rlocus import root_locus, _RLClickDispatcher from control.xferfcn import TransferFunction @@ -14,17 +14,20 @@ from control.bdalg import feedback -class TestRootLocus(unittest.TestCase): +class TestRootLocus: """These are tests for the feedback function in rlocus.py.""" - def setUp(self): - """This contains some random LTI systems and scalars for testing.""" - - # Two random SISO systems. - sys1 = TransferFunction([1, 2], [1, 2, 3]) - sys2 = StateSpace([[1., 4.], [3., 2.]], [[1.], [-4.]], - [[1., 0.]], [[0.]]) - self.systems = (sys1, sys2) + @pytest.fixture(params=[(TransferFunction, ([1, 2], [1, 2, 3])), + (StateSpace, ([[1., 4.], [3., 2.]], + [[1.], [-4.]], + [[1., 0.]], [[0.]]))], + ids=["tf", "ss"]) + def sys(self, request): + """Return some simple LTI system for testing""" + # avoid construction during collection time: prevent unfiltered + # deprecation warning + sysfn, args = request.param + return sysfn(*args) def check_cl_poles(self, sys, pole_list, k_list): for k, poles in zip(k_list, pole_list): @@ -32,19 +35,18 @@ def check_cl_poles(self, sys, pole_list, k_list): poles = np.sort(poles) np.testing.assert_array_almost_equal(poles, poles_expected) - def testRootLocus(self): + def testRootLocus(self, sys): """Basic root locus plot""" klist = [-1, 0, 1] - for sys in self.systems: - roots, k_out = root_locus(sys, klist, plot=False) - np.testing.assert_equal(len(roots), len(klist)) - np.testing.assert_array_equal(klist, k_out) - self.check_cl_poles(sys, roots, klist) - def test_without_gains(self): - for sys in self.systems: - roots, kvect = root_locus(sys, plot=False) - self.check_cl_poles(sys, roots, kvect) + roots, k_out = root_locus(sys, klist, plot=False) + np.testing.assert_equal(len(roots), len(klist)) + np.testing.assert_array_equal(klist, k_out) + self.check_cl_poles(sys, roots, klist) + + def test_without_gains(self, sys): + roots, kvect = root_locus(sys, plot=False) + self.check_cl_poles(sys, roots, kvect) def test_root_locus_zoom(self): """Check the zooming functionality of the Root locus plot""" @@ -54,7 +56,9 @@ def test_root_locus_zoom(self): fig = plt.gcf() ax_rlocus = fig.axes[0] - event = type('test', (object,), {'xdata': 14.7607954359, 'ydata': -35.6171379864, 'inaxes': ax_rlocus.axes})() + event = type('test', (object,), {'xdata': 14.7607954359, + 'ydata': -35.6171379864, + 'inaxes': ax_rlocus.axes})() ax_rlocus.set_xlim((-10.813628105112421, 14.760795435937652)) ax_rlocus.set_ylim((-35.61713798641108, 33.879716621220311)) plt.get_current_fig_manager().toolbar.mode = 'zoom rect' @@ -64,12 +68,9 @@ def test_root_locus_zoom(self): zoom_y = ax_rlocus.lines[-2].get_data()[1][0:5] zoom_y = [abs(y) for y in zoom_y] - zoom_x_valid = [-5. ,- 4.61281263, - 4.16689986, - 4.04122642, - 3.90736502] - zoom_y_valid = [0. ,0., 0., 0., 0.] - - assert_array_almost_equal(zoom_x,zoom_x_valid) - assert_array_almost_equal(zoom_y,zoom_y_valid) - + zoom_x_valid = [ + -5., - 4.61281263, - 4.16689986, - 4.04122642, - 3.90736502] + zoom_y_valid = [0., 0., 0., 0., 0.] -if __name__ == "__main__": - unittest.main() + assert_array_almost_equal(zoom_x, zoom_x_valid) + assert_array_almost_equal(zoom_y, zoom_y_valid) From f745bd2ccea4a8636832cc933a3d3d17b0d45813 Mon Sep 17 00:00:00 2001 From: bnavigator Date: Tue, 29 Dec 2020 23:00:08 +0100 Subject: [PATCH 22/30] pytestify robust tests --- control/tests/robust_array_test.py | 393 ------------------ .../{robust_matrix_test.py => robust_test.py} | 129 +++--- 2 files changed, 64 insertions(+), 458 deletions(-) delete mode 100644 control/tests/robust_array_test.py rename control/tests/{robust_matrix_test.py => robust_test.py} (80%) diff --git a/control/tests/robust_array_test.py b/control/tests/robust_array_test.py deleted file mode 100644 index beb44d2de..000000000 --- a/control/tests/robust_array_test.py +++ /dev/null @@ -1,393 +0,0 @@ -import unittest -import numpy as np -import control -import control.robust -from control.exception import slycot_check - -class TestHinf(unittest.TestCase): - def setUp(self): - # Use array instead of matrix (and save old value to restore at end) - control.use_numpy_matrix(False) - - @unittest.skipIf(not slycot_check(), "slycot not installed") - def testHinfsyn(self): - """Test hinfsyn""" - p = control.ss(-1, [[1, 1]], [[1], [1]], [[0, 1], [1, 0]]) - k, cl, gam, rcond = control.robust.hinfsyn(p, 1, 1) - # from Octave, which also uses SB10AD: - # a= -1; b1= 1; b2= 1; c1= 1; c2= 1; d11= 0; d12= 1; d21= 1; d22= 0; - # g = ss(a,[b1,b2],[c1;c2],[d11,d12;d21,d22]); - # [k,cl] = hinfsyn(g,1,1); - np.testing.assert_array_almost_equal(k.A, [[-3]]) - np.testing.assert_array_almost_equal(k.B, [[1]]) - np.testing.assert_array_almost_equal(k.C, [[-1]]) - np.testing.assert_array_almost_equal(k.D, [[0]]) - np.testing.assert_array_almost_equal(cl.A, [[-1, -1], [1, -3]]) - np.testing.assert_array_almost_equal(cl.B, [[1], [1]]) - np.testing.assert_array_almost_equal(cl.C, [[1, -1]]) - np.testing.assert_array_almost_equal(cl.D, [[0]]) - - # TODO: add more interesting examples - - def tearDown(self): - control.config.reset_defaults() - - -class TestH2(unittest.TestCase): - def setUp(self): - # Use array instead of matrix (and save old value to restore at end) - control.use_numpy_matrix(False) - - @unittest.skipIf(not slycot_check(), "slycot not installed") - def testH2syn(self): - """Test h2syn""" - p = control.ss(-1, [[1, 1]], [[1], [1]], [[0, 1], [1, 0]]) - k = control.robust.h2syn(p, 1, 1) - # from Octave, which also uses SB10HD for H-2 synthesis: - # a= -1; b1= 1; b2= 1; c1= 1; c2= 1; d11= 0; d12= 1; d21= 1; d22= 0; - # g = ss(a,[b1,b2],[c1;c2],[d11,d12;d21,d22]); - # k = h2syn(g,1,1); - # the solution is the same as for the hinfsyn test - np.testing.assert_array_almost_equal(k.A, [[-3]]) - np.testing.assert_array_almost_equal(k.B, [[1]]) - np.testing.assert_array_almost_equal(k.C, [[-1]]) - np.testing.assert_array_almost_equal(k.D, [[0]]) - - def tearDown(self): - control.config.reset_defaults() - - -class TestAugw(unittest.TestCase): - """Test control.robust.augw""" - def setUp(self): - # Use array instead of matrix (and save old value to restore at end) - control.use_numpy_matrix(False) - - # tolerance for system equality - TOL = 1e-8 - - def siso_almost_equal(self, g, h): - """siso_almost_equal(g,h) -> None - Raises AssertionError if g and h, two SISO LTI objects, are not almost equal""" - from control import tf, minreal - gmh = tf(minreal(g - h, verbose=False)) - if not (gmh.num[0][0] < self.TOL).all(): - maxnum = max(abs(gmh.num[0][0])) - raise AssertionError( - 'systems not approx equal; max num. coeff is {}\nsys 1:\n{}\nsys 2:\n{}'.format( - maxnum, g, h)) - - @unittest.skipIf(not slycot_check(), "slycot not installed") - def testSisoW1(self): - """SISO plant with S weighting""" - from control import augw, ss - g = ss([-1.], [1.], [1.], [1.]) - w1 = ss([-2], [2.], [1.], [2.]) - p = augw(g, w1) - self.assertEqual(2, p.outputs) - self.assertEqual(2, p.inputs) - # w->z1 should be w1 - self.siso_almost_equal(w1, p[0, 0]) - # w->v should be 1 - self.siso_almost_equal(ss([], [], [], [1]), p[1, 0]) - # u->z1 should be -w1*g - self.siso_almost_equal(-w1 * g, p[0, 1]) - # u->v should be -g - self.siso_almost_equal(-g, p[1, 1]) - - @unittest.skipIf(not slycot_check(), "slycot not installed") - def testSisoW2(self): - """SISO plant with KS weighting""" - from control import augw, ss - g = ss([-1.], [1.], [1.], [1.]) - w2 = ss([-2], [1.], [1.], [2.]) - p = augw(g, w2=w2) - self.assertEqual(2, p.outputs) - self.assertEqual(2, p.inputs) - # w->z2 should be 0 - self.siso_almost_equal(ss([], [], [], 0), p[0, 0]) - # w->v should be 1 - self.siso_almost_equal(ss([], [], [], [1]), p[1, 0]) - # u->z2 should be w2 - self.siso_almost_equal(w2, p[0, 1]) - # u->v should be -g - self.siso_almost_equal(-g, p[1, 1]) - - @unittest.skipIf(not slycot_check(), "slycot not installed") - def testSisoW3(self): - """SISO plant with T weighting""" - from control import augw, ss - g = ss([-1.], [1.], [1.], [1.]) - w3 = ss([-2], [1.], [1.], [2.]) - p = augw(g, w3=w3) - self.assertEqual(2, p.outputs) - self.assertEqual(2, p.inputs) - # w->z3 should be 0 - self.siso_almost_equal(ss([], [], [], 0), p[0, 0]) - # w->v should be 1 - self.siso_almost_equal(ss([], [], [], [1]), p[1, 0]) - # u->z3 should be w3*g - self.siso_almost_equal(w3 * g, p[0, 1]) - # u->v should be -g - self.siso_almost_equal(-g, p[1, 1]) - - @unittest.skipIf(not slycot_check(), "slycot not installed") - def testSisoW123(self): - """SISO plant with all weights""" - from control import augw, ss - g = ss([-1.], [1.], [1.], [1.]) - w1 = ss([-2.], [2.], [1.], [2.]) - w2 = ss([-3.], [3.], [1.], [3.]) - w3 = ss([-4.], [4.], [1.], [4.]) - p = augw(g, w1, w2, w3) - self.assertEqual(4, p.outputs) - self.assertEqual(2, p.inputs) - # w->z1 should be w1 - self.siso_almost_equal(w1, p[0, 0]) - # w->z2 should be 0 - self.siso_almost_equal(0, p[1, 0]) - # w->z3 should be 0 - self.siso_almost_equal(0, p[2, 0]) - # w->v should be 1 - self.siso_almost_equal(ss([], [], [], [1]), p[3, 0]) - # u->z1 should be -w1*g - self.siso_almost_equal(-w1 * g, p[0, 1]) - # u->z2 should be w2 - self.siso_almost_equal(w2, p[1, 1]) - # u->z3 should be w3*g - self.siso_almost_equal(w3 * g, p[2, 1]) - # u->v should be -g - self.siso_almost_equal(-g, p[3, 1]) - - @unittest.skipIf(not slycot_check(), "slycot not installed") - def testMimoW1(self): - """MIMO plant with S weighting""" - from control import augw, ss - g = ss([[-1., -2], [-3, -4]], - [[1., 0.], [0., 1.]], - [[1., 0.], [0., 1.]], - [[1., 0.], [0., 1.]]) - w1 = ss([-2], [2.], [1.], [2.]) - p = augw(g, w1) - self.assertEqual(4, p.outputs) - self.assertEqual(4, p.inputs) - # w->z1 should be diag(w1,w1) - self.siso_almost_equal(w1, p[0, 0]) - self.siso_almost_equal(0, p[0, 1]) - self.siso_almost_equal(0, p[1, 0]) - self.siso_almost_equal(w1, p[1, 1]) - # w->v should be I - self.siso_almost_equal(1, p[2, 0]) - self.siso_almost_equal(0, p[2, 1]) - self.siso_almost_equal(0, p[3, 0]) - self.siso_almost_equal(1, p[3, 1]) - # u->z1 should be -w1*g - self.siso_almost_equal(-w1 * g[0, 0], p[0, 2]) - self.siso_almost_equal(-w1 * g[0, 1], p[0, 3]) - self.siso_almost_equal(-w1 * g[1, 0], p[1, 2]) - self.siso_almost_equal(-w1 * g[1, 1], p[1, 3]) - # # u->v should be -g - self.siso_almost_equal(-g[0, 0], p[2, 2]) - self.siso_almost_equal(-g[0, 1], p[2, 3]) - self.siso_almost_equal(-g[1, 0], p[3, 2]) - self.siso_almost_equal(-g[1, 1], p[3, 3]) - - @unittest.skipIf(not slycot_check(), "slycot not installed") - def testMimoW2(self): - """MIMO plant with KS weighting""" - from control import augw, ss - g = ss([[-1., -2], [-3, -4]], - [[1., 0.], [0., 1.]], - [[1., 0.], [0., 1.]], - [[1., 0.], [0., 1.]]) - w2 = ss([-2], [2.], [1.], [2.]) - p = augw(g, w2=w2) - self.assertEqual(4, p.outputs) - self.assertEqual(4, p.inputs) - # w->z2 should be 0 - self.siso_almost_equal(0, p[0, 0]) - self.siso_almost_equal(0, p[0, 1]) - self.siso_almost_equal(0, p[1, 0]) - self.siso_almost_equal(0, p[1, 1]) - # w->v should be I - self.siso_almost_equal(1, p[2, 0]) - self.siso_almost_equal(0, p[2, 1]) - self.siso_almost_equal(0, p[3, 0]) - self.siso_almost_equal(1, p[3, 1]) - # u->z2 should be w2 - self.siso_almost_equal(w2, p[0, 2]) - self.siso_almost_equal(0, p[0, 3]) - self.siso_almost_equal(0, p[1, 2]) - self.siso_almost_equal(w2, p[1, 3]) - # # u->v should be -g - self.siso_almost_equal(-g[0, 0], p[2, 2]) - self.siso_almost_equal(-g[0, 1], p[2, 3]) - self.siso_almost_equal(-g[1, 0], p[3, 2]) - self.siso_almost_equal(-g[1, 1], p[3, 3]) - - @unittest.skipIf(not slycot_check(), "slycot not installed") - def testMimoW3(self): - """MIMO plant with T weighting""" - from control import augw, ss - g = ss([[-1., -2], [-3, -4]], - [[1., 0.], [0., 1.]], - [[1., 0.], [0., 1.]], - [[1., 0.], [0., 1.]]) - w3 = ss([-2], [2.], [1.], [2.]) - p = augw(g, w3=w3) - self.assertEqual(4, p.outputs) - self.assertEqual(4, p.inputs) - # w->z3 should be 0 - self.siso_almost_equal(0, p[0, 0]) - self.siso_almost_equal(0, p[0, 1]) - self.siso_almost_equal(0, p[1, 0]) - self.siso_almost_equal(0, p[1, 1]) - # w->v should be I - self.siso_almost_equal(1, p[2, 0]) - self.siso_almost_equal(0, p[2, 1]) - self.siso_almost_equal(0, p[3, 0]) - self.siso_almost_equal(1, p[3, 1]) - # u->z3 should be w3*g - self.siso_almost_equal(w3 * g[0, 0], p[0, 2]) - self.siso_almost_equal(w3 * g[0, 1], p[0, 3]) - self.siso_almost_equal(w3 * g[1, 0], p[1, 2]) - self.siso_almost_equal(w3 * g[1, 1], p[1, 3]) - # # u->v should be -g - self.siso_almost_equal(-g[0, 0], p[2, 2]) - self.siso_almost_equal(-g[0, 1], p[2, 3]) - self.siso_almost_equal(-g[1, 0], p[3, 2]) - self.siso_almost_equal(-g[1, 1], p[3, 3]) - - @unittest.skipIf(not slycot_check(), "slycot not installed") - def testMimoW123(self): - """MIMO plant with all weights""" - from control import augw, ss, append, minreal - g = ss([[-1., -2], [-3, -4]], - [[1., 0.], [0., 1.]], - [[1., 0.], [0., 1.]], - [[1., 0.], [0., 1.]]) - # this should be expaned to w1*I - w1 = ss([-2.], [2.], [1.], [2.]) - # diagonal weighting - w2 = append(ss([-3.], [3.], [1.], [3.]), ss([-4.], [4.], [1.], [4.])) - # full weighting - w3 = ss([[-4., -5], [-6, -7]], - [[2., 3.], [5., 7.]], - [[11., 13.], [17., 19.]], - [[23., 29.], [31., 37.]]) - p = augw(g, w1, w2, w3) - self.assertEqual(8, p.outputs) - self.assertEqual(4, p.inputs) - # w->z1 should be w1 - self.siso_almost_equal(w1, p[0, 0]) - self.siso_almost_equal(0, p[0, 1]) - self.siso_almost_equal(0, p[1, 0]) - self.siso_almost_equal(w1, p[1, 1]) - # w->z2 should be 0 - self.siso_almost_equal(0, p[2, 0]) - self.siso_almost_equal(0, p[2, 1]) - self.siso_almost_equal(0, p[3, 0]) - self.siso_almost_equal(0, p[3, 1]) - # w->z3 should be 0 - self.siso_almost_equal(0, p[4, 0]) - self.siso_almost_equal(0, p[4, 1]) - self.siso_almost_equal(0, p[5, 0]) - self.siso_almost_equal(0, p[5, 1]) - # w->v should be I - self.siso_almost_equal(1, p[6, 0]) - self.siso_almost_equal(0, p[6, 1]) - self.siso_almost_equal(0, p[7, 0]) - self.siso_almost_equal(1, p[7, 1]) - - # u->z1 should be -w1*g - self.siso_almost_equal(-w1 * g[0, 0], p[0, 2]) - self.siso_almost_equal(-w1 * g[0, 1], p[0, 3]) - self.siso_almost_equal(-w1 * g[1, 0], p[1, 2]) - self.siso_almost_equal(-w1 * g[1, 1], p[1, 3]) - # u->z2 should be w2 - self.siso_almost_equal(w2[0, 0], p[2, 2]) - self.siso_almost_equal(w2[0, 1], p[2, 3]) - self.siso_almost_equal(w2[1, 0], p[3, 2]) - self.siso_almost_equal(w2[1, 1], p[3, 3]) - # u->z3 should be w3*g - w3g = w3 * g; - self.siso_almost_equal(w3g[0, 0], minreal(p[4, 2])) - self.siso_almost_equal(w3g[0, 1], minreal(p[4, 3])) - self.siso_almost_equal(w3g[1, 0], minreal(p[5, 2])) - self.siso_almost_equal(w3g[1, 1], minreal(p[5, 3])) - # u->v should be -g - self.siso_almost_equal(-g[0, 0], p[6, 2]) - self.siso_almost_equal(-g[0, 1], p[6, 3]) - self.siso_almost_equal(-g[1, 0], p[7, 2]) - self.siso_almost_equal(-g[1, 1], p[7, 3]) - - @unittest.skipIf(not slycot_check(), "slycot not installed") - def testErrors(self): - """Error cases handled""" - from control import augw, ss - # no weights - g1by1 = ss(-1, 1, 1, 0) - g2by2 = ss(-np.eye(2), np.eye(2), np.eye(2), np.zeros((2, 2))) - self.assertRaises(ValueError, augw, g1by1) - # mismatched size of weight and plant - self.assertRaises(ValueError, augw, g1by1, w1=g2by2) - self.assertRaises(ValueError, augw, g1by1, w2=g2by2) - self.assertRaises(ValueError, augw, g1by1, w3=g2by2) - - def tearDown(self): - control.config.reset_defaults() - - -class TestMixsyn(unittest.TestCase): - """Test control.robust.mixsyn""" - def setUp(self): - # Use array instead of matrix (and save old value to restore at end) - control.use_numpy_matrix(False) - - # it's a relatively simple wrapper; compare results with augw, hinfsyn - @unittest.skipIf(not slycot_check(), "slycot not installed") - def testSiso(self): - """mixsyn with SISO system""" - from control import tf, augw, hinfsyn, mixsyn - from control import ss - # Skogestad+Postlethwaite, Multivariable Feedback Control, 1st Ed., Example 2.11 - s = tf([1, 0], 1) - # plant - g = 200 / (10 * s + 1) / (0.05 * s + 1) ** 2 - # sensitivity weighting - M = 1.5 - wb = 10 - A = 1e-4 - w1 = (s / M + wb) / (s + wb * A) - # KS weighting - w2 = tf(1, 1) - - p = augw(g, w1, w2) - kref, clref, gam, rcond = hinfsyn(p, 1, 1) - ktest, cltest, info = mixsyn(g, w1, w2) - # check similar to S+P's example - np.testing.assert_allclose(gam, 1.37, atol=1e-2) - - # mixsyn is a convenience wrapper around augw and hinfsyn, so - # results will be exactly the same. Given than, use the lazy - # but fragile testing option. - np.testing.assert_allclose(ktest.A, kref.A) - np.testing.assert_allclose(ktest.B, kref.B) - np.testing.assert_allclose(ktest.C, kref.C) - np.testing.assert_allclose(ktest.D, kref.D) - - np.testing.assert_allclose(cltest.A, clref.A) - np.testing.assert_allclose(cltest.B, clref.B) - np.testing.assert_allclose(cltest.C, clref.C) - np.testing.assert_allclose(cltest.D, clref.D) - - np.testing.assert_allclose(gam, info[0]) - - np.testing.assert_allclose(rcond, info[1]) - - def tearDown(self): - control.config.reset_defaults() - - -if __name__ == "__main__": - unittest.main() diff --git a/control/tests/robust_matrix_test.py b/control/tests/robust_test.py similarity index 80% rename from control/tests/robust_matrix_test.py rename to control/tests/robust_test.py index b23f06c52..2c1a03ef6 100644 --- a/control/tests/robust_matrix_test.py +++ b/control/tests/robust_test.py @@ -1,16 +1,20 @@ -import unittest +"""robust_array_test.py""" + import numpy as np -import control -import control.robust -from control.exception import slycot_check +import pytest + +from control import append, minreal, ss, tf +from control.robust import augw, h2syn, hinfsyn, mixsyn +from control.tests.conftest import slycotonly -class TestHinf(unittest.TestCase): - @unittest.skipIf(not slycot_check(), "slycot not installed") +class TestHinf: + + @slycotonly def testHinfsyn(self): """Test hinfsyn""" - p = control.ss(-1, [1, 1], [[1], [1]], [[0, 1], [1, 0]]) - k, cl, gam, rcond = control.robust.hinfsyn(p, 1, 1) + p = ss(-1, [[1, 1]], [[1], [1]], [[0, 1], [1, 0]]) + k, cl, gam, rcond = hinfsyn(p, 1, 1) # from Octave, which also uses SB10AD: # a= -1; b1= 1; b2= 1; c1= 1; c2= 1; d11= 0; d12= 1; d21= 1; d22= 0; # g = ss(a,[b1,b2],[c1;c2],[d11,d12;d21,d22]); @@ -26,13 +30,13 @@ def testHinfsyn(self): # TODO: add more interesting examples +class TestH2: -class TestH2(unittest.TestCase): - @unittest.skipIf(not slycot_check(), "slycot not installed") + @slycotonly def testH2syn(self): """Test h2syn""" - p = control.ss(-1, [1, 1], [[1], [1]], [[0, 1], [1, 0]]) - k = control.robust.h2syn(p, 1, 1) + p = ss(-1, [[1, 1]], [[1], [1]], [[0, 1], [1, 0]]) + k = h2syn(p, 1, 1) # from Octave, which also uses SB10HD for H-2 synthesis: # a= -1; b1= 1; b2= 1; c1= 1; c2= 1; d11= 0; d12= 1; d21= 1; d22= 0; # g = ss(a,[b1,b2],[c1;c2],[d11,d12;d21,d22]); @@ -44,32 +48,36 @@ def testH2syn(self): np.testing.assert_array_almost_equal(k.D, [[0]]) -class TestAugw(unittest.TestCase): - """Test control.robust.augw""" +class TestAugw: # tolerance for system equality TOL = 1e-8 def siso_almost_equal(self, g, h): """siso_almost_equal(g,h) -> None - Raises AssertionError if g and h, two SISO LTI objects, are not almost equal""" - from control import tf, minreal + + Raises AssertionError if g and h, two SISO LTI objects, are not almost + equal + """ + # TODO: use pytest's assertion rewriting feature gmh = tf(minreal(g - h, verbose=False)) if not (gmh.num[0][0] < self.TOL).all(): maxnum = max(abs(gmh.num[0][0])) - raise AssertionError( - 'systems not approx equal; max num. coeff is {}\nsys 1:\n{}\nsys 2:\n{}'.format( - maxnum, g, h)) + raise AssertionError("systems not approx equal; " + "max num. coeff is {}\n" + "sys 1:\n" + "{}\n" + "sys 2:\n" + "{}".format(maxnum, g, h)) - @unittest.skipIf(not slycot_check(), "slycot not installed") + @slycotonly def testSisoW1(self): """SISO plant with S weighting""" - from control import augw, ss g = ss([-1.], [1.], [1.], [1.]) w1 = ss([-2], [2.], [1.], [2.]) p = augw(g, w1) - self.assertEqual(2, p.outputs) - self.assertEqual(2, p.inputs) + assert p.outputs == 2 + assert p.inputs == 2 # w->z1 should be w1 self.siso_almost_equal(w1, p[0, 0]) # w->v should be 1 @@ -79,15 +87,14 @@ def testSisoW1(self): # u->v should be -g self.siso_almost_equal(-g, p[1, 1]) - @unittest.skipIf(not slycot_check(), "slycot not installed") + @slycotonly def testSisoW2(self): """SISO plant with KS weighting""" - from control import augw, ss g = ss([-1.], [1.], [1.], [1.]) w2 = ss([-2], [1.], [1.], [2.]) p = augw(g, w2=w2) - self.assertEqual(2, p.outputs) - self.assertEqual(2, p.inputs) + assert p.outputs == 2 + assert p.inputs == 2 # w->z2 should be 0 self.siso_almost_equal(ss([], [], [], 0), p[0, 0]) # w->v should be 1 @@ -97,15 +104,14 @@ def testSisoW2(self): # u->v should be -g self.siso_almost_equal(-g, p[1, 1]) - @unittest.skipIf(not slycot_check(), "slycot not installed") + @slycotonly def testSisoW3(self): """SISO plant with T weighting""" - from control import augw, ss g = ss([-1.], [1.], [1.], [1.]) w3 = ss([-2], [1.], [1.], [2.]) p = augw(g, w3=w3) - self.assertEqual(2, p.outputs) - self.assertEqual(2, p.inputs) + assert p.outputs == 2 + assert p.inputs == 2 # w->z3 should be 0 self.siso_almost_equal(ss([], [], [], 0), p[0, 0]) # w->v should be 1 @@ -115,17 +121,16 @@ def testSisoW3(self): # u->v should be -g self.siso_almost_equal(-g, p[1, 1]) - @unittest.skipIf(not slycot_check(), "slycot not installed") + @slycotonly def testSisoW123(self): """SISO plant with all weights""" - from control import augw, ss g = ss([-1.], [1.], [1.], [1.]) w1 = ss([-2.], [2.], [1.], [2.]) w2 = ss([-3.], [3.], [1.], [3.]) w3 = ss([-4.], [4.], [1.], [4.]) p = augw(g, w1, w2, w3) - self.assertEqual(4, p.outputs) - self.assertEqual(2, p.inputs) + assert p.outputs == 4 + assert p.inputs == 2 # w->z1 should be w1 self.siso_almost_equal(w1, p[0, 0]) # w->z2 should be 0 @@ -143,18 +148,17 @@ def testSisoW123(self): # u->v should be -g self.siso_almost_equal(-g, p[3, 1]) - @unittest.skipIf(not slycot_check(), "slycot not installed") + @slycotonly def testMimoW1(self): """MIMO plant with S weighting""" - from control import augw, ss g = ss([[-1., -2], [-3, -4]], [[1., 0.], [0., 1.]], [[1., 0.], [0., 1.]], [[1., 0.], [0., 1.]]) w1 = ss([-2], [2.], [1.], [2.]) p = augw(g, w1) - self.assertEqual(4, p.outputs) - self.assertEqual(4, p.inputs) + assert p.outputs == 4 + assert p.inputs == 4 # w->z1 should be diag(w1,w1) self.siso_almost_equal(w1, p[0, 0]) self.siso_almost_equal(0, p[0, 1]) @@ -176,18 +180,17 @@ def testMimoW1(self): self.siso_almost_equal(-g[1, 0], p[3, 2]) self.siso_almost_equal(-g[1, 1], p[3, 3]) - @unittest.skipIf(not slycot_check(), "slycot not installed") + @slycotonly def testMimoW2(self): """MIMO plant with KS weighting""" - from control import augw, ss g = ss([[-1., -2], [-3, -4]], [[1., 0.], [0., 1.]], [[1., 0.], [0., 1.]], [[1., 0.], [0., 1.]]) w2 = ss([-2], [2.], [1.], [2.]) p = augw(g, w2=w2) - self.assertEqual(4, p.outputs) - self.assertEqual(4, p.inputs) + assert p.outputs == 4 + assert p.inputs == 4 # w->z2 should be 0 self.siso_almost_equal(0, p[0, 0]) self.siso_almost_equal(0, p[0, 1]) @@ -209,18 +212,17 @@ def testMimoW2(self): self.siso_almost_equal(-g[1, 0], p[3, 2]) self.siso_almost_equal(-g[1, 1], p[3, 3]) - @unittest.skipIf(not slycot_check(), "slycot not installed") + @slycotonly def testMimoW3(self): """MIMO plant with T weighting""" - from control import augw, ss g = ss([[-1., -2], [-3, -4]], [[1., 0.], [0., 1.]], [[1., 0.], [0., 1.]], [[1., 0.], [0., 1.]]) w3 = ss([-2], [2.], [1.], [2.]) p = augw(g, w3=w3) - self.assertEqual(4, p.outputs) - self.assertEqual(4, p.inputs) + assert p.outputs == 4 + assert p.inputs == 4 # w->z3 should be 0 self.siso_almost_equal(0, p[0, 0]) self.siso_almost_equal(0, p[0, 1]) @@ -242,10 +244,9 @@ def testMimoW3(self): self.siso_almost_equal(-g[1, 0], p[3, 2]) self.siso_almost_equal(-g[1, 1], p[3, 3]) - @unittest.skipIf(not slycot_check(), "slycot not installed") + @slycotonly def testMimoW123(self): """MIMO plant with all weights""" - from control import augw, ss, append, minreal g = ss([[-1., -2], [-3, -4]], [[1., 0.], [0., 1.]], [[1., 0.], [0., 1.]], @@ -260,8 +261,8 @@ def testMimoW123(self): [[11., 13.], [17., 19.]], [[23., 29.], [31., 37.]]) p = augw(g, w1, w2, w3) - self.assertEqual(8, p.outputs) - self.assertEqual(4, p.inputs) + assert p.outputs == 8 + assert p.inputs == 4 # w->z1 should be w1 self.siso_almost_equal(w1, p[0, 0]) self.siso_almost_equal(0, p[0, 1]) @@ -294,7 +295,7 @@ def testMimoW123(self): self.siso_almost_equal(w2[1, 0], p[3, 2]) self.siso_almost_equal(w2[1, 1], p[3, 3]) # u->z3 should be w3*g - w3g = w3 * g; + w3g = w3 * g self.siso_almost_equal(w3g[0, 0], minreal(p[4, 2])) self.siso_almost_equal(w3g[0, 1], minreal(p[4, 3])) self.siso_almost_equal(w3g[1, 0], minreal(p[5, 2])) @@ -305,29 +306,31 @@ def testMimoW123(self): self.siso_almost_equal(-g[1, 0], p[7, 2]) self.siso_almost_equal(-g[1, 1], p[7, 3]) - @unittest.skipIf(not slycot_check(), "slycot not installed") + @slycotonly def testErrors(self): """Error cases handled""" from control import augw, ss # no weights g1by1 = ss(-1, 1, 1, 0) g2by2 = ss(-np.eye(2), np.eye(2), np.eye(2), np.zeros((2, 2))) - self.assertRaises(ValueError, augw, g1by1) + with pytest.raises(ValueError): + augw(g1by1) # mismatched size of weight and plant - self.assertRaises(ValueError, augw, g1by1, w1=g2by2) - self.assertRaises(ValueError, augw, g1by1, w2=g2by2) - self.assertRaises(ValueError, augw, g1by1, w3=g2by2) + with pytest.raises(ValueError): + augw(g1by1, w1=g2by2) + with pytest.raises(ValueError): + augw(g1by1, w2=g2by2) + with pytest.raises(ValueError): + augw(g1by1, w3=g2by2) -class TestMixsyn(unittest.TestCase): +class TestMixsyn: """Test control.robust.mixsyn""" # it's a relatively simple wrapper; compare results with augw, hinfsyn - @unittest.skipIf(not slycot_check(), "slycot not installed") + @slycotonly def testSiso(self): """mixsyn with SISO system""" - from control import tf, augw, hinfsyn, mixsyn - from control import ss # Skogestad+Postlethwaite, Multivariable Feedback Control, 1st Ed., Example 2.11 s = tf([1, 0], 1) # plant @@ -362,7 +365,3 @@ def testSiso(self): np.testing.assert_allclose(gam, info[0]) np.testing.assert_allclose(rcond, info[1]) - - -if __name__ == "__main__": - unittest.main() From 2ee0773436a6dfb6b60b3afb5f2c81f937734be0 Mon Sep 17 00:00:00 2001 From: bnavigator Date: Tue, 29 Dec 2020 02:22:21 +0100 Subject: [PATCH 23/30] pytestify sisotool_test.py --- control/tests/sisotool_test.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/control/tests/sisotool_test.py b/control/tests/sisotool_test.py index 5b627c22d..65f87f28b 100644 --- a/control/tests/sisotool_test.py +++ b/control/tests/sisotool_test.py @@ -1,22 +1,26 @@ -import unittest +"""sisotool_test.py""" + import matplotlib.pyplot as plt import numpy as np from numpy.testing import assert_array_almost_equal +import pytest from control.sisotool import sisotool from control.rlocus import _RLClickDispatcher from control.xferfcn import TransferFunction -class TestSisotool(unittest.TestCase): +@pytest.mark.usefixtures("mplcleanup") +class TestSisotool: """These are tests for the sisotool in sisotool.py.""" - def setUp(self): - # One random SISO system. - self.system = TransferFunction([1000], [1, 25, 100, 0]) + @pytest.fixture + def sys(self): + """Return a generic SISO transfer function""" + return TransferFunction([1000], [1, 25, 100, 0]) - def test_sisotool(self): - sisotool(self.system, Hz=False) + def test_sisotool(self, sys): + sisotool(sys, Hz=False) fig = plt.gcf() ax_mag, ax_rlocus, ax_phase, ax_step = fig.axes[:4] @@ -57,7 +61,7 @@ def test_sisotool(self): event = type('test', (object,), {'xdata': 2.31206868287, 'ydata': 15.5983051046, 'inaxes': ax_rlocus.axes})() - _RLClickDispatcher(event=event, sys=self.system, fig=fig, + _RLClickDispatcher(event=event, sys=sys, fig=fig, ax_rlocus=ax_rlocus, sisotool=True, plotstr='-', bode_plot_params=bode_plot_params, tvect=None) @@ -82,13 +86,9 @@ def test_sisotool(self): # Check if the step response has changed # new array needed because change in compute step response default time step_response_moved = np.array( - [0. , 0.0072, 0.0516, 0.1554, 0.3281, 0.5681, 0.8646, 1.1987, - 1.5452, 1.875 ]) - #old: array([0., 0.0239, 0.161 , 0.4547, 0.8903, 1.407, - # 1.9121, 2.2989, 2.4686, 2.353]) + [0., 0.0072, 0.0516, 0.1554, 0.3281, 0.5681, 0.8646, 1.1987, + 1.5452, 1.875]) + # old: array([0., 0.0239, 0.161 , 0.4547, 0.8903, 1.407, + # 1.9121, 2.2989, 2.4686, 2.353]) assert_array_almost_equal( ax_step.lines[0].get_data()[1][:10], step_response_moved, 4) - - -if __name__ == "__main__": - unittest.main() From e660363c8af4723b4066c8361cd2cf6a6e04ee74 Mon Sep 17 00:00:00 2001 From: bnavigator Date: Wed, 29 Jul 2020 01:39:11 +0200 Subject: [PATCH 24/30] pytestify slycot_convert_test.py --- control/tests/slycot_convert_test.py | 394 ++++++++++++++------------- 1 file changed, 205 insertions(+), 189 deletions(-) diff --git a/control/tests/slycot_convert_test.py b/control/tests/slycot_convert_test.py index e13bcea8f..edd355b3b 100644 --- a/control/tests/slycot_convert_test.py +++ b/control/tests/slycot_convert_test.py @@ -1,197 +1,213 @@ -#!/usr/bin/env python -# -# slycot_convert_test.py - test SLICOT-based conversions -# RMM, 30 Mar 2011 (based on TestSlycot from v0.4a) +"""slycot_convert_test.py - test SLICOT-based conversions + +RMM, 30 Mar 2011 (based on TestSlycot from v0.4a) +""" -from __future__ import print_function -import unittest import numpy as np -from control import matlab -from control.exception import slycot_check - - -@unittest.skipIf(not slycot_check(), "slycot not installed") -class TestSlycot(unittest.TestCase): - """TestSlycot compares transfer function and state space conversions for - various numbers of inputs,outputs and states. - 1. Usually passes for SISO systems of any state dim, occasonally, - there will be a dimension mismatch if the original randomly - generated ss system is not minimal because td04ad returns a - minimal system. - - 2. For small systems with many inputs, n<5 and with 2 or more - outputs the conversion to statespace (td04ad) intermittently - results in an equivalent realization of higher order than the - original tf order. We think this has to do with minimu - realization tolerances in the Fortran. The algorithm doesn't - recognize that two denominators are identical and so it - creates a system with nearly duplicate eigenvalues and - double the state dimension. This should not be a problem in - the python-control usage because the common_den() method finds - repeated roots within a tolerance that we specify. - - Matlab: Matlab seems to force its statespace system output to - have order less than or equal to the order of denominators provided, - avoiding the problem of very large state dimension we describe in 3. - It does however, still have similar problems with pole/zero - cancellation such as we encounter in 2, where a statespace system - may have fewer states than the original order of transfer function. +import pytest + +from control import bode, rss, ss, tf +from control.tests.conftest import slycotonly + +numTests = 5 +maxStates = 10 +maxI = 1 +maxO = 1 + + +@pytest.fixture(scope="module") +def fixedseed(): + """Get consistent test results""" + np.random.seed(0) + + +@slycotonly +@pytest.mark.usefixtures("fixedseed") +class TestSlycot: + """Test Slycot system conversion + + TestSlycot compares transfer function and state space conversions for + various numbers of inputs,outputs and states. + 1. Usually passes for SISO systems of any state dim, occasonally, + there will be a dimension mismatch if the original randomly + generated ss system is not minimal because td04ad returns a + minimal system. + + 2. For small systems with many inputs, n<5 and with 2 or more + outputs the conversion to statespace (td04ad) intermittently + results in an equivalent realization of higher order than the + original tf order. We think this has to do with minimu + realization tolerances in the Fortran. The algorithm doesn't + recognize that two denominators are identical and so it + creates a system with nearly duplicate eigenvalues and + double the state dimension. This should not be a problem in + the python-control usage because the common_den() method finds + repeated roots within a tolerance that we specify. + + Matlab: Matlab seems to force its statespace system output to + have order less than or equal to the order of denominators provided, + avoiding the problem of very large state dimension we describe in 3. + It does however, still have similar problems with pole/zero + cancellation such as we encounter in 2, where a statespace system + may have fewer states than the original order of transfer function. """ - def setUp(self): - """Define some test parameters.""" - self.numTests = 5 - self.maxStates = 10 - self.maxI = 1 - self.maxO = 1 - - def testTF(self, verbose=False): - """ Directly tests the functions tb04ad and td04ad through direct - comparison of transfer function coefficients. - Similar to convert_test, but tests at a lower level. + + @pytest.fixture + def verbose(self): + """Set to True and switch off pytest stdout capture to print info""" + return False + + @pytest.mark.parametrize("testNum", np.arange(numTests) + 1) + @pytest.mark.parametrize("inputs", np.arange(maxI) + 1) + @pytest.mark.parametrize("outputs", np.arange(maxO) + 1) + @pytest.mark.parametrize("states", np.arange(maxStates) + 1) + def testTF(self, states, outputs, inputs, testNum, verbose): + """Test transfer function conversion. + + Directly tests the functions tb04ad and td04ad through direct + comparison of transfer function coefficients. + Similar to convert_test, but tests at a lower level. """ from slycot import tb04ad, td04ad - for states in range(1, self.maxStates): - for inputs in range(1, self.maxI+1): - for outputs in range(1, self.maxO+1): - for testNum in range(self.numTests): - ssOriginal = matlab.rss(states, outputs, inputs) - if (verbose): - print('====== Original SS ==========') - print(ssOriginal) - print('states=', states) - print('inputs=', inputs) - print('outputs=', outputs) - - tfOriginal_Actrb, tfOriginal_Bctrb, tfOriginal_Cctrb,\ - tfOrigingal_nctrb, tfOriginal_index,\ - tfOriginal_dcoeff, tfOriginal_ucoeff =\ - tb04ad(states, inputs, outputs, - ssOriginal.A, ssOriginal.B, - ssOriginal.C, ssOriginal.D, tol1=0.0) - - ssTransformed_nr, ssTransformed_A, ssTransformed_B,\ - ssTransformed_C, ssTransformed_D\ - = td04ad('R', inputs, outputs, tfOriginal_index, - tfOriginal_dcoeff, tfOriginal_ucoeff, - tol=0.0) - - tfTransformed_Actrb, tfTransformed_Bctrb,\ - tfTransformed_Cctrb, tfTransformed_nctrb,\ - tfTransformed_index, tfTransformed_dcoeff,\ - tfTransformed_ucoeff = tb04ad( - ssTransformed_nr, inputs, outputs, - ssTransformed_A, ssTransformed_B, - ssTransformed_C, ssTransformed_D, tol1=0.0) - # print('size(Trans_A)=',ssTransformed_A.shape) - if (verbose): - print('===== Transformed SS ==========') - print(matlab.ss(ssTransformed_A, ssTransformed_B, - ssTransformed_C, ssTransformed_D)) - # print('Trans_nr=',ssTransformed_nr - # print('tfOrig_index=',tfOriginal_index) - # print('tfOrig_ucoeff=',tfOriginal_ucoeff) - # print('tfOrig_dcoeff=',tfOriginal_dcoeff) - # print('tfTrans_index=',tfTransformed_index) - # print('tfTrans_ucoeff=',tfTransformed_ucoeff) - # print('tfTrans_dcoeff=',tfTransformed_dcoeff) - # Compare the TF directly, must match - # numerators - # TODO test failing! - # np.testing.assert_array_almost_equal( - # tfOriginal_ucoeff, tfTransformed_ucoeff, decimal=3) - # denominators - # np.testing.assert_array_almost_equal( - # tfOriginal_dcoeff, tfTransformed_dcoeff, decimal=3) - - def testFreqResp(self): - """Compare the bode reponses of the SS systems and TF systems to the original SS - They generally are different realizations but have same freq resp. - Currently this test may only be applied to SISO systems. + + ssOriginal = rss(states, outputs, inputs) + if (verbose): + print('====== Original SS ==========') + print(ssOriginal) + print('states=', states) + print('inputs=', inputs) + print('outputs=', outputs) + + tfOriginal_Actrb, tfOriginal_Bctrb, tfOriginal_Cctrb,\ + tfOrigingal_nctrb, tfOriginal_index,\ + tfOriginal_dcoeff, tfOriginal_ucoeff =\ + tb04ad(states, inputs, outputs, + ssOriginal.A, ssOriginal.B, + ssOriginal.C, ssOriginal.D, tol1=0.0) + + ssTransformed_nr, ssTransformed_A, ssTransformed_B,\ + ssTransformed_C, ssTransformed_D\ + = td04ad('R', inputs, outputs, tfOriginal_index, + tfOriginal_dcoeff, tfOriginal_ucoeff, + tol=0.0) + + tfTransformed_Actrb, tfTransformed_Bctrb,\ + tfTransformed_Cctrb, tfTransformed_nctrb,\ + tfTransformed_index, tfTransformed_dcoeff,\ + tfTransformed_ucoeff = tb04ad( + ssTransformed_nr, inputs, outputs, + ssTransformed_A, ssTransformed_B, + ssTransformed_C, ssTransformed_D, tol1=0.0) + # print('size(Trans_A)=',ssTransformed_A.shape) + if (verbose): + print('===== Transformed SS ==========') + print(ss(ssTransformed_A, ssTransformed_B, + ssTransformed_C, ssTransformed_D)) + # print('Trans_nr=',ssTransformed_nr + # print('tfOrig_index=',tfOriginal_index) + # print('tfOrig_ucoeff=',tfOriginal_ucoeff) + # print('tfOrig_dcoeff=',tfOriginal_dcoeff) + # print('tfTrans_index=',tfTransformed_index) + # print('tfTrans_ucoeff=',tfTransformed_ucoeff) + # print('tfTrans_dcoeff=',tfTransformed_dcoeff) + # Compare the TF directly, must match + # numerators + # TODO test failing! + # np.testing.assert_array_almost_equal( + # tfOriginal_ucoeff, tfTransformed_ucoeff, decimal=3) + # denominators + # np.testing.assert_array_almost_equal( + # tfOriginal_dcoeff, tfTransformed_dcoeff, decimal=3) + + @pytest.mark.parametrize("testNum", np.arange(numTests) + 1) + @pytest.mark.parametrize("inputs", np.arange(1) + 1) # SISO only + @pytest.mark.parametrize("outputs", np.arange(1) + 1) # SISO only + @pytest.mark.parametrize("states", np.arange(maxStates) + 1) + def testFreqResp(self, states, outputs, inputs, testNum, verbose): + """Compare bode responses. + + Compare the bode reponses of the SS systems and TF systems to the + original SS. They generally are different realizations but have same + freq resp. Currently this test may only be applied to SISO systems. """ from slycot import tb04ad, td04ad - for states in range(1, self.maxStates): - for testNum in range(self.numTests): - for inputs in range(1, 1): - for outputs in range(1, 1): - ssOriginal = matlab.rss(states, outputs, inputs) - - tfOriginal_Actrb, tfOriginal_Bctrb, tfOriginal_Cctrb,\ - tfOrigingal_nctrb, tfOriginal_index,\ - tfOriginal_dcoeff, tfOriginal_ucoeff = tb04ad( - states, inputs, outputs, ssOriginal.A, - ssOriginal.B, ssOriginal.C, ssOriginal.D, - tol1=0.0) - - ssTransformed_nr, ssTransformed_A, ssTransformed_B,\ - ssTransformed_C, ssTransformed_D\ - = td04ad('R', inputs, outputs, tfOriginal_index, - tfOriginal_dcoeff, tfOriginal_ucoeff, - tol=0.0) - - tfTransformed_Actrb, tfTransformed_Bctrb,\ - tfTransformed_Cctrb, tfTransformed_nctrb,\ - tfTransformed_index, tfTransformed_dcoeff,\ - tfTransformed_ucoeff = tb04ad( - ssTransformed_nr, inputs, outputs, - ssTransformed_A, ssTransformed_B, - ssTransformed_C, ssTransformed_D, - tol1=0.0) - - numTransformed = np.array(tfTransformed_ucoeff) - denTransformed = np.array(tfTransformed_dcoeff) - numOriginal = np.array(tfOriginal_ucoeff) - denOriginal = np.array(tfOriginal_dcoeff) - - ssTransformed = matlab.ss(ssTransformed_A, - ssTransformed_B, - ssTransformed_C, - ssTransformed_D) - for inputNum in range(inputs): - for outputNum in range(outputs): - [ssOriginalMag, ssOriginalPhase, freq] =\ - matlab.bode(ssOriginal, plot=False) - [tfOriginalMag, tfOriginalPhase, freq] =\ - matlab.bode(matlab.tf( - numOriginal[outputNum][inputNum], - denOriginal[outputNum]), plot=False) - [ssTransformedMag, ssTransformedPhase, freq] =\ - matlab.bode(ssTransformed, - freq, plot=False) - [tfTransformedMag, tfTransformedPhase, freq] =\ - matlab.bode(matlab.tf( - numTransformed[outputNum][inputNum], - denTransformed[outputNum]), - freq, plot=False) - # print('numOrig=', - # numOriginal[outputNum][inputNum]) - # print('denOrig=', - # denOriginal[outputNum]) - # print('numTrans=', - # numTransformed[outputNum][inputNum]) - # print('denTrans=', - # denTransformed[outputNum]) - np.testing.assert_array_almost_equal( - ssOriginalMag, tfOriginalMag, decimal=3) - np.testing.assert_array_almost_equal( - ssOriginalPhase, tfOriginalPhase, - decimal=3) - np.testing.assert_array_almost_equal( - ssOriginalMag, ssTransformedMag, decimal=3) - np.testing.assert_array_almost_equal( - ssOriginalPhase, ssTransformedPhase, - decimal=3) - np.testing.assert_array_almost_equal( - tfOriginalMag, tfTransformedMag, decimal=3) - np.testing.assert_array_almost_equal( - tfOriginalPhase, tfTransformedPhase, - decimal=2) - - -if __name__ == '__main__': - unittest.main() + + ssOriginal = rss(states, outputs, inputs) + + tfOriginal_Actrb, tfOriginal_Bctrb, tfOriginal_Cctrb,\ + tfOrigingal_nctrb, tfOriginal_index,\ + tfOriginal_dcoeff, tfOriginal_ucoeff = tb04ad( + states, inputs, outputs, ssOriginal.A, + ssOriginal.B, ssOriginal.C, ssOriginal.D, + tol1=0.0) + + ssTransformed_nr, ssTransformed_A, ssTransformed_B,\ + ssTransformed_C, ssTransformed_D\ + = td04ad('R', inputs, outputs, tfOriginal_index, + tfOriginal_dcoeff, tfOriginal_ucoeff, + tol=0.0) + + tfTransformed_Actrb, tfTransformed_Bctrb,\ + tfTransformed_Cctrb, tfTransformed_nctrb,\ + tfTransformed_index, tfTransformed_dcoeff,\ + tfTransformed_ucoeff = tb04ad( + ssTransformed_nr, inputs, outputs, + ssTransformed_A, ssTransformed_B, + ssTransformed_C, ssTransformed_D, + tol1=0.0) + + numTransformed = np.array(tfTransformed_ucoeff) + denTransformed = np.array(tfTransformed_dcoeff) + numOriginal = np.array(tfOriginal_ucoeff) + denOriginal = np.array(tfOriginal_dcoeff) + + ssTransformed = ss(ssTransformed_A, + ssTransformed_B, + ssTransformed_C, + ssTransformed_D) + for inputNum in range(inputs): + for outputNum in range(outputs): + [ssOriginalMag, ssOriginalPhase, freq] =\ + bode(ssOriginal, plot=False) + [tfOriginalMag, tfOriginalPhase, freq] =\ + bode(tf(numOriginal[outputNum][inputNum], + denOriginal[outputNum]), + plot=False) + [ssTransformedMag, ssTransformedPhase, freq] =\ + bode(ssTransformed, + freq, + plot=False) + [tfTransformedMag, tfTransformedPhase, freq] =\ + bode(tf(numTransformed[outputNum][inputNum], + denTransformed[outputNum]), + freq, + plot=False) + # print('numOrig=', + # numOriginal[outputNum][inputNum]) + # print('denOrig=', + # denOriginal[outputNum]) + # print('numTrans=', + # numTransformed[outputNum][inputNum]) + # print('denTrans=', + # denTransformed[outputNum]) + np.testing.assert_array_almost_equal( + ssOriginalMag, tfOriginalMag, decimal=3) + np.testing.assert_array_almost_equal( + ssOriginalPhase, tfOriginalPhase, + decimal=3) + np.testing.assert_array_almost_equal( + ssOriginalMag, ssTransformedMag, decimal=3) + np.testing.assert_array_almost_equal( + ssOriginalPhase, ssTransformedPhase, + decimal=3) + np.testing.assert_array_almost_equal( + tfOriginalMag, tfTransformedMag, decimal=3) + np.testing.assert_array_almost_equal( + tfOriginalPhase, tfTransformedPhase, + decimal=2) From 952c511c76598771c004bdd95adf7d4f8c4b3d38 Mon Sep 17 00:00:00 2001 From: bnavigator Date: Wed, 30 Dec 2020 00:13:04 +0100 Subject: [PATCH 25/30] pytestify statefbk tests --- control/tests/statefbk_array_test.py | 413 -------------------------- control/tests/statefbk_matrix_test.py | 348 ---------------------- control/tests/statefbk_test.py | 381 ++++++++++++++++++++++++ 3 files changed, 381 insertions(+), 761 deletions(-) delete mode 100644 control/tests/statefbk_array_test.py delete mode 100644 control/tests/statefbk_matrix_test.py create mode 100644 control/tests/statefbk_test.py diff --git a/control/tests/statefbk_array_test.py b/control/tests/statefbk_array_test.py deleted file mode 100644 index 10f450186..000000000 --- a/control/tests/statefbk_array_test.py +++ /dev/null @@ -1,413 +0,0 @@ -#!/usr/bin/env python -# -# statefbk_test.py - test state feedback functions -# RMM, 30 Mar 2011 (based on TestStatefbk from v0.4a) - -from __future__ import print_function -import unittest -import sys as pysys -import numpy as np -import warnings -from control.statefbk import ctrb, obsv, place, place_varga, lqr, gram, acker -from control.matlab import * -from control.exception import slycot_check, ControlDimension -from control.mateqn import care, dare -from control.config import use_numpy_matrix, reset_defaults - -class TestStatefbk(unittest.TestCase): - """Test state feedback functions""" - - def setUp(self): - # Use array instead of matrix (and save old value to restore at end) - use_numpy_matrix(False) - - # Maximum number of states to test + 1 - self.maxStates = 5 - # Maximum number of inputs and outputs to test + 1 - self.maxTries = 4 - # Set to True to print systems to the output. - self.debug = False - # get consistent test results - np.random.seed(0) - - def testCtrbSISO(self): - A = np.array([[1., 2.], [3., 4.]]) - B = np.array([[5.], [7.]]) - Wctrue = np.array([[5., 19.], [7., 43.]]) - - Wc = ctrb(A, B) - np.testing.assert_array_almost_equal(Wc, Wctrue) - self.assertTrue(isinstance(Wc, np.ndarray)) - self.assertFalse(isinstance(Wc, np.matrix)) - - # This test only works in Python 3 due to a conflict with the same - # warning type in other test modules (frd_test.py). See - # https://bugs.python.org/issue4180 for more details - @unittest.skipIf(pysys.version_info < (3, 0), "test requires Python 3+") - def test_ctrb_siso_deprecated(self): - A = np.array([[1., 2.], [3., 4.]]) - B = np.array([[5.], [7.]]) - - # Check that default using np.matrix generates a warning - # TODO: remove this check with matrix type is deprecated - warnings.resetwarnings() - with warnings.catch_warnings(record=True) as w: - use_numpy_matrix(True) - self.assertTrue(issubclass(w[-1].category, UserWarning)) - - Wc = ctrb(A, B) - self.assertTrue(isinstance(Wc, np.matrix)) - self.assertTrue(issubclass(w[-1].category, - PendingDeprecationWarning)) - use_numpy_matrix(False) - - def testCtrbMIMO(self): - A = np.array([[1., 2.], [3., 4.]]) - B = np.array([[5., 6.], [7., 8.]]) - Wctrue = np.array([[5., 6., 19., 22.], [7., 8., 43., 50.]]) - Wc = ctrb(A, B) - np.testing.assert_array_almost_equal(Wc, Wctrue) - - # Make sure default type values are correct - self.assertTrue(isinstance(Wc, np.ndarray)) - - def testObsvSISO(self): - A = np.array([[1., 2.], [3., 4.]]) - C = np.array([[5., 7.]]) - Wotrue = np.array([[5., 7.], [26., 38.]]) - Wo = obsv(A, C) - np.testing.assert_array_almost_equal(Wo, Wotrue) - - # Make sure default type values are correct - self.assertTrue(isinstance(Wo, np.ndarray)) - - # This test only works in Python 3 due to a conflict with the same - # warning type in other test modules (frd_test.py). See - # https://bugs.python.org/issue4180 for more details - @unittest.skipIf(pysys.version_info < (3, 0), "test requires Python 3+") - def test_obsv_siso_deprecated(self): - A = np.array([[1., 2.], [3., 4.]]) - C = np.array([[5., 7.]]) - - # Check that default type generates a warning - # TODO: remove this check with matrix type is deprecated - with warnings.catch_warnings(record=True) as w: - use_numpy_matrix(True, warn=False) # warnings off - self.assertEqual(len(w), 0) - - Wo = obsv(A, C) - self.assertTrue(isinstance(Wo, np.matrix)) - use_numpy_matrix(False) - - def testObsvMIMO(self): - A = np.array([[1., 2.], [3., 4.]]) - C = np.array([[5., 6.], [7., 8.]]) - Wotrue = np.array([[5., 6.], [7., 8.], [23., 34.], [31., 46.]]) - Wo = obsv(A, C) - np.testing.assert_array_almost_equal(Wo, Wotrue) - - def testCtrbObsvDuality(self): - A = np.array([[1.2, -2.3], [3.4, -4.5]]) - B = np.array([[5.8, 6.9], [8., 9.1]]) - Wc = ctrb(A, B) - A = np.transpose(A) - C = np.transpose(B) - Wo = np.transpose(obsv(A, C)); - np.testing.assert_array_almost_equal(Wc,Wo) - - @unittest.skipIf(not slycot_check(), "slycot not installed") - def testGramWc(self): - A = np.array([[1., -2.], [3., -4.]]) - B = np.array([[5., 6.], [7., 8.]]) - C = np.array([[4., 5.], [6., 7.]]) - D = np.array([[13., 14.], [15., 16.]]) - sys = ss(A, B, C, D) - Wctrue = np.array([[18.5, 24.5], [24.5, 32.5]]) - Wc = gram(sys, 'c') - np.testing.assert_array_almost_equal(Wc, Wctrue) - - # This test only works in Python 3 due to a conflict with the same - # warning type in other test modules (frd_test.py). See - # https://bugs.python.org/issue4180 for more details - @unittest.skipIf(pysys.version_info < (3, 0) or not slycot_check(), - "test requires Python 3+ and slycot") - def test_gram_wc_deprecated(self): - A = np.array([[1., -2.], [3., -4.]]) - B = np.array([[5., 6.], [7., 8.]]) - C = np.array([[4., 5.], [6., 7.]]) - D = np.array([[13., 14.], [15., 16.]]) - sys = ss(A, B, C, D) - - # Check that default type generates a warning - # TODO: remove this check with matrix type is deprecated - with warnings.catch_warnings(record=True) as w: - use_numpy_matrix(True) - self.assertTrue(issubclass(w[-1].category, UserWarning)) - - Wc = gram(sys, 'c') - self.assertTrue(isinstance(Wc, np.ndarray)) - use_numpy_matrix(False) - - @unittest.skipIf(not slycot_check(), "slycot not installed") - def testGramRc(self): - A = np.array([[1., -2.], [3., -4.]]) - B = np.array([[5., 6.], [7., 8.]]) - C = np.array([[4., 5.], [6., 7.]]) - D = np.array([[13., 14.], [15., 16.]]) - sys = ss(A, B, C, D) - Rctrue = np.array([[4.30116263, 5.6961343], [0., 0.23249528]]) - Rc = gram(sys, 'cf') - np.testing.assert_array_almost_equal(Rc, Rctrue) - - @unittest.skipIf(not slycot_check(), "slycot not installed") - def testGramWo(self): - A = np.array([[1., -2.], [3., -4.]]) - B = np.array([[5., 6.], [7., 8.]]) - C = np.array([[4., 5.], [6., 7.]]) - D = np.array([[13., 14.], [15., 16.]]) - sys = ss(A, B, C, D) - Wotrue = np.array([[257.5, -94.5], [-94.5, 56.5]]) - Wo = gram(sys, 'o') - np.testing.assert_array_almost_equal(Wo, Wotrue) - - @unittest.skipIf(not slycot_check(), "slycot not installed") - def testGramWo2(self): - A = np.array([[1., -2.], [3., -4.]]) - B = np.array([[5.], [7.]]) - C = np.array([[6., 8.]]) - D = np.array([[9.]]) - sys = ss(A,B,C,D) - Wotrue = np.array([[198., -72.], [-72., 44.]]) - Wo = gram(sys, 'o') - np.testing.assert_array_almost_equal(Wo, Wotrue) - - @unittest.skipIf(not slycot_check(), "slycot not installed") - def testGramRo(self): - A = np.array([[1., -2.], [3., -4.]]) - B = np.array([[5., 6.], [7., 8.]]) - C = np.array([[4., 5.], [6., 7.]]) - D = np.array([[13., 14.], [15., 16.]]) - sys = ss(A, B, C, D) - Rotrue = np.array([[16.04680654, -5.8890222], [0., 4.67112593]]) - Ro = gram(sys, 'of') - np.testing.assert_array_almost_equal(Ro, Rotrue) - - def testGramsys(self): - num =[1.] - den = [1., 1., 1.] - sys = tf(num,den) - self.assertRaises(ValueError, gram, sys, 'o') - self.assertRaises(ValueError, gram, sys, 'c') - - def testAcker(self): - for states in range(1, self.maxStates): - for i in range(self.maxTries): - # start with a random SS system and transform to TF then - # back to SS, check that the matrices are the same. - sys = rss(states, 1, 1) - if (self.debug): - print(sys) - - # Make sure the system is not degenerate - Cmat = ctrb(sys.A, sys.B) - if np.linalg.matrix_rank(Cmat) != states: - if (self.debug): - print(" skipping (not reachable or ill conditioned)") - continue - - # Place the poles at random locations - des = rss(states, 1, 1); - poles = pole(des) - - # Now place the poles using acker - K = acker(sys.A, sys.B, poles) - new = ss(sys.A - sys.B * K, sys.B, sys.C, sys.D) - placed = pole(new) - - # Debugging code - # diff = np.sort(poles) - np.sort(placed) - # if not all(diff < 0.001): - # print("Found a problem:") - # print(sys) - # print("desired = ", poles) - - np.testing.assert_array_almost_equal(np.sort(poles), - np.sort(placed), decimal=4) - - def testPlace(self): - # Matrices shamelessly stolen from scipy example code. - A = np.array([[1.380, -0.2077, 6.715, -5.676], - [-0.5814, -4.290, 0, 0.6750], - [1.067, 4.273, -6.654, 5.893], - [0.0480, 4.273, 1.343, -2.104]]) - - B = np.array([[0, 5.679], - [1.136, 1.136], - [0, 0,], - [-3.146, 0]]) - P = np.array([-0.5+1j, -0.5-1j, -5.0566, -8.6659]) - K = place(A, B, P) - P_placed = np.linalg.eigvals(A - B.dot(K)) - # No guarantee of the ordering, so sort them - P.sort() - P_placed.sort() - np.testing.assert_array_almost_equal(P, P_placed) - - # Test that the dimension checks work. - np.testing.assert_raises(ControlDimension, place, A[1:, :], B, P) - np.testing.assert_raises(ControlDimension, place, A, B[1:, :], P) - - # Check that we get an error if we ask for too many poles in the same - # location. Here, rank(B) = 2, so lets place three at the same spot. - P_repeated = np.array([-0.5, -0.5, -0.5, -8.6659]) - np.testing.assert_raises(ValueError, place, A, B, P_repeated) - - @unittest.skipIf(not slycot_check(), "slycot not installed") - def testPlace_varga_continuous(self): - """ - Check that we can place eigenvalues for dtime=False - """ - A = np.array([[1., -2.], [3., -4.]]) - B = np.array([[5.], [7.]]) - - P = np.array([-2., -2.]) - K = place_varga(A, B, P) - P_placed = np.linalg.eigvals(A - B.dot(K)) - # No guarantee of the ordering, so sort them - P.sort() - P_placed.sort() - np.testing.assert_array_almost_equal(P, P_placed) - - # Test that the dimension checks work. - np.testing.assert_raises(ControlDimension, place, A[1:, :], B, P) - np.testing.assert_raises(ControlDimension, place, A, B[1:, :], P) - - # Regression test against bug #177 - # https://github.com/python-control/python-control/issues/177 - A = np.array([[0, 1], [100, 0]]) - B = np.array([[0], [1]]) - P = np.array([-20 + 10*1j, -20 - 10*1j]) - K = place_varga(A, B, P) - P_placed = np.linalg.eigvals(A - B.dot(K)) - - # No guarantee of the ordering, so sort them - P.sort() - P_placed.sort() - np.testing.assert_array_almost_equal(P, P_placed) - - @unittest.skipIf(not slycot_check(), "slycot not installed") - def testPlace_varga_continuous_partial_eigs(self): - """ - Check that we are able to use the alpha parameter to only place - a subset of the eigenvalues, for the continous time case. - """ - # A matrix has eigenvalues at s=-1, and s=-2. Choose alpha = -1.5 - # and check that eigenvalue at s=-2 stays put. - A = np.array([[1., -2.], [3., -4.]]) - B = np.array([[5.], [7.]]) - - P = np.array([-3.]) - P_expected = np.array([-2.0, -3.0]) - alpha = -1.5 - K = place_varga(A, B, P, alpha=alpha) - - P_placed = np.linalg.eigvals(A - B.dot(K)) - # No guarantee of the ordering, so sort them - P_expected.sort() - P_placed.sort() - np.testing.assert_array_almost_equal(P_expected, P_placed) - - @unittest.skipIf(not slycot_check(), "slycot not installed") - def testPlace_varga_discrete(self): - """ - Check that we can place poles using dtime=True (discrete time) - """ - A = np.array([[1., 0], [0, 0.5]]) - B = np.array([[5.], [7.]]) - - P = np.array([0.5, 0.5]) - K = place_varga(A, B, P, dtime=True) - P_placed = np.linalg.eigvals(A - B.dot(K)) - # No guarantee of the ordering, so sort them - P.sort() - P_placed.sort() - np.testing.assert_array_almost_equal(P, P_placed) - - @unittest.skipIf(not slycot_check(), "slycot not installed") - def testPlace_varga_discrete_partial_eigs(self): - """" - Check that we can only assign a single eigenvalue in the discrete - time case. - """ - # A matrix has eigenvalues at 1.0 and 0.5. Set alpha = 0.51, and - # check that the eigenvalue at 0.5 is not moved. - A = np.array([[1., 0], [0, 0.5]]) - B = np.array([[5.], [7.]]) - P = np.array([0.2, 0.6]) - P_expected = np.array([0.5, 0.6]) - alpha = 0.51 - K = place_varga(A, B, P, dtime=True, alpha=alpha) - P_placed = np.linalg.eigvals(A - B.dot(K)) - P_expected.sort() - P_placed.sort() - np.testing.assert_array_almost_equal(P_expected, P_placed) - - - def check_LQR(self, K, S, poles, Q, R): - S_expected = np.array(np.sqrt(Q * R)) - K_expected = S_expected / R - poles_expected = np.array([-K_expected]) - np.testing.assert_array_almost_equal(S, S_expected) - np.testing.assert_array_almost_equal(K, K_expected) - np.testing.assert_array_almost_equal(poles, poles_expected) - - - @unittest.skipIf(not slycot_check(), "slycot not installed") - def test_LQR_integrator(self): - A, B, Q, R = 0., 1., 10., 2. - K, S, poles = lqr(A, B, Q, R) - self.check_LQR(K, S, poles, Q, R) - - @unittest.skipIf(not slycot_check(), "slycot not installed") - def test_LQR_3args(self): - sys = ss(0., 1., 1., 0.) - Q, R = 10., 2. - K, S, poles = lqr(sys, Q, R) - self.check_LQR(K, S, poles, Q, R) - - @unittest.skipIf(not slycot_check(), "slycot not installed") - def test_care(self): - #unit test for stabilizing and anti-stabilizing feedbacks - #continuous-time - - A = np.diag([1,-1]) - B = np.identity(2) - Q = np.identity(2) - R = np.identity(2) - S = 0 * B - E = np.identity(2) - X, L , G = care(A, B, Q, R, S, E, stabilizing=True) - assert np.all(np.real(L) < 0) - X, L , G = care(A, B, Q, R, S, E, stabilizing=False) - assert np.all(np.real(L) > 0) - - @unittest.skipIf(not slycot_check(), "slycot not installed") - def test_dare(self): - #discrete-time - A = np.diag([0.5,2]) - B = np.identity(2) - Q = np.identity(2) - R = np.identity(2) - S = 0 * B - E = np.identity(2) - X, L , G = dare(A, B, Q, R, S, E, stabilizing=True) - assert np.all(np.abs(L) < 1) - X, L , G = dare(A, B, Q, R, S, E, stabilizing=False) - assert np.all(np.abs(L) > 1) - - def tearDown(self): - reset_defaults() - - -if __name__ == '__main__': - unittest.main() diff --git a/control/tests/statefbk_matrix_test.py b/control/tests/statefbk_matrix_test.py deleted file mode 100644 index 3be70d643..000000000 --- a/control/tests/statefbk_matrix_test.py +++ /dev/null @@ -1,348 +0,0 @@ -#!/usr/bin/env python -# -# statefbk_test.py - test state feedback functions -# RMM, 30 Mar 2011 (based on TestStatefbk from v0.4a) - -from __future__ import print_function -import unittest -import numpy as np -from control.statefbk import ctrb, obsv, place, place_varga, lqr, lqe, gram, acker -from control.matlab import * -from control.exception import slycot_check, ControlDimension -from control.mateqn import care, dare - -class TestStatefbk(unittest.TestCase): - """Test state feedback functions""" - - def setUp(self): - # Maximum number of states to test + 1 - self.maxStates = 5 - # Maximum number of inputs and outputs to test + 1 - self.maxTries = 4 - # Set to True to print systems to the output. - self.debug = False - # get consistent test results - np.random.seed(0) - - def testCtrbSISO(self): - A = np.matrix("1. 2.; 3. 4.") - B = np.matrix("5.; 7.") - Wctrue = np.matrix("5. 19.; 7. 43.") - Wc = ctrb(A,B) - np.testing.assert_array_almost_equal(Wc, Wctrue) - - def testCtrbMIMO(self): - A = np.matrix("1. 2.; 3. 4.") - B = np.matrix("5. 6.; 7. 8.") - Wctrue = np.matrix("5. 6. 19. 22.; 7. 8. 43. 50.") - Wc = ctrb(A,B) - np.testing.assert_array_almost_equal(Wc, Wctrue) - - def testObsvSISO(self): - A = np.matrix("1. 2.; 3. 4.") - C = np.matrix("5. 7.") - Wotrue = np.matrix("5. 7.; 26. 38.") - Wo = obsv(A,C) - np.testing.assert_array_almost_equal(Wo, Wotrue) - - def testObsvMIMO(self): - A = np.matrix("1. 2.; 3. 4.") - C = np.matrix("5. 6.; 7. 8.") - Wotrue = np.matrix("5. 6.; 7. 8.; 23. 34.; 31. 46.") - Wo = obsv(A,C) - np.testing.assert_array_almost_equal(Wo, Wotrue) - - def testCtrbObsvDuality(self): - A = np.matrix("1.2 -2.3; 3.4 -4.5") - B = np.matrix("5.8 6.9; 8. 9.1") - Wc = ctrb(A,B); - A = np.transpose(A) - C = np.transpose(B) - Wo = np.transpose(obsv(A,C)); - np.testing.assert_array_almost_equal(Wc,Wo) - - @unittest.skipIf(not slycot_check(), "slycot not installed") - def testGramWc(self): - A = np.matrix("1. -2.; 3. -4.") - B = np.matrix("5. 6.; 7. 8.") - C = np.matrix("4. 5.; 6. 7.") - D = np.matrix("13. 14.; 15. 16.") - sys = ss(A, B, C, D) - Wctrue = np.matrix("18.5 24.5; 24.5 32.5") - Wc = gram(sys,'c') - np.testing.assert_array_almost_equal(Wc, Wctrue) - - @unittest.skipIf(not slycot_check(), "slycot not installed") - def testGramRc(self): - A = np.matrix("1. -2.; 3. -4.") - B = np.matrix("5. 6.; 7. 8.") - C = np.matrix("4. 5.; 6. 7.") - D = np.matrix("13. 14.; 15. 16.") - sys = ss(A, B, C, D) - Rctrue = np.matrix("4.30116263 5.6961343; 0. 0.23249528") - Rc = gram(sys,'cf') - np.testing.assert_array_almost_equal(Rc, Rctrue) - - @unittest.skipIf(not slycot_check(), "slycot not installed") - def testGramWo(self): - A = np.matrix("1. -2.; 3. -4.") - B = np.matrix("5. 6.; 7. 8.") - C = np.matrix("4. 5.; 6. 7.") - D = np.matrix("13. 14.; 15. 16.") - sys = ss(A, B, C, D) - Wotrue = np.matrix("257.5 -94.5; -94.5 56.5") - Wo = gram(sys,'o') - np.testing.assert_array_almost_equal(Wo, Wotrue) - - @unittest.skipIf(not slycot_check(), "slycot not installed") - def testGramWo2(self): - A = np.matrix("1. -2.; 3. -4.") - B = np.matrix("5.; 7.") - C = np.matrix("6. 8.") - D = np.matrix("9.") - sys = ss(A,B,C,D) - Wotrue = np.matrix("198. -72.; -72. 44.") - Wo = gram(sys,'o') - np.testing.assert_array_almost_equal(Wo, Wotrue) - - @unittest.skipIf(not slycot_check(), "slycot not installed") - def testGramRo(self): - A = np.matrix("1. -2.; 3. -4.") - B = np.matrix("5. 6.; 7. 8.") - C = np.matrix("4. 5.; 6. 7.") - D = np.matrix("13. 14.; 15. 16.") - sys = ss(A, B, C, D) - Rotrue = np.matrix("16.04680654 -5.8890222; 0. 4.67112593") - Ro = gram(sys,'of') - np.testing.assert_array_almost_equal(Ro, Rotrue) - - def testGramsys(self): - num =[1.] - den = [1., 1., 1.] - sys = tf(num,den) - self.assertRaises(ValueError, gram, sys, 'o') - self.assertRaises(ValueError, gram, sys, 'c') - - def testAcker(self): - for states in range(1, self.maxStates): - for i in range(self.maxTries): - # start with a random SS system and transform to TF then - # back to SS, check that the matrices are the same. - sys = rss(states, 1, 1) - if (self.debug): - print(sys) - - # Make sure the system is not degenerate - Cmat = ctrb(sys.A, sys.B) - if np.linalg.matrix_rank(Cmat) != states: - if (self.debug): - print(" skipping (not reachable or ill conditioned)") - continue - - # Place the poles at random locations - des = rss(states, 1, 1); - poles = pole(des) - - # Now place the poles using acker - K = acker(sys.A, sys.B, poles) - new = ss(sys.A - sys.B * K, sys.B, sys.C, sys.D) - placed = pole(new) - - # Debugging code - # diff = np.sort(poles) - np.sort(placed) - # if not all(diff < 0.001): - # print("Found a problem:") - # print(sys) - # print("desired = ", poles) - - np.testing.assert_array_almost_equal(np.sort(poles), - np.sort(placed), decimal=4) - - def testPlace(self): - # Matrices shamelessly stolen from scipy example code. - A = np.array([[1.380, -0.2077, 6.715, -5.676], - [-0.5814, -4.290, 0, 0.6750], - [1.067, 4.273, -6.654, 5.893], - [0.0480, 4.273, 1.343, -2.104]]) - - B = np.array([[0, 5.679], - [1.136, 1.136], - [0, 0,], - [-3.146, 0]]) - P = np.array([-0.5+1j, -0.5-1j, -5.0566, -8.6659]) - K = place(A, B, P) - P_placed = np.linalg.eigvals(A - B.dot(K)) - # No guarantee of the ordering, so sort them - P.sort() - P_placed.sort() - np.testing.assert_array_almost_equal(P, P_placed) - - # Test that the dimension checks work. - np.testing.assert_raises(ControlDimension, place, A[1:, :], B, P) - np.testing.assert_raises(ControlDimension, place, A, B[1:, :], P) - - # Check that we get an error if we ask for too many poles in the same - # location. Here, rank(B) = 2, so lets place three at the same spot. - P_repeated = np.array([-0.5, -0.5, -0.5, -8.6659]) - np.testing.assert_raises(ValueError, place, A, B, P_repeated) - - @unittest.skipIf(not slycot_check(), "slycot not installed") - def testPlace_varga_continuous(self): - """ - Check that we can place eigenvalues for dtime=False - """ - A = np.array([[1., -2.], [3., -4.]]) - B = np.array([[5.], [7.]]) - - P = np.array([-2., -2.]) - K = place_varga(A, B, P) - P_placed = np.linalg.eigvals(A - B.dot(K)) - # No guarantee of the ordering, so sort them - P.sort() - P_placed.sort() - np.testing.assert_array_almost_equal(P, P_placed) - - # Test that the dimension checks work. - np.testing.assert_raises(ControlDimension, place, A[1:, :], B, P) - np.testing.assert_raises(ControlDimension, place, A, B[1:, :], P) - - # Regression test against bug #177 - # https://github.com/python-control/python-control/issues/177 - A = np.array([[0, 1], [100, 0]]) - B = np.array([[0], [1]]) - P = np.array([-20 + 10*1j, -20 - 10*1j]) - K = place_varga(A, B, P) - P_placed = np.linalg.eigvals(A - B.dot(K)) - - # No guarantee of the ordering, so sort them - P.sort() - P_placed.sort() - np.testing.assert_array_almost_equal(P, P_placed) - - @unittest.skipIf(not slycot_check(), "slycot not installed") - def testPlace_varga_continuous_partial_eigs(self): - """ - Check that we are able to use the alpha parameter to only place - a subset of the eigenvalues, for the continous time case. - """ - # A matrix has eigenvalues at s=-1, and s=-2. Choose alpha = -1.5 - # and check that eigenvalue at s=-2 stays put. - A = np.array([[1., -2.], [3., -4.]]) - B = np.array([[5.], [7.]]) - - P = np.array([-3.]) - P_expected = np.array([-2.0, -3.0]) - alpha = -1.5 - K = place_varga(A, B, P, alpha=alpha) - - P_placed = np.linalg.eigvals(A - B.dot(K)) - # No guarantee of the ordering, so sort them - P_expected.sort() - P_placed.sort() - np.testing.assert_array_almost_equal(P_expected, P_placed) - - @unittest.skipIf(not slycot_check(), "slycot not installed") - def testPlace_varga_discrete(self): - """ - Check that we can place poles using dtime=True (discrete time) - """ - A = np.array([[1., 0], [0, 0.5]]) - B = np.array([[5.], [7.]]) - - P = np.array([0.5, 0.5]) - K = place_varga(A, B, P, dtime=True) - P_placed = np.linalg.eigvals(A - B.dot(K)) - # No guarantee of the ordering, so sort them - P.sort() - P_placed.sort() - np.testing.assert_array_almost_equal(P, P_placed) - - @unittest.skipIf(not slycot_check(), "slycot not installed") - def testPlace_varga_discrete_partial_eigs(self): - """" - Check that we can only assign a single eigenvalue in the discrete - time case. - """ - # A matrix has eigenvalues at 1.0 and 0.5. Set alpha = 0.51, and - # check that the eigenvalue at 0.5 is not moved. - A = np.array([[1., 0], [0, 0.5]]) - B = np.array([[5.], [7.]]) - P = np.array([0.2, 0.6]) - P_expected = np.array([0.5, 0.6]) - alpha = 0.51 - K = place_varga(A, B, P, dtime=True, alpha=alpha) - P_placed = np.linalg.eigvals(A - B.dot(K)) - P_expected.sort() - P_placed.sort() - np.testing.assert_array_almost_equal(P_expected, P_placed) - - - def check_LQR(self, K, S, poles, Q, R): - S_expected = np.array(np.sqrt(Q * R)) - K_expected = S_expected / R - poles_expected = np.array([-K_expected]) - np.testing.assert_array_almost_equal(S, S_expected) - np.testing.assert_array_almost_equal(K, K_expected) - np.testing.assert_array_almost_equal(poles, poles_expected) - - - @unittest.skipIf(not slycot_check(), "slycot not installed") - def test_LQR_integrator(self): - A, B, Q, R = 0., 1., 10., 2. - K, S, poles = lqr(A, B, Q, R) - self.check_LQR(K, S, poles, Q, R) - - @unittest.skipIf(not slycot_check(), "slycot not installed") - def test_LQR_3args(self): - sys = ss(0., 1., 1., 0.) - Q, R = 10., 2. - K, S, poles = lqr(sys, Q, R) - self.check_LQR(K, S, poles, Q, R) - - def check_LQE(self, L, P, poles, G, QN, RN): - P_expected = np.array(np.sqrt(G*QN*G * RN)) - L_expected = P_expected / RN - poles_expected = np.array([-L_expected]) - np.testing.assert_array_almost_equal(P, P_expected) - np.testing.assert_array_almost_equal(L, L_expected) - np.testing.assert_array_almost_equal(poles, poles_expected) - - @unittest.skipIf(not slycot_check(), "slycot not installed") - def test_LQE(self): - A, G, C, QN, RN = 0., .1, 1., 10., 2. - L, P, poles = lqe(A, G, C, QN, RN) - self.check_LQE(L, P, poles, G, QN, RN) - - @unittest.skipIf(not slycot_check(), "slycot not installed") - def test_care(self): - #unit test for stabilizing and anti-stabilizing feedbacks - #continuous-time - - A = np.diag([1,-1]) - B = np.identity(2) - Q = np.identity(2) - R = np.identity(2) - S = 0 * B - E = np.identity(2) - X, L , G = care(A, B, Q, R, S, E, stabilizing=True) - assert np.all(np.real(L) < 0) - X, L , G = care(A, B, Q, R, S, E, stabilizing=False) - assert np.all(np.real(L) > 0) - - @unittest.skipIf(not slycot_check(), "slycot not installed") - def test_dare(self): - #discrete-time - A = np.diag([0.5,2]) - B = np.identity(2) - Q = np.identity(2) - R = np.identity(2) - S = 0 * B - E = np.identity(2) - X, L , G = dare(A, B, Q, R, S, E, stabilizing=True) - assert np.all(np.abs(L) < 1) - X, L , G = dare(A, B, Q, R, S, E, stabilizing=False) - assert np.all(np.abs(L) > 1) - - -if __name__ == '__main__': - unittest.main() diff --git a/control/tests/statefbk_test.py b/control/tests/statefbk_test.py new file mode 100644 index 000000000..1dca98659 --- /dev/null +++ b/control/tests/statefbk_test.py @@ -0,0 +1,381 @@ +"""statefbk_test.py - test state feedback functions + +RMM, 30 Mar 2011 (based on TestStatefbk from v0.4a) +""" + +import numpy as np +import pytest + +from control import lqe, pole, rss, ss, tf +from control.exception import ControlDimension +from control.mateqn import care, dare +from control.statefbk import ctrb, obsv, place, place_varga, lqr, gram, acker +from control.tests.conftest import (slycotonly, check_deprecated_matrix, + ismatarrayout, asmatarrayout) + + +@pytest.fixture +def fixedseed(): + """Get consistent test results""" + np.random.seed(0) + + +class TestStatefbk: + """Test state feedback functions""" + + # Maximum number of states to test + 1 + maxStates = 5 + # Maximum number of inputs and outputs to test + 1 + maxTries = 4 + # Set to True to print systems to the output. + debug = False + + def testCtrbSISO(self, matarrayin, matarrayout): + A = matarrayin([[1., 2.], [3., 4.]]) + B = matarrayin([[5.], [7.]]) + Wctrue = np.array([[5., 19.], [7., 43.]]) + + with check_deprecated_matrix(): + Wc = ctrb(A, B) + assert ismatarrayout(Wc) + + np.testing.assert_array_almost_equal(Wc, Wctrue) + + def testCtrbMIMO(self, matarrayin): + A = matarrayin([[1., 2.], [3., 4.]]) + B = matarrayin([[5., 6.], [7., 8.]]) + Wctrue = np.array([[5., 6., 19., 22.], [7., 8., 43., 50.]]) + Wc = ctrb(A, B) + np.testing.assert_array_almost_equal(Wc, Wctrue) + + # Make sure default type values are correct + assert ismatarrayout(Wc) + + def testObsvSISO(self, matarrayin): + A = matarrayin([[1., 2.], [3., 4.]]) + C = matarrayin([[5., 7.]]) + Wotrue = np.array([[5., 7.], [26., 38.]]) + Wo = obsv(A, C) + np.testing.assert_array_almost_equal(Wo, Wotrue) + + # Make sure default type values are correct + assert ismatarrayout(Wo) + + + def testObsvMIMO(self, matarrayin): + A = matarrayin([[1., 2.], [3., 4.]]) + C = matarrayin([[5., 6.], [7., 8.]]) + Wotrue = np.array([[5., 6.], [7., 8.], [23., 34.], [31., 46.]]) + Wo = obsv(A, C) + np.testing.assert_array_almost_equal(Wo, Wotrue) + + def testCtrbObsvDuality(self, matarrayin): + A = matarrayin([[1.2, -2.3], [3.4, -4.5]]) + B = matarrayin([[5.8, 6.9], [8., 9.1]]) + Wc = ctrb(A, B) + A = np.transpose(A) + C = np.transpose(B) + Wo = np.transpose(obsv(A, C)); + np.testing.assert_array_almost_equal(Wc,Wo) + + @slycotonly + def testGramWc(self, matarrayin, matarrayout): + A = matarrayin([[1., -2.], [3., -4.]]) + B = matarrayin([[5., 6.], [7., 8.]]) + C = matarrayin([[4., 5.], [6., 7.]]) + D = matarrayin([[13., 14.], [15., 16.]]) + sys = ss(A, B, C, D) + Wctrue = np.array([[18.5, 24.5], [24.5, 32.5]]) + + with check_deprecated_matrix(): + Wc = gram(sys, 'c') + + assert ismatarrayout(Wc) + np.testing.assert_array_almost_equal(Wc, Wctrue) + + @slycotonly + def testGramRc(self, matarrayin): + A = matarrayin([[1., -2.], [3., -4.]]) + B = matarrayin([[5., 6.], [7., 8.]]) + C = matarrayin([[4., 5.], [6., 7.]]) + D = matarrayin([[13., 14.], [15., 16.]]) + sys = ss(A, B, C, D) + Rctrue = np.array([[4.30116263, 5.6961343], [0., 0.23249528]]) + Rc = gram(sys, 'cf') + np.testing.assert_array_almost_equal(Rc, Rctrue) + + @slycotonly + def testGramWo(self, matarrayin): + A = matarrayin([[1., -2.], [3., -4.]]) + B = matarrayin([[5., 6.], [7., 8.]]) + C = matarrayin([[4., 5.], [6., 7.]]) + D = matarrayin([[13., 14.], [15., 16.]]) + sys = ss(A, B, C, D) + Wotrue = np.array([[257.5, -94.5], [-94.5, 56.5]]) + Wo = gram(sys, 'o') + np.testing.assert_array_almost_equal(Wo, Wotrue) + + @slycotonly + def testGramWo2(self, matarrayin): + A = matarrayin([[1., -2.], [3., -4.]]) + B = matarrayin([[5.], [7.]]) + C = matarrayin([[6., 8.]]) + D = matarrayin([[9.]]) + sys = ss(A,B,C,D) + Wotrue = np.array([[198., -72.], [-72., 44.]]) + Wo = gram(sys, 'o') + np.testing.assert_array_almost_equal(Wo, Wotrue) + + @slycotonly + def testGramRo(self, matarrayin): + A = matarrayin([[1., -2.], [3., -4.]]) + B = matarrayin([[5., 6.], [7., 8.]]) + C = matarrayin([[4., 5.], [6., 7.]]) + D = matarrayin([[13., 14.], [15., 16.]]) + sys = ss(A, B, C, D) + Rotrue = np.array([[16.04680654, -5.8890222], [0., 4.67112593]]) + Ro = gram(sys, 'of') + np.testing.assert_array_almost_equal(Ro, Rotrue) + + def testGramsys(self): + num =[1.] + den = [1., 1., 1.] + sys = tf(num,den) + with pytest.raises(ValueError): + gram(sys, 'o') + with pytest.raises(ValueError): + gram(sys, 'c') + + def testAcker(self, fixedseed): + for states in range(1, self.maxStates): + for i in range(self.maxTries): + # start with a random SS system and transform to TF then + # back to SS, check that the matrices are the same. + sys = rss(states, 1, 1) + if (self.debug): + print(sys) + + # Make sure the system is not degenerate + Cmat = ctrb(sys.A, sys.B) + if np.linalg.matrix_rank(Cmat) != states: + if (self.debug): + print(" skipping (not reachable or ill conditioned)") + continue + + # Place the poles at random locations + des = rss(states, 1, 1); + poles = pole(des) + + # Now place the poles using acker + K = acker(sys.A, sys.B, poles) + new = ss(sys.A - sys.B * K, sys.B, sys.C, sys.D) + placed = pole(new) + + # Debugging code + # diff = np.sort(poles) - np.sort(placed) + # if not all(diff < 0.001): + # print("Found a problem:") + # print(sys) + # print("desired = ", poles) + + np.testing.assert_array_almost_equal(np.sort(poles), + np.sort(placed), decimal=4) + + def checkPlaced(self, P_expected, P_placed): + """Check that placed poles are correct""" + # No guarantee of the ordering, so sort them + P_expected = np.squeeze(np.asarray(P_expected)) + P_expected.sort() + P_placed.sort() + np.testing.assert_array_almost_equal(P_expected, P_placed) + + def testPlace(self, matarrayin): + # Matrices shamelessly stolen from scipy example code. + A = matarrayin([[1.380, -0.2077, 6.715, -5.676], + [-0.5814, -4.290, 0, 0.6750], + [1.067, 4.273, -6.654, 5.893], + [0.0480, 4.273, 1.343, -2.104]]) + B = matarrayin([[0, 5.679], + [1.136, 1.136], + [0, 0], + [-3.146, 0]]) + P = matarrayin([-0.5 + 1j, -0.5 - 1j, -5.0566, -8.6659]) + K = place(A, B, P) + assert ismatarrayout(K) + P_placed = np.linalg.eigvals(A - B.dot(K)) + self.checkPlaced(P, P_placed) + + # Test that the dimension checks work. + with pytest.raises(ControlDimension): + place(A[1:, :], B, P) + with pytest.raises(ControlDimension): + place(A, B[1:, :], P) + + # Check that we get an error if we ask for too many poles in the same + # location. Here, rank(B) = 2, so lets place three at the same spot. + P_repeated = matarrayin([-0.5, -0.5, -0.5, -8.6659]) + with pytest.raises(ValueError): + place(A, B, P_repeated) + + @slycotonly + def testPlace_varga_continuous(self, matarrayin): + """ + Check that we can place eigenvalues for dtime=False + """ + A = matarrayin([[1., -2.], [3., -4.]]) + B = matarrayin([[5.], [7.]]) + + P = [-2., -2.] + K = place_varga(A, B, P) + P_placed = np.linalg.eigvals(A - B.dot(K)) + self.checkPlaced(P, P_placed) + + # Test that the dimension checks work. + np.testing.assert_raises(ControlDimension, place, A[1:, :], B, P) + np.testing.assert_raises(ControlDimension, place, A, B[1:, :], P) + + # Regression test against bug #177 + # https://github.com/python-control/python-control/issues/177 + A = matarrayin([[0, 1], [100, 0]]) + B = matarrayin([[0], [1]]) + P = matarrayin([-20 + 10*1j, -20 - 10*1j]) + K = place_varga(A, B, P) + P_placed = np.linalg.eigvals(A - B.dot(K)) + self.checkPlaced(P, P_placed) + + + @slycotonly + def testPlace_varga_continuous_partial_eigs(self, matarrayin): + """ + Check that we are able to use the alpha parameter to only place + a subset of the eigenvalues, for the continous time case. + """ + # A matrix has eigenvalues at s=-1, and s=-2. Choose alpha = -1.5 + # and check that eigenvalue at s=-2 stays put. + A = matarrayin([[1., -2.], [3., -4.]]) + B = matarrayin([[5.], [7.]]) + + P = matarrayin([-3.]) + P_expected = np.array([-2.0, -3.0]) + alpha = -1.5 + K = place_varga(A, B, P, alpha=alpha) + + P_placed = np.linalg.eigvals(A - B.dot(K)) + # No guarantee of the ordering, so sort them + self.checkPlaced(P_expected, P_placed) + + @slycotonly + def testPlace_varga_discrete(self, matarrayin): + """ + Check that we can place poles using dtime=True (discrete time) + """ + A = matarrayin([[1., 0], [0, 0.5]]) + B = matarrayin([[5.], [7.]]) + + P = matarrayin([0.5, 0.5]) + K = place_varga(A, B, P, dtime=True) + P_placed = np.linalg.eigvals(A - B.dot(K)) + # No guarantee of the ordering, so sort them + self.checkPlaced(P, P_placed) + + @slycotonly + def testPlace_varga_discrete_partial_eigs(self, matarrayin): + """" + Check that we can only assign a single eigenvalue in the discrete + time case. + """ + # A matrix has eigenvalues at 1.0 and 0.5. Set alpha = 0.51, and + # check that the eigenvalue at 0.5 is not moved. + A = matarrayin([[1., 0], [0, 0.5]]) + B = matarrayin([[5.], [7.]]) + P = matarrayin([0.2, 0.6]) + P_expected = np.array([0.5, 0.6]) + alpha = 0.51 + K = place_varga(A, B, P, dtime=True, alpha=alpha) + P_placed = np.linalg.eigvals(A - B.dot(K)) + self.checkPlaced(P_expected, P_placed) + + + def check_LQR(self, K, S, poles, Q, R): + S_expected = asmatarrayout(np.sqrt(Q.dot(R))) + K_expected = asmatarrayout(S_expected / R) + poles_expected = -np.squeeze(np.asarray(K_expected)) + np.testing.assert_array_almost_equal(S, S_expected) + np.testing.assert_array_almost_equal(K, K_expected) + np.testing.assert_array_almost_equal(poles, poles_expected) + + + @slycotonly + def test_LQR_integrator(self, matarrayin, matarrayout): + A, B, Q, R = (matarrayin([[X]]) for X in [0., 1., 10., 2.]) + K, S, poles = lqr(A, B, Q, R) + self.check_LQR(K, S, poles, Q, R) + + @slycotonly + def test_LQR_3args(self, matarrayin, matarrayout): + sys = ss(0., 1., 1., 0.) + Q, R = (matarrayin([[X]]) for X in [10., 2.]) + K, S, poles = lqr(sys, Q, R) + self.check_LQR(K, S, poles, Q, R) + + @slycotonly + @pytest.mark.xfail(reason="warning not implemented") + def testLQR_warning(self): + """Test lqr() + + Make sure we get a warning if [Q N;N' R] is not positive semi-definite + """ + # from matlab_test siso.ss2 (testLQR); probably not referenced before + # not yet implemented check + A = np.array([[-2, 3, 1], + [-1, 0, 0], + [0, 1, 0]]) + B = np.array([[-1, 0, 0]]).T + Q = np.eye(3) + R = np.eye(1) + N = np.array([[1, 1, 2]]).T + # assert any(np.linalg.eigvals(np.block([[Q, N], [N.T, R]])) < 0) + with pytest.warns(UserWarning): + (K, S, E) = lqr(A, B, Q, R, N) + + def check_LQE(self, L, P, poles, G, QN, RN): + P_expected = asmatarrayout(np.sqrt(G.dot(QN.dot(G).dot(RN)))) + L_expected = asmatarrayout(P_expected / RN) + poles_expected = -np.squeeze(np.asarray(L_expected)) + np.testing.assert_array_almost_equal(P, P_expected) + np.testing.assert_array_almost_equal(L, L_expected) + np.testing.assert_array_almost_equal(poles, poles_expected) + + @slycotonly + def test_LQE(self, matarrayin): + A, G, C, QN, RN = (matarrayin([[X]]) for X in [0., .1, 1., 10., 2.]) + L, P, poles = lqe(A, G, C, QN, RN) + self.check_LQE(L, P, poles, G, QN, RN) + + @slycotonly + def test_care(self, matarrayin): + """Test stabilizing and anti-stabilizing feedbacks, continuous""" + A = matarrayin(np.diag([1, -1])) + B = matarrayin(np.identity(2)) + Q = matarrayin(np.identity(2)) + R = matarrayin(np.identity(2)) + S = matarrayin(np.zeros((2, 2))) + E = matarrayin(np.identity(2)) + X, L, G = care(A, B, Q, R, S, E, stabilizing=True) + assert np.all(np.real(L) < 0) + X, L, G = care(A, B, Q, R, S, E, stabilizing=False) + assert np.all(np.real(L) > 0) + + @slycotonly + def test_dare(self, matarrayin): + """Test stabilizing and anti-stabilizing feedbacks, discrete""" + A = matarrayin(np.diag([0.5, 2])) + B = matarrayin(np.identity(2)) + Q = matarrayin(np.identity(2)) + R = matarrayin(np.identity(2)) + S = matarrayin(np.zeros((2, 2))) + E = matarrayin(np.identity(2)) + X, L, G = dare(A, B, Q, R, S, E, stabilizing=True) + assert np.all(np.abs(L) < 1) + X, L, G = dare(A, B, Q, R, S, E, stabilizing=False) + assert np.all(np.abs(L) > 1) From 63868327be05597076550e4cc43e1df00adac3ce Mon Sep 17 00:00:00 2001 From: bnavigator Date: Wed, 30 Dec 2020 00:14:24 +0100 Subject: [PATCH 26/30] pytestify statesp tests --- control/tests/statesp_array_test.py | 639 ------------------ ...statesp_matrix_test.py => statesp_test.py} | 623 ++++++++++------- 2 files changed, 375 insertions(+), 887 deletions(-) delete mode 100644 control/tests/statesp_array_test.py rename control/tests/{statesp_matrix_test.py => statesp_test.py} (56%) diff --git a/control/tests/statesp_array_test.py b/control/tests/statesp_array_test.py deleted file mode 100644 index f0574cf24..000000000 --- a/control/tests/statesp_array_test.py +++ /dev/null @@ -1,639 +0,0 @@ -#!/usr/bin/env python -# -# statesp_test.py - test state space class with use_numpy_matrix(False) -# RMM, 14 Jun 2019 (coverted from statesp_test.py) - -import unittest -import numpy as np -from numpy.linalg import solve -from scipy.linalg import eigvals, block_diag -from control import matlab -from control.statesp import StateSpace, _convertToStateSpace, tf2ss -from control.xferfcn import TransferFunction, ss2tf -from control.lti import evalfr -from control.exception import slycot_check -from control.config import use_numpy_matrix, reset_defaults -from control.config import defaults - -class TestStateSpace(unittest.TestCase): - """Tests for the StateSpace class.""" - - def setUp(self): - """Set up a MIMO system to test operations on.""" - use_numpy_matrix(False) - - # sys1: 3-states square system (2 inputs x 2 outputs) - A322 = [[-3., 4., 2.], - [-1., -3., 0.], - [2., 5., 3.]] - B322 = [[1., 4.], - [-3., -3.], - [-2., 1.]] - C322 = [[4., 2., -3.], - [1., 4., 3.]] - D322 = [[-2., 4.], - [0., 1.]] - self.sys322 = StateSpace(A322, B322, C322, D322) - - # sys1: 2-states square system (2 inputs x 2 outputs) - A222 = [[4., 1.], - [2., -3]] - B222 = [[5., 2.], - [-3., -3.]] - C222 = [[2., -4], - [0., 1.]] - D222 = [[3., 2.], - [1., -1.]] - self.sys222 = StateSpace(A222, B222, C222, D222) - - # sys3: 6 states non square system (2 inputs x 3 outputs) - A623 = np.array([[1, 0, 0, 0, 0, 0], - [0, 1, 0, 0, 0, 0], - [0, 0, 3, 0, 0, 0], - [0, 0, 0, -4, 0, 0], - [0, 0, 0, 0, -1, 0], - [0, 0, 0, 0, 0, 3]]) - B623 = np.array([[0, -1], - [-1, 0], - [1, -1], - [0, 0], - [0, 1], - [-1, -1]]) - C623 = np.array([[1, 0, 0, 1, 0, 0], - [0, 1, 0, 1, 0, 1], - [0, 0, 1, 0, 0, 1]]) - D623 = np.zeros((3, 2)) - self.sys623 = StateSpace(A623, B623, C623, D623) - - def test_matlab_style_constructor(self): - # Use (deprecated?) matrix-style construction string (w/ warnings off) - import warnings - warnings.filterwarnings("ignore") # turn off warnings - sys = StateSpace("-1 1; 0 2", "0; 1", "1, 0", "0") - warnings.resetwarnings() # put things back to original state - self.assertEqual(sys.A.shape, (2, 2)) - self.assertEqual(sys.B.shape, (2, 1)) - self.assertEqual(sys.C.shape, (1, 2)) - self.assertEqual(sys.D.shape, (1, 1)) - if defaults['statesp.use_numpy_matrix']: - for X in [sys.A, sys.B, sys.C, sys.D]: - self.assertTrue(isinstance(X, np.matrix)) - else: - for X in [sys.A, sys.B, sys.C, sys.D]: - self.assertTrue(isinstance(X, np.ndarray)) - - def test_pole(self): - """Evaluate the poles of a MIMO system.""" - - p = np.sort(self.sys322.pole()) - true_p = np.sort([3.34747678408874, - -3.17373839204437 + 1.47492908003839j, - -3.17373839204437 - 1.47492908003839j]) - - np.testing.assert_array_almost_equal(p, true_p) - - def test_zero_empty(self): - """Test to make sure zero() works with no zeros in system.""" - sys = _convertToStateSpace(TransferFunction([1], [1, 2, 1])) - np.testing.assert_array_equal(sys.zero(), np.array([])) - - @unittest.skipIf(not slycot_check(), "slycot not installed") - def test_zero_siso(self): - """Evaluate the zeros of a SISO system.""" - # extract only first input / first output system of sys222. This system is denoted sys111 - # or tf111 - tf111 = ss2tf(self.sys222) - sys111 = tf2ss(tf111[0, 0]) - - # compute zeros as root of the characteristic polynomial at the numerator of tf111 - # this method is simple and assumed as valid in this test - true_z = np.sort(tf111[0, 0].zero()) - # Compute the zeros through ab08nd, which is tested here - z = np.sort(sys111.zero()) - - np.testing.assert_almost_equal(true_z, z) - - @unittest.skipIf(not slycot_check(), "slycot not installed") - def test_zero_mimo_sys322_square(self): - """Evaluate the zeros of a square MIMO system.""" - - z = np.sort(self.sys322.zero()) - true_z = np.sort([44.41465, -0.490252, -5.924398]) - np.testing.assert_array_almost_equal(z, true_z) - - @unittest.skipIf(not slycot_check(), "slycot not installed") - def test_zero_mimo_sys222_square(self): - """Evaluate the zeros of a square MIMO system.""" - - z = np.sort(self.sys222.zero()) - true_z = np.sort([-10.568501, 3.368501]) - np.testing.assert_array_almost_equal(z, true_z) - - @unittest.skipIf(not slycot_check(), "slycot not installed") - def test_zero_mimo_sys623_non_square(self): - """Evaluate the zeros of a non square MIMO system.""" - - z = np.sort(self.sys623.zero()) - true_z = np.sort([2., -1.]) - np.testing.assert_array_almost_equal(z, true_z) - - def test_add_ss(self): - """Add two MIMO systems.""" - - A = [[-3., 4., 2., 0., 0.], [-1., -3., 0., 0., 0.], - [2., 5., 3., 0., 0.], [0., 0., 0., 4., 1.], [0., 0., 0., 2., -3.]] - B = [[1., 4.], [-3., -3.], [-2., 1.], [5., 2.], [-3., -3.]] - C = [[4., 2., -3., 2., -4.], [1., 4., 3., 0., 1.]] - D = [[1., 6.], [1., 0.]] - - sys = self.sys322 + self.sys222 - - np.testing.assert_array_almost_equal(sys.A, A) - np.testing.assert_array_almost_equal(sys.B, B) - np.testing.assert_array_almost_equal(sys.C, C) - np.testing.assert_array_almost_equal(sys.D, D) - - def test_subtract_ss(self): - """Subtract two MIMO systems.""" - - A = [[-3., 4., 2., 0., 0.], [-1., -3., 0., 0., 0.], - [2., 5., 3., 0., 0.], [0., 0., 0., 4., 1.], [0., 0., 0., 2., -3.]] - B = [[1., 4.], [-3., -3.], [-2., 1.], [5., 2.], [-3., -3.]] - C = [[4., 2., -3., -2., 4.], [1., 4., 3., 0., -1.]] - D = [[-5., 2.], [-1., 2.]] - - sys = self.sys322 - self.sys222 - - np.testing.assert_array_almost_equal(sys.A, A) - np.testing.assert_array_almost_equal(sys.B, B) - np.testing.assert_array_almost_equal(sys.C, C) - np.testing.assert_array_almost_equal(sys.D, D) - - def test_multiply_ss(self): - """Multiply two MIMO systems.""" - - A = [[4., 1., 0., 0., 0.], [2., -3., 0., 0., 0.], [2., 0., -3., 4., 2.], - [-6., 9., -1., -3., 0.], [-4., 9., 2., 5., 3.]] - B = [[5., 2.], [-3., -3.], [7., -2.], [-12., -3.], [-5., -5.]] - C = [[-4., 12., 4., 2., -3.], [0., 1., 1., 4., 3.]] - D = [[-2., -8.], [1., -1.]] - - sys = self.sys322 * self.sys222 - - np.testing.assert_array_almost_equal(sys.A, A) - np.testing.assert_array_almost_equal(sys.B, B) - np.testing.assert_array_almost_equal(sys.C, C) - np.testing.assert_array_almost_equal(sys.D, D) - - def test_evalfr(self): - """Evaluate the frequency response at one frequency.""" - - A = [[-2, 0.5], [0.5, -0.3]] - B = [[0.3, -1.3], [0.1, 0.]] - C = [[0., 0.1], [-0.3, -0.2]] - D = [[0., -0.8], [-0.3, 0.]] - sys = StateSpace(A, B, C, D) - - resp = [[4.37636761487965e-05 - 0.0152297592997812j, - -0.792603938730853 + 0.0261706783369803j], - [-0.331544857768052 + 0.0576105032822757j, - 0.128919037199125 - 0.143824945295405j]] - - # Correct versions of the call - np.testing.assert_almost_equal(evalfr(sys, 1j), resp) - np.testing.assert_almost_equal(sys._evalfr(1.), resp) - - # Deprecated version of the call (should generate warning) - import warnings - with warnings.catch_warnings(record=True) as w: - # Set up warnings filter to only show warnings in control module - warnings.filterwarnings("ignore") - warnings.filterwarnings("always", module="control") - - # Make sure that we get a pending deprecation warning - sys.evalfr(1.) - assert len(w) == 1 - assert issubclass(w[-1].category, PendingDeprecationWarning) - - # Leave the warnings filter like we found it - warnings.resetwarnings() - - @unittest.skipIf(not slycot_check(), "slycot not installed") - def test_freq_resp(self): - """Evaluate the frequency response at multiple frequencies.""" - - A = [[-2, 0.5], [0.5, -0.3]] - B = [[0.3, -1.3], [0.1, 0.]] - C = [[0., 0.1], [-0.3, -0.2]] - D = [[0., -0.8], [-0.3, 0.]] - sys = StateSpace(A, B, C, D) - - true_mag = [[[0.0852992637230322, 0.00103596611395218], - [0.935374692849736, 0.799380720864549]], - [[0.55656854563842, 0.301542699860857], - [0.609178071542849, 0.0382108097985257]]] - true_phase = [[[-0.566195599644593, -1.68063565332582], - [3.0465958317514, 3.14141384339534]], - [[2.90457947657161, 3.10601268291914], - [-0.438157380501337, -1.40720969147217]]] - true_omega = [0.1, 10.] - - mag, phase, omega = sys.freqresp(true_omega) - - np.testing.assert_almost_equal(mag, true_mag) - np.testing.assert_almost_equal(phase, true_phase) - np.testing.assert_equal(omega, true_omega) - - @unittest.skipIf(not slycot_check(), "slycot not installed") - def test_minreal(self): - """Test a minreal model reduction.""" - # A = [-2, 0.5, 0; 0.5, -0.3, 0; 0, 0, -0.1] - A = [[-2, 0.5, 0], [0.5, -0.3, 0], [0, 0, -0.1]] - # B = [0.3, -1.3; 0.1, 0; 1, 0] - B = [[0.3, -1.3], [0.1, 0.], [1.0, 0.0]] - # C = [0, 0.1, 0; -0.3, -0.2, 0] - C = [[0., 0.1, 0.0], [-0.3, -0.2, 0.0]] - # D = [0 -0.8; -0.3 0] - D = [[0., -0.8], [-0.3, 0.]] - # sys = ss(A, B, C, D) - - sys = StateSpace(A, B, C, D) - sysr = sys.minreal() - self.assertEqual(sysr.states, 2) - self.assertEqual(sysr.inputs, sys.inputs) - self.assertEqual(sysr.outputs, sys.outputs) - np.testing.assert_array_almost_equal( - eigvals(sysr.A), [-2.136154, -0.1638459]) - - def test_append_ss(self): - """Test appending two state-space systems.""" - A1 = [[-2, 0.5, 0], [0.5, -0.3, 0], [0, 0, -0.1]] - B1 = [[0.3, -1.3], [0.1, 0.], [1.0, 0.0]] - C1 = [[0., 0.1, 0.0], [-0.3, -0.2, 0.0]] - D1 = [[0., -0.8], [-0.3, 0.]] - A2 = [[-1.]] - B2 = [[1.2]] - C2 = [[0.5]] - D2 = [[0.4]] - A3 = [[-2, 0.5, 0, 0], [0.5, -0.3, 0, 0], [0, 0, -0.1, 0], - [0, 0, 0., -1.]] - B3 = [[0.3, -1.3, 0], [0.1, 0., 0], [1.0, 0.0, 0], [0., 0, 1.2]] - C3 = [[0., 0.1, 0.0, 0.0], [-0.3, -0.2, 0.0, 0.0], [0., 0., 0., 0.5]] - D3 = [[0., -0.8, 0.], [-0.3, 0., 0.], [0., 0., 0.4]] - sys1 = StateSpace(A1, B1, C1, D1) - sys2 = StateSpace(A2, B2, C2, D2) - sys3 = StateSpace(A3, B3, C3, D3) - sys3c = sys1.append(sys2) - np.testing.assert_array_almost_equal(sys3.A, sys3c.A) - np.testing.assert_array_almost_equal(sys3.B, sys3c.B) - np.testing.assert_array_almost_equal(sys3.C, sys3c.C) - np.testing.assert_array_almost_equal(sys3.D, sys3c.D) - - def test_append_tf(self): - """Test appending a state-space system with a tf""" - A1 = [[-2, 0.5, 0], [0.5, -0.3, 0], [0, 0, -0.1]] - B1 = [[0.3, -1.3], [0.1, 0.], [1.0, 0.0]] - C1 = [[0., 0.1, 0.0], [-0.3, -0.2, 0.0]] - D1 = [[0., -0.8], [-0.3, 0.]] - s = TransferFunction([1, 0], [1]) - h = 1 / (s + 1) / (s + 2) - sys1 = StateSpace(A1, B1, C1, D1) - sys2 = _convertToStateSpace(h) - sys3c = sys1.append(sys2) - np.testing.assert_array_almost_equal(sys1.A, sys3c.A[:3, :3]) - np.testing.assert_array_almost_equal(sys1.B, sys3c.B[:3, :2]) - np.testing.assert_array_almost_equal(sys1.C, sys3c.C[:2, :3]) - np.testing.assert_array_almost_equal(sys1.D, sys3c.D[:2, :2]) - np.testing.assert_array_almost_equal(sys2.A, sys3c.A[3:, 3:]) - np.testing.assert_array_almost_equal(sys2.B, sys3c.B[3:, 2:]) - np.testing.assert_array_almost_equal(sys2.C, sys3c.C[2:, 3:]) - np.testing.assert_array_almost_equal(sys2.D, sys3c.D[2:, 2:]) - 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): - - sys1 = StateSpace([[1., 2.], [3., 4.]], - [[5., 6.], [6., 8.]], - [[9., 10.], [11., 12.]], - [[13., 14.], [15., 16.]], 1) - - sys1_11 = sys1[0, 1] - np.testing.assert_array_almost_equal(sys1_11.A, - sys1.A) - np.testing.assert_array_almost_equal(sys1_11.B, - sys1.B[:, [1]]) - np.testing.assert_array_almost_equal(sys1_11.C, - sys1.C[[0], :]) - np.testing.assert_array_almost_equal(sys1_11.D, sys1.D[0,1]) - - assert sys1.dt == sys1_11.dt - - def test_dc_gain_cont(self): - """Test DC gain for continuous-time state-space systems.""" - sys = StateSpace(-2., 6., 5., 0) - np.testing.assert_equal(sys.dcgain(), 15.) - - sys2 = StateSpace(-2, [[6., 4.]], [[5.], [7.], [11]], np.zeros((3, 2))) - expected = np.array([[15., 10.], [21., 14.], [33., 22.]]) - np.testing.assert_array_equal(sys2.dcgain(), expected) - - sys3 = StateSpace(0., 1., 1., 0.) - np.testing.assert_equal(sys3.dcgain(), np.nan) - - def test_dc_gain_discr(self): - """Test DC gain for discrete-time state-space systems.""" - # static gain - sys = StateSpace([], [], [], 2, True) - np.testing.assert_equal(sys.dcgain(), 2) - - # averaging filter - sys = StateSpace(0.5, 0.5, 1, 0, True) - np.testing.assert_almost_equal(sys.dcgain(), 1) - - # differencer - sys = StateSpace(0, 1, -1, 1, True) - np.testing.assert_equal(sys.dcgain(), 0) - - # summer - sys = StateSpace(1, 1, 1, 0, True) - np.testing.assert_equal(sys.dcgain(), np.nan) - - def test_dc_gain_integrator(self): - """DC gain when eigenvalue at DC returns appropriately sized array of nan.""" - # the SISO case is also tested in test_dc_gain_{cont,discr} - import itertools - # iterate over input and output sizes, and continuous (dt=None) and discrete (dt=True) time - for inputs, outputs, dt in itertools.product(range(1, 6), range(1, 6), [None, True]): - states = max(inputs, outputs) - - # a matrix that is singular at DC, and has no "useless" states as in - # _remove_useless_states - a = np.triu(np.tile(2, (states, states))) - # eigenvalues all +2, except for ... - a[0, 0] = 0 if dt is None else 1 - b = np.eye(max(inputs, states))[:states, :inputs] - c = np.eye(max(outputs, states))[:outputs, :states] - d = np.zeros((outputs, inputs)) - sys = StateSpace(a, b, c, d, dt) - dc = np.squeeze(np.tile(np.nan, (outputs, inputs))) - np.testing.assert_array_equal(dc, sys.dcgain()) - - def test_scalar_static_gain(self): - """Regression: can we create a scalar static gain?""" - g1 = StateSpace([], [], [], [2]) - g2 = StateSpace([], [], [], [3]) - - # make sure StateSpace internals, specifically ABC matrix - # sizes, are OK for LTI operations - g3 = g1 * g2 - self.assertEqual(6, g3.D[0, 0]) - g4 = g1 + g2 - self.assertEqual(5, g4.D[0, 0]) - g5 = g1.feedback(g2) - np.testing.assert_array_almost_equal(2. / 7, g5.D[0, 0]) - g6 = g1.append(g2) - np.testing.assert_array_equal(np.diag([2, 3]), g6.D) - - def test_matrix_static_gain(self): - """Regression: can we create matrix static gains?""" - d1 = np.array([[1, 2, 3], [4, 5, 6]]) - d2 = np.array([[7, 8], [9, 10], [11, 12]]) - g1 = StateSpace([], [], [], d1) - - # _remove_useless_states was making A = [[0]] - self.assertEqual((0, 0), g1.A.shape) - - g2 = StateSpace([], [], [], d2) - g3 = StateSpace([], [], [], d2.T) - - h1 = g1 * g2 - np.testing.assert_array_equal(np.dot(d1, d2), h1.D) - h2 = g1 + g3 - np.testing.assert_array_equal(d1 + d2.T, h2.D) - h3 = g1.feedback(g2) - np.testing.assert_array_almost_equal( - solve(np.eye(2) + np.dot(d1, d2), d1), h3.D) - h4 = g1.append(g2) - np.testing.assert_array_equal(block_diag(d1, d2), h4.D) - - def test_remove_useless_states(self): - """Regression: _remove_useless_states gives correct ABC sizes.""" - g1 = StateSpace(np.zeros((3, 3)), - np.zeros((3, 4)), - np.zeros((5, 3)), - np.zeros((5, 4))) - self.assertEqual((0, 0), g1.A.shape) - self.assertEqual((0, 4), g1.B.shape) - self.assertEqual((5, 0), g1.C.shape) - self.assertEqual((5, 4), g1.D.shape) - self.assertEqual(0, g1.states) - - def test_bad_empty_matrices(self): - """Mismatched ABCD matrices when some are empty.""" - self.assertRaises(ValueError, StateSpace, [1], [], [], [1]) - self.assertRaises(ValueError, StateSpace, [1], [1], [], [1]) - self.assertRaises(ValueError, StateSpace, [1], [], [1], [1]) - self.assertRaises(ValueError, StateSpace, [], [1], [], [1]) - self.assertRaises(ValueError, StateSpace, [], [1], [1], [1]) - self.assertRaises(ValueError, StateSpace, [], [], [1], [1]) - self.assertRaises(ValueError, StateSpace, [1], [1], [1], []) - - def test_minreal_static_gain(self): - """Regression: minreal on static gain was failing.""" - g1 = StateSpace([], [], [], [1]) - g2 = g1.minreal() - np.testing.assert_array_equal(g1.A, g2.A) - np.testing.assert_array_equal(g1.B, g2.B) - np.testing.assert_array_equal(g1.C, g2.C) - np.testing.assert_array_equal(g1.D, g2.D) - - def test_empty(self): - """Regression: can we create an empty StateSpace object?""" - g1 = StateSpace([], [], [], []) - self.assertEqual(0, g1.states) - self.assertEqual(0, g1.inputs) - self.assertEqual(0, g1.outputs) - - def test_matrix_to_state_space(self): - """_convertToStateSpace(matrix) gives ss([],[],[],D)""" - D = np.array([[1, 2, 3], [4, 5, 6]]) - g = _convertToStateSpace(D) - - def empty(shape): - m = np.array([]) - m.shape = shape - return m - np.testing.assert_array_equal(empty((0, 0)), g.A) - np.testing.assert_array_equal(empty((0, D.shape[1])), g.B) - np.testing.assert_array_equal(empty((D.shape[0], 0)), g.C) - np.testing.assert_array_equal(D, g.D) - - def test_lft(self): - """ test lft function with result obtained from matlab implementation""" - # test case - A = [[1, 2, 3], - [1, 4, 5], - [2, 3, 4]] - B = [[0, 2], - [5, 6], - [5, 2]] - C = [[1, 4, 5], - [2, 3, 0]] - D = [[0, 0], - [3, 0]] - P = StateSpace(A, B, C, D) - Ak = [[0, 2, 3], - [2, 3, 5], - [2, 1, 9]] - Bk = [[1, 1], - [2, 3], - [9, 4]] - Ck = [[1, 4, 5], - [2, 3, 6]] - Dk = [[0, 2], - [0, 0]] - K = StateSpace(Ak, Bk, Ck, Dk) - - # case 1 - pk = P.lft(K, 2, 1) - Amatlab = [1, 2, 3, 4, 6, 12, 1, 4, 5, 17, 38, 61, 2, 3, 4, 9, 26, 37, 2, 3, 0, 3, 14, 18, 4, 6, 0, 8, 27, 35, 18, 27, 0, 29, 109, 144] - Bmatlab = [0, 10, 10, 7, 15, 58] - Cmatlab = [1, 4, 5, 0, 0, 0] - Dmatlab = [0] - np.testing.assert_allclose(np.array(pk.A).reshape(-1), Amatlab) - np.testing.assert_allclose(np.array(pk.B).reshape(-1), Bmatlab) - np.testing.assert_allclose(np.array(pk.C).reshape(-1), Cmatlab) - np.testing.assert_allclose(np.array(pk.D).reshape(-1), Dmatlab) - - # case 2 - pk = P.lft(K) - Amatlab = [1, 2, 3, 4, 6, 12, -3, -2, 5, 11, 14, 31, -2, -3, 4, 3, 2, 7, 0.6, 3.4, 5, -0.6, -0.4, 0, 0.8, 6.2, 10, 0.2, -4.2, -4, 7.4, 33.6, 45, -0.4, -8.6, -3] - Bmatlab = [] - Cmatlab = [] - Dmatlab = [] - np.testing.assert_allclose(np.array(pk.A).reshape(-1), Amatlab) - np.testing.assert_allclose(np.array(pk.B).reshape(-1), Bmatlab) - np.testing.assert_allclose(np.array(pk.C).reshape(-1), Cmatlab) - np.testing.assert_allclose(np.array(pk.D).reshape(-1), Dmatlab) - - def test_horner(self): - """Test horner() function""" - # Make sure we can compute the transfer function at a complex value - self.sys322.horner(1.+1.j) - - # Make sure result agrees with frequency response - mag, phase, omega = self.sys322.freqresp([1]) - np.testing.assert_array_almost_equal( - self.sys322.horner(1.j), - mag[:,:,0] * np.exp(1.j * phase[:,:,0])) - - def tearDown(self): - reset_defaults() # reset configuration defaults - - -class TestRss(unittest.TestCase): - """These are tests for the proper functionality of statesp.rss.""" - - def setUp(self): - use_numpy_matrix(False) - - # Number of times to run each of the randomized tests. - self.numTests = 100 - # Maxmimum number of states to test + 1 - self.maxStates = 10 - # Maximum number of inputs and outputs to test + 1 - self.maxIO = 5 - - def test_shape(self): - """Test that rss outputs have the right state, input, and output size.""" - - for states in range(1, self.maxStates): - for inputs in range(1, self.maxIO): - for outputs in range(1, self.maxIO): - sys = matlab.rss(states, outputs, inputs) - self.assertEqual(sys.states, states) - self.assertEqual(sys.inputs, inputs) - self.assertEqual(sys.outputs, outputs) - - def test_pole(self): - """Test that the poles of rss outputs have a negative real part.""" - - for states in range(1, self.maxStates): - for inputs in range(1, self.maxIO): - for outputs in range(1, self.maxIO): - sys = matlab.rss(states, outputs, inputs) - p = sys.pole() - for z in p: - self.assertTrue(z.real < 0) - - def tearDown(self): - reset_defaults() # reset configuration defaults - - -class TestDrss(unittest.TestCase): - """These are tests for the proper functionality of statesp.drss.""" - - def setUp(self): - use_numpy_matrix(False) - - # Number of times to run each of the randomized tests. - self.numTests = 100 - # Maximum number of states to test + 1 - self.maxStates = 10 - # Maximum number of inputs and outputs to test + 1 - self.maxIO = 5 - - def test_shape(self): - """Test that drss outputs have the right state, input, and output size.""" - - for states in range(1, self.maxStates): - for inputs in range(1, self.maxIO): - for outputs in range(1, self.maxIO): - sys = matlab.drss(states, outputs, inputs) - self.assertEqual(sys.states, states) - self.assertEqual(sys.inputs, inputs) - self.assertEqual(sys.outputs, outputs) - - def test_pole(self): - """Test that the poles of drss outputs have less than unit magnitude.""" - - for states in range(1, self.maxStates): - for inputs in range(1, self.maxIO): - for outputs in range(1, self.maxIO): - sys = matlab.drss(states, outputs, inputs) - p = sys.pole() - for z in p: - self.assertTrue(abs(z) < 1) - - def test_pole_static(self): - """Regression: pole() of static gain is empty array.""" - np.testing.assert_array_equal(np.array([]), - StateSpace([], [], [], [[1]]).pole()) - - def test_copy_constructor(self): - # Create a set of matrices for a simple linear system - A = np.array([[-1]]) - B = np.array([[1]]) - C = np.array([[1]]) - D = np.array([[0]]) - - # Create the first linear system and a copy - linsys = StateSpace(A, B, C, D) - cpysys = StateSpace(linsys) - - # Change the original A matrix - A[0, 0] = -2 - np.testing.assert_array_equal(linsys.A, [[-1]]) # original value - np.testing.assert_array_equal(cpysys.A, [[-1]]) # original value - - # Change the A matrix for the original system - linsys.A[0, 0] = -3 - np.testing.assert_array_equal(cpysys.A, [[-1]]) # original value - - def tearDown(self): - reset_defaults() # reset configuration defaults - - -if __name__ == "__main__": - unittest.main() diff --git a/control/tests/statesp_matrix_test.py b/control/tests/statesp_test.py similarity index 56% rename from control/tests/statesp_matrix_test.py rename to control/tests/statesp_test.py index e7e91364a..9dbb6da94 100644 --- a/control/tests/statesp_matrix_test.py +++ b/control/tests/statesp_test.py @@ -1,27 +1,32 @@ -#!/usr/bin/env python -# -# statesp_test.py - test state space class -# RMM, 30 Mar 2011 (based on TestStateSp from v0.4a) +"""statesp_test.py - test state space 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 + with use_numpy_matrix(False) +BG, 26 Jul 2020 merge statesp_array_test.py differences into statesp_test.py + convert to pytest +""" -import unittest import numpy as np import pytest from numpy.linalg import solve -from scipy.linalg import eigvals, block_diag -from control import matlab -from control.statesp import StateSpace, _convertToStateSpace, tf2ss -from control.xferfcn import TransferFunction, ss2tf +from scipy.linalg import block_diag, eigvals + +from control.config import defaults +from control.dtime import sample_system from control.lti import evalfr -from control.exception import slycot_check +from control.statesp import (StateSpace, _convertToStateSpace, drss, rss, ss, + tf2ss) +from control.tests.conftest import ismatarrayout, slycotonly +from control.xferfcn import TransferFunction, ss2tf -class TestStateSpace(unittest.TestCase): +class TestStateSpace: """Tests for the StateSpace class.""" - def setUp(self): - """Set up a MIMO system to test operations on.""" - - # sys1: 3-states square system (2 inputs x 2 outputs) + @pytest.fixture + def sys322ABCD(self): + """Matrices for sys322""" A322 = [[-3., 4., 2.], [-1., -3., 0.], [2., 5., 3.]] @@ -32,9 +37,16 @@ def setUp(self): [1., 4., 3.]] D322 = [[-2., 4.], [0., 1.]] - self.sys322 = StateSpace(A322, B322, C322, D322) + return (A322, B322, C322, D322) + + @pytest.fixture + def sys322(self, sys322ABCD): + """3-states square system (2 inputs x 2 outputs)""" + return StateSpace(*sys322ABCD) - # sys1: 2-states square system (2 inputs x 2 outputs) + @pytest.fixture + def sys222(self): + """2-states square system (2 inputs x 2 outputs)""" A222 = [[4., 1.], [2., -3]] B222 = [[5., 2.], @@ -43,9 +55,11 @@ def setUp(self): [0., 1.]] D222 = [[3., 2.], [1., -1.]] - self.sys222 = StateSpace(A222, B222, C222, D222) + return StateSpace(A222, B222, C222, D222) - # sys3: 6 states non square system (2 inputs x 3 outputs) + @pytest.fixture + def sys623(self): + """sys3: 6 states non square system (2 inputs x 3 outputs)""" A623 = np.array([[1, 0, 0, 0, 0, 0], [0, 1, 0, 0, 0, 0], [0, 0, 3, 0, 0, 0], @@ -62,16 +76,129 @@ def setUp(self): [0, 1, 0, 1, 0, 1], [0, 0, 1, 0, 0, 1]]) D623 = np.zeros((3, 2)) - self.sys623 = StateSpace(A623, B623, C623, D623) + return StateSpace(A623, B623, C623, D623) + + @pytest.mark.parametrize( + "dt", + [(), (None, ), (0, ), (1, ), (0.1, ), (True, )], + ids=lambda i: "dt " + ("unspec" if len(i) == 0 else str(i[0]))) + @pytest.mark.parametrize( + "argfun", + [pytest.param( + lambda ABCDdt: (ABCDdt, {}), + id="A, B, C, D[, dt]"), + pytest.param( + lambda ABCDdt: (ABCDdt[:4], {'dt': dt_ for dt_ in ABCDdt[4:]}), + id="A, B, C, D[, dt=dt]"), + pytest.param( + lambda ABCDdt: ((StateSpace(*ABCDdt), ), {}), + id="sys") + ]) + def test_constructor(self, sys322ABCD, dt, argfun): + """Test different ways to call the StateSpace() constructor""" + args, kwargs = argfun(sys322ABCD + dt) + sys = StateSpace(*args, **kwargs) + + dtref = defaults['control.default_dt'] if len(dt) == 0 else dt[0] + np.testing.assert_almost_equal(sys.A, sys322ABCD[0]) + np.testing.assert_almost_equal(sys.B, sys322ABCD[1]) + np.testing.assert_almost_equal(sys.C, sys322ABCD[2]) + np.testing.assert_almost_equal(sys.D, sys322ABCD[3]) + assert sys.dt == dtref + + @pytest.mark.parametrize("args, exc, errmsg", + [((True, ), TypeError, + "(can only take in|sys must be) a StateSpace"), + ((1, 2), ValueError, "1, 4, or 5 arguments"), + ((np.ones((3, 2)), np.ones((3, 2)), + np.ones((2, 2)), np.ones((2, 2))), + ValueError, "A must be square"), + ((np.ones((3, 3)), np.ones((2, 2)), + np.ones((2, 3)), np.ones((2, 2))), + ValueError, "A and B"), + ((np.ones((3, 3)), np.ones((3, 2)), + np.ones((2, 2)), np.ones((2, 2))), + ValueError, "A and C"), + ((np.ones((3, 3)), np.ones((3, 2)), + np.ones((2, 3)), np.ones((2, 3))), + ValueError, "B and D"), + ((np.ones((3, 3)), np.ones((3, 2)), + np.ones((2, 3)), np.ones((3, 2))), + ValueError, "C and D"), + ]) + def test_constructor_invalid(self, args, exc, errmsg): + """Test invalid input to StateSpace() constructor""" + with pytest.raises(exc, match=errmsg): + StateSpace(*args) + with pytest.raises(exc, match=errmsg): + ss(*args) + + def test_constructor_warns(self, sys322ABCD): + """Test ambiguos input to StateSpace() constructor""" + with pytest.warns(UserWarning, match="received multiple dt"): + sys = StateSpace(*(sys322ABCD + (0.1, )), dt=0.2) + np.testing.assert_almost_equal(sys.A, sys322ABCD[0]) + np.testing.assert_almost_equal(sys.B, sys322ABCD[1]) + np.testing.assert_almost_equal(sys.C, sys322ABCD[2]) + np.testing.assert_almost_equal(sys.D, sys322ABCD[3]) + assert sys.dt == 0.1 + + def test_copy_constructor(self): + """Test the copy constructor""" + # Create a set of matrices for a simple linear system + A = np.array([[-1]]) + B = np.array([[1]]) + C = np.array([[1]]) + D = np.array([[0]]) + + # Create the first linear system and a copy + linsys = StateSpace(A, B, C, D) + cpysys = StateSpace(linsys) - def test_D_broadcast(self): + # Change the original A matrix + A[0, 0] = -2 + np.testing.assert_array_equal(linsys.A, [[-1]]) # original value + np.testing.assert_array_equal(cpysys.A, [[-1]]) # original value + + # Change the A matrix for the original system + linsys.A[0, 0] = -3 + np.testing.assert_array_equal(cpysys.A, [[-1]]) # original value + + def test_copy_constructor_nodt(self, sys322): + """Test the copy constructor when an object without dt is passed + + FIXME: may be obsolete in case gh-431 is updated + """ + sysin = sample_system(sys322, 1.) + del sysin.dt + sys = StateSpace(sysin) + assert sys.dt == defaults['control.default_dt'] + + # test for static gain + sysin = StateSpace([], [], [], [[1, 2], [3, 4]], 1.) + del sysin.dt + sys = StateSpace(sysin) + assert sys.dt is None + + def test_matlab_style_constructor(self): + """Use (deprecated) matrix-style construction string""" + with pytest.deprecated_call(): + sys = StateSpace("-1 1; 0 2", "0; 1", "1, 0", "0") + assert sys.A.shape == (2, 2) + assert sys.B.shape == (2, 1) + assert sys.C.shape == (1, 2) + assert sys.D.shape == (1, 1) + for X in [sys.A, sys.B, sys.C, sys.D]: + assert ismatarrayout(X) + + def test_D_broadcast(self, sys623): """Test broadcast of D=0 to the right shape""" # Giving D as a scalar 0 should broadcast to the right shape - sys = StateSpace(self.sys623.A, self.sys623.B, self.sys623.C, 0) - np.testing.assert_array_equal(self.sys623.D, sys.D) + sys = StateSpace(sys623.A, sys623.B, sys623.C, 0) + np.testing.assert_array_equal(sys623.D, sys.D) # Giving D as a matrix of the wrong size should generate an error - with self.assertRaises(ValueError): + with pytest.raises(ValueError): sys = StateSpace(sys.A, sys.B, sys.C, np.array([[0]])) # Make sure that empty systems still work @@ -87,10 +214,10 @@ def test_D_broadcast(self): sys = StateSpace([], [], [], 0) np.testing.assert_array_equal(sys.D, [[0]]) - def test_pole(self): + def test_pole(self, sys322): """Evaluate the poles of a MIMO system.""" - p = np.sort(self.sys322.pole()) + p = np.sort(sys322.pole()) true_p = np.sort([3.34747678408874, -3.17373839204437 + 1.47492908003839j, -3.17373839204437 - 1.47492908003839j]) @@ -102,12 +229,12 @@ def test_zero_empty(self): sys = _convertToStateSpace(TransferFunction([1], [1, 2, 1])) np.testing.assert_array_equal(sys.zero(), np.array([])) - @unittest.skipIf(not slycot_check(), "slycot not installed") - def test_zero_siso(self): + @slycotonly + def test_zero_siso(self, sys222): """Evaluate the zeros of a SISO system.""" # extract only first input / first output system of sys222. This system is denoted sys111 # or tf111 - tf111 = ss2tf(self.sys222) + tf111 = ss2tf(sys222) sys111 = tf2ss(tf111[0, 0]) # compute zeros as root of the characteristic polynomial at the numerator of tf111 @@ -118,31 +245,31 @@ def test_zero_siso(self): np.testing.assert_almost_equal(true_z, z) - @unittest.skipIf(not slycot_check(), "slycot not installed") - def test_zero_mimo_sys322_square(self): + @slycotonly + def test_zero_mimo_sys322_square(self, sys322): """Evaluate the zeros of a square MIMO system.""" - z = np.sort(self.sys322.zero()) + z = np.sort(sys322.zero()) true_z = np.sort([44.41465, -0.490252, -5.924398]) np.testing.assert_array_almost_equal(z, true_z) - @unittest.skipIf(not slycot_check(), "slycot not installed") - def test_zero_mimo_sys222_square(self): + @slycotonly + def test_zero_mimo_sys222_square(self, sys222): """Evaluate the zeros of a square MIMO system.""" - z = np.sort(self.sys222.zero()) + z = np.sort(sys222.zero()) true_z = np.sort([-10.568501, 3.368501]) np.testing.assert_array_almost_equal(z, true_z) - @unittest.skipIf(not slycot_check(), "slycot not installed") - def test_zero_mimo_sys623_non_square(self): + @slycotonly + def test_zero_mimo_sys623_non_square(self, sys623): """Evaluate the zeros of a non square MIMO system.""" - z = np.sort(self.sys623.zero()) + z = np.sort(sys623.zero()) true_z = np.sort([2., -1.]) np.testing.assert_array_almost_equal(z, true_z) - def test_add_ss(self): + def test_add_ss(self, sys222, sys322): """Add two MIMO systems.""" A = [[-3., 4., 2., 0., 0.], [-1., -3., 0., 0., 0.], @@ -151,14 +278,14 @@ def test_add_ss(self): C = [[4., 2., -3., 2., -4.], [1., 4., 3., 0., 1.]] D = [[1., 6.], [1., 0.]] - sys = self.sys322 + self.sys222 + sys = sys322 + sys222 np.testing.assert_array_almost_equal(sys.A, A) np.testing.assert_array_almost_equal(sys.B, B) np.testing.assert_array_almost_equal(sys.C, C) np.testing.assert_array_almost_equal(sys.D, D) - def test_subtract_ss(self): + def test_subtract_ss(self, sys222, sys322): """Subtract two MIMO systems.""" A = [[-3., 4., 2., 0., 0.], [-1., -3., 0., 0., 0.], @@ -167,14 +294,14 @@ def test_subtract_ss(self): C = [[4., 2., -3., -2., 4.], [1., 4., 3., 0., -1.]] D = [[-5., 2.], [-1., 2.]] - sys = self.sys322 - self.sys222 + sys = sys322 - sys222 np.testing.assert_array_almost_equal(sys.A, A) np.testing.assert_array_almost_equal(sys.B, B) np.testing.assert_array_almost_equal(sys.C, C) np.testing.assert_array_almost_equal(sys.D, D) - def test_multiply_ss(self): + def test_multiply_ss(self, sys222, sys322): """Multiply two MIMO systems.""" A = [[4., 1., 0., 0., 0.], [2., -3., 0., 0., 0.], [2., 0., -3., 4., 2.], @@ -183,44 +310,53 @@ def test_multiply_ss(self): C = [[-4., 12., 4., 2., -3.], [0., 1., 1., 4., 3.]] D = [[-2., -8.], [1., -1.]] - sys = self.sys322 * self.sys222 + sys = sys322 * sys222 np.testing.assert_array_almost_equal(sys.A, A) np.testing.assert_array_almost_equal(sys.B, B) np.testing.assert_array_almost_equal(sys.C, C) np.testing.assert_array_almost_equal(sys.D, D) - def test_evalfr(self): - """Evaluate the frequency response at one frequency.""" - + @pytest.mark.parametrize("omega, resp", + [(1., + np.array([[ 4.37636761e-05-0.01522976j, + -7.92603939e-01+0.02617068j], + [-3.31544858e-01+0.0576105j, + 1.28919037e-01-0.14382495j]])), + (32, + np.array([[-1.16548243e-05-3.13444825e-04j, + -7.99936828e-01+4.54201816e-06j], + [-3.00137118e-01+3.42881660e-03j, + 6.32015038e-04-1.21462255e-02j]]))]) + @pytest.mark.parametrize("dt", [None, 0, 1e-3]) + def test_evalfr(self, dt, omega, resp): + """Evaluate the frequency response at single frequencies""" A = [[-2, 0.5], [0.5, -0.3]] B = [[0.3, -1.3], [0.1, 0.]] C = [[0., 0.1], [-0.3, -0.2]] D = [[0., -0.8], [-0.3, 0.]] sys = StateSpace(A, B, C, D) - resp = [[4.37636761487965e-05 - 0.0152297592997812j, - -0.792603938730853 + 0.0261706783369803j], - [-0.331544857768052 + 0.0576105032822757j, - 0.128919037199125 - 0.143824945295405j]] - - # Correct versions of the call - np.testing.assert_almost_equal(evalfr(sys, 1j), resp) - np.testing.assert_almost_equal(sys._evalfr(1.), resp) + if dt: + sys = sample_system(sys, dt) + s = np.exp(omega * 1j * dt) + else: + s = omega * 1j + # Correct version of the call + np.testing.assert_allclose(evalfr(sys, s), resp, atol=1e-3) # Deprecated version of the call (should generate warning) - import warnings - with warnings.catch_warnings(record=True) as w: - # Set up warnings filter to only show warnings in control module - warnings.filterwarnings("ignore") - warnings.filterwarnings("always", module="control") - - # Make sure that we get a pending deprecation warning - sys.evalfr(1.) - assert len(w) == 1 - assert issubclass(w[-1].category, PendingDeprecationWarning) - - @unittest.skipIf(not slycot_check(), "slycot not installed") + with pytest.deprecated_call(): + np.testing.assert_allclose(sys.evalfr(omega), resp, atol=1e-3) + + # call above nyquist frequency + if dt: + with pytest.warns(UserWarning): + np.testing.assert_allclose(sys._evalfr(omega + 2 * np.pi / dt), + resp, + atol=1e-3) + + @slycotonly def test_freq_resp(self): """Evaluate the frequency response at multiple frequencies.""" @@ -246,7 +382,28 @@ def test_freq_resp(self): np.testing.assert_almost_equal(phase, true_phase) np.testing.assert_equal(omega, true_omega) - @unittest.skipIf(not slycot_check(), "slycot not installed") + def test_is_static_gain(self): + A0 = np.zeros((2,2)) + A1 = A0.copy() + A1[0,1] = 1.1 + B0 = np.zeros((2,1)) + B1 = B0.copy() + B1[0,0] = 1.3 + C0 = A0 + C1 = np.eye(2) + D0 = 0 + D1 = np.ones((2,1)) + assert StateSpace(A0, B0, C1, D1).is_static_gain() + # TODO: fix this once remove_useless_states is false by default + # should be False when remove_useless is false + # print(StateSpace(A1, B0, C1, D1).is_static_gain()) + assert not StateSpace(A0, B1, C1, D1).is_static_gain() + assert not StateSpace(A1, B1, C1, D1).is_static_gain() + assert StateSpace(A0, B0, C0, D0).is_static_gain() + assert StateSpace(A0, B0, C0, D1).is_static_gain() + assert StateSpace(A0, B0, C1, D0).is_static_gain() + + @slycotonly def test_minreal(self): """Test a minreal model reduction.""" # A = [-2, 0.5, 0; 0.5, -0.3, 0; 0, 0, -0.1] @@ -261,9 +418,9 @@ def test_minreal(self): sys = StateSpace(A, B, C, D) sysr = sys.minreal() - self.assertEqual(sysr.states, 2) - self.assertEqual(sysr.inputs, sys.inputs) - self.assertEqual(sysr.outputs, sys.outputs) + assert sysr.states == 2 + assert sysr.inputs == sys.inputs + assert sysr.outputs == sys.outputs np.testing.assert_array_almost_equal( eigvals(sysr.A), [-2.136154, -0.1638459]) @@ -335,11 +492,11 @@ def test_array_access_ss(self): def test_dc_gain_cont(self): """Test DC gain for continuous-time state-space systems.""" sys = StateSpace(-2., 6., 5., 0) - np.testing.assert_equal(sys.dcgain(), 15.) + np.testing.assert_allclose(sys.dcgain(), 15.) sys2 = StateSpace(-2, [6., 4.], [[5.], [7.], [11]], np.zeros((3, 2))) expected = np.array([[15., 10.], [21., 14.], [33., 22.]]) - np.testing.assert_array_equal(sys2.dcgain(), expected) + np.testing.assert_allclose(sys2.dcgain(), expected) sys3 = StateSpace(0., 1., 1., 0.) np.testing.assert_equal(sys3.dcgain(), np.nan) @@ -352,7 +509,7 @@ def test_dc_gain_discr(self): # averaging filter sys = StateSpace(0.5, 0.5, 1, 0, True) - np.testing.assert_almost_equal(sys.dcgain(), 1) + np.testing.assert_allclose(sys.dcgain(), 1) # differencer sys = StateSpace(0, 1, -1, 1, True) @@ -362,61 +519,67 @@ def test_dc_gain_discr(self): sys = StateSpace(1, 1, 1, 0, True) np.testing.assert_equal(sys.dcgain(), np.nan) - def test_dc_gain_integrator(self): - """DC gain when eigenvalue at DC returns appropriately sized array of nan.""" - # the SISO case is also tested in test_dc_gain_{cont,discr} - import itertools - # iterate over input and output sizes, and continuous (dt=None) and discrete (dt=True) time - for inputs, outputs, dt in itertools.product(range(1, 6), range(1, 6), [None, True]): - states = max(inputs, outputs) - - # a matrix that is singular at DC, and has no "useless" states as in - # _remove_useless_states - a = np.triu(np.tile(2, (states, states))) - # eigenvalues all +2, except for ... - a[0, 0] = 0 if dt is None else 1 - b = np.eye(max(inputs, states))[:states, :inputs] - c = np.eye(max(outputs, states))[:outputs, :states] - d = np.zeros((outputs, inputs)) - sys = StateSpace(a, b, c, d, dt) - dc = np.squeeze(np.tile(np.nan, (outputs, inputs))) - np.testing.assert_array_equal(dc, sys.dcgain()) + @pytest.mark.parametrize("outputs", range(1, 6)) + @pytest.mark.parametrize("inputs", range(1, 6)) + @pytest.mark.parametrize("dt", [None, 0, 1, True], + ids=["dtNone", "c", "dt1", "dtTrue"]) + def test_dc_gain_integrator(self, outputs, inputs, dt): + """DC gain when eigenvalue at DC returns appropriately sized array of nan. + + the SISO case is also tested in test_dc_gain_{cont,discr} + time systems (dt=0) + """ + states = max(inputs, outputs) + + # a matrix that is singular at DC, and has no "useless" states as in + # _remove_useless_states + a = np.triu(np.tile(2, (states, states))) + # eigenvalues all +2, except for ... + a[0, 0] = 0 if dt in [0, None] else 1 + b = np.eye(max(inputs, states))[:states, :inputs] + c = np.eye(max(outputs, states))[:outputs, :states] + d = np.zeros((outputs, inputs)) + sys = StateSpace(a, b, c, d, dt) + dc = np.squeeze(np.full_like(d, np.nan)) + np.testing.assert_array_equal(dc, sys.dcgain()) def test_scalar_static_gain(self): - """Regression: can we create a scalar static gain?""" + """Regression: can we create a scalar static gain? + + make sure StateSpace internals, specifically ABC matrix + sizes, are OK for LTI operations + """ g1 = StateSpace([], [], [], [2]) g2 = StateSpace([], [], [], [3]) - # make sure StateSpace internals, specifically ABC matrix - # sizes, are OK for LTI operations g3 = g1 * g2 - self.assertEqual(6, g3.D[0, 0]) + assert 6 == g3.D[0, 0] g4 = g1 + g2 - self.assertEqual(5, g4.D[0, 0]) + assert 5 == g4.D[0, 0] g5 = g1.feedback(g2) - self.assertAlmostEqual(2. / 7, g5.D[0, 0]) + np.testing.assert_allclose(2. / 7, g5.D[0, 0]) g6 = g1.append(g2) - np.testing.assert_array_equal(np.diag([2, 3]), g6.D) + np.testing.assert_allclose(np.diag([2, 3]), g6.D) def test_matrix_static_gain(self): """Regression: can we create matrix static gains?""" - d1 = np.matrix([[1, 2, 3], [4, 5, 6]]) - d2 = np.matrix([[7, 8], [9, 10], [11, 12]]) + d1 = np.array([[1, 2, 3], [4, 5, 6]]) + d2 = np.array([[7, 8], [9, 10], [11, 12]]) g1 = StateSpace([], [], [], d1) # _remove_useless_states was making A = [[0]] - self.assertEqual((0, 0), g1.A.shape) + assert (0, 0) == g1.A.shape g2 = StateSpace([], [], [], d2) g3 = StateSpace([], [], [], d2.T) h1 = g1 * g2 - np.testing.assert_array_equal(d1 * d2, h1.D) + np.testing.assert_array_equal(np.dot(d1, d2), h1.D) h2 = g1 + g3 np.testing.assert_array_equal(d1 + d2.T, h2.D) h3 = g1.feedback(g2) np.testing.assert_array_almost_equal( - solve(np.eye(2) + d1 * d2, d1), h3.D) + solve(np.eye(2) + np.dot(d1, d2), d1), h3.D) h4 = g1.append(g2) np.testing.assert_array_equal(block_diag(d1, d2), h4.D) @@ -426,21 +589,25 @@ def test_remove_useless_states(self): np.zeros((3, 4)), np.zeros((5, 3)), np.zeros((5, 4))) - self.assertEqual((0, 0), g1.A.shape) - self.assertEqual((0, 4), g1.B.shape) - self.assertEqual((5, 0), g1.C.shape) - self.assertEqual((5, 4), g1.D.shape) - self.assertEqual(0, g1.states) - - def test_bad_empty_matrices(self): + assert (0, 0) == g1.A.shape + assert (0, 4) == g1.B.shape + assert (5, 0) == g1.C.shape + assert (5, 4) == g1.D.shape + assert 0 == g1.states + + @pytest.mark.parametrize("A, B, C, D", + [([1], [], [], [1]), + ([1], [1], [], [1]), + ([1], [], [1], [1]), + ([], [1], [], [1]), + ([], [1], [1], [1]), + ([], [], [1], [1]), + ([1], [1], [1], [])]) + def test_bad_empty_matrices(self, A, B, C, D): """Mismatched ABCD matrices when some are empty.""" - self.assertRaises(ValueError, StateSpace, [1], [], [], [1]) - self.assertRaises(ValueError, StateSpace, [1], [1], [], [1]) - self.assertRaises(ValueError, StateSpace, [1], [], [1], [1]) - self.assertRaises(ValueError, StateSpace, [], [1], [], [1]) - self.assertRaises(ValueError, StateSpace, [], [1], [1], [1]) - self.assertRaises(ValueError, StateSpace, [], [], [1], [1]) - self.assertRaises(ValueError, StateSpace, [1], [1], [1], []) + with pytest.raises(ValueError): + StateSpace(A, B, C, D) + def test_minreal_static_gain(self): """Regression: minreal on static gain was failing.""" @@ -454,22 +621,19 @@ def test_minreal_static_gain(self): def test_empty(self): """Regression: can we create an empty StateSpace object?""" g1 = StateSpace([], [], [], []) - self.assertEqual(0, g1.states) - self.assertEqual(0, g1.inputs) - self.assertEqual(0, g1.outputs) + assert 0 == g1.states + assert 0 == g1.inputs + assert 0 == g1.outputs def test_matrix_to_state_space(self): """_convertToStateSpace(matrix) gives ss([],[],[],D)""" - D = np.matrix([[1, 2, 3], [4, 5, 6]]) + with pytest.deprecated_call(): + D = np.matrix([[1, 2, 3], [4, 5, 6]]) g = _convertToStateSpace(D) - def empty(shape): - m = np.matrix([]) - m.shape = shape - return m - np.testing.assert_array_equal(empty((0, 0)), g.A) - np.testing.assert_array_equal(empty((0, D.shape[1])), g.B) - np.testing.assert_array_equal(empty((D.shape[0], 0)), g.C) + np.testing.assert_array_equal(np.empty((0, 0)), g.A) + np.testing.assert_array_equal(np.empty((0, D.shape[1])), g.B) + np.testing.assert_array_equal(np.empty((D.shape[0], 0)), g.C) np.testing.assert_array_equal(D, g.D) def test_lft(self): @@ -500,7 +664,9 @@ def test_lft(self): # case 1 pk = P.lft(K, 2, 1) - Amatlab = [1, 2, 3, 4, 6, 12, 1, 4, 5, 17, 38, 61, 2, 3, 4, 9, 26, 37, 2, 3, 0, 3, 14, 18, 4, 6, 0, 8, 27, 35, 18, 27, 0, 29, 109, 144] + Amatlab = [1, 2, 3, 4, 6, 12, 1, 4, 5, 17, 38, 61, 2, 3, 4, 9, 26, 37, + 2, 3, 0, 3, 14, 18, 4, 6, 0, 8, 27, 35, 18, 27, 0, 29, 109, + 144] Bmatlab = [0, 10, 10, 7, 15, 58] Cmatlab = [1, 4, 5, 0, 0, 0] Dmatlab = [0] @@ -511,7 +677,9 @@ def test_lft(self): # case 2 pk = P.lft(K) - Amatlab = [1, 2, 3, 4, 6, 12, -3, -2, 5, 11, 14, 31, -2, -3, 4, 3, 2, 7, 0.6, 3.4, 5, -0.6, -0.4, 0, 0.8, 6.2, 10, 0.2, -4.2, -4, 7.4, 33.6, 45, -0.4, -8.6, -3] + Amatlab = [1, 2, 3, 4, 6, 12, -3, -2, 5, 11, 14, 31, -2, -3, 4, 3, 2, + 7, 0.6, 3.4, 5, -0.6, -0.4, 0, 0.8, 6.2, 10, 0.2, -4.2, + -4, 7.4, 33.6, 45, -0.4, -8.6, -3] Bmatlab = [] Cmatlab = [] Dmatlab = [] @@ -520,28 +688,29 @@ def test_lft(self): np.testing.assert_allclose(np.array(pk.C).reshape(-1), Cmatlab) np.testing.assert_allclose(np.array(pk.D).reshape(-1), Dmatlab) - def test_repr(self): - ref322 = """StateSpace(array([[-3., 4., 2.], - [-1., -3., 0.], - [ 2., 5., 3.]]), array([[ 1., 4.], - [-3., -3.], - [-2., 1.]]), array([[ 4., 2., -3.], - [ 1., 4., 3.]]), array([[-2., 4.], - [ 0., 1.]]){dt})""" - self.assertEqual(repr(self.sys322), ref322.format(dt='')) - sysd = StateSpace(self.sys322.A, self.sys322.B, - self.sys322.C, self.sys322.D, 0.4) - self.assertEqual(repr(sysd), ref322.format(dt=", 0.4")) - array = np.array + def test_repr(self, sys322): + """Test string representation""" + ref322 = "\n".join(["StateSpace(array([[-3., 4., 2.],", + " [-1., -3., 0.],", + " [ 2., 5., 3.]]), array([[ 1., 4.],", + " [-3., -3.],", + " [-2., 1.]]), array([[ 4., 2., -3.],", + " [ 1., 4., 3.]]), array([[-2., 4.],", + " [ 0., 1.]]){dt})"]) + assert repr(sys322) == ref322.format(dt='') + sysd = StateSpace(sys322.A, sys322.B, + sys322.C, sys322.D, 0.4) + assert repr(sysd), ref322.format(dt=" == 0.4") + array = np.array # noqa sysd2 = eval(repr(sysd)) np.testing.assert_allclose(sysd.A, sysd2.A) np.testing.assert_allclose(sysd.B, sysd2.B) np.testing.assert_allclose(sysd.C, sysd2.C) np.testing.assert_allclose(sysd.D, sysd2.D) - def test_str(self): + def test_str(self, sys322): """Test that printing the system works""" - tsys = self.sys322 + tsys = sys322 tref = ("A = [[-3. 4. 2.]\n" " [-1. -3. 0.]\n" " [ 2. 5. 3.]]\n" @@ -561,117 +730,78 @@ def test_str(self): sysdt1 = StateSpace(tsys.A, tsys.B, tsys.C, tsys.D, 1.) assert str(sysdt1) == tref + "\ndt = 1.0\n" + def test_pole_static(self): + """Regression: pole() of static gain is empty array.""" + np.testing.assert_array_equal(np.array([]), + StateSpace([], [], [], [[1]]).pole()) + + def test_horner(self, sys322): + """Test horner() function""" + # Make sure we can compute the transfer function at a complex value + sys322.horner(1. + 1.j) -class TestRss(unittest.TestCase): + # Make sure result agrees with frequency response + mag, phase, omega = sys322.freqresp([1]) + np.testing.assert_array_almost_equal( + sys322.horner(1.j), + mag[:, :, 0] * np.exp(1.j * phase[:, :, 0])) + +class TestRss: """These are tests for the proper functionality of statesp.rss.""" - def setUp(self): - # Number of times to run each of the randomized tests. - self.numTests = 100 - # Maxmimum number of states to test + 1 - self.maxStates = 10 - # Maximum number of inputs and outputs to test + 1 - self.maxIO = 5 + # Maxmimum number of states to test + 1 + maxStates = 10 + # Maximum number of inputs and outputs to test + 1 + maxIO = 5 - def test_shape(self): + @pytest.mark.parametrize('states', range(1, maxStates)) + @pytest.mark.parametrize('outputs', range(1, maxIO)) + @pytest.mark.parametrize('inputs', range(1, maxIO)) + def test_shape(self, states, outputs, inputs): """Test that rss outputs have the right state, input, and output size.""" - - for states in range(1, self.maxStates): - for inputs in range(1, self.maxIO): - for outputs in range(1, self.maxIO): - sys = matlab.rss(states, outputs, inputs) - self.assertEqual(sys.states, states) - self.assertEqual(sys.inputs, inputs) - self.assertEqual(sys.outputs, outputs) - - def test_pole(self): + sys = rss(states, outputs, inputs) + assert sys.states == states + assert sys.inputs == inputs + assert sys.outputs == outputs + + @pytest.mark.parametrize('states', range(1, maxStates)) + @pytest.mark.parametrize('outputs', range(1, maxIO)) + @pytest.mark.parametrize('inputs', range(1, maxIO)) + def test_pole(self, states, outputs, inputs): """Test that the poles of rss outputs have a negative real part.""" - - for states in range(1, self.maxStates): - for inputs in range(1, self.maxIO): - for outputs in range(1, self.maxIO): - sys = matlab.rss(states, outputs, inputs) - p = sys.pole() - for z in p: - self.assertTrue(z.real < 0) + sys = rss(states, outputs, inputs) + p = sys.pole() + for z in p: + assert z.real < 0 -class TestDrss(unittest.TestCase): +class TestDrss: """These are tests for the proper functionality of statesp.drss.""" - def setUp(self): - # Number of times to run each of the randomized tests. - self.numTests = 100 - # Maximum number of states to test + 1 - self.maxStates = 10 - # Maximum number of inputs and outputs to test + 1 - self.maxIO = 5 + # Maximum number of states to test + 1 + maxStates = 10 + # Maximum number of inputs and outputs to test + 1 + maxIO = 5 - def test_shape(self): + @pytest.mark.parametrize('states', range(1, maxStates)) + @pytest.mark.parametrize('outputs', range(1, maxIO)) + @pytest.mark.parametrize('inputs', range(1, maxIO)) + def test_shape(self, states, outputs, inputs): """Test that drss outputs have the right state, input, and output size.""" - - for states in range(1, self.maxStates): - for inputs in range(1, self.maxIO): - for outputs in range(1, self.maxIO): - sys = matlab.drss(states, outputs, inputs) - self.assertEqual(sys.states, states) - self.assertEqual(sys.inputs, inputs) - self.assertEqual(sys.outputs, outputs) - - def test_pole(self): + sys = drss(states, outputs, inputs) + assert sys.states == states + assert sys.inputs == inputs + assert sys.outputs == outputs + + @pytest.mark.parametrize('states', range(1, maxStates)) + @pytest.mark.parametrize('outputs', range(1, maxIO)) + @pytest.mark.parametrize('inputs', range(1, maxIO)) + def test_pole(self, states, outputs, inputs): """Test that the poles of drss outputs have less than unit magnitude.""" - - for states in range(1, self.maxStates): - for inputs in range(1, self.maxIO): - for outputs in range(1, self.maxIO): - sys = matlab.drss(states, outputs, inputs) - p = sys.pole() - for z in p: - self.assertTrue(abs(z) < 1) - - def test_pole_static(self): - """Regression: pole() of static gain is empty array.""" - np.testing.assert_array_equal(np.array([]), - StateSpace([], [], [], [[1]]).pole()) - - def test_copy_constructor(self): - # Create a set of matrices for a simple linear system - A = np.array([[-1]]) - B = np.array([[1]]) - C = np.array([[1]]) - D = np.array([[0]]) - - # Create the first linear system and a copy - linsys = StateSpace(A, B, C, D) - cpysys = StateSpace(linsys) - - # Change the original A matrix - A[0, 0] = -2 - np.testing.assert_array_equal(linsys.A, [[-1]]) # original value - np.testing.assert_array_equal(cpysys.A, [[-1]]) # original value - - # Change the A matrix for the original system - linsys.A[0, 0] = -3 - np.testing.assert_array_equal(cpysys.A, [[-1]]) # original value - - def test_sample_system_prewarping(self): - """test that prewarping works when converting from cont to discrete time system""" - A = np.array([ - [ 0.00000000e+00, 1.00000000e+00, 0.00000000e+00, 0.00000000e+00], - [-3.81097561e+01, -1.12500000e+00, 0.00000000e+00, 0.00000000e+00], - [ 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 1.00000000e+00], - [ 0.00000000e+00, 0.00000000e+00, -1.66356135e+04, -1.34748470e+01]]) - B = np.array([ - [ 0. ], [ 38.1097561 ],[ 0. ],[16635.61352143]]) - C = np.array([[0.90909091, 0. , 0.09090909, 0. ],]) - wwarp = 50 - Ts = 0.025 - plant = StateSpace(A,B,C,0) - plant_d_warped = plant.sample(Ts, 'bilinear', prewarp_frequency=wwarp) - np.testing.assert_array_almost_equal( - evalfr(plant, wwarp*1j), - evalfr(plant_d_warped, np.exp(wwarp*1j*Ts)), - decimal=4) + sys = drss(states, outputs, inputs) + p = sys.pole() + for z in p: + assert abs(z) < 1 class TestLTIConverter: @@ -724,6 +854,3 @@ def test_returnScipySignalLTI_error(self, mimoss): with pytest.raises(ValueError): mimoss.returnScipySignalLTI(strict=True) - -if __name__ == "__main__": - unittest.main() From 1dd2525a8ae570835fde0f7f2ecbec0fecd012d6 Mon Sep 17 00:00:00 2001 From: bnavigator Date: Tue, 29 Dec 2020 06:05:19 +0100 Subject: [PATCH 27/30] pytestify timeresp_test.py --- control/tests/timeresp_test.py | 1128 ++++++++++++++++---------------- 1 file changed, 576 insertions(+), 552 deletions(-) diff --git a/control/tests/timeresp_test.py b/control/tests/timeresp_test.py index 8020d8078..6977973ff 100644 --- a/control/tests/timeresp_test.py +++ b/control/tests/timeresp_test.py @@ -1,121 +1,273 @@ -#!/usr/bin/env python -# -# timeresp_test.py - test time response functions -# RMM, 17 Jun 2011 (based on TestMatlab from v0.4c) -# -# This test suite just goes through and calls all of the MATLAB -# functions using different systems and arguments to make sure that -# nothing crashes. It doesn't test actual functionality; the module -# specific unit tests will do that. - -import unittest +"""timeresp_test.py - test time response functions + +RMM, 17 Jun 2011 (based on TestMatlab from v0.4c) + +This test suite just goes through and calls all of the MATLAB +functions using different systems and arguments to make sure that +nothing crashes. It doesn't test actual functionality; the module +specific unit tests will do that. +""" + +from copy import copy from distutils.version import StrictVersion import numpy as np import pytest import scipy as sp -from control.timeresp import * -from control.timeresp import _ideal_tfinal_and_dt, _default_time_vector -from control.statesp import * -from control.xferfcn import TransferFunction, _convert_to_transfer_function -from control.dtime import c2d -from control.exception import slycot_check - -class TestTimeresp(unittest.TestCase): - def setUp(self): - """Set up some systems for testing out MATLAB functions""" - A = np.matrix("1. -2.; 3. -4.") - B = np.matrix("5.; 7.") - C = np.matrix("6. 8.") - D = np.matrix("9.") - self.siso_ss1 = StateSpace(A, B, C, D) - # Create some transfer functions - self.siso_tf1 = TransferFunction([1], [1, 2, 1]) - self.siso_tf2 = _convert_to_transfer_function(self.siso_ss1) +from control import (StateSpace, TransferFunction, c2d, isctime, isdtime, + ss2tf, tf2ss) +from control.timeresp import (_default_time_vector, _ideal_tfinal_and_dt, + forced_response, impulse_response, + initial_response, step_info, step_response) +from control.tests.conftest import slycotonly - # tests for pole cancellation - self.pole_cancellation = TransferFunction([1.067e+05, 5.791e+04], - [10.67, 1.067e+05, 5.791e+04]) - self.no_pole_cancellation = TransferFunction([1.881e+06], - [188.1, 1.881e+06]) - # Create MIMO system, contains ``siso_ss1`` twice - A = np.matrix("1. -2. 0. 0.;" - "3. -4. 0. 0.;" - "0. 0. 1. -2.;" - "0. 0. 3. -4. ") - B = np.matrix("5. 0.;" - "7. 0.;" - "0. 5.;" - "0. 7. ") - C = np.matrix("6. 8. 0. 0.;" - "0. 0. 6. 8. ") - D = np.matrix("9. 0.;" - "0. 9. ") - self.mimo_ss1 = StateSpace(A, B, C, D) - - # Create discrete time systems - self.siso_dtf1 = TransferFunction([1], [1, 1, 0.25], True) - self.siso_dtf2 = TransferFunction([1], [1, 1, 0.25], 0.2) - self.siso_dss1 = tf2ss(self.siso_dtf1) - self.siso_dss2 = tf2ss(self.siso_dtf2) - self.mimo_dss1 = StateSpace(A, B, C, D, True) - self.mimo_dss2 = c2d(self.mimo_ss1, 0.2) - - def test_step_response(self): - # Test SISO system - sys = self.siso_ss1 - t = np.linspace(0, 1, 10) - youttrue = np.array([9., 17.6457, 24.7072, 30.4855, 35.2234, 39.1165, - 42.3227, 44.9694, 47.1599, 48.9776]) +class TSys: + """Struct of test system""" + def __init__(self, sys=None): + self.sys = sys - # SISO call - tout, yout = step_response(sys, T=t) - np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) - np.testing.assert_array_almost_equal(tout, t) + def __repr__(self): + """Show system when debugging""" + return self.sys.__repr__() - # Play with arguments - tout, yout = step_response(sys, T=t, X0=0) - np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) - np.testing.assert_array_almost_equal(tout, t) - X0 = np.array([0, 0]) - tout, yout = step_response(sys, T=t, X0=X0) - np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) - np.testing.assert_array_almost_equal(tout, t) +class TestTimeresp: + + @pytest.fixture + def siso_ss1(self): + + A = np.array([[1., -2.], [3., -4.]]) + B = np.array([[5.], [7.]]) + C = np.array([[6., 8.]]) + D = np.array([[9.]]) + T = TSys(StateSpace(A, B, C, D, 0)) + + T.t = np.linspace(0, 1, 10) + T.ystep = np.array([9., 17.6457, 24.7072, 30.4855, 35.2234, + 39.1165, 42.3227, 44.9694, 47.1599, 48.9776]) + + T.yinitial = np.array([11., 8.1494, 5.9361, 4.2258, 2.9118, + 1.9092, 1.1508, 0.5833, 0.1645, -0.1391]) + + return T + + @pytest.fixture + def siso_ss2(self, siso_ss1): + """System siso_ss2 with D=0""" + ss1 = siso_ss1.sys + T = TSys(StateSpace(ss1.A, ss1.B, ss1.C, 0, 0)) + T.t = siso_ss1.t + T.ystep = siso_ss1.ystep - 9 + T.initial = siso_ss1.yinitial - 9 + T.yimpulse = np.array([86., 70.1808, 57.3753, 46.9975, 38.5766, + 31.7344, 26.1668, 21.6292, 17.9245, 14.8945]) + return T + - tout, yout, xout = step_response(sys, T=t, X0=0, return_x=True) - np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) + @pytest.fixture + def siso_tf1(self): + # Create some transfer functions + return TSys(TransferFunction([1], [1, 2, 1], 0)) + + @pytest.fixture + def siso_tf2(self, siso_ss1): + T = copy(siso_ss1) + T.sys = ss2tf(siso_ss1.sys) + return T + + @pytest.fixture + def mimo_ss1(self, siso_ss1): + # Create MIMO system, contains ``siso_ss1`` twice + A = np.zeros((4, 4)) + A[:2, :2] = siso_ss1.sys.A + A[2:, 2:] = siso_ss1.sys.A + B = np.zeros((4, 2)) + B[:2, :1] = siso_ss1.sys.B + B[2:, 1:] = siso_ss1.sys.B + C = np.zeros((2, 4)) + C[:1, :2] = siso_ss1.sys.C + C[1:, 2:] = siso_ss1.sys.C + D = np.zeros((2, 2)) + D[:1, :1] = siso_ss1.sys.D + D[1:, 1:] = siso_ss1.sys.D + T = copy(siso_ss1) + T.sys = StateSpace(A, B, C, D) + return T + + @pytest.fixture + def mimo_ss2(self, siso_ss2): + # Create MIMO system, contains ``siso_ss2`` twice + A = np.zeros((4, 4)) + A[:2, :2] = siso_ss2.sys.A + A[2:, 2:] = siso_ss2.sys.A + B = np.zeros((4, 2)) + B[:2, :1] = siso_ss2.sys.B + B[2:, 1:] = siso_ss2.sys.B + C = np.zeros((2, 4)) + C[:1, :2] = siso_ss2.sys.C + C[1:, 2:] = siso_ss2.sys.C + D = np.zeros((2, 2)) + T = copy(siso_ss2) + T.sys = StateSpace(A, B, C, D, 0) + return T + + # Create discrete time systems + + @pytest.fixture + def siso_dtf0(self): + T = TSys(TransferFunction([1.], [1., 0.], 1.)) + T.t = np.arange(4) + T.yimpulse = [0., 1., 0., 0.] + return T + + @pytest.fixture + def siso_dtf1(self): + T = TSys(TransferFunction([1], [1, 1, 0.25], True)) + T.t = np.arange(0, 5, 1) + return T + + @pytest.fixture + def siso_dtf2(self): + T = TSys(TransferFunction([1], [1, 1, 0.25], 0.2)) + T.t = np.arange(0, 5, 0.2) + return T + + @pytest.fixture + def siso_dss1(self, siso_dtf1): + T = copy(siso_dtf1) + T.sys = tf2ss(siso_dtf1.sys) + return T + + @pytest.fixture + def siso_dss2(self, siso_dtf2): + T = copy(siso_dtf2) + T.sys = tf2ss(siso_dtf2.sys) + return T + + @pytest.fixture + def mimo_dss1(self, mimo_ss1): + ss1 = mimo_ss1.sys + T = TSys( + StateSpace(ss1.A, ss1.B, ss1.C, ss1.D, True)) + T.t = np.arange(0, 5, 0.2) + return T + + @pytest.fixture + def mimo_dss2(self, mimo_ss1): + T = copy(mimo_ss1) + T.sys = c2d(mimo_ss1.sys, T.t[1]-T.t[0]) + return T + + @pytest.fixture + def mimo_tf2(self, siso_ss2, mimo_ss2): + T = copy(mimo_ss2) + # construct from siso to avoid slycot during fixture setup + tf_ = ss2tf(siso_ss2.sys) + T.sys = TransferFunction([[tf_.num[0][0], [0]], [[0], tf_.num[0][0]]], + [[tf_.den[0][0], [1]], [[1], tf_.den[0][0]]], + 0) + return T + + @pytest.fixture + def mimo_dtf1(self, siso_dtf1): + T = copy(siso_dtf1) + # construct from siso to avoid slycot during fixture setup + tf_ = siso_dtf1.sys + T.sys = TransferFunction([[tf_.num[0][0], [0]], [[0], tf_.num[0][0]]], + [[tf_.den[0][0], [1]], [[1], tf_.den[0][0]]], + True) + return T + + @pytest.fixture + def pole_cancellation(self): + # for pole cancellation tests + return TransferFunction([1.067e+05, 5.791e+04], + [10.67, 1.067e+05, 5.791e+04]) + + @pytest.fixture + def no_pole_cancellation(self): + return TransferFunction([1.881e+06], + [188.1, 1.881e+06]) + + @pytest.fixture + def tsystem(self, + request, + siso_ss1, siso_ss2, siso_tf1, siso_tf2, + mimo_ss1, mimo_ss2, mimo_tf2, + siso_dtf0, siso_dtf1, siso_dtf2, + siso_dss1, siso_dss2, + mimo_dss1, mimo_dss2, mimo_dtf1, + pole_cancellation, no_pole_cancellation): + systems = {"siso_ss1": siso_ss1, + "siso_ss2": siso_ss2, + "siso_tf1": siso_tf1, + "siso_tf2": siso_tf2, + "mimo_ss1": mimo_ss1, + "mimo_ss2": mimo_ss2, + "mimo_tf2": mimo_tf2, + "siso_dtf0": siso_dtf0, + "siso_dtf1": siso_dtf1, + "siso_dtf2": siso_dtf2, + "siso_dss1": siso_dss1, + "siso_dss2": siso_dss2, + "mimo_dss1": mimo_dss1, + "mimo_dss2": mimo_dss2, + "mimo_dtf1": mimo_dtf1, + "pole_cancellation": pole_cancellation, + "no_pole_cancellation": no_pole_cancellation, + } + return systems[request.param] + + @pytest.mark.parametrize( + "kwargs", + [{}, + {'X0': 0}, + {'X0': np.array([0, 0])}, + {'X0': 0, 'return_x': True}, + ]) + def test_step_response_siso(self, siso_ss1, kwargs): + """Test SISO system step response""" + sys = siso_ss1.sys + t = siso_ss1.t + yref = siso_ss1.ystep + # SISO call + out = step_response(sys, T=t, **kwargs) + tout, yout = out[:2] + assert len(out) == 3 if ('return_x', True) in kwargs.items() else 2 np.testing.assert_array_almost_equal(tout, t) + np.testing.assert_array_almost_equal(yout, yref, decimal=4) - # Test MIMO system, which contains ``siso_ss1`` twice - sys = self.mimo_ss1 + def test_step_response_mimo(self, mimo_ss1): + """Test MIMO system, which contains ``siso_ss1`` twice""" + sys = mimo_ss1.sys + t = mimo_ss1.t + yref = mimo_ss1.ystep _t, y_00 = step_response(sys, T=t, input=0, output=0) _t, y_11 = step_response(sys, T=t, input=1, output=1) - np.testing.assert_array_almost_equal(y_00, youttrue, decimal=4) - np.testing.assert_array_almost_equal(y_11, youttrue, decimal=4) - - # Make sure continuous and discrete time use same return conventions - sysc = self.mimo_ss1 - sysd = c2d(sysc, 1) # discrete time system - Tvec = np.linspace(0, 10, 11) # make sure to use integer times 0..10 + np.testing.assert_array_almost_equal(y_00, yref, decimal=4) + np.testing.assert_array_almost_equal(y_11, yref, decimal=4) + + def test_step_response_return(self, mimo_ss1): + """Verify continuous and discrete time use same return conventions""" + sysc = mimo_ss1.sys + sysd = c2d(sysc, 1) # discrete time system + Tvec = np.linspace(0, 10, 11) # make sure to use integer times 0..10 Tc, youtc = step_response(sysc, Tvec, input=0) Td, youtd = step_response(sysd, Tvec, input=0) np.testing.assert_array_equal(Tc.shape, Td.shape) np.testing.assert_array_equal(youtc.shape, youtd.shape) - # Recreate issue #374 ("Bug in step_response()") - def test_step_nostates(self): - # Continuous time, constant system - sys = TransferFunction([1], [1]) - t, y = step_response(sys) - np.testing.assert_array_equal(y, np.ones(len(t))) + @pytest.mark.parametrize("dt", [0, 1], ids=["continuous", "discrete"]) + def test_step_nostates(self, dt): + """Constant system, continuous and discrete time - # Discrete time, constant system - sys = TransferFunction([1], [1], 1) + gh-374 "Bug in step_response()" + """ + sys = TransferFunction([1], [1], dt) t, y = step_response(sys) np.testing.assert_array_equal(y, np.ones(len(t))) @@ -130,549 +282,421 @@ def test_step_info(self): 'Overshoot': 7.4915, 'Undershoot': 0, 'Peak': 2.6873, - 'PeakTime': 8.0530 + 'PeakTime': 8.0530, + 'SteadyStateValue': 2.50 } S = step_info(sys) + Sk = sorted(S.keys()) + Sktrue = sorted(Strue.keys()) + assert Sk == Sktrue # Very arbitrary tolerance because I don't know if the # response from the MATLAB is really that accurate. # maybe it is a good idea to change the Strue to match # but I didn't do it because I don't know if it is # accurate either... rtol = 2e-2 - np.testing.assert_allclose( - S.get('RiseTime'), - Strue.get('RiseTime'), - rtol=rtol) - np.testing.assert_allclose( - S.get('SettlingTime'), - Strue.get('SettlingTime'), - rtol=rtol) - np.testing.assert_allclose( - S.get('SettlingMin'), - Strue.get('SettlingMin'), - rtol=rtol) - np.testing.assert_allclose( - S.get('SettlingMax'), - Strue.get('SettlingMax'), - rtol=rtol) - np.testing.assert_allclose( - S.get('Overshoot'), - Strue.get('Overshoot'), - rtol=rtol) - np.testing.assert_allclose( - S.get('Undershoot'), - Strue.get('Undershoot'), - rtol=rtol) - np.testing.assert_allclose( - S.get('Peak'), - Strue.get('Peak'), - rtol=rtol) - np.testing.assert_allclose( - S.get('PeakTime'), - Strue.get('PeakTime'), - rtol=rtol) - np.testing.assert_allclose( - S.get('SteadyStateValue'), - 2.50, - rtol=rtol) + np.testing.assert_allclose([S[k] for k in Sk], + [Strue[k] for k in Sktrue], + rtol=rtol) + def test_step_pole_cancellation(self, pole_cancellation, + no_pole_cancellation): # confirm that pole-zero cancellation doesn't perturb results # https://github.com/python-control/python-control/issues/440 - step_info_no_cancellation = step_info(self.no_pole_cancellation) - step_info_cancellation = step_info(self.pole_cancellation) + step_info_no_cancellation = step_info(no_pole_cancellation) + step_info_cancellation = step_info(pole_cancellation) for key in step_info_no_cancellation: np.testing.assert_allclose(step_info_no_cancellation[key], step_info_cancellation[key], rtol=1e-4) - def test_impulse_response(self): - # Test SISO system - sys = self.siso_ss1 - t = np.linspace(0, 1, 10) - youttrue = np.array([86., 70.1808, 57.3753, 46.9975, 38.5766, 31.7344, - 26.1668, 21.6292, 17.9245, 14.8945]) - tout, yout = impulse_response(sys, T=t) - np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) - np.testing.assert_array_almost_equal(tout, t) - - # Play with arguments - tout, yout = impulse_response(sys, T=t, X0=0) - np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) - np.testing.assert_array_almost_equal(tout, t) - - X0 = np.array([0, 0]) - tout, yout = impulse_response(sys, T=t, X0=X0) - np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) + @pytest.mark.parametrize( + "tsystem, kwargs", + [("siso_ss2", {}), + ("siso_ss2", {'X0': 0}), + ("siso_ss2", {'X0': np.array([0, 0])}), + ("siso_ss2", {'X0': 0, 'return_x': True}), + ("siso_dtf0", {})], + indirect=["tsystem"]) + def test_impulse_response_siso(self, tsystem, kwargs): + """Test impulse response of SISO systems""" + sys = tsystem.sys + t = tsystem.t + yref = tsystem.yimpulse + + out = impulse_response(sys, T=t, **kwargs) + tout, yout = out[:2] + assert len(out) == 3 if ('return_x', True) in kwargs.items() else 2 np.testing.assert_array_almost_equal(tout, t) + np.testing.assert_array_almost_equal(yout, yref, decimal=4) - tout, yout, xout = impulse_response(sys, T=t, X0=0, return_x=True) - np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) - np.testing.assert_array_almost_equal(tout, t) + def test_impulse_response_mimo(self, mimo_ss2): + """"Test impulse response of MIMO systems""" + sys = mimo_ss2.sys + t = mimo_ss2.t - # Test MIMO system, which contains ``siso_ss1`` twice - sys = self.mimo_ss1 + yref = mimo_ss2.yimpulse _t, y_00 = impulse_response(sys, T=t, input=0, output=0) + np.testing.assert_array_almost_equal(y_00, yref, decimal=4) _t, y_11 = impulse_response(sys, T=t, input=1, output=1) - np.testing.assert_array_almost_equal(y_00, youttrue, decimal=4) - np.testing.assert_array_almost_equal(y_11, youttrue, decimal=4) + np.testing.assert_array_almost_equal(y_11, yref, decimal=4) - # Test MIMO system, as mimo, and don't trim outputs - sys = self.mimo_ss1 + yref_notrim = np.zeros((2, len(t))) + yref_notrim[:1, :] = yref _t, yy = impulse_response(sys, T=t, input=0) - np.testing.assert_array_almost_equal( - yy, np.vstack((youttrue, np.zeros_like(youttrue))), decimal=4) + np.testing.assert_array_almost_equal(yy, yref_notrim, decimal=4) - @pytest.mark.skipif(StrictVersion(sp.__version__) < "1.3.0", - reason="requires SciPy 1.3.0 or greater") - def test_discrete_time_impulse(self): + @pytest.mark.skipif(StrictVersion(sp.__version__) < "1.3", + reason="requires SciPy 1.3 or greater") + def test_discrete_time_impulse(self, siso_tf1): # discrete time impulse sampled version should match cont time dt = 0.1 t = np.arange(0, 3, dt) - sys = self.siso_tf1 + sys = siso_tf1.sys sysdt = sys.sample(dt, 'impulse') np.testing.assert_array_almost_equal(impulse_response(sys, t)[1], impulse_response(sysdt, t)[1]) - def test_initial_response(self): - # Test SISO system - sys = self.siso_ss1 - t = np.linspace(0, 1, 10) - x0 = np.array([[0.5], [1]]) - youttrue = np.array([11., 8.1494, 5.9361, 4.2258, 2.9118, 1.9092, - 1.1508, 0.5833, 0.1645, -0.1391]) - tout, yout = initial_response(sys, T=t, X0=x0) - np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) + def test_impulse_response_warnD(self, siso_ss1): + """Test warning about direct feedthrough""" + with pytest.warns(UserWarning, match="System has direct feedthrough"): + _ = impulse_response(siso_ss1.sys, siso_ss1.t) + + @pytest.mark.parametrize( + "kwargs", + [{}, + {'X0': 0}, + {'X0': np.array([0.5, 1])}, + {'X0': np.array([[0.5], [1]])}, + {'X0': np.array([0.5, 1]), 'return_x': True}, + ]) + def test_initial_response(self, siso_ss1, kwargs): + """Test initial response of SISO system""" + sys = siso_ss1.sys + t = siso_ss1.t + x0 = kwargs.get('X0', 0) + yref = siso_ss1.yinitial if np.any(x0) else np.zeros_like(t) + + out = initial_response(sys, T=t, **kwargs) + tout, yout = out[:2] + assert len(out) == 3 if ('return_x', True) in kwargs.items() else 2 np.testing.assert_array_almost_equal(tout, t) + np.testing.assert_array_almost_equal(yout, yref, decimal=4) - # Play with arguments - tout, yout, xout = initial_response(sys, T=t, X0=x0, return_x=True) - np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) - np.testing.assert_array_almost_equal(tout, t) + def test_initial_response_mimo(self, mimo_ss1): + """Test initial response of MIMO system""" + sys = mimo_ss1.sys + t = mimo_ss1.t + x0 = np.array([[.5], [1.], [.5], [1.]]) + yref = mimo_ss1.yinitial + yref_notrim = np.broadcast_to(yref, (2, len(t))) - # Test MIMO system, which contains ``siso_ss1`` twice - sys = self.mimo_ss1 - x0 = np.matrix(".5; 1.; .5; 1.") _t, y_00 = initial_response(sys, T=t, X0=x0, input=0, output=0) - _t, y_11 = initial_response(sys, T=t, X0=x0, input=1, output=1) - np.testing.assert_array_almost_equal(y_00, youttrue, decimal=4) - np.testing.assert_array_almost_equal(y_11, youttrue, decimal=4) - - def test_initial_response_no_trim(self): - # test MIMO system without trimming - t = np.linspace(0, 1, 10) - x0 = np.matrix(".5; 1.; .5; 1.") - youttrue = np.array([11., 8.1494, 5.9361, 4.2258, 2.9118, 1.9092, - 1.1508, 0.5833, 0.1645, -0.1391]) - sys = self.mimo_ss1 + np.testing.assert_array_almost_equal(y_00, yref, decimal=4) + _t, y_11 = initial_response(sys, T=t, X0=x0, input=0, output=1) + np.testing.assert_array_almost_equal(y_11, yref, decimal=4) _t, yy = initial_response(sys, T=t, X0=x0) - np.testing.assert_array_almost_equal( - yy, np.vstack((youttrue, youttrue)), - decimal=4) - - def test_forced_response(self): - t = np.linspace(0, 1, 10) - - # compute step response - test with state space, and transfer function - # objects - u = np.array([1., 1, 1, 1, 1, 1, 1, 1, 1, 1]) - youttrue = np.array([9., 17.6457, 24.7072, 30.4855, 35.2234, 39.1165, - 42.3227, 44.9694, 47.1599, 48.9776]) - tout, yout, _xout = forced_response(self.siso_ss1, t, u) - np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) + np.testing.assert_array_almost_equal(yy, yref_notrim, decimal=4) + + @pytest.mark.parametrize("tsystem", + ["siso_ss1", "siso_tf2"], + indirect=True) + def test_forced_response_step(self, tsystem): + """Test forced response of SISO systems as step response""" + sys = tsystem.sys + t = tsystem.t + u = np.ones_like(t, dtype=np.float) + yref = tsystem.ystep + + tout, yout, _xout = forced_response(sys, t, u) + np.testing.assert_array_almost_equal(tout, t) + np.testing.assert_array_almost_equal(yout, yref, decimal=4) + + @pytest.mark.parametrize("u", + [np.zeros((10,), dtype=np.float), + 0] # special algorithm + ) + def test_forced_response_initial(self, siso_ss1, u): + """Test forced response of SISO system as intitial response""" + sys = siso_ss1.sys + t = siso_ss1.t + x0 = np.array([[.5], [1.]]) + yref = siso_ss1.yinitial + + tout, yout, _xout = forced_response(sys, t, u, X0=x0) np.testing.assert_array_almost_equal(tout, t) - _t, yout, _xout = forced_response(self.siso_tf2, t, u) - np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) - - # test with initial value and special algorithm for ``U=0`` - u = 0 - x0 = np.matrix(".5; 1.") - youttrue = np.array([11., 8.1494, 5.9361, 4.2258, 2.9118, 1.9092, - 1.1508, 0.5833, 0.1645, -0.1391]) - _t, yout, _xout = forced_response(self.siso_ss1, t, u, x0) - np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) - - # Test MIMO system, which contains ``siso_ss1`` twice + np.testing.assert_array_almost_equal(yout, yref, decimal=4) + + @pytest.mark.parametrize("tsystem, useT", + [("mimo_ss1", True), + ("mimo_dss2", True), + ("mimo_dss2", False)], + indirect=["tsystem"]) + def test_forced_response_mimo(self, tsystem, useT): + """Test forced response of MIMO system""" # first system: initial value, second system: step response + sys = tsystem.sys + t = tsystem.t u = np.array([[0., 0, 0, 0, 0, 0, 0, 0, 0, 0], [1., 1, 1, 1, 1, 1, 1, 1, 1, 1]]) x0 = np.array([[.5], [1], [0], [0]]) - youttrue = np.array([[11., 8.1494, 5.9361, 4.2258, 2.9118, 1.9092, - 1.1508, 0.5833, 0.1645, -0.1391], - [9., 17.6457, 24.7072, 30.4855, 35.2234, 39.1165, - 42.3227, 44.9694, 47.1599, 48.9776]]) - _t, yout, _xout = forced_response(self.mimo_ss1, t, u, x0) - np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) - - # Test discrete MIMO system to use correct convention for input - sysc = self.mimo_ss1 - dt=t[1]-t[0] - sysd = c2d(sysc, dt) # discrete time system - Tc, youtc, _xoutc = forced_response(sysc, t, u, x0) - Td, youtd, _xoutd = forced_response(sysd, t, u, x0) - np.testing.assert_array_equal(Tc.shape, Td.shape) - np.testing.assert_array_equal(youtc.shape, youtd.shape) - np.testing.assert_array_almost_equal(youtc, youtd, decimal=4) - - # Test discrete MIMO system without default T argument - u = np.array([[0., 0, 0, 0, 0, 0, 0, 0, 0, 0], - [1., 1, 1, 1, 1, 1, 1, 1, 1, 1]]) - x0 = np.array([[.5], [1], [0], [0]]) - youttrue = np.array([[11., 8.1494, 5.9361, 4.2258, 2.9118, 1.9092, - 1.1508, 0.5833, 0.1645, -0.1391], - [9., 17.6457, 24.7072, 30.4855, 35.2234, 39.1165, - 42.3227, 44.9694, 47.1599, 48.9776]]) - _t, yout, _xout = forced_response(sysd, U=u, X0=x0) - np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) - - def test_lsim_double_integrator(self): + yref = np.vstack([tsystem.yinitial, tsystem.ystep]) + + if useT: + _t, yout, _xout = forced_response(sys, t, u, x0) + else: + _t, yout, _xout = forced_response(sys, U=u, X0=x0) + np.testing.assert_array_almost_equal(yout, yref, decimal=4) + + @pytest.mark.parametrize("u, x0, xtrue", + [(np.zeros((10,)), + np.array([2., 3.]), + np.vstack([np.linspace(2, 5, 10), + np.full((10,), 3)])), + (np.ones((10,)), + np.array([0., 0.]), + np.vstack([0.5 * np.linspace(0, 1, 10)**2, + np.linspace(0, 1, 10)])), + (np.linspace(0, 1, 10), + np.array([0., 0.]), + np.vstack([np.linspace(0, 1, 10)**3 / 6., + np.linspace(0, 1, 10)**2 / 2.]))], + ids=["zeros", "ones", "linear"]) + def test_lsim_double_integrator(self, u, x0, xtrue): + """Test forced response of double integrator""" # Note: scipy.signal.lsim fails if A is not invertible - A = np.mat("0. 1.;0. 0.") - B = np.mat("0.; 1.") - C = np.mat("1. 0.") + A = np.array([[0., 1.], + [0., 0.]]) + B = np.array([[0.], + [1.]]) + C = np.array([[1., 0.]]) D = 0. sys = StateSpace(A, B, C, D) + t = np.linspace(0, 1, 10) + + _t, yout, xout = forced_response(sys, t, u, x0) + np.testing.assert_array_almost_equal(xout, xtrue, decimal=6) + ytrue = np.squeeze(np.asarray(C.dot(xtrue))) + np.testing.assert_array_almost_equal(yout, ytrue, decimal=6) - def check(u, x0, xtrue): - _t, yout, xout = forced_response(sys, t, u, x0) - np.testing.assert_array_almost_equal(xout, xtrue, decimal=6) - ytrue = np.squeeze(np.asarray(C.dot(xtrue))) - np.testing.assert_array_almost_equal(yout, ytrue, decimal=6) - - # test with zero input - npts = 10 - t = np.linspace(0, 1, npts) - u = np.zeros_like(t) - x0 = np.array([2., 3.]) - xtrue = np.zeros((2, npts)) - xtrue[0, :] = x0[0] + t * x0[1] - xtrue[1, :] = x0[1] - check(u, x0, xtrue) - - # test with step input - u = np.ones_like(t) - xtrue = np.array([0.5 * t**2, t]) - x0 = np.array([0., 0.]) - check(u, x0, xtrue) - - # test with linear input - u = t - xtrue = np.array([1./6. * t**3, 0.5 * t**2]) - check(u, x0, xtrue) - - def test_discrete_initial(self): - h1 = TransferFunction([1.], [1., 0.], 1.) - t, yout = impulse_response(h1, np.arange(4)) - np.testing.assert_array_equal(yout, [0., 1., 0., 0.]) - - @unittest.skipIf(not slycot_check(), "slycot not installed") + + @slycotonly def test_step_robustness(self): - "Unit test: https://github.com/python-control/python-control/issues/240" + "Test robustness os step_response against denomiantors: gh-240" # Create 2 input, 2 output system - num = [ [[0], [1]], [[1], [0]] ] + num = [[[0], [1]], [[1], [0]]] - den1 = [ [[1], [1,1]], [[1,4], [1]] ] + den1 = [[[1], [1,1]], [[1, 4], [1]]] sys1 = TransferFunction(num, den1) - den2 = [ [[1], [1e-10, 1, 1]], [[1,4], [1]] ] # slight perturbation + den2 = [[[1], [1e-10, 1, 1]], [[1, 4], [1]]] # slight perturbation sys2 = TransferFunction(num, den2) - # Compute step response from input 1 to output 1, 2 t1, y1 = step_response(sys1, input=0, T=2, T_num=100) t2, y2 = step_response(sys2, input=0, T=2, T_num=100) np.testing.assert_array_almost_equal(y1, y2) - def test_auto_generated_time_vector(self): - # confirm a TF with a pole at p simulates for ratio/p seconds - p = 0.5 - ratio = 9.21034*p # taken from code - ratio2 = 25*p - np.testing.assert_array_almost_equal( - _ideal_tfinal_and_dt(TransferFunction(1, [1, .5]))[0], - (ratio/p)) - np.testing.assert_array_almost_equal( - _ideal_tfinal_and_dt(TransferFunction(1, [1, .5]).sample(.1))[0], - (ratio2/p)) - # confirm a TF with poles at 0 and p simulates for ratio/p seconds - np.testing.assert_array_almost_equal( - _ideal_tfinal_and_dt(TransferFunction(1, [1, .5, 0]))[0], - (ratio2/p)) - - # confirm a TF with a natural frequency of wn rad/s gets a - # dt of 1/(ratio*wn) - wn = 10 - ratio_dt = 1/(0.025133 * ratio * wn) - np.testing.assert_array_almost_equal( - _ideal_tfinal_and_dt(TransferFunction(1, [1, 0, wn**2]))[1], - 1/(ratio_dt*ratio*wn)) + + @pytest.mark.parametrize( + "tfsys, tfinal", + [(TransferFunction(1, [1, .5]), 9.21034), # pole at 0.5 + (TransferFunction(1, [1, .5]).sample(.1), 25), # discrete pole at 0.5 + (TransferFunction(1, [1, .5, 0]), 25)]) # poles at 0.5 and 0 + def test_auto_generated_time_vector_tfinal(self, tfsys, tfinal): + """Confirm a TF with a pole at p simulates for tfinal seconds""" + np.testing.assert_almost_equal( + _ideal_tfinal_and_dt(tfsys)[0], tfinal, decimal=4) + + @pytest.mark.parametrize("wn, zeta", [(10, 0), (100, 0), (100, .1)]) + def test_auto_generated_time_vector_dt_cont(self, wn, zeta): + """Confirm a TF with a natural frequency of wn rad/s gets a + dt of 1/(ratio*wn)""" + + dtref = 0.25133 / wn + + tfsys = TransferFunction(1, [1, 2*zeta*wn, wn**2]) + np.testing.assert_almost_equal(_ideal_tfinal_and_dt(tfsys)[1], dtref) + + def test_auto_generated_time_vector_dt_cont(self): + """A sampled tf keeps its dt""" wn = 100 - np.testing.assert_array_almost_equal( - _ideal_tfinal_and_dt(TransferFunction(1, [1, 0, wn**2]))[1], - 1/(ratio_dt*ratio*wn)) zeta = .1 - np.testing.assert_array_almost_equal( - _ideal_tfinal_and_dt(TransferFunction(1, [1, 2*zeta*wn, wn**2]))[1], - 1/(ratio_dt*ratio*wn)) - # but a smapled one keeps its dt - np.testing.assert_array_almost_equal( - _ideal_tfinal_and_dt(TransferFunction(1, [1, 2*zeta*wn, wn**2]).sample(.1))[1], - .1) - np.testing.assert_array_almost_equal( - np.diff(initial_response(TransferFunction(1, [1, 2*zeta*wn, wn**2]).sample(.1))[0][0:2]), - .1) - np.testing.assert_array_almost_equal( - _ideal_tfinal_and_dt(TransferFunction(1, [1, 2*zeta*wn, wn**2]))[1], - 1/(ratio_dt*ratio*wn)) - - - # TF with fast oscillations simulates only 5000 time steps even with long tfinal - self.assertEqual(5000, - len(_default_time_vector(TransferFunction(1, [1, 0, wn**2]),tfinal=100))) + tfsys = TransferFunction(1, [1, 2*zeta*wn, wn**2]).sample(.1) + tfinal, dt = _ideal_tfinal_and_dt(tfsys) + np.testing.assert_almost_equal(dt, .1) + T, _ = initial_response(tfsys) + np.testing.assert_almost_equal(np.diff(T[:2]), [.1]) + + def test_default_timevector_long(self): + """Test long time vector""" + + # TF with fast oscillations simulates only 5000 time steps + # even with long tfinal + wn = 100 + tfsys = TransferFunction(1, [1, 0, wn**2]) + tout = _default_time_vector(tfsys, tfinal=100) + assert len(tout) == 5000 + + @pytest.mark.parametrize("fun", [step_response, + impulse_response, + initial_response]) + def test_default_timevector_functions_c(self, fun): + """Test that functions can calculate the time vector automatically""" sys = TransferFunction(1, [1, .5, 0]) - sysdt = TransferFunction(1, [1, .5, 0], .1) + # test impose number of time steps - self.assertEqual(10, len(step_response(sys, T_num=10)[0])) - # test that discrete ignores T_num - self.assertNotEqual(15, len(step_response(sysdt, T_num=15)[0])) + tout, _ = fun(sys, T_num=10) + assert len(tout) == 10 + + # test impose final time + tout, _ = fun(sys, 100) + np.testing.assert_allclose(tout[-1], 100., atol=0.5) + + @pytest.mark.parametrize("fun", [step_response, + impulse_response, + initial_response]) + def test_default_timevector_functions_d(self, fun): + """Test that functions can calculate the time vector automatically""" + sys = TransferFunction(1, [1, .5, 0], 0.1) + + # test impose number of time steps is ignored with dt given + tout, _ = fun(sys, T_num=15) + assert len(tout) != 15 + # test impose final time - np.testing.assert_array_almost_equal( - 100, - np.ceil(step_response(sys, 100)[0][-1])) - np.testing.assert_array_almost_equal( - 100, - np.ceil(step_response(sysdt, 100)[0][-1])) - np.testing.assert_array_almost_equal( - 100, - np.ceil(impulse_response(sys, 100)[0][-1])) - np.testing.assert_array_almost_equal( - 100, - np.ceil(initial_response(sys, 100)[0][-1])) - - - def test_time_vector(self): - "Unit test: https://github.com/python-control/python-control/issues/239" - # Discrete time simulations with specified time vectors - Tin1 = np.arange(0, 5, 1) # matches dtf1, dss1; multiple of 0.2 - Tin2 = np.arange(0, 5, 0.2) # matches dtf2, dss2 - Tin3 = np.arange(0, 5, 0.5) # incompatible with 0.2 - - # Initial conditions to use for the different systems - siso_x0 = [1, 2] - mimo_x0 = [1, 2, 3, 4] - - # - # Easy cases: make sure that output sample time matches input - # - # No timebase in system => output should match input - # - # Initial response - tout, yout = initial_response(self.siso_dtf1, Tin2, siso_x0, - squeeze=False) - self.assertEqual(np.shape(tout), np.shape(yout[0,:])) - np.testing.assert_array_equal(tout, Tin2) - - # Impulse response - tout, yout = impulse_response(self.siso_dtf1, Tin2, - squeeze=False) - self.assertEqual(np.shape(tout), np.shape(yout[0,:])) - np.testing.assert_array_equal(tout, Tin2) - - # Step response - tout, yout = step_response(self.siso_dtf1, Tin2, - squeeze=False) - self.assertEqual(np.shape(tout), np.shape(yout[0,:])) - np.testing.assert_array_equal(tout, Tin2) - - # Forced response with specified time vector - tout, yout, xout = forced_response(self.siso_dtf1, Tin2, np.sin(Tin2), - squeeze=False) - self.assertEqual(np.shape(tout), np.shape(yout[0,:])) - np.testing.assert_array_equal(tout, Tin2) - - # Forced response with no time vector, no sample time (should use 1) - tout, yout, xout = forced_response(self.siso_dtf1, None, np.sin(Tin1), - squeeze=False) - self.assertEqual(np.shape(tout), np.shape(yout[0,:])) - np.testing.assert_array_equal(tout, Tin1) - - # MIMO forced response - tout, yout, xout = forced_response(self.mimo_dss1, Tin1, - (np.sin(Tin1), np.cos(Tin1)), - mimo_x0) - self.assertEqual(np.shape(tout), np.shape(yout[0,:])) - self.assertEqual(np.shape(tout), np.shape(yout[1,:])) - np.testing.assert_array_equal(tout, Tin1) - - # Matching timebase in system => output should match input - # - # Initial response - tout, yout = initial_response(self.siso_dtf2, Tin2, siso_x0, - squeeze=False) - self.assertEqual(np.shape(tout), np.shape(yout[0,:])) - np.testing.assert_array_equal(tout, Tin2) - - # Impulse response - tout, yout = impulse_response(self.siso_dtf2, Tin2, - squeeze=False) - self.assertEqual(np.shape(tout), np.shape(yout[0,:])) - np.testing.assert_array_equal(tout, Tin2) - - # Step response - tout, yout = step_response(self.siso_dtf2, Tin2, - squeeze=False) - self.assertEqual(np.shape(tout), np.shape(yout[0,:])) - np.testing.assert_array_equal(tout, Tin2) - - # Forced response - tout, yout, xout = forced_response(self.siso_dtf2, Tin2, np.sin(Tin2), - squeeze=False) - self.assertEqual(np.shape(tout), np.shape(yout[0,:])) - np.testing.assert_array_equal(tout, Tin2) - - # Forced response with no time vector, use sample time - tout, yout, xout = forced_response(self.siso_dtf2, None, np.sin(Tin2), - squeeze=False) - self.assertEqual(np.shape(tout), np.shape(yout[0,:])) - np.testing.assert_array_equal(tout, Tin2) - - # Compatible timebase in system => output should match input - # - # Initial response - tout, yout = initial_response(self.siso_dtf2, Tin1, siso_x0, - squeeze=False) - self.assertEqual(np.shape(tout), np.shape(yout[0,:])) - np.testing.assert_array_equal(tout, Tin1) - - # Impulse response - tout, yout = impulse_response(self.siso_dtf2, Tin1, - squeeze=False) - self.assertEqual(np.shape(tout), np.shape(yout[0,:])) - np.testing.assert_array_equal(tout, Tin1) - - # Step response - tout, yout = step_response(self.siso_dtf2, Tin1, - squeeze=False) - self.assertEqual(np.shape(tout), np.shape(yout[0,:])) - np.testing.assert_array_equal(tout, Tin1) - - # Forced response - tout, yout, xout = forced_response(self.siso_dtf2, Tin1, np.sin(Tin1), - squeeze=False) - self.assertEqual(np.shape(tout), np.shape(yout[0,:])) - np.testing.assert_array_equal(tout, Tin1) - - # - # Interpolation of the input (to match scipy.signal.dlsim) - # - # Initial response - tout, yout, xout = forced_response(self.siso_dtf2, Tin1, - np.sin(Tin1), interpolate=True, - squeeze=False) - self.assertEqual(np.shape(tout), np.shape(yout[0,:])) - self.assertTrue(np.allclose(tout[1:] - tout[:-1], self.siso_dtf2.dt)) - - # - # Incompatible cases: make sure an error is thrown - # - # System timebase and given time vector are incompatible - # - # Initial response - with self.assertRaises(Exception) as context: - tout, yout = initial_response(self.siso_dtf2, Tin3, siso_x0, - squeeze=False) - self.assertTrue(isinstance(context.exception, ValueError)) - - def test_discrete_time_steps(self): - """Make sure rounding errors in sample time are handled properly""" - # See https://github.com/python-control/python-control/issues/332) - # - # These tests play around with the input time vector to make sure that - # small rounding errors don't generate spurious errors. - - # Discrete time system to use for simulation - # self.siso_dtf2 = TransferFunction([1], [1, 1, 0.25], 0.2) + tout, _ = fun(sys, 100) + np.testing.assert_allclose(tout[-1], 100., atol=0.5) + + + @pytest.mark.parametrize("tsystem", + ["siso_ss2", # continuous + "siso_tf1", + "siso_dss1", # no timebase + "siso_dtf1", + "siso_dss2", # matching timebase + "siso_dtf2", + "mimo_ss2", # MIMO + pytest.param("mimo_tf2", marks=slycotonly), + "mimo_dss1", + pytest.param("mimo_dtf1", marks=slycotonly), + ], + indirect=True) + @pytest.mark.parametrize("fun", [step_response, + impulse_response, + initial_response, + forced_response]) + @pytest.mark.parametrize("squeeze", [None, True, False]) + def test_time_vector(self, tsystem, fun, squeeze, matarrayout): + """Test time vector handling and correct output convention + + gh-239, gh-295 + """ + sys = tsystem.sys + + kw = {} + if hasattr(tsystem, "t"): + t = tsystem.t + kw['T'] = t + if fun == forced_response: + kw['U'] = np.vstack([np.sin(t) for i in range(sys.inputs)]) + elif fun == forced_response and isctime(sys): + pytest.skip("No continuous forced_response without time vector.") + if hasattr(tsystem.sys, "states"): + kw['X0'] = np.arange(sys.states) + 1 + if sys.inputs > 1 and fun in [step_response, impulse_response]: + kw['input'] = 1 + if squeeze is not None: + kw['squeeze'] = squeeze + + out = fun(sys, **kw) + tout, yout = out[:2] + + assert tout.ndim == 1 + if hasattr(tsystem, 't'): + # tout should always match t, which has shape (n, ) + np.testing.assert_allclose(tout, tsystem.t) + if squeeze is False or sys.outputs > 1: + assert yout.shape[0] == sys.outputs + assert yout.shape[1] == tout.shape[0] + else: + assert yout.shape == tout.shape + + if sys.dt > 0 and sys.dt is not True and not np.isclose(sys.dt, 0.5): + kw['T'] = np.arange(0, 5, 0.5) # incompatible timebase + with pytest.raises(ValueError): + fun(sys, **kw) + + @pytest.mark.parametrize("squeeze", [None, True, False]) + def test_time_vector_interpolation(self, siso_dtf2, squeeze): + """Test time vector handling in case of interpolation + + Interpolation of the input (to match scipy.signal.dlsim) + + gh-239, gh-295 + """ + sys = siso_dtf2.sys + t = np.arange(0, 10, 1.) + u = np.sin(t) + x0 = 0 + + squeezekw = {} if squeeze is None else {"squeeze": squeeze} + + tout, yout, xout = forced_response(sys, t, u, x0, + interpolate=True, **squeezekw) + if squeeze is False or sys.outputs > 1: + assert yout.shape[0] == sys.outputs + assert yout.shape[1] == tout.shape[0] + else: + assert yout.shape == tout.shape + assert np.allclose(tout[1:] - tout[:-1], sys.dt) + + def test_discrete_time_steps(self, siso_dtf2): + """Make sure rounding errors in sample time are handled properly + + These tests play around with the input time vector to make sure that + small rounding errors don't generate spurious errors. + + gh-332 + """ + sys = siso_dtf2.sys # Set up a time range and simulate T = np.arange(0, 100, 0.2) - tout1, yout1 = step_response(self.siso_dtf2, T) + tout1, yout1 = step_response(sys, T) # Simulate every other time step T = np.arange(0, 100, 0.4) - tout2, yout2 = step_response(self.siso_dtf2, T) + tout2, yout2 = step_response(sys, T) np.testing.assert_array_almost_equal(tout1[::2], tout2) np.testing.assert_array_almost_equal(yout1[::2], yout2) # Add a small error into some of the time steps T = np.arange(0, 100, 0.2) T[1:-2:2] -= 1e-12 # tweak second value and a few others - tout3, yout3 = step_response(self.siso_dtf2, T) + tout3, yout3 = step_response(sys, T) np.testing.assert_array_almost_equal(tout1, tout3) np.testing.assert_array_almost_equal(yout1, yout3) # Add a small error into some of the time steps (w/ skipping) T = np.arange(0, 100, 0.4) T[1:-2:2] -= 1e-12 # tweak second value and a few others - tout4, yout4 = step_response(self.siso_dtf2, T) + tout4, yout4 = step_response(sys, T) np.testing.assert_array_almost_equal(tout2, tout4) np.testing.assert_array_almost_equal(yout2, yout4) # Make sure larger errors *do* generate an error T = np.arange(0, 100, 0.2) T[1:-2:2] -= 1e-3 # change second value and a few others - self.assertRaises(ValueError, step_response, self.siso_dtf2, T) - - def test_time_series_data_convention(self): - """Make sure time series data matches documentation conventions""" - # SISO continuous time - t, y = step_response(self.siso_ss1) - self.assertTrue(isinstance(t, np.ndarray) - and not isinstance(t, np.matrix)) - self.assertTrue(len(t.shape) == 1) - self.assertTrue(len(y.shape) == 1) # SISO returns "scalar" output - self.assertTrue(len(t) == len(y)) # Allows direct plotting of output - - # SISO discrete time - t, y = step_response(self.siso_dss1) - self.assertTrue(isinstance(t, np.ndarray) - and not isinstance(t, np.matrix)) - self.assertTrue(len(t.shape) == 1) - self.assertTrue(len(y.shape) == 1) # SISO returns "scalar" output - self.assertTrue(len(t) == len(y)) # Allows direct plotting of output - - # MIMO continuous time - tin = np.linspace(0, 10, 100) - uin = [np.sin(tin), np.cos(tin)] - t, y, x = forced_response(self.mimo_ss1, tin, uin) - self.assertTrue(isinstance(t, np.ndarray) - and not isinstance(t, np.matrix)) - self.assertTrue(len(t.shape) == 1) - self.assertTrue(len(y[0].shape) == 1) - self.assertTrue(len(y[1].shape) == 1) - self.assertTrue(len(t) == len(y[0])) - self.assertTrue(len(t) == len(y[1])) - - # MIMO discrete time - tin = np.linspace(0, 10, 100) - uin = [np.sin(tin), np.cos(tin)] - t, y, x = forced_response(self.mimo_dss1, tin, uin) - self.assertTrue(isinstance(t, np.ndarray) - and not isinstance(t, np.matrix)) - self.assertTrue(len(t.shape) == 1) - self.assertTrue(len(y[0].shape) == 1) - self.assertTrue(len(y[1].shape) == 1) - self.assertTrue(len(t) == len(y[0])) - self.assertTrue(len(t) == len(y[1])) - - # Allow input time as 2D array (output should be 1D) - tin = np.array(np.linspace(0, 10, 100), ndmin=2) - t, y = step_response(self.siso_ss1, tin) - self.assertTrue(isinstance(t, np.ndarray) - and not isinstance(t, np.matrix)) - self.assertTrue(len(t.shape) == 1) - self.assertTrue(len(y.shape) == 1) # SISO returns "scalar" output - self.assertTrue(len(t) == len(y)) # Allows direct plotting of output + with pytest.raises(ValueError): + step_response(sys, T) - -if __name__ == '__main__': - unittest.main() + def test_time_series_data_convention_2D(self, siso_ss1): + """Allow input time as 2D array (output should be 1D)""" + tin = np.array(np.linspace(0, 10, 100), ndmin=2) + t, y = step_response(siso_ss1.sys, tin) + assert isinstance(t, np.ndarray) and not isinstance(t, np.matrix) + assert t.ndim == 1 + assert y.ndim == 1 # SISO returns "scalar" output + assert t.shape == y.shape # Allows direct plotting of output From 29883e8c791377c51aca9437157f139bc058ed93 Mon Sep 17 00:00:00 2001 From: bnavigator Date: Fri, 31 Jul 2020 15:52:55 +0200 Subject: [PATCH 28/30] pytestify xferfcn_input_test.py remove a lot of duplicate code by converting everything into a single parametrized test function. --- control/tests/xferfcn_input_test.py | 328 +++++++--------------------- 1 file changed, 74 insertions(+), 254 deletions(-) mode change 100644 => 100755 control/tests/xferfcn_input_test.py diff --git a/control/tests/xferfcn_input_test.py b/control/tests/xferfcn_input_test.py old mode 100644 new mode 100755 index 52fb85c29..995f6ac03 --- a/control/tests/xferfcn_input_test.py +++ b/control/tests/xferfcn_input_test.py @@ -1,259 +1,79 @@ -#!/usr/bin/env python -# -# xferfcn_input_test.py - test inputs to TransferFunction class -# jed-frey, 18 Feb 2017 (based on xferfcn_test.py) +"""xferfcn_input_test.py - test inputs to TransferFunction class -import unittest -import numpy as np +jed-frey, 18 Feb 2017 (based on xferfcn_test.py) +BG, 31 Jul 2020 convert to pytest and parametrize into single function +""" -from numpy import int, int8, int16, int32, int64 -from numpy import float, float16, float32, float64, longdouble -from numpy import all, ndarray, array +import numpy as np +import pytest from control.xferfcn import _clean_part - -class TestXferFcnInput(unittest.TestCase): - """These are tests for functionality of cleaning and validating XferFcnInput.""" - - # Tests for raising exceptions. - def test_clean_part_bad_input_type(self): - """Give the part cleaner invalid input type.""" - - self.assertRaises(TypeError, _clean_part, [[0., 1.], [2., 3.]]) - - def test_clean_part_bad_input_type2(self): - """Give the part cleaner another invalid input type.""" - self.assertRaises(TypeError, _clean_part, [1, "a"]) - - def test_clean_part_scalar(self): - """Test single scalar value.""" - num = 1 - num_ = _clean_part(num) - - assert isinstance(num_, list) - assert np.all([isinstance(part, list) for part in num_]) - np.testing.assert_array_equal(num_[0][0], array([1.0], dtype=float)) - - def test_clean_part_list_scalar(self): - """Test single scalar value in list.""" - num = [1] - num_ = _clean_part(num) - - assert isinstance(num_, list) - assert np.all([isinstance(part, list) for part in num_]) - np.testing.assert_array_equal(num_[0][0], array([1.0], dtype=float)) - - def test_clean_part_tuple_scalar(self): - """Test single scalar value in tuple.""" - num = (1) - num_ = _clean_part(num) - - assert isinstance(num_, list) - assert np.all([isinstance(part, list) for part in num_]) - np.testing.assert_array_equal(num_[0][0], array([1.0], dtype=float)) - - def test_clean_part_list(self): - """Test multiple values in a list.""" - num = [1, 2] - num_ = _clean_part(num) - - assert isinstance(num_, list) - assert np.all([isinstance(part, list) for part in num_]) - np.testing.assert_array_equal(num_[0][0], array([1.0, 2.0], dtype=float)) - - def test_clean_part_tuple(self): - """Test multiple values in tuple.""" - num = (1, 2) - num_ = _clean_part(num) - - assert isinstance(num_, list) - assert np.all([isinstance(part, list) for part in num_]) - np.testing.assert_array_equal(num_[0][0], array([1.0, 2.0], dtype=float)) - - def test_clean_part_all_scalar_types(self): - """Test single scalar value for all valid data types.""" - for dtype in [int, int8, int16, int32, int64, float, float16, float32, float64, longdouble]: - num = dtype(1) - num_ = _clean_part(num) - - assert isinstance(num_, list) - assert np.all([isinstance(part, list) for part in num_]) - np.testing.assert_array_equal(num_[0][0], array([1.0], dtype=float)) - - def test_clean_part_np_array(self): - """Test multiple values in numpy array.""" - num = np.array([1, 2]) - num_ = _clean_part(num) - - assert isinstance(num_, list) - assert np.all([isinstance(part, list) for part in num_]) - np.testing.assert_array_equal(num_[0][0], array([1.0, 2.0], dtype=float)) - - def test_clean_part_all_np_array_types(self): - """Test scalar value in numpy array of ndim=0 for all data types.""" - for dtype in [int, int8, int16, int32, int64, float, float16, float32, float64, longdouble]: - num = np.array(1, dtype=dtype) - num_ = _clean_part(num) - - assert isinstance(num_, list) - assert np.all([isinstance(part, list) for part in num_]) - np.testing.assert_array_equal(num_[0][0], array([1.0], dtype=float)) - - def test_clean_part_all_np_array_types2(self): - """Test numpy array for all types.""" - for dtype in [int, int8, int16, int32, int64, float, float16, float32, float64, longdouble]: - num = np.array([1, 2], dtype=dtype) - num_ = _clean_part(num) - - assert isinstance(num_, list) - assert np.all([isinstance(part, list) for part in num_]) - np.testing.assert_array_equal(num_[0][0], array([1.0, 2.0], dtype=float)) - - def test_clean_part_list_all_types(self): - """Test list of a single value for all data types.""" - for dtype in [int, int8, int16, int32, int64, float, float16, float32, float64, longdouble]: - num = [dtype(1)] - num_ = _clean_part(num) - assert isinstance(num_, list) - assert np.all([isinstance(part, list) for part in num_]) - np.testing.assert_array_equal(num_[0][0], array([1.0], dtype=float)) - - def test_clean_part_list_all_types2(self): - """List of list of numbers of all data types.""" - for dtype in [int, int8, int16, int32, int64, float, float16, float32, float64, longdouble]: - num = [dtype(1), dtype(2)] - num_ = _clean_part(num) - assert isinstance(num_, list) - assert np.all([isinstance(part, list) for part in num_]) - np.testing.assert_array_equal(num_[0][0], array([1.0, 2.0], dtype=float)) - - def test_clean_part_tuple_all_types(self): - """Test tuple of a single value for all data types.""" - for dtype in [int, int8, int16, int32, int64, float, float16, float32, float64, longdouble]: - num = (dtype(1),) - num_ = _clean_part(num) - assert isinstance(num_, list) - assert np.all([isinstance(part, list) for part in num_]) - np.testing.assert_array_equal(num_[0][0], array([1.0], dtype=float)) - - def test_clean_part_tuple_all_types2(self): - """Test tuple of a single value for all data types.""" - for dtype in [int, int8, int16, int32, int64, float, float16, float32, float64, longdouble]: - num = (dtype(1), dtype(2)) - num_ = _clean_part(num) - assert isinstance(num_, list) - assert np.all([isinstance(part, list) for part in num_]) - np.testing.assert_array_equal(num_[0][0], array([1, 2], dtype=float)) - - def test_clean_part_list_list_list_int(self): - """ Test an int in a list of a list of a list.""" - num = [[[1]]] - num_ = _clean_part(num) - assert isinstance(num_, list) - assert np.all([isinstance(part, list) for part in num_]) - np.testing.assert_array_equal(num_[0][0], array([1.0], dtype=float)) - - def test_clean_part_list_list_list_float(self): - """ Test a float in a list of a list of a list.""" - num = [[[1.0]]] - num_ = _clean_part(num) - assert isinstance(num_, list) - assert np.all([isinstance(part, list) for part in num_]) - np.testing.assert_array_equal(num_[0][0], array([1.0], dtype=float)) - - def test_clean_part_list_list_list_ints(self): - """Test 2 lists of ints in a list in a list.""" - num = [[[1, 1], [2, 2]]] - num_ = _clean_part(num) - - assert isinstance(num_, list) - assert np.all([isinstance(part, list) for part in num_]) - np.testing.assert_array_equal(num_[0][0], array([1.0, 1.0], dtype=float)) - np.testing.assert_array_equal(num_[0][1], array([2.0, 2.0], dtype=float)) - - def test_clean_part_list_list_list_floats(self): - """Test 2 lists of ints in a list in a list.""" - num = [[[1.0, 1.0], [2.0, 2.0]]] - num_ = _clean_part(num) - - assert isinstance(num_, list) - assert np.all([isinstance(part, list) for part in num_]) - np.testing.assert_array_equal(num_[0][0], array([1.0, 1.0], dtype=float)) - np.testing.assert_array_equal(num_[0][1], array([2.0, 2.0], dtype=float)) - - def test_clean_part_list_list_array(self): - """List of list of numpy arrays for all valid types.""" - for dtype in int, int8, int16, int32, int64, float, float16, float32, float64, longdouble: - num = [[array([1, 1], dtype=dtype), array([2, 2], dtype=dtype)]] - num_ = _clean_part(num) - - assert isinstance(num_, list) - assert np.all([isinstance(part, list) for part in num_]) - np.testing.assert_array_equal(num_[0][0], array([1.0, 1.0], dtype=float)) - np.testing.assert_array_equal(num_[0][1], array([2.0, 2.0], dtype=float)) - - def test_clean_part_tuple_list_array(self): - """Tuple of list of numpy arrays for all valid types.""" - for dtype in int, int8, int16, int32, int64, float, float16, float32, float64, longdouble: - num = ([array([1, 1], dtype=dtype), array([2, 2], dtype=dtype)],) - num_ = _clean_part(num) - - assert isinstance(num_, list) - assert np.all([isinstance(part, list) for part in num_]) - np.testing.assert_array_equal(num_[0][0], array([1.0, 1.0], dtype=float)) - np.testing.assert_array_equal(num_[0][1], array([2.0, 2.0], dtype=float)) - - def test_clean_part_list_tuple_array(self): - """List of tuple of numpy array for all valid types.""" - for dtype in int, int8, int16, int32, int64, float, float16, float32, float64, longdouble: - num = [(array([1, 1], dtype=dtype), array([2, 2], dtype=dtype))] - num_ = _clean_part(num) - - assert isinstance(num_, list) - assert np.all([isinstance(part, list) for part in num_]) - np.testing.assert_array_equal(num_[0][0], array([1.0, 1.0], dtype=float)) - np.testing.assert_array_equal(num_[0][1], array([2.0, 2.0], dtype=float)) - - def test_clean_part_tuple_tuples_arrays(self): - """Tuple of tuples of numpy arrays for all valid types.""" - for dtype in int, int8, int16, int32, int64, float, float16, float32, float64, longdouble: - num = ((array([1, 1], dtype=dtype), array([2, 2], dtype=dtype)), - (array([3, 4], dtype=dtype), array([4, 4], dtype=dtype))) - num_ = _clean_part(num) - - assert isinstance(num_, list) - assert np.all([isinstance(part, list) for part in num_]) - np.testing.assert_array_equal(num_[0][0], array([1.0, 1.0], dtype=float)) - np.testing.assert_array_equal(num_[0][1], array([2.0, 2.0], dtype=float)) - - def test_clean_part_list_tuples_arrays(self): - """List of tuples of numpy arrays for all valid types.""" - for dtype in int, int8, int16, int32, int64, float, float16, float32, float64, longdouble: - num = [(array([1, 1], dtype=dtype), array([2, 2], dtype=dtype)), - (array([3, 4], dtype=dtype), array([4, 4], dtype=dtype))] - num_ = _clean_part(num) - - assert isinstance(num_, list) - assert np.all([isinstance(part, list) for part in num_]) - np.testing.assert_array_equal(num_[0][0], array([1.0, 1.0], dtype=float)) - np.testing.assert_array_equal(num_[0][1], array([2.0, 2.0], dtype=float)) - - def test_clean_part_list_list_arrays(self): - """List of list of numpy arrays for all valid types.""" - for dtype in int, int8, int16, int32, int64, float, float16, float32, float64, longdouble: - num = [[array([1, 1], dtype=dtype), array([2, 2], dtype=dtype)], - [array([3, 3], dtype=dtype), array([4, 4], dtype=dtype)]] - num_ = _clean_part(num) - - assert len(num_) == 2 - assert np.all([isinstance(part, list) for part in num_]) - assert np.all([len(part) == 2 for part in num_]) - np.testing.assert_array_equal(num_[0][0], array([1.0, 1.0], dtype=float)) - np.testing.assert_array_equal(num_[0][1], array([2.0, 2.0], dtype=float)) - np.testing.assert_array_equal(num_[1][0], array([3.0, 3.0], dtype=float)) - np.testing.assert_array_equal(num_[1][1], array([4.0, 4.0], dtype=float)) - - -if __name__ == "__main__": - unittest.main() +cases = { + "scalar": + (1, lambda dtype, v: dtype(v)), + "scalar in 0d array": + (1, lambda dtype, v: np.array(v, dtype=dtype)), + "numpy array": + ([1, 2], lambda dtype, v: np.array(v, dtype=dtype)), + "list of scalar": + (1, lambda dtype, v: [dtype(v)]), + "list of scalars": + ([1, 2], lambda dtype, v: [dtype(vi) for vi in v]), + "list of list of list of scalar": + (1, lambda dtype, v: [[[dtype(v)]]]), + "list of list of list of scalars": + ([[1, 1], [2, 2]], + lambda dtype, v: [[[dtype(vi) for vi in vr] for vr in v]]), + "tuple of scalar": + (1, lambda dtype, v: (dtype(v),)), + "tuple of scalars": + ([1, 2], lambda dtype, v: tuple(dtype(vi) for vi in v)), + "list of list of numpy arrays": + ([[1, 1], [2, 2]], + lambda dtype, v: [[np.array(vr, dtype=dtype) for vr in v]]), + "tuple of list of numpy arrays": + ([[1, 1], [2, 2]], + lambda dtype, v: ([np.array(vr, dtype=dtype) for vr in v],)), + "list of tuple of numpy arrays": + ([[1, 1], [2, 2]], + lambda dtype, v: [tuple(np.array(vr, dtype=dtype) for vr in v)]), + "tuple of tuples of numpy arrays": + ([[[1, 1], [2, 2]], [[3, 3], [4, 4]]], + lambda dtype, v: tuple(tuple(np.array(vr, dtype=dtype) for vr in vp) + for vp in v)), + "list of tuples of numpy arrays": + ([[[1, 1], [2, 2]], [[3, 3], [4, 4]]], + lambda dtype, v: [tuple(np.array(vr, dtype=dtype) for vr in vp) + for vp in v]), + "list of lists of numpy arrays": + ([[[1, 1], [2, 2]], [[3, 3], [4, 4]]], + lambda dtype, v: [[np.array(vr, dtype=dtype) for vr in vp] + for vp in v]), +} + + +@pytest.mark.parametrize("dtype", + [np.int, np.int8, np.int16, np.int32, np.int64, + np.float, np.float16, np.float32, np.float64, + np.longdouble]) +@pytest.mark.parametrize("num, fun", cases.values(), ids=cases.keys()) +def test_clean_part(num, fun, dtype): + """Test clean part for various inputs""" + numa = fun(dtype, num) + num_ = _clean_part(numa) + ref_ = np.array(num, dtype=np.float, ndmin=3) + + assert isinstance(num_, list) + assert np.all([isinstance(part, list) for part in num_]) + for i, numi in enumerate(num_): + assert len(numi) == ref_.shape[1] + for j, numj in enumerate(numi): + np.testing.assert_array_equal(numj, ref_[i, j, ...]) + + +@pytest.mark.parametrize("badinput", [[[0., 1.], [2., 3.]], "a"]) +def test_clean_part_bad_input(badinput): + """Give the part cleaner invalid input type.""" + with pytest.raises(TypeError): + _clean_part(badinput) From ca9476faca6854936933b3835ffc86611e61c9bd Mon Sep 17 00:00:00 2001 From: bnavigator Date: Tue, 29 Dec 2020 23:05:19 +0100 Subject: [PATCH 29/30] pytestify xferfcn tests --- control/tests/xferfcn_input_test.py | 0 control/tests/xferfcn_test.py | 573 ++++++++++++++-------------- 2 files changed, 294 insertions(+), 279 deletions(-) mode change 100755 => 100644 control/tests/xferfcn_input_test.py diff --git a/control/tests/xferfcn_input_test.py b/control/tests/xferfcn_input_test.py old mode 100755 new mode 100644 diff --git a/control/tests/xferfcn_test.py b/control/tests/xferfcn_test.py index 17e602090..b0673de1e 100644 --- a/control/tests/xferfcn_test.py +++ b/control/tests/xferfcn_test.py @@ -1,117 +1,123 @@ -#!/usr/bin/env python -# -# xferfcn_test.py - test TransferFunction class -# RMM, 30 Mar 2011 (based on TestXferFcn from v0.4a) +"""xferfcn_test.py - test TransferFunction class -import unittest -import pytest +RMM, 30 Mar 2011 (based on TestXferFcn from v0.4a) +""" -import sys as pysys import numpy as np +import pytest + from control.statesp import StateSpace, _convertToStateSpace, rss from control.xferfcn import TransferFunction, _convert_to_transfer_function, \ ss2tf from control.lti import evalfr -from control.exception import slycot_check +from control.tests.conftest import slycotonly, nopython2, matrixfilter from control.lti import isctime, isdtime from control.dtime import sample_system -class TestXferFcn(unittest.TestCase): - """These are tests for functionality and correct reporting of the transfer - function class. Throughout these tests, we will give different input +class TestXferFcn: + """Test functionality and correct reporting of the transfer function class. + + Throughout these tests, we will give different input formats to the xTranferFunction constructor, to try to break it. These - tests have been verified in MATLAB.""" + tests have been verified in MATLAB. + """ # Tests for raising exceptions. def test_constructor_bad_input_type(self): """Give the constructor invalid input types.""" - # MIMO requires lists of lists of vectors (not lists of vectors) - self.assertRaises( - TypeError, - TransferFunction, [[0., 1.], [2., 3.]], [[5., 2.], [3., 0.]]) - TransferFunction([[ [0., 1.], [2., 3.] ]], [[ [5., 2.], [3., 0.] ]]) + with pytest.raises(TypeError): + TransferFunction([[0., 1.], [2., 3.]], [[5., 2.], [3., 0.]]) + # good input + TransferFunction([[[0., 1.], [2., 3.]]], + [[[5., 2.], [3., 0.]]]) # Single argument of the wrong type - self.assertRaises(TypeError, TransferFunction, [1]) + with pytest.raises(TypeError): + TransferFunction([1]) # Too many arguments - self.assertRaises(ValueError, TransferFunction, 1, 2, 3, 4) + with pytest.raises(ValueError): + TransferFunction(1, 2, 3, 4) # Different numbers of elements in numerator rows - self.assertRaises( - ValueError, - TransferFunction, [ [[0, 1], [2, 3]], - [[4, 5]] ], - [ [[6, 7], [4, 5]], - [[2, 3], [0, 1]] ]) - self.assertRaises( - ValueError, - TransferFunction, [ [[0, 1], [2, 3]], - [[4, 5], [6, 7]] ], - [ [[6, 7], [4, 5]], - [[2, 3]] ]) - TransferFunction( # This version is OK - [ [[0, 1], [2, 3]], [[4, 5], [6, 7]] ], - [ [[6, 7], [4, 5]], [[2, 3], [0, 1]] ]) + with pytest.raises(ValueError): + TransferFunction([[[0, 1], [2, 3]], + [[4, 5]]], + [[[6, 7], [4, 5]], + [[2, 3], [0, 1]]]) + with pytest.raises(ValueError): + TransferFunction([[[0, 1], [2, 3]], + [[4, 5], [6, 7]]], + [[[6, 7], [4, 5]], + [[2, 3]]]) + # good input + TransferFunction([[[0, 1], [2, 3]], + [[4, 5], [6, 7]]], + [[[6, 7], [4, 5]], + [[2, 3], [0, 1]]]) def test_constructor_inconsistent_dimension(self): """Give constructor numerators, denominators of different sizes.""" - - self.assertRaises(ValueError, TransferFunction, - [[[1.]]], [[[1.], [2., 3.]]]) - self.assertRaises(ValueError, TransferFunction, - [[[1.]]], [[[1.]], [[2., 3.]]]) - self.assertRaises(ValueError, TransferFunction, - [[[1.]]], [[[1.], [1., 2.]], [[5., 2.], [2., 3.]]]) + with pytest.raises(ValueError): + TransferFunction([[[1.]]], [[[1.], [2., 3.]]]) + with pytest.raises(ValueError): + TransferFunction([[[1.]]], [[[1.]], [[2., 3.]]]) + with pytest.raises(ValueError): + TransferFunction([[[1.]]], + [[[1.], [1., 2.]], [[5., 2.], [2., 3.]]]) def test_constructor_inconsistent_columns(self): """Give the constructor inputs that do not have the same number of columns in each row.""" - - self.assertRaises(ValueError, TransferFunction, - 1., [[[1.]], [[2.], [3.]]]) - self.assertRaises(ValueError, TransferFunction, - [[[1.]], [[2.], [3.]]], 1.) + with pytest.raises(ValueError): + TransferFunction(1., [[[1.]], [[2.], [3.]]]) + with pytest.raises(ValueError): + TransferFunction([[[1.]], [[2.], [3.]]], 1.) def test_constructor_zero_denominator(self): """Give the constructor a transfer function with a zero denominator.""" - - self.assertRaises(ValueError, TransferFunction, 1., 0.) - self.assertRaises(ValueError, TransferFunction, - [[[1.], [2., 3.]], [[-1., 4.], [3., 2.]]], - [[[1., 0.], [0.]], [[0., 0.], [2.]]]) + with pytest.raises(ValueError): + TransferFunction(1., 0.) + with pytest.raises(ValueError): + TransferFunction([[[1.], [2., 3.]], [[-1., 4.], [3., 2.]]], + [[[1., 0.], [0.]], [[0., 0.], [2.]]]) def test_add_inconsistent_dimension(self): """Add two transfer function matrices of different sizes.""" - sys1 = TransferFunction([[[1., 2.]]], [[[4., 5.]]]) sys2 = TransferFunction([[[4., 3.]], [[1., 2.]]], [[[1., 6.]], [[2., 4.]]]) - self.assertRaises(ValueError, sys1.__add__, sys2) - self.assertRaises(ValueError, sys1.__sub__, sys2) - self.assertRaises(ValueError, sys1.__radd__, sys2) - self.assertRaises(ValueError, sys1.__rsub__, sys2) + with pytest.raises(ValueError): + sys1.__add__(sys2) + with pytest.raises(ValueError): + sys1.__sub__(sys2) + with pytest.raises(ValueError): + sys1.__radd__(sys2) + with pytest.raises(ValueError): + sys1.__rsub__(sys2) def test_mul_inconsistent_dimension(self): """Multiply two transfer function matrices of incompatible sizes.""" - sys1 = TransferFunction([[[1., 2.], [4., 5.]], [[2., 5.], [4., 3.]]], [[[6., 2.], [4., 1.]], [[6., 7.], [2., 4.]]]) sys2 = TransferFunction([[[1.]], [[2.]], [[3.]]], [[[4.]], [[5.]], [[6.]]]) - self.assertRaises(ValueError, sys1.__mul__, sys2) - self.assertRaises(ValueError, sys2.__mul__, sys1) - self.assertRaises(ValueError, sys1.__rmul__, sys2) - self.assertRaises(ValueError, sys2.__rmul__, sys1) + with pytest.raises(ValueError): + sys1.__mul__(sys2) + with pytest.raises(ValueError): + sys2.__mul__(sys1) + with pytest.raises(ValueError): + sys1.__rmul__(sys2) + with pytest.raises(ValueError): + sys2.__rmul__(sys1) # Tests for TransferFunction._truncatecoeff def test_truncate_coefficients_non_null_numerator(self): """Remove extraneous zeros in polynomial representations.""" - sys1 = TransferFunction([0., 0., 1., 2.], [[[0., 0., 0., 3., 2., 1.]]]) np.testing.assert_array_equal(sys1.num, [[[1., 2.]]]) @@ -119,7 +125,6 @@ def test_truncate_coefficients_non_null_numerator(self): def test_truncate_coefficients_null_numerator(self): """Remove extraneous zeros in polynomial representations.""" - sys1 = TransferFunction([0., 0., 0.], 1.) np.testing.assert_array_equal(sys1.num, [[[0.]]]) @@ -129,7 +134,6 @@ def test_truncate_coefficients_null_numerator(self): def test_reverse_sign_scalar(self): """Negate a direct feedthrough system.""" - sys1 = TransferFunction(2., np.array([-3.])) sys2 = - sys1 @@ -138,17 +142,15 @@ def test_reverse_sign_scalar(self): def test_reverse_sign_siso(self): """Negate a SISO system.""" - sys1 = TransferFunction([1., 3., 5], [1., 6., 2., -1.]) sys2 = - sys1 np.testing.assert_array_equal(sys2.num, [[[-1., -3., -5.]]]) np.testing.assert_array_equal(sys2.den, [[[1., 6., 2., -1.]]]) - @unittest.skipIf(not slycot_check(), "slycot not installed") + @slycotonly def test_reverse_sign_mimo(self): """Negate a MIMO system.""" - num1 = [[[1., 2.], [0., 3.], [2., -1.]], [[1.], [4., 0.], [1., -4., 3.]]] num3 = [[[-1., -2.], [0., -3.], [-2., 1.]], @@ -169,7 +171,6 @@ def test_reverse_sign_mimo(self): def test_add_scalar(self): """Add two direct feedthrough systems.""" - sys1 = TransferFunction(1., [[[1.]]]) sys2 = TransferFunction(np.array([2.]), [1.]) sys3 = sys1 + sys2 @@ -179,7 +180,6 @@ def test_add_scalar(self): def test_add_siso(self): """Add two SISO systems.""" - sys1 = TransferFunction([1., 3., 5], [1., 6., 2., -1]) sys2 = TransferFunction([[np.array([-1., 3.])]], [[[1., 0., -1.]]]) sys3 = sys1 + sys2 @@ -188,10 +188,9 @@ def test_add_siso(self): np.testing.assert_array_equal(sys3.num, [[[20., 4., -8]]]) np.testing.assert_array_equal(sys3.den, [[[1., 6., 1., -7., -2., 1.]]]) - @unittest.skipIf(not slycot_check(), "slycot not installed") + @slycotonly def test_add_mimo(self): """Add two MIMO systems.""" - num1 = [[[1., 2.], [0., 3.], [2., -1.]], [[1.], [4., 0.], [1., -4., 3.]]] den1 = [[[-3., 2., 4.], [1., 0., 0.], [2., -1.]], @@ -218,7 +217,6 @@ def test_add_mimo(self): def test_subtract_scalar(self): """Subtract two direct feedthrough systems.""" - sys1 = TransferFunction(1., [[[1.]]]) sys2 = TransferFunction(np.array([2.]), [1.]) sys3 = sys1 - sys2 @@ -228,7 +226,6 @@ def test_subtract_scalar(self): def test_subtract_siso(self): """Subtract two SISO systems.""" - sys1 = TransferFunction([1., 3., 5], [1., 6., 2., -1]) sys2 = TransferFunction([[np.array([-1., 3.])]], [[[1., 0., -1.]]]) sys3 = sys1 - sys2 @@ -239,10 +236,9 @@ def test_subtract_siso(self): np.testing.assert_array_equal(sys4.num, [[[-2., -6., 12., 10., 2.]]]) np.testing.assert_array_equal(sys4.den, [[[1., 6., 1., -7., -2., 1.]]]) - @unittest.skipIf(not slycot_check(), "slycot not installed") + @slycotonly def test_subtract_mimo(self): """Subtract two MIMO systems.""" - num1 = [[[1., 2.], [0., 3.], [2., -1.]], [[1.], [4., 0.], [1., -4., 3.]]] den1 = [[[-3., 2., 4.], [1., 0., 0.], [2., -1.]], @@ -269,7 +265,6 @@ def test_subtract_mimo(self): def test_multiply_scalar(self): """Multiply two direct feedthrough systems.""" - sys1 = TransferFunction(2., [1.]) sys2 = TransferFunction(1., 4.) sys3 = sys1 * sys2 @@ -282,7 +277,6 @@ def test_multiply_scalar(self): def test_multiply_siso(self): """Multiply two SISO systems.""" - sys1 = TransferFunction([1., 3., 5], [1., 6., 2., -1]) sys2 = TransferFunction([[[-1., 3.]]], [[[1., 0., -1.]]]) sys3 = sys1 * sys2 @@ -293,10 +287,9 @@ def test_multiply_siso(self): np.testing.assert_array_equal(sys3.num, sys4.num) np.testing.assert_array_equal(sys3.den, sys4.den) - @unittest.skipIf(not slycot_check(), "slycot not installed") + @slycotonly def test_multiply_mimo(self): """Multiply two MIMO systems.""" - num1 = [[[1., 2.], [0., 3.], [2., -1.]], [[1.], [4., 0.], [1., -4., 3.]]] den1 = [[[-3., 2., 4.], [1., 0., 0.], [2., -1.]], @@ -328,7 +321,6 @@ def test_multiply_mimo(self): def test_divide_scalar(self): """Divide two direct feedthrough systems.""" - sys1 = TransferFunction(np.array([3.]), -4.) sys2 = TransferFunction(5., 2.) sys3 = sys1 / sys2 @@ -338,7 +330,6 @@ def test_divide_scalar(self): def test_divide_siso(self): """Divide two SISO systems.""" - sys1 = TransferFunction([1., 3., 5], [1., 6., 2., -1]) sys2 = TransferFunction([[[-1., 3.]]], [[[1., 0., -1.]]]) sys3 = sys1 / sys2 @@ -351,91 +342,99 @@ def test_divide_siso(self): def test_div(self): # Make sure that sampling times work correctly - sys1 = TransferFunction([1., 3., 5], [1., 6., 2., -1]) + sys1 = TransferFunction([1., 3., 5], [1., 6., 2., -1], None) sys2 = TransferFunction([[[-1., 3.]]], [[[1., 0., -1.]]], True) sys3 = sys1 / sys2 - self.assertEqual(sys3.dt, True) + assert sys3.dt is True sys2 = TransferFunction([[[-1., 3.]]], [[[1., 0., -1.]]], 0.5) sys3 = sys1 / sys2 - self.assertEqual(sys3.dt, 0.5) + assert sys3.dt == 0.5 sys1 = TransferFunction([1., 3., 5], [1., 6., 2., -1], 0.1) - self.assertRaises(ValueError, TransferFunction.__truediv__, sys1, sys2) + with pytest.raises(ValueError): + TransferFunction.__truediv__(sys1, sys2) sys1 = sample_system(rss(4, 1, 1), 0.5) sys3 = TransferFunction.__rtruediv__(sys2, sys1) - self.assertEqual(sys3.dt, 0.5) + assert sys3.dt == 0.5 def test_pow(self): sys1 = TransferFunction([1., 3., 5], [1., 6., 2., -1]) - self.assertRaises(ValueError, TransferFunction.__pow__, sys1, 0.5) + with pytest.raises(ValueError): + TransferFunction.__pow__(sys1, 0.5) def test_slice(self): sys = TransferFunction( [ [ [1], [2], [3]], [ [3], [4], [5]] ], [ [[1, 2], [1, 3], [1, 4]], [[1, 4], [1, 5], [1, 6]] ]) sys1 = sys[1:, 1:] - self.assertEqual((sys1.inputs, sys1.outputs), (2, 1)) + assert (sys1.inputs, sys1.outputs) == (2, 1) sys2 = sys[:2, :2] - self.assertEqual((sys2.inputs, sys2.outputs), (2, 2)) + assert (sys2.inputs, sys2.outputs) == (2, 2) sys = TransferFunction( [ [ [1], [2], [3]], [ [3], [4], [5]] ], [ [[1, 2], [1, 3], [1, 4]], [[1, 4], [1, 5], [1, 6]] ], 0.5) sys1 = sys[1:, 1:] - self.assertEqual((sys1.inputs, sys1.outputs), (2, 1)) - self.assertEqual(sys1.dt, 0.5) - - def test_evalfr_siso(self): - """Evaluate the frequency response of a SISO system at one frequency.""" - + assert (sys1.inputs, sys1.outputs) == (2, 1) + assert sys1.dt == 0.5 + + @pytest.mark.parametrize("omega, resp", + [(1, np.array([[-0.5 - 0.5j]])), + (32, np.array([[0.002819593 - 0.03062847j]]))]) + @pytest.mark.parametrize("dt", [None, 0, 1e-3]) + def test_evalfr_siso(self, dt, omega, resp): + """Evaluate the frequency response at single frequencies""" sys = TransferFunction([1., 3., 5], [1., 6., 2., -1]) - np.testing.assert_array_almost_equal(evalfr(sys, 1j), - np.array([[-0.5 - 0.5j]])) - np.testing.assert_array_almost_equal( - evalfr(sys, 32j), - np.array([[0.00281959302585077 - 0.030628473607392j]])) - - # Test call version as well - np.testing.assert_almost_equal(sys(1.j), -0.5 - 0.5j) - np.testing.assert_almost_equal( - sys(32.j), 0.00281959302585077 - 0.030628473607392j) - - # Test internal version (with real argument) - np.testing.assert_array_almost_equal( - sys._evalfr(1.), np.array([[-0.5 - 0.5j]])) - np.testing.assert_array_almost_equal( - sys._evalfr(32.), - np.array([[0.00281959302585077 - 0.030628473607392j]])) - - # This test only works in Python 3 due to a conflict with the same - # warning type in other test modules (frd_test.py). See - # https://bugs.python.org/issue4180 for more details - @unittest.skipIf(pysys.version_info < (3, 0), "test requires Python 3+") - def test_evalfr_deprecated(self): - sys = TransferFunction([1., 3., 5], [1., 6., 2., -1]) + if dt: + sys = sample_system(sys, dt) + s = np.exp(omega * 1j * dt) + else: + s = omega * 1j + # Correct versions of the call + np.testing.assert_allclose(evalfr(sys, s), resp, atol=1e-3) + np.testing.assert_allclose(sys(s), resp, atol=1e-3) # Deprecated version of the call (should generate warning) - import warnings - with warnings.catch_warnings(): - # Make warnings generate an exception - warnings.simplefilter('error') - - # Make sure that we get a pending deprecation warning - self.assertRaises(PendingDeprecationWarning, sys.evalfr, 1.) - - @unittest.skipIf(pysys.version_info < (3, 0), "test requires Python 3+") - def test_evalfr_dtime(self): - sys = TransferFunction([1., 3., 5], [1., 6., 2., -1], 0.1) - np.testing.assert_array_almost_equal(sys(1j), -0.5 - 0.5j) - - @unittest.skipIf(not slycot_check(), "slycot not installed") + with pytest.deprecated_call(): + np.testing.assert_allclose(sys.evalfr(omega), resp, atol=1e-3) + + # call above nyquist frequency + if dt: + with pytest.warns(UserWarning): + np.testing.assert_allclose(sys._evalfr(omega + 2 * np.pi / dt), + resp, + atol=1e-3) + + def test_is_static_gain(self): + numstatic = 1.1 + denstatic = 1.2 + numdynamic = [1, 1] + dendynamic = [2, 1] + numstaticmimo = [[[1.1,], [1.2,]], [[1.2,], [0.8,]]] + denstaticmimo = [[[1.9,], [1.2,]], [[1.2,], [0.8,]]] + numdynamicmimo = [[[1.1, 0.9], [1.2]], [[1.2], [0.8]]] + dendynamicmimo = [[[1.1, 0.7], [0.2]], [[1.2], [0.8]]] + assert TransferFunction(numstatic, denstatic).is_static_gain() + assert TransferFunction(numstaticmimo, denstaticmimo).is_static_gain() + + assert not TransferFunction(numstatic, dendynamic).is_static_gain() + assert not TransferFunction(numdynamic, dendynamic).is_static_gain() + assert not TransferFunction(numdynamic, denstatic).is_static_gain() + assert not TransferFunction(numstatic, dendynamic).is_static_gain() + + assert not TransferFunction(numstaticmimo, + dendynamicmimo).is_static_gain() + assert not TransferFunction(numdynamicmimo, + denstaticmimo).is_static_gain() + + + @slycotonly def test_evalfr_mimo(self): - """Evaluate the frequency response of a MIMO system at one frequency.""" - + """Evaluate the frequency response of a MIMO system at a freq""" num = [[[1., 2.], [0., 3.], [2., -1.]], [[1.], [4., 0.], [1., -4., 3.]]] den = [[[-3., 2., 4.], [1., 0., 0.], [2., -1.]], @@ -451,9 +450,7 @@ def test_evalfr_mimo(self): np.testing.assert_array_almost_equal(sys(2.j), resp) def test_freqresp_siso(self): - """Evaluate the magnitude and phase of a SISO system at - multiple frequencies.""" - + """Evaluate the SISO magnitude and phase at multiple frequencies""" sys = TransferFunction([1., 3., 5], [1., 6., 2., -1]) truemag = [[[4.63507337473906, 0.707106781186548, 0.0866592803995351]]] @@ -467,11 +464,9 @@ def test_freqresp_siso(self): np.testing.assert_array_almost_equal(phase, truephase) np.testing.assert_array_almost_equal(omega, trueomega) - @unittest.skipIf(not slycot_check(), "slycot not installed") + @slycotonly def test_freqresp_mimo(self): - """Evaluate the magnitude and phase of a MIMO system at - multiple frequencies.""" - + """Evaluate the MIMO magnitude and phase at multiple frequencies.""" num = [[[1., 2.], [0., 3.], [2., -1.]], [[1.], [4., 0.], [1., -4., 3.]]] den = [[[-3., 2., 4.], [1., 0., 0.], [2., -1.]], @@ -500,7 +495,6 @@ def test_freqresp_mimo(self): # Tests for TransferFunction.pole and TransferFunction.zero. def test_common_den(self): """ Test the helper function to compute common denomitators.""" - # _common_den() computes the common denominator per input/column. # The testing columns are: # 0: no common poles @@ -550,7 +544,6 @@ def test_common_den(self): def test_common_den_nonproper(self): """ Test _common_den with order(num)>order(den) """ - tf1 = TransferFunction( [[[1., 2., 3.]], [[1., 2.]]], [[[1., -2.]], [[1., -3.]]]) @@ -568,10 +561,9 @@ def test_common_den_nonproper(self): _, den2, _ = tf2._common_den(allow_nonproper=True) np.testing.assert_array_almost_equal(den2, common_den_ref) - @unittest.skipIf(not slycot_check(), "slycot not installed") + @slycotonly def test_pole_mimo(self): """Test for correct MIMO poles.""" - sys = TransferFunction( [[[1.], [1.]], [[1.], [1.]]], [[[1., 2.], [1., 3.]], [[1., 4., 4.], [1., 9., 14.]]]) @@ -596,7 +588,6 @@ def test_double_cancelling_poles_siso(self): # Tests for TransferFunction.feedback def test_feedback_siso(self): """Test for correct SISO transfer function feedback.""" - sys1 = TransferFunction([-1., 4.], [1., 3., 5.]) sys2 = TransferFunction([2., 3., 0.], [1., -3., 4., 0]) @@ -608,10 +599,9 @@ def test_feedback_siso(self): np.testing.assert_array_equal(sys4.num, [[[-1., 7., -16., 16., 0.]]]) np.testing.assert_array_equal(sys4.den, [[[1., 0., 2., -8., 8., 0.]]]) - @unittest.skipIf(not slycot_check(), "slycot not installed") + @slycotonly def test_convert_to_transfer_function(self): """Test for correct state space to transfer function conversion.""" - A = [[1., -2.], [-3., 4.]] B = [[6., 5.], [4., 3.]] C = [[1., -2.], [3., -4.], [5., -6.]] @@ -628,8 +618,10 @@ def test_convert_to_transfer_function(self): for i in range(sys.outputs): for j in range(sys.inputs): - np.testing.assert_array_almost_equal(tfsys.num[i][j], num[i][j]) - np.testing.assert_array_almost_equal(tfsys.den[i][j], den[i][j]) + np.testing.assert_array_almost_equal(tfsys.num[i][j], + num[i][j]) + np.testing.assert_array_almost_equal(tfsys.den[i][j], + den[i][j]) def test_minreal(self): """Try the minreal function, and also test easy entry by creation @@ -661,7 +653,6 @@ def test_minreal_3(self): g = TransferFunction([1,1],[1,1]).minreal() np.testing.assert_array_almost_equal(1.0, g.num[0][0]) np.testing.assert_array_almost_equal(1.0, g.den[0][0]) - np.testing.assert_equal(None, g.dt) def test_minreal_4(self): """Check minreal on discrete TFs.""" @@ -673,7 +664,7 @@ def test_minreal_4(self): np.testing.assert_array_almost_equal(hm.num[0][0], hr.num[0][0]) np.testing.assert_equal(hr.dt, hm.dt) - @unittest.skipIf(not slycot_check(), "slycot not installed") + @slycotonly def test_state_space_conversion_mimo(self): """Test conversion of a single input, two-output state-space system against the same TF""" @@ -695,8 +686,9 @@ def test_state_space_conversion_mimo(self): np.testing.assert_array_almost_equal(H.num[1][0], H2.num[1][0]) np.testing.assert_array_almost_equal(H.den[1][0], H2.den[1][0]) - @unittest.skipIf(not slycot_check(), "slycot not installed") + @slycotonly def test_indexing(self): + """Test TF scalar indexing and slice""" tm = ss2tf(rss(5, 3, 3)) # scalar indexing @@ -715,26 +707,64 @@ def test_indexing(self): np.testing.assert_array_almost_equal(sys.num[1][1], tm.num[1][2]) np.testing.assert_array_almost_equal(sys.den[1][1], tm.den[1][2]) - def test_matrix_multiply(self): - """MIMO transfer functions should be multiplyable by constant - matrices""" - s = TransferFunction([1, 0], [1]) - b0 = 0.2 - b1 = 0.1 - b2 = 0.5 - a0 = 2.3 - a1 = 6.3 - a2 = 3.6 - a3 = 1.0 - h = (b0 + b1*s + b2*s**2)/(a0 + a1*s + a2*s**2 + a3*s**3) - H = TransferFunction([[h.num[0][0]], [(h*s).num[0][0]]], - [[h.den[0][0]], [h.den[0][0]]]) - H1 = (np.matrix([[1.0, 0]])*H).minreal() - H2 = (np.matrix([[0, 1.0]])*H).minreal() - np.testing.assert_array_almost_equal(H.num[0][0], H1.num[0][0]) - np.testing.assert_array_almost_equal(H.den[0][0], H1.den[0][0]) - np.testing.assert_array_almost_equal(H.num[1][0], H2.num[0][0]) - np.testing.assert_array_almost_equal(H.den[1][0], H2.den[0][0]) + @pytest.mark.parametrize( + "matarrayin", + [pytest.param(np.array, + id="arrayin", + marks=[nopython2, + pytest.mark.skip(".__matmul__ not implemented")]), + pytest.param(np.matrix, + id="matrixin", + marks=matrixfilter)], + indirect=True) + @pytest.mark.parametrize("X_, ij", + [([[2., 0., ]], 0), + ([[0., 2., ]], 1)]) + def test_matrix_array_multiply(self, matarrayin, X_, ij): + """Test mulitplication of MIMO TF with matrix and matmul with array""" + # 2 inputs, 2 outputs with prime zeros so they do not cancel + n = 2 + p = [3, 5, 7, 11, 13, 17, 19, 23] + H = TransferFunction( + [[np.poly(p[2 * i + j:2 * i + j + 1]) for j in range(n)] + for i in range(n)], + [[[1, -1]] * n] * n) + + X = matarrayin(X_) + + if matarrayin is np.matrix: + XH = X * H + else: + # XH = X @ H + XH = np.matmul(X, H) + XH = XH.minreal() + assert XH.inputs == n + assert XH.outputs == X.shape[0] + assert len(XH.num) == XH.outputs + assert len(XH.den) == XH.outputs + assert len(XH.num[0]) == n + assert len(XH.den[0]) == n + np.testing.assert_allclose(2. * H.num[ij][0], XH.num[0][0], rtol=1e-4) + np.testing.assert_allclose( H.den[ij][0], XH.den[0][0], rtol=1e-4) + np.testing.assert_allclose(2. * H.num[ij][1], XH.num[0][1], rtol=1e-4) + np.testing.assert_allclose( H.den[ij][1], XH.den[0][1], rtol=1e-4) + + if matarrayin is np.matrix: + HXt = H * X.T + else: + # HXt = H @ X.T + HXt = np.matmul(H, X.T) + HXt = HXt.minreal() + assert HXt.inputs == X.T.shape[1] + assert HXt.outputs == n + assert len(HXt.num) == n + assert len(HXt.den) == n + assert len(HXt.num[0]) == HXt.inputs + assert len(HXt.den[0]) == HXt.inputs + np.testing.assert_allclose(2. * H.num[0][ij], HXt.num[0][0], rtol=1e-4) + np.testing.assert_allclose( H.den[0][ij], HXt.den[0][0], rtol=1e-4) + np.testing.assert_allclose(2. * H.num[1][ij], HXt.num[1][0], rtol=1e-4) + np.testing.assert_allclose( H.den[1][ij], HXt.den[1][0], rtol=1e-4) def test_dcgain_cont(self): """Test DC gain for continuous-time transfer functions""" @@ -765,14 +795,15 @@ def test_dcgain_discr(self): # differencer sys = TransferFunction(1, [1, -1], True) - np.testing.assert_equal(sys.dcgain(), np.inf) + with pytest.warns(RuntimeWarning, match="divide by zero"): + np.testing.assert_equal(sys.dcgain(), np.inf) # summer - # causes a RuntimeWarning due to the divide by zero sys = TransferFunction([1, -1], [1], True) np.testing.assert_equal(sys.dcgain(), 0) def test_ss2tf(self): + """Test SISO ss2tf""" A = np.array([[-4, -1], [-1, -4]]) B = np.array([[1], [3]]) C = np.array([[3, 1]]) @@ -782,79 +813,90 @@ def test_ss2tf(self): np.testing.assert_almost_equal(sys.num, true_sys.num) np.testing.assert_almost_equal(sys.den, true_sys.den) - def test_class_constants(self): - # Make sure that the 's' variable is defined properly + def test_class_constants_s(self): + """Make sure that the 's' variable is defined properly""" s = TransferFunction.s G = (s + 1)/(s**2 + 2*s + 1) np.testing.assert_array_almost_equal(G.num, [[[1, 1]]]) np.testing.assert_array_almost_equal(G.den, [[[1, 2, 1]]]) - self.assertTrue(isctime(G, strict=True)) + assert isctime(G, strict=True) - # Make sure that the 'z' variable is defined properly + def test_class_constants_z(self): + """Make sure that the 'z' variable is defined properly""" z = TransferFunction.z G = (z + 1)/(z**2 + 2*z + 1) np.testing.assert_array_almost_equal(G.num, [[[1, 1]]]) np.testing.assert_array_almost_equal(G.den, [[[1, 2, 1]]]) - self.assertTrue(isdtime(G, strict=True)) + assert isdtime(G, strict=True) def test_printing(self): - # SISO, continuous time + """Print SISO""" sys = ss2tf(rss(4, 1, 1)) - self.assertTrue(isinstance(str(sys), str)) - self.assertTrue(isinstance(sys._repr_latex_(), str)) + assert isinstance(str(sys), str) + assert isinstance(sys._repr_latex_(), str) # SISO, discrete time sys = sample_system(sys, 1) - self.assertTrue(isinstance(str(sys), str)) - self.assertTrue(isinstance(sys._repr_latex_(), str)) - - def test_printing_polynomial(self): - """Cover all _tf_polynomial_to_string code branches""" - # Note: the assertions below use plain assert statements instead of - # unittest methods so that debugging with pytest is easier - - assert str(TransferFunction([0], [1])) == "\n0\n-\n1\n" - assert str(TransferFunction([1.0001], [-1.1111])) == \ - "\n 1\n------\n-1.111\n" - assert str(TransferFunction([0, 1], [0, 1.])) == "\n1\n-\n1\n" - for var, dt, dtstring in zip(["s", "z", "z"], - [None, True, 1], - ['', '', '\ndt = 1\n']): - assert str(TransferFunction([1, 0], [2, 1], dt)) == \ - "\n {var}\n-------\n2 {var} + 1\n{dtstring}".format( - var=var, dtstring=dtstring) - assert str(TransferFunction([2, 0, -1], [1, 0, 0, 1.2], dt)) == \ - "\n2 {var}^2 - 1\n---------\n{var}^3 + 1.2\n{dtstring}".format( - var=var, dtstring=dtstring) - - @unittest.skipIf(not slycot_check(), "slycot not installed") + assert isinstance(str(sys), str) + assert isinstance(sys._repr_latex_(), str) + + @pytest.mark.parametrize( + "args, output", + [(([0], [1]), "\n0\n-\n1\n"), + (([1.0001], [-1.1111]), "\n 1\n------\n-1.111\n"), + (([0, 1], [0, 1.]), "\n1\n-\n1\n"), + ]) + def test_printing_polynomial_const(self, args, output): + """Test _tf_polynomial_to_string for constant systems""" + assert str(TransferFunction(*args)) == output + + @pytest.mark.parametrize( + "args, outputfmt", + [(([1, 0], [2, 1]), + "\n {var}\n-------\n2 {var} + 1\n{dtstring}"), + (([2, 0, -1], [1, 0, 0, 1.2]), + "\n2 {var}^2 - 1\n---------\n{var}^3 + 1.2\n{dtstring}")]) + @pytest.mark.parametrize("var, dt, dtstring", + [("s", None, ''), + ("z", True, ''), + ("z", 1, '\ndt = 1\n')]) + def test_printing_polynomial(self, args, outputfmt, var, dt, dtstring): + """Test _tf_polynomial_to_string for all other code branches""" + assert str(TransferFunction(*(args + (dt,)))) == \ + outputfmt.format(var=var, dtstring=dtstring) + + @slycotonly def test_printing_mimo(self): - # MIMO, continuous time + """Print MIMO, continuous time""" sys = ss2tf(rss(4, 2, 3)) - self.assertTrue(isinstance(str(sys), str)) - self.assertTrue(isinstance(sys._repr_latex_(), str)) + assert isinstance(str(sys), str) + assert isinstance(sys._repr_latex_(), str) - @unittest.skipIf(not slycot_check(), "slycot not installed") + @slycotonly def test_size_mismatch(self): + """Test size mismacht""" sys1 = ss2tf(rss(2, 2, 2)) # Different number of inputs sys2 = ss2tf(rss(3, 1, 2)) - self.assertRaises(ValueError, TransferFunction.__add__, sys1, sys2) + with pytest.raises(ValueError): + TransferFunction.__add__(sys1, sys2) # Different number of outputs sys2 = ss2tf(rss(3, 2, 1)) - self.assertRaises(ValueError, TransferFunction.__add__, sys1, sys2) + with pytest.raises(ValueError): + TransferFunction.__add__(sys1, sys2) # Inputs and outputs don't match - self.assertRaises(ValueError, TransferFunction.__mul__, sys2, sys1) + with pytest.raises(ValueError): + TransferFunction.__mul__(sys2, sys1) # Feedback mismatch (MIMO not implemented) - self.assertRaises(NotImplementedError, - TransferFunction.feedback, sys2, sys1) + with pytest.raises(NotImplementedError): + TransferFunction.feedback(sys2, sys1) def test_latex_repr(self): - """ Test latex printout for TransferFunction """ + """Test latex printout for TransferFunction""" Hc = TransferFunction([1e-5, 2e5, 3e-4], [1.2e34, 2.3e-4, 2.3e-45]) Hd = TransferFunction([1e-5, 2e5, 3e-4], @@ -873,67 +915,44 @@ def test_latex_repr(self): r'+ 0.00023 ' + var + ' ' r'+ 2.3 ' + expmul + ' 10^{-45}' r'}' + suffix + '$$') - self.assertEqual(H._repr_latex_(), ref) - - def test_repr(self): + assert H._repr_latex_() == ref + + @pytest.mark.parametrize( + "Hargs, ref", + [(([-1., 4.], [1., 3., 5.]), + "TransferFunction(array([-1., 4.]), array([1., 3., 5.]))"), + (([2., 3., 0.], [1., -3., 4., 0], 2.0), + "TransferFunction(array([2., 3., 0.])," + " array([ 1., -3., 4., 0.]), 2.0)"), + + (([[[0, 1], [2, 3]], [[4, 5], [6, 7]]], + [[[6, 7], [4, 5]], [[2, 3], [0, 1]]]), + "TransferFunction([[array([1]), array([2, 3])]," + " [array([4, 5]), array([6, 7])]]," + " [[array([6, 7]), array([4, 5])]," + " [array([2, 3]), array([1])]])"), + (([[[0, 1], [2, 3]], [[4, 5], [6, 7]]], + [[[6, 7], [4, 5]], [[2, 3], [0, 1]]], + 0.5), + "TransferFunction([[array([1]), array([2, 3])]," + " [array([4, 5]), array([6, 7])]]," + " [[array([6, 7]), array([4, 5])]," + " [array([2, 3]), array([1])]], 0.5)") + ]) + def test_repr(self, Hargs, ref): """Test __repr__ printout.""" - Hc = TransferFunction([-1., 4.], [1., 3., 5.]) - Hd = TransferFunction([2., 3., 0.], [1., -3., 4., 0], 2.0) - Hcm = TransferFunction( - [ [[0, 1], [2, 3]], [[4, 5], [6, 7]] ], - [ [[6, 7], [4, 5]], [[2, 3], [0, 1]] ]) - Hdm = TransferFunction( - [ [[0, 1], [2, 3]], [[4, 5], [6, 7]] ], - [ [[6, 7], [4, 5]], [[2, 3], [0, 1]] ], 0.5) - - refs = [ - "TransferFunction(array([-1., 4.]), array([1., 3., 5.]))", - "TransferFunction(array([2., 3., 0.])," - " array([ 1., -3., 4., 0.]), 2.0)", - "TransferFunction([[array([1]), array([2, 3])]," - " [array([4, 5]), array([6, 7])]]," - " [[array([6, 7]), array([4, 5])]," - " [array([2, 3]), array([1])]])", - "TransferFunction([[array([1]), array([2, 3])]," - " [array([4, 5]), array([6, 7])]]," - " [[array([6, 7]), array([4, 5])]," - " [array([2, 3]), array([1])]], 0.5)" ] - self.assertEqual(repr(Hc), refs[0]) - self.assertEqual(repr(Hd), refs[1]) - self.assertEqual(repr(Hcm), refs[2]) - self.assertEqual(repr(Hdm), refs[3]) + H = TransferFunction(*Hargs) + + assert repr(H) == ref # and reading back - array = np.array - for H in (Hc, Hd, Hcm, Hdm): - H2 = eval(H.__repr__()) - for p in range(len(H.num)): - for m in range(len(H.num[0])): - np.testing.assert_array_almost_equal( - H.num[p][m], H2.num[p][m]) - np.testing.assert_array_almost_equal( - H.den[p][m], H2.den[p][m]) - self.assertEqual(H.dt, H2.dt) - - def test_sample_system_prewarping(self): - """test that prewarping works when converting from cont to discrete time system""" - A = np.array([ - [ 0.00000000e+00, 1.00000000e+00, 0.00000000e+00, 0.00000000e+00], - [-3.81097561e+01, -1.12500000e+00, 0.00000000e+00, 0.00000000e+00], - [ 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 1.00000000e+00], - [ 0.00000000e+00, 0.00000000e+00, -1.66356135e+04, -1.34748470e+01]]) - B = np.array([ - [ 0. ], [ 38.1097561 ],[ 0. ],[16635.61352143]]) - C = np.array([[0.90909091, 0. , 0.09090909, 0. ],]) - wwarp = 50 - Ts = 0.025 - plant = StateSpace(A,B,C,0) - plant = ss2tf(plant) - plant_d_warped = plant.sample(Ts, 'bilinear', prewarp_frequency=wwarp) - np.testing.assert_array_almost_equal( - evalfr(plant, wwarp*1j), - evalfr(plant_d_warped, np.exp(wwarp*1j*Ts)), - decimal=4) + array = np.array # noqa + H2 = eval(H.__repr__()) + for p in range(len(H.num)): + for m in range(len(H.num[0])): + np.testing.assert_array_almost_equal(H.num[p][m], H2.num[p][m]) + np.testing.assert_array_almost_equal(H.den[p][m], H2.den[p][m]) + assert H.dt == H2.dt class TestLTIConverter: @@ -973,7 +992,3 @@ def test_returnScipySignalLTI_error(self, mimotf): mimotf.returnScipySignalLTI() with pytest.raises(ValueError): mimotf.returnScipySignalLTI(strict=True) - - -if __name__ == "__main__": - unittest.main() From 2b98769c9e51ca45291b39c6e5fa53f27a6357d9 Mon Sep 17 00:00:00 2001 From: bnavigator Date: Tue, 29 Dec 2020 23:54:54 +0100 Subject: [PATCH 30/30] make tests work with pre #431 source code state revert this commit when merging a rebased #431 (remove statesp_test.py::test_copy_constructor_nodt if not applicable) --- control/tests/config_test.py | 1 + control/tests/discrete_test.py | 75 +++++++++--------------------- control/tests/iosys_test.py | 30 ++++++------ control/tests/lti_test.py | 83 +--------------------------------- control/tests/statesp_test.py | 34 ++------------ control/tests/xferfcn_test.py | 1 + 6 files changed, 43 insertions(+), 181 deletions(-) diff --git a/control/tests/config_test.py b/control/tests/config_test.py index ede683fe1..3979ffca5 100644 --- a/control/tests/config_test.py +++ b/control/tests/config_test.py @@ -244,6 +244,7 @@ def test_change_default_dt(self, dt): # lambda t, x, u: x, inputs=1, outputs=1) # assert nlsys.dt == dt + @pytest.mark.skip("implemented in gh-431") def test_change_default_dt_static(self): """Test that static gain systems always have dt=None""" ct.set_defaults('control', default_dt=0) diff --git a/control/tests/discrete_test.py b/control/tests/discrete_test.py index ffdd1aeb4..7aee216d4 100644 --- a/control/tests/discrete_test.py +++ b/control/tests/discrete_test.py @@ -6,10 +6,9 @@ import numpy as np import pytest -from control import (StateSpace, TransferFunction, bode, common_timebase, - evalfr, feedback, forced_response, impulse_response, - isctime, isdtime, rss, sample_system, step_response, - timebase) +from control import StateSpace, TransferFunction, feedback, step_response, \ + isdtime, timebase, isctime, sample_system, bode, impulse_response, \ + evalfr, timebaseEqual, forced_response, rss class TestDiscrete: @@ -52,21 +51,13 @@ class Tsys: return T - def testCompatibleTimebases(self, tsys): - """test that compatible timebases don't throw errors and vice versa""" - common_timebase(tsys.siso_ss1.dt, tsys.siso_tf1.dt) - common_timebase(tsys.siso_ss1.dt, tsys.siso_ss1c.dt) - common_timebase(tsys.siso_ss1d.dt, tsys.siso_ss1.dt) - common_timebase(tsys.siso_ss1.dt, tsys.siso_ss1d.dt) - common_timebase(tsys.siso_ss1.dt, tsys.siso_ss1d.dt) - common_timebase(tsys.siso_ss1d.dt, tsys.siso_ss3d.dt) - common_timebase(tsys.siso_ss3d.dt, tsys.siso_ss1d.dt) - with pytest.raises(ValueError): - # cont + discrete - common_timebase(tsys.siso_ss1d.dt, tsys.siso_ss1c.dt) - with pytest.raises(ValueError): - # incompatible discrete - common_timebase(tsys.siso_ss1d.dt, tsys.siso_ss2d.dt) + def testTimebaseEqual(self, tsys): + """Test for equal timebases and not so equal ones""" + assert timebaseEqual(tsys.siso_ss1, tsys.siso_tf1) + assert timebaseEqual(tsys.siso_ss1, tsys.siso_ss1c) + assert not timebaseEqual(tsys.siso_ss1d, tsys.siso_ss1c) + assert not timebaseEqual(tsys.siso_ss1d, tsys.siso_ss2d) + assert not timebaseEqual(tsys.siso_ss1d, tsys.siso_ss3d) def testSystemInitialization(self, tsys): # Check to make sure systems are discrete time with proper variables @@ -84,18 +75,6 @@ def testSystemInitialization(self, tsys): assert tsys.siso_tf2d.dt == 0.2 assert tsys.siso_tf3d.dt is True - # keyword argument check - # dynamic systems - assert TransferFunction(1, [1, 1], dt=0.1).dt == 0.1 - assert TransferFunction(1, [1, 1], 0.1).dt == 0.1 - assert StateSpace(1,1,1,1, dt=0.1).dt == 0.1 - assert StateSpace(1,1,1,1, 0.1).dt == 0.1 - # static gain system, dt argument should still override default dt - assert TransferFunction(1, [1,], dt=0.1).dt == 0.1 - assert TransferFunction(1, [1,], 0.1).dt == 0.1 - assert StateSpace(0,0,1,1, dt=0.1).dt == 0.1 - assert StateSpace(0,0,1,1, 0.1).dt == 0.1 - def testCopyConstructor(self, tsys): for sys in (tsys.siso_ss1, tsys.siso_ss1c, tsys.siso_ss1d): newsys = StateSpace(sys) @@ -135,7 +114,6 @@ def test_timebase_conversions(self, tsys): assert timebase(tf1*tf2) == timebase(tf2) assert timebase(tf1*tf3) == timebase(tf3) assert timebase(tf1*tf4) == timebase(tf4) - assert timebase(tf3*tf4) == timebase(tf4) assert timebase(tf2*tf1) == timebase(tf2) assert timebase(tf3*tf1) == timebase(tf3) assert timebase(tf4*tf1) == timebase(tf4) @@ -150,36 +128,33 @@ def test_timebase_conversions(self, tsys): # Make sure discrete time without sampling is converted correctly assert timebase(tf3*tf3) == timebase(tf3) - assert timebase(tf3*tf4) == timebase(tf4) assert timebase(tf3+tf3) == timebase(tf3) - assert timebase(tf3+tf4) == timebase(tf4) assert timebase(feedback(tf3, tf3)) == timebase(tf3) - assert timebase(feedback(tf3, tf4)) == timebase(tf4) # Make sure all other combinations are errors - with pytest.raises(ValueError, match="incompatible timebases"): + with pytest.raises(ValueError, match="different sampling times"): tf2 * tf3 - with pytest.raises(ValueError, match="incompatible timebases"): + with pytest.raises(ValueError, match="different sampling times"): tf3 * tf2 - with pytest.raises(ValueError, match="incompatible timebases"): + with pytest.raises(ValueError, match="different sampling times"): tf2 * tf4 - with pytest.raises(ValueError, match="incompatible timebases"): + with pytest.raises(ValueError, match="different sampling times"): tf4 * tf2 - with pytest.raises(ValueError, match="incompatible timebases"): + with pytest.raises(ValueError, match="different sampling times"): tf2 + tf3 - with pytest.raises(ValueError, match="incompatible timebases"): + with pytest.raises(ValueError, match="different sampling times"): tf3 + tf2 - with pytest.raises(ValueError, match="incompatible timebases"): + with pytest.raises(ValueError, match="different sampling times"): tf2 + tf4 - with pytest.raises(ValueError, match="incompatible timebases"): + with pytest.raises(ValueError, match="different sampling times"): tf4 + tf2 - with pytest.raises(ValueError, match="incompatible timebases"): + with pytest.raises(ValueError, match="different sampling times"): feedback(tf2, tf3) - with pytest.raises(ValueError, match="incompatible timebases"): + with pytest.raises(ValueError, match="different sampling times"): feedback(tf3, tf2) - with pytest.raises(ValueError, match="incompatible timebases"): + with pytest.raises(ValueError, match="different sampling times"): feedback(tf2, tf4) - with pytest.raises(ValueError, match="incompatible timebases"): + with pytest.raises(ValueError, match="different sampling times"): feedback(tf4, tf2) def testisdtime(self, tsys): @@ -237,7 +212,6 @@ def testAddition(self, tsys): sys = tsys.siso_ss1c + tsys.siso_ss1c sys = tsys.siso_ss1d + tsys.siso_ss1d sys = tsys.siso_ss3d + tsys.siso_ss3d - sys = tsys.siso_ss1d + tsys.siso_ss3d with pytest.raises(ValueError): StateSpace.__add__(tsys.mimo_ss1c, tsys.mimo_ss1d) @@ -254,7 +228,6 @@ def testAddition(self, tsys): sys = tsys.siso_tf1c + tsys.siso_tf1c sys = tsys.siso_tf1d + tsys.siso_tf1d sys = tsys.siso_tf2d + tsys.siso_tf2d - sys = tsys.siso_tf1d + tsys.siso_tf3d with pytest.raises(ValueError): TransferFunction.__add__(tsys.siso_tf1c, tsys.siso_tf1d) @@ -279,7 +252,6 @@ def testMultiplication(self, tsys): sys = tsys.siso_ss1d * tsys.siso_ss1 sys = tsys.siso_ss1c * tsys.siso_ss1c sys = tsys.siso_ss1d * tsys.siso_ss1d - sys = tsys.siso_ss1d * tsys.siso_ss3d with pytest.raises(ValueError): StateSpace.__mul__(tsys.mimo_ss1c, tsys.mimo_ss1d) @@ -295,7 +267,6 @@ def testMultiplication(self, tsys): sys = tsys.siso_tf1d * tsys.siso_tf1 sys = tsys.siso_tf1c * tsys.siso_tf1c sys = tsys.siso_tf1d * tsys.siso_tf1d - sys = tsys.siso_tf1d * tsys.siso_tf3d with pytest.raises(ValueError): TransferFunction.__mul__(tsys.siso_tf1c, tsys.siso_tf1d) @@ -322,7 +293,6 @@ def testFeedback(self, tsys): sys = feedback(tsys.siso_ss1d, tsys.siso_ss1) sys = feedback(tsys.siso_ss1c, tsys.siso_ss1c) sys = feedback(tsys.siso_ss1d, tsys.siso_ss1d) - sys = feedback(tsys.siso_ss1d, tsys.siso_ss3d) with pytest.raises(ValueError): feedback(tsys.mimo_ss1c, tsys.mimo_ss1d) @@ -338,7 +308,6 @@ def testFeedback(self, tsys): sys = feedback(tsys.siso_tf1d, tsys.siso_tf1) sys = feedback(tsys.siso_tf1c, tsys.siso_tf1c) sys = feedback(tsys.siso_tf1d, tsys.siso_tf1d) - sys = feedback(tsys.siso_tf1d, tsys.siso_tf3d) with pytest.raises(ValueError): feedback(tsys.siso_tf1c, tsys.siso_tf1d) diff --git a/control/tests/iosys_test.py b/control/tests/iosys_test.py index 2bb6f066c..740416507 100644 --- a/control/tests/iosys_test.py +++ b/control/tests/iosys_test.py @@ -29,17 +29,17 @@ class TSys: """Return some test systems""" # Create a single input/single output linear system T.siso_linsys = ct.StateSpace( - [[-1, 1], [0, -2]], [[0], [1]], [[1, 0]], [[0]]) + [[-1, 1], [0, -2]], [[0], [1]], [[1, 0]], [[0]], 0) # Create a multi input/multi output linear system T.mimo_linsys1 = ct.StateSpace( [[-1, 1], [0, -2]], [[1, 0], [0, 1]], - [[1, 0], [0, 1]], np.zeros((2, 2))) + [[1, 0], [0, 1]], np.zeros((2, 2)), 0) # Create a multi input/multi output linear system T.mimo_linsys2 = ct.StateSpace( [[-1, 1], [0, -2]], [[0, 1], [1, 0]], - [[1, 0], [0, 1]], np.zeros((2, 2))) + [[1, 0], [0, 1]], np.zeros((2, 2)), 0) # Create simulation parameters T.T = np.linspace(0, 10, 100) @@ -281,7 +281,7 @@ def test_algebraic_loop(self, tsys): linsys = tsys.siso_linsys lnios = ios.LinearIOSystem(linsys) nlios = ios.NonlinearIOSystem(None, \ - lambda t, x, u, params: u*u, inputs=1, outputs=1) + lambda t, x, u, params: u*u, inputs=1, outputs=1, dt=0) nlios1 = nlios.copy() nlios2 = nlios.copy() @@ -310,7 +310,7 @@ def test_algebraic_loop(self, tsys): iosys = ios.InterconnectedSystem( (lnios, nlios), # linear system w/ nonlinear feedback ((1,), # feedback interconnection (sig to 0) - (0, (1, 0, -1))), + (0, (1, 0, -1))), 0, # input to linear system 0 # output from linear system ) @@ -331,7 +331,7 @@ def test_algebraic_loop(self, tsys): # Algebraic loop due to feedthrough term linsys = ct.StateSpace( - [[-1, 1], [0, -2]], [[0], [1]], [[1, 0]], [[1]]) + [[-1, 1], [0, -2]], [[0], [1]], [[1, 0]], [[1]], 0) lnios = ios.LinearIOSystem(linsys) iosys = ios.InterconnectedSystem( (nlios, lnios), # linear system w/ nonlinear feedback @@ -374,7 +374,7 @@ def test_rmul(self, tsys): # Also creates a nested interconnected system ioslin = ios.LinearIOSystem(tsys.siso_linsys) nlios = ios.NonlinearIOSystem(None, \ - lambda t, x, u, params: u*u, inputs=1, outputs=1) + lambda t, x, u, params: u*u, inputs=1, outputs=1, dt=0) sys1 = nlios * ioslin sys2 = ios.InputOutputSystem.__rmul__(nlios, sys1) @@ -414,7 +414,7 @@ def test_feedback(self, tsys): # Linear system with constant feedback (via "nonlinear" mapping) ioslin = ios.LinearIOSystem(tsys.siso_linsys) nlios = ios.NonlinearIOSystem(None, \ - lambda t, x, u, params: u, inputs=1, outputs=1) + lambda t, x, u, params: u, inputs=1, outputs=1, dt=0) iosys = ct.feedback(ioslin, nlios) linsys = ct.feedback(tsys.siso_linsys, 1) @@ -740,7 +740,7 @@ def test_named_signals(self, tsys): inputs = ('u[0]', 'u[1]'), outputs = ('y[0]', 'y[1]'), states = tsys.mimo_linsys1.states, - name = 'sys1') + name = 'sys1', dt=0) sys2 = ios.LinearIOSystem(tsys.mimo_linsys2, inputs = ('u[0]', 'u[1]'), outputs = ('y[0]', 'y[1]'), @@ -1015,7 +1015,7 @@ def test_duplicates(self, tsys): nlios = ios.NonlinearIOSystem(lambda t, x, u, params: x, lambda t, x, u, params: u * u, inputs=1, outputs=1, states=1, - name="sys") + name="sys", dt=0) # Duplicate objects with pytest.warns(UserWarning, match="Duplicate object"): @@ -1024,7 +1024,7 @@ def test_duplicates(self, tsys): # Nonduplicate objects nlios1 = nlios.copy() nlios2 = nlios.copy() - with pytest.warns(UserWarning, match="copy of sys") as record: + with pytest.warns(UserWarning, match="Duplicate name"): ios_series = nlios1 * nlios2 assert "copy of sys_1.x[0]" in ios_series.state_index.keys() assert "copy of sys.x[0]" in ios_series.state_index.keys() @@ -1033,10 +1033,10 @@ def test_duplicates(self, tsys): iosys_siso = ct.LinearIOSystem(tsys.siso_linsys) nlios1 = ios.NonlinearIOSystem(None, lambda t, x, u, params: u * u, - inputs=1, outputs=1, name="sys") + inputs=1, outputs=1, name="sys", dt=0) nlios2 = ios.NonlinearIOSystem(None, lambda t, x, u, params: u * u, - inputs=1, outputs=1, name="sys") + inputs=1, outputs=1, name="sys", dt=0) with pytest.warns(UserWarning, match="Duplicate name"): ct.InterconnectedSystem((nlios1, iosys_siso, nlios2), @@ -1045,10 +1045,10 @@ def test_duplicates(self, tsys): # Same system, different names => everything should be OK nlios1 = ios.NonlinearIOSystem(None, lambda t, x, u, params: u * u, - inputs=1, outputs=1, name="nlios1") + inputs=1, outputs=1, name="nlios1", dt=0) nlios2 = ios.NonlinearIOSystem(None, lambda t, x, u, params: u * u, - inputs=1, outputs=1, name="nlios2") + inputs=1, outputs=1, name="nlios2", dt=0) with pytest.warns(None) as record: ct.InterconnectedSystem((nlios1, iosys_siso, nlios2), inputs=0, outputs=0, states=0) diff --git a/control/tests/lti_test.py b/control/tests/lti_test.py index ee9d95a09..762e1435a 100644 --- a/control/tests/lti_test.py +++ b/control/tests/lti_test.py @@ -4,7 +4,7 @@ import pytest from control import c2d, tf, tf2ss, NonlinearIOSystem -from control.lti import (LTI, common_timebase, damp, dcgain, isctime, isdtime, +from control.lti import (LTI, damp, dcgain, isctime, isdtime, issiso, pole, timebaseEqual, zero) from control.tests.conftest import slycotonly @@ -72,84 +72,3 @@ def test_dcgain(self): sys = tf(84, [1, 2]) np.testing.assert_equal(sys.dcgain(), 42) np.testing.assert_equal(dcgain(sys), 42) - - @pytest.mark.parametrize("dt1, dt2, expected", - [(None, None, True), - (None, 0, True), - (None, 1, True), - pytest.param(None, True, True, - marks=pytest.mark.xfail( - reason="returns false")), - (0, 0, True), - (0, 1, False), - (0, True, False), - (1, 1, True), - (1, 2, False), - (1, True, False), - (True, True, True)]) - def test_timebaseEqual_deprecated(self, dt1, dt2, expected): - """Test that timbaseEqual throws a warning and returns as documented""" - sys1 = tf([1], [1, 2, 3], dt1) - sys2 = tf([1], [1, 4, 5], dt2) - - print(sys1.dt) - print(sys2.dt) - - with pytest.deprecated_call(): - assert timebaseEqual(sys1, sys2) is expected - # Make sure behaviour is symmetric - with pytest.deprecated_call(): - assert timebaseEqual(sys2, sys1) is expected - - @pytest.mark.parametrize("dt1, dt2, expected", - [(None, None, None), - (None, 0, 0), - (None, 1, 1), - (None, True, True), - (True, True, True), - (True, 1, 1), - (1, 1, 1), - (0, 0, 0), - ]) - @pytest.mark.parametrize("sys1", [True, False]) - @pytest.mark.parametrize("sys2", [True, False]) - def test_common_timebase(self, dt1, dt2, expected, sys1, sys2): - """Test that common_timbase adheres to :ref:`conventions-ref`""" - i1 = tf([1], [1, 2, 3], dt1) if sys1 else dt1 - i2 = tf([1], [1, 4, 5], dt2) if sys2 else dt2 - assert common_timebase(i1, i2) == expected - # Make sure behaviour is symmetric - assert common_timebase(i2, i1) == expected - - @pytest.mark.parametrize("i1, i2", - [(True, 0), - (0, 1), - (1, 2)]) - def test_common_timebase_errors(self, i1, i2): - """Test that common_timbase throws errors on invalid combinations""" - with pytest.raises(ValueError): - common_timebase(i1, i2) - # Make sure behaviour is symmetric - with pytest.raises(ValueError): - common_timebase(i2, i1) - - @pytest.mark.parametrize("dt, ref, strictref", - [(None, True, False), - (0, False, False), - (1, True, True), - (True, True, True)]) - @pytest.mark.parametrize("objfun, arg", - [(LTI, ()), - (NonlinearIOSystem, (lambda x: x, ))]) - def test_isdtime(self, objfun, arg, dt, ref, strictref): - """Test isdtime and isctime functions to follow convention""" - obj = objfun(*arg, dt=dt) - - assert isdtime(obj) == ref - assert isdtime(obj, strict=True) == strictref - - if dt is not None: - ref = not ref - strictref = not strictref - assert isctime(obj) == ref - assert isctime(obj, strict=True) == strictref diff --git a/control/tests/statesp_test.py b/control/tests/statesp_test.py index 9dbb6da94..c7b0a0aaf 100644 --- a/control/tests/statesp_test.py +++ b/control/tests/statesp_test.py @@ -80,16 +80,13 @@ def sys623(self): @pytest.mark.parametrize( "dt", - [(), (None, ), (0, ), (1, ), (0.1, ), (True, )], + [(None, ), (0, ), (1, ), (0.1, ), (True, )], ids=lambda i: "dt " + ("unspec" if len(i) == 0 else str(i[0]))) @pytest.mark.parametrize( "argfun", [pytest.param( lambda ABCDdt: (ABCDdt, {}), id="A, B, C, D[, dt]"), - pytest.param( - lambda ABCDdt: (ABCDdt[:4], {'dt': dt_ for dt_ in ABCDdt[4:]}), - id="A, B, C, D[, dt=dt]"), pytest.param( lambda ABCDdt: ((StateSpace(*ABCDdt), ), {}), id="sys") @@ -109,7 +106,7 @@ def test_constructor(self, sys322ABCD, dt, argfun): @pytest.mark.parametrize("args, exc, errmsg", [((True, ), TypeError, "(can only take in|sys must be) a StateSpace"), - ((1, 2), ValueError, "1, 4, or 5 arguments"), + ((1, 2), ValueError, "1 or 4 arguments"), ((np.ones((3, 2)), np.ones((3, 2)), np.ones((2, 2)), np.ones((2, 2))), ValueError, "A must be square"), @@ -133,16 +130,6 @@ def test_constructor_invalid(self, args, exc, errmsg): with pytest.raises(exc, match=errmsg): ss(*args) - def test_constructor_warns(self, sys322ABCD): - """Test ambiguos input to StateSpace() constructor""" - with pytest.warns(UserWarning, match="received multiple dt"): - sys = StateSpace(*(sys322ABCD + (0.1, )), dt=0.2) - np.testing.assert_almost_equal(sys.A, sys322ABCD[0]) - np.testing.assert_almost_equal(sys.B, sys322ABCD[1]) - np.testing.assert_almost_equal(sys.C, sys322ABCD[2]) - np.testing.assert_almost_equal(sys.D, sys322ABCD[3]) - assert sys.dt == 0.1 - def test_copy_constructor(self): """Test the copy constructor""" # Create a set of matrices for a simple linear system @@ -164,22 +151,6 @@ def test_copy_constructor(self): linsys.A[0, 0] = -3 np.testing.assert_array_equal(cpysys.A, [[-1]]) # original value - def test_copy_constructor_nodt(self, sys322): - """Test the copy constructor when an object without dt is passed - - FIXME: may be obsolete in case gh-431 is updated - """ - sysin = sample_system(sys322, 1.) - del sysin.dt - sys = StateSpace(sysin) - assert sys.dt == defaults['control.default_dt'] - - # test for static gain - sysin = StateSpace([], [], [], [[1, 2], [3, 4]], 1.) - del sysin.dt - sys = StateSpace(sysin) - assert sys.dt is None - def test_matlab_style_constructor(self): """Use (deprecated) matrix-style construction string""" with pytest.deprecated_call(): @@ -382,6 +353,7 @@ def test_freq_resp(self): np.testing.assert_almost_equal(phase, true_phase) np.testing.assert_equal(omega, true_omega) + @pytest.mark.skip("is_static_gain is introduced in gh-431") def test_is_static_gain(self): A0 = np.zeros((2,2)) A1 = A0.copy() diff --git a/control/tests/xferfcn_test.py b/control/tests/xferfcn_test.py index b0673de1e..62c4bfb23 100644 --- a/control/tests/xferfcn_test.py +++ b/control/tests/xferfcn_test.py @@ -409,6 +409,7 @@ def test_evalfr_siso(self, dt, omega, resp): resp, atol=1e-3) + @pytest.mark.skip("is_static_gain is introduced in gh-431") def test_is_static_gain(self): numstatic = 1.1 denstatic = 1.2 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