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

python-control / python-control / 9823966420

07 Jul 2024 02:33AM UTC coverage: 94.554% (+0.2%) from 94.389%
9823966420

push

github

web-flow
Merge pull request #1018 from murrayrm/timeresp_improvements-01Jun2024

Time response plot improvements

8890 of 9402 relevant lines covered (94.55%)

8.24 hits per line

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

96.07
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
from warnings import warn
9✔
12

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

17
from . import config
9✔
18
from .ctrlplot import _make_legend_labels, _update_suptitle
9✔
19

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

22
# Default font dictionary
23
_timeplot_rcParams = mpl.rcParams.copy()
9✔
24
_timeplot_rcParams.update({
9✔
25
    'axes.labelsize': 'small',
26
    'axes.titlesize': 'small',
27
    'figure.titlesize': 'medium',
28
    'legend.fontsize': 'x-small',
29
    'xtick.labelsize': 'small',
30
    'ytick.labelsize': 'small',
31
})
32

33
# Default values for module parameter variables
34
_timeplot_defaults = {
9✔
35
    'timeplot.rcParams': _timeplot_rcParams,
36
    'timeplot.trace_props': [
37
        {'linestyle': s} for s in ['-', '--', ':', '-.']],
38
    'timeplot.output_props': [
39
        {'color': c} for c in [
40
            'tab:blue', 'tab:orange', 'tab:green', 'tab:pink', 'tab:gray']],
41
    'timeplot.input_props': [
42
        {'color': c} for c in [
43
            'tab:red', 'tab:purple', 'tab:brown', 'tab:olive', 'tab:cyan']],
44
    'timeplot.time_label': "Time [s]",
45
}
46

47

48
# Plot the input/output response of a system
49
def time_response_plot(
9✔
50
        data, *fmt, ax=None, plot_inputs=None, plot_outputs=True,
51
        transpose=False, overlay_traces=False, overlay_signals=False,
52
        legend_map=None, legend_loc=None, add_initial_zero=True, label=None,
53
        trace_labels=None, title=None, relabel=True, show_legend=None,
54
        **kwargs):
55
    """Plot the time response of an input/output system.
56

57
    This function creates a standard set of plots for the input/output
58
    response of a system, with the data provided via a `TimeResponseData`
59
    object, which is the standard output for python-control simulation
60
    functions.
61

62
    Parameters
63
    ----------
64
    data : TimeResponseData
65
        Data to be plotted.
66
    ax : array of Axes
67
        The matplotlib Axes to draw the figure on.  If not specified, the
68
        Axes for the current figure are used or, if there is no current
69
        figure with the correct number and shape of Axes, a new figure is
70
        created.  The default shape of the array should be (noutputs +
71
        ninputs, ntraces), but if `overlay_traces` is set to `True` then
72
        only one row is needed and if `overlay_signals` is set to `True`
73
        then only one or two columns are needed (depending on plot_inputs
74
        and plot_outputs).
75
    plot_inputs : bool or str, optional
76
        Sets how and where to plot the inputs:
77
            * False: don't plot the inputs
78
            * None: use value from time response data (default)
79
            * 'overlay`: plot inputs overlaid with outputs
80
            * True: plot the inputs on their own axes
81
    plot_outputs : bool, optional
82
        If False, suppress plotting of the outputs.
83
    overlay_traces : bool, optional
84
        If set to True, combine all traces onto a single row instead of
85
        plotting a separate row for each trace.
86
    overlay_signals : bool, optional
87
        If set to True, combine all input and output signals onto a single
88
        plot (for each).
89
    transpose : bool, optional
90
        If transpose is False (default), signals are plotted from top to
91
        bottom, starting with outputs (if plotted) and then inputs.
92
        Multi-trace plots are stacked horizontally.  If transpose is True,
93
        signals are plotted from left to right, starting with the inputs
94
        (if plotted) and then the outputs.  Multi-trace responses are
95
        stacked vertically.
96
    *fmt : :func:`matplotlib.pyplot.plot` format string, optional
97
        Passed to `matplotlib` as the format string for all lines in the plot.
98
    **kwargs : :func:`matplotlib.pyplot.plot` keyword properties, optional
99
        Additional keywords passed to `matplotlib` to specify line properties.
100

101
    Returns
102
    -------
103
    out : array of list of Line2D
104
        Array of Line2D objects for each line in the plot.  The shape of
105
        the array matches the subplots shape and the value of the array is a
106
        list of Line2D objects in that subplot.
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
    input_props : array of dicts
114
        List of line properties to use when plotting combined inputs.  The
115
        default values are set by config.defaults['timeplot.input_props'].
116
    label : str or array_like of str
117
        If present, replace automatically generated label(s) with the given
118
        label(s).  If more than one line is being generated, an array of
119
        labels should be provided with label[trace, :, 0] representing the
120
        output labels and label[trace, :, 1] representing the input labels.
121
    legend_map : array of str, option
122
        Location of the legend for multi-trace plots.  Specifies an array
123
        of legend location strings matching the shape of the subplots, with
124
        each entry being either None (for no legend) or a legend location
125
        string (see :func:`~matplotlib.pyplot.legend`).
126
    legend_loc : str
127
        Location of the legend within the axes for which it appears.  This
128
        value is used if legend_map is None.
129
    output_props : array of dicts
130
        List of line properties to use when plotting combined outputs.  The
131
        default values are set by config.defaults['timeplot.output_props'].
132
    relabel : bool, optional
133
        By default, existing figures and axes are relabeled when new data
134
        are added.  If set to `False`, just plot new data on existing axes.
135
    show_legend : bool, optional
136
        Force legend to be shown if ``True`` or hidden if ``False``.  If
137
        ``None``, then show legend when there is more than one line on an
138
        axis or ``legend_loc`` or ``legend_map`` have been specified.
139
    time_label : str, optional
140
        Label to use for the time axis.
141
    trace_props : array of dicts
142
        List of line properties to use when plotting combined outputs.  The
143
        default values are set by config.defaults['timeplot.trace_props'].
144

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

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

161
    3. The default plot properties, such as font sizes, can be set using
162
       config.defaults[''timeplot.rcParams'].
163

164
    """
