commkit 3.6.1__tar.gz → 3.7.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 (322) hide show
  1. {commkit-3.6.1 → commkit-3.7.0}/.github/workflows/ci.yml +3 -1
  2. {commkit-3.6.1 → commkit-3.7.0}/AGENTS.md +7 -4
  3. {commkit-3.6.1 → commkit-3.7.0}/CHANGELOG.md +38 -1
  4. {commkit-3.6.1 → commkit-3.7.0}/PKG-INFO +2 -2
  5. {commkit-3.6.1 → commkit-3.7.0}/README.md +1 -1
  6. {commkit-3.6.1 → commkit-3.7.0}/benchmarks/bench_block_blind.py +9 -9
  7. {commkit-3.6.1 → commkit-3.7.0}/benchmarks/bench_block_lms.py +4 -3
  8. commkit-3.7.0/benchmarks/bench_gardner.py +36 -0
  9. {commkit-3.6.1 → commkit-3.7.0}/benchmarks/workloads.py +29 -0
  10. {commkit-3.6.1 → commkit-3.7.0}/commkit/__init__.py +1 -1
  11. {commkit-3.6.1 → commkit-3.7.0}/commkit/_constants.py +1 -0
  12. {commkit-3.6.1 → commkit-3.7.0}/commkit/_dispersion.py +2 -3
  13. {commkit-3.6.1 → commkit-3.7.0}/commkit/_fiber.py +1 -1
  14. {commkit-3.6.1 → commkit-3.7.0}/commkit/analysis/_common.py +1 -1
  15. {commkit-3.6.1 → commkit-3.7.0}/commkit/coding/exit.py +0 -6
  16. {commkit-3.6.1 → commkit-3.7.0}/commkit/coding/linear.py +1 -2
  17. {commkit-3.6.1 → commkit-3.7.0}/commkit/coding/polar.py +2 -2
  18. {commkit-3.6.1 → commkit-3.7.0}/commkit/coding/shaping.py +4 -7
  19. {commkit-3.6.1 → commkit-3.7.0}/commkit/equalization/_block/_blind.py +6 -6
  20. {commkit-3.6.1 → commkit-3.7.0}/commkit/equalization/_block/_dd.py +25 -15
  21. {commkit-3.6.1 → commkit-3.7.0}/commkit/equalization/_block/_engine.py +0 -1
  22. {commkit-3.6.1 → commkit-3.7.0}/commkit/equalization/_common.py +19 -17
  23. {commkit-3.6.1 → commkit-3.7.0}/commkit/equalization/_sequential/_dd.py +40 -8
  24. {commkit-3.6.1 → commkit-3.7.0}/commkit/equalization/_sequential/_setup.py +0 -1
  25. {commkit-3.6.1 → commkit-3.7.0}/commkit/equalization/linear.py +3 -3
  26. {commkit-3.6.1 → commkit-3.7.0}/commkit/equalization/result.py +82 -10
  27. {commkit-3.6.1 → commkit-3.7.0}/commkit/filtering/fiber.py +1 -10
  28. {commkit-3.6.1 → commkit-3.7.0}/commkit/frequency.py +1 -1
  29. {commkit-3.6.1 → commkit-3.7.0}/commkit/impairments/__init__.py +25 -6
  30. {commkit-3.6.1 → commkit-3.7.0}/commkit/impairments/frontend.py +307 -5
  31. commkit-3.7.0/commkit/impairments/noise.py +685 -0
  32. {commkit-3.6.1 → commkit-3.7.0}/commkit/impairments/source.py +69 -4
  33. {commkit-3.6.1 → commkit-3.7.0}/commkit/mapping/constellation.py +6 -0
  34. {commkit-3.6.1 → commkit-3.7.0}/commkit/metrics/__init__.py +10 -2
  35. commkit-3.7.0/commkit/metrics/awgn.py +515 -0
  36. {commkit-3.6.1 → commkit-3.7.0}/commkit/multirate.py +126 -3
  37. {commkit-3.6.1 → commkit-3.7.0}/commkit/plotting/analysis.py +1 -1
  38. {commkit-3.6.1 → commkit-3.7.0}/commkit/plotting/spectral.py +3 -1
  39. {commkit-3.6.1 → commkit-3.7.0}/commkit/spectral.py +4 -1
  40. commkit-3.7.0/commkit/timing/__init__.py +47 -0
  41. commkit-3.7.0/commkit/timing/_numba_loops.py +185 -0
  42. commkit-3.7.0/commkit/timing/clock_recovery.py +481 -0
  43. commkit-3.6.1/commkit/timing.py → commkit-3.7.0/commkit/timing/synchronization.py +10 -10
  44. {commkit-3.6.1 → commkit-3.7.0}/examples/00-getting-started.ipynb +2 -2
  45. {commkit-3.6.1 → commkit-3.7.0}/examples/01-constellations-and-bit-mapping.ipynb +1 -1
  46. {commkit-3.6.1 → commkit-3.7.0}/examples/02-spectral-analysis.ipynb +2 -2
  47. {commkit-3.6.1 → commkit-3.7.0}/examples/06-awgn-and-performance-metrics.ipynb +45 -28
  48. {commkit-3.6.1 → commkit-3.7.0}/examples/07-channel-coding.ipynb +2 -2
  49. {commkit-3.6.1 → commkit-3.7.0}/examples/09-laser-phase-noise.ipynb +2 -2
  50. {commkit-3.6.1 → commkit-3.7.0}/examples/10-front-end-impairments.ipynb +163 -11
  51. {commkit-3.6.1 → commkit-3.7.0}/examples/11-frame-synchronization-and-timing.ipynb +2 -2
  52. commkit-3.7.0/examples/12-sampling-clock-recovery.ipynb +316 -0
  53. commkit-3.6.1/examples/12-frequency-offset-estimation.ipynb → commkit-3.7.0/examples/13-frequency-offset-estimation.ipynb +3 -3
  54. commkit-3.6.1/examples/13-blind-carrier-phase-recovery.ipynb → commkit-3.7.0/examples/14-blind-carrier-phase-recovery.ipynb +3 -3
  55. commkit-3.6.1/examples/14-pilot-aided-carrier-recovery.ipynb → commkit-3.7.0/examples/15-pilot-aided-carrier-recovery.ipynb +5 -5
  56. commkit-3.6.1/examples/15-linear-channel-equalization.ipynb → commkit-3.7.0/examples/16-linear-channel-equalization.ipynb +3 -3
  57. commkit-3.6.1/examples/16-adaptive-equalization.ipynb → commkit-3.7.0/examples/17-adaptive-equalization.ipynb +8 -8
  58. commkit-3.6.1/examples/17-blind-equalization.ipynb → commkit-3.7.0/examples/18-blind-equalization.ipynb +7 -7
  59. commkit-3.6.1/examples/18-polarization-multiplexing.ipynb → commkit-3.7.0/examples/19-polarization-multiplexing.ipynb +5 -5
  60. commkit-3.6.1/examples/19-chromatic-dispersion.ipynb → commkit-3.7.0/examples/20-chromatic-dispersion.ipynb +4 -4
  61. commkit-3.6.1/examples/20-nonlinear-fiber-and-backpropagation.ipynb → commkit-3.7.0/examples/21-nonlinear-fiber-and-backpropagation.ipynb +4 -4
  62. commkit-3.7.0/examples/22-direct-and-self-coherent-detection.ipynb +278 -0
  63. commkit-3.6.1/examples/21-fec-in-optical-standards.ipynb → commkit-3.7.0/examples/23-fec-in-optical-standards.ipynb +2 -2
  64. commkit-3.6.1/examples/22-coherent-transceiver-end-to-end.ipynb → commkit-3.7.0/examples/24-coherent-transceiver-end-to-end.ipynb +5 -6
  65. commkit-3.6.1/examples/23-carrier-phase-characterization.ipynb → commkit-3.7.0/examples/25-carrier-phase-characterization.ipynb +3 -3
  66. commkit-3.6.1/examples/24-laser-linewidth-self-heterodyne.ipynb → commkit-3.7.0/examples/26-laser-linewidth-self-heterodyne.ipynb +10 -10
  67. commkit-3.6.1/examples/25-laser-linewidth-short-delay-and-flicker.ipynb → commkit-3.7.0/examples/27-laser-linewidth-short-delay-and-flicker.ipynb +5 -5
  68. commkit-3.6.1/examples/26-laser-linewidth-self-homodyne-iq.ipynb → commkit-3.7.0/examples/28-laser-linewidth-self-homodyne-iq.ipynb +5 -5
  69. commkit-3.6.1/examples/27-measurement-template-self-heterodyne.ipynb → commkit-3.7.0/examples/29-measurement-template-self-heterodyne.ipynb +2 -2
  70. commkit-3.6.1/examples/28-measurement-template-self-homodyne-iq.ipynb → commkit-3.7.0/examples/30-measurement-template-self-homodyne-iq.ipynb +2 -2
  71. {commkit-3.6.1 → commkit-3.7.0}/examples/README.md +19 -17
  72. {commkit-3.6.1 → commkit-3.7.0}/pyproject.toml +2 -2
  73. {commkit-3.6.1 → commkit-3.7.0}/tests/coding/test_exit.py +1 -1
  74. {commkit-3.6.1 → commkit-3.7.0}/tests/coding/test_shaping.py +1 -2
  75. {commkit-3.6.1 → commkit-3.7.0}/tests/common/reference_impl.py +97 -3
  76. {commkit-3.6.1 → commkit-3.7.0}/tests/common/signals.py +9 -0
  77. commkit-3.7.0/tests/common/theory.py +246 -0
  78. {commkit-3.6.1 → commkit-3.7.0}/tests/equalization/test_blind.py +10 -7
  79. {commkit-3.6.1 → commkit-3.7.0}/tests/equalization/test_block.py +8 -5
  80. commkit-3.7.0/tests/equalization/test_cold_start.py +104 -0
  81. commkit-3.7.0/tests/equalization/test_result.py +67 -0
  82. {commkit-3.6.1 → commkit-3.7.0}/tests/equalization/test_sequential.py +9 -9
  83. {commkit-3.6.1 → commkit-3.7.0}/tests/impairments/test_fiber_channel_nonlinear.py +94 -2
  84. commkit-3.7.0/tests/impairments/test_frontend.py +593 -0
  85. commkit-3.7.0/tests/impairments/test_noise.py +702 -0
  86. {commkit-3.6.1 → commkit-3.7.0}/tests/impairments/test_source.py +45 -1
  87. commkit-3.7.0/tests/metrics/test_awgn.py +188 -0
  88. {commkit-3.6.1 → commkit-3.7.0}/tests/plugins/transfer_trace.py +4 -0
  89. {commkit-3.6.1 → commkit-3.7.0}/tests/test_api_contracts.py +71 -7
  90. {commkit-3.6.1 → commkit-3.7.0}/tests/test_filtering.py +4 -4
  91. {commkit-3.6.1 → commkit-3.7.0}/tests/test_multirate.py +12 -0
  92. {commkit-3.6.1 → commkit-3.7.0}/tests/test_receiver_chain.py +82 -5
  93. commkit-3.7.0/tests/timing/test_clock_recovery.py +225 -0
  94. commkit-3.6.1/tests/test_timing.py → commkit-3.7.0/tests/timing/test_synchronization.py +3 -2
  95. {commkit-3.6.1 → commkit-3.7.0}/uv.lock +1 -1
  96. commkit-3.6.1/commkit/impairments/noise.py +0 -320
  97. commkit-3.6.1/tests/common/theory.py +0 -101
  98. commkit-3.6.1/tests/impairments/test_frontend.py +0 -271
  99. commkit-3.6.1/tests/impairments/test_noise.py +0 -338
  100. {commkit-3.6.1 → commkit-3.7.0}/.gitattributes +0 -0
  101. {commkit-3.6.1 → commkit-3.7.0}/.github/icon.svg +0 -0
  102. {commkit-3.6.1 → commkit-3.7.0}/.github/logo.svg +0 -0
  103. {commkit-3.6.1 → commkit-3.7.0}/.github/workflows/publish.yml +0 -0
  104. {commkit-3.6.1 → commkit-3.7.0}/.gitignore +0 -0
  105. {commkit-3.6.1 → commkit-3.7.0}/.python-version +0 -0
  106. {commkit-3.6.1 → commkit-3.7.0}/LICENSE +0 -0
  107. {commkit-3.6.1 → commkit-3.7.0}/benchmarks/baselines/Linux-CPython-3.14-64bit/0002_pre_v2.json +0 -0
  108. {commkit-3.6.1 → commkit-3.7.0}/benchmarks/baselines/Linux-CPython-3.14-64bit/0003_v2_0.json +0 -0
  109. {commkit-3.6.1 → commkit-3.7.0}/benchmarks/baselines/Linux-CPython-3.14-64bit/0004_fiber_dbp.json +0 -0
  110. {commkit-3.6.1 → commkit-3.7.0}/benchmarks/baselines/Linux-CPython-3.14-64bit/0005_gpu_decoder_path.json +0 -0
  111. {commkit-3.6.1 → commkit-3.7.0}/benchmarks/baselines/Linux-CPython-3.14-64bit/0006_perf_cleanup.json +0 -0
  112. {commkit-3.6.1 → commkit-3.7.0}/benchmarks/baselines/Linux-CPython-3.14-64bit/0007_v3_0.json +0 -0
  113. {commkit-3.6.1 → commkit-3.7.0}/benchmarks/baselines/Linux-CPython-3.14-64bit/0008_v3_1.json +0 -0
  114. {commkit-3.6.1 → commkit-3.7.0}/benchmarks/bench_bps.py +0 -0
  115. {commkit-3.6.1 → commkit-3.7.0}/benchmarks/bench_coding.py +0 -0
  116. {commkit-3.6.1 → commkit-3.7.0}/benchmarks/bench_equalizers.py +0 -0
  117. {commkit-3.6.1 → commkit-3.7.0}/benchmarks/bench_fiber.py +0 -0
  118. {commkit-3.6.1 → commkit-3.7.0}/benchmarks/bench_filters.py +0 -0
  119. {commkit-3.6.1 → commkit-3.7.0}/benchmarks/bench_import.py +0 -0
  120. {commkit-3.6.1 → commkit-3.7.0}/benchmarks/bench_llr.py +0 -0
  121. {commkit-3.6.1 → commkit-3.7.0}/benchmarks/bench_mapping.py +0 -0
  122. {commkit-3.6.1 → commkit-3.7.0}/benchmarks/bench_signal_pipeline.py +0 -0
  123. {commkit-3.6.1 → commkit-3.7.0}/benchmarks/bench_sync_misc.py +0 -0
  124. {commkit-3.6.1 → commkit-3.7.0}/benchmarks/benchutils.py +0 -0
  125. {commkit-3.6.1 → commkit-3.7.0}/benchmarks/conftest.py +0 -0
  126. {commkit-3.6.1 → commkit-3.7.0}/benchmarks/record_baseline.py +0 -0
  127. {commkit-3.6.1 → commkit-3.7.0}/commkit/_array.py +0 -0
  128. {commkit-3.6.1 → commkit-3.7.0}/commkit/_circular.py +0 -0
  129. {commkit-3.6.1 → commkit-3.7.0}/commkit/_cuda/__init__.py +0 -0
  130. {commkit-3.6.1 → commkit-3.7.0}/commkit/_cuda/compiler.py +0 -0
  131. {commkit-3.6.1 → commkit-3.7.0}/commkit/_cuda/graphs.py +0 -0
  132. {commkit-3.6.1 → commkit-3.7.0}/commkit/_cuda/src/algebraic.cu +0 -0
  133. {commkit-3.6.1 → commkit-3.7.0}/commkit/_cuda/src/bps_anchor.cu +0 -0
  134. {commkit-3.6.1 → commkit-3.7.0}/commkit/_cuda/src/bps_min_d2.cu +0 -0
  135. {commkit-3.6.1 → commkit-3.7.0}/commkit/_cuda/src/cs_block.cu +0 -0
  136. {commkit-3.6.1 → commkit-3.7.0}/commkit/_cuda/src/iir.cu +0 -0
  137. {commkit-3.6.1 → commkit-3.7.0}/commkit/_cuda/src/ldpc.cu +0 -0
  138. {commkit-3.6.1 → commkit-3.7.0}/commkit/_cuda/src/llr.cu +0 -0
  139. {commkit-3.6.1 → commkit-3.7.0}/commkit/_cuda/src/selftest.cu +0 -0
  140. {commkit-3.6.1 → commkit-3.7.0}/commkit/_fft.py +0 -0
  141. {commkit-3.6.1 → commkit-3.7.0}/commkit/_overlap_save.py +0 -0
  142. {commkit-3.6.1 → commkit-3.7.0}/commkit/_random.py +0 -0
  143. {commkit-3.6.1 → commkit-3.7.0}/commkit/_sequences.py +0 -0
  144. {commkit-3.6.1 → commkit-3.7.0}/commkit/analysis/__init__.py +0 -0
  145. {commkit-3.6.1 → commkit-3.7.0}/commkit/analysis/allan.py +0 -0
  146. {commkit-3.6.1 → commkit-3.7.0}/commkit/analysis/drift.py +0 -0
  147. {commkit-3.6.1 → commkit-3.7.0}/commkit/analysis/fm_noise.py +0 -0
  148. {commkit-3.6.1 → commkit-3.7.0}/commkit/analysis/interferometry.py +0 -0
  149. {commkit-3.6.1 → commkit-3.7.0}/commkit/analysis/linewidth/__init__.py +0 -0
  150. {commkit-3.6.1 → commkit-3.7.0}/commkit/analysis/linewidth/dsh.py +0 -0
  151. {commkit-3.6.1 → commkit-3.7.0}/commkit/analysis/linewidth/phase.py +0 -0
  152. {commkit-3.6.1 → commkit-3.7.0}/commkit/analysis/linewidth/result.py +0 -0
  153. {commkit-3.6.1 → commkit-3.7.0}/commkit/analysis/trajectory.py +0 -0
  154. {commkit-3.6.1 → commkit-3.7.0}/commkit/backend.py +0 -0
  155. {commkit-3.6.1 → commkit-3.7.0}/commkit/coding/__init__.py +0 -0
  156. {commkit-3.6.1 → commkit-3.7.0}/commkit/coding/_galois.py +0 -0
  157. {commkit-3.6.1 → commkit-3.7.0}/commkit/coding/_gf2.py +0 -0
  158. {commkit-3.6.1 → commkit-3.7.0}/commkit/coding/_ldpc_tables.py +0 -0
  159. {commkit-3.6.1 → commkit-3.7.0}/commkit/coding/_numba_loops.py +0 -0
  160. {commkit-3.6.1 → commkit-3.7.0}/commkit/coding/algebraic.py +0 -0
  161. {commkit-3.6.1 → commkit-3.7.0}/commkit/coding/base.py +0 -0
  162. {commkit-3.6.1 → commkit-3.7.0}/commkit/coding/convolutional.py +0 -0
  163. {commkit-3.6.1 → commkit-3.7.0}/commkit/coding/crc.py +0 -0
  164. {commkit-3.6.1 → commkit-3.7.0}/commkit/coding/interleaving.py +0 -0
  165. {commkit-3.6.1 → commkit-3.7.0}/commkit/coding/ldpc.py +0 -0
  166. {commkit-3.6.1 → commkit-3.7.0}/commkit/coding/performance.py +0 -0
  167. {commkit-3.6.1 → commkit-3.7.0}/commkit/coding/product.py +0 -0
  168. {commkit-3.6.1 → commkit-3.7.0}/commkit/coding/ratematch.py +0 -0
  169. {commkit-3.6.1 → commkit-3.7.0}/commkit/coding/turbo.py +0 -0
  170. {commkit-3.6.1 → commkit-3.7.0}/commkit/core/__init__.py +0 -0
  171. {commkit-3.6.1 → commkit-3.7.0}/commkit/core/_signal_adapter.py +0 -0
  172. {commkit-3.6.1 → commkit-3.7.0}/commkit/core/frame.py +0 -0
  173. {commkit-3.6.1 → commkit-3.7.0}/commkit/core/generation.py +0 -0
  174. {commkit-3.6.1 → commkit-3.7.0}/commkit/core/preamble.py +0 -0
  175. {commkit-3.6.1 → commkit-3.7.0}/commkit/core/signal.py +0 -0
  176. {commkit-3.6.1 → commkit-3.7.0}/commkit/equalization/__init__.py +0 -0
  177. {commkit-3.6.1 → commkit-3.7.0}/commkit/equalization/_block/__init__.py +0 -0
  178. {commkit-3.6.1 → commkit-3.7.0}/commkit/equalization/_numba_loops.py +0 -0
  179. {commkit-3.6.1 → commkit-3.7.0}/commkit/equalization/_sequential/__init__.py +0 -0
  180. {commkit-3.6.1 → commkit-3.7.0}/commkit/equalization/_sequential/_blind.py +0 -0
  181. {commkit-3.6.1 → commkit-3.7.0}/commkit/equalization/pilots.py +0 -0
  182. {commkit-3.6.1 → commkit-3.7.0}/commkit/equalization/polarization.py +0 -0
  183. {commkit-3.6.1 → commkit-3.7.0}/commkit/filtering/__init__.py +0 -0
  184. {commkit-3.6.1 → commkit-3.7.0}/commkit/filtering/linear.py +0 -0
  185. {commkit-3.6.1 → commkit-3.7.0}/commkit/filtering/pulses.py +0 -0
  186. {commkit-3.6.1 → commkit-3.7.0}/commkit/filtering/response.py +0 -0
  187. {commkit-3.6.1 → commkit-3.7.0}/commkit/impairments/fiber_channel.py +0 -0
  188. {commkit-3.6.1 → commkit-3.7.0}/commkit/io.py +0 -0
  189. {commkit-3.6.1 → commkit-3.7.0}/commkit/logger.py +0 -0
  190. {commkit-3.6.1 → commkit-3.7.0}/commkit/mapping/__init__.py +0 -0
  191. {commkit-3.6.1 → commkit-3.7.0}/commkit/mapping/_numba_llr.py +0 -0
  192. {commkit-3.6.1 → commkit-3.7.0}/commkit/mapping/bits.py +0 -0
  193. {commkit-3.6.1 → commkit-3.7.0}/commkit/mapping/gray.py +0 -0
  194. {commkit-3.6.1 → commkit-3.7.0}/commkit/mapping/llr.py +0 -0
  195. {commkit-3.6.1 → commkit-3.7.0}/commkit/mapping/shaping.py +0 -0
  196. {commkit-3.6.1 → commkit-3.7.0}/commkit/math.py +0 -0
  197. {commkit-3.6.1 → commkit-3.7.0}/commkit/metrics/_common.py +0 -0
  198. {commkit-3.6.1 → commkit-3.7.0}/commkit/metrics/error_rates.py +0 -0
  199. {commkit-3.6.1 → commkit-3.7.0}/commkit/metrics/information.py +0 -0
  200. {commkit-3.6.1 → commkit-3.7.0}/commkit/metrics/quality.py +0 -0
  201. {commkit-3.6.1 → commkit-3.7.0}/commkit/plotting/__init__.py +0 -0
  202. {commkit-3.6.1 → commkit-3.7.0}/commkit/plotting/coding.py +0 -0
  203. {commkit-3.6.1 → commkit-3.7.0}/commkit/plotting/constellation.py +0 -0
  204. {commkit-3.6.1 → commkit-3.7.0}/commkit/plotting/equalizer.py +0 -0
  205. {commkit-3.6.1 → commkit-3.7.0}/commkit/plotting/eye.py +0 -0
  206. {commkit-3.6.1 → commkit-3.7.0}/commkit/plotting/filter_response.py +0 -0
  207. {commkit-3.6.1 → commkit-3.7.0}/commkit/plotting/frequency.py +0 -0
  208. {commkit-3.6.1 → commkit-3.7.0}/commkit/plotting/recovery.py +0 -0
  209. {commkit-3.6.1 → commkit-3.7.0}/commkit/plotting/theme.py +0 -0
  210. {commkit-3.6.1 → commkit-3.7.0}/commkit/plotting/timing.py +0 -0
  211. {commkit-3.6.1 → commkit-3.7.0}/commkit/plotting/waveform.py +0 -0
  212. {commkit-3.6.1 → commkit-3.7.0}/commkit/py.typed +0 -0
  213. {commkit-3.6.1 → commkit-3.7.0}/commkit/recovery/__init__.py +0 -0
  214. {commkit-3.6.1 → commkit-3.7.0}/commkit/recovery/_common.py +0 -0
  215. {commkit-3.6.1 → commkit-3.7.0}/commkit/recovery/bps.py +0 -0
  216. {commkit-3.6.1 → commkit-3.7.0}/commkit/recovery/carrier_phase.py +0 -0
  217. {commkit-3.6.1 → commkit-3.7.0}/commkit/recovery/corrections.py +0 -0
  218. {commkit-3.6.1 → commkit-3.7.0}/commkit/recovery/pilots.py +0 -0
  219. {commkit-3.6.1 → commkit-3.7.0}/commkit/recovery/pll.py +0 -0
  220. {commkit-3.6.1 → commkit-3.7.0}/commkit/recovery/tikhonov.py +0 -0
  221. {commkit-3.6.1 → commkit-3.7.0}/commkit/recovery/viterbi_viterbi.py +0 -0
  222. {commkit-3.6.1 → commkit-3.7.0}/commkit/smoothing.py +0 -0
  223. {commkit-3.6.1 → commkit-3.7.0}/examples/03-digital-filters.ipynb +0 -0
  224. {commkit-3.6.1 → commkit-3.7.0}/examples/04-pulse-shaping-and-matched-filtering.ipynb +0 -0
  225. {commkit-3.6.1 → commkit-3.7.0}/examples/05-multirate-and-resampling.ipynb +0 -0
  226. {commkit-3.6.1 → commkit-3.7.0}/examples/08-probabilistic-shaping-and-coded-modulation.ipynb +0 -0
  227. {commkit-3.6.1 → commkit-3.7.0}/scripts/sanitize.sh +0 -0
  228. {commkit-3.6.1 → commkit-3.7.0}/tests/analysis/test_allan.py +0 -0
  229. {commkit-3.6.1 → commkit-3.7.0}/tests/analysis/test_drift.py +0 -0
  230. {commkit-3.6.1 → commkit-3.7.0}/tests/analysis/test_fm_noise.py +0 -0
  231. {commkit-3.6.1 → commkit-3.7.0}/tests/analysis/test_interferometry.py +0 -0
  232. {commkit-3.6.1 → commkit-3.7.0}/tests/analysis/test_linewidth.py +0 -0
  233. {commkit-3.6.1 → commkit-3.7.0}/tests/analysis/test_trajectory.py +0 -0
  234. {commkit-3.6.1 → commkit-3.7.0}/tests/coding/test_algebraic.py +0 -0
  235. {commkit-3.6.1 → commkit-3.7.0}/tests/coding/test_coding_base.py +0 -0
  236. {commkit-3.6.1 → commkit-3.7.0}/tests/coding/test_convolutional.py +0 -0
  237. {commkit-3.6.1 → commkit-3.7.0}/tests/coding/test_crc.py +0 -0
  238. {commkit-3.6.1 → commkit-3.7.0}/tests/coding/test_gf2.py +0 -0
  239. {commkit-3.6.1 → commkit-3.7.0}/tests/coding/test_interleaving.py +0 -0
  240. {commkit-3.6.1 → commkit-3.7.0}/tests/coding/test_ldpc.py +0 -0
  241. {commkit-3.6.1 → commkit-3.7.0}/tests/coding/test_ldpc_exit.py +0 -0
  242. {commkit-3.6.1 → commkit-3.7.0}/tests/coding/test_ldpc_kernel.py +0 -0
  243. {commkit-3.6.1 → commkit-3.7.0}/tests/coding/test_ldpc_tables.py +0 -0
  244. {commkit-3.6.1 → commkit-3.7.0}/tests/coding/test_linear_codes.py +0 -0
  245. {commkit-3.6.1 → commkit-3.7.0}/tests/coding/test_performance.py +0 -0
  246. {commkit-3.6.1 → commkit-3.7.0}/tests/coding/test_polar.py +0 -0
  247. {commkit-3.6.1 → commkit-3.7.0}/tests/coding/test_product.py +0 -0
  248. {commkit-3.6.1 → commkit-3.7.0}/tests/coding/test_ratematch.py +0 -0
  249. {commkit-3.6.1 → commkit-3.7.0}/tests/coding/test_turbo.py +0 -0
  250. {commkit-3.6.1 → commkit-3.7.0}/tests/common/__init__.py +0 -0
  251. {commkit-3.6.1 → commkit-3.7.0}/tests/common/coding.py +0 -0
  252. {commkit-3.6.1 → commkit-3.7.0}/tests/common/conversions.py +0 -0
  253. {commkit-3.6.1 → commkit-3.7.0}/tests/common/kernel_utils.py +0 -0
  254. {commkit-3.6.1 → commkit-3.7.0}/tests/common/memory.py +0 -0
  255. {commkit-3.6.1 → commkit-3.7.0}/tests/common/metrics.py +0 -0
  256. {commkit-3.6.1 → commkit-3.7.0}/tests/common/periodic.py +0 -0
  257. {commkit-3.6.1 → commkit-3.7.0}/tests/common/scoring.py +0 -0
  258. {commkit-3.6.1 → commkit-3.7.0}/tests/conftest.py +0 -0
  259. {commkit-3.6.1 → commkit-3.7.0}/tests/core/test_frame.py +0 -0
  260. {commkit-3.6.1 → commkit-3.7.0}/tests/core/test_generation.py +0 -0
  261. {commkit-3.6.1 → commkit-3.7.0}/tests/core/test_preamble.py +0 -0
  262. {commkit-3.6.1 → commkit-3.7.0}/tests/core/test_psqam.py +0 -0
  263. {commkit-3.6.1 → commkit-3.7.0}/tests/core/test_signal.py +0 -0
  264. {commkit-3.6.1 → commkit-3.7.0}/tests/core/test_signal_adapter.py +0 -0
  265. {commkit-3.6.1 → commkit-3.7.0}/tests/core/test_signal_mimo.py +0 -0
  266. {commkit-3.6.1 → commkit-3.7.0}/tests/equalization/test_bps_kernel.py +0 -0
  267. {commkit-3.6.1 → commkit-3.7.0}/tests/equalization/test_cpr.py +0 -0
  268. {commkit-3.6.1 → commkit-3.7.0}/tests/equalization/test_cs_kernel.py +0 -0
  269. {commkit-3.6.1 → commkit-3.7.0}/tests/equalization/test_equalizer_oracles.py +0 -0
  270. {commkit-3.6.1 → commkit-3.7.0}/tests/equalization/test_linear.py +0 -0
  271. {commkit-3.6.1 → commkit-3.7.0}/tests/equalization/test_mimo.py +0 -0
  272. {commkit-3.6.1 → commkit-3.7.0}/tests/equalization/test_polarization.py +0 -0
  273. {commkit-3.6.1 → commkit-3.7.0}/tests/equalization/test_state.py +0 -0
  274. {commkit-3.6.1 → commkit-3.7.0}/tests/equalization/test_winit.py +0 -0
  275. {commkit-3.6.1 → commkit-3.7.0}/tests/impairments/test_fiber_channel_linear.py +0 -0
  276. {commkit-3.6.1 → commkit-3.7.0}/tests/mapping/test_bits.py +0 -0
  277. {commkit-3.6.1 → commkit-3.7.0}/tests/mapping/test_constellation.py +0 -0
  278. {commkit-3.6.1 → commkit-3.7.0}/tests/mapping/test_gray.py +0 -0
  279. {commkit-3.6.1 → commkit-3.7.0}/tests/mapping/test_llr.py +0 -0
  280. {commkit-3.6.1 → commkit-3.7.0}/tests/metrics/test_common.py +0 -0
  281. {commkit-3.6.1 → commkit-3.7.0}/tests/metrics/test_error_rates.py +0 -0
  282. {commkit-3.6.1 → commkit-3.7.0}/tests/metrics/test_information.py +0 -0
  283. {commkit-3.6.1 → commkit-3.7.0}/tests/metrics/test_quality.py +0 -0
  284. {commkit-3.6.1 → commkit-3.7.0}/tests/plotting/test_plot_analysis.py +0 -0
  285. {commkit-3.6.1 → commkit-3.7.0}/tests/plotting/test_plot_coding.py +0 -0
  286. {commkit-3.6.1 → commkit-3.7.0}/tests/plotting/test_plot_constellation.py +0 -0
  287. {commkit-3.6.1 → commkit-3.7.0}/tests/plotting/test_plot_equalizer.py +0 -0
  288. {commkit-3.6.1 → commkit-3.7.0}/tests/plotting/test_plot_eye.py +0 -0
  289. {commkit-3.6.1 → commkit-3.7.0}/tests/plotting/test_plot_filter_response.py +0 -0
  290. {commkit-3.6.1 → commkit-3.7.0}/tests/plotting/test_plot_frequency.py +0 -0
  291. {commkit-3.6.1 → commkit-3.7.0}/tests/plotting/test_plot_recovery.py +0 -0
  292. {commkit-3.6.1 → commkit-3.7.0}/tests/plotting/test_plot_spectral.py +0 -0
  293. {commkit-3.6.1 → commkit-3.7.0}/tests/plotting/test_plot_theme.py +0 -0
  294. {commkit-3.6.1 → commkit-3.7.0}/tests/plotting/test_plot_timing.py +0 -0
  295. {commkit-3.6.1 → commkit-3.7.0}/tests/plotting/test_plot_waveform.py +0 -0
  296. {commkit-3.6.1 → commkit-3.7.0}/tests/plugins/__init__.py +0 -0
  297. {commkit-3.6.1 → commkit-3.7.0}/tests/plugins/precision_trace.py +0 -0
  298. {commkit-3.6.1 → commkit-3.7.0}/tests/recovery/test_bps.py +0 -0
  299. {commkit-3.6.1 → commkit-3.7.0}/tests/recovery/test_corrections.py +0 -0
  300. {commkit-3.6.1 → commkit-3.7.0}/tests/recovery/test_joint.py +0 -0
  301. {commkit-3.6.1 → commkit-3.7.0}/tests/recovery/test_pilots.py +0 -0
  302. {commkit-3.6.1 → commkit-3.7.0}/tests/recovery/test_pll.py +0 -0
  303. {commkit-3.6.1 → commkit-3.7.0}/tests/recovery/test_recovery_oracles.py +0 -0
  304. {commkit-3.6.1 → commkit-3.7.0}/tests/recovery/test_tikhonov.py +0 -0
  305. {commkit-3.6.1 → commkit-3.7.0}/tests/recovery/test_viterbi_viterbi.py +0 -0
  306. {commkit-3.6.1 → commkit-3.7.0}/tests/test_array.py +0 -0
  307. {commkit-3.6.1 → commkit-3.7.0}/tests/test_backend.py +0 -0
  308. {commkit-3.6.1 → commkit-3.7.0}/tests/test_cuda_infra.py +0 -0
  309. {commkit-3.6.1 → commkit-3.7.0}/tests/test_dispersion.py +0 -0
  310. {commkit-3.6.1 → commkit-3.7.0}/tests/test_examples.py +0 -0
  311. {commkit-3.6.1 → commkit-3.7.0}/tests/test_fiber.py +0 -0
  312. {commkit-3.6.1 → commkit-3.7.0}/tests/test_filter_response.py +0 -0
  313. {commkit-3.6.1 → commkit-3.7.0}/tests/test_frequency.py +0 -0
  314. {commkit-3.6.1 → commkit-3.7.0}/tests/test_import_hygiene.py +0 -0
  315. {commkit-3.6.1 → commkit-3.7.0}/tests/test_io.py +0 -0
  316. {commkit-3.6.1 → commkit-3.7.0}/tests/test_logger.py +0 -0
  317. {commkit-3.6.1 → commkit-3.7.0}/tests/test_math.py +0 -0
  318. {commkit-3.6.1 → commkit-3.7.0}/tests/test_pulse_shaping.py +0 -0
  319. {commkit-3.6.1 → commkit-3.7.0}/tests/test_signal_pipeline.py +0 -0
  320. {commkit-3.6.1 → commkit-3.7.0}/tests/test_smoothing.py +0 -0
  321. {commkit-3.6.1 → commkit-3.7.0}/tests/test_spectral.py +0 -0
  322. {commkit-3.6.1 → commkit-3.7.0}/tests/test_working_memory.py +0 -0
