8000 Merge pull request #456 from bnavigator/fix-mpl-grid · python-control/python-control@6ede92e · GitHub
[go: up one dir, main page]

Skip to content

Commit 6ede92e

Browse files
authored
Merge pull request #456 from bnavigator/fix-mpl-grid
Fix pzmap grid (matplotlib angle_helper)
2 parents 08d5e6c + 5632796 commit 6ede92e

File tree

4 files changed

+147
-13
lines changed

4 files changed

+147
-13
lines changed

control/grid.py

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,20 @@
1212
class FormatterDMS(object):
1313
'''Transforms angle ticks to damping ratios'''
1414
def __call__(self, direction, factor, values):
15-
angles_deg = values/factor
15+
angles_deg = np.asarray(values)/factor
1616
damping_ratios = np.cos((180-angles_deg) * np.pi/180)
1717
ret = ["%.2f" % val for val in damping_ratios]
1818
return ret
1919

2020

2121
class ModifiedExtremeFinderCycle(angle_helper.ExtremeFinderCycle):
22-
'''Changed to allow only left hand-side polar grid'''
22+
'''Changed to allow only left hand-side polar grid
23+
24+
https://matplotlib.org/_modules/mpl_toolkits/axisartist/angle_helper.html#ExtremeFinderCycle.__call__
25+
'''
2326
def __call__(self, transform_xy, x1, y1, x2, y2):
24-
x_, y_ = np.linspace(x1, x2, self.nx), np.linspace(y1, y2, self.ny)
25-
x, y = np.meshgrid(x_, y_)
27+
x, y = np.meshgrid(
28+
np.linspace(x1, x2, self.nx), np.linspace(y1, y2, self.ny))
2629
lon, lat = transform_xy(np.ravel(x), np.ravel(y))
2730

2831
with np.errstate(invalid='ignore'):
@@ -31,17 +34,33 @@ def __call__(self, transform_xy, x1, y1, x2, y2):
3134
# Changed from 180 to 360 to be able to span only
3235
# 90-270 (left hand side)
3336
lon -= 360. * ((lon - lon0) > 360.)
34-
if self.lat_cycle is not None:
37+
if self.lat_cycle is not None: # pragma: no cover
3538
lat0 = np.nanmin(lat)
36-
# Changed from 180 to 360 to be able to span only
37-
# 90-270 (left hand side)
38-
lat -= 360. * ((lat - lat0) > 360.)
39+
lat -= 360. * ((lat - lat0) > 180.)
3940

4041
lon_min, lon_max = np.nanmin(lon), np.nanmax(lon)
4142
lat_min, lat_max = np.nanmin(lat), np.nanmax(lat)
4243

4344
lon_min, lon_max, lat_min, lat_max = \
44-
self._adjust_extremes(lon_min, lon_max, lat_min, lat_max)
45+
self._add_pad(lon_min, lon_max, lat_min, lat_max)
46+
47+
# check cycle
48+
if self.lon_cycle:
49+
lon_max = min(lon_max, lon_min + self.lon_cycle)
50+
if self.lat_cycle: # pragma: no cover
51+
lat_max = min(lat_max, lat_min + self.lat_cycle)
52+
53+
if self.lon_minmax is not None:
54+
min0 = self.lon_minmax[0]
55+
lon_min = max(min0, lon_min)
56+
max0 = self.lon_minmax[1]
57+
lon_max = min(max0, lon_max)
58+
59+
if self.lat_minmax is not None:
60+
min0 = self.lat_minmax[0]
61+
lat_min = max(min0, lat_min)
62+
max0 = self.lat_minmax[1]
63+
lat_max = min(max0, lat_max)
4564

4665
return lon_min, lon_max, lat_min, lat_max
4766

control/pzmap.py