165
    from .freqplot import _process_ax_keyword, _process_line_labels
9✔
166
    from .iosys import InputOutputSystem
9✔
167
    from .timeresp import TimeResponseData
9✔
168

169
    #
170
    # Process keywords and set defaults
171
    #
172
    # Set up defaults
173
    time_label = config._get_param(
9✔
174
        'timeplot', 'time_label', kwargs, _timeplot_defaults, pop=True)
175
    rcParams = config._get_param(
9✔
176
        'timeplot', 'rcParams', kwargs, _timeplot_defaults, pop=True)
177

178
    if kwargs.get('input_props', None) and len(fmt) > 0:
9✔
179
        warn("input_props ignored since fmt string was present")
9✔
180
    input_props = config._get_param(
9✔
181
        'timeplot', 'input_props', kwargs, _timeplot_defaults, pop=True)
182
    iprop_len = len(input_props)
9✔
183

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

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

196
    # Set the title for the data
197
    title = data.title if title == None else title
9✔
198

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

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

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

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

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

289
    # See if we can use the current figure axes
290
    fig, ax_array = _process_ax_keyword(ax, (nrows, ncols), rcParams=rcParams)
9✔
291

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

302
    # Create the map from trace, signal to axes, accounting for overlay_*
303
    output_map = np.empty((noutputs, ntraces), dtype=tuple)
9✔
304
    input_map = np.empty((ninputs, ntraces), dtype=tuple)
9✔
305

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

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

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

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

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

354
    # Utility function for creating line label
355
    # TODO: combine with freqplot version?
356
    def _make_line_label(signal_index, signal_labels, trace_index):
9✔
357
        label = ""              # start with an empty label
9✔
358

359
        # Add the signal name if it won't appear as an axes label
360
        if overlay_signals or plot_inputs == 'overlay':
9✔
361
            label += signal_labels[signal_index]
9✔
362

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

373
        # Add the system name (will strip off later if redundant)
374
        label += ", " if label != "" else ""
9✔
375
        label += f"{data.sysname}"
9✔
376

377
        return label
9✔
378

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

389
    #
390
    # Plot the lines for the response
391
    #
392

393
    # Process labels
394
    line_labels = _process_line_labels(
9✔
395
        label, ntraces, max(ninputs, noutputs), 2)
396

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

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

417
            out[output_map[i, trace]] += ax_array[output_map[i, trace]].plot(
9✔
418
                data.time, outputs[i][trace], *fmt, label=label, **line_props)
419

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

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

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

445
            out[input_map[i, trace]] += ax_array[input_map[i, trace]].plot(
9✔
446
                x, y, *fmt, label=label, **line_props)
447

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

454
    # Stop here if the user wants to control everything
455
    if not relabel:
9✔
456
        return out
9✔
457

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

470
    # Time units on the bottom
471
    for col in range(ncols):
9✔
472
        ax_array[-1, col].set_xlabel(time_label)
9✔
473

474
    # Keep track of whether inputs are overlaid on outputs
475
    overlaid = plot_inputs == 'overlay'
