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
@@ -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: