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

python-control / python-control / 13106139066

03 Feb 2025 04:04AM UTC coverage: 94.709% (+0.2%) from 94.536%
13106139066

push

github

web-flow
Merge pull request #1118 from roryyorke/lint-lib-only

Lint library code only with `ruff check`

9630 of 10168 relevant lines covered (94.71%)

8.28 hits per line

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

96.8
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.pyplot as plt
9✔
15
import numpy as np
9✔
16

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

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

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

38

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

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

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

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

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

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

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

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

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

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

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

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

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

178
    """
179
    from .ctrlplot import _process_ax_keyword, _process_line_labels
9✔
180

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

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

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

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

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

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

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

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

296
    ncols = ntraces if not overlay_traces else 1
9✔
297
    if transpose:
9✔
298
        nrows, ncols = ncols, nrows
9✔
299

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

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

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

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

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

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

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

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

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

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

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

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

391
        return label
9✔
392

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

403
    #
404
    # Plot the lines for the response
405
    #
406

407
    # Process labels
408
    line_labels = _process_line_labels(
9✔
409
        label, ntraces, max(ninputs, noutputs), 2)
410

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

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

431
            out[output_map[i, trace]] += ax_array[output_map[i, trace]].plot(
9✔
432
                data.time, outputs[i][trace], *fmt, label=label, **line_props)
433

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

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

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

459
            out[input_map[i, trace]] += ax_array[input_map[i, trace]].plot(
9✔
460
                x, y, *fmt, label=label, **line_props)
461

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

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

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

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

489
    # Keep track of whether inputs are overlaid on outputs
490
    overlaid = plot_inputs == 'overlay'
9✔
491
    overlaid_title = "Inputs, Outputs"
9✔
492

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

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

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

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

530
                ax_array[trace, 0].set_ylabel(label)
5✔
531

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

543
                with plt.rc_context(rcParams):
9✔
544
                    ax_array[0, trace].set_title(label)
9✔
545

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

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

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

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

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

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

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

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

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

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

670
    return ControlPlot(out, ax_array, fig, legend=legend_map)
9✔
671

672

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

676
    This function combines multiple instances of :class:`TimeResponseData`
677
    into a multi-trace :class:`TimeResponseData` object.
678

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

690
    Returns
691
    -------
692
    data : :class:`TimeResponseData`
693
        Multi-trace input/output data.
694

695
    """
696
    from .timeresp import TimeResponseData
9✔
697

698
    # Save the first trace as the base case
699
    base = response_list[0]
9✔
700

701
    # Process keywords
702
    title = base.title if title is None else title
9✔
703

704
    # Figure out the size of the data (and check for consistency)
705
    ntraces = max(1, base.ntraces)
9✔
706

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

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

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

724
        ntraces += max(1, response.ntraces)
9✔
725

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

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

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

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

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

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

774
                offset += 1
9✔
775

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

782
    return TimeResponseData(
9✔
783
        base.t, outputs, states if nstates else None, inputs,
784
        output_labels=base.output_labels, input_labels=base.input_labels,
785
        state_labels=base.state_labels if nstates else None,
786
        title=title, transpose=base.transpose, return_x=base.return_x,
787
        issiso=base.issiso, squeeze=base.squeeze, sysname=base.sysname,
788
        trace_labels=trace_labels, trace_types=trace_types,
789
        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

© 2025 Coveralls, Inc