oscura 0.7.0__py3-none-any.whl → 0.10.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/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/eye/__init__.py +5 -1
- oscura/analyzers/eye/generation.py +501 -0
- oscura/analyzers/jitter/__init__.py +6 -6
- oscura/analyzers/jitter/timing.py +419 -0
- oscura/analyzers/patterns/__init__.py +94 -0
- oscura/analyzers/patterns/reverse_engineering.py +991 -0
- oscura/analyzers/power/__init__.py +35 -12
- 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/__init__.py +4 -0
- oscura/analyzers/statistics/basic.py +152 -0
- oscura/analyzers/statistics/correlation.py +47 -6
- oscura/analyzers/validation.py +1 -1
- oscura/analyzers/waveform/__init__.py +2 -0
- oscura/analyzers/waveform/measurements.py +329 -163
- oscura/analyzers/waveform/measurements_with_uncertainty.py +91 -35
- oscura/analyzers/waveform/spectral.py +498 -54
- 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 +102 -17
- 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/config/loader.py +0 -1
- oscura/core/measurement_result.py +286 -0
- oscura/core/progress.py +1 -1
- oscura/core/schemas/device_mapping.json +8 -2
- oscura/core/schemas/packet_format.json +24 -4
- oscura/core/schemas/protocol_definition.json +12 -2
- oscura/core/types.py +300 -199
- 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/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/__init__.py +88 -1
- oscura/reporting/automation.py +348 -0
- oscura/reporting/citations.py +374 -0
- oscura/reporting/core.py +54 -0
- oscura/reporting/formatting/__init__.py +11 -0
- oscura/reporting/formatting/measurements.py +320 -0
- oscura/reporting/html.py +57 -0
- oscura/reporting/interpretation.py +431 -0
- oscura/reporting/summary.py +329 -0
- oscura/reporting/templates/enhanced/protocol_re.html +504 -503
- oscura/reporting/visualization.py +542 -0
- 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 +47 -284
- oscura/visualization/batch.py +160 -0
- oscura/visualization/plot.py +542 -53
- oscura/visualization/styles.py +184 -318
- oscura/workflows/__init__.py +2 -0
- oscura/workflows/batch/advanced.py +1 -1
- oscura/workflows/batch/aggregate.py +7 -8
- oscura/workflows/complete_re.py +251 -23
- oscura/workflows/digital.py +27 -4
- oscura/workflows/multi_trace.py +136 -17
- oscura/workflows/waveform.py +788 -0
- {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/METADATA +59 -79
- {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/RECORD +135 -149
- 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.7.0.dist-info → oscura-0.10.0.dist-info}/WHEEL +0 -0
- {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/licenses/LICENSE +0 -0
oscura/visualization/jitter.py
DELETED
|
@@ -1,1042 +0,0 @@
|
|
|
1
|
-
"""Jitter Analysis Visualization Functions.
|
|
2
|
-
|
|
3
|
-
This module provides visualization functions for jitter analysis including
|
|
4
|
-
TIE histograms, bathtub curves, DDJ/DCD plots, and jitter trend analysis.
|
|
5
|
-
|
|
6
|
-
Example:
|
|
7
|
-
>>> from oscura.visualization.jitter import plot_tie_histogram, plot_bathtub_full
|
|
8
|
-
>>> fig = plot_tie_histogram(tie_data)
|
|
9
|
-
>>> fig = plot_bathtub_full(bathtub_result)
|
|
10
|
-
|
|
11
|
-
References:
|
|
12
|
-
- IEEE 802.3: Jitter measurement specifications
|
|
13
|
-
- JEDEC JESD65B: High-Speed Interface Measurements
|
|
14
|
-
"""
|
|
15
|
-
|
|
16
|
-
from __future__ import annotations
|
|
17
|
-
|
|
18
|
-
from pathlib import Path
|
|
19
|
-
from typing import TYPE_CHECKING, Any, cast
|
|
20
|
-
|
|
21
|
-
import numpy as np
|
|
22
|
-
|
|
23
|
-
try:
|
|
24
|
-
import matplotlib.pyplot as plt
|
|
25
|
-
from scipy.stats import norm
|
|
26
|
-
|
|
27
|
-
HAS_MATPLOTLIB = True
|
|
28
|
-
HAS_SCIPY = True
|
|
29
|
-
except ImportError:
|
|
30
|
-
HAS_MATPLOTLIB = False
|
|
31
|
-
HAS_SCIPY = False
|
|
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
|
-
__all__ = [
|
|
39
|
-
"plot_bathtub_full",
|
|
40
|
-
"plot_dcd",
|
|
41
|
-
"plot_ddj",
|
|
42
|
-
"plot_jitter_trend",
|
|
43
|
-
"plot_tie_histogram",
|
|
44
|
-
]
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def _determine_tie_time_unit(
|
|
48
|
-
tie_data: NDArray[np.floating[Any]], time_unit: str
|
|
49
|
-
) -> tuple[str, float]:
|
|
50
|
-
"""Determine time unit and multiplier for TIE display.
|
|
51
|
-
|
|
52
|
-
Args:
|
|
53
|
-
tie_data: TIE values in seconds.
|
|
54
|
-
time_unit: Requested time unit or "auto".
|
|
55
|
-
|
|
56
|
-
Returns:
|
|
57
|
-
Tuple of (time_unit, time_multiplier).
|
|
58
|
-
"""
|
|
59
|
-
if time_unit == "auto":
|
|
60
|
-
max_tie = np.max(np.abs(tie_data))
|
|
61
|
-
if max_tie < 1e-12:
|
|
62
|
-
return "fs", 1e15
|
|
63
|
-
elif max_tie < 1e-9:
|
|
64
|
-
return "ps", 1e12
|
|
65
|
-
elif max_tie < 1e-6:
|
|
66
|
-
return "ns", 1e9
|
|
67
|
-
else:
|
|
68
|
-
return "us", 1e6
|
|
69
|
-
else:
|
|
70
|
-
time_mult_map = {
|
|
71
|
-
"s": 1,
|
|
72
|
-
"ms": 1e3,
|
|
73
|
-
"us": 1e6,
|
|
74
|
-
"ns": 1e9,
|
|
75
|
-
"ps": 1e12,
|
|
76
|
-
"fs": 1e15,
|
|
77
|
-
}
|
|
78
|
-
if time_unit in time_mult_map:
|
|
79
|
-
return time_unit, time_mult_map[time_unit]
|
|
80
|
-
else:
|
|
81
|
-
# Fallback to ps for invalid unit
|
|
82
|
-
return "ps", 1e12
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def _calculate_tie_statistics(
|
|
86
|
-
tie_scaled: NDArray[np.floating[Any]],
|
|
87
|
-
) -> tuple[float, float, float, float]:
|
|
88
|
-
"""Calculate TIE statistical metrics.
|
|
89
|
-
|
|
90
|
-
Args:
|
|
91
|
-
tie_scaled: Scaled TIE values.
|
|
92
|
-
|
|
93
|
-
Returns:
|
|
94
|
-
Tuple of (mean, std, peak-to-peak, rms).
|
|
95
|
-
"""
|
|
96
|
-
mean_val = float(np.mean(tie_scaled))
|
|
97
|
-
std_val = float(np.std(tie_scaled))
|
|
98
|
-
pp_val = float(np.ptp(tie_scaled))
|
|
99
|
-
rms_val = float(np.sqrt(np.mean(tie_scaled**2)))
|
|
100
|
-
return mean_val, std_val, pp_val, rms_val
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
def _add_gaussian_fit(
|
|
104
|
-
ax: Axes, bin_edges: NDArray[np.floating[Any]], mean_val: float, std_val: float, time_unit: str
|
|
105
|
-
) -> None:
|
|
106
|
-
"""Add Gaussian fit overlay to histogram.
|
|
107
|
-
|
|
108
|
-
Args:
|
|
109
|
-
ax: Matplotlib axes to plot on.
|
|
110
|
-
bin_edges: Histogram bin edges.
|
|
111
|
-
mean_val: Mean value.
|
|
112
|
-
std_val: Standard deviation.
|
|
113
|
-
time_unit: Time unit string for label.
|
|
114
|
-
"""
|
|
115
|
-
if not HAS_SCIPY:
|
|
116
|
-
return
|
|
117
|
-
|
|
118
|
-
x_fit = np.linspace(bin_edges[0], bin_edges[-1], 200)
|
|
119
|
-
y_fit = norm.pdf(x_fit, mean_val, std_val)
|
|
120
|
-
ax.plot(
|
|
121
|
-
x_fit, y_fit, "r-", linewidth=2, label=f"Gaussian Fit (sigma={std_val:.2f} {time_unit})"
|
|
122
|
-
)
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
def _add_rj_dj_indicators(ax: Axes, mean_val: float, std_val: float) -> None:
|
|
126
|
-
"""Add RJ/DJ separation indicators to plot.
|
|
127
|
-
|
|
128
|
-
Args:
|
|
129
|
-
ax: Matplotlib axes to plot on.
|
|
130
|
-
mean_val: Mean value.
|
|
131
|
-
std_val: Standard deviation.
|
|
132
|
-
"""
|
|
133
|
-
# Mark ±3sigma region (RJ contribution)
|
|
134
|
-
ax.axvline(mean_val - 3 * std_val, color="#E74C3C", linestyle="--", linewidth=1.5, alpha=0.7)
|
|
135
|
-
ax.axvline(mean_val + 3 * std_val, color="#E74C3C", linestyle="--", linewidth=1.5, alpha=0.7)
|
|
136
|
-
|
|
137
|
-
# Shade RJ region
|
|
138
|
-
ax.axvspan(
|
|
139
|
-
mean_val - 3 * std_val,
|
|
140
|
-
mean_val + 3 * std_val,
|
|
141
|
-
alpha=0.1,
|
|
142
|
-
color="#E74C3C",
|
|
143
|
-
label="±3sigma (99.7% RJ)",
|
|
144
|
-
)
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
def _add_statistics_box(
|
|
148
|
-
ax: Axes, mean_val: float, rms_val: float, std_val: float, pp_val: float, time_unit: str
|
|
149
|
-
) -> None:
|
|
150
|
-
"""Add statistics text box to plot.
|
|
151
|
-
|
|
152
|
-
Args:
|
|
153
|
-
ax: Matplotlib axes to plot on.
|
|
154
|
-
mean_val: Mean value.
|
|
155
|
-
rms_val: RMS value.
|
|
156
|
-
std_val: Standard deviation.
|
|
157
|
-
pp_val: Peak-to-peak value.
|
|
158
|
-
time_unit: Time unit string.
|
|
159
|
-
"""
|
|
160
|
-
stats_text = (
|
|
161
|
-
f"Mean: {mean_val:.2f} {time_unit}\n"
|
|
162
|
-
f"RMS: {rms_val:.2f} {time_unit}\n"
|
|
163
|
-
f"Std Dev: {std_val:.2f} {time_unit}\n"
|
|
164
|
-
f"Peak-Peak: {pp_val:.2f} {time_unit}"
|
|
165
|
-
)
|
|
166
|
-
ax.text(
|
|
167
|
-
0.98,
|
|
168
|
-
0.98,
|
|
169
|
-
stats_text,
|
|
170
|
-
transform=ax.transAxes,
|
|
171
|
-
fontsize=9,
|
|
172
|
-
verticalalignment="top",
|
|
173
|
-
horizontalalignment="right",
|
|
174
|
-
bbox={"boxstyle": "round", "facecolor": "wheat", "alpha": 0.9},
|
|
175
|
-
fontfamily="monospace",
|
|
176
|
-
)
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
def plot_tie_histogram(
|
|
180
|
-
tie_data: NDArray[np.floating[Any]],
|
|
181
|
-
*,
|
|
182
|
-
ax: Axes | None = None,
|
|
183
|
-
figsize: tuple[float, float] = (10, 6),
|
|
184
|
-
title: str | None = None,
|
|
185
|
-
time_unit: str = "auto",
|
|
186
|
-
bins: int | str = "auto",
|
|
187
|
-
show_gaussian_fit: bool = True,
|
|
188
|
-
show_statistics: bool = True,
|
|
189
|
-
show_rj_dj: bool = True,
|
|
190
|
-
show: bool = True,
|
|
191
|
-
save_path: str | Path | None = None,
|
|
192
|
-
) -> Figure:
|
|
193
|
-
"""Plot Time Interval Error (TIE) histogram with statistical analysis.
|
|
194
|
-
|
|
195
|
-
Creates a histogram of TIE values with optional Gaussian fit overlay
|
|
196
|
-
and RJ/DJ decomposition indicators.
|
|
197
|
-
|
|
198
|
-
Args:
|
|
199
|
-
tie_data: Array of TIE values in seconds.
|
|
200
|
-
ax: Matplotlib axes. If None, creates new figure.
|
|
201
|
-
figsize: Figure size in inches.
|
|
202
|
-
title: Plot title.
|
|
203
|
-
time_unit: Time unit ("s", "ms", "us", "ns", "ps", "fs", "auto").
|
|
204
|
-
bins: Number of bins or "auto" for automatic selection.
|
|
205
|
-
show_gaussian_fit: Overlay Gaussian fit for RJ estimation.
|
|
206
|
-
show_statistics: Show statistics box.
|
|
207
|
-
show_rj_dj: Show RJ/DJ separation indicators.
|
|
208
|
-
show: Display plot interactively.
|
|
209
|
-
save_path: Save plot to file.
|
|
210
|
-
|
|
211
|
-
Returns:
|
|
212
|
-
Matplotlib Figure object.
|
|
213
|
-
|
|
214
|
-
Example:
|
|
215
|
-
>>> tie = np.random.randn(10000) * 2e-12 # 2 ps RMS jitter
|
|
216
|
-
>>> fig = plot_tie_histogram(tie, time_unit="ps")
|
|
217
|
-
"""
|
|
218
|
-
if not HAS_MATPLOTLIB:
|
|
219
|
-
raise ImportError("matplotlib is required for visualization")
|
|
220
|
-
|
|
221
|
-
fig, ax = _setup_tie_figure(ax, figsize)
|
|
222
|
-
time_unit, time_mult = _determine_tie_time_unit(tie_data, time_unit)
|
|
223
|
-
tie_scaled = tie_data * time_mult
|
|
224
|
-
mean_val, std_val, pp_val, rms_val = _calculate_tie_statistics(tie_scaled)
|
|
225
|
-
|
|
226
|
-
counts, bin_edges, patches = _plot_tie_histogram_data(ax, tie_scaled, bins)
|
|
227
|
-
_add_tie_overlays(ax, show_gaussian_fit, show_rj_dj, bin_edges, mean_val, std_val, time_unit)
|
|
228
|
-
_format_tie_plot(ax, show_statistics, mean_val, rms_val, std_val, pp_val, time_unit, title)
|
|
229
|
-
|
|
230
|
-
fig.tight_layout()
|
|
231
|
-
_save_and_show_tie_plot(fig, save_path, show)
|
|
232
|
-
|
|
233
|
-
return fig
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
def _setup_tie_figure(ax: Axes | None, figsize: tuple[float, float]) -> tuple[Figure, Axes]:
|
|
237
|
-
"""Setup figure and axes for TIE plot.
|
|
238
|
-
|
|
239
|
-
Args:
|
|
240
|
-
ax: Existing axes or None.
|
|
241
|
-
figsize: Figure size.
|
|
242
|
-
|
|
243
|
-
Returns:
|
|
244
|
-
Tuple of (figure, axes).
|
|
245
|
-
|
|
246
|
-
Raises:
|
|
247
|
-
ValueError: If axes has no figure.
|
|
248
|
-
"""
|
|
249
|
-
if ax is None:
|
|
250
|
-
fig, ax = plt.subplots(figsize=figsize)
|
|
251
|
-
else:
|
|
252
|
-
fig_temp = ax.get_figure()
|
|
253
|
-
if fig_temp is None:
|
|
254
|
-
raise ValueError("Axes must have an associated figure")
|
|
255
|
-
fig = cast("Figure", fig_temp)
|
|
256
|
-
return fig, ax
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
def _plot_tie_histogram_data(
|
|
260
|
-
ax: Axes, tie_scaled: NDArray[np.floating[Any]], bins: int | str
|
|
261
|
-
) -> tuple[Any, NDArray[Any], Any]:
|
|
262
|
-
"""Plot histogram data.
|
|
263
|
-
|
|
264
|
-
Args:
|
|
265
|
-
ax: Matplotlib axes.
|
|
266
|
-
tie_scaled: Scaled TIE data.
|
|
267
|
-
bins: Bin specification.
|
|
268
|
-
|
|
269
|
-
Returns:
|
|
270
|
-
Tuple of (counts, bin_edges, patches) from matplotlib hist.
|
|
271
|
-
"""
|
|
272
|
-
result: tuple[Any, NDArray[Any], Any] = ax.hist(
|
|
273
|
-
tie_scaled,
|
|
274
|
-
bins=bins,
|
|
275
|
-
density=True,
|
|
276
|
-
color="#3498DB",
|
|
277
|
-
alpha=0.7,
|
|
278
|
-
edgecolor="black",
|
|
279
|
-
linewidth=0.5,
|
|
280
|
-
)
|
|
281
|
-
return result
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
def _add_tie_overlays(
|
|
285
|
-
ax: Axes,
|
|
286
|
-
show_gaussian_fit: bool,
|
|
287
|
-
show_rj_dj: bool,
|
|
288
|
-
bin_edges: NDArray[Any],
|
|
289
|
-
mean_val: float,
|
|
290
|
-
std_val: float,
|
|
291
|
-
time_unit: str,
|
|
292
|
-
) -> None:
|
|
293
|
-
"""Add Gaussian fit and RJ/DJ overlays to TIE plot.
|
|
294
|
-
|
|
295
|
-
Args:
|
|
296
|
-
ax: Matplotlib axes.
|
|
297
|
-
show_gaussian_fit: Whether to show Gaussian fit.
|
|
298
|
-
show_rj_dj: Whether to show RJ/DJ indicators.
|
|
299
|
-
bin_edges: Histogram bin edges.
|
|
300
|
-
mean_val: Mean TIE value.
|
|
301
|
-
std_val: Standard deviation.
|
|
302
|
-
time_unit: Time unit string.
|
|
303
|
-
"""
|
|
304
|
-
if show_gaussian_fit:
|
|
305
|
-
_add_gaussian_fit(ax, bin_edges, mean_val, std_val, time_unit)
|
|
306
|
-
if show_rj_dj:
|
|
307
|
-
_add_rj_dj_indicators(ax, mean_val, std_val)
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
def _format_tie_plot(
|
|
311
|
-
ax: Axes,
|
|
312
|
-
show_statistics: bool,
|
|
313
|
-
mean_val: float,
|
|
314
|
-
rms_val: float,
|
|
315
|
-
std_val: float,
|
|
316
|
-
pp_val: float,
|
|
317
|
-
time_unit: str,
|
|
318
|
-
title: str | None,
|
|
319
|
-
) -> None:
|
|
320
|
-
"""Format TIE plot axes and labels.
|
|
321
|
-
|
|
322
|
-
Args:
|
|
323
|
-
ax: Matplotlib axes.
|
|
324
|
-
show_statistics: Whether to show statistics box.
|
|
325
|
-
mean_val: Mean value.
|
|
326
|
-
rms_val: RMS value.
|
|
327
|
-
std_val: Standard deviation.
|
|
328
|
-
pp_val: Peak-to-peak value.
|
|
329
|
-
time_unit: Time unit.
|
|
330
|
-
title: Plot title.
|
|
331
|
-
"""
|
|
332
|
-
if show_statistics:
|
|
333
|
-
_add_statistics_box(ax, mean_val, rms_val, std_val, pp_val, time_unit)
|
|
334
|
-
|
|
335
|
-
ax.set_xlabel(f"TIE ({time_unit})", fontsize=11)
|
|
336
|
-
ax.set_ylabel("Probability Density", fontsize=11)
|
|
337
|
-
ax.grid(True, alpha=0.3)
|
|
338
|
-
ax.legend(loc="upper left")
|
|
339
|
-
|
|
340
|
-
final_title = title if title else "Time Interval Error Distribution"
|
|
341
|
-
ax.set_title(final_title, fontsize=12, fontweight="bold")
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
def _save_and_show_tie_plot(fig: Figure, save_path: str | Path | None, show: bool) -> None:
|
|
345
|
-
"""Save and/or show TIE plot.
|
|
346
|
-
|
|
347
|
-
Args:
|
|
348
|
-
fig: Matplotlib figure.
|
|
349
|
-
save_path: Path to save file.
|
|
350
|
-
show: Whether to display interactively.
|
|
351
|
-
"""
|
|
352
|
-
if save_path is not None:
|
|
353
|
-
fig.savefig(save_path, dpi=300, bbox_inches="tight")
|
|
354
|
-
if show:
|
|
355
|
-
plt.show()
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
def plot_bathtub_full(
|
|
359
|
-
positions: NDArray[np.floating[Any]],
|
|
360
|
-
ber_left: NDArray[np.floating[Any]],
|
|
361
|
-
ber_right: NDArray[np.floating[Any]],
|
|
362
|
-
*,
|
|
363
|
-
ber_total: NDArray[np.floating[Any]] | None = None,
|
|
364
|
-
target_ber: float = 1e-12,
|
|
365
|
-
eye_opening: float | None = None,
|
|
366
|
-
ax: Axes | None = None,
|
|
367
|
-
figsize: tuple[float, float] = (10, 6),
|
|
368
|
-
title: str | None = None,
|
|
369
|
-
show_target: bool = True,
|
|
370
|
-
show_eye_opening: bool = True,
|
|
371
|
-
show: bool = True,
|
|
372
|
-
save_path: str | Path | None = None,
|
|
373
|
-
) -> Figure:
|
|
374
|
-
"""Plot full bathtub curve with left/right BER and eye opening.
|
|
375
|
-
|
|
376
|
-
Creates a bathtub curve showing bit error rate vs sampling position
|
|
377
|
-
within the unit interval, with target BER marker and eye opening
|
|
378
|
-
annotation.
|
|
379
|
-
|
|
380
|
-
Args:
|
|
381
|
-
positions: Sample positions in UI (0 to 1).
|
|
382
|
-
ber_left: Left-side BER values.
|
|
383
|
-
ber_right: Right-side BER values.
|
|
384
|
-
ber_total: Total BER values (optional, computed if not provided).
|
|
385
|
-
target_ber: Target BER for eye opening calculation.
|
|
386
|
-
eye_opening: Pre-calculated eye opening in UI (optional).
|
|
387
|
-
ax: Matplotlib axes.
|
|
388
|
-
figsize: Figure size.
|
|
389
|
-
title: Plot title.
|
|
390
|
-
show_target: Show target BER line.
|
|
391
|
-
show_eye_opening: Annotate eye opening.
|
|
392
|
-
show: Display plot.
|
|
393
|
-
save_path: Save path.
|
|
394
|
-
|
|
395
|
-
Returns:
|
|
396
|
-
Matplotlib Figure object.
|
|
397
|
-
|
|
398
|
-
Example:
|
|
399
|
-
>>> pos = np.linspace(0, 1, 100)
|
|
400
|
-
>>> ber_l = 0.5 * erfc((pos - 0) / 0.1 / np.sqrt(2))
|
|
401
|
-
>>> ber_r = 0.5 * erfc((1 - pos) / 0.1 / np.sqrt(2))
|
|
402
|
-
>>> fig = plot_bathtub_full(pos, ber_l, ber_r, target_ber=1e-12)
|
|
403
|
-
"""
|
|
404
|
-
if not HAS_MATPLOTLIB:
|
|
405
|
-
raise ImportError("matplotlib is required for visualization")
|
|
406
|
-
|
|
407
|
-
fig, ax = _get_or_create_figure(ax, figsize)
|
|
408
|
-
ber_total = ber_total if ber_total is not None else ber_left + ber_right
|
|
409
|
-
|
|
410
|
-
# Plot BER curves
|
|
411
|
-
ber_total_plot = _plot_bathtub_ber_curves(ax, positions, ber_left, ber_right, ber_total)
|
|
412
|
-
|
|
413
|
-
# Optional annotations
|
|
414
|
-
if show_target:
|
|
415
|
-
_add_target_ber_line(ax, target_ber)
|
|
416
|
-
|
|
417
|
-
if show_eye_opening:
|
|
418
|
-
_add_eye_opening_annotation(ax, positions, ber_total_plot, target_ber, eye_opening)
|
|
419
|
-
|
|
420
|
-
# Styling
|
|
421
|
-
_style_bathtub_plot(ax, positions, ber_total_plot, title)
|
|
422
|
-
|
|
423
|
-
fig.tight_layout()
|
|
424
|
-
|
|
425
|
-
if save_path is not None:
|
|
426
|
-
fig.savefig(save_path, dpi=300, bbox_inches="tight")
|
|
427
|
-
|
|
428
|
-
if show:
|
|
429
|
-
plt.show()
|
|
430
|
-
|
|
431
|
-
return fig
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
def _get_or_create_figure(ax: Axes | None, figsize: tuple[float, float]) -> tuple[Figure, Axes]:
|
|
435
|
-
"""Get existing figure or create new one."""
|
|
436
|
-
if ax is None:
|
|
437
|
-
fig, ax = plt.subplots(figsize=figsize)
|
|
438
|
-
else:
|
|
439
|
-
fig_temp = ax.get_figure()
|
|
440
|
-
if fig_temp is None:
|
|
441
|
-
raise ValueError("Axes must have an associated figure")
|
|
442
|
-
fig = cast("Figure", fig_temp)
|
|
443
|
-
return fig, ax
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
def _plot_bathtub_ber_curves(
|
|
447
|
-
ax: Axes,
|
|
448
|
-
positions: NDArray[np.floating[Any]],
|
|
449
|
-
ber_left: NDArray[np.floating[Any]],
|
|
450
|
-
ber_right: NDArray[np.floating[Any]],
|
|
451
|
-
ber_total: NDArray[np.floating[Any]],
|
|
452
|
-
) -> NDArray[np.floating[Any]]:
|
|
453
|
-
"""Plot BER curves and return clipped total BER."""
|
|
454
|
-
ber_left_plot = np.clip(ber_left, 1e-18, 1)
|
|
455
|
-
ber_right_plot = np.clip(ber_right, 1e-18, 1)
|
|
456
|
-
ber_total_plot = np.clip(ber_total, 1e-18, 1)
|
|
457
|
-
|
|
458
|
-
ax.semilogy(positions, ber_left_plot, "b-", linewidth=2, label="BER Left", alpha=0.8)
|
|
459
|
-
ax.semilogy(positions, ber_right_plot, "r-", linewidth=2, label="BER Right", alpha=0.8)
|
|
460
|
-
ax.semilogy(positions, ber_total_plot, "k-", linewidth=2.5, label="BER Total")
|
|
461
|
-
|
|
462
|
-
return ber_total_plot
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
def _add_target_ber_line(ax: Axes, target_ber: float) -> None:
|
|
466
|
-
"""Add target BER horizontal line."""
|
|
467
|
-
ax.axhline(
|
|
468
|
-
target_ber,
|
|
469
|
-
color="#27AE60",
|
|
470
|
-
linestyle="--",
|
|
471
|
-
linewidth=2,
|
|
472
|
-
label=f"Target BER = {target_ber:.0e}",
|
|
473
|
-
)
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
def _add_eye_opening_annotation(
|
|
477
|
-
ax: Axes,
|
|
478
|
-
positions: NDArray[np.floating[Any]],
|
|
479
|
-
ber_total_plot: NDArray[np.floating[Any]],
|
|
480
|
-
target_ber: float,
|
|
481
|
-
eye_opening: float | None,
|
|
482
|
-
) -> None:
|
|
483
|
-
"""Add eye opening annotation if applicable."""
|
|
484
|
-
if eye_opening is None:
|
|
485
|
-
eye_opening = _calculate_eye_opening(positions, ber_total_plot, target_ber)
|
|
486
|
-
|
|
487
|
-
if eye_opening <= 0:
|
|
488
|
-
return
|
|
489
|
-
|
|
490
|
-
center = 0.5
|
|
491
|
-
left_edge = center - eye_opening / 2
|
|
492
|
-
right_edge = center + eye_opening / 2
|
|
493
|
-
|
|
494
|
-
ax.annotate(
|
|
495
|
-
"",
|
|
496
|
-
xy=(right_edge, target_ber),
|
|
497
|
-
xytext=(left_edge, target_ber),
|
|
498
|
-
arrowprops={"arrowstyle": "<->", "color": "#27AE60", "lw": 2},
|
|
499
|
-
)
|
|
500
|
-
ax.text(
|
|
501
|
-
center,
|
|
502
|
-
target_ber * 0.1,
|
|
503
|
-
f"Eye Opening: {eye_opening:.3f} UI",
|
|
504
|
-
ha="center",
|
|
505
|
-
va="top",
|
|
506
|
-
fontsize=10,
|
|
507
|
-
fontweight="bold",
|
|
508
|
-
color="#27AE60",
|
|
509
|
-
)
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
def _calculate_eye_opening(
|
|
513
|
-
positions: NDArray[np.floating[Any]],
|
|
514
|
-
ber_total: NDArray[np.floating[Any]],
|
|
515
|
-
target_ber: float,
|
|
516
|
-
) -> float:
|
|
517
|
-
"""Calculate eye opening at target BER."""
|
|
518
|
-
left_cross = np.where(ber_total < target_ber)[0]
|
|
519
|
-
if len(left_cross) > 0:
|
|
520
|
-
left_edge = positions[left_cross[0]]
|
|
521
|
-
right_edge = positions[left_cross[-1]]
|
|
522
|
-
return float(right_edge - left_edge)
|
|
523
|
-
return 0.0
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
def _style_bathtub_plot(
|
|
527
|
-
ax: Axes,
|
|
528
|
-
positions: NDArray[np.floating[Any]],
|
|
529
|
-
ber_total_plot: NDArray[np.floating[Any]],
|
|
530
|
-
title: str | None,
|
|
531
|
-
) -> None:
|
|
532
|
-
"""Apply styling to bathtub plot."""
|
|
533
|
-
ax.fill_between(positions, 1e-18, ber_total_plot, alpha=0.1, color="gray")
|
|
534
|
-
ax.set_xlabel("Sample Position (UI)", fontsize=11)
|
|
535
|
-
ax.set_ylabel("Bit Error Rate", fontsize=11)
|
|
536
|
-
ax.set_xlim(0, 1)
|
|
537
|
-
ax.set_ylim(1e-15, 1)
|
|
538
|
-
ax.grid(True, which="both", alpha=0.3)
|
|
539
|
-
ax.legend(loc="upper right")
|
|
540
|
-
ax.set_title(title or "Bathtub Curve", fontsize=12, fontweight="bold")
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
def plot_ddj(
|
|
544
|
-
patterns: list[str],
|
|
545
|
-
jitter_values: NDArray[np.floating[Any]],
|
|
546
|
-
*,
|
|
547
|
-
ax: Axes | None = None,
|
|
548
|
-
figsize: tuple[float, float] = (12, 6),
|
|
549
|
-
title: str | None = None,
|
|
550
|
-
time_unit: str = "ps",
|
|
551
|
-
show: bool = True,
|
|
552
|
-
save_path: str | Path | None = None,
|
|
553
|
-
) -> Figure:
|
|
554
|
-
"""Plot Data-Dependent Jitter (DDJ) by bit pattern.
|
|
555
|
-
|
|
556
|
-
Creates a bar chart showing jitter contribution for each bit pattern,
|
|
557
|
-
useful for identifying pattern-dependent timing variations.
|
|
558
|
-
|
|
559
|
-
Args:
|
|
560
|
-
patterns: List of bit pattern strings (e.g., ["010", "011", "100"]).
|
|
561
|
-
jitter_values: Jitter values for each pattern.
|
|
562
|
-
ax: Matplotlib axes.
|
|
563
|
-
figsize: Figure size.
|
|
564
|
-
title: Plot title.
|
|
565
|
-
time_unit: Time unit for display.
|
|
566
|
-
show: Display plot.
|
|
567
|
-
save_path: Save path.
|
|
568
|
-
|
|
569
|
-
Returns:
|
|
570
|
-
Matplotlib Figure object.
|
|
571
|
-
|
|
572
|
-
Example:
|
|
573
|
-
>>> patterns = ["000", "001", "010", "011", "100", "101", "110", "111"]
|
|
574
|
-
>>> ddj = np.array([0, 2.1, -1.5, 0.5, 0.8, -0.3, 1.2, -0.8]) # ps
|
|
575
|
-
>>> fig = plot_ddj(patterns, ddj, time_unit="ps")
|
|
576
|
-
"""
|
|
577
|
-
if not HAS_MATPLOTLIB:
|
|
578
|
-
raise ImportError("matplotlib is required for visualization")
|
|
579
|
-
|
|
580
|
-
# Validate input lengths match
|
|
581
|
-
if len(patterns) != len(jitter_values):
|
|
582
|
-
raise ValueError(
|
|
583
|
-
f"Mismatched lengths: patterns has {len(patterns)} elements "
|
|
584
|
-
f"but jitter_values has {len(jitter_values)} elements"
|
|
585
|
-
)
|
|
586
|
-
|
|
587
|
-
if ax is None:
|
|
588
|
-
fig, ax = plt.subplots(figsize=figsize)
|
|
589
|
-
else:
|
|
590
|
-
fig_temp = ax.get_figure()
|
|
591
|
-
if fig_temp is None:
|
|
592
|
-
raise ValueError("Axes must have an associated figure")
|
|
593
|
-
fig = cast("Figure", fig_temp)
|
|
594
|
-
|
|
595
|
-
# Color bars based on sign
|
|
596
|
-
colors = ["#E74C3C" if v < 0 else "#27AE60" for v in jitter_values]
|
|
597
|
-
|
|
598
|
-
# Bar chart
|
|
599
|
-
x_pos = np.arange(len(patterns))
|
|
600
|
-
ax.bar(x_pos, jitter_values, color=colors, edgecolor="black", linewidth=0.5)
|
|
601
|
-
|
|
602
|
-
# Reference line at zero
|
|
603
|
-
ax.axhline(0, color="gray", linestyle="-", linewidth=1)
|
|
604
|
-
|
|
605
|
-
# Labels
|
|
606
|
-
ax.set_xticks(x_pos)
|
|
607
|
-
ax.set_xticklabels(patterns, fontfamily="monospace", fontsize=10)
|
|
608
|
-
ax.set_xlabel("Bit Pattern", fontsize=11)
|
|
609
|
-
ax.set_ylabel(f"DDJ ({time_unit})", fontsize=11)
|
|
610
|
-
ax.grid(True, axis="y", alpha=0.3)
|
|
611
|
-
|
|
612
|
-
# Add DDJ pp annotation
|
|
613
|
-
ddj_pp = np.ptp(jitter_values)
|
|
614
|
-
ax.text(
|
|
615
|
-
0.98,
|
|
616
|
-
0.98,
|
|
617
|
-
f"DDJ pk-pk: {ddj_pp:.2f} {time_unit}",
|
|
618
|
-
transform=ax.transAxes,
|
|
619
|
-
fontsize=10,
|
|
620
|
-
ha="right",
|
|
621
|
-
va="top",
|
|
622
|
-
bbox={"boxstyle": "round", "facecolor": "wheat", "alpha": 0.9},
|
|
623
|
-
)
|
|
624
|
-
|
|
625
|
-
if title:
|
|
626
|
-
ax.set_title(title, fontsize=12, fontweight="bold")
|
|
627
|
-
else:
|
|
628
|
-
ax.set_title("Data-Dependent Jitter by Pattern", fontsize=12, fontweight="bold")
|
|
629
|
-
|
|
630
|
-
fig.tight_layout()
|
|
631
|
-
|
|
632
|
-
if save_path is not None:
|
|
633
|
-
fig.savefig(save_path, dpi=300, bbox_inches="tight")
|
|
634
|
-
|
|
635
|
-
if show:
|
|
636
|
-
plt.show()
|
|
637
|
-
|
|
638
|
-
return fig
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
def _determine_dcd_time_unit(
|
|
642
|
-
high_times: NDArray[np.floating[Any]], low_times: NDArray[np.floating[Any]], time_unit: str
|
|
643
|
-
) -> tuple[str, float]:
|
|
644
|
-
"""Determine time unit and scaling for DCD plot.
|
|
645
|
-
|
|
646
|
-
Args:
|
|
647
|
-
high_times: High-state durations.
|
|
648
|
-
low_times: Low-state durations.
|
|
649
|
-
time_unit: Requested time unit or "auto".
|
|
650
|
-
|
|
651
|
-
Returns:
|
|
652
|
-
Tuple of (time_unit, time_multiplier).
|
|
653
|
-
"""
|
|
654
|
-
if time_unit == "auto":
|
|
655
|
-
max_time = max(np.max(high_times), np.max(low_times))
|
|
656
|
-
if max_time < 1e-9:
|
|
657
|
-
return "ps", 1e12
|
|
658
|
-
elif max_time < 1e-6:
|
|
659
|
-
return "ns", 1e9
|
|
660
|
-
else:
|
|
661
|
-
return "us", 1e6
|
|
662
|
-
else:
|
|
663
|
-
time_mult = {"s": 1, "ms": 1e3, "us": 1e6, "ns": 1e9, "ps": 1e12}.get(time_unit, 1e9)
|
|
664
|
-
return time_unit, time_mult
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
def _compute_dcd_statistics(
|
|
668
|
-
high_scaled: NDArray[np.floating[Any]], low_scaled: NDArray[np.floating[Any]]
|
|
669
|
-
) -> tuple[float, float, float, float]:
|
|
670
|
-
"""Compute DCD statistics.
|
|
671
|
-
|
|
672
|
-
Args:
|
|
673
|
-
high_scaled: Scaled high-state durations.
|
|
674
|
-
low_scaled: Scaled low-state durations.
|
|
675
|
-
|
|
676
|
-
Returns:
|
|
677
|
-
Tuple of (mean_high, mean_low, duty_cycle, dcd).
|
|
678
|
-
"""
|
|
679
|
-
mean_high = float(np.mean(high_scaled))
|
|
680
|
-
mean_low = float(np.mean(low_scaled))
|
|
681
|
-
period = mean_high + mean_low
|
|
682
|
-
duty_cycle = mean_high / period * 100
|
|
683
|
-
dcd = (mean_high - mean_low) / 2
|
|
684
|
-
return mean_high, mean_low, duty_cycle, dcd
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
def _plot_dcd_histograms(
|
|
688
|
-
ax: Axes,
|
|
689
|
-
high_scaled: NDArray[np.floating[Any]],
|
|
690
|
-
low_scaled: NDArray[np.floating[Any]],
|
|
691
|
-
mean_high: float,
|
|
692
|
-
mean_low: float,
|
|
693
|
-
) -> None:
|
|
694
|
-
"""Plot DCD histograms with mean lines.
|
|
695
|
-
|
|
696
|
-
Args:
|
|
697
|
-
ax: Matplotlib axes.
|
|
698
|
-
high_scaled: Scaled high-state durations.
|
|
699
|
-
low_scaled: Scaled low-state durations.
|
|
700
|
-
mean_high: Mean high value.
|
|
701
|
-
mean_low: Mean low value.
|
|
702
|
-
"""
|
|
703
|
-
all_times = np.concatenate([high_scaled, low_scaled])
|
|
704
|
-
bins = np.linspace(np.min(all_times) * 0.95, np.max(all_times) * 1.05, 50)
|
|
705
|
-
|
|
706
|
-
ax.hist(
|
|
707
|
-
high_scaled,
|
|
708
|
-
bins=bins,
|
|
709
|
-
alpha=0.6,
|
|
710
|
-
color="#E74C3C",
|
|
711
|
-
label="High Time",
|
|
712
|
-
edgecolor="black",
|
|
713
|
-
linewidth=0.5,
|
|
714
|
-
)
|
|
715
|
-
ax.hist(
|
|
716
|
-
low_scaled,
|
|
717
|
-
bins=bins,
|
|
718
|
-
alpha=0.6,
|
|
719
|
-
color="#3498DB",
|
|
720
|
-
label="Low Time",
|
|
721
|
-
edgecolor="black",
|
|
722
|
-
linewidth=0.5,
|
|
723
|
-
)
|
|
724
|
-
|
|
725
|
-
ax.axvline(mean_high, color="#E74C3C", linestyle="--", linewidth=2, alpha=0.8)
|
|
726
|
-
ax.axvline(mean_low, color="#3498DB", linestyle="--", linewidth=2, alpha=0.8)
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
def plot_dcd(
|
|
730
|
-
high_times: NDArray[np.floating[Any]],
|
|
731
|
-
low_times: NDArray[np.floating[Any]],
|
|
732
|
-
*,
|
|
733
|
-
ax: Axes | None = None,
|
|
734
|
-
figsize: tuple[float, float] = (10, 6),
|
|
735
|
-
title: str | None = None,
|
|
736
|
-
time_unit: str = "auto",
|
|
737
|
-
show: bool = True,
|
|
738
|
-
save_path: str | Path | None = None,
|
|
739
|
-
) -> Figure:
|
|
740
|
-
"""Plot Duty Cycle Distortion (DCD) analysis.
|
|
741
|
-
|
|
742
|
-
Creates overlaid histograms of high and low pulse times to visualize
|
|
743
|
-
duty cycle distortion.
|
|
744
|
-
|
|
745
|
-
Args:
|
|
746
|
-
high_times: Array of high-state durations.
|
|
747
|
-
low_times: Array of low-state durations.
|
|
748
|
-
ax: Matplotlib axes.
|
|
749
|
-
figsize: Figure size.
|
|
750
|
-
title: Plot title.
|
|
751
|
-
time_unit: Time unit.
|
|
752
|
-
show: Display plot.
|
|
753
|
-
save_path: Save path.
|
|
754
|
-
|
|
755
|
-
Returns:
|
|
756
|
-
Matplotlib Figure object.
|
|
757
|
-
"""
|
|
758
|
-
if not HAS_MATPLOTLIB:
|
|
759
|
-
raise ImportError("matplotlib is required for visualization")
|
|
760
|
-
|
|
761
|
-
fig, ax = _get_or_create_figure(ax, figsize)
|
|
762
|
-
|
|
763
|
-
# Scale times
|
|
764
|
-
time_unit, time_mult = _determine_dcd_time_unit(high_times, low_times, time_unit)
|
|
765
|
-
high_scaled = high_times * time_mult
|
|
766
|
-
low_scaled = low_times * time_mult
|
|
767
|
-
|
|
768
|
-
# Calculate statistics
|
|
769
|
-
mean_high, mean_low, duty_cycle, dcd = _compute_dcd_statistics(high_scaled, low_scaled)
|
|
770
|
-
|
|
771
|
-
# Plot histograms
|
|
772
|
-
_plot_dcd_histograms(ax, high_scaled, low_scaled, mean_high, mean_low)
|
|
773
|
-
|
|
774
|
-
# Statistics box
|
|
775
|
-
stats_text = (
|
|
776
|
-
f"Mean High: {mean_high:.2f} {time_unit}\n"
|
|
777
|
-
f"Mean Low: {mean_low:.2f} {time_unit}\n"
|
|
778
|
-
f"Duty Cycle: {duty_cycle:.1f}%\n"
|
|
779
|
-
f"DCD: {dcd:.2f} {time_unit}"
|
|
780
|
-
)
|
|
781
|
-
ax.text(
|
|
782
|
-
0.98,
|
|
783
|
-
0.98,
|
|
784
|
-
stats_text,
|
|
785
|
-
transform=ax.transAxes,
|
|
786
|
-
fontsize=9,
|
|
787
|
-
va="top",
|
|
788
|
-
ha="right",
|
|
789
|
-
bbox={"boxstyle": "round", "facecolor": "wheat", "alpha": 0.9},
|
|
790
|
-
fontfamily="monospace",
|
|
791
|
-
)
|
|
792
|
-
|
|
793
|
-
ax.set_xlabel(f"Pulse Width ({time_unit})", fontsize=11)
|
|
794
|
-
ax.set_ylabel("Count", fontsize=11)
|
|
795
|
-
ax.grid(True, alpha=0.3)
|
|
796
|
-
ax.legend(loc="upper left")
|
|
797
|
-
|
|
798
|
-
if title:
|
|
799
|
-
ax.set_title(title, fontsize=12, fontweight="bold")
|
|
800
|
-
else:
|
|
801
|
-
ax.set_title("Duty Cycle Distortion Analysis", fontsize=12, fontweight="bold")
|
|
802
|
-
|
|
803
|
-
fig.tight_layout()
|
|
804
|
-
|
|
805
|
-
if save_path is not None:
|
|
806
|
-
fig.savefig(save_path, dpi=300, bbox_inches="tight")
|
|
807
|
-
|
|
808
|
-
if show:
|
|
809
|
-
plt.show()
|
|
810
|
-
|
|
811
|
-
return fig
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
def plot_jitter_trend(
|
|
815
|
-
time_axis: NDArray[np.floating[Any]],
|
|
816
|
-
jitter_values: NDArray[np.floating[Any]],
|
|
817
|
-
*,
|
|
818
|
-
ax: Axes | None = None,
|
|
819
|
-
figsize: tuple[float, float] = (12, 5),
|
|
820
|
-
title: str | None = None,
|
|
821
|
-
time_unit: str = "auto",
|
|
822
|
-
jitter_unit: str = "auto",
|
|
823
|
-
show_trend: bool = True,
|
|
824
|
-
show_bounds: bool = True,
|
|
825
|
-
show: bool = True,
|
|
826
|
-
save_path: str | Path | None = None,
|
|
827
|
-
) -> Figure:
|
|
828
|
-
"""Plot jitter trend over time.
|
|
829
|
-
|
|
830
|
-
Creates a time series plot of jitter values with optional trend line
|
|
831
|
-
and statistical bounds.
|
|
832
|
-
|
|
833
|
-
Args:
|
|
834
|
-
time_axis: Time values (e.g., cycle number or time in seconds).
|
|
835
|
-
jitter_values: Jitter values at each time point.
|
|
836
|
-
ax: Matplotlib axes.
|
|
837
|
-
figsize: Figure size.
|
|
838
|
-
title: Plot title.
|
|
839
|
-
time_unit: Time axis unit.
|
|
840
|
-
jitter_unit: Jitter axis unit.
|
|
841
|
-
show_trend: Show linear trend line.
|
|
842
|
-
show_bounds: Show ±3σ bounds.
|
|
843
|
-
show: Display plot.
|
|
844
|
-
save_path: Save path.
|
|
845
|
-
|
|
846
|
-
Returns:
|
|
847
|
-
Matplotlib Figure object.
|
|
848
|
-
"""
|
|
849
|
-
if not HAS_MATPLOTLIB:
|
|
850
|
-
raise ImportError("matplotlib is required for visualization")
|
|
851
|
-
|
|
852
|
-
fig, ax = _setup_jitter_trend_figure(ax, figsize)
|
|
853
|
-
jitter_unit, jitter_mult = _determine_jitter_unit(jitter_values, jitter_unit)
|
|
854
|
-
jitter_scaled = jitter_values * jitter_mult
|
|
855
|
-
|
|
856
|
-
mean_val, std_val = _plot_jitter_data(ax, time_axis, jitter_scaled, jitter_unit)
|
|
857
|
-
_add_jitter_bounds(ax, time_axis, mean_val, std_val, jitter_unit, show_bounds)
|
|
858
|
-
_add_jitter_trend(ax, time_axis, jitter_scaled, jitter_unit, show_trend)
|
|
859
|
-
_format_jitter_trend_plot(ax, time_unit, jitter_unit, title)
|
|
860
|
-
|
|
861
|
-
fig.tight_layout()
|
|
862
|
-
_save_and_show_jitter_trend(fig, save_path, show)
|
|
863
|
-
|
|
864
|
-
return fig
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
def _setup_jitter_trend_figure(
|
|
868
|
-
ax: Axes | None, figsize: tuple[float, float]
|
|
869
|
-
) -> tuple[Figure, Axes]:
|
|
870
|
-
"""Setup figure and axes for jitter trend plot.
|
|
871
|
-
|
|
872
|
-
Args:
|
|
873
|
-
ax: Existing axes or None.
|
|
874
|
-
figsize: Figure size.
|
|
875
|
-
|
|
876
|
-
Returns:
|
|
877
|
-
Tuple of (figure, axes).
|
|
878
|
-
|
|
879
|
-
Raises:
|
|
880
|
-
ValueError: If axes has no figure.
|
|
881
|
-
"""
|
|
882
|
-
if ax is None:
|
|
883
|
-
fig, ax = plt.subplots(figsize=figsize)
|
|
884
|
-
else:
|
|
885
|
-
fig_temp = ax.get_figure()
|
|
886
|
-
if fig_temp is None:
|
|
887
|
-
raise ValueError("Axes must have an associated figure")
|
|
888
|
-
fig = cast("Figure", fig_temp)
|
|
889
|
-
return fig, ax
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
def _determine_jitter_unit(
|
|
893
|
-
jitter_values: NDArray[np.floating[Any]], jitter_unit: str
|
|
894
|
-
) -> tuple[str, float]:
|
|
895
|
-
"""Determine jitter unit and multiplier.
|
|
896
|
-
|
|
897
|
-
Args:
|
|
898
|
-
jitter_values: Jitter value array.
|
|
899
|
-
jitter_unit: Requested unit or "auto".
|
|
900
|
-
|
|
901
|
-
Returns:
|
|
902
|
-
Tuple of (unit_str, multiplier).
|
|
903
|
-
"""
|
|
904
|
-
if jitter_unit == "auto":
|
|
905
|
-
max_jitter = np.max(np.abs(jitter_values))
|
|
906
|
-
if max_jitter < 1e-9:
|
|
907
|
-
return "ps", 1e12
|
|
908
|
-
elif max_jitter < 1e-6:
|
|
909
|
-
return "ns", 1e9
|
|
910
|
-
else:
|
|
911
|
-
return "us", 1e6
|
|
912
|
-
else:
|
|
913
|
-
jitter_mult = {"s": 1, "ms": 1e3, "us": 1e6, "ns": 1e9, "ps": 1e12}.get(jitter_unit, 1e12)
|
|
914
|
-
return jitter_unit, jitter_mult
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
def _plot_jitter_data(
|
|
918
|
-
ax: Axes,
|
|
919
|
-
time_axis: NDArray[np.floating[Any]],
|
|
920
|
-
jitter_scaled: NDArray[np.floating[Any]],
|
|
921
|
-
jitter_unit: str,
|
|
922
|
-
) -> tuple[float, float]:
|
|
923
|
-
"""Plot jitter data and mean line.
|
|
924
|
-
|
|
925
|
-
Args:
|
|
926
|
-
ax: Matplotlib axes.
|
|
927
|
-
time_axis: Time array.
|
|
928
|
-
jitter_scaled: Scaled jitter values.
|
|
929
|
-
jitter_unit: Jitter unit string.
|
|
930
|
-
|
|
931
|
-
Returns:
|
|
932
|
-
Tuple of (mean_val, std_val).
|
|
933
|
-
"""
|
|
934
|
-
ax.plot(time_axis, jitter_scaled, "b-", linewidth=0.8, alpha=0.7, label="Jitter")
|
|
935
|
-
|
|
936
|
-
mean_val = float(np.mean(jitter_scaled))
|
|
937
|
-
std_val = float(np.std(jitter_scaled))
|
|
938
|
-
|
|
939
|
-
ax.axhline(
|
|
940
|
-
mean_val,
|
|
941
|
-
color="gray",
|
|
942
|
-
linestyle="-",
|
|
943
|
-
linewidth=1,
|
|
944
|
-
label=f"Mean: {mean_val:.2f} {jitter_unit}",
|
|
945
|
-
)
|
|
946
|
-
|
|
947
|
-
return mean_val, std_val
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
def _add_jitter_bounds(
|
|
951
|
-
ax: Axes,
|
|
952
|
-
time_axis: NDArray[np.floating[Any]],
|
|
953
|
-
mean_val: float,
|
|
954
|
-
std_val: float,
|
|
955
|
-
jitter_unit: str,
|
|
956
|
-
show_bounds: bool,
|
|
957
|
-
) -> None:
|
|
958
|
-
"""Add statistical bounds to plot.
|
|
959
|
-
|
|
960
|
-
Args:
|
|
961
|
-
ax: Matplotlib axes.
|
|
962
|
-
time_axis: Time array.
|
|
963
|
-
mean_val: Mean value.
|
|
964
|
-
std_val: Standard deviation.
|
|
965
|
-
jitter_unit: Unit string.
|
|
966
|
-
show_bounds: Whether to show bounds.
|
|
967
|
-
"""
|
|
968
|
-
if not show_bounds:
|
|
969
|
-
return
|
|
970
|
-
|
|
971
|
-
ax.axhline(mean_val + 3 * std_val, color="#E74C3C", linestyle="--", linewidth=1, alpha=0.7)
|
|
972
|
-
ax.axhline(
|
|
973
|
-
mean_val - 3 * std_val,
|
|
974
|
-
color="#E74C3C",
|
|
975
|
-
linestyle="--",
|
|
976
|
-
linewidth=1,
|
|
977
|
-
alpha=0.7,
|
|
978
|
-
label=f"±3sigma: {3 * std_val:.2f} {jitter_unit}",
|
|
979
|
-
)
|
|
980
|
-
ax.fill_between(
|
|
981
|
-
time_axis, mean_val - 3 * std_val, mean_val + 3 * std_val, alpha=0.1, color="#E74C3C"
|
|
982
|
-
)
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
def _add_jitter_trend(
|
|
986
|
-
ax: Axes,
|
|
987
|
-
time_axis: NDArray[np.floating[Any]],
|
|
988
|
-
jitter_scaled: NDArray[np.floating[Any]],
|
|
989
|
-
jitter_unit: str,
|
|
990
|
-
show_trend: bool,
|
|
991
|
-
) -> None:
|
|
992
|
-
"""Add trend line to plot.
|
|
993
|
-
|
|
994
|
-
Args:
|
|
995
|
-
ax: Matplotlib axes.
|
|
996
|
-
time_axis: Time array.
|
|
997
|
-
jitter_scaled: Scaled jitter values.
|
|
998
|
-
jitter_unit: Unit string.
|
|
999
|
-
show_trend: Whether to show trend.
|
|
1000
|
-
"""
|
|
1001
|
-
if not show_trend:
|
|
1002
|
-
return
|
|
1003
|
-
|
|
1004
|
-
z = np.polyfit(time_axis, jitter_scaled, 1)
|
|
1005
|
-
p = np.poly1d(z)
|
|
1006
|
-
ax.plot(
|
|
1007
|
-
time_axis, p(time_axis), "g-", linewidth=2, label=f"Trend: {z[0]:.2e} {jitter_unit}/unit"
|
|
1008
|
-
)
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
def _format_jitter_trend_plot(
|
|
1012
|
-
ax: Axes, time_unit: str, jitter_unit: str, title: str | None
|
|
1013
|
-
) -> None:
|
|
1014
|
-
"""Format jitter trend plot axes and labels.
|
|
1015
|
-
|
|
1016
|
-
Args:
|
|
1017
|
-
ax: Matplotlib axes.
|
|
1018
|
-
time_unit: Time unit string.
|
|
1019
|
-
jitter_unit: Jitter unit string.
|
|
1020
|
-
title: Plot title.
|
|
1021
|
-
"""
|
|
1022
|
-
ax.set_xlabel(f"Time ({time_unit})" if time_unit != "auto" else "Sample Index", fontsize=11)
|
|
1023
|
-
ax.set_ylabel(f"Jitter ({jitter_unit})", fontsize=11)
|
|
1024
|
-
ax.grid(True, alpha=0.3)
|
|
1025
|
-
ax.legend(loc="upper right")
|
|
1026
|
-
|
|
1027
|
-
final_title = title if title else "Jitter Trend Analysis"
|
|
1028
|
-
ax.set_title(final_title, fontsize=12, fontweight="bold")
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
def _save_and_show_jitter_trend(fig: Figure, save_path: str | Path | None, show: bool) -> None:
|
|
1032
|
-
"""Save and/or show jitter trend plot.
|
|
1033
|
-
|
|
1034
|
-
Args:
|
|
1035
|
-
fig: Matplotlib figure.
|
|
1036
|
-
save_path: Path to save file.
|
|
1037
|
-
show: Whether to display interactively.
|
|
1038
|
-
"""
|
|
1039
|
-
if save_path is not None:
|
|
1040
|
-
fig.savefig(save_path, dpi=300, bbox_inches="tight")
|
|
1041
|
-
if show:
|
|
1042
|
-
plt.show()
|