Skip to content

Commit a1fd47e

Browse files
committed
step_info from MIMO step_response
1 parent 468ffbf commit a1fd47e

File tree

2 files changed

+149
-86
lines changed

2 files changed

+149
-86
lines changed

control/tests/timeresp_test.py

Lines changed: 134 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@
2626

2727
class TSys:
2828
"""Struct of test system"""
29-
def __init__(self, sys=None):
29+
def __init__(self, sys=None, call_kwargs=None):
3030
self.sys = sys
31+
self.kwargs = call_kwargs if call_kwargs else {}
3132

3233
def __repr__(self):
3334
"""Show system when debugging"""
@@ -210,39 +211,42 @@ def siso_tf_type1(self):
210211

211212
@pytest.fixture
212213
def siso_tf_kpos(self):
213-
# SISO under shoot response and positive final value G(s)=(-s+1)/(s²+s+1)
214+
# SISO under shoot response and positive final value
215+
# G(s)=(-s+1)/(s²+s+1)
214216
T = TSys(TransferFunction([-1, 1], [1, 1, 1]))
215217
T.step_info = {
216218
'RiseTime': 1.242,
217219
'SettlingTime': 9.110,
218-
'SettlingMin': 0.950,
220+
'SettlingMin': 0.90,
219221
'SettlingMax': 1.208,
220222
'Overshoot': 20.840,
221-
'Undershoot': 27.840,
223+
'Undershoot': 28.0,
222224
'Peak': 1.208,
223225
'PeakTime': 4.282,
224226
'SteadyStateValue': 1.0}
225227
return T
226228

227229
@pytest.fixture
228230
def siso_tf_kneg(self):
229-
# SISO under shoot response and negative final value k=-1 G(s)=-(-s+1)/(s²+s+1)
231+
# SISO under shoot response and negative final value
232+
# k=-1 G(s)=-(-s+1)/(s²+s+1)
230233
T = TSys(TransferFunction([1, -1], [1, 1, 1]))
231234
T.step_info = {
232235
'RiseTime': 1.242,
233236
'SettlingTime': 9.110,
234237
'SettlingMin': -1.208,
235-
'SettlingMax': -0.950,
238+
'SettlingMax': -0.90,
236239
'Overshoot': 20.840,
237-
'Undershoot': 27.840,
240+
'Undershoot': 28.0,
238241
'Peak': 1.208,
239242
'PeakTime': 4.282,
240243
'SteadyStateValue': -1.0}
241244
return T
242245

