oscura 0.8.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 (151) 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/patterns/__init__.py +66 -0
  7. oscura/analyzers/power/basic.py +3 -3
  8. oscura/analyzers/power/soa.py +1 -1
  9. oscura/analyzers/power/switching.py +3 -3
  10. oscura/analyzers/signal_classification.py +529 -0
  11. oscura/analyzers/signal_integrity/sparams.py +3 -3
  12. oscura/analyzers/statistics/basic.py +10 -7
  13. oscura/analyzers/validation.py +1 -1
  14. oscura/analyzers/waveform/measurements.py +200 -156
  15. oscura/analyzers/waveform/measurements_with_uncertainty.py +91 -35
  16. oscura/analyzers/waveform/spectral.py +164 -73
  17. oscura/api/dsl/commands.py +15 -6
  18. oscura/api/server/templates/base.html +137 -146
  19. oscura/api/server/templates/export.html +84 -110
  20. oscura/api/server/templates/home.html +248 -267
  21. oscura/api/server/templates/protocols.html +44 -48
  22. oscura/api/server/templates/reports.html +27 -35
  23. oscura/api/server/templates/session_detail.html +68 -78
  24. oscura/api/server/templates/sessions.html +62 -72
  25. oscura/api/server/templates/waveforms.html +54 -64
  26. oscura/automotive/__init__.py +1 -1
  27. oscura/automotive/can/session.py +1 -1
  28. oscura/automotive/dbc/generator.py +638 -23
  29. oscura/automotive/uds/decoder.py +99 -6
  30. oscura/cli/analyze.py +8 -2
  31. oscura/cli/batch.py +36 -5
  32. oscura/cli/characterize.py +18 -4
  33. oscura/cli/export.py +47 -5
  34. oscura/cli/main.py +2 -0
  35. oscura/cli/onboarding/wizard.py +10 -6
  36. oscura/cli/pipeline.py +585 -0
  37. oscura/cli/visualize.py +6 -4
  38. oscura/convenience.py +400 -32
  39. oscura/core/measurement_result.py +286 -0
  40. oscura/core/progress.py +1 -1
  41. oscura/core/types.py +232 -239
  42. oscura/correlation/multi_protocol.py +1 -1
  43. oscura/export/legacy/__init__.py +11 -0
  44. oscura/export/legacy/wav.py +75 -0
  45. oscura/exporters/__init__.py +19 -0
  46. oscura/exporters/wireshark.py +809 -0
  47. oscura/hardware/acquisition/file.py +5 -19
  48. oscura/hardware/acquisition/saleae.py +10 -10
  49. oscura/hardware/acquisition/socketcan.py +4 -6
  50. oscura/hardware/acquisition/synthetic.py +1 -5
  51. oscura/hardware/acquisition/visa.py +6 -6
  52. oscura/hardware/security/side_channel_detector.py +5 -508
  53. oscura/inference/message_format.py +686 -1
  54. oscura/jupyter/display.py +2 -2
  55. oscura/jupyter/magic.py +3 -3
  56. oscura/loaders/__init__.py +17 -12
  57. oscura/loaders/binary.py +1 -1
  58. oscura/loaders/chipwhisperer.py +1 -2
  59. oscura/loaders/configurable.py +1 -1
  60. oscura/loaders/csv_loader.py +2 -2
  61. oscura/loaders/hdf5_loader.py +1 -1
  62. oscura/loaders/lazy.py +6 -1
  63. oscura/loaders/mmap_loader.py +0 -1
  64. oscura/loaders/numpy_loader.py +8 -7
  65. oscura/loaders/preprocessing.py +3 -5
  66. oscura/loaders/rigol.py +21 -7
  67. oscura/loaders/sigrok.py +2 -5
  68. oscura/loaders/tdms.py +3 -2
  69. oscura/loaders/tektronix.py +38 -32
  70. oscura/loaders/tss.py +20 -27
  71. oscura/loaders/vcd.py +13 -8
  72. oscura/loaders/wav.py +1 -6
  73. oscura/pipeline/__init__.py +76 -0
  74. oscura/pipeline/handlers/__init__.py +165 -0
  75. oscura/pipeline/handlers/analyzers.py +1045 -0
  76. oscura/pipeline/handlers/decoders.py +899 -0
  77. oscura/pipeline/handlers/exporters.py +1103 -0
  78. oscura/pipeline/handlers/filters.py +891 -0
  79. oscura/pipeline/handlers/loaders.py +640 -0
  80. oscura/pipeline/handlers/transforms.py +768 -0
  81. oscura/reporting/formatting/measurements.py +55 -14
  82. oscura/reporting/templates/enhanced/protocol_re.html +504 -503
  83. oscura/side_channel/__init__.py +38 -57
  84. oscura/utils/builders/signal_builder.py +5 -5
  85. oscura/utils/comparison/compare.py +7 -9
  86. oscura/utils/comparison/golden.py +1 -1
  87. oscura/utils/filtering/convenience.py +2 -2
  88. oscura/utils/math/arithmetic.py +38 -62
  89. oscura/utils/math/interpolation.py +20 -20
  90. oscura/utils/pipeline/__init__.py +4 -17
  91. oscura/utils/progressive.py +1 -4
  92. oscura/utils/triggering/edge.py +1 -1
  93. oscura/utils/triggering/pattern.py +2 -2
  94. oscura/utils/triggering/pulse.py +2 -2
  95. oscura/utils/triggering/window.py +3 -3
  96. oscura/validation/hil_testing.py +11 -11
  97. oscura/visualization/__init__.py +46 -284
  98. oscura/visualization/batch.py +72 -433
  99. oscura/visualization/plot.py +542 -53
  100. oscura/visualization/styles.py +184 -318
  101. oscura/workflows/batch/advanced.py +1 -1
  102. oscura/workflows/batch/aggregate.py +7 -8
  103. oscura/workflows/complete_re.py +251 -23
  104. oscura/workflows/digital.py +27 -4
  105. oscura/workflows/multi_trace.py +136 -17
  106. oscura/workflows/waveform.py +11 -6
  107. {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/METADATA +59 -79
  108. {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/RECORD +111 -136
  109. oscura/side_channel/dpa.py +0 -1025
  110. oscura/utils/optimization/__init__.py +0 -19
  111. oscura/utils/optimization/parallel.py +0 -443
  112. oscura/utils/optimization/search.py +0 -532
  113. oscura/utils/pipeline/base.py +0 -338
  114. oscura/utils/pipeline/composition.py +0 -248
  115. oscura/utils/pipeline/parallel.py +0 -449
  116. oscura/utils/pipeline/pipeline.py +0 -375
  117. oscura/utils/search/__init__.py +0 -16
  118. oscura/utils/search/anomaly.py +0 -424
  119. oscura/utils/search/context.py +0 -294
  120. oscura/utils/search/pattern.py +0 -288
  121. oscura/utils/storage/__init__.py +0 -61
  122. oscura/utils/storage/database.py +0 -1166
  123. oscura/visualization/accessibility.py +0 -526
  124. oscura/visualization/annotations.py +0 -371
  125. oscura/visualization/axis_scaling.py +0 -305
  126. oscura/visualization/colors.py +0 -451
  127. oscura/visualization/digital.py +0 -436
  128. oscura/visualization/eye.py +0 -571
  129. oscura/visualization/histogram.py +0 -281
  130. oscura/visualization/interactive.py +0 -1035
  131. oscura/visualization/jitter.py +0 -1042
  132. oscura/visualization/keyboard.py +0 -394
  133. oscura/visualization/layout.py +0 -400
  134. oscura/visualization/optimization.py +0 -1079
  135. oscura/visualization/palettes.py +0 -446
  136. oscura/visualization/power.py +0 -508
  137. oscura/visualization/power_extended.py +0 -955
  138. oscura/visualization/presets.py +0 -469
  139. oscura/visualization/protocols.py +0 -1246
  140. oscura/visualization/render.py +0 -223
  141. oscura/visualization/rendering.py +0 -444
  142. oscura/visualization/reverse_engineering.py +0 -838
  143. oscura/visualization/signal_integrity.py +0 -989
  144. oscura/visualization/specialized.py +0 -643
  145. oscura/visualization/spectral.py +0 -1226
  146. oscura/visualization/thumbnails.py +0 -340
  147. oscura/visualization/time_axis.py +0 -351
  148. oscura/visualization/waveform.py +0 -454
  149. {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/WHEEL +0 -0
  150. {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/entry_points.txt +0 -0
  151. {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,12 +1,12 @@
1
1
  """Batch plot generation for comprehensive signal visualization.
2
2
 
3
- This module provides utilities for generating multiple related plots from
4
- signal traces in a single operation, useful for comprehensive analysis reports.
3
+ Generate multiple related plots from signal traces in a single operation,
4
+ useful for comprehensive analysis reports.
5
5
 
6
6
  Example:
7
7
  >>> from oscura.visualization import batch
8
8
  >>> trace = osc.load("signal.wfm")
9
- >>> plots = batch.generate_all_plots(trace, output_format="base64")
9
+ >>> plots = batch.generate_all_plots(trace)
10
10
  >>> # Returns: {"waveform": <base64>, "fft": <base64>, ...}
11
11
  """
12
12
 
@@ -14,35 +14,22 @@ from __future__ import annotations
14
14
 
15
15
  import base64
16
16
  from io import BytesIO
17
- from typing import TYPE_CHECKING, Any
17
+ from typing import TYPE_CHECKING
18
18
 
19
19
  import matplotlib
20
20
 
21
- matplotlib.use("Agg") # Set non-interactive backend before importing pyplot
21
+ matplotlib.use("Agg") # Non-interactive backend
22
22
  import matplotlib.pyplot as plt
23
- import numpy as np
24
23
 
25
- # Import trace types for runtime isinstance checks
26
24
  from oscura.core.types import DigitalTrace, WaveformTrace
27
25
 
28
26
  if TYPE_CHECKING:
29
27
  from matplotlib.figure import Figure
30
- from numpy.typing import NDArray
31
28
 
32
- # Plot configuration constants
29
+ # Plot configuration
33
30
  PLOT_DPI = 150
34
31
  FIGURE_SIZE = (10, 6)
35
32
 
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
33
 
47
34
  def fig_to_base64(fig: Figure, *, dpi: int = PLOT_DPI) -> str:
48
35
  """Convert matplotlib figure to base64-encoded PNG.
@@ -55,467 +42,119 @@ def fig_to_base64(fig: Figure, *, dpi: int = PLOT_DPI) -> str:
55
42
  Base64-encoded PNG image string with data URI prefix.
56
43
 
57
44
  Example:
58
- >>> import matplotlib.pyplot as plt
59
45
  >>> fig, ax = plt.subplots()
60
46
  >>> ax.plot([1, 2, 3])
61
- >>> b64_str = fig_to_base64(fig)
62
- >>> assert b64_str.startswith("data:image/png;base64,")
47
+ >>> b64 = fig_to_base64(fig)
63
48
  """
64
49
  buf = BytesIO()
65
- fig.savefig(
66
- buf, format="png", dpi=dpi, bbox_inches="tight", facecolor="white", edgecolor="none"
67
- )
50
+ fig.savefig(buf, format="png", dpi=dpi, bbox_inches="tight", facecolor="white")
68
51
  buf.seek(0)
69
52
  img_base64 = base64.b64encode(buf.read()).decode("utf-8")
70
53
  plt.close(fig)
71
54
  return f"data:image/png;base64,{img_base64}"
72
55
 
73
56
 
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
57
  def generate_all_plots(
414
58
  trace: WaveformTrace | DigitalTrace,
415
59
  *,
416
- output_format: str = "base64",
417
60
  verbose: bool = True,
418
61
  ) -> dict[str, str]:
419
62
  """Generate all applicable plots for a signal trace.
420
63
 
421
64
  Automatically detects signal type and generates appropriate plots:
422
- - Analog signals: waveform, FFT spectrum, histogram, spectrogram, statistics
423
- - Digital signals: waveform, logic analyzer view
65
+ - Analog: waveform, FFT, histogram, spectrogram
66
+ - Digital: waveform, timing diagram
424
67
 
425
68
  Args:
426
69
  trace: WaveformTrace or DigitalTrace to visualize.
427
- output_format: Output format ("base64" only currently supported).
428
70
  verbose: Print progress messages.
429
71
 
430
72
  Returns:
431
73
  Dictionary mapping plot names to base64 image strings.
432
74
 
433
- Raises:
434
- ValueError: If output_format is not "base64".
435
-
436
75
  Example:
437
76
  >>> trace = osc.load("signal.wfm")
438
77
  >>> plots = generate_all_plots(trace)
439
- >>> len(plots) # 5 plots for analog signal
440
- 5
78
+ >>> len(plots) # 4-5 plots for analog signal
79
+ 4
441
80
  """
442
- if output_format != "base64":
443
- raise ValueError(f"Only 'base64' output format supported, got '{output_format}'")
81
+ from oscura.visualization.plot import (
82
+ plot_fft,
83
+ plot_histogram,
84
+ plot_spectrogram,
85
+ plot_timing,
86
+ plot_waveform,
87
+ )
444
88
 
445
89
  plots = {}
446
- is_digital = (
447
- trace.is_digital if hasattr(trace, "is_digital") else isinstance(trace, DigitalTrace)
448
- )
449
90
 
450
- # Always generate waveform plot
91
+ # Always generate waveform (handle both analog and digital)
451
92
  try:
452
- plots["waveform"] = plot_waveform(trace)
93
+ fig, ax = plt.subplots(figsize=FIGURE_SIZE)
94
+ if isinstance(trace, WaveformTrace):
95
+ plot_waveform(ax, trace)
96
+ else:
97
+ # Digital trace
98
+ plot_timing(ax, trace)
99
+ plots["waveform"] = fig_to_base64(fig)
453
100
  if verbose:
454
101
  print(" ✓ Generated waveform plot")
455
102
  except Exception as e:
456
103
  if verbose:
457
104
  print(f" ⚠ Waveform plot failed: {e}")
458
105
 
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}")
106
+ if isinstance(trace, WaveformTrace):
107
+ # FFT spectrum
108
+ try:
109
+ fig, ax = plt.subplots(figsize=FIGURE_SIZE)
110
+ plot_fft(ax, trace)
111
+ plots["fft"] = fig_to_base64(fig)
112
+ if verbose:
113
+ print(" ✓ Generated FFT spectrum")
114
+ except Exception as e:
115
+ if verbose:
116
+ print(f" ⚠ FFT plot failed: {e}")
117
+
118
+ # Histogram
119
+ try:
120
+ fig, ax = plt.subplots(figsize=FIGURE_SIZE)
121
+ plot_histogram(ax, trace.data)
122
+ plots["histogram"] = fig_to_base64(fig)
123
+ if verbose:
124
+ print(" ✓ Generated histogram")
125
+ except Exception as e:
126
+ if verbose:
127
+ print(f" ⚠ Histogram plot failed: {e}")
128
+
129
+ # Spectrogram
130
+ try:
131
+ fig, ax = plt.subplots(figsize=FIGURE_SIZE)
132
+ plot_spectrogram(ax, trace)
133
+ plots["spectrogram"] = fig_to_base64(fig)
134
+ if verbose:
135
+ print(" ✓ Generated spectrogram")
136
+ except Exception as e:
137
+ if verbose:
138
+ print(f" ⚠ Spectrogram plot failed: {e}")
139
+
140
+ elif isinstance(trace, DigitalTrace):
141
+ # Timing diagram
142
+ try:
143
+ fig, ax = plt.subplots(figsize=FIGURE_SIZE)
144
+ plot_timing(ax, trace)
145
+ plots["timing"] = fig_to_base64(fig)
146
+ if verbose:
147
+ print(" ✓ Generated timing diagram")
148
+ except Exception as e:
149
+ if verbose:
150
+ print(f" ⚠ Timing plot failed: {e}")
505
151
 
506
152
  return plots
507
153
 
508
154
 
509
155
  __all__ = [
510
- "COLORS",
511
156
  "FIGURE_SIZE",
512
157
  "PLOT_DPI",
513
158
  "fig_to_base64",
514
159
  "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
160
  ]