diff --git a/control/grid.py b/control/grid.py index a383dd27c..07ca4a59d 100644 --- a/control/grid.py +++ b/control/grid.py @@ -12,17 +12,20 @@ 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 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'): @@ -31,17 +34,33 @@ 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) 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: # pragma: no cover + 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 diff --git a/control/pzmap.py b/control/pzmap.py index fe8e551a0..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. @@ -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.') 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..8d41807b8 --- /dev/null +++ b/control/tests/pzmap_test.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- +""" pzmap_test.py - test pzmap() + +Created on Thu Aug 20 20:06:21 2020 + +@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 + + +@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') + + # 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): + 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(): + 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])) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy