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.
- oscura/__init__.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 +28 -0
- oscura/analyzers/patterns/reverse_engineering.py +991 -0
- oscura/analyzers/power/__init__.py +35 -12
- oscura/analyzers/statistics/__init__.py +4 -0
- oscura/analyzers/statistics/basic.py +149 -0
- oscura/analyzers/statistics/correlation.py +47 -6
- oscura/analyzers/waveform/__init__.py +2 -0
- oscura/analyzers/waveform/measurements.py +145 -23
- oscura/analyzers/waveform/spectral.py +361 -8
- oscura/automotive/__init__.py +1 -1
- oscura/automotive/dtc/data.json +102 -17
- oscura/core/config/loader.py +0 -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 +108 -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 +279 -0
- oscura/reporting/html.py +57 -0
- oscura/reporting/interpretation.py +431 -0
- oscura/reporting/summary.py +329 -0
- oscura/reporting/visualization.py +542 -0
- oscura/visualization/__init__.py +2 -1
- oscura/visualization/batch.py +521 -0
- oscura/workflows/__init__.py +2 -0
- oscura/workflows/waveform.py +783 -0
- {oscura-0.7.0.dist-info → oscura-0.8.0.dist-info}/METADATA +1 -1
- {oscura-0.7.0.dist-info → oscura-0.8.0.dist-info}/RECORD +40 -29
- {oscura-0.7.0.dist-info → oscura-0.8.0.dist-info}/WHEEL +0 -0
- {oscura-0.7.0.dist-info → oscura-0.8.0.dist-info}/entry_points.txt +0 -0
- {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}"
|
oscura/visualization/__init__.py
CHANGED
|
@@ -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",
|