commkit 2.2.0__tar.gz → 2.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (268) hide show
  1. {commkit-2.2.0 → commkit-2.3.0}/.github/workflows/ci.yml +2 -2
  2. {commkit-2.2.0 → commkit-2.3.0}/AGENTS.md +89 -37
  3. commkit-2.3.0/CHANGELOG.md +116 -0
  4. {commkit-2.2.0 → commkit-2.3.0}/PKG-INFO +3 -3
  5. {commkit-2.2.0 → commkit-2.3.0}/README.md +2 -2
  6. commkit-2.3.0/benchmarks/baselines/Linux-CPython-3.14-64bit/0005_gpu_decoder_path.json +4458 -0
  7. commkit-2.3.0/benchmarks/baselines/Linux-CPython-3.14-64bit/0006_perf_cleanup.json +5054 -0
  8. {commkit-2.2.0 → commkit-2.3.0}/benchmarks/bench_block_blind.py +1 -1
  9. {commkit-2.2.0 → commkit-2.3.0}/benchmarks/bench_block_lms.py +3 -2
  10. commkit-2.3.0/benchmarks/bench_filters.py +41 -0
  11. {commkit-2.2.0 → commkit-2.3.0}/benchmarks/bench_llr.py +16 -0
  12. commkit-2.3.0/benchmarks/bench_mapping.py +47 -0
  13. {commkit-2.2.0 → commkit-2.3.0}/benchmarks/bench_sync_misc.py +43 -2
  14. {commkit-2.2.0 → commkit-2.3.0}/benchmarks/workloads.py +28 -8
  15. {commkit-2.2.0 → commkit-2.3.0}/commkit/__init__.py +27 -15
  16. {commkit-2.2.0 → commkit-2.3.0}/commkit/_array.py +30 -27
  17. {commkit-2.2.0 → commkit-2.3.0}/commkit/_circular.py +3 -5
  18. commkit-2.3.0/commkit/_cuda/__init__.py +674 -0
  19. {commkit-2.2.0 → commkit-2.3.0}/commkit/_cuda/compiler.py +11 -12
  20. {commkit-2.2.0 → commkit-2.3.0}/commkit/_cuda/graphs.py +25 -28
  21. {commkit-2.2.0 → commkit-2.3.0}/commkit/_cuda/src/bps_min_d2.cu +36 -33
  22. commkit-2.3.0/commkit/_cuda/src/iir.cu +222 -0
  23. commkit-2.3.0/commkit/_cuda/src/llr.cu +337 -0
  24. {commkit-2.2.0 → commkit-2.3.0}/commkit/_dispersion.py +10 -6
  25. {commkit-2.2.0 → commkit-2.3.0}/commkit/_fft.py +13 -6
  26. {commkit-2.2.0 → commkit-2.3.0}/commkit/_fiber.py +64 -149
  27. {commkit-2.2.0 → commkit-2.3.0}/commkit/_overlap_save.py +13 -15
  28. {commkit-2.2.0 → commkit-2.3.0}/commkit/_random.py +13 -9
  29. {commkit-2.2.0 → commkit-2.3.0}/commkit/_sequences.py +25 -25
  30. commkit-2.3.0/commkit/analysis/__init__.py +65 -0
  31. {commkit-2.2.0 → commkit-2.3.0}/commkit/analysis/_common.py +11 -10
  32. {commkit-2.2.0 → commkit-2.3.0}/commkit/analysis/allan.py +30 -28
  33. {commkit-2.2.0 → commkit-2.3.0}/commkit/analysis/drift.py +48 -43
  34. {commkit-2.2.0 → commkit-2.3.0}/commkit/analysis/fm_noise.py +63 -62
  35. {commkit-2.2.0 → commkit-2.3.0}/commkit/analysis/interferometry.py +99 -97
  36. commkit-2.3.0/commkit/analysis/linewidth/__init__.py +138 -0
  37. commkit-2.3.0/commkit/analysis/linewidth/dsh.py +623 -0
  38. commkit-2.3.0/commkit/analysis/linewidth/phase.py +381 -0
  39. commkit-2.3.0/commkit/analysis/linewidth/result.py +141 -0
  40. commkit-2.3.0/commkit/analysis/trajectory.py +113 -0
  41. {commkit-2.2.0 → commkit-2.3.0}/commkit/backend.py +61 -63
  42. commkit-2.3.0/commkit/coding/__init__.py +21 -0
  43. {commkit-2.2.0 → commkit-2.3.0}/commkit/coding/base.py +1 -2
  44. {commkit-2.2.0 → commkit-2.3.0}/commkit/coding/bch.py +1 -2
  45. {commkit-2.2.0 → commkit-2.3.0}/commkit/coding/convolutional.py +1 -2
  46. {commkit-2.2.0 → commkit-2.3.0}/commkit/coding/crc.py +1 -2
  47. {commkit-2.2.0 → commkit-2.3.0}/commkit/coding/galois.py +2 -3
  48. {commkit-2.2.0 → commkit-2.3.0}/commkit/coding/hamming.py +1 -2
  49. {commkit-2.2.0 → commkit-2.3.0}/commkit/coding/interleaving.py +1 -2
  50. {commkit-2.2.0 → commkit-2.3.0}/commkit/coding/ldpc.py +1 -2
  51. {commkit-2.2.0 → commkit-2.3.0}/commkit/coding/polar.py +1 -2
  52. {commkit-2.2.0 → commkit-2.3.0}/commkit/coding/ratematch.py +1 -2
  53. {commkit-2.2.0 → commkit-2.3.0}/commkit/coding/reed_solomon.py +1 -2
  54. {commkit-2.2.0 → commkit-2.3.0}/commkit/coding/turbo.py +1 -2
  55. commkit-2.3.0/commkit/core/__init__.py +23 -0
  56. {commkit-2.2.0 → commkit-2.3.0}/commkit/core/_signal_adapter.py +10 -3
  57. {commkit-2.2.0 → commkit-2.3.0}/commkit/core/frame.py +149 -230
  58. {commkit-2.2.0 → commkit-2.3.0}/commkit/core/generation.py +21 -20
  59. {commkit-2.2.0 → commkit-2.3.0}/commkit/core/signal.py +25 -21
  60. commkit-2.3.0/commkit/equalization/__init__.py +55 -0
  61. commkit-2.3.0/commkit/equalization/_block/__init__.py +18 -0
  62. {commkit-2.2.0 → commkit-2.3.0}/commkit/equalization/_block/_blind.py +42 -11
  63. {commkit-2.2.0 → commkit-2.3.0}/commkit/equalization/_block/_dd.py +199 -215
  64. {commkit-2.2.0 → commkit-2.3.0}/commkit/equalization/_block/_engine.py +124 -30
  65. {commkit-2.2.0 → commkit-2.3.0}/commkit/equalization/_common.py +129 -70
  66. commkit-2.3.0/commkit/equalization/_kernels_numba.py +48 -0
  67. commkit-2.3.0/commkit/equalization/_numba_loops.py +839 -0
  68. commkit-2.3.0/commkit/equalization/blind.py +338 -0
  69. {commkit-2.2.0 → commkit-2.3.0}/commkit/equalization/linear.py +91 -87
  70. {commkit-2.2.0 → commkit-2.3.0}/commkit/equalization/polarization.py +280 -261
  71. {commkit-2.2.0 → commkit-2.3.0}/commkit/equalization/result.py +52 -52
  72. commkit-2.3.0/commkit/equalization/sequential/__init__.py +19 -0
  73. commkit-2.3.0/commkit/equalization/sequential/_blind.py +463 -0
  74. commkit-2.3.0/commkit/equalization/sequential/_dd.py +667 -0
  75. {commkit-2.2.0 → commkit-2.3.0}/commkit/equalization/sequential/_setup.py +24 -21
  76. commkit-2.3.0/commkit/filtering/__init__.py +75 -0
  77. commkit-2.3.0/commkit/filtering/fiber.py +204 -0
  78. commkit-2.3.0/commkit/filtering/linear.py +808 -0
  79. commkit-2.3.0/commkit/filtering/pulses.py +488 -0
  80. {commkit-2.2.0 → commkit-2.3.0}/commkit/frequency.py +137 -123
  81. commkit-2.3.0/commkit/impairments/__init__.py +39 -0
  82. {commkit-2.2.0 → commkit-2.3.0}/commkit/impairments/channel/__init__.py +3 -4
  83. {commkit-2.2.0 → commkit-2.3.0}/commkit/impairments/channel/linear.py +59 -84
  84. {commkit-2.2.0 → commkit-2.3.0}/commkit/impairments/channel/nonlinear.py +9 -11
  85. {commkit-2.2.0 → commkit-2.3.0}/commkit/impairments/frontend.py +73 -65
  86. {commkit-2.2.0 → commkit-2.3.0}/commkit/impairments/noise.py +17 -21
  87. {commkit-2.2.0 → commkit-2.3.0}/commkit/impairments/source.py +47 -43
  88. {commkit-2.2.0 → commkit-2.3.0}/commkit/io.py +53 -51
  89. {commkit-2.2.0 → commkit-2.3.0}/commkit/logger.py +10 -13
  90. {commkit-2.2.0 → commkit-2.3.0}/commkit/mapping/__init__.py +6 -7
  91. {commkit-2.2.0 → commkit-2.3.0}/commkit/mapping/bits.py +10 -13
  92. {commkit-2.2.0 → commkit-2.3.0}/commkit/mapping/constellation.py +57 -43
  93. {commkit-2.2.0 → commkit-2.3.0}/commkit/mapping/gray.py +117 -119
  94. {commkit-2.2.0 → commkit-2.3.0}/commkit/mapping/llr.py +285 -70
  95. {commkit-2.2.0 → commkit-2.3.0}/commkit/mapping/shaping.py +12 -13
  96. {commkit-2.2.0 → commkit-2.3.0}/commkit/math.py +105 -109
  97. {commkit-2.2.0 → commkit-2.3.0}/commkit/metrics.py +53 -44
  98. {commkit-2.2.0 → commkit-2.3.0}/commkit/multirate.py +34 -76
  99. {commkit-2.2.0 → commkit-2.3.0}/commkit/plotting/__init__.py +16 -8
  100. {commkit-2.2.0 → commkit-2.3.0}/commkit/plotting/analysis.py +101 -72
  101. {commkit-2.2.0 → commkit-2.3.0}/commkit/plotting/constellation.py +38 -54
  102. {commkit-2.2.0 → commkit-2.3.0}/commkit/plotting/equalizer.py +39 -28
  103. {commkit-2.2.0 → commkit-2.3.0}/commkit/plotting/eye.py +55 -71
  104. {commkit-2.2.0 → commkit-2.3.0}/commkit/plotting/filter_response.py +35 -42
  105. {commkit-2.2.0 → commkit-2.3.0}/commkit/plotting/spectral.py +58 -125
  106. {commkit-2.2.0 → commkit-2.3.0}/commkit/plotting/sync.py +371 -752
  107. {commkit-2.2.0 → commkit-2.3.0}/commkit/plotting/theme.py +61 -26
  108. {commkit-2.2.0 → commkit-2.3.0}/commkit/plotting/waveform.py +25 -42
  109. {commkit-2.2.0 → commkit-2.3.0}/commkit/recovery/__init__.py +12 -9
  110. {commkit-2.2.0 → commkit-2.3.0}/commkit/recovery/_common.py +64 -39
  111. {commkit-2.2.0 → commkit-2.3.0}/commkit/recovery/bps.py +77 -50
  112. {commkit-2.2.0 → commkit-2.3.0}/commkit/recovery/carrier_phase.py +14 -14
  113. {commkit-2.2.0 → commkit-2.3.0}/commkit/recovery/corrections.py +145 -131
  114. {commkit-2.2.0 → commkit-2.3.0}/commkit/recovery/pilots.py +76 -60
  115. {commkit-2.2.0 → commkit-2.3.0}/commkit/recovery/pll.py +119 -115
  116. {commkit-2.2.0 → commkit-2.3.0}/commkit/recovery/tikhonov.py +46 -42
  117. {commkit-2.2.0 → commkit-2.3.0}/commkit/recovery/viterbi_viterbi.py +8 -8
  118. commkit-2.3.0/commkit/smoothing.py +148 -0
  119. {commkit-2.2.0 → commkit-2.3.0}/commkit/spectral.py +115 -141
  120. {commkit-2.2.0 → commkit-2.3.0}/commkit/timing.py +143 -160
  121. {commkit-2.2.0 → commkit-2.3.0}/pyproject.toml +7 -8
  122. {commkit-2.2.0 → commkit-2.3.0}/tests/common/__init__.py +2 -4
  123. {commkit-2.2.0 → commkit-2.3.0}/tests/common/kernel_utils.py +5 -41
  124. {commkit-2.2.0 → commkit-2.3.0}/tests/conftest.py +1 -19
  125. {commkit-2.2.0 → commkit-2.3.0}/tests/core/test_frame.py +25 -11
  126. {commkit-2.2.0 → commkit-2.3.0}/tests/equalization/test_blind.py +46 -2
  127. {commkit-2.2.0 → commkit-2.3.0}/tests/equalization/test_block.py +47 -26
  128. {commkit-2.2.0 → commkit-2.3.0}/tests/equalization/test_bps_kernel.py +81 -54
  129. commkit-2.3.0/tests/equalization/test_cs_kernel.py +144 -0
  130. {commkit-2.2.0 → commkit-2.3.0}/tests/equalization/test_polarization.py +24 -0
  131. {commkit-2.2.0 → commkit-2.3.0}/tests/equalization/test_sequential.py +12 -8
  132. {commkit-2.2.0 → commkit-2.3.0}/tests/equalization/test_state.py +3 -1
  133. {commkit-2.2.0 → commkit-2.3.0}/tests/equalization/test_winit.py +32 -0
  134. {commkit-2.2.0 → commkit-2.3.0}/tests/impairments/channel/test_channel_nonlinear.py +35 -0
  135. {commkit-2.2.0 → commkit-2.3.0}/tests/mapping/test_constellation.py +48 -0
  136. {commkit-2.2.0 → commkit-2.3.0}/tests/mapping/test_gray.py +62 -0
  137. {commkit-2.2.0 → commkit-2.3.0}/tests/mapping/test_llr.py +210 -42
  138. {commkit-2.2.0 → commkit-2.3.0}/tests/plotting/test_plot_equalizer.py +3 -2
  139. {commkit-2.2.0 → commkit-2.3.0}/tests/plotting/test_plot_eye.py +14 -0
  140. {commkit-2.2.0 → commkit-2.3.0}/tests/plugins/precision_trace.py +24 -2
  141. commkit-2.3.0/tests/plugins/transfer_trace.py +158 -0
  142. {commkit-2.2.0 → commkit-2.3.0}/tests/recovery/test_corrections.py +20 -4
  143. {commkit-2.2.0 → commkit-2.3.0}/tests/test_api_contracts.py +31 -31
  144. {commkit-2.2.0 → commkit-2.3.0}/tests/test_cuda_infra.py +20 -49
  145. {commkit-2.2.0 → commkit-2.3.0}/tests/test_fiber.py +0 -39
  146. {commkit-2.2.0 → commkit-2.3.0}/tests/test_filtering.py +125 -0
  147. {commkit-2.2.0 → commkit-2.3.0}/tests/test_frequency.py +20 -0
  148. {commkit-2.2.0 → commkit-2.3.0}/tests/test_math.py +18 -5
  149. {commkit-2.2.0 → commkit-2.3.0}/tests/test_smoothing.py +25 -0
  150. {commkit-2.2.0 → commkit-2.3.0}/uv.lock +2 -2
  151. commkit-2.2.0/commkit/_cuda/__init__.py +0 -467
  152. commkit-2.2.0/commkit/_cuda/src/llr.cu +0 -110
  153. commkit-2.2.0/commkit/analysis/__init__.py +0 -78
  154. commkit-2.2.0/commkit/analysis/linewidth.py +0 -1206
  155. commkit-2.2.0/commkit/analysis/trajectory.py +0 -112
  156. commkit-2.2.0/commkit/coding/__init__.py +0 -23
  157. commkit-2.2.0/commkit/core/__init__.py +0 -24
  158. commkit-2.2.0/commkit/equalization/__init__.py +0 -46
  159. commkit-2.2.0/commkit/equalization/_block/__init__.py +0 -24
  160. commkit-2.2.0/commkit/equalization/_kernels_numba.py +0 -1748
  161. commkit-2.2.0/commkit/equalization/blind.py +0 -252
  162. commkit-2.2.0/commkit/equalization/sequential/__init__.py +0 -21
  163. commkit-2.2.0/commkit/equalization/sequential/_blind.py +0 -494
  164. commkit-2.2.0/commkit/equalization/sequential/_dd.py +0 -732
  165. commkit-2.2.0/commkit/filtering.py +0 -1351
  166. commkit-2.2.0/commkit/impairments/__init__.py +0 -44
  167. commkit-2.2.0/commkit/smoothing.py +0 -133
  168. commkit-2.2.0/tests/equalization/test_cs_kernel.py +0 -116
  169. {commkit-2.2.0 → commkit-2.3.0}/.gitattributes +0 -0
  170. {commkit-2.2.0 → commkit-2.3.0}/.github/workflows/publish.yml +0 -0
  171. {commkit-2.2.0 → commkit-2.3.0}/.gitignore +0 -0
  172. {commkit-2.2.0 → commkit-2.3.0}/.python-version +0 -0
  173. {commkit-2.2.0 → commkit-2.3.0}/LICENSE +0 -0
  174. {commkit-2.2.0 → commkit-2.3.0}/benchmarks/baselines/Linux-CPython-3.14-64bit/0002_pre_v2.json +0 -0
  175. {commkit-2.2.0 → commkit-2.3.0}/benchmarks/baselines/Linux-CPython-3.14-64bit/0003_v2_0.json +0 -0
  176. {commkit-2.2.0 → commkit-2.3.0}/benchmarks/baselines/Linux-CPython-3.14-64bit/0004_fiber_dbp.json +0 -0
  177. {commkit-2.2.0 → commkit-2.3.0}/benchmarks/bench_bps.py +0 -0
  178. {commkit-2.2.0 → commkit-2.3.0}/benchmarks/bench_equalizers.py +0 -0
  179. {commkit-2.2.0 → commkit-2.3.0}/benchmarks/bench_fiber.py +0 -0
  180. {commkit-2.2.0 → commkit-2.3.0}/benchmarks/bench_import.py +0 -0
  181. {commkit-2.2.0 → commkit-2.3.0}/benchmarks/bench_signal_pipeline.py +0 -0
  182. {commkit-2.2.0 → commkit-2.3.0}/benchmarks/benchutils.py +0 -0
  183. {commkit-2.2.0 → commkit-2.3.0}/benchmarks/conftest.py +0 -0
  184. {commkit-2.2.0 → commkit-2.3.0}/benchmarks/record_baseline.py +0 -0
  185. {commkit-2.2.0 → commkit-2.3.0}/commkit/_cuda/src/bps_anchor.cu +0 -0
  186. {commkit-2.2.0 → commkit-2.3.0}/commkit/_cuda/src/cs_block.cu +0 -0
  187. {commkit-2.2.0 → commkit-2.3.0}/commkit/_cuda/src/selftest.cu +0 -0
  188. {commkit-2.2.0 → commkit-2.3.0}/commkit/py.typed +0 -0
  189. {commkit-2.2.0 → commkit-2.3.0}/examples/00-getting-started.ipynb +0 -0
  190. {commkit-2.2.0 → commkit-2.3.0}/examples/01-constellations-and-bit-mapping.ipynb +0 -0
  191. {commkit-2.2.0 → commkit-2.3.0}/examples/02-spectral-analysis.ipynb +0 -0
  192. {commkit-2.2.0 → commkit-2.3.0}/examples/03-digital-filters.ipynb +0 -0
  193. {commkit-2.2.0 → commkit-2.3.0}/examples/04-pulse-shaping-and-matched-filtering.ipynb +0 -0
  194. {commkit-2.2.0 → commkit-2.3.0}/examples/05-multirate-and-resampling.ipynb +0 -0
  195. {commkit-2.2.0 → commkit-2.3.0}/examples/06-awgn-and-performance-metrics.ipynb +0 -0
  196. {commkit-2.2.0 → commkit-2.3.0}/examples/07-laser-phase-noise.ipynb +0 -0
  197. {commkit-2.2.0 → commkit-2.3.0}/examples/08-front-end-impairments.ipynb +0 -0
  198. {commkit-2.2.0 → commkit-2.3.0}/examples/09-frame-synchronization-and-timing.ipynb +0 -0
  199. {commkit-2.2.0 → commkit-2.3.0}/examples/10-frequency-offset-estimation.ipynb +0 -0
  200. {commkit-2.2.0 → commkit-2.3.0}/examples/11-blind-carrier-phase-recovery.ipynb +0 -0
  201. {commkit-2.2.0 → commkit-2.3.0}/examples/12-pilot-aided-carrier-recovery.ipynb +0 -0
  202. {commkit-2.2.0 → commkit-2.3.0}/examples/13-linear-channel-equalization.ipynb +0 -0
  203. {commkit-2.2.0 → commkit-2.3.0}/examples/14-adaptive-equalization.ipynb +0 -0
  204. {commkit-2.2.0 → commkit-2.3.0}/examples/15-blind-equalization.ipynb +0 -0
  205. {commkit-2.2.0 → commkit-2.3.0}/examples/16-polarization-multiplexing.ipynb +0 -0
  206. {commkit-2.2.0 → commkit-2.3.0}/examples/17-chromatic-dispersion.ipynb +0 -0
  207. {commkit-2.2.0 → commkit-2.3.0}/examples/18-nonlinear-fiber-and-backpropagation.ipynb +0 -0
  208. {commkit-2.2.0 → commkit-2.3.0}/examples/19-coherent-transceiver-end-to-end.ipynb +0 -0
  209. {commkit-2.2.0 → commkit-2.3.0}/examples/20-carrier-phase-characterization.ipynb +0 -0
  210. {commkit-2.2.0 → commkit-2.3.0}/examples/21-laser-linewidth-self-heterodyne.ipynb +0 -0
  211. {commkit-2.2.0 → commkit-2.3.0}/examples/22-laser-linewidth-short-delay-and-flicker.ipynb +0 -0
  212. {commkit-2.2.0 → commkit-2.3.0}/examples/23-laser-linewidth-self-homodyne-iq.ipynb +0 -0
  213. {commkit-2.2.0 → commkit-2.3.0}/examples/24-measurement-template-self-heterodyne.ipynb +0 -0
  214. {commkit-2.2.0 → commkit-2.3.0}/examples/25-measurement-template-self-homodyne-iq.ipynb +0 -0
  215. {commkit-2.2.0 → commkit-2.3.0}/examples/README.md +0 -0
  216. {commkit-2.2.0 → commkit-2.3.0}/tests/analysis/test_allan.py +0 -0
  217. {commkit-2.2.0 → commkit-2.3.0}/tests/analysis/test_drift.py +0 -0
  218. {commkit-2.2.0 → commkit-2.3.0}/tests/analysis/test_fm_noise.py +0 -0
  219. {commkit-2.2.0 → commkit-2.3.0}/tests/analysis/test_interferometry.py +0 -0
  220. {commkit-2.2.0 → commkit-2.3.0}/tests/analysis/test_linewidth.py +0 -0
  221. {commkit-2.2.0 → commkit-2.3.0}/tests/analysis/test_trajectory.py +0 -0
  222. {commkit-2.2.0 → commkit-2.3.0}/tests/common/conversions.py +0 -0
  223. {commkit-2.2.0 → commkit-2.3.0}/tests/common/metrics.py +0 -0
  224. {commkit-2.2.0 → commkit-2.3.0}/tests/common/periodic.py +0 -0
  225. {commkit-2.2.0 → commkit-2.3.0}/tests/common/reference_impl.py +0 -0
  226. {commkit-2.2.0 → commkit-2.3.0}/tests/common/signals.py +0 -0
  227. {commkit-2.2.0 → commkit-2.3.0}/tests/core/test_generation.py +0 -0
  228. {commkit-2.2.0 → commkit-2.3.0}/tests/core/test_psqam.py +0 -0
  229. {commkit-2.2.0 → commkit-2.3.0}/tests/core/test_signal.py +0 -0
  230. {commkit-2.2.0 → commkit-2.3.0}/tests/core/test_signal_adapter.py +0 -0
  231. {commkit-2.2.0 → commkit-2.3.0}/tests/core/test_signal_mimo.py +0 -0
  232. {commkit-2.2.0 → commkit-2.3.0}/tests/equalization/test_cpr.py +0 -0
  233. {commkit-2.2.0 → commkit-2.3.0}/tests/equalization/test_equalizer_oracles.py +0 -0
  234. {commkit-2.2.0 → commkit-2.3.0}/tests/equalization/test_linear.py +0 -0
  235. {commkit-2.2.0 → commkit-2.3.0}/tests/equalization/test_mimo.py +0 -0
  236. {commkit-2.2.0 → commkit-2.3.0}/tests/impairments/channel/test_channel_linear.py +0 -0
  237. {commkit-2.2.0 → commkit-2.3.0}/tests/impairments/test_frontend.py +0 -0
  238. {commkit-2.2.0 → commkit-2.3.0}/tests/impairments/test_noise.py +0 -0
  239. {commkit-2.2.0 → commkit-2.3.0}/tests/impairments/test_source.py +0 -0
  240. {commkit-2.2.0 → commkit-2.3.0}/tests/mapping/test_bits.py +0 -0
  241. {commkit-2.2.0 → commkit-2.3.0}/tests/plotting/test_plot_analysis.py +0 -0
  242. {commkit-2.2.0 → commkit-2.3.0}/tests/plotting/test_plot_constellation.py +0 -0
  243. {commkit-2.2.0 → commkit-2.3.0}/tests/plotting/test_plot_filter_response.py +0 -0
  244. {commkit-2.2.0 → commkit-2.3.0}/tests/plotting/test_plot_spectral.py +0 -0
  245. {commkit-2.2.0 → commkit-2.3.0}/tests/plotting/test_plot_sync.py +0 -0
  246. {commkit-2.2.0 → commkit-2.3.0}/tests/plotting/test_plot_theme.py +0 -0
  247. {commkit-2.2.0 → commkit-2.3.0}/tests/plotting/test_plot_waveform.py +0 -0
  248. {commkit-2.2.0 → commkit-2.3.0}/tests/plugins/__init__.py +0 -0
  249. {commkit-2.2.0 → commkit-2.3.0}/tests/recovery/test_bps.py +0 -0
  250. {commkit-2.2.0 → commkit-2.3.0}/tests/recovery/test_joint.py +0 -0
  251. {commkit-2.2.0 → commkit-2.3.0}/tests/recovery/test_pilots.py +0 -0
  252. {commkit-2.2.0 → commkit-2.3.0}/tests/recovery/test_pll.py +0 -0
  253. {commkit-2.2.0 → commkit-2.3.0}/tests/recovery/test_recovery_oracles.py +0 -0
  254. {commkit-2.2.0 → commkit-2.3.0}/tests/recovery/test_tikhonov.py +0 -0
  255. {commkit-2.2.0 → commkit-2.3.0}/tests/recovery/test_viterbi_viterbi.py +0 -0
  256. {commkit-2.2.0 → commkit-2.3.0}/tests/test_array.py +0 -0
  257. {commkit-2.2.0 → commkit-2.3.0}/tests/test_backend.py +0 -0
  258. {commkit-2.2.0 → commkit-2.3.0}/tests/test_dispersion.py +0 -0
  259. {commkit-2.2.0 → commkit-2.3.0}/tests/test_examples.py +0 -0
  260. {commkit-2.2.0 → commkit-2.3.0}/tests/test_import_hygiene.py +0 -0
  261. {commkit-2.2.0 → commkit-2.3.0}/tests/test_io.py +0 -0
  262. {commkit-2.2.0 → commkit-2.3.0}/tests/test_logger.py +0 -0
  263. {commkit-2.2.0 → commkit-2.3.0}/tests/test_metrics.py +0 -0
  264. {commkit-2.2.0 → commkit-2.3.0}/tests/test_multirate.py +0 -0
  265. {commkit-2.2.0 → commkit-2.3.0}/tests/test_pulse_shaping.py +0 -0
  266. {commkit-2.2.0 → commkit-2.3.0}/tests/test_signal_pipeline.py +0 -0
  267. {commkit-2.2.0 → commkit-2.3.0}/tests/test_spectral.py +0 -0
  268. {commkit-2.2.0 → commkit-2.3.0}/tests/test_timing.py +0 -0
