oscura 0.8.0__py3-none-any.whl → 0.10.0__py3-none-any.whl

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