| # Copyright 2026 The Fuchsia Authors. All rights reserved. | |
| # Use of this source code is governed by a BSD-style license that can be | |
| # found in the LICENSE file. | |
| import math | |
| # This implements the quantile function (the inverse of the CDF) for | |
| # Student's t-distribution, which is used by perfcompare.py for calculating | |
| # confidence intervals. | |
| # | |
| # This function is implemented by SciPy as scipy.stats.t.ppf(), but we | |
| # don't want to depend on SciPy because we don't have a build of it which | |
| # works with fuchsia-vendored-python, and in general it is difficult to | |
| # build Python libraries that use C extension modules for use with | |
| # fuchsia-vendored-python. It is simpler to provide our own implementation | |
| # of this one function instead! | |
| # | |
| # The implementation here was written by Gemini. | |
| def t_pdf(x, df): | |
| """Probability Density Function of Student's t-distribution.""" | |
| coeff = math.exp(math.lgamma((df + 1) / 2) - math.lgamma(df / 2)) | |
| coeff /= math.sqrt(df * math.pi) | |
| return coeff * (1 + (x**2) / df) ** (-(df + 1) / 2) | |
| def _beta_cf(a, b, x, max_iter=200, eps=1e-12): | |
| """Continued fraction helper for incomplete beta (modified for stability).""" | |
| m = 1 | |
| qab, qap, qam = a + b, a + 1, a - 1 | |
| c, d = 1.0, 1.0 - qab * x / qap | |
| if abs(d) < eps: | |
| d = eps | |
| d = 1.0 / d | |
| h = d | |
| for m in range(1, max_iter + 1): | |
| m2 = 2 * m | |
| # Even step | |
| aa = m * (b - m) * x / ((qam + m2) * (a + m2)) | |
| d = 1.0 + aa * d | |
| if abs(d) < eps: | |
| d = eps | |
| c = 1.0 + aa / c | |
| if abs(c) < eps: | |
| c = eps | |
| d = 1.0 / d | |
| h *= d * c | |
| # Odd step | |
| aa = -(a + m) * (qab + m) * x / ((a + m2) * (qap + m2)) | |
| d = 1.0 + aa * d | |
| if abs(d) < eps: | |
| d = eps | |
| c = 1.0 + aa / c | |
| if abs(c) < eps: | |
| c = eps | |
| d = 1.0 / d | |
| h *= d * c | |
| # Early exit on convergence | |
| if abs(d * c - 1.0) < eps: | |
| break | |
| return h | |
| def beta_inc(a, b, x): | |
| """Regularized incomplete beta function with domain guards.""" | |
| if x <= 0: | |
| return 0.0 | |
| if x >= 1: | |
| return 1.0 | |
| # Log-gamma for the coefficient | |
| lbeta = math.lgamma(a) + math.lgamma(b) - math.lgamma(a + b) | |
| bt = math.exp(a * math.log(x) + b * math.log(1 - x) - lbeta) | |
| if x < (a + 1) / (a + b + 2): | |
| return bt * _beta_cf(a, b, x) / a | |
| else: | |
| return 1.0 - bt * _beta_cf(b, a, 1 - x) / b | |
| def t_cdf(x, df): | |
| """CDF of Student's t using the relationship with the Beta function.""" | |
| if x == 0: | |
| return 0.5 | |
| # The symmetry of the t-distribution | |
| xt = df / (df + x**2) | |
| prob = beta_inc(df / 2, 0.5, xt) | |
| return 1 - 0.5 * prob if x > 0 else 0.5 * prob | |
| def t_ppf(p, df, tol=1e-10): | |
| """Inverse CDF (Quantile function) using Newton-Raphson.""" | |
| if p <= 0 or p >= 1: | |
| raise ValueError("Probability p must be in (0, 1)") | |
| # Initial guess using a rough normal approximation | |
| # For df > 2, the variance is df/(df-2), but 0 is a safe start | |
| x = 0.0 | |
| for _ in range(50): | |
| f_x = t_cdf(x, df) - p | |
| f_prime_x = t_pdf(x, df) | |
| # Avoid division by zero | |
| if f_prime_x == 0: | |
| break | |
| dx = f_x / f_prime_x | |
| x = x - dx | |
| if abs(dx) < tol: | |
| return x | |
| return x |