diff --git a/control/matlab/timeresp.py b/control/matlab/timeresp.py index 31b761bcd..b1fa24bb0 100644 --- a/control/matlab/timeresp.py +++ b/control/matlab/timeresp.py @@ -64,38 +64,59 @@ def step(sys, T=None, X0=0., input=0, output=None, return_x=False): transpose=True, return_x=return_x) return (out[1], out[0], out[2]) if return_x else (out[1], out[0]) -def stepinfo(sys, T=None, SettlingTimeThreshold=0.02, + +def stepinfo(sysdata, T=None, yfinal=None, SettlingTimeThreshold=0.02, RiseTimeLimits=(0.1, 0.9)): - ''' + """ Step response characteristics (Rise time, Settling Time, Peak and others). Parameters ---------- - sys: StateSpace, or TransferFunction - LTI system to simulate - - T: array-like or number, optional + sysdata : StateSpace or TransferFunction or array_like + The system data. Either LTI system to similate (StateSpace, + TransferFunction), or a time series of step response data. + T : array_like or float, optional Time vector, or simulation time duration if a number (time vector is - autocomputed if not given) - - SettlingTimeThreshold: float value, optional + autocomputed if not given). + Required, if sysdata is a time series of response data. + yfinal : scalar or array_like, optional + Steady-state response. If not given, sysdata.dcgain() is used for + systems to simulate and the last value of the the response data is + used for a given time series of response data. Scalar for SISO, + (noutputs, ninputs) array_like for MIMO systems. + SettlingTimeThreshold : float, optional Defines the error to compute settling time (default = 0.02) - - RiseTimeLimits: tuple (lower_threshold, upper_theshold) + RiseTimeLimits : tuple (lower_threshold, upper_theshold) Defines the lower and upper threshold for RiseTime computation Returns ------- - S: a dictionary containing: - RiseTime: Time from 10% to 90% of the steady-state value. - SettlingTime: Time to enter inside a default error of 2% - SettlingMin: Minimum value after RiseTime - SettlingMax: Maximum value after RiseTime - Overshoot: Percentage of the Peak relative to steady value - Undershoot: Percentage of undershoot - Peak: Absolute peak value - PeakTime: time of the Peak - SteadyStateValue: Steady-state value + S : dict or list of list of dict + If `sysdata` corresponds to a SISO system, S is a dictionary + containing: + + RiseTime: + Time from 10% to 90% of the steady-state value. + SettlingTime: + Time to enter inside a default error of 2% + SettlingMin: + Minimum value after RiseTime + SettlingMax: + Maximum value after RiseTime + Overshoot: + Percentage of the Peak relative to steady value + Undershoot: + Percentage of undershoot + Peak: + Absolute peak value + PeakTime: + time of the Peak + SteadyStateValue: + Steady-state value + + If `sysdata` corresponds to a MIMO system, `S` is a 2D list of dicts. + To get the step response characteristics from the j-th input to the + i-th output, access ``S[i][j]`` See Also @@ -105,11 +126,13 @@ def stepinfo(sys, T=None, SettlingTimeThreshold=0.02, Examples -------- >>> S = stepinfo(sys, T) - ''' + """ from ..timeresp import step_info # Call step_info with MATLAB defaults - S = step_info(sys, T, None, SettlingTimeThreshold, RiseTimeLimits) + S = step_info(sysdata, T=T, T_num=None, yfinal=yfinal, + SettlingTimeThreshold=SettlingTimeThreshold, + RiseTimeLimits=RiseTimeLimits) return S diff --git a/control/tests/sisotool_test.py b/control/tests/sisotool_test.py index 6df2493cb..14e9692c1 100644 --- a/control/tests/sisotool_test.py +++ b/control/tests/sisotool_test.py @@ -68,8 +68,8 @@ def test_sisotool(self, sys): # Check the step response before moving the point step_response_original = np.array( - [0. , 0.021 , 0.124 , 0.3146, 0.5653, 0.8385, 1.0969, 1.3095, - 1.4549, 1.5231]) + [0. , 0.0216, 0.1271, 0.3215, 0.5762, 0.8522, 1.1114, 1.3221, + 1.4633, 1.5254]) assert_array_almost_equal( ax_step.lines[0].get_data()[1][:10], step_response_original, 4) @@ -113,8 +113,8 @@ def test_sisotool(self, sys): # Check if the step response has changed step_response_moved = np.array( - [0. , 0.023 , 0.1554, 0.4401, 0.8646, 1.3722, 1.875 , 2.2709, - 2.4633, 2.3827]) + [0. , 0.0237, 0.1596, 0.4511, 0.884 , 1.3985, 1.9031, 2.2922, + 2.4676, 2.3606]) assert_array_almost_equal( ax_step.lines[0].get_data()[1][:10], step_response_moved, 4) @@ -157,7 +157,3 @@ def test_sisotool_mimo(self, sys222, sys221): # but 2 input, 1 output should with pytest.raises(ControlMIMONotImplemented): sisotool(sys221) - - - - diff --git a/control/tests/timeresp_test.py b/control/tests/timeresp_test.py index 8705f3805..a576d0903 100644 --- a/control/tests/timeresp_test.py +++ b/control/tests/timeresp_test.py @@ -15,21 +15,20 @@ import pytest import scipy as sp - import control as ct -from control import (StateSpace, TransferFunction, c2d, isctime, isdtime, - ss2tf, tf2ss) +from control import StateSpace, TransferFunction, c2d, isctime, ss2tf, tf2ss +from control.exception import slycot_check +from control.tests.conftest import slycotonly from control.timeresp import (_default_time_vector, _ideal_tfinal_and_dt, forced_response, impulse_response, initial_response, step_info, step_response) -from control.tests.conftest import slycotonly -from control.exception import slycot_check class TSys: """Struct of test system""" - def __init__(self, sys=None): + def __init__(self, sys=None, call_kwargs=None): self.sys = sys + self.kwargs = call_kwargs if call_kwargs else {} def __repr__(self): """Show system when debugging""" @@ -66,8 +65,8 @@ def siso_ss2(self, siso_ss1): T.initial = siso_ss1.yinitial - 9 T.yimpulse = np.array([86., 70.1808, 57.3753, 46.9975, 38.5766, 31.7344, 26.1668, 21.6292, 17.9245, 14.8945]) - return T + return T @pytest.fixture def siso_tf1(self): @@ -193,35 +192,150 @@ def pole_cancellation(self): def no_pole_cancellation(self): return TransferFunction([1.881e+06], [188.1, 1.881e+06]) - + @pytest.fixture def siso_tf_type1(self): # System Type 1 - Step response not stationary: G(s)=1/s(s+1) - return TransferFunction(1, [1, 1, 0]) + T = TSys(TransferFunction(1, [1, 1, 0])) + T.step_info = { + 'RiseTime': np.NaN, + 'SettlingTime': np.NaN, + 'SettlingMin': np.NaN, + 'SettlingMax': np.NaN, + 'Overshoot': np.NaN, + 'Undershoot': np.NaN, + 'Peak': np.Inf, + 'PeakTime': np.Inf, + 'SteadyStateValue': np.NaN} + return T @pytest.fixture def siso_tf_kpos(self): - # SISO under shoot response and positive final value G(s)=(-s+1)/(s²+s+1) - return TransferFunction([-1, 1], [1, 1, 1]) + # SISO under shoot response and positive final value + # G(s)=(-s+1)/(s²+s+1) + T = TSys(TransferFunction([-1, 1], [1, 1, 1])) + T.step_info = { + 'RiseTime': 1.242, + 'SettlingTime': 9.110, + 'SettlingMin': 0.90, + 'SettlingMax': 1.208, + 'Overshoot': 20.840, + 'Undershoot': 28.0, + 'Peak': 1.208, + 'PeakTime': 4.282, + 'SteadyStateValue': 1.0} + return T @pytest.fixture def siso_tf_kneg(self): - # SISO under shoot response and negative final value k=-1 G(s)=-(-s+1)/(s²+s+1) - return TransferFunction([1, -1], [1, 1, 1]) + # SISO under shoot response and negative final value + # k=-1 G(s)=-(-s+1)/(s²+s+1) + T = TSys(TransferFunction([1, -1], [1, 1, 1])) + T.step_info = { + 'RiseTime': 1.242, + 'SettlingTime': 9.110, + 'SettlingMin': -1.208, + 'SettlingMax': -0.90, + 'Overshoot': 20.840, + 'Undershoot': 28.0, + 'Peak': 1.208, + 'PeakTime': 4.282, + 'SteadyStateValue': -1.0} + return T @pytest.fixture - def tf1_matlab_help(self): - # example from matlab online help https://www.mathworks.com/help/control/ref/stepinfo.html - return TransferFunction([1, 5, 5], [1, 1.65, 5, 6.5, 2]) + def siso_tf_step_matlab(self): + # example from matlab online help + # https://www.mathworks.com/help/control/ref/stepinfo.html + T = TSys(TransferFunction([1, 5, 5], [1, 1.65, 5, 6.5, 2])) + T.step_info = { + 'RiseTime': 3.8456, + 'SettlingTime': 27.9762, + 'SettlingMin': 2.0689, + 'SettlingMax': 2.6873, + 'Overshoot': 7.4915, + 'Undershoot': 0, + 'Peak': 2.6873, + 'PeakTime': 8.0530, + 'SteadyStateValue': 2.5} + return T + + @pytest.fixture + def mimo_ss_step_matlab(self): + A = [[0.68, -0.34], + [0.34, 0.68]] + B = [[0.18, -0.05], + [0.04, 0.11]] + C = [[0, -1.53], + [-1.12, -1.10]] + D = [[0, 0], + [0.06, -0.37]] + T = TSys(StateSpace(A, B, C, D, 0.2)) + T.kwargs['step_info'] = {'T': 4.6} + T.step_info = [[{'RiseTime': 0.6000, + 'SettlingTime': 3.0000, + 'SettlingMin': -0.5999, + 'SettlingMax': -0.4689, + 'Overshoot': 15.5072, + 'Undershoot': 0., + 'Peak': 0.5999, + 'PeakTime': 1.4000, + 'SteadyStateValue': -0.5193}, + {'RiseTime': 0., + 'SettlingTime': 3.6000, + 'SettlingMin': -0.2797, + 'SettlingMax': -0.1043, + 'Overshoot': 118.9918, + 'Undershoot': 0, + 'Peak': 0.2797, + 'PeakTime': .6000, + 'SteadyStateValue': -0.1277}], + [{'RiseTime': 0.4000, + 'SettlingTime': 2.8000, + 'SettlingMin': -0.6724, + 'SettlingMax': -0.5188, + 'Overshoot': 24.6476, + 'Undershoot': 11.1224, + 'Peak': 0.6724, + 'PeakTime': 1, + 'SteadyStateValue': -0.5394}, + {'RiseTime': 0.0000, # (*) + 'SettlingTime': 3.4000, + 'SettlingMin': -0.1034, + 'SettlingMax': -0.1485, + 'Overshoot': 132.0170, + 'Undershoot': 79.222, # 0. in MATLAB + 'Peak': 0.4350, + 'PeakTime': .2, + 'SteadyStateValue': -0.1875}]] + # (*): MATLAB gives 0.4 here, but it is unclear what + # 10% and 90% of the steady state response mean, when + # the step for this channel does not start a 0 for + # 0 initial conditions + return T @pytest.fixture - def tf2_matlab_help(self): - A = [[0.68, - 0.34], [0.34, 0.68]] - B = [[0.18], [0.04]] - C = [-1.12, - 1.10] - D = [0.06] - sys = StateSpace(A, B, C, D, 0.2) - return sys + def siso_ss_step_matlab(self, mimo_ss_step_matlab): + T = copy(mimo_ss_step_matlab) + T.sys = T.sys[1, 0] + T.step_info = T.step_info[1][0] + return T + + @pytest.fixture + def mimo_tf_step_info(self, + siso_tf_kpos, siso_tf_kneg, + siso_tf_step_matlab): + Ta = [[siso_tf_kpos, siso_tf_kneg, siso_tf_step_matlab], + [siso_tf_step_matlab, siso_tf_kpos, siso_tf_kneg]] + T = TSys(TransferFunction( + [[Ti.sys.num[0][0] for Ti in Tr] for Tr in Ta], + [[Ti.sys.den[0][0] for Ti in Tr] for Tr in Ta])) + T.step_info = [[Ti.step_info for Ti in Tr] for Tr in Ta] + # enforce enough sample points for all channels (they have different + # characteristics) + T.kwargs['step_info'] = {'T_num': 2000} + return T + @pytest.fixture def tsystem(self, @@ -232,8 +346,9 @@ def tsystem(self, siso_dss1, siso_dss2, mimo_dss1, mimo_dss2, mimo_dtf1, pole_cancellation, no_pole_cancellation, siso_tf_type1, - siso_tf_kpos, siso_tf_kneg, tf1_matlab_help, - tf2_matlab_help): + siso_tf_kpos, siso_tf_kneg, + siso_tf_step_matlab, siso_ss_step_matlab, + mimo_ss_step_matlab, mimo_tf_step_info): systems = {"siso_ss1": siso_ss1, "siso_ss2": siso_ss2, "siso_tf1": siso_tf1, @@ -254,8 +369,10 @@ def tsystem(self, "siso_tf_type1": siso_tf_type1, "siso_tf_kpos": siso_tf_kpos, "siso_tf_kneg": siso_tf_kneg, - "tf1_matlab_help": tf1_matlab_help, - "tf2_matlab_help": tf2_matlab_help, + "siso_tf_step_matlab": siso_tf_step_matlab, + "siso_ss_step_matlab": siso_ss_step_matlab, + "mimo_ss_step_matlab": mimo_ss_step_matlab, + "mimo_tf_step": mimo_tf_step_info, } return systems[request.param] @@ -309,102 +426,110 @@ def test_step_nostates(self, dt): t, y = step_response(sys) np.testing.assert_array_equal(y, np.ones(len(t))) - def test_step_info(self): - # From matlab docs: - sys = TransferFunction([1, 5, 5], [1, 1.65, 5, 6.5, 2]) - Strue = { - 'RiseTime': 3.8456, - 'SettlingTime': 27.9762, - 'SettlingMin': 2.0689, - 'SettlingMax': 2.6873, - 'Overshoot': 7.4915, - 'Undershoot': 0, - 'Peak': 2.6873, - 'PeakTime': 8.0530, - 'SteadyStateValue': 2.50 - } - - S = step_info(sys) - - Sk = sorted(S.keys()) - Sktrue = sorted(Strue.keys()) - assert Sk == Sktrue - # Very arbitrary tolerance because I don't know if the - # response from the MATLAB is really that accurate. - # maybe it is a good idea to change the Strue to match - # but I didn't do it because I don't know if it is - # accurate either... - rtol = 2e-2 - np.testing.assert_allclose([S[k] for k in Sk], - [Strue[k] for k in Sktrue], - rtol=rtol) - - # tolerance for all parameters could be wrong for some systems - # discrete systems time parameters tolerance could be +/-dt + def assert_step_info_match(self, sys, info, info_ref): + """Assert reasonable step_info accuracy.""" + if sys.isdtime(strict=True): + dt = sys.dt + else: + _, dt = _ideal_tfinal_and_dt(sys, is_step=True) + + for k in ['RiseTime', 'SettlingTime', 'PeakTime']: + np.testing.assert_allclose(info[k], info_ref[k], atol=dt, + err_msg=f"{k} does not match") + for k in ['Overshoot', 'Undershoot', 'Peak', 'SteadyStateValue']: + np.testing.assert_allclose(info[k], info_ref[k], rtol=5e-3, + err_msg=f"{k} does not match") + + # steep gradient right after RiseTime + absrefinf = np.abs(info_ref['SteadyStateValue']) + if info_ref['RiseTime'] > 0: + y_next_sample_max = 0.8*absrefinf/info_ref['RiseTime']*dt + else: + y_next_sample_max = 0 + for k in ['SettlingMin', 'SettlingMax']: + if (np.abs(info_ref[k]) - 0.9 * absrefinf) > y_next_sample_max: + # local min/max peak well after signal has risen + np.testing.assert_allclose(info[k], info_ref[k], rtol=1e-3) + @pytest.mark.parametrize( - "tsystem, info_true, tolerance", - [("tf1_matlab_help", { - 'RiseTime': 3.8456, - 'SettlingTime': 27.9762, - 'SettlingMin': 2.0689, - 'SettlingMax': 2.6873, - 'Overshoot': 7.4915, - 'Undershoot': 0, - 'Peak': 2.6873, - 'PeakTime': 8.0530, - 'SteadyStateValue': 2.5}, 2e-2), - ("tf2_matlab_help", { - 'RiseTime': 0.4000, - 'SettlingTime': 2.8000, - 'SettlingMin': -0.6724, - 'SettlingMax': -0.5188, - 'Overshoot': 24.6476, - 'Undershoot': 11.1224, - 'Peak': 0.6724, - 'PeakTime': 1, - 'SteadyStateValue': -0.5394}, .2), - ("siso_tf_kpos", { - 'RiseTime': 1.242, - 'SettlingTime': 9.110, - 'SettlingMin': 0.950, - 'SettlingMax': 1.208, - 'Overshoot': 20.840, - 'Undershoot': 27.840, - 'Peak': 1.208, - 'PeakTime': 4.282, - 'SteadyStateValue': 1.0}, 2e-2), - ("siso_tf_kneg", { - 'RiseTime': 1.242, - 'SettlingTime': 9.110, - 'SettlingMin': -1.208, - 'SettlingMax': -0.950, - 'Overshoot': 20.840, - 'Undershoot': 27.840, - 'Peak': 1.208, - 'PeakTime': 4.282, - 'SteadyStateValue': -1.0}, 2e-2), - ("siso_tf_type1", {'RiseTime': np.NaN, - 'SettlingTime': np.NaN, - 'SettlingMin': np.NaN, - 'SettlingMax': np.NaN, - 'Overshoot': np.NaN, - 'Undershoot': np.NaN, - 'Peak': np.Inf, - 'PeakTime': np.Inf, - 'SteadyStateValue': np.NaN}, 2e-2)], + "yfinal", [True, False], ids=["yfinal", "no yfinal"]) + @pytest.mark.parametrize( + "systype, time_2d", + [("ltisys", False), + ("time response", False), + ("time response", True), + ], + ids=["ltisys", "time response (n,)", "time response (1,n)"]) + @pytest.mark.parametrize( + "tsystem", + ["siso_tf_step_matlab", + "siso_ss_step_matlab", + "siso_tf_kpos", + "siso_tf_kneg", + "siso_tf_type1"], indirect=["tsystem"]) - def test_step_info(self, tsystem, info_true, tolerance): - """ """ - info = step_info(tsystem) + def test_step_info(self, tsystem, systype, time_2d, yfinal): + """Test step info for SISO systems.""" + step_info_kwargs = tsystem.kwargs.get('step_info', {}) + if systype == "time response": + # simulate long enough for steady state value + tfinal = 3 * tsystem.step_info['SettlingTime'] + if np.isnan(tfinal): + pytest.skip("test system does not settle") + t, y = step_response(tsystem.sys, T=tfinal, T_num=5000) + sysdata = y + step_info_kwargs['T'] = t[np.newaxis, :] if time_2d else t + else: + sysdata = tsystem.sys + if yfinal: + step_info_kwargs['yfinal'] = tsystem.step_info['SteadyStateValue'] - info_true_sorted = sorted(info_true.keys()) - info_sorted = sorted(info.keys()) + info = step_info(sysdata, **step_info_kwargs) - assert info_sorted == info_true_sorted + self.assert_step_info_match(tsystem.sys, info, tsystem.step_info) - np.testing.assert_allclose([info_true[k] for k in info_true_sorted], - [info[k] for k in info_sorted], - rtol=tolerance) + @pytest.mark.parametrize( + "yfinal", [True, False], ids=["yfinal", "no_yfinal"]) + @pytest.mark.parametrize( + "systype", ["ltisys", "time response"]) + @pytest.mark.parametrize( + "tsystem", + ['mimo_ss_step_matlab', + pytest.param('mimo_tf_step', marks=slycotonly)], + indirect=["tsystem"]) + def test_step_info_mimo(self, tsystem, systype, yfinal): + """Test step info for MIMO systems.""" + step_info_kwargs = tsystem.kwargs.get('step_info', {}) + if systype == "time response": + tfinal = 3 * max([S['SettlingTime'] + for Srow in tsystem.step_info for S in Srow]) + t, y = step_response(tsystem.sys, T=tfinal, T_num=5000) + sysdata = y + step_info_kwargs['T'] = t + else: + sysdata = tsystem.sys + if yfinal: + step_info_kwargs['yfinal'] = [[S['SteadyStateValue'] + for S in Srow] + for Srow in tsystem.step_info] + + info_dict = step_info(sysdata, **step_info_kwargs) + + for i, row in enumerate(info_dict): + for j, info in enumerate(row): + self.assert_step_info_match(tsystem.sys, + info, tsystem.step_info[i][j]) + + def test_step_info_invalid(self): + """Call step_info with invalid parameters.""" + with pytest.raises(ValueError, match="time series data convention"): + step_info(["not numeric data"]) + with pytest.raises(ValueError, match="time series data convention"): + step_info(np.ones((10, 15))) # invalid shape + with pytest.raises(ValueError, match="matching time vector"): + step_info(np.ones(15), T=np.linspace(0, 1, 20)) # time too long + with pytest.raises(ValueError, match="matching time vector"): + step_info(np.ones((2, 2, 15))) # no time vector def test_step_pole_cancellation(self, pole_cancellation, no_pole_cancellation): @@ -412,13 +537,9 @@ def test_step_pole_cancellation(self, pole_cancellation, # https://github.com/python-control/python-control/issues/440 step_info_no_cancellation = step_info(no_pole_cancellation) step_info_cancellation = step_info(pole_cancellation) - for key in step_info_no_cancellation: - if key == 'Overshoot': - # skip this test because these systems have no overshoot - # => very sensitive to parameters - continue - np.testing.assert_allclose(step_info_no_cancellation[key], - step_info_cancellation[key], rtol=1e-4) + self.assert_step_info_match(no_pole_cancellation, + step_info_no_cancellation, + step_info_cancellation) @pytest.mark.parametrize( "tsystem, kwargs", @@ -634,20 +755,23 @@ def test_step_robustness(self): (TransferFunction(1, [1, .5, 0]), 25)]) # poles at 0.5 and 0 def test_auto_generated_time_vector_tfinal(self, tfsys, tfinal): """Confirm a TF with a pole at p simulates for tfinal seconds""" - np.testing.assert_almost_equal( - _ideal_tfinal_and_dt(tfsys)[0], tfinal, decimal=4) + ideal_tfinal, ideal_dt = _ideal_tfinal_and_dt(tfsys) + np.testing.assert_allclose(ideal_tfinal, tfinal, rtol=1e-4) + T = _default_time_vector(tfsys) + np.testing.assert_allclose(T[-1], tfinal, atol=0.5*ideal_dt) @pytest.mark.parametrize("wn, zeta", [(10, 0), (100, 0), (100, .1)]) - def test_auto_generated_time_vector_dt_cont(self, wn, zeta): + def test_auto_generated_time_vector_dt_cont1(self, wn, zeta): """Confirm a TF with a natural frequency of wn rad/s gets a dt of 1/(ratio*wn)""" dtref = 0.25133 / wn tfsys = TransferFunction(1, [1, 2*zeta*wn, wn**2]) - np.testing.assert_almost_equal(_ideal_tfinal_and_dt(tfsys)[1], dtref) + np.testing.assert_almost_equal(_ideal_tfinal_and_dt(tfsys)[1], dtref, + decimal=5) - def test_auto_generated_time_vector_dt_cont(self): + def test_auto_generated_time_vector_dt_cont2(self): """A sampled tf keeps its dt""" wn = 100 zeta = .1 @@ -674,21 +798,23 @@ def test_default_timevector_long(self): def test_default_timevector_functions_c(self, fun): """Test that functions can calculate the time vector automatically""" sys = TransferFunction(1, [1, .5, 0]) + _tfinal, _dt = _ideal_tfinal_and_dt(sys) # test impose number of time steps tout, _ = fun(sys, T_num=10) assert len(tout) == 10 # test impose final time - tout, _ = fun(sys, 100) - np.testing.assert_allclose(tout[-1], 100., atol=0.5) + tout, _ = fun(sys, T=100.) + np.testing.assert_allclose(tout[-1], 100., atol=0.5*_dt) @pytest.mark.parametrize("fun", [step_response, impulse_response, initial_response]) - def test_default_timevector_functions_d(self, fun): + @pytest.mark.parametrize("dt", [0.1, 0.112]) + def test_default_timevector_functions_d(self, fun, dt): """Test that functions can calculate the time vector automatically""" - sys = TransferFunction(1, [1, .5, 0], 0.1) + sys = TransferFunction(1, [1, .5, 0], dt) # test impose number of time steps is ignored with dt given tout, _ = fun(sys, T_num=15) @@ -696,7 +822,7 @@ def test_default_timevector_functions_d(self, fun): # test impose final time tout, _ = fun(sys, 100) - np.testing.assert_allclose(tout[-1], 100., atol=0.5) + np.testing.assert_allclose(tout[-1], 100., atol=0.5*dt) @pytest.mark.parametrize("tsystem", diff --git a/control/timeresp.py b/control/timeresp.py index 9c7b7a990..630eff03a 100644 --- a/control/timeresp.py +++ b/control/timeresp.py @@ -1,9 +1,7 @@ -# timeresp.py - time-domain simulation routines -# -# This file contains a collection of functions that calculate time -# responses for linear systems. +""" +timeresp.py - time-domain simulation routines. -"""The :mod:`~control.timeresp` module contains a collection of +The :mod:`~control.timeresp` module contains a collection of functions that are used to compute time-domain simulations of LTI systems. @@ -21,9 +19,7 @@ See :ref:`time-series-convention` for more information on how time series data are represented. -""" - -"""Copyright (c) 2011 by California Institute of Technology +Copyright (c) 2011 by California Institute of Technology All rights reserved. Copyright (c) 2011 by Eike Welk @@ -71,18 +67,17 @@ $Id$ """ -# Libraries that we make use of -import scipy as sp # SciPy library (used all over) -import numpy as np # NumPy library -from scipy.linalg import eig, eigvals, matrix_balance, norm -from numpy import (einsum, maximum, minimum, - atleast_1d) import warnings -from .lti import LTI # base class of StateSpace, TransferFunction -from .xferfcn import TransferFunction -from .statesp import _convert_to_statespace, _mimo2simo, _mimo2siso, ssdata -from .lti import isdtime, isctime + +import numpy as np +import scipy as sp +from numpy import einsum, maximum, minimum +from scipy.linalg import eig, eigvals, matrix_balance, norm + from . import config +from .lti import isctime, isdtime +from .statesp import StateSpace, _convert_to_statespace, _mimo2simo, _mimo2siso +from .xferfcn import TransferFunction __all__ = ['forced_response', 'step_response', 'step_info', 'initial_response', 'impulse_response'] @@ -210,7 +205,7 @@ def forced_response(sys, T=None, U=0., X0=0., transpose=False, Parameters ---------- - sys : LTI (StateSpace or TransferFunction) + sys : StateSpace or TransferFunction LTI system to simulate T : array_like, optional for discrete LTI `sys` @@ -285,9 +280,9 @@ def forced_response(sys, T=None, U=0., X0=0., transpose=False, See :ref:`time-series-convention`. """ - if not isinstance(sys, LTI): - raise TypeError('Parameter ``sys``: must be a ``LTI`` object. ' - '(For example ``StateSpace`` or ``TransferFunction``)') + if not isinstance(sys, (StateSpace, TransferFunction)): + raise TypeError('Parameter ``sys``: must be a ``StateSpace`` or' + ' ``TransferFunction``)') # If return_x was not specified, figure out the default if return_x is None: @@ -739,43 +734,62 @@ def step_response(sys, T=None, X0=0., input=None, output=None, T_num=None, squeeze=squeeze, input=input, output=output) -def step_info(sys, T=None, T_num=None, SettlingTimeThreshold=0.02, - RiseTimeLimits=(0.1, 0.9)): - ''' +def step_info(sysdata, T=None, T_num=None, yfinal=None, + SettlingTimeThreshold=0.02, RiseTimeLimits=(0.1, 0.9)): + """ Step response characteristics (Rise time, Settling Time, Peak and others). Parameters ---------- - sys : SISO dynamic system model. Dynamic systems that you can use include: - StateSpace or TransferFunction - LTI system to simulate - + sysdata : StateSpace or TransferFunction or array_like + The system data. Either LTI system to similate (StateSpace, + TransferFunction), or a time series of step response data. T : array_like or float, optional Time vector, or simulation time duration if a number (time vector is - autocomputed if not given, see :func:`step_response` for more detail) - + autocomputed if not given, see :func:`step_response` for more detail). + Required, if sysdata is a time series of response data. T_num : int, optional Number of time steps to use in simulation if T is not provided as an - array (autocomputed if not given); ignored if sys is discrete-time. - - SettlingTimeThreshold : float value, optional + array; autocomputed if not given; ignored if sysdata is a + discrete-time system or a time series or response data. + yfinal : scalar or array_like, optional + Steady-state response. If not given, sysdata.dcgain() is used for + systems to simulate and the last value of the the response data is + used for a given time series of response data. Scalar for SISO, + (noutputs, ninputs) array_like for MIMO systems. + SettlingTimeThreshold : float, optional Defines the error to compute settling time (default = 0.02) - RiseTimeLimits : tuple (lower_threshold, upper_theshold) Defines the lower and upper threshold for RiseTime computation Returns ------- - S: a dictionary containing: - RiseTime: Time from 10% to 90% of the steady-state value. - SettlingTime: Time to enter inside a default error of 2% - SettlingMin: Minimum value after RiseTime - SettlingMax: Maximum value after RiseTime - Overshoot: Percentage of the Peak relative to steady value - Undershoot: Percentage of undershoot - Peak: Absolute peak value - PeakTime: time of the Peak - SteadyStateValue: Steady-state value + S : dict or list of list of dict + If `sysdata` corresponds to a SISO system, S is a dictionary + containing: + + RiseTime: + Time from 10% to 90% of the steady-state value. + SettlingTime: + Time to enter inside a default error of 2% + SettlingMin: + Minimum value after RiseTime + SettlingMax: + Maximum value after RiseTime + Overshoot: + Percentage of the Peak relative to steady value + Undershoot: + Percentage of undershoot + Peak: + Absolute peak value + PeakTime: + time of the Peak + SteadyStateValue: + Steady-state value + + If `sysdata` corresponds to a MIMO system, `S` is a 2D list of dicts. + To get the step response characteristics from the j-th input to the + i-th output, access ``S[i][j]`` See Also @@ -784,83 +798,163 @@ def step_info(sys, T=None, T_num=None, SettlingTimeThreshold=0.02, Examples -------- - >>> info = step_info(sys, T) - ''' - - if not sys.issiso(): - sys = _mimo2siso(sys,0,0) - warnings.warn(" Internal conversion from a MIMO system to a SISO system," - " the first input and the first output were used (u1 -> y1);" - " it may not be the result you are looking for") - - if T is None or np.asarray(T).size == 1: - T = _default_time_vector(sys, N=T_num, tfinal=T, is_step=True) - - T, yout = step_response(sys, T) - - # Steady state value - InfValue = sys.dcgain().real - - # TODO: this could be a function step_info4data(t,y,yfinal) - rise_time: float = np.NaN - settling_time: float = np.NaN - settling_min: float = np.NaN - settling_max: float = np.NaN - peak_value: float = np.Inf - peak_time: float = np.Inf - undershoot: float = np.NaN - overshoot: float = np.NaN - steady_state_value: float = np.NaN - - if not np.isnan(InfValue) and not np.isinf(InfValue): - # SteadyStateValue - steady_state_value = InfValue - # Peak - peak_index = np.abs(yout).argmax() - peak_value = np.abs(yout[peak_index]) - peak_time = T[peak_index] - - sup_margin = (1. + SettlingTimeThreshold) * InfValue - inf_margin = (1. - SettlingTimeThreshold) * InfValue - - # RiseTime - tr_lower_index = (np.where(np.sign(InfValue.real) * (yout- RiseTimeLimits[0] * InfValue) >= 0 )[0])[0] - tr_upper_index = (np.where(np.sign(InfValue.real) * yout >= np.sign(InfValue.real) * RiseTimeLimits[1] * InfValue)[0])[0] - - # SettlingTime - settling_time = T[np.where(np.abs(yout-InfValue) >= np.abs(SettlingTimeThreshold*InfValue))[0][-1]+1] - # Overshoot and Undershoot - y_os = (np.sign(InfValue.real)*yout).max() - dy_os = np.abs(y_os) - np.abs(InfValue) - if dy_os > 0: - overshoot = np.abs(100. * dy_os / InfValue) + >>> from control import step_info, TransferFunction + >>> sys = TransferFunction([-1, 1], [1, 1, 1]) + >>> S = step_info(sys) + >>> for k in S: + ... print(f"{k}: {S[k]:3.4}") + ... + RiseTime: 1.256 + SettlingTime: 9.071 + SettlingMin: 0.9011 + SettlingMax: 1.208 + Overshoot: 20.85 + Undershoot: 27.88 + Peak: 1.208 + PeakTime: 4.187 + SteadyStateValue: 1.0 + + MIMO System: Simulate until a final time of 10. Get the step response + characteristics for the second input and specify a 5% error until the + signal is considered settled. + + >>> from numpy import sqrt + >>> from control import step_info, StateSpace + >>> sys = StateSpace([[-1., -1.], + ... [1., 0.]], + ... [[-1./sqrt(2.), 1./sqrt(2.)], + ... [0, 0]], + ... [[sqrt(2.), -sqrt(2.)]], + ... [[0, 0]]) + >>> S = step_info(sys, T=10., SettlingTimeThreshold=0.05) + >>> for k, v in S[0][1].items(): + ... print(f"{k}: {float(v):3.4}") + RiseTime: 1.212 + SettlingTime: 6.061 + SettlingMin: -1.209 + SettlingMax: -0.9184 + Overshoot: 20.87 + Undershoot: 28.02 + Peak: 1.209 + PeakTime: 4.242 + SteadyStateValue: -1.0 + """ + if isinstance(sysdata, (StateSpace, TransferFunction)): + if T is None or np.asarray(T).size == 1: + T = _default_time_vector(sysdata, N=T_num, tfinal=T, is_step=True) + T, Yout = step_response(sysdata, T, squeeze=False) + if yfinal: + InfValues = np.atleast_2d(yfinal) else: - overshoot = 0 - - y_us = (np.sign(InfValue.real)*yout).min() - dy_us = np.abs(y_us) - if dy_us > 0: - undershoot = np.abs(100. * dy_us / InfValue) + InfValues = np.atleast_2d(sysdata.dcgain()) + retsiso = sysdata.issiso() + noutputs = sysdata.noutputs + ninputs = sysdata.ninputs + else: + # Time series of response data + errmsg = ("`sys` must be a LTI system, or time response data" + " with a shape following the python-control" + " time series data convention.") + try: + Yout = np.array(sysdata, dtype=float) + except ValueError: + raise ValueError(errmsg) + if Yout.ndim == 1 or (Yout.ndim == 2 and Yout.shape[0] == 1): + Yout = Yout[np.newaxis, np.newaxis, :] + retsiso = True + elif Yout.ndim == 3: + retsiso = False else: - undershoot = 0 - - # RiseTime - rise_time = T[tr_upper_index] - T[tr_lower_index] - - settling_max = (yout[tr_upper_index:]).max() - settling_min = (yout[tr_upper_index:]).min() - - return { - 'RiseTime': rise_time, - 'SettlingTime': settling_time, - 'SettlingMin': settling_min, - 'SettlingMax': settling_max, - 'Overshoot': overshoot, - 'Undershoot': undershoot, - 'Peak': peak_value, - 'PeakTime': peak_time, - 'SteadyStateValue': steady_state_value - } + raise ValueError(errmsg) + if T is None or Yout.shape[2] != len(np.squeeze(T)): + raise ValueError("For time response data, a matching time vector" + " must be given") + T = np.squeeze(T) + noutputs = Yout.shape[0] + ninputs = Yout.shape[1] + InfValues = np.atleast_2d(yfinal) if yfinal else Yout[:, :, -1] + + ret = [] + for i in range(noutputs): + retrow = [] + for j in range(ninputs): + yout = Yout[i, j, :] + + # Steady state value + InfValue = InfValues[i, j] + sgnInf = np.sign(InfValue.real) + + rise_time: float = np.NaN + settling_time: float = np.NaN + settling_min: float = np.NaN + settling_max: float = np.NaN + peak_value: float = np.Inf + peak_time: float = np.Inf + undershoot: float = np.NaN + overshoot: float = np.NaN + steady_state_value: complex = np.NaN + + if not np.isnan(InfValue) and not np.isinf(InfValue): + # RiseTime + tr_lower_index = np.where( + sgnInf * (yout - RiseTimeLimits[0] * InfValue) >= 0 + )[0][0] + tr_upper_index = np.where( + sgnInf * (yout - RiseTimeLimits[1] * InfValue) >= 0 + )[0][0] + rise_time = T[tr_upper_index] - T[tr_lower_index] + + # SettlingTime + settled = np.where( + np.abs(yout/InfValue-1) >= SettlingTimeThreshold)[0][-1]+1 + # MIMO systems can have unsettled channels without infinite + # InfValue + if settled < len(T): + settling_time = T[settled] + + settling_min = (yout[tr_upper_index:]).min() + settling_max = (yout[tr_upper_index:]).max() + + # Overshoot + y_os = (sgnInf * yout).max() + dy_os = np.abs(y_os) - np.abs(InfValue) + if dy_os > 0: + overshoot = np.abs(100. * dy_os / InfValue) + else: + overshoot = 0 + + # Undershoot + y_us = (sgnInf * yout).min() + dy_us = np.abs(y_us) + if dy_us > 0: + undershoot = np.abs(100. * dy_us / InfValue) + else: + undershoot = 0 + + # Peak + peak_index = np.abs(yout).argmax() + peak_value = np.abs(yout[peak_index]) + peak_time = T[peak_index] + + # SteadyStateValue + steady_state_value = InfValue.real + + retij = { + 'RiseTime': rise_time, + 'SettlingTime': settling_time, + 'SettlingMin': settling_min, + 'SettlingMax': settling_max, + 'Overshoot': overshoot, + 'Undershoot': undershoot, + 'Peak': peak_value, + 'PeakTime': peak_time, + 'SteadyStateValue': steady_state_value + } + retrow.append(retij) + + ret.append(retrow) + + return ret[0][0] if retsiso else ret def initial_response(sys, T=None, X0=0., input=0, output=None, T_num=None, transpose=False, return_x=False, squeeze=None): @@ -1287,16 +1381,18 @@ def _default_time_vector(sys, N=None, tfinal=None, is_step=True): # only need to use default_tfinal if not given; N is ignored. if tfinal is None: # for discrete time, change from ideal_tfinal if N too large/small - N = int(np.clip(ideal_tfinal/sys.dt, N_min_dt, N_max))# [N_min, N_max] - tfinal = sys.dt * N + # [N_min, N_max] + N = int(np.clip(np.ceil(ideal_tfinal/sys.dt)+1, N_min_dt, N_max)) + tfinal = sys.dt * (N-1) else: - N = int(tfinal/sys.dt) - tfinal = N * sys.dt # make tfinal an integer multiple of sys.dt + N = int(np.ceil(tfinal/sys.dt)) + 1 + tfinal = sys.dt * (N-1) # make tfinal an integer multiple of sys.dt else: if tfinal is None: # for continuous time, simulate to ideal_tfinal but limit N tfinal = ideal_tfinal if N is None: - N = int(np.clip(tfinal/ideal_dt, N_min_ct, N_max)) # N<-[N_min, N_max] + # [N_min, N_max] + N = int(np.clip(np.ceil(tfinal/ideal_dt)+1, N_min_ct, N_max)) - return np.linspace(0, tfinal, N, endpoint=False) + return np.linspace(0, tfinal, N, endpoint=True) 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