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,1704 @@
|
|
|
1
|
+
"""Numba-compiled sequential equalizer kernels (CPU hot loops)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
# -----------------------------------------------------------------------------
|
|
8
|
+
# NUMBA LAZY LOADER
|
|
9
|
+
# -----------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
_NUMBA_CACHE: dict = {}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _get_numba():
|
|
15
|
+
"""Lazy loader for Numba.
|
|
16
|
+
|
|
17
|
+
Returns the ``numba`` module if installed, else ``None``.
|
|
18
|
+
"""
|
|
19
|
+
if "numba" not in _NUMBA_CACHE:
|
|
20
|
+
try:
|
|
21
|
+
import numba
|
|
22
|
+
|
|
23
|
+
_NUMBA_CACHE["numba"] = numba
|
|
24
|
+
except ImportError:
|
|
25
|
+
_NUMBA_CACHE["numba"] = None
|
|
26
|
+
return _NUMBA_CACHE.get("numba")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# -----------------------------------------------------------------------------
|
|
30
|
+
# NUMBA KERNELS - ADAPTIVE EQUALIZER LOOPS
|
|
31
|
+
# -----------------------------------------------------------------------------
|
|
32
|
+
#
|
|
33
|
+
# Each factory lazily compiles a @njit kernel on first call and caches it in
|
|
34
|
+
# _NUMBA_KERNELS. Numba specialises over argument *types* (all complex64/
|
|
35
|
+
# float32/int32/bool), so a single compiled version handles any (C, T, M).
|
|
36
|
+
#
|
|
37
|
+
# All kernels share the same calling conventions:
|
|
38
|
+
# - x_padded : (C, N_pad) complex64, C-contiguous
|
|
39
|
+
# - W : (C, C, T) complex64, modified **in-place** - caller owns
|
|
40
|
+
# - y_out : (N_sym, C) complex64, pre-allocated output
|
|
41
|
+
# - e_out : (N_sym, C) complex64, pre-allocated output
|
|
42
|
+
# - w_hist_out : (N_sym, C, C, T) if store_weights=True, else (1, C, C, T)
|
|
43
|
+
#
|
|
44
|
+
# Performance tricks:
|
|
45
|
+
# - fastmath=True - LLVM unsafe-math: enables SIMD vectorisation of
|
|
46
|
+
# inner tap loops (AVX-512 on modern CPUs)
|
|
47
|
+
# - cache=True - compiled LLVM IR cached to __pycache__ (*.nbi/*.nbc)
|
|
48
|
+
# - Pre-allocated temporaries (X_wins, y, e, x_bar, Px ...) - no heap
|
|
49
|
+
# allocation inside the hot symbol loop
|
|
50
|
+
# - Manual argmin slicer - explicit loop over M; LLVM can SIMD-vectorise
|
|
51
|
+
# the distance computation for small constellations
|
|
52
|
+
# - In-place W update - no copy per step, minimal working-set pressure
|
|
53
|
+
# - np.conj() on scalars - valid in Numba 0.64+
|
|
54
|
+
|
|
55
|
+
_NUMBA_KERNELS: dict = {}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _get_numba_lms():
|
|
59
|
+
"""JIT-compile and cache the Numba LMS butterfly loop kernel.
|
|
60
|
+
|
|
61
|
+
Returns
|
|
62
|
+
-------
|
|
63
|
+
lms_loop : numba-compiled callable
|
|
64
|
+
See kernel source for argument shapes and semantics.
|
|
65
|
+
"""
|
|
66
|
+
if "lms" not in _NUMBA_KERNELS:
|
|
67
|
+
numba_mod = _get_numba()
|
|
68
|
+
if numba_mod is None:
|
|
69
|
+
raise ImportError("Numba is required for backend='numba'.")
|
|
70
|
+
|
|
71
|
+
@numba_mod.njit(cache=True, fastmath=True, nogil=True)
|
|
72
|
+
def lms_loop(
|
|
73
|
+
x_padded,
|
|
74
|
+
training,
|
|
75
|
+
constellation,
|
|
76
|
+
W,
|
|
77
|
+
step_size,
|
|
78
|
+
n_train,
|
|
79
|
+
stride,
|
|
80
|
+
store_weights,
|
|
81
|
+
y_out,
|
|
82
|
+
e_out,
|
|
83
|
+
w_hist_out,
|
|
84
|
+
sq_lev_min,
|
|
85
|
+
sq_d_grid,
|
|
86
|
+
sq_side,
|
|
87
|
+
):
|
|
88
|
+
# x_padded : (C, N_pad) complex64
|
|
89
|
+
# training : (C, N_sym) complex64
|
|
90
|
+
# constellation : (M,) complex64
|
|
91
|
+
# W : (C, C, num_taps) complex64 - modified in-place
|
|
92
|
+
# step_size : float32 - LMS step size μ; stable when 0 < μ < 2/(C·num_taps·P_x)
|
|
93
|
+
# n_train : int32 - training/DD boundary
|
|
94
|
+
# stride : int - sps (== 2 for T/2-spaced)
|
|
95
|
+
# store_weights : bool
|
|
96
|
+
# y_out : (N_sym, C) complex64 - pre-allocated
|
|
97
|
+
# e_out : (N_sym, C) complex64 - pre-allocated
|
|
98
|
+
# w_hist_out : (N_sym or 1, C, C, T) complex64 - pre-allocated
|
|
99
|
+
C = W.shape[0]
|
|
100
|
+
num_taps = W.shape[2]
|
|
101
|
+
n_sym = y_out.shape[0]
|
|
102
|
+
M = len(constellation)
|
|
103
|
+
|
|
104
|
+
X_wins = np.empty((C, num_taps), dtype=np.complex64)
|
|
105
|
+
y = np.empty(C, dtype=np.complex64)
|
|
106
|
+
e = np.empty(C, dtype=np.complex64)
|
|
107
|
+
|
|
108
|
+
for idx in range(n_sym):
|
|
109
|
+
sample_idx = idx * stride
|
|
110
|
+
|
|
111
|
+
# Window extraction
|
|
112
|
+
for c in range(C):
|
|
113
|
+
for t in range(num_taps):
|
|
114
|
+
X_wins[c, t] = x_padded[c, sample_idx + t]
|
|
115
|
+
|
|
116
|
+
# Butterfly forward pass: y[i] = Σ_j conj(W[i,j]) · X_wins[j]
|
|
117
|
+
for i in range(C):
|
|
118
|
+
acc_re = 0.0
|
|
119
|
+
acc_im = 0.0
|
|
120
|
+
for j in range(C):
|
|
121
|
+
for t in range(num_taps):
|
|
122
|
+
w = W[i, j, t]
|
|
123
|
+
x = X_wins[j, t]
|
|
124
|
+
acc_re += np.float64(w.real) * np.float64(
|
|
125
|
+
x.real
|
|
126
|
+
) + np.float64(w.imag) * np.float64(x.imag)
|
|
127
|
+
acc_im += np.float64(w.real) * np.float64(
|
|
128
|
+
x.imag
|
|
129
|
+
) - np.float64(w.imag) * np.float64(x.real)
|
|
130
|
+
y[i] = acc_re + 1j * acc_im
|
|
131
|
+
y_out[idx, i] = acc_re + 1j * acc_im
|
|
132
|
+
|
|
133
|
+
# Desired symbol: training or decision-directed slicer
|
|
134
|
+
for i in range(C):
|
|
135
|
+
if idx < n_train:
|
|
136
|
+
d_i = training[i, idx]
|
|
137
|
+
elif sq_side > np.int32(0):
|
|
138
|
+
ir = np.int32(np.round((y[i].real - sq_lev_min) / sq_d_grid))
|
|
139
|
+
if ir < np.int32(0):
|
|
140
|
+
ir = np.int32(0)
|
|
141
|
+
if ir >= sq_side:
|
|
142
|
+
ir = sq_side - np.int32(1)
|
|
143
|
+
ii = np.int32(np.round((y[i].imag - sq_lev_min) / sq_d_grid))
|
|
144
|
+
if ii < np.int32(0):
|
|
145
|
+
ii = np.int32(0)
|
|
146
|
+
if ii >= sq_side:
|
|
147
|
+
ii = sq_side - np.int32(1)
|
|
148
|
+
nr = sq_lev_min + np.float32(ir) * sq_d_grid
|
|
149
|
+
ni = sq_lev_min + np.float32(ii) * sq_d_grid
|
|
150
|
+
d_i = np.complex64(nr + ni * np.complex64(1j))
|
|
151
|
+
else:
|
|
152
|
+
min_dist = np.float32(1e38)
|
|
153
|
+
min_idx = 0
|
|
154
|
+
for k in range(M):
|
|
155
|
+
diff = y[i] - constellation[k]
|
|
156
|
+
dist = diff.real * diff.real + diff.imag * diff.imag
|
|
157
|
+
if dist < min_dist:
|
|
158
|
+
min_dist = dist
|
|
159
|
+
min_idx = k
|
|
160
|
+
d_i = constellation[min_idx]
|
|
161
|
+
e[i] = d_i - y[i]
|
|
162
|
+
e_out[idx, i] = e[i]
|
|
163
|
+
|
|
164
|
+
# Weight update: W[i,j,t] += μ * conj(e[i]) * X_wins[j,t]
|
|
165
|
+
for i in range(C):
|
|
166
|
+
ce_i = np.conj(e[i])
|
|
167
|
+
for j in range(C):
|
|
168
|
+
for t in range(num_taps):
|
|
169
|
+
W[i, j, t] = W[i, j, t] + step_size * ce_i * X_wins[j, t]
|
|
170
|
+
|
|
171
|
+
if store_weights:
|
|
172
|
+
for i in range(C):
|
|
173
|
+
for j in range(C):
|
|
174
|
+
for t in range(num_taps):
|
|
175
|
+
w_hist_out[idx, i, j, t] = W[i, j, t]
|
|
176
|
+
|
|
177
|
+
_NUMBA_KERNELS["lms"] = lms_loop
|
|
178
|
+
return _NUMBA_KERNELS["lms"]
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _get_numba_rls():
|
|
182
|
+
"""JIT-compile and cache the Numba Leaky-RLS butterfly loop kernel.
|
|
183
|
+
|
|
184
|
+
Returns
|
|
185
|
+
-------
|
|
186
|
+
rls_loop : numba-compiled callable
|
|
187
|
+
See kernel source for argument shapes and semantics.
|
|
188
|
+
"""
|
|
189
|
+
if "rls" not in _NUMBA_KERNELS:
|
|
190
|
+
numba_mod = _get_numba()
|
|
191
|
+
if numba_mod is None:
|
|
192
|
+
raise ImportError("Numba is required for backend='numba'.")
|
|
193
|
+
|
|
194
|
+
@numba_mod.njit(cache=True, fastmath=True, nogil=True)
|
|
195
|
+
def rls_loop(
|
|
196
|
+
x_padded,
|
|
197
|
+
training,
|
|
198
|
+
constellation,
|
|
199
|
+
W,
|
|
200
|
+
P,
|
|
201
|
+
lam,
|
|
202
|
+
leakage,
|
|
203
|
+
n_train,
|
|
204
|
+
n_update_halt,
|
|
205
|
+
stride,
|
|
206
|
+
store_weights,
|
|
207
|
+
y_out,
|
|
208
|
+
e_out,
|
|
209
|
+
w_hist_out,
|
|
210
|
+
sq_lev_min,
|
|
211
|
+
sq_d_grid,
|
|
212
|
+
sq_side,
|
|
213
|
+
):
|
|
214
|
+
# x_padded : (C, N_pad) complex64
|
|
215
|
+
# training : (C, N_sym) complex64
|
|
216
|
+
# constellation : (M,) complex64
|
|
217
|
+
# W : (C, C, num_taps) complex64 - in-place
|
|
218
|
+
# P : (C*num_taps, C*num_taps) complex128 - in-place
|
|
219
|
+
# lam : float32 - forgetting factor λ ∈ (0, 1]
|
|
220
|
+
# leakage : float32 - weight-decay coefficient γ ∈ [0, 1)
|
|
221
|
+
# n_train : int32 - training/DD boundary
|
|
222
|
+
# n_update_halt : int32 - freeze W,P beyond this index
|
|
223
|
+
# stride : int - sps
|
|
224
|
+
# store_weights : bool
|
|
225
|
+
# y_out : (N_sym, C) complex64
|
|
226
|
+
# e_out : (N_sym, C) complex64
|
|
227
|
+
# w_hist_out : (N_sym or 1, C, C, T) complex64
|
|
228
|
+
C = W.shape[0]
|
|
229
|
+
num_taps = W.shape[2]
|
|
230
|
+
n_sym = y_out.shape[0]
|
|
231
|
+
N = C * num_taps # regressor dimension
|
|
232
|
+
M = len(constellation)
|
|
233
|
+
|
|
234
|
+
x_bar = np.empty(N, dtype=np.complex128)
|
|
235
|
+
Px = np.empty(N, dtype=np.complex128)
|
|
236
|
+
xH_P = np.empty(N, dtype=np.complex128)
|
|
237
|
+
k = np.empty(N, dtype=np.complex128)
|
|
238
|
+
y = np.empty(C, dtype=np.complex64)
|
|
239
|
+
e = np.empty(C, dtype=np.complex64)
|
|
240
|
+
|
|
241
|
+
lam_f64 = np.float64(lam)
|
|
242
|
+
leak_term = np.float32(1.0) - np.float32(leakage)
|
|
243
|
+
|
|
244
|
+
for idx in range(n_sym):
|
|
245
|
+
sample_idx = idx * stride
|
|
246
|
+
|
|
247
|
+
# Flatten regressor directly from input
|
|
248
|
+
for j in range(C):
|
|
249
|
+
for t in range(num_taps):
|
|
250
|
+
x_bar[j * num_taps + t] = x_padded[j, sample_idx + t]
|
|
251
|
+
|
|
252
|
+
# Butterfly forward pass
|
|
253
|
+
for i in range(C):
|
|
254
|
+
acc_re = 0.0
|
|
255
|
+
acc_im = 0.0
|
|
256
|
+
for j in range(C):
|
|
257
|
+
for t in range(num_taps):
|
|
258
|
+
w = W[i, j, t]
|
|
259
|
+
x = x_bar[j * num_taps + t]
|
|
260
|
+
acc_re += np.float64(w.real) * np.float64(
|
|
261
|
+
x.real
|
|
262
|
+
) + np.float64(w.imag) * np.float64(x.imag)
|
|
263
|
+
acc_im += np.float64(w.real) * np.float64(
|
|
264
|
+
x.imag
|
|
265
|
+
) - np.float64(w.imag) * np.float64(x.real)
|
|
266
|
+
y[i] = acc_re + 1j * acc_im
|
|
267
|
+
y_out[idx, i] = acc_re + 1j * acc_im
|
|
268
|
+
|
|
269
|
+
# Desired and error
|
|
270
|
+
for i in range(C):
|
|
271
|
+
if idx < n_train:
|
|
272
|
+
d_i = training[i, idx]
|
|
273
|
+
elif sq_side > np.int32(0):
|
|
274
|
+
ir = np.int32(np.round((y[i].real - sq_lev_min) / sq_d_grid))
|
|
275
|
+
if ir < np.int32(0):
|
|
276
|
+
ir = np.int32(0)
|
|
277
|
+
if ir >= sq_side:
|
|
278
|
+
ir = sq_side - np.int32(1)
|
|
279
|
+
ii = np.int32(np.round((y[i].imag - sq_lev_min) / sq_d_grid))
|
|
280
|
+
if ii < np.int32(0):
|
|
281
|
+
ii = np.int32(0)
|
|
282
|
+
if ii >= sq_side:
|
|
283
|
+
ii = sq_side - np.int32(1)
|
|
284
|
+
nr = sq_lev_min + np.float32(ir) * sq_d_grid
|
|
285
|
+
ni = sq_lev_min + np.float32(ii) * sq_d_grid
|
|
286
|
+
d_i = np.complex64(nr + ni * np.complex64(1j))
|
|
287
|
+
else:
|
|
288
|
+
min_dist = np.float32(1e38)
|
|
289
|
+
min_idx = 0
|
|
290
|
+
for kk in range(M):
|
|
291
|
+
diff = y[i] - constellation[kk]
|
|
292
|
+
dist = diff.real * diff.real + diff.imag * diff.imag
|
|
293
|
+
if dist < min_dist:
|
|
294
|
+
min_dist = dist
|
|
295
|
+
min_idx = kk
|
|
296
|
+
d_i = constellation[min_idx]
|
|
297
|
+
e[i] = d_i - y[i]
|
|
298
|
+
e_out[idx, i] = e[i]
|
|
299
|
+
|
|
300
|
+
# Kalman gain: Px = P @ x_bar (in-place, no heap allocation)
|
|
301
|
+
for ii in range(N):
|
|
302
|
+
acc_re = 0.0
|
|
303
|
+
acc_im = 0.0
|
|
304
|
+
for jj in range(N):
|
|
305
|
+
p_val = P[ii, jj]
|
|
306
|
+
x_val = x_bar[jj]
|
|
307
|
+
acc_re += p_val.real * x_val.real - p_val.imag * x_val.imag
|
|
308
|
+
acc_im += p_val.real * x_val.imag + p_val.imag * x_val.real
|
|
309
|
+
Px[ii] = acc_re + 1j * acc_im
|
|
310
|
+
|
|
311
|
+
# denom = λ + real(conj(x_bar) · Px)
|
|
312
|
+
denom = lam_f64
|
|
313
|
+
for jj in range(N):
|
|
314
|
+
denom = denom + (np.conj(x_bar[jj]) * Px[jj]).real
|
|
315
|
+
|
|
316
|
+
# k = Px / denom
|
|
317
|
+
inv_denom = np.float64(1.0) / denom
|
|
318
|
+
for ii in range(N):
|
|
319
|
+
k[ii] = Px[ii] * inv_denom
|
|
320
|
+
|
|
321
|
+
if idx < n_update_halt:
|
|
322
|
+
# Leaky W update: W[i,j,t] = (1-γ)W[i,j,t] + k[j*T+t]*conj(e[i])
|
|
323
|
+
for i in range(C):
|
|
324
|
+
ce_i = np.conj(e[i])
|
|
325
|
+
for j in range(C):
|
|
326
|
+
for t in range(num_taps):
|
|
327
|
+
W[i, j, t] = np.complex64(
|
|
328
|
+
leak_term * W[i, j, t] + k[j * num_taps + t] * ce_i
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
# Exploit Hermitian symmetry P = P^H: for each j, (x^H P)[j] = conj((Px)[j]).
|
|
332
|
+
# Reuse the already-computed Px to avoid a second O(N²) mat-vec.
|
|
333
|
+
for jj in range(N):
|
|
334
|
+
xH_P[jj] = np.conj(Px[jj])
|
|
335
|
+
|
|
336
|
+
# Riccati + Hermitian re-symmetrization in one pass (upper
|
|
337
|
+
# triangle only). Halves memory traffic vs. two separate
|
|
338
|
+
# O(N²) loops; correctness is identical.
|
|
339
|
+
for ii in range(N):
|
|
340
|
+
for jj in range(ii, N):
|
|
341
|
+
val = (P[ii, jj] - k[ii] * xH_P[jj]) / lam_f64
|
|
342
|
+
if ii == jj:
|
|
343
|
+
P[ii, ii] = np.complex128(val.real)
|
|
344
|
+
else:
|
|
345
|
+
conj_val = (P[jj, ii] - k[jj] * xH_P[ii]) / lam_f64
|
|
346
|
+
avg = (val + np.conj(conj_val)) * np.float64(0.5)
|
|
347
|
+
P[ii, jj] = avg
|
|
348
|
+
P[jj, ii] = np.conj(avg)
|
|
349
|
+
|
|
350
|
+
if store_weights:
|
|
351
|
+
for i in range(C):
|
|
352
|
+
for j in range(C):
|
|
353
|
+
for t in range(num_taps):
|
|
354
|
+
w_hist_out[idx, i, j, t] = W[i, j, t]
|
|
355
|
+
|
|
356
|
+
_NUMBA_KERNELS["rls"] = rls_loop
|
|
357
|
+
return _NUMBA_KERNELS["rls"]
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _get_numba_lms_cpr():
|
|
361
|
+
"""JIT-compile and cache the Numba LMS+CPR butterfly loop kernel.
|
|
362
|
+
|
|
363
|
+
Extends the baseline LMS kernel with an inline carrier phase tracker
|
|
364
|
+
(PLL or single-shot BPS) and a causal cycle-slip corrector.
|
|
365
|
+
|
|
366
|
+
cpr_mode 1 = PLL (DD second-order PI loop).
|
|
367
|
+
cpr_mode 2 = BPS (block-averaged Blind Phase Search, window=bps_block_size).
|
|
368
|
+
|
|
369
|
+
Returns
|
|
370
|
+
-------
|
|
371
|
+
lms_cpr_loop : numba-compiled callable
|
|
372
|
+
"""
|
|
373
|
+
if "lms_cpr" not in _NUMBA_KERNELS:
|
|
374
|
+
numba_mod = _get_numba()
|
|
375
|
+
if numba_mod is None:
|
|
376
|
+
raise ImportError("Numba is required for backend='numba'.")
|
|
377
|
+
|
|
378
|
+
@numba_mod.njit(cache=True, fastmath=False, nogil=True)
|
|
379
|
+
def lms_cpr_loop(
|
|
380
|
+
x_padded,
|
|
381
|
+
training,
|
|
382
|
+
constellation,
|
|
383
|
+
bps_phases_neg,
|
|
384
|
+
bps_angles,
|
|
385
|
+
bps_block_size,
|
|
386
|
+
bps_joint_channels,
|
|
387
|
+
W,
|
|
388
|
+
step_size,
|
|
389
|
+
n_train,
|
|
390
|
+
stride,
|
|
391
|
+
store_weights,
|
|
392
|
+
cpr_mode,
|
|
393
|
+
pll_mu,
|
|
394
|
+
pll_beta,
|
|
395
|
+
symmetry,
|
|
396
|
+
cs_enabled,
|
|
397
|
+
cs_threshold,
|
|
398
|
+
pll_phi,
|
|
399
|
+
pll_freq,
|
|
400
|
+
cs_buf_x,
|
|
401
|
+
cs_buf_y,
|
|
402
|
+
cs_buf_ptr,
|
|
403
|
+
cs_buf_n,
|
|
404
|
+
cs_stats,
|
|
405
|
+
bps_prev4,
|
|
406
|
+
y_out,
|
|
407
|
+
e_out,
|
|
408
|
+
phase_out,
|
|
409
|
+
w_hist_out,
|
|
410
|
+
sq_lev_min,
|
|
411
|
+
sq_d_grid,
|
|
412
|
+
sq_side,
|
|
413
|
+
):
|
|
414
|
+
# x_padded : (C, N_pad) complex64
|
|
415
|
+
# training : (C, N_sym) complex64
|
|
416
|
+
# constellation : (M,) complex64
|
|
417
|
+
# bps_phases_neg : (B,) complex64 exp(-j*theta_k)
|
|
418
|
+
# bps_angles : (B,) float32 theta_k
|
|
419
|
+
# bps_block_size : int32 BPS averaging window (≥1)
|
|
420
|
+
# bps_joint_channels: bool True -> joint metric across C channels
|
|
421
|
+
# W : (C, C, T) complex64 - in-place
|
|
422
|
+
# step_size : float32
|
|
423
|
+
# n_train : int32
|
|
424
|
+
# stride : int
|
|
425
|
+
# store_weights : bool
|
|
426
|
+
# cpr_mode : int32 1=pll 2=bps
|
|
427
|
+
# pll_mu : float32
|
|
428
|
+
# pll_beta : float32
|
|
429
|
+
# symmetry : int32 - cycle-slip quantum = 2π/symmetry
|
|
430
|
+
# cs_enabled : bool
|
|
431
|
+
# cs_threshold : float32
|
|
432
|
+
# pll_phi : (C,) float64 - in-place
|
|
433
|
+
# pll_freq : (C,) float64 - in-place
|
|
434
|
+
# cs_buf_x : (C, H) float64 - in-place
|
|
435
|
+
# cs_buf_y : (C, H) float64 - in-place
|
|
436
|
+
# cs_buf_ptr : (C,) int64 - in-place
|
|
437
|
+
# cs_buf_n : (C,) int64 - in-place
|
|
438
|
+
# cs_stats : (C, 4) float64 [Sx,Sy,Sxx,Sxy] - in-place
|
|
439
|
+
# bps_prev4 : (C,) float64 - causal 4-fold unwrap state - in-place
|
|
440
|
+
# y_out : (N_sym, C) complex64
|
|
441
|
+
# e_out : (N_sym, C) complex64
|
|
442
|
+
# phase_out : (N_sym, C) float32
|
|
443
|
+
# w_hist_out : (N_sym or 1, C, C, T) complex64
|
|
444
|
+
C = W.shape[0]
|
|
445
|
+
num_taps = W.shape[2]
|
|
446
|
+
n_sym = y_out.shape[0]
|
|
447
|
+
M = len(constellation)
|
|
448
|
+
B = len(bps_phases_neg)
|
|
449
|
+
H = cs_buf_x.shape[1]
|
|
450
|
+
two_pi = np.float64(2.0 * np.pi)
|
|
451
|
+
quantum = two_pi / np.float64(symmetry)
|
|
452
|
+
|
|
453
|
+
X_wins = np.empty((C, num_taps), dtype=np.complex64)
|
|
454
|
+
y_raw = np.empty(C, dtype=np.complex64)
|
|
455
|
+
y_fin = np.empty(C, dtype=np.complex64)
|
|
456
|
+
phi_c = np.empty(C, dtype=np.float64)
|
|
457
|
+
d_sym = np.empty(C, dtype=np.complex64)
|
|
458
|
+
e_clean = np.empty(C, dtype=np.complex64)
|
|
459
|
+
e_eq = np.empty(C, dtype=np.complex64)
|
|
460
|
+
phi_hat_bps = np.zeros(C, dtype=np.float64)
|
|
461
|
+
# BPS: incremental running-sum (avoids O(K·B·M) per-symbol rescan)
|
|
462
|
+
bps_dist_buf = np.zeros((C, bps_block_size, B), dtype=np.float32)
|
|
463
|
+
bps_running_sum = np.zeros((C, B), dtype=np.float64)
|
|
464
|
+
bps_dist_ptr = np.int64(0)
|
|
465
|
+
|
|
466
|
+
for idx in range(n_sym):
|
|
467
|
+
sample_idx = idx * stride
|
|
468
|
+
|
|
469
|
+
# Window extraction
|
|
470
|
+
for c in range(C):
|
|
471
|
+
for t in range(num_taps):
|
|
472
|
+
X_wins[c, t] = x_padded[c, sample_idx + t]
|
|
473
|
+
|
|
474
|
+
# Butterfly forward pass: y_raw[i] = Σ_j conj(W[i,j]) · X
|
|
475
|
+
for i in range(C):
|
|
476
|
+
acc_re = 0.0
|
|
477
|
+
acc_im = 0.0
|
|
478
|
+
for j in range(C):
|
|
479
|
+
for t in range(num_taps):
|
|
480
|
+
w = W[i, j, t]
|
|
481
|
+
x = X_wins[j, t]
|
|
482
|
+
acc_re += np.float64(w.real) * np.float64(
|
|
483
|
+
x.real
|
|
484
|
+
) + np.float64(w.imag) * np.float64(x.imag)
|
|
485
|
+
acc_im += np.float64(w.real) * np.float64(
|
|
486
|
+
x.imag
|
|
487
|
+
) - np.float64(w.imag) * np.float64(x.real)
|
|
488
|
+
y_raw[i] = acc_re + 1j * acc_im
|
|
489
|
+
|
|
490
|
+
# -- CPR: Phase estimation ----------------------------------
|
|
491
|
+
|
|
492
|
+
# BPS: incremental running-sum over a causal window of K symbols.
|
|
493
|
+
# For each new symbol, compute min-dist for all B candidates,
|
|
494
|
+
# subtract the oldest slot from the running sum and add the new
|
|
495
|
+
# one - O(B·M·C) per symbol, independent of window size K.
|
|
496
|
+
# O(1) square-QAM path: snap real/imag to nearest level grid point.
|
|
497
|
+
if cpr_mode == 2:
|
|
498
|
+
slot = bps_dist_ptr % np.int64(bps_block_size)
|
|
499
|
+
for i in range(C):
|
|
500
|
+
for k in range(B):
|
|
501
|
+
y_rot = y_raw[i] * bps_phases_neg[k]
|
|
502
|
+
if sq_side > np.int32(0):
|
|
503
|
+
ir = np.int32(
|
|
504
|
+
np.round((y_rot.real - sq_lev_min) / sq_d_grid)
|
|
505
|
+
)
|
|
506
|
+
if ir < np.int32(0):
|
|
507
|
+
ir = np.int32(0)
|
|
508
|
+
if ir >= sq_side:
|
|
509
|
+
ir = sq_side - np.int32(1)
|
|
510
|
+
ii = np.int32(
|
|
511
|
+
np.round((y_rot.imag - sq_lev_min) / sq_d_grid)
|
|
512
|
+
)
|
|
513
|
+
if ii < np.int32(0):
|
|
514
|
+
ii = np.int32(0)
|
|
515
|
+
if ii >= sq_side:
|
|
516
|
+
ii = sq_side - np.int32(1)
|
|
517
|
+
nr = sq_lev_min + np.float32(ir) * sq_d_grid
|
|
518
|
+
ni = sq_lev_min + np.float32(ii) * sq_d_grid
|
|
519
|
+
d2_min = (y_rot.real - nr) ** 2 + (y_rot.imag - ni) ** 2
|
|
520
|
+
else:
|
|
521
|
+
d2_min = np.float32(1e38)
|
|
522
|
+
for m in range(M):
|
|
523
|
+
dv = y_rot - constellation[m]
|
|
524
|
+
d2 = dv.real * dv.real + dv.imag * dv.imag
|
|
525
|
+
if d2 < d2_min:
|
|
526
|
+
d2_min = d2
|
|
527
|
+
# Subtract evicted slot, add new slot
|
|
528
|
+
bps_running_sum[i, k] = (
|
|
529
|
+
bps_running_sum[i, k]
|
|
530
|
+
- bps_dist_buf[i, slot, k]
|
|
531
|
+
+ d2_min
|
|
532
|
+
)
|
|
533
|
+
bps_dist_buf[i, slot, k] = d2_min
|
|
534
|
+
bps_dist_ptr = bps_dist_ptr + np.int64(1)
|
|
535
|
+
|
|
536
|
+
if bps_joint_channels:
|
|
537
|
+
best_k_joint = np.int32(0)
|
|
538
|
+
min_tot_joint = np.float64(1e300)
|
|
539
|
+
for k in range(B):
|
|
540
|
+
metric_k = np.float64(0.0)
|
|
541
|
+
for i in range(C):
|
|
542
|
+
metric_k = metric_k + bps_running_sum[i, k]
|
|
543
|
+
if metric_k < min_tot_joint:
|
|
544
|
+
min_tot_joint = metric_k
|
|
545
|
+
best_k_joint = k
|
|
546
|
+
# Raw argmin in [0, π/2); apply 4-fold causal unwrap
|
|
547
|
+
raw4 = np.float64(bps_angles[best_k_joint]) * np.float64(4.0)
|
|
548
|
+
for i in range(C):
|
|
549
|
+
diff4 = raw4 - bps_prev4[i]
|
|
550
|
+
diff4 = diff4 - np.float64(2.0 * np.pi) * np.round(
|
|
551
|
+
diff4 / (np.float64(2.0 * np.pi))
|
|
552
|
+
)
|
|
553
|
+
bps_prev4[i] = bps_prev4[i] + diff4
|
|
554
|
+
phi_hat_bps[i] = bps_prev4[i] / np.float64(4.0)
|
|
555
|
+
else:
|
|
556
|
+
for i in range(C):
|
|
557
|
+
best_k = np.int32(0)
|
|
558
|
+
min_tot = np.float64(1e300)
|
|
559
|
+
for k in range(B):
|
|
560
|
+
if bps_running_sum[i, k] < min_tot:
|
|
561
|
+
min_tot = bps_running_sum[i, k]
|
|
562
|
+
best_k = k
|
|
563
|
+
# Raw argmin in [0, π/2); apply 4-fold causal unwrap
|
|
564
|
+
raw4 = np.float64(bps_angles[best_k]) * np.float64(4.0)
|
|
565
|
+
diff4 = raw4 - bps_prev4[i]
|
|
566
|
+
diff4 = diff4 - np.float64(2.0 * np.pi) * np.round(
|
|
567
|
+
diff4 / (np.float64(2.0 * np.pi))
|
|
568
|
+
)
|
|
569
|
+
bps_prev4[i] = bps_prev4[i] + diff4
|
|
570
|
+
phi_hat_bps[i] = bps_prev4[i] / np.float64(4.0)
|
|
571
|
+
|
|
572
|
+
for i in range(C):
|
|
573
|
+
if cpr_mode == 1: # PLL: read current integrator state
|
|
574
|
+
phi_hat = pll_phi[i]
|
|
575
|
+
else: # BPS: unwrapped phase estimate
|
|
576
|
+
phi_hat = phi_hat_bps[i]
|
|
577
|
+
|
|
578
|
+
# -- Cycle-slip correction ------------------------------
|
|
579
|
+
if cs_enabled:
|
|
580
|
+
n_b = cs_buf_n[i]
|
|
581
|
+
ptr = cs_buf_ptr[i]
|
|
582
|
+
y_b = np.float64(phi_hat)
|
|
583
|
+
|
|
584
|
+
if n_b == 0:
|
|
585
|
+
phi_corr = phi_hat
|
|
586
|
+
else:
|
|
587
|
+
if n_b < np.int64(10):
|
|
588
|
+
last_pos = (ptr - np.int64(1) + np.int64(H)) % np.int64(
|
|
589
|
+
H
|
|
590
|
+
)
|
|
591
|
+
phi_expected = cs_buf_y[i, last_pos]
|
|
592
|
+
else:
|
|
593
|
+
# cs_stats layout: [0]=Sy, [1]=Sxy (relative coords)
|
|
594
|
+
Sy = cs_stats[i, 0]
|
|
595
|
+
Sxy = cs_stats[i, 1]
|
|
596
|
+
n_f = np.float64(n_b)
|
|
597
|
+
if n_b < np.int64(H):
|
|
598
|
+
Sx_c = (
|
|
599
|
+
n_f * (n_f - np.float64(1.0)) / np.float64(2.0)
|
|
600
|
+
)
|
|
601
|
+
Sxx_c = (
|
|
602
|
+
n_f
|
|
603
|
+
* (n_f - np.float64(1.0))
|
|
604
|
+
* (np.float64(2.0) * n_f - np.float64(1.0))
|
|
605
|
+
/ np.float64(6.0)
|
|
606
|
+
)
|
|
607
|
+
denom = n_f * Sxx_c - Sx_c * Sx_c
|
|
608
|
+
else:
|
|
609
|
+
H_f = np.float64(H)
|
|
610
|
+
Sx_c = (
|
|
611
|
+
H_f * (H_f - np.float64(1.0)) / np.float64(2.0)
|
|
612
|
+
)
|
|
613
|
+
Sxx_c = (
|
|
614
|
+
H_f
|
|
615
|
+
* (H_f - np.float64(1.0))
|
|
616
|
+
* (np.float64(2.0) * H_f - np.float64(1.0))
|
|
617
|
+
/ np.float64(6.0)
|
|
618
|
+
)
|
|
619
|
+
denom = H_f * Sxx_c - Sx_c * Sx_c
|
|
620
|
+
if np.abs(denom) > np.float64(1e-30):
|
|
621
|
+
slope = (n_f * Sxy - Sx_c * Sy) / denom
|
|
622
|
+
intercept = (Sy - slope * Sx_c) / n_f
|
|
623
|
+
else:
|
|
624
|
+
slope = np.float64(0.0)
|
|
625
|
+
intercept = Sy / n_f
|
|
626
|
+
phi_expected = slope * n_f + intercept
|
|
627
|
+
|
|
628
|
+
diff = y_b - phi_expected
|
|
629
|
+
k_slip = np.int64(np.round(diff / quantum))
|
|
630
|
+
if np.abs(diff) > np.float64(
|
|
631
|
+
cs_threshold
|
|
632
|
+
) and k_slip != np.int64(0):
|
|
633
|
+
y_b = y_b - np.float64(k_slip) * quantum
|
|
634
|
+
phi_corr = y_b
|
|
635
|
+
|
|
636
|
+
# Update circular buffer - relative coords, only y needed
|
|
637
|
+
write_pos = ptr % np.int64(H)
|
|
638
|
+
if n_b == np.int64(H):
|
|
639
|
+
old_y = cs_buf_y[i, write_pos]
|
|
640
|
+
H_f = np.float64(H)
|
|
641
|
+
old_Sy = cs_stats[i, 0]
|
|
642
|
+
# Sxy_new = Sxy_old - Sy_old + y_old + (H-1)*y_new
|
|
643
|
+
cs_stats[i, 1] = (
|
|
644
|
+
cs_stats[i, 1]
|
|
645
|
+
- old_Sy
|
|
646
|
+
+ old_y
|
|
647
|
+
+ (H_f - np.float64(1.0)) * phi_corr
|
|
648
|
+
)
|
|
649
|
+
cs_stats[i, 0] = old_Sy - old_y + phi_corr
|
|
650
|
+
else:
|
|
651
|
+
n_b_f = np.float64(n_b)
|
|
652
|
+
cs_stats[i, 1] += n_b_f * phi_corr
|
|
653
|
+
cs_stats[i, 0] += phi_corr
|
|
654
|
+
cs_buf_y[i, write_pos] = phi_corr
|
|
655
|
+
cs_buf_ptr[i] = ptr + np.int64(1)
|
|
656
|
+
if n_b < np.int64(H):
|
|
657
|
+
cs_buf_n[i] = n_b + np.int64(1)
|
|
658
|
+
else:
|
|
659
|
+
phi_corr = np.float64(phi_hat)
|
|
660
|
+
|
|
661
|
+
phi_c[i] = phi_corr
|
|
662
|
+
phase_out[idx, i] = phi_corr
|
|
663
|
+
|
|
664
|
+
# y_final = y_raw * exp(-j * phi_corr)
|
|
665
|
+
cos_p = np.cos(phi_corr)
|
|
666
|
+
sin_p = np.sin(phi_corr)
|
|
667
|
+
yr = y_raw[i].real * np.float32(cos_p) + y_raw[i].imag * np.float32(
|
|
668
|
+
sin_p
|
|
669
|
+
)
|
|
670
|
+
yi = -y_raw[i].real * np.float32(sin_p) + y_raw[
|
|
671
|
+
i
|
|
672
|
+
].imag * np.float32(cos_p)
|
|
673
|
+
y_fin[i] = np.complex64(yr + yi * np.complex64(1j))
|
|
674
|
+
y_out[idx, i] = y_fin[i]
|
|
675
|
+
|
|
676
|
+
# -- Slicer and error ---------------------------------------
|
|
677
|
+
for i in range(C):
|
|
678
|
+
if idx < n_train:
|
|
679
|
+
d_i = training[i, idx]
|
|
680
|
+
elif sq_side > np.int32(0):
|
|
681
|
+
ir = np.int32(
|
|
682
|
+
np.round((y_fin[i].real - sq_lev_min) / sq_d_grid)
|
|
683
|
+
)
|
|
684
|
+
if ir < np.int32(0):
|
|
685
|
+
ir = np.int32(0)
|
|
686
|
+
if ir >= sq_side:
|
|
687
|
+
ir = sq_side - np.int32(1)
|
|
688
|
+
ii = np.int32(
|
|
689
|
+
np.round((y_fin[i].imag - sq_lev_min) / sq_d_grid)
|
|
690
|
+
)
|
|
691
|
+
if ii < np.int32(0):
|
|
692
|
+
ii = np.int32(0)
|
|
693
|
+
if ii >= sq_side:
|
|
694
|
+
ii = sq_side - np.int32(1)
|
|
695
|
+
nr = sq_lev_min + np.float32(ir) * sq_d_grid
|
|
696
|
+
ni = sq_lev_min + np.float32(ii) * sq_d_grid
|
|
697
|
+
d_i = np.complex64(nr + ni * np.complex64(1j))
|
|
698
|
+
else:
|
|
699
|
+
min_dist = np.float32(1e38)
|
|
700
|
+
min_idx = 0
|
|
701
|
+
for k in range(M):
|
|
702
|
+
dv = y_fin[i] - constellation[k]
|
|
703
|
+
dist = dv.real * dv.real + dv.imag * dv.imag
|
|
704
|
+
if dist < min_dist:
|
|
705
|
+
min_dist = dist
|
|
706
|
+
min_idx = k
|
|
707
|
+
d_i = constellation[min_idx]
|
|
708
|
+
d_sym[i] = d_i
|
|
709
|
+
e_clean[i] = d_i - y_fin[i]
|
|
710
|
+
e_out[idx, i] = e_clean[i]
|
|
711
|
+
|
|
712
|
+
# -- PLL state update ---------------------------------------
|
|
713
|
+
if cpr_mode == 1:
|
|
714
|
+
if bps_joint_channels:
|
|
715
|
+
# Average phase error across channels (shared LO)
|
|
716
|
+
e_ph_sum = np.float32(0.0)
|
|
717
|
+
for i in range(C):
|
|
718
|
+
e_ph_sum = e_ph_sum + (
|
|
719
|
+
y_fin[i].imag * d_sym[i].real
|
|
720
|
+
- y_fin[i].real * d_sym[i].imag
|
|
721
|
+
)
|
|
722
|
+
e_ph_avg = e_ph_sum / np.float32(C)
|
|
723
|
+
for i in range(C):
|
|
724
|
+
pll_phi[i] = pll_phi[i] + pll_mu * e_ph_avg + pll_freq[i]
|
|
725
|
+
pll_freq[i] = pll_freq[i] + pll_beta * e_ph_avg
|
|
726
|
+
else:
|
|
727
|
+
for i in range(C):
|
|
728
|
+
# Cross-product phase detector: Im(y_fin · conj(d))
|
|
729
|
+
e_ph = (
|
|
730
|
+
y_fin[i].imag * d_sym[i].real
|
|
731
|
+
- y_fin[i].real * d_sym[i].imag
|
|
732
|
+
)
|
|
733
|
+
pll_phi[i] = (
|
|
734
|
+
pll_phi[i] + pll_mu * np.float32(e_ph) + pll_freq[i]
|
|
735
|
+
)
|
|
736
|
+
pll_freq[i] = pll_freq[i] + pll_beta * np.float32(e_ph)
|
|
737
|
+
|
|
738
|
+
# -- De-rotate error and weight update ----------------------
|
|
739
|
+
for i in range(C):
|
|
740
|
+
# e_eq = e_clean * exp(+j * phi_corr) (back to pre-rotation plane)
|
|
741
|
+
cos_p = np.cos(phi_c[i])
|
|
742
|
+
sin_p = np.sin(phi_c[i])
|
|
743
|
+
er = e_clean[i].real * np.float32(cos_p) - e_clean[
|
|
744
|
+
i
|
|
745
|
+
].imag * np.float32(sin_p)
|
|
746
|
+
ei = e_clean[i].real * np.float32(sin_p) + e_clean[
|
|
747
|
+
i
|
|
748
|
+
].imag * np.float32(cos_p)
|
|
749
|
+
e_eq[i] = np.complex64(er + ei * np.complex64(1j))
|
|
750
|
+
|
|
751
|
+
for i in range(C):
|
|
752
|
+
ce_i = np.conj(e_eq[i])
|
|
753
|
+
for j in range(C):
|
|
754
|
+
for t in range(num_taps):
|
|
755
|
+
W[i, j, t] = W[i, j, t] + step_size * ce_i * X_wins[j, t]
|
|
756
|
+
|
|
757
|
+
if store_weights:
|
|
758
|
+
for i in range(C):
|
|
759
|
+
for j in range(C):
|
|
760
|
+
for t in range(num_taps):
|
|
761
|
+
w_hist_out[idx, i, j, t] = W[i, j, t]
|
|
762
|
+
|
|
763
|
+
_NUMBA_KERNELS["lms_cpr"] = lms_cpr_loop
|
|
764
|
+
return _NUMBA_KERNELS["lms_cpr"]
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
def _get_numba_rls_cpr():
|
|
768
|
+
"""JIT-compile and cache the Numba RLS+CPR butterfly loop kernel.
|
|
769
|
+
|
|
770
|
+
Combines the Leaky-RLS Riccati update with the same inline CPR tracker
|
|
771
|
+
used by the LMS CPR kernel.
|
|
772
|
+
|
|
773
|
+
Returns
|
|
774
|
+
-------
|
|
775
|
+
rls_cpr_loop : numba-compiled callable
|
|
776
|
+
"""
|
|
777
|
+
if "rls_cpr" not in _NUMBA_KERNELS:
|
|
778
|
+
numba_mod = _get_numba()
|
|
779
|
+
if numba_mod is None:
|
|
780
|
+
raise ImportError("Numba is required for backend='numba'.")
|
|
781
|
+
|
|
782
|
+
@numba_mod.njit(cache=True, fastmath=False, nogil=True)
|
|
783
|
+
def rls_cpr_loop(
|
|
784
|
+
x_padded,
|
|
785
|
+
training,
|
|
786
|
+
constellation,
|
|
787
|
+
bps_phases_neg,
|
|
788
|
+
bps_angles,
|
|
789
|
+
bps_block_size,
|
|
790
|
+
bps_joint_channels,
|
|
791
|
+
W,
|
|
792
|
+
P,
|
|
793
|
+
lam,
|
|
794
|
+
leakage,
|
|
795
|
+
n_train,
|
|
796
|
+
n_update_halt,
|
|
797
|
+
stride,
|
|
798
|
+
store_weights,
|
|
799
|
+
cpr_mode,
|
|
800
|
+
pll_mu,
|
|
801
|
+
pll_beta,
|
|
802
|
+
symmetry,
|
|
803
|
+
cs_enabled,
|
|
804
|
+
cs_threshold,
|
|
805
|
+
pll_phi,
|
|
806
|
+
pll_freq,
|
|
807
|
+
cs_buf_x,
|
|
808
|
+
cs_buf_y,
|
|
809
|
+
cs_buf_ptr,
|
|
810
|
+
cs_buf_n,
|
|
811
|
+
cs_stats,
|
|
812
|
+
bps_prev4,
|
|
813
|
+
y_out,
|
|
814
|
+
e_out,
|
|
815
|
+
phase_out,
|
|
816
|
+
w_hist_out,
|
|
817
|
+
sq_lev_min,
|
|
818
|
+
sq_d_grid,
|
|
819
|
+
sq_side,
|
|
820
|
+
):
|
|
821
|
+
# Same shapes as rls_loop plus CPR state - see lms_cpr_loop header.
|
|
822
|
+
C = W.shape[0]
|
|
823
|
+
num_taps = W.shape[2]
|
|
824
|
+
n_sym = y_out.shape[0]
|
|
825
|
+
N = C * num_taps
|
|
826
|
+
M = len(constellation)
|
|
827
|
+
B = len(bps_phases_neg)
|
|
828
|
+
H = cs_buf_x.shape[1]
|
|
829
|
+
two_pi = np.float64(2.0 * np.pi)
|
|
830
|
+
quantum = two_pi / np.float64(symmetry)
|
|
831
|
+
|
|
832
|
+
x_bar = np.empty(N, dtype=np.complex128)
|
|
833
|
+
Px = np.empty(N, dtype=np.complex128)
|
|
834
|
+
xH_P = np.empty(N, dtype=np.complex128)
|
|
835
|
+
k_gain = np.empty(N, dtype=np.complex128)
|
|
836
|
+
y_raw = np.empty(C, dtype=np.complex64)
|
|
837
|
+
y_fin = np.empty(C, dtype=np.complex64)
|
|
838
|
+
phi_c = np.empty(C, dtype=np.float64)
|
|
839
|
+
d_sym = np.empty(C, dtype=np.complex64)
|
|
840
|
+
e_clean = np.empty(C, dtype=np.complex64)
|
|
841
|
+
e_eq = np.empty(C, dtype=np.complex64)
|
|
842
|
+
phi_hat_bps = np.zeros(C, dtype=np.float64)
|
|
843
|
+
bps_dist_buf = np.zeros((C, bps_block_size, B), dtype=np.float32)
|
|
844
|
+
bps_running_sum = np.zeros((C, B), dtype=np.float64)
|
|
845
|
+
bps_dist_ptr = np.int64(0)
|
|
846
|
+
|
|
847
|
+
lam_f64 = np.float64(lam)
|
|
848
|
+
leak_term = np.float32(1.0) - np.float32(leakage)
|
|
849
|
+
|
|
850
|
+
for idx in range(n_sym):
|
|
851
|
+
sample_idx = idx * stride
|
|
852
|
+
|
|
853
|
+
for j in range(C):
|
|
854
|
+
for t in range(num_taps):
|
|
855
|
+
x_bar[j * num_taps + t] = x_padded[j, sample_idx + t]
|
|
856
|
+
|
|
857
|
+
# Butterfly forward pass
|
|
858
|
+
for i in range(C):
|
|
859
|
+
acc_re = 0.0
|
|
860
|
+
acc_im = 0.0
|
|
861
|
+
for j in range(C):
|
|
862
|
+
for t in range(num_taps):
|
|
863
|
+
w = W[i, j, t]
|
|
864
|
+
x = x_bar[j * num_taps + t]
|
|
865
|
+
acc_re += np.float64(w.real) * np.float64(
|
|
866
|
+
x.real
|
|
867
|
+
) + np.float64(w.imag) * np.float64(x.imag)
|
|
868
|
+
acc_im += np.float64(w.real) * np.float64(
|
|
869
|
+
x.imag
|
|
870
|
+
) - np.float64(w.imag) * np.float64(x.real)
|
|
871
|
+
y_raw[i] = acc_re + 1j * acc_im
|
|
872
|
+
|
|
873
|
+
# -- CPR: Phase estimation ----------------------------------
|
|
874
|
+
|
|
875
|
+
# BPS: incremental running-sum (O(B·M·C) per symbol, independent of K)
|
|
876
|
+
# O(1) square-QAM path: snap real/imag to nearest level grid point.
|
|
877
|
+
if cpr_mode == 2:
|
|
878
|
+
slot = bps_dist_ptr % np.int64(bps_block_size)
|
|
879
|
+
for i in range(C):
|
|
880
|
+
for k in range(B):
|
|
881
|
+
y_rot = y_raw[i] * bps_phases_neg[k]
|
|
882
|
+
if sq_side > np.int32(0):
|
|
883
|
+
ir = np.int32(
|
|
884
|
+
np.round((y_rot.real - sq_lev_min) / sq_d_grid)
|
|
885
|
+
)
|
|
886
|
+
if ir < np.int32(0):
|
|
887
|
+
ir = np.int32(0)
|
|
888
|
+
if ir >= sq_side:
|
|
889
|
+
ir = sq_side - np.int32(1)
|
|
890
|
+
ii = np.int32(
|
|
891
|
+
np.round((y_rot.imag - sq_lev_min) / sq_d_grid)
|
|
892
|
+
)
|
|
893
|
+
if ii < np.int32(0):
|
|
894
|
+
ii = np.int32(0)
|
|
895
|
+
if ii >= sq_side:
|
|
896
|
+
ii = sq_side - np.int32(1)
|
|
897
|
+
nr = sq_lev_min + np.float32(ir) * sq_d_grid
|
|
898
|
+
ni = sq_lev_min + np.float32(ii) * sq_d_grid
|
|
899
|
+
d2_min = (y_rot.real - nr) ** 2 + (y_rot.imag - ni) ** 2
|
|
900
|
+
else:
|
|
901
|
+
d2_min = np.float32(1e38)
|
|
902
|
+
for m in range(M):
|
|
903
|
+
dv = y_rot - constellation[m]
|
|
904
|
+
d2 = dv.real * dv.real + dv.imag * dv.imag
|
|
905
|
+
if d2 < d2_min:
|
|
906
|
+
d2_min = d2
|
|
907
|
+
bps_running_sum[i, k] = (
|
|
908
|
+
bps_running_sum[i, k]
|
|
909
|
+
- bps_dist_buf[i, slot, k]
|
|
910
|
+
+ d2_min
|
|
911
|
+
)
|
|
912
|
+
bps_dist_buf[i, slot, k] = d2_min
|
|
913
|
+
bps_dist_ptr = bps_dist_ptr + np.int64(1)
|
|
914
|
+
|
|
915
|
+
if bps_joint_channels:
|
|
916
|
+
best_k_joint = np.int32(0)
|
|
917
|
+
min_tot_joint = np.float64(1e300)
|
|
918
|
+
for k in range(B):
|
|
919
|
+
metric_k = np.float64(0.0)
|
|
920
|
+
for i in range(C):
|
|
921
|
+
metric_k = metric_k + bps_running_sum[i, k]
|
|
922
|
+
if metric_k < min_tot_joint:
|
|
923
|
+
min_tot_joint = metric_k
|
|
924
|
+
best_k_joint = k
|
|
925
|
+
raw4 = np.float64(bps_angles[best_k_joint]) * np.float64(4.0)
|
|
926
|
+
for i in range(C):
|
|
927
|
+
diff4 = raw4 - bps_prev4[i]
|
|
928
|
+
diff4 = diff4 - np.float64(2.0 * np.pi) * np.round(
|
|
929
|
+
diff4 / (np.float64(2.0 * np.pi))
|
|
930
|
+
)
|
|
931
|
+
bps_prev4[i] = bps_prev4[i] + diff4
|
|
932
|
+
phi_hat_bps[i] = bps_prev4[i] / np.float64(4.0)
|
|
933
|
+
else:
|
|
934
|
+
for i in range(C):
|
|
935
|
+
best_k = np.int32(0)
|
|
936
|
+
min_tot = np.float64(1e300)
|
|
937
|
+
for k in range(B):
|
|
938
|
+
if bps_running_sum[i, k] < min_tot:
|
|
939
|
+
min_tot = bps_running_sum[i, k]
|
|
940
|
+
best_k = k
|
|
941
|
+
raw4 = np.float64(bps_angles[best_k]) * np.float64(4.0)
|
|
942
|
+
diff4 = raw4 - bps_prev4[i]
|
|
943
|
+
diff4 = diff4 - np.float64(2.0 * np.pi) * np.round(
|
|
944
|
+
diff4 / (np.float64(2.0 * np.pi))
|
|
945
|
+
)
|
|
946
|
+
bps_prev4[i] = bps_prev4[i] + diff4
|
|
947
|
+
phi_hat_bps[i] = bps_prev4[i] / np.float64(4.0)
|
|
948
|
+
|
|
949
|
+
for i in range(C):
|
|
950
|
+
if cpr_mode == 1:
|
|
951
|
+
phi_hat = pll_phi[i]
|
|
952
|
+
else:
|
|
953
|
+
phi_hat = phi_hat_bps[i]
|
|
954
|
+
|
|
955
|
+
# -- Cycle-slip correction ------------------------------
|
|
956
|
+
if cs_enabled:
|
|
957
|
+
n_b = cs_buf_n[i]
|
|
958
|
+
ptr = cs_buf_ptr[i]
|
|
959
|
+
y_b = np.float64(phi_hat)
|
|
960
|
+
|
|
961
|
+
if n_b == 0:
|
|
962
|
+
phi_corr = phi_hat
|
|
963
|
+
else:
|
|
964
|
+
if n_b < np.int64(10):
|
|
965
|
+
last_pos = (ptr - np.int64(1) + np.int64(H)) % np.int64(
|
|
966
|
+
H
|
|
967
|
+
)
|
|
968
|
+
phi_expected = cs_buf_y[i, last_pos]
|
|
969
|
+
else:
|
|
970
|
+
# cs_stats layout: [0]=Sy, [1]=Sxy (relative coords)
|
|
971
|
+
Sy = cs_stats[i, 0]
|
|
972
|
+
Sxy = cs_stats[i, 1]
|
|
973
|
+
n_f = np.float64(n_b)
|
|
974
|
+
if n_b < np.int64(H):
|
|
975
|
+
Sx_c = (
|
|
976
|
+
n_f * (n_f - np.float64(1.0)) / np.float64(2.0)
|
|
977
|
+
)
|
|
978
|
+
Sxx_c = (
|
|
979
|
+
n_f
|
|
980
|
+
* (n_f - np.float64(1.0))
|
|
981
|
+
* (np.float64(2.0) * n_f - np.float64(1.0))
|
|
982
|
+
/ np.float64(6.0)
|
|
983
|
+
)
|
|
984
|
+
denom = n_f * Sxx_c - Sx_c * Sx_c
|
|
985
|
+
else:
|
|
986
|
+
H_f = np.float64(H)
|
|
987
|
+
Sx_c = (
|
|
988
|
+
H_f * (H_f - np.float64(1.0)) / np.float64(2.0)
|
|
989
|
+
)
|
|
990
|
+
Sxx_c = (
|
|
991
|
+
H_f
|
|
992
|
+
* (H_f - np.float64(1.0))
|
|
993
|
+
* (np.float64(2.0) * H_f - np.float64(1.0))
|
|
994
|
+
/ np.float64(6.0)
|
|
995
|
+
)
|
|
996
|
+
denom = H_f * Sxx_c - Sx_c * Sx_c
|
|
997
|
+
if np.abs(denom) > np.float64(1e-30):
|
|
998
|
+
slope = (n_f * Sxy - Sx_c * Sy) / denom
|
|
999
|
+
intercept = (Sy - slope * Sx_c) / n_f
|
|
1000
|
+
else:
|
|
1001
|
+
slope = np.float64(0.0)
|
|
1002
|
+
intercept = Sy / n_f
|
|
1003
|
+
phi_expected = slope * n_f + intercept
|
|
1004
|
+
|
|
1005
|
+
diff = y_b - phi_expected
|
|
1006
|
+
k_slip = np.int64(np.round(diff / quantum))
|
|
1007
|
+
if np.abs(diff) > np.float64(
|
|
1008
|
+
cs_threshold
|
|
1009
|
+
) and k_slip != np.int64(0):
|
|
1010
|
+
y_b = y_b - np.float64(k_slip) * quantum
|
|
1011
|
+
phi_corr = y_b
|
|
1012
|
+
|
|
1013
|
+
# Update circular buffer - relative coords, only y needed
|
|
1014
|
+
write_pos = ptr % np.int64(H)
|
|
1015
|
+
if n_b == np.int64(H):
|
|
1016
|
+
old_y = cs_buf_y[i, write_pos]
|
|
1017
|
+
H_f = np.float64(H)
|
|
1018
|
+
old_Sy = cs_stats[i, 0]
|
|
1019
|
+
# Sxy_new = Sxy_old - Sy_old + y_old + (H-1)*y_new
|
|
1020
|
+
cs_stats[i, 1] = (
|
|
1021
|
+
cs_stats[i, 1]
|
|
1022
|
+
- old_Sy
|
|
1023
|
+
+ old_y
|
|
1024
|
+
+ (H_f - np.float64(1.0)) * phi_corr
|
|
1025
|
+
)
|
|
1026
|
+
cs_stats[i, 0] = old_Sy - old_y + phi_corr
|
|
1027
|
+
else:
|
|
1028
|
+
n_b_f = np.float64(n_b)
|
|
1029
|
+
cs_stats[i, 1] += n_b_f * phi_corr
|
|
1030
|
+
cs_stats[i, 0] += phi_corr
|
|
1031
|
+
cs_buf_y[i, write_pos] = phi_corr
|
|
1032
|
+
cs_buf_ptr[i] = ptr + np.int64(1)
|
|
1033
|
+
if n_b < np.int64(H):
|
|
1034
|
+
cs_buf_n[i] = n_b + np.int64(1)
|
|
1035
|
+
else:
|
|
1036
|
+
phi_corr = np.float64(phi_hat)
|
|
1037
|
+
|
|
1038
|
+
phi_c[i] = phi_corr
|
|
1039
|
+
phase_out[idx, i] = phi_corr
|
|
1040
|
+
|
|
1041
|
+
cos_p = np.cos(phi_corr)
|
|
1042
|
+
sin_p = np.sin(phi_corr)
|
|
1043
|
+
yr = y_raw[i].real * np.float32(cos_p) + y_raw[i].imag * np.float32(
|
|
1044
|
+
sin_p
|
|
1045
|
+
)
|
|
1046
|
+
yi = -y_raw[i].real * np.float32(sin_p) + y_raw[
|
|
1047
|
+
i
|
|
1048
|
+
].imag * np.float32(cos_p)
|
|
1049
|
+
y_fin[i] = np.complex64(yr + yi * np.complex64(1j))
|
|
1050
|
+
y_out[idx, i] = y_fin[i]
|
|
1051
|
+
|
|
1052
|
+
# -- Slicer and error ---------------------------------------
|
|
1053
|
+
for i in range(C):
|
|
1054
|
+
if idx < n_train:
|
|
1055
|
+
d_i = training[i, idx]
|
|
1056
|
+
elif sq_side > np.int32(0):
|
|
1057
|
+
ir = np.int32(
|
|
1058
|
+
np.round((y_fin[i].real - sq_lev_min) / sq_d_grid)
|
|
1059
|
+
)
|
|
1060
|
+
if ir < np.int32(0):
|
|
1061
|
+
ir = np.int32(0)
|
|
1062
|
+
if ir >= sq_side:
|
|
1063
|
+
ir = sq_side - np.int32(1)
|
|
1064
|
+
ii = np.int32(
|
|
1065
|
+
np.round((y_fin[i].imag - sq_lev_min) / sq_d_grid)
|
|
1066
|
+
)
|
|
1067
|
+
if ii < np.int32(0):
|
|
1068
|
+
ii = np.int32(0)
|
|
1069
|
+
if ii >= sq_side:
|
|
1070
|
+
ii = sq_side - np.int32(1)
|
|
1071
|
+
nr = sq_lev_min + np.float32(ir) * sq_d_grid
|
|
1072
|
+
ni = sq_lev_min + np.float32(ii) * sq_d_grid
|
|
1073
|
+
d_i = np.complex64(nr + ni * np.complex64(1j))
|
|
1074
|
+
else:
|
|
1075
|
+
min_dist = np.float32(1e38)
|
|
1076
|
+
min_idx = 0
|
|
1077
|
+
for kk in range(M):
|
|
1078
|
+
dv = y_fin[i] - constellation[kk]
|
|
1079
|
+
dist = dv.real * dv.real + dv.imag * dv.imag
|
|
1080
|
+
if dist < min_dist:
|
|
1081
|
+
min_dist = dist
|
|
1082
|
+
min_idx = kk
|
|
1083
|
+
d_i = constellation[min_idx]
|
|
1084
|
+
d_sym[i] = d_i
|
|
1085
|
+
e_clean[i] = d_i - y_fin[i]
|
|
1086
|
+
e_out[idx, i] = e_clean[i]
|
|
1087
|
+
|
|
1088
|
+
# -- PLL state update ---------------------------------------
|
|
1089
|
+
if cpr_mode == 1:
|
|
1090
|
+
if bps_joint_channels:
|
|
1091
|
+
e_ph_sum = np.float32(0.0)
|
|
1092
|
+
for i in range(C):
|
|
1093
|
+
e_ph_sum = e_ph_sum + (
|
|
1094
|
+
y_fin[i].imag * d_sym[i].real
|
|
1095
|
+
- y_fin[i].real * d_sym[i].imag
|
|
1096
|
+
)
|
|
1097
|
+
e_ph_avg = e_ph_sum / np.float32(C)
|
|
1098
|
+
for i in range(C):
|
|
1099
|
+
pll_phi[i] = pll_phi[i] + pll_mu * e_ph_avg + pll_freq[i]
|
|
1100
|
+
pll_freq[i] = pll_freq[i] + pll_beta * e_ph_avg
|
|
1101
|
+
else:
|
|
1102
|
+
for i in range(C):
|
|
1103
|
+
e_ph = (
|
|
1104
|
+
y_fin[i].imag * d_sym[i].real
|
|
1105
|
+
- y_fin[i].real * d_sym[i].imag
|
|
1106
|
+
)
|
|
1107
|
+
pll_phi[i] = (
|
|
1108
|
+
pll_phi[i] + pll_mu * np.float32(e_ph) + pll_freq[i]
|
|
1109
|
+
)
|
|
1110
|
+
pll_freq[i] = pll_freq[i] + pll_beta * np.float32(e_ph)
|
|
1111
|
+
|
|
1112
|
+
# -- De-rotate error ----------------------------------------
|
|
1113
|
+
for i in range(C):
|
|
1114
|
+
cos_p = np.cos(phi_c[i])
|
|
1115
|
+
sin_p = np.sin(phi_c[i])
|
|
1116
|
+
er = e_clean[i].real * np.float32(cos_p) - e_clean[
|
|
1117
|
+
i
|
|
1118
|
+
].imag * np.float32(sin_p)
|
|
1119
|
+
ei = e_clean[i].real * np.float32(sin_p) + e_clean[
|
|
1120
|
+
i
|
|
1121
|
+
].imag * np.float32(cos_p)
|
|
1122
|
+
e_eq[i] = np.complex64(er + ei * np.complex64(1j))
|
|
1123
|
+
|
|
1124
|
+
# -- Kalman gain --------------------------------------------
|
|
1125
|
+
for ii in range(N):
|
|
1126
|
+
acc_re = 0.0
|
|
1127
|
+
acc_im = 0.0
|
|
1128
|
+
for jj in range(N):
|
|
1129
|
+
p_val = P[ii, jj]
|
|
1130
|
+
x_val = x_bar[jj]
|
|
1131
|
+
acc_re += p_val.real * x_val.real - p_val.imag * x_val.imag
|
|
1132
|
+
acc_im += p_val.real * x_val.imag + p_val.imag * x_val.real
|
|
1133
|
+
Px[ii] = acc_re + 1j * acc_im
|
|
1134
|
+
denom_k = lam_f64
|
|
1135
|
+
for jj in range(N):
|
|
1136
|
+
denom_k = denom_k + (np.conj(x_bar[jj]) * Px[jj]).real
|
|
1137
|
+
inv_denom = np.float64(1.0) / denom_k
|
|
1138
|
+
for ii in range(N):
|
|
1139
|
+
k_gain[ii] = Px[ii] * inv_denom
|
|
1140
|
+
|
|
1141
|
+
if idx < n_update_halt:
|
|
1142
|
+
for i in range(C):
|
|
1143
|
+
ce_i = np.conj(e_eq[i])
|
|
1144
|
+
for j in range(C):
|
|
1145
|
+
for t in range(num_taps):
|
|
1146
|
+
W[i, j, t] = np.complex64(
|
|
1147
|
+
leak_term * W[i, j, t]
|
|
1148
|
+
+ k_gain[j * num_taps + t] * ce_i
|
|
1149
|
+
)
|
|
1150
|
+
for jj in range(N):
|
|
1151
|
+
xH_P[jj] = np.conj(Px[jj])
|
|
1152
|
+
for ii in range(N):
|
|
1153
|
+
for jj in range(ii, N):
|
|
1154
|
+
val = (P[ii, jj] - k_gain[ii] * xH_P[jj]) / lam_f64
|
|
1155
|
+
if ii == jj:
|
|
1156
|
+
P[ii, ii] = np.complex128(val.real)
|
|
1157
|
+
else:
|
|
1158
|
+
conj_val = (P[jj, ii] - k_gain[jj] * xH_P[ii]) / lam_f64
|
|
1159
|
+
avg = (val + np.conj(conj_val)) * np.float64(0.5)
|
|
1160
|
+
P[ii, jj] = avg
|
|
1161
|
+
P[jj, ii] = np.conj(avg)
|
|
1162
|
+
|
|
1163
|
+
if store_weights:
|
|
1164
|
+
for i in range(C):
|
|
1165
|
+
for j in range(C):
|
|
1166
|
+
for t in range(num_taps):
|
|
1167
|
+
w_hist_out[idx, i, j, t] = W[i, j, t]
|
|
1168
|
+
|
|
1169
|
+
_NUMBA_KERNELS["rls_cpr"] = rls_cpr_loop
|
|
1170
|
+
return _NUMBA_KERNELS["rls_cpr"]
|
|
1171
|
+
|
|
1172
|
+
|
|
1173
|
+
def _get_numba_cma():
|
|
1174
|
+
"""JIT-compile and cache the Numba CMA butterfly loop kernel.
|
|
1175
|
+
|
|
1176
|
+
Returns
|
|
1177
|
+
-------
|
|
1178
|
+
cma_loop : numba-compiled callable
|
|
1179
|
+
See kernel source for argument shapes and semantics.
|
|
1180
|
+
"""
|
|
1181
|
+
if "cma" not in _NUMBA_KERNELS:
|
|
1182
|
+
numba_mod = _get_numba()
|
|
1183
|
+
if numba_mod is None:
|
|
1184
|
+
raise ImportError("Numba is required for backend='numba'.")
|
|
1185
|
+
|
|
1186
|
+
@numba_mod.njit(cache=True, fastmath=True, nogil=True)
|
|
1187
|
+
def cma_loop(
|
|
1188
|
+
x_padded,
|
|
1189
|
+
W,
|
|
1190
|
+
step_size,
|
|
1191
|
+
r2,
|
|
1192
|
+
stride,
|
|
1193
|
+
store_weights,
|
|
1194
|
+
y_out,
|
|
1195
|
+
e_out,
|
|
1196
|
+
w_hist_out,
|
|
1197
|
+
):
|
|
1198
|
+
# x_padded : (C, N_pad) complex64
|
|
1199
|
+
# W : (C, C, num_taps) complex64 - modified in-place
|
|
1200
|
+
# step_size : float32 - fixed CMA gradient step μ
|
|
1201
|
+
# r2 : float32 - Godard radius R² = E[|s|⁴]/E[|s|²]
|
|
1202
|
+
# stride : int - sps
|
|
1203
|
+
# store_weights : bool
|
|
1204
|
+
# y_out : (N_sym, C) complex64
|
|
1205
|
+
# e_out : (N_sym, C) complex64
|
|
1206
|
+
# w_hist_out : (N_sym or 1, C, C, T) complex64
|
|
1207
|
+
C = W.shape[0]
|
|
1208
|
+
num_taps = W.shape[2]
|
|
1209
|
+
n_sym = y_out.shape[0]
|
|
1210
|
+
|
|
1211
|
+
X_wins = np.empty((C, num_taps), dtype=np.complex64)
|
|
1212
|
+
y = np.empty(C, dtype=np.complex64)
|
|
1213
|
+
e = np.empty(C, dtype=np.complex64)
|
|
1214
|
+
|
|
1215
|
+
for idx in range(n_sym):
|
|
1216
|
+
sample_idx = idx * stride
|
|
1217
|
+
|
|
1218
|
+
# Window extraction
|
|
1219
|
+
for c in range(C):
|
|
1220
|
+
for t in range(num_taps):
|
|
1221
|
+
X_wins[c, t] = x_padded[c, sample_idx + t]
|
|
1222
|
+
|
|
1223
|
+
# Butterfly forward pass
|
|
1224
|
+
for i in range(C):
|
|
1225
|
+
acc_re = 0.0
|
|
1226
|
+
acc_im = 0.0
|
|
1227
|
+
for j in range(C):
|
|
1228
|
+
for t in range(num_taps):
|
|
1229
|
+
w = W[i, j, t]
|
|
1230
|
+
x = X_wins[j, t]
|
|
1231
|
+
acc_re += np.float64(w.real) * np.float64(
|
|
1232
|
+
x.real
|
|
1233
|
+
) + np.float64(w.imag) * np.float64(x.imag)
|
|
1234
|
+
acc_im += np.float64(w.real) * np.float64(
|
|
1235
|
+
x.imag
|
|
1236
|
+
) - np.float64(w.imag) * np.float64(x.real)
|
|
1237
|
+
y[i] = acc_re + 1j * acc_im
|
|
1238
|
+
y_out[idx, i] = acc_re + 1j * acc_im
|
|
1239
|
+
|
|
1240
|
+
# CMA error: e[i] = y[i] * (|y[i]|² - R²)
|
|
1241
|
+
# Real-valued |y|² prevents imaginary leakage from FP noise
|
|
1242
|
+
# from causing a parasitic phase rotation via multiplication by y.
|
|
1243
|
+
for i in range(C):
|
|
1244
|
+
mod2 = y[i].real * y[i].real + y[i].imag * y[i].imag
|
|
1245
|
+
e[i] = y[i] * np.float32(mod2 - r2)
|
|
1246
|
+
e_out[idx, i] = e[i]
|
|
1247
|
+
|
|
1248
|
+
# Weight update (gradient descent): W -= mu * conj(e[i]) * X_wins[j,t]
|
|
1249
|
+
for i in range(C):
|
|
1250
|
+
ce_i = np.conj(e[i])
|
|
1251
|
+
for j in range(C):
|
|
1252
|
+
for t in range(num_taps):
|
|
1253
|
+
W[i, j, t] = W[i, j, t] - step_size * ce_i * X_wins[j, t]
|
|
1254
|
+
|
|
1255
|
+
if store_weights:
|
|
1256
|
+
for i in range(C):
|
|
1257
|
+
for j in range(C):
|
|
1258
|
+
for t in range(num_taps):
|
|
1259
|
+
w_hist_out[idx, i, j, t] = W[i, j, t]
|
|
1260
|
+
|
|
1261
|
+
_NUMBA_KERNELS["cma"] = cma_loop
|
|
1262
|
+
return _NUMBA_KERNELS["cma"]
|
|
1263
|
+
|
|
1264
|
+
|
|
1265
|
+
def _get_numba_rde():
|
|
1266
|
+
"""JIT-compile and cache the Numba RDE butterfly loop kernel.
|
|
1267
|
+
|
|
1268
|
+
RDE (Radius Directed Equalizer) is a CMA variant that selects a
|
|
1269
|
+
per-symbol target radius from the set of unique constellation radii
|
|
1270
|
+
rather than using a single fixed Godard radius. This makes it
|
|
1271
|
+
converge correctly on multi-ring constellations (16-QAM, 64-QAM).
|
|
1272
|
+
|
|
1273
|
+
Returns
|
|
1274
|
+
-------
|
|
1275
|
+
rde_loop : numba-compiled callable
|
|
1276
|
+
See kernel source for argument shapes and semantics.
|
|
1277
|
+
"""
|
|
1278
|
+
if "rde" not in _NUMBA_KERNELS:
|
|
1279
|
+
numba_mod = _get_numba()
|
|
1280
|
+
if numba_mod is None:
|
|
1281
|
+
raise ImportError("Numba is required for backend='numba'.")
|
|
1282
|
+
|
|
1283
|
+
@numba_mod.njit(cache=True, fastmath=True, nogil=True)
|
|
1284
|
+
def rde_loop(
|
|
1285
|
+
x_padded,
|
|
1286
|
+
W,
|
|
1287
|
+
step_size,
|
|
1288
|
+
radii,
|
|
1289
|
+
stride,
|
|
1290
|
+
store_weights,
|
|
1291
|
+
y_out,
|
|
1292
|
+
e_out,
|
|
1293
|
+
w_hist_out,
|
|
1294
|
+
):
|
|
1295
|
+
# x_padded : (C, N_pad) complex64
|
|
1296
|
+
# W : (C, C, num_taps) complex64 - modified in-place
|
|
1297
|
+
# step_size : float32 - fixed gradient step μ
|
|
1298
|
+
# radii : (K,) float32 - unique |c| radii, sorted ascending
|
|
1299
|
+
# stride : int - sps
|
|
1300
|
+
# store_weights : bool
|
|
1301
|
+
# y_out : (N_sym, C) complex64
|
|
1302
|
+
# e_out : (N_sym, C) complex64
|
|
1303
|
+
# w_hist_out : (N_sym or 1, C, C, T) complex64
|
|
1304
|
+
C = W.shape[0]
|
|
1305
|
+
num_taps = W.shape[2]
|
|
1306
|
+
n_sym = y_out.shape[0]
|
|
1307
|
+
K = radii.shape[0]
|
|
1308
|
+
|
|
1309
|
+
X_wins = np.empty((C, num_taps), dtype=np.complex64)
|
|
1310
|
+
y = np.empty(C, dtype=np.complex64)
|
|
1311
|
+
e = np.empty(C, dtype=np.complex64)
|
|
1312
|
+
|
|
1313
|
+
for idx in range(n_sym):
|
|
1314
|
+
sample_idx = idx * stride
|
|
1315
|
+
|
|
1316
|
+
# Window extraction
|
|
1317
|
+
for c in range(C):
|
|
1318
|
+
for t in range(num_taps):
|
|
1319
|
+
X_wins[c, t] = x_padded[c, sample_idx + t]
|
|
1320
|
+
|
|
1321
|
+
# Butterfly forward pass
|
|
1322
|
+
for i in range(C):
|
|
1323
|
+
acc_re = 0.0
|
|
1324
|
+
acc_im = 0.0
|
|
1325
|
+
for j in range(C):
|
|
1326
|
+
for t in range(num_taps):
|
|
1327
|
+
w = W[i, j, t]
|
|
1328
|
+
x = X_wins[j, t]
|
|
1329
|
+
acc_re += np.float64(w.real) * np.float64(
|
|
1330
|
+
x.real
|
|
1331
|
+
) + np.float64(w.imag) * np.float64(x.imag)
|
|
1332
|
+
acc_im += np.float64(w.real) * np.float64(
|
|
1333
|
+
x.imag
|
|
1334
|
+
) - np.float64(w.imag) * np.float64(x.real)
|
|
1335
|
+
y[i] = acc_re + 1j * acc_im
|
|
1336
|
+
y_out[idx, i] = acc_re + 1j * acc_im
|
|
1337
|
+
|
|
1338
|
+
# RDE error: e[i] = y[i] * (|y[i]|² - R_d²)
|
|
1339
|
+
# R_d is the magnitude of the nearest constellation ring.
|
|
1340
|
+
# Linear scan over K unique radii is cheap (K ≤ 8 for typical QAM).
|
|
1341
|
+
for i in range(C):
|
|
1342
|
+
mod2 = y[i].real * y[i].real + y[i].imag * y[i].imag
|
|
1343
|
+
abs_y = np.sqrt(mod2)
|
|
1344
|
+
|
|
1345
|
+
best_rd = radii[0]
|
|
1346
|
+
best_dist = abs(abs_y - radii[0])
|
|
1347
|
+
for k in range(1, K):
|
|
1348
|
+
dist_k = abs(abs_y - radii[k])
|
|
1349
|
+
if dist_k < best_dist:
|
|
1350
|
+
best_dist = dist_k
|
|
1351
|
+
best_rd = radii[k]
|
|
1352
|
+
|
|
1353
|
+
e[i] = y[i] * np.float32(mod2 - best_rd * best_rd)
|
|
1354
|
+
e_out[idx, i] = e[i]
|
|
1355
|
+
|
|
1356
|
+
# Weight update: W -= mu * conj(e[i]) * X_wins[j,t]
|
|
1357
|
+
for i in range(C):
|
|
1358
|
+
ce_i = np.conj(e[i])
|
|
1359
|
+
for j in range(C):
|
|
1360
|
+
for t in range(num_taps):
|
|
1361
|
+
W[i, j, t] = W[i, j, t] - step_size * ce_i * X_wins[j, t]
|
|
1362
|
+
|
|
1363
|
+
if store_weights:
|
|
1364
|
+
for i in range(C):
|
|
1365
|
+
for j in range(C):
|
|
1366
|
+
for t in range(num_taps):
|
|
1367
|
+
w_hist_out[idx, i, j, t] = W[i, j, t]
|
|
1368
|
+
|
|
1369
|
+
_NUMBA_KERNELS["rde"] = rde_loop
|
|
1370
|
+
return _NUMBA_KERNELS["rde"]
|
|
1371
|
+
|
|
1372
|
+
|
|
1373
|
+
# -----------------------------------------------------------------------------
|
|
1374
|
+
# HYBRID DA / BLIND KERNELS (internal - used by cma/rde when pilot_ref supplied)
|
|
1375
|
+
# -----------------------------------------------------------------------------
|
|
1376
|
+
#
|
|
1377
|
+
# These kernels process every symbol with a per-step error-function switch:
|
|
1378
|
+
#
|
|
1379
|
+
# pilot_mask[idx] == 1 -> DA-LMS error: e[i] = pilot_ref[i, idx] - y[i]
|
|
1380
|
+
# pilot_mask[idx] == 0 -> blind error: e[i] = CMA or RDE formula
|
|
1381
|
+
#
|
|
1382
|
+
# The weight update rule is identical to the blind case. This achieves
|
|
1383
|
+
# phase-resolved convergence at pilot/preamble positions while preserving
|
|
1384
|
+
# correct blind tracking at data positions - all in a single kernel pass.
|
|
1385
|
+
|
|
1386
|
+
|
|
1387
|
+
def _get_numba_pa_cma():
|
|
1388
|
+
"""JIT-compile and cache the Numba pilot-aided CMA butterfly loop kernel.
|
|
1389
|
+
|
|
1390
|
+
Hybrid CMA: LMS error at pilot positions (pilot_mask==1), standard
|
|
1391
|
+
Godard CMA error at data positions (pilot_mask==0). "Pilot-Aided" (PA)
|
|
1392
|
+
is the standard telecom term for this mixed DA/blind adaptation strategy.
|
|
1393
|
+
|
|
1394
|
+
Returns
|
|
1395
|
+
-------
|
|
1396
|
+
pa_cma_loop : numba-compiled callable
|
|
1397
|
+
"""
|
|
1398
|
+
if "pa_cma" not in _NUMBA_KERNELS:
|
|
1399
|
+
numba_mod = _get_numba()
|
|
1400
|
+
if numba_mod is None:
|
|
1401
|
+
raise ImportError("Numba is required for backend='numba'.")
|
|
1402
|
+
|
|
1403
|
+
@numba_mod.njit(cache=True, fastmath=True, nogil=True)
|
|
1404
|
+
def pa_cma_loop(
|
|
1405
|
+
x_padded,
|
|
1406
|
+
W,
|
|
1407
|
+
step_size,
|
|
1408
|
+
r2,
|
|
1409
|
+
stride,
|
|
1410
|
+
store_weights,
|
|
1411
|
+
y_out,
|
|
1412
|
+
e_out,
|
|
1413
|
+
w_hist_out,
|
|
1414
|
+
pilot_ref,
|
|
1415
|
+
pilot_mask,
|
|
1416
|
+
):
|
|
1417
|
+
# x_padded : (C, N_pad) complex64
|
|
1418
|
+
# W : (C, C, num_taps) complex64 - modified in-place
|
|
1419
|
+
# step_size : float32
|
|
1420
|
+
# r2 : float32 - Godard radius R²
|
|
1421
|
+
# stride : int
|
|
1422
|
+
# store_weights : bool
|
|
1423
|
+
# y_out : (N_sym, C) complex64
|
|
1424
|
+
# e_out : (N_sym, C) complex64
|
|
1425
|
+
# w_hist_out : (N_sym or 1, C, C, T) complex64
|
|
1426
|
+
# pilot_ref : (C, N_sym) complex64 - known symbol; 0 at data positions
|
|
1427
|
+
# pilot_mask : (N_sym,) uint8 - 1 at pilot/preamble, 0 at data
|
|
1428
|
+
C = W.shape[0]
|
|
1429
|
+
num_taps = W.shape[2]
|
|
1430
|
+
n_sym = y_out.shape[0]
|
|
1431
|
+
|
|
1432
|
+
X_wins = np.empty((C, num_taps), dtype=np.complex64)
|
|
1433
|
+
y = np.empty(C, dtype=np.complex64)
|
|
1434
|
+
e = np.empty(C, dtype=np.complex64)
|
|
1435
|
+
|
|
1436
|
+
for idx in range(n_sym):
|
|
1437
|
+
sample_idx = idx * stride
|
|
1438
|
+
|
|
1439
|
+
# Window extraction
|
|
1440
|
+
for c in range(C):
|
|
1441
|
+
for t in range(num_taps):
|
|
1442
|
+
X_wins[c, t] = x_padded[c, sample_idx + t]
|
|
1443
|
+
|
|
1444
|
+
# Butterfly forward pass
|
|
1445
|
+
for i in range(C):
|
|
1446
|
+
acc_re = 0.0
|
|
1447
|
+
acc_im = 0.0
|
|
1448
|
+
for j in range(C):
|
|
1449
|
+
for t in range(num_taps):
|
|
1450
|
+
w = W[i, j, t]
|
|
1451
|
+
x = X_wins[j, t]
|
|
1452
|
+
acc_re += np.float64(w.real) * np.float64(
|
|
1453
|
+
x.real
|
|
1454
|
+
) + np.float64(w.imag) * np.float64(x.imag)
|
|
1455
|
+
acc_im += np.float64(w.real) * np.float64(
|
|
1456
|
+
x.imag
|
|
1457
|
+
) - np.float64(w.imag) * np.float64(x.real)
|
|
1458
|
+
y[i] = acc_re + 1j * acc_im
|
|
1459
|
+
y_out[idx, i] = acc_re + 1j * acc_im
|
|
1460
|
+
|
|
1461
|
+
# Error: DA-LMS at pilots, CMA Godard at data
|
|
1462
|
+
for i in range(C):
|
|
1463
|
+
if pilot_mask[idx]:
|
|
1464
|
+
e[i] = (
|
|
1465
|
+
y[i] - pilot_ref[i, idx]
|
|
1466
|
+
) # inverted to match blind subtractive update
|
|
1467
|
+
else:
|
|
1468
|
+
mod2 = y[i].real * y[i].real + y[i].imag * y[i].imag
|
|
1469
|
+
e[i] = y[i] * np.float32(mod2 - r2)
|
|
1470
|
+
e_out[idx, i] = e[i]
|
|
1471
|
+
|
|
1472
|
+
# Weight update
|
|
1473
|
+
for i in range(C):
|
|
1474
|
+
ce_i = np.conj(e[i])
|
|
1475
|
+
for j in range(C):
|
|
1476
|
+
for t in range(num_taps):
|
|
1477
|
+
W[i, j, t] = W[i, j, t] - step_size * ce_i * X_wins[j, t]
|
|
1478
|
+
|
|
1479
|
+
if store_weights:
|
|
1480
|
+
for i in range(C):
|
|
1481
|
+
for j in range(C):
|
|
1482
|
+
for t in range(num_taps):
|
|
1483
|
+
w_hist_out[idx, i, j, t] = W[i, j, t]
|
|
1484
|
+
|
|
1485
|
+
_NUMBA_KERNELS["pa_cma"] = pa_cma_loop
|
|
1486
|
+
return _NUMBA_KERNELS["pa_cma"]
|
|
1487
|
+
|
|
1488
|
+
|
|
1489
|
+
def _get_numba_pa_rde():
|
|
1490
|
+
"""JIT-compile and cache the Numba pilot-aided RDE butterfly loop kernel.
|
|
1491
|
+
|
|
1492
|
+
Hybrid RDE: LMS error at pilot positions (pilot_mask==1), standard
|
|
1493
|
+
ring-directed RDE error at data positions (pilot_mask==0). "Pilot-Aided"
|
|
1494
|
+
(PA) is the standard telecom term for this mixed DA/blind adaptation strategy.
|
|
1495
|
+
|
|
1496
|
+
Returns
|
|
1497
|
+
-------
|
|
1498
|
+
pa_rde_loop : numba-compiled callable
|
|
1499
|
+
"""
|
|
1500
|
+
if "pa_rde" not in _NUMBA_KERNELS:
|
|
1501
|
+
numba_mod = _get_numba()
|
|
1502
|
+
if numba_mod is None:
|
|
1503
|
+
raise ImportError("Numba is required for backend='numba'.")
|
|
1504
|
+
|
|
1505
|
+
@numba_mod.njit(cache=True, fastmath=True, nogil=True)
|
|
1506
|
+
def pa_rde_loop(
|
|
1507
|
+
x_padded,
|
|
1508
|
+
W,
|
|
1509
|
+
step_size,
|
|
1510
|
+
radii,
|
|
1511
|
+
stride,
|
|
1512
|
+
store_weights,
|
|
1513
|
+
y_out,
|
|
1514
|
+
e_out,
|
|
1515
|
+
w_hist_out,
|
|
1516
|
+
pilot_ref,
|
|
1517
|
+
pilot_mask,
|
|
1518
|
+
):
|
|
1519
|
+
# x_padded : (C, N_pad) complex64
|
|
1520
|
+
# W : (C, C, num_taps) complex64 - modified in-place
|
|
1521
|
+
# step_size : float32
|
|
1522
|
+
# radii : (K,) float32 - unique |c| radii, sorted
|
|
1523
|
+
# stride : int
|
|
1524
|
+
# store_weights : bool
|
|
1525
|
+
# y_out : (N_sym, C) complex64
|
|
1526
|
+
# e_out : (N_sym, C) complex64
|
|
1527
|
+
# w_hist_out : (N_sym or 1, C, C, T) complex64
|
|
1528
|
+
# pilot_ref : (C, N_sym) complex64 - 0 at data positions
|
|
1529
|
+
# pilot_mask : (N_sym,) uint8 - 1 at pilot/preamble
|
|
1530
|
+
C = W.shape[0]
|
|
1531
|
+
num_taps = W.shape[2]
|
|
1532
|
+
n_sym = y_out.shape[0]
|
|
1533
|
+
K = radii.shape[0]
|
|
1534
|
+
|
|
1535
|
+
X_wins = np.empty((C, num_taps), dtype=np.complex64)
|
|
1536
|
+
y = np.empty(C, dtype=np.complex64)
|
|
1537
|
+
e = np.empty(C, dtype=np.complex64)
|
|
1538
|
+
|
|
1539
|
+
for idx in range(n_sym):
|
|
1540
|
+
sample_idx = idx * stride
|
|
1541
|
+
|
|
1542
|
+
# Window extraction
|
|
1543
|
+
for c in range(C):
|
|
1544
|
+
for t in range(num_taps):
|
|
1545
|
+
X_wins[c, t] = x_padded[c, sample_idx + t]
|
|
1546
|
+
|
|
1547
|
+
# Butterfly forward pass
|
|
1548
|
+
for i in range(C):
|
|
1549
|
+
acc_re = 0.0
|
|
1550
|
+
acc_im = 0.0
|
|
1551
|
+
for j in range(C):
|
|
1552
|
+
for t in range(num_taps):
|
|
1553
|
+
w = W[i, j, t]
|
|
1554
|
+
x = X_wins[j, t]
|
|
1555
|
+
acc_re += np.float64(w.real) * np.float64(
|
|
1556
|
+
x.real
|
|
1557
|
+
) + np.float64(w.imag) * np.float64(x.imag)
|
|
1558
|
+
acc_im += np.float64(w.real) * np.float64(
|
|
1559
|
+
x.imag
|
|
1560
|
+
) - np.float64(w.imag) * np.float64(x.real)
|
|
1561
|
+
y[i] = acc_re + 1j * acc_im
|
|
1562
|
+
y_out[idx, i] = acc_re + 1j * acc_im
|
|
1563
|
+
|
|
1564
|
+
# Error: DA-LMS at pilots, RDE ring-directed at data
|
|
1565
|
+
for i in range(C):
|
|
1566
|
+
if pilot_mask[idx]:
|
|
1567
|
+
e[i] = (
|
|
1568
|
+
y[i] - pilot_ref[i, idx]
|
|
1569
|
+
) # inverted to match blind subtractive update
|
|
1570
|
+
else:
|
|
1571
|
+
mod2 = y[i].real * y[i].real + y[i].imag * y[i].imag
|
|
1572
|
+
abs_y = np.sqrt(mod2)
|
|
1573
|
+
best_rd = radii[0]
|
|
1574
|
+
best_dist = abs(abs_y - radii[0])
|
|
1575
|
+
for k in range(1, K):
|
|
1576
|
+
dist_k = abs(abs_y - radii[k])
|
|
1577
|
+
if dist_k < best_dist:
|
|
1578
|
+
best_dist = dist_k
|
|
1579
|
+
best_rd = radii[k]
|
|
1580
|
+
e[i] = y[i] * np.float32(mod2 - best_rd * best_rd)
|
|
1581
|
+
e_out[idx, i] = e[i]
|
|
1582
|
+
|
|
1583
|
+
# Weight update
|
|
1584
|
+
for i in range(C):
|
|
1585
|
+
ce_i = np.conj(e[i])
|
|
1586
|
+
for j in range(C):
|
|
1587
|
+
for t in range(num_taps):
|
|
1588
|
+
W[i, j, t] = W[i, j, t] - step_size * ce_i * X_wins[j, t]
|
|
1589
|
+
|
|
1590
|
+
if store_weights:
|
|
1591
|
+
for i in range(C):
|
|
1592
|
+
for j in range(C):
|
|
1593
|
+
for t in range(num_taps):
|
|
1594
|
+
w_hist_out[idx, i, j, t] = W[i, j, t]
|
|
1595
|
+
|
|
1596
|
+
_NUMBA_KERNELS["pa_rde"] = pa_rde_loop
|
|
1597
|
+
return _NUMBA_KERNELS["pa_rde"]
|
|
1598
|
+
|
|
1599
|
+
|
|
1600
|
+
def _get_numba_cs_block():
|
|
1601
|
+
"""Lazy-compile a Numba JIT kernel for the block_lms cycle-slip correction loop.
|
|
1602
|
+
|
|
1603
|
+
Replaces the Python ``for ci in range(C): for i in range(B)`` loop with
|
|
1604
|
+
compiled native code. D->H/H->D transfers (few KB) still happen, but the
|
|
1605
|
+
loop body itself runs at native speed, eliminating the dominant overhead
|
|
1606
|
+
when ``cpr_cycle_slip_correction=True`` on GPU.
|
|
1607
|
+
"""
|
|
1608
|
+
if "cs_block" not in _NUMBA_KERNELS:
|
|
1609
|
+
numba_mod = _get_numba()
|
|
1610
|
+
if numba_mod is None:
|
|
1611
|
+
return None
|
|
1612
|
+
|
|
1613
|
+
@numba_mod.njit(cache=True, fastmath=True, nogil=True)
|
|
1614
|
+
def cs_block(
|
|
1615
|
+
phi_blk,
|
|
1616
|
+
phi_corr,
|
|
1617
|
+
cs_buf_x,
|
|
1618
|
+
cs_buf_y,
|
|
1619
|
+
cs_buf_ptr,
|
|
1620
|
+
cs_buf_n,
|
|
1621
|
+
cs_stats,
|
|
1622
|
+
b_start,
|
|
1623
|
+
quantum,
|
|
1624
|
+
threshold,
|
|
1625
|
+
cs_H,
|
|
1626
|
+
):
|
|
1627
|
+
"""Per-symbol cycle-slip correction for one equalizer block.
|
|
1628
|
+
|
|
1629
|
+
Parameters
|
|
1630
|
+
----------
|
|
1631
|
+
phi_blk : (C, B) float64 - BPS phase before correction (input)
|
|
1632
|
+
phi_corr : (C, B) float64 - corrected phase (output, pre-allocated)
|
|
1633
|
+
cs_buf_x : (C, H) float64 - unused (retained for call-site compat)
|
|
1634
|
+
cs_buf_y : (C, H) float64 - circular buffer of past corrected phases
|
|
1635
|
+
cs_buf_ptr: (C,) int64 - write pointer (monotonically increasing)
|
|
1636
|
+
cs_buf_n : (C,) int64 - number of valid entries (≤ H)
|
|
1637
|
+
cs_stats : (C, 4) float64 - [0]=Sy, [1]=Sxy (relative coords); [2..3] unused
|
|
1638
|
+
b_start : int - global symbol index of block start (unused)
|
|
1639
|
+
quantum : float64 - slip quantum (2π / symmetry)
|
|
1640
|
+
threshold : float64 - |diff| threshold for declaring a slip
|
|
1641
|
+
cs_H : int - circular buffer length
|
|
1642
|
+
"""
|
|
1643
|
+
C = phi_blk.shape[0]
|
|
1644
|
+
B = phi_blk.shape[1]
|
|
1645
|
+
H_f = float(cs_H)
|
|
1646
|
+
Sx_full = H_f * (H_f - 1.0) / 2.0
|
|
1647
|
+
Sxx_full = H_f * (H_f - 1.0) * (2.0 * H_f - 1.0) / 6.0
|
|
1648
|
+
denom_full = H_f * Sxx_full - Sx_full * Sx_full
|
|
1649
|
+
for ci in range(C):
|
|
1650
|
+
for i in range(B):
|
|
1651
|
+
y_b = phi_blk[ci, i]
|
|
1652
|
+
n_b = cs_buf_n[ci]
|
|
1653
|
+
ptr = cs_buf_ptr[ci]
|
|
1654
|
+
|
|
1655
|
+
if n_b == 0:
|
|
1656
|
+
phi_expected = y_b
|
|
1657
|
+
elif n_b < 10:
|
|
1658
|
+
last_pos = (ptr - 1 + cs_H) % cs_H
|
|
1659
|
+
phi_expected = cs_buf_y[ci, last_pos]
|
|
1660
|
+
else:
|
|
1661
|
+
sy = cs_stats[ci, 0]
|
|
1662
|
+
sxy = cs_stats[ci, 1]
|
|
1663
|
+
n_f = float(n_b)
|
|
1664
|
+
if n_b < cs_H:
|
|
1665
|
+
Sx_c = n_f * (n_f - 1.0) / 2.0
|
|
1666
|
+
Sxx_c = n_f * (n_f - 1.0) * (2.0 * n_f - 1.0) / 6.0
|
|
1667
|
+
denom = n_f * Sxx_c - Sx_c * Sx_c
|
|
1668
|
+
else:
|
|
1669
|
+
Sx_c = Sx_full
|
|
1670
|
+
denom = denom_full
|
|
1671
|
+
if abs(denom) > 1e-30:
|
|
1672
|
+
slope = (n_f * sxy - Sx_c * sy) / denom
|
|
1673
|
+
intercept = (sy - slope * Sx_c) / n_f
|
|
1674
|
+
else:
|
|
1675
|
+
slope = 0.0
|
|
1676
|
+
intercept = sy / n_f
|
|
1677
|
+
phi_expected = slope * n_f + intercept
|
|
1678
|
+
|
|
1679
|
+
diff = y_b - phi_expected
|
|
1680
|
+
k_slip = int(round(diff / quantum))
|
|
1681
|
+
if abs(diff) > threshold and k_slip != 0:
|
|
1682
|
+
y_b -= float(k_slip) * quantum
|
|
1683
|
+
phi_corr[ci, i] = y_b
|
|
1684
|
+
|
|
1685
|
+
# Update circular buffer - relative coords, only y needed
|
|
1686
|
+
write_pos = ptr % cs_H
|
|
1687
|
+
if n_b == cs_H:
|
|
1688
|
+
old_y = cs_buf_y[ci, write_pos]
|
|
1689
|
+
old_sy = cs_stats[ci, 0]
|
|
1690
|
+
# Sxy_new = Sxy_old - Sy_old + y_old + (H-1)*y_new
|
|
1691
|
+
cs_stats[ci, 1] = (
|
|
1692
|
+
cs_stats[ci, 1] - old_sy + old_y + (H_f - 1.0) * y_b
|
|
1693
|
+
)
|
|
1694
|
+
cs_stats[ci, 0] = old_sy - old_y + y_b
|
|
1695
|
+
else:
|
|
1696
|
+
cs_stats[ci, 1] += float(n_b) * y_b
|
|
1697
|
+
cs_stats[ci, 0] += y_b
|
|
1698
|
+
cs_buf_y[ci, write_pos] = y_b
|
|
1699
|
+
cs_buf_ptr[ci] = ptr + 1
|
|
1700
|
+
if n_b < cs_H:
|
|
1701
|
+
cs_buf_n[ci] = n_b + 1
|
|
1702
|
+
|
|
1703
|
+
_NUMBA_KERNELS["cs_block"] = cs_block
|
|
1704
|
+
return _NUMBA_KERNELS["cs_block"]
|