#!/usr/bin/env python # # timeresp_test.py - test time response functions # RMM, 17 Jun 2011 (based on TestMatlab from v0.4c) # # This test suite just goes through and calls all of the MATLAB # functions using different systems and arguments to make sure that # nothing crashes. It doesn't test actual functionality; the module # specific unit tests will do that. import unittest import numpy as np from control.timeresp import * from control.statesp import * from control.xferfcn import TransferFunction, _convert_to_transfer_function from control.dtime import c2d from control.exception import slycot_check class TestTimeresp(unittest.TestCase): def setUp(self): """Set up some systems for testing out MATLAB functions""" A = np.matrix("1. -2.; 3. -4.") B = np.matrix("5.; 7.") C = np.matrix("6. 8.") D = np.matrix("9.") self.siso_ss1 = StateSpace(A, B, C, D) # Create some transfer functions self.siso_tf1 = TransferFunction([1], [1, 2, 1]) self.siso_tf2 = _convert_to_transfer_function(self.siso_ss1) # Create MIMO system, contains ``siso_ss1`` twice A = np.matrix("1. -2. 0. 0.;" "3. -4. 0. 0.;" "0. 0. 1. -2.;" "0. 0. 3. -4. ") B = np.matrix("5. 0.;" "7. 0.;" "0. 5.;" "0. 7. ") C = np.matrix("6. 8. 0. 0.;" "0. 0. 6. 8. ") D = np.matrix("9. 0.;" "0. 9. ") self.mimo_ss1 = StateSpace(A, B, C, D) # Create discrete time systems self.siso_dtf1 = TransferFunction([1], [1, 1, 0.25], True) self.siso_dtf2 = TransferFunction([1], [1, 1, 0.25], 0.2) self.siso_dss1 = tf2ss(self.siso_dtf1) self.siso_dss2 = tf2ss(self.siso_dtf2) self.mimo_dss1 = StateSpace(A, B, C, D, True) self.mimo_dss2 = c2d(self.mimo_ss1, 0.2) def test_step_response(self): # Test SISO system sys = self.siso_ss1 t = np.linspace(0, 1, 10) youttrue = np.array([9., 17.6457, 24.7072, 30.4855, 35.2234, 39.1165, 42.3227, 44.9694, 47.1599, 48.9776]) # SISO call tout, yout = step_response(sys, T=t) np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) np.testing.assert_array_almost_equal(tout, t) # Play with arguments tout, yout = step_response(sys, T=t, X0=0) np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) np.testing.assert_array_almost_equal(tout, t) X0 = np.array([0, 0]) tout, yout = step_response(sys, T=t, X0=X0) np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) np.testing.assert_array_almost_equal(tout, t) tout, yout, xout = step_response(sys, T=t, X0=0, return_x=True) np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) np.testing.assert_array_almost_equal(tout, t) # Test MIMO system, which contains ``siso_ss1`` twice sys = self.mimo_ss1 _t, y_00 = step_response(sys, T=t, input=0, output=0) _t, y_11 = step_response(sys, T=t, input=1, output=1) np.testing.assert_array_almost_equal(y_00, youttrue, decimal=4) np.testing.assert_array_almost_equal(y_11, youttrue, decimal=4) # Make sure continuous and discrete time use same return conventions sysc = self.mimo_ss1 sysd = c2d(sysc, 1) # discrete time system Tvec = np.linspace(0, 10, 11) # make sure to use integer times 0..10 Tc, youtc = step_response(sysc, Tvec, input=0) Td, youtd = step_response(sysd, Tvec, input=0) 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 sys = TransferFunction([1], [1]) t, y = step_response(sys) np.testing.assert_array_equal(y, np.ones(len(t))) # Discrete time, constant system sys = TransferFunction([1], [1], 1) 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 } S = step_info(sys) # 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.get('RiseTime'), Strue.get('RiseTime'), rtol=rtol) np.testing.assert_allclose( S.get('SettlingTime'), Strue.get('SettlingTime'), rtol=rtol) np.testing.assert_allclose( S.get('SettlingMin'), Strue.get('SettlingMin'), rtol=rtol) np.testing.assert_allclose( S.get('SettlingMax'), Strue.get('SettlingMax'), rtol=rtol) np.testing.assert_allclose( S.get('Overshoot'), Strue.get('Overshoot'), rtol=rtol) np.testing.assert_allclose( S.get('Undershoot'), Strue.get('Undershoot'), rtol=rtol) np.testing.assert_allclose( S.get('Peak'), Strue.get('Peak'), rtol=rtol) np.testing.assert_allclose( S.get('PeakTime'), Strue.get('PeakTime'), rtol=rtol) np.testing.assert_allclose( S.get('SteadyStateValue'), 2.50, rtol=rtol) def test_impulse_response(self): # Test SISO system sys = self.siso_ss1 t = np.linspace(0, 1, 10) youttrue = np.array([86., 70.1808, 57.3753, 46.9975, 38.5766, 31.7344, 26.1668, 21.6292, 17.9245, 14.8945]) tout, yout = impulse_response(sys, T=t) np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) np.testing.assert_array_almost_equal(tout, t) # Play with arguments tout, yout = impulse_response(sys, T=t, X0=0) np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) np.testing.assert_array_almost_equal(tout, t) X0 = np.array([0, 0]) tout, yout = impulse_response(sys, T=t, X0=X0) np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) np.testing.assert_array_almost_equal(tout, t) tout, yout, xout = impulse_response(sys, T=t, X0=0, return_x=True) np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) np.testing.assert_array_almost_equal(tout, t) # Test MIMO system, which contains ``siso_ss1`` twice sys = self.mimo_ss1 _t, y_00 = impulse_response(sys, T=t, input=0, output=0) _t, y_11 = impulse_response(sys, T=t, input=1, output=1) np.testing.assert_array_almost_equal(y_00, youttrue, decimal=4) np.testing.assert_array_almost_equal(y_11, youttrue, decimal=4) # Test MIMO system, as mimo, and don't trim outputs sys = self.mimo_ss1 _t, yy = impulse_response(sys, T=t, input=0) np.testing.assert_array_almost_equal( yy, np.vstack((youttrue, np.zeros_like(youttrue))), decimal=4) def test_initial_response(self): # Test SISO system sys = self.siso_ss1 t = np.linspace(0, 1, 10) x0 = np.array([[0.5], [1]]) youttrue = np.array([11., 8.1494, 5.9361, 4.2258, 2.9118, 1.9092, 1.1508, 0.5833, 0.1645, -0.1391]) tout, yout = initial_response(sys, T=t, X0=x0) np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) np.testing.assert_array_almost_equal(tout, t) # Play with arguments tout, yout, xout = initial_response(sys, T=t, X0=x0, return_x=True) np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) np.testing.assert_array_almost_equal(tout, t) # Test MIMO system, which contains ``siso_ss1`` twice sys = self.mimo_ss1 x0 = np.matrix(".5; 1.; .5; 1.") _t, y_00 = initial_response(sys, T=t, X0=x0, input=0, output=0) _t, y_11 = initial_response(sys, T=t, X0=x0, input=1, output=1) np.testing.assert_array_almost_equal(y_00, youttrue, decimal=4) np.testing.assert_array_almost_equal(y_11, youttrue, decimal=4) def test_initial_response_no_trim(self): # test MIMO system without trimming t = np.linspace(0, 1, 10) x0 = np.matrix(".5; 1.; .5; 1.") youttrue = np.array([11., 8.1494, 5.9361, 4.2258, 2.9118, 1.9092, 1.1508, 0.5833, 0.1645, -0.1391]) sys = self.mimo_ss1 _t, yy = initial_response(sys, T=t, X0=x0) np.testing.assert_array_almost_equal( yy, np.vstack((youttrue, youttrue)), decimal=4) def test_forced_response(self): t = np.linspace(0, 1, 10) # compute step response - test with state space, and transfer function # objects u = np.array([1., 1, 1, 1, 1, 1, 1, 1, 1, 1]) youttrue = np.array([9., 17.6457, 24.7072, 30.4855, 35.2234, 39.1165, 42.3227, 44.9694, 47.1599, 48.9776]) tout, yout, _xout = forced_response(self.siso_ss1, t, u) np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) np.testing.assert_array_almost_equal(tout, t) _t, yout, _xout = forced_response(self.siso_tf2, t, u) np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) # test with initial value and special algorithm for ``U=0`` u = 0 x0 = np.matrix(".5; 1.") youttrue = np.array([11., 8.1494, 5.9361, 4.2258, 2.9118, 1.9092, 1.1508, 0.5833, 0.1645, -0.1391]) _t, yout, _xout = forced_response(self.siso_ss1, t, u, x0) np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) # Test MIMO system, which contains ``siso_ss1`` twice # first system: initial value, second system: step response u = np.array([[0., 0, 0, 0, 0, 0, 0, 0, 0, 0], [1., 1, 1, 1, 1, 1, 1, 1, 1, 1]]) x0 = np.array([[.5], [1], [0], [0]]) youttrue = np.array([[11., 8.1494, 5.9361, 4.2258, 2.9118, 1.9092, 1.1508, 0.5833, 0.1645, -0.1391], [9., 17.6457, 24.7072, 30.4855, 35.2234, 39.1165, 42.3227, 44.9694, 47.1599, 48.9776]]) _t, yout, _xout = forced_response(self.mimo_ss1, t, u, x0) np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) # Test discrete MIMO system to use correct convention for input sysc = self.mimo_ss1 dt=t[1]-t[0] sysd = c2d(sysc, dt) # discrete time system Tc, youtc, _xoutc = forced_response(sysc, t, u, x0) Td, youtd, _xoutd = forced_response(sysd, t, u, x0) np.testing.assert_array_equal(Tc.shape, Td.shape) np.testing.assert_array_equal(youtc.shape, youtd.shape) np.testing.assert_array_almost_equal(youtc, youtd, decimal=4) # Test discrete MIMO system without default T argument u = np.array([[0., 0, 0, 0, 0, 0, 0, 0, 0, 0], [1., 1, 1, 1, 1, 1, 1, 1, 1, 1]]) x0 = np.array([[.5], [1], [0], [0]]) youttrue = np.array([[11., 8.1494, 5.9361, 4.2258, 2.9118, 1.9092, 1.1508, 0.5833, 0.1645, -0.1391], [9., 17.6457, 24.7072, 30.4855, 35.2234, 39.1165, 42.3227, 44.9694, 47.1599, 48.9776]]) _t, yout, _xout = forced_response(sysd, U=u, X0=x0) np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) def test_lsim_double_integrator(self): # Note: scipy.signal.lsim fails if A is not invertible A = np.mat("0. 1.;0. 0.") B = np.mat("0.; 1.") C = np.mat("1. 0.") D = 0. sys = StateSpace(A, B, C, D) def check(u, x0, xtrue): _t, yout, xout = forced_response(sys, t, u, x0) np.testing.assert_array_almost_equal(xout, xtrue, decimal=6) ytrue = np.squeeze(np.asarray(C.dot(xtrue))) np.testing.assert_array_almost_equal(yout, ytrue, decimal=6) # test with zero input npts = 10 t = np.linspace(0, 1, npts) u = np.zeros_like(t) x0 = np.array([2., 3.]) xtrue = np.zeros((2, npts)) xtrue[0, :] = x0[0] + t * x0[1] xtrue[1, :] = x0[1] check(u, x0, xtrue) # test with step input u = np.ones_like(t) xtrue = np.array([0.5 * t**2, t]) x0 = np.array([0., 0.]) check(u, x0, xtrue) # test with linear input u = t xtrue = np.array([1./6. * t**3, 0.5 * t**2]) check(u, x0, xtrue) def test_discrete_initial(self): h1 = TransferFunction([1.], [1., 0.], 1.) t, yout = impulse_response(h1, np.arange(4)) np.testing.assert_array_equal(yout, [0., 1., 0., 0.]) @unittest.skipIf(not slycot_check(), "slycot not installed") def test_step_robustness(self): "Unit test: https://github.com/python-control/python-control/issues/240" # Create 2 input, 2 output system num = [ [[0], [1]], [[1], [0]] ] den1 = [ [[1], [1,1]], [[1,4], [1]] ] sys1 = TransferFunction(num, den1) den2 = [ [[1], [1e-10, 1, 1]], [[1,4], [1]] ] # slight perturbation 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) np.testing.assert_array_almost_equal(y1, y2) def test_time_vector(self): "Unit test: https://github.com/python-control/python-control/issues/239" # Discrete time simulations with specified time vectors Tin1 = np.arange(0, 5, 1) # matches dtf1, dss1; multiple of 0.2 Tin2 = np.arange(0, 5, 0.2) # matches dtf2, dss2 Tin3 = np.arange(0, 5, 0.5) # incompatible with 0.2 # Initial conditions to use for the different systems siso_x0 = [1, 2] mimo_x0 = [1, 2, 3, 4] # # Easy cases: make sure that output sample time matches input # # No timebase in system => output should match input # # Initial response tout, yout = initial_response(self.siso_dtf1, Tin2, siso_x0, squeeze=False) self.assertEqual(np.shape(tout), np.shape(yout[0,:])) np.testing.assert_array_equal(tout, Tin2) # Impulse response tout, yout = impulse_response(self.siso_dtf1, Tin2, squeeze=False) self.assertEqual(np.shape(tout), np.shape(yout[0,:])) np.testing.assert_array_equal(tout, Tin2) # Step response tout, yout = step_response(self.siso_dtf1, Tin2, squeeze=False) self.assertEqual(np.shape(tout), np.shape(yout[0,:])) np.testing.assert_array_equal(tout, Tin2) # Forced response with specified time vector tout, yout, xout = forced_response(self.siso_dtf1, Tin2, np.sin(Tin2), squeeze=False) self.assertEqual(np.shape(tout), np.shape(yout[0,:])) np.testing.assert_array_equal(tout, Tin2) # Forced response with no time vector, no sample time (should use 1) tout, yout, xout = forced_response(self.siso_dtf1, None, np.sin(Tin1), squeeze=False) self.assertEqual(np.shape(tout), np.shape(yout[0,:])) np.testing.assert_array_equal(tout, Tin1) # MIMO forced response tout, yout, xout = forced_response(self.mimo_dss1, Tin1, (np.sin(Tin1), np.cos(Tin1)), mimo_x0) self.assertEqual(np.shape(tout), np.shape(yout[0,:])) self.assertEqual(np.shape(tout), np.shape(yout[1,:])) np.testing.assert_array_equal(tout, Tin1) # Matching timebase in system => output should match input # # Initial response tout, yout = initial_response(self.siso_dtf2, Tin2, siso_x0, squeeze=False) self.assertEqual(np.shape(tout), np.shape(yout[0,:])) np.testing.assert_array_equal(tout, Tin2) # Impulse response tout, yout = impulse_response(self.siso_dtf2, Tin2, squeeze=False) self.assertEqual(np.shape(tout), np.shape(yout[0,:])) np.testing.assert_array_equal(tout, Tin2) # Step response tout, yout = step_response(self.siso_dtf2, Tin2, squeeze=False) self.assertEqual(np.shape(tout), np.shape(yout[0,:])) np.testing.assert_array_equal(tout, Tin2) # Forced response tout, yout, xout = forced_response(self.siso_dtf2, Tin2, np.sin(Tin2), squeeze=False) self.assertEqual(np.shape(tout), np.shape(yout[0,:])) np.testing.assert_array_equal(tout, Tin2) # Forced response with no time vector, use sample time tout, yout, xout = forced_response(self.siso_dtf2, None, np.sin(Tin2), squeeze=False) self.assertEqual(np.shape(tout), np.shape(yout[0,:])) np.testing.assert_array_equal(tout, Tin2) # Compatible timebase in system => output should match input # # Initial response tout, yout = initial_response(self.siso_dtf2, Tin1, siso_x0, squeeze=False) self.assertEqual(np.shape(tout), np.shape(yout[0,:])) np.testing.assert_array_equal(tout, Tin1) # Impulse response tout, yout = impulse_response(self.siso_dtf2, Tin1, squeeze=False) self.assertEqual(np.shape(tout), np.shape(yout[0,:])) np.testing.assert_array_equal(tout, Tin1) # Step response tout, yout = step_response(self.siso_dtf2, Tin1, squeeze=False) self.assertEqual(np.shape(tout), np.shape(yout[0,:])) np.testing.assert_array_equal(tout, Tin1) # Forced response tout, yout, xout = forced_response(self.siso_dtf2, Tin1, np.sin(Tin1), squeeze=False) self.assertEqual(np.shape(tout), np.shape(yout[0,:])) np.testing.assert_array_equal(tout, Tin1) # # Interpolation of the input (to match scipy.signal.dlsim) # # Initial response tout, yout, xout = forced_response(self.siso_dtf2, Tin1, np.sin(Tin1), interpolate=True, squeeze=False) self.assertEqual(np.shape(tout), np.shape(yout[0,:])) self.assertTrue(np.allclose(tout[1:] - tout[:-1], self.siso_dtf2.dt)) # # Incompatible cases: make sure an error is thrown # # System timebase and given time vector are incompatible # # Initial response with self.assertRaises(Exception) as context: tout, yout = initial_response(self.siso_dtf2, Tin3, siso_x0, squeeze=False) self.assertTrue(isinstance(context.exception, ValueError)) def test_discrete_time_steps(self): """Make sure rounding errors in sample time are handled properly""" # See https://github.com/python-control/python-control/issues/332) # # These tests play around with the input time vector to make sure that # small rounding errors don't generate spurious errors. # Discrete time system to use for simulation # self.siso_dtf2 = TransferFunction([1], [1, 1, 0.25], 0.2) # Set up a time range and simulate T = np.arange(0, 100, 0.2) tout1, yout1 = step_response(self.siso_dtf2, T) # Simulate every other time step T = np.arange(0, 100, 0.4) tout2, yout2 = step_response(self.siso_dtf2, T) np.testing.assert_array_almost_equal(tout1[::2], tout2) np.testing.assert_array_almost_equal(yout1[::2], yout2) # Add a small error into some of the time steps T = np.arange(0, 100, 0.2) T[1:-2:2] -= 1e-12 # tweak second value and a few others tout3, yout3 = step_response(self.siso_dtf2, T) np.testing.assert_array_almost_equal(tout1, tout3) np.testing.assert_array_almost_equal(yout1, yout3) # Add a small error into some of the time steps (w/ skipping) T = np.arange(0, 100, 0.4) T[1:-2:2] -= 1e-12 # tweak second value and a few others tout4, yout4 = step_response(self.siso_dtf2, T) np.testing.assert_array_almost_equal(tout2, tout4) np.testing.assert_array_almost_equal(yout2, yout4) # Make sure larger errors *do* generate an error T = np.arange(0, 100, 0.2) T[1:-2:2] -= 1e-3 # change second value and a few others self.assertRaises(ValueError, step_response, self.siso_dtf2, T) def test_time_series_data_convention(self): """Make sure time series data matches documentation conventions""" # SISO continuous time t, y = step_response(self.siso_ss1) self.assertTrue(isinstance(t, np.ndarray) and not isinstance(t, np.matrix)) self.assertTrue(len(t.shape) == 1) self.assertTrue(len(y.shape) == 1) # SISO returns "scalar" output self.assertTrue(len(t) == len(y)) # Allows direct plotting of output # SISO discrete time t, y = step_response(self.siso_dss1) self.assertTrue(isinstance(t, np.ndarray) and not isinstance(t, np.matrix)) self.assertTrue(len(t.shape) == 1) self.assertTrue(len(y.shape) == 1) # SISO returns "scalar" output self.assertTrue(len(t) == len(y)) # Allows direct plotting of output # MIMO continuous time tin = np.linspace(0, 10, 100) uin = [np.sin(tin), np.cos(tin)] t, y, x = forced_response(self.mimo_ss1, tin, uin) self.assertTrue(isinstance(t, np.ndarray) and not isinstance(t, np.matrix)) self.assertTrue(len(t.shape) == 1) self.assertTrue(len(y[0].shape) == 1) self.assertTrue(len(y[1].shape) == 1) self.assertTrue(len(t) == len(y[0])) self.assertTrue(len(t) == len(y[1])) # MIMO discrete time tin = np.linspace(0, 10, 100) uin = [np.sin(tin), np.cos(tin)] t, y, x = forced_response(self.mimo_dss1, tin, uin) self.assertTrue(isinstance(t, np.ndarray) and not isinstance(t, np.matrix)) self.assertTrue(len(t.shape) == 1) self.assertTrue(len(y[0].shape) == 1) self.assertTrue(len(y[1].shape) == 1) self.assertTrue(len(t) == len(y[0])) self.assertTrue(len(t) == len(y[1])) # Allow input time as 2D array (output should be 1D) tin = np.array(np.linspace(0, 10, 100), ndmin=2) t, y = step_response(self.siso_ss1, tin) self.assertTrue(isinstance(t, np.ndarray) and not isinstance(t, np.matrix)) self.assertTrue(len(t.shape) == 1) self.assertTrue(len(y.shape) == 1) # SISO returns "scalar" output self.assertTrue(len(t) == len(y)) # Allows direct plotting of output if __name__ == '__main__': unittest.main()