commkit 3.2.1__tar.gz → 3.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (295) hide show
  1. {commkit-3.2.1 → commkit-3.3.0}/CHANGELOG.md +24 -1
  2. {commkit-3.2.1 → commkit-3.3.0}/PKG-INFO +29 -3
  3. {commkit-3.2.1 → commkit-3.3.0}/README.md +28 -2
  4. {commkit-3.2.1 → commkit-3.3.0}/benchmarks/bench_filters.py +19 -1
  5. {commkit-3.2.1 → commkit-3.3.0}/commkit/__init__.py +1 -1
  6. {commkit-3.2.1 → commkit-3.3.0}/commkit/filtering/__init__.py +8 -0
  7. commkit-3.3.0/commkit/filtering/response.py +260 -0
  8. {commkit-3.2.1 → commkit-3.3.0}/commkit/plotting/__init__.py +4 -3
  9. {commkit-3.2.1 → commkit-3.3.0}/commkit/plotting/analysis.py +166 -101
  10. {commkit-3.2.1 → commkit-3.3.0}/commkit/plotting/coding.py +146 -73
  11. {commkit-3.2.1 → commkit-3.3.0}/commkit/plotting/constellation.py +11 -11
  12. commkit-3.3.0/commkit/plotting/equalizer.py +319 -0
  13. {commkit-3.2.1 → commkit-3.3.0}/commkit/plotting/eye.py +21 -4
  14. commkit-3.3.0/commkit/plotting/filter_response.py +184 -0
  15. {commkit-3.2.1 → commkit-3.3.0}/commkit/plotting/spectral.py +16 -1
  16. {commkit-3.2.1 → commkit-3.3.0}/commkit/plotting/sync.py +404 -253
  17. {commkit-3.2.1 → commkit-3.3.0}/commkit/plotting/theme.py +73 -2
  18. {commkit-3.2.1 → commkit-3.3.0}/commkit/plotting/waveform.py +31 -5
  19. {commkit-3.2.1 → commkit-3.3.0}/examples/01-constellations-and-bit-mapping.ipynb +10 -13
  20. {commkit-3.2.1 → commkit-3.3.0}/examples/02-spectral-analysis.ipynb +9 -3
  21. {commkit-3.2.1 → commkit-3.3.0}/examples/03-digital-filters.ipynb +61 -49
  22. {commkit-3.2.1 → commkit-3.3.0}/examples/04-pulse-shaping-and-matched-filtering.ipynb +25 -12
  23. {commkit-3.2.1 → commkit-3.3.0}/examples/05-multirate-and-resampling.ipynb +8 -13
  24. {commkit-3.2.1 → commkit-3.3.0}/examples/06-awgn-and-performance-metrics.ipynb +2 -2
  25. {commkit-3.2.1 → commkit-3.3.0}/examples/07-channel-coding.ipynb +5 -3
  26. {commkit-3.2.1 → commkit-3.3.0}/examples/08-probabilistic-shaping-and-coded-modulation.ipynb +25 -4
  27. {commkit-3.2.1 → commkit-3.3.0}/examples/09-laser-phase-noise.ipynb +6 -8
  28. {commkit-3.2.1 → commkit-3.3.0}/examples/10-front-end-impairments.ipynb +16 -8
  29. {commkit-3.2.1 → commkit-3.3.0}/examples/11-frame-synchronization-and-timing.ipynb +10 -3
  30. {commkit-3.2.1 → commkit-3.3.0}/examples/12-frequency-offset-estimation.ipynb +3 -1
  31. {commkit-3.2.1 → commkit-3.3.0}/examples/13-blind-carrier-phase-recovery.ipynb +11 -3
  32. {commkit-3.2.1 → commkit-3.3.0}/examples/14-pilot-aided-carrier-recovery.ipynb +11 -3
  33. {commkit-3.2.1 → commkit-3.3.0}/examples/15-linear-channel-equalization.ipynb +18 -9
  34. {commkit-3.2.1 → commkit-3.3.0}/examples/16-adaptive-equalization.ipynb +23 -11
  35. {commkit-3.2.1 → commkit-3.3.0}/examples/18-polarization-multiplexing.ipynb +15 -3
  36. {commkit-3.2.1 → commkit-3.3.0}/examples/19-chromatic-dispersion.ipynb +15 -3
  37. {commkit-3.2.1 → commkit-3.3.0}/examples/20-nonlinear-fiber-and-backpropagation.ipynb +1 -1
  38. {commkit-3.2.1 → commkit-3.3.0}/examples/21-fec-in-optical-standards.ipynb +17 -2
  39. {commkit-3.2.1 → commkit-3.3.0}/examples/22-coherent-transceiver-end-to-end.ipynb +3 -1
  40. {commkit-3.2.1 → commkit-3.3.0}/pyproject.toml +2 -2
  41. {commkit-3.2.1 → commkit-3.3.0}/tests/plotting/test_plot_analysis.py +40 -0
  42. {commkit-3.2.1 → commkit-3.3.0}/tests/plotting/test_plot_coding.py +37 -0
  43. {commkit-3.2.1 → commkit-3.3.0}/tests/plotting/test_plot_constellation.py +23 -0
  44. {commkit-3.2.1 → commkit-3.3.0}/tests/plotting/test_plot_equalizer.py +73 -0
  45. {commkit-3.2.1 → commkit-3.3.0}/tests/plotting/test_plot_eye.py +30 -0
  46. {commkit-3.2.1 → commkit-3.3.0}/tests/plotting/test_plot_filter_response.py +51 -4
  47. {commkit-3.2.1 → commkit-3.3.0}/tests/plotting/test_plot_spectral.py +11 -0
  48. {commkit-3.2.1 → commkit-3.3.0}/tests/plotting/test_plot_sync.py +123 -0
  49. {commkit-3.2.1 → commkit-3.3.0}/tests/plotting/test_plot_waveform.py +14 -0
  50. {commkit-3.2.1 → commkit-3.3.0}/tests/plugins/transfer_trace.py +1 -0
  51. {commkit-3.2.1 → commkit-3.3.0}/tests/test_api_contracts.py +4 -0
  52. commkit-3.3.0/tests/test_filter_response.py +190 -0
  53. {commkit-3.2.1 → commkit-3.3.0}/uv.lock +1 -1
  54. commkit-3.2.1/commkit/plotting/equalizer.py +0 -290
  55. commkit-3.2.1/commkit/plotting/filter_response.py +0 -217
  56. {commkit-3.2.1 → commkit-3.3.0}/.gitattributes +0 -0
  57. {commkit-3.2.1 → commkit-3.3.0}/.github/workflows/ci.yml +0 -0
  58. {commkit-3.2.1 → commkit-3.3.0}/.github/workflows/publish.yml +0 -0
  59. {commkit-3.2.1 → commkit-3.3.0}/.gitignore +0 -0
  60. {commkit-3.2.1 → commkit-3.3.0}/.python-version +0 -0
  61. {commkit-3.2.1 → commkit-3.3.0}/AGENTS.md +0 -0
  62. {commkit-3.2.1 → commkit-3.3.0}/LICENSE +0 -0
  63. {commkit-3.2.1 → commkit-3.3.0}/benchmarks/baselines/Linux-CPython-3.14-64bit/0002_pre_v2.json +0 -0
  64. {commkit-3.2.1 → commkit-3.3.0}/benchmarks/baselines/Linux-CPython-3.14-64bit/0003_v2_0.json +0 -0
  65. {commkit-3.2.1 → commkit-3.3.0}/benchmarks/baselines/Linux-CPython-3.14-64bit/0004_fiber_dbp.json +0 -0
  66. {commkit-3.2.1 → commkit-3.3.0}/benchmarks/baselines/Linux-CPython-3.14-64bit/0005_gpu_decoder_path.json +0 -0
  67. {commkit-3.2.1 → commkit-3.3.0}/benchmarks/baselines/Linux-CPython-3.14-64bit/0006_perf_cleanup.json +0 -0
  68. {commkit-3.2.1 → commkit-3.3.0}/benchmarks/baselines/Linux-CPython-3.14-64bit/0007_v3_0.json +0 -0
  69. {commkit-3.2.1 → commkit-3.3.0}/benchmarks/baselines/Linux-CPython-3.14-64bit/0008_v3_1.json +0 -0
  70. {commkit-3.2.1 → commkit-3.3.0}/benchmarks/bench_block_blind.py +0 -0
  71. {commkit-3.2.1 → commkit-3.3.0}/benchmarks/bench_block_lms.py +0 -0
  72. {commkit-3.2.1 → commkit-3.3.0}/benchmarks/bench_bps.py +0 -0
  73. {commkit-3.2.1 → commkit-3.3.0}/benchmarks/bench_coding.py +0 -0
  74. {commkit-3.2.1 → commkit-3.3.0}/benchmarks/bench_equalizers.py +0 -0
  75. {commkit-3.2.1 → commkit-3.3.0}/benchmarks/bench_fiber.py +0 -0
  76. {commkit-3.2.1 → commkit-3.3.0}/benchmarks/bench_import.py +0 -0
  77. {commkit-3.2.1 → commkit-3.3.0}/benchmarks/bench_llr.py +0 -0
  78. {commkit-3.2.1 → commkit-3.3.0}/benchmarks/bench_mapping.py +0 -0
  79. {commkit-3.2.1 → commkit-3.3.0}/benchmarks/bench_signal_pipeline.py +0 -0
  80. {commkit-3.2.1 → commkit-3.3.0}/benchmarks/bench_sync_misc.py +0 -0
  81. {commkit-3.2.1 → commkit-3.3.0}/benchmarks/benchutils.py +0 -0
  82. {commkit-3.2.1 → commkit-3.3.0}/benchmarks/conftest.py +0 -0
  83. {commkit-3.2.1 → commkit-3.3.0}/benchmarks/record_baseline.py +0 -0
  84. {commkit-3.2.1 → commkit-3.3.0}/benchmarks/workloads.py +0 -0
  85. {commkit-3.2.1 → commkit-3.3.0}/commkit/_array.py +0 -0
  86. {commkit-3.2.1 → commkit-3.3.0}/commkit/_circular.py +0 -0
  87. {commkit-3.2.1 → commkit-3.3.0}/commkit/_constants.py +0 -0
  88. {commkit-3.2.1 → commkit-3.3.0}/commkit/_cuda/__init__.py +0 -0
  89. {commkit-3.2.1 → commkit-3.3.0}/commkit/_cuda/compiler.py +0 -0
  90. {commkit-3.2.1 → commkit-3.3.0}/commkit/_cuda/graphs.py +0 -0
  91. {commkit-3.2.1 → commkit-3.3.0}/commkit/_cuda/src/algebraic.cu +0 -0
  92. {commkit-3.2.1 → commkit-3.3.0}/commkit/_cuda/src/bps_anchor.cu +0 -0
  93. {commkit-3.2.1 → commkit-3.3.0}/commkit/_cuda/src/bps_min_d2.cu +0 -0
  94. {commkit-3.2.1 → commkit-3.3.0}/commkit/_cuda/src/cs_block.cu +0 -0
  95. {commkit-3.2.1 → commkit-3.3.0}/commkit/_cuda/src/iir.cu +0 -0
  96. {commkit-3.2.1 → commkit-3.3.0}/commkit/_cuda/src/ldpc.cu +0 -0
  97. {commkit-3.2.1 → commkit-3.3.0}/commkit/_cuda/src/llr.cu +0 -0
  98. {commkit-3.2.1 → commkit-3.3.0}/commkit/_cuda/src/selftest.cu +0 -0
  99. {commkit-3.2.1 → commkit-3.3.0}/commkit/_dispersion.py +0 -0
  100. {commkit-3.2.1 → commkit-3.3.0}/commkit/_fft.py +0 -0
  101. {commkit-3.2.1 → commkit-3.3.0}/commkit/_fiber.py +0 -0
  102. {commkit-3.2.1 → commkit-3.3.0}/commkit/_overlap_save.py +0 -0
  103. {commkit-3.2.1 → commkit-3.3.0}/commkit/_random.py +0 -0
  104. {commkit-3.2.1 → commkit-3.3.0}/commkit/_sequences.py +0 -0
  105. {commkit-3.2.1 → commkit-3.3.0}/commkit/analysis/__init__.py +0 -0
  106. {commkit-3.2.1 → commkit-3.3.0}/commkit/analysis/_common.py +0 -0
  107. {commkit-3.2.1 → commkit-3.3.0}/commkit/analysis/allan.py +0 -0
  108. {commkit-3.2.1 → commkit-3.3.0}/commkit/analysis/drift.py +0 -0
  109. {commkit-3.2.1 → commkit-3.3.0}/commkit/analysis/fm_noise.py +0 -0
  110. {commkit-3.2.1 → commkit-3.3.0}/commkit/analysis/interferometry.py +0 -0
  111. {commkit-3.2.1 → commkit-3.3.0}/commkit/analysis/linewidth/__init__.py +0 -0
  112. {commkit-3.2.1 → commkit-3.3.0}/commkit/analysis/linewidth/dsh.py +0 -0
  113. {commkit-3.2.1 → commkit-3.3.0}/commkit/analysis/linewidth/phase.py +0 -0
  114. {commkit-3.2.1 → commkit-3.3.0}/commkit/analysis/linewidth/result.py +0 -0
  115. {commkit-3.2.1 → commkit-3.3.0}/commkit/analysis/trajectory.py +0 -0
  116. {commkit-3.2.1 → commkit-3.3.0}/commkit/backend.py +0 -0
  117. {commkit-3.2.1 → commkit-3.3.0}/commkit/coding/__init__.py +0 -0
  118. {commkit-3.2.1 → commkit-3.3.0}/commkit/coding/_galois.py +0 -0
  119. {commkit-3.2.1 → commkit-3.3.0}/commkit/coding/_gf2.py +0 -0
  120. {commkit-3.2.1 → commkit-3.3.0}/commkit/coding/_ldpc_tables.py +0 -0
  121. {commkit-3.2.1 → commkit-3.3.0}/commkit/coding/_numba_loops.py +0 -0
  122. {commkit-3.2.1 → commkit-3.3.0}/commkit/coding/algebraic.py +0 -0
  123. {commkit-3.2.1 → commkit-3.3.0}/commkit/coding/base.py +0 -0
  124. {commkit-3.2.1 → commkit-3.3.0}/commkit/coding/convolutional.py +0 -0
  125. {commkit-3.2.1 → commkit-3.3.0}/commkit/coding/crc.py +0 -0
  126. {commkit-3.2.1 → commkit-3.3.0}/commkit/coding/exit.py +0 -0
  127. {commkit-3.2.1 → commkit-3.3.0}/commkit/coding/interleaving.py +0 -0
  128. {commkit-3.2.1 → commkit-3.3.0}/commkit/coding/ldpc.py +0 -0
  129. {commkit-3.2.1 → commkit-3.3.0}/commkit/coding/linear.py +0 -0
  130. {commkit-3.2.1 → commkit-3.3.0}/commkit/coding/polar.py +0 -0
  131. {commkit-3.2.1 → commkit-3.3.0}/commkit/coding/product.py +0 -0
  132. {commkit-3.2.1 → commkit-3.3.0}/commkit/coding/ratematch.py +0 -0
  133. {commkit-3.2.1 → commkit-3.3.0}/commkit/coding/shaping.py +0 -0
  134. {commkit-3.2.1 → commkit-3.3.0}/commkit/coding/turbo.py +0 -0
  135. {commkit-3.2.1 → commkit-3.3.0}/commkit/core/__init__.py +0 -0
  136. {commkit-3.2.1 → commkit-3.3.0}/commkit/core/_signal_adapter.py +0 -0
  137. {commkit-3.2.1 → commkit-3.3.0}/commkit/core/frame.py +0 -0
  138. {commkit-3.2.1 → commkit-3.3.0}/commkit/core/generation.py +0 -0
  139. {commkit-3.2.1 → commkit-3.3.0}/commkit/core/signal.py +0 -0
  140. {commkit-3.2.1 → commkit-3.3.0}/commkit/equalization/__init__.py +0 -0
  141. {commkit-3.2.1 → commkit-3.3.0}/commkit/equalization/_block/__init__.py +0 -0
  142. {commkit-3.2.1 → commkit-3.3.0}/commkit/equalization/_block/_blind.py +0 -0
  143. {commkit-3.2.1 → commkit-3.3.0}/commkit/equalization/_block/_dd.py +0 -0
  144. {commkit-3.2.1 → commkit-3.3.0}/commkit/equalization/_block/_engine.py +0 -0
  145. {commkit-3.2.1 → commkit-3.3.0}/commkit/equalization/_common.py +0 -0
  146. {commkit-3.2.1 → commkit-3.3.0}/commkit/equalization/_kernels_numba.py +0 -0
  147. {commkit-3.2.1 → commkit-3.3.0}/commkit/equalization/_numba_loops.py +0 -0
  148. {commkit-3.2.1 → commkit-3.3.0}/commkit/equalization/blind.py +0 -0
  149. {commkit-3.2.1 → commkit-3.3.0}/commkit/equalization/linear.py +0 -0
  150. {commkit-3.2.1 → commkit-3.3.0}/commkit/equalization/pilots.py +0 -0
  151. {commkit-3.2.1 → commkit-3.3.0}/commkit/equalization/polarization.py +0 -0
  152. {commkit-3.2.1 → commkit-3.3.0}/commkit/equalization/result.py +0 -0
  153. {commkit-3.2.1 → commkit-3.3.0}/commkit/equalization/sequential/__init__.py +0 -0
  154. {commkit-3.2.1 → commkit-3.3.0}/commkit/equalization/sequential/_blind.py +0 -0
  155. {commkit-3.2.1 → commkit-3.3.0}/commkit/equalization/sequential/_dd.py +0 -0
  156. {commkit-3.2.1 → commkit-3.3.0}/commkit/equalization/sequential/_setup.py +0 -0
  157. {commkit-3.2.1 → commkit-3.3.0}/commkit/filtering/fiber.py +0 -0
  158. {commkit-3.2.1 → commkit-3.3.0}/commkit/filtering/linear.py +0 -0
  159. {commkit-3.2.1 → commkit-3.3.0}/commkit/filtering/pulses.py +0 -0
  160. {commkit-3.2.1 → commkit-3.3.0}/commkit/frequency.py +0 -0
  161. {commkit-3.2.1 → commkit-3.3.0}/commkit/impairments/__init__.py +0 -0
  162. {commkit-3.2.1 → commkit-3.3.0}/commkit/impairments/channel/__init__.py +0 -0
  163. {commkit-3.2.1 → commkit-3.3.0}/commkit/impairments/channel/linear.py +0 -0
  164. {commkit-3.2.1 → commkit-3.3.0}/commkit/impairments/channel/nonlinear.py +0 -0
  165. {commkit-3.2.1 → commkit-3.3.0}/commkit/impairments/frontend.py +0 -0
  166. {commkit-3.2.1 → commkit-3.3.0}/commkit/impairments/noise.py +0 -0
  167. {commkit-3.2.1 → commkit-3.3.0}/commkit/impairments/source.py +0 -0
  168. {commkit-3.2.1 → commkit-3.3.0}/commkit/io.py +0 -0
  169. {commkit-3.2.1 → commkit-3.3.0}/commkit/logger.py +0 -0
  170. {commkit-3.2.1 → commkit-3.3.0}/commkit/mapping/__init__.py +0 -0
  171. {commkit-3.2.1 → commkit-3.3.0}/commkit/mapping/_numba_llr.py +0 -0
  172. {commkit-3.2.1 → commkit-3.3.0}/commkit/mapping/bits.py +0 -0
  173. {commkit-3.2.1 → commkit-3.3.0}/commkit/mapping/constellation.py +0 -0
  174. {commkit-3.2.1 → commkit-3.3.0}/commkit/mapping/gray.py +0 -0
  175. {commkit-3.2.1 → commkit-3.3.0}/commkit/mapping/llr.py +0 -0
  176. {commkit-3.2.1 → commkit-3.3.0}/commkit/mapping/shaping.py +0 -0
  177. {commkit-3.2.1 → commkit-3.3.0}/commkit/math.py +0 -0
  178. {commkit-3.2.1 → commkit-3.3.0}/commkit/metrics.py +0 -0
  179. {commkit-3.2.1 → commkit-3.3.0}/commkit/multirate.py +0 -0
  180. {commkit-3.2.1 → commkit-3.3.0}/commkit/py.typed +0 -0
  181. {commkit-3.2.1 → commkit-3.3.0}/commkit/recovery/__init__.py +0 -0
  182. {commkit-3.2.1 → commkit-3.3.0}/commkit/recovery/_common.py +0 -0
  183. {commkit-3.2.1 → commkit-3.3.0}/commkit/recovery/bps.py +0 -0
  184. {commkit-3.2.1 → commkit-3.3.0}/commkit/recovery/carrier_phase.py +0 -0
  185. {commkit-3.2.1 → commkit-3.3.0}/commkit/recovery/corrections.py +0 -0
  186. {commkit-3.2.1 → commkit-3.3.0}/commkit/recovery/pilots.py +0 -0
  187. {commkit-3.2.1 → commkit-3.3.0}/commkit/recovery/pll.py +0 -0
  188. {commkit-3.2.1 → commkit-3.3.0}/commkit/recovery/tikhonov.py +0 -0
  189. {commkit-3.2.1 → commkit-3.3.0}/commkit/recovery/viterbi_viterbi.py +0 -0
  190. {commkit-3.2.1 → commkit-3.3.0}/commkit/smoothing.py +0 -0
  191. {commkit-3.2.1 → commkit-3.3.0}/commkit/spectral.py +0 -0
  192. {commkit-3.2.1 → commkit-3.3.0}/commkit/timing.py +0 -0
  193. {commkit-3.2.1 → commkit-3.3.0}/examples/00-getting-started.ipynb +0 -0
  194. {commkit-3.2.1 → commkit-3.3.0}/examples/17-blind-equalization.ipynb +0 -0
  195. {commkit-3.2.1 → commkit-3.3.0}/examples/23-carrier-phase-characterization.ipynb +0 -0
  196. {commkit-3.2.1 → commkit-3.3.0}/examples/24-laser-linewidth-self-heterodyne.ipynb +0 -0
  197. {commkit-3.2.1 → commkit-3.3.0}/examples/25-laser-linewidth-short-delay-and-flicker.ipynb +0 -0
  198. {commkit-3.2.1 → commkit-3.3.0}/examples/26-laser-linewidth-self-homodyne-iq.ipynb +0 -0
  199. {commkit-3.2.1 → commkit-3.3.0}/examples/27-measurement-template-self-heterodyne.ipynb +0 -0
  200. {commkit-3.2.1 → commkit-3.3.0}/examples/28-measurement-template-self-homodyne-iq.ipynb +0 -0
  201. {commkit-3.2.1 → commkit-3.3.0}/examples/README.md +0 -0
  202. {commkit-3.2.1 → commkit-3.3.0}/tests/analysis/test_allan.py +0 -0
  203. {commkit-3.2.1 → commkit-3.3.0}/tests/analysis/test_drift.py +0 -0
  204. {commkit-3.2.1 → commkit-3.3.0}/tests/analysis/test_fm_noise.py +0 -0
  205. {commkit-3.2.1 → commkit-3.3.0}/tests/analysis/test_interferometry.py +0 -0
  206. {commkit-3.2.1 → commkit-3.3.0}/tests/analysis/test_linewidth.py +0 -0
  207. {commkit-3.2.1 → commkit-3.3.0}/tests/analysis/test_trajectory.py +0 -0
  208. {commkit-3.2.1 → commkit-3.3.0}/tests/coding/test_algebraic.py +0 -0
  209. {commkit-3.2.1 → commkit-3.3.0}/tests/coding/test_coding_base.py +0 -0
  210. {commkit-3.2.1 → commkit-3.3.0}/tests/coding/test_convolutional.py +0 -0
  211. {commkit-3.2.1 → commkit-3.3.0}/tests/coding/test_crc.py +0 -0
  212. {commkit-3.2.1 → commkit-3.3.0}/tests/coding/test_exit.py +0 -0
  213. {commkit-3.2.1 → commkit-3.3.0}/tests/coding/test_gf2.py +0 -0
  214. {commkit-3.2.1 → commkit-3.3.0}/tests/coding/test_interleaving.py +0 -0
  215. {commkit-3.2.1 → commkit-3.3.0}/tests/coding/test_ldpc.py +0 -0
  216. {commkit-3.2.1 → commkit-3.3.0}/tests/coding/test_ldpc_exit.py +0 -0
  217. {commkit-3.2.1 → commkit-3.3.0}/tests/coding/test_ldpc_kernel.py +0 -0
  218. {commkit-3.2.1 → commkit-3.3.0}/tests/coding/test_ldpc_tables.py +0 -0
  219. {commkit-3.2.1 → commkit-3.3.0}/tests/coding/test_linear_codes.py +0 -0
  220. {commkit-3.2.1 → commkit-3.3.0}/tests/coding/test_polar.py +0 -0
  221. {commkit-3.2.1 → commkit-3.3.0}/tests/coding/test_product.py +0 -0
  222. {commkit-3.2.1 → commkit-3.3.0}/tests/coding/test_ratematch.py +0 -0
  223. {commkit-3.2.1 → commkit-3.3.0}/tests/coding/test_shaping.py +0 -0
  224. {commkit-3.2.1 → commkit-3.3.0}/tests/coding/test_turbo.py +0 -0
  225. {commkit-3.2.1 → commkit-3.3.0}/tests/common/__init__.py +0 -0
  226. {commkit-3.2.1 → commkit-3.3.0}/tests/common/coding.py +0 -0
  227. {commkit-3.2.1 → commkit-3.3.0}/tests/common/conversions.py +0 -0
  228. {commkit-3.2.1 → commkit-3.3.0}/tests/common/kernel_utils.py +0 -0
  229. {commkit-3.2.1 → commkit-3.3.0}/tests/common/memory.py +0 -0
  230. {commkit-3.2.1 → commkit-3.3.0}/tests/common/metrics.py +0 -0
  231. {commkit-3.2.1 → commkit-3.3.0}/tests/common/periodic.py +0 -0
  232. {commkit-3.2.1 → commkit-3.3.0}/tests/common/reference_impl.py +0 -0
  233. {commkit-3.2.1 → commkit-3.3.0}/tests/common/scoring.py +0 -0
  234. {commkit-3.2.1 → commkit-3.3.0}/tests/common/signals.py +0 -0
  235. {commkit-3.2.1 → commkit-3.3.0}/tests/common/theory.py +0 -0
  236. {commkit-3.2.1 → commkit-3.3.0}/tests/conftest.py +0 -0
  237. {commkit-3.2.1 → commkit-3.3.0}/tests/core/test_frame.py +0 -0
  238. {commkit-3.2.1 → commkit-3.3.0}/tests/core/test_generation.py +0 -0
  239. {commkit-3.2.1 → commkit-3.3.0}/tests/core/test_psqam.py +0 -0
  240. {commkit-3.2.1 → commkit-3.3.0}/tests/core/test_signal.py +0 -0
  241. {commkit-3.2.1 → commkit-3.3.0}/tests/core/test_signal_adapter.py +0 -0
  242. {commkit-3.2.1 → commkit-3.3.0}/tests/core/test_signal_mimo.py +0 -0
  243. {commkit-3.2.1 → commkit-3.3.0}/tests/equalization/test_blind.py +0 -0
  244. {commkit-3.2.1 → commkit-3.3.0}/tests/equalization/test_block.py +0 -0
  245. {commkit-3.2.1 → commkit-3.3.0}/tests/equalization/test_bps_kernel.py +0 -0
  246. {commkit-3.2.1 → commkit-3.3.0}/tests/equalization/test_cpr.py +0 -0
  247. {commkit-3.2.1 → commkit-3.3.0}/tests/equalization/test_cs_kernel.py +0 -0
  248. {commkit-3.2.1 → commkit-3.3.0}/tests/equalization/test_equalizer_oracles.py +0 -0
  249. {commkit-3.2.1 → commkit-3.3.0}/tests/equalization/test_linear.py +0 -0
  250. {commkit-3.2.1 → commkit-3.3.0}/tests/equalization/test_mimo.py +0 -0
  251. {commkit-3.2.1 → commkit-3.3.0}/tests/equalization/test_polarization.py +0 -0
  252. {commkit-3.2.1 → commkit-3.3.0}/tests/equalization/test_sequential.py +0 -0
  253. {commkit-3.2.1 → commkit-3.3.0}/tests/equalization/test_state.py +0 -0
  254. {commkit-3.2.1 → commkit-3.3.0}/tests/equalization/test_winit.py +0 -0
  255. {commkit-3.2.1 → commkit-3.3.0}/tests/impairments/channel/test_channel_linear.py +0 -0
  256. {commkit-3.2.1 → commkit-3.3.0}/tests/impairments/channel/test_channel_nonlinear.py +0 -0
  257. {commkit-3.2.1 → commkit-3.3.0}/tests/impairments/test_frontend.py +0 -0
  258. {commkit-3.2.1 → commkit-3.3.0}/tests/impairments/test_noise.py +0 -0
  259. {commkit-3.2.1 → commkit-3.3.0}/tests/impairments/test_source.py +0 -0
  260. {commkit-3.2.1 → commkit-3.3.0}/tests/mapping/test_bits.py +0 -0
  261. {commkit-3.2.1 → commkit-3.3.0}/tests/mapping/test_constellation.py +0 -0
  262. {commkit-3.2.1 → commkit-3.3.0}/tests/mapping/test_gray.py +0 -0
  263. {commkit-3.2.1 → commkit-3.3.0}/tests/mapping/test_llr.py +0 -0
  264. {commkit-3.2.1 → commkit-3.3.0}/tests/plotting/test_plot_theme.py +0 -0
  265. {commkit-3.2.1 → commkit-3.3.0}/tests/plugins/__init__.py +0 -0
  266. {commkit-3.2.1 → commkit-3.3.0}/tests/plugins/precision_trace.py +0 -0
  267. {commkit-3.2.1 → commkit-3.3.0}/tests/recovery/test_bps.py +0 -0
  268. {commkit-3.2.1 → commkit-3.3.0}/tests/recovery/test_corrections.py +0 -0
  269. {commkit-3.2.1 → commkit-3.3.0}/tests/recovery/test_joint.py +0 -0
  270. {commkit-3.2.1 → commkit-3.3.0}/tests/recovery/test_pilots.py +0 -0
  271. {commkit-3.2.1 → commkit-3.3.0}/tests/recovery/test_pll.py +0 -0
  272. {commkit-3.2.1 → commkit-3.3.0}/tests/recovery/test_recovery_oracles.py +0 -0
  273. {commkit-3.2.1 → commkit-3.3.0}/tests/recovery/test_tikhonov.py +0 -0
  274. {commkit-3.2.1 → commkit-3.3.0}/tests/recovery/test_viterbi_viterbi.py +0 -0
  275. {commkit-3.2.1 → commkit-3.3.0}/tests/test_array.py +0 -0
  276. {commkit-3.2.1 → commkit-3.3.0}/tests/test_backend.py +0 -0
  277. {commkit-3.2.1 → commkit-3.3.0}/tests/test_cuda_infra.py +0 -0
  278. {commkit-3.2.1 → commkit-3.3.0}/tests/test_dispersion.py +0 -0
  279. {commkit-3.2.1 → commkit-3.3.0}/tests/test_examples.py +0 -0
  280. {commkit-3.2.1 → commkit-3.3.0}/tests/test_fiber.py +0 -0
  281. {commkit-3.2.1 → commkit-3.3.0}/tests/test_filtering.py +0 -0
  282. {commkit-3.2.1 → commkit-3.3.0}/tests/test_frequency.py +0 -0
  283. {commkit-3.2.1 → commkit-3.3.0}/tests/test_import_hygiene.py +0 -0
  284. {commkit-3.2.1 → commkit-3.3.0}/tests/test_io.py +0 -0
  285. {commkit-3.2.1 → commkit-3.3.0}/tests/test_logger.py +0 -0
  286. {commkit-3.2.1 → commkit-3.3.0}/tests/test_math.py +0 -0
  287. {commkit-3.2.1 → commkit-3.3.0}/tests/test_metrics.py +0 -0
  288. {commkit-3.2.1 → commkit-3.3.0}/tests/test_multirate.py +0 -0
  289. {commkit-3.2.1 → commkit-3.3.0}/tests/test_pulse_shaping.py +0 -0
  290. {commkit-3.2.1 → commkit-3.3.0}/tests/test_receiver_chain.py +0 -0
  291. {commkit-3.2.1 → commkit-3.3.0}/tests/test_signal_pipeline.py +0 -0
  292. {commkit-3.2.1 → commkit-3.3.0}/tests/test_smoothing.py +0 -0
  293. {commkit-3.2.1 → commkit-3.3.0}/tests/test_spectral.py +0 -0
  294. {commkit-3.2.1 → commkit-3.3.0}/tests/test_timing.py +0 -0
  295. {commkit-3.2.1 → commkit-3.3.0}/tests/test_working_memory.py +0 -0
@@ -2,6 +2,28 @@
2
2
 
3
3
  Notable changes to CommKit, newest first. Performance figures are from the reference machine (RTX 4070 Ti, Ryzen 7 7800X3D, WSL2) unless stated otherwise, as a ratio of new to old time (below 1 is faster).
4
4
 
5
+ ## [Unreleased]
6
+
7
+ ## [3.3.0] - 2026-11-03
8
+
9
+ Numerical filter responses and plots built for comparisons. `filter_response`, `impulse_response` and `step_response` evaluate any FIR, SOS or `(b, a)` filter on its device; the multi-panel diagnostics draw one selected panel, and every plot can overlay several results on one axis. The example notebooks explain what each result shows.
10
+
11
+ ### Changes that may need code updates
12
+
13
+ - `plot_filter_response` raises `ValueError` when `ax` holds other than four axes for the dashboard (or one for a selected `quantity=`), as the other multi-panel plots do; it used to log a warning and open a new figure. Pass the right number of axes, or none.
14
+
15
+ ### Added
16
+
17
+ - `filter_response` (returning a `FilterResponse` of frequencies, complex response and group delay), `impulse_response` and `step_response` compute the responses of FIR taps, second-order sections or a `(b, a)` pair on the coefficients' device. The group delay of SOS filters is summed section by section, so high orders keep their accuracy; real IIR time responses run through `iir_filter`, in double-precision state whatever the coefficients' precision.
18
+ - Multi-panel diagnostics (`plot_filter_response`, `plot_equalizer_result`, `plot_zf_equalizer_response`, `plot_llr_distribution`, `plot_parity_check`, `plot_timing_correlation`, `plot_mm_autocorrelation`, `plot_frequency_offset_blockwise_result`, `plot_pilot_phase_estimate`, `plot_pilot_tone_phase_estimate`, `plot_pilot_tones_phase_estimate`) take `quantity=` to draw one panel on one axis.
19
+ - Comparisons on one axis: the single-panel plots and a selected `quantity=` take `label=`, `xlim=`, `ylim=` and `legend=` (`True` shows, `False` hides, `None` keeps the plot's default), and the plots with fits, guides or markers take `annotations=False` to draw only the measured curves of an overlay. `plot_eye_diagram(quantity="i" | "q")` draws one component; `plot_carrier_phase_characterization` accepts existing axes.
20
+ - The example notebooks explain what each receiver result shows, their subplot grids scale the theme's figure size, and notebooks 13 and 16 overlay filter, smoothing, shaping and equalizer comparisons on one axis.
21
+
22
+ ### Development
23
+
24
+ - The transfer audit documents the Tikhonov smoother's host round trip, which it flagged as undocumented.
25
+ - `bench_filter_response` times responses on the DFT grid and on an arbitrary grid at 31, 257 and 4097 taps.
26
+
5
27
  ## [3.2.1] - 2026-11-02
6
28
 
7
29
  Fixes found by a rework of the test suite, which now checks what each algorithm achieves against the transmitted truth and theory. Five numerical defaults or models were wrong and change results (PLL cycle-slip repair, Tikhonov smoothing, `block_cma`'s step, `ols_fir_filter`'s alignment of even taps, `MthPower` near 0 Hz); no signature changes. Plots take their sizes from the theme.
@@ -341,7 +363,8 @@ See the [GitHub release](https://github.com/lokgar/commkit/releases/tag/v1.1.0)
341
363
 
342
364
  First release. See the [GitHub release](https://github.com/lokgar/commkit/releases/tag/v1.0.0).
343
365
 
344
- [Unreleased]: https://github.com/lokgar/commkit/compare/v3.2.1...HEAD
366
+ [Unreleased]: https://github.com/lokgar/commkit/compare/v3.3.0...HEAD
367
+ [3.3.0]: https://github.com/lokgar/commkit/compare/v3.2.1...v3.3.0
345
368
  [3.2.1]: https://github.com/lokgar/commkit/compare/v3.2.0...v3.2.1
346
369
  [3.2.0]: https://github.com/lokgar/commkit/compare/v3.1.3...v3.2.0
347
370
  [3.1.3]: https://github.com/lokgar/commkit/compare/v3.1.2...v3.1.3
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: commkit
3
- Version: 3.2.1
3
+ Version: 3.3.0
4
4
  Summary: A Python library for high-performance digital communications research.
5
5
  Project-URL: Homepage, https://github.com/lokgar/commkit
6
6
  Project-URL: Repository, https://github.com/lokgar/commkit
@@ -49,7 +49,7 @@ CommKit covers the receiver chain of coherent and IM/DD links (waveform generati
49
49
 
50
50
  ## The model
51
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.
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 or call the public compute functions needed to produce them.
53
53
 
54
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
55
  - **A `Signal` carries facts and ground truth.** Facts are `sampling_rate`, `symbol_rate`, `center_frequency` and `domain`; the carrier of an optical Signal gives its `center_wavelength_nm`, which dispersion, fiber propagation and OSNR noise read. The description is `constellation` and `pulse`. The ground truth is `reference`: the transmitted symbols, the bits on the air and the source bits before coding or distribution matching (the same array when there is neither). 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.
@@ -87,6 +87,32 @@ res2 = ck.equalization.lms(rx, num_taps=21, step_size=1e-3, cpr=cpr, state=res.s
87
87
 
88
88
  Without CuPy, pass `device="cpu"` (the default): the same code runs on the CPU.
89
89
 
90
+ ## Comparing filter responses
91
+
92
+ Calculate a response on the coefficients' device, or overlay selected panels
93
+ of several filters:
94
+
95
+ ```python
96
+ short = ck.filtering.fir_taps(sampling_rate=1e6, num_taps=21, cutoff=100e3)
97
+ long = ck.filtering.fir_taps(sampling_rate=1e6, num_taps=101, cutoff=100e3)
98
+ response = ck.filtering.filter_response(long, sampling_rate=1e6)
99
+ print(response.frequencies, response.response, response.group_delay)
100
+
101
+ ck.plotting.apply_default_theme()
102
+ _, ax = ck.plotting.plot_filter_response(
103
+ short, sampling_rate=1e6, quantity="magnitude", label="21 taps"
104
+ )
105
+ ck.plotting.plot_filter_response(
106
+ long, sampling_rate=1e6, quantity="magnitude", ax=ax,
107
+ label="101 taps", legend=True, show=True,
108
+ )
109
+ ```
110
+
111
+ See notebook 03 for filter and smoothing comparisons. The multi-panel
112
+ equalizer, timing, recovery and coding diagnostics take `quantity=` in the
113
+ same way: draw a second result on the returned axis with its own `label=`,
114
+ and `annotations=False` to leave out repeated fits and guides.
115
+
90
116
  ## CPU and GPU
91
117
 
92
118
  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:
@@ -116,7 +142,7 @@ CommKit never picks a device for you: data stays where you put it, and each func
116
142
  | --- | --- |
117
143
  | [`commkit.core`](https://github.com/lokgar/commkit/tree/main/commkit/core) | `Signal`, `Reference`, `generate` (uniform, shaped through a distribution matcher, or coded with `code=`), `SingleCarrierFrame` and `Preamble` (pilots, guard intervals, MIMO channels), `extract_payload`. |
118
144
  | [`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/tree/main/commkit/filtering) | Pulses (`RRC`, `RC`, `Gaussian`, `Rect`, `SmoothRect`), FIR and IIR design (Butterworth, Chebyshev I/II, elliptic, Bessel), `fir_filter`, `iir_filter`, `matched_filter`, overlap-save, chromatic-dispersion compensation and digital backpropagation (`correct_fiber_propagation`). |
145
+ | [`commkit.filtering`](https://github.com/lokgar/commkit/tree/main/commkit/filtering) | Pulses (`RRC`, `RC`, `Gaussian`, `Rect`, `SmoothRect`), FIR and IIR design (Butterworth, Chebyshev I/II, elliptic, Bessel), numerical filter responses, `fir_filter`, `iir_filter`, `matched_filter`, overlap-save, chromatic-dispersion compensation and digital backpropagation (`correct_fiber_propagation`). |
120
146
  | [`commkit.multirate`](https://github.com/lokgar/commkit/blob/main/commkit/multirate.py) | `resample` (fractional), `decimate`, `upsample`, `decimate_to_symbol_rate`. |
121
147
  | [`commkit.spectral`](https://github.com/lokgar/commkit/blob/main/commkit/spectral.py) | Welch PSD, spectrograms, frequency shifting, pilot tones. |
122
148
  | [`commkit.impairments`](https://github.com/lokgar/commkit/tree/main/commkit/impairments) | AWGN at an Es/N0 (measured as energy per symbol) or of an absolute variance, ASE at an OSNR (`apply_ase`), phase noise, IQ imbalance (blind estimate; Löwdin and Gram-Schmidt correction), chromatic dispersion, PMD and polarization mixing, and nonlinear fiber propagation over amplified spans (split-step Kerr effect with dispersion, loss, PMD and ASE) of a `Fiber`. |
@@ -13,7 +13,7 @@ CommKit covers the receiver chain of coherent and IM/DD links (waveform generati
13
13
 
14
14
  ## The model
15
15
 
16
- 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.
16
+ 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 or call the public compute functions needed to produce them.
17
17
 
18
18
  - **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())`.
19
19
  - **A `Signal` carries facts and ground truth.** Facts are `sampling_rate`, `symbol_rate`, `center_frequency` and `domain`; the carrier of an optical Signal gives its `center_wavelength_nm`, which dispersion, fiber propagation and OSNR noise read. The description is `constellation` and `pulse`. The ground truth is `reference`: the transmitted symbols, the bits on the air and the source bits before coding or distribution matching (the same array when there is neither). 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.
@@ -51,6 +51,32 @@ res2 = ck.equalization.lms(rx, num_taps=21, step_size=1e-3, cpr=cpr, state=res.s
51
51
 
52
52
  Without CuPy, pass `device="cpu"` (the default): the same code runs on the CPU.
53
53
 
54
+ ## Comparing filter responses
55
+
56
+ Calculate a response on the coefficients' device, or overlay selected panels
57
+ of several filters:
58
+
59
+ ```python
60
+ short = ck.filtering.fir_taps(sampling_rate=1e6, num_taps=21, cutoff=100e3)
61
+ long = ck.filtering.fir_taps(sampling_rate=1e6, num_taps=101, cutoff=100e3)
62
+ response = ck.filtering.filter_response(long, sampling_rate=1e6)
63
+ print(response.frequencies, response.response, response.group_delay)
64
+
65
+ ck.plotting.apply_default_theme()
66
+ _, ax = ck.plotting.plot_filter_response(
67
+ short, sampling_rate=1e6, quantity="magnitude", label="21 taps"
68
+ )
69
+ ck.plotting.plot_filter_response(
70
+ long, sampling_rate=1e6, quantity="magnitude", ax=ax,
71
+ label="101 taps", legend=True, show=True,
72
+ )
73
+ ```
74
+
75
+ See notebook 03 for filter and smoothing comparisons. The multi-panel
76
+ equalizer, timing, recovery and coding diagnostics take `quantity=` in the
77
+ same way: draw a second result on the returned axis with its own `label=`,
78
+ and `annotations=False` to leave out repeated fits and guides.
79
+
54
80
  ## CPU and GPU
55
81
 
56
82
  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:
@@ -80,7 +106,7 @@ CommKit never picks a device for you: data stays where you put it, and each func
80
106
  | --- | --- |
81
107
  | [`commkit.core`](https://github.com/lokgar/commkit/tree/main/commkit/core) | `Signal`, `Reference`, `generate` (uniform, shaped through a distribution matcher, or coded with `code=`), `SingleCarrierFrame` and `Preamble` (pilots, guard intervals, MIMO channels), `extract_payload`. |
82
108
  | [`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. |
83
- | [`commkit.filtering`](https://github.com/lokgar/commkit/tree/main/commkit/filtering) | Pulses (`RRC`, `RC`, `Gaussian`, `Rect`, `SmoothRect`), FIR and IIR design (Butterworth, Chebyshev I/II, elliptic, Bessel), `fir_filter`, `iir_filter`, `matched_filter`, overlap-save, chromatic-dispersion compensation and digital backpropagation (`correct_fiber_propagation`). |
109
+ | [`commkit.filtering`](https://github.com/lokgar/commkit/tree/main/commkit/filtering) | Pulses (`RRC`, `RC`, `Gaussian`, `Rect`, `SmoothRect`), FIR and IIR design (Butterworth, Chebyshev I/II, elliptic, Bessel), numerical filter responses, `fir_filter`, `iir_filter`, `matched_filter`, overlap-save, chromatic-dispersion compensation and digital backpropagation (`correct_fiber_propagation`). |
84
110
  | [`commkit.multirate`](https://github.com/lokgar/commkit/blob/main/commkit/multirate.py) | `resample` (fractional), `decimate`, `upsample`, `decimate_to_symbol_rate`. |
85
111
  | [`commkit.spectral`](https://github.com/lokgar/commkit/blob/main/commkit/spectral.py) | Welch PSD, spectrograms, frequency shifting, pilot tones. |
86
112
  | [`commkit.impairments`](https://github.com/lokgar/commkit/tree/main/commkit/impairments) | AWGN at an Es/N0 (measured as energy per symbol) or of an absolute variance, ASE at an OSNR (`apply_ase`), phase noise, IQ imbalance (blind estimate; Löwdin and Gram-Schmidt correction), chromatic dispersion, PMD and polarization mixing, and nonlinear fiber propagation over amplified spans (split-step Kerr effect with dispersion, loss, PMD and ASE) of a `Fiber`. |
@@ -4,12 +4,15 @@
4
4
  (1% of the sampling rate, poles near z = 1) on dual-channel complex64; on the
5
5
  GPU it times the double-precision ``cupyx.scipy.signal`` path.
6
6
  ``bench_moving_average_valid`` smooths a two-channel float32 phase record.
7
+ ``bench_filter_response`` evaluates the response and group delay of
8
+ low-pass FIRs on the default 4096-point grid (FFTs) and on an arbitrary
9
+ grid of as many points (Horner evaluation).
7
10
  """
8
11
 
9
12
  import pytest
10
13
  from workloads import FS, bps_workload, phase_workload
11
14
 
12
- from commkit.filtering import butterworth_sos, iir_filter
15
+ from commkit.filtering import butterworth_sos, filter_response, fir_taps, iir_filter
13
16
  from commkit.smoothing import moving_average
14
17
 
15
18
  ROUNDS = dict(rounds=3, warmup_rounds=1, iterations=1)
@@ -39,3 +42,18 @@ def bench_moving_average_valid(benchmark, backend_device, xp, sync, window):
39
42
  return out
40
43
 
41
44
  benchmark.pedantic(run, **ROUNDS)
45
+
46
+
47
+ @pytest.mark.parametrize("grid", ["dft", "custom"])
48
+ @pytest.mark.parametrize("num_taps", [31, 257, 4097])
49
+ def bench_filter_response(benchmark, backend_device, xp, sync, num_taps, grid):
50
+ taps = fir_taps(sampling_rate=FS, num_taps=num_taps, cutoff=0.1 * FS)
51
+ taps = xp.asarray(taps, dtype=xp.float32)
52
+ frequencies = xp.linspace(-0.5, 0.5, 4096) if grid == "custom" else None
53
+
54
+ def run():
55
+ result = filter_response(taps, nfft=4096, frequencies=frequencies)
56
+ sync()
57
+ return result
58
+
59
+ benchmark.pedantic(run, **ROUNDS)
@@ -31,7 +31,7 @@ Matplotlib, change warning filters, or touch the GPU.
31
31
  import importlib
32
32
  from typing import Any
33
33
 
34
- __version__ = "3.2.1"
34
+ __version__ = "3.3.0"
35
35
 
36
36
  from .backend import PrecisionWarning
37
37
  from .core import (
@@ -6,6 +6,9 @@
6
6
  - Design: :func:`fir_taps` (window method) and the IIR designs in
7
7
  second-order sections, :func:`butterworth_sos`, :func:`chebyshev1_sos`,
8
8
  :func:`chebyshev2_sos`, :func:`elliptic_sos` and :func:`bessel_sos`.
9
+ - Responses: :func:`filter_response` (a :class:`FilterResponse`),
10
+ :func:`impulse_response` and :func:`step_response`, computed on the
11
+ coefficients' device.
9
12
  - Application: :func:`fir_filter` (FFT convolution, optionally circular),
10
13
  :func:`ols_fir_filter` (overlap-save, for long filters), :func:`iir_filter`
11
14
  and :func:`matched_filter`.
@@ -40,8 +43,10 @@ from .pulses import (
40
43
  Rect,
41
44
  SmoothRect,
42
45
  )
46
+ from .response import FilterResponse, filter_response, impulse_response, step_response
43
47
 
44
48
  __all__ = [
49
+ "FilterResponse",
45
50
  "Gaussian",
46
51
  "Pulse",
47
52
  "RC",
@@ -55,9 +60,12 @@ __all__ = [
55
60
  "correct_chromatic_dispersion",
56
61
  "correct_fiber_propagation",
57
62
  "elliptic_sos",
63
+ "filter_response",
58
64
  "fir_filter",
59
65
  "fir_taps",
60
66
  "iir_filter",
67
+ "impulse_response",
61
68
  "matched_filter",
62
69
  "ols_fir_filter",
70
+ "step_response",
63
71
  ]
@@ -0,0 +1,260 @@
1
+ """Frequency, impulse and step responses of digital filters.
2
+
3
+ :func:`filter_response` (returning a :class:`FilterResponse`),
4
+ :func:`impulse_response` and :func:`step_response` accept FIR taps,
5
+ second-order sections, or a ``(b, a)`` transfer function, and compute on the
6
+ coefficients' device. Frequencies are in cycles per sample unless a sampling
7
+ rate in Hz is given.
8
+ """
9
+
10
+ from dataclasses import dataclass
11
+ from typing import Any
12
+
13
+ import numpy as np
14
+ import scipy
15
+
16
+ from .. import _fft
17
+ from ..backend import ArrayType, dispatch, to_device
18
+ from ..math import _phasor
19
+ from .linear import iir_filter
20
+
21
+ __all__ = ["FilterResponse", "filter_response", "impulse_response", "step_response"]
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class FilterResponse:
26
+ """Frequency response and group delay of a digital filter.
27
+
28
+ Attributes
29
+ ----------
30
+ frequencies : ndarray
31
+ Frequency grid in Hz, or cycles per sample without a sampling rate.
32
+ response : ndarray
33
+ Complex frequency response on the input device.
34
+ group_delay : ndarray
35
+ Group delay in seconds, or samples without a sampling rate. Undefined
36
+ values at response zeros are ``nan``.
37
+ """
38
+
39
+ frequencies: ArrayType
40
+ response: ArrayType
41
+ group_delay: ArrayType
42
+
43
+
44
+ def _system(system: Any) -> tuple[str, ArrayType, ArrayType | None, Any, Any]:
45
+ """Validate a filter representation and select its device."""
46
+ if isinstance(system, tuple):
47
+ if len(system) != 2:
48
+ raise ValueError("system must be FIR taps, SOS, or a (b, a) pair")
49
+ b, xp, sp = dispatch(system[0])
50
+ a, axp, _ = dispatch(system[1])
51
+ if xp is not axp:
52
+ raise ValueError("system coefficients must be on the same device")
53
+ if b.ndim != 1 or a.ndim != 1 or not b.size or not a.size:
54
+ raise ValueError("system coefficients b and a must be nonempty 1-D arrays")
55
+ return "ba", b, a, xp, sp
56
+ x, xp, sp = dispatch(system)
57
+ if x.ndim == 1 and x.size:
58
+ return "fir", x, None, xp, sp
59
+ if x.ndim == 2 and x.shape[0] and x.shape[1] == 6:
60
+ return "sos", x, None, xp, sp
61
+ raise ValueError("system must be nonempty FIR taps, SOS, or a (b, a) pair")
62
+
63
+
64
+ def filter_response(
65
+ system: Any,
66
+ *,
67
+ nfft: int = 2048,
68
+ sampling_rate: float | None = None,
69
+ frequencies: ArrayType | None = None,
70
+ ) -> FilterResponse:
71
+ """Calculate the frequency response and group delay of a digital filter.
72
+
73
+ Parameters
74
+ ----------
75
+ system : array_like or (b, a) tuple
76
+ FIR taps, second-order sections ``(S, 6)``, or transfer coefficients.
77
+ nfft : int, default 2048
78
+ Number of frequency points from DC to just below Nyquist when
79
+ ``frequencies`` is not given.
80
+ sampling_rate : float, optional
81
+ Sampling rate in Hz. Without it, frequencies are cycles per sample
82
+ and group delays are samples; otherwise they are Hz and seconds.
83
+ frequencies : array_like, optional
84
+ Frequencies to evaluate, in Hz or cycles per sample when no sampling
85
+ rate is given. Must be one-dimensional and on the coefficients' device.
86
+ When omitted, a uniform grid of ``nfft`` points is used.
87
+
88
+ Returns
89
+ -------
90
+ FilterResponse
91
+ Frequency grid, complex response and group delay on the input device.
92
+
93
+ Notes
94
+ -----
95
+ The group delay is the sum of the section delays for SOS filters. It is
96
+ undefined at a zero or pole and is returned as ``nan`` there. Values
97
+ indistinguishable from zero in the coefficient precision are also marked
98
+ undefined. Frequency grids and phase wrapping use float64; coefficient
99
+ arithmetic follows the coefficient dtype.
100
+ """
101
+ if not isinstance(nfft, (int, np.integer)) or nfft <= 0:
102
+ raise ValueError("nfft must be a positive integer")
103
+ if sampling_rate is not None and (
104
+ not np.isfinite(sampling_rate) or sampling_rate <= 0
105
+ ):
106
+ raise ValueError("sampling_rate must be positive and finite")
107
+ kind, b, a, xp, _ = _system(system)
108
+ scale = float(sampling_rate) if sampling_rate is not None else 1.0
109
+ if frequencies is None:
110
+ f = xp.arange(nfft, dtype=xp.float64) * (scale / (2 * nfft))
111
+ else:
112
+ f, fxp, _ = dispatch(frequencies)
113
+ if fxp is not xp:
114
+ raise ValueError("frequencies must be on the system's device")
115
+ if f.ndim != 1 or f.size == 0 or xp.iscomplexobj(f):
116
+ raise ValueError("frequencies must be a nonempty real 1-D array")
117
+ if not bool(xp.all(xp.isfinite(f))):
118
+ raise ValueError("frequencies must be finite")
119
+ f = f.astype(xp.float64, copy=False)
120
+ # Wrap before forming the phase: multiplying a large frequency by 2π
121
+ # first discards its fractional cycle, especially in single precision.
122
+ cycles = xp.remainder(f, scale) / scale
123
+ dtype = xp.result_type(b.dtype, a.dtype if a is not None else b.dtype, xp.complex64)
124
+ z = _phasor(-2 * np.pi * cycles, dtype, xp)
125
+
126
+ def evaluate(coeff: ArrayType) -> tuple[ArrayType, ArrayType]:
127
+ coeff = coeff.astype(dtype, copy=False)
128
+ weighted_coeff = coeff * xp.arange(coeff.size, dtype=coeff.real.dtype)
129
+ if coeff.size > 16 and frequencies is None:
130
+ # Frequencies lie on a 2*nfft DFT grid. Folding coefficients
131
+ # preserves their response there even when the filter is longer.
132
+ fft_size = 2 * nfft
133
+ if coeff.size > fft_size:
134
+ pad = (-coeff.size) % fft_size
135
+ coeff = xp.pad(coeff, (0, pad)).reshape(-1, fft_size).sum(axis=0)
136
+ weighted_coeff = (
137
+ xp.pad(weighted_coeff, (0, pad)).reshape(-1, fft_size).sum(axis=0)
138
+ )
139
+ value = _fft.fft(coeff, n=fft_size)[:nfft]
140
+ weighted = _fft.fft(weighted_coeff, n=fft_size)[:nfft]
141
+ else:
142
+ value = xp.polyval(coeff[::-1], z)
143
+ weighted = xp.polyval(weighted_coeff[::-1], z)
144
+ return value, weighted
145
+
146
+ response = xp.ones(f.size, dtype=dtype)
147
+ delay = xp.zeros(f.size, dtype=xp.empty((), dtype=dtype).real.dtype)
148
+ sections = (
149
+ ((b[i, :3], b[i, 3:]) for i in range(b.shape[0]))
150
+ if kind == "sos"
151
+ else ((b, a),)
152
+ )
153
+ for num, den in sections:
154
+ h, hn = evaluate(num)
155
+ if den is None:
156
+ d = xp.ones_like(h)
157
+ dn = xp.zeros_like(h)
158
+ else:
159
+ d, dn = evaluate(den)
160
+ # Evaluating a mathematical zero on the unit circle can leave a
161
+ # rounding-sized residual (e.g. sin(π)); its delay is not meaningful.
162
+ eps = xp.finfo(delay.dtype).eps
163
+ h_floor = eps * xp.sum(xp.abs(num))
164
+ d_floor = eps * xp.sum(xp.abs(den)) if den is not None else 0.0
165
+ valid = (xp.abs(h) > h_floor) & (xp.abs(d) > d_floor)
166
+ with np.errstate(divide="ignore", invalid="ignore"):
167
+ response *= h / d
168
+ delay += xp.where(valid, xp.real(hn / h - dn / d), xp.nan)
169
+ return FilterResponse(f, response, delay / scale)
170
+
171
+
172
+ def _time_response(system: Any, num_samples: int, step: bool) -> ArrayType:
173
+ """Impulse (``step=False``) or unit-step response of ``num_samples``."""
174
+ if not isinstance(num_samples, (int, np.integer)) or num_samples <= 0:
175
+ raise ValueError("num_samples must be a positive integer")
176
+ kind, b, a, xp, sp = _system(system)
177
+ if kind == "ba" and a is not None and a.size == 1:
178
+ kind, b = "fir", b / a[0]
179
+ dtype = xp.result_type(b.dtype, a.dtype if a is not None else b.dtype, xp.float32)
180
+ if kind == "fir":
181
+ # The impulse response of an FIR filter is its taps, the step response
182
+ # their running sum: no recursion, so nothing to filter.
183
+ h = xp.zeros(num_samples, dtype=dtype)
184
+ count = min(num_samples, b.size)
185
+ h[:count] = b[:count]
186
+ return xp.cumsum(h) if step else h
187
+ if step:
188
+ x = xp.ones(num_samples, dtype=dtype)
189
+ else:
190
+ x = xp.zeros(num_samples, dtype=dtype)
191
+ x[0] = 1
192
+ if x.dtype.kind == "c":
193
+ # iir_filter takes real sections; complex coefficients run through
194
+ # SciPy's direct form in their own precision.
195
+ if kind == "sos":
196
+ return sp.signal.sosfilt(b, x)
197
+ return sp.signal.lfilter(b, a, x)
198
+ # The sections go to the host, as iir_filter takes them (its kernels and
199
+ # their state transitions read float64 host sections; tf2sos is SciPy's
200
+ # root finding): a few coefficients, while x stays on its device.
201
+ if kind == "sos":
202
+ sos = np.asarray(to_device(b, "cpu"), dtype=np.float64)
203
+ sos = sos / sos[:, 3:4]
204
+ else:
205
+ sos = scipy.signal.tf2sos(
206
+ np.asarray(to_device(b, "cpu"), dtype=np.float64),
207
+ np.asarray(to_device(a, "cpu"), dtype=np.float64),
208
+ )
209
+ # iir_filter keeps the sections and the state in double precision, which
210
+ # poles near z = 1 need, and the samples in their own precision.
211
+ return iir_filter(x, sos)
212
+
213
+
214
+ def impulse_response(system: Any, *, num_samples: int = 500) -> ArrayType:
215
+ """Calculate a filter's causal impulse response.
216
+
217
+ Parameters
218
+ ----------
219
+ system : array_like or (b, a) tuple
220
+ FIR taps, second-order sections, or transfer coefficients.
221
+ num_samples : int, default 500
222
+ Number of output samples, including zeros after an FIR response.
223
+
224
+ Returns
225
+ -------
226
+ ndarray
227
+ Impulse response, ``(num_samples,)``, on the coefficients' device.
228
+
229
+ Notes
230
+ -----
231
+ Real IIR filters run through :func:`iir_filter`, with the sections and
232
+ the state in double precision whatever the coefficients' precision (a
233
+ ``(b, a)`` pair is first converted to sections); the response keeps the
234
+ coefficients' precision, at least single. Complex IIR coefficients run
235
+ through SciPy's direct form in their own precision.
236
+ """
237
+ return _time_response(system, num_samples, False)
238
+
239
+
240
+ def step_response(system: Any, *, num_samples: int = 500) -> ArrayType:
241
+ """Calculate a filter's causal unit-step response.
242
+
243
+ Parameters
244
+ ----------
245
+ system : array_like or (b, a) tuple
246
+ FIR taps, second-order sections, or transfer coefficients.
247
+ num_samples : int, default 500
248
+ Number of output samples.
249
+
250
+ Returns
251
+ -------
252
+ ndarray
253
+ Step response, ``(num_samples,)``, on the coefficients' device.
254
+
255
+ Notes
256
+ -----
257
+ Computed as :func:`impulse_response` is; for FIR taps it is their running
258
+ sum.
259
+ """
260
+ return _time_response(system, num_samples, True)
@@ -2,9 +2,10 @@
2
2
 
3
3
  Every public function but :func:`apply_default_theme` is a ``plot_*`` function
4
4
  that draws a Signal, an array or a result object (an estimate, an
5
- :class:`~commkit.equalization.EqualizerResult`, an analysis summary) and never
6
- computes the result itself; device arrays are reduced on the device and only
7
- what is drawn is transferred. Axis labels use SI prefixes.
5
+ :class:`~commkit.equalization.EqualizerResult`, an analysis summary). A plot
6
+ that needs a calculation calls the corresponding public compute function;
7
+ device arrays are reduced on the device and only what is drawn is transferred.
8
+ Tick labels use SI prefixes.
8
9
  :func:`apply_default_theme` sets the library's Matplotlib style; importing the
9
10
  package changes no Matplotlib settings.
10
11