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