Skip to content

Commit 3f7e275

Browse files
committed
add user documentation (with figures) + combine_traces function
1 parent 9f99219 commit 3f7e275

10 files changed

Lines changed: 295 additions & 18 deletions

control/tests/timeplot_test.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import matplotlib.pyplot as plt
88
import numpy as np
99

10+
from .conftest import slycotonly
11+
1012
# Detailed test of (almost) all functionality
1113
# (uncomment rows for developmental testing, but otherwise takes too long)
1214
@pytest.mark.parametrize(
@@ -123,6 +125,7 @@ def test_response_plots(
123125
plt.clf()
124126

125127

128+
@slycotonly
126129
def test_legend_map():
127130
sys_mimo = ct.tf2ss(
128131
[[[1], [0.1]], [[0.2], [1]]],
@@ -194,7 +197,47 @@ def test_errors():
194197
test_response_plots(*args, clear=F)
195198

196199
#
197-
# Run a few more special cases to show off capabilities
200+
# Run a few more special cases to show off capabilities (and save some
201+
# of them for use in the documentation).
198202
#
199203

200204
test_legend_map() # show ability to set legend location
205+
206+
# Basic step response
207+
plt.figure()
208+
ct.step_response(sys_mimo).plot()
209+
plt.savefig('timeplot-mimo_step-default.png')
210+
211+
# Step response with plot_inputs, combine_signals
212+
plt.figure()
213+
ct.step_response(sys_mimo).plot(
214+
plot_inputs=True, combine_signals=True,
215+
title="Step response for 2x2 MIMO system " +
216+
"[plot_inputs, combine_signals]")
217+
plt.savefig('timeplot-mimo_step-pi_cs.png')
218+
219+
# Input/output response with overlaid inputs, legend_map
220+
plt.figure()
221+
timepts = np.linspace(0, 10, 100)
222+
U = np.vstack([np.sin(timepts), np.cos(2*timepts)])
223+
ct.input_output_response(sys_mimo, timepts, U).plot(
224+
plot_inputs='overlay',
225+
legend_map=np.array([['lower right'], ['lower right']]),
226+
title="I/O response for 2x2 MIMO system " +
227+
"[plot_inputs='overlay', legend_map]")
228+
plt.savefig('timeplot-mimo_ioresp-ov_lm.png')
229+
230+
# Multi-trace plot, transpose
231+
plt.figure()
232+
U = np.vstack([np.sin(timepts), np.cos(2*timepts)])
233+
resp1 = ct.input_output_response(sys_mimo, timepts, U)
234+
235+
U = np.vstack([np.cos(2*timepts), np.sin(timepts)])
236+
resp2 = ct.input_output_response(sys_mimo, timepts, U)
237+
238+
ct.combine_traces(
239+
[resp1, resp2], trace_labels=["Scenario #1", "Scenario #2"]).plot(
240+
transpose=True,
241+
title="I/O responses for 2x2 MIMO system, multiple traces "
242+
"[transpose]")
243+
plt.savefig('timeplot-mimo_ioresp-mt_tr.png')

control/timeplot.py

Lines changed: 104 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828

2929
from . import config
3030

31-
__all__ = ['ioresp_plot']
31+
__all__ = ['ioresp_plot', 'combine_traces']
3232

3333
# Default font dictionary
3434
_timeplot_rcParams = mpl.rcParams.copy()
@@ -71,11 +71,11 @@ def ioresp_plot(
7171
The matplotlib Axes to draw the figure on. If not specified, the
7272
Axes for the current figure are used or, if there is no current
7373
figure with the correct number and shape of Axes, a new figure is
74-
created. The default shape of the array should be (data.ntraces,
75-
data.ninputs + data.inputs), but if combine_traces == True then
76-
only one row is needed and if combine_signals == True then only one
77-
or two columns are needed (depending on plot_inputs and
78-
plot_outputs).
74+
created. The default shape of the array should be (noutputs +
75+
ninputs, ntraces), but if `combine_traces` is set to `True` then
76+
only one row is needed and if `combine_signals` is set to `True`
77+
then only one or two columns are needed (depending on plot_inputs
78+
and plot_outputs).
7979
plot_inputs : bool or str, optional
8080
Sets how and where to plot the inputs:
8181
* False: don't plot the inputs
@@ -121,8 +121,7 @@ def ioresp_plot(
121121
value is used if legend_map is None.
122122
add_initial_zero : bool
123123
Add an initial point of zero at the first time point for all
124-
inputs. This is useful when the initial value of the input is
125-
nonzero (for example in a step input). Default is True.
124+
inputs with type 'step'. Default is True.
126125
trace_cycler: :class:`~matplotlib.Cycler`
127126
Line style cycle to use for traces. Default = ['-', '--', ':', '-.'].
128127
@@ -367,7 +366,8 @@ def _make_line_label(signal_index, signal_labels, trace_index):
367366
for i in range(ninputs):
368367
label = _make_line_label(i, data.input_labels, trace)
369368

370-
if add_initial_zero: # start trace from the origin
369+
if add_initial_zero and data.trace_types \
370+
and data.trace_types[i] == 'step':
371371
x = np.hstack([np.array([data.time[0]]), data.time])
372372
y = np.hstack([np.array([0]), inputs[i][trace]])
373373
else:
@@ -603,3 +603,98 @@ def _make_line_label(signal_index, signal_labels, trace_index):
603603
fig.suptitle(new_title)
604604

605605
return out
606+
607+
608+
def combine_traces(trace_list, trace_labels=None, title=None):
609+
"""Combine multiple individual time responses into a multi-trace response.
610+
611+
This function combines multiple instances of :class:`TimeResponseData`
612+
into a multi-trace :class:`TimeResponseData` object.
613+
614+
Parameters
615+
----------
616+
trace_list : list of :class:`TimeResponseData` objects
617+
Traces to be combined.
618+
trace_labels : list of str, optional
619+
List of labels for each trace. If not specified, trace names are
620+
taken from the input data or set to None.
621+
622+
Returns
623+
-------
624+
data : :class:`TimeResponseData`
625+
Multi-trace input/output data.
626+
627+
"""
628+
from .timeresp import TimeResponseData
629+
630+
# Save the first trace as the base case
631+
base = trace_list[0]
632+
633+
# Process keywords
634+
title = base.title if title is None else title
635+
636+
# Figure out the size of the data (and check for consistency)
637+
ntraces = max(1, base.ntraces)
638+
639+
# Initial pass through trace list to count things up and do error checks
640+
for trace in trace_list[1:]:
641+
# Make sure the time vector is the same
642+
if not np.allclose(base.t, trace.t):
643+
raise ValueError("all traces must have the same time vector")
644+
645+
# Make sure the dimensions are all the same
646+
if base.ninputs != trace.ninputs or base.noutputs != trace.noutputs \
647+
or base.nstates != trace.nstates:
648+
raise ValuError("all traces must have the same number of "
649+
"inputs, outputs, and states")
650+
651+
ntraces += max(1, trace.ntraces)
652+
653+
# Create data structures for the new time response data object
654+
inputs = np.empty((base.ninputs, ntraces, base.t.size))
655+
outputs = np.empty((base.noutputs, ntraces, base.t.size))
656+
states = np.empty((base.nstates, ntraces, base.t.size))
657+
658+
# See whether we should create labels or not
659+
if trace_labels is None:
660+
generate_trace_labels = True
661+
trace_labels = []
662+
elif len(trace_labels) != ntraces:
663+
raise ValueError(
664+
"number of trace labels does not match number of traces")
665+
else:
666+
generate_trace_labels = False
667+
668+
offset = 0
669+
trace_types = []
670+
for trace in trace_list:
671+
if trace.ntraces == 0:
672+
# Single trace
673+
inputs[:, offset, :] = trace.u
674+
outputs[:, offset, :] = trace.y
675+
states[:, offset, :] = trace.x
676+
if generate_trace_labels:
677+
trace_labels.append(trace.title)
678+
if trace.trace_types is not None:
679+
trace_types.append(trace.types[0])
680+
offset += 1
681+
else:
682+
for i in range(trace.ntraces):
683+
inputs[:, offset, :] = trace.u[:, i, :]
684+
outputs[:, offset, :] = trace.y[:, i, :]
685+
states[:, offset, :] = trace.x[:, i, :]
686+
if generate_trace_labels and trace.trace_labels is not None:
687+
trace_labels.append(trace.trace_labels)
688+
else:
689+
trace_labels.append(trace.title, f", trace {i}")
690+
if trace.trace_types is not None:
691+
trace_types.append(trace.trace_types)
692+
offset += trace.ntraces
693+
694+
return TimeResponseData(
695+
base.t, outputs, states, inputs, issiso=base.issiso,
696+
output_labels=base.output_labels, input_labels=base.input_labels,
697+
state_labels=base.state_labels, title=title, transpose=base.transpose,
698+
return_x=base.return_x, squeeze=base.squeeze, sysname=base.sysname,
699+
trace_labels=trace_labels, trace_types=trace_types,
700+
plot_inputs=base.plot_inputs)

control/timeresp.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -177,14 +177,18 @@ class TimeResponseData:
177177
Whether or not to plot the inputs by default (can be overridden in
178178
the plot() method)
179179
180-
ntraces : int
180+
ntraces : int, optional
181181
Number of independent traces represented in the input/output
182-
response. If ntraces is 0 then the data represents a single trace
183-
with the trace index surpressed in the data.
182+
response. If ntraces is 0 (default) then the data represents a
183+
single trace with the trace index surpressed in the data.
184184
185-
trace_labels : array of string
185+
trace_labels : array of string, optional
186186
Labels to use for traces (set to sysname it ntraces is 0)
187187
188+
trace_labels : array of string, optional
189+
Type of trace. Currently only 'step' is supported, which controls
190+
the way in which the signal is plotted.
191+
188192
Notes
189193
-----
190194
1. For backward compatibility with earlier versions of python-control,
@@ -222,7 +226,8 @@ def __init__(
222226
self, time, outputs, states=None, inputs=None, issiso=None,
223227
output_labels=None, state_labels=None, input_labels=None,
224228
title=None, transpose=False, return_x=False, squeeze=None,
225-
multi_trace=False, trace_labels=None, plot_inputs=True,
229+
multi_trace=False, trace_labels=None, trace_types=None,
230+
plot_inputs=True,
226231
sysname=None
227232
):
228233
"""Create an input/output time response object.
@@ -423,6 +428,7 @@ def __init__(
423428
# Check and store trace labels, if present
424429
self.trace_labels = _process_labels(
425430
trace_labels, "trace", self.ntraces)
431+
self.trace_types = trace_types
426432

427433
# Figure out if the system is SISO
428434
if issiso is None:
@@ -1382,13 +1388,15 @@ def step_response(sys, T=None, X0=0, input=None, output=None, T_num=None,
13821388

13831389
# Simulate the response for each input
13841390
trace_labels = []
1391+
trace_types = []
13851392
for i in range(sys.ninputs):
13861393
# If input keyword was specified, only simulate for that input
13871394
if isinstance(input, int) and i != input:
13881395
continue
13891396

1390-
# Save a label for this plot
1397+
# Save a label and type for this plot
13911398
trace_labels.append(f"From {sys.input_labels[i]}")
1399+
trace_types.append('step')
13921400

13931401
# Create a set of single inputs system for simulation
13941402
U = np.zeros((sys.ninputs, T.size))
@@ -1415,7 +1423,8 @@ def step_response(sys, T=None, X0=0, input=None, output=None, T_num=None,
14151423
output_labels=output_labels, input_labels=input_labels,
14161424
state_labels=sys.state_labels, title="Step response for " + sys.name,
14171425
transpose=transpose, return_x=return_x, squeeze=squeeze,
1418-
sysname=sys.name, trace_labels=trace_labels, plot_inputs=False)
1426+
sysname=sys.name, trace_labels=trace_labels,
1427+
trace_types=trace_types, plot_inputs=False)
14191428

14201429

14211430
def step_info(sysdata, T=None, T_num=None, yfinal=None,

doc/Makefile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,13 @@ help:
1515
.PHONY: help Makefile
1616

1717
# Rules to create figures
18-
FIGS = classes.pdf
18+
FIGS = classes.pdf timeplot-mimo_step-pi_cs.png
1919
classes.pdf: classes.fig
2020
fig2dev -Lpdf $< $@
2121

22+
timeplot-mimo_step-pi_cs.png: ../control/tests/timeplot_test.py
23+
PYTHONPATH=.. python $<
24+
2225
# Catch-all target: route all unknown targets to Sphinx using the new
2326
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
2427
html pdf clean doctest: Makefile $(FIGS)

doc/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ implements basic operations for analysis and design of feedback control systems.
2626
conventions
2727
control
2828
classes
29+
plotting
2930
matlab
3031
flatsys
3132
iosys

0 commit comments

Comments
 (0)