package classifier import "math" type MathFeatures struct { EnvCoV float64 `json:"env_cov"` EnvKurtosis float64 `json:"env_kurtosis"` InstFreqStd float64 `json:"inst_freq_std"` InstFreqRange float64 `json:"inst_freq_range"` AMIndex float64 `json:"am_index"` FMIndex float64 `json:"fm_index"` InstFreqModes int `json:"inst_freq_modes"` } func ExtractMathFeatures(iq []complex64) MathFeatures { if len(iq) < 10 { return MathFeatures{} } n := len(iq) env := make([]float64, n) var envMean float64 for i, v := range iq { a := math.Hypot(float64(real(v)), float64(imag(v))) env[i] = a envMean += a } envMean /= float64(n) var envVar, envM4 float64 for _, a := range env { d := a - envMean envVar += d * d envM4 += d * d * d * d } envVar /= float64(n) envM4 /= float64(n) envStd := math.Sqrt(envVar) envCoV := 0.0 if envMean > 1e-12 { envCoV = envStd / envMean } envKurtosis := 0.0 if envVar > 1e-20 { envKurtosis = envM4 / (envVar * envVar) } instFreq := make([]float64, n-1) var ifMean float64 ifMin := math.Inf(1) ifMax := math.Inf(-1) for i := 1; i < n; i++ { p := iq[i-1] c := iq[i] num := float64(real(p))*float64(imag(c)) - float64(imag(p))*float64(real(c)) den := float64(real(p))*float64(real(c)) + float64(imag(p))*float64(imag(c)) f := math.Atan2(num, den) instFreq[i-1] = f ifMean += f if f < ifMin { ifMin = f } if f > ifMax { ifMax = f } } ifMean /= float64(n - 1) var ifVar float64 for _, f := range instFreq { d := f - ifMean ifVar += d * d } ifVar /= float64(n - 1) ifStd := math.Sqrt(ifVar) ifRange := ifMax - ifMin modes := countHistogramPeaks(instFreq, 32) amIndex := envCoV / math.Max(ifStd, 0.001) fmIndex := ifStd / math.Max(envCoV, 0.001) return MathFeatures{ EnvCoV: envCoV, EnvKurtosis: envKurtosis, InstFreqStd: ifStd, InstFreqRange: ifRange, AMIndex: amIndex, FMIndex: fmIndex, InstFreqModes: modes, } } func countHistogramPeaks(vals []float64, bins int) int { if len(vals) == 0 || bins < 3 { return 0 } minV, maxV := vals[0], vals[0] for _, v := range vals { if v < minV { minV = v } if v > maxV { maxV = v } } span := maxV - minV if span < 1e-10 { return 1 } hist := make([]int, bins) for _, v := range vals { idx := int(float64(bins-1) * (v - minV) / span) if idx >= bins { idx = bins - 1 } if idx < 0 { idx = 0 } hist[idx]++ } smooth := make([]int, bins) maxSmooth := 0 for i := range hist { s := hist[i] if i > 0 { s += hist[i-1] } if i < bins-1 { s += hist[i+1] } smooth[i] = s if s > maxSmooth { maxSmooth = s } } peaks := 0 for i := 1; i < bins-1; i++ { if smooth[i] > smooth[i-1] && smooth[i] > smooth[i+1] { if float64(smooth[i]) > 0.1*float64(maxSmooth) { peaks++ } } } if peaks == 0 { peaks = 1 } return peaks } func MathClassify(mf MathFeatures, bw float64, centerHz float64, snrDb float64) Classification { scores := map[SignalClass]float64{} if bw < 500 && mf.InstFreqStd < 0.15 { scores[ClassCW] += 3.0 } if mf.AMIndex > 3.0 { scores[ClassAM] += 2.0 } else if mf.AMIndex > 1.5 { scores[ClassAM] += 1.0 } if mf.FMIndex > 5.0 && mf.EnvCoV < 0.1 { if bw >= 80e3 { scores[ClassWFM] += 2.5 } else if bw >= 6e3 { scores[ClassNFM] += 2.5 } else { scores[ClassNFM] += 1.5 } } else if mf.FMIndex > 2.0 && mf.EnvCoV < 0.15 { if bw >= 50e3 { scores[ClassWFM] += 1.5 } else { scores[ClassNFM] += 1.5 } } if mf.AMIndex > 0.5 && mf.AMIndex < 3.0 && mf.FMIndex > 0.5 && mf.FMIndex < 3.0 { if bw >= 2000 && bw <= 4000 { scores[ClassSSBUSB] += 1.5 scores[ClassSSBLSB] += 1.5 } } if bw < 500 && mf.EnvKurtosis > 5.0 && mf.InstFreqStd < 0.1 { scores[ClassCW] += 2.5 } else if bw < 200 && mf.InstFreqStd < 0.15 { scores[ClassCW] += 1.5 } if bw < 500 { scores[ClassAM] *= 0.4 } if mf.EnvCoV < 0.05 && mf.InstFreqModes >= 2 { if bw >= 10000 && bw <= 14000 { scores[ClassDMR] += 2.0 } else if bw >= 5000 && bw <= 8000 { scores[ClassDStar] += 1.8 } else { scores[ClassFSK] += 1.5 } } if mf.EnvCoV < 0.08 && mf.InstFreqModes <= 1 && mf.InstFreqStd < 0.3 { if bw >= 100 && bw < 500 { scores[ClassWSPR] += 1.3 } if bw >= 100 && bw < 3000 { scores[ClassPSK] += 1.0 } } if mf.EnvCoV < 0.15 && mf.InstFreqModes >= 3 && bw >= 2000 && bw < 3500 { scores[ClassFT8] += 1.8 } if mf.AMIndex < 0.5 && mf.FMIndex < 0.5 && bw > 2000 { scores[ClassNoise] += 1.0 } best, _, second, _ := top2(scores) if best == "" { best = ClassUnknown } if second == "" { second = ClassUnknown } conf := softmaxConfidence(scores, best) if snrDb < 20 { snrFactor := clamp01((snrDb - 3) / 17.0) conf *= 0.3 + 0.7*snrFactor } if math.IsNaN(conf) || conf <= 0 { conf = 0.1 } return Classification{ ModType: best, Confidence: conf, BW3dB: bw, SecondBest: second, Scores: scores, MathFeatures: &mf, } }