ソースを参照

Add derived candidate fusion and evidence scoring

master
Jan Svabenik 14時間前
コミット
1524000fc3
6個のファイルの変更252行の追加9行の削除
  1. +8
    -0
      cmd/sdrd/dsp_loop.go
  2. +96
    -1
      cmd/sdrd/pipeline_runtime.go
  3. +9
    -7
      cmd/sdrd/types.go
  4. +124
    -0
      internal/pipeline/candidate_fusion.go
  5. +14
    -0
      internal/pipeline/scheduler.go
  6. +1
    -1
      internal/pipeline/types.go

+ 8
- 0
cmd/sdrd/dsp_loop.go ファイルの表示

@@ -133,6 +133,14 @@ func runDSP(ctx context.Context, srcMgr *sourceManager, cfg config.Config, det *
}) })
} }
debugInfo = &SpectrumDebug{Thresholds: thresholds, NoiseFloor: noiseFloor, Scores: scoreDebug} debugInfo = &SpectrumDebug{Thresholds: thresholds, NoiseFloor: noiseFloor, Scores: scoreDebug}
candidateSources := buildCandidateSourceSummary(state.surveillance.Candidates)
candidateEvidence := buildCandidateEvidenceSummary(state.surveillance.Candidates)
if len(candidateSources) > 0 {
debugInfo.CandidateSources = candidateSources
}
if len(candidateEvidence) > 0 {
debugInfo.CandidateEvidence = candidateEvidence
}
if hasPlan { if hasPlan {
debugInfo.RefinementPlan = &plan debugInfo.RefinementPlan = &plan
} }


+ 96
- 1
cmd/sdrd/pipeline_runtime.go ファイルの表示

@@ -1,6 +1,7 @@
package main package main


