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

Add window-based record/decode actions

master
Jan Svabenik пре 7 часа
родитељ
комит
838c94156d
10 измењених фајлова са 173 додато и 29 уклоњено
  1. +4
    -0
      cmd/sdrd/decision_compact.go
  2. +4
    -0
      cmd/sdrd/level_summary.go
  3. +8
    -6
      internal/config/config.go
  4. +10
    -8
      internal/pipeline/arbitration_reasons.go
  5. +1
    -1
      internal/pipeline/decision_queue.go
  6. +72
    -0
      internal/pipeline/decisions.go
  7. +46
    -0
      internal/pipeline/decisions_test.go
  8. +20
    -14
      internal/pipeline/monitor_rules.go
  9. +2
    -0
      internal/pipeline/scheduler.go
  10. +6
    -0
      internal/pipeline/types.go

+ 4
- 0
cmd/sdrd/decision_compact.go Прегледај датотеку

@@ -10,6 +10,8 @@ type compactDecision struct {
Reason string `json:"reason,omitempty"` Reason string `json:"reason,omitempty"`
MonitorBias float64 `json:"monitor_bias,omitempty"` MonitorBias float64 `json:"monitor_bias,omitempty"`
MonitorDetail *pipeline.MonitorWindowMatch `json:"monitor_detail,omitempty"` MonitorDetail *pipeline.MonitorWindowMatch `json:"monitor_detail,omitempty"`
RecordWindow *pipeline.MonitorWindowMatch `json:"record_window,omitempty"`
DecodeWindow *pipeline.MonitorWindowMatch `json:"decode_window,omitempty"`
RecordAdmission *pipeline.PriorityAdmission `json:"record_admission,omitempty"` RecordAdmission *pipeline.PriorityAdmission `json:"record_admission,omitempty"`
DecodeAdmission *pipeline.PriorityAdmission `json:"decode_admission,omitempty"` DecodeAdmission *pipeline.PriorityAdmission `json:"decode_admission,omitempty"`
Candidate pipeline.Candidate `json:"candidate"` Candidate pipeline.Candidate `json:"candidate"`
@@ -26,6 +28,8 @@ func compactDecisions(decisions []pipeline.SignalDecision) []compactDecision {
Reason: d.Reason, Reason: d.Reason,
MonitorBias: d.MonitorBias, MonitorBias: d.MonitorBias,
MonitorDetail: d.MonitorDetail, MonitorDetail: d.MonitorDetail,
RecordWindow: d.RecordWindow,
DecodeWindow: d.DecodeWindow,
RecordAdmission: d.RecordAdmission, RecordAdmission: d.RecordAdmission,
DecodeAdmission: d.DecodeAdmission, DecodeAdmission: d.DecodeAdmission,
Candidate: d.Candidate, Candidate: d.Candidate,


+ 4
- 0
cmd/sdrd/level_summary.go Прегледај датотеку

@@ -53,6 +53,8 @@ type CandidateWindowSummary struct {
SpanHz float64 `json:"span_hz,omitempty"` SpanHz float64 `json:"span_hz,omitempty"`
Priority float64 `json:"priority,omitempty"` Priority float64 `json:"priority,omitempty"`
PriorityBias float64 `json:"priority_bias,omitempty"` PriorityBias float64 `json:"priority_bias,omitempty"`
AutoRecord bool `json:"auto_record,omitempty"`
AutoDecode bool `json:"auto_decode,omitempty"`
Candidates int `json:"candidates"` Candidates int `json:"candidates"`
} }


@@ -235,6 +237,8 @@ func buildCandidateWindowSummary(candidates []pipeline.Candidate, windows []pipe
SpanHz: win.SpanHz, SpanHz: win.SpanHz,
Priority: win.Priority, Priority: win.Priority,
PriorityBias: win.PriorityBias, PriorityBias: win.PriorityBias,
AutoRecord: win.AutoRecord,
AutoDecode: win.AutoDecode,
} }
index[win.Index] = len(out) index[win.Index] = len(out)
out = append(out, entry) out = append(out, entry)


+ 8
- 6
internal/config/config.go Прегледај датотеку

@@ -16,12 +16,14 @@ type Band struct {
} }


