Procházet zdrojové kódy

Consolidate monitor window summary in debug outputs

master
Jan Svabenik před 4 hodinami
rodič
revize
402a772fe3
9 změnil soubory, kde provedl 204 přidání a 9 odebrání
  1. +2
    -0
      cmd/sdrd/decision_compact.go
  2. +18
    -5
      cmd/sdrd/dsp_loop.go
  3. +15
    -2
      cmd/sdrd/http_handlers.go
  4. +2
    -0
      cmd/sdrd/types.go
  5. +113
    -0
      cmd/sdrd/window_summary.go
  6. +47
    -0
      cmd/sdrd/window_summary_test.go
  7. +2
    -0
      internal/pipeline/decisions.go
  8. +2
    -0
      internal/pipeline/scheduler.go
  9. +3
    -2
      web/app.js

+ 2
- 0
cmd/sdrd/decision_compact.go Zobrazit soubor

@@ -10,6 +10,7 @@ type compactDecision struct {
Reason string `json:"reason,omitempty"`
MonitorBias float64 `json:"monitor_bias,omitempty"`
MonitorDetail *pipeline.MonitorWindowMatch `json:"monitor_detail,omitempty"`
MonitorWindow *pipeline.MonitorWindowMatch `json:"monitor_window,omitempty"`
RecordWindow *pipeline.MonitorWindowMatch `json:"record_window,omitempty"`
DecodeWindow *pipeline.MonitorWindowMatch `json:"decode_window,omitempty"`
RecordAdmission *pipeline.PriorityAdmission `json:"record_admission,omitempty"`
@@ -28,6 +29,7 @@ func compactDecisions(decisions []pipeline.SignalDecision) []compactDecision {
Reason: d.Reason,
MonitorBias: d.MonitorBias,
MonitorDetail: d.MonitorDetail,
MonitorWindow: d.MonitorWindow,
RecordWindow: d.RecordWindow,
DecodeWindow: d.DecodeWindow,
RecordAdmission: d.RecordAdmission,


+ 18
- 5
cmd/sdrd/dsp_loop.go Zobrazit soubor

@@ -13,6 +13,7 @@ import (
"sdr-wideband-suite/internal/config"
"sdr-wideband-suite/internal/detector"
"sdr-wideband-suite/internal/dsp"
"sdr-wideband-suite/internal/pipeline"
"sdr-wideband-suite/internal/recorder"
)

@@ -110,7 +111,13 @@ func runDSP(ctx context.Context, srcMgr *sourceManager, cfg config.Config, det *
}
var debugInfo *SpectrumDebug
plan := state.refinement.Input.Plan
windowStats := buildWindowStats(state.refinement.Input.Windows)
windowSummary := buildWindowSummary(plan, state.refinement.Input.Windows, state.surveillance.Candidates)
var windowStats *RefinementWindowStats
var monitorSummary []pipeline.MonitorWindowStats
if windowSummary != nil {
windowStats = windowSummary.Refinement
monitorSummary = windowSummary.MonitorWindows
}
hasPlan := plan.TotalCandidates > 0 || plan.Budget > 0 || plan.DroppedBySNR > 0 || plan.DroppedByBudget > 0
hasWindows := windowStats != nil && windowStats.Count > 0
if len(thresholds) > 0 || len(displaySignals) > 0 || noiseFloor != 0 || hasPlan || hasWindows {
@@ -149,8 +156,11 @@ func runDSP(ctx context.Context, srcMgr *sourceManager, cfg config.Config, det *
if len(candidateWindows) > 0 {
debugInfo.CandidateWindows = candidateWindows
}
if len(plan.MonitorWindowStats) > 0 {
debugInfo.MonitorWindowStats = plan.MonitorWindowStats
if len(monitorSummary) > 0 {
debugInfo.MonitorWindowStats = monitorSummary
}
if windowSummary != nil {
debugInfo.WindowSummary = windowSummary
}
if hasPlan {
debugInfo.RefinementPlan = &plan
@@ -167,8 +177,11 @@ func runDSP(ctx context.Context, srcMgr *sourceManager, cfg config.Config, det *
if hasWindows {
refinementDebug.Windows = windowStats
}
if len(plan.MonitorWindowStats) > 0 {
refinementDebug.MonitorWindowStats = plan.MonitorWindowStats
if len(monitorSummary) > 0 {
refinementDebug.MonitorWindowStats = monitorSummary
}
if windowSummary != nil {
refinementDebug.WindowSummary = windowSummary
}
refinementDebug.Arbitration = buildArbitrationSnapshot(state.refinement, state.arbitration)
debugInfo.Refinement = refinementDebug


+ 15
- 2
cmd/sdrd/http_handlers.go Zobrazit soubor

@@ -165,7 +165,19 @@ func registerAPIHandlers(mux *http.ServeMux, cfgPath string, cfgManager *runtime
mux.HandleFunc("/api/refinement", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
snap := phaseSnap.Snapshot()
windowStats := buildWindowStats(snap.refinement.Input.Windows)
windowSummary := buildWindowSummary(snap.refinement.Input.Plan, snap.refinement.Input.Windows, snap.surveillance.Candidates)
var windowStats *RefinementWindowStats
var monitorSummary []pipeline.MonitorWindowStats
if windowSummary != nil {
windowStats = windowSummary.Refinement
monitorSummary = windowSummary.MonitorWindows
}
if windowStats == nil {
windowStats = buildWindowStats(snap.refinement.Input.Windows)
}
if len(monitorSummary) == 0 && len(snap.refinement.Input.Plan.MonitorWindowStats) > 0 {
monitorSummary = snap.refinement.Input.Plan.MonitorWindowStats
}
arbitration := buildArbitrationSnapshot(snap.refinement, snap.arbitration)
levelSet := snap.surveillance.LevelSet
spectraBins := map[string]int{}
@@ -184,6 +196,7 @@ func registerAPIHandlers(mux *http.ServeMux, cfgPath string, cfgManager *runtime
"plan": snap.refinement.Input.Plan,
"windows": snap.refinement.Input.Windows,
"window_stats": windowStats,
"window_summary": windowSummary,
"request": snap.refinement.Input.Request,
"context": snap.refinement.Input.Context,
"detail_level": snap.refinement.Input.Detail,
@@ -220,7 +233,7 @@ func registerAPIHandlers(mux *http.ServeMux, cfgPath string, cfgManager *runtime
"candidate_evidence_states": candidateEvidenceStates,
"candidate_windows": candidateWindows,
"monitor_windows": snap.refinement.Input.Plan.MonitorWindows,
"monitor_window_stats": snap.refinement.Input.Plan.MonitorWindowStats,
"monitor_window_stats": monitorSummary,
"display_level": snap.surveillance.DisplayLevel,
"refinement_level": snap.refinement.Input.Level,
"presentation_level": snap.presentation,


+ 2
- 0
cmd/sdrd/types.go Zobrazit soubor

@@ -22,6 +22,7 @@ type SpectrumDebug struct {
CandidateEvidenceStates *CandidateEvidenceStateSummary `json:"candidate_evidence_states,omitempty"`
CandidateWindows []CandidateWindowSummary `json:"candidate_windows,omitempty"`
MonitorWindowStats []pipeline.MonitorWindowStats `json:"monitor_window_stats,omitempty"`
WindowSummary *WindowSummary `json:"window_summary,omitempty"`
RefinementPlan *pipeline.RefinementPlan `json:"refinement_plan,omitempty"`
Windows *RefinementWindowStats `json:"refinement_windows,omitempty"`
Refinement *RefinementDebug `json:"refinement,omitempty"`
@@ -42,6 +43,7 @@ type RefinementDebug struct {
WorkItems []pipeline.RefinementWorkItem `json:"work_items,omitempty"`
Windows *RefinementWindowStats `json:"windows,omitempty"`
MonitorWindowStats []pipeline.MonitorWindowStats `json:"monitor_window_stats,omitempty"`
WindowSummary *WindowSummary `json:"window_summary,omitempty"`
Arbitration *ArbitrationSnapshot `json:"arbitration,omitempty"`
}



+ 113
- 0
cmd/sdrd/window_summary.go Zobrazit soubor

@@ -0,0 +1,113 @@
package main

import (
"sort"

"sdr-wideband-suite/internal/pipeline"
)

type WindowSummary struct {
Refinement *RefinementWindowStats `json:"refinement,omitempty"`
MonitorWindows []pipeline.MonitorWindowStats `json:"monitor_windows,omitempty"`
}

func buildWindowSummary(plan pipeline.RefinementPlan, refinementWindows []pipeline.RefinementWindow, candidates []pipeline.Candidate) *WindowSummary {
refinementStats := buildWindowStats(refinementWindows)
monitorSummary := buildMonitorWindowSummary(plan.MonitorWindows, plan.MonitorWindowStats, candidates)
if refinementStats == nil && len(monitorSummary) == 0 {
return nil
}
return &WindowSummary{
Refinement: refinementStats,
MonitorWindows: monitorSummary,
}
}

func buildMonitorWindowSummary(windows []pipeline.MonitorWindow, stats []pipeline.MonitorWindowStats, candidates []pipeline.Candidate) []pipeline.MonitorWindowStats {
var summary []pipeline.MonitorWindowStats
switch {
case len(stats) > 0:
summary = append([]pipeline.MonitorWindowStats(nil), stats...)
case len(windows) > 0:
summary = make([]pipeline.MonitorWindowStats, 0, len(windows))
for _, win := range windows {
summary = append(summary, pipeline.MonitorWindowStats{
Index: win.Index,
Label: win.Label,
Zone: win.Zone,
Source: win.Source,
StartHz: win.StartHz,
EndHz: win.EndHz,
CenterHz: win.CenterHz,
SpanHz: win.SpanHz,
Priority: win.Priority,
PriorityBias: win.PriorityBias,
RecordBias: win.RecordBias,
DecodeBias: win.DecodeBias,
AutoRecord: win.AutoRecord,
AutoDecode: win.AutoDecode,
})
}
default:
return nil
}

if len(candidates) > 0 && len(summary) > 0 {
windowsForMatch := windows
if len(windowsForMatch) == 0 {
windowsForMatch = monitorWindowsFromStats(summary)
}
if len(windowsForMatch) > 0 {
counts := map[int]int{}
total := 0
for _, cand := range candidates {
matches := cand.MonitorMatches
if len(matches) == 0 {
matches = pipeline.MonitorWindowMatchesForCandidate(windowsForMatch, cand)
}
for _, match := range matches {
counts[match.Index]++
total++
}
}
if total > 0 {
for i := range summary {
if summary[i].Candidates == 0 {
summary[i].Candidates = counts[summary[i].Index]
}
}
}
}
}

sort.Slice(summary, func(i, j int) bool {
return summary[i].Index < summary[j].Index
})
return summary
}

func monitorWindowsFromStats(stats []pipeline.MonitorWindowStats) []pipeline.MonitorWindow {
if len(stats) == 0 {
return nil
}
windows := make([]pipeline.MonitorWindow, 0, len(stats))
for _, stat := range stats {
windows = append(windows, pipeline.MonitorWindow{
Index: stat.Index,
Label: stat.Label,
Zone: stat.Zone,
Source: stat.Source,
StartHz: stat.StartHz,
EndHz: stat.EndHz,
CenterHz: stat.CenterHz,
SpanHz: stat.SpanHz,
Priority: stat.Priority,
PriorityBias: stat.PriorityBias,
RecordBias: stat.RecordBias,
DecodeBias: stat.DecodeBias,
AutoRecord: stat.AutoRecord,
AutoDecode: stat.AutoDecode,
})
}
return windows
}

+ 47
- 0
cmd/sdrd/window_summary_test.go Zobrazit soubor

@@ -0,0 +1,47 @@
package main

import (
"testing"

"sdr-wideband-suite/internal/pipeline"
)

func TestBuildMonitorWindowSummaryCountsCandidates(t *testing.T) {
windows := []pipeline.MonitorWindow{
{Index: 0, Label: "primary", StartHz: 100, EndHz: 200, CenterHz: 150, SpanHz: 100},
{Index: 1, Label: "secondary", StartHz: 300, EndHz: 400, CenterHz: 350, SpanHz: 100},
}
candidates := []pipeline.Candidate{
{ID: 1, CenterHz: 150, BandwidthHz: 20},
{ID: 2, CenterHz: 320, BandwidthHz: 10},
}
summary := buildMonitorWindowSummary(windows, nil, candidates)
if len(summary) != 2 {
t.Fatalf("expected 2 window summaries, got %d", len(summary))
}
if summary[0].Candidates != 1 || summary[1].Candidates != 1 {
t.Fatalf("unexpected candidate counts: %+v", summary)
}
}

func TestBuildMonitorWindowSummaryPreservesStatsCounts(t *testing.T) {
stats := []pipeline.MonitorWindowStats{
{Index: 0, Label: "primary", StartHz: 100, EndHz: 200, CenterHz: 150, SpanHz: 100, Candidates: 2, Planned: 1},
}
windows := []pipeline.MonitorWindow{
{Index: 0, Label: "primary", StartHz: 100, EndHz: 200, CenterHz: 150, SpanHz: 100},
}
candidates := []pipeline.Candidate{
{ID: 1, CenterHz: 150, BandwidthHz: 20},
}
summary := buildMonitorWindowSummary(windows, stats, candidates)
if len(summary) != 1 {
t.Fatalf("expected 1 window summary, got %d", len(summary))
}
if summary[0].Candidates != 2 {
t.Fatalf("expected candidates to stay at 2, got %d", summary[0].Candidates)
}
if summary[0].Planned != 1 {
t.Fatalf("expected planned to stay at 1, got %d", summary[0].Planned)
}
}

+ 2
- 0
internal/pipeline/decisions.go Zobrazit soubor

@@ -16,6 +16,7 @@ type SignalDecision struct {
RecordBias float64 `json:"record_bias,omitempty"`
DecodeBias float64 `json:"decode_bias,omitempty"`
MonitorDetail *MonitorWindowMatch `json:"monitor_detail,omitempty"`
MonitorWindow *MonitorWindowMatch `json:"monitor_window,omitempty"`
RecordWindow *MonitorWindowMatch `json:"record_window,omitempty"`
DecodeWindow *MonitorWindowMatch `json:"decode_window,omitempty"`
RecordAdmission *PriorityAdmission `json:"record_admission,omitempty"`
@@ -89,6 +90,7 @@ func DecideSignalAction(policy Policy, candidate Candidate, cls *classifier.Clas
}
if monitorDetail != nil {
decision.MonitorDetail = monitorDetail
decision.MonitorWindow = monitorDetail
}
return decision
}


+ 2
- 0
internal/pipeline/scheduler.go Zobrazit soubor

@@ -30,6 +30,7 @@ type RefinementScoreDetails struct {
PolicyBoost float64 `json:"policy_boost"`
MonitorBias float64 `json:"monitor_bias,omitempty"`
MonitorDetail *MonitorWindowMatch `json:"monitor_detail,omitempty"`
MonitorWindow *MonitorWindowMatch `json:"monitor_window,omitempty"`
EvidenceScore float64 `json:"evidence_score"`
EvidenceDetail *EvidenceScoreDetails `json:"evidence_detail,omitempty"`
}
@@ -201,6 +202,7 @@ func BuildRefinementPlanWithBudget(candidates []Candidate, policy Policy, budget
PolicyBoost: policyBoost,
MonitorBias: monitorBias,
MonitorDetail: monitorDetail,
MonitorWindow: monitorDetail,
EvidenceScore: evidenceScore,
EvidenceDetail: &evidenceDetail,
},


+ 3
- 2
web/app.js Zobrazit soubor

@@ -849,7 +849,8 @@ function updateHeroMetrics() {
? `CFAR ${showDebugOverlay ? 'on' : 'hidden'} · noise ${(Number.isFinite(debug.noise_floor) ? debug.noise_floor.toFixed(1) : 'n/a')} dB`
: `CFAR off · noise ${(Number.isFinite(debug.noise_floor) ? debug.noise_floor.toFixed(1) : 'n/a')} dB`;
const plan = debug.refinement_plan || null;
const windows = debug.refinement_windows || null;
const windowSummary = debug.window_summary || null;
const windows = (windowSummary && windowSummary.refinement) || debug.refinement_windows || null;
const refineInfo = plan && showDebugOverlay
? `refine ${plan.selected?.length || 0}/${plan.budget || 0} drop ${plan.dropped_by_snr || 0}/${plan.dropped_by_budget || 0}`
: '';
@@ -881,7 +882,7 @@ function updateHeroMetrics() {
healthRefinePlan.textContent = `${plan.selected?.length || 0}/${plan.budget || 0} · drop ${plan.dropped_by_snr || 0}/${plan.dropped_by_budget || 0} · rec ${recOn} dec ${decOn} ${queueText} ${reasonText}`;
}
if (healthRefineWindows) {
const stats = refinementInfo.window_stats || null;
const stats = refinementInfo.window_summary?.refinement || refinementInfo.window_stats || null;
if (stats && stats.count) {
const levelSet = refinementInfo.surveillance_level_set || {};
const primary = levelSet.primary || refinementInfo.surveillance_level;


Načítá se…
Zrušit
Uložit