// Copyright (C) 2026 Kiyotsugu Arai // SPDX-License-Identifier: LGPL-3.0-or-later // gamma.hpp // Template wrappers for the gamma-function family // // Functions provided: // gamma(x) — gamma function Γ(x) // lnGamma(x) — log-gamma function ln Γ(x) // beta(a, b) — beta function B(a,b) = Γ(a)·Γ(b)/Γ(a+b) // digamma(x) — digamma function ψ(x) = Γ'(x)/Γ(x) // trigamma(x) — trigamma function ψ₁(x) // // Supported types: // float, double, long double — delegate to std:: // Float — delegate to the implementation in FloatMath.cpp #ifndef SANGI_SPECIAL_GAMMA_HPP #define SANGI_SPECIAL_GAMMA_HPP #include #include #include #include #include namespace sangi { namespace special { // ================================================================ // gamma function Γ(x) // ================================================================ /// native floating-point types: delegate to std::tgamma template [[nodiscard]] T gamma(T x) { return std::tgamma(x); } // Float type: please use sangi::gamma(x, precision) directly. // ================================================================ // log-gamma function ln Γ(x) // ================================================================ /// native floating-point types: delegate to std::lgamma template [[nodiscard]] T lnGamma(T x) { return std::lgamma(x); } // Float type: please use sangi::lnGamma(x, precision) directly. // ================================================================ // beta function B(a,b) = Γ(a)·Γ(b)/Γ(a+b) // ================================================================ /// native floating-point types: delegate to C++17 std::beta template [[nodiscard]] T beta(T a, T b) { #if defined(__cpp_lib_math_special_functions) || defined(_MSC_VER) return std::beta(a, b); #else return std::exp(std::lgamma(a) + std::lgamma(b) - std::lgamma(a + b)); #endif } // Float type: please use sangi::beta(a, b, precision) directly. // ================================================================ // digamma function ψ(x) = d/dx ln Γ(x) // ================================================================ /// native floating-point types: implemented via asymptotic expansion /// the C++ standard library has no digamma, so this is a custom implementation template [[nodiscard]] T digamma(T x) { // negative argument: reflection formula ψ(1-x) - ψ(x) = π·cot(πx) if (x < T(0)) { if (x == std::floor(x)) { return std::numeric_limits::quiet_NaN(); // pole } const T pi = static_cast(3.14159265358979323846L); return digamma(T(1) - x) - pi / std::tan(pi * x); } // x == 0: pole if (x == T(0)) { return -std::numeric_limits::infinity(); } T result = T(0); // argument shift: ψ(x+1) = ψ(x) + 1/x → raise x up to >= 8 T z = x; while (z < T(8)) { result -= T(1) / z; z += T(1); } // asymptotic expansion: ψ(z) ≈ ln(z) - 1/(2z) - Σ B_{2k}/(2k·z^{2k}) // coefficients are 1/(2k) times the Bernoulli numbers B_{2k} (k=1..7) // reference: Abramowitz & Stegun 6.3.18 T z2 = T(1) / (z * z); // 1/(2k) times the Bernoulli numbers B_2, B_4, ..., B_14 // B_2=1/6, B_4=-1/30, B_6=1/42, B_8=-1/30, B_10=5/66, B_12=-691/2730, B_14=7/6 constexpr T coeffs[] = { static_cast(1.0L / 12.0L), // B_2/2 = (1/6)/2 = 1/12 static_cast(-1.0L / 120.0L), // B_4/4 = (-1/30)/4 = -1/120 static_cast(1.0L / 252.0L), // B_6/6 = (1/42)/6 = 1/252 static_cast(-1.0L / 240.0L), // B_8/8 = (-1/30)/8 = -1/240 static_cast(1.0L / 132.0L), // B_10/10 = (5/66)/10 = 1/132 static_cast(-691.0L / 32760.0L), // B_12/12 = (-691/2730)/12 = -691/32760 static_cast(1.0L / 12.0L), // B_14/14 = (7/6)/14 = 1/12 (same value as B_2/2 but a different coefficient) }; T series = coeffs[6]; for (int i = 5; i >= 0; --i) { series = series * z2 + coeffs[i]; } result += std::log(z) - T(0.5) / z - series * z2; return result; } // Float type: please use sangi::digamma(x, precision) directly. // ================================================================ // trigamma function ψ₁(x) = d²/dx² ln Γ(x) // ================================================================ /// native floating-point types: implemented via asymptotic expansion template [[nodiscard]] T trigamma(T x) { // negative argument: reflection formula ψ₁(1-x) + ψ₁(x) = π²/sin²(πx) if (x < T(0)) { if (x == std::floor(x)) { return std::numeric_limits::quiet_NaN(); // pole } const T pi = static_cast(3.14159265358979323846L); T sinpx = std::sin(pi * x); return (pi * pi) / (sinpx * sinpx) - trigamma(T(1) - x); } // x == 0: pole if (x == T(0)) { return std::numeric_limits::infinity(); } T result = T(0); // argument shift: ψ₁(x) = ψ₁(x+1) + 1/x² T z = x; while (z < T(8)) { result += T(1) / (z * z); z += T(1); } // asymptotic expansion: ψ₁(z) ≈ 1/z + 1/(2z²) + Σ B_{2k}/z^{2k+1} T z_inv = T(1) / z; T z2 = z_inv * z_inv; // B_2, B_4, B_6, B_8, B_10, B_12 constexpr T bernoulli[] = { static_cast(1.0L / 6.0L), static_cast(-1.0L / 30.0L), static_cast(1.0L / 42.0L), static_cast(-1.0L / 30.0L), static_cast(5.0L / 66.0L), static_cast(-691.0L / 2730.0L), }; T series = bernoulli[5]; for (int i = 4; i >= 0; --i) { series = series * z2 + bernoulli[i]; } result += z_inv + T(0.5) * z2 + series * z2 * z_inv; return result; } // Float type: please use sangi::trigamma(x, precision) directly. // ================================================================ // polygamma function ψ^(n)(x) = d^{n+1}/dx^{n+1} ln Γ(x) // ================================================================ /// native floating-point types: implemented via asymptotic expansion /// n=0 → delegate to digamma, n=1 → trigamma. n≥2 uses a generic asymptotic expansion. template [[nodiscard]] T polygamma(int n, T x) { if (n < 0) return std::numeric_limits::quiet_NaN(); if (n == 0) return digamma(x); if (n == 1) return trigamma(x); // special values if (std::isnan(x)) return std::numeric_limits::quiet_NaN(); if (x == T(0)) return std::numeric_limits::quiet_NaN(); if (x < T(0) && x == std::floor(x)) return std::numeric_limits::quiet_NaN(); if (std::isinf(x)) { return x > T(0) ? T(0) : std::numeric_limits::quiet_NaN(); } // compute n! T n_fact = T(1); for (int i = 2; i <= n; i++) n_fact *= static_cast(i); // argument shift: shift until z ≥ 10 // ψ^(n)(z) = ψ^(n)(z+1) + (-1)^{n+1} · n! / z^{n+1} T z = x; T shift_sum = T(0); while (z < T(10)) { T zi_pow = T(1); for (int j = 0; j <= n; j++) zi_pow *= z; shift_sum += T(1) / zi_pow; z += T(1); } // asymptotic expansion: ψ^(n)(z) = (-1)^{n+1} · A // A = (n-1)!/z^n + n!/(2z^{n+1}) // + Σ_{k=1}^{K} B_{2k} · (2k+n-1)!/((2k)!) · z^{-(2k+n)} T n_minus_1_fact = n_fact / static_cast(n); T z_inv = T(1) / z; T z_inv_n = T(1); for (int i = 0; i < n; i++) z_inv_n *= z_inv; T A = n_minus_1_fact * z_inv_n + n_fact * z_inv_n * z_inv / T(2); // B_{2k} (k=1..8): B_2, B_4, ..., B_16 constexpr long double B[] = { 1.0L / 6.0L, // B_2 -1.0L / 30.0L, // B_4 1.0L / 42.0L, // B_6 -1.0L / 30.0L, // B_8 5.0L / 66.0L, // B_10 -691.0L / 2730.0L, // B_12 7.0L / 6.0L, // B_14 -3617.0L / 510.0L, // B_16 }; T z2 = z_inv * z_inv; T z_power = z_inv_n * z2; // z^{-(n+2)} for (int k = 0; k < 8; k++) { int kk = k + 1; // R(n,kk) = (2kk+n-1)!/(2kk)! = Π_{j=1}^{n-1} (2kk+j) T R = T(1); for (int j = 1; j <= n - 1; j++) { R *= static_cast(2 * kk + j); } T term = static_cast(B[k]) * R * z_power; A += term; if (std::abs(term) < std::abs(A) * std::numeric_limits::epsilon()) break; z_power *= z2; } // sign: (-1)^{n+1} T asymp = (n % 2 == 0) ? -A : A; // shift correction: (-1)^{n+1} · n! · shift_sum T shift_correction = n_fact * shift_sum; if (n % 2 == 0) shift_correction = -shift_correction; return asymp + shift_correction; } // Float type: please use sangi::polygamma(n, x, precision) directly. // ================================================================ // regularized incomplete gamma functions — forward declarations (for mutual recursion) // ================================================================ template [[nodiscard]] T gammaP(T a, T x); template [[nodiscard]] T gammaQ(T a, T x); // ================================================================ // regularized lower incomplete gamma P(a,x) = γ(a,x)/Γ(a) // ================================================================ /// native floating-point types /// x < a+1: Taylor series, x ≥ a+1: 1 - Q(a,x) template [[nodiscard]] T gammaP(T a, T x) { if (std::isnan(a) || std::isnan(x)) return std::numeric_limits::quiet_NaN(); if (x < T(0)) return std::numeric_limits::quiet_NaN(); if (a <= T(0)) return std::numeric_limits::quiet_NaN(); if (x == T(0)) return T(0); if (std::isinf(x)) return T(1); // prefactor: exp(-x + a·ln(x) - lgamma(a)) T front = std::exp(-x + a * std::log(x) - std::lgamma(a)); if (x < a + T(1)) { // Taylor series: S = Σ x^n / (a·(a+1)···(a+n)) T term = T(1) / a; T sum = term; for (int n = 1; n < 200; n++) { term *= x / (a + static_cast(n)); sum += term; if (std::abs(term) < std::abs(sum) * std::numeric_limits::epsilon()) break; } return front * sum; } else { return T(1) - gammaQ(a, x); } } // Float type: please use sangi::gammaP(a, x, precision) directly. // ================================================================ // regularized upper incomplete gamma Q(a,x) = Γ(a,x)/Γ(a) = 1 - P(a,x) // ================================================================ /// native floating-point types /// x ≥ a+1: Legendre continued fraction, x < a+1: 1 - P(a,x) template [[nodiscard]] T gammaQ(T a, T x) { if (std::isnan(a) || std::isnan(x)) return std::numeric_limits::quiet_NaN(); if (x < T(0)) return std::numeric_limits::quiet_NaN(); if (a <= T(0)) return std::numeric_limits::quiet_NaN(); if (x == T(0)) return T(1); if (std::isinf(x)) return T(0); T front = std::exp(-x + a * std::log(x) - std::lgamma(a)); if (x >= a + T(1)) { // Legendre CF: Q = front / f // b_0 = x+1-a, a_n = -n(n-a), b_n = x+2n+1-a constexpr T tiny = std::numeric_limits::min(); T b0 = x + T(1) - a; T f = (std::abs(b0) < tiny) ? tiny : b0; T C = f; T D = T(0); for (int n = 1; n < 200; n++) { T an = static_cast(-n) * (static_cast(n) - a); T bn = x + static_cast(2 * n + 1) - a; D = bn + an * D; if (std::abs(D) < tiny) D = tiny; D = T(1) / D; C = bn + an / C; if (std::abs(C) < tiny) C = tiny; T delta = C * D; f *= delta; if (std::abs(delta - T(1)) < std::numeric_limits::epsilon()) break; } return front / f; } else { return T(1) - gammaP(a, x); } } // Float type: please use sangi::gammaQ(a, x, precision) directly. // ================================================================ // lower incomplete gamma γ(a,x) = P(a,x)·Γ(a) // ================================================================ template [[nodiscard]] T gammaLower(T a, T x) { return gammaP(a, x) * std::tgamma(a); } // Float type: please use sangi::gammaLower(a, x, precision) directly. // ================================================================ // upper incomplete gamma Γ(a,x) = Q(a,x)·Γ(a) // ================================================================ template [[nodiscard]] T gammaUpper(T a, T x) { return gammaQ(a, x) * std::tgamma(a); } // Float type: please use sangi::gammaUpper(a, x, precision) directly. // ================================================================ // regularized incomplete beta function I_x(a,b) // ================================================================ /// native floating-point types: continued-fraction expansion + symmetry template [[nodiscard]] T betaRegularized(T x, T a, T b) { if (std::isnan(x) || std::isnan(a) || std::isnan(b)) return std::numeric_limits::quiet_NaN(); if (x < T(0) || x > T(1)) return std::numeric_limits::quiet_NaN(); if (a <= T(0) || b <= T(0)) return std::numeric_limits::quiet_NaN(); if (x == T(0)) return T(0); if (x == T(1)) return T(1); // symmetry: invert if x ≥ (a+1)/(a+b+2) bool flip = x >= (a + T(1)) / (a + b + T(2)); T xx = flip ? (T(1) - x) : x; T aa = flip ? b : a; T bb = flip ? a : b; // front = exp(a·ln(x) + b·ln(1-x) + lgamma(a+b) - lgamma(a) - lgamma(b)) / a T ln_front = aa * std::log(xx) + bb * std::log(T(1) - xx) + std::lgamma(aa + bb) - std::lgamma(aa) - std::lgamma(bb); T front = std::exp(ln_front) / aa; // CF: 1 + d_1/(1 + d_2/(1 + ...)) constexpr T tiny = std::numeric_limits::min(); T f = T(1); T C = T(1); T D = T(0); for (int n = 1; n < 200; n++) { T d; if (n % 2 == 1) { int m = (n - 1) / 2; T am = aa + static_cast(m); T abm = aa + bb + static_cast(m); T denom = (aa + static_cast(2 * m)) * (aa + static_cast(2 * m + 1)); d = -(am * abm * xx) / denom; } else { int m = n / 2; T fm = static_cast(m); T bm = bb - static_cast(m); T denom = (aa + static_cast(2 * m - 1)) * (aa + static_cast(2 * m)); d = (fm * bm * xx) / denom; } D = T(1) + d * D; if (std::abs(D) < tiny) D = tiny; D = T(1) / D; C = T(1) + d / C; if (std::abs(C) < tiny) C = tiny; T delta = C * D; f *= delta; if (std::abs(delta - T(1)) < std::numeric_limits::epsilon()) break; } T result = front / f; return flip ? (T(1) - result) : result; } // Float type: please use sangi::betaRegularized(x, a, b, precision) directly. // ================================================================ // Complex overloads // ================================================================ // ---------------------------------------------------------------- // Γ(z) — Lanczos approximation (g=7, n=9) // ---------------------------------------------------------------- // Re(z) ≥ 0.5: direct Lanczos // Re(z) < 0.5: reflection formula Γ(z) = π / (sin(πz)·Γ(1-z)) template [[nodiscard]] Complex gamma(Complex z) { using C = Complex; // Lanczos coefficients (g=7, n=9) static constexpr R lanczos_g = R(7); static constexpr R lanczos_c[] = { R( 0.99999999999980993), R( 676.5203681218851), R(-1259.1392167224028), R( 771.32342877765313), R(-176.61502916214059), R( 12.507343278686905), R(-0.13857109526572012), R( 9.9843695780195716e-6), R( 1.5056327351493116e-7), }; // reflection formula: Re(z) < 0.5 if (z.re < R(0.5)) { C pi(std::numbers::pi_v); return pi / (sin(pi * z) * gamma(C(R(1)) - z)); } // Lanczos: Γ(z) = √(2π)·t^{z-0.5}·e^{-t}·A(z), t = z-1+g+0.5 C zm1 = z - C(R(1)); C x(lanczos_c[0]); for (int k = 1; k <= 8; k++) { x = x + C(lanczos_c[k]) / (zm1 + C(R(k))); } C t = zm1 + C(lanczos_g + R(0.5)); constexpr R sqrt_2pi = R(2.5066282746310005024157652848110452L); return C(sqrt_2pi) * pow(t, zm1 + C(R(0.5))) * exp(-t) * x; } // ---------------------------------------------------------------- // lnΓ(z) — Lanczos + log // ---------------------------------------------------------------- template [[nodiscard]] Complex lnGamma(Complex z) { using C = Complex; static constexpr R lanczos_g = R(7); static constexpr R lanczos_c[] = { R( 0.99999999999980993), R( 676.5203681218851), R(-1259.1392167224028), R( 771.32342877765313), R(-176.61502916214059), R( 12.507343278686905), R(-0.13857109526572012), R( 9.9843695780195716e-6), R( 1.5056327351493116e-7), }; // reflection: Re(z) < 0.5 → lnΓ(z) = ln(π) - ln(sin(πz)) - lnΓ(1-z) if (z.re < R(0.5)) { C pi(std::numbers::pi_v); return C(std::log(std::numbers::pi_v)) - log(sin(pi * z)) - lnGamma(C(R(1)) - z); } C zm1 = z - C(R(1)); C x(lanczos_c[0]); for (int k = 1; k <= 8; k++) { x = x + C(lanczos_c[k]) / (zm1 + C(R(k))); } C t = zm1 + C(lanczos_g + R(0.5)); constexpr R half_ln_2pi = R(0.91893853320467274178032973640561764L); return C(half_ln_2pi) + (zm1 + C(R(0.5))) * log(t) - t + log(x); } // ---------------------------------------------------------------- // B(a,b) = exp(lnΓ(a) + lnΓ(b) - lnΓ(a+b)) // ---------------------------------------------------------------- template [[nodiscard]] Complex beta(Complex a, Complex b) { return exp(lnGamma(a) + lnGamma(b) - lnGamma(a + b)); } // ---------------------------------------------------------------- // ψ(z) — asymptotic expansion + argument shift // ---------------------------------------------------------------- template [[nodiscard]] Complex digamma(Complex z) { using C = Complex; // reflection formula: Re(z) < 0.5 → ψ(z) = ψ(1-z) - π·cot(πz) if (z.re < R(0.5)) { C pi(std::numbers::pi_v); return digamma(C(R(1)) - z) - pi * (cos(pi * z) / sin(pi * z)); } C result(R(0)); // argument shift: ψ(z+1) = ψ(z) + 1/z → until Re(z) ≥ 8 C w = z; while (w.re < R(8)) { result = result - C(R(1)) / w; w = w + C(R(1)); } // asymptotic expansion: ψ(z) ≈ ln(z) - 1/(2z) - Σ B_{2k}/(2k·z^{2k}) C w2 = C(R(1)) / (w * w); constexpr R coeffs[] = { R( 1.0L / 12.0L), // B_2 / 2 R(-1.0L / 120.0L), // B_4 / 4 R( 1.0L / 252.0L), // B_6 / 6 R(-1.0L / 240.0L), // B_8 / 8 R( 1.0L / 132.0L), // B_10 / 10 R(-691.0L / 32760.0L), // B_12 / 12 R( 1.0L / 12.0L), // B_14 / 14 }; C series(coeffs[6]); for (int i = 5; i >= 0; --i) { series = series * w2 + C(coeffs[i]); } result = result + log(w) - C(R(0.5)) / w - series * w2; return result; } // ---------------------------------------------------------------- // ψ^(n)(z) — polygamma (Complex) n≥0 // ---------------------------------------------------------------- // asymptotic expansion + argument shift. n=0 delegates to digamma. template [[nodiscard]] Complex polygamma(int n, Complex z) { using C = Complex; if (n < 0) return C(std::numeric_limits::quiet_NaN()); if (n == 0) return digamma(z); // compute n! R n_fact = R(1); for (int i = 2; i <= n; i++) n_fact *= static_cast(i); // argument shift: until Re(z) ≥ 10 // ψ^(n)(z) = ψ^(n)(z+1) + (-1)^{n+1} · n! / z^{n+1} C w = z; C shift_sum(R(0)); while (w.re < R(10)) { C wpow = C(R(1)); for (int j = 0; j <= n; j++) wpow = wpow * w; shift_sum = shift_sum + C(R(1)) / wpow; w = w + C(R(1)); } // asymptotic expansion: ψ^(n)(z) = (-1)^{n+1} · A // A = (n-1)!/z^n + n!/(2z^{n+1}) // + Σ_{k=1}^{K} B_{2k} · R(n,k) · z^{-(2k+n)} R n_minus_1_fact = n_fact / static_cast(n); C w_inv = C(R(1)) / w; C w_inv_n = C(R(1)); for (int i = 0; i < n; i++) w_inv_n = w_inv_n * w_inv; C A = C(n_minus_1_fact) * w_inv_n + C(n_fact) * w_inv_n * w_inv / C(R(2)); constexpr long double B[] = { 1.0L / 6.0L, -1.0L / 30.0L, 1.0L / 42.0L, -1.0L / 30.0L, 5.0L / 66.0L, -691.0L / 2730.0L, 7.0L / 6.0L, -3617.0L / 510.0L, }; C w2 = w_inv * w_inv; C w_power = w_inv_n * w2; // z^{-(n+2)} for (int k = 0; k < 8; k++) { int kk = k + 1; R Rval = R(1); for (int j = 1; j <= n - 1; j++) { Rval *= static_cast(2 * kk + j); } C term = C(static_cast(B[k]) * Rval) * w_power; A = A + term; if (sangi::abs(term) < sangi::abs(A) * std::numeric_limits::epsilon()) break; w_power = w_power * w2; } // sign: (-1)^{n+1} C asymp = (n % 2 == 0) ? C(R(0)) - A : A; // shift correction: (-1)^{n+1} · n! · shift_sum C shift_correction = C(n_fact) * shift_sum; if (n % 2 == 0) shift_correction = C(R(0)) - shift_correction; return asymp + shift_correction; } // ================================================================ // Complex-specific overloads (Stirling asymptotic expansion) // ================================================================ // Lanczos (g=7, 9 coefficients, ~15-digit precision) is kept for IsNativeFloat only. // Complex uses an arbitrary-precision Stirling series. namespace detail { // detail::PrecisionGuard was removed from all special-function headers when FLOAT_PRECISION_PLAN Step 12 completed (commit 6fa993f // + Step 12b-j in bulk). From Phase 2 onward, the combination of // the 1-arg ADL overload (via requestedPrecision(x)) + setResultPrecision(wp) // ensures req=wp propagation, so there is no longer any need to temporarily // rewrite Float::defaultPrecision. /// Compute Bernoulli numbers B_{2k} in Float (Akiyama-Tanigawa) /// result[k] = B_{2k} (k = 0, 1, ..., max_k) inline std::vector computeBernoulliFloat(int max_k, int precision) { int n = 2 * max_k + 1; std::vector a(n + 1, Float(0)); std::vector result(max_k + 1, Float(0)); result[0] = Float(1); // Mark the value 1 with a "precision-digit" request. This avoids the defaultPrecision // fallback when dividing by an exact (Float(m+1)), ensuring computation at the requested precision. // (this helper must work independently of PrecisionGuard) Float one_p(1); one_p.setResultPrecision(precision); for (int m = 0; m <= n; m++) { a[m] = one_p / Float(m + 1); a[m].setPrecision(precision); for (int j = m; j >= 1; j--) { a[j - 1] = Float(j) * (a[j - 1] - a[j]); a[j - 1].setPrecision(precision); } if (m >= 2 && m % 2 == 0) result[m / 2] = a[0]; } return result; } } // namespace detail // ---------------------------------------------------------------- // lnΓ(z) — Stirling asymptotic expansion (Complex) // ---------------------------------------------------------------- // lnΓ(z) = (z-1/2)·ln(z) - z + ln(2π)/2 + Σ B_{2k}/(2k·(2k-1)·z^{2k-1}) // Re(z) < 0.5: reflection formula lnΓ(z) = ln(π) - ln(sin(πz)) - lnΓ(1-z) [[nodiscard]] inline Complex lnGamma(Complex z, int precision) { using C = Complex; int wp = precision + 20; Float::PrecisionScope _ps(wp); // compute exact÷exact inside the function at wp digits // PrecisionGuard removed: w.re.setResultPrecision(wp) propagates z's req=wp within the function. // Even in the reflection branch, Float::pi(wp) carries req=wp, and Complex arithmetic keeps >= wp. // computeBernoulliFloat is not called (used only by digamma/polygamma). // reflection formula: Re(z) < 0.5 if (z.re.toDouble() < 0.5) { Float pi_val = Float::pi(wp); C ln_pi(log(pi_val, wp)); C sin_pi_z = sin(C(pi_val) * z); return ln_pi - log(sin_pi_z) - lnGamma(C(Float(1)) - z, precision); } // argument shift: until Re(w) is large enough double shift_target = wp * 0.45 + 10; // BUGFIX 2026-05-30: 0.35 is too small for asymptotic convergence C w = z; w.re.setResultPrecision(wp); // pad the input to working precision (also sets eff) w.im.setResultPrecision(wp); C prod_log(Float(0)); double w_re = w.re.toDouble(); int m = 0; if (w_re < shift_target) { m = static_cast(shift_target - w_re) + 1; for (int i = 0; i < m; i++) { C val = w + C(Float(i)); prod_log = prod_log + log(val); } w = w + C(Float(m)); } // number of Bernoulli terms: optimal truncation k* ≈ π·|w| (same as the real lnGamma/digamma). // BUGFIX (2026-05-30): num_terms = wp/(2·log2|w|) misused a bit-formula assuming a geometric series, // giving too few terms → capped at ~0.3·P digits (AUDIT_FLOAT_UNIT_MIXING pattern B). // the convergence test abs(term) < eps·abs(result) uses relative eps, so it is correct and fires before truncation. double w_mag = std::sqrt(w.re.toDouble() * w.re.toDouble() + w.im.toDouble() * w.im.toDouble()); constexpr double PI_VAL = 3.141592653589793238462643383279502884; int num_terms = static_cast(PI_VAL * w_mag) + 10; if (num_terms < 5) num_terms = 5; int num_terms_cap = static_cast(PI_VAL * shift_target * 1.2 + 100.0); if (num_terms_cap < 2000) num_terms_cap = 2000; if (num_terms > num_terms_cap) num_terms = num_terms_cap; auto bern = detail::computeBernoulliFloat(num_terms, wp); // main computation C ln_w = log(w); Float half = Float(1) / Float(2); // (w - 1/2)·ln(w) - w C result = (w - C(half)) * ln_w - w; // + ln(2π)/2 Float ln2pi = log(Float(2) * Float::pi(wp), wp); result = result + C(ln2pi * half); // Stirling correction: Σ B_{2k} / (2k·(2k-1)·z^{2k-1}) C w_inv = C(Float(1)) / w; C w_inv2 = w_inv * w_inv; C w_power = w_inv; Float eps = Float::epsilon(wp); // BUGFIX (2026-05-30): when num_terms is raised to the optimal point π·|w|, the asymptotic series diverges // past its smallest term, so divergence detection is required (without it, it explodes to ~10^116). Same as the real version. Float prev_abs = Float::positiveInfinity(); for (int k = 1; k <= num_terms; k++) { Float coeff = bern[k] / Float(2 * k * (2 * k - 1)); C term = C(coeff) * w_power; Float at = abs(term); if (k >= 3 && at > prev_abs) break; // divergence detection: passed the smallest term result = result + term; if (k >= 3 && at < eps * abs(result)) break; prev_abs = at; w_power = w_power * w_inv2; } // shift correction result = result - prod_log; result.re.setPrecision(precision); result.im.setPrecision(precision); return result; } [[nodiscard]] inline Complex lnGamma(Complex z) { return lnGamma(std::move(z), Float::defaultPrecision()); } // ---------------------------------------------------------------- // Γ(z) = exp(lnΓ(z)) (Complex) // ---------------------------------------------------------------- [[nodiscard]] inline Complex gamma(Complex z, int precision) { using C = Complex; int wp = precision + 15; Float::PrecisionScope _ps(wp); // compute exact÷exact inside the function at wp digits // PrecisionGuard removed: lnGamma(z, wp) gives a result with req=wp at explicit precision, // and exp(lng) is Complex exp → uses requestedPrecision = wp via the 1-arg ADL. C lng = lnGamma(z, wp); C result = exp(lng); result.re.setPrecision(precision); result.im.setPrecision(precision); return result; } [[nodiscard]] inline Complex gamma(Complex z) { return gamma(std::move(z), Float::defaultPrecision()); } // ---------------------------------------------------------------- // ψ(z) — asymptotic expansion + argument shift (Complex) // ---------------------------------------------------------------- // ψ(z) ≈ ln(z) - 1/(2z) - Σ B_{2k}/(2k·z^{2k}) // Re(z) < 0.5: reflection ψ(z) = ψ(1-z) - π·cot(πz) [[nodiscard]] inline Complex digamma(Complex z, int precision) { using C = Complex; int wp = precision + 20; Float::PrecisionScope _ps(wp); // compute exact÷exact inside the function at wp digits // PrecisionGuard removed: w.re.setResultPrecision(wp) propagates z's req=wp, // and computeBernoulliFloat is also independent of defaultPrecision via its precision argument. // reflection formula if (z.re.toDouble() < 0.5) { Float pi_val = Float::pi(wp); C pi_c(pi_val); C pi_z = pi_c * z; return digamma(C(Float(1)) - z, precision) - pi_c * (cos(pi_z) / sin(pi_z)); } C result(Float(0)); C w = z; w.re.setResultPrecision(wp); // pad the input to working precision (also sets eff) w.im.setResultPrecision(wp); // argument shift: ψ(z+1) = ψ(z) + 1/z double shift_target = wp * 0.45 + 10; // BUGFIX 2026-05-30: 0.35 is too small for asymptotic convergence double w_re = w.re.toDouble(); if (w_re < shift_target) { int m_shift = static_cast(shift_target - w_re) + 1; for (int i = 0; i < m_shift; i++) { C val = w + C(Float(i)); result = result - C(Float(1)) / val; } w = w + C(Float(m_shift)); } // number of Bernoulli terms: optimal truncation k* ≈ π·|w| (same as the real lnGamma/digamma). // BUGFIX (2026-05-30): num_terms = wp/(2·log2|w|) misused a bit-formula assuming a geometric series, // giving too few terms → capped at ~0.3·P digits (AUDIT_FLOAT_UNIT_MIXING pattern B). // the convergence test abs(term) < eps·abs(result) uses relative eps, so it is correct and fires before truncation. double w_mag = std::sqrt(w.re.toDouble() * w.re.toDouble() + w.im.toDouble() * w.im.toDouble()); constexpr double PI_VAL = 3.141592653589793238462643383279502884; int num_terms = static_cast(PI_VAL * w_mag) + 10; if (num_terms < 5) num_terms = 5; int num_terms_cap = static_cast(PI_VAL * shift_target * 1.2 + 100.0); if (num_terms_cap < 2000) num_terms_cap = 2000; if (num_terms > num_terms_cap) num_terms = num_terms_cap; auto bern = detail::computeBernoulliFloat(num_terms, wp); // asymptotic expansion: ψ(w) ≈ ln(w) - 1/(2w) - Σ B_{2k}/(2k·w^{2k}) result = result + log(w) - C(Float(1) / Float(2)) / w; C w_inv2 = C(Float(1)) / (w * w); C w_power = w_inv2; Float eps = Float::epsilon(wp); // BUGFIX (2026-05-30): divergence detection (truncate when the smallest term is passed). Required for num_terms=π·|w|. Float prev_abs = Float::positiveInfinity(); for (int k = 1; k <= num_terms; k++) { Float coeff = bern[k] / Float(2 * k); C term = C(coeff) * w_power; Float at = abs(term); if (k >= 3 && at > prev_abs) break; // divergence detection result = result - term; if (k >= 3 && at < eps * abs(result)) break; prev_abs = at; w_power = w_power * w_inv2; } result.re.setPrecision(precision); result.im.setPrecision(precision); return result; } [[nodiscard]] inline Complex digamma(Complex z) { return digamma(std::move(z), Float::defaultPrecision()); } // ---------------------------------------------------------------- // ψ^(n)(z) — polygamma (Complex) n≥0 // ---------------------------------------------------------------- [[nodiscard]] inline Complex polygamma(int n, Complex z, int precision) { using C = Complex; if (n < 0) return C(Float::nan()); if (n == 0) return digamma(std::move(z), precision); int wp = precision + 20; Float::PrecisionScope _ps(wp); // compute exact÷exact inside the function at wp digits // PrecisionGuard removed: w's setResultPrecision(wp) and computeBernoulliFloat's // precision-independence make manipulating defaultPrecision unnecessary. C w = z; w.re.setResultPrecision(wp); w.im.setResultPrecision(wp); // compute n! Float n_fact(1); for (int i = 2; i <= n; i++) n_fact = n_fact * Float(i); // argument shift: until Re(w) is large enough // ψ^(n)(z) = ψ^(n)(z+1) + (-1)^{n+1} · n! / z^{n+1} double shift_target = wp * 0.45 + 10; // BUGFIX 2026-05-30: 0.35 is too small for asymptotic convergence C shift_sum(Float(0)); double w_re = w.re.toDouble(); if (w_re < shift_target) { int m_shift = static_cast(shift_target - w_re) + 1; for (int i = 0; i < m_shift; i++) { C val = w + C(Float(i)); C vpow = C(Float(1)); for (int j = 0; j <= n; j++) vpow = vpow * val; shift_sum = shift_sum + C(Float(1)) / vpow; } w = w + C(Float(m_shift)); } // number of Bernoulli terms: optimal truncation k* ≈ π·|w| (same as the real lnGamma/digamma). // BUGFIX (2026-05-30): num_terms = wp/(2·log2|w|) misused a bit-formula assuming a geometric series, // giving too few terms → capped at ~0.3·P digits (AUDIT_FLOAT_UNIT_MIXING pattern B). // the convergence test abs(term) < eps·abs(result) uses relative eps, so it is correct and fires before truncation. double w_mag = std::sqrt(w.re.toDouble() * w.re.toDouble() + w.im.toDouble() * w.im.toDouble()); constexpr double PI_VAL = 3.141592653589793238462643383279502884; int num_terms = static_cast(PI_VAL * w_mag) + 10; if (num_terms < 5) num_terms = 5; int num_terms_cap = static_cast(PI_VAL * shift_target * 1.2 + 100.0); if (num_terms_cap < 2000) num_terms_cap = 2000; if (num_terms > num_terms_cap) num_terms = num_terms_cap; auto bern = detail::computeBernoulliFloat(num_terms, wp); // asymptotic expansion: ψ^(n)(z) = (-1)^{n+1} · A // A = (n-1)!/z^n + n!/(2z^{n+1}) // + Σ_{k=1}^{K} B_{2k} · R(n,k) · z^{-(2k+n)} Float n_minus_1_fact = n_fact / Float(n); C w_inv = C(Float(1)) / w; C w_inv_n = C(Float(1)); for (int i = 0; i < n; i++) w_inv_n = w_inv_n * w_inv; C A = C(n_minus_1_fact) * w_inv_n + C(n_fact) * w_inv_n * w_inv / C(Float(2)); C w2 = w_inv * w_inv; C w_power = w_inv_n * w2; // z^{-(n+2)} Float eps = Float::epsilon(wp); // BUGFIX (2026-05-30): divergence detection is required. The polygamma asymptotic series, due to polynomial growth of R(n,k), // diverges sharply past the smallest term; summing up to num_terms=π·|w| exploded to ~10^116. Float prev_abs = Float::positiveInfinity(); for (int k = 1; k <= num_terms; k++) { // R(n,k) = Π_{j=1}^{n-1} (2k+j) Float Rval(1); for (int j = 1; j <= n - 1; j++) { Rval = Rval * Float(2 * k + j); } C term = C(bern[k] * Rval) * w_power; Float at = abs(term); if (k >= 3 && at > prev_abs) break; // divergence detection: passed the smallest term A = A + term; if (k >= 3 && at < eps * abs(A)) break; prev_abs = at; w_power = w_power * w2; } // sign: (-1)^{n+1} C asymp = (n % 2 == 0) ? C(Float(0)) - A : A; // shift correction: (-1)^{n+1} · n! · shift_sum C shift_correction = C(n_fact) * shift_sum; if (n % 2 == 0) shift_correction = C(Float(0)) - shift_correction; C result = asymp + shift_correction; result.re.setPrecision(precision); result.im.setPrecision(precision); return result; } [[nodiscard]] inline Complex polygamma(int n, Complex z) { return polygamma(n, std::move(z), Float::defaultPrecision()); } // ---------------------------------------------------------------- // B(a,b) = exp(lnΓ(a) + lnΓ(b) - lnΓ(a+b)) (Complex) // ---------------------------------------------------------------- [[nodiscard]] inline Complex beta(Complex a, Complex b, int precision) { int wp = precision + 10; // PrecisionGuard removed: lnGamma(a, wp) etc. produce results with req=wp at explicit precision, // and the subsequent Complex exp uses requestedPrecision (= wp) via the 1-arg ADL, // so manipulating defaultPrecision is unnecessary. auto result = exp(lnGamma(a, wp) + lnGamma(b, wp) - lnGamma(a + b, wp)); result.re.setPrecision(precision); result.im.setPrecision(precision); return result; } [[nodiscard]] inline Complex beta(Complex a, Complex b) { return beta(std::move(a), std::move(b), Float::defaultPrecision()); } // ================================================================ // Complex incomplete gamma functions // ================================================================ // P(a,z) = γ(a,z)/Γ(a) — Taylor series (|z| < Re(a)+1) / 1-Q (otherwise) // Q(a,z) = Γ(a,z)/Γ(a) — Legendre CF (|z| ≥ Re(a)+1) / 1-P (otherwise) template [[nodiscard]] Complex gammaP(Complex a, Complex z); template [[nodiscard]] Complex gammaQ(Complex a, Complex z); template [[nodiscard]] Complex gammaP(Complex a, Complex z) { using C = Complex; if (sangi::abs(z) == R(0)) return C(R(0)); // prefactor: exp(-z + a·ln(z) - lnΓ(a)) C front = exp(-z + a * log(z) - lnGamma(a)); if (sangi::abs(z) < a.re + R(1)) { // Taylor series: S = Σ z^n / (a·(a+1)···(a+n)) C term = C(R(1)) / a; C sum = term; for (int n = 1; n < 500; n++) { term = term * z / (a + C(R(n))); sum = sum + term; if (sangi::abs(term) < sangi::abs(sum) * std::numeric_limits::epsilon()) break; } return front * sum; } else { return C(R(1)) - gammaQ(a, z); } } template [[nodiscard]] Complex gammaQ(Complex a, Complex z) { using C = Complex; if (sangi::abs(z) == R(0)) return C(R(1)); C front = exp(-z + a * log(z) - lnGamma(a)); if (sangi::abs(z) >= a.re + R(1)) { // Legendre CF: Q = front / f constexpr R tiny = std::numeric_limits::min(); C b0 = z + C(R(1)) - a; C f = (sangi::abs(b0) < tiny) ? C(tiny) : b0; C big_C = f; C D(R(0)); for (int n = 1; n < 500; n++) { C an = C(R(-n)) * (C(R(n)) - a); C bn = z + C(R(2 * n + 1)) - a; D = bn + an * D; if (sangi::abs(D) < tiny) D = C(tiny); D = C(R(1)) / D; big_C = bn + an / big_C; if (sangi::abs(big_C) < tiny) big_C = C(tiny); C delta = big_C * D; f = f * delta; if (sangi::abs(delta - C(R(1))) < std::numeric_limits::epsilon()) break; } return front / f; } else { return C(R(1)) - gammaP(a, z); } } template [[nodiscard]] Complex gammaLower(Complex a, Complex z) { return gammaP(a, z) * gamma(a); } template [[nodiscard]] Complex gammaUpper(Complex a, Complex z) { return gammaQ(a, z) * gamma(a); } // ================================================================ // Complex regularized incomplete beta I_z(a,b) // ================================================================ template [[nodiscard]] Complex betaRegularized(Complex z, Complex a, Complex b) { using C = Complex; if (sangi::abs(z) == R(0)) return C(R(0)); if (sangi::abs(z - C(R(1))) < std::numeric_limits::epsilon()) return C(R(1)); // symmetry inversion: invert if |z| ≥ (Re(a)+1)/(Re(a)+Re(b)+2) bool flip = sangi::abs(z) >= (a.re + R(1)) / (a.re + b.re + R(2)); C zz = flip ? (C(R(1)) - z) : z; C aa = flip ? b : a; C bb = flip ? a : b; // front = exp(a·ln(z) + b·ln(1-z) + lnΓ(a+b) - lnΓ(a) - lnΓ(b)) / a C ln_front = aa * log(zz) + bb * log(C(R(1)) - zz) + lnGamma(aa + bb) - lnGamma(aa) - lnGamma(bb); C front = exp(ln_front) / aa; // CF: Modified Lentz constexpr R tiny = std::numeric_limits::min(); C f(R(1)), big_C(R(1)), D(R(0)); for (int n = 1; n < 500; n++) { C d; if (n % 2 == 1) { int m = (n - 1) / 2; C am = aa + C(R(m)); C abm = aa + bb + C(R(m)); C denom = (aa + C(R(2 * m))) * (aa + C(R(2 * m + 1))); d = -(am * abm * zz) / denom; } else { int m = n / 2; C c_fm{R(m)}; C bm = bb - C(R(m)); C denom = (aa + C(R(2 * m - 1))) * (aa + C(R(2 * m))); d = (c_fm * bm * zz) / denom; } C one_c{R(1)}; D = one_c + d * D; if (sangi::abs(D) < tiny) D = C(tiny); D = one_c / D; big_C = one_c + d / big_C; if (sangi::abs(big_C) < tiny) big_C = C(tiny); C delta = big_C * D; f = f * delta; if (sangi::abs(delta - one_c) < std::numeric_limits::epsilon()) break; } C result = front / f; return flip ? (C{R(1)} - result) : result; } // ================================================================ // Complex incomplete gamma functions // ================================================================ namespace detail { template Complex gammaPQ_taylor(const Complex& a, const Complex& z, const Complex& front, R eps, int max_iter) { using C = Complex; C term = C(R(1)) / a; C sum = term; for (int n = 1; n < max_iter; n++) { term = term * z / (a + C(R(n))); sum = sum + term; if (n >= 3 && sangi::abs(term) < sangi::abs(sum) * eps) break; } return front * sum; } template Complex gammaPQ_cf(const Complex& a, const Complex& z, const Complex& front, R eps, int max_iter) { using C = Complex; R tiny = eps * eps; C b0 = z + C(R(1)) - a; C f = (sangi::abs(b0) < sangi::abs(C(tiny))) ? C(tiny) : b0; C big_C = f; C D(R(0)); for (int n = 1; n < max_iter; n++) { C an = C(R(-n)) * (C(R(n)) - a); C bn = z + C(R(2 * n + 1)) - a; D = bn + an * D; if (sangi::abs(D) < tiny) D = C(tiny); D = C(R(1)) / D; big_C = bn + an / big_C; if (sangi::abs(big_C) < tiny) big_C = C(tiny); C delta = big_C * D; f = f * delta; if (n >= 3 && sangi::abs(delta - C(R(1))) < eps) break; } return front / f; } } // namespace detail [[nodiscard]] inline Complex gammaP(Complex a, Complex z, int precision) { using C = Complex; int wp = precision + 25; // PrecisionGuard removed: a, z's setResultPrecision(wp) propagates req=wp. a.re.setResultPrecision(wp); a.im.setResultPrecision(wp); z.re.setResultPrecision(wp); z.im.setResultPrecision(wp); if (sangi::abs(z) == Float(0)) { Float zero(0); zero.setPrecision(precision); return C(zero); } // prefactor C front = exp(-z + a * log(z) - lnGamma(a, wp)); Float eps = Float::epsilon(wp); int max_iter = 10 * (wp / 50 + 1); if (max_iter < 500) max_iter = 500; C result; if (sangi::abs(z).toDouble() < a.re.toDouble() + 1.0) { result = detail::gammaPQ_taylor(a, z, front, eps, max_iter); } else { result = C(Float(1)) - detail::gammaPQ_cf(a, z, front, eps, max_iter); } result.re.setPrecision(precision); result.im.setPrecision(precision); return result; } [[nodiscard]] inline Complex gammaQ(Complex a, Complex z, int precision) { using C = Complex; int wp = precision + 25; // PrecisionGuard removed: a, z's setResultPrecision(wp) propagates req=wp. a.re.setResultPrecision(wp); a.im.setResultPrecision(wp); z.re.setResultPrecision(wp); z.im.setResultPrecision(wp); if (sangi::abs(z) == Float(0)) { Float one(1); one.setPrecision(precision); return C(one); } C front = exp(-z + a * log(z) - lnGamma(a, wp)); Float eps = Float::epsilon(wp); int max_iter = 10 * (wp / 50 + 1); if (max_iter < 500) max_iter = 500; C result; if (sangi::abs(z).toDouble() >= a.re.toDouble() + 1.0) { result = detail::gammaPQ_cf(a, z, front, eps, max_iter); } else { result = C(Float(1)) - detail::gammaPQ_taylor(a, z, front, eps, max_iter); } result.re.setPrecision(precision); result.im.setPrecision(precision); return result; } [[nodiscard]] inline Complex gammaLower(Complex a, Complex z, int precision) { using C = Complex; int wp = precision + 15; // PrecisionGuard removed: explicit wp to gammaP / gamma; result has req=wp. C p = gammaP(a, z, wp); C ga = gamma(a, wp); C result = p * ga; result.re.setPrecision(precision); result.im.setPrecision(precision); return result; } [[nodiscard]] inline Complex gammaUpper(Complex a, Complex z, int precision) { using C = Complex; int wp = precision + 15; // PrecisionGuard removed: explicit wp to gammaQ / gamma; result has req=wp. C q = gammaQ(a, z, wp); C ga = gamma(a, wp); C result = q * ga; result.re.setPrecision(precision); result.im.setPrecision(precision); return result; } // ================================================================ // Complex regularized incomplete beta I_z(a,b) // ================================================================ [[nodiscard]] inline Complex betaRegularized(Complex z, Complex a, Complex b, int precision) { using C = Complex; int wp = precision + 25; // PrecisionGuard removed: z, a, b's setResultPrecision(wp) propagates req=wp. z.re.setResultPrecision(wp); z.im.setResultPrecision(wp); a.re.setResultPrecision(wp); a.im.setResultPrecision(wp); b.re.setResultPrecision(wp); b.im.setResultPrecision(wp); if (sangi::abs(z) == Float(0)) { Float zero(0); zero.setPrecision(precision); return C(zero); } Float eps = Float::epsilon(wp); int max_iter = 10 * (wp / 50 + 1); if (max_iter < 500) max_iter = 500; // symmetry inversion Float one(1), zero_f(0); bool flip = sangi::abs(z).toDouble() >= (a.re.toDouble() + 1.0) / (a.re.toDouble() + b.re.toDouble() + 2.0); C zz = flip ? (C(one) - z) : z; C aa = flip ? b : a; C bb = flip ? a : b; // front = exp(a·ln(z) + b·ln(1-z) + lnΓ(a+b) - lnΓ(a) - lnΓ(b)) / a C ln_front = aa * log(zz) + bb * log(C(one) - zz) + lnGamma(aa + bb, wp) - lnGamma(aa, wp) - lnGamma(bb, wp); C front = exp(ln_front) / aa; // CF: Modified Lentz Float tiny = eps * eps; C f{one}, big_C{one}, D{zero_f}; for (int n = 1; n < max_iter; n++) { C d; if (n % 2 == 1) { int m = (n - 1) / 2; Float fm(m), f2m(2 * m), f2m1(2 * m + 1); C am = aa + C(fm); C abm = aa + bb + C(fm); C denom = (aa + C(f2m)) * (aa + C(f2m1)); d = -(am * abm * zz) / denom; } else { int m = n / 2; Float fm(m), fbm(m), f2m(2 * m), f2m_1(2 * m - 1); C c_fm{fm}; C bm = bb - c_fm; C denom = (aa + C(f2m_1)) * (aa + C(f2m)); d = (c_fm * bm * zz) / denom; } D = C(one) + d * D; if (sangi::abs(D) < tiny) D = C(tiny); D = C(one) / D; big_C = C(one) + d / big_C; if (sangi::abs(big_C) < tiny) big_C = C(tiny); C delta = big_C * D; f = f * delta; if (n >= 3 && sangi::abs(delta - C(one)) < eps) break; } C result = front / f; if (flip) result = C(one) - result; result.re.setPrecision(precision); result.im.setPrecision(precision); return result; } } // namespace special } // namespace sangi #endif // SANGI_SPECIAL_GAMMA_HPP