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
commkit/recovery/pll.py
ADDED
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
"""Decision-directed PLL carrier phase recovery."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from ..backend import ArrayType, dispatch, to_device
|
|
8
|
+
from ..logger import logger
|
|
9
|
+
from .corrections import correct_cycle_slips
|
|
10
|
+
|
|
11
|
+
_NUMBA_PLL: dict = {}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _get_numba_dd_pll():
|
|
15
|
+
"""JIT-compile and cache the Numba DD-PLL sample-wise loop kernel.
|
|
16
|
+
|
|
17
|
+
Returns
|
|
18
|
+
-------
|
|
19
|
+
callable
|
|
20
|
+
Numba-compiled ``_dd_pll_loop``.
|
|
21
|
+
"""
|
|
22
|
+
if "dd_pll" not in _NUMBA_PLL:
|
|
23
|
+
import numba
|
|
24
|
+
|
|
25
|
+
@numba.njit(cache=True, fastmath=True, nogil=True)
|
|
26
|
+
def _dd_pll_loop(
|
|
27
|
+
sym_r,
|
|
28
|
+
sym_i,
|
|
29
|
+
const_r,
|
|
30
|
+
const_i,
|
|
31
|
+
mu,
|
|
32
|
+
beta,
|
|
33
|
+
phi0,
|
|
34
|
+
freq0,
|
|
35
|
+
is_sq_qam,
|
|
36
|
+
levels,
|
|
37
|
+
d_grid,
|
|
38
|
+
lev_min,
|
|
39
|
+
side,
|
|
40
|
+
):
|
|
41
|
+
"""Inner DD-PLL loop compiled to machine code by Numba.
|
|
42
|
+
|
|
43
|
+
Parameters
|
|
44
|
+
----------
|
|
45
|
+
sym_r, sym_i : (N,) float64
|
|
46
|
+
Real and imaginary parts of received symbols.
|
|
47
|
+
const_r, const_i : (M,) float64
|
|
48
|
+
Real and imaginary parts of reference constellation.
|
|
49
|
+
mu : float64
|
|
50
|
+
Proportional (phase) gain - corrects the instantaneous phase error.
|
|
51
|
+
beta : float64
|
|
52
|
+
Integral (frequency) gain - tracks residual frequency drift.
|
|
53
|
+
Set to 0.0 for a 1st-order loop.
|
|
54
|
+
phi0 : float64
|
|
55
|
+
Initial phase state in radians.
|
|
56
|
+
freq0 : float64
|
|
57
|
+
Initial frequency correction state in radians/symbol.
|
|
58
|
+
is_sq_qam : bool
|
|
59
|
+
True when the constellation is a square QAM grid. Enables the
|
|
60
|
+
O(1) rounding decision path instead of the O(M) linear search.
|
|
61
|
+
levels : (side,) float64
|
|
62
|
+
Sorted unique axis levels for square QAM (ignored when not sq_qam).
|
|
63
|
+
d_grid : float64
|
|
64
|
+
Grid spacing (levels[1] - levels[0]).
|
|
65
|
+
lev_min : float64
|
|
66
|
+
Minimum level value (levels[0]).
|
|
67
|
+
side : int
|
|
68
|
+
Number of points per axis (sqrt of constellation order).
|
|
69
|
+
|
|
70
|
+
Returns
|
|
71
|
+
-------
|
|
72
|
+
phase_est : (N,) float64
|
|
73
|
+
Per-symbol phase trajectory φ[n].
|
|
74
|
+
"""
|
|
75
|
+
N = len(sym_r)
|
|
76
|
+
M = len(const_r)
|
|
77
|
+
phase_est = np.empty(N, dtype=np.float64)
|
|
78
|
+
phi = phi0
|
|
79
|
+
freq = freq0
|
|
80
|
+
|
|
81
|
+
for n in range(N):
|
|
82
|
+
# Rotate received symbol by current phase estimate:
|
|
83
|
+
# y[n] = s[n] · exp(-jφ[n])
|
|
84
|
+
cos_phi = np.cos(phi)
|
|
85
|
+
sin_phi = np.sin(phi)
|
|
86
|
+
yr = sym_r[n] * cos_phi + sym_i[n] * sin_phi
|
|
87
|
+
yi = -sym_r[n] * sin_phi + sym_i[n] * cos_phi
|
|
88
|
+
|
|
89
|
+
# Hard decision: argmin_{c ∈ C} |y - c|²
|
|
90
|
+
if is_sq_qam:
|
|
91
|
+
# O(1) grid rounding for square QAM
|
|
92
|
+
r_idx = int(round((yr - lev_min) / d_grid))
|
|
93
|
+
if r_idx < 0:
|
|
94
|
+
r_idx = 0
|
|
95
|
+
elif r_idx >= side:
|
|
96
|
+
r_idx = side - 1
|
|
97
|
+
d_r = levels[r_idx]
|
|
98
|
+
i_idx = int(round((yi - lev_min) / d_grid))
|
|
99
|
+
if i_idx < 0:
|
|
100
|
+
i_idx = 0
|
|
101
|
+
elif i_idx >= side:
|
|
102
|
+
i_idx = side - 1
|
|
103
|
+
d_i = levels[i_idx]
|
|
104
|
+
else:
|
|
105
|
+
min_d2 = (yr - const_r[0]) ** 2 + (yi - const_i[0]) ** 2
|
|
106
|
+
d_r = const_r[0]
|
|
107
|
+
d_i = const_i[0]
|
|
108
|
+
for k in range(1, M):
|
|
109
|
+
d2 = (yr - const_r[k]) ** 2 + (yi - const_i[k]) ** 2
|
|
110
|
+
if d2 < min_d2:
|
|
111
|
+
min_d2 = d2
|
|
112
|
+
d_r = const_r[k]
|
|
113
|
+
d_i = const_i[k]
|
|
114
|
+
|
|
115
|
+
# Cross-product phase error: e = Im(y · d*) = yi·d_r - yr·d_i
|
|
116
|
+
e = yi * d_r - yr * d_i
|
|
117
|
+
|
|
118
|
+
# Record the phase used to derotate symbol n - before the update.
|
|
119
|
+
phase_est[n] = phi
|
|
120
|
+
|
|
121
|
+
# 2nd-order loop filter (reduces to 1st order when beta=0):
|
|
122
|
+
# φ[n+1] = φ[n] + μ·e[n] + ν[n]
|
|
123
|
+
# ν[n] = ν[n-1] + β·e[n]
|
|
124
|
+
phi = phi + mu * e + freq
|
|
125
|
+
freq = freq + beta * e
|
|
126
|
+
|
|
127
|
+
return phase_est
|
|
128
|
+
|
|
129
|
+
_NUMBA_PLL["dd_pll"] = _dd_pll_loop
|
|
130
|
+
|
|
131
|
+
return _NUMBA_PLL["dd_pll"]
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _get_numba_dd_pll_joint():
|
|
135
|
+
"""JIT-compile and cache the joint-channel DD-PLL PI kernel.
|
|
136
|
+
|
|
137
|
+
Averages the cross-product phase error across C channels at each symbol
|
|
138
|
+
before updating the single shared phase/frequency state. This is the
|
|
139
|
+
MVUE joint estimator for shared-LO systems.
|
|
140
|
+
|
|
141
|
+
Returns
|
|
142
|
+
-------
|
|
143
|
+
callable
|
|
144
|
+
Numba-compiled ``_dd_pll_joint_loop``.
|
|
145
|
+
"""
|
|
146
|
+
if "dd_pll_joint" not in _NUMBA_PLL:
|
|
147
|
+
import numba
|
|
148
|
+
|
|
149
|
+
@numba.njit(cache=True, fastmath=True, nogil=True)
|
|
150
|
+
def _dd_pll_joint_loop(
|
|
151
|
+
sym_r,
|
|
152
|
+
sym_i,
|
|
153
|
+
const_r,
|
|
154
|
+
const_i,
|
|
155
|
+
mu,
|
|
156
|
+
beta,
|
|
157
|
+
phi0,
|
|
158
|
+
freq0,
|
|
159
|
+
is_sq_qam,
|
|
160
|
+
levels,
|
|
161
|
+
d_grid,
|
|
162
|
+
lev_min,
|
|
163
|
+
side,
|
|
164
|
+
):
|
|
165
|
+
"""Joint-channel DD-PLL with PI loop filter.
|
|
166
|
+
|
|
167
|
+
Parameters
|
|
168
|
+
----------
|
|
169
|
+
sym_r, sym_i : (C, N) float64
|
|
170
|
+
Real and imaginary parts of received symbols, all channels.
|
|
171
|
+
const_r, const_i : (M,) float64
|
|
172
|
+
Reference constellation.
|
|
173
|
+
mu, beta, phi0, freq0 : float64
|
|
174
|
+
Loop parameters - same semantics as ``_dd_pll_loop``.
|
|
175
|
+
is_sq_qam : bool
|
|
176
|
+
Enables O(1) rounding decision for square QAM grids.
|
|
177
|
+
levels : (side,) float64
|
|
178
|
+
d_grid, lev_min : float64
|
|
179
|
+
side : int
|
|
180
|
+
|
|
181
|
+
Returns
|
|
182
|
+
-------
|
|
183
|
+
phase_est : (N,) float64
|
|
184
|
+
Single shared phase trajectory (broadcast to all channels by caller).
|
|
185
|
+
"""
|
|
186
|
+
C = sym_r.shape[0]
|
|
187
|
+
N = sym_r.shape[1]
|
|
188
|
+
M = len(const_r)
|
|
189
|
+
phase_est = np.empty(N, dtype=np.float64)
|
|
190
|
+
phi = phi0
|
|
191
|
+
freq = freq0
|
|
192
|
+
|
|
193
|
+
for n in range(N):
|
|
194
|
+
cos_phi = np.cos(phi)
|
|
195
|
+
sin_phi = np.sin(phi)
|
|
196
|
+
e_sum = 0.0
|
|
197
|
+
for c in range(C):
|
|
198
|
+
yr = sym_r[c, n] * cos_phi + sym_i[c, n] * sin_phi
|
|
199
|
+
yi = -sym_r[c, n] * sin_phi + sym_i[c, n] * cos_phi
|
|
200
|
+
if is_sq_qam:
|
|
201
|
+
r_idx = int(round((yr - lev_min) / d_grid))
|
|
202
|
+
if r_idx < 0:
|
|
203
|
+
r_idx = 0
|
|
204
|
+
elif r_idx >= side:
|
|
205
|
+
r_idx = side - 1
|
|
206
|
+
d_r = levels[r_idx]
|
|
207
|
+
i_idx = int(round((yi - lev_min) / d_grid))
|
|
208
|
+
if i_idx < 0:
|
|
209
|
+
i_idx = 0
|
|
210
|
+
elif i_idx >= side:
|
|
211
|
+
i_idx = side - 1
|
|
212
|
+
d_i = levels[i_idx]
|
|
213
|
+
else:
|
|
214
|
+
min_d2 = (yr - const_r[0]) ** 2 + (yi - const_i[0]) ** 2
|
|
215
|
+
d_r = const_r[0]
|
|
216
|
+
d_i = const_i[0]
|
|
217
|
+
for k in range(1, M):
|
|
218
|
+
d2 = (yr - const_r[k]) ** 2 + (yi - const_i[k]) ** 2
|
|
219
|
+
if d2 < min_d2:
|
|
220
|
+
min_d2 = d2
|
|
221
|
+
d_r = const_r[k]
|
|
222
|
+
d_i = const_i[k]
|
|
223
|
+
e_sum += yi * d_r - yr * d_i
|
|
224
|
+
# Average error across channels - MVUE for shared LO
|
|
225
|
+
e = e_sum / float(C)
|
|
226
|
+
phase_est[n] = phi
|
|
227
|
+
phi = phi + mu * e + freq
|
|
228
|
+
freq = freq + beta * e
|
|
229
|
+
|
|
230
|
+
return phase_est
|
|
231
|
+
|
|
232
|
+
_NUMBA_PLL["dd_pll_joint"] = _dd_pll_joint_loop
|
|
233
|
+
|
|
234
|
+
return _NUMBA_PLL["dd_pll_joint"]
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def recover_carrier_phase_pll(
|
|
238
|
+
symbols: ArrayType,
|
|
239
|
+
modulation: str,
|
|
240
|
+
order: int,
|
|
241
|
+
mu: float | None = 1e-2,
|
|
242
|
+
beta: float | None = None,
|
|
243
|
+
phase_init: float = 0.0,
|
|
244
|
+
loop_bandwidth_normalized: float = 1e-3,
|
|
245
|
+
joint_channels: bool = False,
|
|
246
|
+
cycle_slip_correction: bool = False,
|
|
247
|
+
cycle_slip_history: int = 100,
|
|
248
|
+
cycle_slip_threshold: float = np.pi / 4,
|
|
249
|
+
debug_plot: bool = False,
|
|
250
|
+
) -> ArrayType:
|
|
251
|
+
r"""
|
|
252
|
+
Carrier phase recovery via a Decision-Directed Phase-Locked Loop (DD-PLL).
|
|
253
|
+
|
|
254
|
+
Tracks the carrier phase symbol-by-symbol using hard decisions as phase
|
|
255
|
+
references. A 1st-order loop (``beta=0``) corrects static or slowly
|
|
256
|
+
varying phase noise; a 2nd-order loop (``beta > 0``) additionally tracks
|
|
257
|
+
a residual frequency offset left over after coarse FOE.
|
|
258
|
+
|
|
259
|
+
This is the standard streaming CPR for hardware implementations: it is
|
|
260
|
+
modulation-format agnostic (works for any QAM/PSK order) and converges
|
|
261
|
+
much faster than block-based methods (VV, BPS) after equalizer pull-in.
|
|
262
|
+
|
|
263
|
+
Note: the DD-PLL requires reliable decisions at the input. For a cold
|
|
264
|
+
start the first ~1/mu symbols may show slow convergence; a common strategy
|
|
265
|
+
is to pre-converge with BPS or a short preamble and pass the phase as
|
|
266
|
+
``phase_init``.
|
|
267
|
+
|
|
268
|
+
Parameters
|
|
269
|
+
----------
|
|
270
|
+
symbols : array_like
|
|
271
|
+
1-SPS complex symbols after matched filtering and FOE.
|
|
272
|
+
Shape: ``(N,)`` or ``(C, N)``.
|
|
273
|
+
modulation : str
|
|
274
|
+
Modulation scheme (case-insensitive): ``'qam'``, ``'psk'``, etc.
|
|
275
|
+
Used to fetch the reference constellation via
|
|
276
|
+
``gray_constellation``.
|
|
277
|
+
order : int
|
|
278
|
+
Modulation order (4, 16, 64, ...).
|
|
279
|
+
mu : float or None, default 1e-2
|
|
280
|
+
Proportional gain - controls convergence speed and steady-state
|
|
281
|
+
jitter. Larger ``mu`` converges faster but amplifies noise.
|
|
282
|
+
Typical range: ``1e-3`` (high-SNR, high-order QAM) to ``5e-2``
|
|
283
|
+
(QPSK, low latency). Set ``mu=None`` to opt into the
|
|
284
|
+
``loop_bandwidth_normalized`` shortcut instead (see below). These
|
|
285
|
+
gains are interchangeable with the inline equalizer PLL's
|
|
286
|
+
``cpr_pll_mu``/``cpr_pll_beta`` (``lms``/``rls``, ``cpr_type='pll'``).
|
|
287
|
+
beta : float or None, default None
|
|
288
|
+
Integral gain - enables 2nd-order frequency tracking. ``None`` (or
|
|
289
|
+
``0.0``) gives a 1st-order loop; set ``beta > 0`` when a residual
|
|
290
|
+
frequency offset remains after FOE (e.g. ``beta ≈ mu² / 4``).
|
|
291
|
+
Requires ``mu`` to be set (passing ``beta`` with ``mu=None`` raises
|
|
292
|
+
``ValueError``).
|
|
293
|
+
phase_init : float, default 0.0
|
|
294
|
+
Initial phase state in radians. Use the last sample of a
|
|
295
|
+
preceding BPS or pilot-aided estimate to warm-start the loop.
|
|
296
|
+
loop_bandwidth_normalized : float, default 1e-3
|
|
297
|
+
Critically-damped (ζ=1) loop bandwidth shortcut, used only when
|
|
298
|
+
``mu is None``. Normalized one-sided bandwidth in ``(0, 0.5)``;
|
|
299
|
+
gains are derived as mu = 4*B_L, beta = 4*B_L^2.
|
|
300
|
+
joint_channels : bool, default False
|
|
301
|
+
For MIMO inputs (C > 1): if ``True``, average the cross-product
|
|
302
|
+
phase error across all channels at each symbol before updating the
|
|
303
|
+
shared loop state. Both polarisations drive a single phase/frequency
|
|
304
|
+
trajectory, giving ~√C variance reduction for shared-LO systems.
|
|
305
|
+
The output ``phi_full[ch]`` rows are all identical.
|
|
306
|
+
Has no effect for SISO (C = 1).
|
|
307
|
+
cycle_slip_correction : bool, default False
|
|
308
|
+
If ``True``, apply ``correct_cycle_slips`` to the per-symbol
|
|
309
|
+
phase trajectory after the loop, to detect and fix sudden ``π/2``
|
|
310
|
+
jumps caused by incorrect hard decisions near the branch boundary.
|
|
311
|
+
cycle_slip_history : int, default 100
|
|
312
|
+
``history_length`` passed to ``correct_cycle_slips``.
|
|
313
|
+
Default is higher than for block-phase methods because the trajectory
|
|
314
|
+
is per-symbol (not per-block).
|
|
315
|
+
cycle_slip_threshold : float, default π/4
|
|
316
|
+
``threshold`` passed to ``correct_cycle_slips`` (radians).
|
|
317
|
+
|
|
318
|
+
Returns
|
|
319
|
+
-------
|
|
320
|
+
array_like
|
|
321
|
+
Per-symbol phase estimate φ[n] in radians.
|
|
322
|
+
Shape matches ``symbols``. Same backend as input.
|
|
323
|
+
|
|
324
|
+
Notes
|
|
325
|
+
-----
|
|
326
|
+
Inner loop: derotate by phi_hat, hard-decide, compute cross-product error
|
|
327
|
+
e[n] = Im(y[n] * d_hat*[n]), update phi_hat[n+1] = phi_hat[n] + mu*e + nu,
|
|
328
|
+
nu += beta*e. Numba-compiled on CPU; GPU inputs are offloaded transparently.
|
|
329
|
+
|
|
330
|
+
A global M-fold phase ambiguity always remains - resolve via a pilot or
|
|
331
|
+
preamble reference after CPR.
|
|
332
|
+
"""
|
|
333
|
+
from ..helpers import normalize, resolve_pll_gains
|
|
334
|
+
from ..mapping import gray_constellation
|
|
335
|
+
|
|
336
|
+
# Resolve PI gains: raw mu/beta if given, else the critically-damped
|
|
337
|
+
# bandwidth shortcut (mu=None). Validate the bandwidth only on that path.
|
|
338
|
+
if mu is None and not (0.0 < loop_bandwidth_normalized < 0.5):
|
|
339
|
+
raise ValueError(
|
|
340
|
+
f"loop_bandwidth_normalized must be in (0, 0.5), got {loop_bandwidth_normalized}."
|
|
341
|
+
)
|
|
342
|
+
mu, beta = resolve_pll_gains(loop_bandwidth_normalized, mu, beta)
|
|
343
|
+
|
|
344
|
+
symbols, xp, _ = dispatch(symbols)
|
|
345
|
+
was_1d = symbols.ndim == 1
|
|
346
|
+
if was_1d:
|
|
347
|
+
symbols = symbols[None, :]
|
|
348
|
+
C, N = symbols.shape
|
|
349
|
+
|
|
350
|
+
# Normalise to unit average power so the effective loop gain is mu regardless
|
|
351
|
+
# of input amplitude. The error signal is e[n] = Im(y[n]*d_hat*), which
|
|
352
|
+
# scales with signal amplitude; without this, the effective gain is mu*A
|
|
353
|
+
# (where A is the RMS amplitude), making loop bandwidth input-dependent.
|
|
354
|
+
symbols = normalize(symbols, mode="average_power", axis=-1)
|
|
355
|
+
|
|
356
|
+
# Constellation on CPU (decisions are scalar operations in the loop)
|
|
357
|
+
const_np = gray_constellation(modulation, order).astype(np.complex128)
|
|
358
|
+
const_r = const_np.real.copy()
|
|
359
|
+
const_i = const_np.imag.copy()
|
|
360
|
+
|
|
361
|
+
# Square-QAM O(1) decision parameters. For square QAM (order a perfect
|
|
362
|
+
# square, e.g. 4/16/64/256/1024) the constellation is a uniform grid and
|
|
363
|
+
# the nearest point can be found by rounding to the closest axis level.
|
|
364
|
+
import math as _math
|
|
365
|
+
|
|
366
|
+
_sq_root = _math.isqrt(order)
|
|
367
|
+
_is_sq_qam = ("qam" in modulation.lower()) and (_sq_root * _sq_root == order)
|
|
368
|
+
if _is_sq_qam:
|
|
369
|
+
_levels = np.unique(const_np.real).astype(np.float64)
|
|
370
|
+
_d_grid = float(_levels[1] - _levels[0]) if len(_levels) > 1 else 1.0
|
|
371
|
+
_lev_min = float(_levels[0])
|
|
372
|
+
_side = _sq_root
|
|
373
|
+
else:
|
|
374
|
+
_levels = np.empty(0, dtype=np.float64)
|
|
375
|
+
_d_grid = 1.0
|
|
376
|
+
_lev_min = 0.0
|
|
377
|
+
_side = 0
|
|
378
|
+
|
|
379
|
+
# Move to CPU for sequential processing
|
|
380
|
+
if xp is not np:
|
|
381
|
+
symbols_cpu = to_device(symbols, "cpu")
|
|
382
|
+
else:
|
|
383
|
+
symbols_cpu = symbols
|
|
384
|
+
|
|
385
|
+
phi_full = np.zeros((C, N), dtype=np.float64)
|
|
386
|
+
use_joint = joint_channels and C > 1
|
|
387
|
+
|
|
388
|
+
# Pre-build (C, N) float64 views used by joint kernels
|
|
389
|
+
if use_joint:
|
|
390
|
+
symbols_np = symbols_cpu.astype(np.complex128)
|
|
391
|
+
sym_r_all = np.ascontiguousarray(symbols_np.real) # (C, N) float64
|
|
392
|
+
sym_i_all = np.ascontiguousarray(symbols_np.imag)
|
|
393
|
+
|
|
394
|
+
if use_joint:
|
|
395
|
+
j_kernel = _get_numba_dd_pll_joint()
|
|
396
|
+
phi_joint = j_kernel(
|
|
397
|
+
sym_r_all,
|
|
398
|
+
sym_i_all,
|
|
399
|
+
const_r,
|
|
400
|
+
const_i,
|
|
401
|
+
float(mu),
|
|
402
|
+
float(beta),
|
|
403
|
+
float(phase_init),
|
|
404
|
+
0.0,
|
|
405
|
+
_is_sq_qam,
|
|
406
|
+
_levels,
|
|
407
|
+
_d_grid,
|
|
408
|
+
_lev_min,
|
|
409
|
+
_side,
|
|
410
|
+
)
|
|
411
|
+
for ch in range(C):
|
|
412
|
+
phi_full[ch] = phi_joint
|
|
413
|
+
else:
|
|
414
|
+
pi_kernel = _get_numba_dd_pll()
|
|
415
|
+
for ch in range(C):
|
|
416
|
+
sym = symbols_cpu[ch].astype(np.complex128)
|
|
417
|
+
phi_full[ch] = pi_kernel(
|
|
418
|
+
sym.real.copy(),
|
|
419
|
+
sym.imag.copy(),
|
|
420
|
+
const_r,
|
|
421
|
+
const_i,
|
|
422
|
+
float(mu),
|
|
423
|
+
float(beta),
|
|
424
|
+
float(phase_init),
|
|
425
|
+
0.0,
|
|
426
|
+
_is_sq_qam,
|
|
427
|
+
_levels,
|
|
428
|
+
_d_grid,
|
|
429
|
+
_lev_min,
|
|
430
|
+
_side,
|
|
431
|
+
)
|
|
432
|
+
loop_order = "2nd" if beta > 0.0 else "1st"
|
|
433
|
+
loop_desc = f"PI {loop_order}-order, mu={mu}, beta={beta}"
|
|
434
|
+
|
|
435
|
+
if cycle_slip_correction:
|
|
436
|
+
if use_joint:
|
|
437
|
+
# All rows are identical - correct once and broadcast
|
|
438
|
+
phi_full[0] = correct_cycle_slips(
|
|
439
|
+
phi_full[0],
|
|
440
|
+
symmetry=4,
|
|
441
|
+
history_length=cycle_slip_history,
|
|
442
|
+
threshold=cycle_slip_threshold,
|
|
443
|
+
)
|
|
444
|
+
for ch in range(1, C):
|
|
445
|
+
phi_full[ch] = phi_full[0]
|
|
446
|
+
else:
|
|
447
|
+
for ch in range(C):
|
|
448
|
+
phi_full[ch] = correct_cycle_slips(
|
|
449
|
+
phi_full[ch],
|
|
450
|
+
symmetry=4,
|
|
451
|
+
history_length=cycle_slip_history,
|
|
452
|
+
threshold=cycle_slip_threshold,
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
# Move result back to original device
|
|
456
|
+
if xp is not np:
|
|
457
|
+
phi_full = xp.asarray(phi_full)
|
|
458
|
+
|
|
459
|
+
if logger.isEnabledFor(logging.INFO):
|
|
460
|
+
# Two reductions + host syncs, needed only for the summary below.
|
|
461
|
+
phi_mean_deg = float(np.mean(phi_full)) * 180.0 / np.pi
|
|
462
|
+
phi_std_deg = float(np.std(phi_full)) * 180.0 / np.pi
|
|
463
|
+
logger.info(
|
|
464
|
+
"CPR (DD-PLL, %s): phase mean=%.2f°, std=%.2f° [C=%s]",
|
|
465
|
+
loop_desc,
|
|
466
|
+
phi_mean_deg,
|
|
467
|
+
phi_std_deg,
|
|
468
|
+
C,
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
if debug_plot:
|
|
472
|
+
from .. import plotting as _plotting
|
|
473
|
+
|
|
474
|
+
_plotting.plot_carrier_phase_trajectory(
|
|
475
|
+
phi_full=phi_full if xp is np else to_device(phi_full, "cpu"),
|
|
476
|
+
show=True,
|
|
477
|
+
title=f"CPR - DD-PLL ({loop_desc})",
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
if was_1d:
|
|
481
|
+
return phi_full[0]
|
|
482
|
+
return phi_full
|