Skip to content

Commit 199858b

Browse files
committed
pick margin_test.py from #438
1 parent 2e98881 commit 199858b

1 file changed

Lines changed: 151 additions & 135 deletions

File tree

control/tests/margin_test.py

Lines changed: 151 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -3,51 +3,65 @@
33
margin_test.py - test suite for stability margin commands
44
55
RMM, 15 Jul 2011
6-
BG, 30 Juin 2020 -- convert to pytest, gh-425
6+
BG, 30 June 2020 -- convert to pytest, gh-425
77
"""
88
from __future__ import print_function
99

1010
import numpy as np
11+
import pytest
1112
from numpy import inf, nan
1213
from numpy.testing import assert_allclose
13-
import pytest
1414

1515
from control.frdata import FrequencyResponseData
16-
from control.margins import margin, phase_crossover_frequencies, \
17-
stability_margins
16+
from control.margins import (margin, phase_crossover_frequencies,
17+
stability_margins)
1818
from control.statesp import StateSpace
1919
from control.xferfcn import TransferFunction
2020
from control.exception import ControlMIMONotImplemented
2121

22-
23-
s = TransferFunction([1, 0], [1])
24-
25-
# (system, stability_margins(sys), stability_margins(sys, returnall=True))
26-
tsys = [(TransferFunction([1, 2], [1, 2, 3]),
27-
(inf, inf, inf, nan, nan, nan),
28-
([], [], [], [], [], [])),
29-
(TransferFunction([1], [1, 2, 3, 4]),
30-
(2., inf, 0.4170, 1.7321, nan, 1.6620),
31-
([2.], [], [1.2500, 0.4170], [1.7321], [], [0.1690, 1.6620])),
32-
(StateSpace([[1., 4.], [3., 2.]], [[1.], [-4.]],
33-
[[1., 0.]], [[0.]]),
34-
(inf, 147.0743, inf, nan, 2.5483, nan),
35-
([], [147.0743], [], [], [2.5483], [])),
36-
((8.75*(4*s**2+0.4*s+1)) / ((100*s+1)*(s**2+0.22*s+1))
37-
/ (s**2/(10.**2)+2*0.04*s/10.+1),
38-
(2.2716, 97.5941, 0.5591, 10.0053, 0.0850, 9.9918),
39-
([2.2716], [97.5941, -157.7844, 134.7359], [1.0381, 0.5591],
40-
[10.0053], [0.0850, 0.9373, 1.0919], [0.4064, 9.9918])),
41-
(1/(1+s), # no gain/phase crossovers
42-
(inf, inf, inf, nan, nan, nan),
43-
([], [], [], [], [], [])),
44-
(3*(10+s)/(2+s), # no gain/phase crossovers
45-
(inf, inf, inf, nan, nan, nan),
46-
([], [], [], [], [], [])),
47-
(0.01*(10-s)/(2+s)/(1+s), # no phase crossovers
48-
(300.0, inf, 0.9917, 5.6569, nan, 2.3171),
49-
([300.0], [], [0.9917], [5.6569], [], 2.3171))]
50-
22+
s = TransferFunction.s
23+
24+
@pytest.fixture(params=[
25+
# sysfn, args,
26+
# stability_margins(sys),
27+
# stability_margins(sys, returnall=True)
28+
(TransferFunction, ([1, 2], [1, 2, 3]),
29+
(inf, inf, inf, nan, nan, nan),
30+
([], [], [], [], [], [])),
31+
(TransferFunction, ([1], [1, 2, 3, 4]),
32+
(2., inf, 0.4170, 1.7321, nan, 1.6620),
33+
([2.], [], [1.2500, 0.4170], [1.7321], [], [0.1690, 1.6620])),
34+
(StateSpace, ([[1., 4.],
35+
[3., 2.]],
36+
[[1.], [-4.]],
37+
[[1., 0.]],
38+
[[0.]]),
39+
(inf, 147.0743, inf, nan, 2.5483, nan),
40+
([], [147.0743], [], [], [2.5483], [])),
41+
(None, ((8.75 * (4 * s**2 + 0.4 * s + 1))
42+
/ ((100 * s + 1) * (s**2 + 0.22 * s + 1))
43+
/ (s**2 / 10.**2 + 2 * 0.04 * s / 10. + 1)),
44+
(2.2716, 97.5941, 0.5591, 10.0053, 0.0850, 9.9918),
45+
([2.2716], [97.5941, -157.7844, 134.7359], [1.0381, 0.5591],
46+
[10.0053], [0.0850, 0.9373, 1.0919], [0.4064, 9.9918])),
47+
(None, (1 / (1 + s)), # no gain/phase crossovers
48+
(inf, inf, inf, nan, nan, nan),
49+
([], [], [], [], [], [])),
50+
(None, (3 * (10 + s) / (2 + s)), # no gain/phase crossovers
51+
(inf, inf, inf, nan, nan, nan),
52+
([], [], [], [], [], [])),
53+
(None, 0.01 * (10 - s) / (2 + s) / (1 + s), # no phase crossovers
54+
(300.0, inf, 0.9917, 5.6569, nan, 2.3171),
55+
([300.0], [], [0.9917], [5.6569], [], 2.3171)),
56+
])
57+
def tsys(request):
58+
"""Return test systems and reference data"""
59+
sysfn, args = request.param[:2]
60+
if sysfn:
61+
sys = sysfn(*args)
62+
else:
63+
sys = args
64+
return (sys,) + request.param[2:]
5165

5266
def compare_allmargins(actual, desired, **kwargs):
5367
"""Compare all elements of stability_margins(returnall=True) result"""
@@ -56,41 +70,42 @@ def compare_allmargins(actual, desired, **kwargs):
5670
assert_allclose(a, d, **kwargs)
5771

5872

59-
@pytest.mark.parametrize("sys, refout, refoutall", tsys)
60-
def test_stability_margins(sys, refout, refoutall):
73+
def test_stability_margins(tsys):
74+
sys, refout, refoutall = tsys
6175
"""Test stability_margins() function"""
6276
out = stability_margins(sys)
6377
assert_allclose(out, refout, atol=1.5e-2)
6478
out = stability_margins(sys, returnall=True)
6579
compare_allmargins(out, refoutall, atol=1.5e-2)
6680

6781

68-
@pytest.mark.parametrize("sys, refout, refoutall", tsys)
69-
def test_stability_margins_omega(sys, refout, refoutall):
82+
83+
def test_stability_margins_omega(tsys):
84+
sys, refout, refoutall = tsys
7085
"""Test stability_margins() with interpolated frequencies"""
7186
omega = np.logspace(-2, 2, 2000)
7287
out = stability_margins(FrequencyResponseData(sys, omega))
7388
assert_allclose(out, refout, atol=1.5e-3)
7489

7590

76-
@pytest.mark.parametrize("sys, refout, refoutall", tsys)
77-
def test_stability_margins_3input(sys, refout, refoutall):
91+
def test_stability_margins_3input(tsys):
92+
sys, refout, refoutall = tsys
7893
"""Test stability_margins() function with mag, phase, omega input"""
7994
omega = np.logspace(-2, 2, 2000)
8095
mag, phase, omega_ = sys.freqresp(omega)
8196
out = stability_margins((mag, phase*180/np.pi, omega_))
8297
assert_allclose(out, refout, atol=1.5e-3)
8398

8499

85-
@pytest.mark.parametrize("sys, refout, refoutall", tsys)
86-
def test_margin_sys(sys, refout, refoutall):
100+
def test_margin_sys(tsys):
101+
sys, refout, refoutall = tsys
87102
"""Test margin() function with system input"""
88103
out = margin(sys)
89104
assert_allclose(out, np.array(refout)[[0, 1, 3, 4]], atol=1.5e-3)
90105

91106

92-
@pytest.mark.parametrize("sys, refout, refoutall", tsys)
93-
def test_margin_3input(sys, refout, refoutall):
107+
def test_margin_3input(tsys):
108+
sys, refout, refoutall = tsys
94109
"""Test margin() function with mag, phase, omega input"""
95110
omega = np.logspace(-2, 2, 2000)
96111
mag, phase, omega_ = sys.freqresp(omega)
@@ -100,7 +115,8 @@ def test_margin_3input(sys, refout, refoutall):
100115

101116
def test_phase_crossover_frequencies():
102117
"""Test phase_crossover_frequencies() function"""
103-
omega, gain = phase_crossover_frequencies(tsys[1][0])
118+
sys = TransferFunction([1], [1, 2, 3, 4])
119+
omega, gain = phase_crossover_frequencies(sys)
104120
assert_allclose(omega, [1.73205, 0.], atol=1.5e-3)
105121
assert_allclose(gain, [-0.5, 0.25], atol=1.5e-3)
106122

@@ -215,99 +231,99 @@ def test_frd_indexing():
215231
assert_allclose(ws, [1., 2.], atol=0.01)
216232

217233

218-
"""
219-
NOTE:
220-
Matlab gives gain margin 0 for system `type2`, python-control gives inf
221-
Difficult to argue which is right? Special case or different approach?
234+
@pytest.fixture
235+
def tsys_zmoresystems():
236+
"""A cornucopia of tricky systems for phase / gain margin
222237
223-
Edge cases, like `type0` which approaches a gain of 1 for w -> 0, are also not
224-
identically indicated, Matlab gives phase margin -180, at w = 0. For higher or
225-
lower gains, results match.
226-
"""
227-
tzmore_sys = {
228-
'typem1': s/(s+1),
229-
'type0': 1/(s+1)**3,
230-
'type1': (s + 0.1)/s/(s+1),
231-
'type2': (s + 0.1)/s**2/(s+1),
232-
'type3': (s + 0.1)*(s+0.1)/s**3/(s+1)}
233-
tzmore_margin = [
234-
dict(sys='typem1', K=2.0, atol=1.5e-3, result=(
235-
float('Inf'), -120.0007, float('NaN'), 0.5774)),
236-
dict(sys='type0', K=0.8, atol=1.5e-3, result=(
237-
10.0014, float('inf'), 1.7322, float('nan'))),
238-
dict(sys='type0', K=2.0, atol=1e-2, result=(
239-
4.000, 67.6058, 1.7322, 0.7663)),
240-
dict(sys='type1', K=1.0, atol=1e-4, result=(
241-
float('Inf'), 144.9032, float('NaN'), 0.3162)),
242-
dict(sys='type2', K=1.0, atol=1e-4, result=(
243-
float('Inf'), 44.4594, float('NaN'), 0.7907)),
244-
dict(sys='type3', K=1.0, atol=1.5e-3, result=(
245-
0.0626, 37.1748, 0.1119, 0.7951)),
246-
]
247-
tzmore_stability_margins = []
238+
`example*` from "A note on the Gain and Phase Margin Concepts
239+
Journal of Control and Systems Engineering, Yazdan Bavafi-Toosi,
240+
Dec 2015, vol 3 iss 1, pp 51-59
248241
249-
"""
250-
from "A note on the Gain and Phase Margin Concepts
251-
Journal of Control and Systems Engineering, Yazdan Bavafi-Toosi,
252-
Dec 2015, vol 3 iss 1, pp 51-59
242+
TODO: still have to convert more to tests + fix margin to handle
243+
also these torture cases
244+
"""
253245

