Coverage for src / tracekit / analyzers / signal_integrity / embedding.py: 100%

83 statements  

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

1"""Channel embedding and de-embedding functions. 

2 

3This module provides time-domain de-embedding to remove fixture 

4effects and embedding to simulate channel effects. 

5 

6 

7Example: 

8 >>> from tracekit.analyzers.signal_integrity.embedding import deembed 

9 >>> clean_trace = deembed(trace, s_params) 

10 

11References: 

12 IEEE 370-2020: Standard for Electrical Characterization of PCBs 

13""" 

14 

15from __future__ import annotations 

16 

17import numpy as np 

18 

19from tracekit.analyzers.signal_integrity.sparams import ( 

20 SParameterData, 

21 abcd_to_s, 

22 s_to_abcd, 

23) 

24from tracekit.core.exceptions import AnalysisError 

25from tracekit.core.types import WaveformTrace 

26 

27 

28def deembed( 

29 trace: WaveformTrace, 

30 s_params: SParameterData, 

31 *, 

32 method: str = "frequency_domain", 

33 regularization: float = 1e-6, 

34) -> WaveformTrace: 

35 """Remove fixture effects from waveform using S-parameters. 

36 

37 Applies the inverse of the fixture transfer function in the 

38 frequency domain to recover the signal at the DUT reference plane. 

39 

40 Args: 

41 trace: Input waveform trace. 

42 s_params: S-parameters of fixture to remove. 

43 method: De-embedding method ("frequency_domain" or "time_domain"). 

44 regularization: Regularization for matrix inversion. 

45 

46 Returns: 

47 De-embedded waveform trace. 

48 

49 Raises: 

50 ValueError: If method is unknown. 

51 AnalysisError: If de-embedding fails. 

52 

53 Example: 

54 >>> clean = deembed(measured_trace, fixture_sparams) 

55 >>> # clean now has fixture effects removed 

56 

57 References: 

58 IEEE 370-2020 Section 7 

59 """ 

60 if s_params.n_ports != 2: 

61 raise AnalysisError( 

62 f"De-embedding requires 2-port S-parameters, got {s_params.n_ports}-port" 

63 ) 

64 

65 if method == "frequency_domain": 

66 return _deembed_frequency_domain(trace, s_params, regularization) 

67 elif method == "time_domain": 

68 return _deembed_time_domain(trace, s_params) 

69 else: 

70 raise ValueError(f"Unknown method: {method}") 

71 

72 

73def _deembed_frequency_domain( 

74 trace: WaveformTrace, 

75 s_params: SParameterData, 

76 regularization: float, 

77) -> WaveformTrace: 

78 """De-embed using frequency domain approach.""" 

79 data = trace.data 

80 sample_rate = trace.metadata.sample_rate 

81 n = len(data) 

82 

83 # Compute FFT of input signal 

84 signal_fft = np.fft.rfft(data) 

85 frequencies = np.fft.rfftfreq(n, d=1.0 / sample_rate) 

86 

87 # Interpolate S21 to signal frequencies 

88 s21 = np.interp( 

89 frequencies, 

90 s_params.frequencies, 

91 s_params.s_matrix[:, 1, 0], 

92 ) 

93 

94 # Apply inverse transfer function with regularization 

95 # H_inv = 1 / S21, but regularized 

96 magnitude = np.abs(s21) 

97 magnitude_reg = np.maximum(magnitude, regularization) 

98 h_inv = np.conj(s21) / (magnitude_reg**2 + regularization) 

99 

100 # Apply inverse filter 

101 deembedded_fft = signal_fft * h_inv 

102 

103 # Inverse FFT 

104 deembedded_data = np.fft.irfft(deembedded_fft, n=n) 

105 

106 return WaveformTrace( 

107 data=deembedded_data.astype(np.float64), 

108 metadata=trace.metadata, 

109 ) 

110 

111 

112def _deembed_time_domain( 

113 trace: WaveformTrace, 

114 s_params: SParameterData, 

115) -> WaveformTrace: 

116 """De-embed using time domain impulse response.""" 

117 # Convert S-parameters to impulse response 

118 s21 = s_params.s_matrix[:, 1, 0] 

119 frequencies = s_params.frequencies 

120 

121 # Create symmetric frequency axis for IFFT 

122 n_freq = len(frequencies) 

123 2 * (n_freq - 1) 

124 

125 # Pad with conjugate symmetric extension 

126 s21_symmetric = np.concatenate([s21, np.conj(s21[-2:0:-1])]) 

127 

128 # IFFT to get impulse response 

129 impulse_response = np.fft.ifft(s21_symmetric).real 

130 

131 # Create inverse filter (approximate) 

132 # Use Wiener deconvolution approach 

133 data = trace.data 

134 n = len(data) 

135 

136 # Pad impulse response 

137 ir_padded = np.zeros(n) 

138 ir_len = min(len(impulse_response), n) 

139 ir_padded[:ir_len] = impulse_response[:ir_len] 

140 

141 # FFT-based deconvolution 

142 data_fft = np.fft.fft(data) 

143 ir_fft = np.fft.fft(ir_padded) 

144 

145 # Wiener filter 

