commkit 3.1.2__tar.gz → 3.1.3__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 (285) hide show
  1. {commkit-3.1.2 → commkit-3.1.3}/CHANGELOG.md +21 -0
  2. {commkit-3.1.2 → commkit-3.1.3}/PKG-INFO +2 -2
  3. {commkit-3.1.2 → commkit-3.1.3}/README.md +1 -1
  4. {commkit-3.1.2 → commkit-3.1.3}/commkit/__init__.py +1 -1
  5. {commkit-3.1.2 → commkit-3.1.3}/commkit/impairments/__init__.py +4 -2
  6. commkit-3.1.3/commkit/impairments/noise.py +283 -0
  7. {commkit-3.1.2 → commkit-3.1.3}/examples/06-awgn-and-performance-metrics.ipynb +59 -10
  8. {commkit-3.1.2 → commkit-3.1.3}/examples/11-frame-synchronization-and-timing.ipynb +5 -5
  9. {commkit-3.1.2 → commkit-3.1.3}/examples/15-linear-channel-equalization.ipynb +3 -2
  10. {commkit-3.1.2 → commkit-3.1.3}/examples/16-adaptive-equalization.ipynb +8 -4
  11. {commkit-3.1.2 → commkit-3.1.3}/examples/22-coherent-transceiver-end-to-end.ipynb +3 -3
  12. {commkit-3.1.2 → commkit-3.1.3}/examples/26-laser-linewidth-self-homodyne-iq.ipynb +3 -5
  13. {commkit-3.1.2 → commkit-3.1.3}/pyproject.toml +2 -2
  14. commkit-3.1.3/tests/impairments/test_noise.py +299 -0
  15. {commkit-3.1.2 → commkit-3.1.3}/tests/test_api_contracts.py +2 -0
  16. {commkit-3.1.2 → commkit-3.1.3}/uv.lock +1 -1
  17. commkit-3.1.2/commkit/impairments/noise.py +0 -229
  18. commkit-3.1.2/tests/impairments/test_noise.py +0 -244
  19. {commkit-3.1.2 → commkit-3.1.3}/.gitattributes +0 -0
  20. {commkit-3.1.2 → commkit-3.1.3}/.github/workflows/ci.yml +0 -0
  21. {commkit-3.1.2 → commkit-3.1.3}/.github/workflows/publish.yml +0 -0
  22. {commkit-3.1.2 → commkit-3.1.3}/.gitignore +0 -0
  23. {commkit-3.1.2 → commkit-3.1.3}/.python-version +0 -0
  24. {commkit-3.1.2 → commkit-3.1.3}/AGENTS.md +0 -0
  25. {commkit-3.1.2 → commkit-3.1.3}/LICENSE +0 -0
  26. {commkit-3.1.2 → commkit-3.1.3}/benchmarks/baselines/Linux-CPython-3.14-64bit/0002_pre_v2.json +0 -0
  27. {commkit-3.1.2 → commkit-3.1.3}/benchmarks/baselines/Linux-CPython-3.14-64bit/0003_v2_0.json +0 -0
  28. {commkit-3.1.2 → commkit-3.1.3}/benchmarks/baselines/Linux-CPython-3.14-64bit/0004_fiber_dbp.json +0 -0
  29. {commkit-3.1.2 → commkit-3.1.3}/benchmarks/baselines/Linux-CPython-3.14-64bit/0005_gpu_decoder_path.json +0 -0
  30. {commkit-3.1.2 → commkit-3.1.3}/benchmarks/baselines/Linux-CPython-3.14-64bit/0006_perf_cleanup.json +0 -0
  31. {commkit-3.1.2 → commkit-3.1.3}/benchmarks/baselines/Linux-CPython-3.14-64bit/0007_v3_0.json +0 -0
  32. {commkit-3.1.2 → commkit-3.1.3}/benchmarks/baselines/Linux-CPython-3.14-64bit/0008_v3_1.json +0 -0
  33. {commkit-3.1.2 → commkit-3.1.3}/benchmarks/bench_block_blind.py +0 -0
  34. {commkit-3.1.2 → commkit-3.1.3}/benchmarks/bench_block_lms.py +0 -0
  35. {commkit-3.1.2 → commkit-3.1.3}/benchmarks/bench_bps.py +0 -0
  36. {commkit-3.1.2 → commkit-3.1.3}/benchmarks/bench_coding.py +0 -0
  37. {commkit-3.1.2 → commkit-3.1.3}/benchmarks/bench_equalizers.py +0 -0
  38. {commkit-3.1.2 → commkit-3.1.3}/benchmarks/bench_fiber.py +0 -0
  39. {commkit-3.1.2 → commkit-3.1.3}/benchmarks/bench_filters.py +0 -0
  40. {commkit-3.1.2 → commkit-3.1.3}/benchmarks/bench_import.py +0 -0
  41. {commkit-3.1.2 → commkit-3.1.3}/benchmarks/bench_llr.py +0 -0
  42. {commkit-3.1.2 → commkit-3.1.3}/benchmarks/bench_mapping.py +0 -0
  43. {commkit-3.1.2 → commkit-3.1.3}/benchmarks/bench_signal_pipeline.py +0 -0
  44. {commkit-3.1.2 → commkit-3.1.3}/benchmarks/bench_sync_misc.py +0 -0
  45. {commkit-3.1.2 → commkit-3.1.3}/benchmarks/benchutils.py +0 -0
  46. {commkit-3.1.2 → commkit-3.1.3}/benchmarks/conftest.py +0 -0
  47. {commkit-3.1.2 → commkit-3.1.3}/benchmarks/record_baseline.py +0 -0
  48. {commkit-3.1.2 → commkit-3.1.3}/benchmarks/workloads.py +0 -0
  49. {commkit-3.1.2 → commkit-3.1.3}/commkit/_array.py +0 -0
  50. {commkit-3.1.2 → commkit-3.1.3}/commkit/_circular.py +0 -0
  51. {commkit-3.1.2 → commkit-3.1.3}/commkit/_constants.py +0 -0
  52. {commkit-3.1.2 → commkit-3.1.3}/commkit/_cuda/__init__.py +0 -0
  53. {commkit-3.1.2 → commkit-3.1.3}/commkit/_cuda/compiler.py +0 -0
  54. {commkit-3.1.2 → commkit-3.1.3}/commkit/_cuda/graphs.py +0 -0
  55. {commkit-3.1.2 → commkit-3.1.3}/commkit/_cuda/src/algebraic.cu +0 -0
  56. {commkit-3.1.2 → commkit-3.1.3}/commkit/_cuda/src/bps_anchor.cu +0 -0
  57. {commkit-3.1.2 → commkit-3.1.3}/commkit/_cuda/src/bps_min_d2.cu +0 -0
  58. {commkit-3.1.2 → commkit-3.1.3}/commkit/_cuda/src/cs_block.cu +0 -0
  59. {commkit-3.1.2 → commkit-3.1.3}/commkit/_cuda/src/iir.cu +0 -0
  60. {commkit-3.1.2 → commkit-3.1.3}/commkit/_cuda/src/ldpc.cu +0 -0
  61. {commkit-3.1.2 → commkit-3.1.3}/commkit/_cuda/src/llr.cu +0 -0
  62. {commkit-3.1.2 → commkit-3.1.3}/commkit/_cuda/src/selftest.cu +0 -0
  63. {commkit-3.1.2 → commkit-3.1.3}/commkit/_dispersion.py +0 -0
  64. {commkit-3.1.2 → commkit-3.1.3}/commkit/_fft.py +0 -0
  65. {commkit-3.1.2 → commkit-3.1.3}/commkit/_fiber.py +0 -0
  66. {commkit-3.1.2 → commkit-3.1.3}/commkit/_overlap_save.py +0 -0
  67. {commkit-3.1.2 → commkit-3.1.3}/commkit/_random.py +0 -0
  68. {commkit-3.1.2 → commkit-3.1.3}/commkit/_sequences.py +0 -0
  69. {commkit-3.1.2 → commkit-3.1.3}/commkit/analysis/__init__.py +0 -0
  70. {commkit-3.1.2 → commkit-3.1.3}/commkit/analysis/_common.py +0 -0
  71. {commkit-3.1.2 → commkit-3.1.3}/commkit/analysis/allan.py +0 -0
  72. {commkit-3.1.2 → commkit-3.1.3}/commkit/analysis/drift.py +0 -0
  73. {commkit-3.1.2 → commkit-3.1.3}/commkit/analysis/fm_noise.py +0 -0
  74. {commkit-3.1.2 → commkit-3.1.3}/commkit/analysis/interferometry.py +0 -0
  75. {commkit-3.1.2 → commkit-3.1.3}/commkit/analysis/linewidth/__init__.py +0 -0
  76. {commkit-3.1.2 → commkit-3.1.3}/commkit/analysis/linewidth/dsh.py +0 -0
  77. {commkit-3.1.2 → commkit-3.1.3}/commkit/analysis/linewidth/phase.py +0 -0
  78. {commkit-3.1.2 → commkit-3.1.3}/commkit/analysis/linewidth/result.py +0 -0
  79. {commkit-3.1.2 → commkit-3.1.3}/commkit/analysis/trajectory.py +0 -0
  80. {commkit-3.1.2 → commkit-3.1.3}/commkit/backend.py +0 -0
  81. {commkit-3.1.2 → commkit-3.1.3}/commkit/coding/__init__.py +0 -0
  82. {commkit-3.1.2 → commkit-3.1.3}/commkit/coding/_galois.py +0 -0
  83. {commkit-3.1.2 → commkit-3.1.3}/commkit/coding/_gf2.py +0 -0
  84. {commkit-3.1.2 → commkit-3.1.3}/commkit/coding/_ldpc_tables.py +0 -0
  85. {commkit-3.1.2 → commkit-3.1.3}/commkit/coding/_numba_loops.py +0 -0
  86. {commkit-3.1.2 → commkit-3.1.3}/commkit/coding/algebraic.py +0 -0
  87. {commkit-3.1.2 → commkit-3.1.3}/commkit/coding/base.py +0 -0
  88. {commkit-3.1.2 → commkit-3.1.3}/commkit/coding/convolutional.py +0 -0
  89. {commkit-3.1.2 → commkit-3.1.3}/commkit/coding/crc.py +0 -0
  90. {commkit-3.1.2 → commkit-3.1.3}/commkit/coding/interleaving.py +0 -0
  91. {commkit-3.1.2 → commkit-3.1.3}/commkit/coding/ldpc.py +0 -0
  92. {commkit-3.1.2 → commkit-3.1.3}/commkit/coding/linear.py +0 -0
  93. {commkit-3.1.2 → commkit-3.1.3}/commkit/coding/polar.py +0 -0
  94. {commkit-3.1.2 → commkit-3.1.3}/commkit/coding/product.py +0 -0
  95. {commkit-3.1.2 → commkit-3.1.3}/commkit/coding/ratematch.py +0 -0
  96. {commkit-3.1.2 → commkit-3.1.3}/commkit/coding/shaping.py +0 -0
  97. {commkit-3.1.2 → commkit-3.1.3}/commkit/coding/turbo.py +0 -0
  98. {commkit-3.1.2 → commkit-3.1.3}/commkit/core/__init__.py +0 -0
  99. {commkit-3.1.2 → commkit-3.1.3}/commkit/core/_signal_adapter.py +0 -0
  100. {commkit-3.1.2 → commkit-3.1.3}/commkit/core/frame.py +0 -0
  101. {commkit-3.1.2 → commkit-3.1.3}/commkit/core/generation.py +0 -0
  102. {commkit-3.1.2 → commkit-3.1.3}/commkit/core/signal.py +0 -0
  103. {commkit-3.1.2 → commkit-3.1.3}/commkit/equalization/__init__.py +0 -0
  104. {commkit-3.1.2 → commkit-3.1.3}/commkit/equalization/_block/__init__.py +0 -0
  105. {commkit-3.1.2 → commkit-3.1.3}/commkit/equalization/_block/_blind.py +0 -0
  106. {commkit-3.1.2 → commkit-3.1.3}/commkit/equalization/_block/_dd.py +0 -0
  107. {commkit-3.1.2 → commkit-3.1.3}/commkit/equalization/_block/_engine.py +0 -0
  108. {commkit-3.1.2 → commkit-3.1.3}/commkit/equalization/_common.py +0 -0
  109. {commkit-3.1.2 → commkit-3.1.3}/commkit/equalization/_kernels_numba.py +0 -0
  110. {commkit-3.1.2 → commkit-3.1.3}/commkit/equalization/_numba_loops.py +0 -0
  111. {commkit-3.1.2 → commkit-3.1.3}/commkit/equalization/blind.py +0 -0
  112. {commkit-3.1.2 → commkit-3.1.3}/commkit/equalization/linear.py +0 -0
  113. {commkit-3.1.2 → commkit-3.1.3}/commkit/equalization/pilots.py +0 -0
  114. {commkit-3.1.2 → commkit-3.1.3}/commkit/equalization/polarization.py +0 -0
  115. {commkit-3.1.2 → commkit-3.1.3}/commkit/equalization/result.py +0 -0
  116. {commkit-3.1.2 → commkit-3.1.3}/commkit/equalization/sequential/__init__.py +0 -0
  117. {commkit-3.1.2 → commkit-3.1.3}/commkit/equalization/sequential/_blind.py +0 -0
  118. {commkit-3.1.2 → commkit-3.1.3}/commkit/equalization/sequential/_dd.py +0 -0
  119. {commkit-3.1.2 → commkit-3.1.3}/commkit/equalization/sequential/_setup.py +0 -0
  120. {commkit-3.1.2 → commkit-3.1.3}/commkit/filtering/__init__.py +0 -0
  121. {commkit-3.1.2 → commkit-3.1.3}/commkit/filtering/fiber.py +0 -0
  122. {commkit-3.1.2 → commkit-3.1.3}/commkit/filtering/linear.py +0 -0
  123. {commkit-3.1.2 → commkit-3.1.3}/commkit/filtering/pulses.py +0 -0
  124. {commkit-3.1.2 → commkit-3.1.3}/commkit/frequency.py +0 -0
  125. {commkit-3.1.2 → commkit-3.1.3}/commkit/impairments/channel/__init__.py +0 -0
  126. {commkit-3.1.2 → commkit-3.1.3}/commkit/impairments/channel/linear.py +0 -0
  127. {commkit-3.1.2 → commkit-3.1.3}/commkit/impairments/channel/nonlinear.py +0 -0
  128. {commkit-3.1.2 → commkit-3.1.3}/commkit/impairments/frontend.py +0 -0
  129. {commkit-3.1.2 → commkit-3.1.3}/commkit/impairments/source.py +0 -0
  130. {commkit-3.1.2 → commkit-3.1.3}/commkit/io.py +0 -0
  131. {commkit-3.1.2 → commkit-3.1.3}/commkit/logger.py +0 -0
  132. {commkit-3.1.2 → commkit-3.1.3}/commkit/mapping/__init__.py +0 -0
  133. {commkit-3.1.2 → commkit-3.1.3}/commkit/mapping/_numba_llr.py +0 -0
  134. {commkit-3.1.2 → commkit-3.1.3}/commkit/mapping/bits.py +0 -0
  135. {commkit-3.1.2 → commkit-3.1.3}/commkit/mapping/constellation.py +0 -0
  136. {commkit-3.1.2 → commkit-3.1.3}/commkit/mapping/gray.py +0 -0
  137. {commkit-3.1.2 → commkit-3.1.3}/commkit/mapping/llr.py +0 -0
  138. {commkit-3.1.2 → commkit-3.1.3}/commkit/mapping/shaping.py +0 -0
  139. {commkit-3.1.2 → commkit-3.1.3}/commkit/math.py +0 -0
  140. {commkit-3.1.2 → commkit-3.1.3}/commkit/metrics.py +0 -0
  141. {commkit-3.1.2 → commkit-3.1.3}/commkit/multirate.py +0 -0
  142. {commkit-3.1.2 → commkit-3.1.3}/commkit/plotting/__init__.py +0 -0
  143. {commkit-3.1.2 → commkit-3.1.3}/commkit/plotting/analysis.py +0 -0
  144. {commkit-3.1.2 → commkit-3.1.3}/commkit/plotting/constellation.py +0 -0
  145. {commkit-3.1.2 → commkit-3.1.3}/commkit/plotting/equalizer.py +0 -0
  146. {commkit-3.1.2 → commkit-3.1.3}/commkit/plotting/eye.py +0 -0
  147. {commkit-3.1.2 → commkit-3.1.3}/commkit/plotting/filter_response.py +0 -0
  148. {commkit-3.1.2 → commkit-3.1.3}/commkit/plotting/spectral.py +0 -0
  149. {commkit-3.1.2 → commkit-3.1.3}/commkit/plotting/sync.py +0 -0
  150. {commkit-3.1.2 → commkit-3.1.3}/commkit/plotting/theme.py +0 -0
  151. {commkit-3.1.2 → commkit-3.1.3}/commkit/plotting/waveform.py +0 -0
  152. {commkit-3.1.2 → commkit-3.1.3}/commkit/py.typed +0 -0
  153. {commkit-3.1.2 → commkit-3.1.3}/commkit/recovery/__init__.py +0 -0
  154. {commkit-3.1.2 → commkit-3.1.3}/commkit/recovery/_common.py +0 -0
  155. {commkit-3.1.2 → commkit-3.1.3}/commkit/recovery/bps.py +0 -0
  156. {commkit-3.1.2 → commkit-3.1.3}/commkit/recovery/carrier_phase.py +0 -0
  157. {commkit-3.1.2 → commkit-3.1.3}/commkit/recovery/corrections.py +0 -0
  158. {commkit-3.1.2 → commkit-3.1.3}/commkit/recovery/pilots.py +0 -0
  159. {commkit-3.1.2 → commkit-3.1.3}/commkit/recovery/pll.py +0 -0
  160. {commkit-3.1.2 → commkit-3.1.3}/commkit/recovery/tikhonov.py +0 -0
  161. {commkit-3.1.2 → commkit-3.1.3}/commkit/recovery/viterbi_viterbi.py +0 -0
  162. {commkit-3.1.2 → commkit-3.1.3}/commkit/smoothing.py +0 -0
  163. {commkit-3.1.2 → commkit-3.1.3}/commkit/spectral.py +0 -0
  164. {commkit-3.1.2 → commkit-3.1.3}/commkit/timing.py +0 -0
  165. {commkit-3.1.2 → commkit-3.1.3}/examples/00-getting-started.ipynb +0 -0
  166. {commkit-3.1.2 → commkit-3.1.3}/examples/01-constellations-and-bit-mapping.ipynb +0 -0
  167. {commkit-3.1.2 → commkit-3.1.3}/examples/02-spectral-analysis.ipynb +0 -0
  168. {commkit-3.1.2 → commkit-3.1.3}/examples/03-digital-filters.ipynb +0 -0
  169. {commkit-3.1.2 → commkit-3.1.3}/examples/04-pulse-shaping-and-matched-filtering.ipynb +0 -0
  170. {commkit-3.1.2 → commkit-3.1.3}/examples/05-multirate-and-resampling.ipynb +0 -0
  171. {commkit-3.1.2 → commkit-3.1.3}/examples/07-channel-coding.ipynb +0 -0
  172. {commkit-3.1.2 → commkit-3.1.3}/examples/08-probabilistic-shaping-and-coded-modulation.ipynb +0 -0
  173. {commkit-3.1.2 → commkit-3.1.3}/examples/09-laser-phase-noise.ipynb +0 -0
  174. {commkit-3.1.2 → commkit-3.1.3}/examples/10-front-end-impairments.ipynb +0 -0
  175. {commkit-3.1.2 → commkit-3.1.3}/examples/12-frequency-offset-estimation.ipynb +0 -0
  176. {commkit-3.1.2 → commkit-3.1.3}/examples/13-blind-carrier-phase-recovery.ipynb +0 -0
  177. {commkit-3.1.2 → commkit-3.1.3}/examples/14-pilot-aided-carrier-recovery.ipynb +0 -0
  178. {commkit-3.1.2 → commkit-3.1.3}/examples/17-blind-equalization.ipynb +0 -0
  179. {commkit-3.1.2 → commkit-3.1.3}/examples/18-polarization-multiplexing.ipynb +0 -0
  180. {commkit-3.1.2 → commkit-3.1.3}/examples/19-chromatic-dispersion.ipynb +0 -0
  181. {commkit-3.1.2 → commkit-3.1.3}/examples/20-nonlinear-fiber-and-backpropagation.ipynb +0 -0
  182. {commkit-3.1.2 → commkit-3.1.3}/examples/21-fec-in-optical-standards.ipynb +0 -0
  183. {commkit-3.1.2 → commkit-3.1.3}/examples/23-carrier-phase-characterization.ipynb +0 -0
  184. {commkit-3.1.2 → commkit-3.1.3}/examples/24-laser-linewidth-self-heterodyne.ipynb +0 -0
  185. {commkit-3.1.2 → commkit-3.1.3}/examples/25-laser-linewidth-short-delay-and-flicker.ipynb +0 -0
  186. {commkit-3.1.2 → commkit-3.1.3}/examples/27-measurement-template-self-heterodyne.ipynb +0 -0
  187. {commkit-3.1.2 → commkit-3.1.3}/examples/28-measurement-template-self-homodyne-iq.ipynb +0 -0
  188. {commkit-3.1.2 → commkit-3.1.3}/examples/README.md +0 -0
  189. {commkit-3.1.2 → commkit-3.1.3}/tests/analysis/test_allan.py +0 -0
  190. {commkit-3.1.2 → commkit-3.1.3}/tests/analysis/test_drift.py +0 -0
  191. {commkit-3.1.2 → commkit-3.1.3}/tests/analysis/test_fm_noise.py +0 -0
  192. {commkit-3.1.2 → commkit-3.1.3}/tests/analysis/test_interferometry.py +0 -0
  193. {commkit-3.1.2 → commkit-3.1.3}/tests/analysis/test_linewidth.py +0 -0
  194. {commkit-3.1.2 → commkit-3.1.3}/tests/analysis/test_trajectory.py +0 -0
  195. {commkit-3.1.2 → commkit-3.1.3}/tests/coding/test_algebraic.py +0 -0
  196. {commkit-3.1.2 → commkit-3.1.3}/tests/coding/test_coding_base.py +0 -0
  197. {commkit-3.1.2 → commkit-3.1.3}/tests/coding/test_convolutional.py +0 -0
  198. {commkit-3.1.2 → commkit-3.1.3}/tests/coding/test_crc.py +0 -0
  199. {commkit-3.1.2 → commkit-3.1.3}/tests/coding/test_gf2.py +0 -0
  200. {commkit-3.1.2 → commkit-3.1.3}/tests/coding/test_interleaving.py +0 -0
  201. {commkit-3.1.2 → commkit-3.1.3}/tests/coding/test_ldpc.py +0 -0
  202. {commkit-3.1.2 → commkit-3.1.3}/tests/coding/test_ldpc_kernel.py +0 -0
  203. {commkit-3.1.2 → commkit-3.1.3}/tests/coding/test_ldpc_tables.py +0 -0
  204. {commkit-3.1.2 → commkit-3.1.3}/tests/coding/test_linear_codes.py +0 -0
  205. {commkit-3.1.2 → commkit-3.1.3}/tests/coding/test_polar.py +0 -0
  206. {commkit-3.1.2 → commkit-3.1.3}/tests/coding/test_product.py +0 -0
  207. {commkit-3.1.2 → commkit-3.1.3}/tests/coding/test_ratematch.py +0 -0
  208. {commkit-3.1.2 → commkit-3.1.3}/tests/coding/test_shaping.py +0 -0
  209. {commkit-3.1.2 → commkit-3.1.3}/tests/coding/test_turbo.py +0 -0
  210. {commkit-3.1.2 → commkit-3.1.3}/tests/common/__init__.py +0 -0
  211. {commkit-3.1.2 → commkit-3.1.3}/tests/common/coding.py +0 -0
  212. {commkit-3.1.2 → commkit-3.1.3}/tests/common/conversions.py +0 -0
  213. {commkit-3.1.2 → commkit-3.1.3}/tests/common/kernel_utils.py +0 -0
  214. {commkit-3.1.2 → commkit-3.1.3}/tests/common/memory.py +0 -0
  215. {commkit-3.1.2 → commkit-3.1.3}/tests/common/metrics.py +0 -0
  216. {commkit-3.1.2 → commkit-3.1.3}/tests/common/periodic.py +0 -0
  217. {commkit-3.1.2 → commkit-3.1.3}/tests/common/reference_impl.py +0 -0
  218. {commkit-3.1.2 → commkit-3.1.3}/tests/common/signals.py +0 -0
  219. {commkit-3.1.2 → commkit-3.1.3}/tests/conftest.py +0 -0
  220. {commkit-3.1.2 → commkit-3.1.3}/tests/core/test_frame.py +0 -0
  221. {commkit-3.1.2 → commkit-3.1.3}/tests/core/test_generation.py +0 -0
  222. {commkit-3.1.2 → commkit-3.1.3}/tests/core/test_psqam.py +0 -0
  223. {commkit-3.1.2 → commkit-3.1.3}/tests/core/test_signal.py +0 -0
  224. {commkit-3.1.2 → commkit-3.1.3}/tests/core/test_signal_adapter.py +0 -0
  225. {commkit-3.1.2 → commkit-3.1.3}/tests/core/test_signal_mimo.py +0 -0
  226. {commkit-3.1.2 → commkit-3.1.3}/tests/equalization/test_blind.py +0 -0
  227. {commkit-3.1.2 → commkit-3.1.3}/tests/equalization/test_block.py +0 -0
  228. {commkit-3.1.2 → commkit-3.1.3}/tests/equalization/test_bps_kernel.py +0 -0
  229. {commkit-3.1.2 → commkit-3.1.3}/tests/equalization/test_cpr.py +0 -0
  230. {commkit-3.1.2 → commkit-3.1.3}/tests/equalization/test_cs_kernel.py +0 -0
  231. {commkit-3.1.2 → commkit-3.1.3}/tests/equalization/test_equalizer_oracles.py +0 -0
  232. {commkit-3.1.2 → commkit-3.1.3}/tests/equalization/test_linear.py +0 -0
  233. {commkit-3.1.2 → commkit-3.1.3}/tests/equalization/test_mimo.py +0 -0
  234. {commkit-3.1.2 → commkit-3.1.3}/tests/equalization/test_polarization.py +0 -0
  235. {commkit-3.1.2 → commkit-3.1.3}/tests/equalization/test_sequential.py +0 -0
  236. {commkit-3.1.2 → commkit-3.1.3}/tests/equalization/test_state.py +0 -0
  237. {commkit-3.1.2 → commkit-3.1.3}/tests/equalization/test_winit.py +0 -0
  238. {commkit-3.1.2 → commkit-3.1.3}/tests/impairments/channel/test_channel_linear.py +0 -0
  239. {commkit-3.1.2 → commkit-3.1.3}/tests/impairments/channel/test_channel_nonlinear.py +0 -0
  240. {commkit-3.1.2 → commkit-3.1.3}/tests/impairments/test_frontend.py +0 -0
  241. {commkit-3.1.2 → commkit-3.1.3}/tests/impairments/test_source.py +0 -0
  242. {commkit-3.1.2 → commkit-3.1.3}/tests/mapping/test_bits.py +0 -0
  243. {commkit-3.1.2 → commkit-3.1.3}/tests/mapping/test_constellation.py +0 -0
  244. {commkit-3.1.2 → commkit-3.1.3}/tests/mapping/test_gray.py +0 -0
  245. {commkit-3.1.2 → commkit-3.1.3}/tests/mapping/test_llr.py +0 -0
  246. {commkit-3.1.2 → commkit-3.1.3}/tests/plotting/test_plot_analysis.py +0 -0
  247. {commkit-3.1.2 → commkit-3.1.3}/tests/plotting/test_plot_constellation.py +0 -0
  248. {commkit-3.1.2 → commkit-3.1.3}/tests/plotting/test_plot_equalizer.py +0 -0
  249. {commkit-3.1.2 → commkit-3.1.3}/tests/plotting/test_plot_eye.py +0 -0
  250. {commkit-3.1.2 → commkit-3.1.3}/tests/plotting/test_plot_filter_response.py +0 -0
  251. {commkit-3.1.2 → commkit-3.1.3}/tests/plotting/test_plot_spectral.py +0 -0
  252. {commkit-3.1.2 → commkit-3.1.3}/tests/plotting/test_plot_sync.py +0 -0
  253. {commkit-3.1.2 → commkit-3.1.3}/tests/plotting/test_plot_theme.py +0 -0
  254. {commkit-3.1.2 → commkit-3.1.3}/tests/plotting/test_plot_waveform.py +0 -0
  255. {commkit-3.1.2 → commkit-3.1.3}/tests/plugins/__init__.py +0 -0
  256. {commkit-3.1.2 → commkit-3.1.3}/tests/plugins/precision_trace.py +0 -0
  257. {commkit-3.1.2 → commkit-3.1.3}/tests/plugins/transfer_trace.py +0 -0
  258. {commkit-3.1.2 → commkit-3.1.3}/tests/recovery/test_bps.py +0 -0
  259. {commkit-3.1.2 → commkit-3.1.3}/tests/recovery/test_corrections.py +0 -0
  260. {commkit-3.1.2 → commkit-3.1.3}/tests/recovery/test_joint.py +0 -0
  261. {commkit-3.1.2 → commkit-3.1.3}/tests/recovery/test_pilots.py +0 -0
  262. {commkit-3.1.2 → commkit-3.1.3}/tests/recovery/test_pll.py +0 -0
  263. {commkit-3.1.2 → commkit-3.1.3}/tests/recovery/test_recovery_oracles.py +0 -0
  264. {commkit-3.1.2 → commkit-3.1.3}/tests/recovery/test_tikhonov.py +0 -0
  265. {commkit-3.1.2 → commkit-3.1.3}/tests/recovery/test_viterbi_viterbi.py +0 -0
  266. {commkit-3.1.2 → commkit-3.1.3}/tests/test_array.py +0 -0
  267. {commkit-3.1.2 → commkit-3.1.3}/tests/test_backend.py +0 -0
  268. {commkit-3.1.2 → commkit-3.1.3}/tests/test_cuda_infra.py +0 -0
  269. {commkit-3.1.2 → commkit-3.1.3}/tests/test_dispersion.py +0 -0
  270. {commkit-3.1.2 → commkit-3.1.3}/tests/test_examples.py +0 -0
  271. {commkit-3.1.2 → commkit-3.1.3}/tests/test_fiber.py +0 -0
  272. {commkit-3.1.2 → commkit-3.1.3}/tests/test_filtering.py +0 -0
  273. {commkit-3.1.2 → commkit-3.1.3}/tests/test_frequency.py +0 -0
  274. {commkit-3.1.2 → commkit-3.1.3}/tests/test_import_hygiene.py +0 -0
  275. {commkit-3.1.2 → commkit-3.1.3}/tests/test_io.py +0 -0
  276. {commkit-3.1.2 → commkit-3.1.3}/tests/test_logger.py +0 -0
  277. {commkit-3.1.2 → commkit-3.1.3}/tests/test_math.py +0 -0
  278. {commkit-3.1.2 → commkit-3.1.3}/tests/test_metrics.py +0 -0
  279. {commkit-3.1.2 → commkit-3.1.3}/tests/test_multirate.py +0 -0
  280. {commkit-3.1.2 → commkit-3.1.3}/tests/test_pulse_shaping.py +0 -0
  281. {commkit-3.1.2 → commkit-3.1.3}/tests/test_signal_pipeline.py +0 -0
  282. {commkit-3.1.2 → commkit-3.1.3}/tests/test_smoothing.py +0 -0
  283. {commkit-3.1.2 → commkit-3.1.3}/tests/test_spectral.py +0 -0
  284. {commkit-3.1.2 → commkit-3.1.3}/tests/test_timing.py +0 -0
  285. {commkit-3.1.2 → commkit-3.1.3}/tests/test_working_memory.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.1.3] - 2026-10-30
8
+
9
+ Additive noise without a scaling knob. `apply_awgn` either measures the signal or takes an absolute noise variance, and the OSNR moves to its own function, `apply_ase`.
10
+
11
+ ### Changes that may need code updates
12
+
13
+ - **`apply_awgn(signal_power=)` is removed.** Its uses each have a direct replacement:
14
+ - silence or zero padding around the signal: nothing to pass, as Es is now measured as energy per symbol (below);
15
+ - a fixed noise floor, noise referenced to the transmitter, or noise on a dark capture: `noise_var=`, the absolute noise variance per sample. For unit-energy symbols, `noise_var = 10 ** (-esn0_db / 10)` at any `sps`.
16
+ - **`apply_awgn(osnr_db=, reference_bandwidth=, noise_modes=)` is `apply_ase(osnr_db=, reference_bandwidth=, noise_modes=)`.** Same arguments and noise.
17
+
18
+ ### Changes that alter results
19
+
20
+ - **`apply_awgn(esn0_db=)` measures Es as energy per symbol**: each channel's energy over the number of symbols the record carries (a Signal's frame length without a zero guard, else its reference's symbol count, else `N / sps`). Zero padding and silence around a frame no longer lower the noise level; without silence the noise is as before. `apply_ase` measures the signal power the same way.
21
+
22
+ ### Added
23
+
24
+ - `apply_awgn(noise_var=)`, an absolute noise variance per sample, scalar or `(C,)`; exactly one of `esn0_db` and `noise_var` is given.
25
+ - `apply_awgn(per_channel=False)` gives every channel one noise level, from the mean symbol energy over channels, as identical receivers with one noise floor; the default gives each channel the requested Es/N0.
26
+ - Notebook 06 shows relative (`esn0_db`) against absolute (`noise_var`) noise.
27
+
7
28
  ## [3.1.2] - 2026-10-29
