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/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() 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() diff --git a/control/tests/config_test.py b/control/tests/config_test.py index a8e86d1ff..3979ffca5 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,28 @@ 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 + + @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) + 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 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/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() 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() 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() diff --git a/control/tests/discrete_test.py b/control/tests/discrete_test.py index 9c1928dab..7aee216d4 100644 --- a/control/tests/discrete_test.py +++ b/control/tests/discrete_test.py @@ -1,351 +1,383 @@ -#!/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 +import pytest + from control import StateSpace, TransferFunction, feedback, step_response, \ isdtime, timebase, isctime, sample_system, bode, impulse_response, \ - timebaseEqual, forced_response -from control import matlab + evalfr, timebaseEqual, forced_response, rss -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 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 - 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 + + 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(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+tf3) == timebase(tf3) + assert timebase(feedback(tf3, tf3)) == timebase(tf3) # 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="different sampling times"): + tf2 * tf3 + with pytest.raises(ValueError, match="different sampling times"): + tf3 * tf2 + with pytest.raises(ValueError, match="different sampling times"): + tf2 * tf4 + with pytest.raises(ValueError, match="different sampling times"): + tf4 * tf2 + with pytest.raises(ValueError, match="different sampling times"): + tf2 + tf3 + with pytest.raises(ValueError, match="different sampling times"): + tf3 + tf2 + with pytest.raises(ValueError, match="different sampling times"): + tf2 + tf4 + with pytest.raises(ValueError, match="different sampling times"): + tf4 + tf2 + with pytest.raises(ValueError, match="different sampling times"): + feedback(tf2, tf3) + with pytest.raises(ValueError, match="different sampling times"): + feedback(tf3, tf2) + with pytest.raises(ValueError, match="different sampling times"): + feedback(tf2, tf4) + with pytest.raises(ValueError, match="different sampling times"): + 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 + + 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 + + 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 + + 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 + + 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) + + 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) + + 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 +391,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 +415,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() 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() 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 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 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 diff --git a/control/tests/iosys_test.py b/control/tests/iosys_test.py index 20f289d8c..740416507 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( - [[-1, 1], [0, -2]], [[0], [1]], [[1, 0]], [[0]]) + T.siso_linsys = ct.StateSpace( + [[-1, 1], [0, -2]], [[0], [1]], [[1, 0]], [[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)), 0) # 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)), 0) # 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,19 +273,20 @@ 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) + lambda t, x, u, params: u*u, inputs=1, outputs=1, dt=0) nlios1 = nlios.copy() 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) @@ -308,7 +310,7 @@ def test_algebraic_loop(self): 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 ) @@ -324,11 +326,12 @@ 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( - [[-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 @@ -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,20 +362,19 @@ 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) + lambda t, x, u, params: u*u, inputs=1, outputs=1, dt=0) sys1 = nlios * ioslin sys2 = ios.InputOutputSystem.__rmul__(nlios, sys1) @@ -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) + lambda t, x, u, params: u, inputs=1, outputs=1, dt=0) 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, - name = 'sys1') - sys2 = ios.LinearIOSystem(self.mimo_linsys2, + states = tsys.mimo_linsys1.states, + name = 'sys1', dt=0) + 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", dt=0) # 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="Duplicate name"): 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", dt=0) + nlios2 = ios.NonlinearIOSystem(None, + lambda t, x, u, params: u * u, + inputs=1, outputs=1, name="sys", dt=0) + + 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", dt=0) + nlios2 = ios.NonlinearIOSystem(None, + lambda t, x, u, params: u * u, + 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) + 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]]) diff --git a/control/tests/lti_test.py b/control/tests/lti_test.py index ed832fb05..762e1435a 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, 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. @@ -69,7 +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) - - -if __name__ == "__main__": - unittest.main() 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/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) 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() 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() 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) + 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) 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() diff --git a/control/tests/pzmap_test.py b/control/tests/pzmap_test.py old mode 100755 new mode 100644 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) 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() 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() 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) 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) 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 57% rename from control/tests/statesp_matrix_test.py rename to control/tests/statesp_test.py index e7e91364a..c7b0a0aaf 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,100 @@ 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: ((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 or 4 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_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_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 +185,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 +200,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 +216,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 +249,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 +265,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 +281,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 +353,29 @@ 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") + @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() + 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 +390,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 +464,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 +481,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 +491,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 +561,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 +593,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 +636,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 +649,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 +660,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 +702,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 +826,3 @@ def test_returnScipySignalLTI_error(self, mimoss): with pytest.raises(ValueError): mimoss.returnScipySignalLTI(strict=True) - -if __name__ == "__main__": - unittest.main() 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 diff --git a/control/tests/xferfcn_input_test.py b/control/tests/xferfcn_input_test.py index 52fb85c29..995f6ac03 100644 --- 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) diff --git a/control/tests/xferfcn_test.py b/control/tests/xferfcn_test.py index 17e602090..62c4bfb23 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,100 @@ 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) + + @pytest.mark.skip("is_static_gain is introduced in gh-431") + 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 +451,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 +465,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 +496,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 +545,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 +562,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 +589,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 +600,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 +619,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 +654,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 +665,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 +687,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 +708,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 +796,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 +814,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 +916,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 +993,3 @@ def test_returnScipySignalLTI_error(self, mimotf): mimotf.returnScipySignalLTI() with pytest.raises(ValueError): mimotf.returnScipySignalLTI(strict=True) - - -if __name__ == "__main__": - unittest.main() 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