commkit 3.7.0__tar.gz → 3.7.1__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 (320) hide show
  1. {commkit-3.7.0 → commkit-3.7.1}/CHANGELOG.md +23 -1
  2. {commkit-3.7.0 → commkit-3.7.1}/PKG-INFO +1 -1
  3. {commkit-3.7.0 → commkit-3.7.1}/commkit/__init__.py +1 -1
  4. {commkit-3.7.0 → commkit-3.7.1}/commkit/timing/_numba_loops.py +21 -5
  5. {commkit-3.7.0 → commkit-3.7.1}/commkit/timing/clock_recovery.py +78 -2
  6. {commkit-3.7.0 → commkit-3.7.1}/examples/10-front-end-impairments.ipynb +259 -75
  7. commkit-3.7.1/examples/12-sampling-clock-recovery.ipynb +743 -0
  8. commkit-3.7.1/examples/22-direct-and-self-coherent-detection.ipynb +736 -0
  9. {commkit-3.7.0 → commkit-3.7.1}/examples/README.md +3 -3
  10. {commkit-3.7.0 → commkit-3.7.1}/pyproject.toml +2 -2
  11. {commkit-3.7.0 → commkit-3.7.1}/tests/common/reference_impl.py +4 -2
  12. {commkit-3.7.0 → commkit-3.7.1}/tests/timing/test_clock_recovery.py +73 -0
  13. {commkit-3.7.0 → commkit-3.7.1}/uv.lock +1 -1
  14. commkit-3.7.0/examples/12-sampling-clock-recovery.ipynb +0 -316
  15. commkit-3.7.0/examples/22-direct-and-self-coherent-detection.ipynb +0 -278
  16. {commkit-3.7.0 → commkit-3.7.1}/.gitattributes +0 -0
  17. {commkit-3.7.0 → commkit-3.7.1}/.github/icon.svg +0 -0
  18. {commkit-3.7.0 → commkit-3.7.1}/.github/logo.svg +0 -0
  19. {commkit-3.7.0 → commkit-3.7.1}/.github/workflows/ci.yml +0 -0
  20. {commkit-3.7.0 → commkit-3.7.1}/.github/workflows/publish.yml +0 -0
  21. {commkit-3.7.0 → commkit-3.7.1}/.gitignore +0 -0
  22. {commkit-3.7.0 → commkit-3.7.1}/.python-version +0 -0
  23. {commkit-3.7.0 → commkit-3.7.1}/AGENTS.md +0 -0
  24. {commkit-3.7.0 → commkit-3.7.1}/LICENSE +0 -0
  25. {commkit-3.7.0 → commkit-3.7.1}/README.md +0 -0
  26. {commkit-3.7.0 → commkit-3.7.1}/benchmarks/baselines/Linux-CPython-3.14-64bit/0002_pre_v2.json +0 -0
  27. {commkit-3.7.0 → commkit-3.7.1}/benchmarks/baselines/Linux-CPython-3.14-64bit/0003_v2_0.json +0 -0
  28. {commkit-3.7.0 → commkit-3.7.1}/benchmarks/baselines/Linux-CPython-3.14-64bit/0004_fiber_dbp.json +0 -0
  29. {commkit-3.7.0 → commkit-3.7.1}/benchmarks/baselines/Linux-CPython-3.14-64bit/0005_gpu_decoder_path.json +0 -0
  30. {commkit-3.7.0 → commkit-3.7.1}/benchmarks/baselines/Linux-CPython-3.14-64bit/0006_perf_cleanup.json +0 -0
  31. {commkit-3.7.0 → commkit-3.7.1}/benchmarks/baselines/Linux-CPython-3.14-64bit/0007_v3_0.json +0 -0
  32. {commkit-3.7.0 → commkit-3.7.1}/benchmarks/baselines/Linux-CPython-3.14-64bit/0008_v3_1.json +0 -0
  33. {commkit-3.7.0 → commkit-3.7.1}/benchmarks/bench_block_blind.py +0 -0
  34. {commkit-3.7.0 → commkit-3.7.1}/benchmarks/bench_block_lms.py +0 -0
  35. {commkit-3.7.0 → commkit-3.7.1}/benchmarks/bench_bps.py +0 -0
  36. {commkit-3.7.0 → commkit-3.7.1}/benchmarks/bench_coding.py +0 -0
  37. {commkit-3.7.0 → commkit-3.7.1}/benchmarks/bench_equalizers.py +0 -0
  38. {commkit-3.7.0 → commkit-3.7.1}/benchmarks/bench_fiber.py +0 -0
  39. {commkit-3.7.0 → commkit-3.7.1}/benchmarks/bench_filters.py +0 -0
  40. {commkit-3.7.0 → commkit-3.7.1}/benchmarks/bench_gardner.py +0 -0
  41. {commkit-3.7.0 → commkit-3.7.1}/benchmarks/bench_import.py +0 -0
  42. {commkit-3.7.0 → commkit-3.7.1}/benchmarks/bench_llr.py +0 -0
  43. {commkit-3.7.0 → commkit-3.7.1}/benchmarks/bench_mapping.py +0 -0
  44. {commkit-3.7.0 → commkit-3.7.1}/benchmarks/bench_signal_pipeline.py +0 -0
  45. {commkit-3.7.0 → commkit-3.7.1}/benchmarks/bench_sync_misc.py +0 -0
  46. {commkit-3.7.0 → commkit-3.7.1}/benchmarks/benchutils.py +0 -0
  47. {commkit-3.7.0 → commkit-3.7.1}/benchmarks/conftest.py +0 -0
  48. {commkit-3.7.0 → commkit-3.7.1}/benchmarks/record_baseline.py +0 -0
  49. {commkit-3.7.0 → commkit-3.7.1}/benchmarks/workloads.py +0 -0
  50. {commkit-3.7.0 → commkit-3.7.1}/commkit/_array.py +0 -0
  51. {commkit-3.7.0 → commkit-3.7.1}/commkit/_circular.py +0 -0
  52. {commkit-3.7.0 → commkit-3.7.1}/commkit/_constants.py +0 -0
  53. {commkit-3.7.0 → commkit-3.7.1}/commkit/_cuda/__init__.py +0 -0
  54. {commkit-3.7.0 → commkit-3.7.1}/commkit/_cuda/compiler.py +0 -0
  55. {commkit-3.7.0 → commkit-3.7.1}/commkit/_cuda/graphs.py +0 -0
  56. {commkit-3.7.0 → commkit-3.7.1}/commkit/_cuda/src/algebraic.cu +0 -0
  57. {commkit-3.7.0 → commkit-3.7.1}/commkit/_cuda/src/bps_anchor.cu +0 -0
  58. {commkit-3.7.0 → commkit-3.7.1}/commkit/_cuda/src/bps_min_d2.cu +0 -0
  59. {commkit-3.7.0 → commkit-3.7.1}/commkit/_cuda/src/cs_block.cu +0 -0
  60. {commkit-3.7.0 → commkit-3.7.1}/commkit/_cuda/src/iir.cu +0 -0
  61. {commkit-3.7.0 → commkit-3.7.1}/commkit/_cuda/src/ldpc.cu +0 -0
  62. {commkit-3.7.0 → commkit-3.7.1}/commkit/_cuda/src/llr.cu +0 -0
  63. {commkit-3.7.0 → commkit-3.7.1}/commkit/_cuda/src/selftest.cu +0 -0
  64. {commkit-3.7.0 → commkit-3.7.1}/commkit/_dispersion.py +0 -0
  65. {commkit-3.7.0 → commkit-3.7.1}/commkit/_fft.py +0 -0
  66. {commkit-3.7.0 → commkit-3.7.1}/commkit/_fiber.py +0 -0
  67. {commkit-3.7.0 → commkit-3.7.1}/commkit/_overlap_save.py +0 -0
  68. {commkit-3.7.0 → commkit-3.7.1}/commkit/_random.py +0 -0
  69. {commkit-3.7.0 → commkit-3.7.1}/commkit/_sequences.py +0 -0
  70. {commkit-3.7.0 → commkit-3.7.1}/commkit/analysis/__init__.py +0 -0
  71. {commkit-3.7.0 → commkit-3.7.1}/commkit/analysis/_common.py +0 -0
  72. {commkit-3.7.0 → commkit-3.7.1}/commkit/analysis/allan.py +0 -0
  73. {commkit-3.7.0 → commkit-3.7.1}/commkit/analysis/drift.py +0 -0
  74. {commkit-3.7.0 → commkit-3.7.1}/commkit/analysis/fm_noise.py +0 -0
  75. {commkit-3.7.0 → commkit-3.7.1}/commkit/analysis/interferometry.py +0 -0
  76. {commkit-3.7.0 → commkit-3.7.1}/commkit/analysis/linewidth/__init__.py +0 -0
  77. {commkit-3.7.0 → commkit-3.7.1}/commkit/analysis/linewidth/dsh.py +0 -0
  78. {commkit-3.7.0 → commkit-3.7.1}/commkit/analysis/linewidth/phase.py +0 -0
  79. {commkit-3.7.0 → commkit-3.7.1}/commkit/analysis/linewidth/result.py +0 -0
  80. {commkit-3.7.0 → commkit-3.7.1}/commkit/analysis/trajectory.py +0 -0
  81. {commkit-3.7.0 → commkit-3.7.1}/commkit/backend.py +0 -0
  82. {commkit-3.7.0 → commkit-3.7.1}/commkit/coding/__init__.py +0 -0
  83. {commkit-3.7.0 → commkit-3.7.1}/commkit/coding/_galois.py +0 -0
  84. {commkit-3.7.0 → commkit-3.7.1}/commkit/coding/_gf2.py +0 -0
  85. {commkit-3.7.0 → commkit-3.7.1}/commkit/coding/_ldpc_tables.py +0 -0
  86. {commkit-3.7.0 → commkit-3.7.1}/commkit/coding/_numba_loops.py +0 -0
  87. {commkit-3.7.0 → commkit-3.7.1}/commkit/coding/algebraic.py +0 -0
  88. {commkit-3.7.0 → commkit-3.7.1}/commkit/coding/base.py +0 -0
  89. {commkit-3.7.0 → commkit-3.7.1}/commkit/coding/convolutional.py +0 -0
  90. {commkit-3.7.0 → commkit-3.7.1}/commkit/coding/crc.py +0 -0
  91. {commkit-3.7.0 → commkit-3.7.1}/commkit/coding/exit.py +0 -0
  92. {commkit-3.7.0 → commkit-3.7.1}/commkit/coding/interleaving.py +0 -0
  93. {commkit-3.7.0 → commkit-3.7.1}/commkit/coding/ldpc.py +0 -0
  94. {commkit-3.7.0 → commkit-3.7.1}/commkit/coding/linear.py +0 -0
  95. {commkit-3.7.0 → commkit-3.7.1}/commkit/coding/performance.py +0 -0
  96. {commkit-3.7.0 → commkit-3.7.1}/commkit/coding/polar.py +0 -0
  97. {commkit-3.7.0 → commkit-3.7.1}/commkit/coding/product.py +0 -0
  98. {commkit-3.7.0 → commkit-3.7.1}/commkit/coding/ratematch.py +0 -0
  99. {commkit-3.7.0 → commkit-3.7.1}/commkit/coding/shaping.py +0 -0
  100. {commkit-3.7.0 → commkit-3.7.1}/commkit/coding/turbo.py +0 -0
  101. {commkit-3.7.0 → commkit-3.7.1}/commkit/core/__init__.py +0 -0
  102. {commkit-3.7.0 → commkit-3.7.1}/commkit/core/_signal_adapter.py +0 -0
  103. {commkit-3.7.0 → commkit-3.7.1}/commkit/core/frame.py +0 -0
  104. {commkit-3.7.0 → commkit-3.7.1}/commkit/core/generation.py +0 -0
  105. {commkit-3.7.0 → commkit-3.7.1}/commkit/core/preamble.py +0 -0
  106. {commkit-3.7.0 → commkit-3.7.1}/commkit/core/signal.py +0 -0
  107. {commkit-3.7.0 → commkit-3.7.1}/commkit/equalization/__init__.py +0 -0
  108. {commkit-3.7.0 → commkit-3.7.1}/commkit/equalization/_block/__init__.py +0 -0
  109. {commkit-3.7.0 → commkit-3.7.1}/commkit/equalization/_block/_blind.py +0 -0
  110. {commkit-3.7.0 → commkit-3.7.1}/commkit/equalization/_block/_dd.py +0 -0
  111. {commkit-3.7.0 → commkit-3.7.1}/commkit/equalization/_block/_engine.py +0 -0
  112. {commkit-3.7.0 → commkit-3.7.1}/commkit/equalization/_common.py +0 -0
  113. {commkit-3.7.0 → commkit-3.7.1}/commkit/equalization/_numba_loops.py +0 -0
  114. {commkit-3.7.0 → commkit-3.7.1}/commkit/equalization/_sequential/__init__.py +0 -0
  115. {commkit-3.7.0 → commkit-3.7.1}/commkit/equalization/_sequential/_blind.py +0 -0
  116. {commkit-3.7.0 → commkit-3.7.1}/commkit/equalization/_sequential/_dd.py +0 -0
  117. {commkit-3.7.0 → commkit-3.7.1}/commkit/equalization/_sequential/_setup.py +0 -0
  118. {commkit-3.7.0 → commkit-3.7.1}/commkit/equalization/linear.py +0 -0
  119. {commkit-3.7.0 → commkit-3.7.1}/commkit/equalization/pilots.py +0 -0
  120. {commkit-3.7.0 → commkit-3.7.1}/commkit/equalization/polarization.py +0 -0
  121. {commkit-3.7.0 → commkit-3.7.1}/commkit/equalization/result.py +0 -0
  122. {commkit-3.7.0 → commkit-3.7.1}/commkit/filtering/__init__.py +0 -0
  123. {commkit-3.7.0 → commkit-3.7.1}/commkit/filtering/fiber.py +0 -0
  124. {commkit-3.7.0 → commkit-3.7.1}/commkit/filtering/linear.py +0 -0
  125. {commkit-3.7.0 → commkit-3.7.1}/commkit/filtering/pulses.py +0 -0
  126. {commkit-3.7.0 → commkit-3.7.1}/commkit/filtering/response.py +0 -0
  127. {commkit-3.7.0 → commkit-3.7.1}/commkit/frequency.py +0 -0
  128. {commkit-3.7.0 → commkit-3.7.1}/commkit/impairments/__init__.py +0 -0
  129. {commkit-3.7.0 → commkit-3.7.1}/commkit/impairments/fiber_channel.py +0 -0
  130. {commkit-3.7.0 → commkit-3.7.1}/commkit/impairments/frontend.py +0 -0
  131. {commkit-3.7.0 → commkit-3.7.1}/commkit/impairments/noise.py +0 -0
  132. {commkit-3.7.0 → commkit-3.7.1}/commkit/impairments/source.py +0 -0
  133. {commkit-3.7.0 → commkit-3.7.1}/commkit/io.py +0 -0
  134. {commkit-3.7.0 → commkit-3.7.1}/commkit/logger.py +0 -0
  135. {commkit-3.7.0 → commkit-3.7.1}/commkit/mapping/__init__.py +0 -0
  136. {commkit-3.7.0 → commkit-3.7.1}/commkit/mapping/_numba_llr.py +0 -0
  137. {commkit-3.7.0 → commkit-3.7.1}/commkit/mapping/bits.py +0 -0
  138. {commkit-3.7.0 → commkit-3.7.1}/commkit/mapping/constellation.py +0 -0
  139. {commkit-3.7.0 → commkit-3.7.1}/commkit/mapping/gray.py +0 -0
  140. {commkit-3.7.0 → commkit-3.7.1}/commkit/mapping/llr.py +0 -0
  141. {commkit-3.7.0 → commkit-3.7.1}/commkit/mapping/shaping.py +0 -0
  142. {commkit-3.7.0 → commkit-3.7.1}/commkit/math.py +0 -0
  143. {commkit-3.7.0 → commkit-3.7.1}/commkit/metrics/__init__.py +0 -0
  144. {commkit-3.7.0 → commkit-3.7.1}/commkit/metrics/_common.py +0 -0
  145. {commkit-3.7.0 → commkit-3.7.1}/commkit/metrics/awgn.py +0 -0
  146. {commkit-3.7.0 → commkit-3.7.1}/commkit/metrics/error_rates.py +0 -0
  147. {commkit-3.7.0 → commkit-3.7.1}/commkit/metrics/information.py +0 -0
  148. {commkit-3.7.0 → commkit-3.7.1}/commkit/metrics/quality.py +0 -0
  149. {commkit-3.7.0 → commkit-3.7.1}/commkit/multirate.py +0 -0
  150. {commkit-3.7.0 → commkit-3.7.1}/commkit/plotting/__init__.py +0 -0
  151. {commkit-3.7.0 → commkit-3.7.1}/commkit/plotting/analysis.py +0 -0
  152. {commkit-3.7.0 → commkit-3.7.1}/commkit/plotting/coding.py +0 -0
  153. {commkit-3.7.0 → commkit-3.7.1}/commkit/plotting/constellation.py +0 -0
  154. {commkit-3.7.0 → commkit-3.7.1}/commkit/plotting/equalizer.py +0 -0
  155. {commkit-3.7.0 → commkit-3.7.1}/commkit/plotting/eye.py +0 -0
  156. {commkit-3.7.0 → commkit-3.7.1}/commkit/plotting/filter_response.py +0 -0
  157. {commkit-3.7.0 → commkit-3.7.1}/commkit/plotting/frequency.py +0 -0
  158. {commkit-3.7.0 → commkit-3.7.1}/commkit/plotting/recovery.py +0 -0
  159. {commkit-3.7.0 → commkit-3.7.1}/commkit/plotting/spectral.py +0 -0
  160. {commkit-3.7.0 → commkit-3.7.1}/commkit/plotting/theme.py +0 -0
  161. {commkit-3.7.0 → commkit-3.7.1}/commkit/plotting/timing.py +0 -0
  162. {commkit-3.7.0 → commkit-3.7.1}/commkit/plotting/waveform.py +0 -0
  163. {commkit-3.7.0 → commkit-3.7.1}/commkit/py.typed +0 -0
  164. {commkit-3.7.0 → commkit-3.7.1}/commkit/recovery/__init__.py +0 -0
  165. {commkit-3.7.0 → commkit-3.7.1}/commkit/recovery/_common.py +0 -0
  166. {commkit-3.7.0 → commkit-3.7.1}/commkit/recovery/bps.py +0 -0
  167. {commkit-3.7.0 → commkit-3.7.1}/commkit/recovery/carrier_phase.py +0 -0
  168. {commkit-3.7.0 → commkit-3.7.1}/commkit/recovery/corrections.py +0 -0
  169. {commkit-3.7.0 → commkit-3.7.1}/commkit/recovery/pilots.py +0 -0
  170. {commkit-3.7.0 → commkit-3.7.1}/commkit/recovery/pll.py +0 -0
  171. {commkit-3.7.0 → commkit-3.7.1}/commkit/recovery/tikhonov.py +0 -0
  172. {commkit-3.7.0 → commkit-3.7.1}/commkit/recovery/viterbi_viterbi.py +0 -0
  173. {commkit-3.7.0 → commkit-3.7.1}/commkit/smoothing.py +0 -0
  174. {commkit-3.7.0 → commkit-3.7.1}/commkit/spectral.py +0 -0
  175. {commkit-3.7.0 → commkit-3.7.1}/commkit/timing/__init__.py +0 -0
  176. {commkit-3.7.0 → commkit-3.7.1}/commkit/timing/synchronization.py +0 -0
  177. {commkit-3.7.0 → commkit-3.7.1}/examples/00-getting-started.ipynb +0 -0
  178. {commkit-3.7.0 → commkit-3.7.1}/examples/01-constellations-and-bit-mapping.ipynb +0 -0
  179. {commkit-3.7.0 → commkit-3.7.1}/examples/02-spectral-analysis.ipynb +0 -0
  180. {commkit-3.7.0 → commkit-3.7.1}/examples/03-digital-filters.ipynb +0 -0
  181. {commkit-3.7.0 → commkit-3.7.1}/examples/04-pulse-shaping-and-matched-filtering.ipynb +0 -0
  182. {commkit-3.7.0 → commkit-3.7.1}/examples/05-multirate-and-resampling.ipynb +0 -0
  183. {commkit-3.7.0 → commkit-3.7.1}/examples/06-awgn-and-performance-metrics.ipynb +0 -0
  184. {commkit-3.7.0 → commkit-3.7.1}/examples/07-channel-coding.ipynb +0 -0
  185. {commkit-3.7.0 → commkit-3.7.1}/examples/08-probabilistic-shaping-and-coded-modulation.ipynb +0 -0
  186. {commkit-3.7.0 → commkit-3.7.1}/examples/09-laser-phase-noise.ipynb +0 -0
  187. {commkit-3.7.0 → commkit-3.7.1}/examples/11-frame-synchronization-and-timing.ipynb +0 -0
  188. {commkit-3.7.0 → commkit-3.7.1}/examples/13-frequency-offset-estimation.ipynb +0 -0
  189. {commkit-3.7.0 → commkit-3.7.1}/examples/14-blind-carrier-phase-recovery.ipynb +0 -0
  190. {commkit-3.7.0 → commkit-3.7.1}/examples/15-pilot-aided-carrier-recovery.ipynb +0 -0
  191. {commkit-3.7.0 → commkit-3.7.1}/examples/16-linear-channel-equalization.ipynb +0 -0
  192. {commkit-3.7.0 → commkit-3.7.1}/examples/17-adaptive-equalization.ipynb +0 -0
  193. {commkit-3.7.0 → commkit-3.7.1}/examples/18-blind-equalization.ipynb +0 -0
  194. {commkit-3.7.0 → commkit-3.7.1}/examples/19-polarization-multiplexing.ipynb +0 -0
  195. {commkit-3.7.0 → commkit-3.7.1}/examples/20-chromatic-dispersion.ipynb +0 -0
  196. {commkit-3.7.0 → commkit-3.7.1}/examples/21-nonlinear-fiber-and-backpropagation.ipynb +0 -0
  197. {commkit-3.7.0 → commkit-3.7.1}/examples/23-fec-in-optical-standards.ipynb +0 -0
  198. {commkit-3.7.0 → commkit-3.7.1}/examples/24-coherent-transceiver-end-to-end.ipynb +0 -0
  199. {commkit-3.7.0 → commkit-3.7.1}/examples/25-carrier-phase-characterization.ipynb +0 -0
  200. {commkit-3.7.0 → commkit-3.7.1}/examples/26-laser-linewidth-self-heterodyne.ipynb +0 -0
  201. {commkit-3.7.0 → commkit-3.7.1}/examples/27-laser-linewidth-short-delay-and-flicker.ipynb +0 -0
  202. {commkit-3.7.0 → commkit-3.7.1}/examples/28-laser-linewidth-self-homodyne-iq.ipynb +0 -0
  203. {commkit-3.7.0 → commkit-3.7.1}/examples/29-measurement-template-self-heterodyne.ipynb +0 -0
  204. {commkit-3.7.0 → commkit-3.7.1}/examples/30-measurement-template-self-homodyne-iq.ipynb +0 -0
  205. {commkit-3.7.0 → commkit-3.7.1}/scripts/sanitize.sh +0 -0
  206. {commkit-3.7.0 → commkit-3.7.1}/tests/analysis/test_allan.py +0 -0
  207. {commkit-3.7.0 → commkit-3.7.1}/tests/analysis/test_drift.py +0 -0
  208. {commkit-3.7.0 → commkit-3.7.1}/tests/analysis/test_fm_noise.py +0 -0
  209. {commkit-3.7.0 → commkit-3.7.1}/tests/analysis/test_interferometry.py +0 -0
  210. {commkit-3.7.0 → commkit-3.7.1}/tests/analysis/test_linewidth.py +0 -0
  211. {commkit-3.7.0 → commkit-3.7.1}/tests/analysis/test_trajectory.py +0 -0
  212. {commkit-3.7.0 → commkit-3.7.1}/tests/coding/test_algebraic.py +0 -0
  213. {commkit-3.7.0 → commkit-3.7.1}/tests/coding/test_coding_base.py +0 -0
  214. {commkit-3.7.0 → commkit-3.7.1}/tests/coding/test_convolutional.py +0 -0
  215. {commkit-3.7.0 → commkit-3.7.1}/tests/coding/test_crc.py +0 -0
  216. {commkit-3.7.0 → commkit-3.7.1}/tests/coding/test_exit.py +0 -0
  217. {commkit-3.7.0 → commkit-3.7.1}/tests/coding/test_gf2.py +0 -0
  218. {commkit-3.7.0 → commkit-3.7.1}/tests/coding/test_interleaving.py +0 -0
  219. {commkit-3.7.0 → commkit-3.7.1}/tests/coding/test_ldpc.py +0 -0
  220. {commkit-3.7.0 → commkit-3.7.1}/tests/coding/test_ldpc_exit.py +0 -0
  221. {commkit-3.7.0 → commkit-3.7.1}/tests/coding/test_ldpc_kernel.py +0 -0
  222. {commkit-3.7.0 → commkit-3.7.1}/tests/coding/test_ldpc_tables.py +0 -0
  223. {commkit-3.7.0 → commkit-3.7.1}/tests/coding/test_linear_codes.py +0 -0
  224. {commkit-3.7.0 → commkit-3.7.1}/tests/coding/test_performance.py +0 -0
  225. {commkit-3.7.0 → commkit-3.7.1}/tests/coding/test_polar.py +0 -0
  226. {commkit-3.7.0 → commkit-3.7.1}/tests/coding/test_product.py +0 -0
  227. {commkit-3.7.0 → commkit-3.7.1}/tests/coding/test_ratematch.py +0 -0
  228. {commkit-3.7.0 → commkit-3.7.1}/tests/coding/test_shaping.py +0 -0
  229. {commkit-3.7.0 → commkit-3.7.1}/tests/coding/test_turbo.py +0 -0
  230. {commkit-3.7.0 → commkit-3.7.1}/tests/common/__init__.py +0 -0
  231. {commkit-3.7.0 → commkit-3.7.1}/tests/common/coding.py +0 -0
  232. {commkit-3.7.0 → commkit-3.7.1}/tests/common/conversions.py +0 -0
  233. {commkit-3.7.0 → commkit-3.7.1}/tests/common/kernel_utils.py +0 -0
  234. {commkit-3.7.0 → commkit-3.7.1}/tests/common/memory.py +0 -0
  235. {commkit-3.7.0 → commkit-3.7.1}/tests/common/metrics.py +0 -0
  236. {commkit-3.7.0 → commkit-3.7.1}/tests/common/periodic.py +0 -0
  237. {commkit-3.7.0 → commkit-3.7.1}/tests/common/scoring.py +0 -0
  238. {commkit-3.7.0 → commkit-3.7.1}/tests/common/signals.py +0 -0
  239. {commkit-3.7.0 → commkit-3.7.1}/tests/common/theory.py +0 -0
  240. {commkit-3.7.0 → commkit-3.7.1}/tests/conftest.py +0 -0
  241. {commkit-3.7.0 → commkit-3.7.1}/tests/core/test_frame.py +0 -0
  242. {commkit-3.7.0 → commkit-3.7.1}/tests/core/test_generation.py +0 -0
  243. {commkit-3.7.0 → commkit-3.7.1}/tests/core/test_preamble.py +0 -0
  244. {commkit-3.7.0 → commkit-3.7.1}/tests/core/test_psqam.py +0 -0
  245. {commkit-3.7.0 → commkit-3.7.1}/tests/core/test_signal.py +0 -0
  246. {commkit-3.7.0 → commkit-3.7.1}/tests/core/test_signal_adapter.py +0 -0
  247. {commkit-3.7.0 → commkit-3.7.1}/tests/core/test_signal_mimo.py +0 -0
  248. {commkit-3.7.0 → commkit-3.7.1}/tests/equalization/test_blind.py +0 -0
  249. {commkit-3.7.0 → commkit-3.7.1}/tests/equalization/test_block.py +0 -0
  250. {commkit-3.7.0 → commkit-3.7.1}/tests/equalization/test_bps_kernel.py +0 -0
  251. {commkit-3.7.0 → commkit-3.7.1}/tests/equalization/test_cold_start.py +0 -0
  252. {commkit-3.7.0 → commkit-3.7.1}/tests/equalization/test_cpr.py +0 -0
  253. {commkit-3.7.0 → commkit-3.7.1}/tests/equalization/test_cs_kernel.py +0 -0
  254. {commkit-3.7.0 → commkit-3.7.1}/tests/equalization/test_equalizer_oracles.py +0 -0
  255. {commkit-3.7.0 → commkit-3.7.1}/tests/equalization/test_linear.py +0 -0
  256. {commkit-3.7.0 → commkit-3.7.1}/tests/equalization/test_mimo.py +0 -0
  257. {commkit-3.7.0 → commkit-3.7.1}/tests/equalization/test_polarization.py +0 -0
  258. {commkit-3.7.0 → commkit-3.7.1}/tests/equalization/test_result.py +0 -0
  259. {commkit-3.7.0 → commkit-3.7.1}/tests/equalization/test_sequential.py +0 -0
  260. {commkit-3.7.0 → commkit-3.7.1}/tests/equalization/test_state.py +0 -0
  261. {commkit-3.7.0 → commkit-3.7.1}/tests/equalization/test_winit.py +0 -0
  262. {commkit-3.7.0 → commkit-3.7.1}/tests/impairments/test_fiber_channel_linear.py +0 -0
  263. {commkit-3.7.0 → commkit-3.7.1}/tests/impairments/test_fiber_channel_nonlinear.py +0 -0
  264. {commkit-3.7.0 → commkit-3.7.1}/tests/impairments/test_frontend.py +0 -0
  265. {commkit-3.7.0 → commkit-3.7.1}/tests/impairments/test_noise.py +0 -0
  266. {commkit-3.7.0 → commkit-3.7.1}/tests/impairments/test_source.py +0 -0
  267. {commkit-3.7.0 → commkit-3.7.1}/tests/mapping/test_bits.py +0 -0
  268. {commkit-3.7.0 → commkit-3.7.1}/tests/mapping/test_constellation.py +0 -0
  269. {commkit-3.7.0 → commkit-3.7.1}/tests/mapping/test_gray.py +0 -0
  270. {commkit-3.7.0 → commkit-3.7.1}/tests/mapping/test_llr.py +0 -0
  271. {commkit-3.7.0 → commkit-3.7.1}/tests/metrics/test_awgn.py +0 -0
  272. {commkit-3.7.0 → commkit-3.7.1}/tests/metrics/test_common.py +0 -0
  273. {commkit-3.7.0 → commkit-3.7.1}/tests/metrics/test_error_rates.py +0 -0
  274. {commkit-3.7.0 → commkit-3.7.1}/tests/metrics/test_information.py +0 -0
  275. {commkit-3.7.0 → commkit-3.7.1}/tests/metrics/test_quality.py +0 -0
  276. {commkit-3.7.0 → commkit-3.7.1}/tests/plotting/test_plot_analysis.py +0 -0
  277. {commkit-3.7.0 → commkit-3.7.1}/tests/plotting/test_plot_coding.py +0 -0
  278. {commkit-3.7.0 → commkit-3.7.1}/tests/plotting/test_plot_constellation.py +0 -0
  279. {commkit-3.7.0 → commkit-3.7.1}/tests/plotting/test_plot_equalizer.py +0 -0
  280. {commkit-3.7.0 → commkit-3.7.1}/tests/plotting/test_plot_eye.py +0 -0
  281. {commkit-3.7.0 → commkit-3.7.1}/tests/plotting/test_plot_filter_response.py +0 -0
  282. {commkit-3.7.0 → commkit-3.7.1}/tests/plotting/test_plot_frequency.py +0 -0
  283. {commkit-3.7.0 → commkit-3.7.1}/tests/plotting/test_plot_recovery.py +0 -0
  284. {commkit-3.7.0 → commkit-3.7.1}/tests/plotting/test_plot_spectral.py +0 -0
  285. {commkit-3.7.0 → commkit-3.7.1}/tests/plotting/test_plot_theme.py +0 -0
  286. {commkit-3.7.0 → commkit-3.7.1}/tests/plotting/test_plot_timing.py +0 -0
  287. {commkit-3.7.0 → commkit-3.7.1}/tests/plotting/test_plot_waveform.py +0 -0
  288. {commkit-3.7.0 → commkit-3.7.1}/tests/plugins/__init__.py +0 -0
  289. {commkit-3.7.0 → commkit-3.7.1}/tests/plugins/precision_trace.py +0 -0
  290. {commkit-3.7.0 → commkit-3.7.1}/tests/plugins/transfer_trace.py +0 -0
  291. {commkit-3.7.0 → commkit-3.7.1}/tests/recovery/test_bps.py +0 -0
  292. {commkit-3.7.0 → commkit-3.7.1}/tests/recovery/test_corrections.py +0 -0
  293. {commkit-3.7.0 → commkit-3.7.1}/tests/recovery/test_joint.py +0 -0
  294. {commkit-3.7.0 → commkit-3.7.1}/tests/recovery/test_pilots.py +0 -0
  295. {commkit-3.7.0 → commkit-3.7.1}/tests/recovery/test_pll.py +0 -0
  296. {commkit-3.7.0 → commkit-3.7.1}/tests/recovery/test_recovery_oracles.py +0 -0
  297. {commkit-3.7.0 → commkit-3.7.1}/tests/recovery/test_tikhonov.py +0 -0
  298. {commkit-3.7.0 → commkit-3.7.1}/tests/recovery/test_viterbi_viterbi.py +0 -0
  299. {commkit-3.7.0 → commkit-3.7.1}/tests/test_api_contracts.py +0 -0
  300. {commkit-3.7.0 → commkit-3.7.1}/tests/test_array.py +0 -0
  301. {commkit-3.7.0 → commkit-3.7.1}/tests/test_backend.py +0 -0
  302. {commkit-3.7.0 → commkit-3.7.1}/tests/test_cuda_infra.py +0 -0
  303. {commkit-3.7.0 → commkit-3.7.1}/tests/test_dispersion.py +0 -0
  304. {commkit-3.7.0 → commkit-3.7.1}/tests/test_examples.py +0 -0
  305. {commkit-3.7.0 → commkit-3.7.1}/tests/test_fiber.py +0 -0
  306. {commkit-3.7.0 → commkit-3.7.1}/tests/test_filter_response.py +0 -0
  307. {commkit-3.7.0 → commkit-3.7.1}/tests/test_filtering.py +0 -0
  308. {commkit-3.7.0 → commkit-3.7.1}/tests/test_frequency.py +0 -0
  309. {commkit-3.7.0 → commkit-3.7.1}/tests/test_import_hygiene.py +0 -0
  310. {commkit-3.7.0 → commkit-3.7.1}/tests/test_io.py +0 -0
  311. {commkit-3.7.0 → commkit-3.7.1}/tests/test_logger.py +0 -0
  312. {commkit-3.7.0 → commkit-3.7.1}/tests/test_math.py +0 -0
  313. {commkit-3.7.0 → commkit-3.7.1}/tests/test_multirate.py +0 -0
  314. {commkit-3.7.0 → commkit-3.7.1}/tests/test_pulse_shaping.py +0 -0
  315. {commkit-3.7.0 → commkit-3.7.1}/tests/test_receiver_chain.py +0 -0
  316. {commkit-3.7.0 → commkit-3.7.1}/tests/test_signal_pipeline.py +0 -0
  317. {commkit-3.7.0 → commkit-3.7.1}/tests/test_smoothing.py +0 -0
  318. {commkit-3.7.0 → commkit-3.7.1}/tests/test_spectral.py +0 -0
  319. {commkit-3.7.0 → commkit-3.7.1}/tests/test_working_memory.py +0 -0
  320. {commkit-3.7.0 → commkit-3.7.1}/tests/timing/test_synchronization.py +0 -0
