1010from control .tests .conftest import slycotonly
1111pytestmark = pytest .mark .usefixtures ("mplcleanup" )
1212
13+ #
14+ # Define a system for testing out different sharing options
15+ #
16+
17+ omega = np .logspace (- 2 , 2 , 5 )
18+ fresp1 = np .array ([10 + 0j , 5 - 5j , 1 - 1j , 0.5 - 1j , - .1j ])
19+ fresp2 = np .array ([1j , 0.5 - 0.5j , - 0.5 , 0.1 - 0.1j , - .05j ]) * 0.1
20+ fresp3 = np .array ([10 + 0j , - 20j , - 10 , 2j , 1 ])
21+ fresp4 = np .array ([10 + 0j , 5 - 5j , 1 - 1j , 0.5 - 1j , - .1j ]) * 0.01
22+
23+ fresp = np .empty ((2 , 2 , omega .size ), dtype = complex )
24+ fresp [0 , 0 ] = fresp1
25+ fresp [0 , 1 ] = fresp2
26+ fresp [1 , 0 ] = fresp3
27+ fresp [1 , 1 ] = fresp4
28+ manual_response = ct .FrequencyResponseData (
29+ fresp , omega , sysname = "Manual Response" )
30+
31+ @pytest .mark .parametrize (
32+ "sys" , [
33+ ct .tf ([1 ], [1 , 2 , 1 ], name = 'System 1' ), # SISO
34+ manual_response , # simple MIMO
35+ ])
36+ # @pytest.mark.parametrize("pltmag", [True, False])
37+ # @pytest.mark.parametrize("pltphs", [True, False])
38+ # @pytest.mark.parametrize("shrmag", ['row', 'all', False, None])
39+ # @pytest.mark.parametrize("shrphs", ['row', 'all', False, None])
40+ # @pytest.mark.parametrize("shrfrq", ['col', 'all', False, None])
41+ # @pytest.mark.parametrize("secsys", [False, True])
42+ @pytest .mark .parametrize ( # combinatorial-style test (faster)
43+ "pltmag, pltphs, shrmag, shrphs, shrfrq, secsys" ,
44+ [(True , True , None , None , None , False ),
45+ (True , False , None , None , None , False ),
46+ (False , True , None , None , None , False ),
47+ (True , True , None , None , None , True ),
48+ (True , True , 'row' , 'row' , 'col' , False ),
49+ (True , True , 'row' , 'row' , 'all' , True ),
50+ (True , True , 'all' , 'row' , None , False ),
51+ (True , True , 'row' , 'all' , None , True ),
52+ (True , True , 'none' , 'none' , None , True ),
53+ (True , False , 'all' , 'row' , None , False ),
54+ (True , True , True , 'row' , None , True ),
55+ (True , True , None , 'row' , True , False ),
56+ (True , True , 'row' , None , None , True ),
57+ ])
58+ def test_response_plots (
59+ sys , pltmag , pltphs , shrmag , shrphs , shrfrq , secsys , clear = True ):
60+
61+ # Save up the keyword arguments
62+ kwargs = dict (
63+ plot_magnitude = pltmag , plot_phase = pltphs ,
64+ share_magnitude = shrmag , share_phase = shrphs , share_frequency = shrfrq ,
65+ # overlay_outputs=ovlout, overlay_inputs=ovlinp
66+ )
67+
68+ # Create the response
69+ if isinstance (sys , ct .FrequencyResponseData ):
70+ response = sys
71+ else :
72+ response = ct .frequency_response (sys )
73+
74+ # Look for cases where there are no data to plot
75+ if not pltmag and not pltphs :
76+ return None
77+
78+ # Plot the frequency response
79+ plt .figure ()
80+ out = response .plot (** kwargs )
81+
82+ # Make sure all of the outputs are of the right type
83+ nlines_plotted = 0
84+ for ax_lines in np .nditer (out , flags = ["refs_ok" ]):
85+ for line in ax_lines .item ():
86+ assert isinstance (line , mpl .lines .Line2D )
87+ nlines_plotted += 1
88+
89+ # Make sure number of plots is correct
90+ nlines_expected = response .ninputs * response .noutputs * \
91+ (2 if pltmag and pltphs else 1 )
92+ assert nlines_plotted == nlines_expected
93+
94+ # Save the old axes to compare later
95+ old_axes = plt .gcf ().get_axes ()
96+
97+ # Add additional data (and provide info in the title)
98+ if secsys :
99+ newsys = ct .rss (
100+ 4 , sys .noutputs , sys .ninputs , strictly_proper = True )
101+ ct .frequency_response (newsys ).plot (** kwargs )
102+
103+ # Make sure we have the same axes
104+ new_axes = plt .gcf ().get_axes ()
105+ assert new_axes == old_axes
106+
107+ # Make sure every axes has multiple lines
108+ for ax in new_axes :
109+ assert len (ax .get_lines ()) > 1
110+
111+ # Update the title so we can see what is going on
112+ fig = out [0 , 0 ][0 ].axes .figure
113+ fig .suptitle (
114+ fig ._suptitle ._text +
115+ f" [{ sys .noutputs } x{ sys .ninputs } , pm={ pltmag } , pp={ pltphs } ,"
116+ f" sm={ shrmag } , sp={ shrphs } , sf={ shrfrq } ]" , # TODO: ", "
117+ # f"oo={ovlout}, oi={ovlinp}, ss={secsys}]", # TODO: add back
118+ fontsize = 'small' )
119+
120+ # Get rid of the figure to free up memory
121+ if clear :
122+ plt .close ('.Figure' )
123+
124+
125+ # Use the manaul response to verify that different settings are working
126+ def test_manual_response_limits ():
127+ # Default response: limits should be the same across rows
128+ out = manual_response .plot ()
129+ axs = ct .get_plot_axes (out )
130+ for i in range (manual_response .noutputs ):
131+ for j in range (1 , manual_response .ninputs ):
132+ # Everything in the same row should have the same limits
133+ assert axs [i * 2 , 0 ].get_ylim () == axs [i * 2 , j ].get_ylim ()
134+ assert axs [i * 2 + 1 , 0 ].get_ylim () == axs [i * 2 + 1 , j ].get_ylim ()
135+ # Different rows have different limits
136+ assert axs [0 , 0 ].get_ylim () != axs [2 , 0 ].get_ylim ()
137+ assert axs [1 , 0 ].get_ylim () != axs [3 , 0 ].get_ylim ()
138+
139+ # TODO: finish writing tests
140+
13141def test_basic_freq_plots (savefigs = False ):
14142 # Basic SISO Bode plot
15143 plt .figure ()
@@ -23,7 +151,7 @@ def test_basic_freq_plots(savefigs=False):
23151
24152 # Basic MIMO Bode plot
25153 plt .figure ()
26- sys_mimo = ct .tf2ss (
154+ sys_mimo = ct .tf (
27155 [[[1 ], [0.1 ]], [[0.2 ], [1 ]]],
28156 [[[1 , 0.6 , 1 ], [1 , 1 , 1 ]], [[1 , 0.4 , 1 ], [1 , 2 , 1 ]]], name = "MIMO" )
29157 ct .frequency_response (sys_mimo ).plot ()
@@ -41,6 +169,17 @@ def test_basic_freq_plots(savefigs=False):
41169 ct .frequency_response (sys_mimo ).plot (plot_magnitude = False )
42170
43171
172+ def test_gangof4_plots (savefigs = False ):
173+ proc = ct .tf ([1 ], [1 , 1 , 1 ], name = "process" )
174+ ctrl = ct .tf ([100 ], [1 , 5 ], name = "control" )
175+
176+ plt .figure ()
177+ ct .gangof4_plot (proc , ctrl )
178+
179+ if savefigs :
180+ plt .savefig ('freqplot-gangof4.png' )
181+
182+
44183if __name__ == "__main__" :
45184 #
46185 # Interactive mode: generate plots for manual viewing
@@ -55,10 +194,36 @@ def test_basic_freq_plots(savefigs=False):
55194 # Start by clearing existing figures
56195 plt .close ('all' )
57196
197+ # Define a set of systems to test
198+ sys_siso = ct .tf ([1 ], [1 , 2 , 1 ], name = "SISO" )
199+ sys_mimo = ct .tf (
200+ [[[1 ], [0.1 ]], [[0.2 ], [1 ]]],
201+ [[[1 , 0.6 , 1 ], [1 , 1 , 1 ]], [[1 , 0.4 , 1 ], [1 , 2 , 1 ]]], name = "MIMO" )
202+ sys_test = manual_response
203+
204+ # Run through a large number of test cases
205+ test_cases = [
206+ # sys pltmag pltphs shrmag shrphs shrfrq secsys
207+ (sys_siso , True , True , None , None , None , False ),
208+ (sys_siso , True , True , None , None , None , True ),
209+ (sys_mimo , True , True , 'row' , 'row' , 'col' , False ),
210+ (sys_mimo , True , True , 'row' , 'row' , 'col' , True ),
211+ (sys_test , True , True , 'row' , 'row' , 'col' , False ),
212+ (sys_test , True , True , 'row' , 'row' , 'col' , True ),
213+ (sys_test , True , True , 'none' , 'none' , 'col' , True ),
214+ (sys_test , True , True , 'all' , 'row' , 'col' , False ),
215+ (sys_test , True , True , 'row' , 'all' , 'col' , True ),
216+ (sys_test , True , True , None , 'row' , 'col' , False ),
217+ (sys_test , True , True , 'row' , None , 'col' , True ),
218+ ]
219+ for args in test_cases :
220+ test_response_plots (* args , clear = False )
221+
58222 # Define and run a selected set of interesting tests
59223 # TODO: TBD (see timeplot_test.py for format)
60224
61225 test_basic_freq_plots (savefigs = True )
226+ test_gangof4_plots (savefigs = True )
62227
63228 #
64229 # Run a few more special cases to show off capabilities (and save some
0 commit comments