Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
add signal labels + more unit tests/coverage + docstring tweaks
  • Loading branch information
murrayrm committed Aug 28, 2021
commit 136d6f4fe5e3ff4634dd5a8701d02f6ea744422f
9 changes: 7 additions & 2 deletions control/iosys.py
Original file line number Diff line number Diff line change
Expand Up @@ -1527,7 +1527,9 @@ def input_output_response(

The return value of the system can also be accessed by assigning the
function to a tuple of length 2 (time, output) or of length 3 (time,
output, state) if ``return_x`` is ``True``.
output, state) if ``return_x`` is ``True``. If the input/output
system signals are named, these names will be used as labels for the
time response.

Other parameters
----------------
Expand Down Expand Up @@ -1590,7 +1592,8 @@ def input_output_response(
u = U[i] if len(U.shape) == 1 else U[:, i]
y[:, i] = sys._out(T[i], [], u)
return TimeResponseData(
T, y, None, None, issiso=sys.issiso(),
T, y, None, U, issiso=sys.issiso(),
output_labels=sys.output_index, input_labels=sys.input_index,
transpose=transpose, return_x=return_x, squeeze=squeeze)

# create X0 if not given, test if X0 has correct shape
Expand Down Expand Up @@ -1687,6 +1690,8 @@ def ivp_rhs(t, x):

return TimeResponseData(
soln.t, y, soln.y, U, issiso=sys.issiso(),
output_labels=sys.output_index, input_labels=sys.input_index,
state_labels=sys.state_index,
transpose=transpose, return_x=return_x, squeeze=squeeze)


Expand Down
2 changes: 1 addition & 1 deletion control/tests/timeresp_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -1117,7 +1117,7 @@ def test_squeeze(self, fcn, nstate, nout, ninp, squeeze, shape1, shape2):
@pytest.mark.parametrize("fcn", [ct.ss, ct.tf, ct.ss2io])
def test_squeeze_exception(self, fcn):
sys = fcn(ct.rss(2, 1, 1))
with pytest.raises(ValueError, match="unknown squeeze value"):
with pytest.raises(ValueError, match="Unknown squeeze value"):
step_response(sys, squeeze=1)

@pytest.mark.usefixtures("editsdefaults")
Expand Down
189 changes: 185 additions & 4 deletions control/tests/trdata_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@
[2, 1, None],
[2, 1, True],
[2, 1, False],
[2, 2, None],
[2, 2, True],
[2, 2, False],
[2, 3, None],
[2, 3, True],
[2, 3, False],
])
def test_trdata_shapes(nin, nout, squeeze):
# SISO, single trace
Expand All @@ -48,6 +48,12 @@ def test_trdata_shapes(nin, nout, squeeze):
assert res.x.shape == (sys.nstates, ntimes)
assert res.u is None

# Check dimensions of the response
assert res.ntraces == 0 # single trace
assert res.ninputs == 0 # no input for initial response
assert res.noutputs == sys.noutputs
assert res.nstates == sys.nstates

# Check shape of class properties
if sys.issiso():
assert res.outputs.shape == (ntimes,)
Expand Down Expand Up @@ -78,6 +84,12 @@ def test_trdata_shapes(nin, nout, squeeze):
assert res.x.shape == (sys.nstates, sys.ninputs, ntimes)
assert res.u.shape == (sys.ninputs, sys.ninputs, ntimes)

# Check shape of class members
assert res.ntraces == sys.ninputs
assert res.ninputs == sys.ninputs
assert res.noutputs == sys.noutputs
assert res.nstates == sys.nstates

# Check shape of inputs and outputs
if sys.issiso() and squeeze is not False:
assert res.outputs.shape == (ntimes, )
Expand Down Expand Up @@ -108,11 +120,19 @@ def test_trdata_shapes(nin, nout, squeeze):
res = ct.forced_response(sys, T, U, X0, squeeze=squeeze)
ntimes = res.time.shape[0]

# Check shape of class members
assert len(res.time.shape) == 1
assert res.y.shape == (sys.noutputs, ntimes)
assert res.x.shape == (sys.nstates, ntimes)
assert res.u.shape == (sys.ninputs, ntimes)

# Check dimensions of the response
assert res.ntraces == 0 # single trace
assert res.ninputs == sys.ninputs
assert res.noutputs == sys.noutputs
assert res.nstates == sys.nstates

# Check shape of inputs and outputs
if sys.issiso() and squeeze is not False:
assert res.outputs.shape == (ntimes,)
assert res.states.shape == (sys.nstates, ntimes)
Expand Down Expand Up @@ -176,6 +196,167 @@ def test_response_copy():
with pytest.raises(ValueError, match="not enough"):
t, y, x = response_mimo

# Labels
assert response_mimo.output_labels is None
assert response_mimo.state_labels is None
assert response_mimo.input_labels is None
response = response_mimo(
output_labels=['y1', 'y2'], input_labels='u',
state_labels=["x[%d]" % i for i in range(4)])
assert response.output_labels == ['y1', 'y2']
assert response.state_labels == ['x[0]', 'x[1]', 'x[2]', 'x[3]']
assert response.input_labels == ['u']

# Unknown keyword
with pytest.raises(ValueError, match="unknown"):
with pytest.raises(ValueError, match="Unknown parameter(s)*"):
response_bad_kw = response_mimo(input=0)


def test_trdata_labels():
# Create an I/O system with labels
sys = ct.rss(4, 3, 2)
iosys = ct.LinearIOSystem(sys)

