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,508 +0,0 @@
1
- """Power profile visualization.
2
-
3
-
4
- This module provides comprehensive power visualization including
5
- time-domain plots, energy accumulation, and multi-channel views.
6
- """
7
-
8
- from __future__ import annotations
9
-
10
- from pathlib import Path
11
-
12
- import matplotlib.pyplot as plt
13
- import numpy as np
14
- from matplotlib.axes import Axes
15
- from matplotlib.figure import Figure
16
- from numpy.typing import NDArray
17
-
18
-
19
- def _normalize_power_channels(
20
- power: NDArray[np.float64] | dict[str, NDArray[np.float64]],
21
- ) -> tuple[dict[str, NDArray[np.float64]], bool]:
22
- """Normalize power input into channels dictionary.
23
-
24
- Args:
25
- power: Single array or dict of arrays.
26
-
27
- Returns:
28
- Tuple of (channels dict, is_multi boolean).
29
-
30
- Example:
31
- >>> channels, is_multi = _normalize_power_channels(np.array([1, 2, 3]))
32
- >>> channels
33
- {'Power': array([1, 2, 3])}
34
- """
35
- if isinstance(power, dict):
36
- return power, True
37
- return {"Power": np.asarray(power, dtype=np.float64)}, False
38
-
39
-
40
- def _validate_and_create_time_array(
41
- time_array: NDArray[np.float64] | None,
42
- sample_rate: float | None,
43
- trace_length: int,
44
- ) -> NDArray[np.float64]:
45
- """Validate inputs and create time array.
46
-
47
- Args:
48
- time_array: Optional explicit time array.
49
- sample_rate: Optional sample rate in Hz.
50
- trace_length: Length of power trace.
51
-
52
- Returns:
53
- Validated time array.
54
-
55
- Raises:
56
- ValueError: If neither time_array nor sample_rate provided.
57
- ValueError: If time_array length doesn't match trace.
58
-
59
- Example:
60
- >>> time = _validate_and_create_time_array(None, 1000.0, 100)
61
- >>> len(time)
62
- 100
63
- """
64
- if time_array is None and sample_rate is None:
65
- raise ValueError("Either time_array or sample_rate must be provided")
66
-
67
- if time_array is None:
68
- if sample_rate is None:
69
- raise ValueError("sample_rate is required when time_array is not provided")
70
- return np.arange(trace_length) / sample_rate
71
-
72
- time_array_validated = np.asarray(time_array, dtype=np.float64)
73
- if len(time_array_validated) != trace_length:
74
- raise ValueError(
75
- f"time_array length {len(time_array_validated)} doesn't match "
76
- f"power trace length {trace_length}"
77
- )
78
- return time_array_validated
79
-
80
-
81
- def _compute_time_scale(time_array: NDArray[np.float64]) -> tuple[NDArray[np.float64], str]:
82
- """Compute time scaling factor and units.
83
-
84
- Args:
85
- time_array: Time array in seconds.
86
-
87
- Returns:
88
- Tuple of (scaled time array, unit string).
89
-
90
- Example:
91
- >>> time = np.array([0, 1e-6, 2e-6])
92
- >>> scaled, unit = _compute_time_scale(time)
93
- >>> unit
94
- 'µs'
95
- """
96
- time_max = time_array[-1]
97
-
98
- if time_max < 1e-6:
99
- return time_array * 1e9, "ns"
100
- if time_max < 1e-3:
101
- return time_array * 1e6, "µs"
102
- if time_max < 1:
103
- return time_array * 1e3, "ms"
104
- return time_array, "s"
105
-
106
-
107
- def _create_figure_layout(
108
- is_multi: bool,
109
- layout: str,
110
- n_channels: int,
111
- show_energy: bool,
112
- figsize: tuple[float, float],
113
- ) -> tuple[Figure, list[Axes]]:
114
- """Create figure and axes layout.
115
-
116
- Args:
117
- is_multi: Multiple channels flag.
118
- layout: 'stacked' or 'overlay'.
119
- n_channels: Number of channels.
120
- show_energy: Show energy plot flag.
121
- figsize: Figure size.
122
-
123
- Returns:
124
- Tuple of (figure, axes list).
125
-
126
- Example:
127
- >>> fig, axes = _create_figure_layout(True, 'stacked', 2, True, (12, 6))
128
- >>> len(axes)
129
- 3
130
- """
131
- if is_multi and layout == "stacked":
132
- n_plots = n_channels + (1 if show_energy else 0)
133
- fig, axes_obj = plt.subplots(n_plots, 1, figsize=figsize, sharex=True)
134
- if n_plots == 1:
135
- return fig, [axes_obj]
136
- return fig, list(axes_obj)
137
-
138
- fig, ax_power = plt.subplots(figsize=figsize)
139
- return fig, [ax_power]
140
-
141
-
142
- def _plot_stacked_channels(
143
- axes: list[Axes],
144
- channels: dict[str, NDArray[np.float64]],
145
- time_scaled: NDArray[np.float64],
146
- time_unit: str,
147
- statistics: dict[str, float] | None,
148
- show_average: bool,
149
- show_peak: bool,
150
- show_energy: bool,
151
- sample_rate: float | None,
152
- ) -> None:
153
- """Plot channels in stacked layout.
154
-
155
- Args:
156
- axes: List of axes objects.
157
- channels: Channel data dictionary.
158
- time_scaled: Scaled time array.
159
- time_unit: Time unit string.
160
- statistics: Optional statistics dictionary.
161
- show_average: Show average line flag.
162
- show_peak: Show peak marker flag.
163
- show_energy: Show energy plot flag.
164
- sample_rate: Sample rate in Hz.
165
-
166
- Example:
167
- >>> _plot_stacked_channels(axes, channels, time, 'ms', None, True, True, True, 1e6)
168
- """
169
- for idx, (name, trace) in enumerate(channels.items()):
170
- ax = axes[idx]
171
- ax.plot(time_scaled, trace * 1e3, linewidth=0.8, label=name)
172
-
173
- # Compute or use statistics
174
- if statistics is None or name not in statistics:
175
- avg = np.mean(trace)
176
- peak = np.max(trace)
177
- else:
178
- avg = statistics[name]["average"] # type: ignore[index]
179
- peak = statistics[name]["peak"] # type: ignore[index]
180
-
181
- # Annotations
182
- if show_average:
183
- ax.axhline(
184
- float(avg * 1e3),
185
- color="r",
186
- linestyle="--",
187
- linewidth=1,
188
- alpha=0.7,
189
- label=f"Avg: {avg * 1e3:.2f} mW",
190
- )
191
-
192
- if show_peak:
193
- peak_idx = np.argmax(trace)
194
- ax.plot(
195
- time_scaled[peak_idx],
196
- peak * 1e3,
197
- "rv",
198
- markersize=8,
199
- label=f"Peak: {peak * 1e3:.2f} mW",
200
- )
201
-
202
- ax.set_ylabel(f"{name}\n(mW)")
203
- ax.legend(loc="upper right", fontsize=8)
204
- ax.grid(True, alpha=0.3)
205
-
206
- # Energy accumulation plot
207
- if show_energy:
208
- ax_energy = axes[-1]
209
- for name, trace in channels.items():
210
- if sample_rate is not None:
211
- energy = np.cumsum(trace) / sample_rate * 1e6 # µJ
212
- ax_energy.plot(time_scaled, energy, linewidth=0.8, label=name)
213
-
214
- ax_energy.set_ylabel("Cumulative\nEnergy (µJ)")
215
- ax_energy.set_xlabel(f"Time ({time_unit})")
216
- ax_energy.legend(loc="upper left", fontsize=8)
217
- ax_energy.grid(True, alpha=0.3)
218
-
219
-
220
- def _plot_overlay_channels(
221
- ax: Axes,
222
- channels: dict[str, NDArray[np.float64]],
223
- time_scaled: NDArray[np.float64],
224
- time_unit: str,
225
- statistics: dict[str, float] | None,
226
- show_average: bool,
227
- show_peak: bool,
228
- show_energy: bool,
229
- sample_rate: float | None,
230
- ) -> None:
231
- """Plot channels in overlay layout.
232
-
233
- Args:
234
- ax: Axes object.
235
- channels: Channel data dictionary.
236
- time_scaled: Scaled time array.
237
- time_unit: Time unit string.
238
- statistics: Optional statistics dictionary.
239
- show_average: Show average line flag.
240
- show_peak: Show peak marker flag.
241
- show_energy: Show energy plot flag.
242
- sample_rate: Sample rate in Hz.
243
-
244
- Example:
245
- >>> _plot_overlay_channels(ax, channels, time, 'ms', None, True, True, True, 1e6)
246
- """
247
- for name, trace in channels.items():
248
- ax.plot(time_scaled, trace * 1e3, linewidth=0.8, label=name)
249
-
250
- # Statistics for first channel (or combined if overlay)
251
- first_trace = next(iter(channels.values()))
252
- if statistics is None:
253
- avg_val = float(np.mean(first_trace))
254
- peak_val = float(np.max(first_trace))
255
- total_energy_val: float | None = (
256
- float(np.sum(first_trace) / sample_rate) if sample_rate else None
257
- )
258
- else:
259
- avg_val = float(statistics.get("average", float(np.mean(first_trace))))
260
- peak_val = float(statistics.get("peak", float(np.max(first_trace))))
261
- total_energy_val = statistics.get("energy", None)
262
-
263
- # Annotations
264
- if show_average:
265
- ax.axhline(
266
- avg_val * 1e3,
267
- color="r",
268
- linestyle="--",
269
- linewidth=1,
270
- alpha=0.7,
271
- label=f"Avg: {avg_val * 1e3:.2f} mW",
272
- )
273
-
274
- if show_peak:
275
- peak_idx = np.argmax(first_trace)
276
- ax.plot(
277
- time_scaled[peak_idx],
278
- peak_val * 1e3,
279
- "rv",
280
- markersize=8,
281
- label=f"Peak: {peak_val * 1e3:.2f} mW",
282
- )
283
-
284
- ax.set_ylabel("Power (mW)")
285
- ax.set_xlabel(f"Time ({time_unit})")
286
- ax.legend(loc="upper right")
287
- ax.grid(True, alpha=0.3)
288
-
289
- # Energy overlay on secondary y-axis
290
- if show_energy and sample_rate is not None:
291
- ax2 = ax.twinx()
292
- energy = np.cumsum(first_trace) / sample_rate * 1e6 # µJ
293
- ax2.plot(time_scaled, energy, "g--", linewidth=1.5, alpha=0.6)
294
- ax2.set_ylabel("Cumulative Energy (µJ)", color="g")
295
- ax2.tick_params(axis="y", labelcolor="g")
296
-
297
- if total_energy_val is not None:
298
- ax2.text(
299
- 0.98,
300
- 0.98,
301
- f"Total: {total_energy_val * 1e6:.2f} µJ",
302
- transform=ax.transAxes,
303
- ha="right",
304
- va="top",
305
- bbox={"boxstyle": "round", "facecolor": "wheat", "alpha": 0.5},
306
- )
307
-
308
-
309
- def _prepare_power_plot_data(
310
- power: NDArray[np.float64] | dict[str, NDArray[np.float64]],
311
- time_array: NDArray[np.float64] | None,
312
- sample_rate: float | None,
313
- ) -> tuple[dict[str, NDArray[np.float64]], bool, NDArray[np.float64], str]:
314
- """Prepare data for power plot.
315
-
316
- Args:
317
- power: Power data (array or dict).
318
- time_array: Optional time array.
319
- sample_rate: Optional sample rate.
320
-
321
- Returns:
322
- Tuple of (channels, is_multi, time_scaled, time_unit).
323
- """
324
- channels, is_multi = _normalize_power_channels(power)
325
- time_array_validated = _validate_and_create_time_array(
326
- time_array, sample_rate, len(next(iter(channels.values())))
327
- )
328
- time_scaled, time_unit = _compute_time_scale(time_array_validated)
329
- return channels, is_multi, time_scaled, time_unit
330
-
331
-
332
- def _render_power_plots(
333
- axes: list[Axes],
334
- channels: dict[str, NDArray[np.float64]],
335
- time_scaled: NDArray[np.float64],
336
- time_unit: str,
337
- is_multi: bool,
338
- layout: str,
339
- statistics: dict[str, float] | None,
340
- show_average: bool,
341
- show_peak: bool,
342
- show_energy: bool,
343
- sample_rate: float | None,
344
- ) -> None:
345
- """Render power plots based on layout.
346
-
347
- Args:
348
- axes: List of axes.
349
- channels: Channel data.
350
- time_scaled: Scaled time array.
351
- time_unit: Time unit string.
352
- is_multi: Multiple channels flag.
353
- layout: Layout type.
354
- statistics: Optional statistics.
355
- show_average: Show average line.
356
- show_peak: Show peak marker.
357
- show_energy: Show energy plot.
358
- sample_rate: Sample rate.
359
- """
360
- if is_multi and layout == "stacked":
361
- _plot_stacked_channels(
362
- axes,
363
- channels,
364
- time_scaled,
365
- time_unit,
366
- statistics,
367
- show_average,
368
- show_peak,
369
- show_energy,
370
- sample_rate,
371
- )
372
- else:
373
- _plot_overlay_channels(
374
- axes[0],
375
- channels,
376
- time_scaled,
377
- time_unit,
378
- statistics,
379
- show_average,
380
- show_peak,
381
- show_energy,
382
- sample_rate,
383
- )
384
-
385
-
386
- def _finalize_plot(
387
- fig: Figure,
388
- title: str | None,
389
- is_multi: bool,
390
- save_path: str | Path | None,
391
- show: bool,
392
- ) -> None:
393
- """Finalize plot with title, layout, save, and display.
394
-
395
- Args:
396
- fig: Figure object.
397
- title: Plot title.
398
- is_multi: Multiple channels flag.
399
- save_path: Optional save path.
400
- show: Display flag.
401
-
402
- Example:
403
- >>> _finalize_plot(fig, "Power Profile", False, None, True)
404
- """
405
- if title is None:
406
- title = "Power Profile" + (" (Multi-Channel)" if is_multi else "")
407
- fig.suptitle(title, fontsize=14, fontweight="bold")
408
-
409
- plt.tight_layout()
410
-
411
- if save_path is not None:
412
- fig.savefig(save_path, dpi=150, bbox_inches="tight")
413
-
414
- if show:
415
- plt.show()
416
-
417
-
418
- def plot_power_profile(
419
- power: NDArray[np.float64] | dict[str, NDArray[np.float64]],
420
- *,
421
- sample_rate: float | None = None,
422
- time_array: NDArray[np.float64] | None = None,
423
- statistics: dict[str, float] | None = None,
424
- show_average: bool = True,
425
- show_peak: bool = True,
426
- show_energy: bool = True,
427
- multi_channel_layout: str = "stacked",
428
- title: str | None = None,
429
- figsize: tuple[float, float] = (12, 6),
430
- save_path: str | Path | None = None,
431
- show: bool = True,
432
- ) -> Figure:
433
- """Generate power profile plot with annotations.
434
-
435
- Time-domain power visualization with average/peak markers
436
- and optional energy accumulation overlay. Supports multi-channel stacked view.
437
-
438
- Args:
439
- power: Power trace in watts. Can be:
440
- - Array: Single channel power trace
441
- - Dict: Multiple channels {name: trace}
442
- sample_rate: Sample rate in Hz (required if time_array not provided)
443
- time_array: Optional explicit time array (overrides sample_rate)
444
- statistics: Optional pre-computed statistics dict from power_statistics()
445
- If provided, used for annotations. Otherwise computed automatically.
446
- show_average: Show average power horizontal line (default: True)
447
- show_peak: Show peak power marker (default: True)
448
- show_energy: Show cumulative energy overlay (default: True)
449
- multi_channel_layout: Layout for multiple channels:
450
- - 'stacked': Separate subplots stacked vertically (default)
451
- - 'overlay': All channels on same plot
452
- title: Plot title (default: "Power Profile")
453
- figsize: Figure size as (width, height) in inches
454
- save_path: Optional path to save figure
455
- show: Display the figure (default: True)
456
-
457
- Returns:
458
- Matplotlib Figure object for further customization
459
-
460
- Raises:
461
- ValueError: If neither sample_rate nor time_array provided
462
- ValueError: If time_array length doesn't match power trace
463
-
464
- Examples:
465
- >>> import numpy as np
466
- >>> power = np.random.rand(1000) * 0.5 + 0.3
467
- >>> fig = plot_power_profile(power, sample_rate=1e6, title="Power")
468
-
469
- >>> from oscura.analyzers.power import power_statistics
470
- >>> stats = power_statistics(power, sample_rate=1e6)
471
- >>> fig = plot_power_profile(power, sample_rate=1e6, statistics=stats)
472
-
473
- >>> power_channels = {
474
- ... 'VDD_CORE': np.random.rand(1000) * 0.5,
475
- ... 'VDD_IO': np.random.rand(1000) * 0.3,
476
- ... }
477
- >>> fig = plot_power_profile(power_channels, sample_rate=1e6)
478
-
479
- Notes:
480
- - Energy accumulation computed via cumulative sum
481
- - Multiple channels can be overlaid or stacked
482
- - Annotations include average, peak, and total energy
483
- - Time axis auto-scaled to appropriate units (ns/µs/ms/s)
484
-
485
- References:
486
- PWR-004: Power Profile Visualization
487
- """
488
- channels, is_multi, time_scaled, time_unit = _prepare_power_plot_data(
489
- power, time_array, sample_rate
490
- )
491
- fig, axes = _create_figure_layout(
492
- is_multi, multi_channel_layout, len(channels), show_energy, figsize
493
- )
494
- _render_power_plots(
495
- axes,
496
- channels,
497
- time_scaled,
498
- time_unit,
499
- is_multi,
500
- multi_channel_layout,
501
- statistics,
502
- show_average,
503
- show_peak,
504
- show_energy,
505
- sample_rate,
506
- )
507
- _finalize_plot(fig, title, is_multi, save_path, show)
508
- return fig