From fa844534378e97133cfc4806d98942bd7c06a548 Mon Sep 17 00:00:00 2001 From: Jan Svabenik Date: Sun, 22 Mar 2026 04:45:55 +0100 Subject: [PATCH] Refine window sources and span reporting --- cmd/sdrd/pipeline_runtime.go | 25 ++++++++++++++++++++++++- cmd/sdrd/pipeline_runtime_test.go | 15 +++++++++++++++ internal/pipeline/window_rules.go | 3 +++ internal/pipeline/window_rules_test.go | 23 +++++++++++++++++++++++ 4 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 internal/pipeline/window_rules_test.go diff --git a/cmd/sdrd/pipeline_runtime.go b/cmd/sdrd/pipeline_runtime.go index 2850d01..4a8c72b 100644 --- a/cmd/sdrd/pipeline_runtime.go +++ b/cmd/sdrd/pipeline_runtime.go @@ -296,12 +296,16 @@ func (rt *dspRuntime) buildRefinementInput(surv pipeline.SurveillanceResult) pip for _, sc := range scheduled { windows = append(windows, pipeline.RefinementWindowForCandidate(policy, sc.Candidate)) } + levelSpan := spanForPolicy(policy, float64(rt.cfg.SampleRate)) + if _, maxSpan, ok := windowSpanBounds(windows); ok { + levelSpan = maxSpan + } level := pipeline.AnalysisLevel{ Name: "refinement", SampleRate: rt.cfg.SampleRate, FFTSize: rt.cfg.FFTSize, CenterHz: rt.cfg.CenterHz, - SpanHz: spanForPolicy(policy, float64(rt.cfg.SampleRate)), + SpanHz: levelSpan, Source: "refinement-window", } input := pipeline.RefinementInput{ @@ -508,6 +512,25 @@ func spanForPolicy(policy pipeline.Policy, fallback float64) float64 { return fallback } +func windowSpanBounds(windows []pipeline.RefinementWindow) (float64, float64, bool) { + minSpan := 0.0 + maxSpan := 0.0 + ok := false + for _, w := range windows { + if w.SpanHz <= 0 { + continue + } + if !ok || w.SpanHz < minSpan { + minSpan = w.SpanHz + } + if !ok || w.SpanHz > maxSpan { + maxSpan = w.SpanHz + } + ok = true + } + return minSpan, maxSpan, ok +} + func surveillanceLevels(policy pipeline.Policy, primary pipeline.AnalysisLevel, secondary pipeline.AnalysisLevel) []pipeline.AnalysisLevel { levels := []pipeline.AnalysisLevel{primary} strategy := strings.ToLower(strings.TrimSpace(policy.SurveillanceStrategy)) diff --git a/cmd/sdrd/pipeline_runtime_test.go b/cmd/sdrd/pipeline_runtime_test.go index 0fded23..0067dbf 100644 --- a/cmd/sdrd/pipeline_runtime_test.go +++ b/cmd/sdrd/pipeline_runtime_test.go @@ -57,3 +57,18 @@ func TestSurveillanceLevelsRespectStrategy(t *testing.T) { t.Fatalf("expected secondary level for multi-res, got %d", len(levels)) } } + +func TestWindowSpanBounds(t *testing.T) { + windows := []pipeline.RefinementWindow{ + {SpanHz: 8000}, + {SpanHz: 16000}, + {SpanHz: 12000}, + } + minSpan, maxSpan, ok := windowSpanBounds(windows) + if !ok { + t.Fatalf("expected spans to be found") + } + if minSpan != 8000 || maxSpan != 16000 { + t.Fatalf("unexpected span bounds: min %.0f max %.0f", minSpan, maxSpan) + } +} diff --git a/internal/pipeline/window_rules.go b/internal/pipeline/window_rules.go index 09cc8e7..a4e6d51 100644 --- a/internal/pipeline/window_rules.go +++ b/internal/pipeline/window_rules.go @@ -42,12 +42,15 @@ func RefinementWindowForCandidate(policy Policy, candidate Candidate) Refinement } if policy.RefinementMinSpanHz > 0 && span < policy.RefinementMinSpanHz { span = policy.RefinementMinSpanHz + windowSource = "policy:min_span" } if policy.RefinementMaxSpanHz > 0 && span > policy.RefinementMaxSpanHz { span = policy.RefinementMaxSpanHz + windowSource = "policy:max_span" } if span <= 0 { span = 12000 + windowSource = "default" } return RefinementWindow{ CenterHz: candidate.CenterHz, diff --git a/internal/pipeline/window_rules_test.go b/internal/pipeline/window_rules_test.go new file mode 100644 index 0000000..741e340 --- /dev/null +++ b/internal/pipeline/window_rules_test.go @@ -0,0 +1,23 @@ +package pipeline + +import "testing" + +func TestRefinementWindowClampsToPolicy(t *testing.T) { + policy := Policy{RefinementMinSpanHz: 12000, RefinementMaxSpanHz: 20000, RefinementAutoSpan: false} + win := RefinementWindowForCandidate(policy, Candidate{CenterHz: 1e6, BandwidthHz: 8000}) + if win.SpanHz != 12000 || win.Source != "policy:min_span" { + t.Fatalf("expected min clamp, got span %.0f source %q", win.SpanHz, win.Source) + } + win = RefinementWindowForCandidate(policy, Candidate{CenterHz: 1e6, BandwidthHz: 50000}) + if win.SpanHz != 20000 || win.Source != "policy:max_span" { + t.Fatalf("expected max clamp, got span %.0f source %q", win.SpanHz, win.Source) + } +} + +func TestRefinementWindowDefaultsWhenEmpty(t *testing.T) { + policy := Policy{RefinementAutoSpan: false} + win := RefinementWindowForCandidate(policy, Candidate{CenterHz: 1e6, BandwidthHz: 0}) + if win.SpanHz != 12000 || win.Source != "default" { + t.Fatalf("expected default span, got span %.0f source %q", win.SpanHz, win.Source) + } +}