commkit 3.5.0__tar.gz → 3.6.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 (311) hide show
  1. {commkit-3.5.0 → commkit-3.6.0}/AGENTS.md +2 -1
  2. {commkit-3.5.0 → commkit-3.6.0}/CHANGELOG.md +37 -1
  3. {commkit-3.5.0 → commkit-3.6.0}/PKG-INFO +3 -3
  4. {commkit-3.5.0 → commkit-3.6.0}/README.md +2 -2
  5. {commkit-3.5.0 → commkit-3.6.0}/benchmarks/bench_fiber.py +18 -1
  6. {commkit-3.5.0 → commkit-3.6.0}/commkit/__init__.py +1 -1
  7. commkit-3.6.0/commkit/_dispersion.py +194 -0
  8. {commkit-3.5.0 → commkit-3.6.0}/commkit/_fiber.py +8 -9
  9. {commkit-3.5.0 → commkit-3.6.0}/commkit/analysis/__init__.py +3 -3
  10. {commkit-3.5.0 → commkit-3.6.0}/commkit/analysis/_common.py +0 -115
  11. {commkit-3.5.0 → commkit-3.6.0}/commkit/analysis/fm_noise.py +2 -2
  12. {commkit-3.5.0 → commkit-3.6.0}/commkit/analysis/interferometry.py +1 -1
  13. {commkit-3.5.0 → commkit-3.6.0}/commkit/analysis/linewidth/__init__.py +7 -7
  14. {commkit-3.5.0 → commkit-3.6.0}/commkit/analysis/linewidth/dsh.py +13 -10
  15. {commkit-3.5.0 → commkit-3.6.0}/commkit/analysis/linewidth/phase.py +106 -50
  16. {commkit-3.5.0 → commkit-3.6.0}/commkit/analysis/linewidth/result.py +19 -15
  17. {commkit-3.5.0 → commkit-3.6.0}/commkit/coding/__init__.py +3 -3
  18. {commkit-3.5.0 → commkit-3.6.0}/commkit/coding/product.py +7 -7
  19. {commkit-3.5.0 → commkit-3.6.0}/commkit/equalization/polarization.py +2 -2
  20. {commkit-3.5.0 → commkit-3.6.0}/commkit/filtering/__init__.py +21 -3
  21. commkit-3.6.0/commkit/filtering/fiber.py +1238 -0
  22. {commkit-3.5.0 → commkit-3.6.0}/commkit/filtering/response.py +19 -7
  23. {commkit-3.5.0 → commkit-3.6.0}/commkit/frequency.py +20 -20
  24. {commkit-3.5.0 → commkit-3.6.0}/commkit/impairments/__init__.py +2 -2
  25. commkit-3.5.0/commkit/impairments/channel.py → commkit-3.6.0/commkit/impairments/fiber_channel.py +9 -3
  26. {commkit-3.5.0 → commkit-3.6.0}/commkit/plotting/analysis.py +52 -33
  27. {commkit-3.5.0 → commkit-3.6.0}/commkit/plotting/filter_response.py +5 -1
  28. {commkit-3.5.0 → commkit-3.6.0}/commkit/plotting/recovery.py +2 -2
  29. {commkit-3.5.0 → commkit-3.6.0}/commkit/plotting/theme.py +102 -5
  30. {commkit-3.5.0 → commkit-3.6.0}/commkit/recovery/__init__.py +7 -7
  31. {commkit-3.5.0 → commkit-3.6.0}/commkit/recovery/_common.py +1 -1
  32. {commkit-3.5.0 → commkit-3.6.0}/commkit/recovery/carrier_phase.py +13 -13
  33. {commkit-3.5.0 → commkit-3.6.0}/commkit/recovery/corrections.py +11 -11
  34. {commkit-3.5.0 → commkit-3.6.0}/commkit/recovery/pilots.py +9 -7
  35. {commkit-3.5.0 → commkit-3.6.0}/commkit/timing.py +1 -1
  36. {commkit-3.5.0 → commkit-3.6.0}/examples/10-front-end-impairments.ipynb +3 -3
  37. {commkit-3.5.0 → commkit-3.6.0}/examples/12-frequency-offset-estimation.ipynb +6 -6
  38. {commkit-3.5.0 → commkit-3.6.0}/examples/14-pilot-aided-carrier-recovery.ipynb +16 -12
  39. {commkit-3.5.0 → commkit-3.6.0}/examples/18-polarization-multiplexing.ipynb +6 -4
  40. commkit-3.6.0/examples/19-chromatic-dispersion.ipynb +657 -0
  41. {commkit-3.5.0 → commkit-3.6.0}/examples/20-nonlinear-fiber-and-backpropagation.ipynb +23 -19
  42. {commkit-3.5.0 → commkit-3.6.0}/examples/21-fec-in-optical-standards.ipynb +3 -3
  43. {commkit-3.5.0 → commkit-3.6.0}/examples/22-coherent-transceiver-end-to-end.ipynb +8 -7
  44. {commkit-3.5.0 → commkit-3.6.0}/examples/23-carrier-phase-characterization.ipynb +75 -49
  45. {commkit-3.5.0 → commkit-3.6.0}/examples/24-laser-linewidth-self-heterodyne.ipynb +16 -10
  46. {commkit-3.5.0 → commkit-3.6.0}/examples/25-laser-linewidth-short-delay-and-flicker.ipynb +26 -14
  47. {commkit-3.5.0 → commkit-3.6.0}/examples/26-laser-linewidth-self-homodyne-iq.ipynb +25 -20
  48. {commkit-3.5.0 → commkit-3.6.0}/examples/27-measurement-template-self-heterodyne.ipynb +26 -12
  49. {commkit-3.5.0 → commkit-3.6.0}/examples/28-measurement-template-self-homodyne-iq.ipynb +34 -16
  50. {commkit-3.5.0 → commkit-3.6.0}/examples/README.md +1 -1
  51. {commkit-3.5.0 → commkit-3.6.0}/pyproject.toml +2 -2
  52. {commkit-3.5.0 → commkit-3.6.0}/scripts/sanitize.sh +1 -1
  53. {commkit-3.5.0 → commkit-3.6.0}/tests/analysis/test_linewidth.py +76 -39
  54. {commkit-3.5.0 → commkit-3.6.0}/tests/coding/test_product.py +6 -4
  55. commkit-3.5.0/tests/impairments/test_channel_linear.py → commkit-3.6.0/tests/impairments/test_fiber_channel_linear.py +4 -4
  56. {commkit-3.5.0 → commkit-3.6.0}/tests/plotting/test_plot_analysis.py +9 -4
  57. {commkit-3.5.0 → commkit-3.6.0}/tests/plotting/test_plot_frequency.py +2 -2
  58. {commkit-3.5.0 → commkit-3.6.0}/tests/plotting/test_plot_recovery.py +4 -2
  59. {commkit-3.5.0 → commkit-3.6.0}/tests/plotting/test_plot_spectral.py +8 -10
  60. commkit-3.6.0/tests/plotting/test_plot_theme.py +88 -0
  61. {commkit-3.5.0 → commkit-3.6.0}/tests/plotting/test_plot_waveform.py +6 -11
  62. {commkit-3.5.0 → commkit-3.6.0}/tests/plugins/transfer_trace.py +6 -1
  63. {commkit-3.5.0 → commkit-3.6.0}/tests/recovery/test_corrections.py +16 -12
  64. {commkit-3.5.0 → commkit-3.6.0}/tests/recovery/test_pilots.py +19 -14
  65. {commkit-3.5.0 → commkit-3.6.0}/tests/test_api_contracts.py +32 -11
  66. {commkit-3.5.0 → commkit-3.6.0}/tests/test_dispersion.py +7 -15
  67. {commkit-3.5.0 → commkit-3.6.0}/tests/test_fiber.py +1 -1
  68. {commkit-3.5.0 → commkit-3.6.0}/tests/test_filter_response.py +21 -0
  69. {commkit-3.5.0 → commkit-3.6.0}/tests/test_filtering.py +500 -19
  70. {commkit-3.5.0 → commkit-3.6.0}/tests/test_frequency.py +25 -23
  71. {commkit-3.5.0 → commkit-3.6.0}/tests/test_receiver_chain.py +2 -2
  72. {commkit-3.5.0 → commkit-3.6.0}/uv.lock +1 -1
  73. commkit-3.5.0/commkit/_dispersion.py +0 -137
  74. commkit-3.5.0/commkit/filtering/fiber.py +0 -211
  75. commkit-3.5.0/examples/19-chromatic-dispersion.ipynb +0 -304
  76. commkit-3.5.0/tests/plotting/test_plot_theme.py +0 -28
  77. {commkit-3.5.0 → commkit-3.6.0}/.gitattributes +0 -0
  78. {commkit-3.5.0 → commkit-3.6.0}/.github/icon.svg +0 -0
  79. {commkit-3.5.0 → commkit-3.6.0}/.github/logo.svg +0 -0
  80. {commkit-3.5.0 → commkit-3.6.0}/.github/workflows/ci.yml +0 -0
  81. {commkit-3.5.0 → commkit-3.6.0}/.github/workflows/publish.yml +0 -0
  82. {commkit-3.5.0 → commkit-3.6.0}/.gitignore +0 -0
  83. {commkit-3.5.0 → commkit-3.6.0}/.python-version +0 -0
  84. {commkit-3.5.0 → commkit-3.6.0}/LICENSE +0 -0
  85. {commkit-3.5.0 → commkit-3.6.0}/benchmarks/baselines/Linux-CPython-3.14-64bit/0002_pre_v2.json +0 -0
  86. {commkit-3.5.0 → commkit-3.6.0}/benchmarks/baselines/Linux-CPython-3.14-64bit/0003_v2_0.json +0 -0
  87. {commkit-3.5.0 → commkit-3.6.0}/benchmarks/baselines/Linux-CPython-3.14-64bit/0004_fiber_dbp.json +0 -0
  88. {commkit-3.5.0 → commkit-3.6.0}/benchmarks/baselines/Linux-CPython-3.14-64bit/0005_gpu_decoder_path.json +0 -0
  89. {commkit-3.5.0 → commkit-3.6.0}/benchmarks/baselines/Linux-CPython-3.14-64bit/0006_perf_cleanup.json +0 -0
  90. {commkit-3.5.0 → commkit-3.6.0}/benchmarks/baselines/Linux-CPython-3.14-64bit/0007_v3_0.json +0 -0
  91. {commkit-3.5.0 → commkit-3.6.0}/benchmarks/baselines/Linux-CPython-3.14-64bit/0008_v3_1.json +0 -0
  92. {commkit-3.5.0 → commkit-3.6.0}/benchmarks/bench_block_blind.py +0 -0
  93. {commkit-3.5.0 → commkit-3.6.0}/benchmarks/bench_block_lms.py +0 -0
  94. {commkit-3.5.0 → commkit-3.6.0}/benchmarks/bench_bps.py +0 -0
  95. {commkit-3.5.0 → commkit-3.6.0}/benchmarks/bench_coding.py +0 -0
  96. {commkit-3.5.0 → commkit-3.6.0}/benchmarks/bench_equalizers.py +0 -0
  97. {commkit-3.5.0 → commkit-3.6.0}/benchmarks/bench_filters.py +0 -0
  98. {commkit-3.5.0 → commkit-3.6.0}/benchmarks/bench_import.py +0 -0
  99. {commkit-3.5.0 → commkit-3.6.0}/benchmarks/bench_llr.py +0 -0
  100. {commkit-3.5.0 → commkit-3.6.0}/benchmarks/bench_mapping.py +0 -0
  101. {commkit-3.5.0 → commkit-3.6.0}/benchmarks/bench_signal_pipeline.py +0 -0
  102. {commkit-3.5.0 → commkit-3.6.0}/benchmarks/bench_sync_misc.py +0 -0
  103. {commkit-3.5.0 → commkit-3.6.0}/benchmarks/benchutils.py +0 -0
  104. {commkit-3.5.0 → commkit-3.6.0}/benchmarks/conftest.py +0 -0
  105. {commkit-3.5.0 → commkit-3.6.0}/benchmarks/record_baseline.py +0 -0
  106. {commkit-3.5.0 → commkit-3.6.0}/benchmarks/workloads.py +0 -0
  107. {commkit-3.5.0 → commkit-3.6.0}/commkit/_array.py +0 -0
  108. {commkit-3.5.0 → commkit-3.6.0}/commkit/_circular.py +0 -0
  109. {commkit-3.5.0 → commkit-3.6.0}/commkit/_constants.py +0 -0
  110. {commkit-3.5.0 → commkit-3.6.0}/commkit/_cuda/__init__.py +0 -0
  111. {commkit-3.5.0 → commkit-3.6.0}/commkit/_cuda/compiler.py +0 -0
  112. {commkit-3.5.0 → commkit-3.6.0}/commkit/_cuda/graphs.py +0 -0
  113. {commkit-3.5.0 → commkit-3.6.0}/commkit/_cuda/src/algebraic.cu +0 -0
  114. {commkit-3.5.0 → commkit-3.6.0}/commkit/_cuda/src/bps_anchor.cu +0 -0
  115. {commkit-3.5.0 → commkit-3.6.0}/commkit/_cuda/src/bps_min_d2.cu +0 -0
  116. {commkit-3.5.0 → commkit-3.6.0}/commkit/_cuda/src/cs_block.cu +0 -0
  117. {commkit-3.5.0 → commkit-3.6.0}/commkit/_cuda/src/iir.cu +0 -0
  118. {commkit-3.5.0 → commkit-3.6.0}/commkit/_cuda/src/ldpc.cu +0 -0
  119. {commkit-3.5.0 → commkit-3.6.0}/commkit/_cuda/src/llr.cu +0 -0
  120. {commkit-3.5.0 → commkit-3.6.0}/commkit/_cuda/src/selftest.cu +0 -0
  121. {commkit-3.5.0 → commkit-3.6.0}/commkit/_fft.py +0 -0
  122. {commkit-3.5.0 → commkit-3.6.0}/commkit/_overlap_save.py +0 -0
  123. {commkit-3.5.0 → commkit-3.6.0}/commkit/_random.py +0 -0
  124. {commkit-3.5.0 → commkit-3.6.0}/commkit/_sequences.py +0 -0
  125. {commkit-3.5.0 → commkit-3.6.0}/commkit/analysis/allan.py +0 -0
  126. {commkit-3.5.0 → commkit-3.6.0}/commkit/analysis/drift.py +0 -0
  127. {commkit-3.5.0 → commkit-3.6.0}/commkit/analysis/trajectory.py +0 -0
  128. {commkit-3.5.0 → commkit-3.6.0}/commkit/backend.py +0 -0
  129. {commkit-3.5.0 → commkit-3.6.0}/commkit/coding/_galois.py +0 -0
  130. {commkit-3.5.0 → commkit-3.6.0}/commkit/coding/_gf2.py +0 -0
  131. {commkit-3.5.0 → commkit-3.6.0}/commkit/coding/_ldpc_tables.py +0 -0
  132. {commkit-3.5.0 → commkit-3.6.0}/commkit/coding/_numba_loops.py +0 -0
  133. {commkit-3.5.0 → commkit-3.6.0}/commkit/coding/algebraic.py +0 -0
  134. {commkit-3.5.0 → commkit-3.6.0}/commkit/coding/base.py +0 -0
  135. {commkit-3.5.0 → commkit-3.6.0}/commkit/coding/convolutional.py +0 -0
  136. {commkit-3.5.0 → commkit-3.6.0}/commkit/coding/crc.py +0 -0
  137. {commkit-3.5.0 → commkit-3.6.0}/commkit/coding/exit.py +0 -0
  138. {commkit-3.5.0 → commkit-3.6.0}/commkit/coding/interleaving.py +0 -0
  139. {commkit-3.5.0 → commkit-3.6.0}/commkit/coding/ldpc.py +0 -0
  140. {commkit-3.5.0 → commkit-3.6.0}/commkit/coding/linear.py +0 -0
  141. {commkit-3.5.0 → commkit-3.6.0}/commkit/coding/performance.py +0 -0
  142. {commkit-3.5.0 → commkit-3.6.0}/commkit/coding/polar.py +0 -0
  143. {commkit-3.5.0 → commkit-3.6.0}/commkit/coding/ratematch.py +0 -0
  144. {commkit-3.5.0 → commkit-3.6.0}/commkit/coding/shaping.py +0 -0
  145. {commkit-3.5.0 → commkit-3.6.0}/commkit/coding/turbo.py +0 -0
  146. {commkit-3.5.0 → commkit-3.6.0}/commkit/core/__init__.py +0 -0
  147. {commkit-3.5.0 → commkit-3.6.0}/commkit/core/_signal_adapter.py +0 -0
  148. {commkit-3.5.0 → commkit-3.6.0}/commkit/core/frame.py +0 -0
  149. {commkit-3.5.0 → commkit-3.6.0}/commkit/core/generation.py +0 -0
  150. {commkit-3.5.0 → commkit-3.6.0}/commkit/core/preamble.py +0 -0
  151. {commkit-3.5.0 → commkit-3.6.0}/commkit/core/signal.py +0 -0
  152. {commkit-3.5.0 → commkit-3.6.0}/commkit/equalization/__init__.py +0 -0
  153. {commkit-3.5.0 → commkit-3.6.0}/commkit/equalization/_block/__init__.py +0 -0
  154. {commkit-3.5.0 → commkit-3.6.0}/commkit/equalization/_block/_blind.py +0 -0
  155. {commkit-3.5.0 → commkit-3.6.0}/commkit/equalization/_block/_dd.py +0 -0
  156. {commkit-3.5.0 → commkit-3.6.0}/commkit/equalization/_block/_engine.py +0 -0
  157. {commkit-3.5.0 → commkit-3.6.0}/commkit/equalization/_common.py +0 -0
  158. {commkit-3.5.0 → commkit-3.6.0}/commkit/equalization/_numba_loops.py +0 -0
  159. {commkit-3.5.0 → commkit-3.6.0}/commkit/equalization/_sequential/__init__.py +0 -0
  160. {commkit-3.5.0 → commkit-3.6.0}/commkit/equalization/_sequential/_blind.py +0 -0
  161. {commkit-3.5.0 → commkit-3.6.0}/commkit/equalization/_sequential/_dd.py +0 -0
  162. {commkit-3.5.0 → commkit-3.6.0}/commkit/equalization/_sequential/_setup.py +0 -0
  163. {commkit-3.5.0 → commkit-3.6.0}/commkit/equalization/linear.py +0 -0
  164. {commkit-3.5.0 → commkit-3.6.0}/commkit/equalization/pilots.py +0 -0
  165. {commkit-3.5.0 → commkit-3.6.0}/commkit/equalization/result.py +0 -0
  166. {commkit-3.5.0 → commkit-3.6.0}/commkit/filtering/linear.py +0 -0
  167. {commkit-3.5.0 → commkit-3.6.0}/commkit/filtering/pulses.py +0 -0
  168. {commkit-3.5.0 → commkit-3.6.0}/commkit/impairments/frontend.py +0 -0
  169. {commkit-3.5.0 → commkit-3.6.0}/commkit/impairments/noise.py +0 -0
  170. {commkit-3.5.0 → commkit-3.6.0}/commkit/impairments/source.py +0 -0
  171. {commkit-3.5.0 → commkit-3.6.0}/commkit/io.py +0 -0
  172. {commkit-3.5.0 → commkit-3.6.0}/commkit/logger.py +0 -0
  173. {commkit-3.5.0 → commkit-3.6.0}/commkit/mapping/__init__.py +0 -0
  174. {commkit-3.5.0 → commkit-3.6.0}/commkit/mapping/_numba_llr.py +0 -0
  175. {commkit-3.5.0 → commkit-3.6.0}/commkit/mapping/bits.py +0 -0
  176. {commkit-3.5.0 → commkit-3.6.0}/commkit/mapping/constellation.py +0 -0
  177. {commkit-3.5.0 → commkit-3.6.0}/commkit/mapping/gray.py +0 -0
  178. {commkit-3.5.0 → commkit-3.6.0}/commkit/mapping/llr.py +0 -0
  179. {commkit-3.5.0 → commkit-3.6.0}/commkit/mapping/shaping.py +0 -0
  180. {commkit-3.5.0 → commkit-3.6.0}/commkit/math.py +0 -0
  181. {commkit-3.5.0 → commkit-3.6.0}/commkit/metrics/__init__.py +0 -0
  182. {commkit-3.5.0 → commkit-3.6.0}/commkit/metrics/_common.py +0 -0
  183. {commkit-3.5.0 → commkit-3.6.0}/commkit/metrics/error_rates.py +0 -0
  184. {commkit-3.5.0 → commkit-3.6.0}/commkit/metrics/information.py +0 -0
  185. {commkit-3.5.0 → commkit-3.6.0}/commkit/metrics/quality.py +0 -0
  186. {commkit-3.5.0 → commkit-3.6.0}/commkit/multirate.py +0 -0
  187. {commkit-3.5.0 → commkit-3.6.0}/commkit/plotting/__init__.py +0 -0
  188. {commkit-3.5.0 → commkit-3.6.0}/commkit/plotting/coding.py +0 -0
  189. {commkit-3.5.0 → commkit-3.6.0}/commkit/plotting/constellation.py +0 -0
  190. {commkit-3.5.0 → commkit-3.6.0}/commkit/plotting/equalizer.py +0 -0
  191. {commkit-3.5.0 → commkit-3.6.0}/commkit/plotting/eye.py +0 -0
  192. {commkit-3.5.0 → commkit-3.6.0}/commkit/plotting/frequency.py +0 -0
  193. {commkit-3.5.0 → commkit-3.6.0}/commkit/plotting/spectral.py +0 -0
  194. {commkit-3.5.0 → commkit-3.6.0}/commkit/plotting/timing.py +0 -0
  195. {commkit-3.5.0 → commkit-3.6.0}/commkit/plotting/waveform.py +0 -0
  196. {commkit-3.5.0 → commkit-3.6.0}/commkit/py.typed +0 -0
  197. {commkit-3.5.0 → commkit-3.6.0}/commkit/recovery/bps.py +0 -0
  198. {commkit-3.5.0 → commkit-3.6.0}/commkit/recovery/pll.py +0 -0
  199. {commkit-3.5.0 → commkit-3.6.0}/commkit/recovery/tikhonov.py +0 -0
  200. {commkit-3.5.0 → commkit-3.6.0}/commkit/recovery/viterbi_viterbi.py +0 -0
  201. {commkit-3.5.0 → commkit-3.6.0}/commkit/smoothing.py +0 -0
  202. {commkit-3.5.0 → commkit-3.6.0}/commkit/spectral.py +0 -0
  203. {commkit-3.5.0 → commkit-3.6.0}/examples/00-getting-started.ipynb +0 -0
  204. {commkit-3.5.0 → commkit-3.6.0}/examples/01-constellations-and-bit-mapping.ipynb +0 -0
  205. {commkit-3.5.0 → commkit-3.6.0}/examples/02-spectral-analysis.ipynb +0 -0
  206. {commkit-3.5.0 → commkit-3.6.0}/examples/03-digital-filters.ipynb +0 -0
  207. {commkit-3.5.0 → commkit-3.6.0}/examples/04-pulse-shaping-and-matched-filtering.ipynb +0 -0
  208. {commkit-3.5.0 → commkit-3.6.0}/examples/05-multirate-and-resampling.ipynb +0 -0
  209. {commkit-3.5.0 → commkit-3.6.0}/examples/06-awgn-and-performance-metrics.ipynb +0 -0
  210. {commkit-3.5.0 → commkit-3.6.0}/examples/07-channel-coding.ipynb +0 -0
  211. {commkit-3.5.0 → commkit-3.6.0}/examples/08-probabilistic-shaping-and-coded-modulation.ipynb +0 -0
  212. {commkit-3.5.0 → commkit-3.6.0}/examples/09-laser-phase-noise.ipynb +0 -0
  213. {commkit-3.5.0 → commkit-3.6.0}/examples/11-frame-synchronization-and-timing.ipynb +0 -0
  214. {commkit-3.5.0 → commkit-3.6.0}/examples/13-blind-carrier-phase-recovery.ipynb +0 -0
  215. {commkit-3.5.0 → commkit-3.6.0}/examples/15-linear-channel-equalization.ipynb +0 -0
  216. {commkit-3.5.0 → commkit-3.6.0}/examples/16-adaptive-equalization.ipynb +0 -0
  217. {commkit-3.5.0 → commkit-3.6.0}/examples/17-blind-equalization.ipynb +0 -0
  218. {commkit-3.5.0 → commkit-3.6.0}/tests/analysis/test_allan.py +0 -0
  219. {commkit-3.5.0 → commkit-3.6.0}/tests/analysis/test_drift.py +0 -0
  220. {commkit-3.5.0 → commkit-3.6.0}/tests/analysis/test_fm_noise.py +0 -0
  221. {commkit-3.5.0 → commkit-3.6.0}/tests/analysis/test_interferometry.py +0 -0
  222. {commkit-3.5.0 → commkit-3.6.0}/tests/analysis/test_trajectory.py +0 -0
  223. {commkit-3.5.0 → commkit-3.6.0}/tests/coding/test_algebraic.py +0 -0
  224. {commkit-3.5.0 → commkit-3.6.0}/tests/coding/test_coding_base.py +0 -0
  225. {commkit-3.5.0 → commkit-3.6.0}/tests/coding/test_convolutional.py +0 -0
  226. {commkit-3.5.0 → commkit-3.6.0}/tests/coding/test_crc.py +0 -0
  227. {commkit-3.5.0 → commkit-3.6.0}/tests/coding/test_exit.py +0 -0
  228. {commkit-3.5.0 → commkit-3.6.0}/tests/coding/test_gf2.py +0 -0
  229. {commkit-3.5.0 → commkit-3.6.0}/tests/coding/test_interleaving.py +0 -0
  230. {commkit-3.5.0 → commkit-3.6.0}/tests/coding/test_ldpc.py +0 -0
  231. {commkit-3.5.0 → commkit-3.6.0}/tests/coding/test_ldpc_exit.py +0 -0
  232. {commkit-3.5.0 → commkit-3.6.0}/tests/coding/test_ldpc_kernel.py +0 -0
  233. {commkit-3.5.0 → commkit-3.6.0}/tests/coding/test_ldpc_tables.py +0 -0
  234. {commkit-3.5.0 → commkit-3.6.0}/tests/coding/test_linear_codes.py +0 -0
  235. {commkit-3.5.0 → commkit-3.6.0}/tests/coding/test_performance.py +0 -0
  236. {commkit-3.5.0 → commkit-3.6.0}/tests/coding/test_polar.py +0 -0
  237. {commkit-3.5.0 → commkit-3.6.0}/tests/coding/test_ratematch.py +0 -0
  238. {commkit-3.5.0 → commkit-3.6.0}/tests/coding/test_shaping.py +0 -0
  239. {commkit-3.5.0 → commkit-3.6.0}/tests/coding/test_turbo.py +0 -0
  240. {commkit-3.5.0 → commkit-3.6.0}/tests/common/__init__.py +0 -0
  241. {commkit-3.5.0 → commkit-3.6.0}/tests/common/coding.py +0 -0
  242. {commkit-3.5.0 → commkit-3.6.0}/tests/common/conversions.py +0 -0
  243. {commkit-3.5.0 → commkit-3.6.0}/tests/common/kernel_utils.py +0 -0
  244. {commkit-3.5.0 → commkit-3.6.0}/tests/common/memory.py +0 -0
  245. {commkit-3.5.0 → commkit-3.6.0}/tests/common/metrics.py +0 -0
  246. {commkit-3.5.0 → commkit-3.6.0}/tests/common/periodic.py +0 -0
  247. {commkit-3.5.0 → commkit-3.6.0}/tests/common/reference_impl.py +0 -0
  248. {commkit-3.5.0 → commkit-3.6.0}/tests/common/scoring.py +0 -0
  249. {commkit-3.5.0 → commkit-3.6.0}/tests/common/signals.py +0 -0
  250. {commkit-3.5.0 → commkit-3.6.0}/tests/common/theory.py +0 -0
  251. {commkit-3.5.0 → commkit-3.6.0}/tests/conftest.py +0 -0
  252. {commkit-3.5.0 → commkit-3.6.0}/tests/core/test_frame.py +0 -0
  253. {commkit-3.5.0 → commkit-3.6.0}/tests/core/test_generation.py +0 -0
  254. {commkit-3.5.0 → commkit-3.6.0}/tests/core/test_preamble.py +0 -0
  255. {commkit-3.5.0 → commkit-3.6.0}/tests/core/test_psqam.py +0 -0
  256. {commkit-3.5.0 → commkit-3.6.0}/tests/core/test_signal.py +0 -0
  257. {commkit-3.5.0 → commkit-3.6.0}/tests/core/test_signal_adapter.py +0 -0
  258. {commkit-3.5.0 → commkit-3.6.0}/tests/core/test_signal_mimo.py +0 -0
  259. {commkit-3.5.0 → commkit-3.6.0}/tests/equalization/test_blind.py +0 -0
  260. {commkit-3.5.0 → commkit-3.6.0}/tests/equalization/test_block.py +0 -0
  261. {commkit-3.5.0 → commkit-3.6.0}/tests/equalization/test_bps_kernel.py +0 -0
  262. {commkit-3.5.0 → commkit-3.6.0}/tests/equalization/test_cpr.py +0 -0
  263. {commkit-3.5.0 → commkit-3.6.0}/tests/equalization/test_cs_kernel.py +0 -0
  264. {commkit-3.5.0 → commkit-3.6.0}/tests/equalization/test_equalizer_oracles.py +0 -0
  265. {commkit-3.5.0 → commkit-3.6.0}/tests/equalization/test_linear.py +0 -0
  266. {commkit-3.5.0 → commkit-3.6.0}/tests/equalization/test_mimo.py +0 -0
  267. {commkit-3.5.0 → commkit-3.6.0}/tests/equalization/test_polarization.py +0 -0
  268. {commkit-3.5.0 → commkit-3.6.0}/tests/equalization/test_sequential.py +0 -0
  269. {commkit-3.5.0 → commkit-3.6.0}/tests/equalization/test_state.py +0 -0
  270. {commkit-3.5.0 → commkit-3.6.0}/tests/equalization/test_winit.py +0 -0
  271. /commkit-3.5.0/tests/impairments/test_channel_nonlinear.py → /commkit-3.6.0/tests/impairments/test_fiber_channel_nonlinear.py +0 -0
  272. {commkit-3.5.0 → commkit-3.6.0}/tests/impairments/test_frontend.py +0 -0
  273. {commkit-3.5.0 → commkit-3.6.0}/tests/impairments/test_noise.py +0 -0
  274. {commkit-3.5.0 → commkit-3.6.0}/tests/impairments/test_source.py +0 -0
  275. {commkit-3.5.0 → commkit-3.6.0}/tests/mapping/test_bits.py +0 -0
  276. {commkit-3.5.0 → commkit-3.6.0}/tests/mapping/test_constellation.py +0 -0
  277. {commkit-3.5.0 → commkit-3.6.0}/tests/mapping/test_gray.py +0 -0
  278. {commkit-3.5.0 → commkit-3.6.0}/tests/mapping/test_llr.py +0 -0
  279. {commkit-3.5.0 → commkit-3.6.0}/tests/metrics/test_common.py +0 -0
  280. {commkit-3.5.0 → commkit-3.6.0}/tests/metrics/test_error_rates.py +0 -0
  281. {commkit-3.5.0 → commkit-3.6.0}/tests/metrics/test_information.py +0 -0
  282. {commkit-3.5.0 → commkit-3.6.0}/tests/metrics/test_quality.py +0 -0
  283. {commkit-3.5.0 → commkit-3.6.0}/tests/plotting/test_plot_coding.py +0 -0
  284. {commkit-3.5.0 → commkit-3.6.0}/tests/plotting/test_plot_constellation.py +0 -0
  285. {commkit-3.5.0 → commkit-3.6.0}/tests/plotting/test_plot_equalizer.py +0 -0
  286. {commkit-3.5.0 → commkit-3.6.0}/tests/plotting/test_plot_eye.py +0 -0
  287. {commkit-3.5.0 → commkit-3.6.0}/tests/plotting/test_plot_filter_response.py +0 -0
  288. {commkit-3.5.0 → commkit-3.6.0}/tests/plotting/test_plot_timing.py +0 -0
  289. {commkit-3.5.0 → commkit-3.6.0}/tests/plugins/__init__.py +0 -0
  290. {commkit-3.5.0 → commkit-3.6.0}/tests/plugins/precision_trace.py +0 -0
  291. {commkit-3.5.0 → commkit-3.6.0}/tests/recovery/test_bps.py +0 -0
  292. {commkit-3.5.0 → commkit-3.6.0}/tests/recovery/test_joint.py +0 -0
  293. {commkit-3.5.0 → commkit-3.6.0}/tests/recovery/test_pll.py +0 -0
  294. {commkit-3.5.0 → commkit-3.6.0}/tests/recovery/test_recovery_oracles.py +0 -0
  295. {commkit-3.5.0 → commkit-3.6.0}/tests/recovery/test_tikhonov.py +0 -0
  296. {commkit-3.5.0 → commkit-3.6.0}/tests/recovery/test_viterbi_viterbi.py +0 -0
  297. {commkit-3.5.0 → commkit-3.6.0}/tests/test_array.py +0 -0
  298. {commkit-3.5.0 → commkit-3.6.0}/tests/test_backend.py +0 -0
  299. {commkit-3.5.0 → commkit-3.6.0}/tests/test_cuda_infra.py +0 -0
  300. {commkit-3.5.0 → commkit-3.6.0}/tests/test_examples.py +0 -0
  301. {commkit-3.5.0 → commkit-3.6.0}/tests/test_import_hygiene.py +0 -0
  302. {commkit-3.5.0 → commkit-3.6.0}/tests/test_io.py +0 -0
  303. {commkit-3.5.0 → commkit-3.6.0}/tests/test_logger.py +0 -0
  304. {commkit-3.5.0 → commkit-3.6.0}/tests/test_math.py +0 -0
  305. {commkit-3.5.0 → commkit-3.6.0}/tests/test_multirate.py +0 -0
  306. {commkit-3.5.0 → commkit-3.6.0}/tests/test_pulse_shaping.py +0 -0
  307. {commkit-3.5.0 → commkit-3.6.0}/tests/test_signal_pipeline.py +0 -0
  308. {commkit-3.5.0 → commkit-3.6.0}/tests/test_smoothing.py +0 -0
  309. {commkit-3.5.0 → commkit-3.6.0}/tests/test_spectral.py +0 -0
  310. {commkit-3.5.0 → commkit-3.6.0}/tests/test_timing.py +0 -0
  311. {commkit-3.5.0 → commkit-3.6.0}/tests/test_working_memory.py +0 -0
