From c3e9ff8bb8ce71034a33966564eeb5a7b6e558a7 Mon Sep 17 00:00:00 2001 From: Jan Svabenik Date: Sat, 21 Mar 2026 16:38:18 +0100 Subject: [PATCH] feat: add declarative pipeline goals --- README.md | 9 ++++++++ config.yaml | 8 +++++++ internal/config/config.go | 36 +++++++++++++++++++++++++++----- internal/pipeline/policy.go | 32 +++++++++++++++++++++------- internal/pipeline/policy_test.go | 13 +++++++++++- 5 files changed, 85 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index cd0c2e1..2984a8d 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,12 @@ Edit `config.yaml` (autosave goes to `config.autosave.yaml`). ### New phase-1 pipeline fields - `pipeline.mode` — operating mode label (`legacy`, `wideband-balanced`, ...) +- `pipeline.goals.*` — declarative target/intent layer for future autonomous operation + - `intent` + - `monitor_start_hz` / `monitor_end_hz` / `monitor_span_hz` + - `signal_priorities` + - `auto_record_classes` + - `auto_decode_classes` - `surveillance.analysis_fft_size` — analysis FFT size used by the surveillance layer - `surveillance.frame_rate` — surveillance cadence target - `surveillance.strategy` — currently `single-resolution`, reserved for future multi-resolution modes @@ -73,6 +79,9 @@ In phase 1, the engine stays backward compatible, but the config model now refle - local refinement - resource policy - presentation +- operator goals / future autonomous intent + +The long-term target is that you describe *what the system should do* (for example broad-span monitoring intent, preferred signal families, auto-record/decode priorities), while the engine decides *how* to allocate surveillance, refinement and decoding budgets. **CFAR modes:** `OFF`, `CA`, `OS`, `GOSCA`, `CASO` diff --git a/config.yaml b/config.yaml index a717041..7a881e6 100644 --- a/config.yaml +++ b/config.yaml @@ -13,6 +13,14 @@ dc_block: false iq_balance: false pipeline: mode: wideband-balanced + goals: + intent: wideband-surveillance + monitor_start_hz: 7.0e6 + monitor_end_hz: 7.2e6 + monitor_span_hz: 200000 + signal_priorities: ["hf-voice", "digital", "cw"] + auto_record_classes: [] + auto_decode_classes: [] surveillance: analysis_fft_size: 2048 frame_rate: 15 diff --git a/internal/config/config.go b/internal/config/config.go index 447a80a..e321a5e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -70,8 +70,19 @@ type DecoderConfig struct { PSKCmd string `yaml:"psk_cmd" json:"psk_cmd"` } +type PipelineGoalConfig struct { + Intent string `yaml:"intent" json:"intent"` + MonitorStartHz float64 `yaml:"monitor_start_hz" json:"monitor_start_hz"` + MonitorEndHz float64 `yaml:"monitor_end_hz" json:"monitor_end_hz"` + MonitorSpanHz float64 `yaml:"monitor_span_hz" json:"monitor_span_hz"` + SignalPriorities []string `yaml:"signal_priorities" json:"signal_priorities"` + AutoRecordClasses []string `yaml:"auto_record_classes" json:"auto_record_classes"` + AutoDecodeClasses []string `yaml:"auto_decode_classes" json:"auto_decode_classes"` +} + type PipelineConfig struct { - Mode string `yaml:"mode" json:"mode"` + Mode string `yaml:"mode" json:"mode"` + Goals PipelineGoalConfig `yaml:"goals" json:"goals"` } type SurveillanceConfig struct { @@ -93,8 +104,12 @@ type ResourceConfig struct { } type ProfileConfig struct { - Name string `yaml:"name" json:"name"` - Description string `yaml:"description" json:"description"` + Name string `yaml:"name" json:"name"` + Description string `yaml:"description" json:"description"` + Pipeline *PipelineConfig `yaml:"pipeline,omitempty" json:"pipeline,omitempty"` + Surveillance *SurveillanceConfig `yaml:"surveillance,omitempty" json:"surveillance,omitempty"` + Refinement *RefinementConfig `yaml:"refinement,omitempty" json:"refinement,omitempty"` + Resources *ResourceConfig `yaml:"resources,omitempty" json:"resources,omitempty"` } type Config struct { @@ -141,6 +156,9 @@ func Default() Config { IQBalance: false, Pipeline: PipelineConfig{ Mode: "legacy", + Goals: PipelineGoalConfig{ + Intent: "general-monitoring", + }, }, Surveillance: SurveillanceConfig{ AnalysisFFTSize: 2048, @@ -158,8 +176,10 @@ func Default() Config { MaxRecordingStreams: 16, }, Profiles: []ProfileConfig{ - {Name: "legacy", Description: "Current single-band pipeline behavior"}, - {Name: "wideband-balanced", Description: "Prepared profile for scalable wideband surveillance"}, + {Name: "legacy", Description: "Current single-band pipeline behavior", Pipeline: &PipelineConfig{Mode: "legacy", Goals: PipelineGoalConfig{Intent: "general-monitoring"}}}, + {Name: "wideband-balanced", Description: "Prepared baseline for scalable wideband surveillance", Pipeline: &PipelineConfig{Mode: "wideband-balanced", Goals: PipelineGoalConfig{Intent: "wideband-surveillance"}}}, + {Name: "wideband-aggressive", Description: "Higher surveillance/refinement budgets for future broad-span monitoring", Pipeline: &PipelineConfig{Mode: "wideband-aggressive", Goals: PipelineGoalConfig{Intent: "high-density-wideband-surveillance"}}}, + {Name: "archive", Description: "Record-first monitoring profile", Pipeline: &PipelineConfig{Mode: "archive", Goals: PipelineGoalConfig{Intent: "archive-and-triage"}}}, }, Detector: DetectorConfig{ ThresholdDb: -20, @@ -295,6 +315,12 @@ func applyDefaults(cfg Config) Config { if cfg.Pipeline.Mode == "" { cfg.Pipeline.Mode = "legacy" } + if cfg.Pipeline.Goals.Intent == "" { + cfg.Pipeline.Goals.Intent = "general-monitoring" + } + if cfg.Pipeline.Goals.MonitorSpanHz <= 0 && cfg.Pipeline.Goals.MonitorStartHz != 0 && cfg.Pipeline.Goals.MonitorEndHz != 0 && cfg.Pipeline.Goals.MonitorEndHz > cfg.Pipeline.Goals.MonitorStartHz { + cfg.Pipeline.Goals.MonitorSpanHz = cfg.Pipeline.Goals.MonitorEndHz - cfg.Pipeline.Goals.MonitorStartHz + } if cfg.Surveillance.AnalysisFFTSize <= 0 { cfg.Surveillance.AnalysisFFTSize = cfg.FFTSize } diff --git a/internal/pipeline/policy.go b/internal/pipeline/policy.go index db94c97..127a9c0 100644 --- a/internal/pipeline/policy.go +++ b/internal/pipeline/policy.go @@ -3,18 +3,32 @@ package pipeline import "sdr-wideband-suite/internal/config" type Policy struct { - Mode string `json:"mode"` - SurveillanceFFTSize int `json:"surveillance_fft_size"` - SurveillanceFPS int `json:"surveillance_fps"` - RefinementEnabled bool `json:"refinement_enabled"` - MaxRefinementJobs int `json:"max_refinement_jobs"` - MinCandidateSNRDb float64 `json:"min_candidate_snr_db"` - PreferGPU bool `json:"prefer_gpu"` + Mode string `json:"mode"` + Intent string `json:"intent"` + 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"` + RefinementEnabled bool `json:"refinement_enabled"` + MaxRefinementJobs int `json:"max_refinement_jobs"` + MinCandidateSNRDb float64 `json:"min_candidate_snr_db"` + PreferGPU bool `json:"prefer_gpu"` } func PolicyFromConfig(cfg config.Config) Policy { return Policy{ Mode: cfg.Pipeline.Mode, + Intent: cfg.Pipeline.Goals.Intent, + 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, RefinementEnabled: cfg.Refinement.Enabled, @@ -31,6 +45,7 @@ func ApplyNamedProfile(cfg *config.Config, name string) { switch name { case "legacy": cfg.Pipeline.Mode = "legacy" + cfg.Pipeline.Goals.Intent = "general-monitoring" cfg.Surveillance.Strategy = "single-resolution" cfg.Refinement.Enabled = true if cfg.Resources.MaxRefinementJobs <= 0 { @@ -38,6 +53,7 @@ func ApplyNamedProfile(cfg *config.Config, name string) { } case "wideband-balanced": cfg.Pipeline.Mode = "wideband-balanced" + cfg.Pipeline.Goals.Intent = "wideband-surveillance" cfg.Surveillance.Strategy = "single-resolution" if cfg.Surveillance.AnalysisFFTSize < 4096 { cfg.Surveillance.AnalysisFFTSize = 4096 @@ -58,6 +74,7 @@ func ApplyNamedProfile(cfg *config.Config, name string) { cfg.Resources.PreferGPU = true case "wideband-aggressive": cfg.Pipeline.Mode = "wideband-aggressive" + cfg.Pipeline.Goals.Intent = "high-density-wideband-surveillance" cfg.Surveillance.Strategy = "single-resolution" if cfg.Surveillance.AnalysisFFTSize < 8192 { cfg.Surveillance.AnalysisFFTSize = 8192 @@ -78,6 +95,7 @@ func ApplyNamedProfile(cfg *config.Config, name string) { cfg.Resources.PreferGPU = true case "archive": cfg.Pipeline.Mode = "archive" + cfg.Pipeline.Goals.Intent = "archive-and-triage" cfg.Refinement.Enabled = true if cfg.Refinement.MaxConcurrent < 12 { cfg.Refinement.MaxConcurrent = 12 diff --git a/internal/pipeline/policy_test.go b/internal/pipeline/policy_test.go index 8c87271..ad7d21f 100644 --- a/internal/pipeline/policy_test.go +++ b/internal/pipeline/policy_test.go @@ -12,6 +12,9 @@ func TestApplyNamedProfile(t *testing.T) { if cfg.Pipeline.Mode != "wideband-balanced" { t.Fatalf("mode not applied: %s", cfg.Pipeline.Mode) } + if cfg.Pipeline.Goals.Intent != "wideband-surveillance" { + t.Fatalf("intent not applied: %s", cfg.Pipeline.Goals.Intent) + } if cfg.Surveillance.AnalysisFFTSize < 4096 { t.Fatalf("analysis fft too small: %d", cfg.Surveillance.AnalysisFFTSize) } @@ -26,6 +29,11 @@ func TestApplyNamedProfile(t *testing.T) { func TestPolicyFromConfig(t *testing.T) { cfg := config.Default() cfg.Pipeline.Mode = "archive" + cfg.Pipeline.Goals.Intent = "archive-and-triage" + cfg.Pipeline.Goals.MonitorStartHz = 88e6 + cfg.Pipeline.Goals.MonitorEndHz = 108e6 + cfg.Pipeline.Goals.MonitorSpanHz = 20e6 + cfg.Pipeline.Goals.SignalPriorities = []string{"broadcast-fm", "rds"} cfg.Surveillance.AnalysisFFTSize = 8192 cfg.Surveillance.FrameRate = 9 cfg.Refinement.Enabled = true @@ -33,9 +41,12 @@ func TestPolicyFromConfig(t *testing.T) { cfg.Refinement.MinCandidateSNRDb = 2.5 cfg.Resources.PreferGPU = true p := PolicyFromConfig(cfg) - if p.Mode != "archive" || p.SurveillanceFFTSize != 8192 || p.SurveillanceFPS != 9 { + if p.Mode != "archive" || p.Intent != "archive-and-triage" || p.SurveillanceFFTSize != 8192 || p.SurveillanceFPS != 9 { t.Fatalf("unexpected policy: %+v", p) } + if p.MonitorSpanHz != 20e6 || len(p.SignalPriorities) != 2 { + t.Fatalf("unexpected policy goals: %+v", p) + } if !p.RefinementEnabled || p.MaxRefinementJobs != 5 || p.MinCandidateSNRDb != 2.5 || !p.PreferGPU { t.Fatalf("unexpected policy details: %+v", p) }