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
oscura/convenience.py CHANGED
@@ -11,6 +11,14 @@ Example:
11
11
  >>> metrics = osc.quick_spectral(trace, fundamental=1000)
12
12
  >>> print(f"THD: {metrics.thd_db:.1f} dB, SNR: {metrics.snr_db:.1f} dB")
13
13
  >>>
14
+ >>> # One-call power analysis
15
+ >>> power_metrics = osc.quick_power(v_trace, i_trace, frequency=60)
16
+ >>> print(f"Power Factor: {power_metrics.power_factor_value:.3f}")
17
+ >>>
18
+ >>> # One-call timing analysis
19
+ >>> timing = osc.quick_timing(trace)
20
+ >>> print(f"Rise Time: {timing.rise_time_seconds:.2e} s")
21
+ >>>
14
22
  >>> # Auto-detect and decode protocol
15
23
  >>> result = osc.auto_decode(trace)
16
24
  >>> print(f"Protocol: {result.protocol}, Frames: {len(result.frames)}")
@@ -21,6 +29,8 @@ Example:
21
29
  References:
22
30
  - Oscura API Design Guidelines
23
31
  - IEEE 1241-2010 (ADC Characterization)
32
+ - IEEE 181-2011 (Transitional Waveform Definitions)
33
+ - IEEE 1459-2010 (Power Quality Definitions)
24
34
  """
25
35
 
26
36
  from __future__ import annotations
@@ -31,35 +41,242 @@ from typing import TYPE_CHECKING, Any, Literal
31
41
  import numpy as np
32
42
 
33
43
  if TYPE_CHECKING:
34
- from oscura.core.types import DigitalTrace, WaveformTrace
44
+ from oscura.core.types import DigitalTrace, MeasurementResult, WaveformTrace
35
45
 
36
46
 
37
47
  @dataclass
38
48
  class SpectralMetrics:
39
49
  """Results from quick_spectral analysis.
40
50
 
51
+ All measurements are stored as MeasurementResult objects for proper
52
+ error handling and metadata tracking.
53
+
41
54
  Attributes:
42
- thd_db: Total Harmonic Distortion in dB.
43
- thd_percent: Total Harmonic Distortion as percentage.
44
- snr_db: Signal-to-Noise Ratio in dB.
45
- sinad_db: Signal-to-Noise and Distortion in dB.
46
- enob: Effective Number of Bits.
47
- sfdr_db: Spurious-Free Dynamic Range in dBc.
55
+ thd: Total Harmonic Distortion MeasurementResult.
56
+ snr: Signal-to-Noise Ratio MeasurementResult.
57
+ sinad: Signal-to-Noise and Distortion MeasurementResult.
58
+ enob: Effective Number of Bits MeasurementResult.
59
+ sfdr: Spurious-Free Dynamic Range MeasurementResult.
48
60
  fundamental_freq: Detected fundamental frequency in Hz.
49
61
  fundamental_mag_db: Fundamental magnitude in dB.
50
62
  noise_floor_db: Estimated noise floor in dB.
