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.
- oscura/__init__.py +19 -19
- oscura/__main__.py +4 -0
- 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/ml/signal_classifier.py +6 -0
- oscura/analyzers/patterns/__init__.py +66 -0
- 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/basic.py +10 -7
- oscura/analyzers/validation.py +1 -1
- oscura/analyzers/waveform/measurements.py +200 -156
- oscura/analyzers/waveform/measurements_with_uncertainty.py +91 -35
- oscura/analyzers/waveform/spectral.py +182 -84
- 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 +17 -102
- oscura/automotive/flexray/fibex.py +9 -1
- 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/measurement_result.py +286 -0
- oscura/core/progress.py +1 -1
- oscura/core/schemas/device_mapping.json +2 -8
- oscura/core/schemas/packet_format.json +4 -24
- oscura/core/schemas/protocol_definition.json +2 -12
- oscura/core/types.py +232 -239
- 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/validation.py +17 -10
- 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/formatting/measurements.py +55 -14
- oscura/reporting/templates/enhanced/protocol_re.html +504 -503
- oscura/sessions/legacy.py +49 -1
- 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 +46 -284
- oscura/visualization/batch.py +72 -433
- oscura/visualization/plot.py +542 -53
- oscura/visualization/styles.py +184 -318
- oscura/workflows/batch/advanced.py +1 -1
- oscura/workflows/batch/aggregate.py +12 -9
- oscura/workflows/complete_re.py +251 -23
- oscura/workflows/digital.py +27 -4
- oscura/workflows/multi_trace.py +136 -17
- oscura/workflows/waveform.py +11 -6
- oscura-0.11.0.dist-info/METADATA +460 -0
- {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/RECORD +120 -145
- 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.8.0.dist-info/METADATA +0 -661
- {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/WHEEL +0 -0
- {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,955 +0,0 @@
|
|
|
1
|
-
"""Extended Power Analysis Visualization Functions.
|
|
2
|
-
|
|
3
|
-
This module provides visualization functions for power conversion analysis
|
|
4
|
-
including efficiency curves, ripple analysis, loss breakdown, and
|
|
5
|
-
multi-channel power waveforms.
|
|
6
|
-
|
|
7
|
-
Example:
|
|
8
|
-
>>> from oscura.visualization.power_extended import (
|
|
9
|
-
... plot_efficiency_curve, plot_ripple_waveform, plot_loss_breakdown
|
|
10
|
-
... )
|
|
11
|
-
>>> fig = plot_efficiency_curve(load_currents, efficiencies)
|
|
12
|
-
>>> fig = plot_ripple_waveform(voltage_trace, ripple_trace)
|
|
13
|
-
|
|
14
|
-
References:
|
|
15
|
-
- Power supply measurement best practices
|
|
16
|
-
- DC-DC converter efficiency testing
|
|
17
|
-
"""
|
|
18
|
-
|
|
19
|
-
from __future__ import annotations
|
|
20
|
-
|
|
21
|
-
from collections.abc import Callable
|
|
22
|
-
from pathlib import Path
|
|
23
|
-
from typing import TYPE_CHECKING, Any, cast
|
|
24
|
-
|
|
25
|
-
import numpy as np
|
|
26
|
-
|
|
27
|
-
try:
|
|
28
|
-
import matplotlib.pyplot as plt
|
|
29
|
-
|
|
30
|
-
HAS_MATPLOTLIB = True
|
|
31
|
-
except ImportError:
|
|
32
|
-
HAS_MATPLOTLIB = False
|
|
33
|
-
|
|
34
|
-
if TYPE_CHECKING:
|
|
35
|
-
from matplotlib.axes import Axes
|
|
36
|
-
from matplotlib.figure import Figure
|
|
37
|
-
from numpy.typing import NDArray
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
__all__ = [
|
|
41
|
-
"plot_efficiency_curve",
|
|
42
|
-
"plot_loss_breakdown",
|
|
43
|
-
"plot_power_waveforms",
|
|
44
|
-
"plot_ripple_waveform",
|
|
45
|
-
]
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def _normalize_efficiency_values(
|
|
49
|
-
efficiency_values: NDArray[np.floating[Any]],
|
|
50
|
-
efficiency_sets: list[NDArray[np.floating[Any]]] | None,
|
|
51
|
-
) -> tuple[NDArray[np.floating[Any]], list[NDArray[np.floating[Any]]] | None]:
|
|
52
|
-
"""Normalize efficiency values to percentage (0-100).
|
|
53
|
-
|
|
54
|
-
Args:
|
|
55
|
-
efficiency_values: Efficiency values (0-100 or 0-1).
|
|
56
|
-
efficiency_sets: List of efficiency arrays or None.
|
|
57
|
-
|
|
58
|
-
Returns:
|
|
59
|
-
Tuple of (normalized_efficiency, normalized_sets).
|
|
60
|
-
"""
|
|
61
|
-
if np.max(efficiency_values) <= 1.0:
|
|
62
|
-
efficiency_values = efficiency_values * 100
|
|
63
|
-
if efficiency_sets is not None:
|
|
64
|
-
efficiency_sets = [e * 100 for e in efficiency_sets]
|
|
65
|
-
|
|
66
|
-
return efficiency_values, efficiency_sets
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def _plot_multi_efficiency_curves(
|
|
70
|
-
ax: Axes,
|
|
71
|
-
load_values: NDArray[np.floating[Any]],
|
|
72
|
-
v_in_values: list[float],
|
|
73
|
-
efficiency_sets: list[NDArray[np.floating[Any]]],
|
|
74
|
-
show_peak: bool,
|
|
75
|
-
) -> None:
|
|
76
|
-
"""Plot multiple efficiency curves for different input voltages.
|
|
77
|
-
|
|
78
|
-
Args:
|
|
79
|
-
ax: Matplotlib axes to plot on.
|
|
80
|
-
load_values: Load current or power array.
|
|
81
|
-
v_in_values: List of input voltages.
|
|
82
|
-
efficiency_sets: List of efficiency arrays.
|
|
83
|
-
show_peak: Show peak efficiency markers.
|
|
84
|
-
"""
|
|
85
|
-
colors = ["#3498DB", "#E74C3C", "#27AE60", "#9B59B6", "#F39C12"]
|
|
86
|
-
|
|
87
|
-
for i, (v_in, eff) in enumerate(zip(v_in_values, efficiency_sets, strict=False)):
|
|
88
|
-
color = colors[i % len(colors)]
|
|
89
|
-
ax.plot(load_values, eff, "-", linewidth=2, color=color, label=f"Vin = {v_in}V")
|
|
90
|
-
|
|
91
|
-
if show_peak:
|
|
92
|
-
peak_idx = np.argmax(eff)
|
|
93
|
-
ax.plot(load_values[peak_idx], eff[peak_idx], "o", color=color, markersize=8)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
def _plot_single_efficiency_curve(
|
|
97
|
-
ax: Axes,
|
|
98
|
-
load_values: NDArray[np.floating[Any]],
|
|
99
|
-
efficiency_values: NDArray[np.floating[Any]],
|
|
100
|
-
load_unit: str,
|
|
101
|
-
show_peak: bool,
|
|
102
|
-
) -> None:
|
|
103
|
-
"""Plot single efficiency curve with peak annotation.
|
|
104
|
-
|
|
105
|
-
Args:
|
|
106
|
-
ax: Matplotlib axes to plot on.
|
|
107
|
-
load_values: Load current or power array.
|
|
108
|
-
efficiency_values: Efficiency values in %.
|
|
109
|
-
load_unit: Load axis unit.
|
|
110
|
-
show_peak: Show peak efficiency annotation.
|
|
111
|
-
"""
|
|
112
|
-
ax.plot(load_values, efficiency_values, "-", linewidth=2.5, color="#3498DB", label="Efficiency")
|
|
113
|
-
|
|
114
|
-
if show_peak:
|
|
115
|
-
peak_idx = np.argmax(efficiency_values)
|
|
116
|
-
peak_load = load_values[peak_idx]
|
|
117
|
-
peak_eff = efficiency_values[peak_idx]
|
|
118
|
-
ax.plot(peak_load, peak_eff, "o", color="#E74C3C", markersize=10, zorder=5)
|
|
119
|
-
ax.annotate(
|
|
120
|
-
f"Peak: {peak_eff:.1f}%\n@ {peak_load:.2f} {load_unit}",
|
|
121
|
-
xy=(peak_load, peak_eff),
|
|
122
|
-
xytext=(15, -15),
|
|
123
|
-
textcoords="offset points",
|
|
124
|
-
fontsize=9,
|
|
125
|
-
ha="left",
|
|
126
|
-
bbox={"boxstyle": "round,pad=0.3", "facecolor": "white", "alpha": 0.9},
|
|
127
|
-
arrowprops={"arrowstyle": "->", "connectionstyle": "arc3,rad=0.2"},
|
|
128
|
-
)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
def _format_efficiency_plot(
|
|
132
|
-
ax: Axes,
|
|
133
|
-
load_values: NDArray[np.floating[Any]],
|
|
134
|
-
efficiency_values: NDArray[np.floating[Any]],
|
|
135
|
-
efficiency_sets: list[NDArray[np.floating[Any]]] | None,
|
|
136
|
-
load_unit: str,
|
|
137
|
-
target_efficiency: float | None,
|
|
138
|
-
title: str | None,
|
|
139
|
-
) -> None:
|
|
140
|
-
"""Format efficiency plot axes and labels.
|
|
141
|
-
|
|
142
|
-
Args:
|
|
143
|
-
ax: Matplotlib axes to format.
|
|
144
|
-
load_values: Load current or power array.
|
|
145
|
-
efficiency_values: Efficiency values in %.
|
|
146
|
-
efficiency_sets: List of efficiency arrays or None.
|
|
147
|
-
load_unit: Load axis unit.
|
|
148
|
-
target_efficiency: Target efficiency line.
|
|
149
|
-
title: Plot title.
|
|
150
|
-
"""
|
|
151
|
-
# Target efficiency line
|
|
152
|
-
if target_efficiency is not None:
|
|
153
|
-
ax.axhline(
|
|
154
|
-
target_efficiency,
|
|
155
|
-
color="#E74C3C",
|
|
156
|
-
linestyle="--",
|
|
157
|
-
linewidth=1.5,
|
|
158
|
-
label=f"Target: {target_efficiency}%",
|
|
159
|
-
)
|
|
160
|
-
|
|
161
|
-
# Fill area under curve
|
|
162
|
-
ax.fill_between(
|
|
163
|
-
load_values,
|
|
164
|
-
0,
|
|
165
|
-
efficiency_values if efficiency_sets is None else efficiency_sets[0],
|
|
166
|
-
alpha=0.1,
|
|
167
|
-
color="#3498DB",
|
|
168
|
-
)
|
|
169
|
-
|
|
170
|
-
# Labels and formatting
|
|
171
|
-
ax.set_xlabel(f"Load ({load_unit})", fontsize=11)
|
|
172
|
-
ax.set_ylabel("Efficiency (%)", fontsize=11)
|
|
173
|
-
ax.set_ylim(0, 100)
|
|
174
|
-
ax.set_xlim(load_values[0], load_values[-1])
|
|
175
|
-
ax.grid(True, alpha=0.3)
|
|
176
|
-
ax.legend(loc="best")
|
|
177
|
-
|
|
178
|
-
if title:
|
|
179
|
-
ax.set_title(title, fontsize=12, fontweight="bold")
|
|
180
|
-
else:
|
|
181
|
-
ax.set_title("Converter Efficiency vs Load", fontsize=12, fontweight="bold")
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
def plot_efficiency_curve(
|
|
185
|
-
load_values: NDArray[np.floating[Any]],
|
|
186
|
-
efficiency_values: NDArray[np.floating[Any]],
|
|
187
|
-
*,
|
|
188
|
-
v_in_values: list[float] | None = None,
|
|
189
|
-
efficiency_sets: list[NDArray[np.floating[Any]]] | None = None,
|
|
190
|
-
ax: Axes | None = None,
|
|
191
|
-
figsize: tuple[float, float] = (10, 6),
|
|
192
|
-
title: str | None = None,
|
|
193
|
-
load_unit: str = "A",
|
|
194
|
-
target_efficiency: float | None = None,
|
|
195
|
-
show_peak: bool = True,
|
|
196
|
-
show: bool = True,
|
|
197
|
-
save_path: str | Path | None = None,
|
|
198
|
-
) -> Figure:
|
|
199
|
-
"""Plot efficiency vs load curve for power converters.
|
|
200
|
-
|
|
201
|
-
Creates an efficiency plot showing converter efficiency as a function
|
|
202
|
-
of load current or power, with optional multiple input voltage curves.
|
|
203
|
-
|
|
204
|
-
Args:
|
|
205
|
-
load_values: Load current or power array.
|
|
206
|
-
efficiency_values: Efficiency values (0-100 or 0-1).
|
|
207
|
-
v_in_values: List of input voltages for multi-curve plot.
|
|
208
|
-
efficiency_sets: List of efficiency arrays for each v_in.
|
|
209
|
-
ax: Matplotlib axes.
|
|
210
|
-
figsize: Figure size.
|
|
211
|
-
title: Plot title.
|
|
212
|
-
load_unit: Load axis unit ("A", "W", "%").
|
|
213
|
-
target_efficiency: Target efficiency line.
|
|
214
|
-
show_peak: Annotate peak efficiency point.
|
|
215
|
-
show: Display plot.
|
|
216
|
-
save_path: Save path.
|
|
217
|
-
|
|
218
|
-
Returns:
|
|
219
|
-
Matplotlib Figure object.
|
|
220
|
-
|
|
221
|
-
Example:
|
|
222
|
-
>>> load = np.linspace(0.1, 5, 50) # 0.1A to 5A
|
|
223
|
-
>>> eff = 90 - 5 * np.exp(-load) # Example efficiency curve
|
|
224
|
-
>>> fig = plot_efficiency_curve(load, eff, target_efficiency=85)
|
|
225
|
-
"""
|
|
226
|
-
if not HAS_MATPLOTLIB:
|
|
227
|
-
raise ImportError("matplotlib is required for visualization")
|
|
228
|
-
|
|
229
|
-
# Figure/axes creation
|
|
230
|
-
if ax is None:
|
|
231
|
-
fig, ax = plt.subplots(figsize=figsize)
|
|
232
|
-
else:
|
|
233
|
-
fig_temp = ax.get_figure()
|
|
234
|
-
if fig_temp is None:
|
|
235
|
-
raise ValueError("Axes must have an associated figure")
|
|
236
|
-
fig = cast("Figure", fig_temp)
|
|
237
|
-
|
|
238
|
-
# Data preparation/validation
|
|
239
|
-
efficiency_values, efficiency_sets = _normalize_efficiency_values(
|
|
240
|
-
efficiency_values, efficiency_sets
|
|
241
|
-
)
|
|
242
|
-
|
|
243
|
-
# Plotting/rendering
|
|
244
|
-
if v_in_values is not None and efficiency_sets is not None:
|
|
245
|
-
_plot_multi_efficiency_curves(ax, load_values, v_in_values, efficiency_sets, show_peak)
|
|
246
|
-
else:
|
|
247
|
-
_plot_single_efficiency_curve(ax, load_values, efficiency_values, load_unit, show_peak)
|
|
248
|
-
|
|
249
|
-
# Annotation/labeling and layout/formatting
|
|
250
|
-
_format_efficiency_plot(
|
|
251
|
-
ax, load_values, efficiency_values, efficiency_sets, load_unit, target_efficiency, title
|
|
252
|
-
)
|
|
253
|
-
|
|
254
|
-
fig.tight_layout()
|
|
255
|
-
|
|
256
|
-
if save_path is not None:
|
|
257
|
-
fig.savefig(save_path, dpi=300, bbox_inches="tight")
|
|
258
|
-
|
|
259
|
-
if show:
|
|
260
|
-
plt.show()
|
|
261
|
-
|
|
262
|
-
return fig
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
def _determine_time_scale(time: NDArray[np.floating[Any]], time_unit: str) -> tuple[str, float]:
|
|
266
|
-
"""Determine time axis scale and multiplier.
|
|
267
|
-
|
|
268
|
-
Args:
|
|
269
|
-
time: Time array in seconds.
|
|
270
|
-
time_unit: Requested time unit ("auto" or specific).
|
|
271
|
-
|
|
272
|
-
Returns:
|
|
273
|
-
Tuple of (unit_name, multiplier).
|
|
274
|
-
"""
|
|
275
|
-
if time_unit != "auto":
|
|
276
|
-
time_mult = {"s": 1, "ms": 1e3, "us": 1e6, "ns": 1e9}.get(time_unit, 1.0)
|
|
277
|
-
return time_unit, time_mult
|
|
278
|
-
|
|
279
|
-
max_time = np.max(time)
|
|
280
|
-
if max_time < 1e-6:
|
|
281
|
-
return "us", 1e6
|
|
282
|
-
elif max_time < 1e-3:
|
|
283
|
-
return "ms", 1e3
|
|
284
|
-
else:
|
|
285
|
-
return "s", 1.0
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
def _plot_voltage_current_panel(
|
|
289
|
-
ax: Axes,
|
|
290
|
-
time_scaled: NDArray[np.floating[Any]],
|
|
291
|
-
voltage: NDArray[np.floating[Any]],
|
|
292
|
-
current: NDArray[np.floating[Any]] | None,
|
|
293
|
-
v_label: str,
|
|
294
|
-
i_label: str,
|
|
295
|
-
v_color: str,
|
|
296
|
-
i_color: str,
|
|
297
|
-
panel_title: str,
|
|
298
|
-
) -> None:
|
|
299
|
-
"""Plot voltage and current on dual-axis panel.
|
|
300
|
-
|
|
301
|
-
Args:
|
|
302
|
-
ax: Matplotlib axes for voltage.
|
|
303
|
-
time_scaled: Scaled time array.
|
|
304
|
-
voltage: Voltage waveform.
|
|
305
|
-
current: Current waveform (optional).
|
|
306
|
-
v_label: Voltage axis label.
|
|
307
|
-
i_label: Current axis label.
|
|
308
|
-
v_color: Voltage plot color.
|
|
309
|
-
i_color: Current plot color.
|
|
310
|
-
panel_title: Panel title.
|
|
311
|
-
"""
|
|
312
|
-
ax.plot(time_scaled, voltage, v_color, linewidth=1.5)
|
|
313
|
-
ax.set_ylabel(v_label, color=v_color, fontsize=10)
|
|
314
|
-
ax.tick_params(axis="y", labelcolor=v_color)
|
|
315
|
-
ax.grid(True, alpha=0.3)
|
|
316
|
-
|
|
317
|
-
if current is not None:
|
|
318
|
-
ax2 = ax.twinx()
|
|
319
|
-
ax2.plot(time_scaled, current, i_color, linewidth=1.5)
|
|
320
|
-
ax2.set_ylabel(i_label, color=i_color, fontsize=10)
|
|
321
|
-
ax2.tick_params(axis="y", labelcolor=i_color)
|
|
322
|
-
|
|
323
|
-
ax.set_title(panel_title, fontsize=10, fontweight="bold", loc="left")
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
def _plot_power_panel(
|
|
327
|
-
ax: Axes,
|
|
328
|
-
time_scaled: NDArray[np.floating[Any]],
|
|
329
|
-
v_in: NDArray[np.floating[Any]] | None,
|
|
330
|
-
i_in: NDArray[np.floating[Any]] | None,
|
|
331
|
-
v_out: NDArray[np.floating[Any]] | None,
|
|
332
|
-
i_out: NDArray[np.floating[Any]] | None,
|
|
333
|
-
) -> None:
|
|
334
|
-
"""Plot instantaneous power panel.
|
|
335
|
-
|
|
336
|
-
Args:
|
|
337
|
-
ax: Matplotlib axes.
|
|
338
|
-
time_scaled: Scaled time array.
|
|
339
|
-
v_in: Input voltage (optional).
|
|
340
|
-
i_in: Input current (optional).
|
|
341
|
-
v_out: Output voltage (optional).
|
|
342
|
-
i_out: Output current (optional).
|
|
343
|
-
"""
|
|
344
|
-
if v_in is not None and i_in is not None:
|
|
345
|
-
p_in = v_in * i_in
|
|
346
|
-
ax.plot(
|
|
347
|
-
time_scaled,
|
|
348
|
-
p_in,
|
|
349
|
-
"#3498DB",
|
|
350
|
-
linewidth=1.5,
|
|
351
|
-
label=f"P_in (avg: {np.mean(p_in):.2f}W)",
|
|
352
|
-
)
|
|
353
|
-
|
|
354
|
-
if v_out is not None and i_out is not None:
|
|
355
|
-
p_out = v_out * i_out
|
|
356
|
-
ax.plot(
|
|
357
|
-
time_scaled,
|
|
358
|
-
p_out,
|
|
359
|
-
"#27AE60",
|
|
360
|
-
linewidth=1.5,
|
|
361
|
-
label=f"P_out (avg: {np.mean(p_out):.2f}W)",
|
|
362
|
-
)
|
|
363
|
-
|
|
364
|
-
ax.set_ylabel("Power (W)", fontsize=10)
|
|
365
|
-
ax.set_title("Instantaneous Power", fontsize=10, fontweight="bold", loc="left")
|
|
366
|
-
ax.legend(loc="upper right", fontsize=9)
|
|
367
|
-
ax.grid(True, alpha=0.3)
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
def _setup_power_waveform_figure(
|
|
371
|
-
figsize: tuple[float, float],
|
|
372
|
-
v_in: NDArray[np.floating[Any]] | None,
|
|
373
|
-
v_out: NDArray[np.floating[Any]] | None,
|
|
374
|
-
show_power: bool,
|
|
375
|
-
) -> tuple[Figure, list[Axes]]:
|
|
376
|
-
"""Setup figure and axes for power waveform plot.
|
|
377
|
-
|
|
378
|
-
Args:
|
|
379
|
-
figsize: Figure size.
|
|
380
|
-
v_in: Input voltage (optional).
|
|
381
|
-
v_out: Output voltage (optional).
|
|
382
|
-
show_power: Show power panel.
|
|
383
|
-
|
|
384
|
-
Returns:
|
|
385
|
-
Tuple of (figure, axes_list).
|
|
386
|
-
"""
|
|
387
|
-
n_plots = sum(
|
|
388
|
-
[
|
|
389
|
-
v_in is not None,
|
|
390
|
-
v_out is not None,
|
|
391
|
-
show_power and (v_in is not None or v_out is not None),
|
|
392
|
-
]
|
|
393
|
-
)
|
|
394
|
-
if n_plots == 0:
|
|
395
|
-
raise ValueError("At least one voltage waveform must be provided")
|
|
396
|
-
|
|
397
|
-
fig, axes = plt.subplots(n_plots, 1, figsize=figsize, sharex=True)
|
|
398
|
-
if n_plots == 1:
|
|
399
|
-
axes = [axes]
|
|
400
|
-
|
|
401
|
-
return fig, axes
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
def _plot_power_waveform_panels(
|
|
405
|
-
axes: list[Axes],
|
|
406
|
-
time_scaled: NDArray[np.floating[Any]],
|
|
407
|
-
v_in: NDArray[np.floating[Any]] | None,
|
|
408
|
-
i_in: NDArray[np.floating[Any]] | None,
|
|
409
|
-
v_out: NDArray[np.floating[Any]] | None,
|
|
410
|
-
i_out: NDArray[np.floating[Any]] | None,
|
|
411
|
-
show_power: bool,
|
|
412
|
-
) -> None:
|
|
413
|
-
"""Plot all voltage/current panels.
|
|
414
|
-
|
|
415
|
-
Args:
|
|
416
|
-
axes: List of axes to plot on.
|
|
417
|
-
time_scaled: Scaled time array.
|
|
418
|
-
v_in: Input voltage (optional).
|
|
419
|
-
i_in: Input current (optional).
|
|
420
|
-
v_out: Output voltage (optional).
|
|
421
|
-
i_out: Output current (optional).
|
|
422
|
-
show_power: Show power panel.
|
|
423
|
-
"""
|
|
424
|
-
ax_idx = 0
|
|
425
|
-
|
|
426
|
-
if v_in is not None:
|
|
427
|
-
_plot_voltage_current_panel(
|
|
428
|
-
axes[ax_idx],
|
|
429
|
-
time_scaled,
|
|
430
|
-
v_in,
|
|
431
|
-
i_in,
|
|
432
|
-
"V_in (V)",
|
|
433
|
-
"I_in (A)",
|
|
434
|
-
"#3498DB",
|
|
435
|
-
"#E74C3C",
|
|
436
|
-
"Input",
|
|
437
|
-
)
|
|
438
|
-
ax_idx += 1
|
|
439
|
-
|
|
440
|
-
if v_out is not None:
|
|
441
|
-
_plot_voltage_current_panel(
|
|
442
|
-
axes[ax_idx],
|
|
443
|
-
time_scaled,
|
|
444
|
-
v_out,
|
|
445
|
-
i_out,
|
|
446
|
-
"V_out (V)",
|
|
447
|
-
"I_out (A)",
|
|
448
|
-
"#27AE60",
|
|
449
|
-
"#9B59B6",
|
|
450
|
-
"Output",
|
|
451
|
-
)
|
|
452
|
-
ax_idx += 1
|
|
453
|
-
|
|
454
|
-
if show_power:
|
|
455
|
-
_plot_power_panel(axes[ax_idx], time_scaled, v_in, i_in, v_out, i_out)
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
def _finalize_power_waveform_plot(
|
|
459
|
-
fig: Figure,
|
|
460
|
-
axes: list[Axes],
|
|
461
|
-
time_unit: str,
|
|
462
|
-
title: str | None,
|
|
463
|
-
save_path: str | Path | None,
|
|
464
|
-
show: bool,
|
|
465
|
-
) -> None:
|
|
466
|
-
"""Finalize power waveform plot formatting and save.
|
|
467
|
-
|
|
468
|
-
Args:
|
|
469
|
-
fig: Matplotlib figure.
|
|
470
|
-
axes: List of axes.
|
|
471
|
-
time_unit: Time axis unit.
|
|
472
|
-
title: Plot title.
|
|
473
|
-
save_path: Save path.
|
|
474
|
-
show: Display plot.
|
|
475
|
-
"""
|
|
476
|
-
axes[-1].set_xlabel(f"Time ({time_unit})", fontsize=11)
|
|
477
|
-
fig.suptitle(title if title else "Power Converter Waveforms", fontsize=14, fontweight="bold")
|
|
478
|
-
fig.tight_layout()
|
|
479
|
-
|
|
480
|
-
if save_path is not None:
|
|
481
|
-
fig.savefig(save_path, dpi=300, bbox_inches="tight")
|
|
482
|
-
|
|
483
|
-
if show:
|
|
484
|
-
plt.show()
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
def plot_power_waveforms(
|
|
488
|
-
time: NDArray[np.floating[Any]],
|
|
489
|
-
*,
|
|
490
|
-
v_in: NDArray[np.floating[Any]] | None = None,
|
|
491
|
-
i_in: NDArray[np.floating[Any]] | None = None,
|
|
492
|
-
v_out: NDArray[np.floating[Any]] | None = None,
|
|
493
|
-
i_out: NDArray[np.floating[Any]] | None = None,
|
|
494
|
-
figsize: tuple[float, float] = (12, 10),
|
|
495
|
-
title: str | None = None,
|
|
496
|
-
time_unit: str = "auto",
|
|
497
|
-
show_power: bool = True,
|
|
498
|
-
show: bool = True,
|
|
499
|
-
save_path: str | Path | None = None,
|
|
500
|
-
) -> Figure:
|
|
501
|
-
"""Plot multi-channel power waveforms with optional power calculation.
|
|
502
|
-
|
|
503
|
-
Creates a multi-panel plot showing input/output voltage and current
|
|
504
|
-
waveforms with optional instantaneous power overlay.
|
|
505
|
-
|
|
506
|
-
Args:
|
|
507
|
-
time: Time array in seconds.
|
|
508
|
-
v_in: Input voltage waveform.
|
|
509
|
-
i_in: Input current waveform.
|
|
510
|
-
v_out: Output voltage waveform.
|
|
511
|
-
i_out: Output current waveform.
|
|
512
|
-
figsize: Figure size.
|
|
513
|
-
title: Plot title.
|
|
514
|
-
time_unit: Time axis unit.
|
|
515
|
-
show_power: Calculate and show instantaneous power.
|
|
516
|
-
show: Display plot.
|
|
517
|
-
save_path: Save path.
|
|
518
|
-
|
|
519
|
-
Returns:
|
|
520
|
-
Matplotlib Figure object.
|
|
521
|
-
"""
|
|
522
|
-
if not HAS_MATPLOTLIB:
|
|
523
|
-
raise ImportError("matplotlib is required for visualization")
|
|
524
|
-
|
|
525
|
-
# Setup: determine layout and prepare axes
|
|
526
|
-
fig, axes = _setup_power_waveform_figure(figsize, v_in, v_out, show_power)
|
|
527
|
-
time_unit, time_mult = _determine_time_scale(time, time_unit)
|
|
528
|
-
time_scaled = time * time_mult
|
|
529
|
-
|
|
530
|
-
# Processing: plot data panels
|
|
531
|
-
_plot_power_waveform_panels(axes, time_scaled, v_in, i_in, v_out, i_out, show_power)
|
|
532
|
-
|
|
533
|
-
# Formatting: finalize and save
|
|
534
|
-
_finalize_power_waveform_plot(fig, axes, time_unit, title, save_path, show)
|
|
535
|
-
|
|
536
|
-
return fig
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
def _determine_time_unit_and_multiplier(
|
|
540
|
-
time: NDArray[np.floating[Any]], time_unit: str
|
|
541
|
-
) -> tuple[str, float]:
|
|
542
|
-
"""Determine time unit and multiplier for time axis scaling.
|
|
543
|
-
|
|
544
|
-
Args:
|
|
545
|
-
time: Time array in seconds.
|
|
546
|
-
time_unit: Requested time unit ("auto" or specific unit).
|
|
547
|
-
|
|
548
|
-
Returns:
|
|
549
|
-
Tuple of (time_unit, time_multiplier).
|
|
550
|
-
"""
|
|
551
|
-
if time_unit == "auto":
|
|
552
|
-
max_time = np.max(time)
|
|
553
|
-
if max_time < 1e-6:
|
|
554
|
-
return "us", 1e6
|
|
555
|
-
elif max_time < 1e-3:
|
|
556
|
-
return "ms", 1e3
|
|
557
|
-
else:
|
|
558
|
-
return "s", 1.0
|
|
559
|
-
else:
|
|
560
|
-
time_mult = {"s": 1, "ms": 1e3, "us": 1e6, "ns": 1e9}.get(time_unit, 1.0)
|
|
561
|
-
return time_unit, time_mult
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
def _calculate_ripple_metrics(
|
|
565
|
-
voltage: NDArray[np.floating[Any]],
|
|
566
|
-
) -> tuple[float, NDArray[np.floating[Any]], float, float]:
|
|
567
|
-
"""Calculate DC level and AC ripple metrics.
|
|
568
|
-
|
|
569
|
-
Args:
|
|
570
|
-
voltage: Voltage waveform array.
|
|
571
|
-
|
|
572
|
-
Returns:
|
|
573
|
-
Tuple of (dc_level, ac_ripple, ripple_pp, ripple_rms).
|
|
574
|
-
"""
|
|
575
|
-
dc_level = float(np.mean(voltage))
|
|
576
|
-
ac_ripple = voltage - dc_level
|
|
577
|
-
ripple_pp = float(np.ptp(ac_ripple))
|
|
578
|
-
ripple_rms = float(np.std(ac_ripple))
|
|
579
|
-
return dc_level, ac_ripple, ripple_pp, ripple_rms
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
def _plot_dc_coupled_waveform(
|
|
583
|
-
ax: Axes,
|
|
584
|
-
time_scaled: NDArray[np.floating[Any]],
|
|
585
|
-
voltage: NDArray[np.floating[Any]],
|
|
586
|
-
dc_level: float,
|
|
587
|
-
) -> None:
|
|
588
|
-
"""Plot DC-coupled waveform with DC level indicator.
|
|
589
|
-
|
|
590
|
-
Args:
|
|
591
|
-
ax: Matplotlib axes to plot on.
|
|
592
|
-
time_scaled: Scaled time array.
|
|
593
|
-
voltage: Voltage waveform.
|
|
594
|
-
dc_level: DC level value.
|
|
595
|
-
"""
|
|
596
|
-
ax.plot(time_scaled, voltage, "#3498DB", linewidth=1)
|
|
597
|
-
ax.axhline(
|
|
598
|
-
dc_level, color="#E74C3C", linestyle="--", linewidth=1.5, label=f"DC: {dc_level:.3f}V"
|
|
599
|
-
)
|
|
600
|
-
ax.set_ylabel("Voltage (V)", fontsize=10)
|
|
601
|
-
ax.set_title("DC-Coupled Waveform", fontsize=10, fontweight="bold", loc="left")
|
|
602
|
-
ax.legend(loc="upper right", fontsize=9)
|
|
603
|
-
ax.grid(True, alpha=0.3)
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
def _plot_ac_ripple_waveform(
|
|
607
|
-
ax: Axes,
|
|
608
|
-
time_scaled: NDArray[np.floating[Any]],
|
|
609
|
-
ac_ripple: NDArray[np.floating[Any]],
|
|
610
|
-
ripple_pp: float,
|
|
611
|
-
ripple_rms: float,
|
|
612
|
-
) -> None:
|
|
613
|
-
"""Plot AC-coupled ripple waveform with peak-to-peak annotation.
|
|
614
|
-
|
|
615
|
-
Args:
|
|
616
|
-
ax: Matplotlib axes to plot on.
|
|
617
|
-
time_scaled: Scaled time array.
|
|
618
|
-
ac_ripple: AC ripple waveform.
|
|
619
|
-
ripple_pp: Peak-to-peak ripple voltage.
|
|
620
|
-
ripple_rms: RMS ripple voltage.
|
|
621
|
-
"""
|
|
622
|
-
ax.plot(time_scaled, ac_ripple * 1e3, "#27AE60", linewidth=1) # Convert to mV
|
|
623
|
-
ax.axhline(0, color="gray", linestyle="-", linewidth=0.5)
|
|
624
|
-
|
|
625
|
-
# Mark peak-to-peak
|
|
626
|
-
max_idx = int(np.argmax(ac_ripple))
|
|
627
|
-
min_idx = int(np.argmin(ac_ripple))
|
|
628
|
-
ax.annotate(
|
|
629
|
-
"",
|
|
630
|
-
xy=(time_scaled[max_idx], ac_ripple[max_idx] * 1e3),
|
|
631
|
-
xytext=(time_scaled[min_idx], ac_ripple[min_idx] * 1e3),
|
|
632
|
-
arrowprops={"arrowstyle": "<->", "color": "#E74C3C", "lw": 1.5},
|
|
633
|
-
)
|
|
634
|
-
|
|
635
|
-
ax.set_ylabel("Ripple (mV)", fontsize=10)
|
|
636
|
-
ax.set_title(
|
|
637
|
-
f"AC Ripple (pk-pk: {ripple_pp * 1e3:.2f}mV, RMS: {ripple_rms * 1e3:.2f}mV)",
|
|
638
|
-
fontsize=10,
|
|
639
|
-
fontweight="bold",
|
|
640
|
-
loc="left",
|
|
641
|
-
)
|
|
642
|
-
ax.grid(True, alpha=0.3)
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
def _plot_ripple_spectrum(
|
|
646
|
-
ax: Axes,
|
|
647
|
-
ac_ripple: NDArray[np.floating[Any]],
|
|
648
|
-
sample_rate: float,
|
|
649
|
-
) -> None:
|
|
650
|
-
"""Plot ripple frequency spectrum.
|
|
651
|
-
|
|
652
|
-
Args:
|
|
653
|
-
ax: Matplotlib axes to plot on.
|
|
654
|
-
ac_ripple: AC ripple waveform.
|
|
655
|
-
sample_rate: Sample rate in Hz.
|
|
656
|
-
"""
|
|
657
|
-
n_fft = len(ac_ripple)
|
|
658
|
-
freq = np.fft.rfftfreq(n_fft, 1 / sample_rate)
|
|
659
|
-
fft_mag = np.abs(np.fft.rfft(ac_ripple)) / n_fft * 2
|
|
660
|
-
fft_db = 20 * np.log10(fft_mag + 1e-12)
|
|
661
|
-
|
|
662
|
-
# Find dominant ripple frequency
|
|
663
|
-
peak_idx = int(np.argmax(fft_mag[1:])) + 1 # Skip DC
|
|
664
|
-
peak_freq = freq[peak_idx]
|
|
665
|
-
|
|
666
|
-
# Plot in kHz
|
|
667
|
-
freq_khz = freq / 1e3
|
|
668
|
-
ax.plot(freq_khz, fft_db, "#9B59B6", linewidth=1)
|
|
669
|
-
ax.plot(
|
|
670
|
-
freq_khz[peak_idx],
|
|
671
|
-
fft_db[peak_idx],
|
|
672
|
-
"ro",
|
|
673
|
-
markersize=8,
|
|
674
|
-
label=f"Peak: {peak_freq / 1e3:.1f}kHz",
|
|
675
|
-
)
|
|
676
|
-
|
|
677
|
-
ax.set_ylabel("Magnitude (dB)", fontsize=10)
|
|
678
|
-
ax.set_xlabel("Frequency (kHz)", fontsize=10)
|
|
679
|
-
ax.set_title("Ripple Spectrum", fontsize=10, fontweight="bold", loc="left")
|
|
680
|
-
ax.set_xlim(0, min(freq_khz[-1], sample_rate / 2e3))
|
|
681
|
-
ax.legend(loc="upper right", fontsize=9)
|
|
682
|
-
ax.grid(True, alpha=0.3)
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
def _estimate_sample_rate(time: NDArray[np.floating[Any]]) -> float:
|
|
686
|
-
"""Estimate sample rate from time array.
|
|
687
|
-
|
|
688
|
-
Args:
|
|
689
|
-
time: Time array in seconds.
|
|
690
|
-
|
|
691
|
-
Returns:
|
|
692
|
-
Estimated sample rate in Hz.
|
|
693
|
-
"""
|
|
694
|
-
if len(time) > 1:
|
|
695
|
-
return float(1 / (time[1] - time[0]))
|
|
696
|
-
return 1e6 # Default 1 MHz
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
def plot_ripple_waveform(
|
|
700
|
-
time: NDArray[np.floating[Any]],
|
|
701
|
-
voltage: NDArray[np.floating[Any]],
|
|
702
|
-
*,
|
|
703
|
-
ax: Axes | None = None,
|
|
704
|
-
figsize: tuple[float, float] = (12, 8),
|
|
705
|
-
title: str | None = None,
|
|
706
|
-
time_unit: str = "auto",
|
|
707
|
-
show_dc: bool = True,
|
|
708
|
-
show_ac: bool = True,
|
|
709
|
-
show_spectrum: bool = True,
|
|
710
|
-
sample_rate: float | None = None,
|
|
711
|
-
show: bool = True,
|
|
712
|
-
save_path: str | Path | None = None,
|
|
713
|
-
) -> Figure:
|
|
714
|
-
"""Plot ripple waveform with DC, AC, and spectral analysis.
|
|
715
|
-
|
|
716
|
-
Creates a multi-panel view showing DC-coupled waveform, AC-coupled
|
|
717
|
-
ripple, and optionally the ripple frequency spectrum.
|
|
718
|
-
|
|
719
|
-
Args:
|
|
720
|
-
time: Time array in seconds.
|
|
721
|
-
voltage: Voltage waveform.
|
|
722
|
-
ax: Matplotlib axes (creates multi-panel if None).
|
|
723
|
-
figsize: Figure size.
|
|
724
|
-
title: Plot title.
|
|
725
|
-
time_unit: Time axis unit ("auto", "s", "ms", "us", "ns").
|
|
726
|
-
show_dc: Show DC-coupled waveform.
|
|
727
|
-
show_ac: Show AC-coupled ripple.
|
|
728
|
-
show_spectrum: Show ripple spectrum.
|
|
729
|
-
sample_rate: Sample rate for FFT (estimated if None).
|
|
730
|
-
show: Display plot.
|
|
731
|
-
save_path: Save path.
|
|
732
|
-
|
|
733
|
-
Returns:
|
|
734
|
-
Matplotlib Figure object.
|
|
735
|
-
|
|
736
|
-
Example:
|
|
737
|
-
>>> time = np.linspace(0, 1e-3, 1000) # 1ms capture
|
|
738
|
-
>>> voltage = 5.0 + 0.01 * np.sin(2 * np.pi * 100e3 * time) # 5V + 10mV ripple
|
|
739
|
-
>>> fig = plot_ripple_waveform(time, voltage, show_spectrum=True)
|
|
740
|
-
"""
|
|
741
|
-
if not HAS_MATPLOTLIB:
|
|
742
|
-
raise ImportError("matplotlib is required for visualization")
|
|
743
|
-
|
|
744
|
-
n_plots = sum([show_dc, show_ac, show_spectrum])
|
|
745
|
-
if n_plots == 0:
|
|
746
|
-
raise ValueError("At least one display option must be True")
|
|
747
|
-
|
|
748
|
-
fig, axes = plt.subplots(n_plots, 1, figsize=figsize)
|
|
749
|
-
if n_plots == 1:
|
|
750
|
-
axes = [axes]
|
|
751
|
-
|
|
752
|
-
# Determine time scaling
|
|
753
|
-
time_unit, time_mult = _determine_time_unit_and_multiplier(time, time_unit)
|
|
754
|
-
time_scaled = time * time_mult
|
|
755
|
-
|
|
756
|
-
# Calculate ripple metrics
|
|
757
|
-
dc_level, ac_ripple, ripple_pp, ripple_rms = _calculate_ripple_metrics(voltage)
|
|
758
|
-
|
|
759
|
-
ax_idx = 0
|
|
760
|
-
|
|
761
|
-
# Plot DC-coupled waveform
|
|
762
|
-
if show_dc:
|
|
763
|
-
_plot_dc_coupled_waveform(axes[ax_idx], time_scaled, voltage, dc_level)
|
|
764
|
-
ax_idx += 1
|
|
765
|
-
|
|
766
|
-
# Plot AC-coupled ripple
|
|
767
|
-
if show_ac:
|
|
768
|
-
_plot_ac_ripple_waveform(axes[ax_idx], time_scaled, ac_ripple, ripple_pp, ripple_rms)
|
|
769
|
-
ax_idx += 1
|
|
770
|
-
|
|
771
|
-
# Plot ripple spectrum
|
|
772
|
-
if show_spectrum:
|
|
773
|
-
sr = sample_rate if sample_rate is not None else _estimate_sample_rate(time)
|
|
774
|
-
_plot_ripple_spectrum(axes[ax_idx], ac_ripple, sr)
|
|
775
|
-
else:
|
|
776
|
-
axes[-1].set_xlabel(f"Time ({time_unit})", fontsize=11)
|
|
777
|
-
|
|
778
|
-
# Finalize figure
|
|
779
|
-
if title:
|
|
780
|
-
fig.suptitle(title, fontsize=14, fontweight="bold")
|
|
781
|
-
else:
|
|
782
|
-
fig.suptitle("Ripple Analysis", fontsize=14, fontweight="bold")
|
|
783
|
-
|
|
784
|
-
fig.tight_layout()
|
|
785
|
-
|
|
786
|
-
if save_path is not None:
|
|
787
|
-
fig.savefig(save_path, dpi=300, bbox_inches="tight")
|
|
788
|
-
|
|
789
|
-
if show:
|
|
790
|
-
plt.show()
|
|
791
|
-
|
|
792
|
-
return fig
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
def _create_loss_autopct_formatter(
|
|
796
|
-
show_watts: bool, total_loss: float
|
|
797
|
-
) -> str | Callable[[float], str]:
|
|
798
|
-
"""Create autopct formatter for pie chart labels.
|
|
799
|
-
|
|
800
|
-
Args:
|
|
801
|
-
show_watts: Whether to show watt values.
|
|
802
|
-
total_loss: Total loss in watts.
|
|
803
|
-
|
|
804
|
-
Returns:
|
|
805
|
-
Format string or callable for autopct.
|
|
806
|
-
"""
|
|
807
|
-
if show_watts:
|
|
808
|
-
|
|
809
|
-
def autopct_func(pct: float) -> str:
|
|
810
|
-
watts = pct / 100 * total_loss
|
|
811
|
-
return f"{pct:.1f}%\n({watts * 1e3:.1f}mW)"
|
|
812
|
-
|
|
813
|
-
return autopct_func
|
|
814
|
-
return "%1.1f%%"
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
def _create_loss_pie_chart(
|
|
818
|
-
ax: Axes,
|
|
819
|
-
labels: list[str],
|
|
820
|
-
values: list[float],
|
|
821
|
-
colors: list[str],
|
|
822
|
-
autopct_val: str | Callable[[float], str],
|
|
823
|
-
) -> tuple[Any, ...]:
|
|
824
|
-
"""Create pie chart with loss breakdown.
|
|
825
|
-
|
|
826
|
-
Args:
|
|
827
|
-
ax: Matplotlib axes.
|
|
828
|
-
labels: Loss type labels.
|
|
829
|
-
values: Loss values.
|
|
830
|
-
colors: Color palette.
|
|
831
|
-
autopct_val: Autopct formatter.
|
|
832
|
-
|
|
833
|
-
Returns:
|
|
834
|
-
Pie chart result tuple.
|
|
835
|
-
"""
|
|
836
|
-
return ax.pie(
|
|
837
|
-
values,
|
|
838
|
-
labels=labels,
|
|
839
|
-
autopct=autopct_val,
|
|
840
|
-
colors=colors[: len(labels)],
|
|
841
|
-
startangle=90,
|
|
842
|
-
explode=[0.02] * len(labels),
|
|
843
|
-
shadow=True,
|
|
844
|
-
)
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
def _format_loss_pie_chart(
|
|
848
|
-
ax: Axes, pie_result: tuple[Any, ...], total_loss: float, title: str | None
|
|
849
|
-
) -> None:
|
|
850
|
-
"""Format pie chart styling and annotations.
|
|
851
|
-
|
|
852
|
-
Args:
|
|
853
|
-
ax: Matplotlib axes.
|
|
854
|
-
pie_result: Result from ax.pie.
|
|
855
|
-
total_loss: Total loss value.
|
|
856
|
-
title: Chart title.
|
|
857
|
-
"""
|
|
858
|
-
# Style autotexts if available
|
|
859
|
-
if len(pie_result) >= 3:
|
|
860
|
-
autotexts = pie_result[2]
|
|
861
|
-
for autotext in autotexts:
|
|
862
|
-
autotext.set_fontsize(9)
|
|
863
|
-
autotext.set_fontweight("bold")
|
|
864
|
-
|
|
865
|
-
# Add total loss annotation
|
|
866
|
-
ax.text(
|
|
867
|
-
0,
|
|
868
|
-
-1.3,
|
|
869
|
-
f"Total Loss: {total_loss * 1e3:.1f}mW ({total_loss:.3f}W)",
|
|
870
|
-
ha="center",
|
|
871
|
-
fontsize=11,
|
|
872
|
-
fontweight="bold",
|
|
873
|
-
)
|
|
874
|
-
|
|
875
|
-
ax.set_aspect("equal")
|
|
876
|
-
ax.set_title(title if title else "Power Loss Breakdown", fontsize=12, fontweight="bold", pad=20)
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
def plot_loss_breakdown(
|
|
880
|
-
loss_values: dict[str, float],
|
|
881
|
-
*,
|
|
882
|
-
ax: Axes | None = None,
|
|
883
|
-
figsize: tuple[float, float] = (10, 8),
|
|
884
|
-
title: str | None = None,
|
|
885
|
-
show_watts: bool = True,
|
|
886
|
-
show: bool = True,
|
|
887
|
-
save_path: str | Path | None = None,
|
|
888
|
-
) -> Figure:
|
|
889
|
-
"""Plot power loss breakdown as pie chart.
|
|
890
|
-
|
|
891
|
-
Creates a pie chart showing the contribution of each loss mechanism
|
|
892
|
-
(switching, conduction, magnetic, etc.) to total power dissipation.
|
|
893
|
-
|
|
894
|
-
Args:
|
|
895
|
-
loss_values: Dictionary mapping loss type to value in Watts.
|
|
896
|
-
ax: Matplotlib axes.
|
|
897
|
-
figsize: Figure size.
|
|
898
|
-
title: Plot title.
|
|
899
|
-
show_watts: Show watt values on slices.
|
|
900
|
-
show: Display plot.
|
|
901
|
-
save_path: Save path.
|
|
902
|
-
|
|
903
|
-
Returns:
|
|
904
|
-
Matplotlib Figure object.
|
|
905
|
-
|
|
906
|
-
Example:
|
|
907
|
-
>>> losses = {
|
|
908
|
-
... "Switching": 0.5,
|
|
909
|
-
... "Conduction": 0.3,
|
|
910
|
-
... "Magnetic": 0.15,
|
|
911
|
-
... "Gate Drive": 0.05
|
|
912
|
-
... }
|
|
913
|
-
>>> fig = plot_loss_breakdown(losses)
|
|
914
|
-
"""
|
|
915
|
-
if not HAS_MATPLOTLIB:
|
|
916
|
-
raise ImportError("matplotlib is required for visualization")
|
|
917
|
-
|
|
918
|
-
# Setup: create figure and extract data
|
|
919
|
-
if ax is None:
|
|
920
|
-
fig, ax = plt.subplots(figsize=figsize)
|
|
921
|
-
else:
|
|
922
|
-
fig_temp = ax.get_figure()
|
|
923
|
-
if fig_temp is None:
|
|
924
|
-
raise ValueError("Axes must have an associated figure")
|
|
925
|
-
fig = cast("Figure", fig_temp)
|
|
926
|
-
|
|
927
|
-
labels = list(loss_values.keys())
|
|
928
|
-
values = list(loss_values.values())
|
|
929
|
-
total_loss = sum(values)
|
|
930
|
-
colors = [
|
|
931
|
-
"#3498DB",
|
|
932
|
-
"#E74C3C",
|
|
933
|
-
"#27AE60",
|
|
934
|
-
"#9B59B6",
|
|
935
|
-
"#F39C12",
|
|
936
|
-
"#1ABC9C",
|
|
937
|
-
"#E67E22",
|
|
938
|
-
"#95A5A6",
|
|
939
|
-
]
|
|
940
|
-
|
|
941
|
-
# Processing: create pie chart
|
|
942
|
-
autopct_val = _create_loss_autopct_formatter(show_watts, total_loss)
|
|
943
|
-
pie_result = _create_loss_pie_chart(ax, labels, values, colors, autopct_val)
|
|
944
|
-
|
|
945
|
-
# Result building: format and finalize
|
|
946
|
-
_format_loss_pie_chart(ax, pie_result, total_loss, title)
|
|
947
|
-
fig.tight_layout()
|
|
948
|
-
|
|
949
|
-
if save_path is not None:
|
|
950
|
-
fig.savefig(save_path, dpi=300, bbox_inches="tight")
|
|
951
|
-
|
|
952
|
-
if show:
|
|
953
|
-
plt.show()
|
|
954
|
-
|
|
955
|
-
return fig
|