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
7.3KB

  1. package main
  2. import (
  3. "sort"
  4. "strings"
  5. "sdr-wideband-suite/internal/pipeline"
  6. )
  7. type SurveillanceLevelSummary struct {
  8. Name string `json:"name"`
  9. Role string `json:"role,omitempty"`
  10. Truth string `json:"truth,omitempty"`
  11. Kind string `json:"kind,omitempty"`
  12. SampleRate int `json:"sample_rate,omitempty"`
  13. FFTSize int `json:"fft_size,omitempty"`
  14. BinHz float64 `json:"bin_hz,omitempty"`
  15. Decimation int `json:"decimation,omitempty"`
  16. SpanHz float64 `json:"span_hz,omitempty"`
  17. CenterHz float64 `json:"center_hz,omitempty"`
  18. Source string `json:"source,omitempty"`
  19. SpectrumBins int `json:"spectrum_bins,omitempty"`
  20. }
  21. type CandidateEvidenceSummary struct {
  22. Level string `json:"level"`
  23. Role string `json:"role,omitempty"`
  24. Kind string `json:"kind,omitempty"`
  25. Provenance string `json:"provenance,omitempty"`
  26. Count int `json:"count"`
  27. }
  28. type CandidateEvidenceStateSummary struct {
  29. Total int `json:"total"`
  30. WithEvidence int `json:"with_evidence"`
  31. Fused int `json:"fused"`
  32. MultiLevelConfirmed int `json:"multi_level_confirmed"`
  33. DerivedOnly int `json:"derived_only"`
  34. SupportOnly int `json:"support_only"`
  35. PrimaryPresent int `json:"primary_present"`
  36. DerivedPresent int `json:"derived_present"`
  37. SupportPresent int `json:"support_present"`
  38. PrimaryOnly int `json:"primary_only"`
  39. }
  40. type CandidateWindowSummary struct {
  41. Index int `json:"index"`
  42. Label string `json:"label,omitempty"`
  43. Source string `json:"source,omitempty"`
  44. StartHz float64 `json:"start_hz,omitempty"`
  45. EndHz float64 `json:"end_hz,omitempty"`
  46. CenterHz float64 `json:"center_hz,omitempty"`
  47. SpanHz float64 `json:"span_hz,omitempty"`
  48. Priority float64 `json:"priority,omitempty"`
  49. PriorityBias float64 `json:"priority_bias,omitempty"`
  50. Candidates int `json:"candidates"`
  51. }
  52. func buildSurveillanceLevelSummaries(set pipeline.SurveillanceLevelSet, spectra []pipeline.SurveillanceLevelSpectrum) map[string]SurveillanceLevelSummary {
  53. if set.Primary.Name == "" && len(set.Derived) == 0 && len(set.Support) == 0 && set.Presentation.Name == "" && len(set.All) == 0 {
  54. return nil
  55. }
  56. bins := map[string]int{}
  57. for _, spec := range spectra {
  58. if spec.Level.Name == "" || len(spec.Spectrum) == 0 {
  59. continue
  60. }
  61. bins[spec.Level.Name] = len(spec.Spectrum)
  62. }
  63. levels := set.All
  64. if len(levels) == 0 {
  65. if set.Primary.Name != "" {
  66. levels = append(levels, set.Primary)
  67. }
  68. if len(set.Derived) > 0 {
  69. levels = append(levels, set.Derived...)
  70. }
  71. if len(set.Support) > 0 {
  72. levels = append(levels, set.Support...)
  73. }
  74. if set.Presentation.Name != "" {
  75. levels = append(levels, set.Presentation)
  76. }
  77. }
  78. out := make(map[string]SurveillanceLevelSummary, len(levels))
  79. for _, level := range levels {
  80. name := level.Name
  81. if name == "" {
  82. continue
  83. }
  84. binHz := level.BinHz
  85. if binHz == 0 && level.SampleRate > 0 && level.FFTSize > 0 {
  86. binHz = float64(level.SampleRate) / float64(level.FFTSize)
  87. }
  88. kind := evidenceKind(level)
  89. out[name] = SurveillanceLevelSummary{
  90. Name: name,
  91. Role: level.Role,
  92. Truth: level.Truth,
  93. Kind: kind,
  94. SampleRate: level.SampleRate,
  95. FFTSize: level.FFTSize,
  96. BinHz: binHz,
  97. Decimation: level.Decimation,
  98. SpanHz: level.SpanHz,
  99. CenterHz: level.CenterHz,
  100. Source: level.Source,
  101. SpectrumBins: bins[name],
  102. }
  103. }
  104. if len(out) == 0 {
  105. return nil
  106. }
  107. return out
  108. }
  109. func buildCandidateSourceSummary(candidates []pipeline.Candidate) map[string]int {
  110. if len(candidates) == 0 {
  111. return nil
  112. }
  113. out := map[string]int{}
  114. for _, cand := range candidates {
  115. if cand.Source == "" {
  116. continue
  117. }
  118. out[cand.Source]++
  119. }
  120. if len(out) == 0 {
  121. return nil
  122. }
  123. return out
  124. }
  125. func buildCandidateEvidenceSummary(candidates []pipeline.Candidate) []CandidateEvidenceSummary {
  126. if len(candidates) == 0 {
  127. return nil
  128. }
  129. type key struct {
  130. level string
  131. role string
  132. kind string
  133. provenance string
  134. }
  135. counts := map[key]int{}
  136. for _, cand := range candidates {
  137. for _, ev := range cand.Evidence {
  138. name := ev.Level.Name
  139. if name == "" {
  140. name = "unknown"
  141. }
  142. role := strings.TrimSpace(ev.Level.Role)
  143. kind := evidenceKind(ev.Level)
  144. k := key{level: name, role: role, kind: kind, provenance: ev.Provenance}
  145. counts[k]++
  146. }
  147. }
  148. if len(counts) == 0 {
  149. return nil
  150. }
  151. out := make([]CandidateEvidenceSummary, 0, len(counts))
  152. for k, v := range counts {
  153. out = append(out, CandidateEvidenceSummary{Level: k.level, Role: k.role, Kind: k.kind, Provenance: k.provenance, Count: v})
  154. }
  155. sort.Slice(out, func(i, j int) bool {
  156. if out[i].Count == out[j].Count {
  157. if out[i].Level == out[j].Level {
  158. if out[i].Kind == out[j].Kind {
  159. return out[i].Provenance < out[j].Provenance
  160. }
  161. return out[i].Kind < out[j].Kind
  162. }
  163. return out[i].Level < out[j].Level
  164. }
  165. return out[i].Count > out[j].Count
  166. })
  167. return out
  168. }
  169. func buildCandidateEvidenceStateSummary(candidates []pipeline.Candidate) *CandidateEvidenceStateSummary {
  170. if len(candidates) == 0 {
  171. return nil
  172. }
  173. summary := CandidateEvidenceStateSummary{Total: len(candidates)}
  174. for _, cand := range candidates {
  175. state := pipeline.CandidateEvidenceStateFor(cand)
  176. if state.TotalLevelEntries == 0 {
  177. continue
  178. }
  179. summary.WithEvidence++
  180. if state.Fused {
  181. summary.Fused++
  182. }
  183. if state.MultiLevelConfirmed {
  184. summary.MultiLevelConfirmed++
  185. }
  186. if state.DerivedOnly {
  187. summary.DerivedOnly++
  188. }
  189. if state.SupportOnly {
  190. summary.SupportOnly++
  191. }
  192. if state.PrimaryLevelCount > 0 {
  193. summary.PrimaryPresent++
  194. }
  195. if state.DerivedLevelCount > 0 {
  196. summary.DerivedPresent++
  197. }
  198. if state.SupportLevelCount > 0 {
  199. summary.SupportPresent++
  200. }
  201. if state.PrimaryLevelCount > 0 && state.DerivedLevelCount == 0 {
  202. summary.PrimaryOnly++
  203. }
  204. }
  205. if summary.WithEvidence == 0 {
  206. return nil
  207. }
  208. return &summary
  209. }
  210. func buildCandidateWindowSummary(candidates []pipeline.Candidate, windows []pipeline.MonitorWindow) []CandidateWindowSummary {
  211. if len(windows) == 0 {
  212. return nil
  213. }
  214. out := make([]CandidateWindowSummary, 0, len(windows))
  215. index := make(map[int]int, len(windows))
  216. for _, win := range windows {
  217. entry := CandidateWindowSummary{
  218. Index: win.Index,
  219. Label: win.Label,
  220. Source: win.Source,
  221. StartHz: win.StartHz,
  222. EndHz: win.EndHz,
  223. CenterHz: win.CenterHz,
  224. SpanHz: win.SpanHz,
  225. Priority: win.Priority,
  226. PriorityBias: win.PriorityBias,
  227. }
  228. index[win.Index] = len(out)
  229. out = append(out, entry)
  230. }
  231. totalCandidates := 0
  232. for _, cand := range candidates {
  233. matches := cand.MonitorMatches
  234. if len(matches) == 0 {
  235. matches = pipeline.MonitorWindowMatchesForCandidate(windows, cand)
  236. }
  237. for _, match := range matches {
  238. idx, ok := index[match.Index]
  239. if !ok {
  240. continue
  241. }
  242. out[idx].Candidates++
  243. totalCandidates++
  244. }
  245. }
  246. if totalCandidates == 0 {
  247. return nil
  248. }
  249. sort.Slice(out, func(i, j int) bool {
  250. return out[i].Index < out[j].Index
  251. })
  252. return out
  253. }
  254. func evidenceKind(level pipeline.AnalysisLevel) string {
  255. if pipeline.IsPresentationLevel(level) {
  256. return "presentation"
  257. }
  258. if pipeline.IsSupportLevel(level) {
  259. return "support"
  260. }
  261. if pipeline.IsDetectionLevel(level) {
  262. return "detection"
  263. }
  264. return "unknown"
  265. }