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

python-control / python-control / 10029876295

21 Jul 2024 04:45PM UTC coverage: 94.629%. Remained the same
10029876295

push

github

web-flow
Merge pull request #1033 from murrayrm/ctrlplot_refactor-27Jun2024

Move ctrlplot code prior to upcoming PR

8915 of 9421 relevant lines covered (94.63%)

8.25 hits per line

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

96.04
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 _ctrlplot_rcParams, _make_legend_labels, _update_suptitle
9✔
19

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

22
# Default values for module parameter variables
23
_timeplot_defaults = {
9✔
24
    'timeplot.rcParams': _ctrlplot_rcParams,
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
}
35

36

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

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

51
    Parameters
52
    ----------
53
    data : TimeResponseData
54
        Data to be plotted.
55
    ax : array of Axes
56
        The matplotlib Axes to draw the figure on.  If not specified, the
57
        Axes for the current figure are used or, if there is no current
58
        figure with the correct number and shape of Axes, a new figure is
59
        created.  The default shape of the array should be (noutputs +
60
        ninputs, ntraces), but if `overlay_traces` is set to `True` then
61
        only one row is needed and if `overlay_signals` is set to `True`
62
        then only one or two columns are needed (depending on plot_inputs
63
        and plot_outputs).
64
    plot_inputs : bool or str, optional
65
        Sets how and where to plot the inputs:
66
            * False: don't plot the inputs
67
            * None: use value from time response data (default)
68
            * 'overlay`: plot inputs overlaid with outputs
69
            * True: plot the inputs on their own axes
70
    plot_outputs : bool, optional
71
        If False, suppress plotting of the outputs.
72
    overlay_traces : bool, optional
73
        If set to True, combine all traces onto a single row instead of
74
        plotting a separate row for each trace.
75
    overlay_signals : bool, optional
76
        If set to True, combine all input and output signals onto a single
77
        plot (for each).
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
    out : array of list of Line2D
93
        Array of Line2D objects for each line in the plot.  The shape of
94
        the array matches the subplots shape and the value of the array is a
95
        list of Line2D objects in that subplot.
96

97
    Other Parameters
98
    ----------------
99
    add_initial_zero : bool
100
        Add an initial point of zero at the first time point for all
101
        inputs with type 'step'.  Default is True.
102
    input_props : array of dicts
103
        List of line properties to use when plotting combined inputs.  The
104
        default values are set by config.defaults['timeplot.input_props'].
105
    label : str or array_like of str
106
        If present, replace automatically generated label(s) with the given
107
        label(s).  If more than one line is being generated, an array of
108
        labels should be provided with label[trace, :, 0] representing the
109
        output labels and label[trace, :, 1] representing the input labels.
110
    legend_map : array of str, option
111
        Location of the legend for multi-trace plots.  Specifies an array
112
        of legend location strings matching the shape of the subplots, with
113
        each entry being either None (for no legend) or a legend location
114
        string (see :func:`~matplotlib.pyplot.legend`).
115
    legend_loc : str
116
        Location of the legend within the axes for which it appears.  This
117
        value is used if legend_map is None.
118
    output_props : array of dicts
119
        List of line properties to use when plotting combined outputs.  The
120
        default values are set by config.defaults['timeplot.output_props'].
121
    relabel : bool, optional
122
        By default, existing figures and axes are relabeled when new data
123
        are added.  If set to `False`, just plot new data on existing axes.
124
    show_legend : bool, optional
125
        Force legend to be shown if ``True`` or hidden if ``False``.  If
126
        ``None``, then show legend when there is more than one line on an
127
        axis or ``legend_loc`` or ``legend_map`` have been specified.
128
    time_label : str, optional
129
        Label to use for the time axis.
130
    trace_props : array of dicts
131
        List of line properties to use when plotting combined outputs.  The
132
        default values are set by config.defaults['timeplot.trace_props'].
133

134
    Notes
135
    -----
136
    1. A new figure will be generated if there is no current figure or
137
       the current figure has an incompatible number of axes.  To
138
       force the creation of a new figures, use `plt.figure()`.  To reuse
139
       a portion of an existing figure, use the `ax` keyword.
140

141
    2. The line properties (color, linestyle, etc) can be set for the
