commkit 3.3.0__tar.gz → 3.4.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 (306) hide show
  1. {commkit-3.3.0 → commkit-3.4.0}/.github/workflows/ci.yml +1 -0
  2. commkit-3.4.0/AGENTS.md +330 -0
  3. {commkit-3.3.0 → commkit-3.4.0}/CHANGELOG.md +34 -1
  4. {commkit-3.3.0 → commkit-3.4.0}/PKG-INFO +1 -1
  5. {commkit-3.3.0 → commkit-3.4.0}/commkit/__init__.py +1 -1
  6. {commkit-3.3.0 → commkit-3.4.0}/commkit/coding/__init__.py +5 -1
  7. {commkit-3.3.0 → commkit-3.4.0}/commkit/coding/_numba_loops.py +73 -5
  8. {commkit-3.3.0 → commkit-3.4.0}/commkit/coding/algebraic.py +2 -0
  9. {commkit-3.3.0 → commkit-3.4.0}/commkit/coding/base.py +65 -11
  10. {commkit-3.3.0 → commkit-3.4.0}/commkit/coding/convolutional.py +1 -0
  11. {commkit-3.3.0 → commkit-3.4.0}/commkit/coding/crc.py +1 -0
  12. {commkit-3.3.0 → commkit-3.4.0}/commkit/coding/exit.py +10 -9
  13. {commkit-3.3.0 → commkit-3.4.0}/commkit/coding/ldpc.py +1 -0
  14. {commkit-3.3.0 → commkit-3.4.0}/commkit/coding/linear.py +134 -16
  15. commkit-3.4.0/commkit/coding/performance.py +369 -0
  16. {commkit-3.3.0 → commkit-3.4.0}/commkit/coding/polar.py +1 -0
  17. {commkit-3.3.0 → commkit-3.4.0}/commkit/coding/product.py +10 -2
  18. {commkit-3.3.0 → commkit-3.4.0}/commkit/coding/ratematch.py +2 -0
  19. {commkit-3.3.0 → commkit-3.4.0}/commkit/coding/shaping.py +2 -0
  20. {commkit-3.3.0 → commkit-3.4.0}/commkit/coding/turbo.py +8 -3
  21. {commkit-3.3.0 → commkit-3.4.0}/commkit/impairments/noise.py +65 -28
  22. {commkit-3.3.0 → commkit-3.4.0}/commkit/mapping/constellation.py +10 -3
  23. {commkit-3.3.0 → commkit-3.4.0}/commkit/mapping/llr.py +43 -6
  24. commkit-3.4.0/commkit/metrics/__init__.py +125 -0
  25. commkit-3.4.0/commkit/metrics/_common.py +180 -0
  26. commkit-3.4.0/commkit/metrics/error_rates.py +241 -0
  27. commkit-3.4.0/commkit/metrics/information.py +354 -0
  28. commkit-3.4.0/commkit/metrics/quality.py +248 -0
  29. {commkit-3.3.0 → commkit-3.4.0}/commkit/plotting/__init__.py +9 -3
  30. {commkit-3.3.0 → commkit-3.4.0}/commkit/plotting/coding.py +221 -6
  31. {commkit-3.3.0 → commkit-3.4.0}/examples/00-getting-started.ipynb +87 -19
  32. {commkit-3.3.0 → commkit-3.4.0}/examples/01-constellations-and-bit-mapping.ipynb +77 -6
  33. {commkit-3.3.0 → commkit-3.4.0}/examples/02-spectral-analysis.ipynb +42 -11
  34. {commkit-3.3.0 → commkit-3.4.0}/examples/03-digital-filters.ipynb +57 -21
  35. {commkit-3.3.0 → commkit-3.4.0}/examples/04-pulse-shaping-and-matched-filtering.ipynb +79 -21
  36. {commkit-3.3.0 → commkit-3.4.0}/examples/05-multirate-and-resampling.ipynb +59 -9
  37. {commkit-3.3.0 → commkit-3.4.0}/examples/06-awgn-and-performance-metrics.ipynb +199 -25
  38. {commkit-3.3.0 → commkit-3.4.0}/examples/07-channel-coding.ipynb +221 -77
  39. {commkit-3.3.0 → commkit-3.4.0}/examples/08-probabilistic-shaping-and-coded-modulation.ipynb +61 -37
  40. {commkit-3.3.0 → commkit-3.4.0}/examples/09-laser-phase-noise.ipynb +37 -21
  41. {commkit-3.3.0 → commkit-3.4.0}/examples/10-front-end-impairments.ipynb +45 -7
  42. {commkit-3.3.0 → commkit-3.4.0}/examples/11-frame-synchronization-and-timing.ipynb +49 -3
  43. {commkit-3.3.0 → commkit-3.4.0}/examples/12-frequency-offset-estimation.ipynb +58 -8
  44. {commkit-3.3.0 → commkit-3.4.0}/examples/13-blind-carrier-phase-recovery.ipynb +86 -19
  45. {commkit-3.3.0 → commkit-3.4.0}/examples/14-pilot-aided-carrier-recovery.ipynb +15 -5
  46. {commkit-3.3.0 → commkit-3.4.0}/examples/15-linear-channel-equalization.ipynb +51 -14
  47. {commkit-3.3.0 → commkit-3.4.0}/examples/16-adaptive-equalization.ipynb +21 -25
  48. {commkit-3.3.0 → commkit-3.4.0}/examples/17-blind-equalization.ipynb +50 -7
  49. {commkit-3.3.0 → commkit-3.4.0}/examples/18-polarization-multiplexing.ipynb +45 -20
  50. {commkit-3.3.0 → commkit-3.4.0}/examples/19-chromatic-dispersion.ipynb +11 -5
  51. {commkit-3.3.0 → commkit-3.4.0}/examples/20-nonlinear-fiber-and-backpropagation.ipynb +58 -0
  52. {commkit-3.3.0 → commkit-3.4.0}/examples/21-fec-in-optical-standards.ipynb +63 -26
  53. {commkit-3.3.0 → commkit-3.4.0}/examples/22-coherent-transceiver-end-to-end.ipynb +16 -13
  54. {commkit-3.3.0 → commkit-3.4.0}/examples/24-laser-linewidth-self-heterodyne.ipynb +3 -3
  55. {commkit-3.3.0 → commkit-3.4.0}/examples/25-laser-linewidth-short-delay-and-flicker.ipynb +3 -3
  56. {commkit-3.3.0 → commkit-3.4.0}/examples/26-laser-linewidth-self-homodyne-iq.ipynb +19 -18
  57. {commkit-3.3.0 → commkit-3.4.0}/examples/27-measurement-template-self-heterodyne.ipynb +1 -1
  58. {commkit-3.3.0 → commkit-3.4.0}/examples/28-measurement-template-self-homodyne-iq.ipynb +12 -12
  59. {commkit-3.3.0 → commkit-3.4.0}/examples/README.md +3 -3
  60. {commkit-3.3.0 → commkit-3.4.0}/pyproject.toml +2 -2
  61. commkit-3.4.0/scripts/sanitize.sh +76 -0
  62. {commkit-3.3.0 → commkit-3.4.0}/tests/coding/test_coding_base.py +97 -1
  63. {commkit-3.3.0 → commkit-3.4.0}/tests/coding/test_exit.py +9 -2
  64. {commkit-3.3.0 → commkit-3.4.0}/tests/coding/test_linear_codes.py +53 -0
  65. commkit-3.4.0/tests/coding/test_performance.py +214 -0
  66. {commkit-3.3.0 → commkit-3.4.0}/tests/coding/test_product.py +37 -0
  67. {commkit-3.3.0 → commkit-3.4.0}/tests/coding/test_turbo.py +47 -1
  68. {commkit-3.3.0 → commkit-3.4.0}/tests/common/theory.py +17 -0
  69. {commkit-3.3.0 → commkit-3.4.0}/tests/impairments/test_noise.py +48 -2
  70. {commkit-3.3.0 → commkit-3.4.0}/tests/mapping/test_llr.py +78 -7
  71. commkit-3.4.0/tests/metrics/test_common.py +169 -0
  72. commkit-3.4.0/tests/metrics/test_error_rates.py +233 -0
  73. commkit-3.4.0/tests/metrics/test_information.py +283 -0
  74. commkit-3.4.0/tests/metrics/test_quality.py +185 -0
  75. {commkit-3.3.0 → commkit-3.4.0}/tests/plotting/test_plot_coding.py +113 -1
  76. {commkit-3.3.0 → commkit-3.4.0}/tests/plugins/transfer_trace.py +6 -2
  77. {commkit-3.3.0 → commkit-3.4.0}/tests/test_api_contracts.py +12 -9
  78. {commkit-3.3.0 → commkit-3.4.0}/tests/test_working_memory.py +2 -1
  79. {commkit-3.3.0 → commkit-3.4.0}/uv.lock +1 -1
  80. commkit-3.3.0/AGENTS.md +0 -398
  81. commkit-3.3.0/commkit/metrics.py +0 -991
  82. commkit-3.3.0/tests/test_metrics.py +0 -839
  83. {commkit-3.3.0 → commkit-3.4.0}/.gitattributes +0 -0
  84. {commkit-3.3.0 → commkit-3.4.0}/.github/workflows/publish.yml +0 -0
  85. {commkit-3.3.0 → commkit-3.4.0}/.gitignore +0 -0
  86. {commkit-3.3.0 → commkit-3.4.0}/.python-version +0 -0
  87. {commkit-3.3.0 → commkit-3.4.0}/LICENSE +0 -0
  88. {commkit-3.3.0 → commkit-3.4.0}/README.md +0 -0
  89. {commkit-3.3.0 → commkit-3.4.0}/benchmarks/baselines/Linux-CPython-3.14-64bit/0002_pre_v2.json +0 -0
  90. {commkit-3.3.0 → commkit-3.4.0}/benchmarks/baselines/Linux-CPython-3.14-64bit/0003_v2_0.json +0 -0
  91. {commkit-3.3.0 → commkit-3.4.0}/benchmarks/baselines/Linux-CPython-3.14-64bit/0004_fiber_dbp.json +0 -0
  92. {commkit-3.3.0 → commkit-3.4.0}/benchmarks/baselines/Linux-CPython-3.14-64bit/0005_gpu_decoder_path.json +0 -0
  93. {commkit-3.3.0 → commkit-3.4.0}/benchmarks/baselines/Linux-CPython-3.14-64bit/0006_perf_cleanup.json +0 -0
  94. {commkit-3.3.0 → commkit-3.4.0}/benchmarks/baselines/Linux-CPython-3.14-64bit/0007_v3_0.json +0 -0
  95. {commkit-3.3.0 → commkit-3.4.0}/benchmarks/baselines/Linux-CPython-3.14-64bit/0008_v3_1.json +0 -0
  96. {commkit-3.3.0 → commkit-3.4.0}/benchmarks/bench_block_blind.py +0 -0
  97. {commkit-3.3.0 → commkit-3.4.0}/benchmarks/bench_block_lms.py +0 -0
  98. {commkit-3.3.0 → commkit-3.4.0}/benchmarks/bench_bps.py +0 -0
  99. {commkit-3.3.0 → commkit-3.4.0}/benchmarks/bench_coding.py +0 -0
  100. {commkit-3.3.0 → commkit-3.4.0}/benchmarks/bench_equalizers.py +0 -0
  101. {commkit-3.3.0 → commkit-3.4.0}/benchmarks/bench_fiber.py +0 -0
  102. {commkit-3.3.0 → commkit-3.4.0}/benchmarks/bench_filters.py +0 -0
  103. {commkit-3.3.0 → commkit-3.4.0}/benchmarks/bench_import.py +0 -0
  104. {commkit-3.3.0 → commkit-3.4.0}/benchmarks/bench_llr.py +0 -0
  105. {commkit-3.3.0 → commkit-3.4.0}/benchmarks/bench_mapping.py +0 -0
  106. {commkit-3.3.0 → commkit-3.4.0}/benchmarks/bench_signal_pipeline.py +0 -0
  107. {commkit-3.3.0 → commkit-3.4.0}/benchmarks/bench_sync_misc.py +0 -0
  108. {commkit-3.3.0 → commkit-3.4.0}/benchmarks/benchutils.py +0 -0
  109. {commkit-3.3.0 → commkit-3.4.0}/benchmarks/conftest.py +0 -0
  110. {commkit-3.3.0 → commkit-3.4.0}/benchmarks/record_baseline.py +0 -0
  111. {commkit-3.3.0 → commkit-3.4.0}/benchmarks/workloads.py +0 -0
  112. {commkit-3.3.0 → commkit-3.4.0}/commkit/_array.py +0 -0
  113. {commkit-3.3.0 → commkit-3.4.0}/commkit/_circular.py +0 -0
  114. {commkit-3.3.0 → commkit-3.4.0}/commkit/_constants.py +0 -0
  115. {commkit-3.3.0 → commkit-3.4.0}/commkit/_cuda/__init__.py +0 -0
  116. {commkit-3.3.0 → commkit-3.4.0}/commkit/_cuda/compiler.py +0 -0
  117. {commkit-3.3.0 → commkit-3.4.0}/commkit/_cuda/graphs.py +0 -0
  118. {commkit-3.3.0 → commkit-3.4.0}/commkit/_cuda/src/algebraic.cu +0 -0
  119. {commkit-3.3.0 → commkit-3.4.0}/commkit/_cuda/src/bps_anchor.cu +0 -0
  120. {commkit-3.3.0 → commkit-3.4.0}/commkit/_cuda/src/bps_min_d2.cu +0 -0
  121. {commkit-3.3.0 → commkit-3.4.0}/commkit/_cuda/src/cs_block.cu +0 -0
  122. {commkit-3.3.0 → commkit-3.4.0}/commkit/_cuda/src/iir.cu +0 -0
  123. {commkit-3.3.0 → commkit-3.4.0}/commkit/_cuda/src/ldpc.cu +0 -0
  124. {commkit-3.3.0 → commkit-3.4.0}/commkit/_cuda/src/llr.cu +0 -0
  125. {commkit-3.3.0 → commkit-3.4.0}/commkit/_cuda/src/selftest.cu +0 -0
  126. {commkit-3.3.0 → commkit-3.4.0}/commkit/_dispersion.py +0 -0
  127. {commkit-3.3.0 → commkit-3.4.0}/commkit/_fft.py +0 -0
  128. {commkit-3.3.0 → commkit-3.4.0}/commkit/_fiber.py +0 -0
  129. {commkit-3.3.0 → commkit-3.4.0}/commkit/_overlap_save.py +0 -0
  130. {commkit-3.3.0 → commkit-3.4.0}/commkit/_random.py +0 -0
  131. {commkit-3.3.0 → commkit-3.4.0}/commkit/_sequences.py +0 -0
  132. {commkit-3.3.0 → commkit-3.4.0}/commkit/analysis/__init__.py +0 -0
  133. {commkit-3.3.0 → commkit-3.4.0}/commkit/analysis/_common.py +0 -0
  134. {commkit-3.3.0 → commkit-3.4.0}/commkit/analysis/allan.py +0 -0
  135. {commkit-3.3.0 → commkit-3.4.0}/commkit/analysis/drift.py +0 -0
  136. {commkit-3.3.0 → commkit-3.4.0}/commkit/analysis/fm_noise.py +0 -0
  137. {commkit-3.3.0 → commkit-3.4.0}/commkit/analysis/interferometry.py +0 -0
  138. {commkit-3.3.0 → commkit-3.4.0}/commkit/analysis/linewidth/__init__.py +0 -0
  139. {commkit-3.3.0 → commkit-3.4.0}/commkit/analysis/linewidth/dsh.py +0 -0
  140. {commkit-3.3.0 → commkit-3.4.0}/commkit/analysis/linewidth/phase.py +0 -0
  141. {commkit-3.3.0 → commkit-3.4.0}/commkit/analysis/linewidth/result.py +0 -0
  142. {commkit-3.3.0 → commkit-3.4.0}/commkit/analysis/trajectory.py +0 -0
  143. {commkit-3.3.0 → commkit-3.4.0}/commkit/backend.py +0 -0
  144. {commkit-3.3.0 → commkit-3.4.0}/commkit/coding/_galois.py +0 -0
  145. {commkit-3.3.0 → commkit-3.4.0}/commkit/coding/_gf2.py +0 -0
  146. {commkit-3.3.0 → commkit-3.4.0}/commkit/coding/_ldpc_tables.py +0 -0
  147. {commkit-3.3.0 → commkit-3.4.0}/commkit/coding/interleaving.py +0 -0
  148. {commkit-3.3.0 → commkit-3.4.0}/commkit/core/__init__.py +0 -0
  149. {commkit-3.3.0 → commkit-3.4.0}/commkit/core/_signal_adapter.py +0 -0
  150. {commkit-3.3.0 → commkit-3.4.0}/commkit/core/frame.py +0 -0
  151. {commkit-3.3.0 → commkit-3.4.0}/commkit/core/generation.py +0 -0
  152. {commkit-3.3.0 → commkit-3.4.0}/commkit/core/signal.py +0 -0
  153. {commkit-3.3.0 → commkit-3.4.0}/commkit/equalization/__init__.py +0 -0
  154. {commkit-3.3.0 → commkit-3.4.0}/commkit/equalization/_block/__init__.py +0 -0
  155. {commkit-3.3.0 → commkit-3.4.0}/commkit/equalization/_block/_blind.py +0 -0
  156. {commkit-3.3.0 → commkit-3.4.0}/commkit/equalization/_block/_dd.py +0 -0
  157. {commkit-3.3.0 → commkit-3.4.0}/commkit/equalization/_block/_engine.py +0 -0
  158. {commkit-3.3.0 → commkit-3.4.0}/commkit/equalization/_common.py +0 -0
  159. {commkit-3.3.0 → commkit-3.4.0}/commkit/equalization/_kernels_numba.py +0 -0
  160. {commkit-3.3.0 → commkit-3.4.0}/commkit/equalization/_numba_loops.py +0 -0
  161. {commkit-3.3.0 → commkit-3.4.0}/commkit/equalization/blind.py +0 -0
  162. {commkit-3.3.0 → commkit-3.4.0}/commkit/equalization/linear.py +0 -0
  163. {commkit-3.3.0 → commkit-3.4.0}/commkit/equalization/pilots.py +0 -0
  164. {commkit-3.3.0 → commkit-3.4.0}/commkit/equalization/polarization.py +0 -0
  165. {commkit-3.3.0 → commkit-3.4.0}/commkit/equalization/result.py +0 -0
  166. {commkit-3.3.0 → commkit-3.4.0}/commkit/equalization/sequential/__init__.py +0 -0
  167. {commkit-3.3.0 → commkit-3.4.0}/commkit/equalization/sequential/_blind.py +0 -0
  168. {commkit-3.3.0 → commkit-3.4.0}/commkit/equalization/sequential/_dd.py +0 -0
  169. {commkit-3.3.0 → commkit-3.4.0}/commkit/equalization/sequential/_setup.py +0 -0
  170. {commkit-3.3.0 → commkit-3.4.0}/commkit/filtering/__init__.py +0 -0
  171. {commkit-3.3.0 → commkit-3.4.0}/commkit/filtering/fiber.py +0 -0
  172. {commkit-3.3.0 → commkit-3.4.0}/commkit/filtering/linear.py +0 -0
  173. {commkit-3.3.0 → commkit-3.4.0}/commkit/filtering/pulses.py +0 -0
  174. {commkit-3.3.0 → commkit-3.4.0}/commkit/filtering/response.py +0 -0
  175. {commkit-3.3.0 → commkit-3.4.0}/commkit/frequency.py +0 -0
  176. {commkit-3.3.0 → commkit-3.4.0}/commkit/impairments/__init__.py +0 -0
  177. {commkit-3.3.0 → commkit-3.4.0}/commkit/impairments/channel/__init__.py +0 -0
  178. {commkit-3.3.0 → commkit-3.4.0}/commkit/impairments/channel/linear.py +0 -0
  179. {commkit-3.3.0 → commkit-3.4.0}/commkit/impairments/channel/nonlinear.py +0 -0
  180. {commkit-3.3.0 → commkit-3.4.0}/commkit/impairments/frontend.py +0 -0
  181. {commkit-3.3.0 → commkit-3.4.0}/commkit/impairments/source.py +0 -0
  182. {commkit-3.3.0 → commkit-3.4.0}/commkit/io.py +0 -0
  183. {commkit-3.3.0 → commkit-3.4.0}/commkit/logger.py +0 -0
  184. {commkit-3.3.0 → commkit-3.4.0}/commkit/mapping/__init__.py +0 -0
  185. {commkit-3.3.0 → commkit-3.4.0}/commkit/mapping/_numba_llr.py +0 -0
  186. {commkit-3.3.0 → commkit-3.4.0}/commkit/mapping/bits.py +0 -0
  187. {commkit-3.3.0 → commkit-3.4.0}/commkit/mapping/gray.py +0 -0
  188. {commkit-3.3.0 → commkit-3.4.0}/commkit/mapping/shaping.py +0 -0
  189. {commkit-3.3.0 → commkit-3.4.0}/commkit/math.py +0 -0
  190. {commkit-3.3.0 → commkit-3.4.0}/commkit/multirate.py +0 -0
  191. {commkit-3.3.0 → commkit-3.4.0}/commkit/plotting/analysis.py +0 -0
  192. {commkit-3.3.0 → commkit-3.4.0}/commkit/plotting/constellation.py +0 -0
  193. {commkit-3.3.0 → commkit-3.4.0}/commkit/plotting/equalizer.py +0 -0
  194. {commkit-3.3.0 → commkit-3.4.0}/commkit/plotting/eye.py +0 -0
  195. {commkit-3.3.0 → commkit-3.4.0}/commkit/plotting/filter_response.py +0 -0
  196. {commkit-3.3.0 → commkit-3.4.0}/commkit/plotting/spectral.py +0 -0
  197. {commkit-3.3.0 → commkit-3.4.0}/commkit/plotting/sync.py +0 -0
  198. {commkit-3.3.0 → commkit-3.4.0}/commkit/plotting/theme.py +0 -0
  199. {commkit-3.3.0 → commkit-3.4.0}/commkit/plotting/waveform.py +0 -0
  200. {commkit-3.3.0 → commkit-3.4.0}/commkit/py.typed +0 -0
  201. {commkit-3.3.0 → commkit-3.4.0}/commkit/recovery/__init__.py +0 -0
  202. {commkit-3.3.0 → commkit-3.4.0}/commkit/recovery/_common.py +0 -0
  203. {commkit-3.3.0 → commkit-3.4.0}/commkit/recovery/bps.py +0 -0
  204. {commkit-3.3.0 → commkit-3.4.0}/commkit/recovery/carrier_phase.py +0 -0
  205. {commkit-3.3.0 → commkit-3.4.0}/commkit/recovery/corrections.py +0 -0
  206. {commkit-3.3.0 → commkit-3.4.0}/commkit/recovery/pilots.py +0 -0
  207. {commkit-3.3.0 → commkit-3.4.0}/commkit/recovery/pll.py +0 -0
  208. {commkit-3.3.0 → commkit-3.4.0}/commkit/recovery/tikhonov.py +0 -0
  209. {commkit-3.3.0 → commkit-3.4.0}/commkit/recovery/viterbi_viterbi.py +0 -0
  210. {commkit-3.3.0 → commkit-3.4.0}/commkit/smoothing.py +0 -0
  211. {commkit-3.3.0 → commkit-3.4.0}/commkit/spectral.py +0 -0
  212. {commkit-3.3.0 → commkit-3.4.0}/commkit/timing.py +0 -0
  213. {commkit-3.3.0 → commkit-3.4.0}/examples/23-carrier-phase-characterization.ipynb +0 -0
  214. {commkit-3.3.0 → commkit-3.4.0}/tests/analysis/test_allan.py +0 -0
  215. {commkit-3.3.0 → commkit-3.4.0}/tests/analysis/test_drift.py +0 -0
  216. {commkit-3.3.0 → commkit-3.4.0}/tests/analysis/test_fm_noise.py +0 -0
  217. {commkit-3.3.0 → commkit-3.4.0}/tests/analysis/test_interferometry.py +0 -0
  218. {commkit-3.3.0 → commkit-3.4.0}/tests/analysis/test_linewidth.py +0 -0
  219. {commkit-3.3.0 → commkit-3.4.0}/tests/analysis/test_trajectory.py +0 -0
  220. {commkit-3.3.0 → commkit-3.4.0}/tests/coding/test_algebraic.py +0 -0
  221. {commkit-3.3.0 → commkit-3.4.0}/tests/coding/test_convolutional.py +0 -0
  222. {commkit-3.3.0 → commkit-3.4.0}/tests/coding/test_crc.py +0 -0
  223. {commkit-3.3.0 → commkit-3.4.0}/tests/coding/test_gf2.py +0 -0
  224. {commkit-3.3.0 → commkit-3.4.0}/tests/coding/test_interleaving.py +0 -0
  225. {commkit-3.3.0 → commkit-3.4.0}/tests/coding/test_ldpc.py +0 -0
  226. {commkit-3.3.0 → commkit-3.4.0}/tests/coding/test_ldpc_exit.py +0 -0
  227. {commkit-3.3.0 → commkit-3.4.0}/tests/coding/test_ldpc_kernel.py +0 -0
  228. {commkit-3.3.0 → commkit-3.4.0}/tests/coding/test_ldpc_tables.py +0 -0
  229. {commkit-3.3.0 → commkit-3.4.0}/tests/coding/test_polar.py +0 -0
  230. {commkit-3.3.0 → commkit-3.4.0}/tests/coding/test_ratematch.py +0 -0
  231. {commkit-3.3.0 → commkit-3.4.0}/tests/coding/test_shaping.py +0 -0
  232. {commkit-3.3.0 → commkit-3.4.0}/tests/common/__init__.py +0 -0
  233. {commkit-3.3.0 → commkit-3.4.0}/tests/common/coding.py +0 -0
  234. {commkit-3.3.0 → commkit-3.4.0}/tests/common/conversions.py +0 -0
  235. {commkit-3.3.0 → commkit-3.4.0}/tests/common/kernel_utils.py +0 -0
  236. {commkit-3.3.0 → commkit-3.4.0}/tests/common/memory.py +0 -0
  237. {commkit-3.3.0 → commkit-3.4.0}/tests/common/metrics.py +0 -0
  238. {commkit-3.3.0 → commkit-3.4.0}/tests/common/periodic.py +0 -0
  239. {commkit-3.3.0 → commkit-3.4.0}/tests/common/reference_impl.py +0 -0
  240. {commkit-3.3.0 → commkit-3.4.0}/tests/common/scoring.py +0 -0
  241. {commkit-3.3.0 → commkit-3.4.0}/tests/common/signals.py +0 -0
  242. {commkit-3.3.0 → commkit-3.4.0}/tests/conftest.py +0 -0
  243. {commkit-3.3.0 → commkit-3.4.0}/tests/core/test_frame.py +0 -0
  244. {commkit-3.3.0 → commkit-3.4.0}/tests/core/test_generation.py +0 -0
  245. {commkit-3.3.0 → commkit-3.4.0}/tests/core/test_psqam.py +0 -0
  246. {commkit-3.3.0 → commkit-3.4.0}/tests/core/test_signal.py +0 -0
  247. {commkit-3.3.0 → commkit-3.4.0}/tests/core/test_signal_adapter.py +0 -0
  248. {commkit-3.3.0 → commkit-3.4.0}/tests/core/test_signal_mimo.py +0 -0
  249. {commkit-3.3.0 → commkit-3.4.0}/tests/equalization/test_blind.py +0 -0
  250. {commkit-3.3.0 → commkit-3.4.0}/tests/equalization/test_block.py +0 -0
  251. {commkit-3.3.0 → commkit-3.4.0}/tests/equalization/test_bps_kernel.py +0 -0
  252. {commkit-3.3.0 → commkit-3.4.0}/tests/equalization/test_cpr.py +0 -0
  253. {commkit-3.3.0 → commkit-3.4.0}/tests/equalization/test_cs_kernel.py +0 -0
  254. {commkit-3.3.0 → commkit-3.4.0}/tests/equalization/test_equalizer_oracles.py +0 -0
  255. {commkit-3.3.0 → commkit-3.4.0}/tests/equalization/test_linear.py +0 -0
  256. {commkit-3.3.0 → commkit-3.4.0}/tests/equalization/test_mimo.py +0 -0
  257. {commkit-3.3.0 → commkit-3.4.0}/tests/equalization/test_polarization.py +0 -0
  258. {commkit-3.3.0 → commkit-3.4.0}/tests/equalization/test_sequential.py +0 -0
  259. {commkit-3.3.0 → commkit-3.4.0}/tests/equalization/test_state.py +0 -0
  260. {commkit-3.3.0 → commkit-3.4.0}/tests/equalization/test_winit.py +0 -0
  261. {commkit-3.3.0 → commkit-3.4.0}/tests/impairments/channel/test_channel_linear.py +0 -0
  262. {commkit-3.3.0 → commkit-3.4.0}/tests/impairments/channel/test_channel_nonlinear.py +0 -0
  263. {commkit-3.3.0 → commkit-3.4.0}/tests/impairments/test_frontend.py +0 -0
  264. {commkit-3.3.0 → commkit-3.4.0}/tests/impairments/test_source.py +0 -0
  265. {commkit-3.3.0 → commkit-3.4.0}/tests/mapping/test_bits.py +0 -0
  266. {commkit-3.3.0 → commkit-3.4.0}/tests/mapping/test_constellation.py +0 -0
  267. {commkit-3.3.0 → commkit-3.4.0}/tests/mapping/test_gray.py +0 -0
  268. {commkit-3.3.0 → commkit-3.4.0}/tests/plotting/test_plot_analysis.py +0 -0
  269. {commkit-3.3.0 → commkit-3.4.0}/tests/plotting/test_plot_constellation.py +0 -0
  270. {commkit-3.3.0 → commkit-3.4.0}/tests/plotting/test_plot_equalizer.py +0 -0
  271. {commkit-3.3.0 → commkit-3.4.0}/tests/plotting/test_plot_eye.py +0 -0
  272. {commkit-3.3.0 → commkit-3.4.0}/tests/plotting/test_plot_filter_response.py +0 -0
  273. {commkit-3.3.0 → commkit-3.4.0}/tests/plotting/test_plot_spectral.py +0 -0
  274. {commkit-3.3.0 → commkit-3.4.0}/tests/plotting/test_plot_sync.py +0 -0
  275. {commkit-3.3.0 → commkit-3.4.0}/tests/plotting/test_plot_theme.py +0 -0
  276. {commkit-3.3.0 → commkit-3.4.0}/tests/plotting/test_plot_waveform.py +0 -0
  277. {commkit-3.3.0 → commkit-3.4.0}/tests/plugins/__init__.py +0 -0
  278. {commkit-3.3.0 → commkit-3.4.0}/tests/plugins/precision_trace.py +0 -0
  279. {commkit-3.3.0 → commkit-3.4.0}/tests/recovery/test_bps.py +0 -0
  280. {commkit-3.3.0 → commkit-3.4.0}/tests/recovery/test_corrections.py +0 -0
  281. {commkit-3.3.0 → commkit-3.4.0}/tests/recovery/test_joint.py +0 -0
  282. {commkit-3.3.0 → commkit-3.4.0}/tests/recovery/test_pilots.py +0 -0
  283. {commkit-3.3.0 → commkit-3.4.0}/tests/recovery/test_pll.py +0 -0
  284. {commkit-3.3.0 → commkit-3.4.0}/tests/recovery/test_recovery_oracles.py +0 -0
  285. {commkit-3.3.0 → commkit-3.4.0}/tests/recovery/test_tikhonov.py +0 -0
  286. {commkit-3.3.0 → commkit-3.4.0}/tests/recovery/test_viterbi_viterbi.py +0 -0
  287. {commkit-3.3.0 → commkit-3.4.0}/tests/test_array.py +0 -0
  288. {commkit-3.3.0 → commkit-3.4.0}/tests/test_backend.py +0 -0
  289. {commkit-3.3.0 → commkit-3.4.0}/tests/test_cuda_infra.py +0 -0
  290. {commkit-3.3.0 → commkit-3.4.0}/tests/test_dispersion.py +0 -0
  291. {commkit-3.3.0 → commkit-3.4.0}/tests/test_examples.py +0 -0
  292. {commkit-3.3.0 → commkit-3.4.0}/tests/test_fiber.py +0 -0
  293. {commkit-3.3.0 → commkit-3.4.0}/tests/test_filter_response.py +0 -0
  294. {commkit-3.3.0 → commkit-3.4.0}/tests/test_filtering.py +0 -0
  295. {commkit-3.3.0 → commkit-3.4.0}/tests/test_frequency.py +0 -0
  296. {commkit-3.3.0 → commkit-3.4.0}/tests/test_import_hygiene.py +0 -0
  297. {commkit-3.3.0 → commkit-3.4.0}/tests/test_io.py +0 -0
  298. {commkit-3.3.0 → commkit-3.4.0}/tests/test_logger.py +0 -0
  299. {commkit-3.3.0 → commkit-3.4.0}/tests/test_math.py +0 -0
  300. {commkit-3.3.0 → commkit-3.4.0}/tests/test_multirate.py +0 -0
  301. {commkit-3.3.0 → commkit-3.4.0}/tests/test_pulse_shaping.py +0 -0
  302. {commkit-3.3.0 → commkit-3.4.0}/tests/test_receiver_chain.py +0 -0
  303. {commkit-3.3.0 → commkit-3.4.0}/tests/test_signal_pipeline.py +0 -0
  304. {commkit-3.3.0 → commkit-3.4.0}/tests/test_smoothing.py +0 -0
  305. {commkit-3.3.0 → commkit-3.4.0}/tests/test_spectral.py +0 -0
  306. {commkit-3.3.0 → commkit-3.4.0}/tests/test_timing.py +0 -0
