8000 refactoring of pzmap into response/plot + unit test changes · bnavigator/python-control@6b5cafc · GitHub
[go: up one dir, main page]

8000
Skip to content

Commit 6b5cafc

Browse files
committed
refactoring of pzmap into response/plot + unit test changes
1 parent b389b64 commit 6b5cafc

File tree

5 files changed

+212
-138
lines changed

5 files changed

+212
-138
lines changed

control/pzmap.py

Lines changed: 205 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,133 +1,262 @@
11
# pzmap.py - computations involving poles and zeros
22
#
3-
# Author: Richard M. Murray
3+
# Original author: Richard M. Murray
44
# Date: 7 Sep 2009
55
#
66
# This file contains functions that compute poles, zeros and related
77
# quantities for a linear system.
88
#
9-
# Copyright (c) 2009 by California Institute of Technology
10-
# All rights reserved.
11-
#
12-
# Redistribution and use in source and binary forms, with or without
13-
# modification, are permitted provided that the following conditions
14-
# are met:
15-
#
16-
# 1. Redistributions of source code must retain the above copyright
17-
# notice, this list of conditions and the following disclaimer.
18-
#
19-
# 2. Redistributions in binary form must reproduce the above copyright
20-
# notice, this list of conditions and the following disclaimer in the
21-
# documentation and/or other materials provided with the distribution.
22-
#
23-
# 3. Neither the name of the California Institute of Technology nor
24-
# the names of its contributors may be used to endorse or promote
25-
# products derived from this software without specific prior
26-
# written permission.
27-
#
28-
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
29-
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
30-
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
31-
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL CALTECH
32-
# OR THE CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
33-
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
34-
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
35-
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
36-
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
37-
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
38-
# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
39-
# SUCH DAMAGE.
40-
#
419

10+
import numpy as np
4211
from numpy import real, imag, linspace, exp, cos, sin, sqrt
12+
import matplotlib.pyplot as plt
4313
from math import pi
14+
import itertools
15+
import warnings
16+
4417
from .lti import LTI
4518
from .iosys import isdtime, isctime
4619
from .grid import sgrid, zgrid, nogrid
20+
from .statesp import StateSpace
21+
from .xferfcn import TransferFunction
22+
from .freqplot import _freqplot_defaults, _get_line_labels
4723
from . import config
4824

49-
__all__ = ['pzmap']
25+
__all__ = ['pzmap_response', 'pzmap_plot', 'pzmap']
5026

5127

5228
# Define default parameter values for this module
5329
_pzmap_defaults = {
5430
'pzmap.grid': False, # Plot omega-damping grid
55-
'pzmap.plot': True, # Generate plot using Matplotlib
31+
'pzmap.marker_size': 6, # Size of the markers
32+
'pzmap.marker_width': 1.5, # Width of the markers
5633
}
5734

5835

