commkit 3.2.0__tar.gz → 3.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (298) hide show
  1. {commkit-3.2.0 → commkit-3.3.0}/AGENTS.md +18 -2
  2. {commkit-3.2.0 → commkit-3.3.0}/CHANGELOG.md +48 -1
  3. {commkit-3.2.0 → commkit-3.3.0}/PKG-INFO +29 -3
  4. {commkit-3.2.0 → commkit-3.3.0}/README.md +28 -2
  5. {commkit-3.2.0 → commkit-3.3.0}/benchmarks/bench_block_blind.py +13 -6
  6. {commkit-3.2.0 → commkit-3.3.0}/benchmarks/bench_fiber.py +6 -4
  7. {commkit-3.2.0 → commkit-3.3.0}/benchmarks/bench_filters.py +19 -1
  8. {commkit-3.2.0 → commkit-3.3.0}/benchmarks/benchutils.py +11 -3
  9. {commkit-3.2.0 → commkit-3.3.0}/benchmarks/conftest.py +14 -0
  10. {commkit-3.2.0 → commkit-3.3.0}/commkit/__init__.py +1 -1
  11. {commkit-3.2.0 → commkit-3.3.0}/commkit/coding/linear.py +3 -1
  12. {commkit-3.2.0 → commkit-3.3.0}/commkit/equalization/_block/_blind.py +1 -1
  13. {commkit-3.2.0 → commkit-3.3.0}/commkit/equalization/_block/_engine.py +1 -1
  14. {commkit-3.2.0 → commkit-3.3.0}/commkit/equalization/_common.py +2 -2
  15. {commkit-3.2.0 → commkit-3.3.0}/commkit/equalization/_numba_loops.py +4 -0
  16. {commkit-3.2.0 → commkit-3.3.0}/commkit/equalization/blind.py +4 -2
  17. {commkit-3.2.0 → commkit-3.3.0}/commkit/equalization/result.py +3 -1
  18. {commkit-3.2.0 → commkit-3.3.0}/commkit/equalization/sequential/_dd.py +20 -10
  19. {commkit-3.2.0 → commkit-3.3.0}/commkit/equalization/sequential/_setup.py +3 -1
  20. {commkit-3.2.0 → commkit-3.3.0}/commkit/filtering/__init__.py +8 -0
  21. {commkit-3.2.0 → commkit-3.3.0}/commkit/filtering/linear.py +7 -5
  22. commkit-3.3.0/commkit/filtering/response.py +260 -0
  23. {commkit-3.2.0 → commkit-3.3.0}/commkit/frequency.py +0 -4
  24. {commkit-3.2.0 → commkit-3.3.0}/commkit/plotting/__init__.py +4 -3
  25. {commkit-3.2.0 → commkit-3.3.0}/commkit/plotting/analysis.py +173 -111
  26. {commkit-3.2.0 → commkit-3.3.0}/commkit/plotting/coding.py +147 -75
  27. {commkit-3.2.0 → commkit-3.3.0}/commkit/plotting/constellation.py +11 -11
  28. commkit-3.3.0/commkit/plotting/equalizer.py +319 -0
  29. {commkit-3.2.0 → commkit-3.3.0}/commkit/plotting/eye.py +22 -5
  30. commkit-3.3.0/commkit/plotting/filter_response.py +184 -0
  31. {commkit-3.2.0 → commkit-3.3.0}/commkit/plotting/spectral.py +19 -4
  32. {commkit-3.2.0 → commkit-3.3.0}/commkit/plotting/sync.py +405 -254
  33. {commkit-3.2.0 → commkit-3.3.0}/commkit/plotting/theme.py +85 -12
  34. {commkit-3.2.0 → commkit-3.3.0}/commkit/plotting/waveform.py +32 -6
  35. {commkit-3.2.0 → commkit-3.3.0}/commkit/recovery/corrections.py +24 -6
  36. {commkit-3.2.0 → commkit-3.3.0}/commkit/recovery/pll.py +28 -3
  37. {commkit-3.2.0 → commkit-3.3.0}/commkit/recovery/tikhonov.py +38 -4
  38. {commkit-3.2.0 → commkit-3.3.0}/commkit/timing.py +3 -3
  39. {commkit-3.2.0 → commkit-3.3.0}/examples/01-constellations-and-bit-mapping.ipynb +10 -13
  40. {commkit-3.2.0 → commkit-3.3.0}/examples/02-spectral-analysis.ipynb +9 -3
  41. {commkit-3.2.0 → commkit-3.3.0}/examples/03-digital-filters.ipynb +61 -49
  42. {commkit-3.2.0 → commkit-3.3.0}/examples/04-pulse-shaping-and-matched-filtering.ipynb +25 -12
  43. {commkit-3.2.0 → commkit-3.3.0}/examples/05-multirate-and-resampling.ipynb +8 -13
  44. {commkit-3.2.0 → commkit-3.3.0}/examples/06-awgn-and-performance-metrics.ipynb +2 -2
  45. {commkit-3.2.0 → commkit-3.3.0}/examples/07-channel-coding.ipynb +5 -3
  46. {commkit-3.2.0 → commkit-3.3.0}/examples/08-probabilistic-shaping-and-coded-modulation.ipynb +25 -4
  47. {commkit-3.2.0 → commkit-3.3.0}/examples/09-laser-phase-noise.ipynb +6 -8
  48. {commkit-3.2.0 → commkit-3.3.0}/examples/10-front-end-impairments.ipynb +16 -8
  49. {commkit-3.2.0 → commkit-3.3.0}/examples/11-frame-synchronization-and-timing.ipynb +10 -3
  50. {commkit-3.2.0 → commkit-3.3.0}/examples/12-frequency-offset-estimation.ipynb +3 -1
  51. {commkit-3.2.0 → commkit-3.3.0}/examples/13-blind-carrier-phase-recovery.ipynb +11 -3
  52. {commkit-3.2.0 → commkit-3.3.0}/examples/14-pilot-aided-carrier-recovery.ipynb +11 -3
  53. {commkit-3.2.0 → commkit-3.3.0}/examples/15-linear-channel-equalization.ipynb +18 -9
  54. {commkit-3.2.0 → commkit-3.3.0}/examples/16-adaptive-equalization.ipynb +23 -11
  55. {commkit-3.2.0 → commkit-3.3.0}/examples/17-blind-equalization.ipynb +3 -3
  56. {commkit-3.2.0 → commkit-3.3.0}/examples/18-polarization-multiplexing.ipynb +15 -3
  57. {commkit-3.2.0 → commkit-3.3.0}/examples/19-chromatic-dispersion.ipynb +15 -3
  58. {commkit-3.2.0 → commkit-3.3.0}/examples/20-nonlinear-fiber-and-backpropagation.ipynb +1 -1
  59. {commkit-3.2.0 → commkit-3.3.0}/examples/21-fec-in-optical-standards.ipynb +17 -2
  60. {commkit-3.2.0 → commkit-3.3.0}/examples/22-coherent-transceiver-end-to-end.ipynb +3 -1
  61. {commkit-3.2.0 → commkit-3.3.0}/pyproject.toml +4 -2
  62. commkit-3.3.0/tests/analysis/test_allan.py +75 -0
  63. {commkit-3.2.0 → commkit-3.3.0}/tests/analysis/test_linewidth.py +3 -0
  64. {commkit-3.2.0 → commkit-3.3.0}/tests/coding/test_ldpc.py +24 -6
  65. {commkit-3.2.0 → commkit-3.3.0}/tests/coding/test_product.py +87 -20
  66. {commkit-3.2.0 → commkit-3.3.0}/tests/coding/test_ratematch.py +30 -9
  67. {commkit-3.2.0 → commkit-3.3.0}/tests/coding/test_shaping.py +12 -5
  68. {commkit-3.2.0 → commkit-3.3.0}/tests/coding/test_turbo.py +41 -17
  69. {commkit-3.2.0 → commkit-3.3.0}/tests/common/__init__.py +2 -2
  70. {commkit-3.2.0 → commkit-3.3.0}/tests/common/metrics.py +0 -27
  71. commkit-3.3.0/tests/common/scoring.py +129 -0
  72. {commkit-3.2.0 → commkit-3.3.0}/tests/common/signals.py +91 -13
  73. commkit-3.3.0/tests/common/theory.py +84 -0
  74. {commkit-3.2.0 → commkit-3.3.0}/tests/conftest.py +64 -7
  75. {commkit-3.2.0 → commkit-3.3.0}/tests/core/test_frame.py +11 -43
  76. {commkit-3.2.0 → commkit-3.3.0}/tests/core/test_signal.py +13 -34
  77. {commkit-3.2.0 → commkit-3.3.0}/tests/equalization/test_blind.py +99 -66
  78. {commkit-3.2.0 → commkit-3.3.0}/tests/equalization/test_block.py +177 -333
  79. {commkit-3.2.0 → commkit-3.3.0}/tests/equalization/test_cpr.py +241 -291
  80. {commkit-3.2.0 → commkit-3.3.0}/tests/equalization/test_equalizer_oracles.py +14 -11
  81. {commkit-3.2.0 → commkit-3.3.0}/tests/equalization/test_linear.py +65 -89
  82. commkit-3.3.0/tests/equalization/test_mimo.py +164 -0
  83. {commkit-3.2.0 → commkit-3.3.0}/tests/equalization/test_polarization.py +13 -0
  84. commkit-3.3.0/tests/equalization/test_sequential.py +662 -0
  85. {commkit-3.2.0 → commkit-3.3.0}/tests/equalization/test_state.py +2 -0
  86. {commkit-3.2.0 → commkit-3.3.0}/tests/equalization/test_winit.py +36 -151
  87. {commkit-3.2.0 → commkit-3.3.0}/tests/impairments/channel/test_channel_linear.py +68 -69
  88. {commkit-3.2.0 → commkit-3.3.0}/tests/impairments/channel/test_channel_nonlinear.py +2 -0
  89. {commkit-3.2.0 → commkit-3.3.0}/tests/impairments/test_frontend.py +0 -42
  90. {commkit-3.2.0 → commkit-3.3.0}/tests/impairments/test_noise.py +0 -7
  91. {commkit-3.2.0 → commkit-3.3.0}/tests/impairments/test_source.py +0 -12
  92. {commkit-3.2.0 → commkit-3.3.0}/tests/mapping/test_bits.py +0 -16
  93. {commkit-3.2.0 → commkit-3.3.0}/tests/plotting/test_plot_analysis.py +41 -0
  94. {commkit-3.2.0 → commkit-3.3.0}/tests/plotting/test_plot_coding.py +37 -0
  95. {commkit-3.2.0 → commkit-3.3.0}/tests/plotting/test_plot_constellation.py +23 -0
  96. {commkit-3.2.0 → commkit-3.3.0}/tests/plotting/test_plot_equalizer.py +73 -0
  97. {commkit-3.2.0 → commkit-3.3.0}/tests/plotting/test_plot_eye.py +30 -0
  98. {commkit-3.2.0 → commkit-3.3.0}/tests/plotting/test_plot_filter_response.py +52 -3
  99. {commkit-3.2.0 → commkit-3.3.0}/tests/plotting/test_plot_spectral.py +15 -1
  100. {commkit-3.2.0 → commkit-3.3.0}/tests/plotting/test_plot_sync.py +125 -0
  101. {commkit-3.2.0 → commkit-3.3.0}/tests/plotting/test_plot_waveform.py +16 -1
  102. {commkit-3.2.0 → commkit-3.3.0}/tests/plugins/transfer_trace.py +1 -0
  103. {commkit-3.2.0 → commkit-3.3.0}/tests/recovery/test_bps.py +35 -64
  104. {commkit-3.2.0 → commkit-3.3.0}/tests/recovery/test_corrections.py +34 -38
  105. {commkit-3.2.0 → commkit-3.3.0}/tests/recovery/test_pilots.py +14 -57
  106. {commkit-3.2.0 → commkit-3.3.0}/tests/recovery/test_pll.py +24 -47
  107. {commkit-3.2.0 → commkit-3.3.0}/tests/recovery/test_tikhonov.py +129 -106
  108. {commkit-3.2.0 → commkit-3.3.0}/tests/recovery/test_viterbi_viterbi.py +38 -85
  109. {commkit-3.2.0 → commkit-3.3.0}/tests/test_api_contracts.py +11 -0
  110. commkit-3.3.0/tests/test_filter_response.py +190 -0
  111. {commkit-3.2.0 → commkit-3.3.0}/tests/test_filtering.py +75 -37
  112. {commkit-3.2.0 → commkit-3.3.0}/tests/test_frequency.py +72 -68
  113. {commkit-3.2.0 → commkit-3.3.0}/tests/test_io.py +16 -4
  114. {commkit-3.2.0 → commkit-3.3.0}/tests/test_metrics.py +51 -34
  115. {commkit-3.2.0 → commkit-3.3.0}/tests/test_multirate.py +96 -36
  116. {commkit-3.2.0 → commkit-3.3.0}/tests/test_pulse_shaping.py +23 -32
  117. commkit-3.3.0/tests/test_receiver_chain.py +244 -0
  118. {commkit-3.2.0 → commkit-3.3.0}/tests/test_spectral.py +83 -0
  119. {commkit-3.2.0 → commkit-3.3.0}/tests/test_timing.py +54 -39
  120. {commkit-3.2.0 → commkit-3.3.0}/uv.lock +1 -1
  121. commkit-3.2.0/commkit/plotting/equalizer.py +0 -292
  122. commkit-3.2.0/commkit/plotting/filter_response.py +0 -216
  123. commkit-3.2.0/tests/analysis/test_allan.py +0 -34
  124. commkit-3.2.0/tests/equalization/test_mimo.py +0 -218
  125. commkit-3.2.0/tests/equalization/test_sequential.py +0 -1170
  126. {commkit-3.2.0 → commkit-3.3.0}/.gitattributes +0 -0
  127. {commkit-3.2.0 → commkit-3.3.0}/.github/workflows/ci.yml +0 -0
  128. {commkit-3.2.0 → commkit-3.3.0}/.github/workflows/publish.yml +0 -0
  129. {commkit-3.2.0 → commkit-3.3.0}/.gitignore +0 -0
  130. {commkit-3.2.0 → commkit-3.3.0}/.python-version +0 -0
  131. {commkit-3.2.0 → commkit-3.3.0}/LICENSE +0 -0
  132. {commkit-3.2.0 → commkit-3.3.0}/benchmarks/baselines/Linux-CPython-3.14-64bit/0002_pre_v2.json +0 -0
  133. {commkit-3.2.0 → commkit-3.3.0}/benchmarks/baselines/Linux-CPython-3.14-64bit/0003_v2_0.json +0 -0
  134. {commkit-3.2.0 → commkit-3.3.0}/benchmarks/baselines/Linux-CPython-3.14-64bit/0004_fiber_dbp.json +0 -0
  135. {commkit-3.2.0 → commkit-3.3.0}/benchmarks/baselines/Linux-CPython-3.14-64bit/0005_gpu_decoder_path.json +0 -0
  136. {commkit-3.2.0 → commkit-3.3.0}/benchmarks/baselines/Linux-CPython-3.14-64bit/0006_perf_cleanup.json +0 -0
  137. {commkit-3.2.0 → commkit-3.3.0}/benchmarks/baselines/Linux-CPython-3.14-64bit/0007_v3_0.json +0 -0
  138. {commkit-3.2.0 → commkit-3.3.0}/benchmarks/baselines/Linux-CPython-3.14-64bit/0008_v3_1.json +0 -0
  139. {commkit-3.2.0 → commkit-3.3.0}/benchmarks/bench_block_lms.py +0 -0
  140. {commkit-3.2.0 → commkit-3.3.0}/benchmarks/bench_bps.py +0 -0
  141. {commkit-3.2.0 → commkit-3.3.0}/benchmarks/bench_coding.py +0 -0
  142. {commkit-3.2.0 → commkit-3.3.0}/benchmarks/bench_equalizers.py +0 -0
  143. {commkit-3.2.0 → commkit-3.3.0}/benchmarks/bench_import.py +0 -0
  144. {commkit-3.2.0 → commkit-3.3.0}/benchmarks/bench_llr.py +0 -0
  145. {commkit-3.2.0 → commkit-3.3.0}/benchmarks/bench_mapping.py +0 -0
  146. {commkit-3.2.0 → commkit-3.3.0}/benchmarks/bench_signal_pipeline.py +0 -0
  147. {commkit-3.2.0 → commkit-3.3.0}/benchmarks/bench_sync_misc.py +0 -0
  148. {commkit-3.2.0 → commkit-3.3.0}/benchmarks/record_baseline.py +0 -0
  149. {commkit-3.2.0 → commkit-3.3.0}/benchmarks/workloads.py +0 -0
  150. {commkit-3.2.0 → commkit-3.3.0}/commkit/_array.py +0 -0
  151. {commkit-3.2.0 → commkit-3.3.0}/commkit/_circular.py +0 -0
  152. {commkit-3.2.0 → commkit-3.3.0}/commkit/_constants.py +0 -0
  153. {commkit-3.2.0 → commkit-3.3.0}/commkit/_cuda/__init__.py +0 -0
  154. {commkit-3.2.0 → commkit-3.3.0}/commkit/_cuda/compiler.py +0 -0
  155. {commkit-3.2.0 → commkit-3.3.0}/commkit/_cuda/graphs.py +0 -0
  156. {commkit-3.2.0 → commkit-3.3.0}/commkit/_cuda/src/algebraic.cu +0 -0
  157. {commkit-3.2.0 → commkit-3.3.0}/commkit/_cuda/src/bps_anchor.cu +0 -0
  158. {commkit-3.2.0 → commkit-3.3.0}/commkit/_cuda/src/bps_min_d2.cu +0 -0
  159. {commkit-3.2.0 → commkit-3.3.0}/commkit/_cuda/src/cs_block.cu +0 -0
  160. {commkit-3.2.0 → commkit-3.3.0}/commkit/_cuda/src/iir.cu +0 -0
  161. {commkit-3.2.0 → commkit-3.3.0}/commkit/_cuda/src/ldpc.cu +0 -0
  162. {commkit-3.2.0 → commkit-3.3.0}/commkit/_cuda/src/llr.cu +0 -0
  163. {commkit-3.2.0 → commkit-3.3.0}/commkit/_cuda/src/selftest.cu +0 -0
  164. {commkit-3.2.0 → commkit-3.3.0}/commkit/_dispersion.py +0 -0
  165. {commkit-3.2.0 → commkit-3.3.0}/commkit/_fft.py +0 -0
  166. {commkit-3.2.0 → commkit-3.3.0}/commkit/_fiber.py +0 -0
  167. {commkit-3.2.0 → commkit-3.3.0}/commkit/_overlap_save.py +0 -0
  168. {commkit-3.2.0 → commkit-3.3.0}/commkit/_random.py +0 -0
  169. {commkit-3.2.0 → commkit-3.3.0}/commkit/_sequences.py +0 -0
  170. {commkit-3.2.0 → commkit-3.3.0}/commkit/analysis/__init__.py +0 -0
  171. {commkit-3.2.0 → commkit-3.3.0}/commkit/analysis/_common.py +0 -0
  172. {commkit-3.2.0 → commkit-3.3.0}/commkit/analysis/allan.py +0 -0
  173. {commkit-3.2.0 → commkit-3.3.0}/commkit/analysis/drift.py +0 -0
  174. {commkit-3.2.0 → commkit-3.3.0}/commkit/analysis/fm_noise.py +0 -0
  175. {commkit-3.2.0 → commkit-3.3.0}/commkit/analysis/interferometry.py +0 -0
  176. {commkit-3.2.0 → commkit-3.3.0}/commkit/analysis/linewidth/__init__.py +0 -0
  177. {commkit-3.2.0 → commkit-3.3.0}/commkit/analysis/linewidth/dsh.py +0 -0
  178. {commkit-3.2.0 → commkit-3.3.0}/commkit/analysis/linewidth/phase.py +0 -0
  179. {commkit-3.2.0 → commkit-3.3.0}/commkit/analysis/linewidth/result.py +0 -0
  180. {commkit-3.2.0 → commkit-3.3.0}/commkit/analysis/trajectory.py +0 -0
  181. {commkit-3.2.0 → commkit-3.3.0}/commkit/backend.py +0 -0
  182. {commkit-3.2.0 → commkit-3.3.0}/commkit/coding/__init__.py +0 -0
  183. {commkit-3.2.0 → commkit-3.3.0}/commkit/coding/_galois.py +0 -0
  184. {commkit-3.2.0 → commkit-3.3.0}/commkit/coding/_gf2.py +0 -0
  185. {commkit-3.2.0 → commkit-3.3.0}/commkit/coding/_ldpc_tables.py +0 -0
  186. {commkit-3.2.0 → commkit-3.3.0}/commkit/coding/_numba_loops.py +0 -0
  187. {commkit-3.2.0 → commkit-3.3.0}/commkit/coding/algebraic.py +0 -0
  188. {commkit-3.2.0 → commkit-3.3.0}/commkit/coding/base.py +0 -0
  189. {commkit-3.2.0 → commkit-3.3.0}/commkit/coding/convolutional.py +0 -0
  190. {commkit-3.2.0 → commkit-3.3.0}/commkit/coding/crc.py +0 -0
  191. {commkit-3.2.0 → commkit-3.3.0}/commkit/coding/exit.py +0 -0
  192. {commkit-3.2.0 → commkit-3.3.0}/commkit/coding/interleaving.py +0 -0
  193. {commkit-3.2.0 → commkit-3.3.0}/commkit/coding/ldpc.py +0 -0
  194. {commkit-3.2.0 → commkit-3.3.0}/commkit/coding/polar.py +0 -0
  195. {commkit-3.2.0 → commkit-3.3.0}/commkit/coding/product.py +0 -0
  196. {commkit-3.2.0 → commkit-3.3.0}/commkit/coding/ratematch.py +0 -0
  197. {commkit-3.2.0 → commkit-3.3.0}/commkit/coding/shaping.py +0 -0
  198. {commkit-3.2.0 → commkit-3.3.0}/commkit/coding/turbo.py +0 -0
  199. {commkit-3.2.0 → commkit-3.3.0}/commkit/core/__init__.py +0 -0
  200. {commkit-3.2.0 → commkit-3.3.0}/commkit/core/_signal_adapter.py +0 -0
  201. {commkit-3.2.0 → commkit-3.3.0}/commkit/core/frame.py +0 -0
  202. {commkit-3.2.0 → commkit-3.3.0}/commkit/core/generation.py +0 -0
  203. {commkit-3.2.0 → commkit-3.3.0}/commkit/core/signal.py +0 -0
  204. {commkit-3.2.0 → commkit-3.3.0}/commkit/equalization/__init__.py +0 -0
  205. {commkit-3.2.0 → commkit-3.3.0}/commkit/equalization/_block/__init__.py +0 -0
  206. {commkit-3.2.0 → commkit-3.3.0}/commkit/equalization/_block/_dd.py +0 -0
  207. {commkit-3.2.0 → commkit-3.3.0}/commkit/equalization/_kernels_numba.py +0 -0
  208. {commkit-3.2.0 → commkit-3.3.0}/commkit/equalization/linear.py +0 -0
  209. {commkit-3.2.0 → commkit-3.3.0}/commkit/equalization/pilots.py +0 -0
  210. {commkit-3.2.0 → commkit-3.3.0}/commkit/equalization/polarization.py +0 -0
  211. {commkit-3.2.0 → commkit-3.3.0}/commkit/equalization/sequential/__init__.py +0 -0
  212. {commkit-3.2.0 → commkit-3.3.0}/commkit/equalization/sequential/_blind.py +0 -0
  213. {commkit-3.2.0 → commkit-3.3.0}/commkit/filtering/fiber.py +0 -0
  214. {commkit-3.2.0 → commkit-3.3.0}/commkit/filtering/pulses.py +0 -0
  215. {commkit-3.2.0 → commkit-3.3.0}/commkit/impairments/__init__.py +0 -0
  216. {commkit-3.2.0 → commkit-3.3.0}/commkit/impairments/channel/__init__.py +0 -0
  217. {commkit-3.2.0 → commkit-3.3.0}/commkit/impairments/channel/linear.py +0 -0
  218. {commkit-3.2.0 → commkit-3.3.0}/commkit/impairments/channel/nonlinear.py +0 -0
  219. {commkit-3.2.0 → commkit-3.3.0}/commkit/impairments/frontend.py +0 -0
  220. {commkit-3.2.0 → commkit-3.3.0}/commkit/impairments/noise.py +0 -0
  221. {commkit-3.2.0 → commkit-3.3.0}/commkit/impairments/source.py +0 -0
  222. {commkit-3.2.0 → commkit-3.3.0}/commkit/io.py +0 -0
  223. {commkit-3.2.0 → commkit-3.3.0}/commkit/logger.py +0 -0
  224. {commkit-3.2.0 → commkit-3.3.0}/commkit/mapping/__init__.py +0 -0
  225. {commkit-3.2.0 → commkit-3.3.0}/commkit/mapping/_numba_llr.py +0 -0
  226. {commkit-3.2.0 → commkit-3.3.0}/commkit/mapping/bits.py +0 -0
  227. {commkit-3.2.0 → commkit-3.3.0}/commkit/mapping/constellation.py +0 -0
  228. {commkit-3.2.0 → commkit-3.3.0}/commkit/mapping/gray.py +0 -0
  229. {commkit-3.2.0 → commkit-3.3.0}/commkit/mapping/llr.py +0 -0
  230. {commkit-3.2.0 → commkit-3.3.0}/commkit/mapping/shaping.py +0 -0
  231. {commkit-3.2.0 → commkit-3.3.0}/commkit/math.py +0 -0
  232. {commkit-3.2.0 → commkit-3.3.0}/commkit/metrics.py +0 -0
  233. {commkit-3.2.0 → commkit-3.3.0}/commkit/multirate.py +0 -0
  234. {commkit-3.2.0 → commkit-3.3.0}/commkit/py.typed +0 -0
  235. {commkit-3.2.0 → commkit-3.3.0}/commkit/recovery/__init__.py +0 -0
  236. {commkit-3.2.0 → commkit-3.3.0}/commkit/recovery/_common.py +0 -0
  237. {commkit-3.2.0 → commkit-3.3.0}/commkit/recovery/bps.py +0 -0
  238. {commkit-3.2.0 → commkit-3.3.0}/commkit/recovery/carrier_phase.py +0 -0
  239. {commkit-3.2.0 → commkit-3.3.0}/commkit/recovery/pilots.py +0 -0
  240. {commkit-3.2.0 → commkit-3.3.0}/commkit/recovery/viterbi_viterbi.py +0 -0
  241. {commkit-3.2.0 → commkit-3.3.0}/commkit/smoothing.py +0 -0
  242. {commkit-3.2.0 → commkit-3.3.0}/commkit/spectral.py +0 -0
  243. {commkit-3.2.0 → commkit-3.3.0}/examples/00-getting-started.ipynb +0 -0
  244. {commkit-3.2.0 → commkit-3.3.0}/examples/23-carrier-phase-characterization.ipynb +0 -0
  245. {commkit-3.2.0 → commkit-3.3.0}/examples/24-laser-linewidth-self-heterodyne.ipynb +0 -0
  246. {commkit-3.2.0 → commkit-3.3.0}/examples/25-laser-linewidth-short-delay-and-flicker.ipynb +0 -0
  247. {commkit-3.2.0 → commkit-3.3.0}/examples/26-laser-linewidth-self-homodyne-iq.ipynb +0 -0
  248. {commkit-3.2.0 → commkit-3.3.0}/examples/27-measurement-template-self-heterodyne.ipynb +0 -0
  249. {commkit-3.2.0 → commkit-3.3.0}/examples/28-measurement-template-self-homodyne-iq.ipynb +0 -0
  250. {commkit-3.2.0 → commkit-3.3.0}/examples/README.md +0 -0
  251. {commkit-3.2.0 → commkit-3.3.0}/tests/analysis/test_drift.py +0 -0
  252. {commkit-3.2.0 → commkit-3.3.0}/tests/analysis/test_fm_noise.py +0 -0
  253. {commkit-3.2.0 → commkit-3.3.0}/tests/analysis/test_interferometry.py +0 -0
  254. {commkit-3.2.0 → commkit-3.3.0}/tests/analysis/test_trajectory.py +0 -0
  255. {commkit-3.2.0 → commkit-3.3.0}/tests/coding/test_algebraic.py +0 -0
  256. {commkit-3.2.0 → commkit-3.3.0}/tests/coding/test_coding_base.py +0 -0
  257. {commkit-3.2.0 → commkit-3.3.0}/tests/coding/test_convolutional.py +0 -0
  258. {commkit-3.2.0 → commkit-3.3.0}/tests/coding/test_crc.py +0 -0
  259. {commkit-3.2.0 → commkit-3.3.0}/tests/coding/test_exit.py +0 -0
  260. {commkit-3.2.0 → commkit-3.3.0}/tests/coding/test_gf2.py +0 -0
  261. {commkit-3.2.0 → commkit-3.3.0}/tests/coding/test_interleaving.py +0 -0
  262. {commkit-3.2.0 → commkit-3.3.0}/tests/coding/test_ldpc_exit.py +0 -0
  263. {commkit-3.2.0 → commkit-3.3.0}/tests/coding/test_ldpc_kernel.py +0 -0
  264. {commkit-3.2.0 → commkit-3.3.0}/tests/coding/test_ldpc_tables.py +0 -0
  265. {commkit-3.2.0 → commkit-3.3.0}/tests/coding/test_linear_codes.py +0 -0
  266. {commkit-3.2.0 → commkit-3.3.0}/tests/coding/test_polar.py +0 -0
  267. {commkit-3.2.0 → commkit-3.3.0}/tests/common/coding.py +0 -0
  268. {commkit-3.2.0 → commkit-3.3.0}/tests/common/conversions.py +0 -0
  269. {commkit-3.2.0 → commkit-3.3.0}/tests/common/kernel_utils.py +0 -0
  270. {commkit-3.2.0 → commkit-3.3.0}/tests/common/memory.py +0 -0
  271. {commkit-3.2.0 → commkit-3.3.0}/tests/common/periodic.py +0 -0
  272. {commkit-3.2.0 → commkit-3.3.0}/tests/common/reference_impl.py +0 -0
  273. {commkit-3.2.0 → commkit-3.3.0}/tests/core/test_generation.py +0 -0
  274. {commkit-3.2.0 → commkit-3.3.0}/tests/core/test_psqam.py +0 -0
  275. {commkit-3.2.0 → commkit-3.3.0}/tests/core/test_signal_adapter.py +0 -0
  276. {commkit-3.2.0 → commkit-3.3.0}/tests/core/test_signal_mimo.py +0 -0
  277. {commkit-3.2.0 → commkit-3.3.0}/tests/equalization/test_bps_kernel.py +0 -0
  278. {commkit-3.2.0 → commkit-3.3.0}/tests/equalization/test_cs_kernel.py +0 -0
  279. {commkit-3.2.0 → commkit-3.3.0}/tests/mapping/test_constellation.py +0 -0
  280. {commkit-3.2.0 → commkit-3.3.0}/tests/mapping/test_gray.py +0 -0
  281. {commkit-3.2.0 → commkit-3.3.0}/tests/mapping/test_llr.py +0 -0
  282. {commkit-3.2.0 → commkit-3.3.0}/tests/plotting/test_plot_theme.py +0 -0
  283. {commkit-3.2.0 → commkit-3.3.0}/tests/plugins/__init__.py +0 -0
  284. {commkit-3.2.0 → commkit-3.3.0}/tests/plugins/precision_trace.py +0 -0
  285. {commkit-3.2.0 → commkit-3.3.0}/tests/recovery/test_joint.py +0 -0
  286. {commkit-3.2.0 → commkit-3.3.0}/tests/recovery/test_recovery_oracles.py +0 -0
  287. {commkit-3.2.0 → commkit-3.3.0}/tests/test_array.py +0 -0
  288. {commkit-3.2.0 → commkit-3.3.0}/tests/test_backend.py +0 -0
  289. {commkit-3.2.0 → commkit-3.3.0}/tests/test_cuda_infra.py +0 -0
  290. {commkit-3.2.0 → commkit-3.3.0}/tests/test_dispersion.py +0 -0
  291. {commkit-3.2.0 → commkit-3.3.0}/tests/test_examples.py +0 -0
  292. {commkit-3.2.0 → commkit-3.3.0}/tests/test_fiber.py +0 -0
  293. {commkit-3.2.0 → commkit-3.3.0}/tests/test_import_hygiene.py +0 -0
  294. {commkit-3.2.0 → commkit-3.3.0}/tests/test_logger.py +0 -0
  295. {commkit-3.2.0 → commkit-3.3.0}/tests/test_math.py +0 -0
  296. {commkit-3.2.0 → commkit-3.3.0}/tests/test_signal_pipeline.py +0 -0
  297. {commkit-3.2.0 → commkit-3.3.0}/tests/test_smoothing.py +0 -0
  298. {commkit-3.2.0 → commkit-3.3.0}/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.
@@ -298,13 +299,24 @@ Docstrings describe the code as it is; a change in behavior updates them in the
298
299
 
299
300
  ## 7. Tests
300
301
 
301
- - **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(...)`.
302
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.
303
304
  - **Shared builders** live in `tests/common/` and use the public API.
304
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.
305
306
  - Do not duplicate these checks in per-module tests.
306
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.
307
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.
308
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.
309
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.
310
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.
@@ -370,6 +382,10 @@ Steps, once CuPy 14.3 (with cp315 wheels for `cupy-cuda13x`) and a fixed `jupyte
370
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.
371
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.
372
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
+
373
389
  ### Nonlinear fiber channel: future features
374
390
 
375
391
  Each is an addition that leaves the current API unchanged.
@@ -4,6 +4,51 @@ Notable changes to CommKit, newest first. Performance figures are from the refer
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [3.3.0] - 2026-11-03
8
+
9
+ 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.
10
+
11
+ ### Changes that may need code updates
12
+
13
+ - `plot_filter_response` raises `ValueError` when `ax` holds other than four axes for the dashboard (or one for a selected `quantity=`), as the other multi-panel plots do; it used to log a warning and open a new figure. Pass the right number of axes, or none.
14
+
15
+ ### Added
16
+
17
+ - `filter_response` (returning a `FilterResponse` of frequencies, complex response and group delay), `impulse_response` and `step_response` compute the responses of FIR taps, second-order sections or a `(b, a)` pair on the coefficients' device. The group delay of SOS filters is summed section by section, so high orders keep their accuracy; real IIR time responses run through `iir_filter`, in double-precision state whatever the coefficients' precision.
18
+ - Multi-panel diagnostics (`plot_filter_response`, `plot_equalizer_result`, `plot_zf_equalizer_response`, `plot_llr_distribution`, `plot_parity_check`, `plot_timing_correlation`, `plot_mm_autocorrelation`, `plot_frequency_offset_blockwise_result`, `plot_pilot_phase_estimate`, `plot_pilot_tone_phase_estimate`, `plot_pilot_tones_phase_estimate`) take `quantity=` to draw one panel on one axis.
19
+ - Comparisons on one axis: the single-panel plots and a selected `quantity=` take `label=`, `xlim=`, `ylim=` and `legend=` (`True` shows, `False` hides, `None` keeps the plot's default), and the plots with fits, guides or markers take `annotations=False` to draw only the measured curves of an overlay. `plot_eye_diagram(quantity="i" | "q")` draws one component; `plot_carrier_phase_characterization` accepts existing axes.
20
+ - The example notebooks explain what each receiver result shows, their subplot grids scale the theme's figure size, and notebooks 13 and 16 overlay filter, smoothing, shaping and equalizer comparisons on one axis.
21
+
22
+ ### Development
23
+
24
+ - The transfer audit documents the Tikhonov smoother's host round trip, which it flagged as undocumented.
25
+ - `bench_filter_response` times responses on the DFT grid and on an arbitrary grid at 31, 257 and 4097 taps.
26
+
27
+ ## [3.2.1] - 2026-11-02
28
+
29
+ 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.
30
+
31
+ ### Changes that alter results
32
+
33
+ - **`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).
34
+ - **`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.
35
+ - **`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.
36
+ - **`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`.
37
+
38
+ - **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.
39
+
40
+ ### Fixed
41
+
42
+ - **`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.
43
+ - 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.
44
+ - `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.
45
+
46
+ ### Development
47
+
48
+ - 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.
49
+ - 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.
50
+ - `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.
51
+
7
52
  ## [3.2.0] - 2026-10-30
8
53
 
9
54
  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.
@@ -318,7 +363,9 @@ See the [GitHub release](https://github.com/lokgar/commkit/releases/tag/v1.1.0)
318
363
 
319
364
  First release. See the [GitHub release](https://github.com/lokgar/commkit/releases/tag/v1.0.0).
320
365
 
321
- [Unreleased]: https://github.com/lokgar/commkit/compare/v3.2.0...HEAD
366
+ [Unreleased]: https://github.com/lokgar/commkit/compare/v3.3.0...HEAD
367
+ [3.3.0]: https://github.com/lokgar/commkit/compare/v3.2.1...v3.3.0
368
+ [3.2.1]: https://github.com/lokgar/commkit/compare/v3.2.0...v3.2.1
322
369
  [3.2.0]: https://github.com/lokgar/commkit/compare/v3.1.3...v3.2.0
323
370
  [3.1.3]: https://github.com/lokgar/commkit/compare/v3.1.2...v3.1.3
324
371
  [3.1.2]: https://github.com/lokgar/commkit/compare/v3.1.1...v3.1.2
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: commkit
3
- Version: 3.2.0
3
+ Version: 3.3.0
4
4
  Summary: A Python library for high-performance digital communications research.
5
5
  Project-URL: Homepage, https://github.com/lokgar/commkit
6
6
  Project-URL: Repository, https://github.com/lokgar/commkit
@@ -49,7 +49,7 @@ CommKit covers the receiver chain of coherent and IM/DD links (waveform generati
49
49
 
50
50
  ## The model
51
51
 
52
- Data is a NumPy or CuPy array, or a `Signal` that wraps the samples together with what is known about them. Algorithms are plain functions that take the data and return a new `Signal` or array, or a typed result such as `EqualizerResult` or an `*Estimate`. Plotting functions draw those results; they never compute them.
52
+ Data is a NumPy or CuPy array, or a `Signal` that wraps the samples together with what is known about them. Algorithms are plain functions that take the data and return a new `Signal` or array, or a typed result such as `EqualizerResult` or an `*Estimate`. Plotting functions draw those results or call the public compute functions needed to produce them.
53
53
 
54
54
  - **Things that describe** a modulation, a pulse or a sub-algorithm are small frozen objects written in the call: `lms(rx, cpr=BPS(test_phases=64))`. The same object works standalone: `correct_carrier_phase(y, BPS())`.
55
55
  - **A `Signal` carries facts and ground truth.** Facts are `sampling_rate`, `symbol_rate`, `center_frequency` and `domain`; the carrier of an optical Signal gives its `center_wavelength_nm`, which dispersion, fiber propagation and OSNR noise read. The description is `constellation` and `pulse`. The ground truth is `reference`: the transmitted symbols, the bits on the air and the source bits before coding or distribution matching (the same array when there is neither). Functions take facts from the Signal and raise on a conflicting argument. Choices such as the decision constellation default to the Signal's and can be overridden.
@@ -87,6 +87,32 @@ res2 = ck.equalization.lms(rx, num_taps=21, step_size=1e-3, cpr=cpr, state=res.s
87
87
 
88
88
  Without CuPy, pass `device="cpu"` (the default): the same code runs on the CPU.
89
89
 
90
+ ## Comparing filter responses
91
+
92
+ Calculate a response on the coefficients' device, or overlay selected panels
93
+ of several filters:
94
+
95
+ ```python
96
+ short = ck.filtering.fir_taps(sampling_rate=1e6, num_taps=21, cutoff=100e3)
97
+ long = ck.filtering.fir_taps(sampling_rate=1e6, num_taps=101, cutoff=100e3)
98
+ response = ck.filtering.filter_response(long, sampling_rate=1e6)
99
+ print(response.frequencies, response.response, response.group_delay)
100
+
101
+ ck.plotting.apply_default_theme()
102
+ _, ax = ck.plotting.plot_filter_response(
103
+ short, sampling_rate=1e6, quantity="magnitude", label="21 taps"
104
+ )
105
+ ck.plotting.plot_filter_response(
106
+ long, sampling_rate=1e6, quantity="magnitude", ax=ax,
107
+ label="101 taps", legend=True, show=True,
108
+ )
109
+ ```
110
+
111
+ See notebook 03 for filter and smoothing comparisons. The multi-panel
112
+ equalizer, timing, recovery and coding diagnostics take `quantity=` in the
113
+ same way: draw a second result on the returned axis with its own `label=`,
114
+ and `annotations=False` to leave out repeated fits and guides.
115
+
90
116
  ## CPU and GPU
91
117
 
92
118
  CommKit never picks a device for you: data stays where you put it, and each function runs on the device of its input. A few habits get the speed out of a GPU:
@@ -116,7 +142,7 @@ CommKit never picks a device for you: data stays where you put it, and each func
116
142
  | --- | --- |
117
143
  | [`commkit.core`](https://github.com/lokgar/commkit/tree/main/commkit/core) | `Signal`, `Reference`, `generate` (uniform, shaped through a distribution matcher, or coded with `code=`), `SingleCarrierFrame` and `Preamble` (pilots, guard intervals, MIMO channels), `extract_payload`. |
118
144
  | [`commkit.mapping`](https://github.com/lokgar/commkit/tree/main/commkit/mapping) | `Constellation` (QAM, PSK, PAM, arbitrary points; Gray labels; probabilistic shaping with `.shaped()`), bit mapping, hard demapping, max-log and exact LLRs. |
119
- | [`commkit.filtering`](https://github.com/lokgar/commkit/tree/main/commkit/filtering) | Pulses (`RRC`, `RC`, `Gaussian`, `Rect`, `SmoothRect`), FIR and IIR design (Butterworth, Chebyshev I/II, elliptic, Bessel), `fir_filter`, `iir_filter`, `matched_filter`, overlap-save, chromatic-dispersion compensation and digital backpropagation (`correct_fiber_propagation`). |
145
+ | [`commkit.filtering`](https://github.com/lokgar/commkit/tree/main/commkit/filtering) | Pulses (`RRC`, `RC`, `Gaussian`, `Rect`, `SmoothRect`), FIR and IIR design (Butterworth, Chebyshev I/II, elliptic, Bessel), numerical filter responses, `fir_filter`, `iir_filter`, `matched_filter`, overlap-save, chromatic-dispersion compensation and digital backpropagation (`correct_fiber_propagation`). |
120
146
  | [`commkit.multirate`](https://github.com/lokgar/commkit/blob/main/commkit/multirate.py) | `resample` (fractional), `decimate`, `upsample`, `decimate_to_symbol_rate`. |
121
147
  | [`commkit.spectral`](https://github.com/lokgar/commkit/blob/main/commkit/spectral.py) | Welch PSD, spectrograms, frequency shifting, pilot tones. |
122
148
  | [`commkit.impairments`](https://github.com/lokgar/commkit/tree/main/commkit/impairments) | AWGN at an Es/N0 (measured as energy per symbol) or of an absolute variance, ASE at an OSNR (`apply_ase`), phase noise, IQ imbalance (blind estimate; Löwdin and Gram-Schmidt correction), chromatic dispersion, PMD and polarization mixing, and nonlinear fiber propagation over amplified spans (split-step Kerr effect with dispersion, loss, PMD and ASE) of a `Fiber`. |
@@ -13,7 +13,7 @@ CommKit covers the receiver chain of coherent and IM/DD links (waveform generati
13
13
 
14
14
  ## The model
15
15
 
16
- Data is a NumPy or CuPy array, or a `Signal` that wraps the samples together with what is known about them. Algorithms are plain functions that take the data and return a new `Signal` or array, or a typed result such as `EqualizerResult` or an `*Estimate`. Plotting functions draw those results; they never compute them.
16
+ Data is a NumPy or CuPy array, or a `Signal` that wraps the samples together with what is known about them. Algorithms are plain functions that take the data and return a new `Signal` or array, or a typed result such as `EqualizerResult` or an `*Estimate`. Plotting functions draw those results or call the public compute functions needed to produce them.
17
17
 
18
18
  - **Things that describe** a modulation, a pulse or a sub-algorithm are small frozen objects written in the call: `lms(rx, cpr=BPS(test_phases=64))`. The same object works standalone: `correct_carrier_phase(y, BPS())`.
19
19
  - **A `Signal` carries facts and ground truth.** Facts are `sampling_rate`, `symbol_rate`, `center_frequency` and `domain`; the carrier of an optical Signal gives its `center_wavelength_nm`, which dispersion, fiber propagation and OSNR noise read. The description is `constellation` and `pulse`. The ground truth is `reference`: the transmitted symbols, the bits on the air and the source bits before coding or distribution matching (the same array when there is neither). Functions take facts from the Signal and raise on a conflicting argument. Choices such as the decision constellation default to the Signal's and can be overridden.
@@ -51,6 +51,32 @@ res2 = ck.equalization.lms(rx, num_taps=21, step_size=1e-3, cpr=cpr, state=res.s
51
51
 
52
52
  Without CuPy, pass `device="cpu"` (the default): the same code runs on the CPU.
53
53
 
54
+ ## Comparing filter responses
55
+
56
+ Calculate a response on the coefficients' device, or overlay selected panels
57
+ of several filters:
58
+
59
+ ```python
60
+ short = ck.filtering.fir_taps(sampling_rate=1e6, num_taps=21, cutoff=100e3)
61
+ long = ck.filtering.fir_taps(sampling_rate=1e6, num_taps=101, cutoff=100e3)
62
+ response = ck.filtering.filter_response(long, sampling_rate=1e6)
63
+ print(response.frequencies, response.response, response.group_delay)
64
+
65
+ ck.plotting.apply_default_theme()
66
+ _, ax = ck.plotting.plot_filter_response(
67
+ short, sampling_rate=1e6, quantity="magnitude", label="21 taps"
68
+ )
69
+ ck.plotting.plot_filter_response(
70
+ long, sampling_rate=1e6, quantity="magnitude", ax=ax,
71
+ label="101 taps", legend=True, show=True,
72
+ )
73
+ ```
74
+
75
+ See notebook 03 for filter and smoothing comparisons. The multi-panel
76
+ equalizer, timing, recovery and coding diagnostics take `quantity=` in the
77
+ same way: draw a second result on the returned axis with its own `label=`,
78
+ and `annotations=False` to leave out repeated fits and guides.
79
+
54
80
  ## CPU and GPU
55
81
 
56
82
  CommKit never picks a device for you: data stays where you put it, and each function runs on the device of its input. A few habits get the speed out of a GPU:
@@ -80,7 +106,7 @@ CommKit never picks a device for you: data stays where you put it, and each func
80
106
  | --- | --- |
81
107
  | [`commkit.core`](https://github.com/lokgar/commkit/tree/main/commkit/core) | `Signal`, `Reference`, `generate` (uniform, shaped through a distribution matcher, or coded with `code=`), `SingleCarrierFrame` and `Preamble` (pilots, guard intervals, MIMO channels), `extract_payload`. |
82
108
  | [`commkit.mapping`](https://github.com/lokgar/commkit/tree/main/commkit/mapping) | `Constellation` (QAM, PSK, PAM, arbitrary points; Gray labels; probabilistic shaping with `.shaped()`), bit mapping, hard demapping, max-log and exact LLRs. |
83
- | [`commkit.filtering`](https://github.com/lokgar/commkit/tree/main/commkit/filtering) | Pulses (`RRC`, `RC`, `Gaussian`, `Rect`, `SmoothRect`), FIR and IIR design (Butterworth, Chebyshev I/II, elliptic, Bessel), `fir_filter`, `iir_filter`, `matched_filter`, overlap-save, chromatic-dispersion compensation and digital backpropagation (`correct_fiber_propagation`). |
109
+ | [`commkit.filtering`](https://github.com/lokgar/commkit/tree/main/commkit/filtering) | Pulses (`RRC`, `RC`, `Gaussian`, `Rect`, `SmoothRect`), FIR and IIR design (Butterworth, Chebyshev I/II, elliptic, Bessel), numerical filter responses, `fir_filter`, `iir_filter`, `matched_filter`, overlap-save, chromatic-dispersion compensation and digital backpropagation (`correct_fiber_propagation`). |
84
110
  | [`commkit.multirate`](https://github.com/lokgar/commkit/blob/main/commkit/multirate.py) | `resample` (fractional), `decimate`, `upsample`, `decimate_to_symbol_rate`. |
85
111
  | [`commkit.spectral`](https://github.com/lokgar/commkit/blob/main/commkit/spectral.py) | Welch PSD, spectrograms, frequency shifting, pilot tones. |
86
112
  | [`commkit.impairments`](https://github.com/lokgar/commkit/tree/main/commkit/impairments) | AWGN at an Es/N0 (measured as energy per symbol) or of an absolute variance, ASE at an OSNR (`apply_ase`), phase noise, IQ imbalance (blind estimate; Löwdin and Gram-Schmidt correction), chromatic dispersion, PMD and polarization mixing, and nonlinear fiber propagation over amplified spans (split-step Kerr effect with dispersion, loss, PMD and ASE) of a `Fiber`. |
@@ -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)
@@ -4,12 +4,15 @@
4
4
  (1% of the sampling rate, poles near z = 1) on dual-channel complex64; on the
5
5
  GPU it times the double-precision ``cupyx.scipy.signal`` path.
6
6
  ``bench_moving_average_valid`` smooths a two-channel float32 phase record.
7
+ ``bench_filter_response`` evaluates the response and group delay of
8
+ low-pass FIRs on the default 4096-point grid (FFTs) and on an arbitrary
9
+ grid of as many points (Horner evaluation).
7
10
  """
8
11
 
9
12
  import pytest
10
13
  from workloads import FS, bps_workload, phase_workload
11
14
 
12
- from commkit.filtering import butterworth_sos, iir_filter
15
+ from commkit.filtering import butterworth_sos, filter_response, fir_taps, iir_filter
13
16
  from commkit.smoothing import moving_average
14
17
 
15
18
  ROUNDS = dict(rounds=3, warmup_rounds=1, iterations=1)
@@ -39,3 +42,18 @@ def bench_moving_average_valid(benchmark, backend_device, xp, sync, window):
39
42
  return out
40
43
 
41
44
  benchmark.pedantic(run, **ROUNDS)
45
+
46
+
47
+ @pytest.mark.parametrize("grid", ["dft", "custom"])
48
+ @pytest.mark.parametrize("num_taps", [31, 257, 4097])
49
+ def bench_filter_response(benchmark, backend_device, xp, sync, num_taps, grid):
50
+ taps = fir_taps(sampling_rate=FS, num_taps=num_taps, cutoff=0.1 * FS)
51
+ taps = xp.asarray(taps, dtype=xp.float32)
52
+ frequencies = xp.linspace(-0.5, 0.5, 4096) if grid == "custom" else None
53
+
54
+ def run():
55
+ result = filter_response(taps, nfft=4096, frequencies=frequencies)
56
+ sync()
57
+ return result
58
+
59
+ benchmark.pedantic(run, **ROUNDS)
@@ -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.2.0"
34
+ __version__ = "3.3.0"
35
35
 
36
36
  from .backend import PrecisionWarning
37
37
  from .core import (
@@ -247,7 +247,9 @@ class Chase:
247
247
  code, ``Syndrome(max_weight=1)`` rejects test words with two errors.
248
248
  beta : float, optional
249
249
  Soft-output magnitude of bits without a competitor; ``None`` takes
250
- the largest competitor magnitude of the same word.
250
+ the largest competitor magnitude of the same word, or, when no
251
+ candidate of the word differs from the decision anywhere, the
252
+ word's largest input ``|LLR|``.
251
253
  """
252
254
 
253
255
  test_positions: int = 4
@@ -74,7 +74,7 @@ def _block_fdaf_blind(
74
74
  pilot positions overridden by the LMS residual. ``r2`` is the Godard radius
75
75
  (CMA) and ``radii_np`` the unique ring radii (RDE), decided by their
76
76
  squared midpoints (``_rde_rings``). ``step_size=None``
77
- is block_rde's default, ``min(1e-3, 0.5 / block_size)``.
77
+ is the default of both, ``min(1e-3, 0.5 / block_size)``.
78
78
  """
79
79
  use_pilots = pilot_ref is not None and pilot_mask is not None
80
80
  equalizer = f"block_{kind}"
@@ -198,7 +198,7 @@ def _prepare_block(
198
198
 
199
199
  samples, xp, _ = dispatch(samples)
200
200
  if xp is np:
201
- logger.warning(
201
+ logger.info(
202
202
  "%s is running on CPU (NumPy). For CPU workloads %s is typically "
203
203
  "faster. Move samples to GPU (CuPy) to benefit from block-FFT "
204
204
  "acceleration.",
@@ -328,7 +328,7 @@ def _unpack_result_numpy(
328
328
 
329
329
  w_history = None
330
330
  if store_weights:
331
- w_history = xp.asarray(w_hist)
331
+ w_history = xp.asarray(w_hist if n_sym is None else w_hist[:n_sym])
332
332
  if was_1d:
333
333
  w_history = w_history[:, 0, 0, :]
334
334
 
@@ -375,7 +375,7 @@ def _validate_sps(sps: Any, num_taps: int) -> None:
375
375
  "proportionally more taps.",
376
376
  sps,
377
377
  )
378
- if num_taps < 2 * sps:
378
+ if sps > 1 and num_taps < 2 * sps: # fractionally spaced only
379
379
  logger.warning(
380
380
  "num_taps=%s is small for sps=%s. Recommend num_taps >= %s "
381
381
  "for fractionally-spaced equalization.",
@@ -150,6 +150,10 @@ def _rls_update(W, P, x_bar, e, Px, xH_P, k_gain, lam_f64, leak_term, update):
150
150
  denom = lam_f64
151
151
  for jj in range(N):
152
152
  denom = denom + (np.conj(x_bar[jj]) * Px[jj]).real
153
+ # A positive-definite P gives denom >= λ; anything else means P has lost
154
+ # it, and rls() reports the divergence.
155
+ if not denom > 0.0:
156
+ raise FloatingPointError("RLS inverse correlation matrix lost definiteness")
153
157
  inv_denom = np.float64(1.0) / denom
154
158
  for ii in range(N):
155
159
  k_gain[ii] = Px[ii] * inv_denom
@@ -79,7 +79,7 @@ def block_cma(
79
79
  *,
80
80
  num_taps: int = 21,
81
81
  sps: int | None = None,
82
- step_size: float = 2e-4,
82
+ step_size: float | None = None,
83
83
  block_size: int = 256,
84
84
  constellation: Any = None,
85
85
  initial_taps: ArrayType | None = None,
@@ -113,10 +113,12 @@ def block_cma(
113
113
  sps : int, optional
114
114
  Samples per symbol, an integer. Taken from the Signal; required for
115
115
  array input. A value that disagrees with the Signal raises.
116
- step_size : float, default 2e-4
116
+ step_size : float, optional
117
117
  Step size on the scale of :func:`cma`. The weights are frozen
118
118
  across each block, so the stability ceiling is about ``block_size``
119
119
  times lower than per symbol; reduce it if the run raises divergence.
120
+ Defaults to ``min(1e-3, 0.5 / block_size)``, as for
121
+ :func:`block_rde`.
120
122
  block_size : int, default 256
121
123
  Output symbols per weight update.
122
124
  constellation : Constellation, optional
@@ -170,7 +170,9 @@ class EqualizerResult:
170
170
  error : ndarray
171
171
  Error signal, ``(N_sym,)`` or ``(C, N_sym)``.
172
172
  weights_history : ndarray or None
173
- Tap weights over time, only with ``store_weights=True``.
173
+ Tap weights over time, only with ``store_weights=True``: one row per
174
+ symbol of ``y_hat`` (RLS's trimmed tail included), the taps that
175
+ produced it.
174
176
  num_train_symbols : int
175
177
  Training symbols consumed in the data-aided phase (LMS/RLS); skip
176
178
  them before steady-state metrics such as EVM, SNR and BER.
@@ -367,13 +367,17 @@ def _check_rls_divergence(
367
367
  actionable error instead of returning garbage weights.
368
368
  """
369
369
  if not bool(xp.isfinite(weights).all()):
370
- raise RuntimeError(
371
- f"RLS equalizer diverged (forgetting_factor={forgetting_factor}, "
372
- f"regularization={regularization}). RLS requires a positive-definite "
373
- "correlation matrix. "
374
- "Try increasing 'regularization', reducing 'forgetting_factor', "
375
- "or adding 'leakage' (e.g. 1e-4) to stabilize fractionally-spaced inputs."
376
- )
370
+ raise RuntimeError(_rls_divergence_message(forgetting_factor, regularization))
371
+
372
+
373
+ def _rls_divergence_message(forgetting_factor: float, regularization: float) -> str:
374
+ return (
375
+ f"RLS equalizer diverged (forgetting_factor={forgetting_factor}, "
376
+ f"regularization={regularization}). RLS requires a positive-definite "
377
+ "correlation matrix. "
378
+ "Try increasing 'regularization', raising 'forgetting_factor' toward 1, "
379
+ "or adding 'leakage' (e.g. 1e-4) to stabilize fractionally-spaced inputs."
380
+ )
377
381
 
378
382
 
379
383
  def rls(
@@ -477,7 +481,8 @@ def rls(
477
481
  Returns
478
482
  -------
479
483
  EqualizerResult
480
- The fields described for :func:`lms`. ``y_hat`` lacks the last
484
+ The fields described for :func:`lms`. ``y_hat``, ``error``,
485
+ ``weights_history`` and ``phase_trajectory`` lack the last
481
486
  ``num_taps // 2`` symbols (``tail_trim``), and ``signal`` carries the
482
487
  reference cut to match.
483
488
 
@@ -565,7 +570,7 @@ def rls(
565
570
  n_update_halt = max(0, run.n_sym - num_taps // 2)
566
571
  tail_trim = num_taps // 2
567
572
  if tail_trim > 0:
568
- logger.warning(
573
+ logger.info(
569
574
  "RLS tail trim: last %s symbols removed from y_hat "
570
575
  "(zero-padding contamination zone). result.signal carries the "
571
576
  "trimmed reference; trim reference arrays to match: "
@@ -663,7 +668,12 @@ def rls(
663
668
 
664
669
  # W and P freeze at n_update_halt, so the state resumes there at the latest.
665
670
  resume = min(run.n_done, n_update_halt)
666
- snap = _run_resumable(run, resume, segment, snapshot)
671
+ try:
672
+ snap = _run_resumable(run, resume, segment, snapshot)
673
+ except FloatingPointError as err: # raised by the kernel when P is no longer PD
674
+ raise RuntimeError(
675
+ _rls_divergence_message(forgetting_factor, regularization)
676
+ ) from err
667
677
  result = _assemble_sequential(
668
678
  run,
669
679
  equalizer="rls",
@@ -397,7 +397,9 @@ def _inline_cpr(cpr: Any, constellation: Any, function_name: str) -> _InlineCpr
397
397
  joint=bool(cpr.joint_channels),
398
398
  symmetry=symmetry,
399
399
  cycle_slip=cycle_slip is not None,
400
- history=slip._history_values(1), # per-symbol phase
400
+ history=slip._history_values( # per-symbol phase
401
+ 1, cpr._slip_history if isinstance(cpr, PLL) else None
402
+ ),
401
403
  threshold=float(slip.threshold),
402
404
  )
403
405
 
@@ -6,6 +6,9 @@
6
6
  - Design: :func:`fir_taps` (window method) and the IIR designs in
7
7
  second-order sections, :func:`butterworth_sos`, :func:`chebyshev1_sos`,
8
8
  :func:`chebyshev2_sos`, :func:`elliptic_sos` and :func:`bessel_sos`.
9
+ - Responses: :func:`filter_response` (a :class:`FilterResponse`),
10
+ :func:`impulse_response` and :func:`step_response`, computed on the
11
+ coefficients' device.
9
12
  - Application: :func:`fir_filter` (FFT convolution, optionally circular),
10
13
  :func:`ols_fir_filter` (overlap-save, for long filters), :func:`iir_filter`
11
14
  and :func:`matched_filter`.
@@ -40,8 +43,10 @@ from .pulses import (
40
43
  Rect,
41
44
  SmoothRect,
42
45
  )
46
+ from .response import FilterResponse, filter_response, impulse_response, step_response
43
47
 
44
48
  __all__ = [
49
+ "FilterResponse",
45
50
  "Gaussian",
46
51
  "Pulse",
47
52
  "RC",
@@ -55,9 +60,12 @@ __all__ = [
55
60
  "correct_chromatic_dispersion",
56
61
  "correct_fiber_propagation",
57
62
  "elliptic_sos",
63
+ "filter_response",
58
64
  "fir_filter",
59
65
  "fir_taps",
60
66
  "iir_filter",
67
+ "impulse_response",
61
68
  "matched_filter",
62
69
  "ols_fir_filter",
70
+ "step_response",
63
71
  ]