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.

356 lines
9.0KB

  1. package pipeline
  2. import (
  3. "math"
  4. "strings"
  5. "sdr-wideband-suite/internal/config"
  6. )
  7. const maxMonitorWindowBias = 0.2
  8. const maxMonitorWindowZoneBias = 0.15
  9. func NormalizeMonitorWindows(goals config.PipelineGoalConfig, centerHz float64) []MonitorWindow {
  10. if len(goals.MonitorWindows) > 0 {
  11. windows := make([]MonitorWindow, 0, len(goals.MonitorWindows))
  12. for _, raw := range goals.MonitorWindows {
  13. if win, ok := normalizeGoalWindow(raw, centerHz); ok {
  14. windows = append(windows, win)
  15. }
  16. }
  17. if len(windows) > 0 {
  18. return finalizeMonitorWindows(windows)
  19. }
  20. }
  21. if goals.MonitorStartHz > 0 && goals.MonitorEndHz > goals.MonitorStartHz {
  22. start := goals.MonitorStartHz
  23. end := goals.MonitorEndHz
  24. span := end - start
  25. return finalizeMonitorWindows([]MonitorWindow{{
  26. Label: "primary",
  27. StartHz: start,
  28. EndHz: end,
  29. CenterHz: (start + end) / 2,
  30. SpanHz: span,
  31. Source: "goals:bounds",
  32. }})
  33. }
  34. if goals.MonitorSpanHz > 0 && centerHz != 0 {
  35. half := goals.MonitorSpanHz / 2
  36. start := centerHz - half
  37. end := centerHz + half
  38. return finalizeMonitorWindows([]MonitorWindow{{
  39. Label: "primary",
  40. StartHz: start,
  41. EndHz: end,
  42. CenterHz: centerHz,
  43. SpanHz: goals.MonitorSpanHz,
  44. Source: "goals:span",
  45. }})
  46. }
  47. return nil
  48. }
  49. func finalizeMonitorWindows(windows []MonitorWindow) []MonitorWindow {
  50. if len(windows) == 0 {
  51. return nil
  52. }
  53. maxSpan := 0.0
  54. for _, w := range windows {
  55. if w.SpanHz > maxSpan {
  56. maxSpan = w.SpanHz
  57. }
  58. }
  59. for i := range windows {
  60. windows[i].Index = i
  61. windows[i].Zone = normalizeMonitorZone(windows[i].Zone)
  62. priority := normalizeMonitorPriority(windows[i].Priority)
  63. windows[i].Priority = priority
  64. spanBias := 0.0
  65. if maxSpan > 0 && len(windows) > 1 && windows[i].SpanHz > 0 {
  66. spanBias = maxMonitorWindowBias * (1 - (windows[i].SpanHz / maxSpan))
  67. if spanBias < 0 {
  68. spanBias = 0
  69. }
  70. }
  71. policyBias := priority * maxMonitorWindowBias
  72. totalBias := spanBias + policyBias
  73. if totalBias > maxMonitorWindowBias {
  74. totalBias = maxMonitorWindowBias
  75. } else if totalBias < -maxMonitorWindowBias {
  76. totalBias = -maxMonitorWindowBias
  77. }
  78. windows[i].PriorityBias = totalBias
  79. recordBias, decodeBias := monitorWindowZoneBias(windows[i].Zone)
  80. windows[i].RecordBias = recordBias
  81. windows[i].DecodeBias = decodeBias
  82. }
  83. return windows
  84. }
  85. func MonitorWindowBounds(windows []MonitorWindow) (float64, float64, bool) {
  86. minStart := 0.0
  87. maxEnd := 0.0
  88. ok := false
  89. for _, w := range windows {
  90. if w.StartHz <= 0 || w.EndHz <= 0 || w.EndHz <= w.StartHz {
  91. continue
  92. }
  93. if !ok || w.StartHz < minStart {
  94. minStart = w.StartHz
  95. }
  96. if !ok || w.EndHz > maxEnd {
  97. maxEnd = w.EndHz
  98. }
  99. ok = true
  100. }
  101. return minStart, maxEnd, ok
  102. }
  103. func normalizeGoalWindow(raw config.MonitorWindow, fallbackCenter float64) (MonitorWindow, bool) {
  104. zone := normalizeMonitorZone(raw.Zone)
  105. if raw.StartHz > 0 && raw.EndHz > raw.StartHz {
  106. span := raw.EndHz - raw.StartHz
  107. return MonitorWindow{
  108. Label: raw.Label,
  109. Zone: zone,
  110. StartHz: raw.StartHz,
  111. EndHz: raw.EndHz,
  112. CenterHz: (raw.StartHz + raw.EndHz) / 2,
  113. SpanHz: span,
  114. Source: "goals:window:start_end",
  115. Priority: raw.Priority,
  116. AutoRecord: raw.AutoRecord,
  117. AutoDecode: raw.AutoDecode,
  118. }, true
  119. }
  120. center := raw.CenterHz
  121. if center == 0 {
  122. center = fallbackCenter
  123. }
  124. if center != 0 && raw.SpanHz > 0 {
  125. half := raw.SpanHz / 2
  126. source := "goals:window:center_span"
  127. if raw.CenterHz == 0 {
  128. source = "goals:window:span_default"
  129. }
  130. return MonitorWindow{
  131. Label: raw.Label,
  132. Zone: zone,
  133. StartHz: center - half,
  134. EndHz: center + half,
  135. CenterHz: center,
  136. SpanHz: raw.SpanHz,
  137. Source: source,
  138. Priority: raw.Priority,
  139. AutoRecord: raw.AutoRecord,
  140. AutoDecode: raw.AutoDecode,
  141. }, true
  142. }
  143. return MonitorWindow{}, false
  144. }
  145. func normalizeMonitorPriority(priority float64) float64 {
  146. if math.IsNaN(priority) || math.IsInf(priority, 0) {
  147. return 0
  148. }
  149. if priority > 1 {
  150. return 1
  151. }
  152. if priority < -1 {
  153. return -1
  154. }
  155. return priority
  156. }
  157. func normalizeMonitorZone(raw string) string {
  158. zone := strings.ToLower(strings.TrimSpace(raw))
  159. switch zone {
  160. case "", "neutral", "monitor", "default":
  161. return ""
  162. case "focus", "priority", "hot":
  163. return "focus"
  164. case "record", "recording", "record-only":
  165. return "record"
  166. case "decode", "decoding", "decode-only":
  167. return "decode"
  168. case "background", "bg", "defer":
  169. return "background"
  170. default:
  171. return ""
  172. }
  173. }
  174. func monitorWindowZoneBias(zone string) (float64, float64) {
  175. switch normalizeMonitorZone(zone) {
  176. case "focus":
  177. return maxMonitorWindowZoneBias, maxMonitorWindowZoneBias
  178. case "record":
  179. return maxMonitorWindowZoneBias, 0
  180. case "decode":
  181. return 0, maxMonitorWindowZoneBias
  182. case "background":
  183. return -maxMonitorWindowZoneBias, -maxMonitorWindowZoneBias
  184. default:
  185. return 0, 0
  186. }
  187. }
  188. func monitorBounds(policy Policy) (float64, float64, bool) {
  189. if len(policy.MonitorWindows) > 0 {
  190. return MonitorWindowBounds(policy.MonitorWindows)
  191. }
  192. start := policy.MonitorStartHz
  193. end := policy.MonitorEndHz
  194. if start != 0 && end != 0 && end > start {
  195. return start, end, true
  196. }
  197. if policy.MonitorSpanHz > 0 && policy.MonitorCenterHz != 0 {
  198. half := policy.MonitorSpanHz / 2
  199. return policy.MonitorCenterHz - half, policy.MonitorCenterHz + half, true
  200. }
  201. return 0, 0, false
  202. }
  203. func candidateInMonitor(policy Policy, candidate Candidate) bool {
  204. if len(policy.MonitorWindows) > 0 {
  205. matches := MonitorWindowMatchesForCandidate(policy.MonitorWindows, candidate)
  206. return len(matches) > 0
  207. }
  208. start, end, ok := monitorBounds(policy)
  209. if !ok {
  210. return true
  211. }
  212. left, right := candidateBounds(candidate)
  213. return right >= start && left <= end
  214. }
  215. func candidateBounds(candidate Candidate) (float64, float64) {
  216. left := candidate.CenterHz
  217. right := candidate.CenterHz
  218. if candidate.BandwidthHz > 0 {
  219. left = candidate.CenterHz - candidate.BandwidthHz/2
  220. right = candidate.CenterHz + candidate.BandwidthHz/2
  221. }
  222. return left, right
  223. }
  224. func ApplyMonitorWindowMatches(policy Policy, candidate *Candidate) bool {
  225. if candidate == nil {
  226. return true
  227. }
  228. if len(policy.MonitorWindows) == 0 {
  229. candidate.MonitorMatches = nil
  230. if start, end, ok := monitorBounds(policy); ok {
  231. left, right := candidateBounds(*candidate)
  232. if right < start || left > end {
  233. return false
  234. }
  235. }
  236. return true
  237. }
  238. matches := MonitorWindowMatchesForCandidate(policy.MonitorWindows, *candidate)
  239. if len(matches) == 0 {
  240. candidate.MonitorMatches = nil
  241. return false
  242. }
  243. candidate.MonitorMatches = matches
  244. return true
  245. }
  246. func ApplyMonitorWindowMatchesToCandidates(policy Policy, candidates []Candidate) {
  247. if len(candidates) == 0 || len(policy.MonitorWindows) == 0 {
  248. return
  249. }
  250. for i := range candidates {
  251. _ = ApplyMonitorWindowMatches(policy, &candidates[i])
  252. }
  253. }
  254. func MonitorWindowMatches(policy Policy, candidate Candidate) []MonitorWindowMatch {
  255. return MonitorWindowMatchesForCandidate(policy.MonitorWindows, candidate)
  256. }
  257. func MonitorWindowMatchesForCandidate(windows []MonitorWindow, candidate Candidate) []MonitorWindowMatch {
  258. if len(windows) == 0 {
  259. return nil
  260. }
  261. left, right := candidateBounds(candidate)
  262. pointCandidate := candidate.BandwidthHz <= 0
  263. matches := make([]MonitorWindowMatch, 0, len(windows))
  264. for _, win := range windows {
  265. if win.StartHz <= 0 || win.EndHz <= 0 || win.EndHz <= win.StartHz {
  266. continue
  267. }
  268. if right < win.StartHz || left > win.EndHz {
  269. continue
  270. }
  271. overlap := math.Min(right, win.EndHz) - math.Max(left, win.StartHz)
  272. coverage := 0.0
  273. if win.SpanHz > 0 && overlap > 0 {
  274. coverage = overlap / win.SpanHz
  275. }
  276. if pointCandidate && candidate.CenterHz >= win.StartHz && candidate.CenterHz <= win.EndHz {
  277. coverage = 1
  278. }
  279. if coverage < 0 {
  280. coverage = 0
  281. }
  282. if coverage > 1 {
  283. coverage = 1
  284. }
  285. center := win.CenterHz
  286. if center == 0 {
  287. center = (win.StartHz + win.EndHz) / 2
  288. }
  289. distance := math.Abs(candidate.CenterHz - center)
  290. bias := win.PriorityBias * coverage
  291. recordBias := win.RecordBias * coverage
  292. decodeBias := win.DecodeBias * coverage
  293. matches = append(matches, MonitorWindowMatch{
  294. Index: win.Index,
  295. Label: win.Label,
  296. Zone: win.Zone,
  297. Source: win.Source,
  298. StartHz: win.StartHz,
  299. EndHz: win.EndHz,
  300. CenterHz: center,
  301. SpanHz: win.SpanHz,
  302. OverlapHz: overlap,
  303. Coverage: coverage,
  304. DistanceHz: distance,
  305. Bias: bias,
  306. RecordBias: recordBias,
  307. DecodeBias: decodeBias,
  308. AutoRecord: win.AutoRecord,
  309. AutoDecode: win.AutoDecode,
  310. })
  311. }
  312. if len(matches) == 0 {
  313. return nil
  314. }
  315. return matches
  316. }
  317. func MonitorWindowBias(policy Policy, candidate Candidate) (float64, *MonitorWindowMatch) {
  318. matches := candidate.MonitorMatches
  319. if len(matches) == 0 {
  320. matches = MonitorWindowMatches(policy, candidate)
  321. }
  322. if len(matches) == 0 {
  323. return 0, nil
  324. }
  325. bestIdx := 0
  326. for i := 1; i < len(matches); i++ {
  327. if matches[i].Bias > matches[bestIdx].Bias {
  328. bestIdx = i
  329. continue
  330. }
  331. if matches[i].Bias == matches[bestIdx].Bias && matches[i].Coverage > matches[bestIdx].Coverage {
  332. bestIdx = i
  333. }
  334. }
  335. best := matches[bestIdx]
  336. return best.Bias, &best
  337. }