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,451 +0,0 @@
1
- """Color palette selection and accessibility utilities.
2
-
3
- This module provides intelligent color palette selection based on data
4
- characteristics and accessibility requirements with WCAG contrast checking.
5
-
6
-
7
- Example:
8
- >>> from oscura.visualization.colors import select_optimal_palette
9
- >>> colors = select_optimal_palette(n_channels=3, palette_type="qualitative")
10
-
11
- References:
12
- WCAG 2.1 contrast guidelines
13
- Colorblind-safe palette design (Brettel 1997)
14
- ColorBrewer schemes
15
- """
16
-
17
- from typing import Literal
18
-
19
- import numpy as np
20
-
21
- # Predefined colorblind-safe palettes
22
- COLORBLIND_SAFE_QUALITATIVE = [
23
- "#0173B2", # Blue
24
- "#DE8F05", # Orange
25
- "#029E73", # Green
26
- "#CC78BC", # Purple
27
- "#CA9161", # Brown
28
- "#949494", # Gray
29
- "#ECE133", # Yellow
30
- "#56B4E9", # Light blue
31
- ]
32
-
33
- SEQUENTIAL_VIRIDIS = [
34
- "#440154",
35
- "#481567",
36
- "#482677",
37
- "#453781",
38
- "#404788",
39
- "#39568C",
40
- "#33638D",
41
- "#2D708E",
42
- "#287D8E",
43
- "#238A8D",
44
- "#1F968B",
45
- "#20A387",
46
- "#29AF7F",
47
- "#3CBB75",
48
- "#55C667",
49
- "#73D055",
50
- "#95D840",
51
- "#B8DE29",
52
- "#DCE319",
53
- "#FDE724",
54
- ]
55
-
56
- DIVERGING_COOLWARM = [
57
- "#3B4CC0",
58
- "#5977E3",
59
- "#7D9EF2",
60
- "#A2C0F9",
61
- "#C7DDFA",
62
- "#E8F0FC",
63
- "#F9EBE5",
64
- "#F6CFBB",
65
- "#F0AD8E",
66
- "#E68462",
67
- "#D8583E",
68
- "#C52A1E",
69
- "#B40426",
70
- ]
71
-
72
-
73
- def select_optimal_palette(
74
- n_colors: int,
75
- *,
76
- palette_type: Literal["sequential", "diverging", "qualitative"] | None = None,
77
- data_range: tuple[float, float] | None = None,
78
- colorblind_safe: bool = True,
79
- background_color: str = "#FFFFFF",
80
- min_contrast_ratio: float = 4.5,
81
- ) -> list[str]:
82
- """Select optimal color palette based on data characteristics.
83
-
84
- : Automatically select optimal color palettes based on
85
- data characteristics, plot type, and accessibility requirements.
86
-
87
- Args:
88
- n_colors: Number of colors needed
89
- palette_type: Type of palette ("sequential", "diverging", "qualitative")
90
- If None, auto-select based on n_colors and data_range
91
- data_range: Data range (min, max) for auto-detecting bipolar signals
92
- colorblind_safe: Ensure colorblind-safe palette (default: True)
93
- background_color: Background color for contrast checking (default: white)
94
- min_contrast_ratio: Minimum WCAG contrast ratio (default: 4.5 for AA)
95
-
96
- Returns:
97
- List of color hex codes
98
-
99
- Raises:
100
- ValueError: If n_colors is invalid or palette cannot meet requirements
101
-
102
- Example:
103
- >>> # Auto-select for 3 channels
104
- >>> colors = select_optimal_palette(3)
105
- >>> # Diverging palette for bipolar data
106
- >>> colors = select_optimal_palette(10, palette_type="diverging")
107
-
108
- References:
109
- VIS-023: Data-Driven Color Palette
110
- WCAG 2.1 contrast ratio guidelines (AA: 4.5:1, AAA: 7:1)
111
- ColorBrewer sequential/diverging schemes
112
- """
113
- if n_colors < 1:
114
- raise ValueError("n_colors must be >= 1")
115
- if min_contrast_ratio < 1.0:
116
- raise ValueError("min_contrast_ratio must be >= 1.0")
117
-
118
- # Auto-select palette type if not specified
119
- if palette_type is None:
120
- palette_type = _auto_select_palette_type(n_colors, data_range)
121
-
122
- # Select base palette
123
- if palette_type == "qualitative":
124
- base_colors = (
125
- COLORBLIND_SAFE_QUALITATIVE if colorblind_safe else _generate_qualitative(n_colors)
126
- )
127
- elif palette_type == "sequential":
128
- base_colors = SEQUENTIAL_VIRIDIS
129
- elif palette_type == "diverging":
130
- base_colors = DIVERGING_COOLWARM
131
- else:
132
- raise ValueError(f"Unknown palette_type: {palette_type}")
133
-
134
- # Sample colors if we need fewer than available
135
- if n_colors <= len(base_colors):
136
- # Evenly sample from palette
137
- indices = np.linspace(0, len(base_colors) - 1, n_colors).astype(int)
138
- colors = [base_colors[i] for i in indices]
139
- else:
140
- # Interpolate if we need more colors
141
- colors = _interpolate_colors(base_colors, n_colors)
142
-
143
- # Check contrast ratios
144
- colors_with_contrast = []
145
- bg_luminance = _relative_luminance(background_color)
146
-
147
- for color in colors:
148
- color_luminance = _relative_luminance(color)
149
- contrast = _contrast_ratio(color_luminance, bg_luminance)
150
-
151
- if contrast >= min_contrast_ratio:
152
- colors_with_contrast.append(color)
153
- else:
154
- # Adjust lightness to meet contrast requirement
155
- adjusted = _adjust_for_contrast(color, background_color, min_contrast_ratio)
156
- colors_with_contrast.append(adjusted)
157
-
158
- return colors_with_contrast
159
-
160
-
161
- def _auto_select_palette_type(
162
- n_colors: int,
163
- data_range: tuple[float, float] | None,
164
- ) -> Literal["sequential", "diverging", "qualitative"]:
165
- """Auto-select palette type based on data characteristics.
166
-
167
- Args:
168
- n_colors: Number of colors needed
169
- data_range: Data range (min, max)
170
-
171
- Returns:
172
- Palette type
173
- """
174
- # Check for bipolar data (zero-crossing)
175
- if data_range is not None:
176
- min_val, max_val = data_range
177
- if min_val < 0 and max_val > 0:
178
- # Bipolar signal - use diverging
179
- return "diverging"
180
-
181
- # Multi-channel (distinct categories)
182
- if n_colors <= 8:
183
- return "qualitative"
184
-
185
- # Many colors or continuous data
186
- return "sequential"
187
-
188
-
189
- def _relative_luminance(color: str) -> float:
190
- """Calculate relative luminance per WCAG 2.1.
191
-
192
- Args:
193
- color: Hex color code
194
-
195
- Returns:
196
- Relative luminance (0-1)
197
- """
198
- # Parse hex color
199
- color = color.removeprefix("#")
200
-
201
- r = int(color[0:2], 16) / 255.0
202
- g = int(color[2:4], 16) / 255.0
203
- b = int(color[4:6], 16) / 255.0
204
-
205
- # Convert to linear RGB
206
- def to_linear(c: float) -> float:
207
- if c <= 0.03928:
208
- return c / 12.92
209
- else:
210
- return ((c + 0.055) / 1.055) ** 2.4 # type: ignore[no-any-return]
211
-
212
- r_linear = to_linear(r)
213
- g_linear = to_linear(g)
214
- b_linear = to_linear(b)
215
-
216
- # Calculate luminance
217
- return 0.2126 * r_linear + 0.7152 * g_linear + 0.0722 * b_linear
218
-
219
-
220
- def _contrast_ratio(lum1: float, lum2: float) -> float:
221
- """Calculate WCAG contrast ratio between two luminances.
222
-
223
- Args:
224
- lum1: First luminance (0-1)
225
- lum2: Second luminance (0-1)
226
-
227
- Returns:
228
- Contrast ratio (1-21)
229
- """
230
- lighter = max(lum1, lum2)
231
- darker = min(lum1, lum2)
232
-
233
- return (lighter + 0.05) / (darker + 0.05)
234
-
235
-
236
- def _adjust_for_contrast(
237
- color: str,
238
- background: str,
239
- target_ratio: float,
240
- ) -> str:
241
- """Adjust color lightness to meet contrast requirement.
242
-
243
- Args:
244
- color: Color to adjust
245
- background: Background color
246
- target_ratio: Target contrast ratio
247
-
248
- Returns:
249
- Adjusted color hex code
250
- """
251
- # Parse color
252
- color_val = color.removeprefix("#")
253
-
254
- r = int(color_val[0:2], 16)
255
- g = int(color_val[2:4], 16)
256
- b = int(color_val[4:6], 16)
257
-
258
- # Convert to HSL for easier lightness adjustment
259
- h, s, l = _rgb_to_hsl(r, g, b)
260
-
261
- bg_lum = _relative_luminance(background)
262
-
263
- # Binary search for appropriate lightness
264
- l_min, l_max = 0.0, 1.0
265
- iterations = 0
266
- max_iterations = 20
267
-
268
- while iterations < max_iterations:
269
- # Try current lightness
270
- test_r, test_g, test_b = _hsl_to_rgb(h, s, l)
271
- test_color = f"#{test_r:02x}{test_g:02x}{test_b:02x}"
272
- test_lum = _relative_luminance(test_color)
273
- ratio = _contrast_ratio(test_lum, bg_lum)
274
-
275
- if abs(ratio - target_ratio) < 0.1:
276
- break
277
-
278
- if ratio < target_ratio:
279
- # Need more contrast - adjust lightness
280
- if bg_lum > 0.5:
281
- # Dark background - make lighter
282
- l_min = l
283
- l = (l + l_max) / 2
284
- else:
285
- # Light background - make darker
286
- l_max = l
287
- l = (l_min + l) / 2
288
- # Too much contrast - move back
289
- elif bg_lum > 0.5:
290
- l_max = l
291
- l = (l_min + l) / 2
292
- else:
293
- l_min = l
294
- l = (l + l_max) / 2
295
-
296
- iterations += 1
297
-
298
- final_r, final_g, final_b = _hsl_to_rgb(h, s, l)
299
- return f"#{final_r:02x}{final_g:02x}{final_b:02x}"
300
-
301
-
302
- def _rgb_to_hsl(r: int, g: int, b: int) -> tuple[float, float, float]:
303
- """Convert RGB to HSL color space.
304
-
305
- Args:
306
- r: Red value (0-255).
307
- g: Green value (0-255).
308
- b: Blue value (0-255).
309
-
310
- Returns:
311
- (h, s, l) tuple where h in [0, 360), s and l in [0, 1]
312
- """
313
- r_norm = r / 255.0
314
- g_norm = g / 255.0
315
- b_norm = b / 255.0
316
-
317
- max_c = max(r_norm, g_norm, b_norm)
318
- min_c = min(r_norm, g_norm, b_norm)
319
- delta = max_c - min_c
320
-
321
- # Lightness
322
- l = (max_c + min_c) / 2.0
323
-
324
- if delta == 0:
325
- # Achromatic
326
- return (0.0, 0.0, l)
327
-
328
- # Saturation
329
- s = delta / (max_c + min_c) if l < 0.5 else delta / (2.0 - max_c - min_c)
330
-
331
- # Hue
332
- if max_c == r_norm:
333
- h = ((g_norm - b_norm) / delta) % 6
334
- elif max_c == g_norm:
335
- h = ((b_norm - r_norm) / delta) + 2
336
- else:
337
- h = ((r_norm - g_norm) / delta) + 4
338
-
339
- h = h * 60.0
340
-
341
- return (h, s, l)
342
-
343
-
344
- def _hsl_to_rgb(h: float, s: float, l: float) -> tuple[int, int, int]:
345
- """Convert HSL to RGB color space.
346
-
347
- Args:
348
- h: Hue in [0, 360)
349
- s: Saturation in [0, 1]
350
- l: Lightness in [0, 1]
351
-
352
- Returns:
353
- (r, g, b) tuple with values in [0, 255]
354
- """
355
- if s == 0:
356
- # Achromatic
357
- gray = int(l * 255)
358
- return (gray, gray, gray)
359
-
360
- def hue_to_rgb(p: float, q: float, t: float) -> float:
361
- if t < 0:
362
- t += 1
363
- if t > 1:
364
- t -= 1
365
- if t < 1 / 6:
366
- return p + (q - p) * 6 * t
367
- if t < 1 / 2:
368
- return q
369
- if t < 2 / 3:
370
- return p + (q - p) * (2 / 3 - t) * 6
371
- return p
372
-
373
- q = l * (1 + s) if l < 0.5 else l + s - l * s
374
-
375
- p = 2 * l - q
376
-
377
- h_norm = h / 360.0
378
-
379
- r = hue_to_rgb(p, q, h_norm + 1 / 3)
380
- g = hue_to_rgb(p, q, h_norm)
381
- b = hue_to_rgb(p, q, h_norm - 1 / 3)
382
-
383
- return (int(r * 255), int(g * 255), int(b * 255))
384
-
385
-
386
- def _generate_qualitative(n_colors: int) -> list[str]:
387
- """Generate qualitative color palette.
388
-
389
- Args:
390
- n_colors: Number of colors
391
-
392
- Returns:
393
- List of hex color codes
394
- """
395
- # Generate evenly spaced hues
396
- colors = []
397
- for i in range(n_colors):
398
- hue = (i * 360.0 / n_colors) % 360
399
- r, g, b = _hsl_to_rgb(hue, 0.7, 0.5)
400
- colors.append(f"#{r:02x}{g:02x}{b:02x}")
401
-
402
- return colors
403
-
404
-
405
- def _interpolate_colors(base_colors: list[str], n_colors: int) -> list[str]:
406
- """Interpolate between base colors to generate more colors.
407
-
408
- Args:
409
- base_colors: Base color palette
410
- n_colors: Target number of colors
411
-
412
- Returns:
413
- List of interpolated hex color codes
414
- """
415
- if n_colors <= len(base_colors):
416
- return base_colors[:n_colors]
417
-
418
- # Convert to RGB arrays
419
- rgb_array = np.zeros((len(base_colors), 3))
420
- for i, color in enumerate(base_colors):
421
- color = color.removeprefix("#")
422
- rgb_array[i, 0] = int(color[0:2], 16)
423
- rgb_array[i, 1] = int(color[2:4], 16)
424
- rgb_array[i, 2] = int(color[4:6], 16)
425
-
426
- # Interpolate
427
- indices = np.linspace(0, len(base_colors) - 1, n_colors)
428
- interp_rgb = np.zeros((n_colors, 3))
429
-
430
- for channel in range(3):
431
- interp_rgb[:, channel] = np.interp(
432
- indices, np.arange(len(base_colors)), rgb_array[:, channel]
433
- )
434
-
435
- # Convert back to hex
436
- colors = []
437
- for i in range(n_colors):
438
- r = int(interp_rgb[i, 0])
439
- g = int(interp_rgb[i, 1])
440
- b = int(interp_rgb[i, 2])
441
- colors.append(f"#{r:02x}{g:02x}{b:02x}")
442
-
443
- return colors
444
-
445
-
446
- __all__ = [
447
- "COLORBLIND_SAFE_QUALITATIVE",
448
- "DIVERGING_COOLWARM",
449
- "SEQUENTIAL_VIRIDIS",
450
- "select_optimal_palette",
451
- ]