Wideband autonomous SDR analysis engine forked from sdr-visual-suite
Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

364 Zeilen
12KB

  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. Tier string `json:"tier,omitempty"`
  10. Score *RefinementScore `json:"score,omitempty"`
  11. Breakdown *RefinementScoreDetails `json:"breakdown,omitempty"`
  12. }
  13. type RefinementScoreModel struct {
  14. SNRWeight float64 `json:"snr_weight"`
  15. BandwidthWeight float64 `json:"bandwidth_weight"`
  16. PeakWeight float64 `json:"peak_weight"`
  17. EvidenceWeight float64 `json:"evidence_weight"`
  18. }
  19. type RefinementScoreDetails struct {
  20. SNRScore float64 `json:"snr_score"`
  21. BandwidthScore float64 `json:"bandwidth_score"`
  22. PeakScore float64 `json:"peak_score"`
  23. PolicyBoost float64 `json:"policy_boost"`
  24. EvidenceScore float64 `json:"evidence_score"`
  25. EvidenceDetail *EvidenceScoreDetails `json:"evidence_detail,omitempty"`
  26. }
  27. type RefinementScore struct {
  28. Total float64 `json:"total"`
  29. Breakdown RefinementScoreDetails `json:"breakdown"`
  30. Weights *RefinementScoreModel `json:"weights,omitempty"`
  31. }
  32. type RefinementWorkItem struct {
  33. Candidate Candidate `json:"candidate"`
  34. Window RefinementWindow `json:"window,omitempty"`
  35. Execution *RefinementExecution `json:"execution,omitempty"`
  36. Priority float64 `json:"priority,omitempty"`
  37. Score *RefinementScore `json:"score,omitempty"`
  38. Breakdown *RefinementScoreDetails `json:"breakdown,omitempty"`
  39. Status string `json:"status,omitempty"`
  40. Reason string `json:"reason,omitempty"`
  41. Admission *PriorityAdmission `json:"admission,omitempty"`
  42. }
  43. type RefinementExecution struct {
  44. Stage string `json:"stage,omitempty"`
  45. SampleRate int `json:"sample_rate,omitempty"`
  46. FFTSize int `json:"fft_size,omitempty"`
  47. CenterHz float64 `json:"center_hz,omitempty"`
  48. SpanHz float64 `json:"span_hz,omitempty"`
  49. Source string `json:"source,omitempty"`
  50. }
  51. const (
  52. RefinementStatusPlanned = "planned"
  53. RefinementStatusAdmitted = "admitted"
  54. RefinementStatusRunning = "running"
  55. RefinementStatusCompleted = "completed"
  56. RefinementStatusDropped = "dropped"
  57. RefinementStatusSkipped = "skipped"
  58. RefinementStatusDisplaced = "displaced"
  59. )
  60. const (
  61. RefinementReasonPlanned = "refinement:planned"
  62. RefinementReasonAdmitted = "refinement:admitted"
  63. RefinementReasonRunning = "refinement:running"
  64. RefinementReasonCompleted = "refinement:completed"
  65. RefinementReasonMonitorGate = "refinement:drop:monitor"
  66. RefinementReasonBelowSNR = "refinement:drop:snr"
  67. RefinementReasonBudget = "refinement:skip:budget"
  68. RefinementReasonDisabled = "refinement:drop:disabled"
  69. RefinementReasonUnclassified = "refinement:drop:unclassified"
  70. RefinementReasonDisplaced = "refinement:skip:displaced"
  71. )
  72. // BuildRefinementPlan scores and ranks candidates for costly local refinement.
  73. // Admission/budget enforcement is handled by arbitration to keep refinement/record/decode consistent.
  74. // Current heuristic is intentionally simple and deterministic; later phases can add
  75. // richer scoring (novelty, persistence, profile-aware band priorities, decoder value).
  76. func BuildRefinementPlan(candidates []Candidate, policy Policy) RefinementPlan {
  77. strategy, strategyReason := refinementStrategy(policy)
  78. budgetModel := BudgetModelFromPolicy(policy)
  79. budget := budgetModel.Refinement.Max
  80. holdPolicy := HoldPolicyFromPolicy(policy)
  81. plan := RefinementPlan{
  82. TotalCandidates: len(candidates),
  83. MinCandidateSNRDb: policy.MinCandidateSNRDb,
  84. Budget: budget,
  85. BudgetSource: budgetModel.Refinement.Source,
  86. Strategy: strategy,
  87. StrategyReason: strategyReason,
  88. }
  89. if start, end, ok := monitorBounds(policy); ok {
  90. plan.MonitorStartHz = start
  91. plan.MonitorEndHz = end
  92. if end > start {
  93. plan.MonitorSpanHz = end - start
  94. }
  95. }
  96. if len(candidates) == 0 {
  97. return plan
  98. }
  99. snrWeight, bwWeight, peakWeight := refinementIntentWeights(policy.Intent)
  100. scoreModel := RefinementScoreModel{
  101. SNRWeight: snrWeight,
  102. BandwidthWeight: bwWeight,
  103. PeakWeight: peakWeight,
  104. EvidenceWeight: 0.6,
  105. }
  106. scoreModel = applyStrategyWeights(strategy, scoreModel)
  107. plan.ScoreModel = scoreModel
  108. scored := make([]ScheduledCandidate, 0, len(candidates))
  109. workItems := make([]RefinementWorkItem, 0, len(candidates))
  110. for _, c := range candidates {
  111. candidate := c
  112. RefreshCandidateEvidenceState(&candidate)
  113. if !candidateInMonitor(policy, candidate) {
  114. plan.DroppedByMonitor++
  115. workItems = append(workItems, RefinementWorkItem{
  116. Candidate: candidate,
  117. Status: RefinementStatusDropped,
  118. Reason: RefinementReasonMonitorGate,
  119. Admission: &PriorityAdmission{
  120. Tier: PriorityTierBackground,
  121. Class: AdmissionClassDrop,
  122. Basis: "refinement",
  123. Reason: admissionReason(RefinementReasonMonitorGate, policy, holdPolicy),
  124. },
  125. })
  126. continue
  127. }
  128. if candidate.SNRDb < policy.MinCandidateSNRDb {
  129. plan.DroppedBySNR++
  130. workItems = append(workItems, RefinementWorkItem{
  131. Candidate: candidate,
  132. Status: RefinementStatusDropped,
  133. Reason: RefinementReasonBelowSNR,
  134. Admission: &PriorityAdmission{
  135. Tier: PriorityTierBackground,
  136. Class: AdmissionClassDrop,
  137. Basis: "refinement",
  138. Reason: admissionReason(RefinementReasonBelowSNR, policy, holdPolicy),
  139. },
  140. })
  141. continue
  142. }
  143. snrScore := candidate.SNRDb * scoreModel.SNRWeight
  144. bwScore := 0.0
  145. peakScore := 0.0
  146. policyBoost := CandidatePriorityBoost(policy, candidate.Hint)
  147. if candidate.BandwidthHz > 0 {
  148. bwScore = minFloat64(candidate.BandwidthHz/25000.0, 6) * scoreModel.BandwidthWeight
  149. }
  150. if candidate.PeakDb > 0 {
  151. peakScore = (candidate.PeakDb / 20.0) * scoreModel.PeakWeight
  152. }
  153. rawEvidenceScore, evidenceDetail := candidateEvidenceScore(candidate, strategy)
  154. evidenceDetail.Weight = scoreModel.EvidenceWeight
  155. evidenceDetail.RawScore = rawEvidenceScore
  156. evidenceDetail.WeightedScore = rawEvidenceScore * scoreModel.EvidenceWeight
  157. evidenceScore := evidenceDetail.WeightedScore
  158. priority := snrScore + bwScore + peakScore + policyBoost
  159. priority += evidenceScore
  160. score := &RefinementScore{
  161. Total: priority,
  162. Breakdown: RefinementScoreDetails{
  163. SNRScore: snrScore,
  164. BandwidthScore: bwScore,
  165. PeakScore: peakScore,
  166. PolicyBoost: policyBoost,
  167. EvidenceScore: evidenceScore,
  168. EvidenceDetail: &evidenceDetail,
  169. },
  170. Weights: &scoreModel,
  171. }
  172. scored = append(scored, ScheduledCandidate{
  173. Candidate: candidate,
  174. Priority: priority,
  175. Score: score,
  176. Breakdown: &score.Breakdown,
  177. })
  178. workItems = append(workItems, RefinementWorkItem{
  179. Candidate: candidate,
  180. Priority: priority,
  181. Score: score,
  182. Breakdown: &score.Breakdown,
  183. Status: RefinementStatusPlanned,
  184. Reason: RefinementReasonPlanned,
  185. Admission: &PriorityAdmission{
  186. Class: AdmissionClassPlanned,
  187. Score: priority,
  188. Basis: "refinement",
  189. Reason: admissionReason(RefinementReasonPlanned, policy, holdPolicy),
  190. },
  191. })
  192. }
  193. sort.Slice(scored, func(i, j int) bool {
  194. if scored[i].Priority == scored[j].Priority {
  195. return scored[i].Candidate.CenterHz < scored[j].Candidate.CenterHz
  196. }
  197. return scored[i].Priority > scored[j].Priority
  198. })
  199. if len(scored) > 0 {
  200. minPriority := scored[0].Priority
  201. maxPriority := scored[0].Priority
  202. sumPriority := 0.0
  203. for _, s := range scored {
  204. if s.Priority < minPriority {
  205. minPriority = s.Priority
  206. }
  207. if s.Priority > maxPriority {
  208. maxPriority = s.Priority
  209. }
  210. sumPriority += s.Priority
  211. }
  212. plan.PriorityMin = minPriority
  213. plan.PriorityMax = maxPriority
  214. plan.PriorityAvg = sumPriority / float64(len(scored))
  215. for i := range scored {
  216. scored[i].Tier = PriorityTierFromRange(scored[i].Priority, minPriority, maxPriority)
  217. }
  218. for i := range workItems {
  219. if workItems[i].Admission == nil {
  220. continue
  221. }
  222. if workItems[i].Status != RefinementStatusPlanned {
  223. continue
  224. }
  225. workItems[i].Admission.Tier = PriorityTierFromRange(workItems[i].Priority, minPriority, maxPriority)
  226. }
  227. }
  228. plan.Ranked = append(plan.Ranked, scored...)
  229. plan.WorkItems = workItems
  230. return plan
  231. }
  232. func ScheduleCandidates(candidates []Candidate, policy Policy) []ScheduledCandidate {
  233. plan := BuildRefinementPlan(candidates, policy)
  234. if len(plan.Ranked) > 0 {
  235. return plan.Ranked
  236. }
  237. return plan.Selected
  238. }
  239. func refinementStrategy(policy Policy) (string, string) {
  240. intent := strings.ToLower(strings.TrimSpace(policy.Intent))
  241. profile := strings.ToLower(strings.TrimSpace(policy.Profile))
  242. switch {
  243. case strings.Contains(profile, "digital"):
  244. return "digital-hunting", "profile"
  245. case strings.Contains(profile, "archive"):
  246. return "archive-oriented", "profile"
  247. case strings.Contains(profile, "aggressive"):
  248. return "multi-resolution", "profile"
  249. case strings.Contains(intent, "digital") || strings.Contains(intent, "hunt") || strings.Contains(intent, "decode"):
  250. return "digital-hunting", "intent"
  251. case strings.Contains(intent, "archive") || strings.Contains(intent, "triage") || strings.Contains(policy.Mode, "archive"):
  252. return "archive-oriented", "intent"
  253. case strings.Contains(strings.ToLower(policy.SurveillanceStrategy), "multi"):
  254. return "multi-resolution", "surveillance-strategy"
  255. default:
  256. return "single-resolution", "default"
  257. }
  258. }
  259. func applyStrategyWeights(strategy string, model RefinementScoreModel) RefinementScoreModel {
  260. switch strings.ToLower(strings.TrimSpace(strategy)) {
  261. case "digital-hunting":
  262. model.SNRWeight *= 1.4
  263. model.BandwidthWeight *= 0.75
  264. model.PeakWeight *= 1.2
  265. case "archive-oriented":
  266. model.SNRWeight *= 1.1
  267. model.BandwidthWeight *= 1.6
  268. model.PeakWeight *= 1.05
  269. case "multi-resolution", "multi", "multi-res", "multi_res":
  270. model.SNRWeight *= 1.15
  271. model.BandwidthWeight *= 1.1
  272. model.PeakWeight *= 1.15
  273. case "single-resolution":
  274. model.SNRWeight *= 1.1
  275. model.BandwidthWeight *= 1.0
  276. model.PeakWeight *= 1.0
  277. }
  278. return model
  279. }
  280. func candidateEvidenceScore(candidate Candidate, strategy string) (float64, EvidenceScoreDetails) {
  281. state := CandidateEvidenceStateFor(candidate)
  282. details := EvidenceScoreDetails{
  283. DetectionLevels: state.DetectionLevelCount,
  284. PrimaryLevels: state.PrimaryLevelCount,
  285. DerivedLevels: state.DerivedLevelCount,
  286. SupportLevels: state.SupportLevelCount,
  287. ProvenanceCount: len(state.Provenance),
  288. DerivedOnly: state.DerivedOnly,
  289. MultiLevelConfirmed: state.MultiLevelConfirmed,
  290. }
  291. score := 0.0
  292. if state.MultiLevelConfirmed && state.DetectionLevelCount > 1 {
  293. bonus := 0.85 * float64(state.DetectionLevelCount-1)
  294. score += bonus
  295. details.MultiLevelBonus = bonus
  296. }
  297. if len(state.Provenance) > 1 {
  298. bonus := 0.15 * float64(len(state.Provenance)-1)
  299. score += bonus
  300. details.ProvenanceBonus = bonus
  301. }
  302. if state.DerivedOnly {
  303. penalty := 0.35
  304. score -= penalty
  305. details.DerivedPenalty = -penalty
  306. }
  307. switch strings.ToLower(strings.TrimSpace(strategy)) {
  308. case "multi-resolution", "multi", "multi-res", "multi_res":
  309. if state.DerivedOnly {
  310. bias := 0.2
  311. score += bias
  312. details.StrategyBias = bias
  313. } else if state.MultiLevelConfirmed {
  314. bias := 0.1
  315. score += bias
  316. details.StrategyBias = bias
  317. }
  318. case "digital-hunting":
  319. if state.DerivedOnly {
  320. bias := -0.15
  321. score += bias
  322. details.StrategyBias = bias
  323. } else if state.MultiLevelConfirmed {
  324. bias := 0.05
  325. score += bias
  326. details.StrategyBias = bias
  327. }
  328. case "archive-oriented":
  329. if state.DerivedOnly {
  330. bias := -0.1
  331. score += bias
  332. details.StrategyBias = bias
  333. }
  334. case "single-resolution":
  335. if state.MultiLevelConfirmed {
  336. bias := 0.05
  337. score += bias
  338. details.StrategyBias = bias
  339. }
  340. }
  341. return score, details
  342. }
  343. func minFloat64(a, b float64) float64 {
  344. if a < b {
  345. return a
  346. }
  347. return b
  348. }