Wideband autonomous SDR analysis engine forked from sdr-visual-suite
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

363 lines
11KB

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