Преглед изворни кода

feat: add refinement plan with budgeted selection stats

master
Jan Svabenik пре 16 часа
родитељ
комит
3ca0e85895
5 измењених фајлова са 70 додато и 13 уклоњено
  1. +7
    -1
      cmd/sdrd/pipeline_runtime.go
  2. +10
    -0
      internal/pipeline/phases.go
  3. +7
    -0
      internal/pipeline/phases_test.go
  4. +24
    -12
      internal/pipeline/scheduler.go
  5. +22
    -0
      internal/pipeline/scheduler_test.go

+ 7
- 1
cmd/sdrd/pipeline_runtime.go Прегледај датотеку

@@ -231,9 +231,15 @@ func (rt *dspRuntime) buildSurveillanceResult(art *spectrumArtifacts) pipeline.S

func (rt *dspRuntime) buildRefinementInput(surv pipeline.SurveillanceResult) pipeline.RefinementInput {
policy := pipeline.PolicyFromConfig(rt.cfg)
plan := pipeline.BuildRefinementPlan(surv.Candidates, policy)
scheduled := append([]pipeline.ScheduledCandidate(nil), surv.Scheduled...)
if len(scheduled) == 0 && len(plan.Selected) > 0 {
scheduled = append([]pipeline.ScheduledCandidate(nil), plan.Selected...)
}
input := pipeline.RefinementInput{
Candidates: append([]pipeline.Candidate(nil), surv.Candidates...),
Scheduled: append([]pipeline.ScheduledCandidate(nil), surv.Scheduled...),
Scheduled: scheduled,
Plan: plan,
SampleRate: rt.cfg.SampleRate,
FFTSize: rt.cfg.FFTSize,
CenterHz: rt.cfg.CenterHz,


+ 10
- 0
internal/pipeline/phases.go Прегледај датотеку

@@ -11,9 +11,19 @@ type SurveillanceResult struct {
Thresholds []float64 `json:"thresholds,omitempty"`
}

type RefinementPlan struct {
TotalCandidates int `json:"total_candidates"`
MinCandidateSNRDb float64 `json:"min_candidate_snr_db"`
Budget int `json:"budget"`
DroppedBySNR int `json:"dropped_by_snr"`
DroppedByBudget int `json:"dropped_by_budget"`
Selected []ScheduledCandidate `json:"selected,omitempty"`
}

type RefinementInput struct {
Candidates []Candidate `json:"candidates,omitempty"`
Scheduled []ScheduledCandidate `json:"scheduled,omitempty"`
Plan RefinementPlan `json:"plan,omitempty"`
SampleRate int `json:"sample_rate"`
FFTSize int `json:"fft_size"`
CenterHz float64 `json:"center_hz"`


+ 7
- 0
internal/pipeline/phases_test.go Прегледај датотеку

@@ -31,6 +31,10 @@ func TestRefinementInputCarriesScheduledCandidates(t *testing.T) {
res := RefinementInput{
Candidates: []Candidate{{ID: 2}},
Scheduled: []ScheduledCandidate{{Candidate: Candidate{ID: 2}, Priority: 4}},
Plan: RefinementPlan{
TotalCandidates: 1,
Budget: 4,
},
SampleRate: 2048000,
FFTSize: 2048,
CenterHz: 7.1e6,
@@ -42,4 +46,7 @@ func TestRefinementInputCarriesScheduledCandidates(t *testing.T) {
if res.SampleRate != 2048000 || res.FFTSize != 2048 || res.CenterHz != 7.1e6 {
t.Fatalf("unexpected refinement input fields: %+v", res)
}
if res.Plan.TotalCandidates != 1 || res.Plan.Budget != 4 {
t.Fatalf("unexpected refinement plan fields: %+v", res.Plan)
}
}

+ 24
- 12
internal/pipeline/scheduler.go Прегледај датотеку

@@ -7,16 +7,22 @@ type ScheduledCandidate struct {
Priority float64 `json:"priority"`
}

// ScheduleCandidates picks the most valuable candidates for costly local refinement.
// 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 ScheduleCandidates(candidates []Candidate, policy Policy) []ScheduledCandidate {
func BuildRefinementPlan(candidates []Candidate, policy Policy) RefinementPlan {
plan := RefinementPlan{
TotalCandidates: len(candidates),
MinCandidateSNRDb: policy.MinCandidateSNRDb,
Budget: policy.MaxRefinementJobs,
}
if len(candidates) == 0 {
return nil
return plan
}
out := make([]ScheduledCandidate, 0, len(candidates))
scored := make([]ScheduledCandidate, 0, len(candidates))
for _, c := range candidates {
if c.SNRDb < policy.MinCandidateSNRDb {
plan.DroppedBySNR++
continue
}
priority := c.SNRDb + CandidatePriorityBoost(policy, c.Hint)
@@ -26,19 +32,25 @@ func ScheduleCandidates(candidates []Candidate, policy Policy) []ScheduledCandid
if c.PeakDb > 0 {
priority += c.PeakDb / 20.0
}
out = append(out, ScheduledCandidate{Candidate: c, Priority: priority})
scored = append(scored, ScheduledCandidate{Candidate: c, Priority: priority})
}
sort.Slice(out, func(i, j int) bool {
if out[i].Priority == out[j].Priority {
return out[i].Candidate.CenterHz < out[j].Candidate.CenterHz
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 out[i].Priority > out[j].Priority
return scored[i].Priority > scored[j].Priority
})
limit := policy.MaxRefinementJobs
if limit <= 0 || limit > len(out) {
limit = len(out)
if limit <= 0 || limit > len(scored) {
limit = len(scored)
}
return out[:limit]
plan.Selected = scored[:limit]
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 {


+ 22
- 0
internal/pipeline/scheduler_test.go Прегледај датотеку

@@ -22,6 +22,28 @@ func TestScheduleCandidates(t *testing.T) {
}
}

func TestBuildRefinementPlanTracksDrops(t *testing.T) {
policy := Policy{MaxRefinementJobs: 1, MinCandidateSNRDb: 10}
cands := []Candidate{
{ID: 1, CenterHz: 100, SNRDb: 4, BandwidthHz: 10000, PeakDb: 1},
{ID: 2, CenterHz: 200, SNRDb: 12, BandwidthHz: 50000, PeakDb: 3},
{ID: 3, CenterHz: 300, SNRDb: 11, BandwidthHz: 25000, PeakDb: 2},
}
plan := BuildRefinementPlan(cands, policy)
if plan.TotalCandidates != 3 {
t.Fatalf("expected total candidates 3, got %d", plan.TotalCandidates)
}
if plan.DroppedBySNR != 1 {
t.Fatalf("expected 1 dropped by SNR, got %d", plan.DroppedBySNR)
}
if plan.DroppedByBudget != 1 {
t.Fatalf("expected 1 dropped by budget, got %d", plan.DroppedByBudget)
}
if len(plan.Selected) != 1 || plan.Selected[0].Candidate.ID != 2 {
t.Fatalf("unexpected plan selection: %+v", plan.Selected)
}
}

func TestScheduleCandidatesPriorityBoost(t *testing.T) {
policy := Policy{MaxRefinementJobs: 1, MinCandidateSNRDb: 0, SignalPriorities: []string{"digital"}}
got := ScheduleCandidates([]Candidate{


Loading…
Откажи
Сачувај