commkit 3.1.3__tar.gz → 3.2.1__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 (294) hide show
  1. {commkit-3.1.3 → commkit-3.2.1}/AGENTS.md +21 -3
  2. {commkit-3.1.3 → commkit-3.2.1}/CHANGELOG.md +45 -2
  3. {commkit-3.1.3 → commkit-3.2.1}/PKG-INFO +3 -3
  4. {commkit-3.1.3 → commkit-3.2.1}/README.md +2 -2
  5. {commkit-3.1.3 → commkit-3.2.1}/benchmarks/bench_block_blind.py +13 -6
  6. {commkit-3.1.3 → commkit-3.2.1}/benchmarks/bench_fiber.py +6 -4
  7. {commkit-3.1.3 → commkit-3.2.1}/benchmarks/benchutils.py +11 -3
  8. {commkit-3.1.3 → commkit-3.2.1}/benchmarks/conftest.py +14 -0
  9. {commkit-3.1.3 → commkit-3.2.1}/commkit/__init__.py +1 -1
  10. {commkit-3.1.3 → commkit-3.2.1}/commkit/coding/__init__.py +21 -1
  11. {commkit-3.1.3 → commkit-3.2.1}/commkit/coding/base.py +23 -1
  12. commkit-3.2.1/commkit/coding/exit.py +946 -0
  13. {commkit-3.1.3 → commkit-3.2.1}/commkit/coding/ldpc.py +3 -0
  14. {commkit-3.1.3 → commkit-3.2.1}/commkit/coding/linear.py +3 -1
  15. {commkit-3.1.3 → commkit-3.2.1}/commkit/core/frame.py +14 -0
  16. {commkit-3.1.3 → commkit-3.2.1}/commkit/equalization/_block/_blind.py +1 -1
  17. {commkit-3.1.3 → commkit-3.2.1}/commkit/equalization/_block/_engine.py +1 -1
  18. {commkit-3.1.3 → commkit-3.2.1}/commkit/equalization/_common.py +2 -2
  19. {commkit-3.1.3 → commkit-3.2.1}/commkit/equalization/_numba_loops.py +4 -0
  20. {commkit-3.1.3 → commkit-3.2.1}/commkit/equalization/blind.py +4 -2
  21. {commkit-3.1.3 → commkit-3.2.1}/commkit/equalization/result.py +3 -1
  22. {commkit-3.1.3 → commkit-3.2.1}/commkit/equalization/sequential/_dd.py +20 -10
  23. {commkit-3.1.3 → commkit-3.2.1}/commkit/equalization/sequential/_setup.py +3 -1
  24. {commkit-3.1.3 → commkit-3.2.1}/commkit/filtering/linear.py +7 -5
  25. {commkit-3.1.3 → commkit-3.2.1}/commkit/frequency.py +0 -4
  26. {commkit-3.1.3 → commkit-3.2.1}/commkit/impairments/frontend.py +8 -2
  27. {commkit-3.1.3 → commkit-3.2.1}/commkit/io.py +3 -0
  28. {commkit-3.1.3 → commkit-3.2.1}/commkit/plotting/__init__.py +6 -0
  29. {commkit-3.1.3 → commkit-3.2.1}/commkit/plotting/analysis.py +8 -11
  30. commkit-3.2.1/commkit/plotting/coding.py +361 -0
  31. {commkit-3.1.3 → commkit-3.2.1}/commkit/plotting/equalizer.py +2 -4
  32. {commkit-3.1.3 → commkit-3.2.1}/commkit/plotting/eye.py +1 -1
  33. {commkit-3.1.3 → commkit-3.2.1}/commkit/plotting/filter_response.py +4 -3
  34. {commkit-3.1.3 → commkit-3.2.1}/commkit/plotting/spectral.py +3 -3
  35. {commkit-3.1.3 → commkit-3.2.1}/commkit/plotting/sync.py +2 -2
  36. {commkit-3.1.3 → commkit-3.2.1}/commkit/plotting/theme.py +12 -10
  37. {commkit-3.1.3 → commkit-3.2.1}/commkit/plotting/waveform.py +1 -1
  38. {commkit-3.1.3 → commkit-3.2.1}/commkit/recovery/corrections.py +24 -6
  39. {commkit-3.1.3 → commkit-3.2.1}/commkit/recovery/pll.py +28 -3
  40. {commkit-3.1.3 → commkit-3.2.1}/commkit/recovery/tikhonov.py +38 -4
  41. {commkit-3.1.3 → commkit-3.2.1}/commkit/timing.py +3 -3
  42. {commkit-3.1.3 → commkit-3.2.1}/examples/00-getting-started.ipynb +18 -4
  43. {commkit-3.1.3 → commkit-3.2.1}/examples/06-awgn-and-performance-metrics.ipynb +43 -28
  44. {commkit-3.1.3 → commkit-3.2.1}/examples/07-channel-coding.ipynb +133 -6
  45. {commkit-3.1.3 → commkit-3.2.1}/examples/17-blind-equalization.ipynb +3 -3
  46. {commkit-3.1.3 → commkit-3.2.1}/examples/README.md +2 -2
  47. {commkit-3.1.3 → commkit-3.2.1}/pyproject.toml +4 -2
  48. commkit-3.2.1/tests/analysis/test_allan.py +75 -0
  49. {commkit-3.1.3 → commkit-3.2.1}/tests/analysis/test_linewidth.py +3 -0
  50. {commkit-3.1.3 → commkit-3.2.1}/tests/coding/test_coding_base.py +27 -0
  51. commkit-3.2.1/tests/coding/test_exit.py +258 -0
  52. {commkit-3.1.3 → commkit-3.2.1}/tests/coding/test_ldpc.py +24 -6
  53. commkit-3.2.1/tests/coding/test_ldpc_exit.py +172 -0
  54. {commkit-3.1.3 → commkit-3.2.1}/tests/coding/test_product.py +87 -20
  55. {commkit-3.1.3 → commkit-3.2.1}/tests/coding/test_ratematch.py +30 -9
  56. {commkit-3.1.3 → commkit-3.2.1}/tests/coding/test_shaping.py +12 -5
  57. {commkit-3.1.3 → commkit-3.2.1}/tests/coding/test_turbo.py +41 -17
  58. {commkit-3.1.3 → commkit-3.2.1}/tests/common/__init__.py +2 -2
  59. {commkit-3.1.3 → commkit-3.2.1}/tests/common/metrics.py +0 -27
  60. commkit-3.2.1/tests/common/scoring.py +129 -0
  61. {commkit-3.1.3 → commkit-3.2.1}/tests/common/signals.py +91 -13
  62. commkit-3.2.1/tests/common/theory.py +84 -0
  63. {commkit-3.1.3 → commkit-3.2.1}/tests/conftest.py +64 -7
  64. {commkit-3.1.3 → commkit-3.2.1}/tests/core/test_frame.py +11 -43
  65. {commkit-3.1.3 → commkit-3.2.1}/tests/core/test_signal.py +13 -34
  66. {commkit-3.1.3 → commkit-3.2.1}/tests/equalization/test_blind.py +99 -66
  67. {commkit-3.1.3 → commkit-3.2.1}/tests/equalization/test_block.py +177 -333
  68. {commkit-3.1.3 → commkit-3.2.1}/tests/equalization/test_cpr.py +241 -291
  69. {commkit-3.1.3 → commkit-3.2.1}/tests/equalization/test_equalizer_oracles.py +14 -11
  70. {commkit-3.1.3 → commkit-3.2.1}/tests/equalization/test_linear.py +65 -89
  71. commkit-3.2.1/tests/equalization/test_mimo.py +164 -0
  72. {commkit-3.1.3 → commkit-3.2.1}/tests/equalization/test_polarization.py +13 -0
  73. commkit-3.2.1/tests/equalization/test_sequential.py +662 -0
  74. {commkit-3.1.3 → commkit-3.2.1}/tests/equalization/test_state.py +2 -0
  75. {commkit-3.1.3 → commkit-3.2.1}/tests/equalization/test_winit.py +36 -151
  76. {commkit-3.1.3 → commkit-3.2.1}/tests/impairments/channel/test_channel_linear.py +68 -69
  77. {commkit-3.1.3 → commkit-3.2.1}/tests/impairments/channel/test_channel_nonlinear.py +2 -0
  78. {commkit-3.1.3 → commkit-3.2.1}/tests/impairments/test_frontend.py +0 -42
  79. {commkit-3.1.3 → commkit-3.2.1}/tests/impairments/test_noise.py +0 -7
  80. {commkit-3.1.3 → commkit-3.2.1}/tests/impairments/test_source.py +0 -12
  81. {commkit-3.1.3 → commkit-3.2.1}/tests/mapping/test_bits.py +0 -16
  82. {commkit-3.1.3 → commkit-3.2.1}/tests/plotting/test_plot_analysis.py +1 -0
  83. commkit-3.2.1/tests/plotting/test_plot_coding.py +182 -0
  84. {commkit-3.1.3 → commkit-3.2.1}/tests/plotting/test_plot_filter_response.py +2 -0
  85. {commkit-3.1.3 → commkit-3.2.1}/tests/plotting/test_plot_spectral.py +4 -1
  86. {commkit-3.1.3 → commkit-3.2.1}/tests/plotting/test_plot_sync.py +2 -0
  87. {commkit-3.1.3 → commkit-3.2.1}/tests/plotting/test_plot_waveform.py +2 -1
  88. {commkit-3.1.3 → commkit-3.2.1}/tests/recovery/test_bps.py +35 -64
  89. {commkit-3.1.3 → commkit-3.2.1}/tests/recovery/test_corrections.py +34 -38
  90. {commkit-3.1.3 → commkit-3.2.1}/tests/recovery/test_pilots.py +14 -57
  91. {commkit-3.1.3 → commkit-3.2.1}/tests/recovery/test_pll.py +24 -47
  92. {commkit-3.1.3 → commkit-3.2.1}/tests/recovery/test_tikhonov.py +129 -106
  93. {commkit-3.1.3 → commkit-3.2.1}/tests/recovery/test_viterbi_viterbi.py +38 -85
  94. {commkit-3.1.3 → commkit-3.2.1}/tests/test_api_contracts.py +17 -0
  95. {commkit-3.1.3 → commkit-3.2.1}/tests/test_filtering.py +75 -37
  96. {commkit-3.1.3 → commkit-3.2.1}/tests/test_frequency.py +72 -68
  97. {commkit-3.1.3 → commkit-3.2.1}/tests/test_io.py +16 -4
  98. {commkit-3.1.3 → commkit-3.2.1}/tests/test_metrics.py +51 -34
  99. {commkit-3.1.3 → commkit-3.2.1}/tests/test_multirate.py +96 -36
  100. {commkit-3.1.3 → commkit-3.2.1}/tests/test_pulse_shaping.py +23 -32
  101. commkit-3.2.1/tests/test_receiver_chain.py +244 -0
  102. {commkit-3.1.3 → commkit-3.2.1}/tests/test_spectral.py +83 -0
  103. {commkit-3.1.3 → commkit-3.2.1}/tests/test_timing.py +54 -39
  104. {commkit-3.1.3 → commkit-3.2.1}/uv.lock +1 -1
  105. commkit-3.1.3/tests/analysis/test_allan.py +0 -34
  106. commkit-3.1.3/tests/equalization/test_mimo.py +0 -218
  107. commkit-3.1.3/tests/equalization/test_sequential.py +0 -1170
  108. {commkit-3.1.3 → commkit-3.2.1}/.gitattributes +0 -0
  109. {commkit-3.1.3 → commkit-3.2.1}/.github/workflows/ci.yml +0 -0
  110. {commkit-3.1.3 → commkit-3.2.1}/.github/workflows/publish.yml +0 -0
  111. {commkit-3.1.3 → commkit-3.2.1}/.gitignore +0 -0
  112. {commkit-3.1.3 → commkit-3.2.1}/.python-version +0 -0
  113. {commkit-3.1.3 → commkit-3.2.1}/LICENSE +0 -0
  114. {commkit-3.1.3 → commkit-3.2.1}/benchmarks/baselines/Linux-CPython-3.14-64bit/0002_pre_v2.json +0 -0
  115. {commkit-3.1.3 → commkit-3.2.1}/benchmarks/baselines/Linux-CPython-3.14-64bit/0003_v2_0.json +0 -0
  116. {commkit-3.1.3 → commkit-3.2.1}/benchmarks/baselines/Linux-CPython-3.14-64bit/0004_fiber_dbp.json +0 -0
  117. {commkit-3.1.3 → commkit-3.2.1}/benchmarks/baselines/Linux-CPython-3.14-64bit/0005_gpu_decoder_path.json +0 -0
  118. {commkit-3.1.3 → commkit-3.2.1}/benchmarks/baselines/Linux-CPython-3.14-64bit/0006_perf_cleanup.json +0 -0
  119. {commkit-3.1.3 → commkit-3.2.1}/benchmarks/baselines/Linux-CPython-3.14-64bit/0007_v3_0.json +0 -0
  120. {commkit-3.1.3 → commkit-3.2.1}/benchmarks/baselines/Linux-CPython-3.14-64bit/0008_v3_1.json +0 -0
  121. {commkit-3.1.3 → commkit-3.2.1}/benchmarks/bench_block_lms.py +0 -0
  122. {commkit-3.1.3 → commkit-3.2.1}/benchmarks/bench_bps.py +0 -0
  123. {commkit-3.1.3 → commkit-3.2.1}/benchmarks/bench_coding.py +0 -0
  124. {commkit-3.1.3 → commkit-3.2.1}/benchmarks/bench_equalizers.py +0 -0
  125. {commkit-3.1.3 → commkit-3.2.1}/benchmarks/bench_filters.py +0 -0
  126. {commkit-3.1.3 → commkit-3.2.1}/benchmarks/bench_import.py +0 -0
  127. {commkit-3.1.3 → commkit-3.2.1}/benchmarks/bench_llr.py +0 -0
  128. {commkit-3.1.3 → commkit-3.2.1}/benchmarks/bench_mapping.py +0 -0
  129. {commkit-3.1.3 → commkit-3.2.1}/benchmarks/bench_signal_pipeline.py +0 -0
  130. {commkit-3.1.3 → commkit-3.2.1}/benchmarks/bench_sync_misc.py +0 -0
  131. {commkit-3.1.3 → commkit-3.2.1}/benchmarks/record_baseline.py +0 -0
  132. {commkit-3.1.3 → commkit-3.2.1}/benchmarks/workloads.py +0 -0
  133. {commkit-3.1.3 → commkit-3.2.1}/commkit/_array.py +0 -0
  134. {commkit-3.1.3 → commkit-3.2.1}/commkit/_circular.py +0 -0
  135. {commkit-3.1.3 → commkit-3.2.1}/commkit/_constants.py +0 -0
  136. {commkit-3.1.3 → commkit-3.2.1}/commkit/_cuda/__init__.py +0 -0
  137. {commkit-3.1.3 → commkit-3.2.1}/commkit/_cuda/compiler.py +0 -0
  138. {commkit-3.1.3 → commkit-3.2.1}/commkit/_cuda/graphs.py +0 -0
  139. {commkit-3.1.3 → commkit-3.2.1}/commkit/_cuda/src/algebraic.cu +0 -0
  140. {commkit-3.1.3 → commkit-3.2.1}/commkit/_cuda/src/bps_anchor.cu +0 -0
  141. {commkit-3.1.3 → commkit-3.2.1}/commkit/_cuda/src/bps_min_d2.cu +0 -0
  142. {commkit-3.1.3 → commkit-3.2.1}/commkit/_cuda/src/cs_block.cu +0 -0
  143. {commkit-3.1.3 → commkit-3.2.1}/commkit/_cuda/src/iir.cu +0 -0
  144. {commkit-3.1.3 → commkit-3.2.1}/commkit/_cuda/src/ldpc.cu +0 -0
  145. {commkit-3.1.3 → commkit-3.2.1}/commkit/_cuda/src/llr.cu +0 -0
  146. {commkit-3.1.3 → commkit-3.2.1}/commkit/_cuda/src/selftest.cu +0 -0
  147. {commkit-3.1.3 → commkit-3.2.1}/commkit/_dispersion.py +0 -0
  148. {commkit-3.1.3 → commkit-3.2.1}/commkit/_fft.py +0 -0
  149. {commkit-3.1.3 → commkit-3.2.1}/commkit/_fiber.py +0 -0
  150. {commkit-3.1.3 → commkit-3.2.1}/commkit/_overlap_save.py +0 -0
  151. {commkit-3.1.3 → commkit-3.2.1}/commkit/_random.py +0 -0
  152. {commkit-3.1.3 → commkit-3.2.1}/commkit/_sequences.py +0 -0
  153. {commkit-3.1.3 → commkit-3.2.1}/commkit/analysis/__init__.py +0 -0
  154. {commkit-3.1.3 → commkit-3.2.1}/commkit/analysis/_common.py +0 -0
  155. {commkit-3.1.3 → commkit-3.2.1}/commkit/analysis/allan.py +0 -0
  156. {commkit-3.1.3 → commkit-3.2.1}/commkit/analysis/drift.py +0 -0
  157. {commkit-3.1.3 → commkit-3.2.1}/commkit/analysis/fm_noise.py +0 -0
  158. {commkit-3.1.3 → commkit-3.2.1}/commkit/analysis/interferometry.py +0 -0
  159. {commkit-3.1.3 → commkit-3.2.1}/commkit/analysis/linewidth/__init__.py +0 -0
  160. {commkit-3.1.3 → commkit-3.2.1}/commkit/analysis/linewidth/dsh.py +0 -0
  161. {commkit-3.1.3 → commkit-3.2.1}/commkit/analysis/linewidth/phase.py +0 -0
  162. {commkit-3.1.3 → commkit-3.2.1}/commkit/analysis/linewidth/result.py +0 -0
  163. {commkit-3.1.3 → commkit-3.2.1}/commkit/analysis/trajectory.py +0 -0
  164. {commkit-3.1.3 → commkit-3.2.1}/commkit/backend.py +0 -0
  165. {commkit-3.1.3 → commkit-3.2.1}/commkit/coding/_galois.py +0 -0
  166. {commkit-3.1.3 → commkit-3.2.1}/commkit/coding/_gf2.py +0 -0
  167. {commkit-3.1.3 → commkit-3.2.1}/commkit/coding/_ldpc_tables.py +0 -0
  168. {commkit-3.1.3 → commkit-3.2.1}/commkit/coding/_numba_loops.py +0 -0
  169. {commkit-3.1.3 → commkit-3.2.1}/commkit/coding/algebraic.py +0 -0
  170. {commkit-3.1.3 → commkit-3.2.1}/commkit/coding/convolutional.py +0 -0
  171. {commkit-3.1.3 → commkit-3.2.1}/commkit/coding/crc.py +0 -0
  172. {commkit-3.1.3 → commkit-3.2.1}/commkit/coding/interleaving.py +0 -0
  173. {commkit-3.1.3 → commkit-3.2.1}/commkit/coding/polar.py +0 -0
  174. {commkit-3.1.3 → commkit-3.2.1}/commkit/coding/product.py +0 -0
  175. {commkit-3.1.3 → commkit-3.2.1}/commkit/coding/ratematch.py +0 -0
  176. {commkit-3.1.3 → commkit-3.2.1}/commkit/coding/shaping.py +0 -0
  177. {commkit-3.1.3 → commkit-3.2.1}/commkit/coding/turbo.py +0 -0
  178. {commkit-3.1.3 → commkit-3.2.1}/commkit/core/__init__.py +0 -0
  179. {commkit-3.1.3 → commkit-3.2.1}/commkit/core/_signal_adapter.py +0 -0
  180. {commkit-3.1.3 → commkit-3.2.1}/commkit/core/generation.py +0 -0
  181. {commkit-3.1.3 → commkit-3.2.1}/commkit/core/signal.py +0 -0
  182. {commkit-3.1.3 → commkit-3.2.1}/commkit/equalization/__init__.py +0 -0
  183. {commkit-3.1.3 → commkit-3.2.1}/commkit/equalization/_block/__init__.py +0 -0
  184. {commkit-3.1.3 → commkit-3.2.1}/commkit/equalization/_block/_dd.py +0 -0
  185. {commkit-3.1.3 → commkit-3.2.1}/commkit/equalization/_kernels_numba.py +0 -0
  186. {commkit-3.1.3 → commkit-3.2.1}/commkit/equalization/linear.py +0 -0
  187. {commkit-3.1.3 → commkit-3.2.1}/commkit/equalization/pilots.py +0 -0
  188. {commkit-3.1.3 → commkit-3.2.1}/commkit/equalization/polarization.py +0 -0
  189. {commkit-3.1.3 → commkit-3.2.1}/commkit/equalization/sequential/__init__.py +0 -0
  190. {commkit-3.1.3 → commkit-3.2.1}/commkit/equalization/sequential/_blind.py +0 -0
  191. {commkit-3.1.3 → commkit-3.2.1}/commkit/filtering/__init__.py +0 -0
  192. {commkit-3.1.3 → commkit-3.2.1}/commkit/filtering/fiber.py +0 -0
  193. {commkit-3.1.3 → commkit-3.2.1}/commkit/filtering/pulses.py +0 -0
  194. {commkit-3.1.3 → commkit-3.2.1}/commkit/impairments/__init__.py +0 -0
  195. {commkit-3.1.3 → commkit-3.2.1}/commkit/impairments/channel/__init__.py +0 -0
  196. {commkit-3.1.3 → commkit-3.2.1}/commkit/impairments/channel/linear.py +0 -0
  197. {commkit-3.1.3 → commkit-3.2.1}/commkit/impairments/channel/nonlinear.py +0 -0
  198. {commkit-3.1.3 → commkit-3.2.1}/commkit/impairments/noise.py +0 -0
  199. {commkit-3.1.3 → commkit-3.2.1}/commkit/impairments/source.py +0 -0
  200. {commkit-3.1.3 → commkit-3.2.1}/commkit/logger.py +0 -0
  201. {commkit-3.1.3 → commkit-3.2.1}/commkit/mapping/__init__.py +0 -0
  202. {commkit-3.1.3 → commkit-3.2.1}/commkit/mapping/_numba_llr.py +0 -0
  203. {commkit-3.1.3 → commkit-3.2.1}/commkit/mapping/bits.py +0 -0
  204. {commkit-3.1.3 → commkit-3.2.1}/commkit/mapping/constellation.py +0 -0
  205. {commkit-3.1.3 → commkit-3.2.1}/commkit/mapping/gray.py +0 -0
  206. {commkit-3.1.3 → commkit-3.2.1}/commkit/mapping/llr.py +0 -0
  207. {commkit-3.1.3 → commkit-3.2.1}/commkit/mapping/shaping.py +0 -0
  208. {commkit-3.1.3 → commkit-3.2.1}/commkit/math.py +0 -0
  209. {commkit-3.1.3 → commkit-3.2.1}/commkit/metrics.py +0 -0
  210. {commkit-3.1.3 → commkit-3.2.1}/commkit/multirate.py +0 -0
  211. {commkit-3.1.3 → commkit-3.2.1}/commkit/plotting/constellation.py +0 -0
  212. {commkit-3.1.3 → commkit-3.2.1}/commkit/py.typed +0 -0
  213. {commkit-3.1.3 → commkit-3.2.1}/commkit/recovery/__init__.py +0 -0
  214. {commkit-3.1.3 → commkit-3.2.1}/commkit/recovery/_common.py +0 -0
  215. {commkit-3.1.3 → commkit-3.2.1}/commkit/recovery/bps.py +0 -0
  216. {commkit-3.1.3 → commkit-3.2.1}/commkit/recovery/carrier_phase.py +0 -0
  217. {commkit-3.1.3 → commkit-3.2.1}/commkit/recovery/pilots.py +0 -0
  218. {commkit-3.1.3 → commkit-3.2.1}/commkit/recovery/viterbi_viterbi.py +0 -0
  219. {commkit-3.1.3 → commkit-3.2.1}/commkit/smoothing.py +0 -0
  220. {commkit-3.1.3 → commkit-3.2.1}/commkit/spectral.py +0 -0
  221. {commkit-3.1.3 → commkit-3.2.1}/examples/01-constellations-and-bit-mapping.ipynb +0 -0
  222. {commkit-3.1.3 → commkit-3.2.1}/examples/02-spectral-analysis.ipynb +0 -0
  223. {commkit-3.1.3 → commkit-3.2.1}/examples/03-digital-filters.ipynb +0 -0
  224. {commkit-3.1.3 → commkit-3.2.1}/examples/04-pulse-shaping-and-matched-filtering.ipynb +0 -0
  225. {commkit-3.1.3 → commkit-3.2.1}/examples/05-multirate-and-resampling.ipynb +0 -0
  226. {commkit-3.1.3 → commkit-3.2.1}/examples/08-probabilistic-shaping-and-coded-modulation.ipynb +0 -0
  227. {commkit-3.1.3 → commkit-3.2.1}/examples/09-laser-phase-noise.ipynb +0 -0
  228. {commkit-3.1.3 → commkit-3.2.1}/examples/10-front-end-impairments.ipynb +0 -0
  229. {commkit-3.1.3 → commkit-3.2.1}/examples/11-frame-synchronization-and-timing.ipynb +0 -0
  230. {commkit-3.1.3 → commkit-3.2.1}/examples/12-frequency-offset-estimation.ipynb +0 -0
  231. {commkit-3.1.3 → commkit-3.2.1}/examples/13-blind-carrier-phase-recovery.ipynb +0 -0
  232. {commkit-3.1.3 → commkit-3.2.1}/examples/14-pilot-aided-carrier-recovery.ipynb +0 -0
  233. {commkit-3.1.3 → commkit-3.2.1}/examples/15-linear-channel-equalization.ipynb +0 -0
  234. {commkit-3.1.3 → commkit-3.2.1}/examples/16-adaptive-equalization.ipynb +0 -0
  235. {commkit-3.1.3 → commkit-3.2.1}/examples/18-polarization-multiplexing.ipynb +0 -0
  236. {commkit-3.1.3 → commkit-3.2.1}/examples/19-chromatic-dispersion.ipynb +0 -0
  237. {commkit-3.1.3 → commkit-3.2.1}/examples/20-nonlinear-fiber-and-backpropagation.ipynb +0 -0
  238. {commkit-3.1.3 → commkit-3.2.1}/examples/21-fec-in-optical-standards.ipynb +0 -0
  239. {commkit-3.1.3 → commkit-3.2.1}/examples/22-coherent-transceiver-end-to-end.ipynb +0 -0
  240. {commkit-3.1.3 → commkit-3.2.1}/examples/23-carrier-phase-characterization.ipynb +0 -0
  241. {commkit-3.1.3 → commkit-3.2.1}/examples/24-laser-linewidth-self-heterodyne.ipynb +0 -0
  242. {commkit-3.1.3 → commkit-3.2.1}/examples/25-laser-linewidth-short-delay-and-flicker.ipynb +0 -0
  243. {commkit-3.1.3 → commkit-3.2.1}/examples/26-laser-linewidth-self-homodyne-iq.ipynb +0 -0
  244. {commkit-3.1.3 → commkit-3.2.1}/examples/27-measurement-template-self-heterodyne.ipynb +0 -0
  245. {commkit-3.1.3 → commkit-3.2.1}/examples/28-measurement-template-self-homodyne-iq.ipynb +0 -0
  246. {commkit-3.1.3 → commkit-3.2.1}/tests/analysis/test_drift.py +0 -0
  247. {commkit-3.1.3 → commkit-3.2.1}/tests/analysis/test_fm_noise.py +0 -0
  248. {commkit-3.1.3 → commkit-3.2.1}/tests/analysis/test_interferometry.py +0 -0
  249. {commkit-3.1.3 → commkit-3.2.1}/tests/analysis/test_trajectory.py +0 -0
  250. {commkit-3.1.3 → commkit-3.2.1}/tests/coding/test_algebraic.py +0 -0
  251. {commkit-3.1.3 → commkit-3.2.1}/tests/coding/test_convolutional.py +0 -0
  252. {commkit-3.1.3 → commkit-3.2.1}/tests/coding/test_crc.py +0 -0
  253. {commkit-3.1.3 → commkit-3.2.1}/tests/coding/test_gf2.py +0 -0
  254. {commkit-3.1.3 → commkit-3.2.1}/tests/coding/test_interleaving.py +0 -0
  255. {commkit-3.1.3 → commkit-3.2.1}/tests/coding/test_ldpc_kernel.py +0 -0
  256. {commkit-3.1.3 → commkit-3.2.1}/tests/coding/test_ldpc_tables.py +0 -0
  257. {commkit-3.1.3 → commkit-3.2.1}/tests/coding/test_linear_codes.py +0 -0
  258. {commkit-3.1.3 → commkit-3.2.1}/tests/coding/test_polar.py +0 -0
  259. {commkit-3.1.3 → commkit-3.2.1}/tests/common/coding.py +0 -0
  260. {commkit-3.1.3 → commkit-3.2.1}/tests/common/conversions.py +0 -0
  261. {commkit-3.1.3 → commkit-3.2.1}/tests/common/kernel_utils.py +0 -0
  262. {commkit-3.1.3 → commkit-3.2.1}/tests/common/memory.py +0 -0
  263. {commkit-3.1.3 → commkit-3.2.1}/tests/common/periodic.py +0 -0
  264. {commkit-3.1.3 → commkit-3.2.1}/tests/common/reference_impl.py +0 -0
  265. {commkit-3.1.3 → commkit-3.2.1}/tests/core/test_generation.py +0 -0
  266. {commkit-3.1.3 → commkit-3.2.1}/tests/core/test_psqam.py +0 -0
  267. {commkit-3.1.3 → commkit-3.2.1}/tests/core/test_signal_adapter.py +0 -0
  268. {commkit-3.1.3 → commkit-3.2.1}/tests/core/test_signal_mimo.py +0 -0
  269. {commkit-3.1.3 → commkit-3.2.1}/tests/equalization/test_bps_kernel.py +0 -0
  270. {commkit-3.1.3 → commkit-3.2.1}/tests/equalization/test_cs_kernel.py +0 -0
  271. {commkit-3.1.3 → commkit-3.2.1}/tests/mapping/test_constellation.py +0 -0
  272. {commkit-3.1.3 → commkit-3.2.1}/tests/mapping/test_gray.py +0 -0
  273. {commkit-3.1.3 → commkit-3.2.1}/tests/mapping/test_llr.py +0 -0
  274. {commkit-3.1.3 → commkit-3.2.1}/tests/plotting/test_plot_constellation.py +0 -0
  275. {commkit-3.1.3 → commkit-3.2.1}/tests/plotting/test_plot_equalizer.py +0 -0
  276. {commkit-3.1.3 → commkit-3.2.1}/tests/plotting/test_plot_eye.py +0 -0
  277. {commkit-3.1.3 → commkit-3.2.1}/tests/plotting/test_plot_theme.py +0 -0
  278. {commkit-3.1.3 → commkit-3.2.1}/tests/plugins/__init__.py +0 -0
  279. {commkit-3.1.3 → commkit-3.2.1}/tests/plugins/precision_trace.py +0 -0
  280. {commkit-3.1.3 → commkit-3.2.1}/tests/plugins/transfer_trace.py +0 -0
  281. {commkit-3.1.3 → commkit-3.2.1}/tests/recovery/test_joint.py +0 -0
  282. {commkit-3.1.3 → commkit-3.2.1}/tests/recovery/test_recovery_oracles.py +0 -0
  283. {commkit-3.1.3 → commkit-3.2.1}/tests/test_array.py +0 -0
  284. {commkit-3.1.3 → commkit-3.2.1}/tests/test_backend.py +0 -0
  285. {commkit-3.1.3 → commkit-3.2.1}/tests/test_cuda_infra.py +0 -0
  286. {commkit-3.1.3 → commkit-3.2.1}/tests/test_dispersion.py +0 -0
  287. {commkit-3.1.3 → commkit-3.2.1}/tests/test_examples.py +0 -0
  288. {commkit-3.1.3 → commkit-3.2.1}/tests/test_fiber.py +0 -0
  289. {commkit-3.1.3 → commkit-3.2.1}/tests/test_import_hygiene.py +0 -0
  290. {commkit-3.1.3 → commkit-3.2.1}/tests/test_logger.py +0 -0
  291. {commkit-3.1.3 → commkit-3.2.1}/tests/test_math.py +0 -0
  292. {commkit-3.1.3 → commkit-3.2.1}/tests/test_signal_pipeline.py +0 -0
  293. {commkit-3.1.3 → commkit-3.2.1}/tests/test_smoothing.py +0 -0
  294. {commkit-3.1.3 → commkit-3.2.1}/tests/test_working_memory.py +0 -0
