diff --git a/control/config.py b/control/config.py index ef3184a5e..a713e33d2 100644 --- a/control/config.py +++ b/control/config.py @@ -17,7 +17,6 @@ _control_defaults = { 'control.default_dt': 0, 'control.squeeze_frequency_response': None, - 'control.squeeze_time_response': True, 'control.squeeze_time_response': None, 'forced_response.return_x': False, } diff --git a/control/iosys.py b/control/iosys.py index d16cbce93..66ca20ebb 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -1428,8 +1428,9 @@ def input_output_response(sys, T, U=0., X0=0, params={}, method='RK45', for i in range(len(T)): u = U[i] if len(U.shape) == 1 else U[:, i] y[:, i] = sys._out(T[i], [], u) - return _process_time_response(sys, T, y, [], transpose=transpose, - return_x=return_x, squeeze=squeeze) + return _process_time_response( + sys, T, y, np.array((0, 0, np.asarray(T).size)), + transpose=transpose, return_x=return_x, squeeze=squeeze) # create X0 if not given, test if X0 has correct shape X0 = _check_convert_array(X0, [(nstates,), (nstates, 1)], diff --git a/control/tests/matlab2_test.py b/control/tests/matlab2_test.py index 5db4156df..633ceef6f 100644 --- a/control/tests/matlab2_test.py +++ b/control/tests/matlab2_test.py @@ -121,25 +121,25 @@ def test_step(self, SISO_mats, MIMO_mats, mplcleanup): #print("gain:", dcgain(sys)) subplot2grid(plot_shape, (0, 0)) - t, y = step(sys) + y, t = step(sys) plot(t, y) subplot2grid(plot_shape, (0, 1)) T = linspace(0, 2, 100) X0 = array([1, 1]) - t, y = step(sys, T, X0) + y, t = step(sys, T, X0) plot(t, y) # Test output of state vector - t, y, x = step(sys, return_x=True) + y, t, x = step(sys, return_x=True) #Test MIMO system A, B, C, D = MIMO_mats sys = ss(A, B, C, D) subplot2grid(plot_shape, (0, 2)) - t, y = step(sys) - plot(t, y) + y, t = step(sys) + plot(t, y[:, 0, 0]) def test_impulse(self, SISO_mats, mplcleanup): A, B, C, D = SISO_mats @@ -168,8 +168,8 @@ def test_impulse_mimo(self, MIMO_mats, mplcleanup): #Test MIMO system A, B, C, D = MIMO_mats sys = ss(A, B, C, D) - t, y = impulse(sys) - plot(t, y, label='MIMO System') + y, t = impulse(sys) + plot(t, y[:, :, 0], label='MIMO System') legend(loc='best') #show() diff --git a/control/tests/timeresp_test.py b/control/tests/timeresp_test.py index e5c7471b6..4bf52b498 100644 --- a/control/tests/timeresp_test.py +++ b/control/tests/timeresp_test.py @@ -347,7 +347,7 @@ def test_impulse_response_mimo(self, mimo_ss2): yref_notrim = np.zeros((2, len(t))) yref_notrim[:1, :] = yref _t, yy = impulse_response(sys, T=t, input=0) - np.testing.assert_array_almost_equal(yy, yref_notrim, decimal=4) + np.testing.assert_array_almost_equal(yy[:,0,:], yref_notrim, decimal=4) @pytest.mark.skipif(StrictVersion(sp.__version__) < "1.3", reason="requires SciPy 1.3 or greater") @@ -639,9 +639,10 @@ def test_time_vector(self, tsystem, fun, squeeze, matarrayout): if hasattr(tsystem, 't'): # tout should always match t, which has shape (n, ) np.testing.assert_allclose(tout, tsystem.t) - if squeeze is False or sys.outputs > 1: + + if squeeze is False or not sys.issiso(): assert yout.shape[0] == sys.outputs - assert yout.shape[1] == tout.shape[0] + assert yout.shape[-1] == tout.shape[0] else: assert yout.shape == tout.shape @@ -725,21 +726,22 @@ def test_time_series_data_convention_2D(self, siso_ss1): @pytest.mark.usefixtures("editsdefaults") @pytest.mark.parametrize("fcn", [ct.ss, ct.tf, ct.ss2io]) - @pytest.mark.parametrize("nstate, nout, ninp, squeeze, shape", [ - [1, 1, 1, None, (8,)], - [2, 1, 1, True, (8,)], - [3, 1, 1, False, (1, 8)], - [3, 2, 1, None, (2, 8)], - [4, 2, 1, True, (2, 8)], - [5, 2, 1, False, (2, 8)], - [3, 1, 2, None, (1, 8)], - [4, 1, 2, True, (8,)], - [5, 1, 2, False, (1, 8)], - [4, 2, 2, None, (2, 8)], - [5, 2, 2, True, (2, 8)], - [6, 2, 2, False, (2, 8)], + @pytest.mark.parametrize("nstate, nout, ninp, squeeze, shape1, shape2", [ + # state out in squeeze in/out out-only + [1, 1, 1, None, (8,), (8,)], + [2, 1, 1, True, (8,), (8,)], + [3, 1, 1, False, (1, 1, 8), (1, 8)], + [3, 2, 1, None, (2, 1, 8), (2, 8)], + [4, 2, 1, True, (2, 8), (2, 8)], + [5, 2, 1, False, (2, 1, 8), (2, 8)], + [3, 1, 2, None, (1, 2, 8), (1, 8)], + [4, 1, 2, True, (2, 8), (8,)], + [5, 1, 2, False, (1, 2, 8), (1, 8)], + [4, 2, 2, None, (2, 2, 8), (2, 8)], + [5, 2, 2, True, (2, 2, 8), (2, 8)], + [6, 2, 2, False, (2, 2, 8), (2, 8)], ]) - def test_squeeze(self, fcn, nstate, nout, ninp, squeeze, shape): + def test_squeeze(self, fcn, nstate, nout, ninp, squeeze, shape1, shape2): # Figure out if we have SciPy 1+ scipy0 = StrictVersion(sp.__version__) < '1.0' @@ -750,27 +752,56 @@ def test_squeeze(self, fcn, nstate, nout, ninp, squeeze, shape): else: sys = fcn(ct.rss(nstate, nout, ninp, strictly_proper=True)) - # Keep track of expect users warnings - warntype = UserWarning if sys.inputs > 1 else None - # Generate the time and input vectors tvec = np.linspace(0, 1, 8) uvec = np.dot( np.ones((sys.inputs, 1)), np.reshape(np.sin(tvec), (1, 8))) + # # Pass squeeze argument and make sure the shape is correct - with pytest.warns(warntype, match="Converting MIMO system"): - _, yvec = ct.impulse_response(sys, tvec, squeeze=squeeze) - assert yvec.shape == shape + # + # For responses that are indexed by the input, check against shape1 + # For responses that have no/fixed input, check against shape2 + # - _, yvec = ct.initial_response(sys, tvec, 1, squeeze=squeeze) - assert yvec.shape == shape + # Impulse response + if isinstance(sys, StateSpace): + # Check the states as well + _, yvec, xvec = ct.impulse_response( + sys, tvec, squeeze=squeeze, return_x=True) + if sys.issiso(): + assert xvec.shape == (sys.states, 8) + else: + assert xvec.shape == (sys.states, sys.inputs, 8) + else: + _, yvec = ct.impulse_response(sys, tvec, squeeze=squeeze) + assert yvec.shape == shape1 - with pytest.warns(warntype, match="Converting MIMO system"): + # Step response + if isinstance(sys, StateSpace): + # Check the states as well + _, yvec, xvec = ct.step_response( + sys, tvec, squeeze=squeeze, return_x=True) + if sys.issiso(): + assert xvec.shape == (sys.states, 8) + else: + assert xvec.shape == (sys.states, sys.inputs, 8) + else: _, yvec = ct.step_response(sys, tvec, squeeze=squeeze) - assert yvec.shape == shape + assert yvec.shape == shape1 + + # Initial response (only indexed by output) + if isinstance(sys, StateSpace): + # Check the states as well + _, yvec, xvec = ct.initial_response( + sys, tvec, 1, squeeze=squeeze, return_x=True) + assert xvec.shape == (sys.states, 8) + else: + _, yvec = ct.initial_response(sys, tvec, 1, squeeze=squeeze) + assert yvec.shape == shape2 + # Forced response (only indexed by output) if isinstance(sys, StateSpace): # Check the states as well _, yvec, xvec = ct.forced_response( @@ -779,39 +810,39 @@ def test_squeeze(self, fcn, nstate, nout, ninp, squeeze, shape): else: # Just check the input/output response _, yvec = ct.forced_response(sys, tvec, uvec, 0, squeeze=squeeze) - assert yvec.shape == shape + assert yvec.shape == shape2 # Test cases where we choose a subset of inputs and outputs _, yvec = ct.step_response( sys, tvec, input=ninp-1, output=nout-1, squeeze=squeeze) - # Possible code if we implemenet a squeeze='siso' option if squeeze is False: # Shape should be unsqueezed - assert yvec.shape == (1, 8) + assert yvec.shape == (1, 1, 8) else: # Shape should be squeezed assert yvec.shape == (8, ) - # For InputOutputSystems, also test input_output_response + # For InputOutputSystems, also test input/output response if isinstance(sys, ct.InputOutputSystem) and not scipy0: _, yvec = ct.input_output_response(sys, tvec, uvec, squeeze=squeeze) - assert yvec.shape == shape + assert yvec.shape == shape2 # # Changing config.default to False should return 3D frequency response # ct.config.set_defaults('control', squeeze_time_response=False) - with pytest.warns(warntype, match="Converting MIMO system"): - _, yvec = ct.impulse_response(sys, tvec) - assert yvec.shape == (sys.outputs, 8) + _, yvec = ct.impulse_response(sys, tvec) + if squeeze is not True or sys.inputs > 1 or sys.outputs > 1: + assert yvec.shape == (sys.outputs, sys.inputs, 8) - _, yvec = ct.initial_response(sys, tvec, 1) - assert yvec.shape == (sys.outputs, 8) + _, yvec = ct.step_response(sys, tvec) + if squeeze is not True or sys.inputs > 1 or sys.outputs > 1: + assert yvec.shape == (sys.outputs, sys.inputs, 8) - with pytest.warns(warntype, match="Converting MIMO system"): - _, yvec = ct.step_response(sys, tvec) - assert yvec.shape == (sys.outputs, 8) + _, yvec = ct.initial_response(sys, tvec, 1) + if squeeze is not True or sys.outputs > 1: + assert yvec.shape == (sys.outputs, 8) if isinstance(sys, ct.StateSpace): _, yvec, xvec = ct.forced_response( @@ -819,12 +850,14 @@ def test_squeeze(self, fcn, nstate, nout, ninp, squeeze, shape): assert xvec.shape == (sys.states, 8) else: _, yvec = ct.forced_response(sys, tvec, uvec, 0) - assert yvec.shape == (sys.outputs, 8) + if squeeze is not True or sys.outputs > 1: + assert yvec.shape == (sys.outputs, 8) # For InputOutputSystems, also test input_output_response if isinstance(sys, ct.InputOutputSystem) and not scipy0: _, yvec = ct.input_output_response(sys, tvec, uvec) - assert yvec.shape == (sys.noutputs, 8) + if squeeze is not True or sys.outputs > 1: + assert yvec.shape == (sys.outputs, 8) @pytest.mark.parametrize("fcn", [ct.ss, ct.tf, ct.ss2io]) def test_squeeze_exception(self, fcn): @@ -861,3 +894,37 @@ def test_squeeze_0_8_4(self, nstate, nout, ninp, squeeze, shape): _, yvec = ct.initial_response(sys, tvec, 1, squeeze=squeeze) assert yvec.shape == shape + + @pytest.mark.parametrize( + "nstate, nout, ninp, squeeze, ysh_in, ysh_no, xsh_in", [ + [4, 1, 1, None, (8,), (8,), (8, 4)], + [4, 1, 1, True, (8,), (8,), (8, 4)], + [4, 1, 1, False, (8, 1, 1), (8, 1), (8, 4)], + [4, 2, 1, None, (8, 2, 1), (8, 2), (8, 4, 1)], + [4, 2, 1, True, (8, 2), (8, 2), (8, 4, 1)], + [4, 2, 1, False, (8, 2, 1), (8, 2), (8, 4, 1)], + [4, 1, 2, None, (8, 1, 2), (8, 1), (8, 4, 2)], + [4, 1, 2, True, (8, 2), (8,), (8, 4, 2)], + [4, 1, 2, False, (8, 1, 2), (8, 1), (8, 4, 2)], + [4, 2, 2, None, (8, 2, 2), (8, 2), (8, 4, 2)], + [4, 2, 2, True, (8, 2, 2), (8, 2), (8, 4, 2)], + [4, 2, 2, False, (8, 2, 2), (8, 2), (8, 4, 2)], + ]) + def test_response_transpose( + self, nstate, nout, ninp, squeeze, ysh_in, ysh_no, xsh_in): + sys = ct.rss(nstate, nout, ninp) + T = np.linspace(0, 1, 8) + + # Step response - input indexed + t, y, x = ct.step_response( + sys, T, transpose=True, return_x=True, squeeze=squeeze) + assert t.shape == (T.size, ) + assert y.shape == ysh_in + assert x.shape == xsh_in + + # Initial response - no input indexing + t, y, x = ct.initial_response( + sys, T, 1, transpose=True, return_x=True, squeeze=squeeze) + assert t.shape == (T.size, ) + assert y.shape == ysh_no + assert x.shape == (T.size, sys.states) diff --git a/control/timeresp.py b/control/timeresp.py index b8dc6631a..bfa2ec149 100644 --- a/control/timeresp.py +++ b/control/timeresp.py @@ -91,8 +91,7 @@ # Helper function for checking array-like parameters def _check_convert_array(in_obj, legal_shapes, err_msg_start, squeeze=False, transpose=False): - """ - Helper function for checking array_like parameters. + """Helper function for checking array_like parameters. * Check type and shape of ``in_obj``. * Convert ``in_obj`` to an array if necessary. @@ -128,8 +127,8 @@ def _check_convert_array(in_obj, legal_shapes, err_msg_start, squeeze=False, For example: ``array([[1,2,3]])`` is converted to ``array([1, 2, 3])`` - transpose : bool - If True, assume that input arrays are transposed for the standard + transpose : bool, optional + If True, assume that 2D input arrays are transposed from the standard format. Used to convert MATLAB-style inputs to our format. Returns @@ -137,6 +136,7 @@ def _check_convert_array(in_obj, legal_shapes, err_msg_start, squeeze=False, out_array : array The checked and converted contents of ``in_obj``. + """ # convert nearly everything to an array. out_array = np.asarray(in_obj) @@ -226,9 +226,10 @@ def forced_response(sys, T=None, U=0., X0=0., transpose=False, X0 : array_like or float, optional Initial condition (default = 0). - transpose : bool, optional (default=False) + transpose : bool, optional If True, transpose all input and output arrays (for backward - compatibility with MATLAB and :func:`scipy.signal.lsim`) + compatibility with MATLAB and :func:`scipy.signal.lsim`). Default + value is False. interpolate : bool, optional (default=False) If True and system is a discrete time system, the input will @@ -456,36 +457,122 @@ def forced_response(sys, T=None, U=0., X0=0., transpose=False, # Process time responses in a uniform way -def _process_time_response(sys, tout, yout, xout, transpose=None, - return_x=False, squeeze=None): - # If squeeze was not specified, figure out the default +def _process_time_response( + sys, tout, yout, xout, transpose=None, return_x=False, + squeeze=None, input=None, output=None): + """Process time response signals. + + This function processes the outputs of the time response functions and + processes the transpose and squeeze keywords. + + Parameters + ---------- + T : 1D array + Time values of the output + + yout : ndarray + Response of the system. This can either be a 1D array indexed by time + (for SISO systems), a 2D array indexed by output and time (for MIMO + systems with no input indexing, such as initial_response or forced + response) or a 3D array indexed by output, input, and time. + + xout : array, optional + Individual response of each x variable (if return_x is True). For a + SISO system (or if a single input is specified), This should be a 2D + array indexed by the state index and time (for single input systems) + or a 3D array indexed by state, input, and time. + + transpose : bool, optional + If True, transpose all input and output arrays (for backward + compatibility with MATLAB and :func:`scipy.signal.lsim`). Default + value is False. + + return_x : bool, optional + If True, return the state vector (default = False). + + squeeze : bool, optional + By default, if a system is single-input, single-output (SISO) then the + output response is returned as a 1D array (indexed by time). If + squeeze=True, remove single-dimensional entries from the shape of the + output even if the system is not SISO. If squeeze=False, keep the + output as a 3D array (indexed by the output, input, and time) even if + the system is SISO. The default value can be set using + config.defaults['control.squeeze_time_response']. + + input : int, optional + If present, the response represents only the listed input. + + output : int, optional + If present, the response represents only the listed output. + + Returns + ------- + T : 1D array + Time values of the output + + yout : ndarray + Response of the system. If the system is SISO and squeeze is not + True, the array is 1D (indexed by time). If the system is not SISO or + squeeze is False, the array is either 2D (indexed by output and time) + or 3D (indexed by input, output, and time). + + xout : array, optional + Individual response of each x variable (if return_x is True). For a + SISO system (or if a single input is specified), xout is a 2D array + indexed by the state index and time. For a non-SISO system, xout is a + 3D array indexed by the state, the input, and time. The shape of xout + is not affected by the ``squeeze`` keyword. + """ + # If squeeze was not specified, figure out the default (might remain None) if squeeze is None: squeeze = config.defaults['control.squeeze_time_response'] - # Figure out whether and now to squeeze output data + # Determine if the system is SISO + issiso = sys.issiso() or (input is not None and output is not None) + + # Figure out whether and how to squeeze output data if squeeze is True: # squeeze all dimensions yout = np.squeeze(yout) elif squeeze is False: # squeeze no dimensions pass elif squeeze is None: # squeeze signals if SISO - yout = yout[0] if sys.issiso() else yout + if issiso: + if len(yout.shape) == 3: + yout = yout[0][0] # remove input and output + else: + yout = yout[0] # remove input else: raise ValueError("unknown squeeze value") + # Figure out whether and how to squeeze the state data + if issiso and len(xout.shape) > 2: + xout = xout[:, 0, :] # remove input + # See if we need to transpose the data back into MATLAB form if transpose: + # Transpose time vector in case we are using np.matrix tout = np.transpose(tout) - yout = np.transpose(yout) - xout = np.transpose(xout) + + # For signals, put the last index (time) into the first slot + yout = np.transpose(yout, np.roll(range(yout.ndim), 1)) + xout = np.transpose(xout, np.roll(range(xout.ndim), 1)) # Return time, output, and (optionally) state return (tout, yout, xout) if return_x else (tout, yout) def _get_ss_simo(sys, input=None, output=None, squeeze=None): - """Return a SISO or SIMO state-space version of sys + """Return a SISO or SIMO state-space version of sys. + + This function converts the given system to a state space system in + preparation for simulation and sets the system matrixes to match the + desired input and output. + + If input is not specified, select first input and issue warning (legacy + behavior that should eventually not be used). + + If the output is not specified, report on all outputs. - If input is not specified, select first input and issue warning """ # If squeeze was not specified, figure out the default if squeeze is None: @@ -514,12 +601,13 @@ def _get_ss_simo(sys, input=None, output=None, squeeze=None): def step_response(sys, T=None, X0=0., input=None, output=None, T_num=None, transpose=False, return_x=False, squeeze=None): # pylint: disable=W0622 - """Step response of a linear system + """Compute the step response for a linear system. - If the system has multiple inputs or outputs (MIMO), one input has - to be selected for the simulation. Optionally, one output may be - selected. The parameters `input` and `output` do this. All other - inputs are set to 0, all other outputs are ignored. + If the system has multiple inputs and/or multiple outputs, the step + response is computed for each input/output pair, with all other inputs set + to zero. Optionally, a single input and/or single output can be selected, + in which case all other inputs are set to 0 and all other outputs are + ignored. For information on the **shape** of parameters `T`, `X0` and return values `T`, `yout`, see :ref:`time-series-convention`. @@ -545,11 +633,12 @@ def step_response(sys, T=None, X0=0., input=None, output=None, T_num=None, arrays with the correct shape. input : int, optional - Index of the input that will be used in this simulation. Default = 0. + Only compute the step response for the listed input. If not + specified, the step responses for each independent input are computed. output : int, optional - Index of the output that will be used in this simulation. Set to None - to not trim outputs + Only report the step response for the listed output. If not + specified, all outputs are reported. T_num : int, optional Number of time steps to use in simulation if T is not provided as an @@ -557,7 +646,8 @@ def step_response(sys, T=None, X0=0., input=None, output=None, T_num=None, transpose : bool, optional If True, transpose all input and output arrays (for backward - compatibility with MATLAB and :func:`scipy.signal.lsim`) + compatibility with MATLAB and :func:`scipy.signal.lsim`). Default + value is False. return_x : bool, optional If True, return the state vector (default = False). @@ -567,23 +657,27 @@ def step_response(sys, T=None, X0=0., input=None, output=None, T_num=None, output response is returned as a 1D array (indexed by time). If squeeze=True, remove single-dimensional entries from the shape of the output even if the system is not SISO. If squeeze=False, keep the - output as a 2D array (indexed by the output number and time) even if + output as a 3D array (indexed by the output, input, and time) even if the system is SISO. The default value can be set using config.defaults['control.squeeze_time_response']. Returns ------- - T : array + T : 1D array Time values of the output - yout : array + yout : ndarray Response of the system. If the system is SISO and squeeze is not True, the array is 1D (indexed by time). If the system is not SISO or - squeeze is False, the array is 2D (indexed by the output number and + squeeze is False, the array is 3D (indexed by the input, output, and time). xout : array, optional - Individual response of each x variable (if return_x is True). + Individual response of each x variable (if return_x is True). For a + SISO system (or if a single input is specified), xout is a 2D array + indexed by the state index and time. For a non-SISO system, xout is a + 3D array indexed by the state, the input, and time. The shape of xout + is not affected by the ``squeeze`` keyword. See Also -------- @@ -599,13 +693,46 @@ def step_response(sys, T=None, X0=0., input=None, output=None, T_num=None, >>> T, yout = step_response(sys, T, X0) """ - squeeze, sys = _get_ss_simo(sys, input, output, squeeze=squeeze) + # Create the time and input vectors if T is None or np.asarray(T).size == 1: T = _default_time_vector(sys, N=T_num, tfinal=T, is_step=True) U = np.ones_like(T) - return forced_response(sys, T, U, X0, transpose=transpose, - return_x=return_x, squeeze=squeeze) + # If we are passed a transfer function and X0 is non-zero, warn the user + if isinstance(sys, TransferFunction) and np.any(X0 != 0): + warnings.warn( + "Non-zero initial condition given for transfer function system. " + "Internal conversation to state space used; may not be consistent " + "with given X0.") + + # Convert to state space so that we can simulate + sys = _convert_to_statespace(sys) + + # Set up arrays to handle the output + ninputs = sys.inputs if input is None else 1 + noutputs = sys.outputs if output is None else 1 + yout = np.empty((noutputs, ninputs, np.asarray(T).size)) + xout = np.empty((sys.states, ninputs, np.asarray(T).size)) + + # Simulate the response for each input + for i in range(sys.inputs): + # If input keyword was specified, only simulate for that input + if isinstance(input, int) and i != input: + continue + + # Create a set of single inputs system for simulation + squeeze, simo = _get_ss_simo(sys, i, output, squeeze=squeeze) + + out = forced_response(simo, T, U, X0, transpose=False, + return_x=return_x, squeeze=True) + inpidx = i if input is None else 0 + yout[:, inpidx, :] = out[1] + if return_x: + xout[:, i, :] = out[2] + + return _process_time_response( + sys, out[0], yout, xout, transpose=transpose, return_x=return_x, + squeeze=squeeze, input=input, output=output) def step_info(sys, T=None, T_num=None, SettlingTimeThreshold=0.02, @@ -788,12 +915,13 @@ def initial_response(sys, T=None, X0=0., input=0, output=None, T_num=None, def impulse_response(sys, T=None, X0=0., input=None, output=None, T_num=None, transpose=False, return_x=False, squeeze=None): # pylint: disable=W0622 - """Impulse response of a linear system + """Compute the impulse response for a linear system. - If the system has multiple inputs or outputs (MIMO), one input has - to be selected for the simulation. Optionally, one output may be - selected. The parameters `input` and `output` do this. All other - inputs are set to 0, all other outputs are ignored. + If the system has multiple inputs and/or multiple outputs, the impulse + response is computed for each input/output pair, with all other inputs set + to zero. Optionally, a single input and/or single output can be selected, + in which case all other inputs are set to 0 and all other outputs are + ignored. For information on the **shape** of parameters `T`, `X0` and return values `T`, `yout`, see :ref:`time-series-convention`. @@ -813,19 +941,22 @@ def impulse_response(sys, T=None, X0=0., input=None, output=None, T_num=None, Numbers are converted to constant arrays with the correct shape. input : int, optional - Index of the input that will be used in this simulation. Default = 0. + Only compute the impulse response for the listed input. If not + specified, the impulse responses for each independent input are + computed. output : int, optional - Index of the output that will be used in this simulation. Set to None - to not trim outputs + Only report the step response for the listed output. If not + specified, all outputs are reported. 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. - transpose : bool + transpose : bool, optional If True, transpose all input and output arrays (for backward - compatibility with MATLAB and :func:`scipy.signal.lsim`) + compatibility with MATLAB and :func:`scipy.signal.lsim`). Default + value is False. return_x : bool, optional If True, return the state vector (default = False). @@ -851,7 +982,11 @@ def impulse_response(sys, T=None, X0=0., input=None, output=None, T_num=None, time). xout : array, optional - Individual response of each x variable (if return_x is True). + Individual response of each x variable (if return_x is True). For a + SISO system (or if a single input is specified), xout is a 2D array + indexed by the state index and time. For a non-SISO system, xout is a + 3D array indexed by the state, the input, and time. The shape of xout + is not affected by the ``squeeze`` keyword. See Also -------- @@ -868,10 +1003,10 @@ def impulse_response(sys, T=None, X0=0., input=None, output=None, T_num=None, >>> T, yout = impulse_response(sys, T, X0) """ - squeeze, sys = _get_ss_simo(sys, input, output, squeeze=squeeze) + # Convert to state space so that we can simulate + sys = _convert_to_statespace(sys) - # if system has direct feedthrough, can't simulate impulse response - # numerically + # Check to make sure there is not a direct term if np.any(sys.D != 0) and isctime(sys): warnings.warn("System has direct feedthrough: ``D != 0``. The " "infinite impulse at ``t=0`` does not appear in the " @@ -889,19 +1024,48 @@ def impulse_response(sys, T=None, X0=0., input=None, output=None, T_num=None, T = _default_time_vector(sys, N=T_num, tfinal=T, is_step=False) U = np.zeros_like(T) - # Compute new X0 that contains the impulse - # We can't put the impulse into U because there is no numerical - # representation for it (infinitesimally short, infinitely high). - # See also: http://www.mathworks.com/support/tech-notes/1900/1901.html - if isctime(sys): - B = np.asarray(sys.B).squeeze() - new_X0 = B + X0 - else: - new_X0 = X0 - U[0] = 1./sys.dt # unit area impulse - - return forced_response(sys, T, U, new_X0, transpose=transpose, - return_x=return_x, squeeze=squeeze) + # Set up arrays to handle the output + ninputs = sys.inputs if input is None else 1 + noutputs = sys.outputs if output is None else 1 + yout = np.empty((noutputs, ninputs, np.asarray(T).size)) + xout = np.empty((sys.states, ninputs, np.asarray(T).size)) + + # Simulate the response for each input + for i in range(sys.inputs): + # If input keyword was specified, only handle that case + if isinstance(input, int) and i != input: + continue + + # Get the system we need to simulate + squeeze, simo = _get_ss_simo(sys, i, output, squeeze=squeeze) + + # + # Compute new X0 that contains the impulse + # + # We can't put the impulse into U because there is no numerical + # representation for it (infinitesimally short, infinitely high). + # See also: http://www.mathworks.com/support/tech-notes/1900/1901.html + # + if isctime(simo): + B = np.asarray(simo.B).squeeze() + new_X0 = B + X0 + else: + new_X0 = X0 + U[0] = 1./simo.dt # unit area impulse + + # Simulate the impulse response fo this input + out = forced_response(simo, T, U, new_X0, transpose=False, + return_x=return_x, squeeze=squeeze) + + # Store the output (and states) + inpidx = i if input is None else 0 + yout[:, inpidx, :] = out[1] + if return_x: + xout[:, i, :] = out[2] + + return _process_time_response( + sys, out[0], yout, xout, transpose=transpose, return_x=return_x, + squeeze=squeeze, input=input, output=output) # utility function to find time period and time increment using pole locations