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/mapping/gray.py
ADDED
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Constellation geometry and Gray labelling.
|
|
3
|
+
|
|
4
|
+
Gray code sequences, their inverse, and the Gray-mapped constellation
|
|
5
|
+
generators for PSK / ASK / square-, 8-rect-, and cross-QAM. These are the
|
|
6
|
+
geometric foundation consumed by the bit mapping, soft demapping, and
|
|
7
|
+
probabilistic-shaping layers.
|
|
8
|
+
|
|
9
|
+
Note: codes and constellations are generated using NumPy (host-side).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from functools import lru_cache
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
|
|
16
|
+
from ..logger import logger
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"gray_code",
|
|
20
|
+
"gray_constellation",
|
|
21
|
+
"gray_to_binary",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@lru_cache(maxsize=128)
|
|
26
|
+
def gray_code(n: int) -> np.ndarray:
|
|
27
|
+
"""
|
|
28
|
+
Generates a Gray code sequence for `n` bits.
|
|
29
|
+
|
|
30
|
+
Gray coding ensures that adjacent symbols in the constellation differ
|
|
31
|
+
by only one bit, minimizing bit error rate (BER) for a given symbol
|
|
32
|
+
error rate (SER).
|
|
33
|
+
|
|
34
|
+
Parameters
|
|
35
|
+
----------
|
|
36
|
+
n : int
|
|
37
|
+
Number of bits.
|
|
38
|
+
|
|
39
|
+
Returns
|
|
40
|
+
-------
|
|
41
|
+
np.ndarray
|
|
42
|
+
Array of integers representing the Gray code sequence.
|
|
43
|
+
Shape: (2^n,).
|
|
44
|
+
|
|
45
|
+
Examples
|
|
46
|
+
--------
|
|
47
|
+
>>> gray_code(2)
|
|
48
|
+
array([0, 1, 3, 2])
|
|
49
|
+
"""
|
|
50
|
+
if n < 0:
|
|
51
|
+
raise ValueError("n must be non-negative")
|
|
52
|
+
if n == 0:
|
|
53
|
+
return np.array([0], dtype=int)
|
|
54
|
+
|
|
55
|
+
# Vectorized implementation: s ^ (s >> 1)
|
|
56
|
+
i = np.arange(1 << n, dtype=int)
|
|
57
|
+
|
|
58
|
+
return i ^ (i >> 1)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@lru_cache(maxsize=128)
|
|
62
|
+
def gray_to_binary(n: int) -> np.ndarray:
|
|
63
|
+
"""
|
|
64
|
+
Computes the inverse Gray code mapping: symbol index to bit pattern.
|
|
65
|
+
|
|
66
|
+
In soft demapping, it is necessary to identify which bits correspond to
|
|
67
|
+
each constellation point. Since `gray_code(n)[i]` provides the Gray
|
|
68
|
+
code for natural binary `i`, this inverse mapping finds the natural
|
|
69
|
+
binary representation corresponding to a specific Gray-coded index.
|
|
70
|
+
|
|
71
|
+
Parameters
|
|
72
|
+
----------
|
|
73
|
+
n : int
|
|
74
|
+
Number of bits per symbol.
|
|
75
|
+
|
|
76
|
+
Returns
|
|
77
|
+
-------
|
|
78
|
+
np.ndarray
|
|
79
|
+
Array where `result[s]` provides the natural binary integer
|
|
80
|
+
representing the bit pattern for symbol `s`. Shape: (2^n,).
|
|
81
|
+
"""
|
|
82
|
+
if n < 0:
|
|
83
|
+
raise ValueError("n must be non-negative")
|
|
84
|
+
if n == 0:
|
|
85
|
+
return np.array([0], dtype=int)
|
|
86
|
+
|
|
87
|
+
# gray_code gives: natural_binary -> gray_symbol
|
|
88
|
+
# We want: gray_symbol -> natural_binary (inverse)
|
|
89
|
+
gray = gray_code(n)
|
|
90
|
+
inverse = np.zeros(1 << n, dtype=int)
|
|
91
|
+
inverse[gray] = np.arange(1 << n)
|
|
92
|
+
|
|
93
|
+
return inverse
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@lru_cache(maxsize=128)
|
|
97
|
+
def gray_constellation(
|
|
98
|
+
modulation: str,
|
|
99
|
+
order: int,
|
|
100
|
+
normalize: bool = True,
|
|
101
|
+
unipolar: bool = False,
|
|
102
|
+
) -> np.ndarray:
|
|
103
|
+
"""
|
|
104
|
+
Generate constellation points with Gray mapping.
|
|
105
|
+
|
|
106
|
+
The returned array is indexed by the symbol's natural binary value.
|
|
107
|
+
`constellation[i]` is the complex/float value for the symbol representing
|
|
108
|
+
the bit pattern for integer `i`.
|
|
109
|
+
|
|
110
|
+
Parameters
|
|
111
|
+
----------
|
|
112
|
+
modulation : {"psk", "qam", "ask", "pam"}
|
|
113
|
+
Modulation type. If string contains 'unipolar', unipolar=True is triggered.
|
|
114
|
+
order : int
|
|
115
|
+
Modulation order (number of symbols). Must be a power of 2.
|
|
116
|
+
normalize : bool, default True
|
|
117
|
+
If True, scales the constellation to unit average power (E_s = 1).
|
|
118
|
+
unipolar : bool, default False
|
|
119
|
+
If True, shifts the constellation to be strictly non-negative (for 'ask'/'pam').
|
|
120
|
+
If the modulation string contains 'unipol', this is automatically forced to True.
|
|
121
|
+
|
|
122
|
+
Returns
|
|
123
|
+
-------
|
|
124
|
+
np.ndarray
|
|
125
|
+
Array of constellation points (NumPy). Shape: (order,).
|
|
126
|
+
Complex for 'psk' and 'qam', Float for 'ask'.
|
|
127
|
+
|
|
128
|
+
Notes
|
|
129
|
+
-----
|
|
130
|
+
By default, constellations are normalized to **unit average power (E_s = 1)**.
|
|
131
|
+
This ensures that when different modulation schemes are mixed (e.g., PSK
|
|
132
|
+
pilots with QAM payload), they start from a consistent power baseline.
|
|
133
|
+
"""
|
|
134
|
+
logger.debug(
|
|
135
|
+
"Generating Gray-coded constellation: modulation=%s, order=%s, normalize=%s",
|
|
136
|
+
modulation,
|
|
137
|
+
order,
|
|
138
|
+
normalize,
|
|
139
|
+
)
|
|
140
|
+
modulation = modulation.lower()
|
|
141
|
+
|
|
142
|
+
# Extract core modulation scheme
|
|
143
|
+
if "psk" in modulation:
|
|
144
|
+
modulation = "psk"
|
|
145
|
+
elif "qam" in modulation:
|
|
146
|
+
modulation = "qam"
|
|
147
|
+
elif "ask" in modulation or "pam" in modulation:
|
|
148
|
+
modulation = "ask"
|
|
149
|
+
|
|
150
|
+
if order < 2:
|
|
151
|
+
raise ValueError("Order must be at least 2 for modulation")
|
|
152
|
+
|
|
153
|
+
if modulation == "psk":
|
|
154
|
+
result = _gray_psk(order)
|
|
155
|
+
elif modulation == "ask":
|
|
156
|
+
result = _gray_ask(order, unipolar=unipolar)
|
|
157
|
+
elif modulation == "qam":
|
|
158
|
+
k = int(np.log2(order))
|
|
159
|
+
if 2**k != order:
|
|
160
|
+
raise ValueError(f"Order must be power of 2 for {modulation}")
|
|
161
|
+
|
|
162
|
+
# Check for 8-QAM
|
|
163
|
+
if order == 8:
|
|
164
|
+
result = _gray_qam_8_rect()
|
|
165
|
+
elif k % 2 == 0:
|
|
166
|
+
result = _gray_qam_square(order)
|
|
167
|
+
else:
|
|
168
|
+
result = _gray_qam_cross(order)
|
|
169
|
+
else:
|
|
170
|
+
raise ValueError(f"Unsupported modulation type: {modulation}")
|
|
171
|
+
|
|
172
|
+
if normalize:
|
|
173
|
+
if modulation == "psk":
|
|
174
|
+
# PSK is already on unit circle (E_s = 1)
|
|
175
|
+
pass
|
|
176
|
+
else:
|
|
177
|
+
from .. import helpers
|
|
178
|
+
|
|
179
|
+
result = helpers.normalize(result, mode="average_power")
|
|
180
|
+
|
|
181
|
+
return result
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _gray_psk(order: int) -> np.ndarray:
|
|
185
|
+
"""
|
|
186
|
+
Generates an M-PSK constellation with Gray mapping.
|
|
187
|
+
|
|
188
|
+
Parameters
|
|
189
|
+
----------
|
|
190
|
+
order : int
|
|
191
|
+
Modulation order (must be a power of 2).
|
|
192
|
+
|
|
193
|
+
Returns
|
|
194
|
+
-------
|
|
195
|
+
np.ndarray
|
|
196
|
+
Complex array of PSK constellation points.
|
|
197
|
+
"""
|
|
198
|
+
# Bits per symbol
|
|
199
|
+
k = int(np.log2(order))
|
|
200
|
+
if 2**k != order:
|
|
201
|
+
raise ValueError("Order must be power of 2 for psk")
|
|
202
|
+
|
|
203
|
+
# Gray codes for k bits
|
|
204
|
+
gray = gray_code(k)
|
|
205
|
+
|
|
206
|
+
# BPSK Special Case: strictly real values
|
|
207
|
+
if order == 2:
|
|
208
|
+
points = np.array([-1.0, 1.0], dtype=float)
|
|
209
|
+
constellation = np.zeros(order, dtype=float)
|
|
210
|
+
constellation[gray] = points
|
|
211
|
+
return constellation
|
|
212
|
+
|
|
213
|
+
# Geometric points (phases)
|
|
214
|
+
# Phases are 0, 2pi/M, ..., 2pi(M-1)/M
|
|
215
|
+
phases = np.arange(order) * 2 * np.pi / order
|
|
216
|
+
points = np.exp(1j * phases)
|
|
217
|
+
|
|
218
|
+
constellation = np.zeros(order, dtype=complex)
|
|
219
|
+
constellation[gray] = points
|
|
220
|
+
|
|
221
|
+
return constellation
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _gray_ask(order: int, unipolar: bool = False) -> np.ndarray:
|
|
225
|
+
"""
|
|
226
|
+
Generates an M-ASK constellation with Gray mapping.
|
|
227
|
+
|
|
228
|
+
By default, returns bipolar values centered at zero. If unipolar=True,
|
|
229
|
+
shifts values to start from zero.
|
|
230
|
+
|
|
231
|
+
Parameters
|
|
232
|
+
----------
|
|
233
|
+
order : int
|
|
234
|
+
Modulation order (must be a power of 2).
|
|
235
|
+
|
|
236
|
+
Returns
|
|
237
|
+
-------
|
|
238
|
+
np.ndarray
|
|
239
|
+
Real-valued array of ASK constellation points.
|
|
240
|
+
"""
|
|
241
|
+
k = int(np.log2(order))
|
|
242
|
+
if 2**k != order:
|
|
243
|
+
raise ValueError("Order must be power of 2 for ask")
|
|
244
|
+
|
|
245
|
+
gray = gray_code(k)
|
|
246
|
+
# Points are symmetric: -M+1, -M+3, ..., M-3, M-1
|
|
247
|
+
points = np.linspace(-order + 1, order - 1, order)
|
|
248
|
+
|
|
249
|
+
if unipolar:
|
|
250
|
+
points = points - np.min(points)
|
|
251
|
+
|
|
252
|
+
constellation = np.zeros(order, dtype=float)
|
|
253
|
+
constellation[gray] = points
|
|
254
|
+
|
|
255
|
+
return constellation
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _gray_qam_square(order: int) -> np.ndarray:
|
|
259
|
+
"""
|
|
260
|
+
Generates a Square M-QAM constellation with Gray mapping.
|
|
261
|
+
|
|
262
|
+
Constructed as the Cartesian product of two Gray-coded ASK
|
|
263
|
+
constellations. Valid for orders where log2(M) is even.
|
|
264
|
+
|
|
265
|
+
Parameters
|
|
266
|
+
----------
|
|
267
|
+
order : int
|
|
268
|
+
Modulation order (must be an even power of 2, e.g., 16, 64).
|
|
269
|
+
|
|
270
|
+
Returns
|
|
271
|
+
-------
|
|
272
|
+
np.ndarray
|
|
273
|
+
Complex array of square QAM constellation points.
|
|
274
|
+
"""
|
|
275
|
+
# M = 2^(2k). I bits = k, Q bits = k.
|
|
276
|
+
k_total = int(np.log2(order))
|
|
277
|
+
n = k_total // 2 # bits per axis
|
|
278
|
+
|
|
279
|
+
# Generate ASK for each axis
|
|
280
|
+
m_axis = 2**n
|
|
281
|
+
pam = _gray_ask(m_axis)
|
|
282
|
+
|
|
283
|
+
# Cartesian product
|
|
284
|
+
# Symbol s (2n bits). High n bits -> I, Low n bits -> Q.
|
|
285
|
+
s = np.arange(order)
|
|
286
|
+
mask_q = (1 << n) - 1
|
|
287
|
+
|
|
288
|
+
idx_i = s >> n
|
|
289
|
+
idx_q = s & mask_q
|
|
290
|
+
|
|
291
|
+
i_vals = pam[idx_i]
|
|
292
|
+
q_vals = pam[idx_q]
|
|
293
|
+
|
|
294
|
+
constellation = i_vals + 1j * q_vals
|
|
295
|
+
|
|
296
|
+
return constellation
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _gray_qam_8_rect() -> np.ndarray:
|
|
300
|
+
"""
|
|
301
|
+
Generates an 8-QAM constellation with Rectangular Gray mapping.
|
|
302
|
+
|
|
303
|
+
Returns
|
|
304
|
+
-------
|
|
305
|
+
np.ndarray
|
|
306
|
+
Complex array of 8-QAM constellation points.
|
|
307
|
+
"""
|
|
308
|
+
|
|
309
|
+
points = np.zeros(8, dtype=complex)
|
|
310
|
+
|
|
311
|
+
# 000 (I=-3, Q=-1)
|
|
312
|
+
points[0] = -3.0 - 1.0j
|
|
313
|
+
|
|
314
|
+
# 001 (I=-3, Q=+1)
|
|
315
|
+
points[1] = -3.0 + 1.0j
|
|
316
|
+
|
|
317
|
+
# 010 (I=-1, Q=-1)
|
|
318
|
+
points[2] = -1.0 - 1.0j
|
|
319
|
+
|
|
320
|
+
# 011 (I=-1, Q=+1)
|
|
321
|
+
points[3] = -1.0 + 1.0j
|
|
322
|
+
|
|
323
|
+
# 100 (I=+3, Q=-1)
|
|
324
|
+
points[4] = 3.0 - 1.0j
|
|
325
|
+
|
|
326
|
+
# 101 (I=+3, Q=+1)
|
|
327
|
+
points[5] = 3.0 + 1.0j
|
|
328
|
+
|
|
329
|
+
# 110 (I=+1, Q=-1)
|
|
330
|
+
points[6] = 1.0 - 1.0j
|
|
331
|
+
|
|
332
|
+
# 111 (I=+1, Q=+1)
|
|
333
|
+
points[7] = 1.0 + 1.0j
|
|
334
|
+
|
|
335
|
+
return points
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _gray_qam_cross(order: int) -> np.ndarray:
|
|
339
|
+
"""
|
|
340
|
+
Generates a Cross-QAM constellation using a Quasi-Gray mapping.
|
|
341
|
+
|
|
342
|
+
Cross constellations are used for QAM orders where log2(M) is odd (e.g., 32, 128)
|
|
343
|
+
to maintain a more circular/compact shape than a purely rectangular grid.
|
|
344
|
+
|
|
345
|
+
Parameters
|
|
346
|
+
----------
|
|
347
|
+
order : int
|
|
348
|
+
Modulation order (must be an odd power of 2).
|
|
349
|
+
|
|
350
|
+
Returns
|
|
351
|
+
-------
|
|
352
|
+
np.ndarray
|
|
353
|
+
Complex array of cross QAM constellation points.
|
|
354
|
+
"""
|
|
355
|
+
# M = 2^(2k+1).
|
|
356
|
+
k_total = int(np.log2(order))
|
|
357
|
+
|
|
358
|
+
# I bits n = ceil(k_total/2) = (k+1)//2
|
|
359
|
+
# Q bits m = floor(k_total/2) = k // 2
|
|
360
|
+
n = (k_total + 1) // 2
|
|
361
|
+
m = k_total // 2
|
|
362
|
+
|
|
363
|
+
width = 2**n
|
|
364
|
+
height = 2**m
|
|
365
|
+
|
|
366
|
+
# 1. Generate Gray sequences for underlying Rectangular Grid
|
|
367
|
+
gray_i_seq = gray_code(n)
|
|
368
|
+
gray_q_seq = gray_code(m)
|
|
369
|
+
|
|
370
|
+
# 2. Build Inverse Lookup Tables (Symbol -> Geometric Index)
|
|
371
|
+
inv_gray_i = np.zeros(width, dtype=int)
|
|
372
|
+
inv_gray_i[gray_i_seq] = np.arange(width)
|
|
373
|
+
|
|
374
|
+
inv_gray_q = np.zeros(height, dtype=int)
|
|
375
|
+
inv_gray_q[gray_q_seq] = np.arange(height)
|
|
376
|
+
|
|
377
|
+
# 3. Decompose input Symbols into I and Q components
|
|
378
|
+
s = np.arange(order)
|
|
379
|
+
mask_q = (1 << m) - 1
|
|
380
|
+
idx_i_sym = s >> m
|
|
381
|
+
idx_q_sym = s & mask_q # This is the Gray-coded Symbol Value for Q
|
|
382
|
+
|
|
383
|
+
# 4. Map Symbols to Geometric Indices (0..Width-1, 0..Height-1)
|
|
384
|
+
geo_i = inv_gray_i[idx_i_sym]
|
|
385
|
+
geo_q = inv_gray_q[idx_q_sym]
|
|
386
|
+
|
|
387
|
+
# 5. Determine Folding Parameters
|
|
388
|
+
n_shift = 0
|
|
389
|
+
if n >= 3:
|
|
390
|
+
n_shift = 2 ** (n - 3)
|
|
391
|
+
|
|
392
|
+
if n_shift == 0:
|
|
393
|
+
# Fallback
|
|
394
|
+
val_i = (-width + 1 + 2 * geo_i).astype(float)
|
|
395
|
+
val_q = (-height + 1 + 2 * geo_q).astype(float)
|
|
396
|
+
return val_i + 1j * val_q
|
|
397
|
+
|
|
398
|
+
# 6. Identify Wings
|
|
399
|
+
mask_left = geo_i < n_shift
|
|
400
|
+
mask_right = geo_i >= (width - n_shift)
|
|
401
|
+
|
|
402
|
+
# 7. Calculate Rotation/Translation
|
|
403
|
+
center_offset = (width - height) // 2
|
|
404
|
+
|
|
405
|
+
# Left Wing -> Top Cap
|
|
406
|
+
rotated_i_left = geo_q + center_offset
|
|
407
|
+
rotated_q_left = height + (n_shift - 1 - geo_i)
|
|
408
|
+
|
|
409
|
+
# Right Wing -> Bottom Cap
|
|
410
|
+
rotated_i_right = geo_q + center_offset
|
|
411
|
+
rotated_q_right = -1 - (geo_i - (width - n_shift))
|
|
412
|
+
|
|
413
|
+
# Apply Updates
|
|
414
|
+
geo_i_final = geo_i.copy()
|
|
415
|
+
geo_q_final = geo_q.copy()
|
|
416
|
+
|
|
417
|
+
geo_i_final = np.where(mask_left, rotated_i_left, geo_i_final)
|
|
418
|
+
geo_i_final = np.where(mask_right, rotated_i_right, geo_i_final)
|
|
419
|
+
|
|
420
|
+
geo_q_final = np.where(mask_left, rotated_q_left, geo_q_final)
|
|
421
|
+
geo_q_final = np.where(mask_right, rotated_q_right, geo_q_final)
|
|
422
|
+
|
|
423
|
+
# Convert Final Geometric Indices to Values
|
|
424
|
+
final_i_vals = (-width + 1 + 2 * geo_i_final).astype(float)
|
|
425
|
+
final_q_vals = (-height + 1 + 2 * geo_q_final).astype(float)
|
|
426
|
+
|
|
427
|
+
constellation = final_i_vals + 1j * final_q_vals
|
|
428
|
+
|
|
429
|
+
return constellation
|
commkit/mapping/llr.py
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Soft-decision demapping (log-likelihood ratios).
|
|
3
|
+
|
|
4
|
+
JAX JIT-compiled max-log and exact LLR kernels, with optional probabilistic-
|
|
5
|
+
shaping prior. Sign convention: positive LLR -> bit 0 more likely.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
from ..backend import ArrayType, _get_jax, dispatch, is_jax_array, to_jax
|
|
13
|
+
from ..logger import logger
|
|
14
|
+
from .gray import gray_constellation
|
|
15
|
+
|
|
16
|
+
__all__ = ["compute_llr"]
|
|
17
|
+
|
|
18
|
+
# Lazy cache for JIT-compiled soft demapping kernels
|
|
19
|
+
_JITTED_SOFT_DEMAP: dict[str, Any] = {}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _get_jitted_soft_demap():
|
|
23
|
+
"""
|
|
24
|
+
Returns JIT-compiled maxlog and exact LLR computation functions.
|
|
25
|
+
|
|
26
|
+
Functions are defined and compiled lazily on first call to avoid
|
|
27
|
+
importing JAX at module load time.
|
|
28
|
+
"""
|
|
29
|
+
if not _JITTED_SOFT_DEMAP:
|
|
30
|
+
jax, jnp, _ = _get_jax()
|
|
31
|
+
if jax is None:
|
|
32
|
+
raise ImportError(
|
|
33
|
+
"JAX is required for soft demapping. Install with: pip install jax"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
@jax.jit
|
|
37
|
+
def maxlog(symbols, constellation, bits_table_t, sigma_sq, log_pmf):
|
|
38
|
+
"""Max-log LLR with PS prior.
|
|
39
|
+
|
|
40
|
+
symbols (N,), constellation (M,), bits_table_t (k, M), log_pmf (M,).
|
|
41
|
+
Uniform case: pass log_pmf = jnp.zeros(M) - constant offset cancels.
|
|
42
|
+
PS case: log_pmf = log P(sₘ).
|
|
43
|
+
|
|
44
|
+
Effective metric: eff_m = d_m/σ² - log P(sₘ)
|
|
45
|
+
LLR_k = min_{b=1} eff - min_{b=0} eff
|
|
46
|
+
"""
|
|
47
|
+
distances_sq = (
|
|
48
|
+
jnp.abs(symbols[:, None] - constellation[None, :]) ** 2
|
|
49
|
+
) # (N, M)
|
|
50
|
+
eff = distances_sq / sigma_sq - log_pmf[None, :] # (N, M)
|
|
51
|
+
|
|
52
|
+
def bit_llr(bit_row): # (M,)
|
|
53
|
+
d0 = jnp.where(bit_row == 0, eff, jnp.inf) # (N, M)
|
|
54
|
+
d1 = jnp.where(bit_row == 1, eff, jnp.inf) # (N, M)
|
|
55
|
+
return jnp.min(d1, axis=1) - jnp.min(d0, axis=1) # (N,)
|
|
56
|
+
|
|
57
|
+
return jax.vmap(bit_llr)(bits_table_t).T # (k, N) -> (N, k)
|
|
58
|
+
|
|
59
|
+
@jax.jit
|
|
60
|
+
def exact(symbols, constellation, bits_table_t, sigma_sq, log_pmf):
|
|
61
|
+
"""Exact LLR with PS prior via log-sum-exp.
|
|
62
|
+
|
|
63
|
+
symbols (N,), constellation (M,), bits_table_t (k, M), log_pmf (M,).
|
|
64
|
+
Uniform case: pass log_pmf = jnp.zeros(M).
|
|
65
|
+
PS case: log_pmf = log P(sₘ).
|
|
66
|
+
|
|
67
|
+
log_terms_m = log P(sₘ) - d_m/σ²
|
|
68
|
+
LLR_k = LSE_{b=0}(log_terms) - LSE_{b=1}(log_terms)
|
|
69
|
+
"""
|
|
70
|
+
distances_sq = (
|
|
71
|
+
jnp.abs(symbols[:, None] - constellation[None, :]) ** 2
|
|
72
|
+
) # (N, M)
|
|
73
|
+
log_terms = log_pmf[None, :] - distances_sq / sigma_sq # (N, M)
|
|
74
|
+
|
|
75
|
+
def bit_llr(bit_row): # (M,)
|
|
76
|
+
e0 = jnp.where(bit_row == 0, log_terms, -jnp.inf) # (N, M)
|
|
77
|
+
e1 = jnp.where(bit_row == 1, log_terms, -jnp.inf) # (N, M)
|
|
78
|
+
return jax.scipy.special.logsumexp(
|
|
79
|
+
e0, axis=1
|
|
80
|
+
) - jax.scipy.special.logsumexp(e1, axis=1) # (N,)
|
|
81
|
+
|
|
82
|
+
return jax.vmap(bit_llr)(bits_table_t).T # (k, N) -> (N, k)
|
|
83
|
+
|
|
84
|
+
_JITTED_SOFT_DEMAP["maxlog"] = maxlog
|
|
85
|
+
_JITTED_SOFT_DEMAP["exact"] = exact
|
|
86
|
+
|
|
87
|
+
return _JITTED_SOFT_DEMAP["maxlog"], _JITTED_SOFT_DEMAP["exact"]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def compute_llr(
|
|
91
|
+
symbols: ArrayType,
|
|
92
|
+
modulation: str,
|
|
93
|
+
order: int,
|
|
94
|
+
noise_var: float,
|
|
95
|
+
method: str = "maxlog",
|
|
96
|
+
unipolar: bool = False,
|
|
97
|
+
output: str = "jax",
|
|
98
|
+
pmf: np.ndarray | None = None,
|
|
99
|
+
) -> ArrayType:
|
|
100
|
+
"""
|
|
101
|
+
Compute Log-Likelihood Ratios (LLRs) for soft-decision decoding.
|
|
102
|
+
|
|
103
|
+
Positive LLR -> bit 0 more likely; negative -> bit 1; magnitude = confidence.
|
|
104
|
+
JAX JIT-compiled with ``jax.vmap`` over bit positions; fully differentiable.
|
|
105
|
+
|
|
106
|
+
Parameters
|
|
107
|
+
----------
|
|
108
|
+
symbols : array_like
|
|
109
|
+
Received noisy symbols. Shape: (..., N_symbols). NumPy, CuPy, or JAX.
|
|
110
|
+
modulation : {"psk", "qam", "ask"}
|
|
111
|
+
Modulation type.
|
|
112
|
+
order : int
|
|
113
|
+
Modulation order.
|
|
114
|
+
noise_var : float
|
|
115
|
+
Complex noise variance sigma^2 referenced to the normalised
|
|
116
|
+
constellation (unit avg power). sigma^2 = 10^(-EsN0_dB / 10).
|
|
117
|
+
method : {"maxlog", "exact"}, default "maxlog"
|
|
118
|
+
LLR algorithm. ``"maxlog"`` is faster; ``"exact"`` uses log-sum-exp.
|
|
119
|
+
unipolar : bool, default False
|
|
120
|
+
Use unipolar constellation for ASK/PAM.
|
|
121
|
+
output : {"jax", "input", "numpy"}, default "jax"
|
|
122
|
+
Output array type. ``"jax"`` preserves differentiability;
|
|
123
|
+
``"input"`` matches the input backend; ``"numpy"`` forces NumPy.
|
|
124
|
+
pmf : np.ndarray, optional
|
|
125
|
+
Symbol PMF of shape ``(order,)`` for PS-QAM. Pass
|
|
126
|
+
``maxwell_boltzmann(order, nu)`` to incorporate the non-uniform prior.
|
|
127
|
+
``None`` assumes uniform prior.
|
|
128
|
+
|
|
129
|
+
Returns
|
|
130
|
+
-------
|
|
131
|
+
array_like
|
|
132
|
+
LLR values. Shape: (..., N_symbols * log2(order)).
|
|
133
|
+
Array type determined by ``output``.
|
|
134
|
+
|
|
135
|
+
Notes
|
|
136
|
+
-----
|
|
137
|
+
Max-Log: LLR_k ≈ (1/sigma^2) * (min_{S_1^k} |r-s|^2 - min_{S_0^k} |r-s|^2).
|
|
138
|
+
Exact: LLR_k = log sum_{S_0^k} exp(-|r-s|^2/sigma^2) - log sum_{S_1^k} ...
|
|
139
|
+
|
|
140
|
+
For PS-QAM, ``symbols`` must be on the same scale as
|
|
141
|
+
``gray_constellation`` (unit avg power). After
|
|
142
|
+
``resolve_symbols`` the receiver renormalises;
|
|
143
|
+
use ``gmi`` instead for correct scale.
|
|
144
|
+
"""
|
|
145
|
+
logger.debug(
|
|
146
|
+
"Computing LLRs for %s %s-level (method=%s, output=%s).",
|
|
147
|
+
modulation.upper(),
|
|
148
|
+
order,
|
|
149
|
+
method,
|
|
150
|
+
output,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
k = int(np.log2(order))
|
|
154
|
+
if 2**k != order:
|
|
155
|
+
raise ValueError(f"Order must be a power of 2, got {order}")
|
|
156
|
+
if method not in ("maxlog", "exact"):
|
|
157
|
+
raise ValueError(f"Unknown method: {method}. Use 'maxlog' or 'exact'.")
|
|
158
|
+
if output not in ("jax", "input", "numpy"):
|
|
159
|
+
raise ValueError(f"Unknown output: {output!r}. Use 'jax', 'input', or 'numpy'.")
|
|
160
|
+
|
|
161
|
+
# Convert to JAX if not already
|
|
162
|
+
jax, jnp, _ = _get_jax()
|
|
163
|
+
if jax is None:
|
|
164
|
+
raise ImportError(
|
|
165
|
+
"JAX is required for LLR computation. Install with: pip install jax"
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# Build constellation and bits table on CPU once - shared by both paths.
|
|
169
|
+
is_complex = symbols.dtype.kind == "c"
|
|
170
|
+
const = gray_constellation(modulation, order, unipolar=unipolar).astype(
|
|
171
|
+
"complex64" if is_complex else "float32"
|
|
172
|
+
)
|
|
173
|
+
bits_table_np = (
|
|
174
|
+
(
|
|
175
|
+
(
|
|
176
|
+
np.arange(order, dtype="int32")[:, None]
|
|
177
|
+
>> np.arange(k - 1, -1, -1, dtype="int32")
|
|
178
|
+
)
|
|
179
|
+
& 1
|
|
180
|
+
)
|
|
181
|
+
.astype("int32")
|
|
182
|
+
.T
|
|
183
|
+
)
|
|
184
|
+
sigma_np = np.float32(max(noise_var, 1e-20))
|
|
185
|
+
|
|
186
|
+
# Build log_pmf: zeros = uniform (constant offset cancels in LLR difference).
|
|
187
|
+
if pmf is not None:
|
|
188
|
+
log_pmf_np = np.log(np.clip(np.asarray(pmf, dtype=np.float32), 1e-40, None))
|
|
189
|
+
else:
|
|
190
|
+
log_pmf_np = np.zeros(order, dtype=np.float32)
|
|
191
|
+
|
|
192
|
+
# JAX path
|
|
193
|
+
jax_module, jnp, _ = _get_jax()
|
|
194
|
+
if is_jax_array(symbols):
|
|
195
|
+
assert jnp is not None
|
|
196
|
+
if hasattr(symbols, "shape"):
|
|
197
|
+
original_shape = symbols.shape
|
|
198
|
+
else:
|
|
199
|
+
# Fallback for JAX tracers or odd objects
|
|
200
|
+
original_shape = jnp.shape(symbols)
|
|
201
|
+
|
|
202
|
+
jax_symbols_flat = symbols.flatten()
|
|
203
|
+
constellation_jax = jnp.asarray(const)
|
|
204
|
+
bits_table_t_jax = jnp.asarray(bits_table_np)
|
|
205
|
+
sigma_sq = jnp.asarray(sigma_np)
|
|
206
|
+
log_pmf_jax = jnp.asarray(log_pmf_np)
|
|
207
|
+
|
|
208
|
+
# NumPy/CuPy path
|
|
209
|
+
else:
|
|
210
|
+
symbols, xp, _ = dispatch(symbols)
|
|
211
|
+
original_shape = symbols.shape
|
|
212
|
+
|
|
213
|
+
jax_symbols_flat = to_jax(
|
|
214
|
+
symbols, dtype="complex64" if is_complex else "float32"
|
|
215
|
+
).flatten()
|
|
216
|
+
device = jax_symbols_flat.device
|
|
217
|
+
|
|
218
|
+
assert jax_module is not None
|
|
219
|
+
assert jnp is not None
|
|
220
|
+
# device_put accepts NumPy arrays directly - no intermediate jnp.asarray needed
|
|
221
|
+
constellation_jax = jax_module.device_put(const, device)
|
|
222
|
+
bits_table_t_jax = jax_module.device_put(bits_table_np, device)
|
|
223
|
+
sigma_sq = jax_module.device_put(jnp.asarray(sigma_np), device)
|
|
224
|
+
log_pmf_jax = jax_module.device_put(log_pmf_np, device)
|
|
225
|
+
|
|
226
|
+
# Compute LLRs via JIT-compiled kernels
|
|
227
|
+
maxlog_fn, exact_fn = _get_jitted_soft_demap()
|
|
228
|
+
if method == "maxlog":
|
|
229
|
+
llrs = maxlog_fn(
|
|
230
|
+
jax_symbols_flat, constellation_jax, bits_table_t_jax, sigma_sq, log_pmf_jax
|
|
231
|
+
)
|
|
232
|
+
else:
|
|
233
|
+
llrs = exact_fn(
|
|
234
|
+
jax_symbols_flat, constellation_jax, bits_table_t_jax, sigma_sq, log_pmf_jax
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
# Reshape to match input structure
|
|
238
|
+
flat_llrs = llrs.flatten()
|
|
239
|
+
if len(original_shape) > 1:
|
|
240
|
+
new_shape = list(original_shape)
|
|
241
|
+
new_shape[-1] = new_shape[-1] * k
|
|
242
|
+
flat_llrs = flat_llrs.reshape(new_shape)
|
|
243
|
+
|
|
244
|
+
# Convert output to the requested backend
|
|
245
|
+
if output == "jax":
|
|
246
|
+
return flat_llrs
|
|
247
|
+
elif output == "numpy":
|
|
248
|
+
return np.asarray(flat_llrs)
|
|
249
|
+
else: # output == "input"
|
|
250
|
+
if is_jax_array(symbols):
|
|
251
|
+
return flat_llrs # already JAX
|
|
252
|
+
# xp is NumPy or CuPy - convert via NumPy intermediate
|
|
253
|
+
return xp.asarray(np.asarray(flat_llrs))
|