Skip to content

Commit ce5a95c

Browse files
committed
consistent squeezing for state property + legacy interface + doc updates
1 parent 8aa68eb commit ce5a95c

File tree

4 files changed

+122
-42
lines changed

4 files changed

+122
-42
lines changed

control/tests/trdata_test.py

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,17 @@ def test_trdata_shapes(nin, nout, squeeze):
5151
# Check shape of class properties
5252
if sys.issiso():
5353
assert res.outputs.shape == (ntimes,)
54+
assert res._legacy_states.shape == (sys.nstates, ntimes)
5455
assert res.states.shape == (sys.nstates, ntimes)
5556
assert res.inputs is None
5657
elif res.squeeze is True:
5758
assert res.outputs.shape == (ntimes, )
59+
assert res._legacy_states.shape == (sys.nstates, ntimes)
5860
assert res.states.shape == (sys.nstates, ntimes)
5961
assert res.inputs is None
6062
else:
6163
assert res.outputs.shape == (sys.noutputs, ntimes)
64+
assert res._legacy_states.shape == (sys.nstates, ntimes)
6265
assert res.states.shape == (sys.nstates, ntimes)
6366
assert res.inputs is None
6467

@@ -78,21 +81,26 @@ def test_trdata_shapes(nin, nout, squeeze):
7881
# Check shape of inputs and outputs
7982
if sys.issiso() and squeeze is not False:
8083
assert res.outputs.shape == (ntimes, )
84+
assert res.states.shape == (sys.nstates, ntimes)
8185
assert res.inputs.shape == (ntimes, )
8286
elif res.squeeze is True:
8387
assert res.outputs.shape == \
8488
np.empty((sys.noutputs, sys.ninputs, ntimes)).squeeze().shape
89+
assert res.states.shape == \
90+
np.empty((sys.nstates, sys.ninputs, ntimes)).squeeze().shape
8591
assert res.inputs.shape == \
8692
np.empty((sys.ninputs, sys.ninputs, ntimes)).squeeze().shape
8793
else:
8894
assert res.outputs.shape == (sys.noutputs, sys.ninputs, ntimes)
95+
assert res.states.shape == (sys.nstates, sys.ninputs, ntimes)
8996
assert res.inputs.shape == (sys.ninputs, sys.ninputs, ntimes)
9097

91-
# Check state space dimensions (not affected by squeeze)
98+
# Check legacy state space dimensions (not affected by squeeze)
9299
if sys.issiso():
93-
assert res.states.shape == (sys.nstates, ntimes)
100+
assert res._legacy_states.shape == (sys.nstates, ntimes)
94101
else:
95-
assert res.states.shape == (sys.nstates, sys.ninputs, ntimes)
102+
assert res._legacy_states.shape == \
103+
(sys.nstates, sys.ninputs, ntimes)
96104

97105
#
98106
# Forced response
@@ -107,14 +115,18 @@ def test_trdata_shapes(nin, nout, squeeze):
107115

108116
if sys.issiso() and squeeze is not False:
109117
assert res.outputs.shape == (ntimes,)
118+
assert res.states.shape == (sys.nstates, ntimes)
110119
assert res.inputs.shape == (ntimes,)
111120
elif squeeze is True:
112121
assert res.outputs.shape == \
113122
np.empty((sys.noutputs, 1, ntimes)).squeeze().shape
123+
assert res.states.shape == \
124+
np.empty((sys.nstates, 1, ntimes)).squeeze().shape
114125
assert res.inputs.shape == \
115126
np.empty((sys.ninputs, 1, ntimes)).squeeze().shape
116127
else: # MIMO or squeeze is False
117128
assert res.outputs.shape == (sys.noutputs, ntimes)
129+
assert res.states.shape == (sys.nstates, ntimes)
118130
assert res.inputs.shape == (sys.ninputs, ntimes)
119131

