-
Notifications
You must be signed in to change notification settings - Fork 452
Expand file tree
/
Copy pathtimeplot_test.py
More file actions
712 lines (606 loc) · 27.8 KB
/
timeplot_test.py
File metadata and controls
712 lines (606 loc) · 27.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
# timeplot_test.py - test out time response plots
# RMM, 23 Jun 2023
import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np
import pytest
import control as ct
# Detailed test of (almost) all functionality
#
# The commented out rows lead to very long testing times => these should be
# used only for developmental testing and not day-to-day testing.
@pytest.mark.parametrize(
"sys", [
# ct.rss(1, 1, 1, strictly_proper=True, name="rss"),
ct.nlsys(
lambda t, x, u, params: -x + u, None,
inputs=1, outputs=1, states=1, name="nlsys"),
# ct.rss(2, 1, 2, strictly_proper=True, name="rss"),
ct.rss(2, 2, 1, strictly_proper=True, name="rss"),
# ct.drss(2, 2, 2, name="drss"),
# ct.rss(2, 2, 3, strictly_proper=True, name="rss"),
])
# @pytest.mark.parametrize("transpose", [False, True])
# @pytest.mark.parametrize("plot_inputs", [False, None, True, 'overlay'])
# @pytest.mark.parametrize("plot_outputs", [True, False])
# @pytest.mark.parametrize("overlay_signals", [False, True])
# @pytest.mark.parametrize("overlay_traces", [False, True])
# @pytest.mark.parametrize("second_system", [False, True])
# @pytest.mark.parametrize("fcn", [
# ct.step_response, ct.impulse_response, ct.initial_response,
# ct.forced_response])
@pytest.mark.parametrize( # combinatorial-style test (faster)
"fcn, pltinp, pltout, cmbsig, cmbtrc, trpose, secsys",
[(ct.step_response, False, True, False, False, False, False),
(ct.step_response, None, True, False, False, False, False),
(ct.step_response, True, True, False, False, False, False),
(ct.step_response, 'overlay', True, False, False, False, False),
(ct.step_response, 'overlay', True, True, False, False, False),
(ct.step_response, 'overlay', True, False, True, False, False),
(ct.step_response, 'overlay', True, False, False, True, False),
(ct.step_response, 'overlay', True, False, False, False, True),
(ct.step_response, False, False, False, False, False, False),
(ct.step_response, None, False, False, False, False, False),
(ct.step_response, 'overlay', False, False, False, False, False),
(ct.step_response, True, True, False, True, False, False),
(ct.step_response, True, True, False, False, False, True),
(ct.step_response, True, True, False, True, False, True),
(ct.step_response, True, True, True, False, True, True),
(ct.step_response, True, True, False, True, True, True),
(ct.impulse_response, False, True, True, False, False, False),
(ct.initial_response, None, True, False, False, False, False),
(ct.initial_response, False, True, False, False, False, False),
(ct.initial_response, True, True, False, False, False, False),
(ct.forced_response, True, True, False, False, False, False),
(ct.forced_response, None, True, False, False, False, False),
(ct.forced_response, False, True, False, False, False, False),
(ct.forced_response, True, True, True, False, False, False),
(ct.forced_response, True, True, True, True, False, False),
(ct.forced_response, True, True, True, True, True, False),
(ct.forced_response, True, True, True, True, True, True),
(ct.forced_response, 'overlay', True, True, True, False, True),
(ct.input_output_response,
True, True, False, False, False, False),
])
@pytest.mark.usefixtures('mplcleanup')
def test_response_plots(
fcn, sys, pltinp, pltout, cmbsig, cmbtrc,
trpose, secsys, clear=True):
# Figure out the time range to use and check some special cases
if not isinstance(sys, ct.lti.LTI):
if fcn == ct.impulse_response:
pytest.skip("impulse response not implemented for nlsys")
# Nonlinear systems require explicit time limits
T = 10
timepts = np.linspace(0, T)
elif isinstance(sys, ct.TransferFunction) and fcn == ct.initial_response:
pytest.skip("initial response not tested for tf")
else:
# Linear systems figure things out on their own
T = None
timepts = np.linspace(0, 10) # for input_output_response
# Save up the keyword arguments
kwargs = dict(
plot_inputs=pltinp, plot_outputs=pltout, transpose=trpose,
overlay_signals=cmbsig, overlay_traces=cmbtrc)
# Create the response
if fcn is ct.input_output_response and \
not isinstance(sys, ct.NonlinearIOSystem):
# Skip transfer functions and other non-state space systems
return None
if fcn in [ct.input_output_response, ct.forced_response]:
U = np.zeros((sys.ninputs, timepts.size))
for i in range(sys.ninputs):
U[i] = np.cos(timepts * i + i)
args = [timepts, U]
elif fcn == ct.initial_response:
args = [T, np.ones(sys.nstates)] # T, X0
elif not isinstance(sys, ct.lti.LTI):
args = [T] # nonlinear systems require final time
else: # step, initial, impulse responses
args = []
# Create a new figure (in case previous one is of the same size) and plot
if not clear:
plt.figure()
response = fcn(sys, *args)
# Look for cases where there are no data to plot
if not pltout and (
pltinp is False or response.ninputs == 0 or
pltinp is None and response.plot_inputs is False):
with pytest.raises(ValueError, match=".* no data to plot"):
cplt = response.plot(**kwargs)
return None
elif not pltout and pltinp == 'overlay':
with pytest.raises(ValueError, match="can't overlay inputs"):
cplt = response.plot(**kwargs)
return None
elif pltinp in [True, 'overlay'] and response.ninputs == 0:
with pytest.raises(ValueError, match=".* but no inputs"):
cplt = response.plot(**kwargs)
return None
cplt = response.plot(**kwargs)
# Make sure all of the outputs are of the right type
nlines_plotted = 0
for ax_lines in np.nditer(cplt.lines, flags=["refs_ok"]):
for line in ax_lines.item():
assert isinstance(line, mpl.lines.Line2D)
nlines_plotted += 1
# Make sure number of plots is correct
if pltinp is None:
if fcn in [ct.forced_response, ct.input_output_response]:
pltinp = True
else:
pltinp = False
ntraces = max(1, response.ntraces)
nlines_expected = (response.ninputs if pltinp else 0) * ntraces + \
(response.noutputs if pltout else 0) * ntraces
assert nlines_plotted == nlines_expected
# Save the old axes to compare later
old_axes = plt.gcf().get_axes()
# Add additional data (and provide info in the title)
if secsys:
newsys = ct.rss(
sys.nstates, sys.noutputs, sys.ninputs, strictly_proper=True)
if fcn not in [ct.initial_response, ct.forced_response,
ct.input_output_response] and \
isinstance(sys, ct.lti.LTI):
# Reuse the previously computed time to make plots look nicer
fcn(newsys, *args, T=response.time[-1]).plot(**kwargs)
else:
# Compute and plot new response (time is one of the arguments)
fcn(newsys, *args).plot(**kwargs)
# Make sure we have the same axes
new_axes = plt.gcf().get_axes()
assert new_axes == old_axes
# Make sure every axes has more than one line
for ax in new_axes:
assert len(ax.get_lines()) > 1
# Update the title so we can see what is going on
fig = cplt.figure
fig.suptitle(
fig._suptitle._text +
f" [{sys.noutputs}x{sys.ninputs}, cs={cmbsig}, "
f"ct={cmbtrc}, pi={pltinp}, tr={trpose}]",
fontsize='small')
# Get rid of the figure to free up memory
if clear:
plt.clf()
@pytest.mark.usefixtures('mplcleanup')
def test_axes_setup():
sys_2x3 = ct.rss(4, 2, 3)
sys_2x3b = ct.rss(4, 2, 3)
sys_3x2 = ct.rss(4, 3, 2)
sys_3x1 = ct.rss(4, 3, 1)
# Two plots of the same size leaves axes unchanged
cplt1 = ct.step_response(sys_2x3).plot()
cplt2 = ct.step_response(sys_2x3b).plot()
np.testing.assert_equal(cplt1.axes, cplt2.axes)
plt.close()
# Two plots of same net size leaves axes unchanged (unfortunately)
cplt1 = ct.step_response(sys_2x3).plot()
cplt2 = ct.step_response(sys_3x2).plot()
np.testing.assert_equal(
cplt1.axes.reshape(-1), cplt2.axes.reshape(-1))
plt.close()
# Plots of different shapes generate new plots
cplt1 = ct.step_response(sys_2x3).plot()
cplt2 = ct.step_response(sys_3x1).plot()
ax1_list = cplt1.axes.reshape(-1).tolist()
ax2_list = cplt2.axes.reshape(-1).tolist()
for ax in ax1_list:
assert ax not in ax2_list
plt.close()
# Passing a list of axes preserves those axes
cplt1 = ct.step_response(sys_2x3).plot()
cplt2 = ct.step_response(sys_3x1).plot()
cplt3 = ct.step_response(sys_2x3b).plot(ax=cplt1.axes)
np.testing.assert_equal(cplt1.axes, cplt3.axes)
plt.close()
# Sending an axes array of the wrong size raises exception
with pytest.raises(ValueError, match="not the right shape"):
cplt = ct.step_response(sys_2x3).plot()
ct.step_response(sys_3x1).plot(ax=cplt.axes)
sys_2x3 = ct.rss(4, 2, 3)
sys_2x3b = ct.rss(4, 2, 3)
sys_3x2 = ct.rss(4, 3, 2)
sys_3x1 = ct.rss(4, 3, 1)
@pytest.mark.slycot
@pytest.mark.usefixtures('mplcleanup')
def test_legend_map():
sys_mimo = ct.tf2ss(
[[[1], [0.1]], [[0.2], [1]]],
[[[1, 0.6, 1], [1, 1, 1]], [[1, 0.4, 1], [1, 2, 1]]], name="MIMO")
response = ct.step_response(sys_mimo)
response.plot(
legend_map=np.array([['center', 'upper right'],
[None, 'center right']]),
plot_inputs=True, overlay_signals=True, transpose=True,
title='MIMO step response with custom legend placement')
@pytest.mark.usefixtures('mplcleanup')
def test_combine_time_responses():
sys_mimo = ct.rss(4, 2, 2)
timepts = np.linspace(0, 10, 100)
# Combine two responses with ntrace = 0
U = np.vstack([np.sin(timepts), np.cos(2*timepts)])
resp1 = ct.input_output_response(sys_mimo, timepts, U)
U = np.vstack([np.cos(2*timepts), np.sin(timepts)])
resp2 = ct.input_output_response(sys_mimo, timepts, U)
combresp1 = ct.combine_time_responses([resp1, resp2])
assert combresp1.ntraces == 2
np.testing.assert_equal(combresp1.y[:, 0, :], resp1.y)
np.testing.assert_equal(combresp1.y[:, 1, :], resp2.y)
# Combine two responses with ntrace != 0
resp3 = ct.step_response(sys_mimo, timepts)
resp4 = ct.step_response(sys_mimo, timepts)
combresp2 = ct.combine_time_responses([resp3, resp4])
assert combresp2.ntraces == resp3.ntraces + resp4.ntraces
np.testing.assert_equal(combresp2.y[:, 0:2, :], resp3.y)
np.testing.assert_equal(combresp2.y[:, 2:4, :], resp4.y)
# Mixture
combresp3 = ct.combine_time_responses([resp1, resp2, resp3])
assert combresp3.ntraces == resp3.ntraces + resp4.ntraces
np.testing.assert_equal(combresp3.y[:, 0, :], resp1.y)
np.testing.assert_equal(combresp3.y[:, 1, :], resp2.y)
np.testing.assert_equal(combresp3.y[:, 2:4, :], resp3.y)
assert combresp3.trace_types == [None, None] + resp3.trace_types
assert combresp3.trace_labels == \
[resp1.title, resp2.title] + resp3.trace_labels
# Rename the traces
labels = ["T1", "T2", "T3", "T4"]
combresp4 = ct.combine_time_responses(
[resp1, resp2, resp3], trace_labels=labels)
assert combresp4.trace_labels == labels
assert combresp4.trace_types == [None, None, 'step', 'step']
# Automatically generated trace label names and types
resp5 = ct.step_response(sys_mimo, timepts)
resp5.title = "test"
resp5.trace_labels = None
resp5.trace_types = None
combresp5 = ct.combine_time_responses([resp1, resp5])
assert combresp5.trace_labels == [resp1.title] + \
["test, trace 0", "test, trace 1"]
assert combresp5.trace_types == [None, None, None]
# ntraces = 0 with trace_types != None
# https://github.com/python-control/python-control/issues/1025
resp6 = ct.forced_response(sys_mimo, timepts, U)
combresp6 = ct.combine_time_responses([resp1, resp6])
assert combresp6.trace_types == [None, 'forced']
with pytest.raises(ValueError, match="must have the same number"):
resp = ct.step_response(ct.rss(4, 2, 3), timepts)
ct.combine_time_responses([resp1, resp])
with pytest.raises(ValueError, match="trace labels does not match"):
ct.combine_time_responses(
[resp1, resp2], trace_labels=["T1", "T2", "T3"])
with pytest.raises(ValueError, match="must have the same time"):
resp = ct.step_response(ct.rss(4, 2, 3), timepts/2)
ct.combine_time_responses([resp1, resp])
@pytest.mark.parametrize("resp_fcn", [
ct.step_response, ct.initial_response, ct.impulse_response,
ct.forced_response, ct.input_output_response])
@pytest.mark.usefixtures('mplcleanup')
def test_list_responses(resp_fcn):
sys1 = ct.rss(2, 2, 2, strictly_proper=True)
sys2 = ct.rss(2, 2, 2, strictly_proper=True)
# Figure out the expected shape of the system
match resp_fcn:
case ct.step_response | ct.impulse_response:
shape = (2, 2)
kwargs = {}
case ct.initial_response:
shape = (2, 1)
kwargs = {}
case ct.forced_response | ct.input_output_response:
shape = (4, 1) # outputs and inputs both plotted
T = np.linspace(0, 10)
U = [np.sin(T), np.cos(T)]
kwargs = {'T': T, 'U': U}
resp1 = resp_fcn(sys1, **kwargs)
resp2 = resp_fcn(sys2, **kwargs)
# Sequential plotting results in colors rotating
plt.figure()
cplt1 = resp1.plot()
cplt2 = resp2.plot()
assert cplt1.shape == shape # legacy access (OK here)
assert cplt2.shape == shape # legacy access (OK here)
for row in range(2): # just look at the outputs
for col in range(shape[1]):
assert cplt1.lines[row, col][0].get_color() == 'tab:blue'
assert cplt2.lines[row, col][0].get_color() == 'tab:orange'
plt.figure()
resp_combined = resp_fcn([sys1, sys2], **kwargs)
assert isinstance(resp_combined, ct.timeresp.TimeResponseList)
assert resp_combined[0].time[-1] == max(resp1.time[-1], resp2.time[-1])
assert resp_combined[1].time[-1] == max(resp1.time[-1], resp2.time[-1])
cplt = resp_combined.plot()
assert cplt.lines.shape == shape
for row in range(2): # just look at the outputs
for col in range(shape[1]):
assert cplt.lines[row, col][0].get_color() == 'tab:blue'
assert cplt.lines[row, col][1].get_color() == 'tab:orange'
@pytest.mark.slycot
@pytest.mark.usefixtures('mplcleanup')
def test_linestyles():
# Check to make sure we can change line styles
sys_mimo = ct.tf2ss(
[[[1], [0.1]], [[0.2], [1]]],
[[[1, 0.6, 1], [1, 1, 1]], [[1, 0.4, 1], [1, 2, 1]]], name="MIMO")
cplt = ct.step_response(sys_mimo).plot('k--', plot_inputs=True)
for ax in np.nditer(cplt.lines, flags=["refs_ok"]):
for line in ax.item():
assert line.get_color() == 'k'
assert line.get_linestyle() == '--'
cplt = ct.step_response(sys_mimo).plot(
plot_inputs='overlay', overlay_signals=True, overlay_traces=True,
output_props=[{'color': c} for c in ['blue', 'orange']],
input_props=[{'color': c} for c in ['red', 'green']],
trace_props=[{'linestyle': s} for s in ['-', '--']])
assert cplt.lines.shape == (1, 1)
lines = cplt.lines[0, 0]
assert lines[0].get_color() == 'blue' and lines[0].get_linestyle() == '-'
assert lines[1].get_color() == 'orange' and lines[1].get_linestyle() == '-'
assert lines[2].get_color() == 'red' and lines[2].get_linestyle() == '-'
assert lines[3].get_color() == 'green' and lines[3].get_linestyle() == '-'
assert lines[4].get_color() == 'blue' and lines[4].get_linestyle() == '--'
assert lines[5].get_color() == 'orange' and lines[5].get_linestyle() == '--'
assert lines[6].get_color() == 'red' and lines[6].get_linestyle() == '--'
assert lines[7].get_color() == 'green' and lines[7].get_linestyle() == '--'
@pytest.mark.parametrize("resp_fcn", [
ct.step_response, ct.initial_response, ct.impulse_response,
ct.forced_response, ct.input_output_response])
@pytest.mark.usefixtures('editsdefaults', 'mplcleanup')
def test_timeplot_trace_labels(resp_fcn):
plt.close('all')
sys1 = ct.rss(2, 2, 2, strictly_proper=True, name='sys1')
sys2 = ct.rss(2, 2, 2, strictly_proper=True, name='sys2')
# Figure out the expected shape of the system
match resp_fcn:
case ct.step_response | ct.impulse_response:
kwargs = {}
case ct.initial_response:
kwargs = {}
case ct.forced_response | ct.input_output_response:
T = np.linspace(0, 10)
U = [np.sin(T), np.cos(T)]
kwargs = {'T': T, 'U': U}
# Use figure frame for suptitle to speed things up
ct.set_defaults('freqplot', title_frame='figure')
# Make sure default labels are as expected
cplt = resp_fcn([sys1, sys2], **kwargs).plot()
axs = cplt.axes
if axs.ndim == 1:
legend = axs[0].get_legend().get_texts()
else:
legend = axs[0, -1].get_legend().get_texts()
assert legend[0].get_text() == 'sys1'
assert legend[1].get_text() == 'sys2'
plt.close()
# Override labels all at once
cplt = resp_fcn([sys1, sys2], **kwargs).plot(label=['line1', 'line2'])
axs = cplt.axes
if axs.ndim == 1:
legend = axs[0].get_legend().get_texts()
else:
legend = axs[0, -1].get_legend().get_texts()
assert legend[0].get_text() == 'line1'
assert legend[1].get_text() == 'line2'
plt.close()
# Override labels one at a time
cplt = resp_fcn(sys1, **kwargs).plot(label='line1')
cplt = resp_fcn(sys2, **kwargs).plot(label='line2')
axs = cplt.axes
if axs.ndim == 1:
legend = axs[0].get_legend().get_texts()
else:
legend = axs[0, -1].get_legend().get_texts()
assert legend[0].get_text() == 'line1'
assert legend[1].get_text() == 'line2'
plt.close()
@pytest.mark.usefixtures('mplcleanup')
def test_full_label_override():
sys1 = ct.rss(2, 2, 2, strictly_proper=True, name='sys1')
sys2 = ct.rss(2, 2, 2, strictly_proper=True, name='sys2')
labels_2d = np.array([
["outsys1u1y1", "outsys1u1y2", "outsys1u2y1", "outsys1u2y2",
"outsys2u1y1", "outsys2u1y2", "outsys2u2y1", "outsys2u2y2"],
["inpsys1u1y1", "inpsys1u1y2", "inpsys1u2y1", "inpsys1u2y2",
"inpsys2u1y1", "inpsys2u1y2", "inpsys2u2y1", "inpsys2u2y2"]])
labels_4d = np.empty((2, 2, 2, 2), dtype=object)
for i, sys in enumerate(['sys1', 'sys2']):
for j, trace in enumerate(['u1', 'u2']):
for k, out in enumerate(['y1', 'y2']):
labels_4d[i, j, k, 0] = "out" + sys + trace + out
labels_4d[i, j, k, 1] = "inp" + sys + trace + out
# Test 4D labels
cplt = ct.step_response([sys1, sys2]).plot(
overlay_signals=True, overlay_traces=True, plot_inputs=True,
label=labels_4d)
axs = cplt.axes
assert axs.shape == (2, 1)
legend_text = axs[0, 0].get_legend().get_texts()
for i, label in enumerate(labels_2d[0]):
assert legend_text[i].get_text() == label
legend_text = axs[1, 0].get_legend().get_texts()
for i, label in enumerate(labels_2d[1]):
assert legend_text[i].get_text() == label
# Test 2D labels
cplt = ct.step_response([sys1, sys2]).plot(
overlay_signals=True, overlay_traces=True, plot_inputs=True,
label=labels_2d)
axs = cplt.axes
assert axs.shape == (2, 1)
legend_text = axs[0, 0].get_legend().get_texts()
for i, label in enumerate(labels_2d[0]):
assert legend_text[i].get_text() == label
legend_text = axs[1, 0].get_legend().get_texts()
for i, label in enumerate(labels_2d[1]):
assert legend_text[i].get_text() == label
@pytest.mark.usefixtures('mplcleanup')
def test_relabel():
sys1 = ct.rss(2, inputs='u', outputs='y')
sys2 = ct.rss(1, 1, 1) # uses default i/o labels
# Generate a plot with specific labels
ct.step_response(sys1).plot()
# Generate a new plot, which overwrites labels
cplt = ct.step_response(sys2).plot()
ax = cplt.axes
assert ax[0, 0].get_ylabel() == 'y[0]'
# Regenerate the first plot
plt.figure()
ct.step_response(sys1).plot()
# Generate a new plt, without relabeling
with pytest.warns(FutureWarning, match="deprecated"):
cplt = ct.step_response(sys2).plot(relabel=False)
assert cplt.axes[0, 0].get_ylabel() == 'y'
def test_errors():
sys = ct.rss(2, 1, 1)
stepresp = ct.step_response(sys)
with pytest.raises(AttributeError,
match="(has no property|unexpected keyword)"):
stepresp.plot(unknown=None)
with pytest.raises(AttributeError,
match="(has no property|unexpected keyword)"):
ct.time_response_plot(stepresp, unknown=None)
with pytest.raises(ValueError, match="unrecognized value"):
stepresp.plot(plot_inputs='unknown')
for kw in ['input_props', 'output_props', 'trace_props']:
propkw = {kw: {'color': 'green'}}
with pytest.warns(UserWarning, match="ignored since fmt string"):
cplt = stepresp.plot('k-', **propkw)
assert cplt.lines[0, 0][0].get_color() == 'k'
# Make sure TimeResponseLists also work
stepresp = ct.step_response([sys, sys])
with pytest.raises(AttributeError,
match="(has no property|unexpected keyword)"):
stepresp.plot(unknown=None)
def test_legend_customization():
sys = ct.rss(4, 2, 1, name='sys')
timepts = np.linspace(0, 10)
U = np.sin(timepts)
resp = ct.input_output_response(sys, timepts, U)
# Generic input/output plot
cplt = resp.plot(overlay_signals=True)
axs = cplt.axes
assert axs[0, 0].get_legend()._loc == 7 # center right
assert len(axs[0, 0].get_legend().get_texts()) == 2
assert axs[1, 0].get_legend() == None
plt.close()
# Hide legend
cplt = resp.plot(overlay_signals=True, show_legend=False)
axs = cplt.axes
assert axs[0, 0].get_legend() == None
assert axs[1, 0].get_legend() == None
plt.close()
# Put legend in both axes
cplt = resp.plot(
overlay_signals=True, legend_map=[['center left'], ['center right']])
axs = cplt.axes
assert axs[0, 0].get_legend()._loc == 6 # center left
assert len(axs[0, 0].get_legend().get_texts()) == 2
assert axs[1, 0].get_legend()._loc == 7 # center right
assert len(axs[1, 0].get_legend().get_texts()) == 1
plt.close()
if __name__ == "__main__":
#
# Interactive mode: generate plots for manual viewing
#
# Running this script in python (or better ipython) will show a
# collection of figures that should all look OK on the screeen.
#
# In interactive mode, turn on ipython interactive graphics
plt.ion()
# Start by clearing existing figures
plt.close('all')
# Define a set of systems to test
sys_siso = ct.tf2ss([1], [1, 2, 1], name="SISO")
sys_mimo = ct.tf2ss(
[[[1], [0.1]], [[0.2], [1]]],
[[[1, 0.6, 1], [1, 1, 1]], [[1, 0.4, 1], [1, 2, 1]]], name="MIMO")
# Define and run a selected set of interesting tests
# def test_response_plots(
# fcn, sys, plot_inputs, plot_outputs, overlay_signals,
# overlay_traces, transpose, second_system, clear=True):
N, T, F = None, True, False
test_cases = [
# response fcn system in out cs ct tr ss
(ct.step_response, sys_siso, N, T, F, F, F, F), # 1
(ct.step_response, sys_siso, T, F, F, F, F, F), # 2
(ct.step_response, sys_siso, T, T, F, F, F, T), # 3
(ct.step_response, sys_siso, 'overlay', T, F, F, F, T), # 4
(ct.step_response, sys_mimo, F, T, F, F, F, F), # 5
(ct.step_response, sys_mimo, T, T, F, F, F, F), # 6
(ct.step_response, sys_mimo, 'overlay', T, F, F, F, F), # 7
(ct.step_response, sys_mimo, T, T, T, F, F, F), # 8
(ct.step_response, sys_mimo, T, T, T, T, F, F), # 9
(ct.step_response, sys_mimo, T, T, F, F, T, F), # 10
(ct.step_response, sys_mimo, T, T, T, F, T, F), # 11
(ct.step_response, sys_mimo, 'overlay', T, T, F, T, F), # 12
(ct.forced_response, sys_mimo, N, T, T, F, T, F), # 13
(ct.forced_response, sys_mimo, 'overlay', T, F, F, F, F), # 14
]
for args in test_cases:
test_response_plots(*args, clear=F)
#
# Run a few more special cases to show off capabilities (and save some
# of them for use in the documentation).
#
test_legend_map() # show ability to set legend location
# Basic step response
plt.figure()
ct.step_response(sys_mimo).plot()
plt.savefig('timeplot-mimo_step-default.png')
# Step response with plot_inputs, overlay_signals
plt.figure()
ct.step_response(sys_mimo).plot(
plot_inputs=True, overlay_signals=True,
title="Step response for 2x2 MIMO system " +
"[plot_inputs, overlay_signals]")
plt.savefig('timeplot-mimo_step-pi_cs.png')
# Input/output response with overlaid inputs, legend_map
plt.figure()
timepts = np.linspace(0, 10, 100)
U = np.vstack([np.sin(timepts), np.cos(2*timepts)])
ct.input_output_response(sys_mimo, timepts, U).plot(
plot_inputs='overlay',
legend_map=np.array([['lower right'], ['lower right']]),
title="I/O response for 2x2 MIMO system " +
"[plot_inputs='overlay', legend_map]")
plt.savefig('timeplot-mimo_ioresp-ov_lm.png')
# Multi-trace plot, transpose
plt.figure()
U = np.vstack([np.sin(timepts), np.cos(2*timepts)])
resp1 = ct.input_output_response(sys_mimo, timepts, U)
U = np.vstack([np.cos(2*timepts), np.sin(timepts)])
resp2 = ct.input_output_response(sys_mimo, timepts, U)
ct.combine_time_responses(
[resp1, resp2], trace_labels=["Scenario #1", "Scenario #2"]).plot(
transpose=True,
title="I/O responses for 2x2 MIMO system, multiple traces "
"[transpose]")
plt.savefig('timeplot-mimo_ioresp-mt_tr.png')
plt.figure()
cplt = ct.step_response(sys_mimo).plot(
plot_inputs='overlay', overlay_signals=True, overlay_traces=True,
output_props=[{'color': c} for c in ['blue', 'orange']],
input_props=[{'color': c} for c in ['red', 'green']],
trace_props=[{'linestyle': s} for s in ['-', '--']])
plt.savefig('timeplot-mimo_step-linestyle.png')
sys1 = ct.rss(4, 2, 2)
sys2 = ct.rss(4, 2, 2)
resp_list = ct.step_response([sys1, sys2])
fig = plt.figure()
cplt = ct.combine_time_responses(
[ct.step_response(sys1, resp_list[0].time),
ct.step_response(sys2, resp_list[1].time)]
).plot(overlay_traces=True)
cplt.set_plot_title("[Combine] " + fig._suptitle._text)
fig = plt.figure()
ct.step_response(sys1).plot()
cplt = ct.step_response(sys2).plot()
cplt.set_plot_title("[Sequential] " + fig._suptitle._text)
fig = plt.figure()
ct.step_response(sys1).plot(color='b')
cplt = ct.step_response(sys2).plot(color='r')
cplt.set_plot_title("[Seq w/color] " + fig._suptitle._text)
fig = plt.figure()
cplt = ct.step_response([sys1, sys2]).plot()
cplt.set_plot_title("[List] " + fig._suptitle._text)