Procházet zdrojové kódy

Extend policy monitor bounds and runtime updates

master
Jan Svabenik před 3 hodinami
rodič
revize
da41379a72
6 změnil soubory, kde provedl 165 přidání a 11 odebrání
  1. +14
    -2
      internal/pipeline/monitor_rules.go
  2. +2
    -0
      internal/pipeline/policy.go
  3. +3
    -0
      internal/pipeline/policy_test.go
  4. +17
    -0
      internal/pipeline/scheduler_test.go
  5. +81
    -7
      internal/runtime/runtime.go
  6. +48
    -2
      internal/runtime/runtime_test.go

+ 14
- 2
internal/pipeline/monitor_rules.go Zobrazit soubor

@@ -1,9 +1,21 @@
package pipeline

func candidateInMonitor(policy Policy, candidate Candidate) bool {
func monitorBounds(policy Policy) (float64, float64, bool) {
start := policy.MonitorStartHz
end := policy.MonitorEndHz
if start == 0 || end == 0 || end <= start {
if start != 0 && end != 0 && end > start {
return start, end, true
}
if policy.MonitorSpanHz > 0 && policy.MonitorCenterHz != 0 {
half := policy.MonitorSpanHz / 2
return policy.MonitorCenterHz - half, policy.MonitorCenterHz + half, true
}
return 0, 0, false
}

func candidateInMonitor(policy Policy, candidate Candidate) bool {
start, end, ok := monitorBounds(policy)
if !ok {
return true
}
left := candidate.CenterHz


+ 2
- 0
internal/pipeline/policy.go Zobrazit soubor

@@ -5,6 +5,7 @@ import "sdr-wideband-suite/internal/config"
type Policy struct {
Mode string `json:"mode"`
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"`
@@ -32,6 +33,7 @@ func PolicyFromConfig(cfg config.Config) Policy {
return Policy{
Mode: cfg.Pipeline.Mode,
Intent: cfg.Pipeline.Goals.Intent,
MonitorCenterHz: cfg.CenterHz,
MonitorStartHz: cfg.Pipeline.Goals.MonitorStartHz,
MonitorEndHz: cfg.Pipeline.Goals.MonitorEndHz,
MonitorSpanHz: cfg.Pipeline.Goals.MonitorSpanHz,


+ 3
- 0
internal/pipeline/policy_test.go Zobrazit soubor

@@ -49,6 +49,9 @@ func TestPolicyFromConfig(t *testing.T) {
if p.MonitorSpanHz != 20e6 || len(p.SignalPriorities) != 2 {
t.Fatalf("unexpected policy goals: %+v", p)
}
if p.MonitorCenterHz != cfg.CenterHz {
t.Fatalf("unexpected monitor center: %+v", p.MonitorCenterHz)
}
if !p.RefinementEnabled || p.MaxRefinementJobs != 5 || p.MinCandidateSNRDb != 2.5 || !p.PreferGPU {
t.Fatalf("unexpected policy details: %+v", p)
}


+ 17
- 0
internal/pipeline/scheduler_test.go Zobrazit soubor

@@ -77,6 +77,23 @@ func TestBuildRefinementPlanAppliesMonitorSpan(t *testing.T) {
}
}

func TestBuildRefinementPlanAppliesMonitorSpanCentered(t *testing.T) {
policy := Policy{MaxRefinementJobs: 5, MinCandidateSNRDb: 0, MonitorCenterHz: 300, MonitorSpanHz: 200}
cands := []Candidate{
{ID: 1, CenterHz: 100, BandwidthHz: 20},
{ID: 2, CenterHz: 250, BandwidthHz: 50},
{ID: 3, CenterHz: 300, BandwidthHz: 100},
{ID: 4, CenterHz: 420, BandwidthHz: 50},
}
plan := BuildRefinementPlan(cands, policy)
if plan.DroppedByMonitor != 1 {
t.Fatalf("expected 1 dropped by monitor, got %d", plan.DroppedByMonitor)
}
if len(plan.Selected) != 3 {
t.Fatalf("expected 3 selected within monitor, got %d", len(plan.Selected))
}
}

func TestAutoSpanForHint(t *testing.T) {
span, source := AutoSpanForHint("WFM_STEREO")
if span < 150000 || source == "" {


+ 81
- 7
internal/runtime/runtime.go Zobrazit soubor

@@ -10,8 +10,15 @@ import (
)

type PipelineUpdate struct {
Mode *string `json:"mode"`
Profile *string `json:"profile"`
Mode *string `json:"mode"`
Profile *string `json:"profile"`
Intent *string `json:"intent"`
MonitorStartHz *float64 `json:"monitor_start_hz"`
MonitorEndHz *float64 `json:"monitor_end_hz"`
MonitorSpanHz *float64 `json:"monitor_span_hz"`
SignalPriorities *[]string `json:"signal_priorities"`
AutoRecordClasses *[]string `json:"auto_record_classes"`
AutoDecodeClasses *[]string `json:"auto_decode_classes"`
}

type SurveillanceUpdate struct {
@@ -26,12 +33,17 @@ type RefinementUpdate struct {
Enabled *bool `json:"enabled"`
MaxConcurrent *int `json:"max_concurrent"`
MinCandidateSNRDb *float64 `json:"min_candidate_snr_db"`
MinSpanHz *float64 `json:"min_span_hz"`
MaxSpanHz *float64 `json:"max_span_hz"`
AutoSpan *bool `json:"auto_span"`
}

type ResourcesUpdate struct {
PreferGPU *bool `json:"prefer_gpu"`
MaxRefinementJobs *int `json:"max_refinement_jobs"`
MaxRecordingStreams *int `json:"max_recording_streams"`
PreferGPU *bool `json:"prefer_gpu"`
MaxRefinementJobs *int `json:"max_refinement_jobs"`
MaxRecordingStreams *int `json:"max_recording_streams"`
MaxDecodeJobs *int `json:"max_decode_jobs"`
DecisionHoldMs *int `json:"decision_hold_ms"`
}

type ConfigUpdate struct {
@@ -163,8 +175,40 @@ func (m *Manager) ApplyConfig(update ConfigUpdate) (config.Config, error) {
return m.cfg, errors.New("classifier_mode must be rule, math, or combined")
}
}
if update.Pipeline != nil && update.Pipeline.Mode != nil {
next.Pipeline.Mode = *update.Pipeline.Mode
if update.Pipeline != nil {
if update.Pipeline.Mode != nil {
next.Pipeline.Mode = *update.Pipeline.Mode
}
if update.Pipeline.Intent != nil {
next.Pipeline.Goals.Intent = *update.Pipeline.Intent
}
if update.Pipeline.MonitorStartHz != nil {
next.Pipeline.Goals.MonitorStartHz = *update.Pipeline.MonitorStartHz
}
if update.Pipeline.MonitorEndHz != nil {
next.Pipeline.Goals.MonitorEndHz = *update.Pipeline.MonitorEndHz
}
if update.Pipeline.MonitorSpanHz != nil {
if *update.Pipeline.MonitorSpanHz <= 0 {
return m.cfg, errors.New("monitor_span_hz must be > 0")
}
next.Pipeline.Goals.MonitorSpanHz = *update.Pipeline.MonitorSpanHz
}
if update.Pipeline.SignalPriorities != nil {
next.Pipeline.Goals.SignalPriorities = append([]string(nil), (*update.Pipeline.SignalPriorities)...)
}
if update.Pipeline.AutoRecordClasses != nil {
next.Pipeline.Goals.AutoRecordClasses = append([]string(nil), (*update.Pipeline.AutoRecordClasses)...)
}
if update.Pipeline.AutoDecodeClasses != nil {
next.Pipeline.Goals.AutoDecodeClasses = append([]string(nil), (*update.Pipeline.AutoDecodeClasses)...)
}
if next.Pipeline.Goals.MonitorStartHz != 0 && next.Pipeline.Goals.MonitorEndHz != 0 && next.Pipeline.Goals.MonitorEndHz <= next.Pipeline.Goals.MonitorStartHz {
return m.cfg, errors.New("monitor_end_hz must be > monitor_start_hz")
}
if next.Pipeline.Goals.MonitorSpanHz <= 0 && next.Pipeline.Goals.MonitorStartHz != 0 && next.Pipeline.Goals.MonitorEndHz != 0 && next.Pipeline.Goals.MonitorEndHz > next.Pipeline.Goals.MonitorStartHz {
next.Pipeline.Goals.MonitorSpanHz = next.Pipeline.Goals.MonitorEndHz - next.Pipeline.Goals.MonitorStartHz
}
}
if update.Surveillance != nil {
if update.Surveillance.AnalysisFFTSize != nil {
@@ -217,6 +261,24 @@ func (m *Manager) ApplyConfig(update ConfigUpdate) (config.Config, error) {
if update.Refinement.MinCandidateSNRDb != nil {
next.Refinement.MinCandidateSNRDb = *update.Refinement.MinCandidateSNRDb
}
if update.Refinement.MinSpanHz != nil {
if *update.Refinement.MinSpanHz < 0 {
return m.cfg, errors.New("refinement.min_span_hz must be >= 0")
}
next.Refinement.MinSpanHz = *update.Refinement.MinSpanHz
}
if update.Refinement.MaxSpanHz != nil {
if *update.Refinement.MaxSpanHz < 0 {
return m.cfg, errors.New("refinement.max_span_hz must be >= 0")
}
next.Refinement.MaxSpanHz = *update.Refinement.MaxSpanHz
}
if update.Refinement.AutoSpan != nil {
next.Refinement.AutoSpan = update.Refinement.AutoSpan
}
if next.Refinement.MaxSpanHz > 0 && next.Refinement.MinSpanHz > next.Refinement.MaxSpanHz {
return m.cfg, errors.New("refinement.min_span_hz must be <= refinement.max_span_hz")
}
}
if update.Resources != nil {
if update.Resources.PreferGPU != nil {
@@ -234,6 +296,18 @@ func (m *Manager) ApplyConfig(update ConfigUpdate) (config.Config, error) {
}
next.Resources.MaxRecordingStreams = *update.Resources.MaxRecordingStreams
}
if update.Resources.MaxDecodeJobs != nil {
if *update.Resources.MaxDecodeJobs <= 0 {
return m.cfg, errors.New("resources.max_decode_jobs must be > 0")
}
next.Resources.MaxDecodeJobs = *update.Resources.MaxDecodeJobs
}
if update.Resources.DecisionHoldMs != nil {
if *update.Resources.DecisionHoldMs < 0 {
return m.cfg, errors.New("resources.decision_hold_ms must be >= 0")
}
next.Resources.DecisionHoldMs = *update.Resources.DecisionHoldMs
}
}
if update.Detector != nil {
if update.Detector.ThresholdDb != nil {


+ 48
- 2
internal/runtime/runtime_test.go Zobrazit soubor

@@ -24,18 +24,37 @@ func TestApplyConfigUpdate(t *testing.T) {

mode := "wideband-balanced"
profile := "wideband-balanced"
intent := "wideband-surveillance"
monitorSpan := 500000.0
signalPriorities := []string{"digital", "weak"}
autoRecord := []string{"WFM"}
autoDecode := []string{"FT8"}
survFPS := 12
displayBins := 1024
displayFPS := 8
maxRefJobs := 24
minSpan := 4000.0
maxSpan := 200000.0
autoSpan := false
maxDecode := 12
decisionHold := 1500
updated, err := mgr.ApplyConfig(ConfigUpdate{
CenterHz: &center,
SampleRate: &sampleRate,
FFTSize: &fftSize,
TunerBwKHz: &bw,
Pipeline: &PipelineUpdate{Mode: &mode, Profile: &profile},
Pipeline: &PipelineUpdate{
Mode: &mode,
Profile: &profile,
Intent: &intent,
MonitorSpanHz: &monitorSpan,
SignalPriorities: &signalPriorities,
AutoRecordClasses: &autoRecord,
AutoDecodeClasses: &autoDecode,
},
Surveillance: &SurveillanceUpdate{FrameRate: &survFPS, DisplayBins: &displayBins, DisplayFPS: &displayFPS},
Resources: &ResourcesUpdate{MaxRefinementJobs: &maxRefJobs},
Refinement: &RefinementUpdate{MinSpanHz: &minSpan, MaxSpanHz: &maxSpan, AutoSpan: &autoSpan},
Resources: &ResourcesUpdate{MaxRefinementJobs: &maxRefJobs, MaxDecodeJobs: &maxDecode, DecisionHoldMs: &decisionHold},
Detector: &DetectorUpdate{
ThresholdDb: &threshold,
CFARMode: &cfarMode,
@@ -88,15 +107,42 @@ func TestApplyConfigUpdate(t *testing.T) {
if updated.Pipeline.Mode != mode {
t.Fatalf("pipeline mode: %v", updated.Pipeline.Mode)
}
if updated.Pipeline.Goals.Intent != intent {
t.Fatalf("pipeline intent: %v", updated.Pipeline.Goals.Intent)
}
if updated.Pipeline.Goals.MonitorSpanHz != monitorSpan {
t.Fatalf("monitor span: %v", updated.Pipeline.Goals.MonitorSpanHz)
}
if len(updated.Pipeline.Goals.SignalPriorities) != len(signalPriorities) {
t.Fatalf("signal priorities not applied")
}
if len(updated.Pipeline.Goals.AutoRecordClasses) != len(autoRecord) {
t.Fatalf("auto record classes not applied")
}
if len(updated.Pipeline.Goals.AutoDecodeClasses) != len(autoDecode) {
t.Fatalf("auto decode classes not applied")
}
if updated.Surveillance.FrameRate != survFPS || updated.FrameRate != survFPS {
t.Fatalf("surveillance frame rate: %v / %v", updated.Surveillance.FrameRate, updated.FrameRate)
}
if updated.Resources.MaxRefinementJobs != maxRefJobs {
t.Fatalf("max refinement jobs: %v", updated.Resources.MaxRefinementJobs)
}
if updated.Resources.MaxDecodeJobs != maxDecode {
t.Fatalf("max decode jobs: %v", updated.Resources.MaxDecodeJobs)
}
if updated.Resources.DecisionHoldMs != decisionHold {
t.Fatalf("decision hold: %v", updated.Resources.DecisionHoldMs)
}
if updated.Surveillance.DisplayBins != displayBins || updated.Surveillance.DisplayFPS != displayFPS {
t.Fatalf("display settings not applied: bins=%d fps=%d", updated.Surveillance.DisplayBins, updated.Surveillance.DisplayFPS)
}
if updated.Refinement.MinSpanHz != minSpan || updated.Refinement.MaxSpanHz != maxSpan {
t.Fatalf("refinement span not applied: %v / %v", updated.Refinement.MinSpanHz, updated.Refinement.MaxSpanHz)
}
if updated.Refinement.AutoSpan == nil || *updated.Refinement.AutoSpan != autoSpan {
t.Fatalf("refinement auto span not applied")
}
}

func TestApplyConfigRejectsInvalid(t *testing.T) {


Načítá se…
Zrušit
Uložit