commkit 1.0.0__py3-none-any.whl

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 (84) hide show
  1. commkit/__init__.py +74 -0
  2. commkit/_cuda/__init__.py +321 -0
  3. commkit/_cuda/compiler.py +88 -0
  4. commkit/_cuda/src/bps_min_d2.cu +104 -0
  5. commkit/_cuda/src/cs_block.cu +119 -0
  6. commkit/_cuda/src/selftest.cu +14 -0
  7. commkit/analysis/__init__.py +55 -0
  8. commkit/analysis/_common.py +236 -0
  9. commkit/analysis/allan.py +108 -0
  10. commkit/analysis/drift.py +213 -0
  11. commkit/analysis/interferometry.py +887 -0
  12. commkit/analysis/linewidth.py +480 -0
  13. commkit/analysis/trajectory.py +91 -0
  14. commkit/backend.py +507 -0
  15. commkit/coding/__init__.py +23 -0
  16. commkit/coding/base.py +17 -0
  17. commkit/coding/bch.py +6 -0
  18. commkit/coding/convolutional.py +7 -0
  19. commkit/coding/crc.py +7 -0
  20. commkit/coding/galois.py +8 -0
  21. commkit/coding/hamming.py +6 -0
  22. commkit/coding/interleaving.py +7 -0
  23. commkit/coding/ldpc.py +8 -0
  24. commkit/coding/polar.py +8 -0
  25. commkit/coding/ratematch.py +6 -0
  26. commkit/coding/reed_solomon.py +6 -0
  27. commkit/coding/turbo.py +8 -0
  28. commkit/core/__init__.py +32 -0
  29. commkit/core/frame.py +992 -0
  30. commkit/core/generation.py +581 -0
  31. commkit/core/signal.py +725 -0
  32. commkit/equalization/__init__.py +49 -0
  33. commkit/equalization/_block.py +1855 -0
  34. commkit/equalization/_common.py +606 -0
  35. commkit/equalization/_kernels_jax.py +1720 -0
  36. commkit/equalization/_kernels_numba.py +1704 -0
  37. commkit/equalization/blind.py +223 -0
  38. commkit/equalization/linear.py +365 -0
  39. commkit/equalization/polarization.py +790 -0
  40. commkit/equalization/result.py +191 -0
  41. commkit/equalization/sequential.py +2805 -0
  42. commkit/filtering.py +1120 -0
  43. commkit/frequency.py +1191 -0
  44. commkit/helpers.py +489 -0
  45. commkit/impairments/__init__.py +43 -0
  46. commkit/impairments/channel/__init__.py +20 -0
  47. commkit/impairments/channel/linear.py +310 -0
  48. commkit/impairments/channel/nonlinear.py +11 -0
  49. commkit/impairments/frontend.py +229 -0
  50. commkit/impairments/noise.py +105 -0
  51. commkit/impairments/source.py +219 -0
  52. commkit/io.py +308 -0
  53. commkit/logger.py +103 -0
  54. commkit/mapping/__init__.py +46 -0
  55. commkit/mapping/bits.py +240 -0
  56. commkit/mapping/constellation.py +153 -0
  57. commkit/mapping/gray.py +429 -0
  58. commkit/mapping/llr.py +253 -0
  59. commkit/mapping/shaping.py +218 -0
  60. commkit/metrics.py +949 -0
  61. commkit/multirate.py +476 -0
  62. commkit/plotting/__init__.py +78 -0
  63. commkit/plotting/analysis.py +627 -0
  64. commkit/plotting/constellation.py +483 -0
  65. commkit/plotting/equalizer.py +390 -0
  66. commkit/plotting/eye.py +388 -0
  67. commkit/plotting/spectral.py +575 -0
  68. commkit/plotting/sync.py +953 -0
  69. commkit/plotting/theme.py +203 -0
  70. commkit/plotting/waveform.py +200 -0
  71. commkit/py.typed +0 -0
  72. commkit/recovery/__init__.py +51 -0
  73. commkit/recovery/bps.py +337 -0
  74. commkit/recovery/corrections.py +751 -0
  75. commkit/recovery/pilots.py +803 -0
  76. commkit/recovery/pll.py +482 -0
  77. commkit/recovery/tikhonov.py +424 -0
  78. commkit/recovery/viterbi_viterbi.py +227 -0
  79. commkit/spectral.py +560 -0
  80. commkit/timing.py +841 -0
  81. commkit-1.0.0.dist-info/METADATA +145 -0
  82. commkit-1.0.0.dist-info/RECORD +84 -0
  83. commkit-1.0.0.dist-info/WHEEL +4 -0
  84. commkit-1.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,213 @@
