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
commkit/__init__.py ADDED
@@ -0,0 +1,74 @@
1
+ """
2
+ `commkit` is a high-performance library for simulating and analyzing
3
+ digital communication systems. It provides a unified API for generating,
4
+ transforming, and assessing signals across diverse computational backends
5
+ (CPU, GPU, and JAX).
6
+
7
+ Main Features
8
+ -------------
9
+ - **Signal Abstractions**: Unified `Signal` and `SingleCarrierFrame` containers.
10
+ - **Modulation**: Support for PAM, PSK, and QAM (NRZ/RZ) with Gray coding.
11
+ - **Impairments**: Simulation of AWGN, Phase Noise, and Frequency Offset.
12
+ - **Synchronization**: Time and frequency synchronization algorithms.
13
+ - **Execution backends**: Transparent NumPy, CuPy, and JAX support.
14
+ """
15
+
16
+ import warnings
17
+
18
+ __version__ = "1.0.0"
19
+
20
+ # Leaf modules import ``Signal`` from ``.core.signal`` at top level for
21
+ # Signal/array dispatch. ``core.signal`` is a leaf (it imports no sibling
22
+ # domain modules), so the first leaf module imported below pulls ``core`` in
23
+ # without a cycle, regardless of statement order here.
24
+ from . import (
25
+ analysis,
26
+ equalization,
27
+ frequency,
28
+ impairments,
29
+ metrics,
30
+ recovery,
31
+ spectral,
32
+ timing,
33
+ )
34
+ from .core import (
35
+ Preamble,
36
+ Signal,
37
+ SingleCarrierFrame,
38
+ generate,
39
+ generate_pam,
40
+ generate_psk,
41
+ generate_psqam,
42
+ generate_qam,
43
+ )
44
+ from .io import load_npz, save_npz
45
+ from .logger import set_log_level
46
+ from .plotting import apply_default_theme
47
+
48
+ # Filter the specific warning message using a regular expression match
49
+ warnings.filterwarnings("ignore", message=".*cupyx.jit.rawkernel is experimental.*")
50
+
51
+ __all__ = [
52
+ "Preamble",
53
+ "Signal",
54
+ "SingleCarrierFrame",
55
+ "__version__",
56
+ "analysis",
57
+ "equalization",
58
+ "frequency",
59
+ "generate",
60
+ "generate_pam",
61
+ "generate_psk",
62
+ "generate_psqam",
63
+ "generate_qam",
64
+ "impairments",
65
+ "load_npz",
66
+ "metrics",
67
+ "recovery",
68
+ "save_npz",
69
+ "set_log_level",
70
+ "spectral",
71
+ "timing",
72
+ ]
73
+
74
+ apply_default_theme()
@@ -0,0 +1,321 @@
1
+ """
2
+ Hand-written CUDA kernel infrastructure for the CuPy backend.
3
+
4
+ This subpackage hosts raw CUDA C++ kernels (under ``src/``) together with the
5
+ lazy compilation machinery built on :class:`cupy.RawModule`. Its public
6
+ surface is two functions:
7
+
8
+ is_available :
9
+ True when CuPy is functional and a CUDA device with compute
10
+ capability >= 7.0 is present.
11
+ get_kernel :
12
+ Returns a launchable kernel wrapper, or ``None`` - in which case the
13
+ caller **must** fall back to its existing ``xp`` implementation.
14
+
15
+ Importing this package (or ``commkit``) never touches NVRTC: kernel
16
+ sources are read and compiled on the first ``get_kernel()`` call for a
17
+ given specialization, and cached in-process afterwards. Cross-process
18
+ reuse comes from CuPy's on-disk kernel cache (``~/.cupy/kernel_cache``).
19
+ """
20
+
21
+ from collections.abc import Callable
22
+ from functools import lru_cache
23
+ from typing import Any
24
+
25
+ from ..backend import is_cupy_available
26
+ from ..logger import logger
27
+
28
+ # Volta and newer. Older parts lack the independent-thread-scheduling and
29
+ # shared-memory sizes the kernels in src/ are written against.
30
+ _MIN_COMPUTE_CAPABILITY = 70
31
+
32
+ # Kernel names that have already produced a compile/load warning; the
33
+ # fallback contract promises at most one warning per process per kernel.
34
+ _warned_kernels: set[str] = set()
35
+
36
+
37
+ @lru_cache(maxsize=1)
38
+ def _device_supported() -> bool:
39
+ """Hardware probe, cached for the process lifetime.
40
+
41
+ Checks that at least one CUDA device is present and that the current
42
+ device has compute capability >= 7.0. Only called when CuPy itself is
43
+ importable and functional.
44
+ """
45
+ import cupy as cp
46
+
47
+ try:
48
+ if cp.cuda.runtime.getDeviceCount() < 1:
49
+ return False
50
+ cc = int(cp.cuda.Device().compute_capability)
51
+ except Exception:
52
+ return False
53
+ return cc >= _MIN_COMPUTE_CAPABILITY
54
+
55
+
56
+ def is_available() -> bool:
57
+ """Checks whether custom CUDA kernels can be compiled and launched.
58
+
59
+ Returns
60
+ -------
61
+ bool
62
+ True if CuPy is installed and functional (and not disabled via
63
+ ``backend.use_cpu_only``), at least one CUDA device is present,
64
+ and the current device has compute capability >= 7.0.
65
+ """
66
+ # The CuPy check is evaluated fresh on every call so that
67
+ # backend.use_cpu_only() is honored; only the hardware probe is cached.
68
+ if not is_cupy_available():
69
+ return False
70
+ return _device_supported()
71
+
72
+
73
+ def get_kernel(name: str, **spec: Any) -> Callable | None:
74
+ """Returns a launchable kernel wrapper, or ``None`` if unavailable.
75
+
76
+ ``None`` means the caller **must** fall back to the existing ``xp``
77
+ implementation. Any compile/load failure logs one warning per kernel
78
+ name per process and returns ``None``.
79
+
80
+ Parameters
81
+ ----------
82
+ name : str
83
+ Registered kernel name (see ``_KERNEL_FACTORIES``).
84
+ **spec
85
+ Kernel-specific specialization options (e.g. mode or dtype),
86
+ forwarded to the kernel's wrapper factory.
87
+
88
+ Returns
89
+ -------
90
+ callable or None
91
+ A wrapper that handles grid/block computation and input
92
+ validation. Call sites pass CuPy arrays and plain Python scalars
93
+ only. ``None`` when no usable GPU is present or compilation fails.
94
+
95
+ Raises
96
+ ------
97
+ KeyError
98
+ If `name` is not a registered kernel. Unknown names are
99
+ programming errors, not runtime fallback conditions.
100
+ """
101
+ factory = _KERNEL_FACTORIES.get(name)
102
+ if factory is None:
103
+ raise KeyError(
104
+ f"Unknown CUDA kernel {name!r}; registered: {sorted(_KERNEL_FACTORIES)}"
105
+ )
106
+ if not is_available():
107
+ return None
108
+ try:
109
+ return factory(**spec)
110
+ except Exception as exc:
111
+ if name not in _warned_kernels:
112
+ _warned_kernels.add(name)
113
+ logger.warning(
114
+ "CUDA kernel %r failed to compile/load (%s); "
115
+ "falling back to the array-module implementation.",
116
+ name,
117
+ exc,
118
+ )
119
+ return None
120
+
121
+
122
+ def _selftest_scale_factory(dtype: str = "float32") -> Callable:
123
+ """Wrapper factory for the infrastructure self-test kernel.
124
+
125
+ Validates the full compile -> specialize -> launch path; not used by
126
+ any DSP code.
127
+ """
128
+ import cupy as cp
129
+
130
+ from . import compiler
131
+
132
+ ctype = {"float32": "float", "float64": "double"}[dtype]
133
+ kern = compiler.get_raw_kernel("selftest", f"selftest_scale<{ctype}>")
134
+ np_dtype = cp.dtype(dtype)
135
+ block = 256
136
+
137
+ def launch(x: Any, alpha: float) -> Any:
138
+ x = cp.ascontiguousarray(x)
139
+ if x.dtype != np_dtype:
140
+ raise TypeError(f"expected {np_dtype} input, got {x.dtype}")
141
+ y = cp.empty_like(x)
142
+ n = x.size
143
+ grid = (n + block - 1) // block
144
+ kern((grid,), (block,), (x, y, np_dtype.type(alpha), cp.int64(n)))
145
+ return y
146
+
147
+ return launch
148
+
149
+
150
+ def _bps_min_d2_factory(mode: str = "table", return_argmin: bool = False) -> Callable:
151
+ """Wrapper factory for the fused BPS minimum-distance kernel.
152
+
153
+ Computes ``min_d2[p, c, n] = min_m |x[c, n] * phasor[p] - const[m]|**2``
154
+ in one pass. ``mode="table"`` searches an explicit constellation table;
155
+ ``mode="grid"`` snaps to the uniform square-QAM level grid. With
156
+ ``return_argmin=True`` (TABLE only) the nearest-point indices are
157
+ returned alongside the distances.
158
+ """
159
+ import cupy as cp
160
+
161
+ from . import compiler
162
+
163
+ mode_id = {"table": 0, "grid": 1}[mode]
164
+ if mode_id == 1 and return_argmin:
165
+ raise ValueError("return_argmin is only supported in TABLE mode")
166
+ kern = compiler.get_raw_kernel(
167
+ "bps_min_d2",
168
+ f"bps_min_d2<{mode_id}, {'true' if return_argmin else 'false'}>",
169
+ )
170
+ block_n = 128
171
+ _empty_i32 = cp.empty(0, dtype=cp.int32)
172
+
173
+ def launch(
174
+ x: Any,
175
+ phasor: Any,
176
+ constellation: Any = None,
177
+ lev_min: float = 0.0,
178
+ d_grid: float = 1.0,
179
+ side: int = 0,
180
+ ) -> Any:
181
+ """Returns min_d2 (P, C, N) float32; with argmin, an (min_d2, idx) tuple.
182
+
183
+ `x` is (C, N) complex64 with time on the last axis; `phasor` is
184
+ (P,) complex64, P <= 128. TABLE mode requires `constellation`
185
+ ((M,) complex64, M <= 1024); GRID mode requires the level-grid
186
+ scalars `lev_min`, `d_grid`, `side`.
187
+ """
188
+ x = cp.ascontiguousarray(x)
189
+ phasor = cp.ascontiguousarray(phasor)
190
+ if x.ndim != 2:
191
+ raise ValueError(f"x must be 2-D (C, N), got shape {x.shape}")
192
+ if x.dtype != cp.complex64 or phasor.dtype != cp.complex64:
193
+ raise TypeError(
194
+ f"x and phasor must be complex64, got {x.dtype}/{phasor.dtype}"
195
+ )
196
+ C, N = x.shape
197
+ P = int(phasor.size)
198
+ if P < 1 or P > 128:
199
+ raise ValueError(f"phasor count must be in [1, 128], got {P}")
200
+
201
+ if mode_id == 0:
202
+ constellation = cp.ascontiguousarray(constellation)
203
+ if constellation.dtype != cp.complex64:
204
+ raise TypeError(
205
+ f"constellation must be complex64, got {constellation.dtype}"
206
+ )
207
+ M = int(constellation.size)
208
+ if M < 1 or M > 1024:
209
+ raise ValueError(f"constellation size must be in [1, 1024], got {M}")
210
+ shared_mem = M * 8
211
+ else:
212
+ if side < 2:
213
+ raise ValueError(f"GRID mode requires side >= 2, got {side}")
214
+ constellation = _empty_i32 # never dereferenced (M == 0)
215
+ M = 0
216
+ shared_mem = 0
217
+
218
+ min_d2 = cp.empty((P, C, N), dtype=cp.float32)
219
+ argmin = cp.empty((P, C, N), dtype=cp.int32) if return_argmin else _empty_i32
220
+ grid = ((N + block_n - 1) // block_n, P, C)
221
+ kern(
222
+ grid,
223
+ (block_n,),
224
+ (
225
+ x,
226
+ phasor,
227
+ constellation,
228
+ cp.float32(lev_min),
229
+ cp.float32(d_grid),
230
+ cp.int32(side),
231
+ cp.int32(M),
232
+ cp.int64(N),
233
+ min_d2,
234
+ argmin,
235
+ ),
236
+ shared_mem=shared_mem,
237
+ )
238
+ if return_argmin:
239
+ return min_d2, argmin
240
+ return min_d2
241
+
242
+ return launch
243
+
244
+
245
+ def _cs_block_factory() -> Callable:
246
+ """Wrapper factory for the block_lms cycle-slip correction kernel.
247
+
248
+ Sequential per-channel slip detector (one block, one thread per channel)
249
+ operating in-place on device-resident state buffers. One launch processes
250
+ one equalizer block, replacing the per-block D2H -> CPU Numba -> H2D
251
+ round trip of the fallback path. All state arrays are float64/int64 and
252
+ are mutated in place - the wrapper therefore rejects non-contiguous
253
+ inputs instead of silently copying them.
254
+ """
255
+ import cupy as cp
256
+
257
+ from . import compiler
258
+
259
+ kern = compiler.get_raw_kernel("cs_block", "cs_block")
260
+
261
+ def launch(
262
+ phi_blk: Any,
263
+ phi_corr: Any,
264
+ cs_buf_y: Any,
265
+ cs_buf_ptr: Any,
266
+ cs_buf_n: Any,
267
+ cs_stats: Any,
268
+ quantum: float,
269
+ threshold: float,
270
+ cs_H: int,
271
+ ) -> Any:
272
+ """Corrects phi_blk into phi_corr ((C, B) float64), updating the
273
+ per-channel circular history buffers in place."""
274
+ if phi_blk.ndim != 2:
275
+ raise ValueError(f"phi_blk must be 2-D (C, B), got shape {phi_blk.shape}")
276
+ C, B = phi_blk.shape
277
+ if C > 1024:
278
+ raise ValueError(f"channel count must be <= 1024, got {C}")
279
+ cs_H = int(cs_H)
280
+ for label, arr, dtype, shape in (
281
+ ("phi_blk", phi_blk, cp.float64, (C, B)),
282
+ ("phi_corr", phi_corr, cp.float64, (C, B)),
283
+ ("cs_buf_y", cs_buf_y, cp.float64, (C, cs_H)),
284
+ ("cs_buf_ptr", cs_buf_ptr, cp.int64, (C,)),
285
+ ("cs_buf_n", cs_buf_n, cp.int64, (C,)),
286
+ ("cs_stats", cs_stats, cp.float64, (C, 4)),
287
+ ):
288
+ if arr.dtype != dtype:
289
+ raise TypeError(f"{label} must be {dtype}, got {arr.dtype}")
290
+ if arr.shape != shape:
291
+ raise ValueError(f"{label} must have shape {shape}, got {arr.shape}")
292
+ if not arr.flags.c_contiguous:
293
+ raise ValueError(f"{label} must be C-contiguous (mutated in place)")
294
+ kern(
295
+ (1,),
296
+ (C,),
297
+ (
298
+ phi_blk,
299
+ phi_corr,
300
+ cs_buf_y,
301
+ cs_buf_ptr,
302
+ cs_buf_n,
303
+ cs_stats,
304
+ cp.float64(quantum),
305
+ cp.float64(threshold),
306
+ cp.int32(cs_H),
307
+ cp.int32(B),
308
+ ),
309
+ )
310
+ return phi_corr
311
+
312
+ return launch
313
+
314
+
315
+ # name -> wrapper factory. Factories may raise; get_kernel translates any
316
+ # failure into the warn-once-and-return-None fallback contract.
317
+ _KERNEL_FACTORIES: dict[str, Callable[..., Callable]] = {
318
+ "selftest_scale": _selftest_scale_factory,
319
+ "bps_min_d2": _bps_min_d2_factory,
320
+ "cs_block": _cs_block_factory,
321
+ }
@@ -0,0 +1,88 @@
1
+ """
2
+ Lazy CUDA kernel compilation with in-process caching.
3
+
4
+ Kernel sources are real ``.cu`` files under ``commkit/_cuda/src/``
5
+ (shipped as package data) and are compiled via :class:`cupy.RawModule`
6
+ with C++17 and templates for mode/dtype specialization. ``name_expressions``
7
+ provides mangled-name resolution for the template instantiations.
8
+
9
+ Compilation is lazy: nothing here imports CuPy or touches NVRTC until
10
+ ``get_raw_kernel()`` is called. Compiled kernels are cached per
11
+ ``(source_name, options, name_expression)`` for the process lifetime;
12
+ cross-process reuse comes from CuPy's on-disk NVRTC cache.
13
+ """
14
+
15
+ from importlib import resources
16
+ from typing import Any
17
+
18
+ DEFAULT_OPTIONS: tuple[str, ...] = ("-std=c++17", "--use_fast_math")
19
+
20
+ _SOURCE_CACHE: dict[str, str] = {}
21
+ # (source_name, options, name_expression) -> (RawModule, RawKernel).
22
+ # The module is cached alongside the kernel to keep the loaded CUmodule
23
+ # alive for the process lifetime.
24
+ _KERNEL_CACHE: dict[tuple[str, tuple[str, ...], str], tuple[Any, Any]] = {}
25
+
26
+
27
+ def read_source(source_name: str) -> str:
28
+ """Loads a CUDA C++ source file from the package's ``src/`` directory.
29
+
30
+ Parameters
31
+ ----------
32
+ source_name : str
33
+ Base name of the source file, without the ``.cu`` extension
34
+ (e.g. ``"bps_min_d2"``).
35
+
36
+ Returns
37
+ -------
38
+ str
39
+ The file contents.
40
+ """
41
+ src = _SOURCE_CACHE.get(source_name)
42
+ if src is None:
43
+ src = (
44
+ resources.files("commkit._cuda")
45
+ .joinpath(f"src/{source_name}.cu")
46
+ .read_text(encoding="utf-8")
47
+ )
48
+ _SOURCE_CACHE[source_name] = src
49
+ return src
50
+
51
+
52
+ def get_raw_kernel(
53
+ source_name: str,
54
+ name_expression: str,
55
+ options: tuple[str, ...] = DEFAULT_OPTIONS,
56
+ ) -> Any:
57
+ """Compiles (or retrieves from cache) one kernel specialization.
58
+
59
+ Parameters
60
+ ----------
61
+ source_name : str
62
+ Base name of the ``.cu`` file under ``src/``.
63
+ name_expression : str
64
+ C++ name expression for the kernel instantiation,
65
+ e.g. ``"bps_min_d2<TABLE>"``.
66
+ options : tuple of str, default ``DEFAULT_OPTIONS``
67
+ NVRTC compile options.
68
+
69
+ Returns
70
+ -------
71
+ cupy.RawKernel
72
+ The launchable kernel for `name_expression`.
73
+ """
74
+ key = (source_name, options, name_expression)
75
+ cached = _KERNEL_CACHE.get(key)
76
+ if cached is not None:
77
+ return cached[1]
78
+
79
+ import cupy as cp
80
+
81
+ module = cp.RawModule(
82
+ code=read_source(source_name),
83
+ options=options,
84
+ name_expressions=[name_expression],
85
+ )
86
+ kernel = module.get_function(name_expression)
87
+ _KERNEL_CACHE[key] = (module, kernel)
88
+ return kernel
@@ -0,0 +1,104 @@
1
+ // Fused minimum-squared-distance kernel for Blind Phase Search (BPS) and
2
+ // decision-directed slicing.
3
+ //
4
+ // Computes, in a single pass over the input symbols:
5
+ //
6
+ // min_d2[p, c, n] = min_m | x[c, n] * phasor[p] - const[m] |^2
7
+ //
8
+ // replacing the materialized (.., P/B, M) candidate-distance tensors of the
9
+ // array-module implementations. The input symbols are read once and only the
10
+ // (P, C, N) float32 minima (plus optional int32 argmin indices) are written,
11
+ // removing the elementwise-chain DRAM traffic that dominates the xp path.
12
+ //
13
+ // Template modes
14
+ // MODE = MODE_TABLE : general constellation search over const[0..M-1],
15
+ // cooperatively staged in shared memory.
16
+ // MODE = MODE_GRID : square-QAM O(1) nearest point by per-component
17
+ // rounding onto the uniform level grid
18
+ // lev_min + k * d_grid, k in [0, side-1].
19
+ // RETURN_ARGMIN : additionally writes the index of the nearest point
20
+ // (TABLE: table index m; GRID: re_idx * side + im_idx).
21
+ //
22
+ // Layout contract (enforced by the Python wrapper):
23
+ // x (C, N) complex64, C-contiguous, time on the last axis
24
+ // phasor (P,) complex64, P <= 128
25
+ // constel (M,) complex64, M <= 1024 (TABLE mode only)
26
+ // min_d2 (P, C, N) float32
27
+ // argmin (P, C, N) int32 (RETURN_ARGMIN only)
28
+ //
29
+ // Launch contract: block = (BLOCK_N, 1, 1); grid = (ceil(N/BLOCK_N), P, C);
30
+ // dynamic shared memory = M * sizeof(complex<float>) in TABLE mode, 0 in GRID.
31
+ // Arithmetic is FP32 throughout, matching the float32 metric of the xp path.
32
+
33
+ #include <cupy/complex.cuh>
34
+
35
+ #define MODE_TABLE 0
36
+ #define MODE_GRID 1
37
+
38
+ template <int MODE, bool RETURN_ARGMIN>
39
+ __global__ void bps_min_d2(const complex<float>* __restrict__ x,
40
+ const complex<float>* __restrict__ phasor,
41
+ const complex<float>* __restrict__ constel,
42
+ const float lev_min,
43
+ const float d_grid,
44
+ const int side,
45
+ const int M,
46
+ const long long N,
47
+ float* __restrict__ min_d2,
48
+ int* __restrict__ argmin_out) {
49
+ extern __shared__ complex<float> s_const[];
50
+
51
+ const int p = blockIdx.y;
52
+ const int c = blockIdx.z;
53
+
54
+ if (MODE == MODE_TABLE) {
55
+ for (int m = threadIdx.x; m < M; m += blockDim.x) {
56
+ s_const[m] = constel[m];
57
+ }
58
+ __syncthreads();
59
+ }
60
+
61
+ const long long n =
62
+ static_cast<long long>(blockIdx.x) * blockDim.x + threadIdx.x;
63
+ if (n >= N) {
64
+ return;
65
+ }
66
+
67
+ // Coalesced in n; the per-block phasor load is an L1-cached broadcast.
68
+ const complex<float> xr = x[static_cast<long long>(c) * N + n] * phasor[p];
69
+ const float re = xr.real();
70
+ const float im = xr.imag();
71
+
72
+ float best = 3.402823466e+38f; // FLT_MAX
73
+ int best_m = 0;
74
+
75
+ if (MODE == MODE_TABLE) {
76
+ // Warp-uniform m => shared-memory broadcast, no bank conflicts.
77
+ for (int m = 0; m < M; ++m) {
78
+ const float dr = re - s_const[m].real();
79
+ const float di = im - s_const[m].imag();
80
+ const float d2 = fmaf(dr, dr, di * di);
81
+ if (d2 < best) {
82
+ best = d2;
83
+ best_m = m;
84
+ }
85
+ }
86
+ } else { // MODE_GRID
87
+ const float hi = static_cast<float>(side - 1);
88
+ const float ri = fminf(fmaxf(roundf((re - lev_min) / d_grid), 0.0f), hi);
89
+ const float ii = fminf(fmaxf(roundf((im - lev_min) / d_grid), 0.0f), hi);
90
+ const float dr = re - fmaf(ri, d_grid, lev_min);
91
+ const float di = im - fmaf(ii, d_grid, lev_min);
92
+ best = fmaf(dr, dr, di * di);
93
+ if (RETURN_ARGMIN) {
94
+ best_m = static_cast<int>(ri) * side + static_cast<int>(ii);
95
+ }
96
+ }
97
+
98
+ const long long out_idx =
99
+ (static_cast<long long>(p) * gridDim.z + c) * N + n;
100
+ min_d2[out_idx] = best;
101
+ if (RETURN_ARGMIN) {
102
+ argmin_out[out_idx] = best_m;
103
+ }
104
+ }