9✔
476
    overlaid_title = "Inputs, Outputs"
9✔
477

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

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

501
        # Set the trace titles, if needed
502
        if ntraces > 1 and not overlay_traces:
9✔
503
            for trace in range(ntraces):
5✔
504
                # Get the existing ylabel for left column
505
                label = ax_array[trace, 0].get_ylabel()
5✔
506

507
                # Add on the trace title
508
                if trace_labels:
5✔
509
                    label = trace_labels[trace] + "\n" + label
×
510
                elif data.trace_labels:
5✔
511
                    label = data.trace_labels[trace] + "\n" + label
5✔
512
                else:
513
                    label = f"Trace {trace}" + "\n" + label
×
514

515
                ax_array[trace, 0].set_ylabel(label)
5✔
516

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

528
                with plt.rc_context(rcParams):
9✔
529
                    ax_array[0, trace].set_title(label)
9✔
530

531
        # Label the outputs
532
        if overlay_signals and plot_outputs:
9✔
533
            ax_array[output_map[0, 0]].set_ylabel("Outputs")
9✔
534
        else:
535
            for i in range(noutputs):
9✔
536
                ax_array[output_map[i, 0]].set_ylabel(
9✔
537
                    overlaid_title if overlaid else data.output_labels[i])
538

539
        # Label the inputs
540
        if overlay_signals and plot_inputs:
9✔
541
            label = overlaid_title if overlaid else "Inputs"
9✔
542
            ax_array[input_map[0, 0]].set_ylabel(label)
9✔
543
        else:
544
            for i in range(ninputs):
9✔
545
                label = overlaid_title if overlaid else data.input_labels[i]
9✔
546
                ax_array[input_map[i, 0]].set_ylabel(label)
9✔
547

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

568
    # Figure out where to put legends
569
    if legend_map is None:
9✔
570
        legend_map = np.full(ax_array.shape, None, dtype=object)
9✔
571
        if legend_loc == None:
9✔
572
            legend_loc = 'center right'
9✔
573
        else:
574
            show_legend = True if show_legend is None else show_legend
×
575

576
        if transpose:
9✔
577
            if (overlay_signals or plot_inputs == 'overlay') and overlay_traces:
9✔
578
                # Put a legend in each plot for inputs and outputs
579
                if plot_outputs is True:
9✔
580
                    legend_map[0, ninput_axes] = legend_loc
9✔
581
                if plot_inputs is True:
9✔
582
                    legend_map[0, 0] = legend_loc
9✔
583
            elif overlay_signals:
9✔
584
                # Put a legend in rightmost input/output plot
585
                if plot_inputs is True:
9✔
586
                    legend_map[0, 0] = legend_loc
9✔
587
                if plot_outputs is True:
9✔
588
                    legend_map[0, ninput_axes] = legend_loc
9✔
589
            elif plot_inputs == 'overlay':
9✔
590
                # Put a legend on the top of each column
591
                for i in range(ntraces):
9✔
592
                    legend_map[0, i] = legend_loc
9✔
593
            elif overlay_traces:
9✔
594
                # Put a legend topmost input/output plot
595
                legend_map[0, -1] = legend_loc
9✔
596
            else:
597
                # Put legend in the upper right
598
                legend_map[0, -1] = legend_loc
×
599
        else:                   # regular layout
600
            if (overlay_signals or plot_inputs == 'overlay') and overlay_traces:
9✔
601
                # Put a legend in each plot for inputs and outputs
602
                if plot_outputs is True:
9✔
603
                    legend_map[0, -1] = legend_loc
9✔
604
                if plot_inputs is True:
9✔
605
                    legend_map[noutput_axes, -1] = legend_loc
9✔
606
            elif overlay_signals:
9✔
607
                # Put a legend in rightmost input/output plot
608
                if plot_outputs is True:
9✔
609
                    legend_map[0, -1] = legend_loc
9✔
610
                if plot_inputs is True:
9✔
611
                    legend_map[noutput_axes, -1] = legend_loc
9✔
612
            elif plot_inputs == 'overlay':
9✔
613
                # Put a legend on the right of each row
614
                for i in range(max(ninputs, noutputs)):
9✔
615
                    legend_map[i, -1] = legend_loc
9✔
616
            elif overlay_traces:
9✔
617
                # Put a legend topmost input/output plot
618
                legend_map[0, -1] = legend_loc
9✔
619
            else:
620
                # Put legend in the upper right
621
                legend_map[0, -1] = legend_loc
9✔
622
    else:
623
        # Make sure the legend map is the right size
624
        legend_map = np.atleast_2d(legend_map)
