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.
Files changed (161) hide show
  1. oscura/__init__.py +19 -19
  2. oscura/__main__.py +4 -0
  3. oscura/analyzers/__init__.py +2 -0
  4. oscura/analyzers/digital/extraction.py +2 -3
  5. oscura/analyzers/digital/quality.py +1 -1
  6. oscura/analyzers/digital/timing.py +1 -1
  7. oscura/analyzers/ml/signal_classifier.py +6 -0
  8. oscura/analyzers/patterns/__init__.py +66 -0
  9. oscura/analyzers/power/basic.py +3 -3
  10. oscura/analyzers/power/soa.py +1 -1
  11. oscura/analyzers/power/switching.py +3 -3
  12. oscura/analyzers/signal_classification.py +529 -0
  13. oscura/analyzers/signal_integrity/sparams.py +3 -3
  14. oscura/analyzers/statistics/basic.py +10 -7
  15. oscura/analyzers/validation.py +1 -1
  16. oscura/analyzers/waveform/measurements.py +200 -156
  17. oscura/analyzers/waveform/measurements_with_uncertainty.py +91 -35
  18. oscura/analyzers/waveform/spectral.py +182 -84
  19. oscura/api/dsl/commands.py +15 -6
  20. oscura/api/server/templates/base.html +137 -146
  21. oscura/api/server/templates/export.html +84 -110
  22. oscura/api/server/templates/home.html +248 -267
  23. oscura/api/server/templates/protocols.html +44 -48
  24. oscura/api/server/templates/reports.html +27 -35
  25. oscura/api/server/templates/session_detail.html +68 -78
  26. oscura/api/server/templates/sessions.html +62 -72
  27. oscura/api/server/templates/waveforms.html +54 -64
  28. oscura/automotive/__init__.py +1 -1
  29. oscura/automotive/can/session.py +1 -1
  30. oscura/automotive/dbc/generator.py +638 -23
  31. oscura/automotive/dtc/data.json +17 -102
  32. oscura/automotive/flexray/fibex.py +9 -1
  33. oscura/automotive/uds/decoder.py +99 -6
  34. oscura/cli/analyze.py +8 -2
  35. oscura/cli/batch.py +36 -5
  36. oscura/cli/characterize.py +18 -4
  37. oscura/cli/export.py +47 -5
  38. oscura/cli/main.py +2 -0
  39. oscura/cli/onboarding/wizard.py +10 -6
  40. oscura/cli/pipeline.py +585 -0
  41. oscura/cli/visualize.py +6 -4
  42. oscura/convenience.py +400 -32
  43. oscura/core/measurement_result.py +286 -0
  44. oscura/core/progress.py +1 -1
  45. oscura/core/schemas/device_mapping.json +2 -8
  46. oscura/core/schemas/packet_format.json +4 -24
  47. oscura/core/schemas/protocol_definition.json +2 -12
  48. oscura/core/types.py +232 -239
  49. oscura/correlation/multi_protocol.py +1 -1
  50. oscura/export/legacy/__init__.py +11 -0
  51. oscura/export/legacy/wav.py +75 -0
  52. oscura/exporters/__init__.py +19 -0
  53. oscura/exporters/wireshark.py +809 -0
  54. oscura/hardware/acquisition/file.py +5 -19
  55. oscura/hardware/acquisition/saleae.py +10 -10
  56. oscura/hardware/acquisition/socketcan.py +4 -6
  57. oscura/hardware/acquisition/synthetic.py +1 -5
  58. oscura/hardware/acquisition/visa.py +6 -6
  59. oscura/hardware/security/side_channel_detector.py +5 -508
  60. oscura/inference/message_format.py +686 -1
  61. oscura/jupyter/display.py +2 -2
  62. oscura/jupyter/magic.py +3 -3
  63. oscura/loaders/__init__.py +17 -12
  64. oscura/loaders/binary.py +1 -1
  65. oscura/loaders/chipwhisperer.py +1 -2
  66. oscura/loaders/configurable.py +1 -1
  67. oscura/loaders/csv_loader.py +2 -2
  68. oscura/loaders/hdf5_loader.py +1 -1
  69. oscura/loaders/lazy.py +6 -1
  70. oscura/loaders/mmap_loader.py +0 -1
  71. oscura/loaders/numpy_loader.py +8 -7
  72. oscura/loaders/preprocessing.py +3 -5
  73. oscura/loaders/rigol.py +21 -7
  74. oscura/loaders/sigrok.py +2 -5
  75. oscura/loaders/tdms.py +3 -2
  76. oscura/loaders/tektronix.py +38 -32
  77. oscura/loaders/tss.py +20 -27
  78. oscura/loaders/validation.py +17 -10
  79. oscura/loaders/vcd.py +13 -8
  80. oscura/loaders/wav.py +1 -6
  81. oscura/pipeline/__init__.py +76 -0
  82. oscura/pipeline/handlers/__init__.py +165 -0
  83. oscura/pipeline/handlers/analyzers.py +1045 -0
  84. oscura/pipeline/handlers/decoders.py +899 -0
  85. oscura/pipeline/handlers/exporters.py +1103 -0
  86. oscura/pipeline/handlers/filters.py +891 -0
  87. oscura/pipeline/handlers/loaders.py +640 -0
  88. oscura/pipeline/handlers/transforms.py +768 -0
  89. oscura/reporting/formatting/measurements.py +55 -14
  90. oscura/reporting/templates/enhanced/protocol_re.html +504 -503
  91. oscura/sessions/legacy.py +49 -1
  92. oscura/side_channel/__init__.py +38 -57
  93. oscura/utils/builders/signal_builder.py +5 -5
  94. oscura/utils/comparison/compare.py +7 -9
  95. oscura/utils/comparison/golden.py +1 -1
  96. oscura/utils/filtering/convenience.py +2 -2
  97. oscura/utils/math/arithmetic.py +38 -62
  98. oscura/utils/math/interpolation.py +20 -20
  99. oscura/utils/pipeline/__init__.py +4 -17
  100. oscura/utils/progressive.py +1 -4
  101. oscura/utils/triggering/edge.py +1 -1
  102. oscura/utils/triggering/pattern.py +2 -2
  103. oscura/utils/triggering/pulse.py +2 -2
  104. oscura/utils/triggering/window.py +3 -3
  105. oscura/validation/hil_testing.py +11 -11
  106. oscura/visualization/__init__.py +46 -284
  107. oscura/visualization/batch.py +72 -433
  108. oscura/visualization/plot.py +542 -53
  109. oscura/visualization/styles.py +184 -318
  110. oscura/workflows/batch/advanced.py +1 -1
  111. oscura/workflows/batch/aggregate.py +12 -9
  112. oscura/workflows/complete_re.py +251 -23
  113. oscura/workflows/digital.py +27 -4
  114. oscura/workflows/multi_trace.py +136 -17
  115. oscura/workflows/waveform.py +11 -6
  116. oscura-0.11.0.dist-info/METADATA +460 -0
  117. {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/RECORD +120 -145
  118. oscura/side_channel/dpa.py +0 -1025
  119. oscura/utils/optimization/__init__.py +0 -19
  120. oscura/utils/optimization/parallel.py +0 -443
  121. oscura/utils/optimization/search.py +0 -532
  122. oscura/utils/pipeline/base.py +0 -338
  123. oscura/utils/pipeline/composition.py +0 -248
  124. oscura/utils/pipeline/parallel.py +0 -449
  125. oscura/utils/pipeline/pipeline.py +0 -375
  126. oscura/utils/search/__init__.py +0 -16
  127. oscura/utils/search/anomaly.py +0 -424
  128. oscura/utils/search/context.py +0 -294
  129. oscura/utils/search/pattern.py +0 -288
  130. oscura/utils/storage/__init__.py +0 -61
  131. oscura/utils/storage/database.py +0 -1166
  132. oscura/visualization/accessibility.py +0 -526
  133. oscura/visualization/annotations.py +0 -371
  134. oscura/visualization/axis_scaling.py +0 -305
  135. oscura/visualization/colors.py +0 -451
  136. oscura/visualization/digital.py +0 -436
  137. oscura/visualization/eye.py +0 -571
  138. oscura/visualization/histogram.py +0 -281
  139. oscura/visualization/interactive.py +0 -1035
  140. oscura/visualization/jitter.py +0 -1042
  141. oscura/visualization/keyboard.py +0 -394
  142. oscura/visualization/layout.py +0 -400
  143. oscura/visualization/optimization.py +0 -1079
  144. oscura/visualization/palettes.py +0 -446
  145. oscura/visualization/power.py +0 -508
  146. oscura/visualization/power_extended.py +0 -955
  147. oscura/visualization/presets.py +0 -469
  148. oscura/visualization/protocols.py +0 -1246
  149. oscura/visualization/render.py +0 -223
  150. oscura/visualization/rendering.py +0 -444
  151. oscura/visualization/reverse_engineering.py +0 -838
  152. oscura/visualization/signal_integrity.py +0 -989
  153. oscura/visualization/specialized.py +0 -643
  154. oscura/visualization/spectral.py +0 -1226
  155. oscura/visualization/thumbnails.py +0 -340
  156. oscura/visualization/time_axis.py +0 -351
  157. oscura/visualization/waveform.py +0 -454
  158. oscura-0.8.0.dist-info/METADATA +0 -661
  159. {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/WHEEL +0 -0
  160. {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/entry_points.txt +0 -0
  161. {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/licenses/LICENSE +0 -0
@@ -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
- ]