diff --git a/control/ctrlplot.py b/control/ctrlplot.py index 7f6a37af4..bb57f19fd 100644 --- a/control/ctrlplot.py +++ b/control/ctrlplot.py @@ -334,7 +334,7 @@ def reset_rcParams(): def _process_ax_keyword( axs, shape=(1, 1), rcParams=None, squeeze=False, clear_text=False, - create_axes=True): + create_axes=True, sharex=False, sharey=False): """Process ax keyword to plotting commands. This function processes the `ax` keyword to plotting commands. If no @@ -364,10 +364,12 @@ def _process_ax_keyword( with plt.rc_context(rcParams): if len(axs) != 0 and create_axes: # Create a new figure - fig, axs = plt.subplots(*shape, squeeze=False) + fig, axs = plt.subplots( + *shape, sharex=sharex, sharey=sharey, squeeze=False) elif create_axes: # Create new axes on (empty) figure - axs = fig.subplots(*shape, squeeze=False) + axs = fig.subplots( + *shape, sharex=sharex, sharey=sharey, squeeze=False) else: # Create an empty array and let user create axes axs = np.full(shape, None) diff --git a/control/freqplot.py b/control/freqplot.py index d80ee5a8f..544425298 100644 --- a/control/freqplot.py +++ b/control/freqplot.py @@ -198,7 +198,12 @@ def bode_plot( Determine whether and how axis limits are shared between the indicated variables. Can be set set to 'row' to share across all subplots in a row, 'col' to set across all subplots in a column, or - `False` to allow independent limits. + `False` to allow independent limits. Note: if `sharex` is given, + it sets the value of `share_frequency`; if `sharey` is given, it + sets the value of both `share_magnitude` and `share_phase`. + Default values are 'row' for `share_magnitude` and `share_phase', + 'col', for `share_frequency`, and can be set using + config.defaults['freqplot.share_']. show_legend : bool, optional Force legend to be shown if ``True`` or hidden if ``False``. If ``None``, then show legend when there is more than one line on an @@ -228,12 +233,12 @@ def bode_plot( Notes ----- - 1. Starting with python-control version 0.10, `bode_plot`returns an - array of lines instead of magnitude, phase, and frequency. To - recover the old behavior, call `bode_plot` with `plot=True`, which - will force the legacy values (mag, phase, omega) to be returned - (with a warning). To obtain just the frequency response of a system - (or list of systems) without plotting, use the + 1. Starting with python-control version 0.10, `bode_plot` returns a + :class:`ControlPlot` object instead of magnitude, phase, and + frequency. To recover the old behavior, call `bode_plot` with + `plot=True`, which will force the legacy values (mag, phase, omega) + to be returned (with a warning). To obtain just the frequency + response of a system (or list of systems) without plotting, use the :func:`~control.frequency_response` command. 2. If a discrete time model is given, the frequency response is plotted @@ -583,7 +588,7 @@ def bode_plot( # axes are available and no updates should be made. # - # Utility function to turn off sharing + # Utility function to turn on sharing def _share_axes(ref, share_map, axis): ref_ax = ax_array[ref] for index in np.nditer(share_map, flags=["refs_ok"]): diff --git a/control/tests/ctrlplot_test.py b/control/tests/ctrlplot_test.py index 1baab8761..78ec7d895 100644 --- a/control/tests/ctrlplot_test.py +++ b/control/tests/ctrlplot_test.py @@ -2,6 +2,7 @@ # RMM, 27 Jun 2024 import inspect +import itertools import warnings import matplotlib.pyplot as plt @@ -577,6 +578,45 @@ def test_plot_title_processing(resp_fcn, plot_fcn): assert "title : str, optional" in plot_fcn.__doc__ +@pytest.mark.parametrize("plot_fcn", multiaxes_plot_fcns) +@pytest.mark.usefixtures('mplcleanup') +def test_tickmark_label_processing(plot_fcn): + # Generate the response that we will use for plotting + top_row, bot_row = 0, -1 + match plot_fcn: + case ct.bode_plot: + resp = ct.frequency_response(ct.rss(4, 2, 2)) + top_row = 1 + case ct.time_response_plot: + resp = ct.step_response(ct.rss(4, 2, 2)) + case ct.gangof4_plot: + resp = ct.gangof4_response(ct.rss(4, 1, 1), ct.rss(3, 1, 1)) + case _: + pytest.fail("unknown plot_fcn") + + # Turn off axis sharing => all axes have ticklabels + cplt = resp.plot(sharex=False, sharey=False) + for i, j in itertools.product( + range(cplt.axes.shape[0]), range(cplt.axes.shape[1])): + assert len(cplt.axes[i, j].get_xticklabels()) > 0 + assert len(cplt.axes[i, j].get_yticklabels()) > 0 + plt.clf() + + # Turn on axis sharing => only outer axes have ticklabels + cplt = resp.plot(sharex=True, sharey=True) + for i, j in itertools.product( + range(cplt.axes.shape[0]), range(cplt.axes.shape[1])): + if i < cplt.axes.shape[0] - 1: + assert len(cplt.axes[i, j].get_xticklabels()) == 0 + else: + assert len(cplt.axes[i, j].get_xticklabels()) > 0 + + if j > 0: + assert len(cplt.axes[i, j].get_yticklabels()) == 0 + else: + assert len(cplt.axes[i, j].get_yticklabels()) > 0 + + @pytest.mark.parametrize("resp_fcn, plot_fcn", resp_plot_fcns) @pytest.mark.usefixtures('mplcleanup', 'editsdefaults') def test_rcParams(resp_fcn, plot_fcn): diff --git a/control/timeplot.py b/control/timeplot.py index 9f389b4ab..1c7efe894 100644 --- a/control/timeplot.py +++ b/control/timeplot.py @@ -32,6 +32,8 @@ {'color': c} for c in [ 'tab:red', 'tab:purple', 'tab:brown', 'tab:olive', 'tab:cyan']], 'timeplot.time_label': "Time [s]", + 'timeplot.sharex': 'col', + 'timeplot.sharey': False, } @@ -66,6 +68,14 @@ def time_response_plot( overlay_signals : bool, optional If set to True, combine all input and output signals onto a single plot (for each). + sharex, sharey : str or bool, optional + Determine whether and how x- and y-axis limits are shared between + subplots. Can be set set to 'row' to share across all subplots in + a row, 'col' to set across all subplots in a column, 'all' to share + across all subplots, or `False` to allow independent limits. + Default values are `False` for `sharex' and 'col' for `sharey`, and + can be set using config.defaults['timeplot.sharex'] and + config.defaults['timeplot.sharey']. transpose : bool, optional If transpose is False (default), signals are plotted from top to bottom, starting with outputs (if plotted) and then inputs. @@ -176,6 +186,8 @@ def time_response_plot( # # Set up defaults ax_user = ax + sharex = config._get_param('timeplot', 'sharex', kwargs, pop=True) + sharey = config._get_param('timeplot', 'sharey', kwargs, pop=True) time_label = config._get_param( 'timeplot', 'time_label', kwargs, _timeplot_defaults, pop=True) rcParams = config._get_param('ctrlplot', 'rcParams', kwargs, pop=True) @@ -289,7 +301,8 @@ def time_response_plot( nrows, ncols = ncols, nrows # See if we can use the current figure axes - fig, ax_array = _process_ax_keyword(ax, (nrows, ncols), rcParams=rcParams) + fig, ax_array = _process_ax_keyword( + ax, (nrows, ncols), rcParams=rcParams, sharex=sharex, sharey=sharey) legend_loc, legend_map, show_legend = _process_legend_keywords( kwargs, (nrows, ncols), 'center right') diff --git a/doc/plotting.rst b/doc/plotting.rst index 167f5d001..a4611f1bd 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -536,6 +536,19 @@ various ways. The following general rules apply: The default values for style parameters for control plots can be restored using :func:`~control.reset_rcParams`. +* For multi-input, multi-output time and frequency domain plots, the + `sharex` and `sharey` keyword arguments can be used to determine whether + and how axis limits are shared between the individual subplots. Setting + the keyword to 'row' will share the axes limits across all subplots in a + row, 'col' will share across all subplots in a column, 'all' will share + across all subplots in the figure, and `False` will allow independent + limits for each subplot. + + For Bode plots, the `share_magnitude` and `share_phase` keyword arguments + can be used to independently control axis limit sharing for the magnitude + and phase portions of the plot, and `share_frequency` can be used instead + of `sharex`. + * 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 diff --git a/doc/timeplot-mimo_ioresp-mt_tr.png b/doc/timeplot-mimo_ioresp-mt_tr.png index 2371d50b6..7ddbe3c49 100644 Binary files a/doc/timeplot-mimo_ioresp-mt_tr.png and b/doc/timeplot-mimo_ioresp-mt_tr.png differ diff --git a/doc/timeplot-mimo_ioresp-ov_lm.png b/doc/timeplot-mimo_ioresp-ov_lm.png index d65056e0f..987a08f34 100644 Binary files a/doc/timeplot-mimo_ioresp-ov_lm.png and b/doc/timeplot-mimo_ioresp-ov_lm.png differ diff --git a/doc/timeplot-mimo_step-default.png b/doc/timeplot-mimo_step-default.png index 4d9a87456..5850dcb87 100644 Binary files a/doc/timeplot-mimo_step-default.png and b/doc/timeplot-mimo_step-default.png differ diff --git a/doc/timeplot-mimo_step-pi_cs.png b/doc/timeplot-mimo_step-pi_cs.png index bbeb6b189..44a630f55 100644 Binary files a/doc/timeplot-mimo_step-pi_cs.png and b/doc/timeplot-mimo_step-pi_cs.png differ pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy