From b53bcfcdd9951669d79a3e319d75d92b0b526160 Mon Sep 17 00:00:00 2001 From: Jan Svabenik Date: Sat, 21 Mar 2026 18:42:04 +0100 Subject: [PATCH] feat: add refinement windows for local analysis --- cmd/sdrd/pipeline_runtime.go | 11 ++++++++++- internal/pipeline/phases.go | 1 + internal/pipeline/refiner.go | 7 ++++++- internal/pipeline/types.go | 38 ++++++++++++++++++++++-------------- 4 files changed, 40 insertions(+), 17 deletions(-) diff --git a/cmd/sdrd/pipeline_runtime.go b/cmd/sdrd/pipeline_runtime.go index c72918c..ae9cd6e 100644 --- a/cmd/sdrd/pipeline_runtime.go +++ b/cmd/sdrd/pipeline_runtime.go @@ -236,10 +236,19 @@ func (rt *dspRuntime) buildRefinementInput(surv pipeline.SurveillanceResult) pip if len(scheduled) == 0 && len(plan.Selected) > 0 { scheduled = append([]pipeline.ScheduledCandidate(nil), plan.Selected...) } + windows := make([]pipeline.RefinementWindow, 0, len(scheduled)) + for _, sc := range scheduled { + windows = append(windows, pipeline.RefinementWindow{ + CenterHz: sc.Candidate.CenterHz, + SpanHz: sc.Candidate.BandwidthHz, + Source: "candidate", + }) + } input := pipeline.RefinementInput{ Candidates: append([]pipeline.Candidate(nil), surv.Candidates...), Scheduled: scheduled, Plan: plan, + Windows: windows, SampleRate: rt.cfg.SampleRate, FFTSize: rt.cfg.FFTSize, CenterHz: rt.cfg.CenterHz, @@ -284,7 +293,7 @@ func (rt *dspRuntime) refineSignals(art *spectrumArtifacts, input pipeline.Refin centerHz = rt.cfg.CenterHz } snips, snipRates := extractSignalIQBatch(extractMgr, art.iq, sampleRate, centerHz, selectedSignals) - refined := pipeline.RefineCandidates(selectedCandidates, art.spectrum, sampleRate, fftSize, snips, snipRates, classifier.ClassifierMode(rt.cfg.ClassifierMode)) + refined := pipeline.RefineCandidates(selectedCandidates, input.Windows, art.spectrum, sampleRate, fftSize, snips, snipRates, classifier.ClassifierMode(rt.cfg.ClassifierMode)) signals := make([]detector.Signal, 0, len(refined)) decisions := make([]pipeline.SignalDecision, 0, len(refined)) for i, ref := range refined { diff --git a/internal/pipeline/phases.go b/internal/pipeline/phases.go index 2fc1d3e..138cd95 100644 --- a/internal/pipeline/phases.go +++ b/internal/pipeline/phases.go @@ -24,6 +24,7 @@ type RefinementInput struct { Candidates []Candidate `json:"candidates,omitempty"` Scheduled []ScheduledCandidate `json:"scheduled,omitempty"` Plan RefinementPlan `json:"plan,omitempty"` + Windows []RefinementWindow `json:"windows,omitempty"` SampleRate int `json:"sample_rate"` FFTSize int `json:"fft_size"` CenterHz float64 `json:"center_hz"` diff --git a/internal/pipeline/refiner.go b/internal/pipeline/refiner.go index d9e8364..2406772 100644 --- a/internal/pipeline/refiner.go +++ b/internal/pipeline/refiner.go @@ -7,7 +7,7 @@ import ( // RefineCandidates upgrades coarse detector candidates into refined signals // by attaching local IQ-derived classification and PLL metadata. -func RefineCandidates(candidates []Candidate, spectrum []float64, sampleRate int, fftSize int, snippets [][]complex64, snippetRates []int, mode classifier.ClassifierMode) []Refinement { +func RefineCandidates(candidates []Candidate, windows []RefinementWindow, spectrum []float64, sampleRate int, fftSize int, snippets [][]complex64, snippetRates []int, mode classifier.ClassifierMode) []Refinement { out := make([]Refinement, 0, len(candidates)) for i, c := range candidates { sig := detector.Signal{ @@ -44,8 +44,13 @@ func RefineCandidates(candidates []Candidate, spectrum []float64, sampleRate int cls.ModType = classifier.ClassWFMStereo } } + var window RefinementWindow + if i < len(windows) { + window = windows[i] + } out = append(out, Refinement{ Candidate: c, + Window: window, Signal: sig, SnippetRate: snipRate, Class: cls, diff --git a/internal/pipeline/types.go b/internal/pipeline/types.go index 858d0ab..e248e0c 100644 --- a/internal/pipeline/types.go +++ b/internal/pipeline/types.go @@ -8,25 +8,33 @@ import ( // Candidate is the coarse output of the surveillance detector. // It intentionally stays lightweight and cheap to produce. type Candidate struct { - ID int64 `json:"id"` - CenterHz float64 `json:"center_hz"` - BandwidthHz float64 `json:"bandwidth_hz"` - PeakDb float64 `json:"peak_db"` - SNRDb float64 `json:"snr_db"` - FirstBin int `json:"first_bin"` - LastBin int `json:"last_bin"` - NoiseDb float64 `json:"noise_db,omitempty"` - Source string `json:"source,omitempty"` - Hint string `json:"hint,omitempty"` + ID int64 `json:"id"` + CenterHz float64 `json:"center_hz"` + BandwidthHz float64 `json:"bandwidth_hz"` + PeakDb float64 `json:"peak_db"` + SNRDb float64 `json:"snr_db"` + FirstBin int `json:"first_bin"` + LastBin int `json:"last_bin"` + NoiseDb float64 `json:"noise_db,omitempty"` + Source string `json:"source,omitempty"` + Hint string `json:"hint,omitempty"` +} + +// RefinementWindow describes the local analysis span that refinement should use. +type RefinementWindow struct { + CenterHz float64 `json:"center_hz"` + SpanHz float64 `json:"span_hz"` + Source string `json:"source,omitempty"` } // Refinement contains higher-cost local analysis derived from a candidate. type Refinement struct { - Candidate Candidate `json:"candidate"` - Signal detector.Signal `json:"signal"` - SnippetRate int `json:"snippet_rate"` - Class *classifier.Classification `json:"class,omitempty"` - Stage string `json:"stage,omitempty"` + Candidate Candidate `json:"candidate"` + Window RefinementWindow `json:"window"` + Signal detector.Signal `json:"signal"` + SnippetRate int `json:"snippet_rate"` + Class *classifier.Classification `json:"class,omitempty"` + Stage string `json:"stage,omitempty"` } func CandidatesFromSignals(signals []detector.Signal, source string) []Candidate {