Example #1
0
    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
Example #2
0
    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))
Example #5
0
    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)
Example #7
0
    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)
Example #8
0
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
Example #9
0
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
Example #10
0
    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 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
Example #13
0
    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
Example #14
0
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
Example #15
0
    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)
Example #16
0
    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);
Example #17
0
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
Example #18
0
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