oscura 0.6.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.
Files changed (38) hide show
  1. oscura/__init__.py +1 -1
  2. oscura/analyzers/eye/__init__.py +5 -1
  3. oscura/analyzers/eye/generation.py +501 -0
  4. oscura/analyzers/jitter/__init__.py +6 -6
  5. oscura/analyzers/jitter/timing.py +419 -0
  6. oscura/analyzers/patterns/__init__.py +28 -0
  7. oscura/analyzers/patterns/reverse_engineering.py +991 -0
  8. oscura/analyzers/power/__init__.py +35 -12
  9. oscura/analyzers/statistics/__init__.py +4 -0
  10. oscura/analyzers/statistics/basic.py +149 -0
  11. oscura/analyzers/statistics/correlation.py +47 -6
  12. oscura/analyzers/waveform/__init__.py +2 -0
  13. oscura/analyzers/waveform/measurements.py +145 -23
  14. oscura/analyzers/waveform/spectral.py +361 -8
  15. oscura/automotive/__init__.py +1 -1
  16. oscura/core/config/loader.py +0 -1
  17. oscura/core/types.py +108 -0
  18. oscura/loaders/__init__.py +12 -4
  19. oscura/loaders/tss.py +456 -0
  20. oscura/reporting/__init__.py +88 -1
  21. oscura/reporting/automation.py +348 -0
  22. oscura/reporting/citations.py +374 -0
  23. oscura/reporting/core.py +54 -0
  24. oscura/reporting/formatting/__init__.py +11 -0
  25. oscura/reporting/formatting/measurements.py +279 -0
  26. oscura/reporting/html.py +57 -0
  27. oscura/reporting/interpretation.py +431 -0
  28. oscura/reporting/summary.py +329 -0
  29. oscura/reporting/visualization.py +542 -0
  30. oscura/visualization/__init__.py +2 -1
  31. oscura/visualization/batch.py +521 -0
  32. oscura/workflows/__init__.py +2 -0
  33. oscura/workflows/waveform.py +783 -0
  34. {oscura-0.6.0.dist-info → oscura-0.8.0.dist-info}/METADATA +37 -19
  35. {oscura-0.6.0.dist-info → oscura-0.8.0.dist-info}/RECORD +38 -26
  36. {oscura-0.6.0.dist-info → oscura-0.8.0.dist-info}/WHEEL +0 -0
  37. {oscura-0.6.0.dist-info → oscura-0.8.0.dist-info}/entry_points.txt +0 -0
  38. {oscura-0.6.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
+ ]
@@ -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
  ]