commkit 1.0.0__tar.gz → 2.0.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 (289) hide show
  1. commkit-2.0.0/.gitattributes +1 -0
  2. {commkit-1.0.0 → commkit-2.0.0}/.github/workflows/ci.yml +48 -1
  3. commkit-2.0.0/AGENTS.md +384 -0
  4. {commkit-1.0.0 → commkit-2.0.0}/LICENSE +1 -1
  5. commkit-2.0.0/PKG-INFO +254 -0
  6. commkit-2.0.0/README.md +218 -0
  7. commkit-2.0.0/benchmarks/baselines/Linux-CPython-3.14-64bit/0002_pre_v2.json +3257 -0
  8. commkit-2.0.0/benchmarks/baselines/Linux-CPython-3.14-64bit/0003_v2_0.json +3157 -0
  9. {commkit-1.0.0 → commkit-2.0.0}/benchmarks/bench_block_blind.py +24 -8
  10. {commkit-1.0.0 → commkit-2.0.0}/benchmarks/bench_block_lms.py +36 -20
  11. {commkit-1.0.0 → commkit-2.0.0}/benchmarks/bench_bps.py +15 -12
  12. commkit-2.0.0/benchmarks/bench_equalizers.py +93 -0
  13. commkit-2.0.0/benchmarks/bench_import.py +30 -0
  14. commkit-2.0.0/benchmarks/bench_llr.py +52 -0
  15. commkit-2.0.0/benchmarks/bench_signal_pipeline.py +146 -0
  16. {commkit-1.0.0 → commkit-2.0.0}/benchmarks/bench_sync_misc.py +10 -5
  17. {commkit-1.0.0 → commkit-2.0.0}/benchmarks/benchutils.py +20 -0
  18. {commkit-1.0.0 → commkit-2.0.0}/benchmarks/conftest.py +30 -9
  19. commkit-2.0.0/benchmarks/record_baseline.py +74 -0
  20. {commkit-1.0.0 → commkit-2.0.0}/benchmarks/workloads.py +40 -12
  21. commkit-2.0.0/commkit/__init__.py +106 -0
  22. commkit-2.0.0/commkit/_array.py +294 -0
  23. {commkit-1.0.0 → commkit-2.0.0}/commkit/_cuda/__init__.py +160 -14
  24. {commkit-1.0.0 → commkit-2.0.0}/commkit/_cuda/compiler.py +4 -1
  25. commkit-2.0.0/commkit/_cuda/src/bps_anchor.cu +146 -0
  26. commkit-2.0.0/commkit/_cuda/src/cs_block.cu +248 -0
  27. commkit-2.0.0/commkit/_cuda/src/llr.cu +110 -0
  28. commkit-2.0.0/commkit/_dispersion.py +84 -0
  29. commkit-2.0.0/commkit/_overlap_save.py +94 -0
  30. commkit-2.0.0/commkit/_random.py +39 -0
  31. commkit-2.0.0/commkit/_sequences.py +146 -0
  32. {commkit-1.0.0 → commkit-2.0.0}/commkit/analysis/__init__.py +37 -14
  33. {commkit-1.0.0 → commkit-2.0.0}/commkit/analysis/_common.py +177 -17
  34. {commkit-1.0.0 → commkit-2.0.0}/commkit/analysis/allan.py +38 -28
  35. commkit-2.0.0/commkit/analysis/drift.py +187 -0
  36. commkit-2.0.0/commkit/analysis/fm_noise.py +251 -0
  37. commkit-2.0.0/commkit/analysis/interferometry.py +271 -0
  38. commkit-2.0.0/commkit/analysis/linewidth.py +1057 -0
  39. {commkit-1.0.0 → commkit-2.0.0}/commkit/analysis/trajectory.py +49 -30
  40. commkit-2.0.0/commkit/backend.py +270 -0
  41. {commkit-1.0.0 → commkit-2.0.0}/commkit/core/__init__.py +6 -14
  42. commkit-2.0.0/commkit/core/_signal_adapter.py +164 -0
  43. {commkit-1.0.0 → commkit-2.0.0}/commkit/core/frame.py +281 -453
  44. commkit-2.0.0/commkit/core/generation.py +210 -0
  45. commkit-2.0.0/commkit/core/signal.py +454 -0
  46. {commkit-1.0.0 → commkit-2.0.0}/commkit/equalization/__init__.py +8 -11
  47. commkit-2.0.0/commkit/equalization/_block/__init__.py +24 -0
  48. commkit-2.0.0/commkit/equalization/_block/_blind.py +169 -0
  49. commkit-2.0.0/commkit/equalization/_block/_dd.py +867 -0
  50. commkit-2.0.0/commkit/equalization/_block/_engine.py +374 -0
  51. commkit-2.0.0/commkit/equalization/_common.py +378 -0
  52. {commkit-1.0.0 → commkit-2.0.0}/commkit/equalization/_kernels_numba.py +108 -64
  53. {commkit-1.0.0 → commkit-2.0.0}/commkit/equalization/blind.py +69 -46
  54. {commkit-1.0.0 → commkit-2.0.0}/commkit/equalization/linear.py +55 -42
  55. {commkit-1.0.0 → commkit-2.0.0}/commkit/equalization/polarization.py +141 -73
  56. commkit-2.0.0/commkit/equalization/result.py +302 -0
  57. commkit-2.0.0/commkit/equalization/sequential/__init__.py +21 -0
  58. commkit-2.0.0/commkit/equalization/sequential/_blind.py +494 -0
  59. commkit-2.0.0/commkit/equalization/sequential/_dd.py +732 -0
  60. commkit-2.0.0/commkit/equalization/sequential/_setup.py +454 -0
  61. commkit-2.0.0/commkit/filtering.py +1225 -0
  62. commkit-2.0.0/commkit/frequency.py +914 -0
  63. {commkit-1.0.0 → commkit-2.0.0}/commkit/impairments/__init__.py +6 -10
  64. {commkit-1.0.0 → commkit-2.0.0}/commkit/impairments/channel/linear.py +111 -97
  65. commkit-2.0.0/commkit/impairments/frontend.py +211 -0
  66. {commkit-1.0.0 → commkit-2.0.0}/commkit/impairments/noise.py +40 -32
  67. {commkit-1.0.0 → commkit-2.0.0}/commkit/impairments/source.py +86 -63
  68. commkit-2.0.0/commkit/io.py +347 -0
  69. {commkit-1.0.0 → commkit-2.0.0}/commkit/logger.py +26 -37
  70. commkit-2.0.0/commkit/mapping/__init__.py +33 -0
  71. commkit-2.0.0/commkit/mapping/bits.py +90 -0
  72. commkit-2.0.0/commkit/mapping/constellation.py +393 -0
  73. {commkit-1.0.0 → commkit-2.0.0}/commkit/mapping/gray.py +116 -12
  74. commkit-2.0.0/commkit/mapping/llr.py +255 -0
  75. commkit-2.0.0/commkit/mapping/shaping.py +185 -0
  76. commkit-2.0.0/commkit/math.py +324 -0
  77. commkit-2.0.0/commkit/metrics.py +685 -0
  78. commkit-2.0.0/commkit/multirate.py +339 -0
  79. {commkit-1.0.0 → commkit-2.0.0}/commkit/plotting/__init__.py +5 -12
  80. {commkit-1.0.0 → commkit-2.0.0}/commkit/plotting/analysis.py +133 -194
  81. commkit-2.0.0/commkit/plotting/constellation.py +430 -0
  82. {commkit-1.0.0 → commkit-2.0.0}/commkit/plotting/equalizer.py +12 -130
  83. {commkit-1.0.0 → commkit-2.0.0}/commkit/plotting/eye.py +84 -53
  84. commkit-2.0.0/commkit/plotting/filter_response.py +222 -0
  85. {commkit-1.0.0 → commkit-2.0.0}/commkit/plotting/spectral.py +179 -144
  86. {commkit-1.0.0 → commkit-2.0.0}/commkit/plotting/sync.py +468 -72
  87. {commkit-1.0.0 → commkit-2.0.0}/commkit/plotting/theme.py +30 -0
  88. commkit-2.0.0/commkit/plotting/waveform.py +219 -0
  89. commkit-2.0.0/commkit/recovery/__init__.py +54 -0
  90. commkit-2.0.0/commkit/recovery/_common.py +256 -0
  91. commkit-2.0.0/commkit/recovery/bps.py +306 -0
  92. commkit-2.0.0/commkit/recovery/carrier_phase.py +283 -0
  93. commkit-2.0.0/commkit/recovery/corrections.py +831 -0
  94. commkit-2.0.0/commkit/recovery/pilots.py +630 -0
  95. commkit-2.0.0/commkit/recovery/pll.py +378 -0
  96. commkit-2.0.0/commkit/recovery/tikhonov.py +274 -0
  97. commkit-2.0.0/commkit/recovery/viterbi_viterbi.py +123 -0
  98. commkit-2.0.0/commkit/smoothing.py +131 -0
  99. {commkit-1.0.0 → commkit-2.0.0}/commkit/spectral.py +232 -194
  100. {commkit-1.0.0 → commkit-2.0.0}/commkit/timing.py +393 -325
  101. commkit-2.0.0/examples/carrier_phase_analysis.ipynb +515 -0
  102. commkit-2.0.0/examples/laser_linewidth_dsh.ipynb +673 -0
  103. commkit-2.0.0/examples/laser_linewidth_homodyne_iq.ipynb +497 -0
  104. commkit-2.0.0/examples/measurement_laser_linewidth_dsh.ipynb +318 -0
  105. commkit-2.0.0/examples/measurement_laser_linewidth_homodyne_iq.ipynb +336 -0
  106. commkit-2.0.0/examples/qam_receiver_quickstart.ipynb +245 -0
  107. {commkit-1.0.0 → commkit-2.0.0}/pyproject.toml +86 -20
  108. commkit-2.0.0/tests/analysis/test_allan.py +34 -0
  109. commkit-2.0.0/tests/analysis/test_drift.py +45 -0
  110. commkit-2.0.0/tests/analysis/test_interferometry.py +114 -0
  111. commkit-2.0.0/tests/analysis/test_linewidth.py +473 -0
  112. commkit-2.0.0/tests/analysis/test_trajectory.py +112 -0
  113. commkit-2.0.0/tests/common/__init__.py +62 -0
  114. commkit-2.0.0/tests/common/conversions.py +47 -0
  115. commkit-2.0.0/tests/common/kernel_utils.py +140 -0
  116. commkit-2.0.0/tests/common/metrics.py +131 -0
  117. commkit-2.0.0/tests/common/reference_impl.py +539 -0
  118. commkit-2.0.0/tests/common/signals.py +376 -0
  119. {commkit-1.0.0 → commkit-2.0.0}/tests/conftest.py +59 -17
  120. commkit-2.0.0/tests/core/test_frame.py +492 -0
  121. commkit-2.0.0/tests/core/test_generation.py +158 -0
  122. commkit-2.0.0/tests/core/test_psqam.py +272 -0
  123. commkit-2.0.0/tests/core/test_signal.py +875 -0
  124. commkit-2.0.0/tests/core/test_signal_adapter.py +107 -0
  125. commkit-2.0.0/tests/core/test_signal_mimo.py +169 -0
  126. {commkit-1.0.0 → commkit-2.0.0}/tests/equalization/test_blind.py +90 -71
  127. commkit-2.0.0/tests/equalization/test_block.py +760 -0
  128. commkit-2.0.0/tests/equalization/test_bps_kernel.py +333 -0
  129. commkit-2.0.0/tests/equalization/test_cpr.py +753 -0
  130. commkit-2.0.0/tests/equalization/test_cs_kernel.py +116 -0
  131. commkit-2.0.0/tests/equalization/test_equalizer_oracles.py +300 -0
  132. {commkit-1.0.0 → commkit-2.0.0}/tests/equalization/test_linear.py +65 -33
  133. {commkit-1.0.0 → commkit-2.0.0}/tests/equalization/test_mimo.py +44 -67
  134. commkit-2.0.0/tests/equalization/test_polarization.py +448 -0
  135. {commkit-1.0.0 → commkit-2.0.0}/tests/equalization/test_sequential.py +397 -401
  136. commkit-2.0.0/tests/equalization/test_state.py +226 -0
  137. commkit-2.0.0/tests/equalization/test_winit.py +319 -0
  138. {commkit-1.0.0 → commkit-2.0.0}/tests/impairments/channel/test_channel_linear.py +138 -36
  139. {commkit-1.0.0 → commkit-2.0.0}/tests/impairments/test_frontend.py +86 -34
  140. commkit-2.0.0/tests/impairments/test_noise.py +97 -0
  141. {commkit-1.0.0 → commkit-2.0.0}/tests/impairments/test_source.py +69 -32
  142. commkit-2.0.0/tests/mapping/test_bits.py +137 -0
  143. commkit-2.0.0/tests/mapping/test_constellation.py +296 -0
  144. commkit-2.0.0/tests/mapping/test_gray.py +166 -0
  145. commkit-2.0.0/tests/mapping/test_llr.py +314 -0
  146. commkit-2.0.0/tests/plotting/test_plot_analysis.py +159 -0
  147. commkit-2.0.0/tests/plotting/test_plot_constellation.py +195 -0
  148. commkit-2.0.0/tests/plotting/test_plot_equalizer.py +191 -0
  149. commkit-2.0.0/tests/plotting/test_plot_eye.py +181 -0
  150. commkit-2.0.0/tests/plotting/test_plot_filter_response.py +71 -0
  151. commkit-2.0.0/tests/plotting/test_plot_spectral.py +264 -0
  152. commkit-2.0.0/tests/plotting/test_plot_sync.py +214 -0
  153. commkit-2.0.0/tests/plotting/test_plot_theme.py +28 -0
  154. commkit-2.0.0/tests/plotting/test_plot_waveform.py +113 -0
  155. commkit-2.0.0/tests/recovery/test_bps.py +252 -0
  156. commkit-2.0.0/tests/recovery/test_corrections.py +620 -0
  157. commkit-2.0.0/tests/recovery/test_joint.py +85 -0
  158. commkit-2.0.0/tests/recovery/test_pilots.py +721 -0
  159. commkit-2.0.0/tests/recovery/test_pll.py +216 -0
  160. commkit-2.0.0/tests/recovery/test_recovery_oracles.py +125 -0
  161. commkit-2.0.0/tests/recovery/test_tikhonov.py +269 -0
  162. commkit-2.0.0/tests/recovery/test_viterbi_viterbi.py +229 -0
  163. commkit-2.0.0/tests/test_api_contracts.py +933 -0
  164. commkit-2.0.0/tests/test_array.py +153 -0
  165. commkit-2.0.0/tests/test_backend.py +117 -0
  166. commkit-2.0.0/tests/test_cuda_infra.py +111 -0
  167. commkit-2.0.0/tests/test_dispersion.py +107 -0
  168. commkit-2.0.0/tests/test_examples.py +64 -0
  169. commkit-2.0.0/tests/test_filtering.py +417 -0
  170. {commkit-1.0.0 → commkit-2.0.0}/tests/test_frequency.py +438 -308
  171. commkit-2.0.0/tests/test_import_hygiene.py +116 -0
  172. commkit-2.0.0/tests/test_io.py +360 -0
  173. commkit-2.0.0/tests/test_logger.py +64 -0
  174. commkit-2.0.0/tests/test_math.py +169 -0
  175. commkit-2.0.0/tests/test_metrics.py +650 -0
  176. commkit-2.0.0/tests/test_multirate.py +105 -0
  177. commkit-2.0.0/tests/test_pulse_shaping.py +227 -0
  178. commkit-2.0.0/tests/test_signal_pipeline.py +339 -0
  179. commkit-2.0.0/tests/test_smoothing.py +56 -0
  180. commkit-2.0.0/tests/test_spectral.py +379 -0
  181. commkit-2.0.0/tests/test_timing.py +882 -0
  182. {commkit-1.0.0 → commkit-2.0.0}/uv.lock +95 -266
  183. commkit-1.0.0/.gitattributes +0 -1
  184. commkit-1.0.0/CLAUDE.md +0 -386
  185. commkit-1.0.0/PKG-INFO +0 -145
  186. commkit-1.0.0/README.md +0 -123
  187. commkit-1.0.0/benchmarks/baselines/Linux-CPython-3.14-64bit/0001_commkit_baseline.json +0 -2375
  188. commkit-1.0.0/benchmarks/bench_equalizers.py +0 -174
  189. commkit-1.0.0/commkit/__init__.py +0 -74
  190. commkit-1.0.0/commkit/_cuda/src/cs_block.cu +0 -119
  191. commkit-1.0.0/commkit/analysis/drift.py +0 -213
  192. commkit-1.0.0/commkit/analysis/interferometry.py +0 -887
  193. commkit-1.0.0/commkit/analysis/linewidth.py +0 -480
  194. commkit-1.0.0/commkit/backend.py +0 -507
  195. commkit-1.0.0/commkit/core/generation.py +0 -581
  196. commkit-1.0.0/commkit/core/signal.py +0 -725
  197. commkit-1.0.0/commkit/equalization/_block.py +0 -1855
  198. commkit-1.0.0/commkit/equalization/_common.py +0 -606
  199. commkit-1.0.0/commkit/equalization/_kernels_jax.py +0 -1720
  200. commkit-1.0.0/commkit/equalization/result.py +0 -191
  201. commkit-1.0.0/commkit/equalization/sequential.py +0 -2805
  202. commkit-1.0.0/commkit/filtering.py +0 -1120
  203. commkit-1.0.0/commkit/frequency.py +0 -1191
  204. commkit-1.0.0/commkit/helpers.py +0 -489
  205. commkit-1.0.0/commkit/impairments/frontend.py +0 -229
  206. commkit-1.0.0/commkit/io.py +0 -308
  207. commkit-1.0.0/commkit/mapping/__init__.py +0 -46
  208. commkit-1.0.0/commkit/mapping/bits.py +0 -240
  209. commkit-1.0.0/commkit/mapping/constellation.py +0 -153
  210. commkit-1.0.0/commkit/mapping/llr.py +0 -253
  211. commkit-1.0.0/commkit/mapping/shaping.py +0 -218
  212. commkit-1.0.0/commkit/metrics.py +0 -949
  213. commkit-1.0.0/commkit/multirate.py +0 -476
  214. commkit-1.0.0/commkit/plotting/constellation.py +0 -483
  215. commkit-1.0.0/commkit/plotting/waveform.py +0 -200
  216. commkit-1.0.0/commkit/recovery/__init__.py +0 -51
  217. commkit-1.0.0/commkit/recovery/bps.py +0 -337
  218. commkit-1.0.0/commkit/recovery/corrections.py +0 -751
  219. commkit-1.0.0/commkit/recovery/pilots.py +0 -803
  220. commkit-1.0.0/commkit/recovery/pll.py +0 -482
  221. commkit-1.0.0/commkit/recovery/tikhonov.py +0 -424
  222. commkit-1.0.0/commkit/recovery/viterbi_viterbi.py +0 -227
  223. commkit-1.0.0/examples/carrier_phase_analysis.py +0 -378
  224. commkit-1.0.0/examples/laser_linewidth_dsh.py +0 -498
  225. commkit-1.0.0/examples/laser_linewidth_homodyne_iq.py +0 -358
  226. commkit-1.0.0/examples/measurement_laser_linewidth_dsh.py +0 -206
  227. commkit-1.0.0/examples/measurement_laser_linewidth_homodyne_iq.py +0 -212
  228. commkit-1.0.0/tests/analysis/test_allan.py +0 -25
  229. commkit-1.0.0/tests/analysis/test_drift.py +0 -44
  230. commkit-1.0.0/tests/analysis/test_interferometry.py +0 -258
  231. commkit-1.0.0/tests/analysis/test_linewidth.py +0 -135
  232. commkit-1.0.0/tests/analysis/test_trajectory.py +0 -50
  233. commkit-1.0.0/tests/core/test_frame.py +0 -484
  234. commkit-1.0.0/tests/core/test_psqam.py +0 -314
  235. commkit-1.0.0/tests/core/test_signal.py +0 -904
  236. commkit-1.0.0/tests/core/test_signal_mimo.py +0 -197
  237. commkit-1.0.0/tests/equalization/test_block.py +0 -1032
  238. commkit-1.0.0/tests/equalization/test_block_update.py +0 -373
  239. commkit-1.0.0/tests/equalization/test_bps_kernel.py +0 -252
  240. commkit-1.0.0/tests/equalization/test_cpr.py +0 -961
  241. commkit-1.0.0/tests/equalization/test_cs_kernel.py +0 -173
  242. commkit-1.0.0/tests/equalization/test_polarization.py +0 -300
  243. commkit-1.0.0/tests/equalization/test_sequential_jax.py +0 -556
  244. commkit-1.0.0/tests/equalization/test_winit.py +0 -407
  245. commkit-1.0.0/tests/impairments/test_noise.py +0 -58
  246. commkit-1.0.0/tests/mapping/test_bits.py +0 -159
  247. commkit-1.0.0/tests/mapping/test_constellation.py +0 -68
  248. commkit-1.0.0/tests/mapping/test_gray.py +0 -108
  249. commkit-1.0.0/tests/mapping/test_llr.py +0 -296
  250. commkit-1.0.0/tests/recovery/test_bps.py +0 -139
  251. commkit-1.0.0/tests/recovery/test_corrections.py +0 -352
  252. commkit-1.0.0/tests/recovery/test_joint.py +0 -94
  253. commkit-1.0.0/tests/recovery/test_pilots.py +0 -600
  254. commkit-1.0.0/tests/recovery/test_pll.py +0 -205
  255. commkit-1.0.0/tests/recovery/test_tikhonov.py +0 -276
  256. commkit-1.0.0/tests/recovery/test_viterbi_viterbi.py +0 -185
  257. commkit-1.0.0/tests/test_backend.py +0 -285
  258. commkit-1.0.0/tests/test_cuda_infra.py +0 -106
  259. commkit-1.0.0/tests/test_filtering.py +0 -429
  260. commkit-1.0.0/tests/test_helpers.py +0 -225
  261. commkit-1.0.0/tests/test_io.py +0 -410
  262. commkit-1.0.0/tests/test_logger.py +0 -11
  263. commkit-1.0.0/tests/test_metrics.py +0 -572
  264. commkit-1.0.0/tests/test_multirate.py +0 -122
  265. commkit-1.0.0/tests/test_plotting.py +0 -898
  266. commkit-1.0.0/tests/test_pulse_shaping.py +0 -154
  267. commkit-1.0.0/tests/test_spectral.py +0 -345
  268. commkit-1.0.0/tests/test_timing.py +0 -985
  269. {commkit-1.0.0 → commkit-2.0.0}/.github/workflows/publish.yml +0 -0
  270. {commkit-1.0.0 → commkit-2.0.0}/.gitignore +0 -0
  271. {commkit-1.0.0 → commkit-2.0.0}/.python-version +0 -0
  272. {commkit-1.0.0 → commkit-2.0.0}/commkit/_cuda/src/bps_min_d2.cu +0 -0
  273. {commkit-1.0.0 → commkit-2.0.0}/commkit/_cuda/src/selftest.cu +0 -0
  274. {commkit-1.0.0 → commkit-2.0.0}/commkit/coding/__init__.py +0 -0
  275. {commkit-1.0.0 → commkit-2.0.0}/commkit/coding/base.py +0 -0
  276. {commkit-1.0.0 → commkit-2.0.0}/commkit/coding/bch.py +0 -0
  277. {commkit-1.0.0 → commkit-2.0.0}/commkit/coding/convolutional.py +0 -0
  278. {commkit-1.0.0 → commkit-2.0.0}/commkit/coding/crc.py +0 -0
  279. {commkit-1.0.0 → commkit-2.0.0}/commkit/coding/galois.py +0 -0
  280. {commkit-1.0.0 → commkit-2.0.0}/commkit/coding/hamming.py +0 -0
  281. {commkit-1.0.0 → commkit-2.0.0}/commkit/coding/interleaving.py +0 -0
  282. {commkit-1.0.0 → commkit-2.0.0}/commkit/coding/ldpc.py +0 -0
  283. {commkit-1.0.0 → commkit-2.0.0}/commkit/coding/polar.py +0 -0
  284. {commkit-1.0.0 → commkit-2.0.0}/commkit/coding/ratematch.py +0 -0
  285. {commkit-1.0.0 → commkit-2.0.0}/commkit/coding/reed_solomon.py +0 -0
  286. {commkit-1.0.0 → commkit-2.0.0}/commkit/coding/turbo.py +0 -0
  287. {commkit-1.0.0 → commkit-2.0.0}/commkit/impairments/channel/__init__.py +0 -0
  288. {commkit-1.0.0 → commkit-2.0.0}/commkit/impairments/channel/nonlinear.py +0 -0
  289. {commkit-1.0.0 → commkit-2.0.0}/commkit/py.typed +0 -0
