Skip to content

Commit 9abfee5

Browse files
committed
work in progress on margins
1 parent 07d004a commit 9abfee5

2 files changed

Lines changed: 101 additions & 34 deletions

File tree

control/margins.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ def _polysqr(pol):
8787
# RvP, July 8, 2015, augmented to calculate all phase/gain crossings with
8888
# frd data. Correct to return smallest phase
8989
# margin, smallest gain margin and their frequencies
90-
def stability_margins(sysdata, returnall=False, epsw=1e-8):
90+
def stability_margins(sysdata, returnall=False, epsw=0.0):
9191
"""Calculate stability margins and associated crossover frequencies.
9292
9393
Parameters
@@ -157,16 +157,15 @@ def stability_margins(sysdata, returnall=False, epsw=1e-8):
157157

158158
# first remove imaginary and negative frequencies, epsw removes the
159159
# "0" frequency for type-2 systems
160-
w_180 = np.real(w_180[(np.imag(w_180) == 0) * (w_180 >= epsw)])
160+
w_180 = np.real(w_180[(np.imag(w_180) == 0) * (w_180 > epsw)])
161161
#print ('2:w_180', w_180)
162162

163163
# evaluate response at remaining frequencies, to test for phase 180 vs 0
164164
resp_w_180 = np.real(np.polyval(sys.num[0][0], 1.j*w_180) /
165165
np.polyval(sys.den[0][0], 1.j*w_180))
166-
#print ('resp_w_180', resp_w_180)
167-
166+
168167
# only keep frequencies where the negative real axis is crossed
169-
w_180 = w_180[np.real(resp_w_180) < 0.0]
168+
w_180 = w_180[np.real(resp_w_180) <= 0.0]
170169

171170
# and sort
172171
w_180.sort()
@@ -253,20 +252,21 @@ def dstab(w):
253252

254253
# margins, as iterables, converted frdata and xferfcn calculations to
255254
# vector for this
256-
GM = 1/np.abs(sys.evalfr(w_180)[0][0])
255+
gain_w_180 = np.abs(sys.evalfr(w_180)[0][0])
256+
GM = np.where(gain_w_180 == 0.0, float('inf'), 1.0/gain_w_180)
257257
SM = np.abs(sys.evalfr(wstab)[0][0]+1)
258-
PM = np.angle(sys.evalfr(wc)[0][0], deg=True) + 180
258+
PM = np.remainder(np.angle(sys.evalfr(wc)[0][0], deg=True), 360.0) - 180.0
259259

260260
if returnall:
261261
return GM, PM, SM, w_180, wc, wstab
262262
else:
263263
return (
264-
(GM.shape[0] or None) and np.amin(GM),
265-
(PM.shape[0] or None) and np.amin(PM),
266-
(SM.shape[0] or None) and np.amin(SM),
267-
(w_180.shape[0] or None) and w_180[GM==np.amin(GM)][0],
268-
(wc.shape[0] or None) and wc[PM==np.amin(PM)][0],
269-
(wstab.shape[0] or None) and wstab[SM==np.amin(SM)][0])
264+
(not GM.shape[0] and float('inf')) or np.amin(GM),
265+
(not PM.shape[0] and float('inf')) or np.amin(PM),
266+
(not SM.shape[0] and float('inf')) or np.amin(SM),
267+
(not w_180.shape[0] and float('nan')) or w_180[GM==np.amin(GM)][0],
268+
(not wc.shape[0] and float('nan')) or wc[PM==np.amin(PM)][0],
269+
(not wstab.shape[0] and float('nan')) or wstab[SM==np.amin(SM)][0])
270270

271271

272272
# Contributed by Steffen Waldherr <waldherr@ist.uni-stuttgart.de>

control/tests/margin_test.py

Lines changed: 88 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,22 @@
1111
from control.statesp import StateSpace
1212
from control.margins import *
1313

14+
def assert_array_almost_equal(x, y, ndigit=4):
15+
16+
x = np.array(x)
17+
y = np.array(y)
18+
try:
19+
if np.isfinite(x).any() and (np.isfinite(x) == np.isfinite(y)).all() and \
20+
(x[np.logical_not(np.isfinite(x))] ==
21+
y[np.logical_not(np.isfinite(y))]).all():
22+
np.testing.assert_array_almost_equal(
23+
x[np.isfinite(x)], y[np.isfinite(y)], ndigit)
24+
return
25+
except TypeError as e:
26+
print("Error", e, "with", x, "and", y)
27+
#raise e
28+
np.testing.assert_array_almost_equal(x, y, ndigit)
29+
1430
class TestMargin(unittest.TestCase):
1531
"""These are tests for the margin commands in margin.py."""
1632

@@ -27,10 +43,19 @@ def setUp(self):
2743
[], [], [147.0743], [2.5483]),
2844
((8.75*(4*s**2+0.4*s+1))/((100*s+1)*(s**2+0.22*s+1)) *
2945
1./(s**2/(10.**2)+2*0.04*s/10.+1),
30-
[2.2716], [10.0053], [97.5941, 360-157.7904, 134.7359],
46+
[2.2716], [10.0053], [97.5941, -157.7904, 134.7359],
3147
[0.0850, 0.9373, 1.0919]))
3248

33-
49+
50+
"""
51+
sys1 = tf([1, 2], [1, 2, 3]);
52+
sys2 = tf([1], [1, 2, 3, 4]);
53+
sys3 = ss([1, 4; 3, 2], [1; -4], ...
54+
[1, 0], [0])
55+
s = tf('s')
56+
sys4 = (8.75*(4*s^2+0.4*s+1))/((100*s+1)*(s^2+0.22*s+1)) * ...
57+
1.0/(s^2/(10.0^2)+2*0.04*s/10.0+1);
58+
"""
3459
self.sys1 = TransferFunction([1, 2], [1, 2, 3])
3560
# alternative
3661
# sys1 = tf([1, 2], [1, 2, 3])
@@ -43,13 +68,44 @@ def setUp(self):
4368
self.stability_margins4 = \
4469
[2.2716, 97.5941, 0.5591, 10.0053, 0.0850, 9.9918]
4570

71+
"""
72+
h1 = (s + 0.1)/s/(s+1);
73+
h2 = (s + 0.1)/s^2/(s+1);
74+
h3 = (s + 0.1)*(s+0.1)/s^3/(s+1);
75+
"""
76+
self.types = {
77+
'type1': (s + 0.1)/s/(s+1),
78+
'type2': (s + 0.1)/s**2/(s+1),
79+
'type3': (s + 0.1)*(s+0.1)/s**3/(s+1) }
80+
self.tmargin = ( self.types,
81+
dict(sys='type1', K=1.0, digits=4, result=(
82+
float('Inf'), 144.9032, float('NaN'), 0.3162)),
83+
dict(sys='type2', K=1.0, digits=4, result=(
84+
float('Inf'), 44.4594, float('NaN'), 0.7907)),
85+
dict(sys='type3', K=1.0, digits=3, result=(
86+
0.0626, 37.1748, 0.1119, 0.7951)),
87+
)
88+
4689
# from "A note on the Gain and Phase Margin Concepts
4790
# Journal of Control and Systems Engineering, Yazdan Bavafi-Toosi,
4891
# Dec 205, vol 3 iss 1, pp 51-59
4992
#
5093
# A cornucopia of tricky systems for phase / gain margin
5194
# Still have to convert this to tests + fix margin to handle
5295
# also these torture cases
96+
"""
97+
% matlab compatible
98+
s = tf('s');
99+
h21 = 0.002*(s+0.02)*(s+0.05)*(s+5)*(s+10)/( ...
100+
(s-0.0005)*(s+0.0001)*(s+0.01)*(s+0.2)*(s+1)*(s+100)^2 );
101+
h23 = ((s+0.1)^2 + 1)*(s-0.1)/( ...
102+
((s+0.1)^2+4)*(s+1) );
103+
h25a = s/(s^2+2*s+2)^4; h25b = h25a*100;
104+
h26a = ((s-0.1)^2 + 1)/( ...
105+
(s + 0.1)*((s-0.2)^2 + 4) ) ;
106+
h26b = ((s-0.1)^2 + 1)/( ...
107+
(s - 0.3)*((s-0.2)^2 + 4) );
108+
"""
53109
self.yazdan = {
54110
'example21' :
55111
0.002*(s+0.02)*(s+0.05)*(s+5)*(s+10)/(
@@ -93,22 +149,22 @@ def test_stability_margins(self):
93149
gm, pm, sm, wg, wp, ws = out
94150
outf = np.array(stability_margins(FRD(sys, omega)))
95151
print(out,'\n', outf)
96-
print(out != np.array(None))
97-
np.testing.assert_array_almost_equal(
98-
out[out != np.array(None)],
99-
outf[outf != np.array(None)], 2)
152+
#print(out != np.array(None))
153+
assert_array_almost_equal(
154+
out, outf, 2)
100155

101156
# final one with fixed values
102-
np.testing.assert_array_almost_equal(
157+
assert_array_almost_equal(
103158
[gm, pm, sm, wg, wp, ws],
104159
self.stability_margins4, 3)
105160

106161
def test_margin(self):
107162
gm, pm, wg, wp = margin(self.sys4)
108-
np.testing.assert_array_almost_equal(
163+
assert_array_almost_equal(
109164
[gm, pm, wg, wp],
110165
self.stability_margins4[:2] + self.stability_margins4[3:5], 3)
111166

167+
112168
def test_stability_margins_all(self):
113169
for sys,rgm,rwgm,rpm,rwpm in self.tsys:
114170
out = stability_margins(sys, returnall=True)
@@ -117,25 +173,25 @@ def test_stability_margins_all(self):
117173
for res,comp in zip(out, (rgm,rpm,[],rwgm,rwpm,[])):
118174
if comp:
119175
print(res, '\n', comp)
120-
np.testing.assert_array_almost_equal(
176+
assert_array_almost_equal(
121177
res, comp, 2)
122178

123179
def test_phase_crossover_frequencies(self):
124180
omega, gain = phase_crossover_frequencies(self.sys2)
125-
np.testing.assert_array_almost_equal(omega, [1.73205, 0.])
126-
np.testing.assert_array_almost_equal(gain, [-0.5, 0.25])
181+
assert_array_almost_equal(omega, [1.73205, 0.])
182+
assert_array_almost_equal(gain, [-0.5, 0.25])
127183

128184
tf = TransferFunction([1],[1,1])
129185
omega, gain = phase_crossover_frequencies(tf)
130-
np.testing.assert_array_almost_equal(omega, [0.])
131-
np.testing.assert_array_almost_equal(gain, [1.])
186+
assert_array_almost_equal(omega, [0.])
187+
assert_array_almost_equal(gain, [1.])
132188

133189
# testing MIMO, only (0,0) element is considered
134190
tf = TransferFunction([[[1],[2]],[[3],[4]]],
135191
[[[1, 2, 3, 4],[1,1]],[[1,1],[1,1]]])
136192
omega, gain = phase_crossover_frequencies(tf)
137-
np.testing.assert_array_almost_equal(omega, [1.73205081, 0.])
138-
np.testing.assert_array_almost_equal(gain, [-0.5, 0.25])
193+
assert_array_almost_equal(omega, [1.73205081, 0.])
194+
assert_array_almost_equal(gain, [-0.5, 0.25])
139195

140196
def test_mag_phase_omega(self):
141197
# test for bug reported in gh-58
@@ -148,7 +204,7 @@ def test_mag_phase_omega(self):
148204
ind = [0,1,3,4] # indices of gm, pm, wg, wp -- ignore sm
149205
marg1 = np.array(out)[ind]
150206
marg2 = np.array(out2)[ind]
151-
np.testing.assert_array_almost_equal(marg1, marg2, 4)
207+
assert_array_almost_equal(marg1, marg2, 4)
152208

153209
def test_frd(self):
154210
f = np.array([0.005, 0.010, 0.020, 0.030, 0.040,
@@ -185,7 +241,7 @@ def test_frd(self):
185241
C=K*(1+1.9*s)
186242
TFopen=fresp*C*G
187243
gm, pm, sm, wg, wp, ws = stability_margins(TFopen)
188-
np.testing.assert_array_almost_equal(
244+
assert_array_almost_equal(
189245
[pm], [44.55], 2)
190246

191247
def test_nocross(self):
@@ -195,16 +251,27 @@ def test_nocross(self):
195251
h2 = 3*(10+s)/(2+s)
196252
h3 = 0.01*(10-s)/(2+s)/(1+s)
197253
gm, pm, wm, wg, wp, ws = stability_margins(h1)
198-
self.assertEqual(gm, None)
199-
self.assertEqual(wg, None)
254+
assert_array_almost_equal(
255+
[gm, pm, wg, wp],
256+
[float('Inf'), float('Inf'), float('NaN'), float('NaN')])
200257
gm, pm, wm, wg, wp, ws = stability_margins(h2)
201-
self.assertEqual(pm, None)
258+
self.assertEqual(pm, float('Inf'))
202259
gm, pm, wm, wg, wp, ws = stability_margins(h3)
203-
self.assertEqual(pm, None)
260+
self.assertTrue(np.isnan(wp))
204261
omega = np.logspace(-2,2, 100)
205262
out1b = stability_margins(FRD(h1, omega))
206263
out2b = stability_margins(FRD(h2, omega))
207264
out3b = stability_margins(FRD(h3, omega))
265+
266+
def test_zmore_margin(self):
267+
sdict = self.tmargin[0]
268+
for test in self.tmargin[1:]:
269+
res = margin(sdict[test['sys']]*test['K'])
270+
print("more margin {}\n".format(sdict[test['sys']]),
271+
res, '\n', test['result'])
272+
assert_array_almost_equal(
273+
res, test['result'], test['digits'])
274+
208275

209276

210277
def test_suite():

0 commit comments

Comments
 (0)