diff --git a/cmd/sdrd/dsp_loop.go b/cmd/sdrd/dsp_loop.go index 3716a6f..ac41ed0 100644 --- a/cmd/sdrd/dsp_loop.go +++ b/cmd/sdrd/dsp_loop.go @@ -135,12 +135,16 @@ func runDSP(ctx context.Context, srcMgr *sourceManager, cfg config.Config, det * debugInfo = &SpectrumDebug{Thresholds: thresholds, NoiseFloor: noiseFloor, Scores: scoreDebug} candidateSources := buildCandidateSourceSummary(state.surveillance.Candidates) candidateEvidence := buildCandidateEvidenceSummary(state.surveillance.Candidates) + candidateEvidenceStates := buildCandidateEvidenceStateSummary(state.surveillance.Candidates) if len(candidateSources) > 0 { debugInfo.CandidateSources = candidateSources } if len(candidateEvidence) > 0 { debugInfo.CandidateEvidence = candidateEvidence } + if candidateEvidenceStates != nil { + debugInfo.CandidateEvidenceStates = candidateEvidenceStates + } if hasPlan { debugInfo.RefinementPlan = &plan } diff --git a/cmd/sdrd/http_handlers.go b/cmd/sdrd/http_handlers.go index a8eac35..1964a20 100644 --- a/cmd/sdrd/http_handlers.go +++ b/cmd/sdrd/http_handlers.go @@ -176,6 +176,7 @@ func registerAPIHandlers(mux *http.ServeMux, cfgPath string, cfgManager *runtime levelSummaries := buildSurveillanceLevelSummaries(levelSet, snap.surveillance.Spectra) candidateSources := buildCandidateSourceSummary(snap.surveillance.Candidates) candidateEvidence := buildCandidateEvidenceSummary(snap.surveillance.Candidates) + candidateEvidenceStates := buildCandidateEvidenceStateSummary(snap.surveillance.Candidates) out := map[string]any{ "plan": snap.refinement.Input.Plan, "windows": snap.refinement.Input.Windows, @@ -210,6 +211,7 @@ func registerAPIHandlers(mux *http.ServeMux, cfgPath string, cfgManager *runtime "surveillance_spectra_bins": spectraBins, "candidate_sources": candidateSources, "candidate_evidence": candidateEvidence, + "candidate_evidence_states": candidateEvidenceStates, "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 045ea0a..17e54f6 100644 --- a/cmd/sdrd/level_summary.go +++ b/cmd/sdrd/level_summary.go @@ -26,6 +26,15 @@ type CandidateEvidenceSummary struct { Count int `json:"count"` } +type CandidateEvidenceStateSummary struct { + Total int `json:"total"` + WithEvidence int `json:"with_evidence"` + Fused int `json:"fused"` + MultiLevelConfirmed int `json:"multi_level_confirmed"` + DerivedOnly int `json:"derived_only"` + PrimaryOnly int `json:"primary_only"` +} + func buildSurveillanceLevelSummaries(set pipeline.SurveillanceLevelSet, spectra []pipeline.SurveillanceLevelSpectrum) map[string]SurveillanceLevelSummary { if set.Primary.Name == "" && len(set.Derived) == 0 && set.Presentation.Name == "" && len(set.All) == 0 { return nil @@ -133,3 +142,33 @@ func buildCandidateEvidenceSummary(candidates []pipeline.Candidate) []CandidateE }) return out } + +func buildCandidateEvidenceStateSummary(candidates []pipeline.Candidate) *CandidateEvidenceStateSummary { + if len(candidates) == 0 { + return nil + } + summary := CandidateEvidenceStateSummary{Total: len(candidates)} + for _, cand := range candidates { + state := pipeline.CandidateEvidenceStateFor(cand) + if state.TotalLevelEntries == 0 { + continue + } + summary.WithEvidence++ + if state.Fused { + summary.Fused++ + } + if state.MultiLevelConfirmed { + summary.MultiLevelConfirmed++ + } + if state.DerivedOnly { + summary.DerivedOnly++ + } + if state.PrimaryLevelCount > 0 && state.DerivedLevelCount == 0 { + summary.PrimaryOnly++ + } + } + if summary.WithEvidence == 0 { + return nil + } + return &summary +} diff --git a/cmd/sdrd/pipeline_runtime.go b/cmd/sdrd/pipeline_runtime.go index e272acf..4b20658 100644 --- a/cmd/sdrd/pipeline_runtime.go +++ b/cmd/sdrd/pipeline_runtime.go @@ -476,6 +476,9 @@ func (rt *dspRuntime) detectDerivedCandidates(art *spectrumArtifacts, plan surve if level.Name == "" { continue } + if !pipeline.IsDetectionLevel(level) { + continue + } spectrum := spectra[level.Name] if len(spectrum) == 0 { continue diff --git a/cmd/sdrd/types.go b/cmd/sdrd/types.go index f964dfc..f02895f 100644 --- a/cmd/sdrd/types.go +++ b/cmd/sdrd/types.go @@ -14,15 +14,16 @@ import ( ) type SpectrumDebug struct { - 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"` + 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"` + CandidateEvidenceStates *CandidateEvidenceStateSummary `json:"candidate_evidence_states,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 {