@@ -4,6 +4,27 @@ Notable changes to CommKit, newest first. Performance figures are from the refer
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [3.7.1] - 2026-11-10
8
+
9
+ Gardner clock recovery no longer runs away when its loop is too wide: the integrator is bounded to plausible clock offsets, and slips are reported. The notebooks on clock recovery, direct and coherent detection and the ADC now introduce their topics with figures.
10
+
11
+ ### Added
12
+
13
+ - `timing.Gardner(max_offset_ppm=1000.0)`: the largest clock offset the loop tracks. The loop's integrator is held within it.
14
+ - A WARNING when Gardner clock recovery slips whole symbols (the timing moves by more than 0.75 symbol within `8 / (B·T)` symbols), after which output symbols no longer match the transmitted ones in order. A second WARNING when the estimated offset sits at `max_offset_ppm`.
15
+
16
+ ### Performance
17
+
18
+ - Gardner clock recovery checks its trajectory for slips after the loop: 1.01 to 1.04 of the former time (paired runs, 2^17 symbols).
19
+
20
+ ### Fixed
21
+
22
+ - A Gardner loop too wide for its detector's noise, such as the classical detector at `normalized_bandwidth=1e-2` on a 0.1-rolloff signal, no longer runs away in frequency without notice. Once it slipped, nothing pulled its integrator back, and it reached tens of thousands of ppm on a 50 ppm clock, with thousands of extra symbols in the corrected output. The bounded loop keeps returning to lock and reports the slips.
23
+
24
+ ### Development
25
+
26
+ - Notebooks 12 (sampling-clock recovery), 22 (direct and self-coherent detection, now also coherent detection and its LO gain) and the skew and ADC sections of 10 explain their topics with figures: the drifting eye, how the Gardner detector tells early from late, loop bandwidth and cycle slips, PAM-4 eyes and sensitivity, thermal against shot noise, laser RIN, balanced photodiodes, quantization and jitter.
27
+
7
28
  ## [3.7.0] - 2026-11-10
