• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

python-control / python-control / 10399836602

15 Aug 2024 06:31AM UTC coverage: 94.694% (+0.001%) from 94.693%
10399836602

push

github

web-flow
Merge pull request #1040 from murrayrm/tickmark_labels-08Aug2024

Update shared axes processing in plot_time_response

9138 of 9650 relevant lines covered (94.69%)

8.27 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.83
control/timeplot.py
1
# timeplot.py - time plotting functions
2
# RMM, 20 Jun 2023
3
#
4
# This file contains routines for plotting out time responses.  These
5
# functions can be called either as standalone functions or access from the
6
# TimeDataResponse class.
7
#
8
# Note: It might eventually make sense to put the functions here
9
# directly into timeresp.py.
10

11
import itertools
9✔
12
from warnings import warn
9✔
13

14
import matplotlib as mpl
9✔
15
import matplotlib.pyplot as plt
9✔
16
import numpy as np
9✔
17

18
from . import config
9✔
19
from .ctrlplot import ControlPlot, _make_legend_labels,\
9✔
20
    _process_legend_keywords, _update_plot_title
21

22
__all__ = ['time_response_plot', 'combine_time_responses']
9✔
23

24
# Default values for module parameter variables
25
_timeplot_defaults = {
9✔
26
    'timeplot.trace_props': [
27
        {'linestyle': s} for s in ['-', '--', ':', '-.']],
28
    'timeplot.output_props': [
29
        {'color': c} for c in [
30
            'tab:blue', 'tab:orange', 'tab:green', 'tab:pink', 'tab:gray']],
31
    'timeplot.input_props': [
32
        {'color': c} for c in [
33
            'tab:red', 'tab:purple', 'tab:brown', 'tab:olive', 'tab:cyan']],
34
    'timeplot.time_label': "Time [s]",
35
    'timeplot.sharex': 'col',
36
    'timeplot.sharey': False,
37
}
38

39

40
# Plot the input/output response of a system
41
def time_response_plot(
9✔
42
        data, *fmt, ax=None, plot_inputs=None, plot_outputs=True,
43
        transpose=False, overlay_traces=False, overlay_signals=False,
44
        add_initial_zero=True, label=None, trace_labels=None, title=None,
45
        relabel=True, **kwargs):
