def simulate_impulse_response(sys, t=None): """ Compute the linear model response to an Dirac delta pulse (or all-zeros array except the first sample being 1/dt at each channel) sampled at given time instances. If the time array is omitted then a time sequence is generated based on the poles of the model. Parameters ---------- sys : {State, Transfer} The system model to be simulated t : array_like The real-valued sequence to be used for the evolution of the system. The values should be equally spaced otherwise an error is raised. For discrete time models increments different than the sampling period also raises an error. On the other hand for discrete models this can be omitted and a time sequence will be generated automatically. Returns ------- yout : ndarray The resulting response array. The array is 1D if sys is SISO and has p columns if sys has p outputs. If there are also m inputs the array is 3D array with the shape (<num of samples>, p, m) tout : ndarray The time sequence used in the simulation. If the parameter t is not None then a copy of t is given. """ _check_for_state_or_transfer(sys) # Always works with State Models try: _check_for_state(sys) except ValueError: sys = transfer_to_state(sys) if t is None: tf, ts = _compute_tfinal_and_dt(sys, is_step=False) t = np.arange(0, tf+ts, ts, dtype=float) else: t, ts = _check_custom_time_input(t) m = sys.shape[1] u = np.zeros([len(t), m], dtype=float) u[0] = 1./ts return simulate_linear_system(sys, u=u, t=t, per_channel=1)
def _check_for_feedback_ic(G, H): """ A helper function to sanitize the inputs to the feedback() function Paramaters ---------- G, H : {State, Transfer, ndarray, int, float} Models on the feedforward and feedback paths Returns ------- g, h : {State} Sanitized models tf_convert : bool If, initially, both models are Transfer representations, then this flag is set to True and the returned object is a Transfer. Otherwise State always have higher precedence. """ # Get the boring model comparison and shape checks out of the way. flag_sys = [isinstance(G, (State, Transfer)), isinstance(H, (State, Transfer))] tf_convert = False # Both are either ndarrays ints floats or rejected if not np.any(flag_sys): g, h = [State(_check_for_int_float_array(x)) for x in ([G, H])] # both are system models elif np.all(flag_sys): _check_equal_dts(G, H) flag_tf = [isinstance(G, Transfer), isinstance(H, Transfer)] # both transfer if np.all(flag_tf): g, h = transfer_to_state(G), transfer_to_state(H) tf_convert = True # one is a transfer elif np.logical_xor(*flag_tf): if flag_tf[0]: g, h = transfer_to_state(G), H else: g, h = G, transfer_to_state(H) # both state else: g, h = G, H else: # one of them is a model the other one is checked if flag_sys[0]: # If G is a model then check H h = _check_for_int_float_array(H) h = State(h, dt=G.SamplingPeriod) tf_convert = True if isinstance(G, Transfer) else False g = G if isinstance(G, State) else transfer_to_state(G) else: g = _check_for_int_float_array(G) g = State(g, dt=H.SamplingPeriod) tf_convert = True if isinstance(H, Transfer) else False h = H if isinstance(H, State) else transfer_to_state(H) (p1, m1), (p2, m2) = g.shape, h.shape if p1 != m2 or m1 != p2: raise ValueError('Forward path model should have equal number of' ' inputs/outputs to the feedback path outputs/inputs.' f' G has the shape {g.shape} but H has {h.shape}.') return g, h, tf_convert
def simulate_linear_system(sys, u, t=None, x0=None, per_channel=False): """ Compute the linear model response to an input array sampled at given time instances. Parameters ---------- sys : {State, Transfer} The system model to be simulated u : array_like The real-valued input sequence to force the model. 1D arrays for single input models and 2D arrays that has as many columns as the number of inputs are valid inputs. t : array_like, optional The real-valued sequence to be used for the evolution of the system. The values should be equally spaced otherwise an error is raised. For discrete time models increments different than the sampling period also raises an error. On the other hand for discrete models this can be omitted and a time sequence will be generated automatically. x0 : array_like, optional The initial condition array. If omitted an array of zeros is assumed. Note that Transfer models by definition assume zero initial conditions and will raise an error. per_channel : bool, optional If this is set to True and if the system has multiple inputs, the response of each input is returned individually. For example, if a system has 4 inputs and 3 outputs then the response shape becomes (num, p, m) instead of (num, p) where k-th slice (:, :, k) is the response from the k-th input channel. For single input systems, this keyword has no effect. Returns ------- yout : ndarray The resulting response array. The array is 1D if sys is SISO and has p columns if sys has p outputs. tout : ndarray The time sequence used in the simulation. If the parameter t is not None then a copy of t is given. Notes ----- For Transfer models, first conversion to a state model is performed and then the resulting model is used for computations. """ _check_for_state_or_transfer(sys) # Quick initial condition checks if x0 is not None: if sys._isgain: raise ValueError('Static system models can\'t have initial ' 'conditions set.') if isinstance(sys, Transfer): raise ValueError('Transfer models can\'t have initial conditions ' 'set.') x0 = np.asarray(x0, dtype=float).squeeze() if x0.ndim > 1: raise ValueError('Initial condition can only be a 1D array.') else: x0 = x0[:, None] if sys.NumberOfStates != x0.size: raise ValueError('The initial condition size does not match the ' 'number of states of the model.') # Always works with State Models try: _check_for_state(sys) except ValueError: sys = transfer_to_state(sys) n, m = sys.NumberOfStates, sys.shape[1] is_discrete = sys.SamplingSet == 'Z' u = np.asarray(u, dtype=float).squeeze() if u.ndim == 1: u = u[:, None] t = _check_u_and_t_for_simulation(m, sys._dt, u, t, is_discrete) # input and time arrays are regular move on # Static gains are simple matrix multiplications with no x0 if sys._isgain: if sys._isSISO: yout = u * sys.d.squeeze() else: # don't bother for single inputs if m == 1: per_channel = False if per_channel: yout = np.einsum('ij,jk->ikj', u, sys.d.T) else: yout = u @ sys.d.T # Dynamic model else: # TODO: Add FOH discretization for funky input # ZOH discretize the continuous system based on the time increment if not is_discrete: sys = discretize(sys, t[1] - t[0], method='zoh') sample_num = len(u) a, b, c, d = sys.matrices # Bu and Du are constant matrices so get them ready (transposed) M_u = np.block([b.T, d.T]) at = a.T # Explicitly skip single inputs for per_channel if m == 1: per_channel = False # Shape the response as a 3D array if per_channel: xout = np.empty([sample_num, n, m], dtype=float) for col in range(m): xout[0, :, col] = 0. if x0 is None else x0.T Bu = u[:, [col]] @ b.T[[col], :] # Main loop for xdot eq. for row in range(1, sample_num): xout[row, :, col] = xout[row - 1, :, col] @ at + Bu[row - 1] # Get the output equation for each slice of inputs # Cx + Du yout = np.einsum('ijk,jl->ilk', xout, c.T) + \ np.einsum('ij,jk->ikj', u, d.T) # Combined output else: BDu = u @ M_u xout = np.empty([sample_num, n], dtype=float) xout[0] = 0. if x0 is None else x0.T # Main loop for xdot eq. for row in range(1, sample_num): xout[row] = (xout[row - 1] @ at) + BDu[row - 1, :n] # Now we have all the state evolution get the output equation yout = xout @ c.T + BDu[:, n:] return yout, t
def simulate_linear_system(sys, u, t=None, x0=None, per_channel=False): """ Compute the linear model response to an input array sampled at given time instances. Parameters ---------- sys : {State, Transfer} The system model to be simulated u : array_like The real-valued input sequence to force the model. 1D arrays for single input models and 2D arrays that has as many columns as the number of inputs are valid inputs. t : array_like, optional The real-valued sequence to be used for the evolution of the system. The values should be equally spaced otherwise an error is raised. For discrete time models increments different than the sampling period also raises an error. On the other hand for discrete models this can be omitted and a time sequence will be generated automatically. x0 : array_like, optional The initial condition array. If omitted an array of zeros is assumed. Note that Transfer models by definition assume zero initial conditions and will raise an error. per_channel : bool, optional If this is set to True and if the system has multiple inputs, the response of each input is returned individually. For example, if a system has 4 inputs and 3 outputs then the response shape becomes (num, p, m) instead of (num, p) where k-th slice (:, :, k) is the response from the k-th input channel. For single input systems, this keyword has no effect. Returns ------- yout : ndarray The resulting response array. The array is 1D if sys is SISO and has p columns if sys has p outputs. tout : ndarray The time sequence used in the simulation. If the parameter t is not None then a copy of t is given. Notes ----- For Transfer models, first conversion to a state model is performed and then the resulting model is used for computations. """ _check_for_state_or_transfer(sys) # Quick initial condition checks if x0 is not None: if sys._isgain: raise ValueError('Static system models can\'t have initial ' 'conditions set.') if isinstance(sys, Transfer): raise ValueError('Transfer models can\'t have initial conditions ' 'set.') x0 = np.asarray(x0, dtype=float).squeeze() if x0.ndim > 1: raise ValueError('Initial condition can only be a 1D array.') else: x0 = x0[:, None] if sys.NumberOfStates != x0.size: raise ValueError('The initial condition size does not match the ' 'number of states of the model.') # Always works with State Models try: _check_for_state(sys) except ValueError: sys = transfer_to_state(sys) n, m = sys.NumberOfStates, sys.shape[1] is_discrete = sys.SamplingSet == 'Z' u = np.asarray(u, dtype=float).squeeze() if u.ndim == 1: u = u[:, None] t = _check_u_and_t_for_simulation(m, sys._dt, u, t, is_discrete) # input and time arrays are regular move on # Static gains are simple matrix multiplications with no x0 if sys._isgain: if sys._isSISO: yout = u * sys.d.squeeze() else: # don't bother for single inputs if m == 1: per_channel = False if per_channel: yout = np.einsum('ij,jk->ikj', u, sys.d.T) else: yout = u @ sys.d.T # Dynamic model else: # TODO: Add FOH discretization for funky input # ZOH discretize the continuous system based on the time increment if not is_discrete: sys = discretize(sys, t[1]-t[0], method='zoh') sample_num = len(u) a, b, c, d = sys.matrices # Bu and Du are constant matrices so get them ready (transposed) M_u = np.block([b.T, d.T]) at = a.T # Explicitly skip single inputs for per_channel if m == 1: per_channel = False # Shape the response as a 3D array if per_channel: xout = np.empty([sample_num, n, m], dtype=float) for col in range(m): xout[0, :, col] = 0. if x0 is None else x0.T Bu = u[:, [col]] @ b.T[[col], :] # Main loop for xdot eq. for row in range(1, sample_num): xout[row, :, col] = xout[row-1, :, col] @ at + Bu[row-1] # Get the output equation for each slice of inputs # Cx + Du yout = np.einsum('ijk,jl->ilk', xout, c.T) + \ np.einsum('ij,jk->ikj', u, d.T) # Combined output else: BDu = u @ M_u xout = np.empty([sample_num, n], dtype=float) xout[0] = 0. if x0 is None else x0.T # Main loop for xdot eq. for row in range(1, sample_num): xout[row] = (xout[row-1] @ at) + BDu[row-1, :n] # Now we have all the state evolution get the output equation yout = xout @ c.T + BDu[:, n:] return yout, t