// Copyright (C) 2026 Kiyotsugu Arai // SPDX-License-Identifier: LGPL-3.0-or-later // legendre.hpp // Legendre polynomial / associated Legendre function / spherical harmonic / Jacobi polynomial templates // // Functions provided: // legendreP(n, x) — Legendre polynomial P_n(x) // assocLegendreP(n, m, x) — associated Legendre function P_n^m(x) (without Condon-Shortley) // sphLegendre(n, m, theta) — normalized spherical harmonic Y_n^m(θ) // jacobiP(n, alpha, beta, x) — Jacobi polynomial P_n^{(α,β)}(x) // // The Jacobi polynomial generalizes Legendre, Chebyshev, and Gegenbauer: // P_n^{(0,0)}(x) = P_n(x) (Legendre) // P_n^{(-1/2,-1/2)}(x) ∝ T_n(x) (Chebyshev, first kind) // P_n^{(1/2,1/2)}(x) ∝ U_n(x) (Chebyshev, second kind) // P_n^{(α,α)}(x) ∝ C_n^{α+1/2}(x) (Gegenbauer/ultraspherical) // // Supported types: // float, double, long double — Bonnet recurrence / three-term recurrence // Float — delegated to the sangi:: implementation (except sphLegendre) // Complex — Bonnet recurrence (complex argument) // Complex — arbitrary-precision Bonnet recurrence #ifndef SANGI_SPECIAL_LEGENDRE_HPP #define SANGI_SPECIAL_LEGENDRE_HPP #include #include #include #include #include #include #include namespace sangi { namespace special { // ================================================================ // Legendre polynomial P_n(x) // ================================================================ // Bonnet recurrence: (n+1)P_{n+1} = (2n+1)·x·P_n - n·P_{n-1} // P_0 = 1, P_1 = x template [[nodiscard]] T legendreP(int n, T x) { if (std::isnan(x)) return std::numeric_limits::quiet_NaN(); if (n < 0) return std::numeric_limits::quiet_NaN(); if (n == 0) return T(1); if (n == 1) return x; T p_prev = T(1); // P_0 T p_curr = x; // P_1 for (int k = 1; k < n; k++) { T p_next = (T(2 * k + 1) * x * p_curr - T(k) * p_prev) / T(k + 1); p_prev = p_curr; p_curr = p_next; } return p_curr; } // ================================================================ // Associated Legendre function P_n^m(x) // ================================================================ // Without the Condon-Shortley phase factor (matches C++17 std::assoc_legendre) // // 1. P_m^m = (2m-1)!! · (1-x²)^{m/2} // 2. P_{m+1}^m = x·(2m+1)·P_m^m // 3. (n-m+1)P_{n+1}^m = (2n+1)·x·P_n^m - (n+m)·P_{n-1}^m template [[nodiscard]] T assocLegendreP(int n, int m, T x) { if (std::isnan(x)) return std::numeric_limits::quiet_NaN(); if (n < 0 || m < 0) return std::numeric_limits::quiet_NaN(); if (m > n) return T(0); // m = 0 → P_n(x) if (m == 0) return legendreP(n, x); // P_m^m = (2m-1)!! · (1-x²)^{m/2} T sin2 = T(1) - x * x; if (sin2 < T(0)) sin2 = T(0); // guard against numerical error T sin_factor = std::sqrt(sin2); T pmm = T(1); for (int i = 1; i <= m; i++) { pmm *= T(2 * i - 1) * sin_factor; } if (n == m) return pmm; // P_{m+1}^m = x·(2m+1)·P_m^m T pm1m = x * T(2 * m + 1) * pmm; if (n == m + 1) return pm1m; // Recurrence: (k-m+1)P_{k+1}^m = (2k+1)·x·P_k^m - (k+m)·P_{k-1}^m T p_prev = pmm; T p_curr = pm1m; for (int k = m + 1; k < n; k++) { T p_next = (T(2 * k + 1) * x * p_curr - T(k + m) * p_prev) / T(k - m + 1); p_prev = p_curr; p_curr = p_next; } return p_curr; } // ================================================================ // Normalized spherical harmonic Y_n^m(θ) (real part) // ================================================================ // Y_n^m(θ) = √((2n+1)/(4π) · (n-m)!/(n+m)!) · P_n^m(cos θ) // Only m ≥ 0 supported (real part only) template [[nodiscard]] T sphLegendre(int n, int m, T theta) { if (std::isnan(theta)) return std::numeric_limits::quiet_NaN(); if (n < 0 || m < 0 || m > n) return std::numeric_limits::quiet_NaN(); // Normalization coefficient: √((2n+1)/(4π) · (n-m)!/(n+m)!) // (n-m)!/(n+m)! = 1/((n-m+1)(n-m+2)···(n+m)) computed in log space T log_coeff = T(0); for (int i = n - m + 1; i <= n + m; i++) { log_coeff -= std::log(T(i)); } log_coeff += std::log(T(2 * n + 1) / (T(4) * std::numbers::pi_v)); T coeff = std::exp(T(0.5) * log_coeff); T cos_theta = std::cos(theta); T plm = assocLegendreP(n, m, cos_theta); return coeff * plm; } // Float type: use sangi::legendreP, sangi::assocLegendreP directly. // ================================================================ // Complex Legendre polynomial P_n(z) // ================================================================ // The Bonnet recurrence holds as-is for complex arguments. [[nodiscard]] inline Complex legendreP(int n, const Complex& z) { using C = Complex; if (n < 0) return C(std::numeric_limits::quiet_NaN()); if (n == 0) return C(1.0); if (n == 1) return z; C p_prev(1.0); C p_curr = z; for (int k = 1; k < n; k++) { C p_next = (C(2.0 * k + 1.0) * z * p_curr - C(double(k)) * p_prev) / C(double(k + 1)); p_prev = p_curr; p_curr = p_next; } return p_curr; } // ================================================================ // Complex associated Legendre function P_n^m(z) // ================================================================ // In the complex plane (1-z²) may become negative, so the real-valued // version's sin2 < 0 guard is unnecessary. sqrt(1-z²) uses Complex sqrt. [[nodiscard]] inline Complex assocLegendreP(int n, int m, const Complex& z) { using C = Complex; if (n < 0 || m < 0) return C(std::numeric_limits::quiet_NaN()); if (m > n) return C(0.0); if (m == 0) return legendreP(n, z); // sqrt(1 - z²) C sin_factor = sangi::sqrt(C(1.0) - z * z); // P_m^m = (2m-1)!! · (1-z²)^{m/2} C pmm(1.0); for (int i = 1; i <= m; i++) { pmm = pmm * C(2.0 * i - 1.0) * sin_factor; } if (n == m) return pmm; // P_{m+1}^m = z·(2m+1)·P_m^m C pm1m = z * C(2.0 * m + 1.0) * pmm; if (n == m + 1) return pm1m; // Recurrence C p_prev = pmm; C p_curr = pm1m; for (int k = m + 1; k < n; k++) { C p_next = (C(2.0 * k + 1.0) * z * p_curr - C(double(k + m)) * p_prev) / C(double(k - m + 1)); p_prev = p_curr; p_curr = p_next; } return p_curr; } // ================================================================ // Complex Legendre polynomial P_n(z) // ================================================================ [[nodiscard]] inline Complex legendreP(int n, const Complex& z, int precision) { using C = Complex; if (n < 0) return C(Float::nan()); if (n == 0) { Float one(1); one.setPrecision(precision); return C(one); } if (n == 1) { C result = z; result.re.setPrecision(precision); result.im.setPrecision(precision); return result; } int wp = precision + 15; // PrecisionGuard removed: zw's setResultPrecision(wp) propagates req=wp. C zw = z; zw.re.setResultPrecision(wp); zw.im.setResultPrecision(wp); C p_prev(Float(1)); C p_curr = zw; for (int k = 1; k < n; k++) { C p_next = (C(Float(2 * k + 1)) * zw * p_curr - C(Float(k)) * p_prev) / C(Float(k + 1)); p_prev = p_curr; p_curr = p_next; } p_curr.re.setPrecision(precision); p_curr.im.setPrecision(precision); return p_curr; } // ================================================================ // Complex associated Legendre function P_n^m(z) // ================================================================ [[nodiscard]] inline Complex assocLegendreP(int n, int m, const Complex& z, int precision) { using C = Complex; if (n < 0 || m < 0) return C(Float::nan()); if (m > n) { Float zero(0); zero.setPrecision(precision); return C(zero); } if (m == 0) return legendreP(n, z, precision); int wp = precision + 20; // PrecisionGuard removed: zw's setResultPrecision(wp) propagates req=wp. C zw = z; zw.re.setResultPrecision(wp); zw.im.setResultPrecision(wp); // sqrt(1 - z²) C sin_factor = sangi::sqrt(C(Float(1)) - zw * zw); // P_m^m = (2m-1)!! · (1-z²)^{m/2} C pmm(Float(1)); for (int i = 1; i <= m; i++) { pmm = pmm * C(Float(2 * i - 1)) * sin_factor; } if (n == m) { pmm.re.setPrecision(precision); pmm.im.setPrecision(precision); return pmm; } // P_{m+1}^m = z·(2m+1)·P_m^m C pm1m = zw * C(Float(2 * m + 1)) * pmm; if (n == m + 1) { pm1m.re.setPrecision(precision); pm1m.im.setPrecision(precision); return pm1m; } // Recurrence C p_prev = pmm; C p_curr = pm1m; for (int k = m + 1; k < n; k++) { C p_next = (C(Float(2 * k + 1)) * zw * p_curr - C(Float(k + m)) * p_prev) / C(Float(k - m + 1)); p_prev = p_curr; p_curr = p_next; } p_curr.re.setPrecision(precision); p_curr.im.setPrecision(precision); return p_curr; } // ================================================================ // Jacobi polynomial P_n^{(α,β)}(x) // ================================================================ // DLMF §18.9.2 three-term recurrence: // P_0^{(α,β)}(x) = 1 // P_1^{(α,β)}(x) = (α - β)/2 + (α + β + 2)·x/2 // 2n(n+α+β)(2n+α+β-2) P_n = (2n+α+β-1)[(2n+α+β)(2n+α+β-2)x + α²-β²] P_{n-1} // - 2(n+α-1)(n+β-1)(2n+α+β) P_{n-2} // // Requires α, β > -1 (orthogonality condition) template [[nodiscard]] T jacobiP(int n, T alpha, T beta, T x) { if (std::isnan(x) || std::isnan(alpha) || std::isnan(beta)) return std::numeric_limits::quiet_NaN(); if (n < 0) return std::numeric_limits::quiet_NaN(); if (alpha <= T(-1) || beta <= T(-1)) return std::numeric_limits::quiet_NaN(); if (n == 0) return T(1); T ab = alpha + beta; T p_prev = T(1); // P_0 T p_curr = (alpha - beta) / T(2) + (ab + T(2)) * x / T(2); // P_1 if (n == 1) return p_curr; for (int k = 2; k <= n; k++) { T kf = T(k); T c = T(2) * kf + ab; // 2k + α + β // Coefficients (DLMF 18.9.2) T a1 = T(2) * kf * (kf + ab) * (c - T(2)); T a2 = (c - T(1)) * (alpha * alpha - beta * beta); T a3 = (c - T(2)) * (c - T(1)) * c; T a4 = T(2) * (kf + alpha - T(1)) * (kf + beta - T(1)) * c; T p_next = ((a2 + a3 * x) * p_curr - a4 * p_prev) / a1; p_prev = p_curr; p_curr = p_next; } return p_curr; } // ================================================================ // Complex Jacobi polynomial P_n^{(α,β)}(z) // ================================================================ [[nodiscard]] inline Complex jacobiP(int n, double alpha, double beta, const Complex& z) { using C = Complex; if (n < 0 || alpha <= -1.0 || beta <= -1.0) return C(std::numeric_limits::quiet_NaN()); if (n == 0) return C(1.0); double ab = alpha + beta; C p_prev(1.0); C p_curr = C((alpha - beta) / 2.0) + C((ab + 2.0) / 2.0) * z; if (n == 1) return p_curr; for (int k = 2; k <= n; k++) { double kf = double(k); double c = 2.0 * kf + ab; double a1 = 2.0 * kf * (kf + ab) * (c - 2.0); double a2 = (c - 1.0) * (alpha * alpha - beta * beta); double a3 = (c - 2.0) * (c - 1.0) * c; double a4 = 2.0 * (kf + alpha - 1.0) * (kf + beta - 1.0) * c; C p_next = (C(a2) + C(a3) * z) * p_curr - C(a4) * p_prev; p_next = p_next / C(a1); p_prev = p_curr; p_curr = p_next; } return p_curr; } // ================================================================ // Complex Jacobi polynomial P_n^{(α,β)}(z) // ================================================================ [[nodiscard]] inline Complex jacobiP(int n, const Float& alpha, const Float& beta, const Complex& z, int precision) { using C = Complex; if (n < 0 || alpha <= Float(-1) || beta <= Float(-1)) return C(Float::nan()); if (n == 0) { Float one(1); one.setPrecision(precision); return C(one); } int wp = precision + 20; // PrecisionGuard removed: aw/bw/zw's setResultPrecision(wp) propagates req=wp. Float aw = alpha; aw.setResultPrecision(wp); Float bw = beta; bw.setResultPrecision(wp); C zw = z; zw.re.setResultPrecision(wp); zw.im.setResultPrecision(wp); Float ab = aw + bw; C p_prev(Float(1)); C p_curr = C(ldexp(aw - bw, -1)) + C(ldexp(ab + Float(2), -1)) * zw; if (n == 1) { p_curr.re.setPrecision(precision); p_curr.im.setPrecision(precision); return p_curr; } for (int k = 2; k <= n; k++) { Float kf(k); Float c = mulScalarF(kf, uint64_t(2)) + ab; Float a1 = mulScalarF(kf, uint64_t(2)) * (kf + ab) * (c - Float(2)); Float a2 = (c - Float(1)) * (aw * aw - bw * bw); Float a3 = (c - Float(2)) * (c - Float(1)) * c; Float a4 = mulScalarF(kf + aw - Float(1), uint64_t(2)) * (kf + bw - Float(1)) * c; C p_next = (C(a2) + C(a3) * zw) * p_curr - C(a4) * p_prev; p_next = p_next / C(a1); p_prev = p_curr; p_curr = p_next; } p_curr.re.setPrecision(precision); p_curr.im.setPrecision(precision); return p_curr; } [[nodiscard]] inline Complex jacobiP(int n, const Float& alpha, const Float& beta, const Complex& z) { return jacobiP(n, alpha, beta, z, Float::defaultPrecision()); } // ================================================================ // Gegenbauer (ultraspherical) polynomial C_n^{λ}(x) // ================================================================ // C_n^{λ}(x) is a specialization of the Jacobi polynomial: // C_n^{λ}(x) = Γ(2λ+n)Γ(λ+1/2) / (Γ(2λ)Γ(λ+n+1/2)) · P_n^{(λ-1/2, λ-1/2)}(x) // // Three-term recurrence (DLMF 18.9.1): // C_0^{λ}(x) = 1 // C_1^{λ}(x) = 2λx // n·C_n^{λ}(x) = 2(n+λ-1)·x·C_{n-1}^{λ}(x) - (n+2λ-2)·C_{n-2}^{λ}(x) template [[nodiscard]] T gegenbauerC(int n, T lambda, T x) { if (std::isnan(x) || std::isnan(lambda)) return std::numeric_limits::quiet_NaN(); if (n < 0) return std::numeric_limits::quiet_NaN(); if (n == 0) return T(1); T p_prev = T(1); T p_curr = T(2) * lambda * x; if (n == 1) return p_curr; for (int k = 2; k <= n; k++) { T p_next = (T(2) * (T(k) + lambda - T(1)) * x * p_curr - (T(k) + T(2) * lambda - T(2)) * p_prev) / T(k); p_prev = p_curr; p_curr = p_next; } return p_curr; } } // namespace special } // namespace sangi #endif // SANGI_SPECIAL_LEGENDRE_HPP