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

python-control / python-control / 13107996232

03 Feb 2025 06:53AM UTC coverage: 94.731% (+0.02%) from 94.709%
13107996232

push

github

web-flow
Merge pull request #1094 from murrayrm/userguide-22Dec2024

Updated user documentation (User Guide, Reference Manual)

9673 of 10211 relevant lines covered (94.73%)

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
"""Time plotting functions.
5

6
This module contains routines for plotting out time responses.  These
7
functions can be called either as standalone functions or access from
8
the TimeResponseData class.
9

10
"""
11

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

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 : `matplotlib.pyplot.plot` format string, optional
87
        Passed to `matplotlib` as the format string for all lines in the plot.
88
    **kwargs : `matplotlib.pyplot.plot` keyword properties, optional
89
        Additional keywords passed to `matplotlib` to specify line properties.
90

91
    Returns
92
    -------
93
    cplt : `ControlPlot` object
94
        Object containing the data that were plotted.  See `ControlPlot`
95
        for more detailed information.
96
    cplt.lines : 2D array of `matplotlib.lines.Line2D`
97
        Array containing information on each line in the plot.  The shape
98
        of the array matches the subplots shape and the value of the array
99
        is a list of Line2D objects in that subplot.
100
    cplt.axes : 2D array of `matplotlib.axes.Axes`
101
        Axes for each subplot.
102
    cplt.figure : `matplotlib.figure.Figure`
103
        Figure containing the plot.
104
    cplt.legend : 2D array of `matplotlib.legend.Legend`
105
        Legend object(s) contained in the plot.
106

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

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

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

173
    The default plot properties, such as font sizes, can be set using
174
    `config.defaults[''timeplot.rcParams']`.
175

176
    """
177
    from .ctrlplot import _process_ax_keyword, _process_line_labels
9✔
178

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

389
        return label
9✔
390

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

401
    #
402
    # Plot the lines for the response
403
    #
404

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

528
                ax_array[trace, 0].set_ylabel(label)
5✔
529

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

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

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

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

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

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

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

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

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

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

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

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

668
    return ControlPlot(out, ax_array, fig, legend=legend_map)
9✔
669

670

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

674
    This function combines multiple instances of `TimeResponseData`
675
    into a multi-trace `TimeResponseData` object.
676

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

688
    Returns
689
    -------
690
    data : `TimeResponseData`
691
        Multi-trace input/output data.
692

693
    """
694
    from .timeresp import TimeResponseData
9✔
695

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

699
    # Process keywords
700
    title = base.title if title is None else title
9✔
701

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

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

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

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

722
        ntraces += max(1, response.ntraces)
9✔
723

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

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

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

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

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

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

775
                offset += 1
9✔
776

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

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