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

Expose refinement plan scoring details

master
Jan Svabenik 6 часов назад
Родитель
Сommit
34830c64fd
5 измененных файлов: 90 добавлений и 7 удалений
  1. +3
    -0
      cmd/sdrd/http_handlers.go
  2. +7
    -0
      internal/pipeline/phases.go
  3. +5
    -1
      internal/pipeline/policy.go
  4. +54
    -6
      internal/pipeline/scheduler.go
  5. +21
    -0
      internal/pipeline/scheduler_test.go

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

@@ -140,6 +140,9 @@ func registerAPIHandlers(mux *http.ServeMux, cfgPath string, cfgManager *runtime
recommend := map[string]any{ recommend := map[string]any{
"mode": policy.Mode, "mode": policy.Mode,
"intent": policy.Intent, "intent": policy.Intent,
"monitor_center_hz": policy.MonitorCenterHz,
"monitor_start_hz": policy.MonitorStartHz,
"monitor_end_hz": policy.MonitorEndHz,
"monitor_span_hz": policy.MonitorSpanHz, "monitor_span_hz": policy.MonitorSpanHz,
"signal_priorities": policy.SignalPriorities, "signal_priorities": policy.SignalPriorities,
"auto_record_classes": policy.AutoRecordClasses, "auto_record_classes": policy.AutoRecordClasses,


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

@@ -27,9 +27,16 @@ type RefinementPlan struct {
TotalCandidates int `json:"total_candidates"` TotalCandidates int `json:"total_candidates"`
MinCandidateSNRDb float64 `json:"min_candidate_snr_db"` MinCandidateSNRDb float64 `json:"min_candidate_snr_db"`
Budget int `json:"budget"` Budget int `json:"budget"`
MonitorStartHz float64 `json:"monitor_start_hz,omitempty"`
MonitorEndHz float64 `json:"monitor_end_hz,omitempty"`
MonitorSpanHz float64 `json:"monitor_span_hz,omitempty"`
DroppedByMonitor int `json:"dropped_by_monitor"` DroppedByMonitor int `json:"dropped_by_monitor"`
DroppedBySNR int `json:"dropped_by_snr"` DroppedBySNR int `json:"dropped_by_snr"`
DroppedByBudget int `json:"dropped_by_budget"` DroppedByBudget int `json:"dropped_by_budget"`
PriorityMin float64 `json:"priority_min,omitempty"`
PriorityMax float64 `json:"priority_max,omitempty"`
PriorityAvg float64 `json:"priority_avg,omitempty"`
PriorityCutoff float64 `json:"priority_cutoff,omitempty"`
Selected []ScheduledCandidate `json:"selected,omitempty"` Selected []ScheduledCandidate `json:"selected,omitempty"`
} }




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

@@ -30,7 +30,7 @@ type Policy struct {
} }


func PolicyFromConfig(cfg config.Config) Policy { func PolicyFromConfig(cfg config.Config) Policy {
return Policy{
p := Policy{
Mode: cfg.Pipeline.Mode, Mode: cfg.Pipeline.Mode,
Intent: cfg.Pipeline.Goals.Intent, Intent: cfg.Pipeline.Goals.Intent,
MonitorCenterHz: cfg.CenterHz, MonitorCenterHz: cfg.CenterHz,
@@ -56,6 +56,10 @@ func PolicyFromConfig(cfg config.Config) Policy {
MaxDecodeJobs: cfg.Resources.MaxDecodeJobs, MaxDecodeJobs: cfg.Resources.MaxDecodeJobs,
DecisionHoldMs: cfg.Resources.DecisionHoldMs, DecisionHoldMs: cfg.Resources.DecisionHoldMs,
} }
if p.MonitorSpanHz <= 0 && p.MonitorStartHz != 0 && p.MonitorEndHz != 0 && p.MonitorEndHz > p.MonitorStartHz {
p.MonitorSpanHz = p.MonitorEndHz - p.MonitorStartHz
}
return p
} }


func ApplyNamedProfile(cfg *config.Config, name string) { func ApplyNamedProfile(cfg *config.Config, name string) {


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

@@ -3,8 +3,16 @@ package pipeline
import "sort" import "sort"


type ScheduledCandidate struct { type ScheduledCandidate struct {
Candidate Candidate `json:"candidate"`
Priority float64 `json:"priority"`
Candidate Candidate `json:"candidate"`
Priority float64 `json:"priority"`
Breakdown *PriorityBreakdown `json:"breakdown,omitempty"`
}

type PriorityBreakdown struct {
SNRScore float64 `json:"snr_score"`
BandwidthScore float64 `json:"bandwidth_score"`
PeakScore float64 `json:"peak_score"`
PolicyBoost float64 `json:"policy_boost"`
} }


// BuildRefinementPlan scores and budgets candidates for costly local refinement. // BuildRefinementPlan scores and budgets candidates for costly local refinement.
@@ -20,6 +28,13 @@ func BuildRefinementPlan(candidates []Candidate, policy Policy) RefinementPlan {
MinCandidateSNRDb: policy.MinCandidateSNRDb, MinCandidateSNRDb: policy.MinCandidateSNRDb,
Budget: budget, Budget: budget,
} }
if start, end, ok := monitorBounds(policy); ok {
plan.MonitorStartHz = start
plan.MonitorEndHz = end
if end > start {
plan.MonitorSpanHz = end - start
}
}
if len(candidates) == 0 { if len(candidates) == 0 {
return plan return plan
} }
@@ -34,14 +49,27 @@ func BuildRefinementPlan(candidates []Candidate, policy Policy) RefinementPlan {
plan.DroppedBySNR++ plan.DroppedBySNR++
continue continue
} }
priority := c.SNRDb*snrWeight + CandidatePriorityBoost(policy, c.Hint)
snrScore := c.SNRDb * snrWeight
bwScore := 0.0
peakScore := 0.0
policyBoost := CandidatePriorityBoost(policy, c.Hint)
if c.BandwidthHz > 0 { if c.BandwidthHz > 0 {
priority += minFloat64(c.BandwidthHz/25000.0, 6) * bwWeight
bwScore = minFloat64(c.BandwidthHz/25000.0, 6) * bwWeight
} }
if c.PeakDb > 0 { if c.PeakDb > 0 {
priority += (c.PeakDb / 20.0) * peakWeight
peakScore = (c.PeakDb / 20.0) * peakWeight
} }
scored = append(scored, ScheduledCandidate{Candidate: c, Priority: priority})
priority := snrScore + bwScore + peakScore + policyBoost
scored = append(scored, ScheduledCandidate{
Candidate: c,
Priority: priority,
Breakdown: &PriorityBreakdown{
SNRScore: snrScore,
BandwidthScore: bwScore,
PeakScore: peakScore,
PolicyBoost: policyBoost,
},
})
} }
sort.Slice(scored, func(i, j int) bool { sort.Slice(scored, func(i, j int) bool {
if scored[i].Priority == scored[j].Priority { if scored[i].Priority == scored[j].Priority {
@@ -49,11 +77,31 @@ func BuildRefinementPlan(candidates []Candidate, policy Policy) RefinementPlan {
} }
return scored[i].Priority > scored[j].Priority return scored[i].Priority > scored[j].Priority
}) })
if len(scored) > 0 {
minPriority := scored[0].Priority
maxPriority := scored[0].Priority
sumPriority := 0.0
for _, s := range scored {
if s.Priority < minPriority {
minPriority = s.Priority
}
if s.Priority > maxPriority {
maxPriority = s.Priority
}
sumPriority += s.Priority
}
plan.PriorityMin = minPriority
plan.PriorityMax = maxPriority
plan.PriorityAvg = sumPriority / float64(len(scored))
}
limit := plan.Budget limit := plan.Budget
if limit <= 0 || limit > len(scored) { if limit <= 0 || limit > len(scored) {
limit = len(scored) limit = len(scored)
} }
plan.Selected = scored[:limit] plan.Selected = scored[:limit]
if len(plan.Selected) > 0 {
plan.PriorityCutoff = plan.Selected[len(plan.Selected)-1].Priority
}
plan.DroppedByBudget = len(scored) - len(plan.Selected) plan.DroppedByBudget = len(scored) - len(plan.Selected)
return plan return plan
} }


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

