diff --git a/internal/pipeline/policy.go b/internal/pipeline/policy.go index b708b3a..ffc12b9 100644 --- a/internal/pipeline/policy.go +++ b/internal/pipeline/policy.go @@ -3,48 +3,50 @@ package pipeline import "sdr-wideband-suite/internal/config" type Policy struct { - 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"` - DisplayBins int `json:"display_bins"` - DisplayFPS int `json:"display_fps"` - RefinementEnabled bool `json:"refinement_enabled"` - MaxRefinementJobs int `json:"max_refinement_jobs"` - 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"` + 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"` + DisplayBins int `json:"display_bins"` + DisplayFPS int `json:"display_fps"` + RefinementEnabled bool `json:"refinement_enabled"` + MaxRefinementJobs int `json:"max_refinement_jobs"` + RefinementMaxConcurrent int `json:"refinement_max_concurrent"` + 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"` } 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, - DisplayBins: cfg.Surveillance.DisplayBins, - DisplayFPS: cfg.Surveillance.DisplayFPS, - RefinementEnabled: cfg.Refinement.Enabled, - MaxRefinementJobs: cfg.Resources.MaxRefinementJobs, - MinCandidateSNRDb: cfg.Refinement.MinCandidateSNRDb, - RefinementMinSpanHz: cfg.Refinement.MinSpanHz, - RefinementMaxSpanHz: cfg.Refinement.MaxSpanHz, - RefinementAutoSpan: config.BoolValue(cfg.Refinement.AutoSpan, true), - PreferGPU: cfg.Resources.PreferGPU, + 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, + DisplayBins: cfg.Surveillance.DisplayBins, + DisplayFPS: cfg.Surveillance.DisplayFPS, + RefinementEnabled: cfg.Refinement.Enabled, + MaxRefinementJobs: cfg.Resources.MaxRefinementJobs, + RefinementMaxConcurrent: cfg.Refinement.MaxConcurrent, + MinCandidateSNRDb: cfg.Refinement.MinCandidateSNRDb, + RefinementMinSpanHz: cfg.Refinement.MinSpanHz, + RefinementMaxSpanHz: cfg.Refinement.MaxSpanHz, + RefinementAutoSpan: config.BoolValue(cfg.Refinement.AutoSpan, true), + PreferGPU: cfg.Resources.PreferGPU, } } diff --git a/internal/pipeline/scheduler.go b/internal/pipeline/scheduler.go index 9a57ca5..725ab6b 100644 --- a/internal/pipeline/scheduler.go +++ b/internal/pipeline/scheduler.go @@ -11,10 +11,14 @@ type ScheduledCandidate struct { // Current heuristic is intentionally simple and deterministic; later phases can add // richer scoring (novelty, persistence, profile-aware band priorities, decoder value). func BuildRefinementPlan(candidates []Candidate, policy Policy) RefinementPlan { + budget := policy.MaxRefinementJobs + if policy.RefinementMaxConcurrent > 0 && (budget <= 0 || policy.RefinementMaxConcurrent < budget) { + budget = policy.RefinementMaxConcurrent + } plan := RefinementPlan{ TotalCandidates: len(candidates), MinCandidateSNRDb: policy.MinCandidateSNRDb, - Budget: policy.MaxRefinementJobs, + Budget: budget, } if len(candidates) == 0 { return plan @@ -40,7 +44,7 @@ func BuildRefinementPlan(candidates []Candidate, policy Policy) RefinementPlan { } return scored[i].Priority > scored[j].Priority }) - limit := policy.MaxRefinementJobs + limit := plan.Budget if limit <= 0 || limit > len(scored) { limit = len(scored) } diff --git a/internal/pipeline/scheduler_test.go b/internal/pipeline/scheduler_test.go index b0c5565..f98095b 100644 --- a/internal/pipeline/scheduler_test.go +++ b/internal/pipeline/scheduler_test.go @@ -44,6 +44,22 @@ func TestBuildRefinementPlanTracksDrops(t *testing.T) { } } +func TestBuildRefinementPlanRespectsMaxConcurrent(t *testing.T) { + policy := Policy{MaxRefinementJobs: 5, RefinementMaxConcurrent: 2, MinCandidateSNRDb: 0} + cands := []Candidate{ + {ID: 1, CenterHz: 100, SNRDb: 9}, + {ID: 2, CenterHz: 200, SNRDb: 8}, + {ID: 3, CenterHz: 300, SNRDb: 7}, + } + plan := BuildRefinementPlan(cands, policy) + if plan.Budget != 2 { + t.Fatalf("expected budget 2, got %d", plan.Budget) + } + if len(plan.Selected) != 2 { + t.Fatalf("expected 2 selected, got %d", len(plan.Selected)) + } +} + func TestAutoSpanForHint(t *testing.T) { span, source := AutoSpanForHint("WFM_STEREO") if span < 150000 || source == "" {