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.

296 line
8.8KB

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