46
    """Plot the time response of an input/output system.
47

48
    This function creates a standard set of plots for the input/output
49
    response of a system, with the data provided via a `TimeResponseData`
50
    object, which is the standard output for python-control simulation
51
    functions.
52

53
    Parameters
54
    ----------
55
    data : TimeResponseData
56
        Data to be plotted.
57
    plot_inputs : bool or str, optional
58
        Sets how and where to plot the inputs:
59
            * False: don't plot the inputs
60
            * None: use value from time response data (default)
61
            * 'overlay`: plot inputs overlaid with outputs
62
            * True: plot the inputs on their own axes
63
    plot_outputs : bool, optional
64
        If False, suppress plotting of the outputs.
65
    overlay_traces : bool, optional
66
        If set to True, combine all traces onto a single row instead of
67
        plotting a separate row for each trace.
68
    overlay_signals : bool, optional
69
        If set to True, combine all input and output signals onto a single
70
        plot (for each).
71
    sharex, sharey : str or bool, optional
72
        Determine whether and how x- and y-axis limits are shared between
73
        subplots.  Can be set set to 'row' to share across all subplots in
74
        a row, 'col' to set across all subplots in a column, 'all' to share
75
        across all subplots, or `False` to allow independent limits.
76
        Default values are `False` for `sharex' and 'col' for `sharey`, and
77
        can be set using config.defaults['timeplot.sharex'] and
78
        config.defaults['timeplot.sharey'].
79
    transpose : bool, optional
80
        If transpose is False (default), signals are plotted from top to
81
        bottom, starting with outputs (if plotted) and then inputs.
82
        Multi-trace plots are stacked horizontally.  If transpose is True,
83
        signals are plotted from left to right, starting with the inputs
84
        (if plotted) and then the outputs.  Multi-trace responses are
85
        stacked vertically.
86
    *fmt : :func:`matplotlib.pyplot.plot` format string, optional
87
        Passed to `matplotlib` as the format string for all lines in the plot.
88
    **kwargs : :func:`matplotlib.pyplot.plot` keyword properties, optional
89
        Additional keywords passed to `matplotlib` to specify line properties.
90

91
    Returns
92
    -------
93
    cplt : :class:`ControlPlot` object
94
        Object containing the data that were plotted:
95

96
          * cplt.lines: Array of :class:`matplotlib.lines.Line2D` objects
97
            for each line in the plot.  The shape of the array matches the
98
            subplots shape and the value of the array is a list of Line2D
99
            objects in that subplot.
100

101
          * cplt.axes: 2D array of :class:`matplotlib.axes.Axes` for the plot.
102

103
          * cplt.figure: :class:`matplotlib.figure.Figure` containing the plot.
104

105
          * cplt.legend: legend object(s) contained in the plot
106

107
        See :class:`ControlPlot` for more detailed information.
108

109
    Other Parameters
110
    ----------------
111
    add_initial_zero : bool
112
        Add an initial point of zero at the first time point for all
113
        inputs with type 'step'.  Default is True.
114
    ax : array of matplotlib.axes.Axes, optional
115
        The matplotlib axes to draw the figure on.  If not specified, the
116
        axes for the current figure are used or, if there is no current
117
        figure with the correct number and shape of axes, a new figure is
118
        created.  The shape of the array must match the shape of the
119
        plotted data.
120
    input_props : array of dicts
121
        List of line properties to use when plotting combined inputs.  The
122
        default values are set by config.defaults['timeplot.input_props'].
123
    label : str or array_like of str, optional
124
        If present, replace automatically generated label(s) with the given
125
        label(s).  If more than one line is being generated, an array of
126
        labels should be provided with label[trace, :, 0] representing the
127
        output labels and label[trace, :, 1] representing the input labels.
128
    legend_map : array of str, optional
129
        Location of the legend for multi-axes plots.  Specifies an array
130
        of legend location strings matching the shape of the subplots, with
131
        each entry being either None (for no legend) or a legend location
132
        string (see :func:`~matplotlib.pyplot.legend`).
133
    legend_loc : int or str, optional
134
        Include a legend in the given location. Default is 'center right',
135
        with no legend for a single response.  Use False to suppress legend.
136
    output_props : array of dicts, optional
137
        List of line properties to use when plotting combined outputs.  The
138
        default values are set by config.defaults['timeplot.output_props'].
139
    rcParams : dict
140
        Override the default parameters used for generating plots.
141
        Default is set by config.default['ctrlplot.rcParams'].
142
    relabel : bool, optional
143
        (deprecated) By default, existing figures and axes are relabeled
144
        when new data are added.  If set to `False`, just plot new data on
145
        existing axes.
146
    show_legend : bool, optional
147
        Force legend to be shown if ``True`` or hidden if ``False``.  If
148
        ``None``, then show legend when there is more than one line on an
149
        axis or ``legend_loc`` or ``legend_map`` has been specified.
150
    time_label : str, optional
151
        Label to use for the time axis.
152
    title : str, optional
153
        Set the title of the plot.  Defaults to plot type and system name(s).
154
    trace_labels : list of str, optional
155
        Replace the default trace labels with the given labels.
156
    trace_props : array of dicts
157
        List of line properties to use when plotting combined outputs.  The
158
        default values are set by config.defaults['timeplot.trace_props'].
159

160
    Notes
161
    -----
162
    1. A new figure will be generated if there is no current figure or
163
       the current figure has an incompatible number of axes.  To
164
       force the creation of a new figures, use `plt.figure()`.  To reuse
165
       a portion of an existing figure, use the `ax` keyword.
166

167
    2. The line properties (color, linestyle, etc) can be set for the
168
       entire plot using the `fmt` and/or `kwargs` parameter, which
169
       are passed on to `matplotlib`.  When combining signals or
170
       traces, the `input_props`, `output_props`, and `trace_props`
171
       parameters can be used to pass a list of dictionaries
172
       containing the line properties to use.  These input/output
173
       properties are combined with the trace properties and finally
174
       the kwarg properties to determine the final line properties.
175

176
    3. The default plot properties, such as font sizes, can be set using
177
       config.defaults[''timeplot.rcParams'].
178

179
    """
180
    from .ctrlplot import _process_ax_keyword, _process_line_labels
9✔
181
    from .iosys import InputOutputSystem
9✔
182
    from .timeresp import TimeResponseData
9✔
183

184
    #
185
    # Process keywords and set defaults
186
    #
187
    # Set up defaults
188
    ax_user = ax
9✔
189
    sharex = config._get_param('timeplot', 'sharex', kwargs, pop=True)
9✔
190
    sharey = config._get_param('timeplot', 'sharey', kwargs, pop=True)