@@ -119,6 +119,7 @@ jobs:
119
119
  tests/coding/test_polar.py::TestDecoding::test_crc_failure_keeps_the_best_path
120
120
  tests/coding/test_convolutional.py tests/coding/test_turbo.py::TestEncoding
121
121
  tests/coding/test_turbo.py::TestDecoding::test_noiseless
122
+ tests/coding/test_linear_codes.py::TestMaximumLikelihood
122
123
  -k "not waterfall and not threshold"
123
124
  --device=cpu --cov=commkit --cov-report=
124
125
 
@@ -0,0 +1,330 @@
1
+ # AGENTS.md
2
+
3
+ Guide for anyone, human or coding agent, changing the **CommKit** repository.
4
+
5
+ The rules below describe the code as it is. New code follows them; a change that breaks one changes this file in the same commit.
6
+
7
+ ---
8
+
9
+ ## 1. What CommKit is
10
+
11
+ A Python library for digital-communications research: simulation, receiver DSP, metrics, plotting and experimental captures, on NumPy (CPU) and CuPy (GPU) data. It is **not differentiable**. A separate JAX library, *commax*, will hold the blocks that must sit inside a gradient. commax depends on commkit, never the reverse, and takes its conventions (`Constellation`, bit labels, pulse taps, frame layouts) from commkit.
12
+
13
+ Users are researchers and engineers. Design for them:
14
+
15
+ - **Algorithms are plain functions.** Data is a `Signal` or an array. Things that *describe* a modulation, a pulse or an embedded sub-algorithm are small frozen value objects written inline in the call:
16
+
17
+ ```python
18
+ res = lms(rx, num_taps=21, step_size=1e-3, cpr=BPS(test_phases=64))
19
+ ```
20
+
21
+ - **Everything is expressed in Python.** No configuration files, registries, global switches or multi-step setup.
22
+ - **We deliberately do not build** pipeline or receiver objects, DSP methods on `Signal`, string shorthands for constellations, or public "config objects per algorithm".
23
+ - **Scope is the reference model and its inverse**, not a catalogue of research variants: the fiber channel and standard digital backpropagation (`correct_fiber_propagation`, with `steps_per_span` and `nonlinear_scale`), like chromatic dispersion and its compensation. Reduced-complexity nonlinearity compensation (filtered or enhanced split step, perturbation-based) is left to user code built on these; learned DBP, which needs gradients, belongs in commax.
24
+
25
+ ---
26
+
27
+ ## 2. Commands
28
+
29
+ Always use `uv`. Python 3.12+.
30
+
31
+ ```bash
32
+ uv sync --all-extras # environment incl. GPU packages
33
+ uv run nbstripout --install # once per clone: strip notebook outputs on commit
34
+ uv run pytest # CPU + GPU tests (default --device=all)
35
+ uv run pytest --device=cpu # what CI runs
36
+ uv run pytest --run-slow # also the long statistical validation (marker slow)
37
+ uv run pytest tests/equalization/ -k lms # subset
38
+ uv run pytest --cov=commkit # coverage
39
+ uv run pytest --device=cpu -p tests.plugins.precision_trace \
40
+ --ignore=tests/test_examples.py # precision audit (§5); the suite passes under it
41
+ uv run pytest --device=gpu -p tests.plugins.transfer_trace \
42
+ --ignore=tests/test_examples.py # transfer audit (§6); fails on an undocumented transfer
43
+ scripts/sanitize.sh # kernels under compute-sanitizer (§6); setup in the script
44
+ ```
45
+
46
+ Numba kernel coverage runs the kernel tests with `NUMBA_DISABLE_JIT=1` and reports only the kernel modules; the test selection and the `--include` list are in the `kernel-coverage` job of `.github/workflows/ci.yml`.
47
+
48
+ **Commit gate.** CI fails on any of these, so run all of them before every commit, with `ruff format` last:
49
+
50
+ ```bash
51
+ uv run ruff check . --fix
52
+ uv run mypy commkit/
53
+ uv run pytest
54
+ uv run ruff format .
55
+ ```
56
+
57
+ **Benchmarks** are explicit-only; `uv run pytest` never collects them. A commit that changes a public signature smoke-runs them once, untimed, so their call sites cannot rot:
58
+
59
+ ```bash
60
+ uv run pytest benchmarks/ --benchmark-disable --device=all
61
+ uv run pytest benchmarks/bench_bps.py --benchmark-only --device=all # timed
62
+ uv run pytest benchmarks/ --benchmark-only --device=all \
63
+ --benchmark-compare=0008 --benchmark-storage=file://benchmarks/baselines
64
+ uv run python benchmarks/record_baseline.py NAME # record a new baseline (§8)
65
+ ```
66
+
67
+ ---
68
+
69
+ ## 3. Architecture
70
+
71
+ Dependencies point downward only:
72
+
73
+ | Layer | Modules | May import |
74
+ | --- | --- | --- |
75
+ | Infrastructure | `backend`, `logger`, `_cuda` | NumPy, CuPy |
76
+ | Array and math helpers | `_array` (shape and validation), `math` (power, normalization, dB, linear trend) | infrastructure |
77
+ | Value objects | `mapping.Constellation`, pulse classes, `recovery` CPR configs, `coding` (code objects with their `encode` / `decode`; array-only, never imports `core`) | layers above |
78
+ | Core | `core` (`Signal`, frames, generation), `io` | layers above |
79
+ | DSP | `mapping`, `filtering`, `multirate`, `spectral`, `smoothing`, `impairments`, `timing`, `frequency`, `recovery`, `equalization`, `metrics`, `analysis` | layers above, and lower DSP modules |
80
+ | Presentation | `plotting` | everything; **nothing imports plotting** (no `debug_plot`: plot functions consume results or recompute through public functions) |
81
+
82
+ - **Responsibilities:** `filtering` holds signal-chain filters with a real frequency response; `smoothing` display and estimation smoothers only; `analysis` laser and phase characterization; `metrics` is the reporting layer.
83
+ - **Codes are storable:** `io` stores a code as its class and constructor fields, so those fields are numbers, strings, tuples, host arrays, constellations or other codes.
84
+ - **Splitting modules:** promote a flat module to a package only when it exceeds about 1000 lines *and* has two or more separable concerns. Never create a `utils/` package or one-function files.
85
+ - **One documented home per public name.** A package `__init__` re-exports its subpackages' public names, and users and documentation import them from the package (`commkit.impairments.Fiber`), not from the subpackage that defines them.
86
+ - **Shared physics lives in one private module**, used by the channel and by its compensation: `_dispersion` (β₂, β₃, the dispersion response) and `_fiber` (`Fiber` and the split-step solver behind `apply_fiber_propagation` and `correct_fiber_propagation`).
87
+
88
+ ---
89
+
90
+ ## 4. API rules
91
+
92
+ ### Signal
93
+
94
+ - `Signal` holds:
95
+ - samples: NumPy or CuPy, `(N,)` or `(C, N)`, time on the last axis;
96
+ - the **facts** `sampling_rate`, `symbol_rate`, `center_frequency` and `domain` (`"DIG"`, `"RF"` or `"OPT"`; only RF and optical signals have a carrier, and an optical carrier gives the derived fact `center_wavelength_nm`);
97
+ - the **description** `constellation` and `pulse`;
98
+ - the ground truth `reference`: transmitted symbols, the bits on the air `bits`, the source bits `info_bits` (the same array when nothing is coded or matched) and the `code` between them, plus the `frame` layout.
99
+ - It never holds pipeline results.
100
+ - Construction does no hidden work: no mapping, no normalization, no device move, no shape guessing. Factories (`generate`, frames) do that work.
101
+ - `sig.to("gpu")` and `sig.replace(...)` return new Signals sharing the arrays; `clone()` copies.
102
+ - **Alignment invariant:** reference symbol *k* corresponds to symbol period *k* of the samples. A function that drops or shifts symbol periods slices the reference to match. Metrics raise on mismatch; they never guess.
103
+
104
+ ### Function signatures
105
+
106
+ - `f(data, other_data, *, params)`: everything after the data arguments is keyword-only. A function without data arguments may keep a single primary parameter positional (`barker_sequence(13)`); with several, all are keyword-only. The contract registry records each function's data-argument count.
107
+ - **Signal awareness.** A function whose main argument is waveform data (or a field a Signal carries) accepts an array or a Signal: it unwraps once with `adapt_signal()` (`core/_signal_adapter.py`), works on arrays, and returns through `wrap_samples()`, never by re-entering the public function. Functions on *derived* quantities (phase trajectories, correlations, taps, PSD bins, results) and synthesis primitives stay array-only.
108
+ - **Types:** a transform is `def f(samples: S, ...) -> S` with the TypeVar `S` from `core/_signal_adapter.py`. Do not reassign `samples`; name the unwrapped array `x`.
109
+ - **Facts versus choices.** Facts (`sampling_rate`, `symbol_rate`, hence `sps`) come from the Signal; a conflicting argument raises `ValueError`, an equal one is fine, and a missing fact for array input raises. Choices (decision `constellation`, `noise_var`, `pulse`) default to the Signal's value; an explicit argument wins. Neither case warns.
110
+ - Integer-SPS algorithms validate with `require_integer_sps()` before any cast; never truncate 1.5 SPS to 1.
111
+ - **Shapes:** SISO is `(N,)`, MIMO `(C, N)`. Use `as_2d`, `restore_1d`, `broadcast_channels` and `require_channels`; never hand-roll the promote/squeeze idiom.
112
+ - **Value objects are frozen dataclasses** with read-only NumPy arrays:
113
+ - `Constellation`: `.qam(M)`, `.psk(M)`, `.pam(M, unipolar=)`, or arbitrary `points`; `.shaped(nu=|entropy=)` keeps unit power. Functions accept the object only, never strings.
114
+ - Pulses: `RRC`, `RC`, `Gaussian`, `Rect`, `SmoothRect`. A raw taps array is accepted wherever a pulse is.
115
+ - Algorithm objects, one per method (`BPS`, `PLL`, `ViterbiViterbi`, `MthPower`, `CycleSlip`, ...), used standalone (`correct_carrier_phase(y, BPS())`) and nested (`lms(..., cpr=BPS())`). Each lives next to its kernel and validates its parameters on construction. Class names are unique across the package.
116
+ - **Design and apply are separate functions**: `RRC(...).taps(sps)` and `fir_filter`, `butterworth_sos` and `iir_filter`.
117
+ - **Randomness:** `rng: int | np.random.Generator | None` (SciPy SPEC 7); never a global RNG.
118
+ - What is transmitted (bits, symbols, pilots) and the few random parameters of a realization (the PMD waveplates of `Fiber`) are drawn on the host, so a seed gives the same payload and fiber on every device.
119
+ - What the channel adds (AWGN, phase noise, ASE) is drawn on the data's device through `_random.standard_normal` / `complex_normal`; realizations differ between devices, statistics agree.
120
+ - A seed is reproducible within a library version only. Tests and baselines assert statistics, never exact realizations.
121
+ - **Device follows the data.** No `backend=` arguments, no global switches. Unsupported array types (JAX, PyTorch) raise `TypeError`.
122
+ - Only factories with no input data to follow take `device: str = "cpu"` (never "GPU if available"): `generate`, `generate_phase_noise`, `Preamble`/`SingleCarrierFrame.to_signal`, `load_npz`, `decoder_exit_curve`, `demapper_exit_curve`, `error_rate_curve`. They validate it with `require_device()` (in `coding`, by the same check in `coding.exit`), draw randomness as above and do the vectorized work on `device`.
123
+
124
+ ### Returns
125
+
126
+ - **Transforms:** array in gives array out on the same device; Signal in gives a new Signal (with `sampling_rate` updated when the rate changes).
127
+ - **Estimators** reduce over the time axis: `(N,)` gives 0-d, `(C, N)` gives `(C,)`, on the input device.
128
+ - **Metrics** (`evm`, `snr`, `ber`, `fer`, `ser`, `gmi`, `ngmi`, `mi`) and **analysis summaries** (`estimate_linewidth`, `allan_deviation`, the `frequency_drift` statistics, which fit plot-sized reductions) return host values: a `float` for 1-D input, `np.ndarray (C,)` for 2-D. Sample-rate outputs (trajectories, PSDs, `dsh_phase`) stay on the input device.
129
+ - Metrics **raise** on empty selections, never return `None` or `0.0` for "nothing measured", and never silently align, rotate or permute against the reference.
130
+ - **More than one value** is a frozen dataclass with named fields. No dicts, no tuples longer than two.
131
+ - **Equalizers** return `EqualizerResult`: `y_hat` is always an array, `signal` the 1-SPS Signal for Signal input, `state` the continuation state (`state=result.state`), and diagnostics as data.
132
+
133
+ ### Naming
134
+
135
+ | Prefix | Meaning |
136
+ | --- | --- |
137
+ | `apply_*` | add an impairment or apply a model |
138
+ | `estimate_<quantity>(x, method)` | measure, never change the data; returns a frozen `<Quantity>Estimate` (value plus diagnostics) |
139
+ | `correct_<quantity>(x, how)` | remove the impairment; returns only the corrected data. `how` is an estimate or an algorithm object (which estimates first) |
140
+ | `resolve_*` | pick among discrete candidates (π/2 rotation, channel permutation); nothing else |
141
+ | `generate` | synthesis from parameters, takes `rng` |
142
+ | `encode` / `decode` | information bits to a code's transmitted bits, and LLRs (or hard decisions) back to a `DecodeResult`; dispatched on the code object, the decoder chosen by a `method` object |
143
+ | `plot_*` | every public plotting function (only `apply_default_theme` is exempt) |
144
+
145
+ - The algorithm is chosen by an object, never by the function name or a string: `estimate_carrier_phase(y, BPS())`, not `recover_carrier_phase_bps(y)`. The verb function dispatches on the object's type through a private table in its module. There is no `recover_*` or `compensate_*`.
146
+ - Computations use plain nouns (`allan_deviation`).
147
+ - **Glossary**, one name each:
148
+ - `num_taps`, `step_size`, `sps`, `constellation`, `pulse`, `rng`, `num_symbols`, `num_channels` (never streams), `sampling_rate`, `symbol_rate`, `noise_var`, `nfft`, `regularization`, `max_iterations`.
149
+ - Noise has three names, each with one meaning: `noise_var` is the variance E|n|² of the noise in the samples a function receives (measured by `estimate_noise_variance`); `snr_db` a power ratio in those samples (measured by `snr`); `esn0_db` the channel's textbook Es/N0, independent of the dtype (real samples carry the in-phase half of the noise, `N0/2`). A function taking `noise_var` for a demapping model (`compute_llr`, `mi`, `gmi`, `ngmi`) uses the likelihood of complex or real noise after the samples' dtype and also accepts `esn0_db`, so users never convert by hand.
150
+ - A size names its unit: `num_symbols`, `num_blocks` (codewords of a FEC code), `info_bits` (a code's information length where it is a parameter), `blocks_per_frame`, `num_variables`, `num_segments`. In `coding`, `length` and `n` mean the codeword length in bits and `k` the information length, nothing else.
151
+ - A seeded part of a factory takes `<part>_rng` (`payload_rng`), with the semantics of `rng`.
152
+ - `method=` strings name only a numerical flavor of one algorithm (`method="maxlog" | "exact"`); algorithms are objects.
153
+ - Values are in SI units without a suffix (`linewidth` in Hz, `drift_rate` in rad/s, `taus` in s). A result holding one quantity in two units suffixes the SI one (`lags` in samples, `lag_s`). A conventional non-SI unit goes into the name: `_db`, `_dbm`, `_deg`, `_nm`, `_km`, `_ps_nm_km`. Normalized quantities say so (`normalized_bandwidth`). No second name for one quantity in another unit (no `center_frequency_ghz`); `center_wavelength_nm` is a different, derived quantity.
154
+
155
+ ### Docstrings
156
+
157
+ 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.
158
+
159
+ - **Summary line** on the line of the opening quotes, one line, ending with a period. Functions: imperative mood (`Apply ...`), or a noun phrase for a function named after what it returns (`evm`, `welch_psd`). Classes and modules: a noun phrase.
160
+ - **Module docstrings** name what the module holds (`:func:`, `:class:`), then the conventions particular to it (a sign convention, a scaling, a file layout). No marketing adjectives, no history of earlier layouts, no restatement of this file. A private module's summary ends with "(private)" and the body names its users.
161
+ - **Types:** inputs are `array_like`, or `array_like or Signal` where Signal-aware; returned arrays and fields are `ndarray` (on the input's device), `ndarray or Signal` for transforms, `numpy.ndarray` where always on the host. Shapes go in the description in double backticks: ``(C, N)``.
162
+ - **Plots** return `(fig, ax) or None`, multi-panel plots `(fig, axes) or None` (`None` with `show=True`).
163
+ - **Defaults:** `name : type, default value`; `name : type, optional` when the default is `None`, with the description saying what `None` does.
164
+ - Units go in the description; inline code in double backticks; library names through `:func:` / `:class:` / `:mod:` / `:meth:`.
165
+
166
+ ### Errors and logging
167
+
168
+ - Wrong input raises, with a message naming the argument.
169
+ - **No fallbacks for a broken environment.** Code never catches a failure of CuPy, a CUDA kernel (compile, load, launch), a graph capture or another dependency to run a different implementation; the failure raises to the caller. A path kept "just in case" is code no test or user runs. (Dispatch on the *input* is allowed; see §6 Kernel policy.)
170
+ - `logger` is for diagnostics only. Per-channel logging that needs a device-to-host transfer is wrapped in `if logger.isEnabledFor(logging.INFO):`.
171
+ - **Levels:** `DEBUG` traces what a call is about to do (method, parameters, sizes). `INFO` is one summary per public call of what a user would print next (the values found, the parameters applied, an equalizer's exit MSE, a decoder's failed checks, what `generate` or `io` produced). `WARNING` flags a returned but suspect result (no convergence, a fit outside its valid range, near-zero power, a code with 4-cycles). Value objects and pure helpers do not log.
172
+ - `warnings.warn` is used only for `PrecisionWarning` (§5). The library never configures handlers, Matplotlib or warning filters at import time.
173
+
174
+ ---
175
+
176
+ ## 5. Numerics
177
+
178
+ - **Storage dtypes:** `complex64` for IQ samples, `float32` for real signals; `generate`, frames and `load_npz` produce them, so single precision is the default path.
179
+ - **Precision contract.** Every function computes in a predictable precision, and none promotes single-precision data on its own:
180
+ - **Transforms keep the input's precision** throughout (filters, impairments, the fiber solver and DBP, resampling, delays, carrier-phase correction): complex64 in and out, complex128 in and out.
181
+ - **Fixed-precision blocks** compute in single precision whatever the input: the equalizers in complex64, `compute_llr` / `Constellation.llr` in float32. Double input emits `commkit.PrecisionWarning` (`_array.warn_if_double`, attributed to the caller's line). The test suite turns it into an error (`pytest_configure` in `tests/conftest.py`; not pyproject's `filterwarnings`, which imports commkit before coverage starts), so a library path feeding double data to these blocks fails CI. Training and reference symbols are not checked.
182
+ - **Deliberate float64 / complex128**, documented where it happens, only on phases, small arrays or accumulators, never on sample-rate data that is then cast back:
183
+ - phases before `xp.unwrap()` (float32 rounding causes spurious quadrant slips), phase trajectories and ramps (wrapped in float64, rotated in the data's precision), frequency grids and estimates;
184
+ - recursion state: PLL, Tikhonov/Kalman block transitions, the IIR filter's sections and state (poles near z = 1);
185
+ - RLS keeps `P`, the gain `k` and the regressor buffers in complex128 (in single precision `P` loses positive definiteness and the filter diverges);
186
+ - LMS/CMA/RDE accumulate dot products and gradient updates in complex128 around complex64 inputs and weights; the block equalizers stay complex64, as their tap sums run inside the FFTs;
187
+ - sums: metrics compute per element in the input's precision and accumulate in float64.
188
+ - 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.
189
+ - **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 to complex128. Combine single-precision data only with Python scalars or arrays cast to its precision. Frequency responses (dispersion, delays, Jones matrices) are computed at float64 phase accuracy and cast to the spectrum's precision before the multiply; rotate with `math._derotate`.
190
+ - **Audit with `tests/plugins/precision_trace.py`** after touching numerics. It lists every float64/complex128 array of sample-rate size that a NumPy path creates from single-precision, integer or bool data or from scratch, with its source line; output dtypes alone hide a compute-in-double-and-cast-back. Every reported site must be one of the deliberate uses above.
191
+ - **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
+
193
+ ---
194
+
195
+ ## 6. Performance
196
+
197
+ | Algorithm structure | Implementation | Device |
198
+ | --- | --- | --- |
199
+ | Vectorized: filters, resampling, spectra, impairments, metrics, frequency-domain equalizers | NumPy/CuPy via `x, xp, sp = dispatch(samples)` | follows the input |
200
+ | Per-symbol searches over a small table: LLRs (all points, or per axis for separable QAM; max-log without a prior in closed form), hard decisions without a square lattice | CPU: Numba `parallel=True` over blocks of symbols (LLRs), chunked NumPy (decisions). GPU: one thread per symbol (`llr`, `llr_separable`, `llr_separable_maxlog`, `bps_min_d2`'s nearest-point mode); decisions on double input or above 1024 points use the chunked search on both devices; LLRs above 16 bits per symbol raise on the GPU | follows the input |
201
+ | Sequential recursions: LMS/RLS/CMA/RDE, PLL, Tikhonov, cycle slips, the Mengali-Morelli bootstrap, per-codeword decoders on trellises and trees (CCDM, polar SC/SCL, Viterbi, BCJR, turbo, recursive systematic encoding) | Numba `@njit(cache=True, fastmath=True, nogil=True)`, independent rows in `prange`. The equalizers' kernel with inline carrier recovery compiles strict (unwrap, running sums) and calls fast-math tap and distance loops as separate functions (an `inline="always"` helper takes its caller's flags) | CPU; GPU input does one round trip |
202
+ | Linear recursions: IIR filtering | CPU: Numba without `fastmath`, so double input matches SciPy bit for bit. GPU: `iir` kernel: each block's state from a zero start in parallel, states carried across blocks in two levels (groups, then across groups), blocks filtered again, in double precision | follows the input |
203
+ | Binary linear algebra of codes: encoding, syndromes, ML-decoding correlations on the GPU, Chase's test patterns | one float32 GEMM per chunk (`coding._gf2.matmul_mod2`), exact below an inner dimension of 2^24, parity in place (CPU) or by one elementwise kernel writing int8 (GPU); small tables cached per code and device (`backend._device_tables`) | follows the input |
204
+ | Algebraic decoding (BCH, Reed-Solomon): Berlekamp-Massey, Chien search to the locator's degree, Forney | CPU: Numba `parallel=True` over codewords. GPU: `algebraic_correct`, one thread per codeword in integer table arithmetic, so both agree exactly (a lockstep CuPy version was 12x slower) | follows the input |
205
+ | Layered LDPC belief propagation (min-sum, sum-product) | CPU: Numba `parallel=True` over codewords, all iterations inside, no fast-math. GPU: `ldpc_layered`, one block per codeword, rows of a layer in parallel, early stop per codeword; same row order, so min-sum agrees bit for bit (a CuPy version was 7-23x slower) | follows the input |
206
+ | Maximum-likelihood decoding: the correlation with all `2^k` codewords, the decision and the per-bit maxima of the max-log soft output | CPU: one Numba pass, `parallel=True` over rows, branch-free selects under fast-math without the no-NaN and no-Inf flags (a GEMM then a separate pass ran 3x slower, its threads competing with BLAS's). GPU: the GEMM per chunk, the per-bit maxima by a CuPy `ReductionKernel` that broadcasts over the codewords instead of building `(rows, codewords, n)`. `soft=False` skips the maxima on both | follows the input |
207
+ | Monte Carlo analysis (EXIT curves, error-rate curves) | the existing encoders, decoders and demapper and vectorized NumPy/CuPy, through the private block functions (no INFO line per batch); closed-form parts are host NumPy on small grids. No kernel of its own | follows `device` |
208
+ | Elementwise chains between multi-core FFTs (fiber solver's Kerr and PMD stages) | NumPy: a Numba `parallel=True` pass competes with `scipy.fft`'s threads and measured slower end to end; adopt one only on a paired end-to-end A/B | CPU |
209
+ | Other hot spots limited by intermediates, launches or round trips | CUDA C++ via CuPy `RawModule` (`commkit/_cuda/src`) | GPU |
210
+
211
+ **Numba kernels live at module level** in a private module that a getter imports on first use (`equalization/_numba_loops.py`, `coding/_numba_loops.py`, `mapping/_numba_llr.py`), so `import commkit` does not load Numba and `cache=True` works across processes. A kernel defined inside a function that calls another kernel defined there recompiles in every process (Numba's cache key misses the captured dispatcher); a getter may build a closure that captures only constants (`_get_numba_sos`).
212
+
213
+ **Host-sync hygiene:**
214
+
215
+ - Never extract scalars from a possibly-GPU array inside a loop (`float(x[c])`, `.item()`). Compute the vector on the device, transfer it once with `to_device(v, "cpu")`, then loop on the host.
216
+ - Reduce on the device before transferring, including for plotting; prefer on-device gathers (`xp.take_along_axis`); chunk `(N, M)` intermediates over N with an on-device accumulator.
217
+ - Build grids, windows and index tables on the data's device (`xp.fft.fftfreq`, `xp.hanning`, `xp.arange`).
218
+ - Every FFT goes through `commkit._fft` (`fft`, `ifft`, `rfft`, `irfft`), never `np.fft` / `xp.fft` (frequency grids excepted): on the CPU it runs multi-threaded `scipy.fft` and keeps complex64 in single precision (NumPy 2 computes it in double); on the GPU it is `cupy.fft`.
219
+ - Pass whole records to compiled code. No Python loops over symbols or small blocks, except inside a CUDA graph.
220
+ - Repeated GPU work with fixed buffers (the block equalizers' blocks, the fiber solver's spans) is replayed from CUDA graphs through `_cuda.graphs.GraphRunner`: warm once, capture on the second run, replay; a failed capture raises. Per-replay offsets come from a device index the graph advances, so the host launches one graph per block. Its stream is reused per device and thread, because CuPy's pool caches freed blocks per stream.
221
+ - **Audit with `tests/plugins/transfer_trace.py`** after touching a GPU path; the contract registry cannot see a silent round trip through the host. The plugin records every sample-rate host-device copy during a `--device=gpu` run and fails on a site missing from its documented list (the sequential recursions' round trip, equalizer continuation state, the reporting layer, explicit device moves, the transmitted payload, host values such as reference symbols or SciPy windows moved to the device). A new site is fixed or added to the list with its reason.
222
+
223
+ **Kernel policy:**
224
+
225
+ - A `.cu` kernel needs a benchmark showing that CuPy is limited by intermediates, launch count or round trips. It optimizes a CPU reference (Numba or NumPy) and is tested against it with `--device=all`.
226
+ - `_cuda.get_kernel` returns the launch wrapper or raises (compile, load, bad specialization); it is called only on CuPy input.
227
+ - Wrappers check dtype, contiguity, shape and size before launch and never launch a zero-size grid: empty input returns an empty result, or raises where the caller never has one (`cs_block`, `bps_anchor`). Grid dimensions y and z stop at 65535; a kernel mapping channels or rows there walks the rest in a grid-stride loop (`bps_min_d2`).
228
+ - **Dispatch on input limits:** a kernel's documented limits (dtype, number of points, phases, channels) may route to the general implementation where it exists anyway (the CPU's), saying so where it happens. A GPU-only path serving only inputs beyond a kernel's limit is not written; such input raises (`compute_llr` above 16 bits per symbol).
229
+ - `float32` with explicit literals. `--use_fast_math` only per kernel (through `options`), after an accuracy check.
230
+ - Short elementwise chains use `ElementwiseKernel` (named `commkit_*`) or `cupy.fuse`.
231
+ - Sequential recursions over a whole record are never ported to the GPU (IIR, above, is the linear exception). 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; `bps_anchor`); keep its parallel part parallel, as float64 runs at 1/64 of float32 on consumer GPUs.
232
+ - A new or changed kernel passes `scripts/sanitize.sh` (memcheck, racecheck, synccheck, initcheck) before release; add its tests and kernel-name prefix to the script.
233
+
234
+ ---
235
+
236
+ ## 7. Tests
237
+
238
+ - **Fixtures:** use `backend_device`, `xp` and `xpt` from `tests/conftest.py`. Assert with `xpt.assert_allclose(...)` (`numpy.testing` raises on CuPy arrays); cast expectations with `xp.asarray` and reductions with `float(...)`.
239
+ - **No configuration skips.** The GPU legs skip only when CuPy is not installed, and fail instead under an explicit `--device=gpu`; a broken CuPy fails under any setting. Tests outside the selected run are deselected at collection (`gpu_only`, `cpu_only`, `slow` without `--run-slow`), so a run with CuPy reports no skips. A new test never calls `pytest.skip` for a configuration.
240
+ - **Layout mirrors the source:** `tests/<subpackage>/test_<module>.py`, `tests/test_<module>.py` for flat modules. Basenames are globally unique (prepend import mode), hence `channel/test_channel_linear.py`. Split a large file by concern. Shared builders live in `tests/common/` and use the public API.
241
+ - **Contract registry** (`tests/test_api_contracts.py`): every public function has a row, checked generically for array and Signal in/out, device preserved, input not mutated, keyword-only parameters, `TypeError` on unsupported arrays, fact conflicts and return rules. Do not repeat these checks per module.
242
+ - **Oracles:** sequential kernels are tested against plain-Python references in `tests/common/reference_impl.py`, CUDA kernels against their CPU reference. Kernel tests skip only on the CPU parametrization (`skip_unless_gpu`), so a kernel that fails to compile fails its tests.
243
+ - **Mathematical meaning:** test documented definitions (a pulse width, a dispersion sign, noise-variance scaling) against independently derived expectations, not only shapes and energies.
244
+ - **A test of what an algorithm achieves** (an equalizer converges, carrier recovery tracks, a decoder corrects, an estimator is calibrated) must fail on a plausibly wrong implementation:
245
+ - **Score against the truth**: the transmitted reference or the true phase, delay or offset, never the receiver's own decisions or a sibling estimate; constellation symmetry hides a wrong quadrant, a slip or a duplicated stream. Helpers are in `tests/common/scoring.py` (`ser`, `blind_ser`, `phase_rms`, `block_rotations`, `stream_correlation`).
246
+ - **Resolve only the legitimate ambiguities**, each once over the whole scored interval, never per segment: one rotation per stream (by the constellation's symmetry, or any angle for a phase-blind equalizer), one bijective channel permutation for blind MIMO, a documented fixed delay.
247
+ - **A baseline must fail**: the same test without the block it credits (step size 0, `cpr=None`, the corrector off) asserts failure.
248
+ - **Tolerances are derived** (AWGN theory, a Cramér–Rao bound, a Wiener or loop-noise prediction, a binomial interval), and the docstring states the derivation and the measured value. `tests/common/theory.py` holds the closed forms, in NumPy and SciPy only.
249
+ - **Real channels**: convergence tests use `make_channel_signal` (RRC pulses, ISI, fractional delay, polarization rotation, DGD, AWGN). The repeat-upsampled builders serve plumbing tests only.
250
+ - **Break the code to check the test**: monkeypatch a plausible wrong result (zeros, a factor of two, the feature off) and confirm the test fails.
251
+ - A strengthened test that exposes a library fault is marked `pytest.mark.xfail(strict=True, reason=...)` with the measured numbers and listed in §10; the fix lands in its own commit, which removes the mark.
252
+ - **Monte Carlo tests** state what their sample bounds (0 failures in 12 frames bounds the FER below 0.22 at 95 %) and check the operating point (the pre-FEC BER against theory). The default run uses a smoke size; a `slow` parametrization repeats it with enough trials to mean something.
253
+ - **End-to-end chains** in `tests/test_receiver_chain.py` run several impairments and their receiver, scored against the source bits or AWGN theory, each with one correction left out as a failing baseline.
254
+ - **Warnings are failures**: a WARNING from the `commkit` logger fails the test unless it is marked `allow_warnings` (it provokes a suspect result on purpose) or asserts the log through `caplog`. A WARNING that reports what the caller asked for is a log-level bug, fixed in the library.
255
+ - **API changes never change expected values.** A signature change only changes how tests call the code; numerical changes land in their own commit with independent validation.
256
+ - **Examples** are Jupyter notebooks in `examples/`, committed without outputs (the `nbstripout` filter, §2). `tests/test_examples.py` executes each one, so an API change updates the notebooks in the same commit.
257
+ - They form a course: one topic per notebook, `NN-lowercase-words.ipynb`, in teaching order (signals and DSP, channel and metrics, coding, receiver algorithms, fiber systems, characterization). Each opens with what it covers and links the notebooks it builds on, and ends with a link to the next. A new notebook takes its place in the order and updates `examples/README.md` and its neighbors' links.
258
+ - Every claim in the text matches what the cells print: run the notebook and read its numbers before committing. Keep each under about 20 s on the CPU.
259
+ - Plots of hand-computed arrays go through `to_device(x, "cpu")`, so the notebooks run unchanged on the GPU.
260
+ - **Coverage** must not drop below the CI floors: 78 % for the package, 85 % for the Numba kernel modules (the `NUMBA_DISABLE_JIT=1` job, §2).
261
+
262
+ ---
263
+
264
+ ## 8. Benchmarks
265
+
266
+ `benchmarks/` tracks the GPU-relevant hot paths. Baselines are committed under `benchmarks/baselines/`. The reference is `0008` (`v3_1`, the 3.1 state) on the reference machine: RTX 4070 Ti, Ryzen 7 7800X3D, WSL2. Earlier baselines lack later benchmark files; `0002` ran the CPR equalizer and Viterbi-Viterbi benchmarks on workloads that did not converge, so compare those only from `0003` on.
267
+
268
+ - **Workloads must converge**, or the benchmark times a failure mode (slip storms, divergence). Every equalizer benchmark asserts its symbol error rate with `benchutils.assert_converged`.
269
+ - **Workloads** come from `benchmarks/workloads.py` with fixed seeds; never generate data inline.
270
+ - **Baselines** are recorded with `benchmarks/record_baseline.py`: each file in its own process, best of `--passes` (default three; raise it when small benchmarks keep outliers, as `0008` did with ten). Never record from a single `pytest benchmarks/` run: small GPU benchmarks after the large equalizer workloads ran several times slower than alone.
271
+ - **IDs** are `[cpu]` or `[gpu]`, meaning the input device.
272
+ - **Timing:** timed bodies end with the `sync` fixture, or GPU timings measure launches. Each benchmark has one warmup round (compilation, pool growth), and a session fixture warms Numba's thread pool before the first.
273
+ - **Tools:** `benchmarks/benchutils.py` provides `CudaEventTimer` and `nvtx_range` for `nsys`. Logging is set to WARNING in `benchmarks/conftest.py`.
274
+ - **Trust deltas, not single runs.** Swings of ±20-40 % have been seen under load. Re-run a suspected regression in isolation, or do a paired A/B of at least 7 repetitions, alternating old and new. When even unchanged benchmarks move, time the changed part with `CudaEventTimer` and quote it against the step it sits in.
275
+ - A commit touching a hot path quotes its delta against `0008`. Tolerance is 5 % on CPU and 10 % on GPU.
276
+
277
+ ---
278
+
279
+ ## 9. Release
280
+
281
+ 1. On `main`: the CPU and GPU suites pass with no failures or skips, slow validation included (`uv run pytest -rs --run-slow`; strict xfails, if any, are listed in §10); `scripts/sanitize.sh` is clean if a kernel changed; `uv build` succeeds and `uv run --with twine twine check dist/*` passes.
282
+ 2. `CHANGELOG.md`: a `## [X.Y.Z] - YYYY-MM-DD` section 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.
283
+ 3. `uv run bump-my-version bump {patch|minor|major}` commits the new version and tags `vX.Y.Z`.
284
+ 4. `git push origin main --tags`. The ruleset on `main` requires pull requests; repository admins bypass it for these two commits.
285
+ 5. `gh release create vX.Y.Z --title vX.Y.Z --notes-file notes.md` with the changelog section as notes. Publishing the release runs `.github/workflows/publish.yml`, which uploads to PyPI through Trusted Publishing; a pushed tag alone publishes nothing.
286
+
287
+ ---
288
+
289
+ ## 10. Roadmap and open items
290
+
291
+ What is not done yet, in suggested order. Unlike the sections above, this is a to-do list: remove an item in the commit that resolves it, and add one when a known gap is left open.
292
+
293
+ ### Switch to Python 3.15 (blocked)
294
+
295
+ Goal: require Python 3.15 only, mainly for explicit lazy imports (PEP 810). State checked on 2026-10-23 (uv 0.13.5, CPython 3.15.1):
296
+
297
+ - **Blocker: CuPy has no 3.15 wheels.** `cupy-cuda13x` 14.2.0 ships cp310-cp314 only; support is tracked in cupy/cupy#10259, milestone v14.3.0.
298
+ - **Blocker: `jupyter_client` fails to import on 3.15**: `jsonutil.py` calls `strptime` with a day and no year, which 3.15 rejects (cpython#70647), so `tests/test_examples.py` fails at collection. Wait for a fixed release; do not work around it in commkit.
299
+ - **Ready:** wheels on 3.15 exist for NumPy 2.5.4, SciPy 1.18.1, Numba 0.68.0, Matplotlib 3.11.2, mypy 2.4.0 and the dev tools. The CPU suite has not been run on 3.15.
300
+
301
+ Steps, each its own commit (the version switch first, then each refactor):
302
+
303
+ 1. **Validate unchanged code** in a 3.15 venv: full CPU+GPU suite with notebooks (`-rs`), benchmark smoke run, kernel-coverage job, precision audit.
304
+ 2. **Switch the floor:** `requires-python = ">=3.15"`, classifiers, ruff `target-version`, dependency minimums to the first 3.15 wheels (`cupy-cuda13x[ctk]>=14.3`, `numba>=0.68`, `numpy>=2.5`, `scipy>=1.18`; recheck each); `ci.yml` matrix and jobs and `publish.yml` on 3.15 (keep `locked` and `lowest-direct`); `uv lock`; README and §2. Record a 3.15 baseline, compare it with `0008` by a paired A/B on unchanged code, and make it the reference.
305
+ 3. **Lazy imports.** `lazy` is module-level only (not in functions, classes or `try`, not with `*`); confirm on the interpreter.
306
+ - Move the function-local imports (`import cupy as cp`, `import numba`, `from . import compiler` in `_cuda`, cycle-breaking imports of `Constellation`, `Signal`, `normalize`) to `lazy import` / `lazy from`; find them with `grep -rnE '^\s{4,}(import|from) ' commkit`.
307
+ - CuPy stays optional: every use stays behind `is_cupy_available` / `dispatch`; decide per site whether `backend.py`'s probes stay eager inside `try`.
308
+ - Check that a lazy `numba` does not move the import cost to an odd first call, and turn `TYPE_CHECKING` blocks into `lazy from` where the name is useful at run time.
309
+ - Measure with `benchmarks/bench_import.py`; keep `tests/test_import_hygiene.py` passing and extend it to assert that `import commkit` loads neither CuPy nor Matplotlib, if that becomes true.
310
+ 4. **Other cleanups** (verify each against What's New in 3.14 and 3.15): drop `from __future__ import annotations` (PEP 649/749; check nothing reads `__annotations__` as strings); check `open()` without `encoding=` under default UTF-8 mode (PEP 686); grep for `strptime` with a day and no year; try the sampling profiler (PEP 799) on the CPU hot paths and mention it in §8 if useful.
311
+
312
+ ### Channel coding
313
+
314
+ 1. **oFEC** (OpenROADM, ITU-T G.709.3) is not implemented: its block structure is specified in OpenROADM MSA documents that were not available in a form to implement from. It would build on `BCHCode` (extended BCH(256, 239)), soft-output `Chase` and the staircase decoder's windowed iteration.
315
+ 2. **NR rate matching for polar codes** (sub-block interleaving, circular buffer, puncturing and shortening with their frozen-set rules, parity-check bits) and **LTE rate matching for turbo codes** are not included: `PolarCode` and `TurboCode` are the mother codes; `Punctured` and `Shortened` cover generic rate matching.
316
+
317
+ ### Test suite
318
+
319
+ - **Standard tables against the standards.** The NR polar reliability sequence (TS 38.212 Table 5.3.1.2-1), the LTE QPP interleaver parameters (TS 36.212 Table 5.1.3-3) and the DVB-S2 and PON parity-check address tables are tested against commkit's own tables. They should be compared in full with fixtures under `tests/data/standards/` transcribed from the specifications, each with a provenance header (document, version, table). ETSI's download pages refuse scripted access; the documents have to be supplied.
320
+
321
+ ### Nonlinear fiber channel
322
+
323
+ Each is an addition that leaves the current API unchanged.
324
+
325
+ - **Batched realizations**: a leading axis `(B, 2, N)` (launch-power sweeps, Monte Carlo over seeds) so cuFFT batches them and short records fill the GPU.
326
+ - **Adaptive step rules**: a `step_rule=` object (maximum nonlinear phase per step, local-error method) next to the equal `steps_per_span`, which measured 3x more accurate than logarithmic steps on a 32 GBd, 80 km span.
327
+ - **WDM helpers**: combining frequency-shifted channels into one wideband field and selecting one after propagation; the solver already handles the wide field (with `dispersion_slope_ps_nm2_km` set).
328
+ - **Distributed (Raman) amplification and per-span fiber types** in one call (chaining calls already works: every call speaks launch-relative units).
329
+ - **cuFFT callbacks** fusing the linear stage into the transforms (CuPy's support is experimental and compiles with nvcc at run time).
330
+ - **Self-steepening and Raman scattering** for ultra-wideband fields.
@@ -4,6 +4,38 @@ Notable changes to CommKit, newest first. Performance figures are from the refer
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [3.4.0] - 2026-11-03
8
+
9
+ Error-rate curves for every code, decoders that say what they checked, and one noise convention for real and complex samples.
10
+
11
+ ### Changes that may need code updates
12
+
13
+ - Real-valued samples (PAM, intensity modulation) now follow one noise convention, so their LLRs and information rates are right. Complex samples are unaffected.
14
+ - `apply_awgn(esn0_db=)` adds half the noise variance to real samples: the in-phase half of the channel's noise. A constellation's error rates at an Es/N0 no longer depend on whether its samples are real or complex, and BPSK has `BER = Q(sqrt(2 Es/N0))`. For a power ratio in the samples, as measurement code means it, use the new `apply_awgn(snr_db=)`; it adds what `esn0_db` used to add to real samples at one sample per symbol.
15
+ - `compute_llr`, `Constellation.llr`, `mi`, `gmi` and `ngmi` use the likelihood of real noise for real symbols. With the `noise_var` that `estimate_noise_variance` measures, real-symbol LLRs used to be twice too confident.
16
+ - `DecodeResult` has a new field, `checked`: whether `valid` comes from a check that can fail. Code that builds a `DecodeResult` itself must pass it. For decoders without a check, `valid` stays `True` everywhere, as before.
17
+
18
+ ### Added
19
+
20
+ - `coding.error_rate_curve(code, constellation=, esn0_db=, ...)` measures a code's BER, FER and pre-FEC BER over AWGN by Monte Carlo. Each point stops after `min_block_errors` block errors or `max_blocks` blocks. It returns an `ErrorRateCurve` of counts, which you can also build from your own simulation loop.
21
+ - `plotting.plot_error_rate(curves)`: BER and FER against Es/N0 or Eb/N0, with 95 % intervals on the FER and the pre-FEC BER for reference, plus a panel splitting failures into detected and undetected ones.
22
+ - Soft output from more decoders: `TurboIterations` (a-posteriori LLRs of the last iteration), `MaximumLikelihood` (max-log) and `Concatenated` (the inner decoder's LLRs). `decoder_exit_curve` now covers turbo and maximum-likelihood decoding.
23
+ - `Chase(..., soft=False)` and `MaximumLikelihood(soft=False)` skip the soft output when only the decisions matter (`llrs` is `None`, the bits are the same).
24
+ - `compute_llr`, `mi`, `gmi` and `ngmi` take `esn0_db=` instead of `noise_var`, and convert it for real or complex symbols.
25
+ - `plot_llr_distribution(res, sig)` takes a `DecodeResult` and the transmitted Signal or Reference, as `ber` does.
26
+ - The example notebooks draw with the library's plot functions wherever one exists, and show spectra, constellations and eye diagrams before and after each block. Notebook 06 explains Es/N0, SNR and noise variance for real and complex samples; notebook 07 measures error-rate curves and the decoding outcome, compares the polar, convolutional and turbo codes against Eb/N0 and the turbo code at three block sizes; notebook 21 charts the optical standards' thresholds against their overhead.
27
+
28
+ ### Performance
29
+
30
+ - `MaximumLikelihood` searches the codewords in one Numba pass on the CPU. For 16384 Hamming(16, 11) words (paired A/B against 3.3.0, 7 runs), decisions alone (`soft=False`) take 0.62x the time on the CPU and 1.0x on the GPU; with the new soft output, 1.55x and 4.3x.
31
+ - `Chase(soft=False)`: 0.44x the time of the default on the CPU and 0.74x on the GPU (400ZR Hamming(128, 119), 4 test positions, 4096 words).
32
+ - Turbo decoding is unchanged (1.00), now with its soft output.
33
+
34
+ ### Development
35
+
36
+ - `metrics` is a package (`quality`, `error_rates`, `information`); every name is still imported from `commkit.metrics`.
37
+ - The transfer audit lists the payload moves of the EXIT and error-rate curves, which it flagged as undocumented; the kernel-coverage job runs the maximum-likelihood tests.
38
+
7
39
  ## [3.3.0] - 2026-11-03
8
40
 
9
41
  Numerical filter responses and plots built for comparisons. `filter_response`, `impulse_response` and `step_response` evaluate any FIR, SOS or `(b, a)` filter on its device; the multi-panel diagnostics draw one selected panel, and every plot can overlay several results on one axis. The example notebooks explain what each result shows.
@@ -363,7 +395,8 @@ See the [GitHub release](https://github.com/lokgar/commkit/releases/tag/v1.1.0)
363
395
 
364
396
  First release. See the [GitHub release](https://github.com/lokgar/commkit/releases/tag/v1.0.0).
365
397
 
366
- [Unreleased]: https://github.com/lokgar/commkit/compare/v3.3.0...HEAD
398
+ [Unreleased]: https://github.com/lokgar/commkit/compare/v3.4.0...HEAD
399
+ [3.4.0]: https://github.com/lokgar/commkit/compare/v3.3.0...v3.4.0
367
400
  [3.3.0]: https://github.com/lokgar/commkit/compare/v3.2.1...v3.3.0
368
401
  [3.2.1]: https://github.com/lokgar/commkit/compare/v3.2.0...v3.2.1
369
402
  [3.2.0]: https://github.com/lokgar/commkit/compare/v3.1.3...v3.2.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: commkit
3
- Version: 3.3.0
3
+ Version: 3.4.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
@@ -31,7 +31,7 @@ Matplotlib, change warning filters, or touch the GPU.
31
31
  import importlib
32
32
  from typing import Any
33
33
 
34
- __version__ = "3.3.0"
34
+ __version__ = "3.4.0"
35
35
 
36
36
  from .backend import PrecisionWarning
37
37
  from .core import (
@@ -27,7 +27,8 @@ permutations: :func:`interleave` and :func:`deinterleave` with an
27
27
  :class:`ExitCurve` objects, from :func:`decoder_exit_curve` (any soft-output
28
28
  decoder), :func:`demapper_exit_curve` (iterative demapping) or
29
29
  :func:`ldpc_exit_curves` (LDPC nodes, closed form, with
30
- :func:`ldpc_threshold`).
30
+ :func:`ldpc_threshold`). Error rates over AWGN: :func:`error_rate_curve`,
31
+ an :class:`ErrorRateCurve` of counts by Monte Carlo.
31
32
  """
32
33
 
33
34
  from .algebraic import BCHCode, BerlekampMassey, ReedSolomonCode
@@ -46,6 +47,7 @@ from .exit import (
46
47
  from .interleaving import Interleaver, deinterleave, interleave
47
48
  from .ldpc import LDPCCode, MinSum, SumProduct
48
49
  from .linear import Chase, LinearBlockCode, MaximumLikelihood, Syndrome
50
+ from .performance import ErrorRateCurve, error_rate_curve
49
51
  from .polar import SC, SCL, PolarCode
50
52
  from .product import Concatenated, HardIterations, Stages, StaircaseCode
51
53
  from .ratematch import Punctured, Shortened
@@ -62,6 +64,7 @@ __all__ = [
62
64
  "Concatenated",
63
65
  "ConvolutionalCode",
64
66
  "DecodeResult",
67
+ "ErrorRateCurve",
65
68
  "ExitChart",
66
69
  "ExitCurve",
67
70
  "HardIterations",
@@ -87,6 +90,7 @@ __all__ = [
87
90
  "decode",
88
91
  "deinterleave",
89
92
  "encode",
93
+ "error_rate_curve",
90
94
  "decoder_exit_curve",
91
95
  "demapper_exit_curve",
92
96
  "exit_chart",
@@ -145,6 +145,49 @@ def ldpc_layered( # type: ignore[no-untyped-def]
145
145
  valid[b] = ok
146
146
 
147
147
 
148
+ # -----------------------------------------------------------------------------
149
+ # Maximum-likelihood soft output
150
+ # -----------------------------------------------------------------------------
151
+
152
+
153
+ # Fast-math without the no-NaN and no-Inf assumptions: -inf marks an empty
154
+ # maximum. Branch-free selects under it measured 2.3x faster than
155
+ # branching on the bit under full fast-math.
156
+ _SELECT_FASTMATH = {"nsz", "arcp", "contract", "afn", "reassoc"}
157
+
158
+
159
+ @numba.njit(cache=True, fastmath=_SELECT_FASTMATH, nogil=True, parallel=True)
160
+ def ml_search(x, signs, ones, w0, soft, best_metric, best, m0, m1): # type: ignore[no-untyped-def]
161
+ """Correlate the rows of ``x`` with codewords ``w0 + w`` (signs ``(W, n)``
162
+ and bits ``ones``), updating each row's best metric and index and, with
163
+ ``soft``, per bit the best metric with it 0 (``m0``) and 1 (``m1``)."""
164
+ num_words, n = signs.shape
165
+ neg = np.float32(-np.inf)
166
+ for r in numba.prange(x.shape[0]): # type: ignore[attr-defined,no-untyped-call]
167
+ xr = x[r]
168
+ a0 = m0[r].copy()
169
+ a1 = m1[r].copy()
170
+ bv = best_metric[r]
171
+ bi = best[r]
172
+ for w in range(num_words):
173
+ v = np.float32(0.0)
174
+ for j in range(n):
175
+ v += signs[w, j] * xr[j]
176
+ if v > bv:
177
+ bv = v
178
+ bi = w0 + w
179
+ if not soft:
180
+ continue
181
+ for j in range(n):
182
+ one = ones[w, j]
183
+ a1[j] = max(a1[j], v if one else neg)
184
+ a0[j] = max(a0[j], neg if one else v)
185
+ best_metric[r] = bv
186
+ best[r] = bi
187
+ m0[r] = a0
188
+ m1[r] = a1
189
+
190
+
148
191
  # -----------------------------------------------------------------------------
149
192
  # Polar list decoding
150
193
  # -----------------------------------------------------------------------------
@@ -628,8 +671,13 @@ def turbo_encode(bits, pi, nxt, outputs, tail, out): # type: ignore[no-untyped-
628
671
 
629
672
 
630
673
  @numba.njit(cache=True, nogil=True, parallel=True)
631
- def turbo_decode(llrs, pi, nxt, outputs, tail, iterations, exact, scale, out): # type: ignore[no-untyped-def]
632
- """Iterative turbo decoding of the rows of ``llrs`` (see ``_get_numba_turbo``)."""
674
+ def turbo_decode(llrs, pi, nxt, outputs, tail, iterations, exact, scale, out, out_llrs): # type: ignore[no-untyped-def]
675
+ """Iterative turbo decoding of the rows of ``llrs`` (see ``_get_numba_turbo``).
676
+
677
+ ``out_llrs`` receives the a-posteriori LLRs of the transmitted bits from
678
+ the last iteration: the systematic bits from the second decoder, each
679
+ parity stream and its tail from the decoder that produced it.
680
+ """
633
681
  k = pi.shape[0]
634
682
  steps = k + 3
635
683
  forced = np.zeros(steps, dtype=np.bool_)
@@ -662,23 +710,43 @@ def turbo_decode(llrs, pi, nxt, outputs, tail, iterations, exact, scale, out):
662
710
  app1 = np.empty(steps)
663
711
  app2 = np.empty(steps)
664
712
  no_app_c = np.empty((0, 2)) # the iterations use the inputs' LLRs only
713
+ app_c1 = np.empty((steps, 2)) # the outputs' LLRs, in the last one
714
+ app_c2 = np.empty((steps, 2))
665
715
  alpha = np.empty((steps + 1, nxt.shape[0]))
666
716
  beta = np.empty((steps + 1, nxt.shape[0]))
667
717
  gamma = np.empty((steps, nxt.shape[0], 2))
668
- for _ in range(iterations):
669
- bcjr(ch1, la1, nxt, outputs, tail, forced, 0, 0, exact, app1, no_app_c,
718
+ for it in range(iterations):
719
+ last = it == iterations - 1
720
+ c1 = app_c1 if last else no_app_c
721
+ c2 = app_c2 if last else no_app_c
722
+ bcjr(ch1, la1, nxt, outputs, tail, forced, 0, 0, exact, app1, c1,
670
723
  alpha, beta, gamma) # fmt: skip
671
724
  for t in range(k):
672
725
  # Extrinsic of decoder 1, kept in la1 (natural order).
673
726
  la1[t] = scale * (app1[t] - la1[t] - ch1[t, 0])
674
727
  for t in range(k):
675
728
  la2[t] = la1[pi[t]]
676
- bcjr(ch2, la2, nxt, outputs, tail, forced, 0, 0, exact, app2, no_app_c,
729
+ bcjr(ch2, la2, nxt, outputs, tail, forced, 0, 0, exact, app2, c2,
677
730
  alpha, beta, gamma) # fmt: skip
678
731
  for t in range(k):
679
732
  la1[pi[t]] = scale * (app2[t] - la2[t] - ch2[t, 0])
680
733
  for t in range(k):
681
734
  out[row, pi[t]] = 1 if app2[t] < 0 else 0
735
+ o0 = out_llrs[row, : k + 4]
736
+ o1 = out_llrs[row, k + 4 : 2 * k + 8]
737
+ o2 = out_llrs[row, 2 * k + 8 :]
738
+ for t in range(k):
739
+ o0[pi[t]] = app2[t]
740
+ o1[t] = app_c1[t, 1]
741
+ o2[t] = app_c2[t, 1]
742
+ # The tails in the order the channel LLRs were read above.
743
+ for j, c in ((0, app_c1), (2, app_c2)):
744
+ o0[k + j], o1[k + j], o2[k + j] = c[k, 0], c[k, 1], c[k + 1, 0]
745
+ o0[k + j + 1], o1[k + j + 1], o2[k + j + 1] = (
746
+ c[k + 1, 1],
747
+ c[k + 2, 0],
748
+ c[k + 2, 1],
749
+ )
682
750
 
683
751
 
684
752
  # -----------------------------------------------------------------------------
@@ -474,6 +474,8 @@ for _cls, _dec, _methods in (
474
474
  decode=_dec,
475
475
  default_method=lambda code: BerlekampMassey(),
476
476
  methods=_methods,
477
+ # Chase is checked when its hard decoder (BerlekampMassey) is.
478
+ checked=lambda code, method: True,
477
479
  info_positions=lambda code: np.arange(code.k),
478
480
  ),
479
481
  )