Wideband autonomous SDR analysis engine forked from sdr-visual-suite
Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

253 linhas
8.0KB

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