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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. oscura/__init__.py +19 -19
  2. oscura/analyzers/__init__.py +2 -0
  3. oscura/analyzers/digital/extraction.py +2 -3
  4. oscura/analyzers/digital/quality.py +1 -1
  5. oscura/analyzers/digital/timing.py +1 -1
  6. oscura/analyzers/eye/__init__.py +5 -1
  7. oscura/analyzers/eye/generation.py +501 -0
  8. oscura/analyzers/jitter/__init__.py +6 -6
  9. oscura/analyzers/jitter/timing.py +419 -0
  10. oscura/analyzers/patterns/__init__.py +94 -0
  11. oscura/analyzers/patterns/reverse_engineering.py +991 -0
  12. oscura/analyzers/power/__init__.py +35 -12
  13. oscura/analyzers/power/basic.py +3 -3
  14. oscura/analyzers/power/soa.py +1 -1
  15. oscura/analyzers/power/switching.py +3 -3
  16. oscura/analyzers/signal_classification.py +529 -0
  17. oscura/analyzers/signal_integrity/sparams.py +3 -3
  18. oscura/analyzers/statistics/__init__.py +4 -0
  19. oscura/analyzers/statistics/basic.py +152 -0
  20. oscura/analyzers/statistics/correlation.py +47 -6
  21. oscura/analyzers/validation.py +1 -1
  22. oscura/analyzers/waveform/__init__.py +2 -0
  23. oscura/analyzers/waveform/measurements.py +329 -163
  24. oscura/analyzers/waveform/measurements_with_uncertainty.py +91 -35
  25. oscura/analyzers/waveform/spectral.py +498 -54
  26. oscura/api/dsl/commands.py +15 -6
  27. oscura/api/server/templates/base.html +137 -146
  28. oscura/api/server/templates/export.html +84 -110
  29. oscura/api/server/templates/home.html +248 -267
  30. oscura/api/server/templates/protocols.html +44 -48
  31. oscura/api/server/templates/reports.html +27 -35
  32. oscura/api/server/templates/session_detail.html +68 -78
  33. oscura/api/server/templates/sessions.html +62 -72
  34. oscura/api/server/templates/waveforms.html +54 -64
  35. oscura/automotive/__init__.py +1 -1
  36. oscura/automotive/can/session.py +1 -1
  37. oscura/automotive/dbc/generator.py +638 -23
  38. oscura/automotive/dtc/data.json +102 -17
  39. oscura/automotive/uds/decoder.py +99 -6
  40. oscura/cli/analyze.py +8 -2
  41. oscura/cli/batch.py +36 -5
  42. oscura/cli/characterize.py +18 -4
  43. oscura/cli/export.py +47 -5
  44. oscura/cli/main.py +2 -0
  45. oscura/cli/onboarding/wizard.py +10 -6
  46. oscura/cli/pipeline.py +585 -0
  47. oscura/cli/visualize.py +6 -4
  48. oscura/convenience.py +400 -32
  49. oscura/core/config/loader.py +0 -1
  50. oscura/core/measurement_result.py +286 -0
  51. oscura/core/progress.py +1 -1
  52. oscura/core/schemas/device_mapping.json +8 -2
  53. oscura/core/schemas/packet_format.json +24 -4
  54. oscura/core/schemas/protocol_definition.json +12 -2
  55. oscura/core/types.py +300 -199
  56. oscura/correlation/multi_protocol.py +1 -1
  57. oscura/export/legacy/__init__.py +11 -0
  58. oscura/export/legacy/wav.py +75 -0
  59. oscura/exporters/__init__.py +19 -0
  60. oscura/exporters/wireshark.py +809 -0
  61. oscura/hardware/acquisition/file.py +5 -19
  62. oscura/hardware/acquisition/saleae.py +10 -10
  63. oscura/hardware/acquisition/socketcan.py +4 -6
  64. oscura/hardware/acquisition/synthetic.py +1 -5
  65. oscura/hardware/acquisition/visa.py +6 -6
  66. oscura/hardware/security/side_channel_detector.py +5 -508
  67. oscura/inference/message_format.py +686 -1
  68. oscura/jupyter/display.py +2 -2
  69. oscura/jupyter/magic.py +3 -3
  70. oscura/loaders/__init__.py +17 -12
  71. oscura/loaders/binary.py +1 -1
  72. oscura/loaders/chipwhisperer.py +1 -2
  73. oscura/loaders/configurable.py +1 -1
  74. oscura/loaders/csv_loader.py +2 -2
  75. oscura/loaders/hdf5_loader.py +1 -1
  76. oscura/loaders/lazy.py +6 -1
  77. oscura/loaders/mmap_loader.py +0 -1
  78. oscura/loaders/numpy_loader.py +8 -7
  79. oscura/loaders/preprocessing.py +3 -5
  80. oscura/loaders/rigol.py +21 -7
  81. oscura/loaders/sigrok.py +2 -5
  82. oscura/loaders/tdms.py +3 -2
  83. oscura/loaders/tektronix.py +38 -32
  84. oscura/loaders/tss.py +20 -27
  85. oscura/loaders/vcd.py +13 -8
  86. oscura/loaders/wav.py +1 -6
  87. oscura/pipeline/__init__.py +76 -0
  88. oscura/pipeline/handlers/__init__.py +165 -0
  89. oscura/pipeline/handlers/analyzers.py +1045 -0
  90. oscura/pipeline/handlers/decoders.py +899 -0
  91. oscura/pipeline/handlers/exporters.py +1103 -0
  92. oscura/pipeline/handlers/filters.py +891 -0
  93. oscura/pipeline/handlers/loaders.py +640 -0
  94. oscura/pipeline/handlers/transforms.py +768 -0
  95. oscura/reporting/__init__.py +88 -1
  96. oscura/reporting/automation.py +348 -0
  97. oscura/reporting/citations.py +374 -0
  98. oscura/reporting/core.py +54 -0
  99. oscura/reporting/formatting/__init__.py +11 -0
  100. oscura/reporting/formatting/measurements.py +320 -0
  101. oscura/reporting/html.py +57 -0
  102. oscura/reporting/interpretation.py +431 -0
  103. oscura/reporting/summary.py +329 -0
  104. oscura/reporting/templates/enhanced/protocol_re.html +504 -503
  105. oscura/reporting/visualization.py +542 -0
  106. oscura/side_channel/__init__.py +38 -57
  107. oscura/utils/builders/signal_builder.py +5 -5
  108. oscura/utils/comparison/compare.py +7 -9
  109. oscura/utils/comparison/golden.py +1 -1
  110. oscura/utils/filtering/convenience.py +2 -2
  111. oscura/utils/math/arithmetic.py +38 -62
  112. oscura/utils/math/interpolation.py +20 -20
  113. oscura/utils/pipeline/__init__.py +4 -17
  114. oscura/utils/progressive.py +1 -4
  115. oscura/utils/triggering/edge.py +1 -1
  116. oscura/utils/triggering/pattern.py +2 -2
  117. oscura/utils/triggering/pulse.py +2 -2
  118. oscura/utils/triggering/window.py +3 -3
  119. oscura/validation/hil_testing.py +11 -11
  120. oscura/visualization/__init__.py +47 -284
  121. oscura/visualization/batch.py +160 -0
  122. oscura/visualization/plot.py +542 -53
  123. oscura/visualization/styles.py +184 -318
  124. oscura/workflows/__init__.py +2 -0
  125. oscura/workflows/batch/advanced.py +1 -1
  126. oscura/workflows/batch/aggregate.py +7 -8
  127. oscura/workflows/complete_re.py +251 -23
  128. oscura/workflows/digital.py +27 -4
  129. oscura/workflows/multi_trace.py +136 -17
  130. oscura/workflows/waveform.py +788 -0
  131. {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/METADATA +59 -79
  132. {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/RECORD +135 -149
  133. oscura/side_channel/dpa.py +0 -1025
  134. oscura/utils/optimization/__init__.py +0 -19
  135. oscura/utils/optimization/parallel.py +0 -443
  136. oscura/utils/optimization/search.py +0 -532
  137. oscura/utils/pipeline/base.py +0 -338
  138. oscura/utils/pipeline/composition.py +0 -248
  139. oscura/utils/pipeline/parallel.py +0 -449
  140. oscura/utils/pipeline/pipeline.py +0 -375
  141. oscura/utils/search/__init__.py +0 -16
  142. oscura/utils/search/anomaly.py +0 -424
  143. oscura/utils/search/context.py +0 -294
  144. oscura/utils/search/pattern.py +0 -288
  145. oscura/utils/storage/__init__.py +0 -61
  146. oscura/utils/storage/database.py +0 -1166
  147. oscura/visualization/accessibility.py +0 -526
  148. oscura/visualization/annotations.py +0 -371
  149. oscura/visualization/axis_scaling.py +0 -305
  150. oscura/visualization/colors.py +0 -451
  151. oscura/visualization/digital.py +0 -436
  152. oscura/visualization/eye.py +0 -571
  153. oscura/visualization/histogram.py +0 -281
  154. oscura/visualization/interactive.py +0 -1035
  155. oscura/visualization/jitter.py +0 -1042
  156. oscura/visualization/keyboard.py +0 -394
  157. oscura/visualization/layout.py +0 -400
  158. oscura/visualization/optimization.py +0 -1079
  159. oscura/visualization/palettes.py +0 -446
  160. oscura/visualization/power.py +0 -508
  161. oscura/visualization/power_extended.py +0 -955
  162. oscura/visualization/presets.py +0 -469
  163. oscura/visualization/protocols.py +0 -1246
  164. oscura/visualization/render.py +0 -223
  165. oscura/visualization/rendering.py +0 -444
  166. oscura/visualization/reverse_engineering.py +0 -838
  167. oscura/visualization/signal_integrity.py +0 -989
  168. oscura/visualization/specialized.py +0 -643
  169. oscura/visualization/spectral.py +0 -1226
  170. oscura/visualization/thumbnails.py +0 -340
  171. oscura/visualization/time_axis.py +0 -351
  172. oscura/visualization/waveform.py +0 -454
  173. {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/WHEEL +0 -0
  174. {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/entry_points.txt +0 -0
  175. {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,454 +0,0 @@
1
- """Waveform visualization functions.
2
-
3
- This module provides time-domain waveform and multi-channel plots
4
- with measurement annotations.
5
-
6
-
7
- Example:
8
- >>> from oscura.visualization.waveform import plot_waveform, plot_multi_channel
9
- >>> plot_waveform(trace)
10
- >>> plot_multi_channel([ch1, ch2, ch3])
11
-
12
- References:
13
- matplotlib best practices for scientific visualization
14
- """
15
-
16
- from __future__ import annotations
17
-
18
- from typing import TYPE_CHECKING, Any, cast
19
-
20
- import numpy as np
21
-
22
- try:
23
- import matplotlib.pyplot as plt
24
-
25
- HAS_MATPLOTLIB = True
26
- except ImportError:
27
- HAS_MATPLOTLIB = False
28
-
29
- from oscura.core.types import DigitalTrace, WaveformTrace
30
-
31
- if TYPE_CHECKING:
32
- from matplotlib.axes import Axes
33
- from matplotlib.figure import Figure
34
- from numpy.typing import NDArray
35
-
36
-
37
- def plot_waveform(
38
- trace: WaveformTrace,
39
- *,
40
- ax: Axes | None = None,
41
- time_unit: str = "auto",
42
- time_range: tuple[float, float] | None = None,
43
- show_grid: bool = True,
44
- color: str = "C0",
45
- label: str | None = None,
46
- show_measurements: dict[str, Any] | None = None,
47
- title: str | None = None,
48
- xlabel: str = "Time",
49
- ylabel: str = "Amplitude",
50
- show: bool = True,
51
- save_path: str | None = None,
52
- figsize: tuple[float, float] = (10, 6),
53
- ) -> Figure:
54
- """Plot time-domain waveform.
55
-
56
- Args:
57
- trace: Waveform trace to plot.
58
- ax: Matplotlib axes. If None, creates new figure.
59
- time_unit: Time unit ("s", "ms", "us", "ns", "auto").
60
- time_range: Optional (start, end) time range in seconds to display.
61
- show_grid: Show grid lines.
62
- color: Line color.
63
- label: Legend label.
64
- show_measurements: Dictionary of measurements to annotate.
65
- title: Plot title.
66
- xlabel: X-axis label (appended with time unit).
67
- ylabel: Y-axis label.
68
- show: If True, call plt.show() to display the plot.
69
- save_path: Path to save the figure. If None, figure is not saved.
70
- figsize: Figure size (width, height) in inches. Only used if ax is None.
71
-
72
- Returns:
73
- Matplotlib Figure object.
74
-
75
- Raises:
76
- ImportError: If matplotlib is not installed.
77
- ValueError: If axes has no associated figure.
78
-
79
- Example:
80
- >>> import oscura as osc
81
- >>> trace = osc.load("signal.wfm")
82
- >>> fig = osc.plot_waveform(trace, time_unit="us", show=False)
83
- >>> fig.savefig("waveform.png")
84
-
85
- >>> # With custom styling
86
- >>> fig = osc.plot_waveform(trace,
87
- ... title="Captured Signal",
88
- ... xlabel="Time",
89
- ... ylabel="Voltage",
90
- ... color="blue")
91
- """
92
- if not HAS_MATPLOTLIB:
93
- raise ImportError("matplotlib is required for visualization")
94
-
95
- # Setup figure and axes
96
- fig, ax = _setup_waveform_figure(ax, figsize)
97
-
98
- # Prepare time axis
99
- time_unit_final, time_info = _prepare_time_axis(trace, time_unit)
100
- time_scaled, _ = time_info
101
-
102
- # Plot waveform
103
- _plot_waveform_data(ax, time_scaled, trace.data, color, label)
104
-
105
- # Apply styling and formatting
106
- _apply_waveform_formatting(
107
- ax,
108
- time_range,
109
- time_unit_final,
110
- time_info,
111
- xlabel,
112
- ylabel,
113
- title,
114
- trace.metadata.channel_name,
115
- show_grid,
116
- label,
117
- )
118
-
119
- # Add measurements if provided
120
- if show_measurements:
121
- _add_measurement_annotations(ax, trace, show_measurements, time_unit_final, time_info[1])
122
-
123
- fig.tight_layout()
124
-
125
- # Save and show
126
- _save_and_show_figure(fig, save_path, show)
127
-
128
- return fig
129
-
130
-
131
- def _setup_waveform_figure(ax: Axes | None, figsize: tuple[float, float]) -> tuple[Figure, Axes]:
132
- """Setup figure and axes for waveform plot."""
133
- if ax is None:
134
- fig, ax = plt.subplots(figsize=figsize)
135
- return fig, ax
136
-
137
- fig_temp = ax.get_figure()
138
- if fig_temp is None:
139
- raise ValueError("Axes must have an associated figure")
140
- return cast("Figure", fig_temp), ax
141
-
142
-
143
- def _prepare_time_axis(
144
- trace: WaveformTrace, time_unit: str
145
- ) -> tuple[str, tuple[NDArray[np.float64], float]]:
146
- """Prepare time axis with appropriate unit and scaling."""
147
- time = trace.time_vector
148
-
149
- # Auto-select time unit
150
- if time_unit == "auto":
151
- duration = time[-1] if len(time) > 0 else 0
152
- if duration < 1e-6:
153
- time_unit = "ns"
154
- elif duration < 1e-3:
155
- time_unit = "us"
156
- elif duration < 1:
157
- time_unit = "ms"
158
- else:
159
- time_unit = "s"
160
-
161
- time_multipliers = {"s": 1.0, "ms": 1e3, "us": 1e6, "ns": 1e9}
162
- multiplier = time_multipliers.get(time_unit, 1.0)
163
- time_scaled = time * multiplier
164
-
165
- return time_unit, (time_scaled, multiplier)
166
-
167
-
168
- def _plot_waveform_data(
169
- ax: Axes,
170
- time_scaled: NDArray[np.float64],
171
- data: NDArray[np.float64],
172
- color: str,
173
- label: str | None,
174
- ) -> None:
175
- """Plot waveform data on axes."""
176
- ax.plot(time_scaled, data, color=color, label=label, linewidth=0.8)
177
-
178
-
179
- def _apply_waveform_formatting(
180
- ax: Axes,
181
- time_range: tuple[float, float] | None,
182
- time_unit: str,
183
- time_info: tuple[NDArray[np.float64], float],
184
- xlabel: str,
185
- ylabel: str,
186
- title: str | None,
187
- channel_name: str | None,
188
- show_grid: bool,
189
- label: str | None,
190
- ) -> None:
191
- """Apply formatting to waveform plot."""
192
- _, multiplier = time_info
193
-
194
- # Apply time range if specified
195
- if time_range is not None:
196
- ax.set_xlim(time_range[0] * multiplier, time_range[1] * multiplier)
197
-
198
- # Labels
199
- ax.set_xlabel(f"{xlabel} ({time_unit})")
200
- ax.set_ylabel(ylabel)
201
-
202
- # Title
203
- if title:
204
- ax.set_title(title)
205
- elif channel_name:
206
- ax.set_title(f"Waveform - {channel_name}")
207
-
208
- # Grid and legend
209
- if show_grid:
210
- ax.grid(True, alpha=0.3)
211
-
212
- if label:
213
- ax.legend()
214
-
215
-
216
- def _save_and_show_figure(fig: Figure, save_path: str | None, show: bool) -> None:
217
- """Save and/or display the figure."""
218
- if save_path is not None:
219
- fig.savefig(save_path, dpi=300, bbox_inches="tight")
220
-
221
- if show:
222
- plt.show()
223
-
224
-
225
- def plot_multi_channel(
226
- traces: list[WaveformTrace | DigitalTrace],
227
- *,
228
- names: list[str] | None = None,
229
- shared_x: bool = True,
230
- share_x: bool | None = None,
231
- colors: list[str] | None = None,
232
- time_unit: str = "auto",
233
- show_grid: bool = True,
234
- figsize: tuple[float, float] | None = None,
235
- title: str | None = None,
236
- ) -> Figure:
237
- """Plot multiple channels in stacked subplots.
238
-
239
- Args:
240
- traces: List of traces to plot.
241
- names: Channel names for labels.
242
- shared_x: Share x-axis across subplots.
243
- share_x: Alias for shared_x (for compatibility).
244
- colors: List of colors for each trace. If None, uses default cycle.
245
- time_unit: Time unit ("s", "ms", "us", "ns", "auto").
246
- show_grid: Show grid lines.
247
- figsize: Figure size (width, height) in inches.
248
- title: Overall figure title.
249
-
250
- Returns:
251
- Matplotlib Figure object.
252
-
253
- Raises:
254
- ImportError: If matplotlib is not available.
255
-
256
- Example:
257
- >>> fig = plot_multi_channel([ch1, ch2, ch3], names=["CLK", "DATA", "CS"])
258
- >>> plt.show()
259
- """
260
- if not HAS_MATPLOTLIB:
261
- raise ImportError("matplotlib is required for visualization")
262
-
263
- shared_x = share_x if share_x is not None else shared_x
264
- n_channels = len(traces)
265
- names = names or [f"CH{i + 1}" for i in range(n_channels)]
266
- figsize = figsize or (10, 2 * n_channels)
267
-
268
- fig, axes = plt.subplots(n_channels, 1, figsize=figsize, sharex=shared_x)
269
- axes = [axes] if n_channels == 1 else axes
270
-
271
- time_unit, multiplier = _determine_time_unit_and_multiplier(time_unit, traces)
272
-
273
- _plot_channels(traces, names, axes, colors, time_unit, multiplier, show_grid, n_channels)
274
-
275
- if title:
276
- fig.suptitle(title)
277
-
278
- fig.tight_layout()
279
- return fig
280
-
281
-
282
- def _determine_time_unit_and_multiplier(
283
- time_unit: str, traces: list[WaveformTrace | DigitalTrace]
284
- ) -> tuple[str, float]:
285
- """Determine time unit and multiplier for plotting."""
286
- if time_unit == "auto" and len(traces) > 0:
287
- ref_trace = traces[0]
288
- duration = len(ref_trace.data) * ref_trace.metadata.time_base
289
- time_unit = _select_time_unit_from_duration(duration)
290
-
291
- time_multipliers = {"s": 1.0, "ms": 1e3, "us": 1e6, "ns": 1e9}
292
- multiplier = time_multipliers.get(time_unit, 1.0)
293
-
294
- return time_unit, multiplier
295
-
296
-
297
- def _select_time_unit_from_duration(duration: float) -> str:
298
- """Select appropriate time unit based on duration."""
299
- if duration < 1e-6:
300
- return "ns"
301
- if duration < 1e-3:
302
- return "us"
303
- if duration < 1:
304
- return "ms"
305
- return "s"
306
-
307
-
308
- def _plot_channels(
309
- traces: list[WaveformTrace | DigitalTrace],
310
- names: list[str],
311
- axes: list[Any],
312
- colors: list[str] | None,
313
- time_unit: str,
314
- multiplier: float,
315
- show_grid: bool,
316
- n_channels: int,
317
- ) -> None:
318
- """Plot each channel on its subplot."""
319
- for i, (trace, name, ax) in enumerate(zip(traces, names, axes, strict=False)):
320
- time = trace.time_vector * multiplier
321
- color = colors[i] if colors and i < len(colors) else f"C{i}"
322
-
323
- _plot_single_channel(ax, trace, time, color, name, show_grid)
324
-
325
- if i == n_channels - 1:
326
- ax.set_xlabel(f"Time ({time_unit})")
327
-
328
-
329
- def _plot_single_channel(
330
- ax: Any,
331
- trace: WaveformTrace | DigitalTrace,
332
- time: Any,
333
- color: str,
334
- name: str,
335
- show_grid: bool,
336
- ) -> None:
337
- """Plot a single channel (analog or digital)."""
338
- if isinstance(trace, WaveformTrace):
339
- ax.plot(time, trace.data, color=color, linewidth=0.8)
340
- ax.set_ylabel("V")
341
- else:
342
- ax.step(time, trace.data.astype(int), color=color, where="post", linewidth=1.0)
343
- ax.set_ylim(-0.1, 1.1)
344
- ax.set_yticks([0, 1])
345
- ax.set_yticklabels(["L", "H"])
346
-
347
- ax.set_ylabel(name, rotation=0, ha="right", va="center")
348
-
349
- if show_grid:
350
- ax.grid(True, alpha=0.3)
351
-
352
-
353
- def plot_xy(
354
- x_trace: WaveformTrace | NDArray[np.float64],
355
- y_trace: WaveformTrace | NDArray[np.float64],
356
- *,
357
- ax: Axes | None = None,
358
- color: str = "C0",
359
- marker: str = "",
360
- alpha: float = 0.7,
361
- title: str | None = None,
362
- ) -> Figure:
363
- """Plot X-Y (Lissajous) diagram.
364
-
365
- Args:
366
- x_trace: X-axis waveform.
367
- y_trace: Y-axis waveform.
368
- ax: Matplotlib axes.
369
- color: Line/marker color.
370
- marker: Marker style.
371
- alpha: Transparency.
372
- title: Plot title.
373
-
374
- Returns:
375
- Matplotlib Figure object.
376
-
377
- Raises:
378
- ImportError: If matplotlib is not available.
379
- ValueError: If axes has no associated figure.
380
-
381
- Example:
382
- >>> fig = plot_xy(ch1, ch2) # Phase relationship
383
- """
384
- if not HAS_MATPLOTLIB:
385
- raise ImportError("matplotlib is required for visualization")
386
-
387
- if ax is None:
388
- fig, ax = plt.subplots(figsize=(6, 6))
389
- else:
390
- fig_temp = ax.get_figure()
391
- if fig_temp is None:
392
- raise ValueError("Axes must have an associated figure")
393
- fig = cast("Figure", fig_temp)
394
-
395
- x_data = x_trace.data if isinstance(x_trace, WaveformTrace) else x_trace
396
- y_data = y_trace.data if isinstance(y_trace, WaveformTrace) else y_trace
397
-
398
- # Ensure same length
399
- min_len = min(len(x_data), len(y_data))
400
- x_data = x_data[:min_len]
401
- y_data = y_data[:min_len]
402
-
403
- ax.plot(x_data, y_data, color=color, marker=marker, alpha=alpha, linewidth=0.5)
404
-
405
- ax.set_xlabel("X (V)")
406
- ax.set_ylabel("Y (V)")
407
- ax.set_aspect("equal")
408
- ax.grid(True, alpha=0.3)
409
-
410
- if title:
411
- ax.set_title(title)
412
-
413
- fig.tight_layout()
414
- return fig
415
-
416
-
417
- def _add_measurement_annotations(
418
- ax: Axes,
419
- trace: WaveformTrace,
420
- measurements: dict[str, Any],
421
- time_unit: str,
422
- multiplier: float,
423
- ) -> None:
424
- """Add measurement annotations to plot."""
425
- # Create annotation text
426
- text_lines = []
427
-
428
- for name, value in measurements.items():
429
- if isinstance(value, dict):
430
- val = value.get("value", value)
431
- unit = value.get("unit", "")
432
- if isinstance(val, float) and not np.isnan(val):
433
- text_lines.append(f"{name}: {val:.4g} {unit}")
434
- elif isinstance(value, float) and not np.isnan(value):
435
- text_lines.append(f"{name}: {value:.4g}")
436
-
437
- if text_lines:
438
- text = "\n".join(text_lines)
439
- ax.annotate(
440
- text,
441
- xy=(0.02, 0.98),
442
- xycoords="axes fraction",
443
- verticalalignment="top",
444
- fontfamily="monospace",
445
- fontsize=8,
446
- bbox={"boxstyle": "round", "facecolor": "wheat", "alpha": 0.8},
447
- )
448
-
449
-
450
- __all__ = [
451
- "plot_multi_channel",
452
- "plot_waveform",
453
- "plot_xy",
454
- ]