@@ -620,12 +620,14 @@ func TestAllMargin_ThirdOrderPythonControl(t *testing.T) {
620620 }
621621}
622622
623- // python-control: tf([1], [1,2,3,4]) discretized at dt=0.01
623+ // python-control: tf([2],[1,3,2,0]) sampled at dt=0.01
624+ // Expected: gm=2.955761 (9.41 dB), pm=32.398, wg=1.403725, wp=0.749367
624625func TestAllMargin_Discrete (t * testing.T ) {
626+ // G(s) = 2/(s^3+3s^2+2s) = 2/(s(s+1)(s+2))
625627 sys , err := NewFromSlices (3 , 1 , 1 ,
626- []float64 {0 , 1 , 0 , 0 , 0 , 1 , - 4 , - 3 , - 2 },
628+ []float64 {0 , 1 , 0 , 0 , 0 , 1 , 0 , - 2 , - 3 },
627629 []float64 {0 , 0 , 1 },
628- []float64 {1 , 0 , 0 },
630+ []float64 {2 , 0 , 0 },
629631 []float64 {0 }, 0 )
630632 if err != nil {
631633 t .Fatal (err )
@@ -636,16 +638,24 @@ func TestAllMargin_Discrete(t *testing.T) {
636638 t .Fatal (err )
637639 }
638640
639- all , err := AllMargin (dsys )
641+ m , err := Margin (dsys )
640642 if err != nil {
641643 t .Fatal (err )
642644 }
643645
644- if len (all .PhaseCrossFreqs ) == 0 {
645- t .Fatal ("expected phase crossover" )
646+ // python-control: gm ≈ 9.41 dB (linear 2.9558)
647+ wantGM := 20 * math .Log10 (2.9558 )
648+ if math .Abs (m .GainMargin - wantGM ) > 0.5 {
649+ t .Errorf ("GM = %v dB, want ~%v dB" , m .GainMargin , wantGM )
646650 }
647- if len (all .GainCrossFreqs ) > 0 {
648- t .Log ("gain crossover freqs:" , all .GainCrossFreqs )
651+ if math .Abs (m .PhaseMargin - 32.4 ) > 2 {
652+ t .Errorf ("PM = %v deg, want ~32.4 deg" , m .PhaseMargin )
653+ }
654+ if math .Abs (m .WgFreq - 0.749 ) > 0.05 {
655+ t .Errorf ("WgFreq = %v, want ~0.749" , m .WgFreq )
656+ }
657+ if math .Abs (m .WpFreq - 1.404 ) > 0.05 {
658+ t .Errorf ("WpFreq = %v, want ~1.404" , m .WpFreq )
649659 }
650660}
651661
@@ -670,17 +680,23 @@ func TestAllMargin_MultipleGainCrossovers(t *testing.T) {
670680 t .Fatal (err )
671681 }
672682
673- // python-control: gm=4.0 (12.04 dB), pm=67.6 deg, wg=1.732 , wp=0.766
683+ // python-control: gm=4.0 (12.04 dB), pm=67.6058 deg, wg=1.7322 , wp=0.7663
674684 m , err := Margin (sys )
675685 if err != nil {
676686 t .Fatal (err )
677687 }
678688 wantGM := 20 * math .Log10 (4.0 )
679- if math .Abs (m .GainMargin - wantGM ) > 0.5 {
689+ if math .Abs (m .GainMargin - wantGM ) > 0.3 {
680690 t .Errorf ("GM = %v dB, want ~%v dB" , m .GainMargin , wantGM )
681691 }
682- if math .Abs (m .PhaseMargin - 67.6 ) > 2 {
683- t .Errorf ("PM = %v deg, want ~67.6 deg" , m .PhaseMargin )
692+ if math .Abs (m .PhaseMargin - 67.6058 ) > 1.0 {
693+ t .Errorf ("PM = %v deg, want ~67.6058 deg" , m .PhaseMargin )
694+ }
695+ if math .Abs (m .WgFreq - 0.7663 ) > 0.03 {
696+ t .Errorf ("WgFreq = %v, want ~0.7663" , m .WgFreq )
697+ }
698+ if math .Abs (m .WpFreq - 1.7322 ) > 0.05 {
699+ t .Errorf ("WpFreq = %v, want ~1.7322" , m .WpFreq )
684700 }
685701 _ = all
686702}
@@ -703,10 +719,14 @@ func TestMargin_NonMinimumPhase(t *testing.T) {
703719 t .Fatal (err )
704720 }
705721
722+ // python-control: gm=300 (49.54 dB), wg=5.6569
706723 wantGM := 20 * math .Log10 (300.0 )
707- if math .Abs (m .GainMargin - wantGM ) > 1 {
724+ if math .Abs (m .GainMargin - wantGM ) > 0.5 {
708725 t .Errorf ("GM = %v dB, want ~%v dB" , m .GainMargin , wantGM )
709726 }
727+ if math .Abs (m .WpFreq - 5.6569 ) > 0.1 {
728+ t .Errorf ("WpFreq = %v, want ~5.6569" , m .WpFreq )
729+ }
710730}
711731
712732// AllMargin for system with no crossings
@@ -733,6 +753,8 @@ func TestAllMargin_NoCrossings(t *testing.T) {
733753}
734754
735755// Bandwidth with MIMO system (exercises Sigma path)
756+ // Diagonal MIMO: diag(1/(s+1), 1/(s+2)) → max singular value = 1/(s+1)
757+ // -3dB bandwidth of 1/(s+1) is w=1
736758func TestBandwidth_MIMO (t * testing.T ) {
737759 sys , err := NewFromSlices (2 , 2 , 2 ,
738760 []float64 {- 1 , 0 , 0 , - 2 },
@@ -747,11 +769,8 @@ func TestBandwidth_MIMO(t *testing.T) {
747769 if err != nil {
748770 t .Fatal (err )
749771 }
750- if bw <= 0 {
751- t .Errorf ("MIMO bandwidth = %v, want > 0" , bw )
752- }
753- if bw > 3 {
754- t .Errorf ("MIMO bandwidth = %v, want ≤ 3" , bw )
772+ if math .Abs (bw - 1.0 ) > 0.15 {
773+ t .Errorf ("MIMO bandwidth = %v, want ~1.0 (from 1/(s+1) channel)" , bw )
755774 }
756775}
757776
@@ -791,14 +810,20 @@ func TestDiskMargin_PythonControl(t *testing.T) {
791810 t .Fatal (err )
792811 }
793812
794- if dm .Alpha < 0.3 || dm .Alpha > 0.6 {
795- t .Errorf ("Alpha = %v, want in [0.3, 0.6]" , dm .Alpha )
813+ // python-control: DM=0.46, DGM=4.05 dB, DPM=25.8 deg, peak at ~1.94 rad/s
814+ // Our implementation computes α differently (1/Ms vs full disk), so we allow
815+ // wider tolerance but still verify the values are in the correct ballpark
816+ if math .Abs (dm .Alpha - 0.40 ) > 0.10 {
817+ t .Errorf ("Alpha = %v, want ~0.40 (python-control: 0.46)" , dm .Alpha )
818+ }
819+ if math .Abs (dm .GainMarginDB [1 ]- 3.5 ) > 1.0 {
820+ t .Errorf ("DGM_high = %v dB, want ~3.5 (python-control: 4.05)" , dm .GainMarginDB [1 ])
796821 }
797- if dm .GainMarginDB [ 1 ] < 2.5 || dm . GainMarginDB [ 1 ] > 5.5 {
798- t .Errorf ("DGM_high = %v dB , want in [2.5, 5.5] " , dm .GainMarginDB [ 1 ] )
822+ if math . Abs ( dm .PhaseMargin - 23.0 ) > 4.0 {
823+ t .Errorf ("DPM = %v deg , want ~23 (python-control: 25.8) " , dm .PhaseMargin )
799824 }
800- if dm .PhaseMargin < 18 || dm .PhaseMargin > 30 {
801- t .Errorf ("DPM = %v deg , want in [18, 30] " , dm .PhaseMargin )
825+ if dm .PeakFreq < 1.0 || dm .PeakFreq > 3.0 {
826+ t .Errorf ("PeakFreq = %v, want ~1.94 " , dm .PeakFreq )
802827 }
803828}
804829
0 commit comments