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,436 +0,0 @@
1
- """Digital timing diagram visualization.
2
-
3
- This module provides timing diagrams for digital signals with
4
- protocol decode overlay support.
5
-
6
-
7
- Example:
8
- >>> from oscura.visualization.digital import plot_timing
9
- >>> fig = plot_timing([clk, data, cs], names=["CLK", "DATA", "CS"])
10
- >>> plt.show()
11
-
12
- References:
13
- matplotlib best practices for digital waveform visualization
14
- """
15
-
16
- from __future__ import annotations
17
-
18
- from typing import TYPE_CHECKING
19
-
20
- import numpy as np
21
-
22
- try:
23
- import matplotlib.pyplot as plt
24
- from matplotlib.patches import Rectangle
25
-
26
- HAS_MATPLOTLIB = True
27
- except ImportError:
28
- HAS_MATPLOTLIB = False
29
-
30
- from oscura.core.types import DigitalTrace, WaveformTrace
31
-
32
- if TYPE_CHECKING:
33
- from collections.abc import Sequence
34
-
35
- from matplotlib.axes import Axes
36
- from matplotlib.figure import Figure
37
-
38
- from oscura.analyzers.protocols.base import Annotation
39
-
40
-
41
- def _validate_timing_inputs(
42
- traces: Sequence[WaveformTrace | DigitalTrace],
43
- names: list[str] | None,
44
- ) -> tuple[int, list[str]]:
45
- """Validate plot_timing inputs and generate default names.
46
-
47
- Args:
48
- traces: List of traces to validate.
49
- names: Channel names or None for defaults.
50
-
51
- Returns:
52
- Tuple of (n_channels, validated_names).
53
-
54
- Raises:
55
- ValueError: If traces empty or names length mismatch.
56
- """
57
- if len(traces) == 0:
58
- raise ValueError("traces list cannot be empty")
59
-
60
- n_channels = len(traces)
61
-
62
- if names is None:
63
- names = [f"CH{i + 1}" for i in range(n_channels)]
64
-
65
- if len(names) != n_channels:
66
- raise ValueError(f"names length ({len(names)}) must match traces ({n_channels})")
67
-
68
- return n_channels, names
69
-
70
-
71
- def _convert_to_digital_traces(
72
- traces: Sequence[WaveformTrace | DigitalTrace],
73
- threshold: float | str,
74
- ) -> list[DigitalTrace]:
75
- """Convert analog traces to digital using threshold.
76
-
77
- Args:
78
- traces: List of analog or digital traces.
79
- threshold: Threshold for analog-to-digital conversion.
80
-
81
- Returns:
82
- List of digital traces.
83
- """
84
- from oscura.analyzers.digital.extraction import to_digital
85
-
86
- digital_traces: list[DigitalTrace] = []
87
- for trace in traces:
88
- if isinstance(trace, WaveformTrace):
89
- digital_traces.append(to_digital(trace, threshold=threshold)) # type: ignore[arg-type]
90
- else:
91
- digital_traces.append(trace)
92
-
93
- return digital_traces
94
-
95
-
96
- def _select_time_unit_and_multiplier(
97
- digital_traces: list[DigitalTrace],
98
- time_unit: str,
99
- ) -> tuple[str, float]:
100
- """Select appropriate time unit based on signal duration.
101
-
102
- Args:
103
- digital_traces: List of digital traces.
104
- time_unit: Time unit ("auto" or specific unit).
105
-
106
- Returns:
107
- Tuple of (time_unit, multiplier).
108
- """
109
- if time_unit == "auto" and len(digital_traces) > 0:
110
- ref_trace = digital_traces[0]
111
- duration = len(ref_trace.data) * ref_trace.metadata.time_base
112
- if duration < 1e-6:
113
- time_unit = "ns"
114
- elif duration < 1e-3:
115
- time_unit = "us"
116
- elif duration < 1:
117
- time_unit = "ms"
118
- else:
119
- time_unit = "s"
120
-
121
- time_multipliers = {"s": 1.0, "ms": 1e3, "us": 1e6, "ns": 1e9}
122
- multiplier = time_multipliers.get(time_unit, 1.0)
123
-
124
- return time_unit, multiplier
125
-
126
-
127
- def _determine_plot_time_range(
128
- digital_traces: list[DigitalTrace],
129
- time_range: tuple[float, float] | None,
130
- ) -> tuple[float, float]:
131
- """Determine start and end times for plot.
132
-
133
- Args:
134
- digital_traces: List of digital traces.
135
- time_range: User-specified time range or None for auto.
136
-
137
- Returns:
138
- Tuple of (start_time, end_time) in seconds.
139
- """
140
- if time_range is not None:
141
- return time_range
142
-
143
- start_time = 0.0
144
- end_time = max(trace.duration for trace in digital_traces if len(trace.data) > 0)
145
- return start_time, end_time
146
-
147
-
148
- def _plot_timing_channel(
149
- ax: Axes,
150
- trace: DigitalTrace,
151
- name: str,
152
- channel_index: int,
153
- multiplier: float,
154
- time_range: tuple[float, float] | None,
155
- show_grid: bool,
156
- annotations: list[Annotation] | None,
157
- time_unit: str,
158
- ) -> None:
159
- """Plot a single channel in the timing diagram.
160
-
161
- Args:
162
- ax: Matplotlib axes to plot on.
163
- trace: Digital trace to plot.
164
- name: Channel name for label.
165
- channel_index: Index for color selection.
166
- multiplier: Time unit multiplier.
167
- time_range: Optional time range to display.
168
- show_grid: Show vertical grid lines.
169
- annotations: Optional protocol annotations.
170
- time_unit: Time unit string.
171
- """
172
- time = trace.time_vector * multiplier
173
-
174
- # Filter to time range
175
- if time_range is not None:
176
- start_time, end_time = time_range
177
- start_idx = int(np.searchsorted(trace.time_vector, start_time))
178
- end_idx = int(np.searchsorted(trace.time_vector, end_time))
179
- time = time[start_idx:end_idx]
180
- data_slice = trace.data[start_idx:end_idx]
181
- else:
182
- data_slice = trace.data
183
-
184
- # Plot digital waveform as step function
185
- ax.step(
186
- time,
187
- data_slice.astype(int),
188
- where="post",
189
- color=f"C{channel_index}",
190
- linewidth=1.5,
191
- )
192
-
193
- # Set up digital signal display
194
- ax.set_ylim(-0.2, 1.2)
195
- ax.set_yticks([0, 1])
196
- ax.set_yticklabels(["0", "1"])
197
- ax.set_ylabel(name, rotation=0, ha="right", va="center", fontweight="bold")
198
-
199
- if show_grid:
200
- ax.grid(True, alpha=0.2, axis="x")
201
-
202
- # Add protocol annotations if provided
203
- if annotations:
204
- _add_protocol_annotations(ax, annotations, multiplier, time_unit)
205
-
206
-
207
- def plot_timing(
208
- traces: Sequence[WaveformTrace | DigitalTrace],
209
- *,
210
- names: list[str] | None = None,
211
- annotations: list[list[Annotation]] | None = None,
212
- time_unit: str = "auto",
213
- show_grid: bool = True,
214
- figsize: tuple[float, float] | None = None,
215
- title: str | None = None,
216
- time_range: tuple[float, float] | None = None,
217
- threshold: float | str = "auto",
218
- ) -> Figure:
219
- """Plot digital timing diagram with protocol decode overlay.
220
-
221
- Creates a stacked timing diagram showing digital waveforms with
222
- timing information and optional protocol decode annotations.
223
-
224
- Args:
225
- traces: List of traces to plot (analog or digital).
226
- names: Channel names for labels. If None, uses CH1, CH2, etc.
227
- annotations: List of protocol annotations per channel (optional).
228
- time_unit: Time unit ("s", "ms", "us", "ns", "auto").
229
- show_grid: Show vertical grid lines at time intervals.
230
- figsize: Figure size (width, height) in inches.
231
- title: Overall figure title.
232
- time_range: Optional (start, end) time range to display in seconds.
233
- threshold: Threshold for analog-to-digital conversion ("auto" or float).
234
-
235
- Returns:
236
- Matplotlib Figure object.
237
-
238
- Raises:
239
- ImportError: If matplotlib is not available.
240
- ValueError: If traces list is empty.
241
-
242
- Example:
243
- >>> fig = plot_timing(
244
- ... [clk_trace, data_trace, cs_trace],
245
- ... names=["CLK", "DATA", "CS"],
246
- ... annotations=[[], uart_annotations, []]
247
- ... )
248
- >>> plt.savefig("timing.png")
249
-
250
- References:
251
- IEEE 181-2011: Standard for Transitional Waveform Definitions
252
- """
253
- if not HAS_MATPLOTLIB:
254
- raise ImportError("matplotlib is required for visualization")
255
-
256
- # Data preparation/validation
257
- n_channels, names = _validate_timing_inputs(traces, names)
258
- digital_traces = _convert_to_digital_traces(traces, threshold)
259
-
260
- # Unit/scale selection
261
- time_unit, multiplier = _select_time_unit_and_multiplier(digital_traces, time_unit)
262
- start_time, end_time = _determine_plot_time_range(digital_traces, time_range)
263
-
264
- # Figure/axes creation
265
- if figsize is None:
266
- figsize = (12, 1.5 * n_channels)
267
-
268
- fig, axes = plt.subplots(n_channels, 1, figsize=figsize, sharex=True)
269
-
270
- if n_channels == 1:
271
- axes = [axes]
272
-
273
- # Plotting/rendering
274
- for i, (trace, name, ax) in enumerate(zip(digital_traces, names, axes, strict=False)):
275
- channel_annotations = annotations[i] if annotations and i < len(annotations) else None
276
- _plot_timing_channel(
277
- ax, trace, name, i, multiplier, time_range, show_grid, channel_annotations, time_unit
278
- )
279
-
280
- # Remove x-axis labels except for bottom plot
281
- if i < n_channels - 1:
282
- ax.set_xticklabels([])
283
-
284
- # Annotation/labeling
285
- axes[-1].set_xlabel(f"Time ({time_unit})")
286
-
287
- if title:
288
- fig.suptitle(title, fontsize=14, fontweight="bold")
289
-
290
- # Layout/formatting
291
- fig.tight_layout()
292
- return fig
293
-
294
-
295
- def _add_protocol_annotations(
296
- ax: Axes,
297
- annotations: list[Annotation],
298
- multiplier: float,
299
- time_unit: str,
300
- ) -> None:
301
- """Add protocol decode annotations to timing diagram.
302
-
303
- Args:
304
- ax: Matplotlib axes to annotate.
305
- annotations: List of protocol annotations.
306
- multiplier: Time unit multiplier for display.
307
- time_unit: Time unit string.
308
- """
309
- for ann in annotations:
310
- # Get annotation time range
311
- start_time = ann.start_sample * multiplier if hasattr(ann, "start_sample") else 0
312
- end_time = ann.end_sample * multiplier if hasattr(ann, "end_sample") else start_time
313
-
314
- # Get annotation text and level
315
- if hasattr(ann, "data"):
316
- text = str(ann.data)
317
- elif hasattr(ann, "value"):
318
- text = str(ann.value)
319
- else:
320
- text = str(ann)
321
-
322
- # Determine annotation color based on type/level
323
- color = "lightblue"
324
- if hasattr(ann, "level"):
325
- level_str = str(ann.level).lower()
326
- if "error" in level_str or "warn" in level_str:
327
- color = "lightcoral"
328
- elif "data" in level_str or "byte" in level_str:
329
- color = "lightgreen"
330
- elif "start" in level_str or "stop" in level_str:
331
- color = "lightyellow"
332
-
333
- # Draw annotation box
334
- width = end_time - start_time if end_time > start_time else multiplier * 10
335
- rect = Rectangle(
336
- (start_time, 1.05),
337
- width,
338
- 0.15,
339
- facecolor=color,
340
- edgecolor="black",
341
- linewidth=0.5,
342
- alpha=0.7,
343
- )
344
- ax.add_patch(rect)
345
-
346
- # Add text label
347
- mid_time = start_time + width / 2
348
- ax.text(
349
- mid_time,
350
- 1.125,
351
- text,
352
- ha="center",
353
- va="center",
354
- fontsize=7,
355
- fontfamily="monospace",
356
- )
357
-
358
-
359
- def plot_logic_analyzer(
360
- traces: Sequence[DigitalTrace],
361
- *,
362
- names: list[str] | None = None,
363
- bus_groups: dict[str, list[int]] | None = None,
364
- time_unit: str = "auto",
365
- show_grid: bool = True,
366
- figsize: tuple[float, float] | None = None,
367
- title: str | None = None,
368
- ) -> Figure:
369
- """Plot logic analyzer style multi-channel display with bus grouping.
370
-
371
- Creates a timing diagram optimized for logic analyzer visualization
372
- with support for bus grouping (showing multi-bit buses as hex values).
373
-
374
- Args:
375
- traces: List of digital traces.
376
- names: Channel names.
377
- bus_groups: Dictionary mapping bus names to channel indices.
378
- Example: {"DATA": [0, 1, 2, 3], "ADDR": [4, 5, 6, 7]}
379
- time_unit: Time unit for display.
380
- show_grid: Show vertical grid lines.
381
- figsize: Figure size.
382
- title: Plot title.
383
-
384
- Returns:
385
- Matplotlib Figure object.
386
-
387
- Raises:
388
- ImportError: If matplotlib is not available.
389
- ValueError: If traces list is empty.
390
-
391
- Example:
392
- >>> fig = plot_logic_analyzer(
393
- ... traces,
394
- ... names=[f"D{i}" for i in range(8)],
395
- ... bus_groups={"DATA": [0, 1, 2, 3, 4, 5, 6, 7]}
396
- ... )
397
-
398
- References:
399
- Logic analyzer display conventions
400
- """
401
- if not HAS_MATPLOTLIB:
402
- raise ImportError("matplotlib is required for visualization")
403
-
404
- if len(traces) == 0:
405
- raise ValueError("traces list cannot be empty")
406
-
407
- # Convert to list for plot_timing
408
- traces_list: list[WaveformTrace | DigitalTrace] = list(traces)
409
-
410
- # If no bus groups, just use regular timing diagram
411
- if bus_groups is None:
412
- return plot_timing(
413
- traces_list,
414
- names=names,
415
- time_unit=time_unit,
416
- show_grid=show_grid,
417
- figsize=figsize,
418
- title=title,
419
- )
420
-
421
- # Implementation for bus grouping would go here
422
- # For MVP, delegate to plot_timing
423
- return plot_timing(
424
- traces_list,
425
- names=names,
426
- time_unit=time_unit,
427
- show_grid=show_grid,
428
- figsize=figsize,
429
- title=title,
430
- )
431
-
432
-
433
- __all__ = [
434
- "plot_logic_analyzer",
435
- "plot_timing",
436
- ]