// Copyright (C) 2026 Kiyotsugu Arai // SPDX-License-Identifier: LGPL-3.0-or-later // hypergeometric_pq.hpp // General pFq hypergeometric function and Meijer G function (partial, stocking phase initial drop) // // The existing hypergeometric.hpp provides ₀F₁, ₁F₁, ₂F₁ individually. This header: // // pFq(a, b, z) — general ₚFq Σ_{k} [Π(a_i)_k / Π(b_j)_k] z^k / k! // meijerG(m, n, a, b, z) — Meijer G function G^{m,n}_{p,q}(z | a; b) (residue representation) // // Convergence conditions for pFq: // p ≤ q : converges for any z (entire function) // p = q + 1 : converges for |z| < 1, conditional at z=1 // p > q + 1 : formal series (asymptotic) — not handled by this implementation // // Current state of Meijer G (stocking phase initial): // - n = 0 (a parameters do not appear in the numerator) and the m ≤ q constraint // - series representation via the residue theorem only when all b_j are simple poles (distinct) // - resonance (integer differences between b_j) is not handled in this drop (returns NaN) // - the general (m, n, p, q) case is continued development in a future release // // API design: // - p, q are passed as std::span (C++20) // - polynomial termination (a_i a non-positive integer) is detected automatically // - non-converging cases (p > q+1, |z| ≥ 1 with p = q+1) return NaN // // References: // - DLMF §16.2 (pFq), §16.17 (Meijer G) // - Bühring 1992 (pFq analytic continuation) — this drop is direct Taylor summation only // - mpmath.hyper, mpmath.meijerg #ifndef SANGI_SPECIAL_HYPERGEOMETRIC_PQ_HPP #define SANGI_SPECIAL_HYPERGEOMETRIC_PQ_HPP #include #include #include #include #include #include #include namespace sangi { namespace special { namespace detail_pq { // Detect polynomial termination: whether any a_i is a non-positive integer template [[nodiscard]] int polynomial_terms(std::span a) { int min_terms = -1; for (T ai : a) { if (ai <= T(0) && ai == std::floor(ai)) { int terms = static_cast(-ai) + 1; if (min_terms < 0 || terms < min_terms) min_terms = terms; } } return min_terms; // -1 = not a polynomial } // NaN when b_j is a non-positive integer (pole) and the corresponding a_i does not terminate earlier template [[nodiscard]] bool b_is_pole(std::span a, std::span b) { // For each b_j, if b_j is a non-positive integer → pole candidate for (T bj : b) { if (bj <= T(0) && bj == std::floor(bj)) { int bj_int = static_cast(bj); // ≤ 0 // if any a_i is a larger (or equal) non-positive integer → the polynomial stops first bool saved = false; for (T ai : a) { if (ai <= T(0) && ai == std::floor(ai)) { int ai_int = static_cast(ai); if (ai_int >= bj_int) { saved = true; break; } } } if (!saved) return true; } } return false; } } // namespace detail_pq // ================================================================ // general ₚFq hypergeometric function // ================================================================ // // pFq(a; b; z) = Σ_{k=0}^∞ [Π(a_i)_k / Π(b_j)_k] · z^k / k! // // Recurrence: // term_{k+1}/term_k = z · Π(a_i + k) / [(k+1) · Π(b_j + k)] // // Arguments: // a: numerator parameters a_1, ..., a_p // b: denominator parameters b_1, ..., b_q // z: argument // // Return value: // p ≤ q : converges for any z // p=q+1 : converges for |z| < 1, |z|=1 is parameter-dependent // p>q+1 : NaN // template [[nodiscard]] T pFq(std::span a, std::span b, T z) { if (std::isnan(z)) return std::numeric_limits::quiet_NaN(); for (T ai : a) if (std::isnan(ai)) return std::numeric_limits::quiet_NaN(); for (T bj : b) if (std::isnan(bj)) return std::numeric_limits::quiet_NaN(); const int p = static_cast(a.size()); const int q = static_cast(b.size()); // divergent case: p > q+1 if (p > q + 1) return std::numeric_limits::quiet_NaN(); // b has a pole and is not rescued by a polynomial if (detail_pq::b_is_pole(a, b)) return std::numeric_limits::quiet_NaN(); if (z == T(0)) return T(1); // detect polynomial termination int poly_terms = detail_pq::polynomial_terms(a); bool is_poly = (poly_terms > 0); // p = q+1 with |z| ≥ 1 and not a polynomial → divergent (conditional convergence not handled here) if (!is_poly && p == q + 1 && std::abs(z) >= T(1)) return std::numeric_limits::quiet_NaN(); int max_iter = is_poly ? poly_terms : 5000; T sum = T(1); T term = T(1); const T eps = std::numeric_limits::epsilon(); for (int k = 0; k < max_iter; ++k) { // Pochhammer factors T num = T(1), den = T(k + 1); bool zero_term = false; for (T ai : a) { T factor = ai + T(k); if (factor == T(0)) { zero_term = true; break; } num *= factor; } if (zero_term) break; for (T bj : b) { T factor = bj + T(k); den *= factor; } term = term * z * num / den; sum += term; if (!is_poly && k >= 5 && std::abs(term) < eps * std::abs(sum)) break; } return sum; } // initializer-list version (convenience) template [[nodiscard]] T pFq(std::initializer_list a, std::initializer_list b, T z) { std::vector av(a), bv(b); return pFq(std::span(av), std::span(bv), z); } // ================================================================ // Float arbitrary-precision ₚFq // ================================================================ // // Same strategy as native. Computes while truncating the Pochhammer factor each time. [[nodiscard]] inline Float pFq(std::span a, std::span b, const Float& z, int precision) { if (z.isNaN()) return Float::nan(); for (const auto& ai : a) if (ai.isNaN()) return Float::nan(); for (const auto& bj : b) if (bj.isNaN()) return Float::nan(); const int p = static_cast(a.size()); const int q = static_cast(b.size()); if (p > q + 1) return Float::nan(); int wp = precision + 30; if (z.isZero()) return Float::one(precision); // detect polynomial termination (a_i a non-positive integer) int poly_terms = -1; for (const auto& ai : a) { if (ai.isInteger() && (ai.isZero() || ai.isNegative())) { // -ai is a non-negative integer int terms = static_cast(-ai.toDouble()) + 1; if (poly_terms < 0 || terms < poly_terms) poly_terms = terms; } } bool is_poly = (poly_terms > 0); Float zw = z; zw.truncateToApprox(wp); // p = q+1 with |z| ≥ 1 and not a polynomial → divergent if (!is_poly && p == q + 1) { Float az = abs(zw); if (az >= Float::one(wp)) return Float::nan(); } int max_iter = is_poly ? poly_terms : (wp * 6 + 500); Float sum = Float::one(wp); Float term = Float::one(wp); int small_count = 0; for (int k = 0; k < max_iter; ++k) { Float num = Float::one(wp); Float den = Float(k + 1); bool zero_term = false; for (const auto& ai : a) { Float factor = ai + Float(k); if (factor.isZero()) { zero_term = true; break; } num = num * factor; num.truncateToApprox(wp); } if (zero_term) break; for (const auto& bj : b) { Float factor = bj + Float(k); den = den * factor; den.truncateToApprox(wp); } term = term * zw * num / den; term.truncateToApprox(wp); sum = sum + term; sum.truncateToApprox(wp); if (!is_poly && k >= 5) { Float at = abs(term); Float as = abs(sum); Float thresh = as * Float::epsilon(wp); if (at < thresh) { if (++small_count >= 3) break; } else { small_count = 0; } } } sum.setPrecision(precision); return sum; } [[nodiscard]] inline Float pFq(std::initializer_list a, std::initializer_list b, const Float& z, int precision) { std::vector av(a), bv(b); return pFq(std::span(av), std::span(bv), z, precision); } // ================================================================ // Meijer G function (residue representation, partial) // ================================================================ // // G^{m,n}_{p,q}(z | a_1...a_p; b_1...b_q) // = (1/2πi) ∫_L Π_{j=1}^m Γ(b_j-s) · Π_{j=1}^n Γ(1-a_j+s) // / [Π_{j=m+1}^q Γ(1-b_j+s) · Π_{j=n+1}^p Γ(a_j-s)] // · z^s ds // // Constraints of the partial implementation provided here: // - n = 0 : the a parameters are not split into upper/lower halves but simply collected // on the "denominator side" (= best tested when a is an empty vector) // - m ≤ q : the poles of Γ(b_j-s) lie in the left half-plane // - all b_j distinct (also distinct mod ℤ) : no resonance // // In this setting G is a sum of residues at the simple poles at b_h: // G(z) = Σ_{h=1}^m Π'_{j=1}^m Γ(b_j-b_h) · Π_{j=1}^n Γ(1-a_j+b_h) // / [Π_{j=m+1}^q Γ(1-b_j+b_h) · Π_{j=n+1}^p Γ(a_j-b_h)] // · z^{b_h} · ₚFq-1(...; z·(-1)^{p-m-n}) // // For simplicity, the n=0 formula (based on DLMF 16.17.2): // G^{m,0}_{p,q}(z | a; b) // = Σ_{h=1}^m [Π_{j≠h, j≤m} Γ(b_j-b_h)] / [Π_{j>m} Γ(1-b_j+b_h) · Π_j Γ(a_j-b_h)] // · z^{b_h} · pFq^* // // The argument of pFq^* is determined by the shift of the b parameters, but this drop // only computes the subfamily with empty a (n=0, p=0) (= covers the Helmholtz-Green // function family). // // ⚠️ More general cases are staged extensions in a future release: // - n > 0 // - resonance handling (logarithmic terms) // - p > 0 with non-empty a (interaction between the b and a parameters) template [[nodiscard]] T meijerG_simple_b(int m, std::span b, T z) { // G^{m,0}_{0,q}(z | -; b) — a empty, n=0 // = Σ_{h=1}^m [Π_{j≠h, j≤m} Γ(b_j - b_h)] / [Π_{j=m+1}^q Γ(1 - b_j + b_h)] // · z^{b_h} · ₀F_{q-1}(; b_*; z·(-1)^{q-m}) // where b_* = the set of shifted parameters obtained from b by removing b_h if (std::isnan(z)) return std::numeric_limits::quiet_NaN(); if (z <= T(0)) return std::numeric_limits::quiet_NaN(); // z^{b_h} could become complex const int q = static_cast(b.size()); if (m < 1 || m > q) return std::numeric_limits::quiet_NaN(); // resonance check: NaN in this implementation if b_j - b_h is an integer (j ≠ h, both ≤ m) for (int i = 0; i < m; ++i) { for (int j = i + 1; j < m; ++j) { T diff = b[i] - b[j]; if (diff == std::floor(diff)) return std::numeric_limits::quiet_NaN(); } } T result = T(0); for (int h = 0; h < m; ++h) { // coefficient: prefactor T prefac = T(1); for (int j = 0; j < m; ++j) { if (j == h) continue; prefac *= std::tgamma(b[j] - b[h]); } for (int j = m; j < q; ++j) { prefac /= std::tgamma(T(1) - b[j] + b[h]); } // pFq^* part (₀F_{q-1}): arguments b_*= 1 + b_h - b_j (j ≠ h) std::vector b_star; b_star.reserve(q - 1); for (int j = 0; j < q; ++j) { if (j == h) continue; b_star.push_back(T(1) + b[h] - b[j]); } // general sign factor: (-1)^{p-m-n}. The partial implementation in this header has p=n=0, so (-1)^m. T sign = (m % 2 == 0) ? T(1) : T(-1); // ₀F_{q-1}(; b_*; sign · z) has p=0 std::vector a_empty; T hyp = pFq(std::span(a_empty), std::span(b_star), sign * z); // z^{b_h} T z_pow = std::pow(z, b[h]); result += prefac * z_pow * hyp; } return result; } // more general stub (currently delegates only to n=0, empty a) template [[nodiscard]] T meijerG(int m, int n, std::span a, std::span b, T z) { if (n != 0 || !a.empty()) { // extension in a future release return std::numeric_limits::quiet_NaN(); } return meijerG_simple_b(m, b, z); } template [[nodiscard]] T meijerG(int m, int n, std::initializer_list a, std::initializer_list b, T z) { std::vector av(a), bv(b); return meijerG(m, n, std::span(av), std::span(bv), z); } } // namespace special } // namespace sangi #endif // SANGI_SPECIAL_HYPERGEOMETRIC_PQ_HPP