diff --git a/control/freqplot.py b/control/freqplot.py index a1772fea7..7b296c111 100644 --- a/control/freqplot.py +++ b/control/freqplot.py @@ -822,10 +822,10 @@ def default_frequency_range(syslist, Hz=None, number_of_samples=None, # Set the range to be an order of magnitude beyond any features if number_of_samples: - omega = sp.logspace( + omega = np.logspace( lsp_min, lsp_max, num=number_of_samples, endpoint=True) else: - omega = sp.logspace(lsp_min, lsp_max, endpoint=True) + omega = np.logspace(lsp_min, lsp_max, endpoint=True) return omega diff --git a/control/matlab/timeresp.py b/control/matlab/timeresp.py index 647210a9c..b9d4004ca 100644 --- a/control/matlab/timeresp.py +++ b/control/matlab/timeresp.py @@ -21,8 +21,9 @@ def step(sys, T=None, X0=0., input=0, output=None, return_x=False): sys: StateSpace, or TransferFunction LTI system to simulate - T: array-like object, optional - Time vector (argument is autocomputed if not given) + T: array-like or number, optional + Time vector, or simulation time duration if a number (time vector is + autocomputed if not given) X0: array-like or number, optional Initial condition (default = 0) @@ -59,7 +60,7 @@ def step(sys, T=None, X0=0., input=0, output=None, return_x=False): from ..timeresp import step_response T, yout, xout = step_response(sys, T, X0, input, output, - transpose = True, return_x=True) + transpose=True, return_x=True) if return_x: return yout, T, xout @@ -75,8 +76,9 @@ def stepinfo(sys, T=None, SettlingTimeThreshold=0.02, RiseTimeLimits=(0.1,0.9)): sys: StateSpace, or TransferFunction LTI system to simulate - T: array-like object, optional - Time vector (argument is autocomputed if not given) + T: array-like or number, optional + Time vector, or simulation time duration if a number (time vector is + autocomputed if not given) SettlingTimeThreshold: float value, optional Defines the error to compute settling time (default = 0.02) @@ -127,9 +129,10 @@ def impulse(sys, T=None, X0=0., input=0, output=None, return_x=False): sys: StateSpace, TransferFunction LTI system to simulate - T: array-like object, optional - Time vector (argument is autocomputed if not given) - + T: array-like or number, optional + Time vector, or simulation time duration if a number (time vector is + autocomputed if not given) + X0: array-like or number, optional Initial condition (default = 0) @@ -182,9 +185,10 @@ def initial(sys, T=None, X0=0., input=None, output=None, return_x=False): sys: StateSpace, or TransferFunction LTI system to simulate - T: array-like object, optional - Time vector (argument is autocomputed if not given) - + T: array-like or number, optional + Time vector, or simulation time duration if a number (time vector is + autocomputed if not given) + X0: array-like object or number, optional Initial condition (default = 0) @@ -245,9 +249,8 @@ def lsim(sys, U=0., T=None, X0=0.): If `U` is ``None`` or ``0``, a special algorithm is used. This special algorithm is faster than the general algorithm, which is used otherwise. - T: array-like - Time steps at which the input is defined, numbers must be (strictly - monotonic) increasing. + T: array-like, optional for discrete LTI `sys` + Time steps at which the input is defined; values must be evenly spaced. X0: array-like or number, optional Initial condition (default = 0). diff --git a/control/rlocus.py b/control/rlocus.py index 955c5c56d..56e0c55d1 100644 --- a/control/rlocus.py +++ b/control/rlocus.py @@ -50,7 +50,7 @@ import numpy as np import matplotlib import matplotlib.pyplot as plt -from scipy import array, poly1d, row_stack, zeros_like, real, imag +from numpy import array, poly1d, row_stack, zeros_like, real, imag import scipy.signal # signal processing toolbox import pylab # plotting routines from .xferfcn import _convert_to_transfer_function diff --git a/control/sisotool.py b/control/sisotool.py index e700875ca..c2db4b5ab 100644 --- a/control/sisotool.py +++ b/control/sisotool.py @@ -136,7 +136,7 @@ def _SisotoolUpdate(sys,fig,K,bode_plot_params,tvect=None): # Generate the step response and plot it sys_closed = (K*sys).feedback(1) if tvect is None: - tvect, yout = step_response(sys_closed) + tvect, yout = step_response(sys_closed, T_num=100) else: tvect, yout = step_response(sys_closed,tvect) ax_step.plot(tvect, yout) diff --git a/control/tests/sisotool_test.py b/control/tests/sisotool_test.py index e0012a373..f93de54f8 100644 --- a/control/tests/sisotool_test.py +++ b/control/tests/sisotool_test.py @@ -33,10 +33,10 @@ def test_sisotool(self): # Check the step response before moving the point step_response_original = np.array( - [0., 0.02233651, 0.13118374, 0.33078542, 0.5907113, 0.87041549, - 1.13038536, 1.33851053, 1.47374666, 1.52757114]) - assert_array_almost_equal(ax_step.lines[0].get_data()[1][:10], - step_response_original, 4) + [0., 0.0217, 0.1281, 0.3237, 0.5797, 0.8566, 1.116, + 1.3261, 1.4659, 1.526]) + assert_array_almost_equal( + ax_step.lines[0].get_data()[1][:10], step_response_original, 4) bode_plot_params = { 'omega': None, @@ -78,10 +78,10 @@ def test_sisotool(self): # Check if the step response has changed step_response_moved = np.array( - [0., 0.02458187, 0.16529784, 0.46602716, 0.91012035, 1.43364313, - 1.93996334, 2.3190105, 2.47041552, 2.32724853]) - assert_array_almost_equal(ax_step.lines[0].get_data()[1][:10], - step_response_moved, 4) + [0., 0.0239, 0.161 , 0.4547, 0.8903, 1.407, + 1.9121, 2.2989, 2.4686, 2.353]) + assert_array_almost_equal( + ax_step.lines[0].get_data()[1][:10], step_response_moved, 4) if __name__ == "__main__": diff --git a/control/tests/timeresp_test.py b/control/tests/timeresp_test.py index b208e70d2..5549b2a88 100644 --- a/control/tests/timeresp_test.py +++ b/control/tests/timeresp_test.py @@ -11,6 +11,7 @@ import unittest import numpy as np from control.timeresp import * +from control.timeresp import _ideal_tfinal_and_dt, _default_time_vector from control.statesp import * from control.xferfcn import TransferFunction, _convert_to_transfer_function from control.dtime import c2d @@ -94,6 +95,7 @@ def test_step_response(self): np.testing.assert_array_equal(Tc.shape, Td.shape) np.testing.assert_array_equal(youtc.shape, youtd.shape) + # Recreate issue #374 ("Bug in step_response()") def test_step_nostates(self): # Continuous time, constant system @@ -346,10 +348,75 @@ def test_step_robustness(self): sys2 = TransferFunction(num, den2) # Compute step response from input 1 to output 1, 2 - t1, y1 = step_response(sys1, input=0) - t2, y2 = step_response(sys2, input=0) + t1, y1 = step_response(sys1, input=0, T_num=100) + t2, y2 = step_response(sys2, input=0, T_num=100) np.testing.assert_array_almost_equal(y1, y2) + def test_auto_generated_time_vector(self): + # confirm a TF with a pole at p simulates for 7.0/p seconds + p = 0.5 + np.testing.assert_array_almost_equal( + _ideal_tfinal_and_dt(TransferFunction(1, [1, .5]))[0], + (7/p)) + np.testing.assert_array_almost_equal( + _ideal_tfinal_and_dt(TransferFunction(1, [1, .5]).sample(.1))[0], + (7/p)) + # confirm a TF with poles at 0 and p simulates for 7.0/p seconds + np.testing.assert_array_almost_equal( + _ideal_tfinal_and_dt(TransferFunction(1, [1, .5, 0]))[0], + (7/p)) + # confirm a TF with a natural frequency of wn rad/s gets a + # dt of 1/(7.0*wn) + wn = 10 + np.testing.assert_array_almost_equal( + _ideal_tfinal_and_dt(TransferFunction(1, [1, 0, wn**2]))[1], + 1/(7.0*wn)) + zeta = .1 + np.testing.assert_array_almost_equal( + _ideal_tfinal_and_dt(TransferFunction(1, [1, 2*zeta*wn, wn**2]))[1], + 1/(7.0*wn)) + # but a smapled one keeps its dt + np.testing.assert_array_almost_equal( + _ideal_tfinal_and_dt(TransferFunction(1, [1, 2*zeta*wn, wn**2]).sample(.1))[1], + .1) + np.testing.assert_array_almost_equal( + np.diff(initial_response(TransferFunction(1, [1, 2*zeta*wn, wn**2]).sample(.1))[0][0:2]), + .1) + np.testing.assert_array_almost_equal( + _ideal_tfinal_and_dt(TransferFunction(1, [1, 2*zeta*wn, wn**2]))[1], + 1/(7.0*wn)) + # TF with fast oscillations simulates only 5000 time steps even with long tfinal + self.assertEqual(5000, + len(_default_time_vector(TransferFunction(1, [1, 0, wn**2]),tfinal=100))) + # and simulates for 7.0/dt time steps + self.assertEqual( + len(_default_time_vector(TransferFunction(1, [1, 0, wn**2]))), + int(7.0/(1/(7.0*wn)))) + + sys = TransferFunction(1, [1, .5, 0]) + sysdt = TransferFunction(1, [1, .5, 0], .1) + # test impose number of time steps + self.assertEqual(10, len(step_response(sys, T_num=10)[0])) + self.assertEqual(10, len(step_response(sysdt, T_num=10)[0])) + # test impose final time + np.testing.assert_array_almost_equal( + 100, + step_response(sys, 100)[0][-1], + decimal=.5) + np.testing.assert_array_almost_equal( + 100, + step_response(sysdt, 100)[0][-1], + decimal=.5) + np.testing.assert_array_almost_equal( + 100, + impulse_response(sys, 100)[0][-1], + decimal=.5) + np.testing.assert_array_almost_equal( + 100, + initial_response(sys, 100)[0][-1], + decimal=.5) + + def test_time_vector(self): "Unit test: https://github.com/python-control/python-control/issues/239" # Discrete time simulations with specified time vectors diff --git a/control/timeresp.py b/control/timeresp.py index 4c0fbd940..8670c180d 100644 --- a/control/timeresp.py +++ b/control/timeresp.py @@ -60,16 +60,20 @@ Initial Author: Eike Welk Date: 12 May 2011 + +Modified: Sawyer B. Fuller (minster@uw.edu) to add discrete-time +capability and better automatic time vector creation +Date: June 2020 + $Id$ """ # Libraries that we make use of import scipy as sp # SciPy library (used all over) import numpy as np # NumPy library -from scipy.signal.ltisys import _default_response_times import warnings from .lti import LTI # base class of StateSpace, TransferFunction -from .statesp import _convertToStateSpace, _mimo2simo, _mimo2siso +from .statesp import _convertToStateSpace, _mimo2simo, _mimo2siso, ssdata from .lti import isdtime, isctime __all__ = ['forced_response', 'step_response', 'step_info', 'initial_response', @@ -440,7 +444,7 @@ def _get_ss_simo(sys, input=None, output=None): return _mimo2siso(sys_ss, input, output, warn_conversion=warn) -def step_response(sys, T=None, X0=0., input=None, output=None, +def step_response(sys, T=None, X0=0., input=None, output=None, T_num=None, transpose=False, return_x=False, squeeze=True): # pylint: disable=W0622 """Step response of a linear system @@ -458,8 +462,15 @@ def step_response(sys, T=None, X0=0., input=None, output=None, sys: StateSpace, or TransferFunction LTI system to simulate - T: array-like object, optional - Time vector (argument is autocomputed if not given) + T: array-like or number, optional + Time vector, or simulation time duration if a number. If T is not + provided, an attempt is made to create it automatically from the + dynamics of sys. If sys is continuous-time, the time increment dt + is chosen small enough to show the fastest mode, and the simulation + time period tfinal long enough to show the slowest mode, excluding + poles at the origin. If this results in too many time steps (>5000), + dt is reduced. If sys is discrete-time, only tfinal is computed, and + tfinal is reduced if it requires too many simulation steps. X0: array-like or number, optional Initial condition (default = 0) @@ -472,6 +483,10 @@ def step_response(sys, T=None, X0=0., input=None, output=None, output: int Index of the output that will be used in this simulation. Set to None to not trim outputs + + T_num: number, 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. transpose: bool If True, transpose all input and output arrays (for backward @@ -511,8 +526,8 @@ def step_response(sys, T=None, X0=0., input=None, output=None, """ sys = _get_ss_simo(sys, input, output) - if T is None: - T = _get_response_times(sys, N=100) + if T is None or np.asarray(T).size == 1: + T = _default_time_vector(sys, N=T_num, tfinal=T) U = np.ones_like(T) T, yout, xout = forced_response(sys, T, U, X0, transpose=transpose, @@ -524,7 +539,7 @@ def step_response(sys, T=None, X0=0., input=None, output=None, return T, yout -def step_info(sys, T=None, SettlingTimeThreshold=0.02, +def step_info(sys, T=None, T_num=None, SettlingTimeThreshold=0.02, RiseTimeLimits=(0.1, 0.9)): ''' Step response characteristics (Rise time, Settling Time, Peak and others). @@ -534,8 +549,13 @@ def step_info(sys, T=None, SettlingTimeThreshold=0.02, sys: StateSpace, or TransferFunction LTI system to simulate - T: array-like object, optional - Time vector (argument is autocomputed if not given) + T: array-like or number, optional + Time vector, or simulation time duration if a number (time vector is + autocomputed if not given, see :func:`step_response` for more detail) + + T_num: number, 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 Defines the error to compute settling time (default = 0.02) @@ -566,9 +586,9 @@ def step_info(sys, T=None, SettlingTimeThreshold=0.02, >>> info = step_info(sys, T) ''' sys = _get_ss_simo(sys) - if T is None: - T = _get_response_times(sys, N=1000) - + if T is None or np.asarray(T).size == 1: + T = _default_time_vector(sys, N=T_num, tfinal=T) + T, yout = step_response(sys, T) # Steady state value @@ -588,33 +608,21 @@ def step_info(sys, T=None, SettlingTimeThreshold=0.02, SettlingTime = T[i + 1] break - # Peak PeakIndex = np.abs(yout).argmax() - PeakValue = yout[PeakIndex] - PeakTime = T[PeakIndex] - SettlingMax = (yout).max() - SettlingMin = (yout[tr_upper_index:]).min() - # I'm really not very confident about UnderShoot: - UnderShoot = yout.min() - OverShoot = 100. * (yout.max() - InfValue) / (InfValue - yout[0]) - - # Return as a dictionary - S = { + return { 'RiseTime': RiseTime, 'SettlingTime': SettlingTime, - 'SettlingMin': SettlingMin, - 'SettlingMax': SettlingMax, - 'Overshoot': OverShoot, - 'Undershoot': UnderShoot, - 'Peak': PeakValue, - 'PeakTime': PeakTime, + 'SettlingMin': yout[tr_upper_index:].min(), + 'SettlingMax': yout.max(), + 'Overshoot': 100. * (yout.max() - InfValue) / (InfValue - yout[0]), + 'Undershoot': yout.min(), # not very confident about this + 'Peak': yout[PeakIndex], + 'PeakTime': T[PeakIndex], 'SteadyStateValue': InfValue - } - - return S + } -def initial_response(sys, T=None, X0=0., input=0, output=None, +def initial_response(sys, T=None, X0=0., input=0, output=None, T_num=None, transpose=False, return_x=False, squeeze=True): # pylint: disable=W0622 """Initial condition response of a linear system @@ -631,10 +639,11 @@ def initial_response(sys, T=None, X0=0., input=0, output=None, sys: StateSpace, or TransferFunction LTI system to simulate - T: array-like object, optional - Time vector (argument is autocomputed if not given) + T: array-like or number, optional + Time vector, or simulation time duration if a number (time vector is + autocomputed if not given; see :func:`step_response` for more detail) - X0: array-like object or number, optional + X0: array-like or number, optional Initial condition (default = 0) Numbers are converted to constant arrays with the correct shape. @@ -646,6 +655,10 @@ def initial_response(sys, T=None, X0=0., input=0, output=None, output: int Index of the output that will be used in this simulation. Set to None to not trim outputs + + T_num: number, 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. transpose: bool If True, transpose all input and output arrays (for backward @@ -685,9 +698,8 @@ def initial_response(sys, T=None, X0=0., input=0, output=None, # Create time and input vectors; checking is done in forced_response(...) # The initial vector X0 is created in forced_response(...) if necessary - if T is None: - # TODO: default step size inconsistent with step/impulse_response() - T = _get_response_times(sys, N=1000) + if T is None or np.asarray(T).size == 1: + T = _default_time_vector(sys, N=T_num, tfinal=T) U = np.zeros_like(T) T, yout, _xout = forced_response(sys, T, U, X0, transpose=transpose, @@ -699,7 +711,7 @@ def initial_response(sys, T=None, X0=0., input=0, output=None, return T, yout -def impulse_response(sys, T=None, X0=0., input=0, output=None, +def impulse_response(sys, T=None, X0=0., input=0, output=None, T_num=None, transpose=False, return_x=False, squeeze=True): # pylint: disable=W0622 """Impulse response of a linear system @@ -717,10 +729,11 @@ def impulse_response(sys, T=None, X0=0., input=0, output=None, sys: StateSpace, TransferFunction LTI system to simulate - T: array-like object, optional - Time vector (argument is autocomputed if not given) + T: array-like or number, optional + Time vector, or simulation time duration if a number (time vector is + autocomputed if not given; see :func:`step_response` for more detail) - X0: array-like object or number, optional + X0: array-like or number, optional Initial condition (default = 0) Numbers are converted to constant arrays with the correct shape. @@ -732,6 +745,10 @@ def impulse_response(sys, T=None, X0=0., input=0, output=None, Index of the output that will be used in this simulation. Set to None to not trim outputs + T_num: number, 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. + transpose: bool If True, transpose all input and output arrays (for backward compatibility with MATLAB and scipy.signal.lsim) @@ -770,7 +787,7 @@ def impulse_response(sys, T=None, X0=0., input=0, output=None, """ sys = _get_ss_simo(sys, input, output) - # System has direct feedthrough, can't simulate impulse response + # if system has direct feedthrough, can't simulate impulse response # numerically if np.any(sys.D != 0) and isctime(sys): warnings.warn("System has direct feedthrough: ``D != 0``. The " @@ -779,14 +796,14 @@ def impulse_response(sys, T=None, X0=0., input=0, output=None, "Results may be meaningless!") # create X0 if not given, test if X0 has correct shape. - # Must be done here because it is used for computations here. + # Must be done here because it is used for computations below. n_states = sys.A.shape[0] X0 = _check_convert_array(X0, [(n_states,), (n_states, 1)], 'Parameter ``X0``: \n', squeeze=True) - # Compute T and U, no checks necessary, they will be checked in lsim - if T is None: - T = _get_response_times(sys, N=100) + # Compute T and U, no checks necessary, will be checked in forced_response + if T is None or np.asarray(T).size == 1: + T = _default_time_vector(sys, N=T_num, tfinal=T) U = np.zeros_like(T) # Compute new X0 that contains the impulse @@ -808,21 +825,61 @@ def impulse_response(sys, T=None, X0=0., input=0, output=None, return T, yout - -# Utility function to get response times -def _get_response_times(sys, N=100): - if isctime(sys): - if sys.A.shape == (0, 0): - # No dynamics; use the unit time interval - T = np.linspace(0, 1, N, endpoint=False) - else: - T = _default_response_times(sys.A, N) +# utility function to find time period and time increment using pole locations +def _ideal_tfinal_and_dt(sys): + constant = 7.0 + tolerance = 1e-10 + A = ssdata(sys)[0] + if A.shape == (0,0): + # no dynamics + tfinal = constant * 1.0 + dt = sys.dt if isdtime(sys, strict=True) else 1.0 else: - # For discrete time, use integers - if sys.A.shape == (0, 0): - # No dynamics; use N time steps - T = range(N) + poles = sp.linalg.eigvals(A) + if isdtime(sys, strict=True): + poles = np.log(poles)/sys.dt # z-poles to s-plane using s=(lnz)/dt + + # calculate ideal dt + if isdtime(sys, strict=True): + dt = sys.dt else: - tvec = _default_response_times(sys.A, N) - T = range(int(np.ceil(max(tvec)))) - return T + fastest_natural_frequency = max(abs(poles)) + dt = 1/constant / fastest_natural_frequency + + # calculate ideal tfinal + poles = poles[abs(poles.real) > tolerance] # ignore poles near im axis + if poles.size == 0: + slowest_decay_rate = 1.0 + else: + slowest_decay_rate = min(abs(poles.real)) + tfinal = constant / slowest_decay_rate + + return tfinal, dt + +# test below: ct with pole at the origin is 7 seconds, ct with pole at .5 is 14 s long, +def _default_time_vector(sys, N=None, tfinal=None): + """Returns a time vector suitable for observing the response of the + both the slowest poles and fastest resonant modes. if system is + discrete-time, N is ignored """ + + N_max = 5000 + N_min_ct = 100 + N_min_dt = 7 # more common to see just a few samples in discrete-time + + ideal_tfinal, ideal_dt = _ideal_tfinal_and_dt(sys) + + if isdtime(sys, strict=True): + 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 + else: + N = int(tfinal/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] + + return np.linspace(0, tfinal, N, endpoint=False) \ No newline at end of file
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: