oscura 0.7.0__py3-none-any.whl → 0.8.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) 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/automotive/dtc/data.json +102 -17
  17. oscura/core/config/loader.py +0 -1
  18. oscura/core/schemas/device_mapping.json +8 -2
  19. oscura/core/schemas/packet_format.json +24 -4
  20. oscura/core/schemas/protocol_definition.json +12 -2
  21. oscura/core/types.py +108 -0
  22. oscura/reporting/__init__.py +88 -1
  23. oscura/reporting/automation.py +348 -0
  24. oscura/reporting/citations.py +374 -0
  25. oscura/reporting/core.py +54 -0
  26. oscura/reporting/formatting/__init__.py +11 -0
  27. oscura/reporting/formatting/measurements.py +279 -0
  28. oscura/reporting/html.py +57 -0
  29. oscura/reporting/interpretation.py +431 -0
  30. oscura/reporting/summary.py +329 -0
  31. oscura/reporting/visualization.py +542 -0
  32. oscura/visualization/__init__.py +2 -1
  33. oscura/visualization/batch.py +521 -0
  34. oscura/workflows/__init__.py +2 -0
  35. oscura/workflows/waveform.py +783 -0
  36. {oscura-0.7.0.dist-info → oscura-0.8.0.dist-info}/METADATA +1 -1
  37. {oscura-0.7.0.dist-info → oscura-0.8.0.dist-info}/RECORD +40 -29
  38. {oscura-0.7.0.dist-info → oscura-0.8.0.dist-info}/WHEEL +0 -0
  39. {oscura-0.7.0.dist-info → oscura-0.8.0.dist-info}/entry_points.txt +0 -0
  40. {oscura-0.7.0.dist-info → oscura-0.8.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}"
@@ -25,7 +25,7 @@ Example:
25
25
  # The module itself can be imported without matplotlib.
26
26
 
27
27
  # Import plot module as namespace for DSL compatibility
28
- from oscura.visualization import plot
28
+ from oscura.visualization import batch, plot
29
29
  from oscura.visualization.accessibility import (
30
30
  FAIL_SYMBOL,
31
31
  LINE_STYLES,
@@ -227,6 +227,7 @@ __all__ = [
227
227
  "apply_rendering_config",
228
228
  # Styles
229
229
  "apply_style_preset",
230
+ "batch",
230
231
  "calculate_axis_limits",
231
232
  "calculate_bin_edges",
232
233
  "calculate_grid_spacing",