commkit 3.2.0__tar.gz → 3.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (294) hide show
  1. {commkit-3.2.0 → commkit-3.2.1}/AGENTS.md +18 -2
  2. {commkit-3.2.0 → commkit-3.2.1}/CHANGELOG.md +26 -2
  3. {commkit-3.2.0 → commkit-3.2.1}/PKG-INFO +1 -1
  4. {commkit-3.2.0 → commkit-3.2.1}/benchmarks/bench_block_blind.py +13 -6
  5. {commkit-3.2.0 → commkit-3.2.1}/benchmarks/bench_fiber.py +6 -4
  6. {commkit-3.2.0 → commkit-3.2.1}/benchmarks/benchutils.py +11 -3
  7. {commkit-3.2.0 → commkit-3.2.1}/benchmarks/conftest.py +14 -0
  8. {commkit-3.2.0 → commkit-3.2.1}/commkit/__init__.py +1 -1
  9. {commkit-3.2.0 → commkit-3.2.1}/commkit/coding/linear.py +3 -1
  10. {commkit-3.2.0 → commkit-3.2.1}/commkit/equalization/_block/_blind.py +1 -1
  11. {commkit-3.2.0 → commkit-3.2.1}/commkit/equalization/_block/_engine.py +1 -1
  12. {commkit-3.2.0 → commkit-3.2.1}/commkit/equalization/_common.py +2 -2
  13. {commkit-3.2.0 → commkit-3.2.1}/commkit/equalization/_numba_loops.py +4 -0
  14. {commkit-3.2.0 → commkit-3.2.1}/commkit/equalization/blind.py +4 -2
  15. {commkit-3.2.0 → commkit-3.2.1}/commkit/equalization/result.py +3 -1
  16. {commkit-3.2.0 → commkit-3.2.1}/commkit/equalization/sequential/_dd.py +20 -10
  17. {commkit-3.2.0 → commkit-3.2.1}/commkit/equalization/sequential/_setup.py +3 -1
  18. {commkit-3.2.0 → commkit-3.2.1}/commkit/filtering/linear.py +7 -5
  19. {commkit-3.2.0 → commkit-3.2.1}/commkit/frequency.py +0 -4
  20. {commkit-3.2.0 → commkit-3.2.1}/commkit/plotting/analysis.py +8 -11
  21. {commkit-3.2.0 → commkit-3.2.1}/commkit/plotting/coding.py +3 -4
  22. {commkit-3.2.0 → commkit-3.2.1}/commkit/plotting/equalizer.py +2 -4
  23. {commkit-3.2.0 → commkit-3.2.1}/commkit/plotting/eye.py +1 -1
  24. {commkit-3.2.0 → commkit-3.2.1}/commkit/plotting/filter_response.py +4 -3
  25. {commkit-3.2.0 → commkit-3.2.1}/commkit/plotting/spectral.py +3 -3
  26. {commkit-3.2.0 → commkit-3.2.1}/commkit/plotting/sync.py +2 -2
  27. {commkit-3.2.0 → commkit-3.2.1}/commkit/plotting/theme.py +12 -10
  28. {commkit-3.2.0 → commkit-3.2.1}/commkit/plotting/waveform.py +1 -1
  29. {commkit-3.2.0 → commkit-3.2.1}/commkit/recovery/corrections.py +24 -6
  30. {commkit-3.2.0 → commkit-3.2.1}/commkit/recovery/pll.py +28 -3
  31. {commkit-3.2.0 → commkit-3.2.1}/commkit/recovery/tikhonov.py +38 -4
  32. {commkit-3.2.0 → commkit-3.2.1}/commkit/timing.py +3 -3
  33. {commkit-3.2.0 → commkit-3.2.1}/examples/17-blind-equalization.ipynb +3 -3
  34. {commkit-3.2.0 → commkit-3.2.1}/pyproject.toml +4 -2
  35. commkit-3.2.1/tests/analysis/test_allan.py +75 -0
  36. {commkit-3.2.0 → commkit-3.2.1}/tests/analysis/test_linewidth.py +3 -0
  37. {commkit-3.2.0 → commkit-3.2.1}/tests/coding/test_ldpc.py +24 -6
  38. {commkit-3.2.0 → commkit-3.2.1}/tests/coding/test_product.py +87 -20
  39. {commkit-3.2.0 → commkit-3.2.1}/tests/coding/test_ratematch.py +30 -9
  40. {commkit-3.2.0 → commkit-3.2.1}/tests/coding/test_shaping.py +12 -5
  41. {commkit-3.2.0 → commkit-3.2.1}/tests/coding/test_turbo.py +41 -17
  42. {commkit-3.2.0 → commkit-3.2.1}/tests/common/__init__.py +2 -2
  43. {commkit-3.2.0 → commkit-3.2.1}/tests/common/metrics.py +0 -27
  44. commkit-3.2.1/tests/common/scoring.py +129 -0
  45. {commkit-3.2.0 → commkit-3.2.1}/tests/common/signals.py +91 -13
  46. commkit-3.2.1/tests/common/theory.py +84 -0
  47. {commkit-3.2.0 → commkit-3.2.1}/tests/conftest.py +64 -7
  48. {commkit-3.2.0 → commkit-3.2.1}/tests/core/test_frame.py +11 -43
  49. {commkit-3.2.0 → commkit-3.2.1}/tests/core/test_signal.py +13 -34
  50. {commkit-3.2.0 → commkit-3.2.1}/tests/equalization/test_blind.py +99 -66
  51. {commkit-3.2.0 → commkit-3.2.1}/tests/equalization/test_block.py +177 -333
  52. {commkit-3.2.0 → commkit-3.2.1}/tests/equalization/test_cpr.py +241 -291
  53. {commkit-3.2.0 → commkit-3.2.1}/tests/equalization/test_equalizer_oracles.py +14 -11
  54. {commkit-3.2.0 → commkit-3.2.1}/tests/equalization/test_linear.py +65 -89
  55. commkit-3.2.1/tests/equalization/test_mimo.py +164 -0
  56. {commkit-3.2.0 → commkit-3.2.1}/tests/equalization/test_polarization.py +13 -0
  57. commkit-3.2.1/tests/equalization/test_sequential.py +662 -0
  58. {commkit-3.2.0 → commkit-3.2.1}/tests/equalization/test_state.py +2 -0
  59. {commkit-3.2.0 → commkit-3.2.1}/tests/equalization/test_winit.py +36 -151
  60. {commkit-3.2.0 → commkit-3.2.1}/tests/impairments/channel/test_channel_linear.py +68 -69
  61. {commkit-3.2.0 → commkit-3.2.1}/tests/impairments/channel/test_channel_nonlinear.py +2 -0
  62. {commkit-3.2.0 → commkit-3.2.1}/tests/impairments/test_frontend.py +0 -42
  63. {commkit-3.2.0 → commkit-3.2.1}/tests/impairments/test_noise.py +0 -7
  64. {commkit-3.2.0 → commkit-3.2.1}/tests/impairments/test_source.py +0 -12
  65. {commkit-3.2.0 → commkit-3.2.1}/tests/mapping/test_bits.py +0 -16
  66. {commkit-3.2.0 → commkit-3.2.1}/tests/plotting/test_plot_analysis.py +1 -0
  67. {commkit-3.2.0 → commkit-3.2.1}/tests/plotting/test_plot_filter_response.py +2 -0
  68. {commkit-3.2.0 → commkit-3.2.1}/tests/plotting/test_plot_spectral.py +4 -1
  69. {commkit-3.2.0 → commkit-3.2.1}/tests/plotting/test_plot_sync.py +2 -0
  70. {commkit-3.2.0 → commkit-3.2.1}/tests/plotting/test_plot_waveform.py +2 -1
  71. {commkit-3.2.0 → commkit-3.2.1}/tests/recovery/test_bps.py +35 -64
  72. {commkit-3.2.0 → commkit-3.2.1}/tests/recovery/test_corrections.py +34 -38
  73. {commkit-3.2.0 → commkit-3.2.1}/tests/recovery/test_pilots.py +14 -57
  74. {commkit-3.2.0 → commkit-3.2.1}/tests/recovery/test_pll.py +24 -47
  75. {commkit-3.2.0 → commkit-3.2.1}/tests/recovery/test_tikhonov.py +129 -106
  76. {commkit-3.2.0 → commkit-3.2.1}/tests/recovery/test_viterbi_viterbi.py +38 -85
  77. {commkit-3.2.0 → commkit-3.2.1}/tests/test_api_contracts.py +7 -0
  78. {commkit-3.2.0 → commkit-3.2.1}/tests/test_filtering.py +75 -37
  79. {commkit-3.2.0 → commkit-3.2.1}/tests/test_frequency.py +72 -68
  80. {commkit-3.2.0 → commkit-3.2.1}/tests/test_io.py +16 -4
  81. {commkit-3.2.0 → commkit-3.2.1}/tests/test_metrics.py +51 -34
  82. {commkit-3.2.0 → commkit-3.2.1}/tests/test_multirate.py +96 -36
  83. {commkit-3.2.0 → commkit-3.2.1}/tests/test_pulse_shaping.py +23 -32
  84. commkit-3.2.1/tests/test_receiver_chain.py +244 -0
  85. {commkit-3.2.0 → commkit-3.2.1}/tests/test_spectral.py +83 -0
  86. {commkit-3.2.0 → commkit-3.2.1}/tests/test_timing.py +54 -39
  87. {commkit-3.2.0 → commkit-3.2.1}/uv.lock +1 -1
  88. commkit-3.2.0/tests/analysis/test_allan.py +0 -34
  89. commkit-3.2.0/tests/equalization/test_mimo.py +0 -218
  90. commkit-3.2.0/tests/equalization/test_sequential.py +0 -1170
  91. {commkit-3.2.0 → commkit-3.2.1}/.gitattributes +0 -0
  92. {commkit-3.2.0 → commkit-3.2.1}/.github/workflows/ci.yml +0 -0
  93. {commkit-3.2.0 → commkit-3.2.1}/.github/workflows/publish.yml +0 -0
  94. {commkit-3.2.0 → commkit-3.2.1}/.gitignore +0 -0
  95. {commkit-3.2.0 → commkit-3.2.1}/.python-version +0 -0
  96. {commkit-3.2.0 → commkit-3.2.1}/LICENSE +0 -0
  97. {commkit-3.2.0 → commkit-3.2.1}/README.md +0 -0
  98. {commkit-3.2.0 → commkit-3.2.1}/benchmarks/baselines/Linux-CPython-3.14-64bit/0002_pre_v2.json +0 -0
  99. {commkit-3.2.0 → commkit-3.2.1}/benchmarks/baselines/Linux-CPython-3.14-64bit/0003_v2_0.json +0 -0
  100. {commkit-3.2.0 → commkit-3.2.1}/benchmarks/baselines/Linux-CPython-3.14-64bit/0004_fiber_dbp.json +0 -0
  101. {commkit-3.2.0 → commkit-3.2.1}/benchmarks/baselines/Linux-CPython-3.14-64bit/0005_gpu_decoder_path.json +0 -0
  102. {commkit-3.2.0 → commkit-3.2.1}/benchmarks/baselines/Linux-CPython-3.14-64bit/0006_perf_cleanup.json +0 -0
  103. {commkit-3.2.0 → commkit-3.2.1}/benchmarks/baselines/Linux-CPython-3.14-64bit/0007_v3_0.json +0 -0
  104. {commkit-3.2.0 → commkit-3.2.1}/benchmarks/baselines/Linux-CPython-3.14-64bit/0008_v3_1.json +0 -0
  105. {commkit-3.2.0 → commkit-3.2.1}/benchmarks/bench_block_lms.py +0 -0
  106. {commkit-3.2.0 → commkit-3.2.1}/benchmarks/bench_bps.py +0 -0
  107. {commkit-3.2.0 → commkit-3.2.1}/benchmarks/bench_coding.py +0 -0
  108. {commkit-3.2.0 → commkit-3.2.1}/benchmarks/bench_equalizers.py +0 -0
  109. {commkit-3.2.0 → commkit-3.2.1}/benchmarks/bench_filters.py +0 -0
  110. {commkit-3.2.0 → commkit-3.2.1}/benchmarks/bench_import.py +0 -0
  111. {commkit-3.2.0 → commkit-3.2.1}/benchmarks/bench_llr.py +0 -0
  112. {commkit-3.2.0 → commkit-3.2.1}/benchmarks/bench_mapping.py +0 -0
  113. {commkit-3.2.0 → commkit-3.2.1}/benchmarks/bench_signal_pipeline.py +0 -0
  114. {commkit-3.2.0 → commkit-3.2.1}/benchmarks/bench_sync_misc.py +0 -0
  115. {commkit-3.2.0 → commkit-3.2.1}/benchmarks/record_baseline.py +0 -0
  116. {commkit-3.2.0 → commkit-3.2.1}/benchmarks/workloads.py +0 -0
  117. {commkit-3.2.0 → commkit-3.2.1}/commkit/_array.py +0 -0
  118. {commkit-3.2.0 → commkit-3.2.1}/commkit/_circular.py +0 -0
  119. {commkit-3.2.0 → commkit-3.2.1}/commkit/_constants.py +0 -0
  120. {commkit-3.2.0 → commkit-3.2.1}/commkit/_cuda/__init__.py +0 -0
  121. {commkit-3.2.0 → commkit-3.2.1}/commkit/_cuda/compiler.py +0 -0
  122. {commkit-3.2.0 → commkit-3.2.1}/commkit/_cuda/graphs.py +0 -0
  123. {commkit-3.2.0 → commkit-3.2.1}/commkit/_cuda/src/algebraic.cu +0 -0
  124. {commkit-3.2.0 → commkit-3.2.1}/commkit/_cuda/src/bps_anchor.cu +0 -0
  125. {commkit-3.2.0 → commkit-3.2.1}/commkit/_cuda/src/bps_min_d2.cu +0 -0
  126. {commkit-3.2.0 → commkit-3.2.1}/commkit/_cuda/src/cs_block.cu +0 -0
  127. {commkit-3.2.0 → commkit-3.2.1}/commkit/_cuda/src/iir.cu +0 -0
  128. {commkit-3.2.0 → commkit-3.2.1}/commkit/_cuda/src/ldpc.cu +0 -0
  129. {commkit-3.2.0 → commkit-3.2.1}/commkit/_cuda/src/llr.cu +0 -0
  130. {commkit-3.2.0 → commkit-3.2.1}/commkit/_cuda/src/selftest.cu +0 -0
  131. {commkit-3.2.0 → commkit-3.2.1}/commkit/_dispersion.py +0 -0
  132. {commkit-3.2.0 → commkit-3.2.1}/commkit/_fft.py +0 -0
  133. {commkit-3.2.0 → commkit-3.2.1}/commkit/_fiber.py +0 -0
  134. {commkit-3.2.0 → commkit-3.2.1}/commkit/_overlap_save.py +0 -0
  135. {commkit-3.2.0 → commkit-3.2.1}/commkit/_random.py +0 -0
  136. {commkit-3.2.0 → commkit-3.2.1}/commkit/_sequences.py +0 -0
  137. {commkit-3.2.0 → commkit-3.2.1}/commkit/analysis/__init__.py +0 -0
  138. {commkit-3.2.0 → commkit-3.2.1}/commkit/analysis/_common.py +0 -0
  139. {commkit-3.2.0 → commkit-3.2.1}/commkit/analysis/allan.py +0 -0
  140. {commkit-3.2.0 → commkit-3.2.1}/commkit/analysis/drift.py +0 -0
  141. {commkit-3.2.0 → commkit-3.2.1}/commkit/analysis/fm_noise.py +0 -0
  142. {commkit-3.2.0 → commkit-3.2.1}/commkit/analysis/interferometry.py +0 -0
  143. {commkit-3.2.0 → commkit-3.2.1}/commkit/analysis/linewidth/__init__.py +0 -0
  144. {commkit-3.2.0 → commkit-3.2.1}/commkit/analysis/linewidth/dsh.py +0 -0
  145. {commkit-3.2.0 → commkit-3.2.1}/commkit/analysis/linewidth/phase.py +0 -0
  146. {commkit-3.2.0 → commkit-3.2.1}/commkit/analysis/linewidth/result.py +0 -0
  147. {commkit-3.2.0 → commkit-3.2.1}/commkit/analysis/trajectory.py +0 -0
  148. {commkit-3.2.0 → commkit-3.2.1}/commkit/backend.py +0 -0
  149. {commkit-3.2.0 → commkit-3.2.1}/commkit/coding/__init__.py +0 -0
  150. {commkit-3.2.0 → commkit-3.2.1}/commkit/coding/_galois.py +0 -0
  151. {commkit-3.2.0 → commkit-3.2.1}/commkit/coding/_gf2.py +0 -0
  152. {commkit-3.2.0 → commkit-3.2.1}/commkit/coding/_ldpc_tables.py +0 -0
  153. {commkit-3.2.0 → commkit-3.2.1}/commkit/coding/_numba_loops.py +0 -0
  154. {commkit-3.2.0 → commkit-3.2.1}/commkit/coding/algebraic.py +0 -0
  155. {commkit-3.2.0 → commkit-3.2.1}/commkit/coding/base.py +0 -0
  156. {commkit-3.2.0 → commkit-3.2.1}/commkit/coding/convolutional.py +0 -0
  157. {commkit-3.2.0 → commkit-3.2.1}/commkit/coding/crc.py +0 -0
  158. {commkit-3.2.0 → commkit-3.2.1}/commkit/coding/exit.py +0 -0
  159. {commkit-3.2.0 → commkit-3.2.1}/commkit/coding/interleaving.py +0 -0
  160. {commkit-3.2.0 → commkit-3.2.1}/commkit/coding/ldpc.py +0 -0
  161. {commkit-3.2.0 → commkit-3.2.1}/commkit/coding/polar.py +0 -0
  162. {commkit-3.2.0 → commkit-3.2.1}/commkit/coding/product.py +0 -0
  163. {commkit-3.2.0 → commkit-3.2.1}/commkit/coding/ratematch.py +0 -0
  164. {commkit-3.2.0 → commkit-3.2.1}/commkit/coding/shaping.py +0 -0
  165. {commkit-3.2.0 → commkit-3.2.1}/commkit/coding/turbo.py +0 -0
  166. {commkit-3.2.0 → commkit-3.2.1}/commkit/core/__init__.py +0 -0
  167. {commkit-3.2.0 → commkit-3.2.1}/commkit/core/_signal_adapter.py +0 -0
  168. {commkit-3.2.0 → commkit-3.2.1}/commkit/core/frame.py +0 -0
  169. {commkit-3.2.0 → commkit-3.2.1}/commkit/core/generation.py +0 -0
  170. {commkit-3.2.0 → commkit-3.2.1}/commkit/core/signal.py +0 -0
  171. {commkit-3.2.0 → commkit-3.2.1}/commkit/equalization/__init__.py +0 -0
  172. {commkit-3.2.0 → commkit-3.2.1}/commkit/equalization/_block/__init__.py +0 -0
  173. {commkit-3.2.0 → commkit-3.2.1}/commkit/equalization/_block/_dd.py +0 -0
  174. {commkit-3.2.0 → commkit-3.2.1}/commkit/equalization/_kernels_numba.py +0 -0
  175. {commkit-3.2.0 → commkit-3.2.1}/commkit/equalization/linear.py +0 -0
  176. {commkit-3.2.0 → commkit-3.2.1}/commkit/equalization/pilots.py +0 -0
  177. {commkit-3.2.0 → commkit-3.2.1}/commkit/equalization/polarization.py +0 -0
  178. {commkit-3.2.0 → commkit-3.2.1}/commkit/equalization/sequential/__init__.py +0 -0
  179. {commkit-3.2.0 → commkit-3.2.1}/commkit/equalization/sequential/_blind.py +0 -0
  180. {commkit-3.2.0 → commkit-3.2.1}/commkit/filtering/__init__.py +0 -0
  181. {commkit-3.2.0 → commkit-3.2.1}/commkit/filtering/fiber.py +0 -0
  182. {commkit-3.2.0 → commkit-3.2.1}/commkit/filtering/pulses.py +0 -0
  183. {commkit-3.2.0 → commkit-3.2.1}/commkit/impairments/__init__.py +0 -0
  184. {commkit-3.2.0 → commkit-3.2.1}/commkit/impairments/channel/__init__.py +0 -0
  185. {commkit-3.2.0 → commkit-3.2.1}/commkit/impairments/channel/linear.py +0 -0
  186. {commkit-3.2.0 → commkit-3.2.1}/commkit/impairments/channel/nonlinear.py +0 -0
  187. {commkit-3.2.0 → commkit-3.2.1}/commkit/impairments/frontend.py +0 -0
  188. {commkit-3.2.0 → commkit-3.2.1}/commkit/impairments/noise.py +0 -0
  189. {commkit-3.2.0 → commkit-3.2.1}/commkit/impairments/source.py +0 -0
  190. {commkit-3.2.0 → commkit-3.2.1}/commkit/io.py +0 -0
  191. {commkit-3.2.0 → commkit-3.2.1}/commkit/logger.py +0 -0
  192. {commkit-3.2.0 → commkit-3.2.1}/commkit/mapping/__init__.py +0 -0
  193. {commkit-3.2.0 → commkit-3.2.1}/commkit/mapping/_numba_llr.py +0 -0
  194. {commkit-3.2.0 → commkit-3.2.1}/commkit/mapping/bits.py +0 -0
  195. {commkit-3.2.0 → commkit-3.2.1}/commkit/mapping/constellation.py +0 -0
  196. {commkit-3.2.0 → commkit-3.2.1}/commkit/mapping/gray.py +0 -0
  197. {commkit-3.2.0 → commkit-3.2.1}/commkit/mapping/llr.py +0 -0
  198. {commkit-3.2.0 → commkit-3.2.1}/commkit/mapping/shaping.py +0 -0
  199. {commkit-3.2.0 → commkit-3.2.1}/commkit/math.py +0 -0
  200. {commkit-3.2.0 → commkit-3.2.1}/commkit/metrics.py +0 -0
  201. {commkit-3.2.0 → commkit-3.2.1}/commkit/multirate.py +0 -0
  202. {commkit-3.2.0 → commkit-3.2.1}/commkit/plotting/__init__.py +0 -0
  203. {commkit-3.2.0 → commkit-3.2.1}/commkit/plotting/constellation.py +0 -0
  204. {commkit-3.2.0 → commkit-3.2.1}/commkit/py.typed +0 -0
  205. {commkit-3.2.0 → commkit-3.2.1}/commkit/recovery/__init__.py +0 -0
  206. {commkit-3.2.0 → commkit-3.2.1}/commkit/recovery/_common.py +0 -0
  207. {commkit-3.2.0 → commkit-3.2.1}/commkit/recovery/bps.py +0 -0
  208. {commkit-3.2.0 → commkit-3.2.1}/commkit/recovery/carrier_phase.py +0 -0
  209. {commkit-3.2.0 → commkit-3.2.1}/commkit/recovery/pilots.py +0 -0
  210. {commkit-3.2.0 → commkit-3.2.1}/commkit/recovery/viterbi_viterbi.py +0 -0
  211. {commkit-3.2.0 → commkit-3.2.1}/commkit/smoothing.py +0 -0
  212. {commkit-3.2.0 → commkit-3.2.1}/commkit/spectral.py +0 -0
  213. {commkit-3.2.0 → commkit-3.2.1}/examples/00-getting-started.ipynb +0 -0
  214. {commkit-3.2.0 → commkit-3.2.1}/examples/01-constellations-and-bit-mapping.ipynb +0 -0
  215. {commkit-3.2.0 → commkit-3.2.1}/examples/02-spectral-analysis.ipynb +0 -0
  216. {commkit-3.2.0 → commkit-3.2.1}/examples/03-digital-filters.ipynb +0 -0
  217. {commkit-3.2.0 → commkit-3.2.1}/examples/04-pulse-shaping-and-matched-filtering.ipynb +0 -0
  218. {commkit-3.2.0 → commkit-3.2.1}/examples/05-multirate-and-resampling.ipynb +0 -0
  219. {commkit-3.2.0 → commkit-3.2.1}/examples/06-awgn-and-performance-metrics.ipynb +0 -0
  220. {commkit-3.2.0 → commkit-3.2.1}/examples/07-channel-coding.ipynb +0 -0
  221. {commkit-3.2.0 → commkit-3.2.1}/examples/08-probabilistic-shaping-and-coded-modulation.ipynb +0 -0
  222. {commkit-3.2.0 → commkit-3.2.1}/examples/09-laser-phase-noise.ipynb +0 -0
  223. {commkit-3.2.0 → commkit-3.2.1}/examples/10-front-end-impairments.ipynb +0 -0
  224. {commkit-3.2.0 → commkit-3.2.1}/examples/11-frame-synchronization-and-timing.ipynb +0 -0
  225. {commkit-3.2.0 → commkit-3.2.1}/examples/12-frequency-offset-estimation.ipynb +0 -0
  226. {commkit-3.2.0 → commkit-3.2.1}/examples/13-blind-carrier-phase-recovery.ipynb +0 -0
  227. {commkit-3.2.0 → commkit-3.2.1}/examples/14-pilot-aided-carrier-recovery.ipynb +0 -0
  228. {commkit-3.2.0 → commkit-3.2.1}/examples/15-linear-channel-equalization.ipynb +0 -0
  229. {commkit-3.2.0 → commkit-3.2.1}/examples/16-adaptive-equalization.ipynb +0 -0
  230. {commkit-3.2.0 → commkit-3.2.1}/examples/18-polarization-multiplexing.ipynb +0 -0
  231. {commkit-3.2.0 → commkit-3.2.1}/examples/19-chromatic-dispersion.ipynb +0 -0
  232. {commkit-3.2.0 → commkit-3.2.1}/examples/20-nonlinear-fiber-and-backpropagation.ipynb +0 -0
  233. {commkit-3.2.0 → commkit-3.2.1}/examples/21-fec-in-optical-standards.ipynb +0 -0
  234. {commkit-3.2.0 → commkit-3.2.1}/examples/22-coherent-transceiver-end-to-end.ipynb +0 -0
  235. {commkit-3.2.0 → commkit-3.2.1}/examples/23-carrier-phase-characterization.ipynb +0 -0
  236. {commkit-3.2.0 → commkit-3.2.1}/examples/24-laser-linewidth-self-heterodyne.ipynb +0 -0
  237. {commkit-3.2.0 → commkit-3.2.1}/examples/25-laser-linewidth-short-delay-and-flicker.ipynb +0 -0
  238. {commkit-3.2.0 → commkit-3.2.1}/examples/26-laser-linewidth-self-homodyne-iq.ipynb +0 -0
  239. {commkit-3.2.0 → commkit-3.2.1}/examples/27-measurement-template-self-heterodyne.ipynb +0 -0
  240. {commkit-3.2.0 → commkit-3.2.1}/examples/28-measurement-template-self-homodyne-iq.ipynb +0 -0
  241. {commkit-3.2.0 → commkit-3.2.1}/examples/README.md +0 -0
  242. {commkit-3.2.0 → commkit-3.2.1}/tests/analysis/test_drift.py +0 -0
  243. {commkit-3.2.0 → commkit-3.2.1}/tests/analysis/test_fm_noise.py +0 -0
  244. {commkit-3.2.0 → commkit-3.2.1}/tests/analysis/test_interferometry.py +0 -0
  245. {commkit-3.2.0 → commkit-3.2.1}/tests/analysis/test_trajectory.py +0 -0
  246. {commkit-3.2.0 → commkit-3.2.1}/tests/coding/test_algebraic.py +0 -0
  247. {commkit-3.2.0 → commkit-3.2.1}/tests/coding/test_coding_base.py +0 -0
  248. {commkit-3.2.0 → commkit-3.2.1}/tests/coding/test_convolutional.py +0 -0
  249. {commkit-3.2.0 → commkit-3.2.1}/tests/coding/test_crc.py +0 -0
  250. {commkit-3.2.0 → commkit-3.2.1}/tests/coding/test_exit.py +0 -0
  251. {commkit-3.2.0 → commkit-3.2.1}/tests/coding/test_gf2.py +0 -0
  252. {commkit-3.2.0 → commkit-3.2.1}/tests/coding/test_interleaving.py +0 -0
  253. {commkit-3.2.0 → commkit-3.2.1}/tests/coding/test_ldpc_exit.py +0 -0
  254. {commkit-3.2.0 → commkit-3.2.1}/tests/coding/test_ldpc_kernel.py +0 -0
  255. {commkit-3.2.0 → commkit-3.2.1}/tests/coding/test_ldpc_tables.py +0 -0
  256. {commkit-3.2.0 → commkit-3.2.1}/tests/coding/test_linear_codes.py +0 -0
  257. {commkit-3.2.0 → commkit-3.2.1}/tests/coding/test_polar.py +0 -0
  258. {commkit-3.2.0 → commkit-3.2.1}/tests/common/coding.py +0 -0
  259. {commkit-3.2.0 → commkit-3.2.1}/tests/common/conversions.py +0 -0
  260. {commkit-3.2.0 → commkit-3.2.1}/tests/common/kernel_utils.py +0 -0
  261. {commkit-3.2.0 → commkit-3.2.1}/tests/common/memory.py +0 -0
  262. {commkit-3.2.0 → commkit-3.2.1}/tests/common/periodic.py +0 -0
  263. {commkit-3.2.0 → commkit-3.2.1}/tests/common/reference_impl.py +0 -0
  264. {commkit-3.2.0 → commkit-3.2.1}/tests/core/test_generation.py +0 -0
  265. {commkit-3.2.0 → commkit-3.2.1}/tests/core/test_psqam.py +0 -0
  266. {commkit-3.2.0 → commkit-3.2.1}/tests/core/test_signal_adapter.py +0 -0
  267. {commkit-3.2.0 → commkit-3.2.1}/tests/core/test_signal_mimo.py +0 -0
  268. {commkit-3.2.0 → commkit-3.2.1}/tests/equalization/test_bps_kernel.py +0 -0
  269. {commkit-3.2.0 → commkit-3.2.1}/tests/equalization/test_cs_kernel.py +0 -0
  270. {commkit-3.2.0 → commkit-3.2.1}/tests/mapping/test_constellation.py +0 -0
  271. {commkit-3.2.0 → commkit-3.2.1}/tests/mapping/test_gray.py +0 -0
  272. {commkit-3.2.0 → commkit-3.2.1}/tests/mapping/test_llr.py +0 -0
  273. {commkit-3.2.0 → commkit-3.2.1}/tests/plotting/test_plot_coding.py +0 -0
  274. {commkit-3.2.0 → commkit-3.2.1}/tests/plotting/test_plot_constellation.py +0 -0
  275. {commkit-3.2.0 → commkit-3.2.1}/tests/plotting/test_plot_equalizer.py +0 -0
  276. {commkit-3.2.0 → commkit-3.2.1}/tests/plotting/test_plot_eye.py +0 -0
  277. {commkit-3.2.0 → commkit-3.2.1}/tests/plotting/test_plot_theme.py +0 -0
  278. {commkit-3.2.0 → commkit-3.2.1}/tests/plugins/__init__.py +0 -0
  279. {commkit-3.2.0 → commkit-3.2.1}/tests/plugins/precision_trace.py +0 -0
  280. {commkit-3.2.0 → commkit-3.2.1}/tests/plugins/transfer_trace.py +0 -0
  281. {commkit-3.2.0 → commkit-3.2.1}/tests/recovery/test_joint.py +0 -0
  282. {commkit-3.2.0 → commkit-3.2.1}/tests/recovery/test_recovery_oracles.py +0 -0
  283. {commkit-3.2.0 → commkit-3.2.1}/tests/test_array.py +0 -0
  284. {commkit-3.2.0 → commkit-3.2.1}/tests/test_backend.py +0 -0
  285. {commkit-3.2.0 → commkit-3.2.1}/tests/test_cuda_infra.py +0 -0
  286. {commkit-3.2.0 → commkit-3.2.1}/tests/test_dispersion.py +0 -0
  287. {commkit-3.2.0 → commkit-3.2.1}/tests/test_examples.py +0 -0
  288. {commkit-3.2.0 → commkit-3.2.1}/tests/test_fiber.py +0 -0
  289. {commkit-3.2.0 → commkit-3.2.1}/tests/test_import_hygiene.py +0 -0
  290. {commkit-3.2.0 → commkit-3.2.1}/tests/test_logger.py +0 -0
  291. {commkit-3.2.0 → commkit-3.2.1}/tests/test_math.py +0 -0
  292. {commkit-3.2.0 → commkit-3.2.1}/tests/test_signal_pipeline.py +0 -0
  293. {commkit-3.2.0 → commkit-3.2.1}/tests/test_smoothing.py +0 -0
  294. {commkit-3.2.0 → commkit-3.2.1}/tests/test_working_memory.py +0 -0
@@ -33,6 +33,7 @@ uv sync --all-extras # environment incl. GPU packages
33
33
  uv run nbstripout --install # once per clone: strip notebook outputs on commit
34
34
  uv run pytest # CPU + GPU tests (default --device=all)
35
35
  uv run pytest --device=cpu # what CI runs
36
+ uv run pytest --run-slow # also the long statistical validation (marker slow)
36
37
  uv run pytest tests/equalization/ -k lms # subset
37
38
  uv run pytest --cov=commkit # coverage
38
39
  NUMBA_DISABLE_JIT=1 uv run pytest tests/equalization tests/recovery \
@@ -111,7 +112,7 @@ uv run python benchmarks/record_baseline.py NAME # record a new baseline
111
112
 
112
113
  **Release:**
113
114
 
114
- 1. On `main`: the CPU and GPU suites pass with no failures or skips (`uv run pytest -rs`), `uv build` succeeds and `uv run --with twine twine check dist/*` passes.
115
+ 1. On `main`: the CPU and GPU suites pass with no failures or skips, slow validation included (`uv run pytest -rs --run-slow`; strict xfails, if any, are listed in §9), `uv build` succeeds and `uv run --with twine twine check dist/*` passes.
115
116
  2. `CHANGELOG.md`: a `## [X.Y.Z] - YYYY-MM-DD` section for the release, written for users. Changes that may need code updates come first, with how to migrate, then Added, Performance (measured, as a ratio of new to old time), Fixed and Development. Add its compare link at the bottom, and commit it.
