commkit 2.0.0__tar.gz → 2.2.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 (241) hide show
  1. {commkit-2.0.0 → commkit-2.2.0}/.github/workflows/ci.yml +1 -1
  2. {commkit-2.0.0 → commkit-2.2.0}/.gitignore +13 -0
  3. commkit-2.2.0/AGENTS.md +295 -0
  4. {commkit-2.0.0 → commkit-2.2.0}/PKG-INFO +36 -100
  5. {commkit-2.0.0 → commkit-2.2.0}/README.md +35 -99
  6. commkit-2.2.0/benchmarks/baselines/Linux-CPython-3.14-64bit/0004_fiber_dbp.json +3676 -0
  7. {commkit-2.0.0 → commkit-2.2.0}/benchmarks/bench_block_blind.py +3 -2
  8. commkit-2.2.0/benchmarks/bench_fiber.py +62 -0
  9. {commkit-2.0.0 → commkit-2.2.0}/benchmarks/workloads.py +17 -0
  10. {commkit-2.0.0 → commkit-2.2.0}/commkit/__init__.py +3 -1
  11. {commkit-2.0.0 → commkit-2.2.0}/commkit/_array.py +47 -10
  12. commkit-2.2.0/commkit/_circular.py +75 -0
  13. commkit-2.2.0/commkit/_cuda/graphs.py +121 -0
  14. {commkit-2.0.0 → commkit-2.2.0}/commkit/_dispersion.py +57 -6
  15. commkit-2.2.0/commkit/_fft.py +80 -0
  16. commkit-2.2.0/commkit/_fiber.py +806 -0
  17. {commkit-2.0.0 → commkit-2.2.0}/commkit/_overlap_save.py +3 -2
  18. {commkit-2.0.0 → commkit-2.2.0}/commkit/_random.py +17 -0
  19. {commkit-2.0.0 → commkit-2.2.0}/commkit/analysis/_common.py +73 -0
  20. {commkit-2.0.0 → commkit-2.2.0}/commkit/analysis/drift.py +5 -5
  21. commkit-2.2.0/commkit/analysis/fm_noise.py +322 -0
  22. {commkit-2.0.0 → commkit-2.2.0}/commkit/analysis/interferometry.py +113 -14
  23. {commkit-2.0.0 → commkit-2.2.0}/commkit/analysis/linewidth.py +288 -139
  24. {commkit-2.0.0 → commkit-2.2.0}/commkit/analysis/trajectory.py +2 -0
  25. {commkit-2.0.0 → commkit-2.2.0}/commkit/backend.py +12 -0
  26. {commkit-2.0.0 → commkit-2.2.0}/commkit/coding/__init__.py +1 -1
  27. {commkit-2.0.0 → commkit-2.2.0}/commkit/core/frame.py +67 -1
  28. {commkit-2.0.0 → commkit-2.2.0}/commkit/core/generation.py +29 -3
  29. {commkit-2.0.0 → commkit-2.2.0}/commkit/equalization/_block/_blind.py +9 -9
  30. {commkit-2.0.0 → commkit-2.2.0}/commkit/equalization/_block/_dd.py +34 -39
  31. {commkit-2.0.0 → commkit-2.2.0}/commkit/equalization/_block/_engine.py +22 -61
  32. {commkit-2.0.0 → commkit-2.2.0}/commkit/equalization/blind.py +8 -2
  33. {commkit-2.0.0 → commkit-2.2.0}/commkit/equalization/linear.py +55 -19
  34. {commkit-2.0.0 → commkit-2.2.0}/commkit/equalization/polarization.py +2 -1
  35. {commkit-2.0.0 → commkit-2.2.0}/commkit/equalization/result.py +29 -56
  36. {commkit-2.0.0 → commkit-2.2.0}/commkit/equalization/sequential/_blind.py +4 -4
  37. {commkit-2.0.0 → commkit-2.2.0}/commkit/equalization/sequential/_dd.py +3 -3
  38. {commkit-2.0.0 → commkit-2.2.0}/commkit/equalization/sequential/_setup.py +3 -2
  39. {commkit-2.0.0 → commkit-2.2.0}/commkit/filtering.py +164 -38
  40. {commkit-2.0.0 → commkit-2.2.0}/commkit/frequency.py +12 -26
  41. {commkit-2.0.0 → commkit-2.2.0}/commkit/impairments/__init__.py +6 -1
  42. {commkit-2.0.0 → commkit-2.2.0}/commkit/impairments/channel/__init__.py +6 -3
  43. {commkit-2.0.0 → commkit-2.2.0}/commkit/impairments/channel/linear.py +19 -19
  44. commkit-2.2.0/commkit/impairments/channel/nonlinear.py +180 -0
  45. {commkit-2.0.0 → commkit-2.2.0}/commkit/impairments/noise.py +2 -3
  46. {commkit-2.0.0 → commkit-2.2.0}/commkit/impairments/source.py +58 -13
  47. {commkit-2.0.0 → commkit-2.2.0}/commkit/mapping/constellation.py +74 -16
  48. {commkit-2.0.0 → commkit-2.2.0}/commkit/mapping/gray.py +0 -36
  49. commkit-2.2.0/commkit/mapping/llr.py +417 -0
  50. {commkit-2.0.0 → commkit-2.2.0}/commkit/math.py +69 -1
  51. {commkit-2.0.0 → commkit-2.2.0}/commkit/metrics.py +48 -37
  52. {commkit-2.0.0 → commkit-2.2.0}/commkit/multirate.py +52 -10
  53. {commkit-2.0.0 → commkit-2.2.0}/commkit/plotting/analysis.py +16 -6
  54. {commkit-2.0.0 → commkit-2.2.0}/commkit/plotting/equalizer.py +3 -2
  55. {commkit-2.0.0 → commkit-2.2.0}/commkit/plotting/filter_response.py +2 -2
  56. {commkit-2.0.0 → commkit-2.2.0}/commkit/plotting/sync.py +2 -1
  57. {commkit-2.0.0 → commkit-2.2.0}/commkit/recovery/_common.py +6 -8
  58. {commkit-2.0.0 → commkit-2.2.0}/commkit/recovery/bps.py +14 -6
  59. {commkit-2.0.0 → commkit-2.2.0}/commkit/recovery/carrier_phase.py +4 -41
  60. {commkit-2.0.0 → commkit-2.2.0}/commkit/recovery/corrections.py +58 -11
  61. {commkit-2.0.0 → commkit-2.2.0}/commkit/recovery/pilots.py +10 -6
  62. {commkit-2.0.0 → commkit-2.2.0}/commkit/recovery/pll.py +1 -1
  63. {commkit-2.0.0 → commkit-2.2.0}/commkit/recovery/tikhonov.py +1 -1
  64. {commkit-2.0.0 → commkit-2.2.0}/commkit/recovery/viterbi_viterbi.py +1 -1
  65. {commkit-2.0.0 → commkit-2.2.0}/commkit/smoothing.py +3 -1
  66. {commkit-2.0.0 → commkit-2.2.0}/commkit/spectral.py +7 -21
  67. {commkit-2.0.0 → commkit-2.2.0}/commkit/timing.py +14 -18
  68. commkit-2.2.0/examples/00-getting-started.ipynb +352 -0
  69. commkit-2.2.0/examples/01-constellations-and-bit-mapping.ipynb +282 -0
  70. commkit-2.2.0/examples/02-spectral-analysis.ipynb +304 -0
  71. commkit-2.2.0/examples/03-digital-filters.ipynb +347 -0
  72. commkit-2.2.0/examples/04-pulse-shaping-and-matched-filtering.ipynb +387 -0
  73. commkit-2.2.0/examples/05-multirate-and-resampling.ipynb +340 -0
  74. commkit-2.2.0/examples/06-awgn-and-performance-metrics.ipynb +381 -0
  75. commkit-2.2.0/examples/07-laser-phase-noise.ipynb +319 -0
  76. commkit-2.2.0/examples/08-front-end-impairments.ipynb +294 -0
  77. commkit-2.2.0/examples/09-frame-synchronization-and-timing.ipynb +310 -0
  78. commkit-2.2.0/examples/10-frequency-offset-estimation.ipynb +397 -0
  79. commkit-2.2.0/examples/11-blind-carrier-phase-recovery.ipynb +367 -0
  80. commkit-2.2.0/examples/12-pilot-aided-carrier-recovery.ipynb +324 -0
  81. commkit-2.2.0/examples/13-linear-channel-equalization.ipynb +293 -0
  82. commkit-2.2.0/examples/14-adaptive-equalization.ipynb +433 -0
  83. commkit-2.2.0/examples/15-blind-equalization.ipynb +275 -0
  84. commkit-2.2.0/examples/16-polarization-multiplexing.ipynb +314 -0
  85. commkit-2.2.0/examples/17-chromatic-dispersion.ipynb +286 -0
  86. commkit-2.2.0/examples/18-nonlinear-fiber-and-backpropagation.ipynb +228 -0
  87. commkit-2.2.0/examples/19-coherent-transceiver-end-to-end.ipynb +397 -0
  88. commkit-2.0.0/examples/carrier_phase_analysis.ipynb → commkit-2.2.0/examples/20-carrier-phase-characterization.ipynb +22 -10
  89. commkit-2.0.0/examples/laser_linewidth_dsh.ipynb → commkit-2.2.0/examples/21-laser-linewidth-self-heterodyne.ipynb +115 -275
  90. commkit-2.2.0/examples/22-laser-linewidth-short-delay-and-flicker.ipynb +387 -0
  91. commkit-2.0.0/examples/laser_linewidth_homodyne_iq.ipynb → commkit-2.2.0/examples/23-laser-linewidth-self-homodyne-iq.ipynb +18 -8
  92. commkit-2.0.0/examples/measurement_laser_linewidth_dsh.ipynb → commkit-2.2.0/examples/24-measurement-template-self-heterodyne.ipynb +23 -23
  93. commkit-2.0.0/examples/measurement_laser_linewidth_homodyne_iq.ipynb → commkit-2.2.0/examples/25-measurement-template-self-homodyne-iq.ipynb +9 -9
  94. commkit-2.2.0/examples/README.md +63 -0
  95. {commkit-2.0.0 → commkit-2.2.0}/pyproject.toml +8 -5
  96. {commkit-2.0.0 → commkit-2.2.0}/tests/analysis/test_drift.py +10 -0
  97. commkit-2.2.0/tests/analysis/test_fm_noise.py +60 -0
  98. {commkit-2.0.0 → commkit-2.2.0}/tests/analysis/test_interferometry.py +54 -1
  99. {commkit-2.0.0 → commkit-2.2.0}/tests/analysis/test_linewidth.py +209 -39
  100. commkit-2.2.0/tests/common/periodic.py +40 -0
  101. {commkit-2.0.0 → commkit-2.2.0}/tests/common/reference_impl.py +5 -2
  102. {commkit-2.0.0 → commkit-2.2.0}/tests/common/signals.py +38 -2
  103. {commkit-2.0.0 → commkit-2.2.0}/tests/conftest.py +6 -1
  104. {commkit-2.0.0 → commkit-2.2.0}/tests/core/test_frame.py +63 -0
  105. {commkit-2.0.0 → commkit-2.2.0}/tests/core/test_generation.py +68 -0
  106. {commkit-2.0.0 → commkit-2.2.0}/tests/core/test_signal.py +1 -1
  107. {commkit-2.0.0 → commkit-2.2.0}/tests/equalization/test_blind.py +42 -0
  108. {commkit-2.0.0 → commkit-2.2.0}/tests/equalization/test_block.py +34 -1
  109. {commkit-2.0.0 → commkit-2.2.0}/tests/equalization/test_cpr.py +8 -8
  110. {commkit-2.0.0 → commkit-2.2.0}/tests/equalization/test_linear.py +67 -4
  111. {commkit-2.0.0 → commkit-2.2.0}/tests/equalization/test_sequential.py +24 -0
  112. {commkit-2.0.0 → commkit-2.2.0}/tests/equalization/test_state.py +24 -0
  113. commkit-2.2.0/tests/impairments/channel/test_channel_nonlinear.py +401 -0
  114. {commkit-2.0.0 → commkit-2.2.0}/tests/impairments/test_source.py +63 -4
  115. {commkit-2.0.0 → commkit-2.2.0}/tests/mapping/test_constellation.py +4 -1
  116. {commkit-2.0.0 → commkit-2.2.0}/tests/mapping/test_gray.py +27 -29
  117. {commkit-2.0.0 → commkit-2.2.0}/tests/mapping/test_llr.py +71 -10
  118. commkit-2.2.0/tests/plugins/__init__.py +0 -0
  119. commkit-2.2.0/tests/plugins/precision_trace.py +176 -0
  120. {commkit-2.0.0 → commkit-2.2.0}/tests/recovery/test_bps.py +25 -0
  121. {commkit-2.0.0 → commkit-2.2.0}/tests/recovery/test_corrections.py +75 -1
  122. {commkit-2.0.0 → commkit-2.2.0}/tests/test_api_contracts.py +25 -2
  123. {commkit-2.0.0 → commkit-2.2.0}/tests/test_array.py +45 -2
  124. {commkit-2.0.0 → commkit-2.2.0}/tests/test_cuda_infra.py +49 -0
  125. {commkit-2.0.0 → commkit-2.2.0}/tests/test_dispersion.py +29 -1
  126. commkit-2.2.0/tests/test_fiber.py +248 -0
  127. {commkit-2.0.0 → commkit-2.2.0}/tests/test_filtering.py +163 -0
  128. {commkit-2.0.0 → commkit-2.2.0}/tests/test_metrics.py +4 -2
  129. {commkit-2.0.0 → commkit-2.2.0}/tests/test_multirate.py +54 -0
  130. {commkit-2.0.0 → commkit-2.2.0}/tests/test_pulse_shaping.py +3 -3
  131. {commkit-2.0.0 → commkit-2.2.0}/uv.lock +1 -1
  132. commkit-2.0.0/AGENTS.md +0 -384
  133. commkit-2.0.0/commkit/analysis/fm_noise.py +0 -251
  134. commkit-2.0.0/commkit/impairments/channel/nonlinear.py +0 -11
  135. commkit-2.0.0/commkit/mapping/llr.py +0 -255
  136. commkit-2.0.0/examples/qam_receiver_quickstart.ipynb +0 -245
  137. {commkit-2.0.0 → commkit-2.2.0}/.gitattributes +0 -0
  138. {commkit-2.0.0 → commkit-2.2.0}/.github/workflows/publish.yml +0 -0
  139. {commkit-2.0.0 → commkit-2.2.0}/.python-version +0 -0
  140. {commkit-2.0.0 → commkit-2.2.0}/LICENSE +0 -0
  141. {commkit-2.0.0 → commkit-2.2.0}/benchmarks/baselines/Linux-CPython-3.14-64bit/0002_pre_v2.json +0 -0
  142. {commkit-2.0.0 → commkit-2.2.0}/benchmarks/baselines/Linux-CPython-3.14-64bit/0003_v2_0.json +0 -0
  143. {commkit-2.0.0 → commkit-2.2.0}/benchmarks/bench_block_lms.py +0 -0
  144. {commkit-2.0.0 → commkit-2.2.0}/benchmarks/bench_bps.py +0 -0
  145. {commkit-2.0.0 → commkit-2.2.0}/benchmarks/bench_equalizers.py +0 -0
  146. {commkit-2.0.0 → commkit-2.2.0}/benchmarks/bench_import.py +0 -0
  147. {commkit-2.0.0 → commkit-2.2.0}/benchmarks/bench_llr.py +0 -0
  148. {commkit-2.0.0 → commkit-2.2.0}/benchmarks/bench_signal_pipeline.py +0 -0
  149. {commkit-2.0.0 → commkit-2.2.0}/benchmarks/bench_sync_misc.py +0 -0
  150. {commkit-2.0.0 → commkit-2.2.0}/benchmarks/benchutils.py +0 -0
  151. {commkit-2.0.0 → commkit-2.2.0}/benchmarks/conftest.py +0 -0
  152. {commkit-2.0.0 → commkit-2.2.0}/benchmarks/record_baseline.py +0 -0
  153. {commkit-2.0.0 → commkit-2.2.0}/commkit/_cuda/__init__.py +0 -0
  154. {commkit-2.0.0 → commkit-2.2.0}/commkit/_cuda/compiler.py +0 -0
  155. {commkit-2.0.0 → commkit-2.2.0}/commkit/_cuda/src/bps_anchor.cu +0 -0
  156. {commkit-2.0.0 → commkit-2.2.0}/commkit/_cuda/src/bps_min_d2.cu +0 -0
  157. {commkit-2.0.0 → commkit-2.2.0}/commkit/_cuda/src/cs_block.cu +0 -0
  158. {commkit-2.0.0 → commkit-2.2.0}/commkit/_cuda/src/llr.cu +0 -0
  159. {commkit-2.0.0 → commkit-2.2.0}/commkit/_cuda/src/selftest.cu +0 -0
  160. {commkit-2.0.0 → commkit-2.2.0}/commkit/_sequences.py +0 -0
  161. {commkit-2.0.0 → commkit-2.2.0}/commkit/analysis/__init__.py +0 -0
  162. {commkit-2.0.0 → commkit-2.2.0}/commkit/analysis/allan.py +0 -0
  163. {commkit-2.0.0 → commkit-2.2.0}/commkit/coding/base.py +0 -0
  164. {commkit-2.0.0 → commkit-2.2.0}/commkit/coding/bch.py +0 -0
  165. {commkit-2.0.0 → commkit-2.2.0}/commkit/coding/convolutional.py +0 -0
  166. {commkit-2.0.0 → commkit-2.2.0}/commkit/coding/crc.py +0 -0
  167. {commkit-2.0.0 → commkit-2.2.0}/commkit/coding/galois.py +0 -0
  168. {commkit-2.0.0 → commkit-2.2.0}/commkit/coding/hamming.py +0 -0
  169. {commkit-2.0.0 → commkit-2.2.0}/commkit/coding/interleaving.py +0 -0
  170. {commkit-2.0.0 → commkit-2.2.0}/commkit/coding/ldpc.py +0 -0
  171. {commkit-2.0.0 → commkit-2.2.0}/commkit/coding/polar.py +0 -0
  172. {commkit-2.0.0 → commkit-2.2.0}/commkit/coding/ratematch.py +0 -0
  173. {commkit-2.0.0 → commkit-2.2.0}/commkit/coding/reed_solomon.py +0 -0
  174. {commkit-2.0.0 → commkit-2.2.0}/commkit/coding/turbo.py +0 -0
  175. {commkit-2.0.0 → commkit-2.2.0}/commkit/core/__init__.py +0 -0
  176. {commkit-2.0.0 → commkit-2.2.0}/commkit/core/_signal_adapter.py +0 -0
  177. {commkit-2.0.0 → commkit-2.2.0}/commkit/core/signal.py +0 -0
  178. {commkit-2.0.0 → commkit-2.2.0}/commkit/equalization/__init__.py +0 -0
  179. {commkit-2.0.0 → commkit-2.2.0}/commkit/equalization/_block/__init__.py +0 -0
  180. {commkit-2.0.0 → commkit-2.2.0}/commkit/equalization/_common.py +0 -0
  181. {commkit-2.0.0 → commkit-2.2.0}/commkit/equalization/_kernels_numba.py +0 -0
  182. {commkit-2.0.0 → commkit-2.2.0}/commkit/equalization/sequential/__init__.py +0 -0
  183. {commkit-2.0.0 → commkit-2.2.0}/commkit/impairments/frontend.py +0 -0
  184. {commkit-2.0.0 → commkit-2.2.0}/commkit/io.py +0 -0
  185. {commkit-2.0.0 → commkit-2.2.0}/commkit/logger.py +0 -0
  186. {commkit-2.0.0 → commkit-2.2.0}/commkit/mapping/__init__.py +0 -0
  187. {commkit-2.0.0 → commkit-2.2.0}/commkit/mapping/bits.py +0 -0
  188. {commkit-2.0.0 → commkit-2.2.0}/commkit/mapping/shaping.py +0 -0
  189. {commkit-2.0.0 → commkit-2.2.0}/commkit/plotting/__init__.py +0 -0
  190. {commkit-2.0.0 → commkit-2.2.0}/commkit/plotting/constellation.py +0 -0
  191. {commkit-2.0.0 → commkit-2.2.0}/commkit/plotting/eye.py +0 -0
  192. {commkit-2.0.0 → commkit-2.2.0}/commkit/plotting/spectral.py +0 -0
  193. {commkit-2.0.0 → commkit-2.2.0}/commkit/plotting/theme.py +0 -0
  194. {commkit-2.0.0 → commkit-2.2.0}/commkit/plotting/waveform.py +0 -0
  195. {commkit-2.0.0 → commkit-2.2.0}/commkit/py.typed +0 -0
  196. {commkit-2.0.0 → commkit-2.2.0}/commkit/recovery/__init__.py +0 -0
  197. {commkit-2.0.0 → commkit-2.2.0}/tests/analysis/test_allan.py +0 -0
  198. {commkit-2.0.0 → commkit-2.2.0}/tests/analysis/test_trajectory.py +0 -0
  199. {commkit-2.0.0 → commkit-2.2.0}/tests/common/__init__.py +0 -0
  200. {commkit-2.0.0 → commkit-2.2.0}/tests/common/conversions.py +0 -0
  201. {commkit-2.0.0 → commkit-2.2.0}/tests/common/kernel_utils.py +0 -0
  202. {commkit-2.0.0 → commkit-2.2.0}/tests/common/metrics.py +0 -0
  203. {commkit-2.0.0 → commkit-2.2.0}/tests/core/test_psqam.py +0 -0
  204. {commkit-2.0.0 → commkit-2.2.0}/tests/core/test_signal_adapter.py +0 -0
  205. {commkit-2.0.0 → commkit-2.2.0}/tests/core/test_signal_mimo.py +0 -0
  206. {commkit-2.0.0 → commkit-2.2.0}/tests/equalization/test_bps_kernel.py +0 -0
  207. {commkit-2.0.0 → commkit-2.2.0}/tests/equalization/test_cs_kernel.py +0 -0
  208. {commkit-2.0.0 → commkit-2.2.0}/tests/equalization/test_equalizer_oracles.py +0 -0
  209. {commkit-2.0.0 → commkit-2.2.0}/tests/equalization/test_mimo.py +0 -0
  210. {commkit-2.0.0 → commkit-2.2.0}/tests/equalization/test_polarization.py +0 -0
  211. {commkit-2.0.0 → commkit-2.2.0}/tests/equalization/test_winit.py +0 -0
  212. {commkit-2.0.0 → commkit-2.2.0}/tests/impairments/channel/test_channel_linear.py +0 -0
  213. {commkit-2.0.0 → commkit-2.2.0}/tests/impairments/test_frontend.py +0 -0
  214. {commkit-2.0.0 → commkit-2.2.0}/tests/impairments/test_noise.py +0 -0
  215. {commkit-2.0.0 → commkit-2.2.0}/tests/mapping/test_bits.py +0 -0
  216. {commkit-2.0.0 → commkit-2.2.0}/tests/plotting/test_plot_analysis.py +0 -0
  217. {commkit-2.0.0 → commkit-2.2.0}/tests/plotting/test_plot_constellation.py +0 -0
  218. {commkit-2.0.0 → commkit-2.2.0}/tests/plotting/test_plot_equalizer.py +0 -0
  219. {commkit-2.0.0 → commkit-2.2.0}/tests/plotting/test_plot_eye.py +0 -0
  220. {commkit-2.0.0 → commkit-2.2.0}/tests/plotting/test_plot_filter_response.py +0 -0
  221. {commkit-2.0.0 → commkit-2.2.0}/tests/plotting/test_plot_spectral.py +0 -0
  222. {commkit-2.0.0 → commkit-2.2.0}/tests/plotting/test_plot_sync.py +0 -0
  223. {commkit-2.0.0 → commkit-2.2.0}/tests/plotting/test_plot_theme.py +0 -0
  224. {commkit-2.0.0 → commkit-2.2.0}/tests/plotting/test_plot_waveform.py +0 -0
  225. {commkit-2.0.0 → commkit-2.2.0}/tests/recovery/test_joint.py +0 -0
  226. {commkit-2.0.0 → commkit-2.2.0}/tests/recovery/test_pilots.py +0 -0
  227. {commkit-2.0.0 → commkit-2.2.0}/tests/recovery/test_pll.py +0 -0
  228. {commkit-2.0.0 → commkit-2.2.0}/tests/recovery/test_recovery_oracles.py +0 -0
  229. {commkit-2.0.0 → commkit-2.2.0}/tests/recovery/test_tikhonov.py +0 -0
  230. {commkit-2.0.0 → commkit-2.2.0}/tests/recovery/test_viterbi_viterbi.py +0 -0
  231. {commkit-2.0.0 → commkit-2.2.0}/tests/test_backend.py +0 -0
  232. {commkit-2.0.0 → commkit-2.2.0}/tests/test_examples.py +0 -0
  233. {commkit-2.0.0 → commkit-2.2.0}/tests/test_frequency.py +0 -0
  234. {commkit-2.0.0 → commkit-2.2.0}/tests/test_import_hygiene.py +0 -0
  235. {commkit-2.0.0 → commkit-2.2.0}/tests/test_io.py +0 -0
  236. {commkit-2.0.0 → commkit-2.2.0}/tests/test_logger.py +0 -0
  237. {commkit-2.0.0 → commkit-2.2.0}/tests/test_math.py +0 -0
  238. {commkit-2.0.0 → commkit-2.2.0}/tests/test_signal_pipeline.py +0 -0
  239. {commkit-2.0.0 → commkit-2.2.0}/tests/test_smoothing.py +0 -0
  240. {commkit-2.0.0 → commkit-2.2.0}/tests/test_spectral.py +0 -0
  241. {commkit-2.0.0 → commkit-2.2.0}/tests/test_timing.py +0 -0