@@ -0,0 +1 @@
1
+ *.ipynb filter=nbstripout diff=ipynb
@@ -38,6 +38,22 @@ jobs:
38
38
  - name: Type check
39
39
  run: uv run mypy commkit/
40
40
 
41
+ notebooks:
42
+ name: Notebooks carry no outputs
43
+ # Outputs (plots are base64 PNGs) would bloat the repository. The
44
+ # nbstripout git filter strips them on commit once a clone runs
45
+ # 'uv run nbstripout --install'; this required check catches a clone
46
+ # without it before anything reaches main.
47
+ runs-on: ubuntu-latest
48
+ steps:
49
+ - uses: actions/checkout@v4
50
+
51
+ - name: Install uv
52
+ uses: astral-sh/setup-uv@v5
53
+
54
+ - name: Every committed notebook is stripped
55
+ run: uvx nbstripout@0.9.1 --verify $(git ls-files '*.ipynb')
56
+
41
57
  test:
42
58
  name: Test (Python ${{ matrix.python-version }} | ${{ matrix.resolution }})
43
59
  runs-on: ubuntu-latest
@@ -68,7 +84,38 @@ jobs:
68
84
  run: uv sync --python ${{ matrix.python-version }} --resolution lowest-direct
69
85
 
70
86
  - name: Run tests (CPU) with coverage
