| @@ -143,6 +143,7 @@ func registerAPIHandlers(mux *http.ServeMux, cfgPath string, cfgManager *runtime | |||
| "mode": policy.Mode, | |||
| "intent": policy.Intent, | |||
| "surveillance_strategy": policy.SurveillanceStrategy, | |||
| "surveillance_detection": policy.SurveillanceDetection, | |||
| "refinement_strategy": policy.RefinementStrategy, | |||
| "monitor_center_hz": policy.MonitorCenterHz, | |||
| "monitor_start_hz": policy.MonitorStartHz, | |||
| @@ -178,21 +179,24 @@ func registerAPIHandlers(mux *http.ServeMux, cfgPath string, cfgManager *runtime | |||
| candidateEvidence := buildCandidateEvidenceSummary(snap.surveillance.Candidates) | |||
| candidateEvidenceStates := buildCandidateEvidenceStateSummary(snap.surveillance.Candidates) | |||
| out := map[string]any{ | |||
| "plan": snap.refinement.Input.Plan, | |||
| "windows": snap.refinement.Input.Windows, | |||
| "window_stats": windowStats, | |||
| "request": snap.refinement.Input.Request, | |||
| "context": snap.refinement.Input.Context, | |||
| "detail_level": snap.refinement.Input.Detail, | |||
| "arbitration": arbitration, | |||
| "work_items": snap.refinement.Input.WorkItems, | |||
| "candidates": len(snap.refinement.Input.Candidates), | |||
| "scheduled": len(snap.refinement.Input.Scheduled), | |||
| "signals": len(snap.refinement.Result.Signals), | |||
| "decisions": len(snap.refinement.Result.Decisions), | |||
| "surveillance_level": snap.surveillance.Level, | |||
| "surveillance_levels": snap.surveillance.Levels, | |||
| "surveillance_level_set": levelSet, | |||
| "plan": snap.refinement.Input.Plan, | |||
| "windows": snap.refinement.Input.Windows, | |||
| "window_stats": windowStats, | |||
| "request": snap.refinement.Input.Request, | |||
| "context": snap.refinement.Input.Context, | |||
| "detail_level": snap.refinement.Input.Detail, | |||
| "arbitration": arbitration, | |||
| "work_items": snap.refinement.Input.WorkItems, | |||
| "candidates": len(snap.refinement.Input.Candidates), | |||
| "scheduled": len(snap.refinement.Input.Scheduled), | |||
| "signals": len(snap.refinement.Result.Signals), | |||
| "decisions": len(snap.refinement.Result.Decisions), | |||
| "surveillance_level": snap.surveillance.Level, | |||
| "surveillance_levels": snap.surveillance.Levels, | |||
| "surveillance_level_set": levelSet, | |||
| "surveillance_detection_policy": snap.surveillance.DetectionPolicy, | |||
| "surveillance_detection_levels": levelSet.Detection, | |||
| "surveillance_support_levels": levelSet.Support, | |||
| "surveillance_active_levels": func() []pipeline.AnalysisLevel { | |||
| if len(levelSet.All) > 0 { | |||
| return levelSet.All | |||
| @@ -36,7 +36,7 @@ type CandidateEvidenceStateSummary struct { | |||
| } | |||
| 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 { | |||
| if set.Primary.Name == "" && len(set.Derived) == 0 && len(set.Support) == 0 && set.Presentation.Name == "" && len(set.All) == 0 { | |||
| return nil | |||
| } | |||
| bins := map[string]int{} | |||
| @@ -54,6 +54,9 @@ func buildSurveillanceLevelSummaries(set pipeline.SurveillanceLevelSet, spectra | |||
| if len(set.Derived) > 0 { | |||
| levels = append(levels, set.Derived...) | |||
| } | |||
| if len(set.Support) > 0 { | |||
| levels = append(levels, set.Support...) | |||
| } | |||
| if set.Presentation.Name != "" { | |||
| levels = append(levels, set.Presentation) | |||
| } | |||
| @@ -82,12 +82,13 @@ type surveillanceLevelSpec struct { | |||
| } | |||
| type surveillancePlan struct { | |||
| Primary pipeline.AnalysisLevel | |||
| Levels []pipeline.AnalysisLevel | |||
| LevelSet pipeline.SurveillanceLevelSet | |||
| Presentation pipeline.AnalysisLevel | |||
| Context pipeline.AnalysisContext | |||
| Specs []surveillanceLevelSpec | |||
| Primary pipeline.AnalysisLevel | |||
| Levels []pipeline.AnalysisLevel | |||
| LevelSet pipeline.SurveillanceLevelSet | |||
| Presentation pipeline.AnalysisLevel | |||
| Context pipeline.AnalysisContext | |||
| DetectionPolicy pipeline.SurveillanceDetectionPolicy | |||
| Specs []surveillanceLevelSpec | |||
| } | |||
| const derivedIDBlock = int64(1_000_000_000) | |||
| @@ -442,18 +443,19 @@ func (rt *dspRuntime) buildSurveillanceResult(art *spectrumArtifacts) pipeline.S | |||
| candidates := pipeline.FuseCandidates(primaryCandidates, derivedCandidates) | |||
| scheduled := pipeline.ScheduleCandidates(candidates, policy) | |||
| return pipeline.SurveillanceResult{ | |||
| Level: plan.Primary, | |||
| Levels: plan.Levels, | |||
| LevelSet: plan.LevelSet, | |||
| DisplayLevel: plan.Presentation, | |||
| Context: plan.Context, | |||
| Spectra: art.surveillanceSpectra, | |||
| Candidates: candidates, | |||
| Scheduled: scheduled, | |||
| Finished: art.finished, | |||
| Signals: art.detected, | |||
| NoiseFloor: art.noiseFloor, | |||
| Thresholds: art.thresholds, | |||
| Level: plan.Primary, | |||
| Levels: plan.Levels, | |||
| LevelSet: plan.LevelSet, | |||
| DetectionPolicy: plan.DetectionPolicy, | |||
| DisplayLevel: plan.Presentation, | |||
| Context: plan.Context, | |||
| Spectra: art.surveillanceSpectra, | |||
| Candidates: candidates, | |||
| Scheduled: scheduled, | |||
| Finished: art.finished, | |||
| Signals: art.detected, | |||
| NoiseFloor: art.noiseFloor, | |||
| Thresholds: art.thresholds, | |||
| } | |||
| } | |||
| @@ -862,11 +864,13 @@ func (rt *dspRuntime) buildSurveillancePlan(policy pipeline.Policy) surveillance | |||
| baseFFT = rt.cfg.FFTSize | |||
| } | |||
| span := spanForPolicy(policy, float64(baseRate)) | |||
| primary := analysisLevel("surveillance", "surveillance", "surveillance", baseRate, baseFFT, rt.cfg.CenterHz, span, "baseband", 1, baseRate) | |||
| detectionPolicy := pipeline.SurveillanceDetectionPolicyFromPolicy(policy) | |||
| primary := analysisLevel("surveillance", pipeline.RoleSurveillancePrimary, "surveillance", baseRate, baseFFT, rt.cfg.CenterHz, span, "baseband", 1, baseRate) | |||
| levels := []pipeline.AnalysisLevel{primary} | |||
| specs := []surveillanceLevelSpec{{Level: primary, Decim: 1, AllowGPU: true}} | |||
| context := pipeline.AnalysisContext{Surveillance: primary} | |||
| derivedLevels := make([]pipeline.AnalysisLevel, 0, 2) | |||
| supportLevels := make([]pipeline.AnalysisLevel, 0, 2) | |||
| strategy := strings.ToLower(strings.TrimSpace(policy.SurveillanceStrategy)) | |||
| switch strategy { | |||
| @@ -876,36 +880,51 @@ func (rt *dspRuntime) buildSurveillancePlan(policy pipeline.Policy) surveillance | |||
| derivedFFT := baseFFT / decim | |||
| if derivedRate >= 200000 && derivedFFT >= 256 { | |||
| derivedSpan := spanForPolicy(policy, float64(derivedRate)) | |||
| derived := analysisLevel("surveillance-lowres", "surveillance-lowres", "surveillance", derivedRate, derivedFFT, rt.cfg.CenterHz, derivedSpan, "decimated", decim, baseRate) | |||
| levels = append(levels, derived) | |||
| role := pipeline.RoleSurveillanceSupport | |||
| if detectionPolicy.DerivedDetectionEnabled { | |||
| role = pipeline.RoleSurveillanceDerived | |||
| } | |||
| derived := analysisLevel("surveillance-lowres", role, "surveillance", derivedRate, derivedFFT, rt.cfg.CenterHz, derivedSpan, "decimated", decim, baseRate) | |||
| if detectionPolicy.DerivedDetectionEnabled { | |||
| levels = append(levels, derived) | |||
| derivedLevels = append(derivedLevels, derived) | |||
| } else { | |||
| supportLevels = append(supportLevels, derived) | |||
| } | |||
| specs = append(specs, surveillanceLevelSpec{Level: derived, Decim: decim, AllowGPU: false}) | |||
| context.Derived = append(context.Derived, derived) | |||
| derivedLevels = append(derivedLevels, derived) | |||
| } | |||
| } | |||
| presentation := analysisLevel("presentation", "presentation", "presentation", baseRate, rt.cfg.Surveillance.DisplayBins, rt.cfg.CenterHz, span, "display", 1, baseRate) | |||
| presentation := analysisLevel("presentation", pipeline.RolePresentation, "presentation", baseRate, rt.cfg.Surveillance.DisplayBins, rt.cfg.CenterHz, span, "display", 1, baseRate) | |||
| context.Presentation = presentation | |||
| levelSet := pipeline.SurveillanceLevelSet{ | |||
| Primary: primary, | |||
| Derived: append([]pipeline.AnalysisLevel(nil), derivedLevels...), | |||
| Support: append([]pipeline.AnalysisLevel(nil), supportLevels...), | |||
| Presentation: presentation, | |||
| } | |||
| allLevels := make([]pipeline.AnalysisLevel, 0, 1+len(derivedLevels)+1) | |||
| detectionLevels := make([]pipeline.AnalysisLevel, 0, 1+len(derivedLevels)) | |||
| detectionLevels = append(detectionLevels, primary) | |||
| detectionLevels = append(detectionLevels, derivedLevels...) | |||
| levelSet.Detection = detectionLevels | |||
| allLevels := make([]pipeline.AnalysisLevel, 0, 1+len(derivedLevels)+len(supportLevels)+1) | |||
| allLevels = append(allLevels, primary) | |||
| allLevels = append(allLevels, derivedLevels...) | |||
| allLevels = append(allLevels, supportLevels...) | |||
| if presentation.Name != "" { | |||
| allLevels = append(allLevels, presentation) | |||
| } | |||
| levelSet.All = allLevels | |||
| return surveillancePlan{ | |||
| Primary: primary, | |||
| Levels: levels, | |||
| LevelSet: levelSet, | |||
| Presentation: presentation, | |||
| Context: context, | |||
| Specs: specs, | |||
| Primary: primary, | |||
| Levels: levels, | |||
| LevelSet: levelSet, | |||
| Presentation: presentation, | |||
| Context: context, | |||
| DetectionPolicy: detectionPolicy, | |||
| Specs: specs, | |||
| } | |||
| } | |||
| @@ -3,6 +3,7 @@ package config | |||
| import ( | |||
| "math" | |||
| "os" | |||
| "strings" | |||
| "time" | |||
| "gopkg.in/yaml.v3" | |||
| @@ -87,11 +88,12 @@ type PipelineConfig struct { | |||
| } | |||
| type SurveillanceConfig struct { | |||
| AnalysisFFTSize int `yaml:"analysis_fft_size" json:"analysis_fft_size"` | |||
| FrameRate int `yaml:"frame_rate" json:"frame_rate"` | |||
| Strategy string `yaml:"strategy" json:"strategy"` | |||
| DisplayBins int `yaml:"display_bins" json:"display_bins"` | |||
| DisplayFPS int `yaml:"display_fps" json:"display_fps"` | |||
| AnalysisFFTSize int `yaml:"analysis_fft_size" json:"analysis_fft_size"` | |||
| FrameRate int `yaml:"frame_rate" json:"frame_rate"` | |||
| Strategy string `yaml:"strategy" json:"strategy"` | |||
| DisplayBins int `yaml:"display_bins" json:"display_bins"` | |||
| DisplayFPS int `yaml:"display_fps" json:"display_fps"` | |||
| DerivedDetection string `yaml:"derived_detection" json:"derived_detection"` | |||
| } | |||
| type RefinementConfig struct { | |||
| @@ -170,11 +172,12 @@ func Default() Config { | |||
| }, | |||
| }, | |||
| Surveillance: SurveillanceConfig{ | |||
| AnalysisFFTSize: 2048, | |||
| FrameRate: 15, | |||
| Strategy: "single-resolution", | |||
| DisplayBins: 2048, | |||
| DisplayFPS: 15, | |||
| AnalysisFFTSize: 2048, | |||
| FrameRate: 15, | |||
| Strategy: "single-resolution", | |||
| DisplayBins: 2048, | |||
| DisplayFPS: 15, | |||
| DerivedDetection: "auto", | |||
| }, | |||
| Refinement: RefinementConfig{ | |||
| Enabled: true, | |||
| @@ -198,11 +201,12 @@ func Default() Config { | |||
| Description: "Current single-band pipeline behavior", | |||
| Pipeline: &PipelineConfig{Mode: "legacy", Profile: "legacy", Goals: PipelineGoalConfig{Intent: "general-monitoring"}}, | |||
| Surveillance: &SurveillanceConfig{ | |||
| AnalysisFFTSize: 2048, | |||
| FrameRate: 15, | |||
| Strategy: "single-resolution", | |||
| DisplayBins: 2048, | |||
| DisplayFPS: 15, | |||
| AnalysisFFTSize: 2048, | |||
| FrameRate: 15, | |||
| Strategy: "single-resolution", | |||
| DisplayBins: 2048, | |||
| DisplayFPS: 15, | |||
| DerivedDetection: "auto", | |||
| }, | |||
| Refinement: &RefinementConfig{ | |||
| Enabled: true, | |||
| @@ -229,11 +233,12 @@ func Default() Config { | |||
| SignalPriorities: []string{"digital", "wfm"}, | |||
| }}, | |||
| Surveillance: &SurveillanceConfig{ | |||
| AnalysisFFTSize: 4096, | |||
| FrameRate: 12, | |||
| Strategy: "multi-resolution", | |||
| DisplayBins: 2048, | |||
| DisplayFPS: 12, | |||
| AnalysisFFTSize: 4096, | |||
| FrameRate: 12, | |||
| Strategy: "multi-resolution", | |||
| DisplayBins: 2048, | |||
| DisplayFPS: 12, | |||
| DerivedDetection: "auto", | |||
| }, | |||
| Refinement: &RefinementConfig{ | |||
| Enabled: true, | |||
| @@ -260,11 +265,12 @@ func Default() Config { | |||
| SignalPriorities: []string{"digital", "wfm", "trunk"}, | |||
| }}, | |||
| Surveillance: &SurveillanceConfig{ | |||
| AnalysisFFTSize: 8192, | |||
| FrameRate: 10, | |||
| Strategy: "multi-resolution", | |||
| DisplayBins: 4096, | |||
| DisplayFPS: 10, | |||
| AnalysisFFTSize: 8192, | |||
| FrameRate: 10, | |||
| Strategy: "multi-resolution", | |||
| DisplayBins: 4096, | |||
| DisplayFPS: 10, | |||
| DerivedDetection: "auto", | |||
| }, | |||
| Refinement: &RefinementConfig{ | |||
| Enabled: true, | |||
| @@ -291,11 +297,12 @@ func Default() Config { | |||
| SignalPriorities: []string{"wfm", "nfm", "digital"}, | |||
| }}, | |||
| Surveillance: &SurveillanceConfig{ | |||
| AnalysisFFTSize: 4096, | |||
| FrameRate: 12, | |||
| Strategy: "single-resolution", | |||
| DisplayBins: 2048, | |||
| DisplayFPS: 12, | |||
| AnalysisFFTSize: 4096, | |||
| FrameRate: 12, | |||
| Strategy: "single-resolution", | |||
| DisplayBins: 2048, | |||
| DisplayFPS: 12, | |||
| DerivedDetection: "auto", | |||
| }, | |||
| Refinement: &RefinementConfig{ | |||
| Enabled: true, | |||
| @@ -322,11 +329,12 @@ func Default() Config { | |||
| SignalPriorities: []string{"ft8", "wspr", "fsk", "psk", "dmr"}, | |||
| }}, | |||
| Surveillance: &SurveillanceConfig{ | |||
| AnalysisFFTSize: 4096, | |||
| FrameRate: 12, | |||
| Strategy: "multi-resolution", | |||
| DisplayBins: 2048, | |||
| DisplayFPS: 12, | |||
| AnalysisFFTSize: 4096, | |||
| FrameRate: 12, | |||
| Strategy: "multi-resolution", | |||
| DisplayBins: 2048, | |||
| DisplayFPS: 12, | |||
| DerivedDetection: "auto", | |||
| }, | |||
| Refinement: &RefinementConfig{ | |||
| Enabled: true, | |||
| @@ -495,6 +503,14 @@ func applyDefaults(cfg Config) Config { | |||
| if cfg.Surveillance.Strategy == "" { | |||
| cfg.Surveillance.Strategy = "single-resolution" | |||
| } | |||
| if cfg.Surveillance.DerivedDetection == "" { | |||
| cfg.Surveillance.DerivedDetection = "auto" | |||
| } | |||
| switch strings.ToLower(strings.TrimSpace(cfg.Surveillance.DerivedDetection)) { | |||
| case "auto", "on", "off", "true", "false", "enabled", "disabled", "enable", "disable": | |||
| default: | |||
| cfg.Surveillance.DerivedDetection = "auto" | |||
| } | |||
| if cfg.Surveillance.DisplayBins <= 0 { | |||
| cfg.Surveillance.DisplayBins = cfg.FFTSize | |||
| } | |||
| @@ -6,6 +6,13 @@ import ( | |||
| "strings" | |||
| ) | |||
| const ( | |||
| RoleSurveillancePrimary = "surveillance-primary" | |||
| RoleSurveillanceDerived = "surveillance-derived" | |||
| RoleSurveillanceSupport = "surveillance-support" | |||
| RolePresentation = "presentation" | |||
| ) | |||
| // CandidateEvidenceState summarizes fused evidence semantics for a candidate. | |||
| type CandidateEvidenceState struct { | |||
| TotalLevelEntries int `json:"total_level_entries"` | |||
| @@ -13,6 +20,7 @@ type CandidateEvidenceState struct { | |||
| DetectionLevelCount int `json:"detection_level_count"` | |||
| PrimaryLevelCount int `json:"primary_level_count,omitempty"` | |||
| DerivedLevelCount int `json:"derived_level_count,omitempty"` | |||
| SupportLevelCount int `json:"support_level_count,omitempty"` | |||
| PresentationLevelCount int `json:"presentation_level_count,omitempty"` | |||
| Levels []string `json:"levels,omitempty"` | |||
| Provenance []string `json:"provenance,omitempty"` | |||
| @@ -30,6 +38,7 @@ type EvidenceScoreDetails struct { | |||
| DetectionLevels int `json:"detection_levels"` | |||
| PrimaryLevels int `json:"primary_levels,omitempty"` | |||
| DerivedLevels int `json:"derived_levels,omitempty"` | |||
| SupportLevels int `json:"support_levels,omitempty"` | |||
| ProvenanceCount int `json:"provenance_count,omitempty"` | |||
| DerivedOnly bool `json:"derived_only,omitempty"` | |||
| MultiLevelConfirmed bool `json:"multi_level_confirmed,omitempty"` | |||
| @@ -44,20 +53,41 @@ func IsPresentationLevel(level AnalysisLevel) bool { | |||
| role := strings.ToLower(strings.TrimSpace(level.Role)) | |||
| truth := strings.ToLower(strings.TrimSpace(level.Truth)) | |||
| name := strings.ToLower(strings.TrimSpace(level.Name)) | |||
| if role == RolePresentation { | |||
| return true | |||
| } | |||
| if strings.Contains(role, "presentation") || strings.Contains(truth, "presentation") { | |||
| return true | |||
| } | |||
| return strings.Contains(name, "presentation") || strings.Contains(name, "display") | |||
| } | |||
| // IsSupportLevel reports whether a level is a non-detection support level. | |||
| func IsSupportLevel(level AnalysisLevel) bool { | |||
| role := strings.ToLower(strings.TrimSpace(level.Role)) | |||
| if role == RoleSurveillanceSupport { | |||
| return true | |||
| } | |||
| return strings.Contains(role, "surveillance-support") || strings.Contains(role, "support") | |||
| } | |||
| // IsDetectionLevel reports whether a level is intended for detection/analysis. | |||
| func IsDetectionLevel(level AnalysisLevel) bool { | |||
| if IsPresentationLevel(level) { | |||
| return false | |||
| } | |||
| if IsSupportLevel(level) { | |||
| return false | |||
| } | |||
| role := strings.ToLower(strings.TrimSpace(level.Role)) | |||
| truth := strings.ToLower(strings.TrimSpace(level.Truth)) | |||
| name := strings.ToLower(strings.TrimSpace(level.Name)) | |||
| switch role { | |||
| case RoleSurveillancePrimary, RoleSurveillanceDerived: | |||
| return true | |||
| case RoleSurveillanceSupport: | |||
| return false | |||
| } | |||
| if strings.Contains(truth, "surveillance") { | |||
| return true | |||
| } | |||
| @@ -70,12 +100,21 @@ func IsDetectionLevel(level AnalysisLevel) bool { | |||
| func isPrimarySurveillanceLevel(level AnalysisLevel) bool { | |||
| role := strings.ToLower(strings.TrimSpace(level.Role)) | |||
| name := strings.ToLower(strings.TrimSpace(level.Name)) | |||
| if role == RoleSurveillancePrimary { | |||
| return true | |||
| } | |||
| return role == "surveillance" || name == "surveillance" | |||
| } | |||
| func isDerivedSurveillanceLevel(level AnalysisLevel) bool { | |||
| role := strings.ToLower(strings.TrimSpace(level.Role)) | |||
| name := strings.ToLower(strings.TrimSpace(level.Name)) | |||
| if role == RoleSurveillanceSupport { | |||
| return false | |||
| } | |||
| if role == RoleSurveillanceDerived { | |||
| return true | |||
| } | |||
| if strings.HasPrefix(role, "surveillance-") && role != "surveillance" { | |||
| return true | |||
| } | |||
| @@ -107,6 +146,7 @@ func CandidateEvidenceStateFor(candidate Candidate) CandidateEvidenceState { | |||
| primaryLevels := map[string]struct{}{} | |||
| derivedLevels := map[string]struct{}{} | |||
| presentationLevels := map[string]struct{}{} | |||
| supportLevels := map[string]struct{}{} | |||
| for _, ev := range candidate.Evidence { | |||
| levelKey := evidenceLevelKey(ev.Level) | |||
| levelSet[levelKey] = struct{}{} | |||
| @@ -117,6 +157,10 @@ func CandidateEvidenceStateFor(candidate Candidate) CandidateEvidenceState { | |||
| presentationLevels[levelKey] = struct{}{} | |||
| continue | |||
| } | |||
| if IsSupportLevel(ev.Level) { | |||
| supportLevels[levelKey] = struct{}{} | |||
| continue | |||
| } | |||
| if IsDetectionLevel(ev.Level) { | |||
| detectionLevels[levelKey] = struct{}{} | |||
| if isPrimarySurveillanceLevel(ev.Level) { | |||
| @@ -131,6 +175,7 @@ func CandidateEvidenceStateFor(candidate Candidate) CandidateEvidenceState { | |||
| state.DetectionLevelCount = len(detectionLevels) | |||
| state.PrimaryLevelCount = len(primaryLevels) | |||
| state.DerivedLevelCount = len(derivedLevels) | |||
| state.SupportLevelCount = len(supportLevels) | |||
| state.PresentationLevelCount = len(presentationLevels) | |||
| state.Levels = sortedKeys(levelSet) | |||
| state.Provenance = sortedKeys(provenanceSet) | |||
| @@ -31,23 +31,26 @@ type AnalysisContext struct { | |||
| type SurveillanceLevelSet struct { | |||
| Primary AnalysisLevel `json:"primary"` | |||
| Derived []AnalysisLevel `json:"derived,omitempty"` | |||
| Support []AnalysisLevel `json:"support,omitempty"` | |||
| Presentation AnalysisLevel `json:"presentation,omitempty"` | |||
| Detection []AnalysisLevel `json:"detection,omitempty"` | |||
| All []AnalysisLevel `json:"all,omitempty"` | |||
| } | |||
| type SurveillanceResult struct { | |||
| Level AnalysisLevel `json:"level"` | |||
| Levels []AnalysisLevel `json:"levels,omitempty"` | |||
| LevelSet SurveillanceLevelSet `json:"level_set,omitempty"` | |||
| Candidates []Candidate `json:"candidates"` | |||
| Scheduled []ScheduledCandidate `json:"scheduled,omitempty"` | |||
| Finished []detector.Event `json:"finished"` | |||
| Signals []detector.Signal `json:"signals"` | |||
| NoiseFloor float64 `json:"noise_floor"` | |||
| Thresholds []float64 `json:"thresholds,omitempty"` | |||
| DisplayLevel AnalysisLevel `json:"display_level"` | |||
| Context AnalysisContext `json:"context,omitempty"` | |||
| Spectra []SurveillanceLevelSpectrum `json:"spectra,omitempty"` | |||
| Level AnalysisLevel `json:"level"` | |||
| Levels []AnalysisLevel `json:"levels,omitempty"` | |||
| LevelSet SurveillanceLevelSet `json:"level_set,omitempty"` | |||
| DetectionPolicy SurveillanceDetectionPolicy `json:"detection_policy,omitempty"` | |||
| Candidates []Candidate `json:"candidates"` | |||
| Scheduled []ScheduledCandidate `json:"scheduled,omitempty"` | |||
| Finished []detector.Event `json:"finished"` | |||
| Signals []detector.Signal `json:"signals"` | |||
| NoiseFloor float64 `json:"noise_floor"` | |||
| Thresholds []float64 `json:"thresholds,omitempty"` | |||
| DisplayLevel AnalysisLevel `json:"display_level"` | |||
| Context AnalysisContext `json:"context,omitempty"` | |||
| Spectra []SurveillanceLevelSpectrum `json:"spectra,omitempty"` | |||
| } | |||
| type RefinementPlan struct { | |||
| @@ -3,34 +3,36 @@ package pipeline | |||
| import "sdr-wideband-suite/internal/config" | |||
| type Policy struct { | |||
| Mode string `json:"mode"` | |||
| Profile string `json:"profile,omitempty"` | |||
| Intent string `json:"intent"` | |||
| MonitorCenterHz float64 `json:"monitor_center_hz,omitempty"` | |||
| MonitorStartHz float64 `json:"monitor_start_hz,omitempty"` | |||
| MonitorEndHz float64 `json:"monitor_end_hz,omitempty"` | |||
| MonitorSpanHz float64 `json:"monitor_span_hz,omitempty"` | |||
| SignalPriorities []string `json:"signal_priorities,omitempty"` | |||
| AutoRecordClasses []string `json:"auto_record_classes,omitempty"` | |||
| AutoDecodeClasses []string `json:"auto_decode_classes,omitempty"` | |||
| SurveillanceFFTSize int `json:"surveillance_fft_size"` | |||
| SurveillanceFPS int `json:"surveillance_fps"` | |||
| DisplayBins int `json:"display_bins"` | |||
| DisplayFPS int `json:"display_fps"` | |||
| SurveillanceStrategy string `json:"surveillance_strategy"` | |||
| RefinementStrategy string `json:"refinement_strategy,omitempty"` | |||
| RefinementEnabled bool `json:"refinement_enabled"` | |||
| MaxRefinementJobs int `json:"max_refinement_jobs"` | |||
| RefinementMaxConcurrent int `json:"refinement_max_concurrent"` | |||
| RefinementDetailFFTSize int `json:"refinement_detail_fft_size"` | |||
| MinCandidateSNRDb float64 `json:"min_candidate_snr_db"` | |||
| RefinementMinSpanHz float64 `json:"refinement_min_span_hz"` | |||
| RefinementMaxSpanHz float64 `json:"refinement_max_span_hz"` | |||
| RefinementAutoSpan bool `json:"refinement_auto_span"` | |||
| PreferGPU bool `json:"prefer_gpu"` | |||
| MaxRecordingStreams int `json:"max_recording_streams"` | |||
| MaxDecodeJobs int `json:"max_decode_jobs"` | |||
| DecisionHoldMs int `json:"decision_hold_ms"` | |||
| Mode string `json:"mode"` | |||
| Profile string `json:"profile,omitempty"` | |||
| Intent string `json:"intent"` | |||
| MonitorCenterHz float64 `json:"monitor_center_hz,omitempty"` | |||
| MonitorStartHz float64 `json:"monitor_start_hz,omitempty"` | |||
| MonitorEndHz float64 `json:"monitor_end_hz,omitempty"` | |||
| MonitorSpanHz float64 `json:"monitor_span_hz,omitempty"` | |||
| SignalPriorities []string `json:"signal_priorities,omitempty"` | |||
| AutoRecordClasses []string `json:"auto_record_classes,omitempty"` | |||
| AutoDecodeClasses []string `json:"auto_decode_classes,omitempty"` | |||
| SurveillanceFFTSize int `json:"surveillance_fft_size"` | |||
| SurveillanceFPS int `json:"surveillance_fps"` | |||
| DisplayBins int `json:"display_bins"` | |||
| DisplayFPS int `json:"display_fps"` | |||
| SurveillanceStrategy string `json:"surveillance_strategy"` | |||
| SurveillanceDerivedDetection string `json:"surveillance_derived_detection"` | |||
| RefinementStrategy string `json:"refinement_strategy,omitempty"` | |||
| RefinementEnabled bool `json:"refinement_enabled"` | |||
| MaxRefinementJobs int `json:"max_refinement_jobs"` | |||
| RefinementMaxConcurrent int `json:"refinement_max_concurrent"` | |||
| RefinementDetailFFTSize int `json:"refinement_detail_fft_size"` | |||
| MinCandidateSNRDb float64 `json:"min_candidate_snr_db"` | |||
| RefinementMinSpanHz float64 `json:"refinement_min_span_hz"` | |||
| RefinementMaxSpanHz float64 `json:"refinement_max_span_hz"` | |||
| RefinementAutoSpan bool `json:"refinement_auto_span"` | |||
| PreferGPU bool `json:"prefer_gpu"` | |||
| MaxRecordingStreams int `json:"max_recording_streams"` | |||
| MaxDecodeJobs int `json:"max_decode_jobs"` | |||
| DecisionHoldMs int `json:"decision_hold_ms"` | |||
| SurveillanceDetection SurveillanceDetectionPolicy `json:"surveillance_detection,omitempty"` | |||
| } | |||
| func PolicyFromConfig(cfg config.Config) Policy { | |||
| @@ -39,35 +41,37 @@ func PolicyFromConfig(cfg config.Config) Policy { | |||
| detailFFT = cfg.Surveillance.AnalysisFFTSize | |||
| } | |||
| p := Policy{ | |||
| Mode: cfg.Pipeline.Mode, | |||
| Profile: cfg.Pipeline.Profile, | |||
| Intent: cfg.Pipeline.Goals.Intent, | |||
| MonitorCenterHz: cfg.CenterHz, | |||
| MonitorStartHz: cfg.Pipeline.Goals.MonitorStartHz, | |||
| MonitorEndHz: cfg.Pipeline.Goals.MonitorEndHz, | |||
| MonitorSpanHz: cfg.Pipeline.Goals.MonitorSpanHz, | |||
| SignalPriorities: append([]string(nil), cfg.Pipeline.Goals.SignalPriorities...), | |||
| AutoRecordClasses: append([]string(nil), cfg.Pipeline.Goals.AutoRecordClasses...), | |||
| AutoDecodeClasses: append([]string(nil), cfg.Pipeline.Goals.AutoDecodeClasses...), | |||
| SurveillanceFFTSize: cfg.Surveillance.AnalysisFFTSize, | |||
| SurveillanceFPS: cfg.Surveillance.FrameRate, | |||
| DisplayBins: cfg.Surveillance.DisplayBins, | |||
| DisplayFPS: cfg.Surveillance.DisplayFPS, | |||
| SurveillanceStrategy: cfg.Surveillance.Strategy, | |||
| RefinementEnabled: cfg.Refinement.Enabled, | |||
| MaxRefinementJobs: cfg.Resources.MaxRefinementJobs, | |||
| RefinementMaxConcurrent: cfg.Refinement.MaxConcurrent, | |||
| RefinementDetailFFTSize: detailFFT, | |||
| MinCandidateSNRDb: cfg.Refinement.MinCandidateSNRDb, | |||
| RefinementMinSpanHz: cfg.Refinement.MinSpanHz, | |||
| RefinementMaxSpanHz: cfg.Refinement.MaxSpanHz, | |||
| RefinementAutoSpan: config.BoolValue(cfg.Refinement.AutoSpan, true), | |||
| PreferGPU: cfg.Resources.PreferGPU, | |||
| MaxRecordingStreams: cfg.Resources.MaxRecordingStreams, | |||
| MaxDecodeJobs: cfg.Resources.MaxDecodeJobs, | |||
| DecisionHoldMs: cfg.Resources.DecisionHoldMs, | |||
| Mode: cfg.Pipeline.Mode, | |||
| Profile: cfg.Pipeline.Profile, | |||
| Intent: cfg.Pipeline.Goals.Intent, | |||
| MonitorCenterHz: cfg.CenterHz, | |||
| MonitorStartHz: cfg.Pipeline.Goals.MonitorStartHz, | |||
| MonitorEndHz: cfg.Pipeline.Goals.MonitorEndHz, | |||
| MonitorSpanHz: cfg.Pipeline.Goals.MonitorSpanHz, | |||
| SignalPriorities: append([]string(nil), cfg.Pipeline.Goals.SignalPriorities...), | |||
| AutoRecordClasses: append([]string(nil), cfg.Pipeline.Goals.AutoRecordClasses...), | |||
| AutoDecodeClasses: append([]string(nil), cfg.Pipeline.Goals.AutoDecodeClasses...), | |||
| SurveillanceFFTSize: cfg.Surveillance.AnalysisFFTSize, | |||
| SurveillanceFPS: cfg.Surveillance.FrameRate, | |||
| DisplayBins: cfg.Surveillance.DisplayBins, | |||
| DisplayFPS: cfg.Surveillance.DisplayFPS, | |||
| SurveillanceStrategy: cfg.Surveillance.Strategy, | |||
| SurveillanceDerivedDetection: cfg.Surveillance.DerivedDetection, | |||
| RefinementEnabled: cfg.Refinement.Enabled, | |||
| MaxRefinementJobs: cfg.Resources.MaxRefinementJobs, | |||
| RefinementMaxConcurrent: cfg.Refinement.MaxConcurrent, | |||
| RefinementDetailFFTSize: detailFFT, | |||
| MinCandidateSNRDb: cfg.Refinement.MinCandidateSNRDb, | |||
| RefinementMinSpanHz: cfg.Refinement.MinSpanHz, | |||
| RefinementMaxSpanHz: cfg.Refinement.MaxSpanHz, | |||
| RefinementAutoSpan: config.BoolValue(cfg.Refinement.AutoSpan, true), | |||
| PreferGPU: cfg.Resources.PreferGPU, | |||
| MaxRecordingStreams: cfg.Resources.MaxRecordingStreams, | |||
| MaxDecodeJobs: cfg.Resources.MaxDecodeJobs, | |||
| DecisionHoldMs: cfg.Resources.DecisionHoldMs, | |||
| } | |||
| p.RefinementStrategy, _ = refinementStrategy(p) | |||
| p.SurveillanceDetection = SurveillanceDetectionPolicyFromPolicy(p) | |||
| if p.MonitorSpanHz <= 0 && p.MonitorStartHz != 0 && p.MonitorEndHz != 0 && p.MonitorEndHz > p.MonitorStartHz { | |||
| p.MonitorSpanHz = p.MonitorEndHz - p.MonitorStartHz | |||
| } | |||
| @@ -264,6 +264,7 @@ func candidateEvidenceScore(candidate Candidate, strategy string) (float64, Evid | |||
| DetectionLevels: state.DetectionLevelCount, | |||
| PrimaryLevels: state.PrimaryLevelCount, | |||
| DerivedLevels: state.DerivedLevelCount, | |||
| SupportLevels: state.SupportLevelCount, | |||
| ProvenanceCount: len(state.Provenance), | |||
| DerivedOnly: state.DerivedOnly, | |||
| MultiLevelConfirmed: state.MultiLevelConfirmed, | |||
| @@ -0,0 +1,80 @@ | |||
| package pipeline | |||
| import "strings" | |||
| // SurveillanceDetectionPolicy describes how surveillance levels are governed for detection. | |||
| type SurveillanceDetectionPolicy struct { | |||
| DerivedDetection string `json:"derived_detection"` | |||
| DerivedDetectionEnabled bool `json:"derived_detection_enabled"` | |||
| DerivedDetectionReason string `json:"derived_detection_reason,omitempty"` | |||
| PrimaryRole string `json:"primary_role"` | |||
| DerivedRole string `json:"derived_role"` | |||
| SupportRole string `json:"support_role"` | |||
| PresentationRole string `json:"presentation_role"` | |||
| } | |||
| func normalizeDerivedDetection(mode string) string { | |||
| switch strings.ToLower(strings.TrimSpace(mode)) { | |||
| case "on", "true", "enabled", "enable": | |||
| return "on" | |||
| case "off", "false", "disabled", "disable": | |||
| return "off" | |||
| case "auto", "": | |||
| return "auto" | |||
| default: | |||
| return "auto" | |||
| } | |||
| } | |||
| func strategyIsMulti(strategy string) bool { | |||
| switch strings.ToLower(strings.TrimSpace(strategy)) { | |||
| case "multi-resolution", "multi", "multi-res", "multi_res": | |||
| return true | |||
| default: | |||
| return strings.Contains(strings.ToLower(strategy), "multi") | |||
| } | |||
| } | |||
| // SurveillanceDetectionPolicyFromPolicy derives detection governance from policy intent/profile. | |||
| func SurveillanceDetectionPolicyFromPolicy(policy Policy) SurveillanceDetectionPolicy { | |||
| mode := normalizeDerivedDetection(policy.SurveillanceDerivedDetection) | |||
| enabled := false | |||
| reason := "" | |||
| switch mode { | |||
| case "on": | |||
| enabled = true | |||
| reason = "config" | |||
| case "off": | |||
| enabled = false | |||
| reason = "config" | |||
| default: | |||
| if !strategyIsMulti(policy.SurveillanceStrategy) { | |||
| enabled = false | |||
| reason = "strategy" | |||
| } else { | |||
| intent := strings.ToLower(strings.TrimSpace(policy.Intent)) | |||
| profile := strings.ToLower(strings.TrimSpace(policy.Profile)) | |||
| modeName := strings.ToLower(strings.TrimSpace(policy.Mode)) | |||
| switch { | |||
| case strings.Contains(profile, "archive") || strings.Contains(intent, "archive") || strings.Contains(intent, "triage") || strings.Contains(modeName, "archive"): | |||
| enabled = false | |||
| reason = "archive" | |||
| case strings.Contains(profile, "legacy") || strings.Contains(modeName, "legacy"): | |||
| enabled = false | |||
| reason = "legacy" | |||
| default: | |||
| enabled = true | |||
| reason = "strategy" | |||
| } | |||
| } | |||
| } | |||
| return SurveillanceDetectionPolicy{ | |||
| DerivedDetection: mode, | |||
| DerivedDetectionEnabled: enabled, | |||
| DerivedDetectionReason: reason, | |||
| PrimaryRole: RoleSurveillancePrimary, | |||
| DerivedRole: RoleSurveillanceDerived, | |||
| SupportRole: RoleSurveillanceSupport, | |||
| PresentationRole: RolePresentation, | |||
| } | |||
| } | |||
| @@ -22,11 +22,12 @@ type PipelineUpdate struct { | |||
| } | |||
| type SurveillanceUpdate struct { | |||
| AnalysisFFTSize *int `json:"analysis_fft_size"` | |||
| FrameRate *int `json:"frame_rate"` | |||
| Strategy *string `json:"strategy"` | |||
| DisplayBins *int `json:"display_bins"` | |||
| DisplayFPS *int `json:"display_fps"` | |||
| AnalysisFFTSize *int `json:"analysis_fft_size"` | |||
| FrameRate *int `json:"frame_rate"` | |||
| Strategy *string `json:"strategy"` | |||
| DisplayBins *int `json:"display_bins"` | |||
| DisplayFPS *int `json:"display_fps"` | |||
| DerivedDetection *string `json:"derived_detection"` | |||
| } | |||
| type RefinementUpdate struct { | |||
| @@ -251,6 +252,15 @@ func (m *Manager) ApplyConfig(update ConfigUpdate) (config.Config, error) { | |||
| } | |||
| next.Surveillance.DisplayFPS = v | |||
| } | |||
| if update.Surveillance.DerivedDetection != nil { | |||
| mode := strings.ToLower(strings.TrimSpace(*update.Surveillance.DerivedDetection)) | |||
| switch mode { | |||
| case "auto", "on", "off", "true", "false", "enabled", "disabled", "enable", "disable": | |||
| next.Surveillance.DerivedDetection = mode | |||
| default: | |||
| return m.cfg, errors.New("surveillance.derived_detection must be auto, on, or off") | |||
| } | |||
| } | |||
| } | |||
| if update.Refinement != nil { | |||
| if update.Refinement.Enabled != nil { | |||