Coverage for src / tracekit / analyzers / waveform / measurements_with_uncertainty.py: 0%
105 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 23:04 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 23:04 +0000
1"""Waveform measurements with uncertainty propagation.
3This module extends the standard measurements module with uncertainty
4estimation following GUM (Guide to the Expression of Uncertainty in Measurement)
5principles.
7All measurements return MeasurementResult objects that include both the
8value and its associated uncertainty.
10Example:
11 >>> from tracekit.analyzers.waveform import measurements_with_uncertainty as meas_u
12 >>> result = meas_u.rise_time(trace)
13 >>> print(f"Rise time: {result.value*1e9:.2f} ± {result.uncertainty*1e9:.2f} ns")
14 Rise time: 2.34 ± 0.12 ns
16References:
17 JCGM 100:2008 - Guide to the Expression of Uncertainty in Measurement (GUM)
18 IEEE 181-2011 - Standard for Transitional Waveform Definitions (Annex B: Uncertainty)
19"""
21from __future__ import annotations
23from typing import TYPE_CHECKING
25import numpy as np
27from tracekit.analyzers.waveform import measurements as meas
28from tracekit.core.uncertainty import MeasurementWithUncertainty, UncertaintyEstimator
30if TYPE_CHECKING:
31 from tracekit.core.types import WaveformTrace
34def rise_time(
35 trace: WaveformTrace,
36 *,
37 ref_levels: tuple[float, float] = (0.1, 0.9),
38 include_uncertainty: bool = True,
39) -> MeasurementWithUncertainty:
40 """Measure rise time with uncertainty estimation.
42 Uncertainty sources:
43 - Time base accuracy (from calibration info if available)
44 - Sample interpolation error (sub-sample timing)
45 - Noise-induced edge jitter (from signal SNR)
47 Args:
48 trace: Input waveform trace.
49 ref_levels: Reference levels as fractions (0.0 to 1.0).
50 include_uncertainty: If False, only return value estimate (faster).
52 Returns:
53 MeasurementResult with value and uncertainty.
55 Example:
56 >>> result = rise_time(trace)
57 >>> print(f"t_rise = {result.value*1e9:.2f} ± {result.uncertainty*1e9:.2f} ns")
59 References:
60 IEEE 181-2011 Section 5.2 (rise time)
61 IEEE 181-2011 Annex B (measurement uncertainty)
62 """
63 # Get the measurement value
64 value = meas.rise_time(trace, ref_levels=ref_levels)
66 if not include_uncertainty or np.isnan(value):
67 return MeasurementWithUncertainty(value=float(value), uncertainty=float(np.nan), unit="s")
69 # Estimate uncertainty components
70 uncertainties = []
72 # 1. Time base uncertainty (Type B)
73 if trace.metadata.calibration_info is not None:
74 # Use calibration info if available
75 # Typical scope: 25-50 ppm timebase accuracy
76 timebase_ppm = 25.0 # Conservative estimate
77 u_timebase = UncertaintyEstimator.time_base_uncertainty(
78 trace.metadata.sample_rate, timebase_ppm
79 )
80 # Rise time involves 2 samples (start and stop), so uncertainty scales
81 u_timebase_rise = u_timebase * np.sqrt(2)
82 uncertainties.append(u_timebase_rise)
83 else:
84 # No calibration info - use conservative estimate
85 u_timebase_rise = (1.0 / trace.metadata.sample_rate) * 50e-6 # 50 ppm
86 uncertainties.append(u_timebase_rise)
88 # 2. Interpolation uncertainty (Type B - rectangular distribution)
89 # Linear interpolation error: typically ±0.5 samples worst case
90 sample_period = trace.metadata.time_base
91 u_interp = UncertaintyEstimator.type_b_rectangular(0.5 * sample_period)
92 uncertainties.append(u_interp)
94 # 3. Noise-induced uncertainty (Type A equivalent)
95 # Estimate from local signal noise
96 # Find the data region near the edge
97 data_slice = trace.data # Could refine to edge region
98 if len(data_slice) > 10:
99 noise_estimate = np.std(data_slice[:10]) if len(data_slice) >= 10 else 0.0
100 # Noise-to-slew-rate ratio gives time uncertainty
101 amplitude = np.ptp(trace.data)
102 if amplitude > 0:
103 # Approximate slew rate: amplitude / rise_time
104 slew_rate = amplitude / value if value > 0 else np.inf
105 u_noise = noise_estimate / slew_rate if slew_rate != np.inf else 0.0
106 uncertainties.append(u_noise)
108 # Combine all uncertainty sources (uncorrelated)
109 total_uncertainty = UncertaintyEstimator.combined_uncertainty(uncertainties)
111 return MeasurementWithUncertainty(
112 value=float(value),
113 uncertainty=total_uncertainty,
114 unit="s",
115 n_samples=len(trace.data),
116 )
119def fall_time(
120 trace: WaveformTrace,
121 *,
122 ref_levels: tuple[float, float] = (0.9, 0.1),
123 include_uncertainty: bool = True,
124) -> MeasurementWithUncertainty:
125 """Measure fall time with uncertainty estimation.
127 Similar uncertainty sources as rise_time().
129 Args:
130 trace: Input waveform trace.
131 ref_levels: Reference levels as fractions (0.0 to 1.0).
132 include_uncertainty: If False, only return value estimate (faster).
134 Returns:
135 MeasurementResult with value and uncertainty.
137 References:
138 IEEE 181-2011 Section 5.2
139 """
140 value = meas.fall_time(trace, ref_levels=ref_levels)
142 if not include_uncertainty or np.isnan(value):
143 return MeasurementWithUncertainty(value=float(value), uncertainty=float(np.nan), unit="s")
145 # Similar uncertainty calculation as rise_time
146 uncertainties = []
148 # Time base uncertainty
149 timebase_ppm = 25.0
150 u_timebase = UncertaintyEstimator.time_base_uncertainty(
151 trace.metadata.sample_rate, timebase_ppm
152 )
153 uncertainties.append(u_timebase * np.sqrt(2))
155 # Interpolation uncertainty
156 sample_period = trace.metadata.time_base
157 u_interp = UncertaintyEstimator.type_b_rectangular(0.5 * sample_period)
158 uncertainties.append(u_interp)
160 total_uncertainty = UncertaintyEstimator.combined_uncertainty(uncertainties)
162 return MeasurementWithUncertainty(
163 value=float(value),
164 uncertainty=total_uncertainty,
165 unit="s",
166 n_samples=len(trace.data),
167 )
170def frequency(
171 trace: WaveformTrace, *, include_uncertainty: bool = True
172) -> MeasurementWithUncertainty:
173 """Measure frequency with uncertainty estimation.
175 Uncertainty sources:
176 - Time base accuracy
177 - Period measurement uncertainty
178 - Allan variance (short-term stability)
180 Args:
181 trace: Input waveform trace.
182 include_uncertainty: If False, only return value estimate (faster).
184 Returns:
185 MeasurementResult with value and uncertainty in Hz.
187 Example:
188 >>> result = frequency(trace)
189 >>> print(f"f = {result.value/1e6:.6f} ± {result.relative_uncertainty*100:.2f}% MHz")
191 References:
192 IEEE 181-2011 Section 5.3
193 IEEE 1057-2017 Section 4.3
194 """
195 value = meas.frequency(trace)
197 if not include_uncertainty or np.isnan(value):
198 return MeasurementWithUncertainty(value=float(value), uncertainty=float(np.nan), unit="Hz")
200 # Frequency is 1/period, so uncertainty propagation:
201 # u(f) = f^2 * u(T) where T is period
202 period = 1.0 / value if value != 0 else np.nan
204 if np.isnan(period):
205 return MeasurementWithUncertainty(value=float(value), uncertainty=float(np.nan), unit="Hz")
207 # Estimate period uncertainty
208 uncertainties = []
210 # Time base uncertainty
211 timebase_ppm = 25.0
212 # Period measurement spans multiple cycles, typically more accurate
213 u_period_timebase = period * (timebase_ppm * 1e-6)
214 uncertainties.append(u_period_timebase)
216 # Interpolation uncertainty for edge detection
217 sample_period = trace.metadata.time_base
218 u_interp = UncertaintyEstimator.type_b_rectangular(0.5 * sample_period)
219 # Two edges per period
220 u_period_interp = u_interp * np.sqrt(2)
221 uncertainties.append(u_period_interp)
223 # Combine to get period uncertainty
224 u_period = UncertaintyEstimator.combined_uncertainty([float(u) for u in uncertainties])
226 # Propagate to frequency: u(f) = |df/dT| * u(T) = f^2 * u(T)
227 u_frequency = float((value**2) * u_period)
229 return MeasurementWithUncertainty(
230 value=float(value), uncertainty=u_frequency, unit="Hz", n_samples=len(trace.data)
231 )
234def amplitude(
235 trace: WaveformTrace, *, include_uncertainty: bool = True
236) -> MeasurementWithUncertainty:
237 """Measure amplitude (Vpp) with uncertainty estimation.
239 Uncertainty sources:
240 - Vertical gain accuracy (from calibration info)
241 - Vertical offset error
242 - Quantization noise (ADC resolution)
243 - Signal noise (statistical)
245 Args:
246 trace: Input waveform trace.
247 include_uncertainty: If False, only return value estimate (faster).
249 Returns:
250 MeasurementResult with value and uncertainty in volts.
252 References:
253 IEEE 1057-2017 Section 4.2 (amplitude measurement)
254 IEEE 1057-2017 Section 4.4 (amplitude accuracy)
255 """
256 value = meas.amplitude(trace)
258 if not include_uncertainty or np.isnan(value):
259 return MeasurementWithUncertainty(value=float(value), uncertainty=float(np.nan), unit="V")
261 uncertainties = []
263 # 1. Vertical accuracy (Type B)
264 # Typical scope: ±2% of reading ± 0.1% of full scale
265 vertical_accuracy_pct = 2.0 # Conservative
266 if trace.metadata.vertical_scale is not None:
267 full_scale = trace.metadata.vertical_scale * 10 # 10 divisions typical
268 offset_error = full_scale * 0.001 # 0.1%
269 else:
270 offset_error = 0.001 # 1 mV default
272 u_vertical = UncertaintyEstimator.vertical_uncertainty(
273 float(value), vertical_accuracy_pct, offset_error
274 )
275 uncertainties.append(u_vertical)
277 # 2. Quantization uncertainty (Type B - rectangular)
278 if (
279 trace.metadata.calibration_info is not None
280 and trace.metadata.calibration_info.vertical_resolution is not None
281 ):
282 bits = trace.metadata.calibration_info.vertical_resolution
283 vertical_range = np.ptp(trace.data) # Simplification
284 lsb = vertical_range / (2**bits)
285 u_quant = UncertaintyEstimator.type_b_rectangular(0.5 * lsb)
286 uncertainties.append(u_quant)
287 else:
288 # Default: 8-bit ADC assumption
289 vertical_range = np.ptp(trace.data)
290 lsb = vertical_range / 256
291 u_quant = UncertaintyEstimator.type_b_rectangular(0.5 * lsb)
292 uncertainties.append(u_quant)
294 # 3. Signal noise (Type A)
295 # Estimate from flat regions (if available)
296 # Simplified: use standard deviation as proxy
297 if len(trace.data) > 100:
298 # Sample first and last 50 points (assume flat regions)
299 noise_start = np.std(trace.data[:50])
300 noise_end = np.std(trace.data[-50:])
301 u_noise = np.mean([noise_start, noise_end])
302 # Amplitude involves max and min, so sqrt(2) factor
303 uncertainties.append(u_noise * np.sqrt(2))
305 total_uncertainty = UncertaintyEstimator.combined_uncertainty(uncertainties)
307 return MeasurementWithUncertainty(
308 value=float(value),
309 uncertainty=total_uncertainty,
310 unit="V",
311 n_samples=len(trace.data),
312 )
315def rms(
316 trace: WaveformTrace,
317 *,
318 ac_coupled: bool = False,
319 include_uncertainty: bool = True,
320) -> MeasurementWithUncertainty:
321 """Measure RMS voltage with uncertainty estimation.
323 Args:
324 trace: Input waveform trace.
325 ac_coupled: If True, remove DC component before calculating RMS.
326 include_uncertainty: If False, only return value estimate (faster).
328 Returns:
329 MeasurementResult with value and uncertainty in volts RMS.
331 References:
332 IEEE 1057-2017 Section 4.3
333 """
334 value = meas.rms(trace, ac_coupled=ac_coupled)
336 if not include_uncertainty or np.isnan(value):
337 return MeasurementWithUncertainty(value=float(value), uncertainty=float(np.nan), unit="V")
339 uncertainties = []
341 # Vertical accuracy
342 vertical_accuracy_pct = 2.0
343 offset_error = 0.001 # 1 mV
344 u_vertical = UncertaintyEstimator.vertical_uncertainty(
345 float(value), vertical_accuracy_pct, offset_error
346 )
347 uncertainties.append(u_vertical)
349 # Statistical uncertainty (Type A)
350 # RMS of N samples: u(RMS) ≈ RMS / sqrt(2N) for Gaussian noise
351 n = len(trace.data)
352 u_statistical = value / np.sqrt(2 * n) if n > 0 else 0.0
353 uncertainties.append(u_statistical)
355 total_uncertainty = UncertaintyEstimator.combined_uncertainty(uncertainties)
357 return MeasurementWithUncertainty(
358 value=float(value),
359 uncertainty=total_uncertainty,
360 unit="V",
361 n_samples=len(trace.data),
362 )
365__all__ = [
366 "amplitude",
367 "fall_time",
368 "frequency",
369 "rise_time",
370 "rms",
371]