@@ -33,6 +33,7 @@ uv sync --all-extras # environment incl. GPU packages
33
33
  uv run nbstripout --install # once per clone: strip notebook outputs on commit
34
34
  uv run pytest # CPU + GPU tests (default --device=all)
35
35
  uv run pytest --device=cpu # what CI runs
36
+ uv run pytest --run-slow # also the long statistical validation (marker slow)
36
37
  uv run pytest tests/equalization/ -k lms # subset
37
38
  uv run pytest --cov=commkit # coverage
38
39
  NUMBA_DISABLE_JIT=1 uv run pytest tests/equalization tests/recovery \
@@ -111,7 +112,7 @@ uv run python benchmarks/record_baseline.py NAME # record a new baseline
111
112
 
112
113
  **Release:**
113
114
 
114
- 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.
115
+ 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 §9), `uv build` succeeds and `uv run --with twine twine check dist/*` passes.
115
116
  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.
116
117
  3. `uv run bump-my-version bump {patch|minor|major}`. This commits the new version and tags it `vX.Y.Z`.
117
118
  4. `git push origin main --tags`. The ruleset on `main` requires pull requests; repository admins bypass it for these two commits.
@@ -175,7 +176,7 @@ Dependencies point downward only:
175
176
  - Never use a global RNG.