9✔
625
        if legend_map.shape != ax_array.shape:
9✔
626
            raise ValueError("legend_map shape just match axes shape")
×
627

628
        # Turn legend on unless overridden by user
629
        show_legend = True if show_legend is None else show_legend
9✔
630

631
    # Create axis legends
632
    for i in range(nrows):
9✔
633
        for j in range(ncols):
9✔
634
            ax = ax_array[i, j]
9✔
635
            labels = [line.get_label() for line in ax.get_lines()]
9✔
636
            if line_labels is None:
9✔
637
                labels = _make_legend_labels(labels, plot_inputs == 'overlay')
9✔
638

639
            # Update the labels to remove common strings
640
            if show_legend != False and \
9✔
641
               (len(labels) > 1 or show_legend) and \
642
               legend_map[i, j] != None:
643
                with plt.rc_context(rcParams):
9✔
644
                    ax.legend(labels, loc=legend_map[i, j])
9✔
645

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

656
    _update_suptitle(fig, title, rcParams=rcParams)
9✔
657

658
    return out
9✔
659

660

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

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

667
    Parameters
668
    ----------
669
    response_list : list of :class:`TimeResponseData` objects
670
        Reponses to be combined.
671
    trace_labels : list of str, optional
672
        List of labels for each trace.  If not specified, trace names are
673
        taken from the input data or set to None.
674

675
    Returns
676
    -------
677
    data : :class:`TimeResponseData`
678
        Multi-trace input/output data.
679

680
    """
681
    from .timeresp import TimeResponseData
9✔
682

683
    # Save the first trace as the base case
684
    base = response_list[0]
9✔
685

686
    # Process keywords
687
    title = base.title if title is None else title
9✔
688

689
    # Figure out the size of the data (and check for consistency)
690
    ntraces = max(1, base.ntraces)
9✔
691

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

699
        # Make sure the dimensions are all the same
700
        if base.ninputs != response.ninputs or \
9✔
701
           base.noutputs != response.noutputs:
702
            raise ValueError("all responses must have the same number of "
9✔
703
                            "inputs, outputs, and states")
704

705
        if nstates != response.nstates:
9✔
706
            warn("responses have different state dimensions; dropping states")
×
707
            nstates = 0
×
708

709
        ntraces += max(1, response.ntraces)
9✔
710

711
    # Create data structures for the new time response data object
712
    inputs = np.empty((base.ninputs, ntraces, base.t.size))
9✔
713
    outputs = np.empty((base.noutputs, ntraces, base.t.size))
9✔
714
    states = np.empty((nstates, ntraces, base.t.size))
9✔
715

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

726
    offset = 0
9✔
727
    trace_types = []
9✔
728
    for response in response_list:
9✔
729
        if response.ntraces == 0:
9✔
730
            # Single trace
731
            inputs[:, offset, :] = response.u
9✔
732
            outputs[:, offset, :] = response.y
9✔
733
            if nstates:
9✔
734
                states[:, offset, :] = response.x
9✔
735
            offset += 1
9✔
736

737
            # Add on trace label and trace type
738
            if generate_trace_labels:
9✔
739
                trace_labels.append(response.title)
9✔
740
            trace_types.append(
9✔
741
                None if response.trace_types is None else response.types[0])
742

743
        else:
744
            # Save the data
745
            for i in range(response.ntraces):
9✔
746
                inputs[:, offset, :] = response.u[:, i, :]
9✔
747
                outputs[:, offset, :] = response.y[:, i, :]
9✔
748
                if nstates:
9✔
749
                    states[:, offset, :] = response.x[:, i, :]
9✔
750

751
                # Save the trace labels
752
                if generate_trace_labels:
9✔
753
                    if response.trace_labels is not None:
9✔
754
                        trace_labels.append(response.trace_labels[i])
9✔
755
                    else:
756
                        trace_labels.append(response.title + f", trace {i}")
9✔
757

758
                offset += 1
9✔
759

760
            # Save the trace types
761
            if response.trace_types is not None:
9✔
762
                trace_types += response.trace_types
9✔
763
            else:
764
                trace_types += [None] * response.ntraces
9✔
765

766
    return TimeResponseData(
9✔
767
        base.t, outputs, states if nstates else None, inputs,
768
        output_labels=base.output_labels, input_labels=base.input_labels,
769
        state_labels=base.state_labels if nstates else None,
770
        title=title, transpose=base.transpose, return_x=base.return_x,
771
        issiso=base.issiso, squeeze=base.squeeze, sysname=base.sysname,
772
        trace_labels=trace_labels, trace_types=trace_types,
773
        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