oscura 0.7.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.
- oscura/__init__.py +19 -19
- oscura/analyzers/__init__.py +2 -0
- oscura/analyzers/digital/extraction.py +2 -3
- oscura/analyzers/digital/quality.py +1 -1
- oscura/analyzers/digital/timing.py +1 -1
- oscura/analyzers/eye/__init__.py +5 -1
- oscura/analyzers/eye/generation.py +501 -0
- oscura/analyzers/jitter/__init__.py +6 -6
- oscura/analyzers/jitter/timing.py +419 -0
- oscura/analyzers/patterns/__init__.py +94 -0
- oscura/analyzers/patterns/reverse_engineering.py +991 -0
- oscura/analyzers/power/__init__.py +35 -12
- oscura/analyzers/power/basic.py +3 -3
- oscura/analyzers/power/soa.py +1 -1
- oscura/analyzers/power/switching.py +3 -3
- oscura/analyzers/signal_classification.py +529 -0
- oscura/analyzers/signal_integrity/sparams.py +3 -3
- oscura/analyzers/statistics/__init__.py +4 -0
- oscura/analyzers/statistics/basic.py +152 -0
- oscura/analyzers/statistics/correlation.py +47 -6
- oscura/analyzers/validation.py +1 -1
- oscura/analyzers/waveform/__init__.py +2 -0
- oscura/analyzers/waveform/measurements.py +329 -163
- oscura/analyzers/waveform/measurements_with_uncertainty.py +91 -35
- oscura/analyzers/waveform/spectral.py +498 -54
- oscura/api/dsl/commands.py +15 -6
- oscura/api/server/templates/base.html +137 -146
- oscura/api/server/templates/export.html +84 -110
- oscura/api/server/templates/home.html +248 -267
- oscura/api/server/templates/protocols.html +44 -48
- oscura/api/server/templates/reports.html +27 -35
- oscura/api/server/templates/session_detail.html +68 -78
- oscura/api/server/templates/sessions.html +62 -72
- oscura/api/server/templates/waveforms.html +54 -64
- oscura/automotive/__init__.py +1 -1
- oscura/automotive/can/session.py +1 -1
- oscura/automotive/dbc/generator.py +638 -23
- oscura/automotive/dtc/data.json +102 -17
- oscura/automotive/uds/decoder.py +99 -6
- oscura/cli/analyze.py +8 -2
- oscura/cli/batch.py +36 -5
- oscura/cli/characterize.py +18 -4
- oscura/cli/export.py +47 -5
- oscura/cli/main.py +2 -0
- oscura/cli/onboarding/wizard.py +10 -6
- oscura/cli/pipeline.py +585 -0
- oscura/cli/visualize.py +6 -4
- oscura/convenience.py +400 -32
- oscura/core/config/loader.py +0 -1
- oscura/core/measurement_result.py +286 -0
- oscura/core/progress.py +1 -1
- oscura/core/schemas/device_mapping.json +8 -2
- oscura/core/schemas/packet_format.json +24 -4
- oscura/core/schemas/protocol_definition.json +12 -2
- oscura/core/types.py +300 -199
- oscura/correlation/multi_protocol.py +1 -1
- oscura/export/legacy/__init__.py +11 -0
- oscura/export/legacy/wav.py +75 -0
- oscura/exporters/__init__.py +19 -0
- oscura/exporters/wireshark.py +809 -0
- oscura/hardware/acquisition/file.py +5 -19
- oscura/hardware/acquisition/saleae.py +10 -10
- oscura/hardware/acquisition/socketcan.py +4 -6
- oscura/hardware/acquisition/synthetic.py +1 -5
- oscura/hardware/acquisition/visa.py +6 -6
- oscura/hardware/security/side_channel_detector.py +5 -508
- oscura/inference/message_format.py +686 -1
- oscura/jupyter/display.py +2 -2
- oscura/jupyter/magic.py +3 -3
- oscura/loaders/__init__.py +17 -12
- oscura/loaders/binary.py +1 -1
- oscura/loaders/chipwhisperer.py +1 -2
- oscura/loaders/configurable.py +1 -1
- oscura/loaders/csv_loader.py +2 -2
- oscura/loaders/hdf5_loader.py +1 -1
- oscura/loaders/lazy.py +6 -1
- oscura/loaders/mmap_loader.py +0 -1
- oscura/loaders/numpy_loader.py +8 -7
- oscura/loaders/preprocessing.py +3 -5
- oscura/loaders/rigol.py +21 -7
- oscura/loaders/sigrok.py +2 -5
- oscura/loaders/tdms.py +3 -2
- oscura/loaders/tektronix.py +38 -32
- oscura/loaders/tss.py +20 -27
- oscura/loaders/vcd.py +13 -8
- oscura/loaders/wav.py +1 -6
- oscura/pipeline/__init__.py +76 -0
- oscura/pipeline/handlers/__init__.py +165 -0
- oscura/pipeline/handlers/analyzers.py +1045 -0
- oscura/pipeline/handlers/decoders.py +899 -0
- oscura/pipeline/handlers/exporters.py +1103 -0
- oscura/pipeline/handlers/filters.py +891 -0
- oscura/pipeline/handlers/loaders.py +640 -0
- oscura/pipeline/handlers/transforms.py +768 -0
- oscura/reporting/__init__.py +88 -1
- oscura/reporting/automation.py +348 -0
- oscura/reporting/citations.py +374 -0
- oscura/reporting/core.py +54 -0
- oscura/reporting/formatting/__init__.py +11 -0
- oscura/reporting/formatting/measurements.py +320 -0
- oscura/reporting/html.py +57 -0
- oscura/reporting/interpretation.py +431 -0
- oscura/reporting/summary.py +329 -0
- oscura/reporting/templates/enhanced/protocol_re.html +504 -503
- oscura/reporting/visualization.py +542 -0
- oscura/side_channel/__init__.py +38 -57
- oscura/utils/builders/signal_builder.py +5 -5
- oscura/utils/comparison/compare.py +7 -9
- oscura/utils/comparison/golden.py +1 -1
- oscura/utils/filtering/convenience.py +2 -2
- oscura/utils/math/arithmetic.py +38 -62
- oscura/utils/math/interpolation.py +20 -20
- oscura/utils/pipeline/__init__.py +4 -17
- oscura/utils/progressive.py +1 -4
- oscura/utils/triggering/edge.py +1 -1
- oscura/utils/triggering/pattern.py +2 -2
- oscura/utils/triggering/pulse.py +2 -2
- oscura/utils/triggering/window.py +3 -3
- oscura/validation/hil_testing.py +11 -11
- oscura/visualization/__init__.py +47 -284
- oscura/visualization/batch.py +160 -0
- oscura/visualization/plot.py +542 -53
- oscura/visualization/styles.py +184 -318
- oscura/workflows/__init__.py +2 -0
- oscura/workflows/batch/advanced.py +1 -1
- oscura/workflows/batch/aggregate.py +7 -8
- oscura/workflows/complete_re.py +251 -23
- oscura/workflows/digital.py +27 -4
- oscura/workflows/multi_trace.py +136 -17
- oscura/workflows/waveform.py +788 -0
- {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/METADATA +59 -79
- {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/RECORD +135 -149
- oscura/side_channel/dpa.py +0 -1025
- oscura/utils/optimization/__init__.py +0 -19
- oscura/utils/optimization/parallel.py +0 -443
- oscura/utils/optimization/search.py +0 -532
- oscura/utils/pipeline/base.py +0 -338
- oscura/utils/pipeline/composition.py +0 -248
- oscura/utils/pipeline/parallel.py +0 -449
- oscura/utils/pipeline/pipeline.py +0 -375
- oscura/utils/search/__init__.py +0 -16
- oscura/utils/search/anomaly.py +0 -424
- oscura/utils/search/context.py +0 -294
- oscura/utils/search/pattern.py +0 -288
- oscura/utils/storage/__init__.py +0 -61
- oscura/utils/storage/database.py +0 -1166
- oscura/visualization/accessibility.py +0 -526
- oscura/visualization/annotations.py +0 -371
- oscura/visualization/axis_scaling.py +0 -305
- oscura/visualization/colors.py +0 -451
- oscura/visualization/digital.py +0 -436
- oscura/visualization/eye.py +0 -571
- oscura/visualization/histogram.py +0 -281
- oscura/visualization/interactive.py +0 -1035
- oscura/visualization/jitter.py +0 -1042
- oscura/visualization/keyboard.py +0 -394
- oscura/visualization/layout.py +0 -400
- oscura/visualization/optimization.py +0 -1079
- oscura/visualization/palettes.py +0 -446
- oscura/visualization/power.py +0 -508
- oscura/visualization/power_extended.py +0 -955
- oscura/visualization/presets.py +0 -469
- oscura/visualization/protocols.py +0 -1246
- oscura/visualization/render.py +0 -223
- oscura/visualization/rendering.py +0 -444
- oscura/visualization/reverse_engineering.py +0 -838
- oscura/visualization/signal_integrity.py +0 -989
- oscura/visualization/specialized.py +0 -643
- oscura/visualization/spectral.py +0 -1226
- oscura/visualization/thumbnails.py +0 -340
- oscura/visualization/time_axis.py +0 -351
- oscura/visualization/waveform.py +0 -454
- {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/WHEEL +0 -0
- {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.7.0.dist-info → oscura-0.10.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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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.
|
|
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
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
|
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
|
-
|
|
200
|
-
|
|
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(
|
oscura/core/config/loader.py
CHANGED
|
@@ -225,7 +225,6 @@ def load_config(
|
|
|
225
225
|
config = copy.deepcopy(DEFAULT_CONFIG)
|
|
226
226
|
|
|
227
227
|
# Search for config files if no explicit path provided
|
|
228
|
-
# use_defaults flag only controls whether to merge with DEFAULT_CONFIG
|
|
229
228
|
if config_path is None:
|
|
230
229
|
# Search standard locations
|
|
231
230
|
search_paths = [
|