146 noise_power = 0.01 # Assumed noise level 

147 ir_power = np.abs(ir_fft) ** 2 

148 wiener = np.conj(ir_fft) / (ir_power + noise_power) 

149 

150 deembedded_fft = data_fft * wiener 

151 deembedded_data = np.fft.ifft(deembedded_fft).real 

152 

153 return WaveformTrace( 

154 data=deembedded_data.astype(np.float64), 

155 metadata=trace.metadata, 

156 ) 

157 

158 

159def embed( 

160 trace: WaveformTrace, 

161 s_params: SParameterData, 

162) -> WaveformTrace: 

163 """Apply channel effects to waveform using S-parameters. 

164 

165 Convolves the signal with the channel impulse response 

166 derived from S21 to simulate channel effects. 

167 

168 Args: 

169 trace: Input (ideal) waveform trace. 

170 s_params: S-parameters of channel to apply. 

171 

172 Returns: 

173 Waveform with channel effects applied. 

174 

175 Raises: 

176 AnalysisError: If embedding fails. 

177 

178 Example: 

179 >>> degraded = embed(ideal_trace, channel_sparams) 

180 >>> # degraded now has channel ISI/loss 

181 

182 References: 

183 IEEE 370-2020 Section 7 

184 """ 

185 if s_params.n_ports != 2: 

186 raise AnalysisError(f"Embedding requires 2-port S-parameters, got {s_params.n_ports}-port") 

187 

188 data = trace.data 

189 sample_rate = trace.metadata.sample_rate 

190 n = len(data) 

191 

192 # Compute FFT 

193 signal_fft = np.fft.rfft(data) 

194 frequencies = np.fft.rfftfreq(n, d=1.0 / sample_rate) 

195 

196 # Interpolate S21 to signal frequencies 

197 s21 = np.interp( 

198 frequencies, 

199 s_params.frequencies, 

200 s_params.s_matrix[:, 1, 0], 

201 ) 

202 

203 # Apply transfer function 

204 embedded_fft = signal_fft * s21 

205 

206 # Inverse FFT 

207 embedded_data = np.fft.irfft(embedded_fft, n=n) 

208 

209 return WaveformTrace( 

210 data=embedded_data.astype(np.float64), 

211 metadata=trace.metadata, 

212 ) 

213 

214 

215def cascade_deembed( 

216 trace: WaveformTrace, 

217 fixtures: list[SParameterData], 

218 *, 

219 regularization: float = 1e-6, 

220) -> WaveformTrace: 

221 """Remove multiple fixture effects from waveform. 

222 

223 Cascades multiple fixtures and removes their combined effect 

224 using ABCD matrix multiplication. 

225 

226 Args: 

227 trace: Input waveform trace. 

228 fixtures: List of S-parameter fixtures to remove. 

229 regularization: Regularization for matrix inversion. 

230 

231 Returns: 

232 De-embedded waveform trace. 

233 

234 Example: 

235 >>> clean = cascade_deembed(trace, [fixture1, fixture2]) 

236 

237 References: 

238 IEEE 370-2020 Section 7.3 

239 """ 

240 if len(fixtures) == 0: 

241 return trace 

242 

243 if len(fixtures) == 1: 

244 return deembed(trace, fixtures[0], regularization=regularization) 

245 

246 # Find common frequency points 

247 all_freqs = [f.frequencies for f in fixtures] 

248 min_freq = max(f.min() for f in all_freqs) 

249 max_freq = min(f.max() for f in all_freqs) 

250 n_freq = min(len(f) for f in all_freqs) 

251 

252 common_freqs = np.linspace(min_freq, max_freq, n_freq) 

253 

254 # Convert each fixture to ABCD and cascade 

255 abcd_cascade = np.zeros((n_freq, 2, 2), dtype=np.complex128) 

256 abcd_cascade[:, 0, 0] = 1 

257 abcd_cascade[:, 1, 1] = 1 # Identity matrix 

258 

259 for fixture in fixtures: 

260 abcd = s_to_abcd(fixture) 

261 

262 # Interpolate to common frequencies 

263 abcd_interp = np.zeros((n_freq, 2, 2), dtype=np.complex128) 

264 for i in range(2): 

265 for j in range(2): 

266 abcd_interp[:, i, j] = np.interp( 

267 common_freqs, 

268 fixture.frequencies, 

269 abcd[:, i, j], 

270 ) 

271 

272 # Matrix multiply for each frequency 

273 for f_idx in range(n_freq): 

274 abcd_cascade[f_idx] = abcd_cascade[f_idx] @ abcd_interp[f_idx] 

275 

276 # Convert cascaded ABCD back to S-parameters 

277 s_cascade = abcd_to_s(abcd_cascade, z0=fixtures[0].z0) 

278 

279 # Create combined S-parameter object 

280 combined = SParameterData( 

281 frequencies=common_freqs, 

282 s_matrix=s_cascade, 

283 n_ports=2, 

284 z0=fixtures[0].z0, 

285 ) 

286 

287 return deembed(trace, combined, regularization=regularization) 

288 

289 

290__all__ = [ 

291 "cascade_deembed", 

292 "deembed", 

293 "embed", 

294]