Skip to content

Fix pzmap grid (matplotlib angle_helper) #456

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Aug 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 28 additions & 9 deletions control/grid.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'):
Expand All @@ -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

Expand Down
6 changes: 3 additions & 3 deletions control/pzmap.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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.')
Expand Down
28 changes: 27 additions & 1 deletion control/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -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")
89 changes: 89 additions & 0 deletions control/tests/pzmap_test.py
Original file line number Diff line number Diff line change
@@ -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]))