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 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 test_class_constants(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]]]) self.assertTrue(isctime(G, strict=True)) # Make sure that the 'z' variable is defined properly z = TransferFunction.z G = (z + 1) / (z**2 + 2 * z + 1) np.testing.assert_array_almost_equal(G.num, [[[1, 1]]]) np.testing.assert_array_almost_equal(G.den, [[[1, 2, 1]]]) self.assertTrue(isdtime(G, strict=True))
def test_class_constants(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]]]) self.assertTrue(isctime(G, strict=True)) # Make sure that the 'z' variable is defined properly z = TransferFunction.z G = (z + 1)/(z**2 + 2*z + 1) np.testing.assert_array_almost_equal(G.num, [[[1, 1]]]) np.testing.assert_array_almost_equal(G.den, [[[1, 2, 1]]]) self.assertTrue(isdtime(G, strict=True))
def sample_system(sysc, Ts, method='matched'): # TODO: add docstring # Make sure we have a continuous time system if not isctime(sysc): raise ValueError("First argument must be continuous time system") # TODO: impelement MIMO version if (sysc.inputs != 1 or sysc.outputs != 1): raise NotImplementedError("MIMO implementation not available") # If we are passed a state space system, convert to transfer function first if isinstance(sysc, StateSpace): warn("sample_system: converting to transfer function") sysc = _convertToTransferFunction(sysc) # Decide what to do based on the methods available if method == 'matched': sysd = _c2dmatched(sysc, Ts) elif method == 'tustin': sys = [sysc.num[0][0], sysc.den[0][0]] scipySysD = cont2discrete(sys, Ts, method='bilinear') sysd = TransferFunction(scipySysD[0][0], scipySysD[1], dt) elif method == 'zoh': sys = [sysc.num[0][0], sysc.den[0][0]] scipySysD = cont2discrete(sys, Ts, method='zoh') sysd = TransferFunction(scipySysD[0][0],scipySysD[1], dt) elif method == 'foh' or method == 'impulse': raise ValueError("Method not developed yet") else: raise ValueError("Invalid discretization method: %s" % method) # TODO: Convert back into the input form # Set sampling time return sysd
def sample_system(sysc, Ts, method='matched'): # TODO: add docstring # Make sure we have a continuous time system if not isctime(sysc): raise ValueError("First argument must be continuous time system") # TODO: impelement MIMO version if (sysc.inputs != 1 or sysc.outputs != 1): raise NotImplementedError("MIMO implementation not available") # If we are passed a state space system, convert to transfer function first if isinstance(sysc, StateSpace): warn("sample_system: converting to transfer function") sysc = _convertToTransferFunction(sysc) # Decide what to do based on the methods available if method == 'matched': sysd = _c2dmatched(sysc, Ts) elif method == 'tustin': sys = [sysc.num[0][0], sysc.den[0][0]] scipySysD = cont2discrete(sys, Ts, method='bilinear') sysd = TransferFunction(scipySysD[0][0], scipySysD[1], Ts) elif method == 'zoh': sys = [sysc.num[0][0], sysc.den[0][0]] scipySysD = cont2discrete(sys, Ts, method='zoh') sysd = TransferFunction(scipySysD[0][0], scipySysD[1], Ts) elif method == 'foh' or method == 'impulse': raise ValueError("Method not developed yet") else: raise ValueError("Invalid discretization method: %s" % method) # TODO: Convert back into the input form # Set sampling time return sysd
def rootlocus_pid_designer(plant, gain='P', sign=+1, input_signal='r', Kp0=0, Ki0=0, Kd0=0, tau=0.01, C_ff=0, derivative_in_feedback_path=False, plot=True): """Manual PID controller design based on root locus using Sisotool Uses `Sisotool` to investigate the effect of adding or subtracting an amount `deltaK` to the proportional, integral, or derivative (PID) gains of a controller. One of the PID gains, `Kp`, `Ki`, or `Kd`, respectively, can be modified at a time. `Sisotool` plots the step response, frequency response, and root locus. When first run, `deltaK` is set to 0; click on a branch of the root locus plot to try a different value. Each click updates plots and prints the corresponding `deltaK`. To tune all three PID gains, repeatedly call `rootlocus_pid_designer`, and select a different `gain` each time (`'P'`, `'I'`, or `'D'`). Make sure to add the resulting `deltaK` to your chosen initial gain on the next iteration. Example: to examine the effect of varying `Kp` starting from an intial value of 10, use the arguments `gain='P', Kp0=10`. Suppose a `deltaK` value of 5 gives satisfactory performance. Then on the next iteration, to tune the derivative gain, use the arguments `gain='D', Kp0=15`. By default, all three PID terms are in the forward path C_f in the diagram shown below, that is, C_f = Kp + Ki/s + Kd*s/(tau*s + 1). If `plant` is a discrete-time system, then the proportional, integral, and derivative terms are given instead by Kp, Ki*dt/2*(z+1)/(z-1), and Kd/dt*(z-1)/z, respectively. ------> C_ff ------ d | | | r | e V V u y ------->O---> C_f --->O--->O---> plant ---> ^- ^- | | | | | ----- C_b <-------| --------------------------------- It is also possible to move the derivative term into the feedback path `C_b` using `derivative_in_feedback_path=True`. This may be desired to avoid that the plant is subject to an impulse function when the reference `r` is a step input. `C_b` is otherwise set to zero. If `plant` is a 2-input system, the disturbance `d` is fed directly into its second input rather than being added to `u`. Remark: It may be helpful to zoom in using the magnifying glass on the plot. Just ake sure to deactivate magnification mode when you are done by clicking the magnifying glass. Otherwise you will not be able to be able to choose a gain on the root locus plot. Parameters ---------- plant : :class:`LTI` (:class:`TransferFunction` or :class:`StateSpace` system) The dynamical system to be controlled gain : string (optional) Which gain to vary by `deltaK`. Must be one of `'P'`, `'I'`, or `'D'` (proportional, integral, or derative) sign : int (optional) The sign of deltaK gain perturbation input : string (optional) The input used for the step response; must be `'r'` (reference) or `'d'` (disturbance) (see figure above) Kp0, Ki0, Kd0 : float (optional) Initial values for proportional, integral, and derivative gains, respectively tau : float (optional) The time constant associated with the pole in the continuous-time derivative term. This is required to make the derivative transfer function proper. C_ff : float or :class:`LTI` system (optional) Feedforward controller. If :class:`LTI`, must have timebase that is compatible with plant. derivative_in_feedback_path : bool (optional) Whether to place the derivative term in feedback transfer function `C_b` instead of the forward transfer function `C_f`. plot : bool (optional) Whether to create Sisotool interactive plot. Returns ---------- closedloop : class:`StateSpace` system The closed-loop system using initial gains. """ plant = _convert_to_statespace(plant) if plant.ninputs == 1: plant = ss2io(plant, inputs='u', outputs='y') elif plant.ninputs == 2: plant = ss2io(plant, inputs=['u', 'd'], outputs='y') else: raise ValueError("plant must have one or two inputs") C_ff = ss2io(_convert_to_statespace(C_ff), inputs='r', outputs='uff') dt = common_timebase(plant, C_ff) # create systems used for interconnections e_summer = summing_junction(['r', '-y'], 'e') if plant.ninputs == 2: u_summer = summing_junction(['ufb', 'uff'], 'u') else: u_summer = summing_junction(['ufb', 'uff', 'd'], 'u') if isctime(plant): prop = tf(1, 1) integ = tf(1, [1, 0]) deriv = tf([1, 0], [tau, 1]) else: # discrete-time prop = tf(1, 1, dt) integ = tf([dt/2, dt/2], [1, -1], dt) deriv = tf([1, -1], [dt, 0], dt) # add signal names by turning into iosystems prop = tf2io(prop, inputs='e', outputs='prop_e') integ = tf2io(integ, inputs='e', outputs='int_e') if derivative_in_feedback_path: deriv = tf2io(-deriv, inputs='y', outputs='deriv') else: deriv = tf2io(deriv, inputs='e', outputs='deriv') # create gain blocks Kpgain = tf2io(tf(Kp0, 1), inputs='prop_e', outputs='ufb') Kigain = tf2io(tf(Ki0, 1), inputs='int_e', outputs='ufb') Kdgain = tf2io(tf(Kd0, 1), inputs='deriv', outputs='ufb') # for the gain that is varied, replace gain block with a special block # that has an 'input' and an 'output' that creates loop transfer function if gain in ('P', 'p'): Kpgain = ss2io(ss([],[],[],[[0, 1], [-sign, Kp0]]), inputs=['input', 'prop_e'], outputs=['output', 'ufb']) elif gain in ('I', 'i'): Kigain = ss2io(ss([],[],[],[[0, 1], [-sign, Ki0]]), inputs=['input', 'int_e'], outputs=['output', 'ufb']) elif gain in ('D', 'd'): Kdgain = ss2io(ss([],[],[],[[0, 1], [-sign, Kd0]]), inputs=['input', 'deriv'], outputs=['output', 'ufb']) else: raise ValueError(gain + ' gain not recognized.') # the second input and output are used by sisotool to plot step response loop = interconnect((plant, Kpgain, Kigain, Kdgain, prop, integ, deriv, C_ff, e_summer, u_summer), inplist=['input', input_signal], outlist=['output', 'y'], check_unused=False) if plot: sisotool(loop, kvect=(0.,)) cl = loop[1, 1] # closed loop transfer function with initial gains return StateSpace(cl.A, cl.B, cl.C, cl.D, cl.dt)
def impulse_response(sys, T=None, X0=0., input=0, output=0, transpose=False, **keywords): #pylint: disable=W0622 """Impulse response of a linear system If the system has multiple inputs or outputs (MIMO), one input and one output have to be selected for the simulation. The parameters `input` and `output` do this. All other inputs are set to 0, all other outputs are ignored. For information on the **shape** of parameters `T`, `X0` and return values `T`, `yout` see: :ref:`time-series-convention` Parameters ---------- sys: StateSpace, TransferFunction LTI system to simulate T: array-like object, optional Time vector (argument is autocomputed if not given) X0: array-like object or number, optional Initial condition (default = 0) Numbers are converted to constant arrays with the correct shape. input: int Index of the input that will be used in this simulation. output: int Index of the output that will be used in this simulation. transpose: bool If True, transpose all input and output arrays (for backward compatibility with MATLAB and scipy.signal.lsim) **keywords: Additional keyword arguments control the solution algorithm for the differential equations. These arguments are passed on to the function :func:`lsim`, which in turn passes them on to :func:`scipy.integrate.odeint`. See the documentation for :func:`scipy.integrate.odeint` for information about these arguments. Returns ------- T: array Time values of the output yout: array Response of the system See Also -------- ForcedReponse, initial_response, step_response Examples -------- >>> T, yout = impulse_response(sys, T, X0) """ sys = _convertToStateSpace(sys) sys = _mimo2siso(sys, input, output, warn_conversion=True) # 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 infinite ' 'impulse at ``t=0`` does not appear in the output. \n' '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. n_states = sys.A.shape[0] X0 = _check_convert_array(X0, [(n_states,), (n_states,1)], 'Parameter ``X0``: \n', squeeze=True) # 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 B = np.asarray(sys.B).squeeze() new_X0 = B + X0 # Compute T and U, no checks necessary, they will be checked in lsim if T is None: T = _default_response_times(sys.A, 100) U = np.zeros_like(T) T, yout, _xout = forced_response(sys, T, U, new_X0, \ transpose=transpose, **keywords) return T, yout
def step_response(sys, T=None, X0=0., input=0, output=0, \ transpose = False, **keywords): #pylint: disable=W0622 """Step response of a linear system If the system has multiple inputs or outputs (MIMO), one input and one output have to be selected for the simulation. The parameters `input` and `output` do this. All other inputs are set to 0, all other outputs are ignored. For information on the **shape** of parameters `T`, `X0` and return values `T`, `yout` see: :ref:`time-series-convention` Parameters ---------- sys: StateSpace, or TransferFunction LTI system to simulate T: array-like object, optional Time vector (argument is autocomputed if not given) X0: array-like or number, optional Initial condition (default = 0) Numbers are converted to constant arrays with the correct shape. input: int Index of the input that will be used in this simulation. output: int Index of the output that will be used in this simulation. transpose: bool If True, transpose all input and output arrays (for backward compatibility with MATLAB and scipy.signal.lsim) **keywords: Additional keyword arguments control the solution algorithm for the differential equations. These arguments are passed on to the function :func:`lsim`, which in turn passes them on to :func:`scipy.integrate.odeint`. See the documentation for :func:`scipy.integrate.odeint` for information about these arguments. Returns ------- T: array Time values of the output yout: array Response of the system See Also -------- forced_response, initial_response, impulse_response Examples -------- >>> T, yout = step_response(sys, T, X0) """ sys = _convertToStateSpace(sys) sys = _mimo2siso(sys, input, output, warn_conversion=True) if T is None: if isctime(sys): T = _default_response_times(sys.A, 100) else: # For discrete time, use integers tvec = _default_response_times(sys.A, 100) T = range(int(np.ceil(max(tvec)))) U = np.ones_like(T) T, yout, _xout = forced_response(sys, T, U, X0, transpose=transpose, **keywords) return T, yout
def forced_response(sys, T=None, U=0., X0=0., transpose=False, **keywords): """Simulate the output of a linear system. As a convenience for parameters `U`, `X0`: Numbers (scalars) are converted to constant arrays with the correct shape. The correct shape is inferred from arguments `sys` and `T`. For information on the **shape** of parameters `U`, `T`, `X0` and return values `T`, `yout`, `xout` see: :ref:`time-series-convention` Parameters ---------- sys: Lti (StateSpace, or TransferFunction) LTI system to simulate T: array-like Time steps at which the input is defined, numbers must be (strictly monotonic) increasing. U: array-like or number, optional Input array giving input at each time `T` (default = 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. X0: array-like or number, optional Initial condition (default = 0). transpose: bool If True, transpose all input and output arrays (for backward compatibility with MATLAB and scipy.signal.lsim) **keywords: Additional keyword arguments control the solution algorithm for the differential equations. These arguments are passed on to the function :func:`scipy.integrate.odeint`. See the documentation for :func:`scipy.integrate.odeint` for information about these arguments. Returns ------- T: array Time values of the output. yout: array Response of the system. xout: array Time evolution of the state vector. See Also -------- step_response, initial_response, impulse_response Examples -------- >>> T, yout, xout = forced_response(sys, T, u, X0) """ if not isinstance(sys, Lti): raise TypeError('Parameter ``sys``: must be a ``Lti`` object. ' '(For example ``StateSpace`` or ``TransferFunction``)') sys = _convertToStateSpace(sys) A, B, C, D = np.asarray(sys.A), np.asarray(sys.B), np.asarray(sys.C), \ np.asarray(sys.D) # d_type = A.dtype n_states = A.shape[0] n_inputs = B.shape[1] # Set and/or check time vector in discrete time case if isdtime(sys, strict=True): if T == None: if U == None: raise ValueError('Parameters ``T`` and ``U`` can\'t both be' 'zero for discrete-time simulation') # Set T to integers with same length as U T = range(len(U)) else: # Make sure the input vector and time vector have same length # TODO: allow interpolation of the input vector if len(U) != len(T): ValueError('Pamameter ``T`` must have same length as' 'input vector ``U``') # Test if T has shape (n,) or (1, n); # T must be array-like and values must be increasing. # The length of T determines the length of the input vector. if T is None: raise ValueError('Parameter ``T``: must be array-like, and contain ' '(strictly monotonic) increasing numbers.') T = _check_convert_array(T, [('any',), (1,'any')], 'Parameter ``T``: ', squeeze=True, transpose = transpose) if not all(T[1:] - T[:-1] > 0): raise ValueError('Parameter ``T``: time values must be ' '(strictly monotonic) increasing numbers.') n_steps = len(T) # number of simulation steps #create X0 if not given, test if X0 has correct shape X0 = _check_convert_array(X0, [(n_states,), (n_states,1)], 'Parameter ``X0``: ', squeeze=True) # Separate out the discrete and continuous time cases if isctime(sys): # Solve the differential equation, copied from scipy.signal.ltisys. dot, squeeze, = np.dot, np.squeeze #Faster and shorter code # Faster algorithm if U is zero if U is None or (isinstance(U, (int, float)) and U == 0): # Function that computes the time derivative of the linear system def f_dot(x, _t): return dot(A,x) xout = sp.integrate.odeint(f_dot, X0, T, **keywords) yout = dot(C, xout.T) # General algorithm that interpolates U in between output points else: # Test if U has correct shape and type legal_shapes = [(n_steps,), (1,n_steps)] if n_inputs == 1 else \ [(n_inputs, n_steps)] U = _check_convert_array(U, legal_shapes, 'Parameter ``U``: ', squeeze=False, transpose=transpose) # convert 1D array to D2 array with only one row if len(U.shape) == 1: U = U.reshape(1,-1) #pylint: disable=E1103 # Create a callable that uses linear interpolation to # calculate the input at any time. compute_u = \ sp.interpolate.interp1d(T, U, kind='linear', copy=False, axis=-1, bounds_error=False, fill_value=0) # Function that computes the time derivative of the linear system def f_dot(x, t): return dot(A,x) + squeeze(dot(B,compute_u([t]))) xout = sp.integrate.odeint(f_dot, X0, T, **keywords) yout = dot(C, xout.T) + dot(D, U) yout = squeeze(yout) xout = xout.T else: # Discrete time simulation using signal processing toolbox dsys = (A, B, C, D, sys.dt) tout, yout, xout = sp.signal.dlsim(dsys, U, T, X0) # See if we need to transpose the data back into MATLAB form if (transpose): T = np.transpose(T) yout = np.transpose(yout) xout = np.transpose(xout) return T, yout, xout
def step_response(sys, T=None, X0=0., input=0, output=None, transpose=False, **keywords): #pylint: disable=W0622 """Step response of 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. For information on the **shape** of parameters `T`, `X0` and return values `T`, `yout` see: :ref:`time-series-convention` Parameters ---------- sys: StateSpace, or TransferFunction LTI system to simulate T: array-like object, optional Time vector (argument is autocomputed if not given) X0: array-like or number, optional Initial condition (default = 0) Numbers are converted to constant arrays with the correct shape. input: int Index of the input that will be used in this simulation. output: int Index of the output that will be used in this simulation. Set to None to not trim outputs transpose: bool If True, transpose all input and output arrays (for backward compatibility with MATLAB and scipy.signal.lsim) **keywords: Additional keyword arguments control the solution algorithm for the differential equations. These arguments are passed on to the function :func:`lsim`, which in turn passes them on to :func:`scipy.integrate.odeint`. See the documentation for :func:`scipy.integrate.odeint` for information about these arguments. Returns ------- T: array Time values of the output yout: array Response of the system See Also -------- forced_response, initial_response, impulse_response Examples -------- >>> T, yout = step_response(sys, T, X0) """ sys = _convertToStateSpace(sys) if output == None: sys = _mimo2simo(sys, input, warn_conversion=True) else: sys = _mimo2siso(sys, input, output, warn_conversion=True) if T is None: if isctime(sys): T = _default_response_times(sys.A, 100) else: # For discrete time, use integers tvec = _default_response_times(sys.A, 100) T = range(int(np.ceil(max(tvec)))) U = np.ones_like(T) T, yout, _xout = forced_response(sys, T, U, X0, transpose=transpose, **keywords) return T, yout
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 modred(sys, ELIM, method='matchdc'): """ Model reduction of `sys` by eliminating the states in `ELIM` using a given method. Parameters ---------- sys: StateSpace Original system to reduce ELIM: array Vector of states to eliminate method: string Method of removing states in `ELIM`: either ``'truncate'`` or ``'matchdc'``. Returns ------- rsys: StateSpace A reduced order model Raises ------ ValueError * if `method` is not either ``'matchdc'`` or ``'truncate'`` * if eigenvalues of `sys.A` are not all in left half plane (`sys` must be stable) Examples -------- >>> rsys = modred(sys, ELIM, method='truncate') """ #Check for ss system object, need a utility for this? #TODO: Check for continous or discrete, only continuous supported right now # if isCont(): # dico = 'C' # elif isDisc(): # dico = 'D' # else: if (isctime(sys)): dico = 'C' else: raise NotImplementedError("Function not implemented in discrete time") #Check system is stable D, V = np.linalg.eig(sys.A) for e in D: if e.real >= 0: raise ValueError("Oops, the system is unstable!") ELIM = np.sort(ELIM) NELIM = [] # Create list of elements not to eliminate (NELIM) for i in range(0, len(sys.A)): if i not in ELIM: NELIM.append(i) # A1 is a matrix of all columns of sys.A not to eliminate A1 = sys.A[:, NELIM[0]] for i in NELIM[1:]: A1 = np.hstack((A1, sys.A[:, i])) A11 = A1[NELIM, :] A21 = A1[ELIM, :] # A2 is a matrix of all columns of sys.A to eliminate A2 = sys.A[:, ELIM[0]] for i in ELIM[1:]: A2 = np.hstack((A2, sys.A[:, i])) A12 = A2[NELIM, :] A22 = A2[ELIM, :] C1 = sys.C[:, NELIM] C2 = sys.C[:, ELIM] B1 = sys.B[NELIM, :] B2 = sys.B[ELIM, :] A22I = np.linalg.inv(A22) if method == 'matchdc': # if matchdc, residualize Ar = A11 - A12 * A22.I * A21 Br = B1 - A12 * A22.I * B2 Cr = C1 - C2 * A22.I * A21 Dr = sys.D - C2 * A22.I * B2 elif method == 'truncate': # if truncate, simply discard state x2 Ar = A11 Br = B1 Cr = C1 Dr = sys.D else: raise ValueError("Oops, method is not supported!") rsys = StateSpace(Ar, Br, Cr, Dr) return rsys
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 impulse_response(sys, T=None, X0=0., input=0, output=None, transpose=False, **keywords): #pylint: disable=W0622 """Impulse response of a linear system If the system has multiple inputs or outputs (MIMO), one input and one output have to be selected for the simulation. The parameters `input` and `output` do this. All other inputs are set to 0, all other outputs are ignored. For information on the **shape** of parameters `T`, `X0` and return values `T`, `yout` see: :ref:`time-series-convention` Parameters ---------- sys: StateSpace, TransferFunction LTI system to simulate T: array-like object, optional Time vector (argument is autocomputed if not given) X0: array-like object or number, optional Initial condition (default = 0) Numbers are converted to constant arrays with the correct shape. input: int Index of the input that will be used in this simulation. output: int Index of the output that will be used in this simulation. Set to None to not trim outputs transpose: bool If True, transpose all input and output arrays (for backward compatibility with MATLAB and scipy.signal.lsim) **keywords: Additional keyword arguments control the solution algorithm for the differential equations. These arguments are passed on to the function :func:`lsim`, which in turn passes them on to :func:`scipy.integrate.odeint`. See the documentation for :func:`scipy.integrate.odeint` for information about these arguments. Returns ------- T: array Time values of the output yout: array Response of the system See Also -------- ForcedReponse, initial_response, step_response Examples -------- >>> T, yout = impulse_response(sys, T, X0) """ sys = _convertToStateSpace(sys) if output == None: sys = _mimo2simo(sys, input, warn_conversion=True) else: sys = _mimo2siso(sys, input, output, warn_conversion=True) # 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 infinite ' 'impulse at ``t=0`` does not appear in the output. \n' '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. n_states = sys.A.shape[0] X0 = _check_convert_array(X0, [(n_states, ), (n_states, 1)], 'Parameter ``X0``: \n', squeeze=True) # 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 B = np.asarray(sys.B).squeeze() new_X0 = B + X0 # Compute T and U, no checks necessary, they will be checked in lsim if T is None: T = _default_response_times(sys.A, 100) U = np.zeros_like(T) T, yout, _xout = forced_response(sys, T, U, new_X0, \ transpose=transpose, **keywords) return T, yout
def modred(sys, ELIM, method='matchdc'): """ Model reduction of `sys` by eliminating the states in `ELIM` using a given method. Parameters ---------- sys: StateSpace Original system to reduce ELIM: array Vector of states to eliminate method: string Method of removing states in `ELIM`: either ``'truncate'`` or ``'matchdc'``. Returns ------- rsys: StateSpace A reduced order model Raises ------ ValueError * if `method` is not either ``'matchdc'`` or ``'truncate'`` * if eigenvalues of `sys.A` are not all in left half plane (`sys` must be stable) Examples -------- >>> rsys = modred(sys, ELIM, method='truncate') """ #Check for ss system object, need a utility for this? #TODO: Check for continous or discrete, only continuous supported right now # if isCont(): # dico = 'C' # elif isDisc(): # dico = 'D' # else: if (isctime(sys)): dico = 'C' else: raise NotImplementedError("Function not implemented in discrete time") #Check system is stable D,V = np.linalg.eig(sys.A) for e in D: if e.real >= 0: raise ValueError("Oops, the system is unstable!") ELIM = np.sort(ELIM) NELIM = [] # Create list of elements not to eliminate (NELIM) for i in range(0,len(sys.A)): if i not in ELIM: NELIM.append(i) # A1 is a matrix of all columns of sys.A not to eliminate A1 = sys.A[:,NELIM[0]] for i in NELIM[1:]: A1 = np.hstack((A1, sys.A[:,i])) A11 = A1[NELIM,:] A21 = A1[ELIM,:] # A2 is a matrix of all columns of sys.A to eliminate A2 = sys.A[:,ELIM[0]] for i in ELIM[1:]: A2 = np.hstack((A2, sys.A[:,i])) A12 = A2[NELIM,:] A22 = A2[ELIM,:] C1 = sys.C[:,NELIM] C2 = sys.C[:,ELIM] B1 = sys.B[NELIM,:] B2 = sys.B[ELIM,:] A22I = np.linalg.inv(A22) if method=='matchdc': # if matchdc, residualize Ar = A11 - A12*A22.I*A21 Br = B1 - A12*A22.I*B2 Cr = C1 - C2*A22.I*A21 Dr = sys.D - C2*A22.I*B2 elif method=='truncate': # if truncate, simply discard state x2 Ar = A11 Br = B1 Cr = C1 Dr = sys.D else: raise ValueError("Oops, method is not supported!") rsys = StateSpace(Ar,Br,Cr,Dr) return rsys
def forced_response(sys, T=None, U=0., X0=0., transpose=False, **keywords): """Simulate the output of a linear system. As a convenience for parameters `U`, `X0`: Numbers (scalars) are converted to constant arrays with the correct shape. The correct shape is inferred from arguments `sys` and `T`. For information on the **shape** of parameters `U`, `T`, `X0` and return values `T`, `yout`, `xout` see: :ref:`time-series-convention` Parameters ---------- sys: Lti (StateSpace, or TransferFunction) LTI system to simulate T: array-like Time steps at which the input is defined, numbers must be (strictly monotonic) increasing. U: array-like or number, optional Input array giving input at each time `T` (default = 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. X0: array-like or number, optional Initial condition (default = 0). transpose: bool If True, transpose all input and output arrays (for backward compatibility with MATLAB and scipy.signal.lsim) **keywords: Additional keyword arguments control the solution algorithm for the differential equations. These arguments are passed on to the function :func:`scipy.integrate.odeint`. See the documentation for :func:`scipy.integrate.odeint` for information about these arguments. Returns ------- T: array Time values of the output. yout: array Response of the system. xout: array Time evolution of the state vector. See Also -------- step_response, initial_response, impulse_response Examples -------- >>> T, yout, xout = forced_response(sys, T, u, X0) """ if not isinstance(sys, Lti): raise TypeError('Parameter ``sys``: must be a ``Lti`` object. ' '(For example ``StateSpace`` or ``TransferFunction``)') sys = _convertToStateSpace(sys) A, B, C, D = np.asarray(sys.A), np.asarray(sys.B), np.asarray(sys.C), \ np.asarray(sys.D) # d_type = A.dtype n_states = A.shape[0] n_inputs = B.shape[1] # Set and/or check time vector in discrete time case if isdtime(sys, strict=True): if T == None: if U == None: raise ValueError('Parameters ``T`` and ``U`` can\'t both be' 'zero for discrete-time simulation') # Set T to integers with same length as U T = range(len(U)) else: # Make sure the input vector and time vector have same length # TODO: allow interpolation of the input vector if len(U) != len(T): ValueError('Pamameter ``T`` must have same length as' 'input vector ``U``') # Test if T has shape (n,) or (1, n); # T must be array-like and values must be increasing. # The length of T determines the length of the input vector. if T is None: raise ValueError('Parameter ``T``: must be array-like, and contain ' '(strictly monotonic) increasing numbers.') T = _check_convert_array(T, [('any', ), (1, 'any')], 'Parameter ``T``: ', squeeze=True, transpose=transpose) if not all(T[1:] - T[:-1] > 0): raise ValueError('Parameter ``T``: time values must be ' '(strictly monotonic) increasing numbers.') n_steps = len(T) # number of simulation steps #create X0 if not given, test if X0 has correct shape X0 = _check_convert_array(X0, [(n_states, ), (n_states, 1)], 'Parameter ``X0``: ', squeeze=True) # Separate out the discrete and continuous time cases if isctime(sys): # Solve the differential equation, copied from scipy.signal.ltisys. dot, squeeze, = np.dot, np.squeeze #Faster and shorter code # Faster algorithm if U is zero if U is None or (isinstance(U, (int, float)) and U == 0): # Function that computes the time derivative of the linear system def f_dot(x, _t): return dot(A, x) xout = sp.integrate.odeint(f_dot, X0, T, **keywords) yout = dot(C, xout.T) # General algorithm that interpolates U in between output points else: # Test if U has correct shape and type legal_shapes = [(n_steps,), (1,n_steps)] if n_inputs == 1 else \ [(n_inputs, n_steps)] U = _check_convert_array(U, legal_shapes, 'Parameter ``U``: ', squeeze=False, transpose=transpose) # convert 1D array to D2 array with only one row if len(U.shape) == 1: U = U.reshape(1, -1) #pylint: disable=E1103 # Create a callable that uses linear interpolation to # calculate the input at any time. compute_u = \ sp.interpolate.interp1d(T, U, kind='linear', copy=False, axis=-1, bounds_error=False, fill_value=0) # Function that computes the time derivative of the linear system def f_dot(x, t): return dot(A, x) + squeeze(dot(B, compute_u([t]))) xout = sp.integrate.odeint(f_dot, X0, T, **keywords) yout = dot(C, xout.T) + dot(D, U) yout = squeeze(yout) xout = xout.T else: # Discrete time simulation using signal processing toolbox dsys = (A, B, C, D, sys.dt) tout, yout, xout = sp.signal.dlsim(dsys, U, T, X0) # See if we need to transpose the data back into MATLAB form if (transpose): T = np.transpose(T) yout = np.transpose(yout) xout = np.transpose(xout) return T, yout, xout