import ( import (
"fmt"
"math" "math"
"strings" "strings"
"sync" "sync"
@@ -30,6 +31,8 @@ type rdsState struct {
type dspRuntime struct { type dspRuntime struct {
cfg config.Config cfg config.Config
det *detector.Detector det *detector.Detector
derivedDetectors map[string]*derivedDetector
nextDerivedBase int64
window []float64 window []float64
plan *fftutil.CmplxPlan plan *fftutil.CmplxPlan
detailWindow []float64 detailWindow []float64
@@ -65,6 +68,13 @@ type spectrumArtifacts struct {
now time.Time now time.Time
} }


type derivedDetector struct {
det *detector.Detector
sampleRate int
fftSize int
idBase int64
}

type surveillanceLevelSpec struct { type surveillanceLevelSpec struct {
Level pipeline.AnalysisLevel Level pipeline.AnalysisLevel
Decim int Decim int
@@ -80,6 +90,8 @@ type surveillancePlan struct {
Specs []surveillanceLevelSpec Specs []surveillanceLevelSpec
} }


const derivedIDBlock = int64(1_000_000_000)

func newDSPRuntime(cfg config.Config, det *detector.Detector, window []float64, gpuState *gpuStatus) *dspRuntime { func newDSPRuntime(cfg config.Config, det *detector.Detector, window []float64, gpuState *gpuStatus) *dspRuntime {
detailFFT := cfg.Refinement.DetailFFTSize detailFFT := cfg.Refinement.DetailFFTSize
if detailFFT <= 0 { if detailFFT <= 0 {
@@ -88,6 +100,8 @@ func newDSPRuntime(cfg config.Config, det *detector.Detector, window []float64,
rt := &dspRuntime{ rt := &dspRuntime{
cfg: cfg, cfg: cfg,
det: det, det: det,
derivedDetectors: map[string]*derivedDetector{},
nextDerivedBase: -derivedIDBlock,
window: window, window: window,
plan: fftutil.NewCmplxPlan(cfg.FFTSize), plan: fftutil.NewCmplxPlan(cfg.FFTSize),
detailWindow: fftutil.Hann(detailFFT), detailWindow: fftutil.Hann(detailFFT),
@@ -168,6 +182,10 @@ func (rt *dspRuntime) applyUpdate(upd dspUpdate, srcMgr *sourceManager, rec *rec
rt.survWindows = map[int][]float64{} rt.survWindows = map[int][]float64{}
rt.survPlans = map[int]*fftutil.CmplxPlan{} rt.survPlans = map[int]*fftutil.CmplxPlan{}
} }
if upd.det != nil || prevSampleRate != rt.cfg.SampleRate || prevFFT != rt.cfg.FFTSize {
rt.derivedDetectors = map[string]*derivedDetector{}
rt.nextDerivedBase = -derivedIDBlock
}
rt.dcEnabled = upd.dcBlock rt.dcEnabled = upd.dcBlock
rt.iqEnabled = upd.iqBalance rt.iqEnabled = upd.iqBalance
if rt.cfg.FFTSize != prevFFT || rt.cfg.UseGPUFFT != prevUseGPU { if rt.cfg.FFTSize != prevFFT || rt.cfg.UseGPUFFT != prevUseGPU {
@@ -419,7 +437,9 @@ func (rt *dspRuntime) buildSurveillanceResult(art *spectrumArtifacts) pipeline.S
if plan.Primary.Name == "" { if plan.Primary.Name == "" {
plan = rt.buildSurveillancePlan(policy) plan = rt.buildSurveillancePlan(policy)
} }
candidates := pipeline.CandidatesFromSignalsWithLevel(art.detected, "surveillance-detector", plan.Primary)
primaryCandidates := pipeline.CandidatesFromSignalsWithLevel(art.detected, "surveillance-detector", plan.Primary)
derivedCandidates := rt.detectDerivedCandidates(art, plan)
candidates := pipeline.FuseCandidates(primaryCandidates, derivedCandidates)
scheduled := pipeline.ScheduleCandidates(candidates, policy) scheduled := pipeline.ScheduleCandidates(candidates, policy)
return pipeline.SurveillanceResult{ return pipeline.SurveillanceResult{
Level: plan.Primary, Level: plan.Primary,
@@ -437,6 +457,81 @@ func (rt *dspRuntime) buildSurveillanceResult(art *spectrumArtifacts) pipeline.S
} }
} }


func (rt *dspRuntime) detectDerivedCandidates(art *spectrumArtifacts, plan surveillancePlan) []pipeline.Candidate {
if art == nil || len(plan.LevelSet.Derived) == 0 {
return nil
}
spectra := map[string][]float64{}
for _, spec := range art.surveillanceSpectra {
if spec.Level.Name == "" || len(spec.Spectrum) == 0 {
continue
}
spectra[spec.Level.Name] = spec.Spectrum
}
if len(spectra) == 0 {
return nil
}
out := make([]pipeline.Candidate, 0, len(plan.LevelSet.Derived))
for _, level := range plan.LevelSet.Derived {
if level.Name == "" {
continue
}
spectrum := spectra[level.Name]
if len(spectrum) == 0 {
continue
}
entry := rt.derivedDetectorForLevel(level)
if entry == nil || entry.det == nil {
continue
}
_, signals := entry.det.Process(art.now, spectrum, level.CenterHz)
if len(signals) == 0 {
continue
}
cands := pipeline.CandidatesFromSignalsWithLevel(signals, "surveillance-derived", level)
for i := range cands {
if cands[i].ID == 0 {
continue
}
cands[i].ID = entry.idBase - cands[i].ID
}
out = append(out, cands...)
}
if len(out) == 0 {
return nil
}
return out
}

func (rt *dspRuntime) derivedDetectorForLevel(level pipeline.AnalysisLevel) *derivedDetector {
if level.SampleRate <= 0 || level.FFTSize <= 0 {
return nil
}
if rt.derivedDetectors == nil {
rt.derivedDetectors = map[string]*derivedDetector{}
}
key := level.Name
if key == "" {
key = fmt.Sprintf("%d:%d", level.SampleRate, level.FFTSize)
}
entry := rt.derivedDetectors[key]
if entry != nil && entry.sampleRate == level.SampleRate && entry.fftSize == level.FFTSize {
return entry
}
if rt.nextDerivedBase == 0 {
rt.nextDerivedBase = -derivedIDBlock
}
entry = &derivedDetector{
det: detector.New(rt.cfg.Detector, level.SampleRate, level.FFTSize),
sampleRate: level.SampleRate,
fftSize: level.FFTSize,
idBase: rt.nextDerivedBase,
}
rt.nextDerivedBase -= derivedIDBlock
rt.derivedDetectors[key] = entry
return entry
}

func (rt *dspRuntime) buildRefinementInput(surv pipeline.SurveillanceResult, now time.Time) pipeline.RefinementInput { func (rt *dspRuntime) buildRefinementInput(surv pipeline.SurveillanceResult, now time.Time) pipeline.RefinementInput {
policy := pipeline.PolicyFromConfig(rt.cfg) policy := pipeline.PolicyFromConfig(rt.cfg)
plan := pipeline.BuildRefinementPlan(surv.Candidates, policy) plan := pipeline.BuildRefinementPlan(surv.Candidates, policy)


+ 9
- 7
cmd/sdrd/types.go ファイルの表示

@@ -14,13 +14,15 @@ import (
) )


type SpectrumDebug struct { type SpectrumDebug struct {
Thresholds []float64 `json:"thresholds,omitempty"`
NoiseFloor float64 `json:"noise_floor,omitempty"`
Scores []map[string]any `json:"scores,omitempty"`
RefinementPlan *pipeline.RefinementPlan `json:"refinement_plan,omitempty"`
Windows *RefinementWindowStats `json:"refinement_windows,omitempty"`
Refinement *RefinementDebug `json:"refinement,omitempty"`
Decisions *DecisionDebug `json:"decisions,omitempty"`
Thresholds []float64 `json:"thresholds,omitempty"`
NoiseFloor float64 `json:"noise_floor,omitempty"`
Scores []map[string]any `json:"scores,omitempty"`
CandidateSources map[string]int `json:"candidate_sources,omitempty"`
CandidateEvidence []CandidateEvidenceSummary `json:"candidate_evidence,omitempty"`
RefinementPlan *pipeline.RefinementPlan `json:"refinement_plan,omitempty"`
Windows *RefinementWindowStats `json:"refinement_windows,omitempty"`
Refinement *RefinementDebug `json:"refinement,omitempty"`
Decisions *DecisionDebug `json:"decisions,omitempty"`
} }


type RefinementWindowStats struct { type RefinementWindowStats struct {


+ 124
- 0
internal/pipeline/candidate_fusion.go ファイルの表示

@@ -0,0 +1,124 @@
package pipeline

import "math"

func AddCandidateEvidence(candidate *Candidate, evidence LevelEvidence) {
if candidate == nil {
return
}
levelName := evidence.Level.Name
if levelName == "" {
levelName = "unknown"
}
for _, ev := range candidate.Evidence {
evLevel := ev.Level.Name
if evLevel == "" {
evLevel = "unknown"
}
if evLevel == levelName && ev.Provenance == evidence.Provenance {
return
}
}
candidate.Evidence = append(candidate.Evidence, evidence)
}

func MergeCandidateEvidence(dst *Candidate, src Candidate) {
if dst == nil || len(src.Evidence) == 0 {
return
}
for _, ev := range src.Evidence {
AddCandidateEvidence(dst, ev)
}
}

func CandidateEvidenceLevelCount(candidate Candidate) int {
if len(candidate.Evidence) == 0 {
return 0
}
levels := map[string]struct{}{}
for _, ev := range candidate.Evidence {
name := ev.Level.Name
if name == "" {
name = "unknown"
}
levels[name] = struct{}{}
}
return len(levels)
}

func FuseCandidates(primary []Candidate, derived []Candidate) []Candidate {
if len(primary) == 0 && len(derived) == 0 {
return nil
}
out := make([]Candidate, 0, len(primary)+len(derived))
out = append(out, primary...)
if len(derived) == 0 {
return out
}
used := make([]bool, len(derived))
for i := range out {
for j, cand := range derived {
if used[j] {
continue
}
if !candidatesOverlap(out[i], cand) {
continue
}
MergeCandidateEvidence(&out[i], cand)
used[j] = true
}
}
for j, cand := range derived {
if used[j] {
continue
}
out = append(out, cand)
}
return out
}

func candidatesOverlap(a Candidate, b Candidate) bool {
spanA := candidateSpanHz(a)
spanB := candidateSpanHz(b)
if spanA <= 0 {
spanA = 25000
}
if spanB <= 0 {
spanB = 25000
}
guard := 0.0
if binA, binB := candidateBinHz(a), candidateBinHz(b); binA > 0 || binB > 0 {
guard = 0.5 * math.Max(binA, binB)
}
leftA := a.CenterHz - spanA/2 - guard
rightA := a.CenterHz + spanA/2 + guard
leftB := b.CenterHz - spanB/2 - guard
rightB := b.CenterHz + spanB/2 + guard
return leftA <= rightB && leftB <= rightA
}

func candidateSpanHz(candidate Candidate) float64 {
if candidate.BandwidthHz > 0 {
return candidate.BandwidthHz
}
if candidate.LastBin < candidate.FirstBin {
return 0
}
binHz := candidateBinHz(candidate)
if binHz <= 0 {
return 0
}
return float64(candidate.LastBin-candidate.FirstBin+1) * binHz
}

func candidateBinHz(candidate Candidate) float64 {
for _, ev := range candidate.Evidence {
if ev.Level.BinHz > 0 {
return ev.Level.BinHz
}
if ev.Level.SampleRate > 0 && ev.Level.FFTSize > 0 {
return float64(ev.Level.SampleRate) / float64(ev.Level.FFTSize)
}
}
return 0
}

+ 14
- 0
internal/pipeline/scheduler.go ファイルの表示

@@ -16,6 +16,7 @@ type RefinementScoreModel struct {
SNRWeight float64 `json:"snr_weight"` SNRWeight float64 `json:"snr_weight"`
BandwidthWeight float64 `json:"bandwidth_weight"` BandwidthWeight float64 `json:"bandwidth_weight"`
PeakWeight float64 `json:"peak_weight"` PeakWeight float64 `json:"peak_weight"`
EvidenceWeight float64 `json:"evidence_weight"`
} }


type RefinementScoreDetails struct { type RefinementScoreDetails struct {
@@ -23,6 +24,7 @@ type RefinementScoreDetails struct {
BandwidthScore float64 `json:"bandwidth_score"` BandwidthScore float64 `json:"bandwidth_score"`
PeakScore float64 `json:"peak_score"` PeakScore float64 `json:"peak_score"`
PolicyBoost float64 `json:"policy_boost"` PolicyBoost float64 `json:"policy_boost"`
EvidenceScore float64 `json:"evidence_score"`
} }


type RefinementScore struct { type RefinementScore struct {
@@ -105,6 +107,7 @@ func BuildRefinementPlan(candidates []Candidate, policy Policy) RefinementPlan {
SNRWeight: snrWeight, SNRWeight: snrWeight,
BandwidthWeight: bwWeight, BandwidthWeight: bwWeight,
PeakWeight: peakWeight, PeakWeight: peakWeight,
EvidenceWeight: 0.6,
} }
scoreModel = applyStrategyWeights(strategy, scoreModel) scoreModel = applyStrategyWeights(strategy, scoreModel)
plan.ScoreModel = scoreModel plan.ScoreModel = scoreModel
@@ -139,7 +142,9 @@ func BuildRefinementPlan(candidates []Candidate, policy Policy) RefinementPlan {
if c.PeakDb > 0 { if c.PeakDb > 0 {
peakScore = (c.PeakDb / 20.0) * scoreModel.PeakWeight peakScore = (c.PeakDb / 20.0) * scoreModel.PeakWeight
} }
evidenceScore := candidateEvidenceScore(c) * scoreModel.EvidenceWeight
priority := snrScore + bwScore + peakScore + policyBoost priority := snrScore + bwScore + peakScore + policyBoost
priority += evidenceScore
score := &RefinementScore{ score := &RefinementScore{
Total: priority, Total: priority,
Breakdown: RefinementScoreDetails{ Breakdown: RefinementScoreDetails{
@@ -147,6 +152,7 @@ func BuildRefinementPlan(candidates []Candidate, policy Policy) RefinementPlan {
BandwidthScore: bwScore, BandwidthScore: bwScore,
PeakScore: peakScore, PeakScore: peakScore,
PolicyBoost: policyBoost, PolicyBoost: policyBoost,
EvidenceScore: evidenceScore,
}, },
Weights: &scoreModel, Weights: &scoreModel,
} }
@@ -244,6 +250,14 @@ func applyStrategyWeights(strategy string, model RefinementScoreModel) Refinemen
return model return model
} }


func candidateEvidenceScore(candidate Candidate) float64 {
levels := CandidateEvidenceLevelCount(candidate)
if levels <= 1 {
return 0
}
return float64(levels - 1)
}

func minFloat64(a, b float64) float64 { func minFloat64(a, b float64) float64 {
if a < b { if a < b {
return a return a


+ 1
- 1
internal/pipeline/types.go ファイルの表示

@@ -75,7 +75,7 @@ func CandidatesFromSignalsWithLevel(signals []detector.Signal, source string, le
} }
evidence := LevelEvidence{Level: level, Provenance: source} evidence := LevelEvidence{Level: level, Provenance: source}
for i := range out { for i := range out {
out[i].Evidence = append(out[i].Evidence, evidence)
AddCandidateEvidence(&out[i], evidence)
} }
return out return out
} }

読み込み中…
キャンセル
保存