@@ -120,13 +120,15 @@ jobs:
120
120
  tests/coding/test_convolutional.py tests/coding/test_turbo.py::TestEncoding
121
121
  tests/coding/test_turbo.py::TestDecoding::test_noiseless
122
122
  tests/coding/test_linear_codes.py::TestMaximumLikelihood
123
+ tests/timing/test_clock_recovery.py::TestEstimate::test_matches_oracle
124
+ tests/timing/test_clock_recovery.py::TestValidation
123
125
  -k "not waterfall and not threshold"
124
126
  --device=cpu --cov=commkit --cov-report=
125
127
 
126
128
  - name: Kernel coverage report
127
129
  run: >-
128
130
  uv run coverage report --show-missing --fail-under=85
129
- --include="commkit/equalization/_numba_loops.py,commkit/equalization/_sequential/*,commkit/frequency.py,commkit/recovery/*,commkit/coding/ldpc.py,commkit/coding/shaping.py,commkit/coding/polar.py,commkit/coding/convolutional.py,commkit/coding/turbo.py"
131
+ --include="commkit/equalization/_numba_loops.py,commkit/equalization/_sequential/*,commkit/frequency.py,commkit/recovery/*,commkit/coding/ldpc.py,commkit/coding/shaping.py,commkit/coding/polar.py,commkit/coding/convolutional.py,commkit/coding/turbo.py,commkit/timing/_numba_loops.py"
130
132
 