9✔
191
    time_label = config._get_param(
9✔
192
        'timeplot', 'time_label', kwargs, _timeplot_defaults, pop=True)
193
    rcParams = config._get_param('ctrlplot', 'rcParams', kwargs, pop=True)
9✔
194

195
    if kwargs.get('input_props', None) and len(fmt) > 0:
9✔
196
        warn("input_props ignored since fmt string was present")
9✔
197
    input_props = config._get_param(
9✔
198
        'timeplot', 'input_props', kwargs, _timeplot_defaults, pop=True)
199
    iprop_len = len(input_props)
9✔
200

201
    if kwargs.get('output_props', None) and len(fmt) > 0:
9✔
202
        warn("output_props ignored since fmt string was present")
9✔
203
    output_props = config._get_param(
9✔
204
        'timeplot', 'output_props', kwargs, _timeplot_defaults, pop=True)
205
    oprop_len = len(output_props)
9✔
206

207
    if kwargs.get('trace_props', None) and len(fmt) > 0:
9✔
208
        warn("trace_props ignored since fmt string was present")
9✔
209
    trace_props = config._get_param(
9✔
210
        'timeplot', 'trace_props', kwargs, _timeplot_defaults, pop=True)
211
    tprop_len = len(trace_props)
9✔
212

213
    # Determine whether or not to plot the input data (and how)
214
    if plot_inputs is None:
9✔
215
        plot_inputs = data.plot_inputs
9✔
216
    if plot_inputs not in [True, False, 'overlay']:
9✔
217
        raise ValueError(f"unrecognized value: {plot_inputs=}")
9✔
218

219
    #
220
    # Find/create axes
221
    #
222
    # Data are plotted in a standard subplots array, whose size depends on
223
    # which signals are being plotted and how they are combined.  The
224
    # baseline layout for data is to plot everything separately, with
225
    # outputs and inputs making up the rows and traces making up the
226
    # columns:
227
    #
228
    # Trace 0        Trace q
229
    # +------+       +------+
230
    # | y[0] |  ...  | y[0] |
231
    # +------+       +------+
232
    #    :
233
    # +------+       +------+
234
    # | y[p] |  ...  | y[p] |
235
    # +------+       +------+
236
    #
237
    # +------+       +------+
238
    # | u[0] |  ...  | u[0] |
239
    # +------+       +------+
240
    #    :
241
    # +------+       +------+
242
    # | u[m] |  ...  | u[m] |
243
    # +------+       +------+
244
    #
245
    # A variety of options are available to modify this format:
246
    #
247
    # * Omitting: either the inputs or the outputs can be omitted.
248
    #
249
    # * Overlay: inputs, outputs, and traces can be combined onto a
250
    #   single set of axes using various keyword combinations
251
    #   (overlay_signals, overlay_traces, plot_inputs='overlay').  This
252
    #   basically collapses data along either the rows or columns, and a
253
    #   legend is generated.
254
    #
255
    # * Transpose: if the `transpose` keyword is True, then instead of
256
    #   plotting the data vertically (outputs over inputs), we plot left to
257
    #   right (inputs, outputs):
258
    #
259
    #         +------+       +------+     +------+       +------+
260
    # Trace 0 | u[0] |  ...  | u[m] |     | y[0] |  ...  | y[p] |
261
    #         +------+       +------+     +------+       +------+
262
    #    :
263
    #    :
264
    #         +------+       +------+     +------+       +------+
265
    # Trace q | u[0] |  ...  | u[m] |     | y[0] |  ...  | y[p] |
266
    #         +------+       +------+     +------+       +------+
267
    #
268
    # This also affects the way in which legends and labels are generated.
269

270
    # Decide on the number of inputs and outputs
271
    ninputs = data.ninputs if plot_inputs else 0
9✔
272
    noutputs = data.noutputs if plot_outputs else 0
9✔
273
    ntraces = max(1, data.ntraces)      # treat data.ntraces == 0 as 1 trace
9✔
274
    if ninputs == 0 and noutputs == 0:
9✔
275
        raise ValueError(
9✔
276
            "plot_inputs and plot_outputs both False; no data to plot")
277
    elif plot_inputs == 'overlay' and noutputs == 0:
9✔
278
        raise ValueError(
9✔
279
            "can't overlay inputs with no outputs")
280
    elif plot_inputs in [True, 'overlay'] and data.ninputs == 0:
9✔
281
        raise ValueError(
9✔
282
            "input plotting requested but no inputs in time response data")
283

284
    # Figure how how many rows and columns to use + offsets for inputs/outputs