71
- run: uv run pytest --device=cpu --cov=commkit --cov-report=term-missing
87
+ # Floor = total at the start of the 2.0 work; raise it, never lower it.
88
+ run: uv run pytest --device=cpu --cov=commkit --cov-report=term-missing --cov-fail-under=78
89
+
90
+ kernel-coverage:
91
+ name: Numba kernel coverage (JIT disabled)
92
+ # Compiled Numba code is invisible to coverage, so the main job reports the
93
+ # kernels as almost untested. Here the same tests run the kernels as plain
94
+ # Python, measuring their real line coverage.
95
+ runs-on: ubuntu-latest
96
+ steps:
97
+ - uses: actions/checkout@v4
98
+
99
+ - name: Install uv
100
+ uses: astral-sh/setup-uv@v5
101
+ with:
102
+ enable-cache: true
103
+
104
+ - name: Set up Python
105
+ run: uv python install 3.12
106
+
107
+ - name: Sync environment
108
+ run: uv sync
109
+
110
+ - name: Run kernel tests with JIT disabled
111
+ env:
112
+ NUMBA_DISABLE_JIT: "1"
113
+ run: uv run pytest tests/equalization tests/recovery tests/test_frequency.py --device=cpu --cov=commkit --cov-report=
114
+
115
+ - name: Kernel coverage report
116
+ run: >-
117
+ uv run coverage report --show-missing --fail-under=85
118
+ --include="commkit/equalization/_kernels_numba.py,commkit/equalization/sequential/*,commkit/frequency.py,commkit/recovery/*"
72
119
 
73
120
  # GPU legs (--device=gpu / --device=all) require CuPy + CUDA and are run
74
121
  # manually or on a self-hosted runner; they are intentionally not part of
@@ -0,0 +1,384 @@
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
6
+ that breaks one changes this file in the same commit.
7
+
8
+ ---
9
+
10
+ ## 1. What CommKit is
11
+
12
+ A Python library for digital-communications research: simulation, receiver DSP,
13
+ metrics, plotting and experimental captures, on NumPy (CPU) and CuPy (GPU) data.
14
+ It is **not differentiable**. A separate JAX library, *commax*, will hold the
15
+ blocks that must sit inside a gradient. commax depends on commkit, never the
16
+ reverse, and takes its conventions (`Constellation`, bit labels, pulse taps,
17
+ frame layouts) from commkit.
18
+
19
+ Users are researchers and engineers. Design for them:
20
+
21
+ - **Algorithms are plain functions.** Data is a `Signal` or an array. Things
22
+ that *describe* a modulation, a pulse or an embedded sub-algorithm are small
23
+ frozen value objects written inline in the call:
24
+
25
+ ```python
26
+ res = lms(rx, num_taps=21, step_size=1e-3, cpr=BPS(test_phases=64))
27
+ ```
28
+
29
+ - **Everything is expressed in Python.** No configuration files, registries,
30
+ global switches or multi-step setup.
31
+ - **We deliberately do not build** pipeline or receiver objects, DSP methods on
32
+ `Signal`, string shorthands for constellations, or public "config objects per
33
+ algorithm".
34
+
35
+ ---
36
+
37
+ ## 2. Commands
38
+
39
+ Always use `uv`. Python 3.12+.
40
+
41
+ ```bash
42
+ uv sync --all-extras # environment incl. GPU packages
43
+ uv run nbstripout --install # once per clone: strip notebook outputs on commit
44
+ uv run pytest # CPU + GPU tests (default --device=all)
45
+ uv run pytest --device=cpu # what CI runs
46
+ uv run pytest tests/equalization/ -k lms # subset
47
+ uv run pytest --cov=commkit # coverage
48
+ NUMBA_DISABLE_JIT=1 uv run pytest tests/equalization tests/recovery \
49
+ tests/test_frequency.py --device=cpu --cov=commkit # real kernel coverage
50
+ ```
51
+
52
+ **Commit gate.** CI fails on any of these, so run all of them before every
53
+ commit, with `ruff format` last:
54
+
55
+ ```bash
56
+ uv run ruff check . --fix
57
+ uv run mypy commkit/
58
+ uv run pytest
59
+ uv run ruff format .
60
+ ```
61
+
62
+ **Benchmarks** are explicit-only; `uv run pytest` never collects them. A
63
+ commit that changes a public signature also smoke-runs them once, without
64
+ timing, so their call sites cannot rot unnoticed:
65
+
66
+ ```bash
67
+ uv run pytest benchmarks/ --benchmark-disable --device=all
68
+ ```
69
+
70
+ Timed runs:
71
+
72
+ ```bash
73
+ uv run pytest benchmarks/bench_bps.py --benchmark-only --device=all
74
+ uv run pytest benchmarks/ --benchmark-only --device=all \
75
+ --benchmark-compare=0003 --benchmark-storage=file://benchmarks/baselines
76
+ uv run python benchmarks/record_baseline.py NAME # record a new baseline
77
+ ```
78
+
79
+ **Release:**
80
+
81
+ 1. On `main`: the CPU and GPU suites pass with no failures or skips
82
+ (`uv run pytest -rs`), `uv build` succeeds and
83
+ `uv run --with twine twine check dist/*` passes.
84
+ 2. `uv run bump-my-version bump {patch|minor|major}`. This commits the new
85
+ version and tags it `vX.Y.Z`.
86
+ 3. `git push origin main --tags`.
87
+ 4. Publish a GitHub Release for the tag (`gh release create vX.Y.Z
88
+ --generate-notes`). Publishing the release runs
89
+ `.github/workflows/publish.yml`, which builds and uploads to PyPI through
90
+ Trusted Publishing; pushing a tag alone publishes nothing.
91
+
92
+ ---
93
+
94
+ ## 3. Architecture
95
+
96
+ Dependencies point downward only:
97
+
98
+ | Layer | Modules | May import |
99
+ | --- | --- | --- |
100
+ | Infrastructure | `backend`, `logger`, `_cuda` | NumPy, CuPy |
101
+ | Array and math helpers | `_array` (shape and validation), `math` (power, normalization, dB, linear trend) | infrastructure |
102
+ | Value objects | `mapping.Constellation`, pulse classes, `recovery` CPR configs | layers above |
103
+ | Core | `core` (`Signal`, frames, generation), `io` | layers above |
104
+ | DSP | `mapping`, `filtering`, `multirate`, `spectral`, `smoothing`, `impairments`, `timing`, `frequency`, `recovery`, `equalization`, `metrics`, `analysis` | layers above, and lower DSP modules |
105
+ | Presentation | `plotting` | everything; **nothing imports plotting** |
106
+
107
+ - **Responsibilities:**
108
+ - `filtering` holds signal-chain filters with a real frequency response.
109
+ - `smoothing` holds display and estimation smoothers only.
110
+ - `analysis` holds laser and phase characterization.
111
+ - `metrics` is the reporting layer.
112
+ - **Splitting modules:** promote a flat module to a package only when it
113
+ exceeds about 1000 lines *and* has two or more separable concerns. The
114
+ package `__init__` re-exports the public names. Never create a `utils/`
115
+ package or one-function files.
116
+ - **Placeholder modules are kept on purpose.** `commkit.coding` and
117
+ `commkit.impairments.channel.nonlinear` are reminders of planned work. Never
118
+ delete them or treat them as dead code. They are excluded from coverage and
119
+ from the contract registry, and the README labels them "planned, not
120
+ implemented".
121
+
122
+ ---
123
+
124
+ ## 4. API rules
125
+
126
+ ### Signal
127
+
128
+ - `Signal` holds: samples (NumPy or CuPy, `(N,)` or `(C, N)`, time on the last
129
+ axis), the **facts** `sampling_rate`, `symbol_rate` and `center_frequency`, the
130
+ **description** `constellation` and `pulse`, and the ground truth
131
+ `reference` (transmitted symbols and bits) plus the `frame` layout.
132
+ - It never holds pipeline results.
133
+ - Construction does no hidden work: no mapping, no normalization, no device
134
+ move, no shape guessing. Factories (`generate`, frames) do that work.
135
+ - `sig.to("gpu")` and `sig.replace(...)` return new Signals. Arrays are shared,
136
+ not copied; use `clone()` for an independent copy.
137
+ - **Alignment invariant:** reference symbol *k* corresponds to symbol period *k*
138
+ of the samples. Any function that drops or shifts symbol periods slices the
139
+ reference to match. Metrics raise on mismatch; they never guess.
140
+
141
+ ### Function signatures
142
+
143
+ - `f(data, other_data, *, params)`: everything after the data arguments is
144
+ keyword-only. A function without data arguments may keep its single primary
145
+ parameter positional (`barker_sequence(13)`, `gray_code(5)`); with several
146
+ parameters, all are keyword-only (`rrc_taps(sps=2, rolloff=0.1, span=10)`).
147
+ The data-argument count is recorded per function in the contract registry.
148
+ - **Signal awareness.** A function whose main argument is waveform data (or a
149
+ field a Signal really carries) accepts an array or a Signal. It unwraps once
150
+ with `adapt_signal()` (in `core/_signal_adapter.py`), works on arrays, and
151
+ returns through `wrap_samples()`. Never re-enter the public function
152
+ recursively.
153
+ - Functions on *derived* quantities (phase trajectories, correlations, taps,
154
+ PSD bins, results) and synthesis primitives stay array-only.
155
+ - **Metadata rule: facts versus choices.**
156
+ - Facts (`sampling_rate`, `symbol_rate`, and so `sps`) come from the Signal.
157
+ A conflicting argument raises `ValueError`; an equal one is fine.
158
+ - Choices (decision `constellation`, `noise_var`, `pulse`) default to the
159
+ Signal's value, and an explicit argument wins.
160
+ - A missing fact for array input raises. Neither case warns.
161
+ - Integer-SPS algorithms validate with `require_integer_sps()` before any cast.
162
+ Never truncate 1.5 SPS to 1.
163
+ - **Value objects are frozen dataclasses** with read-only NumPy arrays:
164
+ - `Constellation`: `.qam(M)`, `.psk(M)`, `.pam(M, unipolar=)`, or arbitrary
165
+ `points`; `.shaped(nu=|entropy=)` keeps unit power. Functions accept the
166
+ object only, never strings.
167
+ - Pulses: `RRC`, `RC`, `Gaussian`, `Rect`, `SmoothRect`. A raw taps array is
168
+ accepted wherever a pulse is.
169
+ - Algorithm objects, one per estimation method (`BPS`, `PLL`,
170
+ `ViterbiViterbi`, `MthPower`, `CycleSlip`, ...). The same object is used
171
+ standalone (`correct_carrier_phase(y, BPS())`) and nested
172
+ (`lms(..., cpr=BPS())`). It lives next to its kernel and validates its
173
+ parameters on construction. Class names are unique across the package.
174
+ - Equalizer continuation uses `state=result.state`.
175
+ - **Randomness:** `rng: int | np.random.Generator | None` (SciPy SPEC 7).
176
+ - What is transmitted (bits, symbols, pilots): draw on the host with the
177
+ Generator, then transfer. A seed gives the same payload on CPU and GPU.
178
+ - What the channel adds (AWGN, phase noise): draw on the data's device
179
+ through `_random.standard_normal`. Realizations differ between devices;
180
+ statistics agree, and tests assert statistics.
181
+ - Never use a global RNG.
182
+ - **Types:** a transform is written `def f(samples: S, ...) -> S` with the
183
+ TypeVar `S` from `core/_signal_adapter.py` (bound to `np.ndarray | Signal`),
184
+ so a Signal gives a Signal and an array an array. Do not reassign the
185
+ `samples` parameter; name the unwrapped array `x`.
186
+ - **Device follows the data.** There are no `backend=` arguments and no
187
+ global switches. Unsupported array types (JAX, PyTorch) raise `TypeError`.
188
+ - Only factories with no input data to follow (`generate`,
189
+ `generate_phase_noise`, `Preamble`/`SingleCarrierFrame.to_signal`,
190
+ `load_npz`) take `device: str = "cpu"`, validated with
191
+ `require_device()`. They draw randomness as the rule above says and do
192
+ the vectorized work (mapping, shaping) on `device`. The default is never
193
+ "GPU if available".
194
+ - **No `debug_plot`.** Numerical code never imports plotting. Plot functions
195
+ consume results or recompute through public compute functions.
196
+ - **Design and apply are separate functions**, for example `rrc_taps` and
197
+ `fir_filter`, or `butterworth_sos` and `iir_filter`.
198
+
199
+ ### Returns
200
+
201
+ - **Transforms:** array in gives array out on the same device; Signal in gives
202
+ a new Signal out (update `sampling_rate` when the rate changes).
203
+ - **Estimators** reduce over the time axis by input rank: `(N,)` gives 0-d and
204
+ `(C, N)` gives `(C,)`. Results stay on the input device.
205
+ - **Analysis summaries** (`estimate_linewidth`, `allan_deviation`, the
206
+ `frequency_drift` statistics) are reporting-layer fits on plot-sized
207
+ reductions. Like the metrics, they return host floats or `(C,)` arrays.
208
+ Sample-rate outputs (trajectories, PSDs, `dsh_phase`) stay on the input
209
+ device.
210
+ - **Metrics** (`evm`, `snr`, `ber`, `ser`, `gmi`, `mi`) return host values: a
211
+ `float` for 1-D input and `np.ndarray (C,)` for 2-D input. They **raise** on
212
+ empty selections; they never return `None` or `0.0` for "nothing measured".
213
+ Metrics never silently align, rotate or permute against the reference.
214
+ - **More than one value** is returned as a frozen dataclass with named fields.
215
+ No dicts, and no tuples longer than two.
216
+ - **Equalizers** return `EqualizerResult`. `y_hat` is always an array,
217
+ `signal` holds the 1-SPS Signal for Signal input, `state` is the
218
+ continuation state, and diagnostics are returned as data.
219
+
220
+ ### Naming
221
+
222
+ | Prefix | Meaning |
223
+ | --- | --- |
224
+ | `apply_*` | add an impairment or apply a model |
225
+ | `estimate_<quantity>(x, method)` | measure, never change the data; returns a frozen `<Quantity>Estimate` (value plus diagnostics) |
226
+ | `correct_<quantity>(x, how)` | remove the impairment; returns only the corrected data. `how` is an estimate or an algorithm object (which estimates first) |
227
+ | `resolve_*` | pick among discrete candidates (π/2 rotation, channel permutation); nothing else |
228
+ | `generate` | synthesis from parameters, takes `rng` |
229
+ | `plot_*` | every public plotting function (only `apply_default_theme` is exempt) |
230
+
231
+ - The algorithm is chosen by an object, never by the function name or a
232
+ string: `estimate_carrier_phase(y, BPS())`, not
233
+ `recover_carrier_phase_bps(y)`. The verb function dispatches on the object's
234
+ type through a private table in its module. There is no `recover_*` or
235
+ `compensate_*`.
236
+ - Computations use plain nouns (`allan_deviation`).
237
+ - **Glossary**, one name each:
238
+ - `num_taps`, `step_size`, `sps`, `constellation`, `pulse`, `rng`,
239
+ `num_symbols`, `num_channels`, `sampling_rate`, `symbol_rate`.
240
+ - Units are in the docstring. Use `_db` and `_hz` suffixes where the unit is
241
+ ambiguous.
242
+
243
+ ### Errors and logging
244
+
245
+ - Wrong input raises, with a message naming the argument.
246
+ - `logger` is for diagnostics only. Per-channel logging that needs a
247
+ device-to-host transfer is wrapped in
248
+ `if logger.isEnabledFor(logging.INFO):`.
249
+ - The library never configures handlers, Matplotlib or warning filters at
250
+ import time.
251
+
252
+ ---
253
+
254
+ ## 5. Numerics
255
+
256
+ - **Storage dtypes:** `complex64` for IQ samples and `float32` for real
257
+ signals.
258
+ - **Accumulation precision:**
259
+ - LMS/CMA/RDE: inputs and weights are `complex64`, but dot products and
260
+ gradient updates in the hot loop are accumulated in `complex128`.
261
+ - RLS keeps `P`, the gain `k` and the regressor buffers in `complex128`
262
+ throughout. Single precision loses the Hermitian positive-definite
263
+ property and the filter diverges.
264
+ - Carrier phase: promote angles to `float64` before `xp.unwrap()`, because
265
+ float32 rounding causes spurious quadrant slips. Tikhonov/Kalman block
266
+ transitions also run in `float64`.
267
+ - **Normalization invariant:** at `sps` samples per symbol, `E[|x|²] = 1/sps`;
268
+ at symbol rate, `E[|x|²] = 1`. Any rate-changing block applies the exact
269
+ gain correction.
270
+ - **Shapes:** SISO is `(N,)` and MIMO is `(C, N)`. Use `as_2d`, `restore_1d`,
271
+ `broadcast_channels` and `require_channels`; never hand-roll the
272
+ promote/squeeze idiom.
273
+ - **Reproducibility:** a seed is reproducible within a library version only.
274
+ Tests and baselines assert statistics, never exact noise realizations.
275
+
276
+ ---
277
+
278
+ ## 6. Performance
279
+
280
+ | Algorithm structure | Implementation | Device |
281
+ | --- | --- | --- |
282
+ | Vectorized: filters, resampling, spectra, impairments, metrics, LLR, frequency-domain equalizers | NumPy/CuPy via `x, xp, sp = dispatch(samples)` | follows the input |
283
+ | 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 |
284
+ | Hot spots limited by intermediates, launches or round trips | CUDA C++ via CuPy `RawModule` (`commkit/_cuda`) | GPU |
285
+
286
+ **Host-sync hygiene:**
287
+
288
+ - Never extract scalars from a possibly-GPU array inside a loop (`float(x[c])`,
289
+ `.item()`). Compute the vector on the device, transfer it once with
290
+ `to_device(v, "cpu")`, then loop on the host.
291
+ - Prefer on-device gathers (`xp.take_along_axis`).
292
+ - Chunk `(N, M)` intermediates over N with an on-device accumulator.
293
+ - Reduce on the device before transferring, including for plotting.
294
+ - Pass whole records to compiled code. There are no Python loops over symbols
295
+ or small blocks, except inside a CUDA graph.
296
+
297
+ **Kernel policy:**
298
+
299
+ - A `.cu` kernel needs a benchmark showing that CuPy is limited by
300
+ intermediates, launch count or round trips.
301
+ - It optimizes a CPU reference (Numba or NumPy) and is tested against it with
302
+ `--device=all`.
303
+ - Wrappers check dtype, contiguity, shape and size before launch.
304
+ - `float32` with explicit literals.
305
+ - `--use_fast_math` only per kernel (through `options`), after an accuracy
306
+ check; the compiler default does not pass it.
307
+ - Short elementwise chains use `ElementwiseKernel` or `cupy.fuse`.
308
+ - Sequential recursions over a whole record are never ported to the GPU; one
309
+ record has no parallel work per step. A short per-block scan may be, as one
310
+ launch, when it replaces a host round trip per block (`cs_block`,
311
+ `bps_anchor`); keep its parallel part parallel (float64 is 1/64 of float32
312
+ throughput on consumer GPUs).
313
+
314
+ ---
315
+
316
+ ## 7. Tests
317
+
318
+ - **Fixtures:** use `backend_device`, `xp` and `xpt` from `tests/conftest.py`.
319
+ Assert with `xpt.assert_allclose(...)`, because `numpy.testing` raises on
320
+ CuPy arrays. Cast expectations with `xp.asarray`, and reductions with
321
+ `float(...)`.
322
+ - **Layout mirrors the source:** `tests/<subpackage>/test_<module>.py`, and
323
+ `tests/test_<module>.py` for flat modules. Test basenames must be globally
324
+ unique (pytest prepend import mode), hence names like
325
+ `channel/test_channel_linear.py`. Split a large test file by concern.
326
+ - **Shared builders** live in `tests/common/` and use the public API.
327
+ - **Contract registry** (`tests/test_api_contracts.py`): every public function
328
+ has a row. Generic checks run per row: array in/out, Signal in/out, device
329
+ preserved, input not mutated, keyword-only parameters, `TypeError` on
330
+ unsupported arrays, fact-conflict errors, return rules.
331
+ - Do not duplicate these checks in per-module tests.
332
+ - **Oracles:** sequential kernels are tested against plain-Python reference
333
+ implementations in `tests/common/reference_impl.py`. CUDA kernels are tested
334
+ against their CPU reference.
335
+ - **Mathematical meaning:** test documented definitions, such as a pulse
336
+ width, a dispersion sign, or noise-variance scaling, with independently
337
+ derived expectations, not only shapes and energies.
338
+ - **API changes never change expected values.** A commit that changes a
339
+ signature only changes how tests call the code. Numerical changes land in
340
+ their own commit with independent validation.
341
+ - **Examples** are Jupyter notebooks in `examples/`, committed without outputs:
342
+ `.gitattributes` routes them through the `nbstripout` filter, which each
343
+ clone enables once with `uv run nbstripout --install` (git does not ship
344
+ filter configuration). `tests/test_examples.py` executes each one, so an API change
345
+ updates the notebooks in the same commit.
346
+ - **Coverage** must not drop below the CI floor. Numba kernel coverage is
347
+ measured by a separate `NUMBA_DISABLE_JIT=1` job.
348
+
349
+ ---
350
+
351
+ ## 8. Benchmarks
352
+
353
+ `benchmarks/` tracks the GPU-relevant hot paths. Baselines are committed under
354
+ `benchmarks/baselines/`. The current reference is `0003` (`v2_0`) on the
355
+ reference machine: RTX 4070 Ti, Ryzen 7 7800X3D, WSL2; `0002` (`pre_v2`) is
356
+ the 1.x state. `0002` ran the CPR equalizer and Viterbi-Viterbi benchmarks
357
+ on workloads that did not converge, so compare those only from `0003` on.
358
+ - **Workloads must converge.** A workload the algorithm cannot handle times
359
+ a failure mode (slip storms, divergence), not the operating point. Every
360
+ equalizer benchmark asserts its symbol error rate with
361
+ `benchutils.assert_converged`.
362
+ - **Baselines** are recorded with `benchmarks/record_baseline.py`: each file
363
+ in its own process, best of three passes. In one full-suite process, small
364
+ GPU benchmarks after the large equalizer workloads ran 2-12x slower than
365
+ alone, so never record a baseline from a single `pytest benchmarks/` run.
366
+
367
+ - **IDs** are `[cpu]` or `[gpu]`, meaning the input device.
368
+ - **Timing:**
369
+ - Timed bodies end with the `sync` fixture; otherwise GPU timings measure
370
+ launches, not execution.
371
+ - Each benchmark has one warmup round, so compilation and pool growth are
372
+ excluded.
373
+ - **Workloads** come from `benchmarks/workloads.py` with fixed seeds; never
374
+ generate data inline.
375
+ - **Tools:** `benchmarks/benchutils.py` provides `CudaEventTimer` and
376
+ `nvtx_range` for `nsys` profiling.
377
+ - **Logging** is set to WARNING in `benchmarks/conftest.py`.
378
+ - **Trust deltas, not single runs.** A ±20-40% swing has been seen under load.
379
+ Re-run a suspected regression in isolation, or do a controlled A/B with at
380
+ least 7 repetitions, alternating old and new per repetition and comparing
381
+ the paired ratios. When even unchanged benchmarks move, time the changed
382
+ part itself with `CudaEventTimer` and quote it against the step it sits in.
383
+ - A commit touching a hot path quotes its delta against `0003`. Tolerance is
384
+ 5% on CPU and 10% on GPU.
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 lokgar
3
+ Copyright (c) 2026 Denis Fatkhiev
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal