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.
Files changed (84) hide show
  1. commkit/__init__.py +74 -0
  2. commkit/_cuda/__init__.py +321 -0
  3. commkit/_cuda/compiler.py +88 -0
  4. commkit/_cuda/src/bps_min_d2.cu +104 -0
  5. commkit/_cuda/src/cs_block.cu +119 -0
  6. commkit/_cuda/src/selftest.cu +14 -0
  7. commkit/analysis/__init__.py +55 -0
  8. commkit/analysis/_common.py +236 -0
  9. commkit/analysis/allan.py +108 -0
  10. commkit/analysis/drift.py +213 -0
  11. commkit/analysis/interferometry.py +887 -0
  12. commkit/analysis/linewidth.py +480 -0
  13. commkit/analysis/trajectory.py +91 -0
  14. commkit/backend.py +507 -0
  15. commkit/coding/__init__.py +23 -0
  16. commkit/coding/base.py +17 -0
  17. commkit/coding/bch.py +6 -0
  18. commkit/coding/convolutional.py +7 -0
  19. commkit/coding/crc.py +7 -0
  20. commkit/coding/galois.py +8 -0
  21. commkit/coding/hamming.py +6 -0
  22. commkit/coding/interleaving.py +7 -0
  23. commkit/coding/ldpc.py +8 -0
  24. commkit/coding/polar.py +8 -0
  25. commkit/coding/ratematch.py +6 -0
  26. commkit/coding/reed_solomon.py +6 -0
  27. commkit/coding/turbo.py +8 -0
  28. commkit/core/__init__.py +32 -0
  29. commkit/core/frame.py +992 -0
  30. commkit/core/generation.py +581 -0
  31. commkit/core/signal.py +725 -0
  32. commkit/equalization/__init__.py +49 -0
  33. commkit/equalization/_block.py +1855 -0
  34. commkit/equalization/_common.py +606 -0
  35. commkit/equalization/_kernels_jax.py +1720 -0
  36. commkit/equalization/_kernels_numba.py +1704 -0
  37. commkit/equalization/blind.py +223 -0
  38. commkit/equalization/linear.py +365 -0
  39. commkit/equalization/polarization.py +790 -0
  40. commkit/equalization/result.py +191 -0
  41. commkit/equalization/sequential.py +2805 -0
  42. commkit/filtering.py +1120 -0
  43. commkit/frequency.py +1191 -0
  44. commkit/helpers.py +489 -0
  45. commkit/impairments/__init__.py +43 -0
  46. commkit/impairments/channel/__init__.py +20 -0
  47. commkit/impairments/channel/linear.py +310 -0
  48. commkit/impairments/channel/nonlinear.py +11 -0
  49. commkit/impairments/frontend.py +229 -0
  50. commkit/impairments/noise.py +105 -0
  51. commkit/impairments/source.py +219 -0
  52. commkit/io.py +308 -0
  53. commkit/logger.py +103 -0
  54. commkit/mapping/__init__.py +46 -0
  55. commkit/mapping/bits.py +240 -0
  56. commkit/mapping/constellation.py +153 -0
  57. commkit/mapping/gray.py +429 -0
  58. commkit/mapping/llr.py +253 -0
  59. commkit/mapping/shaping.py +218 -0
  60. commkit/metrics.py +949 -0
  61. commkit/multirate.py +476 -0
  62. commkit/plotting/__init__.py +78 -0
  63. commkit/plotting/analysis.py +627 -0
  64. commkit/plotting/constellation.py +483 -0
  65. commkit/plotting/equalizer.py +390 -0
  66. commkit/plotting/eye.py +388 -0
  67. commkit/plotting/spectral.py +575 -0
  68. commkit/plotting/sync.py +953 -0
  69. commkit/plotting/theme.py +203 -0
  70. commkit/plotting/waveform.py +200 -0
  71. commkit/py.typed +0 -0
  72. commkit/recovery/__init__.py +51 -0
  73. commkit/recovery/bps.py +337 -0
  74. commkit/recovery/corrections.py +751 -0
  75. commkit/recovery/pilots.py +803 -0
  76. commkit/recovery/pll.py +482 -0
  77. commkit/recovery/tikhonov.py +424 -0
  78. commkit/recovery/viterbi_viterbi.py +227 -0
  79. commkit/spectral.py +560 -0
  80. commkit/timing.py +841 -0
  81. commkit-1.0.0.dist-info/METADATA +145 -0
  82. commkit-1.0.0.dist-info/RECORD +84 -0
  83. commkit-1.0.0.dist-info/WHEEL +4 -0
  84. commkit-1.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,240 @@
1
+ """
2
+ Hard bit mapping and demapping.
3
+
4
+ Maps a flat bit sequence to Gray-coded symbols (``map_bits``) and recovers the
5
+ most-likely bits from received symbols via minimum-distance decoding
6
+ (``demap_symbols_hard``).
7
+ """
8
+
9
+ import numpy as np
10
+
11
+ from ..backend import ArrayType, dispatch
12
+ from ..core.signal import Signal
13
+ from ..logger import logger
14
+ from .gray import gray_code, gray_constellation
15
+ from .shaping import constellation_power
16
+
17
+ __all__ = ["demap_symbols_hard", "map_bits"]
18
+
19
+
20
+ def map_bits(
21
+ bits: ArrayType,
22
+ modulation: str,
23
+ order: int,
24
+ unipolar: bool = False,
25
+ ) -> ArrayType:
26
+ """
27
+ Maps a bit sequence to complex or real symbols.
28
+
29
+ This function follows a bit-first architecture: it takes a flat sequence
30
+ of bits and packs them into symbols according to the modulation scheme
31
+ and Gray mapping.
32
+
33
+ Output dtype is ``complex64`` for PSK/QAM and ``float32`` for ASK/PAM.
34
+
35
+ Parameters
36
+ ----------
37
+ bits : array_like
38
+ Input binary sequence (0s and 1s).
39
+ modulation : {"psk", "qam", "ask", "pam"}
40
+ Modulation scheme.
41
+ order : int
42
+ Modulation order (number of symbols).
43
+ unipolar : bool, default False
44
+ Trigger unipolar mapping for ASK/PAM.
45
+
46
+ Returns
47
+ -------
48
+ array_like
49
+ Array of symbols. Shape: (N_bits / log2(order),).
50
+ Backend (NumPy/CuPy) matches the input `bits`.
51
+ """
52
+ logger.debug("Mapping bits to %s %s-level symbols.", modulation.upper(), order)
53
+ bits, xp, _ = dispatch(bits)
54
+
55
+ if order < 2:
56
+ raise ValueError("Order must be at least 2 for modulation")
57
+
58
+ k = int(np.log2(order))
59
+ if 2**k != order:
60
+ raise ValueError(f"Order must be a power of 2, got {order}")
61
+
62
+ if len(bits) % k != 0:
63
+ raise ValueError(
64
+ f"Number of bits ({len(bits)}) must be divisible by bits per symbol ({k})"
65
+ )
66
+
67
+ # Reshape bits into symbols (N/k, k)
68
+ num_symbols = len(bits) // k
69
+
70
+ # Pack bits into integer indices
71
+ bits_reshaped = bits.reshape((num_symbols, k))
72
+ powers = 2 ** xp.arange(k - 1, -1, -1, dtype=int)
73
+ indices = xp.sum(bits_reshaped * powers, axis=1)
74
+
75
+ # Get constellation (returns NumPy)
76
+ constellation = gray_constellation(modulation, order, unipolar=unipolar)
77
+
78
+ # Ensure constellation is on the same backend and dtype
79
+ constellation = xp.asarray(constellation)
80
+
81
+ # ASK/PAM constellations are real-valued; PSK/QAM are complex.
82
+ mod_lower = modulation.lower()
83
+
84
+ if "ask" in mod_lower or "pam" in mod_lower:
85
+ constellation = constellation.astype(xp.float32)
86
+ else:
87
+ constellation = constellation.astype(xp.complex64)
88
+
89
+ # Map indices to points
90
+ return constellation[indices]
91
+
92
+
93
+ def demap_symbols_hard(
94
+ symbols: ArrayType | Signal,
95
+ modulation: str | None = None,
96
+ order: int | None = None,
97
+ unipolar: bool = False,
98
+ pmf: np.ndarray | None = None,
99
+ ) -> ArrayType | Signal:
100
+ """
101
+ Maps complex symbols back to a sequence of bits (hard decisions).
102
+
103
+ This function performs minimum Euclidean distance decoding to find the
104
+ most likely bit pattern for each received symbol.
105
+
106
+ Parameters
107
+ ----------
108
+ symbols : array_like
109
+ Input array of received symbols. Shape: (..., N_symbols).
110
+ modulation : {"psk", "qam", "ask"}
111
+ Modulation type.
112
+ order : int
113
+ Modulation order.
114
+ unipolar : bool, default False
115
+ Trigger unipolar demapping for ASK/PAM.
116
+ pmf : np.ndarray, optional
117
+ Symbol PMF of shape ``(order,)`` for PS-QAM. When provided, the
118
+ input symbols are scaled by ``sqrt(E_PS)`` (where
119
+ ``E_PS = Σ P(s_m) |s_m|²`` on the normalised grid) before the
120
+ nearest-neighbour search, mapping unit-avg-power resolved symbols
121
+ back to the ``{s_m}`` grid used by ``gray_constellation``.
122
+ Use this when ``symbols`` comes from
123
+ ``resolved_symbols`` of a PS-QAM signal.
124
+ Has no effect for uniform modulations.
125
+
126
+ Returns
127
+ -------
128
+ array_like
129
+ Sequence of bits (0s and 1s). Shape: (..., N_symbols * log2(order)).
130
+ The backend (NumPy/CuPy) matches the input `symbols`.
131
+ """
132
+ if isinstance(symbols, Signal):
133
+ sig = symbols
134
+ if sig.signal_type is not None:
135
+ logger.warning(
136
+ "demap_symbols_hard() called on a frame-generated signal - skipping. "
137
+ "Extract the payload segment via frame.get_structure_map() and build "
138
+ "a plain Signal before demapping."
139
+ )
140
+ return sig.copy()
141
+ if sig.mod_scheme is None or sig.mod_order is None:
142
+ raise ValueError("Modulation scheme and order required for demapping.")
143
+ if sig.resolved_symbols is None:
144
+ raise ValueError(
145
+ "No resolved symbols available. Call resolve_symbols(sig) first."
146
+ )
147
+ new = sig.copy()
148
+ new.resolved_bits = demap_symbols_hard(
149
+ sig.resolved_symbols,
150
+ sig.mod_scheme,
151
+ sig.mod_order,
152
+ unipolar=sig.mod_unipolar or False,
153
+ pmf=sig.ps_pmf if pmf is None else pmf,
154
+ )
155
+ return new
156
+
157
+ if modulation is None or order is None:
158
+ raise ValueError("demap_symbols_hard() requires modulation and order.")
159
+
160
+ logger.debug("Demapping %s %s-level symbols to bits.", modulation.upper(), order)
161
+ symbols, xp, _ = dispatch(symbols)
162
+
163
+ # Capture original shape to restore structure later
164
+ # If input is (N,), output (N*k,)
165
+ # If input is (C, N), output (C, N*k)
166
+ original_shape = symbols.shape
167
+
168
+ # Ensure 1D for vectorized distance broadcasting
169
+ symbols_flat = symbols.flatten()
170
+
171
+ # Get constellation
172
+ constellation = gray_constellation(modulation, order, unipolar=unipolar)
173
+
174
+ # PS-QAM: receive-path symbols at unit average power live on the
175
+ # ``{s_m/sqrt(E_PS)}`` grid. Rescale by ``sqrt(E_PS)`` to bring them back
176
+ # to the ``{s_m}`` grid that ``gray_constellation`` returns, so the
177
+ # nearest-neighbour search is exact. No-op for uniform modulations.
178
+ if pmf is not None:
179
+ e_ps = constellation_power(constellation, pmf)
180
+ if e_ps < 1.0 - 1e-6:
181
+ symbols_flat = symbols_flat * xp.asarray(
182
+ np.sqrt(e_ps), dtype=symbols_flat.real.dtype
183
+ )
184
+
185
+ constellation = xp.asarray(constellation)
186
+
187
+ # 1. Find nearest constellation point (Hard Decision)
188
+ k = int(np.log2(order))
189
+ is_sq_qam = (modulation == "qam") and (order != 8) and (k % 2 == 0)
190
+
191
+ if is_sq_qam:
192
+ # O(1) component-wise rounding - no (N, M) distance matrix.
193
+ # Levels are evenly spaced; round each axis independently.
194
+ n_ax = k // 2 # bits per axis
195
+ side = 2**n_ax # points per axis
196
+ levels = xp.sort(xp.unique(constellation.real)) # (side,)
197
+ lev_min = float(levels[0])
198
+ d_grid = float(levels[1] - levels[0])
199
+ # Gray LUT: sorted-level index (geometric) -> natural-binary symbol index
200
+ gray_lut = xp.asarray(gray_code(n_ax)) # (side,)
201
+ g_i = xp.clip(
202
+ xp.round((symbols_flat.real - lev_min) / d_grid).astype(xp.int64),
203
+ 0,
204
+ side - 1,
205
+ )
206
+ g_q = xp.clip(
207
+ xp.round((symbols_flat.imag - lev_min) / d_grid).astype(xp.int64),
208
+ 0,
209
+ side - 1,
210
+ )
211
+ indices = (gray_lut[g_i] << n_ax) | gray_lut[g_q] # (N_flat,)
212
+ else:
213
+ # General path: chunk N to bound peak memory at (CHUNK_N, M_const).
214
+ CHUNK_N = 4096
215
+ indices = xp.empty(len(symbols_flat), dtype=xp.int64)
216
+ for n0 in range(0, len(symbols_flat), CHUNK_N):
217
+ n1 = min(n0 + CHUNK_N, len(symbols_flat))
218
+ d = xp.abs(symbols_flat[n0:n1, xp.newaxis] - constellation[xp.newaxis, :])
219
+ indices[n0:n1] = xp.argmin(d, axis=1)
220
+
221
+ # 2. Convert indices to bits
222
+ # Extract bits from indices: (N, k)
223
+ # We use bit shifting: (index >> shift) & 1
224
+ shifts = xp.arange(k - 1, -1, -1, dtype="int32")
225
+ bits = ((indices[:, xp.newaxis] >> shifts) & 1).astype(xp.int8)
226
+
227
+ # 3. Reshape to restore original structure
228
+ # bits is currently (Total_Symbols, k)
229
+ # We flatten it to (Total_Symbols * k)
230
+ flat_bits = bits.flatten()
231
+
232
+ # Calculate new shape: replace last dimension D with D*k
233
+ # If original shape was (D1, D2, ..., Dn), new shape is (D1, D2, ..., Dn * k)
234
+ if len(original_shape) > 0:
235
+ new_shape = list(original_shape)
236
+ new_shape[-1] = new_shape[-1] * k
237
+ return flat_bits.reshape(new_shape)
238
+ else:
239
+ # Scalar input case (if supported), returns 1D array of bits
240
+ return flat_bits
@@ -0,0 +1,153 @@
1
+ """
2
+ The :class:`Constellation` value object.
3
+
4
+ A small immutable container bundling the three things that are otherwise
5
+ passed around as loose tuples/arrays at every CMA/RDE radius and GMI/MI call
6
+ site: the constellation ``points``, their natural-binary ``bit_labels``, and an
7
+ optional probabilistic-shaping ``pmf``. ``Constellation.gray(...)`` is the
8
+ canonical constructor (delegating to :func:`gray_constellation`), and
9
+ ``power()`` / ``map()`` / ``demap()`` / ``llr()`` are thin wrappers over the
10
+ existing free functions so a single object carries geometry + labels + prior
11
+ together.
12
+
13
+ The loose-array forms (``gray_constellation``, ``map_bits``,
14
+ ``demap_symbols_hard``, ``compute_llr``, ``constellation_power``) remain the
15
+ public surface; this object is an additive convenience layered on top.
16
+ """
17
+
18
+ from dataclasses import dataclass, replace
19
+ from functools import lru_cache
20
+
21
+ import numpy as np
22
+
23
+ from ..backend import ArrayType
24
+ from .bits import demap_symbols_hard, map_bits
25
+ from .gray import gray_constellation
26
+ from .llr import compute_llr
27
+ from .shaping import constellation_power
28
+
29
+ __all__ = ["Constellation"]
30
+
31
+
32
+ @dataclass(frozen=True, eq=False)
33
+ class Constellation:
34
+ """Immutable constellation: points, Gray bit-labels, and optional PS pmf.
35
+
36
+ Attributes
37
+ ----------
38
+ points : np.ndarray
39
+ Constellation points, shape ``(M,)``, indexed by natural-binary symbol
40
+ value (``points[i]`` is the symbol for integer ``i``). Complex for
41
+ PSK/QAM, real for ASK/PAM.
42
+ bit_labels : np.ndarray
43
+ Natural-binary bit pattern per symbol index, shape ``(M, k)`` where
44
+ ``k = log2(M)``, dtype ``int8`` (MSB first).
45
+ modulation : str
46
+ Modulation scheme (``"psk"``, ``"qam"``, ``"ask"``, ``"pam"``).
47
+ order : int
48
+ Modulation order ``M``.
49
+ unipolar : bool
50
+ Whether the grid is the unipolar ASK/PAM variant.
51
+ pmf : np.ndarray or None
52
+ Optional Maxwell-Boltzmann shaping prior, shape ``(M,)``, aligned with
53
+ ``points``. ``None`` means uniform.
54
+ """
55
+
56
+ points: np.ndarray
57
+ bit_labels: np.ndarray
58
+ modulation: str
59
+ order: int
60
+ unipolar: bool = False
61
+ pmf: np.ndarray | None = None
62
+
63
+ @property
64
+ def bits_per_symbol(self) -> int:
65
+ """Number of bits per symbol, ``log2(order)``."""
66
+ return int(self.bit_labels.shape[1])
67
+
68
+ @classmethod
69
+ def gray(
70
+ cls,
71
+ modulation: str,
72
+ order: int,
73
+ *,
74
+ normalize: bool = True,
75
+ unipolar: bool = False,
76
+ pmf: np.ndarray | None = None,
77
+ ) -> "Constellation":
78
+ """Build a Gray-mapped constellation (cached for the ``pmf=None`` base).
79
+
80
+ Parameters mirror :func:`gray_constellation`. ``pmf`` attaches a
81
+ probabilistic-shaping prior without rebuilding the geometry.
82
+ """
83
+ base = _gray_base(modulation, order, normalize, unipolar)
84
+ if pmf is None:
85
+ return base
86
+ return replace(base, pmf=np.asarray(pmf, dtype=np.float64))
87
+
88
+ def power(self) -> float:
89
+ """pmf-weighted average symbol power ``E[|s|^2]`` (uniform if no pmf)."""
90
+ return constellation_power(self.points, self.pmf)
91
+
92
+ def map(self, bits: ArrayType) -> ArrayType:
93
+ """Map a flat bit sequence to symbols (see :func:`map_bits`)."""
94
+ return map_bits(bits, self.modulation, self.order, unipolar=self.unipolar)
95
+
96
+ def demap(self, symbols: ArrayType) -> ArrayType:
97
+ """Hard-decision demap symbols to bits (see :func:`demap_symbols_hard`).
98
+
99
+ Carries this constellation's ``pmf`` through for PS-QAM rescaling.
100
+ """
101
+ return demap_symbols_hard(
102
+ symbols,
103
+ self.modulation,
104
+ self.order,
105
+ unipolar=self.unipolar,
106
+ pmf=self.pmf,
107
+ )
108
+
109
+ def llr(
110
+ self,
111
+ symbols: ArrayType,
112
+ noise_var: float,
113
+ *,
114
+ method: str = "maxlog",
115
+ output: str = "jax",
116
+ ) -> ArrayType:
117
+ """Soft-decision LLRs (see :func:`compute_llr`), carrying this ``pmf``."""
118
+ return compute_llr(
119
+ symbols,
120
+ self.modulation,
121
+ self.order,
122
+ noise_var,
123
+ method=method,
124
+ unipolar=self.unipolar,
125
+ output=output,
126
+ pmf=self.pmf,
127
+ )
128
+
129
+
130
+ @lru_cache(maxsize=128)
131
+ def _gray_base(
132
+ modulation: str, order: int, normalize: bool, unipolar: bool
133
+ ) -> Constellation:
134
+ """Cached Gray constellation (geometry + labels), without a shaping pmf."""
135
+ points = gray_constellation(
136
+ modulation, order, normalize=normalize, unipolar=unipolar
137
+ )
138
+ k = int(np.log2(order))
139
+ bit_labels = (
140
+ (
141
+ np.arange(order, dtype="int32")[:, None]
142
+ >> np.arange(k - 1, -1, -1, dtype="int32")
143
+ )
144
+ & 1
145
+ ).astype(np.int8)
146
+ return Constellation(
147
+ points=points,
148
+ bit_labels=bit_labels,
149
+ modulation=modulation,
150
+ order=order,
151
+ unipolar=unipolar,
152
+ pmf=None,
153
+ )