def test_stability_margins(tsys): sys, refout, refoutall = tsys """Test stability_margins() function""" out = stability_margins(sys) assert_allclose(out, refout, atol=1.5e-2) out = stability_margins(sys, returnall=True) compare_allmargins(out, refoutall, atol=1.5e-2)
def test_mag_phase_omega(): """Test for bug reported in gh-58""" sys = TransferFunction(15, [1, 6, 11, 6]) out = stability_margins(sys) omega = np.logspace(-2, 2, 1000) mag, phase, omega = sys.frequency_response(omega) out2 = stability_margins((mag, phase * 180 / np.pi, omega)) ind = [0, 1, 3, 4] # indices of gm, pm, wg, wp -- ignore sm marg1 = np.array(out)[ind] marg2 = np.array(out2)[ind] assert_allclose(marg1, marg2, atol=1.5e-3)
def test_stability_margins_discrete(cnum, cden, dt, ref, rtol, poly_is_inaccurate): """Test stability_margins with discrete TF input""" tf = TransferFunction(cnum, cden).sample(dt) if poly_is_inaccurate: with pytest.warns(UserWarning, match="numerical inaccuracy in 'poly'"): out = stability_margins(tf) # cover the explicit frd branch and make sure it yields the same # results as the fallback mechanism out_frd = stability_margins(tf, method='frd') assert_allclose(out, out_frd) else: out = stability_margins(tf) assert_allclose(out, ref, rtol=rtol)
def test_stability_margins_3input(tsys): sys, refout, refoutall = tsys """Test stability_margins() function with mag, phase, omega input""" omega = np.logspace(-2, 2, 2000) mag, phase, omega_ = sys.frequency_response(omega) out = stability_margins((mag, phase * 180 / np.pi, omega_)) assert_allclose(out, refout, atol=1.5e-3)
def test_frd(): """Test FrequencyResonseData margins""" f = np.array([ 0.005, 0.010, 0.020, 0.030, 0.040, 0.050, 0.060, 0.070, 0.080, 0.090, 0.100, 0.200, 0.300, 0.400, 0.500, 0.750, 1.000, 1.250, 1.500, 1.750, 2.000, 2.250, 2.500, 2.750, 3.000, 3.250, 3.500, 3.750, 4.000, 4.250, 4.500, 4.750, 5.000, 6.000, 7.000, 8.000, 9.000, 10.000 ]) gain = np.array([ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.1, 0.2, 0.3, 0.5, 0.5, -0.4, -2.3, -4.8, -7.3, -9.6, -11.7, -13.6, -15.3, -16.9, -18.3, -19.6, -20.8, -22.0, -23.1, -24.1, -25.0, -25.9, -29.1, -31.9, -34.2, -36.2, -38.1 ]) phase = np.array([ 0, -1, -2, -3, -4, -5, -6, -7, -8, -9, -10, -19, -29, -40, -51, -81, -114, -144, -168, -187, -202, -214, -224, -233, -240, -247, -253, -259, -264, -269, -273, -277, -280, -292, -301, -307, -313, -317 ]) # calculate response as complex number resp = 10**(gain / 20) * np.exp(1j * phase / (180. / np.pi)) # frequency response data fresp = FrequencyResponseData(resp, f * 2 * np.pi, smooth=True) s = TransferFunction([1, 0], [1]) G = 1. / (s**2) K = 1. C = K * (1 + 1.9 * s) TFopen = fresp * C * G gm, pm, sm, wg, wp, ws = stability_margins(TFopen) assert_allclose([pm], [44.55], atol=.01)
def test_zmore_stability_margins(tsys_zmore): """Test stability_margins for more tricky systems with returnall""" res = stability_margins(tsys_zmore['sys'] * tsys_zmore['K'], returnall=True) compare_allmargins(res, tsys_zmore['result'], atol=tsys_zmore['atol'], rtol=tsys_zmore['rtol'])
def margin(*args): """Calculate gain and phase margins and associated crossover frequencies Function ``margin`` takes either 1 or 3 parameters. Parameters ---------- sys : StateSpace or TransferFunction Linear SISO system mag, phase, w : array_like Input magnitude, phase (in deg.), and frequencies (rad/sec) from bode frequency response data Returns ------- gm, pm, Wcg, Wcp : float Gain margin gm, phase margin pm (in deg), gain crossover frequency (corresponding to phase margin) and phase crossover frequency (corresponding to gain margin), in rad/sec of SISO open-loop. If more than one crossover frequency is detected, returns the lowest corresponding margin. Examples -------- >>> sys = ss("1. -2; 3. -4", "5.; 7", "6. 8", "9.") >>> gm, pm, wg, wp = margin(sys) margin: no magnitude crossings found .. todo:: better ecample system! #>>> gm, pm, wg, wp = margin(mag, phase, w) """ if len(args) == 1: sys = args[0] margin = margins.stability_margins(sys) elif len(args) == 3: margin = margins.stability_margins(args) else: raise ValueError("Margin needs 1 or 3 arguments; received %i." % len(args)) return margin[0], margin[1], margin[4], margin[3]
def margin(*args): """Calculate gain and phase margins and associated crossover frequencies Function ``margin`` takes either 1 or 3 parameters. Parameters ---------- sys : StateSpace or TransferFunction Linear SISO system mag, phase, w : array_like Input magnitude, phase (in deg.), and frequencies (rad/sec) from bode frequency response data Returns ------- gm, pm, Wcg, Wcp : float Gain margin gm, phase margin pm (in deg), gain crossover frequency (corresponding to phase margin) and phase crossover frequency (corresponding to gain margin), in rad/sec of SISO open-loop. If more than one crossover frequency is detected, returns the lowest corresponding margin. Examples -------- >>> sys = ss("1. -2; 3. -4", "5.; 7", "6. 8", "9.") >>> gm, pm, wg, wp = margin(sys) margin: no magnitude crossings found .. todo:: better ecample system! #>>> gm, pm, wg, wp = margin(mag, phase, w) """ if len(args) == 1: sys = args[0] margin = margins.stability_margins(sys) elif len(args) == 3: margin = margins.stability_margins(args) else: raise ValueError("Margin needs 1 or 3 arguments; received %i." % len(args)) return margin[0], margin[1], margin[4], margin[3]
def test_frd_indexing(): """Test FRD edge cases Make sure frd objects with non benign data do not raise exceptions when the stability criteria evaluate at the first or last frequency point bug reported in gh-407 """ # frequency points just a little under 1. and over 2. w = np.linspace(.99, 2.01, 11) # Note: stability_margins will convert the frd with smooth=True # gain margins # p crosses -180 at w[0]=1. and w[-1]=2. m = 0.6 p = -180 * (2 * w - 1) d = m * np.exp(1J * np.pi / 180 * p) frd_gm = FrequencyResponseData(d, w) gm, _, _, wg, _, _ = stability_margins(frd_gm, returnall=True) assert_allclose(gm, [1 / m, 1 / m], atol=0.01) assert_allclose(wg, [1., 2.], atol=0.01) # phase margins # m crosses 1 at w[0]=1. and w[-1]=2. m = -(2 * w - 3)**4 + 2 p = -90. d = m * np.exp(1J * np.pi / 180 * p) frd_pm = FrequencyResponseData(d, w) _, pm, _, _, wp, _ = stability_margins(frd_pm, returnall=True) assert_allclose(pm, [90., 90.], atol=0.01) assert_allclose(wp, [1., 2.], atol=0.01) # stability margins # minimum abs(d+1)=1-m at w[1]=1. and w[-2]=2., in nyquist plot w = np.arange(.9, 2.1, 0.1) m = 0.6 p = -180 * (2 * w - 1) d = m * np.exp(1J * np.pi / 180 * p) frd_sm = FrequencyResponseData(d, w) _, _, sm, _, _, ws = stability_margins(frd_sm, returnall=True) assert_allclose(sm, [1 - m, 1 - m], atol=0.01) assert_allclose(ws, [1., 2.], atol=0.01)
def test_stability_margins_omega(tsys): sys, refout, refoutall = tsys """Test stability_margins() with interpolated frequencies""" omega = np.logspace(-2, 2, 2000) out = stability_margins(FrequencyResponseData(sys, omega)) assert_allclose(out, refout, atol=1.5e-3)
def test_stability_margins_discrete(cnum, cden, dt, ref, rtol): """Test stability_margins with discrete TF input""" tf = TransferFunction(cnum, cden).sample(dt) out = stability_margins(tf) assert_allclose(out, ref, rtol=rtol)