// Copyright (C) 2026 Kiyotsugu Arai // SPDX-License-Identifier: LGPL-3.0-or-later // airy.hpp // Templates for the Airy functions and their derivatives // // Provided functions: // airyAi(x), airyBi(x) — Airy functions of the first/second kind // airyAiPrime(x), airyBiPrime(x) — derivatives of the Airy functions // // Supported types: // float, double, long double — computed directly from the Maclaurin series // Float — delegated to the sangi:: implementation // Complex — Maclaurin series (entire function) // Complex — arbitrary-precision Maclaurin series #ifndef SANGI_SPECIAL_AIRY_HPP #define SANGI_SPECIAL_AIRY_HPP #include #include #include #include #include #include namespace sangi { namespace special { // ================================================================ // Airy constants (double precision) // ================================================================ namespace detail { // Ai(0) = 1 / (3^{2/3} · Γ(2/3)) inline constexpr double kAi0 = 0.35502805388781723926; // Ai'(0) = -1 / (3^{1/3} · Γ(1/3)) inline constexpr double kAip0 = -0.25881940379280679841; // Bi(0) = 1 / (3^{1/6} · Γ(2/3)) inline constexpr double kBi0 = 0.61492662744600073515; // Bi'(0) = 3^{1/6} / Γ(1/3) inline constexpr double kBip0 = 0.44828835735382635789; } // namespace detail // ================================================================ // Shared Maclaurin-series computation // ================================================================ // // Two fundamental solutions of the Airy equation y'' = xy: // f(x) = 1 + x³/(2·3) + x⁶/(2·3·5·6) + ... // g(x) = x + x⁴/(3·4) + x⁷/(3·4·6·7) + ... // // Recurrence: // f_term_{k+1} = f_term_k · x³ / ((3k+2)(3k+3)) // g_term_{k+1} = g_term_k · x³ / ((3k+3)(3k+4)) // // Ai(x) = Ai(0)·f(x) + Ai'(0)·g(x) // Bi(x) = Bi(0)·f(x) + Bi'(0)·g(x) // // Derivatives: // f'(x) = Σ 3(k+1)·f_term_{k+1}/x (k=0,1,2,...) // g'(x) = 1 + Σ (3k+4)·g_term_{k+1}/x (k=0,1,2,...) // // Ai'(x) = Ai(0)·f'(x) + Ai'(0)·g'(x) // Bi'(x) = Bi(0)·f'(x) + Bi'(0)·g'(x) // ================================================================ // Airy Ai(x) // ================================================================ template [[nodiscard]] T airyAi(T x) { if (std::isnan(x)) return std::numeric_limits::quiet_NaN(); if (std::isinf(x)) return T(0); // Ai(±∞) → 0 const T ai0 = static_cast(detail::kAi0); const T aip0 = static_cast(detail::kAip0); if (x == T(0)) return ai0; const T x3 = x * x * x; T f_sum = T(1); T g_sum = x; T f_term = T(1); T g_term = x; for (int k = 0; k < 500; k++) { f_term *= x3 / (T(3*k+2) * T(3*k+3)); g_term *= x3 / (T(3*k+3) * T(3*k+4)); f_sum += f_term; g_sum += g_term; if (k >= 3 && std::abs(f_term) + std::abs(g_term) < std::numeric_limits::epsilon() * (std::abs(f_sum) + std::abs(g_sum))) break; } return ai0 * f_sum + aip0 * g_sum; } // Float type: use sangi::airyAi(x, precision) directly. // ================================================================ // Airy Bi(x) // ================================================================ template [[nodiscard]] T airyBi(T x) { if (std::isnan(x)) return std::numeric_limits::quiet_NaN(); if (std::isinf(x)) { if (x < T(0)) return T(0); // Bi(-∞) → 0 return std::numeric_limits::infinity(); // Bi(+∞) → +∞ } const T bi0 = static_cast(detail::kBi0); const T bip0 = static_cast(detail::kBip0); if (x == T(0)) return bi0; const T x3 = x * x * x; T f_sum = T(1); T g_sum = x; T f_term = T(1); T g_term = x; for (int k = 0; k < 500; k++) { f_term *= x3 / (T(3*k+2) * T(3*k+3)); g_term *= x3 / (T(3*k+3) * T(3*k+4)); f_sum += f_term; g_sum += g_term; if (k >= 3 && std::abs(f_term) + std::abs(g_term) < std::numeric_limits::epsilon() * (std::abs(f_sum) + std::abs(g_sum))) break; } return bi0 * f_sum + bip0 * g_sum; } // Float type: use sangi::airyBi(x, precision) directly. // ================================================================ // Airy Ai'(x) // ================================================================ template [[nodiscard]] T airyAiPrime(T x) { if (std::isnan(x)) return std::numeric_limits::quiet_NaN(); if (std::isinf(x)) return T(0); // Ai'(±∞) → 0 const T ai0 = static_cast(detail::kAi0); const T aip0 = static_cast(detail::kAip0); // Ai'(0) = aip0 (f'(0)=0, g'(0)=1) if (x == T(0)) return aip0; const T x3 = x * x * x; const T inv_x = T(1) / x; T f_term = T(1); T g_term = x; T fp_sum = T(0); // f'(0) = 0 T gp_sum = T(1); // g'(0) = 1 for (int k = 0; k < 500; k++) { f_term *= x3 / (T(3*k+2) * T(3*k+3)); g_term *= x3 / (T(3*k+3) * T(3*k+4)); T fp_contrib = T(3*(k+1)) * f_term * inv_x; T gp_contrib = T(3*k+4) * g_term * inv_x; fp_sum += fp_contrib; gp_sum += gp_contrib; if (k >= 3 && std::abs(fp_contrib) + std::abs(gp_contrib) < std::numeric_limits::epsilon() * (std::abs(fp_sum) + std::abs(gp_sum))) break; } return ai0 * fp_sum + aip0 * gp_sum; } // Float type: use sangi::airyAiPrime(x, precision) directly. // ================================================================ // Airy Bi'(x) // ================================================================ template [[nodiscard]] T airyBiPrime(T x) { if (std::isnan(x)) return std::numeric_limits::quiet_NaN(); if (std::isinf(x)) { if (x < T(0)) return T(0); // Bi'(-∞) → 0 return std::numeric_limits::infinity(); // Bi'(+∞) → +∞ } const T bi0 = static_cast(detail::kBi0); const T bip0 = static_cast(detail::kBip0); if (x == T(0)) return bip0; const T x3 = x * x * x; const T inv_x = T(1) / x; T f_term = T(1); T g_term = x; T fp_sum = T(0); T gp_sum = T(1); for (int k = 0; k < 500; k++) { f_term *= x3 / (T(3*k+2) * T(3*k+3)); g_term *= x3 / (T(3*k+3) * T(3*k+4)); T fp_contrib = T(3*(k+1)) * f_term * inv_x; T gp_contrib = T(3*k+4) * g_term * inv_x; fp_sum += fp_contrib; gp_sum += gp_contrib; if (k >= 3 && std::abs(fp_contrib) + std::abs(gp_contrib) < std::numeric_limits::epsilon() * (std::abs(fp_sum) + std::abs(gp_sum))) break; } return bi0 * fp_sum + bip0 * gp_sum; } // Float type: use sangi::airyBiPrime(x, precision) directly. // ================================================================ // Complex Airy — shared Maclaurin-series template // ================================================================ // // f(z), g(z) are entire functions, so the same recurrence as the real case can be used as is. // The derivatives are likewise computed in the complex domain as f'(z), g'(z). namespace detail { /// Shared computation of Ai(z), Bi(z): returns f_sum, g_sum template void airy_fg_complex(const Complex& z, R eps, int max_iter, Complex& f_sum, Complex& g_sum) { using C = Complex; C z3 = z * z * z; f_sum = C(R(1)); g_sum = z; C f_term(R(1)); C g_term = z; for (int k = 0; k < max_iter; k++) { f_term = f_term * z3 / R(static_cast(3*k+2) * (3*k+3)); g_term = g_term * z3 / R(static_cast(3*k+3) * (3*k+4)); f_sum = f_sum + f_term; g_sum = g_sum + g_term; if (k >= 3 && sangi::abs(f_term) + sangi::abs(g_term) < eps * (sangi::abs(f_sum) + sangi::abs(g_sum))) break; } } /// Shared computation of Ai'(z), Bi'(z): returns fp_sum, gp_sum template void airy_fg_prime_complex(const Complex& z, R eps, int max_iter, Complex& fp_sum, Complex& gp_sum) { using C = Complex; C z3 = z * z * z; C inv_z = C(R(1)) / z; C f_term(R(1)); C g_term = z; fp_sum = C(R(0)); gp_sum = C(R(1)); for (int k = 0; k < max_iter; k++) { f_term = f_term * z3 / R(static_cast(3*k+2) * (3*k+3)); g_term = g_term * z3 / R(static_cast(3*k+3) * (3*k+4)); C fp_contrib = C(R(3*(k+1))) * f_term * inv_z; C gp_contrib = C(R(3*k+4)) * g_term * inv_z; fp_sum = fp_sum + fp_contrib; gp_sum = gp_sum + gp_contrib; if (k >= 3 && sangi::abs(fp_contrib) + sangi::abs(gp_contrib) < eps * (sangi::abs(fp_sum) + sangi::abs(gp_sum))) break; } } } // namespace detail // ================================================================ // Complex overloads // ================================================================ [[nodiscard]] inline Complex airyAi(const Complex& z) { using C = Complex; C f_sum, g_sum; detail::airy_fg_complex(z, std::numeric_limits::epsilon(), 500, f_sum, g_sum); return C(detail::kAi0) * f_sum + C(detail::kAip0) * g_sum; } [[nodiscard]] inline Complex airyBi(const Complex& z) { using C = Complex; C f_sum, g_sum; detail::airy_fg_complex(z, std::numeric_limits::epsilon(), 500, f_sum, g_sum); return C(detail::kBi0) * f_sum + C(detail::kBip0) * g_sum; } [[nodiscard]] inline Complex airyAiPrime(const Complex& z) { using C = Complex; if (sangi::abs(z) == 0.0) return C(detail::kAip0); C fp_sum, gp_sum; detail::airy_fg_prime_complex(z, std::numeric_limits::epsilon(), 500, fp_sum, gp_sum); return C(detail::kAi0) * fp_sum + C(detail::kAip0) * gp_sum; } [[nodiscard]] inline Complex airyBiPrime(const Complex& z) { using C = Complex; if (sangi::abs(z) == 0.0) return C(detail::kBip0); C fp_sum, gp_sum; detail::airy_fg_prime_complex(z, std::numeric_limits::epsilon(), 500, fp_sum, gp_sum); return C(detail::kBi0) * fp_sum + C(detail::kBip0) * gp_sum; } // ================================================================ // Complex overloads // ================================================================ [[nodiscard]] inline Complex airyAi(const Complex& z, int precision) { using C = Complex; int wp = precision + 30; Float::PrecisionScope _ps(wp); // compute the in-function exact÷exact / pow(exact) at wp digits C zw = z; zw.re.setResultPrecision(wp); zw.im.setResultPrecision(wp); // Ai(0) = 1 / (3^{2/3} · Γ(2/3)), Ai'(0) = -1 / (3^{1/3} · Γ(1/3)) Float three(3); // no setResultPrecision needed: PrecisionScope promotes Float(1)/three to wp Float cbrt3 = sangi::pow(three, Float(1) / three); // 3^{1/3} Float cbrt3_sq = cbrt3 * cbrt3; // 3^{2/3} Float g23 = sangi::gamma(Float(2) / three, wp); // Γ(2/3) Float g13 = sangi::gamma(Float(1) / three, wp); // Γ(1/3) Float ai0 = Float(1) / (cbrt3_sq * g23); Float aip0 = -Float(1) / (cbrt3 * g13); C f_sum, g_sum; detail::airy_fg_complex(zw, Float::epsilon(wp), 10 * wp, f_sum, g_sum); C result = C(ai0) * f_sum + C(aip0) * g_sum; result.re.setPrecision(precision); result.im.setPrecision(precision); return result; } [[nodiscard]] inline Complex airyBi(const Complex& z, int precision) { using C = Complex; int wp = precision + 30; Float::PrecisionScope _ps(wp); // compute the in-function exact÷exact / pow(exact) at wp digits C zw = z; zw.re.setResultPrecision(wp); zw.im.setResultPrecision(wp); Float three(3); // no setResultPrecision needed: PrecisionScope promotes Float(1)/three and Float(1)/Float(6) to wp Float sixth = sangi::pow(three, Float(1) / Float(6)); // 3^{1/6} (PrecisionScope prevents the precision from being capped) Float g23 = sangi::gamma(Float(2) / three, wp); Float g13 = sangi::gamma(Float(1) / three, wp); Float bi0 = Float(1) / (sixth * g23); Float bip0 = sixth / g13; C f_sum, g_sum; detail::airy_fg_complex(zw, Float::epsilon(wp), 10 * wp, f_sum, g_sum); C result = C(bi0) * f_sum + C(bip0) * g_sum; result.re.setPrecision(precision); result.im.setPrecision(precision); return result; } [[nodiscard]] inline Complex airyAiPrime(const Complex& z, int precision) { using C = Complex; int wp = precision + 30; Float::PrecisionScope _ps(wp); // compute the in-function exact÷exact / pow(exact) at wp digits C zw = z; zw.re.setResultPrecision(wp); zw.im.setResultPrecision(wp); Float three(3); // no setResultPrecision needed: PrecisionScope promotes Float(1)/three to wp Float cbrt3 = sangi::pow(three, Float(1) / three); Float cbrt3_sq = cbrt3 * cbrt3; Float g23 = sangi::gamma(Float(2) / three, wp); Float g13 = sangi::gamma(Float(1) / three, wp); Float ai0 = Float(1) / (cbrt3_sq * g23); Float aip0 = -Float(1) / (cbrt3 * g13); if (sangi::abs(zw).isZero()) { C result(aip0); result.re.setPrecision(precision); result.im.setPrecision(precision); return result; } C fp_sum, gp_sum; detail::airy_fg_prime_complex(zw, Float::epsilon(wp), 10 * wp, fp_sum, gp_sum); C result = C(ai0) * fp_sum + C(aip0) * gp_sum; result.re.setPrecision(precision); result.im.setPrecision(precision); return result; } [[nodiscard]] inline Complex airyBiPrime(const Complex& z, int precision) { using C = Complex; int wp = precision + 30; Float::PrecisionScope _ps(wp); // compute the in-function exact÷exact / pow(exact) at wp digits C zw = z; zw.re.setResultPrecision(wp); zw.im.setResultPrecision(wp); Float three(3); // no setResultPrecision needed: PrecisionScope promotes Float(1)/three and Float(1)/Float(6) to wp Float sixth = sangi::pow(three, Float(1) / Float(6)); // 3^{1/6} (PrecisionScope prevents the precision from being capped) Float g23 = sangi::gamma(Float(2) / three, wp); Float g13 = sangi::gamma(Float(1) / three, wp); Float bi0 = Float(1) / (sixth * g23); Float bip0 = sixth / g13; if (sangi::abs(zw).isZero()) { C result(bip0); result.re.setPrecision(precision); result.im.setPrecision(precision); return result; } C fp_sum, gp_sum; detail::airy_fg_prime_complex(zw, Float::epsilon(wp), 10 * wp, fp_sum, gp_sum); C result = C(bi0) * fp_sum + C(bip0) * gp_sum; result.re.setPrecision(precision); result.im.setPrecision(precision); return result; } } // namespace special } // namespace sangi #endif // SANGI_SPECIAL_AIRY_HPP