36+
# Classes for keeping track of pzmap plots
37+
class PoleZeroResponseList(list):
341A 38+
def plot(self, *args, **kwargs):
39+
return pzmap_plot(self, *args, **kwargs)
40+
41+
42+
class PoleZeroResponseData:
43+
def __init__(
44+
self, poles, zeros, gains=None, loci=None, dt=None, sysname=None):
45+
self.poles = poles
46+
self.zeros = zeros
< F438 /code>47+
self.gains = gains
48+
self.loci = loci
49+
self.dt = dt
50+
self.sysname = sysname
51+
52+
# Implement functions to allow legacy assignment to tuple
53+
def __iter__(self):
54+
return iter((self.poles, self.zeros))
55+
56+
def plot(self, *args, **kwargs):
57+
return pzmap_plot(self, *args, **kwargs)
58+
59+
60+
# pzmap response funciton
61+
def pzmap_response(sysdata):
62+
# Convert the first argument to a list
63+
syslist = sysdata if isinstance(sysdata, (list, tuple)) else [sysdata]
64+
65+
responses = []
66+
for idx, sys in enumerate(syslist):
67+
responses.append(
68+
PoleZeroResponseData(
69+
sys.poles(), sys.zeros(), dt=sys.dt, sysname=sys.name))
70+
71+
if isinstance(sysdata, (list, tuple)):
72+
return PoleZeroResponseList(responses)
73+
else:
74+
return responses[0]
75+
76+
5977
# TODO: Implement more elegant cross-style axes. See:
60-
# http://matplotlib.sourceforge.net/examples/axes_grid/demo_axisline_style.html
61-
# http://matplotlib.sourceforge.net/examples/axes_grid/demo_curvelinear_grid.html
62-
def pzmap(sys, plot=None, grid=None, title='Pole Zero Map', **kwargs):
78+
# https://matplotlib.org/2.0.2/examples/axes_grid/demo_axisline_style.html
79+
# https://matplotlib.org/2.0.2/examples/axes_grid/demo_curvelinear_grid.html
80+
def pzmap_plot(
81+
data, plot=None, grid=None, title=None, marker_color=None,
82+
marker_size=None, marker_width=None, legend_loc='upper right',
83+
**kwargs):
6384
"""Plot a pole/zero map for a linear system.
6485
6586
Parameters
6687
----------
67-
sys: LTI (StateSpace or TransferFunction)
68-
Linear system for which poles and zeros are computed.
69-
plot: bool, optional
70-
If ``True`` a graph is generated with Matplotlib,
71-
otherwise the poles and zeros are only computed and returned.
88+
sysdata: List of PoleZeroResponseData objects or LTI systems
89+
List of pole/zero response data objects generated by pzmap_response
90+
or rootlocus_response() that are to be plotted. If a list of systems
91+
is given, the poles and zeros of those systems will be plotted.
7292
grid: boolean (default = False)
7393
If True plot omega-damping grid.
94+
plot: bool, optional
95+
(legacy) If ``True`` a graph is generated with Matplotlib,
96+
otherwise the poles and zeros are only computed and returned.
97+
If this argument is present, the legacy value of poles and
98+
zero is returned.
7499
75100
Returns
76101
-------
77-
poles: array
78-
The systems poles
79-
zeros: array
80-
The system's zeros.
102+
lines : List of Line2D
103+
Array of Line2D objects for each set of markers in the plot. The
104+
shape of the array is given by (nsys, 2) where nsys is the number
105+
of systems or Nyquist responses passed to the function. The second
106+
index specifies the pzmap object type:
107+
108+
* lines[idx, 0]: poles
109+
* lines[idx, 1]: zeros
110+
111+
poles, zeros: list of arrays
112+
(legacy) If the `plot` keyword is given, the system poles and zeros
113+
are returned.
81114
82-
Notes
115+
Notes (TODO: update)
83116
-----
84117
The pzmap function calls matplotlib.pyplot.axis('equal'), which means
85118
that trying to reset the axis limits may not behave as expected. To
86119
change the axis limits, use matplotlib.pyplot.gca().axis('auto') and
87120
then set the axis limits to the desired values.
88121
89122
"""
90-
# Check to see if legacy 'Plot' keyword was used
91-
if 'Plot' in kwargs:
92-
import warnings
93-
warnings.warn("'Plot' keyword is deprecated in pzmap; use 'plot'",
94-
FutureWarning)
95-
plot = kwargs.pop('Plot')
96-
97-
# Make sure there were no extraneous keywords
98-
if kwargs:
99-
raise TypeError("unrecognized keywords: ", str(kwargs))
100-
101123
# Get parameter values
102-
plot = config._get_param('pzmap', 'plot', plot, True)
103124
grid = config._get_param('pzmap', 'grid', grid, False)
125+
marker_size = config._get_param('pzmap', 'marker_size', marker_size, 6)
126+
marker_width = config._get_param('pzmap', 'marker_width', marker_width, 1.5)
127+
fre A851 qplot_rcParams = config._get_param(
128+
'freqplot', 'rcParams', kwargs, _freqplot_defaults,
129+
pop=True, last=True)
130+
131+
# If argument was a singleton, turn it into a tuple
132+
if not isinstance(data, (list, tuple)):
133+
data = [data]
104134

105-
if not isinstance(sys, LTI):
106-
raise TypeError('Argument ``sys``: must be a linear system.')
135+
# If we are passed a list of systems, compute response first
136+
if all([isinstance(
137+
sys, (StateSpace, TransferFunction)) for sys in data]):
138+
# Get the response, popping off keywords used there
139+
pzmap_responses = pzmap_response(data)
140+
elif all([isinstance(d, PoleZeroResponseData) for d in data]):
141+
pzmap_responses = data
142+
else:
143+
raise TypeError("unknown system data type")
107144

108-
poles = sys.poles()
109-
zeros = sys.zeros()
145+
# Legacy return value processing
146+
if plot is not None:
147+
warnings.warn(
148+
"`pzmap_plot` return values of poles, zeros is deprecated; "
149+
"use pzmap_response()", DeprecationWarning)
150+
151+
# Extract out the values that we will eventually return
152+
poles = [response.poles for response in pzmap_responses]
153+
zeros = [response.zeros for response in pzmap_responses]
154+
155+
if plot is False:
156+
if len(data) == 1:
157+
return poles[0], zeros[0]
158+
else:
159+
return poles, zeros
110160