285
    if plot_inputs == 'overlay' and not overlay_signals:
9✔
286
        nrows = max(ninputs, noutputs)          # Plot inputs on top of outputs
9✔
287
        noutput_axes = 0                        # No offset required
9✔
288
        ninput_axes = 0                         # No offset required
9✔
289
    elif overlay_signals:
9✔
290
        nrows = int(plot_outputs)               # Start with outputs
9✔
291
        nrows += int(plot_inputs == True)       # Add plot for inputs if needed
9✔
292
        noutput_axes = 1 if plot_outputs and plot_inputs is True else 0
9✔
293
        ninput_axes = 1 if plot_inputs is True else 0
9✔
294
    else:
295
        nrows = noutputs + ninputs              # Plot inputs separately
9✔
296
        noutput_axes = noutputs if plot_outputs else 0
9✔
297
        ninput_axes = ninputs if plot_inputs else 0
9✔
298

299
    ncols = ntraces if not overlay_traces else 1
9✔
300
    if transpose:
9✔
301
        nrows, ncols = ncols, nrows
9✔
302

303
    # See if we can use the current figure axes
304
    fig, ax_array = _process_ax_keyword(
9✔
305
        ax, (nrows, ncols), rcParams=rcParams, sharex=sharex, sharey=sharey)
306
    legend_loc, legend_map, show_legend = _process_legend_keywords(
9✔
307
        kwargs, (nrows, ncols), 'center right')
308

309
    #
310
    # Map inputs/outputs and traces to axes
311
    #
312
    # This set of code takes care of all of the various options for how to
313
    # plot the data.  The arrays output_map and input_map are used to map
314
    # the different signals that are plotted onto the axes created above.
315
    # This code is complicated because it has to handle lots of different
316
    # variations.
317
    #
318

319
    # Create the map from trace, signal to axes, accounting for overlay_*
320
    output_map = np.empty((noutputs, ntraces), dtype=tuple)
9✔
321
    input_map = np.empty((ninputs, ntraces), dtype=tuple)
9✔
322

323
    for i in range(noutputs):
9✔
324
        for j in range(ntraces):
9✔
325
            signal_index = i if not overlay_signals else 0
9✔
326
            trace_index = j if not overlay_traces else 0
9✔
327
            if transpose:
9✔
328
                output_map[i, j] = (trace_index, signal_index + ninput_axes)
9✔
329
            else:
330
                output_map[i, j] = (signal_index, trace_index)
9✔
331

332
    for i in range(ninputs):
9✔
333
        for j in range(ntraces):
9✔
334
            signal_index = noutput_axes + (i if not overlay_signals else 0)
9✔
335
            trace_index = j if not overlay_traces else 0
9✔
336
            if transpose:
9✔
337
                input_map[i, j] = (trace_index, signal_index - noutput_axes)
9✔
338
            else:
339
                input_map[i, j] = (signal_index, trace_index)
9✔
340

341
    #
342
    # Plot the data
343
    #
344
    # The ax_output and ax_input arrays have the axes needed for making the
345
    # plots.  Labels are used on each axes for later creation of legends.
346
    # The generic labels if of the form:
347
    #
348
    #     signal name, trace label, system name
349
    #
350
    # The signal name or trace label can be omitted if they will appear on
351
    # the axes title or ylabel.  The system name is always included, since
352
    # multiple calls to plot() will require a legend that distinguishes
353
    # which system signals are plotted.  The system name is stripped off
354
    # later (in the legend-handling code) if it is not needed, but must be
355
    # included here since a plot may be built up by multiple calls to plot().
356
    #
357

358
    # Reshape the inputs and outputs for uniform indexing
359
    outputs = data.y.reshape(data.noutputs, ntraces, -1)
9✔
360
    if data.u is None or not plot_inputs:
9✔
361
        inputs = None
9✔
362
    else:
363
        inputs = data.u.reshape(data.ninputs, ntraces, -1)
9✔
364

365
    # Create a list of lines for the output
366
    out = np.empty((nrows, ncols), dtype=object)
9✔
367
    for i in range(nrows):
9✔
368
        for j in range(ncols):
9✔
369
            out[i, j] = []      # unique list in each element
9✔
370

371
    # Utility function for creating line label
372
    # TODO: combine with freqplot version?
373
    def _make_line_label(signal_index, signal_labels, trace_index):
9✔
374
        label = ""              # start with an empty label
9✔
375

376
        # Add the signal name if it won't appear as an axes label
377
        if overlay_signals or plot_inputs == 'overlay':
