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.

278 lines
6.9KB

  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. if maxSpan > 0 && len(windows) > 1 && windows[i].SpanHz > 0 {
  60. bias := maxMonitorWindowBias * (1 - (windows[i].SpanHz / maxSpan))
  61. if bias < 0 {
  62. bias = 0
  63. }
  64. windows[i].PriorityBias = bias
  65. }
  66. }
  67. return windows
  68. }
  69. func MonitorWindowBounds(windows []MonitorWindow) (float64, float64, bool) {
  70. minStart := 0.0
  71. maxEnd := 0.0
  72. ok := false
  73. for _, w := range windows {
  74. if w.StartHz <= 0 || w.EndHz <= 0 || w.EndHz <= w.StartHz {
  75. continue
  76. }
  77. if !ok || w.StartHz < minStart {
  78. minStart = w.StartHz
  79. }
  80. if !ok || w.EndHz > maxEnd {
  81. maxEnd = w.EndHz
  82. }
  83. ok = true
  84. }
  85. return minStart, maxEnd, ok
  86. }
  87. func normalizeGoalWindow(raw config.MonitorWindow, fallbackCenter float64) (MonitorWindow, bool) {
  88. if raw.StartHz > 0 && raw.EndHz > raw.StartHz {
  89. span := raw.EndHz - raw.StartHz
  90. return MonitorWindow{
  91. Label: raw.Label,
  92. StartHz: raw.StartHz,
  93. EndHz: raw.EndHz,
  94. CenterHz: (raw.StartHz + raw.EndHz) / 2,
  95. SpanHz: span,
  96. Source: "goals:window:start_end",
  97. }, true
  98. }
  99. center := raw.CenterHz
  100. if center == 0 {
  101. center = fallbackCenter
  102. }
  103. if center != 0 && raw.SpanHz > 0 {
  104. half := raw.SpanHz / 2
  105. source := "goals:window:center_span"
  106. if raw.CenterHz == 0 {
  107. source = "goals:window:span_default"
  108. }
  109. return MonitorWindow{
  110. Label: raw.Label,
  111. StartHz: center - half,
  112. EndHz: center + half,
  113. CenterHz: center,
  114. SpanHz: raw.SpanHz,
  115. Source: source,
  116. }, true
  117. }
  118. return MonitorWindow{}, false
  119. }
  120. func monitorBounds(policy Policy) (float64, float64, bool) {
  121. if len(policy.MonitorWindows) > 0 {
  122. return MonitorWindowBounds(policy.MonitorWindows)
  123. }
  124. start := policy.MonitorStartHz
  125. end := policy.MonitorEndHz
  126. if start != 0 && end != 0 && end > start {
  127. return start, end, true
  128. }
  129. if policy.MonitorSpanHz > 0 && policy.MonitorCenterHz != 0 {
  130. half := policy.MonitorSpanHz / 2
  131. return policy.MonitorCenterHz - half, policy.MonitorCenterHz + half, true
  132. }
  133. return 0, 0, false
  134. }
  135. func candidateInMonitor(policy Policy, candidate Candidate) bool {
  136. if len(policy.MonitorWindows) > 0 {
  137. matches := MonitorWindowMatchesForCandidate(policy.MonitorWindows, candidate)
  138. return len(matches) > 0
  139. }
  140. start, end, ok := monitorBounds(policy)
  141. if !ok {
  142. return true
  143. }
  144. left, right := candidateBounds(candidate)
  145. return right >= start && left <= end
  146. }
  147. func candidateBounds(candidate Candidate) (float64, float64) {
  148. left := candidate.CenterHz
  149. right := candidate.CenterHz
  150. if candidate.BandwidthHz > 0 {
  151. left = candidate.CenterHz - candidate.BandwidthHz/2
  152. right = candidate.CenterHz + candidate.BandwidthHz/2
  153. }
  154. return left, right
  155. }
  156. func ApplyMonitorWindowMatches(policy Policy, candidate *Candidate) bool {
  157. if candidate == nil {
  158. return true
  159. }
  160. if len(policy.MonitorWindows) == 0 {
  161. candidate.MonitorMatches = nil
  162. if start, end, ok := monitorBounds(policy); ok {
  163. left, right := candidateBounds(*candidate)
  164. if right < start || left > end {
  165. return false
  166. }
  167. }
  168. return true
  169. }
  170. matches := MonitorWindowMatchesForCandidate(policy.MonitorWindows, *candidate)
  171. if len(matches) == 0 {
  172. candidate.MonitorMatches = nil
  173. return false
  174. }
  175. candidate.MonitorMatches = matches
  176. return true
  177. }
  178. func ApplyMonitorWindowMatchesToCandidates(policy Policy, candidates []Candidate) {
  179. if len(candidates) == 0 || len(policy.MonitorWindows) == 0 {
  180. return
  181. }
  182. for i := range candidates {
  183. _ = ApplyMonitorWindowMatches(policy, &candidates[i])
  184. }
  185. }
  186. func MonitorWindowMatches(policy Policy, candidate Candidate) []MonitorWindowMatch {
  187. return MonitorWindowMatchesForCandidate(policy.MonitorWindows, candidate)
  188. }
  189. func MonitorWindowMatchesForCandidate(windows []MonitorWindow, candidate Candidate) []MonitorWindowMatch {
  190. if len(windows) == 0 {
  191. return nil
  192. }
  193. left, right := candidateBounds(candidate)
  194. pointCandidate := candidate.BandwidthHz <= 0
  195. matches := make([]MonitorWindowMatch, 0, len(windows))
  196. for _, win := range windows {
  197. if win.StartHz <= 0 || win.EndHz <= 0 || win.EndHz <= win.StartHz {
  198. continue
  199. }
  200. if right < win.StartHz || left > win.EndHz {
  201. continue
  202. }
  203. overlap := math.Min(right, win.EndHz) - math.Max(left, win.StartHz)
  204. coverage := 0.0
  205. if win.SpanHz > 0 && overlap > 0 {
  206. coverage = overlap / win.SpanHz
  207. }
  208. if pointCandidate && candidate.CenterHz >= win.StartHz && candidate.CenterHz <= win.EndHz {
  209. coverage = 1
  210. }
  211. if coverage < 0 {
  212. coverage = 0
  213. }
  214. if coverage > 1 {
  215. coverage = 1
  216. }
  217. center := win.CenterHz
  218. if center == 0 {
  219. center = (win.StartHz + win.EndHz) / 2
  220. }
  221. distance := math.Abs(candidate.CenterHz - center)
  222. bias := win.PriorityBias * coverage
  223. matches = append(matches, MonitorWindowMatch{
  224. Index: win.Index,
  225. Label: win.Label,
  226. Source: win.Source,
  227. StartHz: win.StartHz,
  228. EndHz: win.EndHz,
  229. CenterHz: center,
  230. SpanHz: win.SpanHz,
  231. OverlapHz: overlap,
  232. Coverage: coverage,
  233. DistanceHz: distance,
  234. Bias: bias,
  235. })
  236. }
  237. if len(matches) == 0 {
  238. return nil
  239. }
  240. return matches
  241. }
  242. func MonitorWindowBias(policy Policy, candidate Candidate) (float64, *MonitorWindowMatch) {
  243. matches := candidate.MonitorMatches
  244. if len(matches) == 0 {
  245. matches = MonitorWindowMatches(policy, candidate)
  246. }
  247. if len(matches) == 0 {
  248. return 0, nil
  249. }
  250. bestIdx := 0
  251. for i := 1; i < len(matches); i++ {
  252. if matches[i].Bias > matches[bestIdx].Bias {
  253. bestIdx = i
  254. continue
  255. }
  256. if matches[i].Bias == matches[bestIdx].Bias && matches[i].Coverage > matches[bestIdx].Coverage {
  257. bestIdx = i
  258. }
  259. }
  260. best := matches[bestIdx]
  261. return best.Bias, &best
  262. }