176
177
  - **Types:** a transform is written `def f(samples: S, ...) -> S` with the TypeVar `S` from `core/_signal_adapter.py` (bound to `np.ndarray | Signal`), so a Signal gives a Signal and an array an array. Do not reassign the `samples` parameter; name the unwrapped array `x`.
177
178
  - **Device follows the data.** There are no `backend=` arguments and no global switches. Unsupported array types (JAX, PyTorch) raise `TypeError`.
178
- - Only factories with no input data to follow (`generate`, `generate_phase_noise`, `Preamble`/`SingleCarrierFrame.to_signal`, `load_npz`) take `device: str = "cpu"`, validated with `require_device()`. They draw randomness as the rule above says and do the vectorized work (mapping, shaping) on `device`. The default is never "GPU if available".
179
+ - Only factories with no input data to follow (`generate`, `generate_phase_noise`, `Preamble`/`SingleCarrierFrame.to_signal`, `load_npz`, and the Monte Carlo EXIT curves `decoder_exit_curve` and `demapper_exit_curve`) take `device: str = "cpu"`, validated with `require_device()` (in `coding`, which cannot import `core`, by the same check in `coding.exit`). They draw randomness as the rule above says and do the vectorized work (mapping, shaping) on `device`. The default is never "GPU if available".
179
180
  - **No `debug_plot`.** Numerical code never imports plotting. Plot functions consume results or recompute through public compute functions.
180
181
  - **Design and apply are separate functions**, for example `RRC(...).taps(sps)` and `fir_filter`, or `butterworth_sos` and `iir_filter`.
181
182
 
@@ -227,6 +228,7 @@ Docstrings describe the code as it is; a change in behavior updates them in the
227
228
  - Wrong input raises, with a message naming the argument.
228
229
  - **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).
229
230
  - `logger` is for diagnostics only. Per-channel logging that needs a device-to-host transfer is wrapped in `if logger.isEnabledFor(logging.INFO):`.
231
+ - **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 a metric or estimator found, the parameters an impairment applied, an equalizer's exit MSE, a decoder's failed checks and iterations, what `generate`, a frame or `io` produced. `WARNING` flags a result that is returned but suspect (no convergence, a fit outside its valid range, a near-zero power, a code with 4-cycles). Value objects and pure helpers (taps, tables, code construction) do not log. `warnings.warn` is used only for `PrecisionWarning`, which callers filter.
230
232
  - The library never configures handlers, Matplotlib or warning filters at import time.
231
233
 
232
234
  ---
@@ -263,6 +265,7 @@ Docstrings describe the code as it is; a change in behavior updates them in the
263
265
  | Binary linear algebra of codes: encoding, syndromes, the correlations of maximum-likelihood decoding, the syndromes of algebraic decoding, Chase's test patterns through a hard decoder | one float32 GEMM per chunk (`coding._gf2.matmul_mod2`: BLAS, cuBLAS), exact below an inner dimension of 2^24, its parity taken in place on the CPU and by one elementwise kernel writing int8 on the GPU; small tables (generators, coset leaders) cached per code object and device (`backend._device_tables`) | follows the input |
