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
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 23:04 +0000
1"""Channel embedding and de-embedding functions.
3This module provides time-domain de-embedding to remove fixture
4effects and embedding to simulate channel effects.
7Example:
8 >>> from tracekit.analyzers.signal_integrity.embedding import deembed
9 >>> clean_trace = deembed(trace, s_params)
11References:
12 IEEE 370-2020: Standard for Electrical Characterization of PCBs
13"""
15from __future__ import annotations
17import numpy as np
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
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.
37 Applies the inverse of the fixture transfer function in the
38 frequency domain to recover the signal at the DUT reference plane.
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.
46 Returns:
47 De-embedded waveform trace.
49 Raises:
50 ValueError: If method is unknown.
51 AnalysisError: If de-embedding fails.
53 Example:
54 >>> clean = deembed(measured_trace, fixture_sparams)
55 >>> # clean now has fixture effects removed
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 )
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}")
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)
83 # Compute FFT of input signal
84 signal_fft = np.fft.rfft(data)
85 frequencies = np.fft.rfftfreq(n, d=1.0 / sample_rate)
87 # Interpolate S21 to signal frequencies
88 s21 = np.interp(
89 frequencies,
90 s_params.frequencies,
91 s_params.s_matrix[:, 1, 0],
92 )
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)
100 # Apply inverse filter
101 deembedded_fft = signal_fft * h_inv
103 # Inverse FFT
104 deembedded_data = np.fft.irfft(deembedded_fft, n=n)
106 return WaveformTrace(
107 data=deembedded_data.astype(np.float64),
108 metadata=trace.metadata,
109 )
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
121 # Create symmetric frequency axis for IFFT
122 n_freq = len(frequencies)
123 2 * (n_freq - 1)
125 # Pad with conjugate symmetric extension
126 s21_symmetric = np.concatenate([s21, np.conj(s21[-2:0:-1])])
128 # IFFT to get impulse response
129 impulse_response = np.fft.ifft(s21_symmetric).real
131 # Create inverse filter (approximate)
132 # Use Wiener deconvolution approach
133 data = trace.data
134 n = len(data)
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]
141 # FFT-based deconvolution
142 data_fft = np.fft.fft(data)
143 ir_fft = np.fft.fft(ir_padded)
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)
150 deembedded_fft = data_fft * wiener
151 deembedded_data = np.fft.ifft(deembedded_fft).real
153 return WaveformTrace(
154 data=deembedded_data.astype(np.float64),
155 metadata=trace.metadata,
156 )
159def embed(
160 trace: WaveformTrace,
161 s_params: SParameterData,
162) -> WaveformTrace:
163 """Apply channel effects to waveform using S-parameters.
165 Convolves the signal with the channel impulse response
166 derived from S21 to simulate channel effects.
168 Args:
169 trace: Input (ideal) waveform trace.
170 s_params: S-parameters of channel to apply.
172 Returns:
173 Waveform with channel effects applied.
175 Raises:
176 AnalysisError: If embedding fails.
178 Example:
179 >>> degraded = embed(ideal_trace, channel_sparams)
180 >>> # degraded now has channel ISI/loss
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")
188 data = trace.data
189 sample_rate = trace.metadata.sample_rate
190 n = len(data)
192 # Compute FFT
193 signal_fft = np.fft.rfft(data)
194 frequencies = np.fft.rfftfreq(n, d=1.0 / sample_rate)
196 # Interpolate S21 to signal frequencies
197 s21 = np.interp(
198 frequencies,
199 s_params.frequencies,
200 s_params.s_matrix[:, 1, 0],
201 )
203 # Apply transfer function
204 embedded_fft = signal_fft * s21
206 # Inverse FFT
207 embedded_data = np.fft.irfft(embedded_fft, n=n)
209 return WaveformTrace(
210 data=embedded_data.astype(np.float64),
211 metadata=trace.metadata,
212 )
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.
223 Cascades multiple fixtures and removes their combined effect
224 using ABCD matrix multiplication.
226 Args:
227 trace: Input waveform trace.
228 fixtures: List of S-parameter fixtures to remove.
229 regularization: Regularization for matrix inversion.
231 Returns:
232 De-embedded waveform trace.
234 Example:
235 >>> clean = cascade_deembed(trace, [fixture1, fixture2])
237 References:
238 IEEE 370-2020 Section 7.3
239 """
240 if len(fixtures) == 0:
241 return trace
243 if len(fixtures) == 1:
244 return deembed(trace, fixtures[0], regularization=regularization)
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)
252 common_freqs = np.linspace(min_freq, max_freq, n_freq)
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
259 for fixture in fixtures:
260 abcd = s_to_abcd(fixture)
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 )
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]
276 # Convert cascaded ABCD back to S-parameters
277 s_cascade = abcd_to_s(abcd_cascade, z0=fixtures[0].z0)
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 )
287 return deembed(trace, combined, regularization=regularization)
290__all__ = [
291 "cascade_deembed",
292 "deembed",
293 "embed",
294]