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/waveform.py
DELETED
|
@@ -1,454 +0,0 @@
|
|
|
1
|
-
"""Waveform visualization functions.
|
|
2
|
-
|
|
3
|
-
This module provides time-domain waveform and multi-channel plots
|
|
4
|
-
with measurement annotations.
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Example:
|
|
8
|
-
>>> from oscura.visualization.waveform import plot_waveform, plot_multi_channel
|
|
9
|
-
>>> plot_waveform(trace)
|
|
10
|
-
>>> plot_multi_channel([ch1, ch2, ch3])
|
|
11
|
-
|
|
12
|
-
References:
|
|
13
|
-
matplotlib best practices for scientific visualization
|
|
14
|
-
"""
|
|
15
|
-
|
|
16
|
-
from __future__ import annotations
|
|
17
|
-
|
|
18
|
-
from typing import TYPE_CHECKING, Any, cast
|
|
19
|
-
|
|
20
|
-
import numpy as np
|
|
21
|
-
|
|
22
|
-
try:
|
|
23
|
-
import matplotlib.pyplot as plt
|
|
24
|
-
|
|
25
|
-
HAS_MATPLOTLIB = True
|
|
26
|
-
except ImportError:
|
|
27
|
-
HAS_MATPLOTLIB = False
|
|
28
|
-
|
|
29
|
-
from oscura.core.types import DigitalTrace, WaveformTrace
|
|
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
|
-
|
|
37
|
-
def plot_waveform(
|
|
38
|
-
trace: WaveformTrace,
|
|
39
|
-
*,
|
|
40
|
-
ax: Axes | None = None,
|
|
41
|
-
time_unit: str = "auto",
|
|
42
|
-
time_range: tuple[float, float] | None = None,
|
|
43
|
-
show_grid: bool = True,
|
|
44
|
-
color: str = "C0",
|
|
45
|
-
label: str | None = None,
|
|
46
|
-
show_measurements: dict[str, Any] | None = None,
|
|
47
|
-
title: str | None = None,
|
|
48
|
-
xlabel: str = "Time",
|
|
49
|
-
ylabel: str = "Amplitude",
|
|
50
|
-
show: bool = True,
|
|
51
|
-
save_path: str | None = None,
|
|
52
|
-
figsize: tuple[float, float] = (10, 6),
|
|
53
|
-
) -> Figure:
|
|
54
|
-
"""Plot time-domain waveform.
|
|
55
|
-
|
|
56
|
-
Args:
|
|
57
|
-
trace: Waveform trace to plot.
|
|
58
|
-
ax: Matplotlib axes. If None, creates new figure.
|
|
59
|
-
time_unit: Time unit ("s", "ms", "us", "ns", "auto").
|
|
60
|
-
time_range: Optional (start, end) time range in seconds to display.
|
|
61
|
-
show_grid: Show grid lines.
|
|
62
|
-
color: Line color.
|
|
63
|
-
label: Legend label.
|
|
64
|
-
show_measurements: Dictionary of measurements to annotate.
|
|
65
|
-
title: Plot title.
|
|
66
|
-
xlabel: X-axis label (appended with time unit).
|
|
67
|
-
ylabel: Y-axis label.
|
|
68
|
-
show: If True, call plt.show() to display the plot.
|
|
69
|
-
save_path: Path to save the figure. If None, figure is not saved.
|
|
70
|
-
figsize: Figure size (width, height) in inches. Only used if ax is None.
|
|
71
|
-
|
|
72
|
-
Returns:
|
|
73
|
-
Matplotlib Figure object.
|
|
74
|
-
|
|
75
|
-
Raises:
|
|
76
|
-
ImportError: If matplotlib is not installed.
|
|
77
|
-
ValueError: If axes has no associated figure.
|
|
78
|
-
|
|
79
|
-
Example:
|
|
80
|
-
>>> import oscura as osc
|
|
81
|
-
>>> trace = osc.load("signal.wfm")
|
|
82
|
-
>>> fig = osc.plot_waveform(trace, time_unit="us", show=False)
|
|
83
|
-
>>> fig.savefig("waveform.png")
|
|
84
|
-
|
|
85
|
-
>>> # With custom styling
|
|
86
|
-
>>> fig = osc.plot_waveform(trace,
|
|
87
|
-
... title="Captured Signal",
|
|
88
|
-
... xlabel="Time",
|
|
89
|
-
... ylabel="Voltage",
|
|
90
|
-
... color="blue")
|
|
91
|
-
"""
|
|
92
|
-
if not HAS_MATPLOTLIB:
|
|
93
|
-
raise ImportError("matplotlib is required for visualization")
|
|
94
|
-
|
|
95
|
-
# Setup figure and axes
|
|
96
|
-
fig, ax = _setup_waveform_figure(ax, figsize)
|
|
97
|
-
|
|
98
|
-
# Prepare time axis
|
|
99
|
-
time_unit_final, time_info = _prepare_time_axis(trace, time_unit)
|
|
100
|
-
time_scaled, _ = time_info
|
|
101
|
-
|
|
102
|
-
# Plot waveform
|
|
103
|
-
_plot_waveform_data(ax, time_scaled, trace.data, color, label)
|
|
104
|
-
|
|
105
|
-
# Apply styling and formatting
|
|
106
|
-
_apply_waveform_formatting(
|
|
107
|
-
ax,
|
|
108
|
-
time_range,
|
|
109
|
-
time_unit_final,
|
|
110
|
-
time_info,
|
|
111
|
-
xlabel,
|
|
112
|
-
ylabel,
|
|
113
|
-
title,
|
|
114
|
-
trace.metadata.channel_name,
|
|
115
|
-
show_grid,
|
|
116
|
-
label,
|
|
117
|
-
)
|
|
118
|
-
|
|
119
|
-
# Add measurements if provided
|
|
120
|
-
if show_measurements:
|
|
121
|
-
_add_measurement_annotations(ax, trace, show_measurements, time_unit_final, time_info[1])
|
|
122
|
-
|
|
123
|
-
fig.tight_layout()
|
|
124
|
-
|
|
125
|
-
# Save and show
|
|
126
|
-
_save_and_show_figure(fig, save_path, show)
|
|
127
|
-
|
|
128
|
-
return fig
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
def _setup_waveform_figure(ax: Axes | None, figsize: tuple[float, float]) -> tuple[Figure, Axes]:
|
|
132
|
-
"""Setup figure and axes for waveform plot."""
|
|
133
|
-
if ax is None:
|
|
134
|
-
fig, ax = plt.subplots(figsize=figsize)
|
|
135
|
-
return fig, ax
|
|
136
|
-
|
|
137
|
-
fig_temp = ax.get_figure()
|
|
138
|
-
if fig_temp is None:
|
|
139
|
-
raise ValueError("Axes must have an associated figure")
|
|
140
|
-
return cast("Figure", fig_temp), ax
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
def _prepare_time_axis(
|
|
144
|
-
trace: WaveformTrace, time_unit: str
|
|
145
|
-
) -> tuple[str, tuple[NDArray[np.float64], float]]:
|
|
146
|
-
"""Prepare time axis with appropriate unit and scaling."""
|
|
147
|
-
time = trace.time_vector
|
|
148
|
-
|
|
149
|
-
# Auto-select time unit
|
|
150
|
-
if time_unit == "auto":
|
|
151
|
-
duration = time[-1] if len(time) > 0 else 0
|
|
152
|
-
if duration < 1e-6:
|
|
153
|
-
time_unit = "ns"
|
|
154
|
-
elif duration < 1e-3:
|
|
155
|
-
time_unit = "us"
|
|
156
|
-
elif duration < 1:
|
|
157
|
-
time_unit = "ms"
|
|
158
|
-
else:
|
|
159
|
-
time_unit = "s"
|
|
160
|
-
|
|
161
|
-
time_multipliers = {"s": 1.0, "ms": 1e3, "us": 1e6, "ns": 1e9}
|
|
162
|
-
multiplier = time_multipliers.get(time_unit, 1.0)
|
|
163
|
-
time_scaled = time * multiplier
|
|
164
|
-
|
|
165
|
-
return time_unit, (time_scaled, multiplier)
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
def _plot_waveform_data(
|
|
169
|
-
ax: Axes,
|
|
170
|
-
time_scaled: NDArray[np.float64],
|
|
171
|
-
data: NDArray[np.float64],
|
|
172
|
-
color: str,
|
|
173
|
-
label: str | None,
|
|
174
|
-
) -> None:
|
|
175
|
-
"""Plot waveform data on axes."""
|
|
176
|
-
ax.plot(time_scaled, data, color=color, label=label, linewidth=0.8)
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
def _apply_waveform_formatting(
|
|
180
|
-
ax: Axes,
|
|
181
|
-
time_range: tuple[float, float] | None,
|
|
182
|
-
time_unit: str,
|
|
183
|
-
time_info: tuple[NDArray[np.float64], float],
|
|
184
|
-
xlabel: str,
|
|
185
|
-
ylabel: str,
|
|
186
|
-
title: str | None,
|
|
187
|
-
channel_name: str | None,
|
|
188
|
-
show_grid: bool,
|
|
189
|
-
label: str | None,
|
|
190
|
-
) -> None:
|
|
191
|
-
"""Apply formatting to waveform plot."""
|
|
192
|
-
_, multiplier = time_info
|
|
193
|
-
|
|
194
|
-
# Apply time range if specified
|
|
195
|
-
if time_range is not None:
|
|
196
|
-
ax.set_xlim(time_range[0] * multiplier, time_range[1] * multiplier)
|
|
197
|
-
|
|
198
|
-
# Labels
|
|
199
|
-
ax.set_xlabel(f"{xlabel} ({time_unit})")
|
|
200
|
-
ax.set_ylabel(ylabel)
|
|
201
|
-
|
|
202
|
-
# Title
|
|
203
|
-
if title:
|
|
204
|
-
ax.set_title(title)
|
|
205
|
-
elif channel_name:
|
|
206
|
-
ax.set_title(f"Waveform - {channel_name}")
|
|
207
|
-
|
|
208
|
-
# Grid and legend
|
|
209
|
-
if show_grid:
|
|
210
|
-
ax.grid(True, alpha=0.3)
|
|
211
|
-
|
|
212
|
-
if label:
|
|
213
|
-
ax.legend()
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
def _save_and_show_figure(fig: Figure, save_path: str | None, show: bool) -> None:
|
|
217
|
-
"""Save and/or display the figure."""
|
|
218
|
-
if save_path is not None:
|
|
219
|
-
fig.savefig(save_path, dpi=300, bbox_inches="tight")
|
|
220
|
-
|
|
221
|
-
if show:
|
|
222
|
-
plt.show()
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
def plot_multi_channel(
|
|
226
|
-
traces: list[WaveformTrace | DigitalTrace],
|
|
227
|
-
*,
|
|
228
|
-
names: list[str] | None = None,
|
|
229
|
-
shared_x: bool = True,
|
|
230
|
-
share_x: bool | None = None,
|
|
231
|
-
colors: list[str] | None = None,
|
|
232
|
-
time_unit: str = "auto",
|
|
233
|
-
show_grid: bool = True,
|
|
234
|
-
figsize: tuple[float, float] | None = None,
|
|
235
|
-
title: str | None = None,
|
|
236
|
-
) -> Figure:
|
|
237
|
-
"""Plot multiple channels in stacked subplots.
|
|
238
|
-
|
|
239
|
-
Args:
|
|
240
|
-
traces: List of traces to plot.
|
|
241
|
-
names: Channel names for labels.
|
|
242
|
-
shared_x: Share x-axis across subplots.
|
|
243
|
-
share_x: Alias for shared_x (for compatibility).
|
|
244
|
-
colors: List of colors for each trace. If None, uses default cycle.
|
|
245
|
-
time_unit: Time unit ("s", "ms", "us", "ns", "auto").
|
|
246
|
-
show_grid: Show grid lines.
|
|
247
|
-
figsize: Figure size (width, height) in inches.
|
|
248
|
-
title: Overall figure title.
|
|
249
|
-
|
|
250
|
-
Returns:
|
|
251
|
-
Matplotlib Figure object.
|
|
252
|
-
|
|
253
|
-
Raises:
|
|
254
|
-
ImportError: If matplotlib is not available.
|
|
255
|
-
|
|
256
|
-
Example:
|
|
257
|
-
>>> fig = plot_multi_channel([ch1, ch2, ch3], names=["CLK", "DATA", "CS"])
|
|
258
|
-
>>> plt.show()
|
|
259
|
-
"""
|
|
260
|
-
if not HAS_MATPLOTLIB:
|
|
261
|
-
raise ImportError("matplotlib is required for visualization")
|
|
262
|
-
|
|
263
|
-
shared_x = share_x if share_x is not None else shared_x
|
|
264
|
-
n_channels = len(traces)
|
|
265
|
-
names = names or [f"CH{i + 1}" for i in range(n_channels)]
|
|
266
|
-
figsize = figsize or (10, 2 * n_channels)
|
|
267
|
-
|
|
268
|
-
fig, axes = plt.subplots(n_channels, 1, figsize=figsize, sharex=shared_x)
|
|
269
|
-
axes = [axes] if n_channels == 1 else axes
|
|
270
|
-
|
|
271
|
-
time_unit, multiplier = _determine_time_unit_and_multiplier(time_unit, traces)
|
|
272
|
-
|
|
273
|
-
_plot_channels(traces, names, axes, colors, time_unit, multiplier, show_grid, n_channels)
|
|
274
|
-
|
|
275
|
-
if title:
|
|
276
|
-
fig.suptitle(title)
|
|
277
|
-
|
|
278
|
-
fig.tight_layout()
|
|
279
|
-
return fig
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
def _determine_time_unit_and_multiplier(
|
|
283
|
-
time_unit: str, traces: list[WaveformTrace | DigitalTrace]
|
|
284
|
-
) -> tuple[str, float]:
|
|
285
|
-
"""Determine time unit and multiplier for plotting."""
|
|
286
|
-
if time_unit == "auto" and len(traces) > 0:
|
|
287
|
-
ref_trace = traces[0]
|
|
288
|
-
duration = len(ref_trace.data) * ref_trace.metadata.time_base
|
|
289
|
-
time_unit = _select_time_unit_from_duration(duration)
|
|
290
|
-
|
|
291
|
-
time_multipliers = {"s": 1.0, "ms": 1e3, "us": 1e6, "ns": 1e9}
|
|
292
|
-
multiplier = time_multipliers.get(time_unit, 1.0)
|
|
293
|
-
|
|
294
|
-
return time_unit, multiplier
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
def _select_time_unit_from_duration(duration: float) -> str:
|
|
298
|
-
"""Select appropriate time unit based on duration."""
|
|
299
|
-
if duration < 1e-6:
|
|
300
|
-
return "ns"
|
|
301
|
-
if duration < 1e-3:
|
|
302
|
-
return "us"
|
|
303
|
-
if duration < 1:
|
|
304
|
-
return "ms"
|
|
305
|
-
return "s"
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
def _plot_channels(
|
|
309
|
-
traces: list[WaveformTrace | DigitalTrace],
|
|
310
|
-
names: list[str],
|
|
311
|
-
axes: list[Any],
|
|
312
|
-
colors: list[str] | None,
|
|
313
|
-
time_unit: str,
|
|
314
|
-
multiplier: float,
|
|
315
|
-
show_grid: bool,
|
|
316
|
-
n_channels: int,
|
|
317
|
-
) -> None:
|
|
318
|
-
"""Plot each channel on its subplot."""
|
|
319
|
-
for i, (trace, name, ax) in enumerate(zip(traces, names, axes, strict=False)):
|
|
320
|
-
time = trace.time_vector * multiplier
|
|
321
|
-
color = colors[i] if colors and i < len(colors) else f"C{i}"
|
|
322
|
-
|
|
323
|
-
_plot_single_channel(ax, trace, time, color, name, show_grid)
|
|
324
|
-
|
|
325
|
-
if i == n_channels - 1:
|
|
326
|
-
ax.set_xlabel(f"Time ({time_unit})")
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
def _plot_single_channel(
|
|
330
|
-
ax: Any,
|
|
331
|
-
trace: WaveformTrace | DigitalTrace,
|
|
332
|
-
time: Any,
|
|
333
|
-
color: str,
|
|
334
|
-
name: str,
|
|
335
|
-
show_grid: bool,
|
|
336
|
-
) -> None:
|
|
337
|
-
"""Plot a single channel (analog or digital)."""
|
|
338
|
-
if isinstance(trace, WaveformTrace):
|
|
339
|
-
ax.plot(time, trace.data, color=color, linewidth=0.8)
|
|
340
|
-
ax.set_ylabel("V")
|
|
341
|
-
else:
|
|
342
|
-
ax.step(time, trace.data.astype(int), color=color, where="post", linewidth=1.0)
|
|
343
|
-
ax.set_ylim(-0.1, 1.1)
|
|
344
|
-
ax.set_yticks([0, 1])
|
|
345
|
-
ax.set_yticklabels(["L", "H"])
|
|
346
|
-
|
|
347
|
-
ax.set_ylabel(name, rotation=0, ha="right", va="center")
|
|
348
|
-
|
|
349
|
-
if show_grid:
|
|
350
|
-
ax.grid(True, alpha=0.3)
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
def plot_xy(
|
|
354
|
-
x_trace: WaveformTrace | NDArray[np.float64],
|
|
355
|
-
y_trace: WaveformTrace | NDArray[np.float64],
|
|
356
|
-
*,
|
|
357
|
-
ax: Axes | None = None,
|
|
358
|
-
color: str = "C0",
|
|
359
|
-
marker: str = "",
|
|
360
|
-
alpha: float = 0.7,
|
|
361
|
-
title: str | None = None,
|
|
362
|
-
) -> Figure:
|
|
363
|
-
"""Plot X-Y (Lissajous) diagram.
|
|
364
|
-
|
|
365
|
-
Args:
|
|
366
|
-
x_trace: X-axis waveform.
|
|
367
|
-
y_trace: Y-axis waveform.
|
|
368
|
-
ax: Matplotlib axes.
|
|
369
|
-
color: Line/marker color.
|
|
370
|
-
marker: Marker style.
|
|
371
|
-
alpha: Transparency.
|
|
372
|
-
title: Plot title.
|
|
373
|
-
|
|
374
|
-
Returns:
|
|
375
|
-
Matplotlib Figure object.
|
|
376
|
-
|
|
377
|
-
Raises:
|
|
378
|
-
ImportError: If matplotlib is not available.
|
|
379
|
-
ValueError: If axes has no associated figure.
|
|
380
|
-
|
|
381
|
-
Example:
|
|
382
|
-
>>> fig = plot_xy(ch1, ch2) # Phase relationship
|
|
383
|
-
"""
|
|
384
|
-
if not HAS_MATPLOTLIB:
|
|
385
|
-
raise ImportError("matplotlib is required for visualization")
|
|
386
|
-
|
|
387
|
-
if ax is None:
|
|
388
|
-
fig, ax = plt.subplots(figsize=(6, 6))
|
|
389
|
-
else:
|
|
390
|
-
fig_temp = ax.get_figure()
|
|
391
|
-
if fig_temp is None:
|
|
392
|
-
raise ValueError("Axes must have an associated figure")
|
|
393
|
-
fig = cast("Figure", fig_temp)
|
|
394
|
-
|
|
395
|
-
x_data = x_trace.data if isinstance(x_trace, WaveformTrace) else x_trace
|
|
396
|
-
y_data = y_trace.data if isinstance(y_trace, WaveformTrace) else y_trace
|
|
397
|
-
|
|
398
|
-
# Ensure same length
|
|
399
|
-
min_len = min(len(x_data), len(y_data))
|
|
400
|
-
x_data = x_data[:min_len]
|
|
401
|
-
y_data = y_data[:min_len]
|
|
402
|
-
|
|
403
|
-
ax.plot(x_data, y_data, color=color, marker=marker, alpha=alpha, linewidth=0.5)
|
|
404
|
-
|
|
405
|
-
ax.set_xlabel("X (V)")
|
|
406
|
-
ax.set_ylabel("Y (V)")
|
|
407
|
-
ax.set_aspect("equal")
|
|
408
|
-
ax.grid(True, alpha=0.3)
|
|
409
|
-
|
|
410
|
-
if title:
|
|
411
|
-
ax.set_title(title)
|
|
412
|
-
|
|
413
|
-
fig.tight_layout()
|
|
414
|
-
return fig
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
def _add_measurement_annotations(
|
|
418
|
-
ax: Axes,
|
|
419
|
-
trace: WaveformTrace,
|
|
420
|
-
measurements: dict[str, Any],
|
|
421
|
-
time_unit: str,
|
|
422
|
-
multiplier: float,
|
|
423
|
-
) -> None:
|
|
424
|
-
"""Add measurement annotations to plot."""
|
|
425
|
-
# Create annotation text
|
|
426
|
-
text_lines = []
|
|
427
|
-
|
|
428
|
-
for name, value in measurements.items():
|
|
429
|
-
if isinstance(value, dict):
|
|
430
|
-
val = value.get("value", value)
|
|
431
|
-
unit = value.get("unit", "")
|
|
432
|
-
if isinstance(val, float) and not np.isnan(val):
|
|
433
|
-
text_lines.append(f"{name}: {val:.4g} {unit}")
|
|
434
|
-
elif isinstance(value, float) and not np.isnan(value):
|
|
435
|
-
text_lines.append(f"{name}: {value:.4g}")
|
|
436
|
-
|
|
437
|
-
if text_lines:
|
|
438
|
-
text = "\n".join(text_lines)
|
|
439
|
-
ax.annotate(
|
|
440
|
-
text,
|
|
441
|
-
xy=(0.02, 0.98),
|
|
442
|
-
xycoords="axes fraction",
|
|
443
|
-
verticalalignment="top",
|
|
444
|
-
fontfamily="monospace",
|
|
445
|
-
fontsize=8,
|
|
446
|
-
bbox={"boxstyle": "round", "facecolor": "wheat", "alpha": 0.8},
|
|
447
|
-
)
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
__all__ = [
|
|
451
|
-
"plot_multi_channel",
|
|
452
|
-
"plot_waveform",
|
|
453
|
-
"plot_xy",
|
|
454
|
-
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|