Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
5f6833f
speed up suptitle centering
murrayrm Jun 28, 2024
ca61be3
implement ControlPlot class for plotting return type
murrayrm Jun 27, 2024
639ffb4
add storage of legend objects
murrayrm Jun 29, 2024
acfadf0
fix bugs in legend generation for pole/zero plots
murrayrm Jul 3, 2024
8b7d399
improve consistency in use of cplt as return type for plots
murrayrm Jul 4, 2024
fb5c194
add unit tests for common plotting functionality
murrayrm Jul 4, 2024
97a5230
update phaseplot to use common ax, rcParams processing
murrayrm Jul 6, 2024
da5be15
TRV: fix legend size in Nyquist plots
murrayrm Jul 6, 2024
f818034
Fix suptitle for root locus plots
murrayrm Jul 6, 2024
0103fe7
update root_locus_plot to use common ax processing
murrayrm Jul 6, 2024
4f3d618
implement pole_zero_subplots
murrayrm Jul 6, 2024
0dc4790
update unit test for ax_processing
murrayrm Jul 14, 2024
cec9e70
update label processing to provide common functionality + unit tests
murrayrm Jul 15, 2024
89cda3d
update title processing to be uniform across _plot functions
murrayrm Jul 17, 2024
3a8fa7a
update plot title handling and make uniform
murrayrm Jul 18, 2024
8abb618
turn off title update if ax is given
murrayrm Jul 20, 2024
fc09a85
deprecate relabel keyword in time_response_plot
murrayrm Jul 20, 2024
4fe5f53
implement uniform legend processing + unit test/doc updates
murrayrm Jul 20, 2024
2e4961c
change suptitle_frame to title_frame
murrayrm Jul 20, 2024
02f2724
update rcParams processing
murrayrm Jul 21, 2024
4f4746d
deprecate get_plot_axes (with legacy testing)
murrayrm Jul 21, 2024
c3707d3
update plotting documentation
murrayrm Jul 21, 2024
4ec3612
add unit tests + docuementation for subplots
murrayrm Jul 21, 2024
e2f971b
small updates to increase code coverage
murrayrm Jul 21, 2024
d6df4be
update unit tests and documentation for line properties
murrayrm Jul 22, 2024
351bafe
unify color processesing
murrayrm Jul 26, 2024
36d1aef
fix small typos
murrayrm Jul 27, 2024
6b78134
fix color error introduced in phaseplot/separatrices
murrayrm Jul 27, 2024
d66064d
fix inconsistency in title for Nyquist plots
murrayrm Jul 27, 2024
9739750
small documentation updates (+ ruff configuration)
murrayrm Aug 4, 2024
9c6ff85
add gangof4_plot back as non-legacy function
murrayrm Aug 4, 2024
cd8fcfd
address review comments from @slivingston
murrayrm Aug 8, 2024
9f143ec
update freqplot unit test to avoid intermittent warnings
murrayrm Aug 8, 2024
1a04541
allow title to be None in _update_plot_title (fixes markov.py crash)
murrayrm Aug 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
update title processing to be uniform across _plot functions
  • Loading branch information
murrayrm committed Aug 8, 2024
commit 89cda3d0c22d87f43d23a062e2586cc43aeee717
9 changes: 7 additions & 2 deletions control/ctrlplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,9 @@
# sysnames = [response.sysname for response in data]
# if title is None:
# title = "Name plot for " + ", ".join(sysnames)
# _update_suptitle(fig, title, rcParams=rcParams)
# _update_suptitle(title, fig, rcParams=rcParams)
# else
# suptitle(title, fig, rcParams=rcParams)
#
# # Legacy processing of plot keyword
# if plot is True:
Expand Down Expand Up @@ -159,6 +161,9 @@ def __setitem__(self, item, val):
def reshape(self, *args):
return self.lines.reshape(*args)

def set_plot_title(self, title, frame='axes'):
suptitle(title, fig=self.figure, frame=frame)


#
# User functions
Expand Down Expand Up @@ -467,7 +472,7 @@ def _make_legend_labels(labels, ignore_common=False):
return labels


