oscura 0.7.0__py3-none-any.whl → 0.8.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 +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 +28 -0
- oscura/analyzers/patterns/reverse_engineering.py +991 -0
- oscura/analyzers/power/__init__.py +35 -12
- oscura/analyzers/statistics/__init__.py +4 -0
- oscura/analyzers/statistics/basic.py +149 -0
- oscura/analyzers/statistics/correlation.py +47 -6
- oscura/analyzers/waveform/__init__.py +2 -0
- oscura/analyzers/waveform/measurements.py +145 -23
- oscura/analyzers/waveform/spectral.py +361 -8
- oscura/automotive/__init__.py +1 -1
- oscura/automotive/dtc/data.json +102 -17
- oscura/core/config/loader.py +0 -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 +108 -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 +279 -0
- oscura/reporting/html.py +57 -0
- oscura/reporting/interpretation.py +431 -0
- oscura/reporting/summary.py +329 -0
- oscura/reporting/visualization.py +542 -0
- oscura/visualization/__init__.py +2 -1
- oscura/visualization/batch.py +521 -0
- oscura/workflows/__init__.py +2 -0
- oscura/workflows/waveform.py +783 -0
- {oscura-0.7.0.dist-info → oscura-0.8.0.dist-info}/METADATA +1 -1
- {oscura-0.7.0.dist-info → oscura-0.8.0.dist-info}/RECORD +40 -29
- {oscura-0.7.0.dist-info → oscura-0.8.0.dist-info}/WHEEL +0 -0
- {oscura-0.7.0.dist-info → oscura-0.8.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.7.0.dist-info → oscura-0.8.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
"""Batch plot generation for comprehensive signal visualization.
|
|
2
|
+
|
|
3
|
+
This module provides utilities for generating multiple related plots from
|
|
4
|
+
signal traces in a single operation, useful for comprehensive analysis reports.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
>>> from oscura.visualization import batch
|
|
8
|
+
>>> trace = osc.load("signal.wfm")
|
|
9
|
+
>>> plots = batch.generate_all_plots(trace, output_format="base64")
|
|
10
|
+
>>> # Returns: {"waveform": <base64>, "fft": <base64>, ...}
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import base64
|
|
16
|
+
from io import BytesIO
|
|
17
|
+
from typing import TYPE_CHECKING, Any
|
|
18
|
+
|
|
19
|
+
import matplotlib
|
|
20
|
+
|
|
21
|
+
matplotlib.use("Agg") # Set non-interactive backend before importing pyplot
|
|
22
|
+
import matplotlib.pyplot as plt
|
|
23
|
+
import numpy as np
|
|
24
|
+
|
|
25
|
+
# Import trace types for runtime isinstance checks
|
|
26
|
+
from oscura.core.types import DigitalTrace, WaveformTrace
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from matplotlib.figure import Figure
|
|
30
|
+
from numpy.typing import NDArray
|
|
31
|
+
|
|
32
|
+
# Plot configuration constants
|
|
33
|
+
PLOT_DPI = 150
|
|
34
|
+
FIGURE_SIZE = (10, 6)
|
|
35
|
+
|
|
36
|
+
# Colorblind-safe palette (Tol Bright)
|
|
37
|
+
COLORS = {
|
|
38
|
+
"primary": "#4477AA", # Blue
|
|
39
|
+
"secondary": "#EE6677", # Red
|
|
40
|
+
"success": "#228833", # Green
|
|
41
|
+
"warning": "#CCBB44", # Yellow
|
|
42
|
+
"danger": "#CC78BC", # Purple
|
|
43
|
+
"gray": "#949494", # Gray
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def fig_to_base64(fig: Figure, *, dpi: int = PLOT_DPI) -> str:
|
|
48
|
+
"""Convert matplotlib figure to base64-encoded PNG.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
fig: Matplotlib figure object.
|
|
52
|
+
dpi: Resolution in dots per inch.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Base64-encoded PNG image string with data URI prefix.
|
|
56
|
+
|
|
57
|
+
Example:
|
|
58
|
+
>>> import matplotlib.pyplot as plt
|
|
59
|
+
>>> fig, ax = plt.subplots()
|
|
60
|
+
>>> ax.plot([1, 2, 3])
|
|
61
|
+
>>> b64_str = fig_to_base64(fig)
|
|
62
|
+
>>> assert b64_str.startswith("data:image/png;base64,")
|
|
63
|
+
"""
|
|
64
|
+
buf = BytesIO()
|
|
65
|
+
fig.savefig(
|
|
66
|
+
buf, format="png", dpi=dpi, bbox_inches="tight", facecolor="white", edgecolor="none"
|
|
67
|
+
)
|
|
68
|
+
buf.seek(0)
|
|
69
|
+
img_base64 = base64.b64encode(buf.read()).decode("utf-8")
|
|
70
|
+
plt.close(fig)
|
|
71
|
+
return f"data:image/png;base64,{img_base64}"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def plot_waveform(
|
|
75
|
+
trace: WaveformTrace | DigitalTrace,
|
|
76
|
+
*,
|
|
77
|
+
title: str = "Time-Domain Waveform",
|
|
78
|
+
sample_limit: int = 10000,
|
|
79
|
+
) -> str:
|
|
80
|
+
"""Generate time-domain waveform plot.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
trace: WaveformTrace or DigitalTrace to plot.
|
|
84
|
+
title: Plot title.
|
|
85
|
+
sample_limit: Maximum samples to plot (downsamples if exceeded).
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Base64-encoded PNG image string.
|
|
89
|
+
|
|
90
|
+
Example:
|
|
91
|
+
>>> trace = osc.load("signal.wfm")
|
|
92
|
+
>>> plot_data = plot_waveform(trace, title="My Signal")
|
|
93
|
+
"""
|
|
94
|
+
fig, ax = plt.subplots(figsize=FIGURE_SIZE)
|
|
95
|
+
|
|
96
|
+
# Prepare data with downsampling if needed
|
|
97
|
+
data = trace.data
|
|
98
|
+
if len(data) > sample_limit:
|
|
99
|
+
step = len(data) // sample_limit
|
|
100
|
+
data = data[::step]
|
|
101
|
+
time = np.arange(len(data)) * step / trace.metadata.sample_rate
|
|
102
|
+
else:
|
|
103
|
+
time = np.arange(len(data)) / trace.metadata.sample_rate
|
|
104
|
+
|
|
105
|
+
# Plot waveform
|
|
106
|
+
ax.plot(time * 1000, data, color=COLORS["primary"], linewidth=0.8, alpha=0.9)
|
|
107
|
+
|
|
108
|
+
# Styling
|
|
109
|
+
ax.set_xlabel("Time (ms)", fontsize=11, fontweight="bold")
|
|
110
|
+
is_bool = isinstance(data[0], (bool, np.bool_)) if len(data) > 0 else False
|
|
111
|
+
ylabel = "Logic Level" if is_bool else "Amplitude (V)"
|
|
112
|
+
ax.set_ylabel(ylabel, fontsize=11, fontweight="bold")
|
|
113
|
+
ax.set_title(title, fontsize=13, fontweight="bold", pad=15)
|
|
114
|
+
ax.grid(True, alpha=0.3, linestyle="--")
|
|
115
|
+
|
|
116
|
+
# Add mean line for analog signals
|
|
117
|
+
if not is_bool:
|
|
118
|
+
mean_val = float(np.mean(data))
|
|
119
|
+
ax.axhline(
|
|
120
|
+
mean_val,
|
|
121
|
+
color=COLORS["danger"],
|
|
122
|
+
linestyle="--",
|
|
123
|
+
linewidth=1.5,
|
|
124
|
+
alpha=0.7,
|
|
125
|
+
label=f"Mean: {mean_val:.3f} V",
|
|
126
|
+
)
|
|
127
|
+
ax.legend(loc="upper right", fontsize=9)
|
|
128
|
+
|
|
129
|
+
plt.tight_layout()
|
|
130
|
+
return fig_to_base64(fig)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def plot_fft_spectrum(
|
|
134
|
+
trace: WaveformTrace,
|
|
135
|
+
*,
|
|
136
|
+
title: str = "FFT Magnitude Spectrum",
|
|
137
|
+
) -> str:
|
|
138
|
+
"""Generate FFT magnitude spectrum plot.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
trace: WaveformTrace to analyze.
|
|
142
|
+
title: Plot title.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Base64-encoded PNG image string.
|
|
146
|
+
|
|
147
|
+
Example:
|
|
148
|
+
>>> trace = osc.load("signal.wfm")
|
|
149
|
+
>>> spectrum_plot = plot_fft_spectrum(trace)
|
|
150
|
+
"""
|
|
151
|
+
import oscura as osc
|
|
152
|
+
|
|
153
|
+
fig, ax = plt.subplots(figsize=FIGURE_SIZE)
|
|
154
|
+
|
|
155
|
+
# Compute FFT using oscura framework
|
|
156
|
+
fft_result = osc.fft(trace)
|
|
157
|
+
freqs = fft_result[0]
|
|
158
|
+
mags = fft_result[1]
|
|
159
|
+
|
|
160
|
+
# Convert to dB
|
|
161
|
+
mags_db = 20 * np.log10(np.abs(mags) + 1e-12)
|
|
162
|
+
|
|
163
|
+
# Plot only positive frequencies up to Nyquist
|
|
164
|
+
nyquist_idx = len(freqs) // 2
|
|
165
|
+
freqs_plot = freqs[:nyquist_idx]
|
|
166
|
+
mags_plot = mags_db[:nyquist_idx]
|
|
167
|
+
|
|
168
|
+
ax.plot(freqs_plot / 1000, mags_plot, color=COLORS["primary"], linewidth=1.2)
|
|
169
|
+
|
|
170
|
+
# Find and mark fundamental frequency
|
|
171
|
+
max_idx = np.argmax(mags_plot[10:]) + 10 # Skip DC component
|
|
172
|
+
fund_freq = freqs_plot[max_idx]
|
|
173
|
+
fund_mag = mags_plot[max_idx]
|
|
174
|
+
ax.plot(
|
|
175
|
+
fund_freq / 1000,
|
|
176
|
+
fund_mag,
|
|
177
|
+
"o",
|
|
178
|
+
color=COLORS["secondary"],
|
|
179
|
+
markersize=10,
|
|
180
|
+
label=f"Fundamental: {fund_freq:.1f} Hz",
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# Styling
|
|
184
|
+
ax.set_xlabel("Frequency (kHz)", fontsize=11, fontweight="bold")
|
|
185
|
+
ax.set_ylabel("Magnitude (dB)", fontsize=11, fontweight="bold")
|
|
186
|
+
ax.set_title(title, fontsize=13, fontweight="bold", pad=15)
|
|
187
|
+
ax.grid(True, alpha=0.3, linestyle="--")
|
|
188
|
+
ax.legend(loc="upper right", fontsize=9)
|
|
189
|
+
|
|
190
|
+
plt.tight_layout()
|
|
191
|
+
return fig_to_base64(fig)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def plot_histogram(
|
|
195
|
+
data: NDArray[Any],
|
|
196
|
+
*,
|
|
197
|
+
title: str = "Amplitude Distribution",
|
|
198
|
+
bins: int = 50,
|
|
199
|
+
) -> str:
|
|
200
|
+
"""Generate amplitude histogram with statistical overlays.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
data: Signal data array.
|
|
204
|
+
title: Plot title.
|
|
205
|
+
bins: Number of histogram bins.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
Base64-encoded PNG image string.
|
|
209
|
+
|
|
210
|
+
Example:
|
|
211
|
+
>>> import numpy as np
|
|
212
|
+
>>> data = np.random.randn(1000)
|
|
213
|
+
>>> hist_plot = plot_histogram(data)
|
|
214
|
+
"""
|
|
215
|
+
fig, ax = plt.subplots(figsize=FIGURE_SIZE)
|
|
216
|
+
|
|
217
|
+
# Create histogram
|
|
218
|
+
_n, _bins_edges, _patches = ax.hist(
|
|
219
|
+
data, bins=bins, color=COLORS["primary"], alpha=0.7, edgecolor="black", linewidth=0.5
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# Add statistical overlays
|
|
223
|
+
mean_val = np.mean(data)
|
|
224
|
+
median_val = np.median(data)
|
|
225
|
+
std_val = np.std(data)
|
|
226
|
+
|
|
227
|
+
ax.axvline(
|
|
228
|
+
mean_val, color=COLORS["danger"], linestyle="--", linewidth=2, label=f"Mean: {mean_val:.3f}"
|
|
229
|
+
)
|
|
230
|
+
ax.axvline(
|
|
231
|
+
median_val,
|
|
232
|
+
color=COLORS["success"],
|
|
233
|
+
linestyle="--",
|
|
234
|
+
linewidth=2,
|
|
235
|
+
label=f"Median: {median_val:.3f}",
|
|
236
|
+
)
|
|
237
|
+
ax.axvline(
|
|
238
|
+
mean_val + std_val,
|
|
239
|
+
color=COLORS["gray"],
|
|
240
|
+
linestyle=":",
|
|
241
|
+
linewidth=1.5,
|
|
242
|
+
label=f"±1std: {std_val:.3f}",
|
|
243
|
+
alpha=0.7,
|
|
244
|
+
)
|
|
245
|
+
ax.axvline(mean_val - std_val, color=COLORS["gray"], linestyle=":", linewidth=1.5, alpha=0.7)
|
|
246
|
+
|
|
247
|
+
# Styling
|
|
248
|
+
ax.set_xlabel("Amplitude (V)", fontsize=11, fontweight="bold")
|
|
249
|
+
ax.set_ylabel("Count", fontsize=11, fontweight="bold")
|
|
250
|
+
ax.set_title(title, fontsize=13, fontweight="bold", pad=15)
|
|
251
|
+
ax.grid(True, alpha=0.3, axis="y", linestyle="--")
|
|
252
|
+
ax.legend(loc="upper right", fontsize=9)
|
|
253
|
+
|
|
254
|
+
plt.tight_layout()
|
|
255
|
+
return fig_to_base64(fig)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def plot_spectrogram(
|
|
259
|
+
trace: WaveformTrace,
|
|
260
|
+
*,
|
|
261
|
+
title: str = "Spectrogram",
|
|
262
|
+
nfft: int = 1024,
|
|
263
|
+
noverlap: int | None = None,
|
|
264
|
+
) -> str:
|
|
265
|
+
"""Generate spectrogram (time-frequency) plot.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
trace: WaveformTrace to analyze.
|
|
269
|
+
title: Plot title.
|
|
270
|
+
nfft: FFT window size.
|
|
271
|
+
noverlap: Number of overlapping samples (default: nfft // 2).
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
Base64-encoded PNG image string.
|
|
275
|
+
|
|
276
|
+
Example:
|
|
277
|
+
>>> trace = osc.load("signal.wfm")
|
|
278
|
+
>>> specgram = plot_spectrogram(trace, nfft=512)
|
|
279
|
+
"""
|
|
280
|
+
if noverlap is None:
|
|
281
|
+
noverlap = nfft // 2
|
|
282
|
+
|
|
283
|
+
fig, ax = plt.subplots(figsize=(FIGURE_SIZE[0], FIGURE_SIZE[1] * 0.8))
|
|
284
|
+
|
|
285
|
+
# Generate spectrogram
|
|
286
|
+
sample_rate = trace.metadata.sample_rate
|
|
287
|
+
data = trace.data
|
|
288
|
+
|
|
289
|
+
# Use matplotlib's specgram
|
|
290
|
+
_spectrum, _freqs, _t, im = ax.specgram(
|
|
291
|
+
data, Fs=sample_rate, cmap="viridis", NFFT=nfft, noverlap=noverlap
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
# Add colorbar
|
|
295
|
+
cbar = plt.colorbar(im, ax=ax, label="Power (dB)")
|
|
296
|
+
cbar.set_label("Power (dB)", fontsize=10, fontweight="bold")
|
|
297
|
+
|
|
298
|
+
# Styling
|
|
299
|
+
ax.set_xlabel("Time (s)", fontsize=11, fontweight="bold")
|
|
300
|
+
ax.set_ylabel("Frequency (Hz)", fontsize=11, fontweight="bold")
|
|
301
|
+
ax.set_title(title, fontsize=13, fontweight="bold", pad=15)
|
|
302
|
+
|
|
303
|
+
plt.tight_layout()
|
|
304
|
+
return fig_to_base64(fig)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def plot_logic_analyzer(
|
|
308
|
+
trace: DigitalTrace,
|
|
309
|
+
*,
|
|
310
|
+
title: str = "Logic Analyzer View",
|
|
311
|
+
max_samples: int = 1000,
|
|
312
|
+
) -> str:
|
|
313
|
+
"""Generate logic analyzer view for digital signals.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
trace: DigitalTrace to plot.
|
|
317
|
+
title: Plot title.
|
|
318
|
+
max_samples: Maximum samples to display.
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
Base64-encoded PNG image string.
|
|
322
|
+
|
|
323
|
+
Example:
|
|
324
|
+
>>> digital_trace = osc.load("digital_signal.wfm")
|
|
325
|
+
>>> logic_plot = plot_logic_analyzer(digital_trace)
|
|
326
|
+
"""
|
|
327
|
+
fig, ax = plt.subplots(figsize=FIGURE_SIZE)
|
|
328
|
+
|
|
329
|
+
# Prepare data with downsampling if needed
|
|
330
|
+
data = trace.data.astype(float)
|
|
331
|
+
if len(data) > max_samples:
|
|
332
|
+
step = len(data) // max_samples
|
|
333
|
+
data = data[::step]
|
|
334
|
+
time = np.arange(len(data)) * step / trace.metadata.sample_rate
|
|
335
|
+
else:
|
|
336
|
+
time = np.arange(len(data)) / trace.metadata.sample_rate
|
|
337
|
+
|
|
338
|
+
# Plot as step function (digital signal)
|
|
339
|
+
ax.step(time * 1e6, data, where="post", color=COLORS["primary"], linewidth=1.5)
|
|
340
|
+
ax.fill_between(time * 1e6, 0, data, step="post", alpha=0.3, color=COLORS["primary"])
|
|
341
|
+
|
|
342
|
+
# Add grid lines at logic levels
|
|
343
|
+
ax.axhline(0, color="black", linestyle="-", linewidth=0.5, alpha=0.3)
|
|
344
|
+
ax.axhline(1, color="black", linestyle="-", linewidth=0.5, alpha=0.3)
|
|
345
|
+
|
|
346
|
+
# Styling
|
|
347
|
+
ax.set_xlabel("Time (μs)", fontsize=11, fontweight="bold")
|
|
348
|
+
ax.set_ylabel("Logic Level", fontsize=11, fontweight="bold")
|
|
349
|
+
ax.set_title(title, fontsize=13, fontweight="bold", pad=15)
|
|
350
|
+
ax.set_ylim(-0.2, 1.3)
|
|
351
|
+
ax.set_yticks([0, 1])
|
|
352
|
+
ax.set_yticklabels(["LOW", "HIGH"])
|
|
353
|
+
ax.grid(True, alpha=0.3, axis="x", linestyle="--")
|
|
354
|
+
|
|
355
|
+
plt.tight_layout()
|
|
356
|
+
return fig_to_base64(fig)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def plot_statistics_summary(
|
|
360
|
+
data: NDArray[Any],
|
|
361
|
+
*,
|
|
362
|
+
title: str = "Statistical Summary",
|
|
363
|
+
) -> str:
|
|
364
|
+
"""Generate statistical summary with box and violin plots.
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
data: Signal data array.
|
|
368
|
+
title: Plot title.
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
Base64-encoded PNG image string.
|
|
372
|
+
|
|
373
|
+
Example:
|
|
374
|
+
>>> import numpy as np
|
|
375
|
+
>>> data = np.random.randn(1000)
|
|
376
|
+
>>> stats_plot = plot_statistics_summary(data)
|
|
377
|
+
"""
|
|
378
|
+
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(FIGURE_SIZE[0], FIGURE_SIZE[1] * 0.7))
|
|
379
|
+
|
|
380
|
+
# Box plot
|
|
381
|
+
ax1.boxplot(
|
|
382
|
+
[data],
|
|
383
|
+
vert=True,
|
|
384
|
+
patch_artist=True,
|
|
385
|
+
widths=0.5,
|
|
386
|
+
boxprops={"facecolor": COLORS["primary"], "alpha": 0.7},
|
|
387
|
+
medianprops={"color": COLORS["danger"], "linewidth": 2},
|
|
388
|
+
whiskerprops={"color": "black", "linewidth": 1.5},
|
|
389
|
+
capprops={"color": "black", "linewidth": 1.5},
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
ax1.set_ylabel("Amplitude (V)", fontsize=11, fontweight="bold")
|
|
393
|
+
ax1.set_title("Box Plot", fontsize=12, fontweight="bold")
|
|
394
|
+
ax1.grid(True, alpha=0.3, axis="y", linestyle="--")
|
|
395
|
+
ax1.set_xticks([])
|
|
396
|
+
|
|
397
|
+
# Violin plot
|
|
398
|
+
parts = ax2.violinplot([data], vert=True, widths=0.7, showmeans=True, showextrema=True)
|
|
399
|
+
for pc in parts["bodies"]:
|
|
400
|
+
pc.set_facecolor(COLORS["success"])
|
|
401
|
+
pc.set_alpha(0.7)
|
|
402
|
+
|
|
403
|
+
ax2.set_ylabel("Amplitude (V)", fontsize=11, fontweight="bold")
|
|
404
|
+
ax2.set_title("Violin Plot", fontsize=12, fontweight="bold")
|
|
405
|
+
ax2.grid(True, alpha=0.3, axis="y", linestyle="--")
|
|
406
|
+
ax2.set_xticks([])
|
|
407
|
+
|
|
408
|
+
fig.suptitle(title, fontsize=13, fontweight="bold", y=1.02)
|
|
409
|
+
plt.tight_layout()
|
|
410
|
+
return fig_to_base64(fig)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def generate_all_plots(
|
|
414
|
+
trace: WaveformTrace | DigitalTrace,
|
|
415
|
+
*,
|
|
416
|
+
output_format: str = "base64",
|
|
417
|
+
verbose: bool = True,
|
|
418
|
+
) -> dict[str, str]:
|
|
419
|
+
"""Generate all applicable plots for a signal trace.
|
|
420
|
+
|
|
421
|
+
Automatically detects signal type and generates appropriate plots:
|
|
422
|
+
- Analog signals: waveform, FFT spectrum, histogram, spectrogram, statistics
|
|
423
|
+
- Digital signals: waveform, logic analyzer view
|
|
424
|
+
|
|
425
|
+
Args:
|
|
426
|
+
trace: WaveformTrace or DigitalTrace to visualize.
|
|
427
|
+
output_format: Output format ("base64" only currently supported).
|
|
428
|
+
verbose: Print progress messages.
|
|
429
|
+
|
|
430
|
+
Returns:
|
|
431
|
+
Dictionary mapping plot names to base64 image strings.
|
|
432
|
+
|
|
433
|
+
Raises:
|
|
434
|
+
ValueError: If output_format is not "base64".
|
|
435
|
+
|
|
436
|
+
Example:
|
|
437
|
+
>>> trace = osc.load("signal.wfm")
|
|
438
|
+
>>> plots = generate_all_plots(trace)
|
|
439
|
+
>>> len(plots) # 5 plots for analog signal
|
|
440
|
+
5
|
|
441
|
+
"""
|
|
442
|
+
if output_format != "base64":
|
|
443
|
+
raise ValueError(f"Only 'base64' output format supported, got '{output_format}'")
|
|
444
|
+
|
|
445
|
+
plots = {}
|
|
446
|
+
is_digital = (
|
|
447
|
+
trace.is_digital if hasattr(trace, "is_digital") else isinstance(trace, DigitalTrace)
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
# Always generate waveform plot
|
|
451
|
+
try:
|
|
452
|
+
plots["waveform"] = plot_waveform(trace)
|
|
453
|
+
if verbose:
|
|
454
|
+
print(" ✓ Generated waveform plot")
|
|
455
|
+
except Exception as e:
|
|
456
|
+
if verbose:
|
|
457
|
+
print(f" ⚠ Waveform plot failed: {e}")
|
|
458
|
+
|
|
459
|
+
if not is_digital:
|
|
460
|
+
# Analog signal plots
|
|
461
|
+
if isinstance(trace, WaveformTrace): # Type narrowing
|
|
462
|
+
try:
|
|
463
|
+
plots["fft"] = plot_fft_spectrum(trace)
|
|
464
|
+
if verbose:
|
|
465
|
+
print(" ✓ Generated FFT spectrum")
|
|
466
|
+
except Exception as e:
|
|
467
|
+
if verbose:
|
|
468
|
+
print(f" ⚠ FFT plot failed: {e}")
|
|
469
|
+
|
|
470
|
+
try:
|
|
471
|
+
plots["histogram"] = plot_histogram(trace.data)
|
|
472
|
+
if verbose:
|
|
473
|
+
print(" ✓ Generated histogram")
|
|
474
|
+
except Exception as e:
|
|
475
|
+
if verbose:
|
|
476
|
+
print(f" ⚠ Histogram plot failed: {e}")
|
|
477
|
+
|
|
478
|
+
try:
|
|
479
|
+
plots["spectrogram"] = plot_spectrogram(trace)
|
|
480
|
+
if verbose:
|
|
481
|
+
print(" ✓ Generated spectrogram")
|
|
482
|
+
except Exception as e:
|
|
483
|
+
if verbose:
|
|
484
|
+
print(f" ⚠ Spectrogram plot failed: {e}")
|
|
485
|
+
|
|
486
|
+
try:
|
|
487
|
+
plots["statistics"] = plot_statistics_summary(trace.data)
|
|
488
|
+
if verbose:
|
|
489
|
+
print(" ✓ Generated statistics summary")
|
|
490
|
+
except Exception as e:
|
|
491
|
+
if verbose:
|
|
492
|
+
print(f" ⚠ Statistics plot failed: {e}")
|
|
493
|
+
else:
|
|
494
|
+
# Digital signal plots
|
|
495
|
+
from oscura.core.types import DigitalTrace as DigitalTraceType
|
|
496
|
+
|
|
497
|
+
if isinstance(trace, DigitalTraceType): # Type narrowing
|
|
498
|
+
try:
|
|
499
|
+
plots["logic"] = plot_logic_analyzer(trace)
|
|
500
|
+
if verbose:
|
|
501
|
+
print(" ✓ Generated logic analyzer view")
|
|
502
|
+
except Exception as e:
|
|
503
|
+
if verbose:
|
|
504
|
+
print(f" ⚠ Logic analyzer plot failed: {e}")
|
|
505
|
+
|
|
506
|
+
return plots
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
__all__ = [
|
|
510
|
+
"COLORS",
|
|
511
|
+
"FIGURE_SIZE",
|
|
512
|
+
"PLOT_DPI",
|
|
513
|
+
"fig_to_base64",
|
|
514
|
+
"generate_all_plots",
|
|
515
|
+
"plot_fft_spectrum",
|
|
516
|
+
"plot_histogram",
|
|
517
|
+
"plot_logic_analyzer",
|
|
518
|
+
"plot_spectrogram",
|
|
519
|
+
"plot_statistics_summary",
|
|
520
|
+
"plot_waveform",
|
|
521
|
+
]
|
oscura/workflows/__init__.py
CHANGED
|
@@ -16,6 +16,7 @@ Example:
|
|
|
16
16
|
>>> stats = osc.workflows.load_all(["trace1.wfm", "trace2.wfm"])
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
|
+
from oscura.workflows import waveform
|
|
19
20
|
from oscura.workflows.complete_re import CompleteREResult, full_protocol_re
|
|
20
21
|
from oscura.workflows.compliance import emc_compliance_test
|
|
21
22
|
from oscura.workflows.digital import characterize_buffer
|
|
@@ -58,4 +59,5 @@ __all__ = [
|
|
|
58
59
|
"power_analysis",
|
|
59
60
|
"reverse_engineer_signal",
|
|
60
61
|
"signal_integrity_audit",
|
|
62
|
+
"waveform",
|
|
61
63
|
]
|