oscura 0.8.0__py3-none-any.whl → 0.11.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.
- oscura/__init__.py +19 -19
- oscura/__main__.py +4 -0
- oscura/analyzers/__init__.py +2 -0
- oscura/analyzers/digital/extraction.py +2 -3
- oscura/analyzers/digital/quality.py +1 -1
- oscura/analyzers/digital/timing.py +1 -1
- oscura/analyzers/ml/signal_classifier.py +6 -0
- oscura/analyzers/patterns/__init__.py +66 -0
- oscura/analyzers/power/basic.py +3 -3
- oscura/analyzers/power/soa.py +1 -1
- oscura/analyzers/power/switching.py +3 -3
- oscura/analyzers/signal_classification.py +529 -0
- oscura/analyzers/signal_integrity/sparams.py +3 -3
- oscura/analyzers/statistics/basic.py +10 -7
- oscura/analyzers/validation.py +1 -1
- oscura/analyzers/waveform/measurements.py +200 -156
- oscura/analyzers/waveform/measurements_with_uncertainty.py +91 -35
- oscura/analyzers/waveform/spectral.py +182 -84
- oscura/api/dsl/commands.py +15 -6
- oscura/api/server/templates/base.html +137 -146
- oscura/api/server/templates/export.html +84 -110
- oscura/api/server/templates/home.html +248 -267
- oscura/api/server/templates/protocols.html +44 -48
- oscura/api/server/templates/reports.html +27 -35
- oscura/api/server/templates/session_detail.html +68 -78
- oscura/api/server/templates/sessions.html +62 -72
- oscura/api/server/templates/waveforms.html +54 -64
- oscura/automotive/__init__.py +1 -1
- oscura/automotive/can/session.py +1 -1
- oscura/automotive/dbc/generator.py +638 -23
- oscura/automotive/dtc/data.json +17 -102
- oscura/automotive/flexray/fibex.py +9 -1
- oscura/automotive/uds/decoder.py +99 -6
- oscura/cli/analyze.py +8 -2
- oscura/cli/batch.py +36 -5
- oscura/cli/characterize.py +18 -4
- oscura/cli/export.py +47 -5
- oscura/cli/main.py +2 -0
- oscura/cli/onboarding/wizard.py +10 -6
- oscura/cli/pipeline.py +585 -0
- oscura/cli/visualize.py +6 -4
- oscura/convenience.py +400 -32
- oscura/core/measurement_result.py +286 -0
- oscura/core/progress.py +1 -1
- oscura/core/schemas/device_mapping.json +2 -8
- oscura/core/schemas/packet_format.json +4 -24
- oscura/core/schemas/protocol_definition.json +2 -12
- oscura/core/types.py +232 -239
- oscura/correlation/multi_protocol.py +1 -1
- oscura/export/legacy/__init__.py +11 -0
- oscura/export/legacy/wav.py +75 -0
- oscura/exporters/__init__.py +19 -0
- oscura/exporters/wireshark.py +809 -0
- oscura/hardware/acquisition/file.py +5 -19
- oscura/hardware/acquisition/saleae.py +10 -10
- oscura/hardware/acquisition/socketcan.py +4 -6
- oscura/hardware/acquisition/synthetic.py +1 -5
- oscura/hardware/acquisition/visa.py +6 -6
- oscura/hardware/security/side_channel_detector.py +5 -508
- oscura/inference/message_format.py +686 -1
- oscura/jupyter/display.py +2 -2
- oscura/jupyter/magic.py +3 -3
- oscura/loaders/__init__.py +17 -12
- oscura/loaders/binary.py +1 -1
- oscura/loaders/chipwhisperer.py +1 -2
- oscura/loaders/configurable.py +1 -1
- oscura/loaders/csv_loader.py +2 -2
- oscura/loaders/hdf5_loader.py +1 -1
- oscura/loaders/lazy.py +6 -1
- oscura/loaders/mmap_loader.py +0 -1
- oscura/loaders/numpy_loader.py +8 -7
- oscura/loaders/preprocessing.py +3 -5
- oscura/loaders/rigol.py +21 -7
- oscura/loaders/sigrok.py +2 -5
- oscura/loaders/tdms.py +3 -2
- oscura/loaders/tektronix.py +38 -32
- oscura/loaders/tss.py +20 -27
- oscura/loaders/validation.py +17 -10
- oscura/loaders/vcd.py +13 -8
- oscura/loaders/wav.py +1 -6
- oscura/pipeline/__init__.py +76 -0
- oscura/pipeline/handlers/__init__.py +165 -0
- oscura/pipeline/handlers/analyzers.py +1045 -0
- oscura/pipeline/handlers/decoders.py +899 -0
- oscura/pipeline/handlers/exporters.py +1103 -0
- oscura/pipeline/handlers/filters.py +891 -0
- oscura/pipeline/handlers/loaders.py +640 -0
- oscura/pipeline/handlers/transforms.py +768 -0
- oscura/reporting/formatting/measurements.py +55 -14
- oscura/reporting/templates/enhanced/protocol_re.html +504 -503
- oscura/sessions/legacy.py +49 -1
- oscura/side_channel/__init__.py +38 -57
- oscura/utils/builders/signal_builder.py +5 -5
- oscura/utils/comparison/compare.py +7 -9
- oscura/utils/comparison/golden.py +1 -1
- oscura/utils/filtering/convenience.py +2 -2
- oscura/utils/math/arithmetic.py +38 -62
- oscura/utils/math/interpolation.py +20 -20
- oscura/utils/pipeline/__init__.py +4 -17
- oscura/utils/progressive.py +1 -4
- oscura/utils/triggering/edge.py +1 -1
- oscura/utils/triggering/pattern.py +2 -2
- oscura/utils/triggering/pulse.py +2 -2
- oscura/utils/triggering/window.py +3 -3
- oscura/validation/hil_testing.py +11 -11
- oscura/visualization/__init__.py +46 -284
- oscura/visualization/batch.py +72 -433
- oscura/visualization/plot.py +542 -53
- oscura/visualization/styles.py +184 -318
- oscura/workflows/batch/advanced.py +1 -1
- oscura/workflows/batch/aggregate.py +12 -9
- oscura/workflows/complete_re.py +251 -23
- oscura/workflows/digital.py +27 -4
- oscura/workflows/multi_trace.py +136 -17
- oscura/workflows/waveform.py +11 -6
- oscura-0.11.0.dist-info/METADATA +460 -0
- {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/RECORD +120 -145
- oscura/side_channel/dpa.py +0 -1025
- oscura/utils/optimization/__init__.py +0 -19
- oscura/utils/optimization/parallel.py +0 -443
- oscura/utils/optimization/search.py +0 -532
- oscura/utils/pipeline/base.py +0 -338
- oscura/utils/pipeline/composition.py +0 -248
- oscura/utils/pipeline/parallel.py +0 -449
- oscura/utils/pipeline/pipeline.py +0 -375
- oscura/utils/search/__init__.py +0 -16
- oscura/utils/search/anomaly.py +0 -424
- oscura/utils/search/context.py +0 -294
- oscura/utils/search/pattern.py +0 -288
- oscura/utils/storage/__init__.py +0 -61
- oscura/utils/storage/database.py +0 -1166
- oscura/visualization/accessibility.py +0 -526
- oscura/visualization/annotations.py +0 -371
- oscura/visualization/axis_scaling.py +0 -305
- oscura/visualization/colors.py +0 -451
- oscura/visualization/digital.py +0 -436
- oscura/visualization/eye.py +0 -571
- oscura/visualization/histogram.py +0 -281
- oscura/visualization/interactive.py +0 -1035
- oscura/visualization/jitter.py +0 -1042
- oscura/visualization/keyboard.py +0 -394
- oscura/visualization/layout.py +0 -400
- oscura/visualization/optimization.py +0 -1079
- oscura/visualization/palettes.py +0 -446
- oscura/visualization/power.py +0 -508
- oscura/visualization/power_extended.py +0 -955
- oscura/visualization/presets.py +0 -469
- oscura/visualization/protocols.py +0 -1246
- oscura/visualization/render.py +0 -223
- oscura/visualization/rendering.py +0 -444
- oscura/visualization/reverse_engineering.py +0 -838
- oscura/visualization/signal_integrity.py +0 -989
- oscura/visualization/specialized.py +0 -643
- oscura/visualization/spectral.py +0 -1226
- oscura/visualization/thumbnails.py +0 -340
- oscura/visualization/time_axis.py +0 -351
- oscura/visualization/waveform.py +0 -454
- oscura-0.8.0.dist-info/METADATA +0 -661
- {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/WHEEL +0 -0
- {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/licenses/LICENSE +0 -0
oscura/visualization/eye.py
DELETED
|
@@ -1,571 +0,0 @@
|
|
|
1
|
-
"""Eye diagram visualization for signal integrity analysis.
|
|
2
|
-
|
|
3
|
-
This module provides eye diagram plotting with clock recovery and
|
|
4
|
-
eye opening measurements.
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Example:
|
|
8
|
-
>>> from oscura.visualization.eye import plot_eye
|
|
9
|
-
>>> fig = plot_eye(trace, bit_rate=1e9)
|
|
10
|
-
>>> plt.show()
|
|
11
|
-
|
|
12
|
-
References:
|
|
13
|
-
IEEE 802.3 Ethernet standards for eye diagram testing
|
|
14
|
-
JEDEC eye diagram measurement specifications
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
|
-
from __future__ import annotations
|
|
18
|
-
|
|
19
|
-
from typing import TYPE_CHECKING, Any, Literal, cast
|
|
20
|
-
|
|
21
|
-
import numpy as np
|
|
22
|
-
|
|
23
|
-
try:
|
|
24
|
-
import matplotlib.pyplot as plt
|
|
25
|
-
from matplotlib.colors import LinearSegmentedColormap # noqa: F401
|
|
26
|
-
|
|
27
|
-
HAS_MATPLOTLIB = True
|
|
28
|
-
except ImportError:
|
|
29
|
-
HAS_MATPLOTLIB = False
|
|
30
|
-
|
|
31
|
-
from oscura.core.exceptions import InsufficientDataError
|
|
32
|
-
|
|
33
|
-
if TYPE_CHECKING:
|
|
34
|
-
from matplotlib.axes import Axes
|
|
35
|
-
from matplotlib.figure import Figure
|
|
36
|
-
from numpy.typing import NDArray
|
|
37
|
-
|
|
38
|
-
from oscura.core.types import WaveformTrace
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def plot_eye(
|
|
42
|
-
trace: WaveformTrace,
|
|
43
|
-
*,
|
|
44
|
-
bit_rate: float | None = None,
|
|
45
|
-
clock_recovery: Literal["fft", "edge"] = "edge",
|
|
46
|
-
samples_per_bit: int | None = None,
|
|
47
|
-
ax: Axes | None = None,
|
|
48
|
-
cmap: str = "hot",
|
|
49
|
-
alpha: float = 0.3,
|
|
50
|
-
show_measurements: bool = True,
|
|
51
|
-
title: str | None = None,
|
|
52
|
-
colorbar: bool = False,
|
|
53
|
-
) -> Figure:
|
|
54
|
-
"""Plot eye diagram for signal integrity analysis.
|
|
55
|
-
|
|
56
|
-
Creates an eye diagram by overlaying multiple bit periods from a
|
|
57
|
-
serial data signal. Automatically recovers clock from signal if
|
|
58
|
-
bit_rate is not specified.
|
|
59
|
-
|
|
60
|
-
Args:
|
|
61
|
-
trace: Input waveform trace (serial data signal).
|
|
62
|
-
bit_rate: Bit rate in bits/second. If None, auto-recovered from signal.
|
|
63
|
-
clock_recovery: Method for clock recovery ("fft" or "edge").
|
|
64
|
-
samples_per_bit: Number of samples per bit period. Auto-calculated if None.
|
|
65
|
-
ax: Matplotlib axes. If None, creates new figure.
|
|
66
|
-
cmap: Colormap for density visualization ("hot", "viridis", "Blues").
|
|
67
|
-
alpha: Transparency for overlaid traces (0.0 to 1.0).
|
|
68
|
-
show_measurements: Annotate eye opening measurements.
|
|
69
|
-
title: Plot title.
|
|
70
|
-
colorbar: Show colorbar for density plot.
|
|
71
|
-
|
|
72
|
-
Returns:
|
|
73
|
-
Matplotlib Figure object with eye diagram.
|
|
74
|
-
|
|
75
|
-
Raises:
|
|
76
|
-
ImportError: If matplotlib is not available.
|
|
77
|
-
InsufficientDataError: If trace is too short for analysis.
|
|
78
|
-
ValueError: If clock recovery failed or axes has no figure.
|
|
79
|
-
|
|
80
|
-
Example:
|
|
81
|
-
>>> # With known bit rate
|
|
82
|
-
>>> fig = plot_eye(trace, bit_rate=1e9) # 1 Gbps
|
|
83
|
-
>>> plt.show()
|
|
84
|
-
|
|
85
|
-
>>> # Auto-recover clock
|
|
86
|
-
>>> fig = plot_eye(trace, clock_recovery="fft")
|
|
87
|
-
>>> plt.show()
|
|
88
|
-
|
|
89
|
-
References:
|
|
90
|
-
IEEE 802.3: Ethernet eye diagram specifications
|
|
91
|
-
JEDEC JESD65B: High-Speed Interface Eye Diagram Measurements
|
|
92
|
-
"""
|
|
93
|
-
_validate_matplotlib_available()
|
|
94
|
-
_validate_trace_length(trace, min_samples=100)
|
|
95
|
-
|
|
96
|
-
bit_rate, samples_per_bit = _determine_timing_parameters(
|
|
97
|
-
trace, bit_rate, clock_recovery, samples_per_bit
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
fig, ax = _prepare_figure(ax)
|
|
101
|
-
data, n_bits, time_ui = _prepare_eye_data(trace, samples_per_bit)
|
|
102
|
-
|
|
103
|
-
_plot_eye_traces(ax, fig, data, n_bits, samples_per_bit, time_ui, cmap, alpha, colorbar)
|
|
104
|
-
_format_eye_plot(ax, bit_rate, title)
|
|
105
|
-
|
|
106
|
-
if show_measurements:
|
|
107
|
-
eye_metrics = _calculate_eye_metrics(data, samples_per_bit, n_bits)
|
|
108
|
-
_add_eye_measurements(ax, eye_metrics, time_ui)
|
|
109
|
-
|
|
110
|
-
fig.tight_layout()
|
|
111
|
-
return fig
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
def _validate_matplotlib_available() -> None:
|
|
115
|
-
"""Validate matplotlib is available for plotting.
|
|
116
|
-
|
|
117
|
-
Raises:
|
|
118
|
-
ImportError: If matplotlib not installed.
|
|
119
|
-
"""
|
|
120
|
-
if not HAS_MATPLOTLIB:
|
|
121
|
-
raise ImportError("matplotlib is required for visualization")
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
def _validate_trace_length(trace: WaveformTrace, min_samples: int) -> None:
|
|
125
|
-
"""Validate trace has sufficient samples.
|
|
126
|
-
|
|
127
|
-
Args:
|
|
128
|
-
trace: Waveform trace to validate.
|
|
129
|
-
min_samples: Minimum required samples.
|
|
130
|
-
|
|
131
|
-
Raises:
|
|
132
|
-
InsufficientDataError: If trace too short.
|
|
133
|
-
"""
|
|
134
|
-
if len(trace.data) < min_samples:
|
|
135
|
-
raise InsufficientDataError(
|
|
136
|
-
f"Eye diagram requires at least {min_samples} samples",
|
|
137
|
-
required=min_samples,
|
|
138
|
-
available=len(trace.data),
|
|
139
|
-
analysis_type="eye_diagram",
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
def _determine_timing_parameters(
|
|
144
|
-
trace: WaveformTrace,
|
|
145
|
-
bit_rate: float | None,
|
|
146
|
-
clock_recovery: Literal["fft", "edge"],
|
|
147
|
-
samples_per_bit: int | None,
|
|
148
|
-
) -> tuple[float, int]:
|
|
149
|
-
"""Determine bit rate and samples per bit.
|
|
150
|
-
|
|
151
|
-
Args:
|
|
152
|
-
trace: Input waveform trace.
|
|
153
|
-
bit_rate: Bit rate or None for auto-recovery.
|
|
154
|
-
clock_recovery: Clock recovery method.
|
|
155
|
-
samples_per_bit: Samples per bit or None for auto-calculation.
|
|
156
|
-
|
|
157
|
-
Returns:
|
|
158
|
-
Tuple of (bit_rate, samples_per_bit).
|
|
159
|
-
|
|
160
|
-
Raises:
|
|
161
|
-
ValueError: If clock recovery fails.
|
|
162
|
-
InsufficientDataError: If too few samples per bit.
|
|
163
|
-
"""
|
|
164
|
-
if bit_rate is None:
|
|
165
|
-
bit_rate, bit_period = _recover_clock(trace, clock_recovery)
|
|
166
|
-
else:
|
|
167
|
-
bit_period = 1.0 / bit_rate
|
|
168
|
-
|
|
169
|
-
if samples_per_bit is None:
|
|
170
|
-
samples_per_bit = int(bit_period / trace.metadata.time_base)
|
|
171
|
-
|
|
172
|
-
if samples_per_bit < 10:
|
|
173
|
-
raise InsufficientDataError(
|
|
174
|
-
f"Insufficient samples per bit period (need ≥10, got {samples_per_bit})",
|
|
175
|
-
required=10,
|
|
176
|
-
available=samples_per_bit,
|
|
177
|
-
analysis_type="eye_diagram",
|
|
178
|
-
)
|
|
179
|
-
|
|
180
|
-
return bit_rate, samples_per_bit
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
def _recover_clock(trace: WaveformTrace, method: Literal["fft", "edge"]) -> tuple[float, float]:
|
|
184
|
-
"""Recover clock from signal.
|
|
185
|
-
|
|
186
|
-
Args:
|
|
187
|
-
trace: Input trace.
|
|
188
|
-
method: Recovery method.
|
|
189
|
-
|
|
190
|
-
Returns:
|
|
191
|
-
Tuple of (bit_rate, bit_period).
|
|
192
|
-
|
|
193
|
-
Raises:
|
|
194
|
-
ValueError: If recovery fails.
|
|
195
|
-
"""
|
|
196
|
-
from oscura.analyzers.digital.timing import recover_clock_edge, recover_clock_fft
|
|
197
|
-
|
|
198
|
-
result = recover_clock_fft(trace) if method == "fft" else recover_clock_edge(trace)
|
|
199
|
-
|
|
200
|
-
if np.isnan(result.frequency):
|
|
201
|
-
raise ValueError("Clock recovery failed - cannot determine bit rate")
|
|
202
|
-
|
|
203
|
-
return result.frequency, result.period
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
def _prepare_figure(ax: Axes | None) -> tuple[Figure, Axes]:
|
|
207
|
-
"""Prepare matplotlib figure and axes.
|
|
208
|
-
|
|
209
|
-
Args:
|
|
210
|
-
ax: Existing axes or None to create new.
|
|
211
|
-
|
|
212
|
-
Returns:
|
|
213
|
-
Tuple of (figure, axes).
|
|
214
|
-
|
|
215
|
-
Raises:
|
|
216
|
-
ValueError: If axes has no associated figure.
|
|
217
|
-
"""
|
|
218
|
-
if ax is None:
|
|
219
|
-
fig, ax_new = plt.subplots(figsize=(8, 6))
|
|
220
|
-
return fig, ax_new
|
|
221
|
-
|
|
222
|
-
fig_temp = ax.get_figure()
|
|
223
|
-
if fig_temp is None:
|
|
224
|
-
raise ValueError("Axes must have an associated figure")
|
|
225
|
-
return cast("Figure", fig_temp), ax
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
def _prepare_eye_data(
|
|
229
|
-
trace: WaveformTrace, samples_per_bit: int
|
|
230
|
-
) -> tuple[NDArray[np.floating[Any]], int, NDArray[np.float64]]:
|
|
231
|
-
"""Prepare data for eye diagram plotting.
|
|
232
|
-
|
|
233
|
-
Args:
|
|
234
|
-
trace: Input trace.
|
|
235
|
-
samples_per_bit: Samples per bit period.
|
|
236
|
-
|
|
237
|
-
Returns:
|
|
238
|
-
Tuple of (data, n_bits, time_ui).
|
|
239
|
-
|
|
240
|
-
Raises:
|
|
241
|
-
InsufficientDataError: If not enough bit periods.
|
|
242
|
-
"""
|
|
243
|
-
data = trace.data
|
|
244
|
-
n_bits = len(data) // samples_per_bit
|
|
245
|
-
|
|
246
|
-
if n_bits < 2:
|
|
247
|
-
raise InsufficientDataError(
|
|
248
|
-
f"Not enough complete bit periods (need ≥2, got {n_bits})",
|
|
249
|
-
required=2,
|
|
250
|
-
available=n_bits,
|
|
251
|
-
analysis_type="eye_diagram",
|
|
252
|
-
)
|
|
253
|
-
|
|
254
|
-
time_ui = np.linspace(0, 1, samples_per_bit)
|
|
255
|
-
return data, n_bits, time_ui
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
def _plot_eye_traces(
|
|
259
|
-
ax: Axes,
|
|
260
|
-
fig: Figure,
|
|
261
|
-
data: NDArray[np.floating[Any]],
|
|
262
|
-
n_bits: int,
|
|
263
|
-
samples_per_bit: int,
|
|
264
|
-
time_ui: NDArray[np.float64],
|
|
265
|
-
cmap: str,
|
|
266
|
-
alpha: float,
|
|
267
|
-
colorbar: bool,
|
|
268
|
-
) -> None:
|
|
269
|
-
"""Plot eye traces as density or line overlay.
|
|
270
|
-
|
|
271
|
-
Args:
|
|
272
|
-
ax: Matplotlib axes.
|
|
273
|
-
fig: Matplotlib figure.
|
|
274
|
-
data: Waveform data.
|
|
275
|
-
n_bits: Number of bit periods.
|
|
276
|
-
samples_per_bit: Samples per bit.
|
|
277
|
-
time_ui: Time axis in UI.
|
|
278
|
-
cmap: Colormap name.
|
|
279
|
-
alpha: Transparency.
|
|
280
|
-
colorbar: Show colorbar.
|
|
281
|
-
"""
|
|
282
|
-
if cmap != "none":
|
|
283
|
-
_plot_density_eye(ax, fig, data, n_bits, samples_per_bit, time_ui, cmap, colorbar)
|
|
284
|
-
else:
|
|
285
|
-
_plot_line_eye(ax, data, n_bits, samples_per_bit, time_ui, alpha)
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
def _plot_density_eye(
|
|
289
|
-
ax: Axes,
|
|
290
|
-
fig: Figure,
|
|
291
|
-
data: NDArray[np.floating[Any]],
|
|
292
|
-
n_bits: int,
|
|
293
|
-
samples_per_bit: int,
|
|
294
|
-
time_ui: NDArray[np.float64],
|
|
295
|
-
cmap: str,
|
|
296
|
-
colorbar: bool,
|
|
297
|
-
) -> None:
|
|
298
|
-
"""Plot eye diagram as density heatmap.
|
|
299
|
-
|
|
300
|
-
Args:
|
|
301
|
-
ax: Axes to plot on.
|
|
302
|
-
fig: Figure for colorbar.
|
|
303
|
-
data: Waveform data.
|
|
304
|
-
n_bits: Number of bits.
|
|
305
|
-
samples_per_bit: Samples per bit.
|
|
306
|
-
time_ui: Time in UI.
|
|
307
|
-
cmap: Colormap.
|
|
308
|
-
colorbar: Show colorbar.
|
|
309
|
-
"""
|
|
310
|
-
all_times: list[np.floating[Any]] = []
|
|
311
|
-
all_voltages: list[np.floating[Any]] = []
|
|
312
|
-
|
|
313
|
-
for i in range(n_bits - 1):
|
|
314
|
-
start_idx = i * samples_per_bit
|
|
315
|
-
end_idx = start_idx + samples_per_bit
|
|
316
|
-
if end_idx <= len(data):
|
|
317
|
-
all_times.extend(time_ui)
|
|
318
|
-
all_voltages.extend(data[start_idx:end_idx])
|
|
319
|
-
|
|
320
|
-
h, xedges, yedges = np.histogram2d(all_times, all_voltages, bins=[200, 200])
|
|
321
|
-
extent_list = [float(xedges[0]), float(xedges[-1]), float(yedges[0]), float(yedges[-1])]
|
|
322
|
-
|
|
323
|
-
im = ax.imshow(
|
|
324
|
-
h.T,
|
|
325
|
-
extent=tuple(extent_list), # type: ignore[arg-type]
|
|
326
|
-
origin="lower",
|
|
327
|
-
aspect="auto",
|
|
328
|
-
cmap=cmap,
|
|
329
|
-
interpolation="bilinear",
|
|
330
|
-
)
|
|
331
|
-
|
|
332
|
-
if colorbar:
|
|
333
|
-
fig.colorbar(im, ax=ax, label="Sample Density")
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
def _plot_line_eye(
|
|
337
|
-
ax: Axes,
|
|
338
|
-
data: NDArray[np.floating[Any]],
|
|
339
|
-
n_bits: int,
|
|
340
|
-
samples_per_bit: int,
|
|
341
|
-
time_ui: NDArray[np.float64],
|
|
342
|
-
alpha: float,
|
|
343
|
-
) -> None:
|
|
344
|
-
"""Plot eye diagram as overlaid lines.
|
|
345
|
-
|
|
346
|
-
Args:
|
|
347
|
-
ax: Axes to plot on.
|
|
348
|
-
data: Waveform data.
|
|
349
|
-
n_bits: Number of bits.
|
|
350
|
-
samples_per_bit: Samples per bit.
|
|
351
|
-
time_ui: Time in UI.
|
|
352
|
-
alpha: Line transparency.
|
|
353
|
-
"""
|
|
354
|
-
for i in range(min(n_bits - 1, 1000)): # Limit for performance
|
|
355
|
-
start_idx = i * samples_per_bit
|
|
356
|
-
end_idx = start_idx + samples_per_bit
|
|
357
|
-
if end_idx <= len(data):
|
|
358
|
-
ax.plot(time_ui, data[start_idx:end_idx], color="blue", alpha=alpha, linewidth=0.5)
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
def _format_eye_plot(ax: Axes, bit_rate: float, title: str | None) -> None:
|
|
362
|
-
"""Format eye diagram plot labels and styling.
|
|
363
|
-
|
|
364
|
-
Args:
|
|
365
|
-
ax: Axes to format.
|
|
366
|
-
bit_rate: Bit rate for title.
|
|
367
|
-
title: Custom title or None.
|
|
368
|
-
"""
|
|
369
|
-
ax.set_xlabel("Time (UI)")
|
|
370
|
-
ax.set_ylabel("Voltage (V)")
|
|
371
|
-
ax.set_xlim(0, 1)
|
|
372
|
-
ax.set_title(title if title else f"Eye Diagram @ {bit_rate / 1e6:.1f} Mbps")
|
|
373
|
-
ax.grid(True, alpha=0.3)
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
def _calculate_eye_metrics(
|
|
377
|
-
data: NDArray[np.floating[Any]],
|
|
378
|
-
samples_per_bit: int,
|
|
379
|
-
n_bits: int,
|
|
380
|
-
) -> dict[str, float]:
|
|
381
|
-
"""Calculate eye diagram opening metrics.
|
|
382
|
-
|
|
383
|
-
Args:
|
|
384
|
-
data: Waveform data.
|
|
385
|
-
samples_per_bit: Samples per bit period.
|
|
386
|
-
n_bits: Number of complete bit periods.
|
|
387
|
-
|
|
388
|
-
Returns:
|
|
389
|
-
Dictionary with eye metrics:
|
|
390
|
-
- eye_height: Vertical eye opening (V)
|
|
391
|
-
- eye_width: Horizontal eye opening (UI)
|
|
392
|
-
- crossing_voltage: Zero-crossing voltage (V)
|
|
393
|
-
- ber_margin: Bit error rate margin estimate
|
|
394
|
-
"""
|
|
395
|
-
# Extract center samples (middle 50% of bit period)
|
|
396
|
-
center_start = samples_per_bit // 4
|
|
397
|
-
center_end = 3 * samples_per_bit // 4
|
|
398
|
-
|
|
399
|
-
# Collect center samples from all bit periods
|
|
400
|
-
center_samples_list: list[np.floating[Any]] = []
|
|
401
|
-
for i in range(n_bits - 1):
|
|
402
|
-
start_idx = i * samples_per_bit + center_start
|
|
403
|
-
end_idx = i * samples_per_bit + center_end
|
|
404
|
-
if end_idx <= len(data):
|
|
405
|
-
center_samples_list.extend(data[start_idx:end_idx])
|
|
406
|
-
|
|
407
|
-
center_samples = np.array(center_samples_list)
|
|
408
|
-
|
|
409
|
-
if len(center_samples) == 0:
|
|
410
|
-
return {
|
|
411
|
-
"eye_height": np.nan,
|
|
412
|
-
"eye_width": np.nan,
|
|
413
|
-
"crossing_voltage": np.nan,
|
|
414
|
-
"ber_margin": np.nan,
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
# Estimate logic levels using histogram
|
|
418
|
-
hist, bin_edges = np.histogram(center_samples, bins=100)
|
|
419
|
-
bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2
|
|
420
|
-
|
|
421
|
-
# Find peaks for logic 0 and logic 1
|
|
422
|
-
mid_idx = len(hist) // 2
|
|
423
|
-
low_peak_idx = np.argmax(hist[:mid_idx])
|
|
424
|
-
high_peak_idx = mid_idx + np.argmax(hist[mid_idx:])
|
|
425
|
-
|
|
426
|
-
v_low = bin_centers[low_peak_idx]
|
|
427
|
-
v_high = bin_centers[high_peak_idx]
|
|
428
|
-
|
|
429
|
-
# Crossing voltage (midpoint)
|
|
430
|
-
v_cross = (v_low + v_high) / 2
|
|
431
|
-
|
|
432
|
-
# Eye height (vertical opening)
|
|
433
|
-
# Use 20th-80th percentile for robustness
|
|
434
|
-
low_samples = center_samples[center_samples < v_cross]
|
|
435
|
-
high_samples = center_samples[center_samples >= v_cross]
|
|
436
|
-
|
|
437
|
-
if len(low_samples) > 0 and len(high_samples) > 0:
|
|
438
|
-
v_low_80 = np.percentile(low_samples, 80)
|
|
439
|
-
v_high_20 = np.percentile(high_samples, 20)
|
|
440
|
-
eye_height = v_high_20 - v_low_80
|
|
441
|
-
else:
|
|
442
|
-
eye_height = v_high - v_low
|
|
443
|
-
|
|
444
|
-
# Eye width estimation (simplified)
|
|
445
|
-
# Find the time span where eye is open (center region)
|
|
446
|
-
eye_width = 0.5 # 50% of UI is typical for good signal
|
|
447
|
-
|
|
448
|
-
# BER margin (simplified estimate)
|
|
449
|
-
signal_swing = v_high - v_low
|
|
450
|
-
ber_margin = (eye_height / signal_swing) if signal_swing > 0 else 0.0
|
|
451
|
-
|
|
452
|
-
return {
|
|
453
|
-
"eye_height": float(eye_height),
|
|
454
|
-
"eye_width": float(eye_width),
|
|
455
|
-
"crossing_voltage": float(v_cross),
|
|
456
|
-
"ber_margin": float(ber_margin),
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
def _add_eye_measurements(
|
|
461
|
-
ax: Axes,
|
|
462
|
-
metrics: dict[str, float],
|
|
463
|
-
time_ui: NDArray[np.float64],
|
|
464
|
-
) -> None:
|
|
465
|
-
"""Add measurement annotations to eye diagram.
|
|
466
|
-
|
|
467
|
-
Args:
|
|
468
|
-
ax: Matplotlib axes.
|
|
469
|
-
metrics: Eye diagram metrics.
|
|
470
|
-
time_ui: Time axis in UI.
|
|
471
|
-
"""
|
|
472
|
-
# Create measurement text
|
|
473
|
-
lines = []
|
|
474
|
-
if not np.isnan(metrics["eye_height"]):
|
|
475
|
-
lines.append(f"Eye Height: {metrics['eye_height'] * 1e3:.1f} mV")
|
|
476
|
-
if not np.isnan(metrics["eye_width"]):
|
|
477
|
-
lines.append(f"Eye Width: {metrics['eye_width']:.2f} UI")
|
|
478
|
-
if not np.isnan(metrics["crossing_voltage"]):
|
|
479
|
-
lines.append(f"Crossing: {metrics['crossing_voltage']:.3f} V")
|
|
480
|
-
if not np.isnan(metrics["ber_margin"]):
|
|
481
|
-
lines.append(f"BER Margin: {metrics['ber_margin'] * 100:.1f}%")
|
|
482
|
-
|
|
483
|
-
if lines:
|
|
484
|
-
text = "\n".join(lines)
|
|
485
|
-
ax.annotate(
|
|
486
|
-
text,
|
|
487
|
-
xy=(0.02, 0.98),
|
|
488
|
-
xycoords="axes fraction",
|
|
489
|
-
verticalalignment="top",
|
|
490
|
-
fontfamily="monospace",
|
|
491
|
-
fontsize=9,
|
|
492
|
-
bbox={"boxstyle": "round", "facecolor": "wheat", "alpha": 0.9},
|
|
493
|
-
)
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
def plot_bathtub(
|
|
497
|
-
trace: WaveformTrace,
|
|
498
|
-
*,
|
|
499
|
-
bit_rate: float | None = None,
|
|
500
|
-
ber_target: float = 1e-12,
|
|
501
|
-
ax: Axes | None = None,
|
|
502
|
-
title: str | None = None,
|
|
503
|
-
) -> Figure:
|
|
504
|
-
"""Plot bathtub curve for BER analysis.
|
|
505
|
-
|
|
506
|
-
Creates a bathtub curve showing bit error rate vs. sampling position
|
|
507
|
-
within the unit interval. Used for determining optimal sampling point
|
|
508
|
-
and timing margin.
|
|
509
|
-
|
|
510
|
-
Args:
|
|
511
|
-
trace: Input waveform trace.
|
|
512
|
-
bit_rate: Bit rate in bits/second.
|
|
513
|
-
ber_target: Target bit error rate for margin calculation.
|
|
514
|
-
ax: Matplotlib axes.
|
|
515
|
-
title: Plot title.
|
|
516
|
-
|
|
517
|
-
Returns:
|
|
518
|
-
Matplotlib Figure object.
|
|
519
|
-
|
|
520
|
-
Raises:
|
|
521
|
-
ImportError: If matplotlib is not available.
|
|
522
|
-
ValueError: If axes has no associated figure.
|
|
523
|
-
|
|
524
|
-
Example:
|
|
525
|
-
>>> fig = plot_bathtub(trace, bit_rate=1e9, ber_target=1e-12)
|
|
526
|
-
|
|
527
|
-
References:
|
|
528
|
-
IEEE 802.3: Bathtub curve methodology
|
|
529
|
-
"""
|
|
530
|
-
if not HAS_MATPLOTLIB:
|
|
531
|
-
raise ImportError("matplotlib is required for visualization")
|
|
532
|
-
|
|
533
|
-
# Placeholder implementation for bathtub curve
|
|
534
|
-
# Full implementation would require statistical analysis of jitter
|
|
535
|
-
# and noise distributions
|
|
536
|
-
|
|
537
|
-
if ax is None:
|
|
538
|
-
fig, ax = plt.subplots(figsize=(8, 5))
|
|
539
|
-
else:
|
|
540
|
-
fig_temp = ax.get_figure()
|
|
541
|
-
if fig_temp is None:
|
|
542
|
-
raise ValueError("Axes must have an associated figure")
|
|
543
|
-
fig = cast("Figure", fig_temp)
|
|
544
|
-
|
|
545
|
-
# Simplified bathtub curve visualization
|
|
546
|
-
ui = np.linspace(0, 1, 100)
|
|
547
|
-
# Bathtub shape: high BER at edges, low in center
|
|
548
|
-
ber = 1e-2 * (np.exp(-(((ui - 0.5) / 0.2) ** 2) * 10) + 1e-12)
|
|
549
|
-
|
|
550
|
-
ax.semilogy(ui, ber, linewidth=2, color="C0")
|
|
551
|
-
ax.axhline(ber_target, color="red", linestyle="--", label=f"BER Target: {ber_target:.0e}")
|
|
552
|
-
|
|
553
|
-
ax.set_xlabel("Sample Position (UI)")
|
|
554
|
-
ax.set_ylabel("Bit Error Rate")
|
|
555
|
-
ax.set_xlim(0, 1)
|
|
556
|
-
ax.grid(True, alpha=0.3, which="both")
|
|
557
|
-
ax.legend()
|
|
558
|
-
|
|
559
|
-
if title:
|
|
560
|
-
ax.set_title(title)
|
|
561
|
-
else:
|
|
562
|
-
ax.set_title("Bathtub Curve")
|
|
563
|
-
|
|
564
|
-
fig.tight_layout()
|
|
565
|
-
return fig
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
__all__ = [
|
|
569
|
-
"plot_bathtub",
|
|
570
|
-
"plot_eye",
|
|
571
|
-
]
|