@@ -119,3 +119,24 @@ func TestScheduleCandidatesPriorityBoost(t *testing.T) {
t.Fatalf("expected priority boost to favor digital candidate, got %+v", got) t.Fatalf("expected priority boost to favor digital candidate, got %+v", got)
} }
} }

func TestBuildRefinementPlanPriorityStats(t *testing.T) {
policy := Policy{MaxRefinementJobs: 1, MinCandidateSNRDb: 0}
cands := []Candidate{
{ID: 1, CenterHz: 100, SNRDb: 8, BandwidthHz: 10000, PeakDb: 2},
{ID: 2, CenterHz: 200, SNRDb: 12, BandwidthHz: 20000, PeakDb: 4},
}
plan := BuildRefinementPlan(cands, policy)
if plan.PriorityMax < plan.PriorityMin {
t.Fatalf("priority bounds invalid: %+v", plan)
}
if len(plan.Selected) != 1 {
t.Fatalf("expected 1 selected, got %d", len(plan.Selected))
}
if plan.PriorityCutoff != plan.Selected[0].Priority {
t.Fatalf("expected cutoff to match selection, got %.2f vs %.2f", plan.PriorityCutoff, plan.Selected[0].Priority)
}
if plan.Selected[0].Breakdown == nil {
t.Fatalf("expected breakdown on selected candidate")
}
}

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