131
133
  # GPU legs (--device=gpu / --device=all) require CuPy + CUDA and are run
132
134
  # manually or on a self-hosted runner; they are intentionally not part of
@@ -54,6 +54,8 @@ uv run pytest
54
54
  uv run ruff format .
55
55
  ```
56
56
 
57
+ A commit that changes only text that cannot change behaviour (docstrings, comments, Markdown) runs `ruff check` and `ruff format` alone. The full gate applies again as soon as code, tests or notebook cells change, or an edited docstring holds an example that runs.
58
+
57
59
  **Benchmarks** are explicit-only; `uv run pytest` never collects them. A commit that changes a public signature smoke-runs them once, untimed, so their call sites cannot rot:
58
60
 
59
61
  ```bash
@@ -101,6 +103,7 @@ Dependencies point downward only:
101
103
  - Construction does no hidden work: no mapping, no normalization, no device move, no shape guessing. Factories (`generate`, frames) do that work.
102
104
  - `sig.to("gpu")` and `sig.replace(...)` return new Signals sharing the arrays; `clone()` copies.
103
105
  - **Alignment invariant:** reference symbol *k* corresponds to symbol period *k* of the samples. A function that drops or shifts symbol periods slices the reference to match. Metrics raise on mismatch; they never guess.
106
+ - **Absolute power.** Samples are dimensionless, and a Signal holds no power. Impairments never rescale the signal component: noise and distortion are added in the samples' units. A function whose model depends on absolute power takes `<where>_power_dbm` (`launch_power_dbm`, `received_power_dbm`, `lo_power_dbm`), the mean power its input stands for, summed over channels, and returns samples in the input's units (a square-law detector: the input's power units). A PSD is in the samples' units per Hz, not dBm/Hz.
104
107
 
105
108
  ### Function signatures
106
109
 
@@ -153,7 +156,7 @@ Dependencies point downward only:
153
156
  - `method=` strings name only a numerical flavor of one algorithm (`method="maxlog" | "exact"`); algorithms are objects.
154
157
  - Output scaling has three names: `normalize` (bool) measures the power and rescales to the library's convention; `correct_power` (bool) applies the exact, known gain of a rate change without measuring; `<thing>_normalization` (a string) chooses how coefficients are scaled (`taps_normalization="unit_energy" | "unity_gain"`).
155
158
  - A search window is `search_range`, a `(start, stop)` pair in the axis's unit, or `search_band`, a half-width in Hz around a nominal frequency.
156
- - Values are in SI units without a suffix (`linewidth` in Hz, `drift_rate` in rad/s, `taus` in s). A result holding one quantity in two units suffixes the SI one (`lags` in samples, `lag_s`). A conventional non-SI unit goes into the name: `_db`, `_dbm`, `_deg`, `_nm`, `_km`, `_ps_nm_km`. Normalized quantities say so (`normalized_bandwidth`). No second name for one quantity in another unit (no `center_frequency_ghz`); `center_wavelength_nm` is a different, derived quantity.
159
+ - Values are in SI units without a suffix (`linewidth` in Hz, `drift_rate` in rad/s, `taus` in s). A result holding one quantity in two units suffixes the SI one (`lags` in samples, `lag_s`). A conventional non-SI unit goes into the name: `_db`, `_dbm`, `_deg`, `_nm`, `_km`, `_ps_nm_km`, `_ppm`, `_db_hz`. Normalized quantities say so (`normalized_bandwidth`). No second name for one quantity in another unit (no `center_frequency_ghz`); `center_wavelength_nm` is a different, derived quantity.
157
160
 
158
161
  ### Docstrings
159
162
 
@@ -184,7 +187,7 @@ Docstrings describe the code as it is; a change in behavior updates them in the
184
187
  - **Transforms keep the input's precision** throughout (filters, impairments, the fiber solver and DBP, resampling, delays, carrier-phase correction): complex64 in and out, complex128 in and out.
185
188
  - **Fixed-precision blocks** compute in single precision whatever the input: the equalizers in complex64, `compute_llr` / `Constellation.llr` in float32. Double input emits `commkit.PrecisionWarning` (`_array.warn_if_double`, attributed to the caller's line). The test suite turns it into an error (`pytest_configure` in `tests/conftest.py`; not pyproject's `filterwarnings`, which imports commkit before coverage starts), so a library path feeding double data to these blocks fails CI. Training and reference symbols are not checked.
186
189
  - **Deliberate float64 / complex128**, documented where it happens, only on phases, small arrays or accumulators, never on sample-rate data that is then cast back:
187
- - phases before `xp.unwrap()` (float32 rounding causes spurious quadrant slips), phase trajectories and ramps (wrapped in float64, rotated in the data's precision), frequency grids and estimates;
190
+ - phases before `xp.unwrap()` (float32 rounding causes spurious quadrant slips), phase trajectories and ramps (wrapped in float64, rotated in the data's precision), the strobe positions of clock recovery (symbol rate; the sampling instants built from them are an integer index and a fraction in the data's precision), frequency grids and estimates;
188
191
  - recursion state: PLL, Tikhonov/Kalman block transitions, the IIR filter's sections and state (poles near z = 1);
189
192
  - RLS keeps `P`, the gain `k` and the regressor buffers in complex128 (in single precision `P` loses positive definiteness and the filter diverges);
190
193
  - LMS/CMA/RDE accumulate dot products and gradient updates in complex128 around complex64 inputs and weights; the block equalizers stay complex64, as their tap sums run inside the FFTs;
@@ -202,7 +205,7 @@ Docstrings describe the code as it is; a change in behavior updates them in the
202
205
  | --- | --- | --- |
203
206
  | Vectorized: filters, resampling, spectra, impairments, metrics, frequency-domain equalizers | NumPy/CuPy via `x, xp, sp = dispatch(samples)` | follows the input |
204
207
  | Per-symbol searches over a small table: LLRs (all points, or per axis for separable QAM; max-log without a prior in closed form), hard decisions without a square lattice | CPU: Numba `parallel=True` over blocks of symbols (LLRs), chunked NumPy (decisions). GPU: one thread per symbol (`llr`, `llr_separable`, `llr_separable_maxlog`, `bps_min_d2`'s nearest-point mode); decisions on double input or above 1024 points use the chunked search on both devices; LLRs above 16 bits per symbol raise on the GPU | follows the input |
205
- | Sequential recursions: LMS/RLS/CMA/RDE, PLL, Tikhonov, cycle slips, the Mengali-Morelli bootstrap, per-codeword decoders on trellises and trees (CCDM, polar SC/SCL, Viterbi, BCJR, turbo, recursive systematic encoding) | Numba `@njit(cache=True, fastmath=True, nogil=True)`, independent rows in `prange`. The equalizers' kernel with inline carrier recovery compiles strict (unwrap, running sums) and calls fast-math tap and distance loops as separate functions (an `inline="always"` helper takes its caller's flags) | CPU; GPU input does one round trip |
208
+ | Sequential recursions: LMS/RLS/CMA/RDE, PLL, Gardner clock recovery, Tikhonov, cycle slips, the Mengali-Morelli bootstrap, per-codeword decoders on trellises and trees (CCDM, polar SC/SCL, Viterbi, BCJR, turbo, recursive systematic encoding) | Numba `@njit(cache=True, fastmath=True, nogil=True)`, independent rows in `prange`. The equalizers' kernel with inline carrier recovery compiles strict (unwrap, running sums) and calls fast-math tap and distance loops as separate functions (an `inline="always"` helper takes its caller's flags) | CPU; GPU input does one round trip |
206
209
  | Linear recursions: IIR filtering | CPU: Numba without `fastmath`, so double input matches SciPy bit for bit. GPU: `iir` kernel: each block's state from a zero start in parallel, states carried across blocks in two levels (groups, then across groups), blocks filtered again, in double precision | follows the input |
207
210
  | Binary linear algebra of codes: encoding, syndromes, ML-decoding correlations on the GPU, Chase's test patterns | one float32 GEMM per chunk (`coding._gf2.matmul_mod2`), exact below an inner dimension of 2^24, parity in place (CPU) or by one elementwise kernel writing int8 (GPU); small tables cached per code and device (`backend._device_tables`) | follows the input |
208
211
  | Algebraic decoding (BCH, Reed-Solomon): Berlekamp-Massey, Chien search to the locator's degree, Forney | CPU: Numba `parallel=True` over codewords. GPU: `algebraic_correct`, one thread per codeword in integer table arithmetic, so both agree exactly | follows the input |
@@ -212,7 +215,7 @@ Docstrings describe the code as it is; a change in behavior updates them in the
212
215
  | Elementwise chains between multi-core FFTs (fiber solver's Kerr and PMD stages) | NumPy: a Numba `parallel=True` pass competes with `scipy.fft`'s threads; adopt one only on a paired end-to-end A/B | CPU |
213
216
  | Other hot spots limited by intermediates, launches or round trips | CUDA C++ via CuPy `RawModule` (`commkit/_cuda/src`) | GPU |
214
217
 
215
- **Numba kernels live at module level** in a private module (`equalization/_numba_loops.py`, `coding/_numba_loops.py`, `mapping/_numba_llr.py`) that a getter `_get_numba_<name>()`, defined in the module that calls the kernel, imports on first use, so `import commkit` does not load Numba and `cache=True` works across processes. A kernel defined inside a function that calls another kernel defined there recompiles in every process (Numba's cache key misses the captured dispatcher); a getter may build a closure that captures only constants (`_get_numba_sos`).
218
+ **Numba kernels live at module level** in a private module (`equalization/_numba_loops.py`, `coding/_numba_loops.py`, `mapping/_numba_llr.py`, `timing/_numba_loops.py`) that a getter `_get_numba_<name>()`, defined in the module that calls the kernel, imports on first use, so `import commkit` does not load Numba and `cache=True` works across processes. A kernel defined inside a function that calls another kernel defined there recompiles in every process (Numba's cache key misses the captured dispatcher); a getter may build a closure that captures only constants (`_get_numba_sos`).
216
219
 
217
220
  **Host-sync hygiene:**
218
221
 
@@ -4,6 +4,42 @@ Notable changes to CommKit, newest first. Performance figures are from the refer
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [3.7.0] - 2026-11-10
8
+
9
+ The receiver's analog front end and the detector, at absolute power. The ADC clock, its jitter and quantizer, IQ skew and the photodiode join the impairments, with Gardner clock recovery as the inverse of the clock; coherent and square-law detection turn received and LO powers into the noise they give. Closed-form AWGN error rates and information rates sit next to the measured ones. Blind decision-directed equalizers no longer lock falsely on unipolar PAM and dense QAM, because their input scaling hands the slicer the right gain.
10
+
11
+ ### Changes that may need code updates
12
+
13
+ - The adaptive equalizers (`lms`, `rls`, `cma`, `rde`, `block_lms`, `block_cma`, `block_rde`, `apply_taps(normalize=True)`) divide each input channel by its RMS instead of `rms * sqrt(sps)`. The old scale applied the transmit convention `E|x|^2 = 1/sps` at the slicer and handed it a gain of `1/sqrt(sps)` (0.71 at 2 sps), below the decision margins of unipolar PAM-4 (0.83) and 64-QAM (0.86), where blind decision-directed adaptation settled at a shrunk false solution (BER 0.25 and 0.20). After a matched filter the symbol instants now reach the slicer within `beta/4` of unit power at any timing phase, and blind adaptation reaches the trained BER. Consequences:
14
+ - an explicit `step_size` of the sequential equalizers acts `sps` times stronger (the regressor power is 1): halve it at 2 sps for the former behaviour;
15
+ - the block equalizers' default steps are divided by `sps`, so the defaults behave as before; explicit block steps act `sps` times stronger, and their stability ceilings read `step_size * block_size * sps`;
16
+ - `EqualizerResult.input_norm_factor` and `EqualizerState.input_norm_factor` are the plain per-channel RMS; the docstring gives the exact rebuild of `y_hat` from the raw samples, the taps and this factor.
17
+ - `commkit.timing` is a package (`synchronization`, `clock_recovery`). Import from `commkit.timing` and nothing changes.
18
+ - The example notebooks are renumbered: the new 12 (sampling-clock recovery) and 22 (direct and self-coherent detection) move the former 12-28 to 13-30.
19
+
20
+ ### Added
21
+
22
+ - `timing.estimate_sampling_clock_offset` and `timing.correct_sampling_clock_offset` with `timing.Gardner(normalized_bandwidth=, proportional_gain=, integral_gain=, joint_channels=, method="classical" | "power")`: Gardner clock recovery in one Numba loop. The offset in ppm comes from the slope of the tracked timing trajectory (`SamplingClockOffsetEstimate`); the correction resamples along the strobes, or by a constant factor from an earlier capture's estimate. An open-loop fit of the S-curve sets the gains from the loop bandwidth and starts the loop at the right phase; the power detector keeps its gain at small rolloffs. Measured within 0.06 ppm on 2^17 symbols at 20 dB; after recovery the SNR is the AWGN's plus the jitter's ISI, as predicted.
23
+ - `impairments.apply_sampling_clock_offset(offset_ppm=)`: an ADC clock off its nominal rate, by band-limited resampling (48-tap windowed sinc, error below -119 dB up to 0.4 cycles per sample).
24
+ - `impairments.apply_sampling_jitter(jitter=)` and `impairments.apply_quantization(num_bits=, full_scale=, enob=)`: sampling at jittered instants (SNR `-20 log10(2 pi f sigma)` of a tone) and a mid-rise quantizer with clipping and the converter's lumped noise.
25
+ - `apply_iq_imbalance(skew=)`: a time skew of Q against I, the frequency-dependent part of IQ imbalance (image `tan^2(pi f tau)`), which the memoryless corrections cannot remove.
26
+ - `metrics.awgn_ser`, `awgn_ber`, `awgn_mi`, `awgn_gmi(constellation, *, esn0_db)`: closed-form performance over AWGN in the conventions of the measured metrics. Error rates are exact for one-dimensional and separable constellations (PAM bipolar and unipolar, square QAM including shaped, BPSK, QPSK) and union bounds otherwise; MI and GMI by Gauss-Hermite quadrature, converged to 3e-7 bit.
27
+ - `impairments.apply_coherent_detection(received_power_dbm=, lo_power_dbm=, responsivity=, thermal_noise_density=, cmrr_db=, lo_rin_db_hz=, balanced=)`: the shot and thermal noise of a phase-diversity receiver from physical parameters (shot-noise limit `Es/N0 = eta n_p`), the common-mode leak of the signal's own power and of the LO's RIN through a finite CMRR, and single-ended detection.
28
+ - `impairments.apply_photodetection(received_power_dbm=, ...)`: the square law in the input's power units with signal-dependent shot noise, thermal noise, dark current, saturation and `sum_channels`; real output, and a Signal becomes digital baseband. `impairments.apply_intensity_noise(rin_db_hz=)`: laser RIN as a real amplitude factor.
29
+ - A WARNING when the power of an equalizer's decided symbols differs from the decision constellation's by more than 1.5 dB: a false lock, or a constellation that does not match the signal.
30
+ - Notebooks: 12 sampling-clock recovery, 22 direct and self-coherent detection; notebook 10 covers IQ skew and the ADC, notebook 06 uses the closed forms.
31
+
32
+ ### Fixed
33
+
34
+ - `upsample`, `decimate` and `resample` promoted float32 to float64 (complex64 to complex128) on the GPU, because `cupyx` designs its polyphase filter in float64; the GPU now applies the CPU's filter in the data's precision.
35
+ - Equalizer docstrings: `weights_history` row `n` of the sequential equalizers holds the taps after the update at symbol `n`; an explicit `constellation` applies to the call only and the returned Signal keeps the input's description; real input is equalized as complex64. `Constellation.pam` states the unipolar energy penalty `2(2M - 1)/(M + 1)` (3.01 dB for OOK, 4.47 dB for PAM-4).
36
+
37
+ ### Development
38
+
39
+ - `AGENTS.md`: the absolute-power rule (samples are dimensionless, a Signal holds no power, physical functions take `<where>_power_dbm` and return samples in the input's units), the `_ppm` and `_db_hz` suffixes, text-only commits skip mypy and pytest.
40
+ - Tests: a GN-model oracle for the split-step solver (first-order NLI with the coherent span factor, within 0.5 dB and the `P^3` law), the scaling chain of every equalizer rebuilt from raw samples, an IM-DD receiver chain with blind LMS on unipolar PAM-4, a plain-Python oracle for the Gardner loop.
41
+ - `benchmarks/bench_gardner.py` (no baseline recorded yet).
42
+
7
43
  ## [3.6.1] - 2026-11-05
8
44
 
9
45
  ### Added
@@ -467,7 +503,8 @@ See the [GitHub release](https://github.com/lokgar/commkit/releases/tag/v1.1.0)
467
503
 
468
504
  First release. See the [GitHub release](https://github.com/lokgar/commkit/releases/tag/v1.0.0).
469
505
 
470
- [Unreleased]: https://github.com/lokgar/commkit/compare/v3.6.1...HEAD
506
+ [Unreleased]: https://github.com/lokgar/commkit/compare/v3.7.0...HEAD
507
+ [3.7.0]: https://github.com/lokgar/commkit/compare/v3.6.1...v3.7.0
471
508
  [3.6.1]: https://github.com/lokgar/commkit/compare/v3.6.0...v3.6.1
472
509
  [3.6.0]: https://github.com/lokgar/commkit/compare/v3.5.0...v3.6.0
473
510
  [3.5.0]: https://github.com/lokgar/commkit/compare/v3.4.0...v3.5.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: commkit
3
- Version: 3.6.1
3
+ Version: 3.7.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
@@ -146,7 +146,7 @@ CommKit never picks a device for you: data stays where you put it, and each func
146
146
  | [`commkit.multirate`](https://github.com/lokgar/commkit/blob/main/commkit/multirate.py) | `resample` (fractional), `decimate`, `upsample`, `decimate_to_symbol_rate`. |
147
147
  | [`commkit.spectral`](https://github.com/lokgar/commkit/blob/main/commkit/spectral.py) | Welch PSD, spectrograms, frequency shifting, pilot tones. |
148
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`. |
149
- | [`commkit.timing`](https://github.com/lokgar/commkit/blob/main/commkit/timing.py) | Barker and Zadoff-Chu sequences, `estimate_timing` / `correct_timing`, fractional delay estimation and correction. |
149
+ | [`commkit.timing`](https://github.com/lokgar/commkit/tree/main/commkit/timing) | Barker and Zadoff-Chu sequences, `estimate_timing` / `correct_timing`, fractional delay estimation and correction. |
150
150
  | [`commkit.frequency`](https://github.com/lokgar/commkit/blob/main/commkit/frequency.py) | `estimate_frequency_offset` / `correct_frequency_offset` with `MthPower`, `MengaliMorelli`, `PilotSlope` and `SpectralPeak`; static and blockwise. |
151
151
  | [`commkit.recovery`](https://github.com/lokgar/commkit/tree/main/commkit/recovery) | `estimate_carrier_phase` / `correct_carrier_phase` with `ViterbiViterbi`, `BPS`, `PLL`, `Tikhonov`, `ConstantPhase`, `PilotInterpolation`, `PilotTone(s)`; cycle-slip correction; `resolve_phase_ambiguity` and `resolve_channel_permutation`. |
152
152
  | [`commkit.equalization`](https://github.com/lokgar/commkit/tree/main/commkit/equalization) | Sequential `lms`, `rls`, `cma`, `rde` (Numba) and frequency-domain `block_lms`, `block_cma`, `block_rde` (CuPy, CUDA graphs), butterfly MIMO, inline carrier recovery (`cpr=PLL()` / `BPS()`), continuation with `state=`, `zf_equalizer`; pilot-aided CMA and RDE from a frame's pilots (`EqualizerPilots`); polarization demultiplexing on pilot tones (`estimate_polarization_tones` / `correct_polarization`). |
@@ -110,7 +110,7 @@ CommKit never picks a device for you: data stays where you put it, and each func
110
110
  | [`commkit.multirate`](https://github.com/lokgar/commkit/blob/main/commkit/multirate.py) | `resample` (fractional), `decimate`, `upsample`, `decimate_to_symbol_rate`. |
111
111
  | [`commkit.spectral`](https://github.com/lokgar/commkit/blob/main/commkit/spectral.py) | Welch PSD, spectrograms, frequency shifting, pilot tones. |
112
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`. |
113
- | [`commkit.timing`](https://github.com/lokgar/commkit/blob/main/commkit/timing.py) | Barker and Zadoff-Chu sequences, `estimate_timing` / `correct_timing`, fractional delay estimation and correction. |
113
+ | [`commkit.timing`](https://github.com/lokgar/commkit/tree/main/commkit/timing) | Barker and Zadoff-Chu sequences, `estimate_timing` / `correct_timing`, fractional delay estimation and correction. |
114
114
  | [`commkit.frequency`](https://github.com/lokgar/commkit/blob/main/commkit/frequency.py) | `estimate_frequency_offset` / `correct_frequency_offset` with `MthPower`, `MengaliMorelli`, `PilotSlope` and `SpectralPeak`; static and blockwise. |
115
115
  | [`commkit.recovery`](https://github.com/lokgar/commkit/tree/main/commkit/recovery) | `estimate_carrier_phase` / `correct_carrier_phase` with `ViterbiViterbi`, `BPS`, `PLL`, `Tikhonov`, `ConstantPhase`, `PilotInterpolation`, `PilotTone(s)`; cycle-slip correction; `resolve_phase_ambiguity` and `resolve_channel_permutation`. |
116
116
  | [`commkit.equalization`](https://github.com/lokgar/commkit/tree/main/commkit/equalization) | Sequential `lms`, `rls`, `cma`, `rde` (Numba) and frequency-domain `block_lms`, `block_cma`, `block_rde` (CuPy, CUDA graphs), butterfly MIMO, inline carrier recovery (`cpr=PLL()` / `BPS()`), continuation with `state=`, `zf_equalizer`; pilot-aided CMA and RDE from a frame's pilots (`EqualizerPilots`); polarization demultiplexing on pilot tones (`estimate_polarization_tones` / `correct_polarization`). |
@@ -30,16 +30,16 @@ from commkit.mapping import Constellation
30
30
  ROUNDS = dict(rounds=3, warmup_rounds=1, iterations=1)
31
31
  N_SYM = 100_000
32
32
 
33
- # Converging step sizes per (equalizer, block_size). RDE on 16-QAM from a
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.
33
+ # Converging step sizes per (equalizer, block_size), on input normalized to
34
+ # unit mean power at 2 sps. RDE on 16-QAM from a cold start (no CMA
35
+ # pre-convergence) converges slowly at 2048: at 2e-4 it is error-free from
36
+ # about symbol 70 000, so its SER is scored after that (3.5e-4 never
37
+ # settled, still 2-5 % SER at the end); its cost does not depend on the data.
38
38
  STEP_SIZE = {
39
- ("cma", 256): 2e-4,
40
- ("cma", 2048): 3e-4,
41
- ("rde", 256): 3e-3,
42
- ("rde", 2048): 4e-4,
39
+ ("cma", 256): 1e-4,
40
+ ("cma", 2048): 1.5e-4,
41
+ ("rde", 256): 1.5e-3,
42
+ ("rde", 2048): 2e-4,
43
43
  }
44
44
  SKIP = {("rde", 2048): 70_000}
45
45
  MAX_SER = {"cma": 1e-2, "rde": 1e-2}
@@ -54,9 +54,10 @@ CPR_CONFIGS = [
54
54
  ]
55
55
 
56
56
 
57
- # Converging step sizes for the 30-degree mixing of the workload: the block
58
- # update's stability ceiling falls with block_size.
59
- STEP_SIZE = {256: 3e-3, 2048: 5e-4}
57
+ # Converging step sizes for the 30-degree mixing of the workload, on input
58
+ # normalized to unit mean power at 2 sps: the block update's stability
59
+ # ceiling falls with block_size.
60
+ STEP_SIZE = {256: 1.5e-3, 2048: 2.5e-4}
60
61
 
61
62
 
62
63
  def _bench_block_lms(benchmark, xp, sync, cpr_kwargs, block_size, n_train=None):
@@ -0,0 +1,36 @@
1
+ """Benchmarks: Gardner clock recovery (CPU Numba loop; GPU input makes one
2
+ round trip and resamples on the device)."""
3
+
4
+ from types import SimpleNamespace
5
+
6
+ import pytest
7
+ from benchutils import assert_converged
8
+ from workloads import clock_offset_workload
9
+
10
+ from commkit.mapping import Constellation
11
+ from commkit.multirate import decimate_to_symbol_rate
12
+ from commkit.timing import Gardner, correct_sampling_clock_offset
13
+
14
+ ROUNDS = dict(rounds=3, warmup_rounds=1, iterations=1)
15
+ N_SYM = 2**18
16
+
17
+
18
+ @pytest.mark.parametrize("method", ["classical", "power"])
19
+ def bench_correct_sampling_clock_offset(benchmark, backend_device, xp, sync, method):
20
+ samples, symbols = clock_offset_workload(N_SYM)
21
+ x = xp.asarray(samples)
22
+
23
+ def run():
24
+ out = correct_sampling_clock_offset(x, Gardner(method=method), sps=2)
25
+ sync()
26
+ return out
27
+
28
+ y = benchmark.pedantic(run, **ROUNDS)
29
+ sym = decimate_to_symbol_rate(y, sps=2)
30
+ assert_converged(
31
+ SimpleNamespace(y_hat=sym),
32
+ symbols,
33
+ Constellation.qam(16),
34
+ skip=1000,
35
+ max_ser=1e-3,
36
+ )
@@ -168,3 +168,32 @@ def coded_workload(code, num_codewords: int, esn0_db: float, seed: int = 0):
168
168
  rx = apply_awgn(sig, esn0_db=esn0_db, rng=seed + 1)
169
169
  llrs = compute_llr(rx, noise_var=10 ** (-esn0_db / 10))
170
170
  return np.asarray(llrs, dtype=np.float32), sig.reference.info_bits
171
+
172
+
173
+ def clock_offset_workload(
174
+ n_sym: int, num_ch: int = 2, ppm: float = 20.0, seed: int = 0
175
+ ):
176
+ """Matched-filtered dual-pol 16-QAM at 2 SPS (RRC 0.1, 20 dB Es/N0, a
177
+ 0.37-sample delay) sampled by a clock ``ppm`` fast, for the clock-recovery
178
+ benches. Returns ``(samples (num_ch, ~2 n_sym) complex64, symbols)``."""
179
+ from commkit import RRC
180
+ from commkit.core import generate
181
+ from commkit.filtering import matched_filter
182
+ from commkit.impairments import apply_sampling_clock_offset
183
+ from commkit.timing import fft_fractional_delay
184
+
185
+ tx = generate(
186
+ Constellation.qam(16),
187
+ num_symbols=n_sym,
188
+ symbol_rate=FS,
189
+ sps=2,
190
+ pulse=RRC(rolloff=0.1),
191
+ num_channels=num_ch,
192
+ rng=seed,
193
+ )
194
+ rx = apply_awgn(tx, esn0_db=20.0, rng=seed + 1)
195
+ rx = apply_sampling_clock_offset(
196
+ fft_fractional_delay(rx, delay=0.37), offset_ppm=ppm
197
+ )
198
+ mf = matched_filter(rx)
199
+ return np.asarray(mf.samples, dtype=np.complex64), np.asarray(tx.reference.symbols)
@@ -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.6.1"
34
+ __version__ = "3.7.0"
35
35
 
36
36
  from .backend import PrecisionWarning
37
37
  from .core import (
@@ -1,3 +1,4 @@
1
1
  """Physical constants shared across the package."""
2
2
 
3
3
  SPEED_OF_LIGHT = 299_792_458.0 # m/s, exact (SI definition)
4
+ ELEMENTARY_CHARGE = 1.602_176_634e-19 # C, exact (SI definition)
@@ -110,7 +110,7 @@ def spread_samples(
110
110
 
111
111
  ``τ(ω) = β₂ L ω`` over ``|f| <= bandwidth / 2`` spans
112
112
  ``2π |β₂ L| · bandwidth`` seconds; over the whole sampled band
113
- (``bandwidth = fs``) this is Savory's FIR length (Opt. Express 16, 2008).
113
+ (``bandwidth = fs``) this is the FIR length that covers the whole spread.
114
114
  """
115
115
  return 2.0 * np.pi * abs(beta2_length) * bandwidth * sampling_rate
116
116
 
@@ -143,8 +143,7 @@ def compensation_taps(
143
143
 
144
144
  Minimizes ``∫_{|θ|<W} |Σ h[n] e^{-jθn} - exp(j a θ²)|² dθ`` with
145
145
  ``θ = ω / fs``, ``W = π · bandwidth / fs`` and ``a = β₂ L fs² / 2``, for
146
- ``n`` centred on tap ``(num_taps - 1) // 2`` (Eghbali et al., J.
147
- Lightw. Technol. 32, 2014): ``Q h = p`` with the Toeplitz Gram matrix
146
+ ``n`` centred on tap ``(num_taps - 1) // 2``: ``Q h = p`` with the Toeplitz Gram matrix
148
147
  ``Q[m, k] = (W/π) sinc(W (m-k) / π)`` and ``p[m]`` the band integral of
149
148
  the response, exact through Fresnel integrals. Over the whole band
150
149
  (``W = π``) ``Q`` is the identity and ``h = p``, the truncated
@@ -15,7 +15,7 @@ fast birefringence of the fiber into ``8/9 · γ · (|x|² + |y|²)``. In
15
15
  CommKit's sign convention (``_dispersion``: propagation multiplies a spectrum
16
16
  by ``exp(-j β₂ ω² L / 2)``) the field obeys
17
17
 
18
- dA/dz = -α/2 A - j β₂/2 ∂²A/∂t² + β₃/6 ∂³A/∂t³ - j γ |A|² A,
18
+ dA/dz = -α/2 A + j β₂/2 ∂²A/∂t² + β₃/6 ∂³A/∂t³ - j γ |A|² A,
19
19
 
20
20
  the complex conjugate of the textbook (Agrawal) form, so the Kerr phase is
21
21
  ``exp(-j γ |A|² L)``. Matching the dispersion sign is what keeps solitons
@@ -21,7 +21,7 @@ __all__ = [
21
21
  "_whittle_fit",
22
22
  ]
23
23
 
24
- # β-separation-line slope (Di Domenico 2010): S_f(f) = (8 ln2 / π²) · f
24
+ # β-separation-line slope: S_f(f) = (8 ln2 / π²) · f
25
25
  _BETA_SLOPE = 8.0 * np.log(2.0) / (np.pi**2)
26
26
  # FWHM linewidth from the integrated FM-noise area above the β-line:
27
27
  # Δν = sqrt(8 ln2 · A)
@@ -655,12 +655,6 @@ def ldpc_exit_curves(
655
655
  code run so close together that the closed-form fits of ``J``
656
656
  (within 1e-3) move its threshold by 0.02 bit.
657
657
 
658
- References
659
- ----------
660
- S. ten Brink, G. Kramer, A. Ashikhmin, "Design of low-density
661
- parity-check codes for modulation and detection," IEEE Trans. Commun.
662
- 52(4), 2004.
663
-
664
658
  Examples
665
659
  --------
666
660
  >>> code = LDPCCode.dvbs2("1/2", "short")
@@ -236,8 +236,7 @@ class Chase:
236
236
  ``sum_i (1 - 2 c_i) LLR_i`` is the decision. The soft output of bit
237
237
  ``i`` is half the metric difference between the best candidates with
238
238
  ``c_i = 0`` and ``c_i = 1`` (max-log), or, when no candidate disagrees
239
- with the decision there, ``beta`` with the decision's sign (Pyndiah,
240
- 1998). For codes with a hard decoder: linear block codes and BCH codes.
239
+ with the decision there, ``beta`` with the decision's sign. For codes with a hard decoder: linear block codes and BCH codes.
241
240
 
242
241
  Parameters
243
242
  ----------
@@ -10,8 +10,8 @@ list decoding (CA-SCL). Encoding is ``n`` butterfly passes on the data's
10
10
  device.
11
11
 
12
12
  Decoding is successive cancellation (:class:`SC`) or its list version
13
- (:class:`SCL`), with min-sum check-node updates and the path metric of
14
- Balatsoukas-Stimming et al. (2015): sequential per codeword, so Numba over
13
+ (:class:`SCL`), with min-sum check-node updates and an LLR-based path
14
+ metric: sequential per codeword, so Numba over
15
15
  codewords on the CPU, and GPU input makes one round trip. LLRs and partial
16
16
  sums are kept level by level in buffers of ``2N`` (level ``s`` at offsets
17
17
  ``2^s ... 2^(s+1) - 1``), so a list path is copied in ``O(N)``.
@@ -7,10 +7,8 @@ target distribution, invertibly. Both classes are code objects, so
7
7
  (Maxwell-Boltzmann priors, ``Constellation.shaped``) is in
8
8
  :mod:`commkit.mapping.shaping`.
9
9
 
10
- :class:`CCDM` (constant-composition distribution matching, Schulte and
11
- Böcherer, 2016) maps ``k`` bits to ``num_symbols`` points whose counts are
12
- fixed: the composition that best approximates the constellation's pmf
13
- (Böcherer and Geiger, 2016). Each block of bits selects a point of
10
+ :class:`CCDM` (constant-composition distribution matching) maps ``k`` bits to ``num_symbols`` points whose counts are
11
+ fixed: the composition that best approximates the constellation's pmf. Each block of bits selects a point of
14
12
  ``[0, 1)``; the sequences of that composition partition ``[0, 1)`` by
15
13
  arithmetic coding over the remaining counts, and the matcher outputs the
16
14
  sequence whose interval holds the point. The coder is a 64-bit range coder:
@@ -24,8 +22,7 @@ sequential recursion per block: Numba over blocks on the CPU; GPU input to
24
22
  :func:`~commkit.coding.decode` makes one round trip, and ``generate`` runs
25
23
  it on the host before moving the payload.
26
24
 
27
- :class:`PAS` (probabilistic amplitude shaping, Böcherer, Steiner and
28
- Schulte, 2015) combines a CCDM over the amplitudes of one real dimension
25
+ :class:`PAS` (probabilistic amplitude shaping) combines a CCDM over the amplitudes of one real dimension
29
26
  with a systematic FEC code, whose parity bits supply the signs. It needs
30
27
  labels with a sign bit per real dimension and the same amplitude label for
31
28
  ``±a``: Gray QAM and bipolar PAM as the factories build them.
@@ -202,7 +199,7 @@ class CCDM(_Code):
202
199
 
203
200
 
204
201
  def _composition(pmf: np.ndarray, n: int) -> np.ndarray:
205
- """KL-optimal counts ``c`` (sum ``n``) for ``pmf`` (Böcherer and Geiger)."""
202
+ """KL-optimal counts ``c`` (sum ``n``) for ``pmf``."""
206
203
  p = np.asarray(pmf, dtype=np.float64)
207
204
  c = np.floor(n * p).astype(np.int64)
208
205
  for _ in range(n - int(c.sum())):
@@ -86,7 +86,7 @@ def _block_fdaf_blind(
86
86
  pilot positions overridden by the LMS residual. ``r2`` is the Godard radius
87
87
  (CMA) and ``radii_np`` the unique ring radii (RDE), decided by their
88
88
  squared midpoints (``_rde_rings``). ``step_size=None``
89
- is the default of both, ``min(1e-3, 0.5 / block_size)``.
89
+ is the default of both, ``min(1e-3, 0.5 / block_size) / sps``.
90
90
  """
91
91
  use_pilots = pilot_ref is not None and pilot_mask is not None
92
92
  equalizer = f"block_{kind}"
@@ -105,7 +105,7 @@ def _block_fdaf_blind(
105
105
  pilot_gain_db=pilot_gain_db,
106
106
  )
107
107
  xp, C, n_sym, sps = run.xp, run.C, run.n_sym, run.sps
108
- mu = min(1e-3, 0.5 / run.block_size) if step_size is None else step_size
108
+ mu = min(1e-3, 0.5 / run.block_size) / sps if step_size is None else step_size
109
109
  logger.info(
110
110
  "%s: C=%s, num_taps=%s, sps=%s, block_size=%s, fftsize=%s, mu=%s, "
111
111
  "n_sym=%s, pilot_aided=%s",
@@ -312,7 +312,7 @@ def block_cma(
312
312
  Step size on the scale of :func:`cma`. The weights are frozen
313
313
  across each block, so the stability ceiling is about ``block_size``
314
314
  times lower than per symbol; reduce it if the run raises divergence.
315
- Defaults to ``min(1e-3, 0.5 / block_size)``, as for
315
+ Defaults to ``min(1e-3, 0.5 / block_size) / sps``, as for
316
316
  :func:`block_rde`.
317
317
  block_size : int, default 256
318
318
  Output symbols per weight update.
@@ -396,9 +396,9 @@ def block_rde(
396
396
  step_size : float, optional
397
397
  Step size on the scale of :func:`rde`, with the about ``block_size``
398
398
  times lower stability ceiling of frozen-block adaptation. Defaults
399
- to ``min(1e-3, 0.5 / block_size)``: the per-symbol step, capped at
400
- half the ceiling (``step_size * block_size`` below about 1) for large
401
- blocks.
399
+ to ``min(1e-3, 0.5 / block_size) / sps``: the per-symbol step, capped
400
+ at half the ceiling (``step_size * block_size * sps`` below about 1,
401
+ on input normalized to unit mean power) for large blocks.
402
402
  block_size : int, default 256
403
403
  Output symbols per weight update.
404
404
  constellation : Constellation, optional
@@ -23,6 +23,7 @@ from ..result import (
23
23
  EqualizerResult,
24
24
  EqualizerState,
25
25
  _attach_equalized_signal,
26
+ _check_decision_power,
26
27
  _log_equalizer_exit,
27
28
  )
28
29
  from ._engine import (
@@ -386,12 +387,13 @@ def block_lms(
386
387
  ``block_size``.
387
388
 
388
389
  Because the weights are frozen across the block, the stability
389
- ceiling falls as ``1/block_size``: ``μ·block_size`` must stay below
390
- about 1.5 (whatever ``num_taps`` and the channel count). The default
391
- ``min(0.5 / block_size, 8e-3)`` sits at a quarter of that ceiling,
392
- where convergence takes a few blocks and the steady-state MSE is
393
- within a few tenths of a dB of the best step; for ``block_size``
394
- below 64 the cap ``8e-3`` limits the per-symbol misadjustment.
390
+ ceiling falls as ``1/block_size``: on input normalized to unit mean
391
+ power, ``μ·block_size·sps`` must stay below about 1.5 (whatever
392
+ ``num_taps`` and the channel count). The default
393
+ ``min(0.5 / block_size, 8e-3) / sps`` sits at a quarter of that
394
+ ceiling, where convergence takes a few blocks and the steady-state
395
+ MSE is within a few tenths of a dB of the best step; for
396
+ ``block_size`` below 64 the cap limits the per-symbol misadjustment.
395
397
  block_size : int, default 256
396
398
  Output symbols per weight update. Larger blocks use the GPU better
397
399
  but adapt more slowly. Independent of the BPS window
@@ -399,7 +401,14 @@ def block_lms(
399
401
  constellation : Constellation, optional
400
402
  Decision constellation, at unit power (a shaped constellation brings
401
403
  its pmf). Defaults to the Signal's ``constellation``; required for
402
- array input.
404
+ array input. A WARNING is logged when the decided symbols of the
405
+ last half of the decision-directed output lack its power by more
406
+ than 1.5 dB: a lock at a wrong gain, or a constellation that does
407
+ not match the signal. An explicit choice applies to this call
408
+ only: ``signal`` of the result keeps the input Signal's
409
+ ``constellation``, which describes its reference symbols, so pass
410
+ the same choice to the metrics, or set it on the Signal with
411
+ ``replace`` (with matching reference symbols) before equalizing.
403
412
  store_weights : bool, default False
404
413
  Record the weights at the start of every block in
405
414
  ``weights_history``.
@@ -465,7 +474,7 @@ def block_lms(
465
474
  **Stability.** ``step_size`` multiplies the gradient summed (not
466
475
  averaged) over the block. That keeps ``μ`` on the scale of :func:`lms`
467
476
  (the same ``μ`` converges at the same speed per symbol), but it makes the
468
- stability ceiling, ``μ·block_size`` below about 1.5, about
477
+ stability ceiling, ``μ·block_size·sps`` below about 1.5, about
469
478
  ``block_size`` times lower than for per-symbol LMS, because the weights
470
479
  are frozen across the block. Above the ceiling the run diverges (NaN
471
480
  weights, detected at the end, raise) or stays bounded without
@@ -553,7 +562,7 @@ def block_lms(
553
562
  xp, C, n_sym, sps, block_size = run.xp, run.C, run.n_sym, run.sps, run.block_size
554
563
  num_taps = run.num_taps
555
564
  training = run.training
556
- mu = min(0.5 / block_size, 8e-3) if step_size is None else step_size
565
+ mu = min(0.5 / block_size, 8e-3) / sps if step_size is None else step_size
557
566
 
558
567
  if constellation is None:
559
568
  raise ValueError(
@@ -757,18 +766,19 @@ def block_lms(
757
766
  raise RuntimeError(
758
767
  f"block_lms diverged (step_size={mu}, block_size={block_size}). "
759
768
  f"Because the weights are frozen across the block, step_size * "
760
- f"block_size must stay below about 1.5; the default "
761
- f"min(0.5 / block_size, 8e-3) = {min(0.5 / block_size, 8e-3):.2e} "
762
- f"sits at a quarter of that ceiling."
769
+ f"block_size * sps must stay below about 1.5; the default "
770
+ f"min(0.5 / block_size, 8e-3) / sps = "
771
+ f"{min(0.5 / block_size, 8e-3) / sps:.2e} sits at a quarter of that "
772
+ f"ceiling."
763
773
  )
764
774
  weights, carrier = snap[0]
765
775
  state_out = _block_state(
766
776
  run, equalizer="block_lms", cpr=cpr, weights=weights, carrier=carrier
767
777
  )
768
778
  result = _assemble_block(run, y_all, e_all, w_hist, phi_all, n_train, state_out)
769
- return _attach_equalized_signal(
770
- _log_equalizer_exit(result, name="Block-LMS"), sig, state
771
- )
779
+ result = _log_equalizer_exit(result, name="Block-LMS")
780
+ result = _check_decision_power(result, constellation, "block_lms()")
781
+ return _attach_equalized_signal(result, sig, state)
772
782
 
773
783
 
774
784
  def _block_bps(
@@ -248,7 +248,6 @@ def _prepare_block(
248
248
  samples, training_symbols, eq_norm = _normalize_inputs(
249
249
  samples,
250
250
  training_symbols,
251
- sps,
252
251
  input_norm_factor=None if state is None else state.input_norm_factor,
253
252
  )
254
253
 
@@ -22,15 +22,21 @@ from .result import EqualizerResult, EqualizerState
22
22
 
23
23
 
24
24
  def _normalize_inputs(
25
- samples: Any, training_symbols: Any, sps: int, input_norm_factor: Any = None
25
+ samples: Any, training_symbols: Any, input_norm_factor: Any = None
26
26
  ) -> tuple[Any, Any, Any]:
27
- """Scale samples to unit symbol power; training symbols pass through.
28
-
29
- At more than one sample per symbol the timing phase is unknown, and the
30
- strided power ``samples[..., ::sps]`` can land on zero crossings of the
31
- Nyquist pulse, underestimating the power and destabilizing the
32
- adaptation. The power of all samples over the whole record is used
33
- instead; it does not depend on whether training symbols are given.
27
+ """Scale samples to unit mean power per channel; training symbols pass.
28
+
29
+ The automatic gain control in front of the equalizer: each channel is
30
+ divided by its RMS over the whole record, so the regressor has unit
31
+ power per tap and a centre tap of 1 hands the slicer the samples at
32
+ about the constellation's unit power. After a matched filter the
33
+ samples at the symbol instants have the symbols' power and those
34
+ between them ``1 - β/2`` of it for a raised-cosine response of roll-off
35
+ ``β``, so unit mean power puts the symbol instants within ``β/4`` of
36
+ unit power, at any timing phase. (The transmit convention ``E|x|² =
37
+ 1/sps`` describes energy per symbol spread over ``sps`` samples; applied
38
+ here it would hand the slicer a gain of ``1/sqrt(sps)``, below the
39
+ decision margins of 64-QAM and unipolar PAM-4.)
34
40
 
35
41
  Parameters
36
42
  ----------
@@ -38,8 +44,6 @@ def _normalize_inputs(
38
44
  Complex samples, ``(N,)`` or ``(C, N)``, NumPy or CuPy.
39
45
  training_symbols : array_like or None
40
46
  Training symbols at one sample per symbol, ``(K,)`` or ``(C, K)``.
41
- sps : int
42
- Samples per symbol.
43
47
  input_norm_factor : float or numpy.ndarray, optional
44
48
  Factor to divide by instead of measuring the RMS: the
45
49
  ``EqualizerResult.input_norm_factor`` of an earlier call, to keep
@@ -49,15 +53,13 @@ def _normalize_inputs(
49
53
  Returns
50
54
  -------
51
55
  samples : ndarray
52
- The samples at unit symbol power, same shape and device.
56
+ The samples at unit mean power, same shape and device.
53
57
  training_symbols : ndarray or None
54
58
  Unchanged: known symbols are already on the constellation's scale.
55
59
  input_norm_factor : float or numpy.ndarray
56
- Per-channel factor ``rms(channel)·sqrt(sps)`` the samples were divided
57
- by: a ``float`` for SISO, ``(C,)`` for MIMO. Stored in
58
- ``EqualizerResult.input_norm_factor``, so that another capture (vacuum
59
- noise) passed through the same frozen taps can be put on the same
60
- absolute power scale.
60
+ Per-channel RMS ``sqrt(mean(|x_c|²))`` the samples were divided by:
61
+ a ``float`` for SISO, ``(C,)`` for MIMO. Stored in
62
+ ``EqualizerResult.input_norm_factor``.
61
63
  """
62
64
  if input_norm_factor is not None:
63
65
  # Caller supplies scale - skip RMS and just apply it.
@@ -82,7 +84,7 @@ def _normalize_inputs(
82
84
 
83
85
  ref_samples = samples
84
86
 
85
- norm_vec = _rms(ref_samples, axis=-1) * (sps**0.5)
87
+ norm_vec = _rms(ref_samples, axis=-1)
86
88
  if samples.ndim == 1:
87
89
  input_norm_factor = float(norm_vec)
88
90
  samples = samples / float(norm_vec)