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

Add monitor window goals for multi-span gating

master
Jan Svabenik пре 3 часа
родитељ
комит
efe137b801
13 измењених фајлова са 295 додато и 17 уклоњено
  1. +3
    -0
      README.md
  2. +1
    -0
      cmd/sdrd/http_handlers.go
  3. +11
    -0
      cmd/sdrd/pipeline_runtime.go
  4. +16
    -7
      internal/config/config.go
  5. +117
    -1
      internal/pipeline/monitor_rules.go
  6. +56
    -0
      internal/pipeline/monitor_rules_test.go
  7. +1
    -0
      internal/pipeline/phases.go
  8. +11
    -0
      internal/pipeline/policy.go
  9. +21
    -0
      internal/pipeline/policy_test.go
  10. +3
    -0
      internal/pipeline/scheduler.go
  11. +10
    -0
      internal/pipeline/types.go
  12. +37
    -9
      internal/runtime/runtime.go
  13. +8
    -0
      internal/runtime/runtime_test.go

+ 3
- 0
README.md Прегледај датотеку

@@ -62,6 +62,7 @@ Edit `config.yaml` (autosave goes to `config.autosave.yaml`).
- `pipeline.goals.*` -- declarative target/intent layer for future autonomous operation
- `intent`
- `monitor_start_hz` / `monitor_end_hz` / `monitor_span_hz`
- `monitor_windows` -- optional list of monitor windows (each window uses `start_hz` + `end_hz` or `center_hz` + `span_hz`)
- `signal_priorities`
- `auto_record_classes`
- `auto_decode_classes`
@@ -121,6 +122,8 @@ Phase-3 status (Wave 3E):

The long-term target is that you describe *what the system should do* (for example broad-span monitoring intent, preferred signal families, auto-record/decode priorities), while the engine decides *how* to allocate surveillance, refinement and decoding budgets.

Phase-4 groundwork: `monitor_windows` allows multi-span gating within a single capture span; it is used to accept/reject candidates and inform refinement plans without changing the acquisition center/rate.

**CFAR modes:** `OFF`, `CA`, `OS`, `GOSCA`, `CASO`

---


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