120132
# Check state space dimensions (not affected by squeeze)
@@ -141,16 +153,28 @@ def test_response_copy():
141153
# Squeeze
142154
response_siso_as_mimo = response_siso(squeeze=False)
143155
assert response_siso_as_mimo.outputs.shape == (1, 1, siso_ntimes)
144-
assert response_siso_as_mimo.states.shape == (4, siso_ntimes)
156+
assert response_siso_as_mimo.states.shape == (4, 1, siso_ntimes)
157+
assert response_siso_as_mimo._legacy_states.shape == (4, siso_ntimes)
145158

146159
response_mimo_squeezed = response_mimo(squeeze=True)
147160
assert response_mimo_squeezed.outputs.shape == (2, mimo_ntimes)
148-
assert response_mimo_squeezed.states.shape == (4, 1, mimo_ntimes)
161+
assert response_mimo_squeezed.states.shape == (4, mimo_ntimes)
162+
assert response_mimo_squeezed._legacy_states.shape == (4, 1, mimo_ntimes)
149163

150164
# Squeeze and transpose
151165
response_mimo_sqtr = response_mimo(squeeze=True, transpose=True)
152166
assert response_mimo_sqtr.outputs.shape == (mimo_ntimes, 2)
153-
assert response_mimo_sqtr.states.shape == (mimo_ntimes, 4, 1)
167+
assert response_mimo_sqtr.states.shape == (mimo_ntimes, 4)
168+
assert response_mimo_sqtr._legacy_states.shape == (mimo_ntimes, 4, 1)
169+
170+
# Return_x
171+
t, y = response_mimo
172+
t, y = response_mimo()
173+
t, y, x = response_mimo(return_x=True)
174+
with pytest.raises(ValueError, match="too many"):
175+
t, y = response_mimo(return_x=True)
176+
with pytest.raises(ValueError, match="not enough"):
177+
t, y, x = response_mimo
154178

155179
# Unknown keyword
156180
with pytest.raises(ValueError, match="unknown"):

control/timeresp.py

Lines changed: 91 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@
8888

8989

