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,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
  ]