@@ -80,6 +80,7 @@ Dependencies point downward only:
80
80
  | Presentation | `plotting` | everything; **nothing imports plotting** (no `debug_plot`: plot functions consume results or recompute through public functions) |
81
81
 
82
82
  - **Responsibilities:** `filtering` holds signal-chain filters with a real frequency response; `smoothing` display and estimation smoothers only; `analysis` laser and phase characterization; `metrics` is the reporting layer.
83
+ - **Plot axes in SI units** are labelled with the base unit in brackets (`Frequency [Hz]`, `Time [s]`) and get `_set_eng_formatter` (`plotting/theme.py`): at each draw it picks one SI prefix from the visible ticks and writes it into the label's unit (`Frequency [GHz]`), with plain numbers on the ticks. Pass data in SI units and never pre-scale it or prefix the label by hand; log axes keep a prefix per tick, as no single one fits decades.
83
84
  - **Codes are storable:** `io` stores a code as its class and constructor fields, so those fields are numbers, strings, tuples, host arrays, constellations or other codes.
84
85
  - **Splitting modules:** promote a flat module to a package only when it exceeds about 1000 lines *and* has two or more separable concerns. Never create a `utils/` package or one-function files.
85
86
  - **One documented home per public name.** A package `__init__` re-exports its subpackages' public names, and users and documentation import them from the package (`commkit.impairments.Fiber`), not from the subpackage that defines them.
