Skip to content

Commit 97f2fd1

Browse files
committed
updated unit tests (coverage)
1 parent 3f7e275 commit 97f2fd1

3 files changed

Lines changed: 280 additions & 61 deletions

File tree

control/tests/timeplot_test.py

Lines changed: 199 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@
77
import matplotlib.pyplot as plt
88
import numpy as np
99

10-
from .conftest import slycotonly
10+
from control.tests.conftest import slycotonly
1111

1212
# Detailed test of (almost) all functionality
13-
# (uncomment rows for developmental testing, but otherwise takes too long)
13+
#
14+
# The commented out rows lead to very long testing times => these should be
15+
# used only for developmental testing and not day-to-day testing.
1416
@pytest.mark.parametrize(
1517
"sys", [
1618
# ct.rss(1, 1, 1, strictly_proper=True, name="rss"),
@@ -22,18 +24,52 @@
2224
# ct.drss(2, 2, 2, name="drss"),
2325
# ct.rss(2, 2, 3, strictly_proper=True, name="rss"),
2426
])
25-
@pytest.mark.parametrize("transpose", [True, False])
26-
@pytest.mark.parametrize("plot_inputs", [None, True, False, 'overlay'])
27-
@pytest.mark.parametrize("plot_outputs", [True, False])
28-
@pytest.mark.parametrize("combine_signals", [True, False])
29-
@pytest.mark.parametrize("combine_traces", [True, False])
30-
@pytest.mark.parametrize("second_system", [False, True])
31-
@pytest.mark.parametrize("fcn", [
32-
ct.step_response, ct.impulse_response, ct.initial_response,
33-
ct.forced_response, ct.input_output_response])
27+
# @pytest.mark.parametrize("transpose", [False, True])
28+
# @pytest.mark.parametrize("plot_inputs", [False, None, True, 'overlay'])
29+
# @pytest.mark.parametrize("plot_outputs", [True, False])
30+
# @pytest.mark.parametrize("combine_signals", [False, True])
31+
# @pytest.mark.parametrize("combine_traces", [False, True])
32+
# @pytest.mark.parametrize("second_system", [False, True])
33+
# @pytest.mark.parametrize("fcn", [
34+
# ct.step_response, ct.impulse_response, ct.initial_response,
35+
# ct.forced_response])
36+
@pytest.mark.parametrize( # combinatorial-style test (faster)
37+
"fcn, pltinp, pltout, cmbsig, cmbtrc, trpose, secsys",
38+
[(ct.step_response, False, True, False, False, False, False),
39+
(ct.step_response, None, True, False, False, False, False),
40+
(ct.step_response, True, True, False, False, False, False),
41+
(ct.step_response, 'overlay', True, False, False, False, False),
42+
(ct.step_response, 'overlay', True, True, False, False, False),
43+
(ct.step_response, 'overlay', True, False, True, False, False),
44+
(ct.step_response, 'overlay', True, False, False, True, False),
45+
(ct.step_response, 'overlay', True, False, False, False, True),
46+
(ct.step_response, False, False, False, False, False, False),
47+
(ct.step_response, None, False, False, False, False, False),
48+
(ct.step_response, 'overlay', False, False, False, False, False),
49+
(ct.step_response, True, True, False, True, False, False),
50+
(ct.step_response, True, True, False, False, False, True),
51+
(ct.step_response, True, True, False, True, False, True),
52+
(ct.step_response, True, True, True, False, True, True),
53+
(ct.step_response, True, True, False, True, True, True),
54+
(ct.impulse_response, False, True, True, False, False, False),
55+
(ct.initial_response, None, True, False, False, False, False),
56+
(ct.initial_response, False, True, False, False, False, False),
57+
(ct.initial_response, True, True, False, False, False, False),
58+
(ct.forced_response, True, True, False, False, False, False),
59+
(ct.forced_response, None, True, False, False, False, False),
60+
(ct.forced_response, False, True, False, False, False, False),
61+
(ct.forced_response, True, True, True, False, False, False),
62+
(ct.forced_response, True, True, True, True, False, False),
63+
(ct.forced_response, True, True, True, True, True, False),
64+
(ct.forced_response, True, True, True, True, True, True),
65+
(ct.forced_response, 'overlay', True, True, True, False, True),
66+
(ct.input_output_response,
67+
True, True, False, False, False, False),
68+
])
69+
3470
def test_response_plots(
35-
fcn, sys, plot_inputs, plot_outputs, combine_signals, combine_traces,
36-
transpose, second_system, clear=True):
71+
fcn, sys, pltinp, pltout, cmbsig, cmbtrc,
72+
trpose, secsys, clear=True):
3773
# Figure out the time range to use and check some special cases
3874
if not isinstance(sys, ct.lti.LTI):
3975
if fcn == ct.impulse_response:
@@ -42,15 +78,19 @@ def test_response_plots(
4278
# Nonlinear systems require explicit time limits
4379
T = 10
4480
timepts = np.linspace(0, T)
81+
82+
elif isinstance(sys, ct.TransferFunction) and fcn == ct.initial_response:
83+
pytest.skip("initial response not tested for tf")
84+
4585
else:
4686
# Linear systems figure things out on their own
4787
T = None
4888
timepts = np.linspace(0, 10) # for input_output_response
4989

5090
# Save up the keyword arguments
5191
kwargs = dict(
52-
plot_inputs=plot_inputs, plot_outputs=plot_outputs, transpose=transpose,
53-
combine_signals=combine_signals, combine_traces=combine_traces)
92+
plot_inputs=pltinp, plot_outputs=pltout, transpose=trpose,
93+
combine_signals=cmbsig, combine_traces=cmbtrc)
5494

