Coverage for src / tracekit / analyzers / jitter / ber.py: 84%
82 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"""BER-related jitter analysis functions.
3This module provides total jitter at BER calculations, bathtub curve
4generation, and eye opening measurements using the dual-Dirac model.
7Example:
8 >>> from tracekit.analyzers.jitter.ber import tj_at_ber, bathtub_curve
9 >>> tj = tj_at_ber(rj_rms=1e-12, dj_pp=10e-12, ber=1e-12)
10 >>> positions, ber_values = bathtub_curve(tie_data, unit_interval=1e-9)
12References:
13 IEEE 2414-2020: Standard for Jitter and Phase Noise
14"""
16from __future__ import annotations
18from dataclasses import dataclass
19from typing import TYPE_CHECKING
21import numpy as np
22from scipy import special
24if TYPE_CHECKING:
25 from numpy.typing import NDArray
28@dataclass
29class BathtubCurveResult:
30 """Result of bathtub curve generation.
32 Attributes:
33 positions: Sampling positions in UI (0.0 to 1.0).
34 ber_left: BER values for left side of eye.
35 ber_right: BER values for right side of eye.
36 ber_total: Combined BER (left + right).
37 eye_opening: Eye opening in UI at specified target BER.
38 target_ber: Target BER for eye opening calculation.
39 unit_interval: Unit interval in seconds.
40 """
42 positions: NDArray[np.float64]
43 ber_left: NDArray[np.float64]
44 ber_right: NDArray[np.float64]
45 ber_total: NDArray[np.float64]
46 eye_opening: float
47 target_ber: float
48 unit_interval: float
51def q_factor_from_ber(ber: float) -> float:
52 """Calculate Q-factor from target BER.
54 The Q-factor is the number of standard deviations from the mean
55 for a Gaussian distribution to achieve the target BER.
57 Args:
58 ber: Bit error rate (e.g., 1e-12).
60 Returns:
61 Q-factor value.
63 Example:
64 >>> q = q_factor_from_ber(1e-12)
65 >>> print(f"Q(1e-12) = {q:.3f}") # ~7.034
67 References:
68 IEEE 2414-2020: Q = sqrt(2) * erfc_inv(2 * BER)
69 """
70 if ber <= 0 or ber >= 0.5:
71 return np.nan # type: ignore[no-any-return]
73 # BER = 0.5 * erfc(Q / sqrt(2))
74 # erfc(Q / sqrt(2)) = 2 * BER
75 # Q / sqrt(2) = erfc_inv(2 * BER)
76 # Q = sqrt(2) * erfc_inv(2 * BER)
78 q = np.sqrt(2) * special.erfcinv(2 * ber)
79 return float(q)
82def ber_from_q_factor(q: float) -> float:
83 """Calculate BER from Q-factor.
85 Args:
86 q: Q-factor value.
88 Returns:
89 Bit error rate.
91 Example:
92 >>> ber = ber_from_q_factor(7.034)
93 >>> print(f"BER = {ber:.2e}") # ~1e-12
94 """
95 if q <= 0: 95 ↛ 96line 95 didn't jump to line 96 because the condition on line 95 was never true
96 return 0.5
98 # BER = 0.5 * erfc(Q / sqrt(2))
99 ber = 0.5 * special.erfc(q / np.sqrt(2))
100 return float(ber)
103def tj_at_ber(
104 rj_rms: float,
105 dj_pp: float,
106 ber: float = 1e-12,
107) -> float:
108 """Calculate total jitter at specified BER using dual-Dirac model.
110 The dual-Dirac model combines random and deterministic jitter:
111 TJ(BER) = 2 * Q(BER) * RJ_rms + DJ_pp
113 Common Q values:
114 - Q(1e-12) = 7.034
115 - Q(1e-15) = 7.941
117 Args:
118 rj_rms: Random jitter RMS in seconds.
119 dj_pp: Deterministic jitter peak-to-peak in seconds.
120 ber: Target bit error rate (default 1e-12).
122 Returns:
123 Total jitter in seconds at specified BER.
125 Raises:
126 ValueError: If rj_rms is negative.
128 Example:
129 >>> tj = tj_at_ber(rj_rms=1e-12, dj_pp=10e-12, ber=1e-12)
130 >>> print(f"TJ@1e-12: {tj * 1e12:.2f} ps")
132 References:
133 IEEE 2414-2020 Section 6.6
134 """
135 if rj_rms < 0:
136 raise ValueError("RJ must be non-negative")
138 if dj_pp < 0: 138 ↛ 139line 138 didn't jump to line 139 because the condition on line 138 was never true
139 raise ValueError("DJ must be non-negative")
141 q = q_factor_from_ber(ber)
143 if np.isnan(q):
144 return np.nan # type: ignore[no-any-return]
146 # TJ = 2 * Q * RJ_rms + DJ_pp
147 tj = 2 * q * rj_rms + dj_pp
149 return tj
152def bathtub_curve(
153 tie_data: NDArray[np.float64],
154 unit_interval: float,
155 *,
156 n_points: int = 100,
157 target_ber: float = 1e-12,
158 rj_rms: float | None = None,
159 dj_delta: float | None = None,
160) -> BathtubCurveResult:
161 """Generate bathtub curve showing BER vs. sampling position.
163 The bathtub curve shows how bit error rate varies across the
164 unit interval, with low BER in the center (eye opening) and
165 high BER near the edges.
167 Args:
168 tie_data: Time Interval Error data in seconds.
169 unit_interval: Unit interval (bit period) in seconds.
170 n_points: Number of points in the curve.
171 target_ber: Target BER for eye opening calculation.
172 rj_rms: Pre-computed RJ (extracted from data if None).
173 dj_delta: Pre-computed DJ delta (extracted from data if None).
175 Returns:
176 BathtubCurveResult with position and BER arrays.
178 Example:
179 >>> result = bathtub_curve(tie_data, unit_interval=1e-9)
180 >>> print(f"Eye opening: {result.eye_opening:.3f} UI at BER=1e-12")
182 References:
183 IEEE 2414-2020 Section 6.7
184 """
185 from tracekit.analyzers.jitter.decomposition import extract_dj, extract_rj
187 valid_data = tie_data[~np.isnan(tie_data)]
189 # Normalize TIE to UI
190 valid_data / unit_interval
192 # Extract jitter components if not provided
193 if rj_rms is None or dj_delta is None: 193 ↛ 207line 193 didn't jump to line 207 because the condition on line 193 was always true
194 try:
195 rj_result = extract_rj(valid_data, min_samples=100)
196 rj_rms = rj_result.rj_rms
197 except Exception:
198 rj_rms = np.std(valid_data)
200 try:
201 dj_result = extract_dj(valid_data, min_samples=100)
202 dj_delta = dj_result.dj_delta
203 except Exception:
204 dj_delta = 0.0
206 # Convert to UI
207 sigma_ui = rj_rms / unit_interval
208 delta_ui = dj_delta / unit_interval
210 # Generate sampling positions (0 to 1 UI)
211 positions = np.linspace(0, 1, n_points)
213 # Calculate BER at each position using dual-Dirac model
214 # Left side: probability of sampling a '1' when '0' is sent
215 # Right side: probability of sampling a '0' when '1' is sent
217 # For a dual-Dirac distribution centered at 0.5 UI:
218 # Left Dirac at 0.5 - delta, Right Dirac at 0.5 + delta
220 ber_left = np.zeros(n_points)
221 ber_right = np.zeros(n_points)
223 for i, pos in enumerate(positions):
224 # Left BER: Q-function from left edge
225 if sigma_ui > 0: 225 ↛ 235line 225 didn't jump to line 235 because the condition on line 225 was always true
226 # Distance from left edge to sampling point
227 q_left = (pos - delta_ui) / sigma_ui
228 ber_left[i] = 0.5 * special.erfc(q_left / np.sqrt(2))
230 # Distance from right edge to sampling point
231 q_right = (1 - pos - delta_ui) / sigma_ui
232 ber_right[i] = 0.5 * special.erfc(q_right / np.sqrt(2))
233 else:
234 # No random jitter - step function
235 ber_left[i] = 0.5 if pos <= delta_ui else 0
236 ber_right[i] = 0.5 if pos >= (1 - delta_ui) else 0
238 # Total BER is sum of left and right
239 ber_total = ber_left + ber_right
241 # Clip to valid range
242 ber_total = np.clip(ber_total, 1e-18, 0.5)
243 ber_left = np.clip(ber_left, 1e-18, 0.5)
244 ber_right = np.clip(ber_right, 1e-18, 0.5)
246 # Calculate eye opening at target BER
247 eye_opening = _calculate_eye_opening(positions, ber_total, target_ber)
249 return BathtubCurveResult(
250 positions=positions,
251 ber_left=ber_left,
252 ber_right=ber_right,
253 ber_total=ber_total,
254 eye_opening=eye_opening,
255 target_ber=target_ber,
256 unit_interval=unit_interval,
257 )
260def _calculate_eye_opening(
261 positions: NDArray[np.float64],
262 ber: NDArray[np.float64],
263 target_ber: float,
264) -> float:
265 """Calculate eye opening at target BER.
267 Args:
268 positions: Sampling positions in UI.
269 ber: BER values at each position.
270 target_ber: Target BER for eye opening.
272 Returns:
273 Eye opening in UI.
274 """
275 # Find positions where BER <= target_ber
276 valid_positions = positions[ber <= target_ber]
278 if len(valid_positions) == 0: 278 ↛ 279line 278 didn't jump to line 279 because the condition on line 278 was never true
279 return 0.0
281 # Eye opening is the range of valid positions
282 eye_opening = float(np.max(valid_positions) - np.min(valid_positions))
284 return eye_opening
287def eye_opening_at_ber(
288 rj_rms: float,
289 dj_pp: float,
290 unit_interval: float,
291 target_ber: float = 1e-12,
292) -> float:
293 """Calculate horizontal eye opening at target BER.
295 Uses the dual-Dirac model to calculate the eye opening width
296 at a specified BER level.
298 Args:
299 rj_rms: Random jitter RMS in seconds.
300 dj_pp: Deterministic jitter peak-to-peak in seconds.
301 unit_interval: Unit interval in seconds.
302 target_ber: Target BER level.
304 Returns:
305 Eye opening in UI (0.0 to 1.0).
307 Example:
308 >>> opening = eye_opening_at_ber(1e-12, 10e-12, 100e-12, 1e-12)
309 >>> print(f"Eye opening: {opening:.3f} UI")
310 """
311 # Total jitter at BER
312 tj = tj_at_ber(rj_rms, dj_pp, target_ber)
314 # Eye opening = UI - TJ
315 opening_seconds = unit_interval - tj
317 if opening_seconds <= 0: 317 ↛ 318line 317 didn't jump to line 318 because the condition on line 317 was never true
318 return 0.0
320 # Convert to UI
321 opening_ui = opening_seconds / unit_interval
323 return max(0.0, min(1.0, opening_ui))
326__all__ = [
327 "BathtubCurveResult",
328 "bathtub_curve",
329 "ber_from_q_factor",
330 "eye_opening_at_ber",
331 "q_factor_from_ber",
332 "tj_at_ber",
333]