Wideband autonomous SDR analysis engine forked from sdr-visual-suite
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

229 строки
6.8KB

  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. Priority float64 `json:"priority,omitempty"`
  32. Score *RefinementScore `json:"score,omitempty"`
  33. Breakdown *RefinementScoreDetails `json:"breakdown,omitempty"`
  34. Status string `json:"status,omitempty"`
  35. Reason string `json:"reason,omitempty"`
  36. }
  37. const (
  38. RefinementStatusSelected = "selected"
  39. RefinementStatusDropped = "dropped"
  40. RefinementStatusDeferred = "deferred"
  41. )
  42. const (
  43. RefinementReasonSelected = "selected"
  44. RefinementReasonMonitorGate = "dropped:monitor"
  45. RefinementReasonBelowSNR = "dropped:snr"
  46. RefinementReasonBudget = "dropped:budget"
  47. RefinementReasonDisabled = "dropped:disabled"
  48. RefinementReasonUnclassified = "dropped:unclassified"
  49. )
  50. // BuildRefinementPlan scores and budgets candidates for costly local refinement.
  51. // Current heuristic is intentionally simple and deterministic; later phases can add
  52. // richer scoring (novelty, persistence, profile-aware band priorities, decoder value).
  53. func BuildRefinementPlan(candidates []Candidate, policy Policy) RefinementPlan {
  54. budget := refinementBudget(policy)
  55. strategy, strategyReason := refinementStrategy(policy)
  56. plan := RefinementPlan{
  57. TotalCandidates: len(candidates),
  58. MinCandidateSNRDb: policy.MinCandidateSNRDb,
  59. Budget: budget,
  60. Strategy: strategy,
  61. StrategyReason: strategyReason,
  62. }
  63. if start, end, ok := monitorBounds(policy); ok {
  64. plan.MonitorStartHz = start
  65. plan.MonitorEndHz = end
  66. if end > start {
  67. plan.MonitorSpanHz = end - start
  68. }
  69. }
  70. if len(candidates) == 0 {
  71. return plan
  72. }
  73. snrWeight, bwWeight, peakWeight := refinementIntentWeights(policy.Intent)
  74. scoreModel := RefinementScoreModel{
  75. SNRWeight: snrWeight,
  76. BandwidthWeight: bwWeight,
  77. PeakWeight: peakWeight,
  78. }
  79. plan.ScoreModel = scoreModel
  80. scored := make([]ScheduledCandidate, 0, len(candidates))
  81. workItems := make([]RefinementWorkItem, 0, len(candidates))
  82. for _, c := range candidates {
  83. if !candidateInMonitor(policy, c) {
  84. plan.DroppedByMonitor++
  85. workItems = append(workItems, RefinementWorkItem{
  86. Candidate: c,
  87. Status: RefinementStatusDropped,
  88. Reason: RefinementReasonMonitorGate,
  89. })
  90. continue
  91. }
  92. if c.SNRDb < policy.MinCandidateSNRDb {
  93. plan.DroppedBySNR++
  94. workItems = append(workItems, RefinementWorkItem{
  95. Candidate: c,
  96. Status: RefinementStatusDropped,
  97. Reason: RefinementReasonBelowSNR,
  98. })
  99. continue
  100. }
  101. snrScore := c.SNRDb * snrWeight
  102. bwScore := 0.0
  103. peakScore := 0.0
  104. policyBoost := CandidatePriorityBoost(policy, c.Hint)
  105. if c.BandwidthHz > 0 {
  106. bwScore = minFloat64(c.BandwidthHz/25000.0, 6) * bwWeight
  107. }
  108. if c.PeakDb > 0 {
  109. peakScore = (c.PeakDb / 20.0) * peakWeight
  110. }
  111. priority := snrScore + bwScore + peakScore + policyBoost
  112. score := &RefinementScore{
  113. Total: priority,
  114. Breakdown: RefinementScoreDetails{
  115. SNRScore: snrScore,
  116. BandwidthScore: bwScore,
  117. PeakScore: peakScore,
  118. PolicyBoost: policyBoost,
  119. },
  120. Weights: &scoreModel,
  121. }
  122. scored = append(scored, ScheduledCandidate{
  123. Candidate: c,
  124. Priority: priority,
  125. Score: score,
  126. Breakdown: &score.Breakdown,
  127. })
  128. workItems = append(workItems, RefinementWorkItem{
  129. Candidate: c,
  130. Priority: priority,
  131. Score: score,
  132. Breakdown: &score.Breakdown,
  133. Status: RefinementStatusDeferred,
  134. })
  135. }
  136. sort.Slice(scored, func(i, j int) bool {
  137. if scored[i].Priority == scored[j].Priority {
  138. return scored[i].Candidate.CenterHz < scored[j].Candidate.CenterHz
  139. }
  140. return scored[i].Priority > scored[j].Priority
  141. })
  142. if len(scored) > 0 {
  143. minPriority := scored[0].Priority
  144. maxPriority := scored[0].Priority
  145. sumPriority := 0.0
  146. for _, s := range scored {
  147. if s.Priority < minPriority {
  148. minPriority = s.Priority
  149. }
  150. if s.Priority > maxPriority {
  151. maxPriority = s.Priority
  152. }
  153. sumPriority += s.Priority
  154. }
  155. plan.PriorityMin = minPriority
  156. plan.PriorityMax = maxPriority
  157. plan.PriorityAvg = sumPriority / float64(len(scored))
  158. }
  159. limit := plan.Budget
  160. if limit <= 0 || limit > len(scored) {
  161. limit = len(scored)
  162. }
  163. plan.Selected = scored[:limit]
  164. if len(plan.Selected) > 0 {
  165. plan.PriorityCutoff = plan.Selected[len(plan.Selected)-1].Priority
  166. }
  167. plan.DroppedByBudget = len(scored) - len(plan.Selected)
  168. if len(plan.Selected) > 0 {
  169. selected := map[int64]struct{}{}
  170. for _, s := range plan.Selected {
  171. selected[s.Candidate.ID] = struct{}{}
  172. }
  173. for i := range workItems {
  174. item := &workItems[i]
  175. if _, ok := selected[item.Candidate.ID]; ok {
  176. item.Status = RefinementStatusSelected
  177. item.Reason = RefinementReasonSelected
  178. } else if item.Status == RefinementStatusDeferred {
  179. item.Status = RefinementStatusDropped
  180. item.Reason = RefinementReasonBudget
  181. }
  182. }
  183. }
  184. plan.WorkItems = workItems
  185. return plan
  186. }
  187. func ScheduleCandidates(candidates []Candidate, policy Policy) []ScheduledCandidate {
  188. return BuildRefinementPlan(candidates, policy).Selected
  189. }
  190. func refinementBudget(policy Policy) int {
  191. budget := policy.MaxRefinementJobs
  192. if policy.RefinementMaxConcurrent > 0 && (budget <= 0 || policy.RefinementMaxConcurrent < budget) {
  193. budget = policy.RefinementMaxConcurrent
  194. }
  195. return budget
  196. }
  197. func refinementStrategy(policy Policy) (string, string) {
  198. intent := strings.ToLower(strings.TrimSpace(policy.Intent))
  199. switch {
  200. case strings.Contains(intent, "digital") || strings.Contains(intent, "hunt") || strings.Contains(intent, "decode"):
  201. return "digital-hunting", "intent"
  202. case strings.Contains(intent, "archive") || strings.Contains(intent, "triage") || strings.Contains(policy.Mode, "archive"):
  203. return "archive-oriented", "intent"
  204. case strings.Contains(strings.ToLower(policy.SurveillanceStrategy), "multi"):
  205. return "multi-resolution", "surveillance-strategy"
  206. default:
  207. return "single-resolution", "default"
  208. }
  209. }
  210. func minFloat64(a, b float64) float64 {
  211. if a < b {
  212. return a
  213. }
  214. return b
  215. }