| @@ -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(), | |||
| @@ -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 | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -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} { | |||
| @@ -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 | |||
| } | |||
| @@ -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 | |||