diff --git a/control/tests/canonical_test.py b/control/tests/canonical_test.py index ecdaa04cb..63afd51c3 100644 --- a/control/tests/canonical_test.py +++ b/control/tests/canonical_test.py @@ -4,8 +4,6 @@ import pytest import scipy.linalg -from control.tests.conftest import slycotonly - from control import ss, tf, tf2ss from control.canonical import canonical_form, reachable_form, \ observable_form, modal_form, similarity_transform, bdschur @@ -244,7 +242,7 @@ def block_diag_from_eig(eigvals): return scipy.linalg.block_diag(*blocks) -@slycotonly +@pytest.mark.slycot @pytest.mark.parametrize( "eigvals, condmax, blksizes", [ @@ -269,7 +267,7 @@ def test_bdschur_ref(eigvals, condmax, blksizes): np.testing.assert_array_almost_equal(solve(t, a) @ t, b) -@slycotonly +@pytest.mark.slycot @pytest.mark.parametrize( "eigvals, sorted_blk_eigvals, sort", [ @@ -300,7 +298,7 @@ def test_bdschur_sort(eigvals, sorted_blk_eigvals, sort): blk_eigval.imag) -@slycotonly +@pytest.mark.slycot def test_bdschur_defective(): # the eigenvalues of this simple defective matrix cannot be separated # a previous version of the bdschur would fail on this @@ -323,14 +321,14 @@ def test_bdschur_condmax_lt_1(): bdschur(1, condmax=np.nextafter(1, 0)) -@slycotonly +@pytest.mark.slycot def test_bdschur_invalid_sort(): # sort must be in ('continuous', 'discrete') with pytest.raises(ValueError): bdschur(1, sort='no-such-sort') -@slycotonly +@pytest.mark.slycot @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 @@ -390,7 +388,7 @@ def test_modal_form(A_true, B_true, C_true, D_true): C @ np.linalg.matrix_power(A, i) @ B) -@slycotonly +@pytest.mark.slycot @pytest.mark.parametrize( "condmax, len_blksizes", [(1.1, 1), @@ -409,7 +407,7 @@ def test_modal_form_condmax(condmax, len_blksizes): np.testing.assert_array_almost_equal(zsys.D, xsys.D) -@slycotonly +@pytest.mark.slycot @pytest.mark.parametrize( "sys_type", ['continuous', diff --git a/control/tests/conftest.py b/control/tests/conftest.py index c10dcc225..d055690d1 100644 --- a/control/tests/conftest.py +++ b/control/tests/conftest.py @@ -6,13 +6,26 @@ import control +def pytest_runtest_setup(item): + if not control.exception.slycot_check(): + if any(mark.name == 'slycot' + for mark in item.iter_markers()): + pytest.skip("slycot not installed") + elif any(mark.name == 'noslycot' + for mark in item.iter_markers()): + # used, e.g., for tests checking ControlSlycot + pytest.skip("slycot installed") + + if (not control.exception.cvxopt_check() + and any(mark.name == 'cvxopt' + for mark in item.iter_markers())): + pytest.skip("cvxopt not installed") + + if (not control.exception.pandas_check() + and any(mark.name == 'pandas' + for mark in item.iter_markers())): + pytest.skip("pandas not installed") -# 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") -cvxoptonly = pytest.mark.skipif( - not control.exception.cvxopt_check(), reason="cvxopt not installed") @pytest.fixture(scope="session", autouse=True) diff --git a/control/tests/convert_test.py b/control/tests/convert_test.py index 7975bbe5a..9cdabbe6c 100644 --- a/control/tests/convert_test.py +++ b/control/tests/convert_test.py @@ -21,17 +21,13 @@ from control import rss, ss, ss2tf, tf, tf2ss from control.statefbk import ctrb, obsv from control.freqplot import bode -from control.exception import slycot_check, ControlMIMONotImplemented -from control.tests.conftest import slycotonly +from control.exception import ControlMIMONotImplemented # 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 @@ -50,8 +46,13 @@ def printSys(self, sys, ind): @pytest.mark.usefixtures("legacy_plot_signature") @pytest.mark.parametrize("states", range(1, maxStates)) - @pytest.mark.parametrize("inputs", range(1, maxIO)) - @pytest.mark.parametrize("outputs", range(1, maxIO)) + # If slycot is not installed, just check SISO + @pytest.mark.parametrize("inputs", + [1] + [pytest.param(i, marks=pytest.mark.slycot) + for i in range(2, 5)]) + @pytest.mark.parametrize("outputs", + [1] + [pytest.param(i, marks=pytest.mark.slycot) + for i in range(2, 5)]) def testConvert(self, fixedseed, states, inputs, outputs): """Test state space to transfer function conversion. @@ -148,7 +149,11 @@ def testConvert(self, fixedseed, states, inputs, outputs): np.testing.assert_array_almost_equal( ssorig_imag, tfxfrm_imag, decimal=5) - def testConvertMIMO(self): + + @pytest.mark.parametrize('have_slycot', + [pytest.param(True, marks=pytest.mark.slycot), + pytest.param(False, marks=pytest.mark.noslycot)]) + def testConvertMIMO(self, have_slycot): """Test state space to transfer function conversion. Do a MIMO conversion and make sure that it is processed @@ -166,7 +171,7 @@ def testConvertMIMO(self): [0.008, 1.39, 48.78]]]) # Convert to state space and look for an error - if (not slycot_check()): + if not have_slycot: with pytest.raises(ControlMIMONotImplemented): tf2ss(tsys) else: @@ -214,7 +219,7 @@ def testSs2tfStaticMimo(self): np.testing.assert_allclose(numref, np.array(gtf.num) / np.array(gtf.den)) - @slycotonly + @pytest.mark.slycot def testTf2SsDuplicatePoles(self): """Tests for 'too few poles for MIMO tf gh-111'""" num = [[[1], [0]], @@ -225,7 +230,7 @@ def testTf2SsDuplicatePoles(self): s = ss(g) np.testing.assert_allclose(g.poles(), s.poles()) - @slycotonly + @pytest.mark.slycot def test_tf2ss_robustness(self): """Unit test to make sure that tf2ss is working correctly. gh-240""" num = [ [[0], [1]], [[1], [0]] ] diff --git a/control/tests/frd_test.py b/control/tests/frd_test.py index 1b370c629..ab8ce3be6 100644 --- a/control/tests/frd_test.py +++ b/control/tests/frd_test.py @@ -12,8 +12,6 @@ from control.xferfcn import TransferFunction from control.frdata import frd, _convert_to_frd, FrequencyResponseData from control import bdalg, freqplot -from control.tests.conftest import slycotonly -from control.exception import pandas_check class TestFRD: @@ -567,7 +565,7 @@ def test_mul_mimo_siso(self, left, right, expected): np.testing.assert_array_almost_equal(expected_frd.omega, result.omega) np.testing.assert_array_almost_equal(expected_frd.frdata, result.frdata) - @slycotonly + @pytest.mark.slycot def test_truediv_mimo_siso(self): omega = np.logspace(-1, 1, 10) tf_mimo = TransferFunction([1], [1, 0]) * np.eye(2) @@ -592,7 +590,7 @@ def test_truediv_mimo_siso(self): np.testing.assert_array_almost_equal(expected.omega, result.omega) np.testing.assert_array_almost_equal(expected.frdata, result.frdata) - @slycotonly + @pytest.mark.slycot def test_rtruediv_mimo_siso(self): omega = np.logspace(-1, 1, 10) tf_mimo = TransferFunction([1], [1, 0]) * np.eye(2) @@ -821,7 +819,7 @@ def test_named_signals(): assert f1.output_labels == ['y0'] -@pytest.mark.skipif(not pandas_check(), reason="pandas not installed") +@pytest.mark.pandas def test_to_pandas(): # Create a SISO frequency response h1 = TransferFunction([1], [1, 2, 2]) diff --git a/control/tests/freqresp_test.py b/control/tests/freqresp_test.py index a268d38eb..5112a99e9 100644 --- a/control/tests/freqresp_test.py +++ b/control/tests/freqresp_test.py @@ -19,7 +19,6 @@ singular_values_plot, singular_values_response) from control.matlab import bode, rss, ss, tf from control.statesp import StateSpace -from control.tests.conftest import slycotonly from control.xferfcn import TransferFunction pytestmark = pytest.mark.usefixtures("mplcleanup") @@ -61,7 +60,7 @@ def test_freqresp_siso(ss_siso): @pytest.mark.filterwarnings(r"ignore:freqresp\(\) is deprecated") -@slycotonly +@pytest.mark.slycot def test_freqresp_mimo_legacy(ss_mimo): """Test MIMO frequency response calls""" omega = np.linspace(10e-2, 10e2, 1000) @@ -70,7 +69,7 @@ def test_freqresp_mimo_legacy(ss_mimo): ctrl.freqresp(tf_mimo, omega) -@slycotonly +@pytest.mark.slycot def test_freqresp_mimo(ss_mimo): """Test MIMO frequency response calls""" omega = np.linspace(10e-2, 10e2, 1000) diff --git a/control/tests/interconnect_test.py b/control/tests/interconnect_test.py index aea3cbbc6..ccce76f34 100644 --- a/control/tests/interconnect_test.py +++ b/control/tests/interconnect_test.py @@ -56,14 +56,12 @@ def test_summation_exceptions(): ct.summing_junction('u', 'y', dimension=False) -@pytest.mark.parametrize("dim", [1, 3]) +@pytest.mark.parametrize("dim", + [1, pytest.param(3, marks=pytest.mark.slycot)]) def test_interconnect_implicit(dim): """Test the use of implicit connections in interconnect()""" import random - if dim != 1 and not ct.slycot_check(): - pytest.xfail("slycot not installed") - # System definition P = ct.rss(2, dim, dim, strictly_proper=True, name='P') diff --git a/control/tests/lti_test.py b/control/tests/lti_test.py index 17dc7796e..dd95f3505 100644 --- a/control/tests/lti_test.py +++ b/control/tests/lti_test.py @@ -8,9 +8,7 @@ import control as ct from control import NonlinearIOSystem, c2d, common_timebase, isctime, \ isdtime, issiso, ss, tf, tf2ss -from control.exception import slycot_check from control.lti import LTI, bandwidth, damp, dcgain, evalfr, poles, zeros -from control.tests.conftest import slycotonly class TestLTI: @@ -59,7 +57,7 @@ def test_issiso(self): assert issiso(sys) assert issiso(sys, strict=True) - @slycotonly + @pytest.mark.slycot def test_issiso_mimo(self): # MIMO transfer function sys = tf([[[-1, 41], [1]], [[1, 2], [3, 4]]], @@ -190,6 +188,10 @@ def test_isdtime(self, objfun, arg, dt, ref, strictref): assert isctime(obj) == ref assert isctime(obj, strict=True) == strictref + def p(*args): + # convenience for parametrize below + return pytest.param(*args, marks=pytest.mark.slycot) + @pytest.mark.usefixtures("editsdefaults") @pytest.mark.parametrize("fcn", [ct.ss, ct.tf, ct.frd]) @pytest.mark.parametrize("nstate, nout, ninp, omega, squeeze, shape", [ @@ -202,26 +204,26 @@ def test_isdtime(self, objfun, arg, dt, ref, strictref): [3, 1, 1, 0.1, False, (1, 1)], [3, 1, 1, [0.1], False, (1, 1, 1)], [3, 1, 1, [0.1, 1, 10], False, (1, 1, 3)], - [1, 2, 1, 0.1, None, (2, 1)], # SIMO - [1, 2, 1, [0.1], None, (2, 1, 1)], - [1, 2, 1, [0.1, 1, 10], None, (2, 1, 3)], - [2, 2, 1, 0.1, True, (2,)], - [2, 2, 1, [0.1], True, (2,)], - [3, 2, 1, 0.1, False, (2, 1)], - [3, 2, 1, [0.1], False, (2, 1, 1)], - [3, 2, 1, [0.1, 1, 10], False, (2, 1, 3)], - [1, 1, 2, [0.1, 1, 10], None, (1, 2, 3)], # MISO - [2, 1, 2, [0.1, 1, 10], True, (2, 3)], - [3, 1, 2, [0.1, 1, 10], False, (1, 2, 3)], - [1, 1, 2, 0.1, None, (1, 2)], - [1, 1, 2, 0.1, True, (2,)], - [1, 1, 2, 0.1, False, (1, 2)], - [1, 2, 2, [0.1, 1, 10], None, (2, 2, 3)], # MIMO - [2, 2, 2, [0.1, 1, 10], True, (2, 2, 3)], - [3, 2, 2, [0.1, 1, 10], False, (2, 2, 3)], - [1, 2, 2, 0.1, None, (2, 2)], - [2, 2, 2, 0.1, True, (2, 2)], - [3, 2, 2, 0.1, False, (2, 2)], + p(1, 2, 1, 0.1, None, (2, 1)), + p(1, 2, 1, [0.1], None, (2, 1, 1)), + p(1, 2, 1, [0.1, 1, 10], None, (2, 1, 3)), + p(2, 2, 1, 0.1, True, (2,)), + p(2, 2, 1, [0.1], True, (2,)), + p(3, 2, 1, 0.1, False, (2, 1)), + p(3, 2, 1, [0.1], False, (2, 1, 1)), + p(3, 2, 1, [0.1, 1, 10], False, (2, 1, 3)), + p(1, 1, 2, [0.1, 1, 10], None, (1, 2, 3)), # MISO + p(2, 1, 2, [0.1, 1, 10], True, (2, 3)), + p(3, 1, 2, [0.1, 1, 10], False, (1, 2, 3)), + p(1, 1, 2, 0.1, None, (1, 2)), + p(1, 1, 2, 0.1, True, (2,)), + p(1, 1, 2, 0.1, False, (1, 2)), + p(1, 2, 2, [0.1, 1, 10], None, (2, 2, 3)), # MIMO + p(2, 2, 2, [0.1, 1, 10], True, (2, 2, 3)), + p(3, 2, 2, [0.1, 1, 10], False, (2, 2, 3)), + p(1, 2, 2, 0.1, None, (2, 2)), + p(2, 2, 2, 0.1, True, (2, 2)), + p(3, 2, 2, 0.1, False, (2, 2)), ]) @pytest.mark.parametrize("omega_type", ["numpy", "native"]) def test_squeeze(self, fcn, nstate, nout, ninp, omega, squeeze, shape, @@ -230,9 +232,6 @@ def test_squeeze(self, fcn, nstate, nout, ninp, omega, squeeze, shape, # Create the system to be tested if fcn == ct.frd: sys = fcn(ct.rss(nstate, nout, ninp), [1e-2, 1e-1, 1, 1e1, 1e2]) - elif fcn == ct.tf and (nout > 1 or ninp > 1) and not slycot_check(): - pytest.skip("Conversion of MIMO systems to transfer functions " - "requires slycot.") else: sys = fcn(ct.rss(nstate, nout, ninp)) diff --git a/control/tests/margin_test.py b/control/tests/margin_test.py index 679c1c685..c8be4ee6c 100644 --- a/control/tests/margin_test.py +++ b/control/tests/margin_test.py @@ -15,7 +15,6 @@ from control import ControlMIMONotImplemented, FrequencyResponseData, \ StateSpace, TransferFunction, margin, phase_crossover_frequencies, \ stability_margins, disk_margins, tf, ss -from control.exception import slycot_check s = TransferFunction.s @@ -394,6 +393,7 @@ def test_siso_disk_margin(): DM = disk_margins(L, omega, skew=1.0)[0] assert_allclose([DM], [SM], atol=0.01) +@pytest.mark.slycot def test_mimo_disk_margin(): # Frequencies of interest omega = np.logspace(-1, 3, 1001) @@ -404,23 +404,32 @@ def test_mimo_disk_margin(): Lo = P * K # loop transfer function, broken at plant output Li = K * P # loop transfer function, broken at plant input - if slycot_check(): - # Balanced (S - T) disk-based stability margins at plant output + # Balanced (S - T) disk-based stability margins at plant output + DMo, DGMo, DPMo = disk_margins(Lo, omega, skew=0.0) + assert_allclose([DMo], [0.3754], atol=0.1) # disk margin of 0.3754 + assert_allclose([DGMo], [3.3], atol=0.1) # disk-based gain margin of 3.3 dB + assert_allclose([DPMo], [21.26], atol=0.1) # disk-based phase margin of 21.26 deg + + # Balanced (S - T) disk-based stability margins at plant input + DMi, DGMi, DPMi = disk_margins(Li, omega, skew=0.0) + assert_allclose([DMi], [0.3754], atol=0.1) # disk margin of 0.3754 + assert_allclose([DGMi], [3.3], atol=0.1) # disk-based gain margin of 3.3 dB + assert_allclose([DPMi], [21.26], atol=0.1) # disk-based phase margin of 21.26 deg + + +@pytest.mark.noslycot +def test_mimo_disk_margin_exception(): + # Slycot not installed. Should throw exception. + # Frequencies of interest + omega = np.logspace(-1, 3, 1001) + + # Loop transfer gain + P = ss([[0, 10], [-10, 0]], np.eye(2), [[1, 10], [-10, 1]], 0) # plant + K = ss([], [], [], [[1, -2], [0, 1]]) # controller + Lo = P * K # loop transfer function, broken at plant output + with pytest.raises(ControlMIMONotImplemented,\ + match="Need slycot to compute MIMO disk_margins"): DMo, DGMo, DPMo = disk_margins(Lo, omega, skew=0.0) - assert_allclose([DMo], [0.3754], atol=0.1) # disk margin of 0.3754 - assert_allclose([DGMo], [3.3], atol=0.1) # disk-based gain margin of 3.3 dB - assert_allclose([DPMo], [21.26], atol=0.1) # disk-based phase margin of 21.26 deg - - # Balanced (S - T) disk-based stability margins at plant input - DMi, DGMi, DPMi = disk_margins(Li, omega, skew=0.0) - assert_allclose([DMi], [0.3754], atol=0.1) # disk margin of 0.3754 - assert_allclose([DGMi], [3.3], atol=0.1) # disk-based gain margin of 3.3 dB - assert_allclose([DPMi], [21.26], atol=0.1) # disk-based phase margin of 21.26 deg - else: - # Slycot not installed. Should throw exception. - with pytest.raises(ControlMIMONotImplemented,\ - match="Need slycot to compute MIMO disk_margins"): - DMo, DGMo, DPMo = disk_margins(Lo, omega, skew=0.0) def test_siso_disk_margin_return_all(): # Frequencies of interest @@ -439,6 +448,8 @@ def test_siso_disk_margin_return_all(): assert_allclose([DPM[np.argmin(DM)]], [25.8],\ atol=0.1) # disk-based phase margin of 25.8 deg + +@pytest.mark.slycot def test_mimo_disk_margin_return_all(): # Frequencies of interest omega = np.logspace(-1, 3, 1001) @@ -450,29 +461,23 @@ def test_mimo_disk_margin_return_all(): Lo = P * K # loop transfer function, broken at plant output Li = K * P # loop transfer function, broken at plant input - if slycot_check(): - # Balanced (S - T) disk-based stability margins at plant output - DMo, DGMo, DPMo = disk_margins(Lo, omega, skew=0.0, returnall=True) - assert_allclose([omega[np.argmin(DMo)]], [omega[0]],\ - atol=0.01) # sensitivity peak at 0 rad/s (or smallest provided) - assert_allclose([min(DMo)], [0.3754], atol=0.1) # disk margin of 0.3754 - assert_allclose([DGMo[np.argmin(DMo)]], [3.3],\ - atol=0.1) # disk-based gain margin of 3.3 dB - assert_allclose([DPMo[np.argmin(DMo)]], [21.26],\ - atol=0.1) # disk-based phase margin of 21.26 deg - - # Balanced (S - T) disk-based stability margins at plant input - DMi, DGMi, DPMi = disk_margins(Li, omega, skew=0.0, returnall=True) - assert_allclose([omega[np.argmin(DMi)]], [omega[0]],\ - atol=0.01) # sensitivity peak at 0 rad/s (or smallest provided) - assert_allclose([min(DMi)], [0.3754],\ - atol=0.1) # disk margin of 0.3754 - assert_allclose([DGMi[np.argmin(DMi)]], [3.3],\ - atol=0.1) # disk-based gain margin of 3.3 dB - assert_allclose([DPMi[np.argmin(DMi)]], [21.26],\ - atol=0.1) # disk-based phase margin of 21.26 deg - else: - # Slycot not installed. Should throw exception. - with pytest.raises(ControlMIMONotImplemented,\ - match="Need slycot to compute MIMO disk_margins"): - DMo, DGMo, DPMo = disk_margins(Lo, omega, skew=0.0, returnall=True) + # Balanced (S - T) disk-based stability margins at plant output + DMo, DGMo, DPMo = disk_margins(Lo, omega, skew=0.0, returnall=True) + assert_allclose([omega[np.argmin(DMo)]], [omega[0]],\ + atol=0.01) # sensitivity peak at 0 rad/s (or smallest provided) + assert_allclose([min(DMo)], [0.3754], atol=0.1) # disk margin of 0.3754 + assert_allclose([DGMo[np.argmin(DMo)]], [3.3],\ + atol=0.1) # disk-based gain margin of 3.3 dB + assert_allclose([DPMo[np.argmin(DMo)]], [21.26],\ + atol=0.1) # disk-based phase margin of 21.26 deg + + # Balanced (S - T) disk-based stability margins at plant input + DMi, DGMi, DPMi = disk_margins(Li, omega, skew=0.0, returnall=True) + assert_allclose([omega[np.argmin(DMi)]], [omega[0]],\ + atol=0.01) # sensitivity peak at 0 rad/s (or smallest provided) + assert_allclose([min(DMi)], [0.3754],\ + atol=0.1) # disk margin of 0.3754 + assert_allclose([DGMi[np.argmin(DMi)]], [3.3],\ + atol=0.1) # disk-based gain margin of 3.3 dB + assert_allclose([DPMi[np.argmin(DMi)]], [21.26],\ + atol=0.1) # disk-based phase margin of 21.26 deg diff --git a/control/tests/mateqn_test.py b/control/tests/mateqn_test.py index 0ae5a7db2..77bf553bf 100644 --- a/control/tests/mateqn_test.py +++ b/control/tests/mateqn_test.py @@ -39,56 +39,58 @@ import pytest from scipy.linalg import eigvals, solve -import control as ct from control.mateqn import lyap, dlyap, care, dare -from control.exception import ControlArgument, ControlDimension, slycot_check -from control.tests.conftest import slycotonly +from control.exception import ControlArgument, ControlDimension class TestMatrixEquations: """These are tests for the matrix equation solvers in mateqn.py""" - def test_lyap(self): + @pytest.mark.parametrize('method', + ['scipy', + pytest.param('slycot', marks=pytest.mark.slycot)]) + def test_lyap(self, method): A = array([[-1, 1], [-1, 0]]) Q = array([[1, 0], [0, 1]]) - X = lyap(A, Q) + X = lyap(A, Q, method=method) # print("The solution obtained is ", X) assert_array_almost_equal(A @ X + X @ A.T + Q, zeros((2,2))) A = array([[1, 2], [-3, -4]]) Q = array([[3, 1], [1, 1]]) - X = lyap(A,Q) + X = lyap(A,Q, method=method) # print("The solution obtained is ", X) assert_array_almost_equal(A @ X + X @ A.T + Q, zeros((2,2))) # Compare methods - if slycot_check(): + if method == 'slycot': X_scipy = lyap(A, Q, method='scipy') - X_slycot = lyap(A, Q, method='slycot') - assert_array_almost_equal(X_scipy, X_slycot) + assert_array_almost_equal(X_scipy, X) - def test_lyap_sylvester(self): + @pytest.mark.parametrize('method', + ['scipy', + pytest.param('slycot', marks=pytest.mark.slycot)]) + def test_lyap_sylvester(self, method): A = 5 B = array([[4, 3], [4, 3]]) C = array([2, 1]) - X = lyap(A, B, C) + X = lyap(A, B, C, method=method) # print("The solution obtained is ", X) assert_array_almost_equal(A * X + X @ B + C, zeros((1,2))) A = array([[2, 1], [1, 2]]) B = array([[1, 2], [0.5, 0.1]]) C = array([[1, 0], [0, 1]]) - X = lyap(A, B, C) + X = lyap(A, B, C, method=method) # print("The solution obtained is ", X) assert_array_almost_equal(A @ X + X @ B + C, zeros((2,2))) # Compare methods - if slycot_check(): + if method=='slycot': X_scipy = lyap(A, B, C, method='scipy') - X_slycot = lyap(A, B, C, method='slycot') - assert_array_almost_equal(X_scipy, X_slycot) + assert_array_almost_equal(X_scipy, X) - @slycotonly + @pytest.mark.slycot def test_lyap_g(self): A = array([[-1, 2], [-3, -4]]) Q = array([[3, 1], [1, 1]]) @@ -102,20 +104,28 @@ def test_lyap_g(self): with pytest.raises(ControlArgument, match="'scipy' not valid"): X = lyap(A, Q, None, E, method='scipy') - def test_dlyap(self): + @pytest.mark.parametrize('method', + ['scipy', + pytest.param('slycot', marks=pytest.mark.slycot)]) + def test_dlyap(self, method): A = array([[-0.6, 0],[-0.1, -0.4]]) Q = array([[1,0],[0,1]]) - X = dlyap(A,Q) + X = dlyap(A,Q,method=method) # print("The solution obtained is ", X) assert_array_almost_equal(A @ X @ A.T - X + Q, zeros((2,2))) A = array([[-0.6, 0],[-0.1, -0.4]]) Q = array([[3, 1],[1, 1]]) - X = dlyap(A,Q) + X = dlyap(A,Q,method=method) # print("The solution obtained is ", X) assert_array_almost_equal(A @ X @ A.T - X + Q, zeros((2,2))) - @slycotonly + # Compare methods + if method=='slycot': + X_scipy = dlyap(A,Q, method='scipy') + assert_array_almost_equal(X_scipy, X) + + @pytest.mark.slycot def test_dlyap_g(self): A = array([[-0.6, 0],[-0.1, -0.4]]) Q = array([[3, 1],[1, 1]]) @@ -129,7 +139,7 @@ def test_dlyap_g(self): with pytest.raises(ControlArgument, match="'scipy' not valid"): X = dlyap(A, Q, None, E, method='scipy') - @slycotonly + @pytest.mark.slycot def test_dlyap_sylvester(self): A = 5 B = array([[4, 3], [4, 3]]) @@ -149,12 +159,15 @@ def test_dlyap_sylvester(self): with pytest.raises(ControlArgument, match="'scipy' not valid"): X = dlyap(A, B, C, method='scipy') - def test_care(self): + @pytest.mark.parametrize('method', + ['scipy', + pytest.param('slycot', marks=pytest.mark.slycot)]) + def test_care(self, method): A = array([[-2, -1],[-1, -1]]) Q = array([[0, 0],[0, 1]]) B = array([[1, 0],[0, 4]]) - X, L, G = care(A, B, Q) + X, L, G = care(A, B, Q, method=method) # print("The solution obtained is", X) M = A.T @ X + X @ A - X @ B @ B.T @ X + Q assert_array_almost_equal(M, @@ -162,14 +175,16 @@ def test_care(self): assert_array_almost_equal(B.T @ X, G) # Compare methods - if slycot_check(): + if method == 'slycot': X_scipy, L_scipy, G_scipy = care(A, B, Q, method='scipy') - X_slycot, L_slycot, G_slycot = care(A, B, Q, method='slycot') - assert_array_almost_equal(X_scipy, X_slycot) - assert_array_almost_equal(np.sort(L_scipy), np.sort(L_slycot)) - assert_array_almost_equal(G_scipy, G_slycot) - - def test_care_g(self): + assert_array_almost_equal(X_scipy, X) + assert_array_almost_equal(np.sort(L_scipy), np.sort(L)) + assert_array_almost_equal(G_scipy, G) + + @pytest.mark.parametrize('method', + ['scipy', + pytest.param('slycot', marks=pytest.mark.slycot)]) + def test_care_g(self, method): A = array([[-2, -1],[-1, -1]]) Q = array([[0, 0],[0, 1]]) B = array([[1, 0],[0, 4]]) @@ -177,7 +192,7 @@ def test_care_g(self): S = array([[0, 0],[0, 0]]) E = array([[2, 1],[1, 2]]) - X,L,G = care(A,B,Q,R,S,E) + X,L,G = care(A,B,Q,R,S,E,method=method) # print("The solution obtained is", X) Gref = solve(R, B.T @ X @ E + S.T) assert_array_almost_equal(Gref, G) @@ -187,16 +202,17 @@ def test_care_g(self): zeros((2,2))) # Compare methods - if slycot_check(): + if method=='slycot': X_scipy, L_scipy, G_scipy = care( A, B, Q, R, S, E, method='scipy') - X_slycot, L_slycot, G_slycot = care( - A, B, Q, R, S, E, method='slycot') - assert_array_almost_equal(X_scipy, X_slycot) - assert_array_almost_equal(np.sort(L_scipy), np.sort(L_slycot)) - assert_array_almost_equal(G_scipy, G_slycot) - - def test_care_g2(self): + assert_array_almost_equal(X_scipy, X) + assert_array_almost_equal(np.sort(L_scipy), np.sort(L)) + assert_array_almost_equal(G_scipy, G) + + @pytest.mark.parametrize('method', + ['scipy', + pytest.param('slycot', marks=pytest.mark.slycot)]) + def test_care_g2(self, method): A = array([[-2, -1],[-1, -1]]) Q = array([[0, 0],[0, 1]]) B = array([[1],[0]]) @@ -204,7 +220,7 @@ def test_care_g2(self): S = array([[1],[0]]) E = array([[2, 1],[1, 2]]) - X,L,G = care(A,B,Q,R,S,E) + X,L,G = care(A,B,Q,R,S,E,method=method) # print("The solution obtained is", X) Gref = 1/R * (B.T @ X @ E + S.T) assert_array_almost_equal( @@ -214,22 +230,23 @@ def test_care_g2(self): assert_array_almost_equal(Gref , G) # Compare methods - if slycot_check(): + if method=='slycot': X_scipy, L_scipy, G_scipy = care( A, B, Q, R, S, E, method='scipy') - X_slycot, L_slycot, G_slycot = care( - A, B, Q, R, S, E, method='slycot') - assert_array_almost_equal(X_scipy, X_slycot) - assert_array_almost_equal(L_scipy, L_slycot) - assert_array_almost_equal(G_scipy, G_slycot) - - def test_dare(self): + assert_array_almost_equal(X_scipy, X) + assert_array_almost_equal(L_scipy, L) + assert_array_almost_equal(G_scipy, G) + + @pytest.mark.parametrize('method', + ['scipy', + pytest.param('slycot', marks=pytest.mark.slycot)]) + def test_dare(self, method): A = array([[-0.6, 0],[-0.1, -0.4]]) Q = array([[2, 1],[1, 0]]) B = array([[2, 1],[0, 1]]) R = array([[1, 0],[0, 1]]) - X, L, G = dare(A, B, Q, R) + X, L, G = dare(A, B, Q, R, method=method) # print("The solution obtained is", X) Gref = solve(B.T @ X @ B + R, B.T @ X @ A) assert_array_almost_equal(Gref, G) @@ -244,7 +261,7 @@ def test_dare(self): B = array([[1],[0]]) R = 2 - X, L, G = dare(A, B, Q, R) + X, L, G = dare(A, B, Q, R, method=method) # print("The solution obtained is", X) AtXA = A.T @ X @ A AtXB = A.T @ X @ B @@ -257,6 +274,7 @@ def test_dare(self): lam = eigvals(A - B @ G) assert_array_less(abs(lam), 1.0) + @pytest.mark.slycot def test_dare_compare(self): A = np.array([[-0.6, 0], [-0.1, -0.4]]) Q = np.array([[2, 1], [1, 0]]) @@ -268,15 +286,16 @@ def test_dare_compare(self): # Solve via scipy X_scipy, L_scipy, G_scipy = dare(A, B, Q, R, method='scipy') - # Solve via slycot - if ct.slycot_check(): - X_slicot, L_slicot, G_slicot = dare( - A, B, Q, R, S, E, method='scipy') - np.testing.assert_almost_equal(X_scipy, X_slicot) - np.testing.assert_almost_equal(L_scipy, L_slicot) - np.testing.assert_almost_equal(G_scipy, G_slicot) + X_slicot, L_slicot, G_slicot = dare( + A, B, Q, R, S, E, method='scipy') + np.testing.assert_almost_equal(X_scipy, X_slicot) + np.testing.assert_almost_equal(L_scipy, L_slicot) + np.testing.assert_almost_equal(G_scipy, G_slicot) - def test_dare_g(self): + @pytest.mark.parametrize('method', + ['scipy', + pytest.param('slycot', marks=pytest.mark.slycot)]) + def test_dare_g(self, method): A = array([[-0.6, 0],[-0.1, -0.4]]) Q = array([[2, 1],[1, 3]]) B = array([[1, 5],[2, 4]]) @@ -284,7 +303,7 @@ def test_dare_g(self): S = array([[1, 0],[2, 0]]) 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, method=method) # print("The solution obtained is", X) Gref = solve(B.T @ X @ B + R, B.T @ X @ A + S.T) assert_array_almost_equal(Gref, G) @@ -294,8 +313,18 @@ def test_dare_g(self): # check for stable closed loop lam = eigvals(A - B @ G, E) assert_array_less(abs(lam), 1.0) - - def test_dare_g2(self): + # Compare methods + if method=='slycot': + X_scipy, L_scipy, G_scipy = dare( + A, B, Q, R, S, E, method='scipy') + assert_array_almost_equal(X_scipy, X) + assert_array_almost_equal(L_scipy, L) + assert_array_almost_equal(G_scipy, G) + + @pytest.mark.parametrize('method', + ['scipy', + pytest.param('slycot', marks=pytest.mark.slycot)]) + def test_dare_g2(self, method): A = array([[-0.6, 0], [-0.1, -0.4]]) Q = array([[2, 1], [1, 3]]) B = array([[1], [2]]) @@ -303,7 +332,7 @@ def test_dare_g2(self): 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, method=method) # print("The solution obtained is", X) AtXA = A.T @ X @ A AtXB = A.T @ X @ B @@ -317,6 +346,13 @@ def test_dare_g2(self): lam = eigvals(A - B @ G, E) assert_array_less(abs(lam), 1.0) + if method=='slycot': + X_scipy, L_scipy, G_scipy = dare( + A, B, Q, R, S, E, method='scipy') + assert_array_almost_equal(X_scipy, X) + assert_array_almost_equal(L_scipy, L) + assert_array_almost_equal(G_scipy, G) + def test_raise(self): """ Test exception raise for invalid inputs """ diff --git a/control/tests/matlab_test.py b/control/tests/matlab_test.py index c6a45e2a2..d1a71bce3 100644 --- a/control/tests/matlab_test.py +++ b/control/tests/matlab_test.py @@ -30,7 +30,6 @@ from control.exception import ControlArgument from control.frdata import FRD -from control.tests.conftest import slycotonly # for running these through Matlab or Octave ''' @@ -487,21 +486,21 @@ def testEvalfr_mimo(self, mimo): ref = np.array([[44.8 - 21.4j, 0.], [0., 44.8 - 21.4j]]) np.testing.assert_array_almost_equal(fr, ref) - @slycotonly + @pytest.mark.slycot def testHsvd(self, siso): """Call hsvd()""" hsvd(siso.ss1) hsvd(siso.ss2) hsvd(siso.ss3) - @slycotonly + @pytest.mark.slycot def testBalred(self, siso): """Call balred()""" balred(siso.ss1, 1) balred(siso.ss2, 2) balred(siso.ss3, [2, 2]) - @slycotonly + @pytest.mark.slycot def testModred(self, siso): """Call modred()""" modred(siso.ss1, [1]) @@ -509,7 +508,7 @@ def testModred(self, siso): modred(siso.ss1, [1], 'matchdc') modred(siso.ss1, [1], 'truncate') - @slycotonly + @pytest.mark.slycot def testPlace_varga(self, siso): """Call place_varga()""" place_varga(siso.ss1.A, siso.ss1.B, [-2, -2]) @@ -552,7 +551,7 @@ def testObsv(self, siso): obsv(siso.ss1.A, siso.ss1.C) obsv(siso.ss2.A, siso.ss2.C) - @slycotonly + @pytest.mark.slycot def testGram(self, siso): """Call gram()""" gram(siso.ss1, 'c') @@ -696,7 +695,7 @@ def testFRD(self): frd2 = frd(frd1.frdata[0, 0, :], omega) assert isinstance(frd2, FRD) - @slycotonly + @pytest.mark.slycot def testMinreal(self, verbose=False): """Test a minreal model reduction""" # A = [-2, 0.5, 0; 0.5, -0.3, 0; 0, 0, -0.1] diff --git a/control/tests/minreal_test.py b/control/tests/minreal_test.py index 10c56d4ca..e8223184c 100644 --- a/control/tests/minreal_test.py +++ b/control/tests/minreal_test.py @@ -11,7 +11,6 @@ from control.statesp import StateSpace from control.xferfcn import TransferFunction from itertools import permutations -from control.tests.conftest import slycotonly @pytest.fixture @@ -19,7 +18,7 @@ def fixedseed(scope="class"): np.random.seed(5) -@slycotonly +@pytest.mark.slycot @pytest.mark.usefixtures("fixedseed") class TestMinreal: """Tests for the StateSpace class.""" diff --git a/control/tests/modelsimp_test.py b/control/tests/modelsimp_test.py index e09446073..c2773231b 100644 --- a/control/tests/modelsimp_test.py +++ b/control/tests/modelsimp_test.py @@ -14,13 +14,12 @@ from control.exception import ControlArgument, ControlDimension from control.modelsimp import balred, eigensys_realization, hsvd, markov, \ modred -from control.tests.conftest import slycotonly class TestModelsimp: """Test model reduction functions""" - @slycotonly + @pytest.mark.slycot def testHSVD(self): A = np.array([[1., -2.], [3., -4.]]) B = np.array([[5.], [7.]]) @@ -390,7 +389,7 @@ def testModredTruncate(self): np.testing.assert_array_almost_equal(rsys.D, Drtrue) - @slycotonly + @pytest.mark.slycot def testBalredTruncate(self): # controlable canonical realization computed in matlab for the transfer # function: @@ -431,7 +430,7 @@ def testBalredTruncate(self): np.testing.assert_array_almost_equal(Cr, Crtrue, decimal=4) np.testing.assert_array_almost_equal(Dr, Drtrue, decimal=4) - @slycotonly + @pytest.mark.slycot def testBalredMatchDC(self): # controlable canonical realization computed in matlab for the transfer # function: diff --git a/control/tests/namedio_test.py b/control/tests/namedio_test.py index ad74d27ba..8c44f5980 100644 --- a/control/tests/namedio_test.py +++ b/control/tests/namedio_test.py @@ -79,21 +79,26 @@ def test_named_ss(): } +def p(*args): + # convenience for parametrize below + return pytest.param(*args, marks=pytest.mark.slycot) + + @pytest.mark.parametrize("fun, args, kwargs", [ - [ct.rss, (4, 1, 1), {}], - [ct.rss, (3, 2, 1), {}], - [ct.drss, (4, 1, 1), {}], - [ct.drss, (3, 2, 1), {}], + p(ct.rss, (4, 1, 1), {}), + p(ct.rss, (3, 2, 1), {}), + p(ct.drss, (4, 1, 1), {}), + p(ct.drss, (3, 2, 1), {}), [ct.FRD, ([1, 2, 3,], [1, 2, 3]), {}], [ct.NonlinearIOSystem, (lambda t, x, u, params: -x, None), {'inputs': 2, 'outputs':2, 'states':2}], - [ct.ss, ([[1, 2], [3, 4]], [[0], [1]], [[1, 0]], 0), {}], - [ct.ss, ([], [], [], 3), {}], # static system - [ct.StateSpace, ([[1, 2], [3, 4]], [[0], [1]], [[1, 0]], 0), {}], - [ct.tf, ([1, 2], [3, 4, 5]), {}], - [ct.tf, (2, 3), {}], # static system - [ct.TransferFunction, ([1, 2], [3, 4, 5]), {}], + p(ct.ss, ([[1, 2], [3, 4]], [[0], [1]], [[1, 0]], 0), {}), + p(ct.ss, ([], [], [], 3), {}), # static system + p(ct.StateSpace, ([[1, 2], [3, 4]], [[0], [1]], [[1, 0]], 0), {}), + p(ct.tf, ([1, 2], [3, 4, 5]), {}), + p(ct.tf, (2, 3), {}), # static system + p(ct.TransferFunction, ([1, 2], [3, 4, 5]), {}), ]) def test_io_naming(fun, args, kwargs): # Reset the ID counter to get uniform generic names @@ -164,8 +169,8 @@ def test_io_naming(fun, args, kwargs): # # Convert the system to state space and make sure labels transfer # - if ct.slycot_check() and not isinstance( - sys_r, (ct.FrequencyResponseData, ct.NonlinearIOSystem)): + if not isinstance(sys_r, + (ct.FrequencyResponseData, ct.NonlinearIOSystem)): sys_ss = ct.ss(sys_r) assert sys_ss != sys_r assert sys_ss.input_labels == input_labels @@ -184,9 +189,8 @@ def test_io_naming(fun, args, kwargs): # # Convert the system to a transfer function and make sure labels transfer # - if not isinstance( - sys_r, (ct.FrequencyResponseData, ct.NonlinearIOSystem)) and \ - ct.slycot_check(): + if not isinstance(sys_r, + (ct.FrequencyResponseData, ct.NonlinearIOSystem)): sys_tf = ct.tf(sys_r) assert sys_tf != sys_r assert sys_tf.input_labels == input_labels @@ -202,9 +206,8 @@ def test_io_naming(fun, args, kwargs): # # Convert the system to a StateSpace and make sure labels transfer # - if not isinstance( - sys_r, (ct.FrequencyResponseData, ct.NonlinearIOSystem)) and \ - ct.slycot_check(): + if not isinstance(sys_r, + (ct.FrequencyResponseData, ct.NonlinearIOSystem)): sys_lio = ct.ss(sys_r) assert sys_lio != sys_r assert sys_lio.input_labels == input_labels diff --git a/control/tests/optimal_test.py b/control/tests/optimal_test.py index fa8fcb941..fb3f4e716 100644 --- a/control/tests/optimal_test.py +++ b/control/tests/optimal_test.py @@ -12,7 +12,6 @@ import control as ct import control.optimal as opt import control.flatsys as flat -from control.tests.conftest import slycotonly from numpy.lib import NumpyVersion @@ -103,7 +102,7 @@ def test_finite_horizon_simple(method): # optimal control problem with terminal cost set to LQR "cost to go" # gives the same answer as LQR. # -@slycotonly +@pytest.mark.slycot def test_discrete_lqr(): # oscillator model defined in 2D # Source: https://www.mpt3.org/UI/RegulationProblem diff --git a/control/tests/passivity_test.py b/control/tests/passivity_test.py index 4d7c8e6eb..22b73e0da 100644 --- a/control/tests/passivity_test.py +++ b/control/tests/passivity_test.py @@ -5,10 +5,9 @@ import pytest import numpy from control import ss, passivity, tf, sample_system, parallel, feedback -from control.tests.conftest import cvxoptonly from control.exception import ControlArgument, ControlDimension -pytestmark = cvxoptonly +pytestmark = pytest.mark.cvxopt def test_ispassive_ctime(): diff --git a/control/tests/robust_test.py b/control/tests/robust_test.py index fc9c9570d..8434ea6cd 100644 --- a/control/tests/robust_test.py +++ b/control/tests/robust_test.py @@ -5,12 +5,11 @@ from control import append, minreal, ss, tf from control.robust import augw, h2syn, hinfsyn, mixsyn -from control.tests.conftest import slycotonly class TestHinf: - @slycotonly + @pytest.mark.slycot def testHinfsyn(self): """Test hinfsyn""" p = ss(-1, [[1, 1]], [[1], [1]], [[0, 1], [1, 0]]) @@ -32,7 +31,7 @@ def testHinfsyn(self): class TestH2: - @slycotonly + @pytest.mark.slycot def testH2syn(self): """Test h2syn""" p = ss(-1, [[1, 1]], [[1], [1]], [[0, 1], [1, 0]]) @@ -71,7 +70,7 @@ def siso_almost_equal(self, g, h): "sys 2:\n" "{}".format(maxnum, g, h)) - @slycotonly + @pytest.mark.slycot def testSisoW1(self): """SISO plant with S weighting""" g = ss([-1.], [1.], [1.], [1.]) @@ -88,7 +87,7 @@ def testSisoW1(self): # u->v should be -g self.siso_almost_equal(-g, p[1, 1]) - @slycotonly + @pytest.mark.slycot def testSisoW2(self): """SISO plant with KS weighting""" g = ss([-1.], [1.], [1.], [1.]) @@ -105,7 +104,7 @@ def testSisoW2(self): # u->v should be -g self.siso_almost_equal(-g, p[1, 1]) - @slycotonly + @pytest.mark.slycot def testSisoW3(self): """SISO plant with T weighting""" g = ss([-1.], [1.], [1.], [1.]) @@ -122,7 +121,7 @@ def testSisoW3(self): # u->v should be -g self.siso_almost_equal(-g, p[1, 1]) - @slycotonly + @pytest.mark.slycot def testSisoW123(self): """SISO plant with all weights""" g = ss([-1.], [1.], [1.], [1.]) @@ -149,7 +148,7 @@ def testSisoW123(self): # u->v should be -g self.siso_almost_equal(-g, p[3, 1]) - @slycotonly + @pytest.mark.slycot def testMimoW1(self): """MIMO plant with S weighting""" g = ss([[-1., -2], [-3, -4]], @@ -181,7 +180,7 @@ def testMimoW1(self): self.siso_almost_equal(-g[1, 0], p[3, 2]) self.siso_almost_equal(-g[1, 1], p[3, 3]) - @slycotonly + @pytest.mark.slycot def testMimoW2(self): """MIMO plant with KS weighting""" g = ss([[-1., -2], [-3, -4]], @@ -213,7 +212,7 @@ def testMimoW2(self): self.siso_almost_equal(-g[1, 0], p[3, 2]) self.siso_almost_equal(-g[1, 1], p[3, 3]) - @slycotonly + @pytest.mark.slycot def testMimoW3(self): """MIMO plant with T weighting""" g = ss([[-1., -2], [-3, -4]], @@ -245,7 +244,7 @@ def testMimoW3(self): self.siso_almost_equal(-g[1, 0], p[3, 2]) self.siso_almost_equal(-g[1, 1], p[3, 3]) - @slycotonly + @pytest.mark.slycot def testMimoW123(self): """MIMO plant with all weights""" g = ss([[-1., -2], [-3, -4]], @@ -307,7 +306,7 @@ def testMimoW123(self): self.siso_almost_equal(-g[1, 0], p[7, 2]) self.siso_almost_equal(-g[1, 1], p[7, 3]) - @slycotonly + @pytest.mark.slycot def testErrors(self): """Error cases handled""" from control import augw, ss @@ -330,7 +329,7 @@ class TestMixsyn: """Test control.robust.mixsyn""" # it's a relatively simple wrapper; compare results with augw, hinfsyn - @slycotonly + @pytest.mark.slycot def testSiso(self): """mixsyn with SISO system""" # Skogestad+Postlethwaite, Multivariable Feedback Control, 1st Ed., Example 2.11 diff --git a/control/tests/slycot_convert_test.py b/control/tests/slycot_convert_test.py index 25beeb908..2739a4cf1 100644 --- a/control/tests/slycot_convert_test.py +++ b/control/tests/slycot_convert_test.py @@ -7,7 +7,6 @@ import pytest from control import bode, rss, ss, tf -from control.tests.conftest import slycotonly numTests = 5 maxStates = 10 @@ -21,7 +20,7 @@ def fixedseed(): np.random.seed(0) -@slycotonly +@pytest.mark.slycot @pytest.mark.usefixtures("fixedseed") class TestSlycot: """Test Slycot system conversion diff --git a/control/tests/statefbk_test.py b/control/tests/statefbk_test.py index 3f4b4849a..97cf7be68 100644 --- a/control/tests/statefbk_test.py +++ b/control/tests/statefbk_test.py @@ -12,11 +12,10 @@ import control as ct from control import poles, rss, ss, tf from control.exception import ControlDimension, ControlSlycot, \ - ControlArgument, slycot_check + ControlArgument from control.mateqn import care, dare from control.statefbk import (ctrb, obsv, place, place_varga, lqr, dlqr, gram, place_acker) -from control.tests.conftest import slycotonly @pytest.fixture @@ -128,7 +127,7 @@ def testCtrbObsvDuality(self): Wo = np.transpose(obsv(A, C)) np.testing.assert_array_almost_equal(Wc,Wo) - @slycotonly + @pytest.mark.slycot def testGramWc(self): A = np.array([[1., -2.], [3., -4.]]) B = np.array([[5., 6.], [7., 8.]]) @@ -144,7 +143,7 @@ def testGramWc(self): Wc = gram(sysd, 'c') np.testing.assert_array_almost_equal(Wc, Wctrue) - @slycotonly + @pytest.mark.slycot def testGramWc2(self): A = np.array([[1., -2.], [3., -4.]]) B = np.array([[5.], [7.]]) @@ -161,7 +160,7 @@ def testGramWc2(self): Wc = gram(sysd, 'c') np.testing.assert_array_almost_equal(Wc, Wctrue) - @slycotonly + @pytest.mark.slycot def testGramRc(self): A = np.array([[1., -2.], [3., -4.]]) B = np.array([[5., 6.], [7., 8.]]) @@ -177,7 +176,7 @@ def testGramRc(self): Rc = gram(sysd, 'cf') np.testing.assert_array_almost_equal(Rc, Rctrue) - @slycotonly + @pytest.mark.slycot def testGramWo(self): A = np.array([[1., -2.], [3., -4.]]) B = np.array([[5., 6.], [7., 8.]]) @@ -193,7 +192,7 @@ def testGramWo(self): Wo = gram(sysd, 'o') np.testing.assert_array_almost_equal(Wo, Wotrue) - @slycotonly + @pytest.mark.slycot def testGramWo2(self): A = np.array([[1., -2.], [3., -4.]]) B = np.array([[5.], [7.]]) @@ -209,7 +208,7 @@ def testGramWo2(self): Wo = gram(sysd, 'o') np.testing.assert_array_almost_equal(Wo, Wotrue) - @slycotonly + @pytest.mark.slycot def testGramRo(self): A = np.array([[1., -2.], [3., -4.]]) B = np.array([[5., 6.], [7., 8.]]) @@ -318,7 +317,7 @@ def testPlace(self): with pytest.raises(ValueError): place(A, B, P_repeated) - @slycotonly + @pytest.mark.slycot def testPlace_varga_continuous(self): """ Check that we can place eigenvalues for dtime=False @@ -345,7 +344,7 @@ def testPlace_varga_continuous(self): self.checkPlaced(P, P_placed) - @slycotonly + @pytest.mark.slycot def testPlace_varga_continuous_partial_eigs(self): """ Check that we are able to use the alpha parameter to only place @@ -365,7 +364,7 @@ def testPlace_varga_continuous_partial_eigs(self): # No guarantee of the ordering, so sort them self.checkPlaced(P_expected, P_placed) - @slycotonly + @pytest.mark.slycot def testPlace_varga_discrete(self): """ Check that we can place poles using dtime=True (discrete time) @@ -379,7 +378,7 @@ def testPlace_varga_discrete(self): # No guarantee of the ordering, so sort them self.checkPlaced(P, P_placed) - @slycotonly + @pytest.mark.slycot def testPlace_varga_discrete_partial_eigs(self): """" Check that we can only assign a single eigenvalue in the discrete @@ -412,27 +411,30 @@ def check_DLQR(self, K, S, poles, Q, R): np.testing.assert_array_almost_equal(K, K_expected) np.testing.assert_array_almost_equal(poles, poles_expected) - @pytest.mark.parametrize("method", [None, 'slycot', 'scipy']) + @pytest.mark.parametrize("method", + [None, + pytest.param('slycot', marks=pytest.mark.slycot), + 'scipy']) def test_LQR_integrator(self, method): - if method == 'slycot' and not slycot_check(): - return A, B, Q, R = (np.array([[X]]) for X in [0., 1., 10., 2.]) K, S, poles = lqr(A, B, Q, R, method=method) self.check_LQR(K, S, poles, Q, R) - @pytest.mark.parametrize("method", [None, 'slycot', 'scipy']) + @pytest.mark.parametrize("method", + [None, + pytest.param('slycot', marks=pytest.mark.slycot), + 'scipy']) def test_LQR_3args(self, method): - if method == 'slycot' and not slycot_check(): - return sys = ss(0., 1., 1., 0.) Q, R = (np.array([[X]]) for X in [10., 2.]) K, S, poles = lqr(sys, Q, R, method=method) self.check_LQR(K, S, poles, Q, R) - @pytest.mark.parametrize("method", [None, 'slycot', 'scipy']) + @pytest.mark.parametrize("method", + [None, + pytest.param('slycot', marks=pytest.mark.slycot), + 'scipy']) def test_DLQR_3args(self, method): - if method == 'slycot' and not slycot_check(): - return dsys = ss(0., 1., 1., 0., .1) Q, R = (np.array([[X]]) for X in [10., 2.]) K, S, poles = dlqr(dsys, Q, R, method=method) @@ -449,12 +451,12 @@ def test_lqr_badmethod(self, cdlqr): with pytest.raises(ControlArgument, match="Unknown method"): K, S, poles = cdlqr(A, B, Q, R, method='nosuchmethod') + @pytest.mark.noslycot @pytest.mark.parametrize("cdlqr", [lqr, dlqr]) def test_lqr_slycot_not_installed(self, cdlqr): A, B, Q, R = 0, 1, 10, 2 - if not slycot_check(): - with pytest.raises(ControlSlycot, match="Can't find slycot"): - K, S, poles = cdlqr(A, B, Q, R, method='slycot') + with pytest.raises(ControlSlycot, match="Can't find slycot"): + K, S, poles = cdlqr(A, B, Q, R, method='slycot') @pytest.mark.xfail(reason="warning not implemented") def testLQR_warning(self): @@ -538,7 +540,13 @@ def testDLQR_warning(self): with pytest.warns(UserWarning): (K, S, E) = dlqr(A, B, Q, R, N) - def test_care(self): + @pytest.mark.parametrize('have_slycot', + [pytest.param(True, marks=pytest.mark.slycot), + pytest.param(False, marks=pytest.mark.noslycot)]) + @pytest.mark.parametrize("method", + [pytest.param('slycot', marks=pytest.mark.slycot), + 'scipy']) + def test_care(self, have_slycot, method): """Test stabilizing and anti-stabilizing feedback, continuous""" A = np.diag([1, -1]) B = np.identity(2) @@ -547,19 +555,19 @@ def test_care(self): S = np.zeros((2, 2)) E = np.identity(2) - X, L, G = care(A, B, Q, R, S, E, stabilizing=True) + X, L, G = care(A, B, Q, R, S, E, stabilizing=True, method=method) assert np.all(np.real(L) < 0) - if slycot_check(): - X, L, G = care(A, B, Q, R, S, E, stabilizing=False) + if have_slycot and method=='slycot': + X, L, G = care(A, B, Q, R, S, E, stabilizing=False, method=method) assert np.all(np.real(L) > 0) else: with pytest.raises(ControlArgument, match="'scipy' not valid"): - X, L, G = care(A, B, Q, R, S, E, stabilizing=False) + X, L, G = care(A, B, Q, R, S, E, stabilizing=False, method=method) @pytest.mark.parametrize( "stabilizing", - [True, pytest.param(False, marks=slycotonly)]) + [True, pytest.param(False, marks=pytest.mark.slycot)]) def test_dare(self, stabilizing): """Test stabilizing and anti-stabilizing feedback, discrete""" A = np.diag([0.5, 2]) @@ -782,7 +790,10 @@ def test_statefbk_iosys_unused(self): np.testing.assert_allclose(clsys0_lin.A, clsys2_lin.A) - def test_lqr_integral_continuous(self): + @pytest.mark.parametrize('have_slycot', + [pytest.param(True, marks=pytest.mark.slycot), + pytest.param(False, marks=pytest.mark.noslycot)]) + def test_lqr_integral_continuous(self, have_slycot): # Generate a continuous-time system for testing sys = ct.rss(4, 4, 2, strictly_proper=True) sys.C = np.eye(4) # reset output to be full state @@ -844,7 +855,7 @@ def test_lqr_integral_continuous(self): assert all(np.real(clsys.poles()) < 0) # Make sure controller infinite zero frequency gain - if slycot_check(): + if have_slycot: ctrl_tf = tf(ctrl) assert abs(ctrl_tf(1e-9)[0][0]) > 1e6 assert abs(ctrl_tf(1e-9)[1][1]) > 1e6 diff --git a/control/tests/statesp_test.py b/control/tests/statesp_test.py index 3c1411f04..9b3c677fe 100644 --- a/control/tests/statesp_test.py +++ b/control/tests/statesp_test.py @@ -22,7 +22,7 @@ from control.statesp import StateSpace, _convert_to_statespace, \ _rss_generate, _statesp_defaults, drss, linfnorm, rss, ss, tf2ss from control.xferfcn import TransferFunction, ss2tf -from .conftest import assert_tf_close_coeff, slycotonly +from .conftest import assert_tf_close_coeff class TestStateSpace: """Tests for the StateSpace class.""" @@ -229,7 +229,7 @@ def test_zero_empty(self): sys = _convert_to_statespace(TransferFunction([1], [1, 2, 1])) np.testing.assert_array_equal(sys.zeros(), np.array([])) - @slycotonly + @pytest.mark.slycot 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 @@ -259,7 +259,7 @@ def test_zero_mimo_sys222_square(self, sys222): true_z = np.sort([-10.568501, 3.368501]) np.testing.assert_array_almost_equal(z, true_z) - @slycotonly + @pytest.mark.slycot def test_zero_mimo_sys623_non_square(self, sys623): """Evaluate the zeros of a non square MIMO system.""" @@ -406,7 +406,7 @@ def test_add_sub_mimo_siso(self): ss2tf(result).minreal(), ) - @slycotonly + @pytest.mark.slycot @pytest.mark.parametrize( "left, right, expected", [ @@ -481,7 +481,7 @@ def test_mul_mimo_siso(self, left, right, expected): ss2tf(result).minreal(), ) - @slycotonly + @pytest.mark.slycot @pytest.mark.parametrize( "left, right, expected", [ @@ -556,7 +556,7 @@ def test_rmul_mimo_siso(self, left, right, expected): ss2tf(result).minreal(), ) - @slycotonly + @pytest.mark.slycot @pytest.mark.parametrize("power", [0, 1, 3, -3]) @pytest.mark.parametrize("sysname", ["sys222", "sys322"]) def test_pow(self, request, sysname, power): @@ -575,7 +575,7 @@ def test_pow(self, request, sysname, power): np.testing.assert_allclose(expected.C, result.C) np.testing.assert_allclose(expected.D, result.D) - @slycotonly + @pytest.mark.slycot @pytest.mark.parametrize("order", ["left", "right"]) @pytest.mark.parametrize("sysname", ["sys121", "sys222", "sys322"]) def test_pow_inv(self, request, sysname, order): @@ -599,7 +599,7 @@ def test_pow_inv(self, request, sysname, order): # Check that the output is the same as the input np.testing.assert_allclose(R.outputs, U) - @slycotonly + @pytest.mark.slycot def test_truediv(self, sys222, sys322): """Test state space truediv""" for sys in [sys222, sys322]: @@ -618,7 +618,7 @@ def test_truediv(self, sys222, sys322): ss2tf(result).minreal(), ) - @slycotonly + @pytest.mark.slycot def test_rtruediv(self, sys222, sys322): """Test state space rtruediv""" for sys in [sys222, sys322]: @@ -719,7 +719,7 @@ def test_freq_resp(self): mag, phase, omega = sys.freqresp(true_omega) np.testing.assert_almost_equal(mag, true_mag) - @slycotonly + @pytest.mark.slycot def test_minreal(self): """Test a minreal model reduction.""" # A = [-2, 0.5, 0; 0.5, -0.3, 0; 0, 0, -0.1] @@ -1514,7 +1514,7 @@ def dt_siso(self, request): name, systype, sysargs, dt, refgpeak, reffpeak = request.param return ct.c2d(systype(*sysargs), dt), refgpeak, reffpeak - @slycotonly + @pytest.mark.slycot @pytest.mark.usefixtures('ignore_future_warning') def test_linfnorm_ct_siso(self, ct_siso): sys, refgpeak, reffpeak = ct_siso @@ -1522,7 +1522,7 @@ def test_linfnorm_ct_siso(self, ct_siso): np.testing.assert_allclose(gpeak, refgpeak) np.testing.assert_allclose(fpeak, reffpeak) - @slycotonly + @pytest.mark.slycot @pytest.mark.usefixtures('ignore_future_warning') def test_linfnorm_dt_siso(self, dt_siso): sys, refgpeak, reffpeak = dt_siso @@ -1531,7 +1531,7 @@ def test_linfnorm_dt_siso(self, dt_siso): np.testing.assert_allclose(gpeak, refgpeak) np.testing.assert_allclose(fpeak, reffpeak) - @slycotonly + @pytest.mark.slycot @pytest.mark.usefixtures('ignore_future_warning') def test_linfnorm_ct_mimo(self, ct_siso): siso, refgpeak, reffpeak = ct_siso @@ -1602,10 +1602,13 @@ def test_tf2ss_unstable(method): np.testing.assert_allclose(tf_poles, ss_poles, rtol=1e-4) -def test_tf2ss_mimo(): +@pytest.mark.parametrize('have_slycot', + [pytest.param(True, marks=pytest.mark.slycot), + pytest.param(False, marks=pytest.mark.noslycot)]) +def test_tf2ss_mimo(have_slycot): sys_tf = ct.tf([[[1], [1, 1, 1]]], [[[1, 1, 1], [1, 2, 1]]]) - if ct.slycot_check(): + if have_slycot: sys_ss = ct.ss(sys_tf) np.testing.assert_allclose( np.sort(sys_tf.poles()), np.sort(sys_ss.poles())) diff --git a/control/tests/stochsys_test.py b/control/tests/stochsys_test.py index 6fc87461b..20e799643 100644 --- a/control/tests/stochsys_test.py +++ b/control/tests/stochsys_test.py @@ -6,7 +6,7 @@ import control as ct import control.optimal as opt -from control import lqe, dlqe, rss, tf, ControlArgument, slycot_check +from control import lqe, dlqe, rss, tf, ControlArgument from math import log, pi # Utility function to check LQE answer @@ -27,11 +27,10 @@ def check_DLQE(L, P, poles, G, QN, RN): np.testing.assert_almost_equal(L, L_expected) np.testing.assert_almost_equal(poles, poles_expected) -@pytest.mark.parametrize("method", [None, 'slycot', 'scipy']) +@pytest.mark.parametrize("method", [None, + pytest.param('slycot', marks=pytest.mark.slycot), + 'scipy']) def test_LQE(method): - if method == 'slycot' and not slycot_check(): - return - A, G, C, QN, RN = (np.array([[X]]) for X in [0., .1, 1., 10., 2.]) L, P, poles = lqe(A, G, C, QN, RN, method=method) check_LQE(L, P, poles, G, QN, RN) @@ -78,11 +77,10 @@ def test_lqe_call_format(cdlqe): with pytest.raises(ct.ControlArgument, match="LTI system must be"): L, P, E = cdlqe(sys_tf, Q, R) -@pytest.mark.parametrize("method", [None, 'slycot', 'scipy']) +@pytest.mark.parametrize("method", [None, + pytest.param('slycot', marks=pytest.mark.slycot), + 'scipy']) def test_DLQE(method): - if method == 'slycot' and not slycot_check(): - return - A, G, C, QN, RN = (np.array([[X]]) for X in [0., .1, 1., 10., 2.]) L, P, poles = dlqe(A, G, C, QN, RN, method=method) check_DLQE(L, P, poles, G, QN, RN) diff --git a/control/tests/timeplot_test.py b/control/tests/timeplot_test.py index 888ff9080..ea0a290c9 100644 --- a/control/tests/timeplot_test.py +++ b/control/tests/timeplot_test.py @@ -7,7 +7,6 @@ import pytest import control as ct -from control.tests.conftest import slycotonly # Detailed test of (almost) all functionality # @@ -237,7 +236,7 @@ def test_axes_setup(): sys_3x1 = ct.rss(4, 3, 1) -@slycotonly +@pytest.mark.slycot @pytest.mark.usefixtures('mplcleanup') def test_legend_map(): sys_mimo = ct.tf2ss( @@ -371,7 +370,7 @@ def test_list_responses(resp_fcn): assert cplt.lines[row, col][1].get_color() == 'tab:orange' -@slycotonly +@pytest.mark.slycot @pytest.mark.usefixtures('mplcleanup') def test_linestyles(): # Check to make sure we can change line styles diff --git a/control/tests/timeresp_test.py b/control/tests/timeresp_test.py index 8bbd27d73..16ee01a3d 100644 --- a/control/tests/timeresp_test.py +++ b/control/tests/timeresp_test.py @@ -8,8 +8,7 @@ import control as ct from control import StateSpace, TransferFunction, c2d, isctime, ss2tf, tf2ss -from control.exception import pandas_check, slycot_check -from control.tests.conftest import slycotonly +from control.exception import pandas_check from control.timeresp import _default_time_vector, _ideal_tfinal_and_dt, \ forced_response, impulse_response, initial_response, step_info, \ step_response @@ -454,7 +453,7 @@ def test_step_info(self, tsystem, systype, time_2d, yfinal): @pytest.mark.parametrize( "tsystem", ['mimo_ss_step_matlab', - pytest.param('mimo_tf_step_info', marks=slycotonly)], + pytest.param('mimo_tf_step_info', marks=pytest.mark.slycot)], indirect=["tsystem"]) def test_step_info_mimo(self, tsystem, systype, yfinal): """Test step info for MIMO systems.""" @@ -799,7 +798,7 @@ def test_lsim_double_integrator(self, u, x0, xtrue): np.testing.assert_array_almost_equal(yout, ytrue, decimal=6) - @slycotonly + @pytest.mark.slycot def test_step_robustness(self): "Test robustness os step_response against denomiantors: gh-240" # Create 2 input, 2 output system @@ -902,9 +901,9 @@ def test_default_timevector_functions_d(self, fun, dt): "siso_dtf2", "siso_ss2_dtnone", # undetermined timebase "mimo_ss2", # MIMO - pytest.param("mimo_tf2", marks=slycotonly), + pytest.param("mimo_tf2", marks=pytest.mark.slycot), "mimo_dss1", - pytest.param("mimo_dtf1", marks=slycotonly), + pytest.param("mimo_dtf1", marks=pytest.mark.slycot), ], indirect=True) @pytest.mark.parametrize("fun", [step_response, @@ -1033,30 +1032,41 @@ def test_time_series_data_convention_2D(self, tsystem): assert y.ndim == 1 # SISO returns "scalar" output assert t.shape == y.shape # Allows direct plotting of output + def p(*args): + # convenience for parametrize below + return pytest.param(*args, marks=pytest.mark.slycot) + @pytest.mark.usefixtures("editsdefaults") - @pytest.mark.parametrize("fcn", [ct.ss, ct.tf]) - @pytest.mark.parametrize("nstate, nout, ninp, squeeze, shape1, shape2", [ - # state out in squeeze in/out out-only - [1, 1, 1, None, (8,), (8,)], - [2, 1, 1, True, (8,), (8,)], - [3, 1, 1, False, (1, 1, 8), (1, 8)], - [3, 2, 1, None, (2, 1, 8), (2, 8)], - [4, 2, 1, True, (2, 8), (2, 8)], - [5, 2, 1, False, (2, 1, 8), (2, 8)], - [3, 1, 2, None, (1, 2, 8), (1, 8)], - [4, 1, 2, True, (2, 8), (8,)], - [5, 1, 2, False, (1, 2, 8), (1, 8)], - [4, 2, 2, None, (2, 2, 8), (2, 8)], - [5, 2, 2, True, (2, 2, 8), (2, 8)], - [6, 2, 2, False, (2, 2, 8), (2, 8)], + @pytest.mark.parametrize("fcn, nstate, nout, ninp, squeeze, shape1, shape2", [ + # fcn, state out in squeeze in/out out-only + [ct.ss, 1, 1, 1, None, (8,), (8,)], + [ct.ss, 2, 1, 1, True, (8,), (8,)], + [ct.ss, 3, 1, 1, False, (1, 1, 8), (1, 8)], + [ct.ss, 3, 2, 1, None, (2, 1, 8), (2, 8)], + [ct.ss, 4, 2, 1, True, (2, 8), (2, 8)], + [ct.ss, 5, 2, 1, False, (2, 1, 8), (2, 8)], + [ct.ss, 3, 1, 2, None, (1, 2, 8), (1, 8)], + [ct.ss, 4, 1, 2, True, (2, 8), (8,)], + [ct.ss, 5, 1, 2, False, (1, 2, 8), (1, 8)], + [ct.ss, 4, 2, 2, None, (2, 2, 8), (2, 8)], + [ct.ss, 5, 2, 2, True, (2, 2, 8), (2, 8)], + [ct.ss, 6, 2, 2, False, (2, 2, 8), (2, 8)], + [ct.tf, 1, 1, 1, None, (8,), (8,)], + [ct.tf, 2, 1, 1, True, (8,), (8,)], + [ct.tf, 3, 1, 1, False, (1, 1, 8), (1, 8)], + p(ct.tf, 3, 2, 1, None, (2, 1, 8), (2, 8)), + p(ct.tf, 4, 2, 1, True, (2, 8), (2, 8)), + p(ct.tf, 5, 2, 1, False, (2, 1, 8), (2, 8)), + p(ct.tf, 3, 1, 2, None, (1, 2, 8), (1, 8)), + p(ct.tf, 4, 1, 2, True, (2, 8), (8,)), + p(ct.tf, 5, 1, 2, False, (1, 2, 8), (1, 8)), + p(ct.tf, 4, 2, 2, None, (2, 2, 8), (2, 8)), + p(ct.tf, 5, 2, 2, True, (2, 2, 8), (2, 8)), + p(ct.tf, 6, 2, 2, False, (2, 2, 8), (2, 8)), ]) def test_squeeze(self, fcn, nstate, nout, ninp, squeeze, shape1, shape2): # Define the system - if fcn == ct.tf and (nout > 1 or ninp > 1) and not slycot_check(): - pytest.skip("Conversion of MIMO systems to transfer functions " - "requires slycot.") - else: - sys = fcn(ct.rss(nstate, nout, ninp, strictly_proper=True)) + sys = fcn(ct.rss(nstate, nout, ninp, strictly_proper=True)) # Generate the time and input vectors tvec = np.linspace(0, 1, 8) @@ -1236,7 +1246,7 @@ def test_response_transpose( assert x.shape == (T.size, sys.nstates) -@pytest.mark.skipif(not pandas_check(), reason="pandas not installed") +@pytest.mark.pandas def test_to_pandas(): # Create a SISO time response sys = ct.rss(2, 1, 1) diff --git a/control/tests/xferfcn_test.py b/control/tests/xferfcn_test.py index d3db08ef6..a9be040ab 100644 --- a/control/tests/xferfcn_test.py +++ b/control/tests/xferfcn_test.py @@ -14,7 +14,7 @@ isdtime, reset_defaults, rss, sample_system, set_defaults, ss, ss2tf, tf, tf2ss, zpk) from control.statesp import _convert_to_statespace -from control.tests.conftest import assert_tf_close_coeff, slycotonly +from control.tests.conftest import assert_tf_close_coeff from control.xferfcn import _convert_to_transfer_function @@ -997,7 +997,7 @@ def test_minreal_4(self): np.testing.assert_allclose(hm.num[0][0], hr.num[0][0]) np.testing.assert_allclose(hr.dt, hm.dt) - @slycotonly + @pytest.mark.slycot def test_state_space_conversion_mimo(self): """Test conversion of a single input, two-output state-space system against the same TF""" diff --git a/pyproject.toml b/pyproject.toml index b47f7462c..b76a3731f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,10 +51,17 @@ source = "https://github.com/python-control/python-control" write_to = "control/_version.py" [tool.pytest.ini_options] -addopts = "-ra" +addopts = "-ra --strict-markers" filterwarnings = [ "error:.*matrix subclass:PendingDeprecationWarning", ] +markers = [ + "slycot: tests needing slycot", + "noslycot: test needing slycot absent", + "cvxopt: tests needing cvxopt", + "pandas: tests needing pandas", +] + [tool.ruff]