2626
2727class TSys :
2828 """Struct of test system"""
29- def __init__ (self , sys = None ):
29+ def __init__ (self , sys = None , call_kwargs = None ):
3030 self .sys = sys
31+ self .kwargs = call_kwargs if call_kwargs else {}
3132
3233 def __repr__ (self ):
3334 """Show system when debugging"""
@@ -210,39 +211,42 @@ def siso_tf_type1(self):
210211
211212 @pytest .fixture
212213 def siso_tf_kpos (self ):
213- # SISO under shoot response and positive final value G(s)=(-s+1)/(s²+s+1)
214+ # SISO under shoot response and positive final value
215+ # G(s)=(-s+1)/(s²+s+1)
214216 T = TSys (TransferFunction ([- 1 , 1 ], [1 , 1 , 1 ]))
215217 T .step_info = {
216218 'RiseTime' : 1.242 ,
217219 'SettlingTime' : 9.110 ,
218- 'SettlingMin' : 0.950 ,
220+ 'SettlingMin' : 0.90 ,
219221 'SettlingMax' : 1.208 ,
220222 'Overshoot' : 20.840 ,
221- 'Undershoot' : 27.840 ,
223+ 'Undershoot' : 28.0 ,
222224 'Peak' : 1.208 ,
223225 'PeakTime' : 4.282 ,
224226 'SteadyStateValue' : 1.0 }
225227 return T
226228
227229 @pytest .fixture
228230 def siso_tf_kneg (self ):
229- # SISO under shoot response and negative final value k=-1 G(s)=-(-s+1)/(s²+s+1)
231+ # SISO under shoot response and negative final value
232+ # k=-1 G(s)=-(-s+1)/(s²+s+1)
230233 T = TSys (TransferFunction ([1 , - 1 ], [1 , 1 , 1 ]))
231234 T .step_info = {
232235 'RiseTime' : 1.242 ,
233236 'SettlingTime' : 9.110 ,
234237 'SettlingMin' : - 1.208 ,
235- 'SettlingMax' : - 0.950 ,
238+ 'SettlingMax' : - 0.90 ,
236239 'Overshoot' : 20.840 ,
237- 'Undershoot' : 27.840 ,
240+ 'Undershoot' : 28.0 ,
238241 'Peak' : 1.208 ,
239242 'PeakTime' : 4.282 ,
240243 'SteadyStateValue' : - 1.0 }
241244 return T
242245
243246 @pytest .fixture
244- def tf1_matlab_help (self ):
245- # example from matlab online help https://www.mathworks.com/help/control/ref/stepinfo.html
247+ def siso_tf_step_matlab (self ):
248+ # example from matlab online help
249+ # https://www.mathworks.com/help/control/ref/stepinfo.html
246250 T = TSys (TransferFunction ([1 , 5 , 5 ], [1 , 1.65 , 5 , 6.5 , 2 ]))
247251 T .step_info = {
248252 'RiseTime' : 3.8456 ,
@@ -257,37 +261,82 @@ def tf1_matlab_help(self):
257261 return T
258262
259263 @pytest .fixture
260- def ss2_matlab_help (self ):
261- A = [[0.68 , - 0.34 ], [0.34 , 0.68 ]]
262- B = [[0.18 ], [0.04 ]]
263- C = [- 1.12 , - 1.10 ]
264- D = [0.06 ]
264+ def mimo_ss_step_matlab (self ):
265+ A = [[0.68 , - 0.34 ],
266+ [0.34 , 0.68 ]]
267+ B = [[0.18 , - 0.05 ],
268+ [0.04 , 0.11 ]]
269+ C = [[0 , - 1.53 ],
270+ [- 1.12 , - 1.10 ]]
271+ D = [[0 , 0 ],
272+ [0.06 , - 0.37 ]]
265273 T = TSys (StateSpace (A , B , C , D , 0.2 ))
266- T .step_info = {
267- 'RiseTime' : 0.4000 ,
268- 'SettlingTime' : 2.8000 ,
269- 'SettlingMin' : - 0.6724 ,
270- 'SettlingMax' : - 0.5188 ,
271- 'Overshoot' : 24.6476 ,
272- 'Undershoot' : 11.1224 ,
273- 'Peak' : 0.6724 ,
274- 'PeakTime' : 1 ,
275- 'SteadyStateValue' : - 0.5394 }
274+ T .kwargs ['step_info' ] = {'T' : 4.6 }
275+ T .step_info = [[{'RiseTime' : 0.6000 ,
276+ 'SettlingTime' : 3.0000 ,
277+ 'SettlingMin' : - 0.5999 ,
278+ 'SettlingMax' : - 0.4689 ,
279+ 'Overshoot' : 15.5072 ,
280+ 'Undershoot' : 0. ,
281+ 'Peak' : 0.5999 ,
282+ 'PeakTime' : 1.4000 ,
283+ 'SteadyStateValue' : - 0.5193 },
284+ {'RiseTime' : 0. ,
285+ 'SettlingTime' : 3.6000 ,
286+ 'SettlingMin' : - 0.2797 ,
287+ 'SettlingMax' : - 0.1043 ,
288+ 'Overshoot' : 118.9918 ,
289+ 'Undershoot' : 0 ,
290+ 'Peak' : 0.2797 ,
291+ 'PeakTime' : .6000 ,
292+ 'SteadyStateValue' : - 0.1277 }],
293+ [{'RiseTime' : 0.4000 ,
294+ 'SettlingTime' : 2.8000 ,
295+ 'SettlingMin' : - 0.6724 ,
296+ 'SettlingMax' : - 0.5188 ,
297+ 'Overshoot' : 24.6476 ,
298+ 'Undershoot' : 11.1224 ,
299+ 'Peak' : 0.6724 ,
300+ 'PeakTime' : 1 ,
301+ 'SteadyStateValue' : - 0.5394 },
302+ {'RiseTime' : 0.0000 , # (*)
303+ 'SettlingTime' : 3.4000 ,
304+ 'SettlingMin' : - 0.1034 ,
305+ 'SettlingMax' : - 0.1485 ,
306+ 'Overshoot' : 132.0170 ,
307+ 'Undershoot' : 79.222 , # 0. in MATLAB
308+ 'Peak' : 0.4350 ,
309+ 'PeakTime' : .2 ,
310+ 'SteadyStateValue' : - 0.1875 }]]
311+ # (*): MATLAB gives 0.4 here, but it is unclear what
312+ # 10% and 90% of the steady state response mean, when
313+ # the step for this channel does not start a 0 for
314+ # 0 initial conditions
276315 return T
277316
278317 @pytest .fixture
279- def mimo_tf_step (self , tf1_matlab_help ,
280- siso_tf_kpos ,
281- siso_tf_kneg ,
282- siso_tf_type1 ):
283- Ta = [[tf1_matlab_help , tf1_matlab_help , siso_tf_kpos ],
284- [siso_tf_kneg , siso_tf_type1 , siso_tf_type1 ]]
318+ def siso_ss_step_matlab (self , mimo_ss_step_matlab ):
319+ T = copy (mimo_ss_step_matlab )
320+ T .sys = T .sys [1 , 0 ]
321+ T .step_info = T .step_info [1 ][0 ]
322+ return T
323+
324+ @pytest .fixture
325+ def mimo_tf_step_info (self ,
326+ siso_tf_kpos , siso_tf_kneg ,
327+ siso_tf_step_matlab ):
328+ Ta = [[siso_tf_kpos , siso_tf_kneg , siso_tf_step_matlab ],
329+ [siso_tf_step_matlab , siso_tf_kpos , siso_tf_kneg ]]
285330 T = TSys (TransferFunction (
286331 [[Ti .sys .num [0 ][0 ] for Ti in Tr ] for Tr in Ta ],
287332 [[Ti .sys .den [0 ][0 ] for Ti in Tr ] for Tr in Ta ]))
288333 T .step_info = [[Ti .step_info for Ti in Tr ] for Tr in Ta ]
334+ # enforce enough sample points for all channels (they have different
335+ # characteristics)
336+ T .kwargs ['step_info' ] = {'T_num' : 2000 }
289337 return T
290338
339+
291340 @pytest .fixture
292341 def tsystem (self ,
293342 request ,
@@ -297,8 +346,9 @@ def tsystem(self,
297346 siso_dss1 , siso_dss2 ,
298347 mimo_dss1 , mimo_dss2 , mimo_dtf1 ,
299348 pole_cancellation , no_pole_cancellation , siso_tf_type1 ,
300- siso_tf_kpos , siso_tf_kneg , tf1_matlab_help ,
301- ss2_matlab_help , mimo_tf_step ):
349+ siso_tf_kpos , siso_tf_kneg ,
350+ siso_tf_step_matlab , siso_ss_step_matlab ,
351+ mimo_ss_step_matlab , mimo_tf_step_info ):
302352 systems = {"siso_ss1" : siso_ss1 ,
303353 "siso_ss2" : siso_ss2 ,
304354 "siso_tf1" : siso_tf1 ,
@@ -319,9 +369,10 @@ def tsystem(self,
319369 "siso_tf_type1" : siso_tf_type1 ,
320370 "siso_tf_kpos" : siso_tf_kpos ,
321371 "siso_tf_kneg" : siso_tf_kneg ,
322- "tf1_matlab_help" : tf1_matlab_help ,
323- "ss2_matlab_help" : ss2_matlab_help ,
324- "mimo_tf_step" : mimo_tf_step ,
372+ "siso_tf_step_matlab" : siso_tf_step_matlab ,
373+ "siso_ss_step_matlab" : siso_ss_step_matlab ,
374+ "mimo_ss_step_matlab" : mimo_ss_step_matlab ,
375+ "mimo_tf_step" : mimo_tf_step_info ,
325376 }
326377 return systems [request .param ]
327378
@@ -375,60 +426,71 @@ def test_step_nostates(self, dt):
375426 t , y = step_response (sys )
376427 np .testing .assert_array_equal (y , np .ones (len (t )))
377428
378- # tolerance for all parameters could be wrong for some systems
379- # discrete systems time parameters tolerance could be +/-dt
380- @pytest .mark .parametrize (
381- "tsystem, tolerance" ,
382- [("tf1_matlab_help" , 2e-2 ),
383- ("ss2_matlab_help" , .2 ),
384- ("siso_tf_kpos" , 2e-2 ),
385- ("siso_tf_kneg" , 2e-2 ),
386- ("siso_tf_type1" , 2e-2 )],
387- indirect = ["tsystem" ])
388- def test_step_info (self , tsystem , tolerance ):
389- """Test step info for SISO systems"""
390- info = step_info (tsystem .sys )
429+ def assert_step_info_match (self , sys , info , info_ref ):
430+ """Assert reasonable step_info accuracy"""
391431
392- info_true_sorted = sorted (tsystem .step_info .keys ())
393- info_sorted = sorted (info .keys ())
394- assert info_sorted == info_true_sorted
432+ if sys .isdtime (strict = True ):
433+ dt = sys .dt
434+ else :
435+ _ , dt = _ideal_tfinal_and_dt (sys , is_step = True )
395436
396- for k in info :
397- np .testing .assert_allclose (info [k ], tsystem .step_info [k ],
398- rtol = tolerance ,
437+ for k in ['RiseTime' , 'SettlingTime' , 'PeakTime' ]:
438+ np .testing .assert_allclose (info [k ], info_ref [k ], atol = dt ,
399439 err_msg = f"{ k } does not match" )
440+ for k in ['Overshoot' , 'Undershoot' , 'Peak' , 'SteadyStateValue' ]:
441+ np .testing .assert_allclose (info [k ], info_ref [k ], rtol = 5e-3 ,
442+ err_msg = f"{ k } does not match" )
443+
444+ # steep gradient right after RiseTime
445+ absrefinf = np .abs (info_ref ['SteadyStateValue' ])
446+ if info_ref ['RiseTime' ] > 0 :
447+ y_next_sample_max = 0.8 * absrefinf / info_ref ['RiseTime' ]* dt
448+ else :
449+ y_next_sample_max = 0
450+ for k in ['SettlingMin' , 'SettlingMax' ]:
451+ if (np .abs (info_ref [k ]) - 0.9 * absrefinf ) > y_next_sample_max :
452+ # local min/max peak well after signal has risen
453+ np .testing .assert_allclose (info [k ], info_ref [k ], rtol = 1e-3 )
454+
455+ @pytest .mark .parametrize (
456+ "tsystem" ,
457+ ["siso_tf_step_matlab" ,
458+ "siso_ss_step_matlab" ,
459+ "siso_tf_kpos" ,
460+ "siso_tf_kneg" ,
461+ "siso_tf_type1" ],
462+ indirect = ["tsystem" ])
463+ def test_step_info (self , tsystem ):
464+ """Test step info for SISO systems"""
465+ step_info_kwargs = tsystem .kwargs .get ('step_info' ,{})
466+ info = step_info (tsystem .sys , ** step_info_kwargs )
467+ self .assert_step_info_match (tsystem .sys , info , tsystem .step_info )
400468
401469 @pytest .mark .parametrize (
402- "tsystem, tolerance" ,
403- [('mimo_tf_step' , 2e-2 )],
470+ "tsystem" ,
471+ ['mimo_ss_step_matlab' ,
472+ 'mimo_tf_step' ],
404473 indirect = ["tsystem" ])
405- def test_step_info_mimo (self , tsystem , tolerance ):
474+ def test_step_info_mimo (self , tsystem ):
406475 """Test step info for MIMO systems"""
407- info_dict = step_info (tsystem .sys )
408- from pprint import pprint
409- pprint (info_dict )
476+ step_info_kwargs = tsystem .kwargs .get ('step_info' ,{})
477+ info_dict = step_info (tsystem .sys , ** step_info_kwargs )
410478 for i , row in enumerate (info_dict ):
411479 for j , info in enumerate (row ):
412480 for k in info :
413- np .testing .assert_allclose (
414- info [k ], tsystem .step_info [i ][j ][k ],
415- rtol = tolerance ,
416- err_msg = f"{ k } for input { j } to output { i } "
417- "does not match" )
481+ self .assert_step_info_match (tsystem .sys ,
482+ info , tsystem .step_info [i ][j ])
418483
419484 def test_step_pole_cancellation (self , pole_cancellation ,
420485 no_pole_cancellation ):
421486 # confirm that pole-zero cancellation doesn't perturb results
422487 # https://github.com/python-control/python-control/issues/440
423488 step_info_no_cancellation = step_info (no_pole_cancellation )
424489 step_info_cancellation = step_info (pole_cancellation )
425- for key in step_info_no_cancellation :
426- if key == 'Overshoot' :
427- # skip this test because these systems have no overshoot
428- # => very sensitive to parameters
429- continue
430- np .testing .assert_allclose (step_info_no_cancellation [key ],
431- step_info_cancellation [key ], rtol = 1e-4 )
490+ self .assert_step_info_match (no_pole_cancellation ,
491+ step_info_no_cancellation ,
492+ step_info_cancellation )
493+
432494
433495 @pytest .mark .parametrize (
434496 "tsystem, kwargs" ,
0 commit comments