| @@ -56,6 +56,12 @@ Edit `config.yaml` (autosave goes to `config.autosave.yaml`). | |||||
| ### New phase-1 pipeline fields | ### New phase-1 pipeline fields | ||||
| - `pipeline.mode` — operating mode label (`legacy`, `wideband-balanced`, ...) | - `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.analysis_fft_size` — analysis FFT size used by the surveillance layer | ||||
| - `surveillance.frame_rate` — surveillance cadence target | - `surveillance.frame_rate` — surveillance cadence target | ||||
| - `surveillance.strategy` — currently `single-resolution`, reserved for future multi-resolution modes | - `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 | - local refinement | ||||
| - resource policy | - resource policy | ||||
| - presentation | - 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` | **CFAR modes:** `OFF`, `CA`, `OS`, `GOSCA`, `CASO` | ||||
| @@ -13,6 +13,14 @@ dc_block: false | |||||
| iq_balance: false | iq_balance: false | ||||
| pipeline: | pipeline: | ||||
| mode: wideband-balanced | 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: | surveillance: | ||||
| analysis_fft_size: 2048 | analysis_fft_size: 2048 | ||||
| frame_rate: 15 | frame_rate: 15 | ||||
| @@ -70,8 +70,19 @@ type DecoderConfig struct { | |||||
| PSKCmd string `yaml:"psk_cmd" json:"psk_cmd"` | 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 { | type PipelineConfig struct { | ||||
| Mode string `yaml:"mode" json:"mode"` | |||||
| Mode string `yaml:"mode" json:"mode"` | |||||
| Goals PipelineGoalConfig `yaml:"goals" json:"goals"` | |||||
| } | } | ||||
| type SurveillanceConfig struct { | type SurveillanceConfig struct { | ||||
| @@ -93,8 +104,12 @@ type ResourceConfig struct { | |||||
| } | } | ||||
| type ProfileConfig 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 { | type Config struct { | ||||
| @@ -141,6 +156,9 @@ func Default() Config { | |||||
| IQBalance: false, | IQBalance: false, | ||||
| Pipeline: PipelineConfig{ | Pipeline: PipelineConfig{ | ||||
| Mode: "legacy", | Mode: "legacy", | ||||
| Goals: PipelineGoalConfig{ | |||||
| Intent: "general-monitoring", | |||||
| }, | |||||
| }, | }, | ||||
| Surveillance: SurveillanceConfig{ | Surveillance: SurveillanceConfig{ | ||||
| AnalysisFFTSize: 2048, | AnalysisFFTSize: 2048, | ||||
| @@ -158,8 +176,10 @@ func Default() Config { | |||||
| MaxRecordingStreams: 16, | MaxRecordingStreams: 16, | ||||
| }, | }, | ||||
| Profiles: []ProfileConfig{ | 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{ | Detector: DetectorConfig{ | ||||
| ThresholdDb: -20, | ThresholdDb: -20, | ||||
| @@ -295,6 +315,12 @@ func applyDefaults(cfg Config) Config { | |||||
| if cfg.Pipeline.Mode == "" { | if cfg.Pipeline.Mode == "" { | ||||
| cfg.Pipeline.Mode = "legacy" | 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 { | if cfg.Surveillance.AnalysisFFTSize <= 0 { | ||||
| cfg.Surveillance.AnalysisFFTSize = cfg.FFTSize | cfg.Surveillance.AnalysisFFTSize = cfg.FFTSize | ||||
| } | } | ||||
| @@ -3,18 +3,32 @@ package pipeline | |||||
| import "sdr-wideband-suite/internal/config" | import "sdr-wideband-suite/internal/config" | ||||
| type Policy struct { | 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 { | func PolicyFromConfig(cfg config.Config) Policy { | ||||
| return Policy{ | return Policy{ | ||||
| Mode: cfg.Pipeline.Mode, | 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, | SurveillanceFFTSize: cfg.Surveillance.AnalysisFFTSize, | ||||
| SurveillanceFPS: cfg.Surveillance.FrameRate, | SurveillanceFPS: cfg.Surveillance.FrameRate, | ||||
| RefinementEnabled: cfg.Refinement.Enabled, | RefinementEnabled: cfg.Refinement.Enabled, | ||||
| @@ -31,6 +45,7 @@ func ApplyNamedProfile(cfg *config.Config, name string) { | |||||
| switch name { | switch name { | ||||
| case "legacy": | case "legacy": | ||||
| cfg.Pipeline.Mode = "legacy" | cfg.Pipeline.Mode = "legacy" | ||||
| cfg.Pipeline.Goals.Intent = "general-monitoring" | |||||
| cfg.Surveillance.Strategy = "single-resolution" | cfg.Surveillance.Strategy = "single-resolution" | ||||
| cfg.Refinement.Enabled = true | cfg.Refinement.Enabled = true | ||||
| if cfg.Resources.MaxRefinementJobs <= 0 { | if cfg.Resources.MaxRefinementJobs <= 0 { | ||||
| @@ -38,6 +53,7 @@ func ApplyNamedProfile(cfg *config.Config, name string) { | |||||
| } | } | ||||
| case "wideband-balanced": | case "wideband-balanced": | ||||
| cfg.Pipeline.Mode = "wideband-balanced" | cfg.Pipeline.Mode = "wideband-balanced" | ||||
| cfg.Pipeline.Goals.Intent = "wideband-surveillance" | |||||
| cfg.Surveillance.Strategy = "single-resolution" | cfg.Surveillance.Strategy = "single-resolution" | ||||
| if cfg.Surveillance.AnalysisFFTSize < 4096 { | if cfg.Surveillance.AnalysisFFTSize < 4096 { | ||||
| cfg.Surveillance.AnalysisFFTSize = 4096 | cfg.Surveillance.AnalysisFFTSize = 4096 | ||||
| @@ -58,6 +74,7 @@ func ApplyNamedProfile(cfg *config.Config, name string) { | |||||
| cfg.Resources.PreferGPU = true | cfg.Resources.PreferGPU = true | ||||
| case "wideband-aggressive": | case "wideband-aggressive": | ||||
| cfg.Pipeline.Mode = "wideband-aggressive" | cfg.Pipeline.Mode = "wideband-aggressive" | ||||
| cfg.Pipeline.Goals.Intent = "high-density-wideband-surveillance" | |||||
| cfg.Surveillance.Strategy = "single-resolution" | cfg.Surveillance.Strategy = "single-resolution" | ||||
| if cfg.Surveillance.AnalysisFFTSize < 8192 { | if cfg.Surveillance.AnalysisFFTSize < 8192 { | ||||
| cfg.Surveillance.AnalysisFFTSize = 8192 | cfg.Surveillance.AnalysisFFTSize = 8192 | ||||
| @@ -78,6 +95,7 @@ func ApplyNamedProfile(cfg *config.Config, name string) { | |||||
| cfg.Resources.PreferGPU = true | cfg.Resources.PreferGPU = true | ||||
| case "archive": | case "archive": | ||||
| cfg.Pipeline.Mode = "archive" | cfg.Pipeline.Mode = "archive" | ||||
| cfg.Pipeline.Goals.Intent = "archive-and-triage" | |||||
| cfg.Refinement.Enabled = true | cfg.Refinement.Enabled = true | ||||
| if cfg.Refinement.MaxConcurrent < 12 { | if cfg.Refinement.MaxConcurrent < 12 { | ||||
| cfg.Refinement.MaxConcurrent = 12 | cfg.Refinement.MaxConcurrent = 12 | ||||
| @@ -12,6 +12,9 @@ func TestApplyNamedProfile(t *testing.T) { | |||||
| if cfg.Pipeline.Mode != "wideband-balanced" { | if cfg.Pipeline.Mode != "wideband-balanced" { | ||||
| t.Fatalf("mode not applied: %s", cfg.Pipeline.Mode) | 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 { | if cfg.Surveillance.AnalysisFFTSize < 4096 { | ||||
| t.Fatalf("analysis fft too small: %d", cfg.Surveillance.AnalysisFFTSize) | t.Fatalf("analysis fft too small: %d", cfg.Surveillance.AnalysisFFTSize) | ||||
| } | } | ||||
| @@ -26,6 +29,11 @@ func TestApplyNamedProfile(t *testing.T) { | |||||
| func TestPolicyFromConfig(t *testing.T) { | func TestPolicyFromConfig(t *testing.T) { | ||||
| cfg := config.Default() | cfg := config.Default() | ||||
| cfg.Pipeline.Mode = "archive" | 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.AnalysisFFTSize = 8192 | ||||
| cfg.Surveillance.FrameRate = 9 | cfg.Surveillance.FrameRate = 9 | ||||
| cfg.Refinement.Enabled = true | cfg.Refinement.Enabled = true | ||||
| @@ -33,9 +41,12 @@ func TestPolicyFromConfig(t *testing.T) { | |||||
| cfg.Refinement.MinCandidateSNRDb = 2.5 | cfg.Refinement.MinCandidateSNRDb = 2.5 | ||||
| cfg.Resources.PreferGPU = true | cfg.Resources.PreferGPU = true | ||||
| p := PolicyFromConfig(cfg) | 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) | 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 { | if !p.RefinementEnabled || p.MaxRefinementJobs != 5 || p.MinCandidateSNRDb != 2.5 || !p.PreferGPU { | ||||
| t.Fatalf("unexpected policy details: %+v", p) | t.Fatalf("unexpected policy details: %+v", p) | ||||
| } | } | ||||