9✔
378
            label += signal_labels[signal_index]
9✔
379

380
        # Add the trace label if this is a multi-trace figure
381
        if overlay_traces and ntraces > 1 or trace_labels:
9✔
382
            label += ", " if label != "" else ""
9✔
383
            if trace_labels:
9✔
384
                label += trace_labels[trace_index]
×
385
            elif data.trace_labels:
9✔
386
                label += data.trace_labels[trace_index]
9✔
387
            else:
388
                label += f"trace {trace_index}"
×
389

390
        # Add the system name (will strip off later if redundant)
391
        label += ", " if label != "" else ""
9✔
392
        label += f"{data.sysname}"
9✔
393

394
        return label
9✔
395

396
    #
397
    # Store the color offsets with the figure to allow color/style cycling
398
    #
399
    # To allow repeated calls to time_response_plot() to cycle through
400
    # colors, we store an offset in the figure object that we can
401
    # retrieve in a later call, if needed.
402
    #
403
    output_offset = fig._output_offset = getattr(fig, '_output_offset', 0)
9✔
404
    input_offset = fig._input_offset = getattr(fig, '_input_offset', 0)
9✔
405

406
    #
407
    # Plot the lines for the response
408
    #
409

410
    # Process labels
411
    line_labels = _process_line_labels(
9✔
412
        label, ntraces, max(ninputs, noutputs), 2)
413

414
    # Go through each trace and each input/output
415
    for trace in range(ntraces):
9✔
416
        # Plot the output
417
        for i in range(noutputs):
9✔
418
            if line_labels is None:
9✔
419
                label = _make_line_label(i, data.output_labels, trace)
9✔
420
            else:
421
                label = line_labels[trace, i, 0]
9✔
422

423
            # Set up line properties for this output, trace
424
            if len(fmt) == 0:
9✔
425
                line_props = output_props[
9✔
426
                    (i + output_offset) % oprop_len if overlay_signals
427
                    else output_offset].copy()
428
                line_props.update(
9✔
429
                    trace_props[trace % tprop_len if overlay_traces else 0])
430
                line_props.update(kwargs)
9✔
431
            else:
432
                line_props = kwargs
9✔
433

434
            out[output_map[i, trace]] += ax_array[output_map[i, trace]].plot(
9✔
435
                data.time, outputs[i][trace], *fmt, label=label, **line_props)
436

437
        # Plot the input
438
        for i in range(ninputs):
9✔
439
            if line_labels is None:
9✔
440
                label = _make_line_label(i, data.input_labels, trace)
9✔
441
            else:
442
                label = line_labels[trace, i, 1]
9✔
443

444
            if add_initial_zero and data.ntraces > i \
9✔
445
               and data.trace_types[i] == 'step':
446
                x = np.hstack([np.array([data.time[0]]), data.time])
9✔
447
                y = np.hstack([np.array([0]), inputs[i][trace]])
9✔
448
            else:
449
                x, y = data.time, inputs[i][trace]
9✔
450

451
            # Set up line properties for this output, trace
452
            if len(fmt) == 0:
9✔
453
                line_props = input_props[
9✔
454
                    (i + input_offset) % iprop_len if overlay_signals
455
                    else input_offset].copy()
456
                line_props.update(
9✔
457
                    trace_props[trace % tprop_len if overlay_traces else 0])
458
                line_props.update(kwargs)
9✔
459
            else:
460
                line_props = kwargs
9✔
461

462
            out[input_map[i, trace]] += ax_array[input_map[i, trace]].plot(
9✔
463
                x, y, *fmt, label=label, **line_props)
464

465
    # Update the offsets so that we start at a new color/style the next time
466
    fig._output_offset = (
9✔
467
        output_offset + (noutputs if overlay_signals else 1)) % oprop_len
468
    fig._input_offset = (
9✔
469
        input_offset + (ninputs if overlay_signals else 1)) % iprop_len
470

471
    # Stop here if the user wants to control everything
472
    if not relabel:
9✔
473
        warn("relabel keyword is deprecated", FutureWarning)
9✔
474
        return ControlPlot(out, ax_array, fig)
9✔
475

476
    #
477
    # Label the axes (including trace labels)
478
    #
479
    # Once the data are plotted, we label the axes.  The horizontal axes is
480
    # always time and this is labeled only on the bottom most row.  The
481
    # vertical axes can consist either of a single signal or a combination
482
    # of signals (when overlay_signal is True or plot+inputs = 'overlay'.
483
    #
