Wideband autonomous SDR analysis engine forked from sdr-visual-suite
Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

1118 řádky
35KB

  1. package main
  2. import (
  3. "fmt"
  4. "math"
  5. "os"
  6. "strconv"
  7. "strings"
  8. "sync"
  9. "sync/atomic"
  10. "time"
  11. "sdr-wideband-suite/internal/classifier"
  12. "sdr-wideband-suite/internal/config"
  13. "sdr-wideband-suite/internal/demod"
  14. "sdr-wideband-suite/internal/detector"
  15. "sdr-wideband-suite/internal/dsp"
  16. "sdr-wideband-suite/internal/logging"
  17. fftutil "sdr-wideband-suite/internal/fft"
  18. "sdr-wideband-suite/internal/fft/gpufft"
  19. "sdr-wideband-suite/internal/pipeline"
  20. "sdr-wideband-suite/internal/rds"
  21. "sdr-wideband-suite/internal/recorder"
  22. "sdr-wideband-suite/internal/telemetry"
  23. )
  24. type rdsState struct {
  25. dec rds.Decoder
  26. result rds.Result
  27. lastDecode time.Time
  28. busy int32
  29. mu sync.Mutex
  30. }
  31. var forceFixedStreamReadSamples = func() int {
  32. raw := strings.TrimSpace(os.Getenv("SDR_FORCE_FIXED_STREAM_READ_SAMPLES"))
  33. if raw == "" {
  34. return 0
  35. }
  36. v, err := strconv.Atoi(raw)
  37. if err != nil || v <= 0 {
  38. return 0
  39. }
  40. return v
  41. }()
  42. type dspRuntime struct {
  43. cfg config.Config
  44. det *detector.Detector
  45. derivedDetectors map[string]*derivedDetector
  46. nextDerivedBase int64
  47. window []float64
  48. plan *fftutil.CmplxPlan
  49. detailWindow []float64
  50. detailPlan *fftutil.CmplxPlan
  51. detailFFT int
  52. survWindows map[int][]float64
  53. survPlans map[int]*fftutil.CmplxPlan
  54. survFIR map[int][]float64
  55. dcEnabled bool
  56. iqEnabled bool
  57. useGPU bool
  58. gpuEngine *gpufft.Engine
  59. rdsMap map[int64]*rdsState
  60. streamPhaseState map[int64]*streamExtractState
  61. streamOverlap *streamIQOverlap
  62. arbiter *pipeline.Arbiter
  63. arbitration pipeline.ArbitrationState
  64. gotSamples bool
  65. telemetry *telemetry.Collector
  66. }
  67. type spectrumArtifacts struct {
  68. allIQ []complex64
  69. streamDropped bool
  70. surveillanceIQ []complex64
  71. detailIQ []complex64
  72. surveillanceSpectrum []float64
  73. surveillanceSpectra []pipeline.SurveillanceLevelSpectrum
  74. surveillancePlan surveillancePlan
  75. detailSpectrum []float64
  76. finished []detector.Event
  77. detected []detector.Signal
  78. thresholds []float64
  79. noiseFloor float64
  80. now time.Time
  81. }
  82. type derivedDetector struct {
  83. det *detector.Detector
  84. sampleRate int
  85. fftSize int
  86. idBase int64
  87. }
  88. type surveillanceLevelSpec struct {
  89. Level pipeline.AnalysisLevel
  90. Decim int
  91. AllowGPU bool
  92. }
  93. type surveillancePlan struct {
  94. Primary pipeline.AnalysisLevel
  95. Levels []pipeline.AnalysisLevel
  96. LevelSet pipeline.SurveillanceLevelSet
  97. Presentation pipeline.AnalysisLevel
  98. Context pipeline.AnalysisContext
  99. DetectionPolicy pipeline.SurveillanceDetectionPolicy
  100. Specs []surveillanceLevelSpec
  101. }
  102. const derivedIDBlock = int64(1_000_000_000)
  103. func newDSPRuntime(cfg config.Config, det *detector.Detector, window []float64, gpuState *gpuStatus, coll *telemetry.Collector) *dspRuntime {
  104. detailFFT := cfg.Refinement.DetailFFTSize
  105. if detailFFT <= 0 {
  106. detailFFT = cfg.FFTSize
  107. }
  108. rt := &dspRuntime{
  109. cfg: cfg,
  110. det: det,
  111. derivedDetectors: map[string]*derivedDetector{},
  112. nextDerivedBase: -derivedIDBlock,
  113. window: window,
  114. plan: fftutil.NewCmplxPlan(cfg.FFTSize),
  115. detailWindow: fftutil.Hann(detailFFT),
  116. detailPlan: fftutil.NewCmplxPlan(detailFFT),
  117. detailFFT: detailFFT,
  118. survWindows: map[int][]float64{},
  119. survPlans: map[int]*fftutil.CmplxPlan{},
  120. survFIR: map[int][]float64{},
  121. dcEnabled: cfg.DCBlock,
  122. iqEnabled: cfg.IQBalance,
  123. useGPU: cfg.UseGPUFFT,
  124. rdsMap: map[int64]*rdsState{},
  125. streamPhaseState: map[int64]*streamExtractState{},
  126. streamOverlap: &streamIQOverlap{},
  127. arbiter: pipeline.NewArbiter(),
  128. telemetry: coll,
  129. }
  130. if rt.useGPU && gpuState != nil {
  131. snap := gpuState.snapshot()
  132. if snap.Available {
  133. if eng, err := gpufft.New(cfg.FFTSize); err == nil {
  134. rt.gpuEngine = eng
  135. gpuState.set(true, nil)
  136. } else {
  137. gpuState.set(false, err)
  138. rt.useGPU = false
  139. }
  140. }
  141. }
  142. return rt
  143. }
  144. func (rt *dspRuntime) applyUpdate(upd dspUpdate, srcMgr *sourceManager, rec *recorder.Manager, gpuState *gpuStatus) {
  145. prevFFT := rt.cfg.FFTSize
  146. prevSampleRate := rt.cfg.SampleRate
  147. prevUseGPU := rt.useGPU
  148. prevDetailFFT := rt.detailFFT
  149. rt.cfg = upd.cfg
  150. if rec != nil {
  151. rec.Update(rt.cfg.SampleRate, rt.cfg.FFTSize, recorder.Policy{
  152. Enabled: rt.cfg.Recorder.Enabled,
  153. MinSNRDb: rt.cfg.Recorder.MinSNRDb,
  154. MinDuration: mustParseDuration(rt.cfg.Recorder.MinDuration, 1*time.Second),
  155. MaxDuration: mustParseDuration(rt.cfg.Recorder.MaxDuration, 300*time.Second),
  156. PrerollMs: rt.cfg.Recorder.PrerollMs,
  157. RecordIQ: rt.cfg.Recorder.RecordIQ,
  158. RecordAudio: rt.cfg.Recorder.RecordAudio,
  159. AutoDemod: rt.cfg.Recorder.AutoDemod,
  160. AutoDecode: rt.cfg.Recorder.AutoDecode,
  161. MaxDiskMB: rt.cfg.Recorder.MaxDiskMB,
  162. OutputDir: rt.cfg.Recorder.OutputDir,
  163. ClassFilter: rt.cfg.Recorder.ClassFilter,
  164. RingSeconds: rt.cfg.Recorder.RingSeconds,
  165. DeemphasisUs: rt.cfg.Recorder.DeemphasisUs,
  166. ExtractionTaps: rt.cfg.Recorder.ExtractionTaps,
  167. ExtractionBwMult: rt.cfg.Recorder.ExtractionBwMult,
  168. }, rt.cfg.CenterHz, buildDecoderMap(rt.cfg))
  169. }
  170. if upd.det != nil {
  171. rt.det = upd.det
  172. }
  173. if upd.window != nil {
  174. rt.window = upd.window
  175. rt.plan = fftutil.NewCmplxPlan(rt.cfg.FFTSize)
  176. }
  177. detailFFT := rt.cfg.Refinement.DetailFFTSize
  178. if detailFFT <= 0 {
  179. detailFFT = rt.cfg.FFTSize
  180. }
  181. if detailFFT != prevDetailFFT {
  182. rt.detailFFT = detailFFT
  183. rt.detailWindow = fftutil.Hann(detailFFT)
  184. rt.detailPlan = fftutil.NewCmplxPlan(detailFFT)
  185. }
  186. if prevSampleRate != rt.cfg.SampleRate {
  187. rt.survFIR = map[int][]float64{}
  188. }
  189. if prevFFT != rt.cfg.FFTSize {
  190. rt.survWindows = map[int][]float64{}
  191. rt.survPlans = map[int]*fftutil.CmplxPlan{}
  192. }
  193. if upd.det != nil || prevSampleRate != rt.cfg.SampleRate || prevFFT != rt.cfg.FFTSize {
  194. rt.derivedDetectors = map[string]*derivedDetector{}
  195. rt.nextDerivedBase = -derivedIDBlock
  196. }
  197. rt.dcEnabled = upd.dcBlock
  198. rt.iqEnabled = upd.iqBalance
  199. if rt.cfg.FFTSize != prevFFT || rt.cfg.UseGPUFFT != prevUseGPU {
  200. srcMgr.Flush()
  201. rt.gotSamples = false
  202. if rt.gpuEngine != nil {
  203. rt.gpuEngine.Close()
  204. rt.gpuEngine = nil
  205. }
  206. rt.useGPU = rt.cfg.UseGPUFFT
  207. if rt.useGPU && gpuState != nil {
  208. snap := gpuState.snapshot()
  209. if snap.Available {
  210. if eng, err := gpufft.New(rt.cfg.FFTSize); err == nil {
  211. rt.gpuEngine = eng
  212. gpuState.set(true, nil)
  213. } else {
  214. gpuState.set(false, err)
  215. rt.useGPU = false
  216. }
  217. } else {
  218. gpuState.set(false, nil)
  219. rt.useGPU = false
  220. }
  221. } else if gpuState != nil {
  222. gpuState.set(false, nil)
  223. }
  224. }
  225. if rt.telemetry != nil {
  226. rt.telemetry.Event("dsp_config_update", "info", "dsp runtime configuration updated", nil, map[string]any{
  227. "fft_size": rt.cfg.FFTSize,
  228. "sample_rate": rt.cfg.SampleRate,
  229. "use_gpu_fft": rt.cfg.UseGPUFFT,
  230. "detail_fft": rt.detailFFT,
  231. "surv_strategy": rt.cfg.Surveillance.Strategy,
  232. })
  233. }
  234. }
  235. func (rt *dspRuntime) spectrumFromIQ(iq []complex64, gpuState *gpuStatus) []float64 {
  236. return rt.spectrumFromIQWithPlan(iq, rt.window, rt.plan, gpuState, true)
  237. }
  238. func (rt *dspRuntime) spectrumFromIQWithPlan(iq []complex64, window []float64, plan *fftutil.CmplxPlan, gpuState *gpuStatus, allowGPU bool) []float64 {
  239. if len(iq) == 0 {
  240. return nil
  241. }
  242. if allowGPU && rt.useGPU && rt.gpuEngine != nil {
  243. gpuBuf := make([]complex64, len(iq))
  244. if len(window) == len(iq) {
  245. for i := 0; i < len(iq); i++ {
  246. v := iq[i]
  247. w := float32(window[i])
  248. gpuBuf[i] = complex(real(v)*w, imag(v)*w)
  249. }
  250. } else {
  251. copy(gpuBuf, iq)
  252. }
  253. out, err := rt.gpuEngine.Exec(gpuBuf)
  254. if err != nil {
  255. if gpuState != nil {
  256. gpuState.set(false, err)
  257. }
  258. rt.useGPU = false
  259. return fftutil.SpectrumWithPlan(gpuBuf, nil, plan)
  260. }
  261. return fftutil.SpectrumFromFFT(out)
  262. }
  263. return fftutil.SpectrumWithPlan(iq, window, plan)
  264. }
  265. func (rt *dspRuntime) windowForFFT(fftSize int) []float64 {
  266. if fftSize <= 0 {
  267. return nil
  268. }
  269. if fftSize == rt.cfg.FFTSize {
  270. return rt.window
  271. }
  272. if rt.survWindows == nil {
  273. rt.survWindows = map[int][]float64{}
  274. }
  275. if window, ok := rt.survWindows[fftSize]; ok {
  276. return window
  277. }
  278. window := fftutil.Hann(fftSize)
  279. rt.survWindows[fftSize] = window
  280. return window
  281. }
  282. func (rt *dspRuntime) planForFFT(fftSize int) *fftutil.CmplxPlan {
  283. if fftSize <= 0 {
  284. return nil
  285. }
  286. if fftSize == rt.cfg.FFTSize {
  287. return rt.plan
  288. }
  289. if rt.survPlans == nil {
  290. rt.survPlans = map[int]*fftutil.CmplxPlan{}
  291. }
  292. if plan, ok := rt.survPlans[fftSize]; ok {
  293. return plan
  294. }
  295. plan := fftutil.NewCmplxPlan(fftSize)
  296. rt.survPlans[fftSize] = plan
  297. return plan
  298. }
  299. func (rt *dspRuntime) spectrumForLevel(iq []complex64, fftSize int, gpuState *gpuStatus, allowGPU bool) []float64 {
  300. if len(iq) == 0 || fftSize <= 0 {
  301. return nil
  302. }
  303. if len(iq) > fftSize {
  304. iq = iq[len(iq)-fftSize:]
  305. }
  306. window := rt.windowForFFT(fftSize)
  307. plan := rt.planForFFT(fftSize)
  308. return rt.spectrumFromIQWithPlan(iq, window, plan, gpuState, allowGPU)
  309. }
  310. func sanitizeSpectrum(spectrum []float64) {
  311. for i := range spectrum {
  312. if math.IsNaN(spectrum[i]) || math.IsInf(spectrum[i], 0) {
  313. spectrum[i] = -200
  314. }
  315. }
  316. }
  317. func (rt *dspRuntime) decimationTaps(factor int) []float64 {
  318. if factor <= 1 {
  319. return nil
  320. }
  321. if rt.survFIR == nil {
  322. rt.survFIR = map[int][]float64{}
  323. }
  324. if taps, ok := rt.survFIR[factor]; ok {
  325. return taps
  326. }
  327. cutoff := float64(rt.cfg.SampleRate/factor) * 0.5 * 0.8
  328. taps := dsp.LowpassFIR(cutoff, rt.cfg.SampleRate, 101)
  329. rt.survFIR[factor] = taps
  330. return taps
  331. }
  332. func (rt *dspRuntime) decimateSurveillanceIQ(iq []complex64, factor int) []complex64 {
  333. if factor <= 1 {
  334. return iq
  335. }
  336. taps := rt.decimationTaps(factor)
  337. if len(taps) == 0 {
  338. return dsp.Decimate(iq, factor)
  339. }
  340. filtered := dsp.ApplyFIR(iq, taps)
  341. return dsp.Decimate(filtered, factor)
  342. }
  343. func (rt *dspRuntime) captureSpectrum(srcMgr *sourceManager, rec *recorder.Manager, dcBlocker *dsp.DCBlocker, gpuState *gpuStatus) (*spectrumArtifacts, error) {
  344. start := time.Now()
  345. required := rt.cfg.FFTSize
  346. if rt.detailFFT > required {
  347. required = rt.detailFFT
  348. }
  349. available := required
  350. st := srcMgr.Stats()
  351. if rt.telemetry != nil {
  352. rt.telemetry.SetGauge("source.buffer_samples", float64(st.BufferSamples), nil)
  353. rt.telemetry.SetGauge("source.last_sample_ago_ms", float64(st.LastSampleAgoMs), nil)
  354. rt.telemetry.SetGauge("source.dropped", float64(st.Dropped), nil)
  355. rt.telemetry.SetGauge("source.resets", float64(st.Resets), nil)
  356. }
  357. if forceFixedStreamReadSamples > 0 {
  358. available = forceFixedStreamReadSamples
  359. if available < required {
  360. available = required
  361. }
  362. available = (available / required) * required
  363. if available < required {
  364. available = required
  365. }
  366. logging.Warn("boundary", "fixed_stream_read_samples", "configured", forceFixedStreamReadSamples, "effective", available, "required", required)
  367. } else if st.BufferSamples > required {
  368. available = (st.BufferSamples / required) * required
  369. if available < required {
  370. available = required
  371. }
  372. }
  373. logging.Debug("capture", "read_iq", "required", required, "available", available, "buf", st.BufferSamples, "reset", st.Resets, "drop", st.Dropped)
  374. readStart := time.Now()
  375. allIQ, err := srcMgr.ReadIQ(available)
  376. if err != nil {
  377. if rt.telemetry != nil {
  378. rt.telemetry.IncCounter("capture.read.error", 1, nil)
  379. }
  380. return nil, err
  381. }
  382. if rt.telemetry != nil {
  383. rt.telemetry.Observe("capture.read.duration_ms", float64(time.Since(readStart).Microseconds())/1000.0, nil)
  384. rt.telemetry.Observe("capture.read.samples", float64(len(allIQ)), nil)
  385. }
  386. if rec != nil {
  387. ingestStart := time.Now()
  388. rec.Ingest(time.Now(), allIQ)
  389. if rt.telemetry != nil {
  390. rt.telemetry.Observe("capture.ingest.duration_ms", float64(time.Since(ingestStart).Microseconds())/1000.0, nil)
  391. }
  392. }
  393. // Cap allIQ for downstream extraction to prevent buffer bloat.
  394. // Without this cap, buffer accumulation during processing stalls causes
  395. // increasingly large reads → longer extraction → more accumulation
  396. // (positive feedback loop). For audio streaming this creates >150ms
  397. // feed gaps that produce audible clicks.
  398. // The ring buffer (Ingest above) gets the full data; only extraction is capped.
  399. maxStreamSamples := rt.cfg.SampleRate / rt.cfg.FrameRate * 2
  400. if maxStreamSamples < required {
  401. maxStreamSamples = required
  402. }
  403. maxStreamSamples = (maxStreamSamples / required) * required
  404. streamDropped := false
  405. if len(allIQ) > maxStreamSamples {
  406. allIQ = allIQ[len(allIQ)-maxStreamSamples:]
  407. streamDropped = true
  408. if rt.telemetry != nil {
  409. rt.telemetry.IncCounter("capture.stream_drop.count", 1, nil)
  410. rt.telemetry.Event("iq_dropped", "warn", "capture IQ dropped before extraction", nil, map[string]any{
  411. "max_stream_samples": maxStreamSamples,
  412. "required": required,
  413. })
  414. }
  415. }
  416. logging.Debug("capture", "iq_len", "len", len(allIQ), "surv_fft", rt.cfg.FFTSize, "detail_fft", rt.detailFFT)
  417. survIQ := allIQ
  418. if len(allIQ) > rt.cfg.FFTSize {
  419. survIQ = allIQ[len(allIQ)-rt.cfg.FFTSize:]
  420. }
  421. detailIQ := survIQ
  422. if rt.detailFFT > 0 && len(allIQ) >= rt.detailFFT {
  423. detailIQ = allIQ[len(allIQ)-rt.detailFFT:]
  424. }
  425. if rt.dcEnabled {
  426. dcBlocker.Apply(allIQ)
  427. if rt.telemetry != nil {
  428. rt.telemetry.IncCounter("dsp.dc_block.apply", 1, nil)
  429. }
  430. }
  431. if rt.iqEnabled {
  432. dsp.IQBalance(survIQ)
  433. if !sameIQBuffer(detailIQ, survIQ) {
  434. detailIQ = append([]complex64(nil), detailIQ...)
  435. dsp.IQBalance(detailIQ)
  436. }
  437. }
  438. if rt.telemetry != nil {
  439. rt.telemetry.SetGauge("iq.stage.all.length", float64(len(allIQ)), nil)
  440. rt.telemetry.SetGauge("iq.stage.surveillance.length", float64(len(survIQ)), nil)
  441. rt.telemetry.SetGauge("iq.stage.detail.length", float64(len(detailIQ)), nil)
  442. rt.telemetry.Observe("capture.total.duration_ms", float64(time.Since(start).Microseconds())/1000.0, nil)
  443. if rt.telemetry.ShouldSampleHeavy() {
  444. observeIQStats(rt.telemetry, "capture_all", allIQ, nil)
  445. observeIQStats(rt.telemetry, "capture_surveillance", survIQ, nil)
  446. observeIQStats(rt.telemetry, "capture_detail", detailIQ, nil)
  447. }
  448. }
  449. survSpectrum := rt.spectrumFromIQ(survIQ, gpuState)
  450. sanitizeSpectrum(survSpectrum)
  451. detailSpectrum := survSpectrum
  452. if !sameIQBuffer(detailIQ, survIQ) {
  453. detailSpectrum = rt.spectrumFromIQWithPlan(detailIQ, rt.detailWindow, rt.detailPlan, gpuState, false)
  454. sanitizeSpectrum(detailSpectrum)
  455. }
  456. policy := pipeline.PolicyFromConfig(rt.cfg)
  457. plan := rt.buildSurveillancePlan(policy)
  458. surveillanceSpectra := make([]pipeline.SurveillanceLevelSpectrum, 0, len(plan.Specs))
  459. for _, spec := range plan.Specs {
  460. if spec.Level.FFTSize <= 0 {
  461. continue
  462. }
  463. var spectrum []float64
  464. if spec.Decim <= 1 {
  465. if spec.Level.FFTSize == len(survSpectrum) {
  466. spectrum = survSpectrum
  467. } else {
  468. spectrum = rt.spectrumForLevel(survIQ, spec.Level.FFTSize, gpuState, spec.AllowGPU)
  469. sanitizeSpectrum(spectrum)
  470. }
  471. } else {
  472. required := spec.Level.FFTSize * spec.Decim
  473. if required > len(survIQ) {
  474. continue
  475. }
  476. src := survIQ
  477. if len(src) > required {
  478. src = src[len(src)-required:]
  479. }
  480. decimated := rt.decimateSurveillanceIQ(src, spec.Decim)
  481. spectrum = rt.spectrumForLevel(decimated, spec.Level.FFTSize, gpuState, false)
  482. sanitizeSpectrum(spectrum)
  483. }
  484. if len(spectrum) == 0 {
  485. continue
  486. }
  487. surveillanceSpectra = append(surveillanceSpectra, pipeline.SurveillanceLevelSpectrum{Level: spec.Level, Spectrum: spectrum})
  488. }
  489. now := time.Now()
  490. finished, detected := rt.det.Process(now, survSpectrum, rt.cfg.CenterHz)
  491. if rt.telemetry != nil {
  492. rt.telemetry.SetGauge("signals.detected.count", float64(len(detected)), nil)
  493. rt.telemetry.SetGauge("signals.finished.count", float64(len(finished)), nil)
  494. }
  495. return &spectrumArtifacts{
  496. allIQ: allIQ,
  497. streamDropped: streamDropped,
  498. surveillanceIQ: survIQ,
  499. detailIQ: detailIQ,
  500. surveillanceSpectrum: survSpectrum,
  501. surveillanceSpectra: surveillanceSpectra,
  502. surveillancePlan: plan,
  503. detailSpectrum: detailSpectrum,
  504. finished: finished,
  505. detected: detected,
  506. thresholds: rt.det.LastThresholds(),
  507. noiseFloor: rt.det.LastNoiseFloor(),
  508. now: now,
  509. }, nil
  510. }
  511. func (rt *dspRuntime) buildSurveillanceResult(art *spectrumArtifacts) pipeline.SurveillanceResult {
  512. if art == nil {
  513. return pipeline.SurveillanceResult{}
  514. }
  515. policy := pipeline.PolicyFromConfig(rt.cfg)
  516. plan := art.surveillancePlan
  517. if plan.Primary.Name == "" {
  518. plan = rt.buildSurveillancePlan(policy)
  519. }
  520. primaryCandidates := pipeline.CandidatesFromSignalsWithLevel(art.detected, "surveillance-detector", plan.Primary)
  521. derivedCandidates := rt.detectDerivedCandidates(art, plan)
  522. candidates := pipeline.FuseCandidates(primaryCandidates, derivedCandidates)
  523. pipeline.ApplyMonitorWindowMatchesToCandidates(policy, candidates)
  524. scheduled := pipeline.ScheduleCandidates(candidates, policy)
  525. return pipeline.SurveillanceResult{
  526. Level: plan.Primary,
  527. Levels: plan.Levels,
  528. LevelSet: plan.LevelSet,
  529. DetectionPolicy: plan.DetectionPolicy,
  530. DisplayLevel: plan.Presentation,
  531. Context: plan.Context,
  532. Spectra: art.surveillanceSpectra,
  533. Candidates: candidates,
  534. Scheduled: scheduled,
  535. Finished: art.finished,
  536. Signals: art.detected,
  537. NoiseFloor: art.noiseFloor,
  538. Thresholds: art.thresholds,
  539. }
  540. }
  541. func (rt *dspRuntime) detectDerivedCandidates(art *spectrumArtifacts, plan surveillancePlan) []pipeline.Candidate {
  542. if art == nil || len(plan.LevelSet.Derived) == 0 {
  543. return nil
  544. }
  545. spectra := map[string][]float64{}
  546. for _, spec := range art.surveillanceSpectra {
  547. if spec.Level.Name == "" || len(spec.Spectrum) == 0 {
  548. continue
  549. }
  550. spectra[spec.Level.Name] = spec.Spectrum
  551. }
  552. if len(spectra) == 0 {
  553. return nil
  554. }
  555. out := make([]pipeline.Candidate, 0, len(plan.LevelSet.Derived))
  556. for _, level := range plan.LevelSet.Derived {
  557. if level.Name == "" {
  558. continue
  559. }
  560. if !pipeline.IsDetectionLevel(level) {
  561. continue
  562. }
  563. spectrum := spectra[level.Name]
  564. if len(spectrum) == 0 {
  565. continue
  566. }
  567. entry := rt.derivedDetectorForLevel(level)
  568. if entry == nil || entry.det == nil {
  569. continue
  570. }
  571. _, signals := entry.det.Process(art.now, spectrum, level.CenterHz)
  572. if len(signals) == 0 {
  573. continue
  574. }
  575. cands := pipeline.CandidatesFromSignalsWithLevel(signals, "surveillance-derived", level)
  576. for i := range cands {
  577. if cands[i].ID == 0 {
  578. continue
  579. }
  580. cands[i].ID = entry.idBase - cands[i].ID
  581. }
  582. out = append(out, cands...)
  583. }
  584. if len(out) == 0 {
  585. return nil
  586. }
  587. return out
  588. }
  589. func (rt *dspRuntime) derivedDetectorForLevel(level pipeline.AnalysisLevel) *derivedDetector {
  590. if level.SampleRate <= 0 || level.FFTSize <= 0 {
  591. return nil
  592. }
  593. if rt.derivedDetectors == nil {
  594. rt.derivedDetectors = map[string]*derivedDetector{}
  595. }
  596. key := level.Name
  597. if key == "" {
  598. key = fmt.Sprintf("%d:%d", level.SampleRate, level.FFTSize)
  599. }
  600. entry := rt.derivedDetectors[key]
  601. if entry != nil && entry.sampleRate == level.SampleRate && entry.fftSize == level.FFTSize {
  602. return entry
  603. }
  604. if rt.nextDerivedBase == 0 {
  605. rt.nextDerivedBase = -derivedIDBlock
  606. }
  607. entry = &derivedDetector{
  608. det: detector.New(rt.cfg.Detector, level.SampleRate, level.FFTSize),
  609. sampleRate: level.SampleRate,
  610. fftSize: level.FFTSize,
  611. idBase: rt.nextDerivedBase,
  612. }
  613. rt.nextDerivedBase -= derivedIDBlock
  614. rt.derivedDetectors[key] = entry
  615. return entry
  616. }
  617. func (rt *dspRuntime) buildRefinementInput(surv pipeline.SurveillanceResult, now time.Time) pipeline.RefinementInput {
  618. policy := pipeline.PolicyFromConfig(rt.cfg)
  619. baseBudget := pipeline.BudgetModelFromPolicy(policy)
  620. pressure := pipeline.BuildBudgetPressureSummary(baseBudget, rt.arbitration.Refinement, rt.arbitration.Queue)
  621. budget := pipeline.ApplyBudgetRebalance(policy, baseBudget, pressure)
  622. plan := pipeline.BuildRefinementPlanWithBudget(surv.Candidates, policy, budget)
  623. admission := rt.arbiter.AdmitRefinementWithBudget(plan, policy, budget, now)
  624. plan = admission.Plan
  625. workItems := make([]pipeline.RefinementWorkItem, 0, len(admission.WorkItems))
  626. if len(admission.WorkItems) > 0 {
  627. workItems = append(workItems, admission.WorkItems...)
  628. }
  629. scheduled := append([]pipeline.ScheduledCandidate(nil), admission.Admitted...)
  630. workIndex := map[int64]int{}
  631. for i := range workItems {
  632. if workItems[i].Candidate.ID == 0 {
  633. continue
  634. }
  635. workIndex[workItems[i].Candidate.ID] = i
  636. }
  637. windows := make([]pipeline.RefinementWindow, 0, len(scheduled))
  638. for _, sc := range scheduled {
  639. window := pipeline.RefinementWindowForCandidate(policy, sc.Candidate)
  640. windows = append(windows, window)
  641. if idx, ok := workIndex[sc.Candidate.ID]; ok {
  642. workItems[idx].Window = window
  643. }
  644. }
  645. detailFFT := rt.cfg.Refinement.DetailFFTSize
  646. if detailFFT <= 0 {
  647. detailFFT = rt.cfg.FFTSize
  648. }
  649. levelSpan := spanForPolicy(policy, float64(rt.cfg.SampleRate))
  650. if _, maxSpan, ok := windowSpanBounds(windows); ok {
  651. levelSpan = maxSpan
  652. }
  653. level := analysisLevel("refinement", "refinement", "refinement", rt.cfg.SampleRate, detailFFT, rt.cfg.CenterHz, levelSpan, "refinement-window", 1, rt.cfg.SampleRate)
  654. detailLevel := analysisLevel("detail", "detail", "refinement", rt.cfg.SampleRate, detailFFT, rt.cfg.CenterHz, levelSpan, "detail-spectrum", 1, rt.cfg.SampleRate)
  655. if len(workItems) > 0 {
  656. for i := range workItems {
  657. item := &workItems[i]
  658. if item.Window.SpanHz <= 0 {
  659. continue
  660. }
  661. item.Execution = &pipeline.RefinementExecution{
  662. Stage: "refine",
  663. SampleRate: rt.cfg.SampleRate,
  664. FFTSize: detailFFT,
  665. CenterHz: item.Window.CenterHz,
  666. SpanHz: item.Window.SpanHz,
  667. Source: detailLevel.Source,
  668. }
  669. }
  670. }
  671. input := pipeline.RefinementInput{
  672. Level: level,
  673. Detail: detailLevel,
  674. Context: surv.Context,
  675. Request: pipeline.RefinementRequest{Strategy: plan.Strategy, Reason: "surveillance-plan", SpanHintHz: levelSpan},
  676. Budgets: budget,
  677. Admission: admission.Admission,
  678. Candidates: append([]pipeline.Candidate(nil), surv.Candidates...),
  679. Scheduled: scheduled,
  680. WorkItems: workItems,
  681. Plan: plan,
  682. Windows: windows,
  683. SampleRate: rt.cfg.SampleRate,
  684. FFTSize: detailFFT,
  685. CenterHz: rt.cfg.CenterHz,
  686. Source: "surveillance-detector",
  687. }
  688. input.Context.Refinement = level
  689. input.Context.Detail = detailLevel
  690. if !policy.RefinementEnabled {
  691. for i := range input.WorkItems {
  692. item := &input.WorkItems[i]
  693. if item.Status == pipeline.RefinementStatusDropped {
  694. continue
  695. }
  696. item.Status = pipeline.RefinementStatusDropped
  697. item.Reason = pipeline.RefinementReasonDisabled
  698. }
  699. input.Scheduled = nil
  700. input.Request.Reason = pipeline.ReasonAdmissionDisabled
  701. input.Admission.Reason = pipeline.ReasonAdmissionDisabled
  702. input.Admission.Admitted = 0
  703. input.Admission.Skipped = 0
  704. input.Admission.Displaced = 0
  705. input.Plan.Selected = nil
  706. input.Plan.DroppedByBudget = 0
  707. }
  708. rt.setArbitration(policy, input.Budgets, input.Admission, rt.arbitration.Queue)
  709. return input
  710. }
  711. func (rt *dspRuntime) runRefinement(art *spectrumArtifacts, surv pipeline.SurveillanceResult, extractMgr *extractionManager, rec *recorder.Manager) pipeline.RefinementStep {
  712. input := rt.buildRefinementInput(surv, art.now)
  713. markWorkItemsStatus(input.WorkItems, pipeline.RefinementStatusAdmitted, pipeline.RefinementStatusRunning, pipeline.RefinementReasonRunning)
  714. result := rt.refineSignals(art, input, extractMgr, rec)
  715. markWorkItemsCompleted(input.WorkItems, result.Candidates)
  716. return pipeline.RefinementStep{Input: input, Result: result}
  717. }
  718. func (rt *dspRuntime) refineSignals(art *spectrumArtifacts, input pipeline.RefinementInput, extractMgr *extractionManager, rec *recorder.Manager) pipeline.RefinementResult {
  719. if art == nil || len(art.detailIQ) == 0 || len(input.Scheduled) == 0 {
  720. return pipeline.RefinementResult{}
  721. }
  722. policy := pipeline.PolicyFromConfig(rt.cfg)
  723. selectedCandidates := make([]pipeline.Candidate, 0, len(input.Scheduled))
  724. selectedSignals := make([]detector.Signal, 0, len(input.Scheduled))
  725. for _, sc := range input.Scheduled {
  726. selectedCandidates = append(selectedCandidates, sc.Candidate)
  727. selectedSignals = append(selectedSignals, detector.Signal{
  728. ID: sc.Candidate.ID,
  729. FirstBin: sc.Candidate.FirstBin,
  730. LastBin: sc.Candidate.LastBin,
  731. CenterHz: sc.Candidate.CenterHz,
  732. BWHz: sc.Candidate.BandwidthHz,
  733. PeakDb: sc.Candidate.PeakDb,
  734. SNRDb: sc.Candidate.SNRDb,
  735. NoiseDb: sc.Candidate.NoiseDb,
  736. })
  737. }
  738. sampleRate := input.SampleRate
  739. fftSize := input.FFTSize
  740. centerHz := input.CenterHz
  741. if sampleRate <= 0 {
  742. sampleRate = rt.cfg.SampleRate
  743. }
  744. if fftSize <= 0 {
  745. fftSize = rt.cfg.FFTSize
  746. }
  747. if centerHz == 0 {
  748. centerHz = rt.cfg.CenterHz
  749. }
  750. snips, snipRates := extractSignalIQBatch(extractMgr, art.detailIQ, sampleRate, centerHz, selectedSignals)
  751. refined := pipeline.RefineCandidates(selectedCandidates, input.Windows, art.detailSpectrum, sampleRate, fftSize, snips, snipRates, classifier.ClassifierMode(rt.cfg.ClassifierMode))
  752. signals := make([]detector.Signal, 0, len(refined))
  753. decisions := make([]pipeline.SignalDecision, 0, len(refined))
  754. for i, ref := range refined {
  755. sig := ref.Signal
  756. signals = append(signals, sig)
  757. cls := sig.Class
  758. snipRate := ref.SnippetRate
  759. decision := pipeline.DecideSignalAction(policy, ref.Candidate, cls)
  760. decisions = append(decisions, decision)
  761. if cls != nil {
  762. if cls.ModType == classifier.ClassWFM {
  763. cls.ModType = classifier.ClassWFMStereo
  764. signals[i].PlaybackMode = string(classifier.ClassWFMStereo)
  765. signals[i].DemodName = string(classifier.ClassWFMStereo)
  766. signals[i].StereoState = "searching"
  767. }
  768. pll := classifier.PLLResult{}
  769. if i < len(snips) && snips[i] != nil && len(snips[i]) > 256 {
  770. pll = classifier.EstimateExactFrequency(snips[i], snipRate, signals[i].CenterHz, cls.ModType)
  771. cls.PLL = &pll
  772. signals[i].PLL = &pll
  773. if cls.ModType == classifier.ClassWFMStereo {
  774. if pll.Stereo {
  775. signals[i].StereoState = "locked"
  776. } else if signals[i].StereoState == "" {
  777. signals[i].StereoState = "searching"
  778. }
  779. signals[i].PlaybackMode = string(classifier.ClassWFMStereo)
  780. signals[i].DemodName = string(classifier.ClassWFMStereo)
  781. }
  782. }
  783. if cls.ModType == classifier.ClassWFMStereo && rec != nil {
  784. rt.updateRDS(art.now, rec, &signals[i], cls)
  785. if signals[i].PLL != nil && signals[i].PLL.RDSStation != "" {
  786. signals[i].StereoState = "locked"
  787. }
  788. }
  789. }
  790. }
  791. budget := input.Budgets
  792. queueStats := rt.arbiter.ApplyDecisions(decisions, budget, art.now, policy)
  793. rt.setArbitration(policy, budget, input.Admission, queueStats)
  794. summary := summarizeDecisions(decisions)
  795. if rec != nil {
  796. if summary.RecordEnabled > 0 {
  797. rt.cfg.Recorder.Enabled = true
  798. }
  799. if summary.DecodeEnabled > 0 {
  800. rt.cfg.Recorder.AutoDecode = true
  801. }
  802. }
  803. rt.det.UpdateClasses(signals)
  804. return pipeline.RefinementResult{Level: input.Level, Signals: signals, Decisions: decisions, Candidates: selectedCandidates}
  805. }
  806. func (rt *dspRuntime) updateRDS(now time.Time, rec *recorder.Manager, sig *detector.Signal, cls *classifier.Classification) {
  807. if sig == nil || cls == nil {
  808. return
  809. }
  810. keyHz := sig.CenterHz
  811. if sig.PLL != nil && sig.PLL.ExactHz != 0 {
  812. keyHz = sig.PLL.ExactHz
  813. }
  814. key := int64(math.Round(keyHz / 25000.0))
  815. st := rt.rdsMap[key]
  816. if st == nil {
  817. st = &rdsState{}
  818. rt.rdsMap[key] = st
  819. }
  820. if now.Sub(st.lastDecode) >= 4*time.Second && atomic.LoadInt32(&st.busy) == 0 {
  821. st.lastDecode = now
  822. atomic.StoreInt32(&st.busy, 1)
  823. go func(st *rdsState, sigHz float64) {
  824. defer atomic.StoreInt32(&st.busy, 0)
  825. ringIQ, ringSR, ringCenter := rec.SliceRecent(4.0)
  826. if len(ringIQ) < ringSR || ringSR <= 0 {
  827. return
  828. }
  829. offset := sigHz - ringCenter
  830. shifted := dsp.FreqShift(ringIQ, ringSR, offset)
  831. decim1 := ringSR / 1000000
  832. if decim1 < 1 {
  833. decim1 = 1
  834. }
  835. lp1 := dsp.LowpassFIR(float64(ringSR/decim1)/2.0*0.8, ringSR, 51)
  836. f1 := dsp.ApplyFIR(shifted, lp1)
  837. d1 := dsp.Decimate(f1, decim1)
  838. rate1 := ringSR / decim1
  839. decim2 := rate1 / 250000
  840. if decim2 < 1 {
  841. decim2 = 1
  842. }
  843. lp2 := dsp.LowpassFIR(float64(rate1/decim2)/2.0*0.8, rate1, 101)
  844. f2 := dsp.ApplyFIR(d1, lp2)
  845. decimated := dsp.Decimate(f2, decim2)
  846. actualRate := rate1 / decim2
  847. rdsBase := demod.RDSBasebandComplex(decimated, actualRate)
  848. if len(rdsBase.Samples) == 0 {
  849. return
  850. }
  851. st.mu.Lock()
  852. result := st.dec.Decode(rdsBase.Samples, rdsBase.SampleRate)
  853. if result.PS != "" {
  854. st.result = result
  855. }
  856. st.mu.Unlock()
  857. }(st, sig.CenterHz)
  858. }
  859. st.mu.Lock()
  860. ps := st.result.PS
  861. st.mu.Unlock()
  862. if ps != "" && sig.PLL != nil {
  863. sig.PLL.RDSStation = strings.TrimSpace(ps)
  864. cls.PLL = sig.PLL
  865. }
  866. }
  867. func (rt *dspRuntime) maintenance(displaySignals []detector.Signal, rec *recorder.Manager) {
  868. if len(rt.rdsMap) > 0 {
  869. activeIDs := make(map[int64]bool, len(displaySignals))
  870. for _, s := range displaySignals {
  871. keyHz := s.CenterHz
  872. if s.PLL != nil && s.PLL.ExactHz != 0 {
  873. keyHz = s.PLL.ExactHz
  874. }
  875. activeIDs[int64(math.Round(keyHz/25000.0))] = true
  876. }
  877. for id := range rt.rdsMap {
  878. if !activeIDs[id] {
  879. delete(rt.rdsMap, id)
  880. }
  881. }
  882. }
  883. if len(rt.streamPhaseState) > 0 {
  884. sigIDs := make(map[int64]bool, len(displaySignals))
  885. for _, s := range displaySignals {
  886. sigIDs[s.ID] = true
  887. }
  888. for id := range rt.streamPhaseState {
  889. if !sigIDs[id] {
  890. delete(rt.streamPhaseState, id)
  891. }
  892. }
  893. }
  894. if rec != nil && len(displaySignals) > 0 {
  895. aqCfg := extractionConfig{firTaps: rt.cfg.Recorder.ExtractionTaps, bwMult: rt.cfg.Recorder.ExtractionBwMult}
  896. _ = aqCfg
  897. }
  898. }
  899. func spanForPolicy(policy pipeline.Policy, fallback float64) float64 {
  900. if policy.MonitorSpanHz > 0 {
  901. return policy.MonitorSpanHz
  902. }
  903. if len(policy.MonitorWindows) > 0 {
  904. maxSpan := 0.0
  905. for _, w := range policy.MonitorWindows {
  906. if w.SpanHz > maxSpan {
  907. maxSpan = w.SpanHz
  908. }
  909. }
  910. if maxSpan > 0 {
  911. return maxSpan
  912. }
  913. }
  914. if policy.MonitorStartHz != 0 && policy.MonitorEndHz != 0 && policy.MonitorEndHz > policy.MonitorStartHz {
  915. return policy.MonitorEndHz - policy.MonitorStartHz
  916. }
  917. return fallback
  918. }
  919. func windowSpanBounds(windows []pipeline.RefinementWindow) (float64, float64, bool) {
  920. minSpan := 0.0
  921. maxSpan := 0.0
  922. ok := false
  923. for _, w := range windows {
  924. if w.SpanHz <= 0 {
  925. continue
  926. }
  927. if !ok || w.SpanHz < minSpan {
  928. minSpan = w.SpanHz
  929. }
  930. if !ok || w.SpanHz > maxSpan {
  931. maxSpan = w.SpanHz
  932. }
  933. ok = true
  934. }
  935. return minSpan, maxSpan, ok
  936. }
  937. func analysisLevel(name, role, truth string, sampleRate int, fftSize int, centerHz float64, spanHz float64, source string, decimation int, baseRate int) pipeline.AnalysisLevel {
  938. level := pipeline.AnalysisLevel{
  939. Name: name,
  940. Role: role,
  941. Truth: truth,
  942. SampleRate: sampleRate,
  943. FFTSize: fftSize,
  944. CenterHz: centerHz,
  945. SpanHz: spanHz,
  946. Source: source,
  947. }
  948. if level.SampleRate > 0 && level.FFTSize > 0 {
  949. level.BinHz = float64(level.SampleRate) / float64(level.FFTSize)
  950. }
  951. if decimation > 0 {
  952. level.Decimation = decimation
  953. } else if baseRate > 0 && level.SampleRate > 0 && baseRate%level.SampleRate == 0 {
  954. level.Decimation = baseRate / level.SampleRate
  955. }
  956. return level
  957. }
  958. func (rt *dspRuntime) buildSurveillancePlan(policy pipeline.Policy) surveillancePlan {
  959. baseRate := rt.cfg.SampleRate
  960. baseFFT := rt.cfg.Surveillance.AnalysisFFTSize
  961. if baseFFT <= 0 {
  962. baseFFT = rt.cfg.FFTSize
  963. }
  964. span := spanForPolicy(policy, float64(baseRate))
  965. detectionPolicy := pipeline.SurveillanceDetectionPolicyFromPolicy(policy)
  966. primary := analysisLevel("surveillance", pipeline.RoleSurveillancePrimary, "surveillance", baseRate, baseFFT, rt.cfg.CenterHz, span, "baseband", 1, baseRate)
  967. levels := []pipeline.AnalysisLevel{primary}
  968. specs := []surveillanceLevelSpec{{Level: primary, Decim: 1, AllowGPU: true}}
  969. context := pipeline.AnalysisContext{Surveillance: primary}
  970. derivedLevels := make([]pipeline.AnalysisLevel, 0, 2)
  971. supportLevels := make([]pipeline.AnalysisLevel, 0, 2)
  972. strategy := strings.ToLower(strings.TrimSpace(policy.SurveillanceStrategy))
  973. switch strategy {
  974. case "multi-res", "multi-resolution", "multi", "multi_res":
  975. decim := 2
  976. derivedRate := baseRate / decim
  977. derivedFFT := baseFFT / decim
  978. if derivedRate >= 200000 && derivedFFT >= 256 {
  979. derivedSpan := spanForPolicy(policy, float64(derivedRate))
  980. role := pipeline.RoleSurveillanceSupport
  981. if detectionPolicy.DerivedDetectionEnabled {
  982. role = pipeline.RoleSurveillanceDerived
  983. }
  984. derived := analysisLevel("surveillance-lowres", role, "surveillance", derivedRate, derivedFFT, rt.cfg.CenterHz, derivedSpan, "decimated", decim, baseRate)
  985. if detectionPolicy.DerivedDetectionEnabled {
  986. levels = append(levels, derived)
  987. derivedLevels = append(derivedLevels, derived)
  988. } else {
  989. supportLevels = append(supportLevels, derived)
  990. }
  991. specs = append(specs, surveillanceLevelSpec{Level: derived, Decim: decim, AllowGPU: false})
  992. context.Derived = append(context.Derived, derived)
  993. }
  994. }
  995. presentation := analysisLevel("presentation", pipeline.RolePresentation, "presentation", baseRate, rt.cfg.Surveillance.DisplayBins, rt.cfg.CenterHz, span, "display", 1, baseRate)
  996. context.Presentation = presentation
  997. if len(derivedLevels) == 0 && detectionPolicy.DerivedDetectionEnabled {
  998. detectionPolicy.DerivedDetectionEnabled = false
  999. detectionPolicy.DerivedDetectionReason = "levels"
  1000. }
  1001. switch {
  1002. case len(derivedLevels) > 0:
  1003. detectionPolicy.DerivedDetectionMode = "detection"
  1004. case len(supportLevels) > 0:
  1005. detectionPolicy.DerivedDetectionMode = "support"
  1006. default:
  1007. detectionPolicy.DerivedDetectionMode = "disabled"
  1008. }
  1009. levelSet := pipeline.SurveillanceLevelSet{
  1010. Primary: primary,
  1011. Derived: append([]pipeline.AnalysisLevel(nil), derivedLevels...),
  1012. Support: append([]pipeline.AnalysisLevel(nil), supportLevels...),
  1013. Presentation: presentation,
  1014. }
  1015. detectionLevels := make([]pipeline.AnalysisLevel, 0, 1+len(derivedLevels))
  1016. detectionLevels = append(detectionLevels, primary)
  1017. detectionLevels = append(detectionLevels, derivedLevels...)
  1018. levelSet.Detection = detectionLevels
  1019. allLevels := make([]pipeline.AnalysisLevel, 0, 1+len(derivedLevels)+len(supportLevels)+1)
  1020. allLevels = append(allLevels, primary)
  1021. allLevels = append(allLevels, derivedLevels...)
  1022. allLevels = append(allLevels, supportLevels...)
  1023. if presentation.Name != "" {
  1024. allLevels = append(allLevels, presentation)
  1025. }
  1026. levelSet.All = allLevels
  1027. return surveillancePlan{
  1028. Primary: primary,
  1029. Levels: levels,
  1030. LevelSet: levelSet,
  1031. Presentation: presentation,
  1032. Context: context,
  1033. DetectionPolicy: detectionPolicy,
  1034. Specs: specs,
  1035. }
  1036. }
  1037. func sameIQBuffer(a []complex64, b []complex64) bool {
  1038. if len(a) != len(b) {
  1039. return false
  1040. }
  1041. if len(a) == 0 {
  1042. return true
  1043. }
  1044. return &a[0] == &b[0]
  1045. }
  1046. func markWorkItemsStatus(items []pipeline.RefinementWorkItem, from string, to string, reason string) {
  1047. for i := range items {
  1048. if items[i].Status != from {
  1049. continue
  1050. }
  1051. items[i].Status = to
  1052. if reason != "" {
  1053. items[i].Reason = reason
  1054. }
  1055. }
  1056. }
  1057. func markWorkItemsCompleted(items []pipeline.RefinementWorkItem, candidates []pipeline.Candidate) {
  1058. if len(items) == 0 || len(candidates) == 0 {
  1059. return
  1060. }
  1061. done := map[int64]struct{}{}
  1062. for _, cand := range candidates {
  1063. if cand.ID != 0 {
  1064. done[cand.ID] = struct{}{}
  1065. }
  1066. }
  1067. for i := range items {
  1068. if _, ok := done[items[i].Candidate.ID]; !ok {
  1069. continue
  1070. }
  1071. items[i].Status = pipeline.RefinementStatusCompleted
  1072. items[i].Reason = pipeline.RefinementReasonCompleted
  1073. }
  1074. }
  1075. func (rt *dspRuntime) setArbitration(policy pipeline.Policy, budget pipeline.BudgetModel, admission pipeline.RefinementAdmission, queue pipeline.DecisionQueueStats) {
  1076. rt.arbitration = pipeline.BuildArbitrationState(policy, budget, admission, queue)
  1077. }