Skip to content

Commit 258c9c2

Browse files
authored
Merge pull request #567 from juanodecc/step_info_improve_jpp
Step info improve jpp
2 parents 0f951e1 + 81ae64f commit 258c9c2

File tree

2 files changed

+172
-29
lines changed

2 files changed

+172
-29
lines changed

control/tests/timeresp_test.py

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,35 @@ def pole_cancellation(self):
193193
def no_pole_cancellation(self):
194194
return TransferFunction([1.881e+06],
195195
[188.1, 1.881e+06])
196+
197+
@pytest.fixture
198+
def siso_tf_type1(self):
199+
# System Type 1 - Step response not stationary: G(s)=1/s(s+1)
200+
return TransferFunction(1, [1, 1, 0])
201+
202+
@pytest.fixture
203+
def siso_tf_kpos(self):
204+
# SISO under shoot response and positive final value G(s)=(-s+1)/(s²+s+1)
205+
return TransferFunction([-1, 1], [1, 1, 1])
206+
207+
@pytest.fixture
208+
def siso_tf_kneg(self):
209+
# SISO under shoot response and negative final value k=-1 G(s)=-(-s+1)/(s²+s+1)
210+
return TransferFunction([1, -1], [1, 1, 1])
211+
212+
@pytest.fixture
213+
def tf1_matlab_help(self):
214+
# example from matlab online help https://www.mathworks.com/help/control/ref/stepinfo.html
215+
return TransferFunction([1, 5, 5], [1, 1.65, 5, 6.5, 2])
216+
217+
@pytest.fixture
218+
def tf2_matlab_help(self):
219+
A = [[0.68, - 0.34], [0.34, 0.68]]
220+
B = [[0.18], [0.04]]
221+
C = [-1.12, - 1.10]
222+
D = [0.06]
223+
sys = StateSpace(A, B, C, D, 0.2)
224+
return sys
196225

197226
@pytest.fixture
198227
def tsystem(self,
@@ -202,7 +231,9 @@ def tsystem(self,
202231
siso_dtf0, siso_dtf1, siso_dtf2,
203232
siso_dss1, siso_dss2,
204233
mimo_dss1, mimo_dss2, mimo_dtf1,
205-
pole_cancellation, no_pole_cancellation):
234+
pole_cancellation, no_pole_cancellation, siso_tf_type1,
235+
siso_tf_kpos, siso_tf_kneg, tf1_matlab_help,
236+
tf2_matlab_help):
206237
systems = {"siso_ss1": siso_ss1,
207238
"siso_ss2": siso_ss2,
208239
"siso_tf1": siso_tf1,
@@ -220,6 +251,11 @@ def tsystem(self,
220251
"mimo_dtf1": mimo_dtf1,
221252
"pole_cancellation": pole_cancellation,
222253
"no_pole_cancellation": no_pole_cancellation,
254+
"siso_tf_type1": siso_tf_type1,
255+
"siso_tf_kpos": siso_tf_kpos,
256+
"siso_tf_kneg": siso_tf_kneg,
257+
"tf1_matlab_help": tf1_matlab_help,
258+
"tf2_matlab_help": tf2_matlab_help,
223259
}
224260
return systems[request.param]
225261

@@ -303,6 +339,73 @@ def test_step_info(self):
303339
[Strue[k] for k in Sktrue],
304340
rtol=rtol)
305341