Lines changed: 3 additions & 3 deletions
raise TypeError('Argument ``sys``: must be a linear system.')
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@
5858
# TODO: Implement more elegant cross-style axes. See:
5959
# http://matplotlib.sourceforge.net/examples/axes_grid/demo_axisline_style.html
6060
# http://matplotlib.sourceforge.net/examples/axes_grid/demo_curvelinear_grid.html
61-
def pzmap(sys, plot=True, grid=False, title='Pole Zero Map', **kwargs):
61+
def pzmap(sys, plot=None, grid=None, title='Pole Zero Map', **kwargs):
6262
"""
6363
Plot a pole/zero map for a linear system.
6464
@@ -87,8 +87,8 @@ def pzmap(sys, plot=True, grid=False, title='Pole Zero Map', **kwargs):
8787
plot = kwargs['Plot']
8888

8989
# Get parameter values
90-
plot = config._get_param('rlocus', 'plot', plot, True)
91-
grid = config._get_param('rlocus', 'grid', grid, False)
90+
plot = config._get_param('pzmap', 'plot', plot, True)
91+
grid = config._get_param('pzmap', 'grid', grid, False)
9292

9393
if not isinstance(sys, LTI):
9494

control/tests/conftest.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,39 @@
11
# contest.py - pytest local plugins and fixtures
22

3-
import control
43
import os
54

5+
import matplotlib as mpl
66
import pytest
77

8+
import control
9+
810

911
@pytest.fixture(scope="session", autouse=True)
1012
def use_numpy_ndarray():
1113
"""Switch the config to use ndarray instead of matrix"""
1214
if os.getenv("PYTHON_CONTROL_STATESPACE_ARRAY") == "1":
1315
control.config.defaults['statesp.use_numpy_matrix'] = False
16+
17+
18+
@pytest.fixture(scope="function")
19+
def editsdefaults():
20+
"""Make sure any changes to the defaults only last during a test"""
21+
restore = control.config.defaults.copy()
22+
yield
23+
control.config.defaults.update(restore)
24+
25+
26+
@pytest.fixture(scope="function")
27+
def mplcleanup():
28+
"""Workaround for python2
29+
30+
python 2 does not like to mix the original mpl decorator with pytest
31+
fixtures. So we roll our own.
32+
"""
33+
save = mpl.units.registry.copy()
34+
try:
35+
yield
36+
finally:
37+
mpl.units.registry.clear()
38+
mpl.units.registry.update(save)
39+
mpl.pyplot.close("all")

control/tests/pzmap_test.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# -*- coding: utf-8 -*-
2+
""" pzmap_test.py - test pzmap()
3+
4+
Created on Thu Aug 20 20:06:21 2020
5+
6+
@author: bnavigator
7+
"""
8+
9+
import matplotlib
10+
import numpy as np
11+
import pytest
12+
from matplotlib import pyplot as plt
13+
from mpl_toolkits.axisartist import Axes as mpltAxes
14+
15+
from control import TransferFunction, config, pzmap
16+
17+
18+
@pytest.mark.parametrize("kwargs",
19+
[pytest.param(dict(), id="default"),
20+
pytest.param(dict(plot=False), id="plot=False"),
21+
pytest.param(dict(plot=True), id="plot=True"),
22+
pytest.param(dict(grid=True), id="grid=True"),
23+
pytest.param(dict(title="My Title"), id="title")])
24+
@pytest.mark.parametrize("setdefaults", [False, True], ids=["kw", "config"])
25+
@pytest.mark.parametrize("dt", [0, 1], ids=["s", "z"])
26+
def test_pzmap(kwargs, setdefaults, dt, editsdefaults, mplcleanup):
27+
"""Test pzmap"""
28+
# T from from pvtol-nested example
29+
T = TransferFunction([-9.0250000e-01, -4.7200750e+01, -8.6812900e+02,
30+
+5.6261850e+03, +2.1258472e+05, +8.4724600e+05,
31+
+1.0192000e+06, +2.3520000e+05],
32+
[9.02500000e-03, 9.92862812e-01, 4.96974094e+01,
33+
1.35705659e+03, 2.09294163e+04, 1.64898435e+05,
34+
6.54572220e+05, 1.25274600e+06, 1.02420000e+06,
35+
2.35200000e+05],
36+
dt)
37+
38+
Pref = [-23.8877+19.3837j, -23.8877-19.3837j, -23.8349+15.7846j,
39+
-23.8349-15.7846j, -5.2320 +0.4117j, -5.2320 -0.4117j,
40+
-2.2246 +0.0000j, -1.5160 +0.0000j, -0.3627 +0.0000j]
41+
Zref = [-23.8877+19.3837j, -23.8877-19.3837j, +14.3637 +0.0000j,
42+
-14.3637 +0.0000j, -2.2246 +0.0000j, -2.0000 +0.0000j,
43+
-0.3000 +0.0000j]
44+
45+
pzkwargs = kwargs.copy()
46+
if setdefaults:
47+
for k in ['plot', 'grid']:
48+
if k in pzkwargs:
49+
v = pzkwargs.pop(k)
50+
config.set_defaults('pzmap', **{k: v})
51+
52+
P, Z = pzmap(T, **pzkwargs)
53+
54+
np.testing.assert_allclose(P, Pref, rtol=1e-3)
55+
np.testing.assert_allclose(Z, Zref, rtol=1e-3)
56+
57+
if kwargs.get('plot', True):
58+
ax = plt.gca()
59+
60+
assert ax.get_title() == kwargs.get('title', 'Pole Zero Map')
61+
62+
# FIXME: This won't work when zgrid and sgrid are unified
63+
children = ax.get_children()
64+
has_zgrid = False
65+
for c in children:
66+
if isinstance(c, matplotlib.text.Annotation):
67+
if r'\pi' in c.get_text():
68+
has_zgrid = True
69+
has_sgrid = isinstance(ax, mpltAxes)
70+
71+
if kwargs.get('grid', False):
72+
assert dt == has_zgrid
73+
assert dt != has_sgrid
74+
else:
75+
assert not has_zgrid
76+
assert not has_sgrid
77+
else:
78+
assert not plt.get_fignums()
79+
80+
81+
def test_pzmap_warns():
82+
with pytest.warns(FutureWarning):
83+
pzmap(TransferFunction([1], [1, 2]), Plot=True)
84+
85+
86+
def test_pzmap_raises():
87+
with pytest.raises(TypeError):
88+
# not an LTI system
89+
pzmap(([1], [1,2]))

0 commit comments

Comments
 (0)
0