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

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