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.
- commkit/__init__.py +74 -0
- commkit/_cuda/__init__.py +321 -0
- commkit/_cuda/compiler.py +88 -0
- commkit/_cuda/src/bps_min_d2.cu +104 -0
- commkit/_cuda/src/cs_block.cu +119 -0
- commkit/_cuda/src/selftest.cu +14 -0
- commkit/analysis/__init__.py +55 -0
- commkit/analysis/_common.py +236 -0
- commkit/analysis/allan.py +108 -0
- commkit/analysis/drift.py +213 -0
- commkit/analysis/interferometry.py +887 -0
- commkit/analysis/linewidth.py +480 -0
- commkit/analysis/trajectory.py +91 -0
- commkit/backend.py +507 -0
- commkit/coding/__init__.py +23 -0
- commkit/coding/base.py +17 -0
- commkit/coding/bch.py +6 -0
- commkit/coding/convolutional.py +7 -0
- commkit/coding/crc.py +7 -0
- commkit/coding/galois.py +8 -0
- commkit/coding/hamming.py +6 -0
- commkit/coding/interleaving.py +7 -0
- commkit/coding/ldpc.py +8 -0
- commkit/coding/polar.py +8 -0
- commkit/coding/ratematch.py +6 -0
- commkit/coding/reed_solomon.py +6 -0
- commkit/coding/turbo.py +8 -0
- commkit/core/__init__.py +32 -0
- commkit/core/frame.py +992 -0
- commkit/core/generation.py +581 -0
- commkit/core/signal.py +725 -0
- commkit/equalization/__init__.py +49 -0
- commkit/equalization/_block.py +1855 -0
- commkit/equalization/_common.py +606 -0
- commkit/equalization/_kernels_jax.py +1720 -0
- commkit/equalization/_kernels_numba.py +1704 -0
- commkit/equalization/blind.py +223 -0
- commkit/equalization/linear.py +365 -0
- commkit/equalization/polarization.py +790 -0
- commkit/equalization/result.py +191 -0
- commkit/equalization/sequential.py +2805 -0
- commkit/filtering.py +1120 -0
- commkit/frequency.py +1191 -0
- commkit/helpers.py +489 -0
- commkit/impairments/__init__.py +43 -0
- commkit/impairments/channel/__init__.py +20 -0
- commkit/impairments/channel/linear.py +310 -0
- commkit/impairments/channel/nonlinear.py +11 -0
- commkit/impairments/frontend.py +229 -0
- commkit/impairments/noise.py +105 -0
- commkit/impairments/source.py +219 -0
- commkit/io.py +308 -0
- commkit/logger.py +103 -0
- commkit/mapping/__init__.py +46 -0
- commkit/mapping/bits.py +240 -0
- commkit/mapping/constellation.py +153 -0
- commkit/mapping/gray.py +429 -0
- commkit/mapping/llr.py +253 -0
- commkit/mapping/shaping.py +218 -0
- commkit/metrics.py +949 -0
- commkit/multirate.py +476 -0
- commkit/plotting/__init__.py +78 -0
- commkit/plotting/analysis.py +627 -0
- commkit/plotting/constellation.py +483 -0
- commkit/plotting/equalizer.py +390 -0
- commkit/plotting/eye.py +388 -0
- commkit/plotting/spectral.py +575 -0
- commkit/plotting/sync.py +953 -0
- commkit/plotting/theme.py +203 -0
- commkit/plotting/waveform.py +200 -0
- commkit/py.typed +0 -0
- commkit/recovery/__init__.py +51 -0
- commkit/recovery/bps.py +337 -0
- commkit/recovery/corrections.py +751 -0
- commkit/recovery/pilots.py +803 -0
- commkit/recovery/pll.py +482 -0
- commkit/recovery/tikhonov.py +424 -0
- commkit/recovery/viterbi_viterbi.py +227 -0
- commkit/spectral.py +560 -0
- commkit/timing.py +841 -0
- commkit-1.0.0.dist-info/METADATA +145 -0
- commkit-1.0.0.dist-info/RECORD +84 -0
- commkit-1.0.0.dist-info/WHEEL +4 -0
- 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
|
+
}
|