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.
- 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/patterns/__init__.py +66 -0
- 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/basic.py +10 -7
- oscura/analyzers/validation.py +1 -1
- oscura/analyzers/waveform/measurements.py +200 -156
- oscura/analyzers/waveform/measurements_with_uncertainty.py +91 -35
- oscura/analyzers/waveform/spectral.py +164 -73
- 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/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/measurement_result.py +286 -0
- oscura/core/progress.py +1 -1
- oscura/core/types.py +232 -239
- 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/formatting/measurements.py +55 -14
- oscura/reporting/templates/enhanced/protocol_re.html +504 -503
- 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 +46 -284
- oscura/visualization/batch.py +72 -433
- oscura/visualization/plot.py +542 -53
- oscura/visualization/styles.py +184 -318
- 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 +11 -6
- {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/METADATA +59 -79
- {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/RECORD +111 -136
- 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.8.0.dist-info → oscura-0.10.0.dist-info}/WHEEL +0 -0
- {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -22,10 +22,12 @@ from typing import TYPE_CHECKING, Any, Literal, overload
|
|
|
22
22
|
import numpy as np
|
|
23
23
|
from numpy import floating as np_floating
|
|
24
24
|
|
|
25
|
+
from oscura.core.measurement_result import make_inapplicable, make_measurement
|
|
26
|
+
|
|
25
27
|
if TYPE_CHECKING:
|
|
26
28
|
from numpy.typing import NDArray
|
|
27
29
|
|
|
28
|
-
from oscura.core.types import WaveformTrace
|
|
30
|
+
from oscura.core.types import MeasurementResult, WaveformTrace
|
|
29
31
|
|
|
30
32
|
|
|
31
33
|
# Measurement metadata: unit information for all waveform measurements
|
|
@@ -73,7 +75,7 @@ def rise_time(
|
|
|
73
75
|
trace: WaveformTrace,
|
|
74
76
|
*,
|
|
75
77
|
ref_levels: tuple[float, float] = (0.1, 0.9),
|
|
76
|
-
) ->
|
|
78
|
+
) -> MeasurementResult:
|
|
77
79
|
"""Measure rise time between reference levels.
|
|
78
80
|
|
|
79
81
|
Computes the time for a signal to transition from the lower
|
|
@@ -85,31 +87,32 @@ def rise_time(
|
|
|
85
87
|
Default (0.1, 0.9) for 10%-90% rise time.
|
|
86
88
|
|
|
87
89
|
Returns:
|
|
88
|
-
|
|
90
|
+
MeasurementResult with rise time in seconds, or inapplicable if no rising edge.
|
|
89
91
|
|
|
90
92
|
Example:
|
|
91
|
-
>>>
|
|
92
|
-
>>>
|
|
93
|
+
>>> result = rise_time(trace)
|
|
94
|
+
>>> if result["applicable"]:
|
|
95
|
+
... print(f"Rise time: {result['display']}")
|
|
93
96
|
|
|
94
97
|
References:
|
|
95
98
|
IEEE 181-2011 Section 5.2
|
|
96
99
|
"""
|
|
97
100
|
if len(trace.data) < 3:
|
|
98
|
-
return
|
|
101
|
+
return make_inapplicable("s", "Insufficient data (need ≥3 samples)")
|
|
99
102
|
|
|
100
103
|
data = trace.data
|
|
101
104
|
low, high = _find_levels(data)
|
|
102
105
|
amplitude = high - low
|
|
103
106
|
|
|
104
|
-
if amplitude <= 0:
|
|
105
|
-
return
|
|
107
|
+
if amplitude <= 0 or np.isnan(amplitude):
|
|
108
|
+
return make_inapplicable("s", "Constant signal (no transitions)")
|
|
106
109
|
|
|
107
110
|
# Calculate reference voltages
|
|
108
111
|
low_ref = low + ref_levels[0] * amplitude
|
|
109
112
|
high_ref = low + ref_levels[1] * amplitude
|
|
110
113
|
|
|
111
114
|
# Find rising edge: where signal crosses from below low_ref to above high_ref
|
|
112
|
-
sample_period = trace.metadata.
|
|
115
|
+
sample_period = 1.0 / trace.metadata.sample_rate
|
|
113
116
|
|
|
114
117
|
# Find first crossing of low reference (going up)
|
|
115
118
|
below_low = data < low_ref
|
|
@@ -119,7 +122,7 @@ def rise_time(
|
|
|
119
122
|
transitions = np.where(below_low[:-1] & above_low[1:])[0]
|
|
120
123
|
|
|
121
124
|
if len(transitions) == 0:
|
|
122
|
-
return
|
|
125
|
+
return make_inapplicable("s", "No rising edges detected")
|
|
123
126
|
|
|
124
127
|
best_rise_time: float | np_floating[Any] = np.nan
|
|
125
128
|
|
|
@@ -148,14 +151,17 @@ def rise_time(
|
|
|
148
151
|
if rt > 0 and (np.isnan(best_rise_time) or rt < best_rise_time):
|
|
149
152
|
best_rise_time = rt
|
|
150
153
|
|
|
151
|
-
|
|
154
|
+
if np.isnan(best_rise_time):
|
|
155
|
+
return make_inapplicable("s", "No valid rising edge found")
|
|
156
|
+
|
|
157
|
+
return make_measurement(float(best_rise_time), "s")
|
|
152
158
|
|
|
153
159
|
|
|
154
160
|
def fall_time(
|
|
155
161
|
trace: WaveformTrace,
|
|
156
162
|
*,
|
|
157
163
|
ref_levels: tuple[float, float] = (0.9, 0.1),
|
|
158
|
-
) ->
|
|
164
|
+
) -> MeasurementResult:
|
|
159
165
|
"""Measure fall time between reference levels.
|
|
160
166
|
|
|
161
167
|
Computes the time for a signal to transition from the upper
|
|
@@ -167,30 +173,31 @@ def fall_time(
|
|
|
167
173
|
Default (0.9, 0.1) for 90%-10% fall time.
|
|
168
174
|
|
|
169
175
|
Returns:
|
|
170
|
-
|
|
176
|
+
MeasurementResult with fall time in seconds, or inapplicable if no falling edge.
|
|
171
177
|
|
|
172
178
|
Example:
|
|
173
|
-
>>>
|
|
174
|
-
>>>
|
|
179
|
+
>>> result = fall_time(trace)
|
|
180
|
+
>>> if result["applicable"]:
|
|
181
|
+
... print(f"Fall time: {result['display']}")
|
|
175
182
|
|
|
176
183
|
References:
|
|
177
184
|
IEEE 181-2011 Section 5.2
|
|
178
185
|
"""
|
|
179
186
|
if len(trace.data) < 3:
|
|
180
|
-
return
|
|
187
|
+
return make_inapplicable("s", "Insufficient data (need ≥3 samples)")
|
|
181
188
|
|
|
182
189
|
data = trace.data
|
|
183
190
|
low, high = _find_levels(data)
|
|
184
191
|
amplitude = high - low
|
|
185
192
|
|
|
186
|
-
if amplitude <= 0:
|
|
187
|
-
return
|
|
193
|
+
if amplitude <= 0 or np.isnan(amplitude):
|
|
194
|
+
return make_inapplicable("s", "Constant signal (no transitions)")
|
|
188
195
|
|
|
189
196
|
# Calculate reference voltages (note: ref_levels[0] is the higher one for fall)
|
|
190
197
|
high_ref = low + ref_levels[0] * amplitude
|
|
191
198
|
low_ref = low + ref_levels[1] * amplitude
|
|
192
199
|
|
|
193
|
-
sample_period = trace.metadata.
|
|
200
|
+
sample_period = 1.0 / trace.metadata.sample_rate
|
|
194
201
|
|
|
195
202
|
# Find where signal is above high reference
|
|
196
203
|
above_high = data >= high_ref
|
|
@@ -200,7 +207,7 @@ def fall_time(
|
|
|
200
207
|
transitions = np.where(above_high[:-1] & below_high[1:])[0]
|
|
201
208
|
|
|
202
209
|
if len(transitions) == 0:
|
|
203
|
-
return
|
|
210
|
+
return make_inapplicable("s", "No falling edges detected")
|
|
204
211
|
|
|
205
212
|
best_fall_time: float | np_floating[Any] = np.nan
|
|
206
213
|
|
|
@@ -228,7 +235,10 @@ def fall_time(
|
|
|
228
235
|
if ft > 0 and (np.isnan(best_fall_time) or ft < best_fall_time):
|
|
229
236
|
best_fall_time = ft
|
|
230
237
|
|
|
231
|
-
|
|
238
|
+
if np.isnan(best_fall_time):
|
|
239
|
+
return make_inapplicable("s", "No valid falling edge found")
|
|
240
|
+
|
|
241
|
+
return make_measurement(float(best_fall_time), "s")
|
|
232
242
|
|
|
233
243
|
|
|
234
244
|
@overload
|
|
@@ -237,7 +247,7 @@ def period(
|
|
|
237
247
|
*,
|
|
238
248
|
edge_type: Literal["rising", "falling"] = "rising",
|
|
239
249
|
return_all: Literal[False] = False,
|
|
240
|
-
) ->
|
|
250
|
+
) -> MeasurementResult: ...
|
|
241
251
|
|
|
242
252
|
|
|
243
253
|
@overload
|
|
@@ -254,7 +264,7 @@ def period(
|
|
|
254
264
|
*,
|
|
255
265
|
edge_type: Literal["rising", "falling"] = "rising",
|
|
256
266
|
return_all: bool = False,
|
|
257
|
-
) ->
|
|
267
|
+
) -> MeasurementResult | NDArray[np.float64]:
|
|
258
268
|
"""Measure signal period between consecutive edges.
|
|
259
269
|
|
|
260
270
|
Computes the time between consecutive rising or falling edges.
|
|
@@ -262,14 +272,15 @@ def period(
|
|
|
262
272
|
Args:
|
|
263
273
|
trace: Input waveform trace.
|
|
264
274
|
edge_type: Type of edges to use ("rising" or "falling").
|
|
265
|
-
return_all: If True, return array of all periods. If False, return
|
|
275
|
+
return_all: If True, return array of all periods. If False, return MeasurementResult.
|
|
266
276
|
|
|
267
277
|
Returns:
|
|
268
|
-
|
|
278
|
+
MeasurementResult with period in seconds (mean), or array of periods if return_all=True.
|
|
269
279
|
|
|
270
280
|
Example:
|
|
271
|
-
>>>
|
|
272
|
-
>>>
|
|
281
|
+
>>> result = period(trace)
|
|
282
|
+
>>> if result["applicable"]:
|
|
283
|
+
... print(f"Period: {result['display']}")
|
|
273
284
|
|
|
274
285
|
References:
|
|
275
286
|
IEEE 181-2011 Section 5.3
|
|
@@ -279,20 +290,20 @@ def period(
|
|
|
279
290
|
if len(edges) < 2:
|
|
280
291
|
if return_all:
|
|
281
292
|
return np.array([], dtype=np.float64)
|
|
282
|
-
return
|
|
293
|
+
return make_inapplicable("s", f"Insufficient {edge_type} edges (need ≥2)")
|
|
283
294
|
|
|
284
295
|
periods = np.diff(edges)
|
|
285
296
|
|
|
286
297
|
if return_all:
|
|
287
298
|
return periods
|
|
288
|
-
return float(np.mean(periods))
|
|
299
|
+
return make_measurement(float(np.mean(periods)), "s")
|
|
289
300
|
|
|
290
301
|
|
|
291
302
|
def frequency(
|
|
292
303
|
trace: WaveformTrace,
|
|
293
304
|
*,
|
|
294
305
|
method: Literal["edge", "fft"] = "edge",
|
|
295
|
-
) ->
|
|
306
|
+
) -> MeasurementResult:
|
|
296
307
|
"""Measure signal frequency.
|
|
297
308
|
|
|
298
309
|
Computes frequency either from edge-to-edge period or using FFT.
|
|
@@ -306,17 +317,18 @@ def frequency(
|
|
|
306
317
|
- "fft": Peak of FFT magnitude spectrum (always use FFT)
|
|
307
318
|
|
|
308
319
|
Returns:
|
|
309
|
-
|
|
320
|
+
MeasurementResult with frequency in Hz, or inapplicable if measurement not possible.
|
|
310
321
|
|
|
311
322
|
Raises:
|
|
312
323
|
ValueError: If method is not one of the supported types.
|
|
313
324
|
|
|
314
325
|
Example:
|
|
315
|
-
>>>
|
|
316
|
-
>>>
|
|
326
|
+
>>> result = frequency(trace)
|
|
327
|
+
>>> if result["applicable"]:
|
|
328
|
+
... print(f"Frequency: {result['display']}")
|
|
317
329
|
|
|
318
330
|
>>> # Force FFT method for smooth waveforms
|
|
319
|
-
>>>
|
|
331
|
+
>>> result = frequency(trace, method="fft")
|
|
320
332
|
|
|
321
333
|
References:
|
|
322
334
|
IEEE 181-2011 Section 5.3
|
|
@@ -326,11 +338,11 @@ def frequency(
|
|
|
326
338
|
T = period(trace, edge_type="rising", return_all=False)
|
|
327
339
|
|
|
328
340
|
# Fall back to FFT if edge detection fails
|
|
329
|
-
if
|
|
341
|
+
if not T["applicable"] or not T["value"] or T["value"] <= 0:
|
|
330
342
|
# Try FFT fallback for smooth waveforms (sine, triangle)
|
|
331
343
|
return _frequency_fft(trace)
|
|
332
344
|
|
|
333
|
-
return 1.0 / T
|
|
345
|
+
return make_measurement(1.0 / T["value"], "Hz")
|
|
334
346
|
|
|
335
347
|
elif method == "fft":
|
|
336
348
|
return _frequency_fft(trace)
|
|
@@ -339,7 +351,7 @@ def frequency(
|
|
|
339
351
|
raise ValueError(f"Unknown method: {method}")
|
|
340
352
|
|
|
341
353
|
|
|
342
|
-
def _frequency_fft(trace: WaveformTrace) ->
|
|
354
|
+
def _frequency_fft(trace: WaveformTrace) -> MeasurementResult:
|
|
343
355
|
"""Compute frequency using FFT peak detection.
|
|
344
356
|
|
|
345
357
|
Internal helper function for FFT-based frequency measurement.
|
|
@@ -348,17 +360,17 @@ def _frequency_fft(trace: WaveformTrace) -> float | np_floating[Any]:
|
|
|
348
360
|
trace: Input waveform trace.
|
|
349
361
|
|
|
350
362
|
Returns:
|
|
351
|
-
|
|
363
|
+
MeasurementResult with frequency in Hz, or inapplicable if measurement not possible.
|
|
352
364
|
"""
|
|
353
365
|
if len(trace.data) < 16:
|
|
354
|
-
return
|
|
366
|
+
return make_inapplicable("Hz", "Insufficient data for FFT (need ≥16 samples)")
|
|
355
367
|
|
|
356
368
|
# Remove DC offset before FFT
|
|
357
369
|
data = trace.data - np.mean(trace.data)
|
|
358
370
|
|
|
359
371
|
# Check if signal is essentially constant (DC only)
|
|
360
372
|
if np.std(data) < 1e-10:
|
|
361
|
-
return
|
|
373
|
+
return make_inapplicable("Hz", "Constant signal (DC only)")
|
|
362
374
|
|
|
363
375
|
n = len(data)
|
|
364
376
|
fft_mag = np.abs(np.fft.rfft(data))
|
|
@@ -369,18 +381,18 @@ def _frequency_fft(trace: WaveformTrace) -> float | np_floating[Any]:
|
|
|
369
381
|
# Verify peak is significant (SNR check)
|
|
370
382
|
# If the peak is not at least 3x the mean, it's likely noise
|
|
371
383
|
if fft_mag[peak_idx] < 3.0 * np.mean(fft_mag[1:]):
|
|
372
|
-
return
|
|
384
|
+
return make_inapplicable("Hz", "No dominant frequency (noisy signal)")
|
|
373
385
|
|
|
374
386
|
# Calculate frequency from peak index
|
|
375
387
|
freq_resolution = trace.metadata.sample_rate / n
|
|
376
|
-
return float(peak_idx * freq_resolution)
|
|
388
|
+
return make_measurement(float(peak_idx * freq_resolution), "Hz")
|
|
377
389
|
|
|
378
390
|
|
|
379
391
|
def duty_cycle(
|
|
380
392
|
trace: WaveformTrace,
|
|
381
393
|
*,
|
|
382
394
|
percentage: bool = False,
|
|
383
|
-
) ->
|
|
395
|
+
) -> MeasurementResult:
|
|
384
396
|
"""Measure duty cycle.
|
|
385
397
|
|
|
386
398
|
Computes duty cycle as the ratio of positive pulse width to period.
|
|
@@ -391,14 +403,15 @@ def duty_cycle(
|
|
|
391
403
|
|
|
392
404
|
Args:
|
|
393
405
|
trace: Input waveform trace.
|
|
394
|
-
percentage:
|
|
406
|
+
percentage: Ignored (always returns ratio, display format shows %).
|
|
395
407
|
|
|
396
408
|
Returns:
|
|
397
|
-
|
|
409
|
+
MeasurementResult with duty cycle as ratio (0-1), or inapplicable if not possible.
|
|
398
410
|
|
|
399
411
|
Example:
|
|
400
|
-
>>>
|
|
401
|
-
>>>
|
|
412
|
+
>>> result = duty_cycle(trace)
|
|
413
|
+
>>> if result["applicable"]:
|
|
414
|
+
... print(f"Duty cycle: {result['display']}") # Shows as percentage
|
|
402
415
|
|
|
403
416
|
References:
|
|
404
417
|
IEEE 181-2011 Section 5.4
|
|
@@ -411,17 +424,19 @@ def duty_cycle(
|
|
|
411
424
|
T = period(trace, edge_type="rising", return_all=False)
|
|
412
425
|
|
|
413
426
|
# Method 1: Standard period-based calculation
|
|
414
|
-
if
|
|
415
|
-
|
|
416
|
-
if
|
|
417
|
-
|
|
418
|
-
|
|
427
|
+
if isinstance(pw_pos, dict) and pw_pos.get("applicable") and pw_pos.get("value"):
|
|
428
|
+
T_value = T.get("value") if isinstance(T, dict) else None
|
|
429
|
+
if isinstance(T, dict) and T.get("applicable") and T_value and T_value > 0:
|
|
430
|
+
pw_value = pw_pos["value"]
|
|
431
|
+
if pw_value:
|
|
432
|
+
dc = pw_value / T_value
|
|
433
|
+
return make_measurement(dc, "ratio")
|
|
419
434
|
|
|
420
435
|
# Method 2: Fallback for incomplete waveforms - time-domain calculation
|
|
421
436
|
# Calculate fraction of time signal spends above midpoint threshold
|
|
422
437
|
data = trace.data
|
|
423
438
|
if len(data) < 3:
|
|
424
|
-
return
|
|
439
|
+
return make_inapplicable("ratio", "Insufficient data (need ≥3 samples)")
|
|
425
440
|
|
|
426
441
|
# Convert boolean data to float if needed
|
|
427
442
|
if data.dtype == bool:
|
|
@@ -432,7 +447,7 @@ def duty_cycle(
|
|
|
432
447
|
|
|
433
448
|
# Check for invalid levels
|
|
434
449
|
if amplitude <= 0 or np.isnan(amplitude):
|
|
435
|
-
return
|
|
450
|
+
return make_inapplicable("ratio", "Constant signal (no transitions)")
|
|
436
451
|
|
|
437
452
|
# Calculate threshold at 50% of amplitude
|
|
438
453
|
mid = low + 0.5 * amplitude
|
|
@@ -443,13 +458,10 @@ def duty_cycle(
|
|
|
443
458
|
total_samples = len(data)
|
|
444
459
|
|
|
445
460
|
if total_samples == 0:
|
|
446
|
-
return
|
|
461
|
+
return make_inapplicable("ratio", "No data available")
|
|
447
462
|
|
|
448
463
|
dc = float(samples_high) / total_samples
|
|
449
|
-
|
|
450
|
-
if percentage:
|
|
451
|
-
return dc * 100
|
|
452
|
-
return dc
|
|
464
|
+
return make_measurement(dc, "ratio")
|
|
453
465
|
|
|
454
466
|
|
|
455
467
|
@overload
|
|
@@ -459,7 +471,7 @@ def pulse_width(
|
|
|
459
471
|
polarity: Literal["positive", "negative"] = "positive",
|
|
460
472
|
ref_level: float = 0.5,
|
|
461
473
|
return_all: Literal[False] = False,
|
|
462
|
-
) ->
|
|
474
|
+
) -> MeasurementResult: ...
|
|
463
475
|
|
|
464
476
|
|
|
465
477
|
@overload
|
|
@@ -478,7 +490,7 @@ def pulse_width(
|
|
|
478
490
|
polarity: Literal["positive", "negative"] = "positive",
|
|
479
491
|
ref_level: float = 0.5,
|
|
480
492
|
return_all: bool = False,
|
|
481
|
-
) ->
|
|
493
|
+
) -> MeasurementResult | NDArray[np.float64]:
|
|
482
494
|
"""Measure pulse width.
|
|
483
495
|
|
|
484
496
|
Computes positive or negative pulse width at the specified reference level.
|
|
@@ -487,14 +499,15 @@ def pulse_width(
|
|
|
487
499
|
trace: Input waveform trace.
|
|
488
500
|
polarity: "positive" for high pulses, "negative" for low pulses.
|
|
489
501
|
ref_level: Reference level as fraction (0.0 to 1.0). Default 0.5 (50%).
|
|
490
|
-
return_all: If True, return array of all widths. If False, return
|
|
502
|
+
return_all: If True, return array of all widths. If False, return MeasurementResult.
|
|
491
503
|
|
|
492
504
|
Returns:
|
|
493
|
-
|
|
505
|
+
MeasurementResult with pulse width in seconds (mean), or array if return_all=True.
|
|
494
506
|
|
|
495
507
|
Example:
|
|
496
|
-
>>>
|
|
497
|
-
>>>
|
|
508
|
+
>>> result = pulse_width(trace, polarity="positive")
|
|
509
|
+
>>> if result["applicable"]:
|
|
510
|
+
... print(f"Pulse width: {result['display']}")
|
|
498
511
|
|
|
499
512
|
References:
|
|
500
513
|
IEEE 181-2011 Section 5.4
|
|
@@ -505,7 +518,12 @@ def pulse_width(
|
|
|
505
518
|
if len(rising_edges) == 0 or len(falling_edges) == 0:
|
|
506
519
|
if return_all:
|
|
507
520
|
return np.array([], dtype=np.float64)
|
|
508
|
-
|
|
521
|
+
edge_type = (
|
|
522
|
+
"rising and falling"
|
|
523
|
+
if len(rising_edges) == 0 and len(falling_edges) == 0
|
|
524
|
+
else ("rising" if len(rising_edges) == 0 else "falling")
|
|
525
|
+
)
|
|
526
|
+
return make_inapplicable("s", f"No {edge_type} edges found")
|
|
509
527
|
|
|
510
528
|
widths: list[float] = []
|
|
511
529
|
|
|
@@ -527,16 +545,16 @@ def pulse_width(
|
|
|
527
545
|
if len(widths) == 0:
|
|
528
546
|
if return_all:
|
|
529
547
|
return np.array([], dtype=np.float64)
|
|
530
|
-
return
|
|
548
|
+
return make_inapplicable("s", f"No {polarity} pulses found")
|
|
531
549
|
|
|
532
550
|
widths_arr = np.array(widths, dtype=np.float64)
|
|
533
551
|
|
|
534
552
|
if return_all:
|
|
535
553
|
return widths_arr
|
|
536
|
-
return float(np.mean(widths_arr))
|
|
554
|
+
return make_measurement(float(np.mean(widths_arr)), "s")
|
|
537
555
|
|
|
538
556
|
|
|
539
|
-
def overshoot(trace: WaveformTrace) ->
|
|
557
|
+
def overshoot(trace: WaveformTrace) -> MeasurementResult:
|
|
540
558
|
"""Measure overshoot percentage.
|
|
541
559
|
|
|
542
560
|
Computes overshoot as (max - high) / amplitude * 100%.
|
|
@@ -545,34 +563,35 @@ def overshoot(trace: WaveformTrace) -> float | np_floating[Any]:
|
|
|
545
563
|
trace: Input waveform trace.
|
|
546
564
|
|
|
547
565
|
Returns:
|
|
548
|
-
|
|
566
|
+
MeasurementResult with overshoot as percentage, or inapplicable if not applicable.
|
|
549
567
|
|
|
550
568
|
Example:
|
|
551
|
-
>>>
|
|
552
|
-
>>>
|
|
569
|
+
>>> result = overshoot(trace)
|
|
570
|
+
>>> if result["applicable"]:
|
|
571
|
+
... print(f"Overshoot: {result['display']}")
|
|
553
572
|
|
|
554
573
|
References:
|
|
555
574
|
IEEE 181-2011 Section 5.5
|
|
556
575
|
"""
|
|
557
576
|
if len(trace.data) < 3:
|
|
558
|
-
return
|
|
577
|
+
return make_inapplicable("%", "Insufficient data (need ≥3 samples)")
|
|
559
578
|
|
|
560
579
|
data = trace.data
|
|
561
580
|
low, high = _find_levels(data)
|
|
562
581
|
amplitude = high - low
|
|
563
582
|
|
|
564
|
-
if amplitude <= 0:
|
|
565
|
-
return
|
|
583
|
+
if amplitude <= 0 or np.isnan(amplitude):
|
|
584
|
+
return make_inapplicable("%", "Constant signal (no amplitude)")
|
|
566
585
|
|
|
567
586
|
max_val = np.max(data)
|
|
568
587
|
|
|
569
588
|
if max_val <= high:
|
|
570
|
-
return 0.0
|
|
589
|
+
return make_measurement(0.0, "%")
|
|
571
590
|
|
|
572
|
-
return float((max_val - high) / amplitude * 100)
|
|
591
|
+
return make_measurement(float((max_val - high) / amplitude * 100), "%")
|
|
573
592
|
|
|
574
593
|
|
|
575
|
-
def undershoot(trace: WaveformTrace) ->
|
|
594
|
+
def undershoot(trace: WaveformTrace) -> MeasurementResult:
|
|
576
595
|
"""Measure undershoot percentage.
|
|
577
596
|
|
|
578
597
|
Computes undershoot as (low - min) / amplitude * 100%.
|
|
@@ -581,38 +600,39 @@ def undershoot(trace: WaveformTrace) -> float | np_floating[Any]:
|
|
|
581
600
|
trace: Input waveform trace.
|
|
582
601
|
|
|
583
602
|
Returns:
|
|
584
|
-
|
|
603
|
+
MeasurementResult with undershoot as percentage, or inapplicable if not applicable.
|
|
585
604
|
|
|
586
605
|
Example:
|
|
587
|
-
>>>
|
|
588
|
-
>>>
|
|
606
|
+
>>> result = undershoot(trace)
|
|
607
|
+
>>> if result["applicable"]:
|
|
608
|
+
... print(f"Undershoot: {result['display']}")
|
|
589
609
|
|
|
590
610
|
References:
|
|
591
611
|
IEEE 181-2011 Section 5.5
|
|
592
612
|
"""
|
|
593
613
|
if len(trace.data) < 3:
|
|
594
|
-
return
|
|
614
|
+
return make_inapplicable("%", "Insufficient data (need ≥3 samples)")
|
|
595
615
|
|
|
596
616
|
data = trace.data
|
|
597
617
|
low, high = _find_levels(data)
|
|
598
618
|
amplitude = high - low
|
|
599
619
|
|
|
600
|
-
if amplitude <= 0:
|
|
601
|
-
return
|
|
620
|
+
if amplitude <= 0 or np.isnan(amplitude):
|
|
621
|
+
return make_inapplicable("%", "Constant signal (no amplitude)")
|
|
602
622
|
|
|
603
623
|
min_val = np.min(data)
|
|
604
624
|
|
|
605
625
|
if min_val >= low:
|
|
606
|
-
return 0.0
|
|
626
|
+
return make_measurement(0.0, "%")
|
|
607
627
|
|
|
608
|
-
return float((low - min_val) / amplitude * 100)
|
|
628
|
+
return make_measurement(float((low - min_val) / amplitude * 100), "%")
|
|
609
629
|
|
|
610
630
|
|
|
611
631
|
def preshoot(
|
|
612
632
|
trace: WaveformTrace,
|
|
613
633
|
*,
|
|
614
634
|
edge_type: Literal["rising", "falling"] = "rising",
|
|
615
|
-
) ->
|
|
635
|
+
) -> MeasurementResult:
|
|
616
636
|
"""Measure preshoot percentage.
|
|
617
637
|
|
|
618
638
|
Computes preshoot before transitions as percentage of amplitude.
|
|
@@ -622,25 +642,26 @@ def preshoot(
|
|
|
622
642
|
edge_type: Type of edge to analyze ("rising" or "falling").
|
|
623
643
|
|
|
624
644
|
Returns:
|
|
625
|
-
|
|
645
|
+
MeasurementResult with preshoot as percentage, or inapplicable if not applicable.
|
|
626
646
|
|
|
627
647
|
Example:
|
|
628
|
-
>>>
|
|
629
|
-
>>>
|
|
648
|
+
>>> result = preshoot(trace)
|
|
649
|
+
>>> if result["applicable"]:
|
|
650
|
+
... print(f"Preshoot: {result['display']}")
|
|
630
651
|
|
|
631
652
|
References:
|
|
632
653
|
IEEE 181-2011 Section 5.5
|
|
633
654
|
"""
|
|
634
655
|
if len(trace.data) < 10:
|
|
635
|
-
return
|
|
656
|
+
return make_inapplicable("%", "Insufficient data (need ≥10 samples)")
|
|
636
657
|
|
|
637
658
|
# Convert memoryview to ndarray if needed
|
|
638
659
|
data = np.asarray(trace.data)
|
|
639
660
|
low, high = _find_levels(data)
|
|
640
661
|
amplitude = high - low
|
|
641
662
|
|
|
642
|
-
if amplitude <= 0:
|
|
643
|
-
return
|
|
663
|
+
if amplitude <= 0 or np.isnan(amplitude):
|
|
664
|
+
return make_inapplicable("%", "Constant signal (no amplitude)")
|
|
644
665
|
|
|
645
666
|
# Find edge crossings at 50%
|
|
646
667
|
mid = (low + high) / 2
|
|
@@ -649,7 +670,7 @@ def preshoot(
|
|
|
649
670
|
# Look for minimum before rising edge that goes below low level
|
|
650
671
|
crossings = np.where((data[:-1] < mid) & (data[1:] >= mid))[0]
|
|
651
672
|
if len(crossings) == 0:
|
|
652
|
-
return
|
|
673
|
+
return make_inapplicable("%", "No rising edges found")
|
|
653
674
|
|
|
654
675
|
max_preshoot = 0.0
|
|
655
676
|
for idx in crossings:
|
|
@@ -662,12 +683,12 @@ def preshoot(
|
|
|
662
683
|
preshoot_val = (low - min_pre) / amplitude * 100
|
|
663
684
|
max_preshoot = max(max_preshoot, preshoot_val)
|
|
664
685
|
|
|
665
|
-
return max_preshoot
|
|
686
|
+
return make_measurement(max_preshoot, "%")
|
|
666
687
|
|
|
667
688
|
else: # falling
|
|
668
689
|
crossings = np.where((data[:-1] >= mid) & (data[1:] < mid))[0]
|
|
669
690
|
if len(crossings) == 0:
|
|
670
|
-
return
|
|
691
|
+
return make_inapplicable("%", "No falling edges found")
|
|
671
692
|
|
|
672
693
|
max_preshoot = 0.0
|
|
673
694
|
for idx in crossings:
|
|
@@ -679,10 +700,10 @@ def preshoot(
|
|
|
679
700
|
preshoot_val = (max_pre - high) / amplitude * 100
|
|
680
701
|
max_preshoot = max(max_preshoot, preshoot_val)
|
|
681
702
|
|
|
682
|
-
return max_preshoot
|
|
703
|
+
return make_measurement(max_preshoot, "%")
|
|
683
704
|
|
|
684
705
|
|
|
685
|
-
def amplitude(trace: WaveformTrace) ->
|
|
706
|
+
def amplitude(trace: WaveformTrace) -> MeasurementResult:
|
|
686
707
|
"""Measure peak-to-peak amplitude.
|
|
687
708
|
|
|
688
709
|
Computes Vpp as the difference between histogram-based high and low levels.
|
|
@@ -691,20 +712,26 @@ def amplitude(trace: WaveformTrace) -> float | np_floating[Any]:
|
|
|
691
712
|
trace: Input waveform trace.
|
|
692
713
|
|
|
693
714
|
Returns:
|
|
694
|
-
|
|
715
|
+
MeasurementResult with amplitude in volts (or input units).
|
|
695
716
|
|
|
696
717
|
Example:
|
|
697
|
-
>>>
|
|
698
|
-
>>>
|
|
718
|
+
>>> result = amplitude(trace)
|
|
719
|
+
>>> if result["applicable"]:
|
|
720
|
+
... print(f"Amplitude: {result['display']}")
|
|
699
721
|
|
|
700
722
|
References:
|
|
701
723
|
IEEE 1057-2017 Section 4.2
|
|
702
724
|
"""
|
|
703
725
|
if len(trace.data) < 2:
|
|
704
|
-
return
|
|
726
|
+
return make_inapplicable("V", "Insufficient data (need ≥2 samples)")
|
|
705
727
|
|
|
706
728
|
low, high = _find_levels(trace.data)
|
|
707
|
-
|
|
729
|
+
amp = high - low
|
|
730
|
+
|
|
731
|
+
if np.isnan(amp):
|
|
732
|
+
return make_inapplicable("V", "Cannot determine amplitude")
|
|
733
|
+
|
|
734
|
+
return make_measurement(amp, "V")
|
|
708
735
|
|
|
709
736
|
|
|
710
737
|
def rms(
|
|
@@ -712,7 +739,7 @@ def rms(
|
|
|
712
739
|
*,
|
|
713
740
|
ac_coupled: bool = False,
|
|
714
741
|
nan_policy: Literal["propagate", "omit", "raise"] = "propagate",
|
|
715
|
-
) ->
|
|
742
|
+
) -> MeasurementResult:
|
|
716
743
|
"""Compute RMS voltage.
|
|
717
744
|
|
|
718
745
|
Calculates root-mean-square voltage of the waveform.
|
|
@@ -721,29 +748,29 @@ def rms(
|
|
|
721
748
|
trace: Input waveform trace.
|
|
722
749
|
ac_coupled: If True, remove DC offset before computing RMS.
|
|
723
750
|
nan_policy: How to handle NaN values:
|
|
724
|
-
- "propagate": Return
|
|
751
|
+
- "propagate": Return inapplicable if any NaN present (default)
|
|
725
752
|
- "omit": Ignore NaN values in calculation
|
|
726
753
|
- "raise": Raise ValueError if any NaN present
|
|
727
754
|
|
|
728
755
|
Returns:
|
|
729
|
-
RMS voltage in volts (or input units).
|
|
756
|
+
MeasurementResult with RMS voltage in volts (or input units).
|
|
730
757
|
|
|
731
758
|
Raises:
|
|
732
759
|
ValueError: If nan_policy="raise" and data contains NaN.
|
|
733
760
|
|
|
734
761
|
Example:
|
|
735
|
-
>>>
|
|
736
|
-
>>>
|
|
762
|
+
>>> result = rms(trace)
|
|
763
|
+
>>> if result["applicable"]:
|
|
764
|
+
... print(f"RMS: {result['display']}")
|
|
737
765
|
|
|
738
766
|
>>> # Handle traces with NaN values
|
|
739
|
-
>>>
|
|
740
|
-
|
|
767
|
+
>>> result = rms(trace, nan_policy="omit")
|
|
741
768
|
|
|
742
769
|
References:
|
|
743
770
|
IEEE 1057-2017 Section 4.3
|
|
744
771
|
"""
|
|
745
772
|
if len(trace.data) == 0:
|
|
746
|
-
return
|
|
773
|
+
return make_inapplicable("V", "Empty trace")
|
|
747
774
|
|
|
748
775
|
# Convert memoryview to ndarray if needed
|
|
749
776
|
data = np.asarray(trace.data)
|
|
@@ -756,20 +783,22 @@ def rms(
|
|
|
756
783
|
# Use nanmean and nansum for NaN-safe calculation
|
|
757
784
|
if ac_coupled:
|
|
758
785
|
data = data - np.nanmean(data)
|
|
759
|
-
return float(np.sqrt(np.nanmean(data**2)))
|
|
760
|
-
#
|
|
786
|
+
return make_measurement(float(np.sqrt(np.nanmean(data**2))), "V")
|
|
787
|
+
else: # propagate
|
|
788
|
+
if np.any(np.isnan(data)):
|
|
789
|
+
return make_inapplicable("V", "Data contains NaN values")
|
|
761
790
|
|
|
762
791
|
if ac_coupled:
|
|
763
792
|
data = data - np.mean(data)
|
|
764
793
|
|
|
765
|
-
return float(np.sqrt(np.mean(data**2)))
|
|
794
|
+
return make_measurement(float(np.sqrt(np.mean(data**2))), "V")
|
|
766
795
|
|
|
767
796
|
|
|
768
797
|
def mean(
|
|
769
798
|
trace: WaveformTrace,
|
|
770
799
|
*,
|
|
771
800
|
nan_policy: Literal["propagate", "omit", "raise"] = "propagate",
|
|
772
|
-
) ->
|
|
801
|
+
) -> MeasurementResult:
|
|
773
802
|
"""Compute mean (DC) voltage.
|
|
774
803
|
|
|
775
804
|
Calculates arithmetic mean of the waveform.
|
|
@@ -777,29 +806,29 @@ def mean(
|
|
|
777
806
|
Args:
|
|
778
807
|
trace: Input waveform trace.
|
|
779
808
|
nan_policy: How to handle NaN values:
|
|
780
|
-
- "propagate": Return
|
|
809
|
+
- "propagate": Return inapplicable if any NaN present (default)
|
|
781
810
|
- "omit": Ignore NaN values in calculation
|
|
782
811
|
- "raise": Raise ValueError if any NaN present
|
|
783
812
|
|
|
784
813
|
Returns:
|
|
785
|
-
|
|
814
|
+
MeasurementResult with mean voltage in volts (or input units).
|
|
786
815
|
|
|
787
816
|
Raises:
|
|
788
817
|
ValueError: If nan_policy="raise" and data contains NaN.
|
|
789
818
|
|
|
790
819
|
Example:
|
|
791
|
-
>>>
|
|
792
|
-
>>>
|
|
820
|
+
>>> result = mean(trace)
|
|
821
|
+
>>> if result["applicable"]:
|
|
822
|
+
... print(f"DC: {result['display']}")
|
|
793
823
|
|
|
794
824
|
>>> # Handle traces with NaN values
|
|
795
|
-
>>>
|
|
796
|
-
|
|
825
|
+
>>> result = mean(trace, nan_policy="omit")
|
|
797
826
|
|
|
798
827
|
References:
|
|
799
828
|
IEEE 1057-2017 Section 4.3
|
|
800
829
|
"""
|
|
801
830
|
if len(trace.data) == 0:
|
|
802
|
-
return
|
|
831
|
+
return make_inapplicable("V", "Empty trace")
|
|
803
832
|
|
|
804
833
|
# Convert memoryview to ndarray if needed
|
|
805
834
|
data = np.asarray(trace.data)
|
|
@@ -808,11 +837,13 @@ def mean(
|
|
|
808
837
|
if nan_policy == "raise":
|
|
809
838
|
if np.any(np.isnan(data)):
|
|
810
839
|
raise ValueError("Input data contains NaN values")
|
|
811
|
-
return float(np.mean(data))
|
|
840
|
+
return make_measurement(float(np.mean(data)), "V")
|
|
812
841
|
elif nan_policy == "omit":
|
|
813
|
-
return float(np.nanmean(data))
|
|
842
|
+
return make_measurement(float(np.nanmean(data)), "V")
|
|
814
843
|
else: # propagate
|
|
815
|
-
|
|
844
|
+
if np.any(np.isnan(data)):
|
|
845
|
+
return make_inapplicable("V", "Data contains NaN values")
|
|
846
|
+
return make_measurement(float(np.mean(data)), "V")
|
|
816
847
|
|
|
817
848
|
|
|
818
849
|
def measure(
|
|
@@ -824,46 +855,49 @@ def measure(
|
|
|
824
855
|
"""Compute multiple waveform measurements.
|
|
825
856
|
|
|
826
857
|
Unified function for computing all or selected waveform measurements.
|
|
858
|
+
Returns MeasurementResult format with applicability tracking.
|
|
827
859
|
|
|
828
860
|
Args:
|
|
829
861
|
trace: Input waveform trace.
|
|
830
862
|
parameters: List of measurement names to compute. If None, compute all.
|
|
831
863
|
Valid names: rise_time, fall_time, period, frequency, duty_cycle,
|
|
832
864
|
amplitude, rms, mean, overshoot, undershoot, preshoot
|
|
833
|
-
include_units: If True, include units in output.
|
|
865
|
+
include_units: If True, include units in output (returns MeasurementResult).
|
|
866
|
+
If False, return raw values (with NaN for inapplicable).
|
|
834
867
|
|
|
835
868
|
Returns:
|
|
836
|
-
Dictionary mapping measurement names to
|
|
869
|
+
Dictionary mapping measurement names to MeasurementResults (if include_units=True)
|
|
870
|
+
or raw values (if include_units=False).
|
|
837
871
|
|
|
838
872
|
Example:
|
|
839
873
|
>>> results = measure(trace)
|
|
840
|
-
>>>
|
|
874
|
+
>>> if results['rise_time']['applicable']:
|
|
875
|
+
... print(f"Rise time: {results['rise_time']['display']}")
|
|
841
876
|
|
|
877
|
+
>>> # Get specific measurements
|
|
842
878
|
>>> results = measure(trace, parameters=["frequency", "amplitude"])
|
|
843
879
|
|
|
880
|
+
>>> # Get raw values (legacy compatibility)
|
|
881
|
+
>>> results = measure(trace, include_units=False)
|
|
882
|
+
>>> freq = results["frequency"] # float or np.nan
|
|
883
|
+
|
|
844
884
|
References:
|
|
845
885
|
IEEE 181-2011, IEEE 1057-2017
|
|
846
886
|
"""
|
|
847
887
|
all_measurements = {
|
|
848
|
-
"rise_time":
|
|
849
|
-
"fall_time":
|
|
850
|
-
"period":
|
|
851
|
-
"frequency":
|
|
852
|
-
"duty_cycle":
|
|
853
|
-
"pulse_width_pos": (
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
"
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
"amplitude": (amplitude, "V"),
|
|
862
|
-
"rms": (rms, "V"),
|
|
863
|
-
"mean": (mean, "V"),
|
|
864
|
-
"overshoot": (overshoot, "%"),
|
|
865
|
-
"undershoot": (undershoot, "%"),
|
|
866
|
-
"preshoot": (preshoot, "%"),
|
|
888
|
+
"rise_time": rise_time,
|
|
889
|
+
"fall_time": fall_time,
|
|
890
|
+
"period": lambda t: period(t, return_all=False),
|
|
891
|
+
"frequency": frequency,
|
|
892
|
+
"duty_cycle": duty_cycle,
|
|
893
|
+
"pulse_width_pos": lambda t: pulse_width(t, polarity="positive", return_all=False),
|
|
894
|
+
"pulse_width_neg": lambda t: pulse_width(t, polarity="negative", return_all=False),
|
|
895
|
+
"amplitude": amplitude,
|
|
896
|
+
"rms": rms,
|
|
897
|
+
"mean": mean,
|
|
898
|
+
"overshoot": overshoot,
|
|
899
|
+
"undershoot": undershoot,
|
|
900
|
+
"preshoot": preshoot,
|
|
867
901
|
}
|
|
868
902
|
|
|
869
903
|
if parameters is None:
|
|
@@ -873,16 +907,26 @@ def measure(
|
|
|
873
907
|
|
|
874
908
|
results: dict[str, Any] = {}
|
|
875
909
|
|
|
876
|
-
for name,
|
|
910
|
+
for name, func in selected.items():
|
|
877
911
|
try:
|
|
878
|
-
|
|
879
|
-
except Exception:
|
|
880
|
-
value = np.nan
|
|
912
|
+
measurement_result = func(trace) # type: ignore[operator]
|
|
881
913
|
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
914
|
+
if include_units:
|
|
915
|
+
# Return full MeasurementResult
|
|
916
|
+
results[name] = measurement_result
|
|
917
|
+
else:
|
|
918
|
+
# Legacy mode: extract raw value (NaN if inapplicable)
|
|
919
|
+
if measurement_result["applicable"]:
|
|
920
|
+
results[name] = measurement_result["value"]
|
|
921
|
+
else:
|
|
922
|
+
results[name] = np.nan
|
|
923
|
+
|
|
924
|
+
except Exception:
|
|
925
|
+
# On error, create inapplicable result
|
|
926
|
+
if include_units:
|
|
927
|
+
results[name] = make_inapplicable("", "Measurement failed")
|
|
928
|
+
else:
|
|
929
|
+
results[name] = np.nan
|
|
886
930
|
|
|
887
931
|
return results
|
|
888
932
|
|
|
@@ -958,7 +1002,7 @@ def _find_edges(
|
|
|
958
1002
|
Array of edge timestamps in seconds.
|
|
959
1003
|
"""
|
|
960
1004
|
data = trace.data
|
|
961
|
-
sample_period = trace.metadata.
|
|
1005
|
+
sample_period = 1.0 / trace.metadata.sample_rate
|
|
962
1006
|
|
|
963
1007
|
if len(data) < 3:
|
|
964
1008
|
return np.array([], dtype=np.float64)
|