Coverage for src / tracekit / core / results.py: 98%

89 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-11 23:04 +0000

1"""Analysis result classes with intermediate data access. 

2 

3This module provides rich result objects that store intermediate computation 

4results (FFT coefficients, filter states, etc.) for multi-step analysis 

5without recomputation. 

6""" 

7 

8from __future__ import annotations 

9 

10from dataclasses import dataclass, field 

11from typing import TYPE_CHECKING, Any 

12 

13import numpy as np 

14 

15if TYPE_CHECKING: 

16 from numpy.typing import NDArray 

17 

18 from .types import WaveformTrace 

19 

20 

21@dataclass 

22class AnalysisResult: 

23 """Container for analysis results with intermediate data. 

24 

25 Stores the final result along with intermediate computation artifacts 

26 like FFT coefficients, filter states, wavelet coefficients, etc. 

27 Enables multi-step analysis without recomputation. 

28 

29 Attributes: 

30 value: The final computed value (measurement, trace, etc.). 

31 intermediates: Dictionary of intermediate computation results. 

32 metadata: Additional metadata about the computation. 

33 

34 Example: 

35 >>> result = AnalysisResult( 

36 ... value=42.5, 

37 ... intermediates={'fft_coeffs': coeffs, 'frequencies': freqs} 

38 ... ) 

39 >>> fft_data = result.get_intermediate('fft_coeffs') 

40 

41 References: 

42 API-005: Intermediate Result Access 

43 """ 

44 

45 value: Any 

46 intermediates: dict[str, Any] = field(default_factory=dict) 

47 metadata: dict[str, Any] = field(default_factory=dict) 

48 

49 def get_intermediate(self, key: str) -> Any: 

50 """Get intermediate result by key. 

51 

52 Args: 

53 key: Name of the intermediate result. 

54 

55 Returns: 

56 The intermediate data. 

57 

58 Raises: 

59 KeyError: If key not found in intermediates. 

60 

61 Example: 

62 >>> spectrum = result.get_intermediate('fft_spectrum') 

63 """ 

64 if key not in self.intermediates: 

65 available = list(self.intermediates.keys()) 

66 raise KeyError(f"Intermediate '{key}' not found. Available: {available}") 

67 return self.intermediates[key] 

68 

69 def has_intermediate(self, key: str) -> bool: 

70 """Check if intermediate result exists. 

71 

72 Args: 

73 key: Name of the intermediate result. 

74 

75 Returns: 

76 True if key exists in intermediates. 

77 

78 Example: 

79 >>> if result.has_intermediate('fft_coeffs'): 

80 ... coeffs = result.get_intermediate('fft_coeffs') 

81 """ 

82 return key in self.intermediates 

83 

84 def list_intermediates(self) -> list[str]: 

85 """List all available intermediate result keys. 

86 

87 Returns: 

88 List of intermediate result names. 

89 

90 Example: 

91 >>> print(result.list_intermediates()) 

92 ['fft_spectrum', 'fft_frequencies', 'fft_power', 'fft_phase'] 

93 """ 

94 return list(self.intermediates.keys()) 

95 

96 

97@dataclass 

98class FFTResult(AnalysisResult): 

99 """Result object for FFT analysis with intermediate data. 

100 

101 Provides convenient access to FFT spectrum, frequencies, power, 

102 and phase information. 

103 

104 Attributes: 

105 spectrum: Complex FFT coefficients. 

106 frequencies: Frequency bins in Hz. 

107 power: Power spectrum (magnitude squared). 

108 phase: Phase spectrum in radians. 

109 trace: Original or transformed trace (optional). 

110 

111 Example: 

112 >>> fft_result = tk.fft(trace, nfft=8192) 

113 >>> spectrum = fft_result.spectrum 

114 >>> frequencies = fft_result.frequencies 

115 >>> power = fft_result.power 

116 >>> phase = fft_result.phase 

117 >>> peak_freq = frequencies[power.argmax()] 

118 

119 References: 

120 API-005: Intermediate Result Access 

121 """ 

