Skip to content

Commit 1451f3a

Browse files
committed
pz -> pole_zero + add loci plotting
1 parent 6b5cafc commit 1451f3a

File tree

2 files changed

+108
-42
lines changed

2 files changed

+108
-42
lines changed

control/pzmap.py

Lines changed: 107 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from .freqplot import _freqplot_defaults, _get_line_labels
2323
from . import config
2424

25-
__all__ = ['pzmap_response', 'pzmap_plot', 'pzmap']
25+
__all__ = ['pole_zero_map', 'root_locus_map', 'pole_zero_plot', 'pzmap']
2626

2727

2828
# Define default parameter values for this module
@@ -34,18 +34,21 @@
3434

3535

3636
# Classes for keeping track of pzmap plots
37-
class PoleZeroResponseList(list):
37+
class RootLocusList(list):
3838
def plot(self, *args, **kwargs):
39-
return pzmap_plot(self, *args, **kwargs)
39+
return pole_zero_plot(self, *args, **kwargs)
4040

4141

42-
class PoleZeroResponseData:
42+
class RootLocusData:
4343
def __init__(
44-
self, poles, zeros, gains=None, loci=None, dt=None, sysname=None):
44+
self, poles, zeros, gains=None, loci=None, xlim=None, ylim=None,
45+
dt=None, sysname=None):
4546
self.poles = poles
4647
self.zeros = zeros
4748
self.gains = gains
4849
self.loci = loci
50+
self.xlim = xlim
51+
self.ylim = ylim
4952
self.dt = dt
5053
self.sysname = sysname
5154

@@ -54,38 +57,78 @@ def __iter__(self):
5457
return iter((self.poles, self.zeros))
5558

5659
def plot(self, *args, **kwargs):
57-
return pzmap_plot(self, *args, **kwargs)
60+
return pole_zero_plot(self, *args, **kwargs)
5861

5962

60-
# pzmap response funciton
61-
def pzmap_response(sysdata):
63+
# Pole/zero map
64+
def pole_zero_map(sysdata):
6265
# Convert the first argument to a list
6366
syslist = sysdata if isinstance(sysdata, (list, tuple)) else [sysdata]
6467

