Coverage for src / tracekit / component / transmission_line.py: 88%
95 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"""Transmission line analysis for TraceKit.
3This module provides transmission line characterization including
4characteristic impedance, propagation delay, and velocity factor.
7Example:
8 >>> from tracekit.component import transmission_line_analysis
9 >>> result = transmission_line_analysis(tdr_trace)
11References:
12 IPC-TM-650 2.5.5.7: Characteristic Impedance of Lines on PCBs
13 IEEE 370-2020: Electrical Characterization of Interconnects
14"""
16from __future__ import annotations
18from dataclasses import dataclass, field
19from typing import TYPE_CHECKING
21import numpy as np
22from scipy import signal as sp_signal
24if TYPE_CHECKING:
25 from numpy.typing import NDArray
27 from tracekit.core.types import WaveformTrace
30@dataclass
31class TransmissionLineResult:
32 """Transmission line characterization result.
34 Attributes:
35 z0: Characteristic impedance in ohms.
36 propagation_delay: Propagation delay in seconds.
37 velocity_factor: Velocity factor (0-1).
38 velocity: Propagation velocity in m/s.
39 length: Estimated line length in meters.
40 loss: Estimated loss in dB (if available).
41 return_loss: Return loss in dB (if available).
42 insertion_loss: Insertion loss in dB (if available).
43 statistics: Additional measurements.
44 """
46 z0: float
47 propagation_delay: float
48 velocity_factor: float
49 velocity: float
50 length: float
51 loss: float | None = None
52 return_loss: float | None = None
53 insertion_loss: float | None = None
54 statistics: dict = field(default_factory=dict) # type: ignore[type-arg]
57def transmission_line_analysis(
58 trace: WaveformTrace,
59 *,
60 z0_source: float = 50.0,
61 line_length: float | None = None,
62 dielectric_constant: float | None = None,
63) -> TransmissionLineResult:
64 """Analyze transmission line from TDR measurement.
66 Characterizes a transmission line by extracting characteristic
67 impedance, propagation delay, and loss parameters.
69 Args:
70 trace: TDR reflection waveform.
71 z0_source: Source impedance (default 50 ohms).
72 line_length: Known line length in meters (improves accuracy).
73 dielectric_constant: Known dielectric constant (improves velocity).
75 Returns:
76 TransmissionLineResult with line parameters.
78 Example:
79 >>> result = transmission_line_analysis(tdr_trace, line_length=0.1)
80 >>> print(f"Z0 = {result.z0:.1f} ohms, delay = {result.propagation_delay*1e9:.2f} ns")
81 """
82 from tracekit.component.impedance import extract_impedance
84 # Speed of light
85 c = 299792458.0
87 # Extract impedance profile
88 z0, profile = extract_impedance(trace, z0_source=z0_source)
90 # Estimate propagation delay from reflection
91 data = trace.data.astype(np.float64)
92 sample_rate = trace.metadata.sample_rate
94 # Find incident edge and first reflection
95 incident_time, reflection_time = _find_reflection_times(data, sample_rate)
96 round_trip_time = reflection_time - incident_time
98 # Propagation delay is half the round-trip time
99 propagation_delay = round_trip_time / 2
101 # Calculate velocity
102 if line_length is not None:
103 # Use known length
104 velocity = line_length / propagation_delay if propagation_delay > 0 else c * 0.66
105 velocity_factor = velocity / c
106 elif dielectric_constant is not None:
107 # Calculate from dielectric constant
108 velocity_factor = 1 / np.sqrt(dielectric_constant)
109 velocity = c * velocity_factor
110 line_length = velocity * propagation_delay
111 else:
112 # Estimate from typical FR4
113 velocity_factor = 0.66
114 velocity = c * velocity_factor
115 line_length = velocity * propagation_delay
117 # Estimate loss from reflection amplitude decay
118 loss = _estimate_loss(data, sample_rate, propagation_delay)
120 # Estimate return loss
121 return_loss = _calculate_return_loss(z0, z0_source)
123 return TransmissionLineResult(
124 z0=z0,
125 propagation_delay=propagation_delay,
126 velocity_factor=velocity_factor,
127 velocity=velocity,
128 length=line_length,
129 loss=loss,
130 return_loss=return_loss,
131 statistics={
132 "incident_time": incident_time,
133 "reflection_time": reflection_time,
134 "round_trip_time": round_trip_time,
135 "z0_std": profile.statistics.get("z0_std", 0),
136 },
137 )
140def characteristic_impedance(
141 trace: WaveformTrace,
142 *,
143 z0_source: float = 50.0,
144 start_time: float | None = None,
145 end_time: float | None = None,
146) -> float:
147 """Extract characteristic impedance from TDR measurement.
149 Calculates the characteristic impedance from a stable region
150 of the TDR waveform.
152 Args:
153 trace: TDR reflection waveform.
154 z0_source: Source impedance.
155 start_time: Start of analysis window (seconds).
156 end_time: End of analysis window (seconds).
158 Returns:
159 Characteristic impedance in ohms.
161 Example:
162 >>> z0 = characteristic_impedance(tdr_trace)
163 >>> print(f"Z0 = {z0:.1f} ohms")
164 """
165 from tracekit.component.impedance import extract_impedance
167 z0, _ = extract_impedance(
168 trace,
169 z0_source=z0_source,
170 start_time=start_time,
171 end_time=end_time,
172 )
173 return z0
176def propagation_delay(
177 trace: WaveformTrace,
178 *,
179 threshold: float = 0.5,
180) -> float:
181 """Measure propagation delay from TDR waveform.
183 Calculates the one-way propagation delay from the incident edge
184 to the first reflection.
186 Args:
187 trace: TDR reflection waveform.
188 threshold: Threshold level for edge detection (normalized).
190 Returns:
191 Propagation delay in seconds.
193 Example:
194 >>> delay = propagation_delay(tdr_trace)
195 >>> print(f"Delay = {delay * 1e9:.2f} ns")
196 """
197 data = trace.data.astype(np.float64)
198 sample_rate = trace.metadata.sample_rate
200 incident_time, reflection_time = _find_reflection_times(data, sample_rate, threshold)
202 return (reflection_time - incident_time) / 2
205def velocity_factor(
206 trace: WaveformTrace,
207 line_length: float,
208) -> float:
209 """Calculate velocity factor from TDR and known length.
211 Determines the propagation velocity as a fraction of the
212 speed of light.
214 Args:
215 trace: TDR reflection waveform.
216 line_length: Known line length in meters.
218 Returns:
219 Velocity factor (0 to 1).
221 Example:
222 >>> vf = velocity_factor(tdr_trace, line_length=0.1)
223 >>> print(f"Velocity factor = {vf:.2f}")
224 """
225 c = 299792458.0
226 delay = propagation_delay(trace)
228 if delay > 0: 228 ↛ 231line 228 didn't jump to line 231 because the condition on line 228 was always true
229 velocity = line_length / delay
230 return float(min(1.0, velocity / c))
231 return 0.66 # Default for FR4
234def _find_reflection_times(
235 data: NDArray[np.float64],
236 sample_rate: float,
237 threshold: float = 0.5,
238) -> tuple[float, float]:
239 """Find incident and reflection edge times."""
240 # Normalize data
241 data_norm = (data - np.min(data)) / (np.ptp(data) + 1e-10)
243 # Calculate derivative to find edges
244 derivative = np.abs(np.diff(data_norm))
246 # Find peaks in derivative
247 peaks, _ = sp_signal.find_peaks(derivative, height=0.1 * np.max(derivative))
249 if len(peaks) < 2:
250 # Fallback: use threshold crossing
251 above_thresh = data_norm > threshold
252 crossings = np.where(np.diff(above_thresh.astype(int)))[0]
254 if len(crossings) >= 2: 254 ↛ 255line 254 didn't jump to line 255 because the condition on line 254 was never true
255 incident_idx = crossings[0]
256 reflection_idx = crossings[1]
257 else:
258 # Can't find edges
259 incident_idx = 0
260 reflection_idx = len(data) // 2
261 else:
262 incident_idx = peaks[0]
263 reflection_idx = peaks[1]
265 incident_time = incident_idx / sample_rate
266 reflection_time = reflection_idx / sample_rate
268 return incident_time, reflection_time
271def _estimate_loss(
272 data: NDArray[np.float64],
273 sample_rate: float,
274 delay: float,
275) -> float | None:
276 """Estimate transmission line loss from reflection amplitudes."""
277 # Find incident and reflected amplitudes
278 incident_region = data[: int(delay * sample_rate * 0.5)]
279 if len(incident_region) == 0: 279 ↛ 280line 279 didn't jump to line 280 because the condition on line 279 was never true
280 return None
282 incident_amp = np.max(incident_region) - np.min(incident_region)
284 # Find reflected amplitude (after round-trip)
285 reflection_start = int(delay * 2 * sample_rate)
286 if reflection_start >= len(data): 286 ↛ 287line 286 didn't jump to line 287 because the condition on line 286 was never true
287 return None
289 reflected_region = data[reflection_start:]
290 if len(reflected_region) == 0: 290 ↛ 291line 290 didn't jump to line 291 because the condition on line 290 was never true
291 return None
293 reflected_amp = np.max(reflected_region) - np.min(reflected_region)
295 if incident_amp > 0:
296 # Loss in dB = 20 * log10(reflected / incident)
297 # But this is round-trip, so divide by 2 for one-way
298 ratio = reflected_amp / incident_amp
299 if ratio > 0 and ratio < 1: 299 ↛ 302line 299 didn't jump to line 302 because the condition on line 299 was always true
300 return float(-20 * np.log10(ratio) / 2)
302 return None
305def _calculate_return_loss(z0: float, z0_source: float) -> float:
306 """Calculate return loss from impedance mismatch."""
307 if z0 + z0_source > 0: 307 ↛ 312line 307 didn't jump to line 312 because the condition on line 307 was always true
308 rho = abs((z0 - z0_source) / (z0 + z0_source))
309 if rho > 0:
310 return float(-20 * np.log10(rho))
311 return float("inf") # Perfect match
312 return 0.0