243246
@pytest.fixture
244-
def tf1_matlab_help(self):
245-
# example from matlab online help https://www.mathworks.com/help/control/ref/stepinfo.html
247+
def siso_tf_step_matlab(self):
248+
# example from matlab online help
249+
# https://www.mathworks.com/help/control/ref/stepinfo.html
246250
T = TSys(TransferFunction([1, 5, 5], [1, 1.65, 5, 6.5, 2]))
247251
T.step_info = {
248252
'RiseTime': 3.8456,
@@ -257,37 +261,82 @@ def tf1_matlab_help(self):
257261
return T
258262

259263
@pytest.fixture
260-
def ss2_matlab_help(self):
261-
A = [[0.68, - 0.34], [0.34, 0.68]]
262-
B = [[0.18], [0.04]]
263-
C = [-1.12, - 1.10]
264-
D = [0.06]
264+
def mimo_ss_step_matlab(self):
265+
A = [[0.68, -0.34],
266+
[0.34, 0.68]]
267+
B = [[0.18, -0.05],
268+
[0.04, 0.11]]
269+
C = [[0, -1.53],
270+
[-1.12, -1.10]]
271+
D = [[0, 0],
272+
[0.06, -0.37]]
265273
T = TSys(StateSpace(A, B, C, D, 0.2))
266-
T.step_info = {
267-
'RiseTime': 0.4000,
268-
'SettlingTime': 2.8000,
269-
'SettlingMin': -0.6724,
270-
'SettlingMax': -0.5188,
271-
'Overshoot': 24.6476,
272-
'Undershoot': 11.1224,
273-
'Peak': 0.6724,
274-
'PeakTime': 1,
275-
'SteadyStateValue': -0.5394}
274+
T.kwargs['step_info'] = {'T': 4.6}
275+
T.step_info = [[{'RiseTime': 0.6000,
276+
'SettlingTime': 3.0000,
277+
'SettlingMin': -0.5999,
278+
'SettlingMax': -0.4689,
279+
'Overshoot': 15.5072,
280+
'Undershoot': 0.,
281+
'Peak': 0.5999,
282+
'PeakTime': 1.4000,
283+
'SteadyStateValue': -0.5193},
284+
{'RiseTime': 0.,
285+
'SettlingTime': 3.6000,
286+
'SettlingMin': -0.2797,
287+
'SettlingMax': -0.1043,
288+
'Overshoot': 118.9918,
289+
'Undershoot': 0,
290+
'Peak': 0.2797,
291+
'PeakTime': .6000,
292+
'SteadyStateValue': -0.1277}],
293+
[{'RiseTime': 0.4000,
294+
'SettlingTime': 2.8000,
295+
'SettlingMin': -0.6724,
296+
'SettlingMax': -0.5188,
297+
'Overshoot': 24.6476,
298+
'Undershoot': 11.1224,
299+
'Peak': 0.6724,
300+
'PeakTime': 1,
301+
'SteadyStateValue': -0.5394},
302+
{'RiseTime': 0.0000, # (*)
303+
'SettlingTime': 3.4000,
304+
'SettlingMin': -0.1034,
305+
'SettlingMax': -0.1485,
306+
'Overshoot': 132.0170,
307+
'Undershoot': 79.222, # 0. in MATLAB
308+
'Peak': 0.4350,
309+
'PeakTime': .2,
310+
'SteadyStateValue': -0.1875}]]
311+
# (*): MATLAB gives 0.4 here, but it is unclear what
312+
# 10% and 90% of the steady state response mean, when
313+
# the step for this channel does not start a 0 for
314+
# 0 initial conditions
276315
return T
277316

278317
@pytest.fixture
279-
def mimo_tf_step(self, tf1_matlab_help,
280-
siso_tf_kpos,
281-
siso_tf_kneg,
282-
siso_tf_type1):
283-
Ta = [[tf1_matlab_help, tf1_matlab_help, siso_tf_kpos],
284-
[siso_tf_kneg, siso_tf_type1, siso_tf_type1]]
318+
def siso_ss_step_matlab(self, mimo_ss_step_matlab):
319+
T = copy(mimo_ss_step_matlab)
320+
T.sys = T.sys[1, 0]
321+
T.step_info = T.step_info[1][0]
322+
return T
323+
324+
@pytest.fixture
325+
def mimo_tf_step_info(self,
326+
siso_tf_kpos, siso_tf_kneg,
327+
siso_tf_step_matlab):
328+
Ta = [[siso_tf_kpos, siso_tf_kneg, siso_tf_step_matlab],
329+
[siso_tf_step_matlab, siso_tf_kpos, siso_tf_kneg]]
285330
T = TSys(TransferFunction(
286331
[[Ti.sys.num[0][0] for Ti in Tr] for Tr in Ta],
287332
[[Ti.sys.den[0][0] for Ti in Tr] for Tr in Ta]))
288333
T.step_info = [[Ti.step_info for Ti in Tr] for Tr in Ta]
334+
# enforce enough sample points for all channels (they have different
335+
# characteristics)
336+
T.kwargs['step_info'] = {'T_num': 2000}
289337
return T
290338

339+
291340
@pytest.fixture
292341
def tsystem(self,
293342
request,
@@ -297,8 +346,9 @@ def tsystem(self,
297346
siso_dss1, siso_dss2,
298347
mimo_dss1, mimo_dss2, mimo_dtf1,
299348
pole_cancellation, no_pole_cancellation, siso_tf_type1,
300-
siso_tf_kpos, siso_tf_kneg, tf1_matlab_help,
301-
ss2_matlab_help, mimo_tf_step):
349+
siso_tf_kpos, siso_tf_kneg,
350+
siso_tf_step_matlab, siso_ss_step_matlab,
351+
mimo_ss_step_matlab, mimo_tf_step_info):
302352
systems = {"siso_ss1": siso_ss1,
303353
"siso_ss2": siso_ss2,
304354
"siso_tf1": siso_tf1,
@@ -319,9 +369,10 @@ def tsystem(self,
319369
"siso_tf_type1": siso_tf_type1,
320370
"siso_tf_kpos": siso_tf_kpos,
321371
"siso_tf_kneg": siso_tf_kneg,
322-
"tf1_matlab_help": tf1_matlab_help,
323-
"ss2_matlab_help": ss2_matlab_help,
324-
"mimo_tf_step": mimo_tf_step,
372+
"siso_tf_step_matlab": siso_tf_step_matlab,
373+
"siso_ss_step_matlab": siso_ss_step_matlab,
374+
"mimo_ss_step_matlab": mimo_ss_step_matlab,
375+
"mimo_tf_step": mimo_tf_step_info,
325376
}
326377
return systems[request.param]
327378

@@ -375,60 +426,71 @@ def test_step_nostates(self, dt):
375426
t, y = step_response(sys)
376427
np.testing.assert_array_equal(y, np.ones(len(t)))
377428

378-
# tolerance for all parameters could be wrong for some systems
379-
# discrete systems time parameters tolerance could be +/-dt
380-
@pytest.mark.parametrize(
381-
"tsystem, tolerance",
382-
[("tf1_matlab_help", 2e-2),
383-
("ss2_matlab_help", .2),
384-
("siso_tf_kpos", 2e-2),
385-
("siso_tf_kneg", 2e-2),
386-
("siso_tf_type1", 2e-2)],
387-
indirect=["tsystem"])
388-
def test_step_info(self, tsystem, tolerance):
389-
"""Test step info for SISO systems"""
390-
info = step_info(tsystem.sys)
429+
def assert_step_info_match(self, sys, info, info_ref):
430+
"""Assert reasonable step_info accuracy"""
391431

392-
info_true_sorted = sorted(tsystem.step_info.keys())
393-
info_sorted = sorted(info.keys())
394-
assert info_sorted == info_true_sorted
432+
if sys.isdtime(strict=True):
433+
dt = sys.dt
434+
else:
435+
_, dt = _ideal_tfinal_and_dt(sys, is_step=True)
395436

396-
for k in info:
397-
np.testing.assert_allclose(info[k], tsystem.step_info[k],
398-
rtol=tolerance,
437+
for k in ['RiseTime', 'SettlingTime', 'PeakTime']:
438+
np.testing.assert_allclose(info[k], info_ref[k], atol=dt,
399439
err_msg=f"{k} does not match")
440+
for k in ['Overshoot', 'Undershoot', 'Peak', 'SteadyStateValue']:
441+
np.testing.assert_allclose(info[k], info_ref[k], rtol=5e-3,
442+
err_msg=f"{k} does not match")
443+
444+
# steep gradient right after RiseTime
445+
absrefinf = np.abs(info_ref['SteadyStateValue'])
446+
if info_ref['RiseTime'] > 0:
447+
y_next_sample_max = 0.8*absrefinf/info_ref['RiseTime']*dt
448+
else:
449+
y_next_sample_max = 0
450+
for k in ['SettlingMin', 'SettlingMax']:
451+
if (np.abs(info_ref[k]) - 0.9 * absrefinf) > y_next_sample_max:
452+
# local min/max peak well after signal has risen
453+
np.testing.assert_allclose(info[k], info_ref[k], rtol=1e-3)
454+
455+
@pytest.mark.parametrize(
456+
"tsystem",
457+
["siso_tf_step_matlab",
458+
"siso_ss_step_matlab",
459+
"siso_tf_kpos",
460+
"siso_tf_kneg",
461+
"siso_tf_type1"],
462+
indirect=["tsystem"])
463+
def test_step_info(self, tsystem):
464+
"""Test step info for SISO systems"""
465+
step_info_kwargs = tsystem.kwargs.get('step_info',{})
466+
info = step_info(tsystem.sys, **step_info_kwargs)
467+
self.assert_step_info_match(tsystem.sys, info, tsystem.step_info)
400468

401469
@pytest.mark.parametrize(
402-
"tsystem, tolerance",
403-
[('mimo_tf_step', 2e-2)],
470+
"tsystem",
471+
['mimo_ss_step_matlab',
472+
'mimo_tf_step'],
404473
indirect=["tsystem"])
405-
def test_step_info_mimo(self, tsystem, tolerance):
474+
def test_step_info_mimo(self, tsystem):
406475
"""Test step info for MIMO systems"""
407-
info_dict = step_info(tsystem.sys)
408-
from pprint import pprint
409-
pprint(info_dict)
476+
step_info_kwargs = tsystem.kwargs.get('step_info',{})
477+
info_dict = step_info(tsystem.sys, **step_info_kwargs)
410478
for i, row in enumerate(info_dict):
411479
for j, info in enumerate(row):
412480
for k in info:
413-
np.testing.assert_allclose(
414-
info[k], tsystem.step_info[i][j][k],
415-
rtol=tolerance,
416-
err_msg=f"{k} for input {j} to output {i} "
417-
"does not match")
481+
self.assert_step_info_match(tsystem.sys,
482+
info, tsystem.step_info[i][j])
418483

419484
def test_step_pole_cancellation(self, pole_cancellation,
420485
no_pole_cancellation):
421486
# confirm that pole-zero cancellation doesn't perturb results
422487
# https://github.com/python-control/python-control/issues/440
423488
step_info_no_cancellation = step_info(no_pole_cancellation)
424489
step_info_cancellation = step_info(pole_cancellation)
425-
for key in step_info_no_cancellation:
426-
if key == 'Overshoot':
427-
# skip this test because these systems have no overshoot
428-
# => very sensitive to parameters
429-
continue
430-
np.testing.assert_allclose(step_info_no_cancellation[key],
431-
step_info_cancellation[key], rtol=1e-4)
490+
self.assert_step_info_match(no_pole_cancellation,
491+
step_info_no_cancellation,
492+
step_info_cancellation)
493+
432494

433495
@pytest.mark.parametrize(
434496
"tsystem, kwargs",

control/timeresp.py

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -792,20 +792,18 @@ def step_info(sys, T=None, T_num=None, SettlingTimeThreshold=0.02,
792792
--------
793793
>>> info = step_info(sys, T)
794794
'''
795+
if T is None or np.asarray(T).size == 1:
796+
T = _default_time_vector(sys, N=T_num, tfinal=T, is_step=True)
797+
T, Yout = step_response(sys, T, squeeze=False)
798+
795799
ret = []
796800
for i in range(sys.noutputs):
797801
retrow = []
798802
for j in range(sys.ninputs):
799-
sys_siso = sys[i, j]
800-
if T is None or np.asarray(T).size == 1:
801-
Ti = _default_time_vector(sys_siso, N=T_num, tfinal=T,
802-
is_step=True)
803-
else:
804-
Ti = T
805-
Ti, yout = step_response(sys_siso, Ti)
803+
yout = Yout[i, j, :]
806804

807805
# Steady state value
808-
InfValue = sys_siso.dcgain()
806+
InfValue = sys.dcgain() if sys.issiso() else sys.dcgain()[i, j]
809807
sgnInf = np.sign(InfValue.real)
810808

811809
rise_time: float = np.NaN
@@ -826,12 +824,15 @@ def step_info(sys, T=None, T_num=None, SettlingTimeThreshold=0.02,
826824
tr_upper_index = np.where(
827825
sgnInf * (yout - RiseTimeLimits[1] * InfValue) >= 0
828826
)[0][0]
829-
rise_time = Ti[tr_upper_index] - Ti[tr_lower_index]
827+
rise_time = T[tr_upper_index] - T[tr_lower_index]
830828

831829
# SettlingTime
832-
settling_th = np.abs(SettlingTimeThreshold * InfValue)
833-
not_settled = np.where(np.abs(yout - InfValue) >= settling_th)
834-
settling_time = Ti[not_settled[0][-1] + 1]
830+
settled = np.where(
831+
np.abs(yout/InfValue -1) >= SettlingTimeThreshold)[0][-1]+1
832+
# MIMO systems can have unsettled channels without infinite
833+
# InfValue
834+
if settled < len(T):
835+
settling_time = T[settled]
835836

836837
settling_min = (yout[tr_upper_index:]).min()
837838
settling_max = (yout[tr_upper_index:]).max()
@@ -855,10 +856,10 @@ def step_info(sys, T=None, T_num=None, SettlingTimeThreshold=0.02,
855856
# Peak
856857
peak_index = np.abs(yout).argmax()
857858
peak_value = np.abs(yout[peak_index])
858-
peak_time = Ti[peak_index]
859+
peak_time = T[peak_index]
859860

860861
# SteadyStateValue
861-
steady_state_value = InfValue
862+
steady_state_value = InfValue.real
862863

863864
retij = {
864865
'RiseTime': rise_time,

0 commit comments

Comments
 (0)