77import matplotlib .pyplot as plt
88import 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"),
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+
3470def 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
129239def 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+
141313def 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-
154325if __name__ == "__main__" :
155326 #
156327 # Interactive mode: generate plots for manual viewing
0 commit comments