51
63
  """
52
64
 
53
- thd_db: float
54
- thd_percent: float
55
- snr_db: float
56
- sinad_db: float
57
- enob: float
58
- sfdr_db: float
65
+ thd: MeasurementResult
66
+ snr: MeasurementResult
67
+ sinad: MeasurementResult
68
+ enob: MeasurementResult
69
+ sfdr: MeasurementResult
59
70
  fundamental_freq: float
60
71
  fundamental_mag_db: float
61
72
  noise_floor_db: float
62
73
 
74
+ @property
75
+ def thd_db(self) -> float:
76
+ """Extract THD value in dB (or NaN if not applicable)."""
77
+ if self.thd["applicable"] and self.thd["value"] is not None:
78
+ thd_pct = self.thd["value"]
79
+ # Convert percentage to dB: dB = 20 * log10(ratio) where ratio = thd_pct / 100
80
+ return float(20 * np.log10(thd_pct / 100)) if thd_pct > 0 else -np.inf
81
+ return np.nan
82
+
83
+ @property
84
+ def thd_percent(self) -> float:
85
+ """Extract THD value as percentage (or NaN if not applicable)."""
86
+ return float(self.thd["value"]) if self.thd["applicable"] and self.thd["value"] else np.nan
87
+
88
+ @property
89
+ def snr_db(self) -> float:
90
+ """Extract SNR value in dB (or NaN if not applicable)."""
91
+ return float(self.snr["value"]) if self.snr["applicable"] and self.snr["value"] else np.nan
92
+
93
+ @property
94
+ def sinad_db(self) -> float:
95
+ """Extract SINAD value in dB (or NaN if not applicable)."""
96
+ return (
97
+ float(self.sinad["value"])
98
+ if self.sinad["applicable"] and self.sinad["value"]
99
+ else np.nan
100
+ )
101
+
102
+ @property
103
+ def enob_value(self) -> float:
104
+ """Extract ENOB value (or NaN if not applicable)."""
105
+ return (
106
+ float(self.enob["value"]) if self.enob["applicable"] and self.enob["value"] else np.nan
107
+ )
108
+
109
+ @property
110
+ def sfdr_db(self) -> float:
111
+ """Extract SFDR value in dB (or NaN if not applicable)."""
112
+ return (
113
+ float(self.sfdr["value"]) if self.sfdr["applicable"] and self.sfdr["value"] else np.nan
114
+ )
115
+
116
+
117
+ @dataclass
118
+ class PowerMetrics:
119
+ """Results from quick_power analysis per IEEE 1459.
120
+
121
+ All measurements are stored as MeasurementResult objects for proper
122
+ error handling and metadata tracking.
123
+
124
+ Attributes:
125
+ rms_voltage: RMS voltage MeasurementResult.
126
+ rms_current: RMS current MeasurementResult (None if single trace).
127
+ active_power: Active power (P) MeasurementResult.
128
+ reactive_power: Reactive power (Q) MeasurementResult (None if single trace).
129
+ apparent_power: Apparent power (S) MeasurementResult (None if single trace).
130
+ power_factor: Power factor MeasurementResult (None if single trace).
131
+ """
132
+
133
+ rms_voltage: MeasurementResult
134
+ rms_current: MeasurementResult | None
135
+ active_power: MeasurementResult
136
+ reactive_power: MeasurementResult | None
137
+ apparent_power: MeasurementResult | None
138
+ power_factor: MeasurementResult | None
139
+
140
+ @property
141
+ def rms_voltage_value(self) -> float:
142
+ """Extract RMS voltage value (or NaN if not applicable)."""
143
+ return (
144
+ float(self.rms_voltage["value"])
145
+ if self.rms_voltage["applicable"] and self.rms_voltage["value"]
146
+ else np.nan
147
+ )
148
+
149
+ @property
150
+ def rms_current_value(self) -> float:
151
+ """Extract RMS current value (or NaN if not applicable)."""
152
+ if self.rms_current is None:
153
+ return np.nan
154
+ return (
155
+ float(self.rms_current["value"])
156
+ if self.rms_current["applicable"] and self.rms_current["value"]
157
+ else np.nan
158
+ )
159
+
160
+ @property
161
+ def active_power_value(self) -> float:
162
+ """Extract active power value (or NaN if not applicable)."""
163
+ return (
164
+ float(self.active_power["value"])
165
+ if self.active_power["applicable"] and self.active_power["value"]
166
+ else np.nan
167
+ )
168
+
169
+ @property
170
+ def reactive_power_value(self) -> float:
171
+ """Extract reactive power value (or NaN if not applicable)."""
172
+ if self.reactive_power is None:
173
+ return np.nan
174
+ return (
175
+ float(self.reactive_power["value"])
176
+ if self.reactive_power["applicable"] and self.reactive_power["value"]
177
+ else np.nan
178
+ )
179
+
180
+ @property
181
+ def apparent_power_value(self) -> float:
182
+ """Extract apparent power value (or NaN if not applicable)."""
183
+ if self.apparent_power is None:
184
+ return np.nan
185
+ return (
186
+ float(self.apparent_power["value"])
187
+ if self.apparent_power["applicable"] and self.apparent_power["value"]
188
+ else np.nan
189
+ )
190
+
191
+ @property
192
+ def power_factor_value(self) -> float:
193
+ """Extract power factor value (or NaN if not applicable)."""
194
+ if self.power_factor is None:
195
+ return np.nan
196
+ return (
197
+ float(self.power_factor["value"])
198
+ if self.power_factor["applicable"] and self.power_factor["value"]
199
+ else np.nan
200
+ )
201
+
202
+
203
+ @dataclass
204
+ class TimingMetrics:
205
+ """Results from quick_timing analysis per IEEE 181.
206
+
207
+ All measurements are stored as MeasurementResult objects for proper
208
+ error handling and metadata tracking.
209
+
210
+ Attributes:
211
+ rise_time: Rise time (10%-90%) MeasurementResult.
212
+ fall_time: Fall time (90%-10%) MeasurementResult.
213
+ period: Signal period MeasurementResult.
214
+ frequency: Signal frequency MeasurementResult.
215
+ duty_cycle: Duty cycle MeasurementResult.
216
+ pulse_width: Pulse width MeasurementResult.
217
+ """
218
+
219
+ rise_time: MeasurementResult
220
+ fall_time: MeasurementResult
221
+ period: MeasurementResult
222
+ frequency: MeasurementResult
223
+ duty_cycle: MeasurementResult
224
+ pulse_width: MeasurementResult
225
+
226
+ @property
227
+ def rise_time_seconds(self) -> float:
228
+ """Extract rise time value in seconds (or NaN if not applicable)."""
229
+ return (
230
+ float(self.rise_time["value"])
231
+ if self.rise_time["applicable"] and self.rise_time["value"]
232
+ else np.nan
233
+ )
234
+
235
+ @property
236
+ def fall_time_seconds(self) -> float:
237
+ """Extract fall time value in seconds (or NaN if not applicable)."""
238
+ return (
239
+ float(self.fall_time["value"])
240
+ if self.fall_time["applicable"] and self.fall_time["value"]
241
+ else np.nan
242
+ )
243
+
244
+ @property
245
+ def period_seconds(self) -> float:
246
+ """Extract period value in seconds (or NaN if not applicable)."""
247
+ return (
248
+ float(self.period["value"])
249
+ if self.period["applicable"] and self.period["value"]
250
+ else np.nan
251
+ )
252
+
253
+ @property
254
+ def frequency_hz(self) -> float:
255
+ """Extract frequency value in Hz (or NaN if not applicable)."""
256
+ return (
257
+ float(self.frequency["value"])
258
+ if self.frequency["applicable"] and self.frequency["value"]
259
+ else np.nan
260
+ )
261
+
262
+ @property
263
+ def duty_cycle_ratio(self) -> float:
264
+ """Extract duty cycle value as ratio (or NaN if not applicable)."""
265
+ return (
266
+ float(self.duty_cycle["value"])
267
+ if self.duty_cycle["applicable"] and self.duty_cycle["value"]
268
+ else np.nan
269
+ )
270
+
271
+ @property
272
+ def pulse_width_seconds(self) -> float:
273
+ """Extract pulse width value in seconds (or NaN if not applicable)."""
274
+ return (
275
+ float(self.pulse_width["value"])
276
+ if self.pulse_width["applicable"] and self.pulse_width["value"]
277
+ else np.nan
278
+ )
279
+
63
280
 
64
281
  @dataclass
65
282
  class DecodeResult:
@@ -103,14 +320,14 @@ def quick_spectral(
103
320
  window: Window function for FFT (default "hann").
104
321
 
105
322
  Returns:
106
- SpectralMetrics with all computed values.
323
+ SpectralMetrics with all computed values as MeasurementResults.
107
324
 
108
325
  Example:
109
326
  >>> trace = osc.load("audio_1khz.wfm")
110
327
  >>> metrics = osc.quick_spectral(trace, fundamental=1000)
111
328
  >>> print(f"THD: {metrics.thd_db:.1f} dB")
112
329
  >>> print(f"SNR: {metrics.snr_db:.1f} dB")
113
- >>> print(f"ENOB: {metrics.enob:.1f} bits")
330
+ >>> print(f"ENOB: {metrics.enob_value:.1f} bits")
114
331
 
115
332
  References:
116
333
  IEEE 1241-2010 Section 4.1 (ADC Characterization)
@@ -145,59 +362,210 @@ def quick_spectral(
145
362
  noise_bins = mag_db[10 : len(mag_db) // 2]
146
363
  noise_floor = float(np.median(noise_bins))
147
364
 
148
- # Compute metrics (these functions don't accept 'fundamental' parameter)
149
- thd_db_val = thd(trace, n_harmonics=n_harmonics, window=window)
150
- thd_pct = 100 * 10 ** (thd_db_val / 20) if not np.isnan(thd_db_val) else np.nan
151
- snr_db_val = snr(trace, n_harmonics=n_harmonics, window=window)
152
- sinad_db_val = sinad(trace, window=window)
153
- enob_val = enob(trace, window=window)
154
- sfdr_db_val = sfdr(trace, window=window)
365
+ # Compute metrics (these functions now return MeasurementResult dicts)
366
+ thd_result = thd(trace, n_harmonics=n_harmonics, window=window)
367
+ snr_result = snr(trace, n_harmonics=n_harmonics, window=window)
368
+ sinad_result = sinad(trace, window=window)
369
+ enob_result = enob(trace, window=window)
370
+ sfdr_result = sfdr(trace, window=window)
155
371
 
156
372
  return SpectralMetrics(
157
- thd_db=thd_db_val,
158
- thd_percent=thd_pct,
159
- snr_db=snr_db_val,
160
- sinad_db=sinad_db_val,
161
- enob=enob_val,
162
- sfdr_db=sfdr_db_val,
373
+ thd=thd_result,
374
+ snr=snr_result,
375
+ sinad=sinad_result,
376
+ enob=enob_result,
377
+ sfdr=sfdr_result,
163
378
  fundamental_freq=fundamental,
164
379
  fundamental_mag_db=fundamental_mag,
165
380
  noise_floor_db=noise_floor,
166
381
  )
167
382
 
168
383
 
384
+ def quick_power(
385
+ voltage_trace: WaveformTrace,
386
+ current_trace: WaveformTrace | None = None,
387
+ frequency: float | None = None,
388
+ ) -> PowerMetrics:
389
+ """One-call power analysis per IEEE 1459.
390
+
391
+ Computes RMS voltage/current, active/reactive/apparent power, and power
392
+ factor in a single call. If only voltage trace is provided, computes
393
+ voltage-only metrics.
394
+
395
+ Args:
396
+ voltage_trace: Voltage waveform trace.
397
+ current_trace: Current waveform trace (optional).
398
+ frequency: Fundamental frequency in Hz (optional, for AC analysis).
399
+
400
+ Returns:
401
+ PowerMetrics with all computed values as MeasurementResults.
402
+
403
+ Example:
404
+ >>> # AC power analysis with voltage and current
405
+ >>> metrics = osc.quick_power(v_trace, i_trace, frequency=60)
406
+ >>> print(f"Active Power: {metrics.active_power_value:.2f} W")
407
+ >>> print(f"Power Factor: {metrics.power_factor_value:.3f}")
408
+ >>>
409
+ >>> # Voltage-only analysis
410
+ >>> metrics = osc.quick_power(v_trace)
411
+ >>> print(f"RMS Voltage: {metrics.rms_voltage_value:.2f} V")
412
+
413
+ References:
414
+ IEEE 1459-2010 (Power Quality Definitions)
415
+ """
416
+ from oscura.analyzers.waveform.measurements import rms as waveform_rms
417
+ from oscura.core.measurement_result import make_measurement
418
+
419
+ # Compute RMS voltage
420
+ rms_v = waveform_rms(voltage_trace)
421
+
422
+ # Single trace mode: voltage-only metrics
423
+ if current_trace is None:
424
+ return PowerMetrics(
425
+ rms_voltage=rms_v,
426
+ rms_current=None,
427
+ active_power=make_measurement(0.0, "W"), # No power without current
428
+ reactive_power=None,
429
+ apparent_power=None,
430
+ power_factor=None,
431
+ )
432
+
433
+ # Two-trace mode: full power analysis
434
+ from oscura.analyzers.power.ac_power import (
435
+ apparent_power as calc_apparent_power,
436
+ )
437
+ from oscura.analyzers.power.ac_power import (
438
+ power_factor as calc_power_factor,
439
+ )
440
+ from oscura.analyzers.power.ac_power import (
441
+ reactive_power as calc_reactive_power,
442
+ )
443
+ from oscura.analyzers.power.basic import average_power
444
+
445
+ # Compute RMS current
446
+ rms_i = waveform_rms(current_trace)
447
+
448
+ # Compute power metrics
449
+ p_active = average_power(voltage=voltage_trace, current=current_trace)
450
+ q_reactive = calc_reactive_power(voltage_trace, current_trace, frequency=frequency)
451
+ s_apparent = calc_apparent_power(voltage_trace, current_trace)
452
+ pf = calc_power_factor(voltage_trace, current_trace)
453
+
454
+ return PowerMetrics(
455
+ rms_voltage=rms_v,
456
+ rms_current=rms_i,
457
+ active_power=make_measurement(p_active, "W"),
458
+ reactive_power=make_measurement(q_reactive, "VAR"),
459
+ apparent_power=make_measurement(s_apparent, "VA"),
460
+ power_factor=make_measurement(pf, "ratio"),
461
+ )
462
+
463
+
464
+ def quick_timing(trace: WaveformTrace) -> TimingMetrics:
465
+ """One-call timing analysis per IEEE 181.
466
+
467
+ Computes rise/fall time, period, frequency, duty cycle, and pulse width
468
+ in a single call with proper error handling.
469
+
470
+ Args:
471
+ trace: Input waveform trace.
472
+
473
+ Returns:
474
+ TimingMetrics with all computed values as MeasurementResults.
475
+
476
+ Example:
477
+ >>> trace = osc.load("pulse_capture.wfm")
478
+ >>> metrics = osc.quick_timing(trace)
479
+ >>> print(f"Rise Time: {metrics.rise_time_seconds:.2e} s")
480
+ >>> print(f"Frequency: {metrics.frequency_hz:.2f} Hz")
481
+ >>> print(f"Duty Cycle: {metrics.duty_cycle_ratio*100:.1f}%")
482
+
483
+ References:
484
+ IEEE 181-2011 (Transitional Waveform Definitions)
485
+ """
486
+ from oscura.analyzers.waveform.measurements import (
487
+ duty_cycle,
488
+ fall_time,
489
+ frequency,
490
+ period,
491
+ pulse_width,
492
+ rise_time,
493
+ )
494
+
495
+ # Compute all timing measurements
496
+ rt = rise_time(trace)
497
+ ft = fall_time(trace)
498
+ T = period(trace, return_all=False)
499
+ freq = frequency(trace)
500
+ dc = duty_cycle(trace)
501
+ pw = pulse_width(trace, polarity="positive", return_all=False)
502
+
503
+ return TimingMetrics(
504
+ rise_time=rt,
505
+ fall_time=ft,
506
+ period=T,
507
+ frequency=freq,
508
+ duty_cycle=dc,
509
+ pulse_width=pw,
510
+ )
511
+
512
+
169
513
  def auto_decode(
170
514
  trace: WaveformTrace | DigitalTrace,
171
515
  protocol: str | None = None,
172
516
  min_confidence: float = 0.5,
517
+ channel_mapping: dict[str, int] | None = None,
173
518
  ) -> DecodeResult:
174
519
  """Auto-detect protocol and decode frames in one call.
