Wideband autonomous SDR analysis engine forked from sdr-visual-suite
25개 이상의 토픽을 선택하실 수 없습니다. Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

267 lines
8.5KB

  1. package pipeline
  2. import (
  3. "sort"
  4. "strings"
  5. )
  6. type ScheduledCandidate struct {
  7. Candidate Candidate `json:"candidate"`
  8. Priority float64 `json:"priority"`
  9. Score *RefinementScore `json:"score,omitempty"`
  10. Breakdown *RefinementScoreDetails `json:"breakdown,omitempty"`
  11. }
  12. type RefinementScoreModel struct {
  13. SNRWeight float64 `json:"snr_weight"`
  14. BandwidthWeight float64 `json:"bandwidth_weight"`
  15. PeakWeight float64 `json:"peak_weight"`
  16. EvidenceWeight float64 `json:"evidence_weight"`
  17. }
  18. type RefinementScoreDetails struct {
  19. SNRScore float64 `json:"snr_score"`
  20. BandwidthScore float64 `json:"bandwidth_score"`
  21. PeakScore float64 `json:"peak_score"`
  22. PolicyBoost float64 `json:"policy_boost"`
  23. EvidenceScore float64 `json:"evidence_score"`
  24. }
  25. type RefinementScore struct {
  26. Total float64 `json:"total"`
  27. Breakdown RefinementScoreDetails `json:"breakdown"`
  28. Weights *RefinementScoreModel `json:"weights,omitempty"`
  29. }
  30. type RefinementWorkItem struct {
  31. Candidate Candidate `json:"candidate"`
  32. Window RefinementWindow `json:"window,omitempty"`
  33. Execution *RefinementExecution `json:"execution,omitempty"`
  34. Priority float64 `json:"priority,omitempty"`
  35. Score *RefinementScore `json:"score,omitempty"`
  36. Breakdown *RefinementScoreDetails `json:"breakdown,omitempty"`
  37. Status string `json:"status,omitempty"`
  38. Reason string `json:"reason,omitempty"`
  39. }
  40. type RefinementExecution struct {
  41. Stage string `json:"stage,omitempty"`
  42. SampleRate int `json:"sample_rate,omitempty"`
  43. FFTSize int `json:"fft_size,omitempty"`
  44. CenterHz float64 `json:"center_hz,omitempty"`
  45. SpanHz float64 `json:"span_hz,omitempty"`
  46. Source string `json:"source,omitempty"`
  47. }
  48. const (
  49. RefinementStatusPlanned = "planned"
  50. RefinementStatusAdmitted = "admitted"
  51. RefinementStatusRunning = "running"
  52. RefinementStatusCompleted = "completed"
  53. RefinementStatusDropped = "dropped"
  54. RefinementStatusSkipped = "skipped"
  55. RefinementStatusDisplaced = "displaced"
  56. )
  57. const (
  58. RefinementReasonPlanned = "refinement:planned"
  59. RefinementReasonAdmitted = "refinement:admitted"
  60. RefinementReasonRunning = "refinement:running"
  61. RefinementReasonCompleted = "refinement:completed"
  62. RefinementReasonMonitorGate = "refinement:drop:monitor"
  63. RefinementReasonBelowSNR = "refinement:drop:snr"
  64. RefinementReasonBudget = "refinement:skip:budget"
  65. RefinementReasonDisabled = "refinement:drop:disabled"
  66. RefinementReasonUnclassified = "refinement:drop:unclassified"
  67. RefinementReasonDisplaced = "refinement:skip:displaced"
  68. )
  69. // BuildRefinementPlan scores and ranks candidates for costly local refinement.
  70. // Admission/budget enforcement is handled by arbitration to keep refinement/record/decode consistent.
  71. // Current heuristic is intentionally simple and deterministic; later phases can add
  72. // richer scoring (novelty, persistence, profile-aware band priorities, decoder value).
  73. func BuildRefinementPlan(candidates []Candidate, policy Policy) RefinementPlan {
  74. strategy, strategyReason := refinementStrategy(policy)
  75. budgetModel := BudgetModelFromPolicy(policy)
  76. budget := budgetModel.Refinement.Max
  77. plan := RefinementPlan{
  78. TotalCandidates: len(candidates),
  79. MinCandidateSNRDb: policy.MinCandidateSNRDb,
  80. Budget: budget,
  81. BudgetSource: budgetModel.Refinement.Source,
  82. Strategy: strategy,
  83. StrategyReason: strategyReason,
  84. }
  85. if start, end, ok := monitorBounds(policy); ok {
  86. plan.MonitorStartHz = start
  87. plan.MonitorEndHz = end
  88. if end > start {
  89. plan.MonitorSpanHz = end - start
  90. }
  91. }
  92. if len(candidates) == 0 {
  93. return plan
  94. }
  95. snrWeight, bwWeight, peakWeight := refinementIntentWeights(policy.Intent)
  96. scoreModel := RefinementScoreModel{
  97. SNRWeight: snrWeight,
  98. BandwidthWeight: bwWeight,
  99. PeakWeight: peakWeight,
  100. EvidenceWeight: 0.6,
  101. }
  102. scoreModel = applyStrategyWeights(strategy, scoreModel)
  103. plan.ScoreModel = scoreModel
  104. scored := make([]ScheduledCandidate, 0, len(candidates))
  105. workItems := make([]RefinementWorkItem, 0, len(candidates))
  106. for _, c := range candidates {
  107. if !candidateInMonitor(policy, c) {
  108. plan.DroppedByMonitor++
  109. workItems = append(workItems, RefinementWorkItem{
  110. Candidate: c,
  111. Status: RefinementStatusDropped,
  112. Reason: RefinementReasonMonitorGate,
  113. })
  114. continue
  115. }
  116. if c.SNRDb < policy.MinCandidateSNRDb {
  117. plan.DroppedBySNR++
  118. workItems = append(workItems, RefinementWorkItem{
  119. Candidate: c,
  120. Status: RefinementStatusDropped,
  121. Reason: RefinementReasonBelowSNR,
  122. })
  123. continue
  124. }
  125. snrScore := c.SNRDb * scoreModel.SNRWeight
  126. bwScore := 0.0
  127. peakScore := 0.0
  128. policyBoost := CandidatePriorityBoost(policy, c.Hint)
  129. if c.BandwidthHz > 0 {
  130. bwScore = minFloat64(c.BandwidthHz/25000.0, 6) * scoreModel.BandwidthWeight
  131. }
  132. if c.PeakDb > 0 {
  133. peakScore = (c.PeakDb / 20.0) * scoreModel.PeakWeight
  134. }
  135. evidenceScore := candidateEvidenceScore(c) * scoreModel.EvidenceWeight
  136. priority := snrScore + bwScore + peakScore + policyBoost
  137. priority += evidenceScore
  138. score := &RefinementScore{
  139. Total: priority,
  140. Breakdown: RefinementScoreDetails{
  141. SNRScore: snrScore,
  142. BandwidthScore: bwScore,
  143. PeakScore: peakScore,
  144. PolicyBoost: policyBoost,
  145. EvidenceScore: evidenceScore,
  146. },
  147. Weights: &scoreModel,
  148. }
  149. scored = append(scored, ScheduledCandidate{
  150. Candidate: c,
  151. Priority: priority,
  152. Score: score,
  153. Breakdown: &score.Breakdown,
  154. })
  155. workItems = append(workItems, RefinementWorkItem{
  156. Candidate: c,
  157. Priority: priority,
  158. Score: score,
  159. Breakdown: &score.Breakdown,
  160. Status: RefinementStatusPlanned,
  161. Reason: RefinementReasonPlanned,
  162. })
  163. }
  164. sort.Slice(scored, func(i, j int) bool {
  165. if scored[i].Priority == scored[j].Priority {
  166. return scored[i].Candidate.CenterHz < scored[j].Candidate.CenterHz
  167. }
  168. return scored[i].Priority > scored[j].Priority
  169. })
  170. if len(scored) > 0 {
  171. minPriority := scored[0].Priority
  172. maxPriority := scored[0].Priority
  173. sumPriority := 0.0
  174. for _, s := range scored {
  175. if s.Priority < minPriority {
  176. minPriority = s.Priority
  177. }
  178. if s.Priority > maxPriority {
  179. maxPriority = s.Priority
  180. }
  181. sumPriority += s.Priority
  182. }
  183. plan.PriorityMin = minPriority
  184. plan.PriorityMax = maxPriority
  185. plan.PriorityAvg = sumPriority / float64(len(scored))
  186. }
  187. plan.Ranked = append(plan.Ranked, scored...)
  188. plan.WorkItems = workItems
  189. return plan
  190. }
  191. func ScheduleCandidates(candidates []Candidate, policy Policy) []ScheduledCandidate {
  192. plan := BuildRefinementPlan(candidates, policy)
  193. if len(plan.Ranked) > 0 {
  194. return plan.Ranked
  195. }
  196. return plan.Selected
  197. }
  198. func refinementStrategy(policy Policy) (string, string) {
  199. intent := strings.ToLower(strings.TrimSpace(policy.Intent))
  200. profile := strings.ToLower(strings.TrimSpace(policy.Profile))
  201. switch {
  202. case strings.Contains(profile, "digital"):
  203. return "digital-hunting", "profile"
  204. case strings.Contains(profile, "archive"):
  205. return "archive-oriented", "profile"
  206. case strings.Contains(profile, "aggressive"):
  207. return "multi-resolution", "profile"
  208. case strings.Contains(intent, "digital") || strings.Contains(intent, "hunt") || strings.Contains(intent, "decode"):
  209. return "digital-hunting", "intent"
  210. case strings.Contains(intent, "archive") || strings.Contains(intent, "triage") || strings.Contains(policy.Mode, "archive"):
  211. return "archive-oriented", "intent"
  212. case strings.Contains(strings.ToLower(policy.SurveillanceStrategy), "multi"):
  213. return "multi-resolution", "surveillance-strategy"
  214. default:
  215. return "single-resolution", "default"
  216. }
  217. }
  218. func applyStrategyWeights(strategy string, model RefinementScoreModel) RefinementScoreModel {
  219. switch strings.ToLower(strings.TrimSpace(strategy)) {
  220. case "digital-hunting":
  221. model.SNRWeight *= 1.4
  222. model.BandwidthWeight *= 0.75
  223. model.PeakWeight *= 1.2
  224. case "archive-oriented":
  225. model.SNRWeight *= 1.1
  226. model.BandwidthWeight *= 1.6
  227. model.PeakWeight *= 1.05
  228. case "multi-resolution", "multi", "multi-res", "multi_res":
  229. model.SNRWeight *= 1.15
  230. model.BandwidthWeight *= 1.1
  231. model.PeakWeight *= 1.15
  232. case "single-resolution":
  233. model.SNRWeight *= 1.1
  234. model.BandwidthWeight *= 1.0
  235. model.PeakWeight *= 1.0
  236. }
  237. return model
  238. }
  239. func candidateEvidenceScore(candidate Candidate) float64 {
  240. levels := CandidateEvidenceLevelCount(candidate)
  241. if levels <= 1 {
  242. return 0
  243. }
  244. return float64(levels - 1)
  245. }
  246. func minFloat64(a, b float64) float64 {
  247. if a < b {
  248. return a
  249. }
  250. return b
  251. }