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

458 строки
15KB

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