@@ -110,7 +110,7 @@ jobs:
110
110
  - name: Run kernel tests with JIT disabled
111
111
  env:
112
112
  NUMBA_DISABLE_JIT: "1"
113
- run: uv run pytest tests/equalization tests/recovery tests/test_frequency.py --device=cpu --cov=commkit --cov-report=
113
+ run: uv run pytest tests/equalization tests/recovery tests/test_frequency.py tests/test_fiber.py --device=cpu --cov=commkit --cov-report=
114
114
 
115
115
  - name: Kernel coverage report
116
116
  run: >-
@@ -8,6 +8,11 @@ wheels/
8
8
  *.egg-info
9
9
  .ruff_cache
10
10
  .benchmarks
11
+ .coverage
12
+ .coverage.*
13
+ coverage.xml
14
+ coverage.json
15
+ htmlcov/
11
16
 
12
17
  # Virtual environments
13
18
  .venv
@@ -17,6 +22,14 @@ wheels/
17
22
 
18
23
  .vscode/
19
24
 
25
+ # Jupyter (uv run jupyter lab examples)
26
+ .ipynb_checkpoints/
27
+
28
+ # Nsight profiles (benchmarks/benchutils.nvtx_range)
29
+ *.nsys-rep
30
+ *.qdrep
31
+ *.ncu-rep
32
+
20
33
  tmp/
