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.
Files changed (175) hide show
  1. oscura/__init__.py +19 -19
  2. oscura/analyzers/__init__.py +2 -0
  3. oscura/analyzers/digital/extraction.py +2 -3
  4. oscura/analyzers/digital/quality.py +1 -1
  5. oscura/analyzers/digital/timing.py +1 -1
  6. oscura/analyzers/eye/__init__.py +5 -1
  7. oscura/analyzers/eye/generation.py +501 -0
  8. oscura/analyzers/jitter/__init__.py +6 -6
  9. oscura/analyzers/jitter/timing.py +419 -0
  10. oscura/analyzers/patterns/__init__.py +94 -0
  11. oscura/analyzers/patterns/reverse_engineering.py +991 -0
  12. oscura/analyzers/power/__init__.py +35 -12
  13. oscura/analyzers/power/basic.py +3 -3
  14. oscura/analyzers/power/soa.py +1 -1
  15. oscura/analyzers/power/switching.py +3 -3
  16. oscura/analyzers/signal_classification.py +529 -0
  17. oscura/analyzers/signal_integrity/sparams.py +3 -3
  18. oscura/analyzers/statistics/__init__.py +4 -0
  19. oscura/analyzers/statistics/basic.py +152 -0
  20. oscura/analyzers/statistics/correlation.py +47 -6
  21. oscura/analyzers/validation.py +1 -1
  22. oscura/analyzers/waveform/__init__.py +2 -0
  23. oscura/analyzers/waveform/measurements.py +329 -163
  24. oscura/analyzers/waveform/measurements_with_uncertainty.py +91 -35
  25. oscura/analyzers/waveform/spectral.py +498 -54
  26. oscura/api/dsl/commands.py +15 -6
  27. oscura/api/server/templates/base.html +137 -146
  28. oscura/api/server/templates/export.html +84 -110
  29. oscura/api/server/templates/home.html +248 -267
  30. oscura/api/server/templates/protocols.html +44 -48
  31. oscura/api/server/templates/reports.html +27 -35
  32. oscura/api/server/templates/session_detail.html +68 -78
  33. oscura/api/server/templates/sessions.html +62 -72
  34. oscura/api/server/templates/waveforms.html +54 -64
  35. oscura/automotive/__init__.py +1 -1
  36. oscura/automotive/can/session.py +1 -1
  37. oscura/automotive/dbc/generator.py +638 -23
  38. oscura/automotive/dtc/data.json +102 -17
  39. oscura/automotive/uds/decoder.py +99 -6
  40. oscura/cli/analyze.py +8 -2
  41. oscura/cli/batch.py +36 -5
  42. oscura/cli/characterize.py +18 -4
  43. oscura/cli/export.py +47 -5
  44. oscura/cli/main.py +2 -0
  45. oscura/cli/onboarding/wizard.py +10 -6
  46. oscura/cli/pipeline.py +585 -0
  47. oscura/cli/visualize.py +6 -4
  48. oscura/convenience.py +400 -32
  49. oscura/core/config/loader.py +0 -1
  50. oscura/core/measurement_result.py +286 -0
  51. oscura/core/progress.py +1 -1
  52. oscura/core/schemas/device_mapping.json +8 -2
  53. oscura/core/schemas/packet_format.json +24 -4
  54. oscura/core/schemas/protocol_definition.json +12 -2
  55. oscura/core/types.py +300 -199
  56. oscura/correlation/multi_protocol.py +1 -1
  57. oscura/export/legacy/__init__.py +11 -0
  58. oscura/export/legacy/wav.py +75 -0
  59. oscura/exporters/__init__.py +19 -0
  60. oscura/exporters/wireshark.py +809 -0
  61. oscura/hardware/acquisition/file.py +5 -19
  62. oscura/hardware/acquisition/saleae.py +10 -10
  63. oscura/hardware/acquisition/socketcan.py +4 -6
  64. oscura/hardware/acquisition/synthetic.py +1 -5
  65. oscura/hardware/acquisition/visa.py +6 -6
  66. oscura/hardware/security/side_channel_detector.py +5 -508
  67. oscura/inference/message_format.py +686 -1
  68. oscura/jupyter/display.py +2 -2
  69. oscura/jupyter/magic.py +3 -3
  70. oscura/loaders/__init__.py +17 -12
  71. oscura/loaders/binary.py +1 -1
  72. oscura/loaders/chipwhisperer.py +1 -2
  73. oscura/loaders/configurable.py +1 -1
  74. oscura/loaders/csv_loader.py +2 -2
  75. oscura/loaders/hdf5_loader.py +1 -1
  76. oscura/loaders/lazy.py +6 -1
  77. oscura/loaders/mmap_loader.py +0 -1
  78. oscura/loaders/numpy_loader.py +8 -7
  79. oscura/loaders/preprocessing.py +3 -5
  80. oscura/loaders/rigol.py +21 -7
  81. oscura/loaders/sigrok.py +2 -5
  82. oscura/loaders/tdms.py +3 -2
  83. oscura/loaders/tektronix.py +38 -32
  84. oscura/loaders/tss.py +20 -27
  85. oscura/loaders/vcd.py +13 -8
  86. oscura/loaders/wav.py +1 -6
  87. oscura/pipeline/__init__.py +76 -0
  88. oscura/pipeline/handlers/__init__.py +165 -0
  89. oscura/pipeline/handlers/analyzers.py +1045 -0
  90. oscura/pipeline/handlers/decoders.py +899 -0
  91. oscura/pipeline/handlers/exporters.py +1103 -0
  92. oscura/pipeline/handlers/filters.py +891 -0
  93. oscura/pipeline/handlers/loaders.py +640 -0
  94. oscura/pipeline/handlers/transforms.py +768 -0
  95. oscura/reporting/__init__.py +88 -1
  96. oscura/reporting/automation.py +348 -0
  97. oscura/reporting/citations.py +374 -0
  98. oscura/reporting/core.py +54 -0
  99. oscura/reporting/formatting/__init__.py +11 -0
  100. oscura/reporting/formatting/measurements.py +320 -0
  101. oscura/reporting/html.py +57 -0
  102. oscura/reporting/interpretation.py +431 -0
  103. oscura/reporting/summary.py +329 -0
  104. oscura/reporting/templates/enhanced/protocol_re.html +504 -503
  105. oscura/reporting/visualization.py +542 -0
  106. oscura/side_channel/__init__.py +38 -57
  107. oscura/utils/builders/signal_builder.py +5 -5
  108. oscura/utils/comparison/compare.py +7 -9
  109. oscura/utils/comparison/golden.py +1 -1
  110. oscura/utils/filtering/convenience.py +2 -2
  111. oscura/utils/math/arithmetic.py +38 -62
  112. oscura/utils/math/interpolation.py +20 -20
  113. oscura/utils/pipeline/__init__.py +4 -17
  114. oscura/utils/progressive.py +1 -4
  115. oscura/utils/triggering/edge.py +1 -1
  116. oscura/utils/triggering/pattern.py +2 -2
  117. oscura/utils/triggering/pulse.py +2 -2
  118. oscura/utils/triggering/window.py +3 -3
  119. oscura/validation/hil_testing.py +11 -11
  120. oscura/visualization/__init__.py +47 -284
  121. oscura/visualization/batch.py +160 -0
  122. oscura/visualization/plot.py +542 -53
  123. oscura/visualization/styles.py +184 -318
  124. oscura/workflows/__init__.py +2 -0
  125. oscura/workflows/batch/advanced.py +1 -1
  126. oscura/workflows/batch/aggregate.py +7 -8
  127. oscura/workflows/complete_re.py +251 -23
  128. oscura/workflows/digital.py +27 -4
  129. oscura/workflows/multi_trace.py +136 -17
  130. oscura/workflows/waveform.py +788 -0
  131. {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/METADATA +59 -79
  132. {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/RECORD +135 -149
  133. oscura/side_channel/dpa.py +0 -1025
  134. oscura/utils/optimization/__init__.py +0 -19
  135. oscura/utils/optimization/parallel.py +0 -443
  136. oscura/utils/optimization/search.py +0 -532
  137. oscura/utils/pipeline/base.py +0 -338
  138. oscura/utils/pipeline/composition.py +0 -248
  139. oscura/utils/pipeline/parallel.py +0 -449
  140. oscura/utils/pipeline/pipeline.py +0 -375
  141. oscura/utils/search/__init__.py +0 -16
  142. oscura/utils/search/anomaly.py +0 -424
  143. oscura/utils/search/context.py +0 -294
  144. oscura/utils/search/pattern.py +0 -288
  145. oscura/utils/storage/__init__.py +0 -61
  146. oscura/utils/storage/database.py +0 -1166
  147. oscura/visualization/accessibility.py +0 -526
  148. oscura/visualization/annotations.py +0 -371
  149. oscura/visualization/axis_scaling.py +0 -305
  150. oscura/visualization/colors.py +0 -451
  151. oscura/visualization/digital.py +0 -436
  152. oscura/visualization/eye.py +0 -571
  153. oscura/visualization/histogram.py +0 -281
  154. oscura/visualization/interactive.py +0 -1035
  155. oscura/visualization/jitter.py +0 -1042
  156. oscura/visualization/keyboard.py +0 -394
  157. oscura/visualization/layout.py +0 -400
  158. oscura/visualization/optimization.py +0 -1079
  159. oscura/visualization/palettes.py +0 -446
  160. oscura/visualization/power.py +0 -508
  161. oscura/visualization/power_extended.py +0 -955
  162. oscura/visualization/presets.py +0 -469
  163. oscura/visualization/protocols.py +0 -1246
  164. oscura/visualization/render.py +0 -223
  165. oscura/visualization/rendering.py +0 -444
  166. oscura/visualization/reverse_engineering.py +0 -838
  167. oscura/visualization/signal_integrity.py +0 -989
  168. oscura/visualization/specialized.py +0 -643
  169. oscura/visualization/spectral.py +0 -1226
  170. oscura/visualization/thumbnails.py +0 -340
  171. oscura/visualization/time_axis.py +0 -351
  172. oscura/visualization/waveform.py +0 -454
  173. {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/WHEEL +0 -0
  174. {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/entry_points.txt +0 -0
  175. {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}"
@@ -1,63 +1,44 @@
1
- """Side-channel analysis for cryptographic implementation attacks.
2
-
3
- .. deprecated:: 0.6.0
4
- This module is deprecated. Use :mod:`oscura.analyzers.side_channel` instead.
5
- This module will be removed in v1.0.0.
6
-
7
- This package provides tools for performing side-channel attacks on cryptographic
8
- implementations using power analysis, electromagnetic analysis, timing analysis,
9
- and fault injection techniques.
10
-
11
- Migration Guide:
12
- Old import (deprecated):
13
- >>> from oscura.side_channel.dpa import DPAAnalyzer, PowerTrace
14
-
15
- New import (recommended):
16
- >>> from oscura.analyzers.side_channel import DPAAnalyzer, CPAAnalyzer
17
- >>> from oscura.analyzers.side_channel.power import hamming_weight
18
-
19
- Note:
20
- The oscura.side_channel.dpa module contains a different DPAAnalyzer implementation
21
- than oscura.analyzers.side_channel.power. The older implementation in this module
22
- (oscura.side_channel.dpa.DPAAnalyzer) supports combined DPA/CPA/Template attacks
23
- with a single class. The newer implementation in oscura.analyzers.side_channel
24
- provides separate DPAAnalyzer and CPAAnalyzer classes with cleaner APIs.
25
-
26
- For new code, use the oscura.analyzers.side_channel module.
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
- import warnings
49
-
50
- # Issue deprecation warning on import
51
- warnings.warn(
52
- "oscura.side_channel is deprecated and will be removed in v1.0.0. "
53
- "Use oscura.analyzers.side_channel instead. "
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
- # Re-export from the local dpa module for backward compatibility
60
- # The dpa.py module in this directory contains the legacy implementation
61
- from oscura.side_channel.dpa import DPAAnalyzer, DPAResult, PowerTrace
62
-
63
- __all__ = ["DPAAnalyzer", "DPAResult", "PowerTrace"]
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
- channel_name=channel,
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 channel_name, data in self._channels.items():
980
+ for channel, data in self._channels.items():
981
981
  trace_metadata = TraceMetadata(
982
982
  sample_rate=self._sample_rate,
983
- channel_name=channel_name,
983
+ channel=channel,
984
984
  )
985
- traces[channel_name] = WaveformTrace(data=data, metadata=trace_metadata)
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
- channel_name=trace.metadata.channel_name or "ch1",
1013
+ channel=trace.metadata.channel or "ch1",
1014
1014
  )
1015
1015
 
1016
1016
  return trace