Skip to main content

oxinum_float/
special.rs

1//! Special mathematical functions for arbitrary-precision floating-point.
2//!
3//! Provides pure-Rust implementations of:
4//! - [`gamma`] — Gamma function Γ(x)
5//! - [`ln_gamma`] — log-Gamma function ln(Γ(x))
6//! - [`digamma`] — Digamma function ψ(x) = d/dx ln(Γ(x))
7//! - [`erf`] — Error function
8//! - [`erfc`] — Complementary error function
9//! - [`bessel_j0`] — Bessel function J₀(x)
10//! - [`euler_gamma`] — Euler–Mascheroni constant γ
11//! - [`catalan`] — Catalan's constant G
12//! - [`free_cache`] — No-op (MPFR compatibility shim)
13//!
14//! All functions work on [`DBig`] (decimal arbitrary-precision big float) and
15//! accept a `precision` parameter giving the number of significant decimal
16//! digits to carry through the computation.
17//!
18//! ## Algorithms
19//!
20//! - **Gamma / log-Gamma**: Lanczos approximation (g=7, 9 coefficients) for
21//!   x ∈ (0, 20]; Stirling series for x > 20; reflection formula for x < 0.
22//! - **Digamma**: recurrence to shift x > 8, then Bernoulli asymptotic series.
23//! - **Erf**: Taylor series for |x| ≤ 2; asymptotic continued-fraction for |x| > 2.
24//! - **Erfc**: complement of erf with careful sign handling.
25//! - **Bessel J₀**: power series for |x| ≤ 12; asymptotic expansion for |x| > 12.
26//! - **Euler γ**: pre-stored 200-digit decimal string.
27//! - **Catalan G**: pre-stored 200-digit decimal string.
28
29use crate::elementary::{exp, ln, sqrt, truncate_to_precision};
30use crate::trig::{cos, sin};
31use crate::{DBig, OxiNumError, OxiNumResult};
32use std::str::FromStr;
33
34// ---------------------------------------------------------------------------
35// Pre-stored high-precision constants
36// ---------------------------------------------------------------------------
37
38/// 200 decimal digits of the Euler–Mascheroni constant γ.
39const EULER_GAMMA_200: &str =
40    "0.57721566490153286060651209008240243104215933593992359880576723488486772677766467\
41     09369596694504673425296068890403823946951285765500044721133652219082019609773798\
42     42060299066541920";
43
44/// 200 decimal digits of Catalan's constant G.
45const CATALAN_200: &str =
46    "0.91596559417721901505460351493238411077414937428167213426649811962176301977625476\
47     94709053505388297009241232177413908000993617044680583060800913695168700767543730\
48     49861741618979838";
49
50// ---------------------------------------------------------------------------
51// Public constant functions
52// ---------------------------------------------------------------------------
53
54/// Compute the Euler–Mascheroni constant γ ≈ 0.5772156649… to `precision`
55/// significant decimal digits (capped at 200).
56///
57/// # Examples
58///
59/// ```
60/// let g = oxinum_float::special::euler_gamma(30);
61/// assert!(g.to_string().starts_with("0.577215664901532860606512"));
62/// ```
63pub fn euler_gamma(precision: usize) -> DBig {
64    parse_at_precision(EULER_GAMMA_200, precision)
65}
66
67/// Compute Catalan's constant G ≈ 0.9159655941… to `precision` significant
68/// decimal digits (capped at 200).
69///
70/// # Examples
71///
72/// ```
73/// let g = oxinum_float::special::catalan(30);
74/// assert!(g.to_string().starts_with("0.915965594177219015054603"));
75/// ```
76pub fn catalan(precision: usize) -> DBig {
77    parse_at_precision(CATALAN_200, precision)
78}
79
80/// No-op cache cleanup shim.
81///
82/// MPFR (via `rug`) allocates thread-local caches for constants like π that
83/// must be freed explicitly.  `dashu-float` has no such global caches, so
84/// this function is intentionally a no-op provided for API compatibility.
85pub fn free_cache() {
86    // dashu-float has no MPFR-style global constant cache to free.
87}
88
89// ---------------------------------------------------------------------------
90// Gamma function
91// ---------------------------------------------------------------------------
92
93/// Compute the Gamma function Γ(x) to `precision` significant decimal digits.
94///
95/// Uses Lanczos approximation (g=7) for x ∈ (0, 20], Stirling series for
96/// x > 20, and the reflection formula Γ(x)·Γ(1-x) = π/sin(πx) for x < 0.
97///
98/// # Errors
99///
100/// Returns `OxiNumError::Domain` if x is zero or a negative integer (poles
101/// of the Gamma function).
102pub fn gamma(x: &DBig, precision: usize) -> OxiNumResult<DBig> {
103    validate_precision(precision)?;
104    let guard = precision + 20;
105    gamma_impl(x, guard, precision)
106}
107
108fn gamma_impl(x: &DBig, guard: usize, output_prec: usize) -> OxiNumResult<DBig> {
109    let zero = make_dbig("0.0")?;
110    let one = make_dbig("1.0")?;
111
112    if *x == zero {
113        return Err(OxiNumError::Domain("Gamma(0) is undefined (pole)".into()));
114    }
115
116    // Check for negative integers: x is a negative integer if x < 0 and
117    // x - round(x) == 0 where round(x) = floor(x) (for negative integers floor gives them exactly)
118    if is_negative(x) && is_integer_value(x) {
119        return Err(OxiNumError::Domain(
120            "Gamma undefined at negative integers (poles)".into(),
121        ));
122    }
123
124    if is_positive(x) {
125        if to_f64_approx(x) > 20.0 {
126            stirling_gamma(x, guard, output_prec)
127        } else {
128            lanczos_gamma(x, guard, output_prec)
129        }
130    } else {
131        // Reflection formula: Γ(x) = π / (sin(πx) · Γ(1-x))
132        let pi = crate::constants::compute_pi(guard);
133        let pi_ext = extend(pi, guard);
134        let x_ext = extend(x.clone(), guard);
135        let pi_x = &pi_ext * &x_ext;
136        let sin_pi_x = sin(&pi_x, guard)?;
137        if is_approx_zero(&sin_pi_x) {
138            return Err(OxiNumError::Domain(
139                "Gamma undefined at negative integers (poles)".into(),
140            ));
141        }
142        let one_minus_x = &extend(one, guard) - &x_ext;
143        let gamma_pos = gamma_impl(&one_minus_x, guard, guard)?;
144        let result = &pi_ext / &(&sin_pi_x * &gamma_pos);
145        Ok(truncate_to_precision(result, output_prec))
146    }
147}
148
149/// Lanczos approximation for Γ(x), x > 0, moderate range.
150fn lanczos_gamma(x: &DBig, guard: usize, output_prec: usize) -> OxiNumResult<DBig> {
151    // Lanczos g=7, 9 coefficients (Numerical Recipes)
152    const LANCZOS_G: f64 = 7.0;
153    const LANCZOS_COEFFS: &[f64] = &[
154        0.999_999_999_999_809_9,
155        676.520_368_121_885_1,
156        -1_259.139_216_722_403,
157        771.323_428_777_653_1,
158        -176.615_029_162_140_6,
159        12.507_343_278_686_905,
160        -0.138_571_095_265_720_1,
161        9.984_369_578_019_572e-6,
162        1.505_632_735_149_311_6e-7,
163    ];
164
165    let x_ext = extend(x.clone(), guard);
166    let two_pi = dbig_f64(2.0 * std::f64::consts::PI, guard);
167    let sqrt_2pi = sqrt(&two_pi, guard)?;
168
169    // Aggregate the rational sum
170    let mut ag = dbig_f64(LANCZOS_COEFFS[0], guard);
171    for (i, &c) in LANCZOS_COEFFS[1..].iter().enumerate() {
172        let denom = &x_ext + &dbig_f64(i as f64 + 1.0, guard);
173        ag = &ag + &(&dbig_f64(c, guard) / &denom);
174    }
175
176    // tmp = x + g + 0.5
177    let tmp = &x_ext + &dbig_f64(LANCZOS_G + 0.5, guard);
178
179    // result = sqrt_2pi * ag * tmp^(x+0.5) * exp(-tmp) / x
180    // = sqrt_2pi * ag * (tmp/e)^(x+0.5) via exp((x+0.5)*ln(tmp) - tmp)
181    let x_plus_half = &x_ext + &dbig_f64(0.5, guard);
182    let ln_tmp = ln(&tmp, guard)?;
183    let log_part = &(&x_plus_half * &ln_tmp) - &tmp;
184    let exp_part = exp(&log_part, guard)?;
185
186    let result = &(&sqrt_2pi * &ag) * &exp_part;
187    // Divide by x (Lanczos computes Γ(x+1)/x = Γ(x) form)
188    let final_result = &result / &x_ext;
189    Ok(truncate_to_precision(final_result, output_prec))
190}
191
192/// Stirling series for Γ(x), x > 20.
193fn stirling_gamma(x: &DBig, guard: usize, output_prec: usize) -> OxiNumResult<DBig> {
194    let x_ext = extend(x.clone(), guard);
195    let two_pi = dbig_f64(2.0 * std::f64::consts::PI, guard);
196    let sqrt_2pi = sqrt(&two_pi, guard)?;
197
198    // sqrt(2π/x)
199    let sqrt_2pi_over_x = &sqrt_2pi / &sqrt(&x_ext, guard)?;
200
201    // (x/e)^x via exp(x*ln(x) - x)
202    let ln_x = ln(&x_ext, guard)?;
203    let log_part = &(&x_ext * &ln_x) - &x_ext;
204    let exp_part = exp(&log_part, guard)?;
205
206    // Stirling correction: 1 + 1/(12x) + 1/(288x²) - 139/(51840x³) - 571/(2488320x⁴)
207    let x2 = &x_ext * &x_ext;
208    let x3 = &x2 * &x_ext;
209    let x4 = &x2 * &x2;
210
211    let c1 = &dbig_f64(1.0, guard) / &(&dbig_f64(12.0, guard) * &x_ext);
212    let c2 = &dbig_f64(1.0, guard) / &(&dbig_f64(288.0, guard) * &x2);
213    let c3 = &(&dbig_f64(139.0, guard) / &(&dbig_f64(51840.0, guard) * &x3));
214    let c4 = &(&dbig_f64(571.0, guard) / &(&dbig_f64(2488320.0, guard) * &x4));
215
216    let correction = &(&(&dbig_f64(1.0, guard) + &c1) + &c2) - (&(&c3.clone() + &c4.clone()));
217
218    let result = &(&sqrt_2pi_over_x * &exp_part) * &correction;
219    Ok(truncate_to_precision(result, output_prec))
220}
221
222// ---------------------------------------------------------------------------
223// Log-Gamma
224// ---------------------------------------------------------------------------
225
226/// Compute ln(Γ(x)) to `precision` significant decimal digits.
227///
228/// # Errors
229///
230/// Returns `OxiNumError::Domain` if x ≤ 0.
231pub fn ln_gamma(x: &DBig, precision: usize) -> OxiNumResult<DBig> {
232    validate_precision(precision)?;
233    let zero = make_dbig("0.0")?;
234    if *x <= zero {
235        return Err(OxiNumError::Domain("ln_gamma undefined for x <= 0".into()));
236    }
237
238    let guard = precision + 20;
239    let x_f = to_f64_approx(x);
240
241    if x_f > 10.0 {
242        stirling_ln_gamma(x, guard, precision)
243    } else {
244        // For small x: compute gamma then take ln
245        let g = gamma_impl(x, guard, guard)?;
246        let lg = ln(&g, guard)?;
247        Ok(truncate_to_precision(lg, precision))
248    }
249}
250
251/// Stirling series for ln(Γ(x)), x > 10.
252fn stirling_ln_gamma(x: &DBig, guard: usize, output_prec: usize) -> OxiNumResult<DBig> {
253    let x_ext = extend(x.clone(), guard);
254    let two_pi = dbig_f64(2.0 * std::f64::consts::PI, guard);
255    let ln_2pi = ln(&two_pi, guard)?;
256    let ln_x = ln(&x_ext, guard)?;
257
258    // ln Γ(x) ≈ (x - 1/2) ln(x) - x + ln(2π)/2 + 1/(12x) - 1/(360x³) + 1/(1260x⁵) - 1/(1680x⁷)
259    let half = dbig_f64(0.5, guard);
260    let x_minus_half = &x_ext - &half;
261
262    let x2 = &x_ext * &x_ext;
263    let x3 = &x2 * &x_ext;
264    let x5 = &x3 * &x2;
265    let x7 = &x5 * &x2;
266
267    let base = &(&x_minus_half * &ln_x) - &x_ext;
268    let term0 = &ln_2pi / &dbig_f64(2.0, guard);
269    let term1 = &dbig_f64(1.0, guard) / &(&dbig_f64(12.0, guard) * &x_ext);
270    let term2 = &dbig_f64(1.0, guard) / &(&dbig_f64(360.0, guard) * &x3);
271    let term3 = &dbig_f64(1.0, guard) / &(&dbig_f64(1260.0, guard) * &x5);
272    let term4 = &dbig_f64(1.0, guard) / &(&dbig_f64(1680.0, guard) * &x7);
273
274    let result =
275        &(&(&base + &term0) + &term1) - &(&(&term2.clone() - &term3.clone()) + &term4.clone());
276    Ok(truncate_to_precision(result, output_prec))
277}
278
279// ---------------------------------------------------------------------------
280// Digamma
281// ---------------------------------------------------------------------------
282
283/// Compute the digamma function ψ(x) = d/dx ln(Γ(x)) to `precision` significant
284/// decimal digits.
285///
286/// Uses recurrence ψ(x+1) = ψ(x) + 1/x to shift x > 8, then Bernoulli asymptotic
287/// series. For x < 1 uses the reflection formula ψ(1-x) - ψ(x) = π·cot(πx).
288///
289/// # Errors
290///
291/// Returns `OxiNumError::Domain` if x is zero or a negative integer.
292pub fn digamma(x: &DBig, precision: usize) -> OxiNumResult<DBig> {
293    validate_precision(precision)?;
294    let zero = make_dbig("0.0")?;
295    if *x == zero {
296        return Err(OxiNumError::Domain("digamma(0) is undefined (pole)".into()));
297    }
298    if is_negative(x) && is_integer_value(x) {
299        return Err(OxiNumError::Domain(
300            "digamma undefined at negative integers (poles)".into(),
301        ));
302    }
303
304    let guard = precision + 30;
305    digamma_impl(x, guard, precision)
306}
307
308fn digamma_impl(x: &DBig, guard: usize, output_prec: usize) -> OxiNumResult<DBig> {
309    let zero = make_dbig("0.0")?;
310    let one = dbig_f64(1.0, guard);
311
312    // For x < 1: reflection formula ψ(1-x) - ψ(x) = π·cot(πx)
313    if is_negative(x) || to_f64_approx(x) < 1.0 {
314        let pi = extend(crate::constants::compute_pi(guard), guard);
315        let x_ext = extend(x.clone(), guard);
316        let pi_x = &pi * &x_ext;
317        let sin_pix = sin(&pi_x, guard)?;
318        let cos_pix = cos(&pi_x, guard)?;
319        if is_approx_zero(&sin_pix) {
320            return Err(OxiNumError::Domain(
321                "digamma undefined at non-positive integers".into(),
322            ));
323        }
324        let cot_pi_x = &cos_pix / &sin_pix;
325        let pi_cot = &pi * &cot_pi_x;
326
327        let one_minus_x = &one - &x_ext;
328        let psi_1mx = digamma_impl(&one_minus_x, guard, guard)?;
329        let result = &psi_1mx - &pi_cot;
330        return Ok(truncate_to_precision(result, output_prec));
331    }
332
333    // For x >= 1: use recurrence ψ(x) = ψ(x+n) - Σ_{k=0}^{n-1} 1/(x+k)
334    // to shift x until x > 8, then asymptotic expansion.
335    let x_ext = extend(x.clone(), guard);
336    let mut shift_sum = extend(zero, guard);
337    let mut curr_x = x_ext.clone();
338
339    while to_f64_approx(&curr_x) < 8.0 {
340        shift_sum = &shift_sum + &(&one / &curr_x);
341        curr_x = &curr_x + &one;
342    }
343
344    // Asymptotic: ψ(x) ≈ ln(x) - 1/(2x) - B₂/(2x²) - B₄/(4x⁴) - B₆/(6x⁶) - B₈/(8x⁸)
345    // where B₂=1/6, B₄=-1/30, B₆=1/42, B₈=-1/30
346    let ln_cx = ln(&curr_x, guard)?;
347    let cx2 = &curr_x * &curr_x;
348    let cx4 = &cx2 * &cx2;
349    let cx6 = &cx4 * &cx2;
350    let cx8 = &cx4 * &cx4;
351
352    let t1 = &dbig_f64(1.0, guard) / &(&dbig_f64(2.0, guard) * &curr_x);
353    // B₂/2x² = (1/6)/(2x²) = 1/(12x²)
354    let t2 = &dbig_f64(1.0, guard) / &(&dbig_f64(12.0, guard) * &cx2);
355    // B₄/4x⁴ = (-1/30)/(4x⁴) = -1/(120x⁴)
356    let t3 = &dbig_f64(1.0, guard) / &(&dbig_f64(120.0, guard) * &cx4);
357    // B₆/6x⁶ = (1/42)/(6x⁶) = 1/(252x⁶)
358    let t4 = &dbig_f64(1.0, guard) / &(&dbig_f64(252.0, guard) * &cx6);
359    // B₈/8x⁸ = (-1/30)/(8x⁸) = -1/(240x⁸)
360    let t5 = &dbig_f64(1.0, guard) / &(&dbig_f64(240.0, guard) * &cx8);
361
362    // ψ(curr_x) ≈ ln(x) - 1/(2x) - 1/(12x²) + 1/(120x⁴) - 1/(252x⁶) + 1/(240x⁸)
363    let asymp = &(&(&ln_cx - &t1) - &t2) + &(&(&t3 - &t4) + &t5);
364
365    let result = &asymp - &shift_sum;
366    Ok(truncate_to_precision(result, output_prec))
367}
368
369// ---------------------------------------------------------------------------
370// Error function
371// ---------------------------------------------------------------------------
372
373/// Compute erf(x) to `precision` significant decimal digits.
374///
375/// Uses Taylor series for |x| ≤ 2; asymptotic expansion for |x| > 2.
376pub fn erf(x: &DBig, precision: usize) -> OxiNumResult<DBig> {
377    validate_precision(precision)?;
378    let zero = make_dbig("0.0")?;
379    if *x == zero {
380        return Ok(zero);
381    }
382
383    let guard = precision + 20;
384    let abs_x_f = to_f64_approx(x).abs();
385
386    if abs_x_f <= 2.0 {
387        erf_series(x, guard, precision)
388    } else {
389        // erf(x) = 1 - erfc(x) with appropriate sign
390        let abs_x = abs_dbig(x, guard);
391        let erfc_val = erfc_asymptotic(&abs_x, guard, guard)?;
392        let one = dbig_f64(1.0, guard);
393        let result = if is_positive(x) {
394            &one - &erfc_val
395        } else {
396            &erfc_val - &one
397        };
398        Ok(truncate_to_precision(result, precision))
399    }
400}
401
402/// Taylor series: erf(x) = (2/√π) Σ_{k=0}^∞ (-1)^k x^{2k+1} / (k! (2k+1))
403fn erf_series(x: &DBig, guard: usize, output_prec: usize) -> OxiNumResult<DBig> {
404    let pi = extend(crate::constants::compute_pi(guard), guard);
405    let sqrt_pi = sqrt(&pi, guard)?;
406    let two_over_sqrt_pi = &dbig_f64(2.0, guard) / &sqrt_pi;
407
408    let x_ext = extend(x.clone(), guard);
409    let x2 = &x_ext * &x_ext;
410    let neg_one = dbig_f64(-1.0, guard);
411
412    let mut sum = x_ext.clone();
413    let mut term = x_ext.clone();
414
415    for k in 1u32..=(guard as u32 * 3 + 50) {
416        // term_{k} = term_{k-1} * (-x²) / k
417        let neg_x2_over_k = &(&x2 * &neg_one) / &dbig_f64(k as f64, guard);
418        term = &term * &neg_x2_over_k;
419        // divide by (2k+1)
420        let contrib = &term / &dbig_f64(2.0 * k as f64 + 1.0, guard);
421        sum = &sum + &contrib;
422
423        if is_negligible(&contrib, output_prec) {
424            break;
425        }
426    }
427
428    let result = &two_over_sqrt_pi * &sum;
429    Ok(truncate_to_precision(result, output_prec))
430}
431
432/// Asymptotic expansion for erfc(x) for large x > 0:
433/// erfc(x) = (e^{-x²} / (x√π)) * Σ_{k=0}^∞ (-1)^k (2k-1)!! / (2x²)^k
434fn erfc_asymptotic(x: &DBig, guard: usize, output_prec: usize) -> OxiNumResult<DBig> {
435    let pi = extend(crate::constants::compute_pi(guard), guard);
436    let sqrt_pi = sqrt(&pi, guard)?;
437    let x_ext = extend(x.clone(), guard);
438    let x2 = &x_ext * &x_ext;
439    let neg_x2 = &x2 * &dbig_f64(-1.0, guard);
440    let exp_neg_x2 = exp(&neg_x2, guard)?;
441
442    let mut sum = dbig_f64(1.0, guard);
443    let mut term = dbig_f64(1.0, guard);
444
445    for k in 1u32..50 {
446        // term_{k} = term_{k-1} * (-(2k-1)) / (2x²)
447        let two_x2 = &dbig_f64(2.0, guard) * &x2;
448        let factor = &dbig_f64(-((2 * k - 1) as f64), guard) / &two_x2;
449        term = &term * &factor;
450        sum = &sum + &term;
451
452        if is_negligible(&term, output_prec) {
453            break;
454        }
455        // Asymptotic series is divergent - stop when term grows
456        if to_f64_approx(&abs_dbig(&term, guard)) > to_f64_approx(&abs_dbig(&sum, guard)) {
457            break;
458        }
459    }
460
461    // erfc(x) = exp(-x²) * sum / (x * sqrt(pi))
462    let denom = &x_ext * &sqrt_pi;
463    let result = &(&exp_neg_x2 * &sum) / &denom;
464    Ok(truncate_to_precision(result, output_prec))
465}
466
467// ---------------------------------------------------------------------------
468// Complementary error function
469// ---------------------------------------------------------------------------
470
471/// Compute erfc(x) = 1 - erf(x) to `precision` significant decimal digits.
472///
473/// Uses asymptotic continued fraction for large x (avoids cancellation).
474pub fn erfc(x: &DBig, precision: usize) -> OxiNumResult<DBig> {
475    validate_precision(precision)?;
476    let zero = make_dbig("0.0")?;
477    if *x == zero {
478        return Ok(dbig_f64(1.0, precision));
479    }
480
481    let guard = precision + 20;
482    let abs_x_f = to_f64_approx(x).abs();
483
484    if abs_x_f <= 2.0 {
485        let erf_val = erf_series(x, guard, guard)?;
486        let one = dbig_f64(1.0, guard);
487        let result = &one - &erf_val;
488        Ok(truncate_to_precision(result, precision))
489    } else {
490        let abs_x = abs_dbig(x, guard);
491        let erfc_val = erfc_asymptotic(&abs_x, guard, guard)?;
492        let result = if is_positive(x) {
493            truncate_to_precision(erfc_val, precision)
494        } else {
495            // erfc(-x) = 2 - erfc(x)
496            let two = dbig_f64(2.0, guard);
497            truncate_to_precision(&two - &erfc_val, precision)
498        };
499        Ok(result)
500    }
501}
502
503// ---------------------------------------------------------------------------
504// Bessel J₀
505// ---------------------------------------------------------------------------
506
507/// Compute the Bessel function J₀(x) to `precision` significant decimal digits.
508///
509/// Uses power series for |x| ≤ 12; asymptotic expansion for |x| > 12.
510///
511/// The power series is: J₀(x) = Σ_{k=0}^∞ (-1)^k (x/2)^{2k} / (k!)²
512pub fn bessel_j0(x: &DBig, precision: usize) -> OxiNumResult<DBig> {
513    validate_precision(precision)?;
514    let zero = make_dbig("0.0")?;
515    if *x == zero {
516        return Ok(dbig_f64(1.0, precision));
517    }
518
519    let guard = precision + 20;
520    let abs_x_f = to_f64_approx(x).abs();
521
522    if abs_x_f <= 12.0 {
523        bessel_j0_series(x, guard, precision)
524    } else {
525        bessel_j0_asymptotic(x, guard, precision)
526    }
527}
528
529/// Power series for J₀(x): Σ_{k=0}^∞ (-1)^k (x/2)^{2k} / (k!)²
530fn bessel_j0_series(x: &DBig, guard: usize, output_prec: usize) -> OxiNumResult<DBig> {
531    let x_ext = extend(x.clone(), guard);
532    // x/2
533    let x_half = &x_ext / &dbig_f64(2.0, guard);
534    // (x/2)²
535    let x_half_sq = &x_half * &x_half;
536
537    let mut sum = dbig_f64(1.0, guard);
538    let mut term = dbig_f64(1.0, guard);
539
540    for k in 1u32..=(guard as u32 * 2 + 60) {
541        // term_{k} = term_{k-1} * (-(x/2)²) / k²
542        let k_sq = dbig_f64((k as f64) * (k as f64), guard);
543        let neg_xh2 = &x_half_sq * &dbig_f64(-1.0, guard);
544        term = &term * &(&neg_xh2 / &k_sq);
545        sum = &sum + &term;
546
547        if is_negligible(&term, output_prec) {
548            break;
549        }
550    }
551
552    Ok(truncate_to_precision(sum, output_prec))
553}
554
555/// Asymptotic expansion for J₀(x) for large |x|:
556/// J₀(x) ≈ √(2/(πx)) [P₀(x)·cos(x - π/4) - Q₀(x)·sin(x - π/4)]
557/// where P₀ and Q₀ are asymptotic series truncated at the smallest term.
558fn bessel_j0_asymptotic(x: &DBig, guard: usize, output_prec: usize) -> OxiNumResult<DBig> {
559    let x_ext = extend(x.clone(), guard);
560    let pi = extend(crate::constants::compute_pi(guard), guard);
561
562    // sqrt(2/(pi*x))
563    let pi_x = &pi * &x_ext;
564    let two_over_pi_x = &dbig_f64(2.0, guard) / &pi_x;
565    let sqrt_prefactor = sqrt(&two_over_pi_x, guard)?;
566
567    // phase = x - pi/4
568    let pi_over_4 = &pi / &dbig_f64(4.0, guard);
569    let phase = &x_ext - &pi_over_4;
570
571    // P₀(x) = 1 - (1²·3²)/(2!(8x)²) + ... ≈ 1 - 9/(128x²) + ...
572    // Q₀(x) = -1/(8x) + (1·3·5)/(3!·(8x)³/...) ≈ -1/(8x) + ...
573    // Use only leading terms for asymptotic (valid for large x)
574    let x2 = &x_ext * &x_ext;
575    let x4 = &x2 * &x2;
576
577    // P0: 1 - 9/(128*x²) + 3675/(32768*x⁴)
578    let p0_t1 = &dbig_f64(9.0, guard) / &(&dbig_f64(128.0, guard) * &x2);
579    let p0_t2 = &dbig_f64(3675.0, guard) / &(&dbig_f64(32768.0, guard) * &x4);
580    let p0 = &(&dbig_f64(1.0, guard) - &p0_t1) + &p0_t2;
581
582    // Q0: -1/(8*x) + 75/(1024*x³)
583    let x3 = &x2 * &x_ext;
584    let q0_t1 = &dbig_f64(1.0, guard) / &(&dbig_f64(8.0, guard) * &x_ext);
585    let q0_t2 = &dbig_f64(75.0, guard) / &(&dbig_f64(1024.0, guard) * &x3);
586    let q0 = &q0_t2 - &q0_t1;
587
588    let cos_phase = cos(&phase, guard)?;
589    let sin_phase = sin(&phase, guard)?;
590
591    // J₀ ≈ sqrt_prefactor * (P0*cos_phase - Q0*sin_phase)
592    let result = &sqrt_prefactor * &(&(&p0 * &cos_phase) - &(&q0 * &sin_phase));
593    Ok(truncate_to_precision(result, output_prec))
594}
595
596// ---------------------------------------------------------------------------
597// Internal helpers
598// ---------------------------------------------------------------------------
599
600fn validate_precision(precision: usize) -> OxiNumResult<()> {
601    if precision == 0 {
602        return Err(OxiNumError::Precision("precision must be > 0".into()));
603    }
604    Ok(())
605}
606
607fn make_dbig(s: &str) -> OxiNumResult<DBig> {
608    DBig::from_str(s).map_err(|e| OxiNumError::Parse(format!("{e}").into()))
609}
610
611/// Extend a DBig to carry at least `precision` significant digits.
612fn extend(v: DBig, precision: usize) -> DBig {
613    v.with_precision(precision).value()
614}
615
616/// Create a DBig from f64 at a given precision.
617fn dbig_f64(v: f64, precision: usize) -> DBig {
618    let s = format!("{:.prec$e}", v, prec = precision + 5);
619    DBig::from_str(&s)
620        .unwrap_or_else(|_| DBig::from_str(&format!("{v}")).expect("f64 to DBig should not fail"))
621        .with_precision(precision)
622        .value()
623}
624
625/// Check if value is (approximately) zero.
626fn is_approx_zero(v: &DBig) -> bool {
627    let s = v.to_string().trim_start_matches('-').to_string();
628    s == "0" || s == "0.0" || {
629        // Check for very small values: 0.00000...
630        if let Some(dot) = s.find('.') {
631            let int_part = &s[..dot];
632            let frac = &s[dot + 1..];
633            let leading_zeros = frac.chars().take_while(|&c| c == '0').count();
634            int_part == "0" && leading_zeros >= 15
635        } else {
636            false
637        }
638    }
639}
640
641/// Check if term is negligible relative to `output_prec`.
642fn is_negligible(term: &DBig, precision: usize) -> bool {
643    let s = term.to_string();
644    let s = s.trim_start_matches('-');
645    if let Some(dot_pos) = s.find('.') {
646        let integer_part = &s[..dot_pos];
647        if integer_part == "0" {
648            let frac = &s[dot_pos + 1..];
649            let leading_zeros = frac.chars().take_while(|&c| c == '0').count();
650            return leading_zeros >= precision + 3;
651        }
652    }
653    s == "0" || s == "0.0"
654}
655
656/// Approximate f64 value from DBig for branching decisions.
657fn to_f64_approx(v: &DBig) -> f64 {
658    let s = v.to_string();
659    s.parse::<f64>().unwrap_or(0.0)
660}
661
662/// Check if DBig is positive (> 0).
663fn is_positive(v: &DBig) -> bool {
664    !v.to_string().starts_with('-') && !is_approx_zero(v)
665}
666
667/// Check if DBig is negative (< 0).
668fn is_negative(v: &DBig) -> bool {
669    v.to_string().starts_with('-') && !is_approx_zero(v)
670}
671
672/// Check if DBig represents an integer value (fractional part is zero).
673fn is_integer_value(v: &DBig) -> bool {
674    let s = v.to_string().trim_start_matches('-').to_string();
675    if let Some(dot) = s.find('.') {
676        let frac = &s[dot + 1..];
677        frac.chars().all(|c| c == '0')
678    } else {
679        // No dot means it IS an integer string
680        !s.is_empty()
681    }
682}
683
684/// Compute absolute value of DBig.
685fn abs_dbig(v: &DBig, precision: usize) -> DBig {
686    let s = v.to_string();
687    if let Some(stripped) = s.strip_prefix('-') {
688        DBig::from_str(stripped)
689            .unwrap_or_else(|_| v.clone())
690            .with_precision(precision)
691            .value()
692    } else {
693        v.clone().with_precision(precision).value()
694    }
695}
696
697/// Parse a constant string, truncated to `precision` significant digits.
698fn parse_at_precision(src: &str, precision: usize) -> DBig {
699    use crate::elementary::truncate_decimal_str;
700    let prec = precision.clamp(1, 200);
701    let truncated = truncate_decimal_str(src, prec);
702    DBig::from_str(&truncated).expect("pre-stored constant is a valid decimal literal")
703}
704
705// ---------------------------------------------------------------------------
706// Tests
707// ---------------------------------------------------------------------------
708
709#[cfg(test)]
710mod tests {
711    use super::*;
712
713    fn approx_eq(a: f64, b: f64, tol: f64) -> bool {
714        (a - b).abs() <= tol * b.abs().max(1e-300)
715    }
716
717    #[test]
718    fn euler_gamma_value() {
719        let g = euler_gamma(30);
720        let s = g.to_string();
721        assert!(
722            s.starts_with("0.577215664901532860606512"),
723            "euler_gamma = {s}"
724        );
725    }
726
727    #[test]
728    fn catalan_value() {
729        let g = catalan(30);
730        let s = g.to_string();
731        assert!(s.starts_with("0.915965594177219015054603"), "catalan = {s}");
732    }
733
734    #[test]
735    fn gamma_one() {
736        let x = DBig::from_str("1.0").expect("ok");
737        let g = gamma(&x, 30).expect("gamma(1) ok");
738        let v = to_f64_approx(&g);
739        assert!(approx_eq(v, 1.0, 1e-10), "gamma(1) = {v}");
740    }
741
742    #[test]
743    fn gamma_half_is_sqrt_pi() {
744        let x = DBig::from_str("0.5").expect("ok");
745        let g = gamma(&x, 30).expect("gamma(0.5) ok");
746        let v = to_f64_approx(&g);
747        let expected = std::f64::consts::PI.sqrt();
748        assert!(
749            approx_eq(v, expected, 1e-10),
750            "gamma(0.5) = {v}, expected {expected}"
751        );
752    }
753
754    #[test]
755    fn gamma_five_is_24() {
756        let x = DBig::from_str("5.0").expect("ok");
757        let g = gamma(&x, 30).expect("gamma(5) ok");
758        let v = to_f64_approx(&g);
759        assert!(approx_eq(v, 24.0, 1e-10), "gamma(5) = {v}");
760    }
761
762    #[test]
763    fn gamma_pole_at_zero() {
764        let x = DBig::from_str("0.0").expect("ok");
765        assert!(gamma(&x, 20).is_err());
766    }
767
768    #[test]
769    fn gamma_pole_at_neg_int() {
770        let x = DBig::from_str("-3.0").expect("ok");
771        assert!(gamma(&x, 20).is_err());
772    }
773
774    #[test]
775    fn ln_gamma_one_is_zero() {
776        let x = DBig::from_str("1.0").expect("ok");
777        let lg = ln_gamma(&x, 30).expect("ok");
778        let v = to_f64_approx(&lg).abs();
779        assert!(v < 1e-8, "ln_gamma(1) = {v}");
780    }
781
782    #[test]
783    fn ln_gamma_large() {
784        let x = DBig::from_str("100.0").expect("ok");
785        let lg = ln_gamma(&x, 30).expect("ok");
786        let v = to_f64_approx(&lg);
787        // ln(Γ(100)) = ln(99!) ≈ 359.134...
788        assert!((v - 359.134_f64).abs() < 0.001, "ln_gamma(100) = {v}");
789    }
790
791    #[test]
792    fn digamma_one_is_neg_euler() {
793        // ψ(1) = -γ
794        let x = DBig::from_str("1.0").expect("ok");
795        let d = digamma(&x, 30).expect("ok");
796        let v = to_f64_approx(&d);
797        let expected = -0.5772156649_f64;
798        assert!(
799            (v - expected).abs() < 1e-8,
800            "digamma(1) = {v}, expected {expected}"
801        );
802    }
803
804    #[test]
805    fn digamma_pole_at_zero() {
806        let x = DBig::from_str("0.0").expect("ok");
807        assert!(digamma(&x, 20).is_err());
808    }
809
810    #[test]
811    fn erf_zero() {
812        let x = DBig::from_str("0.0").expect("ok");
813        let v = erf(&x, 20).expect("ok");
814        let s = v.to_string();
815        let clean = s.trim_start_matches('-');
816        assert!(
817            clean == "0" || clean == "0.0" || clean.starts_with("0.000000"),
818            "erf(0) should be zero, got {s}"
819        );
820    }
821
822    #[test]
823    fn erf_one() {
824        let x = DBig::from_str("1.0").expect("ok");
825        let v = erf(&x, 20).expect("ok");
826        let f = to_f64_approx(&v);
827        // erf(1) ≈ 0.8427007929
828        assert!((f - 0.8427007929_f64).abs() < 1e-8, "erf(1) = {f}");
829    }
830
831    #[test]
832    fn erf_symmetry() {
833        let x = DBig::from_str("0.8").expect("ok");
834        let xn = DBig::from_str("-0.8").expect("ok");
835        let ep = erf(&x, 25).expect("ok");
836        let en = erf(&xn, 25).expect("ok");
837        let fp = to_f64_approx(&ep);
838        let fn_ = to_f64_approx(&en);
839        assert!((fp + fn_).abs() < 1e-10, "erf(x) + erf(-x) = {}", fp + fn_);
840    }
841
842    #[test]
843    fn erfc_zero() {
844        let x = DBig::from_str("0.0").expect("ok");
845        let v = erfc(&x, 20).expect("ok");
846        let f = to_f64_approx(&v);
847        assert!((f - 1.0).abs() < 1e-10, "erfc(0) = {f}");
848    }
849
850    #[test]
851    fn erf_plus_erfc_is_one() {
852        let x = DBig::from_str("1.5").expect("ok");
853        let e = erf(&x, 20).expect("ok");
854        let ec = erfc(&x, 20).expect("ok");
855        let sum = to_f64_approx(&e) + to_f64_approx(&ec);
856        assert!((sum - 1.0).abs() < 1e-10, "erf(1.5)+erfc(1.5) = {sum}");
857    }
858
859    #[test]
860    fn bessel_j0_zero() {
861        let x = DBig::from_str("0.0").expect("ok");
862        let v = bessel_j0(&x, 20).expect("ok");
863        let f = to_f64_approx(&v);
864        assert!((f - 1.0).abs() < 1e-10, "J0(0) = {f}");
865    }
866
867    #[test]
868    fn bessel_j0_first_zero_approx() {
869        // First zero of J₀ ≈ 2.4048255577
870        // J₀(2.4048) ≈ 0
871        let x = DBig::from_str("2.4048255577").expect("ok");
872        let v = bessel_j0(&x, 20).expect("ok");
873        let f = to_f64_approx(&v).abs();
874        assert!(f < 1e-5, "J0(2.4048) ≈ 0, got {f}");
875    }
876
877    #[test]
878    fn free_cache_is_noop() {
879        // Should not panic
880        free_cache();
881    }
882}