8
29
 
9
30
  `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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: commkit
3
- Version: 3.1.2
3
+ Version: 3.1.3
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
@@ -119,7 +119,7 @@ CommKit never picks a device for you: data stays where you put it, and each func
119
119
  | [`commkit.filtering`](https://github.com/lokgar/commkit/tree/main/commkit/filtering) | Pulses (`RRC`, `RC`, `Gaussian`, `Rect`, `SmoothRect`), FIR and IIR design (Butterworth, Chebyshev I/II, elliptic, Bessel), `fir_filter`, `iir_filter`, `matched_filter`, overlap-save, chromatic-dispersion compensation and digital backpropagation (`correct_fiber_propagation`). |
120
120
  | [`commkit.multirate`](https://github.com/lokgar/commkit/blob/main/commkit/multirate.py) | `resample` (fractional), `decimate`, `upsample`, `decimate_to_symbol_rate`. |
121
121
  | [`commkit.spectral`](https://github.com/lokgar/commkit/blob/main/commkit/spectral.py) | Welch PSD, spectrograms, frequency shifting, pilot tones. |
122
- | [`commkit.impairments`](https://github.com/lokgar/commkit/tree/main/commkit/impairments) | AWGN at an Es/N0 or an OSNR, 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`. |
122
+ | [`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`. |
123
123
  | [`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. |
124
124
  | [`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. |
125
125
  | [`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`. |
@@ -83,7 +83,7 @@ CommKit never picks a device for you: data stays where you put it, and each func
83
83
  | [`commkit.filtering`](https://github.com/lokgar/commkit/tree/main/commkit/filtering) | Pulses (`RRC`, `RC`, `Gaussian`, `Rect`, `SmoothRect`), FIR and IIR design (Butterworth, Chebyshev I/II, elliptic, Bessel), `fir_filter`, `iir_filter`, `matched_filter`, overlap-save, chromatic-dispersion compensation and digital backpropagation (`correct_fiber_propagation`). |
84
84
  | [`commkit.multirate`](https://github.com/lokgar/commkit/blob/main/commkit/multirate.py) | `resample` (fractional), `decimate`, `upsample`, `decimate_to_symbol_rate`. |
85
85
  | [`commkit.spectral`](https://github.com/lokgar/commkit/blob/main/commkit/spectral.py) | Welch PSD, spectrograms, frequency shifting, pilot tones. |
86
- | [`commkit.impairments`](https://github.com/lokgar/commkit/tree/main/commkit/impairments) | AWGN at an Es/N0 or an OSNR, 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`. |
86
+ | [`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`. |
87
87
  | [`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. |
88
88
  | [`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. |
89
89
  | [`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`. |
@@ -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.2"
34
+ __version__ = "3.1.3"
35
35
 
36
36
  from .backend import PrecisionWarning
37
37
  from .core import (
@@ -1,6 +1,7 @@
1
1
  """Channel impairments, grouped by where in the link they originate.
2
2
 
3
- - :mod:`~commkit.impairments.noise`: additive noise (:func:`apply_awgn`).
3
+ - :mod:`~commkit.impairments.noise`: additive noise (:func:`apply_awgn`) and
4
+ amplified spontaneous emission at an OSNR (:func:`apply_ase`).
4
5
  - :mod:`~commkit.impairments.source`: laser and oscillator phase noise
5
6
  (:func:`generate_phase_noise`, :func:`apply_phase_noise`).
6
7
  - :mod:`~commkit.impairments.frontend`: transceiver IQ imbalance
@@ -28,7 +29,7 @@ from .frontend import (
28
29
  correct_iq_imbalance,
29
30
  estimate_iq_imbalance,
30
31
  )
31
- from .noise import apply_awgn
32
+ from .noise import apply_ase, apply_awgn
32
33
  from .source import apply_phase_noise, generate_phase_noise
33
34
 
34
35
  __all__ = [
@@ -36,6 +37,7 @@ __all__ = [
36
37
  "GramSchmidt",
37
38
  "IQImbalanceEstimate",
38
39
  "Lowdin",
40
+ "apply_ase",
39
41
  "apply_awgn",
40
42
  "apply_chromatic_dispersion",
41
43
  "apply_fiber_propagation",
@@ -0,0 +1,283 @@
1
+ """Additive white Gaussian noise: :func:`apply_awgn`, and amplified
2
+ spontaneous emission at an OSNR: :func:`apply_ase`.
3
+
4
+ Powers are per sample and per channel, in the squared units of the samples.
5
+ The signal's symbol energy is measured as energy per symbol: the energy of
6
+ each channel over the number of symbols it carries, so silence around the
7
+ signal (zero padding, a capture before the frame arrives) lowers neither.
8
+ """
9
+
10
+ from typing import Any
11
+
12
+ from .._random import RNG, as_generator, complex_normal, standard_normal
13
+ from ..backend import ArrayType, dispatch
14
+ from ..core._signal_adapter import S, SignalAdapter, adapt_signal
15
+ from ..logger import logger
16
+ from ..math import db_to_linear
17
+
18
+ __all__ = ["apply_ase", "apply_awgn"]
19
+
20
+
21
+ def apply_awgn(
22
+ samples: S,
23
+ *,
24
+ esn0_db: float | None = None,
25
+ noise_var: float | ArrayType | None = None,
26
+ sps: float | None = None,
27
+ per_channel: bool = True,
28
+ rng: RNG = None,
29
+ ) -> S:
30
+ """Add white Gaussian noise, relative to the signal or of an absolute
31
+ variance.
32
+
33
+ Give exactly one of:
34
+
35
+ - ``esn0_db``, the noise *relative to the signal*: the symbol energy
36
+ ``Es`` is measured, and channel ``c`` gets noise of variance
37
+ ``σ_c² = Es_c / (Es/N0)`` per sample. Scaling the signal scales the
38
+ noise with it; the Es/N0 stays.
39
+ - ``noise_var``, the *absolute* noise variance ``σ² = E|n|²`` per sample,
40
+ in the squared units of the samples; nothing is measured. A fixed
41
+ noise floor, such as a receiver's thermal noise: scaling the signal
42
+ leaves the noise as it is, and the SNR changes.
43
+
44
+ The symbol energy is energy per symbol, ``Es_c = E_c / K`` with ``E_c =
45
+ Σ|x_c[n]|²`` the energy of channel ``c`` and ``K`` the number of symbols
46
+ the record carries: a Signal's frame length in symbols (without a zero
47
+ guard), else its reference's symbol count, else (and for arrays, or a
48
+ record shorter than that) ``N / sps``. Without silence this is
49
+ ``sps · mean(|x_c|²)``, the power summed over the ``sps`` samples of a
50
+ symbol; zero padding adds no energy and no symbols, so it changes
51
+ nothing. With ``Es`` in these units the noise density is ``N0 = σ²``,
52
+ the variance of one sample, and ``Es/N0`` is the SNR per symbol after a
53
+ matched filter and decimation, whatever the oversampling.
54
+
55
+ Parameters
56
+ ----------
57
+ samples : array_like or Signal
58
+ Input samples, time on the last axis, ``(N,)`` or ``(C, N)``. A
59
+ channel is anything with its own receiver noise: a polarization, an
60
+ antenna, a wire pair, a lane.
61
+ esn0_db : float, optional
62
+ Symbol energy over noise spectral density, Es/N0, in dB.
63
+ noise_var : float or array_like, optional
64
+ Noise variance ``E|n|²`` per sample: a scalar for every channel, or
65
+ ``(C,)`` per channel. Instead of ``esn0_db``.
66
+ sps : float, optional
67
+ Samples per symbol, for ``esn0_db``. Taken from the Signal; required
68
+ for array input. A value that disagrees with the Signal raises.
69
+ per_channel : bool, default True
70
+ For ``esn0_db``: ``True`` gives every channel the requested Es/N0
71
+ from its own symbol energy; ``False`` gives every channel the same
72
+ noise, from the mean symbol energy over channels, as identical
73
+ receivers with one noise floor (a channel that arrives weaker, e.g.
74
+ through polarization-dependent loss, then has a lower Es/N0).
75
+ rng : int, numpy.random.Generator or None
76
+ Random source. The noise is drawn on the data's device: from this
77
+ Generator on the CPU, and on the GPU from a CuPy Generator seeded from
78
+ it, so a seed is reproducible per device.
79
+
80
+ Returns
81
+ -------
82
+ ndarray or Signal
83
+ The noisy samples, same shape, dtype and device as the input.
84
+
85
+ Raises
86
+ ------
87
+ ValueError
88
+ If neither or both of ``esn0_db`` and ``noise_var`` are given, or a
89
+ per-channel ``noise_var`` does not have ``C`` entries.
90
+
91
+ Notes
92
+ -----
93
+ Complex input gets circular complex noise, its variance split equally
94
+ between I and Q; real input gets real noise of the full variance.
95
+
96
+ Examples
97
+ --------
98
+ >>> sig = generate(Constellation.pam(4), 1000, symbol_rate=1e6, sps=4)
99
+ >>> noisy = apply_awgn(sig, esn0_db=20) # sps taken from sig
100
+ >>> noisy = apply_awgn(sig.samples, esn0_db=20, sps=4)
101
+ >>> floor = apply_awgn(sig, noise_var=1e-3) # absolute, whatever the signal
102
+ """
103
+ signal_adapter = adapt_signal(samples, function_name="apply_awgn()")
104
+ if (esn0_db is None) == (noise_var is None):
105
+ raise ValueError("apply_awgn(): give one of esn0_db and noise_var.")
106
+ x, xp, _ = dispatch(signal_adapter.array)
107
+ num_channels = 1 if x.ndim == 1 else x.shape[0]
108
+
109
+ if noise_var is not None:
110
+ logger.info("Adding AWGN (noise variance given).")
111
+ var = xp.asarray(noise_var, dtype=xp.float64)
112
+ if var.ndim != 0:
113
+ if var.shape != (num_channels,):
114
+ raise ValueError(
115
+ "apply_awgn(): noise_var must be a scalar or have one entry "
116
+ f"per channel ({num_channels},), got shape {var.shape}."
117
+ )
118
+ var = var[:, None] if x.ndim == 2 else var
119
+ return signal_adapter.wrap_samples(x + _noise(x, xp, var, rng))
120
+
121
+ sps = signal_adapter.resolve_fact("sps", sps)
122
+ logger.info("Adding AWGN (Es/N0 target: %.2f dB).", esn0_db)
123
+ es = _symbol_energy(signal_adapter, x, xp, sps)
124
+ if not per_channel:
125
+ es = xp.mean(es)
126
+ esn0_linear = max(db_to_linear(esn0_db, power=True), 1e-20)
127
+ return signal_adapter.wrap_samples(x + _noise(x, xp, es / esn0_linear, rng))
128
+
129
+
130
+ def apply_ase(
131
+ samples: S,
132
+ *,
133
+ osnr_db: float,
134
+ reference_bandwidth: float = 12.5e9,
135
+ noise_modes: int | None = None,
136
+ rng: RNG = None,
137
+ ) -> S:
138
+ """Add amplified spontaneous emission at an optical signal-to-noise ratio.
139
+
140
+ OSNR is specified as optical links are: the total signal power of all
141
+ channels over the ASE power in ``reference_bandwidth``, summed over the
142
+ ``M = noise_modes`` modes the medium guides (polarizations included).
143
+ ASE has the same density in every mode and is white over the simulated
144
+ band ``f_s``, so complex noise of variance ``σ²`` per mode puts
145
+ ``M σ² B_ref / f_s`` into ``B_ref``, and every channel gets the same
146
+
147
+ σ² = P · f_s / (M · B_ref · OSNR),
148
+
149
+ with ``P = Σ_c Es_c / sps`` the total signal power while the signal is
150
+ present (the symbol energy as in :func:`apply_awgn`, so silence around
151
+ the signal does not lower it). For equal channel powers this gives
152
+ ``Es/N0 = M B_ref / (C R_s) · OSNR`` per channel, whatever the
153
+ oversampling: the textbook ``2 B_ref / (p R_s) · OSNR`` for ``p``
154
+ polarizations of a single-mode fiber.
155
+
156
+ Parameters
157
+ ----------
158
+ samples : Signal
159
+ A complex optical Signal (``domain="OPT"``), ``(N,)`` or ``(C, N)``
160
+ with one channel per guided mode it occupies (polarization, spatial
161
+ mode or core); its sampling rate sets the noise bandwidth.
162
+ osnr_db : float
163
+ Optical signal-to-noise ratio in dB in ``reference_bandwidth``.
164
+ reference_bandwidth : float, default 12.5e9
165
+ Noise reference bandwidth ``B_ref`` in Hz. The standard reference
166
+ is 0.1 nm of optical bandwidth, ``c · 0.1 nm / λ²``: 12.5 GHz in the
167
+ C band (12.48 GHz at 1550 nm), 17.5 GHz at 1310 nm. Outside the C
168
+ band pass the value for the carrier, e.g. ``17.5e9`` for an O-band
169
+ OSNR in 0.1 nm.
170
+ noise_modes : int, optional
171
+ The number ``M`` of guided modes, polarizations included, whose ASE
172
+ the OSNR counts. Defaults to ``max(C, 2)``: both polarizations of a
173
+ single-mode fiber for one or two channels (an OSNR meter's
174
+ definition; a single-polarization signal receives the half of the
175
+ ASE in its own polarization), and one mode per channel beyond that
176
+ (few-mode or multicore fiber with every mode carrying signal). Pass
177
+ it when the medium guides modes that carry no signal, e.g.
178
+ ``noise_modes=6`` for 4 channels in a 3-mode fiber.
179
+ rng : int, numpy.random.Generator or None
180
+ Random source, as in :func:`apply_awgn`.
181
+
182
+ Returns
183
+ -------
184
+ Signal
185
+ The noisy Signal, same shape, dtype and device.
186
+
187
+ Raises
188
+ ------
189
+ ValueError
190
+ If ``samples`` is not a complex optical Signal,
191
+ ``reference_bandwidth`` is not positive, or ``noise_modes`` is not
192
+ an integer of at least ``C``.
193
+
194
+ Notes
195
+ -----
196
+ OSNR is a property of the whole optical field, so pass every channel in
197
+ one call: calling once per polarization of a dual-polarization signal
198
+ counts only that polarization's power and adds half the intended
199
+ noise. Modes left out of a simulation carry no noise, which matters
200
+ for a receiver without a polarizer (direct detection, where the
201
+ orthogonal polarization adds ASE-ASE beat noise); model such a link
202
+ with both polarizations.
203
+
204
+ Examples
205
+ --------
206
+ >>> field = sig.replace(domain="OPT", center_frequency=193.4e12)
207
+ >>> noisy = apply_ase(field, osnr_db=18) # in 12.5 GHz, both polarizations
208
+ """
209
+ signal_adapter = adapt_signal(samples, function_name="apply_ase()")
210
+ x, xp, _ = dispatch(signal_adapter.array)
211
+ sig = signal_adapter.signal
212
+ if sig is None or sig.domain != "OPT" or not xp.iscomplexobj(x):
213
+ raise ValueError(
214
+ "apply_ase(): needs a complex optical Signal (domain='OPT'), whose "
215
+ "sampling rate sets the noise bandwidth."
216
+ )
217
+ if not reference_bandwidth > 0:
218
+ raise ValueError(
219
+ "apply_ase(): reference_bandwidth must be positive (Hz), got "
220
+ f"{reference_bandwidth!r}."
221
+ )
222
+ num_channels = 1 if x.ndim == 1 else x.shape[0]
223
+ if noise_modes is None:
224
+ noise_modes = max(num_channels, 2)
225
+ if (
226
+ isinstance(noise_modes, bool)
227
+ or not isinstance(noise_modes, int)
228
+ or noise_modes < num_channels
229
+ ):
230
+ raise ValueError(
231
+ f"apply_ase(): noise_modes must be an integer of at least the "
232
+ f"{num_channels} channels, got {noise_modes!r}."
233
+ )
234
+ logger.info(
235
+ "Adding ASE noise (OSNR target: %.2f dB, %d modes).", osnr_db, noise_modes
236
+ )
237
+ sps = float(sig.sps)
238
+ # The ASE density is the same in every mode: one variance for all
239
+ # channels, from their total power while the signal is present.
240
+ power = xp.sum(_symbol_energy(signal_adapter, x, xp, sps)) / sps
241
+ noise_power = (
242
+ power
243
+ * sig.sampling_rate
244
+ / (noise_modes * reference_bandwidth * db_to_linear(osnr_db, power=True))
245
+ )
246
+ return signal_adapter.wrap_samples(x + _noise(x, xp, noise_power, rng))
247
+
248
+
249
+ def _symbol_energy(
250
+ signal_adapter: SignalAdapter[Any], x: ArrayType, xp: Any, sps: float
251
+ ) -> ArrayType:
252
+ """Energy per symbol of each channel in sample units, ``(C, 1)`` (``(1,)``
253
+ for 1-D input): the channel's energy over the symbols the record
254
+ carries (see :func:`apply_awgn`)."""
255
+ num_periods = x.shape[-1] / sps
256
+ num_symbols = num_periods
257
+ sig = signal_adapter.signal
258
+ if sig is not None:
259
+ if sig.frame is not None:
260
+ frame = sig.frame
261
+ zero_guard = frame.guard_num_symbols if frame.guard_type == "zero" else 0
262
+ num_symbols = frame.num_symbols - zero_guard
263
+ elif sig.reference is not None and sig.reference.symbols is not None:
264
+ num_symbols = sig.reference.symbols.shape[-1]
265
+ # A record shorter than its frame or reference holds only part of the
266
+ # signal: its own length counts.
267
+ num_symbols = min(num_symbols, num_periods)
268
+ mean_power = xp.mean(xp.abs(x) ** 2, axis=-1, keepdims=True)
269
+ return mean_power * (x.shape[-1] / num_symbols)
270
+
271
+
272
+ def _noise(x: Any, xp: Any, noise_power: Any, rng: RNG) -> Any:
273
+ """White Gaussian noise of ``noise_power`` per sample, shaped like ``x``.
274
+
275
+ Complex noise splits its power equally between I and Q.
276
+ """
277
+ gen = as_generator(rng)
278
+ real_dtype = x.real.dtype if x.real.dtype.kind == "f" else xp.dtype(xp.float64)
279
+ if xp.iscomplexobj(x):
280
+ std = xp.sqrt(noise_power / 2).astype(real_dtype)
281
+ return complex_normal(gen, x.shape, std=std, dtype=real_dtype, xp=xp)
282
+ std = xp.sqrt(noise_power).astype(real_dtype)
283
+ return std * standard_normal(gen, x.shape, dtype=real_dtype, xp=xp)
@@ -97,6 +97,53 @@
97
97
  "cell_type": "markdown",
98
98
  "id": "4",
99
99
  "metadata": {},
100
+ "source": [
101
+ "### Relative or absolute noise\n",
102
+ "\n",
103
+ "`apply_awgn` takes the amount of noise in one of two ways, never both:\n",
104
+ "\n",
105
+ "* `esn0_db`, **relative to the signal**: it measures the symbol energy `Es`\n",
106
+ " and adds noise of variance `σ² = Es / (Es/N0)` per sample, so the Es/N0\n",
107
+ " is what you ask for whatever the signal's scale. `Es` is the energy per\n",
108
+ " symbol the `Signal` carries, so silence around a frame (11) does not lower\n",
109
+ " it, and each channel of a multichannel record gets the requested Es/N0\n",
110
+ " from its own energy (`per_channel=False` gives all channels one noise\n",
111
+ " floor from their mean).\n",
112
+ "* `noise_var`, **absolute**: the variance `σ² = E|n|²` per sample, in the\n",
113
+ " squared units of the samples, with nothing measured. This is a fixed\n",
114
+ " noise floor, such as a receiver's thermal noise: the SNR follows the\n",
115
+ " signal's power.\n",
116
+ "\n",
117
+ "In sample units the symbol energy of unit-energy symbols is 1 at any `sps`,\n",
118
+ "so `noise_var=0.01` is Es/N0 = 20 dB for the transmitted signal. The two\n",
119
+ "part ways when the signal arrives 10 dB weaker:"
120
+ ]
121
+ },
122
+ {
123
+ "cell_type": "code",
124
+ "execution_count": null,
125
+ "id": "5",
126
+ "metadata": {},
127
+ "outputs": [],
128
+ "source": [
129
+ "attenuated = tx.replace(samples=tx.samples * 10 ** (-10 / 20)) # 10 dB of loss\n",
130
+ "for name, sig in ((\"as sent\", tx), (\"10 dB weaker\", attenuated)):\n",
131
+ " snrs = []\n",
132
+ " for noisy in (\n",
133
+ " ck.impairments.apply_awgn(sig, esn0_db=20, rng=2), # relative\n",
134
+ " ck.impairments.apply_awgn(sig, noise_var=0.01, rng=2), # absolute\n",
135
+ " ):\n",
136
+ " y = ck.multirate.decimate_to_symbol_rate(ck.filtering.matched_filter(noisy))\n",
137
+ " snrs.append(ck.metrics.snr(y))\n",
138
+ " print(\n",
139
+ " f\"{name:12s}: esn0_db=20 gives {snrs[0]:5.2f} dB, noise_var=0.01 gives {snrs[1]:5.2f} dB\"\n",
140
+ " )"
141
+ ]
142
+ },
143
+ {
144
+ "cell_type": "markdown",
145
+ "id": "6",
146
+ "metadata": {},
100
147
  "source": [
101
148
  "## 2. SNR and EVM\n",
102
149
  "\n",
@@ -118,7 +165,7 @@
118
165
  {
119
166
  "cell_type": "code",
120
167
  "execution_count": null,
121
- "id": "5",
168
+ "id": "7",
122
169
  "metadata": {},
123
170
  "outputs": [],
124
171
  "source": [
@@ -136,7 +183,7 @@
136
183
  },
137
184
  {
138
185
  "cell_type": "markdown",
139
- "id": "6",
186
+ "id": "8",
140
187
  "metadata": {},
141
188
  "source": [
142
189
  "## 3. Error rates against theory\n",
@@ -157,7 +204,7 @@
157
204
  {
158
205
  "cell_type": "code",
159
206
  "execution_count": null,
160
- "id": "7",
207
+ "id": "9",
161
208
  "metadata": {},
162
209
  "outputs": [],
163
210
  "source": [
@@ -190,7 +237,7 @@
190
237
  },
191
238
  {
192
239
  "cell_type": "markdown",
193
- "id": "8",
240
+ "id": "10",
194
241
  "metadata": {},
195
242
  "source": [
196
243
  "## 4. Soft information: LLRs\n",
@@ -216,7 +263,7 @@
216
263
  {
217
264
  "cell_type": "code",
218
265
  "execution_count": null,
219
- "id": "9",
266
+ "id": "11",
220
267
  "metadata": {},
221
268
  "outputs": [],
222
269
  "source": [
@@ -258,7 +305,7 @@
258
305
  },
259
306
  {
260
307
  "cell_type": "markdown",
261
- "id": "10",
308
+ "id": "12",
262
309
  "metadata": {},
263
310
  "source": [
264
311
  "## 5. Achievable rates: GMI and MI\n",
@@ -279,7 +326,7 @@
279
326
  {
280
327
  "cell_type": "code",
281
328
  "execution_count": null,
282
- "id": "11",
329
+ "id": "13",
283
330
  "metadata": {},
284
331
  "outputs": [],
285
332
  "source": [
@@ -314,7 +361,7 @@
314
361
  },
315
362
  {
316
363
  "cell_type": "markdown",
317
- "id": "12",
364
+ "id": "14",
318
365
  "metadata": {},
319
366
  "source": [
320
367
  "## 6. Conventions of the metrics\n",
@@ -332,7 +379,7 @@
332
379
  {
333
380
  "cell_type": "code",
334
381
  "execution_count": null,
335
- "id": "13",
382
+ "id": "15",
336
383
  "metadata": {},
337
384
  "outputs": [],
338
385
  "source": [
@@ -349,13 +396,15 @@
349
396
  },
350
397
  {
351
398
  "cell_type": "markdown",
352
- "id": "14",
399
+ "id": "16",
353
400
  "metadata": {},
354
401
  "source": [
355
402
  "**Summary**\n",
356
403
  "\n",
357
404
  "* `apply_awgn(esn0_db=...)` gives SNR = Es/N0 after the matched filter at\n",
358
405
  " any `sps`.\n",
406
+ "* `esn0_db` sets the noise relative to the measured symbol energy;\n",
407
+ " `noise_var` sets an absolute noise floor instead.\n",
359
408
  "* `snr` and data-aided `evm` are two views of one number,\n",
360
409
  " `EVM = 1/sqrt(SNR)`.\n",
361
410
  "* `ser` / `ber` match the closed-form QAM curves; LLR signs are the hard\n",
@@ -131,9 +131,9 @@
131
131
  "## 3. A capture with an unknown delay\n",
132
132
  "\n",
133
133
  "The capture below holds 1000.37 samples of silence before the frame and\n",
134
- "some after, with noise at Es/N0 = 15 dB. `signal_power=` pins the noise to\n",
135
- "the frame's power (`1/sps`) rather than to the power of the whole capture,\n",
136
- "which the silence lowers.\n",
134
+ "some after, with noise at Es/N0 = 15 dB. `apply_awgn` measures `Es` as\n",
135
+ "the energy per symbol of the frame the `Signal` carries, so the silence\n",
136
+ "around it does not lower the noise level.\n",
137
137
  "\n",
138
138
  "`estimate_timing` correlates the capture with the preamble, shaped with the\n",
139
139
  "`Signal`'s pulse at its `sps` (a `Signal` with a frame needs no template\n",
@@ -162,7 +162,7 @@
162
162
  "capture = tx.replace(\n",
163
163
  " samples=ck.timing.fft_fractional_delay(padded, delay=TRUE_DELAY - 1000)\n",
164
164
  ")\n",
165
- "capture = ck.impairments.apply_awgn(capture, esn0_db=15, signal_power=1 / tx.sps, rng=1)\n",
165
+ "capture = ck.impairments.apply_awgn(capture, esn0_db=15, rng=1)\n",
166
166
  "\n",
167
167
  "est = ck.timing.estimate_timing(capture)\n",
168
168
  "print(\n",
@@ -268,7 +268,7 @@
268
268
  " ]\n",
269
269
  " )\n",
270
270
  " )\n",
271
- " cap = ck.impairments.apply_awgn(cap, esn0_db=15, signal_power=1 / t.sps, rng=2)\n",
271
+ " cap = ck.impairments.apply_awgn(cap, esn0_db=15, rng=2)\n",
272
272
  " preamble_seconds = preamble.num_symbols / t.symbol_rate\n",
273
273
  " for turns in (0.0, 0.25, 0.5, 1.0, 2.0):\n",
274
274
  " e = ck.timing.estimate_timing(\n",
@@ -85,9 +85,10 @@
85
85
  "def through(h):\n",
86
86
  " xp = tx.xp\n",
87
87
  " r = xp.convolve(tx.samples, xp.asarray(h))[: tx.samples.shape[-1]]\n",
88
- " # The noise is set against the transmitted power (1), not the received one.\n",
88
+ " # A fixed receiver noise floor: Es/N0 = 18 dB for the transmitted\n",
89
+ " # unit-energy symbols, whatever the channel does to the received power.\n",
89
90
  " return ck.impairments.apply_awgn(\n",
90
- " tx.replace(samples=r), esn0_db=ESN0, signal_power=1.0, rng=2\n",
91
+ " tx.replace(samples=r), noise_var=10 ** (-ESN0 / 10), rng=2\n",
91
92
  " )\n",
92
93
  "\n",
93
94
  "\n",
@@ -65,8 +65,9 @@
65
65
  "## 1. The link and the LMS equalizer\n",
66
66
  "\n",
67
67
  "16-QAM at 32 GBd and 2 samples per symbol through a 12 GHz electrical\n",
68
- "bandwidth ([10](10-front-end-impairments.ipynb) · section 3), with AWGN at Es/N0 = 20 dB, then the\n",
69
- "matched filter. The ISI closes the eye completely.\n",
68
+ "bandwidth ([10](10-front-end-impairments.ipynb) · section 3), with receiver noise at Es/N0 = 20 dB\n",
69
+ "for the transmitted symbols, then the matched filter. The noise is a fixed\n",
70
+ "floor (`noise_var`), so the power the filter removes counts against the SNR. The ISI closes the eye completely.\n",
70
71
  "\n",
71
72
  "`lms` runs a **fractionally spaced** FIR filter (here 21 taps at 2 samples\n",
72
73
  "per symbol) and outputs one symbol per symbol period. For each output `y`\n",
@@ -98,6 +99,9 @@
98
99
  " rng=1,\n",
99
100
  " device=DEVICE,\n",
100
101
  ")\n",
102
+ "# Receiver noise at Es/N0 = 20 dB for the transmitted unit-energy symbols\n",
103
+ "# (Es = 1 in sample units at any sps), fixed whatever the filter does.\n",
104
+ "NOISE_VAR = 10 ** (-20 / 10)\n",
101
105
  "bessel = ck.filtering.bessel_sos(\n",
102
106
  " sampling_rate=tx.sampling_rate, cutoff=12e9, order=4, norm=\"mag\"\n",
103
107
  ")\n",
@@ -105,7 +109,7 @@
105
109
  "\n",
106
110
  "def link(rng):\n",
107
111
  " x = ck.filtering.iir_filter(tx, bessel)\n",
108
- " x = ck.impairments.apply_awgn(x, esn0_db=20, signal_power=1 / tx.sps, rng=rng)\n",
112
+ " x = ck.impairments.apply_awgn(x, noise_var=NOISE_VAR, rng=rng)\n",
109
113
  " return ck.filtering.matched_filter(x)\n",
110
114
  "\n",
111
115
  "\n",
@@ -319,7 +323,7 @@
319
323
  " ck.filtering.iir_filter(stream, bessel), linewidth=1e6, rng=4\n",
320
324
  ")\n",
321
325
  "stream = ck.filtering.matched_filter(\n",
322
- " ck.impairments.apply_awgn(stream, esn0_db=20, signal_power=1 / tx.sps, rng=5)\n",
326
+ " ck.impairments.apply_awgn(stream, noise_var=NOISE_VAR, rng=5)\n",
323
327
  ")\n",
324
328
  "\n",
325
329
  "\n",
@@ -142,7 +142,7 @@
142
142
  "x = ck.impairments.apply_iq_imbalance(\n",
143
143
  " x, amplitude_imbalance_db=1.0, phase_imbalance_deg=5.0\n",
144
144
  ")\n",
145
- "rx = ck.impairments.apply_awgn(x, esn0_db=25, signal_power=1 / SPS, rng=3)\n",
145
+ "rx = ck.impairments.apply_awgn(x, esn0_db=25, rng=3)\n",
146
146
  "\n",
147
147
  "ck.plotting.plot_psd(\n",
148
148
  " rx, nperseg=2048, title=\"Received spectrum (both polarizations)\", show=True\n",
@@ -368,9 +368,9 @@
368
368
  " **LINK,\n",
369
369
  " )\n",
370
370
  " if not nonlinear: # ... and the ASE of the 2 dBm link added by hand\n",
371
- " z = ck.impairments.apply_awgn(z, esn0_db=ase_esn0, signal_power=1 / SPS, rng=4)\n",
371
+ " z = ck.impairments.apply_awgn(z, esn0_db=ase_esn0, rng=4)\n",
372
372
  " if receiver_noise:\n",
373
- " z = ck.impairments.apply_awgn(z, esn0_db=25, signal_power=1 / SPS, rng=3)\n",
373
+ " z = ck.impairments.apply_awgn(z, esn0_db=25, rng=3)\n",
374
374
  " z = ck.filtering.correct_chromatic_dispersion(\n",
375
375
  " z,\n",
376
376
  " dispersion_ps_nm_km=fiber.dispersion_ps_nm_km,\n",
@@ -210,13 +210,11 @@
210
210
  "source": [
211
211
  "# Dark capture -> DC calibration. With the signal blocked the ADCs record\n",
212
212
  "# offset + detector noise; the noise level does not depend on the blocked\n",
213
- "# signal, so pin it with signal_power=1.0 - the level a unit-amplitude beat\n",
214
- "# sees (the record's own near-zero power cannot define it).\n",
213
+ "# signal, so give it as an absolute variance: the level a unit-amplitude\n",
214
+ "# beat sees at SNR_DB (the record's own near-zero power cannot define it).\n",
215
215
  "z_dark = apply_awgn(\n",
216
216
  " xp.full(1 << 16, DC_TRUE, dtype=xp.complex128),\n",
217
- " sps=1,\n",
218
- " esn0_db=SNR_DB,\n",
219
- " signal_power=1.0,\n",
217
+ " noise_var=10 ** (-SNR_DB / 10),\n",
220
218
  " rng=2,\n",
221
219
  ")\n",
222
220
  "dc_cal = complex(xp.mean(z_dark))\n",