// Copyright (C) 2026 Kiyotsugu Arai // SPDX-License-Identifier: LGPL-3.0-or-later // error_function.hpp // Template wrappers for the error function family // // Functions provided: // erf(x) — error function // erfc(x) — complementary error function 1 - erf(x) // erfcx(x) — scaled complementary error function exp(x²)·erfc(x) // erfi(x) — imaginary error function erfi(x) = -i·erf(i·x) = (2/√π) ∫_0^x e^{t²} dt // erfInv(x) — inverse error function erfInv(erf(x)) = x, domain (-1, 1) // // Supported types: // float, double, long double — delegated to std:: / direct computation // Float — delegated to the implementation in FloatMath.cpp // Complex — series + Laplace continued fraction // Complex — arbitrary-precision Laplace continued fraction #ifndef SANGI_SPECIAL_ERROR_FUNCTION_HPP #define SANGI_SPECIAL_ERROR_FUNCTION_HPP #include #include #include #include #include #include namespace sangi { namespace special { // ================================================================ // error function erf(x) = (2/√π) ∫₀ˣ e^{-t²} dt // ================================================================ /// Native floating-point types: delegated to std::erf template [[nodiscard]] T erf(T x) { return std::erf(x); } // Float type: please use sangi::erf(x, precision) directly. // ================================================================ // complementary error function erfc(x) = 1 - erf(x) // ================================================================ /// Native floating-point types: delegated to std::erfc template [[nodiscard]] T erfc(T x) { return std::erfc(x); } // Float type: please use sangi::erfc(x, precision) directly. // ================================================================ // scaled complementary error function erfcx(x) = exp(x²)·erfc(x) // ================================================================ /// Native floating-point types: direct computation + asymptotic expansion template [[nodiscard]] T erfcx(T x) { if (std::isnan(x)) return std::numeric_limits::quiet_NaN(); if (std::isinf(x)) { return x > T(0) ? T(0) : std::numeric_limits::infinity(); } if (x == T(0)) return T(1); // x > 0 and large: asymptotic expansion // erfcx(x) = (1/(x√π)) · Σ_{n=0}^{N} (-1)^n (2n-1)!! / (2x²)^n if (x > T(4)) { T t = T(1) / (T(2) * x * x); T series = T(1); T term = T(1); for (int k = 1; k <= 25; k++) { term *= -static_cast(2 * k - 1) * t; if (std::abs(term) > std::abs(series)) break; // divergence begins series += term; if (std::abs(term) < std::numeric_limits::epsilon()) break; } const T sqrt_pi = std::sqrt(static_cast(3.14159265358979323846L)); return series / (x * sqrt_pi); } // x > 0: direct computation (range where exp(x²) does not overflow) if (x > T(0)) { return std::exp(x * x) * std::erfc(x); } // x < 0: erfcx(x) = 2·exp(x²) - erfcx(-x) T erfcx_pos = erfcx(-x); T exp_x2 = std::exp(x * x); if (std::isinf(exp_x2)) return exp_x2; return T(2) * exp_x2 - erfcx_pos; } // Float type: please use sangi::erfcx(x, precision) directly. // ================================================================ // Complex overloads // ================================================================ // ---------------------------------------------------------------- // erf(z) = (2z/√π)·e^{-z²}·Σ_{n=0}^∞ (2z²)^n / (1·3·5···(2n+1)) // ---------------------------------------------------------------- // Series form that converges for all z (₁F₁ based). // Accumulating only positive terms avoids cancellation. template [[nodiscard]] Complex erf(Complex z) { using C = Complex; if (z == C(R(0))) return C(R(0)); C z2 = z * z; C two_z2 = C(R(2)) * z2; C term(R(1)); C sum(R(1)); // BUGFIX (2026-05-31): getEpsilon()/getPi() (no argument) return // defaultPrecision (~55 digits), so the convergence test was cut off early at // ~55 digits and the constant 2/√π was also ~55 digits, capping the // Complex version at ~55 digits regardless of the argument's working // precision (detected by the Complex cross-check harness for erfcx). Take the // working precision of argument z and use the precision-aware version. const int max_iter = IsSangiFloat ? 2000 : 200; R eps; if constexpr (IsSangiFloat) eps = detail::getEpsilon(z.re.precision()); else eps = detail::getEpsilon(); for (int n = 0; n < max_iter; n++) { term = term * two_z2 / C(R(2 * n + 3)); sum = sum + term; if (abs(term) < eps * abs(sum)) break; } R two_over_sqrt_pi; if constexpr (IsSangiFloat) { int wp = z.re.precision(); two_over_sqrt_pi = R(2) / sqrt(detail::getPi(wp), wp); } else two_over_sqrt_pi = R(1.1283791670955125738961589031215452L); return C(two_over_sqrt_pi) * z * exp(-z2) * sum; } // ---------------------------------------------------------------- // erfc(z) = 1 - erf(z) // ---------------------------------------------------------------- template [[nodiscard]] Complex erfc(Complex z) { return Complex(R(1)) - erf(z); } // ---------------------------------------------------------------- // erfcx(z) = exp(z²)·erfc(z) — Complex // ---------------------------------------------------------------- // Re(z) > 0 and |z| > 2: Laplace continued fraction (Modified Lentz) // g = z + a₁/(z + a₂/(z + ...)), a_n = n/2 // erfcx(z) = 1/(√π · g) // |z| ≤ 2: direct computation exp(z²)·erfc(z) // Re(z) < 0 and |z| > 2: reflection erfcx(z) = 2·exp(z²) - erfcx(-z) template [[nodiscard]] Complex erfcx(Complex z) { using C = Complex; // small |z|: direct computation (exp(z²) does not overflow) R z_abs = sangi::abs(z); if (z_abs <= R(2)) { C z2 = z * z; return exp(z2) * erfc(z); } // Re(z) ≥ 0: Laplace continued fraction if (z.re >= R(0)) { R sqrt_pi = std::sqrt(std::numbers::pi_v); R eps = std::numeric_limits::epsilon(); R tiny = R(1e-300); C f = z; C big_C = z; C D(R(0)); for (int n = 1; n <= 300; n++) { C a_n{R(n) / R(2)}; D = z + a_n * D; if (sangi::abs(D) < tiny) D = C(tiny); D = C(R(1)) / D; big_C = z + a_n / big_C; if (sangi::abs(big_C) < tiny) big_C = C(tiny); C delta = big_C * D; f = f * delta; if (sangi::abs(delta - C(R(1))) < eps) break; } return C(R(1)) / (C(sqrt_pi) * f); } // Re(z) < 0: reflection erfcx(z) = 2·exp(z²) - erfcx(-z) C z2 = z * z; return C(R(2)) * exp(z2) - erfcx(C(R(0)) - z); } // ---------------------------------------------------------------- // erfcx(z) = exp(z²)·erfc(z) — Complex // ---------------------------------------------------------------- [[nodiscard]] inline Complex erfcx(Complex z, int precision) { using C = Complex; int wp = precision + 20; // PrecisionGuard removed: setResultPrecision(wp) on w.re/im propagates req=wp // (FLOAT_PRECISION_PLAN Step 12 follow-up, same pattern as gamma.hpp 6fa993f). C w = z; w.re.setResultPrecision(wp); w.im.setResultPrecision(wp); // BUGFIX (2026-05-31): for exact input (e.g. 1+1i, re/im being finite binary // fractions), the internal exact/exact division drops to default_precision // (~55 digits) (detected by the Complex cross-check harness). Add working // precision to make it non-exact. w.re.setEffectiveBits(Float::precisionToBits(wp)); w.im.setEffectiveBits(Float::precisionToBits(wp)); double z_abs = std::sqrt(w.re.toDouble() * w.re.toDouble() + w.im.toDouble() * w.im.toDouble()); // small |z|: direct computation erfcx = exp(z²)·erfc(z). // BUGFIX (2026-05-31): raised the threshold from 2 to 4. The Laplace continued // fraction converges extremely slowly near |z|≈2 and capped out at about 18 // digits (detected by harness erfcx(0.3+2i)). Actually apply to the erfc call // the wp2 guard that compensates for erfc's cancellation (z²/ln2 bits) // (previously wp2 was computed but left unused). if (z_abs <= 4.0) { int extra = static_cast(std::ceil(z_abs * z_abs / std::log(2.0))) + 10; int wp2 = wp + extra; C ww = z; ww.re.setResultPrecision(wp2); ww.im.setResultPrecision(wp2); ww.re.setEffectiveBits(Float::precisionToBits(wp2)); ww.im.setEffectiveBits(Float::precisionToBits(wp2)); C erfc_val = erfc(ww); C z2 = ww * ww; C result = exp(z2) * erfc_val; result.re.setPrecision(precision); result.im.setPrecision(precision); return result; } // Re(z) < 0: reflection erfcx(z) = 2·exp(z²) - erfcx(-z) if (w.re.toDouble() < 0.0) { C neg_w = -w; C erfcx_pos = erfcx(neg_w, precision + 10); erfcx_pos.re.setResultPrecision(wp); erfcx_pos.im.setResultPrecision(wp); C z2 = w * w; C result = C(Float(2)) * exp(z2) - erfcx_pos; result.re.setPrecision(precision); result.im.setPrecision(precision); return result; } // Re(z) ≥ 0, |z| > 2: Laplace continued fraction (Modified Lentz) Float two(2); Float eps = Float::epsilon(wp); C f = w; C big_C = w; C D; for (int n = 1; n < 10 * wp; n++) { C a_n{Float(n) / two}; D = w + a_n * D; if (abs(D) < eps) D = C(eps); D = C(Float(1)) / D; big_C = w + a_n / big_C; if (abs(big_C) < eps) big_C = C(eps); C delta = big_C * D; f = f * delta; // convergence test C diff = delta - C(Float(1)); Float diff_abs = abs(diff); if (diff_abs.isZero() || diff_abs < eps) break; } Float sqrtpi = sangi::sqrt(Float::pi(wp), wp); C result = C(Float(1)) / (C(sqrtpi) * f); result.re.setPrecision(precision); result.im.setPrecision(precision); return result; } [[nodiscard]] inline Complex erfcx(Complex z) { return erfcx(std::move(z), Float::defaultPrecision()); } // ================================================================ // imaginary error function erfi(x) = (2/√π) ∫_0^x e^{t²} dt // ================================================================ // Properties: odd function, erfi(0) = 0, erfi(∞) = +∞ (diverges on the order of e^{x²}) // Relation: erfi(x) = -i · erf(i·x) = imag(erf(0 + i·x)) // // Implementation: reuses the existing sangi::special::erf(Complex) (Taylor + // Laplace continued fraction, ~1e-15 accuracy across the whole double range). // Since erf(i·x) is purely imaginary, just returning its imag part is OK. // // Overflow: in double precision, e^{x²} overflows at x ≈ 27 → ±Inf. template [[nodiscard]] T erfi(T x) { if (std::isnan(x)) return std::numeric_limits::quiet_NaN(); if (x == T(0)) return T(0); if (std::isinf(x)) { return x > T(0) ? std::numeric_limits::infinity() : -std::numeric_limits::infinity(); } double xd = static_cast(x); if (std::abs(xd) > 27.0) { return xd > 0.0 ? std::numeric_limits::infinity() : -std::numeric_limits::infinity(); } // erfi(x) = imag(erf(i x)) Complex r = erf(Complex(0.0, xd)); return static_cast(r.im); } // ================================================================ // inverse error function erfInv(p), p ∈ (-1, 1) // ================================================================ // Solution: y = erfInv(p) is the real number satisfying erf(y) = p // // Implementation: Newton's method (machine precision in 3-4 iterations) // y_{n+1} = y_n - (erf(y_n) - p) / (2/√π · e^{-y_n²}) // Initial guess: // |p| ≤ 0.7 : y_0 = p · (a + b·p²) (Acklam-style minimax) // |p| > 0.7 : y_0 = sign(p) · √(-ln((1-|p|)(1+|p|))) // // Degenerate: p = ±1 → ±∞, |p| > 1 → NaN template [[nodiscard]] T erfInv(T p) { if (std::isnan(p)) return std::numeric_limits::quiet_NaN(); if (p == T(0)) return T(0); if (p == T(1)) return std::numeric_limits::infinity(); if (p == T(-1)) return -std::numeric_limits::infinity(); if (std::abs(p) > T(1)) return std::numeric_limits::quiet_NaN(); constexpr T sqrt_pi = T(1.7724538509055160272981674833); constexpr T half_sqrt_pi = sqrt_pi / T(2); T ap = std::abs(p); T y; if (ap <= T(0.7)) { // first-order initial guess: erfInv(p) ≈ (√π/2) · p y = half_sqrt_pi * p; } else { // tail approximation: erfInv(p) ~ √(-ln((1-|p|)(1+|p|))) · sign(p) T t = std::sqrt(-std::log((T(1) - ap) * (T(1) + ap))); y = (p > T(0) ? T(1) : T(-1)) * t; } // Newton iteration (up to 8 times until convergence) for (int iter = 0; iter < 8; ++iter) { T erf_y = std::erf(y); T err = erf_y - p; if (std::abs(err) < std::numeric_limits::epsilon() * (T(1) + std::abs(y))) break; T dy = err / (T(2) / sqrt_pi * std::exp(-y * y)); y -= dy; if (std::abs(dy) < std::numeric_limits::epsilon() * (T(1) + std::abs(y))) break; } return y; } } // namespace special } // namespace sangi #endif // SANGI_SPECIAL_ERROR_FUNCTION_HPP