Skip to content

Commit 52f8127

Browse files
committed
updated unit test for coverage + some fixes
1 parent 416dff8 commit 52f8127

4 files changed

Lines changed: 79 additions & 18 deletions

File tree

control/pzmap.py

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -205,11 +205,11 @@ def pole_zero_plot(
205205
206206
Returns
207207
-------
208-
lines : List of Line2D
208+
lines : array of list of Line2D
209209
Array of Line2D objects for each set of markers in the plot. The
210210
shape of the array is given by (nsys, 2) where nsys is the number
211-
of systems or Nyquist responses passed to the function. The second
212-
index specifies the pzmap object type:
211+
of systems or responses passed to the function. The second index
212+
specifies the pzmap object type:
213213
214214
* lines[idx, 0]: poles
215215
* lines[idx, 1]: zeros
@@ -276,8 +276,11 @@ def pole_zero_plot(
276276
else:
277277
raise TypeError("unknown system data type")
278278

279+
# Decide whether we are plotting any root loci
280+
rlocus_plot = any([resp.loci is not None for resp in pzmap_responses])
281+
279282
# Turn on interactive mode by default, if allowed
280-
if interactive is None and len(pzmap_responses) == 1 \
283+
if interactive is None and rlocus_plot and len(pzmap_responses) == 1 \
281284
and pzmap_responses[0].sys is not None:
282285
interactive = True
283286

@@ -318,19 +321,26 @@ def pole_zero_plot(
318321
elif all([isdtime(dt=response.dt) for response in data]):
319322
ax, fig = zgrid(scaling=scaling)
320323
else:
321-
ValueError(
322-
"incompatible time responses; don't know how to grid")
324+
raise ValueError(
325+
"incompatible time bases; don't know how to grid")
326+
# Store the limits for later use
327+
xlim, ylim = ax.get_xlim(), ax.get_ylim()
323328
elif len(axs) == 0:
324329
if grid == 'empty':
325330
# Leave off grid entirely
326331
ax = plt.axes()
332+
xlim = ylim = [0, 0] # use data to set limits
327333
else:
328334
# draw stability boundary; use first response timebase
329335
ax, fig = nogrid(data[0].dt, scaling=scaling)
336+
xlim, ylim = ax.get_xlim(), ax.get_ylim()
330337
else:
331338
# Use the existing axes and any grid that is there
332339
ax = axs[0]
333340

341+
# Store the limits for later use
342+
xlim, ylim = ax.get_xlim(), ax.get_ylim()
343+
334344
# Issue a warning if the user tried to set the grid type
335345
if grid:
336346
warnings.warn("axis already exists; grid keyword ignored")
@@ -345,13 +355,12 @@ def pole_zero_plot(
345355
color_offset = color_cycle.index(last_color) + 1
346356

347357
# Create a list of lines for the output
348-
out = np.empty((len(pzmap_responses), 3), dtype=object)
358+
out = np.empty(
359+
(len(pzmap_responses), 3 if rlocus_plot else 2), dtype=object)
349360
for i, j in itertools.product(range(out.shape[0]), range(out.shape[1])):
350361
out[i, j] = [] # unique list in each element
351362

352363
# Plot the responses (and keep track of axes limits)
353-
xlim, ylim = ax.get_xlim(), ax.get_ylim()
354-
loci_count = 0
355364
for idx, response in enumerate(pzmap_responses):
356365
poles = response.poles
357366
zeros = response.zeros
@@ -397,7 +406,7 @@ def pole_zero_plot(
397406
# TODO: add arrows to root loci (reuse Nyquist arrow code?)
398407

399408
# Set the axis limits to something reasonable
400-
if any([response.loci is not None for response in pzmap_responses]):
409+
if rlocus_plot:
401410
# Set up the limits for the plot using information from loci
402411
ax.set_xlim(xlim if xlim_user is None else xlim_user)
403412
ax.set_ylim(ylim if ylim_user is None else ylim_user)

control/rlocus.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -133,14 +133,15 @@ def root_locus_plot(
133133
134134
Returns
135135
-------
136-
lines : List of Line2D
136+
lines : array of list of Line2D
137137
Array of Line2D objects for each set of markers in the plot. The
138-
shape of the array is given by (nsys, 2) where nsys is the number
139-
of systems or Nyquist responses passed to the function. The second
140-
index specifies the pzmap object type:
138+
shape of the array is given by (nsys, 3) where nsys is the number
139+
of systems or responses passed to the function. The second index
140+
specifies the object type:
141141
142142
* lines[idx, 0]: poles
143143
* lines[idx, 1]: zeros
144+
* lines[idx, 2]: loci
144145
145146
roots, gains : ndarray
146147
(legacy) If the `plot` keyword is given, returns the

control/tests/pzmap_test.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from matplotlib import pyplot as plt
1313
from mpl_toolkits.axisartist import Axes as mpltAxes
1414

15+
import control as ct
1516
from control import TransferFunction, config, pzmap
1617

1718

@@ -82,7 +83,47 @@ def test_pzmap(kwargs, setdefaults, dt, editsdefaults, mplcleanup):
8283
assert not plt.get_fignums()
8384

8485

86+
def test_polezerodata():
87+
sys = ct.rss(4, 1, 1)
88+
pzdata = ct.pole_zero_map(sys)
89+
np.testing.assert_equal(pzdata.poles, sys.poles())
90+
np.testing.assert_equal(pzdata.zeros, sys.zeros())
91+
92+
# Extract data from PoleZeroData
93+
poles, zeros = pzdata
94+
np.testing.assert_equal(poles, sys.poles())
95+
np.testing.assert_equal(zeros, sys.zeros())
96+
97+
# Legacy return format
98+
with pytest.warns(DeprecationWarning, match=".* values .* deprecated"):
99+
poles, zeros = ct.pole_zero_plot(pzdata, plot=False)
100+
np.testing.assert_equal(poles, sys.poles())
101+
np.testing.assert_equal(zeros, sys.zeros())
102+
103+
with pytest.warns(DeprecationWarning, match=".* values .* deprecated"):
104+
poles, zeros = ct.pole_zero_plot(pzdata, plot=True)
105+
np.testing.assert_equal(poles, sys.poles())
106+
np.testing.assert_equal(zeros, sys.zeros())
107+
108+
85109
def test_pzmap_raises():
86110
with pytest.raises(TypeError):
87111
# not an LTI system
88-
pzmap(([1], [1,2]))
112+
pzmap(([1], [1, 2]))
113+
114+
sys1 = ct.rss(2, 1, 1)
115+
sys2 = sys1.sample(0.1)
116+
with pytest.raises(ValueError, match="incompatible time bases"):
117+
pzdata = ct.pole_zero_plot([sys1, sys2], grid=True)
118+
119+
with pytest.warns(UserWarning, match="axis already exists"):
120+
fig, ax = plt.figure(), plt.axes()
121+
ct.pole_zero_plot(sys1, ax=ax, grid='empty')
122+
123+
124+
def test_pzmap_limits():
125+
sys = ct.tf([1, 2], [1, 2, 3])
126+
out = ct.pole_zero_plot(sys, xlim=[-1, 1], ylim=[-1, 1])
127+
ax = ct.get_plot_axes(out)[0, 0]
128+
assert ax.get_xlim() == (-1, 1)
129+
assert ax.get_ylim() == (-1, 1)

control/tests/rlocus_test.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,22 @@ def test_without_gains(self, sys):
6767
self.check_cl_poles(sys, roots, kvect)
6868

6969
@pytest.mark.parametrize("grid", [None, True, False, 'empty'])
70-
def test_root_locus_plot_grid(self, sys, grid):
70+
@pytest.mark.parametrize("method", ['plot', 'map', 'response', 'pzmap'])
71+
def test_root_locus_plot_grid(self, sys, grid, method):
7172
import mpl_toolkits.axisartist as AA
7273

7374
# Generate the root locus plot
7475
plt.clf()
75-
ct.root_locus_plot(sys, grid=grid)
76+
if method == 'plot':
77+
ct.root_locus_plot(sys, grid=grid)
78+
elif method == 'map':
79+
ct.root_locus_map(sys).plot(grid=grid)
80+
elif method == 'response':
81+
response = ct.root_locus_map(sys)
82+
ct.root_locus_plot(response, grid=grid)
83+
elif method == 'pzmap':
84+
response = ct.root_locus_map(sys)
85+
ct.pole_zero_plot(response, grid=grid)
7686

7787
# Count the number of dotted/dashed lines in the plot
7888
ax = plt.gca()
@@ -85,7 +95,7 @@ def test_root_locus_plot_grid(self, sys, grid):
8595
if grid == 'empty':
8696
assert n_gridlines == 0
8797
assert not isinstance(ax, AA.Axes)
88-
elif grid is False:
98+
elif grid is False or method == 'pzmap' and grid is None:
8999
assert n_gridlines == 2 if sys.isctime() else 3
90100
assert not isinstance(ax, AA.Axes)
91101
elif sys.isdtime(strict=True):

0 commit comments

Comments
 (0)