484
    # Traces are labeled at the top of the first row of plots (regular) or
485
    # the left edge of rows (tranpose).
486
    #
487

488
    # Time units on the bottom
489
    for col in range(ncols):
9✔
490
        ax_array[-1, col].set_xlabel(time_label)
9✔
491

492
    # Keep track of whether inputs are overlaid on outputs
493
    overlaid = plot_inputs == 'overlay'
9✔
494
    overlaid_title = "Inputs, Outputs"
9✔
495

496
    if transpose:               # inputs on left, outputs on right
9✔
497
        # Label the inputs
498
        if overlay_signals and plot_inputs:
9✔
499
            label = overlaid_title if overlaid else "Inputs"
9✔
500
            for trace in range(ntraces):
9✔
501
                ax_array[input_map[0, trace]].set_ylabel(label)
9✔
502
        else:
503
            for i in range(ninputs):
9✔
504
                label = overlaid_title if overlaid else data.input_labels[i]
9✔
505
                for trace in range(ntraces):
9✔
506
                    ax_array[input_map[i, trace]].set_ylabel(label)
9✔
507

508
        # Label the outputs
509
        if overlay_signals and plot_outputs:
9✔
510
            label = overlaid_title if overlaid else "Outputs"
9✔
511
            for trace in range(ntraces):
9✔
512
                ax_array[output_map[0, trace]].set_ylabel(label)
9✔
513
        else:
514
            for i in range(noutputs):
9✔
515
                label = overlaid_title if overlaid else data.output_labels[i]
9✔
516
                for trace in range(ntraces):
9✔
517
                    ax_array[output_map[i, trace]].set_ylabel(label)
9✔
518

519
        # Set the trace titles, if needed
520
        if ntraces > 1 and not overlay_traces:
9✔
521
            for trace in range(ntraces):
5✔
522
                # Get the existing ylabel for left column
523
                label = ax_array[trace, 0].get_ylabel()
5✔
524

525
                # Add on the trace title
526
                if trace_labels:
5✔
527
                    label = trace_labels[trace] + "\n" + label
×
528
                elif data.trace_labels:
5✔
529
                    label = data.trace_labels[trace] + "\n" + label
5✔
530
                else:
531
                    label = f"Trace {trace}" + "\n" + label
×
532

533
                ax_array[trace, 0].set_ylabel(label)
5✔
534

535
    else:                       # regular plot (outputs over inputs)
536
        # Set the trace titles, if needed
537
        if ntraces > 1 and not overlay_traces:
9✔
538
            for trace in range(ntraces):
9✔
539
                if trace_labels:
9✔
540
                    label = trace_labels[trace]
×
541
                elif data.trace_labels:
9✔
542
                    label = data.trace_labels[trace]
9✔
543
                else:
544
                    label = f"Trace {trace}"
×
545

546
                with plt.rc_context(rcParams):
9✔
547
                    ax_array[0, trace].set_title(label)
9✔
548

549
        # Label the outputs
550
        if overlay_signals and plot_outputs:
9✔
551
            ax_array[output_map[0, 0]].set_ylabel("Outputs")
9✔
552
        else:
553
            for i in range(noutputs):
9✔
554
                ax_array[output_map[i, 0]].set_ylabel(
9✔
555
                    overlaid_title if overlaid else data.output_labels[i])
556

557
        # Label the inputs
558
        if overlay_signals and plot_inputs:
9✔
559
            label = overlaid_title if overlaid else "Inputs"
9✔
560
            ax_array[input_map[0, 0]].set_ylabel(label)
9✔
561
        else:
562
            for i in range(ninputs):
9✔
563
                label = overlaid_title if overlaid else data.input_labels[i]
9✔
564
                ax_array[input_map[i, 0]].set_ylabel(label)
9✔
565

566
    #
567
    # Create legends
568
    #
569
    # Legends can be placed manually by passing a legend_map array that
570
    # matches the shape of the suplots, with each item being a string
571
    # indicating the location of the legend for that axes (or None for no
572
    # legend).
573
    #
574
    # If no legend spec is passed, a minimal number of legends are used so
575
    # that each line in each axis can be uniquely identified.  The details
576
    # depends on the various plotting parameters, but the general rule is
577
    # to place legends in the top row and right column.
578
    #
579
    # Because plots can be built up by multiple calls to plot(), the legend
580
    # strings are created from the line labels manually.  Thus an initial
581
    # call to plot() may not generate any legends (eg, if no signals are
582
    # combined nor overlaid), but subsequent calls to plot() will need a
