commkit 3.1.1__tar.gz → 3.1.2__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 (284) hide show
  1. {commkit-3.1.1 → commkit-3.1.2}/CHANGELOG.md +14 -0
  2. {commkit-3.1.1 → commkit-3.1.2}/PKG-INFO +1 -1
  3. {commkit-3.1.1 → commkit-3.1.2}/commkit/__init__.py +1 -1
  4. commkit-3.1.2/commkit/impairments/noise.py +229 -0
  5. {commkit-3.1.1 → commkit-3.1.2}/pyproject.toml +2 -2
  6. {commkit-3.1.1 → commkit-3.1.2}/tests/impairments/test_noise.py +85 -1
  7. {commkit-3.1.1 → commkit-3.1.2}/uv.lock +1 -1
  8. commkit-3.1.1/commkit/impairments/noise.py +0 -184
  9. {commkit-3.1.1 → commkit-3.1.2}/.gitattributes +0 -0
  10. {commkit-3.1.1 → commkit-3.1.2}/.github/workflows/ci.yml +0 -0
  11. {commkit-3.1.1 → commkit-3.1.2}/.github/workflows/publish.yml +0 -0
  12. {commkit-3.1.1 → commkit-3.1.2}/.gitignore +0 -0
  13. {commkit-3.1.1 → commkit-3.1.2}/.python-version +0 -0
  14. {commkit-3.1.1 → commkit-3.1.2}/AGENTS.md +0 -0
  15. {commkit-3.1.1 → commkit-3.1.2}/LICENSE +0 -0
  16. {commkit-3.1.1 → commkit-3.1.2}/README.md +0 -0
  17. {commkit-3.1.1 → commkit-3.1.2}/benchmarks/baselines/Linux-CPython-3.14-64bit/0002_pre_v2.json +0 -0
  18. {commkit-3.1.1 → commkit-3.1.2}/benchmarks/baselines/Linux-CPython-3.14-64bit/0003_v2_0.json +0 -0
  19. {commkit-3.1.1 → commkit-3.1.2}/benchmarks/baselines/Linux-CPython-3.14-64bit/0004_fiber_dbp.json +0 -0
  20. {commkit-3.1.1 → commkit-3.1.2}/benchmarks/baselines/Linux-CPython-3.14-64bit/0005_gpu_decoder_path.json +0 -0
  21. {commkit-3.1.1 → commkit-3.1.2}/benchmarks/baselines/Linux-CPython-3.14-64bit/0006_perf_cleanup.json +0 -0
  22. {commkit-3.1.1 → commkit-3.1.2}/benchmarks/baselines/Linux-CPython-3.14-64bit/0007_v3_0.json +0 -0
  23. {commkit-3.1.1 → commkit-3.1.2}/benchmarks/baselines/Linux-CPython-3.14-64bit/0008_v3_1.json +0 -0
  24. {commkit-3.1.1 → commkit-3.1.2}/benchmarks/bench_block_blind.py +0 -0
  25. {commkit-3.1.1 → commkit-3.1.2}/benchmarks/bench_block_lms.py +0 -0
  26. {commkit-3.1.1 → commkit-3.1.2}/benchmarks/bench_bps.py +0 -0
  27. {commkit-3.1.1 → commkit-3.1.2}/benchmarks/bench_coding.py +0 -0
  28. {commkit-3.1.1 → commkit-3.1.2}/benchmarks/bench_equalizers.py +0 -0
  29. {commkit-3.1.1 → commkit-3.1.2}/benchmarks/bench_fiber.py +0 -0
  30. {commkit-3.1.1 → commkit-3.1.2}/benchmarks/bench_filters.py +0 -0
  31. {commkit-3.1.1 → commkit-3.1.2}/benchmarks/bench_import.py +0 -0
  32. {commkit-3.1.1 → commkit-3.1.2}/benchmarks/bench_llr.py +0 -0
  33. {commkit-3.1.1 → commkit-3.1.2}/benchmarks/bench_mapping.py +0 -0
  34. {commkit-3.1.1 → commkit-3.1.2}/benchmarks/bench_signal_pipeline.py +0 -0
  35. {commkit-3.1.1 → commkit-3.1.2}/benchmarks/bench_sync_misc.py +0 -0
  36. {commkit-3.1.1 → commkit-3.1.2}/benchmarks/benchutils.py +0 -0
  37. {commkit-3.1.1 → commkit-3.1.2}/benchmarks/conftest.py +0 -0
  38. {commkit-3.1.1 → commkit-3.1.2}/benchmarks/record_baseline.py +0 -0
  39. {commkit-3.1.1 → commkit-3.1.2}/benchmarks/workloads.py +0 -0
  40. {commkit-3.1.1 → commkit-3.1.2}/commkit/_array.py +0 -0
  41. {commkit-3.1.1 → commkit-3.1.2}/commkit/_circular.py +0 -0
  42. {commkit-3.1.1 → commkit-3.1.2}/commkit/_constants.py +0 -0
  43. {commkit-3.1.1 → commkit-3.1.2}/commkit/_cuda/__init__.py +0 -0
  44. {commkit-3.1.1 → commkit-3.1.2}/commkit/_cuda/compiler.py +0 -0
  45. {commkit-3.1.1 → commkit-3.1.2}/commkit/_cuda/graphs.py +0 -0
  46. {commkit-3.1.1 → commkit-3.1.2}/commkit/_cuda/src/algebraic.cu +0 -0
  47. {commkit-3.1.1 → commkit-3.1.2}/commkit/_cuda/src/bps_anchor.cu +0 -0
  48. {commkit-3.1.1 → commkit-3.1.2}/commkit/_cuda/src/bps_min_d2.cu +0 -0
  49. {commkit-3.1.1 → commkit-3.1.2}/commkit/_cuda/src/cs_block.cu +0 -0
  50. {commkit-3.1.1 → commkit-3.1.2}/commkit/_cuda/src/iir.cu +0 -0
  51. {commkit-3.1.1 → commkit-3.1.2}/commkit/_cuda/src/ldpc.cu +0 -0
  52. {commkit-3.1.1 → commkit-3.1.2}/commkit/_cuda/src/llr.cu +0 -0
  53. {commkit-3.1.1 → commkit-3.1.2}/commkit/_cuda/src/selftest.cu +0 -0
  54. {commkit-3.1.1 → commkit-3.1.2}/commkit/_dispersion.py +0 -0
  55. {commkit-3.1.1 → commkit-3.1.2}/commkit/_fft.py +0 -0
  56. {commkit-3.1.1 → commkit-3.1.2}/commkit/_fiber.py +0 -0
  57. {commkit-3.1.1 → commkit-3.1.2}/commkit/_overlap_save.py +0 -0
  58. {commkit-3.1.1 → commkit-3.1.2}/commkit/_random.py +0 -0
  59. {commkit-3.1.1 → commkit-3.1.2}/commkit/_sequences.py +0 -0
  60. {commkit-3.1.1 → commkit-3.1.2}/commkit/analysis/__init__.py +0 -0
  61. {commkit-3.1.1 → commkit-3.1.2}/commkit/analysis/_common.py +0 -0
  62. {commkit-3.1.1 → commkit-3.1.2}/commkit/analysis/allan.py +0 -0
  63. {commkit-3.1.1 → commkit-3.1.2}/commkit/analysis/drift.py +0 -0
  64. {commkit-3.1.1 → commkit-3.1.2}/commkit/analysis/fm_noise.py +0 -0
  65. {commkit-3.1.1 → commkit-3.1.2}/commkit/analysis/interferometry.py +0 -0
  66. {commkit-3.1.1 → commkit-3.1.2}/commkit/analysis/linewidth/__init__.py +0 -0
  67. {commkit-3.1.1 → commkit-3.1.2}/commkit/analysis/linewidth/dsh.py +0 -0
  68. {commkit-3.1.1 → commkit-3.1.2}/commkit/analysis/linewidth/phase.py +0 -0
  69. {commkit-3.1.1 → commkit-3.1.2}/commkit/analysis/linewidth/result.py +0 -0
  70. {commkit-3.1.1 → commkit-3.1.2}/commkit/analysis/trajectory.py +0 -0
  71. {commkit-3.1.1 → commkit-3.1.2}/commkit/backend.py +0 -0
  72. {commkit-3.1.1 → commkit-3.1.2}/commkit/coding/__init__.py +0 -0
  73. {commkit-3.1.1 → commkit-3.1.2}/commkit/coding/_galois.py +0 -0
  74. {commkit-3.1.1 → commkit-3.1.2}/commkit/coding/_gf2.py +0 -0
  75. {commkit-3.1.1 → commkit-3.1.2}/commkit/coding/_ldpc_tables.py +0 -0
  76. {commkit-3.1.1 → commkit-3.1.2}/commkit/coding/_numba_loops.py +0 -0
  77. {commkit-3.1.1 → commkit-3.1.2}/commkit/coding/algebraic.py +0 -0
  78. {commkit-3.1.1 → commkit-3.1.2}/commkit/coding/base.py +0 -0
  79. {commkit-3.1.1 → commkit-3.1.2}/commkit/coding/convolutional.py +0 -0
  80. {commkit-3.1.1 → commkit-3.1.2}/commkit/coding/crc.py +0 -0
  81. {commkit-3.1.1 → commkit-3.1.2}/commkit/coding/interleaving.py +0 -0
  82. {commkit-3.1.1 → commkit-3.1.2}/commkit/coding/ldpc.py +0 -0
  83. {commkit-3.1.1 → commkit-3.1.2}/commkit/coding/linear.py +0 -0
  84. {commkit-3.1.1 → commkit-3.1.2}/commkit/coding/polar.py +0 -0
  85. {commkit-3.1.1 → commkit-3.1.2}/commkit/coding/product.py +0 -0
  86. {commkit-3.1.1 → commkit-3.1.2}/commkit/coding/ratematch.py +0 -0
  87. {commkit-3.1.1 → commkit-3.1.2}/commkit/coding/shaping.py +0 -0
  88. {commkit-3.1.1 → commkit-3.1.2}/commkit/coding/turbo.py +0 -0
  89. {commkit-3.1.1 → commkit-3.1.2}/commkit/core/__init__.py +0 -0
  90. {commkit-3.1.1 → commkit-3.1.2}/commkit/core/_signal_adapter.py +0 -0
  91. {commkit-3.1.1 → commkit-3.1.2}/commkit/core/frame.py +0 -0
  92. {commkit-3.1.1 → commkit-3.1.2}/commkit/core/generation.py +0 -0
  93. {commkit-3.1.1 → commkit-3.1.2}/commkit/core/signal.py +0 -0
  94. {commkit-3.1.1 → commkit-3.1.2}/commkit/equalization/__init__.py +0 -0
  95. {commkit-3.1.1 → commkit-3.1.2}/commkit/equalization/_block/__init__.py +0 -0
  96. {commkit-3.1.1 → commkit-3.1.2}/commkit/equalization/_block/_blind.py +0 -0
  97. {commkit-3.1.1 → commkit-3.1.2}/commkit/equalization/_block/_dd.py +0 -0
  98. {commkit-3.1.1 → commkit-3.1.2}/commkit/equalization/_block/_engine.py +0 -0
  99. {commkit-3.1.1 → commkit-3.1.2}/commkit/equalization/_common.py +0 -0
  100. {commkit-3.1.1 → commkit-3.1.2}/commkit/equalization/_kernels_numba.py +0 -0
  101. {commkit-3.1.1 → commkit-3.1.2}/commkit/equalization/_numba_loops.py +0 -0
  102. {commkit-3.1.1 → commkit-3.1.2}/commkit/equalization/blind.py +0 -0
  103. {commkit-3.1.1 → commkit-3.1.2}/commkit/equalization/linear.py +0 -0
  104. {commkit-3.1.1 → commkit-3.1.2}/commkit/equalization/pilots.py +0 -0
  105. {commkit-3.1.1 → commkit-3.1.2}/commkit/equalization/polarization.py +0 -0
  106. {commkit-3.1.1 → commkit-3.1.2}/commkit/equalization/result.py +0 -0
  107. {commkit-3.1.1 → commkit-3.1.2}/commkit/equalization/sequential/__init__.py +0 -0
  108. {commkit-3.1.1 → commkit-3.1.2}/commkit/equalization/sequential/_blind.py +0 -0
  109. {commkit-3.1.1 → commkit-3.1.2}/commkit/equalization/sequential/_dd.py +0 -0
  110. {commkit-3.1.1 → commkit-3.1.2}/commkit/equalization/sequential/_setup.py +0 -0
  111. {commkit-3.1.1 → commkit-3.1.2}/commkit/filtering/__init__.py +0 -0
  112. {commkit-3.1.1 → commkit-3.1.2}/commkit/filtering/fiber.py +0 -0
  113. {commkit-3.1.1 → commkit-3.1.2}/commkit/filtering/linear.py +0 -0
  114. {commkit-3.1.1 → commkit-3.1.2}/commkit/filtering/pulses.py +0 -0
  115. {commkit-3.1.1 → commkit-3.1.2}/commkit/frequency.py +0 -0
  116. {commkit-3.1.1 → commkit-3.1.2}/commkit/impairments/__init__.py +0 -0
  117. {commkit-3.1.1 → commkit-3.1.2}/commkit/impairments/channel/__init__.py +0 -0
  118. {commkit-3.1.1 → commkit-3.1.2}/commkit/impairments/channel/linear.py +0 -0
  119. {commkit-3.1.1 → commkit-3.1.2}/commkit/impairments/channel/nonlinear.py +0 -0
  120. {commkit-3.1.1 → commkit-3.1.2}/commkit/impairments/frontend.py +0 -0
  121. {commkit-3.1.1 → commkit-3.1.2}/commkit/impairments/source.py +0 -0
  122. {commkit-3.1.1 → commkit-3.1.2}/commkit/io.py +0 -0
  123. {commkit-3.1.1 → commkit-3.1.2}/commkit/logger.py +0 -0
  124. {commkit-3.1.1 → commkit-3.1.2}/commkit/mapping/__init__.py +0 -0
  125. {commkit-3.1.1 → commkit-3.1.2}/commkit/mapping/_numba_llr.py +0 -0
  126. {commkit-3.1.1 → commkit-3.1.2}/commkit/mapping/bits.py +0 -0
  127. {commkit-3.1.1 → commkit-3.1.2}/commkit/mapping/constellation.py +0 -0
  128. {commkit-3.1.1 → commkit-3.1.2}/commkit/mapping/gray.py +0 -0
  129. {commkit-3.1.1 → commkit-3.1.2}/commkit/mapping/llr.py +0 -0
  130. {commkit-3.1.1 → commkit-3.1.2}/commkit/mapping/shaping.py +0 -0
  131. {commkit-3.1.1 → commkit-3.1.2}/commkit/math.py +0 -0
  132. {commkit-3.1.1 → commkit-3.1.2}/commkit/metrics.py +0 -0
  133. {commkit-3.1.1 → commkit-3.1.2}/commkit/multirate.py +0 -0
  134. {commkit-3.1.1 → commkit-3.1.2}/commkit/plotting/__init__.py +0 -0
  135. {commkit-3.1.1 → commkit-3.1.2}/commkit/plotting/analysis.py +0 -0
  136. {commkit-3.1.1 → commkit-3.1.2}/commkit/plotting/constellation.py +0 -0
  137. {commkit-3.1.1 → commkit-3.1.2}/commkit/plotting/equalizer.py +0 -0
  138. {commkit-3.1.1 → commkit-3.1.2}/commkit/plotting/eye.py +0 -0
  139. {commkit-3.1.1 → commkit-3.1.2}/commkit/plotting/filter_response.py +0 -0
  140. {commkit-3.1.1 → commkit-3.1.2}/commkit/plotting/spectral.py +0 -0
  141. {commkit-3.1.1 → commkit-3.1.2}/commkit/plotting/sync.py +0 -0
  142. {commkit-3.1.1 → commkit-3.1.2}/commkit/plotting/theme.py +0 -0
  143. {commkit-3.1.1 → commkit-3.1.2}/commkit/plotting/waveform.py +0 -0
  144. {commkit-3.1.1 → commkit-3.1.2}/commkit/py.typed +0 -0
  145. {commkit-3.1.1 → commkit-3.1.2}/commkit/recovery/__init__.py +0 -0
  146. {commkit-3.1.1 → commkit-3.1.2}/commkit/recovery/_common.py +0 -0
  147. {commkit-3.1.1 → commkit-3.1.2}/commkit/recovery/bps.py +0 -0
  148. {commkit-3.1.1 → commkit-3.1.2}/commkit/recovery/carrier_phase.py +0 -0
  149. {commkit-3.1.1 → commkit-3.1.2}/commkit/recovery/corrections.py +0 -0
  150. {commkit-3.1.1 → commkit-3.1.2}/commkit/recovery/pilots.py +0 -0
  151. {commkit-3.1.1 → commkit-3.1.2}/commkit/recovery/pll.py +0 -0
  152. {commkit-3.1.1 → commkit-3.1.2}/commkit/recovery/tikhonov.py +0 -0
  153. {commkit-3.1.1 → commkit-3.1.2}/commkit/recovery/viterbi_viterbi.py +0 -0
  154. {commkit-3.1.1 → commkit-3.1.2}/commkit/smoothing.py +0 -0
  155. {commkit-3.1.1 → commkit-3.1.2}/commkit/spectral.py +0 -0
  156. {commkit-3.1.1 → commkit-3.1.2}/commkit/timing.py +0 -0
  157. {commkit-3.1.1 → commkit-3.1.2}/examples/00-getting-started.ipynb +0 -0
  158. {commkit-3.1.1 → commkit-3.1.2}/examples/01-constellations-and-bit-mapping.ipynb +0 -0
  159. {commkit-3.1.1 → commkit-3.1.2}/examples/02-spectral-analysis.ipynb +0 -0
  160. {commkit-3.1.1 → commkit-3.1.2}/examples/03-digital-filters.ipynb +0 -0
  161. {commkit-3.1.1 → commkit-3.1.2}/examples/04-pulse-shaping-and-matched-filtering.ipynb +0 -0
  162. {commkit-3.1.1 → commkit-3.1.2}/examples/05-multirate-and-resampling.ipynb +0 -0
  163. {commkit-3.1.1 → commkit-3.1.2}/examples/06-awgn-and-performance-metrics.ipynb +0 -0
  164. {commkit-3.1.1 → commkit-3.1.2}/examples/07-channel-coding.ipynb +0 -0
  165. {commkit-3.1.1 → commkit-3.1.2}/examples/08-probabilistic-shaping-and-coded-modulation.ipynb +0 -0
  166. {commkit-3.1.1 → commkit-3.1.2}/examples/09-laser-phase-noise.ipynb +0 -0
  167. {commkit-3.1.1 → commkit-3.1.2}/examples/10-front-end-impairments.ipynb +0 -0
  168. {commkit-3.1.1 → commkit-3.1.2}/examples/11-frame-synchronization-and-timing.ipynb +0 -0
  169. {commkit-3.1.1 → commkit-3.1.2}/examples/12-frequency-offset-estimation.ipynb +0 -0
  170. {commkit-3.1.1 → commkit-3.1.2}/examples/13-blind-carrier-phase-recovery.ipynb +0 -0
  171. {commkit-3.1.1 → commkit-3.1.2}/examples/14-pilot-aided-carrier-recovery.ipynb +0 -0
  172. {commkit-3.1.1 → commkit-3.1.2}/examples/15-linear-channel-equalization.ipynb +0 -0
  173. {commkit-3.1.1 → commkit-3.1.2}/examples/16-adaptive-equalization.ipynb +0 -0
  174. {commkit-3.1.1 → commkit-3.1.2}/examples/17-blind-equalization.ipynb +0 -0
  175. {commkit-3.1.1 → commkit-3.1.2}/examples/18-polarization-multiplexing.ipynb +0 -0
  176. {commkit-3.1.1 → commkit-3.1.2}/examples/19-chromatic-dispersion.ipynb +0 -0
  177. {commkit-3.1.1 → commkit-3.1.2}/examples/20-nonlinear-fiber-and-backpropagation.ipynb +0 -0
  178. {commkit-3.1.1 → commkit-3.1.2}/examples/21-fec-in-optical-standards.ipynb +0 -0
  179. {commkit-3.1.1 → commkit-3.1.2}/examples/22-coherent-transceiver-end-to-end.ipynb +0 -0
  180. {commkit-3.1.1 → commkit-3.1.2}/examples/23-carrier-phase-characterization.ipynb +0 -0
  181. {commkit-3.1.1 → commkit-3.1.2}/examples/24-laser-linewidth-self-heterodyne.ipynb +0 -0
  182. {commkit-3.1.1 → commkit-3.1.2}/examples/25-laser-linewidth-short-delay-and-flicker.ipynb +0 -0
  183. {commkit-3.1.1 → commkit-3.1.2}/examples/26-laser-linewidth-self-homodyne-iq.ipynb +0 -0
  184. {commkit-3.1.1 → commkit-3.1.2}/examples/27-measurement-template-self-heterodyne.ipynb +0 -0
  185. {commkit-3.1.1 → commkit-3.1.2}/examples/28-measurement-template-self-homodyne-iq.ipynb +0 -0
  186. {commkit-3.1.1 → commkit-3.1.2}/examples/README.md +0 -0
  187. {commkit-3.1.1 → commkit-3.1.2}/tests/analysis/test_allan.py +0 -0
  188. {commkit-3.1.1 → commkit-3.1.2}/tests/analysis/test_drift.py +0 -0
  189. {commkit-3.1.1 → commkit-3.1.2}/tests/analysis/test_fm_noise.py +0 -0
  190. {commkit-3.1.1 → commkit-3.1.2}/tests/analysis/test_interferometry.py +0 -0
  191. {commkit-3.1.1 → commkit-3.1.2}/tests/analysis/test_linewidth.py +0 -0
  192. {commkit-3.1.1 → commkit-3.1.2}/tests/analysis/test_trajectory.py +0 -0
  193. {commkit-3.1.1 → commkit-3.1.2}/tests/coding/test_algebraic.py +0 -0
  194. {commkit-3.1.1 → commkit-3.1.2}/tests/coding/test_coding_base.py +0 -0
  195. {commkit-3.1.1 → commkit-3.1.2}/tests/coding/test_convolutional.py +0 -0
  196. {commkit-3.1.1 → commkit-3.1.2}/tests/coding/test_crc.py +0 -0
  197. {commkit-3.1.1 → commkit-3.1.2}/tests/coding/test_gf2.py +0 -0
  198. {commkit-3.1.1 → commkit-3.1.2}/tests/coding/test_interleaving.py +0 -0
  199. {commkit-3.1.1 → commkit-3.1.2}/tests/coding/test_ldpc.py +0 -0
  200. {commkit-3.1.1 → commkit-3.1.2}/tests/coding/test_ldpc_kernel.py +0 -0
  201. {commkit-3.1.1 → commkit-3.1.2}/tests/coding/test_ldpc_tables.py +0 -0
  202. {commkit-3.1.1 → commkit-3.1.2}/tests/coding/test_linear_codes.py +0 -0
  203. {commkit-3.1.1 → commkit-3.1.2}/tests/coding/test_polar.py +0 -0
  204. {commkit-3.1.1 → commkit-3.1.2}/tests/coding/test_product.py +0 -0
  205. {commkit-3.1.1 → commkit-3.1.2}/tests/coding/test_ratematch.py +0 -0
  206. {commkit-3.1.1 → commkit-3.1.2}/tests/coding/test_shaping.py +0 -0
  207. {commkit-3.1.1 → commkit-3.1.2}/tests/coding/test_turbo.py +0 -0
  208. {commkit-3.1.1 → commkit-3.1.2}/tests/common/__init__.py +0 -0
  209. {commkit-3.1.1 → commkit-3.1.2}/tests/common/coding.py +0 -0
  210. {commkit-3.1.1 → commkit-3.1.2}/tests/common/conversions.py +0 -0
  211. {commkit-3.1.1 → commkit-3.1.2}/tests/common/kernel_utils.py +0 -0
  212. {commkit-3.1.1 → commkit-3.1.2}/tests/common/memory.py +0 -0
  213. {commkit-3.1.1 → commkit-3.1.2}/tests/common/metrics.py +0 -0
  214. {commkit-3.1.1 → commkit-3.1.2}/tests/common/periodic.py +0 -0
  215. {commkit-3.1.1 → commkit-3.1.2}/tests/common/reference_impl.py +0 -0
  216. {commkit-3.1.1 → commkit-3.1.2}/tests/common/signals.py +0 -0
  217. {commkit-3.1.1 → commkit-3.1.2}/tests/conftest.py +0 -0
  218. {commkit-3.1.1 → commkit-3.1.2}/tests/core/test_frame.py +0 -0
  219. {commkit-3.1.1 → commkit-3.1.2}/tests/core/test_generation.py +0 -0
  220. {commkit-3.1.1 → commkit-3.1.2}/tests/core/test_psqam.py +0 -0
  221. {commkit-3.1.1 → commkit-3.1.2}/tests/core/test_signal.py +0 -0
  222. {commkit-3.1.1 → commkit-3.1.2}/tests/core/test_signal_adapter.py +0 -0
  223. {commkit-3.1.1 → commkit-3.1.2}/tests/core/test_signal_mimo.py +0 -0
  224. {commkit-3.1.1 → commkit-3.1.2}/tests/equalization/test_blind.py +0 -0
  225. {commkit-3.1.1 → commkit-3.1.2}/tests/equalization/test_block.py +0 -0
  226. {commkit-3.1.1 → commkit-3.1.2}/tests/equalization/test_bps_kernel.py +0 -0
  227. {commkit-3.1.1 → commkit-3.1.2}/tests/equalization/test_cpr.py +0 -0
  228. {commkit-3.1.1 → commkit-3.1.2}/tests/equalization/test_cs_kernel.py +0 -0
  229. {commkit-3.1.1 → commkit-3.1.2}/tests/equalization/test_equalizer_oracles.py +0 -0
  230. {commkit-3.1.1 → commkit-3.1.2}/tests/equalization/test_linear.py +0 -0
  231. {commkit-3.1.1 → commkit-3.1.2}/tests/equalization/test_mimo.py +0 -0
  232. {commkit-3.1.1 → commkit-3.1.2}/tests/equalization/test_polarization.py +0 -0
  233. {commkit-3.1.1 → commkit-3.1.2}/tests/equalization/test_sequential.py +0 -0
  234. {commkit-3.1.1 → commkit-3.1.2}/tests/equalization/test_state.py +0 -0
  235. {commkit-3.1.1 → commkit-3.1.2}/tests/equalization/test_winit.py +0 -0
  236. {commkit-3.1.1 → commkit-3.1.2}/tests/impairments/channel/test_channel_linear.py +0 -0
  237. {commkit-3.1.1 → commkit-3.1.2}/tests/impairments/channel/test_channel_nonlinear.py +0 -0
  238. {commkit-3.1.1 → commkit-3.1.2}/tests/impairments/test_frontend.py +0 -0
  239. {commkit-3.1.1 → commkit-3.1.2}/tests/impairments/test_source.py +0 -0
  240. {commkit-3.1.1 → commkit-3.1.2}/tests/mapping/test_bits.py +0 -0
  241. {commkit-3.1.1 → commkit-3.1.2}/tests/mapping/test_constellation.py +0 -0
  242. {commkit-3.1.1 → commkit-3.1.2}/tests/mapping/test_gray.py +0 -0
  243. {commkit-3.1.1 → commkit-3.1.2}/tests/mapping/test_llr.py +0 -0
  244. {commkit-3.1.1 → commkit-3.1.2}/tests/plotting/test_plot_analysis.py +0 -0
  245. {commkit-3.1.1 → commkit-3.1.2}/tests/plotting/test_plot_constellation.py +0 -0
  246. {commkit-3.1.1 → commkit-3.1.2}/tests/plotting/test_plot_equalizer.py +0 -0
  247. {commkit-3.1.1 → commkit-3.1.2}/tests/plotting/test_plot_eye.py +0 -0
  248. {commkit-3.1.1 → commkit-3.1.2}/tests/plotting/test_plot_filter_response.py +0 -0
  249. {commkit-3.1.1 → commkit-3.1.2}/tests/plotting/test_plot_spectral.py +0 -0
  250. {commkit-3.1.1 → commkit-3.1.2}/tests/plotting/test_plot_sync.py +0 -0
  251. {commkit-3.1.1 → commkit-3.1.2}/tests/plotting/test_plot_theme.py +0 -0
  252. {commkit-3.1.1 → commkit-3.1.2}/tests/plotting/test_plot_waveform.py +0 -0
  253. {commkit-3.1.1 → commkit-3.1.2}/tests/plugins/__init__.py +0 -0
  254. {commkit-3.1.1 → commkit-3.1.2}/tests/plugins/precision_trace.py +0 -0
  255. {commkit-3.1.1 → commkit-3.1.2}/tests/plugins/transfer_trace.py +0 -0
  256. {commkit-3.1.1 → commkit-3.1.2}/tests/recovery/test_bps.py +0 -0
  257. {commkit-3.1.1 → commkit-3.1.2}/tests/recovery/test_corrections.py +0 -0
  258. {commkit-3.1.1 → commkit-3.1.2}/tests/recovery/test_joint.py +0 -0
  259. {commkit-3.1.1 → commkit-3.1.2}/tests/recovery/test_pilots.py +0 -0
  260. {commkit-3.1.1 → commkit-3.1.2}/tests/recovery/test_pll.py +0 -0
  261. {commkit-3.1.1 → commkit-3.1.2}/tests/recovery/test_recovery_oracles.py +0 -0
  262. {commkit-3.1.1 → commkit-3.1.2}/tests/recovery/test_tikhonov.py +0 -0
  263. {commkit-3.1.1 → commkit-3.1.2}/tests/recovery/test_viterbi_viterbi.py +0 -0
  264. {commkit-3.1.1 → commkit-3.1.2}/tests/test_api_contracts.py +0 -0
  265. {commkit-3.1.1 → commkit-3.1.2}/tests/test_array.py +0 -0
  266. {commkit-3.1.1 → commkit-3.1.2}/tests/test_backend.py +0 -0
  267. {commkit-3.1.1 → commkit-3.1.2}/tests/test_cuda_infra.py +0 -0
  268. {commkit-3.1.1 → commkit-3.1.2}/tests/test_dispersion.py +0 -0
  269. {commkit-3.1.1 → commkit-3.1.2}/tests/test_examples.py +0 -0
  270. {commkit-3.1.1 → commkit-3.1.2}/tests/test_fiber.py +0 -0
  271. {commkit-3.1.1 → commkit-3.1.2}/tests/test_filtering.py +0 -0
  272. {commkit-3.1.1 → commkit-3.1.2}/tests/test_frequency.py +0 -0
  273. {commkit-3.1.1 → commkit-3.1.2}/tests/test_import_hygiene.py +0 -0
  274. {commkit-3.1.1 → commkit-3.1.2}/tests/test_io.py +0 -0
  275. {commkit-3.1.1 → commkit-3.1.2}/tests/test_logger.py +0 -0
  276. {commkit-3.1.1 → commkit-3.1.2}/tests/test_math.py +0 -0
  277. {commkit-3.1.1 → commkit-3.1.2}/tests/test_metrics.py +0 -0
  278. {commkit-3.1.1 → commkit-3.1.2}/tests/test_multirate.py +0 -0
  279. {commkit-3.1.1 → commkit-3.1.2}/tests/test_pulse_shaping.py +0 -0
  280. {commkit-3.1.1 → commkit-3.1.2}/tests/test_signal_pipeline.py +0 -0
  281. {commkit-3.1.1 → commkit-3.1.2}/tests/test_smoothing.py +0 -0
  282. {commkit-3.1.1 → commkit-3.1.2}/tests/test_spectral.py +0 -0
  283. {commkit-3.1.1 → commkit-3.1.2}/tests/test_timing.py +0 -0
  284. {commkit-3.1.1 → commkit-3.1.2}/tests/test_working_memory.py +0 -0