111-
if (plot):
112-
import matplotlib.pyplot as plt
161+
# Initialize the figure
162+
# TODO: turn into standard utility function
163+
fig = plt.gcf()
164+
axs = fig.get_axes()
165+
if len(axs) > 1:
166+
# Need to generate a new figure
167+
fig, axs = plt.figure(), []
113168

169+
with plt.rc_context(freqplot_rcParams):
114170
if grid:
115-
if isdtime(sys, strict=True):
171+
plt.clf()
172+
if all([response.isctime() for response in data]):
173+
ax, fig = sgrid()
174+
elif all([response.isdtime() for response in data]):
116175
ax, fig = zgrid()
117176
else:
118-
ax, fig = sgrid()
119-
else:
177+
ValueError(
178+
"incompatible time responses; don't know how to grid")
179+
elif len(axs) == 0:
120180
ax, fig = nogrid()
181+
else:
182+
# Use the existing axes
183+
ax = axs[0]
184+
185+
# Handle color cycle manually as all singular values
186+
# of the same systems are expected to be of the same color
187+
color_cycle = plt.rcParams['axes.prop_cycle'].by_key()['color']
188+
color_offset = 0
189+
if len(ax.lines) > 0:
190+
last_color = ax.lines[-1].get_color()
191+
if last_color in color_cycle:
192+
color_offset = color_cycle.index(last_color) + 1
193+
194+
# Create a list of lines for the output
195+
out = np.empty((len(pzmap_responses), 2), dtype=object)
196+
for i, j in itertools.product(range(out.shape[0]), range(out.shape[1])):
197+
out[i, j] = [] # unique list in each element
198+
199+
for idx, response in enumerate(pzmap_responses):
200+
poles = response.poles
201+
zeros = response.zeros
202+
203+
# Get the color to use for this system
204+
if marker_color is None:
205+
color = color_cycle[(color_offset + idx) % len(color_cycle)]
206+
else:
207+
color = maker_color
121208

122209
# Plot the locations of the poles and zeros
123210
if len(pol 90A1 es) > 0:
124-
ax.scatter(real(poles), imag(poles), s=50, marker='x',
125-
facecolors='k')
211+
out[idx, 0] = ax.plot(
212+
real(poles), imag(poles), marker='x', linestyle='',
213+
markeredgecolor=color, markerfacecolor=color,
214+
markersize=marker_size, markeredgewidth=marker_width,
215+
label=response.sysname)
126216
if len(zeros) > 0:
127-
ax.scatter(real(zeros), imag(zeros), s=50, marker='o',
128-
facecolors='none', edgecolors='k')
217+
out[idx, 1] = ax.plot(
218+
real(zeros), imag(zeros), marker='o', linestyle='',
219+
markeredgecolor=color, markerfacecolor='none',
220+
markersize=marker_size, markeredgewidth=marker_width)
221+
222+
# List of systems that are included in this plot
223+
lines, labels = _get_line_labels(ax)
224+
225+
# Update the lines to use tuples for poles and zeros
226+
from matplotlib.lines import Line2D
227+
from matplotlib.legend_handler import HandlerTuple
228+
line_tuples = []
229+
for pole_line in lines:
230+
zero_line = Line2D(
231+
[0], [0], marker='o', linestyle='',
232+
markeredgecolor=pole_line.get_markerfacecolor(),
233+
markerfacecolor='none', markersize=marker_size,
234+
markeredgewidth=marker_width)
235+
handle = (pole_line, zero_line)
236+
line_tuples.append(handle)
237+
print(line_tuples)
238+
239+
# Add legend if there is more than one system plotted
240+
if len(labels) > 1 and legend_loc is not False:
241+
with plt.rc_context(freqplot_rcParams):
242+
ax.legend(
243+
line_tuples, labels, loc=legend_loc,
244+
handler_map={tuple: HandlerTuple(ndivide=None)})
245+
246+
# Add the title
247+
if title is None:
248+
title = "Pole/zero map for " + ", ".join(labels)
249+
with plt.rc_context(freqplot_rcParams):
250+
fig.suptitle(title)
251+
252+
# Legacy processing: return locations of poles and zeros as a tuple
253+
if plot is True:
254+
if len(data) == 1:
255+
return poles, zeros
256+
else:
257+
TypeError("system lists not supported with legacy return values")
258+
259+
return out
129260

130-
plt.title(title)
131261

132-
# Return locations of poles and zeros as a tuple
133-
return poles, zeros
262+
pzmap = pzmap_plot

0 commit comments

Comments
 (0)
0