1111from control .statesp import StateSpace
1212from control .margins import *
1313
14+ def assert_array_almost_equal (x , y , ndigit = 4 ):
15+
16+ x = np .array (x )
17+ y = np .array (y )
18+ try :
19+ if np .isfinite (x ).any () and \
20+ np .equal (np .isfinite (x ), np .isfinite (y )).all () and \
21+ np .equal (np .isnan (x ), np .isnan (y )).all ():
22+ np .testing .assert_array_almost_equal (
23+ x [np .isfinite (x )], y [np .isfinite (y )], ndigit )
24+ return
25+ except TypeError as e :
26+ print ("Error" , e , "with" , x , "and" , y )
27+ #raise e
28+ np .testing .assert_array_almost_equal (x , y , ndigit )
29+
1430class TestMargin (unittest .TestCase ):
1531 """These are tests for the margin commands in margin.py."""
1632
@@ -25,11 +41,21 @@ def setUp(self):
2541 (StateSpace ([[1. , 4. ], [3. , 2. ]], [[1. ], [- 4. ]],
2642 [[1. , 0. ]], [[0. ]]),
2743 [], [], [147.0743 ], [2.5483 ]),
28- ((8.75 * (4 * s ** 2 + 0.4 * s + 1 ))/ ((100 * s + 1 )* (s ** 2 + 0.22 * s + 1 )) *
29- 1. / (s ** 2 / (10. ** 2 )+ 2 * 0.04 * s / 10. + 1 ),
30- [2.2716 ], [10.0053 ], [97.5941 , 360 - 157.7904 , 134.7359 ],
44+ ((8.75 * (4 * s ** 2 + 0.4 * s + 1 ))/ ((100 * s + 1 )* (s ** 2 + 0.22 * s + 1 )) *
45+ 1. / (s ** 2 / (10. ** 2 )+ 2 * 0.04 * s / 10. + 1 ),
46+ [2.2716 ], [10.0053 ], [97.5941 , - 157.7904 , 134.7359 ],
3147 [0.0850 , 0.9373 , 1.0919 ]))
48+
3249
50+ """
51+ sys1 = tf([1, 2], [1, 2, 3]);
52+ sys2 = tf([1], [1, 2, 3, 4]);
53+ sys3 = ss([1, 4; 3, 2], [1; -4], ...
54+ [1, 0], [0])
55+ s = tf('s')
56+ sys4 = (8.75*(4*s^2+0.4*s+1))/((100*s+1)*(s^2+0.22*s+1)) * ...
57+ 1.0/(s^2/(10.0^2)+2*0.04*s/10.0+1);
58+ """
3359
3460 self .sys1 = TransferFunction ([1 , 2 ], [1 , 2 , 3 ])
3561 # alternative
@@ -43,6 +69,95 @@ def setUp(self):
4369 self .stability_margins4 = \
4470 [2.2716 , 97.5941 , 0.5591 , 10.0053 , 0.0850 , 9.9918 ]
4571
72+ """
73+ hm1 = s/(s+1);
74+ h0 = 1/(s+1)^3;
75+ h1 = (s + 0.1)/s/(s+1);
76+ h2 = (s + 0.1)/s^2/(s+1);
77+ h3 = (s + 0.1)*(s+0.1)/s^3/(s+1);
78+ """
79+ self .types = {
80+ 'typem1' : s / (s + 1 ),
81+ 'type0' : 1 / (s + 1 )** 3 ,
82+ 'type1' : (s + 0.1 )/ s / (s + 1 ),
83+ 'type2' : (s + 0.1 )/ s ** 2 / (s + 1 ),
84+ 'type3' : (s + 0.1 )* (s + 0.1 )/ s ** 3 / (s + 1 ) }
85+ self .tmargin = ( self .types ,
86+ dict (sys = 'typem1' , K = 2.0 , digits = 3 , result = (
87+ float ('Inf' ), - 120.0007 , float ('NaN' ), 0.5774 )),
88+ dict (sys = 'type0' , K = 0.8 , digits = 3 , result = (
89+ 10.0014 , float ('inf' ), 1.7322 , float ('nan' ))),
90+ dict (sys = 'type0' , K = 2.0 , digits = 2 , result = (
91+ 4.000 , 67.6058 , 1.7322 , 0.7663 )),
92+ dict (sys = 'type1' , K = 1.0 , digits = 4 , result = (
93+ float ('Inf' ), 144.9032 , float ('NaN' ), 0.3162 )),
94+ dict (sys = 'type2' , K = 1.0 , digits = 4 , result = (
95+ float ('Inf' ), 44.4594 , float ('NaN' ), 0.7907 )),
96+ dict (sys = 'type3' , K = 1.0 , digits = 3 , result = (
97+ 0.0626 , 37.1748 , 0.1119 , 0.7951 )),
98+ )
99+
100+
101+ # from "A note on the Gain and Phase Margin Concepts
102+ # Journal of Control and Systems Engineering, Yazdan Bavafi-Toosi,
103+ # Dec 2015, vol 3 iss 1, pp 51-59
104+ #
105+ # A cornucopia of tricky systems for phase / gain margin
106+ # Still have to convert more to tests + fix margin to handle
107+ # also these torture cases
108+ """
109+ % matlab compatible
110+ s = tf('s');
111+ h21 = 0.002*(s+0.02)*(s+0.05)*(s+5)*(s+10)/( ...
112+ (s-0.0005)*(s+0.0001)*(s+0.01)*(s+0.2)*(s+1)*(s+100)^2 );
113+ h23 = ((s+0.1)^2 + 1)*(s-0.1)/( ...
114+ ((s+0.1)^2+4)*(s+1) );
115+ h25a = s/(s^2+2*s+2)^4; h25b = h25a*100;
116+ h26a = ((s-0.1)^2 + 1)/( ...
117+ (s + 0.1)*((s-0.2)^2 + 4) ) ;
118+ h26b = ((s-0.1)^2 + 1)/( ...
119+ (s - 0.3)*((s-0.2)^2 + 4) );
120+ """
121+ self .yazdan = {
122+ 'example21' :
123+ 0.002 * (s + 0.02 )* (s + 0.05 )* (s + 5 )* (s + 10 )/ (
124+ (s - 0.0005 )* (s + 0.0001 )* (s + 0.01 )* (s + 0.2 )* (s + 1 )* (s + 100 )** 2 ),
125+
126+ 'example23' :
127+ ((s + 0.1 )** 2 + 1 )* (s - 0.1 )/ (
128+ ((s + 0.1 )** 2 + 4 )* (s + 1 ) ),
129+
130+ 'example25a' :
131+ s / (s ** 2 + 2 * s + 2 )** 4 ,
132+
133+ 'example26a' :
134+ ((s - 0.1 )** 2 + 1 )/ (
135+ (s + 0.1 )* ((s - 0.2 )** 2 + 4 ) ),
136+
137+ 'example26b' : ((s - 0.1 )** 2 + 1 )/ (
138+ (s - 0.3 )* ((s - 0.2 )** 2 + 4 ) )
139+ }
140+ self .yazdan ['example24' ] = self .yazdan ['example21' ]* 20000
141+ self .yazdan ['example25b' ] = self .yazdan ['example25a' ]* 100
142+ self .yazdan ['example22' ] = self .yazdan ['example21' ]* (s ** 2 - 2 * s + 401 )
143+ self .ymargin = (
144+ dict (sys = 'example21' , K = 1.0 , digits = 2 , result = (
145+ 0.0100 , - 14.5640 , 0 , 0.0022 )),
146+ dict (sys = 'example21' , K = 1000.0 , digits = 2 , result = (
147+ 0.1793 , 22.5215 , 0.0243 , 0.0630 )),
148+ dict (sys = 'example21' , K = 5000.0 , digits = 4 , result = (
149+ 4.5596 , 21.2101 , 0.4385 , 0.1868 )),
150+ )
151+
152+ self .yallmargin = (
153+ dict (sys = 'example21' , K = 1.0 , result = (
154+ [0.01 , 179.2931 , 2.2798e+4 , 1.5946e+07 , 7.2477e+08 ],
155+ [0 , 0.0243 , 0.4385 , 6.8640 , 84.9323 ],
156+ [- 14.5640 ],
157+ [0.0022 ]))
158+ )
159+
160+
46161 def test_stability_margins (self ):
47162 omega = np .logspace (- 2 , 2 , 2000 )
48163 for sys ,rgm ,rwgm ,rpm ,rwpm in self .tsys :
@@ -51,22 +166,21 @@ def test_stability_margins(self):
51166 gm , pm , sm , wg , wp , ws = out
52167 outf = np .array (stability_margins (FRD (sys , omega )))
53168 print (out ,'\n ' , outf )
54- print (out != np .array (None ))
55- np .testing .assert_array_almost_equal (
56- out [out != np .array (None )],
57- outf [outf != np .array (None )], 2 )
58-
169+ #print(out != np.array(None))
170+ assert_array_almost_equal (
171+ out , outf , 2 )
59172 # final one with fixed values
60- np . testing . assert_array_almost_equal (
173+ assert_array_almost_equal (
61174 [gm , pm , sm , wg , wp , ws ],
62175 self .stability_margins4 , 3 )
63176
64177 def test_margin (self ):
65178 gm , pm , wg , wp = margin (self .sys4 )
66- np . testing . assert_array_almost_equal (
179+ assert_array_almost_equal (
67180 [gm , pm , wg , wp ],
68181 self .stability_margins4 [:2 ] + self .stability_margins4 [3 :5 ], 3 )
69182
183+
70184 def test_stability_margins_all (self ):
71185 for sys ,rgm ,rwgm ,rpm ,rwpm in self .tsys :
72186 out = stability_margins (sys , returnall = True )
@@ -75,25 +189,25 @@ def test_stability_margins_all(self):
75189 for res ,comp in zip (out , (rgm ,rpm ,[],rwgm ,rwpm ,[])):
76190 if comp :
77191 print (res , '\n ' , comp )
78- np . testing . assert_array_almost_equal (
192+ assert_array_almost_equal (
79193 res , comp , 2 )
80194
81195 def test_phase_crossover_frequencies (self ):
82196 omega , gain = phase_crossover_frequencies (self .sys2 )
83- np . testing . assert_array_almost_equal (omega , [1.73205 , 0. ])
84- np . testing . assert_array_almost_equal (gain , [- 0.5 , 0.25 ])
197+ assert_array_almost_equal (omega , [1.73205 , 0. ])
198+ assert_array_almost_equal (gain , [- 0.5 , 0.25 ])
85199
86200 tf = TransferFunction ([1 ],[1 ,1 ])
87201 omega , gain = phase_crossover_frequencies (tf )
88- np . testing . assert_array_almost_equal (omega , [0. ])
89- np . testing . assert_array_almost_equal (gain , [1. ])
202+ assert_array_almost_equal (omega , [0. ])
203+ assert_array_almost_equal (gain , [1. ])
90204
91205 # testing MIMO, only (0,0) element is considered
92206 tf = TransferFunction ([[[1 ],[2 ]],[[3 ],[4 ]]],
93207 [[[1 , 2 , 3 , 4 ],[1 ,1 ]],[[1 ,1 ],[1 ,1 ]]])
94208 omega , gain = phase_crossover_frequencies (tf )
95- np . testing . assert_array_almost_equal (omega , [1.73205081 , 0. ])
96- np . testing . assert_array_almost_equal (gain , [- 0.5 , 0.25 ])
209+ assert_array_almost_equal (omega , [1.73205081 , 0. ])
210+ assert_array_almost_equal (gain , [- 0.5 , 0.25 ])
97211
98212 def test_mag_phase_omega (self ):
99213 # test for bug reported in gh-58
@@ -106,7 +220,7 @@ def test_mag_phase_omega(self):
106220 ind = [0 ,1 ,3 ,4 ] # indices of gm, pm, wg, wp -- ignore sm
107221 marg1 = np .array (out )[ind ]
108222 marg2 = np .array (out2 )[ind ]
109- np . testing . assert_array_almost_equal (marg1 , marg2 , 4 )
223+ assert_array_almost_equal (marg1 , marg2 , 4 )
110224
111225 def test_frd (self ):
112226 f = np .array ([0.005 , 0.010 , 0.020 , 0.030 , 0.040 ,
@@ -143,7 +257,7 @@ def test_frd(self):
143257 C = K * (1 + 1.9 * s )
144258 TFopen = fresp * C * G
145259 gm , pm , sm , wg , wp , ws = stability_margins (TFopen )
146- np . testing . assert_array_almost_equal (
260+ assert_array_almost_equal (
147261 [pm ], [44.55 ], 2 )
148262
149263 def test_nocross (self ):
@@ -153,17 +267,48 @@ def test_nocross(self):
153267 h2 = 3 * (10 + s )/ (2 + s )
154268 h3 = 0.01 * (10 - s )/ (2 + s )/ (1 + s )
155269 gm , pm , wm , wg , wp , ws = stability_margins (h1 )
156- self .assertEqual (gm , None )
157- self .assertEqual (wg , None )
270+ assert_array_almost_equal (
271+ [gm , pm , wg , wp ],
272+ [float ('Inf' ), float ('Inf' ), float ('NaN' ), float ('NaN' )])
158273 gm , pm , wm , wg , wp , ws = stability_margins (h2 )
159- self .assertEqual (pm , None )
274+ self .assertEqual (pm , float ( 'Inf' ) )
160275 gm , pm , wm , wg , wp , ws = stability_margins (h3 )
161- self .assertEqual ( pm , None )
276+ self .assertTrue ( np . isnan ( wp ) )
162277 omega = np .logspace (- 2 ,2 , 100 )
163278 out1b = stability_margins (FRD (h1 , omega ))
164279 out2b = stability_margins (FRD (h2 , omega ))
165280 out3b = stability_margins (FRD (h3 , omega ))
166281
282+ def test_zmore_margin (self ):
283+ print ("""
284+ warning, Matlab gives different values (0 and 0) for gain
285+ margin of the following system:
286+ {type2!s}
287+ python-control gives inf
288+ difficult to argue which is right? Special case or different
289+ approach?
290+
291+ edge cases, like
292+ {type0!s}
293+ which approaches a gain of 1 for w -> 0, are also not identically
294+ indicated, Matlab gives phase margin -180, at w = 0. for higher or
295+ lower gains, results match
296+ """ .format (** self .types ))
297+
298+ sdict = self .tmargin [0 ]
299+ for test in self .tmargin [1 :]:
300+ res = margin (sdict [test ['sys' ]]* test ['K' ])
301+ print ("more margin {}\n " .format (sdict [test ['sys' ]]),
302+ res , '\n ' , test ['result' ])
303+ assert_array_almost_equal (
304+ res , test ['result' ], test ['digits' ])
305+ sdict = self .yazdan
306+ for test in self .ymargin :
307+ res = margin (sdict [test ['sys' ]]* test ['K' ])
308+ print ("more margin {}\n " .format (sdict [test ['sys' ]]),
309+ res , '\n ' , test ['result' ])
310+ assert_array_almost_equal (
311+ res , test ['result' ], test ['digits' ])
167312
168313def test_suite ():
169314 return unittest .TestLoader ().loadTestsFromTestCase (TestMargin )
0 commit comments