T = np.linspace(1, 10, 10)
U = [np.sin(T), np.cos(T)]

# Create a response
response = ct.input_output_response(iosys, T, U)

# Make sure the labels got created
np.testing.assert_equal(
response.output_labels, ["y[%d]" % i for i in range(sys.noutputs)])
np.testing.assert_equal(
response.state_labels, ["x[%d]" % i for i in range(sys.nstates)])
np.testing.assert_equal(
response.input_labels, ["u[%d]" % i for i in range(sys.ninputs)])


def test_trdata_multitrace():
#
# Output signal processing
#

# Proper call of multi-trace data w/ ambiguous 2D output
response = ct.TimeResponseData(
np.zeros(5), np.ones((2, 5)), np.zeros((3, 2, 5)),
np.ones((4, 2, 5)), multi_trace=True)
assert response.ntraces == 2
assert response.noutputs == 1
assert response.nstates == 3
assert response.ninputs == 4

# Proper call of single trace w/ ambiguous 2D output
response = ct.TimeResponseData(
np.zeros(5), np.ones((2, 5)), np.zeros((3, 5)),
np.ones((4, 5)), multi_trace=False)
assert response.ntraces == 0
assert response.noutputs == 2
assert response.nstates == 3
assert response.ninputs == 4

# Proper call of multi-trace data w/ ambiguous 1D output
response = ct.TimeResponseData(
np.zeros(5), np.ones(5), np.zeros((3, 5)),
np.ones((4, 5)), multi_trace=False)
assert response.ntraces == 0
assert response.noutputs == 1
assert response.nstates == 3
assert response.ninputs == 4
assert response.y.shape == (1, 5) # Make sure reshape occured

# Output vector not the right shape
with pytest.raises(ValueError, match="Output vector is the wrong shape"):
response = ct.TimeResponseData(
np.zeros(5), np.ones((1, 2, 3, 5)), None, None)

# Inconsistent output vector: different number of time points
with pytest.raises(ValueError, match="Output vector does not match time"):
response = ct.TimeResponseData(
np.zeros(5), np.ones(6), np.zeros(5), np.zeros(5))

#
# State signal processing
#

# For multi-trace, state must be 3D
with pytest.raises(ValueError, match="State vector is the wrong shape"):
response = ct.TimeResponseData(
np.zeros(5), np.ones((1, 5)), np.zeros((3, 5)), multi_trace=True)

# If not multi-trace, state must be 2D
with pytest.raises(ValueError, match="State vector is the wrong shape"):
response = ct.TimeResponseData(
np.zeros(5), np.ones(5), np.zeros((3, 1, 5)), multi_trace=False)

# State vector in the wrong shape
with pytest.raises(ValueError, match="State vector is the wrong shape"):
response = ct.TimeResponseData(
np.zeros(5), np.ones((1, 2, 5)), np.zeros((2, 1, 5)))

# Inconsistent state vector: different number of time points
with pytest.raises(ValueError, match="State vector does not match time"):
response = ct.TimeResponseData(
np.zeros(5), np.ones(5), np.zeros((1, 6)), np.zeros(5))

#
# Input signal processing
#

# Proper call of multi-trace data with 2D input
response = ct.TimeResponseData(
np.zeros(5), np.ones((2, 5)), np.zeros((3, 2, 5)),
np.ones((2, 5)), multi_trace=True)
assert response.ntraces == 2
assert response.noutputs == 1
assert response.nstates == 3
assert response.ninputs == 1

# Input vector in the wrong shape
with pytest.raises(ValueError, match="Input vector is the wrong shape"):
response = ct.TimeResponseData(
np.zeros(5), np.ones((1, 2, 5)), None, np.zeros((2, 1, 5)))

# Inconsistent input vector: different number of time points
with pytest.raises(ValueError, match="Input vector does not match time"):
response = ct.TimeResponseData(
np.zeros(5), np.ones(5), np.zeros((1, 5)), np.zeros(6))


def test_trdata_exceptions():
# Incorrect dimension for time vector
with pytest.raises(ValueError, match="Time vector must be 1D"):
ct.TimeResponseData(np.zeros((2,2)), np.zeros(2), None)

# Infer SISO system from inputs and outputs
response = ct.TimeResponseData(
np.zeros(5), np.ones(5), None, np.ones(5))
assert response.issiso

response = ct.TimeResponseData(
np.zeros(5), np.ones((1, 5)), None, np.ones((1, 5)))
assert response.issiso

response = ct.TimeResponseData(
np.zeros(5), np.ones((1, 2, 5)), None, np.ones((1, 2, 5)))
assert response.issiso

# Not enough input to infer whether SISO
with pytest.raises(ValueError, match="Can't determine if system is SISO"):
response = ct.TimeResponseData(
np.zeros(5), np.ones((1, 2, 5)), np.ones((4, 2, 5)), None)

# Not enough input to infer whether SISO
with pytest.raises(ValueError, match="Keyword `issiso` does not match"):
response = ct.TimeResponseData(
np.zeros(5), np.ones((2, 5)), None, np.ones((1, 5)), issiso=True)

# Unknown squeeze keyword value
with pytest.raises(ValueError, match="Unknown squeeze value"):
response=ct.TimeResponseData(
np.zeros(5), np.ones(5), None, np.ones(5), squeeze=1)

# Legacy interface index error
response[0], response[1], response[2]
with pytest.raises(IndexError):
response[3]
Loading