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
commkit/plotting/eye.py
ADDED
|
@@ -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
|