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