583
    # legend for each different line (system).
584
    #
585

586
    # Figure out where to put legends
587
    if show_legend != False and legend_map is None:
9✔
588
        legend_map = np.full(ax_array.shape, None, dtype=object)
9✔
589

590
        if transpose:
9✔
591
            if (overlay_signals or plot_inputs == 'overlay') and overlay_traces:
9✔
592
                # Put a legend in each plot for inputs and outputs
593
                if plot_outputs is True:
9✔
594
                    legend_map[0, ninput_axes] = legend_loc
9✔
595
                if plot_inputs is True:
9✔
596
                    legend_map[0, 0] = legend_loc
9✔
597
            elif overlay_signals:
9✔
598
                # Put a legend in rightmost input/output plot
599
                if plot_inputs is True:
9✔
600
                    legend_map[0, 0] = legend_loc
9✔
601
                if plot_outputs is True:
9✔
602
                    legend_map[0, ninput_axes] = legend_loc
9✔
603
            elif plot_inputs == 'overlay':
9✔
604
                # Put a legend on the top of each column
605
                for i in range(ntraces):
9✔
606
                    legend_map[0, i] = legend_loc
9✔
607
            elif overlay_traces:
9✔
608
                # Put a legend topmost input/output plot
609
                legend_map[0, -1] = legend_loc
9✔
610
            else:
611
                # Put legend in the upper right
612
                legend_map[0, -1] = legend_loc
×
613

614
        else:                   # regular layout
615
            if (overlay_signals or plot_inputs == 'overlay') and overlay_traces:
9✔
616
                # Put a legend in each plot for inputs and outputs
617
                if plot_outputs is True:
9✔
618
                    legend_map[0, -1] = legend_loc
9✔
619
                if plot_inputs is True:
9✔
620
                    legend_map[noutput_axes, -1] = legend_loc
9✔
621
            elif overlay_signals:
9✔
622
                # Put a legend in rightmost input/output plot
623
                if plot_outputs is True:
9✔
624
                    legend_map[0, -1] = legend_loc
9✔
625
                if plot_inputs is True:
9✔
626
                    legend_map[noutput_axes, -1] = legend_loc
9✔
627
            elif plot_inputs == 'overlay':
9✔
628
                # Put a legend on the right of each row
629
                for i in range(max(ninputs, noutputs)):
9✔
630
                    legend_map[i, -1] = legend_loc
9✔
631
            elif overlay_traces:
9✔
632
                # Put a legend topmost input/output plot
633
                legend_map[0, -1] = legend_loc
9✔
634
            else:
635
                # Put legend in the upper right
636
                legend_map[0, -1] = legend_loc
9✔
637

638
    if show_legend != False:
9✔
639
        # Create axis legends
640
        legend_array = np.full(ax_array.shape, None, dtype=object)
9✔
641
        for i, j in itertools.product(range(nrows), range(ncols)):
9✔
642
            if legend_map[i, j] is None:
9✔
643
                continue
9✔
644
            ax = ax_array[i, j]
9✔
645
            labels = [line.get_label() for line in ax.get_lines()]
9✔
646
            if line_labels is None:
9✔
647
                labels = _make_legend_labels(labels, plot_inputs == 'overlay')
9✔
648

649
            # Update the labels to remove common strings
650
            if show_legend == True or len(labels) > 1:
9✔
651
                with plt.rc_context(rcParams):
9✔
652
                    legend_array[i, j] = ax.legend(
9✔
653
                        labels, loc=legend_map[i, j])
654
    else:
655
        legend_array = None
9✔
656

657
    #
658
    # Update the plot title (= figure suptitle)
659
    #
660
    # If plots are built up by multiple calls to plot() and the title is
661
    # not given, then the title is updated to provide a list of unique text
662
    # items in each successive title.  For data generated by the I/O
663
    # response functions this will generate a common prefix followed by a
664
    # list of systems (e.g., "Step response for sys[1], sys[2]").
665
    #
666

667
    if ax_user is None and title is None:
9✔
668
        title = data.title if title == None else title
9✔
669
        _update_plot_title(title, fig, rcParams=rcParams)
9✔
670
    elif ax_user is None:
9✔
671
        _update_plot_title(title, fig, rcParams=rcParams, use_existing=False)
9✔
672

673
    return ControlPlot(out, ax_array, fig, legend=legend_map)
9✔
674

675

