Control plot refactoring for consistent functionality by murrayrm · Pull Request #1034 · python-control/python-control
This PR makes a (fairly large) number of changes to control plotting functions to provide consistent functionality. The majority of changes involve making functionality that was present in some plot functions but not others available consistently across all _plot() functions. Everything is backward compatible with v0.10.0.
A set of common options are available to customize control plots in various ways. The following general rules apply:
If a plotting function is called multiple times with data that generate control plots with the same shape for the array of subplots, the new data will be overlaid with the old data, with a change in color(s) for the new data (chosen from the standard matplotlib color cycle). If not overridden, the plot title and legends will be updated to reflect all data shown on the plot.
If a plotting function is called and the shape for the array of subplots does not match the currently displayed plot, a new figure is created. Note that only the shape is checked, so if two different types of plotting commands that generate the same shape of subplots are called sequentially, the matplotlib.pyplot.figure command should be used to explicitly create a new figure.
The ax keyword argument can be used to direct the plotting function to use a specific axes or array of axes. The value of the ax keyword must have the proper number of axes for the plot (so a plot generating a 2x2 array of subplots should be given a 2x2 array of axes for the ax keyword).
The color, linestyle, linewidth, and other matplotlib line property arguments can be used to override the default line properties. If these arguments are absent, the default matplotlib line properties are used and the color cycles through the default matplotlib color cycle.
The :func:~control.bode_plot, :func:~control.time_response_plot, and selected other commands can also accept a matplotlib format string (e.g., 'r--'). The format string must appear as a positional argument right after the required data argument.
Note that line property arguments are the same for all lines generated as part of a single plotting command call, including when multiple responses are passed as a list to the plotting command. For this reason it is often easiest to call multiple plot commands in sequence, with each command setting the line properties for that system/trace.
The label keyword argument can be used to override the line labels that are used in generating the title and legend. If more than one line is being plotted in a given call to a plot command, the label argument value should be a list of labels, one for each line, in the order they will appear in the legend.
For input/output plots (frequency and time responses), the labels that appear in the legend are of the form "<output name>, <input name>, <trace name>, <system name>". The trace name is used only for multi-trace time plots (for example, step responses for MIMO systems). Common information present in all traces is removed, so that the labels appearing in the legend represent the unique characteristics of each line.
For non-input/output plots (e.g., Nyquist plots, pole/zero plots, root locus plots), the default labels are the system name.
If label is set to False, individual lines are still given labels, but no legend is generated in the plot (this can also be accomplished by setting legend_map to False.
Note: the label keyword argument is not implemented for describing function plots or phase plane plots, since these plots are primarily intended to be for a single system. Standard matplotlib commands can be used to customize these plots for displaying information for multiple systems.
The legend_loc, legend_map and show_legend keyword arguments can be used to customize the locations for legends. By default, a minimal number of legends are used such that lines can be uniquely identified and no legend is generated if there is only one line in the plot. Setting show_legend to False will suppress the legend and setting it to True will force the legend to be displayed even if there is only a single line in each axes. In addition, if the value of the legend_loc keyword argument is set to a string or integer, it will set the position of the legend as described in the matplotlib.legend documentation. Finally, legend_map can be set to an` array that matches the shape of the subplots, with each item being a string indicating the location of the legend for that axes (or None for no legend).
The rcParams keyword argument can be used to override the default matplotlib style parameters used when creating a plot. The default parameters for all control plots are given by the ct.rcParams dictionary and have the following values:
| Key | Value |
|---|---|
| ‘axes.labelsize’ | ‘small’ |
| ‘axes.titlesize’ | ‘small’ |
| ‘figure.titlesize’ | ‘medium’ |
| ‘legend.fontsize’ | ‘x-small’ |
| ‘xtick.labelsize’ | ‘small’ |
| ‘ytick.labelsize’ | ‘small’ |
Only those values that should be changed from the default need to be specified in the rcParams keyword argument. To override the defaults for all control plots, update the ct.rcParams dictionary entries.
The default values for style parameters for control plots can be restored using :func:~control.reset_rcParams.
The title keyword can be used to override the automatic creation of the plot title. The default title is a string of the form " plot for " where is a list of the sys names contained in the plot (which can be updated if the plotting is called multiple times). Use title=False to suppress the title completely. The title can also be updated using the control.ControlPlot.set_plot_title method for the returned control plot object.
The plot title is only generated if ax is None.
P = ct.tf([0.02], [1, 0.1, 0.01]) # servomechanism
C1 = ct.tf([1, 1], [1, 0]) # unstable
L1 = P * C1
C2 = ct.tf([1, 0.05], [1, 0]) # stable
L2 = P * C2
plt.rcParams.update(ct.rcParams)
fig = plt.figure(figsize=[7, 4])
ax_mag = fig.add_subplot(2, 2, 1)
ax_phase = fig.add_subplot(2, 2, 3)
ax_nyquist = fig.add_subplot(1, 2, 2)
ct.bode_plot(
[L1, L2], ax=[ax_mag, ax_phase],
label=["$L_1$ (unstable)", "$L_2$ (unstable)"],
show_legend=False)
ax_mag.set_title("Bode plot for $L_1$, $L_2$")
ax_mag.tick_params(labelbottom=False)
fig.align_labels()
ct.nyquist_plot(L1, ax=ax_nyquist, label="$L_1$ (unstable)")
ct.nyquist_plot(
L2, ax=ax_nyquist, label="$L_2$ (stable)",
max_curve_magnitude=22, legend_loc='upper right')
ax_nyquist.set_title("Nyquist plot for $L_1$, $L_2$")
fig.suptitle("Loop analysis for servomechanism control design")
plt.tight_layout()