type MonitorWindow struct { type MonitorWindow struct {
Label string `yaml:"label" json:"label"`
StartHz float64 `yaml:"start_hz" json:"start_hz"`
EndHz float64 `yaml:"end_hz" json:"end_hz"`
CenterHz float64 `yaml:"center_hz" json:"center_hz"`
SpanHz float64 `yaml:"span_hz" json:"span_hz"`
Priority float64 `yaml:"priority" json:"priority"`
Label string `yaml:"label" json:"label"`
StartHz float64 `yaml:"start_hz" json:"start_hz"`
EndHz float64 `yaml:"end_hz" json:"end_hz"`
CenterHz float64 `yaml:"center_hz" json:"center_hz"`
SpanHz float64 `yaml:"span_hz" json:"span_hz"`
Priority float64 `yaml:"priority" json:"priority"`
AutoRecord bool `yaml:"auto_record" json:"auto_record"`
AutoDecode bool `yaml:"auto_decode" json:"auto_decode"`
} }


type DetectorConfig struct { type DetectorConfig struct {


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

@@ -6,14 +6,16 @@ const (
) )


const ( const (
DecisionReasonRecordClass = "decision:record:class"
DecisionReasonRecordHint = "decision:record:hint"
DecisionReasonDecodeClass = "decision:decode:class"
DecisionReasonDecodeHint = "decision:decode:hint"
DecisionReasonHintOnly = "decision:hint"
DecisionReasonQueueRecord = "queue:record:budget"
DecisionReasonQueueDecode = "queue:decode:budget"
DecisionReasonUnspecified = "decision:unspecified"
DecisionReasonRecordClass = "decision:record:class"
DecisionReasonRecordHint = "decision:record:hint"
DecisionReasonRecordWindow = "decision:record:window"
DecisionReasonDecodeClass = "decision:decode:class"
DecisionReasonDecodeHint = "decision:decode:hint"
DecisionReasonDecodeWindow = "decision:decode:window"
DecisionReasonHintOnly = "decision:hint"
DecisionReasonQueueRecord = "queue:record:budget"
DecisionReasonQueueDecode = "queue:decode:budget"
DecisionReasonUnspecified = "decision:unspecified"
) )


const ( const (


+ 1
- 1
internal/pipeline/decision_queue.go Прегледај датотеку

@@ -476,7 +476,7 @@ func buildQueueAdmission(queueName string, id int64, selection queueSelection, p
} }


func windowTagForDecision(decision SignalDecision) string { func windowTagForDecision(decision SignalDecision) string {
if decision.MonitorBias == 0 || decision.MonitorDetail == nil {
if decision.MonitorDetail == nil {
return "" return ""
} }
label := strings.TrimSpace(decision.MonitorDetail.Label) label := strings.TrimSpace(decision.MonitorDetail.Label)


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

@@ -14,6 +14,8 @@ type SignalDecision struct {
Reason string `json:"reason,omitempty"` Reason string `json:"reason,omitempty"`
MonitorBias float64 `json:"monitor_bias,omitempty"` MonitorBias float64 `json:"monitor_bias,omitempty"`
MonitorDetail *MonitorWindowMatch `json:"monitor_detail,omitempty"` MonitorDetail *MonitorWindowMatch `json:"monitor_detail,omitempty"`
RecordWindow *MonitorWindowMatch `json:"record_window,omitempty"`
DecodeWindow *MonitorWindowMatch `json:"decode_window,omitempty"`
RecordAdmission *PriorityAdmission `json:"record_admission,omitempty"` RecordAdmission *PriorityAdmission `json:"record_admission,omitempty"`
DecodeAdmission *PriorityAdmission `json:"decode_admission,omitempty"` DecodeAdmission *PriorityAdmission `json:"decode_admission,omitempty"`
} }
@@ -47,13 +49,83 @@ func DecideSignalAction(policy Policy, candidate Candidate, cls *classifier.Clas
decision.Reason = DecisionReasonDecodeHint decision.Reason = DecisionReasonDecodeHint
} }
} }
recordMatch := bestMonitorActionMatch(candidate.MonitorMatches, true, false)
if !decision.ShouldRecord && recordMatch != nil {
decision.ShouldRecord = true
decision.RecordWindow = recordMatch
if decision.Reason == "" {
decision.Reason = DecisionReasonRecordWindow
}
}
decodeMatch := bestMonitorActionMatch(candidate.MonitorMatches, false, true)
if !decision.ShouldAutoDecode && decodeMatch != nil {
decision.ShouldAutoDecode = true
decision.DecodeWindow = decodeMatch
if decision.Reason == "" {
decision.Reason = DecisionReasonDecodeWindow
}
}
if decision.Reason == "" && candidate.Hint != "" { if decision.Reason == "" && candidate.Hint != "" {
decision.Reason = DecisionReasonHintOnly decision.Reason = DecisionReasonHintOnly
} }
monitorBias, monitorDetail := MonitorWindowBias(policy, candidate) monitorBias, monitorDetail := MonitorWindowBias(policy, candidate)
if monitorDetail == nil {
monitorDetail = selectMonitorDetail(recordMatch, decodeMatch)
}
if monitorBias != 0 { if monitorBias != 0 {
decision.MonitorBias = monitorBias decision.MonitorBias = monitorBias
}
if monitorDetail != nil {
decision.MonitorDetail = monitorDetail decision.MonitorDetail = monitorDetail
} }
return decision return decision
} }