342+
# tolerance for all parameters could be wrong for some systems
343+
# discrete systems time parameters tolerance could be +/-dt
344+
@pytest.mark.parametrize(
345+
"tsystem, info_true, tolerance",
346+
[("tf1_matlab_help", {
347+
'RiseTime': 3.8456,
348+
'SettlingTime': 27.9762,
349+
'SettlingMin': 2.0689,
350+
'SettlingMax': 2.6873,
351+
'Overshoot': 7.4915,
352+
'Undershoot': 0,
353+
'Peak': 2.6873,
354+
'PeakTime': 8.0530,
355+
'SteadyStateValue': 2.5}, 2e-2),
356+
("tf2_matlab_help", {
357+
'RiseTime': 0.4000,
358+
'SettlingTime': 2.8000,
359+
'SettlingMin': -0.6724,
360+
'SettlingMax': -0.5188,
361+
'Overshoot': 24.6476,
362+
'Undershoot': 11.1224,
363+
'Peak': 0.6724,
364+
'PeakTime': 1,
365+
'SteadyStateValue': -0.5394}, .2),
366+
("siso_tf_kpos", {
367+
'RiseTime': 1.242,
368+
'SettlingTime': 9.110,
369+
'SettlingMin': 0.950,
370+
'SettlingMax': 1.208,
371+
'Overshoot': 20.840,
372+
'Undershoot': 27.840,
373+
'Peak': 1.208,
374+
'PeakTime': 4.282,
375+
'SteadyStateValue': 1.0}, 2e-2),
376+
("siso_tf_kneg", {
377+
'RiseTime': 1.242,
378+
'SettlingTime': 9.110,
379+
'SettlingMin': -1.208,
380+
'SettlingMax': -0.950,
381+
'Overshoot': 20.840,
382+
'Undershoot': 27.840,
383+
'Peak': 1.208,
384+
'PeakTime': 4.282,
385+
'SteadyStateValue': -1.0}, 2e-2),
386+
("siso_tf_type1", {'RiseTime': np.NaN,
387+
'SettlingTime': np.NaN,
388+
'SettlingMin': np.NaN,
389+
'SettlingMax': np.NaN,
390+
'Overshoot': np.NaN,
391+
'Undershoot': np.NaN,
392+
'Peak': np.Inf,
393+
'PeakTime': np.Inf,
394+
'SteadyStateValue': np.NaN}, 2e-2)],
395+
indirect=["tsystem"])
396+
def test_step_info(self, tsystem, info_true, tolerance):
397+
""" """
398+
info = step_info(tsystem)
399+
400+
info_true_sorted = sorted(info_true.keys())
401+
info_sorted = sorted(info.keys())
402+
403+
assert info_sorted == info_true_sorted
404+
405+
np.testing.assert_allclose([info_true[k] for k in info_true_sorted],
406+
[info[k] for k in info_sorted],
407+
rtol=tolerance)
408+
306409
def test_step_pole_cancellation(self, pole_cancellation,
307410
no_pole_cancellation):
308411
# confirm that pole-zero cancellation doesn't perturb results

control/timeresp.py