@@ -240,7 +241,7 @@ Docstrings describe the code as it is; a change in behavior updates them in the
240
241
 
241
242
  - **Fixtures:** use `backend_device`, `xp` and `xpt` from `tests/conftest.py`. Assert with `xpt.assert_allclose(...)` (`numpy.testing` raises on CuPy arrays); cast expectations with `xp.asarray` and reductions with `float(...)`.
242
243
  - **No configuration skips.** The GPU legs skip only when CuPy is not installed, and fail instead under an explicit `--device=gpu`; a broken CuPy fails under any setting. Tests outside the selected run are deselected at collection (`gpu_only`, `cpu_only`, `slow` without `--run-slow`), so a run with CuPy reports no skips. A new test never calls `pytest.skip` for a configuration.
243
- - **Layout mirrors the source:** `tests/<subpackage>/test_<module>.py`, `tests/test_<module>.py` for flat modules. Basenames are globally unique (prepend import mode). Split a large file by concern, each part named after its module (`impairments/test_channel_linear.py`, `test_channel_nonlinear.py`). Shared builders live in `tests/common/` and use the public API.
244
+ - **Layout mirrors the source:** `tests/<subpackage>/test_<module>.py`, `tests/test_<module>.py` for flat modules. Basenames are globally unique (prepend import mode). Split a large file by concern, each part named after its module (`impairments/test_fiber_channel_linear.py`, `test_fiber_channel_nonlinear.py`). Shared builders live in `tests/common/` and use the public API.
244
245
  - **Contract registry** (`tests/test_api_contracts.py`): every public function has a row, checked generically for array and Signal in/out, device preserved, input not mutated, keyword-only parameters, `TypeError` on unsupported arrays, fact conflicts and return rules. Do not repeat these checks per module.
