Просмотр исходного кода

Add surveillance detection governance

master
Jan Svabenik 2 часов назад
Родитель
Сommit
9d1263743c
10 измененных файлов: 338 добавлений и 153 удалений
  1. +19
    -15
      cmd/sdrd/http_handlers.go
  2. +4
    -1
      cmd/sdrd/level_summary.go
  3. +49
    -30
      cmd/sdrd/pipeline_runtime.go
  4. +51
    -35
      internal/config/config.go
  5. +45
    -0
      internal/pipeline/evidence.go
  6. +15
    -12
      internal/pipeline/phases.go
  7. +59
    -55
      internal/pipeline/policy.go
  8. +1
    -0
      internal/pipeline/scheduler.go
  9. +80
    -0
      internal/pipeline/surveillance_policy.go
  10. +15
    -5
      internal/runtime/runtime.go

+ 19
- 15
cmd/sdrd/http_handlers.go Просмотреть файл

@@ -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


+ 4
- 1
cmd/sdrd/level_summary.go Просмотреть файл

@@ -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)
}


+ 49
- 30
cmd/sdrd/pipeline_runtime.go Просмотреть файл

@@ -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,
}
}



+ 51
- 35
internal/config/config.go Просмотреть файл

@@ -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
}


+ 45
- 0
internal/pipeline/evidence.go Просмотреть файл

@@ -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)


+ 15
- 12
internal/pipeline/phases.go Просмотреть файл

@@ -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 {


+ 59
- 55
internal/pipeline/policy.go Просмотреть файл

@@ -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
}


+ 1
- 0
internal/pipeline/scheduler.go Просмотреть файл

@@ -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,


+ 80
- 0
internal/pipeline/surveillance_policy.go Просмотреть файл

@@ -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,
}
}

+ 15
- 5
internal/runtime/runtime.go Просмотреть файл

@@ -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 {


Загрузка…
Отмена
Сохранить