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