Skip to content

Commit 1d4dd64

Browse files
committed
Correction to the gain margin calculation. Old implementation also returned
gains where arg(H) == 0. Switched around the w_180 and wc return parameters, to match the order of gain margin (matching w_180) and phase margin (matching wc) return.
1 parent c2fd4b4 commit 1d4dd64

2 files changed

Lines changed: 25 additions & 3 deletions

File tree

src/margins.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ def _polysqr(pol):
8080
# idea for the frequency data solution copied/adapted from
8181
# https://github.com/alchemyst/Skogestad-Python/blob/master/BODE.py
8282
# Rene van Paassen <rene.vanpaassen@gmail.com>
83+
#
84+
# RvP, July 8, 2014, corrected to exclude phase=0 crossing for the gain
85+
# margin polynomial
8386
def stability_margins(sysdata, deg=True, returnall=False, epsw=1e-10):
8487
"""Calculate gain, phase and stability margins and associated
8588
crossover frequencies.
@@ -147,7 +150,19 @@ def stability_margins(sysdata, deg=True, returnall=False, epsw=1e-10):
147150
# test imaginary part of tf == 0, for phase crossover/gain margins
148151
test_w_180 = np.polyadd(np.polymul(inum, rden), np.polymul(rnum, -iden))
149152
w_180 = np.roots(test_w_180)
150-
w_180 = np.real(w_180[(np.imag(w_180) == 0) * (w_180 > epsw)])
153+
154+
# first remove imaginary and negative frequencies, epsw removes the
155+
# "0" frequency for type-2 systems
156+
w_180 = np.real(w_180[(np.imag(w_180) == 0) * (w_180 >= epsw)])
157+
158+
# evaluate response at remaining frequencies, to test for phase 180 vs 0
159+
resp_w_180 = np.real(np.polyval(sys.num[0][0], 1.j*w_180) /
160+
np.polyval(sys.den[0][0], 1.j*w_180))
161+
162+
# only keep frequencies where the negative real axis is crossed
163+
w_180 = w_180[(resp_w_180 < 0.0)]
164+
165+
# and sort
151166
w_180.sort()
152167

153168
# test magnitude is 1 for gain crossover/phase margins
@@ -203,14 +218,14 @@ def dstab(w):
203218
SM = np.abs(sys.evalfr(wstab)[0][0]+1)
204219

205220
if returnall:
206-
return GM, PM, SM, wc, w_180, wstab
221+
return GM, PM, SM, w_180, wc, wstab
207222
else:
208223
return (
209224
(GM.shape[0] or None) and GM[0],
210225
(PM.shape[0] or None) and PM[0],
211226
(SM.shape[0] or None) and SM[0],
212-
(wc.shape[0] or None) and wc[0],
213227
(w_180.shape[0] or None) and w_180[0],
228+
(wc.shape[0] or None) and wc[0],
214229
(wstab.shape[0] or None) and wstab[0])
215230

216231

tests/margin_test.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,18 @@ def setUp(self):
1717
self.sys2 = TransferFunction([1], [1, 2, 3, 4])
1818
self.sys3 = StateSpace([[1., 4.], [3., 2.]], [[1.], [-4.]],
1919
[[1., 0.]], [[0.]])
20+
s = TransferFunction([1, 0], [1])
21+
self.sys4 = (8.75*(4*s**2+0.4*s+1))/((100*s+1)*(s**2+0.22*s+1)) * \
22+
1./(s**2/(10.**2)+2*0.04*s/10.+1)
2023

2124
def test_stability_margins(self):
2225
gm, pm, sm, wg, wp, ws = stability_margins(self.sys1);
2326
gm, pm, sm, wg, wp, ws = stability_margins(self.sys2);
2427
gm, pm, sm, wg, wp, ws = stability_margins(self.sys3);
28+
gm, pm, sm, wg, wp, ws = stability_margins(self.sys4);
29+
np.testing.assert_array_almost_equal(
30+
[gm, pm, sm, wg, wp, ws],
31+
[2.2716, 97.5941, 1.0454, 10.0053, 0.0850, 0.4973], 3)
2532

2633
def test_phase_crossover_frequencies(self):
2734
omega, gain = phase_crossover_frequencies(self.sys2)

0 commit comments

Comments
 (0)