Skip to content

Commit a8a536b

Browse files
committed
new method keyword in stability_margins; falls back to FRD when numerical inaccuracy in dt polynomial calculation
1 parent 92de12d commit a8a536b

3 files changed

Lines changed: 60 additions & 10 deletions

File tree

control/freqplot.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@
8888

8989
def bode_plot(syslist, omega=None,
9090
plot=True, omega_limits=None, omega_num=None,
91-
margins=None, *args, **kwargs):
91+
margins=None, method='best', *args, **kwargs):
9292
"""Bode plot for a system
9393
9494
Plots a Bode plot for the system over a (optional) frequency range.
@@ -117,6 +117,7 @@ def bode_plot(syslist, omega=None,
117117
config.defaults['freqplot.number_of_samples'].
118118
margins : bool
119119
If True, plot gain and phase margin.
120+
method : method to use in computing margins (see :func:`stability_margins`)
120121
*args : :func:`matplotlib.pyplot.plot` positional properties, optional
121122
Additional arguments for `matplotlib` plots (color, linestyle, etc)
122123
**kwargs : :func:`matplotlib.pyplot.plot` keyword properties, optional
@@ -373,7 +374,7 @@ def bode_plot(syslist, omega=None,
373374
# Show the phase and gain margins in the plot
374375
if margins:
375376
# Compute stability margins for the system
376-
margin = stability_margins(sys)
377+
margin = stability_margins(sys, method=method)
377378
gm, pm, Wcg, Wcp = (margin[i] for i in (0, 1, 3, 4))
378379

379380
# Figure out sign of the phase at the first gain crossing

control/margins.py

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
from . import xferfcn
5757
from .lti import issiso, evalfr
5858
from . import frdata
59+
from . import freqplot
5960
from .exception import ControlMIMONotImplemented
6061

6162
__all__ = ['stability_margins', 'phase_crossover_frequencies', 'margin']
@@ -206,6 +207,17 @@ def fun(wdt):
206207

207208
return z, w
208209

210+
def _numerical_inaccuracy(sys):
211+
# crude, conservative check for if
212+
# num(z)*num(1/z) << den(z)*den(1/z) for DT systems
213+
num, den, num_inv_zp, den_inv_zq, p_q, dt = _poly_z_invz(sys)
214+
p1 = np.polymul(num, num_inv_zp)
215+
p2 = np.polymul(den, den_inv_zq)
216+
if p_q < 0:
217+
# * z**(-p_q)
218+
x = [1] + [0] * (-p_q)
219+
p1 = np.polymul(p1, x)
220+
return np.linalg.norm(p1) < 1e-3 * np.linalg.norm(p2)
209221

210222
# Took the framework for the old function by
211223
# Sawyer B. Fuller <minster@uw.edu>, removed a lot of the innards
@@ -237,25 +249,34 @@ def fun(wdt):
237249
# systems
238250

239251

240-
def stability_margins(sysdata, returnall=False, epsw=0.0):
252+
def stability_margins(sysdata, returnall=False, epsw=0.0, method='best'):
241253
"""Calculate stability margins and associated crossover frequencies.
242254
243255
Parameters
244256
----------
245-
sysdata: LTI system or (mag, phase, omega) sequence
257+
sysdata : LTI system or (mag, phase, omega) sequence
246258
sys : LTI system
247259
Linear SISO system representing the loop transfer function
248260
mag, phase, omega : sequence of array_like
249261
Arrays of magnitudes (absolute values, not dB), phases (degrees),
250262
and corresponding frequencies. Crossover frequencies returned are
251263
in the same units as those in `omega` (e.g., rad/sec or Hz).
252-
returnall: bool, optional
264+
returnall : bool, optional
253265
If true, return all margins found. If False (default), return only the
254266
minimum stability margins. For frequency data or FRD systems, only
255267
margins in the given frequency region can be found and returned.
256-
epsw: float, optional
268+
epsw : float, optional
257269
Frequencies below this value (default 0.0) are considered static gain,
258270
and not returned as margin.
271+
method : string, optional
272+
Method to use (default is 'best'):
273+
'poly': use polynomial method if passed a :class:`LTI` system.
274+
'frd': calculate crossover frequencies using numerical interpolation
275+
of a :class:`FrequencyResponseData` representation of the system if
276+
passed a :class:`LTI` system.
277+
'best': use the 'poly' method if possible, reverting to 'frd' if it is
278+
detected that numerical inaccuracy is likey to arise in the 'poly'
279+
method for for discrete-time systems.
259280
260281
Returns
261282
-------
@@ -300,6 +321,27 @@ def stability_margins(sysdata, returnall=False, epsw=0.0):
300321
raise ControlMIMONotImplemented(
301322
"Can only do margins for SISO system")
302323

324+
if method == 'frd':
325+
# convert to FRD if we got a transfer function
326+
if isinstance(sys, xferfcn.TransferFunction):
327+
omega_sys = freqplot._default_frequency_range(sys)
328+
if sys.isctime():
329+
sys = frdata.FRD(sys, omega_sys)
330+
else:
331+
omega_sys = omega_sys[omega_sys < np.pi / sys.dt]
332+
sys = frdata.FRD(sys(np.exp(1j*sys.dt*omega_sys)), omega_sys,
333+
smooth=True)
334+
elif method == 'best':
335+
# convert to FRD if anticipated numerical issues
336+
if isinstance(sys, xferfcn.TransferFunction) and not sys.isctime():
337+
if _numerical_inaccuracy(sys):
338+
omega_sys = freqplot._default_frequency_range(sys)
339+
omega_sys = omega_sys[omega_sys < np.pi / sys.dt]
340+
sys = frdata.FRD(sys(np.exp(1j*sys.dt*omega_sys)), omega_sys,
341+
smooth=True)
342+
elif method != 'poly':
343+
raise ValueError("method " + method + " unknown")
344+
303345
if isinstance(sys, xferfcn.TransferFunction):
304346
if sys.isctime():
305347
num_iw, den_iw = _poly_iw(sys)

control/tests/margin_test.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,6 @@ def test_margin_sys(tsys):
104104
out = margin(sys)
105105
assert_allclose(out, np.array(refout)[[0, 1, 3, 4]], atol=1.5e-3)
106106

107-
108107
def test_margin_3input(tsys):
109108
sys, refout, refoutall = tsys
110109
"""Test margin() function with mag, phase, omega input"""
@@ -339,11 +338,11 @@ def test_zmore_stability_margins(tsys_zmore):
339338
'ref,'
340339
'rtol',
341340
[( # gh-465
342-
[2], [1, 3, 2, 0], 1e-2,
341+
[2], [1, 3, 2, 0], 1e-2,
343342
[2.9558, 32.390, 0.43584, 1.4037, 0.74951, 0.97079],
344343
2e-3), # the gradient of the function reduces numerical precision
345344
( # 2/(s+1)**3
346-
[2], [1, 3, 3, 1], .1,
345+
[2], [1, 3, 3, 1], .1,
347346
[3.4927, 65.4212, 0.5763, 1.6283, 0.76625, 1.2019],
348347
1e-4),
349348
( # gh-523
@@ -354,5 +353,13 @@ def test_zmore_stability_margins(tsys_zmore):
354353
def test_stability_margins_discrete(cnum, cden, dt, ref, rtol):
355354
"""Test stability_margins with discrete TF input"""
356355
tf = TransferFunction(cnum, cden).sample(dt)
357-
out = stability_margins(tf)
356+
out = stability_margins(tf, method='poly')
358357
assert_allclose(out, ref, rtol=rtol)
358+
359+
def test_stability_margins_methods():
360+
sys = TransferFunction(1, (1, 2))
361+
"""Test stability_margins() function with different methods"""
362+
out = stability_margins(sys, method='best')
363+
out = stability_margins(sys, method='frd')
364+
out = stability_margins(sys, method='poly')
365+

0 commit comments

Comments
 (0)