142
       entire plot using the `fmt` and/or `kwargs` parameter, which
143
       are passed on to `matplotlib`.  When combining signals or
144
       traces, the `input_props`, `output_props`, and `trace_props`
145
       parameters can be used to pass a list of dictionaries
146
       containing the line properties to use.  These input/output
147
       properties are combined with the trace properties and finally
148
       the kwarg properties to determine the final line properties.
149

150
    3. The default plot properties, such as font sizes, can be set using
151
       config.defaults[''timeplot.rcParams'].
152

153
    """
154
    from .ctrlplot import _process_ax_keyword, _process_line_labels
9✔
155
    from .iosys import InputOutputSystem
9✔
156
    from .timeresp import TimeResponseData
9✔
157

158
    #
159
    # Process keywords and set defaults
160
    #
161
    # Set up defaults
162
    time_label = config._get_param(
9✔
163
        'timeplot', 'time_label', kwargs, _timeplot_defaults, pop=True)
164
    rcParams = config._get_param(
9✔
165
        'timeplot', 'rcParams', kwargs, _timeplot_defaults, pop=True)
166

167
    if kwargs.get('input_props', None) and len(fmt) > 0:
9✔
168
        warn("input_props ignored since fmt string was present")
9✔
169
    input_props = config._get_param(
9✔
170
        'timeplot', 'input_props', kwargs, _timeplot_defaults, pop=True)
171
    iprop_len = len(input_props)
9✔
172

173
    if kwargs.get('output_props', None) and len(fmt) > 0:
9✔
174
        warn("output_props ignored since fmt string was present")
9✔
175
    output_props = config._get_param(
9✔
176
        'timeplot', 'output_props', kwargs, _timeplot_defaults, pop=True)
177
    oprop_len = len(output_props)
9✔
178

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

185
    # Set the title for the data
186
    title = data.title if title == None else title
9✔
187

188
    # Determine whether or not to plot the input data (and how)
189
    if plot_inputs is None:
9✔
190
        plot_inputs = data.plot_inputs
9✔
191
    if plot_inputs not in [True, False, 'overlay']:
9✔
192
        raise ValueError(f"unrecognized value: {plot_inputs=}")
9✔
193

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

245
    # Decide on the number of inputs and outputs
246
    ninputs = data.ninputs if plot_inputs else 0
9✔
247
    noutputs = data.noutputs if plot_outputs else 0
9✔
248
    ntraces = max(1, data.ntraces)      # treat data.ntraces == 0 as 1 trace
9✔
249
    if ninputs == 0 and noutputs == 0:
9✔
250
        raise ValueError(
9✔
251
            "plot_inputs and plot_outputs both False; no data to plot")
252
    elif plot_inputs == 'overlay' and noutputs == 0:
9✔
253
        raise ValueError(
9✔
254
            "can't overlay inputs with no outputs")
255
    elif plot_inputs in [True, 'overlay'] and data.ninputs == 0:
9✔
256
        raise ValueError(
9✔
257
            "input plotting requested but no inputs in time response data")
258

259
    # Figure how how many rows and columns to use + offsets for inputs/outputs
260
    if plot_inputs == 'overlay' and not overlay_signals:
9✔
261
        nrows = max(ninputs, noutputs)          # Plot inputs on top of outputs
9✔
262
        noutput_axes = 0                        # No offset required
9✔
263
        ninput_axes = 0                         # No offset required
9✔
264
    elif overlay_signals:
9✔
265
        nrows = int(plot_outputs)               # Start with outputs
9✔
266
        nrows += int(plot_inputs == True)       # Add plot for inputs if needed
9✔
267
        noutput_axes = 1 if plot_outputs and plot_inputs is True else 0
9✔
268
        ninput_axes = 1 if plot_inputs is True else 0
9✔
269
    else:
270
        nrows = noutputs + ninputs              # Plot inputs separately
9✔
271
        noutput_axes = noutputs if plot_outputs else 0
9✔
272
        ninput_axes = ninputs if plot_inputs else 0
9✔
273

274
    ncols = ntraces if not overlay_traces else 1
9✔
275
    if transpose:
9✔
276
        nrows, ncols = ncols, nrows
9✔
277

278
    # See if we can use the current figure axes
279
    fig, ax_array = _process_ax_keyword(ax, (nrows, ncols), rcParams=rcParams)
9✔
280

281
    #
282
    # Map inputs/outputs and traces to axes
283
    #
284
    # This set of code takes care of all of the various options for how to
285
    # plot the data.  The arrays output_map and input_map are used to map
286
    # the different signals that are plotted onto the axes created above.
287
    # This code is complicated because it has to handle lots of different
288
    # variations.
289
    #
290

291
    # Create the map from trace, signal to axes, accounting for overlay_*
292
    output_map = np.empty((noutputs, ntraces), dtype=tuple)
9✔
293
    input_map = np.empty((ninputs, ntraces), dtype=tuple)
9✔
294

295
    for i in range(noutputs):
9✔
296
        for j in range(ntraces):
9✔
297
            signal_index = i if not overlay_signals else 0
9✔
298
            trace_index = j if not overlay_traces else 0
9✔
299
            if transpose:
9✔
300
                output_map[i, j] = (trace_index, signal_index + ninput_axes)
9✔
301
            else:
302
                output_map[i, j] = (signal_index, trace_index)
9✔
303

304
    for i in range(ninputs):
9✔
305
        for j in range(ntraces):
9✔
306
            signal_index = noutput_axes + (i if not overlay_signals else 0)
9✔
307
            trace_index = j if not overlay_traces else 0
9✔
308
            if transpose:
9✔
309
                input_map[i, j] = (trace_index, signal_index - noutput_axes)
9✔
310
            else:
311
                input_map[i, j] = (signal_index, trace_index)
9✔
312

313
    #
314
    # Plot the data
315
    #
316
    # The ax_output and ax_input arrays have the axes needed for making the
317
    # plots.  Labels are used on each axes for later creation of legends.
318
    # The generic labels if of the form:
319
    #
320
    #     signal name, trace label, system name
321
    #
322
    # The signal name or trace label can be omitted if they will appear on
323
    # the axes title or ylabel.  The system name is always included, since
324
    # multiple calls to plot() will require a legend that distinguishes
325
    # which system signals are plotted.  The system name is stripped off
326
    # later (in the legend-handling code) if it is not needed, but must be
327
    # included here since a plot may be built up by multiple calls to plot().
328
    #
329

330
    # Reshape the inputs and outputs for uniform indexing
331
    outputs = data.y.reshape(data.noutputs, ntraces, -1)
9✔
332
    if data.u is None or not plot_inputs:
9✔
333
        inputs = None
9✔
334
    else:
335
        inputs = data.u.reshape(data.ninputs, ntraces, -1)
9✔
336

337
    # Create a list of lines for the output
338
    out = np.empty((nrows, ncols), dtype=object)
9✔
339
    for i in range(nrows):
9✔
340
        for j in range(ncols):
9✔
341
            out[i, j] = []      # unique list in each element
9✔
342

343
    # Utility function for creating line label
344
    # TODO: combine with freqplot version?
345
    def _make_line_label(signal_index, signal_labels, trace_index):
9✔
346
        label = ""              # start with an empty label
9✔
347

348
        # Add the signal name if it won't appear as an axes label
349
        if overlay_signals or plot_inputs == 'overlay':
9✔
350
            label += signal_labels[signal_index]
9✔
351

352
        # Add the trace label if this is a multi-trace figure
353
        if overlay_traces and ntraces > 1 or trace_labels:
9✔
354
            label += ", " if label != "" else ""
5✔
355
            if trace_labels:
5✔
356
                label += trace_labels[trace_index]
×
357
            elif data.trace_labels:
5✔
358
                label += data.trace_labels[trace_index]
5✔
359
            else:
360
                label += f"trace {trace_index}"
×
361

362
        # Add the system name (will strip off later if redundant)
363
        label += ", " if label != "" else ""
9✔
364
        label += f"{data.sysname}"
9✔
365

366
        return label
9✔
367

368
    #
369
    # Store the color offsets with the figure to allow color/style cycling
370
    #
371
    # To allow repeated calls to time_response_plot() to cycle through
372
    # colors, we store an offset in the figure object that we can
373
    # retrieve at a later date, if needed.
374
    #
375
    output_offset = fig._output_offset = getattr(fig, '_output_offset', 0)
9✔
376
    input_offset = fig._input_offset = getattr(fig, '_input_offset', 0)
9✔
377

378
    #
379
    # Plot the lines for the response
380
    #
381

382
    # Process labels
383
    line_labels = _process_line_labels(
9✔
384
        label, ntraces, max(ninputs, noutputs), 2)
385

386
    # Go through each trace and each input/output
387
    for trace in range(ntraces):
9✔
388
        # Plot the output
389
        for i in range(noutputs):
9✔
390
            if line_labels is None:
9✔
391
                label = _make_line_label(i, data.output_labels, trace)
9✔
392
            else:
393
                label = line_labels[trace, i, 0]
9✔
394

395
            # Set up line properties for this output, trace
396
            if len(fmt) == 0:
9✔
397
                line_props = output_props[
9✔
398
                    (i + output_offset) % oprop_len if overlay_signals
399
                    else output_offset].copy()
400
                line_props.update(
9✔
401
                    trace_props[trace % tprop_len if overlay_traces else 0])
402
                line_props.update(kwargs)
9✔
403
            else:
404
                line_props = kwargs
9✔
405

406
            out[output_map[i, trace]] += ax_array[output_map[i, trace]].plot(
9✔
407
                data.time, outputs[i][trace], *fmt, label=label, **line_props)
408

409
        # Plot the input
410
        for i in range(ninputs):
9✔
411
            if line_labels is None:
9✔
412
                label = _make_line_label(i, data.input_labels, trace)
9✔
413
            else:
414
                label = line_labels[trace, i, 1]
9✔
415

416
            if add_initial_zero and data.ntraces > i \
9✔
417
               and data.trace_types[i] == 'step':
418
                x = np.hstack([np.array([data.time[0]]), data.time])
9✔
419
                y = np.hstack([np.array([0]), inputs[i][trace]])
9✔
420
            else:
421
                x, y = data.time, inputs[i][trace]
9✔
422

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

434
            out[input_map[i, trace]] += ax_array[input_map[i, trace]].plot(
9✔
435
                x, y, *fmt, label=label, **line_props)
436

437
    # Update the offsets so that we start at a new color/style the next time
438
    fig._output_offset = (
9✔
439
        output_offset + (noutputs if overlay_signals else 1)) % oprop_len
440
    fig._input_offset = (
9✔
441
        input_offset + (ninputs if overlay_signals else 1)) % iprop_len
442

443
    # Stop here if the user wants to control everything
444
    if not relabel:
9✔
445
        return out
9✔
446

447
    #
448
    # Label the axes (including trace labels)
449
    #
450
    # Once the data are plotted, we label the axes.  The horizontal axes is
451
    # always time and this is labeled only on the bottom most row.  The
452
    # vertical axes can consist either of a single signal or a combination
453
    # of signals (when overlay_signal is True or plot+inputs = 'overlay'.
454
    #
455
    # Traces are labeled at the top of the first row of plots (regular) or
456
    # the left edge of rows (tranpose).
457
    #
458

459
    # Time units on the bottom
460
    for col in range(ncols):
9✔
461
        ax_array[-1, col].set_xlabel(time_label)
9✔
462

463
    # Keep track of whether inputs are overlaid on outputs
464
    overlaid = plot_inputs == 'overlay'
9✔
465
    overlaid_title = "Inputs, Outputs"
9✔
466

467
    if transpose:               # inputs on left, outputs on right
9✔
468
        # Label the inputs
469
        if overlay_signals and plot_inputs:
9✔
470
            label = overlaid_title if overlaid else "Inputs"
9✔
471
            for trace in range(ntraces):
9✔
472
                ax_array[input_map[0, trace]].set_ylabel(label)
9✔
473
        else:
474
            for i in range(ninputs):
9✔
475
                label = overlaid_title if overlaid else data.input_labels[i]
9✔
476
                for trace in range(ntraces):
9✔
477
                    ax_array[input_map[i, trace]].set_ylabel(label)
9✔
478

479
        # Label the outputs
480
        if overlay_signals and plot_outputs:
9✔
481
            label = overlaid_title if overlaid else "Outputs"
9✔
482
            for trace in range(ntraces):
9✔
483
                ax_array[output_map[0, trace]].set_ylabel(label)
9✔
484
        else:
485
            for i in range(noutputs):
9✔
486
                label = overlaid_title if overlaid else data.output_labels[i]
9✔
487
                for trace in range(ntraces):
9✔
488
                    ax_array[output_map[i, trace]].set_ylabel(label)
9✔
489

490
        # Set the trace titles, if needed
491
        if ntraces > 1 and not overlay_traces:
9✔
492
            for trace in range(ntraces):
5✔
493
                # Get the existing ylabel for left column
494
                label = ax_array[trace, 0].get_ylabel()
5✔
495

496
                # Add on the trace title
497
                if trace_labels:
5✔
498
                    label = trace_labels[trace] + "\n" + label
×
499
                elif data.trace_labels:
5✔
500
                    label = data.trace_labels[trace] + "\n" + label
5✔
501
                else:
502
                    label = f"Trace {trace}" + "\n" + label
×
503

504
                ax_array[trace, 0].set_ylabel(label)
5✔
505

506
    else:                       # regular plot (outputs over inputs)
507
        # Set the trace titles, if needed
508
        if ntraces > 1 and not overlay_traces:
9✔
509
            for trace in range(ntraces):
9✔
510
                if trace_labels:
9✔
511
                    label = trace_labels[trace]
×
512
                elif data.trace_labels:
9✔
513
                    label = data.trace_labels[trace]
9✔
514
                else:
515
                    label = f"Trace {trace}"
×
516

517
                with plt.rc_context(rcParams):
9✔
518
                    ax_array[0, trace].set_title(label)
9✔
519

520
        # Label the outputs
521
        if overlay_signals and plot_outputs:
9✔
522
            ax_array[output_map[0, 0]].set_ylabel("Outputs")
9✔
523
        else:
524
            for i in range(noutputs):
9✔
525
                ax_array[output_map[i, 0]].set_ylabel(
9✔
526
                    overlaid_title if overlaid else data.output_labels[i])
527

528
        # Label the inputs
529
        if overlay_signals and plot_inputs:
9✔
530
            label = overlaid_title if overlaid else "Inputs"
9✔
531
            ax_array[input_map[0, 0]].set_ylabel(label)
9✔
532
        else:
533
            for i in range(ninputs):
9✔
534
                label = overlaid_title if overlaid else data.input_labels[i]
9✔
535
                ax_array[input_map[i, 0]].set_ylabel(label)
9✔
536

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

557
    # Figure out where to put legends
558
    if legend_map is None:
9✔
559
        legend_map = np.full(ax_array.shape, None, dtype=object)
9✔
560
        if legend_loc == None:
9✔
561
            legend_loc = 'center right'
9✔
562
        else:
563
            show_legend = True if show_legend is None else show_legend
×
564

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

617
        # Turn legend on unless overridden by user
618
        show_legend = True if show_legend is None else show_legend
9✔
619

620
    # Create axis legends
621
    for i in range(nrows):
9✔
622
        for j in range(ncols):
9✔
623
            ax = ax_array[i, j]
9✔
624
            labels = [line.get_label() for line in ax.get_lines()]
9✔
625
            if line_labels is None:
9✔
626
                labels = _make_legend_labels(labels, plot_inputs == 'overlay')
9✔
627

628
            # Update the labels to remove common strings
629
            if show_legend != False and \
9✔
630
               (len(labels) > 1 or show_legend) and \
631
               legend_map[i, j] != None:
632
                with plt.rc_context(rcParams):
9✔
633
                    ax.legend(labels, loc=legend_map[i, j])
9✔
634

635
    #
636
    # Update the plot title (= figure suptitle)
637
    #
638
    # If plots are built up by multiple calls to plot() and the title is
639
    # not given, then the title is updated to provide a list of unique text
640
    # items in each successive title.  For data generated by the I/O
641
    # response functions this will generate a common prefix followed by a
642
    # list of systems (e.g., "Step response for sys[1], sys[2]").
643
    #
644

645
    _update_suptitle(fig, title, rcParams=rcParams)
9✔
646

647
    return out
9✔
648

649

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

653
    This function combines multiple instances of :class:`TimeResponseData`
654
    into a multi-trace :class:`TimeResponseData` object.
655

656
    Parameters
657
    ----------
658
    response_list : list of :class:`TimeResponseData` objects
659
        Reponses to be combined.
660
    trace_labels : list of str, optional
661
        List of labels for each trace.  If not specified, trace names are
662
        taken from the input data or set to None.
663

664
    Returns
665
    -------
666
    data : :class:`TimeResponseData`
667
        Multi-trace input/output data.
668

669
    """
670
    from .timeresp import TimeResponseData
9✔
671

672
    # Save the first trace as the base case
673
    base = response_list[0]
9✔
674

675
    # Process keywords
676
    title = base.title if title is None else title
9✔
677

678
    # Figure out the size of the data (and check for consistency)
679
    ntraces = max(1, base.ntraces)
9✔
680

681
    # Initial pass through trace list to count things up and do error checks
682
    nstates = base.nstates
9✔
683
    for response in response_list[1:]:
9✔
684
        # Make sure the time vector is the same
685
        if not np.allclose(base.t, response.t):
9✔
686
            raise ValueError("all responses must have the same time vector")
9✔
687

688
        # Make sure the dimensions are all the same
689
        if base.ninputs != response.ninputs or \
9✔
690
           base.noutputs != response.noutputs:
691
            raise ValueError("all responses must have the same number of "
9✔
692
                            "inputs, outputs, and states")
693

694
        if nstates != response.nstates:
9✔
695
            warn("responses have different state dimensions; dropping states")
×
696
            nstates = 0
×
697

698
        ntraces += max(1, response.ntraces)
9✔
699

700
    # Create data structures for the new time response data object
701
    inputs = np.empty((base.ninputs, ntraces, base.t.size))
9✔
702
    outputs = np.empty((base.noutputs, ntraces, base.t.size))
9✔
703
    states = np.empty((nstates, ntraces, base.t.size))
9✔
704

705
    # See whether we should create labels or not
706
    if trace_labels is None:
9✔
707
        generate_trace_labels = True
9✔
708
        trace_labels = []
9✔
709
    elif len(trace_labels) != ntraces:
9✔
710
        raise ValueError(
9✔
711
            "number of trace labels does not match number of traces")
712
    else:
713
        generate_trace_labels = False
9✔
714

715
    offset = 0
9✔
716
    trace_types = []
9✔
717
    for response in response_list:
9✔
718
        if response.ntraces == 0:
9✔
719
            # Single trace
720
            inputs[:, offset, :] = response.u
9✔
721
            outputs[:, offset, :] = response.y
9✔
722
            if nstates:
9✔
723
                states[:, offset, :] = response.x
9✔
724
            offset += 1
9✔
725

726
            # Add on trace label and trace type
727
            if generate_trace_labels:
9✔
728
                trace_labels.append(response.title)
9✔
729
            trace_types.append(
9✔
730
                None if response.trace_types is None
731
                else response.trace_types[0])
732

733
        else:
734
            # Save the data
735
            for i in range(response.ntraces):
9✔
736
                inputs[:, offset, :] = response.u[:, i, :]
9✔
737
                outputs[:, offset, :] = response.y[:, i, :]
9✔
738
                if nstates:
9✔
739
                    states[:, offset, :] = response.x[:, i, :]
9✔
740

741
                # Save the trace labels
742
                if generate_trace_labels:
9✔
743
                    if response.trace_labels is not None:
9✔
744
                        trace_labels.append(response.trace_labels[i])
9✔
745
                    else:
746
                        trace_labels.append(response.title + f", trace {i}")
9✔
747

748
                offset += 1
9✔
749

750
            # Save the trace types
751
            if response.trace_types is not None:
9✔
752
                trace_types += response.trace_types
9✔
753
            else:
754
                trace_types += [None] * response.ntraces
9✔
755

756
    return TimeResponseData(
9✔
757
        base.t, outputs, states if nstates else None, inputs,
758
        output_labels=base.output_labels, input_labels=base.input_labels,
759
        state_labels=base.state_labels if nstates else None,
760
        title=title, transpose=base.transpose, return_x=base.return_x,
761
        issiso=base.issiso, squeeze=base.squeeze, sysname=base.sysname,
762
        trace_labels=trace_labels, trace_types=trace_types,
763
        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