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

1"""Waveform measurements with uncertainty propagation. 

2 

3This module extends the standard measurements module with uncertainty 

4estimation following GUM (Guide to the Expression of Uncertainty in Measurement) 

5principles. 

6 

7All measurements return MeasurementResult objects that include both the 

8value and its associated uncertainty. 

9 

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 

15 

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""" 

20 

21from __future__ import annotations 

22 

23from typing import TYPE_CHECKING 

24 

25import numpy as np 

26 

27from tracekit.analyzers.waveform import measurements as meas 

28from tracekit.core.uncertainty import MeasurementWithUncertainty, UncertaintyEstimator 

29 

30if TYPE_CHECKING: 

31 from tracekit.core.types import WaveformTrace 

32 

33 

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. 

41 

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) 

46 

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). 

51 

52 Returns: 

53 MeasurementResult with value and uncertainty. 

54 

55 Example: 

56 >>> result = rise_time(trace) 

57 >>> print(f"t_rise = {result.value*1e9:.2f} ± {result.uncertainty*1e9:.2f} ns") 

58 

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) 

65 

66 if not include_uncertainty or np.isnan(value): 

67 return MeasurementWithUncertainty(value=float(value), uncertainty=float(np.nan), unit="s") 

68 

69 # Estimate uncertainty components 

70 uncertainties = [] 

71 

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) 

87 

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) 

93 

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) 

107 

108 # Combine all uncertainty sources (uncorrelated) 

109 total_uncertainty = UncertaintyEstimator.combined_uncertainty(uncertainties) 

110 

111 return MeasurementWithUncertainty( 

112 value=float(value), 

113 uncertainty=total_uncertainty, 

114 unit="s", 

115 n_samples=len(trace.data), 

116 ) 

117 

118 

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. 

126 

127 Similar uncertainty sources as rise_time(). 

128 

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). 

133 

134 Returns: 

135 MeasurementResult with value and uncertainty. 

136 

137 References: 

138 IEEE 181-2011 Section 5.2 

139 """ 

140 value = meas.fall_time(trace, ref_levels=ref_levels) 

141 

142 if not include_uncertainty or np.isnan(value): 

143 return MeasurementWithUncertainty(value=float(value), uncertainty=float(np.nan), unit="s") 

144 

145 # Similar uncertainty calculation as rise_time 

146 uncertainties = [] 

147 

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)) 

154 

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) 

159 

160 total_uncertainty = UncertaintyEstimator.combined_uncertainty(uncertainties) 

161 

162 return MeasurementWithUncertainty( 

163 value=float(value), 

164 uncertainty=total_uncertainty, 

165 unit="s", 

166 n_samples=len(trace.data), 

167 ) 

168 

169 

170def frequency( 

171 trace: WaveformTrace, *, include_uncertainty: bool = True 

172) -> MeasurementWithUncertainty: 

173 """Measure frequency with uncertainty estimation. 

174 

175 Uncertainty sources: 

176 - Time base accuracy 

177 - Period measurement uncertainty 

178 - Allan variance (short-term stability) 

179 

180 Args: 

181 trace: Input waveform trace. 

182 include_uncertainty: If False, only return value estimate (faster). 

183 

184 Returns: 

185 MeasurementResult with value and uncertainty in Hz. 

186 

187 Example: 

188 >>> result = frequency(trace) 

189 >>> print(f"f = {result.value/1e6:.6f} ± {result.relative_uncertainty*100:.2f}% MHz") 

190 

191 References: 

192 IEEE 181-2011 Section 5.3 

193 IEEE 1057-2017 Section 4.3 

194 """ 

195 value = meas.frequency(trace) 

196 

197 if not include_uncertainty or np.isnan(value): 

198 return MeasurementWithUncertainty(value=float(value), uncertainty=float(np.nan), unit="Hz") 

199 

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 

203 

204 if np.isnan(period): 

205 return MeasurementWithUncertainty(value=float(value), uncertainty=float(np.nan), unit="Hz") 

206 

207 # Estimate period uncertainty 

208 uncertainties = [] 

209 

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) 

215 

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) 

222 

223 # Combine to get period uncertainty 

224 u_period = UncertaintyEstimator.combined_uncertainty([float(u) for u in uncertainties]) 

225 

226 # Propagate to frequency: u(f) = |df/dT| * u(T) = f^2 * u(T) 

227 u_frequency = float((value**2) * u_period) 

228 

229 return MeasurementWithUncertainty( 

230 value=float(value), uncertainty=u_frequency, unit="Hz", n_samples=len(trace.data) 

231 ) 

232 

233 

234def amplitude( 

235 trace: WaveformTrace, *, include_uncertainty: bool = True 

236) -> MeasurementWithUncertainty: 

237 """Measure amplitude (Vpp) with uncertainty estimation. 

238 

239 Uncertainty sources: 

240 - Vertical gain accuracy (from calibration info) 

241 - Vertical offset error 

242 - Quantization noise (ADC resolution) 

243 - Signal noise (statistical) 

244 

245 Args: 

246 trace: Input waveform trace. 

247 include_uncertainty: If False, only return value estimate (faster). 

248 

249 Returns: 

250 MeasurementResult with value and uncertainty in volts. 

251 

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) 

257 

258 if not include_uncertainty or np.isnan(value): 

259 return MeasurementWithUncertainty(value=float(value), uncertainty=float(np.nan), unit="V") 

260 

261 uncertainties = [] 

262 

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 

271 

272 u_vertical = UncertaintyEstimator.vertical_uncertainty( 

273 float(value), vertical_accuracy_pct, offset_error 

274 ) 

275 uncertainties.append(u_vertical) 

276 

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) 

293 

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)) 

304 

305 total_uncertainty = UncertaintyEstimator.combined_uncertainty(uncertainties) 

306 

307 return MeasurementWithUncertainty( 

308 value=float(value), 

309 uncertainty=total_uncertainty, 

310 unit="V", 

311 n_samples=len(trace.data), 

312 ) 

313 

314 

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. 

322 

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). 

327 

328 Returns: 

329 MeasurementResult with value and uncertainty in volts RMS. 

330 

331 References: 

332 IEEE 1057-2017 Section 4.3 

333 """ 

334 value = meas.rms(trace, ac_coupled=ac_coupled) 

335 

336 if not include_uncertainty or np.isnan(value): 

337 return MeasurementWithUncertainty(value=float(value), uncertainty=float(np.nan), unit="V") 

338 

339 uncertainties = [] 

340 

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) 

348 

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) 

354 

355 total_uncertainty = UncertaintyEstimator.combined_uncertainty(uncertainties) 

356 

357 return MeasurementWithUncertainty( 

358 value=float(value), 

359 uncertainty=total_uncertainty, 

360 unit="V", 

361 n_samples=len(trace.data), 

362 ) 

363 

364 

365__all__ = [ 

366 "amplitude", 

367 "fall_time", 

368 "frequency", 

369 "rise_time", 

370 "rms", 

371]