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
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
"""Professional plot generation for reports with IEEE compliance.
|
|
2
|
+
|
|
3
|
+
This module extends PlotGenerator with comprehensive plot types for
|
|
4
|
+
signal analysis including waveforms, FFT, PSD, spectrograms, eye diagrams,
|
|
5
|
+
histograms, jitter analysis, and power plots.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import base64
|
|
11
|
+
from io import BytesIO
|
|
12
|
+
from typing import TYPE_CHECKING, Any
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from matplotlib.figure import Figure
|
|
18
|
+
from numpy.typing import NDArray
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
import matplotlib
|
|
22
|
+
import matplotlib.pyplot as plt
|
|
23
|
+
from matplotlib.gridspec import GridSpec
|
|
24
|
+
|
|
25
|
+
matplotlib.use("Agg")
|
|
26
|
+
HAS_MATPLOTLIB = True
|
|
27
|
+
except ImportError:
|
|
28
|
+
HAS_MATPLOTLIB = False
|
|
29
|
+
|
|
30
|
+
# IEEE compliant color scheme
|
|
31
|
+
IEEE_COLORS = {
|
|
32
|
+
"primary": "#003f87", # IEEE blue
|
|
33
|
+
"secondary": "#00629b",
|
|
34
|
+
"accent": "#009fdf",
|
|
35
|
+
"success": "#27ae60",
|
|
36
|
+
"warning": "#f39c12",
|
|
37
|
+
"danger": "#e74c3c",
|
|
38
|
+
"grid": "#cccccc",
|
|
39
|
+
"text": "#2c3e50",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class PlotStyler:
|
|
44
|
+
"""Apply consistent IEEE-compliant styling to plots.
|
|
45
|
+
|
|
46
|
+
Example:
|
|
47
|
+
>>> styler = PlotStyler()
|
|
48
|
+
>>> fig, ax = plt.subplots()
|
|
49
|
+
>>> styler.apply_ieee_style(ax, "Time (s)", "Voltage (V)", "Waveform")
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
@staticmethod
|
|
53
|
+
def apply_ieee_style(
|
|
54
|
+
ax: Any,
|
|
55
|
+
xlabel: str = "",
|
|
56
|
+
ylabel: str = "",
|
|
57
|
+
title: str = "",
|
|
58
|
+
grid: bool = True,
|
|
59
|
+
) -> None:
|
|
60
|
+
"""Apply IEEE-compliant styling to matplotlib axes.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
ax: Matplotlib axes object.
|
|
64
|
+
xlabel: X-axis label.
|
|
65
|
+
ylabel: Y-axis label.
|
|
66
|
+
title: Plot title.
|
|
67
|
+
grid: Whether to show grid.
|
|
68
|
+
|
|
69
|
+
Example:
|
|
70
|
+
>>> fig, ax = plt.subplots()
|
|
71
|
+
>>> PlotStyler.apply_ieee_style(ax, "Time", "Voltage", "Signal")
|
|
72
|
+
"""
|
|
73
|
+
if not HAS_MATPLOTLIB:
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
# Labels and title
|
|
77
|
+
if xlabel:
|
|
78
|
+
ax.set_xlabel(xlabel, fontsize=10, fontweight="normal")
|
|
79
|
+
if ylabel:
|
|
80
|
+
ax.set_ylabel(ylabel, fontsize=10, fontweight="normal")
|
|
81
|
+
if title:
|
|
82
|
+
ax.set_title(title, fontsize=12, fontweight="bold", pad=15)
|
|
83
|
+
|
|
84
|
+
# Grid
|
|
85
|
+
if grid:
|
|
86
|
+
ax.grid(True, alpha=0.3, linestyle="--", linewidth=0.5, color=IEEE_COLORS["grid"])
|
|
87
|
+
|
|
88
|
+
# Spines
|
|
89
|
+
for spine in ax.spines.values():
|
|
90
|
+
spine.set_color(IEEE_COLORS["text"])
|
|
91
|
+
spine.set_linewidth(0.8)
|
|
92
|
+
|
|
93
|
+
# Ticks
|
|
94
|
+
ax.tick_params(labelsize=9, colors=IEEE_COLORS["text"])
|
|
95
|
+
|
|
96
|
+
# Tight layout
|
|
97
|
+
ax.figure.tight_layout()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class IEEEPlotGenerator:
|
|
101
|
+
"""Generate IEEE-compliant plots for signal analysis reports.
|
|
102
|
+
|
|
103
|
+
Provides comprehensive plot types with consistent styling and
|
|
104
|
+
proper axis labels, units, and annotations.
|
|
105
|
+
|
|
106
|
+
Example:
|
|
107
|
+
>>> generator = IEEEPlotGenerator()
|
|
108
|
+
>>> fig = generator.plot_waveform(time, voltage, title="Input Signal")
|
|
109
|
+
>>> base64_img = generator.figure_to_base64(fig)
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
def __init__(self, dpi: int = 150, figsize: tuple[int, int] = (10, 6)) -> None:
|
|
113
|
+
"""Initialize plot generator.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
dpi: Resolution in dots per inch.
|
|
117
|
+
figsize: Figure size in inches (width, height).
|
|
118
|
+
|
|
119
|
+
Raises:
|
|
120
|
+
ImportError: If matplotlib is not installed.
|
|
121
|
+
"""
|
|
122
|
+
if not HAS_MATPLOTLIB:
|
|
123
|
+
raise ImportError("matplotlib is required for plot generation")
|
|
124
|
+
|
|
125
|
+
self.dpi = dpi
|
|
126
|
+
self.figsize = figsize
|
|
127
|
+
self.styler = PlotStyler()
|
|
128
|
+
|
|
129
|
+
def plot_waveform(
|
|
130
|
+
self,
|
|
131
|
+
time: NDArray[np.floating[Any]],
|
|
132
|
+
signal: NDArray[np.floating[Any]],
|
|
133
|
+
title: str = "Waveform",
|
|
134
|
+
xlabel: str = "Time (s)",
|
|
135
|
+
ylabel: str = "Amplitude",
|
|
136
|
+
markers: dict[str, float] | None = None,
|
|
137
|
+
) -> Figure:
|
|
138
|
+
"""Plot time-series waveform.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
time: Time array in seconds.
|
|
142
|
+
signal: Signal amplitude array.
|
|
143
|
+
title: Plot title.
|
|
144
|
+
xlabel: X-axis label.
|
|
145
|
+
ylabel: Y-axis label.
|
|
146
|
+
markers: Optional dict of marker labels to time positions.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Matplotlib Figure object.
|
|
150
|
+
|
|
151
|
+
Example:
|
|
152
|
+
>>> t = np.linspace(0, 1, 1000)
|
|
153
|
+
>>> s = np.sin(2 * np.pi * 10 * t)
|
|
154
|
+
>>> fig = generator.plot_waveform(t, s, "Sine Wave")
|
|
155
|
+
"""
|
|
156
|
+
fig, ax = plt.subplots(figsize=self.figsize, dpi=self.dpi)
|
|
157
|
+
|
|
158
|
+
# Plot signal
|
|
159
|
+
ax.plot(time, signal, color=IEEE_COLORS["primary"], linewidth=1.5, label="Signal")
|
|
160
|
+
|
|
161
|
+
# Add markers if provided
|
|
162
|
+
if markers:
|
|
163
|
+
for label, pos in markers.items():
|
|
164
|
+
ax.axvline(pos, color=IEEE_COLORS["accent"], linestyle="--", alpha=0.7)
|
|
165
|
+
ax.text(
|
|
166
|
+
pos,
|
|
167
|
+
ax.get_ylim()[1] * 0.9,
|
|
168
|
+
label,
|
|
169
|
+
rotation=90,
|
|
170
|
+
verticalalignment="top",
|
|
171
|
+
fontsize=8,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
self.styler.apply_ieee_style(ax, xlabel, ylabel, title)
|
|
175
|
+
return fig
|
|
176
|
+
|
|
177
|
+
def plot_fft(
|
|
178
|
+
self,
|
|
179
|
+
frequencies: NDArray[np.floating[Any]],
|
|
180
|
+
magnitude_db: NDArray[np.floating[Any]],
|
|
181
|
+
title: str = "FFT Magnitude Spectrum",
|
|
182
|
+
peak_markers: int = 5,
|
|
183
|
+
) -> Figure:
|
|
184
|
+
"""Plot FFT magnitude spectrum.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
frequencies: Frequency array in Hz.
|
|
188
|
+
magnitude_db: Magnitude in dB.
|
|
189
|
+
title: Plot title.
|
|
190
|
+
peak_markers: Number of peak frequencies to mark (0 to disable).
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
Matplotlib Figure object.
|
|
194
|
+
|
|
195
|
+
Example:
|
|
196
|
+
>>> freq = np.fft.rfftfreq(1000, 1/1000)
|
|
197
|
+
>>> mag_db = 20 * np.log10(np.abs(np.fft.rfft(signal)))
|
|
198
|
+
>>> fig = generator.plot_fft(freq, mag_db)
|
|
199
|
+
"""
|
|
200
|
+
fig, ax = plt.subplots(figsize=self.figsize, dpi=self.dpi)
|
|
201
|
+
|
|
202
|
+
# Plot spectrum
|
|
203
|
+
ax.plot(frequencies, magnitude_db, color=IEEE_COLORS["primary"], linewidth=1.5)
|
|
204
|
+
|
|
205
|
+
# Mark peaks
|
|
206
|
+
if peak_markers > 0:
|
|
207
|
+
# Find peaks (ignore DC)
|
|
208
|
+
valid_idx = frequencies > 0
|
|
209
|
+
valid_freq = frequencies[valid_idx]
|
|
210
|
+
valid_mag = magnitude_db[valid_idx]
|
|
211
|
+
|
|
212
|
+
if len(valid_mag) > 0:
|
|
213
|
+
peak_indices = np.argsort(valid_mag)[-peak_markers:]
|
|
214
|
+
for idx in peak_indices:
|
|
215
|
+
freq_val = valid_freq[idx]
|
|
216
|
+
mag_val = valid_mag[idx]
|
|
217
|
+
ax.plot(freq_val, mag_val, "ro", markersize=6, alpha=0.7)
|
|
218
|
+
ax.annotate(
|
|
219
|
+
f"{freq_val:.1f} Hz",
|
|
220
|
+
(freq_val, mag_val),
|
|
221
|
+
xytext=(5, 5),
|
|
222
|
+
textcoords="offset points",
|
|
223
|
+
fontsize=8,
|
|
224
|
+
color=IEEE_COLORS["danger"],
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
self.styler.apply_ieee_style(ax, "Frequency (Hz)", "Magnitude (dB)", title)
|
|
228
|
+
|
|
229
|
+
# Log scale for frequency if range > 2 decades
|
|
230
|
+
if len(frequencies) > 1 and frequencies[-1] / frequencies[1] > 100:
|
|
231
|
+
ax.set_xscale("log")
|
|
232
|
+
|
|
233
|
+
return fig
|
|
234
|
+
|
|
235
|
+
def plot_psd(
|
|
236
|
+
self,
|
|
237
|
+
frequencies: NDArray[np.floating[Any]],
|
|
238
|
+
psd: NDArray[np.floating[Any]],
|
|
239
|
+
title: str = "Power Spectral Density",
|
|
240
|
+
units: str = "V²/Hz",
|
|
241
|
+
) -> Figure:
|
|
242
|
+
"""Plot Power Spectral Density.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
frequencies: Frequency array in Hz.
|
|
246
|
+
psd: Power spectral density array.
|
|
247
|
+
title: Plot title.
|
|
248
|
+
units: PSD units.
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
Matplotlib Figure object.
|
|
252
|
+
|
|
253
|
+
Example:
|
|
254
|
+
>>> from scipy import signal as sp_signal
|
|
255
|
+
>>> freq, psd = sp_signal.welch(data, fs=sample_rate)
|
|
256
|
+
>>> fig = generator.plot_psd(freq, psd)
|
|
257
|
+
"""
|
|
258
|
+
fig, ax = plt.subplots(figsize=self.figsize, dpi=self.dpi)
|
|
259
|
+
|
|
260
|
+
# Convert to dB scale
|
|
261
|
+
psd_db = 10 * np.log10(psd + 1e-12) # Add epsilon to avoid log(0)
|
|
262
|
+
|
|
263
|
+
ax.plot(frequencies, psd_db, color=IEEE_COLORS["primary"], linewidth=1.5)
|
|
264
|
+
self.styler.apply_ieee_style(ax, "Frequency (Hz)", f"PSD (dB {units})", title)
|
|
265
|
+
|
|
266
|
+
# Log scale for frequency
|
|
267
|
+
if len(frequencies) > 1 and frequencies[-1] / frequencies[1] > 100:
|
|
268
|
+
ax.set_xscale("log")
|
|
269
|
+
|
|
270
|
+
return fig
|
|
271
|
+
|
|
272
|
+
def plot_spectrogram(
|
|
273
|
+
self,
|
|
274
|
+
time: NDArray[np.floating[Any]],
|
|
275
|
+
frequencies: NDArray[np.floating[Any]],
|
|
276
|
+
spectrogram: NDArray[np.floating[Any]],
|
|
277
|
+
title: str = "Spectrogram",
|
|
278
|
+
) -> Figure:
|
|
279
|
+
"""Plot time-frequency spectrogram.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
time: Time array in seconds.
|
|
283
|
+
frequencies: Frequency array in Hz.
|
|
284
|
+
spectrogram: 2D spectrogram array (freq x time).
|
|
285
|
+
title: Plot title.
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
Matplotlib Figure object.
|
|
289
|
+
|
|
290
|
+
Example:
|
|
291
|
+
>>> from scipy import signal as sp_signal
|
|
292
|
+
>>> f, t, Sxx = sp_signal.spectrogram(data, fs=sample_rate)
|
|
293
|
+
>>> fig = generator.plot_spectrogram(t, f, Sxx)
|
|
294
|
+
"""
|
|
295
|
+
fig, ax = plt.subplots(figsize=self.figsize, dpi=self.dpi)
|
|
296
|
+
|
|
297
|
+
# Convert to dB scale
|
|
298
|
+
spec_db = 10 * np.log10(spectrogram + 1e-12)
|
|
299
|
+
|
|
300
|
+
# Plot spectrogram
|
|
301
|
+
im = ax.pcolormesh(
|
|
302
|
+
time, frequencies, spec_db, shading="auto", cmap="viridis", rasterized=True
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
# Colorbar
|
|
306
|
+
cbar = fig.colorbar(im, ax=ax, label="Power (dB)")
|
|
307
|
+
cbar.ax.tick_params(labelsize=9)
|
|
308
|
+
|
|
309
|
+
self.styler.apply_ieee_style(ax, "Time (s)", "Frequency (Hz)", title, grid=False)
|
|
310
|
+
|
|
311
|
+
return fig
|
|
312
|
+
|
|
313
|
+
def plot_eye_diagram(
|
|
314
|
+
self,
|
|
315
|
+
signal: NDArray[np.floating[Any]],
|
|
316
|
+
samples_per_symbol: int,
|
|
317
|
+
title: str = "Eye Diagram",
|
|
318
|
+
num_traces: int = 100,
|
|
319
|
+
) -> Figure:
|
|
320
|
+
"""Plot eye diagram for digital signals.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
signal: Signal array.
|
|
324
|
+
samples_per_symbol: Samples per symbol period.
|
|
325
|
+
title: Plot title.
|
|
326
|
+
num_traces: Number of eye traces to plot.
|
|
327
|
+
|
|
328
|
+
Returns:
|
|
329
|
+
Matplotlib Figure object.
|
|
330
|
+
|
|
331
|
+
Example:
|
|
332
|
+
>>> # For 1000 samples at 10 samples/symbol
|
|
333
|
+
>>> fig = generator.plot_eye_diagram(signal, 10, num_traces=50)
|
|
334
|
+
"""
|
|
335
|
+
fig, ax = plt.subplots(figsize=self.figsize, dpi=self.dpi)
|
|
336
|
+
|
|
337
|
+
# Extract symbol windows
|
|
338
|
+
num_symbols = len(signal) // samples_per_symbol
|
|
339
|
+
num_traces = min(num_traces, num_symbols - 1)
|
|
340
|
+
|
|
341
|
+
for i in range(num_traces):
|
|
342
|
+
start = i * samples_per_symbol
|
|
343
|
+
end = start + 2 * samples_per_symbol # Two symbol periods
|
|
344
|
+
if end <= len(signal):
|
|
345
|
+
trace = signal[start:end]
|
|
346
|
+
ax.plot(
|
|
347
|
+
np.arange(len(trace)),
|
|
348
|
+
trace,
|
|
349
|
+
color=IEEE_COLORS["primary"],
|
|
350
|
+
alpha=0.3,
|
|
351
|
+
linewidth=0.5,
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
self.styler.apply_ieee_style(ax, "Sample", "Amplitude", title)
|
|
355
|
+
return fig
|
|
356
|
+
|
|
357
|
+
def plot_histogram(
|
|
358
|
+
self,
|
|
359
|
+
data: NDArray[np.floating[Any]],
|
|
360
|
+
bins: int = 50,
|
|
361
|
+
title: str = "Sample Distribution",
|
|
362
|
+
xlabel: str = "Value",
|
|
363
|
+
) -> Figure:
|
|
364
|
+
"""Plot histogram of sample distribution.
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
data: Data array.
|
|
368
|
+
bins: Number of histogram bins.
|
|
369
|
+
title: Plot title.
|
|
370
|
+
xlabel: X-axis label.
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
Matplotlib Figure object.
|
|
374
|
+
|
|
375
|
+
Example:
|
|
376
|
+
>>> fig = generator.plot_histogram(signal, bins=100, title="Voltage Distribution")
|
|
377
|
+
"""
|
|
378
|
+
fig, ax = plt.subplots(figsize=self.figsize, dpi=self.dpi)
|
|
379
|
+
|
|
380
|
+
# Plot histogram
|
|
381
|
+
n, bins_edges, _ = ax.hist(
|
|
382
|
+
data, bins=bins, color=IEEE_COLORS["primary"], alpha=0.7, edgecolor="black"
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
# Fit Gaussian
|
|
386
|
+
mu = np.mean(data)
|
|
387
|
+
sigma = np.std(data)
|
|
388
|
+
x = np.linspace(bins_edges[0], bins_edges[-1], 200)
|
|
389
|
+
gaussian = (
|
|
390
|
+
len(data)
|
|
391
|
+
* (bins_edges[1] - bins_edges[0])
|
|
392
|
+
/ (sigma * np.sqrt(2 * np.pi))
|
|
393
|
+
* np.exp(-0.5 * ((x - mu) / sigma) ** 2)
|
|
394
|
+
)
|
|
395
|
+
ax.plot(x, gaussian, color=IEEE_COLORS["danger"], linewidth=2, label="Gaussian fit")
|
|
396
|
+
|
|
397
|
+
# Add statistics text
|
|
398
|
+
stats_text = f"mean = {mu:.4f}\nstd = {sigma:.4f}"
|
|
399
|
+
ax.text(
|
|
400
|
+
0.98,
|
|
401
|
+
0.98,
|
|
402
|
+
stats_text,
|
|
403
|
+
transform=ax.transAxes,
|
|
404
|
+
fontsize=9,
|
|
405
|
+
verticalalignment="top",
|
|
406
|
+
horizontalalignment="right",
|
|
407
|
+
bbox={"boxstyle": "round", "facecolor": "white", "alpha": 0.8},
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
ax.legend(fontsize=9)
|
|
411
|
+
self.styler.apply_ieee_style(ax, xlabel, "Count", title)
|
|
412
|
+
return fig
|
|
413
|
+
|
|
414
|
+
def plot_jitter(
|
|
415
|
+
self,
|
|
416
|
+
time_intervals: NDArray[np.floating[Any]],
|
|
417
|
+
title: str = "Jitter Analysis",
|
|
418
|
+
) -> Figure:
|
|
419
|
+
"""Plot jitter analysis with histogram and time series.
|
|
420
|
+
|
|
421
|
+
Args:
|
|
422
|
+
time_intervals: Array of time interval measurements.
|
|
423
|
+
title: Plot title.
|
|
424
|
+
|
|
425
|
+
Returns:
|
|
426
|
+
Matplotlib Figure object with two subplots.
|
|
427
|
+
|
|
428
|
+
Example:
|
|
429
|
+
>>> # time_intervals in seconds
|
|
430
|
+
>>> fig = generator.plot_jitter(time_intervals)
|
|
431
|
+
"""
|
|
432
|
+
fig = plt.figure(figsize=self.figsize, dpi=self.dpi)
|
|
433
|
+
gs = GridSpec(2, 1, height_ratios=[2, 1], hspace=0.3)
|
|
434
|
+
|
|
435
|
+
# Time series plot
|
|
436
|
+
ax1 = fig.add_subplot(gs[0])
|
|
437
|
+
ax1.plot(
|
|
438
|
+
np.arange(len(time_intervals)),
|
|
439
|
+
time_intervals * 1e9, # Convert to nanoseconds
|
|
440
|
+
color=IEEE_COLORS["primary"],
|
|
441
|
+
linewidth=1,
|
|
442
|
+
marker="o",
|
|
443
|
+
markersize=2,
|
|
444
|
+
alpha=0.6,
|
|
445
|
+
)
|
|
446
|
+
self.styler.apply_ieee_style(ax1, "Interval #", "Jitter (ns)", f"{title} - Time Series")
|
|
447
|
+
|
|
448
|
+
# Histogram
|
|
449
|
+
ax2 = fig.add_subplot(gs[1])
|
|
450
|
+
jitter_ns = time_intervals * 1e9
|
|
451
|
+
ax2.hist(jitter_ns, bins=50, color=IEEE_COLORS["secondary"], alpha=0.7, edgecolor="black")
|
|
452
|
+
|
|
453
|
+
# Statistics
|
|
454
|
+
mean_jitter = np.mean(jitter_ns)
|
|
455
|
+
std_jitter = np.std(jitter_ns)
|
|
456
|
+
pk_pk_jitter = np.max(jitter_ns) - np.min(jitter_ns)
|
|
457
|
+
|
|
458
|
+
stats_text = (
|
|
459
|
+
f"Mean: {mean_jitter:.3f} ns\nStd: {std_jitter:.3f} ns\nPk-Pk: {pk_pk_jitter:.3f} ns"
|
|
460
|
+
)
|
|
461
|
+
ax2.text(
|
|
462
|
+
0.98,
|
|
463
|
+
0.98,
|
|
464
|
+
stats_text,
|
|
465
|
+
transform=ax2.transAxes,
|
|
466
|
+
fontsize=8,
|
|
467
|
+
verticalalignment="top",
|
|
468
|
+
horizontalalignment="right",
|
|
469
|
+
bbox={"boxstyle": "round", "facecolor": "white", "alpha": 0.8},
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
self.styler.apply_ieee_style(ax2, "Jitter (ns)", "Count", "Distribution")
|
|
473
|
+
|
|
474
|
+
return fig
|
|
475
|
+
|
|
476
|
+
def plot_power(
|
|
477
|
+
self,
|
|
478
|
+
time: NDArray[np.floating[Any]],
|
|
479
|
+
voltage: NDArray[np.floating[Any]],
|
|
480
|
+
current: NDArray[np.floating[Any]],
|
|
481
|
+
title: str = "Power Waveform",
|
|
482
|
+
) -> Figure:
|
|
483
|
+
"""Plot power waveforms (voltage, current, power).
|
|
484
|
+
|
|
485
|
+
Args:
|
|
486
|
+
time: Time array in seconds.
|
|
487
|
+
voltage: Voltage array.
|
|
488
|
+
current: Current array.
|
|
489
|
+
title: Plot title.
|
|
490
|
+
|
|
491
|
+
Returns:
|
|
492
|
+
Matplotlib Figure object with three subplots.
|
|
493
|
+
|
|
494
|
+
Example:
|
|
495
|
+
>>> fig = generator.plot_power(time, voltage, current)
|
|
496
|
+
"""
|
|
497
|
+
fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=self.figsize, dpi=self.dpi, sharex=True)
|
|
498
|
+
|
|
499
|
+
# Voltage
|
|
500
|
+
ax1.plot(time, voltage, color=IEEE_COLORS["primary"], linewidth=1.5)
|
|
501
|
+
self.styler.apply_ieee_style(ax1, "", "Voltage (V)", "Voltage", grid=True)
|
|
502
|
+
|
|
503
|
+
# Current
|
|
504
|
+
ax2.plot(time, current, color=IEEE_COLORS["secondary"], linewidth=1.5)
|
|
505
|
+
self.styler.apply_ieee_style(ax2, "", "Current (A)", "Current", grid=True)
|
|
506
|
+
|
|
507
|
+
# Power
|
|
508
|
+
power = voltage * current
|
|
509
|
+
ax3.plot(time, power, color=IEEE_COLORS["accent"], linewidth=1.5)
|
|
510
|
+
ax3.axhline(0, color="black", linewidth=0.5, linestyle="--", alpha=0.5)
|
|
511
|
+
self.styler.apply_ieee_style(ax3, "Time (s)", "Power (W)", "Instantaneous Power", grid=True)
|
|
512
|
+
|
|
513
|
+
fig.suptitle(title, fontsize=12, fontweight="bold", y=0.995)
|
|
514
|
+
fig.tight_layout()
|
|
515
|
+
|
|
516
|
+
return fig
|
|
517
|
+
|
|
518
|
+
@staticmethod
|
|
519
|
+
def figure_to_base64(fig: Figure, format: str = "png") -> str:
|
|
520
|
+
"""Convert matplotlib figure to base64-encoded string for HTML embedding.
|
|
521
|
+
|
|
522
|
+
Args:
|
|
523
|
+
fig: Matplotlib Figure object.
|
|
524
|
+
format: Image format (png, jpg, svg).
|
|
525
|
+
|
|
526
|
+
Returns:
|
|
527
|
+
Base64-encoded image string with data URI prefix.
|
|
528
|
+
|
|
529
|
+
Example:
|
|
530
|
+
>>> fig = plt.figure()
|
|
531
|
+
>>> img_str = IEEEPlotGenerator.figure_to_base64(fig)
|
|
532
|
+
>>> "data:image/png;base64," in img_str
|
|
533
|
+
True
|
|
534
|
+
"""
|
|
535
|
+
buffer = BytesIO()
|
|
536
|
+
fig.savefig(buffer, format=format, bbox_inches="tight", dpi=150)
|
|
537
|
+
buffer.seek(0)
|
|
538
|
+
img_base64 = base64.b64encode(buffer.read()).decode("utf-8")
|
|
539
|
+
buffer.close()
|
|
540
|
+
plt.close(fig)
|
|
541
|
+
|
|
542
|
+
return f"data:image/{format};base64,{img_base64}"
|
oscura/side_channel/__init__.py
CHANGED
|
@@ -1,63 +1,44 @@
|
|
|
1
|
-
"""Side-channel
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
(
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
For existing code using DPAAnalyzer with attack_type parameter, continue using
|
|
28
|
-
oscura.side_channel.dpa until migration to the new API.
|
|
29
|
-
|
|
30
|
-
Example (new API - recommended):
|
|
31
|
-
>>> from oscura.analyzers.side_channel import DPAAnalyzer, CPAAnalyzer
|
|
32
|
-
>>> # DPA attack
|
|
33
|
-
>>> dpa = DPAAnalyzer(target_bit=0)
|
|
34
|
-
>>> result = dpa.analyze(traces, plaintexts)
|
|
35
|
-
>>> # CPA attack
|
|
36
|
-
>>> cpa = CPAAnalyzer(leakage_model="hamming_weight")
|
|
37
|
-
>>> result = cpa.analyze(traces, plaintexts)
|
|
38
|
-
|
|
39
|
-
Example (old API - deprecated):
|
|
40
|
-
>>> from oscura.side_channel.dpa import DPAAnalyzer, PowerTrace
|
|
41
|
-
>>> analyzer = DPAAnalyzer(attack_type="cpa", leakage_model="hamming_weight")
|
|
42
|
-
>>> traces = [PowerTrace(timestamp=t, power=p, plaintext=pt) for ...]
|
|
43
|
-
>>> result = analyzer.perform_attack(traces, target_byte=0)
|
|
1
|
+
"""Side-channel trace loading.
|
|
2
|
+
|
|
3
|
+
This module ONLY provides ChipWhisperer trace loading for integration
|
|
4
|
+
into Oscura workflows.
|
|
5
|
+
|
|
6
|
+
For actual side-channel attacks, use ChipWhisperer directly:
|
|
7
|
+
https://chipwhisperer.com/
|
|
8
|
+
|
|
9
|
+
What's here:
|
|
10
|
+
- ChipWhisperer .npy/.trs trace loading via oscura.loaders.chipwhisperer
|
|
11
|
+
|
|
12
|
+
What's NOT here (use ChipWhisperer instead):
|
|
13
|
+
- DPA/CPA attacks
|
|
14
|
+
- Key recovery
|
|
15
|
+
- Leakage assessment
|
|
16
|
+
- Template attacks
|
|
17
|
+
- Hardware interfacing
|
|
18
|
+
|
|
19
|
+
Example:
|
|
20
|
+
>>> from oscura.loaders.chipwhisperer import load_chipwhisperer
|
|
21
|
+
>>> traceset = load_chipwhisperer("capture_data.npy")
|
|
22
|
+
>>> print(f"Loaded {traceset.n_traces} traces")
|
|
23
|
+
|
|
24
|
+
References:
|
|
25
|
+
ChipWhisperer Project: https://chipwhisperer.com/
|
|
26
|
+
ChipWhisperer Documentation: https://chipwhisperer.readthedocs.io/
|
|
44
27
|
"""
|
|
45
28
|
|
|
46
29
|
from __future__ import annotations
|
|
47
30
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
"See migration guide in module docstring.",
|
|
55
|
-
DeprecationWarning,
|
|
56
|
-
stacklevel=2,
|
|
31
|
+
# Re-export ChipWhisperer loader for convenience
|
|
32
|
+
from oscura.loaders.chipwhisperer import (
|
|
33
|
+
ChipWhispererTraceSet,
|
|
34
|
+
load_chipwhisperer,
|
|
35
|
+
load_chipwhisperer_npy,
|
|
36
|
+
load_chipwhisperer_trs,
|
|
57
37
|
)
|
|
58
38
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
39
|
+
__all__ = [
|
|
40
|
+
"ChipWhispererTraceSet",
|
|
41
|
+
"load_chipwhisperer",
|
|
42
|
+
"load_chipwhisperer_npy",
|
|
43
|
+
"load_chipwhisperer_trs",
|
|
44
|
+
]
|
|
@@ -940,7 +940,7 @@ class SignalBuilder:
|
|
|
940
940
|
# Build TraceMetadata
|
|
941
941
|
trace_metadata = TraceMetadata(
|
|
942
942
|
sample_rate=self._sample_rate,
|
|
943
|
-
|
|
943
|
+
channel=channel,
|
|
944
944
|
)
|
|
945
945
|
|
|
946
946
|
return WaveformTrace(data=data, metadata=trace_metadata)
|
|
@@ -977,12 +977,12 @@ class SignalBuilder:
|
|
|
977
977
|
|
|
978
978
|
# Build WaveformTrace for each channel
|
|
979
979
|
traces: dict[str, WaveformTrace] = {}
|
|
980
|
-
for
|
|
980
|
+
for channel, data in self._channels.items():
|
|
981
981
|
trace_metadata = TraceMetadata(
|
|
982
982
|
sample_rate=self._sample_rate,
|
|
983
|
-
|
|
983
|
+
channel=channel,
|
|
984
984
|
)
|
|
985
|
-
traces[
|
|
985
|
+
traces[channel] = WaveformTrace(data=data, metadata=trace_metadata)
|
|
986
986
|
|
|
987
987
|
return traces
|
|
988
988
|
|
|
@@ -1010,7 +1010,7 @@ class SignalBuilder:
|
|
|
1010
1010
|
path,
|
|
1011
1011
|
data=trace.data,
|
|
1012
1012
|
sample_rate=trace.metadata.sample_rate,
|
|
1013
|
-
|
|
1013
|
+
channel=trace.metadata.channel or "ch1",
|
|
1014
1014
|
)
|
|
1015
1015
|
|
|
1016
1016
|
return trace
|