264
266
  | Algebraic decoding from the syndromes (BCH, Reed-Solomon): Berlekamp-Massey, a Chien search that stops at the locator's degree, Forney at the roots | CPU: Numba `parallel=True` over codewords. GPU: CUDA kernel (`algebraic_correct`), one thread per codeword, the same steps in integer table arithmetic, so both agree exactly (a CuPy version running Berlekamp-Massey in lockstep over the codewords, about 450 launches per decode, was 12x slower on KP4) | follows the input |
265
267
  | Iterative decoding of many codewords: layered LDPC belief propagation (min-sum, sum-product) | CPU: Numba `parallel=True` over codewords, all layers and iterations inside, without fast-math. GPU: CUDA kernel (`ldpc_layered`), one thread block per codeword, the rows of a layer in parallel, early stop per codeword inside the kernel; the same rows in the same order on both, so min-sum agrees bit for bit (a CuPy version, about 12 launches per layer, was 7-23x slower) | follows the input |
268
+ | Analysis by Monte Carlo (EXIT curves) | the work is the existing compiled decoders and vectorized NumPy/CuPy (the demapper's distance table in chunks, histograms on the device); the closed-form parts (the `J` table, LDPC node curves, the chart's trajectory) are host NumPy on grids of a few thousand points, about 0.1 s. No kernel of its own: a whole curve takes 0.05-0.3 s on the GPU | follows `device` |
266
269
  | 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 |
267
270
 
268
271
  **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 the disk cache (`cache=True`) keeps them across processes. A kernel defined inside a function that calls another kernel defined there captures that dispatcher in its closure, which Numba's cache key does not reproduce in a new process: it compiled again in every process (3 s for `compute_llr`, 6 s for polar list decoding). A getter may build a kernel in a closure that captures only constants (`_get_numba_sos`).
@@ -296,13 +299,24 @@ Docstrings describe the code as it is; a change in behavior updates them in the
296
299
 
297
300
  ## 7. Tests
298
301
 
299
- - **Fixtures:** use `backend_device`, `xp` and `xpt` from `tests/conftest.py`. Assert with `xpt.assert_allclose(...)`, because `numpy.testing` raises on CuPy arrays. Cast expectations with `xp.asarray`, and reductions with `float(...)`.
302
+ - **Fixtures:** use `backend_device`, `xp` and `xpt` from `tests/conftest.py`. The GPU legs skip when CuPy is not installed, except under an explicit `--device=gpu`, which fails; an installed CuPy that does not work fails under any setting. That is the only skip: a test that does not belong to the selected run is deselected at collection (`pytest_collection_modifyitems`: `gpu_only` under `--device=cpu`, `cpu_only` under `--device=gpu`, `slow` without `--run-slow`), so a run with CuPy reports no skips, and a new test never calls `pytest.skip` for a configuration. Assert with `xpt.assert_allclose(...)`, because `numpy.testing` raises on CuPy arrays. Cast expectations with `xp.asarray`, and reductions with `float(...)`.
300
303
  - **Layout mirrors the source:** `tests/<subpackage>/test_<module>.py`, and `tests/test_<module>.py` for flat modules. Test basenames must be globally unique (pytest prepend import mode), hence names like `channel/test_channel_linear.py`. Split a large test file by concern.
301
304
  - **Shared builders** live in `tests/common/` and use the public API.
302
305
  - **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.
303
306
  - Do not duplicate these checks in per-module tests.
304
307
  - **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.
305
308
  - **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.
309
+ - **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:
310
+ - **Score against the truth**: the transmitted reference, the true phase, delay or offset, never the receiver's own decisions or another estimate from the same family. Constellation symmetry hides a wrong quadrant, a slip or a duplicated stream from a distance to the nearest point. `tests/common/scoring.py` has the helpers (`ser`, `blind_ser`, `phase_rms`, `block_rotations`, `stream_correlation`).
311
+ - **Resolve only the legitimate ambiguities**, each once over the whole scored interval: 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. Never per segment.
312
+ - **A baseline must fail**: the same test runs without the block it credits (step size 0, `cpr=None`, the corrector off, no compensation) and asserts that this fails.
313
+ - **Tolerances are derived**, from AWGN theory, a Cramér–Rao bound, a Wiener or loop-noise prediction or a binomial interval, and the docstring states the derivation and the measured value. `tests/common/theory.py` holds the closed forms, written with NumPy and SciPy only.
314
+ - **Real channels**: convergence tests use `make_channel_signal` (RRC pulses, ISI, a fractional delay, polarization rotation, DGD, AWGN). The repeat-upsampled builders have no channel and serve plumbing tests only.
315
+ - **Check the test by breaking the code**: while writing it, monkeypatch a plausible wrong result (zeros, a factor of two, the feature disabled) and confirm the test fails.
316
+ - A strengthened test that exposes a library fault is marked `pytest.mark.xfail(strict=True, reason=...)` with the measured numbers and listed in §9; the fix lands in its own commit, which removes the mark.
317
+ - **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 keeps a cheap smoke size; a `slow` parametrization (deselected unless `--run-slow`) repeats it with enough trials to mean something.
318
+ - **End-to-end chains** in `tests/test_receiver_chain.py` run several impairments and their receiver at once, scored against the source bits or AWGN theory, each with one correction left out as a failing baseline.
319
+ - **Warnings are failures**: a WARNING from the `commkit` logger during a test fails it, unless the test is marked `allow_warnings` (it provokes a suspect result on purpose) or takes `caplog` to assert the log. There is no list of tolerated messages: a WARNING that reports what the caller asked for, not a suspect result, is a library log-level bug, fixed in the library.
306
320
  - **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.
307
321
  - **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.
308
322
  - 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.
