diff --git a/cmd/sdrd/dsp_loop.go b/cmd/sdrd/dsp_loop.go index ac41ed0..31a5a50 100644 --- a/cmd/sdrd/dsp_loop.go +++ b/cmd/sdrd/dsp_loop.go @@ -136,6 +136,7 @@ func runDSP(ctx context.Context, srcMgr *sourceManager, cfg config.Config, det * candidateSources := buildCandidateSourceSummary(state.surveillance.Candidates) candidateEvidence := buildCandidateEvidenceSummary(state.surveillance.Candidates) candidateEvidenceStates := buildCandidateEvidenceStateSummary(state.surveillance.Candidates) + candidateWindows := buildCandidateWindowSummary(state.surveillance.Candidates, plan.MonitorWindows) if len(candidateSources) > 0 { debugInfo.CandidateSources = candidateSources } @@ -145,6 +146,12 @@ func runDSP(ctx context.Context, srcMgr *sourceManager, cfg config.Config, det * if candidateEvidenceStates != nil { debugInfo.CandidateEvidenceStates = candidateEvidenceStates } + if len(candidateWindows) > 0 { + debugInfo.CandidateWindows = candidateWindows + } + if len(plan.MonitorWindowStats) > 0 { + debugInfo.MonitorWindowStats = plan.MonitorWindowStats + } if hasPlan { debugInfo.RefinementPlan = &plan } @@ -160,6 +167,9 @@ 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 + } refinementDebug.Arbitration = buildArbitrationSnapshot(state.refinement, state.arbitration) debugInfo.Refinement = refinementDebug } diff --git a/cmd/sdrd/http_handlers.go b/cmd/sdrd/http_handlers.go index 15e7073..d4d6033 100644 --- a/cmd/sdrd/http_handlers.go +++ b/cmd/sdrd/http_handlers.go @@ -179,6 +179,7 @@ func registerAPIHandlers(mux *http.ServeMux, cfgPath string, cfgManager *runtime candidateSources := buildCandidateSourceSummary(snap.surveillance.Candidates) candidateEvidence := buildCandidateEvidenceSummary(snap.surveillance.Candidates) candidateEvidenceStates := buildCandidateEvidenceStateSummary(snap.surveillance.Candidates) + candidateWindows := buildCandidateWindowSummary(snap.surveillance.Candidates, snap.refinement.Input.Plan.MonitorWindows) out := map[string]any{ "plan": snap.refinement.Input.Plan, "windows": snap.refinement.Input.Windows, @@ -217,6 +218,9 @@ func registerAPIHandlers(mux *http.ServeMux, cfgPath string, cfgManager *runtime "candidate_sources": candidateSources, "candidate_evidence": candidateEvidence, "candidate_evidence_states": candidateEvidenceStates, + "candidate_windows": candidateWindows, + "monitor_windows": snap.refinement.Input.Plan.MonitorWindows, + "monitor_window_stats": snap.refinement.Input.Plan.MonitorWindowStats, "display_level": snap.surveillance.DisplayLevel, "refinement_level": snap.refinement.Input.Level, "presentation_level": snap.presentation, diff --git a/cmd/sdrd/level_summary.go b/cmd/sdrd/level_summary.go index 62005d7..7afa96e 100644 --- a/cmd/sdrd/level_summary.go +++ b/cmd/sdrd/level_summary.go @@ -43,6 +43,18 @@ type CandidateEvidenceStateSummary struct { PrimaryOnly int `json:"primary_only"` } +type CandidateWindowSummary struct { + Index int `json:"index"` + Label string `json:"label,omitempty"` + Source string `json:"source,omitempty"` + StartHz float64 `json:"start_hz,omitempty"` + EndHz float64 `json:"end_hz,omitempty"` + CenterHz float64 `json:"center_hz,omitempty"` + SpanHz float64 `json:"span_hz,omitempty"` + PriorityBias float64 `json:"priority_bias,omitempty"` + Candidates int `json:"candidates"` +} + func buildSurveillanceLevelSummaries(set pipeline.SurveillanceLevelSet, spectra []pipeline.SurveillanceLevelSpectrum) map[string]SurveillanceLevelSummary { if set.Primary.Name == "" && len(set.Derived) == 0 && len(set.Support) == 0 && set.Presentation.Name == "" && len(set.All) == 0 { return nil @@ -205,6 +217,50 @@ func buildCandidateEvidenceStateSummary(candidates []pipeline.Candidate) *Candid return &summary } +func buildCandidateWindowSummary(candidates []pipeline.Candidate, windows []pipeline.MonitorWindow) []CandidateWindowSummary { + if len(windows) == 0 { + return nil + } + out := make([]CandidateWindowSummary, 0, len(windows)) + index := make(map[int]int, len(windows)) + for _, win := range windows { + entry := CandidateWindowSummary{ + Index: win.Index, + Label: win.Label, + Source: win.Source, + StartHz: win.StartHz, + EndHz: win.EndHz, + CenterHz: win.CenterHz, + SpanHz: win.SpanHz, + PriorityBias: win.PriorityBias, + } + index[win.Index] = len(out) + out = append(out, entry) + } + totalCandidates := 0 + for _, cand := range candidates { + matches := cand.MonitorMatches + if len(matches) == 0 { + matches = pipeline.MonitorWindowMatchesForCandidate(windows, cand) + } + for _, match := range matches { + idx, ok := index[match.Index] + if !ok { + continue + } + out[idx].Candidates++ + totalCandidates++ + } + } + if totalCandidates == 0 { + return nil + } + sort.Slice(out, func(i, j int) bool { + return out[i].Index < out[j].Index + }) + return out +} + func evidenceKind(level pipeline.AnalysisLevel) string { if pipeline.IsPresentationLevel(level) { return "presentation" diff --git a/cmd/sdrd/types.go b/cmd/sdrd/types.go index aaf1098..c0655ce 100644 --- a/cmd/sdrd/types.go +++ b/cmd/sdrd/types.go @@ -20,6 +20,8 @@ type SpectrumDebug struct { CandidateSources map[string]int `json:"candidate_sources,omitempty"` CandidateEvidence []CandidateEvidenceSummary `json:"candidate_evidence,omitempty"` CandidateEvidenceStates *CandidateEvidenceStateSummary `json:"candidate_evidence_states,omitempty"` + CandidateWindows []CandidateWindowSummary `json:"candidate_windows,omitempty"` + MonitorWindowStats []pipeline.MonitorWindowStats `json:"monitor_window_stats,omitempty"` RefinementPlan *pipeline.RefinementPlan `json:"refinement_plan,omitempty"` Windows *RefinementWindowStats `json:"refinement_windows,omitempty"` Refinement *RefinementDebug `json:"refinement,omitempty"` @@ -35,11 +37,12 @@ type RefinementWindowStats struct { } type RefinementDebug struct { - Plan *pipeline.RefinementPlan `json:"plan,omitempty"` - Request *pipeline.RefinementRequest `json:"request,omitempty"` - WorkItems []pipeline.RefinementWorkItem `json:"work_items,omitempty"` - Windows *RefinementWindowStats `json:"windows,omitempty"` - Arbitration *ArbitrationSnapshot `json:"arbitration,omitempty"` + Plan *pipeline.RefinementPlan `json:"plan,omitempty"` + Request *pipeline.RefinementRequest `json:"request,omitempty"` + WorkItems []pipeline.RefinementWorkItem `json:"work_items,omitempty"` + Windows *RefinementWindowStats `json:"windows,omitempty"` + MonitorWindowStats []pipeline.MonitorWindowStats `json:"monitor_window_stats,omitempty"` + Arbitration *ArbitrationSnapshot `json:"arbitration,omitempty"` } type DecisionDebug struct {