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.
- oscura/__init__.py +19 -19
- oscura/analyzers/__init__.py +2 -0
- oscura/analyzers/digital/extraction.py +2 -3
- oscura/analyzers/digital/quality.py +1 -1
- oscura/analyzers/digital/timing.py +1 -1
- oscura/analyzers/eye/__init__.py +5 -1
- oscura/analyzers/eye/generation.py +501 -0
- oscura/analyzers/jitter/__init__.py +6 -6
- oscura/analyzers/jitter/timing.py +419 -0
- oscura/analyzers/patterns/__init__.py +94 -0
- oscura/analyzers/patterns/reverse_engineering.py +991 -0
- oscura/analyzers/power/__init__.py +35 -12
- oscura/analyzers/power/basic.py +3 -3
- oscura/analyzers/power/soa.py +1 -1
- oscura/analyzers/power/switching.py +3 -3
- oscura/analyzers/signal_classification.py +529 -0
- oscura/analyzers/signal_integrity/sparams.py +3 -3
- oscura/analyzers/statistics/__init__.py +4 -0
- oscura/analyzers/statistics/basic.py +152 -0
- oscura/analyzers/statistics/correlation.py +47 -6
- oscura/analyzers/validation.py +1 -1
- oscura/analyzers/waveform/__init__.py +2 -0
- oscura/analyzers/waveform/measurements.py +329 -163
- oscura/analyzers/waveform/measurements_with_uncertainty.py +91 -35
- oscura/analyzers/waveform/spectral.py +498 -54
- oscura/api/dsl/commands.py +15 -6
- oscura/api/server/templates/base.html +137 -146
- oscura/api/server/templates/export.html +84 -110
- oscura/api/server/templates/home.html +248 -267
- oscura/api/server/templates/protocols.html +44 -48
- oscura/api/server/templates/reports.html +27 -35
- oscura/api/server/templates/session_detail.html +68 -78
- oscura/api/server/templates/sessions.html +62 -72
- oscura/api/server/templates/waveforms.html +54 -64
- oscura/automotive/__init__.py +1 -1
- oscura/automotive/can/session.py +1 -1
- oscura/automotive/dbc/generator.py +638 -23
- oscura/automotive/dtc/data.json +102 -17
- oscura/automotive/uds/decoder.py +99 -6
- oscura/cli/analyze.py +8 -2
- oscura/cli/batch.py +36 -5
- oscura/cli/characterize.py +18 -4
- oscura/cli/export.py +47 -5
- oscura/cli/main.py +2 -0
- oscura/cli/onboarding/wizard.py +10 -6
- oscura/cli/pipeline.py +585 -0
- oscura/cli/visualize.py +6 -4
- oscura/convenience.py +400 -32
- oscura/core/config/loader.py +0 -1
- oscura/core/measurement_result.py +286 -0
- oscura/core/progress.py +1 -1
- oscura/core/schemas/device_mapping.json +8 -2
- oscura/core/schemas/packet_format.json +24 -4
- oscura/core/schemas/protocol_definition.json +12 -2
- oscura/core/types.py +300 -199
- oscura/correlation/multi_protocol.py +1 -1
- oscura/export/legacy/__init__.py +11 -0
- oscura/export/legacy/wav.py +75 -0
- oscura/exporters/__init__.py +19 -0
- oscura/exporters/wireshark.py +809 -0
- oscura/hardware/acquisition/file.py +5 -19
- oscura/hardware/acquisition/saleae.py +10 -10
- oscura/hardware/acquisition/socketcan.py +4 -6
- oscura/hardware/acquisition/synthetic.py +1 -5
- oscura/hardware/acquisition/visa.py +6 -6
- oscura/hardware/security/side_channel_detector.py +5 -508
- oscura/inference/message_format.py +686 -1
- oscura/jupyter/display.py +2 -2
- oscura/jupyter/magic.py +3 -3
- oscura/loaders/__init__.py +17 -12
- oscura/loaders/binary.py +1 -1
- oscura/loaders/chipwhisperer.py +1 -2
- oscura/loaders/configurable.py +1 -1
- oscura/loaders/csv_loader.py +2 -2
- oscura/loaders/hdf5_loader.py +1 -1
- oscura/loaders/lazy.py +6 -1
- oscura/loaders/mmap_loader.py +0 -1
- oscura/loaders/numpy_loader.py +8 -7
- oscura/loaders/preprocessing.py +3 -5
- oscura/loaders/rigol.py +21 -7
- oscura/loaders/sigrok.py +2 -5
- oscura/loaders/tdms.py +3 -2
- oscura/loaders/tektronix.py +38 -32
- oscura/loaders/tss.py +20 -27
- oscura/loaders/vcd.py +13 -8
- oscura/loaders/wav.py +1 -6
- oscura/pipeline/__init__.py +76 -0
- oscura/pipeline/handlers/__init__.py +165 -0
- oscura/pipeline/handlers/analyzers.py +1045 -0
- oscura/pipeline/handlers/decoders.py +899 -0
- oscura/pipeline/handlers/exporters.py +1103 -0
- oscura/pipeline/handlers/filters.py +891 -0
- oscura/pipeline/handlers/loaders.py +640 -0
- oscura/pipeline/handlers/transforms.py +768 -0
- oscura/reporting/__init__.py +88 -1
- oscura/reporting/automation.py +348 -0
- oscura/reporting/citations.py +374 -0
- oscura/reporting/core.py +54 -0
- oscura/reporting/formatting/__init__.py +11 -0
- oscura/reporting/formatting/measurements.py +320 -0
- oscura/reporting/html.py +57 -0
- oscura/reporting/interpretation.py +431 -0
- oscura/reporting/summary.py +329 -0
- oscura/reporting/templates/enhanced/protocol_re.html +504 -503
- oscura/reporting/visualization.py +542 -0
- oscura/side_channel/__init__.py +38 -57
- oscura/utils/builders/signal_builder.py +5 -5
- oscura/utils/comparison/compare.py +7 -9
- oscura/utils/comparison/golden.py +1 -1
- oscura/utils/filtering/convenience.py +2 -2
- oscura/utils/math/arithmetic.py +38 -62
- oscura/utils/math/interpolation.py +20 -20
- oscura/utils/pipeline/__init__.py +4 -17
- oscura/utils/progressive.py +1 -4
- oscura/utils/triggering/edge.py +1 -1
- oscura/utils/triggering/pattern.py +2 -2
- oscura/utils/triggering/pulse.py +2 -2
- oscura/utils/triggering/window.py +3 -3
- oscura/validation/hil_testing.py +11 -11
- oscura/visualization/__init__.py +47 -284
- oscura/visualization/batch.py +160 -0
- oscura/visualization/plot.py +542 -53
- oscura/visualization/styles.py +184 -318
- oscura/workflows/__init__.py +2 -0
- oscura/workflows/batch/advanced.py +1 -1
- oscura/workflows/batch/aggregate.py +7 -8
- oscura/workflows/complete_re.py +251 -23
- oscura/workflows/digital.py +27 -4
- oscura/workflows/multi_trace.py +136 -17
- oscura/workflows/waveform.py +788 -0
- {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/METADATA +59 -79
- {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/RECORD +135 -149
- oscura/side_channel/dpa.py +0 -1025
- oscura/utils/optimization/__init__.py +0 -19
- oscura/utils/optimization/parallel.py +0 -443
- oscura/utils/optimization/search.py +0 -532
- oscura/utils/pipeline/base.py +0 -338
- oscura/utils/pipeline/composition.py +0 -248
- oscura/utils/pipeline/parallel.py +0 -449
- oscura/utils/pipeline/pipeline.py +0 -375
- oscura/utils/search/__init__.py +0 -16
- oscura/utils/search/anomaly.py +0 -424
- oscura/utils/search/context.py +0 -294
- oscura/utils/search/pattern.py +0 -288
- oscura/utils/storage/__init__.py +0 -61
- oscura/utils/storage/database.py +0 -1166
- oscura/visualization/accessibility.py +0 -526
- oscura/visualization/annotations.py +0 -371
- oscura/visualization/axis_scaling.py +0 -305
- oscura/visualization/colors.py +0 -451
- oscura/visualization/digital.py +0 -436
- oscura/visualization/eye.py +0 -571
- oscura/visualization/histogram.py +0 -281
- oscura/visualization/interactive.py +0 -1035
- oscura/visualization/jitter.py +0 -1042
- oscura/visualization/keyboard.py +0 -394
- oscura/visualization/layout.py +0 -400
- oscura/visualization/optimization.py +0 -1079
- oscura/visualization/palettes.py +0 -446
- oscura/visualization/power.py +0 -508
- oscura/visualization/power_extended.py +0 -955
- oscura/visualization/presets.py +0 -469
- oscura/visualization/protocols.py +0 -1246
- oscura/visualization/render.py +0 -223
- oscura/visualization/rendering.py +0 -444
- oscura/visualization/reverse_engineering.py +0 -838
- oscura/visualization/signal_integrity.py +0 -989
- oscura/visualization/specialized.py +0 -643
- oscura/visualization/spectral.py +0 -1226
- oscura/visualization/thumbnails.py +0 -340
- oscura/visualization/time_axis.py +0 -351
- oscura/visualization/waveform.py +0 -454
- {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/WHEEL +0 -0
- {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/licenses/LICENSE +0 -0
oscura/visualization/power.py
DELETED
|
@@ -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
|