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.

303 lines
7.5KB

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