From ca302d65f6efa3f116dc5660099ac7383ea0161e Mon Sep 17 00:00:00 2001 From: Ben Greiner Date: Thu, 29 Apr 2021 15:09:37 +0200 Subject: [PATCH 1/3] add coverage for rlocus with dtime, grid and sisotool --- control/tests/rlocus_test.py | 18 +++++++++++----- control/tests/sisotool_test.py | 39 ++++++++++------------------------ 2 files changed, 24 insertions(+), 33 deletions(-) diff --git a/control/tests/rlocus_test.py b/control/tests/rlocus_test.py index f7aff9ebe..40c84d335 100644 --- a/control/tests/rlocus_test.py +++ b/control/tests/rlocus_test.py @@ -18,11 +18,19 @@ class TestRootLocus: """These are tests for the feedback function in rlocus.py.""" - @pytest.fixture(params=[(TransferFunction, ([1, 2], [1, 2, 3])), - (StateSpace, ([[1., 4.], [3., 2.]], - [[1.], [-4.]], - [[1., 0.]], [[0.]]))], - ids=["tf", "ss"]) + @pytest.fixture(params=[pytest.param((sysclass, sargs + (dt, )), + id=f"{systypename}-{dtstring}") + for sysclass, systypename, sargs in [ + (TransferFunction, 'TF', ([1, 2], + [1, 2, 3])), + (StateSpace, 'SS', ([[1., 4.], [3., 2.]], + [[1.], [-4.]], + [[1., 0.]], + [[0.]])), + ] + for dt, dtstring in [(0, 'ctime'), + (True, 'dtime')] + ]) def sys(self, request): """Return some simple LTI system for testing""" # avoid construction during collection time: prevent unfiltered diff --git a/control/tests/sisotool_test.py b/control/tests/sisotool_test.py index 14e9692c1..ab5d546dd 100644 --- a/control/tests/sisotool_test.py +++ b/control/tests/sisotool_test.py @@ -17,14 +17,10 @@ class TestSisotool: """These are tests for the sisotool in sisotool.py.""" @pytest.fixture - def sys(self): + def tsys(self, request): """Return a generic SISO transfer function""" - return TransferFunction([1000], [1, 25, 100, 0]) - - @pytest.fixture - def sysdt(self): - """Return a generic SISO transfer function""" - return TransferFunction([1000], [1, 25, 100, 0], True) + dt = getattr(request, 'param', 0) + return TransferFunction([1000], [1, 25, 100, 0], dt) @pytest.fixture def sys222(self): @@ -50,8 +46,8 @@ def sys221(self): D221 = [[1., -1.]] return StateSpace(A222, B222, C221, D221) - def test_sisotool(self, sys): - sisotool(sys, Hz=False) + def test_sisotool(self, tsys): + sisotool(tsys, Hz=False) fig = plt.gcf() ax_mag, ax_rlocus, ax_phase, ax_step = fig.axes[:4] @@ -89,7 +85,7 @@ def test_sisotool(self, sys): event = type('test', (object,), {'xdata': 2.31206868287, 'ydata': 15.5983051046, 'inaxes': ax_rlocus.axes})() - _RLClickDispatcher(event=event, sys=sys, fig=fig, + _RLClickDispatcher(event=event, sys=tsys, fig=fig, ax_rlocus=ax_rlocus, sisotool=True, plotstr='-', bode_plot_params=bode_plot_params, tvect=None) @@ -118,10 +114,12 @@ def test_sisotool(self, sys): assert_array_almost_equal( ax_step.lines[0].get_data()[1][:10], step_response_moved, 4) - def test_sisotool_tvect(self, sys): + @pytest.mark.parametrize('tsys', [0, True], + indirect=True, ids=['ctime', 'dtime']) + def test_sisotool_tvect(self, tsys): # test supply tvect tvect = np.linspace(0, 1, 10) - sisotool(sys, tvect=tvect) + sisotool(tsys, tvect=tvect) fig = plt.gcf() ax_rlocus, ax_step = fig.axes[1], fig.axes[3] @@ -129,26 +127,11 @@ def test_sisotool_tvect(self, sys): event = type('test', (object,), {'xdata': 2.31206868287, 'ydata': 15.5983051046, 'inaxes': ax_rlocus.axes})() - _RLClickDispatcher(event=event, sys=sys, fig=fig, + _RLClickDispatcher(event=event, sys=tsys, fig=fig, ax_rlocus=ax_rlocus, sisotool=True, plotstr='-', bode_plot_params=dict(), tvect=tvect) assert_array_almost_equal(tvect, ax_step.lines[0].get_data()[0]) - def test_sisotool_tvect_dt(self, sysdt): - # test supply tvect - tvect = np.linspace(0, 1, 10) - sisotool(sysdt, tvect=tvect) - fig = plt.gcf() - ax_rlocus, ax_step = fig.axes[1], fig.axes[3] - - # Move the rootlocus to another point and confirm same tvect - event = type('test', (object,), {'xdata': 2.31206868287, - 'ydata': 15.5983051046, - 'inaxes': ax_rlocus.axes})() - _RLClickDispatcher(event=event, sys=sysdt, fig=fig, - ax_rlocus=ax_rlocus, sisotool=True, plotstr='-', - bode_plot_params=dict(), tvect=tvect) - assert_array_almost_equal(tvect, ax_step.lines[0].get_data()[0]) def test_sisotool_mimo(self, sys222, sys221): # a 2x2 should not raise an error: From 39dee6f38fe391027783aacc453489eb2d012000 Mon Sep 17 00:00:00 2001 From: Ben Greiner Date: Thu, 29 Apr 2021 15:11:01 +0200 Subject: [PATCH 2/3] simplify grid and sisotool logic --- control/rlocus.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/control/rlocus.py b/control/rlocus.py index 4f83c019b..91a3c9010 100644 --- a/control/rlocus.py +++ b/control/rlocus.py @@ -232,16 +232,11 @@ def root_locus(sys, kvect=None, xlim=None, ylim=None, ax.set_ylim(ylim) # Draw the grid - if grid and sisotool: + if grid: if isdtime(sys, strict=True): zgrid(ax=ax) else: - _sgrid_func(fig=fig) - elif grid: - if isdtime(sys, strict=True): - zgrid(ax=ax) - else: - _sgrid_func() + _sgrid_func(fig=fig if sisotool else None) else: ax.axhline(0., linestyle=':', color='k', linewidth=.75, zorder=-20) ax.axvline(0., linestyle=':', color='k', linewidth=.75, zorder=-20) From fae38af3578cde05e660b59cf04def5f63e6d567 Mon Sep 17 00:00:00 2001 From: Ben Greiner Date: Thu, 29 Apr 2021 19:00:47 +0200 Subject: [PATCH 3/3] more coverage --- control/rlocus.py | 2 +- control/setup.py | 5 ----- control/tests/rlocus_test.py | 33 ++++++++++++++++++++++++++++++--- setup.cfg | 1 - 4 files changed, 31 insertions(+), 10 deletions(-) delete mode 100644 control/setup.py diff --git a/control/rlocus.py b/control/rlocus.py index 91a3c9010..ee30fe489 100644 --- a/control/rlocus.py +++ b/control/rlocus.py @@ -137,7 +137,7 @@ def root_locus(sys, kvect=None, xlim=None, ylim=None, print_gain = config._get_param( 'rlocus', 'print_gain', print_gain, _rlocus_defaults) - sys_loop = sys if sys.issiso() else sys[0,0] + sys_loop = sys if sys.issiso() else sys[0, 0] # Convert numerator and denominator to polynomials if they aren't (nump, denp) = _systopoly1d(sys_loop) diff --git a/control/setup.py b/control/setup.py deleted file mode 100644 index 3ed3e3a7e..000000000 --- a/control/setup.py +++ /dev/null @@ -1,5 +0,0 @@ -def configuration(parent_package='', top_path=None): - from numpy.distutils.misc_util import Configuration - config = Configuration('control', parent_package, top_path) - config.add_subpackage('tests') - return config diff --git a/control/tests/rlocus_test.py b/control/tests/rlocus_test.py index 40c84d335..ef9bd7ecb 100644 --- a/control/tests/rlocus_test.py +++ b/control/tests/rlocus_test.py @@ -15,6 +15,7 @@ from control.bdalg import feedback +@pytest.mark.usefixtures("mplcleanup") class TestRootLocus: """These are tests for the feedback function in rlocus.py.""" @@ -32,7 +33,7 @@ class TestRootLocus: (True, 'dtime')] ]) def sys(self, request): - """Return some simple LTI system for testing""" + """Return some simple LTI systems for testing""" # avoid construction during collection time: prevent unfiltered # deprecation warning sysfn, args = request.param @@ -45,7 +46,7 @@ def check_cl_poles(self, sys, pole_list, k_list): np.testing.assert_array_almost_equal(poles, poles_expected) def testRootLocus(self, sys): - """Basic root locus plot""" + """Basic root locus (no plot)""" klist = [-1, 0, 1] roots, k_out = root_locus(sys, klist, plot=False) @@ -57,6 +58,33 @@ def test_without_gains(self, sys): roots, kvect = root_locus(sys, plot=False) self.check_cl_poles(sys, roots, kvect) + @pytest.mark.parametrize('grid', [None, True, False]) + def test_root_locus_plot_grid(self, sys, grid): + rlist, klist = root_locus(sys, grid=grid) + ax = plt.gca() + n_gridlines = sum([int(line.get_linestyle() in [':', 'dotted', + '--', 'dashed']) + for line in ax.lines]) + if grid is False: + assert n_gridlines == 2 + else: + assert n_gridlines > 2 + # TODO check validity of grid + + def test_root_locus_warnings(self): + sys = TransferFunction([1000], [1, 25, 100, 0]) + with pytest.warns(FutureWarning, match="Plot.*deprecated"): + rlist, klist = root_locus(sys, Plot=True) + with pytest.warns(FutureWarning, match="PrintGain.*deprecated"): + rlist, klist = root_locus(sys, PrintGain=True) + + def test_root_locus_neg_false_gain_nonproper(self): + """ Non proper TranferFunction with negative gain: Not implemented""" + with pytest.raises(ValueError, match="with equal order"): + root_locus(TransferFunction([-1, 2], [1, 2])) + + # TODO: cover and validate negative false_gain branch in _default_gains() + def test_root_locus_zoom(self): """Check the zooming functionality of the Root locus plot""" system = TransferFunction([1000], [1, 25, 100, 0]) @@ -104,4 +132,3 @@ def test_rlocus_default_wn(self): [-1e-2, 1-1e7j, 1+1e7j], [0, -1e7j, 1e7j], 1)) ct.root_locus(sys) - diff --git a/setup.cfg b/setup.cfg index c72ef19a8..5b1ce28a7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,4 +5,3 @@ universal=1 addopts = -ra filterwarnings = error:.*matrix subclass:PendingDeprecationWarning -