Skip to content

Commit 4ddbda3

Browse files
committed
Fix N[Erf[x], prec] hanging/wrong results for large x by adding continued fraction for erfc
The Taylor series for erf(x) suffers from catastrophic cancellation for large arguments (e.g. x=10), where intermediate terms grow to ~exp(x²) while the result is ≈1. This caused N[Erf[10], 20] to either time out or return nonsense like 108. Add bigfloat_erfc_cf() using modified Lentz's continued fraction method, which converges rapidly for large x. For |x| > 4, erf is now computed as 1 - erfc(x). The Taylor series path also gets 64 guard bits for safety. Erfc dispatch now uses a dedicated bigfloat_erfc() that picks the best algorithm based on argument size.
1 parent c9ca90e commit 4ddbda3

File tree

2 files changed

+203
-21
lines changed

2 files changed

+203
-21
lines changed

src/functions/math_ast/numerical.rs

Lines changed: 141 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -530,9 +530,7 @@ pub fn expr_to_bigfloat(
530530
}
531531
"Erfc" if args.len() == 1 => {
532532
let x = expr_to_bigfloat(&args[0], bits, rm, cc)?;
533-
let erf_val = bigfloat_erf(&x, bits, rm, cc);
534-
let one = BigFloat::from_i32(1, bits);
535-
Ok(one.sub(&erf_val, bits, rm))
533+
Ok(bigfloat_erfc(&x, bits, rm, cc))
536534
}
537535
"ExpIntegralEi" if args.len() == 1 => {
538536
let x = expr_to_bigfloat(&args[0], bits, rm, cc)?;
@@ -2631,6 +2629,78 @@ pub fn manhattan_distance_ast(args: &[Expr]) -> Result<Expr, InterpreterError> {
26312629

26322630
/// Compute the error function erf(x) using BigFloat arithmetic.
26332631
/// Uses the Taylor series: erf(x) = (2/sqrt(π)) * Σ_{n=0}^{∞} (-1)^n * x^(2n+1) / (n! * (2n+1))
2632+
/// Compute erfc(x) for x > 0 using the continued fraction representation.
2633+
/// erfc(x) = exp(-x²) / (f * sqrt(π)) where f is computed via modified Lentz's method.
2634+
fn bigfloat_erfc_cf(
2635+
x: &astro_float::BigFloat,
2636+
bits: usize,
2637+
rm: astro_float::RoundingMode,
2638+
cc: &mut astro_float::Consts,
2639+
) -> astro_float::BigFloat {
2640+
use astro_float::BigFloat;
2641+
2642+
// Use extra guard bits for intermediate computation
2643+
let work_bits = bits + 64;
2644+
2645+
let half = BigFloat::from_i32(1, work_bits).div(
2646+
&BigFloat::from_i32(2, work_bits),
2647+
work_bits,
2648+
rm,
2649+
);
2650+
2651+
// Modified Lentz's method for the continued fraction
2652+
// erfc(x) = (exp(-x²)/sqrt(π)) * 1/(x + 1/(2x + 2/(x + 3/(2x + ...))))
2653+
// Using: a_n = n * 0.5, b_n = x
2654+
let mut f = x.clone();
2655+
let mut c = x.clone();
2656+
let mut d = BigFloat::from_i32(0, work_bits);
2657+
2658+
let max_iterations = work_bits * 2 + 200;
2659+
for n in 1..max_iterations {
2660+
// a_n = n * 0.5
2661+
let a_n = BigFloat::from_i32(n as i32, work_bits).mul(&half, work_bits, rm);
2662+
2663+
// d = x + a_n * d
2664+
d = x.add(&a_n.mul(&d, work_bits, rm), work_bits, rm);
2665+
// Guard against zero
2666+
if d.is_zero() {
2667+
d = BigFloat::min_positive_normal(work_bits);
2668+
}
2669+
2670+
// c = x + a_n / c
2671+
c = x.add(&a_n.div(&c, work_bits, rm), work_bits, rm);
2672+
if c.is_zero() {
2673+
c = BigFloat::min_positive_normal(work_bits);
2674+
}
2675+
2676+
// d = 1/d
2677+
d = BigFloat::from_i32(1, work_bits).div(&d, work_bits, rm);
2678+
let delta = c.mul(&d, work_bits, rm);
2679+
f = f.mul(&delta, work_bits, rm);
2680+
2681+
// Check convergence: |delta - 1| is negligible
2682+
let one = BigFloat::from_i32(1, work_bits);
2683+
let diff = delta.sub(&one, work_bits, rm).abs();
2684+
if diff.is_zero() {
2685+
break;
2686+
}
2687+
if let Some(diff_exp) = diff.exponent()
2688+
&& diff_exp < -(work_bits as i32)
2689+
{
2690+
break;
2691+
}
2692+
}
2693+
2694+
// erfc(x) = exp(-x²) / (f * sqrt(π))
2695+
let x2 = x.mul(x, work_bits, rm);
2696+
let neg_x2 = x2.neg();
2697+
let exp_neg_x2 = neg_x2.exp(work_bits, rm, cc);
2698+
let pi = cc.pi(work_bits, rm);
2699+
let sqrt_pi = pi.sqrt(work_bits, rm);
2700+
let denom = f.mul(&sqrt_pi, work_bits, rm);
2701+
exp_neg_x2.div(&denom, bits, rm)
2702+
}
2703+
26342704
fn bigfloat_erf(
26352705
x: &astro_float::BigFloat,
26362706
bits: usize,
@@ -2647,52 +2717,102 @@ fn bigfloat_erf(
26472717
let is_negative = x.is_negative();
26482718
let x_abs = x.abs();
26492719

2720+
// For large |x|, use the continued fraction for erfc(x) and compute erf = 1 - erfc.
2721+
// The Taylor series suffers from catastrophic cancellation for large arguments.
2722+
let four = BigFloat::from_i32(4, bits);
2723+
if x_abs.cmp(&four) == Some(1) {
2724+
// |x| > 4: use continued fraction
2725+
let erfc_val = bigfloat_erfc_cf(&x_abs, bits, rm, cc);
2726+
let one = BigFloat::from_i32(1, bits);
2727+
let result = one.sub(&erfc_val, bits, rm);
2728+
return if is_negative { result.neg() } else { result };
2729+
}
2730+
2731+
// For small |x| (≤ 4), use the Taylor series with extra guard bits to handle cancellation.
2732+
// With |x| ≤ 4, the peak term is ~exp(x²/2) ≈ exp(8) ≈ 2981, needing ~12 extra bits.
2733+
// We use 64 guard bits for safety.
2734+
let work_bits = bits + 64;
2735+
26502736
// Taylor series: term_0 = x, term_n = term_{n-1} * x^2 / n
26512737
// contribution_n = term_n / (2n+1), alternating sign
26522738
// sum = Σ (-1)^n * contribution_n
2653-
//
2654-
// Use the target precision for all computations to match Wolfram's
2655-
// rounding behavior.
2656-
let x2 = x_abs.mul(&x_abs, bits, rm);
2739+
let x2 = x_abs.mul(&x_abs, work_bits, rm);
26572740
let mut term = x_abs.clone();
26582741
let mut sum = x_abs.clone();
26592742

2660-
let max_iterations = bits * 2 + 100;
2743+
let max_iterations = work_bits * 2 + 100;
26612744
for n in 1..max_iterations {
2662-
term = term.mul(&x2, bits, rm);
2663-
let n_bf = BigFloat::from_i32(n as i32, bits);
2664-
term = term.div(&n_bf, bits, rm);
2745+
term = term.mul(&x2, work_bits, rm);
2746+
let n_bf = BigFloat::from_i32(n as i32, work_bits);
2747+
term = term.div(&n_bf, work_bits, rm);
26652748

2666-
let denom = BigFloat::from_i32((2 * n + 1) as i32, bits);
2667-
let contribution = term.div(&denom, bits, rm);
2749+
let denom = BigFloat::from_i32((2 * n + 1) as i32, work_bits);
2750+
let contribution = term.div(&denom, work_bits, rm);
26682751

26692752
if n % 2 == 1 {
2670-
sum = sum.sub(&contribution, bits, rm);
2753+
sum = sum.sub(&contribution, work_bits, rm);
26712754
} else {
2672-
sum = sum.add(&contribution, bits, rm);
2755+
sum = sum.add(&contribution, work_bits, rm);
26732756
}
26742757

26752758
if contribution.is_zero() {
26762759
break;
26772760
}
26782761
if let (Some(c_exp), Some(s_exp)) =
26792762
(contribution.exponent(), sum.exponent())
2680-
&& s_exp - c_exp > (bits as i32)
2763+
&& s_exp - c_exp > (work_bits as i32)
26812764
{
26822765
break;
26832766
}
26842767
}
26852768

2686-
// Multiply by 2/sqrt(π)
2687-
let two = BigFloat::from_i32(2, bits);
2688-
let pi = cc.pi(bits, rm);
2689-
let sqrt_pi = pi.sqrt(bits, rm);
2690-
let factor = two.div(&sqrt_pi, bits, rm);
2769+
// Multiply by 2/sqrt(π), round to final precision
2770+
let two = BigFloat::from_i32(2, work_bits);
2771+
let pi = cc.pi(work_bits, rm);
2772+
let sqrt_pi = pi.sqrt(work_bits, rm);
2773+
let factor = two.div(&sqrt_pi, work_bits, rm);
26912774
let result = sum.mul(&factor, bits, rm);
26922775

26932776
if is_negative { result.neg() } else { result }
26942777
}
26952778

2779+
/// Compute erfc(x) with arbitrary precision.
2780+
/// For large x, uses continued fraction directly. For small x, uses 1 - erf(x).
2781+
fn bigfloat_erfc(
2782+
x: &astro_float::BigFloat,
2783+
bits: usize,
2784+
rm: astro_float::RoundingMode,
2785+
cc: &mut astro_float::Consts,
2786+
) -> astro_float::BigFloat {
2787+
use astro_float::BigFloat;
2788+
2789+
if x.is_zero() {
2790+
return BigFloat::from_i32(1, bits);
2791+
}
2792+
2793+
let is_negative = x.is_negative();
2794+
let x_abs = x.abs();
2795+
2796+
let four = BigFloat::from_i32(4, bits);
2797+
let result = if x_abs.cmp(&four) == Some(1) {
2798+
// |x| > 4: use continued fraction directly for best precision
2799+
bigfloat_erfc_cf(&x_abs, bits, rm, cc)
2800+
} else {
2801+
// |x| <= 4: compute via 1 - erf(x)
2802+
let erf_val = bigfloat_erf(&x_abs, bits, rm, cc);
2803+
let one = BigFloat::from_i32(1, bits);
2804+
one.sub(&erf_val, bits, rm)
2805+
};
2806+
2807+
// erfc(-x) = 2 - erfc(x)
2808+
if is_negative {
2809+
let two = BigFloat::from_i32(2, bits);
2810+
two.sub(&result, bits, rm)
2811+
} else {
2812+
result
2813+
}
2814+
}
2815+
26962816
/// Compute the exponential integral Ei(x) using BigFloat arithmetic.
26972817
/// For real x: Ei(x) = γ + ln|x| + Σ_{n=1}^{∞} x^n / (n * n!)
26982818
/// where γ is the Euler-Mascheroni constant.

tests/interpreter_tests/calculus.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -798,6 +798,68 @@ mod erf {
798798
// D[Erf[x], x] = 2/(E^(x^2)*Sqrt[Pi]) — tests denominator formatting
799799
assert_eq!(interpret("D[Erf[x],x]").unwrap(), "2/(E^x^2*Sqrt[Pi])");
800800
}
801+
802+
#[test]
803+
fn n_erf_1() {
804+
// N[Erf[1], 20] — small argument, Taylor series path
805+
let result = interpret("N[Erf[1], 20]").unwrap();
806+
assert!(
807+
result.starts_with("0.84270079294971486934"),
808+
"N[Erf[1], 20] = {result}"
809+
);
810+
}
811+
812+
#[test]
813+
fn n_erf_5() {
814+
// N[Erf[5], 20] — large argument, continued fraction path
815+
let result = interpret("N[Erf[5], 20]").unwrap();
816+
assert!(
817+
result.starts_with("0.99999999999846254020"),
818+
"N[Erf[5], 20] = {result}"
819+
);
820+
}
821+
822+
#[test]
823+
fn n_erf_10() {
824+
// N[Erf[10], 20] — very large argument, result is 1 to 20 digits
825+
assert_eq!(interpret("N[Erf[10], 20]").unwrap(), "1.`20.");
826+
}
827+
828+
#[test]
829+
fn n_erf_neg_10() {
830+
// Erf is odd: N[Erf[-10], 20] = -1
831+
assert_eq!(interpret("N[Erf[-10], 20]").unwrap(), "-1.`20.");
832+
}
833+
834+
#[test]
835+
fn n_erfc_5() {
836+
// N[Erfc[5], 20] — continued fraction path
837+
let result = interpret("N[Erfc[5], 20]").unwrap();
838+
assert!(
839+
result.starts_with("1.53745979442803485018"),
840+
"N[Erfc[5], 20] = {result}"
841+
);
842+
}
843+
844+
#[test]
845+
fn n_erfc_10() {
846+
// N[Erfc[10], 20] — very small result
847+
let result = interpret("N[Erfc[10], 20]").unwrap();
848+
assert!(
849+
result.starts_with("2.08848758376254475700"),
850+
"N[Erfc[10], 20] = {result}"
851+
);
852+
}
853+
854+
#[test]
855+
fn n_erf_3() {
856+
// N[Erf[3], 20] — moderate argument, Taylor series path
857+
let result = interpret("N[Erf[3], 20]").unwrap();
858+
assert!(
859+
result.starts_with("0.99997790950300141455"),
860+
"N[Erf[3], 20] = {result}"
861+
);
862+
}
801863
}
802864

803865
mod integrate_gaussian {

0 commit comments

Comments
 (0)