122 

123 spectrum: NDArray[np.complex128] = field(default_factory=lambda: np.array([])) 

124 frequencies: NDArray[np.float64] = field(default_factory=lambda: np.array([])) 

125 power: NDArray[np.float64] = field(default_factory=lambda: np.array([])) 

126 phase: NDArray[np.float64] = field(default_factory=lambda: np.array([])) 

127 trace: WaveformTrace | None = None 

128 

129 def __post_init__(self) -> None: 

130 """Initialize intermediate results dictionary.""" 

131 # Store as intermediates for generic access 

132 self.intermediates.update( 

133 { 

134 "spectrum": self.spectrum, 

135 "frequencies": self.frequencies, 

136 "power": self.power, 

137 "phase": self.phase, 

138 } 

139 ) 

140 if self.trace is not None: 

141 self.intermediates["trace"] = self.trace 

142 

143 # Set value to spectrum by default 

144 if self.value is None: 

145 self.value = self.spectrum 

146 

147 @property 

148 def peak_frequency(self) -> float: 

149 """Frequency of maximum power. 

150 

151 Returns: 

152 Frequency in Hz where power spectrum peaks. 

153 

154 Example: 

155 >>> print(f"Peak at {fft_result.peak_frequency:.2e} Hz") 

156 """ 

157 if len(self.power) == 0: 

158 return 0.0 

159 return float(self.frequencies[self.power.argmax()]) 

160 

161 @property 

162 def magnitude(self) -> NDArray[np.float64]: 

163 """Magnitude spectrum (absolute value of FFT). 

164 

165 Returns: 

166 Magnitude of complex spectrum. 

167 

168 Example: 

169 >>> mag = fft_result.magnitude 

170 """ 

171 return np.abs(self.spectrum) 

172 

173 

174@dataclass 

175class FilterResult(AnalysisResult): 

176 """Result object for filter operations with intermediate data. 

177 

178 Provides access to filtered trace along with filter characteristics 

179 like transfer function and impulse response. 

180 

181 Attributes: 

182 trace: Filtered WaveformTrace. 

183 transfer_function: Filter transfer function H(f) (optional). 

184 impulse_response: Filter impulse response h[n] (optional). 

185 frequency_response: Tuple of (frequencies, response) (optional). 

186 filter_coefficients: Filter coefficients (sos or ba format) (optional). 

187 

188 Example: 

189 >>> filter_result = tk.low_pass(trace, cutoff=1e6, return_details=True) 

190 >>> filtered_trace = filter_result.trace 

191 >>> transfer_func = filter_result.transfer_function 

192 >>> impulse_resp = filter_result.impulse_response 

193 

194 References: 

195 API-005: Intermediate Result Access 

196 API-009: Filter Introspection API 

197 """ 

198 

199 trace: WaveformTrace | None = None 

200 transfer_function: NDArray[np.complex128] | None = None 

201 impulse_response: NDArray[np.float64] | None = None 

202 frequency_response: tuple[NDArray[np.float64], NDArray[np.complex128]] | None = None 

203 filter_coefficients: Any | None = None 

204 

205 def __post_init__(self) -> None: 

206 """Initialize intermediate results dictionary.""" 

207 if self.trace is not None: 

208 self.intermediates["trace"] = self.trace 

209 if self.transfer_function is not None: 

210 self.intermediates["transfer_function"] = self.transfer_function 

211 if self.impulse_response is not None: 

212 self.intermediates["impulse_response"] = self.impulse_response 

213 if self.frequency_response is not None: 

214 self.intermediates["frequency_response"] = self.frequency_response 

215 if self.filter_coefficients is not None: 

216 self.intermediates["filter_coefficients"] = self.filter_coefficients 

217 

218 # Set value to trace by default 