@@ -4,6 +4,20 @@ Notable changes to CommKit, newest first. Performance figures are from the refer
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [3.1.2] - 2026-10-29
8
+
9
+ `apply_awgn` without hidden assumptions about what a channel is: Es/N0 holds per channel, and the OSNR counts the ASE of the modes the medium guides instead of assuming the two polarizations of a single-mode fiber.
10
+
11
+ ### Changes that alter results
12
+
13
+ - **`apply_awgn(esn0_db=)` gives every channel the requested Es/N0.** The default noise level comes from each channel's own mean power; it used to come from the mean over all channels, so with unequal channel powers a stronger channel got a better Es/N0 than requested and a weaker one a worse. Equal powers give the same result as before. Pass a scalar `signal_power` for the old behavior, one absolute noise level shared by every channel.
14
+ - **`apply_awgn(osnr_db=)` counts the ASE of `noise_modes` modes, `max(C, 2)` by default.** One or two channels are unchanged (both polarizations of a single-mode fiber). More channels, such as the modes of a few-mode fiber or the cores of a multicore fiber, each count as a mode; they used to share the ASE of two polarizations, which put C/2 times too much noise on each.
15
+
16
+ ### Added
17
+
18
+ - `apply_awgn(noise_modes=)` for a medium that guides modes without signal, e.g. `noise_modes=6` for four channels in a three-mode fiber.
19
+ - `apply_awgn(signal_power=)` accepts one power per channel, `(C,)`.
20
+
7
21
  ## [3.1.1] - 2026-10-29