@@ -110,12 +110,12 @@ jobs:
110
110
  - name: Run kernel tests with JIT disabled
111
111
  env:
112
112
  NUMBA_DISABLE_JIT: "1"
113
- run: uv run pytest tests/equalization tests/recovery tests/test_frequency.py tests/test_fiber.py --device=cpu --cov=commkit --cov-report=
113
+ run: uv run pytest tests/equalization tests/recovery tests/test_frequency.py --device=cpu --cov=commkit --cov-report=
114
114
 
115
115
  - name: Kernel coverage report
116
116
  run: >-
117
117
  uv run coverage report --show-missing --fail-under=85
118
- --include="commkit/equalization/_kernels_numba.py,commkit/equalization/sequential/*,commkit/frequency.py,commkit/recovery/*"
118
+ --include="commkit/equalization/_kernels_numba.py,commkit/equalization/_numba_loops.py,commkit/equalization/sequential/*,commkit/frequency.py,commkit/recovery/*"
119
119
 
120
120
  # GPU legs (--device=gpu / --device=all) require CuPy + CUDA and are run
121
121
  # manually or on a self-hosted runner; they are intentionally not part of
@@ -36,11 +36,28 @@ uv run pytest --device=cpu # what CI runs
36
36
  uv run pytest tests/equalization/ -k lms # subset
37
37
  uv run pytest --cov=commkit # coverage
38
38
  NUMBA_DISABLE_JIT=1 uv run pytest tests/equalization tests/recovery \
39
- tests/test_frequency.py tests/test_fiber.py --device=cpu --cov=commkit # real kernel coverage
39
+ tests/test_frequency.py --device=cpu --cov=commkit # real kernel coverage
40
40
  uv run pytest --device=cpu -p tests.plugins.precision_trace \
41
41
  --ignore=tests/test_examples.py # precision audit (see Numerics); the suite passes under it
42
+ uv run pytest --device=gpu -p tests.plugins.transfer_trace \
43
+ --ignore=tests/test_examples.py # transfer audit (see Performance); fails on an undocumented transfer
42
44
  ```
43
45
 
46
+ **CUDA sanitizer** (see Kernel policy): NVIDIA's `compute-sanitizer` checks the kernels for out-of-bounds and misaligned accesses (`memcheck`), shared-memory races (`racecheck`), barrier misuse (`synccheck`) and reads of uninitialized device memory (`initcheck`). It is not a dependency: install its wheel, matching the CUDA major version of `cupy-cuda13x`, into a scratch directory and run the GPU tests of the kernels under each tool (10-100x slower):
47
+
48
+ ```bash
49
+ uv pip install --target /tmp/sanitizer "nvidia-cuda-sanitizer-api==13.*"
50
+ for tool in memcheck racecheck synccheck initcheck; do
51
+ /tmp/sanitizer/nvidia/cu13/bin/compute-sanitizer --tool $tool --leak-check full \
52
+ .venv/bin/python -m pytest --device=gpu -p no:cacheprovider \
53
+ tests/equalization/test_cs_kernel.py tests/equalization/test_bps_kernel.py \
54
+ tests/equalization/test_block.py tests/equalization/test_blind.py tests/mapping/test_llr.py \
55
+ tests/test_filtering.py tests/test_fiber.py tests/impairments/channel/test_channel_nonlinear.py
56
+ done
57
+ ```
58
+
59
+ Under WSL2 the sanitizer needs the Windows GPU debugger interface; without it every run stops at "Failed to initialize WDDM debugger interface". Enable it once by running `EnableDebuggerInterface.bat` as a Windows administrator; the script comes with the Windows CUDA Toolkit, not with the Linux wheel.
60
+
44
61
  **Commit gate.** CI fails on any of these, so run all of them before every commit, with `ruff format` last:
45
62
 
46
63
  ```bash