9090
class TimeResponseData():
91-
"""Class for returning time responses.
91+
"""A class for returning time responses.
9292
9393
This class maintains and manipulates the data corresponding to the
9494
temporal response of an input/output system. It is used as the return
@@ -140,20 +140,18 @@ class TimeResponseData():
140140
performs squeeze processing.
141141
142142
squeeze : bool, optional
143-
By default, if a system is single-input, single-output (SISO) then
144-
the inputs and outputs are returned as a 1D array (indexed by time)
145-
and if a system is multi-input or multi-output, then the inputs are
146-
returned as a 2D array (indexed by input and time) and the outputs
147-
are returned as either a 2D array (indexed by output and time) or a
148-
3D array (indexed by output, trace, and time). If ``squeeze=True``,
149-
access to the output response will remove single-dimensional entries
150-
from the shape of the inputs and outputs even if the system is not
151-
SISO. If ``squeeze=False``, the input is returned as a 2D or 3D
152-
array (indexed by the input [if multi-input], trace [if
153-
multi-trace] and time) and the output as a 2D or 3D array (indexed
154-
by the output, trace [if multi-trace], and time) even if the system
155-
is SISO. The default value can be set using
156-
config.defaults['control.squeeze_time_response'].
143+
By default, if a system is single-input, single-output (SISO)
144+
then the outputs (and inputs) are returned as a 1D array
145+
(indexed by time) and if a system is multi-input or
146+
multi-output, then the outputs are returned as a 2D array
147+
(indexed by output and time) or a 3D array (indexed by output,
148+
trace, and time). If ``squeeze=True``, access to the output
149+
response will remove single-dimensional entries from the shape
150+
of the inputs and outputs even if the system is not SISO. If
151+
``squeeze=False``, the output is returned as a 2D or 3D array
152+
(indexed by the output [if multi-input], trace [if multi-trace]
153+
and time) even if the system is SISO. The default value can be
154+
set using config.defaults['control.squeeze_time_response'].
157155
158156
transpose : bool, optional
159157
If True, transpose all input and output arrays (for backward
@@ -183,6 +181,9 @@ class TimeResponseData():
183181
t, y = step_response(sys)
184182
t, y, x = step_response(sys, return_x=True)
185183
184+
When using this (legacy) interface, the state vector is not affected by
185+
the `squeeze` parameter.
186+
186187
2. For backward compatibility with earlier version of python-control,
187188
this class has ``__getitem__`` and ``__len__`` methods that allow the
188189
return value to be indexed:
@@ -191,11 +192,16 @@ class TimeResponseData():
191192
response[1]: returns the output vector
192193
response[2]: returns the state vector
193194
195+
When using this (legacy) interface, the state vector is not affected by
196+
the `squeeze` parameter.
197+
194198
3. The default settings for ``return_x``, ``squeeze`` and ``transpose``
195199
can be changed by calling the class instance and passing new values:
196200
197201
response(tranpose=True).input
198202
203+
See :meth:`TimeResponseData.__call__` for more information.
204+
199205
"""
200206

201207
def __init__(
@@ -251,8 +257,8 @@ def __init__(
251257
the system is SISO. The default value can be set using
252258
config.defaults['control.squeeze_time_response'].
253259
254-
Additional parameters
255-
---------------------
260+
Other parameters
261+
----------------
256262
transpose : bool, optional
257263
If True, transpose all input and output arrays (for backward
258264
compatibility with MATLAB and :func:`scipy.signal.lsim`).
@@ -391,8 +397,10 @@ def __init__(
391397
raise ValueError("unknown squeeze value")
392398
self.squeeze = squeeze
393399

394-
# Store legacy keyword values (only needed for legacy interface)
400+
# Keep track of whether to transpose for MATLAB/scipy.signal
395401
self.transpose = transpose
402+
403+
# Store legacy keyword values (only needed for legacy interface)
396404
self.return_x = return_x
397405

398406
def __call__(self, **kwargs):
@@ -405,13 +413,13 @@ def __call__(self, **kwargs):
405413
Parameters
406414
----------
407415
squeeze : bool, optional
408-
If squeeze=True, access to the output response will
409-
remove single-dimensional entries from the shape of the inputs
410-
and outputs even if the system is not SISO. If squeeze=False,
411-
keep the input as a 2D or 3D array (indexed by the input (if
412-
multi-input), trace (if single input) and time) and the output
413-
as a 3D array (indexed by the output, trace, and time) even if
414-
the system is SISO.
416+
If squeeze=True, access to the output response will remove
417+
single-dimensional entries from the shape of the inputs, outputs,
418+
and states even if the system is not SISO. If squeeze=False, keep
419+
the input as a 2D or 3D array (indexed by the input (if
420+
multi-input), trace (if single input) and time) and the output and
421+
states as a 3D array (indexed by the output/state, trace, and
422+
time) even if the system is SISO.
415423
416424
transpose : bool, optional
417425
If True, transpose all input and output arrays (for backward
@@ -421,13 +429,15 @@ def __call__(self, **kwargs):
421429
return_x : bool, optional
422430
If True, return the state vector when enumerating result by
423431
assigning to a tuple (default = False).
432+
424433
"""
425434
# Make a copy of the object
426435
response = copy(self)
427436

428437
# Update any keywords that we were passed
429438
response.transpose = kwargs.pop('transpose', self.transpose)
430439
response.squeeze = kwargs.pop('squeeze', self.squeeze)
440+
response.return_x = kwargs.pop('return_x', self.squeeze)
431441

432442
# Make sure no unknown keywords were passed
433443
if len(kwargs) != 0:
@@ -452,32 +462,40 @@ def outputs(self):
452462
453463
Output response of the system, indexed by either the output and time
454464
(if only a single input is given) or the output, trace, and time
455-
(for multiple traces).
465+
(for multiple traces). See :attr:`TimeResponseData.squeeze` for a
466+
description of how this can be modified using the `squeeze` keyword.
456467
457468
:type: 1D, 2D, or 3D array
469+
458470
"""
459471
t, y = _process_time_response(
460472
self.t, self.y, issiso=self.issiso,
461473
transpose=self.transpose, squeeze=self.squeeze)
462474
return y
463475

464-
# Getter for state (implements non-standard squeeze processing)
476+
# Getter for states (implements squeeze processing)
465477
@property
466478
def states(self):
467479
"""Time response state vector.
468480
469481
Time evolution of the state vector, indexed indexed by either the
470-
state and time (if only a single trace is given) or the state,
471-
trace, and time (for multiple traces).
482+
state and time (if only a single trace is given) or the state, trace,
483+
and time (for multiple traces). See :attr:`TimeResponseData.squeeze`
484+
for a description of how this can be modified using the `squeeze`
485+
keyword.
472486
473487
:type: 2D or 3D array
474-
"""
475488
489+
"""
476490
if self.x is None:
477491
return None
478492

493+
elif self.squeeze is True:
494+
x = self.x.squeeze()
495+
479496
elif self.ninputs == 1 and self.noutputs == 1 and \
480-
self.ntraces == 1 and self.x.ndim == 3:
497+
self.ntraces == 1 and self.x.ndim == 3 and \
498+
self.squeeze is not False:
481499
# Single-input, single-output system with single trace
482500
x = self.x[:, 0, :]
483501

@@ -491,7 +509,7 @@ def states(self):
491509

492510
return x
493511

494-
# Getter for state (implements squeeze processing)
512+
# Getter for inputs (implements squeeze processing)
495513
@property
496514
def inputs(self):
497515
"""Time response input vector.
@@ -504,7 +522,12 @@ def inputs(self):
504522
the two. If a 3D vector is passed, then it represents a multi-trace,
505523
multi-input signal, indexed by input, trace, and time.
506524
525+
See :attr:`TimeResponseData.squeeze` for a description of how the
526+
dimensions of the input vector can be modified using the `squeeze`
527+
keyword.
528+
507529
:type: 1D or 2D array
530+
508531
"""
509532
if self.u is None:
510533
return None
@@ -514,11 +537,45 @@ def inputs(self):
514537
transpose=self.transpose, squeeze=self.squeeze)
515538
return u
516539