func bestMonitorActionMatch(matches []MonitorWindowMatch, wantRecord bool, wantDecode bool) *MonitorWindowMatch {
if len(matches) == 0 || (!wantRecord && !wantDecode) {
return nil
}
best := -1
for i := range matches {
match := matches[i]
if wantRecord && !match.AutoRecord {
continue
}
if wantDecode && !match.AutoDecode {
continue
}
if best == -1 || betterMonitorActionMatch(match, matches[best]) {
best = i
}
}
if best == -1 {
return nil
}
return &matches[best]
}

func betterMonitorActionMatch(candidate MonitorWindowMatch, best MonitorWindowMatch) bool {
if candidate.Coverage != best.Coverage {
return candidate.Coverage > best.Coverage
}
if candidate.DistanceHz != best.DistanceHz {
return candidate.DistanceHz < best.DistanceHz
}
if candidate.Bias != best.Bias {
return candidate.Bias > best.Bias
}
return candidate.Index < best.Index
}

func selectMonitorDetail(recordMatch *MonitorWindowMatch, decodeMatch *MonitorWindowMatch) *MonitorWindowMatch {
if recordMatch == nil {
return decodeMatch
}
if decodeMatch == nil {
return recordMatch
}
if betterMonitorActionMatch(*recordMatch, *decodeMatch) {
return recordMatch
}
return decodeMatch
}

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

@@ -31,3 +31,49 @@ func TestDecideSignalActionUsesHintWithoutClass(t *testing.T) {
t.Fatalf("expected reason for hint-based decision") t.Fatalf("expected reason for hint-based decision")
} }
} }

func TestDecideSignalActionWindowAutoRecord(t *testing.T) {
policy := Policy{
MonitorWindows: finalizeMonitorWindows([]MonitorWindow{{
Label: "record-zone",
StartHz: 100,
EndHz: 200,
CenterHz: 150,
SpanHz: 100,
AutoRecord: true,
}}),
}
decision := DecideSignalAction(policy, Candidate{ID: 3, CenterHz: 150}, nil)
if !decision.ShouldRecord {
t.Fatalf("expected window auto record decision")
}
if decision.Reason != DecisionReasonRecordWindow {
t.Fatalf("expected window record reason, got %q", decision.Reason)
}
if decision.RecordWindow == nil || decision.RecordWindow.Label != "record-zone" {
t.Fatalf("expected record window match to be set")
}
}

func TestDecideSignalActionWindowAutoDecode(t *testing.T) {
policy := Policy{
MonitorWindows: finalizeMonitorWindows([]MonitorWindow{{
Label: "decode-zone",
StartHz: 300,
EndHz: 350,
CenterHz: 325,
SpanHz: 50,
AutoDecode: true,
}}),
}
decision := DecideSignalAction(policy, Candidate{ID: 4, CenterHz: 325}, nil)
if !decision.ShouldAutoDecode {
t.Fatalf("expected window auto decode decision")
}
if decision.Reason != DecisionReasonDecodeWindow {
t.Fatalf("expected window decode reason, got %q", decision.Reason)
}
if decision.DecodeWindow == nil || decision.DecodeWindow.Label != "decode-zone" {
t.Fatalf("expected decode window match to be set")
}
}

+ 20
- 14
internal/pipeline/monitor_rules.go Прегледај датотеку