6568
responses = []
6669
for idx, sys in enumerate(syslist):
6770
responses.append(
68-
PoleZeroResponseData(
71+
RootLocusData(
6972
sys.poles(), sys.zeros(), dt=sys.dt, sysname=sys.name))
7073

7174
if isinstance(sysdata, (list, tuple)):
72-
return PoleZeroResponseList(responses)
75+
return RootLocusList(responses)
76+
else:
77+
return responses[0]
78+
79+
80+
# Root locus map
81+
def root_locus_map(sysdata, gains=None, xlim=None, ylim=None):
82+
# Convert the first argument to a list
83+
syslist = sysdata if isinstance(sysdata, (list, tuple)) else [sysdata]
84+
85+
responses = []
86+
for idx, sys in enumerate(syslist):
87+
from .rlocus import _systopoly1d, _default_gains
88+
from .rlocus import _RLFindRoots, _RLSortRoots
89+
90+
if not sys.issiso():
91+
raise ControlMIMONotImplemented(
92+
"sys must be single-input single-output (SISO)")
93+
94+
# Convert numerator and denominator to polynomials if they aren't
95+
nump, denp = _systopoly1d(sys[0, 0])
96+
97+
if xlim is None and sys.isdtime(strict=True):
98+
xlim = (-1.2, 1.2)
99+
if ylim is None and sys.isdtime(strict=True):
100+
xlim = (-1.3, 1.3)
101+
102+
if gains is None:
103+
kvect, root_array, xlim, ylim = _default_gains(
104+
nump, denp, xlim, ylim)
105+
else:
106+
kvect = np.atleast_1d(gains)
107+
root_array = _RLFindRoots(nump, denp, kvect)
108+
root_array = _RLSortRoots(root_array)
109+
110+
responses.append(RootLocusData(
111+
sys.poles(), sys.zeros(), kvect, root_array,
112+
dt=sys.dt, sysname=sys.name, xlim=xlim, ylim=ylim))
113+
114+
if isinstance(sysdata, (list, tuple)):
115+
return RootLocusList(responses)
73116
else:
74117
return responses[0]
75118

76119

77120
# TODO: Implement more elegant cross-style axes. See:
78121
# https://matplotlib.org/2.0.2/examples/axes_grid/demo_axisline_style.html
79122
# https://matplotlib.org/2.0.2/examples/axes_grid/demo_curvelinear_grid.html
80-
def pzmap_plot(
123+
def pole_zero_plot(
81124
data, plot=None, grid=None, title=None, marker_color=None,
82125
marker_size=None, marker_width=None, legend_loc='upper right',
83-
**kwargs):
126+
xlim=None, ylim=None, **kwargs):
84127
"""Plot a pole/zero map for a linear system.
85128
86129
Parameters
87130
----------
88-
sysdata: List of PoleZeroResponseData objects or LTI systems
131+
sysdata: List of RootLocusData objects or LTI systems
89132
List of pole/zero response data objects generated by pzmap_response
90133
or rootlocus_response() that are to be plotted. If a list of systems
91134
is given, the poles and zeros of those systems will be plotted.
@@ -124,6 +167,7 @@ def pzmap_plot(
124167
grid = config._get_param('pzmap', 'grid', grid, False)
125168
marker_size = config._get_param('pzmap', 'marker_size', marker_size, 6)
126169
marker_width = config._get_param('pzmap', 'marker_width', marker_width, 1.5)
170+
xlim_user, ylim_user = xlim, ylim
127171
freqplot_rcParams = config._get_param(
128172
'freqplot', 'rcParams', kwargs, _freqplot_defaults,
129173
pop=True, last=True)
@@ -136,17 +180,17 @@ def pzmap_plot(
136180
if all([isinstance(
137181
sys, (StateSpace, TransferFunction)) for sys in data]):
138182
# Get the response, popping off keywords used there
139-
pzmap_responses = pzmap_response(data)
140-
elif all([isinstance(d, PoleZeroResponseData) for d in data]):
183+
pzmap_responses = pole_zero_map(data)
184+
elif all([isinstance(d, RootLocusData) for d in data]):
141185
pzmap_responses = data
142186
else:
143187
raise TypeError("unknown system data type")
144188

145189
# Legacy return value processing
146190
if plot is not None:
147191
warnings.warn(
148-
"`pzmap_plot` return values of poles, zeros is deprecated; "
149-
"use pzmap_response()", DeprecationWarning)
192+
"`pole_zero_plot` return values of poles, zeros is deprecated; "
193+
"use pole_zero_map()", DeprecationWarning)
150194

151195
# Extract out the values that we will eventually return
152196
poles = [response.poles for response in pzmap_responses]
@@ -169,9 +213,9 @@ def pzmap_plot(
169213
with plt.rc_context(freqplot_rcParams):
170214
if grid:
171215
plt.clf()
172-
if all([response.isctime() for response in data]):
216+
if all([response.dt in [0, None] for response in data]):
173217
ax, fig = sgrid()
174-
elif all([response.isdtime() for response in data]):
218+
elif all([response.dt > 0 for response in data]):
175219
ax, fig = zgrid()
176220
else:
177221
ValueError(
@@ -192,10 +236,11 @@ def pzmap_plot(
192236
color_offset = color_cycle.index(last_color) + 1
193237

194238
# Create a list of lines for the output
195-
out = np.empty((len(pzmap_responses), 2), dtype=object)
239+
out = np.empty((len(pzmap_responses), 3), dtype=object)
196240
for i, j in itertools.product(range(out.shape[0]), range(out.shape[1])):
197241
out[i, j] = [] # unique list in each element
198242

243+
xlim, ylim = ax.get_xlim(), ax.get_ylim()
199244
for idx, response in enumerate(pzmap_responses):
200245
poles = response.poles
201246
zeros = response.zeros
@@ -208,44 +253,65 @@ def pzmap_plot(
208253

209254
# Plot the locations of the poles and zeros
210255
if len(poles) > 0:
256+
label = response.sysname if response.loci is None else None
211257
out[idx, 0] = ax.plot(
212258
real(poles), imag(poles), marker='x', linestyle='',
213259
markeredgecolor=color, markerfacecolor=color,
214260
markersize=marker_size, markeredgewidth=marker_width,
215-
label=response.sysname)
261+
label=label)
216262
if len(zeros) > 0:
217263
out[idx, 1] = ax.plot(
218264
real(zeros), imag(zeros), marker='o', linestyle='',
219265
markeredgecolor=color, markerfacecolor='none',
220266
markersize=marker_size, markeredgewidth=marker_width)
221267

268+
# Plot the loci, if present
269+
if response.loci is not None:
270+
for locus in response.loci.transpose():
271+
out[idx, 2] += ax.plot(
272+
real(locus), imag(locus), color=color,
273+
label=response.sysname)
274+
275+
# Compute the axis limits to use
276+
xlim = (min(xlim[0], response.xlim[0]), max(xlim[1], response.xlim[1]))
277+
ylim = (min(ylim[0], response.ylim[0]), max(ylim[1], response.ylim[1]))
278+
279+
# Set up the limits for the plot
280+
ax.set_xlim(xlim if xlim_user is None else xlim_user)
281+
ax.set_ylim(ylim if ylim_user is None else ylim_user)
282+
222283
# List of systems that are included in this plot
223284
lines, labels = _get_line_labels(ax)
224285

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-
239286
# Add legend if there is more than one system plotted
240287
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)})
288+
if response.loci is None:
289+
# Use "x o" for the system label, via matplotlib tuple handler
290+
from matplotlib.lines import Line2D
291+
from matplotlib.legend_handler import HandlerTuple
292+
293+
line_tuples = []
294+
for pole_line in lines:
295+
zero_line = Line2D(
296+
[0], [0], marker='o', linestyle='',
297+
markeredgecolor=pole_line.get_markerfacecolor(),
298+
markerfacecolor='none', markersize=marker_size,
299+
markeredgewidth=marker_width)
300+
handle = (pole_line, zero_line)
301+
line_tuples.append(handle)
302+
303+
with plt.rc_context(freqplot_rcParams):
304+
ax.legend(
305+
line_tuples, labels, loc=legend_loc,
306+
handler_map={tuple: HandlerTuple(ndivide=None)})
307+
else:
308+
# Regular legend, with lines
309+
with plt.rc_context(freqplot_rcParams):
310+
ax.legend(lines, labels, loc=legend_loc)
245311

246312
# Add the title
247313
if title is None:
248-
title = "Pole/zero map for " + ", ".join(labels)
314+
title = "Pole/zero plot for " + ", ".join(labels)
249315
with plt.rc_context(freqplot_rcParams):
250316
fig.suptitle(title)
251317

@@ -259,4 +325,4 @@ def pzmap_plot(
259325
return out
260326

261327

262-
pzmap = pzmap_plot
328+
pzmap = pole_zero_plot

control/tests/kwargs_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ def test_response_plot_kwargs(data_fcn, plot_fcn, mimo):
241241
'nyquist_response': test_response_plot_kwargs,
242242
'nyquist_plot': test_matplotlib_kwargs,
243243
'pzmap': test_unrecognized_kwargs,
244-
'pzmap_plot': test_unrecognized_kwargs,
244+
'pole_zero_plot': test_unrecognized_kwargs,
245245
'rlocus': test_unrecognized_kwargs,
246246
'root_locus': test_unrecognized_kwargs,
247247
'rss': test_unrecognized_kwargs,

0 commit comments

Comments
 (0)