Skip to content

Commit 1502d38

Browse files
authored
Merge pull request #514 from murrayrm/mimo_impulse_step_response
MIMO impulse and step response
2 parents 0a08ff2 + 28bbe7f commit 1502d38

File tree

5 files changed

+342
-111
lines changed

5 files changed

+342
-111
lines changed

control/config.py

-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
_control_defaults = {
1818
'control.default_dt': 0,
1919
'control.squeeze_frequency_response': None,
20-
'control.squeeze_time_response': True,
2120
'control.squeeze_time_response': None,
2221
'forced_response.return_x': False,
2322
}

control/iosys.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -1428,8 +1428,9 @@ def input_output_response(sys, T, U=0., X0=0, params={}, method='RK45',
14281428
for i in range(len(T)):
14291429
u = U[i] if len(U.shape) == 1 else U[:, i]
14301430
y[:, i] = sys._out(T[i], [], u)
1431-
return _process_time_response(sys, T, y, [], transpose=transpose,
1432-
return_x=return_x, squeeze=squeeze)
1431+
return _process_time_response(
1432+
sys, T, y, np.array((0, 0, np.asarray(T).size)),
1433+
transpose=transpose, return_x=return_x, squeeze=squeeze)
14331434

14341435
# create X0 if not given, test if X0 has correct shape
14351436
X0 = _check_convert_array(X0, [(nstates,), (nstates, 1)],

control/tests/matlab2_test.py

+7-7
Original file line numberDiff line numberDiff line change
@@ -121,25 +121,25 @@ def test_step(self, SISO_mats, MIMO_mats, mplcleanup):
121121
#print("gain:", dcgain(sys))
122122

123123
subplot2grid(plot_shape, (0, 0))
124-
t, y = step(sys)
124+
y, t = step(sys)
125125
plot(t, y)
126126

127127
subplot2grid(plot_shape, (0, 1))
128128
T = linspace(0, 2, 100)
129129
X0 = array([1, 1])
130-
t, y = step(sys, T, X0)
130+
y, t = step(sys, T, X0)
131131
plot(t, y)
132132

133133
# Test output of state vector
134-
t, y, x = step(sys, return_x=True)
134+
y, t, x = step(sys, return_x=True)
135135

136136
#Test MIMO system
137137
A, B, C, D = MIMO_mats
138138
sys = ss(A, B, C, D)
139139

140140
subplot2grid(plot_shape, (0, 2))
141-
t, y = step(sys)
142-
plot(t, y)
141+
y, t = step(sys)
142+
plot(t, y[:, 0, 0])
143143

144144
def test_impulse(self, SISO_mats, mplcleanup):
145145
A, B, C, D = SISO_mats
@@ -168,8 +168,8 @@ def test_impulse_mimo(self, MIMO_mats, mplcleanup):
168168
#Test MIMO system
169169
A, B, C, D = MIMO_mats
170170
sys = ss(A, B, C, D)
171-
t, y = impulse(sys)
172-
plot(t, y, label='MIMO System')
171+
y, t = impulse(sys)
172+
plot(t, y[:, :, 0], label='MIMO System')
173173

174174
legend(loc='best')
175175
#show()

control/tests/timeresp_test.py

+109-42
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,7 @@ def test_impulse_response_mimo(self, mimo_ss2):
347347
yref_notrim = np.zeros((2, len(t)))
348348
yref_notrim[:1, :] = yref
349349
_t, yy = impulse_response(sys, T=t, input=0)
350-
np.testing.assert_array_almost_equal(yy, yref_notrim, decimal=4)
350+
np.testing.assert_array_almost_equal(yy[:,0,:], yref_notrim, decimal=4)
351351

352352
@pytest.mark.skipif(StrictVersion(sp.__version__) < "1.3",
353353
reason="requires SciPy 1.3 or greater")
@@ -639,9 +639,10 @@ def test_time_vector(self, tsystem, fun, squeeze, matarrayout):
639639
if hasattr(tsystem, 't'):
640640
# tout should always match t, which has shape (n, )
641641
np.testing.assert_allclose(tout, tsystem.t)
642-
if squeeze is False or sys.outputs > 1:
642+
643+
if squeeze is False or not sys.issiso():
643644
assert yout.shape[0] == sys.outputs
644-
assert yout.shape[1] == tout.shape[0]
645+
assert yout.shape[-1] == tout.shape[0]
645646
else:
646647
assert yout.shape == tout.shape
647648

@@ -725,21 +726,22 @@ def test_time_series_data_convention_2D(self, siso_ss1):
725726

726727
@pytest.mark.usefixtures("editsdefaults")
727728
@pytest.mark.parametrize("fcn", [ct.ss, ct.tf, ct.ss2io])
728-
@pytest.mark.parametrize("nstate, nout, ninp, squeeze, shape", [
729-
[1, 1, 1, None, (8,)],
730-
[2, 1, 1, True, (8,)],
731-
[3, 1, 1, False, (1, 8)],
732-
[3, 2, 1, None, (2, 8)],
733-
[4, 2, 1, True, (2, 8)],
734-
[5, 2, 1, False, (2, 8)],
735-
[3, 1, 2, None, (1, 8)],
736-
[4, 1, 2, True, (8,)],
737-
[5, 1, 2, False, (1, 8)],
738-
[4, 2, 2, None, (2, 8)],
739-
[5, 2, 2, True, (2, 8)],
740-
[6, 2, 2, False, (2, 8)],
729+
@pytest.mark.parametrize("nstate, nout, ninp, squeeze, shape1, shape2", [
730+
# state out in squeeze in/out out-only
731+
[1, 1, 1, None, (8,), (8,)],
732+
[2, 1, 1, True, (8,), (8,)],
733+
[3, 1, 1, False, (1, 1, 8), (1, 8)],
734+
[3, 2, 1, None, (2, 1, 8), (2, 8)],
735+
[4, 2, 1, True, (2, 8), (2, 8)],
736+
[5, 2, 1, False, (2, 1, 8), (2, 8)],
737+
[3, 1, 2, None, (1, 2, 8), (1, 8)],
738+
[4, 1, 2, True, (2, 8), (8,)],
739+
[5, 1, 2, False, (1, 2, 8), (1, 8)],
740+
[4, 2, 2, None, (2, 2, 8), (2, 8)],
741+
[5, 2, 2, True, (2, 2, 8), (2, 8)],
742+
[6, 2, 2, False, (2, 2, 8), (2, 8)],
741743
])
742-
def test_squeeze(self, fcn, nstate, nout, ninp, squeeze, shape):
744+
def test_squeeze(self, fcn, nstate, nout, ninp, squeeze, shape1, shape2):
743745
# Figure out if we have SciPy 1+
744746
scipy0 = StrictVersion(sp.__version__) < '1.0'
745747

@@ -750,27 +752,56 @@ def test_squeeze(self, fcn, nstate, nout, ninp, squeeze, shape):
750752
else:
751753
sys = fcn(ct.rss(nstate, nout, ninp, strictly_proper=True))
752754

753-
# Keep track of expect users warnings
754-
warntype = UserWarning if sys.inputs > 1 else None
755-
756755
# Generate the time and input vectors
757756
tvec = np.linspace(0, 1, 8)
758757
uvec = np.dot(
759758
np.ones((sys.inputs, 1)),
760759
np.reshape(np.sin(tvec), (1, 8)))
761760

761+
#
762762
# Pass squeeze argument and make sure the shape is correct
763-
with pytest.warns(warntype, match="Converting MIMO system"):
764-
_, yvec = ct.impulse_response(sys, tvec, squeeze=squeeze)
765-
assert yvec.shape == shape
763+
#
764+
# For responses that are indexed by the input, check against shape1
765+
# For responses that have no/fixed input, check against shape2
766+
#
766767

767-
_, yvec = ct.initial_response(sys, tvec, 1, squeeze=squeeze)
768-
assert yvec.shape == shape
768+
# Impulse response
769+
if isinstance(sys, StateSpace):
770+
# Check the states as well
771+
_, yvec, xvec = ct.impulse_response(
772+
sys, tvec, squeeze=squeeze, return_x=True)
773+
if sys.issiso():
774+
assert xvec.shape == (sys.states, 8)
775+
else:
776+
assert xvec.shape == (sys.states, sys.inputs, 8)
777+
else:
778+
_, yvec = ct.impulse_response(sys, tvec, squeeze=squeeze)
779+
assert yvec.shape == shape1
769780

770-
with pytest.warns(warntype, match="Converting MIMO system"):
781+
# Step response
782+
if isinstance(sys, StateSpace):
783+
# Check the states as well
784+
_, yvec, xvec = ct.step_response(
785+
sys, tvec, squeeze=squeeze, return_x=True)
786+
if sys.issiso():
787+
assert xvec.shape == (sys.states, 8)
788+
else:
789+
assert xvec.shape == (sys.states, sys.inputs, 8)
790+
else:
771791
_, yvec = ct.step_response(sys, tvec, squeeze=squeeze)
772-
assert yvec.shape == shape
792+
assert yvec.shape == shape1
793+
794+
# Initial response (only indexed by output)
795+
if isinstance(sys, StateSpace):
796+
# Check the states as well
797+
_, yvec, xvec = ct.initial_response(
798+
sys, tvec, 1, squeeze=squeeze, return_x=True)
799+
assert xvec.shape == (sys.states, 8)
800+
else:
801+
_, yvec = ct.initial_response(sys, tvec, 1, squeeze=squeeze)
802+
assert yvec.shape == shape2
773803

804+
# Forced response (only indexed by output)
774805
if isinstance(sys, StateSpace):
775806
# Check the states as well
776807
_, yvec, xvec = ct.forced_response(
@@ -779,52 +810,54 @@ def test_squeeze(self, fcn, nstate, nout, ninp, squeeze, shape):
779810
else:
780811
# Just check the input/output response
781812
_, yvec = ct.forced_response(sys, tvec, uvec, 0, squeeze=squeeze)
782-
assert yvec.shape == shape
813+
assert yvec.shape == shape2
783814

784815
# Test cases where we choose a subset of inputs and outputs
785816
_, yvec = ct.step_response(
786817
sys, tvec, input=ninp-1, output=nout-1, squeeze=squeeze)
787-
# Possible code if we implemenet a squeeze='siso' option
788818
if squeeze is False:
789819
# Shape should be unsqueezed
790-
assert yvec.shape == (1, 8)
820+
assert yvec.shape == (1, 1, 8)
791821
else:
792822
# Shape should be squeezed
793823
assert yvec.shape == (8, )
794824

795-
# For InputOutputSystems, also test input_output_response
825+
# For InputOutputSystems, also test input/output response
796826
if isinstance(sys, ct.InputOutputSystem) and not scipy0:
797827
_, yvec = ct.input_output_response(sys, tvec, uvec, squeeze=squeeze)
798-
assert yvec.shape == shape
828+
assert yvec.shape == shape2
799829

800830
#
801831
# Changing config.default to False should return 3D frequency response
802832
#
803833
ct.config.set_defaults('control', squeeze_time_response=False)
804834

805-
with pytest.warns(warntype, match="Converting MIMO system"):
806-
_, yvec = ct.impulse_response(sys, tvec)
807-
assert yvec.shape == (sys.outputs, 8)
835+
_, yvec = ct.impulse_response(sys, tvec)
836+
if squeeze is not True or sys.inputs > 1 or sys.outputs > 1:
837+
assert yvec.shape == (sys.outputs, sys.inputs, 8)
808838

809-
_, yvec = ct.initial_response(sys, tvec, 1)
810-
assert yvec.shape == (sys.outputs, 8)
839+
_, yvec = ct.step_response(sys, tvec)
840+
if squeeze is not True or sys.inputs > 1 or sys.outputs > 1:
841+
assert yvec.shape == (sys.outputs, sys.inputs, 8)
811842

812-
with pytest.warns(warntype, match="Converting MIMO system"):
813-
_, yvec = ct.step_response(sys, tvec)
814-
assert yvec.shape == (sys.outputs, 8)
843+
_, yvec = ct.initial_response(sys, tvec, 1)
844+
if squeeze is not True or sys.outputs > 1:
845+
assert yvec.shape == (sys.outputs, 8)
815846

816847
if isinstance(sys, ct.StateSpace):
817848
_, yvec, xvec = ct.forced_response(
818849
sys, tvec, uvec, 0, return_x=True)
819850
assert xvec.shape == (sys.states, 8)
820851
else:
821852
_, yvec = ct.forced_response(sys, tvec, uvec, 0)
822-
assert yvec.shape == (sys.outputs, 8)
853+
if squeeze is not True or sys.outputs > 1:
854+
assert yvec.shape == (sys.outputs, 8)
823855

824856
# For InputOutputSystems, also test input_output_response
825857
if isinstance(sys, ct.InputOutputSystem) and not scipy0:
826858
_, yvec = ct.input_output_response(sys, tvec, uvec)
827-
assert yvec.shape == (sys.noutputs, 8)
859+
if squeeze is not True or sys.outputs > 1:
860+
assert yvec.shape == (sys.outputs, 8)
828861

829862
@pytest.mark.parametrize("fcn", [ct.ss, ct.tf, ct.ss2io])
830863
def test_squeeze_exception(self, fcn):
@@ -861,3 +894,37 @@ def test_squeeze_0_8_4(self, nstate, nout, ninp, squeeze, shape):
861894

862895
_, yvec = ct.initial_response(sys, tvec, 1, squeeze=squeeze)
863896
assert yvec.shape == shape
897+
898+
@pytest.mark.parametrize(
899+
"nstate, nout, ninp, squeeze, ysh_in, ysh_no, xsh_in", [
900+
[4, 1, 1, None, (8,), (8,), (8, 4)],
901+
[4, 1, 1, True, (8,), (8,), (8, 4)],
902+
[4, 1, 1, False, (8, 1, 1), (8, 1), (8, 4)],
903+
[4, 2, 1, None, (8, 2, 1), (8, 2), (8, 4, 1)],
904+
[4, 2, 1, True, (8, 2), (8, 2), (8, 4, 1)],
905+
[4, 2, 1, False, (8, 2, 1), (8, 2), (8, 4, 1)],
906+
[4, 1, 2, None, (8, 1, 2), (8, 1), (8, 4, 2)],
907+
[4, 1, 2, True, (8, 2), (8,), (8, 4, 2)],
908+
[4, 1, 2, False, (8, 1, 2), (8, 1), (8, 4, 2)],
909+
[4, 2, 2, None, (8, 2, 2), (8, 2), (8, 4, 2)],
910+
[4, 2, 2, True, (8, 2, 2), (8, 2), (8, 4, 2)],
911+
[4, 2, 2, False, (8, 2, 2), (8, 2), (8, 4, 2)],
912+
])
913+
def test_response_transpose(
914+
self, nstate, nout, ninp, squeeze, ysh_in, ysh_no, xsh_in):
915+
sys = ct.rss(nstate, nout, ninp)
916+
T = np.linspace(0, 1, 8)
917+
918+
# Step response - input indexed
919+
t, y, x = ct.step_response(
920+
sys, T, transpose=True, return_x=True, squeeze=squeeze)
921+
assert t.shape == (T.size, )
922+
assert y.shape == ysh_in
923+
assert x.shape == xsh_in
924+
925+
# Initial response - no input indexing
926+
t, y, x = ct.initial_response(
927+
sys, T, 1, transpose=True, return_x=True, squeeze=squeeze)
928+
assert t.shape == (T.size, )
929+
assert y.shape == ysh_no
930+
assert x.shape == (T.size, sys.states)

0 commit comments

Comments
 (0)