|
29 | 29 | (ct.input_output_response, ct.time_response_plot), |
30 | 30 | ] |
31 | 31 |
|
| 32 | +nolabel_plot_fcns = [ct.describing_function_plot, ct.phase_plane_plot] |
32 | 33 | deprecated_fcns = [ct.phase_plot] |
33 | 34 |
|
34 | 35 | # Make sure we didn't miss any plotting functions |
@@ -135,6 +136,125 @@ def test_plot_ax_processing(resp_fcn, plot_fcn): |
135 | 136 | assert get_line_color(cplt3) != get_line_color(cplt1) |
136 | 137 | assert get_line_color(cplt3) != get_line_color(cplt2) |
137 | 138 |
|
| 139 | + |
| 140 | +@pytest.mark.parametrize("resp_fcn, plot_fcn", resp_plot_fcns) |
| 141 | +@pytest.mark.usefixtures('mplcleanup') |
| 142 | +def test_plot_label_processing(resp_fcn, plot_fcn): |
| 143 | + # Utility function to make sure legends are OK |
| 144 | + def assert_legend(cplt, expected_texts): |
| 145 | + # Check to make sure the labels are OK in legend |
| 146 | + legend = None |
| 147 | + for ax in cplt.axes.flatten(): |
| 148 | + legend = ax.get_legend() |
| 149 | + if legend is not None: |
| 150 | + break |
| 151 | + if expected_texts is None: |
| 152 | + assert legend is None |
| 153 | + else: |
| 154 | + assert legend is not None |
| 155 | + legend_texts = [entry.get_text() for entry in legend.get_texts()] |
| 156 | + assert legend_texts == expected_texts |
| 157 | + |
| 158 | + # Create some systems to use |
| 159 | + sys1 = ct.rss(2, 1, 1, strictly_proper=True, name="sys[1]") |
| 160 | + sys1c = ct.rss(4, 1, 1, strictly_proper=True, name="sys[1]_C") |
| 161 | + sys2 = ct.rss(4, 1, 1, strictly_proper=True, name="sys[2]") |
| 162 | + |
| 163 | + # Set up arguments |
| 164 | + kwargs = meth_kwargs = plot_fcn_kwargs = {} |
| 165 | + default_labels = ["sys[1]", "sys[2]"] |
| 166 | + expected_labels = ["sys1_", "sys2_"] |
| 167 | + match resp_fcn, plot_fcn: |
| 168 | + case ct.describing_function_response, _: |
| 169 | + F = ct.descfcn.saturation_nonlinearity(1) |
| 170 | + amp = np.linspace(1, 4, 10) |
| 171 | + args1 = (sys1, F, amp) |
| 172 | + args2 = (sys2, F, amp) |
| 173 | + |
| 174 | + case ct.gangof4_response, _: |
| 175 | + args1 = (sys1, sys1c) |
| 176 | + args2 = (sys2, sys1c) |
| 177 | + default_labels = ["P=sys[1]", "P=sys[2]"] |
| 178 | + |
| 179 | + case ct.frequency_response, ct.nichols_plot: |
| 180 | + args1 = (sys1, ) |
| 181 | + args2 = (sys2, ) |
| 182 | + meth_kwargs = {'plot_type': 'nichols'} |
| 183 | + |
| 184 | + case ct.root_locus_map, ct.root_locus_plot: |
| 185 | + args1 = (sys1, ) |
| 186 | + args2 = (sys2, ) |
| 187 | + meth_kwargs = plot_fcn_kwargs = {'interactive': False} |
| 188 | + |
| 189 | + case (ct.forced_response | ct.input_output_response, _): |
| 190 | + timepts = np.linspace(1, 10) |
| 191 | + U = np.sin(timepts) |
| 192 | + args1 = (resp_fcn(sys1, timepts, U), ) |
| 193 | + args2 = (resp_fcn(sys2, timepts, U), ) |
| 194 | + argsc = (resp_fcn([sys1, sys2], timepts, U), ) |
| 195 | + |
| 196 | + case (ct.impulse_response | ct.initial_response | ct.step_response, _): |
| 197 | + args1 = (resp_fcn(sys1), ) |
| 198 | + args2 = (resp_fcn(sys2), ) |
| 199 | + argsc = (resp_fcn([sys1, sys2]), ) |
| 200 | + |
| 201 | + case _, _: |
| 202 | + args1 = (sys1, ) |
| 203 | + args2 = (sys2, ) |
| 204 | + |
| 205 | + if plot_fcn in nolabel_plot_fcns: |
| 206 | + pytest.skip(f"labels not implemented for {plot_fcn}") |
| 207 | + |
| 208 | + # Generate the first plot, with default labels |
| 209 | + cplt1 = plot_fcn(*args1, **kwargs, **plot_fcn_kwargs) |
| 210 | + assert isinstance(cplt1, ct.ControlPlot) |
| 211 | + assert_legend(cplt1, None) |
| 212 | + |
| 213 | + # Generate second plot with default labels |
| 214 | + cplt2 = plot_fcn(*args2, **kwargs, **plot_fcn_kwargs) |
| 215 | + assert isinstance(cplt2, ct.ControlPlot) |
| 216 | + assert_legend(cplt2, default_labels) |
| 217 | + plt.close() |
| 218 | + |
| 219 | + # Generate both plots at the same time |
| 220 | + if len(args1) == 1 and plot_fcn != ct.time_response_plot: |
| 221 | + cplt = plot_fcn([*args1, *args2], **kwargs, **plot_fcn_kwargs) |
| 222 | + assert isinstance(cplt, ct.ControlPlot) |
| 223 | + assert_legend(cplt, default_labels) |
| 224 | + elif len(args1) == 1 and plot_fcn == ct.time_response_plot: |
| 225 | + # Use TimeResponseList.plot() to generate combined response |
| 226 | + cplt = argsc[0].plot(**kwargs, **plot_fcn_kwargs) |
| 227 | + assert isinstance(cplt, ct.ControlPlot) |
| 228 | + assert_legend(cplt, default_labels) |
| 229 | + plt.close() |
| 230 | + |
| 231 | + # Generate plots sequentially, with updated labels |
| 232 | + cplt1 = plot_fcn( |
| 233 | + *args1, **kwargs, **plot_fcn_kwargs, label=expected_labels[0]) |
| 234 | + assert isinstance(cplt1, ct.ControlPlot) |
| 235 | + assert_legend(cplt1, None) |
| 236 | + |
| 237 | + cplt2 = plot_fcn( |
| 238 | + *args2, **kwargs, **plot_fcn_kwargs, label=expected_labels[1]) |
| 239 | + assert isinstance(cplt2, ct.ControlPlot) |
| 240 | + assert_legend(cplt2, expected_labels) |
| 241 | + plt.close() |
| 242 | + |
| 243 | + # Generate both plots at the same time, with updated labels |
| 244 | + if len(args1) == 1 and plot_fcn != ct.time_response_plot: |
| 245 | + cplt = plot_fcn( |
| 246 | + [*args1, *args2], **kwargs, **plot_fcn_kwargs, |
| 247 | + label=expected_labels) |
| 248 | + assert isinstance(cplt, ct.ControlPlot) |
| 249 | + assert_legend(cplt, expected_labels) |
| 250 | + elif len(args1) == 1 and plot_fcn == ct.time_response_plot: |
| 251 | + # Use TimeResponseList.plot() to generate combined response |
| 252 | + cplt = argsc[0].plot( |
| 253 | + **kwargs, **plot_fcn_kwargs, label=expected_labels) |
| 254 | + assert isinstance(cplt, ct.ControlPlot) |
| 255 | + assert_legend(cplt, expected_labels) |
| 256 | + |
| 257 | + |
138 | 258 | @pytest.mark.usefixtures('mplcleanup') |
139 | 259 | def test_rcParams(): |
140 | 260 | sys = ct.rss(2, 2, 2) |
|
0 commit comments