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,390 @@
1
+ """Filter and equalizer response plots."""
2
+
3
+ from typing import Any
4
+
5
+ import matplotlib.pyplot as plt
6
+ import numpy as np
7
+
8
+ from ..backend import dispatch, to_device
9
+ from ..logger import logger
10
+ from .theme import _grid_figsize
11
+
12
+
13
+ def plot_filter_response(
14
+ taps: Any, sps: float = 1.0, ax: Any | None = None, show: bool = False
15
+ ) -> tuple[Any, tuple[Any, Any, Any]] | None:
16
+ """
17
+ Plots the impulse and frequency response of a filter.
18
+
19
+ Provides a 3-panel analysis showing the filter taps in the time domain,
20
+ the magnitude response in dB, and the unwrapped phase response.
21
+
22
+ Parameters
23
+ ----------
24
+ taps : array_like
25
+ Filter taps (impulse response).
26
+ sps : float, default 1.0
27
+ Samples per symbol for time-axis normalization.
28
+ ax : array_like, optional
29
+ A list or tuple of 3 axes to plot on.
30
+ show : bool, default False
31
+ If True, calls `plt.show()`.
32
+
33
+ Returns
34
+ -------
35
+ fig : matplotlib.figure.Figure
36
+ The figure object.
37
+ axes : tuple of matplotlib.axes.Axes
38
+ The (impulse, magnitude, phase) axes.
39
+ """
40
+
41
+ import matplotlib.ticker as ticker
42
+
43
+ # Dispatch
44
+ taps, xp, sp = dispatch(taps)
45
+
46
+ if ax is None:
47
+ logger.debug("Generating filter response plot.")
48
+ fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=_grid_figsize(3, 1))
49
+ elif isinstance(ax, (list, tuple, np.ndarray)) and len(ax) == 3:
50
+ fig = ax[0].figure
51
+ ax1, ax2, ax3 = ax
52
+ else:
53
+ logger.warning("filter_response requires 3 axes. Creating new figure.")
54
+ fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=_grid_figsize(1, 3))
55
+
56
+ # 1. Impulse Response
57
+ num_taps = len(taps)
58
+
59
+ t = (xp.arange(num_taps) - (num_taps - 1) / 2) / sps
60
+
61
+ # Move to cpu for plotting
62
+ t_cpu = to_device(t, "cpu")
63
+ taps_cpu = to_device(taps, "cpu")
64
+
65
+ if xp.iscomplexobj(taps):
66
+ ax1.plot(t_cpu, taps_cpu.real, label="Real", color="C0")
67
+ ax1.plot(t_cpu, taps_cpu.imag, label="Imag", color="C1")
68
+ ax1.legend()
69
+ else:
70
+ ax1.plot(t_cpu, taps_cpu, color="C0")
71
+
72
+ ax1.set_title("Impulse Response")
73
+ ax1.set_xlabel("Time [Symbol Periods]")
74
+ ax1.set_ylabel("Amplitude")
75
+
76
+ # Set ticks at integer intervals (1T, 2T, etc.)
77
+ ax1.xaxis.set_major_locator(ticker.MultipleLocator(1))
78
+
79
+ def t_formatter(x, pos):
80
+ if np.isclose(x, 0):
81
+ return "0"
82
+ return f"{int(x)}T" if float(x).is_integer() else f"{x}T"
83
+
84
+ ax1.xaxis.set_major_formatter(ticker.FuncFormatter(t_formatter))
85
+
86
+ # 2. Frequency Response
87
+ # Compute frequency response using backend sp
88
+ w, h = sp.signal.freqz(taps, worN=2048)
89
+
90
+ # Normalize to Nyquist (0 to 1)
91
+ # w is in radians/sample (0 to pi)
92
+ freqs = w / (2 * xp.pi)
93
+
94
+ # Avoid log(0)
95
+ mag = 20 * xp.log10(xp.abs(h) + 1e-12)
96
+ angles = xp.unwrap(xp.angle(h))
97
+
98
+ # Move to cpu
99
+ freqs = to_device(freqs, "cpu")
100
+ mag = to_device(mag, "cpu")
101
+ angles = to_device(angles, "cpu")
102
+
103
+ # Magnitude
104
+ ax2.plot(freqs, mag, color="C2")
105
+ ax2.set_ylabel("Magnitude [dB]")
106
+ ax2.set_title("Frequency Response (Magnitude)")
107
+ ax2.set_xlabel("Frequency [Cycles/Sample]")
108
+ ax2.set_xlim(0, 0.5)
109
+
110
+ # Phase
111
+ ax3.plot(freqs, angles, color="C3")
112
+ ax3.set_ylabel("Phase [rad]")
113
+ ax3.set_title("Frequency Response (Phase)")
114
+ ax3.set_xlabel("Frequency [Cycles/Sample]")
115
+ ax3.set_xlim(0, 0.5)
116
+
117
+ if show:
118
+ plt.show()
119
+ return None
120
+ return fig, (ax1, ax2, ax3)
121
+
122
+
123
+ def plot_equalizer_result(
124
+ result,
125
+ smoothing: int = 50,
126
+ ax=None,
127
+ show: bool = False,
128
+ ) -> tuple[Any, Any] | None:
129
+ """
130
+ Plots adaptive equalizer diagnostics: convergence curve, tap weights, and
131
+ (when CPR was enabled) the recovered phase trajectory.
132
+
133
+ Parameters
134
+ ----------
135
+ result : EqualizerResult
136
+ Equalizer output containing ``error``, ``weights``, and optionally
137
+ ``phase_trajectory`` fields.
138
+ smoothing : int, default 50
139
+ Moving-average window length for the MSE convergence curve.
140
+ ax : list of Axes, optional
141
+ Pre-existing axes. Pass 2 axes when ``phase_trajectory`` is None, or
142
+ 3 axes when CPR was used (``[ax_convergence, ax_taps, ax_phase]``).
143
+ If None, a new figure is created with the appropriate number of panels.
144
+ show : bool, default False
145
+ If True, calls ``plt.show()`` and returns None.
146
+
147
+ Returns
148
+ -------
149
+ (fig, axes) or None
150
+ Figure and axes array when ``show=False``, None otherwise.
151
+ """
152
+ error = to_device(result.error, "cpu")
153
+ weights = to_device(result.weights, "cpu")
154
+ phase_traj = getattr(result, "phase_trajectory", None)
155
+ if phase_traj is not None:
156
+ phase_traj = to_device(phase_traj, "cpu")
157
+
158
+ is_mimo = error.ndim == 2
159
+ n_panels = 3 if phase_traj is not None else 2
160
+
161
+ if ax is None:
162
+ fig, axes = plt.subplots(1, n_panels, figsize=_grid_figsize(1, n_panels))
163
+ else:
164
+ axes = np.asarray(ax).flatten()[:n_panels]
165
+ fig = axes[0].figure
166
+
167
+ # --- Panel 1: MSE convergence ---
168
+ ax_conv = axes[0]
169
+
170
+ def _smooth_mse(mse, smoothing):
171
+ """Return (x_coords, smoothed_mse_db) in raw-symbol-index space.
172
+
173
+ The smoothing window is clipped to at most n//3 so the curve always
174
+ spans at least 2/3 of the data range even for short sequences.
175
+ """
176
+ n = len(mse)
177
+ effective = max(1, min(smoothing, n // 3))
178
+ if effective > 1 and n > effective:
179
+ kernel = np.ones(effective) / effective
180
+ mse_smooth = np.convolve(mse, kernel, mode="valid")
181
+ # Element k of mode="valid" output averages mse[k : k+effective].
182
+ # Place it at the centre of that window so the x-axis is in
183
+ # actual symbol-index space, not smoothed-bin-index space.
184
+ x = np.arange(len(mse_smooth)) + (effective - 1) / 2.0
185
+ else:
186
+ mse_smooth = mse
187
+ x = np.arange(n)
188
+ return x, 10 * np.log10(mse_smooth + 1e-30)
189
+
190
+ if is_mimo:
191
+ num_ch = error.shape[0]
192
+ for ch in range(num_ch):
193
+ mse = np.abs(error[ch]) ** 2
194
+ x_smooth, mse_db = _smooth_mse(mse, smoothing)
195
+ ax_conv.plot(x_smooth, mse_db, label=f"Ch {ch}")
196
+ ax_conv.legend()
197
+ else:
198
+ mse = np.abs(error) ** 2
199
+ x_smooth, mse_db = _smooth_mse(mse, smoothing)
200
+ ax_conv.plot(x_smooth, mse_db)
201
+
202
+ n_train = getattr(result, "num_train_symbols", 0)
203
+ if n_train and n_train > 0:
204
+ ax_conv.axvline(
205
+ n_train,
206
+ color="white",
207
+ linestyle="--",
208
+ label=f"DD start ({n_train})",
209
+ )
210
+ ax_conv.legend()
211
+
212
+ ax_conv.set_xlabel("Symbol Index")
213
+ ax_conv.set_ylabel("MSE [dB]")
214
+ ax_conv.set_title("Convergence")
215
+
216
+ # --- Panel 2: Final tap weights ---
217
+ ax_taps = axes[1]
218
+
219
+ if is_mimo:
220
+ # Butterfly weights: (C, C, num_taps) - plot magnitude of each row
221
+ num_ch = weights.shape[0]
222
+ num_taps = weights.shape[2]
223
+ tap_idx = np.arange(num_taps) - (num_taps // 2)
224
+ for i in range(num_ch):
225
+ for j in range(num_ch):
226
+ label = f"w[{i},{j}]"
227
+ ax_taps.plot(
228
+ tap_idx,
229
+ np.abs(weights[i, j]),
230
+ marker="o",
231
+ markersize=3,
232
+ label=label,
233
+ )
234
+ ax_taps.legend(ncol=2)
235
+ else:
236
+ # SISO: (num_taps,) - stem plot
237
+ num_taps = weights.shape[0]
238
+ tap_idx = np.arange(num_taps) - (num_taps // 2)
239
+ markerline, stemlines, _ = ax_taps.stem(tap_idx, np.abs(weights))
240
+ plt.setp(stemlines)
241
+ plt.setp(markerline, markersize=4)
242
+
243
+ ax_taps.set_xlabel("Tap Index")
244
+ ax_taps.set_ylabel("|w|")
245
+ ax_taps.set_title("Tap Weights")
246
+
247
+ # --- Panel 3: Phase trajectory (CPR only) ---
248
+ if phase_traj is not None:
249
+ ax_phase = axes[2]
250
+ phi = np.asarray(phase_traj)
251
+ if phi.ndim == 1:
252
+ ax_phase.plot(np.degrees(phi))
253
+ else:
254
+ for ch in range(phi.shape[0]):
255
+ ax_phase.plot(np.degrees(phi[ch]), label=f"Ch {ch}")
256
+ ax_phase.legend()
257
+ ax_phase.set_xlabel("Symbol Index")
258
+ ax_phase.set_ylabel("Phase [deg]")
259
+ ax_phase.set_title("CPR Phase Trajectory")
260
+
261
+ if show:
262
+ plt.show()
263
+ return None
264
+ return fig, axes
265
+
266
+
267
+ # -----------------------------------------------------------------------------
268
+ # SYNCHRONIZATION DIAGNOSTICS
269
+ # -----------------------------------------------------------------------------
270
+
271
+
272
+ def plot_zf_equalizer_response(
273
+ channel_estimate,
274
+ noise_variance: float = 0.0,
275
+ nfft: int = 1024,
276
+ sampling_rate: float = 1.0,
277
+ ax=None,
278
+ show: bool = False,
279
+ title: str = "ZF/MMSE Equalizer Response",
280
+ ) -> tuple[Any, Any] | None:
281
+ """
282
+ Plots channel, equalizer, and combined frequency responses for ZF/MMSE.
283
+
284
+ For each channel path, three panels show:
285
+
286
+ * ``|H(f)|`` - channel response in dB.
287
+ * ``|W(f)|`` - equalizer response (``W = H* / (|H|² + σ²)``) in dB.
288
+ * ``|H(f)·W(f)|`` - combined response (≈ 0 dB for ZF, soft mask for MMSE).
289
+
290
+ Parameters
291
+ ----------
292
+ channel_estimate : array_like
293
+ Channel impulse response. Shape: ``(L,)`` for SISO or
294
+ ``(C_rx, C_tx, L)`` for MIMO.
295
+ noise_variance : float, default 0.0
296
+ Noise variance ``σ²`` for MMSE regularization. ``0.0`` gives pure ZF.
297
+ nfft : int, default 1024
298
+ FFT size for the frequency response computation.
299
+ sampling_rate : float, default 1.0
300
+ Sampling rate in Hz for frequency-axis scaling.
301
+ ax : array_like of Axes, optional
302
+ For SISO: 3 Axes. For MIMO: ``(C_rx * C_tx, 3)`` Axes.
303
+ If ``None``, a new figure is created.
304
+ show : bool, default False
305
+ title : str, default "ZF/MMSE Equalizer Response"
306
+
307
+ Returns
308
+ -------
309
+ (fig, axes) or None
310
+ """
311
+ h = to_device(channel_estimate, "cpu")
312
+ reg = max(noise_variance, 1e-12)
313
+ siso = h.ndim == 1
314
+
315
+ freqs = np.fft.fftfreq(nfft, d=1.0 / sampling_rate)
316
+ sort_idx = np.argsort(freqs)
317
+ f_sorted = freqs[sort_idx]
318
+
319
+ max_f = float(np.max(np.abs(f_sorted)))
320
+ if max_f >= 1e9:
321
+ scale, unit = 1e9, "GHz"
322
+ elif max_f >= 1e6:
323
+ scale, unit = 1e6, "MHz"
324
+ elif max_f >= 1e3:
325
+ scale, unit = 1e3, "kHz"
326
+ else:
327
+ scale, unit = 1.0, "Hz"
328
+ f_disp = f_sorted / scale
329
+
330
+ def _draw_triplet(H_f, ax_row, row_label=""):
331
+ H_s = H_f[sort_idx]
332
+ W_s = np.conj(H_s) / (np.abs(H_s) ** 2 + reg)
333
+ HW_s = H_s * W_s
334
+ for axi, data, lbl, col in zip(
335
+ ax_row,
336
+ [H_s, W_s, HW_s],
337
+ ["Channel |H(f)|", "Equalizer |W(f)|", "Combined |H·W(f)|"],
338
+ ["C0", "C1", "C2"],
339
+ ):
340
+ axi.plot(f_disp, 20 * np.log10(np.abs(data) + 1e-12), color=col)
341
+ prefix = f"{row_label} " if row_label else ""
342
+ axi.set_title(f"{prefix}{lbl}")
343
+ axi.set_xlabel(f"Frequency [{unit}]")
344
+ axi.set_ylabel("Magnitude [dB]")
345
+
346
+ if siso:
347
+ H_f = np.fft.fft(h, n=nfft)
348
+ if ax is None:
349
+ fig, axes = plt.subplots(1, 3, figsize=_grid_figsize(1, 3))
350
+ else:
351
+ axes = list(ax) if hasattr(ax, "__len__") else [ax]
352
+ fig = axes[0].figure
353
+ _draw_triplet(H_f, axes)
354
+ fig.suptitle(title)
355
+ if show:
356
+ plt.show()
357
+ return None
358
+ return fig, axes
359
+
360
+ C_rx, C_tx = h.shape[0], h.shape[1]
361
+ n_rows = C_rx * C_tx
362
+ if ax is None:
363
+ fig, raw_axes = plt.subplots(
364
+ n_rows, 3, figsize=_grid_figsize(n_rows, 3), squeeze=False
365
+ )
366
+ axes = [list(raw_axes[r]) for r in range(n_rows)]
367
+ else:
368
+ if hasattr(ax, "__len__") and hasattr(ax[0], "__len__"):
369
+ axes = [list(row) for row in ax]
370
+ else:
371
+ flat = list(ax)
372
+ axes = [flat[r * 3 : (r + 1) * 3] for r in range(n_rows)]
373
+ fig = axes[0][0].figure
374
+
375
+ for i in range(C_rx):
376
+ for j in range(C_tx):
377
+ row = i * C_tx + j
378
+ H_f = np.fft.fft(h[i, j], n=nfft)
379
+ _draw_triplet(H_f, axes[row], row_label=f"[rx{i}$\\rightarrow$tx{j}]")
380
+
381
+ fig.suptitle(title)
382
+ if show:
383
+ plt.show()
384
+ return None
385
+ return fig, axes
386
+
387
+
388
+ # -----------------------------------------------------------------------------
389
+ # CARRIER-PHASE CHARACTERIZATION DIAGNOSTICS
390
+ # -----------------------------------------------------------------------------