From d4ee3be8f1ff8d655bf40cbe3e70ca2a53302cec Mon Sep 17 00:00:00 2001 From: Ben Greiner Date: Thu, 16 Jun 2022 11:37:16 +0200 Subject: [PATCH 1/4] clean from old scipyy<1.3 and python2 code --- control/tests/conftest.py | 13 ++++--------- control/tests/flatsys_test.py | 11 ++++------- control/tests/iosys_test.py | 19 +------------------ control/tests/timeresp_test.py | 10 ++-------- control/tests/xferfcn_test.py | 6 ++---- doc/intro.rst | 2 +- setup.py | 2 +- 7 files changed, 15 insertions(+), 48 deletions(-) diff --git a/control/tests/conftest.py b/control/tests/conftest.py index b67ef3674..ca878337e 100644 --- a/control/tests/conftest.py +++ b/control/tests/conftest.py @@ -1,7 +1,6 @@ """conftest.py - pytest local plugins and fixtures""" from contextlib import contextmanager -from distutils.version import StrictVersion import os import sys @@ -18,10 +17,6 @@ # 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:" @@ -110,10 +105,10 @@ def editsdefaults(): @pytest.fixture(scope="function") def mplcleanup(): - """Workaround for python2 - - python 2 does not like to mix the original mpl decorator with pytest - fixtures. So we roll our own. + """Clean up any plots and changes a test may have made to matplotlib. + + compare matplotlib.testing.decorators.cleanup() but as a fixture instead + of a decorator. """ save = mpl.units.registry.copy() try: diff --git a/control/tests/flatsys_test.py b/control/tests/flatsys_test.py index a12852759..e3584d459 100644 --- a/control/tests/flatsys_test.py +++ b/control/tests/flatsys_test.py @@ -8,8 +8,6 @@ created for that purpose. """ -from distutils.version import StrictVersion - import numpy as np import pytest import scipy as sp @@ -118,11 +116,10 @@ def test_kinematic_car(self, vehicle_flat, poly): resp = ct.input_output_response(vehicle_flat, T, ud, x0) np.testing.assert_array_almost_equal(resp.states, xd, decimal=2) - # For SciPy 1.0+, integrate equations and compare to desired - if StrictVersion(sp.__version__) >= "1.0": - t, y, x = ct.input_output_response( - vehicle_flat, T, ud, x0, return_x=True) - np.testing.assert_allclose(x, xd, atol=0.01, rtol=0.01) + # integrate equations and compare to desired + t, y, x = ct.input_output_response( + vehicle_flat, T, ud, x0, return_x=True) + np.testing.assert_allclose(x, xd, atol=0.01, rtol=0.01) def test_flat_default_output(self, vehicle_flat): # Construct a flat system with the default outputs diff --git a/control/tests/iosys_test.py b/control/tests/iosys_test.py index 825cec5c5..db683e1a1 100644 --- a/control/tests/iosys_test.py +++ b/control/tests/iosys_test.py @@ -15,7 +15,7 @@ import control as ct from control import iosys as ios -from control.tests.conftest import noscipy0, matrixfilter +from control.tests.conftest import matrixfilter class TestIOSys: @@ -46,7 +46,6 @@ class TSys: return T - @noscipy0 def test_linear_iosys(self, tsys): # Create an input/output system from the linear system linsys = tsys.siso_linsys @@ -65,7 +64,6 @@ def test_linear_iosys(self, tsys): np.testing.assert_array_almost_equal(lti_t, ios_t) np.testing.assert_allclose(lti_y, ios_y, atol=0.002, rtol=0.) - @noscipy0 def test_tf2io(self, tsys): # Create a transfer function from the state space system linsys = tsys.siso_linsys @@ -129,7 +127,6 @@ def test_iosys_print(self, tsys, capsys): ios_linearized = ios.linearize(ios_unspecified, [0, 0], [0]) print(ios_linearized) - @noscipy0 @pytest.mark.parametrize("ss", [ios.NonlinearIOSystem, ct.ss]) def test_nonlinear_iosys(self, tsys, ss): # Create a simple nonlinear I/O system @@ -236,7 +233,6 @@ def test_linearize_named_signals(self, kincar): assert lin_nocopy.find_output('x') is None assert lin_nocopy.find_state('x') is None - @noscipy0 def test_connect(self, tsys): # Define a couple of (linear) systems to interconnection linsys1 = tsys.siso_linsys @@ -294,7 +290,6 @@ def test_connect(self, tsys): np.testing.assert_array_almost_equal(lti_t, ios_t) np.testing.assert_allclose(lti_y, ios_y,atol=0.002,rtol=0.) - @noscipy0 @pytest.mark.parametrize( "connections, inplist, outlist", [pytest.param([[(1, 0), (0, 0, 1)]], [[(0, 0, 1)]], [[(1, 0, 1)]], @@ -338,7 +333,6 @@ def test_connect_spec_variants(self, tsys, connections, inplist, outlist): np.testing.assert_array_almost_equal(lti_t, ios_t) np.testing.assert_allclose(lti_y, ios_y, atol=0.002, rtol=0.) - @noscipy0 @pytest.mark.parametrize( "connections, inplist, outlist", [pytest.param([['sys2.u[0]', 'sys1.y[0]']], @@ -375,7 +369,6 @@ def test_connect_spec_warnings(self, tsys, connections, inplist, outlist): np.testing.assert_array_almost_equal(lti_t, ios_t) np.testing.assert_allclose(lti_y, ios_y, atol=0.002, rtol=0.) - @noscipy0 def test_static_nonlinearity(self, tsys): # Linear dynamical system linsys = tsys.siso_linsys @@ -400,7 +393,6 @@ def test_static_nonlinearity(self, tsys): np.testing.assert_array_almost_equal(lti_y, ios_y, decimal=2) - @noscipy0 @pytest.mark.filterwarnings("ignore:Duplicate name::control.iosys") def test_algebraic_loop(self, tsys): # Create some linear and nonlinear systems to play with @@ -470,7 +462,6 @@ def test_algebraic_loop(self, tsys): with pytest.raises(RuntimeError): ios.input_output_response(*args) - @noscipy0 def test_summer(self, tsys): # Construct a MIMO system for testing linsys = tsys.mimo_linsys1 @@ -489,7 +480,6 @@ def test_summer(self, tsys): 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.) - @noscipy0 def test_rmul(self, tsys): # Test right multiplication # TODO: replace with better tests when conversions are implemented @@ -510,7 +500,6 @@ def test_rmul(self, tsys): lti_t, lti_y = ct.forced_response(ioslin, T, U*U, X0) np.testing.assert_array_almost_equal(ios_y, lti_y*lti_y, decimal=3) - @noscipy0 def test_neg(self, tsys): """Test negation of a system""" @@ -533,7 +522,6 @@ def test_neg(self, tsys): lti_t, lti_y = ct.forced_response(ioslin, T, U*U, X0) np.testing.assert_array_almost_equal(ios_y, -lti_y, decimal=3) - @noscipy0 def test_feedback(self, tsys): # Set up parameters for simulation T, U, X0 = tsys.T, tsys.U, tsys.X0 @@ -549,7 +537,6 @@ def test_feedback(self, tsys): lti_t, lti_y = ct.forced_response(linsys, T, U, X0) np.testing.assert_allclose(ios_y, lti_y,atol=0.002,rtol=0.) - @noscipy0 def test_bdalg_functions(self, tsys): """Test block diagram functions algebra on I/O systems""" # Set up parameters for simulation @@ -596,7 +583,6 @@ def test_bdalg_functions(self, tsys): 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.) - @noscipy0 def test_algebraic_functions(self, tsys): """Test algebraic operations on I/O systems""" # Set up parameters for simulation @@ -648,7 +634,6 @@ def test_algebraic_functions(self, tsys): ios_t, ios_y = ios.input_output_response(iosys_negate, T, U, X0) np.testing.assert_allclose(ios_y, lin_y,atol=0.002,rtol=0.) - @noscipy0 def test_nonsquare_bdalg(self, tsys): # Set up parameters for simulation T = tsys.T @@ -699,7 +684,6 @@ def test_nonsquare_bdalg(self, tsys): with pytest.raises(ValueError): ct.series(*args) - @noscipy0 def test_discrete(self, tsys): """Test discrete time functionality""" # Create some linear and nonlinear systems to play with @@ -868,7 +852,6 @@ def test_find_eqpts(self, tsys): assert xeq is None assert ueq is None - @noscipy0 def test_params(self, tsys): # Start with the default set of parameters ios_secord_default = ios.NonlinearIOSystem( diff --git a/control/tests/timeresp_test.py b/control/tests/timeresp_test.py index fe73ab4a9..ff712f849 100644 --- a/control/tests/timeresp_test.py +++ b/control/tests/timeresp_test.py @@ -1,7 +1,6 @@ """timeresp_test.py - test time response functions""" from copy import copy -from distutils.version import StrictVersion import numpy as np import pytest @@ -521,8 +520,6 @@ def test_impulse_response_mimo(self, tsystem): _t, yy = impulse_response(sys, T=t, input=0) np.testing.assert_array_almost_equal(yy[:,0,:], yref_notrim, decimal=4) - @pytest.mark.skipif(StrictVersion(sp.__version__) < "1.3", - reason="requires SciPy 1.3 or greater") @pytest.mark.parametrize("tsystem", ["siso_tf1"], indirect=True) def test_discrete_time_impulse(self, tsystem): # discrete time impulse sampled version should match cont time @@ -998,9 +995,6 @@ def test_time_series_data_convention_2D(self, tsystem): [6, 2, 2, False, (2, 2, 8), (2, 8)], ]) def test_squeeze(self, fcn, nstate, nout, ninp, squeeze, shape1, shape2): - # Figure out if we have SciPy 1+ - scipy0 = StrictVersion(sp.__version__) < '1.0' - # 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 " @@ -1077,7 +1071,7 @@ def test_squeeze(self, fcn, nstate, nout, ninp, squeeze, shape1, shape2): assert yvec.shape == (8, ) # For InputOutputSystems, also test input/output response - if isinstance(sys, ct.InputOutputSystem) and not scipy0: + if isinstance(sys, ct.InputOutputSystem): _, yvec = ct.input_output_response(sys, tvec, uvec, squeeze=squeeze) assert yvec.shape == shape2 @@ -1108,7 +1102,7 @@ def test_squeeze(self, fcn, nstate, nout, ninp, squeeze, shape1, shape2): assert yvec.shape == (sys.noutputs, 8) # For InputOutputSystems, also test input_output_response - if isinstance(sys, ct.InputOutputSystem) and not scipy0: + if isinstance(sys, ct.InputOutputSystem): _, yvec = ct.input_output_response(sys, tvec, uvec) if squeeze is not True or sys.noutputs > 1: assert yvec.shape == (sys.noutputs, 8) diff --git a/control/tests/xferfcn_test.py b/control/tests/xferfcn_test.py index 79273f31b..894da5594 100644 --- a/control/tests/xferfcn_test.py +++ b/control/tests/xferfcn_test.py @@ -12,7 +12,7 @@ from control import isctime, isdtime, sample_system, defaults from control.statesp import _convert_to_statespace from control.xferfcn import _convert_to_transfer_function -from control.tests.conftest import slycotonly, nopython2, matrixfilter +from control.tests.conftest import slycotonly, matrixfilter class TestXferFcn: @@ -448,7 +448,6 @@ def test_call_siso(self, dt, omega, resp): np.testing.assert_allclose(sys.evalfr(omega), resp, atol=1e-3) - @nopython2 def test_call_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) @@ -741,8 +740,7 @@ def test_indexing(self): "matarrayin", [pytest.param(np.array, id="arrayin", - marks=[nopython2, - pytest.mark.skip(".__matmul__ not implemented")]), + marks=[pytest.mark.skip(".__matmul__ not implemented")]), pytest.param(np.matrix, id="matrixin", marks=matrixfilter)], diff --git a/doc/intro.rst b/doc/intro.rst index 01fe81bd0..ce01aca15 100644 --- a/doc/intro.rst +++ b/doc/intro.rst @@ -36,7 +36,7 @@ Installation ============ The `python-control` package can be installed using pip, conda or the -standard distutils/setuptools mechanisms. The package requires `numpy`_ and +standard setuptools mechanisms. The package requires `numpy`_ and `scipy`_, and the plotting routines require `matplotlib `_. In addition, some routines require the `slycot `_ library in order to implement diff --git a/setup.py b/setup.py index a8609148e..df3f8519d 100644 --- a/setup.py +++ b/setup.py @@ -42,7 +42,7 @@ packages=find_packages(exclude=['benchmarks']), classifiers=[f for f in CLASSIFIERS.split('\n') if f], install_requires=['numpy', - 'scipy', + 'scipy>=1.3', 'matplotlib'], extras_require={ 'test': ['pytest', 'pytest-timeout'], From f0393b73921c4a688490f1ca17519c37a2c6bbd5 Mon Sep 17 00:00:00 2001 From: Ben Greiner Date: Thu, 16 Jun 2022 11:42:30 +0200 Subject: [PATCH 2/4] isort and autopep8 conftest.py --- control/tests/conftest.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/control/tests/conftest.py b/control/tests/conftest.py index ca878337e..ac35748f3 100644 --- a/control/tests/conftest.py +++ b/control/tests/conftest.py @@ -1,13 +1,13 @@ """conftest.py - pytest local plugins and fixtures""" -from contextlib import contextmanager import os import sys +from contextlib import contextmanager import matplotlib as mpl import numpy as np -import scipy as sp import pytest +import scipy as sp import control @@ -38,6 +38,7 @@ def control_defaults(): # assert that nothing changed it without reverting assert control.config.defaults == the_defaults + @pytest.fixture(scope="function", autouse=TEST_MATRIX_AND_ARRAY, params=[pytest.param("arrayout", marks=matrixerrorfilter), pytest.param("matrixout", marks=matrixfilter)]) @@ -106,7 +107,7 @@ def editsdefaults(): @pytest.fixture(scope="function") def mplcleanup(): """Clean up any plots and changes a test may have made to matplotlib. - + compare matplotlib.testing.decorators.cleanup() but as a fixture instead of a decorator. """ From 0cd3ac8ee0b1f250183c4c7492e67b90c7e36470 Mon Sep 17 00:00:00 2001 From: Ben Greiner Date: Thu, 16 Jun 2022 11:57:45 +0200 Subject: [PATCH 3/4] move away from deprecated scipy namespace --- control/tests/optimal_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/control/tests/optimal_test.py b/control/tests/optimal_test.py index 1aa307b60..027b55c75 100644 --- a/control/tests/optimal_test.py +++ b/control/tests/optimal_test.py @@ -366,7 +366,7 @@ def test_optimal_logging(capsys): x0 = [-1, 1] # Solve it, with logging turned on (with warning due to mixed constraints) - with pytest.warns(sp.optimize.optimize.OptimizeWarning, + with pytest.warns(sp.optimize.OptimizeWarning, match="Equality and inequality .* same element"): res = opt.solve_ocp( sys, time, x0, cost, input_constraint, terminal_cost=cost, From 7f883f67b7f0e2782a1fa8e965b9e9bd7dbd691d Mon Sep 17 00:00:00 2001 From: Ben Greiner Date: Thu, 16 Jun 2022 12:19:12 +0200 Subject: [PATCH 4/4] Avoid PytestRemovedIn8Warning for pytest.warns(None) Details: https://docs.pytest.org/en/latest/how-to/capture-warnings.html#additional-use-cases-of-warnings-in-tests --- control/tests/iosys_test.py | 39 ++++++++++++++--------------------- control/tests/namedio_test.py | 8 +++++-- control/tests/nyquist_test.py | 7 +++++-- 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/control/tests/iosys_test.py b/control/tests/iosys_test.py index db683e1a1..693be979e 100644 --- a/control/tests/iosys_test.py +++ b/control/tests/iosys_test.py @@ -9,6 +9,7 @@ """ import re +import warnings import numpy as np import pytest @@ -17,6 +18,7 @@ from control import iosys as ios from control.tests.conftest import matrixfilter + class TestIOSys: @pytest.fixture @@ -1416,11 +1418,10 @@ def test_duplicates(self, tsys): nlios2 = ios.NonlinearIOSystem(None, lambda t, x, u, params: u * u, inputs=1, outputs=1, name="nlios2") - with pytest.warns(None) as record: + with warnings.catch_warnings(): + warnings.simplefilter("error") ct.InterconnectedSystem([nlios1, iosys_siso, nlios2], inputs=0, outputs=0, states=0) - if record: - pytest.fail("Warning not expected: " + record[0].message) def test_linear_interconnection(): @@ -1510,7 +1511,7 @@ def predprey(t, x, u, params={}): def pvtol(t, x, u, params={}): """Reduced planar vertical takeoff and landing dynamics""" - from math import sin, cos + from math import cos, sin m = params.get('m', 4.) # kg, system mass J = params.get('J', 0.0475) # kg m^2, system inertia r = params.get('r', 0.25) # m, thrust offset @@ -1526,7 +1527,7 @@ def pvtol(t, x, u, params={}): def pvtol_full(t, x, u, params={}): - from math import sin, cos + from math import cos, sin m = params.get('m', 4.) # kg, system mass J = params.get('J', 0.0475) # kg m^2, system inertia r = params.get('r', 0.25) # m, thrust offset @@ -1579,8 +1580,12 @@ def test_interconnect_unused_input(): inputs=['r'], outputs=['y']) - with pytest.warns(None) as record: + with warnings.catch_warnings(): # no warning if output explicitly ignored, various argument forms + warnings.simplefilter("error") + # strip out matrix warnings + warnings.filterwarnings("ignore", "the matrix subclass", + category=PendingDeprecationWarning) h = ct.interconnect([g,s,k], inputs=['r'], outputs=['y'], @@ -1595,14 +1600,6 @@ def test_interconnect_unused_input(): h = ct.interconnect([g,s,k], connections=False) - #https://docs.pytest.org/en/6.2.x/warnings.html#recwarn - for r in record: - # strip out matrix warnings - if re.match(r'.*matrix subclass', str(r.message)): - continue - print(r.message) - pytest.fail(f'Unexpected warning: {r.message}') - # warn if explicity ignored input in fact used with pytest.warns( @@ -1657,7 +1654,11 @@ def test_interconnect_unused_output(): # no warning if output explicitly ignored - with pytest.warns(None) as record: + with warnings.catch_warnings(): + warnings.simplefilter("error") + # strip out matrix warnings + warnings.filterwarnings("ignore", "the matrix subclass", + category=PendingDeprecationWarning) h = ct.interconnect([g,s,k], inputs=['r'], outputs=['y'], @@ -1672,14 +1673,6 @@ def test_interconnect_unused_output(): h = ct.interconnect([g,s,k], connections=False) - #https://docs.pytest.org/en/6.2.x/warnings.html#recwarn - for r in record: - # strip out matrix warnings - if re.match(r'.*matrix subclass', str(r.message)): - continue - print(r.message) - pytest.fail(f'Unexpected warning: {r.message}') - # warn if explicity ignored output in fact used with pytest.warns( UserWarning, diff --git a/control/tests/namedio_test.py b/control/tests/namedio_test.py index 3a96203a8..4925e9790 100644 --- a/control/tests/namedio_test.py +++ b/control/tests/namedio_test.py @@ -10,6 +10,7 @@ import re from copy import copy +import warnings import numpy as np import control as ct @@ -243,9 +244,12 @@ def test_duplicate_sysname(): sys = ct.rss(4, 1, 1) # No warnings should be generated if we reuse an an unnamed system - with pytest.warns(None) as record: + with warnings.catch_warnings(): + warnings.simplefilter("error") + # strip out matrix warnings + warnings.filterwarnings("ignore", "the matrix subclass", + category=PendingDeprecationWarning) res = sys * sys - assert not any([type(msg) == UserWarning for msg in record]) # Generate a warning if the system is named sys = ct.rss(4, 1, 1, name='sys') diff --git a/control/tests/nyquist_test.py b/control/tests/nyquist_test.py index b1aa00577..ddc69e7bb 100644 --- a/control/tests/nyquist_test.py +++ b/control/tests/nyquist_test.py @@ -8,6 +8,8 @@ """ +import warnings + import pytest import numpy as np import matplotlib.pyplot as plt @@ -225,10 +227,11 @@ def test_nyquist_encirclements(): sys = 1 / (s**2 + s + 1) with pytest.warns(UserWarning, match="encirclements was a non-integer"): count = ct.nyquist_plot(sys, omega_limits=[0.5, 1e3]) - with pytest.warns(None) as records: + with warnings.catch_warnings(): + warnings.simplefilter("error") + # strip out matrix warnings count = ct.nyquist_plot( sys, omega_limits=[0.5, 1e3], encirclement_threshold=0.2) - assert len(records) == 0 plt.title("Non-integer number of encirclements [%g]" % count)