|
- package pipeline
-
- import "sort"
-
- type ScheduledCandidate struct {
- 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.
- // 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: 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 {
- return plan
- }
- snrWeight, bwWeight, peakWeight := refinementIntentWeights(policy.Intent)
- scored := make([]ScheduledCandidate, 0, len(candidates))
- for _, c := range candidates {
- if !candidateInMonitor(policy, c) {
- plan.DroppedByMonitor++
- continue
- }
- if c.SNRDb < policy.MinCandidateSNRDb {
- plan.DroppedBySNR++
- continue
- }
- snrScore := c.SNRDb * snrWeight
- bwScore := 0.0
- peakScore := 0.0
- policyBoost := CandidatePriorityBoost(policy, c.Hint)
- if c.BandwidthHz > 0 {
- bwScore = minFloat64(c.BandwidthHz/25000.0, 6) * bwWeight
- }
- if c.PeakDb > 0 {
- peakScore = (c.PeakDb / 20.0) * peakWeight
- }
- 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 {
- if scored[i].Priority == scored[j].Priority {
- return scored[i].Candidate.CenterHz < scored[j].Candidate.CenterHz
- }
- 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
- if limit <= 0 || limit > len(scored) {
- limit = len(scored)
- }
- 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)
- return plan
- }
-
- func ScheduleCandidates(candidates []Candidate, policy Policy) []ScheduledCandidate {
- return BuildRefinementPlan(candidates, policy).Selected
- }
-
- func minFloat64(a, b float64) float64 {
- if a < b {
- return a
- }
- return b
- }
|