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