commkit 2.1.0__tar.gz → 2.3.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.1.0 → commkit-2.3.0}/.github/workflows/ci.yml +1 -1
- {commkit-2.1.0 → commkit-2.3.0}/.gitignore +13 -0
- commkit-2.3.0/AGENTS.md +347 -0
- commkit-2.3.0/CHANGELOG.md +116 -0
- {commkit-2.1.0 → commkit-2.3.0}/PKG-INFO +12 -12
- {commkit-2.1.0 → commkit-2.3.0}/README.md +11 -11
- commkit-2.3.0/benchmarks/baselines/Linux-CPython-3.14-64bit/0004_fiber_dbp.json +3676 -0
- commkit-2.3.0/benchmarks/baselines/Linux-CPython-3.14-64bit/0005_gpu_decoder_path.json +4458 -0
- commkit-2.3.0/benchmarks/baselines/Linux-CPython-3.14-64bit/0006_perf_cleanup.json +5054 -0
- {commkit-2.1.0 → commkit-2.3.0}/benchmarks/bench_block_blind.py +4 -3
- {commkit-2.1.0 → commkit-2.3.0}/benchmarks/bench_block_lms.py +3 -2
- commkit-2.3.0/benchmarks/bench_fiber.py +62 -0
- commkit-2.3.0/benchmarks/bench_filters.py +41 -0
- {commkit-2.1.0 → commkit-2.3.0}/benchmarks/bench_llr.py +16 -0
- commkit-2.3.0/benchmarks/bench_mapping.py +47 -0
- {commkit-2.1.0 → commkit-2.3.0}/benchmarks/bench_sync_misc.py +43 -2
- {commkit-2.1.0 → commkit-2.3.0}/benchmarks/workloads.py +45 -8
- {commkit-2.1.0 → commkit-2.3.0}/commkit/__init__.py +29 -15
- {commkit-2.1.0 → commkit-2.3.0}/commkit/_array.py +73 -33
- {commkit-2.1.0 → commkit-2.3.0}/commkit/_circular.py +8 -9
- commkit-2.3.0/commkit/_cuda/__init__.py +674 -0
- {commkit-2.1.0 → commkit-2.3.0}/commkit/_cuda/compiler.py +11 -12
- commkit-2.3.0/commkit/_cuda/graphs.py +118 -0
- {commkit-2.1.0 → commkit-2.3.0}/commkit/_cuda/src/bps_min_d2.cu +36 -33
- commkit-2.3.0/commkit/_cuda/src/iir.cu +222 -0
- commkit-2.3.0/commkit/_cuda/src/llr.cu +337 -0
- {commkit-2.1.0 → commkit-2.3.0}/commkit/_dispersion.py +65 -10
- commkit-2.3.0/commkit/_fft.py +87 -0
- commkit-2.3.0/commkit/_fiber.py +721 -0
- {commkit-2.1.0 → commkit-2.3.0}/commkit/_overlap_save.py +16 -17
- commkit-2.3.0/commkit/_random.py +60 -0
- {commkit-2.1.0 → commkit-2.3.0}/commkit/_sequences.py +25 -25
- commkit-2.3.0/commkit/analysis/__init__.py +65 -0
- {commkit-2.1.0 → commkit-2.3.0}/commkit/analysis/_common.py +11 -10
- {commkit-2.1.0 → commkit-2.3.0}/commkit/analysis/allan.py +30 -28
- {commkit-2.1.0 → commkit-2.3.0}/commkit/analysis/drift.py +51 -46
- {commkit-2.1.0 → commkit-2.3.0}/commkit/analysis/fm_noise.py +63 -61
- {commkit-2.1.0 → commkit-2.3.0}/commkit/analysis/interferometry.py +105 -99
- commkit-2.3.0/commkit/analysis/linewidth/__init__.py +138 -0
- commkit-2.3.0/commkit/analysis/linewidth/dsh.py +623 -0
- commkit-2.3.0/commkit/analysis/linewidth/phase.py +381 -0
- commkit-2.3.0/commkit/analysis/linewidth/result.py +141 -0
- commkit-2.3.0/commkit/analysis/trajectory.py +113 -0
- {commkit-2.1.0 → commkit-2.3.0}/commkit/backend.py +72 -62
- commkit-2.3.0/commkit/coding/__init__.py +21 -0
- {commkit-2.1.0 → commkit-2.3.0}/commkit/coding/base.py +1 -2
- {commkit-2.1.0 → commkit-2.3.0}/commkit/coding/bch.py +1 -2
- {commkit-2.1.0 → commkit-2.3.0}/commkit/coding/convolutional.py +1 -2
- {commkit-2.1.0 → commkit-2.3.0}/commkit/coding/crc.py +1 -2
- {commkit-2.1.0 → commkit-2.3.0}/commkit/coding/galois.py +2 -3
- {commkit-2.1.0 → commkit-2.3.0}/commkit/coding/hamming.py +1 -2
- {commkit-2.1.0 → commkit-2.3.0}/commkit/coding/interleaving.py +1 -2
- {commkit-2.1.0 → commkit-2.3.0}/commkit/coding/ldpc.py +1 -2
- {commkit-2.1.0 → commkit-2.3.0}/commkit/coding/polar.py +1 -2
- {commkit-2.1.0 → commkit-2.3.0}/commkit/coding/ratematch.py +1 -2
- {commkit-2.1.0 → commkit-2.3.0}/commkit/coding/reed_solomon.py +1 -2
- {commkit-2.1.0 → commkit-2.3.0}/commkit/coding/turbo.py +1 -2
- commkit-2.3.0/commkit/core/__init__.py +23 -0
- {commkit-2.1.0 → commkit-2.3.0}/commkit/core/_signal_adapter.py +10 -3
- {commkit-2.1.0 → commkit-2.3.0}/commkit/core/frame.py +149 -230
- {commkit-2.1.0 → commkit-2.3.0}/commkit/core/generation.py +21 -20
- {commkit-2.1.0 → commkit-2.3.0}/commkit/core/signal.py +25 -21
- commkit-2.3.0/commkit/equalization/__init__.py +55 -0
- commkit-2.3.0/commkit/equalization/_block/__init__.py +18 -0
- {commkit-2.1.0 → commkit-2.3.0}/commkit/equalization/_block/_blind.py +50 -19
- {commkit-2.1.0 → commkit-2.3.0}/commkit/equalization/_block/_dd.py +218 -239
- {commkit-2.1.0 → commkit-2.3.0}/commkit/equalization/_block/_engine.py +143 -88
- {commkit-2.1.0 → commkit-2.3.0}/commkit/equalization/_common.py +129 -70
- commkit-2.3.0/commkit/equalization/_kernels_numba.py +48 -0
- commkit-2.3.0/commkit/equalization/_numba_loops.py +839 -0
- commkit-2.3.0/commkit/equalization/blind.py +338 -0
- {commkit-2.1.0 → commkit-2.3.0}/commkit/equalization/linear.py +135 -95
- {commkit-2.1.0 → commkit-2.3.0}/commkit/equalization/polarization.py +282 -262
- {commkit-2.1.0 → commkit-2.3.0}/commkit/equalization/result.py +81 -108
- commkit-2.3.0/commkit/equalization/sequential/__init__.py +19 -0
- commkit-2.3.0/commkit/equalization/sequential/_blind.py +463 -0
- commkit-2.3.0/commkit/equalization/sequential/_dd.py +667 -0
- {commkit-2.1.0 → commkit-2.3.0}/commkit/equalization/sequential/_setup.py +27 -23
- commkit-2.3.0/commkit/filtering/__init__.py +75 -0
- commkit-2.3.0/commkit/filtering/fiber.py +204 -0
- commkit-2.3.0/commkit/filtering/linear.py +808 -0
- commkit-2.3.0/commkit/filtering/pulses.py +488 -0
- {commkit-2.1.0 → commkit-2.3.0}/commkit/frequency.py +148 -148
- commkit-2.3.0/commkit/impairments/__init__.py +39 -0
- {commkit-2.1.0 → commkit-2.3.0}/commkit/impairments/channel/__init__.py +8 -6
- {commkit-2.1.0 → commkit-2.3.0}/commkit/impairments/channel/linear.py +78 -103
- commkit-2.3.0/commkit/impairments/channel/nonlinear.py +178 -0
- {commkit-2.1.0 → commkit-2.3.0}/commkit/impairments/frontend.py +73 -65
- {commkit-2.1.0 → commkit-2.3.0}/commkit/impairments/noise.py +19 -24
- {commkit-2.1.0 → commkit-2.3.0}/commkit/impairments/source.py +67 -53
- {commkit-2.1.0 → commkit-2.3.0}/commkit/io.py +53 -51
- {commkit-2.1.0 → commkit-2.3.0}/commkit/logger.py +10 -13
- {commkit-2.1.0 → commkit-2.3.0}/commkit/mapping/__init__.py +6 -7
- {commkit-2.1.0 → commkit-2.3.0}/commkit/mapping/bits.py +10 -13
- {commkit-2.1.0 → commkit-2.3.0}/commkit/mapping/constellation.py +124 -52
- {commkit-2.1.0 → commkit-2.3.0}/commkit/mapping/gray.py +115 -153
- commkit-2.3.0/commkit/mapping/llr.py +632 -0
- {commkit-2.1.0 → commkit-2.3.0}/commkit/mapping/shaping.py +12 -13
- {commkit-2.1.0 → commkit-2.3.0}/commkit/math.py +163 -99
- {commkit-2.1.0 → commkit-2.3.0}/commkit/metrics.py +97 -77
- {commkit-2.1.0 → commkit-2.3.0}/commkit/multirate.py +34 -76
- {commkit-2.1.0 → commkit-2.3.0}/commkit/plotting/__init__.py +16 -8
- {commkit-2.1.0 → commkit-2.3.0}/commkit/plotting/analysis.py +101 -72
- {commkit-2.1.0 → commkit-2.3.0}/commkit/plotting/constellation.py +38 -54
- {commkit-2.1.0 → commkit-2.3.0}/commkit/plotting/equalizer.py +42 -30
- {commkit-2.1.0 → commkit-2.3.0}/commkit/plotting/eye.py +55 -71
- {commkit-2.1.0 → commkit-2.3.0}/commkit/plotting/filter_response.py +35 -42
- {commkit-2.1.0 → commkit-2.3.0}/commkit/plotting/spectral.py +58 -125
- {commkit-2.1.0 → commkit-2.3.0}/commkit/plotting/sync.py +372 -752
- {commkit-2.1.0 → commkit-2.3.0}/commkit/plotting/theme.py +61 -26
- {commkit-2.1.0 → commkit-2.3.0}/commkit/plotting/waveform.py +25 -42
- {commkit-2.1.0 → commkit-2.3.0}/commkit/recovery/__init__.py +12 -9
- {commkit-2.1.0 → commkit-2.3.0}/commkit/recovery/_common.py +70 -47
- {commkit-2.1.0 → commkit-2.3.0}/commkit/recovery/bps.py +92 -57
- {commkit-2.1.0 → commkit-2.3.0}/commkit/recovery/carrier_phase.py +18 -55
- {commkit-2.1.0 → commkit-2.3.0}/commkit/recovery/corrections.py +198 -137
- {commkit-2.1.0 → commkit-2.3.0}/commkit/recovery/pilots.py +85 -65
- {commkit-2.1.0 → commkit-2.3.0}/commkit/recovery/pll.py +120 -116
- {commkit-2.1.0 → commkit-2.3.0}/commkit/recovery/tikhonov.py +47 -43
- {commkit-2.1.0 → commkit-2.3.0}/commkit/recovery/viterbi_viterbi.py +9 -9
- commkit-2.3.0/commkit/smoothing.py +148 -0
- {commkit-2.1.0 → commkit-2.3.0}/commkit/spectral.py +121 -161
- {commkit-2.1.0 → commkit-2.3.0}/commkit/timing.py +157 -178
- commkit-2.3.0/examples/00-getting-started.ipynb +352 -0
- commkit-2.3.0/examples/01-constellations-and-bit-mapping.ipynb +282 -0
- commkit-2.3.0/examples/02-spectral-analysis.ipynb +304 -0
- commkit-2.3.0/examples/03-digital-filters.ipynb +347 -0
- commkit-2.3.0/examples/04-pulse-shaping-and-matched-filtering.ipynb +387 -0
- commkit-2.3.0/examples/05-multirate-and-resampling.ipynb +340 -0
- commkit-2.3.0/examples/06-awgn-and-performance-metrics.ipynb +381 -0
- commkit-2.3.0/examples/07-laser-phase-noise.ipynb +319 -0
- commkit-2.3.0/examples/08-front-end-impairments.ipynb +294 -0
- commkit-2.3.0/examples/09-frame-synchronization-and-timing.ipynb +310 -0
- commkit-2.3.0/examples/10-frequency-offset-estimation.ipynb +397 -0
- commkit-2.3.0/examples/11-blind-carrier-phase-recovery.ipynb +367 -0
- commkit-2.3.0/examples/12-pilot-aided-carrier-recovery.ipynb +324 -0
- commkit-2.3.0/examples/13-linear-channel-equalization.ipynb +293 -0
- commkit-2.3.0/examples/14-adaptive-equalization.ipynb +433 -0
- commkit-2.3.0/examples/15-blind-equalization.ipynb +275 -0
- commkit-2.3.0/examples/16-polarization-multiplexing.ipynb +314 -0
- commkit-2.3.0/examples/17-chromatic-dispersion.ipynb +286 -0
- commkit-2.3.0/examples/18-nonlinear-fiber-and-backpropagation.ipynb +228 -0
- commkit-2.3.0/examples/19-coherent-transceiver-end-to-end.ipynb +397 -0
- commkit-2.1.0/examples/carrier_phase_analysis.ipynb → commkit-2.3.0/examples/20-carrier-phase-characterization.ipynb +22 -10
- commkit-2.1.0/examples/laser_linewidth_dsh.ipynb → commkit-2.3.0/examples/21-laser-linewidth-self-heterodyne.ipynb +32 -306
- commkit-2.3.0/examples/22-laser-linewidth-short-delay-and-flicker.ipynb +387 -0
- commkit-2.1.0/examples/laser_linewidth_homodyne_iq.ipynb → commkit-2.3.0/examples/23-laser-linewidth-self-homodyne-iq.ipynb +18 -8
- commkit-2.1.0/examples/measurement_laser_linewidth_dsh.ipynb → commkit-2.3.0/examples/24-measurement-template-self-heterodyne.ipynb +11 -11
- commkit-2.1.0/examples/measurement_laser_linewidth_homodyne_iq.ipynb → commkit-2.3.0/examples/25-measurement-template-self-homodyne-iq.ipynb +9 -9
- commkit-2.3.0/examples/README.md +63 -0
- {commkit-2.1.0 → commkit-2.3.0}/pyproject.toml +13 -11
- {commkit-2.1.0 → commkit-2.3.0}/tests/analysis/test_drift.py +10 -0
- {commkit-2.1.0 → commkit-2.3.0}/tests/analysis/test_interferometry.py +21 -0
- {commkit-2.1.0 → commkit-2.3.0}/tests/analysis/test_linewidth.py +82 -7
- {commkit-2.1.0 → commkit-2.3.0}/tests/common/__init__.py +2 -4
- {commkit-2.1.0 → commkit-2.3.0}/tests/common/kernel_utils.py +5 -41
- {commkit-2.1.0 → commkit-2.3.0}/tests/common/reference_impl.py +5 -2
- {commkit-2.1.0 → commkit-2.3.0}/tests/common/signals.py +3 -3
- {commkit-2.1.0 → commkit-2.3.0}/tests/conftest.py +7 -20
- {commkit-2.1.0 → commkit-2.3.0}/tests/core/test_frame.py +25 -11
- {commkit-2.1.0 → commkit-2.3.0}/tests/core/test_signal.py +1 -1
- {commkit-2.1.0 → commkit-2.3.0}/tests/equalization/test_blind.py +88 -2
- {commkit-2.1.0 → commkit-2.3.0}/tests/equalization/test_block.py +81 -27
- {commkit-2.1.0 → commkit-2.3.0}/tests/equalization/test_bps_kernel.py +81 -54
- {commkit-2.1.0 → commkit-2.3.0}/tests/equalization/test_cpr.py +8 -8
- commkit-2.3.0/tests/equalization/test_cs_kernel.py +144 -0
- {commkit-2.1.0 → commkit-2.3.0}/tests/equalization/test_linear.py +67 -4
- {commkit-2.1.0 → commkit-2.3.0}/tests/equalization/test_polarization.py +24 -0
- {commkit-2.1.0 → commkit-2.3.0}/tests/equalization/test_sequential.py +36 -8
- {commkit-2.1.0 → commkit-2.3.0}/tests/equalization/test_state.py +27 -1
- {commkit-2.1.0 → commkit-2.3.0}/tests/equalization/test_winit.py +32 -0
- commkit-2.3.0/tests/impairments/channel/test_channel_nonlinear.py +436 -0
- {commkit-2.1.0 → commkit-2.3.0}/tests/impairments/test_source.py +3 -4
- {commkit-2.1.0 → commkit-2.3.0}/tests/mapping/test_constellation.py +52 -1
- {commkit-2.1.0 → commkit-2.3.0}/tests/mapping/test_gray.py +89 -29
- commkit-2.3.0/tests/mapping/test_llr.py +543 -0
- {commkit-2.1.0 → commkit-2.3.0}/tests/plotting/test_plot_equalizer.py +3 -2
- {commkit-2.1.0 → commkit-2.3.0}/tests/plotting/test_plot_eye.py +14 -0
- commkit-2.3.0/tests/plugins/__init__.py +0 -0
- commkit-2.3.0/tests/plugins/precision_trace.py +198 -0
- commkit-2.3.0/tests/plugins/transfer_trace.py +158 -0
- {commkit-2.1.0 → commkit-2.3.0}/tests/recovery/test_bps.py +25 -0
- {commkit-2.1.0 → commkit-2.3.0}/tests/recovery/test_corrections.py +95 -5
- {commkit-2.1.0 → commkit-2.3.0}/tests/test_api_contracts.py +55 -32
- {commkit-2.1.0 → commkit-2.3.0}/tests/test_array.py +45 -2
- commkit-2.3.0/tests/test_cuda_infra.py +131 -0
- {commkit-2.1.0 → commkit-2.3.0}/tests/test_dispersion.py +29 -1
- commkit-2.3.0/tests/test_fiber.py +209 -0
- {commkit-2.1.0 → commkit-2.3.0}/tests/test_filtering.py +252 -0
- {commkit-2.1.0 → commkit-2.3.0}/tests/test_frequency.py +20 -0
- {commkit-2.1.0 → commkit-2.3.0}/tests/test_math.py +18 -5
- {commkit-2.1.0 → commkit-2.3.0}/tests/test_metrics.py +4 -2
- {commkit-2.1.0 → commkit-2.3.0}/tests/test_pulse_shaping.py +3 -3
- {commkit-2.1.0 → commkit-2.3.0}/tests/test_smoothing.py +25 -0
- {commkit-2.1.0 → commkit-2.3.0}/uv.lock +2 -2
- commkit-2.1.0/AGENTS.md +0 -264
- commkit-2.1.0/commkit/_cuda/__init__.py +0 -467
- commkit-2.1.0/commkit/_cuda/src/llr.cu +0 -110
- commkit-2.1.0/commkit/_random.py +0 -39
- commkit-2.1.0/commkit/analysis/__init__.py +0 -78
- commkit-2.1.0/commkit/analysis/linewidth.py +0 -1159
- commkit-2.1.0/commkit/analysis/trajectory.py +0 -110
- commkit-2.1.0/commkit/coding/__init__.py +0 -23
- commkit-2.1.0/commkit/core/__init__.py +0 -24
- commkit-2.1.0/commkit/equalization/__init__.py +0 -46
- commkit-2.1.0/commkit/equalization/_block/__init__.py +0 -24
- commkit-2.1.0/commkit/equalization/_kernels_numba.py +0 -1748
- commkit-2.1.0/commkit/equalization/blind.py +0 -246
- commkit-2.1.0/commkit/equalization/sequential/__init__.py +0 -21
- commkit-2.1.0/commkit/equalization/sequential/_blind.py +0 -494
- commkit-2.1.0/commkit/equalization/sequential/_dd.py +0 -732
- commkit-2.1.0/commkit/filtering.py +0 -1234
- commkit-2.1.0/commkit/impairments/__init__.py +0 -39
- commkit-2.1.0/commkit/impairments/channel/nonlinear.py +0 -11
- commkit-2.1.0/commkit/mapping/llr.py +0 -255
- commkit-2.1.0/commkit/smoothing.py +0 -131
- commkit-2.1.0/examples/qam_receiver_quickstart.ipynb +0 -313
- commkit-2.1.0/tests/equalization/test_cs_kernel.py +0 -116
- commkit-2.1.0/tests/mapping/test_llr.py +0 -314
- commkit-2.1.0/tests/test_cuda_infra.py +0 -111
- {commkit-2.1.0 → commkit-2.3.0}/.gitattributes +0 -0
- {commkit-2.1.0 → commkit-2.3.0}/.github/workflows/publish.yml +0 -0
- {commkit-2.1.0 → commkit-2.3.0}/.python-version +0 -0
- {commkit-2.1.0 → commkit-2.3.0}/LICENSE +0 -0
- {commkit-2.1.0 → commkit-2.3.0}/benchmarks/baselines/Linux-CPython-3.14-64bit/0002_pre_v2.json +0 -0
- {commkit-2.1.0 → commkit-2.3.0}/benchmarks/baselines/Linux-CPython-3.14-64bit/0003_v2_0.json +0 -0
- {commkit-2.1.0 → commkit-2.3.0}/benchmarks/bench_bps.py +0 -0
- {commkit-2.1.0 → commkit-2.3.0}/benchmarks/bench_equalizers.py +0 -0
- {commkit-2.1.0 → commkit-2.3.0}/benchmarks/bench_import.py +0 -0
- {commkit-2.1.0 → commkit-2.3.0}/benchmarks/bench_signal_pipeline.py +0 -0
- {commkit-2.1.0 → commkit-2.3.0}/benchmarks/benchutils.py +0 -0
- {commkit-2.1.0 → commkit-2.3.0}/benchmarks/conftest.py +0 -0
- {commkit-2.1.0 → commkit-2.3.0}/benchmarks/record_baseline.py +0 -0
- {commkit-2.1.0 → commkit-2.3.0}/commkit/_cuda/src/bps_anchor.cu +0 -0
- {commkit-2.1.0 → commkit-2.3.0}/commkit/_cuda/src/cs_block.cu +0 -0
- {commkit-2.1.0 → commkit-2.3.0}/commkit/_cuda/src/selftest.cu +0 -0
- {commkit-2.1.0 → commkit-2.3.0}/commkit/py.typed +0 -0
- {commkit-2.1.0 → commkit-2.3.0}/tests/analysis/test_allan.py +0 -0
- {commkit-2.1.0 → commkit-2.3.0}/tests/analysis/test_fm_noise.py +0 -0
- {commkit-2.1.0 → commkit-2.3.0}/tests/analysis/test_trajectory.py +0 -0
- {commkit-2.1.0 → commkit-2.3.0}/tests/common/conversions.py +0 -0
- {commkit-2.1.0 → commkit-2.3.0}/tests/common/metrics.py +0 -0
- {commkit-2.1.0 → commkit-2.3.0}/tests/common/periodic.py +0 -0
- {commkit-2.1.0 → commkit-2.3.0}/tests/core/test_generation.py +0 -0
- {commkit-2.1.0 → commkit-2.3.0}/tests/core/test_psqam.py +0 -0
- {commkit-2.1.0 → commkit-2.3.0}/tests/core/test_signal_adapter.py +0 -0
- {commkit-2.1.0 → commkit-2.3.0}/tests/core/test_signal_mimo.py +0 -0
- {commkit-2.1.0 → commkit-2.3.0}/tests/equalization/test_equalizer_oracles.py +0 -0
- {commkit-2.1.0 → commkit-2.3.0}/tests/equalization/test_mimo.py +0 -0
- {commkit-2.1.0 → commkit-2.3.0}/tests/impairments/channel/test_channel_linear.py +0 -0
- {commkit-2.1.0 → commkit-2.3.0}/tests/impairments/test_frontend.py +0 -0
- {commkit-2.1.0 → commkit-2.3.0}/tests/impairments/test_noise.py +0 -0
- {commkit-2.1.0 → commkit-2.3.0}/tests/mapping/test_bits.py +0 -0
- {commkit-2.1.0 → commkit-2.3.0}/tests/plotting/test_plot_analysis.py +0 -0
- {commkit-2.1.0 → commkit-2.3.0}/tests/plotting/test_plot_constellation.py +0 -0
- {commkit-2.1.0 → commkit-2.3.0}/tests/plotting/test_plot_filter_response.py +0 -0
- {commkit-2.1.0 → commkit-2.3.0}/tests/plotting/test_plot_spectral.py +0 -0
- {commkit-2.1.0 → commkit-2.3.0}/tests/plotting/test_plot_sync.py +0 -0
- {commkit-2.1.0 → commkit-2.3.0}/tests/plotting/test_plot_theme.py +0 -0
- {commkit-2.1.0 → commkit-2.3.0}/tests/plotting/test_plot_waveform.py +0 -0
- {commkit-2.1.0 → commkit-2.3.0}/tests/recovery/test_joint.py +0 -0
- {commkit-2.1.0 → commkit-2.3.0}/tests/recovery/test_pilots.py +0 -0
- {commkit-2.1.0 → commkit-2.3.0}/tests/recovery/test_pll.py +0 -0
- {commkit-2.1.0 → commkit-2.3.0}/tests/recovery/test_recovery_oracles.py +0 -0
- {commkit-2.1.0 → commkit-2.3.0}/tests/recovery/test_tikhonov.py +0 -0
- {commkit-2.1.0 → commkit-2.3.0}/tests/recovery/test_viterbi_viterbi.py +0 -0
- {commkit-2.1.0 → commkit-2.3.0}/tests/test_backend.py +0 -0
- {commkit-2.1.0 → commkit-2.3.0}/tests/test_examples.py +0 -0
- {commkit-2.1.0 → commkit-2.3.0}/tests/test_import_hygiene.py +0 -0
- {commkit-2.1.0 → commkit-2.3.0}/tests/test_io.py +0 -0
- {commkit-2.1.0 → commkit-2.3.0}/tests/test_logger.py +0 -0
- {commkit-2.1.0 → commkit-2.3.0}/tests/test_multirate.py +0 -0
- {commkit-2.1.0 → commkit-2.3.0}/tests/test_signal_pipeline.py +0 -0
- {commkit-2.1.0 → commkit-2.3.0}/tests/test_spectral.py +0 -0
- {commkit-2.1.0 → commkit-2.3.0}/tests/test_timing.py +0 -0
|
@@ -115,7 +115,7 @@ jobs:
|
|
|
115
115
|
- name: Kernel coverage report
|
|
116
116
|
run: >-
|
|
117
117
|
uv run coverage report --show-missing --fail-under=85
|
|
118
|
-
--include="commkit/equalization/_kernels_numba.py,commkit/equalization/sequential/*,commkit/frequency.py,commkit/recovery/*"
|
|
118
|
+
--include="commkit/equalization/_kernels_numba.py,commkit/equalization/_numba_loops.py,commkit/equalization/sequential/*,commkit/frequency.py,commkit/recovery/*"
|
|
119
119
|
|
|
120
120
|
# GPU legs (--device=gpu / --device=all) require CuPy + CUDA and are run
|
|
121
121
|
# manually or on a self-hosted runner; they are intentionally not part of
|
|
@@ -8,6 +8,11 @@ wheels/
|
|
|
8
8
|
*.egg-info
|
|
9
9
|
.ruff_cache
|
|
10
10
|
.benchmarks
|
|
11
|
+
.coverage
|
|
12
|
+
.coverage.*
|
|
13
|
+
coverage.xml
|
|
14
|
+
coverage.json
|
|
15
|
+
htmlcov/
|
|
11
16
|
|
|
12
17
|
# Virtual environments
|
|
13
18
|
.venv
|
|
@@ -17,6 +22,14 @@ wheels/
|
|
|
17
22
|
|
|
18
23
|
.vscode/
|
|
19
24
|
|
|
25
|
+
# Jupyter (uv run jupyter lab examples)
|
|
26
|
+
.ipynb_checkpoints/
|
|
27
|
+
|
|
28
|
+
# Nsight profiles (benchmarks/benchutils.nvtx_range)
|
|
29
|
+
*.nsys-rep
|
|
30
|
+
*.qdrep
|
|
31
|
+
*.ncu-rep
|
|
32
|
+
|
|
20
33
|
tmp/
|
|
21
34
|
|
|
22
35
|
# DSP captures / large binaries
|
commkit-2.3.0/AGENTS.md
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
Guide for anyone, human or coding agent, changing the **CommKit** repository.
|
|
4
|
+
|
|
5
|
+
The rules below describe the code as it is. New code follows them; a change that breaks one changes this file in the same commit.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. What CommKit is
|
|
10
|
+
|
|
11
|
+
A Python library for digital-communications research: simulation, receiver DSP, metrics, plotting and experimental captures, on NumPy (CPU) and CuPy (GPU) data. It is **not differentiable**. A separate JAX library, *commax*, will hold the blocks that must sit inside a gradient. commax depends on commkit, never the reverse, and takes its conventions (`Constellation`, bit labels, pulse taps, frame layouts) from commkit.
|
|
12
|
+
|
|
13
|
+
Users are researchers and engineers. Design for them:
|
|
14
|
+
|
|
15
|
+
- **Algorithms are plain functions.** Data is a `Signal` or an array. Things that *describe* a modulation, a pulse or an embedded sub-algorithm are small frozen value objects written inline in the call:
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
res = lms(rx, num_taps=21, step_size=1e-3, cpr=BPS(test_phases=64))
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
- **Everything is expressed in Python.** No configuration files, registries, global switches or multi-step setup.
|
|
22
|
+
- **We deliberately do not build** pipeline or receiver objects, DSP methods on `Signal`, string shorthands for constellations, or public "config objects per algorithm".
|
|
23
|
+
- **Scope is the reference model and its inverse**, not a catalogue of research variants: the fiber channel and standard digital backpropagation (`correct_fiber_propagation`, with `steps_per_span` and `nonlinear_scale`), like chromatic dispersion and its compensation. Reduced-complexity nonlinearity compensation (filtered or enhanced split step, perturbation-based) is left to user code built on these, and learned DBP, which needs gradients, belongs in commax.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 2. Commands
|
|
28
|
+
|
|
29
|
+
Always use `uv`. Python 3.12+.
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
uv sync --all-extras # environment incl. GPU packages
|
|
33
|
+
uv run nbstripout --install # once per clone: strip notebook outputs on commit
|
|
34
|
+
uv run pytest # CPU + GPU tests (default --device=all)
|
|
35
|
+
uv run pytest --device=cpu # what CI runs
|
|
36
|
+
uv run pytest tests/equalization/ -k lms # subset
|
|
37
|
+
uv run pytest --cov=commkit # coverage
|
|
38
|
+
NUMBA_DISABLE_JIT=1 uv run pytest tests/equalization tests/recovery \
|
|
39
|
+
tests/test_frequency.py --device=cpu --cov=commkit # real kernel coverage
|
|
40
|
+
uv run pytest --device=cpu -p tests.plugins.precision_trace \
|
|
41
|
+
--ignore=tests/test_examples.py # precision audit (see Numerics); the suite passes under it
|
|
42
|
+
uv run pytest --device=gpu -p tests.plugins.transfer_trace \
|
|
43
|
+
--ignore=tests/test_examples.py # transfer audit (see Performance); fails on an undocumented transfer
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**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):
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
uv pip install --target /tmp/sanitizer "nvidia-cuda-sanitizer-api==13.*"
|
|
50
|
+
for tool in memcheck racecheck synccheck initcheck; do
|
|
51
|
+
/tmp/sanitizer/nvidia/cu13/bin/compute-sanitizer --tool $tool --leak-check full \
|
|
52
|
+
.venv/bin/python -m pytest --device=gpu -p no:cacheprovider \
|
|
53
|
+
tests/equalization/test_cs_kernel.py tests/equalization/test_bps_kernel.py \
|
|
54
|
+
tests/equalization/test_block.py tests/equalization/test_blind.py tests/mapping/test_llr.py \
|
|
55
|
+
tests/test_filtering.py tests/test_fiber.py tests/impairments/channel/test_channel_nonlinear.py
|
|
56
|
+
done
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Under WSL2 the sanitizer needs the Windows GPU debugger interface; without it every run stops at "Failed to initialize WDDM debugger interface". Enable it once by running `EnableDebuggerInterface.bat` as a Windows administrator; the script comes with the Windows CUDA Toolkit, not with the Linux wheel.
|
|
60
|
+
|
|
61
|
+
**Commit gate.** CI fails on any of these, so run all of them before every commit, with `ruff format` last:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
uv run ruff check . --fix
|
|
65
|
+
uv run mypy commkit/
|
|
66
|
+
uv run pytest
|
|
67
|
+
uv run ruff format .
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Benchmarks** are explicit-only; `uv run pytest` never collects them. A commit that changes a public signature also smoke-runs them once, without timing, so their call sites cannot rot unnoticed:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
uv run pytest benchmarks/ --benchmark-disable --device=all
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Timed runs:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
uv run pytest benchmarks/bench_bps.py --benchmark-only --device=all
|
|
80
|
+
uv run pytest benchmarks/ --benchmark-only --device=all \
|
|
81
|
+
--benchmark-compare=0006 --benchmark-storage=file://benchmarks/baselines
|
|
82
|
+
uv run python benchmarks/record_baseline.py NAME # record a new baseline
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Release:**
|
|
86
|
+
|
|
87
|
+
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.
|
|
88
|
+
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.
|
|
89
|
+
3. `uv run bump-my-version bump {patch|minor|major}`. This commits the new version and tags it `vX.Y.Z`.
|
|
90
|
+
4. `git push origin main --tags`. The ruleset on `main` requires pull requests; repository admins bypass it for these two commits.
|
|
91
|
+
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.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## 3. Architecture
|
|
96
|
+
|
|
97
|
+
Dependencies point downward only:
|
|
98
|
+
|
|
99
|
+
| Layer | Modules | May import |
|
|
100
|
+
| --- | --- | --- |
|
|
101
|
+
| Infrastructure | `backend`, `logger`, `_cuda` | NumPy, CuPy |
|
|
102
|
+
| Array and math helpers | `_array` (shape and validation), `math` (power, normalization, dB, linear trend) | infrastructure |
|
|
103
|
+
| Value objects | `mapping.Constellation`, pulse classes, `recovery` CPR configs | layers above |
|
|
104
|
+
| Core | `core` (`Signal`, frames, generation), `io` | layers above |
|
|
105
|
+
| DSP | `mapping`, `filtering`, `multirate`, `spectral`, `smoothing`, `impairments`, `timing`, `frequency`, `recovery`, `equalization`, `metrics`, `analysis` | layers above, and lower DSP modules |
|
|
106
|
+
| Presentation | `plotting` | everything; **nothing imports plotting** |
|
|
107
|
+
|
|
108
|
+
- **Responsibilities:**
|
|
109
|
+
- `filtering` holds signal-chain filters with a real frequency response.
|
|
110
|
+
- `smoothing` holds display and estimation smoothers only.
|
|
111
|
+
- `analysis` holds laser and phase characterization.
|
|
112
|
+
- `metrics` is the reporting layer.
|
|
113
|
+
- **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.
|
|
114
|
+
- **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".
|
|
115
|
+
- **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`).
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## 4. API rules
|
|
120
|
+
|
|
121
|
+
### Signal
|
|
122
|
+
|
|
123
|
+
- `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.
|
|
124
|
+
- It never holds pipeline results.
|
|
125
|
+
- Construction does no hidden work: no mapping, no normalization, no device move, no shape guessing. Factories (`generate`, frames) do that work.
|
|
126
|
+
- `sig.to("gpu")` and `sig.replace(...)` return new Signals. Arrays are shared, not copied; use `clone()` for an independent copy.
|
|
127
|
+
- **Alignment invariant:** reference symbol *k* corresponds to symbol period *k* of the samples. Any function that drops or shifts symbol periods slices the reference to match. Metrics raise on mismatch; they never guess.
|
|
128
|
+
|
|
129
|
+
### Function signatures
|
|
130
|
+
|
|
131
|
+
- `f(data, other_data, *, params)`: everything after the data arguments is keyword-only. A function without data arguments may keep its single primary parameter positional (`barker_sequence(13)`, `gray_code(5)`); with several parameters, all are keyword-only (`rrc_taps(sps=2, rolloff=0.1, span=10)`). The data-argument count is recorded per function in the contract registry.
|
|
132
|
+
- **Signal awareness.** A function whose main argument is waveform data (or a field a Signal really carries) accepts an array or a Signal. It unwraps once with `adapt_signal()` (in `core/_signal_adapter.py`), works on arrays, and returns through `wrap_samples()`. Never re-enter the public function recursively.
|
|
133
|
+
- Functions on *derived* quantities (phase trajectories, correlations, taps, PSD bins, results) and synthesis primitives stay array-only.
|
|
134
|
+
- **Metadata rule: facts versus choices.**
|
|
135
|
+
- Facts (`sampling_rate`, `symbol_rate`, and so `sps`) come from the Signal. A conflicting argument raises `ValueError`; an equal one is fine.
|
|
136
|
+
- Choices (decision `constellation`, `noise_var`, `pulse`) default to the Signal's value, and an explicit argument wins.
|
|
137
|
+
- A missing fact for array input raises. Neither case warns.
|
|
138
|
+
- Integer-SPS algorithms validate with `require_integer_sps()` before any cast. Never truncate 1.5 SPS to 1.
|
|
139
|
+
- **Value objects are frozen dataclasses** with read-only NumPy arrays:
|
|
140
|
+
- `Constellation`: `.qam(M)`, `.psk(M)`, `.pam(M, unipolar=)`, or arbitrary `points`; `.shaped(nu=|entropy=)` keeps unit power. Functions accept the object only, never strings.
|
|
141
|
+
- Pulses: `RRC`, `RC`, `Gaussian`, `Rect`, `SmoothRect`. A raw taps array is accepted wherever a pulse is.
|
|
142
|
+
- Algorithm objects, one per estimation method (`BPS`, `PLL`, `ViterbiViterbi`, `MthPower`, `CycleSlip`, ...). The same object is used standalone (`correct_carrier_phase(y, BPS())`) and nested (`lms(..., cpr=BPS())`). It lives next to its kernel and validates its parameters on construction. Class names are unique across the package.
|
|
143
|
+
- Equalizer continuation uses `state=result.state`.
|
|
144
|
+
- **Randomness:** `rng: int | np.random.Generator | None` (SciPy SPEC 7).
|
|
145
|
+
- What is transmitted (bits, symbols, pilots): draw on the host with the Generator, then transfer. A seed gives the same payload on CPU and GPU.
|
|
146
|
+
- What the channel adds (AWGN, phase noise, ASE): draw on the data's device through `_random.standard_normal` / `complex_normal`. Realizations differ between devices; statistics agree, and tests assert statistics.
|
|
147
|
+
- Random parameters of a realization (the PMD waveplates of `Fiber`) are few and drawn on the host, so a seed gives the same fiber on every device.
|
|
148
|
+
- Never use a global RNG.
|
|
149
|
+
- **Types:** a transform is written `def f(samples: S, ...) -> S` with the TypeVar `S` from `core/_signal_adapter.py` (bound to `np.ndarray | Signal`), so a Signal gives a Signal and an array an array. Do not reassign the `samples` parameter; name the unwrapped array `x`.
|
|
150
|
+
- **Device follows the data.** There are no `backend=` arguments and no global switches. Unsupported array types (JAX, PyTorch) raise `TypeError`.
|
|
151
|
+
- Only factories with no input data to follow (`generate`, `generate_phase_noise`, `Preamble`/`SingleCarrierFrame.to_signal`, `load_npz`) take `device: str = "cpu"`, validated with `require_device()`. They draw randomness as the rule above says and do the vectorized work (mapping, shaping) on `device`. The default is never "GPU if available".
|
|
152
|
+
- **No `debug_plot`.** Numerical code never imports plotting. Plot functions consume results or recompute through public compute functions.
|
|
153
|
+
- **Design and apply are separate functions**, for example `rrc_taps` and `fir_filter`, or `butterworth_sos` and `iir_filter`.
|
|
154
|
+
|
|
155
|
+
### Returns
|
|
156
|
+
|
|
157
|
+
- **Transforms:** array in gives array out on the same device; Signal in gives a new Signal out (update `sampling_rate` when the rate changes).
|
|
158
|
+
- **Estimators** reduce over the time axis by input rank: `(N,)` gives 0-d and `(C, N)` gives `(C,)`. Results stay on the input device.
|
|
159
|
+
- **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.
|
|
160
|
+
- **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.
|
|
161
|
+
- **More than one value** is returned as a frozen dataclass with named fields. No dicts, and no tuples longer than two.
|
|
162
|
+
- **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.
|
|
163
|
+
|
|
164
|
+
### Naming
|
|
165
|
+
|
|
166
|
+
| Prefix | Meaning |
|
|
167
|
+
| --- | --- |
|
|
168
|
+
| `apply_*` | add an impairment or apply a model |
|
|
169
|
+
| `estimate_<quantity>(x, method)` | measure, never change the data; returns a frozen `<Quantity>Estimate` (value plus diagnostics) |
|
|
170
|
+
| `correct_<quantity>(x, how)` | remove the impairment; returns only the corrected data. `how` is an estimate or an algorithm object (which estimates first) |
|
|
171
|
+
| `resolve_*` | pick among discrete candidates (π/2 rotation, channel permutation); nothing else |
|
|
172
|
+
| `generate` | synthesis from parameters, takes `rng` |
|
|
173
|
+
| `plot_*` | every public plotting function (only `apply_default_theme` is exempt) |
|
|
174
|
+
|
|
175
|
+
- 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_*`.
|
|
176
|
+
- Computations use plain nouns (`allan_deviation`).
|
|
177
|
+
- **Glossary**, one name each:
|
|
178
|
+
- `num_taps`, `step_size`, `sps`, `constellation`, `pulse`, `rng`, `num_symbols`, `num_channels`, `sampling_rate`, `symbol_rate`.
|
|
179
|
+
- Units are in the docstring. Use `_db` and `_hz` suffixes where the unit is ambiguous.
|
|
180
|
+
|
|
181
|
+
### Docstrings
|
|
182
|
+
|
|
183
|
+
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.
|
|
184
|
+
|
|
185
|
+
- **Summary line** on the line of the opening quotes (`"""Summary.`), one line, ending with a period.
|
|
186
|
+
- Functions: imperative mood (`Apply ...`, `Estimate ...`, `Plot ...`), or a noun phrase for a function named after the quantity it returns (`evm`, `allan_deviation`, `welch_psd`).
|
|
187
|
+
- Classes and modules: a noun phrase.
|
|
188
|
+
- **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.
|
|
189
|
+
- **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)``.
|
|
190
|
+
- **Plots** return `(fig, ax) or None` (`None` with `show=True`).
|
|
191
|
+
- **Defaults:** `name : type, default value`; `name : type, optional` when the default is `None`, with the description saying what `None` does.
|
|
192
|
+
- Units go in the description; inline code in double backticks; references to library names through `:func:` / `:class:` / `:mod:` / `:meth:`.
|
|
193
|
+
|
|
194
|
+
### Errors and logging
|
|
195
|
+
|
|
196
|
+
- Wrong input raises, with a message naming the argument.
|
|
197
|
+
- **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).
|
|
198
|
+
- `logger` is for diagnostics only. Per-channel logging that needs a device-to-host transfer is wrapped in `if logger.isEnabledFor(logging.INFO):`.
|
|
199
|
+
- The library never configures handlers, Matplotlib or warning filters at import time.
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## 5. Numerics
|
|
204
|
+
|
|
205
|
+
- **Storage dtypes:** `complex64` for IQ samples and `float32` for real signals; `generate`, frames and `load_npz` produce them, so the GPU's single-precision path is the default.
|
|
206
|
+
- **Precision contract.** Every function computes in a predictable precision, and none promotes single-precision data on its own:
|
|
207
|
+
- **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.
|
|
208
|
+
- **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.
|
|
209
|
+
- **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).
|
|
210
|
+
- **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.
|
|
211
|
+
- 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.
|
|
212
|
+
- **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).
|
|
213
|
+
- **Accumulation precision:**
|
|
214
|
+
- 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.
|
|
215
|
+
- 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.
|
|
216
|
+
- Carrier phase: promote angles to `float64` before `xp.unwrap()`, because float32 rounding causes spurious quadrant slips. Tikhonov/Kalman block transitions also run in `float64`.
|
|
217
|
+
- **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.
|
|
218
|
+
- **Shapes:** SISO is `(N,)` and MIMO is `(C, N)`. Use `as_2d`, `restore_1d`, `broadcast_channels` and `require_channels`; never hand-roll the promote/squeeze idiom.
|
|
219
|
+
- **Reproducibility:** a seed is reproducible within a library version only. Tests and baselines assert statistics, never exact noise realizations.
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## 6. Performance
|
|
224
|
+
|
|
225
|
+
| Algorithm structure | Implementation | Device |
|
|
226
|
+
| --- | --- | --- |
|
|
227
|
+
| Vectorized: filters, resampling, spectra, impairments, metrics, frequency-domain equalizers | NumPy/CuPy via `x, xp, sp = dispatch(samples)` | follows the input |
|
|
228
|
+
| 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 |
|
|
229
|
+
| Sequential recursions: LMS/RLS/CMA/RDE, PLL, Tikhonov, cycle slips, the Mengali-Morelli bootstrap over the lags | 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 |
|
|
230
|
+
| 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 |
|
|
231
|
+
| Hot spots limited by intermediates, launches or round trips | CUDA C++ via CuPy `RawModule` (`commkit/_cuda`) | GPU |
|
|
232
|
+
| 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 |
|
|
233
|
+
|
|
234
|
+
**Host-sync hygiene:**
|
|
235
|
+
|
|
236
|
+
- Never extract scalars from a possibly-GPU array inside a loop (`float(x[c])`, `.item()`). Compute the vector on the device, transfer it once with `to_device(v, "cpu")`, then loop on the host.
|
|
237
|
+
- Prefer on-device gathers (`xp.take_along_axis`).
|
|
238
|
+
- Every FFT goes through `commkit._fft` (`fft`, `ifft`, `rfft`, `irfft`), never `np.fft` or `xp.fft` directly: on the CPU it runs `scipy.fft` on every core and keeps complex64 in single precision (NumPy 2 computes it in double, about 3x slower per core); on the GPU it is `cupy.fft`. Frequency grids (`xp.fft.fftfreq`) are exempt.
|
|
239
|
+
- 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.
|
|
240
|
+
- Chunk `(N, M)` intermediates over N with an on-device accumulator.
|
|
241
|
+
- Reduce on the device before transferring, including for plotting.
|
|
242
|
+
- 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.
|
|
243
|
+
- **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.
|
|
244
|
+
- Pass whole records to compiled code. There are no Python loops over symbols or small blocks, except inside a CUDA graph.
|
|
245
|
+
- 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.
|
|
246
|
+
|
|
247
|
+
**Kernel policy:**
|
|
248
|
+
|
|
249
|
+
- A `.cu` kernel needs a benchmark showing that CuPy is limited by intermediates, launch count or round trips.
|
|
250
|
+
- `_cuda.get_kernel` returns the launch wrapper or raises (compile, load or a bad specialization); it is called only on CuPy input.
|
|
251
|
+
- It optimizes a CPU reference (Numba or NumPy) and is tested against it with `--device=all`.
|
|
252
|
+
- 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`).
|
|
253
|
+
- A new or changed kernel runs its GPU tests under `compute-sanitizer` (§2) before release.
|
|
254
|
+
- `float32` with explicit literals.
|
|
255
|
+
- `--use_fast_math` only per kernel (through `options`), after an accuracy check; the compiler default does not pass it.
|
|
256
|
+
- Short elementwise chains use `ElementwiseKernel` or `cupy.fuse`.
|
|
257
|
+
- 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).
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## 7. Tests
|
|
262
|
+
|
|
263
|
+
- **Fixtures:** use `backend_device`, `xp` and `xpt` from `tests/conftest.py`. Assert with `xpt.assert_allclose(...)`, because `numpy.testing` raises on CuPy arrays. Cast expectations with `xp.asarray`, and reductions with `float(...)`.
|
|
264
|
+
- **Layout mirrors the source:** `tests/<subpackage>/test_<module>.py`, and `tests/test_<module>.py` for flat modules. Test basenames must be globally unique (pytest prepend import mode), hence names like `channel/test_channel_linear.py`. Split a large test file by concern.
|
|
265
|
+
- **Shared builders** live in `tests/common/` and use the public API.
|
|
266
|
+
- **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.
|
|
267
|
+
- Do not duplicate these checks in per-module tests.
|
|
268
|
+
- **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.
|
|
269
|
+
- **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.
|
|
270
|
+
- **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.
|
|
271
|
+
- **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.
|
|
272
|
+
- 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.
|
|
273
|
+
- 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.
|
|
274
|
+
- Plots of hand-computed arrays go through `to_device(x, "cpu")`, so the notebooks run unchanged on the GPU.
|
|
275
|
+
- **Coverage** must not drop below the CI floor. Numba kernel coverage is measured by a separate `NUMBA_DISABLE_JIT=1` job.
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## 8. Benchmarks
|
|
280
|
+
|
|
281
|
+
`benchmarks/` tracks the GPU-relevant hot paths. Baselines are committed under `benchmarks/baselines/`. The current reference is `0006` (`perf_cleanup`) on the reference machine: RTX 4070 Ti, Ryzen 7 7800X3D, WSL2. It is the first baseline 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.
|
|
282
|
+
|
|
283
|
+
- **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`.
|
|
284
|
+
- **Baselines** are recorded with `benchmarks/record_baseline.py`: each file in its own process, best of three passes (`--passes`; `0006` 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.
|
|
285
|
+
|
|
286
|
+
- **IDs** are `[cpu]` or `[gpu]`, meaning the input device.
|
|
287
|
+
- **Timing:**
|
|
288
|
+
- Timed bodies end with the `sync` fixture; otherwise GPU timings measure launches, not execution.
|
|
289
|
+
- Each benchmark has one warmup round, so compilation and pool growth are excluded.
|
|
290
|
+
- **Workloads** come from `benchmarks/workloads.py` with fixed seeds; never generate data inline.
|
|
291
|
+
- **Tools:** `benchmarks/benchutils.py` provides `CudaEventTimer` and `nvtx_range` for `nsys` profiling.
|
|
292
|
+
- **Logging** is set to WARNING in `benchmarks/conftest.py`.
|
|
293
|
+
- **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.
|
|
294
|
+
- A commit touching a hot path quotes its delta against `0006`. Tolerance is 5% on CPU and 10% on GPU.
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## 9. Roadmap and open items
|
|
299
|
+
|
|
300
|
+
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.
|
|
301
|
+
|
|
302
|
+
### Switch to Python 3.15 (blocked on CuPy)
|
|
303
|
+
|
|
304
|
+
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:
|
|
305
|
+
|
|
306
|
+
- **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.
|
|
307
|
+
- **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.
|
|
308
|
+
- **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.
|
|
309
|
+
|
|
310
|
+
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.
|
|
311
|
+
|
|
312
|
+
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.
|
|
313
|
+
2. **Switch the version floor**:
|
|
314
|
+
- `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).
|
|
315
|
+
- `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.
|
|
316
|
+
- `uv lock`; README install requirements; §2 here ("Python 3.12+").
|
|
317
|
+
- Benchmarks: record a new baseline on 3.15 (it lands in `benchmarks/baselines/Linux-CPython-3.15-64bit/`), compare it with `0006` by a paired A/B of 3.14 against 3.15 on unchanged code, and make it the reference.
|
|
318
|
+
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 `*`.
|
|
319
|
+
- 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/`.
|
|
320
|
+
- 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`.
|
|
321
|
+
- 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`).
|
|
322
|
+
- 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.
|
|
323
|
+
- 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.
|
|
324
|
+
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):
|
|
325
|
+
- 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.
|
|
326
|
+
- UTF-8 mode is the default from 3.15 (PEP 686): check `io` and any `open()` without `encoding=`.
|
|
327
|
+
- `strptime` with a day and no year now raises (the `jupyter_client` failure above); grep commkit for date parsing.
|
|
328
|
+
- 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.
|
|
329
|
+
|
|
330
|
+
### CUDA sanitizer pass (blocked on the WDDM debugger interface)
|
|
331
|
+
|
|
332
|
+
The kernels have not run under `compute-sanitizer` (§2): on the reference machine (WSL2) it needs the Windows GPU debugger interface, which is not enabled yet. The review that stands in for it (every `.cu` kernel and wrapper read for bounds, barriers, races and overflow; empty, one-sample and NaN/Inf inputs; 4 KB guard zones around every array the library allocated during the GPU kernel tests, 565 tests and 57,723 arrays, none touched) catches out-of-bounds writes but not out-of-bounds reads or reads of uninitialized memory. Once the interface is enabled, run the four tools of §2, fix what they report, and remove this item.
|
|
333
|
+
|
|
334
|
+
### Planned modules
|
|
335
|
+
|
|
336
|
+
1. **Channel coding** (`coding`, a placeholder package). Soft demapping is GPU-validated on both devices. Start with one soft decoder (LDPC) that consumes `compute_llr` output (positive LLR = bit 0) and reports post-FEC BER next to `gmi`, then add encoders. The package stays out of `commkit/__init__.py` until a real encode/decode entry point exists. Decoders are iterative message passing over many codewords: batch codewords on the GPU from the start.
|
|
337
|
+
|
|
338
|
+
### Nonlinear fiber channel: future features
|
|
339
|
+
|
|
340
|
+
Each is an addition that leaves the current API unchanged.
|
|
341
|
+
|
|
342
|
+
- **Batched realizations**: an extra leading axis `(B, 2, N)` (launch-power sweeps, Monte Carlo over seeds) so cuFFT batches them and short records fill the GPU.
|
|
343
|
+
- **Adaptive step rules**: a `step_rule=` object (maximum nonlinear phase per step, local-error method) next to the equal `steps_per_span`, which measured 3x more accurate than logarithmic steps on a 32 GBd, 80 km span.
|
|
344
|
+
- **WDM helpers**: combining frequency-shifted channels into one wideband field and selecting one after propagation; the solver already handles the wide field (with `dispersion_slope_ps_nm2_km` set).
|
|
345
|
+
- **Distributed (Raman) amplification and per-span fiber types** in one call (chaining calls already works: every call speaks launch-relative units).
|
|
346
|
+
- **cuFFT callbacks** fusing the linear stage into the transforms (CuPy's support is experimental and compiles with nvcc at run time).
|
|
347
|
+
- **Self-steepening and Raman scattering** for ultra-wideband fields.
|
|
@@ -0,0 +1,116 @@
|
|
|
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
|
+
## [2.3.0] - 2026-10-28
|
|
6
|
+
|
|
7
|
+
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.
|
|
8
|
+
|
|
9
|
+
### Changes that may need code updates
|
|
10
|
+
|
|
11
|
+
- **`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.
|
|
12
|
+
- **`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)`.
|
|
13
|
+
- **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.
|
|
14
|
+
- **`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.
|
|
15
|
+
- **`moving_average(mode="valid")`** raises on a record shorter than the window, instead of returning NumPy's swapped convolution.
|
|
16
|
+
- **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:
|
|
17
|
+
- fiber propagation on the CPU: about 3e-6 relative;
|
|
18
|
+
- `lms` with inline carrier recovery: up to 5e-6 relative;
|
|
19
|
+
- `correct_iq_imbalance`: 5e-7 relative in complex64;
|
|
20
|
+
- `rde`: 2.5e-7 relative.
|
|
21
|
+
|
|
22
|
+
Everything else changed in this release is bit-identical or within float32 rounding of before.
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- **Single-precision `iir_filter` on both devices.**
|
|
27
|
+
- 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.
|
|
28
|
+
- GPU: a block-parallel CUDA kernel. Blocks are filtered in parallel and the state is carried across them in two levels, in double precision.
|
|
29
|
+
- There is no longer a double-precision copy of the record: complex64 is 0.32–0.41x of the previous GPU path.
|
|
30
|
+
- **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.
|
|
31
|
+
- GPU, every order: 0.87x at 16-QAM, 0.74x at 256-QAM.
|
|
32
|
+
- CPU, from 256-QAM (where it starts to pay): 0.65x at 256-QAM, 0.40x at 1024-QAM, 0.23x at 4096-QAM.
|
|
33
|
+
- Exact LLRs and shaped constellations keep the per-level computation.
|
|
34
|
+
- `TimingEstimate` is listed in `commkit.timing.__all__`, like the other estimate classes.
|
|
35
|
+
- `pad_mode="reflect"` is documented for the equalizers and `apply_taps`; they already accepted it.
|
|
36
|
+
|
|
37
|
+
### Performance
|
|
38
|
+
|
|
39
|
+
Equalizers:
|
|
40
|
+
|
|
41
|
+
- **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`:
|
|
42
|
+
- `block_lms` at `block_size=256`: 0.36x without carrier recovery, 0.78x with BPS, 0.65x with BPS and cycle slips;
|
|
43
|
+
- `block_cma`: 0.50x;
|
|
44
|
+
- `block_rde`: 0.66x.
|
|
45
|
+
|
|
46
|
+
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.
|
|
47
|
+
- **`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.
|
|
48
|
+
- **`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.
|
|
49
|
+
- **`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.
|
|
50
|
+
|
|
51
|
+
Carrier and frequency recovery:
|
|
52
|
+
|
|
53
|
+
- **BPS of square QAM on the CPU:** one Numba pass instead of a chunked NumPy loop: 0.39x.
|
|
54
|
+
- **PLL:** reads the samples in their own precision, with no complex128 copy of the record: 0.95–0.97x.
|
|
55
|
+
- **Blockwise `MengaliMorelli`:** one Numba call for all blocks instead of one per block. CPU 0.85x, GPU 0.62x.
|
|
56
|
+
- **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.
|
|
57
|
+
|
|
58
|
+
Mapping and LLRs:
|
|
59
|
+
|
|
60
|
+
- **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`.
|
|
61
|
+
- **Square QAM on the GPU:** demapped per axis, 2·sqrt(M) metrics instead of M.
|
|
62
|
+
- **Hard decisions of PSK and cross QAM on the GPU:** one kernel launch instead of a Python loop over chunks.
|
|
63
|
+
- **`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.
|
|
64
|
+
- **`Constellation.rotational_symmetry`:** 1024-QAM in 0.026 s instead of 2.2 s.
|
|
65
|
+
|
|
66
|
+
Other functions:
|
|
67
|
+
|
|
68
|
+
- **`correct_iq_imbalance`:** all channels at once from closed-form 2×2 statistics, CPU about 0.45x.
|
|
69
|
+
- **`evm` and `snr`:** measure the reference power once: 0.74–0.78x.
|
|
70
|
+
- **`moving_average(mode="valid")`:** one pass over all channels. CPU 0.11–0.15x, GPU 0.21–0.71x.
|
|
71
|
+
- **`estimate_timing` with several templates:** transforms the signal once: CPU 0.62–0.72x.
|
|
72
|
+
- **`plot_eye_diagram` histograms:** CPU 0.33x, GPU 0.55x.
|
|
73
|
+
- **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.
|
|
74
|
+
- **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.
|
|
75
|
+
|
|
76
|
+
### Fixed
|
|
77
|
+
|
|
78
|
+
- `plot_eye_diagram` ignored `vmin` and `vmax`.
|
|
79
|
+
- BPS on the GPU failed with `CUDA_ERROR_INVALID_VALUE` for more than 65535 channels.
|
|
80
|
+
- 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.
|
|
81
|
+
|
|
82
|
+
### Development
|
|
83
|
+
|
|
84
|
+
- **Test plugins:**
|
|
85
|
+
- `tests/plugins/transfer_trace.py` audits host-device copies in a GPU run.
|
|
86
|
+
- `tests/plugins/precision_trace.py` now follows single-precision data through the FFTs.
|
|
87
|
+
- **CI:** the JIT-disabled kernel-coverage job measures the equalizers' Numba kernels again, and strict mypy covers every module of the `filtering` package.
|
|
88
|
+
- **Benchmarks:** new benchmarks for the IIR filter, moving average, PLL, blockwise Mengali-Morelli and IQ correction. Baseline `0006` is the new reference.
|
|
89
|
+
- **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.
|
|
90
|
+
- **Docs:** docstrings follow one style throughout. AGENTS.md documents the CUDA review rules and how to run `compute-sanitizer`.
|
|
91
|
+
|
|
92
|
+
## [2.2.0] - 2026-10-23
|
|
93
|
+
|
|
94
|
+
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).
|
|
95
|
+
|
|
96
|
+
## [2.1.0] - 2026-10-21
|
|
97
|
+
|
|
98
|
+
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).
|
|
99
|
+
|
|
100
|
+
## [2.0.0] - 2026-10-07
|
|
101
|
+
|
|
102
|
+
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).
|
|
103
|
+
|
|
104
|
+
## [1.1.0] - 2026-09-03
|
|
105
|
+
|
|
106
|
+
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).
|
|
107
|
+
|
|
108
|
+
## [1.0.0] - 2026-08-31
|
|
109
|
+
|
|
110
|
+
First release. See the [GitHub release](https://github.com/lokgar/commkit/releases/tag/v1.0.0).
|
|
111
|
+
|
|
112
|
+
[2.3.0]: https://github.com/lokgar/commkit/compare/v2.2.0...v2.3.0
|
|
113
|
+
[2.2.0]: https://github.com/lokgar/commkit/compare/v2.1.0...v2.2.0
|
|
114
|
+
[2.1.0]: https://github.com/lokgar/commkit/compare/v2.0.0...v2.1.0
|
|
115
|
+
[2.0.0]: https://github.com/lokgar/commkit/compare/v1.1.0...v2.0.0
|
|
116
|
+
[1.1.0]: https://github.com/lokgar/commkit/compare/v1.0.0...v1.1.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: commkit
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: A Python library for high-performance digital communications research.
|
|
5
5
|
Project-URL: Homepage, https://github.com/lokgar/commkit
|
|
6
6
|
Project-URL: Repository, https://github.com/lokgar/commkit
|
|
@@ -79,7 +79,7 @@ res = ck.equalization.lms(
|
|
|
79
79
|
y = res.signal # 1-SPS Signal, reference aligned
|
|
80
80
|
print(f"EVM {ck.metrics.evm(y, num_skip_symbols=2000):.1f} %, "
|
|
81
81
|
f"BER {ck.metrics.ber(y, num_skip_symbols=2000):.1e}")
|
|
82
|
-
# EVM 12.
|
|
82
|
+
# EVM 12.7 %, BER 2.4e-04 (on the CPU; the GPU draws other noise)
|
|
83
83
|
|
|
84
84
|
# The next record continues from the converged taps and CPR state.
|
|
85
85
|
res2 = ck.equalization.lms(rx, num_taps=21, step_size=1e-3, cpr=cpr, state=res.state)
|
|
@@ -101,7 +101,7 @@ CommKit never picks a device for you: data stays where you put it, and each func
|
|
|
101
101
|
| Frequency-domain equalizers `block_lms`, `block_cma`, `block_rde` | runs on the GPU (CUDA graphs) |
|
|
102
102
|
| Sequential equalizers `lms`, `rls`, `cma`, `rde`; `PLL`, `Tikhonov`, cycle-slip correction | runs on the CPU (Numba): one copy to the host and back, no speedup |
|
|
103
103
|
|
|
104
|
-
A sample-by-sample recursion has no parallel work per step. For long records on the GPU, prefer the block equalizers with `block_size=1024` or more. Each block costs a fixed overhead: at 256 the GPU
|
|
104
|
+
A sample-by-sample recursion has no parallel work per step. For long records on the GPU, prefer the block equalizers with `block_size=1024` or more. Each block costs a fixed overhead: at 256 the GPU is about 2x faster than the CPU without carrier recovery and 5x with BPS, at 2048 about 4x and 13x. A larger block also lowers the stable `step_size`.
|
|
105
105
|
- **Short records do not pay off.** Launch overhead dominates below roughly 10⁴-10⁵ samples, where the CPU is as fast.
|
|
106
106
|
- **Batch channels and records.** Pass `(C, N)` arrays rather than looping over channels in Python: one call does the work of C.
|
|
107
107
|
- **Keep scalars on the device inside loops.** `float(x)`, `x.item()` and `if x > 0:` on a CuPy array wait for the GPU and copy. Collect values in an array and transfer once.
|
|
@@ -116,10 +116,10 @@ CommKit never picks a device for you: data stays where you put it, and each func
|
|
|
116
116
|
| --- | --- |
|
|
117
117
|
| [`commkit.core`](https://github.com/lokgar/commkit/tree/main/commkit/core) | `Signal`, `Reference`, `generate`, `SingleCarrierFrame` and `Preamble` (pilots, guard intervals, MIMO streams), `extract_payload`. |
|
|
118
118
|
| [`commkit.mapping`](https://github.com/lokgar/commkit/tree/main/commkit/mapping) | `Constellation` (QAM, PSK, PAM, arbitrary points; Gray labels; probabilistic shaping with `.shaped()`), bit mapping, hard demapping, max-log and exact LLRs. |
|
|
119
|
-
| [`commkit.filtering`](https://github.com/lokgar/commkit/
|
|
119
|
+
| [`commkit.filtering`](https://github.com/lokgar/commkit/tree/main/commkit/filtering) | Pulses (`RRC`, `RC`, `Gaussian`, `Rect`, `SmoothRect`), FIR and IIR design (Butterworth, Chebyshev I/II, elliptic, Bessel), `fir_filter`, `iir_filter`, `matched_filter`, overlap-save, chromatic-dispersion compensation and digital backpropagation (`correct_fiber_propagation`). |
|
|
120
120
|
| [`commkit.multirate`](https://github.com/lokgar/commkit/blob/main/commkit/multirate.py) | `resample` (fractional), `decimate`, `upsample`, `decimate_to_symbol_rate`. |
|
|
121
121
|
| [`commkit.spectral`](https://github.com/lokgar/commkit/blob/main/commkit/spectral.py) | Welch PSD, spectrograms, frequency shifting, pilot tones. |
|
|
122
|
-
| [`commkit.impairments`](https://github.com/lokgar/commkit/tree/main/commkit/impairments) | AWGN, phase noise, IQ imbalance (with Löwdin and Gram-Schmidt correction), chromatic dispersion, PMD and polarization mixing
|
|
122
|
+
| [`commkit.impairments`](https://github.com/lokgar/commkit/tree/main/commkit/impairments) | AWGN, phase noise, IQ imbalance (with Löwdin and Gram-Schmidt correction), chromatic dispersion, PMD and polarization mixing, and nonlinear fiber propagation over amplified spans (split-step Kerr effect with dispersion, loss, PMD and ASE) of a `Fiber`. |
|
|
123
123
|
| [`commkit.timing`](https://github.com/lokgar/commkit/blob/main/commkit/timing.py) | Barker and Zadoff-Chu sequences, `estimate_timing` / `correct_timing`, fractional delay estimation and correction. |
|
|
124
124
|
| [`commkit.frequency`](https://github.com/lokgar/commkit/blob/main/commkit/frequency.py) | `estimate_frequency_offset` / `correct_frequency_offset` with `MthPower`, `MengaliMorelli`, `PilotSymbols` and `BiasTone`; static and blockwise. |
|
|
125
125
|
| [`commkit.recovery`](https://github.com/lokgar/commkit/tree/main/commkit/recovery) | `estimate_carrier_phase` / `correct_carrier_phase` with `ViterbiViterbi`, `BPS`, `PLL`, `Tikhonov`, `DataAided`, `PilotAided`, `PilotTone(s)`; cycle-slip correction; `resolve_phase_ambiguity` and `resolve_channel_permutation`. |
|
|
@@ -160,15 +160,15 @@ With [`uv`](https://github.com/astral-sh/uv), use `uv pip install` in place of `
|
|
|
160
160
|
|
|
161
161
|
## Examples
|
|
162
162
|
|
|
163
|
-
|
|
163
|
+
[`examples/`](https://github.com/lokgar/commkit/tree/main/examples) is a course in 26 Jupyter notebooks, one topic each, in teaching order (install the `notebook` extra and run `jupyter lab examples`):
|
|
164
164
|
|
|
165
|
-
-
|
|
166
|
-
-
|
|
167
|
-
-
|
|
168
|
-
|
|
169
|
-
-
|
|
165
|
+
- **00-05, signals and DSP**: the `Signal`, constellations and bit mapping, spectra, filters, pulse shaping and matched filtering, resampling;
|
|
166
|
+
- **06-08, channel and performance**: AWGN and the metrics (SNR, BER, LLRs, GMI), laser phase noise, front-end impairments;
|
|
167
|
+
- **09-16, receiver algorithms**: frame synchronization, frequency-offset estimation, blind and pilot-aided carrier recovery, linear, adaptive and blind equalization, polarization demultiplexing;
|
|
168
|
+
- **17-19, optical fiber systems**: chromatic dispersion, the nonlinear fiber with digital backpropagation, and an end-to-end coherent transceiver;
|
|
169
|
+
- **20-25, characterization**: carrier phase and laser linewidth measurements, with templates for real captures.
|
|
170
170
|
|
|
171
|
-
The notebooks are committed without outputs and run in CI.
|
|
171
|
+
[`examples/README.md`](https://github.com/lokgar/commkit/blob/main/examples/README.md) lists every notebook with its topics. The quickstart above appears cell by cell in `00-getting-started` and `14-adaptive-equalization`. The notebooks are committed without outputs and run in CI.
|
|
172
172
|
|
|
173
173
|
## Development
|
|
174
174
|
|