Lines changed: 68 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -746,7 +746,8 @@ def step_info(sys, T=None, T_num=None, SettlingTimeThreshold=0.02,
746746
747747
Parameters
748748
----------
749-
sys : StateSpace or TransferFunction
749+
sys : SISO dynamic system model. Dynamic systems that you can use include:
750+
StateSpace or TransferFunction
750751
LTI system to simulate
751752
752753
T : array_like or float, optional
@@ -785,43 +786,82 @@ def step_info(sys, T=None, T_num=None, SettlingTimeThreshold=0.02,
785786
--------
786787
>>> info = step_info(sys, T)
787788
'''
788-
_, sys = _get_ss_simo(sys)
789+
790+
if not sys.issiso():
791+
sys = _mimo2siso(sys,0,0)
792+
warnings.warn(" Internal conversion from a MIMO system to a SISO system,"
793+
" the first input and the first output were used (u1 -> y1);"
794+
" it may not be the result you are looking for")
795+
789796
if T is None or np.asarray(T).size == 1:
790797
T = _default_time_vector(sys, N=T_num, tfinal=T, is_step=True)
791798

792799
T, yout = step_response(sys, T)
793800

794801
# Steady state value
795-
InfValue = sys.dcgain()
796-
797-
# RiseTime
798-
tr_lower_index = (np.where(yout >= RiseTimeLimits[0] * InfValue)[0])[0]
799-
tr_upper_index = (np.where(yout >= RiseTimeLimits[1] * InfValue)[0])[0]
800-
RiseTime = T[tr_upper_index] - T[tr_lower_index]
801-
802-
# SettlingTime
803-
sup_margin = (1. + SettlingTimeThreshold) * InfValue
804-
inf_margin = (1. - SettlingTimeThreshold) * InfValue
805-
# find Steady State looking for the first point out of specified limits
806-
for i in reversed(range(T.size)):
807-
if((yout[i] <= inf_margin) | (yout[i] >= sup_margin)):
808-
SettlingTime = T[i + 1]
809-
break
802+
InfValue = sys.dcgain().real
803+
804+
# TODO: this could be a function step_info4data(t,y,yfinal)
805+
rise_time: float = np.NaN
806+
settling_time: float = np.NaN
807+
settling_min: float = np.NaN
808+
settling_max: float = np.NaN
809+
peak_value: float = np.Inf
810+
peak_time: float = np.Inf
811+
undershoot: float = np.NaN
812+
overshoot: float = np.NaN
813+
steady_state_value: float = np.NaN
814+
815+
if not np.isnan(InfValue) and not np.isinf(InfValue):
816+
# SteadyStateValue
817+
steady_state_value = InfValue
818+
# Peak
819+
peak_index = np.abs(yout).argmax()
820+
peak_value = np.abs(yout[peak_index])
821+
peak_time = T[peak_index]
822+
823+
sup_margin = (1. + SettlingTimeThreshold) * InfValue
824+
inf_margin = (1. - SettlingTimeThreshold) * InfValue
825+
826+
# RiseTime
827+
tr_lower_index = (np.where(np.sign(InfValue.real) * (yout- RiseTimeLimits[0] * InfValue) >= 0 )[0])[0]
828+
tr_upper_index = (np.where(np.sign(InfValue.real) * yout >= np.sign(InfValue.real) * RiseTimeLimits[1] * InfValue)[0])[0]
829+
830+
# SettlingTime
831+
settling_time = T[np.where(np.abs(yout-InfValue) >= np.abs(SettlingTimeThreshold*InfValue))[0][-1]+1]
832+
# Overshoot and Undershoot
833+
y_os = (np.sign(InfValue.real)*yout).max()
834+
dy_os = np.abs(y_os) - np.abs(InfValue)
835+
if dy_os > 0:
836+
overshoot = np.abs(100. * dy_os / InfValue)
837+
else:
838+
overshoot = 0
839+
840+
y_us = (np.sign(InfValue.real)*yout).min()
841+
dy_us = np.abs(y_us)
842+
if dy_us > 0:
843+
undershoot = np.abs(100. * dy_us / InfValue)
844+
else:
845+
undershoot = 0
846+
847+
# RiseTime
848+
rise_time = T[tr_upper_index] - T[tr_lower_index]
849+
850+
settling_max = (yout[tr_upper_index:]).max()
851+
settling_min = (yout[tr_upper_index:]).min()
810852

811-
PeakIndex = np.abs(yout).argmax()
812853
return {
813-
'RiseTime': RiseTime,
814-
'SettlingTime': SettlingTime,
815-
'SettlingMin': yout[tr_upper_index:].min(),
816-
'SettlingMax': yout.max(),
817-
'Overshoot': 100. * (yout.max() - InfValue) / InfValue,
818-
'Undershoot': yout.min(), # not very confident about this
819-
'Peak': yout[PeakIndex],
820-
'PeakTime': T[PeakIndex],
821-
'SteadyStateValue': InfValue
854+
'RiseTime': rise_time,
855+
'SettlingTime': settling_time,
856+
'SettlingMin': settling_min,
857+
'SettlingMax': settling_max,
858+
'Overshoot': overshoot,
859+
'Undershoot': undershoot,
860+
'Peak': peak_value,
861+
'PeakTime': peak_time,
862+
'SteadyStateValue': steady_state_value
822863
}
823864

824-
825865
def initial_response(sys, T=None, X0=0., input=0, output=None, T_num=None,
826866
transpose=False, return_x=False, squeeze=None):
827867
# pylint: disable=W0622

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy