commkit 1.1.0__tar.gz → 2.1.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/.gitattributes +1 -0
- {commkit-1.1.0 → commkit-2.1.0}/.github/workflows/ci.yml +48 -1
- commkit-2.1.0/AGENTS.md +264 -0
- {commkit-1.1.0 → commkit-2.1.0}/LICENSE +1 -1
- commkit-2.1.0/PKG-INFO +190 -0
- commkit-2.1.0/README.md +154 -0
- commkit-2.1.0/benchmarks/baselines/Linux-CPython-3.14-64bit/0002_pre_v2.json +3257 -0
- commkit-2.1.0/benchmarks/baselines/Linux-CPython-3.14-64bit/0003_v2_0.json +3157 -0
- {commkit-1.1.0 → commkit-2.1.0}/benchmarks/bench_block_blind.py +24 -8
- {commkit-1.1.0 → commkit-2.1.0}/benchmarks/bench_block_lms.py +36 -20
- {commkit-1.1.0 → commkit-2.1.0}/benchmarks/bench_bps.py +15 -12
- commkit-2.1.0/benchmarks/bench_equalizers.py +93 -0
- commkit-2.1.0/benchmarks/bench_import.py +30 -0
- commkit-2.1.0/benchmarks/bench_llr.py +52 -0
- commkit-2.1.0/benchmarks/bench_signal_pipeline.py +146 -0
- {commkit-1.1.0 → commkit-2.1.0}/benchmarks/bench_sync_misc.py +10 -5
- {commkit-1.1.0 → commkit-2.1.0}/benchmarks/benchutils.py +20 -0
- {commkit-1.1.0 → commkit-2.1.0}/benchmarks/conftest.py +1 -9
- commkit-2.1.0/benchmarks/record_baseline.py +74 -0
- {commkit-1.1.0 → commkit-2.1.0}/benchmarks/workloads.py +40 -12
- commkit-2.1.0/commkit/__init__.py +106 -0
- commkit-2.1.0/commkit/_array.py +294 -0
- commkit-2.1.0/commkit/_circular.py +74 -0
- {commkit-1.1.0 → commkit-2.1.0}/commkit/_cuda/__init__.py +160 -14
- {commkit-1.1.0 → commkit-2.1.0}/commkit/_cuda/compiler.py +4 -1
- commkit-2.1.0/commkit/_cuda/src/bps_anchor.cu +146 -0
- commkit-2.1.0/commkit/_cuda/src/cs_block.cu +248 -0
- commkit-2.1.0/commkit/_cuda/src/llr.cu +110 -0
- commkit-2.1.0/commkit/_dispersion.py +84 -0
- commkit-2.1.0/commkit/_overlap_save.py +94 -0
- commkit-2.1.0/commkit/_random.py +39 -0
- commkit-2.1.0/commkit/_sequences.py +146 -0
- {commkit-1.1.0 → commkit-2.1.0}/commkit/analysis/__init__.py +36 -17
- {commkit-1.1.0 → commkit-2.1.0}/commkit/analysis/_common.py +73 -0
- {commkit-1.1.0 → commkit-2.1.0}/commkit/analysis/allan.py +36 -26
- commkit-2.1.0/commkit/analysis/drift.py +187 -0
- commkit-2.1.0/commkit/analysis/fm_noise.py +321 -0
- commkit-2.1.0/commkit/analysis/interferometry.py +366 -0
- commkit-2.1.0/commkit/analysis/linewidth.py +1159 -0
- {commkit-1.1.0 → commkit-2.1.0}/commkit/analysis/trajectory.py +28 -12
- commkit-2.1.0/commkit/backend.py +270 -0
- {commkit-1.1.0 → commkit-2.1.0}/commkit/coding/__init__.py +1 -1
- {commkit-1.1.0 → commkit-2.1.0}/commkit/core/__init__.py +6 -14
- commkit-2.1.0/commkit/core/_signal_adapter.py +164 -0
- {commkit-1.1.0 → commkit-2.1.0}/commkit/core/frame.py +332 -442
- commkit-2.1.0/commkit/core/generation.py +236 -0
- commkit-2.1.0/commkit/core/signal.py +454 -0
- {commkit-1.1.0 → commkit-2.1.0}/commkit/equalization/__init__.py +8 -11
- {commkit-1.1.0 → commkit-2.1.0}/commkit/equalization/_block/__init__.py +2 -14
- commkit-2.1.0/commkit/equalization/_block/_blind.py +169 -0
- commkit-2.1.0/commkit/equalization/_block/_dd.py +867 -0
- commkit-2.1.0/commkit/equalization/_block/_engine.py +374 -0
- commkit-2.1.0/commkit/equalization/_common.py +378 -0
- {commkit-1.1.0 → commkit-2.1.0}/commkit/equalization/_kernels_numba.py +108 -64
- {commkit-1.1.0 → commkit-2.1.0}/commkit/equalization/blind.py +48 -124
- {commkit-1.1.0 → commkit-2.1.0}/commkit/equalization/linear.py +29 -54
- {commkit-1.1.0 → commkit-2.1.0}/commkit/equalization/polarization.py +101 -144
- commkit-2.1.0/commkit/equalization/result.py +302 -0
- {commkit-1.1.0 → commkit-2.1.0}/commkit/equalization/sequential/__init__.py +1 -2
- commkit-2.1.0/commkit/equalization/sequential/_blind.py +494 -0
- commkit-2.1.0/commkit/equalization/sequential/_dd.py +732 -0
- commkit-2.1.0/commkit/equalization/sequential/_setup.py +454 -0
- {commkit-1.1.0 → commkit-2.1.0}/commkit/filtering.py +388 -406
- commkit-2.1.0/commkit/frequency.py +914 -0
- {commkit-1.1.0 → commkit-2.1.0}/commkit/impairments/__init__.py +6 -10
- {commkit-1.1.0 → commkit-2.1.0}/commkit/impairments/channel/linear.py +78 -142
- commkit-2.1.0/commkit/impairments/frontend.py +211 -0
- {commkit-1.1.0 → commkit-2.1.0}/commkit/impairments/noise.py +33 -56
- {commkit-1.1.0 → commkit-2.1.0}/commkit/impairments/source.py +107 -89
- commkit-2.1.0/commkit/io.py +347 -0
- {commkit-1.1.0 → commkit-2.1.0}/commkit/logger.py +26 -37
- commkit-2.1.0/commkit/mapping/__init__.py +33 -0
- commkit-2.1.0/commkit/mapping/bits.py +90 -0
- commkit-2.1.0/commkit/mapping/constellation.py +393 -0
- {commkit-1.1.0 → commkit-2.1.0}/commkit/mapping/gray.py +15 -21
- commkit-2.1.0/commkit/mapping/llr.py +255 -0
- commkit-2.1.0/commkit/mapping/shaping.py +185 -0
- commkit-2.1.0/commkit/math.py +324 -0
- commkit-2.1.0/commkit/metrics.py +685 -0
- commkit-2.1.0/commkit/multirate.py +381 -0
- {commkit-1.1.0 → commkit-2.1.0}/commkit/plotting/__init__.py +4 -11
- {commkit-1.1.0 → commkit-2.1.0}/commkit/plotting/analysis.py +135 -150
- commkit-2.1.0/commkit/plotting/constellation.py +430 -0
- {commkit-1.1.0 → commkit-2.1.0}/commkit/plotting/equalizer.py +7 -4
- {commkit-1.1.0 → commkit-2.1.0}/commkit/plotting/eye.py +81 -51
- {commkit-1.1.0 → commkit-2.1.0}/commkit/plotting/filter_response.py +3 -0
- {commkit-1.1.0 → commkit-2.1.0}/commkit/plotting/spectral.py +164 -77
- {commkit-1.1.0 → commkit-2.1.0}/commkit/plotting/sync.py +456 -27
- {commkit-1.1.0 → commkit-2.1.0}/commkit/plotting/theme.py +2 -0
- {commkit-1.1.0 → commkit-2.1.0}/commkit/plotting/waveform.py +92 -48
- commkit-2.1.0/commkit/recovery/__init__.py +54 -0
- commkit-2.1.0/commkit/recovery/_common.py +256 -0
- commkit-2.1.0/commkit/recovery/bps.py +306 -0
- commkit-2.1.0/commkit/recovery/carrier_phase.py +283 -0
- {commkit-1.1.0 → commkit-2.1.0}/commkit/recovery/corrections.py +273 -397
- commkit-2.1.0/commkit/recovery/pilots.py +630 -0
- commkit-2.1.0/commkit/recovery/pll.py +378 -0
- commkit-2.1.0/commkit/recovery/tikhonov.py +274 -0
- commkit-2.1.0/commkit/recovery/viterbi_viterbi.py +123 -0
- {commkit-1.1.0 → commkit-2.1.0}/commkit/smoothing.py +6 -5
- {commkit-1.1.0 → commkit-2.1.0}/commkit/spectral.py +170 -164
- {commkit-1.1.0 → commkit-2.1.0}/commkit/timing.py +343 -342
- commkit-2.1.0/examples/carrier_phase_analysis.ipynb +515 -0
- commkit-2.1.0/examples/laser_linewidth_dsh.ipynb +787 -0
- commkit-2.1.0/examples/laser_linewidth_homodyne_iq.ipynb +497 -0
- commkit-2.1.0/examples/measurement_laser_linewidth_dsh.ipynb +318 -0
- commkit-2.1.0/examples/measurement_laser_linewidth_homodyne_iq.ipynb +336 -0
- commkit-2.1.0/examples/qam_receiver_quickstart.ipynb +313 -0
- {commkit-1.1.0 → commkit-2.1.0}/pyproject.toml +83 -20
- commkit-2.1.0/tests/analysis/test_allan.py +34 -0
- commkit-2.1.0/tests/analysis/test_drift.py +45 -0
- commkit-2.1.0/tests/analysis/test_fm_noise.py +60 -0
- commkit-2.1.0/tests/analysis/test_interferometry.py +146 -0
- commkit-2.1.0/tests/analysis/test_linewidth.py +568 -0
- commkit-2.1.0/tests/analysis/test_trajectory.py +112 -0
- commkit-2.1.0/tests/common/__init__.py +62 -0
- commkit-2.1.0/tests/common/conversions.py +47 -0
- commkit-2.1.0/tests/common/kernel_utils.py +140 -0
- commkit-2.1.0/tests/common/metrics.py +131 -0
- commkit-2.1.0/tests/common/periodic.py +40 -0
- commkit-2.1.0/tests/common/reference_impl.py +539 -0
- commkit-2.1.0/tests/common/signals.py +412 -0
- {commkit-1.1.0 → commkit-2.1.0}/tests/conftest.py +59 -17
- commkit-2.1.0/tests/core/test_frame.py +555 -0
- commkit-2.1.0/tests/core/test_generation.py +226 -0
- commkit-2.1.0/tests/core/test_psqam.py +272 -0
- commkit-2.1.0/tests/core/test_signal.py +875 -0
- commkit-2.1.0/tests/core/test_signal_adapter.py +107 -0
- commkit-2.1.0/tests/core/test_signal_mimo.py +169 -0
- {commkit-1.1.0 → commkit-2.1.0}/tests/equalization/test_blind.py +65 -81
- commkit-2.1.0/tests/equalization/test_block.py +760 -0
- commkit-2.1.0/tests/equalization/test_bps_kernel.py +333 -0
- commkit-2.1.0/tests/equalization/test_cpr.py +753 -0
- commkit-2.1.0/tests/equalization/test_cs_kernel.py +116 -0
- commkit-2.1.0/tests/equalization/test_equalizer_oracles.py +300 -0
- {commkit-1.1.0 → commkit-2.1.0}/tests/equalization/test_linear.py +17 -38
- {commkit-1.1.0 → commkit-2.1.0}/tests/equalization/test_mimo.py +44 -67
- {commkit-1.1.0 → commkit-2.1.0}/tests/equalization/test_polarization.py +146 -122
- {commkit-1.1.0 → commkit-2.1.0}/tests/equalization/test_sequential.py +359 -432
- commkit-2.1.0/tests/equalization/test_state.py +226 -0
- commkit-2.1.0/tests/equalization/test_winit.py +319 -0
- {commkit-1.1.0 → commkit-2.1.0}/tests/impairments/channel/test_channel_linear.py +95 -41
- {commkit-1.1.0 → commkit-2.1.0}/tests/impairments/test_frontend.py +48 -41
- {commkit-1.1.0 → commkit-2.1.0}/tests/impairments/test_noise.py +36 -10
- {commkit-1.1.0 → commkit-2.1.0}/tests/impairments/test_source.py +119 -35
- commkit-2.1.0/tests/mapping/test_bits.py +137 -0
- commkit-2.1.0/tests/mapping/test_constellation.py +296 -0
- commkit-2.1.0/tests/mapping/test_gray.py +166 -0
- commkit-2.1.0/tests/mapping/test_llr.py +314 -0
- commkit-2.1.0/tests/plotting/test_plot_analysis.py +159 -0
- commkit-2.1.0/tests/plotting/test_plot_constellation.py +195 -0
- commkit-2.1.0/tests/plotting/test_plot_equalizer.py +191 -0
- commkit-2.1.0/tests/plotting/test_plot_eye.py +181 -0
- commkit-2.1.0/tests/plotting/test_plot_filter_response.py +71 -0
- commkit-2.1.0/tests/plotting/test_plot_spectral.py +264 -0
- commkit-2.1.0/tests/plotting/test_plot_sync.py +214 -0
- commkit-2.1.0/tests/plotting/test_plot_theme.py +28 -0
- commkit-2.1.0/tests/plotting/test_plot_waveform.py +113 -0
- commkit-2.1.0/tests/recovery/test_bps.py +252 -0
- commkit-2.1.0/tests/recovery/test_corrections.py +620 -0
- commkit-2.1.0/tests/recovery/test_joint.py +85 -0
- {commkit-1.1.0 → commkit-2.1.0}/tests/recovery/test_pilots.py +339 -270
- commkit-2.1.0/tests/recovery/test_pll.py +216 -0
- commkit-2.1.0/tests/recovery/test_recovery_oracles.py +125 -0
- commkit-2.1.0/tests/recovery/test_tikhonov.py +269 -0
- commkit-2.1.0/tests/recovery/test_viterbi_viterbi.py +229 -0
- commkit-2.1.0/tests/test_api_contracts.py +933 -0
- commkit-2.1.0/tests/test_array.py +153 -0
- commkit-2.1.0/tests/test_backend.py +117 -0
- commkit-2.1.0/tests/test_cuda_infra.py +111 -0
- commkit-2.1.0/tests/test_dispersion.py +107 -0
- commkit-2.1.0/tests/test_examples.py +64 -0
- commkit-2.1.0/tests/test_filtering.py +453 -0
- {commkit-1.1.0 → commkit-2.1.0}/tests/test_frequency.py +407 -354
- commkit-2.1.0/tests/test_import_hygiene.py +116 -0
- commkit-2.1.0/tests/test_io.py +360 -0
- commkit-2.1.0/tests/test_logger.py +64 -0
- commkit-2.1.0/tests/test_math.py +169 -0
- commkit-2.1.0/tests/test_metrics.py +650 -0
- commkit-2.1.0/tests/test_multirate.py +159 -0
- commkit-2.1.0/tests/test_pulse_shaping.py +227 -0
- commkit-2.1.0/tests/test_signal_pipeline.py +339 -0
- commkit-2.1.0/tests/test_smoothing.py +56 -0
- commkit-2.1.0/tests/test_spectral.py +379 -0
- commkit-2.1.0/tests/test_timing.py +882 -0
- {commkit-1.1.0 → commkit-2.1.0}/uv.lock +90 -268
- commkit-1.1.0/.gitattributes +0 -1
- commkit-1.1.0/CLAUDE.md +0 -539
- commkit-1.1.0/PKG-INFO +0 -165
- commkit-1.1.0/README.md +0 -138
- commkit-1.1.0/benchmarks/baselines/Linux-CPython-3.14-64bit/0001_commkit_baseline.json +0 -2420
- commkit-1.1.0/benchmarks/bench_equalizers.py +0 -174
- commkit-1.1.0/commkit/__init__.py +0 -74
- commkit-1.1.0/commkit/_cuda/src/cs_block.cu +0 -119
- commkit-1.1.0/commkit/analysis/drift.py +0 -225
- commkit-1.1.0/commkit/analysis/interferometry.py +0 -948
- commkit-1.1.0/commkit/analysis/linewidth.py +0 -472
- commkit-1.1.0/commkit/backend.py +0 -519
- commkit-1.1.0/commkit/core/generation.py +0 -770
- commkit-1.1.0/commkit/core/signal.py +0 -727
- commkit-1.1.0/commkit/equalization/_block/_blind.py +0 -371
- commkit-1.1.0/commkit/equalization/_block/_dd.py +0 -1162
- commkit-1.1.0/commkit/equalization/_block/_seqmode.py +0 -421
- commkit-1.1.0/commkit/equalization/_common.py +0 -588
- commkit-1.1.0/commkit/equalization/_kernels_jax.py +0 -1720
- commkit-1.1.0/commkit/equalization/result.py +0 -193
- commkit-1.1.0/commkit/equalization/sequential/_blind.py +0 -1089
- commkit-1.1.0/commkit/equalization/sequential/_dd.py +0 -1980
- commkit-1.1.0/commkit/frequency.py +0 -1451
- commkit-1.1.0/commkit/helpers.py +0 -1108
- commkit-1.1.0/commkit/impairments/frontend.py +0 -246
- commkit-1.1.0/commkit/io.py +0 -308
- commkit-1.1.0/commkit/mapping/__init__.py +0 -58
- commkit-1.1.0/commkit/mapping/bits.py +0 -273
- commkit-1.1.0/commkit/mapping/constellation.py +0 -147
- commkit-1.1.0/commkit/mapping/llr.py +0 -302
- commkit-1.1.0/commkit/mapping/shaping.py +0 -257
- commkit-1.1.0/commkit/metrics.py +0 -1041
- commkit-1.1.0/commkit/multirate.py +0 -434
- commkit-1.1.0/commkit/plotting/constellation.py +0 -483
- commkit-1.1.0/commkit/recovery/__init__.py +0 -51
- commkit-1.1.0/commkit/recovery/_common.py +0 -119
- commkit-1.1.0/commkit/recovery/bps.py +0 -376
- commkit-1.1.0/commkit/recovery/pilots.py +0 -866
- commkit-1.1.0/commkit/recovery/pll.py +0 -516
- commkit-1.1.0/commkit/recovery/tikhonov.py +0 -434
- commkit-1.1.0/commkit/recovery/viterbi_viterbi.py +0 -215
- commkit-1.1.0/examples/carrier_phase_analysis.py +0 -378
- commkit-1.1.0/examples/laser_linewidth_dsh.py +0 -498
- commkit-1.1.0/examples/laser_linewidth_homodyne_iq.py +0 -358
- commkit-1.1.0/examples/measurement_laser_linewidth_dsh.py +0 -206
- commkit-1.1.0/examples/measurement_laser_linewidth_homodyne_iq.py +0 -212
- commkit-1.1.0/tests/analysis/test_allan.py +0 -25
- commkit-1.1.0/tests/analysis/test_drift.py +0 -44
- commkit-1.1.0/tests/analysis/test_interferometry.py +0 -289
- commkit-1.1.0/tests/analysis/test_linewidth.py +0 -135
- commkit-1.1.0/tests/analysis/test_trajectory.py +0 -97
- commkit-1.1.0/tests/core/test_frame.py +0 -484
- commkit-1.1.0/tests/core/test_generation.py +0 -58
- commkit-1.1.0/tests/core/test_psqam.py +0 -345
- commkit-1.1.0/tests/core/test_signal.py +0 -904
- commkit-1.1.0/tests/core/test_signal_mimo.py +0 -197
- commkit-1.1.0/tests/equalization/test_block.py +0 -1054
- commkit-1.1.0/tests/equalization/test_block_update.py +0 -373
- commkit-1.1.0/tests/equalization/test_bps_kernel.py +0 -252
- commkit-1.1.0/tests/equalization/test_cpr.py +0 -961
- commkit-1.1.0/tests/equalization/test_cs_kernel.py +0 -173
- commkit-1.1.0/tests/equalization/test_sequential_jax.py +0 -560
- commkit-1.1.0/tests/equalization/test_winit.py +0 -407
- commkit-1.1.0/tests/mapping/test_bits.py +0 -159
- commkit-1.1.0/tests/mapping/test_constellation.py +0 -68
- commkit-1.1.0/tests/mapping/test_gray.py +0 -187
- commkit-1.1.0/tests/mapping/test_llr.py +0 -330
- commkit-1.1.0/tests/recovery/test_bps.py +0 -169
- commkit-1.1.0/tests/recovery/test_corrections.py +0 -534
- commkit-1.1.0/tests/recovery/test_joint.py +0 -94
- commkit-1.1.0/tests/recovery/test_pll.py +0 -222
- commkit-1.1.0/tests/recovery/test_tikhonov.py +0 -295
- commkit-1.1.0/tests/recovery/test_viterbi_viterbi.py +0 -202
- commkit-1.1.0/tests/test_backend.py +0 -285
- commkit-1.1.0/tests/test_cuda_infra.py +0 -106
- commkit-1.1.0/tests/test_filtering.py +0 -542
- commkit-1.1.0/tests/test_helpers.py +0 -587
- commkit-1.1.0/tests/test_io.py +0 -410
- commkit-1.1.0/tests/test_logger.py +0 -11
- commkit-1.1.0/tests/test_metrics.py +0 -572
- commkit-1.1.0/tests/test_multirate.py +0 -111
- commkit-1.1.0/tests/test_plotting.py +0 -924
- commkit-1.1.0/tests/test_pulse_shaping.py +0 -154
- commkit-1.1.0/tests/test_smoothing.py +0 -54
- commkit-1.1.0/tests/test_spectral.py +0 -345
- commkit-1.1.0/tests/test_timing.py +0 -1033
- {commkit-1.1.0 → commkit-2.1.0}/.github/workflows/publish.yml +0 -0
- {commkit-1.1.0 → commkit-2.1.0}/.gitignore +0 -0
- {commkit-1.1.0 → commkit-2.1.0}/.python-version +0 -0
- {commkit-1.1.0 → commkit-2.1.0}/commkit/_cuda/src/bps_min_d2.cu +0 -0
- {commkit-1.1.0 → commkit-2.1.0}/commkit/_cuda/src/selftest.cu +0 -0
- {commkit-1.1.0 → commkit-2.1.0}/commkit/coding/base.py +0 -0
- {commkit-1.1.0 → commkit-2.1.0}/commkit/coding/bch.py +0 -0
- {commkit-1.1.0 → commkit-2.1.0}/commkit/coding/convolutional.py +0 -0
- {commkit-1.1.0 → commkit-2.1.0}/commkit/coding/crc.py +0 -0
- {commkit-1.1.0 → commkit-2.1.0}/commkit/coding/galois.py +0 -0
- {commkit-1.1.0 → commkit-2.1.0}/commkit/coding/hamming.py +0 -0
- {commkit-1.1.0 → commkit-2.1.0}/commkit/coding/interleaving.py +0 -0
- {commkit-1.1.0 → commkit-2.1.0}/commkit/coding/ldpc.py +0 -0
- {commkit-1.1.0 → commkit-2.1.0}/commkit/coding/polar.py +0 -0
- {commkit-1.1.0 → commkit-2.1.0}/commkit/coding/ratematch.py +0 -0
- {commkit-1.1.0 → commkit-2.1.0}/commkit/coding/reed_solomon.py +0 -0
- {commkit-1.1.0 → commkit-2.1.0}/commkit/coding/turbo.py +0 -0
- {commkit-1.1.0 → commkit-2.1.0}/commkit/impairments/channel/__init__.py +0 -0
- {commkit-1.1.0 → commkit-2.1.0}/commkit/impairments/channel/nonlinear.py +0 -0
- {commkit-1.1.0 → commkit-2.1.0}/commkit/py.typed +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
*.ipynb filter=nbstripout diff=ipynb
|
|
@@ -38,6 +38,22 @@ jobs:
|
|
|
38
38
|
- name: Type check
|
|
39
39
|
run: uv run mypy commkit/
|
|
40
40
|
|
|
41
|
+
notebooks:
|
|
42
|
+
name: Notebooks carry no outputs
|
|
43
|
+
# Outputs (plots are base64 PNGs) would bloat the repository. The
|
|
44
|
+
# nbstripout git filter strips them on commit once a clone runs
|
|
45
|
+
# 'uv run nbstripout --install'; this required check catches a clone
|
|
46
|
+
# without it before anything reaches main.
|
|
47
|
+
runs-on: ubuntu-latest
|
|
48
|
+
steps:
|
|
49
|
+
- uses: actions/checkout@v4
|
|
50
|
+
|
|
51
|
+
- name: Install uv
|
|
52
|
+
uses: astral-sh/setup-uv@v5
|
|
53
|
+
|
|
54
|
+
- name: Every committed notebook is stripped
|
|
55
|
+
run: uvx nbstripout@0.9.1 --verify $(git ls-files '*.ipynb')
|
|
56
|
+
|
|
41
57
|
test:
|
|
42
58
|
name: Test (Python ${{ matrix.python-version }} | ${{ matrix.resolution }})
|
|
43
59
|
runs-on: ubuntu-latest
|
|
@@ -68,7 +84,38 @@ jobs:
|
|
|
68
84
|
run: uv sync --python ${{ matrix.python-version }} --resolution lowest-direct
|
|
69
85
|
|
|
70
86
|
- name: Run tests (CPU) with coverage
|
|
71
|
-
|
|
87
|
+
# Floor = total at the start of the 2.0 work; raise it, never lower it.
|
|
88
|
+
run: uv run pytest --device=cpu --cov=commkit --cov-report=term-missing --cov-fail-under=78
|
|
89
|
+
|
|
90
|
+
kernel-coverage:
|
|
91
|
+
name: Numba kernel coverage (JIT disabled)
|
|
92
|
+
# Compiled Numba code is invisible to coverage, so the main job reports the
|
|
93
|
+
# kernels as almost untested. Here the same tests run the kernels as plain
|
|
94
|
+
# Python, measuring their real line coverage.
|
|
95
|
+
runs-on: ubuntu-latest
|
|
96
|
+
steps:
|
|
97
|
+
- uses: actions/checkout@v4
|
|
98
|
+
|
|
99
|
+
- name: Install uv
|
|
100
|
+
uses: astral-sh/setup-uv@v5
|
|
101
|
+
with:
|
|
102
|
+
enable-cache: true
|
|
103
|
+
|
|
104
|
+
- name: Set up Python
|
|
105
|
+
run: uv python install 3.12
|
|
106
|
+
|
|
107
|
+
- name: Sync environment
|
|
108
|
+
run: uv sync
|
|
109
|
+
|
|
110
|
+
- name: Run kernel tests with JIT disabled
|
|
111
|
+
env:
|
|
112
|
+
NUMBA_DISABLE_JIT: "1"
|
|
113
|
+
run: uv run pytest tests/equalization tests/recovery tests/test_frequency.py --device=cpu --cov=commkit --cov-report=
|
|
114
|
+
|
|
115
|
+
- name: Kernel coverage report
|
|
116
|
+
run: >-
|
|
117
|
+
uv run coverage report --show-missing --fail-under=85
|
|
118
|
+
--include="commkit/equalization/_kernels_numba.py,commkit/equalization/sequential/*,commkit/frequency.py,commkit/recovery/*"
|
|
72
119
|
|
|
73
120
|
# GPU legs (--device=gpu / --device=all) require CuPy + CUDA and are run
|
|
74
121
|
# manually or on a self-hosted runner; they are intentionally not part of
|
commkit-2.1.0/AGENTS.md
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
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
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 2. Commands
|
|
27
|
+
|
|
28
|
+
Always use `uv`. Python 3.12+.
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
uv sync --all-extras # environment incl. GPU packages
|
|
32
|
+
uv run nbstripout --install # once per clone: strip notebook outputs on commit
|
|
33
|
+
uv run pytest # CPU + GPU tests (default --device=all)
|
|
34
|
+
uv run pytest --device=cpu # what CI runs
|
|
35
|
+
uv run pytest tests/equalization/ -k lms # subset
|
|
36
|
+
uv run pytest --cov=commkit # coverage
|
|
37
|
+
NUMBA_DISABLE_JIT=1 uv run pytest tests/equalization tests/recovery \
|
|
38
|
+
tests/test_frequency.py --device=cpu --cov=commkit # real kernel coverage
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Commit gate.** CI fails on any of these, so run all of them before every commit, with `ruff format` last:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
uv run ruff check . --fix
|
|
45
|
+
uv run mypy commkit/
|
|
46
|
+
uv run pytest
|
|
47
|
+
uv run ruff format .
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**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:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
uv run pytest benchmarks/ --benchmark-disable --device=all
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Timed runs:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
uv run pytest benchmarks/bench_bps.py --benchmark-only --device=all
|
|
60
|
+
uv run pytest benchmarks/ --benchmark-only --device=all \
|
|
61
|
+
--benchmark-compare=0003 --benchmark-storage=file://benchmarks/baselines
|
|
62
|
+
uv run python benchmarks/record_baseline.py NAME # record a new baseline
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Release:**
|
|
66
|
+
|
|
67
|
+
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.
|
|
68
|
+
2. `uv run bump-my-version bump {patch|minor|major}`. This commits the new version and tags it `vX.Y.Z`.
|
|
69
|
+
3. `git push origin main --tags`.
|
|
70
|
+
4. Publish a GitHub Release for the tag (`gh release create vX.Y.Z --generate-notes`). Publishing the release runs `.github/workflows/publish.yml`, which builds and uploads to PyPI through Trusted Publishing; pushing a tag alone publishes nothing.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## 3. Architecture
|
|
75
|
+
|
|
76
|
+
Dependencies point downward only:
|
|
77
|
+
|
|
78
|
+
| Layer | Modules | May import |
|
|
79
|
+
| --- | --- | --- |
|
|
80
|
+
| Infrastructure | `backend`, `logger`, `_cuda` | NumPy, CuPy |
|
|
81
|
+
| Array and math helpers | `_array` (shape and validation), `math` (power, normalization, dB, linear trend) | infrastructure |
|
|
82
|
+
| Value objects | `mapping.Constellation`, pulse classes, `recovery` CPR configs | layers above |
|
|
83
|
+
| Core | `core` (`Signal`, frames, generation), `io` | layers above |
|
|
84
|
+
| DSP | `mapping`, `filtering`, `multirate`, `spectral`, `smoothing`, `impairments`, `timing`, `frequency`, `recovery`, `equalization`, `metrics`, `analysis` | layers above, and lower DSP modules |
|
|
85
|
+
| Presentation | `plotting` | everything; **nothing imports plotting** |
|
|
86
|
+
|
|
87
|
+
- **Responsibilities:**
|
|
88
|
+
- `filtering` holds signal-chain filters with a real frequency response.
|
|
89
|
+
- `smoothing` holds display and estimation smoothers only.
|
|
90
|
+
- `analysis` holds laser and phase characterization.
|
|
91
|
+
- `metrics` is the reporting layer.
|
|
92
|
+
- **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.
|
|
93
|
+
- **Placeholder modules are kept on purpose.** `commkit.coding` and `commkit.impairments.channel.nonlinear` are reminders of planned work. Never delete them or treat them as dead code. They are excluded from coverage and from the contract registry, and the README labels them "planned, not implemented".
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## 4. API rules
|
|
98
|
+
|
|
99
|
+
### Signal
|
|
100
|
+
|
|
101
|
+
- `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.
|
|
102
|
+
- It never holds pipeline results.
|
|
103
|
+
- Construction does no hidden work: no mapping, no normalization, no device move, no shape guessing. Factories (`generate`, frames) do that work.
|
|
104
|
+
- `sig.to("gpu")` and `sig.replace(...)` return new Signals. Arrays are shared, not copied; use `clone()` for an independent copy.
|
|
105
|
+
- **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.
|
|
106
|
+
|
|
107
|
+
### Function signatures
|
|
108
|
+
|
|
109
|
+
- `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.
|
|
110
|
+
- **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.
|
|
111
|
+
- Functions on *derived* quantities (phase trajectories, correlations, taps, PSD bins, results) and synthesis primitives stay array-only.
|
|
112
|
+
- **Metadata rule: facts versus choices.**
|
|
113
|
+
- Facts (`sampling_rate`, `symbol_rate`, and so `sps`) come from the Signal. A conflicting argument raises `ValueError`; an equal one is fine.
|
|
114
|
+
- Choices (decision `constellation`, `noise_var`, `pulse`) default to the Signal's value, and an explicit argument wins.
|
|
115
|
+
- A missing fact for array input raises. Neither case warns.
|
|
116
|
+
- Integer-SPS algorithms validate with `require_integer_sps()` before any cast. Never truncate 1.5 SPS to 1.
|
|
117
|
+
- **Value objects are frozen dataclasses** with read-only NumPy arrays:
|
|
118
|
+
- `Constellation`: `.qam(M)`, `.psk(M)`, `.pam(M, unipolar=)`, or arbitrary `points`; `.shaped(nu=|entropy=)` keeps unit power. Functions accept the object only, never strings.
|
|
119
|
+
- Pulses: `RRC`, `RC`, `Gaussian`, `Rect`, `SmoothRect`. A raw taps array is accepted wherever a pulse is.
|
|
120
|
+
- 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.
|
|
121
|
+
- Equalizer continuation uses `state=result.state`.
|
|
122
|
+
- **Randomness:** `rng: int | np.random.Generator | None` (SciPy SPEC 7).
|
|
123
|
+
- 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.
|
|
124
|
+
- What the channel adds (AWGN, phase noise): draw on the data's device through `_random.standard_normal`. Realizations differ between devices; statistics agree, and tests assert statistics.
|
|
125
|
+
- Never use a global RNG.
|
|
126
|
+
- **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`.
|
|
127
|
+
- **Device follows the data.** There are no `backend=` arguments and no global switches. Unsupported array types (JAX, PyTorch) raise `TypeError`.
|
|
128
|
+
- 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".
|
|
129
|
+
- **No `debug_plot`.** Numerical code never imports plotting. Plot functions consume results or recompute through public compute functions.
|
|
130
|
+
- **Design and apply are separate functions**, for example `rrc_taps` and `fir_filter`, or `butterworth_sos` and `iir_filter`.
|
|
131
|
+
|
|
132
|
+
### Returns
|
|
133
|
+
|
|
134
|
+
- **Transforms:** array in gives array out on the same device; Signal in gives a new Signal out (update `sampling_rate` when the rate changes).
|
|
135
|
+
- **Estimators** reduce over the time axis by input rank: `(N,)` gives 0-d and `(C, N)` gives `(C,)`. Results stay on the input device.
|
|
136
|
+
- **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.
|
|
137
|
+
- **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.
|
|
138
|
+
- **More than one value** is returned as a frozen dataclass with named fields. No dicts, and no tuples longer than two.
|
|
139
|
+
- **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.
|
|
140
|
+
|
|
141
|
+
### Naming
|
|
142
|
+
|
|
143
|
+
| Prefix | Meaning |
|
|
144
|
+
| --- | --- |
|
|
145
|
+
| `apply_*` | add an impairment or apply a model |
|
|
146
|
+
| `estimate_<quantity>(x, method)` | measure, never change the data; returns a frozen `<Quantity>Estimate` (value plus diagnostics) |
|
|
147
|
+
| `correct_<quantity>(x, how)` | remove the impairment; returns only the corrected data. `how` is an estimate or an algorithm object (which estimates first) |
|
|
148
|
+
| `resolve_*` | pick among discrete candidates (π/2 rotation, channel permutation); nothing else |
|
|
149
|
+
| `generate` | synthesis from parameters, takes `rng` |
|
|
150
|
+
| `plot_*` | every public plotting function (only `apply_default_theme` is exempt) |
|
|
151
|
+
|
|
152
|
+
- 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_*`.
|
|
153
|
+
- Computations use plain nouns (`allan_deviation`).
|
|
154
|
+
- **Glossary**, one name each:
|
|
155
|
+
- `num_taps`, `step_size`, `sps`, `constellation`, `pulse`, `rng`, `num_symbols`, `num_channels`, `sampling_rate`, `symbol_rate`.
|
|
156
|
+
- Units are in the docstring. Use `_db` and `_hz` suffixes where the unit is ambiguous.
|
|
157
|
+
|
|
158
|
+
### Errors and logging
|
|
159
|
+
|
|
160
|
+
- Wrong input raises, with a message naming the argument.
|
|
161
|
+
- `logger` is for diagnostics only. Per-channel logging that needs a device-to-host transfer is wrapped in `if logger.isEnabledFor(logging.INFO):`.
|
|
162
|
+
- The library never configures handlers, Matplotlib or warning filters at import time.
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## 5. Numerics
|
|
167
|
+
|
|
168
|
+
- **Storage dtypes:** `complex64` for IQ samples and `float32` for real signals.
|
|
169
|
+
- **Accumulation precision:**
|
|
170
|
+
- LMS/CMA/RDE: inputs and weights are `complex64`, but dot products and gradient updates in the hot loop are accumulated in `complex128`.
|
|
171
|
+
- 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.
|
|
172
|
+
- Carrier phase: promote angles to `float64` before `xp.unwrap()`, because float32 rounding causes spurious quadrant slips. Tikhonov/Kalman block transitions also run in `float64`.
|
|
173
|
+
- **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.
|
|
174
|
+
- **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.
|
|
175
|
+
- **Reproducibility:** a seed is reproducible within a library version only. Tests and baselines assert statistics, never exact noise realizations.
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## 6. Performance
|
|
180
|
+
|
|
181
|
+
| Algorithm structure | Implementation | Device |
|
|
182
|
+
| --- | --- | --- |
|
|
183
|
+
| Vectorized: filters, resampling, spectra, impairments, metrics, LLR, frequency-domain equalizers | NumPy/CuPy via `x, xp, sp = dispatch(samples)` | follows the input |
|
|
184
|
+
| Sequential recursions: LMS/RLS/CMA/RDE, PLL, Tikhonov, cycle slips | Numba `@njit(cache=True, fastmath=True, nogil=True)` | CPU; GPU input does one round trip to the host and back |
|
|
185
|
+
| Hot spots limited by intermediates, launches or round trips | CUDA C++ via CuPy `RawModule` (`commkit/_cuda`) | GPU |
|
|
186
|
+
|
|
187
|
+
**Host-sync hygiene:**
|
|
188
|
+
|
|
189
|
+
- 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.
|
|
190
|
+
- Prefer on-device gathers (`xp.take_along_axis`).
|
|
191
|
+
- Chunk `(N, M)` intermediates over N with an on-device accumulator.
|
|
192
|
+
- Reduce on the device before transferring, including for plotting.
|
|
193
|
+
- Pass whole records to compiled code. There are no Python loops over symbols or small blocks, except inside a CUDA graph.
|
|
194
|
+
|
|
195
|
+
**Kernel policy:**
|
|
196
|
+
|
|
197
|
+
- A `.cu` kernel needs a benchmark showing that CuPy is limited by intermediates, launch count or round trips.
|
|
198
|
+
- It optimizes a CPU reference (Numba or NumPy) and is tested against it with `--device=all`.
|
|
199
|
+
- Wrappers check dtype, contiguity, shape and size before launch.
|
|
200
|
+
- `float32` with explicit literals.
|
|
201
|
+
- `--use_fast_math` only per kernel (through `options`), after an accuracy check; the compiler default does not pass it.
|
|
202
|
+
- Short elementwise chains use `ElementwiseKernel` or `cupy.fuse`.
|
|
203
|
+
- 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).
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## 7. Tests
|
|
208
|
+
|
|
209
|
+
- **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(...)`.
|
|
210
|
+
- **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.
|
|
211
|
+
- **Shared builders** live in `tests/common/` and use the public API.
|
|
212
|
+
- **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.
|
|
213
|
+
- Do not duplicate these checks in per-module tests.
|
|
214
|
+
- **Oracles:** sequential kernels are tested against plain-Python reference implementations in `tests/common/reference_impl.py`. CUDA kernels are tested against their CPU reference.
|
|
215
|
+
- **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.
|
|
216
|
+
- **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.
|
|
217
|
+
- **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.
|
|
218
|
+
- **Coverage** must not drop below the CI floor. Numba kernel coverage is measured by a separate `NUMBA_DISABLE_JIT=1` job.
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## 8. Benchmarks
|
|
223
|
+
|
|
224
|
+
`benchmarks/` tracks the GPU-relevant hot paths. Baselines are committed under `benchmarks/baselines/`. The current reference is `0003` (`v2_0`) on the reference machine: RTX 4070 Ti, Ryzen 7 7800X3D, WSL2; `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.
|
|
225
|
+
|
|
226
|
+
- **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`.
|
|
227
|
+
- **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.
|
|
228
|
+
|
|
229
|
+
- **IDs** are `[cpu]` or `[gpu]`, meaning the input device.
|
|
230
|
+
- **Timing:**
|
|
231
|
+
- Timed bodies end with the `sync` fixture; otherwise GPU timings measure launches, not execution.
|
|
232
|
+
- Each benchmark has one warmup round, so compilation and pool growth are excluded.
|
|
233
|
+
- **Workloads** come from `benchmarks/workloads.py` with fixed seeds; never generate data inline.
|
|
234
|
+
- **Tools:** `benchmarks/benchutils.py` provides `CudaEventTimer` and `nvtx_range` for `nsys` profiling.
|
|
235
|
+
- **Logging** is set to WARNING in `benchmarks/conftest.py`.
|
|
236
|
+
- **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.
|
|
237
|
+
- A commit touching a hot path quotes its delta against `0003`. Tolerance is 5% on CPU and 10% on GPU.
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## 9. Roadmap and open items
|
|
242
|
+
|
|
243
|
+
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.
|
|
244
|
+
|
|
245
|
+
### Planned modules
|
|
246
|
+
|
|
247
|
+
1. **Nonlinear fiber channel** (`impairments.channel.nonlinear`, a placeholder). Split-step Fourier propagation of the Kerr effect together with chromatic dispersion, then digital backpropagation as its compensation counterpart in `filtering`. Comes first because it builds on what exists: the dispersion operator (`_dispersion`), overlap-save and FFT-heavy GPU code. It completes the coherent optical link model.
|
|
248
|
+
2. **Channel coding** (`coding`, a placeholder package). 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.
|
|
249
|
+
|
|
250
|
+
### Usability gaps
|
|
251
|
+
|
|
252
|
+
- **`block_lms` default `step_size`** (2e-4) converges too slowly under strong polarization mixing (30 degrees needs about 3e-3 at `block_size=256`), and the stable step falls as `block_size` grows (5e-4 at 2048). Consider a default that scales with `block_size`, or at least a clearer warning when the error does not drop.
|
|
253
|
+
- **RDE from a cold start** on 16-QAM settles at a few percent SER at large block sizes. CMA pre-convergence works today in two calls (`initial_taps=` or `state=` from a `cma`/`block_cma` result); a worked example in the docs or notebooks would make it discoverable.
|
|
254
|
+
|
|
255
|
+
### Performance
|
|
256
|
+
|
|
257
|
+
- **Exact LLRs on the CPU**: 256-QAM takes about 330 ms per 2^18 symbols (max-log about 18 ms), dominated by one `exp` per point and bit.
|
|
258
|
+
- **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.
|
|
259
|
+
- **`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.
|
|
260
|
+
|
|
261
|
+
### Known limitations
|
|
262
|
+
|
|
263
|
+
- The DSH-input warning of `estimate_linewidth` misses a drift-removed phase residual when `f_shift` is unknown: a real-valued record is valid input for both kinds of method, so the type alone cannot tell them apart.
|
|
264
|
+
- Blind carrier recovery on 8-PSK near ±π/8 can lock to the wrong rotation. Inherent to blind recovery; training symbols or `resolve_phase_ambiguity` resolve it.
|
commkit-2.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: commkit
|
|
3
|
+
Version: 2.1.0
|
|
4
|
+
Summary: A Python library for high-performance digital communications research.
|
|
5
|
+
Project-URL: Homepage, https://github.com/lokgar/commkit
|
|
6
|
+
Project-URL: Repository, https://github.com/lokgar/commkit
|
|
7
|
+
Project-URL: Issues, https://github.com/lokgar/commkit/issues
|
|
8
|
+
Author: Denis Fatkhiev
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: CuPy,DSP,GPU,coherent optics,digital communications,equalization
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Topic :: Communications
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.12
|
|
24
|
+
Requires-Dist: matplotlib>=3.11
|
|
25
|
+
Requires-Dist: numba>=0.65
|
|
26
|
+
Requires-Dist: numpy>=2.4
|
|
27
|
+
Requires-Dist: scipy>=1.17
|
|
28
|
+
Provides-Extra: full
|
|
29
|
+
Requires-Dist: cupy-cuda13x[ctk]>=14.1; extra == 'full'
|
|
30
|
+
Requires-Dist: jupyter>=1.1.1; extra == 'full'
|
|
31
|
+
Provides-Extra: gpu
|
|
32
|
+
Requires-Dist: cupy-cuda13x[ctk]>=14.1; extra == 'gpu'
|
|
33
|
+
Provides-Extra: notebook
|
|
34
|
+
Requires-Dist: jupyter>=1.1.1; extra == 'notebook'
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
|
|
37
|
+
# CommKit
|
|
38
|
+
|
|
39
|
+
**Digital-communications research kit for Python, on CPU and GPU.**
|
|
40
|
+
|
|
41
|
+

|
|
42
|
+

|
|
43
|
+

|
|
44
|
+

|
|
45
|
+
|
|
46
|
+
CommKit covers the receiver chain of coherent and IM/DD links (waveform generation, channel impairments, synchronization, carrier recovery, adaptive equalization, metrics, laser characterization and plotting) on NumPy or CuPy arrays. Computation runs on the device the data lives on.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## The model
|
|
51
|
+
|
|
52
|
+
Data is a NumPy or CuPy array, or a `Signal` that wraps the samples together with what is known about them. Algorithms are plain functions that take the data and return a new `Signal` or array, or a typed result such as `EqualizerResult` or an `*Estimate`. Plotting functions draw those results; they never compute them.
|
|
53
|
+
|
|
54
|
+
- **Things that describe** a modulation, a pulse or a sub-algorithm are small frozen objects written in the call: `lms(rx, cpr=BPS(test_phases=64))`. The same object works standalone: `correct_carrier_phase(y, BPS())`.
|
|
55
|
+
- **A `Signal` carries facts and ground truth.** Facts are `sampling_rate`, `symbol_rate` and `center_frequency`. The description is `constellation` and `pulse`. The ground truth is `reference` (transmitted symbols and bits). Functions take facts from the Signal and raise on a conflicting argument. Choices such as the decision constellation default to the Signal's and can be overridden.
|
|
56
|
+
- **Results with several values are frozen dataclasses** with named fields. Metrics return host floats: one per channel for `(C, N)` input.
|
|
57
|
+
- **The device follows the data.** Processing functions have no `backend=` or `device=` argument. Only factories, which have no input data to follow, take `device=`: `generate(..., device="gpu")`. Move existing data with `sig.to("gpu")`.
|
|
58
|
+
- **Plots draw results.** `plot_*` functions take a Signal, an array or a result object; numerical code never imports plotting.
|
|
59
|
+
- There is no pipeline object, receiver class or configuration file: the orchestration stays in your script.
|
|
60
|
+
|
|
61
|
+
## Quickstart
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
import commkit as ck
|
|
65
|
+
from commkit import RRC, Constellation
|
|
66
|
+
from commkit.recovery import BPS, CycleSlip
|
|
67
|
+
|
|
68
|
+
tx = ck.generate(Constellation.qam(16), num_symbols=2**16, symbol_rate=32e9,
|
|
69
|
+
sps=2, pulse=RRC(rolloff=0.1), rng=1, device="gpu")
|
|
70
|
+
rx = ck.impairments.apply_phase_noise(tx, linewidth=100e3, rng=2)
|
|
71
|
+
rx = ck.impairments.apply_awgn(rx, esn0_db=18, rng=3) # on the GPU, like tx
|
|
72
|
+
rx = ck.filtering.matched_filter(rx) # pulse from rx
|
|
73
|
+
|
|
74
|
+
cpr = BPS(test_phases=64, cycle_slip=CycleSlip(history=100))
|
|
75
|
+
res = ck.equalization.lms(
|
|
76
|
+
rx, num_taps=21, step_size=1e-3,
|
|
77
|
+
training_symbols=rx.reference.symbols[..., :2000], cpr=cpr,
|
|
78
|
+
)
|
|
79
|
+
y = res.signal # 1-SPS Signal, reference aligned
|
|
80
|
+
print(f"EVM {ck.metrics.evm(y, num_skip_symbols=2000):.1f} %, "
|
|
81
|
+
f"BER {ck.metrics.ber(y, num_skip_symbols=2000):.1e}")
|
|
82
|
+
# EVM 12.8 %, BER 2.5e-04
|
|
83
|
+
|
|
84
|
+
# The next record continues from the converged taps and CPR state.
|
|
85
|
+
res2 = ck.equalization.lms(rx, num_taps=21, step_size=1e-3, cpr=cpr, state=res.state)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Without CuPy, pass `device="cpu"` (the default): the same code runs on the CPU.
|
|
89
|
+
|
|
90
|
+
## CPU and GPU
|
|
91
|
+
|
|
92
|
+
CommKit never picks a device for you: data stays where you put it, and each function runs on the device of its input. A few habits get the speed out of a GPU:
|
|
93
|
+
|
|
94
|
+
- **Put data on the GPU once, at the source.** Build it there with `generate(..., device="gpu")`, `frame.to_signal(..., device="gpu")` or `load_npz(path, device="gpu")`; move a capture with `sig.to("gpu")`. Do not move data back and forth between stages. Building on the GPU is faster than `generate(...).to("gpu")`: about 26 ms instead of 370 ms for 4M symbols, because mapping and pulse shaping run there.
|
|
95
|
+
- **You do not need to move results back.** Metrics (`evm`, `ber`, ...) and analysis summaries return host floats, and plots reduce on the device and transfer only what they draw.
|
|
96
|
+
- **Know what runs where.**
|
|
97
|
+
|
|
98
|
+
| Work | GPU input |
|
|
99
|
+
| --- | --- |
|
|
100
|
+
| Generation, filters, resampling, spectra, impairments, BPS, LLRs, metrics, analysis | runs on the GPU |
|
|
101
|
+
| Frequency-domain equalizers `block_lms`, `block_cma`, `block_rde` | runs on the GPU (CUDA graphs) |
|
|
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
|
+
|
|
104
|
+
A sample-by-sample recursion has no parallel work per step. For long records on the GPU, prefer the block equalizers with `block_size=1024` or more. Each block costs a fixed overhead: at 256 the GPU only ties the CPU without carrier recovery, at 2048 it is 6-9x faster. A larger block also lowers the stable `step_size`.
|
|
105
|
+
- **Short records do not pay off.** Launch overhead dominates below roughly 10⁴-10⁵ samples, where the CPU is as fast.
|
|
106
|
+
- **Batch channels and records.** Pass `(C, N)` arrays rather than looping over channels in Python: one call does the work of C.
|
|
107
|
+
- **Keep scalars on the device inside loops.** `float(x)`, `x.item()` and `if x > 0:` on a CuPy array wait for the GPU and copy. Collect values in an array and transfer once.
|
|
108
|
+
- **The first call compiles.** Numba and CUDA kernels compile on first use and are cached on disk. Warm up once before timing anything.
|
|
109
|
+
- **Seeds and devices.** The same `rng` gives the same bits and symbols on both devices. Channel noise (AWGN, phase noise) is drawn on the device, so its realization differs between CPU and GPU while its statistics match. For the exact same phase-noise trajectory on both, draw it on the CPU with `generate_phase_noise(...)` and move it.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Modules
|
|
114
|
+
|
|
115
|
+
| Module | Contents |
|
|
116
|
+
| --- | --- |
|
|
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
|
+
| [`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/blob/main/commkit/filtering.py) | Pulses (`RRC`, `RC`, `Gaussian`, `Rect`, `SmoothRect`), FIR and IIR design (Butterworth, Chebyshev I/II, elliptic, Bessel), `fir_filter`, `iir_filter`, `matched_filter`, overlap-save, chromatic-dispersion compensation. |
|
|
120
|
+
| [`commkit.multirate`](https://github.com/lokgar/commkit/blob/main/commkit/multirate.py) | `resample` (fractional), `decimate`, `upsample`, `decimate_to_symbol_rate`. |
|
|
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. Nonlinear channel models: **planned, not implemented**. |
|
|
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
|
+
| [`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
|
+
| [`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`. |
|
|
126
|
+
| [`commkit.equalization`](https://github.com/lokgar/commkit/tree/main/commkit/equalization) | Sequential `lms`, `rls`, `cma`, `rde` (Numba) and frequency-domain `block_lms`, `block_cma`, `block_rde` (CuPy, CUDA graphs), butterfly MIMO, inline carrier recovery (`cpr=PLL()` / `BPS()`), continuation with `state=`, `zf_equalizer`, polarization-tone demultiplexing. |
|
|
127
|
+
| [`commkit.metrics`](https://github.com/lokgar/commkit/blob/main/commkit/metrics.py) | `evm`, `snr`, `ber`, `ser`, `gmi`, `mi`, including shaped constellations; host values per channel. |
|
|
128
|
+
| [`commkit.analysis`](https://github.com/lokgar/commkit/tree/main/commkit/analysis) | Laser and carrier-phase characterization: `estimate_linewidth` (increment slope, β-separation, delayed self-heterodyne FM-PSD / increment / Lorentzian), FM-noise PSDs, drift separation, Allan deviation. |
|
|
129
|
+
| [`commkit.math`](https://github.com/lokgar/commkit/blob/main/commkit/math.py) | `rms`, `normalize`, dB conversions. |
|
|
130
|
+
| [`commkit.smoothing`](https://github.com/lokgar/commkit/blob/main/commkit/smoothing.py) | Display and estimation smoothers (moving average, Savitzky-Golay, 2-D density). Signal-chain filters are in `filtering`. |
|
|
131
|
+
| [`commkit.io`](https://github.com/lokgar/commkit/blob/main/commkit/io.py) | `save_npz` / `load_npz` for Signals. |
|
|
132
|
+
| [`commkit.plotting`](https://github.com/lokgar/commkit/tree/main/commkit/plotting) | Constellations, eye diagrams, spectra, filter responses, equalizer convergence, and synchronization and laser diagnostics that draw the estimates. Imported on first use. |
|
|
133
|
+
| [`commkit.coding`](https://github.com/lokgar/commkit/tree/main/commkit/coding) | Channel coding and FEC: **planned, not implemented**. |
|
|
134
|
+
|
|
135
|
+
Importing `commkit` has no side effects: it configures no logging, Matplotlib or warning filters, and does not touch the GPU.
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Installation
|
|
140
|
+
|
|
141
|
+
**Requires Python 3.12+.**
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
pip install commkit # CPU
|
|
145
|
+
pip install "commkit[gpu]" # CuPy with the CUDA 13 toolkit libraries
|
|
146
|
+
pip install "commkit[notebook]" # to run the example notebooks
|
|
147
|
+
pip install "commkit[full]" # everything
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
With [`uv`](https://github.com/astral-sh/uv), use `uv pip install` in place of `pip install`. Extras combine, e.g. `commkit[gpu,notebook]`.
|
|
151
|
+
|
|
152
|
+
> [!NOTE]
|
|
153
|
+
> **WSL2 and CUDA.** With NVIDIA drivers and CUDA installed on Windows, there is no need to install CUDA inside WSL2. To let the Python CUDA packages find the bundled NVIDIA libraries, add to `~/.bashrc`:
|
|
154
|
+
>
|
|
155
|
+
> ```bash
|
|
156
|
+
> export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$(echo $HOME/commkit/.venv/lib/python3.*/site-packages/nvidia/cu13/lib)
|
|
157
|
+
> ```
|
|
158
|
+
>
|
|
159
|
+
> *(This assumes the repository is cloned to `$HOME/commkit`; adjust the path otherwise.)*
|
|
160
|
+
|
|
161
|
+
## Examples
|
|
162
|
+
|
|
163
|
+
Jupyter notebooks in [`examples/`](https://github.com/lokgar/commkit/tree/main/examples) (install the `notebook` extra and run `jupyter lab examples`):
|
|
164
|
+
|
|
165
|
+
- [`qam_receiver_quickstart`](https://github.com/lokgar/commkit/blob/main/examples/qam_receiver_quickstart.ipynb) - the quickstart above, cell by cell, with the constellation, spectrum and equalizer plots;
|
|
166
|
+
- [`carrier_phase_analysis`](https://github.com/lokgar/commkit/blob/main/examples/carrier_phase_analysis.ipynb) - drift, linewidth and Allan deviation of a recovered carrier phase;
|
|
167
|
+
- [`laser_linewidth_dsh`](https://github.com/lokgar/commkit/blob/main/examples/laser_linewidth_dsh.ipynb) and [`laser_linewidth_homodyne_iq`](https://github.com/lokgar/commkit/blob/main/examples/laser_linewidth_homodyne_iq.ipynb)
|
|
168
|
+
- laser linewidth from delayed self-heterodyne and homodyne IQ captures;
|
|
169
|
+
- `measurement_laser_linewidth_*` - lean templates for real captures.
|
|
170
|
+
|
|
171
|
+
The notebooks are committed without outputs and run in CI.
|
|
172
|
+
|
|
173
|
+
## Development
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
git clone https://github.com/lokgar/commkit.git
|
|
177
|
+
cd commkit
|
|
178
|
+
uv sync --all-extras
|
|
179
|
+
uv run nbstripout --install # once per clone: notebooks are committed without outputs
|
|
180
|
+
|
|
181
|
+
uv run pytest # CPU and GPU tests
|
|
182
|
+
uv run pytest --device=cpu # what CI runs
|
|
183
|
+
uv run ruff check . && uv run mypy commkit/
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Contributor and coding-agent guidance (architecture, API rules, numerics, performance and test conventions) is in [AGENTS.md](https://github.com/lokgar/commkit/blob/main/AGENTS.md).
|
|
187
|
+
|
|
188
|
+
## License
|
|
189
|
+
|
|
190
|
+
[MIT](https://github.com/lokgar/commkit/blob/main/LICENSE)
|