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

python-control / python-control / 10370763703

13 Aug 2024 01:32PM UTC coverage: 94.693% (-0.001%) from 94.694%
10370763703

push

github

web-flow
Merge pull request #1038 from murrayrm/doc-comment_fixes-11May2024

Documentation updates and docstring unit tests

9136 of 9648 relevant lines covered (94.69%)

8.27 hits per line

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

96.81
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
}
36

37

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

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

51
    Parameters
52
    ----------
53
    data : TimeResponseData
54
        Data to be plotted.
55
    plot_inputs : bool or str, optional
56
        Sets how and where to plot the inputs:
57
            * False: don't plot the inputs
58
            * None: use value from time response data (default)
59
            * 'overlay`: plot inputs overlaid with outputs
60
            * True: plot the inputs on their own axes
61
    plot_outputs : bool, optional
62
        If False, suppress plotting of the outputs.
63
    overlay_traces : bool, optional
64
        If set to True, combine all traces onto a single row instead of
65
        plotting a separate row for each trace.
66
    overlay_signals : bool, optional
67
        If set to True, combine all input and output signals onto a single
68
        plot (for each).
69
    transpose : bool, optional
70
        If transpose is False (default), signals are plotted from top to
71
        bottom, starting with outputs (if plotted) and then inputs.
72
        Multi-trace plots are stacked horizontally.  If transpose is True,
73
        signals are plotted from left to right, starting with the inputs
74
        (if plotted) and then the outputs.  Multi-trace responses are
75
        stacked vertically.
76
    *fmt : :func:`matplotlib.pyplot.plot` format string, optional
77
        Passed to `matplotlib` as the format string for all lines in the plot.
78
    **kwargs : :func:`matplotlib.pyplot.plot` keyword properties, optional
79
        Additional keywords passed to `matplotlib` to specify line properties.
80

81
    Returns
82
    -------
83
    cplt : :class:`ControlPlot` object
84
        Object containing the data that were plotted:
85

86
          * cplt.lines: Array of :class:`matplotlib.lines.Line2D` objects
87
            for each line in the plot.  The shape of the array matches the
88
            subplots shape and the value of the array is a list of Line2D
89
            objects in that subplot.
90

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

93
          * cplt.figure: :class:`matplotlib.figure.Figure` containing the plot.
94

95
          * cplt.legend: legend object(s) contained in the plot
96

97
        See :class:`ControlPlot` for more detailed information.
98

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

150
    Notes
151
    -----
152
    1. A new figure will be generated if there is no current figure or
153
       the current figure has an incompatible number of axes.  To
154
       force the creation of a new figures, use `plt.figure()`.  To reuse
155
       a portion of an existing figure, use the `ax` keyword.
156

157
    2. The line properties (color, linestyle, etc) can be set for the
158
       entire plot using the `fmt` and/or `kwargs` parameter, which
159
       are passed on to `matplotlib`.  When combining signals or
160
       traces, the `input_props`, `output_props`, and `trace_props`
161
       parameters can be used to pass a list of dictionaries
162
       containing the line properties to use.  These input/output
163
       properties are combined with the trace properties and finally
164
       the kwarg properties to determine the final line properties.
165

166
    3. The default plot properties, such as font sizes, can be set using
167
       config.defaults[''timeplot.rcParams'].
168

169
    """
170
    from .ctrlplot import _process_ax_keyword, _process_line_labels
9✔
171
    from .iosys import InputOutputSystem
9✔
172
    from .timeresp import TimeResponseData
9✔
173

174
    #
175
    # Process keywords and set defaults
176
    #
177
    # Set up defaults
178
    ax_user = ax
9✔
179
    time_label = config._get_param(
9✔
180
        'timeplot', 'time_label', kwargs, _timeplot_defaults, pop=True)
181
    rcParams = config._get_param('ctrlplot', 'rcParams', kwargs, pop=True)
9✔
182

183
    if kwargs.get('input_props', None) and len(fmt) > 0:
9✔
184
        warn("input_props ignored since fmt string was present")
9✔
185
    input_props = config._get_param(
9✔
186
        'timeplot', 'input_props', kwargs, _timeplot_defaults, pop=True)
187
    iprop_len = len(input_props)
9✔
188

189
    if kwargs.get('output_props', None) and len(fmt) > 0:
9✔
190
        warn("output_props ignored since fmt string was present")
9✔
191
    output_props = config._get_param(
9✔
192
        'timeplot', 'output_props', kwargs, _timeplot_defaults, pop=True)
193
    oprop_len = len(output_props)
9✔
194

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

201
    # Determine whether or not to plot the input data (and how)
202
    if plot_inputs is None:
9✔
203
        plot_inputs = data.plot_inputs
9✔
204
    if plot_inputs not in [True, False, 'overlay']:
9✔
205
        raise ValueError(f"unrecognized value: {plot_inputs=}")
9✔
206

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

258
    # Decide on the number of inputs and outputs
259
    ninputs = data.ninputs if plot_inputs else 0
9✔
260
    noutputs = data.noutputs if plot_outputs else 0
9✔
261
    ntraces = max(1, data.ntraces)      # treat data.ntraces == 0 as 1 trace
9✔
262
    if ninputs == 0 and noutputs == 0:
9✔
263
        raise ValueError(
9✔
264
            "plot_inputs and plot_outputs both False; no data to plot")
265
    elif plot_inputs == 'overlay' and noutputs == 0:
9✔
266
        raise ValueError(
9✔
267
            "can't overlay inputs with no outputs")
268
    elif plot_inputs in [True, 'overlay'] and data.ninputs == 0:
9✔
269
        raise ValueError(
9✔
270
            "input plotting requested but no inputs in time response data")
271

272
    # Figure how how many rows and columns to use + offsets for inputs/outputs
273
    if plot_inputs == 'overlay' and not overlay_signals:
9✔
274
        nrows = max(ninputs, noutputs)          # Plot inputs on top of outputs
9✔
275
        noutput_axes = 0                        # No offset required
9✔
276
        ninput_axes = 0                         # No offset required
9✔
277
    elif overlay_signals:
9✔
278
        nrows = int(plot_outputs)               # Start with outputs
9✔
279
        nrows += int(plot_inputs == True)       # Add plot for inputs if needed
9✔
280
        noutput_axes = 1 if plot_outputs and plot_inputs is True else 0
9✔
281
        ninput_axes = 1 if plot_inputs is True else 0
9✔
282
    else:
283
        nrows = noutputs + ninputs              # Plot inputs separately
9✔
284
        noutput_axes = noutputs if plot_outputs else 0
9✔
285
        ninput_axes = ninputs if plot_inputs else 0
9✔
286

287
    ncols = ntraces if not overlay_traces else 1
9✔
288
    if transpose:
9✔
289
        nrows, ncols = ncols, nrows
9✔
290

291
    # See if we can use the current figure axes
292
    fig, ax_array = _process_ax_keyword(ax, (nrows, ncols), rcParams=rcParams)
9✔
293
    legend_loc, legend_map, show_legend = _process_legend_keywords(
9✔
294
        kwargs, (nrows, ncols), 'center right')
295

296
    #
297
    # Map inputs/outputs and traces to axes
298
    #
299
    # This set of code takes care of all of the various options for how to
300
    # plot the data.  The arrays output_map and input_map are used to map
301
    # the different signals that are plotted onto the axes created above.
302
    # This code is complicated because it has to handle lots of different
303
    # variations.
304
    #
305

306
    # Create the map from trace, signal to axes, accounting for overlay_*
307
    output_map = np.empty((noutputs, ntraces), dtype=tuple)
9✔
308
    input_map = np.empty((ninputs, ntraces), dtype=tuple)
9✔
309

310
    for i in range(noutputs):
9✔
311
        for j in range(ntraces):
9✔
312
            signal_index = i if not overlay_signals else 0
9✔
313
            trace_index = j if not overlay_traces else 0
9✔
314
            if transpose:
9✔
315
                output_map[i, j] = (trace_index, signal_index + ninput_axes)
9✔
316
            else:
317
                output_map[i, j] = (signal_index, trace_index)
9✔
318

319
    for i in range(ninputs):
9✔
320
        for j in range(ntraces):
9✔
321
            signal_index = noutput_axes + (i if not overlay_signals else 0)
9✔
322
            trace_index = j if not overlay_traces else 0
9✔
323
            if transpose:
9✔
324
                input_map[i, j] = (trace_index, signal_index - noutput_axes)
9✔
325
            else:
326
                input_map[i, j] = (signal_index, trace_index)
9✔
327

328
    #
329
    # Plot the data
330
    #
331
    # The ax_output and ax_input arrays have the axes needed for making the
332
    # plots.  Labels are used on each axes for later creation of legends.
333
    # The generic labels if of the form:
334
    #
335
    #     signal name, trace label, system name
336
    #
337
    # The signal name or trace label can be omitted if they will appear on
338
    # the axes title or ylabel.  The system name is always included, since
339
    # multiple calls to plot() will require a legend that distinguishes
340
    # which system signals are plotted.  The system name is stripped off
341
    # later (in the legend-handling code) if it is not needed, but must be
342
    # included here since a plot may be built up by multiple calls to plot().
343
    #
344

345
    # Reshape the inputs and outputs for uniform indexing
346
    outputs = data.y.reshape(data.noutputs, ntraces, -1)
9✔
347
    if data.u is None or not plot_inputs:
9✔
348
        inputs = None
9✔
349
    else:
350
        inputs = data.u.reshape(data.ninputs, ntraces, -1)
9✔
351

352
    # Create a list of lines for the output
353
    out = np.empty((nrows, ncols), dtype=object)
9✔
354
    for i in range(nrows):
9✔
355
        for j in range(ncols):
9✔
356
            out[i, j] = []      # unique list in each element
9✔
357

358
    # Utility function for creating line label
359
    # TODO: combine with freqplot version?
360
    def _make_line_label(signal_index, signal_labels, trace_index):
9✔
361
        label = ""              # start with an empty label
9✔
362

363
        # Add the signal name if it won't appear as an axes label
364
        if overlay_signals or plot_inputs == 'overlay':
9✔
365
            label += signal_labels[signal_index]
9✔
366

367
        # Add the trace label if this is a multi-trace figure
368
        if overlay_traces and ntraces > 1 or trace_labels:
9✔
369
            label += ", " if label != "" else ""
9✔
370
            if trace_labels:
9✔
371
                label += trace_labels[trace_index]
×
372
            elif data.trace_labels:
9✔
373
                label += data.trace_labels[trace_index]
9✔
374
            else:
375
                label += f"trace {trace_index}"
×
376

377
        # Add the system name (will strip off later if redundant)
378
        label += ", " if label != "" else ""
9✔
379
        label += f"{data.sysname}"
9✔
380

381
        return label
9✔
382

383
    #
384
    # Store the color offsets with the figure to allow color/style cycling
385
    #
386
    # To allow repeated calls to time_response_plot() to cycle through
387
    # colors, we store an offset in the figure object that we can
388
    # retrieve in a later call, if needed.
389
    #
390
    output_offset = fig._output_offset = getattr(fig, '_output_offset', 0)
9✔
391
    input_offset = fig._input_offset = getattr(fig, '_input_offset', 0)
9✔
392

393
    #
394
    # Plot the lines for the response
395
    #
396

397
    # Process labels
398
    line_labels = _process_line_labels(
9✔
399
        label, ntraces, max(ninputs, noutputs), 2)
400

401
    # Go through each trace and each input/output
402
    for trace in range(ntraces):
9✔
403
        # Plot the output
404
        for i in range(noutputs):
9✔
405
            if line_labels is None:
9✔
406
                label = _make_line_label(i, data.output_labels, trace)
9✔
407
            else:
408
                label = line_labels[trace, i, 0]
9✔
409

410
            # Set up line properties for this output, trace
411
            if len(fmt) == 0:
9✔
412
                line_props = output_props[
9✔
413
                    (i + output_offset) % oprop_len if overlay_signals
414
                    else output_offset].copy()
415
                line_props.update(
9✔
416
                    trace_props[trace % tprop_len if overlay_traces else 0])
417
                line_props.update(kwargs)
9✔
418
            else:
419
                line_props = kwargs
9✔
420

421
            out[output_map[i, trace]] += ax_array[output_map[i, trace]].plot(
9✔
422
                data.time, outputs[i][trace], *fmt, label=label, **line_props)
423

424
        # Plot the input
425
        for i in range(ninputs):
9✔
426
            if line_labels is None:
9✔
427
                label = _make_line_label(i, data.input_labels, trace)
9✔
428
            else:
429
                label = line_labels[trace, i, 1]
9✔
430

431
            if add_initial_zero and data.ntraces > i \
9✔
432
               and data.trace_types[i] == 'step':
433
                x = np.hstack([np.array([data.time[0]]), data.time])
9✔
434
                y = np.hstack([np.array([0]), inputs[i][trace]])
9✔
435
            else:
436
                x, y = data.time, inputs[i][trace]
9✔
437

438
            # Set up line properties for this output, trace
439
            if len(fmt) == 0:
9✔
440
                line_props = input_props[
9✔
441
                    (i + input_offset) % iprop_len if overlay_signals
442
                    else input_offset].copy()
443
                line_props.update(
9✔
444
                    trace_props[trace % tprop_len if overlay_traces else 0])
445
                line_props.update(kwargs)
9✔
446
            else:
447
                line_props = kwargs
9✔
448

449
            out[input_map[i, trace]] += ax_array[input_map[i, trace]].plot(
9✔
450
                x, y, *fmt, label=label, **line_props)
451

452
    # Update the offsets so that we start at a new color/style the next time
453
    fig._output_offset = (
9✔
454
        output_offset + (noutputs if overlay_signals else 1)) % oprop_len
455
    fig._input_offset = (
9✔
456
        input_offset + (ninputs if overlay_signals else 1)) % iprop_len
457

458
    # Stop here if the user wants to control everything
459
    if not relabel:
9✔
460
        warn("relabel keyword is deprecated", FutureWarning)
9✔
461
        return ControlPlot(out, ax_array, fig)
9✔
462

463
    #
464
    # Label the axes (including trace labels)
465
    #
466
    # Once the data are plotted, we label the axes.  The horizontal axes is
467
    # always time and this is labeled only on the bottom most row.  The
468
    # vertical axes can consist either of a single signal or a combination
469
    # of signals (when overlay_signal is True or plot+inputs = 'overlay'.
470
    #
471
    # Traces are labeled at the top of the first row of plots (regular) or
472
    # the left edge of rows (tranpose).
473
    #
474

475
    # Time units on the bottom
476
    for col in range(ncols):
9✔
477
        ax_array[-1, col].set_xlabel(time_label)
9✔
478

479
    # Keep track of whether inputs are overlaid on outputs
480
    overlaid = plot_inputs == 'overlay'
9✔
481
    overlaid_title = "Inputs, Outputs"
9✔
482

483
    if transpose:               # inputs on left, outputs on right
9✔
484
        # Label the inputs
485
        if overlay_signals and plot_inputs:
9✔
486
            label = overlaid_title if overlaid else "Inputs"
9✔
487
            for trace in range(ntraces):
9✔
488
                ax_array[input_map[0, trace]].set_ylabel(label)
9✔
489
        else:
490
            for i in range(ninputs):
9✔
491
                label = overlaid_title if overlaid else data.input_labels[i]
9✔
492
                for trace in range(ntraces):
9✔
493
                    ax_array[input_map[i, trace]].set_ylabel(label)
9✔
494

495
        # Label the outputs
496
        if overlay_signals and plot_outputs:
9✔
497
            label = overlaid_title if overlaid else "Outputs"
9✔
498
            for trace in range(ntraces):
9✔
499
                ax_array[output_map[0, trace]].set_ylabel(label)
9✔
500
        else:
501
            for i in range(noutputs):
9✔
502
                label = overlaid_title if overlaid else data.output_labels[i]
9✔
503
                for trace in range(ntraces):
9✔
504
                    ax_array[output_map[i, trace]].set_ylabel(label)
9✔
505

506
        # Set the trace titles, if needed
507
        if ntraces > 1 and not overlay_traces:
9✔
508
            for trace in range(ntraces):
5✔
509
                # Get the existing ylabel for left column
510
                label = ax_array[trace, 0].get_ylabel()
5✔
511

512
                # Add on the trace title
513
                if trace_labels:
5✔
514
                    label = trace_labels[trace] + "\n" + label
×
515
                elif data.trace_labels:
5✔
516
                    label = data.trace_labels[trace] + "\n" + label
5✔
517
                else:
518
                    label = f"Trace {trace}" + "\n" + label
×
519

520
                ax_array[trace, 0].set_ylabel(label)
5✔
521

522
    else:                       # regular plot (outputs over inputs)
523
        # Set the trace titles, if needed
524
        if ntraces > 1 and not overlay_traces:
9✔
525
            for trace in range(ntraces):
9✔
526
                if trace_labels:
9✔
527
                    label = trace_labels[trace]
×
528
                elif data.trace_labels:
9✔
529
                    label = data.trace_labels[trace]
9✔
530
                else:
531
                    label = f"Trace {trace}"
×
532

533
                with plt.rc_context(rcParams):
9✔
534
                    ax_array[0, trace].set_title(label)
9✔
535

536
        # Label the outputs
537
        if overlay_signals and plot_outputs:
9✔
538
            ax_array[output_map[0, 0]].set_ylabel("Outputs")
9✔
539
        else:
540
            for i in range(noutputs):
9✔
541
                ax_array[output_map[i, 0]].set_ylabel(
9✔
542
                    overlaid_title if overlaid else data.output_labels[i])
543

544
        # Label the inputs
545
        if overlay_signals and plot_inputs:
9✔
546
            label = overlaid_title if overlaid else "Inputs"
9✔
547
            ax_array[input_map[0, 0]].set_ylabel(label)
9✔
548
        else:
549
            for i in range(ninputs):
9✔
550
                label = overlaid_title if overlaid else data.input_labels[i]
9✔
551
                ax_array[input_map[i, 0]].set_ylabel(label)
9✔
552

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

573
    # Figure out where to put legends
574
    if show_legend != False and legend_map is None:
9✔
575
        legend_map = np.full(ax_array.shape, None, dtype=object)
9✔
576

577
        if transpose:
9✔
578
            if (overlay_signals or plot_inputs == 'overlay') and overlay_traces:
9✔
579
                # Put a legend in each plot for inputs and outputs
580
                if plot_outputs is True:
9✔
581
                    legend_map[0, ninput_axes] = legend_loc
9✔
582
                if plot_inputs is True:
9✔
583
                    legend_map[0, 0] = legend_loc
9✔
584
            elif overlay_signals:
9✔
585
                # Put a legend in rightmost input/output plot
586
                if plot_inputs is True:
9✔
587
                    legend_map[0, 0] = legend_loc
9✔
588
                if plot_outputs is True:
9✔
589
                    legend_map[0, ninput_axes] = legend_loc
9✔
590
            elif plot_inputs == 'overlay':
9✔
591
                # Put a legend on the top of each column
592
                for i in range(ntraces):
9✔
593
                    legend_map[0, i] = legend_loc
9✔
594
            elif overlay_traces:
9✔
595
                # Put a legend topmost input/output plot
596
                legend_map[0, -1] = legend_loc
9✔
597
            else:
598
                # Put legend in the upper right
599
                legend_map[0, -1] = legend_loc
×
600

601
        else:                   # regular layout
602
            if (overlay_signals or plot_inputs == 'overlay') and overlay_traces:
9✔
603
                # Put a legend in each plot for inputs and outputs
604
                if plot_outputs is True:
9✔
605
                    legend_map[0, -1] = legend_loc
9✔
606
                if plot_inputs is True:
9✔
607
                    legend_map[noutput_axes, -1] = legend_loc
9✔
608
            elif overlay_signals:
9✔
609
                # Put a legend in rightmost input/output plot
610
                if plot_outputs is True:
9✔
611
                    legend_map[0, -1] = legend_loc
9✔
612
                if plot_inputs is True:
9✔
613
                    legend_map[noutput_axes, -1] = legend_loc
9✔
614
            elif plot_inputs == 'overlay':
9✔
615
                # Put a legend on the right of each row
616
                for i in range(max(ninputs, noutputs)):
9✔
617
                    legend_map[i, -1] = legend_loc
9✔
618
            elif overlay_traces:
9✔
619
                # Put a legend topmost input/output plot
620
                legend_map[0, -1] = legend_loc
9✔
621
            else:
622
                # Put legend in the upper right
623
                legend_map[0, -1] = legend_loc
9✔
624

625
    if show_legend != False:
9✔
626
        # Create axis legends
627
        legend_array = np.full(ax_array.shape, None, dtype=object)
9✔
628
        for i, j in itertools.product(range(nrows), range(ncols)):
9✔
629
            if legend_map[i, j] is None:
9✔
630
                continue
9✔
631
            ax = ax_array[i, j]
9✔
632
            labels = [line.get_label() for line in ax.get_lines()]
9✔
633
            if line_labels is None:
9✔
634
                labels = _make_legend_labels(labels, plot_inputs == 'overlay')
9✔
635

636
            # Update the labels to remove common strings
637
            if show_legend == True or len(labels) > 1:
9✔
638
                with plt.rc_context(rcParams):
9✔
639
                    legend_array[i, j] = ax.legend(
9✔
640
                        labels, loc=legend_map[i, j])
641
    else:
642
        legend_array = None
9✔
643

644
    #
645
    # Update the plot title (= figure suptitle)
646
    #
647
    # If plots are built up by multiple calls to plot() and the title is
648
    # not given, then the title is updated to provide a list of unique text
649
    # items in each successive title.  For data generated by the I/O
650
    # response functions this will generate a common prefix followed by a
651
    # list of systems (e.g., "Step response for sys[1], sys[2]").
652
    #
653

654
    if ax_user is None and title is None:
9✔
655
        title = data.title if title == None else title
9✔
656
        _update_plot_title(title, fig, rcParams=rcParams)
9✔
657
    elif ax_user is None:
9✔
658
        _update_plot_title(title, fig, rcParams=rcParams, use_existing=False)
9✔
659

660
    return ControlPlot(out, ax_array, fig, legend=legend_map)
9✔
661

662

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

666
    This function combines multiple instances of :class:`TimeResponseData`
667
    into a multi-trace :class:`TimeResponseData` object.
668

669
    Parameters
670
    ----------
671
    response_list : list of :class:`TimeResponseData` objects
672
        Reponses to be combined.
673
    trace_labels : list of str, optional
674
        List of labels for each trace.  If not specified, trace names are
675
        taken from the input data or set to None.
676
    title : str, optional
677
        Set the title to use when plotting.  Defaults to plot type and
678
        system name(s).
679

680
    Returns
681
    -------
682
    data : :class:`TimeResponseData`
683
        Multi-trace input/output data.
684

685
    """
686
    from .timeresp import TimeResponseData
9✔
687

688
    # Save the first trace as the base case
689
    base = response_list[0]
9✔
690

691
    # Process keywords
692
    title = base.title if title is None else title
9✔
693

694
    # Figure out the size of the data (and check for consistency)
695
    ntraces = max(1, base.ntraces)
9✔
696

697
    # Initial pass through trace list to count things up and do error checks
698
    nstates = base.nstates
9✔
699
    for response in response_list[1:]:
9✔
700
        # Make sure the time vector is the same
701
        if not np.allclose(base.t, response.t):
9✔
702
            raise ValueError("all responses must have the same time vector")
9✔
703

704
        # Make sure the dimensions are all the same
705
        if base.ninputs != response.ninputs or \
9✔
706
           base.noutputs != response.noutputs:
707
            raise ValueError("all responses must have the same number of "
9✔
708
                            "inputs, outputs, and states")
709

710
        if nstates != response.nstates:
9✔
711
            warn("responses have different state dimensions; dropping states")
×
712
            nstates = 0
×
713

714
        ntraces += max(1, response.ntraces)
9✔
715

716
    # Create data structures for the new time response data object
717
    inputs = np.empty((base.ninputs, ntraces, base.t.size))
9✔
718
    outputs = np.empty((base.noutputs, ntraces, base.t.size))
9✔
719
    states = np.empty((nstates, ntraces, base.t.size))
9✔
720

721
    # See whether we should create labels or not
722
    if trace_labels is None:
9✔
723
        generate_trace_labels = True
9✔
724
        trace_labels = []
9✔
725
    elif len(trace_labels) != ntraces:
9✔
726
        raise ValueError(
9✔
727
            "number of trace labels does not match number of traces")
728
    else:
729
        generate_trace_labels = False
9✔
730

731
    offset = 0
9✔
732
    trace_types = []
9✔
733
    for response in response_list:
9✔
734
        if response.ntraces == 0:
9✔
735
            # Single trace
736
            inputs[:, offset, :] = response.u
9✔
737
            outputs[:, offset, :] = response.y
9✔
738
            if nstates:
9✔
739
                states[:, offset, :] = response.x
9✔
740
            offset += 1
9✔
741

742
            # Add on trace label and trace type
743
            if generate_trace_labels:
9✔
744
                trace_labels.append(response.title)
9✔
745
            trace_types.append(
9✔
746
                None if response.trace_types is None
747
                else response.trace_types[0])
748

749
        else:
750
            # Save the data
751
            for i in range(response.ntraces):
9✔
752
                inputs[:, offset, :] = response.u[:, i, :]
9✔
753
                outputs[:, offset, :] = response.y[:, i, :]
9✔
754
                if nstates:
9✔
755
                    states[:, offset, :] = response.x[:, i, :]
9✔
756

757
                # Save the trace labels
758
                if generate_trace_labels:
9✔
759
                    if response.trace_labels is not None:
9✔
760
                        trace_labels.append(response.trace_labels[i])
9✔
761
                    else:
762
                        trace_labels.append(response.title + f", trace {i}")
9✔
763

764
                offset += 1
9✔
765

766
            # Save the trace types
767
            if response.trace_types is not None:
9✔
768
                trace_types += response.trace_types
9✔
769
            else:
770
                trace_types += [None] * response.ntraces
9✔
771

772
    return TimeResponseData(
9✔
773
        base.t, outputs, states if nstates else None, inputs,
774
        output_labels=base.output_labels, input_labels=base.input_labels,
775
        state_labels=base.state_labels if nstates else None,
776
        title=title, transpose=base.transpose, return_x=base.return_x,
777
        issiso=base.issiso, squeeze=base.squeeze, sysname=base.sysname,
778
        trace_labels=trace_labels, trace_types=trace_types,
779
        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