8
29
 
9
30
  The receiver's analog front end and the detector, at absolute power. The ADC clock, its jitter and quantizer, IQ skew and the photodiode join the impairments, with Gardner clock recovery as the inverse of the clock; coherent and square-law detection turn received and LO powers into the noise they give. Closed-form AWGN error rates and information rates sit next to the measured ones. Blind decision-directed equalizers no longer lock falsely on unipolar PAM and dense QAM, because their input scaling hands the slicer the right gain.
@@ -503,7 +524,8 @@ See the [GitHub release](https://github.com/lokgar/commkit/releases/tag/v1.1.0)
503
524
 
504
525
  First release. See the [GitHub release](https://github.com/lokgar/commkit/releases/tag/v1.0.0).
505
526
 
506
- [Unreleased]: https://github.com/lokgar/commkit/compare/v3.7.0...HEAD
527
+ [Unreleased]: https://github.com/lokgar/commkit/compare/v3.7.1...HEAD
528
+ [3.7.1]: https://github.com/lokgar/commkit/compare/v3.7.0...v3.7.1
507
529
  [3.7.0]: https://github.com/lokgar/commkit/compare/v3.6.1...v3.7.0
508
530
  [3.6.1]: https://github.com/lokgar/commkit/compare/v3.6.0...v3.6.1
509
531
  [3.6.0]: https://github.com/lokgar/commkit/compare/v3.5.0...v3.6.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: commkit
3
- Version: 3.7.0
3
+ Version: 3.7.1
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
@@ -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.7.0"
34
+ __version__ = "3.7.1"
35
35
 
36
36
  from .backend import PrecisionWarning
37
37
  from .core import (
@@ -111,9 +111,10 @@ def _clamp_step(step, sps):
111
111
 
112
112
 
113
113
  @_step
114
- def _track(x, c0, c1, pad, sps, p0, mu, beta, power, n_valid, pos, err):
114
+ def _track(x, c0, c1, pad, sps, p0, mu, beta, nu_max, power, n_valid, pos, err):
115
115
  """One loop over channels ``c0 .. c1 - 1`` (their mean error drives it);
116
- fills ``pos`` and ``err`` and returns the number of strobes."""
116
+ fills ``pos`` and ``err`` and returns the number of strobes. The
117
+ integrator stays within ``±nu_max``."""
117
118
  nu = 0.0
118
119
  e_last = 0.0
119
120
  p = p0
@@ -131,14 +132,14 @@ def _track(x, c0, c1, pad, sps, p0, mu, beta, power, n_valid, pos, err):
131
132
  err[k] = e
132
133
  k += 1
133
134
  step = _clamp_step(sps * (1.0 + nu + mu * e_last), sps)
134
- nu += beta * e_last
135
+ nu = min(max(nu + beta * e_last, -nu_max), nu_max)
135
136
  e_last = e
136
137
  p += step
137
138
  return k
138
139
 
139
140
 
140
141
  @numba.njit(cache=True, fastmath=True, nogil=True, parallel=True)
141
- def gardner_loop(x, pad, sps, p0, mu, beta, power, joint, n_valid, num_out):
142
+ def gardner_loop(x, pad, sps, p0, mu, beta, nu_max, power, joint, n_valid, num_out):
142
143
  """Track the symbol strobes of every channel (or one joint loop).
143
144
 
144
145
  Parameters
@@ -155,6 +156,9 @@ def gardner_loop(x, pad, sps, p0, mu, beta, power, joint, n_valid, num_out):
155
156
  mu, beta : numpy.ndarray
156
157
  Proportional and integral gains per row, float64: the strobe spacing
157
158
  is ``sps · (1 + ν + μ·e)`` and ``ν += β·e``.
159
+ nu_max : float
160
+ Bound of the integrator ``|ν|``, the largest clock offset tracked
161
+ (fraction, not ppm).
158
162
  power, joint : bool
159
163
  Power detector; one loop driven by the mean error of all channels.
160
164
  n_valid : int
@@ -180,6 +184,18 @@ def gardner_loop(x, pad, sps, p0, mu, beta, power, joint, n_valid, num_out):
180
184
  c0 = 0 if joint else r
181
185
  c1 = C if joint else r + 1
182
186
  count[r] = _track(
183
- x, c0, c1, pad, sps, p0[r], mu[r], beta[r], power, n_valid, pos[r], err[r]
187
+ x,
188
+ c0,
189
+ c1,
190
+ pad,
191
+ sps,
192
+ p0[r],
193
+ mu[r],
194
+ beta[r],
195
+ nu_max,
196
+ power,
197
+ n_valid,
198
+ pos[r],
199
+ err[r],
184
200
  )
185
201
  return pos, err, count
@@ -40,6 +40,12 @@ _CAL_SYMBOLS = 2048
40
40
  # Below this first-harmonic amplitude of the S-curve (unit-power input) the
41
41
  # detector sees no clock tone.
42
42
  _MIN_CURVE_AMPLITUDE = 1e-3
43
+ # Cycle-slip check: the timing may not move by more than _SLIP_THRESHOLD
44
+ # symbol periods, beyond the record's mean drift, within _SLIP_WINDOW loop
45
+ # time constants 1/(B·T). The loop's own jitter stays below 0.5 symbol over
46
+ # that window up to the bandwidths where slips begin; a slip moves it by 1.
47
+ _SLIP_WINDOW = 8.0
48
+ _SLIP_THRESHOLD = 0.75
43
49
 
44
50
 
45
51
  def _get_numba_gardner() -> Any:
@@ -74,8 +80,10 @@ class Gardner:
74
80
  ``β = 4(BT)² / K_d``, with the detector gain ``K_d`` measured on the
75
81
  input (see Notes). Used when ``proportional_gain`` is not given.
76
82
  The detector's self-noise sets the timing jitter: 0.01 symbol rms at
77
- ``1e-3`` for 16-QAM, 0.001 at ``1e-4``; at ``1e-2`` a 0.1-rolloff
78
- signal slips whole symbols.
83
+ ``1e-3`` for 16-QAM (classical detector, rolloff 0.1), 0.006 at
84
+ ``1e-4``. Too wide a loop slips whole symbols: the classical
85
+ detector at a rolloff of 0.1 from ``1e-2`` on, the power detector
86
+ at 0.1 from ``2e-2`` and at 0.5 from ``3e-3`` (16-QAM, 20 dB).
79
87
  proportional_gain : float, optional
80
88
  Raw proportional gain ``μ`` on the detector output of the input
81
89
  scaled to unit power; overrides ``normalized_bandwidth``.
@@ -83,6 +91,12 @@ class Gardner:
83
91
  Raw integral gain ``β``; requires ``proportional_gain``. Defaults
84
92
  to 0 (a first-order loop, which keeps a timing lag ``ε / μK_d``
85
93
  under a clock offset) with ``proportional_gain``.
94
+ max_offset_ppm : float, default 1000.0
95
+ Largest clock offset the loop tracks, in ppm, in ``(0, 90000)``:
96
+ the integrator ``ν`` is held within ``±max_offset_ppm · 1e-6``.
97
+ Crystal clocks are within about 100 ppm. The bound keeps a loop
98
+ that has lost lock from wandering off in frequency, which it
99
+ otherwise does without limit (see Notes).
86
100
  joint_channels : bool, default False
87
101
  MIMO: one loop driven by the mean detector output of all channels,
88
102
  for channels sampled by one clock.
@@ -109,6 +123,19 @@ class Gardner:
109
123
  pulse) while the power detector's stays at 0.34. The strobe spacing is
110
124
  held within ±10 % of ``sps``.
111
125
 
126
+ A loop that slips leaves the S-curve's stable point, and while its
127
+ timing runs through the symbols the mean detector output is zero:
128
+ nothing pulls the integrator back, and its noise walks it away.
129
+ Unbounded, a classical loop at ``B·T = 1e-2`` on a 0.1-rolloff signal
130
+ reached tens of thousands of ppm on a 50 ppm clock; held within
131
+ ``max_offset_ppm`` it keeps returning to lock. Two checks log a WARNING after the loop: the
132
+ timing moving by more than 0.75 symbol within ``8 / (B·T)`` symbols,
133
+ beyond the record's mean drift (a cycle slip, after which symbol ``k``
134
+ of the output is no longer the ``k``-th transmitted one), and an
135
+ estimated offset within 5 % of ``max_offset_ppm`` (a larger offset,
136
+ which the proportional path follows only with a static timing lag, or
137
+ a loop that lost lock). ``B·T`` is ``μ K_d / 4`` for raw gains.
138
+
112
139
  Numba-compiled on the CPU; GPU input makes one host round trip.
113
140
 
114
141
  References
@@ -120,6 +147,7 @@ class Gardner:
120
147
  normalized_bandwidth: float = 1e-3
121
148
  proportional_gain: float | None = None
122
149
  integral_gain: float | None = None
150
+ max_offset_ppm: float = 1000.0
123
151
  joint_channels: bool = False
124
152
  method: Literal["classical", "power"] = "classical"
125
153
 
@@ -130,6 +158,11 @@ class Gardner:
130
158
  f"Gardner: normalized_bandwidth must be in (0, 0.5) (a fraction "
131
159
  f"of the symbol rate), got {bt}."
132
160
  )