1
+ """Drift / phase-noise separation and residual frequency-wander metrics."""
2
+
3
+ import numpy as np
4
+
5
+ from ..backend import ArrayType, dispatch, to_device
6
+ from ._common import _as_2d, _scalar_or_array
7
+
8
+ __all__ = ["frequency_drift_metrics", "separate_drift_phase_noise"]
9
+
10
+
11
+ def separate_drift_phase_noise(
12
+ phi: ArrayType,
13
+ symbol_rate: float,
14
+ *,
15
+ cutoff: float,
16
+ method: str = "butterworth",
17
+ order: int = 4,
18
+ debug_plot: bool = False,
19
+ ) -> tuple[ArrayType, ArrayType]:
20
+ r"""Split a phase trajectory into slow drift and fast phase-noise residual.
21
+
22
+ Applies a **zero-phase** low-pass (default 4th-order Butterworth in
23
+ second-order-sections form via ``sosfiltfilt``, numerically stable at the
24
+ very low normalized cutoffs typical here) at ``cutoff`` to obtain the
25
+ drift; the residual
26
+ ``pn = phi - drift`` carries the phase noise + AWGN. Zero-phase filtering
27
+ avoids the group-delay bias of a causal filter and the spectral leakage of
28
+ a boxcar moving average.
29
+
30
+ The split is a modelling choice: too low a cutoff lets fast drift leak into
31
+ ``pn`` (inflating the linewidth); too high a cutoff absorbs genuine
32
+ low-frequency phase noise into ``drift``. Because the single-symbol
33
+ increment used downstream is itself a high-pass, the *increment-variance*
34
+ linewidth is only weakly sensitive to this cutoff - but discard the filter
35
+ edge transients (``edge_trim`` on the metric functions) regardless.
36
+
37
+ Parameters
38
+ ----------
39
+ phi : array_like
40
+ Unwrapped carrier phase (radians), ``(N,)`` or ``(C, N)``. Sampled at
41
+ the symbol rate (one value per symbol).
42
+ symbol_rate : float
43
+ Symbol rate in Baud; the effective sampling rate of ``phi``.
44
+ cutoff : float
45
+ Low-pass cutoff in Hz separating drift (below) from phase noise
46
+ (above). Must satisfy ``0 < cutoff < symbol_rate / 2``.
47
+ method : {"butterworth", "savgol", "boxcar"}, default "butterworth"
48
+ Low-pass implementation. ``"savgol"`` is a polynomial (Savitzky-Golay)
49
+ detrend; ``"boxcar"`` is the crude moving average (provided for
50
+ comparison only).
51
+ order : int, default 4
52
+ Butterworth order, Savitzky-Golay polynomial order, or - reinterpreted
53
+ - ignored for the boxcar.
54
+ debug_plot : bool, default False
55
+ If True, plot the phase trajectory with the drift overlaid
56
+ (``carrier_phase_decomposition``).
57
+
58
+ Returns
59
+ -------
60
+ drift_phase : array_like
61
+ Low-frequency drift component, same shape/backend/dtype as ``phi``.
62
+ pn_phase : array_like
63
+ High-frequency phase-noise + AWGN residual ``phi - drift``.
64
+
65
+ Notes
66
+ -----
67
+ **Limitations.**
68
+
69
+ * The drift/phase-noise dichotomy is *spectral*, not physical: a laser's
70
+ 1/f (flicker) FM noise straddles any cutoff, so part of it lands in
71
+ ``drift`` and part in ``pn`` no matter where the cutoff is placed.
72
+ Quote the cutoff alongside any derived metric.
73
+ * ``"savgol"`` sizes its window to ≈ one cutoff period, which is only a
74
+ rough equivalent-noise-bandwidth match to the Butterworth response -
75
+ treat its cutoff as approximate.
76
+ * ``"boxcar"`` has -13 dB sidelobes (sinc response) that leak drift into
77
+ ``pn``; it is provided for comparison only.
78
+ * ``sosfiltfilt`` extends the signal internally, but the first/last
79
+ ``~0.5·fs/cutoff`` samples of ``drift`` remain transient-contaminated -
80
+ trim them via ``edge_trim`` in the downstream metric functions
81
+ (``edge_trim ≈ 0.5·symbol_rate/cutoff``).
82
+ """
83
+ phi_arr, xp, sp = dispatch(phi)
84
+ fs = float(symbol_rate)
85
+ nyq = 0.5 * fs
86
+ if not (0.0 < cutoff < nyq):
87
+ raise ValueError(
88
+ f"cutoff={cutoff} must lie in (0, symbol_rate/2={nyq}). "
89
+ "phi is sampled at the symbol rate."
90
+ )
91
+
92
+ phi2, was_1d = _as_2d(phi_arr)
93
+ in_dtype = phi2.dtype
94
+
95
+ if method == "butterworth":
96
+ # SOS form is numerically stable at the very low normalized cutoffs
97
+ # typical here (cutoff ≪ symbol_rate => poles bunch near z=1).
98
+ sos = sp.signal.butter(order, cutoff / nyq, btype="low", output="sos")
99
+ if xp.__name__ == "cupy":
100
+ sos = xp.asarray(sos)
101
+ drift = sp.signal.sosfiltfilt(sos, phi2.astype(xp.float64), axis=-1)
102
+ elif method == "savgol":
103
+ # Window ≈ one cutoff period (odd, > polyorder).
104
+ win = int(round(fs / cutoff)) | 1
105
+ win = max(win, order + 2 + (order % 2 == 0))
106
+ win = min(win, phi2.shape[-1] - (1 - phi2.shape[-1] % 2))
107
+ drift = sp.signal.savgol_filter(phi2.astype(xp.float64), win, order, axis=-1)
108
+ elif method == "boxcar":
109
+ # uniform_filter1d is vectorized over channels on both backends and
110
+ # its "nearest" edge mode avoids the zero-padding bias of
111
+ # convolve(mode="same"), which drags the drift estimate toward zero
112
+ # over the first/last window.
113
+ w = max(1, int(round(fs / cutoff)))
114
+ drift = sp.ndimage.uniform_filter1d(
115
+ phi2.astype(xp.float64), w, axis=-1, mode="nearest"
116
+ )
117
+ else:
118
+ raise ValueError(f"Unknown method {method!r}.")
119
+
120
+ pn = phi2 - drift
121
+ if was_1d:
122
+ drift, pn = drift[0], pn[0]
123
+
124
+ drift = drift.astype(in_dtype, copy=False)
125
+ pn = pn.astype(in_dtype, copy=False)
126
+
127
+ if debug_plot:
128
+ from .. import plotting as _plotting
129
+
130
+ _plotting.plot_carrier_phase_decomposition(
131
+ phi, drift, symbol_rate=symbol_rate, show=True
132
+ )
133
+
134
+ return drift, pn
135
+
136
+
137
+ def frequency_drift_metrics(
138
+ drift_phase: ArrayType,
139
+ symbol_rate: float,
140
+ *,
141
+ edge_trim: int = 0,
142
+ amp_ref: float | None = None,
143
+ debug_plot: bool = False,
144
+ ) -> dict[str, float | np.ndarray]:
145
+ r"""Residual frequency-wander statistics from a smoothed phase ramp.
146
+
147
+ The instantaneous residual frequency offset is the phase slope
148
+ ``df = diff(drift) / (2π T_sym)`` in Hz. Report the std (typical wander)
149
+ and the peak-to-peak (worst-case spin the CPR must follow).
150
+
151
+ Relate to the BPS tracking limit: a residual ``δf`` rotates the phase by
152
+ ``2π·δf·T_sym`` per symbol, so over a window of ``K`` symbols the
153
+ intra-window rotation must stay below the QAM quarter-symmetry ``π/4`` ->
154
+ ``δf_max ≈ 1/(8·K·T_sym)``. A larger BPS window tracks *less* drift.
155
+
156
+ Parameters
157
+ ----------
158
+ drift_phase : array_like
159
+ Drift phase component (radians), ``(N,)`` or ``(C, N)``.
160
+ symbol_rate : float
161
+ Symbol rate in Baud.
162
+ edge_trim : int, default 0
163
+ Number of samples to discard from each end before differencing
164
+ (removes low-pass filter transients).
165
+ amp_ref : float, optional
166
+ Reference wander amplitude (Hz) drawn as ``±amp_ref`` guides when
167
+ ``debug_plot=True`` (e.g. an injected amplitude in a simulation).
168
+ debug_plot : bool, default False
169
+ If True, plot the residual frequency vs time
170
+ (``frequency_drift``).
171
+
172
+ Returns
173
+ -------
174
+ dict
175
+ ``{'df', 'std', 'pp', 'max_abs'}``. ``df`` is the
176
+ per-symbol residual frequency array; the rest are floats (SISO) or
177
+ per-channel arrays (MIMO).
178
+
179
+ Notes
180
+ -----
181
+ The first difference under-reads a spectral component at ``f`` by
182
+ ``sinc(f/R)`` relative to a true derivative. Because ``drift_phase`` is
183
+ low-passed (``cutoff ≪ R``), this bias is negligible here - it only
184
+ matters when differencing broadband phase (see ``fm_noise_psd``, which
185
+ corrects for it).
186
+ """
187
+ d, xp, _ = dispatch(drift_phase)
188
+ d2, was_1d = _as_2d(d)
189
+ if edge_trim > 0:
190
+ d2 = d2[:, edge_trim:-edge_trim]
191
+
192
+ t_sym = 1.0 / float(symbol_rate)
193
+ df = xp.diff(d2.astype(xp.float64), axis=-1) / (2.0 * np.pi * t_sym)
194
+
195
+ std = to_device(xp.std(df, axis=-1), "cpu")
196
+ pp = to_device(xp.max(df, axis=-1) - xp.min(df, axis=-1), "cpu")
197
+ max_abs = to_device(xp.max(xp.abs(df), axis=-1), "cpu")
198
+
199
+ df_out = df[0] if was_1d else df
200
+
201
+ if debug_plot:
202
+ from .. import plotting as _plotting
203
+
204
+ _plotting.plot_frequency_drift(
205
+ df_out, symbol_rate=symbol_rate, amp_ref=amp_ref, show=True
206
+ )
207
+
208
+ return {
209
+ "df": df_out,
210
+ "std": _scalar_or_array(std),
211
+ "pp": _scalar_or_array(pp),
212
+ "max_abs": _scalar_or_array(max_abs),
213
+ }