From df0051c2849bc7accf772ee80faeb752b0478002 Mon Sep 17 00:00:00 2001 From: bnavigator Date: Thu, 20 Aug 2020 21:14:48 +0200 Subject: [PATCH 1/5] update mpl_toolkit function --- control/grid.py | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/control/grid.py b/control/grid.py index a383dd27c..374b69ead 100644 --- a/control/grid.py +++ b/control/grid.py @@ -19,10 +19,13 @@ def __call__(self, direction, factor, values): class ModifiedExtremeFinderCycle(angle_helper.ExtremeFinderCycle): - '''Changed to allow only left hand-side polar grid''' + '''Changed to allow only left hand-side polar grid + + https://matplotlib.org/_modules/mpl_toolkits/axisartist/angle_helper.html#ExtremeFinderCycle.__call__ + ''' def __call__(self, transform_xy, x1, y1, x2, y2): - x_, y_ = np.linspace(x1, x2, self.nx), np.linspace(y1, y2, self.ny) - x, y = np.meshgrid(x_, y_) + x, y = np.meshgrid( + np.linspace(x1, x2, self.nx), np.linspace(y1, y2, self.ny)) lon, lat = transform_xy(np.ravel(x), np.ravel(y)) with np.errstate(invalid='ignore'): @@ -41,7 +44,25 @@ def __call__(self, transform_xy, x1, y1, x2, y2): lat_min, lat_max = np.nanmin(lat), np.nanmax(lat) lon_min, lon_max, lat_min, lat_max = \ - self._adjust_extremes(lon_min, lon_max, lat_min, lat_max) + self._add_pad(lon_min, lon_max, lat_min, lat_max) + + # check cycle + if self.lon_cycle: + lon_max = min(lon_max, lon_min + self.lon_cycle) + if self.lat_cycle: + lat_max = min(lat_max, lat_min + self.lat_cycle) + + if self.lon_minmax is not None: + min0 = self.lon_minmax[0] + lon_min = max(min0, lon_min) + max0 = self.lon_minmax[1] + lon_max = min(max0, lon_max) + + if self.lat_minmax is not None: + min0 = self.lat_minmax[0] + lat_min = max(min0, lat_min) + max0 = self.lat_minmax[1] + lat_max = min(max0, lat_max) return lon_min, lon_max, lat_min, lat_max From abc0c3c002b485b2a05101a8459628ad9eafe333 Mon Sep 17 00:00:00 2001 From: bnavigator Date: Thu, 20 Aug 2020 21:35:47 +0200 Subject: [PATCH 2/5] fix grid formatter --- control/grid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/control/grid.py b/control/grid.py index 374b69ead..0d0e8b2ea 100644 --- a/control/grid.py +++ b/control/grid.py @@ -12,7 +12,7 @@ class FormatterDMS(object): '''Transforms angle ticks to damping ratios''' def __call__(self, direction, factor, values): - angles_deg = values/factor + angles_deg = np.asarray(values)/factor damping_ratios = np.cos((180-angles_deg) * np.pi/180) ret = ["%.2f" % val for val in damping_ratios] return ret From bdbd1985bbb07f5877825f956aeb13ab765094f4 Mon Sep 17 00:00:00 2001 From: bnavigator Date: Thu, 20 Aug 2020 23:21:53 +0200 Subject: [PATCH 3/5] fix wrong config read on pzmap --- control/pzmap.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/control/pzmap.py b/control/pzmap.py index fe8e551a0..012fccca3 100644 --- a/control/pzmap.py +++ b/control/pzmap.py @@ -87,8 +87,8 @@ def pzmap(sys, plot=True, grid=False, title='Pole Zero Map', **kwargs): plot = kwargs['Plot'] # Get parameter values - plot = config._get_param('rlocus', 'plot', plot, True) - grid = config._get_param('rlocus', 'grid', grid, False) + plot = config._get_param('pzmap', 'plot', plot, True) + grid = config._get_param('pzmap', 'grid', grid, False) if not isinstance(sys, LTI): raise TypeError('Argument ``sys``: must be a linear system.') From 2ca522083594993c91db98f700ce8c43b86a77fd Mon Sep 17 00:00:00 2001 From: bnavigator Date: Thu, 20 Aug 2020 23:22:15 +0200 Subject: [PATCH 4/5] add pzmap_test --- control/tests/conftest.py | 28 ++++++++++++++- control/tests/pzmap_test.py | 71 +++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 1 deletion(-) create mode 100755 control/tests/pzmap_test.py diff --git a/control/tests/conftest.py b/control/tests/conftest.py index e98bbe1d7..60c3d0de1 100755 --- a/control/tests/conftest.py +++ b/control/tests/conftest.py @@ -1,13 +1,39 @@ # contest.py - pytest local plugins and fixtures -import control import os +import matplotlib as mpl import pytest +import control + @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 + + +@pytest.fixture(scope="function") +def editsdefaults(): + """Make sure any changes to the defaults only last during a test""" + restore = control.config.defaults.copy() + yield + control.config.defaults.update(restore) + + +@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. + """ + save = mpl.units.registry.copy() + try: + yield + finally: + mpl.units.registry.clear() + mpl.units.registry.update(save) + mpl.pyplot.close("all") diff --git a/control/tests/pzmap_test.py b/control/tests/pzmap_test.py new file mode 100755 index 000000000..5903999b0 --- /dev/null +++ b/control/tests/pzmap_test.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +""" pzmap_test.py - test pzmap() + +Created on Thu Aug 20 20:06:21 2020 + +@author: bnavigator +""" + +import numpy as np +import pytest +from matplotlib import pyplot as plt + +from control import TransferFunction, config, pzmap + + +@pytest.mark.parametrize("kwargs", + [pytest.param(dict(), id="default"), + pytest.param(dict(plot=False), id="plot=False"), + pytest.param(dict(plot=True), id="plot=True"), + pytest.param(dict(grid=True), id="grid=True"), + pytest.param(dict(title="My Title"), id="title")]) +@pytest.mark.parametrize("setdefaults", [False, True], ids=["kw", "config"]) +@pytest.mark.parametrize("dt", [0, 1], ids=["s", "z"]) +def test_pzmap(kwargs, setdefaults, dt, editsdefaults, mplcleanup): + """Test pzmap""" + # T from from pvtol-nested example + T = TransferFunction([-9.0250000e-01, -4.7200750e+01, -8.6812900e+02, + +5.6261850e+03, +2.1258472e+05, +8.4724600e+05, + +1.0192000e+06, +2.3520000e+05], + [9.02500000e-03, 9.92862812e-01, 4.96974094e+01, + 1.35705659e+03, 2.09294163e+04, 1.64898435e+05, + 6.54572220e+05, 1.25274600e+06, 1.02420000e+06, + 2.35200000e+05], + dt) + + Pref = [-23.8877+19.3837j, -23.8877-19.3837j, -23.8349+15.7846j, + -23.8349-15.7846j, -5.2320 +0.4117j, -5.2320 -0.4117j, + -2.2246 +0.0000j, -1.5160 +0.0000j, -0.3627 +0.0000j] + Zref = [-23.8877+19.3837j, -23.8877-19.3837j, +14.3637 +0.0000j, + -14.3637 +0.0000j, -2.2246 +0.0000j, -2.0000 +0.0000j, + -0.3000 +0.0000j] + + pzkwargs = kwargs.copy() + if setdefaults: + for k in ['plot', 'grid']: + if k in pzkwargs: + v = pzkwargs.pop(k) + config.set_defaults('pzmap', **{k: v}) + + P, Z = pzmap(T, **pzkwargs) + + np.testing.assert_allclose(P, Pref, rtol=1e-3) + np.testing.assert_allclose(Z, Zref, rtol=1e-3) + + if kwargs.get('plot', True): + ax = plt.gca() + assert ax.get_title() == kwargs.get('title', 'Pole Zero Map') + if kwargs.get('grid', False): + # TODO: check for correct grid + pass + + +def test_pzmap_warns(): + with pytest.warns(FutureWarning): + pzmap(TransferFunction([1], [1, 2]), Plot=True) + + +def test_pzmap_raises(): + with pytest.raises(TypeError): + # not an LTI system + pzmap(([1], [1,2])) From 5632796bea9d6047b2b5cfdf0df4b54545840905 Mon Sep 17 00:00:00 2001 From: bnavigator Date: Fri, 21 Aug 2020 01:31:11 +0200 Subject: [PATCH 5/5] test for correct grid and no plot. fix pzmap config handling --- control/grid.py | 8 +++----- control/pzmap.py | 2 +- control/tests/pzmap_test.py | 22 ++++++++++++++++++++-- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/control/grid.py b/control/grid.py index 0d0e8b2ea..07ca4a59d 100644 --- a/control/grid.py +++ b/control/grid.py @@ -34,11 +34,9 @@ def __call__(self, transform_xy, x1, y1, x2, y2): # Changed from 180 to 360 to be able to span only # 90-270 (left hand side) lon -= 360. * ((lon - lon0) > 360.) - if self.lat_cycle is not None: + if self.lat_cycle is not None: # pragma: no cover lat0 = np.nanmin(lat) - # Changed from 180 to 360 to be able to span only - # 90-270 (left hand side) - lat -= 360. * ((lat - lat0) > 360.) + lat -= 360. * ((lat - lat0) > 180.) lon_min, lon_max = np.nanmin(lon), np.nanmax(lon) lat_min, lat_max = np.nanmin(lat), np.nanmax(lat) @@ -49,7 +47,7 @@ def __call__(self, transform_xy, x1, y1, x2, y2): # check cycle if self.lon_cycle: lon_max = min(lon_max, lon_min + self.lon_cycle) - if self.lat_cycle: + if self.lat_cycle: # pragma: no cover lat_max = min(lat_max, lat_min + self.lat_cycle) if self.lon_minmax is not None: diff --git a/control/pzmap.py b/control/pzmap.py index 012fccca3..a7752e484 100644 --- a/control/pzmap.py +++ b/control/pzmap.py @@ -58,7 +58,7 @@ # TODO: Implement more elegant cross-style axes. See: # http://matplotlib.sourceforge.net/examples/axes_grid/demo_axisline_style.html # http://matplotlib.sourceforge.net/examples/axes_grid/demo_curvelinear_grid.html -def pzmap(sys, plot=True, grid=False, title='Pole Zero Map', **kwargs): +def pzmap(sys, plot=None, grid=None, title='Pole Zero Map', **kwargs): """ Plot a pole/zero map for a linear system. diff --git a/control/tests/pzmap_test.py b/control/tests/pzmap_test.py index 5903999b0..8d41807b8 100755 --- a/control/tests/pzmap_test.py +++ b/control/tests/pzmap_test.py @@ -6,9 +6,11 @@ @author: bnavigator """ +import matplotlib import numpy as np import pytest from matplotlib import pyplot as plt +from mpl_toolkits.axisartist import Axes as mpltAxes from control import TransferFunction, config, pzmap @@ -54,10 +56,26 @@ def test_pzmap(kwargs, setdefaults, dt, editsdefaults, mplcleanup): if kwargs.get('plot', True): ax = plt.gca() + assert ax.get_title() == kwargs.get('title', 'Pole Zero Map') + + # FIXME: This won't work when zgrid and sgrid are unified + children = ax.get_children() + has_zgrid = False + for c in children: + if isinstance(c, matplotlib.text.Annotation): + if r'\pi' in c.get_text(): + has_zgrid = True + has_sgrid = isinstance(ax, mpltAxes) + if kwargs.get('grid', False): - # TODO: check for correct grid - pass + assert dt == has_zgrid + assert dt != has_sgrid + else: + assert not has_zgrid + assert not has_sgrid + else: + assert not plt.get_fignums() def test_pzmap_warns():