@@ -368,6 +382,10 @@ Steps, once CuPy 14.3 (with cp315 wheels for `cupy-cuda13x`) and a fixed `jupyte
368
382
  1. **oFEC** (OpenROADM, ITU-T G.709.3) is not implemented: its block structure is specified in the OpenROADM MSA documents, which were not available in a form to implement from. It would build on `BCHCode` (extended BCH(256, 239)), `Chase` with soft output and the staircase decoder's iteration over a window.
369
383
  2. **NR rate matching for polar codes** (sub-block interleaving, the 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.
370
384
 
385
+ ### Test suite: open items
386
+
387
+ - **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 specification documents, each with a provenance header (document, version, table). ETSI's download pages refuse scripted access; the documents have to be supplied.
388
+
371
389
  ### Nonlinear fiber channel: future features
372
390
 
373
391
  Each is an addition that leaves the current API unchanged.
@@ -2,7 +2,45 @@
2
2
 
3
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
4
 
5
- ## [Unreleased]
5
+ ## [3.2.1] - 2026-11-02
6
+
7
+ Fixes found by a rework of the test suite, which now checks what each algorithm achieves against the transmitted truth and theory. Five numerical defaults or models were wrong and change results (PLL cycle-slip repair, Tikhonov smoothing, `block_cma`'s step, `ols_fir_filter`'s alignment of even taps, `MthPower` near 0 Hz); no signature changes. Plots take their sizes from the theme.
8
+
9
+ ### Changes that alter results
10
+
11
+ - **`Tikhonov` uses the actual variance of a Viterbi-Viterbi block phase** as its observation variance, computed from the constellation and `snr_db`: the noise through the M-th power plus the data self-noise of QAM. It used `1/(M²·SNR·N)`, too small by `M²/2` for PSK and by three to four orders of magnitude for QAM, so the smoother followed the raw block phases: on 16-QAM it now lowers the Viterbi-Viterbi error (static phase, 30 dB, 16-symbol blocks: 0.101 rad before and after smoothing, now 0.048 rad).
12
+ - **`PLL(cycle_slip=CycleSlip())` defaults to a history of `8/μ` symbols** (twice the loop memory, between 10 and 640), standalone and inline in `lms` / `rls`; it used 640 symbols, as the block methods do. A PLL trajectory is smoothed by the loop, and the long linear fit lagged the phase noise enough to declare false slips: 16-QAM, `2π·Δν·T = 2π·1e-4`, 16 dB, `B·T = 2e-2`, four records of 2·10⁴ symbols: 10 rotation changes against the transmitted symbols with the repair, 3 without; now 3 with it. The repair does not undo the loop's own slips, which drift over about `1/(B·T)` symbols; the `CycleSlip` docstring says so. An explicit `history` is unchanged.
13
+ - **`block_cma` defaults to `step_size=min(1e-3, 0.5 / block_size)`**, as `block_rde` does; it used 2e-4. On QPSK, where CMA and RDE are the same algorithm, the old default left the worst channel at 2.3 times the AWGN symbol error rate over the second half of 8·10⁴ symbols (dual polarization through rotation and DGD, Es/N0 10 dB; now 1.3 times) and at 16 times on 2·10⁴ symbols (now 2 times); on 16-QAM at 18 dB the SER of 2·10⁴-symbol records falls from 0.13 to 0.008. Above 2500-symbol blocks the new default is the smaller step, inside the stability ceiling that 2e-4 crossed (16-QAM, 4096-symbol blocks: SER 0.02 before, 0.004 now). Pass `step_size=2e-4` for the previous behavior.
14
+ - **`ols_fir_filter` centres even-length taps on tap `(L - 1) // 2`**, as `fir_filter` (SciPy's `mode="same"`) does and as its docstring promised; it used `L // 2`, one sample later. Odd-length taps and `causal=True` are unchanged. The docstrings of both now state the centre tap; they said `L // 2`.
15
+
16
+ - **Plots take line widths, marker sizes and fonts from the theme.** The plot functions no longer set their own line widths and marker sizes, so `apply_default_theme` and the user's rcParams control them; the theme's text is larger (16 pt, legends 14 pt, titles 18 pt) with longer ticks. SI-prefix tick labels show the prefix only (`10 k`, not `10 kHz`), as the axis labels already carry the unit.
17
+
18
+ ### Fixed
19
+
20
+ - **`MthPower` estimates offsets near 0 Hz.** It zeroed bin 0 of the M-th power spectrum and, for constellations without a constant envelope, subtracted the mean of `x^M`, which for QAM is the tone itself: an offset with `|M Δf|` under half a bin was never found (QPSK, 4096 symbols at 1 MS/s: 0 Hz read as -82 kHz, 10 Hz as 30.5 Hz). Both steps are removed; at other offsets the estimates do not change, also with a DC offset of -20 dB in the input.
21
+ - Messages that report what the caller asked for log at INFO, not WARNING, so a WARNING from commkit always means a suspect result: the block equalizers' advice to move CPU data to the GPU, RLS's tail trim, `correct_timing(mode="slice")` shortening the record and the plotting theme's font fallback. The advice to use more taps for fractionally spaced equalization is no longer logged at `sps=1`, where it does not apply.
22
+ - `rls()`: `weights_history` has one row per returned symbol, so it matches `y_hat` after the tail trim (it kept the `num_taps // 2` trimmed rows). A run whose inverse correlation matrix loses positive definiteness (a forgetting factor far below 1) raises the documented `RuntimeError` instead of a bare `ZeroDivisionError` from the kernel, and the message now advises raising the forgetting factor toward 1, not lowering it.
23
+
24
+ ### Development
25
+
26
+ - The test suite checks what the code achieves, not how it is written. Equalizers converge on RRC records through ISI, fractional delays, polarization rotation and DGD, scored against the transmitted symbols with only the legitimate rotation and permutation removed, and each "improves" test also runs a baseline that must fail; carrier recovery is scored against the true phase; estimators and metrics against Cramér–Rao bounds and AWGN theory; Chase and shortened-code decoding against brute force. New end-to-end receiver chains (coded single carrier, dual polarization, 80 km of fiber) each show that leaving out one correction breaks them. Monte Carlo tests state what their frame counts bound and gain `--run-slow` validation runs. A WARNING log from commkit fails the test. Tests that only repeated the contract registry's shape and device checks are removed.
27
+ - The library faults these tests found are fixed above: the PLL's cycle-slip repair adding slips, Tikhonov's noise model on QAM, the dead zone of `MthPower` around 0 Hz, the alignment of even-length taps (`ols_fir_filter` against `fir_filter`) and RLS's `weights_history` length.
28
+ - `benchutils.assert_converged` checks the symbol error rate of the worst channel (it compared bit errors before), and the 2048-tap block RDE benchmark uses a workload that converges.
29
+
30
+ ## [3.2.0] - 2026-10-30
31
+
32
+ EXIT analysis and coding diagnostics. EXIT charts for any soft-in soft-out component, measured through the existing decoders or the demapper, or in closed form for LDPC codes with their threshold; plots of LLR distributions, parity-check matrices and EXIT charts; and INFO logging in the modules where it was missing. Nothing changes for existing code.
33
+
34
+ ### Added
35
+
36
+ - **EXIT analysis** in `commkit.coding.exit`. `ExitCurve` is the transfer curve of one soft-in soft-out component and `exit_chart(first, second)` the `ExitChart` of two, with the decoding trajectory, whether the tunnel is open and where it closes. Curves come from `decoder_exit_curve` (measured through `decode` for any code whose decoder returns a-posteriori LLRs: LDPC, BCJR, Chase), `demapper_exit_curve` (measured, for iterative demapping on AWGN) and `ldpc_exit_curves` (the variable and check nodes of an LDPC code in closed form), with `ldpc_threshold`, the belief-propagation threshold in the Gaussian approximation (0.498 for the DVB-S2 rate-1/2 short code, the NGMI at which simulated QPSK frames start to decode). Measured curves run on the chosen device; the docstrings list the limitations (Gaussian a-priori LLRs, long interleavers, no curve for decoders without soft output such as turbo and polar, early stopping).
37
+ - **Coding plots**: `plot_llr_distribution(llrs, bits)` draws the LLR densities given the transmitted bit and their consistency, `log p(L | 0) / p(L | 1)` against `L`, which shows LLRs from a wrong `noise_var` or demapper model; `plot_parity_check(code)` draws the nonzero entries and node degrees of a parity-check matrix; `plot_exit_chart(chart)` draws an `ExitChart` and marks where a closed tunnel stalls.
38
+ - INFO logging where it was missing: `decode` reports the blocks whose own check failed and the iterations used, `estimate_iq_imbalance` its estimate, and `SingleCarrierFrame.to_signal`, `save_npz` and `load_npz` what they built, saved or loaded. Notebook 00 shows how to turn it on.
39
+ - Notebook 06 checks LLR consistency; notebook 07 predicts the LDPC threshold with an EXIT chart and charts iterative demapping with Gray and natural labels.
40
+
41
+ ### Development
42
+
43
+ - AGENTS.md states the logging levels: DEBUG for what a call is about to do, INFO for one summary per public call, WARNING for a returned but suspect result; value objects do not log, and `warnings.warn` is kept for `PrecisionWarning`.
6
44
 
7
45
  ## [3.1.3] - 2026-10-30
8
46
 
