oscura 0.8.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/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 +164 -73
- 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/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/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/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/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 +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 +11 -6
- {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/METADATA +59 -79
- {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/RECORD +111 -136
- 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 → oscura-0.10.0.dist-info}/WHEEL +0 -0
- {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/licenses/LICENSE +0 -0
oscura/visualization/batch.py
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
"""Batch plot generation for comprehensive signal visualization.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Generate multiple related plots from signal traces in a single operation,
|
|
4
|
+
useful for comprehensive analysis reports.
|
|
5
5
|
|
|
6
6
|
Example:
|
|
7
7
|
>>> from oscura.visualization import batch
|
|
8
8
|
>>> trace = osc.load("signal.wfm")
|
|
9
|
-
>>> plots = batch.generate_all_plots(trace
|
|
9
|
+
>>> plots = batch.generate_all_plots(trace)
|
|
10
10
|
>>> # Returns: {"waveform": <base64>, "fft": <base64>, ...}
|
|
11
11
|
"""
|
|
12
12
|
|
|
@@ -14,35 +14,22 @@ from __future__ import annotations
|
|
|
14
14
|
|
|
15
15
|
import base64
|
|
16
16
|
from io import BytesIO
|
|
17
|
-
from typing import TYPE_CHECKING
|
|
17
|
+
from typing import TYPE_CHECKING
|
|
18
18
|
|
|
19
19
|
import matplotlib
|
|
20
20
|
|
|
21
|
-
matplotlib.use("Agg") #
|
|
21
|
+
matplotlib.use("Agg") # Non-interactive backend
|
|
22
22
|
import matplotlib.pyplot as plt
|
|
23
|
-
import numpy as np
|
|
24
23
|
|
|
25
|
-
# Import trace types for runtime isinstance checks
|
|
26
24
|
from oscura.core.types import DigitalTrace, WaveformTrace
|
|
27
25
|
|
|
28
26
|
if TYPE_CHECKING:
|
|
29
27
|
from matplotlib.figure import Figure
|
|
30
|
-
from numpy.typing import NDArray
|
|
31
28
|
|
|
32
|
-
# Plot configuration
|
|
29
|
+
# Plot configuration
|
|
33
30
|
PLOT_DPI = 150
|
|
34
31
|
FIGURE_SIZE = (10, 6)
|
|
35
32
|
|
|
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
33
|
|
|
47
34
|
def fig_to_base64(fig: Figure, *, dpi: int = PLOT_DPI) -> str:
|
|
48
35
|
"""Convert matplotlib figure to base64-encoded PNG.
|
|
@@ -55,467 +42,119 @@ def fig_to_base64(fig: Figure, *, dpi: int = PLOT_DPI) -> str:
|
|
|
55
42
|
Base64-encoded PNG image string with data URI prefix.
|
|
56
43
|
|
|
57
44
|
Example:
|
|
58
|
-
>>> import matplotlib.pyplot as plt
|
|
59
45
|
>>> fig, ax = plt.subplots()
|
|
60
46
|
>>> ax.plot([1, 2, 3])
|
|
61
|
-
>>>
|
|
62
|
-
>>> assert b64_str.startswith("data:image/png;base64,")
|
|
47
|
+
>>> b64 = fig_to_base64(fig)
|
|
63
48
|
"""
|
|
64
49
|
buf = BytesIO()
|
|
65
|
-
fig.savefig(
|
|
66
|
-
buf, format="png", dpi=dpi, bbox_inches="tight", facecolor="white", edgecolor="none"
|
|
67
|
-
)
|
|
50
|
+
fig.savefig(buf, format="png", dpi=dpi, bbox_inches="tight", facecolor="white")
|
|
68
51
|
buf.seek(0)
|
|
69
52
|
img_base64 = base64.b64encode(buf.read()).decode("utf-8")
|
|
70
53
|
plt.close(fig)
|
|
71
54
|
return f"data:image/png;base64,{img_base64}"
|
|
72
55
|
|
|
73
56
|
|
|
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
57
|
def generate_all_plots(
|
|
414
58
|
trace: WaveformTrace | DigitalTrace,
|
|
415
59
|
*,
|
|
416
|
-
output_format: str = "base64",
|
|
417
60
|
verbose: bool = True,
|
|
418
61
|
) -> dict[str, str]:
|
|
419
62
|
"""Generate all applicable plots for a signal trace.
|
|
420
63
|
|
|
421
64
|
Automatically detects signal type and generates appropriate plots:
|
|
422
|
-
- Analog
|
|
423
|
-
- Digital
|
|
65
|
+
- Analog: waveform, FFT, histogram, spectrogram
|
|
66
|
+
- Digital: waveform, timing diagram
|
|
424
67
|
|
|
425
68
|
Args:
|
|
426
69
|
trace: WaveformTrace or DigitalTrace to visualize.
|
|
427
|
-
output_format: Output format ("base64" only currently supported).
|
|
428
70
|
verbose: Print progress messages.
|
|
429
71
|
|
|
430
72
|
Returns:
|
|
431
73
|
Dictionary mapping plot names to base64 image strings.
|
|
432
74
|
|
|
433
|
-
Raises:
|
|
434
|
-
ValueError: If output_format is not "base64".
|
|
435
|
-
|
|
436
75
|
Example:
|
|
437
76
|
>>> trace = osc.load("signal.wfm")
|
|
438
77
|
>>> plots = generate_all_plots(trace)
|
|
439
|
-
>>> len(plots) # 5 plots for analog signal
|
|
440
|
-
|
|
78
|
+
>>> len(plots) # 4-5 plots for analog signal
|
|
79
|
+
4
|
|
441
80
|
"""
|
|
442
|
-
|
|
443
|
-
|
|
81
|
+
from oscura.visualization.plot import (
|
|
82
|
+
plot_fft,
|
|
83
|
+
plot_histogram,
|
|
84
|
+
plot_spectrogram,
|
|
85
|
+
plot_timing,
|
|
86
|
+
plot_waveform,
|
|
87
|
+
)
|
|
444
88
|
|
|
445
89
|
plots = {}
|
|
446
|
-
is_digital = (
|
|
447
|
-
trace.is_digital if hasattr(trace, "is_digital") else isinstance(trace, DigitalTrace)
|
|
448
|
-
)
|
|
449
90
|
|
|
450
|
-
# Always generate waveform
|
|
91
|
+
# Always generate waveform (handle both analog and digital)
|
|
451
92
|
try:
|
|
452
|
-
|
|
93
|
+
fig, ax = plt.subplots(figsize=FIGURE_SIZE)
|
|
94
|
+
if isinstance(trace, WaveformTrace):
|
|
95
|
+
plot_waveform(ax, trace)
|
|
96
|
+
else:
|
|
97
|
+
# Digital trace
|
|
98
|
+
plot_timing(ax, trace)
|
|
99
|
+
plots["waveform"] = fig_to_base64(fig)
|
|
453
100
|
if verbose:
|
|
454
101
|
print(" ✓ Generated waveform plot")
|
|
455
102
|
except Exception as e:
|
|
456
103
|
if verbose:
|
|
457
104
|
print(f" ⚠ Waveform plot failed: {e}")
|
|
458
105
|
|
|
459
|
-
if
|
|
460
|
-
#
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
#
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
print(f" ⚠ Logic analyzer plot failed: {e}")
|
|
106
|
+
if isinstance(trace, WaveformTrace):
|
|
107
|
+
# FFT spectrum
|
|
108
|
+
try:
|
|
109
|
+
fig, ax = plt.subplots(figsize=FIGURE_SIZE)
|
|
110
|
+
plot_fft(ax, trace)
|
|
111
|
+
plots["fft"] = fig_to_base64(fig)
|
|
112
|
+
if verbose:
|
|
113
|
+
print(" ✓ Generated FFT spectrum")
|
|
114
|
+
except Exception as e:
|
|
115
|
+
if verbose:
|
|
116
|
+
print(f" ⚠ FFT plot failed: {e}")
|
|
117
|
+
|
|
118
|
+
# Histogram
|
|
119
|
+
try:
|
|
120
|
+
fig, ax = plt.subplots(figsize=FIGURE_SIZE)
|
|
121
|
+
plot_histogram(ax, trace.data)
|
|
122
|
+
plots["histogram"] = fig_to_base64(fig)
|
|
123
|
+
if verbose:
|
|
124
|
+
print(" ✓ Generated histogram")
|
|
125
|
+
except Exception as e:
|
|
126
|
+
if verbose:
|
|
127
|
+
print(f" ⚠ Histogram plot failed: {e}")
|
|
128
|
+
|
|
129
|
+
# Spectrogram
|
|
130
|
+
try:
|
|
131
|
+
fig, ax = plt.subplots(figsize=FIGURE_SIZE)
|
|
132
|
+
plot_spectrogram(ax, trace)
|
|
133
|
+
plots["spectrogram"] = fig_to_base64(fig)
|
|
134
|
+
if verbose:
|
|
135
|
+
print(" ✓ Generated spectrogram")
|
|
136
|
+
except Exception as e:
|
|
137
|
+
if verbose:
|
|
138
|
+
print(f" ⚠ Spectrogram plot failed: {e}")
|
|
139
|
+
|
|
140
|
+
elif isinstance(trace, DigitalTrace):
|
|
141
|
+
# Timing diagram
|
|
142
|
+
try:
|
|
143
|
+
fig, ax = plt.subplots(figsize=FIGURE_SIZE)
|
|
144
|
+
plot_timing(ax, trace)
|
|
145
|
+
plots["timing"] = fig_to_base64(fig)
|
|
146
|
+
if verbose:
|
|
147
|
+
print(" ✓ Generated timing diagram")
|
|
148
|
+
except Exception as e:
|
|
149
|
+
if verbose:
|
|
150
|
+
print(f" ⚠ Timing plot failed: {e}")
|
|
505
151
|
|
|
506
152
|
return plots
|
|
507
153
|
|
|
508
154
|
|
|
509
155
|
__all__ = [
|
|
510
|
-
"COLORS",
|
|
511
156
|
"FIGURE_SIZE",
|
|
512
157
|
"PLOT_DPI",
|
|
513
158
|
"fig_to_base64",
|
|
514
159
|
"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
160
|
]
|