245
246
  - **Oracles:** sequential kernels are tested against plain-Python references in `tests/common/reference_impl.py`, CUDA kernels against their CPU reference. Kernel tests skip only on the CPU parametrization (`skip_unless_gpu`), so a kernel that fails to compile fails its tests.
246
247
  - **Mathematical meaning:** test documented definitions (a pulse width, a dispersion sign, noise-variance scaling) against independently derived expectations, not only shapes and energies.
@@ -4,6 +4,41 @@ Notable changes to CommKit, newest first. Performance figures are from the refer
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [3.6.0] - 2026-11-05
8
+
9
+ Chromatic dispersion for real captures. The compensation takes the link's accumulated dispersion and works as a receiver does, an FIR filter (or overlap-save blocks for backpropagation) in bounded memory, with the exact whole-record path as an option that is valid on captures too; three estimators measure an unknown dispersion, blind or from known symbols. The laser-characterization chain and its notebooks were corrected wherever a plot or a number did not mean what it said, method names that were generic now say what they do, and plot axes carry their SI prefix in the label.
10
+
11
+ ### Changes that may need code updates
12
+
13
+ - `correct_chromatic_dispersion` takes the link's accumulated dispersion, `dispersion_ps_nm=` (D·L in ps/nm), instead of `dispersion_ps_nm_km=` and `fiber_length_km=`: a receiver knows the total, and only the product enters the physics. Write `dispersion_ps_nm=17 * 80` for 80 km of standard fiber. `dispersion_compensation_taps` takes the same argument.
14
+ - `correct_chromatic_dispersion(method=)` and `correct_fiber_propagation(method=)` choose between two implementations of the same inverse. The default is the receiver's form, in bounded memory on a record of any length: CD by the FIR of `dispersion_compensation_taps` applied by overlap-save (`bandwidth=` and `num_taps=` choose the filter), DBP in overlapping blocks guarded by the link's dispersion memory, all blocks in one batched pass. `method="whole_record"` is the former path, the exact response over the whole record treated as one period; pass it to reproduce earlier results on simulated records. On a capture both are right except within half the dispersion memory of its ends, which no method can recover; `"whole_record"` is the more precise elsewhere, the default suits streams and studies of the filter.
15
+ - Method objects whose names were generic, or named one thing two ways, say what they do: `recovery.ConstantPhase` replaces `DataAided` (every known-symbol method is data-aided; this one fits one constant phase per channel), `recovery.PilotInterpolation` replaces `PilotAided` and `frequency.PilotSlope` replaces `PilotSymbols` (the same pilots, interpolated as a phase or fitted as a slope), `frequency.SpectralPeak` replaces `BiasTone` (the tone `recovery.PilotTone` names too; its mechanism is the spectral peak, the counterpart of `MthPower`), `analysis.DshFmFit` replaces `DshFmPsd` (one word from the result class `DshFmNoisePsd`), and `coding.StageDecoders` replaces `Stages`. Rename at the call; nothing else changes.
16
+ - `commkit.impairments.channel` is `commkit.impairments.fiber_channel`, leaving room for other channel models. Import from `commkit.impairments` and nothing changes.
17
+ - `filter_response` and `plot_filter_response` evaluate complex coefficients over the whole band `[-fs/2, fs/2)` by default, since their response differs at negative frequencies; real coefficients keep the grid from DC to Nyquist. Pass `frequencies=` for the old grid.
18
+ - `BetaSeparation`'s white-FM floor (`linewidth_floor`) is a Whittle fit of white FM, flicker FM and the detection noise's `f²` tail, the model of `DshFmFit`, instead of the median of an auto-detected plateau, and the estimate now carries `flicker`, `angle_noise_var` and `fm_psd_model`. Without a fence the fit runs from the plateau's lower edge to Nyquist. The default segment is the largest power of two within an eighth of the record (it was capped at 4096 samples), so the β-area can resolve the white-FM crossover; pass `nperseg=` for the old segment.
19
+ - Plot axes in SI units carry one SI prefix in the axis label (`Frequency [GHz]`, `Time [ns]`) with plain numbers on the ticks, chosen anew at every draw, so zooming or new limits rescale both; ticks no longer repeat the prefix (`10 G`). Log axes keep a prefix per tick. Limits stay in SI units (`xlim=(0, 2e9)`).
20
+ - `plot_allan_deviation` draws no `τ^(-1/2)` guide by default (`reference_slopes=False`): anchored at the first point, it was meaningless wherever that point is not white-FM limited.
21
+
22
+ ### Added
23
+
24
+ - `filtering.dispersion_compensation_taps` designs the FIR filter of `correct_chromatic_dispersion`: the least-squares fit of the inverse fiber response over a band (the signal's occupied bandwidth, or the whole sampled band), exact through Fresnel integrals. Its default length is 1.25 times the group-delay spread over the band, which reaches about -45 dB of residual against the frequency-domain compensation; fitting only the occupied band needs about a third of the taps of a fit over the whole band. Notebook 19 shows the taps, their response and the SNR against the length.
25
+ - `filtering.estimate_chromatic_dispersion` with `ClockToneScan` measures a capture's accumulated dispersion blind, before frequency and carrier recovery: Godard's clock-tone spectral correlation, every candidate scanned by one inverse FFT, the estimate the centroid of the peak (a standard deviation of 5 ps/nm at rolloff 0.1 and 1 ps/nm at 0.35 on 2^17 samples at 15 dB; within 62 ps/nm with 10 ps of DGD and a 1 GHz offset). It returns a `ChromaticDispersionEstimate` with the scan, which `correct_chromatic_dispersion(x, estimate)` takes as its `how` (or `correct_chromatic_dispersion(x, ClockToneScan())` to estimate first), and whose value designs the taps to study with `dispersion_compensation_taps(dispersion_ps_nm=estimate.value, ...)`.
26
+ - `estimate_chromatic_dispersion` has two more methods: `FourthMomentScan`, blind and without an excess band (Nyquist pulses), which scores each candidate compensation by the normalized fourth moment of the field (within 11 ps/nm at rolloff 0 and 0.1; about a second on the CPU, 0.05 s on the GPU), and `QuadraticPhaseFit`, data-aided from the Signal's reference or `symbols=`, which fits the quadratic phase of the channel response measured against the known symbols (within 12 ps/nm to ±34000 ps/nm at 6 dB Es/N0, with phase noise and PMD) and returns the measured group delay. `ClockToneScan` warns when the Signal's pulse has a rolloff below 0.05.
27
+
28
+ ### Fixed
29
+
30
+ - `BetaSeparation`'s floor read the foot of the AWGN `f²` tail as plateau: 11 % high on the 32 GBd, 1 MHz, 20 dB trajectory of notebook 23 at the default segment (now 0.999 MHz). When the Welch bins cannot resolve the white-FM crossover with the β-line, a warning says so and names the segment needed; the β-area used to return 0 silently.
31
+ - `DshFmPsd` on a real (single-photodiode) beat reports `fm_psd` and `fm_psd_model` as NaN beyond the beat's support, `min(f_shift, f_s/2 - f_shift)`, where the bins read several times low; `valid` marks it. The fit already excluded them.
32
+ - `plot_frequency_noise_psd` draws the fitted model through the same log-binned reduction as a dense PSD (a fringe-resolving DSH model swung at every notch and was drawn as a filled band), keeps the β-line from setting the vertical range, and no longer breaks log-binned curves at empty low-frequency cells. `plot_dsh_beat_psd` zooms on the line, twice the deep width on each side. `plot_carrier_phase_decomposition` labels the total phase next to the drift.
33
+ - The laser-characterization notebooks (23 to 28) report what their plots show: beat-spectrum limits in Hz (they were MHz on a Hz axis, an empty plot), the β-area resolved and explained as the observed width next to the intrinsic floor, the Allan deviation of the unfiltered frequency with its white-PM, white-FM and wander regions, the flicker corner on segments that resolve it, the record-to-record scatter of `DshIncrement` and of the Lorentzian widths, and verdicts in the measurement templates computed from the numbers instead of printed unconditionally.
34
+ - The fiber channel functions document that they treat their record as one period (circular), which the receiver inverts exactly with `method="whole_record"`.
35
+
36
+ ### Development
37
+
38
+ - The precision audit caught the clock-tone scan's frequency-shift phasor built in complex128, which promoted the complex64 scan; it is built in the data's precision.
39
+ - The transfer audit lists the dispersion estimators' host-side fits and the data-aided fit's known symbols; CD compensation and block DBP add no transfers.
40
+ - `AGENTS.md` states the plotting convention for SI units (the base unit in the label, the prefix chosen at draw time) and names the fiber-channel test files.
41
+
7
42
  ## [3.5.0] - 2026-11-05
8
43
 
9
44
  One name per concept, finished. A review of the API and the module layout renamed the arguments and result fields that broke the library's naming rules, split or merged the modules that broke its size rules, and fixed the two audit tools that could pass without checking anything. Numerical results do not change.
@@ -426,7 +461,8 @@ See the [GitHub release](https://github.com/lokgar/commkit/releases/tag/v1.1.0)
426
461
 
427
462
  First release. See the [GitHub release](https://github.com/lokgar/commkit/releases/tag/v1.0.0).
428
463
 
429
- [Unreleased]: https://github.com/lokgar/commkit/compare/v3.5.0...HEAD
464
+ [Unreleased]: https://github.com/lokgar/commkit/compare/v3.6.0...HEAD
465
+ [3.6.0]: https://github.com/lokgar/commkit/compare/v3.5.0...v3.6.0
430
466
  [3.5.0]: https://github.com/lokgar/commkit/compare/v3.4.0...v3.5.0
431
467
  [3.4.0]: https://github.com/lokgar/commkit/compare/v3.3.0...v3.4.0
432
468
  [3.3.0]: https://github.com/lokgar/commkit/compare/v3.2.1...v3.3.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: commkit
3
- Version: 3.5.0
3
+ Version: 3.6.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
@@ -147,8 +147,8 @@ CommKit never picks a device for you: data stays where you put it, and each func
147
147
  | [`commkit.spectral`](https://github.com/lokgar/commkit/blob/main/commkit/spectral.py) | Welch PSD, spectrograms, frequency shifting, pilot tones. |
148
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`. |
149
149
  | [`commkit.timing`](https://github.com/lokgar/commkit/blob/main/commkit/timing.py) | Barker and Zadoff-Chu sequences, `estimate_timing` / `correct_timing`, fractional delay estimation and correction. |
150
- | [`commkit.frequency`](https://github.com/lokgar/commkit/blob/main/commkit/frequency.py) | `estimate_frequency_offset` / `correct_frequency_offset` with `MthPower`, `MengaliMorelli`, `PilotSymbols` and `BiasTone`; static and blockwise. |
151
- | [`commkit.recovery`](https://github.com/lokgar/commkit/tree/main/commkit/recovery) | `estimate_carrier_phase` / `correct_carrier_phase` with `ViterbiViterbi`, `BPS`, `PLL`, `Tikhonov`, `DataAided`, `PilotAided`, `PilotTone(s)`; cycle-slip correction; `resolve_phase_ambiguity` and `resolve_channel_permutation`. |
150
+ | [`commkit.frequency`](https://github.com/lokgar/commkit/blob/main/commkit/frequency.py) | `estimate_frequency_offset` / `correct_frequency_offset` with `MthPower`, `MengaliMorelli`, `PilotSlope` and `SpectralPeak`; static and blockwise. |
151
+ | [`commkit.recovery`](https://github.com/lokgar/commkit/tree/main/commkit/recovery) | `estimate_carrier_phase` / `correct_carrier_phase` with `ViterbiViterbi`, `BPS`, `PLL`, `Tikhonov`, `ConstantPhase`, `PilotInterpolation`, `PilotTone(s)`; cycle-slip correction; `resolve_phase_ambiguity` and `resolve_channel_permutation`. |
152
152
  | [`commkit.equalization`](https://github.com/lokgar/commkit/tree/main/commkit/equalization) | Sequential `lms`, `rls`, `cma`, `rde` (Numba) and frequency-domain `block_lms`, `block_cma`, `block_rde` (CuPy, CUDA graphs), butterfly MIMO, inline carrier recovery (`cpr=PLL()` / `BPS()`), continuation with `state=`, `zf_equalizer`; pilot-aided CMA and RDE from a frame's pilots (`EqualizerPilots`); polarization demultiplexing on pilot tones (`estimate_polarization_tones` / `correct_polarization`). |
153
153
  | [`commkit.metrics`](https://github.com/lokgar/commkit/blob/main/commkit/metrics.py) | `evm`, `snr`, `ber`, `fer`, `ser`, `gmi`, `ngmi`, `mi`, including shaped constellations; pre-FEC and post-FEC (`ber(decoded, sig)`); `estimate_noise_variance` for LLRs; host values per channel. |
154
154
  | [`commkit.analysis`](https://github.com/lokgar/commkit/tree/main/commkit/analysis) | Laser and carrier-phase characterization: `estimate_linewidth` (increment slope, β-separation, delayed self-heterodyne FM-PSD / increment / Lorentzian), FM-noise PSDs, drift separation, Allan deviation. |
@@ -111,8 +111,8 @@ CommKit never picks a device for you: data stays where you put it, and each func
111
111
  | [`commkit.spectral`](https://github.com/lokgar/commkit/blob/main/commkit/spectral.py) | Welch PSD, spectrograms, frequency shifting, pilot tones. |
112
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`. |
113
113
  | [`commkit.timing`](https://github.com/lokgar/commkit/blob/main/commkit/timing.py) | Barker and Zadoff-Chu sequences, `estimate_timing` / `correct_timing`, fractional delay estimation and correction. |
114
- | [`commkit.frequency`](https://github.com/lokgar/commkit/blob/main/commkit/frequency.py) | `estimate_frequency_offset` / `correct_frequency_offset` with `MthPower`, `MengaliMorelli`, `PilotSymbols` and `BiasTone`; static and blockwise. |
115
- | [`commkit.recovery`](https://github.com/lokgar/commkit/tree/main/commkit/recovery) | `estimate_carrier_phase` / `correct_carrier_phase` with `ViterbiViterbi`, `BPS`, `PLL`, `Tikhonov`, `DataAided`, `PilotAided`, `PilotTone(s)`; cycle-slip correction; `resolve_phase_ambiguity` and `resolve_channel_permutation`. |
114
+ | [`commkit.frequency`](https://github.com/lokgar/commkit/blob/main/commkit/frequency.py) | `estimate_frequency_offset` / `correct_frequency_offset` with `MthPower`, `MengaliMorelli`, `PilotSlope` and `SpectralPeak`; static and blockwise. |
115
+ | [`commkit.recovery`](https://github.com/lokgar/commkit/tree/main/commkit/recovery) | `estimate_carrier_phase` / `correct_carrier_phase` with `ViterbiViterbi`, `BPS`, `PLL`, `Tikhonov`, `ConstantPhase`, `PilotInterpolation`, `PilotTone(s)`; cycle-slip correction; `resolve_phase_ambiguity` and `resolve_channel_permutation`. |
116
116
  | [`commkit.equalization`](https://github.com/lokgar/commkit/tree/main/commkit/equalization) | Sequential `lms`, `rls`, `cma`, `rde` (Numba) and frequency-domain `block_lms`, `block_cma`, `block_rde` (CuPy, CUDA graphs), butterfly MIMO, inline carrier recovery (`cpr=PLL()` / `BPS()`), continuation with `state=`, `zf_equalizer`; pilot-aided CMA and RDE from a frame's pilots (`EqualizerPilots`); polarization demultiplexing on pilot tones (`estimate_polarization_tones` / `correct_polarization`). |
117
117
  | [`commkit.metrics`](https://github.com/lokgar/commkit/blob/main/commkit/metrics.py) | `evm`, `snr`, `ber`, `fer`, `ser`, `gmi`, `ngmi`, `mi`, including shaped constellations; pre-FEC and post-FEC (`ber(decoded, sig)`); `estimate_noise_variance` for LLRs; host values per channel. |
118
118
  | [`commkit.analysis`](https://github.com/lokgar/commkit/tree/main/commkit/analysis) | Laser and carrier-phase characterization: `estimate_linewidth` (increment slope, β-separation, delayed self-heterodyne FM-PSD / increment / Lorentzian), FM-noise PSDs, drift separation, Allan deviation. |
@@ -54,11 +54,28 @@ def bench_fiber_propagation(benchmark, backend_device, xp, sync, monkeypatch, n,
54
54
  @pytest.mark.parametrize("path", PATHS)
55
55
  @pytest.mark.parametrize("steps", [1, 10])
56
56
  def bench_dbp(benchmark, backend_device, xp, sync, monkeypatch, steps, path):
57
+ """The whole record (``method="whole_record"``), as the 0008 baseline measured."""
57
58
  _stages(backend_device, monkeypatch, path)
58
59
  x = xp.asarray(fiber_workload(2**20))
59
60
 
60
61
  def run():
61
- out = correct_fiber_propagation(x, steps_per_span=steps, **LINK)
62
+ out = correct_fiber_propagation(
63
+ x, steps_per_span=steps, method="whole_record", **LINK
64
+ )
65
+ sync()
66
+ return out
67
+
68
+ benchmark.pedantic(run, **ROUNDS)
69
+
70
+
71
+ @pytest.mark.parametrize("path", PATHS)
72
+ def bench_dbp_blocks(benchmark, backend_device, xp, sync, monkeypatch, path):
73
+ """A capture: overlap-save blocks, ten steps per span."""
74
+ _stages(backend_device, monkeypatch, path)
75
+ x = xp.asarray(fiber_workload(2**20))
76
+
77
+ def run():
78
+ out = correct_fiber_propagation(x, steps_per_span=10, **LINK)
62
79
  sync()
63
80
  return out
64
81
 
@@ -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.5.0"
34
+ __version__ = "3.6.0"
35
35
 
36
36
  from .backend import PrecisionWarning
37
37
  from .core import (
@@ -0,0 +1,194 @@
1
+ """Chromatic dispersion physics (private).
2
+
3
+ Single source of the fiber dispersion model used by
4
+ ``impairments.apply_chromatic_dispersion`` (the channel) and
5
+ ``filtering.correct_chromatic_dispersion`` (its inverse):
6
+
7
+ - ``beta2_length``: the accumulated dispersion D·L [ps/nm] and λ [nm] to the
8
+ β₂·L product [s²], ``β₂ = -D λ² / (2π c)``; ``beta3_length`` adds the
9
+ accumulated slope S·L [ps/nm²] for the β₃·L product [s³]
10
+ (``commkit._fiber``). The channel passes D·L of a fiber, the receiver the
11
+ accumulated dispersion it knows.
12
+ - ``dispersion_phase``: the phase ``-(β₂Lω²/2 + β₃Lω³/6)`` on the FFT grid
13
+ (also used by ``commkit._fiber``).
14
+ - ``transfer_function``: ``H(ω) = exp(-j β₂ L ω² / 2)`` on the FFT grid of
15
+ ``num_samples`` samples, or its inverse ``exp(+j β₂ L ω² / 2)``.
16
+ - ``spread_samples`` and ``compensation_taps``: the group-delay spread over
17
+ the sampled band and the FIR form of the inverse
18
+ (``filtering.dispersion_compensation_taps``).
19
+
20
+ Sign convention: NumPy's FFT uses the engineering kernel ``exp(-jωt)``, so a
21
+ field propagating through the fiber is multiplied by ``exp(-j β₂ L ω² / 2)``.
22
+ Its group delay ``τ(ω) = -dφ/dω = β₂ L ω`` makes the higher frequency
23
+ (shorter wavelength) arrive first in anomalous fiber (D > 0, β₂ < 0), with
24
+ ``Δτ = D L Δλ``.
25
+ """
26
+
27
+ from types import ModuleType
28
+ from typing import Any
29
+
30
+ import numpy as np
31
+ import scipy.linalg
32
+ import scipy.special
33
+
34
+ from . import _fft
35
+ from ._array import as_2d, restore_1d
36
+ from ._constants import SPEED_OF_LIGHT as _SPEED_OF_LIGHT
37
+ from .backend import ArrayType, dispatch
38
+ from .math import _phasor
39
+
40
+
41
+ def beta2_length(*, dispersion_ps_nm: float, center_wavelength_nm: float) -> float:
42
+ """The ``β₂ · L`` product in s² for the accumulated dispersion D·L in ps/nm."""
43
+ dl = dispersion_ps_nm * 1e-12 / 1e-9 # s/m
44
+ lam = center_wavelength_nm * 1e-9 # m
45
+ return float(-(dl * lam**2) / (2.0 * np.pi * _SPEED_OF_LIGHT))
46
+
47
+
48
+ def beta3_length(
49
+ *,
50
+ dispersion_ps_nm: float,
51
+ dispersion_slope_ps_nm2: float,
52
+ center_wavelength_nm: float,
53
+ ) -> float:
54
+ """The ``β₃ · L`` product in s³ for D·L, the accumulated slope S·L and λ.
55
+
56
+ From ``D = -(2πc/λ²) β₂`` and ``dω/dλ = -2πc/λ²``:
57
+ ``β₃ = (S + 2D/λ) · (λ² / (2πc))²``, times the length.
58
+ """
59
+ dl = dispersion_ps_nm * 1e-12 / 1e-9 # s/m
60
+ sl = dispersion_slope_ps_nm2 * 1e-12 / (1e-9 * 1e-9) # s/m²
61
+ lam = center_wavelength_nm * 1e-9 # m
62
+ k = lam**2 / (2.0 * np.pi * _SPEED_OF_LIGHT)
63
+ return float((sl + 2.0 * dl / lam) * k**2)
64
+
65
+
66
+ def dispersion_phase(
67
+ num_samples: int,
68
+ *,
69
+ sampling_rate: float,
70
+ beta2_length: float,
71
+ beta3_length: float = 0.0,
72
+ xp: ModuleType,
73
+ ) -> ArrayType:
74
+ """Propagation phase ``-(β₂L ω²/2 + β₃L ω³/6)`` on the ``fftfreq`` grid.
75
+
76
+ float64 on ``xp``'s device, because it reaches thousands of radians.
77
+ """
78
+ omega = 2.0 * np.pi * xp.fft.fftfreq(num_samples, d=1.0 / sampling_rate)
79
+ phase = (beta2_length / 2.0) * omega**2
80
+ if beta3_length:
81
+ phase += (beta3_length / 6.0) * omega**3
82
+ return -phase
83
+
84
+
85
+ def transfer_function(
86
+ num_samples: int,
87
+ *,
88
+ sampling_rate: float,
89
+ beta2_length: float,
90
+ inverse: bool,
91
+ xp: ModuleType,
92
+ dtype: Any = np.complex128,
93
+ ) -> ArrayType:
94
+ """Fiber transfer function on the ``fftfreq`` grid (or its inverse).
95
+
96
+ The phase is computed in float64 and the response is returned as
97
+ ``dtype``, so complex64 data is filtered in single precision instead of
98
+ being promoted.
99
+ """
100
+ phase = dispersion_phase(
101
+ num_samples, sampling_rate=sampling_rate, beta2_length=beta2_length, xp=xp
102
+ )
103
+ return _phasor(-phase if inverse else phase, dtype, xp)
104
+
105
+
106
+ def spread_samples(
107
+ beta2_length: float, *, sampling_rate: float, bandwidth: float
108
+ ) -> float:
109
+ """Group-delay spread over a band, in samples.
110
+
111
+ ``τ(ω) = β₂ L ω`` over ``|f| <= bandwidth / 2`` spans
112
+ ``2π |β₂ L| · bandwidth`` seconds; over the whole sampled band
113
+ (``bandwidth = fs``) this is Savory's FIR length (Opt. Express 16, 2008).
114
+ """
115
+ return 2.0 * np.pi * abs(beta2_length) * bandwidth * sampling_rate
116
+
117
+
118
+ def _band_integral(a: float, n: np.ndarray, half_band: float) -> np.ndarray:
119
+ """``(1/2π) ∫_{-W}^{W} exp(j(a θ² + n θ)) dθ`` by Fresnel integrals.
120
+
121
+ Completing the square, ``a θ² + n θ = a (θ + n/2a)² - n²/4a``.
122
+ """
123
+ scale = np.sqrt(2.0 * abs(a) / np.pi)
124
+ shift = n / (2.0 * a)
125
+ s_hi, c_hi = scipy.special.fresnel(scale * (half_band + shift))
126
+ s_lo, c_lo = scipy.special.fresnel(scale * (-half_band + shift))
127
+ integral = np.sqrt(np.pi / (2.0 * abs(a))) * (
128
+ (c_hi - c_lo) + 1j * np.sign(a) * (s_hi - s_lo)
129
+ )
130
+ phase = np.exp(-1j * n**2 / (4.0 * a))
131
+ return np.asarray(phase * integral / (2.0 * np.pi), dtype=np.complex128)
132
+
133
+
134
+ #: Tikhonov term added to the diagonal of the band's Gram matrix (whose
135
+ #: diagonal is ``bandwidth / fs``): bounds the gain outside the band.
136
+ _LS_REGULARIZATION = 1e-6
137
+
138
+
139
+ def compensation_taps(
140
+ beta2_length: float, *, sampling_rate: float, bandwidth: float, num_taps: int
141
+ ) -> np.ndarray:
142
+ """Least-squares FIR fit of the inverse response over a band.
143
+
144
+ Minimizes ``∫_{|θ|<W} |Σ h[n] e^{-jθn} - exp(j a θ²)|² dθ`` with
145
+ ``θ = ω / fs``, ``W = π · bandwidth / fs`` and ``a = β₂ L fs² / 2``, for
146
+ ``n`` centred on tap ``(num_taps - 1) // 2`` (Eghbali et al., J.
147
+ Lightw. Technol. 32, 2014): ``Q h = p`` with the Toeplitz Gram matrix
148
+ ``Q[m, k] = (W/π) sinc(W (m-k) / π)`` and ``p[m]`` the band integral of
149
+ the response, exact through Fresnel integrals. Over the whole band
150
+ (``W = π``) ``Q`` is the identity and ``h = p``, the truncated
151
+ band-limited impulse response. Host complex128.
152
+ """
153
+ n = np.arange(num_taps) - (num_taps - 1) // 2
154
+ a = beta2_length * sampling_rate**2 / 2.0
155
+ if a == 0.0:
156
+ return np.asarray(n == 0, dtype=np.complex128)
157
+ half_band = np.pi * bandwidth / sampling_rate
158
+ p = _band_integral(a, n, half_band)
159
+ if bandwidth >= sampling_rate:
160
+ return p
161
+ col = (half_band / np.pi) * np.sinc(half_band * np.arange(num_taps) / np.pi)
162
+ col[0] += _LS_REGULARIZATION
163
+ return np.asarray(scipy.linalg.solve_toeplitz(col, p), dtype=np.complex128)
164
+
165
+
166
+ def apply_dispersion(
167
+ samples: ArrayType,
168
+ *,
169
+ sampling_rate: float,
170
+ dispersion_ps_nm: float,
171
+ center_wavelength_nm: float,
172
+ inverse: bool,
173
+ ) -> ArrayType:
174
+ """Filter samples with the fiber's dispersion response or its inverse.
175
+
176
+ ``(N,)`` or ``(C, N)`` samples keep their shape, dtype and device.
177
+ """
178
+ samples, xp, _ = dispatch(samples)
179
+ samples, was_1d = as_2d(samples, name="samples")
180
+ h = transfer_function(
181
+ samples.shape[-1],
182
+ sampling_rate=sampling_rate,
183
+ beta2_length=beta2_length(
184
+ dispersion_ps_nm=dispersion_ps_nm,
185
+ center_wavelength_nm=center_wavelength_nm,
186
+ ),
187
+ inverse=inverse,
188
+ xp=xp,
189
+ dtype=np.result_type(samples.dtype, np.complex64),
190
+ )
191
+ result = _fft.ifft(_fft.fft(samples, axis=-1) * h[None, :], axis=-1)
192
+ if result.dtype != samples.dtype:
193
+ result = result.astype(samples.dtype)
194
+ return restore_1d(was_1d, result)
@@ -620,18 +620,19 @@ def propagate(
620
620
  ) -> ArrayType:
621
621
  """Propagate the field over the link, or backpropagate it.
622
622
 
623
- ``x`` is ``(C, N)`` in sqrt(W) with C in {1, 2}; ``direction=-1``
624
- backpropagates (DBP: no ASE, no PMD).
623
+ ``x`` is ``(C, N)`` in sqrt(W) with C in {1, 2}, or ``(C, B, N)``: ``B``
624
+ independent records (the blocks of an overlap-save DBP) propagated at
625
+ once, which every stage broadcasts over; ``direction=-1`` backpropagates
626
+ (DBP: no ASE, no PMD).
625
627
 
626
628
  ``x`` is consumed: it is the working buffer.
627
629
  """
628
630
  fiber = link.fiber
629
- C, N = x.shape
631
+ C, N = x.shape[0], x.shape[-1]
630
632
  forward = direction > 0
631
633
  plan = _span_plan(link, C)
632
- kw = {
633
- "dispersion_ps_nm_km": fiber.dispersion_ps_nm_km,
634
- "fiber_length_km": 1.0,
634
+ kw = { # per km: the accumulated dispersion of 1 km is D
635
+ "dispersion_ps_nm": fiber.dispersion_ps_nm_km,
635
636
  "center_wavelength_nm": link.center_wavelength_nm,
636
637
  }
637
638
  slope = fiber.dispersion_slope_ps_nm2_km
@@ -640,9 +641,7 @@ def propagate(
640
641
  sampling_rate=sampling_rate,
641
642
  beta2_length=beta2_length(**kw),
642
643
  beta3_length=(
643
- 0.0
644
- if slope is None
645
- else beta3_length(dispersion_slope_ps_nm2_km=slope, **kw)
644
+ 0.0 if slope is None else beta3_length(dispersion_slope_ps_nm2=slope, **kw)
646
645
  ),
647
646
  xp=xp,
648
647
  )
@@ -11,7 +11,7 @@ DSP stages that produce them:
11
11
  - :func:`allan_deviation`: overlapping Allan deviation of the frequency.
12
12
  - :func:`estimate_linewidth`: the laser linewidth, with one method object per
13
13
  estimator (:class:`IncrementSlope`, :class:`IncrementSubtract`,
14
- :class:`BetaSeparation` on phase trajectories; :class:`DshFmPsd`,
14
+ :class:`BetaSeparation` on phase trajectories; :class:`DshFmFit`,
15
15
  :class:`DshIncrement`, :class:`DshLorentzian` on delayed self-heterodyne
16
16
  beats, whose front end is :func:`dsh_beat` and :func:`dsh_phase`).
17
17
 
@@ -32,7 +32,7 @@ from .fm_noise import DshFmNoisePsd, dsh_fm_noise_psd, fm_noise_psd
32
32
  from .interferometry import dsh_beat, dsh_phase
33
33
  from .linewidth import (
34
34
  BetaSeparation,
35
- DshFmPsd,
35
+ DshFmFit,
36
36
  DshIncrement,
37
37
  DshLorentzian,
38
38
  IncrementSlope,
@@ -46,7 +46,7 @@ __all__ = [
46
46
  "AllanDeviation",
47
47
  "BetaSeparation",
48
48
  "DshFmNoisePsd",
49
- "DshFmPsd",
49
+ "DshFmFit",
50
50
  "DshIncrement",
51
51
  "DshLorentzian",
52
52
  "FrequencyDrift",
@@ -10,16 +10,13 @@ from scipy.optimize import nnls
10
10
  from scipy.stats import chi2
11
11
 
12
12
  from ..backend import to_device
13
- from ..logger import logger
14
13
 
15
14
  __all__ = [
16
15
  "_BETA_SLOPE",
17
16
  "_FWHM_FROM_AREA",
18
- "_floor_levels",
19
17
  "_increment_variance_fit",
20
18
  "_plateau_mask",
21
19
  "_resolve_nperseg",
22
- "_welch_floor_bias",
23
20
  "_welch_median_bias",
24
21
  "_whittle_fit",
25
22
  ]
@@ -248,118 +245,6 @@ def _plateau_mask(
248
245
  return used2, levels
249
246
 
250
247
 
251
- def _welch_floor_bias(n_samples: int, nperseg: int, *, label: str) -> tuple[float, int]:
252
- """``_welch_median_bias`` plus the shared segment-count diagnostic.
253
-
254
- Every median-based white-FM floor in this package (β-separation and DSH
255
- alike) divides out the same χ²-median bias and wants the same warning when
256
- too few Welch segments back it, so the message lives here rather than being
257
- re-typed per estimator.
258
-
259
- Parameters
260
- ----------
261
- n_samples : int
262
- Length of the sequence handed to Welch (the differentiated phase).
263
- nperseg : int
264
- Welch segment length actually used (see ``_resolve_nperseg``).
265
- label : str
266
- Caller name used in the log messages.
267
-
268
- Returns
269
- -------
270
- factor : float
271
- ``median/mean`` ratio to divide the median-based floor by.
272
- num_segments : int
273
- Welch segment count ``K``.
274
- """
275
- m_med, k_seg = _welch_median_bias(n_samples, nperseg)
276
- if k_seg < 4:
277
- logger.warning(
278
- "%s: only %d Welch segment(s) at nperseg=%d - the floor median is "
279
- "noisy and the χ²-median correction (÷%.3f) is asymptotic; extend "
280
- "the record (aim for ≥ 5·nperseg samples).",
281
- label,
282
- k_seg,
283
- nperseg,
284
- m_med,
285
- )
286
- else:
287
- logger.info(
288
- "%s: %d Welch segments - χ²-median floor bias corrected by ÷%.4f.",
289
- label,
290
- k_seg,
291
- m_med,
292
- )
293
- return m_med, k_seg
294
-
295
-
296
- def _floor_levels(
297
- f,
298
- s2,
299
- base2,
300
- *,
301
- auto: bool,
302
- label: str,
303
- fallback_desc: str = "the fenced band",
304
- ):
305
- """White-FM floor level per channel, by plateau auto-detection or fence.
306
-
307
- The summary half of both PSD linewidth routes: ``Δν = π · level`` once the
308
- caller divides out the χ²-median bias. Host-side NumPy by design (the
309
- spectrum has already been transferred once).
310
-
311
- Parameters
312
- ----------
313
- f : ndarray, (nf,)
314
- One-sided frequency axis (host).
315
- s2 : ndarray, (C, nf)
316
- FM-noise PSD per channel (host; NaN allowed at masked bins).
317
- base2 : ndarray of bool, (C, nf)
318
- Eligible bins (validity mask and any user fences).
319
- auto : bool
320
- If True, auto-detect the plateau (``_plateau_mask``) and fall back to
321
- ``base2`` per channel where detection fails. If False, the median
322
- runs over ``base2`` as given.
323
- label : str
324
- Caller name used in the fallback warning.
325
- fallback_desc : str
326
- Human-readable description of ``base2``, the fallback, for the warning.
327
-
328
- Returns
329
- -------
330
- used2 : ndarray of bool, (C, nf)
331
- Bins the median actually ran over.
332
- levels : ndarray, (C,)
333
- Raw median level per channel - **not** χ²-corrected - or NaN where no
334
- eligible bin remained (the caller decides whether that is a zero floor
335
- or an error).
336
- """
337
- n_ch = s2.shape[0]
338
-
339
- def _median(mask_row, ch):
340
- return float(np.median(s2[ch][mask_row])) if mask_row.any() else np.nan
341
-
342
- if auto:
343
- used2, levels = _plateau_mask(f, s2, base2)
344
- for c in range(n_ch):
345
- if np.isfinite(levels[c]):
346
- continue
347
- logger.warning(
348
- "%s: no plateau detected (channel %d) - the floor median falls "
349
- "back to %s. Inspect the PSD before quoting Δν.",
350
- label,
351
- c,
352
- fallback_desc,
353
- )
354
- used2[c] = base2[c]
355
- levels[c] = _median(used2[c], c)
356
- return used2, levels
357
-
358
- used2 = np.broadcast_to(base2, s2.shape)
359
- levels = np.array([_median(used2[c], c) for c in range(n_ch)])
360
- return used2, levels
361
-
362
-
363
248
  def _increment_variance_fit(y2, lags, dt: float, xp):
364
249
  r"""Least-squares fit of the increment variance ``Var(Δ_a)`` against lag.
365
250
 
@@ -278,7 +278,7 @@ def dsh_fm_noise_psd(
278
278
  -------
279
279
  DshFmNoisePsd
280
280
  ``f``, ``S_f`` and ``valid``, on the input's device;
281
- ``estimate_linewidth(x, DshFmPsd(...))`` is the reporting layer that
281
+ ``estimate_linewidth(x, DshFmFit(...))`` is the reporting layer that
282
282
  returns host values.
283
283
 
284
284
  See Also
@@ -295,7 +295,7 @@ def dsh_fm_noise_psd(
295
295
  * Additive detection noise on the beat adds white phase noise, an
296
296
  ``∝ sin²(πf/f_s)`` (≈ ``f²``) term that is not shaped by the
297
297
  interferometer; after the division it rises in every lobe.
298
- ``DshFmPsd`` fits it as a separate term instead of dividing it.
298
+ ``DshFmFit`` fits it as a separate term instead of dividing it.
299
299
  * Near the notches (long segments only) the division amplifies the
300
300
  estimation noise by ``1/(4 sin²)``.
301
301
  * With long segments the result depends on τ_d through the notch
@@ -72,7 +72,7 @@ use.
72
72
  The DSH estimators are methods of
73
73
  :func:`~commkit.analysis.linewidth.estimate_linewidth`:
74
74
 
75
- * ``DshFmPsd``: fits white FM, flicker FM and detection noise to the beat's
75
+ * ``DshFmFit``: fits white FM, flicker FM and detection noise to the beat's
76
76
  FM-noise PSD, through the interferometer response as a Welch estimate sees
77
77
  it; it works in the coherent (short-delay) and the incoherent regime and,
78
78
  with its default segments, needs the delay only roughly.
@@ -6,7 +6,7 @@ The method object chooses the estimator and the kind of record it reads:
6
6
  ``IncrementSubtract`` and ``BetaSeparation``, e.g. the output of
7
7
  ``carrier_phase_trajectory`` or ``separate_drift_phase_noise``;
8
8
  * delayed self-heterodyne / self-homodyne beat records (real photocurrent or
9
- complex IQ, array or Signal) - ``DshFmPsd``, ``DshIncrement`` and
9
+ complex IQ, array or Signal) - ``DshFmFit``, ``DshIncrement`` and
10
10
  ``DshLorentzian`` (see :mod:`commkit.analysis.interferometry`).
11
11
 
12
12
  Every method returns a :class:`LinewidthEstimate`: the linewidth in Hz plus
@@ -21,7 +21,7 @@ from ...backend import ArrayType, dispatch
21
21
  from ...core._signal_adapter import adapt_signal
22
22
  from ...core.signal import Signal
23
23
  from .dsh import (
24
- DshFmPsd,
24
+ DshFmFit,
25
25
  DshIncrement,
26
26
  DshLorentzian,
27
27
  _dsh_fm_psd,
@@ -41,7 +41,7 @@ from .result import LinewidthEstimate
41
41
 
42
42
  __all__ = [
43
43
  "BetaSeparation",
44
- "DshFmPsd",
44
+ "DshFmFit",
45
45
  "DshIncrement",
46
46
  "DshLorentzian",
47
47
  "IncrementSlope",
@@ -61,7 +61,7 @@ _PHASE_METHODS: dict[type, Any] = {
61
61
  BetaSeparation: _beta_separation,
62
62
  }
63
63
  _BEAT_METHODS: dict[type, Any] = {
64
- DshFmPsd: _dsh_fm_psd,
64
+ DshFmFit: _dsh_fm_psd,
65
65
  DshIncrement: _dsh_increment,
66
66
  DshLorentzian: _dsh_lorentzian,
67
67
  }
@@ -85,7 +85,7 @@ def estimate_linewidth(
85
85
  real phase trajectory in radians (``carrier_phase_trajectory``,
86
86
  ``separate_drift_phase_noise``, ``dsh_phase``). Complex input
87
87
  raises.
88
- * ``DshFmPsd``, ``DshIncrement``, ``DshLorentzian``: a delayed
88
+ * ``DshFmFit``, ``DshIncrement``, ``DshLorentzian``: a delayed
89
89
  self-heterodyne beat record, real (single photodetector) or complex
90
90
  (IQ front end), as an array or a Signal. A phase trajectory is
91
91
  real too, so it cannot be rejected by type: real input logs a
@@ -94,7 +94,7 @@ def estimate_linewidth(
94
94
  beat line, a peak 10 dB above the spectrum at half and 1.5 times
95
95
  its frequency (a drift-removed residual, or a beat too weak or too
96
96
  wide for its carrier to measure).
97
- method : IncrementSlope, IncrementSubtract, BetaSeparation, DshFmPsd, DshIncrement or DshLorentzian
97
+ method : IncrementSlope, IncrementSubtract, BetaSeparation, DshFmFit, DshIncrement or DshLorentzian
98
98
  The estimator and its parameters.
99
99
  sampling_rate : float, optional
100
100
  Sampling rate of the record in Hz (a fact: the symbol rate for a
@@ -133,6 +133,6 @@ def estimate_linewidth(
133
133
  return _BEAT_METHODS[kind](x, method, fs)
134
134
  raise TypeError(
135
135
  f"{name}: method must be IncrementSlope, IncrementSubtract, "
136
- "BetaSeparation, DshFmPsd, DshIncrement or DshLorentzian, got "
136
+ "BetaSeparation, DshFmFit, DshIncrement or DshLorentzian, got "
137
137
  f"{kind.__name__}."
138
138
  )