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