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.

288 lines
7.7KB

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