diff --git a/cmd/sdrd/main.go b/cmd/sdrd/main.go index 95c70d5..fbf6fac 100644 --- a/cmd/sdrd/main.go +++ b/cmd/sdrd/main.go @@ -854,8 +854,10 @@ func runDSP(ctx context.Context, srcMgr *sourceManager, cfg config.Config, det * _ = enc.Encode(ev) } eventMu.Unlock() - if rec != nil { - go rec.OnEvents(finished) + if rec != nil && len(finished) > 0 { + evCopy := make([]detector.Event, len(finished)) + copy(evCopy, finished) + go rec.OnEvents(evCopy) } h.broadcast(SpectrumFrame{ Timestamp: now.UnixMilli(), diff --git a/internal/cfar/ca.go b/internal/cfar/ca.go index b13f673..feae208 100644 --- a/internal/cfar/ca.go +++ b/internal/cfar/ca.go @@ -1,5 +1,7 @@ package cfar +import "math" + // cellAvg implements CA-CFAR with a sliding sum window. type cellAvg struct { guard int @@ -40,23 +42,27 @@ func (c *cellAvg) Thresholds(spectrum []float64) []float64 { return spectrum[i] } + toLinear := func(db float64) float64 { + return math.Pow(10, db/10.0) + } + var leftSum, rightSum float64 for k := 1; k <= train; k++ { - leftSum += at(0 - guard - k) - rightSum += at(0 + guard + k) + leftSum += toLinear(at(0 - guard - k)) + rightSum += toLinear(at(0 + guard + k)) } invN := 1.0 / float64(total) - out[0] = (leftSum+rightSum)*invN + c.scaleDb + out[0] = 10*math.Log10((leftSum+rightSum)*invN) + c.scaleDb for i := 1; i < n; i++ { - leftSum -= at(i - 1 - guard - train) - leftSum += at(i - guard - 1) + leftSum -= toLinear(at(i - 1 - guard - train)) + leftSum += toLinear(at(i - guard - 1)) - rightSum -= at(i - 1 + guard + 1) - rightSum += at(i + guard + train) + rightSum -= toLinear(at(i - 1 + guard + 1)) + rightSum += toLinear(at(i + guard + train)) - out[i] = (leftSum+rightSum)*invN + c.scaleDb + out[i] = 10*math.Log10((leftSum+rightSum)*invN) + c.scaleDb } return out } diff --git a/internal/cfar/caso.go b/internal/cfar/caso.go index 6a75015..08706ac 100644 --- a/internal/cfar/caso.go +++ b/internal/cfar/caso.go @@ -1,5 +1,7 @@ package cfar +import "math" + type caso struct { guard int train int @@ -34,10 +36,14 @@ func (c *caso) Thresholds(spectrum []float64) []float64 { return spectrum[i] } + toLinear := func(db float64) float64 { + return math.Pow(10, db/10.0) + } + var leftSum, rightSum float64 for k := 1; k <= train; k++ { - leftSum += at(0 - guard - k) - rightSum += at(0 + guard + k) + leftSum += toLinear(at(0 - guard - k)) + rightSum += toLinear(at(0 + guard + k)) } lm := leftSum * inv rm := rightSum * inv @@ -45,20 +51,20 @@ func (c *caso) Thresholds(spectrum []float64) []float64 { if rm < noise { noise = rm } - out[0] = noise + c.scaleDb + out[0] = 10*math.Log10(noise) + c.scaleDb for i := 1; i < n; i++ { - leftSum -= at(i - 1 - guard - train) - leftSum += at(i - guard - 1) - rightSum -= at(i - 1 + guard + 1) - rightSum += at(i + guard + train) + leftSum -= toLinear(at(i - 1 - guard - train)) + leftSum += toLinear(at(i - guard - 1)) + rightSum -= toLinear(at(i - 1 + guard + 1)) + rightSum += toLinear(at(i + guard + train)) lm = leftSum * inv rm = rightSum * inv noise = lm if rm < noise { noise = rm } - out[i] = noise + c.scaleDb + out[i] = 10*math.Log10(noise) + c.scaleDb } return out } diff --git a/internal/cfar/cfar_test.go b/internal/cfar/cfar_test.go index 40efb55..8e6db39 100644 --- a/internal/cfar/cfar_test.go +++ b/internal/cfar/cfar_test.go @@ -1,6 +1,9 @@ package cfar -import "testing" +import ( + "math" + "testing" +) func makeSpectrum(n int, noiseDb float64, signals [][2]int, sigDb float64) []float64 { s := make([]float64, n) @@ -59,6 +62,37 @@ func TestGOSCAMaskingProtection(t *testing.T) { } } +func TestCellAveragingUsesLinearPower(t *testing.T) { + spec := []float64{-100, -100, -100, -80, -100, -90, -100, -100, -100} + cfg := Config{GuardCells: 0, TrainCells: 2, ScaleDb: 0, WrapAround: false} + + ca := New(Config{Mode: ModeCA, GuardCells: cfg.GuardCells, TrainCells: cfg.TrainCells, ScaleDb: cfg.ScaleDb, WrapAround: cfg.WrapAround}) + gosca := New(Config{Mode: ModeGOSCA, GuardCells: cfg.GuardCells, TrainCells: cfg.TrainCells, ScaleDb: cfg.ScaleDb, WrapAround: cfg.WrapAround}) + caso := New(Config{Mode: ModeCASO, GuardCells: cfg.GuardCells, TrainCells: cfg.TrainCells, ScaleDb: cfg.ScaleDb, WrapAround: cfg.WrapAround}) + + mid := 5 + caTh := ca.Thresholds(spec)[mid] + goscaTh := gosca.Thresholds(spec)[mid] + casoTh := caso.Thresholds(spec)[mid] + + dbApproxCA := (-80.0 + -90.0 + -100.0 + -100.0) / 4.0 + dbApproxGOSCA := -85.0 + dbApproxCASO := -100.0 + + if math.Abs(caTh-dbApproxCA) < 1.0 { + t.Fatalf("CA threshold still looks like dB averaging: got %v approx %v", caTh, dbApproxCA) + } + if math.Abs(goscaTh-dbApproxGOSCA) < 1.0 { + t.Fatalf("GOSCA threshold still looks like dB averaging: got %v approx %v", goscaTh, dbApproxGOSCA) + } + if math.Abs(casoTh-dbApproxCASO) < 1.0 { + t.Fatalf("CASO threshold still looks like dB averaging: got %v approx %v", casoTh, dbApproxCASO) + } + if !(goscaTh > caTh && caTh > casoTh) { + t.Fatalf("unexpected ordering: GOSCA=%v CA=%v CASO=%v", goscaTh, caTh, casoTh) + } +} + func BenchmarkCFAR(b *testing.B) { spec := makeSpectrum(2048, -100, [][2]int{{500, 510}, {1000, 1020}}, -20) for _, mode := range []Mode{ModeCA, ModeOS, ModeGOSCA, ModeCASO} { diff --git a/internal/cfar/gosca.go b/internal/cfar/gosca.go index 27f2a28..d3f063c 100644 --- a/internal/cfar/gosca.go +++ b/internal/cfar/gosca.go @@ -1,5 +1,7 @@ package cfar +import "math" + // gosca implements Greatest-Of Selection with Cell Averaging. type gosca struct { guard int @@ -40,10 +42,14 @@ func (g *gosca) Thresholds(spectrum []float64) []float64 { return spectrum[i] } + toLinear := func(db float64) float64 { + return math.Pow(10, db/10.0) + } + var leftSum, rightSum float64 for k := 1; k <= train; k++ { - leftSum += at(0 - guard - k) - rightSum += at(0 + guard + k) + leftSum += toLinear(at(0 - guard - k)) + rightSum += toLinear(at(0 + guard + k)) } leftMean := leftSum * inv @@ -52,13 +58,13 @@ func (g *gosca) Thresholds(spectrum []float64) []float64 { if rightMean > noise { noise = rightMean } - out[0] = noise + g.scaleDb + out[0] = 10*math.Log10(noise) + g.scaleDb for i := 1; i < n; i++ { - leftSum -= at(i - 1 - guard - train) - leftSum += at(i - guard - 1) - rightSum -= at(i - 1 + guard + 1) - rightSum += at(i + guard + train) + leftSum -= toLinear(at(i - 1 - guard - train)) + leftSum += toLinear(at(i - guard - 1)) + rightSum -= toLinear(at(i - 1 + guard + 1)) + rightSum += toLinear(at(i + guard + train)) leftMean = leftSum * inv rightMean = rightSum * inv @@ -66,7 +72,7 @@ func (g *gosca) Thresholds(spectrum []float64) []float64 { if rightMean > noise { noise = rightMean } - out[i] = noise + g.scaleDb + out[i] = 10*math.Log10(noise) + g.scaleDb } return out } diff --git a/internal/cfar/os.go b/internal/cfar/os.go index a318d0e..58d002a 100644 --- a/internal/cfar/os.go +++ b/internal/cfar/os.go @@ -55,6 +55,15 @@ func (o *orderedStat) Thresholds(spectrum []float64) []float64 { sort.Float64s(win) out[0] = win[o.rank] + o.scaleDb + rebuildWindow := func(bin int) { + win = win[:0] + for k := 1; k <= train; k++ { + win = append(win, at(bin-guard-k)) + win = append(win, at(bin+guard+k)) + } + sort.Float64s(win) + } + for i := 1; i < n; i++ { removeFromSorted(&win, at(i-1-guard-train)) removeFromSorted(&win, at(i-1+guard+1)) @@ -62,6 +71,9 @@ func (o *orderedStat) Thresholds(spectrum []float64) []float64 { insertSorted(&win, at(i-guard-1)) insertSorted(&win, at(i+guard+train)) + if len(win) != 2*train { + rebuildWindow(i) + } out[i] = win[o.rank] + o.scaleDb } return out