oscura 0.8.0__py3-none-any.whl → 0.11.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 (161) hide show
  1. oscura/__init__.py +19 -19
  2. oscura/__main__.py +4 -0
  3. oscura/analyzers/__init__.py +2 -0
  4. oscura/analyzers/digital/extraction.py +2 -3
  5. oscura/analyzers/digital/quality.py +1 -1
  6. oscura/analyzers/digital/timing.py +1 -1
  7. oscura/analyzers/ml/signal_classifier.py +6 -0
  8. oscura/analyzers/patterns/__init__.py +66 -0
  9. oscura/analyzers/power/basic.py +3 -3
  10. oscura/analyzers/power/soa.py +1 -1
  11. oscura/analyzers/power/switching.py +3 -3
  12. oscura/analyzers/signal_classification.py +529 -0
  13. oscura/analyzers/signal_integrity/sparams.py +3 -3
  14. oscura/analyzers/statistics/basic.py +10 -7
  15. oscura/analyzers/validation.py +1 -1
  16. oscura/analyzers/waveform/measurements.py +200 -156
  17. oscura/analyzers/waveform/measurements_with_uncertainty.py +91 -35
  18. oscura/analyzers/waveform/spectral.py +182 -84
  19. oscura/api/dsl/commands.py +15 -6
  20. oscura/api/server/templates/base.html +137 -146
  21. oscura/api/server/templates/export.html +84 -110
  22. oscura/api/server/templates/home.html +248 -267
  23. oscura/api/server/templates/protocols.html +44 -48
  24. oscura/api/server/templates/reports.html +27 -35
  25. oscura/api/server/templates/session_detail.html +68 -78
  26. oscura/api/server/templates/sessions.html +62 -72
  27. oscura/api/server/templates/waveforms.html +54 -64
  28. oscura/automotive/__init__.py +1 -1
  29. oscura/automotive/can/session.py +1 -1
  30. oscura/automotive/dbc/generator.py +638 -23
  31. oscura/automotive/dtc/data.json +17 -102
  32. oscura/automotive/flexray/fibex.py +9 -1
  33. oscura/automotive/uds/decoder.py +99 -6
  34. oscura/cli/analyze.py +8 -2
  35. oscura/cli/batch.py +36 -5
  36. oscura/cli/characterize.py +18 -4
  37. oscura/cli/export.py +47 -5
  38. oscura/cli/main.py +2 -0
  39. oscura/cli/onboarding/wizard.py +10 -6
  40. oscura/cli/pipeline.py +585 -0
  41. oscura/cli/visualize.py +6 -4
  42. oscura/convenience.py +400 -32
  43. oscura/core/measurement_result.py +286 -0
  44. oscura/core/progress.py +1 -1
  45. oscura/core/schemas/device_mapping.json +2 -8
  46. oscura/core/schemas/packet_format.json +4 -24
  47. oscura/core/schemas/protocol_definition.json +2 -12
  48. oscura/core/types.py +232 -239
  49. oscura/correlation/multi_protocol.py +1 -1
  50. oscura/export/legacy/__init__.py +11 -0
  51. oscura/export/legacy/wav.py +75 -0
  52. oscura/exporters/__init__.py +19 -0
  53. oscura/exporters/wireshark.py +809 -0
  54. oscura/hardware/acquisition/file.py +5 -19
  55. oscura/hardware/acquisition/saleae.py +10 -10
  56. oscura/hardware/acquisition/socketcan.py +4 -6
  57. oscura/hardware/acquisition/synthetic.py +1 -5
  58. oscura/hardware/acquisition/visa.py +6 -6
  59. oscura/hardware/security/side_channel_detector.py +5 -508
  60. oscura/inference/message_format.py +686 -1
  61. oscura/jupyter/display.py +2 -2
  62. oscura/jupyter/magic.py +3 -3
  63. oscura/loaders/__init__.py +17 -12
  64. oscura/loaders/binary.py +1 -1
  65. oscura/loaders/chipwhisperer.py +1 -2
  66. oscura/loaders/configurable.py +1 -1
  67. oscura/loaders/csv_loader.py +2 -2
  68. oscura/loaders/hdf5_loader.py +1 -1
  69. oscura/loaders/lazy.py +6 -1
  70. oscura/loaders/mmap_loader.py +0 -1
  71. oscura/loaders/numpy_loader.py +8 -7
  72. oscura/loaders/preprocessing.py +3 -5
  73. oscura/loaders/rigol.py +21 -7
  74. oscura/loaders/sigrok.py +2 -5
  75. oscura/loaders/tdms.py +3 -2
  76. oscura/loaders/tektronix.py +38 -32
  77. oscura/loaders/tss.py +20 -27
  78. oscura/loaders/validation.py +17 -10
  79. oscura/loaders/vcd.py +13 -8
  80. oscura/loaders/wav.py +1 -6
  81. oscura/pipeline/__init__.py +76 -0
  82. oscura/pipeline/handlers/__init__.py +165 -0
  83. oscura/pipeline/handlers/analyzers.py +1045 -0
  84. oscura/pipeline/handlers/decoders.py +899 -0
  85. oscura/pipeline/handlers/exporters.py +1103 -0
  86. oscura/pipeline/handlers/filters.py +891 -0
  87. oscura/pipeline/handlers/loaders.py +640 -0
  88. oscura/pipeline/handlers/transforms.py +768 -0
  89. oscura/reporting/formatting/measurements.py +55 -14
  90. oscura/reporting/templates/enhanced/protocol_re.html +504 -503
  91. oscura/sessions/legacy.py +49 -1
  92. oscura/side_channel/__init__.py +38 -57
  93. oscura/utils/builders/signal_builder.py +5 -5
  94. oscura/utils/comparison/compare.py +7 -9
  95. oscura/utils/comparison/golden.py +1 -1
  96. oscura/utils/filtering/convenience.py +2 -2
  97. oscura/utils/math/arithmetic.py +38 -62
  98. oscura/utils/math/interpolation.py +20 -20
  99. oscura/utils/pipeline/__init__.py +4 -17
  100. oscura/utils/progressive.py +1 -4
  101. oscura/utils/triggering/edge.py +1 -1
  102. oscura/utils/triggering/pattern.py +2 -2
  103. oscura/utils/triggering/pulse.py +2 -2
  104. oscura/utils/triggering/window.py +3 -3
  105. oscura/validation/hil_testing.py +11 -11
  106. oscura/visualization/__init__.py +46 -284
  107. oscura/visualization/batch.py +72 -433
  108. oscura/visualization/plot.py +542 -53
  109. oscura/visualization/styles.py +184 -318
  110. oscura/workflows/batch/advanced.py +1 -1
  111. oscura/workflows/batch/aggregate.py +12 -9
  112. oscura/workflows/complete_re.py +251 -23
  113. oscura/workflows/digital.py +27 -4
  114. oscura/workflows/multi_trace.py +136 -17
  115. oscura/workflows/waveform.py +11 -6
  116. oscura-0.11.0.dist-info/METADATA +460 -0
  117. {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/RECORD +120 -145
  118. oscura/side_channel/dpa.py +0 -1025
  119. oscura/utils/optimization/__init__.py +0 -19
  120. oscura/utils/optimization/parallel.py +0 -443
  121. oscura/utils/optimization/search.py +0 -532
  122. oscura/utils/pipeline/base.py +0 -338
  123. oscura/utils/pipeline/composition.py +0 -248
  124. oscura/utils/pipeline/parallel.py +0 -449
  125. oscura/utils/pipeline/pipeline.py +0 -375
  126. oscura/utils/search/__init__.py +0 -16
  127. oscura/utils/search/anomaly.py +0 -424
  128. oscura/utils/search/context.py +0 -294
  129. oscura/utils/search/pattern.py +0 -288
  130. oscura/utils/storage/__init__.py +0 -61
  131. oscura/utils/storage/database.py +0 -1166
  132. oscura/visualization/accessibility.py +0 -526
  133. oscura/visualization/annotations.py +0 -371
  134. oscura/visualization/axis_scaling.py +0 -305
  135. oscura/visualization/colors.py +0 -451
  136. oscura/visualization/digital.py +0 -436
  137. oscura/visualization/eye.py +0 -571
  138. oscura/visualization/histogram.py +0 -281
  139. oscura/visualization/interactive.py +0 -1035
  140. oscura/visualization/jitter.py +0 -1042
  141. oscura/visualization/keyboard.py +0 -394
  142. oscura/visualization/layout.py +0 -400
  143. oscura/visualization/optimization.py +0 -1079
  144. oscura/visualization/palettes.py +0 -446
  145. oscura/visualization/power.py +0 -508
  146. oscura/visualization/power_extended.py +0 -955
  147. oscura/visualization/presets.py +0 -469
  148. oscura/visualization/protocols.py +0 -1246
  149. oscura/visualization/render.py +0 -223
  150. oscura/visualization/rendering.py +0 -444
  151. oscura/visualization/reverse_engineering.py +0 -838
  152. oscura/visualization/signal_integrity.py +0 -989
  153. oscura/visualization/specialized.py +0 -643
  154. oscura/visualization/spectral.py +0 -1226
  155. oscura/visualization/thumbnails.py +0 -340
  156. oscura/visualization/time_axis.py +0 -351
  157. oscura/visualization/waveform.py +0 -454
  158. oscura-0.8.0.dist-info/METADATA +0 -661
  159. {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/WHEEL +0 -0
  160. {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/entry_points.txt +0 -0
  161. {oscura-0.8.0.dist-info → oscura-0.11.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