def _update_suptitle(fig, title, rcParams=None, frame='axes'):
def _update_suptitle(title, fig=None, frame='axes', rcParams=None):
if fig is not None and isinstance(title, str):
# Get the current title, if it exists
old_title = None if fig._suptitle is None else fig._suptitle._text
Expand Down
21 changes: 18 additions & 3 deletions control/freqplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,12 @@ def bode_plot(

Other Parameters
----------------
ax : array of Axes
The matplotlib Axes to draw the figure on. If not specified, the
Axes for the current figure are used or, if there is no current
figure with the correct number and shape of Axes, a new figure is
created. The shape of the array must match the shape of the
plotted data.
grid : bool
If True, plot grid lines on gain and phase plots. Default is set by
`config.defaults['freqplot.grid']`.
Expand Down Expand Up @@ -173,6 +179,8 @@ def bode_plot(
rcParams : dict
Override the default parameters used for generating plots.
Default is set by config.default['freqplot.rcParams'].
title : str, optional
Set the title of the plot. Defaults to plot type and system name(s).
wrap_phase : bool or float
If wrap_phase is `False` (default), then the phase will be unwrapped
so that it is continuously increasing or decreasing. If wrap_phase is
Expand Down Expand Up @@ -948,13 +956,16 @@ def gen_zero_centered_series(val_min, val_max, period):
seen = set()
sysnames = [response.sysname for response in data \
if not (response.sysname in seen or seen.add(response.sysname))]

if title is None:
if data[0].title is None:
title = "Bode plot for " + ", ".join(sysnames)
else:
# Allow data to set the title (used by gangof4)
title = data[0].title

_update_suptitle(fig, title, rcParams=rcParams, frame=suptitle_frame)
_update_suptitle(title, fig, rcParams=rcParams, frame=suptitle_frame)
else:
suptitle(title, fig=fig, rcParams=rcParams, frame=suptitle_frame)

#
# Create legends
Expand Down Expand Up @@ -1603,6 +1614,8 @@ def nyquist_plot(
start_marker_size : float, optional
Start marker size (in display coordinates). Default value is
4 and can be set using config.defaults['nyquist.start_marker_size'].
title : str, optional
Set the title of the plot. Defaults to plot type and system name(s).
warn_nyquist : bool, optional
If set to 'False', turn off warnings about frequencies above Nyquist.
warn_encirclements : bool, optional
Expand Down Expand Up @@ -1870,7 +1883,7 @@ def _parse_linestyle(style_name, allow_false=False):
# Display the unit circle, to read gain crossover frequency
if unit_circle:
plt.plot(cos, sin, **config.defaults['nyquist.circle_style'])

# Draw circles for given magnitudes of sensitivity
if ms_circles is not None:
for ms in ms_circles:
Expand Down Expand Up @@ -2243,6 +2256,8 @@ def singular_values_plot(
rcParams : dict
Override the default parameters used for generating plots.
Default is set up config.default['freqplot.rcParams'].
title : str, optional
Set the title of the plot. Defaults to plot type and system name(s).

See Also
--------
Expand Down
2 changes: 2 additions & 0 deletions control/nichols.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ def nichols_plot(
legend_loc : str, optional
For plots with multiple lines, a legend will be included in the
given location. Default is 'upper left'. Use False to supress.
title : str, optional
Set the title of the plot. Defaults to plot type and system name(s).
**kwargs : :func:`matplotlib.pyplot.plot` keyword properties, optional
Additional keywords passed to `matplotlib` to specify line properties.

Expand Down
9 changes: 7 additions & 2 deletions control/phaseplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@
def phase_plane_plot(
sys, pointdata=None, timedata=None, gridtype=None, gridspec=None,
plot_streamlines=True, plot_vectorfield=False, plot_equilpoints=True,
plot_separatrices=True, ax=None, suppress_warnings=False, **kwargs
plot_separatrices=True, ax=None, suppress_warnings=False, title=None,
**kwargs
):
"""Plot phase plane diagram.

Expand Down Expand Up @@ -135,6 +136,8 @@ def phase_plane_plot(
in the dict as keywords to :func:`~control.phaseplot.separatrices`.
suppress_warnings : bool, optional
If set to `True`, suppress warning messages in generating trajectories.
title : str, optional
Set the title of the plot. Defaults to plot type and system name(s).

"""
# Process arguments
Expand Down Expand Up @@ -217,7 +220,9 @@ def _create_kwargs(global_kwargs, local_kwargs, **other_kwargs):
# TODO: update to common code pattern
if user_ax is None:
with plt.rc_context(rcParams):
suptitle(f"Phase portrait for {sys.name}")
if title is None:
title = f"Phase portrait for {sys.name}"
suptitle(title)
ax.set_xlabel(sys.state_labels[0])
ax.set_ylabel(sys.state_labels[1])

Expand Down
2 changes: 1 addition & 1 deletion control/pzmap.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ def pole_zero_plot(
marker_width : int, optional
Set the line width of the markers used for poles and zeros.
title : str, optional
Set the title of the plot. Defaults plot type and system name(s).
Set the title of the plot. Defaults to plot type and system name(s).
xlim : list, optional
Set the limits for the x axis.
ylim : list, optional
Expand Down
120 changes: 119 additions & 1 deletion control/tests/ctrlplot_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ def test_plot_ax_processing(resp_fcn, plot_fcn):
else:
cplt3 = plot_fcn(*args, **kwargs, **meth_kwargs)
assert cplt3.figure == cplt1.figure

# Plot should have landed on top of previous plot, in different colors
assert np.all(cplt3.axes == cplt1.axes)
assert len(cplt3.lines[0]) == len(cplt1.lines[0])
Expand Down Expand Up @@ -255,6 +255,124 @@ def assert_legend(cplt, expected_texts):
assert_legend(cplt, expected_labels)


@pytest.mark.parametrize("resp_fcn, plot_fcn", resp_plot_fcns)
@pytest.mark.usefixtures('mplcleanup')
def test_plot_title_processing(resp_fcn, plot_fcn):
# Create some systems to use
sys1 = ct.rss(2, 1, 1, strictly_proper=True, name="sys[1]")
sys1c = ct.rss(4, 1, 1, strictly_proper=True, name="sys[1]_C")
sys2 = ct.rss(2, 1, 1, strictly_proper=True, name="sys[2]")

# Set up arguments
kwargs = meth_kwargs = plot_fcn_kwargs = {}
default_title = "sys[1], sys[2]"
expected_title = "sys1_, sys2_"
match resp_fcn, plot_fcn:
case ct.describing_function_response, _:
F = ct.descfcn.saturation_nonlinearity(1)
amp = np.linspace(1, 4, 10)
args1 = (sys1, F, amp)
args2 = (sys2, F, amp)

case ct.gangof4_response, _:
args1 = (sys1, sys1c)
args2 = (sys2, sys1c)
default_title = "P=sys[1], C=sys[1]_C, P=sys[2], C=sys[1]_C"

case ct.frequency_response, ct.nichols_plot:
args1 = (sys1, )
args2 = (sys2, )
meth_kwargs = {'plot_type': 'nichols'}

case ct.root_locus_map, ct.root_locus_plot:
args1 = (sys1, )
args2 = (sys2, )
meth_kwargs = plot_fcn_kwargs = {'interactive': False}

case (ct.forced_response | ct.input_output_response, _):
timepts = np.linspace(1, 10)
U = np.sin(timepts)
args1 = (resp_fcn(sys1, timepts, U), )
args2 = (resp_fcn(sys2, timepts, U), )
argsc = (resp_fcn([sys1, sys2], timepts, U), )

case (ct.impulse_response | ct.initial_response | ct.step_response, _):
args1 = (resp_fcn(sys1), )
args2 = (resp_fcn(sys2), )
argsc = (resp_fcn([sys1, sys2]), )

case _, _:
args1 = (sys1, )
args2 = (sys2, )

# Store the expected title prefix
match resp_fcn, plot_fcn:
case _, ct.bode_plot:
title_prefix = "Bode plot for "
case _, ct.nichols_plot:
title_prefix = "Nichols plot for "
case _, ct.singular_values_plot:
title_prefix = "Singular values for "
case _, ct.gangof4_plot:
title_prefix = "Gang of Four for "
case _, ct.describing_function_plot:
title_prefix = "Nyquist plot for "
case _, ct.phase_plane_plot:
title_prefix = "Phase portrait for "
case _, ct.pole_zero_plot:
title_prefix = "Pole/zero plot for "
case _, ct.nyquist_plot:
title_prefix = "Nyquist plot for "
case _, ct.root_locus_plot:
title_prefix = "Root locus plot for "
case ct.initial_response, _:
title_prefix = "Initial response for "
case ct.step_response, _:
title_prefix = "Step response for "
case ct.impulse_response, _:
title_prefix = "Impulse response for "
case ct.forced_response, _:
title_prefix = "Forced response for "
case ct.input_output_response, _:
title_prefix = "Input/output response for "
case _:
raise RuntimeError(f"didn't recognize {resp_fnc}, {plot_fnc}")

# Generate the first plot, with default labels
cplt1 = plot_fcn(*args1, **kwargs, **plot_fcn_kwargs)
assert cplt1.figure._suptitle._text.startswith(title_prefix)

# Skip functions not intended for sequential calling
if plot_fcn not in nolabel_plot_fcns:
# Generate second plot with default labels
cplt2 = plot_fcn(*args2, **kwargs, **plot_fcn_kwargs)
assert cplt1.figure._suptitle._text == title_prefix + default_title
plt.close()

# Generate both plots at the same time
if len(args1) == 1 and plot_fcn != ct.time_response_plot:
cplt = plot_fcn([*args1, *args2], **kwargs, **plot_fcn_kwargs)
assert cplt.figure._suptitle._text == title_prefix + default_title
elif len(args1) == 1 and plot_fcn == ct.time_response_plot:
# Use TimeResponseList.plot() to generate combined response
cplt = argsc[0].plot(**kwargs, **plot_fcn_kwargs)
assert cplt.figure._suptitle._text == title_prefix + default_title
plt.close()

# Generate plots sequentially, with updated titles
cplt1 = plot_fcn(
*args1, **kwargs, **plot_fcn_kwargs, title="My first title")
cplt2 = plot_fcn(
*args2, **kwargs, **plot_fcn_kwargs, title="My new title")
assert cplt2.figure._suptitle._text == "My new title"
plt.close()

# Update using set_plot_title
cplt2.set_plot_title("Another title")
assert cplt2.figure._suptitle._text == "Another title"
plt.close()


@pytest.mark.usefixtures('mplcleanup')
def test_rcParams():
sys = ct.rss(2, 2, 2)
Expand Down
18 changes: 12 additions & 6 deletions control/timeplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
import numpy as np

from . import config
from .ctrlplot import ControlPlot, _ctrlplot_rcParams, _make_legend_labels, \
_update_suptitle
from .ctrlplot import ControlPlot, suptitle, _ctrlplot_rcParams, \
_make_legend_labels, _update_suptitle

__all__ = ['time_response_plot', 'combine_time_responses']

Expand Down Expand Up @@ -139,6 +139,8 @@ def time_response_plot(
axis or ``legend_loc`` or ``legend_map`` have been specified.
time_label : str, optional
Label to use for the time axis.
title : str, optional
Set the title of the plot. Defaults to plot type and system name(s).
trace_labels : list of str, optional
Replace the default trace labels with the given labels.
trace_props : array of dicts
Expand Down Expand Up @@ -196,9 +198,6 @@ def time_response_plot(
'timeplot', 'trace_props', kwargs, _timeplot_defaults, pop=True)
tprop_len = len(trace_props)

# Set the title for the data
title = data.title if title == None else title

# Determine whether or not to plot the input data (and how)
if plot_inputs is None:
plot_inputs = data.plot_inputs
Expand Down Expand Up @@ -658,7 +657,11 @@ def _make_line_label(signal_index, signal_labels, trace_index):
# list of systems (e.g., "Step response for sys[1], sys[2]").
#

_update_suptitle(fig, title, rcParams=rcParams)
if title is None:
title = data.title if title == None else title
_update_suptitle(title, fig, rcParams=rcParams)
else:
suptitle(title, fig, rcParams=rcParams)

return ControlPlot(out, ax_array, fig, legend=legend_map)

Expand All @@ -676,6 +679,9 @@ def combine_time_responses(response_list, trace_labels=None, title=None):
trace_labels : list of str, optional
List of labels for each trace. If not specified, trace names are
taken from the input data or set to None.
title : str, optional
Set the title to use when plotting. Defaults to plot type and
system name(s).

Returns
-------
Expand Down
3 changes: 3 additions & 0 deletions control/timeresp.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,9 @@ class TimeResponseData:
response. If ntraces is 0 (default) then the data represents a
single trace with the trace index surpressed in the data.

title : str, optional
Set the title to use when plotting.

trace_labels : array of string, optional
Labels to use for traces (set to sysname it ntraces is 0)

Expand Down