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

1"""BER-related jitter analysis functions. 

2 

3This module provides total jitter at BER calculations, bathtub curve 

4generation, and eye opening measurements using the dual-Dirac model. 

5 

6 

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) 

11 

12References: 

13 IEEE 2414-2020: Standard for Jitter and Phase Noise 

14""" 

15 

16from __future__ import annotations 

17 

18from dataclasses import dataclass 

19from typing import TYPE_CHECKING 

20 

21import numpy as np 

22from scipy import special 

23 

24if TYPE_CHECKING: 

25 from numpy.typing import NDArray 

26 

27 

28@dataclass 

29class BathtubCurveResult: 

30 """Result of bathtub curve generation. 

31 

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 """ 

41 

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 

49 

50 

51def q_factor_from_ber(ber: float) -> float: 

52 """Calculate Q-factor from target BER. 

53 

54 The Q-factor is the number of standard deviations from the mean 

55 for a Gaussian distribution to achieve the target BER. 

56 

57 Args: 

58 ber: Bit error rate (e.g., 1e-12). 

59 

60 Returns: 

61 Q-factor value. 

62 

63 Example: 

64 >>> q = q_factor_from_ber(1e-12) 

65 >>> print(f"Q(1e-12) = {q:.3f}") # ~7.034 

66 

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] 

72 

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) 

77 

78 q = np.sqrt(2) * special.erfcinv(2 * ber) 

79 return float(q) 

80 

81 

82def ber_from_q_factor(q: float) -> float: 

83 """Calculate BER from Q-factor. 

84 

85 Args: 

86 q: Q-factor value. 

87 

88 Returns: 

89 Bit error rate. 

90 

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 

97 

98 # BER = 0.5 * erfc(Q / sqrt(2)) 

99 ber = 0.5 * special.erfc(q / np.sqrt(2)) 

100 return float(ber) 

101 

102 

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. 

109 

110 The dual-Dirac model combines random and deterministic jitter: 

111 TJ(BER) = 2 * Q(BER) * RJ_rms + DJ_pp 

112 

113 Common Q values: 

114 - Q(1e-12) = 7.034 

115 - Q(1e-15) = 7.941 

116 

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). 

121 

122 Returns: 

123 Total jitter in seconds at specified BER. 

124 

125 Raises: 

126 ValueError: If rj_rms is negative. 

127 

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") 

131 

132 References: 

133 IEEE 2414-2020 Section 6.6 

134 """ 

135 if rj_rms < 0: 

136 raise ValueError("RJ must be non-negative") 

137 

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") 

140 

141 q = q_factor_from_ber(ber) 

142 

143 if np.isnan(q): 

144 return np.nan # type: ignore[no-any-return] 

145 

146 # TJ = 2 * Q * RJ_rms + DJ_pp 

147 tj = 2 * q * rj_rms + dj_pp 

148 

149 return tj 

150 

151 

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. 

162 

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. 

166 

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). 

174 

175 Returns: 

176 BathtubCurveResult with position and BER arrays. 

177 

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") 

181 

182 References: 

183 IEEE 2414-2020 Section 6.7 

184 """ 

185 from tracekit.analyzers.jitter.decomposition import extract_dj, extract_rj 

186 

187 valid_data = tie_data[~np.isnan(tie_data)] 

188 

189 # Normalize TIE to UI 

190 valid_data / unit_interval 

191 

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) 

199 

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 

205 

206 # Convert to UI 

207 sigma_ui = rj_rms / unit_interval 

208 delta_ui = dj_delta / unit_interval 

209 

210 # Generate sampling positions (0 to 1 UI) 

211 positions = np.linspace(0, 1, n_points) 

212 

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 

216 

217 # For a dual-Dirac distribution centered at 0.5 UI: 

218 # Left Dirac at 0.5 - delta, Right Dirac at 0.5 + delta 

219 

220 ber_left = np.zeros(n_points) 

221 ber_right = np.zeros(n_points) 

222 

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)) 

229 

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 

237 

238 # Total BER is sum of left and right 

239 ber_total = ber_left + ber_right 

240 

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) 

245 

246 # Calculate eye opening at target BER 

247 eye_opening = _calculate_eye_opening(positions, ber_total, target_ber) 

248 

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 ) 

258 

259 

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. 

266 

267 Args: 

268 positions: Sampling positions in UI. 

269 ber: BER values at each position. 

270 target_ber: Target BER for eye opening. 

271 

272 Returns: 

273 Eye opening in UI. 

274 """ 

275 # Find positions where BER <= target_ber 

276 valid_positions = positions[ber <= target_ber] 

277 

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 

280 

281 # Eye opening is the range of valid positions 

282 eye_opening = float(np.max(valid_positions) - np.min(valid_positions)) 

283 

284 return eye_opening 

285 

286 

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. 

294 

295 Uses the dual-Dirac model to calculate the eye opening width 

296 at a specified BER level. 

297 

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. 

303 

304 Returns: 

305 Eye opening in UI (0.0 to 1.0). 

306 

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) 

313 

314 # Eye opening = UI - TJ 

315 opening_seconds = unit_interval - tj 

316 

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 

319 

320 # Convert to UI 

321 opening_ui = opening_seconds / unit_interval 

322 

323 return max(0.0, min(1.0, opening_ui)) 

324 

325 

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]