8
22
 
9
23
  API consistency. One name per concept and a unit in every size, defaults that model the physics, physical units where the rest of the API has them, and features that use what the Signal and the frame already carry: the optical carrier for dispersion, fiber and OSNR noise, the frame's pilots for the pilot-aided algorithms, the code of the reference for post-FEC metrics. Most changes are renames that fail loudly with `TypeError`; the four default changes and the speed of light alter numbers and are listed first.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: commkit
3
- Version: 3.1.1
3
+ Version: 3.1.2
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.1.1"
34
+ __version__ = "3.1.2"
35
35
 
36
36
  from .backend import PrecisionWarning
37
37
  from .core import (
@@ -0,0 +1,229 @@
1
+ """Additive white Gaussian noise: :func:`apply_awgn`."""
2
+
3
+ from typing import Any
4
+
5
+ from .._random import RNG, as_generator, complex_normal, standard_normal
6
+ from ..backend import ArrayType, dispatch
7
+ from ..core._signal_adapter import S, adapt_signal
8
+ from ..logger import logger
9
+ from ..math import db_to_linear
10
+
11
+ __all__ = ["apply_awgn"]
12
+
13
+
14
+ def apply_awgn(
15
+ samples: S,
16
+ *,
17
+ esn0_db: float | None = None,
18
+ osnr_db: float | None = None,
19
+ sps: float | None = None,
20
+ signal_power: float | ArrayType | None = None,
21
+ reference_bandwidth: float = 12.5e9,
22
+ noise_modes: int | None = None,
23
+ rng: RNG = None,
24
+ ) -> S:
25
+ """Add white Gaussian noise at an Es/N0, or at an OSNR for an optical field.
26
+
27
+ Powers are per sample and per channel, in the squared units of the
28
+ samples: ``P_c = mean(|x_c[n]|²)`` over the samples of channel ``c``.
29
+ A symbol lasts ``T = sps / f_s``, so its energy is
30
+ ``Es_c = P_c · T = sps · P_c / f_s``; white noise of variance ``σ²``
31
+ per sample fills the band ``f_s`` with the density ``N0 = σ² / f_s``.
32
+ Hence ``Es/N0 = sps · P_c / σ²``: in sample units, ``Es`` is the power
33
+ summed over the ``sps`` samples of a symbol and ``N0`` the noise
34
+ variance of one sample.
35
+
36
+ With ``esn0_db`` every channel gets the requested Es/N0 on its own:
37
+ channel ``c`` gets noise of variance ``σ_c² = sps · P_c / (Es/N0)`` per
38
+ sample. That is the Es/N0 per symbol after a matched filter and
39
+ decimation, whatever the oversampling. A channel can be anything with
40
+ its own receiver noise: a polarization, an antenna, a wire pair, a lane.
41
+
42
+ With ``osnr_db`` (an optical Signal) the noise is amplified spontaneous
43
+ emission specified as optical links are: the total signal power of all
44
+ channels over the ASE power in ``reference_bandwidth``, summed over the
45
+ ``M = noise_modes`` modes the medium guides (polarizations included).
46
+ ASE has the same density in every mode and is white over the simulated
47
+ band ``f_s``, so complex noise of variance ``σ²`` per mode puts
48
+ ``M σ² B_ref / f_s`` into ``B_ref``; every channel therefore gets the
49
+ same
50
+
51
+ σ² = (Σ_c P_c) · f_s / (M · B_ref · OSNR),
52
+
53
+ which for equal powers gives ``Es/N0 = M B_ref / (C R_s) · OSNR`` per
54
+ channel, whatever the oversampling: the textbook
55
+ ``2 B_ref / (p R_s) · OSNR`` for ``p`` polarizations of a single-mode
56
+ fiber.
57
+
58
+ Parameters
59
+ ----------
60
+ samples : array_like or Signal
61
+ Input samples, time on the last axis, ``(N,)`` or ``(C, N)``; for
62
+ ``osnr_db`` a complex optical Signal (``domain="OPT"``) with one
63
+ channel per guided mode it occupies (polarization, spatial mode or
64
+ core).
65
+ esn0_db : float, optional
66
+ Symbol energy over noise spectral density, Es/N0, in dB, for each
67
+ channel.
68
+ osnr_db : float, optional
69
+ Optical signal-to-noise ratio in dB in ``reference_bandwidth``,
70
+ instead of ``esn0_db``.
71
+ sps : float, optional
72
+ Samples per symbol, for ``esn0_db``. Taken from the Signal; required
73
+ for array input. A value that disagrees with the Signal raises.
74
+ signal_power : float or array_like, optional
75
+ The power ``P_c`` per sample of each channel, ``mean(|x_c|²)`` in
76
+ the squared units of the samples, that the Es/N0 or OSNR refers to:
77
+ ``Es_c = sps · P_c``. A scalar is the power of *each* channel (not
78
+ their sum or a total), so every channel gets the same absolute
79
+ noise level, as identical receivers with one noise floor; ``(C,)``
80
+ gives one power per channel. Defaults to each channel's measured
81
+ mean power over the whole record, which counts any silent stretch.
82
+ Pass the nominal power instead when that is the reference: e.g.
83
+ ``signal_power=1.0`` for unit-power symbols at one sample per
84
+ symbol sent in bursts separated by zero guard intervals, whose
85
+ measured mean falls below the power of the symbols. For a
86
+ pulse-shaped signal the per-sample power of unit-energy symbols is
87
+ ``1 / sps`` (``signal_power=1 / sig.sps``). With ``osnr_db`` the
88
+ signal power is the sum over channels, ``C · signal_power`` for a
89
+ scalar.
90
+ reference_bandwidth : float, default 12.5e9
91
+ Noise reference bandwidth ``B_ref`` of ``osnr_db`` in Hz. The
92
+ standard reference is 0.1 nm of optical bandwidth, ``c · 0.1 nm /
93
+ λ²``: 12.5 GHz in the C band (12.48 GHz at 1550 nm), 17.5 GHz at
94
+ 1310 nm. Outside the C band pass the value for the carrier, e.g.
95
+ ``reference_bandwidth=17.5e9`` for an O-band OSNR in 0.1 nm.
96
+ noise_modes : int, optional
97
+ For ``osnr_db``: the number ``M`` of guided modes, polarizations
98
+ included, whose ASE the OSNR counts. Defaults to ``max(C, 2)``: both
99
+ polarizations of a single-mode fiber for one or two channels (an
100
+ OSNR meter's definition; a single-polarization signal receives the
101
+ half of the ASE in its own polarization), and one mode per channel
102
+ beyond that (few-mode or multicore fiber with every mode carrying
103
+ signal). Pass it when the medium guides modes that carry no signal,
104
+ e.g. ``noise_modes=6`` for 4 channels in a 3-mode fiber.
105
+ rng : int, numpy.random.Generator or None
106
+ Random source. The noise is drawn on the data's device: from this
107
+ Generator on the CPU, and on the GPU from a CuPy Generator seeded from
108
+ it, so a seed is reproducible per device.
109
+
110
+ Returns
111
+ -------
112
+ ndarray or Signal
113
+ The noisy samples, same shape, dtype and device as the input.
114
+
115
+ Raises
116
+ ------
117
+ ValueError
118
+ If neither or both of ``esn0_db`` and ``osnr_db`` are given,
119
+ ``osnr_db`` is given for anything but a complex optical Signal,
120
+ ``reference_bandwidth`` is not positive, ``noise_modes`` is not an
121
+ integer of at least ``C``, or a per-channel ``signal_power`` does not
122
+ have ``C`` entries.
123
+
124
+ Notes
125
+ -----
126
+ Complex input gets circular complex noise, its variance split equally
127
+ between I and Q; real input gets real noise of the full variance. At
128
+ ``sps = 1`` Es/N0 equals the per-sample SNR.
129
+
130
+ OSNR is a property of the whole optical field, so pass every channel in
131
+ one call: calling once per polarization of a dual-polarization signal
132
+ counts only that polarization's power and adds half the intended
133
+ noise. Modes left out of a simulation carry no noise, which matters
134
+ for a receiver without a polarizer (direct detection, where the
135
+ orthogonal polarization adds ASE-ASE beat noise); model such a link
136
+ with both polarizations.
137
+
138
+ Examples
139
+ --------
140
+ >>> sig = generate(Constellation.pam(4), 1000, symbol_rate=1e6, sps=4)
141
+ >>> noisy = apply_awgn(sig.samples, esn0_db=20, sps=4)
142
+ >>> noisy = apply_awgn(sig, esn0_db=20) # Signal input: sps taken from sig
143
+ >>> field = sig.replace(domain="OPT", center_frequency=193.4e12)
144
+ >>> noisy = apply_awgn(field, osnr_db=18) # in 12.5 GHz, both polarizations
145
+ """
146
+ signal_adapter = adapt_signal(samples, function_name="apply_awgn()")
147
+ if (esn0_db is None) == (osnr_db is None):
148
+ raise ValueError("apply_awgn(): give one of esn0_db and osnr_db.")
149
+ x, xp, _ = dispatch(signal_adapter.array)
150
+ num_channels = 1 if x.ndim == 1 else x.shape[0]
151
+ power = _channel_power(x, xp, signal_power, num_channels)
152
+
153
+ if osnr_db is not None:
154
+ sig = signal_adapter.signal
155
+ if sig is None or sig.domain != "OPT" or not xp.iscomplexobj(x):
156
+ raise ValueError(
157
+ "apply_awgn(): osnr_db needs a complex optical Signal "
158
+ "(domain='OPT'), whose sampling rate sets the noise bandwidth."
159
+ )
160
+ if not reference_bandwidth > 0:
161
+ raise ValueError(
162
+ "apply_awgn(): reference_bandwidth must be positive (Hz), got "
163
+ f"{reference_bandwidth!r}."
164
+ )
165
+ if noise_modes is None:
166
+ noise_modes = max(num_channels, 2)
167
+ if (
168
+ isinstance(noise_modes, bool)
169
+ or not isinstance(noise_modes, int)
170
+ or noise_modes < num_channels
171
+ ):
172
+ raise ValueError(
173
+ f"apply_awgn(): noise_modes must be an integer of at least the "
174
+ f"{num_channels} channels, got {noise_modes!r}."
175
+ )
176
+ logger.info(
177
+ "Adding ASE noise (OSNR target: %.2f dB, %d modes).", osnr_db, noise_modes
178
+ )
179
+ # The ASE density is the same in every mode: one variance for all
180
+ # channels, from their total power.
181
+ noise_power = (
182
+ xp.sum(xp.broadcast_to(power, (num_channels, 1)))
183
+ * sig.sampling_rate
184
+ / (noise_modes * reference_bandwidth * db_to_linear(osnr_db, power=True))
185
+ )
186
+ return signal_adapter.wrap_samples(x + _noise(x, xp, noise_power, rng))
187
+
188
+ sps = signal_adapter.resolve_fact("sps", sps)
189
+ logger.info("Adding AWGN (Es/N0 target: %.2f dB).", esn0_db)
190
+
191
+ # Es/N0 to the noise variance per sample: Es = sps · P_c is the energy of
192
+ # a symbol over its sps samples; white noise of variance σ² over the band
193
+ # f_s = sps · R_s has N0 = σ² / f_s, so Es/N0 = P_c · sps / σ² per
194
+ # symbol, i.e. a per-sample SNR of Es/N0 / sps.
195
+ esn0_linear = max(db_to_linear(esn0_db, power=True), 1e-20)
196
+ noise_power = power * (sps / esn0_linear)
197
+ return signal_adapter.wrap_samples(x + _noise(x, xp, noise_power, rng))
198
+
199
+
200
+ def _channel_power(
201
+ x: ArrayType, xp: Any, signal_power: Any, num_channels: int
202
+ ) -> ArrayType:
203
+ """Signal power per channel, broadcastable against ``x``: measured,
204
+ ``(C, 1)`` (``(1,)`` for 1-D input), or the given scalar or ``(C,)``."""
205
+ if signal_power is None:
206
+ return xp.mean(xp.abs(x) ** 2, axis=-1, keepdims=True)
207
+ power = xp.asarray(signal_power, dtype=xp.float64)
208
+ if power.ndim == 0:
209
+ return power
210
+ if power.shape != (num_channels,):
211
+ raise ValueError(
212
+ f"apply_awgn(): signal_power must be a scalar or have one entry per "
213
+ f"channel ({num_channels},), got shape {power.shape}."
214
+ )
215
+ return power[:, None] if x.ndim == 2 else power
216
+
217
+
218
+ def _noise(x: Any, xp: Any, noise_power: Any, rng: RNG) -> Any:
219
+ """White Gaussian noise of ``noise_power`` per sample, shaped like ``x``.
220
+
221
+ Complex noise splits its power equally between I and Q.
222
+ """
223
+ gen = as_generator(rng)
224
+ real_dtype = x.real.dtype if x.real.dtype.kind == "f" else xp.dtype(xp.float64)
225
+ if xp.iscomplexobj(x):
226
+ std = xp.sqrt(noise_power / 2).astype(real_dtype)
227
+ return complex_normal(gen, x.shape, std=std, dtype=real_dtype, xp=xp)
228
+ std = xp.sqrt(noise_power).astype(real_dtype)
229
+ return std * standard_normal(gen, x.shape, dtype=real_dtype, xp=xp)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "commkit"
3
- version = "3.1.1"
3
+ version = "3.1.2"
4
4
  description = "A Python library for high-performance digital communications research."
5
5
  readme = "README.md"
6
6
  authors = [{ name = "Denis Fatkhiev" }]
@@ -86,7 +86,7 @@ markers = [
86
86
  ]
87
87
 
88
88
  [tool.bumpversion]
89
- current_version = "3.1.1"
89
+ current_version = "3.1.2"
90
90
  parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
91
91
  serialize = ["{major}.{minor}.{patch}"]
92
92
  search = "{current_version}"
@@ -37,7 +37,7 @@ class TestAddAWGN:
37
37
  )
