22
33Functions for computing stability margins and related functions.
44
5- Routeins in this module:
5+ Routines in this module:
66
77margin.stability_margins
88margin.phase_crossover_frequencies
9+ margin.margin
910"""
1011
1112# Python 3 compatibility (needs to go here)
@@ -87,7 +88,17 @@ def _polysqr(pol):
8788# RvP, July 8, 2015, augmented to calculate all phase/gain crossings with
8889# frd data. Correct to return smallest phase
8990# margin, smallest gain margin and their frequencies
90- def stability_margins (sysdata , returnall = False , epsw = 1e-8 ):
91+ # RvP, Jun 10, 2017, modified the inclusion of roots found for phase
92+ # crossing to include all >= 0, made subsequent calc
93+ # insensitive to div by 0
94+ # also changed the selection of which crossings to
95+ # return on basis of "A note on the Gain and Phase
96+ # Margin Concepts" Journal of Control and Systems
97+ # Engineering, Yazdan Bavafi-Toosi, Dec 2015, vol 3
98+ # issue 1, pp 51-59, closer to Matlab behavior, but
99+ # not completely identical in edge cases, which don't
100+ # cross but touch gain=1
101+ def stability_margins (sysdata , returnall = False , epsw = 0.0 ):
91102 """Calculate stability margins and associated crossover frequencies.
92103
93104 Parameters
@@ -104,7 +115,7 @@ def stability_margins(sysdata, returnall=False, epsw=1e-8):
104115 minimum stability margins. For frequency data or FRD systems, only one
105116 margin is found and returned.
106117 epsw: float, optional
107- Frequencies below this value (default 1e-8 ) are considered static gain,
118+ Frequencies below this value (default 0.0 ) are considered static gain,
108119 and not returned as margin.
109120
110121 Returns
@@ -161,12 +172,13 @@ def stability_margins(sysdata, returnall=False, epsw=1e-8):
161172 #print ('2:w_180', w_180)
162173
163174 # evaluate response at remaining frequencies, to test for phase 180 vs 0
164- resp_w_180 = np .real (np .polyval (sys .num [0 ][0 ], 1.j * w_180 ) /
165- np .polyval (sys .den [0 ][0 ], 1.j * w_180 ))
166- #print ('resp_w_180', resp_w_180)
175+ with np .errstate (all = 'ignore' ):
176+ resp_w_180 = np .real (
177+ np .polyval (sys .num [0 ][0 ], 1.j * w_180 ) /
178+ np .polyval (sys .den [0 ][0 ], 1.j * w_180 ))
167179
168180 # only keep frequencies where the negative real axis is crossed
169- w_180 = w_180 [np .real (resp_w_180 ) < 0.0 ]
181+ w_180 = w_180 [np .real (resp_w_180 ) <= 0.0 ]
170182
171183 # and sort
172184 w_180 .sort ()
@@ -253,20 +265,30 @@ def dstab(w):
253265
254266 # margins, as iterables, converted frdata and xferfcn calculations to
255267 # vector for this
256- GM = 1 / np .abs (sys .evalfr (w_180 )[0 ][0 ])
268+ with np .errstate (all = 'ignore' ):
269+ gain_w_180 = np .abs (sys .evalfr (w_180 )[0 ][0 ])
270+ GM = 1.0 / gain_w_180
257271 SM = np .abs (sys .evalfr (wstab )[0 ][0 ]+ 1 )
258- PM = np .angle (sys .evalfr (wc )[0 ][0 ], deg = True ) + 180
259-
272+ PM = np .remainder ( np . angle (sys .evalfr (wc )[0 ][0 ], deg = True ), 360.0 ) - 180.0
273+
260274 if returnall :
261275 return GM , PM , SM , w_180 , wc , wstab
262276 else :
277+ if GM .shape [0 ] and not np .isinf (GM ).all ():
278+ with np .errstate (all = 'ignore' ):
279+ gmidx = np .where (np .abs (np .log (GM )) ==
280+ np .min (np .abs (np .log (GM ))))
281+ else :
282+ gmidx = - 1
283+ if PM .shape [0 ]:
284+ pmidx = np .where (np .abs (PM ) == np .amin (np .abs (PM )))[0 ]
263285 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 ])
286+ (not gmidx != - 1 and float ( 'inf' )) or GM [ gmidx ][ 0 ] ,
287+ (not PM .shape [0 ] and float ( 'inf' )) or PM [ pmidx ][ 0 ] ,
288+ (not SM .shape [0 ] and float ( 'inf' )) or np .amin (SM ),
289+ (not gmidx != - 1 and float ( 'nan' )) or w_180 [ gmidx ][0 ],
290+ (not wc .shape [0 ] and float ( 'nan' )) or wc [pmidx ][0 ],
291+ (not wstab .shape [0 ] and float ( 'nan' )) or wstab [SM == np .amin (SM )][0 ])
270292
271293
272294# Contributed by Steffen Waldherr <waldherr@ist.uni-stuttgart.de>
0 commit comments