@@ -105,13 +105,15 @@ func normalizeGoalWindow(raw config.MonitorWindow, fallbackCenter float64) (Moni
if raw.StartHz > 0 && raw.EndHz > raw.StartHz { if raw.StartHz > 0 && raw.EndHz > raw.StartHz {
span := raw.EndHz - raw.StartHz span := raw.EndHz - raw.StartHz
return MonitorWindow{ return MonitorWindow{
Label: raw.Label,
StartHz: raw.StartHz,
EndHz: raw.EndHz,
CenterHz: (raw.StartHz + raw.EndHz) / 2,
SpanHz: span,
Source: "goals:window:start_end",
Priority: raw.Priority,
Label: raw.Label,
StartHz: raw.StartHz,
EndHz: raw.EndHz,
CenterHz: (raw.StartHz + raw.EndHz) / 2,
SpanHz: span,
Source: "goals:window:start_end",
Priority: raw.Priority,
AutoRecord: raw.AutoRecord,
AutoDecode: raw.AutoDecode,
}, true }, true
} }
center := raw.CenterHz center := raw.CenterHz
@@ -125,13 +127,15 @@ func normalizeGoalWindow(raw config.MonitorWindow, fallbackCenter float64) (Moni
source = "goals:window:span_default" source = "goals:window:span_default"
} }
return MonitorWindow{ return MonitorWindow{
Label: raw.Label,
StartHz: center - half,
EndHz: center + half,
CenterHz: center,
SpanHz: raw.SpanHz,
Source: source,
Priority: raw.Priority,
Label: raw.Label,
StartHz: center - half,
EndHz: center + half,
CenterHz: center,
SpanHz: raw.SpanHz,
Source: source,
Priority: raw.Priority,
AutoRecord: raw.AutoRecord,
AutoDecode: raw.AutoDecode,
}, true }, true
} }
return MonitorWindow{}, false return MonitorWindow{}, false
@@ -271,6 +275,8 @@ func MonitorWindowMatchesForCandidate(windows []MonitorWindow, candidate Candida
Coverage: coverage, Coverage: coverage,
DistanceHz: distance, DistanceHz: distance,
Bias: bias, Bias: bias,
AutoRecord: win.AutoRecord,
AutoDecode: win.AutoDecode,
}) })
} }
if len(matches) == 0 { if len(matches) == 0 {


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

@@ -422,6 +422,8 @@ func buildMonitorWindowStats(windows []MonitorWindow) []MonitorWindowStats {
SpanHz: win.SpanHz, SpanHz: win.SpanHz,
Priority: win.Priority, Priority: win.Priority,
PriorityBias: win.PriorityBias, PriorityBias: win.PriorityBias,
AutoRecord: win.AutoRecord,
AutoDecode: win.AutoDecode,
}) })
} }
return stats return stats


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

@@ -41,6 +41,8 @@ type MonitorWindow struct {
Source string `json:"source,omitempty"` Source string `json:"source,omitempty"`
Priority float64 `json:"priority,omitempty"` Priority float64 `json:"priority,omitempty"`
PriorityBias float64 `json:"priority_bias,omitempty"` PriorityBias float64 `json:"priority_bias,omitempty"`
AutoRecord bool `json:"auto_record,omitempty"`
AutoDecode bool `json:"auto_decode,omitempty"`
} }


// MonitorWindowMatch captures how a candidate overlaps a monitor window. // MonitorWindowMatch captures how a candidate overlaps a monitor window.
@@ -56,6 +58,8 @@ type MonitorWindowMatch struct {
Coverage float64 `json:"coverage,omitempty"` Coverage float64 `json:"coverage,omitempty"`
DistanceHz float64 `json:"distance_hz,omitempty"` DistanceHz float64 `json:"distance_hz,omitempty"`
Bias float64 `json:"bias,omitempty"` Bias float64 `json:"bias,omitempty"`
AutoRecord bool `json:"auto_record,omitempty"`
AutoDecode bool `json:"auto_decode,omitempty"`
} }


// MonitorWindowStats summarizes candidate attribution per monitor window. // MonitorWindowStats summarizes candidate attribution per monitor window.
@@ -69,6 +73,8 @@ type MonitorWindowStats struct {
SpanHz float64 `json:"span_hz,omitempty"` SpanHz float64 `json:"span_hz,omitempty"`
Priority float64 `json:"priority,omitempty"` Priority float64 `json:"priority,omitempty"`
PriorityBias float64 `json:"priority_bias,omitempty"` PriorityBias float64 `json:"priority_bias,omitempty"`
AutoRecord bool `json:"auto_record,omitempty"`
AutoDecode bool `json:"auto_decode,omitempty"`
Candidates int `json:"candidates,omitempty"` Candidates int `json:"candidates,omitempty"`
Planned int `json:"planned,omitempty"` Planned int `json:"planned,omitempty"`
Dropped int `json:"dropped,omitempty"` Dropped int `json:"dropped,omitempty"`


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