676
def combine_time_responses(response_list, trace_labels=None, title=None):
9✔
677
    """Combine multiple individual time responses into a multi-trace response.
678

679
    This function combines multiple instances of :class:`TimeResponseData`
680
    into a multi-trace :class:`TimeResponseData` object.
681

682
    Parameters
683
    ----------
684
    response_list : list of :class:`TimeResponseData` objects
685
        Reponses to be combined.
686
    trace_labels : list of str, optional
687
        List of labels for each trace.  If not specified, trace names are
688
        taken from the input data or set to None.
689
    title : str, optional
690
        Set the title to use when plotting.  Defaults to plot type and
691
        system name(s).
692

693
    Returns
694
    -------
695
    data : :class:`TimeResponseData`
696
        Multi-trace input/output data.
697

698
    """
699
    from .timeresp import TimeResponseData
9✔
700

701
    # Save the first trace as the base case
702
    base = response_list[0]
9✔
703

704
    # Process keywords
705
    title = base.title if title is None else title
9✔
706

707
    # Figure out the size of the data (and check for consistency)
708
    ntraces = max(1, base.ntraces)
9✔
709

710
    # Initial pass through trace list to count things up and do error checks
711
    nstates = base.nstates
9✔
712
    for response in response_list[1:]:
9✔
713
        # Make sure the time vector is the same
714
        if not np.allclose(base.t, response.t):
9✔
715
            raise ValueError("all responses must have the same time vector")
9✔
716

717
        # Make sure the dimensions are all the same
718
        if base.ninputs != response.ninputs or \
9✔
719
           base.noutputs != response.noutputs:
720
            raise ValueError("all responses must have the same number of "
9✔
721
                            "inputs, outputs, and states")
722

723
        if nstates != response.nstates:
9✔
724
            warn("responses have different state dimensions; dropping states")
×
725
            nstates = 0
×
726

727
        ntraces += max(1, response.ntraces)
9✔
728

729
    # Create data structures for the new time response data object
730
    inputs = np.empty((base.ninputs, ntraces, base.t.size))
9✔
731
    outputs = np.empty((base.noutputs, ntraces, base.t.size))
9✔
732
    states = np.empty((nstates, ntraces, base.t.size))
9✔
733

734
    # See whether we should create labels or not
735
    if trace_labels is None:
9✔
736
        generate_trace_labels = True
9✔
737
        trace_labels = []
9✔
738
    elif len(trace_labels) != ntraces:
9✔
739
        raise ValueError(
9✔
740
            "number of trace labels does not match number of traces")
741
    else:
742
        generate_trace_labels = False
9✔
743

744
    offset = 0
9✔
745
    trace_types = []
9✔
746
    for response in response_list:
9✔
747
        if response.ntraces == 0:
9✔
748
            # Single trace
749
            inputs[:, offset, :] = response.u
9✔
750
            outputs[:, offset, :] = response.y
9✔
751
            if nstates:
9✔
752
                states[:, offset, :] = response.x
9✔
753
            offset += 1
9✔
754

755
            # Add on trace label and trace type
756
            if generate_trace_labels:
9✔
757
                trace_labels.append(response.title)
9✔
758
            trace_types.append(
9✔
759
                None if response.trace_types is None
760
                else response.trace_types[0])
761

762
        else:
763
            # Save the data
764
            for i in range(response.ntraces):
9✔
765
                inputs[:, offset, :] = response.u[:, i, :]
9✔
766
                outputs[:, offset, :] = response.y[:, i, :]
9✔
767
                if nstates:
9✔
768
                    states[:, offset, :] = response.x[:, i, :]
9✔
769

770
                # Save the trace labels
771
                if generate_trace_labels:
9✔
772
                    if response.trace_labels is not None:
9✔
773
                        trace_labels.append(response.trace_labels[i])
9✔
774
                    else:
775
                        trace_labels.append(response.title + f", trace {i}")
9✔
776

777
                offset += 1
9✔
778

779
            # Save the trace types
780
            if response.trace_types is not None:
9✔
781
                trace_types += response.trace_types
9✔
782
            else:
783
                trace_types += [None] * response.ntraces
9✔
784

785
    return TimeResponseData(
9✔
786
        base.t, outputs, states if nstates else None, inputs,
787
        output_labels=base.output_labels, input_labels=base.input_labels,
788
        state_labels=base.state_labels if nstates else None,
789
        title=title, transpose=base.transpose, return_x=base.return_x,
790
        issiso=base.issiso, squeeze=base.squeeze, sysname=base.sysname,
791
        trace_labels=trace_labels, trace_types=trace_types,
792
        plot_inputs=base.plot_inputs)
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc