Wideband autonomous SDR analysis engine forked from sdr-visual-suite
Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

207 Zeilen
6.9KB

  1. //go:build cufft && windows
  2. package gpudemod
  3. import (
  4. "os"
  5. "path/filepath"
  6. "testing"
  7. )
  8. func configureNativePreparedDLLPath(t *testing.T) {
  9. t.Helper()
  10. candidates := []string{
  11. filepath.Join("build", "gpudemod_kernels.dll"),
  12. filepath.Join("internal", "demod", "gpudemod", "build", "gpudemod_kernels.dll"),
  13. "gpudemod_kernels.dll",
  14. }
  15. for _, candidate := range candidates {
  16. if _, err := os.Stat(candidate); err == nil {
  17. abs, err := filepath.Abs(candidate)
  18. if err != nil {
  19. t.Fatalf("resolve native prepared DLL path: %v", err)
  20. }
  21. t.Setenv("GPUMOD_DLL", abs)
  22. return
  23. }
  24. }
  25. }
  26. func requireNativePreparedTestRunner(t *testing.T) *BatchRunner {
  27. t.Helper()
  28. configureNativePreparedDLLPath(t)
  29. if err := ensureDLLLoaded(); err != nil {
  30. t.Skipf("native prepared path unavailable: %v", err)
  31. }
  32. if !Available() {
  33. t.Skip("native prepared path unavailable: cuda device not available")
  34. }
  35. r, err := NewBatchRunner(32768, 4000000)
  36. if err != nil {
  37. t.Skipf("native prepared path unavailable: %v", err)
  38. }
  39. t.Cleanup(r.Close)
  40. return r
  41. }
  42. func TestStreamingGPUNativePreparedMatchesCPUOracleAcrossChunkPatterns(t *testing.T) {
  43. job := StreamingExtractJob{
  44. SignalID: 1,
  45. OffsetHz: 12500,
  46. Bandwidth: 20000,
  47. OutRate: 200000,
  48. NumTaps: 65,
  49. ConfigHash: 777,
  50. }
  51. exec := func(r *BatchRunner, invocations []StreamingGPUInvocation) ([]StreamingGPUExecutionResult, error) {
  52. return r.executeStreamingGPUNativePrepared(invocations)
  53. }
  54. t.Run("DeterministicIQ", func(t *testing.T) {
  55. r := requireNativePreparedTestRunner(t)
  56. steps := makeStreamingValidationSteps(
  57. makeDeterministicIQ(8192),
  58. []int{0, 1, 2, 17, 63, 64, 65, 129, 511, 2048},
  59. []StreamingExtractJob{job},
  60. )
  61. runPreparedSequenceAgainstOracle(t, r, exec, steps, 1e-4, 1e-8)
  62. })
  63. t.Run("ToneNoiseIQ", func(t *testing.T) {
  64. r := requireNativePreparedTestRunner(t)
  65. steps := makeStreamingValidationSteps(
  66. makeToneNoiseIQ(12288, 0.023),
  67. []int{7, 20, 3, 63, 64, 65, 777, 2048, 4096},
  68. []StreamingExtractJob{job},
  69. )
  70. runPreparedSequenceAgainstOracle(t, r, exec, steps, 1e-4, 1e-8)
  71. })
  72. }
  73. func TestStreamingGPUNativePreparedLifecycleResetAndCapacity(t *testing.T) {
  74. r := requireNativePreparedTestRunner(t)
  75. exec := func(invocations []StreamingGPUInvocation) ([]StreamingGPUExecutionResult, error) {
  76. return r.executeStreamingGPUNativePrepared(invocations)
  77. }
  78. jobA := StreamingExtractJob{
  79. SignalID: 11,
  80. OffsetHz: 12500,
  81. Bandwidth: 20000,
  82. OutRate: 200000,
  83. NumTaps: 65,
  84. ConfigHash: 3001,
  85. }
  86. jobB := StreamingExtractJob{
  87. SignalID: 22,
  88. OffsetHz: -18750,
  89. Bandwidth: 16000,
  90. OutRate: 100000,
  91. NumTaps: 33,
  92. ConfigHash: 4002,
  93. }
  94. steps := []streamingValidationStep{
  95. {
  96. name: "prime_both_signals",
  97. iq: makeDeterministicIQ(256),
  98. jobs: []StreamingExtractJob{jobA, jobB},
  99. },
  100. {
  101. name: "grow_capacity",
  102. iq: makeToneNoiseIQ(4096, 0.037),
  103. jobs: []StreamingExtractJob{jobA, jobB},
  104. },
  105. {
  106. name: "config_reset_zero_new",
  107. iq: nil,
  108. jobs: []StreamingExtractJob{{SignalID: jobA.SignalID, OffsetHz: jobA.OffsetHz, Bandwidth: jobA.Bandwidth, OutRate: jobA.OutRate, NumTaps: jobA.NumTaps, ConfigHash: jobA.ConfigHash + 1}, jobB},
  109. },
  110. {
  111. name: "signal_b_disappears",
  112. iq: makeDeterministicIQ(64),
  113. jobs: []StreamingExtractJob{jobA},
  114. },
  115. {
  116. name: "signal_b_reappears",
  117. iq: makeToneNoiseIQ(96, 0.017),
  118. jobs: []StreamingExtractJob{jobA, jobB},
  119. },
  120. {
  121. name: "history_boundary",
  122. iq: makeDeterministicIQ(65),
  123. jobs: []StreamingExtractJob{jobA, jobB},
  124. },
  125. }
  126. oracle := NewCPUOracleRunner(r.eng.sampleRate)
  127. var grownCap int
  128. for idx, step := range steps {
  129. invocations, err := r.buildStreamingGPUInvocations(step.iq, step.jobs)
  130. if err != nil {
  131. t.Fatalf("step %d (%s): build invocations failed: %v", idx, step.name, err)
  132. }
  133. got, err := exec(invocations)
  134. if err != nil {
  135. t.Fatalf("step %d (%s): native prepared exec failed: %v", idx, step.name, err)
  136. }
  137. want, err := oracle.StreamingExtract(step.iq, step.jobs)
  138. if err != nil {
  139. t.Fatalf("step %d (%s): oracle failed: %v", idx, step.name, err)
  140. }
  141. if len(got) != len(want) {
  142. t.Fatalf("step %d (%s): result count mismatch: got=%d want=%d", idx, step.name, len(got), len(want))
  143. }
  144. applied := r.applyStreamingGPUExecutionResults(got)
  145. for i, job := range step.jobs {
  146. oracleState := oracle.States[job.SignalID]
  147. requirePreparedExecutionResultMatchesOracle(t, got[i], want[i], oracleState, 1e-4, 1e-8)
  148. requireStreamingExtractResultMatchesOracle(t, applied[i], want[i])
  149. requireExtractStateMatchesOracle(t, r.streamState[job.SignalID], oracleState, 1e-8, 1e-4)
  150. state := r.nativeState[job.SignalID]
  151. if state == nil {
  152. t.Fatalf("step %d (%s): missing native state for signal %d", idx, step.name, job.SignalID)
  153. }
  154. if state.configHash != job.ConfigHash {
  155. t.Fatalf("step %d (%s): native config hash mismatch for signal %d: got=%d want=%d", idx, step.name, job.SignalID, state.configHash, job.ConfigHash)
  156. }
  157. if state.decim != oracleState.Decim {
  158. t.Fatalf("step %d (%s): native decim mismatch for signal %d: got=%d want=%d", idx, step.name, job.SignalID, state.decim, oracleState.Decim)
  159. }
  160. if state.numTaps != oracleState.NumTaps {
  161. t.Fatalf("step %d (%s): native num taps mismatch for signal %d: got=%d want=%d", idx, step.name, job.SignalID, state.numTaps, oracleState.NumTaps)
  162. }
  163. if state.historyCap != maxInt(0, oracleState.NumTaps-1) {
  164. t.Fatalf("step %d (%s): native history cap mismatch for signal %d: got=%d want=%d", idx, step.name, job.SignalID, state.historyCap, maxInt(0, oracleState.NumTaps-1))
  165. }
  166. if state.historyLen != len(oracleState.ShiftedHistory) {
  167. t.Fatalf("step %d (%s): native history len mismatch for signal %d: got=%d want=%d", idx, step.name, job.SignalID, state.historyLen, len(oracleState.ShiftedHistory))
  168. }
  169. if len(step.iq) > 0 && state.shiftedCap < len(step.iq) {
  170. t.Fatalf("step %d (%s): native shifted capacity too small for signal %d: got=%d need>=%d", idx, step.name, job.SignalID, state.shiftedCap, len(step.iq))
  171. }
  172. if state.outCap < got[i].NOut {
  173. t.Fatalf("step %d (%s): native out capacity too small for signal %d: got=%d need>=%d", idx, step.name, job.SignalID, state.outCap, got[i].NOut)
  174. }
  175. if job.SignalID == jobA.SignalID && state.shiftedCap > grownCap {
  176. grownCap = state.shiftedCap
  177. }
  178. }
  179. if step.name == "grow_capacity" && grownCap < len(step.iq) {
  180. t.Fatalf("expected capacity growth for signal %d, got=%d want>=%d", jobA.SignalID, grownCap, len(step.iq))
  181. }
  182. if step.name == "config_reset_zero_new" {
  183. state := r.nativeState[jobA.SignalID]
  184. if state == nil {
  185. t.Fatalf("missing native state for signal %d after config reset", jobA.SignalID)
  186. }
  187. if state.historyLen != 0 {
  188. t.Fatalf("expected cleared native history after config reset, got=%d", state.historyLen)
  189. }
  190. }
  191. if step.name == "signal_b_disappears" {
  192. if _, ok := r.nativeState[jobB.SignalID]; ok {
  193. t.Fatalf("expected native state for signal %d to be removed on disappearance", jobB.SignalID)
  194. }
  195. }
  196. }
  197. }