540+
# Getter for legacy state (implements non-standard squeeze processing)
541+
@property
542+
def _legacy_states(self):
543+
"""Time response state vector (legacy version).
544+
545+
Time evolution of the state vector, indexed indexed by either the
546+
state and time (if only a single trace is given) or the state,
547+
trace, and time (for multiple traces).
548+
549+
The `legacy_states` property is not affected by the `squeeze` keyword
550+
and hence it will always have these dimensions.
551+
552+
:type: 2D or 3D array
553+
554+
"""
555+
556+
if self.x is None:
557+
return None
558+
559+
elif self.ninputs == 1 and self.noutputs == 1 and \
560+
self.ntraces == 1 and self.x.ndim == 3:
561+
# Single-input, single-output system with single trace
562+
x = self.x[:, 0, :]
563+
564+
else:
565+
# Return the full set of data
566+
x = self.x
567+
568+
# Transpose processing
569+
if self.transpose:
570+
x = np.transpose(x, np.roll(range(x.ndim), 1))
571+
572+
return x
573+
517574
# Implement iter to allow assigning to a tuple
518575
def __iter__(self):
519576
if not self.return_x:
520577
return iter((self.time, self.outputs))
521-
return iter((self.time, self.outputs, self.states))
578+
return iter((self.time, self.outputs, self._legacy_states))
522579

523580
# Implement (thin) getitem to allow access via legacy indexing
524581
def __getitem__(self, index):
@@ -533,7 +590,7 @@ def __getitem__(self, index):
533590
if index == 1:
534591
return self.outputs
535592
if index == 2:
536-
return self.states
593+
return self._legacy_states
537594
raise IndexError
538595

539596
# Implement (thin) len to emulate legacy testing interface

doc/classes.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ these directly.
1717
TransferFunction
1818
StateSpace
1919
FrequencyResponseData
20+
TimeResponseData
2021

2122
Input/output system subclasses
2223
==============================
@@ -47,4 +48,3 @@ Additional classes
4748
flatsys.SystemTrajectory
4849
optimal.OptimalControlProblem
4950
optimal.OptimalControlResult
50-
TimeResponseData

doc/control.rst

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,6 @@ Time domain simulation
7070
input_output_response
7171
step_response
7272
phase_plot
73-
TimeResponseData
7473

7574
Block diagram algebra
7675
=====================

0 commit comments

Comments
 (0)