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
@@ -0,0 +1,286 @@
1
+ """Helper functions for creating MeasurementResult instances.
2
+
3
+ This module provides utilities to construct MeasurementResult TypedDict
4
+ instances with proper formatting and applicability tracking.
5
+
6
+ Example:
7
+ >>> from oscura.core.measurement_result import make_measurement, make_inapplicable
8
+ >>> # Applicable measurement
9
+ >>> freq = make_measurement(1000.0, "Hz")
10
+ >>> print(freq["display"])
11
+ 1.000 kHz
12
+
13
+ >>> # Inapplicable measurement
14
+ >>> period = make_inapplicable("s", "Aperiodic signal")
15
+ >>> print(f"{period['display']} - {period['reason']}")
16
+ N/A - Aperiodic signal
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import math
22
+ from typing import TYPE_CHECKING, Any
23
+
24
+ import numpy as np
25
+
26
+ if TYPE_CHECKING:
27
+ from oscura.core.types import MeasurementResult
28
+
29
+
30
+ def format_si_prefix(value: float, unit: str, precision: int = 3) -> str:
31
+ """Format value with appropriate SI prefix.
32
+
33
+ Args:
34
+ value: Numeric value to format.
35
+ unit: Base unit (e.g., "Hz", "V", "s").
36
+ precision: Number of significant figures.
37
+
38
+ Returns:
39
+ Formatted string with SI prefix (e.g., "1.234 kHz", "5.67 mV").
40
+
41
+ Example:
42
+ >>> format_si_prefix(1000, "Hz")
43
+ '1.000 kHz'
44
+ >>> format_si_prefix(0.001, "V")
45
+ '1.000 mV'
46
+ >>> format_si_prefix(1e6, "Hz", precision=2)
47
+ '1.00 MHz'
48
+ """
49
+ if value == 0 or not math.isfinite(value):
50
+ return f"{value:.{precision}g} {unit}"
51
+
52
+ # SI prefixes and their powers of 10
53
+ prefixes = [
54
+ (1e24, "Y"), # yotta
55
+ (1e21, "Z"), # zetta
56
+ (1e18, "E"), # exa
57
+ (1e15, "P"), # peta
58
+ (1e12, "T"), # tera
59
+ (1e9, "G"), # giga
60
+ (1e6, "M"), # mega
61
+ (1e3, "k"), # kilo
62
+ (1, ""), # no prefix
63
+ (1e-3, "m"), # milli
64
+ (1e-6, "µ"), # micro
65
+ (1e-9, "n"), # nano
66
+ (1e-12, "p"), # pico
67
+ (1e-15, "f"), # femto
68
+ (1e-18, "a"), # atto
69
+ (1e-21, "z"), # zepto
70
+ (1e-24, "y"), # yocto
71
+ ]
72
+
73
+ abs_value = abs(value)
74
+
75
+ # Find appropriate prefix
76
+ for scale, prefix in prefixes:
77
+ if abs_value >= scale:
78
+ scaled = value / scale
79
+ return f"{scaled:.{precision}f} {prefix}{unit}"
80
+
81
+ # Fallback for very small values
82
+ return f"{value:.{precision}e} {unit}"
83
+
84
+
85
+ def format_percentage(value: float, precision: int = 2) -> str:
86
+ """Format percentage value.
87
+
88
+ Args:
89
+ value: Percentage value (0-100).
90
+ precision: Decimal places.
91
+
92
+ Returns:
93
+ Formatted percentage string.
94
+
95
+ Example:
96
+ >>> format_percentage(5.2)
97
+ '5.20%'
98
+ >>> format_percentage(0.123, precision=3)
99
+ '0.123%'
100
+ """
101
+ return f"{value:.{precision}f}%"
102
+
103
+
104
+ def format_ratio(value: float, precision: int = 3) -> str:
105
+ """Format ratio value (0-1) as percentage.
106
+
107
+ Args:
108
+ value: Ratio value (0.0 to 1.0).
109
+ precision: Decimal places.
110
+
111
+ Returns:
112
+ Formatted percentage string.
113
+
114
+ Example:
115
+ >>> format_ratio(0.052)
116
+ '5.200%'
117
+ >>> format_ratio(0.5)
118
+ '50.000%'
119
+ """
120
+ return f"{value * 100:.{precision}f}%"
121
+
122
+
123
+ def format_decibel(value: float, precision: int = 1) -> str:
124
+ """Format decibel value.
125
+
126
+ Args:
127
+ value: Decibel value.
128
+ precision: Decimal places.
129
+
130
+ Returns:
131
+ Formatted dB string.
132
+
133
+ Example:
134
+ >>> format_decibel(42.5)
135
+ '42.5 dB'
136
+ >>> format_decibel(-3.01, precision=2)
137
+ '-3.01 dB'
138
+ """
139
+ return f"{value:.{precision}f} dB"
140
+
141
+
142
+ def make_measurement(
143
+ value: float, unit: str, *, precision: int = 3, raw_value: bool = False
144
+ ) -> MeasurementResult:
145
+ """Create an applicable MeasurementResult.
146
+
147
+ Args:
148
+ value: Measurement value.
149
+ unit: Unit string ("V", "Hz", "s", "dB", "%", "ratio", or "").
150
+ precision: Formatting precision (default: 3).
151
+ raw_value: If True, format as raw number (default: False).
152
+
153
+ Returns:
154
+ MeasurementResult with applicable=True.
155
+
156
+ Example:
157
+ >>> result = make_measurement(1000, "Hz")
158
+ >>> result["value"]
159
+ 1000.0
160
+ >>> result["display"]
161
+ '1.000 kHz'
162
+ >>> result["applicable"]
163
+ True
164
+
165
+ >>> # Percentage measurement
166
+ >>> thd = make_measurement(5.2, "%")
167
+ >>> thd["display"]
168
+ '5.200%'
169
+
170
+ >>> # Ratio measurement (auto-converts to percentage)
171
+ >>> duty = make_measurement(0.3, "ratio")
172
+ >>> duty["display"]
173
+ '30.000%'
174
+ """
175
+ # Handle NaN/Inf values
176
+ if not math.isfinite(value):
177
+ return make_inapplicable(unit, f"Invalid value: {value}")
178
+
179
+ # Format display string based on unit type
180
+ if raw_value:
181
+ display = f"{value:.{precision}g}"
182
+ elif unit == "dB":
183
+ display = format_decibel(value, precision=precision)
184
+ elif unit == "%":
185
+ display = format_percentage(value, precision=precision)
186
+ elif unit == "ratio":
187
+ # Convert ratio (0-1) to percentage for display
188
+ display = format_ratio(value, precision=precision)
189
+ elif unit == "":
190
+ # Dimensionless - format as integer or float
191
+ if isinstance(value, (int, np.integer)) or value == int(value):
192
+ display = f"{int(value)}"
193
+ else:
194
+ display = f"{value:.{precision}g}"
195
+ else:
196
+ # SI units (V, Hz, s, A, W, etc.)
197
+ display = format_si_prefix(value, unit, precision=precision)
198
+
199
+ return {
200
+ "value": float(value),
201
+ "unit": unit,
202
+ "applicable": True,
203
+ "reason": None,
204
+ "display": display,
205
+ }
206
+
207
+
208
+ def make_inapplicable(unit: str, reason: str) -> MeasurementResult:
209
+ """Create an inapplicable MeasurementResult (replaces NaN).
210
+
211
+ Args:
212
+ unit: Unit string for metadata.
213
+ reason: Human-readable explanation (e.g., "Aperiodic signal").
214
+
215
+ Returns:
216
+ MeasurementResult with applicable=False, value=None.
217
+
218
+ Example:
219
+ >>> result = make_inapplicable("s", "Aperiodic signal (single impulse)")
220
+ >>> result["value"] is None
221
+ True
222
+ >>> result["applicable"]
223
+ False
224
+ >>> result["display"]
225
+ 'N/A'
226
+ >>> result["reason"]
227
+ 'Aperiodic signal (single impulse)'
228
+ """
229
+ return {
230
+ "value": None,
231
+ "unit": unit,
232
+ "applicable": False,
233
+ "reason": reason,
234
+ "display": "N/A",
235
+ }
236
+
237
+
238
+ def make_measurement_safe(
239
+ value: float | None, unit: str, inapplicable_reason: str | None = None, **kwargs: Any
240
+ ) -> MeasurementResult:
241
+ """Create MeasurementResult with automatic NaN/None handling.
242
+
243
+ Args:
244
+ value: Measurement value (can be NaN or None).
245
+ unit: Unit string.
246
+ inapplicable_reason: Reason if inapplicable (required if value is None/NaN).
247
+ **kwargs: Additional arguments passed to make_measurement().
248
+
249
+ Returns:
250
+ MeasurementResult (applicable if value is valid, inapplicable otherwise).
251
+
252
+ Example:
253
+ >>> # Valid value
254
+ >>> result = make_measurement_safe(1000, "Hz")
255
+ >>> result["applicable"]
256
+ True
257
+
258
+ >>> # NaN value
259
+ >>> result = make_measurement_safe(float('nan'), "s", "Aperiodic signal")
260
+ >>> result["applicable"]
261
+ False
262
+ >>> result["reason"]
263
+ 'Aperiodic signal'
264
+
265
+ >>> # None value
266
+ >>> result = make_measurement_safe(None, "V", "DC signal")
267
+ >>> result["display"]
268
+ 'N/A'
269
+ """
270
+ # Check if value is invalid (None, NaN, Inf)
271
+ if value is None or not math.isfinite(value):
272
+ reason = inapplicable_reason or "Invalid or undefined value"
273
+ return make_inapplicable(unit, reason)
274
+
275
+ return make_measurement(value, unit, **kwargs)
276
+
277
+
278
+ __all__ = [
279
+ "format_decibel",
280
+ "format_percentage",
281
+ "format_ratio",
282
+ "format_si_prefix",
283
+ "make_inapplicable",
284
+ "make_measurement",
285
+ "make_measurement_safe",
286
+ ]
oscura/core/progress.py CHANGED
@@ -29,7 +29,7 @@ try:
29
29
 
30
30
  _HAS_PSUTIL = True
31
31
  except ImportError:
32
- psutil = None # type: ignore[assignment]
32
+ psutil = None
33
33
  _HAS_PSUTIL = False
34
34
 
35
35
  if TYPE_CHECKING:
@@ -149,20 +149,14 @@
149
149
  "type": "array",
150
150
  "description": "Device IDs to include (whitelist)",
151
151
  "items": {
152
- "oneOf": [
153
- { "type": "integer" },
154
- { "type": "string", "pattern": "^0[xX][0-9A-Fa-f]+$" }
155
- ]
152
+ "oneOf": [{ "type": "integer" }, { "type": "string", "pattern": "^0[xX][0-9A-Fa-f]+$" }]
156
153
  }
157
154
  },
158
155
  "exclude_devices": {
159
156
  "type": "array",
160
157
  "description": "Device IDs to exclude (blacklist)",
161
158
  "items": {
162
- "oneOf": [
163
- { "type": "integer" },
164
- { "type": "string", "pattern": "^0[xX][0-9A-Fa-f]+$" }
165
- ]
159
+ "oneOf": [{ "type": "integer" }, { "type": "string", "pattern": "^0[xX][0-9A-Fa-f]+$" }]
166
160
  }
167
161
  },
168
162
  "include_categories": {
@@ -118,10 +118,7 @@
118
118
  },
119
119
  "value": {
120
120
  "description": "Expected constant value for validation",
121
- "oneOf": [
122
- { "type": "integer" },
123
- { "type": "array", "items": { "type": "integer" } }
124
- ]
121
+ "oneOf": [{ "type": "integer" }, { "type": "array", "items": { "type": "integer" } }]
125
122
  },
126
123
  "description": {
127
124
  "type": "string",
@@ -188,18 +185,7 @@
188
185
  },
189
186
  "type": {
190
187
  "type": "string",
191
- "enum": [
192
- "uint8",
193
- "uint16",
194
- "uint32",
195
- "uint64",
196
- "int8",
197
- "int16",
198
- "int32",
199
- "int64",
200
- "float32",
201
- "float64"
202
- ],
188
+ "enum": ["uint8", "uint16", "uint32", "uint64", "int8", "int16", "int32", "int64", "float32", "float64"],
203
189
  "description": "Sample data type"
204
190
  },
205
191
  "endian": {
@@ -303,10 +289,7 @@
303
289
  },
304
290
  "expected": {
305
291
  "description": "Expected value",
306
- "oneOf": [
307
- { "type": "integer" },
308
- { "type": "array", "items": { "type": "integer" } }
309
- ]
292
+ "oneOf": [{ "type": "integer" }, { "type": "array", "items": { "type": "integer" } }]
310
293
  },
311
294
  "on_failure": {
312
295
  "type": "string",
@@ -379,10 +362,7 @@
379
362
  },
380
363
  "pattern": {
381
364
  "description": "Idle pattern to detect",
382
- "oneOf": [
383
- { "type": "string", "enum": ["auto", "zeros", "ones"] },
384
- { "type": "integer" }
385
- ]
365
+ "oneOf": [{ "type": "string", "enum": ["auto", "zeros", "ones"] }, { "type": "integer" }]
386
366
  },
387
367
  "min_duration": {
388
368
  "type": "integer",
@@ -241,12 +241,7 @@
241
241
  },
242
242
  "value": {
243
243
  "description": "Expected constant value for validation",
244
- "oneOf": [
245
- { "type": "integer" },
246
- { "type": "number" },
247
- { "type": "string" },
248
- { "type": "array" }
249
- ]
244
+ "oneOf": [{ "type": "integer" }, { "type": "number" }, { "type": "string" }, { "type": "array" }]
250
245
  },
251
246
  "condition": {
252
247
  "type": "string",
@@ -331,12 +326,7 @@
331
326
  },
332
327
  "expected": {
333
328
  "description": "Expected value",
334
- "oneOf": [
335
- { "type": "integer" },
336
- { "type": "number" },
337
- { "type": "string" },
338
- { "type": "array" }
339
- ]
329
+ "oneOf": [{ "type": "integer" }, { "type": "number" }, { "type": "string" }, { "type": "array" }]
340
330
  },
341
331
  "on_mismatch": {
342
332
  "type": "string",