21
34
 
22
35
  # DSP captures / large binaries
@@ -0,0 +1,295 @@
1
+ # AGENTS.md
2
+
3
+ Guide for anyone, human or coding agent, changing the **CommKit** repository.
4
+
5
+ The rules below describe the code as it is. New code follows them; a change that breaks one changes this file in the same commit.
6
+
7
+ ---
8
+
9
+ ## 1. What CommKit is
10
+
11
+ A Python library for digital-communications research: simulation, receiver DSP, metrics, plotting and experimental captures, on NumPy (CPU) and CuPy (GPU) data. It is **not differentiable**. A separate JAX library, *commax*, will hold the blocks that must sit inside a gradient. commax depends on commkit, never the reverse, and takes its conventions (`Constellation`, bit labels, pulse taps, frame layouts) from commkit.
12
+
13
+ Users are researchers and engineers. Design for them:
14
+
15
+ - **Algorithms are plain functions.** Data is a `Signal` or an array. Things that *describe* a modulation, a pulse or an embedded sub-algorithm are small frozen value objects written inline in the call:
16
+
17
+ ```python
18
+ res = lms(rx, num_taps=21, step_size=1e-3, cpr=BPS(test_phases=64))
19
+ ```
20
+
21
+ - **Everything is expressed in Python.** No configuration files, registries, global switches or multi-step setup.
22
+ - **We deliberately do not build** pipeline or receiver objects, DSP methods on `Signal`, string shorthands for constellations, or public "config objects per algorithm".
23
+ - **Scope is the reference model and its inverse**, not a catalogue of research variants: the fiber channel and standard digital backpropagation (`correct_fiber_propagation`, with `steps_per_span` and `nonlinear_scale`), like chromatic dispersion and its compensation. Reduced-complexity nonlinearity compensation (filtered or enhanced split step, perturbation-based) is left to user code built on these, and learned DBP, which needs gradients, belongs in commax.
24
+
25
+ ---
26
+
27
+ ## 2. Commands
28
+
29
+ Always use `uv`. Python 3.12+.
30
+
31
+ ```bash
32
+ uv sync --all-extras # environment incl. GPU packages
33
+ uv run nbstripout --install # once per clone: strip notebook outputs on commit
34
+ uv run pytest # CPU + GPU tests (default --device=all)
35
+ uv run pytest --device=cpu # what CI runs
36
+ uv run pytest tests/equalization/ -k lms # subset
37
+ uv run pytest --cov=commkit # coverage
38
+ NUMBA_DISABLE_JIT=1 uv run pytest tests/equalization tests/recovery \
39
+ tests/test_frequency.py tests/test_fiber.py --device=cpu --cov=commkit # real kernel coverage
40
+ uv run pytest --device=cpu -p tests.plugins.precision_trace \
41
+ --ignore=tests/test_examples.py # precision audit (see Numerics); the suite passes under it
42
+ ```
43
+
44
+ **Commit gate.** CI fails on any of these, so run all of them before every commit, with `ruff format` last:
45
+
46
+ ```bash
47
+ uv run ruff check . --fix
48
+ uv run mypy commkit/
49
+ uv run pytest
50
+ uv run ruff format .
51
+ ```
52
+
53
+ **Benchmarks** are explicit-only; `uv run pytest` never collects them. A commit that changes a public signature also smoke-runs them once, without timing, so their call sites cannot rot unnoticed:
54
+
55
+ ```bash
56
+ uv run pytest benchmarks/ --benchmark-disable --device=all
57
+ ```
58
+
59
+ Timed runs:
60
+
61
+ ```bash
62
+ uv run pytest benchmarks/bench_bps.py --benchmark-only --device=all
63
+ uv run pytest benchmarks/ --benchmark-only --device=all \
64
+ --benchmark-compare=0003 --benchmark-storage=file://benchmarks/baselines
65
+ uv run python benchmarks/record_baseline.py NAME # record a new baseline
66
+ ```
67
+
68
+ **Release:**
69
+
70
+ 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.
71
+ 2. `uv run bump-my-version bump {patch|minor|major}`. This commits the new version and tags it `vX.Y.Z`.
72
+ 3. `git push origin main --tags`.
73
+ 4. Publish a GitHub Release for the tag (`gh release create vX.Y.Z --generate-notes`). Publishing the release runs `.github/workflows/publish.yml`, which builds and uploads to PyPI through Trusted Publishing; pushing a tag alone publishes nothing.
74
+
75
+ ---
76
+
77
+ ## 3. Architecture
78
+
79
+ Dependencies point downward only:
80
+
81
+ | Layer | Modules | May import |
82
+ | --- | --- | --- |
83
+ | Infrastructure | `backend`, `logger`, `_cuda` | NumPy, CuPy |
84
+ | Array and math helpers | `_array` (shape and validation), `math` (power, normalization, dB, linear trend) | infrastructure |
85
+ | Value objects | `mapping.Constellation`, pulse classes, `recovery` CPR configs | layers above |
86
+ | Core | `core` (`Signal`, frames, generation), `io` | layers above |
87
+ | DSP | `mapping`, `filtering`, `multirate`, `spectral`, `smoothing`, `impairments`, `timing`, `frequency`, `recovery`, `equalization`, `metrics`, `analysis` | layers above, and lower DSP modules |
88
+ | Presentation | `plotting` | everything; **nothing imports plotting** |
89
+
90
+ - **Responsibilities:**
91
+ - `filtering` holds signal-chain filters with a real frequency response.
92
+ - `smoothing` holds display and estimation smoothers only.
93
+ - `analysis` holds laser and phase characterization.
94
+ - `metrics` is the reporting layer.
95
+ - **Splitting modules:** promote a flat module to a package only when it exceeds about 1000 lines *and* has two or more separable concerns. The package `__init__` re-exports the public names. Never create a `utils/` package or one-function files.
96
+ - **Placeholder modules are kept on purpose.** `commkit.coding` is a reminder of planned work. Never delete it or treat it as dead code. It is excluded from coverage and from the contract registry, and the README labels it "planned, not implemented".
97
+ - **Shared physics lives in one private module**, used by the channel and by its compensation: `_dispersion` (β₂, β₃, the dispersion response) and `_fiber` (`Fiber` and the split-step solver behind `apply_fiber_propagation` and `correct_fiber_propagation`).
98
+
99
+ ---
100
+
101
+ ## 4. API rules
102
+
103
+ ### Signal
104
+
105
+ - `Signal` holds: samples (NumPy or CuPy, `(N,)` or `(C, N)`, time on the last axis), the **facts** `sampling_rate`, `symbol_rate` and `center_frequency`, the **description** `constellation` and `pulse`, and the ground truth `reference` (transmitted symbols and bits) plus the `frame` layout.
106
+ - It never holds pipeline results.
107
+ - Construction does no hidden work: no mapping, no normalization, no device move, no shape guessing. Factories (`generate`, frames) do that work.
108
+ - `sig.to("gpu")` and `sig.replace(...)` return new Signals. Arrays are shared, not copied; use `clone()` for an independent copy.
109
+ - **Alignment invariant:** reference symbol *k* corresponds to symbol period *k* of the samples. Any function that drops or shifts symbol periods slices the reference to match. Metrics raise on mismatch; they never guess.
110
+
111
+ ### Function signatures
112
+
113
+ - `f(data, other_data, *, params)`: everything after the data arguments is keyword-only. A function without data arguments may keep its single primary parameter positional (`barker_sequence(13)`, `gray_code(5)`); with several parameters, all are keyword-only (`rrc_taps(sps=2, rolloff=0.1, span=10)`). The data-argument count is recorded per function in the contract registry.
114
+ - **Signal awareness.** A function whose main argument is waveform data (or a field a Signal really carries) accepts an array or a Signal. It unwraps once with `adapt_signal()` (in `core/_signal_adapter.py`), works on arrays, and returns through `wrap_samples()`. Never re-enter the public function recursively.
115
+ - Functions on *derived* quantities (phase trajectories, correlations, taps, PSD bins, results) and synthesis primitives stay array-only.
116
+ - **Metadata rule: facts versus choices.**
117
+ - Facts (`sampling_rate`, `symbol_rate`, and so `sps`) come from the Signal. A conflicting argument raises `ValueError`; an equal one is fine.
118
+ - Choices (decision `constellation`, `noise_var`, `pulse`) default to the Signal's value, and an explicit argument wins.
119
+ - A missing fact for array input raises. Neither case warns.
120
+ - Integer-SPS algorithms validate with `require_integer_sps()` before any cast. Never truncate 1.5 SPS to 1.
121
+ - **Value objects are frozen dataclasses** with read-only NumPy arrays:
122
+ - `Constellation`: `.qam(M)`, `.psk(M)`, `.pam(M, unipolar=)`, or arbitrary `points`; `.shaped(nu=|entropy=)` keeps unit power. Functions accept the object only, never strings.
123
+ - Pulses: `RRC`, `RC`, `Gaussian`, `Rect`, `SmoothRect`. A raw taps array is accepted wherever a pulse is.
124
+ - Algorithm objects, one per estimation method (`BPS`, `PLL`, `ViterbiViterbi`, `MthPower`, `CycleSlip`, ...). The same object is used standalone (`correct_carrier_phase(y, BPS())`) and nested (`lms(..., cpr=BPS())`). It lives next to its kernel and validates its parameters on construction. Class names are unique across the package.
125
+ - Equalizer continuation uses `state=result.state`.
126
+ - **Randomness:** `rng: int | np.random.Generator | None` (SciPy SPEC 7).
127
+ - What is transmitted (bits, symbols, pilots): draw on the host with the Generator, then transfer. A seed gives the same payload on CPU and GPU.
128
+ - What the channel adds (AWGN, phase noise, ASE): draw on the data's device through `_random.standard_normal` / `complex_normal`. Realizations differ between devices; statistics agree, and tests assert statistics.
129
+ - Random parameters of a realization (the PMD waveplates of `Fiber`) are few and drawn on the host, so a seed gives the same fiber on every device.
130
+ - Never use a global RNG.
131
+ - **Types:** a transform is written `def f(samples: S, ...) -> S` with the TypeVar `S` from `core/_signal_adapter.py` (bound to `np.ndarray | Signal`), so a Signal gives a Signal and an array an array. Do not reassign the `samples` parameter; name the unwrapped array `x`.
132
+ - **Device follows the data.** There are no `backend=` arguments and no global switches. Unsupported array types (JAX, PyTorch) raise `TypeError`.
133
+ - Only factories with no input data to follow (`generate`, `generate_phase_noise`, `Preamble`/`SingleCarrierFrame.to_signal`, `load_npz`) take `device: str = "cpu"`, validated with `require_device()`. They draw randomness as the rule above says and do the vectorized work (mapping, shaping) on `device`. The default is never "GPU if available".
134
+ - **No `debug_plot`.** Numerical code never imports plotting. Plot functions consume results or recompute through public compute functions.
135
+ - **Design and apply are separate functions**, for example `rrc_taps` and `fir_filter`, or `butterworth_sos` and `iir_filter`.
136
+
137
+ ### Returns
138
+
139
+ - **Transforms:** array in gives array out on the same device; Signal in gives a new Signal out (update `sampling_rate` when the rate changes).
140
+ - **Estimators** reduce over the time axis by input rank: `(N,)` gives 0-d and `(C, N)` gives `(C,)`. Results stay on the input device.
141
+ - **Analysis summaries** (`estimate_linewidth`, `allan_deviation`, the `frequency_drift` statistics) are reporting-layer fits on plot-sized reductions. Like the metrics, they return host floats or `(C,)` arrays. Sample-rate outputs (trajectories, PSDs, `dsh_phase`) stay on the input device.
142
+ - **Metrics** (`evm`, `snr`, `ber`, `ser`, `gmi`, `mi`) return host values: a `float` for 1-D input and `np.ndarray (C,)` for 2-D input. They **raise** on empty selections; they never return `None` or `0.0` for "nothing measured". Metrics never silently align, rotate or permute against the reference.
143
+ - **More than one value** is returned as a frozen dataclass with named fields. No dicts, and no tuples longer than two.
144
+ - **Equalizers** return `EqualizerResult`. `y_hat` is always an array, `signal` holds the 1-SPS Signal for Signal input, `state` is the continuation state, and diagnostics are returned as data.
145
+
146
+ ### Naming
147
+
148
+ | Prefix | Meaning |
149
+ | --- | --- |
150
+ | `apply_*` | add an impairment or apply a model |
151
+ | `estimate_<quantity>(x, method)` | measure, never change the data; returns a frozen `<Quantity>Estimate` (value plus diagnostics) |
152
+ | `correct_<quantity>(x, how)` | remove the impairment; returns only the corrected data. `how` is an estimate or an algorithm object (which estimates first) |
153
+ | `resolve_*` | pick among discrete candidates (π/2 rotation, channel permutation); nothing else |
154
+ | `generate` | synthesis from parameters, takes `rng` |
155
+ | `plot_*` | every public plotting function (only `apply_default_theme` is exempt) |
156
+
157
+ - The algorithm is chosen by an object, never by the function name or a string: `estimate_carrier_phase(y, BPS())`, not `recover_carrier_phase_bps(y)`. The verb function dispatches on the object's type through a private table in its module. There is no `recover_*` or `compensate_*`.
158
+ - Computations use plain nouns (`allan_deviation`).
159
+ - **Glossary**, one name each:
160
+ - `num_taps`, `step_size`, `sps`, `constellation`, `pulse`, `rng`, `num_symbols`, `num_channels`, `sampling_rate`, `symbol_rate`.
161
+ - Units are in the docstring. Use `_db` and `_hz` suffixes where the unit is ambiguous.
162
+
163
+ ### Errors and logging
164
+
165
+ - Wrong input raises, with a message naming the argument.
166
+ - `logger` is for diagnostics only. Per-channel logging that needs a device-to-host transfer is wrapped in `if logger.isEnabledFor(logging.INFO):`.
167
+ - The library never configures handlers, Matplotlib or warning filters at import time.
168
+
169
+ ---
170
+
171
+ ## 5. Numerics
172
+
173
+ - **Storage dtypes:** `complex64` for IQ samples and `float32` for real signals; `generate`, frames and `load_npz` produce them, so the GPU's single-precision path is the default.
174
+ - **Precision contract.** Every function computes in a predictable precision, and none promotes single-precision data on its own:
175
+ - **Transforms keep the input's precision**: complex64 in, complex64 out and computed throughout (filters, impairments, the fiber solver and DBP, resampling, delays, carrier-phase correction); complex128 in, complex128 out.
176
+ - **Fixed-precision blocks** compute in single precision whatever the input: the equalizers in complex64 (with complex128 accumulation inside, below), `compute_llr` / `Constellation.llr` in float32. Double input to them emits `commkit.PrecisionWarning` (`_array.warn_if_double`, attributed to the caller's line; Python shows it once per call site). Users who want strictness turn it into an error with `warnings.simplefilter("error", commkit.PrecisionWarning)`; the test suite does (`pytest_configure` in `tests/conftest.py`; never in pyproject's `filterwarnings`, which imports commkit before coverage starts and drops every module-level line from the report), so a library path that feeds double data to these blocks fails CI. Training and reference symbols are ideal values and are not checked.
177
+ - **Deliberate float64**, documented where it happens, always on phases, small arrays or accumulators, never on sample-rate data that is then cast back: phases before `unwrap`, phase trajectories and phase ramps (wrapped in float64, rotated in the data's precision), frequency grids and estimates, the recursions' state (PLL, Tikhonov, RLS `P`), the IIR filter's sections (poles near z = 1), and sums: the metrics compute per element in the input's precision and accumulate in float64 (`gmi` / `mi` in single precision moved by under 4e-8 bit, far below the Monte Carlo error).
178
+ - **Audit with `tests/plugins/precision_trace.py`** after touching numerics: it lists every float64/complex128 array of sample-rate size that a NumPy path of the library creates from single-precision data or from scratch, with its source line. Every reported site must be a deliberate use from the list above (or a test passing complex128 on purpose). Output dtypes alone do not show this: a function that computes in complex128 and casts back returns complex64.
179
+ - complex128 pays only where single-precision rounding accumulates over many FFT passes, as in the fiber solver (6e-4 after 10^4 steps in complex64, 1e-12 in complex128); there the GPU runs its plain CuPy stages, not the fused complex64 kernels.
180
+ - **The NEP 50 trap**: under NumPy 2 a NumPy scalar is not weak, so `np.float64(a) * x64` (any `np.sqrt`, `np.pi * ...` result, or a float64 array such as `xp.fft.fftfreq`) promotes complex64 data to complex128. Combine single-precision data only with Python scalars or with arrays cast to its precision; rotate with `math._derotate` (float64 phase wrap, single-precision phasor, one fused kernel on the GPU).
181
+ - **Accumulation precision:**
182
+ - LMS/CMA/RDE: inputs and weights are `complex64`, but dot products and gradient updates in the hot loop are accumulated in `complex128`.
183
+ - RLS keeps `P`, the gain `k` and the regressor buffers in `complex128` throughout. Single precision loses the Hermitian positive-definite property and the filter diverges.
184
+ - Carrier phase: promote angles to `float64` before `xp.unwrap()`, because float32 rounding causes spurious quadrant slips. Tikhonov/Kalman block transitions also run in `float64`.
185
+ - **Normalization invariant:** at `sps` samples per symbol, `E[|x|²] = 1/sps`; at symbol rate, `E[|x|²] = 1`. Any rate-changing block applies the exact gain correction.
186
+ - **Shapes:** SISO is `(N,)` and MIMO is `(C, N)`. Use `as_2d`, `restore_1d`, `broadcast_channels` and `require_channels`; never hand-roll the promote/squeeze idiom.
187
+ - **Reproducibility:** a seed is reproducible within a library version only. Tests and baselines assert statistics, never exact noise realizations.
188
+
189
+ ---
190
+
191
+ ## 6. Performance
192
+
193
+ | Algorithm structure | Implementation | Device |
194
+ | --- | --- | --- |
195
+ | Vectorized: filters, resampling, spectra, impairments, metrics, LLR, frequency-domain equalizers | NumPy/CuPy via `x, xp, sp = dispatch(samples)` | follows the input |
196
+ | Sequential recursions: LMS/RLS/CMA/RDE, PLL, Tikhonov, cycle slips | Numba `@njit(cache=True, fastmath=True, nogil=True)` | CPU; GPU input does one round trip to the host and back |
197
+ | Hot spots limited by intermediates, launches or round trips | CUDA C++ via CuPy `RawModule` (`commkit/_cuda`) | GPU |
198
+ | Elementwise chains with many temporaries, where a paired A/B shows the gain end to end (the PMD stage of `_fiber`: 0.61x on 4 cores, but not on the reference machine; see the roadmap) | Numba `@njit(parallel=True)` in one pass, real arithmetic (a `1j` literal promotes complex64 to complex128) | CPU |
199
+
200
+ **Host-sync hygiene:**
201
+
202
+ - Never extract scalars from a possibly-GPU array inside a loop (`float(x[c])`, `.item()`). Compute the vector on the device, transfer it once with `to_device(v, "cpu")`, then loop on the host.
203
+ - Prefer on-device gathers (`xp.take_along_axis`).
204
+ - Every FFT goes through `commkit._fft` (`fft`, `ifft`, `rfft`, `irfft`), never `np.fft` or `xp.fft` directly: on the CPU it runs `scipy.fft` on every core and keeps complex64 in single precision (NumPy 2 computes it in double, about 3x slower per core); on the GPU it is `cupy.fft`. Frequency grids (`xp.fft.fftfreq`) are exempt.
205
+ - Frequency responses (dispersion, delays, Jones matrices) are computed at float64 phase accuracy and cast to the spectrum's precision before the multiply, so complex64 data is never promoted.
206
+ - Chunk `(N, M)` intermediates over N with an on-device accumulator.
207
+ - Reduce on the device before transferring, including for plotting.
208
+ - Pass whole records to compiled code. There are no Python loops over symbols or small blocks, except inside a CUDA graph.
209
+ - Repeated GPU work with fixed buffers (the block equalizers' blocks, the fiber solver's spans) is replayed from CUDA graphs through `_cuda.graphs.GraphRunner`, the one driver: warm once, capture on the second run, replay, fall back to eager on a failed capture. Its stream is reused per device and thread, because CuPy's pool caches freed blocks per stream and a fresh stream per call grows the pool every call.
210
+
211
+ **Kernel policy:**
212
+
213
+ - A `.cu` kernel needs a benchmark showing that CuPy is limited by intermediates, launch count or round trips.
214
+ - It optimizes a CPU reference (Numba or NumPy) and is tested against it with `--device=all`.
215
+ - Wrappers check dtype, contiguity, shape and size before launch.
216
+ - `float32` with explicit literals.
217
+ - `--use_fast_math` only per kernel (through `options`), after an accuracy check; the compiler default does not pass it.
218
+ - Short elementwise chains use `ElementwiseKernel` or `cupy.fuse`.
219
+ - Sequential recursions over a whole record are never ported to the GPU; one record has no parallel work per step. A short per-block scan may be, as one launch, when it replaces a host round trip per block (`cs_block`, `bps_anchor`); keep its parallel part parallel (float64 is 1/64 of float32 throughput on consumer GPUs).
220
+
221
+ ---
222
+
223
+ ## 7. Tests
224
+
225
+ - **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(...)`.
226
+ - **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.
227
+ - **Shared builders** live in `tests/common/` and use the public API.
228
+ - **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.
229
+ - Do not duplicate these checks in per-module tests.
230
+ - **Oracles:** sequential kernels are tested against plain-Python reference implementations in `tests/common/reference_impl.py`. CUDA kernels are tested against their CPU reference.
231
+ - **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.
232
+ - **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.
233
+ - **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.
234
+ - 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 neighbours' links.
235
+ - Every claim in the text matches what the cells print: run the notebook and read its numbers before committing it. Keep each under about 20 s on the CPU.
236
+ - Plots of hand-computed arrays go through `to_device(x, "cpu")`, so the notebooks run unchanged on the GPU.
237
+ - **Coverage** must not drop below the CI floor. Numba kernel coverage is measured by a separate `NUMBA_DISABLE_JIT=1` job.
238
+
239
+ ---
240
+
241
+ ## 8. Benchmarks
242
+
243
+ `benchmarks/` tracks the GPU-relevant hot paths. Baselines are committed under `benchmarks/baselines/`. The current reference is `0004` (`fiber_dbp`) on the reference machine: RTX 4070 Ti, Ryzen 7 7800X3D, WSL2. It adds the fiber and DBP benchmarks to `0003` (`v2_0`, the 2.0 state); `0002` (`pre_v2`) is the 1.x state. `0002` ran the CPR equalizer and Viterbi-Viterbi benchmarks on workloads that did not converge, so compare those only from `0003` on.
244
+
245
+ - **Workloads must converge.** A workload the algorithm cannot handle times a failure mode (slip storms, divergence), not the operating point. Every equalizer benchmark asserts its symbol error rate with `benchutils.assert_converged`.
246
+ - **Baselines** are recorded with `benchmarks/record_baseline.py`: each file in its own process, best of three passes. In one full-suite process, small GPU benchmarks after the large equalizer workloads ran 2-12x slower than alone, so never record a baseline from a single `pytest benchmarks/` run.
247
+
248
+ - **IDs** are `[cpu]` or `[gpu]`, meaning the input device.
249
+ - **Timing:**
250
+ - Timed bodies end with the `sync` fixture; otherwise GPU timings measure launches, not execution.
251
+ - Each benchmark has one warmup round, so compilation and pool growth are excluded.
252
+ - **Workloads** come from `benchmarks/workloads.py` with fixed seeds; never generate data inline.
253
+ - **Tools:** `benchmarks/benchutils.py` provides `CudaEventTimer` and `nvtx_range` for `nsys` profiling.
254
+ - **Logging** is set to WARNING in `benchmarks/conftest.py`.
255
+ - **Trust deltas, not single runs.** A ±20-40% swing has been seen under load. Re-run a suspected regression in isolation, or do a controlled A/B with at least 7 repetitions, alternating old and new per repetition and comparing the paired ratios. When even unchanged benchmarks move, time the changed part itself with `CudaEventTimer` and quote it against the step it sits in.
256
+ - A commit touching a hot path quotes its delta against `0004`. Tolerance is 5% on CPU and 10% on GPU.
257
+
258
+ ---
259
+
260
+ ## 9. Roadmap and open items
261
+
262
+ What is not done yet, in suggested order. Unlike the sections above, this is a to-do list, not a description of the code: remove an item in the commit that resolves it, and add one when a known gap is left open.
263
+
264
+ ### Planned modules
265
+
266
+ 1. **Channel coding** (`coding`, a placeholder package). Resolve the "Before channel coding" items below first, so the decoder is built on soft demapping that is already at its final speed on both devices. Start with one soft decoder (LDPC) that consumes `compute_llr` output (positive LLR = bit 0) and reports post-FEC BER next to `gmi`, then add encoders. The package stays out of `commkit/__init__.py` until a real encode/decode entry point exists. Decoders are iterative message passing over many codewords: batch codewords on the GPU from the start.
267
+
268
+ ### Nonlinear fiber channel: future features
269
+
270
+ Each is an addition that leaves the current API unchanged.
271
+
272
+ - **Batched realizations**: an extra leading axis `(B, 2, N)` (launch-power sweeps, Monte Carlo over seeds) so cuFFT batches them and short records fill the GPU.
273
+ - **Adaptive step rules**: a `step_rule=` object (maximum nonlinear phase per step, local-error method) next to the equal `steps_per_span`, which measured 3x more accurate than logarithmic steps on a 32 GBd, 80 km span.
274
+ - **WDM helpers**: combining frequency-shifted channels into one wideband field and selecting one after propagation; the solver already handles the wide field (with `dispersion_slope_ps_nm2_km` set).
275
+ - **Distributed (Raman) amplification and per-span fiber types** in one call (chaining calls already works: every call speaks launch-relative units).
276
+ - **cuFFT callbacks** fusing the linear stage into the transforms (CuPy's support is experimental and compiles with nvcc at run time).
277
+ - **Self-steepening and Raman scattering** for ultra-wideband fields.
278
+
279
+ ### Before channel coding: mapping and soft demapping
280
+
281
+ The decoder's input path. The GPU items need a CUDA device to write, test (`--device=all`, against the Numba reference) and time.
282
+
283
+ - **GPU LLRs of separable QAM.** `Constellation.llr` takes the per-axis path (2·sqrt(M) metrics instead of M) only on the CPU; on the GPU, 256-QAM evaluates 256 metrics per symbol instead of 32. Port `llr_separable` to a CUDA kernel.
284
+ - **One exponential per point in the CUDA exact LLR.** `llr.cu` evaluates every metric twice and calls `expf` once per point and bit (M·k per symbol, 2048 for 256-QAM). The Numba kernel scales by one peak per symbol and needs M (with the underflow fallback of `_EXACT_FLOOR`). Together with the item above: 32 exponentials per 256-QAM symbol instead of 2048.
285
+ - **CUDA LLR stores and per-call tables.** Each thread writes its k LLRs at stride k (uncoalesced), and every call rebuilds and copies the points, packed labels and log prior to the device; cache them per constellation and device.
286
+ - **Non-square hard decisions on the GPU.** `gray._nearest_index` (used by `Constellation.demap`, `evm(blind=True)` and `ser` for PSK, cross QAM and arbitrary points) loops in Python over 4096-symbol chunks: about 250 iterations of several launches per 10^6 symbols. On the CPU that chunk and `abs` are the fastest NumPy form (measured); on the GPU, time it against a larger element-budget chunk or the nearest-point mode of `bps_min_d2`.
287
+ - **`Constellation.map` intermediates.** It casts the bits to int32 groups and multiplies and sums them: two `(N, k)` int32 temporaries for `N·k` input bits, four times the bits' own size in int8. Encoders hand it long bit records; pack the labels in one pass.
288
+ - **Closed-form max-log LLRs of Gray QAM** (optional, CPU and GPU): per bit, the nearest point of each half is found by snapping, O(k) instead of O(sqrt(M)). Expected gain under 2x on the CPU; measure before adopting.
289
+
290
+ ### Performance
291
+
292
+ - **Block equalizers at small `block_size` on the GPU**: a fixed cost per block; at 256 the GPU only ties the CPU without carrier recovery.
293
+ - **The fiber solver's Numba passes lose end to end on the reference machine.** On 4 cores (where they were tuned) the hybrid Kerr stage won from `_fiber._HYBRID_KERR_MIN = 2^18` and the trig-free Jones (PMD) pass won end to end (0.61x). On the reference machine (8 cores, 16 threads), whole propagations with NumPy stages and multi-core `scipy.fft` are faster: hybrid Kerr 1.74x slower at 2^18, 1.32x at 2^19, 1.12x at 2^20; Numba Jones 1.35x at 2^14, 2.19x at 2^16, 1.54x at 2^18, even at 2^20. The cause is contention: Numba's OpenMP workers spin after each pass and take the cores from the FFT workers (`OMP_WAIT_POLICY=passive` cuts the Kerr loss at 2^18 to 1.09x; one Numba thread makes Jones even with NumPy). The stage timed alone hides this. Options: choose the passes by core count, run them single-threaded, or keep NumPy stages only; decide by end-to-end A/Bs on both machines.
294
+ - **A fully fused Numba Kerr stage** may beat the hybrid one where Numba has SVML (vector sin/cos); neither machine measured so far has it (`numba.core.config.USING_SVML` is False), so it is untimed.
295
+ - **`cs_block` on phases with frequent slip decisions**: one speculation pass per changed decision (about 1 pass per block on a realistic link, more on pathological input). Outside a CUDA graph the Python wrapper (validation, scratch allocation) costs about 18 us per launch.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: commkit
3
- Version: 2.0.0
3
+ Version: 2.2.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
@@ -43,50 +43,20 @@ Description-Content-Type: text/markdown
43
43
  ![License](https://img.shields.io/badge/license-MIT-green)
44
44
  ![CUDA](https://img.shields.io/badge/CUDA-13.x-76B900?logo=nvidia)
45
45
 
46
- CommKit covers the receiver chain of coherent and IM/DD links (waveform
47
- generation, channel impairments, synchronization, carrier recovery, adaptive
48
- equalization, metrics, laser characterization and plotting) on NumPy or CuPy
49
- arrays. Computation runs on the device the data lives on.
46
+ CommKit covers the receiver chain of coherent and IM/DD links (waveform generation, channel impairments, synchronization, carrier recovery, adaptive equalization, metrics, laser characterization and plotting) on NumPy or CuPy arrays. Computation runs on the device the data lives on.
50
47
 
51
48
  ---
52
49
 
53
50
  ## The model
54
51
 
55
- ```text
56
- plain arrays (NumPy / CuPy) value objects (frozen, written inline)
57
- \ Constellation, RRC / RC / Gaussian / Rect,
58
- \ BPS / PLL / CycleSlip, MthPower, ...
59
- v |
60
- Signal = samples + facts + description + reference
61
- |
62
- v
63
- functions: generate, apply_*, estimate_*, correct_*, resolve_*, ...
64
- | \
65
- v v
66
- Signal or array typed results (EqualizerResult, *Estimate, ...)
67
- |
68
- v
69
- plotting (draws results, never computes them)
70
- ```
52
+ Data is a NumPy or CuPy array, or a `Signal` that wraps the samples together with what is known about them. Algorithms are plain functions that take the data and return a new `Signal` or array, or a typed result such as `EqualizerResult` or an `*Estimate`. Plotting functions draw those results; they never compute them.
71
53
 
72
- - **Algorithms are plain functions.** Data is a `Signal` or an array.
73
- - **Things that describe** a modulation, a pulse or a sub-algorithm are small
74
- frozen objects written in the call: `lms(rx, cpr=BPS(test_phases=64))`.
75
- The same object works standalone: `correct_carrier_phase(y, BPS())`.
76
- - **A `Signal` carries facts and ground truth.** Facts are `sampling_rate`,
77
- `symbol_rate` and `center_frequency`. The description is `constellation`
78
- and `pulse`. The ground truth is `reference` (transmitted symbols and
79
- bits). Functions take facts from the Signal and raise on a conflicting
80
- argument. Choices such as the decision constellation default to the
81
- Signal's and can be overridden.
82
- - **Results with several values are frozen dataclasses** with named fields.
83
- Metrics return host floats: one per channel for `(C, N)` input.
84
- - **The device follows the data.** Processing functions have no `backend=`
85
- or `device=` argument. Only factories, which have no input data to
86
- follow, take `device=`: `generate(..., device="gpu")`. Move existing data
87
- with `sig.to("gpu")`.
88
- - There is no pipeline object, receiver class or configuration file: the
89
- orchestration stays in your script.
54
+ - **Things that describe** a modulation, a pulse or a sub-algorithm are small frozen objects written in the call: `lms(rx, cpr=BPS(test_phases=64))`. The same object works standalone: `correct_carrier_phase(y, BPS())`.
55
+ - **A `Signal` carries facts and ground truth.** Facts are `sampling_rate`, `symbol_rate` and `center_frequency`. The description is `constellation` and `pulse`. The ground truth is `reference` (transmitted symbols and bits). Functions take facts from the Signal and raise on a conflicting argument. Choices such as the decision constellation default to the Signal's and can be overridden.
56
+ - **Results with several values are frozen dataclasses** with named fields. Metrics return host floats: one per channel for `(C, N)` input.
57
+ - **The device follows the data.** Processing functions have no `backend=` or `device=` argument. Only factories, which have no input data to follow, take `device=`: `generate(..., device="gpu")`. Move existing data with `sig.to("gpu")`.
58
+ - **Plots draw results.** `plot_*` functions take a Signal, an array or a result object; numerical code never imports plotting.
59
+ - There is no pipeline object, receiver class or configuration file: the orchestration stays in your script.
90
60
 
91
61
  ## Quickstart
92
62
 
@@ -109,30 +79,20 @@ res = ck.equalization.lms(
109
79
  y = res.signal # 1-SPS Signal, reference aligned
110
80
  print(f"EVM {ck.metrics.evm(y, num_skip_symbols=2000):.1f} %, "
111
81
  f"BER {ck.metrics.ber(y, num_skip_symbols=2000):.1e}")
112
- # EVM 12.8 %, BER 2.5e-04
82
+ # EVM 12.7 %, BER 2.4e-04 (on the CPU; the GPU draws other noise)
113
83
 
114
84
  # The next record continues from the converged taps and CPR state.
115
85
  res2 = ck.equalization.lms(rx, num_taps=21, step_size=1e-3, cpr=cpr, state=res.state)
116
86
  ```
117
87
 
118
- Without CuPy, pass `device="cpu"` (the default): the same code runs on the
119
- CPU.
88
+ Without CuPy, pass `device="cpu"` (the default): the same code runs on the CPU.
120
89
 
121
90
  ## CPU and GPU
122
91
 
123
- CommKit never picks a device for you: data stays where you put it, and each
124
- function runs on the device of its input. A few habits get the speed out of
125
- a GPU:
126
-
127
- - **Put data on the GPU once, at the source.** Build it there with
128
- `generate(..., device="gpu")`, `frame.to_signal(..., device="gpu")` or
129
- `load_npz(path, device="gpu")`; move a capture with `sig.to("gpu")`. Do not
130
- move data back and forth between stages. Building on the GPU is faster
131
- than `generate(...).to("gpu")`: about 26 ms instead of 370 ms for 4M
132
- symbols, because mapping and pulse shaping run there.
133
- - **You do not need to move results back.** Metrics (`evm`, `ber`, ...) and
134
- analysis summaries return host floats, and plots reduce on the device and
135
- transfer only what they draw.
92
+ CommKit never picks a device for you: data stays where you put it, and each function runs on the device of its input. A few habits get the speed out of a GPU:
93
+
94
+ - **Put data on the GPU once, at the source.** Build it there with `generate(..., device="gpu")`, `frame.to_signal(..., device="gpu")` or `load_npz(path, device="gpu")`; move a capture with `sig.to("gpu")`. Do not move data back and forth between stages. Building on the GPU is faster than `generate(...).to("gpu")`: about 26 ms instead of 370 ms for 4M symbols, because mapping and pulse shaping run there.
95
+ - **You do not need to move results back.** Metrics (`evm`, `ber`, ...) and analysis summaries return host floats, and plots reduce on the device and transfer only what they draw.
136
96
  - **Know what runs where.**
137
97
 
138
98
  | Work | GPU input |
@@ -141,25 +101,12 @@ a GPU:
141
101
  | Frequency-domain equalizers `block_lms`, `block_cma`, `block_rde` | runs on the GPU (CUDA graphs) |
142
102
  | Sequential equalizers `lms`, `rls`, `cma`, `rde`; `PLL`, `Tikhonov`, cycle-slip correction | runs on the CPU (Numba): one copy to the host and back, no speedup |
143
103
 
144
- A sample-by-sample recursion has no parallel work per step. For long
145
- records on the GPU, prefer the block equalizers with `block_size=1024` or
146
- more. Each block costs a fixed overhead: at 256 the GPU only ties the CPU
147
- without carrier recovery, at 2048 it is 6-9x faster. A larger block also
148
- lowers the stable `step_size`.
149
- - **Short records do not pay off.** Launch overhead dominates below roughly
150
- 10⁴-10⁵ samples, where the CPU is as fast.
151
- - **Batch channels and records.** Pass `(C, N)` arrays rather than looping
152
- over channels in Python: one call does the work of C.
153
- - **Keep scalars on the device inside loops.** `float(x)`, `x.item()` and
154
- `if x > 0:` on a CuPy array wait for the GPU and copy. Collect values in an
155
- array and transfer once.
156
- - **The first call compiles.** Numba and CUDA kernels compile on first use
157
- and are cached on disk. Warm up once before timing anything.
158
- - **Seeds and devices.** The same `rng` gives the same bits and symbols on
159
- both devices. Channel noise (AWGN, phase noise) is drawn on the device, so
160
- its realization differs between CPU and GPU while its statistics match.
161
- For the exact same phase-noise trajectory on both, draw it on the CPU with
162
- `generate_phase_noise(...)` and move it.
104
+ A sample-by-sample recursion has no parallel work per step. For long records on the GPU, prefer the block equalizers with `block_size=1024` or more. Each block costs a fixed overhead: at 256 the GPU only ties the CPU without carrier recovery, at 2048 it is 6-9x faster. A larger block also lowers the stable `step_size`.
105
+ - **Short records do not pay off.** Launch overhead dominates below roughly 10⁴-10⁵ samples, where the CPU is as fast.
106
+ - **Batch channels and records.** Pass `(C, N)` arrays rather than looping over channels in Python: one call does the work of C.
107
+ - **Keep scalars on the device inside loops.** `float(x)`, `x.item()` and `if x > 0:` on a CuPy array wait for the GPU and copy. Collect values in an array and transfer once.
108
+ - **The first call compiles.** Numba and CUDA kernels compile on first use and are cached on disk. Warm up once before timing anything.
109
+ - **Seeds and devices.** The same `rng` gives the same bits and symbols on both devices. Channel noise (AWGN, phase noise) is drawn on the device, so its realization differs between CPU and GPU while its statistics match. For the exact same phase-noise trajectory on both, draw it on the CPU with `generate_phase_noise(...)` and move it.
163
110
 
164
111
  ---
165
112
 
@@ -169,10 +116,10 @@ a GPU:
169
116
  | --- | --- |
170
117
  | [`commkit.core`](https://github.com/lokgar/commkit/tree/main/commkit/core) | `Signal`, `Reference`, `generate`, `SingleCarrierFrame` and `Preamble` (pilots, guard intervals, MIMO streams), `extract_payload`. |
171
118
  | [`commkit.mapping`](https://github.com/lokgar/commkit/tree/main/commkit/mapping) | `Constellation` (QAM, PSK, PAM, arbitrary points; Gray labels; probabilistic shaping with `.shaped()`), bit mapping, hard demapping, max-log and exact LLRs. |
172
- | [`commkit.filtering`](https://github.com/lokgar/commkit/blob/main/commkit/filtering.py) | Pulses (`RRC`, `RC`, `Gaussian`, `Rect`, `SmoothRect`), FIR and IIR design (Butterworth, Chebyshev I/II, elliptic, Bessel), `fir_filter`, `iir_filter`, `matched_filter`, overlap-save, chromatic-dispersion compensation. |
119
+ | [`commkit.filtering`](https://github.com/lokgar/commkit/blob/main/commkit/filtering.py) | Pulses (`RRC`, `RC`, `Gaussian`, `Rect`, `SmoothRect`), FIR and IIR design (Butterworth, Chebyshev I/II, elliptic, Bessel), `fir_filter`, `iir_filter`, `matched_filter`, overlap-save, chromatic-dispersion compensation and digital backpropagation (`correct_fiber_propagation`). |
173
120
  | [`commkit.multirate`](https://github.com/lokgar/commkit/blob/main/commkit/multirate.py) | `resample` (fractional), `decimate`, `upsample`, `decimate_to_symbol_rate`. |
174
121
  | [`commkit.spectral`](https://github.com/lokgar/commkit/blob/main/commkit/spectral.py) | Welch PSD, spectrograms, frequency shifting, pilot tones. |
175
- | [`commkit.impairments`](https://github.com/lokgar/commkit/tree/main/commkit/impairments) | AWGN, phase noise, IQ imbalance (with Löwdin and Gram-Schmidt correction), chromatic dispersion, PMD and polarization mixing. Nonlinear channel models: **planned, not implemented**. |
122
+ | [`commkit.impairments`](https://github.com/lokgar/commkit/tree/main/commkit/impairments) | AWGN, phase noise, IQ imbalance (with 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`. |
176
123
  | [`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. |
177
124
  | [`commkit.frequency`](https://github.com/lokgar/commkit/blob/main/commkit/frequency.py) | `estimate_frequency_offset` / `correct_frequency_offset` with `MthPower`, `MengaliMorelli`, `PilotSymbols` and `BiasTone`; static and blockwise. |
178
125
  | [`commkit.recovery`](https://github.com/lokgar/commkit/tree/main/commkit/recovery) | `estimate_carrier_phase` / `correct_carrier_phase` with `ViterbiViterbi`, `BPS`, `PLL`, `Tikhonov`, `DataAided`, `PilotAided`, `PilotTone(s)`; cycle-slip correction; `resolve_phase_ambiguity` and `resolve_channel_permutation`. |
@@ -185,8 +132,7 @@ a GPU:
185
132
  | [`commkit.plotting`](https://github.com/lokgar/commkit/tree/main/commkit/plotting) | Constellations, eye diagrams, spectra, filter responses, equalizer convergence, and synchronization and laser diagnostics that draw the estimates. Imported on first use. |
186
133
  | [`commkit.coding`](https://github.com/lokgar/commkit/tree/main/commkit/coding) | Channel coding and FEC: **planned, not implemented**. |
187
134
 
188
- Importing `commkit` has no side effects: it configures no logging, Matplotlib
189
- or warning filters, and does not touch the GPU.
135
+ Importing `commkit` has no side effects: it configures no logging, Matplotlib or warning filters, and does not touch the GPU.
190
136
 
191
137
  ---
192
138
 
@@ -201,37 +147,28 @@ pip install "commkit[notebook]" # to run the example notebooks
201
147
  pip install "commkit[full]" # everything
202
148
  ```
203
149
 
204
- With [`uv`](https://github.com/astral-sh/uv), use `uv pip install` in place of
205
- `pip install`. Extras combine, e.g. `commkit[gpu,notebook]`.
150
+ With [`uv`](https://github.com/astral-sh/uv), use `uv pip install` in place of `pip install`. Extras combine, e.g. `commkit[gpu,notebook]`.
206
151
 
207
152
  > [!NOTE]
208
- > **WSL2 and CUDA.** With NVIDIA drivers and CUDA installed on Windows, there
209
- > is no need to install CUDA inside WSL2. To let the Python CUDA packages find
210
- > the bundled NVIDIA libraries, add to `~/.bashrc`:
153
+ > **WSL2 and CUDA.** With NVIDIA drivers and CUDA installed on Windows, there is no need to install CUDA inside WSL2. To let the Python CUDA packages find the bundled NVIDIA libraries, add to `~/.bashrc`:
211
154
  >
212
155
  > ```bash
213
156
  > export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$(echo $HOME/commkit/.venv/lib/python3.*/site-packages/nvidia/cu13/lib)
214
157
  > ```
215
158
  >
216
- > *(This assumes the repository is cloned to `$HOME/commkit`; adjust the path
217
- > otherwise.)*
159
+ > *(This assumes the repository is cloned to `$HOME/commkit`; adjust the path otherwise.)*
218
160
 
219
161
  ## Examples
220
162
 
221
- Jupyter notebooks in [`examples/`](https://github.com/lokgar/commkit/tree/main/examples) (install the `notebook` extra
222
- and run `jupyter lab examples`):
163
+ [`examples/`](https://github.com/lokgar/commkit/tree/main/examples) is a course in 26 Jupyter notebooks, one topic each, in teaching order (install the `notebook` extra and run `jupyter lab examples`):
223
164
 
224
- - [`qam_receiver_quickstart`](https://github.com/lokgar/commkit/blob/main/examples/qam_receiver_quickstart.ipynb) - the
225
- quickstart above, cell by cell, with the constellation, spectrum and
226
- equalizer plots;
227
- - [`carrier_phase_analysis`](https://github.com/lokgar/commkit/blob/main/examples/carrier_phase_analysis.ipynb) - drift,
228
- linewidth and Allan deviation of a recovered carrier phase;
229
- - [`laser_linewidth_dsh`](https://github.com/lokgar/commkit/blob/main/examples/laser_linewidth_dsh.ipynb) and
230
- [`laser_linewidth_homodyne_iq`](https://github.com/lokgar/commkit/blob/main/examples/laser_linewidth_homodyne_iq.ipynb)
231
- - laser linewidth from delayed self-heterodyne and homodyne IQ captures;
232
- - `measurement_laser_linewidth_*` - lean templates for real captures.
165
+ - **00-05, signals and DSP**: the `Signal`, constellations and bit mapping, spectra, filters, pulse shaping and matched filtering, resampling;
166
+ - **06-08, channel and performance**: AWGN and the metrics (SNR, BER, LLRs, GMI), laser phase noise, front-end impairments;
167
+ - **09-16, receiver algorithms**: frame synchronization, frequency-offset estimation, blind and pilot-aided carrier recovery, linear, adaptive and blind equalization, polarization demultiplexing;
168
+ - **17-19, optical fiber systems**: chromatic dispersion, the nonlinear fiber with digital backpropagation, and an end-to-end coherent transceiver;
169
+ - **20-25, characterization**: carrier phase and laser linewidth measurements, with templates for real captures.
233
170
 
234
- The notebooks are committed without outputs and run in CI.
171
+ [`examples/README.md`](https://github.com/lokgar/commkit/blob/main/examples/README.md) lists every notebook with its topics. The quickstart above appears cell by cell in `00-getting-started` and `14-adaptive-equalization`. The notebooks are committed without outputs and run in CI.
235
172
 
236
173
  ## Development
237
174
 
@@ -246,9 +183,8 @@ uv run pytest --device=cpu # what CI runs
246
183
  uv run ruff check . && uv run mypy commkit/
247
184
  ```
248
185
 
249
- Contributor and coding-agent guidance (architecture, API rules, numerics,
250
- performance and test conventions) is in [AGENTS.md](https://github.com/lokgar/commkit/blob/main/AGENTS.md).
186
+ Contributor and coding-agent guidance (architecture, API rules, numerics, performance and test conventions) is in [AGENTS.md](https://github.com/lokgar/commkit/blob/main/AGENTS.md).
251
187
 
252
188
  ## License
253
189
 
254
- [MIT](https://github.com/lokgar/commkit/blob/main/LICENSE).
190
+ [MIT](https://github.com/lokgar/commkit/blob/main/LICENSE)