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 evalfr(self, omega): """Evaluate a transfer function at a single angular frequency. self.evalfr(omega) returns the value of the transfer function matrix with input value s = i * omega. """ # TODO: implement for discrete time systems if isdtime(self, strict=True): # Convert the frequency to discrete time dt = timebase(self) s = exp(1.j * omega * dt) if (omega * dt > pi): warn("evalfr: frequency evaluation above Nyquist frequency") else: s = 1.j * omega # Preallocate the output. out = empty((self.outputs, self.inputs), dtype=complex) for i in range(self.outputs): for j in range(self.inputs): out[i][j] = (polyval(self.num[i][j], s) / polyval(self.den[i][j], s)) return out
def test_class_constants_z(self): """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]]]) assert 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 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 evalfr(self, omega): """Evaluate a SS system's transfer function at a single frequency. self.evalfr(omega) returns the value of the transfer function matrix with input value s = i * omega. """ # Figure out the point to evaluate the transfer function if isdtime(self, strict=True): dt = timebase(self) s = exp(1.j * omega * dt) if (omega * dt > pi): warn("evalfr: frequency evaluation above Nyquist frequency") else: s = omega * 1.j return self.horner(s)
def hsvd(sys): """Calculate the Hankel singular values. Parameters ---------- sys : StateSpace A state space system Returns ------- H : Matrix A list of Hankel singular values See Also -------- gram Notes ----- The Hankel singular values are the singular values of the Hankel operator. In practice, we compute the square root of the eigenvalues of the matrix formed by taking the product of the observability and controllability gramians. There are other (more efficient) methods based on solving the Lyapunov equation in a particular way (more details soon). Examples -------- >>> H = hsvd(sys) """ # TODO: implement for discrete time systems if (isdtime(sys, strict=True)): raise NotImplementedError("Function not implemented in discrete time") Wc = gram(sys, 'c') Wo = gram(sys, 'o') WoWc = np.dot(Wo, Wc) w, v = np.linalg.eig(WoWc) hsv = np.sqrt(w) hsv = np.matrix(hsv) hsv = np.sort(hsv) hsv = np.fliplr(hsv) # Return the Hankel singular values return hsv
def hsvd(sys): """Calculate the Hankel singular values. Parameters ---------- sys : StateSpace A state space system Returns ------- H : Matrix A list of Hankel singular values See Also -------- gram Notes ----- The Hankel singular values are the singular values of the Hankel operator. In practice, we compute the square root of the eigenvalues of the matrix formed by taking the product of the observability and controllability gramians. There are other (more efficient) methods based on solving the Lyapunov equation in a particular way (more details soon). Examples -------- >>> H = hsvd(sys) """ # TODO: implement for discrete time systems if (isdtime(sys, strict=True)): raise NotImplementedError("Function not implemented in discrete time") Wc = gram(sys,'c') Wo = gram(sys,'o') WoWc = np.dot(Wo, Wc) w, v = np.linalg.eig(WoWc) hsv = np.sqrt(w) hsv = np.matrix(hsv) hsv = np.sort(hsv) hsv = np.fliplr(hsv) # Return the Hankel singular values return hsv
def evalfr(self, omega): """Evaluate a transfer function at a single angular frequency. self.evalfr(omega) returns the value of the transfer function matrix with input value s = i * omega. """ # TODO: implement for discrete time systems if isdtime(self, strict=True): # Convert the frequency to discrete time dt = timebase(self) s = exp(1.j * omega * dt) if (omega * dt > pi): warn("evalfr: frequency evaluation above Nyquist frequency") else: s = 1.j * omega return self.horner(s)
def freqresp(self, omega): """Evaluate a transfer function at a list of angular frequencies. mag, phase, omega = self.freqresp(omega) reports the value of the magnitude, phase, and angular frequency of the transfer function matrix evaluated at s = i * omega, where omega is a list of angular frequencies, and is a sorted version of the input omega. """ # Preallocate outputs. numfreq = len(omega) mag = empty((self.outputs, self.inputs, numfreq)) phase = empty((self.outputs, self.inputs, numfreq)) # Figure out the frequencies omega.sort() if isdtime(self, strict=True): dt = timebase(self) slist = map(lambda w: exp(1.j * w * dt), omega) if (max(omega) * dt > pi): warn("evalfr: frequency evaluation above Nyquist frequency") else: slist = map(lambda w: 1.j * w, omega) # Compute frequency response for each input/output pair for i in range(self.outputs): for j in range(self.inputs): fresp = map( lambda s: (polyval(self.num[i][j], s) / polyval(self.den[i][j], s)), slist) fresp = array(list(fresp)) mag[i, j, :] = abs(fresp) phase[i, j, :] = angle(fresp) return mag, phase, omega
def freqresp(self, omega): """Evaluate a transfer function at a list of angular frequencies. mag, phase, omega = self.freqresp(omega) reports the value of the magnitude, phase, and angular frequency of the transfer function matrix evaluated at s = i * omega, where omega is a list of angular frequencies, and is a sorted version of the input omega. """ # Preallocate outputs. numfreq = len(omega) mag = empty((self.outputs, self.inputs, numfreq)) phase = empty((self.outputs, self.inputs, numfreq)) # Figure out the frequencies omega.sort(); if isdtime(self, strict=True): dt = timebase(self) slist = map(lambda w: exp(1.j * w * dt), omega) if (max(omega) * dt > pi): warn("evalfr: frequency evaluation above Nyquist frequency") else: slist = map(lambda w: 1.j * w, omega) # Compute frequency response for each input/output pair for i in range(self.outputs): for j in range(self.inputs): fresp = map(lambda s: (polyval(self.num[i][j], s) / polyval(self.den[i][j], s)), slist) fresp = array(list(fresp)) mag[i, j, :] = abs(fresp) phase[i, j, :] = angle(fresp) return mag, phase, omega
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 testisdtime(self): # Constant self.assertEqual(isdtime(1), True) self.assertEqual(isdtime(1, strict=True), False) # State space self.assertEqual(isdtime(self.siso_ss1), True) self.assertEqual(isdtime(self.siso_ss1, strict=True), False) self.assertEqual(isdtime(self.siso_ss1c), False) self.assertEqual(isdtime(self.siso_ss1c, strict=True), False) self.assertEqual(isdtime(self.siso_ss1d), True) self.assertEqual(isdtime(self.siso_ss1d, strict=True), True) self.assertEqual(isdtime(self.siso_ss3d, strict=True), True) # Transfer function self.assertEqual(isdtime(self.siso_tf1), True) self.assertEqual(isdtime(self.siso_tf1, strict=True), False) self.assertEqual(isdtime(self.siso_tf1c), False) self.assertEqual(isdtime(self.siso_tf1c, strict=True), False) self.assertEqual(isdtime(self.siso_tf1d), True) self.assertEqual(isdtime(self.siso_tf1d, strict=True), True) self.assertEqual(isdtime(self.siso_tf3d, strict=True), True)
def testisdtime(self): # Constant self.assertEqual(isdtime(1), True); self.assertEqual(isdtime(1, strict=True), False); # State space self.assertEqual(isdtime(self.siso_ss1), True); self.assertEqual(isdtime(self.siso_ss1, strict=True), False); self.assertEqual(isdtime(self.siso_ss1c), False); self.assertEqual(isdtime(self.siso_ss1c, strict=True), False); self.assertEqual(isdtime(self.siso_ss1d), True); self.assertEqual(isdtime(self.siso_ss1d, strict=True), True); self.assertEqual(isdtime(self.siso_ss3d, strict=True), True); # Transfer function self.assertEqual(isdtime(self.siso_tf1), True); self.assertEqual(isdtime(self.siso_tf1, strict=True), False); self.assertEqual(isdtime(self.siso_tf1c), False); self.assertEqual(isdtime(self.siso_tf1c, strict=True), False); self.assertEqual(isdtime(self.siso_tf1d), True); self.assertEqual(isdtime(self.siso_tf1d, strict=True), True); self.assertEqual(isdtime(self.siso_tf3d, strict=True), True);
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 stability_margins(sysdata, deg=True): """Calculate gain, phase and stability margins and associated crossover frequencies. Usage ----- gm, pm, sm, wg, wp, ws = stability_margins(sysdata, deg=True) Parameters ---------- sysdata: linsys or (mag, phase, omega) sequence sys : linsys Linear SISO system mag, phase, omega : sequence of array_like Input magnitude, phase, and frequencies (rad/sec) sequence from bode frequency response data deg=True: boolean If true, all input and output phases in degrees, else in radians Returns ------- gm, pm, sm, wg, wp, ws: float Gain margin gm, phase margin pm, stability margin sm, and associated crossover frequencies wg, wp, and ws of SISO open-loop. If more than one crossover frequency is detected, returns the lowest corresponding margin. """ #TODO do this precisely without the effects of discretization of frequencies? #TODO assumes SISO #TODO unit tests, margin plot if (not getattr(sysdata, '__iter__', False)): sys = sysdata # TODO: implement for discrete time systems if (isdtime(sys, strict=True)): raise NotImplementedError("Function not implemented in discrete time") mag, phase, omega = bode(sys, deg=deg, Plot=False) elif len(sysdata) == 3: # TODO: replace with FRD object type? mag, phase, omega = sysdata else: raise ValueError("Margin sysdata must be either a linear system or a 3-sequence of mag, phase, omega.") if deg: cycle = 360. crossover = 180. else: cycle = 2 * np.pi crossover = np.pi wrapped_phase = -np.mod(phase, cycle) # phase margin from minimum phase among all gain crossovers neg_mag_crossings_i = np.nonzero(np.diff(mag < 1) > 0)[0] mag_crossings_p = wrapped_phase[neg_mag_crossings_i] if len(neg_mag_crossings_i) == 0: if mag[0] < 1: # gain always less than one wp = np.nan pm = np.inf else: # gain always greater than one print("margin: no magnitude crossings found") wp = np.nan pm = np.nan else: min_mag_crossing_i = neg_mag_crossings_i[np.argmin(mag_crossings_p)] wp = omega[min_mag_crossing_i] pm = crossover + phase[min_mag_crossing_i] if pm < 0: print("warning: system unstable: negative phase margin") # gain margin from minimum gain margin among all phase crossovers neg_phase_crossings_i = np.nonzero(np.diff(wrapped_phase < -crossover) > 0)[0] neg_phase_crossings_g = mag[neg_phase_crossings_i] if len(neg_phase_crossings_i) == 0: wg = np.nan gm = np.inf else: min_phase_crossing_i = neg_phase_crossings_i[ np.argmax(neg_phase_crossings_g)] wg = omega[min_phase_crossing_i] gm = abs(1/mag[min_phase_crossing_i]) if gm < 1: print("warning: system unstable: gain margin < 1") # stability margin from minimum abs distance from -1 point if deg: phase_rad = phase * np.pi / 180. else: phase_rad = phase L = mag * np.exp(1j * phase_rad) # complex loop response to -1 pt min_Lplus1_i = np.argmin(np.abs(L + 1)) sm = np.abs(L[min_Lplus1_i] + 1) ws = phase[min_Lplus1_i] return gm, pm, sm, wg, wp, ws