| @@ -854,8 +854,10 @@ func runDSP(ctx context.Context, srcMgr *sourceManager, cfg config.Config, det * | |||||
| _ = enc.Encode(ev) | _ = enc.Encode(ev) | ||||
| } | } | ||||
| eventMu.Unlock() | 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{ | h.broadcast(SpectrumFrame{ | ||||
| Timestamp: now.UnixMilli(), | Timestamp: now.UnixMilli(), | ||||
| @@ -1,5 +1,7 @@ | |||||
| package cfar | package cfar | ||||
| import "math" | |||||
| // cellAvg implements CA-CFAR with a sliding sum window. | // cellAvg implements CA-CFAR with a sliding sum window. | ||||
| type cellAvg struct { | type cellAvg struct { | ||||
| guard int | guard int | ||||
| @@ -40,23 +42,27 @@ func (c *cellAvg) Thresholds(spectrum []float64) []float64 { | |||||
| return spectrum[i] | return spectrum[i] | ||||
| } | } | ||||
| toLinear := func(db float64) float64 { | |||||
| return math.Pow(10, db/10.0) | |||||
| } | |||||
| var leftSum, rightSum float64 | var leftSum, rightSum float64 | ||||
| for k := 1; k <= train; k++ { | 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) | 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++ { | 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 | return out | ||||
| } | } | ||||
| @@ -1,5 +1,7 @@ | |||||
| package cfar | package cfar | ||||
| import "math" | |||||
| type caso struct { | type caso struct { | ||||
| guard int | guard int | ||||
| train int | train int | ||||
| @@ -34,10 +36,14 @@ func (c *caso) Thresholds(spectrum []float64) []float64 { | |||||
| return spectrum[i] | return spectrum[i] | ||||
| } | } | ||||
| toLinear := func(db float64) float64 { | |||||
| return math.Pow(10, db/10.0) | |||||
| } | |||||
| var leftSum, rightSum float64 | var leftSum, rightSum float64 | ||||
| for k := 1; k <= train; k++ { | 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 | lm := leftSum * inv | ||||
| rm := rightSum * inv | rm := rightSum * inv | ||||
| @@ -45,20 +51,20 @@ func (c *caso) Thresholds(spectrum []float64) []float64 { | |||||
| if rm < noise { | if rm < noise { | ||||
| noise = rm | noise = rm | ||||
| } | } | ||||
| out[0] = noise + c.scaleDb | |||||
| out[0] = 10*math.Log10(noise) + c.scaleDb | |||||
| for i := 1; i < n; i++ { | 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 | lm = leftSum * inv | ||||
| rm = rightSum * inv | rm = rightSum * inv | ||||
| noise = lm | noise = lm | ||||
| if rm < noise { | if rm < noise { | ||||
| noise = rm | noise = rm | ||||
| } | } | ||||
| out[i] = noise + c.scaleDb | |||||
| out[i] = 10*math.Log10(noise) + c.scaleDb | |||||
| } | } | ||||
| return out | return out | ||||
| } | } | ||||
| @@ -1,6 +1,9 @@ | |||||
| package cfar | package cfar | ||||
| import "testing" | |||||
| import ( | |||||
| "math" | |||||
| "testing" | |||||
| ) | |||||
| func makeSpectrum(n int, noiseDb float64, signals [][2]int, sigDb float64) []float64 { | func makeSpectrum(n int, noiseDb float64, signals [][2]int, sigDb float64) []float64 { | ||||
| s := make([]float64, n) | 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) { | func BenchmarkCFAR(b *testing.B) { | ||||
| spec := makeSpectrum(2048, -100, [][2]int{{500, 510}, {1000, 1020}}, -20) | spec := makeSpectrum(2048, -100, [][2]int{{500, 510}, {1000, 1020}}, -20) | ||||
| for _, mode := range []Mode{ModeCA, ModeOS, ModeGOSCA, ModeCASO} { | for _, mode := range []Mode{ModeCA, ModeOS, ModeGOSCA, ModeCASO} { | ||||
| @@ -1,5 +1,7 @@ | |||||
| package cfar | package cfar | ||||
| import "math" | |||||
| // gosca implements Greatest-Of Selection with Cell Averaging. | // gosca implements Greatest-Of Selection with Cell Averaging. | ||||
| type gosca struct { | type gosca struct { | ||||
| guard int | guard int | ||||
| @@ -40,10 +42,14 @@ func (g *gosca) Thresholds(spectrum []float64) []float64 { | |||||
| return spectrum[i] | return spectrum[i] | ||||
| } | } | ||||
| toLinear := func(db float64) float64 { | |||||
| return math.Pow(10, db/10.0) | |||||
| } | |||||
| var leftSum, rightSum float64 | var leftSum, rightSum float64 | ||||
| for k := 1; k <= train; k++ { | 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 | leftMean := leftSum * inv | ||||
| @@ -52,13 +58,13 @@ func (g *gosca) Thresholds(spectrum []float64) []float64 { | |||||
| if rightMean > noise { | if rightMean > noise { | ||||
| noise = rightMean | noise = rightMean | ||||
| } | } | ||||
| out[0] = noise + g.scaleDb | |||||
| out[0] = 10*math.Log10(noise) + g.scaleDb | |||||
| for i := 1; i < n; i++ { | 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 | leftMean = leftSum * inv | ||||
| rightMean = rightSum * inv | rightMean = rightSum * inv | ||||
| @@ -66,7 +72,7 @@ func (g *gosca) Thresholds(spectrum []float64) []float64 { | |||||
| if rightMean > noise { | if rightMean > noise { | ||||
| noise = rightMean | noise = rightMean | ||||
| } | } | ||||
| out[i] = noise + g.scaleDb | |||||
| out[i] = 10*math.Log10(noise) + g.scaleDb | |||||
| } | } | ||||
| return out | return out | ||||
| } | } | ||||
| @@ -55,6 +55,15 @@ func (o *orderedStat) Thresholds(spectrum []float64) []float64 { | |||||
| sort.Float64s(win) | sort.Float64s(win) | ||||
| out[0] = win[o.rank] + o.scaleDb | 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++ { | for i := 1; i < n; i++ { | ||||
| removeFromSorted(&win, at(i-1-guard-train)) | removeFromSorted(&win, at(i-1-guard-train)) | ||||
| removeFromSorted(&win, at(i-1+guard+1)) | 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-1)) | ||||
| insertSorted(&win, at(i+guard+train)) | insertSorted(&win, at(i+guard+train)) | ||||
| if len(win) != 2*train { | |||||
| rebuildWindow(i) | |||||
| } | |||||
| out[i] = win[o.rank] + o.scaleDb | out[i] = win[o.rank] + o.scaleDb | ||||
| } | } | ||||
| return out | return out | ||||