51
51
"""
52
52
53
53
import math
54
+ from warnings import warn
54
55
import numpy as np
55
56
import scipy as sp
56
57
from . import xferfcn
57
58
from .lti import issiso , evalfr
58
59
from . import frdata
60
+ from . import freqplot
59
61
from .exception import ControlMIMONotImplemented
60
62
61
63
__all__ = ['stability_margins' , 'phase_crossover_frequencies' , 'margin' ]
@@ -206,6 +208,17 @@ def fun(wdt):
206
208
207
209
return z , w
208
210
211
+ def _likely_numerical_inaccuracy (sys ):
212
+ # crude, conservative check for if
213
+ # num(z)*num(1/z) << den(z)*den(1/z) for DT systems
214
+ num , den , num_inv_zp , den_inv_zq , p_q , dt = _poly_z_invz (sys )
215
+ p1 = np .polymul (num , num_inv_zp )
216
+ p2 = np .polymul (den , den_inv_zq )
217
+ if p_q < 0 :
218
+ # * z**(-p_q)
219
+ x = [1 ] + [0 ] * (- p_q )
220
+ p1 = np .polymul (p1 , x )
221
+ return np .linalg .norm (p1 ) < 1e-3 * np .linalg .norm (p2 )
209
222
210
223
# Took the framework for the old function by
211
224
# Sawyer B. Fuller <minster@uw.edu>, removed a lot of the innards
@@ -237,25 +250,34 @@ def fun(wdt):
237
250
# systems
238
251
239
252
240
- def stability_margins (sysdata , returnall = False , epsw = 0.0 ):
253
+ def stability_margins (sysdata , returnall = False , epsw = 0.0 , method = 'best' ):
241
254
"""Calculate stability margins and associated crossover frequencies.
242
255
243
256
Parameters
244
257
----------
245
- sysdata: LTI system or (mag, phase, omega) sequence
258
+ sysdata : LTI system or (mag, phase, omega) sequence
246
259
sys : LTI system
247
260
Linear SISO system representing the loop transfer function
248
261
mag, phase, omega : sequence of array_like
249
262
Arrays of magnitudes (absolute values, not dB), phases (degrees),
250
263
and corresponding frequencies. Crossover frequencies returned are
251
264
in the same units as those in `omega` (e.g., rad/sec or Hz).
252
- returnall: bool, optional
265
+ returnall : bool, optional
253
266
If true, return all margins found. If False (default), return only the
254
267
minimum stability margins. For frequency data or FRD systems, only
255
268
margins in the given frequency region can be found and returned.
256
- epsw: float, optional
269
+ epsw : float, optional
257
270
Frequencies below this value (default 0.0) are considered static gain,
258
271
and not returned as margin.
272
+ method : string, optional
273
+ Method to use (default is 'best'):
274
+ 'poly': use polynomial method if passed a :class:`LTI` system.
275
+ 'frd': calculate crossover frequencies using numerical interpolation
276
+ of a :class:`FrequencyResponseData` representation of the system if
277
+ passed a :class:`LTI` system.
278
+ 'best': use the 'poly' method if possible, reverting to 'frd' if it is
279
+ detected that numerical inaccuracy is likey to arise in the 'poly'
280
+ method for for discrete-time systems.
259
281
260
282
Returns
261
283
-------
@@ -279,6 +301,7 @@ def stability_margins(sysdata, returnall=False, epsw=0.0):
279
301
determined by the frequency of maximum sensitivity (given by the
280
302
magnitude of 1/(1+L)).
281
303
"""
304
+ # TODO: FRD method for cont-time systems doesn't work
282
305
try :
283
306
if isinstance (sysdata , frdata .FRD ):
284
307
sys = frdata .FRD (sysdata , smooth = True )
@@ -300,6 +323,27 @@ def stability_margins(sysdata, returnall=False, epsw=0.0):
300
323
raise ControlMIMONotImplemented (
301
324
"Can only do margins for SISO system" )
302
325
326
+ if method == 'frd' :
327
+ # convert to FRD if we got a transfer function
328
+ if isinstance (sys , xferfcn .TransferFunction ):
329
+ omega_sys = freqplot ._default_frequency_range (sys )
330
+ if sys .isctime ():
331
+ sys = frdata .FRD (sys , omega_sys )
332
+ else :
333
+ omega_sys = omega_sys [omega_sys < np .pi / sys .dt ]
334
+ sys = frdata .FRD (sys , omega_sys , smooth = True )
335
+ elif method == 'best' :
336
+ # convert to FRD if anticipated numerical issues
337
+ if isinstance (sys , xferfcn .TransferFunction ) and not sys .isctime ():
338
+ if _likely_numerical_inaccuracy (sys ):
339
+ warn ("stability_margins: Falling back to 'frd' method "
340
+ "because of chance of numerical inaccuracy in 'poly' method." )
341
+ omega_sys = freqplot ._default_frequency_range (sys )
342
+ omega_sys = omega_sys [omega_sys < np .pi / sys .dt ]
343
+ sys = frdata .FRD (sys , omega_sys , smooth = True )
344
+ elif method != 'poly' :
345
+ raise ValueError ("method " + method + " unknown" )
346
+
303
347
if isinstance (sys , xferfcn .TransferFunction ):
304
348
if sys .isctime ():
305
349
num_iw , den_iw = _poly_iw (sys )
@@ -366,7 +410,6 @@ def _dstab(w):
366
410
w_180 = np .array (
367
411
[sp .optimize .brentq (_arg , sys .omega [i ], sys .omega [i + 1 ])
368
412
for i in widx ])
369
- # TODO: replace by evalfr(sys, 1J*w) or sys(1J*w), (needs gh-449)
370
413
w180_resp = sys (1j * w_180 )
371
414
372
415
# Find all crossings, note that this depends on omega having
0 commit comments