@@ -303,7 +341,12 @@ See the [GitHub release](https://github.com/lokgar/commkit/releases/tag/v1.1.0)
303
341
 
304
342
  First release. See the [GitHub release](https://github.com/lokgar/commkit/releases/tag/v1.0.0).
305
343
 
306
- [Unreleased]: https://github.com/lokgar/commkit/compare/v3.1.0...HEAD
344
+ [Unreleased]: https://github.com/lokgar/commkit/compare/v3.2.1...HEAD
345
+ [3.2.1]: https://github.com/lokgar/commkit/compare/v3.2.0...v3.2.1
346
+ [3.2.0]: https://github.com/lokgar/commkit/compare/v3.1.3...v3.2.0
347
+ [3.1.3]: https://github.com/lokgar/commkit/compare/v3.1.2...v3.1.3
348
+ [3.1.2]: https://github.com/lokgar/commkit/compare/v3.1.1...v3.1.2
349
+ [3.1.1]: https://github.com/lokgar/commkit/compare/v3.1.0...v3.1.1
307
350
  [3.1.0]: https://github.com/lokgar/commkit/compare/v3.0.0...v3.1.0
308
351
  [3.0.0]: https://github.com/lokgar/commkit/compare/v2.3.0...v3.0.0
309
352
  [2.3.0]: https://github.com/lokgar/commkit/compare/v2.2.0...v2.3.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: commkit
3
- Version: 3.1.3
3
+ Version: 3.2.1
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
@@ -129,8 +129,8 @@ CommKit never picks a device for you: data stays where you put it, and each func
129
129
  | [`commkit.math`](https://github.com/lokgar/commkit/blob/main/commkit/math.py) | `rms`, `normalize`, dB conversions. |
130
130
  | [`commkit.smoothing`](https://github.com/lokgar/commkit/blob/main/commkit/smoothing.py) | Display and estimation smoothers (moving average, Savitzky-Golay, 2-D density). Signal-chain filters are in `filtering`. |
131
131
  | [`commkit.io`](https://github.com/lokgar/commkit/blob/main/commkit/io.py) | `save_npz` / `load_npz` for Signals. |
132
- | [`commkit.plotting`](https://github.com/lokgar/commkit/tree/main/commkit/plotting) | Constellations, eye diagrams, spectra, filter responses, equalizer convergence, and synchronization and laser diagnostics that draw the estimates. Imported on first use. |
133
- | [`commkit.coding`](https://github.com/lokgar/commkit/tree/main/commkit/coding) | Channel coding: `encode` / `decode` dispatched on a code object, `LinearBlockCode` (Hamming, extended and shortened), syndrome and maximum-likelihood decoding; `LDPCCode` (DVB-S2, the 802.3ca / 50G-PON code, any parity-check or quasi-cyclic matrix) with layered min-sum and sum-product decoding on the CPU (Numba) and GPU (CUDA); `ReedSolomonCode` (G.709, KR4, KP4, 10G-EPON, XGS-PON) and `BCHCode` with Berlekamp-Massey decoding; Chase-Pyndiah soft decoding; the G.709.2 `StaircaseCode` and `Concatenated` codes (400ZR C-FEC with its exact Hamming(128,119), 802.3dj); `PolarCode` (5G NR information set) with SC and CRC-aided SCL decoding; `ConvolutionalCode` (K = 7, terminated or tail-biting) with Viterbi and BCJR, `TurboCode` (LTE); `CRC` (NR, LTE, Ethernet), `Interleaver`, `Punctured` and `Shortened`; distribution matching (`CCDM`) and probabilistic amplitude shaping (`PAS`). Coded payloads through `generate(..., code=)`. |
132
+ | [`commkit.plotting`](https://github.com/lokgar/commkit/tree/main/commkit/plotting) | Constellations, eye diagrams, spectra, filter responses, equalizer convergence, synchronization and laser diagnostics that draw the estimates, and coding diagnostics: LLR distributions and their consistency, parity-check matrices, EXIT charts. Imported on first use. |
133
+ | [`commkit.coding`](https://github.com/lokgar/commkit/tree/main/commkit/coding) | Channel coding: `encode` / `decode` dispatched on a code object, `LinearBlockCode` (Hamming, extended and shortened), syndrome and maximum-likelihood decoding; `LDPCCode` (DVB-S2, the 802.3ca / 50G-PON code, any parity-check or quasi-cyclic matrix) with layered min-sum and sum-product decoding on the CPU (Numba) and GPU (CUDA); `ReedSolomonCode` (G.709, KR4, KP4, 10G-EPON, XGS-PON) and `BCHCode` with Berlekamp-Massey decoding; Chase-Pyndiah soft decoding; the G.709.2 `StaircaseCode` and `Concatenated` codes (400ZR C-FEC with its exact Hamming(128,119), 802.3dj); `PolarCode` (5G NR information set) with SC and CRC-aided SCL decoding; `ConvolutionalCode` (K = 7, terminated or tail-biting) with Viterbi and BCJR, `TurboCode` (LTE); `CRC` (NR, LTE, Ethernet), `Interleaver`, `Punctured` and `Shortened`; distribution matching (`CCDM`) and probabilistic amplitude shaping (`PAS`); EXIT charts of measured decoder and demapper curves, and of LDPC codes in closed form with their threshold. Coded payloads through `generate(..., code=)`. |
134
134
 
135
135
  Importing `commkit` has no side effects: it configures no logging, Matplotlib or warning filters, and does not touch the GPU.
136
136
 
@@ -93,8 +93,8 @@ CommKit never picks a device for you: data stays where you put it, and each func
93
93
  | [`commkit.math`](https://github.com/lokgar/commkit/blob/main/commkit/math.py) | `rms`, `normalize`, dB conversions. |
94
94
  | [`commkit.smoothing`](https://github.com/lokgar/commkit/blob/main/commkit/smoothing.py) | Display and estimation smoothers (moving average, Savitzky-Golay, 2-D density). Signal-chain filters are in `filtering`. |
95
95
  | [`commkit.io`](https://github.com/lokgar/commkit/blob/main/commkit/io.py) | `save_npz` / `load_npz` for Signals. |
96
- | [`commkit.plotting`](https://github.com/lokgar/commkit/tree/main/commkit/plotting) | Constellations, eye diagrams, spectra, filter responses, equalizer convergence, and synchronization and laser diagnostics that draw the estimates. Imported on first use. |
97
- | [`commkit.coding`](https://github.com/lokgar/commkit/tree/main/commkit/coding) | Channel coding: `encode` / `decode` dispatched on a code object, `LinearBlockCode` (Hamming, extended and shortened), syndrome and maximum-likelihood decoding; `LDPCCode` (DVB-S2, the 802.3ca / 50G-PON code, any parity-check or quasi-cyclic matrix) with layered min-sum and sum-product decoding on the CPU (Numba) and GPU (CUDA); `ReedSolomonCode` (G.709, KR4, KP4, 10G-EPON, XGS-PON) and `BCHCode` with Berlekamp-Massey decoding; Chase-Pyndiah soft decoding; the G.709.2 `StaircaseCode` and `Concatenated` codes (400ZR C-FEC with its exact Hamming(128,119), 802.3dj); `PolarCode` (5G NR information set) with SC and CRC-aided SCL decoding; `ConvolutionalCode` (K = 7, terminated or tail-biting) with Viterbi and BCJR, `TurboCode` (LTE); `CRC` (NR, LTE, Ethernet), `Interleaver`, `Punctured` and `Shortened`; distribution matching (`CCDM`) and probabilistic amplitude shaping (`PAS`). Coded payloads through `generate(..., code=)`. |
96
+ | [`commkit.plotting`](https://github.com/lokgar/commkit/tree/main/commkit/plotting) | Constellations, eye diagrams, spectra, filter responses, equalizer convergence, synchronization and laser diagnostics that draw the estimates, and coding diagnostics: LLR distributions and their consistency, parity-check matrices, EXIT charts. Imported on first use. |
97
+ | [`commkit.coding`](https://github.com/lokgar/commkit/tree/main/commkit/coding) | Channel coding: `encode` / `decode` dispatched on a code object, `LinearBlockCode` (Hamming, extended and shortened), syndrome and maximum-likelihood decoding; `LDPCCode` (DVB-S2, the 802.3ca / 50G-PON code, any parity-check or quasi-cyclic matrix) with layered min-sum and sum-product decoding on the CPU (Numba) and GPU (CUDA); `ReedSolomonCode` (G.709, KR4, KP4, 10G-EPON, XGS-PON) and `BCHCode` with Berlekamp-Massey decoding; Chase-Pyndiah soft decoding; the G.709.2 `StaircaseCode` and `Concatenated` codes (400ZR C-FEC with its exact Hamming(128,119), 802.3dj); `PolarCode` (5G NR information set) with SC and CRC-aided SCL decoding; `ConvolutionalCode` (K = 7, terminated or tail-biting) with Viterbi and BCJR, `TurboCode` (LTE); `CRC` (NR, LTE, Ethernet), `Interleaver`, `Punctured` and `Shortened`; distribution matching (`CCDM`) and probabilistic amplitude shaping (`PAS`); EXIT charts of measured decoder and demapper curves, and of LDPC codes in closed form with their threshold. Coded payloads through `generate(..., code=)`. |
98
98
 
99
99
  Importing `commkit` has no side effects: it configures no logging, Matplotlib or warning filters, and does not touch the GPU.
100
100
 
@@ -31,16 +31,18 @@ ROUNDS = dict(rounds=3, warmup_rounds=1, iterations=1)
31
31
  N_SYM = 100_000
32
32
 
33
33
  # Converging step sizes per (equalizer, block_size). RDE on 16-QAM from a
34
- # cold start (no CMA pre-convergence) converges slowly and is still settling
35
- # after the skipped symbols at 2048, hence its looser SER bound; its cost does
36
- # not depend on the data.
34
+ # cold start (no CMA pre-convergence) converges slowly at 2048: at 4e-4 it is
35
+ # error-free from about symbol 70 000, so its SER is scored after that (7e-4
36
+ # never settled, still 2-5 % SER at the end); its cost does not depend on
37
+ # the data.
37
38
  STEP_SIZE = {
38
39
  ("cma", 256): 2e-4,
39
40
  ("cma", 2048): 3e-4,
40
41
  ("rde", 256): 3e-3,
41
- ("rde", 2048): 7e-4,
42
+ ("rde", 2048): 4e-4,
42
43
  }
43
- MAX_SER = {"cma": 1e-2, "rde": 5e-2}
44
+ SKIP = {("rde", 2048): 70_000}
45
+ MAX_SER = {"cma": 1e-2, "rde": 1e-2}
44
46
 
45
47
  EQUALIZERS = [
46
48
  ("cma", block_cma),
@@ -66,7 +68,12 @@ def _bench_block_blind(benchmark, xp, sync, label, eq_fn, block_size):
66
68
 
67
69
  r = benchmark.pedantic(run, **ROUNDS)
68
70
  assert_converged(
69
- r, syms, Constellation.qam(16), skip=20_000, max_ser=MAX_SER[label], blind=True
71
+ r,
72
+ syms,
73
+ Constellation.qam(16),
74
+ skip=SKIP.get((label, block_size), 20_000),
75
+ max_ser=MAX_SER[label],
76
+ blind=True,
70
77
  )
71
78
 
72
79
 
@@ -26,16 +26,18 @@ LINK = dict(
26
26
  )
27
27
 
28
28
 
29
+ # The plain/fused split exists on the GPU only; its CPU items are deselected.
30
+ PATHS = ["auto", pytest.param("plain", marks=pytest.mark.gpu_only)]
31
+
32
+
29
33
  def _stages(backend_device, monkeypatch, path):
30
34
  if path == "plain":
31
- if backend_device != "gpu":
32
- pytest.skip("the plain/fused split exists on the GPU only")
33
35
  monkeypatch.setattr(
34
36
  _fiber, "_make_ops", lambda x, xp, *args: _fiber._Ops(xp, x.dtype, *args)
35
37
  )
36
38
 
37
39
 
38
- @pytest.mark.parametrize("path", ["auto", "plain"])
40
+ @pytest.mark.parametrize("path", PATHS)
39
41
  @pytest.mark.parametrize("n", [2**16, 2**20], ids=["N64k", "N1M"])
40
42
  def bench_fiber_propagation(benchmark, backend_device, xp, sync, monkeypatch, n, path):
41
43
  _stages(backend_device, monkeypatch, path)
@@ -49,7 +51,7 @@ def bench_fiber_propagation(benchmark, backend_device, xp, sync, monkeypatch, n,
49
51
  benchmark.pedantic(run, **ROUNDS)
50
52
 
51
53
 
52
- @pytest.mark.parametrize("path", ["auto", "plain"])
54
+ @pytest.mark.parametrize("path", PATHS)
53
55
  @pytest.mark.parametrize("steps", [1, 10])
54
56
  def bench_dbp(benchmark, backend_device, xp, sync, monkeypatch, steps, path):
55
57
  _stages(backend_device, monkeypatch, path)
@@ -64,14 +64,22 @@ def assert_converged(result, syms, constellation, *, skip, max_ser, blind=False)
64
64
  storms, divergence) instead of the operating point, so every equalizer
65
65
  benchmark checks its symbol error rate after ``skip`` symbols. Blind
66
66
  equalizers leave a phase ambiguity, which is resolved against the
67
- reference first.
67
+ reference first, after the channel permutation of a butterfly.
68
68
  """
69
69
  from commkit.backend import to_device
70
- from commkit.recovery import resolve_phase_ambiguity
70
+ from commkit.recovery import resolve_channel_permutation, resolve_phase_ambiguity
71
71
 
72
72
  y = to_device(result.y_hat, "cpu")[..., skip:]
73
73
  ref = to_device(syms, "cpu")[..., skip : skip + y.shape[-1]]
74
74
  if blind:
75
+ if y.ndim == 2:
76
+ y = resolve_channel_permutation(y, ref)
75
77
  y = resolve_phase_ambiguity(y, ref, constellation=constellation)
76
- ser = float((constellation.demap(y) != constellation.demap(ref)).mean())
78
+ # Symbol decisions, not bits (one wrong symbol is one error), and the
79
+ # worst channel, so one failed stream cannot hide behind a good one.
80
+ wrong = constellation._nearest(y.reshape(-1)) != constellation._nearest(
81
+ ref.reshape(-1)
82
+ )
83
+ per_channel = to_device(wrong, "cpu").reshape(y.shape).mean(axis=-1)
84
+ ser = float(per_channel.max())
77
85
  assert ser <= max_ser, f"benchmark workload did not converge: SER {ser:.3g}"
@@ -56,6 +56,20 @@ def pytest_generate_tests(metafunc):
56
56
  metafunc.parametrize("backend_device", params, indirect=True)
57
57
 
58
58
 
59
+ def pytest_collection_modifyitems(config, items):
60
+ """Deselect, not skip, the CPU items of ``gpu_only`` parameters."""
61
+ cpu_only_gpu = [
62
+ item
63
+ for item in items
64
+ if item.get_closest_marker("gpu_only")
65
+ and getattr(item, "callspec", None) is not None
66
+ and item.callspec.params.get("backend_device") == "cpu"
67
+ ]
68
+ if cpu_only_gpu:
69
+ items[:] = [item for item in items if item not in cpu_only_gpu]
70
+ config.hook.pytest_deselected(items=cpu_only_gpu)
71
+
72
+
59
73
  @numba.njit(cache=True, parallel=True)
60
74
  def _spin(x):
61
75
  total = 0.0
@@ -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.1.3"
34
+ __version__ = "3.2.1"
35
35
 
36
36
  from .backend import PrecisionWarning
37
37
  from .core import (
@@ -23,13 +23,26 @@ Codes: :class:`LinearBlockCode` (Hamming), :class:`LDPCCode` (DVB-S2, PON),
23
23
  :class:`Stages`, :class:`SC`, :class:`SCL`, :class:`Viterbi`, :class:`BCJR`,
24
24
  :class:`TurboIterations`. Bit
25
25
  permutations: :func:`interleave` and :func:`deinterleave` with an
26
- :class:`Interleaver`.
26
+ :class:`Interleaver`. EXIT analysis: :func:`exit_chart` of two
27
+ :class:`ExitCurve` objects, from :func:`decoder_exit_curve` (any soft-output
28
+ decoder), :func:`demapper_exit_curve` (iterative demapping) or
29
+ :func:`ldpc_exit_curves` (LDPC nodes, closed form, with
30
+ :func:`ldpc_threshold`).
27
31
  """
28
32
 
29
33
  from .algebraic import BCHCode, BerlekampMassey, ReedSolomonCode
30
34
  from .base import DecodeResult, decode, encode
31
35
  from .convolutional import BCJR, ConvolutionalCode, Viterbi
32
36
  from .crc import CRC
37
+ from .exit import (
38
+ ExitChart,
39
+ ExitCurve,
40
+ decoder_exit_curve,
41
+ demapper_exit_curve,
42
+ exit_chart,
43
+ ldpc_exit_curves,
44
+ ldpc_threshold,
45
+ )
33
46
  from .interleaving import Interleaver, deinterleave, interleave
34
47
  from .ldpc import LDPCCode, MinSum, SumProduct
35
48
  from .linear import Chase, LinearBlockCode, MaximumLikelihood, Syndrome
@@ -49,6 +62,8 @@ __all__ = [
49
62
  "Concatenated",
50
63
  "ConvolutionalCode",
51
64
  "DecodeResult",
65
+ "ExitChart",
66
+ "ExitCurve",
52
67
  "HardIterations",
53
68
  "Interleaver",
54
69
  "LDPCCode",
@@ -72,5 +87,10 @@ __all__ = [
72
87
  "decode",
73
88
  "deinterleave",
74
89
  "encode",
90
+ "decoder_exit_curve",
91
+ "demapper_exit_curve",
92
+ "exit_chart",
75
93
  "interleave",
94
+ "ldpc_exit_curves",
95
+ "ldpc_threshold",
76
96
  ]
@@ -17,6 +17,7 @@ along the last axis; a block never spans channels.
17
17
  from __future__ import annotations
18
18
 
19
19
  import dataclasses
20
+ import logging
20
21
  from collections.abc import Callable
21
22
  from dataclasses import dataclass
22
23
  from typing import Any, NamedTuple
@@ -24,7 +25,7 @@ from typing import Any, NamedTuple
24
25
  import numpy as np
25
26
 
26
27
  from .._array import as_2d, restore_1d, validate_array, warn_if_double
27
- from ..backend import ArrayType, get_array_module
28
+ from ..backend import ArrayType, get_array_module, to_device
28
29
  from ..logger import logger
29
30
 
30
31
  __all__ = ["DecodeResult", "decode", "encode"]
@@ -272,6 +273,26 @@ def encode(bits: ArrayType, *, code: Any) -> ArrayType:
272
273
  return restore_1d(was_1d, out.reshape(x2.shape[0], num_blocks * code.n))
273
274
 
274
275
 
276
+ def _log_decode(res: _Decoded, code: Any, method: Any) -> None:
277
+ """Log the decoder's own success check and iterations (INFO level)."""
278
+ # Two (R,) transfers, made only when INFO is enabled.
279
+ if not logger.isEnabledFor(logging.INFO) or res.valid.size == 0:
280
+ return
281
+ valid = to_device(res.valid, "cpu")
282
+ iterations = to_device(res.iterations, "cpu")
283
+ failed = int(valid.size - np.count_nonzero(valid))
284
+ most = int(iterations.max())
285
+ used = f", iterations mean {float(iterations.mean()):.1f}, max {most}"
286
+ logger.info(
287
+ "Decoded %d block(s) of %r with %r: %d failed the decoder's check%s.",
288
+ valid.size,
289
+ code,
290
+ method,
291
+ failed,
292
+ used if most > 1 else "",
293
+ )
294
+
295
+
275
296
  def decode(llrs: ArrayType, *, code: Any, method: Any = None) -> DecodeResult:
276
297
  """Decode LLRs (or hard decisions) of a code's transmitted bits.
277
298
 
@@ -325,6 +346,7 @@ def decode(llrs: ArrayType, *, code: Any, method: Any = None) -> DecodeResult:
325
346
  return None
326
347
  return restore_1d(was_1d, a.reshape(x2.shape[0], num_blocks * size))
327
348
 
349
+ _log_decode(res, code, method)
328
350
  return DecodeResult(
329
351
  bits=shaped(res.bits, code.k),
330
352
  llrs=shaped(res.llrs, code.n),