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

390 строки
13KB

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