@@ -373,6 +373,44 @@ def test_stability_margins_discrete(cnum, cden, dt,
373373 out = stability_margins (tf )
374374 assert_allclose (out , ref , rtol = rtol )
375375
376+
377+ @pytest .mark .parametrize ("method" , ["poly" , "best" ])
378+ def test_discrete_gain_crossovers_on_unit_circle (method ):
379+ n = 17
380+ num = np .full (n , 1 / n )
381+ loop = tf (num , np .r_ [1 , - num ], dt = 1 )
382+
383+ _ , pm , _ , _ , wgc , _ = stability_margins (loop , method = method , returnall = True )
384+
385+ assert_allclose (wgc , [0.1064535 ], rtol = 1e-6 )
386+ assert_allclose (pm , [70.2119 ], rtol = 1e-6 )
387+ assert_allclose (np .abs (loop (np .exp (1j * wgc ))), 1 )
388+
389+
390+ @pytest .mark .parametrize ("method" , ["poly" , "best" ])
391+ def test_discrete_gain_below_unity (method ):
392+ # |z**8 - 0.5| >= 0.5 on the unit circle, so |L(z)| <= 0.5.
393+ loop = tf ([0.25 ], np .r_ [1 , np .zeros (7 ), - 0.5 ], dt = 1 )
394+
395+ _ , pm , _ , _ , wgc , _ = stability_margins (loop , method = method , returnall = True )
396+
397+ assert len (wgc ) == len (pm ) == 0
398+
399+
400+ def test_discrete_gain_crossover_refinement ():
401+ # For L(z) = k * ((z + 1) / (z - a))**n, solve |L(z)| = 1
402+ # directly in cos(theta) to check the numerically sensitive root.
403+ n , a , k , dt = 10 , 0.2 , 1000 , 0.2
404+ loop = tf (k * np .poly ([- 1. ] * n ), np .poly ([a ] * n ), dt = dt )
405+ power = k ** (2 / n )
406+ theta = np .arccos ((1 + a * a - 2 * power ) / (2 * (power + a )))
407+
408+ _ , _ , _ , _ , wgc , _ = stability_margins (loop , method = "poly" , returnall = True )
409+
410+ assert_allclose (wgc * dt , [theta ], atol = 1e-8 , rtol = 0 )
411+ assert_allclose (np .abs (loop (np .exp (1j * wgc * dt ))), 1 , rtol = 1e-8 )
412+
413+
376414def test_siso_disk_margin ():
377415 # Frequencies of interest
378416 omega = np .logspace (- 1 , 2 , 1001 )
0 commit comments