@@ -149,6 +149,7 @@ func registerAPIHandlers(mux *http.ServeMux, cfgPath string, cfgManager *runtime
"monitor_start_hz": policy.MonitorStartHz,
"monitor_end_hz": policy.MonitorEndHz,
"monitor_span_hz": policy.MonitorSpanHz,
"monitor_windows": policy.MonitorWindows,
"signal_priorities": policy.SignalPriorities,
"auto_record_classes": policy.AutoRecordClasses,
"auto_decode_classes": policy.AutoDecodeClasses,


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

@@ -813,6 +813,17 @@ func spanForPolicy(policy pipeline.Policy, fallback float64) float64 {
if policy.MonitorSpanHz > 0 {
return policy.MonitorSpanHz
}
if len(policy.MonitorWindows) > 0 {
maxSpan := 0.0
for _, w := range policy.MonitorWindows {
if w.SpanHz > maxSpan {
maxSpan = w.SpanHz
}
}
if maxSpan > 0 {
return maxSpan
}
}
if policy.MonitorStartHz != 0 && policy.MonitorEndHz != 0 && policy.MonitorEndHz > policy.MonitorStartHz {
return policy.MonitorEndHz - policy.MonitorStartHz
}


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

@@ -15,6 +15,14 @@ type Band struct {
EndHz float64 `yaml:"end_hz" json:"end_hz"`
}

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"`
}

type DetectorConfig struct {
ThresholdDb float64 `yaml:"threshold_db" json:"threshold_db"`
MinDurationMs int `yaml:"min_duration_ms" json:"min_duration_ms"`
@@ -72,13 +80,14 @@ type DecoderConfig struct {
}

type PipelineGoalConfig struct {
Intent string `yaml:"intent" json:"intent"`
MonitorStartHz float64 `yaml:"monitor_start_hz" json:"monitor_start_hz"`
MonitorEndHz float64 `yaml:"monitor_end_hz" json:"monitor_end_hz"`
MonitorSpanHz float64 `yaml:"monitor_span_hz" json:"monitor_span_hz"`
SignalPriorities []string `yaml:"signal_priorities" json:"signal_priorities"`
AutoRecordClasses []string `yaml:"auto_record_classes" json:"auto_record_classes"`
AutoDecodeClasses []string `yaml:"auto_decode_classes" json:"auto_decode_classes"`
Intent string `yaml:"intent" json:"intent"`
MonitorStartHz float64 `yaml:"monitor_start_hz" json:"monitor_start_hz"`
MonitorEndHz float64 `yaml:"monitor_end_hz" json:"monitor_end_hz"`
MonitorSpanHz float64 `yaml:"monitor_span_hz" json:"monitor_span_hz"`
MonitorWindows []MonitorWindow `yaml:"monitor_windows" json:"monitor_windows"`
SignalPriorities []string `yaml:"signal_priorities" json:"signal_priorities"`
AutoRecordClasses []string `yaml:"auto_record_classes" json:"auto_record_classes"`
AutoDecodeClasses []string `yaml:"auto_decode_classes" json:"auto_decode_classes"`
}

type PipelineConfig struct {


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

@@ -1,6 +1,105 @@
package pipeline

import "sdr-wideband-suite/internal/config"

func NormalizeMonitorWindows(goals config.PipelineGoalConfig, centerHz float64) []MonitorWindow {
if len(goals.MonitorWindows) > 0 {
windows := make([]MonitorWindow, 0, len(goals.MonitorWindows))
for _, raw := range goals.MonitorWindows {
if win, ok := normalizeGoalWindow(raw, centerHz); ok {
windows = append(windows, win)
}
}
if len(windows) > 0 {
return windows
}
}
if goals.MonitorStartHz > 0 && goals.MonitorEndHz > goals.MonitorStartHz {
start := goals.MonitorStartHz
end := goals.MonitorEndHz
span := end - start
return []MonitorWindow{{
Label: "primary",
StartHz: start,
EndHz: end,
CenterHz: (start + end) / 2,
SpanHz: span,
Source: "goals:bounds",
}}
}
if goals.MonitorSpanHz > 0 && centerHz != 0 {
half := goals.MonitorSpanHz / 2
start := centerHz - half
end := centerHz + half
return []MonitorWindow{{
Label: "primary",
StartHz: start,
EndHz: end,
CenterHz: centerHz,
SpanHz: goals.MonitorSpanHz,
Source: "goals:span",
}}
}
return nil
}

func MonitorWindowBounds(windows []MonitorWindow) (float64, float64, bool) {
minStart := 0.0
maxEnd := 0.0
ok := false
for _, w := range windows {
if w.StartHz <= 0 || w.EndHz <= 0 || w.EndHz <= w.StartHz {
continue
}
if !ok || w.StartHz < minStart {
minStart = w.StartHz
}
if !ok || w.EndHz > maxEnd {
maxEnd = w.EndHz
}
ok = true
}
return minStart, maxEnd, ok
}

func normalizeGoalWindow(raw config.MonitorWindow, fallbackCenter float64) (MonitorWindow, bool) {
if raw.StartHz > 0 && raw.EndHz > raw.StartHz {
span := raw.EndHz - raw.StartHz
return MonitorWindow{
Label: raw.Label,
StartHz: raw.StartHz,
EndHz: raw.EndHz,
CenterHz: (raw.StartHz + raw.EndHz) / 2,
SpanHz: span,
Source: "goals:window:start_end",
}, true
}
center := raw.CenterHz
if center == 0 {
center = fallbackCenter
}
if center != 0 && raw.SpanHz > 0 {
half := raw.SpanHz / 2
source := "goals:window:center_span"
if raw.CenterHz == 0 {
source = "goals:window:span_default"
}
return MonitorWindow{
Label: raw.Label,
StartHz: center - half,
EndHz: center + half,
CenterHz: center,
SpanHz: raw.SpanHz,
Source: source,
}, true
}
return MonitorWindow{}, false
}

func monitorBounds(policy Policy) (float64, float64, bool) {
if len(policy.MonitorWindows) > 0 {
return MonitorWindowBounds(policy.MonitorWindows)
}
start := policy.MonitorStartHz
end := policy.MonitorEndHz
if start != 0 && end != 0 && end > start {
@@ -14,15 +113,32 @@ func monitorBounds(policy Policy) (float64, float64, bool) {
}

func candidateInMonitor(policy Policy, candidate Candidate) bool {
if len(policy.MonitorWindows) > 0 {
left, right := candidateBounds(candidate)
for _, win := range policy.MonitorWindows {
if win.StartHz <= 0 || win.EndHz <= 0 || win.EndHz <= win.StartHz {
continue
}
if right >= win.StartHz && left <= win.EndHz {
return true
}
}
return false
}
start, end, ok := monitorBounds(policy)
if !ok {
return true
}
left, right := candidateBounds(candidate)
return right >= start && left <= end
}

func candidateBounds(candidate Candidate) (float64, float64) {
left := candidate.CenterHz
right := candidate.CenterHz
if candidate.BandwidthHz > 0 {
left = candidate.CenterHz - candidate.BandwidthHz/2
right = candidate.CenterHz + candidate.BandwidthHz/2
}
return right >= start && left <= end
return left, right
}

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

@@ -0,0 +1,56 @@
package pipeline

import (
"testing"

"sdr-wideband-suite/internal/config"
)

func TestNormalizeMonitorWindows(t *testing.T) {
goals := config.PipelineGoalConfig{
MonitorWindows: []config.MonitorWindow{
{Label: "a", StartHz: 100, EndHz: 200},
{Label: "b", CenterHz: 500, SpanHz: 50},
},
}
windows := NormalizeMonitorWindows(goals, 0)
if len(windows) != 2 {
t.Fatalf("expected 2 windows, got %d", len(windows))
}
if windows[0].StartHz != 100 || windows[0].EndHz != 200 {
t.Fatalf("unexpected first window: %+v", windows[0])
}
if windows[1].CenterHz != 500 || windows[1].SpanHz != 50 {
t.Fatalf("unexpected second window: %+v", windows[1])
}
}

func TestMonitorWindowBounds(t *testing.T) {
windows := []MonitorWindow{
{StartHz: 100, EndHz: 200},
{StartHz: 50, EndHz: 90},
{StartHz: 500, EndHz: 800},
}
start, end, ok := MonitorWindowBounds(windows)
if !ok {
t.Fatalf("expected bounds")
}
if start != 50 || end != 800 {
t.Fatalf("unexpected bounds: %.0f %.0f", start, end)
}
}

func TestCandidateInMonitorWindows(t *testing.T) {
policy := Policy{
MonitorWindows: []MonitorWindow{
{StartHz: 100, EndHz: 200},
{StartHz: 300, EndHz: 400},
},
}
if !candidateInMonitor(policy, Candidate{CenterHz: 150}) {
t.Fatalf("expected candidate inside window")
}
if candidateInMonitor(policy, Candidate{CenterHz: 250}) {
t.Fatalf("expected candidate outside windows")
}
}

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

@@ -63,6 +63,7 @@ type RefinementPlan struct {
MonitorStartHz float64 `json:"monitor_start_hz,omitempty"`
MonitorEndHz float64 `json:"monitor_end_hz,omitempty"`
MonitorSpanHz float64 `json:"monitor_span_hz,omitempty"`
MonitorWindows []MonitorWindow `json:"monitor_windows,omitempty"`
DroppedByMonitor int `json:"dropped_by_monitor"`
DroppedBySNR int `json:"dropped_by_snr"`
DroppedByBudget int `json:"dropped_by_budget"`


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

@@ -10,6 +10,7 @@ type Policy struct {
MonitorStartHz float64 `json:"monitor_start_hz,omitempty"`
MonitorEndHz float64 `json:"monitor_end_hz,omitempty"`
MonitorSpanHz float64 `json:"monitor_span_hz,omitempty"`
MonitorWindows []MonitorWindow `json:"monitor_windows,omitempty"`
SignalPriorities []string `json:"signal_priorities,omitempty"`
AutoRecordClasses []string `json:"auto_record_classes,omitempty"`
AutoDecodeClasses []string `json:"auto_decode_classes,omitempty"`
@@ -72,6 +73,16 @@ func PolicyFromConfig(cfg config.Config) Policy {
}
p.RefinementStrategy, _ = refinementStrategy(p)
p.SurveillanceDetection = SurveillanceDetectionPolicyFromPolicy(p)
p.MonitorWindows = NormalizeMonitorWindows(cfg.Pipeline.Goals, cfg.CenterHz)
if len(p.MonitorWindows) > 0 {
if start, end, ok := MonitorWindowBounds(p.MonitorWindows); ok {
p.MonitorStartHz = start
p.MonitorEndHz = end
if end > start {
p.MonitorSpanHz = end - start
}
}
}
if p.MonitorSpanHz <= 0 && p.MonitorStartHz != 0 && p.MonitorEndHz != 0 && p.MonitorEndHz > p.MonitorStartHz {
p.MonitorSpanHz = p.MonitorEndHz - p.MonitorStartHz
}


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

@@ -76,3 +76,24 @@ func TestPolicyFromConfig(t *testing.T) {
t.Fatalf("expected refinement strategy to be set")
}
}

func TestPolicyFromConfigMonitorWindows(t *testing.T) {
cfg := config.Default()
cfg.Pipeline.Goals.MonitorWindows = []config.MonitorWindow{
{Label: "fm", StartHz: 88e6, EndHz: 108e6},
{Label: "air", CenterHz: 120e6, SpanHz: 2e6},
}
cfg.Pipeline.Goals.MonitorSpanHz = 0
cfg.Pipeline.Goals.MonitorStartHz = 0
cfg.Pipeline.Goals.MonitorEndHz = 0
p := PolicyFromConfig(cfg)
if len(p.MonitorWindows) != 2 {
t.Fatalf("expected monitor windows to be set")
}
if p.MonitorSpanHz <= 0 {
t.Fatalf("expected monitor span to be derived from windows")
}
if p.MonitorStartHz == 0 || p.MonitorEndHz == 0 || p.MonitorEndHz <= p.MonitorStartHz {
t.Fatalf("expected monitor bounds to be derived")
}
}

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

@@ -109,6 +109,9 @@ func BuildRefinementPlanWithBudget(candidates []Candidate, policy Policy, budget
plan.MonitorSpanHz = end - start
}
}
if len(policy.MonitorWindows) > 0 {
plan.MonitorWindows = append([]MonitorWindow(nil), policy.MonitorWindows...)
}
if len(candidates) == 0 {
return plan
}


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

@@ -29,6 +29,16 @@ type LevelEvidence struct {
Provenance string `json:"provenance,omitempty"`
}

// MonitorWindow describes a monitoring window to gate candidates.
type MonitorWindow struct {
Label string `json:"label,omitempty"`
StartHz float64 `json:"start_hz,omitempty"`
EndHz float64 `json:"end_hz,omitempty"`
CenterHz float64 `json:"center_hz,omitempty"`
SpanHz float64 `json:"span_hz,omitempty"`
Source string `json:"source,omitempty"`
}

// RefinementWindow describes the local analysis span that refinement should use.
type RefinementWindow struct {
CenterHz float64 `json:"center_hz"`


+ 37
- 9
internal/runtime/runtime.go Прегледај датотеку

@@ -2,6 +2,7 @@ package runtime

import (
"errors"
"fmt"
"math"
"strings"
"sync"
@@ -10,15 +11,16 @@ import (
)

type PipelineUpdate struct {
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"`
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"`
MonitorWindows *[]config.MonitorWindow `json:"monitor_windows"`
SignalPriorities *[]string `json:"signal_priorities"`
AutoRecordClasses *[]string `json:"auto_record_classes"`
AutoDecodeClasses *[]string `json:"auto_decode_classes"`
}

type SurveillanceUpdate struct {
@@ -199,6 +201,13 @@ func (m *Manager) ApplyConfig(update ConfigUpdate) (config.Config, error) {
}
next.Pipeline.Goals.MonitorSpanHz = *update.Pipeline.MonitorSpanHz
}
if update.Pipeline.MonitorWindows != nil {
windows := *update.Pipeline.MonitorWindows
if err := validateMonitorWindows(windows); err != nil {
return m.cfg, err
}
next.Pipeline.Goals.MonitorWindows = append([]config.MonitorWindow(nil), windows...)
}
if update.Pipeline.SignalPriorities != nil {
next.Pipeline.Goals.SignalPriorities = append([]string(nil), (*update.Pipeline.SignalPriorities)...)
}
@@ -497,6 +506,25 @@ func (m *Manager) ApplyConfig(update ConfigUpdate) (config.Config, error) {
return m.cfg, nil
}

func validateMonitorWindows(windows []config.MonitorWindow) error {
for i, w := range windows {
hasStart := w.StartHz != 0 || w.EndHz != 0
if hasStart {
if w.StartHz <= 0 || w.EndHz <= 0 || w.EndHz <= w.StartHz {
return fmt.Errorf("monitor_windows[%d] requires start_hz < end_hz", i)
}
continue
}
if w.CenterHz <= 0 {
return fmt.Errorf("monitor_windows[%d] requires center_hz when start/end not set", i)
}
if w.SpanHz <= 0 {
return fmt.Errorf("monitor_windows[%d] requires span_hz > 0 when start/end not set", i)
}
}
return nil
}

func (m *Manager) ApplySettings(update SettingsUpdate) (config.Config, error) {
m.mu.Lock()
defer m.mu.Unlock()


+ 8
- 0
internal/runtime/runtime_test.go Прегледај датотеку

@@ -26,6 +26,10 @@ func TestApplyConfigUpdate(t *testing.T) {
profile := "wideband-balanced"
intent := "wideband-surveillance"
monitorSpan := 500000.0
monitorWindows := []config.MonitorWindow{
{Label: "primary", StartHz: 100, EndHz: 200},
{Label: "secondary", CenterHz: 400, SpanHz: 50},
}
signalPriorities := []string{"digital", "weak"}
autoRecord := []string{"WFM"}
autoDecode := []string{"FT8"}
@@ -49,6 +53,7 @@ func TestApplyConfigUpdate(t *testing.T) {
Profile: &profile,
Intent: &intent,
MonitorSpanHz: &monitorSpan,
MonitorWindows: &monitorWindows,
SignalPriorities: &signalPriorities,
AutoRecordClasses: &autoRecord,
AutoDecodeClasses: &autoDecode,
@@ -117,6 +122,9 @@ func TestApplyConfigUpdate(t *testing.T) {
if updated.Pipeline.Goals.MonitorSpanHz != monitorSpan {
t.Fatalf("monitor span: %v", updated.Pipeline.Goals.MonitorSpanHz)
}
if len(updated.Pipeline.Goals.MonitorWindows) != len(monitorWindows) {
t.Fatalf("monitor windows not applied")
}
if len(updated.Pipeline.Goals.SignalPriorities) != len(signalPriorities) {
t.Fatalf("signal priorities not applied")
}


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