5595
# Create the response
5696
if fcn is ct.input_output_response and \
@@ -78,27 +118,44 @@ def test_response_plots(
78118
response = fcn(sys, *args)
79119

80120
# Look for cases where there are no data to plot
81-
if not plot_outputs and (
82-
plot_inputs is False or response.ninputs == 0 or
83-
plot_inputs is None and response.plot_inputs is False):
121+
if not pltout and (
122+
pltinp is False or response.ninputs == 0 or
123+
pltinp is None and response.plot_inputs is False):
84124
with pytest.raises(ValueError, match=".* no data to plot"):
85125
out = response.plot(**kwargs)
86126
return None
87-
elif not plot_outputs and plot_inputs == 'overlay':
127+
elif not pltout and pltinp == 'overlay':
88128
with pytest.raises(ValueError, match="can't overlay inputs"):
89129
out = response.plot(**kwargs)
90130
return None
91-
elif plot_inputs in [True, 'overlay'] and response.ninputs == 0:
131+
elif pltinp in [True, 'overlay'] and response.ninputs == 0:
92132
with pytest.raises(ValueError, match=".* but no inputs"):
93133
out = response.plot(**kwargs)
94134
return None
95135

96136
out = response.plot(**kwargs)
97137

98-
# TODO: add some basic checks here
99-
100-
# Add additional data (and provide infon in the title)
101-
if second_system:
138+
# Make sure number of plots is correct
139+
if pltinp is None:
140+
if fcn in [ct.forced_response, ct.input_output_response]:
141+
pltinp = True
142+
else:
143+
pltinp = False
144+
ntraces = max(1, response.ntraces)
145+
nlines = (response.ninputs if pltinp else 0) * ntraces + \
146+
(response.noutputs if pltout else 0) * ntraces
147+
assert out.size == nlines
148+
149+
# Make sure all of the outputs are of the right type
150+
for ax_lines in np.nditer(out, flags=["refs_ok"]):
151+
for line in ax_lines.item():
152+
assert isinstance(line, mpl.lines.Line2D)
153+
154+
# Save the old axes to compare later
155+
old_axes = plt.gcf().get_axes()
156+
157+
# Add additional data (and provide info in the title)
158+
if secsys:
102159
newsys = ct.rss(
103160
sys.nstates, sys.noutputs, sys.ninputs, strictly_proper=True)
104161
if fcn not in [ct.initial_response, ct.forced_response,
@@ -110,21 +167,74 @@ def test_response_plots(
110167
# Compute and plot new response (time is one of the arguments)
111168
fcn(newsys, *args).plot(**kwargs)
112169

113-
# TODO: add some basic checks here
170+
# Make sure we have the same axes
171+
new_axes = plt.gcf().get_axes()
172+
assert new_axes == old_axes
173+
174+
# Make sure every axes has more than one line
175+
for ax in new_axes:
176+
assert len(ax.get_lines()) > 1
114177

115178
# Update the title so we can see what is going on
116179
fig = out[0, 0][0].axes.figure
117180
fig.suptitle(
118181
fig._suptitle._text +
119-
f" [{sys.noutputs}x{sys.ninputs}, cs={combine_signals}, "
120-
f"ct={combine_traces}, pi={plot_inputs}, tr={transpose}]",
182+
f" [{sys.noutputs}x{sys.ninputs}, cs={cmbsig}, "
183+
f"ct={cmbtrc}, pi={pltinp}, tr={trpose}]",
121184
fontsize='small')
122185

123186
# Get rid of the figure to free up memory
124187
if clear:
125188
plt.clf()
126189

127190

191+
def test_axes_setup():
192+
get_axes = ct.timeplot.get_axes
193+
194+
sys_2x3 = ct.rss(4, 2, 3)
195+
sys_2x3b = ct.rss(4, 2, 3)
196+
sys_3x2 = ct.rss(4, 3, 2)
197+
sys_3x1 = ct.rss(4, 3, 1)
198+
199+
# Two plots of the same size leaves axes unchanged
200+
out1 = ct.step_response(sys_2x3).plot()
201+
out2 = ct.step_response(sys_2x3b).plot()
202+
np.testing.assert_equal(get_axes(out1), get_axes(out2))
203+
plt.close()
204+
205+
# Two plots of same net size leaves axes unchanged (unfortunately)
206+
out1 = ct.step_response(sys_2x3).plot()
207+
out2 = ct.step_response(sys_3x2).plot()
208+
np.testing.assert_equal(
209+
get_axes(out1).reshape(-1), get_axes(out2).reshape(-1))
210+
plt.close()
211+
212+
# Plots of different shapes generate new plots
213+
out1 = ct.step_response(sys_2x3).plot()
214+
out2 = ct.step_response(sys_3x1).plot()
215+
ax1_list = get_axes(out1).reshape(-1).tolist()
216+
ax2_list = get_axes(out2).reshape(-1).tolist()
217+
for ax in ax1_list:
218+
assert ax not in ax2_list
219+
plt.close()
220+
221+
# Passing a list of axes preserves those axes
222+
out1 = ct.step_response(sys_2x3).plot()
223+
out2 = ct.step_response(sys_3x1).plot()
224+
out3 = ct.step_response(sys_2x3b).plot(ax=get_axes(out1))
225+
np.testing.assert_equal(get_axes(out1), get_axes(out3))
226+
plt.close()
227+
228+
# Sending an axes array of the wrong size raises exception
229+
with pytest.raises(ValueError, match="not the right shape"):
230+
out = ct.step_response(sys_2x3).plot()
231+
ct.step_response(sys_3x1).plot(ax=get_axes(out))
232+
sys_2x3 = ct.rss(4, 2, 3)
233+
sys_2x3b = ct.rss(4, 2, 3)
234+
sys_3x2 = ct.rss(4, 3, 2)
235+
sys_3x1 = ct.rss(4, 3, 1)
236+
237+
128238
@slycotonly
129239
def test_legend_map():
130240
sys_mimo = ct.tf2ss(
@@ -138,6 +248,68 @@ def test_legend_map():
138248
title='MIMO step response with custom legend placement')
139249

140250

251+
def test_combine_traces():
252+
sys_mimo = ct.rss(4, 2, 2)
253+
timepts = np.linspace(0, 10, 100)
254+
255+
# Combine two response with ntrace = 0
256+
U = np.vstack([np.sin(timepts), np.cos(2*timepts)])
257+
resp1 = ct.input_output_response(sys_mimo, timepts, U)
258+
259+
U = np.vstack([np.cos(2*timepts), np.sin(timepts)])
260+
resp2 = ct.input_output_response(sys_mimo, timepts, U)
261+
262+
combresp1 = ct.combine_traces([resp1, resp2])
263+
assert combresp1.ntraces == 2
264+
np.testing.assert_equal(combresp1.y[:, 0, :], resp1.y)
265+
np.testing.assert_equal(combresp1.y[:, 1, :], resp2.y)
266+
267+
# Combine two responses with ntrace != 0
268+
resp3 = ct.step_response(sys_mimo, timepts)
269+
resp4 = ct.step_response(sys_mimo, timepts)
270+
combresp2 = ct.combine_traces([resp3, resp4])
271+
assert combresp2.ntraces == resp3.ntraces + resp4.ntraces
272+
np.testing.assert_equal(combresp2.y[:, 0:2, :], resp3.y)
273+
np.testing.assert_equal(combresp2.y[:, 2:4, :], resp4.y)
274+
275+
# Mixture
276+
combresp3 = ct.combine_traces([resp1, resp2, resp3])
277+
assert combresp3.ntraces == resp3.ntraces + resp4.ntraces
278+
np.testing.assert_equal(combresp3.y[:, 0, :], resp1.y)
279+
np.testing.assert_equal(combresp3.y[:, 1, :], resp2.y)
280+
np.testing.assert_equal(combresp3.y[:, 2:4, :], resp3.y)
281+
assert combresp3.trace_types == [None, None] + resp3.trace_types
282+
assert combresp3.trace_labels == \
283+
[resp1.title, resp2.title] + resp3.trace_labels
284+
285+
# Rename the traces
286+
labels = ["T1", "T2", "T3", "T4"]
287+
combresp4 = ct.combine_traces([resp1, resp2, resp3], trace_labels=labels)
288+
assert combresp4.trace_labels == labels
289+
290+
# Automatically generated trace label names and types
291+
resp5 = ct.step_response(sys_mimo, timepts)
292+
resp5.title = "test"
293+
resp5.trace_labels = None
294+
resp5.trace_types = None
295+
combresp5 = ct.combine_traces([resp1, resp5])
296+
assert combresp5.trace_labels == [resp1.title] + \
297+
["test, trace 0", "test, trace 1"]
298+
assert combresp4.trace_types == [None, None, 'step', 'step']
299+
300+
with pytest.raises(ValueError, match="must have the same number"):
301+
resp = ct.step_response(ct.rss(4, 2, 3), timepts)
302+
combresp = ct.combine_traces([resp1, resp])
303+
304+
with pytest.raises(ValueError, match="trace labels does not match"):
305+
combresp = ct.combine_traces(
306+
[resp1, resp2], trace_labels=["T1", "T2", "T3"])
307+
308+
with pytest.raises(ValueError, match="must have the same time"):
309+
resp = ct.step_response(ct.rss(4, 2, 3), timepts/2)
310+
combresp6 = ct.combine_traces([resp1, resp])
311+
312+
141313
def test_errors():
142314
sys = ct.rss(2, 1, 1)
143315
stepresp = ct.step_response(sys)
@@ -150,7 +322,6 @@ def test_errors():
150322
with pytest.raises(ValueError, match="unrecognized value"):
151323
stepresp.plot(plot_inputs='unknown')
152324

153-
154325
if __name__ == "__main__":
155326
#
156327
# Interactive mode: generate plots for manual viewing

0 commit comments

Comments
 (0)