219 if self.value is None: 219 ↛ exitline 219 didn't return from function '__post_init__' because the condition on line 219 was always true

220 self.value = self.trace 

221 

222 

223@dataclass 

224class WaveletResult(AnalysisResult): 

225 """Result object for wavelet transform with intermediate data. 

226 

227 Provides access to wavelet coefficients, scales, and frequencies. 

228 

229 Attributes: 

230 coeffs: Wavelet coefficients. 

231 scales: Wavelet scales. 

232 frequencies: Corresponding frequencies in Hz. 

233 trace: Original trace (optional). 

234 

235 Example: 

236 >>> wavelet_result = tk.wavelet_transform(trace) 

237 >>> coeffs = wavelet_result.coeffs 

238 >>> scales = wavelet_result.scales 

239 >>> frequencies = wavelet_result.frequencies 

240 

241 References: 

242 API-005: Intermediate Result Access 

243 """ 

244 

245 coeffs: NDArray[np.complex128] | None = None 

246 scales: NDArray[np.float64] | None = None 

247 frequencies: NDArray[np.float64] | None = None 

248 trace: WaveformTrace | None = None 

249 

250 def __post_init__(self) -> None: 

251 """Initialize intermediate results dictionary.""" 

252 if self.coeffs is not None: 

253 self.intermediates["coeffs"] = self.coeffs 

254 if self.scales is not None: 

255 self.intermediates["scales"] = self.scales 

256 if self.frequencies is not None: 

257 self.intermediates["frequencies"] = self.frequencies 

258 if self.trace is not None: 

259 self.intermediates["trace"] = self.trace 

260 

261 # Set value to coeffs by default 

262 if self.value is None: 262 ↛ exitline 262 didn't return from function '__post_init__' because the condition on line 262 was always true

263 self.value = self.coeffs 

264 

265 

266@dataclass 

267class MeasurementResult(AnalysisResult): 

268 """Result object for measurements with metadata. 

269 

270 Stores a measurement value along with units, method, and parameters 

271 used for computation. 

272 

273 Attributes: 

274 value: Measured value. 

275 units: Units of measurement (e.g., 'V', 'Hz', 's'). 

276 method: Method or algorithm used. 

277 parameters: Dictionary of parameters used. 

278 confidence: Confidence interval or uncertainty (optional). 

279 

280 Example: 

281 >>> result = MeasurementResult( 

282 ... value=3.3, 

283 ... units='V', 

284 ... method='peak_to_peak', 

285 ... parameters={'window': (0, 1e-3)} 

286 ... ) 

287 

288 References: 

289 API-005: Intermediate Result Access 

290 API-011: Measurement Provenance Tracking 

291 """ 

292 

293 units: str | None = None 

294 method: str | None = None 

295 parameters: dict[str, Any] = field(default_factory=dict) 

296 confidence: tuple[float, float] | None = None 

297 

298 def __post_init__(self) -> None: 

299 """Initialize metadata dictionary.""" 

300 self.metadata.update( 

301 { 

302 "units": self.units, 

303 "method": self.method, 

304 "parameters": self.parameters, 

305 "confidence": self.confidence, 

306 } 

307 ) 

308 

309 def __str__(self) -> str: 

310 """String representation of measurement.""" 

311 if self.units: 

312 return f"{self.value} {self.units}" 

313 return str(self.value) 

314 

315 def __repr__(self) -> str: 

316 """Detailed representation of measurement.""" 

317 parts = [f"value={self.value}"] 

318 if self.units: 

319 parts.append(f"units='{self.units}'") 

320 if self.method: 

321 parts.append(f"method='{self.method}'") 

322 return f"MeasurementResult({', '.join(parts)})" 

323 

324 

325__all__ = [ 

326 "AnalysisResult", 

327 "FFTResult", 

328 "FilterResult", 

329 "MeasurementResult", 

330 "WaveletResult", 

331]