Explorar el Código

feat: expand detected signal edges in second pass

master
Jan Svabenik hace 2 días
padre
commit
a2c306ad7a
Se han modificado 3 ficheros con 242 adiciones y 29 borrados
  1. +10
    -0
      internal/config/config.go
  2. +183
    -27
      internal/detector/detector.go
  3. +49
    -2
      internal/detector/detector_test.go

+ 10
- 0
internal/config/config.go Ver fichero

@@ -28,6 +28,8 @@ type DetectorConfig struct {
CFARRank int `yaml:"cfar_rank" json:"cfar_rank"`
CFARScaleDb float64 `yaml:"cfar_scale_db" json:"cfar_scale_db"`
CFARWrapAround bool `yaml:"cfar_wrap_around" json:"cfar_wrap_around"`
EdgeMarginDb float64 `yaml:"edge_margin_db" json:"edge_margin_db"`
MaxSignalBwHz float64 `yaml:"max_signal_bw_hz" json:"max_signal_bw_hz"`

// Deprecated (backward compatibility)
CFAREnabled *bool `yaml:"cfar_enabled,omitempty" json:"cfar_enabled,omitempty"`
@@ -107,6 +109,8 @@ func Default() Config {
CFARRank: 36,
CFARScaleDb: 6,
CFARWrapAround: true,
EdgeMarginDb: 3.0,
MaxSignalBwHz: 150000,
},
Recorder: RecorderConfig{
Enabled: false,
@@ -187,6 +191,12 @@ func applyDefaults(cfg Config) Config {
if cfg.Detector.CFARScaleDb <= 0 {
cfg.Detector.CFARScaleDb = 6
}
if cfg.Detector.EdgeMarginDb <= 0 {
cfg.Detector.EdgeMarginDb = 3.0
}
if cfg.Detector.MaxSignalBwHz <= 0 {
cfg.Detector.MaxSignalBwHz = 150000
}
if cfg.FrameRate <= 0 {
cfg.FrameRate = 15
}


+ 183
- 27
internal/detector/detector.go Ver fichero

@@ -32,14 +32,16 @@ type Detector struct {
MinStableFrames int
GapTolerance time.Duration
CFARScaleDb float64
EdgeMarginDb float64
MaxSignalBwHz float64
binWidth float64
nbins int
sampleRate int

ema []float64
active map[int64]*activeEvent
nextID int64
cfarEngine cfar.CFAR
ema []float64
active map[int64]*activeEvent
nextID int64
cfarEngine cfar.CFAR
lastThresholds []float64
lastNoiseFloor float64
}
@@ -83,6 +85,8 @@ func New(detCfg config.DetectorConfig, sampleRate int, fftSize int) *Detector {
cfarScaleDb := detCfg.CFARScaleDb
cfarWrap := detCfg.CFARWrapAround
thresholdDb := detCfg.ThresholdDb
edgeMarginDb := detCfg.EdgeMarginDb
maxSignalBwHz := detCfg.MaxSignalBwHz

if minDur <= 0 {
minDur = 250 * time.Millisecond
@@ -111,6 +115,12 @@ func New(detCfg config.DetectorConfig, sampleRate int, fftSize int) *Detector {
if cfarScaleDb <= 0 {
cfarScaleDb = 6
}
if edgeMarginDb <= 0 {
edgeMarginDb = 3.0
}
if maxSignalBwHz <= 0 {
maxSignalBwHz = 150000
}
if cfarRank <= 0 || cfarRank > 2*cfarTrain {
cfarRank = int(math.Round(0.75 * float64(2*cfarTrain)))
if cfarRank <= 0 {
@@ -137,6 +147,8 @@ func New(detCfg config.DetectorConfig, sampleRate int, fftSize int) *Detector {
MinStableFrames: minStable,
GapTolerance: gapTolerance,
CFARScaleDb: cfarScaleDb,
EdgeMarginDb: edgeMarginDb,
MaxSignalBwHz: maxSignalBwHz,
binWidth: float64(sampleRate) / float64(fftSize),
nbins: fftSize,
sampleRate: sampleRate,
@@ -164,7 +176,6 @@ func (d *Detector) LastNoiseFloor() float64 {
return d.lastNoiseFloor
}

// UpdateClasses refreshes active event classes from current signals.
func (d *Detector) UpdateClasses(signals []Signal) {
for _, s := range signals {
for _, ev := range d.active {
@@ -230,9 +241,176 @@ func (d *Detector) detectSignals(spectrum []float64, centerHz float64) []Signal
}
signals = append(signals, d.makeSignal(start, n-1, peak, peakBin, noise, centerHz, smooth))
}
signals = d.expandSignalEdges(signals, smooth, noiseGlobal, centerHz)
for i := range signals {
centerBin := float64(signals[i].FirstBin+signals[i].LastBin) / 2.0
signals[i].CenterHz = d.centerFreqForBin(centerBin, centerHz)
signals[i].BWHz = float64(signals[i].LastBin-signals[i].FirstBin+1) * d.binWidth
}
return signals
}

func (d *Detector) expandSignalEdges(signals []Signal, smooth []float64, noiseFloor float64, centerHz float64) []Signal {
n := len(smooth)
if n == 0 || len(signals) == 0 {
return signals
}
margin := d.EdgeMarginDb
if margin <= 0 {
margin = 3.0
}
maxExpansionBins := int(d.MaxSignalBwHz / d.binWidth)
if maxExpansionBins < 10 {
maxExpansionBins = 10
}
for i := range signals {
seed := signals[i]
peakDb := seed.PeakDb
localNoise := noiseFloor
leftProbe := seed.FirstBin - 50
rightProbe := seed.LastBin + 50
if leftProbe >= 0 && rightProbe < n {
leftNoise := minInRange(smooth, maxInt(0, leftProbe), maxInt(0, seed.FirstBin-5))
rightNoise := minInRange(smooth, minInt(n-1, seed.LastBin+5), minInt(n-1, rightProbe))
localNoise = math.Min(leftNoise, rightNoise)
}
edgeThreshold := localNoise + margin
newFirst := seed.FirstBin
prevVal := smooth[newFirst]
for j := 0; j < maxExpansionBins; j++ {
next := newFirst - 1
if next < 0 {
break
}
val := smooth[next]
if val <= edgeThreshold {
break
}
if val > prevVal+1.0 && val < peakDb-6.0 {
break
}
prevVal = val
newFirst = next
}
newLast := seed.LastBin
prevVal = smooth[newLast]
for j := 0; j < maxExpansionBins; j++ {
next := newLast + 1
if next >= n {
break
}
val := smooth[next]
if val <= edgeThreshold {
break
}
if val > prevVal+1.0 && val < peakDb-6.0 {
break
}
prevVal = val
newLast = next
}
signals[i].FirstBin = newFirst
signals[i].LastBin = newLast
centerBin := float64(newFirst+newLast) / 2.0
signals[i].CenterHz = d.centerFreqForBin(centerBin, centerHz)
signals[i].BWHz = float64(newLast-newFirst+1) * d.binWidth
}
signals = d.mergeOverlapping(signals, centerHz)
return signals
}

func (d *Detector) mergeOverlapping(signals []Signal, centerHz float64) []Signal {
if len(signals) <= 1 {
return signals
}
sort.Slice(signals, func(i, j int) bool {
return signals[i].FirstBin < signals[j].FirstBin
})
merged := []Signal{signals[0]}
for i := 1; i < len(signals); i++ {
last := &merged[len(merged)-1]
cur := signals[i]
if cur.FirstBin <= last.LastBin+1 {
if cur.LastBin > last.LastBin {
last.LastBin = cur.LastBin
}
if cur.PeakDb > last.PeakDb {
last.PeakDb = cur.PeakDb
}
if cur.SNRDb > last.SNRDb {
last.SNRDb = cur.SNRDb
}
centerBin := float64(last.FirstBin+last.LastBin) / 2.0
last.BWHz = float64(last.LastBin-last.FirstBin+1) * d.binWidth
last.CenterHz = d.centerFreqForBin(centerBin, centerHz)
} else {
merged = append(merged, cur)
}
}
return merged
}

func (d *Detector) centerFreqForBin(bin float64, centerHz float64) float64 {
return centerHz + (bin-float64(d.nbins)/2.0)*d.binWidth
}

func minInRange(s []float64, from, to int) float64 {
if len(s) == 0 {
return 0
}
if from < 0 {
from = 0
}
if to >= len(s) {
to = len(s) - 1
}
if from > to {
return s[minInt(maxInt(from, 0), len(s)-1)]
}
m := s[from]
for i := from + 1; i <= to; i++ {
if s[i] < m {
m = s[i]
}
}
return m
}

func maxInt(a, b int) int {
if a > b {
return a
}
return b
}

func minInt(a, b int) int {
if a < b {
return a
}
return b
}

func overlapHz(center1, bw1, center2, bw2 float64) bool {
left1 := center1 - bw1/2
right1 := center1 + bw1/2
left2 := center2 - bw2/2
right2 := center2 + bw2/2
return left1 <= right2 && left2 <= right1
}

func median(vals []float64) float64 {
if len(vals) == 0 {
return 0
}
cp := append([]float64(nil), vals...)
sort.Float64s(cp)
mid := len(cp) / 2
if len(cp)%2 == 0 {
return (cp[mid-1] + cp[mid]) / 2
}
return cp[mid]
}

func (d *Detector) makeSignal(first, last int, peak float64, peakBin int, noise float64, centerHz float64, spectrum []float64) Signal {
centerBin := float64(first+last) / 2.0
centerFreq := centerHz + (centerBin-float64(d.nbins)/2.0)*d.binWidth
@@ -260,7 +438,6 @@ func (d *Detector) smoothSpectrum(spectrum []float64) []float64 {
v := spectrum[i]
d.ema[i] = alpha*v + (1-alpha)*d.ema[i]
}
// IMPORTANT: caller must not modify returned slice
return d.ema
}

@@ -357,24 +534,3 @@ func (d *Detector) matchSignals(now time.Time, signals []Signal) []Event {
}
return finished
}

func overlapHz(c1, b1, c2, b2 float64) bool {
l1 := c1 - b1/2.0
r1 := c1 + b1/2.0
l2 := c2 - b2/2.0
r2 := c2 + b2/2.0
return l1 <= r2 && l2 <= r1
}

func median(vals []float64) float64 {
if len(vals) == 0 {
return 0
}
cpy := append([]float64(nil), vals...)
sort.Float64s(cpy)
mid := len(cpy) / 2
if len(cpy)%2 == 0 {
return (cpy[mid-1] + cpy[mid]) / 2.0
}
return cpy[mid]
}

+ 49
- 2
internal/detector/detector_test.go Ver fichero

@@ -37,10 +37,8 @@ func TestDetectorCreatesEvent(t *testing.T) {
t.Fatalf("expected bandwidth > 0")
}

// Extend signal duration.
_, _ = d.Process(now.Add(5*time.Millisecond), spectrum, center)

// Advance beyond hold with no signal to finalize.
now2 := now.Add(30 * time.Millisecond)
noSignal := make([]float64, len(spectrum))
for i := range noSignal {
@@ -54,3 +52,52 @@ func TestDetectorCreatesEvent(t *testing.T) {
t.Fatalf("event bandwidth not set")
}
}

func TestSignalBandwidthExpansion(t *testing.T) {
sampleRate := 2048000
fftSize := 2048
cfg := config.DetectorConfig{
ThresholdDb: -20,
MinDurationMs: 1,
HoldMs: 10,
EmaAlpha: 1.0,
HysteresisDb: 3,
MinStableFrames: 1,
GapToleranceMs: 10,
CFARMode: "OFF",
CFARGuardCells: 2,
CFARTrainCells: 8,
CFARRank: 12,
CFARScaleDb: 6,
CFARWrapAround: true,
EdgeMarginDb: 3.0,
MaxSignalBwHz: 150000,
}
d := New(cfg, sampleRate, fftSize)
spectrum := make([]float64, fftSize)
for i := range spectrum {
spectrum[i] = -100
}
for i := 1000; i <= 1012; i++ {
spectrum[i] = -20
}
for j := 1; j <= 5; j++ {
level := -20 - float64(j)*3
if 1000-j >= 0 {
spectrum[1000-j] = level
}
if 1012+j < fftSize {
spectrum[1012+j] = level
}
}
now := time.Now()
_, signals := d.Process(now, spectrum, 434e6)
if len(signals) == 0 {
t.Fatal("no signals detected")
}
sig := signals[0]
expectedMinBW := 18.0 * 1000
if sig.BWHz < expectedMinBW {
t.Errorf("BW too narrow: got %.0f Hz, want >= %.0f Hz (FirstBin=%d LastBin=%d)", sig.BWHz, expectedMinBW, sig.FirstBin, sig.LastBin)
}
}

Cargando…
Cancelar
Guardar