// Copyright (C) 2026 Kiyotsugu Arai // SPDX-License-Identifier: LGPL-3.0-or-later // owens_t.hpp // Owen's T function template // // Functions provided: // owensT(h, a) — T(h, a) = (1/(2π)) ∫₀ᵃ exp(-h²(1+t²)/2)/(1+t²) dt // // Owen's T function is used to compute orthant probabilities of the bivariate normal distribution. // It is also the basis for CDF computation of the noncentral t and skew-normal distributions. // // Supported types: // float, double, long double — 10-point Gauss-Legendre quadrature // Float — Cauchy-product series (arbitrary precision) // // References: // Owen, D.B. (1956) "Tables for computing bivariate normal probabilities" // Patefield & Tandy (2000) "Fast and accurate calculation of Owen's T function" // No DLMF entry (a statistics-specific function) #ifndef SANGI_SPECIAL_OWENS_T_HPP #define SANGI_SPECIAL_OWENS_T_HPP #include #include #include #include #include #include namespace sangi { namespace special { // ================================================================ // Owen's T function (double) // ================================================================ // T(h, a) = (1/(2π)) ∫₀ᵃ exp(-h²(1+t²)/2)/(1+t²) dt // // double: compute ∫₀ᵃ exp(-h²(1+t²)/2)/(1+t²) dt directly via Gauss-Legendre quadrature // Float: expand the Cauchy product Σ d_n t^{2n} of exp(-h²t²/2)/(1+t²) as a series // d_n = a_n − d_{n−1}, a_n = (−h²/2)^n/n! // Transformation for a > 1 (Owen's relation): // T(h, a) = (Φ(h)+Φ(ah))/2 − Φ(h)·Φ(ah) − T(ah, 1/a) // // Implementation: // double — 10-point Gauss-Legendre quadrature (high accuracy for all a) // Float — Cauchy-product series + the a > 1 transformation namespace detail { /// Owen's T — 10-point Gauss-Legendre quadrature (for native float) /// Compute ∫₀ᵃ exp(-h²(1+t²)/2)/(1+t²) dt by mapping to [-1,1] template [[nodiscard]] T owensT_gl(T h, T a) { // 10-point GL nodes/weights on [-1, 1] static constexpr double gl_x[] = { -0.97390652851717172, -0.86506336668898451, -0.67940956829902441, -0.43339539412924719, -0.14887433898163121, 0.14887433898163121, 0.43339539412924719, 0.67940956829902441, 0.86506336668898451, 0.97390652851717172 }; static constexpr double gl_w[] = { 0.06667134430868814, 0.14945134915058059, 0.21908636251598204, 0.26926671930999636, 0.29552422471475287, 0.29552422471475287, 0.26926671930999636, 0.21908636251598204, 0.14945134915058059, 0.06667134430868814 }; // Change of variable: t = a(1+x)/2, dt = a/2 dx, x ∈ [-1,1] T half_a = a / T(2); T h2 = h * h; T sum = T(0); for (int i = 0; i < 10; i++) { T t = half_a * (T(1) + T(gl_x[i])); T one_plus_t2 = T(1) + t * t; sum += T(gl_w[i]) * std::exp(-h2 * one_plus_t2 / T(2)) / one_plus_t2; } return half_a * sum / (T(2) * std::numbers::pi_v); } /// Owen's T series expansion — for Float (the Cauchy product correctly accounts for 1/(1+t²)) /// f(t) = exp(-h²t²/2)/(1+t²) = Σ d_n t^{2n}, d_n = a_n − d_{n−1} /// (a_n = (−h²/2)^n / n! is the Taylor coefficient of exp(−h²t²/2)) /// T(h,a) = exp(−h²/2)/(2π) · Σ d_n a^{2n+1}/(2n+1) template [[nodiscard]] R owensT_series(R h, R a, R eps, int max_iter, int wp) { R h_sq_half = h * h / R(2); R exp_val = sangi::exp(-h_sq_half, wp); R a_sq = a * a; R a_pow = a; // a^{2n+1} R neg_h_sq_half = -h_sq_half; R a_n = R(1); // (-h²/2)^n / n! R d_n = R(1); // Cauchy-product coefficient, d_0 = a_0 = 1 R sum = d_n * a; // n=0 term for (int n = 1; n < max_iter; n++) { a_n = a_n * neg_h_sq_half / R(n); // a_n = (-h²/2)^n / n! d_n = a_n - d_n; // d_n = a_n − d_{n−1} a_pow = a_pow * a_sq; R term = d_n * a_pow / R(2 * n + 1); sum = sum + term; if (n >= 5) { R abs_term = sangi::abs(term); R abs_sum = sangi::abs(sum); if (abs_term < eps * abs_sum) break; } } R pi_val = Float::pi(wp); // ★ π at wp precision (was: capped at ~56 digits via defaultPrecision) return exp_val * sum / (R(2) * pi_val); } } // namespace detail /// Owen's T function T(h, a) template [[nodiscard]] T owensT(T h, T a) { if (std::isnan(h) || std::isnan(a)) return std::numeric_limits::quiet_NaN(); // Special cases if (a == T(0)) return T(0); if (h == T(0)) return std::atan(a) / (T(2) * std::numbers::pi_v); // T(h, a) is even in h: T(-h, a) = T(h, a) T abs_h = std::abs(h); // T(h, -a) = -T(h, a) T sign_a = (a < T(0)) ? T(-1) : T(1); T abs_a = std::abs(a); T result; if (abs_a <= T(1)) { // 10-point GL quadrature result = detail::owensT_gl(abs_h, abs_a); } else { // Owen's transformation (a > 1): // T(h, a) = 1/2 · [Φ(h) + Φ(ah)] - Φ(h)·Φ(ah) - T(ah, 1/a) T inv_a = T(1) / abs_a; T ah = abs_h * abs_a; T sqrt2 = std::numbers::sqrt2_v; T phi_h = std::erfc(-abs_h / sqrt2) / T(2); T phi_ah = std::erfc(-ah / sqrt2) / T(2); T t_ah_inv_a = detail::owensT_gl(ah, inv_a); result = (phi_h + phi_ah) / T(2) - phi_h * phi_ah - t_ah_inv_a; } return sign_a * result; } /// Owen's T function — Float (arbitrary precision) [[nodiscard]] inline Float owensT(const Float& h, const Float& a, int precision) { int wp = precision + 20; // PrecisionGuard removed: setResultPrecision(wp) on hw/aw propagates req=wp // (FLOAT_PRECISION_PLAN Step 12 follow-up). Float hw = h; hw.setResultPrecision(wp); Float aw = a; aw.setResultPrecision(wp); // Special cases if (aw.isZero()) { Float z(0); z.setPrecision(precision); return z; } if (hw.isZero()) { Float pi_val = Float::pi(wp); Float result = sangi::atan(aw) / mulScalarF(pi_val, uint64_t(2)); result.setPrecision(precision); return result; } Float abs_h = sangi::abs(hw); Float sign_a = aw.isNegative() ? Float(-1) : Float(1); Float abs_a = sangi::abs(aw); Float result; if (abs_a <= Float(1)) { result = detail::owensT_series(abs_h, abs_a, Float::epsilon(wp), 10 * wp, wp); } else { Float inv_a = Float(1) / abs_a; Float ah = abs_h * abs_a; // Φ(x) = (1 + erf(x/√2)) / 2 Float sqrt2 = sangi::sqrt(Float(2), wp); // ★ wp precision (was: capped at ~56 digits via 1-arg sqrt) Float erf_h = sangi::erf(abs_h / sqrt2, wp); Float erf_ah = sangi::erf(ah / sqrt2, wp); Float phi_h = ldexp(Float(1) + erf_h, -1); Float phi_ah = ldexp(Float(1) + erf_ah, -1); Float t_ah_inv_a = detail::owensT_series(ah, inv_a, Float::epsilon(wp), 10 * wp, wp); result = ldexp(phi_h + phi_ah, -1) - phi_h * phi_ah - t_ah_inv_a; } result = sign_a * result; result.setPrecision(precision); return result; } [[nodiscard]] inline Float owensT(const Float& h, const Float& a) { return owensT(h, a, Float::defaultPrecision()); } } // namespace special } // namespace sangi #endif // SANGI_SPECIAL_OWENS_T_HPP