You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

110 line
3.2KB

  1. package classifier
  2. import "testing"
  3. func TestRuleClassifyWFM(t *testing.T) {
  4. sampleRate := 1_000_000
  5. fftSize := 1024
  6. spectrum := make([]float64, fftSize)
  7. for i := range spectrum {
  8. spectrum[i] = -100
  9. }
  10. start := 100
  11. end := 350
  12. for i := start; i <= end; i++ {
  13. spectrum[i] = -10
  14. }
  15. cls := Classify(SignalInput{FirstBin: start, LastBin: end, CenterHz: 100e6, SNRDb: 30}, spectrum, sampleRate, fftSize, nil, ModeCombined)
  16. if cls == nil || cls.ModType != ClassWFM {
  17. t.Fatalf("expected WFM, got %+v", cls)
  18. }
  19. }
  20. func TestSoftmaxConfidence(t *testing.T) {
  21. scores1 := map[SignalClass]float64{ClassNFM: 2.0, ClassAM: 0.3, ClassNoise: 0.1}
  22. c1 := softmaxConfidence(scores1, ClassNFM)
  23. if c1 < 0.7 {
  24. t.Fatalf("clear winner should have high confidence: %f", c1)
  25. }
  26. scores2 := map[SignalClass]float64{ClassSSBUSB: 1.0, ClassSSBLSB: 0.9, ClassAM: 0.8}
  27. c2 := softmaxConfidence(scores2, ClassSSBUSB)
  28. if c2 > 0.5 {
  29. t.Fatalf("ambiguous should have low confidence: %f", c2)
  30. }
  31. c3 := softmaxConfidence(map[SignalClass]float64{}, ClassNFM)
  32. if c3 != 0.1 {
  33. t.Fatalf("empty should return 0.1: %f", c3)
  34. }
  35. }
  36. func TestClassifierProfiles(t *testing.T) {
  37. tests := []struct {
  38. name string
  39. feat Features
  40. centerHz float64
  41. snrDb float64
  42. wantBest SignalClass
  43. }{
  44. {
  45. name: "FM Broadcast 100 MHz",
  46. feat: Features{BW3dB: 120000, SpectralFlat: 0.3, PeakToAvg: 1.5, Symmetry: 0.05,
  47. RolloffLeft: 20, RolloffRight: 22, EnvVariance: 0.01, InstFreqStd: 0.8},
  48. centerHz: 100.0e6, snrDb: 40,
  49. wantBest: ClassWFM,
  50. },
  51. {
  52. name: "FT8 auf 7.074 MHz",
  53. feat: Features{BW3dB: 2500, SpectralFlat: 0.6, PeakToAvg: 1.8, Symmetry: 0.1,
  54. EnvVariance: 0.03, InstFreqStd: 0.4},
  55. centerHz: 7.074e6, snrDb: 15,
  56. wantBest: ClassFT8,
  57. },
  58. {
  59. name: "USB Voice 14.230 MHz",
  60. feat: Features{BW3dB: 2800, SpectralFlat: 0.35, PeakToAvg: 3.5, Symmetry: 0.4,
  61. RolloffLeft: 5, RolloffRight: 18, EnvVariance: 0.25, InstFreqStd: 0.6},
  62. centerHz: 14.230e6, snrDb: 25,
  63. wantBest: ClassSSBUSB,
  64. },
  65. {
  66. name: "DMR auf 438 MHz",
  67. feat: Features{BW3dB: 12500, SpectralFlat: 0.7, PeakToAvg: 1.2, Symmetry: 0.02,
  68. RolloffLeft: 25, RolloffRight: 24, EnvVariance: 0.01, InstFreqStd: 0.35},
  69. centerHz: 438.5e6, snrDb: 20,
  70. wantBest: ClassDMR,
  71. },
  72. {
  73. name: "Airband AM 121.5 MHz",
  74. feat: Features{BW3dB: 7000, SpectralFlat: 0.25, PeakToAvg: 4.0, Symmetry: 0.05,
  75. RolloffLeft: 15, RolloffRight: 16, EnvVariance: 0.2, InstFreqStd: 0.7},
  76. centerHz: 121.5e6, snrDb: 30,
  77. wantBest: ClassAM,
  78. },
  79. {
  80. name: "CW auf 7.020 MHz",
  81. feat: Features{BW3dB: 80, SpectralFlat: 0.15, PeakToAvg: 8.0, Symmetry: 0.0,
  82. EnvVariance: 0.9, InstFreqStd: 0.05, CrestFactor: 3.5},
  83. centerHz: 7.020e6, snrDb: 20,
  84. wantBest: ClassCW,
  85. },
  86. }
  87. for _, tt := range tests {
  88. t.Run(tt.name, func(t *testing.T) {
  89. cls := RuleClassify(tt.feat, tt.centerHz, tt.snrDb)
  90. if cls.ModType != tt.wantBest {
  91. t.Errorf("got %s (conf=%.2f), want %s. Scores: %v", cls.ModType, cls.Confidence, tt.wantBest, cls.Scores)
  92. }
  93. })
  94. }
  95. }
  96. func TestLowSNRConfidence(t *testing.T) {
  97. feat := Features{BW3dB: 3000, SpectralFlat: 0.5, PeakToAvg: 1.5}
  98. cls := RuleClassify(feat, 14.2e6, 5)
  99. if cls.Confidence > 0.5 {
  100. t.Errorf("low SNR should have low confidence: got %.2f", cls.Confidence)
  101. }
  102. }