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,92 +1,581 @@
1
- """Plotting functions namespace.
1
+ """Core plotting functions for Oscura.
2
2
 
3
- This module provides a namespace for plot functions to support:
4
- from oscura.visualization import plot
5
- plot.plot_trace(trace)
3
+ Thin matplotlib wrappers for waveform, spectral, and digital signal visualization.
4
+ Each function accepts and returns matplotlib axes for composability.
6
5
 
7
- Re-exports main plotting functions.
6
+ Example:
7
+ >>> import matplotlib.pyplot as plt
8
+ >>> import oscura as osc
9
+ >>> fig, ax = plt.subplots()
10
+ >>> osc.plot_waveform(ax, trace)
11
+ >>> plt.savefig("waveform.png")
8
12
  """
9
13
 
10
14
  from __future__ import annotations
11
15
 
12
16
  from typing import TYPE_CHECKING, Any
13
17
 
18
+ import numpy as np
19
+
20
+ try:
21
+ import matplotlib.pyplot as plt
22
+ from matplotlib.axes import Axes
23
+
24
+ HAS_MATPLOTLIB = True
25
+ except ImportError:
26
+ HAS_MATPLOTLIB = False
27
+
28
+
14
29
  if TYPE_CHECKING:
15
- from oscura.core.types import WaveformTrace
16
-
17
- from oscura.visualization.digital import (
18
- plot_logic_analyzer,
19
- plot_timing,
20
- )
21
- from oscura.visualization.eye import (
22
- plot_bathtub,
23
- plot_eye,
24
- )
25
- from oscura.visualization.interactive import (
26
- plot_bode,
27
- plot_histogram,
28
- plot_phase,
29
- plot_waterfall,
30
- )
31
- from oscura.visualization.spectral import (
32
- plot_fft,
33
- plot_psd,
34
- plot_spectrogram,
35
- plot_spectrum,
36
- )
37
- from oscura.visualization.waveform import (
38
- plot_multi_channel,
39
- plot_waveform,
40
- plot_xy,
41
- )
42
-
43
-
44
- def plot_trace(trace: WaveformTrace, **kwargs: Any) -> Any:
45
- """Plot a trace using plot_waveform.
46
-
47
- Convenience alias for plot_waveform.
30
+ from numpy.typing import NDArray
31
+
32
+ from oscura.core.types import DigitalTrace, WaveformTrace
33
+
34
+
35
+ def _scale_time(time: NDArray[Any], unit: str) -> tuple[NDArray[Any], str]:
36
+ """Scale time array to appropriate unit."""
37
+ if unit == "auto":
38
+ max_time = time[-1]
39
+ if max_time < 1e-6:
40
+ unit = "ns"
41
+ elif max_time < 1e-3:
42
+ unit = "us"
43
+ elif max_time < 1:
44
+ unit = "ms"
45
+ else:
46
+ unit = "s"
47
+ scale_factors = {"s": 1.0, "ms": 1e3, "us": 1e6, "ns": 1e9}
48
+ return time * scale_factors.get(unit, 1.0), unit
49
+
50
+
51
+ def plot_waveform(
52
+ ax: Axes | None,
53
+ trace: WaveformTrace,
54
+ *,
55
+ time_unit: str = "auto",
56
+ color: str = "#4477AA",
57
+ label: str | None = None,
58
+ show_grid: bool = True,
59
+ ) -> Axes:
60
+ """Plot time-domain waveform.
48
61
 
49
62
  Args:
63
+ ax: Matplotlib axes (creates new if None).
50
64
  trace: Waveform trace to plot.
51
- **kwargs: Additional arguments passed to plot_waveform.
65
+ time_unit: Time unit ("s", "ms", "us", "ns", "auto").
66
+ color: Line color.
67
+ label: Legend label.
68
+ show_grid: Show grid lines.
52
69
 
53
70
  Returns:
54
71
  Matplotlib axes object.
72
+
73
+ Example:
74
+ >>> fig, ax = plt.subplots()
75
+ >>> plot_waveform(ax, trace, time_unit="us")
55
76
  """
56
- return plot_waveform(trace, **kwargs)
77
+ if not HAS_MATPLOTLIB:
78
+ raise ImportError("matplotlib required for plotting")
79
+
80
+ if ax is None:
81
+ _, ax = plt.subplots(figsize=(10, 6))
82
+
83
+ # Scale time
84
+ time = np.arange(len(trace.data)) / trace.metadata.sample_rate
85
+ time_scaled, time_unit = _scale_time(time, time_unit)
57
86
 
87
+ # Plot
88
+ ax.plot(time_scaled, trace.data, color=color, label=label, linewidth=1.0)
89
+ ax.set_xlabel(f"Time ({time_unit})")
90
+ ax.set_ylabel("Amplitude (V)")
91
+ if show_grid:
92
+ ax.grid(True, alpha=0.3, linestyle="--")
93
+ if label:
94
+ ax.legend()
58
95
 
59
- def add_annotation(text: str, **kwargs: Any) -> None:
60
- """Add annotation to current plot.
96
+ return ax
61
97
 
62
- Placeholder for annotation functionality.
98
+
99
+ def plot_multi_channel(
100
+ ax: Axes | None,
101
+ traces: list[WaveformTrace],
102
+ *,
103
+ labels: list[str] | None = None,
104
+ colors: list[str] | None = None,
105
+ offset: float | None = None,
106
+ time_unit: str = "auto",
107
+ ) -> Axes:
108
+ """Plot multiple waveforms on same axes.
63
109
 
64
110
  Args:
65
- text: Annotation text.
66
- **kwargs: Additional arguments passed to ax.annotate.
111
+ ax: Matplotlib axes (creates new if None).
112
+ traces: List of waveform traces.
113
+ labels: Channel labels.
114
+ colors: Line colors.
115
+ offset: Vertical offset between channels (auto if None).
116
+ time_unit: Time unit for x-axis.
117
+
118
+ Returns:
119
+ Matplotlib axes object.
120
+
121
+ Example:
122
+ >>> fig, ax = plt.subplots()
123
+ >>> plot_multi_channel(ax, [ch1, ch2, ch3], labels=["CH1", "CH2", "CH3"])
67
124
  """
68
- import matplotlib.pyplot as plt
125
+ if not HAS_MATPLOTLIB:
126
+ raise ImportError("matplotlib required for plotting")
127
+
128
+ if ax is None:
129
+ _, ax = plt.subplots(figsize=(10, 8))
130
+
131
+ default_colors = ["#4477AA", "#EE6677", "#228833", "#CCBB44", "#AA3377", "#66CCEE"]
132
+ colors = colors or default_colors
133
+ labels = labels or [f"Channel {i + 1}" for i in range(len(traces))]
134
+
135
+ # Auto-calculate offset
136
+ if offset is None:
137
+ all_data = np.concatenate([t.data for t in traces])
138
+ offset = (np.max(all_data) - np.min(all_data)) * 1.2
139
+
140
+ # Plot each channel
141
+ for i, trace in enumerate(traces):
142
+ time = np.arange(len(trace.data)) / trace.metadata.sample_rate
143
+ time_scaled, time_unit = _scale_time(time, time_unit)
144
+ ax.plot(
145
+ time_scaled,
146
+ trace.data + i * offset,
147
+ color=colors[i % len(colors)],
148
+ label=labels[i] if i < len(labels) else f"Channel {i + 1}",
149
+ linewidth=1.0,
150
+ )
151
+
152
+ ax.set_xlabel(f"Time ({time_unit})")
153
+ ax.set_ylabel("Amplitude (V)")
154
+ ax.grid(True, alpha=0.3, linestyle="--")
155
+ ax.legend()
156
+ return ax
157
+
158
+
159
+ def _scale_freq(freq: NDArray[Any], unit: str) -> tuple[NDArray[Any], str]:
160
+ """Scale frequency array to appropriate unit."""
161
+ if unit == "auto":
162
+ max_freq = freq[-1]
163
+ if max_freq > 1e9:
164
+ unit = "GHz"
165
+ elif max_freq > 1e6:
166
+ unit = "MHz"
167
+ elif max_freq > 1e3:
168
+ unit = "kHz"
169
+ else:
170
+ unit = "Hz"
171
+ divisors = {"Hz": 1.0, "kHz": 1e3, "MHz": 1e6, "GHz": 1e9}
172
+ return freq / divisors.get(unit, 1.0), unit
173
+
174
+
175
+ def plot_fft(
176
+ ax: Axes | None,
177
+ trace: WaveformTrace,
178
+ *,
179
+ freq_unit: str = "auto",
180
+ db_scale: bool = True,
181
+ window: str = "hann",
182
+ color: str = "#4477AA",
183
+ ) -> Axes:
184
+ """Plot FFT magnitude spectrum.
185
+
186
+ Args:
187
+ ax: Matplotlib axes (creates new if None).
188
+ trace: Waveform trace to analyze.
189
+ freq_unit: Frequency unit ("Hz", "kHz", "MHz", "auto").
190
+ db_scale: Use dB scale for magnitude.
191
+ window: Window function ("hann", "hamming", "blackman", "none").
192
+ color: Line color.
193
+
194
+ Returns:
195
+ Matplotlib axes object.
196
+
197
+ Example:
198
+ >>> fig, ax = plt.subplots()
199
+ >>> plot_fft(ax, trace, freq_unit="kHz")
200
+ """
201
+ if not HAS_MATPLOTLIB:
202
+ raise ImportError("matplotlib required for plotting")
203
+
204
+ if ax is None:
205
+ _, ax = plt.subplots(figsize=(10, 6))
206
+
207
+ from oscura.analyzers.waveform.spectral import fft
208
+
209
+ fft_result = fft(trace, window=window)
210
+ freqs = fft_result[0]
211
+ mags = fft_result[1]
212
+ freqs_scaled, freq_unit = _scale_freq(freqs, freq_unit)
213
+
214
+ if db_scale:
215
+ mags = 20 * np.log10(np.abs(mags) + 1e-12)
216
+ ylabel = "Magnitude (dB)"
217
+ else:
218
+ ylabel = "Magnitude"
219
+
220
+ ax.plot(freqs_scaled, mags, color=color, linewidth=1.0)
221
+ ax.set_xlabel(f"Frequency ({freq_unit})")
222
+ ax.set_ylabel(ylabel)
223
+ ax.grid(True, alpha=0.3, linestyle="--")
224
+ return ax
225
+
226
+
227
+ def plot_psd(
228
+ ax: Axes | None,
229
+ trace: WaveformTrace,
230
+ *,
231
+ freq_unit: str = "auto",
232
+ window: str = "hann",
233
+ nperseg: int | None = None,
234
+ color: str = "#4477AA",
235
+ ) -> Axes:
236
+ """Plot power spectral density.
237
+
238
+ Args:
239
+ ax: Matplotlib axes (creates new if None).
240
+ trace: Waveform trace to analyze.
241
+ freq_unit: Frequency unit ("Hz", "kHz", "MHz", "auto").
242
+ window: Window function.
243
+ nperseg: Segment length for Welch method.
244
+ color: Line color.
245
+
246
+ Returns:
247
+ Matplotlib axes object.
248
+
249
+ Example:
250
+ >>> fig, ax = plt.subplots()
251
+ >>> plot_psd(ax, trace)
252
+ """
253
+ if not HAS_MATPLOTLIB:
254
+ raise ImportError("matplotlib required for plotting")
255
+
256
+ if ax is None:
257
+ _, ax = plt.subplots(figsize=(10, 6))
258
+
259
+ from oscura.analyzers.waveform.spectral import psd
260
+
261
+ psd_result = psd(trace, window=window, nperseg=nperseg)
262
+ freqs = psd_result[0]
263
+ power = psd_result[1]
264
+ freqs_scaled, freq_unit = _scale_freq(freqs, freq_unit)
265
+
266
+ ax.plot(freqs_scaled, 10 * np.log10(power + 1e-12), color=color, linewidth=1.0)
267
+ ax.set_xlabel(f"Frequency ({freq_unit})")
268
+ ax.set_ylabel("Power Spectral Density (dB/Hz)")
269
+ ax.grid(True, alpha=0.3, linestyle="--")
270
+ return ax
271
+
272
+
273
+ def plot_spectrogram(
274
+ ax: Axes | None,
275
+ trace: WaveformTrace,
276
+ *,
277
+ nfft: int = 1024,
278
+ noverlap: int | None = None,
279
+ cmap: str = "viridis",
280
+ ) -> Axes:
281
+ """Plot spectrogram (time-frequency representation).
282
+
283
+ Args:
284
+ ax: Matplotlib axes (creates new if None).
285
+ trace: Waveform trace to analyze.
286
+ nfft: FFT window size.
287
+ noverlap: Overlap samples (default: nfft // 2).
288
+ cmap: Colormap name.
289
+
290
+ Returns:
291
+ Matplotlib axes object.
292
+
293
+ Example:
294
+ >>> fig, ax = plt.subplots()
295
+ >>> plot_spectrogram(ax, trace, nfft=512)
296
+ """
297
+ if not HAS_MATPLOTLIB:
298
+ raise ImportError("matplotlib required for plotting")
299
+
300
+ if ax is None:
301
+ _, ax = plt.subplots(figsize=(10, 6))
302
+
303
+ if noverlap is None:
304
+ noverlap = nfft // 2
305
+
306
+ # Generate spectrogram
307
+ sample_rate = trace.metadata.sample_rate
308
+ ax.specgram(trace.data, Fs=sample_rate, NFFT=nfft, noverlap=noverlap, cmap=cmap)
309
+
310
+ ax.set_xlabel("Time (s)")
311
+ ax.set_ylabel("Frequency (Hz)")
312
+
313
+ return ax
314
+
315
+
316
+ def plot_eye(
317
+ ax: Axes | None,
318
+ trace: WaveformTrace,
319
+ *,
320
+ symbol_rate: float,
321
+ n_symbols: int = 100,
322
+ color: str = "#4477AA",
323
+ alpha: float = 0.1,
324
+ ) -> Axes:
325
+ """Plot eye diagram for digital signals.
326
+
327
+ Args:
328
+ ax: Matplotlib axes (creates new if None).
329
+ trace: Waveform trace.
330
+ symbol_rate: Symbol rate in Hz.
331
+ n_symbols: Number of symbols to overlay.
332
+ color: Line color.
333
+ alpha: Line transparency.
334
+
335
+ Returns:
336
+ Matplotlib axes object.
337
+
338
+ Example:
339
+ >>> fig, ax = plt.subplots()
340
+ >>> plot_eye(ax, trace, symbol_rate=1e6)
341
+ """
342
+ if not HAS_MATPLOTLIB:
343
+ raise ImportError("matplotlib required for plotting")
344
+
345
+ if ax is None:
346
+ _, ax = plt.subplots(figsize=(8, 6))
347
+
348
+ # Calculate samples per symbol
349
+ sample_rate = trace.metadata.sample_rate
350
+ samples_per_symbol = int(sample_rate / symbol_rate)
351
+
352
+ # Overlay multiple symbols
353
+ for i in range(n_symbols):
354
+ start = i * samples_per_symbol
355
+ end = start + 2 * samples_per_symbol # Two symbols for eye
356
+ if end > len(trace.data):
357
+ break
358
+
359
+ segment = trace.data[start:end]
360
+ time = np.arange(len(segment)) / sample_rate * 1e6 # Convert to microseconds
361
+ ax.plot(time, segment, color=color, alpha=alpha, linewidth=0.8)
362
+
363
+ ax.set_xlabel("Time (μs)")
364
+ ax.set_ylabel("Amplitude (V)")
365
+ ax.set_title("Eye Diagram")
366
+ ax.grid(True, alpha=0.3)
367
+
368
+ return ax
369
+
370
+
371
+ def plot_timing(
372
+ ax: Axes | None,
373
+ trace: DigitalTrace,
374
+ *,
375
+ time_unit: str = "us",
376
+ color: str = "#4477AA",
377
+ label: str | None = None,
378
+ ) -> Axes:
379
+ """Plot digital timing diagram.
380
+
381
+ Args:
382
+ ax: Matplotlib axes (creates new if None).
383
+ trace: Digital trace.
384
+ time_unit: Time unit ("s", "ms", "us", "ns").
385
+ color: Line color.
386
+ label: Signal label.
387
+
388
+ Returns:
389
+ Matplotlib axes object.
390
+
391
+ Example:
392
+ >>> fig, ax = plt.subplots()
393
+ >>> plot_timing(ax, digital_trace)
394
+ """
395
+ if not HAS_MATPLOTLIB:
396
+ raise ImportError("matplotlib required for plotting")
397
+
398
+ if ax is None:
399
+ _, ax = plt.subplots(figsize=(10, 4))
400
+
401
+ time = np.arange(len(trace.data)) / trace.metadata.sample_rate
402
+ time_scaled, time_unit = _scale_time(time, time_unit)
403
+
404
+ ax.step(
405
+ time_scaled, trace.data.astype(float), where="post", color=color, label=label, linewidth=1.5
406
+ )
407
+ ax.fill_between(time_scaled, 0, trace.data.astype(float), step="post", alpha=0.2, color=color)
408
+
409
+ ax.set_xlabel(f"Time ({time_unit})")
410
+ ax.set_ylabel("Logic Level")
411
+ ax.set_ylim(-0.2, 1.3)
412
+ ax.set_yticks([0, 1])
413
+ ax.set_yticklabels(["LOW", "HIGH"])
414
+ ax.grid(True, alpha=0.3, axis="x")
415
+ if label:
416
+ ax.legend()
417
+ return ax
418
+
419
+
420
+ def plot_histogram(
421
+ ax: Axes | None,
422
+ data: NDArray[Any],
423
+ *,
424
+ bins: int = 50,
425
+ color: str = "#4477AA",
426
+ show_stats: bool = True,
427
+ ) -> Axes:
428
+ """Plot amplitude histogram with optional statistics.
429
+
430
+ Args:
431
+ ax: Matplotlib axes (creates new if None).
432
+ data: Signal data array.
433
+ bins: Number of histogram bins.
434
+ color: Bar color.
435
+ show_stats: Show mean/std overlays.
436
+
437
+ Returns:
438
+ Matplotlib axes object.
439
+
440
+ Example:
441
+ >>> fig, ax = plt.subplots()
442
+ >>> plot_histogram(ax, trace.data, bins=100)
443
+ """
444
+ if not HAS_MATPLOTLIB:
445
+ raise ImportError("matplotlib required for plotting")
446
+
447
+ if ax is None:
448
+ _, ax = plt.subplots(figsize=(8, 6))
449
+
450
+ ax.hist(data, bins=bins, color=color, alpha=0.7, edgecolor="black", linewidth=0.5)
451
+
452
+ if show_stats:
453
+ mean_val, std_val = np.mean(data), np.std(data)
454
+ ax.axvline(
455
+ mean_val, color="#EE6677", linestyle="--", linewidth=2, label=f"Mean: {mean_val:.3f}"
456
+ )
457
+ ax.axvline(
458
+ mean_val + std_val,
459
+ color="#AAA",
460
+ linestyle=":",
461
+ linewidth=1.5,
462
+ label=f"±1std: {std_val:.3f}",
463
+ )
464
+ ax.axvline(mean_val - std_val, color="#AAA", linestyle=":", linewidth=1.5)
465
+ ax.legend()
466
+
467
+ ax.set_xlabel("Amplitude (V)")
468
+ ax.set_ylabel("Count")
469
+ ax.grid(True, alpha=0.3, axis="y")
470
+ return ax
471
+
472
+
473
+ def plot_xy(
474
+ ax: Axes | None,
475
+ trace_x: WaveformTrace,
476
+ trace_y: WaveformTrace,
477
+ *,
478
+ color: str = "#4477AA",
479
+ marker: str = "o",
480
+ markersize: float = 3.0,
481
+ ) -> Axes:
482
+ """Plot XY scatter (Lissajous pattern).
483
+
484
+ Args:
485
+ ax: Matplotlib axes (creates new if None).
486
+ trace_x: X-axis trace.
487
+ trace_y: Y-axis trace.
488
+ color: Marker color.
489
+ marker: Marker style.
490
+ markersize: Marker size.
491
+
492
+ Returns:
493
+ Matplotlib axes object.
494
+
495
+ Example:
496
+ >>> fig, ax = plt.subplots()
497
+ >>> plot_xy(ax, ch1, ch2)
498
+ """
499
+ if not HAS_MATPLOTLIB:
500
+ raise ImportError("matplotlib required for plotting")
501
+
502
+ if ax is None:
503
+ _, ax = plt.subplots(figsize=(8, 8))
504
+
505
+ min_len = min(len(trace_x.data), len(trace_y.data))
506
+ ax.plot(
507
+ trace_x.data[:min_len],
508
+ trace_y.data[:min_len],
509
+ marker=marker,
510
+ linestyle="",
511
+ color=color,
512
+ markersize=markersize,
513
+ alpha=0.5,
514
+ )
515
+
516
+ ax.set_xlabel("X Amplitude (V)")
517
+ ax.set_ylabel("Y Amplitude (V)")
518
+ ax.set_aspect("equal")
519
+ ax.grid(True, alpha=0.3)
520
+ return ax
521
+
522
+
523
+ # Stub functions for backwards compatibility with demos
524
+ def plot_bathtub(*args: Any, **kwargs: Any) -> Axes:
525
+ """Stub for plot_bathtub (not implemented in refactored version)."""
526
+ raise NotImplementedError("plot_bathtub removed in visualization refactor")
527
+
528
+
529
+ def plot_protocol_decode(*args: Any, **kwargs: Any) -> Axes:
530
+ """Stub for plot_protocol_decode (not implemented in refactored version)."""
531
+ raise NotImplementedError("plot_protocol_decode removed in visualization refactor")
532
+
533
+
534
+ def plot_state_machine(*args: Any, **kwargs: Any) -> Axes:
535
+ """Stub for plot_state_machine (not implemented in refactored version)."""
536
+ raise NotImplementedError("plot_state_machine removed in visualization refactor")
537
+
538
+
539
+ def plot_thd_bars(*args: Any, **kwargs: Any) -> Axes:
540
+ """Stub for plot_thd_bars (not implemented in refactored version)."""
541
+ raise NotImplementedError("plot_thd_bars removed in visualization refactor")
542
+
543
+
544
+ def plot_quality_summary(*args: Any, **kwargs: Any) -> Axes:
545
+ """Stub for plot_quality_summary (not implemented in refactored version)."""
546
+ raise NotImplementedError("plot_quality_summary removed in visualization refactor")
547
+
548
+
549
+ def plot_logic_analyzer(*args: Any, **kwargs: Any) -> Axes:
550
+ """Alias for plot_timing for backwards compatibility."""
551
+ if not HAS_MATPLOTLIB:
552
+ raise ImportError("matplotlib required for plotting")
553
+ # Redirect to plot_timing
554
+ return plot_timing(*args, **kwargs)
555
+
69
556
 
70
- ax = plt.gca()
71
- ax.annotate(text, xy=(0.5, 0.95), xycoords="axes fraction", **kwargs)
557
+ def plot_spectrum(*args: Any, **kwargs: Any) -> Axes:
558
+ """Alias for plot_fft for backwards compatibility."""
559
+ if not HAS_MATPLOTLIB:
560
+ raise ImportError("matplotlib required for plotting")
561
+ return plot_fft(*args, **kwargs)
72
562
 
73
563
 
74
564
  __all__ = [
75
- "add_annotation",
76
565
  "plot_bathtub",
77
- "plot_bode",
78
566
  "plot_eye",
79
567
  "plot_fft",
80
568
  "plot_histogram",
81
569
  "plot_logic_analyzer",
82
570
  "plot_multi_channel",
83
- "plot_phase",
571
+ "plot_protocol_decode",
84
572
  "plot_psd",
573
+ "plot_quality_summary",
85
574
  "plot_spectrogram",
86
575
  "plot_spectrum",
576
+ "plot_state_machine",
577
+ "plot_thd_bars",
87
578
  "plot_timing",
88
- "plot_trace",
89
- "plot_waterfall",
90
579
  "plot_waveform",
91
580
  "plot_xy",
92
581
  ]