Skip to content

Commit 8776875

Browse files
committed
updated grid handling (add 'empty')
1 parent 08e55b1 commit 8776875

File tree

5 files changed

+60
-75
lines changed

5 files changed

+60
-75
lines changed

control/freqplot.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ def bode_plot(
162162
values with no plot.
163163
rcParams : dict
164164
Override the default parameters used for generating plots.
165-
Default is set up config.default['freqplot.rcParams'].
165+
Default is set by config.default['freqplot.rcParams'].
166166
wrap_phase : bool or float
167167
If wrap_phase is `False` (default), then the phase will be unwrapped
168168
so that it is continuously increasing or decreasing. If wrap_phase is

control/matlab/wrappers.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ def _parse_freqplot_args(*args):
195195
return syslist, omega, plotstyle, other
196196

197197

198+
# TODO: rewrite to call root_locus_map, without using legacy plot keyword
198199
def rlocus(*args, **kwargs):
199200
"""rlocus(sys[, klist, xlim, ylim, ...])
200201
@@ -248,6 +249,7 @@ def rlocus(*args, **kwargs):
248249
return retval
249250

250251

252+
# TODO: rewrite to call pole_zero_map, without using legacy plot keyword
251253
def pzmap(*args, **kwargs):
252254
"""pzmap(sys[, grid, plot])
253255

control/pzmap.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -125,13 +125,14 @@ def pole_zero_plot(
125125
Parameters
126126
----------
127127
sysdata : List of PoleZeroData objects or LTI systems
128-
List of pole/zero response data objects generated by pzmap_response
128+
List of pole/zero response data objects generated by pzmap_response()
129129
or rootlocus_response() that are to be plotted. If a list of systems
130130
is given, the poles and zeros of those systems will be plotted.
131-
grid : boolean (default = None)
132-
If True plot omega-damping grid, otherwise show imaginary axis for
133-
continuous time systems, unit circle for discrete time systems. If
134-
`False`, do not draw any additonal lines.
131+
grid : bool or str, optional
132+
If `True` plot omega-damping grid, if `False` show imaginary axis
133+
for continuous time systems, unit circle for discrete time systems.
134+
If `empty`, do not draw any additonal lines. Default value is set
135+
by config.default['pzmap.grid'] or config.default['rlocus.grid'].
135136
plot : bool, optional
136137
(legacy) If ``True`` a graph is generated with Matplotlib,
137138
otherwise the poles and zeros are only computed and returned.
@@ -188,7 +189,7 @@ def pole_zero_plot(
188189
189190
"""
190191
# Get parameter values
191-
grid = config._get_param('pzmap', 'grid', grid, None)
192+
grid = config._get_param('pzmap', 'grid', grid, _pzmap_defaults)
192193
marker_size = config._get_param('pzmap', 'marker_size', marker_size, 6)
193194
marker_width = config._get_param('pzmap', 'marker_width', marker_width, 1.5)
194195
xlim_user, ylim_user = xlim, ylim
@@ -246,7 +247,7 @@ def pole_zero_plot(
246247
fig, axs = plt.figure(), []
247248

248249
with plt.rc_context(freqplot_rcParams):
249-
if grid:
250+
if grid and grid != 'empty':
250251
plt.clf()
251252
if all([isctime(dt=response.dt) for response in data]):
252253
ax, fig = sgrid(scaling=scaling)
@@ -256,16 +257,20 @@ def pole_zero_plot(
256257
ValueError(
257258
"incompatible time responses; don't know how to grid")
258259
elif len(axs) == 0:
259-
if grid is False:
260+
if grid == 'empty':
260261
# Leave off grid entirely
261262
ax = plt.axes()
262263
else:
263-
# use first response timebase
264+
# draw stability boundary; use first response timebase
264265
ax, fig = nogrid(data[0].dt, scaling=scaling)
265266
else:
266267
# Use the existing axes and any grid that is there
267268
ax = axs[0]
268269

270+
# Issue a warning if the user tried to set the grid type
271+
if grid:
272+
warnings.warn("axis already exists; grid keyword ignored")
273+
269274
# Handle color cycle manually as all root locus segments
270275
# of the same system are expected to be of the same color
271276
color_cycle = plt.rcParams['axes.prop_cycle'].by_key()['color']

control/rlocus.py

Lines changed: 13 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
from .iosys import isdtime
2525
from .xferfcn import _convert_to_transfer_function
2626
from .exception import ControlMIMONotImplemented
27-
from .grid import sgrid, zgrid
2827
from . import config
2928
import warnings
3029

@@ -96,19 +95,25 @@ def root_locus_plot(
9695
(see :doc:`matplotlib:api/axes_api`).
9796
plotstr : :func:`matplotlib.pyplot.plot` format string, optional
9897
plotting style specification
98+
TODO: check
9999
plot : boolean, optional
100100
If True (default), plot root locus diagram.
101+
TODO: legacy
101102
print_gain : bool
102103
If True (default), report mouse clicks when close to the root locus
103104
branches, calculate gain, damping and print.
104-
grid : bool
105-
If True plot omega-damping grid. Default is False.
105+
TODO: update
106+
grid : bool or str, optional
107+
If `True` plot omega-damping grid, if `False` show imaginary axis
108+
for continuous time systems, unit circle for discrete time systems.
109+
If `empty`, do not draw any additonal lines. Default value is set
110+
by config.default['rlocus.grid'].
106111
ax : :class:`matplotlib.axes.Axes`
107112
Axes on which to create root locus plot
108113
initial_gain : float, optional
109114
Specify the initial gain to use when marking current gain. [TODO: update]
110115
111-
Returns
116+
Returns (TODO: update)
112117
-------
113118
roots : ndarray
114119
Closed-loop root locations, arranged in which each row corresponds
@@ -156,8 +161,7 @@ def root_locus_plot(
156161
return out
157162

158163

159-
# TODO: get rid of zoom functionality?
160-
def _default_gains(num, den, xlim, ylim, zoom_xlim=None, zoom_ylim=None):
164+
def _default_gains(num, den, xlim, ylim):
161165
"""Unsupervised gains calculation for root locus plot.
162166
163167
References
@@ -237,8 +241,7 @@ def _default_gains(num, den, xlim, ylim, zoom_xlim=None, zoom_ylim=None):
237241
tolerance = x_tolerance
238242
else:
239243
tolerance = np.min([x_tolerance, y_tolerance])
240-
indexes_too_far = _indexes_filt(
241-
root_array, tolerance, zoom_xlim, zoom_ylim)
244+
indexes_too_far = _indexes_filt(root_array, tolerance)
242245

243246
# Add more points into the root locus for points that are too far apart
244247
while len(indexes_too_far) > 0 and kvect.size < 5000:
@@ -250,8 +253,7 @@ def _default_gains(num, den, xlim, ylim, zoom_xlim=None, zoom_ylim=None):
250253
root_array = np.insert(root_array, index + 1, new_points, axis=0)
251254

252255
root_array = _RLSortRoots(root_array)
253-
indexes_too_far = _indexes_filt(
254-
root_array, tolerance, zoom_xlim, zoom_ylim)
256+
indexes_too_far = _indexes_filt(root_array, tolerance)
255257

256258
new_gains = kvect[-1] * np.hstack((np.logspace(0, 3, 4)))
257259
new_points = _RLFindRoots(num, den, new_gains[1:4])
@@ -261,7 +263,7 @@ def _default_gains(num, den, xlim, ylim, zoom_xlim=None, zoom_ylim=None):
261263
return kvect, root_array, xlim, ylim
262264

263265

264-
def _indexes_filt(root_array, tolerance, zoom_xlim=None, zoom_ylim=None):
266+
def _indexes_filt(root_array, tolerance):
265267
"""Calculate the distance between points and return the indices.
266268
267269
Filter the indexes so only the resolution of points within the xlim and
@@ -270,48 +272,6 @@ def _indexes_filt(root_array, tolerance, zoom_xlim=None, zoom_ylim=None):
270272
"""
271273
distance_points = np.abs(np.diff(root_array, axis=0))
272274
indexes_too_far = list(np.unique(np.where(distance_points > tolerance)[0]))
273-
274-
if zoom_xlim is not None and zoom_ylim is not None:
275-
x_tolerance_zoom = 0.05 * (zoom_xlim[1] - zoom_xlim[0])
276-
y_tolerance_zoom = 0.05 * (zoom_ylim[1] - zoom_ylim[0])
277-
tolerance_zoom = np.min([x_tolerance_zoom, y_tolerance_zoom])
278-
indexes_too_far_zoom = list(
279-
np.unique(np.where(distance_points > tolerance_zoom)[0]))
280-
indexes_too_far_filtered = []
281-
282-
for index in indexes_too_far_zoom:
283-
for point in root_array[index]:
284-
if (zoom_xlim[0] <= point.real <= zoom_xlim[1]) and \
285-
(zoom_ylim[0] <= point.imag <= zoom_ylim[1]):
286-
indexes_too_far_filtered.append(index)
287-
break
288-
289-
# Check if zoom box is not overshot & insert points where neccessary
290-
if len(indexes_too_far_filtered) == 0 and len(root_array) < 500:
291-
limits = [zoom_xlim[0], zoom_xlim[1], zoom_ylim[0], zoom_ylim[1]]
292-
for index, limit in enumerate(limits):
293-
if index <= 1:
294-
asign = np.sign(real(root_array)-limit)
295-
else:
296-
asign = np.sign(imag(root_array) - limit)
297-
signchange = ((np.roll(asign, 1, axis=0)
298-
- asign) != 0).astype(int)
299-
signchange[0] = np.zeros((len(root_array[0])))
300-
if len(np.where(signchange == 1)[0]) > 0:
301-
indexes_too_far_filtered.append(
302-
np.where(signchange == 1)[0][0]-1)
303-
304-
if len(indexes_too_far_filtered) > 0:
305-
if indexes_too_far_filtered[0] != 0:
306-
indexes_too_far_filtered.insert(
307-
0, indexes_too_far_filtered[0]-1)
308-
if not indexes_too_far_filtered[-1] + 1 >= len(root_array) - 2:
309-
indexes_too_far_filtered.append(
310-
indexes_too_far_filtered[-1] + 1)
311-
312-
indexes_too_far.extend(indexes_too_far_filtered)
313-
314-
indexes_too_far = list(np.unique(indexes_too_far))
315275
indexes_too_far.sort()
316276
return indexes_too_far
317277

control/tests/rlocus_test.py

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -66,20 +66,38 @@ def test_without_gains(self, sys):
6666
roots, kvect = root_locus(sys, plot=False)
6767
self.check_cl_poles(sys, roots, kvect)
6868

69-
@pytest.mark.skip("TODO: update test for rlocus gridlines")
70-
@pytest.mark.slow
71-
@pytest.mark.parametrize('grid', [None, True, False])
69+
@pytest.mark.parametrize("grid", [None, True, False, 'empty'])
7270
def test_root_locus_plot_grid(self, sys, grid):
73-
rlist, klist = root_locus(sys, plot=True, grid=grid)
71+
import mpl_toolkits.axisartist as AA
72+
73+
# Generate the root locus plot
74+
plt.clf()
75+
ct.root_locus_plot(sys, grid=grid)
76+
77+
# Count the number of dotted/dashed lines in the plot
7478
ax = plt.gca()
75-
n_gridlines = sum([int(line.get_linestyle() in [':', 'dotted',
76-
'--', 'dashed'])
77-
for line in ax.lines])
78-
if grid is False:
79-
assert n_gridlines == 2
80-
else:
79+
n_gridlines = sum([int(
80+
line.get_linestyle() in [':', 'dotted', '--', 'dashed'] or
81+
line.get_linewidth() < 1
82+
) for line in ax.lines])
83+
84+
# Make sure they line up with what we expect
85+
if grid == 'empty':
86+
assert n_gridlines == 0
87+
assert not isinstance(ax, AA.Axes)
88+
elif grid is False:
89+
assert n_gridlines == 2 if sys.isctime() else 3
90+
assert not isinstance(ax, AA.Axes)
91+
elif sys.isdtime(strict=True):
8192
assert n_gridlines > 2
82-
# TODO check validity of grid
93+
assert not isinstance(ax, AA.Axes)
94+
else:
95+
# Continuous time, with grid => check that AxisArtist was used
96+
assert isinstance(ax, AA.Axes)
97+
for spine in ['wnxneg', 'wnxpos', 'wnyneg', 'wnypos']:
98+
assert spine in ax.axis
99+
100+
# TODO: check validity of grid
83101

84102
@pytest.mark.filterwarnings("ignore:.*return values.*:DeprecationWarning")
85103
def test_root_locus_neg_false_gain_nonproper(self):
@@ -89,7 +107,7 @@ def test_root_locus_neg_false_gain_nonproper(self):
89107

90108
# TODO: cover and validate negative false_gain branch in _default_gains()
91109

92-
@pytest.mark.skip("TODO: update test to check click dispatcher")
110+
@pytest.mark.skip("Zooming functionality no longer implemented")
93111
@pytest.mark.skipif(plt.get_current_fig_manager().toolbar is None,
94112
reason="Requires the zoom toolbar")
95113
def test_root_locus_zoom(self):

0 commit comments

Comments
 (0)