@@ -61,16 +78,17 @@ Timed runs:
61
78
  ```bash
62
79
  uv run pytest benchmarks/bench_bps.py --benchmark-only --device=all
63
80
  uv run pytest benchmarks/ --benchmark-only --device=all \
64
- --benchmark-compare=0003 --benchmark-storage=file://benchmarks/baselines
81
+ --benchmark-compare=0006 --benchmark-storage=file://benchmarks/baselines
65
82
  uv run python benchmarks/record_baseline.py NAME # record a new baseline
66
83
  ```
67
84
 
68
85
  **Release:**
69
86
 
70
87
  1. On `main`: the CPU and GPU suites pass with no failures or skips (`uv run pytest -rs`), `uv build` succeeds and `uv run --with twine twine check dist/*` passes.
71
- 2. `uv run bump-my-version bump {patch|minor|major}`. This commits the new version and tags it `vX.Y.Z`.
72
- 3. `git push origin main --tags`.
73
- 4. Publish a GitHub Release for the tag (`gh release create vX.Y.Z --generate-notes`). Publishing the release runs `.github/workflows/publish.yml`, which builds and uploads to PyPI through Trusted Publishing; pushing a tag alone publishes nothing.
88
+ 2. `CHANGELOG.md`: a `## [X.Y.Z] - YYYY-MM-DD` section for the release, written for users. Changes that may need code updates come first, with how to migrate, then Added, Performance (measured, as a ratio of new to old time), Fixed and Development. Add its compare link at the bottom, and commit it.
89
+ 3. `uv run bump-my-version bump {patch|minor|major}`. This commits the new version and tags it `vX.Y.Z`.
90
+ 4. `git push origin main --tags`. The ruleset on `main` requires pull requests; repository admins bypass it for these two commits.
91
+ 5. Publish a GitHub Release for the tag, with the release's changelog section as its notes (`gh release create vX.Y.Z --title vX.Y.Z --notes-file notes.md`). Publishing the release runs `.github/workflows/publish.yml`, which builds and uploads to PyPI through Trusted Publishing; pushing a tag alone publishes nothing.
74
92
 
75
93
  ---
76
94
 
@@ -160,9 +178,23 @@ Dependencies point downward only:
160
178
  - `num_taps`, `step_size`, `sps`, `constellation`, `pulse`, `rng`, `num_symbols`, `num_channels`, `sampling_rate`, `symbol_rate`.
161
179
  - Units are in the docstring. Use `_db` and `_hz` suffixes where the unit is ambiguous.
162
180
 
181
+ ### Docstrings
182
+
183
+ Docstrings describe the code as it is; a change in behavior updates them in the same commit. NumPy format (numpydoc), sections in its order: Parameters, Returns, Raises, Warns, See Also, Notes, References, Examples.
184
+
185
+ - **Summary line** on the line of the opening quotes (`"""Summary.`), one line, ending with a period.
186
+ - Functions: imperative mood (`Apply ...`, `Estimate ...`, `Plot ...`), or a noun phrase for a function named after the quantity it returns (`evm`, `allan_deviation`, `welch_psd`).
187
+ - Classes and modules: a noun phrase.
188
+ - **Module docstrings** say what the module holds, by public name (`:func:`, `:class:`), then the conventions particular to it (a sign convention, a scaling, a file layout). No marketing adjectives ("high-performance", "publication-quality"), no history of earlier layouts, and no restatement of the library-wide rules in this file. A private module's summary ends with "(private)" and the body names its users.
189
+ - **Types:** inputs are `array_like` (NumPy or CuPy), `array_like or Signal` for Signal-aware arguments; returned arrays and result fields are `ndarray` (NumPy or CuPy, on the input's device), `ndarray or Signal` for transforms, and `numpy.ndarray` where the array is always on the host. Shapes go in the description, in double backticks: ``(N,)``, ``(C, N)``.
190
+ - **Plots** return `(fig, ax) or None` (`None` with `show=True`).
191
+ - **Defaults:** `name : type, default value`; `name : type, optional` when the default is `None`, with the description saying what `None` does.
192
+ - Units go in the description; inline code in double backticks; references to library names through `:func:` / `:class:` / `:mod:` / `:meth:`.
193
+
163
194
  ### Errors and logging
164
195
 
165
196
  - Wrong input raises, with a message naming the argument.
197
+ - **No fallbacks for a broken environment.** Code never catches a failure of CuPy, a CUDA kernel (compile, load, launch), a CUDA-graph capture or any other dependency in order to run a slower or different implementation; the failure raises to the caller. The kernels and graphs are tested on a GPU before every release (§2), a broken CuPy already fails when data moves to the GPU, and a path kept "just in case" is code that no test or user runs. Dispatch on the *input* is different and allowed: a kernel's documented limits (dtype, number of points, phases or channels) may route to the general implementation where that implementation exists anyway (the CPU's), and the dispatch says so where it happens. A GPU-only path that would serve only inputs beyond a kernel's limit is not written; such input raises (`compute_llr` above 16 bits per symbol).
166
198
  - `logger` is for diagnostics only. Per-channel logging that needs a device-to-host transfer is wrapped in `if logger.isEnabledFor(logging.INFO):`.
167
199
  - The library never configures handlers, Matplotlib or warning filters at import time.
168
200
 
@@ -174,12 +206,12 @@ Dependencies point downward only:
174
206
  - **Precision contract.** Every function computes in a predictable precision, and none promotes single-precision data on its own:
175
207
  - **Transforms keep the input's precision**: complex64 in, complex64 out and computed throughout (filters, impairments, the fiber solver and DBP, resampling, delays, carrier-phase correction); complex128 in, complex128 out.
176
208
  - **Fixed-precision blocks** compute in single precision whatever the input: the equalizers in complex64 (with complex128 accumulation inside, below), `compute_llr` / `Constellation.llr` in float32. Double input to them emits `commkit.PrecisionWarning` (`_array.warn_if_double`, attributed to the caller's line; Python shows it once per call site). Users who want strictness turn it into an error with `warnings.simplefilter("error", commkit.PrecisionWarning)`; the test suite does (`pytest_configure` in `tests/conftest.py`; never in pyproject's `filterwarnings`, which imports commkit before coverage starts and drops every module-level line from the report), so a library path that feeds double data to these blocks fails CI. Training and reference symbols are ideal values and are not checked.
