Skip to content

Commit ae09104

Browse files
committed
Fix the margin calculation for FRD/data based margins.
TODO: verify the stability margin
1 parent 6523270 commit ae09104

2 files changed

Lines changed: 94 additions & 17 deletions

File tree

control/margins.py

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ def _polysqr(pol):
8484
#
8585
# RvP, July 8, 2014, corrected to exclude phase=0 crossing for the gain
8686
# margin polynomial
87+
# RvP, July 8, 2015, augmented to calculate all phase/gain crossings with
88+
# frd data. Correct to return smallest phase
89+
# margin, smallest gain margin and their frequencies
8790
def stability_margins(sysdata, returnall=False, epsw=1e-10):
8891
"""Calculate stability margins and associated crossover frequencies.
8992
@@ -175,6 +178,8 @@ def stability_margins(sysdata, returnall=False, epsw=1e-10):
175178
# stability margin was a bitch to elaborate, relies on magnitude to
176179
# point -1, then take the derivative. Second derivative needs to be >0
177180
# to have a minimum
181+
# from comparison to numerical one below, this seems to be wrong!
182+
# no one complained so far
178183
test_wstabn = np.polyadd(_polysqr(rnum), _polysqr(inum))
179184
test_wstabd = np.polyadd(_polysqr(np.polyadd(rnum,rden)),
180185
_polysqr(np.polyadd(inum,iden)))
@@ -195,37 +200,50 @@ def stability_margins(sysdata, returnall=False, epsw=1e-10):
195200
# a bit coarse, have the interpolated frd evaluated again
196201
def mod(w):
197202
"""to give the function to calculate |G(jw)| = 1"""
198-
return [np.abs(sys.evalfr(w[0])[0][0]) - 1]
203+
return np.abs(sys.evalfr(w)[0][0]) - 1
199204

200205
def arg(w):
201206
"""function to calculate the phase angle at -180 deg"""
202-
return [np.angle(sys.evalfr(w[0])[0][0]) + np.pi]
207+
return np.angle(-sys.evalfr(w)[0][0])
203208

204209
def dstab(w):
205210
"""function to calculate the distance from -1 point"""
206-
return np.abs(sys.evalfr(w[0])[0][0] + 1.)
207-
208-
# how to calculate the frequency at which |G(jw)| = 1
209-
wc = np.array([sp.optimize.fsolve(mod, sys.omega[0])])[0]
210-
w_180 = np.array([sp.optimize.fsolve(arg, sys.omega[0])])[0]
211-
wstab = np.real(
212-
np.array([sp.optimize.fmin(dstab, sys.omega[0], disp=0)])[0])
211+
return np.abs(sys.evalfr(w)[0][0] + 1.)
212+
213+
# Find all crossings, note that this depends on omega having
214+
# a correct range
215+
widx = np.where(np.diff(np.sign(mod(sys.omega))))[0]
216+
wc = np.array(
217+
[ sp.optimize.brentq(mod, sys.omega[i], sys.omega[i+1])
218+
for i in widx if i+1 < len(sys.omega)])
219+
220+
# find the phase crossings ang(H(jw) == -180
221+
widx = np.where(np.diff(np.sign(arg(sys.omega))))[0]
222+
w_180 = np.array(
223+
[ sp.optimize.brentq(arg, sys.omega[i], sys.omega[i+1])
224+
for i in widx if i+1 < len(sys.omega) ])
225+
226+
# there is really only one stab margin; the closest
227+
res = sp.optimize.minimize_scalar(
228+
dstab, bracket=(sys.omega[0], sys.omega[-1]))
229+
wstab = np.array([res.x])
213230

214231
# margins, as iterables, converted frdata and xferfcn calculations to
215232
# vector for this
216-
PM = np.angle(sys.evalfr(wc)[0][0], deg=True) + 180
217233
GM = 1/(np.abs(sys.evalfr(w_180)[0][0]))
218-
SM = np.abs(sys.evalfr(wstab)[0][0]+1)
219-
234+
print(wstab)
235+
SM = 1/np.abs(sys.evalfr(wstab)[0][0]+1)
236+
PM = np.angle(sys.evalfr(wc)[0][0], deg=True) + 180
237+
220238
if returnall:
221239
return GM, PM, SM, w_180, wc, wstab
222240
else:
223241
return (
224-
(GM.shape[0] or None) and GM[0],
225-
(PM.shape[0] or None) and PM[0],
242+
(GM.shape[0] or None) and GM[GM==np.min(GM)][0],
243+
(PM.shape[0] or None) and PM[PM==np.min(PM)][0],
226244
(SM.shape[0] or None) and SM[0],
227-
(w_180.shape[0] or None) and w_180[0],
228-
(wc.shape[0] or None) and wc[0],
245+
(w_180.shape[0] or None) and w_180[GM==np.min(GM)][0],
246+
(wc.shape[0] or None) and wc[PM==np.min(PM)][0],
229247
(wstab.shape[0] or None) and wstab[0])
230248

231249

control/tests/margin_test.py

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import unittest
77
import numpy as np
88
from control.xferfcn import TransferFunction
9+
from control.frdata import FRD
910
from control.statesp import StateSpace
1011
from control.margins import *
1112

@@ -28,7 +29,7 @@ def test_stability_margins(self):
2829
gm, pm, sm, wg, wp, ws = stability_margins(self.sys4);
2930
np.testing.assert_array_almost_equal(
3031
[gm, pm, sm, wg, wp, ws],
31-
[2.2716, 97.5941, 1.0454, 10.0053, 0.0850, 0.4973], 3)
32+
[2.2716, 97.5941, 0.9565, 10.0053, 0.0850, 0.4973], 3)
3233

3334
def test_phase_crossover_frequencies(self):
3435
omega, gain = phase_crossover_frequencies(self.sys2)
@@ -59,7 +60,65 @@ def test_mag_phase_omega(self):
5960
marg2 = np.array(out2)[ind]
6061
np.testing.assert_array_almost_equal(marg1, marg2, 4)
6162

63+
def test_frd(self):
64+
f = np.array([0.005, 0.010, 0.020, 0.030, 0.040,
65+
0.050, 0.060, 0.070, 0.080, 0.090,
66+
0.100, 0.200, 0.300, 0.400, 0.500,
67+
0.750, 1.000, 1.250, 1.500, 1.750,
68+
2.000, 2.250, 2.500, 2.750, 3.000,
69+
3.250, 3.500, 3.750, 4.000, 4.250,
70+
4.500, 4.750, 5.000, 6.000, 7.000,
71+
8.000, 9.000, 10.000 ])
72+
gain = np.array([ 0.0, 0.0, 0.0, 0.0, 0.0,
73+
0.0, 0.0, 0.0, 0.0, 0.0,
74+
0.0, 0.1, 0.2, 0.3, 0.5,
75+
0.5, -0.4, -2.3, -4.8, -7.3,
76+
-9.6, -11.7, -13.6, -15.3, -16.9,
77+
-18.3, -19.6, -20.8, -22.0, -23.1,
78+
-24.1, -25.0, -25.9, -29.1, -31.9,
79+
-34.2, -36.2, -38.1 ])
80+
phase = np.array([ 0, -1, -2, -3, -4,
81+
-5, -6, -7, -8, -9,
82+
-10, -19, -29, -40, -51,
83+
-81, -114, -144, -168, -187,
84+
-202, -214, -224, -233, -240,
85+
-247, -253, -259, -264, -269,
86+
-273, -277, -280, -292, -301,
87+
-307, -313, -317 ])
88+
# calculate response as complex number
89+
resp = 10**(gain / 20) * np.exp(1j * phase / (180./np.pi))
90+
# frequency response data
91+
fresp = FRD(resp, f*2*np.pi, smooth=True)
92+
s=TransferFunction([1,0],[1])
93+
G=1./(s**2)
94+
K=1.
95+
C=K*(1+1.9*s)
96+
TFopen=fresp*C*G
97+
gm, pm, sm, wg, wp, ws = stability_margins(TFopen)
98+
print gm
99+
np.testing.assert_array_almost_equal(
100+
[pm], [44.55], 2)
101+
102+
def test_nocross(self):
103+
# what happens when no gain/phase crossover?
104+
s = TransferFunction([1, 0], [1])
105+
h1 = 1/(1+s)
106+
h2 = 3*(10+s)/(2+s)
107+
h3 = 0.01*(10-s)/(2+s)/(1+s)
108+
gm, pm, sm, wg, wp, ws = stability_margins(h1)
109+
self.assertEqual(gm, None)
110+
self.assertEqual(wg, None)
111+
gm, pm, wm, wg, wp, ws = stability_margins(h2)
112+
self.assertEqual(pm, None)
113+
gm, pm, wm, wg, wp, ws = stability_margins(h3)
114+
self.assertEqual(pm, None)
115+
omega = np.logspace(-2,2, 100)
116+
out1b = stability_margins(FRD(h1, omega))
117+
out2b = stability_margins(FRD(h2, omega))
118+
out3b = stability_margins(FRD(h3, omega))
119+
62120

121+
63122
def test_suite():
64123
return unittest.TestLoader().loadTestsFromTestCase(TestMargin)
65124

0 commit comments

Comments
 (0)