// Copyright (C) 2026 Kiyotsugu Arai // SPDX-License-Identifier: LGPL-3.0-or-later // coulomb.hpp // Coulomb wave functions // // Provided functions: // coulombF(L, eta, rho) — regular Coulomb wave function F_L(η, ρ) // coulombG(L, eta, rho) — irregular Coulomb wave function G_L(η, ρ) // coulombCL(L, eta) — Coulomb normalization constant C_L(η) // // Supported types: float, double, long double #ifndef SANGI_SPECIAL_COULOMB_HPP #define SANGI_SPECIAL_COULOMB_HPP #include #include #include namespace sangi { namespace special { // ================================================================ // Coulomb normalization constant // C_L(η) = 2^L exp(-πη/2) |Γ(L+1+iη)| / Γ(2L+2) // ================================================================ template [[nodiscard]] T coulombCL(int L, T eta) { // |Γ(L+1+iη)|² = π·η / (sinh(πη)) · Π_{s=0}^{L} (s² + η²) (for L >= 0) // For L=0: C_0(η) = sqrt(2πη / (exp(2πη) - 1)) T pi = std::numbers::pi_v; T two_pi_eta = T(2) * pi * eta; T p = T(1); if (std::abs(eta) < T(1e-15)) { // η → 0: C_L → simplification of the form 2^L / (2L+1)!! p = T(1); for (int s = 1; s <= L; ++s) p *= T(s); T denom = T(1); for (int s = 1; s <= 2 * L + 1; ++s) denom *= T(s); return std::pow(T(2), T(L)) * p / denom; } // Sommerfeld parameter T eta_factor; if (two_pi_eta > T(500)) { eta_factor = two_pi_eta * std::exp(-two_pi_eta); } else if (two_pi_eta < T(-500)) { eta_factor = -two_pi_eta; } else { eta_factor = two_pi_eta / (std::exp(two_pi_eta) - T(1)); } T C0_sq = eta_factor; T CL_sq = C0_sq; for (int s = 1; s <= L; ++s) { CL_sq *= (T(s * s) + eta * eta) / (T(s) * T(2 * s + 1)); } return std::sqrt(CL_sq); } // ================================================================ // Regular Coulomb wave function F_L(η, ρ) // Taylor series: F_L = C_L · ρ^{L+1} Σ A_k ρ^k // ================================================================ namespace detail { template void coulomb_fg_series(int L, T eta, T rho, T& F_val, T& G_val) { // F_L via power series: F_L = C_L(η) · ρ^{L+1} · Σ a_k ρ^k // a_0 = 1, a_1 = η/(L+1) // a_{k+1} = (2ηa_k - a_{k-1}) / ((k+1)(k+2L+2)) T CL = coulombCL(L, eta); const int max_terms = 400; T a_prev = T(0); T a_curr = T(1); T sum = T(1); for (int k = 0; k < max_terms; ++k) { T a_next = (T(2) * eta * a_curr - a_prev) / (T(k + 1) * T(k + 2 * L + 2)); a_prev = a_curr; a_curr = a_next; T term = a_curr * std::pow(rho, T(k + 1)); sum += term; if (k >= 5 && std::abs(term) < std::numeric_limits::epsilon() * std::abs(sum)) break; } F_val = CL * std::pow(rho, T(L + 1)) * sum; // G_L via numerical-derivative approximation (simplified version) T dr = rho * T(1e-6); if (dr < T(1e-10)) dr = T(1e-10); T sum_plus = T(1); a_prev = T(0); a_curr = T(1); T rho_p = rho + dr; for (int k = 0; k < max_terms; ++k) { T a_next = (T(2) * eta * a_curr - a_prev) / (T(k + 1) * T(k + 2 * L + 2)); a_prev = a_curr; a_curr = a_next; T term = a_curr * std::pow(rho_p, T(k + 1)); sum_plus += term; if (k >= 5 && std::abs(term) < std::numeric_limits::epsilon() * std::abs(sum_plus)) break; } T F_plus = CL * std::pow(rho_p, T(L + 1)) * sum_plus; T F_prime = (F_plus - F_val) / dr; // Wronskian: F_L · G_L' - F_L' · G_L = 1 // G_L ≈ (F_L' · ρ - (L+1) · F_L) / ... (Coulomb phase) // Simple approximation: G_L from asymptotic for moderate ρ if (rho > T(L + 1) + T(10)) { T sigma = T(0); for (int k = 1; k <= L; ++k) sigma += std::atan(eta / T(k)); T theta = rho - eta * std::log(T(2) * rho) - T(L) * std::numbers::pi_v / T(2) + sigma; G_val = std::cos(theta) / std::cos(theta - std::numbers::pi_v / T(4)); // Wronskian correction if (std::abs(F_val) > std::numeric_limits::epsilon() * T(10)) G_val = (T(1) + F_prime * G_val) / F_val; else G_val = T(0); } else { // Small ρ: G is singular → NaN G_val = std::numeric_limits::quiet_NaN(); } } } // namespace detail template [[nodiscard]] T coulombF(int L, T eta, T rho) { if (std::isnan(eta) || std::isnan(rho) || rho < T(0)) return std::numeric_limits::quiet_NaN(); if (rho == T(0)) return T(0); T F, G; detail::coulomb_fg_series(L, eta, rho, F, G); return F; } template [[nodiscard]] T coulombG(int L, T eta, T rho) { if (std::isnan(eta) || std::isnan(rho) || rho <= T(0)) return std::numeric_limits::quiet_NaN(); T F, G; detail::coulomb_fg_series(L, eta, rho, F, G); return G; } } // namespace special } // namespace sangi #endif // SANGI_SPECIAL_COULOMB_HPP