33margin_test.py - test suite for stability margin commands
44
55RMM, 15 Jul 2011
6- BG, 30 Juin 2020 -- convert to pytest, gh-425
6+ BG, 30 June 2020 -- convert to pytest, gh-425
77"""
88from __future__ import print_function
99
1010import numpy as np
11+ import pytest
1112from numpy import inf , nan
1213from numpy .testing import assert_allclose
13- import pytest
1414
1515from 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 )
1818from control .statesp import StateSpace
1919from control .xferfcn import TransferFunction
2020from 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
5266def 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
101116def 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