oscura 0.8.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 (151) 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/patterns/__init__.py +66 -0
  7. oscura/analyzers/power/basic.py +3 -3
  8. oscura/analyzers/power/soa.py +1 -1
  9. oscura/analyzers/power/switching.py +3 -3
  10. oscura/analyzers/signal_classification.py +529 -0
  11. oscura/analyzers/signal_integrity/sparams.py +3 -3
  12. oscura/analyzers/statistics/basic.py +10 -7
  13. oscura/analyzers/validation.py +1 -1
  14. oscura/analyzers/waveform/measurements.py +200 -156
  15. oscura/analyzers/waveform/measurements_with_uncertainty.py +91 -35
  16. oscura/analyzers/waveform/spectral.py +164 -73
  17. oscura/api/dsl/commands.py +15 -6
  18. oscura/api/server/templates/base.html +137 -146
  19. oscura/api/server/templates/export.html +84 -110
  20. oscura/api/server/templates/home.html +248 -267
  21. oscura/api/server/templates/protocols.html +44 -48
  22. oscura/api/server/templates/reports.html +27 -35
  23. oscura/api/server/templates/session_detail.html +68 -78
  24. oscura/api/server/templates/sessions.html +62 -72
  25. oscura/api/server/templates/waveforms.html +54 -64
  26. oscura/automotive/__init__.py +1 -1
  27. oscura/automotive/can/session.py +1 -1
  28. oscura/automotive/dbc/generator.py +638 -23
  29. oscura/automotive/uds/decoder.py +99 -6
  30. oscura/cli/analyze.py +8 -2
  31. oscura/cli/batch.py +36 -5
  32. oscura/cli/characterize.py +18 -4
  33. oscura/cli/export.py +47 -5
  34. oscura/cli/main.py +2 -0
  35. oscura/cli/onboarding/wizard.py +10 -6
  36. oscura/cli/pipeline.py +585 -0
  37. oscura/cli/visualize.py +6 -4
  38. oscura/convenience.py +400 -32
  39. oscura/core/measurement_result.py +286 -0
  40. oscura/core/progress.py +1 -1
  41. oscura/core/types.py +232 -239
  42. oscura/correlation/multi_protocol.py +1 -1
  43. oscura/export/legacy/__init__.py +11 -0
  44. oscura/export/legacy/wav.py +75 -0
  45. oscura/exporters/__init__.py +19 -0
  46. oscura/exporters/wireshark.py +809 -0
  47. oscura/hardware/acquisition/file.py +5 -19
  48. oscura/hardware/acquisition/saleae.py +10 -10
  49. oscura/hardware/acquisition/socketcan.py +4 -6
  50. oscura/hardware/acquisition/synthetic.py +1 -5
  51. oscura/hardware/acquisition/visa.py +6 -6
  52. oscura/hardware/security/side_channel_detector.py +5 -508
  53. oscura/inference/message_format.py +686 -1
  54. oscura/jupyter/display.py +2 -2
  55. oscura/jupyter/magic.py +3 -3
  56. oscura/loaders/__init__.py +17 -12
  57. oscura/loaders/binary.py +1 -1
  58. oscura/loaders/chipwhisperer.py +1 -2
  59. oscura/loaders/configurable.py +1 -1
  60. oscura/loaders/csv_loader.py +2 -2
  61. oscura/loaders/hdf5_loader.py +1 -1
  62. oscura/loaders/lazy.py +6 -1
  63. oscura/loaders/mmap_loader.py +0 -1
  64. oscura/loaders/numpy_loader.py +8 -7
  65. oscura/loaders/preprocessing.py +3 -5
  66. oscura/loaders/rigol.py +21 -7
  67. oscura/loaders/sigrok.py +2 -5
  68. oscura/loaders/tdms.py +3 -2
  69. oscura/loaders/tektronix.py +38 -32
  70. oscura/loaders/tss.py +20 -27
  71. oscura/loaders/vcd.py +13 -8
  72. oscura/loaders/wav.py +1 -6
  73. oscura/pipeline/__init__.py +76 -0
  74. oscura/pipeline/handlers/__init__.py +165 -0
  75. oscura/pipeline/handlers/analyzers.py +1045 -0
  76. oscura/pipeline/handlers/decoders.py +899 -0
  77. oscura/pipeline/handlers/exporters.py +1103 -0
  78. oscura/pipeline/handlers/filters.py +891 -0
  79. oscura/pipeline/handlers/loaders.py +640 -0
  80. oscura/pipeline/handlers/transforms.py +768 -0
  81. oscura/reporting/formatting/measurements.py +55 -14
  82. oscura/reporting/templates/enhanced/protocol_re.html +504 -503
  83. oscura/side_channel/__init__.py +38 -57
  84. oscura/utils/builders/signal_builder.py +5 -5
  85. oscura/utils/comparison/compare.py +7 -9
  86. oscura/utils/comparison/golden.py +1 -1
  87. oscura/utils/filtering/convenience.py +2 -2
  88. oscura/utils/math/arithmetic.py +38 -62
  89. oscura/utils/math/interpolation.py +20 -20
  90. oscura/utils/pipeline/__init__.py +4 -17
  91. oscura/utils/progressive.py +1 -4
  92. oscura/utils/triggering/edge.py +1 -1
  93. oscura/utils/triggering/pattern.py +2 -2
  94. oscura/utils/triggering/pulse.py +2 -2
  95. oscura/utils/triggering/window.py +3 -3
  96. oscura/validation/hil_testing.py +11 -11
  97. oscura/visualization/__init__.py +46 -284
  98. oscura/visualization/batch.py +72 -433
  99. oscura/visualization/plot.py +542 -53
  100. oscura/visualization/styles.py +184 -318
  101. oscura/workflows/batch/advanced.py +1 -1
  102. oscura/workflows/batch/aggregate.py +7 -8
  103. oscura/workflows/complete_re.py +251 -23
  104. oscura/workflows/digital.py +27 -4
  105. oscura/workflows/multi_trace.py +136 -17
  106. oscura/workflows/waveform.py +11 -6
  107. {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/METADATA +59 -79
  108. {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/RECORD +111 -136
  109. oscura/side_channel/dpa.py +0 -1025
  110. oscura/utils/optimization/__init__.py +0 -19
  111. oscura/utils/optimization/parallel.py +0 -443
  112. oscura/utils/optimization/search.py +0 -532
  113. oscura/utils/pipeline/base.py +0 -338
  114. oscura/utils/pipeline/composition.py +0 -248
  115. oscura/utils/pipeline/parallel.py +0 -449
  116. oscura/utils/pipeline/pipeline.py +0 -375
  117. oscura/utils/search/__init__.py +0 -16
  118. oscura/utils/search/anomaly.py +0 -424
  119. oscura/utils/search/context.py +0 -294
  120. oscura/utils/search/pattern.py +0 -288
  121. oscura/utils/storage/__init__.py +0 -61
  122. oscura/utils/storage/database.py +0 -1166
  123. oscura/visualization/accessibility.py +0 -526
  124. oscura/visualization/annotations.py +0 -371
  125. oscura/visualization/axis_scaling.py +0 -305
  126. oscura/visualization/colors.py +0 -451
  127. oscura/visualization/digital.py +0 -436
  128. oscura/visualization/eye.py +0 -571
  129. oscura/visualization/histogram.py +0 -281
  130. oscura/visualization/interactive.py +0 -1035
  131. oscura/visualization/jitter.py +0 -1042
  132. oscura/visualization/keyboard.py +0 -394
  133. oscura/visualization/layout.py +0 -400
  134. oscura/visualization/optimization.py +0 -1079
  135. oscura/visualization/palettes.py +0 -446
  136. oscura/visualization/power.py +0 -508
  137. oscura/visualization/power_extended.py +0 -955
  138. oscura/visualization/presets.py +0 -469
  139. oscura/visualization/protocols.py +0 -1246
  140. oscura/visualization/render.py +0 -223
  141. oscura/visualization/rendering.py +0 -444
  142. oscura/visualization/reverse_engineering.py +0 -838
  143. oscura/visualization/signal_integrity.py +0 -989
  144. oscura/visualization/specialized.py +0 -643
  145. oscura/visualization/spectral.py +0 -1226
  146. oscura/visualization/thumbnails.py +0 -340
  147. oscura/visualization/time_axis.py +0 -351
  148. oscura/visualization/waveform.py +0 -454
  149. {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/WHEEL +0 -0
  150. {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/entry_points.txt +0 -0
  151. {oscura-0.8.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
- ]