38
38
 
39
39
  def test_awgn_signal_power_override(self, xp):
40
- """Explicit signal_power sets an absolute noise level (dark capture)."""
40
+ """Explicit signal_power sets an absolute noise level (silent capture)."""
41
41
  dark = xp.zeros(20000, dtype=xp.complex128)
42
42
  noisy = apply_awgn(dark, esn0_db=10.0, sps=1, signal_power=1.0, rng=3)
43
43
  # Without the override the noise power would be 0; with it, 0.1.
@@ -158,3 +158,87 @@ class TestOSNR:
158
158
  wide = apply_awgn(sig, osnr_db=15.0, reference_bandwidth=25e9, rng=1)
159
159
  var_wide = float(xp.mean(xp.abs(wide.samples - x) ** 2))
160
160
  assert var_wide == pytest.approx(var / 2, rel=0.02)
161
+
162
+
163
+ class TestPerChannel:
164
+ """Es/N0 holds per channel; a scalar signal_power shares one noise floor."""
165
+
166
+ @staticmethod
167
+ def _noise_var(xp, noisy, x):
168
+ return xp.mean(xp.abs(noisy - x) ** 2, axis=-1)
169
+
170
+ def test_each_channel_gets_its_esn0(self, xp):
171
+ x = xp.ones((2, 1 << 16), dtype=xp.complex64)
172
+ x[1] *= 2.0 # 6 dB stronger
173
+ noisy = apply_awgn(x, esn0_db=10.0, sps=1, rng=1)
174
+ var = self._noise_var(xp, noisy, x)
175
+ assert xp.allclose(var, xp.asarray([0.1, 0.4]), rtol=0.03)
176
+
177
+ def test_scalar_signal_power_is_shared(self, xp):
178
+ x = xp.ones((2, 1 << 16), dtype=xp.complex64)
179
+ x[1] *= 2.0
180
+ noisy = apply_awgn(x, esn0_db=10.0, sps=1, signal_power=1.0, rng=1)
181
+ var = self._noise_var(xp, noisy, x)
182
+ assert xp.allclose(var, xp.asarray([0.1, 0.1]), rtol=0.03)
183
+
184
+ def test_per_channel_signal_power(self, xp):
185
+ x = xp.zeros((2, 1 << 16), dtype=xp.complex64)
186
+ noisy = apply_awgn(x, esn0_db=10.0, sps=2, signal_power=[1.0, 0.5], rng=1)
187
+ var = self._noise_var(xp, noisy, x)
188
+ assert noisy.dtype == xp.complex64
189
+ assert xp.allclose(var, xp.asarray([0.2, 0.1]), rtol=0.03)
190
+ with pytest.raises(ValueError, match="one entry per channel"):
191
+ apply_awgn(x, esn0_db=10.0, sps=1, signal_power=[1.0, 1.0, 1.0])
192
+
193
+
194
+ class TestNoiseModes:
195
+ """apply_awgn(osnr_db=, noise_modes=): ASE counted over the guided modes."""
196
+
197
+ RS = 32e9
198
+
199
+ def _field(self, xp, num_channels, sps=2):
200
+ x = xp.ones((num_channels, 1 << 16), dtype=xp.complex64)
201
+ return Signal(
202
+ samples=x,
203
+ sampling_rate=sps * self.RS,
204
+ symbol_rate=self.RS,
205
+ domain="OPT",
206
+ center_frequency=193.4e12,
207
+ )
208
+
209
+ def _esn0(self, xp, sig, **kwargs):
210
+ noisy = apply_awgn(sig, osnr_db=20.0, rng=2, **kwargs)
211
+ var = float(xp.mean(xp.abs(noisy.samples - sig.samples) ** 2))
212
+ return sig.sps / var
213
+
214
+ def test_mode_division_default_counts_every_channel(self, xp):
215
+ """Six channels in a 3-mode fiber: Es/N0 = 6 B_ref / (6 R_s) · OSNR,
216
+ the single-mode dual-polarization value."""
217
+ esn0 = self._esn0(xp, self._field(xp, 6))
218
+ assert esn0 == pytest.approx(100 * 12.5e9 / self.RS, rel=0.02)
219
+ assert self._esn0(xp, self._field(xp, 2)) == pytest.approx(esn0, rel=0.02)
220
+
221
+ def test_unexcited_modes_still_carry_ase(self, xp):
222
+ """4 of 6 modes carry signal: the ASE of all 6 counts, so each
223
+ channel sees 6/4 of the default's Es/N0."""
224
+ sig = self._field(xp, 4)
225
+ assert self._esn0(xp, sig, noise_modes=6) == pytest.approx(
226
+ 1.5 * self._esn0(xp, sig), rel=0.02
227
+ )
228
+
229
+ def test_unequal_powers_share_one_ase_level(self, xp):
230
+ sig = self._field(xp, 2)
231
+ x = sig.samples.copy()
232
+ x[1] *= 2.0
233
+ noisy = apply_awgn(sig.replace(samples=x), osnr_db=20.0, rng=2)
234
+ var = xp.mean(xp.abs(noisy.samples - x) ** 2, axis=-1)
235
+ assert float(var[1]) == pytest.approx(float(var[0]), rel=0.03)
236
+ # Total power 1 + 4 = 5 over two modes: 5 · f_s / (2 B_ref OSNR).
237
+ expected = 5 * 2 * self.RS / (2 * 12.5e9 * 100)
238
+ assert float(var[0]) == pytest.approx(expected, rel=0.03)
239
+
240
+ def test_noise_modes_validated(self, xp):
241
+ sig = self._field(xp, 4)
242
+ for bad in (3, 0, 4.0, True):
243
+ with pytest.raises(ValueError, match="noise_modes"):
244
+ apply_awgn(sig, osnr_db=20.0, noise_modes=bad)
@@ -418,7 +418,7 @@ wheels = [
418
418
 
419
419
  [[package]]
420
420
  name = "commkit"
421
- version = "3.1.1"
421
+ version = "3.1.2"
422
422
  source = { editable = "." }
423
423
  dependencies = [
424
424
  { name = "matplotlib" },
@@ -1,184 +0,0 @@
1
- """Additive white Gaussian noise: :func:`apply_awgn`."""
2
-
3
- from typing import Any
4
-
5
- from .._random import RNG, as_generator, complex_normal, standard_normal
6
- from ..backend import dispatch
7
- from ..core._signal_adapter import S, adapt_signal
8
- from ..logger import logger
9
- from ..math import db_to_linear
10
-
11
- __all__ = ["apply_awgn"]
12
-
13
-
14
- def apply_awgn(
15
- samples: S,
16
- *,
17
- esn0_db: float | None = None,
18
- osnr_db: float | None = None,
19
- sps: float | None = None,
20
- signal_power: float | None = None,
21
- reference_bandwidth: float = 12.5e9,
22
- rng: RNG = None,
23
- ) -> S:
24
- """Add white Gaussian noise at an Es/N0, or at an OSNR for an optical field.
25
-
26
- With ``esn0_db`` the noise variance per sample is
27
- ``signal_power · sps / (Es/N0)``: with ``Es = sps · signal_power``, that
28
- is ``Es/N0`` per symbol after a matched filter and decimation, whatever
29
- the oversampling.
30
-
31
- With ``osnr_db`` (an optical Signal) the noise is amplified spontaneous
32
- emission specified as optical links are: the total signal power over the
33
- ASE power in ``reference_bandwidth``, counted in both polarizations. ASE
34
- is white over the simulated band ``f_s``, so complex noise of variance
35
- ``σ²`` per polarization has the density ``σ²/f_s`` and puts
36
- ``2 σ² B_ref / f_s`` into ``B_ref`` over both polarizations; each
37
- channel therefore gets
38
-
39
- σ² = C · signal_power · f_s / (2 · B_ref · OSNR),
40
-
41
- which gives the textbook ``Es/N0 = 2 B_ref / (p R_s) · OSNR`` for ``p``
42
- polarizations, whatever the oversampling. A single-polarization Signal
43
- (``C = 1``) still counts the ASE of both polarizations, as an OSNR meter
44
- does, and receives the half that falls into its own: the noise a
45
- single-polarization coherent receiver sees, 3 dB less than a
46
- dual-polarization signal of the same total power at the same OSNR.
47
-
48
- OSNR is a property of the whole optical field, so pass every
49
- polarization in one call. Calling once per polarization of a
50
- dual-polarization signal counts only that polarization's power and
51
- adds half the intended noise. The orthogonal polarization's ASE is not
52
- simulated for ``C = 1``, which matters for a receiver without a
53
- polarizer (direct detection, where it adds ASE-ASE beat noise); model
54
- such a link with both polarizations.
55
-
56
- Parameters
57
- ----------
58
- samples : array_like or Signal
59
- Input samples, time on the last axis; for ``osnr_db`` a complex
60
- optical Signal (``domain="OPT"``), ``(N,)`` or ``(C, N)`` with one
61
- channel per polarization.
62
- esn0_db : float, optional
63
- Symbol energy over noise spectral density, Es/N0, in dB.
64
- osnr_db : float, optional
65
- Optical signal-to-noise ratio in dB in ``reference_bandwidth``,
66
- instead of ``esn0_db``.
67
- sps : float, optional
68
- Samples per symbol, for ``esn0_db``. Taken from the Signal; required
69
- for array input. A value that disagrees with the Signal raises.
70
- signal_power : float, optional
71
- Signal power per channel for the noise scaling. Defaults to the mean
72
- power of ``samples`` over all channels, which counts any silent
73
- stretch. Pass the nominal power instead when that is the reference:
74
- e.g. ``signal_power=1.0`` for a unit-power constellation sent in
75
- bursts separated by zero guard intervals, whose measured mean falls
76
- below the power of the symbols.
77
- reference_bandwidth : float, default 12.5e9
78
- Noise reference bandwidth ``B_ref`` of ``osnr_db`` in Hz. The
79
- standard reference is 0.1 nm of optical bandwidth, ``c · 0.1 nm /
80
- λ²``: 12.5 GHz in the C band (12.48 GHz at 1550 nm), 17.5 GHz at
81
- 1310 nm. Outside the C band pass the value for the carrier, e.g.
82
- ``reference_bandwidth=17.5e9`` for an O-band OSNR in 0.1 nm.
83
- rng : int, numpy.random.Generator or None
84
- Random source. The noise is drawn on the data's device: from this
85
- Generator on the CPU, and on the GPU from a CuPy Generator seeded from
86
- it, so a seed is reproducible per device.
87
-
88
- Returns
89
- -------
90
- ndarray or Signal
91
- The noisy samples, same shape, dtype and device as the input.
92
-
93
- Raises
94
- ------
95
- ValueError
96
- If neither or both of ``esn0_db`` and ``osnr_db`` are given,
97
- ``osnr_db`` is given for anything but a complex optical Signal, or
98
- ``reference_bandwidth`` is not positive.
99
-
100
- Notes
101
- -----
102
- Complex input gets circular complex noise, its variance split equally
103
- between I and Q; real input gets real noise of the full variance. At
104
- ``sps = 1`` Es/N0 equals the per-sample SNR.
105
-
106
- Examples
107
- --------
108
- >>> sig = generate(Constellation.pam(4), 1000, symbol_rate=1e6, sps=4)
109
- >>> noisy = apply_awgn(sig.samples, esn0_db=20, sps=4)
110
- >>> noisy = apply_awgn(sig, esn0_db=20) # Signal input: sps taken from sig
111
- >>> field = sig.replace(domain="OPT", center_frequency=193.4e12)
112
- >>> noisy = apply_awgn(field, osnr_db=18) # in 12.5 GHz, both polarizations
113
- """
114
- signal_adapter = adapt_signal(samples, function_name="apply_awgn()")
115
- if (esn0_db is None) == (osnr_db is None):
116
- raise ValueError("apply_awgn(): give one of esn0_db and osnr_db.")
117
- x, xp, _ = dispatch(signal_adapter.array)
118
- if signal_power is None:
119
- signal_power = xp.mean(xp.abs(x) ** 2)
120
- if osnr_db is not None:
121
- sig = signal_adapter.signal
122
- if sig is None or sig.domain != "OPT" or not xp.iscomplexobj(x):
123
- raise ValueError(
124
- "apply_awgn(): osnr_db needs a complex optical Signal "
125
- "(domain='OPT'), whose sampling rate sets the noise bandwidth."
126
- )
127
- if not reference_bandwidth > 0:
128
- raise ValueError(
129
- "apply_awgn(): reference_bandwidth must be positive (Hz), got "
130
- f"{reference_bandwidth!r}."
131
- )
132
- logger.info("Adding ASE noise (OSNR target: %.2f dB).", osnr_db)
133
- num_channels = 1 if x.ndim == 1 else x.shape[0]
134
- total_power = num_channels * signal_power
135
- noise_power = (
136
- total_power
137
- * sig.sampling_rate
138
- / (2.0 * reference_bandwidth * db_to_linear(osnr_db, power=True))
139
- )
140
- return signal_adapter.wrap_samples(x + _noise(x, xp, noise_power, rng))
141
-
142
- sps = signal_adapter.resolve_fact("sps", sps)
143
- logger.info("Adding AWGN (Es/N0 target: %.2f dB).", esn0_db)
144
-
145
- # === Es/N0 to sample-level SNR conversion ===
146
- #
147
- # Es/N0 = Symbol Energy / Noise Spectral Density
148
- #
149
- # For oversampled signals:
150
- # - Symbol energy Es = sps * (average sample power) [sum over sps samples]
151
- # - Noise power in full bandwidth = N0 * fs = N0 * (sps * symbol_rate)
152
- # - Noise power per sample = N0 * symbol_rate
153
- #
154
- # Therefore:
155
- # Es/N0 = (sps * P_sample) / (N0 * symbol_rate)
156
- # Sample_SNR = P_sample / P_noise_per_sample = P_sample / (N0 * symbol_rate)
157
- #
158
- # Relationship: Sample_SNR = Es/N0 / sps
159
- #
160
- # Or equivalently: P_noise = P_signal * sps / Es_N0_linear
161
-
162
- esn0_linear = db_to_linear(esn0_db, power=True)
163
-
164
- # Noise power accounting for oversampling
165
- if esn0_linear <= 1e-20:
166
- noise_power = signal_power * sps / 1e-20
167
- else:
168
- noise_power = signal_power * sps / esn0_linear
169
-
170
- return signal_adapter.wrap_samples(x + _noise(x, xp, noise_power, rng))
171
-
172
-
173
- def _noise(x: Any, xp: Any, noise_power: Any, rng: RNG) -> Any:
174
- """White Gaussian noise of ``noise_power`` per sample, shaped like ``x``.
175
-
176
- Complex noise splits its power equally between I and Q.
177
- """
178
- gen = as_generator(rng)
179
- real_dtype = x.real.dtype if x.real.dtype.kind == "f" else xp.dtype(xp.float64)
180
- if xp.iscomplexobj(x):
181
- std = xp.sqrt(noise_power / 2).astype(real_dtype)
182
- return complex_normal(gen, x.shape, std=std, dtype=real_dtype, xp=xp)
183
- std = xp.sqrt(noise_power).astype(real_dtype)
184
- return std * standard_normal(gen, x.shape, dtype=real_dtype, xp=xp)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes