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,218 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Probabilistic shaping (PS-QAM).
|
|
3
|
+
|
|
4
|
+
Maxwell-Boltzmann symbol distributions over a QAM constellation, the entropy /
|
|
5
|
+
shaping-parameter inversion, MB sampling, and the pmf-weighted constellation
|
|
6
|
+
power that ties the PS scale conventions together.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from functools import lru_cache
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
from ..backend import ArrayType, to_device
|
|
14
|
+
from .gray import gray_constellation
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"constellation_power",
|
|
18
|
+
"maxwell_boltzmann",
|
|
19
|
+
"optimal_nu",
|
|
20
|
+
"ps_entropy",
|
|
21
|
+
"sample_ps_symbols",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def constellation_power(
|
|
26
|
+
constellation: ArrayType, pmf: ArrayType | None = None
|
|
27
|
+
) -> float:
|
|
28
|
+
r"""Average symbol power ``E[|s|^2]`` of a constellation.
|
|
29
|
+
|
|
30
|
+
For a uniform constellation (``pmf=None``) this is the unweighted mean
|
|
31
|
+
power ``mean(|s|^2)``. For probabilistic shaping it is the pmf-weighted
|
|
32
|
+
power ``Σ_m P(s_m) |s_m|^2`` - the quantity written ``E_PS`` when the
|
|
33
|
+
constellation is on the normalised grid (where it is ``< 1``).
|
|
34
|
+
|
|
35
|
+
The value is *grid-agnostic*: it reports the average power of the
|
|
36
|
+
constellation exactly as passed - ``≈1`` for a normalised uniform grid,
|
|
37
|
+
``E_PS < 1`` for a normalised shaped grid, or the raw integer-grid energy
|
|
38
|
+
(e.g. ``10`` for an unnormalised 16-QAM). Callers apply their own
|
|
39
|
+
rescaling (``√E_PS`` on the received symbols, or ``1/√E_PS`` on the
|
|
40
|
+
constellation) using the returned value.
|
|
41
|
+
|
|
42
|
+
This is the single source of truth for PS-QAM power across the library;
|
|
43
|
+
prefer it over the inline ``Σ pmf·|s|^2`` idiom.
|
|
44
|
+
|
|
45
|
+
Parameters
|
|
46
|
+
----------
|
|
47
|
+
constellation : array_like
|
|
48
|
+
Constellation points, shape ``(M,)``. NumPy, CuPy, or list.
|
|
49
|
+
pmf : array_like, optional
|
|
50
|
+
Per-point probabilities, shape ``(M,)``, aligned with
|
|
51
|
+
``constellation`` and summing to 1 (e.g. from
|
|
52
|
+
:func:`maxwell_boltzmann`). ``None`` (uniform) returns the
|
|
53
|
+
unweighted mean power.
|
|
54
|
+
|
|
55
|
+
Returns
|
|
56
|
+
-------
|
|
57
|
+
float
|
|
58
|
+
Host scalar ``E[|s|^2]``.
|
|
59
|
+
|
|
60
|
+
Raises
|
|
61
|
+
------
|
|
62
|
+
ValueError
|
|
63
|
+
If ``pmf`` is supplied and its length does not match the
|
|
64
|
+
constellation.
|
|
65
|
+
"""
|
|
66
|
+
const = np.asarray(to_device(constellation, "cpu"))
|
|
67
|
+
energies = np.abs(const).astype(np.float64) ** 2
|
|
68
|
+
if pmf is None:
|
|
69
|
+
return float(np.mean(energies))
|
|
70
|
+
pmf_arr = np.asarray(to_device(pmf, "cpu"), dtype=np.float64).ravel()
|
|
71
|
+
if pmf_arr.shape[0] != energies.shape[0]:
|
|
72
|
+
raise ValueError(
|
|
73
|
+
f"pmf length {pmf_arr.shape[0]} does not match constellation "
|
|
74
|
+
f"length {energies.shape[0]}."
|
|
75
|
+
)
|
|
76
|
+
return float(np.dot(pmf_arr, energies))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@lru_cache(maxsize=256)
|
|
80
|
+
def maxwell_boltzmann(order: int, nu: float) -> np.ndarray:
|
|
81
|
+
"""
|
|
82
|
+
Computes the Maxwell-Boltzmann PMF over a QAM constellation.
|
|
83
|
+
|
|
84
|
+
P(s_m) = exp(-nu * |s_m|^2) / Z(nu), where |s_m|^2 is on the unnormalized
|
|
85
|
+
integer grid (literature-compatible nu scale). Indexed consistently with
|
|
86
|
+
``gray_constellation``.
|
|
87
|
+
|
|
88
|
+
Parameters
|
|
89
|
+
----------
|
|
90
|
+
order : int
|
|
91
|
+
QAM modulation order (must be a power of 2, e.g. 16, 64, 256).
|
|
92
|
+
nu : float
|
|
93
|
+
Shaping parameter nu >= 0. nu = 0 returns the uniform distribution.
|
|
94
|
+
Larger nu concentrates probability on lower-energy points.
|
|
95
|
+
|
|
96
|
+
Returns
|
|
97
|
+
-------
|
|
98
|
+
np.ndarray
|
|
99
|
+
PMF array of shape ``(order,)``, dtype float64, summing to 1.
|
|
100
|
+
Cached by ``(order, nu)``.
|
|
101
|
+
"""
|
|
102
|
+
# Energies computed on the unnormalized integer grid for literature-compatible ν.
|
|
103
|
+
# The PMF index ordering matches gray_constellation (normalized), since both
|
|
104
|
+
# use the same Gray-code index assignment.
|
|
105
|
+
unnorm_constellation = gray_constellation("qam", order, normalize=False)
|
|
106
|
+
if nu == 0.0:
|
|
107
|
+
return np.full(order, 1.0 / order, dtype=np.float64)
|
|
108
|
+
energies = np.abs(unnorm_constellation) ** 2 # (M,) unnormalized energies
|
|
109
|
+
log_p = -nu * energies
|
|
110
|
+
log_p -= log_p.max() # shift for numerical stability before exp
|
|
111
|
+
p = np.exp(log_p)
|
|
112
|
+
return p / p.sum()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def ps_entropy(order: int, nu: float) -> float:
|
|
116
|
+
r"""
|
|
117
|
+
Computes the per-symbol Shannon entropy under a Maxwell-Boltzmann distribution.
|
|
118
|
+
|
|
119
|
+
H(X) = -sum_m P(s_m) * log2(P(s_m)) [bits/symbol]
|
|
120
|
+
|
|
121
|
+
Parameters
|
|
122
|
+
----------
|
|
123
|
+
order : int
|
|
124
|
+
QAM modulation order.
|
|
125
|
+
nu : float
|
|
126
|
+
MB shaping parameter nu >= 0. nu = 0 returns log2(order).
|
|
127
|
+
|
|
128
|
+
Returns
|
|
129
|
+
-------
|
|
130
|
+
float
|
|
131
|
+
Entropy in bits per symbol. In the range (0, log2(order)].
|
|
132
|
+
"""
|
|
133
|
+
pmf = maxwell_boltzmann(order, nu)
|
|
134
|
+
nonzero = pmf > 0
|
|
135
|
+
return float(-np.sum(pmf[nonzero] * np.log2(pmf[nonzero])))
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def optimal_nu(order: int, entropy_bits: float) -> tuple:
|
|
139
|
+
r"""
|
|
140
|
+
Finds the MB shaping parameter ``ν`` that achieves a target per-symbol entropy.
|
|
141
|
+
|
|
142
|
+
Uses ``scipy.optimize.brentq`` to bisect on
|
|
143
|
+
``ps_entropy(order, ν) - entropy_bits = 0``.
|
|
144
|
+
|
|
145
|
+
Parameters
|
|
146
|
+
----------
|
|
147
|
+
order : int
|
|
148
|
+
QAM modulation order.
|
|
149
|
+
entropy_bits : float
|
|
150
|
+
Target per-symbol entropy in bits. Must be in ``(0, log₂(order)]``.
|
|
151
|
+
|
|
152
|
+
Returns
|
|
153
|
+
-------
|
|
154
|
+
nu : float
|
|
155
|
+
Shaping parameter achieving the target entropy.
|
|
156
|
+
achieved_entropy : float
|
|
157
|
+
Actual entropy at the returned ``nu`` (may differ by ``< 1e-6`` bits).
|
|
158
|
+
|
|
159
|
+
Raises
|
|
160
|
+
------
|
|
161
|
+
ValueError
|
|
162
|
+
If ``entropy_bits`` is outside ``(0, log₂(order)]``.
|
|
163
|
+
"""
|
|
164
|
+
from scipy.optimize import brentq
|
|
165
|
+
|
|
166
|
+
max_h = np.log2(order)
|
|
167
|
+
if not (0 < entropy_bits <= max_h):
|
|
168
|
+
raise ValueError(
|
|
169
|
+
f"entropy_bits must be in (0, {max_h:.3f}] for {order}-QAM, "
|
|
170
|
+
f"got {entropy_bits:.4f}"
|
|
171
|
+
)
|
|
172
|
+
if np.isclose(entropy_bits, max_h, atol=1e-8):
|
|
173
|
+
return 0.0, float(max_h)
|
|
174
|
+
|
|
175
|
+
def _obj(nu):
|
|
176
|
+
return ps_entropy(order, nu) - entropy_bits
|
|
177
|
+
|
|
178
|
+
# Grow upper bound until entropy drops below target
|
|
179
|
+
nu_hi = 0.01
|
|
180
|
+
while ps_entropy(order, nu_hi) > entropy_bits:
|
|
181
|
+
nu_hi *= 10.0
|
|
182
|
+
|
|
183
|
+
nu_opt = float(brentq(_obj, 0.0, nu_hi, xtol=1e-9, rtol=1e-9))
|
|
184
|
+
return nu_opt, ps_entropy(order, nu_opt)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def sample_ps_symbols(
|
|
188
|
+
num_symbols: int,
|
|
189
|
+
order: int,
|
|
190
|
+
pmf: np.ndarray,
|
|
191
|
+
seed: int | None = None,
|
|
192
|
+
) -> np.ndarray:
|
|
193
|
+
"""
|
|
194
|
+
Draws QAM symbols from a Maxwell-Boltzmann distribution.
|
|
195
|
+
|
|
196
|
+
Parameters
|
|
197
|
+
----------
|
|
198
|
+
num_symbols : int
|
|
199
|
+
Number of symbols to generate.
|
|
200
|
+
order : int
|
|
201
|
+
QAM modulation order.
|
|
202
|
+
pmf : np.ndarray
|
|
203
|
+
Symbol PMF of shape ``(order,)``. Typically from
|
|
204
|
+
``maxwell_boltzmann``.
|
|
205
|
+
seed : int, optional
|
|
206
|
+
Random seed for reproducibility.
|
|
207
|
+
|
|
208
|
+
Returns
|
|
209
|
+
-------
|
|
210
|
+
np.ndarray
|
|
211
|
+
Complex-valued symbols drawn from the MB distribution.
|
|
212
|
+
Shape: ``(num_symbols,)``, dtype ``complex64``.
|
|
213
|
+
All values lie exactly on the normalized QAM constellation grid.
|
|
214
|
+
"""
|
|
215
|
+
rng = np.random.default_rng(seed)
|
|
216
|
+
constellation = gray_constellation("qam", order).astype(np.complex64)
|
|
217
|
+
indices = rng.choice(order, size=num_symbols, p=np.asarray(pmf, dtype=np.float64))
|
|
218
|
+
return constellation[indices]
|