254-
A cornucopia of tricky systems for phase / gain margin
255-
TODO: still have to convert more to tests + fix margin to handle
256-
also these torture cases
257-
"""
258-
yazdan = {
259-
'example21':
260-
0.002*(s+0.02)*(s+0.05)*(s+5)*(s+10)/(
261-
(s-0.0005)*(s+0.0001)*(s+0.01)*(s+0.2)*(s+1)*(s+100)**2),
262-
'example23':
263-
((s+0.1)**2 + 1)*(s-0.1)/(
264-
((s+0.1)**2+4)*(s+1)),
265-
'example25a':
266-
s/(s**2+2*s+2)**4,
267-
'example26a':
268-
((s-0.1)**2 + 1)/(
269-
(s + 0.1)*((s-0.2)**2 + 4)),
270-
'example26b': ((s-0.1)**2 + 1)/(
271-
(s - 0.3)*((s-0.2)**2 + 4))
272-
}
273-
yazdan['example24'] = yazdan['example21']*20000
274-
yazdan['example25b'] = yazdan['example25a']*100
275-
yazdan['example22'] = yazdan['example21']*(s**2 - 2*s + 401)
276-
ymargin = [
277-
dict(sys='example21', K=1.0, atol=1e-2,
278-
result=(0.0100, -14.5640, 0, 0.0022)),
279-
dict(sys='example21', K=1000.0, atol=1e-2,
280-
result=(0.1793, 22.5215, 0.0243, 0.0630)),
281-
dict(sys='example21', K=5000.0, atol=1.5e-3,
282-
result=(4.5596, 21.2101, 0.4385, 0.1868)),
283-
]
284-
ystability_margins = [
285-
dict(sys='example21', K=1.0, rtol=1e-3, atol=1e-3,
286-
result=([0.01, 179.2931, 2.2798e+4, 1.5946e+07, 7.2477e+08],
287-
[-14.5640],
288-
[0.2496],
289-
[0, 0.0243, 0.4385, 6.8640, 84.9323],
290-
[0.0022],
291-
[0.0022])),
292-
]
293-
294-
tzmore_sys.update(yazdan)
295-
tzmore_margin += ymargin
296-
tzmore_stability_margins += ystability_margins
297-
298-
299-
@pytest.mark.parametrize('tmargin', tzmore_margin)
300-
def test_zmore_margin(tmargin):
301-
"""Test margins for more tricky systems"""
302-
res = margin(tzmore_sys[tmargin['sys']]*tmargin['K'])
303-
assert_allclose(res, tmargin['result'], atol=tmargin['atol'])
304-
305-
306-
@pytest.mark.parametrize('tmarginall', tzmore_stability_margins)
307-
def test_zmore_stability_margins(tmarginall):
246+
systems = {
247+
'typem1': s/(s+1),
248+
'type0': 1/(s+1)**3,
249+
'type1': (s + 0.1)/s/(s+1),
250+
'type2': (s + 0.1)/s**2/(s+1),
251+
'type3': (s + 0.1)*(s+0.1)/s**3/(s+1),
252+
'example21': 0.002*(s+0.02)*(s+0.05)*(s+5)*(s+10) / (
253+
(s-0.0005)*(s+0.0001)*(s+0.01)*(s+0.2)*(s+1)*(s+100)**2),
254+
'example23': ((s+0.1)**2 + 1)*(s-0.1)/(((s+0.1)**2+4)*(s+1)),
255+
'example25a': s/(s**2+2*s+2)**4,
256+
'example26a': ((s-0.1)**2 + 1)/((s + 0.1)*((s-0.2)**2 + 4)),
257+
'example26b': ((s-0.1)**2 + 1)/((s - 0.3)*((s-0.2)**2 + 4))
258+
}
259+
systems['example24'] = systems['example21'] * 20000
260+
systems['example25b'] = systems['example25a'] * 100
261+
systems['example22'] = systems['example21'] * (s**2 - 2*s + 401)
262+
return systems
263+
264+
265+
@pytest.fixture
266+
def tsys_zmore(request, tsys_zmoresystems):
267+
tsys = request.param
268+
tsys['sys'] = tsys_zmoresystems[tsys['sysname']]
269+
return tsys
270+
271+
272+
@pytest.mark.parametrize(
273+
'tsys_zmore',
274+
[dict(sysname='typem1', K=2.0, atol=1.5e-3,
275+
result=(float('Inf'), -120.0007, float('NaN'), 0.5774)),
276+
dict(sysname='type0', K=0.8, atol=1.5e-3,
277+
result=(10.0014, float('inf'), 1.7322, float('nan'))),
278+
dict(sysname='type0', K=2.0, atol=1e-2,
279+
result=(4.000, 67.6058, 1.7322, 0.7663)),
280+
dict(sysname='type1', K=1.0, atol=1e-4,
281+
result=(float('Inf'), 144.9032, float('NaN'), 0.3162)),
282+
dict(sysname='type2', K=1.0, atol=1e-4,
283+
result=(float('Inf'), 44.4594, float('NaN'), 0.7907)),
284+
dict(sysname='type3', K=1.0, atol=1.5e-3,
285+
result=(0.0626, 37.1748, 0.1119, 0.7951)),
286+
dict(sysname='example21', K=1.0, atol=1e-2,
287+
result=(0.0100, -14.5640, 0, 0.0022)),
288+
dict(sysname='example21', K=1000.0, atol=1e-2,
289+
result=(0.1793, 22.5215, 0.0243, 0.0630)),
290+
dict(sysname='example21', K=5000.0, atol=1.5e-3,
291+
result=(4.5596, 21.2101, 0.4385, 0.1868)),
292+
],
293+
indirect=True)
294+
def test_zmore_margin(tsys_zmore):
295+
"""Test margins for more tricky systems
296+
297+
Note
298+
----
299+
Matlab gives gain margin 0 for system `type2`, python-control gives inf
300+
Difficult to argue which is right? Special case or different approach?
301+
302+
Edge cases, like `type0` which approaches a gain of 1 for w -> 0, are also
303+
not identically indicated, Matlab gives phase margin -180, at w = 0. For
304+
higher or lower gains, results match.
305+
"""
306+
307+
res = margin(tsys_zmore['sys'] * tsys_zmore['K'])
308+
assert_allclose(res, tsys_zmore['result'], atol=tsys_zmore['atol'])
309+
310+
311+
@pytest.mark.parametrize(
312+
'tsys_zmore',
313+
[dict(sysname='example21', K=1.0, rtol=1e-3, atol=1e-3,
314+
result=([0.01, 179.2931, 2.2798e+4, 1.5946e+07, 7.2477e+08],
315+
[-14.5640],
316+
[0.2496],
317+
[0, 0.0243, 0.4385, 6.8640, 84.9323],
318+
[0.0022],
319+
[0.0022])),
320+
],
321+
indirect=True)
322+
def test_zmore_stability_margins(tsys_zmore):
308323
"""Test stability_margins for more tricky systems with returnall"""
309-
res = stability_margins(tzmore_sys[tmarginall['sys']]*tmarginall['K'],
324+
res = stability_margins(tsys_zmore['sys'] * tsys_zmore['K'],
310325
returnall=True)
311-
compare_allmargins(res, tmarginall['result'],
312-
atol=tmarginall['atol'],
313-
rtol=tmarginall['rtol'])
326+
compare_allmargins(res,
327+
tsys_zmore['result'],
328+
atol=tsys_zmore['atol'],
329+
rtol=tsys_zmore['rtol'])

0 commit comments

Comments
 (0)