oscura 0.8.0__py3-none-any.whl → 0.11.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- oscura/__init__.py +19 -19
- oscura/__main__.py +4 -0
- oscura/analyzers/__init__.py +2 -0
- oscura/analyzers/digital/extraction.py +2 -3
- oscura/analyzers/digital/quality.py +1 -1
- oscura/analyzers/digital/timing.py +1 -1
- oscura/analyzers/ml/signal_classifier.py +6 -0
- oscura/analyzers/patterns/__init__.py +66 -0
- oscura/analyzers/power/basic.py +3 -3
- oscura/analyzers/power/soa.py +1 -1
- oscura/analyzers/power/switching.py +3 -3
- oscura/analyzers/signal_classification.py +529 -0
- oscura/analyzers/signal_integrity/sparams.py +3 -3
- oscura/analyzers/statistics/basic.py +10 -7
- oscura/analyzers/validation.py +1 -1
- oscura/analyzers/waveform/measurements.py +200 -156
- oscura/analyzers/waveform/measurements_with_uncertainty.py +91 -35
- oscura/analyzers/waveform/spectral.py +182 -84
- oscura/api/dsl/commands.py +15 -6
- oscura/api/server/templates/base.html +137 -146
- oscura/api/server/templates/export.html +84 -110
- oscura/api/server/templates/home.html +248 -267
- oscura/api/server/templates/protocols.html +44 -48
- oscura/api/server/templates/reports.html +27 -35
- oscura/api/server/templates/session_detail.html +68 -78
- oscura/api/server/templates/sessions.html +62 -72
- oscura/api/server/templates/waveforms.html +54 -64
- oscura/automotive/__init__.py +1 -1
- oscura/automotive/can/session.py +1 -1
- oscura/automotive/dbc/generator.py +638 -23
- oscura/automotive/dtc/data.json +17 -102
- oscura/automotive/flexray/fibex.py +9 -1
- oscura/automotive/uds/decoder.py +99 -6
- oscura/cli/analyze.py +8 -2
- oscura/cli/batch.py +36 -5
- oscura/cli/characterize.py +18 -4
- oscura/cli/export.py +47 -5
- oscura/cli/main.py +2 -0
- oscura/cli/onboarding/wizard.py +10 -6
- oscura/cli/pipeline.py +585 -0
- oscura/cli/visualize.py +6 -4
- oscura/convenience.py +400 -32
- oscura/core/measurement_result.py +286 -0
- oscura/core/progress.py +1 -1
- oscura/core/schemas/device_mapping.json +2 -8
- oscura/core/schemas/packet_format.json +4 -24
- oscura/core/schemas/protocol_definition.json +2 -12
- oscura/core/types.py +232 -239
- oscura/correlation/multi_protocol.py +1 -1
- oscura/export/legacy/__init__.py +11 -0
- oscura/export/legacy/wav.py +75 -0
- oscura/exporters/__init__.py +19 -0
- oscura/exporters/wireshark.py +809 -0
- oscura/hardware/acquisition/file.py +5 -19
- oscura/hardware/acquisition/saleae.py +10 -10
- oscura/hardware/acquisition/socketcan.py +4 -6
- oscura/hardware/acquisition/synthetic.py +1 -5
- oscura/hardware/acquisition/visa.py +6 -6
- oscura/hardware/security/side_channel_detector.py +5 -508
- oscura/inference/message_format.py +686 -1
- oscura/jupyter/display.py +2 -2
- oscura/jupyter/magic.py +3 -3
- oscura/loaders/__init__.py +17 -12
- oscura/loaders/binary.py +1 -1
- oscura/loaders/chipwhisperer.py +1 -2
- oscura/loaders/configurable.py +1 -1
- oscura/loaders/csv_loader.py +2 -2
- oscura/loaders/hdf5_loader.py +1 -1
- oscura/loaders/lazy.py +6 -1
- oscura/loaders/mmap_loader.py +0 -1
- oscura/loaders/numpy_loader.py +8 -7
- oscura/loaders/preprocessing.py +3 -5
- oscura/loaders/rigol.py +21 -7
- oscura/loaders/sigrok.py +2 -5
- oscura/loaders/tdms.py +3 -2
- oscura/loaders/tektronix.py +38 -32
- oscura/loaders/tss.py +20 -27
- oscura/loaders/validation.py +17 -10
- oscura/loaders/vcd.py +13 -8
- oscura/loaders/wav.py +1 -6
- oscura/pipeline/__init__.py +76 -0
- oscura/pipeline/handlers/__init__.py +165 -0
- oscura/pipeline/handlers/analyzers.py +1045 -0
- oscura/pipeline/handlers/decoders.py +899 -0
- oscura/pipeline/handlers/exporters.py +1103 -0
- oscura/pipeline/handlers/filters.py +891 -0
- oscura/pipeline/handlers/loaders.py +640 -0
- oscura/pipeline/handlers/transforms.py +768 -0
- oscura/reporting/formatting/measurements.py +55 -14
- oscura/reporting/templates/enhanced/protocol_re.html +504 -503
- oscura/sessions/legacy.py +49 -1
- oscura/side_channel/__init__.py +38 -57
- oscura/utils/builders/signal_builder.py +5 -5
- oscura/utils/comparison/compare.py +7 -9
- oscura/utils/comparison/golden.py +1 -1
- oscura/utils/filtering/convenience.py +2 -2
- oscura/utils/math/arithmetic.py +38 -62
- oscura/utils/math/interpolation.py +20 -20
- oscura/utils/pipeline/__init__.py +4 -17
- oscura/utils/progressive.py +1 -4
- oscura/utils/triggering/edge.py +1 -1
- oscura/utils/triggering/pattern.py +2 -2
- oscura/utils/triggering/pulse.py +2 -2
- oscura/utils/triggering/window.py +3 -3
- oscura/validation/hil_testing.py +11 -11
- oscura/visualization/__init__.py +46 -284
- oscura/visualization/batch.py +72 -433
- oscura/visualization/plot.py +542 -53
- oscura/visualization/styles.py +184 -318
- oscura/workflows/batch/advanced.py +1 -1
- oscura/workflows/batch/aggregate.py +12 -9
- oscura/workflows/complete_re.py +251 -23
- oscura/workflows/digital.py +27 -4
- oscura/workflows/multi_trace.py +136 -17
- oscura/workflows/waveform.py +11 -6
- oscura-0.11.0.dist-info/METADATA +460 -0
- {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/RECORD +120 -145
- oscura/side_channel/dpa.py +0 -1025
- oscura/utils/optimization/__init__.py +0 -19
- oscura/utils/optimization/parallel.py +0 -443
- oscura/utils/optimization/search.py +0 -532
- oscura/utils/pipeline/base.py +0 -338
- oscura/utils/pipeline/composition.py +0 -248
- oscura/utils/pipeline/parallel.py +0 -449
- oscura/utils/pipeline/pipeline.py +0 -375
- oscura/utils/search/__init__.py +0 -16
- oscura/utils/search/anomaly.py +0 -424
- oscura/utils/search/context.py +0 -294
- oscura/utils/search/pattern.py +0 -288
- oscura/utils/storage/__init__.py +0 -61
- oscura/utils/storage/database.py +0 -1166
- oscura/visualization/accessibility.py +0 -526
- oscura/visualization/annotations.py +0 -371
- oscura/visualization/axis_scaling.py +0 -305
- oscura/visualization/colors.py +0 -451
- oscura/visualization/digital.py +0 -436
- oscura/visualization/eye.py +0 -571
- oscura/visualization/histogram.py +0 -281
- oscura/visualization/interactive.py +0 -1035
- oscura/visualization/jitter.py +0 -1042
- oscura/visualization/keyboard.py +0 -394
- oscura/visualization/layout.py +0 -400
- oscura/visualization/optimization.py +0 -1079
- oscura/visualization/palettes.py +0 -446
- oscura/visualization/power.py +0 -508
- oscura/visualization/power_extended.py +0 -955
- oscura/visualization/presets.py +0 -469
- oscura/visualization/protocols.py +0 -1246
- oscura/visualization/render.py +0 -223
- oscura/visualization/rendering.py +0 -444
- oscura/visualization/reverse_engineering.py +0 -838
- oscura/visualization/signal_integrity.py +0 -989
- oscura/visualization/specialized.py +0 -643
- oscura/visualization/spectral.py +0 -1226
- oscura/visualization/thumbnails.py +0 -340
- oscura/visualization/time_axis.py +0 -351
- oscura/visualization/waveform.py +0 -454
- oscura-0.8.0.dist-info/METADATA +0 -661
- {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/WHEEL +0 -0
- {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/licenses/LICENSE +0 -0
oscura/visualization/plot.py
CHANGED
|
@@ -1,92 +1,581 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Core plotting functions for Oscura.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
plot.plot_trace(trace)
|
|
3
|
+
Thin matplotlib wrappers for waveform, spectral, and digital signal visualization.
|
|
4
|
+
Each function accepts and returns matplotlib axes for composability.
|
|
6
5
|
|
|
7
|
-
|
|
6
|
+
Example:
|
|
7
|
+
>>> import matplotlib.pyplot as plt
|
|
8
|
+
>>> import oscura as osc
|
|
9
|
+
>>> fig, ax = plt.subplots()
|
|
10
|
+
>>> osc.plot_waveform(ax, trace)
|
|
11
|
+
>>> plt.savefig("waveform.png")
|
|
8
12
|
"""
|
|
9
13
|
|
|
10
14
|
from __future__ import annotations
|
|
11
15
|
|
|
12
16
|
from typing import TYPE_CHECKING, Any
|
|
13
17
|
|
|
18
|
+
import numpy as np
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
import matplotlib.pyplot as plt
|
|
22
|
+
from matplotlib.axes import Axes
|
|
23
|
+
|
|
24
|
+
HAS_MATPLOTLIB = True
|
|
25
|
+
except ImportError:
|
|
26
|
+
HAS_MATPLOTLIB = False
|
|
27
|
+
|
|
28
|
+
|
|
14
29
|
if TYPE_CHECKING:
|
|
15
|
-
from
|
|
16
|
-
|
|
17
|
-
from oscura.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
"""Plot
|
|
46
|
-
|
|
47
|
-
Convenience alias for plot_waveform.
|
|
30
|
+
from numpy.typing import NDArray
|
|
31
|
+
|
|
32
|
+
from oscura.core.types import DigitalTrace, WaveformTrace
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _scale_time(time: NDArray[Any], unit: str) -> tuple[NDArray[Any], str]:
|
|
36
|
+
"""Scale time array to appropriate unit."""
|
|
37
|
+
if unit == "auto":
|
|
38
|
+
max_time = time[-1]
|
|
39
|
+
if max_time < 1e-6:
|
|
40
|
+
unit = "ns"
|
|
41
|
+
elif max_time < 1e-3:
|
|
42
|
+
unit = "us"
|
|
43
|
+
elif max_time < 1:
|
|
44
|
+
unit = "ms"
|
|
45
|
+
else:
|
|
46
|
+
unit = "s"
|
|
47
|
+
scale_factors = {"s": 1.0, "ms": 1e3, "us": 1e6, "ns": 1e9}
|
|
48
|
+
return time * scale_factors.get(unit, 1.0), unit
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def plot_waveform(
|
|
52
|
+
ax: Axes | None,
|
|
53
|
+
trace: WaveformTrace,
|
|
54
|
+
*,
|
|
55
|
+
time_unit: str = "auto",
|
|
56
|
+
color: str = "#4477AA",
|
|
57
|
+
label: str | None = None,
|
|
58
|
+
show_grid: bool = True,
|
|
59
|
+
) -> Axes:
|
|
60
|
+
"""Plot time-domain waveform.
|
|
48
61
|
|
|
49
62
|
Args:
|
|
63
|
+
ax: Matplotlib axes (creates new if None).
|
|
50
64
|
trace: Waveform trace to plot.
|
|
51
|
-
|
|
65
|
+
time_unit: Time unit ("s", "ms", "us", "ns", "auto").
|
|
66
|
+
color: Line color.
|
|
67
|
+
label: Legend label.
|
|
68
|
+
show_grid: Show grid lines.
|
|
52
69
|
|
|
53
70
|
Returns:
|
|
54
71
|
Matplotlib axes object.
|
|
72
|
+
|
|
73
|
+
Example:
|
|
74
|
+
>>> fig, ax = plt.subplots()
|
|
75
|
+
>>> plot_waveform(ax, trace, time_unit="us")
|
|
55
76
|
"""
|
|
56
|
-
|
|
77
|
+
if not HAS_MATPLOTLIB:
|
|
78
|
+
raise ImportError("matplotlib required for plotting")
|
|
79
|
+
|
|
80
|
+
if ax is None:
|
|
81
|
+
_, ax = plt.subplots(figsize=(10, 6))
|
|
82
|
+
|
|
83
|
+
# Scale time
|
|
84
|
+
time = np.arange(len(trace.data)) / trace.metadata.sample_rate
|
|
85
|
+
time_scaled, time_unit = _scale_time(time, time_unit)
|
|
57
86
|
|
|
87
|
+
# Plot
|
|
88
|
+
ax.plot(time_scaled, trace.data, color=color, label=label, linewidth=1.0)
|
|
89
|
+
ax.set_xlabel(f"Time ({time_unit})")
|
|
90
|
+
ax.set_ylabel("Amplitude (V)")
|
|
91
|
+
if show_grid:
|
|
92
|
+
ax.grid(True, alpha=0.3, linestyle="--")
|
|
93
|
+
if label:
|
|
94
|
+
ax.legend()
|
|
58
95
|
|
|
59
|
-
|
|
60
|
-
"""Add annotation to current plot.
|
|
96
|
+
return ax
|
|
61
97
|
|
|
62
|
-
|
|
98
|
+
|
|
99
|
+
def plot_multi_channel(
|
|
100
|
+
ax: Axes | None,
|
|
101
|
+
traces: list[WaveformTrace],
|
|
102
|
+
*,
|
|
103
|
+
labels: list[str] | None = None,
|
|
104
|
+
colors: list[str] | None = None,
|
|
105
|
+
offset: float | None = None,
|
|
106
|
+
time_unit: str = "auto",
|
|
107
|
+
) -> Axes:
|
|
108
|
+
"""Plot multiple waveforms on same axes.
|
|
63
109
|
|
|
64
110
|
Args:
|
|
65
|
-
|
|
66
|
-
|
|
111
|
+
ax: Matplotlib axes (creates new if None).
|
|
112
|
+
traces: List of waveform traces.
|
|
113
|
+
labels: Channel labels.
|
|
114
|
+
colors: Line colors.
|
|
115
|
+
offset: Vertical offset between channels (auto if None).
|
|
116
|
+
time_unit: Time unit for x-axis.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Matplotlib axes object.
|
|
120
|
+
|
|
121
|
+
Example:
|
|
122
|
+
>>> fig, ax = plt.subplots()
|
|
123
|
+
>>> plot_multi_channel(ax, [ch1, ch2, ch3], labels=["CH1", "CH2", "CH3"])
|
|
67
124
|
"""
|
|
68
|
-
|
|
125
|
+
if not HAS_MATPLOTLIB:
|
|
126
|
+
raise ImportError("matplotlib required for plotting")
|
|
127
|
+
|
|
128
|
+
if ax is None:
|
|
129
|
+
_, ax = plt.subplots(figsize=(10, 8))
|
|
130
|
+
|
|
131
|
+
default_colors = ["#4477AA", "#EE6677", "#228833", "#CCBB44", "#AA3377", "#66CCEE"]
|
|
132
|
+
colors = colors or default_colors
|
|
133
|
+
labels = labels or [f"Channel {i + 1}" for i in range(len(traces))]
|
|
134
|
+
|
|
135
|
+
# Auto-calculate offset
|
|
136
|
+
if offset is None:
|
|
137
|
+
all_data = np.concatenate([t.data for t in traces])
|
|
138
|
+
offset = (np.max(all_data) - np.min(all_data)) * 1.2
|
|
139
|
+
|
|
140
|
+
# Plot each channel
|
|
141
|
+
for i, trace in enumerate(traces):
|
|
142
|
+
time = np.arange(len(trace.data)) / trace.metadata.sample_rate
|
|
143
|
+
time_scaled, time_unit = _scale_time(time, time_unit)
|
|
144
|
+
ax.plot(
|
|
145
|
+
time_scaled,
|
|
146
|
+
trace.data + i * offset,
|
|
147
|
+
color=colors[i % len(colors)],
|
|
148
|
+
label=labels[i] if i < len(labels) else f"Channel {i + 1}",
|
|
149
|
+
linewidth=1.0,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
ax.set_xlabel(f"Time ({time_unit})")
|
|
153
|
+
ax.set_ylabel("Amplitude (V)")
|
|
154
|
+
ax.grid(True, alpha=0.3, linestyle="--")
|
|
155
|
+
ax.legend()
|
|
156
|
+
return ax
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _scale_freq(freq: NDArray[Any], unit: str) -> tuple[NDArray[Any], str]:
|
|
160
|
+
"""Scale frequency array to appropriate unit."""
|
|
161
|
+
if unit == "auto":
|
|
162
|
+
max_freq = freq[-1]
|
|
163
|
+
if max_freq > 1e9:
|
|
164
|
+
unit = "GHz"
|
|
165
|
+
elif max_freq > 1e6:
|
|
166
|
+
unit = "MHz"
|
|
167
|
+
elif max_freq > 1e3:
|
|
168
|
+
unit = "kHz"
|
|
169
|
+
else:
|
|
170
|
+
unit = "Hz"
|
|
171
|
+
divisors = {"Hz": 1.0, "kHz": 1e3, "MHz": 1e6, "GHz": 1e9}
|
|
172
|
+
return freq / divisors.get(unit, 1.0), unit
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def plot_fft(
|
|
176
|
+
ax: Axes | None,
|
|
177
|
+
trace: WaveformTrace,
|
|
178
|
+
*,
|
|
179
|
+
freq_unit: str = "auto",
|
|
180
|
+
db_scale: bool = True,
|
|
181
|
+
window: str = "hann",
|
|
182
|
+
color: str = "#4477AA",
|
|
183
|
+
) -> Axes:
|
|
184
|
+
"""Plot FFT magnitude spectrum.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
ax: Matplotlib axes (creates new if None).
|
|
188
|
+
trace: Waveform trace to analyze.
|
|
189
|
+
freq_unit: Frequency unit ("Hz", "kHz", "MHz", "auto").
|
|
190
|
+
db_scale: Use dB scale for magnitude.
|
|
191
|
+
window: Window function ("hann", "hamming", "blackman", "none").
|
|
192
|
+
color: Line color.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
Matplotlib axes object.
|
|
196
|
+
|
|
197
|
+
Example:
|
|
198
|
+
>>> fig, ax = plt.subplots()
|
|
199
|
+
>>> plot_fft(ax, trace, freq_unit="kHz")
|
|
200
|
+
"""
|
|
201
|
+
if not HAS_MATPLOTLIB:
|
|
202
|
+
raise ImportError("matplotlib required for plotting")
|
|
203
|
+
|
|
204
|
+
if ax is None:
|
|
205
|
+
_, ax = plt.subplots(figsize=(10, 6))
|
|
206
|
+
|
|
207
|
+
from oscura.analyzers.waveform.spectral import fft
|
|
208
|
+
|
|
209
|
+
fft_result = fft(trace, window=window)
|
|
210
|
+
freqs = fft_result[0]
|
|
211
|
+
mags = fft_result[1]
|
|
212
|
+
freqs_scaled, freq_unit = _scale_freq(freqs, freq_unit)
|
|
213
|
+
|
|
214
|
+
if db_scale:
|
|
215
|
+
mags = 20 * np.log10(np.abs(mags) + 1e-12)
|
|
216
|
+
ylabel = "Magnitude (dB)"
|
|
217
|
+
else:
|
|
218
|
+
ylabel = "Magnitude"
|
|
219
|
+
|
|
220
|
+
ax.plot(freqs_scaled, mags, color=color, linewidth=1.0)
|
|
221
|
+
ax.set_xlabel(f"Frequency ({freq_unit})")
|
|
222
|
+
ax.set_ylabel(ylabel)
|
|
223
|
+
ax.grid(True, alpha=0.3, linestyle="--")
|
|
224
|
+
return ax
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def plot_psd(
|
|
228
|
+
ax: Axes | None,
|
|
229
|
+
trace: WaveformTrace,
|
|
230
|
+
*,
|
|
231
|
+
freq_unit: str = "auto",
|
|
232
|
+
window: str = "hann",
|
|
233
|
+
nperseg: int | None = None,
|
|
234
|
+
color: str = "#4477AA",
|
|
235
|
+
) -> Axes:
|
|
236
|
+
"""Plot power spectral density.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
ax: Matplotlib axes (creates new if None).
|
|
240
|
+
trace: Waveform trace to analyze.
|
|
241
|
+
freq_unit: Frequency unit ("Hz", "kHz", "MHz", "auto").
|
|
242
|
+
window: Window function.
|
|
243
|
+
nperseg: Segment length for Welch method.
|
|
244
|
+
color: Line color.
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
Matplotlib axes object.
|
|
248
|
+
|
|
249
|
+
Example:
|
|
250
|
+
>>> fig, ax = plt.subplots()
|
|
251
|
+
>>> plot_psd(ax, trace)
|
|
252
|
+
"""
|
|
253
|
+
if not HAS_MATPLOTLIB:
|
|
254
|
+
raise ImportError("matplotlib required for plotting")
|
|
255
|
+
|
|
256
|
+
if ax is None:
|
|
257
|
+
_, ax = plt.subplots(figsize=(10, 6))
|
|
258
|
+
|
|
259
|
+
from oscura.analyzers.waveform.spectral import psd
|
|
260
|
+
|
|
261
|
+
psd_result = psd(trace, window=window, nperseg=nperseg)
|
|
262
|
+
freqs = psd_result[0]
|
|
263
|
+
power = psd_result[1]
|
|
264
|
+
freqs_scaled, freq_unit = _scale_freq(freqs, freq_unit)
|
|
265
|
+
|
|
266
|
+
ax.plot(freqs_scaled, 10 * np.log10(power + 1e-12), color=color, linewidth=1.0)
|
|
267
|
+
ax.set_xlabel(f"Frequency ({freq_unit})")
|
|
268
|
+
ax.set_ylabel("Power Spectral Density (dB/Hz)")
|
|
269
|
+
ax.grid(True, alpha=0.3, linestyle="--")
|
|
270
|
+
return ax
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def plot_spectrogram(
|
|
274
|
+
ax: Axes | None,
|
|
275
|
+
trace: WaveformTrace,
|
|
276
|
+
*,
|
|
277
|
+
nfft: int = 1024,
|
|
278
|
+
noverlap: int | None = None,
|
|
279
|
+
cmap: str = "viridis",
|
|
280
|
+
) -> Axes:
|
|
281
|
+
"""Plot spectrogram (time-frequency representation).
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
ax: Matplotlib axes (creates new if None).
|
|
285
|
+
trace: Waveform trace to analyze.
|
|
286
|
+
nfft: FFT window size.
|
|
287
|
+
noverlap: Overlap samples (default: nfft // 2).
|
|
288
|
+
cmap: Colormap name.
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
Matplotlib axes object.
|
|
292
|
+
|
|
293
|
+
Example:
|
|
294
|
+
>>> fig, ax = plt.subplots()
|
|
295
|
+
>>> plot_spectrogram(ax, trace, nfft=512)
|
|
296
|
+
"""
|
|
297
|
+
if not HAS_MATPLOTLIB:
|
|
298
|
+
raise ImportError("matplotlib required for plotting")
|
|
299
|
+
|
|
300
|
+
if ax is None:
|
|
301
|
+
_, ax = plt.subplots(figsize=(10, 6))
|
|
302
|
+
|
|
303
|
+
if noverlap is None:
|
|
304
|
+
noverlap = nfft // 2
|
|
305
|
+
|
|
306
|
+
# Generate spectrogram
|
|
307
|
+
sample_rate = trace.metadata.sample_rate
|
|
308
|
+
ax.specgram(trace.data, Fs=sample_rate, NFFT=nfft, noverlap=noverlap, cmap=cmap)
|
|
309
|
+
|
|
310
|
+
ax.set_xlabel("Time (s)")
|
|
311
|
+
ax.set_ylabel("Frequency (Hz)")
|
|
312
|
+
|
|
313
|
+
return ax
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def plot_eye(
|
|
317
|
+
ax: Axes | None,
|
|
318
|
+
trace: WaveformTrace,
|
|
319
|
+
*,
|
|
320
|
+
symbol_rate: float,
|
|
321
|
+
n_symbols: int = 100,
|
|
322
|
+
color: str = "#4477AA",
|
|
323
|
+
alpha: float = 0.1,
|
|
324
|
+
) -> Axes:
|
|
325
|
+
"""Plot eye diagram for digital signals.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
ax: Matplotlib axes (creates new if None).
|
|
329
|
+
trace: Waveform trace.
|
|
330
|
+
symbol_rate: Symbol rate in Hz.
|
|
331
|
+
n_symbols: Number of symbols to overlay.
|
|
332
|
+
color: Line color.
|
|
333
|
+
alpha: Line transparency.
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
Matplotlib axes object.
|
|
337
|
+
|
|
338
|
+
Example:
|
|
339
|
+
>>> fig, ax = plt.subplots()
|
|
340
|
+
>>> plot_eye(ax, trace, symbol_rate=1e6)
|
|
341
|
+
"""
|
|
342
|
+
if not HAS_MATPLOTLIB:
|
|
343
|
+
raise ImportError("matplotlib required for plotting")
|
|
344
|
+
|
|
345
|
+
if ax is None:
|
|
346
|
+
_, ax = plt.subplots(figsize=(8, 6))
|
|
347
|
+
|
|
348
|
+
# Calculate samples per symbol
|
|
349
|
+
sample_rate = trace.metadata.sample_rate
|
|
350
|
+
samples_per_symbol = int(sample_rate / symbol_rate)
|
|
351
|
+
|
|
352
|
+
# Overlay multiple symbols
|
|
353
|
+
for i in range(n_symbols):
|
|
354
|
+
start = i * samples_per_symbol
|
|
355
|
+
end = start + 2 * samples_per_symbol # Two symbols for eye
|
|
356
|
+
if end > len(trace.data):
|
|
357
|
+
break
|
|
358
|
+
|
|
359
|
+
segment = trace.data[start:end]
|
|
360
|
+
time = np.arange(len(segment)) / sample_rate * 1e6 # Convert to microseconds
|
|
361
|
+
ax.plot(time, segment, color=color, alpha=alpha, linewidth=0.8)
|
|
362
|
+
|
|
363
|
+
ax.set_xlabel("Time (μs)")
|
|
364
|
+
ax.set_ylabel("Amplitude (V)")
|
|
365
|
+
ax.set_title("Eye Diagram")
|
|
366
|
+
ax.grid(True, alpha=0.3)
|
|
367
|
+
|
|
368
|
+
return ax
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def plot_timing(
|
|
372
|
+
ax: Axes | None,
|
|
373
|
+
trace: DigitalTrace,
|
|
374
|
+
*,
|
|
375
|
+
time_unit: str = "us",
|
|
376
|
+
color: str = "#4477AA",
|
|
377
|
+
label: str | None = None,
|
|
378
|
+
) -> Axes:
|
|
379
|
+
"""Plot digital timing diagram.
|
|
380
|
+
|
|
381
|
+
Args:
|
|
382
|
+
ax: Matplotlib axes (creates new if None).
|
|
383
|
+
trace: Digital trace.
|
|
384
|
+
time_unit: Time unit ("s", "ms", "us", "ns").
|
|
385
|
+
color: Line color.
|
|
386
|
+
label: Signal label.
|
|
387
|
+
|
|
388
|
+
Returns:
|
|
389
|
+
Matplotlib axes object.
|
|
390
|
+
|
|
391
|
+
Example:
|
|
392
|
+
>>> fig, ax = plt.subplots()
|
|
393
|
+
>>> plot_timing(ax, digital_trace)
|
|
394
|
+
"""
|
|
395
|
+
if not HAS_MATPLOTLIB:
|
|
396
|
+
raise ImportError("matplotlib required for plotting")
|
|
397
|
+
|
|
398
|
+
if ax is None:
|
|
399
|
+
_, ax = plt.subplots(figsize=(10, 4))
|
|
400
|
+
|
|
401
|
+
time = np.arange(len(trace.data)) / trace.metadata.sample_rate
|
|
402
|
+
time_scaled, time_unit = _scale_time(time, time_unit)
|
|
403
|
+
|
|
404
|
+
ax.step(
|
|
405
|
+
time_scaled, trace.data.astype(float), where="post", color=color, label=label, linewidth=1.5
|
|
406
|
+
)
|
|
407
|
+
ax.fill_between(time_scaled, 0, trace.data.astype(float), step="post", alpha=0.2, color=color)
|
|
408
|
+
|
|
409
|
+
ax.set_xlabel(f"Time ({time_unit})")
|
|
410
|
+
ax.set_ylabel("Logic Level")
|
|
411
|
+
ax.set_ylim(-0.2, 1.3)
|
|
412
|
+
ax.set_yticks([0, 1])
|
|
413
|
+
ax.set_yticklabels(["LOW", "HIGH"])
|
|
414
|
+
ax.grid(True, alpha=0.3, axis="x")
|
|
415
|
+
if label:
|
|
416
|
+
ax.legend()
|
|
417
|
+
return ax
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def plot_histogram(
|
|
421
|
+
ax: Axes | None,
|
|
422
|
+
data: NDArray[Any],
|
|
423
|
+
*,
|
|
424
|
+
bins: int = 50,
|
|
425
|
+
color: str = "#4477AA",
|
|
426
|
+
show_stats: bool = True,
|
|
427
|
+
) -> Axes:
|
|
428
|
+
"""Plot amplitude histogram with optional statistics.
|
|
429
|
+
|
|
430
|
+
Args:
|
|
431
|
+
ax: Matplotlib axes (creates new if None).
|
|
432
|
+
data: Signal data array.
|
|
433
|
+
bins: Number of histogram bins.
|
|
434
|
+
color: Bar color.
|
|
435
|
+
show_stats: Show mean/std overlays.
|
|
436
|
+
|
|
437
|
+
Returns:
|
|
438
|
+
Matplotlib axes object.
|
|
439
|
+
|
|
440
|
+
Example:
|
|
441
|
+
>>> fig, ax = plt.subplots()
|
|
442
|
+
>>> plot_histogram(ax, trace.data, bins=100)
|
|
443
|
+
"""
|
|
444
|
+
if not HAS_MATPLOTLIB:
|
|
445
|
+
raise ImportError("matplotlib required for plotting")
|
|
446
|
+
|
|
447
|
+
if ax is None:
|
|
448
|
+
_, ax = plt.subplots(figsize=(8, 6))
|
|
449
|
+
|
|
450
|
+
ax.hist(data, bins=bins, color=color, alpha=0.7, edgecolor="black", linewidth=0.5)
|
|
451
|
+
|
|
452
|
+
if show_stats:
|
|
453
|
+
mean_val, std_val = np.mean(data), np.std(data)
|
|
454
|
+
ax.axvline(
|
|
455
|
+
mean_val, color="#EE6677", linestyle="--", linewidth=2, label=f"Mean: {mean_val:.3f}"
|
|
456
|
+
)
|
|
457
|
+
ax.axvline(
|
|
458
|
+
mean_val + std_val,
|
|
459
|
+
color="#AAA",
|
|
460
|
+
linestyle=":",
|
|
461
|
+
linewidth=1.5,
|
|
462
|
+
label=f"±1std: {std_val:.3f}",
|
|
463
|
+
)
|
|
464
|
+
ax.axvline(mean_val - std_val, color="#AAA", linestyle=":", linewidth=1.5)
|
|
465
|
+
ax.legend()
|
|
466
|
+
|
|
467
|
+
ax.set_xlabel("Amplitude (V)")
|
|
468
|
+
ax.set_ylabel("Count")
|
|
469
|
+
ax.grid(True, alpha=0.3, axis="y")
|
|
470
|
+
return ax
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def plot_xy(
|
|
474
|
+
ax: Axes | None,
|
|
475
|
+
trace_x: WaveformTrace,
|
|
476
|
+
trace_y: WaveformTrace,
|
|
477
|
+
*,
|
|
478
|
+
color: str = "#4477AA",
|
|
479
|
+
marker: str = "o",
|
|
480
|
+
markersize: float = 3.0,
|
|
481
|
+
) -> Axes:
|
|
482
|
+
"""Plot XY scatter (Lissajous pattern).
|
|
483
|
+
|
|
484
|
+
Args:
|
|
485
|
+
ax: Matplotlib axes (creates new if None).
|
|
486
|
+
trace_x: X-axis trace.
|
|
487
|
+
trace_y: Y-axis trace.
|
|
488
|
+
color: Marker color.
|
|
489
|
+
marker: Marker style.
|
|
490
|
+
markersize: Marker size.
|
|
491
|
+
|
|
492
|
+
Returns:
|
|
493
|
+
Matplotlib axes object.
|
|
494
|
+
|
|
495
|
+
Example:
|
|
496
|
+
>>> fig, ax = plt.subplots()
|
|
497
|
+
>>> plot_xy(ax, ch1, ch2)
|
|
498
|
+
"""
|
|
499
|
+
if not HAS_MATPLOTLIB:
|
|
500
|
+
raise ImportError("matplotlib required for plotting")
|
|
501
|
+
|
|
502
|
+
if ax is None:
|
|
503
|
+
_, ax = plt.subplots(figsize=(8, 8))
|
|
504
|
+
|
|
505
|
+
min_len = min(len(trace_x.data), len(trace_y.data))
|
|
506
|
+
ax.plot(
|
|
507
|
+
trace_x.data[:min_len],
|
|
508
|
+
trace_y.data[:min_len],
|
|
509
|
+
marker=marker,
|
|
510
|
+
linestyle="",
|
|
511
|
+
color=color,
|
|
512
|
+
markersize=markersize,
|
|
513
|
+
alpha=0.5,
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
ax.set_xlabel("X Amplitude (V)")
|
|
517
|
+
ax.set_ylabel("Y Amplitude (V)")
|
|
518
|
+
ax.set_aspect("equal")
|
|
519
|
+
ax.grid(True, alpha=0.3)
|
|
520
|
+
return ax
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
# Stub functions for backwards compatibility with demos
|
|
524
|
+
def plot_bathtub(*args: Any, **kwargs: Any) -> Axes:
|
|
525
|
+
"""Stub for plot_bathtub (not implemented in refactored version)."""
|
|
526
|
+
raise NotImplementedError("plot_bathtub removed in visualization refactor")
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def plot_protocol_decode(*args: Any, **kwargs: Any) -> Axes:
|
|
530
|
+
"""Stub for plot_protocol_decode (not implemented in refactored version)."""
|
|
531
|
+
raise NotImplementedError("plot_protocol_decode removed in visualization refactor")
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def plot_state_machine(*args: Any, **kwargs: Any) -> Axes:
|
|
535
|
+
"""Stub for plot_state_machine (not implemented in refactored version)."""
|
|
536
|
+
raise NotImplementedError("plot_state_machine removed in visualization refactor")
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def plot_thd_bars(*args: Any, **kwargs: Any) -> Axes:
|
|
540
|
+
"""Stub for plot_thd_bars (not implemented in refactored version)."""
|
|
541
|
+
raise NotImplementedError("plot_thd_bars removed in visualization refactor")
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def plot_quality_summary(*args: Any, **kwargs: Any) -> Axes:
|
|
545
|
+
"""Stub for plot_quality_summary (not implemented in refactored version)."""
|
|
546
|
+
raise NotImplementedError("plot_quality_summary removed in visualization refactor")
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def plot_logic_analyzer(*args: Any, **kwargs: Any) -> Axes:
|
|
550
|
+
"""Alias for plot_timing for backwards compatibility."""
|
|
551
|
+
if not HAS_MATPLOTLIB:
|
|
552
|
+
raise ImportError("matplotlib required for plotting")
|
|
553
|
+
# Redirect to plot_timing
|
|
554
|
+
return plot_timing(*args, **kwargs)
|
|
555
|
+
|
|
69
556
|
|
|
70
|
-
|
|
71
|
-
|
|
557
|
+
def plot_spectrum(*args: Any, **kwargs: Any) -> Axes:
|
|
558
|
+
"""Alias for plot_fft for backwards compatibility."""
|
|
559
|
+
if not HAS_MATPLOTLIB:
|
|
560
|
+
raise ImportError("matplotlib required for plotting")
|
|
561
|
+
return plot_fft(*args, **kwargs)
|
|
72
562
|
|
|
73
563
|
|
|
74
564
|
__all__ = [
|
|
75
|
-
"add_annotation",
|
|
76
565
|
"plot_bathtub",
|
|
77
|
-
"plot_bode",
|
|
78
566
|
"plot_eye",
|
|
79
567
|
"plot_fft",
|
|
80
568
|
"plot_histogram",
|
|
81
569
|
"plot_logic_analyzer",
|
|
82
570
|
"plot_multi_channel",
|
|
83
|
-
"
|
|
571
|
+
"plot_protocol_decode",
|
|
84
572
|
"plot_psd",
|
|
573
|
+
"plot_quality_summary",
|
|
85
574
|
"plot_spectrogram",
|
|
86
575
|
"plot_spectrum",
|
|
576
|
+
"plot_state_machine",
|
|
577
|
+
"plot_thd_bars",
|
|
87
578
|
"plot_timing",
|
|
88
|
-
"plot_trace",
|
|
89
|
-
"plot_waterfall",
|
|
90
579
|
"plot_waveform",
|
|
91
580
|
"plot_xy",
|
|
92
581
|
]
|