Wideband autonomous SDR analysis engine forked from sdr-visual-suite
No puede seleccionar más de 25 temas Los temas deben comenzar con una letra o número, pueden incluir guiones ('-') y pueden tener hasta 35 caracteres de largo.

408 líneas
13KB

  1. package pipeline
  2. import (
  3. "math"
  4. "strings"
  5. "time"
  6. )
  7. type HoldPolicy struct {
  8. BaseMs int `json:"base_ms"`
  9. RefinementMs int `json:"refinement_ms"`
  10. RecordMs int `json:"record_ms"`
  11. DecodeMs int `json:"decode_ms"`
  12. Profile string `json:"profile,omitempty"`
  13. Strategy string `json:"strategy,omitempty"`
  14. Reasons []string `json:"reasons,omitempty"`
  15. }
  16. type RefinementHold struct {
  17. Active map[int64]time.Time
  18. }
  19. type RefinementAdmission struct {
  20. Budget int `json:"budget"`
  21. BudgetSource string `json:"budget_source,omitempty"`
  22. DecisionHoldMs int `json:"decision_hold_ms,omitempty"`
  23. HoldMs int `json:"hold_ms"`
  24. HoldSource string `json:"hold_source,omitempty"`
  25. Planned int `json:"planned"`
  26. Admitted int `json:"admitted"`
  27. Skipped int `json:"skipped"`
  28. Displaced int `json:"displaced"`
  29. DisplacedByHold int `json:"displaced_by_hold,omitempty"`
  30. HoldActive int `json:"hold_active"`
  31. HoldSelected int `json:"hold_selected"`
  32. HoldProtected int `json:"hold_protected"`
  33. HoldExpired int `json:"hold_expired"`
  34. HoldDisplaced int `json:"hold_displaced"`
  35. Opportunistic int `json:"opportunistic"`
  36. PriorityCutoff float64 `json:"priority_cutoff,omitempty"`
  37. PriorityTier string `json:"priority_tier,omitempty"`
  38. Reason string `json:"reason,omitempty"`
  39. Pressure BudgetPressure `json:"pressure,omitempty"`
  40. }
  41. type RefinementAdmissionResult struct {
  42. Plan RefinementPlan
  43. WorkItems []RefinementWorkItem
  44. Admitted []ScheduledCandidate
  45. Admission RefinementAdmission
  46. }
  47. func HoldPolicyFromPolicy(policy Policy) HoldPolicy {
  48. base := policy.DecisionHoldMs
  49. if base < 0 {
  50. base = 0
  51. }
  52. refMult := 1.0
  53. recMult := 1.0
  54. decMult := 1.0
  55. reasons := make([]string, 0, 2)
  56. profile := strings.ToLower(strings.TrimSpace(policy.Profile))
  57. strategy := strings.ToLower(strings.TrimSpace(policy.RefinementStrategy))
  58. intent := strings.ToLower(strings.TrimSpace(policy.Intent))
  59. archiveProfile := profileContains(profile, "archive")
  60. archiveStrategy := strategyContains(strategy, "archive")
  61. if archiveProfile || archiveStrategy {
  62. recMult *= 1.5
  63. decMult *= 1.1
  64. refMult *= 1.2
  65. if archiveProfile {
  66. reasons = append(reasons, HoldReasonProfileArchive)
  67. }
  68. if archiveStrategy {
  69. reasons = append(reasons, HoldReasonStrategyArchive)
  70. }
  71. }
  72. digitalProfile := profileContains(profile, "digital")
  73. digitalStrategy := strategyContains(strategy, "digital")
  74. if digitalProfile || digitalStrategy {
  75. decMult *= 1.6
  76. recMult *= 0.85
  77. refMult *= 1.1
  78. if digitalProfile {
  79. reasons = append(reasons, HoldReasonProfileDigital)
  80. }
  81. if digitalStrategy {
  82. reasons = append(reasons, HoldReasonStrategyDigital)
  83. }
  84. }
  85. if profileContains(profile, "aggressive") {
  86. refMult *= 1.15
  87. reasons = append(reasons, HoldReasonProfileAggressive)
  88. }
  89. if strategyContains(strings.ToLower(strings.TrimSpace(policy.SurveillanceStrategy)), "multi") {
  90. refMult *= 1.1
  91. reasons = append(reasons, HoldReasonStrategyMultiRes)
  92. }
  93. intentArchive := strings.Contains(intent, "archive") || strings.Contains(intent, "triage") || strings.Contains(intent, "record")
  94. intentDecode := strings.Contains(intent, "decode") || strings.Contains(intent, "digital") || strings.Contains(intent, "analysis")
  95. intentSurveillance := strings.Contains(intent, "surveillance") || strings.Contains(intent, "wideband")
  96. if intentArchive {
  97. recMult *= 1.25
  98. refMult *= 1.1
  99. decMult *= 1.05
  100. reasons = append(reasons, HoldReasonIntentArchive)
  101. }
  102. if intentDecode {
  103. decMult *= 1.25
  104. refMult *= 1.05
  105. reasons = append(reasons, HoldReasonIntentDecode)
  106. }
  107. if intentSurveillance {
  108. refMult *= 1.1
  109. reasons = append(reasons, HoldReasonIntentSurveillance)
  110. }
  111. return HoldPolicy{
  112. BaseMs: base,
  113. RefinementMs: scaleHold(base, refMult),
  114. RecordMs: scaleHold(base, recMult),
  115. DecodeMs: scaleHold(base, decMult),
  116. Profile: policy.Profile,
  117. Strategy: policy.RefinementStrategy,
  118. Reasons: reasons,
  119. }
  120. }
  121. func AdmitRefinementPlan(plan RefinementPlan, policy Policy, now time.Time, hold *RefinementHold) RefinementAdmissionResult {
  122. ranked := plan.Ranked
  123. if len(ranked) == 0 {
  124. ranked = plan.Selected
  125. }
  126. workItems := append([]RefinementWorkItem(nil), plan.WorkItems...)
  127. admission := RefinementAdmission{
  128. Budget: plan.Budget,
  129. BudgetSource: plan.BudgetSource,
  130. }
  131. if len(ranked) == 0 {
  132. admission.Reason = ReasonAdmissionNoCandidates
  133. return RefinementAdmissionResult{Plan: plan, WorkItems: workItems, Admission: admission}
  134. }
  135. holdPolicy := HoldPolicyFromPolicy(policy)
  136. budgetModel := BudgetModelFromPolicy(policy)
  137. admission.DecisionHoldMs = holdPolicy.BaseMs
  138. admission.HoldMs = holdPolicy.RefinementMs
  139. admission.HoldSource = "resources.decision_hold_ms"
  140. if len(holdPolicy.Reasons) > 0 {
  141. admission.HoldSource += ":" + strings.Join(holdPolicy.Reasons, ",")
  142. }
  143. planned := len(ranked)
  144. admission.Planned = planned
  145. selected := map[int64]struct{}{}
  146. held := map[int64]struct{}{}
  147. protected := map[int64]struct{}{}
  148. expired := map[int64]struct{}{}
  149. if hold != nil {
  150. expired = expireHold(hold.Active, now)
  151. for id := range hold.Active {
  152. if rankedContains(ranked, id) {
  153. selected[id] = struct{}{}
  154. held[id] = struct{}{}
  155. }
  156. }
  157. }
  158. limit := plan.Budget
  159. if limit <= 0 || limit > planned {
  160. limit = planned
  161. }
  162. if len(selected) > limit {
  163. limit = len(selected)
  164. if limit > planned {
  165. limit = planned
  166. }
  167. }
  168. tierByID := map[int64]string{}
  169. scoreByID := map[int64]float64{}
  170. familyByID := map[int64]string{}
  171. familyRankByID := map[int64]int{}
  172. familyFloorByID := map[int64]string{}
  173. for _, cand := range ranked {
  174. id := cand.Candidate.ID
  175. family, familyRank := signalPriorityMatch(policy, cand.Candidate.Hint, "")
  176. familyByID[id] = family
  177. familyRankByID[id] = familyRank
  178. familyFloorByID[id] = signalPriorityTierFloor(familyRank)
  179. baseTier := PriorityTierFromRange(cand.Priority, plan.PriorityMin, plan.PriorityMax)
  180. tierByID[id] = applyTierFloor(baseTier, familyFloorByID[id])
  181. scoreByID[id] = cand.Priority
  182. }
  183. for id := range held {
  184. if isProtectedTier(tierByID[id]) {
  185. protected[id] = struct{}{}
  186. }
  187. }
  188. displaceable := buildDisplaceableHold(held, protected, tierByID, scoreByID, familyRankByID)
  189. opportunistic := map[int64]struct{}{}
  190. displacedHold := map[int64]struct{}{}
  191. for _, cand := range ranked {
  192. if _, ok := selected[cand.Candidate.ID]; ok {
  193. continue
  194. }
  195. if len(selected) < limit {
  196. selected[cand.Candidate.ID] = struct{}{}
  197. continue
  198. }
  199. if len(displaceable) == 0 {
  200. continue
  201. }
  202. target := displaceable[0]
  203. if priorityTierRank(tierByID[cand.Candidate.ID]) <= priorityTierRank(tierByID[target]) {
  204. continue
  205. }
  206. displaceable = displaceable[1:]
  207. delete(selected, target)
  208. displacedHold[target] = struct{}{}
  209. selected[cand.Candidate.ID] = struct{}{}
  210. opportunistic[cand.Candidate.ID] = struct{}{}
  211. }
  212. if hold != nil && admission.HoldMs > 0 {
  213. until := now.Add(time.Duration(admission.HoldMs) * time.Millisecond)
  214. if hold.Active == nil {
  215. hold.Active = map[int64]time.Time{}
  216. }
  217. for id := range displacedHold {
  218. delete(hold.Active, id)
  219. }
  220. for id := range selected {
  221. hold.Active[id] = until
  222. }
  223. }
  224. admitted := make([]ScheduledCandidate, 0, len(selected))
  225. for _, cand := range ranked {
  226. if _, ok := selected[cand.Candidate.ID]; ok {
  227. admitted = append(admitted, cand)
  228. }
  229. }
  230. admission.Admitted = len(admitted)
  231. admission.Skipped = planned - admission.Admitted
  232. if admission.Skipped < 0 {
  233. admission.Skipped = 0
  234. }
  235. if hold != nil {
  236. admission.HoldActive = len(hold.Active)
  237. }
  238. admission.HoldSelected = len(held) - len(displacedHold)
  239. admission.HoldProtected = len(protected)
  240. admission.HoldExpired = len(expired)
  241. admission.HoldDisplaced = len(displacedHold)
  242. admission.Opportunistic = len(opportunistic)
  243. displacedByHold := map[int64]struct{}{}
  244. if len(admitted) > 0 {
  245. admission.PriorityCutoff = admitted[len(admitted)-1].Priority
  246. for _, cand := range ranked {
  247. if _, ok := selected[cand.Candidate.ID]; ok {
  248. continue
  249. }
  250. if cand.Priority >= admission.PriorityCutoff {
  251. if _, ok := displacedHold[cand.Candidate.ID]; ok {
  252. continue
  253. }
  254. displacedByHold[cand.Candidate.ID] = struct{}{}
  255. }
  256. }
  257. }
  258. admission.Displaced = len(displacedByHold) + len(displacedHold)
  259. admission.DisplacedByHold = len(displacedByHold)
  260. admission.PriorityTier = PriorityTierFromRange(admission.PriorityCutoff, plan.PriorityMin, plan.PriorityMax)
  261. admission.Pressure = buildRefinementPressure(budgetModel, admission)
  262. if admission.PriorityCutoff > 0 {
  263. admission.Reason = admissionReason("admission:budget", policy, holdPolicy, pressureReasonTag(admission.Pressure), "budget:"+slugToken(plan.BudgetSource))
  264. }
  265. plan.Selected = admitted
  266. plan.PriorityCutoff = admission.PriorityCutoff
  267. plan.DroppedByBudget = admission.Skipped
  268. for i := range workItems {
  269. item := &workItems[i]
  270. if item.Status != RefinementStatusPlanned {
  271. continue
  272. }
  273. id := item.Candidate.ID
  274. familyRankOut := familyRankForOutput(familyRankByID[id])
  275. if _, ok := selected[id]; ok {
  276. item.Status = RefinementStatusAdmitted
  277. item.Reason = RefinementReasonAdmitted
  278. class := AdmissionClassAdmit
  279. reason := "refinement:admit:budget"
  280. if _, wasHeld := held[id]; wasHeld {
  281. class = AdmissionClassHold
  282. reason = "refinement:admit:hold"
  283. }
  284. if item.Admission == nil {
  285. item.Admission = &PriorityAdmission{Basis: "refinement"}
  286. }
  287. item.Admission.Class = class
  288. item.Admission.Score = item.Priority
  289. item.Admission.Cutoff = admission.PriorityCutoff
  290. item.Admission.Tier = tierByID[id]
  291. item.Admission.TierFloor = familyFloorByID[id]
  292. item.Admission.Family = familyByID[id]
  293. item.Admission.FamilyRank = familyRankOut
  294. extras := []string{pressureReasonTag(admission.Pressure), "budget:" + slugToken(plan.BudgetSource)}
  295. if _, wasHeld := held[id]; wasHeld {
  296. extras = append(extras, "pressure:hold", ReasonTagHoldActive)
  297. if _, ok := protected[id]; ok {
  298. extras = append(extras, ReasonTagHoldProtected)
  299. }
  300. }
  301. if _, ok := opportunistic[id]; ok {
  302. extras = append(extras, "pressure:hold", ReasonTagDisplaceOpportunist, ReasonTagDisplaceTier, ReasonTagHoldDisplaced)
  303. }
  304. item.Admission.Reason = admissionReason(reason, policy, holdPolicy, extras...)
  305. continue
  306. }
  307. if _, ok := displacedHold[id]; ok {
  308. item.Status = RefinementStatusDisplaced
  309. item.Reason = RefinementReasonDisplaced
  310. if item.Admission == nil {
  311. item.Admission = &PriorityAdmission{Basis: "refinement"}
  312. }
  313. item.Admission.Class = AdmissionClassDisplace
  314. item.Admission.Score = item.Priority
  315. item.Admission.Cutoff = admission.PriorityCutoff
  316. item.Admission.Tier = tierByID[id]
  317. item.Admission.TierFloor = familyFloorByID[id]
  318. item.Admission.Family = familyByID[id]
  319. item.Admission.FamilyRank = familyRankOut
  320. item.Admission.Reason = admissionReason("refinement:displace:hold", policy, holdPolicy, pressureReasonTag(admission.Pressure), "pressure:hold", ReasonTagDisplaceOpportunist, ReasonTagDisplaceTier, ReasonTagHoldDisplaced, "budget:"+slugToken(plan.BudgetSource))
  321. continue
  322. }
  323. if _, ok := displacedByHold[id]; ok {
  324. item.Status = RefinementStatusDisplaced
  325. item.Reason = RefinementReasonDisplaced
  326. if item.Admission == nil {
  327. item.Admission = &PriorityAdmission{Basis: "refinement"}
  328. }
  329. item.Admission.Class = AdmissionClassDisplace
  330. item.Admission.Score = item.Priority
  331. item.Admission.Cutoff = admission.PriorityCutoff
  332. item.Admission.Tier = tierByID[id]
  333. item.Admission.TierFloor = familyFloorByID[id]
  334. item.Admission.Family = familyByID[id]
  335. item.Admission.FamilyRank = familyRankOut
  336. item.Admission.Reason = admissionReason("refinement:displace:hold", policy, holdPolicy, pressureReasonTag(admission.Pressure), "pressure:hold", ReasonTagHoldActive, "budget:"+slugToken(plan.BudgetSource))
  337. continue
  338. }
  339. item.Status = RefinementStatusSkipped
  340. item.Reason = RefinementReasonBudget
  341. if item.Admission == nil {
  342. item.Admission = &PriorityAdmission{Basis: "refinement"}
  343. }
  344. item.Admission.Class = AdmissionClassDefer
  345. item.Admission.Score = item.Priority
  346. item.Admission.Cutoff = admission.PriorityCutoff
  347. item.Admission.Tier = tierByID[id]
  348. item.Admission.TierFloor = familyFloorByID[id]
  349. item.Admission.Family = familyByID[id]
  350. item.Admission.FamilyRank = familyRankOut
  351. extras := []string{pressureReasonTag(admission.Pressure), "pressure:budget", "budget:" + slugToken(plan.BudgetSource)}
  352. if _, ok := expired[id]; ok {
  353. extras = append(extras, ReasonTagHoldExpired)
  354. }
  355. item.Admission.Reason = admissionReason("refinement:skip:budget", policy, holdPolicy, extras...)
  356. }
  357. return RefinementAdmissionResult{
  358. Plan: plan,
  359. WorkItems: workItems,
  360. Admitted: admitted,
  361. Admission: admission,
  362. }
  363. }
  364. func rankedContains(items []ScheduledCandidate, id int64) bool {
  365. for _, item := range items {
  366. if item.Candidate.ID == id {
  367. return true
  368. }
  369. }
  370. return false
  371. }
  372. func scaleHold(base int, mult float64) int {
  373. if base <= 0 {
  374. return 0
  375. }
  376. return int(math.Round(float64(base) * mult))
  377. }
  378. func profileContains(profile string, token string) bool {
  379. if profile == "" || token == "" {
  380. return false
  381. }
  382. return strings.Contains(profile, strings.ToLower(token))
  383. }
  384. func strategyContains(strategy string, token string) bool {
  385. if strategy == "" || token == "" {
  386. return false
  387. }
  388. return strings.Contains(strategy, strings.ToLower(token))
  389. }