@@ -84,7 +84,10 @@ def _polysqr(pol):
84
84
#
85
85
# RvP, July 8, 2014, corrected to exclude phase=0 crossing for the gain
86
86
# margin polynomial
87
- def stability_margins (sysdata , returnall = False , epsw = 1e-10 ):
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
90
+ def stability_margins (sysdata , returnall = False , epsw = 1e-8 ):
88
91
"""Calculate stability margins and associated crossover frequencies.
89
92
90
93
Parameters
@@ -101,7 +104,7 @@ def stability_margins(sysdata, returnall=False, epsw=1e-10):
101
104
minimum stability margins. For frequency data or FRD systems, only one
102
105
margin is found and returned.
103
106
epsw: float, optional
104
- Frequencies below this value (default 1e-10 ) are considered static gain,
107
+ Frequencies below this value (default 1e-8 ) are considered static gain,
105
108
and not returned as margin.
106
109
107
110
Returns
@@ -127,8 +130,8 @@ def stability_margins(sysdata, returnall=False, epsw=1e-10):
127
130
sys = sysdata
128
131
elif getattr (sysdata , '__iter__' , False ) and len (sysdata ) == 3 :
129
132
mag , phase , omega = sysdata
130
- sys = frdata .FRD (mag * np .exp (1j * phase * np .pi / 180 ), omega ,
131
- smooth = True )
133
+ sys = frdata .FRD (mag * np .exp (1j * phase * np .pi / 180 ),
134
+ omega , smooth = True )
132
135
else :
133
136
sys = xferfcn ._convertToTransferFunction (sysdata )
134
137
except Exception as e :
@@ -147,23 +150,27 @@ def stability_margins(sysdata, returnall=False, epsw=1e-10):
147
150
rnum , inum = _polyimsplit (sys .num [0 ][0 ])
148
151
rden , iden = _polyimsplit (sys .den [0 ][0 ])
149
152
150
- # test imaginary part of tf == 0, for phase crossover/gain margins
153
+ # test ( imaginary part of tf) == 0, for phase crossover/gain margins
151
154
test_w_180 = np .polyadd (np .polymul (inum , rden ), np .polymul (rnum , - iden ))
152
155
w_180 = np .roots (test_w_180 )
156
+ #print ('1:w_180', w_180)
153
157
154
158
# first remove imaginary and negative frequencies, epsw removes the
155
159
# "0" frequency for type-2 systems
156
160
w_180 = np .real (w_180 [(np .imag (w_180 ) == 0 ) * (w_180 >= epsw )])
161
+ #print ('2:w_180', w_180)
157
162
158
163
# evaluate response at remaining frequencies, to test for phase 180 vs 0
159
164
resp_w_180 = np .real (np .polyval (sys .num [0 ][0 ], 1.j * w_180 ) /
160
165
np .polyval (sys .den [0 ][0 ], 1.j * w_180 ))
166
+ #print ('resp_w_180', resp_w_180)
161
167
162
168
# only keep frequencies where the negative real axis is crossed
163
- w_180 = w_180 [(resp_w_180 < 0.0 ) ]
169
+ w_180 = w_180 [np . real (resp_w_180 ) < 0.0 ]
164
170
165
171
# and sort
166
172
w_180 .sort ()
173
+ #print ('3:w_180', w_180)
167
174
168
175
# test magnitude is 1 for gain crossover/phase margins
169
176
test_wc = np .polysub (np .polyadd (_polysqr (rnum ), _polysqr (inum )),
@@ -175,58 +182,91 @@ def stability_margins(sysdata, returnall=False, epsw=1e-10):
175
182
# stability margin was a bitch to elaborate, relies on magnitude to
176
183
# point -1, then take the derivative. Second derivative needs to be >0
177
184
# to have a minimum
178
- test_wstabn = np .polyadd (_polysqr (rnum ), _polysqr (inum ))
179
- test_wstabd = np .polyadd (_polysqr (np .polyadd (rnum ,rden )),
185
+ test_wstabd = np .polyadd (_polysqr (rden ), _polysqr (iden ))
186
+ test_wstabn = np .polyadd (_polysqr (np .polyadd (rnum ,rden )),
180
187
_polysqr (np .polyadd (inum ,iden )))
181
188
test_wstab = np .polysub (
182
189
np .polymul (np .polyder (test_wstabn ),test_wstabd ),
183
190
np .polymul (np .polyder (test_wstabd ),test_wstabn ))
184
191
185
- # find the solutions
192
+ # find the solutions, for positive omega, and only real ones
186
193
wstab = np .roots (test_wstab )
194
+ #print('wstabr', wstab)
195
+ wstab = np .real (wstab [(np .imag (wstab ) == 0 ) *
196
+ (np .real (wstab ) >= 0 )])
197
+ #print('wstab', wstab)
187
198
188
199
# and find the value of the 2nd derivative there, needs to be positive
189
200
wstabplus = np .polyval (np .polyder (test_wstab ), wstab )
201
+ #print('wstabplus', wstabplus)
190
202
wstab = np .real (wstab [(np .imag (wstab ) == 0 ) * (wstab > epsw ) *
191
- (np .abs (wstabplus ) > 0. )])
203
+ (wstabplus > 0. )])
204
+ #print('wstab', wstab)
192
205
wstab .sort ()
193
206
194
207
else :
195
208
# a bit coarse, have the interpolated frd evaluated again
196
209
def mod (w ):
197
210
"""to give the function to calculate |G(jw)| = 1"""
198
- return [ np .abs (sys .evalfr (w [ 0 ] )[0 ][0 ]) - 1 ]
211
+ return np .abs (sys .evalfr (w )[0 ][0 ]) - 1
199
212
200
213
def arg (w ):
201
214
"""function to calculate the phase angle at -180 deg"""
202
- return [ np .angle (sys .evalfr (w [ 0 ] )[0 ][0 ]) + np . pi ]
215
+ return np .angle (- sys .evalfr (w )[0 ][0 ])
203
216
204
217
def dstab (w ):
205
218
"""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 ])
219
+ return np .abs (sys .evalfr (w )[0 ][0 ] + 1. )
220
+
221
+ # Find all crossings, note that this depends on omega having
222
+ # a correct range
223
+ widx = np .where (np .diff (np .sign (mod (sys .omega ))))[0 ]
224
+ wc = np .array (
225
+ [ sp .optimize .brentq (mod , sys .omega [i ], sys .omega [i + 1 ])
226
+ for i in widx if i + 1 < len (sys .omega )])
227
+
228
+ # find the phase crossings ang(H(jw) == -180
229
+ widx = np .where (np .diff (np .sign (arg (sys .omega ))))[0 ]
230
+ #print('widx (180)', widx, sys.omega[widx])
231
+ #print('x', sys.evalfr(sys.omega[widx])[0][0])
232
+ widx = widx [np .real (sys .evalfr (sys .omega [widx ])[0 ][0 ]) <= 0 ]
233
+ #print('widx (180,2)', widx)
234
+ w_180 = np .array (
235
+ [ sp .optimize .brentq (arg , sys .omega [i ], sys .omega [i + 1 ])
236
+ for i in widx if i + 1 < len (sys .omega ) ])
237
+ #print('x', sys.evalfr(w_180)[0][0])
238
+ #print('w_180', w_180)
239
+
240
+ # find all stab margins?
241
+ widx = np .where (np .diff (np .sign (np .diff (dstab (sys .omega )))))[0 ]
242
+ #print('widx', widx)
243
+ #print('wstabx', sys.omega[widx])
244
+ wstab = np .array ([ sp .optimize .minimize_scalar (
245
+ dstab , bracket = (sys .omega [i ], sys .omega [i + 1 ])).x
246
+ for i in widx if i + 1 < len (sys .omega ) and
247
+ np .diff (np .diff (dstab (sys .omega [i - 1 :i + 2 ])))[0 ] > 0 ])
248
+ #print('wstabf0', wstab)
249
+ wstab = wstab [(wstab >= sys .omega [0 ]) *
250
+ (wstab <= sys .omega [- 1 ])]
251
+ #print ('wstabf', wstab)
252
+
213
253
214
254
# margins, as iterables, converted frdata and xferfcn calculations to
215
255
# vector for this
216
- PM = np .angle (sys .evalfr (wc )[0 ][0 ], deg = True ) + 180
217
- GM = 1 / (np .abs (sys .evalfr (w_180 )[0 ][0 ]))
256
+ GM = 1 / np .abs (sys .evalfr (w_180 )[0 ][0 ])
218
257
SM = np .abs (sys .evalfr (wstab )[0 ][0 ]+ 1 )
219
-
258
+ PM = np .angle (sys .evalfr (wc )[0 ][0 ], deg = True ) + 180
259
+
220
260
if returnall :
221
261
return GM , PM , SM , w_180 , wc , wstab
222
262
else :
223
263
return (
224
- (GM .shape [0 ] or None ) and GM [ 0 ] ,
225
- (PM .shape [0 ] or None ) and PM [ 0 ] ,
226
- (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 ],
229
- (wstab .shape [0 ] or None ) and wstab [0 ])
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 ])
230
270
231
271
232
272
# Contributed by Steffen Waldherr <waldherr@ist.uni-stuttgart.de>
@@ -291,11 +331,12 @@ def margin(*args):
291
331
Returns
292
332
-------
293
333
gm, pm, Wcg, Wcp : float
294
- Gain margin gm, phase margin pm (in deg), gain crossover frequency
295
- (corresponding to phase margin) and phase crossover frequency
296
- (corresponding to gain margin), in rad/sec of SISO open-loop.
297
- If more than one crossover frequency is detected, returns the lowest
298
- corresponding margin.
334
+ Gain margin gm, phase margin pm (in deg), phase crossover frequency
335
+ (corresponding to gain margin, where phase=-180) and gain crossover
336
+ frequency (corresponding to phase margin, where gain is 0dB),
337
+ in rad/sec of SISO open-loop.
338
+ If more than one crossover frequency is detected for gain or phase,
339
+ this returns one with the lowest corresponding margin.
299
340
300
341
Examples
301
342
--------
0 commit comments