5151"""
5252
5353import math
54+ from warnings import warn
5455import numpy as np
5556import scipy as sp
5657from . import xferfcn
5758from .lti import issiso , evalfr
5859from . import frdata
60+ from . import freqplot
5961from .exception import ControlMIMONotImplemented
6062
6163__all__ = ['stability_margins' , 'phase_crossover_frequencies' , 'margin' ]
@@ -206,6 +208,17 @@ def fun(wdt):
206208
207209 return z , w
208210
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 )
209222
210223# Took the framework for the old function by
211224# Sawyer B. Fuller <minster@uw.edu>, removed a lot of the innards
@@ -237,25 +250,34 @@ def fun(wdt):
237250# systems
238251
239252
240- def stability_margins (sysdata , returnall = False , epsw = 0.0 ):
253+ def stability_margins (sysdata , returnall = False , epsw = 0.0 , method = 'best' ):
241254 """Calculate stability margins and associated crossover frequencies.
242255
243256 Parameters
244257 ----------
245- sysdata: LTI system or (mag, phase, omega) sequence
258+ sysdata : LTI system or (mag, phase, omega) sequence
246259 sys : LTI system
247260 Linear SISO system representing the loop transfer function
248261 mag, phase, omega : sequence of array_like
249262 Arrays of magnitudes (absolute values, not dB), phases (degrees),
250263 and corresponding frequencies. Crossover frequencies returned are
251264 in the same units as those in `omega` (e.g., rad/sec or Hz).
252- returnall: bool, optional
265+ returnall : bool, optional
253266 If true, return all margins found. If False (default), return only the
254267 minimum stability margins. For frequency data or FRD systems, only
255268 margins in the given frequency region can be found and returned.
256- epsw: float, optional
269+ epsw : float, optional
257270 Frequencies below this value (default 0.0) are considered static gain,
258271 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.
259281
260282 Returns
261283 -------
@@ -279,6 +301,7 @@ def stability_margins(sysdata, returnall=False, epsw=0.0):
279301 determined by the frequency of maximum sensitivity (given by the
280302 magnitude of 1/(1+L)).
281303 """
304+ # TODO: FRD method for cont-time systems doesn't work
282305 try :
283306 if isinstance (sysdata , frdata .FRD ):
284307 sys = frdata .FRD (sysdata , smooth = True )
@@ -300,6 +323,27 @@ def stability_margins(sysdata, returnall=False, epsw=0.0):
300323 raise ControlMIMONotImplemented (
301324 "Can only do margins for SISO system" )
302325
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+
303347 if isinstance (sys , xferfcn .TransferFunction ):
304348 if sys .isctime ():
305349 num_iw , den_iw = _poly_iw (sys )
@@ -366,7 +410,6 @@ def _dstab(w):
366410 w_180 = np .array (
367411 [sp .optimize .brentq (_arg , sys .omega [i ], sys .omega [i + 1 ])
368412 for i in widx ])
369- # TODO: replace by evalfr(sys, 1J*w) or sys(1J*w), (needs gh-449)
370413 w180_resp = sys (1j * w_180 )
371414
372415 # Find all crossings, note that this depends on omega having
0 commit comments