def test_isdtime(self, objfun, arg, dt, ref, strictref): """Test isdtime and isctime functions to follow convention""" obj = objfun(*arg, dt=dt) assert isdtime(obj) == ref assert isdtime(obj, strict=True) == strictref if dt is not None: ref = not ref strictref = not strictref assert isctime(obj) == ref assert isctime(obj, strict=True) == strictref
def stepinfo(sys, display=False, T=None, SettlingTimeThreshold=0.05, RiseTimeLimits=(0.1, 0.9)): T_max = get_T_max([sys], T=None, N=200) if T is None: if ctl.isctime(sys): T = np.linspace(0, T_max, 200) else: # For discrete time, use integers T = np.arange(0, T_max, sys.dt) info = ctl.step_info(sys, T, SettlingTimeThreshold=0.05, RiseTimeLimits=(0.1, 0.9)) if display == True: for keys, values in info.items(): print("{} :\t{:.5f}".format(keys, values)) print() return info
def test_class_constants_s(self): """Make sure that the 's' variable is defined properly""" s = TransferFunction.s G = (s + 1)/(s**2 + 2*s + 1) np.testing.assert_array_almost_equal(G.num, [[[1, 1]]]) np.testing.assert_array_almost_equal(G.den, [[[1, 2, 1]]]) assert isctime(G, strict=True)
def step(tf_list=[], N=100, T=None, name=None): data = [] T_max = get_T_max(tf_list, T=T, N=N) for index, tf in enumerate(tf_list): if ctl.isctime(tf): T = np.linspace(0, T_max, N) line_shape = "linear" else: T = np.arange(0, T_max, tf.dt) line_shape = "hv" t, y = ctl.step_response(tf, T=T) tf_name = "tf {}".format(index + 1) data.append({ "x": np.ravel(t), "y": np.ravel(y), "name": tf_name, "mode": "lines", "showlegend": False, "line_shape": line_shape }) layout = default_layout("time (s)", "response", name) fig = go.Figure(data, layout=layout) return fig
def test_connect(self): # Define a couple of (linear) systems to interconnection linsys1 = self.siso_linsys iosys1 = ios.LinearIOSystem(linsys1) linsys2 = self.siso_linsys iosys2 = ios.LinearIOSystem(linsys2) # Connect systems in different ways and compare to StateSpace linsys_series = linsys2 * linsys1 iosys_series = ios.InterconnectedSystem( (iosys1, iosys2), # systems ((1, 0),), # interconnection (series) 0, # input = first system 1 # output = second system ) # Run a simulation and compare to linear response T, U = self.T, self.U X0 = np.concatenate((self.X0, self.X0)) ios_t, ios_y, ios_x = ios.input_output_response( iosys_series, T, U, X0, return_x=True) lti_t, lti_y, lti_x = ct.forced_response(linsys_series, T, U, X0) np.testing.assert_array_almost_equal(lti_t, ios_t) np.testing.assert_array_almost_equal(lti_y, ios_y, decimal=3) # Connect systems with different timebases linsys2c = self.siso_linsys linsys2c.dt = 0 # Reset the timebase iosys2c = ios.LinearIOSystem(linsys2c) iosys_series = ios.InterconnectedSystem( (iosys1, iosys2c), # systems ((1, 0),), # interconnection (series) 0, # input = first system 1 # output = second system ) self.assertTrue(ct.isctime(iosys_series, strict=True)) ios_t, ios_y, ios_x = ios.input_output_response( iosys_series, T, U, X0, return_x=True) lti_t, lti_y, lti_x = ct.forced_response(linsys_series, T, U, X0) np.testing.assert_array_almost_equal(lti_t, ios_t) np.testing.assert_array_almost_equal(lti_y, ios_y, decimal=3) # Feedback interconnection linsys_feedback = ct.feedback(linsys1, linsys2) iosys_feedback = ios.InterconnectedSystem( (iosys1, iosys2), # systems ((1, 0), # input of sys2 = output of sys1 (0, (1, 0, -1))), # input of sys1 = -output of sys2 0, # input = first system 0 # output = first system ) ios_t, ios_y, ios_x = ios.input_output_response( iosys_feedback, T, U, X0, return_x=True) lti_t, lti_y, lti_x = ct.forced_response(linsys_feedback, T, U, X0) np.testing.assert_array_almost_equal(lti_t, ios_t) np.testing.assert_array_almost_equal(lti_y, ios_y, decimal=3)
def damp(sys): is_continuous = ctl.isctime(sys) for pole in sys.pole(): pole = pole.astype( complex ) # WTF: the python control "damp" function is buggy due to this missing cast ! if is_continuous: pole_continuous = pole else: pole_continuous = np.log(pole) / sys.dt wn = np.abs(pole_continuous) m = -np.real(pole_continuous) / wn print("poles {:.3f} : wn={:.3f} rad/s, m= {:.3f}".format(pole, wn, m))
def get_T_max(tf_list, T=None, N=100): """ Get Time vector """ if T is None: T_max = 100000 for tf in tf_list: if ctl.isctime(tf): ss = ctl.tf2ss(tf) T_temp = _default_response_times(ss.A, N) t_max_temp = T_temp[-1] else: t_max_temp = N * tf.dt if t_max_temp < T_max: T_max = t_max_temp else: T_max = T[-1] return T_max
def test_time_vector(self, tsystem, fun, squeeze, matarrayout): """Test time vector handling and correct output convention gh-239, gh-295 """ sys = tsystem.sys kw = {} if hasattr(tsystem, "t"): t = tsystem.t kw['T'] = t if fun == forced_response: kw['U'] = np.vstack([np.sin(t) for i in range(sys.ninputs)]) elif fun == forced_response and isctime(sys, strict=True): pytest.skip("No continuous forced_response without time vector.") if hasattr(sys, "nstates"): kw['X0'] = np.arange(sys.nstates) + 1 if sys.ninputs > 1 and fun in [step_response, impulse_response]: kw['input'] = 1 if squeeze is not None: kw['squeeze'] = squeeze out = fun(sys, **kw) tout, yout = out[:2] assert tout.ndim == 1 if hasattr(tsystem, 't'): # tout should always match t, which has shape (n, ) np.testing.assert_allclose(tout, tsystem.t) elif fun == forced_response and sys.dt in [None, True]: np.testing.assert_allclose(np.diff(tout), 1.) if squeeze is False or not sys.issiso(): assert yout.shape[0] == sys.noutputs assert yout.shape[-1] == tout.shape[0] else: assert yout.shape == tout.shape if sys.isdtime(strict=True) and sys.dt is not True and not \ np.isclose(sys.dt, 0.5): kw['T'] = np.arange(0, 5, 0.5) # incompatible timebase with pytest.raises(ValueError): fun(sys, **kw)
def __init__(self, linsys, inputs=None, outputs=None, states=None, name=None): """Define a flat system from a SISO LTI system. Given a reachable, single-input/single-output, linear time-invariant system, create a differentially flat system representation. """ # Make sure we can handle the system if (not control.isctime(linsys)): raise control.ControlNotImplemented( "requires continuous time, linear control system") elif (not control.issiso(linsys)): raise control.ControlNotImplemented( "only single input, single output systems are supported") # Initialize the object as a LinearIO system LinearIOSystem.__init__(self, linsys, inputs=inputs, outputs=outputs, states=states, name=name) # Find the transformation to chain of integrators form # Note: store all array as ndarray, not matrix zsys, Tr = control.reachable_form(linsys) Tr = np.array(Tr[::-1, ::]) # flip rows # Extract the information that we need self.F = np.array(zsys.A[0, ::-1]) # input function coeffs self.T = Tr # state space transformation self.Tinv = np.linalg.inv(Tr) # compute inverse once # Compute the flat output variable z = C x Cfz = np.zeros(np.shape(linsys.C)) Cfz[0, 0] = 1 self.Cf = Cfz @ Tr
def __init__(self, sys): # Make sure we can handle the system if (not control.isctime(sys)): raise control.ControlNotImplemented( "requires continuous time, linear control system") elif (not control.issiso(sys)): raise control.ControlNotImplemented( "only single input, single output systems are supported") # Initialize the object and store system matrices FlatSystem.__init__(self, sys.states, sys.inputs) self.A = sys.A self.B = sys.B # Find the transformation to bring the system into reachable form zsys, Tr = control.reachable_form(sys) self.F = zsys.A[0,:] # input function coeffs self.T = Tr # state space transformation self.Tinv = np.linalg.inv(Tr) # computer inverse once # Compute the flat output variable z = C x Cfz = np.zeros(np.shape(sys.C)); Cfz[0, -1] = 1 self.C = Cfz * Tr
def __init__(self, sys): # Make sure we can handle the system if (not control.isctime(sys)): raise control.ControlNotImplemented( "requires continuous time, linear control system") elif (not control.issiso(sys)): raise control.ControlNotImplemented( "only single input, single output systems are supported") # Initialize the object and store system matrices FlatSystem.__init__(self, sys.states, sys.inputs) self.A = sys.A self.B = sys.B # Find the transformation to bring the system into reachable form zsys, Tr = control.reachable_form(sys) self.F = zsys.A[0, :] # input function coeffs self.T = Tr # state space transformation self.Tinv = np.linalg.inv(Tr) # computer inverse once # Compute the flat output variable z = C x Cfz = np.zeros(np.shape(sys.C)) Cfz[0, -1] = 1 self.C = Cfz * Tr
def testisctime(self, tsys): # Constant assert isctime(1) assert not isctime(1, strict=True) # State Space assert isctime(tsys.siso_ss1) assert not isctime(tsys.siso_ss1, strict=True) assert isctime(tsys.siso_ss1c) assert isctime(tsys.siso_ss1c, strict=True) assert not isctime(tsys.siso_ss1d) assert not isctime(tsys.siso_ss1d, strict=True) assert not isctime(tsys.siso_ss3d, strict=True) # Transfer Function assert isctime(tsys.siso_tf1) assert not isctime(tsys.siso_tf1, strict=True) assert isctime(tsys.siso_tf1c) assert isctime(tsys.siso_tf1c, strict=True) assert not isctime(tsys.siso_tf1d) assert not isctime(tsys.siso_tf1d, strict=True) assert not isctime(tsys.siso_tf3d, strict=True)
def test_terminal_constraints(sys_args): """Test out the ability to handle terminal constraints""" # Create the system sys = ct.ss2io(ct.ss(*sys_args)) # Shortest path to a point is a line Q = np.zeros((2, 2)) R = np.eye(2) cost = opt.quadratic_cost(sys, Q, R) # Set up the terminal constraint to be the origin final_point = [opt.state_range_constraint(sys, [0, 0], [0, 0])] # Create the optimal control problem time = np.arange(0, 3, 1) optctrl = opt.OptimalControlProblem( sys, time, cost, terminal_constraints=final_point) # Find a path to the origin x0 = np.array([4, 3]) res = optctrl.compute_trajectory(x0, squeeze=True, return_x=True) t, u1, x1 = res.time, res.inputs, res.states # Bug prior to SciPy 1.6 will result in incorrect results if NumpyVersion(sp.__version__) < '1.6.0': pytest.xfail("SciPy 1.6 or higher required") np.testing.assert_almost_equal(x1[:,-1], 0, decimal=4) # Make sure it is a straight line Tf = time[-1] if ct.isctime(sys): # Continuous time is not that accurate on the input, so just skip test pass else: # Final point doesn't affect cost => don't need to test np.testing.assert_almost_equal( u1[:, 0:-1], np.kron((-x0/Tf).reshape((2, 1)), np.ones(time.shape))[:, 0:-1]) np.testing.assert_allclose( x1, np.kron(x0.reshape((2, 1)), time[::-1]/Tf), atol=0.1, rtol=0.01) # Re-run using initial guess = optional and make sure nothing changes res = optctrl.compute_trajectory(x0, initial_guess=u1) np.testing.assert_almost_equal(res.inputs, u1) # Re-run using a basis function and see if we get the same answer res = opt.solve_ocp(sys, time, x0, cost, terminal_constraints=final_point, basis=flat.BezierFamily(4, Tf)) np.testing.assert_almost_equal(res.inputs, u1, decimal=2) # Impose some cost on the state, which should change the path Q = np.eye(2) R = np.eye(2) * 0.1 cost = opt.quadratic_cost(sys, Q, R) optctrl = opt.OptimalControlProblem( sys, time, cost, terminal_constraints=final_point) # Turn off warning messages, since we sometimes don't get convergence with warnings.catch_warnings(): warnings.filterwarnings( "ignore", message="unable to solve", category=UserWarning) # Find a path to the origin res = optctrl.compute_trajectory( x0, squeeze=True, return_x=True, initial_guess=u1) t, u2, x2 = res.time, res.inputs, res.states # Not all configurations are able to converge (?) if res.success: np.testing.assert_almost_equal(x2[:,-1], 0) # Make sure that it is *not* a straight line path assert np.any(np.abs(x2 - x1) > 0.1) assert np.any(np.abs(u2) > 1) # Make sure next test is useful # Add some bounds on the inputs constraints = [opt.input_range_constraint(sys, [-1, -1], [1, 1])] optctrl = opt.OptimalControlProblem( sys, time, cost, constraints, terminal_constraints=final_point) res = optctrl.compute_trajectory(x0, squeeze=True, return_x=True) t, u3, x3 = res.time, res.inputs, res.states # Check the answers only if we converged if res.success: np.testing.assert_almost_equal(x3[:,-1], 0, decimal=4) # Make sure we got a new path and didn't violate the constraints assert np.any(np.abs(x3 - x1) > 0.1) np.testing.assert_array_less(np.abs(u3), 1 + 1e-6) # Make sure that infeasible problems are handled sensibly x0 = np.array([10, 3]) with pytest.warns(UserWarning, match="unable to solve"): res = optctrl.compute_trajectory(x0, squeeze=True, return_x=True) assert not res.success
def testisctime(self): # Constant self.assertEqual(isctime(1), True) self.assertEqual(isctime(1, strict=True), False) # State Space self.assertEqual(isctime(self.siso_ss1), True) self.assertEqual(isctime(self.siso_ss1, strict=True), False) self.assertEqual(isctime(self.siso_ss1c), True) self.assertEqual(isctime(self.siso_ss1c, strict=True), True) self.assertEqual(isctime(self.siso_ss1d), False) self.assertEqual(isctime(self.siso_ss1d, strict=True), False) self.assertEqual(isctime(self.siso_ss3d, strict=True), False) # Transfer Function self.assertEqual(isctime(self.siso_tf1), True) self.assertEqual(isctime(self.siso_tf1, strict=True), False) self.assertEqual(isctime(self.siso_tf1c), True) self.assertEqual(isctime(self.siso_tf1c, strict=True), True) self.assertEqual(isctime(self.siso_tf1d), False) self.assertEqual(isctime(self.siso_tf1d, strict=True), False) self.assertEqual(isctime(self.siso_tf3d, strict=True), False)
def __init__(self, linsys, inputs=None, outputs=None, states=None, name=None): """Define a flat system from a SISO LTI system. Given a reachable, single-input/single-output, linear time-invariant system, create a differentially flat system representation. Parameters ---------- linsys : StateSpace LTI StateSpace system to be converted inputs : int, list of str or None, optional Description of the system inputs. This can be given as an integer count or as a list of strings that name the individual signals. If an integer count is specified, the names of the signal will be of the form `s[i]` (where `s` is one of `u`, `y`, or `x`). If this parameter is not given or given as `None`, the relevant quantity will be determined when possible based on other information provided to functions using the system. outputs : int, list of str or None, optional Description of the system outputs. Same format as `inputs`. states : int, list of str, or None, optional Description of the system states. Same format as `inputs`. dt : None, True or float, optional System timebase. None (default) indicates continuous time, True indicates discrete time with undefined sampling time, positive number is discrete time with specified sampling time. params : dict, optional Parameter values for the systems. Passed to the evaluation functions for the system as default values, overriding internal defaults. name : string, optional System name (used for specifying signals) Returns ------- iosys : LinearFlatSystem Linear system represented as an flat input/output system """ # Make sure we can handle the system if (not control.isctime(linsys)): raise control.ControlNotImplemented( "requires continuous time, linear control system") elif (not control.issiso(linsys)): raise control.ControlNotImplemented( "only single input, single output systems are supported") # Initialize the object as a LinearIO system LinearIOSystem.__init__(self, linsys, inputs=inputs, outputs=outputs, states=states, name=name) # Find the transformation to chain of integrators form # Note: store all array as ndarray, not matrix zsys, Tr = control.reachable_form(linsys) Tr = np.array(Tr[::-1, ::]) # flip rows # Extract the information that we need self.F = np.array(zsys.A[0, ::-1]) # input function coeffs self.T = Tr # state space transformation self.Tinv = np.linalg.inv(Tr) # compute inverse once # Compute the flat output variable z = C x Cfz = np.zeros(np.shape(linsys.C)) Cfz[0, 0] = 1 self.Cf = np.dot(Cfz, Tr)