175
520
 
176
521
  Automatically detects the protocol type (UART, SPI, I2C, CAN, etc.)
177
- if not specified, then decodes all frames.
522
+ if not specified, then decodes all frames. Supports multi-channel traces
523
+ via channel mapping.
178
524
 
179
525
  Args:
180
- trace: Input trace (waveform or digital).
526
+ trace: Input trace (waveform, digital, or multi-channel).
181
527
  protocol: Force specific protocol (None for auto-detect).
182
528
  min_confidence: Minimum confidence for auto-detection (0-1).
529
+ channel_mapping: Channel mapping for multi-channel traces.
530
+ Example: {"data": 0, "clock": 1} for SPI on channels 0 and 1.
183
531
 
184
532
  Returns:
185
533
  DecodeResult with protocol name, decoded frames, and statistics.
186
534
 
187
535
  Example:
536
+ >>> # Single-channel auto-detection
188
537
  >>> trace = osc.load("serial_capture.wfm")
189
538
  >>> result = osc.auto_decode(trace)
190
539
  >>> print(f"Protocol: {result.protocol}")
191
540
  >>> print(f"Frames decoded: {len(result.frames)}")
192
541
  >>> for frame in result.frames[:5]:
193
542
  ... print(f" {frame.data.hex()}")
543
+ >>>
544
+ >>> # Multi-channel with explicit mapping
545
+ >>> multi_trace = osc.load("spi_capture.wfm") # 2 channels
546
+ >>> result = osc.auto_decode(
547
+ ... multi_trace,
548
+ ... protocol="SPI",
549
+ ... channel_mapping={"mosi": 0, "clock": 1}
550
+ ... )
194
551
 
195
552
  References:
196
553
  sigrok Protocol Decoder API
197
554
  """
555
+ # Handle multi-channel traces
556
+ if hasattr(trace, "channels") and channel_mapping is not None:
557
+ # Extract channels based on mapping
558
+ # Note: MultiChannelTrace support would go here when implemented
559
+ # For now, use first channel as fallback
560
+ from oscura.core.types import WaveformTrace
198
561
 
199
- # Prepare digital trace
200
- digital_trace = _prepare_digital_trace(trace)
562
+ if isinstance(trace, WaveformTrace):
563
+ digital_trace = _prepare_digital_trace(trace)
564
+ else:
565
+ digital_trace = trace # type: ignore[assignment]
566
+ else:
567
+ # Prepare digital trace
568
+ digital_trace = _prepare_digital_trace(trace)
201
569
 
202
570
  # Detect or use specified protocol
203
571
  protocol, config, confidence = _detect_or_select_protocol(