// Copyright (C) 2026 Kiyotsugu Arai // SPDX-License-Identifier: LGPL-3.0-or-later // lerch.hpp // Lerch transcendent Φ(z, s, a) = Σ_{k=0}^∞ z^k / (k + a)^s // // Unified representation: // Li_s(z) = z · Φ(z, s, 1) // ζ(s, a) = Φ(1, s, a) (Re(s) > 1) // ζ(s) = Φ(1, s, 1) (Riemann ζ) // η(s) = Φ(-1, s, 1) (Dirichlet η) // // Uses: analytic number theory, unified representation of polylog/Hurwitz, generating functions of statistical distributions. // // Functions provided: // lerchPhi(z, s, a) — native float (float / double / long double) // lerchPhi(z, s, a, precision) — Float arbitrary precision // lerchPhi(z, s, a) — Float (default-precision shorthand) // // Algorithm (stocking phase initial drop): // - z = 0 : a^{-s} // - s = 0 : 1/(1-z) // - z = 1 : Hurwitz ζ(s, a) (finite only for s > 1) // - z = -1 : Hasse-accelerated alternating sum β(s, a) // - |z| < 1 : direct Taylor series (no convergence acceleration) // - |z| ≥ 1, z∉{±1}: NaN (analytic continuation is future work) // // Limitations: // - a must be a positive real. NaN when a is a non-positive integer. // - Complex z and analytic continuation (|z|>1) to be extended in a future release. // // References: // - DLMF §25.14 // - mpmath.lerchphi (high-precision reference) #ifndef SANGI_SPECIAL_LERCH_HPP #define SANGI_SPECIAL_LERCH_HPP #include #include #include #include #include namespace sangi { // Forward declaration: hurwitzZeta is declared in Float.hpp [[nodiscard]] Float hurwitzZeta(const Float& s, const Float& a, int precision); namespace special { namespace detail_lerch { // Direct Taylor: Σ_{k=0}^∞ z^k / (k+a)^s (native) template [[nodiscard]] T direct_series_native(T z, T s, T a) { T sum = std::pow(a, -s); T zk = z; const T eps = std::numeric_limits::epsilon(); // High max_iter to handle |z| close to 1 const T abs_z = std::abs(z); int max_iter = (abs_z > T(0.9)) ? 200000 : 20000; int small_count = 0; for (int k = 1; k < max_iter; ++k) { T term = zk * std::pow(a + T(k), -s); sum += term; if (k >= 10 && std::abs(term) < eps * std::abs(sum)) { if (++small_count >= 3) break; } else { small_count = 0; } zk *= z; } return sum; } // Hasse-accelerated alternating sum: Σ (-1)^k/(k+a)^s // = Σ_{n=0}^N (1/2^{n+1}) Σ_{k=0}^n (-1)^k C(n,k) (k+a)^{-s} template [[nodiscard]] T hasse_alternating_native(T s, T a, int N = 64) { T result = T(0); T half_pow = T(0.5); for (int n = 0; n <= N; ++n) { T inner = T(0); T binom = T(1); T sign = T(1); for (int k = 0; k <= n; ++k) { inner += sign * binom * std::pow(a + T(k), -s); sign = -sign; if (k < n) binom *= T(n - k) / T(k + 1); } result += half_pow * inner; half_pow *= T(0.5); } return result; } } // namespace detail_lerch // ================================================================ // Native floating-point version // ================================================================ template [[nodiscard]] T lerchPhi(T z, T s, T a) { if (std::isnan(z) || std::isnan(s) || std::isnan(a)) return std::numeric_limits::quiet_NaN(); // a non-positive real → pole if (a <= T(0)) return std::numeric_limits::quiet_NaN(); // z = 0 if (z == T(0)) return std::pow(a, -s); // s = 0 if (s == T(0)) { if (std::abs(z) >= T(1)) return std::numeric_limits::quiet_NaN(); return T(1) / (T(1) - z); } // z = 1: Hurwitz ζ if (z == T(1)) { if (s <= T(1)) return std::numeric_limits::quiet_NaN(); return hurwitzZeta(s, a); } // z = -1: Hasse-accelerated alternating sum if (z == T(-1)) { if (s <= T(0)) return std::numeric_limits::quiet_NaN(); return detail_lerch::hasse_alternating_native(s, a); } // |z| > 1: analytic continuation not supported if (std::abs(z) > T(1)) return std::numeric_limits::quiet_NaN(); return detail_lerch::direct_series_native(z, s, a); } // ================================================================ // Float arbitrary-precision version // ================================================================ [[nodiscard]] inline Float lerchPhi(const Float& z, const Float& s, const Float& a, int precision) { if (z.isNaN() || s.isNaN() || a.isNaN()) return Float::nan(); if (a.isZero() || a.isNegative()) return Float::nan(); int wp = precision + 30; Float zw = z; zw.truncateToApprox(wp); Float sw = s; sw.truncateToApprox(wp); Float aw = a; aw.truncateToApprox(wp); Float one = Float::one(wp); Float zero = Float::zero(wp); if (zw.isZero()) { Float r = exp(-sw * log(aw, wp), wp); r.setPrecision(precision); return r; } if (sw.isZero()) { Float r = one / (one - zw); r.setPrecision(precision); return r; } if (zw == one) { return hurwitzZeta(sw, aw, precision); } Float neg_one = -one; if (zw == neg_one) { // Hasse-accelerated alternating sum (error ~2^{-N}, 1 bit/term). BUGFIX (2026-05-30): // N = wp·1.05 misused decimal digits as a bit-term count, capping at ~0.3·P digits // → wp_bits·1.05 (AUDIT_FLOAT_UNIT_MIXING pattern C). int N = static_cast(Float::precisionToBits(wp) * 1.05) + 5; Float result = zero; Float half_pow = ldexp(one, -1); for (int n = 0; n <= N; ++n) { Float inner = zero; Float binom = Float::one(wp); int sgn = 1; for (int k = 0; k <= n; ++k) { Float ka = aw + Float(k); Float term = exp(-sw * log(ka, wp), wp); if (sgn > 0) inner = inner + binom * term; else inner = inner - binom * term; sgn = -sgn; inner.truncateToApprox(wp); if (k < n) { binom = binom * Float(n - k) / Float(k + 1); binom.truncateToApprox(wp); } } result = result + half_pow * inner; result.truncateToApprox(wp); half_pow = ldexp(half_pow, -1); } result.setPrecision(precision); return result; } // |z| > 1: not supported Float abs_z = abs(zw); if (abs_z > one) return Float::nan(); if (abs_z == one) return Float::nan(); // Direct series Float sum = exp(-sw * log(aw, wp), wp); Float zk = zw; int max_iter = wp * 8 + 500; int small_count = 0; for (int k = 1; k < max_iter; ++k) { Float ka = aw + Float(k); Float term = zk * exp(-sw * log(ka, wp), wp); term.truncateToApprox(wp); sum = sum + term; sum.truncateToApprox(wp); Float at = abs(term); Float as = abs(sum); Float thresh = as * Float::epsilon(wp); if (k >= 10 && at < thresh) { if (++small_count >= 3) break; } else { small_count = 0; } zk = zk * zw; zk.truncateToApprox(wp); } sum.setPrecision(precision); return sum; } [[nodiscard]] inline Float lerchPhi(const Float& z, const Float& s, const Float& a) { return lerchPhi(z, s, a, Float::defaultPrecision()); } } // namespace special } // namespace sangi #endif // SANGI_SPECIAL_LERCH_HPP