commkit 2.2.0__tar.gz → 3.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.
- {commkit-2.2.0 → commkit-3.0.0}/.github/workflows/ci.yml +12 -2
- {commkit-2.2.0 → commkit-3.0.0}/AGENTS.md +123 -43
- commkit-3.0.0/CHANGELOG.md +161 -0
- {commkit-2.2.0 → commkit-3.0.0}/PKG-INFO +13 -13
- {commkit-2.2.0 → commkit-3.0.0}/README.md +12 -12
- commkit-3.0.0/benchmarks/baselines/Linux-CPython-3.14-64bit/0005_gpu_decoder_path.json +4458 -0
- commkit-3.0.0/benchmarks/baselines/Linux-CPython-3.14-64bit/0006_perf_cleanup.json +5054 -0
- commkit-3.0.0/benchmarks/baselines/Linux-CPython-3.14-64bit/0007_v3_0.json +5394 -0
- {commkit-2.2.0 → commkit-3.0.0}/benchmarks/bench_block_blind.py +1 -1
- {commkit-2.2.0 → commkit-3.0.0}/benchmarks/bench_block_lms.py +3 -2
- commkit-3.0.0/benchmarks/bench_coding.py +64 -0
- commkit-3.0.0/benchmarks/bench_filters.py +41 -0
- {commkit-2.2.0 → commkit-3.0.0}/benchmarks/bench_llr.py +16 -0
- commkit-3.0.0/benchmarks/bench_mapping.py +47 -0
- {commkit-2.2.0 → commkit-3.0.0}/benchmarks/bench_sync_misc.py +43 -2
- {commkit-2.2.0 → commkit-3.0.0}/benchmarks/conftest.py +25 -0
- {commkit-2.2.0 → commkit-3.0.0}/benchmarks/workloads.py +44 -8
- {commkit-2.2.0 → commkit-3.0.0}/commkit/__init__.py +28 -15
- {commkit-2.2.0 → commkit-3.0.0}/commkit/_array.py +30 -27
- {commkit-2.2.0 → commkit-3.0.0}/commkit/_circular.py +3 -5
- commkit-3.0.0/commkit/_cuda/__init__.py +745 -0
- {commkit-2.2.0 → commkit-3.0.0}/commkit/_cuda/compiler.py +11 -12
- {commkit-2.2.0 → commkit-3.0.0}/commkit/_cuda/graphs.py +25 -28
- {commkit-2.2.0 → commkit-3.0.0}/commkit/_cuda/src/bps_min_d2.cu +36 -33
- commkit-3.0.0/commkit/_cuda/src/iir.cu +222 -0
- commkit-3.0.0/commkit/_cuda/src/ldpc.cu +156 -0
- commkit-3.0.0/commkit/_cuda/src/llr.cu +337 -0
- {commkit-2.2.0 → commkit-3.0.0}/commkit/_dispersion.py +10 -6
- {commkit-2.2.0 → commkit-3.0.0}/commkit/_fft.py +13 -6
- {commkit-2.2.0 → commkit-3.0.0}/commkit/_fiber.py +82 -149
- {commkit-2.2.0 → commkit-3.0.0}/commkit/_overlap_save.py +13 -15
- {commkit-2.2.0 → commkit-3.0.0}/commkit/_random.py +13 -9
- {commkit-2.2.0 → commkit-3.0.0}/commkit/_sequences.py +25 -25
- commkit-3.0.0/commkit/analysis/__init__.py +65 -0
- {commkit-2.2.0 → commkit-3.0.0}/commkit/analysis/_common.py +11 -10
- {commkit-2.2.0 → commkit-3.0.0}/commkit/analysis/allan.py +30 -28
- {commkit-2.2.0 → commkit-3.0.0}/commkit/analysis/drift.py +48 -43
- {commkit-2.2.0 → commkit-3.0.0}/commkit/analysis/fm_noise.py +64 -62
- {commkit-2.2.0 → commkit-3.0.0}/commkit/analysis/interferometry.py +99 -97
- commkit-3.0.0/commkit/analysis/linewidth/__init__.py +138 -0
- commkit-3.0.0/commkit/analysis/linewidth/dsh.py +624 -0
- commkit-3.0.0/commkit/analysis/linewidth/phase.py +381 -0
- commkit-3.0.0/commkit/analysis/linewidth/result.py +141 -0
- commkit-3.0.0/commkit/analysis/trajectory.py +113 -0
- {commkit-2.2.0 → commkit-3.0.0}/commkit/backend.py +97 -63
- commkit-3.0.0/commkit/coding/__init__.py +76 -0
- commkit-3.0.0/commkit/coding/_galois.py +139 -0
- commkit-3.0.0/commkit/coding/_gf2.py +170 -0
- commkit-3.0.0/commkit/coding/_ldpc_tables.py +1590 -0
- commkit-3.0.0/commkit/coding/_numba_loops.py +639 -0
- commkit-3.0.0/commkit/coding/algebraic.py +515 -0
- commkit-3.0.0/commkit/coding/base.py +313 -0
- commkit-3.0.0/commkit/coding/convolutional.py +287 -0
- commkit-3.0.0/commkit/coding/crc.py +244 -0
- commkit-3.0.0/commkit/coding/interleaving.py +178 -0
- commkit-3.0.0/commkit/coding/ldpc.py +568 -0
- commkit-3.0.0/commkit/coding/linear.py +490 -0
- commkit-3.0.0/commkit/coding/polar.py +325 -0
- commkit-3.0.0/commkit/coding/product.py +544 -0
- commkit-3.0.0/commkit/coding/ratematch.py +212 -0
- commkit-3.0.0/commkit/coding/shaping.py +442 -0
- commkit-3.0.0/commkit/coding/turbo.py +220 -0
- commkit-3.0.0/commkit/core/__init__.py +23 -0
- {commkit-2.2.0 → commkit-3.0.0}/commkit/core/_signal_adapter.py +10 -3
- {commkit-2.2.0 → commkit-3.0.0}/commkit/core/frame.py +195 -235
- {commkit-2.2.0 → commkit-3.0.0}/commkit/core/generation.py +109 -34
- {commkit-2.2.0 → commkit-3.0.0}/commkit/core/signal.py +126 -40
- commkit-3.0.0/commkit/equalization/__init__.py +55 -0
- commkit-3.0.0/commkit/equalization/_block/__init__.py +18 -0
- {commkit-2.2.0 → commkit-3.0.0}/commkit/equalization/_block/_blind.py +44 -11
- {commkit-2.2.0 → commkit-3.0.0}/commkit/equalization/_block/_dd.py +207 -215
- {commkit-2.2.0 → commkit-3.0.0}/commkit/equalization/_block/_engine.py +133 -30
- {commkit-2.2.0 → commkit-3.0.0}/commkit/equalization/_common.py +129 -70
- commkit-3.0.0/commkit/equalization/_kernels_numba.py +48 -0
- commkit-3.0.0/commkit/equalization/_numba_loops.py +845 -0
- commkit-3.0.0/commkit/equalization/blind.py +338 -0
- {commkit-2.2.0 → commkit-3.0.0}/commkit/equalization/linear.py +91 -87
- {commkit-2.2.0 → commkit-3.0.0}/commkit/equalization/polarization.py +283 -263
- {commkit-2.2.0 → commkit-3.0.0}/commkit/equalization/result.py +52 -52
- commkit-3.0.0/commkit/equalization/sequential/__init__.py +19 -0
- commkit-3.0.0/commkit/equalization/sequential/_blind.py +464 -0
- commkit-3.0.0/commkit/equalization/sequential/_dd.py +675 -0
- {commkit-2.2.0 → commkit-3.0.0}/commkit/equalization/sequential/_setup.py +30 -21
- commkit-3.0.0/commkit/filtering/__init__.py +75 -0
- commkit-3.0.0/commkit/filtering/fiber.py +204 -0
- commkit-3.0.0/commkit/filtering/linear.py +808 -0
- commkit-3.0.0/commkit/filtering/pulses.py +498 -0
- {commkit-2.2.0 → commkit-3.0.0}/commkit/frequency.py +137 -123
- commkit-3.0.0/commkit/impairments/__init__.py +39 -0
- {commkit-2.2.0 → commkit-3.0.0}/commkit/impairments/channel/__init__.py +3 -4
- {commkit-2.2.0 → commkit-3.0.0}/commkit/impairments/channel/linear.py +59 -84
- {commkit-2.2.0 → commkit-3.0.0}/commkit/impairments/channel/nonlinear.py +9 -11
- {commkit-2.2.0 → commkit-3.0.0}/commkit/impairments/frontend.py +74 -65
- {commkit-2.2.0 → commkit-3.0.0}/commkit/impairments/noise.py +17 -21
- {commkit-2.2.0 → commkit-3.0.0}/commkit/impairments/source.py +47 -43
- {commkit-2.2.0 → commkit-3.0.0}/commkit/io.py +100 -51
- {commkit-2.2.0 → commkit-3.0.0}/commkit/logger.py +10 -13
- {commkit-2.2.0 → commkit-3.0.0}/commkit/mapping/__init__.py +6 -7
- commkit-3.0.0/commkit/mapping/_numba_llr.py +222 -0
- {commkit-2.2.0 → commkit-3.0.0}/commkit/mapping/bits.py +10 -13
- {commkit-2.2.0 → commkit-3.0.0}/commkit/mapping/constellation.py +58 -43
- {commkit-2.2.0 → commkit-3.0.0}/commkit/mapping/gray.py +122 -119
- commkit-3.0.0/commkit/mapping/llr.py +397 -0
- {commkit-2.2.0 → commkit-3.0.0}/commkit/mapping/shaping.py +17 -13
- {commkit-2.2.0 → commkit-3.0.0}/commkit/math.py +105 -109
- {commkit-2.2.0 → commkit-3.0.0}/commkit/metrics.py +247 -84
- {commkit-2.2.0 → commkit-3.0.0}/commkit/multirate.py +34 -76
- {commkit-2.2.0 → commkit-3.0.0}/commkit/plotting/__init__.py +16 -8
- {commkit-2.2.0 → commkit-3.0.0}/commkit/plotting/analysis.py +101 -72
- {commkit-2.2.0 → commkit-3.0.0}/commkit/plotting/constellation.py +38 -54
- {commkit-2.2.0 → commkit-3.0.0}/commkit/plotting/equalizer.py +39 -28
- {commkit-2.2.0 → commkit-3.0.0}/commkit/plotting/eye.py +55 -71
- {commkit-2.2.0 → commkit-3.0.0}/commkit/plotting/filter_response.py +36 -42
- {commkit-2.2.0 → commkit-3.0.0}/commkit/plotting/spectral.py +58 -125
- {commkit-2.2.0 → commkit-3.0.0}/commkit/plotting/sync.py +371 -752
- {commkit-2.2.0 → commkit-3.0.0}/commkit/plotting/theme.py +61 -26
- {commkit-2.2.0 → commkit-3.0.0}/commkit/plotting/waveform.py +25 -42
- {commkit-2.2.0 → commkit-3.0.0}/commkit/recovery/__init__.py +12 -9
- {commkit-2.2.0 → commkit-3.0.0}/commkit/recovery/_common.py +66 -39
- {commkit-2.2.0 → commkit-3.0.0}/commkit/recovery/bps.py +87 -50
- {commkit-2.2.0 → commkit-3.0.0}/commkit/recovery/carrier_phase.py +15 -14
- {commkit-2.2.0 → commkit-3.0.0}/commkit/recovery/corrections.py +145 -131
- {commkit-2.2.0 → commkit-3.0.0}/commkit/recovery/pilots.py +76 -60
- {commkit-2.2.0 → commkit-3.0.0}/commkit/recovery/pll.py +119 -115
- {commkit-2.2.0 → commkit-3.0.0}/commkit/recovery/tikhonov.py +46 -42
- {commkit-2.2.0 → commkit-3.0.0}/commkit/recovery/viterbi_viterbi.py +8 -8
- commkit-3.0.0/commkit/smoothing.py +148 -0
- {commkit-2.2.0 → commkit-3.0.0}/commkit/spectral.py +119 -141
- {commkit-2.2.0 → commkit-3.0.0}/commkit/timing.py +143 -160
- {commkit-2.2.0 → commkit-3.0.0}/examples/01-constellations-and-bit-mapping.ipynb +8 -5
- {commkit-2.2.0 → commkit-3.0.0}/examples/02-spectral-analysis.ipynb +2 -2
- {commkit-2.2.0 → commkit-3.0.0}/examples/04-pulse-shaping-and-matched-filtering.ipynb +1 -1
- {commkit-2.2.0 → commkit-3.0.0}/examples/06-awgn-and-performance-metrics.ipynb +1 -1
- commkit-3.0.0/examples/07-channel-coding.ipynb +412 -0
- commkit-3.0.0/examples/08-probabilistic-shaping-and-coded-modulation.ipynb +278 -0
- commkit-2.2.0/examples/07-laser-phase-noise.ipynb → commkit-3.0.0/examples/09-laser-phase-noise.ipynb +4 -4
- commkit-2.2.0/examples/08-front-end-impairments.ipynb → commkit-3.0.0/examples/10-front-end-impairments.ipynb +6 -6
- commkit-2.2.0/examples/09-frame-synchronization-and-timing.ipynb → commkit-3.0.0/examples/11-frame-synchronization-and-timing.ipynb +3 -3
- commkit-2.2.0/examples/10-frequency-offset-estimation.ipynb → commkit-3.0.0/examples/12-frequency-offset-estimation.ipynb +4 -4
- commkit-2.2.0/examples/11-blind-carrier-phase-recovery.ipynb → commkit-3.0.0/examples/13-blind-carrier-phase-recovery.ipynb +5 -5
- commkit-2.2.0/examples/12-pilot-aided-carrier-recovery.ipynb → commkit-3.0.0/examples/14-pilot-aided-carrier-recovery.ipynb +5 -5
- commkit-2.2.0/examples/13-linear-channel-equalization.ipynb → commkit-3.0.0/examples/15-linear-channel-equalization.ipynb +3 -3
- commkit-2.2.0/examples/14-adaptive-equalization.ipynb → commkit-3.0.0/examples/16-adaptive-equalization.ipynb +8 -8
- commkit-2.2.0/examples/15-blind-equalization.ipynb → commkit-3.0.0/examples/17-blind-equalization.ipynb +3 -3
- commkit-2.2.0/examples/16-polarization-multiplexing.ipynb → commkit-3.0.0/examples/18-polarization-multiplexing.ipynb +5 -5
- commkit-2.2.0/examples/17-chromatic-dispersion.ipynb → commkit-3.0.0/examples/19-chromatic-dispersion.ipynb +3 -3
- commkit-2.2.0/examples/18-nonlinear-fiber-and-backpropagation.ipynb → commkit-3.0.0/examples/20-nonlinear-fiber-and-backpropagation.ipynb +3 -3
- commkit-3.0.0/examples/21-fec-in-optical-standards.ipynb +317 -0
- commkit-2.2.0/examples/19-coherent-transceiver-end-to-end.ipynb → commkit-3.0.0/examples/22-coherent-transceiver-end-to-end.ipynb +61 -13
- commkit-2.2.0/examples/20-carrier-phase-characterization.ipynb → commkit-3.0.0/examples/23-carrier-phase-characterization.ipynb +4 -4
- commkit-2.2.0/examples/21-laser-linewidth-self-heterodyne.ipynb → commkit-3.0.0/examples/24-laser-linewidth-self-heterodyne.ipynb +11 -11
- commkit-2.2.0/examples/22-laser-linewidth-short-delay-and-flicker.ipynb → commkit-3.0.0/examples/25-laser-linewidth-short-delay-and-flicker.ipynb +5 -5
- commkit-2.2.0/examples/23-laser-linewidth-self-homodyne-iq.ipynb → commkit-3.0.0/examples/26-laser-linewidth-self-homodyne-iq.ipynb +5 -5
- commkit-2.2.0/examples/24-measurement-template-self-heterodyne.ipynb → commkit-3.0.0/examples/27-measurement-template-self-heterodyne.ipynb +2 -2
- commkit-2.2.0/examples/25-measurement-template-self-homodyne-iq.ipynb → commkit-3.0.0/examples/28-measurement-template-self-homodyne-iq.ipynb +2 -2
- {commkit-2.2.0 → commkit-3.0.0}/examples/README.md +22 -19
- {commkit-2.2.0 → commkit-3.0.0}/pyproject.toml +7 -15
- commkit-3.0.0/tests/coding/test_algebraic.py +267 -0
- commkit-3.0.0/tests/coding/test_coding_base.py +116 -0
- commkit-3.0.0/tests/coding/test_convolutional.py +113 -0
- commkit-3.0.0/tests/coding/test_crc.py +76 -0
- commkit-3.0.0/tests/coding/test_gf2.py +116 -0
- commkit-3.0.0/tests/coding/test_interleaving.py +61 -0
- commkit-3.0.0/tests/coding/test_ldpc.py +279 -0
- commkit-3.0.0/tests/coding/test_ldpc_kernel.py +75 -0
- commkit-3.0.0/tests/coding/test_ldpc_tables.py +64 -0
- commkit-3.0.0/tests/coding/test_linear_codes.py +226 -0
- commkit-3.0.0/tests/coding/test_polar.py +141 -0
- commkit-3.0.0/tests/coding/test_product.py +238 -0
- commkit-3.0.0/tests/coding/test_ratematch.py +111 -0
- commkit-3.0.0/tests/coding/test_shaping.py +231 -0
- commkit-3.0.0/tests/coding/test_turbo.py +115 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/common/__init__.py +2 -4
- commkit-3.0.0/tests/common/coding.py +45 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/common/kernel_utils.py +5 -41
- {commkit-2.2.0 → commkit-3.0.0}/tests/common/reference_impl.py +61 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/conftest.py +1 -19
- {commkit-2.2.0 → commkit-3.0.0}/tests/core/test_frame.py +68 -11
- {commkit-2.2.0 → commkit-3.0.0}/tests/core/test_generation.py +76 -6
- {commkit-2.2.0 → commkit-3.0.0}/tests/core/test_signal.py +59 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/equalization/test_blind.py +46 -2
- {commkit-2.2.0 → commkit-3.0.0}/tests/equalization/test_block.py +47 -26
- {commkit-2.2.0 → commkit-3.0.0}/tests/equalization/test_bps_kernel.py +81 -54
- commkit-3.0.0/tests/equalization/test_cs_kernel.py +144 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/equalization/test_polarization.py +24 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/equalization/test_sequential.py +12 -8
- {commkit-2.2.0 → commkit-3.0.0}/tests/equalization/test_state.py +3 -1
- {commkit-2.2.0 → commkit-3.0.0}/tests/equalization/test_winit.py +32 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/impairments/channel/test_channel_nonlinear.py +35 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/mapping/test_constellation.py +48 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/mapping/test_gray.py +62 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/mapping/test_llr.py +210 -42
- {commkit-2.2.0 → commkit-3.0.0}/tests/plotting/test_plot_equalizer.py +3 -2
- {commkit-2.2.0 → commkit-3.0.0}/tests/plotting/test_plot_eye.py +14 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/plugins/precision_trace.py +24 -2
- commkit-3.0.0/tests/plugins/transfer_trace.py +171 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/recovery/test_corrections.py +20 -4
- {commkit-2.2.0 → commkit-3.0.0}/tests/test_api_contracts.py +135 -38
- {commkit-2.2.0 → commkit-3.0.0}/tests/test_cuda_infra.py +20 -49
- {commkit-2.2.0 → commkit-3.0.0}/tests/test_fiber.py +0 -39
- {commkit-2.2.0 → commkit-3.0.0}/tests/test_filtering.py +125 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/test_frequency.py +20 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/test_import_hygiene.py +7 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/test_io.py +51 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/test_math.py +18 -5
- {commkit-2.2.0 → commkit-3.0.0}/tests/test_metrics.py +126 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/test_smoothing.py +25 -0
- {commkit-2.2.0 → commkit-3.0.0}/uv.lock +2 -2
- commkit-2.2.0/commkit/_cuda/__init__.py +0 -467
- commkit-2.2.0/commkit/_cuda/src/llr.cu +0 -110
- commkit-2.2.0/commkit/analysis/__init__.py +0 -78
- commkit-2.2.0/commkit/analysis/linewidth.py +0 -1206
- commkit-2.2.0/commkit/analysis/trajectory.py +0 -112
- commkit-2.2.0/commkit/coding/__init__.py +0 -23
- commkit-2.2.0/commkit/coding/base.py +0 -17
- commkit-2.2.0/commkit/coding/bch.py +0 -6
- commkit-2.2.0/commkit/coding/convolutional.py +0 -7
- commkit-2.2.0/commkit/coding/crc.py +0 -7
- commkit-2.2.0/commkit/coding/galois.py +0 -8
- commkit-2.2.0/commkit/coding/hamming.py +0 -6
- commkit-2.2.0/commkit/coding/interleaving.py +0 -7
- commkit-2.2.0/commkit/coding/ldpc.py +0 -8
- commkit-2.2.0/commkit/coding/polar.py +0 -8
- commkit-2.2.0/commkit/coding/ratematch.py +0 -6
- commkit-2.2.0/commkit/coding/reed_solomon.py +0 -6
- commkit-2.2.0/commkit/coding/turbo.py +0 -8
- commkit-2.2.0/commkit/core/__init__.py +0 -24
- commkit-2.2.0/commkit/equalization/__init__.py +0 -46
- commkit-2.2.0/commkit/equalization/_block/__init__.py +0 -24
- commkit-2.2.0/commkit/equalization/_kernels_numba.py +0 -1748
- commkit-2.2.0/commkit/equalization/blind.py +0 -252
- commkit-2.2.0/commkit/equalization/sequential/__init__.py +0 -21
- commkit-2.2.0/commkit/equalization/sequential/_blind.py +0 -494
- commkit-2.2.0/commkit/equalization/sequential/_dd.py +0 -732
- commkit-2.2.0/commkit/filtering.py +0 -1351
- commkit-2.2.0/commkit/impairments/__init__.py +0 -44
- commkit-2.2.0/commkit/mapping/llr.py +0 -417
- commkit-2.2.0/commkit/smoothing.py +0 -133
- commkit-2.2.0/tests/equalization/test_cs_kernel.py +0 -116
- {commkit-2.2.0 → commkit-3.0.0}/.gitattributes +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/.github/workflows/publish.yml +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/.gitignore +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/.python-version +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/LICENSE +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/benchmarks/baselines/Linux-CPython-3.14-64bit/0002_pre_v2.json +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/benchmarks/baselines/Linux-CPython-3.14-64bit/0003_v2_0.json +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/benchmarks/baselines/Linux-CPython-3.14-64bit/0004_fiber_dbp.json +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/benchmarks/bench_bps.py +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/benchmarks/bench_equalizers.py +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/benchmarks/bench_fiber.py +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/benchmarks/bench_import.py +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/benchmarks/bench_signal_pipeline.py +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/benchmarks/benchutils.py +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/benchmarks/record_baseline.py +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/commkit/_cuda/src/bps_anchor.cu +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/commkit/_cuda/src/cs_block.cu +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/commkit/_cuda/src/selftest.cu +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/commkit/py.typed +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/examples/00-getting-started.ipynb +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/examples/03-digital-filters.ipynb +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/examples/05-multirate-and-resampling.ipynb +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/analysis/test_allan.py +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/analysis/test_drift.py +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/analysis/test_fm_noise.py +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/analysis/test_interferometry.py +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/analysis/test_linewidth.py +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/analysis/test_trajectory.py +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/common/conversions.py +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/common/metrics.py +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/common/periodic.py +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/common/signals.py +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/core/test_psqam.py +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/core/test_signal_adapter.py +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/core/test_signal_mimo.py +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/equalization/test_cpr.py +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/equalization/test_equalizer_oracles.py +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/equalization/test_linear.py +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/equalization/test_mimo.py +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/impairments/channel/test_channel_linear.py +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/impairments/test_frontend.py +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/impairments/test_noise.py +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/impairments/test_source.py +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/mapping/test_bits.py +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/plotting/test_plot_analysis.py +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/plotting/test_plot_constellation.py +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/plotting/test_plot_filter_response.py +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/plotting/test_plot_spectral.py +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/plotting/test_plot_sync.py +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/plotting/test_plot_theme.py +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/plotting/test_plot_waveform.py +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/plugins/__init__.py +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/recovery/test_bps.py +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/recovery/test_joint.py +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/recovery/test_pilots.py +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/recovery/test_pll.py +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/recovery/test_recovery_oracles.py +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/recovery/test_tikhonov.py +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/recovery/test_viterbi_viterbi.py +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/test_array.py +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/test_backend.py +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/test_dispersion.py +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/test_examples.py +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/test_logger.py +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/test_multirate.py +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/test_pulse_shaping.py +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/test_signal_pipeline.py +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/test_spectral.py +0 -0
- {commkit-2.2.0 → commkit-3.0.0}/tests/test_timing.py +0 -0
|
@@ -110,12 +110,22 @@ jobs:
|
|
|
110
110
|
- name: Run kernel tests with JIT disabled
|
|
111
111
|
env:
|
|
112
112
|
NUMBA_DISABLE_JIT: "1"
|
|
113
|
-
run:
|
|
113
|
+
run: >-
|
|
114
|
+
uv run pytest tests/equalization tests/recovery tests/test_frequency.py tests/mapping/test_llr.py
|
|
115
|
+
tests/coding/test_ldpc.py tests/coding/test_shaping.py::TestCCDM
|
|
116
|
+
tests/coding/test_polar.py::TestEncoding
|
|
117
|
+
tests/coding/test_polar.py::TestDecoding::test_sc_matches_the_recursive_reference
|
|
118
|
+
tests/coding/test_polar.py::TestDecoding::test_full_list_is_maximum_likelihood
|
|
119
|
+
tests/coding/test_polar.py::TestDecoding::test_crc_failure_keeps_the_best_path
|
|
120
|
+
tests/coding/test_convolutional.py tests/coding/test_turbo.py::TestEncoding
|
|
121
|
+
tests/coding/test_turbo.py::TestDecoding::test_noiseless
|
|
122
|
+
-k "not waterfall and not threshold"
|
|
123
|
+
--device=cpu --cov=commkit --cov-report=
|
|
114
124
|
|
|
115
125
|
- name: Kernel coverage report
|
|
116
126
|
run: >-
|
|
117
127
|
uv run coverage report --show-missing --fail-under=85
|
|
118
|
-
--include="commkit/equalization/_kernels_numba.py,commkit/equalization/sequential/*,commkit/frequency.py,commkit/recovery
|
|
128
|
+
--include="commkit/equalization/_kernels_numba.py,commkit/equalization/_numba_loops.py,commkit/equalization/sequential/*,commkit/frequency.py,commkit/recovery/*,commkit/coding/ldpc.py,commkit/coding/shaping.py,commkit/coding/polar.py,commkit/coding/convolutional.py,commkit/coding/turbo.py"
|
|
119
129
|
|
|
120
130
|
# GPU legs (--device=gpu / --device=all) require CuPy + CUDA and are run
|
|
121
131
|
# manually or on a self-hosted runner; they are intentionally not part of
|
|
@@ -36,11 +36,54 @@ uv run pytest --device=cpu # what CI runs
|
|
|
36
36
|
uv run pytest tests/equalization/ -k lms # subset
|
|
37
37
|
uv run pytest --cov=commkit # coverage
|
|
38
38
|
NUMBA_DISABLE_JIT=1 uv run pytest tests/equalization tests/recovery \
|
|
39
|
-
tests/test_frequency.py tests/
|
|
39
|
+
tests/test_frequency.py tests/mapping/test_llr.py tests/coding/test_ldpc.py tests/coding/test_shaping.py::TestCCDM \
|
|
40
|
+
tests/coding/test_polar.py::TestEncoding "tests/coding/test_polar.py::TestDecoding::test_sc_matches_the_recursive_reference" \
|
|
41
|
+
"tests/coding/test_polar.py::TestDecoding::test_full_list_is_maximum_likelihood" \
|
|
42
|
+
"tests/coding/test_polar.py::TestDecoding::test_crc_failure_keeps_the_best_path" \
|
|
43
|
+
tests/coding/test_convolutional.py tests/coding/test_turbo.py::TestEncoding \
|
|
44
|
+
tests/coding/test_turbo.py::TestDecoding::test_noiseless \
|
|
45
|
+
-k "not waterfall and not threshold" \
|
|
46
|
+
--device=cpu --cov=commkit # real kernel coverage
|
|
40
47
|
uv run pytest --device=cpu -p tests.plugins.precision_trace \
|
|
41
48
|
--ignore=tests/test_examples.py # precision audit (see Numerics); the suite passes under it
|
|
49
|
+
uv run pytest --device=gpu -p tests.plugins.transfer_trace \
|
|
50
|
+
--ignore=tests/test_examples.py # transfer audit (see Performance); fails on an undocumented transfer
|
|
42
51
|
```
|
|
43
52
|
|
|
53
|
+
**CUDA sanitizer** (see Kernel policy): NVIDIA's `compute-sanitizer` checks the kernels for out-of-bounds and misaligned accesses (`memcheck`), shared-memory races (`racecheck`), barrier misuse (`synccheck`) and reads of uninitialized device memory (`initcheck`). It is not a dependency: install its wheel, matching the CUDA major version of `cupy-cuda13x`, into a scratch directory and run the GPU tests of the kernels under each tool (10-100x slower; the whole pass takes about 50 minutes on the reference machine):
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
uv pip install --target /tmp/sanitizer "nvidia-cuda-sanitizer-api==13.*"
|
|
57
|
+
S=/tmp/sanitizer/nvidia/cu13/bin/compute-sanitizer
|
|
58
|
+
OWN=(--kernel-name kns=commkit_ --kernel-name kns=bps_ --kernel-name kns=cs_block \
|
|
59
|
+
--kernel-name kns=llr --kernel-name kns=iir_ --kernel-name kns=selftest --kernel-name kns=ldpc)
|
|
60
|
+
FILES="tests/test_cuda_infra.py tests/equalization/test_cs_kernel.py tests/equalization/test_bps_kernel.py
|
|
61
|
+
tests/equalization/test_blind.py tests/mapping/test_llr.py tests/mapping/test_gray.py tests/recovery/test_bps.py
|
|
62
|
+
tests/test_filtering.py tests/test_fiber.py tests/impairments/channel/test_channel_nonlinear.py
|
|
63
|
+
tests/coding/test_ldpc_kernel.py"
|
|
64
|
+
BLOCK=$(.venv/bin/python -m pytest --device=gpu -q --collect-only tests/equalization/test_block.py | grep '::')
|
|
65
|
+
for tool in memcheck racecheck synccheck initcheck; do
|
|
66
|
+
filter=(); [[ $tool == racecheck || $tool == synccheck ]] && filter=("${OWN[@]}")
|
|
67
|
+
for target in $FILES $BLOCK; do
|
|
68
|
+
"$S" --tool $tool "${filter[@]}" .venv/bin/python -m pytest --device=gpu -p no:cacheprovider -q "$target" \
|
|
69
|
+
2>&1 | grep -E "SUMMARY|passed|failed"
|
|
70
|
+
done
|
|
71
|
+
done
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
- **One process per file, and one per test for `test_block.py`.** Under the sanitizer each CUDA-graph test of the block equalizers gets slower the more tests ran before it in the process (one took 10 minutes after 26 others, 61 s alone).
|
|
75
|
+
- **`racecheck` and `synccheck` check commkit's kernels only** (`OWN`: the `.cu` kernels and the `commkit_*` ElementwiseKernels). Unfiltered, racecheck tracks every CuPy and cuFFT launch of the 100k-symbol graph tests and grows by about 0.1 GB of host memory per second until the machine runs out. Those two tools concern shared memory and barriers, which only the `.cu` kernels use; `memcheck` and `initcheck` run unfiltered, as a CuPy kernel reading a buffer commkit never filled is a commkit bug.
|
|
76
|
+
- **No `--leak-check full`.** CuPy's pool holds device memory until exit, so the check finds nothing in commkit, and with it every CuPy process exits with SIGSEGV after the summary (`import cupy as cp; cp.arange(3).sum()` alone does).
|
|
77
|
+
- **The wheel must be complete.** The 13.2 and 13.3 wheels lack the launcher helpers (`TreeLauncherSubreaper`, `TreeLauncherTargetLdPreloadHelper`) and stop every target, even `/bin/echo`, with "Target application terminated before first instrumented API call". The 13.4 wheel has them and runs on a 13.3 driver. If a driver needs an older version, take NVIDIA's redistributable archive (`developer.download.nvidia.com/compute/cuda/redist/cuda_sanitizer_api/`), which is complete.
|
|
78
|
+
|
|
79
|
+
Under WSL2 the sanitizer needs the Windows GPU debugger interface; without it every run stops at "Error: Device not supported" (older versions: "Failed to initialize WDDM debugger interface"). Enable it once from an elevated Windows prompt, then restart WSL (`wsl --shutdown`):
|
|
80
|
+
|
|
81
|
+
```bat
|
|
82
|
+
reg add "HKLM\SOFTWARE\NVIDIA Corporation\GPUDebugger" /v EnableInterface /t REG_DWORD /d 1 /f
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Windows also kills a kernel that runs longer than `TdrDelay` seconds (`HKLM\System\CurrentControlSet\Control\GraphicsDrivers`, 8 on the reference machine, which the whole pass stays under); raise it if a sanitized launch times out.
|
|
86
|
+
|
|
44
87
|
**Commit gate.** CI fails on any of these, so run all of them before every commit, with `ruff format` last:
|
|
45
88
|
|
|
46
89
|
```bash
|
|
@@ -61,16 +104,17 @@ Timed runs:
|
|
|
61
104
|
```bash
|
|
62
105
|
uv run pytest benchmarks/bench_bps.py --benchmark-only --device=all
|
|
63
106
|
uv run pytest benchmarks/ --benchmark-only --device=all \
|
|
64
|
-
--benchmark-compare=
|
|
107
|
+
--benchmark-compare=0007 --benchmark-storage=file://benchmarks/baselines
|
|
65
108
|
uv run python benchmarks/record_baseline.py NAME # record a new baseline
|
|
66
109
|
```
|
|
67
110
|
|
|
68
111
|
**Release:**
|
|
69
112
|
|
|
70
113
|
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. `
|
|
72
|
-
3. `
|
|
73
|
-
4.
|
|
114
|
+
2. `CHANGELOG.md`: a `## [X.Y.Z] - YYYY-MM-DD` section for the release, written for users. Changes that may need code updates come first, with how to migrate, then Added, Performance (measured, as a ratio of new to old time), Fixed and Development. Add its compare link at the bottom, and commit it.
|
|
115
|
+
3. `uv run bump-my-version bump {patch|minor|major}`. This commits the new version and tags it `vX.Y.Z`.
|
|
116
|
+
4. `git push origin main --tags`. The ruleset on `main` requires pull requests; repository admins bypass it for these two commits.
|
|
117
|
+
5. Publish a GitHub Release for the tag, with the release's changelog section as its notes (`gh release create vX.Y.Z --title vX.Y.Z --notes-file notes.md`). Publishing the release runs `.github/workflows/publish.yml`, which builds and uploads to PyPI through Trusted Publishing; pushing a tag alone publishes nothing.
|
|
74
118
|
|
|
75
119
|
---
|
|
76
120
|
|
|
@@ -82,7 +126,7 @@ Dependencies point downward only:
|
|
|
82
126
|
| --- | --- | --- |
|
|
83
127
|
| Infrastructure | `backend`, `logger`, `_cuda` | NumPy, CuPy |
|
|
84
128
|
| 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 |
|
|
129
|
+
| Value objects | `mapping.Constellation`, pulse classes, `recovery` CPR configs, `coding` (code objects with their `encode` / `decode`; array-only, never imports `core`) | layers above |
|
|
86
130
|
| Core | `core` (`Signal`, frames, generation), `io` | layers above |
|
|
87
131
|
| DSP | `mapping`, `filtering`, `multirate`, `spectral`, `smoothing`, `impairments`, `timing`, `frequency`, `recovery`, `equalization`, `metrics`, `analysis` | layers above, and lower DSP modules |
|
|
88
132
|
| Presentation | `plotting` | everything; **nothing imports plotting** |
|
|
@@ -93,7 +137,6 @@ Dependencies point downward only:
|
|
|
93
137
|
- `analysis` holds laser and phase characterization.
|
|
94
138
|
- `metrics` is the reporting layer.
|
|
95
139
|
- **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
140
|
- **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
141
|
|
|
99
142
|
---
|
|
@@ -102,7 +145,7 @@ Dependencies point downward only:
|
|
|
102
145
|
|
|
103
146
|
### Signal
|
|
104
147
|
|
|
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.
|
|
148
|
+
- `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, the bits on the air `bits` and the source bits `info_bits`, the same array when nothing is coded or matched) plus the `frame` layout.
|
|
106
149
|
- It never holds pipeline results.
|
|
107
150
|
- Construction does no hidden work: no mapping, no normalization, no device move, no shape guessing. Factories (`generate`, frames) do that work.
|
|
108
151
|
- `sig.to("gpu")` and `sig.replace(...)` return new Signals. Arrays are shared, not copied; use `clone()` for an independent copy.
|
|
@@ -139,7 +182,7 @@ Dependencies point downward only:
|
|
|
139
182
|
- **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
183
|
- **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
184
|
- **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.
|
|
185
|
+
- **Metrics** (`evm`, `snr`, `ber`, `fer`, `ser`, `gmi`, `ngmi`, `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
186
|
- **More than one value** is returned as a frozen dataclass with named fields. No dicts, and no tuples longer than two.
|
|
144
187
|
- **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
188
|
|
|
@@ -152,6 +195,7 @@ Dependencies point downward only:
|
|
|
152
195
|
| `correct_<quantity>(x, how)` | remove the impairment; returns only the corrected data. `how` is an estimate or an algorithm object (which estimates first) |
|
|
153
196
|
| `resolve_*` | pick among discrete candidates (π/2 rotation, channel permutation); nothing else |
|
|
154
197
|
| `generate` | synthesis from parameters, takes `rng` |
|
|
198
|
+
| `encode` / `decode` | information bits to a code's transmitted bits, and LLRs (or hard decisions) back to a `DecodeResult`; dispatched on the code object, the decoder chosen by a `method` object |
|
|
155
199
|
| `plot_*` | every public plotting function (only `apply_default_theme` is exempt) |
|
|
156
200
|
|
|
157
201
|
- 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_*`.
|
|
@@ -160,9 +204,23 @@ Dependencies point downward only:
|
|
|
160
204
|
- `num_taps`, `step_size`, `sps`, `constellation`, `pulse`, `rng`, `num_symbols`, `num_channels`, `sampling_rate`, `symbol_rate`.
|
|
161
205
|
- Units are in the docstring. Use `_db` and `_hz` suffixes where the unit is ambiguous.
|
|
162
206
|
|
|
207
|
+
### Docstrings
|
|
208
|
+
|
|
209
|
+
Docstrings describe the code as it is; a change in behavior updates them in the same commit. NumPy format (numpydoc), sections in its order: Parameters, Returns, Raises, Warns, See Also, Notes, References, Examples.
|
|
210
|
+
|
|
211
|
+
- **Summary line** on the line of the opening quotes (`"""Summary.`), one line, ending with a period.
|
|
212
|
+
- Functions: imperative mood (`Apply ...`, `Estimate ...`, `Plot ...`), or a noun phrase for a function named after the quantity it returns (`evm`, `allan_deviation`, `welch_psd`).
|
|
213
|
+
- Classes and modules: a noun phrase.
|
|
214
|
+
- **Module docstrings** say what the module holds, by public name (`:func:`, `:class:`), then the conventions particular to it (a sign convention, a scaling, a file layout). No marketing adjectives ("high-performance", "publication-quality"), no history of earlier layouts, and no restatement of the library-wide rules in this file. A private module's summary ends with "(private)" and the body names its users.
|
|
215
|
+
- **Types:** inputs are `array_like` (NumPy or CuPy), `array_like or Signal` for Signal-aware arguments; returned arrays and result fields are `ndarray` (NumPy or CuPy, on the input's device), `ndarray or Signal` for transforms, and `numpy.ndarray` where the array is always on the host. Shapes go in the description, in double backticks: ``(N,)``, ``(C, N)``.
|
|
216
|
+
- **Plots** return `(fig, ax) or None` (`None` with `show=True`).
|
|
217
|
+
- **Defaults:** `name : type, default value`; `name : type, optional` when the default is `None`, with the description saying what `None` does.
|
|
218
|
+
- Units go in the description; inline code in double backticks; references to library names through `:func:` / `:class:` / `:mod:` / `:meth:`.
|
|
219
|
+
|
|
163
220
|
### Errors and logging
|
|
164
221
|
|
|
165
222
|
- Wrong input raises, with a message naming the argument.
|
|
223
|
+
- **No fallbacks for a broken environment.** Code never catches a failure of CuPy, a CUDA kernel (compile, load, launch), a CUDA-graph capture or any other dependency in order to run a slower or different implementation; the failure raises to the caller. The kernels and graphs are tested on a GPU before every release (§2), a broken CuPy already fails when data moves to the GPU, and a path kept "just in case" is code that no test or user runs. Dispatch on the *input* is different and allowed: a kernel's documented limits (dtype, number of points, phases or channels) may route to the general implementation where that implementation exists anyway (the CPU's), and the dispatch says so where it happens. A GPU-only path that would serve only inputs beyond a kernel's limit is not written; such input raises (`compute_llr` above 16 bits per symbol).
|
|
166
224
|
- `logger` is for diagnostics only. Per-channel logging that needs a device-to-host transfer is wrapped in `if logger.isEnabledFor(logging.INFO):`.
|
|
167
225
|
- The library never configures handlers, Matplotlib or warning filters at import time.
|
|
168
226
|
|
|
@@ -174,12 +232,12 @@ Dependencies point downward only:
|
|
|
174
232
|
- **Precision contract.** Every function computes in a predictable precision, and none promotes single-precision data on its own:
|
|
175
233
|
- **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
234
|
- **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).
|
|
235
|
+
- **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 and state (poles near z = 1; the samples keep their precision on both devices), 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
236
|
- **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
237
|
- 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
238
|
- **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
239
|
- **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`.
|
|
240
|
+
- LMS/CMA/RDE: inputs and weights are `complex64`, but dot products and gradient updates in the hot loop are accumulated in `complex128`. The block equalizers compute in `complex64` throughout: their sums over the taps run inside the FFTs, and the sum over the C input channels between them is as short as C.
|
|
183
241
|
- 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
242
|
- 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
243
|
- **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.
|
|
@@ -192,10 +250,16 @@ Dependencies point downward only:
|
|
|
192
250
|
|
|
193
251
|
| Algorithm structure | Implementation | Device |
|
|
194
252
|
| --- | --- | --- |
|
|
195
|
-
| Vectorized: filters, resampling, spectra, impairments, metrics,
|
|
196
|
-
|
|
|
253
|
+
| Vectorized: filters, resampling, spectra, impairments, metrics, frequency-domain equalizers | NumPy/CuPy via `x, xp, sp = dispatch(samples)` | follows the input |
|
|
254
|
+
| Per-symbol searches over a small table: LLRs (over all points, or per axis for separable QAM; max-log without a prior in closed form, from the nearest levels of each bit's two values), hard decisions of constellations without a square lattice | CPU: Numba `parallel=True` over blocks of symbols (LLRs; the closed form from 16 levels per axis, below which the level metrics are as fast), chunked NumPy (decisions). GPU: CUDA kernels, one thread per symbol (`llr`, `llr_separable`, `llr_separable_maxlog` at every order, `bps_min_d2`'s nearest-point mode); decisions on double input or more than 1024 points run the chunked search on both devices, LLRs above 16 bits per symbol raise on the GPU | follows the input |
|
|
255
|
+
| Sequential recursions: LMS/RLS/CMA/RDE, PLL, Tikhonov, cycle slips, the Mengali-Morelli bootstrap over the lags, per-codeword decoders on trellises and trees (CCDM matching, polar SC/SCL, Viterbi, BCJR, turbo decoding, recursive systematic encoding; independent blocks in parallel) | Numba `@njit(cache=True, fastmath=True, nogil=True)`; independent rows in parallel (`prange`: the bootstrap's blocks). The equalizers' kernel with inline carrier recovery compiles strict (its unwrap and running sums) and calls its tap and distance loops as separate fast-math functions, which keep their flags when inlined (an `inline="always"` helper takes its caller's) | CPU; GPU input does one round trip to the host and back |
|
|
256
|
+
| Linear recursions: IIR filtering | CPU: Numba kernel without `fastmath`, so double input gives SciPy's result bit for bit. GPU: CUDA kernel (`iir`), blocks filtered in parallel and the state carried across them in two levels, in double precision | follows the input |
|
|
197
257
|
| Hot spots limited by intermediates, launches or round trips | CUDA C++ via CuPy `RawModule` (`commkit/_cuda`) | GPU |
|
|
198
|
-
|
|
|
258
|
+
| Binary linear algebra of codes: encoding, syndromes, the correlations of maximum-likelihood decoding, the evaluations of algebraic decoding (Chien search, Forney), Chase's test patterns through a hard decoder | one float32 GEMM per chunk (`coding._gf2.matmul_mod2`: BLAS, cuBLAS), exact below an inner dimension of 2^24; small tables (generators, coset leaders) cached per code object and device (`backend._device_tables`) | follows the input |
|
|
259
|
+
| Iterative decoding of many codewords: layered LDPC belief propagation (min-sum, sum-product) | CPU: Numba `parallel=True` over codewords, all layers and iterations inside, without fast-math. GPU: CUDA kernel (`ldpc_layered`), one thread block per codeword, the rows of a layer in parallel, early stop per codeword inside the kernel; the same rows in the same order on both, so min-sum agrees bit for bit (a CuPy version, about 12 launches per layer, was 7-23x slower) | follows the input |
|
|
260
|
+
| Elementwise chains between multi-core FFTs (the fiber solver's Kerr and PMD stages) | NumPy: a Numba `parallel=True` pass there competes with `scipy.fft`'s threads (the solver's Numba passes measured 1.1-1.8x slower end to end, 2^14-2^20); adopt one only on a paired end-to-end A/B | CPU |
|
|
261
|
+
|
|
262
|
+
**Numba kernels live at module level**, in a private module that a getter imports on first use (`equalization/_numba_loops.py`, `coding/_numba_loops.py`, `mapping/_numba_llr.py`), so `import commkit` does not load Numba and the disk cache (`cache=True`) keeps them across processes. A kernel defined inside a function that calls another kernel defined there captures that dispatcher in its closure, which Numba's cache key does not reproduce in a new process: it compiled again in every process (3 s for `compute_llr`, 6 s for polar list decoding). A getter may build a kernel in a closure that captures only constants (`_get_numba_sos`).
|
|
199
263
|
|
|
200
264
|
**Host-sync hygiene:**
|
|
201
265
|
|
|
@@ -205,18 +269,22 @@ Dependencies point downward only:
|
|
|
205
269
|
- 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
270
|
- Chunk `(N, M)` intermediates over N with an on-device accumulator.
|
|
207
271
|
- Reduce on the device before transferring, including for plotting.
|
|
272
|
+
- Build grids, windows and index tables on the data's device (`xp.fft.fftfreq`, `xp.hanning`, `xp.arange`), not on the host and then upload them.
|
|
273
|
+
- **Audit with `tests/plugins/transfer_trace.py`** after touching a GPU path. The contract registry checks only that a result comes back on the input's device, which a silent round trip through the host passes. The plugin records every host-device copy of sample-rate size that commkit makes during a `--device=gpu` run, with its function and lines, and fails on a site missing from its documented list. The documented sites are the round trip of the sequential recursions (table above), the equalizers' continuation state (host copies), the reporting layer (metrics, analysis summaries, plotting), explicit device moves and the transmitted payload, and host values (reference symbols, pilots, tables from host parameters such as a SciPy window) moved to the data's device. A new site is either fixed or added to the list with its reason.
|
|
208
274
|
- 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,
|
|
275
|
+
- 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; a failed capture raises. Per-replay offsets come from a device index inside the graph (the block equalizers' window, staging and stores read the block index and advance it), so a replay does all of a block's work and the host launches one graph per block. 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
276
|
|
|
211
277
|
**Kernel policy:**
|
|
212
278
|
|
|
213
279
|
- A `.cu` kernel needs a benchmark showing that CuPy is limited by intermediates, launch count or round trips.
|
|
280
|
+
- `_cuda.get_kernel` returns the launch wrapper or raises (compile, load or a bad specialization); it is called only on CuPy input.
|
|
214
281
|
- 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.
|
|
282
|
+
- Wrappers check dtype, contiguity, shape and size before launch, and never launch a zero-size grid (a launch error): empty input returns an empty result, or raises where the kernel's caller never has one (`cs_block`, `bps_anchor`). Grid dimensions y and z are limited to 65535: a kernel that maps channels or rows there walks the rest in a grid-stride loop (`bps_min_d2`).
|
|
283
|
+
- A new or changed kernel runs its GPU tests under `compute-sanitizer` (§2) before release.
|
|
216
284
|
- `float32` with explicit literals.
|
|
217
285
|
- `--use_fast_math` only per kernel (through `options`), after an accuracy check; the compiler default does not pass it.
|
|
218
286
|
- 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).
|
|
287
|
+
- Sequential recursions over a whole record are never ported to the GPU; one record has no parallel work per step. Linear recursions (IIR filters) are the exception: the `iir` kernel finds each block's state from a zero start in parallel, carries the states across blocks (per group of blocks, then across groups), and filters the blocks again from them. A short per-block scan may be, as one launch, when it replaces a host round trip per block (`cs_block`, which speculates its slip decisions in parallel, one pass per changed decision, about one per block on a realistic link; `bps_anchor`); keep its parallel part parallel (float64 is 1/64 of float32 throughput on consumer GPUs).
|
|
220
288
|
|
|
221
289
|
---
|
|
222
290
|
|
|
@@ -227,11 +295,11 @@ Dependencies point downward only:
|
|
|
227
295
|
- **Shared builders** live in `tests/common/` and use the public API.
|
|
228
296
|
- **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
297
|
- 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.
|
|
298
|
+
- **Oracles:** sequential kernels are tested against plain-Python reference implementations in `tests/common/reference_impl.py`. CUDA kernels are tested against their CPU reference. Kernel tests skip only on the CPU parametrization (`skip_unless_gpu`), never because a kernel is unavailable, so a kernel that fails to compile fails its tests.
|
|
231
299
|
- **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
300
|
- **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
301
|
- **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
|
|
302
|
+
- They form a course: one topic per notebook, named `NN-lowercase-words.ipynb`, numbered in teaching order (signals and DSP, channel and metrics, receiver algorithms, fiber systems, characterization). Each opens with what it covers, links the notebooks it builds on and ends with a link to the next. `examples/README.md` indexes them; a new notebook takes its place in the order and updates the index and the neighbors' links.
|
|
235
303
|
- 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
304
|
- Plots of hand-computed arrays go through `to_device(x, "cpu")`, so the notebooks run unchanged on the GPU.
|
|
237
305
|
- **Coverage** must not drop below the CI floor. Numba kernel coverage is measured by a separate `NUMBA_DISABLE_JIT=1` job.
|
|
@@ -240,20 +308,20 @@ Dependencies point downward only:
|
|
|
240
308
|
|
|
241
309
|
## 8. Benchmarks
|
|
242
310
|
|
|
243
|
-
`benchmarks/` tracks the GPU-relevant hot paths. Baselines are committed under `benchmarks/baselines/`. The current reference is `
|
|
311
|
+
`benchmarks/` tracks the GPU-relevant hot paths. Baselines are committed under `benchmarks/baselines/`. The current reference is `0007` (`v3_0`) on the reference machine: RTX 4070 Ti, Ryzen 7 7800X3D, WSL2. It is the first baseline with `bench_coding.py` (LDPC encoding and decoding, KP4 decoding), recorded at the 3.0 state with the session warmup of Numba's thread pool, best of five passes; `0006` (`perf_cleanup`) is the first with `bench_filters.py`, `bench_pll`, `bench_mengali_morelli_blockwise` and `bench_correct_iq_imbalance`, recorded after the performance pass of the IIR kernel and the block equalizers' in-graph block copies, best of five passes; `0005` (`gpu_decoder_path`) is the first with `bench_mapping.py` and `bench_compute_llr_nonseparable`, recorded after the GPU LLR, mapping and decision kernels; `0004` (`fiber_dbp`) 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
312
|
|
|
245
313
|
- **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.
|
|
314
|
+
- **Baselines** are recorded with `benchmarks/record_baseline.py`: each file in its own process, best of three passes (`--passes`; `0006` and `0007` used five, as three still kept outliers of 2-4x on a few small benchmarks). 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
315
|
|
|
248
316
|
- **IDs** are `[cpu]` or `[gpu]`, meaning the input device.
|
|
249
317
|
- **Timing:**
|
|
250
318
|
- 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.
|
|
319
|
+
- Each benchmark has one warmup round, so compilation and pool growth are excluded, and a session fixture keeps Numba's thread pool busy for half a second before the first: the first parallel calls of a process ran 4-8x slower while the OpenMP workers came up, which a kernel loaded from Numba's cache reaches in the timed rounds.
|
|
252
320
|
- **Workloads** come from `benchmarks/workloads.py` with fixed seeds; never generate data inline.
|
|
253
321
|
- **Tools:** `benchmarks/benchutils.py` provides `CudaEventTimer` and `nvtx_range` for `nsys` profiling.
|
|
254
322
|
- **Logging** is set to WARNING in `benchmarks/conftest.py`.
|
|
255
323
|
- **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 `
|
|
324
|
+
- A commit touching a hot path quotes its delta against `0007`. Tolerance is 5% on CPU and 10% on GPU.
|
|
257
325
|
|
|
258
326
|
---
|
|
259
327
|
|
|
@@ -261,9 +329,39 @@ Dependencies point downward only:
|
|
|
261
329
|
|
|
262
330
|
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
331
|
|
|
264
|
-
###
|
|
332
|
+
### Switch to Python 3.15 (blocked on CuPy)
|
|
333
|
+
|
|
334
|
+
Goal: require Python 3.15 and nothing older, mainly for explicit lazy imports (PEP 810, the `lazy` keyword). State checked on 2026-10-23 with uv 0.13.5 and CPython 3.15.1:
|
|
335
|
+
|
|
336
|
+
- **Blocker: CuPy has no 3.15 wheels.** `cupy-cuda13x` 14.2.0 ships cp310-cp314 only, on PyPI and on `pip.cupy.dev/pre`. Support is tracked in cupy/cupy#10259, milestone v14.3.0 (unreleased; the 14.3.0.dev0 docs already list 3.15). Switching before then would leave `commkit[gpu]` uninstallable and the GPU suite unrunnable.
|
|
337
|
+
- **Second blocker: `jupyter_client` fails to import on 3.15.** `jupyter_client/jsonutil.py` (line 32) calls `strptime` with a day of month and no year, which 3.15 rejects (cpython#70647). `tests/test_examples.py` then fails at collection, and the notebooks cannot run. Wait for a fixed release; do not work around it in commkit.
|
|
338
|
+
- **Ready:** wheel-only installs on 3.15 work for NumPy 2.5.4, SciPy 1.18.1, Numba 0.68.0 (llvmlite 0.50.0), Matplotlib 3.11.2, mypy 2.4.0, ruff, pytest, pytest-cov, pytest-benchmark, nbclient, ipykernel, nbstripout and bump-my-version. The CPU suite has not been run on 3.15 beyond the collection error above.
|
|
339
|
+
|
|
340
|
+
Steps, once CuPy 14.3 (with cp315 wheels for `cupy-cuda13x`) and a fixed `jupyter_client` are out. Keep the commits separate: the version switch alone first, then each refactor, so a failure points at one change.
|
|
341
|
+
|
|
342
|
+
1. **Validate before changing anything**: in a 3.15 venv, install the current code and run the full CPU+GPU suite (`-rs`, notebooks included), the benchmark smoke run, the `NUMBA_DISABLE_JIT` job and the precision audit.
|
|
343
|
+
2. **Switch the version floor**:
|
|
344
|
+
- `pyproject.toml`: `requires-python = ">=3.15"`, classifiers 3.15 only, ruff `target-version = "py315"` (if ruff supports it), raise the dependency minimums to the first releases with 3.15 wheels (`cupy-cuda13x[ctk]>=14.3`, `numba>=0.68`, `numpy>=2.5`, `scipy>=1.18`; recheck each).
|
|
345
|
+
- `ci.yml`: test matrix `python-version: ["3.15"]` (keep both `locked` and `lowest-direct`); the static-analysis, kernel-coverage and notebook jobs on 3.15. `publish.yml`: build on 3.15.
|
|
346
|
+
- `uv lock`; README install requirements; §2 here ("Python 3.12+").
|
|
347
|
+
- Benchmarks: record a new baseline on 3.15 (it lands in `benchmarks/baselines/Linux-CPython-3.15-64bit/`), compare it with `0007` by a paired A/B of 3.14 against 3.15 on unchanged code, and make it the reference.
|
|
348
|
+
3. **Lazy imports (PEP 810)**. Confirm the rules on the interpreter first. As specified, `lazy` is allowed only for module-level imports: not inside functions, class bodies or `try` blocks, and not with `*`.
|
|
349
|
+
- About 60 function-local imports exist to keep `import commkit` light or optional: `import cupy as cp` (11 sites), `import numba` (9), `from . import compiler` in `_cuda` (5), and local imports that break cycles (`Constellation`, `Signal`, `normalize`). Move each to a module-level `lazy import` / `lazy from`. Concentrated in `_cuda/__init__.py`, `backend.py`, `io.py`, `recovery/`, `core/`, `plotting/`.
|
|
350
|
+
- CuPy stays optional: a lazy import of a missing module fails only when used, so every use must stay behind the existing availability checks (`is_cupy_available`, `dispatch`). `backend.py`'s probing needs care: decide per site whether the probe imports eagerly inside `try`.
|
|
351
|
+
- Numba: `@njit` at module level compiles lazily already; check that a lazy `numba` import does not make every first call pay the import at an odd moment (time with `benchmarks/bench_import.py`).
|
|
352
|
+
- The six `TYPE_CHECKING` blocks can become `lazy from` imports where the name is also useful at run time; keep them where it is not.
|
|
353
|
+
- Measure `import commkit` before and after with `bench_import.py`, and keep `tests/test_import_hygiene.py` passing; extend it to assert that `import commkit` loads neither CuPy nor Matplotlib, if lazy imports make that true.
|
|
354
|
+
4. **Other cleanups that the 3.15 floor allows** (each its own commit; verify each against What's New in 3.14 and 3.15):
|
|
355
|
+
- Drop `from __future__ import annotations` (25 modules): annotations are evaluated lazily by default since 3.14 (PEP 649/749). Check that nothing reads `__annotations__` as strings.
|
|
356
|
+
- UTF-8 mode is the default from 3.15 (PEP 686): check `io` and any `open()` without `encoding=`.
|
|
357
|
+
- `strptime` with a day and no year now raises (the `jupyter_client` failure above); grep commkit for date parsing.
|
|
358
|
+
- The 3.15 sampling profiler (PEP 799) is a candidate tool for the CPU hot paths next to `nsys`; mention it in §8 if it proves useful.
|
|
359
|
+
|
|
360
|
+
### Channel coding: open items
|
|
265
361
|
|
|
266
|
-
1. **
|
|
362
|
+
1. **Storing frames with a payload code.** `save_npz` raises for a frame with a `payload_code`, because code objects are not serialized; store `extract_payload(signal)` instead.
|
|
363
|
+
2. **oFEC** (OpenROADM, ITU-T G.709.3) is not implemented: its block structure is specified in the OpenROADM MSA documents, which were not available in a form to implement from. It would build on `BCHCode` (extended BCH(256, 239)), `Chase` with soft output and the staircase decoder's iteration over a window.
|
|
364
|
+
3. **NR rate matching for polar codes** (sub-block interleaving, the circular buffer, puncturing and shortening with their frozen-set rules, parity-check bits) and **LTE rate matching for turbo codes** are not included: `PolarCode` and `TurboCode` are the mother codes. `Punctured` and `Shortened` cover generic rate matching.
|
|
267
365
|
|
|
268
366
|
### Nonlinear fiber channel: future features
|
|
269
367
|
|
|
@@ -275,21 +373,3 @@ Each is an addition that leaves the current API unchanged.
|
|
|
275
373
|
- **Distributed (Raman) amplification and per-span fiber types** in one call (chaining calls already works: every call speaks launch-relative units).
|
|
276
374
|
- **cuFFT callbacks** fusing the linear stage into the transforms (CuPy's support is experimental and compiles with nvcc at run time).
|
|
277
375
|
- **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.
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Notable changes to CommKit, newest first. Performance figures are from the reference machine (RTX 4070 Ti, Ryzen 7 7800X3D, WSL2) unless stated otherwise, as a ratio of new to old time (below 1 is faster).
|
|
4
|
+
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
## [3.0.0] - 2026-10-28
|
|
8
|
+
|
|
9
|
+
The channel-coding release. `commkit.coding` grows from an empty scaffold into a full FEC toolbox: one `encode` / `decode` pair dispatched on code objects covers linear block, LDPC, BCH and Reed-Solomon, polar, convolutional and turbo codes, the FEC structures of the optical standards (KP4, the 802.3dj and 400ZR concatenated codes, the G.709.2 staircase code, PON LDPC), CRCs, interleavers, rate matching and probabilistic amplitude shaping. `generate` and frames produce coded payloads, the reference keeps the source bits next to the bits on the air, and three new notebooks teach it. Shaped `generate` now goes through a distribution matcher, and the example notebooks are renumbered; see the first section.
|
|
10
|
+
|
|
11
|
+
### Changes that may need code updates
|
|
12
|
+
|
|
13
|
+
- **Shaped `generate` goes through a distribution matcher.** A shaped constellation used to draw i.i.d. symbols from its pmf and take their labels as the "bits", so the bits described a stream that was never sent. Now random bits pass through `CCDM(constellation, block_length=num_symbols)` per channel: every symbol comes from bits, each record holds every point exactly as often as the pmf (quantized to `1/num_symbols`) says, and `reference.info_bits` holds the matcher's input. For the same seed the symbols differ from 2.x; GMI and MI agree within Monte Carlo error. A shaped record must be long enough to carry a bit (a few symbols). Frames inherit this, and `save_npz` stores a shaped frame's source bits.
|
|
14
|
+
- **`Reference` has two more fields**, `info_bits` and `code_block`. `bits` are the bits on the air; `info_bits` are the source bits before coding. Without coding `info_bits` is `bits` itself and `code_block` is `(1, 1)`, so existing code that reads `reference.bits` is unaffected. Code that builds a `Reference` positionally keeps working.
|
|
15
|
+
- **The placeholder modules of `commkit.coding` are gone.** 2.x shipped empty scaffold modules (`hamming`, `bch`, `reed_solomon`, `galois`) that held only docstrings. The codes now live in `linear` (Hamming and other linear block codes), `algebraic` (BCH and Reed-Solomon) and the private `_galois`; import the public names from `commkit.coding` itself (`from commkit.coding import BCHCode, ReedSolomonCode`), which is the supported path. `commkit.coding` is also reachable as `ck.coding` after `import commkit as ck`.
|
|
16
|
+
- **The example notebooks are renumbered** to fit the coding chapters: 07 Channel coding and 08 Probabilistic shaping and coded modulation join Part II, 21 FEC in optical standards joins Part IV. Old 07-18 are now 09-20, old 19 (the end-to-end transceiver) is 22, and old 20-25 (characterization and templates) are 23-28. Update bookmarks and links to the notebooks.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **`commkit.coding`**, the channel-coding module: `encode(bits, code=...)` and `decode(llrs, code=..., method=...)`, dispatched on a code object, returning a `DecodeResult` (decoded bits, per-block success check and iterations).
|
|
21
|
+
- `LinearBlockCode` from a generator or parity-check matrix, and `LinearBlockCode.hamming(r, extended=, length=)` for Hamming codes, including the inner codes of 400ZR, Hamming(128,119), and of IEEE 802.3dj, Hamming(128,120).
|
|
22
|
+
- Decoders `Syndrome(max_weight=)` (coset leaders; `max_weight=1` makes an extended Hamming code correct one error and detect two) and `MaximumLikelihood()` (exhaustive, for small codes).
|
|
23
|
+
- **LDPC codes:** `LDPCCode.from_parity_check(H)`, `LDPCCode.quasi_cyclic(base, lifting)`, `LDPCCode.dvbs2(rate, frame)` (all DVB-S2 rates, normal and short frames) and `LDPCCode.pon()` (LDPC(17280, 14592) of IEEE 802.3ca and ITU-T G.9804.2). Decoders `MinSum(scale=, offset=)` and `SumProduct()`, layered, with early stopping per codeword, on the CPU (Numba) and the GPU (a CUDA kernel, about 10x the CPU on 64 DVB-S2 codewords). Encoding is systematic: a sparse product and a cumulative XOR for DVB-S2, a sparse product and a GEMM with the inverted parity part for PON.
|
|
24
|
+
- **Algebraic codes:** `ReedSolomonCode(symbols, info_symbols, m=)` with the presets `g709()` (RS(255,239)), `kr4()` (RS(528,514)), `kp4()` (RS(544,514)), `epon10g()` (RS(255,223)) and `xgspon()` (RS(248,216)), and binary `BCHCode(m, t, length=)`, decoded by `BerlekampMassey()` (Chien search, Forney) on both devices; a codeword with more than `t` errors is reported `valid=False` or decoded to a codeword within distance `t`. Encoding, syndromes and the locator evaluations are GEMMs over GF(2): KP4 decodes 2048 codewords in 0.45 s on the CPU and 89 ms on the GPU.
|
|
25
|
+
- **Soft decoding of block codes and optical FEC structures:** `Chase(test_positions=, hard=, beta=)`, Chase-II decoding with Pyndiah's soft output for linear block and BCH codes; `LinearBlockCode.oif400zr()`, the double-extended Hamming(128,119) of OIF 400ZR exactly as its clause 10.5 defines it; `StaircaseCode.g709_2(num_blocks)`, the 512 x 510 staircase code of ITU-T G.709.2 (its double-extended BCH(1022,990) component and column permutation as specified), decoded by `HardIterations()`; and `Concatenated(outer, inner, interleaver)` with the presets `cfec()` (400ZR C-FEC) and `ieee_802_3dj()` (KP4 + Hamming(128,120)), decoded by `Stages(inner=, outer=)`. Measured thresholds match the standards: the staircase code decodes at an input BER of 4.5e-3, the C-FEC at 1.2e-2.
|
|
26
|
+
- **Polar codes:** `PolarCode.nr(k, n, crc=)` with the information set of the 5G NR reliability sequence (TS 38.212 Table 5.3.1.2-1) and an optional NR CRC, or `PolarCode.from_frozen(mask)`; decoders `SC()` and `SCL(list_size)` (CRC-aided when the code has a CRC). Mother code only (no NR rate matching).
|
|
27
|
+
- **Convolutional and turbo codes:** `ConvolutionalCode(generators, constraint_length, block_length, termination=)` and `ConvolutionalCode.k7()` (133, 171), terminated or tail-biting, decoded by `Viterbi()` (maximum likelihood) or `BCJR(exact=)` (a-posteriori LLRs); `TurboCode.lte(K)` for the 188 LTE block sizes (QPP interleaver of TS 36.212 Table 5.1.3-3, trellis termination as clause 5.1.3.2.2), decoded by `TurboIterations(iterations, exact=)`.
|
|
28
|
+
- **Bit-level building blocks:** `CRC` as a code (`CRC.nr` for the 5G NR and LTE polynomials, `CRC.ethernet` for the IEEE 802.3 CRC-32, or any Rocksoft-model CRC), whose `decode` flags blocks that fail the check; `interleave` / `deinterleave` with an `Interleaver` (explicit, `Interleaver.block`, `Interleaver.random`); and the rate-matching codes `Punctured(code, positions)` and `Shortened(code, num_bits)`, decoded with the inner code's decoders.
|
|
29
|
+
- **Distribution matching:** `CCDM(constellation, block_length=)`, a constant-composition matcher (an exact 64-bit range coder, invertible for every input, blocks up to 2^30 points, 0.35 s for 10^6 points of 64-QAM on one core), and `PAS(code, constellation)`, probabilistic amplitude shaping with a systematic FEC code supplying the signs. Both are codes: `generate(..., code=PAS(LDPCCode.dvbs2("3/4"), Constellation.qam(64).shaped(entropy=5)))` builds a coded PS payload, and `decode` runs the FEC decoder and the dematcher.
|
|
30
|
+
- **Coded payloads:** `generate(..., code=)` and `SingleCarrierFrame(payload_code=)` encode random information bits before mapping. The reference keeps both bit streams.
|
|
31
|
+
- **`metrics.fer`**, the block error rate, and **`metrics.ngmi`**, the normalized GMI used as the soft-decision FEC threshold. Post-FEC BER is `ber(res.bits, reference.info_bits)`.
|
|
32
|
+
- **`save_npz` / `load_npz`** store a frame's payload source bits and code block (`frame_payload_info_bits`), so a shaped frame round-trips; a frame with a `payload_code` raises, as code objects are not serialized (store `extract_payload(signal)` instead).
|
|
33
|
+
- **Notebooks:** [07 · Channel coding](examples/07-channel-coding.ipynb) (codes as objects, hard and soft decoding, Reed-Solomon, LDPC waterfalls, NGMI as a threshold, polar, convolutional and turbo codes), [08 · Probabilistic shaping and coded modulation](examples/08-probabilistic-shaping-and-coded-modulation.ipynb) (the CCDM, the shaping gain, PAS with LDPC against uniform 16-QAM) and [21 · FEC in optical standards](examples/21-fec-in-optical-standards.ipynb) (the codes of IEEE 802.3, G.709, 400ZR and PON at their thresholds). The end-to-end transceiver (22) carries an LDPC-coded payload and reports post-FEC BER and FER.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- **`compute_llr` compiled its CPU kernels in every process.** They are declared with Numba's disk cache, but their kernels called helpers built in the same function, which Numba cannot cache, so the first call of every Python process spent about 3 s compiling. They now load from the cache (about 0.2 s, Numba import included); the results are unchanged.
|
|
38
|
+
|
|
39
|
+
### Development
|
|
40
|
+
|
|
41
|
+
- `benchmarks/bench_coding.py` times LDPC decoding (min-sum on both devices) and KP4 decoding; the `ldpc` kernel is in the sanitizer pass of AGENTS §2, and the coding tests of the Numba decoders and the CCDM run in the `NUMBA_DISABLE_JIT` coverage job.
|
|
42
|
+
- Baseline `0007` (`v3_0`) is the new benchmark reference, the first with the coding benchmarks: min-sum decoding of DVB-S2 codewords takes 16 ms on the GPU against 133 ms on the CPU, KP4 decoding 41 ms against 410 ms. The benchmarks without coding match `0006` (geometric mean of the ratios 0.97; the slowest moves reproduced within 10% in isolation, or were within the GPU's run-to-run swing on a 0.1 ms call).
|
|
43
|
+
- A session fixture keeps Numba's thread pool busy for half a second before the first benchmark: kernels now loaded from the cache reached the timed rounds while the OpenMP workers were still coming up, which made the first parallel benchmark of a file 4-8x slower.
|
|
44
|
+
- The Numba kernels of the coding module and of `compute_llr` live in private modules (`coding/_numba_loops.py`, `mapping/_numba_llr.py`), so Numba's disk cache keeps them; the coding decoders compiled again in every process too, costing up to 6 s on their first call.
|
|
45
|
+
- Every helper in the package has a docstring, with one-line summaries throughout.
|
|
46
|
+
- `backend._device_tables` caches small per-object tables (generators, interleavers, field tables) on each device, shared by the coding module and the earlier users; its upload is a documented site of the transfer audit.
|
|
47
|
+
|
|
48
|
+
## [2.3.0] - 2026-10-28
|
|
49
|
+
|
|
50
|
+
A performance and robustness release. GPU block equalizers and the CPU decision-directed equalizers with inline carrier recovery run substantially faster. `iir_filter` keeps single precision on both devices. Max-log LLRs of uniform square QAM have a closed form. The CUDA code was reviewed end to end. A few changes need code updates; see the first section.
|
|
51
|
+
|
|
52
|
+
### Changes that may need code updates
|
|
53
|
+
|
|
54
|
+
- **`build_pilot_ref`** takes `num_symbols=` and `num_channels=` instead of `n_sym=` and `num_ch=`, the library's glossary names. Rename the keywords at call sites.
|
|
55
|
+
- **`EqualizerResult` is a frozen dataclass**, like every other multi-value result. Assigning to a field raises; build a modified copy with `dataclasses.replace(result, field=value)`.
|
|
56
|
+
- **GPU failures raise.** A CUDA kernel that fails to compile or load, or a CUDA-graph capture that fails, used to log one warning and fall back to a slower path. It now raises, because it means a broken install or a bug.
|
|
57
|
+
- **`SingleCarrierFrame` validates its pilots on construction.** `pilot_pattern="block"` without `pilot_block_len` raises; it used to silently build a frame with no pilots. A pilot period that does not tile the body also raises at construction instead of on first use.
|
|
58
|
+
- **`moving_average(mode="valid")`** raises on a record shorter than the window, instead of returning NumPy's swapped convolution.
|
|
59
|
+
- **Numerical results move at rounding level** in a few places, from reordered or fused arithmetic. Seeds reproduce results within one version only. The largest moves:
|
|
60
|
+
- fiber propagation on the CPU: about 3e-6 relative;
|
|
61
|
+
- `lms` with inline carrier recovery: up to 5e-6 relative;
|
|
62
|
+
- `correct_iq_imbalance`: 5e-7 relative in complex64;
|
|
63
|
+
- `rde`: 2.5e-7 relative.
|
|
64
|
+
|
|
65
|
+
Everything else changed in this release is bit-identical or within float32 rounding of before.
|
|
66
|
+
|
|
67
|
+
### Added
|
|
68
|
+
|
|
69
|
+
- **Single-precision `iir_filter` on both devices.**
|
|
70
|
+
- CPU: a Numba kernel that reads and writes the samples in their own dtype and keeps the sections and the state in double precision. Double input still matches SciPy bit for bit.
|
|
71
|
+
- GPU: a block-parallel CUDA kernel. Blocks are filtered in parallel and the state is carried across them in two levels, in double precision.
|
|
72
|
+
- There is no longer a double-precision copy of the record: complex64 is 0.32–0.41x of the previous GPU path.
|
|
73
|
+
- **Closed-form max-log LLRs** for uniform separable QAM, `compute_llr`'s default case. Each bit's LLR comes from the nearest level of each bit value, so an axis costs 1 + 2k metrics instead of sqrt(M). It works for any per-axis labeling, not only Gray, and gives the same values as before: bit for bit on the CPU, within 3e-7 on the GPU.
|
|
74
|
+
- GPU, every order: 0.87x at 16-QAM, 0.74x at 256-QAM.
|
|
75
|
+
- CPU, from 256-QAM (where it starts to pay): 0.65x at 256-QAM, 0.40x at 1024-QAM, 0.23x at 4096-QAM.
|
|
76
|
+
- Exact LLRs and shaped constellations keep the per-level computation.
|
|
77
|
+
- `TimingEstimate` is listed in `commkit.timing.__all__`, like the other estimate classes.
|
|
78
|
+
- `pad_mode="reflect"` is documented for the equalizers and `apply_taps`; they already accepted it.
|
|
79
|
+
|
|
80
|
+
### Performance
|
|
81
|
+
|
|
82
|
+
Equalizers:
|
|
83
|
+
|
|
84
|
+
- **Block equalizers on the GPU:** each block's window load, training staging and output stores now run inside the replayed CUDA graph, so the host launches one graph per block. Baseline `0006` against `0005`:
|
|
85
|
+
- `block_lms` at `block_size=256`: 0.36x without carrier recovery, 0.78x with BPS, 0.65x with BPS and cycle slips;
|
|
86
|
+
- `block_cma`: 0.50x;
|
|
87
|
+
- `block_rde`: 0.66x.
|
|
88
|
+
|
|
89
|
+
At 256 symbols per block the GPU now runs `block_lms` about 2x faster than the CPU without carrier recovery, and 5x faster with BPS.
|
|
90
|
+
- **`block_lms` per block:** one phasor per block and one divergence check after the loop. The cycle-slip kernel binds its buffers once per run, cutting the per-block cost from 17 µs to 10–14 µs. On the GPU, cycle-slip correction runs for any channel count, where above 1024 channels it went through the host every block.
|
|
91
|
+
- **`lms` and `rls` with inline carrier recovery (CPU):** the tap and distance loops compile with fast math while the phase unwrap stays strict. RLS with BPS 0.63x, LMS with PLL 0.87x.
|
|
92
|
+
- **`rde` and `block_rde`:** the ring is decided by squared midpoints, with no square root. On the GPU this is one fused kernel: 112 µs → 10 µs per 2 × 1024-symbol block.
|
|
93
|
+
|
|
94
|
+
Carrier and frequency recovery:
|
|
95
|
+
|
|
96
|
+
- **BPS of square QAM on the CPU:** one Numba pass instead of a chunked NumPy loop: 0.39x.
|
|
97
|
+
- **PLL:** reads the samples in their own precision, with no complex128 copy of the record: 0.95–0.97x.
|
|
98
|
+
- **Blockwise `MengaliMorelli`:** one Numba call for all blocks instead of one per block. CPU 0.85x, GPU 0.62x.
|
|
99
|
+
- **Blockwise `correct_frequency_offset` on the GPU:** builds its phase trajectory on the GPU, where it used to make a round trip through the host.
|
|
100
|
+
|
|
101
|
+
Mapping and LLRs:
|
|
102
|
+
|
|
103
|
+
- **Exact LLRs on the GPU:** one exponential per point instead of one per point and bit. With the third pass over the points also gone, 128-cross QAM is 0.79x of baseline `0005`.
|
|
104
|
+
- **Square QAM on the GPU:** demapped per axis, 2·sqrt(M) metrics instead of M.
|
|
105
|
+
- **Hard decisions of PSK and cross QAM on the GPU:** one kernel launch instead of a Python loop over chunks.
|
|
106
|
+
- **`Constellation.map`:** packs the bits in one pass. On a 4-core machine: 16-QAM 33 → 5.8 ms, 256-QAM 55 → 10.5 ms, 2^20 symbols.
|
|
107
|
+
- **`Constellation.rotational_symmetry`:** 1024-QAM in 0.026 s instead of 2.2 s.
|
|
108
|
+
|
|
109
|
+
Other functions:
|
|
110
|
+
|
|
111
|
+
- **`correct_iq_imbalance`:** all channels at once from closed-form 2×2 statistics, CPU about 0.45x.
|
|
112
|
+
- **`evm` and `snr`:** measure the reference power once: 0.74–0.78x.
|
|
113
|
+
- **`moving_average(mode="valid")`:** one pass over all channels. CPU 0.11–0.15x, GPU 0.21–0.71x.
|
|
114
|
+
- **`estimate_timing` with several templates:** transforms the signal once: CPU 0.62–0.72x.
|
|
115
|
+
- **`plot_eye_diagram` histograms:** CPU 0.33x, GPU 0.55x.
|
|
116
|
+
- **Fiber propagation on the CPU:** NumPy stages replace the Numba passes, which competed with the multithreaded FFTs (0.56–0.92x for 2^14 to 2^20 samples). On the GPU, the plain CuPy stages that complex128 input runs are 0.53x for DBP.
|
|
117
|
+
- **Fewer GPU copies:** frequency grids, windows and the polarization demultiplexer's grid are built on the GPU instead of being copied there on every call.
|
|
118
|
+
|
|
119
|
+
### Fixed
|
|
120
|
+
|
|
121
|
+
- `plot_eye_diagram` ignored `vmin` and `vmax`.
|
|
122
|
+
- BPS on the GPU failed with `CUDA_ERROR_INVALID_VALUE` for more than 65535 channels.
|
|
123
|
+
- Single-precision input was computed in double precision in BPS distances, `block_rde`'s ring distances, the block equalizers' forward filter and `iir_filter`, against the library's precision contract.
|
|
124
|
+
|
|
125
|
+
### Development
|
|
126
|
+
|
|
127
|
+
- **Test plugins:**
|
|
128
|
+
- `tests/plugins/transfer_trace.py` audits host-device copies in a GPU run.
|
|
129
|
+
- `tests/plugins/precision_trace.py` now follows single-precision data through the FFTs.
|
|
130
|
+
- **CI:** the JIT-disabled kernel-coverage job measures the equalizers' Numba kernels again, and strict mypy covers every module of the `filtering` package.
|
|
131
|
+
- **Benchmarks:** new benchmarks for the IIR filter, moving average, PLL, blockwise Mengali-Morelli and IQ correction. Baseline `0006` is the new reference.
|
|
132
|
+
- **Code organization:** `filtering` and `analysis.linewidth` are packages, and every public name imports from the same place as before. The sequential equalizers share their Numba steps, CMA and RDE share one body, and duplicated helpers are merged.
|
|
133
|
+
- **Docs:** docstrings follow one style throughout. AGENTS.md documents the CUDA review rules and how to run `compute-sanitizer`.
|
|
134
|
+
|
|
135
|
+
## [2.2.0] - 2026-10-23
|
|
136
|
+
|
|
137
|
+
See the [GitHub release](https://github.com/lokgar/commkit/releases/tag/v2.2.0) and the [changes since 2.1.0](https://github.com/lokgar/commkit/compare/v2.1.0...v2.2.0).
|
|
138
|
+
|
|
139
|
+
## [2.1.0] - 2026-10-21
|
|
140
|
+
|
|
141
|
+
See the [GitHub release](https://github.com/lokgar/commkit/releases/tag/v2.1.0) and the [changes since 2.0.0](https://github.com/lokgar/commkit/compare/v2.0.0...v2.1.0).
|
|
142
|
+
|
|
143
|
+
## [2.0.0] - 2026-10-07
|
|
144
|
+
|
|
145
|
+
See the [GitHub release](https://github.com/lokgar/commkit/releases/tag/v2.0.0) and the [changes since 1.1.0](https://github.com/lokgar/commkit/compare/v1.1.0...v2.0.0).
|
|
146
|
+
|
|
147
|
+
## [1.1.0] - 2026-09-03
|
|
148
|
+
|
|
149
|
+
See the [GitHub release](https://github.com/lokgar/commkit/releases/tag/v1.1.0) and the [changes since 1.0.0](https://github.com/lokgar/commkit/compare/v1.0.0...v1.1.0).
|
|
150
|
+
|
|
151
|
+
## [1.0.0] - 2026-08-31
|
|
152
|
+
|
|
153
|
+
First release. See the [GitHub release](https://github.com/lokgar/commkit/releases/tag/v1.0.0).
|
|
154
|
+
|
|
155
|
+
[Unreleased]: https://github.com/lokgar/commkit/compare/v3.0.0...HEAD
|
|
156
|
+
[3.0.0]: https://github.com/lokgar/commkit/compare/v2.3.0...v3.0.0
|
|
157
|
+
[2.3.0]: https://github.com/lokgar/commkit/compare/v2.2.0...v2.3.0
|
|
158
|
+
[2.2.0]: https://github.com/lokgar/commkit/compare/v2.1.0...v2.2.0
|
|
159
|
+
[2.1.0]: https://github.com/lokgar/commkit/compare/v2.0.0...v2.1.0
|
|
160
|
+
[2.0.0]: https://github.com/lokgar/commkit/compare/v1.1.0...v2.0.0
|
|
161
|
+
[1.1.0]: https://github.com/lokgar/commkit/compare/v1.0.0...v1.1.0
|