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,400 +0,0 @@
1
- """Visualization layout functions for multi-channel plots and annotation placement.
2
-
3
- This module provides intelligent layout algorithms for stacking multiple
4
- channels and optimizing annotation placement with collision avoidance.
5
-
6
-
7
- Example:
8
- >>> from oscura.visualization.layout import layout_stacked_channels
9
- >>> layout = layout_stacked_channels(n_channels=4, figsize=(10, 8))
10
- >>> print(f"Channel heights: {layout['heights']}")
11
-
12
- References:
13
- - Force-directed graph layout (Fruchterman-Reingold)
14
- - Constrained layout solver for equal spacing
15
- """
16
-
17
- from __future__ import annotations
18
-
19
- from dataclasses import dataclass
20
- from typing import TYPE_CHECKING
21
-
22
- import numpy as np
23
-
24
- from oscura.utils.geometry import generate_leader_line
25
-
26
- if TYPE_CHECKING:
27
- from numpy.typing import NDArray
28
-
29
-
30
- @dataclass
31
- class ChannelLayout:
32
- """Layout specification for stacked channels.
33
-
34
- Attributes:
35
- n_channels: Number of channels to stack
36
- heights: Array of subplot heights (normalized 0-1)
37
- gaps: Array of gap sizes between channels (normalized 0-1)
38
- y_positions: Array of Y positions for each channel (normalized 0-1)
39
- shared_x: Whether channels share X-axis
40
- figsize: Figure size (width, height) in inches
41
- """
42
-
43
- n_channels: int
44
- heights: NDArray[np.float64]
45
- gaps: NDArray[np.float64]
46
- y_positions: NDArray[np.float64]
47
- shared_x: bool
48
- figsize: tuple[float, float]
49
-
50
-
51
- @dataclass
52
- class Annotation:
53
- """Annotation specification with position and bounding box.
54
-
55
- Attributes:
56
- text: Annotation text
57
- x: X coordinate in data units
58
- y: Y coordinate in data units
59
- bbox_width: Bounding box width in display units
60
- bbox_height: Bounding box height in display units
61
- priority: Priority for placement (0-1, higher is more important)
62
- anchor: Preferred anchor position ("top", "bottom", "left", "right", "auto")
63
- """
64
-
65
- text: str
66
- x: float
67
- y: float
68
- bbox_width: float = 50.0
69
- bbox_height: float = 20.0
70
- priority: float = 0.5
71
- anchor: str = "auto"
72
-
73
-
74
- @dataclass
75
- class PlacedAnnotation:
76
- """Annotation with optimized placement.
77
-
78
- Attributes:
79
- annotation: Original annotation
80
- display_x: Optimized X position in display units
81
- display_y: Optimized Y position in display units
82
- needs_leader: Whether a leader line is needed
83
- leader_points: Points for leader line (if needed)
84
- """
85
-
86
- annotation: Annotation
87
- display_x: float
88
- display_y: float
89
- needs_leader: bool
90
- leader_points: list[tuple[float, float]] | None = None
91
-
92
-
93
- def layout_stacked_channels(
94
- n_channels: int,
95
- *,
96
- figsize: tuple[float, float] = (10, 8),
97
- gap_ratio: float = 0.1,
98
- shared_x: bool = True,
99
- ) -> ChannelLayout:
100
- """Calculate equal vertical spacing for stacked multi-channel plots.
101
-
102
- Implements constrained layout solver for equal spacing with configurable
103
- gaps between channels, ensuring proper vertical alignment.
104
-
105
- Args:
106
- n_channels: Number of channels to stack.
107
- figsize: Figure size (width, height) in inches.
108
- gap_ratio: Ratio of gap to channel height (default 0.1 = 10%).
109
- shared_x: Whether channels share X-axis (affects bottom margin).
110
-
111
- Returns:
112
- ChannelLayout with heights, gaps, and positions.
113
-
114
- Raises:
115
- ValueError: If n_channels < 1 or gap_ratio invalid.
116
-
117
- Example:
118
- >>> layout = layout_stacked_channels(n_channels=3, gap_ratio=0.1)
119
- >>> print(f"Channel 0 position: {layout.y_positions[0]:.3f}")
120
-
121
- References:
122
- VIS-015: Multi-Channel Stack Optimization
123
- """
124
- if n_channels < 1:
125
- raise ValueError("n_channels must be >= 1")
126
-
127
- if gap_ratio < 0 or gap_ratio > 1:
128
- raise ValueError(f"gap_ratio must be in [0, 1], got {gap_ratio}")
129
-
130
- # Total available height (normalized to 1.0)
131
- # Reserve space for margins
132
- top_margin = 0.05
133
- bottom_margin = 0.1 if shared_x else 0.05
134
- available_height = 1.0 - top_margin - bottom_margin
135
-
136
- # Calculate channel height with gaps
137
- # Total height = n_channels * h + (n_channels - 1) * gap
138
- # where gap = gap_ratio * h
139
- # Solving: available_height = n_channels * h + (n_channels - 1) * gap_ratio * h
140
- # = h * (n_channels + (n_channels - 1) * gap_ratio)
141
- denominator = n_channels + (n_channels - 1) * gap_ratio
142
- channel_height = available_height / denominator
143
- gap_height = channel_height * gap_ratio
144
-
145
- # Calculate heights and gaps arrays
146
- heights = np.full(n_channels, channel_height, dtype=np.float64)
147
- gaps = np.full(n_channels - 1, gap_height, dtype=np.float64) if n_channels > 1 else np.array([])
148
-
149
- # Calculate Y positions (from bottom)
150
- y_positions = np.zeros(n_channels, dtype=np.float64)
151
- current_y = bottom_margin
152
-
153
- for i in range(n_channels):
154
- # Channels are indexed from bottom to top
155
- y_positions[i] = current_y
156
- current_y += channel_height
157
- if i < n_channels - 1:
158
- current_y += gap_height
159
-
160
- return ChannelLayout(
161
- n_channels=n_channels,
162
- heights=heights,
163
- gaps=gaps,
164
- y_positions=y_positions,
165
- shared_x=shared_x,
166
- figsize=figsize,
167
- )
168
-
169
-
170
- def _initialize_placed_annotations(
171
- annotations: list[Annotation],
172
- ) -> list[PlacedAnnotation]:
173
- """Initialize placed annotations at anchor points.
174
-
175
- Args:
176
- annotations: List of annotations to place.
177
-
178
- Returns:
179
- List of PlacedAnnotation initially at anchor points.
180
- """
181
- placed = []
182
- for annot in annotations:
183
- placed.append(
184
- PlacedAnnotation(
185
- annotation=annot,
186
- display_x=annot.x,
187
- display_y=annot.y,
188
- needs_leader=False,
189
- leader_points=None,
190
- )
191
- )
192
- return placed
193
-
194
-
195
- def _calculate_repulsive_force(
196
- placed_i: PlacedAnnotation,
197
- placed_j: PlacedAnnotation,
198
- min_spacing: float,
199
- repulsion_strength: float,
200
- ) -> tuple[float, float]:
201
- """Calculate repulsive force between two annotations.
202
-
203
- Args:
204
- placed_i: First annotation.
205
- placed_j: Second annotation.
206
- min_spacing: Minimum spacing in pixels.
207
- repulsion_strength: Repulsive force strength.
208
-
209
- Returns:
210
- Tuple of (fx, fy) force components.
211
- """
212
- # Check for bounding box overlap
213
- dx = placed_j.display_x - placed_i.display_x
214
- dy = placed_j.display_y - placed_i.display_y
215
-
216
- # Bounding box sizes
217
- w1 = placed_i.annotation.bbox_width
218
- h1 = placed_i.annotation.bbox_height
219
- w2 = placed_j.annotation.bbox_width
220
- h2 = placed_j.annotation.bbox_height
221
-
222
- # Minimum separation (sum of half-widths + spacing)
223
- min_dx = (w1 + w2) / 2 + min_spacing
224
- min_dy = (h1 + h2) / 2 + min_spacing
225
-
226
- # Check if overlapping
227
- if abs(dx) < min_dx and abs(dy) < min_dy:
228
- # Calculate repulsive force
229
- distance = np.sqrt(dx**2 + dy**2)
230
- if distance < 1e-6:
231
- # Avoid division by zero
232
- distance = 1e-6
233
- dx = np.random.randn() * 0.1
234
- dy = np.random.randn() * 0.1
235
-
236
- # Repulsion inversely proportional to distance
237
- force = repulsion_strength / distance
238
-
239
- # Return force in direction away from overlap
240
- return -force * dx / distance, -force * dy / distance
241
-
242
- return 0.0, 0.0
243
-
244
-
245
- def _apply_force_iteration(
246
- placed: list[PlacedAnnotation],
247
- display_width: float,
248
- display_height: float,
249
- min_spacing: float,
250
- repulsion_strength: float,
251
- ) -> bool:
252
- """Apply one iteration of force-directed layout.
253
-
254
- Args:
255
- placed: List of placed annotations to update.
256
- display_width: Display width for clamping.
257
- display_height: Display height for clamping.
258
- min_spacing: Minimum spacing in pixels.
259
- repulsion_strength: Repulsive force strength.
260
-
261
- Returns:
262
- True if any annotation moved significantly.
263
- """
264
- moved = False
265
-
266
- for i in range(len(placed)):
267
- fx = 0.0
268
- fy = 0.0
269
-
270
- # Calculate forces from all other annotations
271
- for j in range(len(placed)):
272
- if i != j:
273
- force_x, force_y = _calculate_repulsive_force(
274
- placed[i], placed[j], min_spacing, repulsion_strength
275
- )
276
- fx += force_x
277
- fy += force_y
278
-
279
- # Apply forces with damping (priority affects inertia)
280
- damping = 0.5
281
- priority_factor = 1.0 - placed[i].annotation.priority
282
- step_size = damping * priority_factor
283
-
284
- new_x = placed[i].display_x + fx * step_size
285
- new_y = placed[i].display_y + fy * step_size
286
-
287
- # Clamp to display bounds
288
- new_x = np.clip(new_x, 0, display_width)
289
- new_y = np.clip(new_y, 0, display_height)
290
-
291
- # Update if moved significantly
292
- if abs(new_x - placed[i].display_x) > 0.1 or abs(new_y - placed[i].display_y) > 0.1:
293
- placed[i] = PlacedAnnotation(
294
- annotation=placed[i].annotation,
295
- display_x=new_x,
296
- display_y=new_y,
297
- needs_leader=False,
298
- leader_points=None,
299
- )
300
- moved = True
301
-
302
- return moved
303
-
304
-
305
- def _add_leader_lines(placed: list[PlacedAnnotation], leader_threshold: float = 20.0) -> None:
306
- """Add leader lines to annotations displaced from anchor points.
307
-
308
- Args:
309
- placed: List of placed annotations to update in-place.
310
- leader_threshold: Displacement threshold for leader line in pixels.
311
- """
312
- for i, p in enumerate(placed):
313
- anchor_x = p.annotation.x
314
- anchor_y = p.annotation.y
315
-
316
- displacement = np.sqrt((p.display_x - anchor_x) ** 2 + (p.display_y - anchor_y) ** 2)
317
-
318
- if displacement > leader_threshold:
319
- # Generate simple orthogonal leader line
320
- leader_points = generate_leader_line(
321
- (anchor_x, anchor_y),
322
- (p.display_x, p.display_y),
323
- )
324
-
325
- placed[i] = PlacedAnnotation(
326
- annotation=p.annotation,
327
- display_x=p.display_x,
328
- display_y=p.display_y,
329
- needs_leader=True,
330
- leader_points=leader_points,
331
- )
332
-
333
-
334
- def optimize_annotation_placement(
335
- annotations: list[Annotation],
336
- *,
337
- display_width: float = 800.0,
338
- display_height: float = 600.0,
339
- max_iterations: int = 100,
340
- repulsion_strength: float = 10.0,
341
- min_spacing: float = 5.0,
342
- ) -> list[PlacedAnnotation]:
343
- """Optimize annotation placement with collision avoidance.
344
-
345
- Uses force-directed layout algorithm to separate overlapping labels
346
- with repulsive forces. Generates leader lines when labels must be
347
- displaced from anchor points.
348
-
349
- Args:
350
- annotations: List of annotations to place.
351
- display_width: Display area width in pixels.
352
- display_height: Display area height in pixels.
353
- max_iterations: Maximum iterations for force-directed layout.
354
- repulsion_strength: Strength of repulsive force between overlapping labels.
355
- min_spacing: Minimum spacing between annotations in pixels.
356
-
357
- Returns:
358
- List of PlacedAnnotation with optimized positions.
359
-
360
- Raises:
361
- ValueError: If annotations list is empty.
362
-
363
- Example:
364
- >>> annots = [Annotation("Peak", 0.5, 1.0, priority=0.9)]
365
- >>> placed = optimize_annotation_placement(annots)
366
- >>> print(f"Needs leader: {placed[0].needs_leader}")
367
-
368
- References:
369
- VIS-016: Annotation Placement Intelligence
370
- Force-directed graph layout (Fruchterman-Reingold)
371
- """
372
- if len(annotations) == 0:
373
- raise ValueError("annotations list cannot be empty")
374
-
375
- # Data preparation - initialize at anchor points
376
- placed = _initialize_placed_annotations(annotations)
377
-
378
- # Force-directed layout iterations
379
- for _iteration in range(max_iterations):
380
- moved = _apply_force_iteration(
381
- placed, display_width, display_height, min_spacing, repulsion_strength
382
- )
383
-
384
- # Converged if nothing moved
385
- if not moved:
386
- break
387
-
388
- # Annotation - add leader lines for displaced annotations
389
- _add_leader_lines(placed, leader_threshold=20.0)
390
-
391
- return placed
392
-
393
-
394
- __all__ = [
395
- "Annotation",
396
- "ChannelLayout",
397
- "PlacedAnnotation",
398
- "layout_stacked_channels",
399
- "optimize_annotation_placement",
400
- ]