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,388 @@
1
+ """Eye diagram 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 ..core.signal import Signal
10
+ from ..logger import logger
11
+ from .theme import _grid_figsize
12
+
13
+
14
+ def _plot_eye_traces(
15
+ samples: Any,
16
+ sps: float,
17
+ num_symbols: int,
18
+ ax: Any,
19
+ type: str,
20
+ title: str | None,
21
+ **kwargs: Any,
22
+ ) -> None:
23
+ """
24
+ Internal helper to plot eye diagram traces for a single signal component.
25
+
26
+ Parameters
27
+ ----------
28
+ samples : array_like
29
+ The component samples (e.g., Real or Imaginary part).
30
+ sps : float
31
+ Samples per symbol.
32
+ num_symbols : int
33
+ Number of symbol periods per window.
34
+ ax : matplotlib.axes.Axes
35
+ The axis to plot on.
36
+ type : {"line", "hist"}
37
+ Plotting strategy.
38
+ title : str, optional
39
+ Title for the subplot.
40
+ **kwargs : Any
41
+ Additional plotting parameters.
42
+ """
43
+ samples, xp, sp = dispatch(samples)
44
+
45
+ # Normalize to max amplitude 1.0
46
+ from ..helpers import normalize
47
+
48
+ samples = normalize(samples, mode="peak")
49
+
50
+ # We want to include the endpoint to avoid a gap at the end of the plot
51
+ # So we need one extra sample per trace
52
+ trace_len = int(num_symbols * sps) + 1
53
+ if trace_len > samples.shape[0]:
54
+ raise ValueError("Signal is shorter than the required trace length.")
55
+
56
+ # Calculate number of traces
57
+ # We slide by 1 symbol period (sps)
58
+ num_traces = (samples.shape[0] - trace_len) // int(sps) + 1
59
+
60
+ if type == "line":
61
+ # Limit traces for performance/visuals
62
+ max_traces = 5000
63
+ if num_traces > max_traces:
64
+ skip = num_traces // max_traces
65
+ indices = xp.arange(0, num_traces, skip)[:max_traces]
66
+ else:
67
+ indices = xp.arange(num_traces)
68
+
69
+ # Vectorized trace extraction
70
+ # indices shape: (num_traces,)
71
+ # offsets shape: (trace_len,)
72
+ # We want to extract samples at [indices[i] * sps + offset[j]]
73
+ start_indices = (indices * int(sps)).astype(int)
74
+ offsets = xp.arange(trace_len, dtype=int)
75
+
76
+ # Matrix of indices: (num_traces, 1) + (1, trace_len) -> (num_traces, trace_len)
77
+ gather_indices = start_indices[:, None] + offsets[None, :]
78
+
79
+ # Gather samples
80
+ traces = samples[gather_indices] # (num_traces, trace_len)
81
+
82
+ # Transpose for plotting
83
+ traces = traces.T # (trace_len, num_traces)
84
+
85
+ # Move to cpu for plotting
86
+ traces = to_device(traces, "cpu")
87
+
88
+ # Time axis in symbols
89
+ t = np.linspace(0, num_symbols, trace_len, endpoint=True)
90
+
91
+ line_kwargs = {"alpha": 0.2, "linewidth": 1}
92
+ line_kwargs.update(kwargs)
93
+
94
+ ax.plot(t, traces, color="C0", **line_kwargs)
95
+
96
+ elif type == "hist":
97
+ max_traces_hist = 20000
98
+ if num_traces > max_traces_hist:
99
+ skip = num_traces // max_traces_hist
100
+ indices = xp.arange(0, num_traces, skip)[:max_traces_hist]
101
+ else:
102
+ indices = xp.arange(num_traces)
103
+
104
+ start_indices = (indices * int(sps)).astype(int)
105
+ offsets = xp.arange(trace_len, dtype=int)
106
+ gather_indices = start_indices[:, None] + offsets[None, :]
107
+ traces = samples[gather_indices] # (num_traces, trace_len)
108
+
109
+ # Interpolate traces
110
+ target_width = 500
111
+ if trace_len < target_width:
112
+ x_old = xp.arange(trace_len, dtype=float)
113
+ x_new = xp.linspace(0, trace_len - 1, target_width, dtype=float)
114
+
115
+ traces = xp.stack([xp.interp(x_new, x_old, row) for row in traces])
116
+ trace_len = target_width
117
+
118
+ # Create time matrix
119
+ # Use xp.linspace
120
+ t = xp.linspace(0, num_symbols, trace_len, endpoint=True)
121
+ # Use xp.tile
122
+ t_matrix = xp.tile(t, (traces.shape[0], 1)) # shape: (num_traces, trace_len)
123
+
124
+ # Flatten
125
+ t_flat = t_matrix.flatten()
126
+ y_flat = traces.flatten()
127
+
128
+ # Compute 2D histogram
129
+ # Bins: Time (x) and Amplitude (y)
130
+ bins_x = trace_len
131
+ bins_y = 500
132
+
133
+ # Add padding to Y range
134
+ y_min, y_max = xp.min(y_flat), xp.max(y_flat)
135
+ y_range = y_max - y_min
136
+ if y_range == 0:
137
+ y_range = 1.0
138
+ y_pad = y_range * 0.1
139
+ range_y = [float(y_min - y_pad), float(y_max + y_pad)]
140
+
141
+ # Min/max of t_flat
142
+ t_min, t_max = xp.min(t_flat), xp.max(t_flat)
143
+
144
+ h, xedges, yedges = xp.histogram2d(
145
+ t_flat,
146
+ y_flat,
147
+ bins=[bins_x, bins_y],
148
+ range=[[float(t_min), float(t_max)], range_y],
149
+ )
150
+
151
+ h = h.T
152
+ h = sp.ndimage.gaussian_filter(h, sigma=1)
153
+
154
+ # Normalize
155
+ h_max = xp.max(h)
156
+ if h_max > 0:
157
+ h = h / h_max
158
+
159
+ # Move to cpu for plotting
160
+ h = to_device(h, "cpu")
161
+ xedges = to_device(xedges, "cpu")
162
+ yedges = to_device(yedges, "cpu")
163
+
164
+ # Plot using imshow
165
+ imshow_kwargs = {
166
+ "origin": "lower",
167
+ "extent": [xedges[0], xedges[-1], yedges[0], yedges[-1]],
168
+ "aspect": "auto",
169
+ "cmap": "inferno",
170
+ }
171
+ imshow_kwargs.update(kwargs)
172
+
173
+ ax.imshow(h, **imshow_kwargs) # type: ignore[arg-type]
174
+
175
+ else:
176
+ raise ValueError(f"Unknown type: {type}. Supported: 'line', 'hist'")
177
+
178
+ ax.set_xlabel("Time [Symbol Periods]")
179
+ ax.set_ylabel("Amplitude")
180
+ ax.set_xlim(0, num_symbols)
181
+ if title is not None:
182
+ ax.set_title(title)
183
+
184
+
185
+ def plot_eye_diagram(
186
+ samples: Any,
187
+ sps: float | None = None,
188
+ ax: Any | tuple[Any, Any] | None = None,
189
+ num_symbols: int = 2,
190
+ type: str = "hist",
191
+ title: str | None = "Eye Diagram",
192
+ vmin: float | None = None,
193
+ vmax: float | None = None,
194
+ show: bool = False,
195
+ **kwargs: Any,
196
+ ) -> tuple[Any, Any] | None:
197
+ """
198
+ Plots the eye diagram of the signal.
199
+
200
+ Visualizes signal quality by overlapping segments of the signal
201
+ synchronized to the symbol clock. Supports both fast trace-based
202
+ plotting and high-definition density histograms.
203
+
204
+ Parameters
205
+ ----------
206
+ samples : array_like or Signal
207
+ Input signal samples. Usually matched-filtered.
208
+ sps : float
209
+ Samples per symbol (must be an integer for windowing).
210
+ ax : matplotlib.axes.Axes or array_like, optional
211
+ Target axis or list of axes. For complex signals, two axes are
212
+ required per channel (I and Q).
213
+ num_symbols : int, default 2
214
+ Number of symbol periods TO display in each eye window.
215
+ type : {"hist", "line"}, default "hist"
216
+ Visualization mode:
217
+ - "hist": 2D density histogram (recommended for noisy signals).
218
+ - "line": Vectorized overlapping traces (classic look).
219
+ title : str, optional
220
+ Base title for the plot.
221
+ vmin, vmax : float, optional
222
+ Color scaling limits for "hist" mode.
223
+ show : bool, default False
224
+ If True, calls `plt.show()`.
225
+ **kwargs : Any
226
+ Additional keyword arguments passed to the plotting backend.
227
+
228
+ Returns
229
+ -------
230
+ fig : matplotlib.figure.Figure
231
+ The figure object.
232
+ ax : matplotlib.axes.Axes or ndarray
233
+ The axis or array of axes used.
234
+
235
+ Notes
236
+ -----
237
+ The signal should typically be synchronized (no CFO or timing offset)
238
+ and matched-filtered before plotting to produce a clear "eye".
239
+ """
240
+ if isinstance(samples, Signal):
241
+ sig = samples
242
+ return plot_eye_diagram(
243
+ sig.samples,
244
+ sps=sig.sps,
245
+ ax=ax,
246
+ type=type,
247
+ title=title,
248
+ vmin=vmin,
249
+ vmax=vmax,
250
+ show=show,
251
+ **kwargs,
252
+ )
253
+
254
+ if sps is None:
255
+ raise ValueError("plot_eye_diagram() requires sps for array input.")
256
+
257
+ logger.debug("Generating eye diagram (%s mode).", type)
258
+
259
+ if sps % 1 != 0:
260
+ raise ValueError("sps must be an integer")
261
+
262
+ # Dispatch to check backend
263
+ samples, xp, _ = dispatch(samples)
264
+
265
+ # Convention: (Channels, Time)
266
+ if samples.ndim > 1:
267
+ num_channels = samples.shape[0]
268
+
269
+ # Complex eye uses 2 axes (I/Q). So for N channels we need 2*N axes.
270
+ is_complex = xp.iscomplexobj(samples)
271
+ axes_per_channel = 2 if is_complex else 1
272
+
273
+ if ax is None:
274
+ # Grid: Rows = Channels, Cols = Components
275
+ fig, axes = plt.subplots(
276
+ num_channels,
277
+ axes_per_channel,
278
+ figsize=_grid_figsize(num_channels, axes_per_channel),
279
+ squeeze=False, # Ensure 2D array
280
+ )
281
+ else:
282
+ # User provided axes. Must be flat list or correct shape
283
+ # We assume user knows what they are doing or we do best effort
284
+ if isinstance(ax, (np.ndarray, list, tuple)):
285
+ # Flatten
286
+ axes_flat = np.array(ax).flatten()
287
+ if len(axes_flat) < num_channels * axes_per_channel:
288
+ raise ValueError(
289
+ f"Not enough axes provided. Need {num_channels * axes_per_channel}."
290
+ )
291
+ # Reshape to (Channels, Components)
292
+ axes = axes_flat[: num_channels * axes_per_channel].reshape(
293
+ num_channels, axes_per_channel
294
+ )
295
+ fig = axes[0, 0].figure
296
+ else:
297
+ raise ValueError(
298
+ "For multichannel eye diagram, you must provide a list of axes."
299
+ )
300
+
301
+ for i in range(num_channels):
302
+ channel_samples = samples[i]
303
+ ch_axes = axes[i]
304
+
305
+ ch_title = f"{title} (Ch {i})" if title else f"Channel {i}"
306
+
307
+ # Recursive call with 1D sample
308
+ plot_eye_diagram(
309
+ channel_samples,
310
+ sps=sps,
311
+ ax=ch_axes,
312
+ num_symbols=num_symbols,
313
+ type=type,
314
+ title=ch_title,
315
+ vmin=vmin,
316
+ vmax=vmax,
317
+ show=False,
318
+ **kwargs,
319
+ )
320
+
321
+ if show:
322
+ plt.show()
323
+ return None
324
+ return fig, axes
325
+
326
+ # --- 1D Logic ---
327
+
328
+ is_complex = xp.iscomplexobj(samples)
329
+
330
+ if ax is None:
331
+ if is_complex:
332
+ fig, ax = plt.subplots(1, 2, figsize=_grid_figsize(1, 2))
333
+ else:
334
+ fig, ax = plt.subplots(1, 1)
335
+ # Handle the fact that plt.subplots(1, 1) returns a single ax, not a list
336
+ else:
337
+ if isinstance(ax, (list, tuple, np.ndarray)):
338
+ fig = ax[0].figure
339
+ else:
340
+ fig = ax.figure
341
+
342
+ if is_complex:
343
+ if not isinstance(ax, (list, tuple, np.ndarray)) or len(ax) < 2:
344
+ raise ValueError(
345
+ "For complex signals, 'ax' must be a list/tuple of at least 2 axes."
346
+ )
347
+
348
+ # Plot I
349
+ _plot_eye_traces(
350
+ samples.real,
351
+ sps,
352
+ num_symbols,
353
+ ax[0],
354
+ type,
355
+ title=f"{title} (I)" if title else "I-Channel",
356
+ **kwargs,
357
+ )
358
+
359
+ # Plot Q
360
+ _plot_eye_traces(
361
+ samples.imag,
362
+ sps,
363
+ num_symbols,
364
+ ax[1],
365
+ type,
366
+ title=f"{title} (Q)" if title else "Q-Channel",
367
+ **kwargs,
368
+ )
369
+ else:
370
+ # If user passed a list of axes for real signal, use the first one
371
+ target_ax = ax
372
+ if isinstance(ax, (list, tuple, np.ndarray)):
373
+ target_ax = ax[0]
374
+
375
+ _plot_eye_traces(
376
+ samples,
377
+ sps,
378
+ num_symbols,
379
+ target_ax,
380
+ type,
381
+ title=title,
382
+ **kwargs,
383
+ )
384
+
385
+ if show:
386
+ plt.show()
387
+ return None
388
+ return fig, ax