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
|
@@ -1,989 +0,0 @@
|
|
|
1
|
-
"""Signal Integrity Visualization Functions.
|
|
2
|
-
|
|
3
|
-
This module provides visualization functions for signal integrity analysis
|
|
4
|
-
including TDR impedance plots, S-parameter displays, setup/hold timing
|
|
5
|
-
diagrams, and eye diagram enhancements.
|
|
6
|
-
|
|
7
|
-
Example:
|
|
8
|
-
>>> from oscura.visualization.signal_integrity import plot_tdr, plot_sparams
|
|
9
|
-
>>> fig = plot_tdr(impedance_profile, distance_axis)
|
|
10
|
-
>>> fig = plot_sparams(frequencies, s11, s21)
|
|
11
|
-
|
|
12
|
-
References:
|
|
13
|
-
- IEEE 370-2020: Electrical Characterization of Printed Circuit Board
|
|
14
|
-
- TDR impedance measurement best practices
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
|
-
from __future__ import annotations
|
|
18
|
-
|
|
19
|
-
from pathlib import Path
|
|
20
|
-
from typing import TYPE_CHECKING, Any, cast
|
|
21
|
-
|
|
22
|
-
import numpy as np
|
|
23
|
-
|
|
24
|
-
try:
|
|
25
|
-
import matplotlib.pyplot as plt
|
|
26
|
-
|
|
27
|
-
HAS_MATPLOTLIB = True
|
|
28
|
-
except ImportError:
|
|
29
|
-
HAS_MATPLOTLIB = False
|
|
30
|
-
|
|
31
|
-
if TYPE_CHECKING:
|
|
32
|
-
from matplotlib.axes import Axes
|
|
33
|
-
from matplotlib.figure import Figure
|
|
34
|
-
from numpy.typing import NDArray
|
|
35
|
-
|
|
36
|
-
__all__ = [
|
|
37
|
-
"plot_setup_hold_timing",
|
|
38
|
-
"plot_sparams_magnitude",
|
|
39
|
-
"plot_sparams_phase",
|
|
40
|
-
"plot_tdr",
|
|
41
|
-
"plot_timing_margin",
|
|
42
|
-
]
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def plot_tdr(
|
|
46
|
-
impedance: NDArray[np.floating[Any]],
|
|
47
|
-
distance: NDArray[np.floating[Any]],
|
|
48
|
-
*,
|
|
49
|
-
z0: float = 50.0,
|
|
50
|
-
ax: Axes | None = None,
|
|
51
|
-
figsize: tuple[float, float] = (12, 6),
|
|
52
|
-
title: str | None = None,
|
|
53
|
-
distance_unit: str = "auto",
|
|
54
|
-
show_reference: bool = True,
|
|
55
|
-
show_discontinuities: bool = True,
|
|
56
|
-
discontinuity_threshold: float = 5.0,
|
|
57
|
-
show: bool = True,
|
|
58
|
-
save_path: str | Path | None = None,
|
|
59
|
-
) -> Figure:
|
|
60
|
-
"""Plot TDR impedance profile vs distance.
|
|
61
|
-
|
|
62
|
-
Creates a Time Domain Reflectometry impedance plot showing impedance
|
|
63
|
-
as a function of distance along a transmission line, with annotations
|
|
64
|
-
for discontinuities and reference impedance.
|
|
65
|
-
|
|
66
|
-
Args:
|
|
67
|
-
impedance: Impedance values in Ohms.
|
|
68
|
-
distance: Distance values (in meters).
|
|
69
|
-
z0: Reference impedance (Ohms) for the reference line.
|
|
70
|
-
ax: Matplotlib axes. If None, creates new figure.
|
|
71
|
-
figsize: Figure size in inches (only if ax is None).
|
|
72
|
-
title: Plot title.
|
|
73
|
-
distance_unit: Distance unit ("m", "cm", "mm", "auto").
|
|
74
|
-
show_reference: Show reference impedance line at z0.
|
|
75
|
-
show_discontinuities: Annotate significant discontinuities.
|
|
76
|
-
discontinuity_threshold: Impedance change threshold (Ohms) for marking.
|
|
77
|
-
show: Display plot interactively.
|
|
78
|
-
save_path: Save plot to file.
|
|
79
|
-
|
|
80
|
-
Returns:
|
|
81
|
-
Matplotlib Figure object.
|
|
82
|
-
|
|
83
|
-
Raises:
|
|
84
|
-
ImportError: If matplotlib is not available.
|
|
85
|
-
ValueError: If input arrays have different lengths.
|
|
86
|
-
|
|
87
|
-
Example:
|
|
88
|
-
>>> z_profile = np.array([50, 50, 75, 75, 50, 50])
|
|
89
|
-
>>> dist = np.linspace(0, 0.5, 6) # 0 to 50 cm
|
|
90
|
-
>>> fig = plot_tdr(z_profile, dist, z0=50, show=False)
|
|
91
|
-
>>> fig.savefig("tdr_impedance.png")
|
|
92
|
-
"""
|
|
93
|
-
_validate_tdr_inputs(impedance, distance)
|
|
94
|
-
fig, ax = _setup_tdr_figure(ax, figsize)
|
|
95
|
-
|
|
96
|
-
distance_unit_final, dist_scaled = _scale_tdr_distance(distance, distance_unit)
|
|
97
|
-
impedance_display = np.clip(impedance, 0, 500)
|
|
98
|
-
|
|
99
|
-
_plot_tdr_impedance_profile(ax, dist_scaled, impedance_display)
|
|
100
|
-
_fill_tdr_impedance_regions(ax, dist_scaled, impedance_display, z0, discontinuity_threshold)
|
|
101
|
-
|
|
102
|
-
if show_reference:
|
|
103
|
-
_add_tdr_reference_line(ax, z0)
|
|
104
|
-
|
|
105
|
-
if show_discontinuities:
|
|
106
|
-
_annotate_tdr_discontinuities(ax, dist_scaled, impedance_display, discontinuity_threshold)
|
|
107
|
-
|
|
108
|
-
_format_tdr_axes(ax, dist_scaled, impedance_display, distance_unit_final, title)
|
|
109
|
-
_finalize_tdr_plot(fig, save_path, show)
|
|
110
|
-
|
|
111
|
-
return fig
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
def _validate_tdr_inputs(
|
|
115
|
-
impedance: NDArray[np.floating[Any]], distance: NDArray[np.floating[Any]]
|
|
116
|
-
) -> None:
|
|
117
|
-
"""Validate TDR input arrays."""
|
|
118
|
-
if not HAS_MATPLOTLIB:
|
|
119
|
-
raise ImportError("matplotlib is required for visualization")
|
|
120
|
-
|
|
121
|
-
if len(impedance) != len(distance):
|
|
122
|
-
raise ValueError(
|
|
123
|
-
f"impedance and distance must have same length "
|
|
124
|
-
f"(got {len(impedance)} and {len(distance)})"
|
|
125
|
-
)
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
def _setup_tdr_figure(ax: Axes | None, figsize: tuple[float, float]) -> tuple[Figure, Axes]:
|
|
129
|
-
"""Setup TDR figure and axes."""
|
|
130
|
-
if ax is None:
|
|
131
|
-
fig, ax_new = plt.subplots(figsize=figsize)
|
|
132
|
-
return fig, ax_new
|
|
133
|
-
|
|
134
|
-
fig_temp = ax.get_figure()
|
|
135
|
-
if fig_temp is None:
|
|
136
|
-
raise ValueError("Axes must have an associated figure")
|
|
137
|
-
return cast("Figure", fig_temp), ax
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
def _scale_tdr_distance(
|
|
141
|
-
distance: NDArray[np.floating[Any]], distance_unit: str
|
|
142
|
-
) -> tuple[str, NDArray[np.floating[Any]]]:
|
|
143
|
-
"""Scale distance to appropriate unit."""
|
|
144
|
-
if distance_unit == "auto":
|
|
145
|
-
max_dist = np.max(distance)
|
|
146
|
-
if max_dist < 0.01:
|
|
147
|
-
distance_unit = "mm"
|
|
148
|
-
distance_mult = 1000.0
|
|
149
|
-
elif max_dist < 1.0:
|
|
150
|
-
distance_unit = "cm"
|
|
151
|
-
distance_mult = 100.0
|
|
152
|
-
else:
|
|
153
|
-
distance_unit = "m"
|
|
154
|
-
distance_mult = 1.0
|
|
155
|
-
else:
|
|
156
|
-
distance_mult = {"m": 1.0, "cm": 100.0, "mm": 1000.0}.get(distance_unit, 1.0)
|
|
157
|
-
|
|
158
|
-
dist_scaled = distance * distance_mult
|
|
159
|
-
return distance_unit, dist_scaled
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
def _plot_tdr_impedance_profile(
|
|
163
|
-
ax: Axes,
|
|
164
|
-
dist_scaled: NDArray[np.floating[Any]],
|
|
165
|
-
impedance_display: NDArray[np.floating[Any]],
|
|
166
|
-
) -> None:
|
|
167
|
-
"""Plot main impedance profile line."""
|
|
168
|
-
ax.plot(dist_scaled, impedance_display, "b-", linewidth=2, label="Impedance")
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
def _fill_tdr_impedance_regions(
|
|
172
|
-
ax: Axes,
|
|
173
|
-
dist_scaled: NDArray[np.floating[Any]],
|
|
174
|
-
impedance_display: NDArray[np.floating[Any]],
|
|
175
|
-
z0: float,
|
|
176
|
-
discontinuity_threshold: float,
|
|
177
|
-
) -> None:
|
|
178
|
-
"""Fill colored regions based on impedance deviation."""
|
|
179
|
-
for i in range(len(dist_scaled) - 1):
|
|
180
|
-
z = impedance_display[i]
|
|
181
|
-
color, alpha = _get_tdr_region_color(z, z0, discontinuity_threshold)
|
|
182
|
-
|
|
183
|
-
ax.fill_between(
|
|
184
|
-
[dist_scaled[i], dist_scaled[i + 1]],
|
|
185
|
-
[z0, z0],
|
|
186
|
-
[z, impedance_display[i + 1]],
|
|
187
|
-
color=color,
|
|
188
|
-
alpha=alpha,
|
|
189
|
-
)
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
def _get_tdr_region_color(z: float, z0: float, threshold: float) -> tuple[str, float]:
|
|
193
|
-
"""Get color and alpha for impedance region."""
|
|
194
|
-
if z > z0 + threshold:
|
|
195
|
-
return "#FFA500", 0.3 # Orange for high-Z
|
|
196
|
-
elif z < z0 - threshold:
|
|
197
|
-
return "#1E90FF", 0.3 # Blue for low-Z
|
|
198
|
-
else:
|
|
199
|
-
return "#90EE90", 0.2 # Light green for matched
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
def _add_tdr_reference_line(ax: Axes, z0: float) -> None:
|
|
203
|
-
"""Add reference impedance line."""
|
|
204
|
-
ax.axhline(z0, color="gray", linestyle="--", linewidth=1.5, label=f"Z0 = {z0} Ω")
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
def _annotate_tdr_discontinuities(
|
|
208
|
-
ax: Axes,
|
|
209
|
-
dist_scaled: NDArray[np.floating[Any]],
|
|
210
|
-
impedance_display: NDArray[np.floating[Any]],
|
|
211
|
-
discontinuity_threshold: float,
|
|
212
|
-
) -> None:
|
|
213
|
-
"""Find and annotate impedance discontinuities."""
|
|
214
|
-
z_diff = np.abs(np.diff(impedance_display))
|
|
215
|
-
discontinuities = np.where(z_diff > discontinuity_threshold)[0]
|
|
216
|
-
|
|
217
|
-
for idx in discontinuities:
|
|
218
|
-
z_before = impedance_display[idx]
|
|
219
|
-
z_after = impedance_display[idx + 1]
|
|
220
|
-
d = dist_scaled[idx]
|
|
221
|
-
|
|
222
|
-
disc_type, color = _classify_tdr_discontinuity(z_before, z_after, discontinuity_threshold)
|
|
223
|
-
if disc_type is None:
|
|
224
|
-
continue
|
|
225
|
-
|
|
226
|
-
_add_tdr_discontinuity_marker(ax, d, z_after, disc_type, color)
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
def _classify_tdr_discontinuity(
|
|
230
|
-
z_before: float, z_after: float, threshold: float
|
|
231
|
-
) -> tuple[str | None, str]:
|
|
232
|
-
"""Classify discontinuity type."""
|
|
233
|
-
if z_after > z_before + threshold:
|
|
234
|
-
return "High-Z", "orange"
|
|
235
|
-
elif z_after < z_before - threshold:
|
|
236
|
-
return "Low-Z", "blue"
|
|
237
|
-
return None, ""
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
def _add_tdr_discontinuity_marker(
|
|
241
|
-
ax: Axes, d: float, z_after: float, disc_type: str, color: str
|
|
242
|
-
) -> None:
|
|
243
|
-
"""Add marker and annotation for discontinuity."""
|
|
244
|
-
ax.plot(d, z_after, "o", color=color, markersize=8)
|
|
245
|
-
|
|
246
|
-
z_str = f"{z_after:.0f}" if z_after < 500 else "Open"
|
|
247
|
-
ax.annotate(
|
|
248
|
-
f"{disc_type}\n{z_str} Ω",
|
|
249
|
-
xy=(d, z_after),
|
|
250
|
-
xytext=(10, 10),
|
|
251
|
-
textcoords="offset points",
|
|
252
|
-
fontsize=8,
|
|
253
|
-
ha="left",
|
|
254
|
-
bbox={"boxstyle": "round,pad=0.3", "facecolor": "white", "alpha": 0.8},
|
|
255
|
-
)
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
def _format_tdr_axes(
|
|
259
|
-
ax: Axes,
|
|
260
|
-
dist_scaled: NDArray[np.floating[Any]],
|
|
261
|
-
impedance_display: NDArray[np.floating[Any]],
|
|
262
|
-
distance_unit: str,
|
|
263
|
-
title: str | None,
|
|
264
|
-
) -> None:
|
|
265
|
-
"""Format axes labels, limits, and title."""
|
|
266
|
-
ax.set_xlabel(f"Distance ({distance_unit})", fontsize=11)
|
|
267
|
-
ax.set_ylabel("Impedance (Ω)", fontsize=11)
|
|
268
|
-
ax.set_xlim(0, dist_scaled[-1])
|
|
269
|
-
|
|
270
|
-
y_min = max(0, np.min(impedance_display) - 10)
|
|
271
|
-
y_max = min(200, np.max(impedance_display) + 10)
|
|
272
|
-
ax.set_ylim(y_min, y_max)
|
|
273
|
-
|
|
274
|
-
ax.grid(True, alpha=0.3)
|
|
275
|
-
ax.legend(loc="upper right")
|
|
276
|
-
|
|
277
|
-
title_text = title if title else "TDR Impedance Profile"
|
|
278
|
-
ax.set_title(title_text, fontsize=12, fontweight="bold")
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
def _finalize_tdr_plot(fig: Figure, save_path: str | Path | None, show: bool) -> None:
|
|
282
|
-
"""Finalize plot layout, save, and show."""
|
|
283
|
-
fig.tight_layout()
|
|
284
|
-
|
|
285
|
-
if save_path is not None:
|
|
286
|
-
fig.savefig(save_path, dpi=300, bbox_inches="tight")
|
|
287
|
-
|
|
288
|
-
if show:
|
|
289
|
-
plt.show()
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
def _select_sparams_freq_unit(
|
|
293
|
-
frequencies: NDArray[np.floating[Any]], freq_unit: str
|
|
294
|
-
) -> tuple[str, float]:
|
|
295
|
-
"""Select frequency unit and divisor for S-parameter plots."""
|
|
296
|
-
if freq_unit == "auto":
|
|
297
|
-
max_freq = np.max(frequencies)
|
|
298
|
-
if max_freq >= 1e9:
|
|
299
|
-
return "GHz", 1e9
|
|
300
|
-
elif max_freq >= 1e6:
|
|
301
|
-
return "MHz", 1e6
|
|
302
|
-
elif max_freq >= 1e3:
|
|
303
|
-
return "kHz", 1e3
|
|
304
|
-
else:
|
|
305
|
-
return "Hz", 1.0
|
|
306
|
-
else:
|
|
307
|
-
freq_div = {"Hz": 1.0, "kHz": 1e3, "MHz": 1e6, "GHz": 1e9}.get(freq_unit, 1.0)
|
|
308
|
-
return freq_unit, freq_div
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
def _convert_sparam_to_db(s: NDArray[Any]) -> NDArray[np.floating[Any]]:
|
|
312
|
-
"""Convert S-parameter to dB."""
|
|
313
|
-
if np.iscomplexobj(s):
|
|
314
|
-
result: NDArray[np.floating[Any]] = 20 * np.log10(np.abs(s) + 1e-12)
|
|
315
|
-
return result
|
|
316
|
-
return np.asarray(s, dtype=np.float64)
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
def _add_3db_marker(
|
|
320
|
-
ax: Axes,
|
|
321
|
-
freq_scaled: NDArray[np.floating[Any]],
|
|
322
|
-
s_db: NDArray[np.floating[Any]],
|
|
323
|
-
freq_unit: str,
|
|
324
|
-
) -> None:
|
|
325
|
-
"""Add -3dB bandwidth marker to S21 plot."""
|
|
326
|
-
max_db = np.max(s_db)
|
|
327
|
-
db_3_level = max_db - 3
|
|
328
|
-
|
|
329
|
-
crossings = np.where(np.diff(np.sign(s_db - db_3_level)))[0]
|
|
330
|
-
if len(crossings) > 0:
|
|
331
|
-
f_3db = float(freq_scaled[crossings[0]])
|
|
332
|
-
db_3_level_float = float(db_3_level)
|
|
333
|
-
ax.axhline(db_3_level_float, color="gray", linestyle=":", alpha=0.7, linewidth=1)
|
|
334
|
-
ax.axvline(f_3db, color="gray", linestyle=":", alpha=0.7, linewidth=1)
|
|
335
|
-
ax.plot(f_3db, db_3_level_float, "ko", markersize=6)
|
|
336
|
-
ax.annotate(
|
|
337
|
-
f"-3dB: {f_3db:.2f} {freq_unit}",
|
|
338
|
-
xy=(f_3db, db_3_level_float),
|
|
339
|
-
xytext=(10, -15),
|
|
340
|
-
textcoords="offset points",
|
|
341
|
-
fontsize=9,
|
|
342
|
-
ha="left",
|
|
343
|
-
)
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
def plot_sparams_magnitude(
|
|
347
|
-
frequencies: NDArray[np.floating[Any]],
|
|
348
|
-
s11: NDArray[np.complexfloating[Any, Any]] | NDArray[np.floating[Any]] | None = None,
|
|
349
|
-
s21: NDArray[np.complexfloating[Any, Any]] | NDArray[np.floating[Any]] | None = None,
|
|
350
|
-
s12: NDArray[np.complexfloating[Any, Any]] | NDArray[np.floating[Any]] | None = None,
|
|
351
|
-
s22: NDArray[np.complexfloating[Any, Any]] | NDArray[np.floating[Any]] | None = None,
|
|
352
|
-
*,
|
|
353
|
-
ax: Axes | None = None,
|
|
354
|
-
figsize: tuple[float, float] = (12, 6),
|
|
355
|
-
title: str | None = None,
|
|
356
|
-
freq_unit: str = "auto",
|
|
357
|
-
show_markers: bool = True,
|
|
358
|
-
db_3_marker: bool = True,
|
|
359
|
-
show: bool = True,
|
|
360
|
-
save_path: str | Path | None = None,
|
|
361
|
-
) -> Figure:
|
|
362
|
-
"""Plot S-parameter magnitude vs frequency.
|
|
363
|
-
|
|
364
|
-
Creates a frequency response plot showing S-parameter magnitudes
|
|
365
|
-
in dB with optional -3dB marker for bandwidth measurement.
|
|
366
|
-
|
|
367
|
-
Args:
|
|
368
|
-
frequencies: Frequency array in Hz.
|
|
369
|
-
s11: S11 (input reflection) - complex or dB values.
|
|
370
|
-
s21: S21 (forward transmission) - complex or dB values.
|
|
371
|
-
s12: S12 (reverse transmission) - complex or dB values.
|
|
372
|
-
s22: S22 (output reflection) - complex or dB values.
|
|
373
|
-
ax: Matplotlib axes. If None, creates new figure.
|
|
374
|
-
figsize: Figure size in inches.
|
|
375
|
-
title: Plot title.
|
|
376
|
-
freq_unit: Frequency unit ("Hz", "kHz", "MHz", "GHz", "auto").
|
|
377
|
-
show_markers: Show markers at key frequencies.
|
|
378
|
-
db_3_marker: Show -3dB bandwidth marker for S21.
|
|
379
|
-
show: Display plot interactively.
|
|
380
|
-
save_path: Save plot to file.
|
|
381
|
-
|
|
382
|
-
Returns:
|
|
383
|
-
Matplotlib Figure object.
|
|
384
|
-
|
|
385
|
-
Example:
|
|
386
|
-
>>> freq = np.linspace(1e6, 1e9, 1000)
|
|
387
|
-
>>> s21 = 1 / (1 + 1j * freq / 100e6) # Low-pass response
|
|
388
|
-
>>> fig = plot_sparams_magnitude(freq, s21=s21)
|
|
389
|
-
"""
|
|
390
|
-
if not HAS_MATPLOTLIB:
|
|
391
|
-
raise ImportError("matplotlib is required for visualization")
|
|
392
|
-
|
|
393
|
-
fig, ax = _setup_tdr_figure(ax, figsize)
|
|
394
|
-
freq_unit, freq_div = _select_sparams_freq_unit(frequencies, freq_unit)
|
|
395
|
-
freq_scaled = frequencies / freq_div
|
|
396
|
-
|
|
397
|
-
colors = {"S11": "#E74C3C", "S21": "#3498DB", "S12": "#2ECC71", "S22": "#9B59B6"}
|
|
398
|
-
linestyles = {"S11": "-", "S21": "-", "S12": "--", "S22": "--"}
|
|
399
|
-
params = [("S11", s11), ("S21", s21), ("S12", s12), ("S22", s22)]
|
|
400
|
-
|
|
401
|
-
for name, s_param in params:
|
|
402
|
-
if s_param is not None:
|
|
403
|
-
s_db = _convert_sparam_to_db(s_param)
|
|
404
|
-
ax.semilogx(
|
|
405
|
-
freq_scaled,
|
|
406
|
-
s_db,
|
|
407
|
-
color=colors[name],
|
|
408
|
-
linestyle=linestyles[name],
|
|
409
|
-
linewidth=2,
|
|
410
|
-
label=name,
|
|
411
|
-
)
|
|
412
|
-
|
|
413
|
-
if name == "S21" and db_3_marker:
|
|
414
|
-
_add_3db_marker(ax, freq_scaled, s_db, freq_unit)
|
|
415
|
-
|
|
416
|
-
ax.set_xlabel(f"Frequency ({freq_unit})", fontsize=11)
|
|
417
|
-
ax.set_ylabel("Magnitude (dB)", fontsize=11)
|
|
418
|
-
ax.grid(True, which="both", alpha=0.3)
|
|
419
|
-
ax.legend(loc="best")
|
|
420
|
-
ax.set_title(
|
|
421
|
-
title if title else "S-Parameter Magnitude Response", fontsize=12, fontweight="bold"
|
|
422
|
-
)
|
|
423
|
-
|
|
424
|
-
fig.tight_layout()
|
|
425
|
-
|
|
426
|
-
if save_path is not None:
|
|
427
|
-
fig.savefig(save_path, dpi=300, bbox_inches="tight")
|
|
428
|
-
if show:
|
|
429
|
-
plt.show()
|
|
430
|
-
|
|
431
|
-
return fig
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
def plot_sparams_phase(
|
|
435
|
-
frequencies: NDArray[np.floating[Any]],
|
|
436
|
-
s11: NDArray[np.complexfloating[Any, Any]] | None = None,
|
|
437
|
-
s21: NDArray[np.complexfloating[Any, Any]] | None = None,
|
|
438
|
-
*,
|
|
439
|
-
ax: Axes | None = None,
|
|
440
|
-
figsize: tuple[float, float] = (12, 6),
|
|
441
|
-
title: str | None = None,
|
|
442
|
-
freq_unit: str = "auto",
|
|
443
|
-
unwrap: bool = True,
|
|
444
|
-
show: bool = True,
|
|
445
|
-
save_path: str | Path | None = None,
|
|
446
|
-
) -> Figure:
|
|
447
|
-
"""Plot S-parameter phase vs frequency.
|
|
448
|
-
|
|
449
|
-
Args:
|
|
450
|
-
frequencies: Frequency array in Hz.
|
|
451
|
-
s11: S11 complex values.
|
|
452
|
-
s21: S21 complex values.
|
|
453
|
-
ax: Matplotlib axes.
|
|
454
|
-
figsize: Figure size.
|
|
455
|
-
title: Plot title.
|
|
456
|
-
freq_unit: Frequency unit.
|
|
457
|
-
unwrap: Unwrap phase discontinuities.
|
|
458
|
-
show: Display plot.
|
|
459
|
-
save_path: Save path.
|
|
460
|
-
|
|
461
|
-
Returns:
|
|
462
|
-
Matplotlib Figure object.
|
|
463
|
-
"""
|
|
464
|
-
if not HAS_MATPLOTLIB:
|
|
465
|
-
raise ImportError("matplotlib is required for visualization")
|
|
466
|
-
|
|
467
|
-
if ax is None:
|
|
468
|
-
fig, ax = plt.subplots(figsize=figsize)
|
|
469
|
-
else:
|
|
470
|
-
fig_temp = ax.get_figure()
|
|
471
|
-
if fig_temp is None:
|
|
472
|
-
raise ValueError("Axes must have an associated figure")
|
|
473
|
-
fig = cast("Figure", fig_temp)
|
|
474
|
-
|
|
475
|
-
# Select frequency unit
|
|
476
|
-
if freq_unit == "auto":
|
|
477
|
-
max_freq = np.max(frequencies)
|
|
478
|
-
if max_freq >= 1e9:
|
|
479
|
-
freq_unit = "GHz"
|
|
480
|
-
freq_div = 1e9
|
|
481
|
-
elif max_freq >= 1e6:
|
|
482
|
-
freq_unit = "MHz"
|
|
483
|
-
freq_div = 1e6
|
|
484
|
-
else:
|
|
485
|
-
freq_unit = "kHz"
|
|
486
|
-
freq_div = 1e3
|
|
487
|
-
else:
|
|
488
|
-
freq_div = {"Hz": 1.0, "kHz": 1e3, "MHz": 1e6, "GHz": 1e9}.get(freq_unit, 1.0)
|
|
489
|
-
|
|
490
|
-
freq_scaled = frequencies / freq_div
|
|
491
|
-
|
|
492
|
-
colors = {"S11": "#E74C3C", "S21": "#3498DB"}
|
|
493
|
-
|
|
494
|
-
for name, s_param in [("S11", s11), ("S21", s21)]:
|
|
495
|
-
if s_param is not None:
|
|
496
|
-
phase = np.angle(s_param, deg=True)
|
|
497
|
-
if unwrap:
|
|
498
|
-
phase = np.rad2deg(np.unwrap(np.deg2rad(phase)))
|
|
499
|
-
|
|
500
|
-
ax.semilogx(freq_scaled, phase, color=colors[name], linewidth=2, label=name)
|
|
501
|
-
|
|
502
|
-
ax.set_xlabel(f"Frequency ({freq_unit})", fontsize=11)
|
|
503
|
-
ax.set_ylabel("Phase (degrees)", fontsize=11)
|
|
504
|
-
ax.grid(True, which="both", alpha=0.3)
|
|
505
|
-
ax.legend(loc="best")
|
|
506
|
-
|
|
507
|
-
if title:
|
|
508
|
-
ax.set_title(title, fontsize=12, fontweight="bold")
|
|
509
|
-
else:
|
|
510
|
-
ax.set_title("S-Parameter Phase Response", fontsize=12, fontweight="bold")
|
|
511
|
-
|
|
512
|
-
fig.tight_layout()
|
|
513
|
-
|
|
514
|
-
if save_path is not None:
|
|
515
|
-
fig.savefig(save_path, dpi=300, bbox_inches="tight")
|
|
516
|
-
|
|
517
|
-
if show:
|
|
518
|
-
plt.show()
|
|
519
|
-
|
|
520
|
-
return fig
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
def plot_setup_hold_timing(
|
|
524
|
-
clock_edges: NDArray[np.floating[Any]],
|
|
525
|
-
data_edges: NDArray[np.floating[Any]],
|
|
526
|
-
setup_time: float,
|
|
527
|
-
hold_time: float,
|
|
528
|
-
*,
|
|
529
|
-
clock_data: NDArray[np.floating[Any]] | None = None,
|
|
530
|
-
data_data: NDArray[np.floating[Any]] | None = None,
|
|
531
|
-
time_axis: NDArray[np.floating[Any]] | None = None,
|
|
532
|
-
ax: Axes | None = None,
|
|
533
|
-
figsize: tuple[float, float] = (14, 8),
|
|
534
|
-
title: str | None = None,
|
|
535
|
-
time_unit: str = "auto",
|
|
536
|
-
show_margins: bool = True,
|
|
537
|
-
setup_spec: float | None = None,
|
|
538
|
-
hold_spec: float | None = None,
|
|
539
|
-
show: bool = True,
|
|
540
|
-
save_path: str | Path | None = None,
|
|
541
|
-
) -> Figure:
|
|
542
|
-
"""Plot setup/hold timing diagram with annotations.
|
|
543
|
-
|
|
544
|
-
Creates a timing diagram showing clock and data relationships
|
|
545
|
-
with setup and hold time annotations and optional pass/fail
|
|
546
|
-
indication against specifications.
|
|
547
|
-
|
|
548
|
-
Args:
|
|
549
|
-
clock_edges: Array of clock edge times (rising edges).
|
|
550
|
-
data_edges: Array of data transition times.
|
|
551
|
-
setup_time: Measured setup time (seconds).
|
|
552
|
-
hold_time: Measured hold time (seconds).
|
|
553
|
-
clock_data: Optional clock waveform for display.
|
|
554
|
-
data_data: Optional data waveform for display.
|
|
555
|
-
time_axis: Time axis for waveforms.
|
|
556
|
-
ax: Matplotlib axes.
|
|
557
|
-
figsize: Figure size.
|
|
558
|
-
title: Plot title.
|
|
559
|
-
time_unit: Time unit ("s", "ms", "us", "ns", "ps", "auto").
|
|
560
|
-
show_margins: Show setup/hold timing arrows.
|
|
561
|
-
setup_spec: Setup time specification for pass/fail.
|
|
562
|
-
hold_spec: Hold time specification for pass/fail.
|
|
563
|
-
show: Display plot.
|
|
564
|
-
save_path: Save path.
|
|
565
|
-
|
|
566
|
-
Returns:
|
|
567
|
-
Matplotlib Figure object.
|
|
568
|
-
|
|
569
|
-
Example:
|
|
570
|
-
>>> clk_edges = np.array([0, 10e-9, 20e-9])
|
|
571
|
-
>>> data_edges = np.array([8e-9, 18e-9])
|
|
572
|
-
>>> fig = plot_setup_hold_timing(
|
|
573
|
-
... clk_edges, data_edges,
|
|
574
|
-
... setup_time=2e-9, hold_time=1e-9,
|
|
575
|
-
... setup_spec=1e-9, hold_spec=0.5e-9
|
|
576
|
-
... )
|
|
577
|
-
"""
|
|
578
|
-
if not HAS_MATPLOTLIB:
|
|
579
|
-
raise ImportError("matplotlib is required for visualization")
|
|
580
|
-
|
|
581
|
-
# Create figure and axes
|
|
582
|
-
fig, axes = _create_timing_figure(ax, clock_data, figsize)
|
|
583
|
-
|
|
584
|
-
# Determine time scaling
|
|
585
|
-
time_unit_final, time_mult = _select_time_unit(time_unit, clock_edges, data_edges)
|
|
586
|
-
setup_scaled = setup_time * time_mult
|
|
587
|
-
hold_scaled = hold_time * time_mult
|
|
588
|
-
|
|
589
|
-
# Plot waveforms if provided
|
|
590
|
-
ax_timing = _plot_timing_waveforms(axes, clock_data, data_data, time_axis, time_mult)
|
|
591
|
-
|
|
592
|
-
# Setup timing annotation panel
|
|
593
|
-
_setup_timing_panel(ax_timing, clock_edges, data_edges, time_mult)
|
|
594
|
-
|
|
595
|
-
# Draw timing arrows
|
|
596
|
-
if show_margins:
|
|
597
|
-
_draw_timing_arrows(
|
|
598
|
-
ax_timing,
|
|
599
|
-
clock_edges,
|
|
600
|
-
data_edges,
|
|
601
|
-
setup_scaled,
|
|
602
|
-
hold_scaled,
|
|
603
|
-
time_mult,
|
|
604
|
-
time_unit_final,
|
|
605
|
-
)
|
|
606
|
-
|
|
607
|
-
# Add pass/fail status
|
|
608
|
-
_add_passfail_status(
|
|
609
|
-
ax_timing, setup_time, hold_time, setup_spec, hold_spec, time_mult, time_unit_final
|
|
610
|
-
)
|
|
611
|
-
|
|
612
|
-
# Finalize plot
|
|
613
|
-
axes[-1].set_xlabel(f"Time ({time_unit_final})", fontsize=11)
|
|
614
|
-
fig.suptitle(title if title else "Setup/Hold Timing Analysis", fontsize=14, fontweight="bold")
|
|
615
|
-
fig.tight_layout()
|
|
616
|
-
|
|
617
|
-
if save_path is not None:
|
|
618
|
-
fig.savefig(save_path, dpi=300, bbox_inches="tight")
|
|
619
|
-
if show:
|
|
620
|
-
plt.show()
|
|
621
|
-
|
|
622
|
-
return fig
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
def _create_timing_figure(
|
|
626
|
-
ax: Axes | None,
|
|
627
|
-
clock_data: NDArray[np.floating[Any]] | None,
|
|
628
|
-
figsize: tuple[float, float],
|
|
629
|
-
) -> tuple[Figure, list[Any]]:
|
|
630
|
-
"""Create figure and axes for timing diagram.
|
|
631
|
-
|
|
632
|
-
Args:
|
|
633
|
-
ax: Existing axes or None.
|
|
634
|
-
clock_data: Clock waveform data (determines row count).
|
|
635
|
-
figsize: Figure size.
|
|
636
|
-
|
|
637
|
-
Returns:
|
|
638
|
-
Tuple of (figure, axes_list).
|
|
639
|
-
"""
|
|
640
|
-
if ax is not None:
|
|
641
|
-
fig_temp = ax.get_figure()
|
|
642
|
-
if fig_temp is None:
|
|
643
|
-
raise ValueError("Axes must have an associated figure")
|
|
644
|
-
return cast("Figure", fig_temp), [ax]
|
|
645
|
-
|
|
646
|
-
n_rows = 3 if clock_data is not None else 1
|
|
647
|
-
fig, axes = plt.subplots(
|
|
648
|
-
n_rows, 1, figsize=figsize, sharex=True, gridspec_kw={"height_ratios": [1] * n_rows}
|
|
649
|
-
)
|
|
650
|
-
return fig, [axes] if n_rows == 1 else axes
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
def _select_time_unit(
|
|
654
|
-
time_unit: str,
|
|
655
|
-
clock_edges: NDArray[np.floating[Any]],
|
|
656
|
-
data_edges: NDArray[np.floating[Any]],
|
|
657
|
-
) -> tuple[str, float]:
|
|
658
|
-
"""Select appropriate time unit and multiplier.
|
|
659
|
-
|
|
660
|
-
Args:
|
|
661
|
-
time_unit: Requested unit or "auto".
|
|
662
|
-
clock_edges: Clock edge times.
|
|
663
|
-
data_edges: Data edge times.
|
|
664
|
-
|
|
665
|
-
Returns:
|
|
666
|
-
Tuple of (unit_string, multiplier).
|
|
667
|
-
"""
|
|
668
|
-
if time_unit == "auto":
|
|
669
|
-
max_time = max(np.max(clock_edges), np.max(data_edges))
|
|
670
|
-
if max_time < 1e-9:
|
|
671
|
-
return "ps", 1e12
|
|
672
|
-
elif max_time < 1e-6:
|
|
673
|
-
return "ns", 1e9
|
|
674
|
-
elif max_time < 1e-3:
|
|
675
|
-
return "us", 1e6
|
|
676
|
-
else:
|
|
677
|
-
return "ms", 1e3
|
|
678
|
-
else:
|
|
679
|
-
mult = {"s": 1, "ms": 1e3, "us": 1e6, "ns": 1e9, "ps": 1e12}.get(time_unit, 1e9)
|
|
680
|
-
return time_unit, mult
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
def _plot_timing_waveforms(
|
|
684
|
-
axes: list[Any],
|
|
685
|
-
clock_data: NDArray[np.floating[Any]] | None,
|
|
686
|
-
data_data: NDArray[np.floating[Any]] | None,
|
|
687
|
-
time_axis: NDArray[np.floating[Any]] | None,
|
|
688
|
-
time_mult: float,
|
|
689
|
-
) -> Any:
|
|
690
|
-
"""Plot clock and data waveforms.
|
|
691
|
-
|
|
692
|
-
Args:
|
|
693
|
-
axes: List of axes.
|
|
694
|
-
clock_data: Clock waveform.
|
|
695
|
-
data_data: Data waveform.
|
|
696
|
-
time_axis: Time axis.
|
|
697
|
-
time_mult: Time multiplier.
|
|
698
|
-
|
|
699
|
-
Returns:
|
|
700
|
-
Axes for timing annotations.
|
|
701
|
-
"""
|
|
702
|
-
if clock_data is not None and data_data is not None and time_axis is not None:
|
|
703
|
-
time_scaled = time_axis * time_mult
|
|
704
|
-
|
|
705
|
-
# Clock waveform
|
|
706
|
-
ax_clk = axes[0]
|
|
707
|
-
ax_clk.step(time_scaled, clock_data, where="post", color="#3498DB", linewidth=2)
|
|
708
|
-
ax_clk.set_ylabel(
|
|
709
|
-
"CLK", rotation=0, ha="right", va="center", fontsize=11, fontweight="bold"
|
|
710
|
-
)
|
|
711
|
-
ax_clk.set_ylim(-0.2, 1.3)
|
|
712
|
-
ax_clk.set_yticks([0, 1])
|
|
713
|
-
ax_clk.grid(True, axis="x", alpha=0.3)
|
|
714
|
-
|
|
715
|
-
# Data waveform
|
|
716
|
-
ax_data = axes[1]
|
|
717
|
-
ax_data.step(time_scaled, data_data, where="post", color="#E74C3C", linewidth=2)
|
|
718
|
-
ax_data.set_ylabel(
|
|
719
|
-
"DATA", rotation=0, ha="right", va="center", fontsize=11, fontweight="bold"
|
|
720
|
-
)
|
|
721
|
-
ax_data.set_ylim(-0.2, 1.3)
|
|
722
|
-
ax_data.set_yticks([0, 1])
|
|
723
|
-
ax_data.grid(True, axis="x", alpha=0.3)
|
|
724
|
-
|
|
725
|
-
return axes[2] if len(axes) > 2 else axes[-1]
|
|
726
|
-
else:
|
|
727
|
-
return axes[0]
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
def _setup_timing_panel(
|
|
731
|
-
ax: Any,
|
|
732
|
-
clock_edges: NDArray[np.floating[Any]],
|
|
733
|
-
data_edges: NDArray[np.floating[Any]],
|
|
734
|
-
time_mult: float,
|
|
735
|
-
) -> None:
|
|
736
|
-
"""Setup timing annotation panel.
|
|
737
|
-
|
|
738
|
-
Args:
|
|
739
|
-
ax: Timing axes.
|
|
740
|
-
clock_edges: Clock edge times.
|
|
741
|
-
data_edges: Data edge times.
|
|
742
|
-
time_mult: Time multiplier.
|
|
743
|
-
"""
|
|
744
|
-
ax.set_ylim(0, 1)
|
|
745
|
-
ax.set_xlim(0, max(clock_edges[-1], data_edges[-1]) * time_mult * 1.1)
|
|
746
|
-
ax.axis("off")
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
def _draw_timing_arrows(
|
|
750
|
-
ax: Any,
|
|
751
|
-
clock_edges: NDArray[np.floating[Any]],
|
|
752
|
-
data_edges: NDArray[np.floating[Any]],
|
|
753
|
-
setup_scaled: float,
|
|
754
|
-
hold_scaled: float,
|
|
755
|
-
time_mult: float,
|
|
756
|
-
time_unit: str,
|
|
757
|
-
) -> None:
|
|
758
|
-
"""Draw setup and hold timing arrows.
|
|
759
|
-
|
|
760
|
-
Args:
|
|
761
|
-
ax: Timing axes.
|
|
762
|
-
clock_edges: Clock edge times.
|
|
763
|
-
data_edges: Data edge times.
|
|
764
|
-
setup_scaled: Scaled setup time.
|
|
765
|
-
hold_scaled: Scaled hold time.
|
|
766
|
-
time_mult: Time multiplier.
|
|
767
|
-
time_unit: Time unit string.
|
|
768
|
-
"""
|
|
769
|
-
if len(clock_edges) == 0 or len(data_edges) == 0:
|
|
770
|
-
return
|
|
771
|
-
|
|
772
|
-
clk_edge = clock_edges[0] * time_mult
|
|
773
|
-
|
|
774
|
-
# Setup time arrow
|
|
775
|
-
data_before = data_edges[data_edges < clock_edges[0]]
|
|
776
|
-
if len(data_before) > 0:
|
|
777
|
-
data_edge = data_before[-1] * time_mult
|
|
778
|
-
y_setup = 0.7
|
|
779
|
-
ax.annotate(
|
|
780
|
-
"",
|
|
781
|
-
xy=(clk_edge, y_setup),
|
|
782
|
-
xytext=(data_edge, y_setup),
|
|
783
|
-
arrowprops={"arrowstyle": "<->", "color": "#27AE60", "lw": 2},
|
|
784
|
-
)
|
|
785
|
-
ax.text(
|
|
786
|
-
(data_edge + clk_edge) / 2,
|
|
787
|
-
y_setup + 0.1,
|
|
788
|
-
f"Setup: {setup_scaled:.2f} {time_unit}",
|
|
789
|
-
ha="center",
|
|
790
|
-
va="bottom",
|
|
791
|
-
fontsize=10,
|
|
792
|
-
fontweight="bold",
|
|
793
|
-
color="#27AE60",
|
|
794
|
-
)
|
|
795
|
-
|
|
796
|
-
# Hold time arrow
|
|
797
|
-
data_after = data_edges[data_edges > clock_edges[0]]
|
|
798
|
-
if len(data_after) > 0:
|
|
799
|
-
data_edge_after = data_after[0] * time_mult
|
|
800
|
-
y_hold = 0.3
|
|
801
|
-
ax.annotate(
|
|
802
|
-
"",
|
|
803
|
-
xy=(data_edge_after, y_hold),
|
|
804
|
-
xytext=(clk_edge, y_hold),
|
|
805
|
-
arrowprops={"arrowstyle": "<->", "color": "#E67E22", "lw": 2},
|
|
806
|
-
)
|
|
807
|
-
ax.text(
|
|
808
|
-
(clk_edge + data_edge_after) / 2,
|
|
809
|
-
y_hold + 0.1,
|
|
810
|
-
f"Hold: {hold_scaled:.2f} {time_unit}",
|
|
811
|
-
ha="center",
|
|
812
|
-
va="bottom",
|
|
813
|
-
fontsize=10,
|
|
814
|
-
fontweight="bold",
|
|
815
|
-
color="#E67E22",
|
|
816
|
-
)
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
def _add_passfail_status(
|
|
820
|
-
ax: Any,
|
|
821
|
-
setup_time: float,
|
|
822
|
-
hold_time: float,
|
|
823
|
-
setup_spec: float | None,
|
|
824
|
-
hold_spec: float | None,
|
|
825
|
-
time_mult: float,
|
|
826
|
-
time_unit: str,
|
|
827
|
-
) -> None:
|
|
828
|
-
"""Add pass/fail status text.
|
|
829
|
-
|
|
830
|
-
Args:
|
|
831
|
-
ax: Timing axes.
|
|
832
|
-
setup_time: Measured setup time.
|
|
833
|
-
hold_time: Measured hold time.
|
|
834
|
-
setup_spec: Setup specification.
|
|
835
|
-
hold_spec: Hold specification.
|
|
836
|
-
time_mult: Time multiplier.
|
|
837
|
-
time_unit: Time unit string.
|
|
838
|
-
"""
|
|
839
|
-
status_y = 0.9
|
|
840
|
-
|
|
841
|
-
if setup_spec is not None:
|
|
842
|
-
setup_pass = setup_time >= setup_spec
|
|
843
|
-
status = "PASS" if setup_pass else "FAIL"
|
|
844
|
-
color = "#27AE60" if setup_pass else "#E74C3C"
|
|
845
|
-
ax.text(
|
|
846
|
-
0.02,
|
|
847
|
-
status_y,
|
|
848
|
-
f"Setup: {status} (spec: {setup_spec * time_mult:.2f} {time_unit})",
|
|
849
|
-
transform=ax.transAxes,
|
|
850
|
-
fontsize=10,
|
|
851
|
-
color=color,
|
|
852
|
-
fontweight="bold",
|
|
853
|
-
)
|
|
854
|
-
status_y -= 0.15
|
|
855
|
-
|
|
856
|
-
if hold_spec is not None:
|
|
857
|
-
hold_pass = hold_time >= hold_spec
|
|
858
|
-
status = "PASS" if hold_pass else "FAIL"
|
|
859
|
-
color = "#27AE60" if hold_pass else "#E74C3C"
|
|
860
|
-
ax.text(
|
|
861
|
-
0.02,
|
|
862
|
-
status_y,
|
|
863
|
-
f"Hold: {status} (spec: {hold_spec * time_mult:.2f} {time_unit})",
|
|
864
|
-
transform=ax.transAxes,
|
|
865
|
-
fontsize=10,
|
|
866
|
-
color=color,
|
|
867
|
-
fontweight="bold",
|
|
868
|
-
)
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
def plot_timing_margin(
|
|
872
|
-
setup_times: NDArray[np.floating[Any]],
|
|
873
|
-
hold_times: NDArray[np.floating[Any]],
|
|
874
|
-
*,
|
|
875
|
-
setup_spec: float | None = None,
|
|
876
|
-
hold_spec: float | None = None,
|
|
877
|
-
ax: Axes | None = None,
|
|
878
|
-
figsize: tuple[float, float] = (10, 8),
|
|
879
|
-
title: str | None = None,
|
|
880
|
-
time_unit: str = "ns",
|
|
881
|
-
show: bool = True,
|
|
882
|
-
save_path: str | Path | None = None,
|
|
883
|
-
) -> Figure:
|
|
884
|
-
"""Plot setup vs hold timing margin scatter plot.
|
|
885
|
-
|
|
886
|
-
Creates a scatter plot showing the distribution of setup and hold
|
|
887
|
-
times with specification regions marked.
|
|
888
|
-
|
|
889
|
-
Args:
|
|
890
|
-
setup_times: Array of setup time measurements.
|
|
891
|
-
hold_times: Array of hold time measurements.
|
|
892
|
-
setup_spec: Setup time specification.
|
|
893
|
-
hold_spec: Hold time specification.
|
|
894
|
-
ax: Matplotlib axes.
|
|
895
|
-
figsize: Figure size.
|
|
896
|
-
title: Plot title.
|
|
897
|
-
time_unit: Time unit for display.
|
|
898
|
-
show: Display plot.
|
|
899
|
-
save_path: Save path.
|
|
900
|
-
|
|
901
|
-
Returns:
|
|
902
|
-
Matplotlib Figure object.
|
|
903
|
-
"""
|
|
904
|
-
if not HAS_MATPLOTLIB:
|
|
905
|
-
raise ImportError("matplotlib is required for visualization")
|
|
906
|
-
|
|
907
|
-
fig, ax = _get_or_create_axes(ax, figsize)
|
|
908
|
-
time_mult = {"s": 1, "ms": 1e3, "us": 1e6, "ns": 1e9, "ps": 1e12}.get(time_unit, 1e9)
|
|
909
|
-
|
|
910
|
-
setup_scaled, hold_scaled = setup_times * time_mult, hold_times * time_mult
|
|
911
|
-
|
|
912
|
-
# Scatter plot
|
|
913
|
-
ax.scatter(setup_scaled, hold_scaled, c="#3498DB", alpha=0.6, s=50)
|
|
914
|
-
|
|
915
|
-
# Add specification lines and regions
|
|
916
|
-
_add_spec_lines(ax, setup_spec, hold_spec, time_mult, time_unit)
|
|
917
|
-
_add_pass_region(ax, setup_spec, hold_spec, time_mult)
|
|
918
|
-
|
|
919
|
-
# Configure axes
|
|
920
|
-
ax.set_xlabel(f"Setup Time ({time_unit})", fontsize=11)
|
|
921
|
-
ax.set_ylabel(f"Hold Time ({time_unit})", fontsize=11)
|
|
922
|
-
ax.grid(True, alpha=0.3)
|
|
923
|
-
ax.legend(loc="best")
|
|
924
|
-
ax.set_title(title or "Setup/Hold Timing Margin", fontsize=12, fontweight="bold")
|
|
925
|
-
|
|
926
|
-
fig.tight_layout()
|
|
927
|
-
_save_and_show_figure(fig, save_path, show)
|
|
928
|
-
return fig
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
def _get_or_create_axes(ax: Axes | None, figsize: tuple[float, float]) -> tuple[Figure, Axes]:
|
|
932
|
-
"""Get existing axes or create new figure with axes."""
|
|
933
|
-
if ax is None:
|
|
934
|
-
fig, ax = plt.subplots(figsize=figsize)
|
|
935
|
-
else:
|
|
936
|
-
fig_temp = ax.get_figure()
|
|
937
|
-
if fig_temp is None:
|
|
938
|
-
raise ValueError("Axes must have an associated figure")
|
|
939
|
-
fig = cast("Figure", fig_temp)
|
|
940
|
-
return fig, ax
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
def _add_spec_lines(
|
|
944
|
-
ax: Axes, setup_spec: float | None, hold_spec: float | None, time_mult: float, time_unit: str
|
|
945
|
-
) -> None:
|
|
946
|
-
"""Add specification lines to timing margin plot."""
|
|
947
|
-
if setup_spec is not None:
|
|
948
|
-
spec_scaled = setup_spec * time_mult
|
|
949
|
-
ax.axvline(
|
|
950
|
-
spec_scaled,
|
|
951
|
-
color="#E74C3C",
|
|
952
|
-
linestyle="--",
|
|
953
|
-
linewidth=2,
|
|
954
|
-
label=f"Setup Spec ({spec_scaled:.2f} {time_unit})",
|
|
955
|
-
)
|
|
956
|
-
|
|
957
|
-
if hold_spec is not None:
|
|
958
|
-
spec_scaled = hold_spec * time_mult
|
|
959
|
-
ax.axhline(
|
|
960
|
-
spec_scaled,
|
|
961
|
-
color="#E67E22",
|
|
962
|
-
linestyle="--",
|
|
963
|
-
linewidth=2,
|
|
964
|
-
label=f"Hold Spec ({spec_scaled:.2f} {time_unit})",
|
|
965
|
-
)
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
def _add_pass_region(
|
|
969
|
-
ax: Axes, setup_spec: float | None, hold_spec: float | None, time_mult: float
|
|
970
|
-
) -> None:
|
|
971
|
-
"""Add pass/fail region shading to timing margin plot."""
|
|
972
|
-
if setup_spec is not None and hold_spec is not None:
|
|
973
|
-
x_lim, y_lim = ax.get_xlim(), ax.get_ylim()
|
|
974
|
-
ax.fill_between(
|
|
975
|
-
[setup_spec * time_mult, x_lim[1]],
|
|
976
|
-
[hold_spec * time_mult, hold_spec * time_mult],
|
|
977
|
-
[y_lim[1], y_lim[1]],
|
|
978
|
-
color="#27AE60",
|
|
979
|
-
alpha=0.1,
|
|
980
|
-
label="Pass Region",
|
|
981
|
-
)
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
def _save_and_show_figure(fig: Figure, save_path: str | Path | None, show: bool) -> None:
|
|
985
|
-
"""Save and optionally show figure."""
|
|
986
|
-
if save_path is not None:
|
|
987
|
-
fig.savefig(save_path, dpi=300, bbox_inches="tight")
|
|
988
|
-
if show:
|
|
989
|
-
plt.show()
|