116
117
  3. `uv run bump-my-version bump {patch|minor|major}`. This commits the new version and tags it `vX.Y.Z`.
117
118
  4. `git push origin main --tags`. The ruleset on `main` requires pull requests; repository admins bypass it for these two commits.
@@ -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.
@@ -2,7 +2,30 @@
2
2
 
3
3
  Notable changes to CommKit, newest first. Performance figures are from the reference machine (RTX 4070 Ti, Ryzen 7 7800X3D, WSL2) unless stated otherwise, as a ratio of new to old time (below 1 is faster).
4
4
 
5
- ## [Unreleased]
5
+ ## [3.2.1] - 2026-11-02
6
+
7
+ Fixes found by a rework of the test suite, which now checks what each algorithm achieves against the transmitted truth and theory. Five numerical defaults or models were wrong and change results (PLL cycle-slip repair, Tikhonov smoothing, `block_cma`'s step, `ols_fir_filter`'s alignment of even taps, `MthPower` near 0 Hz); no signature changes. Plots take their sizes from the theme.
8
+
9
+ ### Changes that alter results
10
+
11
+ - **`Tikhonov` uses the actual variance of a Viterbi-Viterbi block phase** as its observation variance, computed from the constellation and `snr_db`: the noise through the M-th power plus the data self-noise of QAM. It used `1/(M²·SNR·N)`, too small by `M²/2` for PSK and by three to four orders of magnitude for QAM, so the smoother followed the raw block phases: on 16-QAM it now lowers the Viterbi-Viterbi error (static phase, 30 dB, 16-symbol blocks: 0.101 rad before and after smoothing, now 0.048 rad).
12
+ - **`PLL(cycle_slip=CycleSlip())` defaults to a history of `8/μ` symbols** (twice the loop memory, between 10 and 640), standalone and inline in `lms` / `rls`; it used 640 symbols, as the block methods do. A PLL trajectory is smoothed by the loop, and the long linear fit lagged the phase noise enough to declare false slips: 16-QAM, `2π·Δν·T = 2π·1e-4`, 16 dB, `B·T = 2e-2`, four records of 2·10⁴ symbols: 10 rotation changes against the transmitted symbols with the repair, 3 without; now 3 with it. The repair does not undo the loop's own slips, which drift over about `1/(B·T)` symbols; the `CycleSlip` docstring says so. An explicit `history` is unchanged.
13
+ - **`block_cma` defaults to `step_size=min(1e-3, 0.5 / block_size)`**, as `block_rde` does; it used 2e-4. On QPSK, where CMA and RDE are the same algorithm, the old default left the worst channel at 2.3 times the AWGN symbol error rate over the second half of 8·10⁴ symbols (dual polarization through rotation and DGD, Es/N0 10 dB; now 1.3 times) and at 16 times on 2·10⁴ symbols (now 2 times); on 16-QAM at 18 dB the SER of 2·10⁴-symbol records falls from 0.13 to 0.008. Above 2500-symbol blocks the new default is the smaller step, inside the stability ceiling that 2e-4 crossed (16-QAM, 4096-symbol blocks: SER 0.02 before, 0.004 now). Pass `step_size=2e-4` for the previous behavior.
14
+ - **`ols_fir_filter` centres even-length taps on tap `(L - 1) // 2`**, as `fir_filter` (SciPy's `mode="same"`) does and as its docstring promised; it used `L // 2`, one sample later. Odd-length taps and `causal=True` are unchanged. The docstrings of both now state the centre tap; they said `L // 2`.
15
+
16
+ - **Plots take line widths, marker sizes and fonts from the theme.** The plot functions no longer set their own line widths and marker sizes, so `apply_default_theme` and the user's rcParams control them; the theme's text is larger (16 pt, legends 14 pt, titles 18 pt) with longer ticks. SI-prefix tick labels show the prefix only (`10 k`, not `10 kHz`), as the axis labels already carry the unit.
17
+
18
+ ### Fixed
19
+
20
+ - **`MthPower` estimates offsets near 0 Hz.** It zeroed bin 0 of the M-th power spectrum and, for constellations without a constant envelope, subtracted the mean of `x^M`, which for QAM is the tone itself: an offset with `|M Δf|` under half a bin was never found (QPSK, 4096 symbols at 1 MS/s: 0 Hz read as -82 kHz, 10 Hz as 30.5 Hz). Both steps are removed; at other offsets the estimates do not change, also with a DC offset of -20 dB in the input.
21
+ - Messages that report what the caller asked for log at INFO, not WARNING, so a WARNING from commkit always means a suspect result: the block equalizers' advice to move CPU data to the GPU, RLS's tail trim, `correct_timing(mode="slice")` shortening the record and the plotting theme's font fallback. The advice to use more taps for fractionally spaced equalization is no longer logged at `sps=1`, where it does not apply.
22
+ - `rls()`: `weights_history` has one row per returned symbol, so it matches `y_hat` after the tail trim (it kept the `num_taps // 2` trimmed rows). A run whose inverse correlation matrix loses positive definiteness (a forgetting factor far below 1) raises the documented `RuntimeError` instead of a bare `ZeroDivisionError` from the kernel, and the message now advises raising the forgetting factor toward 1, not lowering it.
23
+
24
+ ### Development
25
+
26
+ - The test suite checks what the code achieves, not how it is written. Equalizers converge on RRC records through ISI, fractional delays, polarization rotation and DGD, scored against the transmitted symbols with only the legitimate rotation and permutation removed, and each "improves" test also runs a baseline that must fail; carrier recovery is scored against the true phase; estimators and metrics against Cramér–Rao bounds and AWGN theory; Chase and shortened-code decoding against brute force. New end-to-end receiver chains (coded single carrier, dual polarization, 80 km of fiber) each show that leaving out one correction breaks them. Monte Carlo tests state what their frame counts bound and gain `--run-slow` validation runs. A WARNING log from commkit fails the test. Tests that only repeated the contract registry's shape and device checks are removed.
27
+ - The library faults these tests found are fixed above: the PLL's cycle-slip repair adding slips, Tikhonov's noise model on QAM, the dead zone of `MthPower` around 0 Hz, the alignment of even-length taps (`ols_fir_filter` against `fir_filter`) and RLS's `weights_history` length.
28
+ - `benchutils.assert_converged` checks the symbol error rate of the worst channel (it compared bit errors before), and the 2048-tap block RDE benchmark uses a workload that converges.
6
29
 
7
30
  ## [3.2.0] - 2026-10-30
8
31
 
@@ -318,7 +341,8 @@ See the [GitHub release](https://github.com/lokgar/commkit/releases/tag/v1.1.0)
318
341
 
319
342
  First release. See the [GitHub release](https://github.com/lokgar/commkit/releases/tag/v1.0.0).
320
343
 
321
- [Unreleased]: https://github.com/lokgar/commkit/compare/v3.2.0...HEAD
344
+ [Unreleased]: https://github.com/lokgar/commkit/compare/v3.2.1...HEAD
345
+ [3.2.1]: https://github.com/lokgar/commkit/compare/v3.2.0...v3.2.1
322
346
  [3.2.0]: https://github.com/lokgar/commkit/compare/v3.1.3...v3.2.0
323
347
  [3.1.3]: https://github.com/lokgar/commkit/compare/v3.1.2...v3.1.3
324
348
  [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.2.1
4
4
  Summary: A Python library for high-performance digital communications research.
5
5
  Project-URL: Homepage, https://github.com/lokgar/commkit
6
6
  Project-URL: Repository, https://github.com/lokgar/commkit
@@ -31,16 +31,18 @@ ROUNDS = dict(rounds=3, warmup_rounds=1, iterations=1)
31
31
  N_SYM = 100_000
32
32
 
33
33
  # Converging step sizes per (equalizer, block_size). RDE on 16-QAM from a
34
- # cold start (no CMA pre-convergence) converges slowly and is still settling
35
- # after the skipped symbols at 2048, hence its looser SER bound; its cost does
36
- # not depend on the data.
34
+ # cold start (no CMA pre-convergence) converges slowly at 2048: at 4e-4 it is
35
+ # error-free from about symbol 70 000, so its SER is scored after that (7e-4
36
+ # never settled, still 2-5 % SER at the end); its cost does not depend on
37
+ # the data.
37
38
  STEP_SIZE = {
38
39
  ("cma", 256): 2e-4,
39
40
  ("cma", 2048): 3e-4,
40
41
  ("rde", 256): 3e-3,
41
- ("rde", 2048): 7e-4,
42
+ ("rde", 2048): 4e-4,
42
43
  }
43
- MAX_SER = {"cma": 1e-2, "rde": 5e-2}
44
+ SKIP = {("rde", 2048): 70_000}
45
+ MAX_SER = {"cma": 1e-2, "rde": 1e-2}
44
46
 
45
47
  EQUALIZERS = [
46
48
  ("cma", block_cma),
@@ -66,7 +68,12 @@ def _bench_block_blind(benchmark, xp, sync, label, eq_fn, block_size):
66
68
 
67
69
  r = benchmark.pedantic(run, **ROUNDS)
68
70
  assert_converged(
69
- r, syms, Constellation.qam(16), skip=20_000, max_ser=MAX_SER[label], blind=True
71
+ r,
72
+ syms,
73
+ Constellation.qam(16),
74
+ skip=SKIP.get((label, block_size), 20_000),
75
+ max_ser=MAX_SER[label],
76
+ blind=True,
70
77
  )
71
78
 
72
79
 
@@ -26,16 +26,18 @@ LINK = dict(
26
26
  )
27
27
 
28
28
 
29
+ # The plain/fused split exists on the GPU only; its CPU items are deselected.
30
+ PATHS = ["auto", pytest.param("plain", marks=pytest.mark.gpu_only)]
31
+
32
+
29
33
  def _stages(backend_device, monkeypatch, path):
30
34
  if path == "plain":
31
- if backend_device != "gpu":
32
- pytest.skip("the plain/fused split exists on the GPU only")
33
35
  monkeypatch.setattr(
34
36
  _fiber, "_make_ops", lambda x, xp, *args: _fiber._Ops(xp, x.dtype, *args)
35
37
  )
36
38
 
37
39
 
38
- @pytest.mark.parametrize("path", ["auto", "plain"])
40
+ @pytest.mark.parametrize("path", PATHS)
39
41
  @pytest.mark.parametrize("n", [2**16, 2**20], ids=["N64k", "N1M"])
40
42
  def bench_fiber_propagation(benchmark, backend_device, xp, sync, monkeypatch, n, path):
41
43
  _stages(backend_device, monkeypatch, path)
@@ -49,7 +51,7 @@ def bench_fiber_propagation(benchmark, backend_device, xp, sync, monkeypatch, n,
49
51
  benchmark.pedantic(run, **ROUNDS)
50
52
 
51
53
 
52
- @pytest.mark.parametrize("path", ["auto", "plain"])
54
+ @pytest.mark.parametrize("path", PATHS)
53
55
  @pytest.mark.parametrize("steps", [1, 10])
54
56
  def bench_dbp(benchmark, backend_device, xp, sync, monkeypatch, steps, path):
55
57
  _stages(backend_device, monkeypatch, path)
@@ -64,14 +64,22 @@ def assert_converged(result, syms, constellation, *, skip, max_ser, blind=False)
64
64
  storms, divergence) instead of the operating point, so every equalizer
65
65
  benchmark checks its symbol error rate after ``skip`` symbols. Blind
66
66
  equalizers leave a phase ambiguity, which is resolved against the
67
- reference first.
67
+ reference first, after the channel permutation of a butterfly.
68
68
  """
69
69
  from commkit.backend import to_device
70
- from commkit.recovery import resolve_phase_ambiguity
70
+ from commkit.recovery import resolve_channel_permutation, resolve_phase_ambiguity
71
71
 
72
72
  y = to_device(result.y_hat, "cpu")[..., skip:]
73
73
  ref = to_device(syms, "cpu")[..., skip : skip + y.shape[-1]]
74
74
  if blind:
75
+ if y.ndim == 2:
76
+ y = resolve_channel_permutation(y, ref)
75
77
  y = resolve_phase_ambiguity(y, ref, constellation=constellation)
76
- ser = float((constellation.demap(y) != constellation.demap(ref)).mean())
78
+ # Symbol decisions, not bits (one wrong symbol is one error), and the
79
+ # worst channel, so one failed stream cannot hide behind a good one.
80
+ wrong = constellation._nearest(y.reshape(-1)) != constellation._nearest(
81
+ ref.reshape(-1)
82
+ )
83
+ per_channel = to_device(wrong, "cpu").reshape(y.shape).mean(axis=-1)
84
+ ser = float(per_channel.max())
77
85
  assert ser <= max_ser, f"benchmark workload did not converge: SER {ser:.3g}"
@@ -56,6 +56,20 @@ def pytest_generate_tests(metafunc):
56
56
  metafunc.parametrize("backend_device", params, indirect=True)
57
57
 
58
58
 
59
+ def pytest_collection_modifyitems(config, items):
60
+ """Deselect, not skip, the CPU items of ``gpu_only`` parameters."""
61
+ cpu_only_gpu = [
62
+ item
63
+ for item in items
64
+ if item.get_closest_marker("gpu_only")
65
+ and getattr(item, "callspec", None) is not None
66
+ and item.callspec.params.get("backend_device") == "cpu"
67
+ ]
68
+ if cpu_only_gpu:
69
+ items[:] = [item for item in items if item not in cpu_only_gpu]
70
+ config.hook.pytest_deselected(items=cpu_only_gpu)
71
+
72
+
59
73
  @numba.njit(cache=True, parallel=True)
60
74
  def _spin(x):
61
75
  total = 0.0
@@ -31,7 +31,7 @@ Matplotlib, change warning filters, or touch the GPU.
31
31
  import importlib
32
32
  from typing import Any
33
33
 
34
- __version__ = "3.2.0"
34
+ __version__ = "3.2.1"
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
 
@@ -387,8 +387,8 @@ def ols_fir_filter(
387
387
  is at least ``L`` samples long.
388
388
  causal : bool, default False
389
389
  By default the output is aligned as :func:`fir_filter` aligns it
390
- (SciPy's ``mode='same'``, centered on tap ``L // 2``):
391
- ``y[n] = sum_k x[n + L//2 - k]·taps[k]``, which removes the group
390
+ (SciPy's ``mode='same'``, centered on tap ``(L - 1) // 2``):
391
+ ``y[n] = sum_k x[n + (L-1)//2 - k]·taps[k]``, which removes the group
392
392
  delay of a symmetric filter before symbol sampling. With ``True``
393
393
  the output is the causal convolution ``y[n] = sum_k x[n-k]·taps[k]``
394
394
  (``numpy.convolve(x, taps, mode='full')[:N]``), for a raw causal
@@ -412,7 +412,7 @@ def ols_fir_filter(
412
412
  A symmetric guard of ``nfft // 4`` samples is discarded from each
413
413
  block edge, so ``nfft // 4 >= len(taps)`` must hold.
414
414
 
415
- Without ``causal`` the input is padded at the end by ``L // 2`` zeros
415
+ Without ``causal`` the input is padded at the end by ``(L - 1) // 2`` zeros
416
416
  and as many leading output samples are dropped, which costs at most one
417
417
  extra block.
418
418
  """
@@ -425,7 +425,7 @@ def ols_fir_filter(
425
425
  out_dtype = x.dtype # capture before any reshape
426
426
 
427
427
  L = len(taps)
428
- half = L // 2
428
+ half = (L - 1) // 2 # SciPy's mode='same' centre, as fir_filter
429
429
 
430
430
  x, was_1d = as_2d(x, name="samples")
431
431
 
@@ -473,7 +473,9 @@ def fir_filter(samples: S, taps: ArrayType, *, periodic: bool = False) -> S:
473
473
  """Apply an FIR filter along the time (last) axis.
474
474
 
475
475
  FFT convolution, ``mode="same"``: the output is centered on tap
476
- ``len(taps) // 2``, so a symmetric filter adds no delay. With
476
+ ``(len(taps) - 1) // 2``, ``y[n] = sum_k x[n + (L-1)//2 - k]·taps[k]``,
477
+ so a symmetric filter of odd length adds no delay (one of even length,
478
+ whose centre falls between two taps, half a sample). With
477
479
  ``periodic=True`` the convolution is circular; away from the record ends
478
480
  both give the same samples.
479
481
 
@@ -476,9 +476,6 @@ def _mth_power(
476
476
  # In the samples' precision: complex128 changed no estimate by more than
477
477
  # 0.005 Hz (16-QAM, 2^16 symbols, 15 dB; the error itself is 726 Hz rms).
478
478
  x_m = x**M
479
- # Non-constant-envelope symbols leave an amplitude residual at DC.
480
- if constellation is not None and np.ptp(np.abs(constellation.points)) > 1e-9:
481
- x_m = x_m - xp.mean(x_m, axis=-1, keepdims=True)
482
479
 
483
480
  X_m = _fft.fft(x_m, n=nfft, axis=-1) # (R, nfft)
484
481
  freqs_np = np.fft.fftfreq(nfft, d=1.0 / fs) # reported on the host
@@ -494,7 +491,6 @@ def _mth_power(
494
491
  freqs = xp.fft.fftfreq(nfft, d=1.0 / fs)
495
492
  mask = (freqs >= tone_lo) & (freqs <= tone_hi)
496
493
  mag = xp.where(mask[None, :], mag, xp.zeros_like(mag))
497
- mag[:, 0] = 0.0 # Δf = 0 is degenerate; also any residual DC
498
494
 
499
495
  if shared_rows:
500
496
  k_peak = xp.broadcast_to(xp.argmax(xp.sum(mag, axis=0)), (R,))
@@ -92,8 +92,8 @@ def plot_frequency_drift(
92
92
  axi.axhline(amp_ref, color="white", ls="--", label="±amplitude")
93
93
  axi.axhline(-amp_ref, color="white", ls="--")
94
94
 
95
- _set_eng_formatter(axi, "x", "s")
96
- _set_eng_formatter(axi, "y", "Hz")
95
+ _set_eng_formatter(axi, "x")
96
+ _set_eng_formatter(axi, "y")
97
97
  axi.set_xlabel("Time [s]")
98
98
  axi.set_ylabel(r"$\Delta f$ [Hz]")
99
99
  axi.set_title(title)
@@ -232,7 +232,7 @@ def plot_frequency_noise_psd(
232
232
  model = getattr(estimate, "S_f_model", None)
233
233
  if model is not None:
234
234
  m_c = _as_channels(model)
235
- axi.loglog(fp, m_c[0, pos], color="#ef476f", lw=1.5, label="Fitted model")
235
+ axi.loglog(fp, m_c[0, pos], color="#ef476f", label="Fitted model")
236
236
 
237
237
  used_label = "Fit bins" if model is not None else "Plateau bins (median)"
238
238
  if used is not None:
@@ -246,7 +246,6 @@ def plot_frequency_noise_psd(
246
246
  S_c[0, pos][sel],
247
247
  ".",
248
248
  color="#06d6a0",
249
- ms=3.0,
250
249
  ls="none",
251
250
  label=used_label,
252
251
  )
@@ -282,7 +281,7 @@ def plot_frequency_noise_psd(
282
281
  for edge in (lo, float(band[1])):
283
282
  axi.axvline(edge, color="#06d6a0")
284
283
 
285
- _set_eng_formatter(axi, "x", "Hz")
284
+ _set_eng_formatter(axi, "x")
286
285
  axi.set_xlabel("Frequency [Hz]")
287
286
  axi.set_ylabel("$S_f$ [Hz²/Hz]")
288
287
  axi.set_title(title)
@@ -338,7 +337,6 @@ def plot_allan_deviation(
338
337
  tau,
339
338
  adv[i],
340
339
  "o-",
341
- ms=3,
342
340
  color=f"C{i}",
343
341
  label=f"Pol {i}" if C > 1 else r"$\sigma_y(\tau)$",
344
342
  )
@@ -356,8 +354,8 @@ def plot_allan_deviation(
356
354
  label=r"$\tau^{-1/2}$ (white-FM)",
357
355
  )
358
356
 
359
- _set_eng_formatter(axi, "x", "s")
360
- _set_eng_formatter(axi, "y", "Hz")
357
+ _set_eng_formatter(axi, "x")
358
+ _set_eng_formatter(axi, "y")
361
359
  axi.set_xlabel(r"Averaging Time $\tau$ [s]")
362
360
  axi.set_ylabel(r"Allan Deviation $\sigma_y(\tau)$ [Hz]")
363
361
  axi.set_title(title)
@@ -414,7 +412,6 @@ def plot_increment_variance(
414
412
  lag,
415
413
  v[i],
416
414
  "o",
417
- ms=4,
418
415
  color=f"C{i}",
419
416
  label=f"Measured pol {i}" if C > 1 else "Measured",
420
417
  )
@@ -437,7 +434,7 @@ def plot_increment_variance(
437
434
  label=r"Intercept (noise $2\sigma_\phi^2$)",
438
435
  )
439
436
 
440
- _set_eng_formatter(axi, "x", "s")
437
+ _set_eng_formatter(axi, "x")
441
438
  axi.set_xlabel("Increment Lag [s]")
442
439
  axi.set_ylabel("Variance [rad²]")
443
440
  axi.set_title(title)
@@ -517,7 +514,7 @@ def plot_dsh_beat_psd(
517
514
  label=f"$W_{{-{level_db:g}\\,dB}}$",
518
515
  )
519
516
 
520
- _set_eng_formatter(axi, "x", "Hz")
517
+ _set_eng_formatter(axi, "x")
521
518
  axi.set_xlabel(
522
519
  "Frequency Offset from Beat Peak [Hz]"
523
520
  if f_peak is not None
@@ -158,7 +158,7 @@ def plot_llr_distribution(
158
158
  with np.errstate(divide="ignore"):
159
159
  ratio = np.log(h0[j][keep] / h1[j][keep]) + np.log(n1[j] / n0[j])
160
160
  label = "measured" if m == 1 else f"bit {j}"
161
- ax_cons.plot(centers[keep], ratio, "o-", markersize=3, label=label)
161
+ ax_cons.plot(centers[keep], ratio, "o-", label=label)
162
162
  if keep.any():
163
163
  span = [min(span[0], centers[keep][0]), max(span[1], centers[keep][-1])]
164
164
  if span[0] > span[1]:
@@ -257,7 +257,7 @@ def plot_parity_check(
257
257
  ax_h.spy(h, markersize=size, aspect="auto", rasterized=True)
258
258
  if punctured:
259
259
  ax_h.axvspan(n - punctured - 0.5, n - 0.5, color="gray", alpha=0.3)
260
- ax_h.text(n - punctured, -0.5, "punctured", va="bottom", fontsize=9)
260
+ ax_h.text(n - punctured, -0.5, "punctured", va="bottom")
261
261
  ax_h.tick_params(labeltop=False, labelbottom=True)
262
262
  ax_h.grid(False)
263
263
  ax_h.set_xlabel("Variable node (column)")
@@ -342,7 +342,6 @@ def plot_exit_chart(
342
342
  chart.trajectory[:, 0],
343
343
  chart.trajectory[:, 1],
344
344
  color="white",
345
- linewidth=0.8,
346
345
  alpha=0.8,
347
346
  label="trajectory",
348
347
  )
@@ -358,5 +357,5 @@ def plot_exit_chart(
358
357
  state = "open" if chart.converges else "closed"
359
358
  title = f"EXIT chart: tunnel {state}, {chart.iterations} iterations"
360
359
  axi.set_title(title)
361
- axi.legend(loc="lower right", fontsize=8)
360
+ axi.legend(loc="lower right")
362
361
  return _finish((fig, axi), show)
@@ -130,7 +130,6 @@ def plot_equalizer_result(
130
130
  tap_idx,
131
131
  np.abs(weights[i, j]),
132
132
  marker="o",
133
- markersize=3,
134
133
  label=label,
135
134
  )
136
135
  ax_taps.legend(ncol=2)
@@ -138,8 +137,7 @@ def plot_equalizer_result(
138
137
  # SISO: (num_taps,) - stem plot
139
138
  num_taps = weights.shape[0]
140
139
  tap_idx = np.arange(num_taps) - (num_taps // 2)
141
- markerline, _, _ = ax_taps.stem(tap_idx, np.abs(weights))
142
- markerline.set_markersize(4)
140
+ ax_taps.stem(tap_idx, np.abs(weights))
143
141
 
144
142
  ax_taps.set_xlabel("Tap Index")
145
143
  ax_taps.set_ylabel("|w|")
@@ -241,7 +239,7 @@ def plot_zf_equalizer_response(
241
239
  axi.plot(f_sorted, 20 * np.log10(np.abs(data) + 1e-12), color=col)
242
240
  prefix = f"{row_label} " if row_label else ""
243
241
  axi.set_title(f"{prefix}{lbl}")
244
- _set_eng_formatter(axi, "x", "Hz")
242
+ _set_eng_formatter(axi, "x")
245
243
  axi.set_xlabel("Frequency [Hz]")
246
244
  axi.set_ylabel("Magnitude [dB]")
247
245
 
@@ -108,7 +108,7 @@ def _plot_eye_traces(
108
108
  segments = np.empty((traces.shape[0], trace_len, 2))
109
109
  segments[:, :, 0] = t
110
110
  segments[:, :, 1] = traces
111
- line_kwargs: dict[str, Any] = {"alpha": 0.2, "linewidth": 1, "color": "C0"}
111
+ line_kwargs: dict[str, Any] = {"alpha": 0.2, "color": "C0"}
112
112
  line_kwargs.update(kwargs)
113
113
  ax.add_collection(LineCollection(list(segments), **line_kwargs))
114
114
  y_lo, y_hi = float(traces.min()), float(traces.max())