177
- - **Deliberate float64**, documented where it happens, always on phases, small arrays or accumulators, never on sample-rate data that is then cast back: phases before `unwrap`, phase trajectories and phase ramps (wrapped in float64, rotated in the data's precision), frequency grids and estimates, the recursions' state (PLL, Tikhonov, RLS `P`), the IIR filter's sections (poles near z = 1), and sums: the metrics compute per element in the input's precision and accumulate in float64 (`gmi` / `mi` in single precision moved by under 4e-8 bit, far below the Monte Carlo error).
209
+ - **Deliberate float64**, documented where it happens, always on phases, small arrays or accumulators, never on sample-rate data that is then cast back: phases before `unwrap`, phase trajectories and phase ramps (wrapped in float64, rotated in the data's precision), frequency grids and estimates, the recursions' state (PLL, Tikhonov, RLS `P`), the IIR filter's sections and state (poles near z = 1; the samples keep their precision on both devices), and sums: the metrics compute per element in the input's precision and accumulate in float64 (`gmi` / `mi` in single precision moved by under 4e-8 bit, far below the Monte Carlo error).
178
210
  - **Audit with `tests/plugins/precision_trace.py`** after touching numerics: it lists every float64/complex128 array of sample-rate size that a NumPy path of the library creates from single-precision data or from scratch, with its source line. Every reported site must be a deliberate use from the list above (or a test passing complex128 on purpose). Output dtypes alone do not show this: a function that computes in complex128 and casts back returns complex64.
179
211
  - complex128 pays only where single-precision rounding accumulates over many FFT passes, as in the fiber solver (6e-4 after 10^4 steps in complex64, 1e-12 in complex128); there the GPU runs its plain CuPy stages, not the fused complex64 kernels.
180
212
  - **The NEP 50 trap**: under NumPy 2 a NumPy scalar is not weak, so `np.float64(a) * x64` (any `np.sqrt`, `np.pi * ...` result, or a float64 array such as `xp.fft.fftfreq`) promotes complex64 data to complex128. Combine single-precision data only with Python scalars or with arrays cast to its precision; rotate with `math._derotate` (float64 phase wrap, single-precision phasor, one fused kernel on the GPU).
181
213
  - **Accumulation precision:**
182
- - LMS/CMA/RDE: inputs and weights are `complex64`, but dot products and gradient updates in the hot loop are accumulated in `complex128`.
214
+ - LMS/CMA/RDE: inputs and weights are `complex64`, but dot products and gradient updates in the hot loop are accumulated in `complex128`. The block equalizers compute in `complex64` throughout: their sums over the taps run inside the FFTs, and the sum over the C input channels between them is as short as C.
183
215
  - RLS keeps `P`, the gain `k` and the regressor buffers in `complex128` throughout. Single precision loses the Hermitian positive-definite property and the filter diverges.
184
216
  - Carrier phase: promote angles to `float64` before `xp.unwrap()`, because float32 rounding causes spurious quadrant slips. Tikhonov/Kalman block transitions also run in `float64`.
185
217
  - **Normalization invariant:** at `sps` samples per symbol, `E[|x|²] = 1/sps`; at symbol rate, `E[|x|²] = 1`. Any rate-changing block applies the exact gain correction.
@@ -192,10 +224,12 @@ Dependencies point downward only:
192
224
 
193
225
  | Algorithm structure | Implementation | Device |
194
226
  | --- | --- | --- |
195
- | Vectorized: filters, resampling, spectra, impairments, metrics, LLR, frequency-domain equalizers | NumPy/CuPy via `x, xp, sp = dispatch(samples)` | follows the input |
196
- | Sequential recursions: LMS/RLS/CMA/RDE, PLL, Tikhonov, cycle slips | Numba `@njit(cache=True, fastmath=True, nogil=True)` | CPU; GPU input does one round trip to the host and back |
227
+ | Vectorized: filters, resampling, spectra, impairments, metrics, frequency-domain equalizers | NumPy/CuPy via `x, xp, sp = dispatch(samples)` | follows the input |
228
+ | Per-symbol searches over a small table: LLRs (over all points, or per axis for separable QAM; max-log without a prior in closed form, from the nearest levels of each bit's two values), hard decisions of constellations without a square lattice | CPU: Numba `parallel=True` over blocks of symbols (LLRs; the closed form from 16 levels per axis, below which the level metrics are as fast), chunked NumPy (decisions). GPU: CUDA kernels, one thread per symbol (`llr`, `llr_separable`, `llr_separable_maxlog` at every order, `bps_min_d2`'s nearest-point mode); decisions on double input or more than 1024 points run the chunked search on both devices, LLRs above 16 bits per symbol raise on the GPU | follows the input |
229
+ | Sequential recursions: LMS/RLS/CMA/RDE, PLL, Tikhonov, cycle slips, the Mengali-Morelli bootstrap over the lags | Numba `@njit(cache=True, fastmath=True, nogil=True)`; independent rows in parallel (`prange`: the bootstrap's blocks). The equalizers' kernel with inline carrier recovery compiles strict (its unwrap and running sums) and calls its tap and distance loops as separate fast-math functions, which keep their flags when inlined (an `inline="always"` helper takes its caller's) | CPU; GPU input does one round trip to the host and back |
230
+ | Linear recursions: IIR filtering | CPU: Numba kernel without `fastmath`, so double input gives SciPy's result bit for bit. GPU: CUDA kernel (`iir`), blocks filtered in parallel and the state carried across them in two levels, in double precision | follows the input |
197
231
  | Hot spots limited by intermediates, launches or round trips | CUDA C++ via CuPy `RawModule` (`commkit/_cuda`) | GPU |
198
- | Elementwise chains with many temporaries, where a paired A/B shows the gain end to end (the PMD stage of `_fiber`: 0.61x on 4 cores, but not on the reference machine; see the roadmap) | Numba `@njit(parallel=True)` in one pass, real arithmetic (a `1j` literal promotes complex64 to complex128) | CPU |
232
+ | Elementwise chains between multi-core FFTs (the fiber solver's Kerr and PMD stages) | NumPy: a Numba `parallel=True` pass there competes with `scipy.fft`'s threads (the solver's Numba passes measured 1.1-1.8x slower end to end, 2^14-2^20); adopt one only on a paired end-to-end A/B | CPU |
199
233
 
200
234
  **Host-sync hygiene:**
201
235
 
@@ -205,18 +239,22 @@ Dependencies point downward only:
205
239
  - Frequency responses (dispersion, delays, Jones matrices) are computed at float64 phase accuracy and cast to the spectrum's precision before the multiply, so complex64 data is never promoted.
206
240
  - Chunk `(N, M)` intermediates over N with an on-device accumulator.
207
241
  - Reduce on the device before transferring, including for plotting.
242
+ - Build grids, windows and index tables on the data's device (`xp.fft.fftfreq`, `xp.hanning`, `xp.arange`), not on the host and then upload them.
243
+ - **Audit with `tests/plugins/transfer_trace.py`** after touching a GPU path. The contract registry checks only that a result comes back on the input's device, which a silent round trip through the host passes. The plugin records every host-device copy of sample-rate size that commkit makes during a `--device=gpu` run, with its function and lines, and fails on a site missing from its documented list. The documented sites are the round trip of the sequential recursions (table above), the equalizers' continuation state (host copies), the reporting layer (metrics, analysis summaries, plotting), explicit device moves and the transmitted payload, and host values (reference symbols, pilots, tables from host parameters such as a SciPy window) moved to the data's device. A new site is either fixed or added to the list with its reason.
208
244
  - Pass whole records to compiled code. There are no Python loops over symbols or small blocks, except inside a CUDA graph.
209
- - Repeated GPU work with fixed buffers (the block equalizers' blocks, the fiber solver's spans) is replayed from CUDA graphs through `_cuda.graphs.GraphRunner`, the one driver: warm once, capture on the second run, replay, fall back to eager on a failed capture. Its stream is reused per device and thread, because CuPy's pool caches freed blocks per stream and a fresh stream per call grows the pool every call.
245
+ - Repeated GPU work with fixed buffers (the block equalizers' blocks, the fiber solver's spans) is replayed from CUDA graphs through `_cuda.graphs.GraphRunner`, the one driver: warm once, capture on the second run, replay; a failed capture raises. Per-replay offsets come from a device index inside the graph (the block equalizers' window, staging and stores read the block index and advance it), so a replay does all of a block's work and the host launches one graph per block. Its stream is reused per device and thread, because CuPy's pool caches freed blocks per stream and a fresh stream per call grows the pool every call.
210
246
 
211
247
  **Kernel policy:**
212
248
 
213
249
  - A `.cu` kernel needs a benchmark showing that CuPy is limited by intermediates, launch count or round trips.
250
+ - `_cuda.get_kernel` returns the launch wrapper or raises (compile, load or a bad specialization); it is called only on CuPy input.
214
251
  - It optimizes a CPU reference (Numba or NumPy) and is tested against it with `--device=all`.
215
- - Wrappers check dtype, contiguity, shape and size before launch.
252
+ - Wrappers check dtype, contiguity, shape and size before launch, and never launch a zero-size grid (a launch error): empty input returns an empty result, or raises where the kernel's caller never has one (`cs_block`, `bps_anchor`). Grid dimensions y and z are limited to 65535: a kernel that maps channels or rows there walks the rest in a grid-stride loop (`bps_min_d2`).
253
+ - A new or changed kernel runs its GPU tests under `compute-sanitizer` (§2) before release.
216
254
  - `float32` with explicit literals.
217
255
  - `--use_fast_math` only per kernel (through `options`), after an accuracy check; the compiler default does not pass it.
218
256
  - Short elementwise chains use `ElementwiseKernel` or `cupy.fuse`.
219
- - Sequential recursions over a whole record are never ported to the GPU; one record has no parallel work per step. A short per-block scan may be, as one launch, when it replaces a host round trip per block (`cs_block`, `bps_anchor`); keep its parallel part parallel (float64 is 1/64 of float32 throughput on consumer GPUs).
257
+ - Sequential recursions over a whole record are never ported to the GPU; one record has no parallel work per step. Linear recursions (IIR filters) are the exception: the `iir` kernel finds each block's state from a zero start in parallel, carries the states across blocks (per group of blocks, then across groups), and filters the blocks again from them. A short per-block scan may be, as one launch, when it replaces a host round trip per block (`cs_block`, which speculates its slip decisions in parallel, one pass per changed decision, about one per block on a realistic link; `bps_anchor`); keep its parallel part parallel (float64 is 1/64 of float32 throughput on consumer GPUs).
220
258
 
221
259
  ---
222
260
 
@@ -227,11 +265,11 @@ Dependencies point downward only:
227
265
  - **Shared builders** live in `tests/common/` and use the public API.
228
266
  - **Contract registry** (`tests/test_api_contracts.py`): every public function has a row. Generic checks run per row: array in/out, Signal in/out, device preserved, input not mutated, keyword-only parameters, `TypeError` on unsupported arrays, fact-conflict errors, return rules.
229
267
  - Do not duplicate these checks in per-module tests.
230
- - **Oracles:** sequential kernels are tested against plain-Python reference implementations in `tests/common/reference_impl.py`. CUDA kernels are tested against their CPU reference.
268
+ - **Oracles:** sequential kernels are tested against plain-Python reference implementations in `tests/common/reference_impl.py`. CUDA kernels are tested against their CPU reference. Kernel tests skip only on the CPU parametrization (`skip_unless_gpu`), never because a kernel is unavailable, so a kernel that fails to compile fails its tests.
231
269
  - **Mathematical meaning:** test documented definitions, such as a pulse width, a dispersion sign, or noise-variance scaling, with independently derived expectations, not only shapes and energies.
232
270
  - **API changes never change expected values.** A commit that changes a signature only changes how tests call the code. Numerical changes land in their own commit with independent validation.
233
271
  - **Examples** are Jupyter notebooks in `examples/`, committed without outputs: `.gitattributes` routes them through the `nbstripout` filter, which each clone enables once with `uv run nbstripout --install` (git does not ship filter configuration). `tests/test_examples.py` executes each one, so an API change updates the notebooks in the same commit.
234
- - They form a course: one topic per notebook, named `NN-lowercase-words.ipynb`, numbered in teaching order (signals and DSP, channel and metrics, receiver algorithms, fiber systems, characterization). Each opens with what it covers, links the notebooks it builds on and ends with a link to the next. `examples/README.md` indexes them; a new notebook takes its place in the order and updates the index and the neighbours' links.
272
+ - They form a course: one topic per notebook, named `NN-lowercase-words.ipynb`, numbered in teaching order (signals and DSP, channel and metrics, receiver algorithms, fiber systems, characterization). Each opens with what it covers, links the notebooks it builds on and ends with a link to the next. `examples/README.md` indexes them; a new notebook takes its place in the order and updates the index and the neighbors' links.
235
273
  - Every claim in the text matches what the cells print: run the notebook and read its numbers before committing it. Keep each under about 20 s on the CPU.
236
274
  - Plots of hand-computed arrays go through `to_device(x, "cpu")`, so the notebooks run unchanged on the GPU.
237
275
  - **Coverage** must not drop below the CI floor. Numba kernel coverage is measured by a separate `NUMBA_DISABLE_JIT=1` job.
@@ -240,10 +278,10 @@ Dependencies point downward only:
240
278
 
241
279
  ## 8. Benchmarks
242
280
 
243
- `benchmarks/` tracks the GPU-relevant hot paths. Baselines are committed under `benchmarks/baselines/`. The current reference is `0004` (`fiber_dbp`) on the reference machine: RTX 4070 Ti, Ryzen 7 7800X3D, WSL2. It adds the fiber and DBP benchmarks to `0003` (`v2_0`, the 2.0 state); `0002` (`pre_v2`) is the 1.x state. `0002` ran the CPR equalizer and Viterbi-Viterbi benchmarks on workloads that did not converge, so compare those only from `0003` on.
281
+ `benchmarks/` tracks the GPU-relevant hot paths. Baselines are committed under `benchmarks/baselines/`. The current reference is `0006` (`perf_cleanup`) on the reference machine: RTX 4070 Ti, Ryzen 7 7800X3D, WSL2. It is the first baseline with `bench_filters.py`, `bench_pll`, `bench_mengali_morelli_blockwise` and `bench_correct_iq_imbalance`, recorded after the performance pass of the IIR kernel and the block equalizers' in-graph block copies, best of five passes; `0005` (`gpu_decoder_path`) is the first with `bench_mapping.py` and `bench_compute_llr_nonseparable`, recorded after the GPU LLR, mapping and decision kernels; `0004` (`fiber_dbp`) adds the fiber and DBP benchmarks to `0003` (`v2_0`, the 2.0 state); `0002` (`pre_v2`) is the 1.x state. `0002` ran the CPR equalizer and Viterbi-Viterbi benchmarks on workloads that did not converge, so compare those only from `0003` on.
244
282
 
245
283
  - **Workloads must converge.** A workload the algorithm cannot handle times a failure mode (slip storms, divergence), not the operating point. Every equalizer benchmark asserts its symbol error rate with `benchutils.assert_converged`.
246
- - **Baselines** are recorded with `benchmarks/record_baseline.py`: each file in its own process, best of three passes. In one full-suite process, small GPU benchmarks after the large equalizer workloads ran 2-12x slower than alone, so never record a baseline from a single `pytest benchmarks/` run.
284
+ - **Baselines** are recorded with `benchmarks/record_baseline.py`: each file in its own process, best of three passes (`--passes`; `0006` used five, as three still kept outliers of 2-4x on a few small benchmarks). In one full-suite process, small GPU benchmarks after the large equalizer workloads ran 2-12x slower than alone, so never record a baseline from a single `pytest benchmarks/` run.
247
285
 
248
286
  - **IDs** are `[cpu]` or `[gpu]`, meaning the input device.
249
287
  - **Timing:**
@@ -253,7 +291,7 @@ Dependencies point downward only:
253
291
  - **Tools:** `benchmarks/benchutils.py` provides `CudaEventTimer` and `nvtx_range` for `nsys` profiling.
254
292
  - **Logging** is set to WARNING in `benchmarks/conftest.py`.
255
293
  - **Trust deltas, not single runs.** A ±20-40% swing has been seen under load. Re-run a suspected regression in isolation, or do a controlled A/B with at least 7 repetitions, alternating old and new per repetition and comparing the paired ratios. When even unchanged benchmarks move, time the changed part itself with `CudaEventTimer` and quote it against the step it sits in.
256
- - A commit touching a hot path quotes its delta against `0004`. Tolerance is 5% on CPU and 10% on GPU.
294
+ - A commit touching a hot path quotes its delta against `0006`. Tolerance is 5% on CPU and 10% on GPU.
257
295
 
258
296
  ---
259
297
 
@@ -261,9 +299,41 @@ Dependencies point downward only:
261
299
 
262
300
  What is not done yet, in suggested order. Unlike the sections above, this is a to-do list, not a description of the code: remove an item in the commit that resolves it, and add one when a known gap is left open.
263
301
 
302
+ ### Switch to Python 3.15 (blocked on CuPy)
303
+
304
+ Goal: require Python 3.15 and nothing older, mainly for explicit lazy imports (PEP 810, the `lazy` keyword). State checked on 2026-10-23 with uv 0.13.5 and CPython 3.15.1:
305
+
306
+ - **Blocker: CuPy has no 3.15 wheels.** `cupy-cuda13x` 14.2.0 ships cp310-cp314 only, on PyPI and on `pip.cupy.dev/pre`. Support is tracked in cupy/cupy#10259, milestone v14.3.0 (unreleased; the 14.3.0.dev0 docs already list 3.15). Switching before then would leave `commkit[gpu]` uninstallable and the GPU suite unrunnable.
307
+ - **Second blocker: `jupyter_client` fails to import on 3.15.** `jupyter_client/jsonutil.py` (line 32) calls `strptime` with a day of month and no year, which 3.15 rejects (cpython#70647). `tests/test_examples.py` then fails at collection, and the notebooks cannot run. Wait for a fixed release; do not work around it in commkit.
308
+ - **Ready:** wheel-only installs on 3.15 work for NumPy 2.5.4, SciPy 1.18.1, Numba 0.68.0 (llvmlite 0.50.0), Matplotlib 3.11.2, mypy 2.4.0, ruff, pytest, pytest-cov, pytest-benchmark, nbclient, ipykernel, nbstripout and bump-my-version. The CPU suite has not been run on 3.15 beyond the collection error above.
309
+
310
+ Steps, once CuPy 14.3 (with cp315 wheels for `cupy-cuda13x`) and a fixed `jupyter_client` are out. Keep the commits separate: the version switch alone first, then each refactor, so a failure points at one change.
311
+
312
+ 1. **Validate before changing anything**: in a 3.15 venv, install the current code and run the full CPU+GPU suite (`-rs`, notebooks included), the benchmark smoke run, the `NUMBA_DISABLE_JIT` job and the precision audit.
313
+ 2. **Switch the version floor**:
314
+ - `pyproject.toml`: `requires-python = ">=3.15"`, classifiers 3.15 only, ruff `target-version = "py315"` (if ruff supports it), raise the dependency minimums to the first releases with 3.15 wheels (`cupy-cuda13x[ctk]>=14.3`, `numba>=0.68`, `numpy>=2.5`, `scipy>=1.18`; recheck each).
315
+ - `ci.yml`: test matrix `python-version: ["3.15"]` (keep both `locked` and `lowest-direct`); the static-analysis, kernel-coverage and notebook jobs on 3.15. `publish.yml`: build on 3.15.
316
+ - `uv lock`; README install requirements; §2 here ("Python 3.12+").
317
+ - Benchmarks: record a new baseline on 3.15 (it lands in `benchmarks/baselines/Linux-CPython-3.15-64bit/`), compare it with `0006` by a paired A/B of 3.14 against 3.15 on unchanged code, and make it the reference.
318
+ 3. **Lazy imports (PEP 810)**. Confirm the rules on the interpreter first. As specified, `lazy` is allowed only for module-level imports: not inside functions, class bodies or `try` blocks, and not with `*`.
319
+ - About 60 function-local imports exist to keep `import commkit` light or optional: `import cupy as cp` (11 sites), `import numba` (9), `from . import compiler` in `_cuda` (5), and local imports that break cycles (`Constellation`, `Signal`, `normalize`). Move each to a module-level `lazy import` / `lazy from`. Concentrated in `_cuda/__init__.py`, `backend.py`, `io.py`, `recovery/`, `core/`, `plotting/`.
320
+ - CuPy stays optional: a lazy import of a missing module fails only when used, so every use must stay behind the existing availability checks (`is_cupy_available`, `dispatch`). `backend.py`'s probing needs care: decide per site whether the probe imports eagerly inside `try`.
321
+ - Numba: `@njit` at module level compiles lazily already; check that a lazy `numba` import does not make every first call pay the import at an odd moment (time with `benchmarks/bench_import.py`).
322
+ - The six `TYPE_CHECKING` blocks can become `lazy from` imports where the name is also useful at run time; keep them where it is not.
323
+ - Measure `import commkit` before and after with `bench_import.py`, and keep `tests/test_import_hygiene.py` passing; extend it to assert that `import commkit` loads neither CuPy nor Matplotlib, if lazy imports make that true.
324
+ 4. **Other cleanups that the 3.15 floor allows** (each its own commit; verify each against What's New in 3.14 and 3.15):
325
+ - Drop `from __future__ import annotations` (25 modules): annotations are evaluated lazily by default since 3.14 (PEP 649/749). Check that nothing reads `__annotations__` as strings.
326
+ - UTF-8 mode is the default from 3.15 (PEP 686): check `io` and any `open()` without `encoding=`.
327
+ - `strptime` with a day and no year now raises (the `jupyter_client` failure above); grep commkit for date parsing.
328
+ - The 3.15 sampling profiler (PEP 799) is a candidate tool for the CPU hot paths next to `nsys`; mention it in §8 if it proves useful.
329
+
330
+ ### CUDA sanitizer pass (blocked on the WDDM debugger interface)
331
+
332
+ The kernels have not run under `compute-sanitizer` (§2): on the reference machine (WSL2) it needs the Windows GPU debugger interface, which is not enabled yet. The review that stands in for it (every `.cu` kernel and wrapper read for bounds, barriers, races and overflow; empty, one-sample and NaN/Inf inputs; 4 KB guard zones around every array the library allocated during the GPU kernel tests, 565 tests and 57,723 arrays, none touched) catches out-of-bounds writes but not out-of-bounds reads or reads of uninitialized memory. Once the interface is enabled, run the four tools of §2, fix what they report, and remove this item.
333
+
264
334
  ### Planned modules
265
335
 
266
- 1. **Channel coding** (`coding`, a placeholder package). Resolve the "Before channel coding" items below first, so the decoder is built on soft demapping that is already at its final speed on both devices. Start with one soft decoder (LDPC) that consumes `compute_llr` output (positive LLR = bit 0) and reports post-FEC BER next to `gmi`, then add encoders. The package stays out of `commkit/__init__.py` until a real encode/decode entry point exists. Decoders are iterative message passing over many codewords: batch codewords on the GPU from the start.
336
+ 1. **Channel coding** (`coding`, a placeholder package). Soft demapping is GPU-validated on both devices. Start with one soft decoder (LDPC) that consumes `compute_llr` output (positive LLR = bit 0) and reports post-FEC BER next to `gmi`, then add encoders. The package stays out of `commkit/__init__.py` until a real encode/decode entry point exists. Decoders are iterative message passing over many codewords: batch codewords on the GPU from the start.
267
337
 
268
338
  ### Nonlinear fiber channel: future features
269
339
 
@@ -275,21 +345,3 @@ Each is an addition that leaves the current API unchanged.
275
345
  - **Distributed (Raman) amplification and per-span fiber types** in one call (chaining calls already works: every call speaks launch-relative units).
276
346
  - **cuFFT callbacks** fusing the linear stage into the transforms (CuPy's support is experimental and compiles with nvcc at run time).
277
347
  - **Self-steepening and Raman scattering** for ultra-wideband fields.
278
-
279
- ### Before channel coding: mapping and soft demapping
280
-
281
- The decoder's input path. The GPU items need a CUDA device to write, test (`--device=all`, against the Numba reference) and time.
282
-
283
- - **GPU LLRs of separable QAM.** `Constellation.llr` takes the per-axis path (2·sqrt(M) metrics instead of M) only on the CPU; on the GPU, 256-QAM evaluates 256 metrics per symbol instead of 32. Port `llr_separable` to a CUDA kernel.
284
- - **One exponential per point in the CUDA exact LLR.** `llr.cu` evaluates every metric twice and calls `expf` once per point and bit (M·k per symbol, 2048 for 256-QAM). The Numba kernel scales by one peak per symbol and needs M (with the underflow fallback of `_EXACT_FLOOR`). Together with the item above: 32 exponentials per 256-QAM symbol instead of 2048.
285
- - **CUDA LLR stores and per-call tables.** Each thread writes its k LLRs at stride k (uncoalesced), and every call rebuilds and copies the points, packed labels and log prior to the device; cache them per constellation and device.
286
- - **Non-square hard decisions on the GPU.** `gray._nearest_index` (used by `Constellation.demap`, `evm(blind=True)` and `ser` for PSK, cross QAM and arbitrary points) loops in Python over 4096-symbol chunks: about 250 iterations of several launches per 10^6 symbols. On the CPU that chunk and `abs` are the fastest NumPy form (measured); on the GPU, time it against a larger element-budget chunk or the nearest-point mode of `bps_min_d2`.
287
- - **`Constellation.map` intermediates.** It casts the bits to int32 groups and multiplies and sums them: two `(N, k)` int32 temporaries for `N·k` input bits, four times the bits' own size in int8. Encoders hand it long bit records; pack the labels in one pass.
288
- - **Closed-form max-log LLRs of Gray QAM** (optional, CPU and GPU): per bit, the nearest point of each half is found by snapping, O(k) instead of O(sqrt(M)). Expected gain under 2x on the CPU; measure before adopting.
289
-
290
- ### Performance
291
-
292
- - **Block equalizers at small `block_size` on the GPU**: a fixed cost per block; at 256 the GPU only ties the CPU without carrier recovery.
293
- - **The fiber solver's Numba passes lose end to end on the reference machine.** On 4 cores (where they were tuned) the hybrid Kerr stage won from `_fiber._HYBRID_KERR_MIN = 2^18` and the trig-free Jones (PMD) pass won end to end (0.61x). On the reference machine (8 cores, 16 threads), whole propagations with NumPy stages and multi-core `scipy.fft` are faster: hybrid Kerr 1.74x slower at 2^18, 1.32x at 2^19, 1.12x at 2^20; Numba Jones 1.35x at 2^14, 2.19x at 2^16, 1.54x at 2^18, even at 2^20. The cause is contention: Numba's OpenMP workers spin after each pass and take the cores from the FFT workers (`OMP_WAIT_POLICY=passive` cuts the Kerr loss at 2^18 to 1.09x; one Numba thread makes Jones even with NumPy). The stage timed alone hides this. Options: choose the passes by core count, run them single-threaded, or keep NumPy stages only; decide by end-to-end A/Bs on both machines.
294
- - **A fully fused Numba Kerr stage** may beat the hybrid one where Numba has SVML (vector sin/cos); neither machine measured so far has it (`numba.core.config.USING_SVML` is False), so it is untimed.
295
- - **`cs_block` on phases with frequent slip decisions**: one speculation pass per changed decision (about 1 pass per block on a realistic link, more on pathological input). Outside a CUDA graph the Python wrapper (validation, scratch allocation) costs about 18 us per launch.
@@ -0,0 +1,116 @@
1
+ # Changelog
2
+
3
+ Notable changes to CommKit, newest first. Performance figures are from the reference machine (RTX 4070 Ti, Ryzen 7 7800X3D, WSL2) unless stated otherwise, as a ratio of new to old time (below 1 is faster).
4
+
5
+ ## [2.3.0] - 2026-10-28
6
+
7
+ A performance and robustness release. GPU block equalizers and the CPU decision-directed equalizers with inline carrier recovery run substantially faster. `iir_filter` keeps single precision on both devices. Max-log LLRs of uniform square QAM have a closed form. The CUDA code was reviewed end to end. A few changes need code updates; see the first section.
8
+
9
+ ### Changes that may need code updates
10
+
11
+ - **`build_pilot_ref`** takes `num_symbols=` and `num_channels=` instead of `n_sym=` and `num_ch=`, the library's glossary names. Rename the keywords at call sites.
12
+ - **`EqualizerResult` is a frozen dataclass**, like every other multi-value result. Assigning to a field raises; build a modified copy with `dataclasses.replace(result, field=value)`.
13
+ - **GPU failures raise.** A CUDA kernel that fails to compile or load, or a CUDA-graph capture that fails, used to log one warning and fall back to a slower path. It now raises, because it means a broken install or a bug.
14
+ - **`SingleCarrierFrame` validates its pilots on construction.** `pilot_pattern="block"` without `pilot_block_len` raises; it used to silently build a frame with no pilots. A pilot period that does not tile the body also raises at construction instead of on first use.
15
+ - **`moving_average(mode="valid")`** raises on a record shorter than the window, instead of returning NumPy's swapped convolution.
16
+ - **Numerical results move at rounding level** in a few places, from reordered or fused arithmetic. Seeds reproduce results within one version only. The largest moves:
17
+ - fiber propagation on the CPU: about 3e-6 relative;
18
+ - `lms` with inline carrier recovery: up to 5e-6 relative;
19
+ - `correct_iq_imbalance`: 5e-7 relative in complex64;
20
+ - `rde`: 2.5e-7 relative.
21
+
22
+ Everything else changed in this release is bit-identical or within float32 rounding of before.
23
+
24
+ ### Added
25
+
26
+ - **Single-precision `iir_filter` on both devices.**
27
+ - CPU: a Numba kernel that reads and writes the samples in their own dtype and keeps the sections and the state in double precision. Double input still matches SciPy bit for bit.
28
+ - GPU: a block-parallel CUDA kernel. Blocks are filtered in parallel and the state is carried across them in two levels, in double precision.
29
+ - There is no longer a double-precision copy of the record: complex64 is 0.32–0.41x of the previous GPU path.
30
+ - **Closed-form max-log LLRs** for uniform separable QAM, `compute_llr`'s default case. Each bit's LLR comes from the nearest level of each bit value, so an axis costs 1 + 2k metrics instead of sqrt(M). It works for any per-axis labeling, not only Gray, and gives the same values as before: bit for bit on the CPU, within 3e-7 on the GPU.
31
+ - GPU, every order: 0.87x at 16-QAM, 0.74x at 256-QAM.
32
+ - CPU, from 256-QAM (where it starts to pay): 0.65x at 256-QAM, 0.40x at 1024-QAM, 0.23x at 4096-QAM.
33
+ - Exact LLRs and shaped constellations keep the per-level computation.
34
+ - `TimingEstimate` is listed in `commkit.timing.__all__`, like the other estimate classes.
35
+ - `pad_mode="reflect"` is documented for the equalizers and `apply_taps`; they already accepted it.
36
+
37
+ ### Performance
38
+
39
+ Equalizers:
40
+
41
+ - **Block equalizers on the GPU:** each block's window load, training staging and output stores now run inside the replayed CUDA graph, so the host launches one graph per block. Baseline `0006` against `0005`:
42
+ - `block_lms` at `block_size=256`: 0.36x without carrier recovery, 0.78x with BPS, 0.65x with BPS and cycle slips;
43
+ - `block_cma`: 0.50x;
44
+ - `block_rde`: 0.66x.
45
+
46
+ At 256 symbols per block the GPU now runs `block_lms` about 2x faster than the CPU without carrier recovery, and 5x faster with BPS.
47
+ - **`block_lms` per block:** one phasor per block and one divergence check after the loop. The cycle-slip kernel binds its buffers once per run, cutting the per-block cost from 17 µs to 10–14 µs. On the GPU, cycle-slip correction runs for any channel count, where above 1024 channels it went through the host every block.
48
+ - **`lms` and `rls` with inline carrier recovery (CPU):** the tap and distance loops compile with fast math while the phase unwrap stays strict. RLS with BPS 0.63x, LMS with PLL 0.87x.
49
+ - **`rde` and `block_rde`:** the ring is decided by squared midpoints, with no square root. On the GPU this is one fused kernel: 112 µs → 10 µs per 2 × 1024-symbol block.
50
+
51
+ Carrier and frequency recovery:
52
+
53
+ - **BPS of square QAM on the CPU:** one Numba pass instead of a chunked NumPy loop: 0.39x.
54
+ - **PLL:** reads the samples in their own precision, with no complex128 copy of the record: 0.95–0.97x.
55
+ - **Blockwise `MengaliMorelli`:** one Numba call for all blocks instead of one per block. CPU 0.85x, GPU 0.62x.
56
+ - **Blockwise `correct_frequency_offset` on the GPU:** builds its phase trajectory on the GPU, where it used to make a round trip through the host.
57
+
58
+ Mapping and LLRs:
59
+
60
+ - **Exact LLRs on the GPU:** one exponential per point instead of one per point and bit. With the third pass over the points also gone, 128-cross QAM is 0.79x of baseline `0005`.
61
+ - **Square QAM on the GPU:** demapped per axis, 2·sqrt(M) metrics instead of M.
62
+ - **Hard decisions of PSK and cross QAM on the GPU:** one kernel launch instead of a Python loop over chunks.
63
+ - **`Constellation.map`:** packs the bits in one pass. On a 4-core machine: 16-QAM 33 → 5.8 ms, 256-QAM 55 → 10.5 ms, 2^20 symbols.
64
+ - **`Constellation.rotational_symmetry`:** 1024-QAM in 0.026 s instead of 2.2 s.
65
+
66
+ Other functions:
67
+
68
+ - **`correct_iq_imbalance`:** all channels at once from closed-form 2×2 statistics, CPU about 0.45x.
69
+ - **`evm` and `snr`:** measure the reference power once: 0.74–0.78x.
70
+ - **`moving_average(mode="valid")`:** one pass over all channels. CPU 0.11–0.15x, GPU 0.21–0.71x.
71
+ - **`estimate_timing` with several templates:** transforms the signal once: CPU 0.62–0.72x.
72
+ - **`plot_eye_diagram` histograms:** CPU 0.33x, GPU 0.55x.
73
+ - **Fiber propagation on the CPU:** NumPy stages replace the Numba passes, which competed with the multithreaded FFTs (0.56–0.92x for 2^14 to 2^20 samples). On the GPU, the plain CuPy stages that complex128 input runs are 0.53x for DBP.
74
+ - **Fewer GPU copies:** frequency grids, windows and the polarization demultiplexer's grid are built on the GPU instead of being copied there on every call.
75
+
76
+ ### Fixed
77
+
78
+ - `plot_eye_diagram` ignored `vmin` and `vmax`.
79
+ - BPS on the GPU failed with `CUDA_ERROR_INVALID_VALUE` for more than 65535 channels.
80
+ - Single-precision input was computed in double precision in BPS distances, `block_rde`'s ring distances, the block equalizers' forward filter and `iir_filter`, against the library's precision contract.
81
+
82
+ ### Development
83
+
84
+ - **Test plugins:**
85
+ - `tests/plugins/transfer_trace.py` audits host-device copies in a GPU run.
86
+ - `tests/plugins/precision_trace.py` now follows single-precision data through the FFTs.
87
+ - **CI:** the JIT-disabled kernel-coverage job measures the equalizers' Numba kernels again, and strict mypy covers every module of the `filtering` package.
88
+ - **Benchmarks:** new benchmarks for the IIR filter, moving average, PLL, blockwise Mengali-Morelli and IQ correction. Baseline `0006` is the new reference.
89
+ - **Code organization:** `filtering` and `analysis.linewidth` are packages, and every public name imports from the same place as before. The sequential equalizers share their Numba steps, CMA and RDE share one body, and duplicated helpers are merged.
90
+ - **Docs:** docstrings follow one style throughout. AGENTS.md documents the CUDA review rules and how to run `compute-sanitizer`.
91
+
92
+ ## [2.2.0] - 2026-10-23
93
+
94
+ See the [GitHub release](https://github.com/lokgar/commkit/releases/tag/v2.2.0) and the [changes since 2.1.0](https://github.com/lokgar/commkit/compare/v2.1.0...v2.2.0).
95
+
96
+ ## [2.1.0] - 2026-10-21
97
+
98
+ See the [GitHub release](https://github.com/lokgar/commkit/releases/tag/v2.1.0) and the [changes since 2.0.0](https://github.com/lokgar/commkit/compare/v2.0.0...v2.1.0).
99
+
100
+ ## [2.0.0] - 2026-10-07
101
+
102
+ See the [GitHub release](https://github.com/lokgar/commkit/releases/tag/v2.0.0) and the [changes since 1.1.0](https://github.com/lokgar/commkit/compare/v1.1.0...v2.0.0).
103
+
104
+ ## [1.1.0] - 2026-09-03
105
+
106
+ See the [GitHub release](https://github.com/lokgar/commkit/releases/tag/v1.1.0) and the [changes since 1.0.0](https://github.com/lokgar/commkit/compare/v1.0.0...v1.1.0).
107
+
108
+ ## [1.0.0] - 2026-08-31
109
+
110
+ First release. See the [GitHub release](https://github.com/lokgar/commkit/releases/tag/v1.0.0).
111
+
112
+ [2.3.0]: https://github.com/lokgar/commkit/compare/v2.2.0...v2.3.0
113
+ [2.2.0]: https://github.com/lokgar/commkit/compare/v2.1.0...v2.2.0
114
+ [2.1.0]: https://github.com/lokgar/commkit/compare/v2.0.0...v2.1.0
115
+ [2.0.0]: https://github.com/lokgar/commkit/compare/v1.1.0...v2.0.0
116
+ [1.1.0]: https://github.com/lokgar/commkit/compare/v1.0.0...v1.1.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: commkit
3
- Version: 2.2.0
3
+ Version: 2.3.0
4
4
  Summary: A Python library for high-performance digital communications research.
5
5
  Project-URL: Homepage, https://github.com/lokgar/commkit
6
6
  Project-URL: Repository, https://github.com/lokgar/commkit
@@ -101,7 +101,7 @@ CommKit never picks a device for you: data stays where you put it, and each func
101
101
  | Frequency-domain equalizers `block_lms`, `block_cma`, `block_rde` | runs on the GPU (CUDA graphs) |
102
102
  | Sequential equalizers `lms`, `rls`, `cma`, `rde`; `PLL`, `Tikhonov`, cycle-slip correction | runs on the CPU (Numba): one copy to the host and back, no speedup |
103
103
 
104
- A sample-by-sample recursion has no parallel work per step. For long records on the GPU, prefer the block equalizers with `block_size=1024` or more. Each block costs a fixed overhead: at 256 the GPU only ties the CPU without carrier recovery, at 2048 it is 6-9x faster. A larger block also lowers the stable `step_size`.
104
+ A sample-by-sample recursion has no parallel work per step. For long records on the GPU, prefer the block equalizers with `block_size=1024` or more. Each block costs a fixed overhead: at 256 the GPU is about 2x faster than the CPU without carrier recovery and 5x with BPS, at 2048 about 4x and 13x. A larger block also lowers the stable `step_size`.
105
105
  - **Short records do not pay off.** Launch overhead dominates below roughly 10⁴-10⁵ samples, where the CPU is as fast.
106
106
  - **Batch channels and records.** Pass `(C, N)` arrays rather than looping over channels in Python: one call does the work of C.
107
107
  - **Keep scalars on the device inside loops.** `float(x)`, `x.item()` and `if x > 0:` on a CuPy array wait for the GPU and copy. Collect values in an array and transfer once.
@@ -116,7 +116,7 @@ CommKit never picks a device for you: data stays where you put it, and each func
116
116
  | --- | --- |
117
117
  | [`commkit.core`](https://github.com/lokgar/commkit/tree/main/commkit/core) | `Signal`, `Reference`, `generate`, `SingleCarrierFrame` and `Preamble` (pilots, guard intervals, MIMO streams), `extract_payload`. |
118
118
  | [`commkit.mapping`](https://github.com/lokgar/commkit/tree/main/commkit/mapping) | `Constellation` (QAM, PSK, PAM, arbitrary points; Gray labels; probabilistic shaping with `.shaped()`), bit mapping, hard demapping, max-log and exact LLRs. |
119
- | [`commkit.filtering`](https://github.com/lokgar/commkit/blob/main/commkit/filtering.py) | Pulses (`RRC`, `RC`, `Gaussian`, `Rect`, `SmoothRect`), FIR and IIR design (Butterworth, Chebyshev I/II, elliptic, Bessel), `fir_filter`, `iir_filter`, `matched_filter`, overlap-save, chromatic-dispersion compensation and digital backpropagation (`correct_fiber_propagation`). |
119
+ | [`commkit.filtering`](https://github.com/lokgar/commkit/tree/main/commkit/filtering) | Pulses (`RRC`, `RC`, `Gaussian`, `Rect`, `SmoothRect`), FIR and IIR design (Butterworth, Chebyshev I/II, elliptic, Bessel), `fir_filter`, `iir_filter`, `matched_filter`, overlap-save, chromatic-dispersion compensation and digital backpropagation (`correct_fiber_propagation`). |
120
120
  | [`commkit.multirate`](https://github.com/lokgar/commkit/blob/main/commkit/multirate.py) | `resample` (fractional), `decimate`, `upsample`, `decimate_to_symbol_rate`. |
121
121
  | [`commkit.spectral`](https://github.com/lokgar/commkit/blob/main/commkit/spectral.py) | Welch PSD, spectrograms, frequency shifting, pilot tones. |
122
122
  | [`commkit.impairments`](https://github.com/lokgar/commkit/tree/main/commkit/impairments) | AWGN, phase noise, IQ imbalance (with Löwdin and Gram-Schmidt correction), chromatic dispersion, PMD and polarization mixing, and nonlinear fiber propagation over amplified spans (split-step Kerr effect with dispersion, loss, PMD and ASE) of a `Fiber`. |
@@ -65,7 +65,7 @@ CommKit never picks a device for you: data stays where you put it, and each func
65
65
  | Frequency-domain equalizers `block_lms`, `block_cma`, `block_rde` | runs on the GPU (CUDA graphs) |
66
66
  | Sequential equalizers `lms`, `rls`, `cma`, `rde`; `PLL`, `Tikhonov`, cycle-slip correction | runs on the CPU (Numba): one copy to the host and back, no speedup |
67
67
 
68
- A sample-by-sample recursion has no parallel work per step. For long records on the GPU, prefer the block equalizers with `block_size=1024` or more. Each block costs a fixed overhead: at 256 the GPU only ties the CPU without carrier recovery, at 2048 it is 6-9x faster. A larger block also lowers the stable `step_size`.
68
+ A sample-by-sample recursion has no parallel work per step. For long records on the GPU, prefer the block equalizers with `block_size=1024` or more. Each block costs a fixed overhead: at 256 the GPU is about 2x faster than the CPU without carrier recovery and 5x with BPS, at 2048 about 4x and 13x. A larger block also lowers the stable `step_size`.
69
69
  - **Short records do not pay off.** Launch overhead dominates below roughly 10⁴-10⁵ samples, where the CPU is as fast.
70
70
  - **Batch channels and records.** Pass `(C, N)` arrays rather than looping over channels in Python: one call does the work of C.
71
71
  - **Keep scalars on the device inside loops.** `float(x)`, `x.item()` and `if x > 0:` on a CuPy array wait for the GPU and copy. Collect values in an array and transfer once.
@@ -80,7 +80,7 @@ CommKit never picks a device for you: data stays where you put it, and each func
80
80
  | --- | --- |
81
81
  | [`commkit.core`](https://github.com/lokgar/commkit/tree/main/commkit/core) | `Signal`, `Reference`, `generate`, `SingleCarrierFrame` and `Preamble` (pilots, guard intervals, MIMO streams), `extract_payload`. |
82
82
  | [`commkit.mapping`](https://github.com/lokgar/commkit/tree/main/commkit/mapping) | `Constellation` (QAM, PSK, PAM, arbitrary points; Gray labels; probabilistic shaping with `.shaped()`), bit mapping, hard demapping, max-log and exact LLRs. |
83
- | [`commkit.filtering`](https://github.com/lokgar/commkit/blob/main/commkit/filtering.py) | Pulses (`RRC`, `RC`, `Gaussian`, `Rect`, `SmoothRect`), FIR and IIR design (Butterworth, Chebyshev I/II, elliptic, Bessel), `fir_filter`, `iir_filter`, `matched_filter`, overlap-save, chromatic-dispersion compensation and digital backpropagation (`correct_fiber_propagation`). |
83
+ | [`commkit.filtering`](https://github.com/lokgar/commkit/tree/main/commkit/filtering) | Pulses (`RRC`, `RC`, `Gaussian`, `Rect`, `SmoothRect`), FIR and IIR design (Butterworth, Chebyshev I/II, elliptic, Bessel), `fir_filter`, `iir_filter`, `matched_filter`, overlap-save, chromatic-dispersion compensation and digital backpropagation (`correct_fiber_propagation`). |
84
84
  | [`commkit.multirate`](https://github.com/lokgar/commkit/blob/main/commkit/multirate.py) | `resample` (fractional), `decimate`, `upsample`, `decimate_to_symbol_rate`. |
85
85
  | [`commkit.spectral`](https://github.com/lokgar/commkit/blob/main/commkit/spectral.py) | Welch PSD, spectrograms, frequency shifting, pilot tones. |
86
86
  | [`commkit.impairments`](https://github.com/lokgar/commkit/tree/main/commkit/impairments) | AWGN, phase noise, IQ imbalance (with Löwdin and Gram-Schmidt correction), chromatic dispersion, PMD and polarization mixing, and nonlinear fiber propagation over amplified spans (split-step Kerr effect with dispersion, loss, PMD and ASE) of a `Fiber`. |