161
+ if not (0.0 < self.max_offset_ppm < 9e4):
162
+ raise ValueError(
163
+ "Gardner: max_offset_ppm must be in (0, 90000), got "
164
+ f"{self.max_offset_ppm}."
165
+ )
133
166
  if self.integral_gain is not None and self.proportional_gain is None:
134
167
  raise ValueError("Gardner: integral_gain requires proportional_gain.")
135
168
  for name in ("proportional_gain", "integral_gain"):
@@ -224,6 +257,7 @@ def _track(
224
257
  pad + tau0 * sps,
225
258
  np.ascontiguousarray(gains[:, 0]),
226
259
  np.ascontiguousarray(gains[:, 1]),
260
+ how.max_offset_ppm * 1e-6,
227
261
  power,
228
262
  rows != C,
229
263
  n,
@@ -232,6 +266,7 @@ def _track(
232
266
  k = int(count.min())
233
267
  pos = pos[:, :k] - pad
234
268
  err = err[:, :k]
269
+ _check_lock(pos / sps - np.arange(k), gains[:, 0] * kd / 4.0, how)
235
270
  if rows != C:
236
271
  pos = np.repeat(pos, C, axis=0)
237
272
  err = np.repeat(err, C, axis=0)
@@ -239,6 +274,47 @@ def _track(
239
274
  return pos, kd, err
240
275
 
241
276
 
277
+ def _first_slip(trajectory: np.ndarray, bandwidth: np.ndarray) -> np.ndarray:
278
+ """Per row, the first symbol where the timing moves by more than
279
+ _SLIP_THRESHOLD within _SLIP_WINDOW / bandwidth symbols beyond the
280
+ row's median drift over that window; -1 where it never does."""
281
+ k = trajectory.shape[-1]
282
+ first = np.full(trajectory.shape[0], -1, dtype=np.int64)
283
+ for r, bt in enumerate(bandwidth):
284
+ if not bt > 0.0:
285
+ continue
286
+ span = int(max(64.0, _SLIP_WINDOW / bt))
287
+ if span >= k:
288
+ continue
289
+ moved = trajectory[r, span:] - trajectory[r, :-span]
290
+ bad = np.abs(moved - np.median(moved)) > _SLIP_THRESHOLD
291
+ if bad.any():
292
+ first[r] = int(np.argmax(bad))
293
+ return first
294
+
295
+
296
+ def _check_lock(trajectory: np.ndarray, bandwidth: np.ndarray, how: Gardner) -> None:
297
+ """Log a WARNING for cycle slips and for an offset at the loop's bound."""
298
+ first = _first_slip(trajectory, bandwidth)
299
+ if np.any(first >= 0):
300
+ logger.warning(
301
+ "Gardner clock recovery slipped whole symbols (first near symbol "
302
+ "%d): after a slip, output symbols no longer match the transmitted "
303
+ "ones in order. Use a smaller normalized_bandwidth (B·T = %s).",
304
+ int(first[first >= 0].min()),
305
+ ", ".join(f"{bt:.1e}" for bt in np.unique(bandwidth)),
306
+ )
307
+ ppm = _offset_ppm(trajectory)
308
+ if np.any(np.abs(ppm) > 0.95 * how.max_offset_ppm):
309
+ logger.warning(
310
+ "Gardner clock recovery: the offset %s ppm is at the loop's bound "
311
+ "max_offset_ppm=%g: the loop lost lock, or the clock is further off and "
312
+ "the timing lags; raise max_offset_ppm.",
313
+ np.array2string(ppm, precision=1),
314
+ how.max_offset_ppm,
315
+ )
316
+
317
+
242
318
  def _offset_ppm(trajectory: np.ndarray) -> np.ndarray:
243
319
  """Least-squares slope of the trajectory over its last three quarters,
244
320
  in ppm."""