Skip to main content

oxinum_float/native/
constants.rs

1//! Arbitrary-precision mathematical constants for native `BigFloat`.
2//!
3//! Provides [`pi`], [`e_const`], and [`ln2`] computed to arbitrary precision
4//! via integer-arithmetic algorithms (binary splitting / iterative atanh sums).
5//!
6//! # Algorithms
7//!
8//! ## π — Chudnovsky formula + binary splitting
9//!
10//! The Chudnovsky series gives ≈14.18 decimal digits per term:
11//!
12//! ```text
13//! 1/π = (12 / C^(3/2)) · Σ_{k=0}^∞ (−1)^k · (6k)! · (Ak+B) / ((3k)! · (k!)³ · C^(3k))
14//! ```
15//!
16//! where `A = 545140134`, `B = 13591409`, `C = 640320`.
17//!
18//! ## e — 1/k! binary splitting
19//!
20//! `e = Σ_{k=0}^∞ 1/k!`  with `p(k)=1`, `q(k)=k` (k>0), `a(k)=1`.
21//!
22//! ## ln 2 — Hwang Machin-like identity
23//!
24//! `ln 2 = 14·atanh(1/31) + 10·atanh(1/49) + 6·atanh(1/161)`
25//!
26//! Each `atanh(1/x)` is evaluated by iterative summation with an analytically
27//! determined term count.
28//!
29//! # Caching
30//!
31//! Results are cached in thread-safe `OnceLock<RwLock<Option<BigFloat>>>`.
32//! If the cached value has at least `prec + 16` bits the cache is reused;
33//! otherwise the constant is recomputed at `prec + 32` guard bits.
34
35use std::sync::{OnceLock, RwLock};
36
37use oxinum_core::{OxiNumError, OxiNumResult};
38use oxinum_int::native::BigInt;
39
40use super::binary_splitting::{binary_split, BSSeries, BSSplit};
41use super::float::{BigFloat, RoundingMode};
42
43// ---------------------------------------------------------------------------
44// Helper: convert a `BigInt` to a `BigFloat` at the requested precision.
45// ---------------------------------------------------------------------------
46
47/// Convert a `BigInt` to `BigFloat` at `prec` bits.
48///
49/// Uses `BigFloat::from_parts` which performs full normalization + rounding.
50fn bigfloat_from_bigint(n: &BigInt, prec: u32, mode: RoundingMode) -> BigFloat {
51    if n.is_zero() {
52        return BigFloat::zero(prec);
53    }
54    // from_parts with exponent=0 means the integer is interpreted as
55    //   (-1)^sign * magnitude * 2^0   = the integer itself (before normalisation).
56    // from_parts strips trailing-zero bits (migrates them into the exponent)
57    // and then rounds to prec bits.
58    BigFloat::from_parts(n.sign(), n.magnitude().clone(), 0, prec, mode)
59}
60
61// ---------------------------------------------------------------------------
62// π via Chudnovsky + binary splitting
63// ---------------------------------------------------------------------------
64
65/// Chudnovsky binary-splitting series.
66///
67/// Terms defined by:
68/// - `k=0`: `p = 1`, `q = 1`, `b = 1`, `a = B = 13591409`
69/// - `k>0`: `p = −(6k−5)(6k−4)(6k−3)(6k−2)(6k−1)(6k)`,
70///   `q = (3k)(3k−1)(3k−2) · k³ · C³` where `C = 640320`,
71///   `b = 1`, `a = A·k + B` where `A = 545140134`
72///
73/// After binary splitting over N terms, the result `t/(q·b)` equals
74/// `Σ_k c_k` where `1/π = 12 · Σ_k c_k / sqrt(C³)`.
75struct Chudnovsky;
76
77impl BSSeries for Chudnovsky {
78    fn term(&self, k: u64) -> (BigInt, BigInt, BigInt, BigInt) {
79        const A: i64 = 545_140_134;
80        const B: i64 = 13_591_409;
81        const C3: i64 = 640_320_i64 * 640_320 * 640_320; // 262_537_412_640_768_000
82
83        let a_val = BigInt::from(A * k as i64 + B);
84
85        if k == 0 {
86            return (BigInt::one(), BigInt::one(), BigInt::one(), a_val);
87        }
88
89        // p_k = −(6k−5)(6k−4)(6k−3)(6k−2)(6k−1)(6k)
90        // This is the signed numerator factor encoding the (6k)! ratio.
91        // Compute each factor as BigInt to avoid overflow for large k.
92        let k6 = BigInt::from((k * 6) as i64);
93        let p_val = {
94            let f1 = &k6 - BigInt::from(5i64);
95            let f2 = &k6 - BigInt::from(4i64);
96            let f3 = &k6 - BigInt::from(3i64);
97            let f4 = &k6 - BigInt::from(2i64);
98            let f5 = &k6 - BigInt::from(1i64);
99            let f6 = k6;
100            let prod = &f1 * &f2 * &f3 * &f4 * &f5 * &f6;
101            -prod
102        };
103
104        // q_k = (3k)(3k−1)(3k−2) · k³ · C³
105        // The (3k)(3k−1)(3k−2) factor encodes the (3k)! denominator ratio.
106        let k3 = BigInt::from((k * 3) as i64);
107        let kb = BigInt::from(k as i64);
108        let trinom = &k3 * (&k3 - BigInt::from(1i64)) * (&k3 - BigInt::from(2i64));
109        let q_val = &trinom * &kb * &kb * &kb * BigInt::from(C3);
110
111        let b_val = BigInt::one();
112
113        (p_val, q_val, b_val, a_val)
114    }
115}
116
117/// Compute the number of Chudnovsky terms needed for `prec` bits.
118///
119/// Each term contributes ≈ 14.181 decimal digits ≈ 47.11 bits.
120fn chudnovsky_terms(prec: u32) -> u64 {
121    // bits_per_term ≈ 14.181 * log2(10) ≈ 14.181 * 3.32193 ≈ 47.11
122    let n = ((prec as f64) / 47.11) as u64 + 8;
123    n.max(2)
124}
125
126/// Compute π using Chudnovsky binary splitting at `work_prec` bits.
127fn compute_pi_at(work_prec: u32) -> OxiNumResult<BigFloat> {
128    let mode = RoundingMode::HalfEven;
129    let n = chudnovsky_terms(work_prec);
130
131    let split: BSSplit = binary_split(&Chudnovsky, 0, n);
132
133    // After binary splitting:
134    //   split.t / (split.q * split.b)  =  Σ_k c_k
135    //   1/π = 12 · Σ_k c_k / sqrt(C³)
136    //   π   = sqrt(C³) · split.q · split.b / (12 · split.t)
137    //
138    // Note: split.t can be negative (Chudnovsky alternates).
139    // We compute |π| and track sign separately.
140
141    // C³ = 640320³ = 262_537_412_640_768_000  < i64::MAX (9.2e18). Use i64.
142    const C3: i64 = 640_320_i64 * 640_320 * 640_320;
143    let c3_float = BigFloat::from_i64(C3, work_prec, mode);
144    let sqrt_c3 = c3_float.sqrt(work_prec, mode)?;
145
146    // Denominator: 12 · split.t
147    let twelve = BigInt::from(12i64);
148    let denom_int: BigInt = &twelve * &split.t;
149
150    // Numerator integer: split.q · split.b
151    let numer_int: BigInt = &split.q * &split.b;
152
153    // Convert to BigFloat.
154    let numer_f = bigfloat_from_bigint(&numer_int, work_prec, mode);
155    let denom_f = bigfloat_from_bigint(&denom_int, work_prec, mode);
156
157    // Numerator in floating-point: sqrt_c3 * numer_f
158    let full_numer = &sqrt_c3 * &numer_f;
159
160    // π = full_numer / denom_f.
161    // Both numer_int = Q*B and denom_int = 12*T must be positive for Chudnovsky.
162    // A sign mismatch here would indicate a bug in the term encoding.
163    let pi_raw = full_numer.div_ref(&denom_f)?;
164
165    if pi_raw.sign() == oxinum_core::Sign::Negative {
166        return Err(OxiNumError::Precision(
167            "pi computation yielded negative result — Chudnovsky term encoding bug".into(),
168        ));
169    }
170
171    Ok(pi_raw)
172}
173
174// ---------------------------------------------------------------------------
175// e via 1/k! binary splitting
176// ---------------------------------------------------------------------------
177
178/// Series for `e = Σ_{k=0}^∞ 1/k!`.
179///
180/// Per-term factors:
181/// - `k=0`: `p = 1`, `q = 1`, `b = 1`, `a = 1`
182/// - `k>0`: `p = 1`, `q = k`, `b = 1`, `a = 1`
183///
184/// After binary splitting: `t / (q · b) = e − epsilon` for N large enough.
185struct ESeries;
186
187impl BSSeries for ESeries {
188    fn term(&self, k: u64) -> (BigInt, BigInt, BigInt, BigInt) {
189        let one = BigInt::one();
190        if k == 0 {
191            (one.clone(), one.clone(), one.clone(), one)
192        } else {
193            (one.clone(), BigInt::from(k as i64), one.clone(), one)
194        }
195    }
196}
197
198/// Number of terms for `e` at `work_prec` bits.
199///
200/// We need `k` large enough so `log2(k!) > work_prec + 8`.
201/// Uses the analytic estimate `log2(k!) ≈ k log2(k) - k/ln(2)`.
202fn e_terms(work_prec: u32) -> u64 {
203    let target = (work_prec as f64) + 8.0;
204    let mut k: u64 = 4;
205    let mut log2_kfact: f64 = (1..=4u64).map(|i| (i as f64).log2()).sum();
206    while log2_kfact < target {
207        k += 1;
208        log2_kfact += (k as f64).log2();
209    }
210    k + 1
211}
212
213/// Compute e using binary splitting at `work_prec` bits.
214fn compute_e_at(work_prec: u32) -> OxiNumResult<BigFloat> {
215    let mode = RoundingMode::HalfEven;
216    let n = e_terms(work_prec);
217
218    let split: BSSplit = binary_split(&ESeries, 0, n);
219
220    // sum = t / (q · b)
221    let denom_int: BigInt = &split.q * &split.b;
222    let numer_f = bigfloat_from_bigint(&split.t, work_prec, mode);
223    let denom_f = bigfloat_from_bigint(&denom_int, work_prec, mode);
224
225    numer_f.div_ref(&denom_f)
226}
227
228// ---------------------------------------------------------------------------
229// ln 2 via Machin-like atanh identity
230// ---------------------------------------------------------------------------
231
232/// Compute `atanh(1/x)` at `work_prec` bits using the series
233/// `atanh(1/x) = 1/x + 1/(3x³) + 1/(5x⁵) + …`
234///
235/// Based on Hwang's identity `ln 2 = 14·atanh(1/31) + 10·atanh(1/49) + 6·atanh(1/161)`.
236///
237/// Term k has magnitude `1/((2k+1)·x^(2k+1))`.
238/// We need enough terms so `(2k+1)·log2(x) > work_prec + 8`.
239fn atanh_inv_x(x: u64, work_prec: u32) -> OxiNumResult<BigFloat> {
240    let mode = RoundingMode::HalfEven;
241
242    // Number of terms: smallest k such that (2k+1)*log2(x) > work_prec + 8.
243    let log2_x = (x as f64).log2();
244    let k_needed = (((work_prec as f64) + 8.0) / log2_x / 2.0) as u64 + 4;
245
246    // We compute the sum iteratively using BigFloat arithmetic.
247    // atanh(1/x) = (1/x) * Σ_{k=0}^{N} (1/x²)^k / (2k+1)
248    //
249    // Iterate: term_0 = 1/x.  For each k>0: term_k = term_{k-1} / x².
250    // Then add term_k / (2k+1) to accumulator.
251
252    let x_f = BigFloat::from_i64(x as i64, work_prec, mode);
253    let x2_f = &x_f * &x_f;
254
255    // term = 1/x
256    let one_f = BigFloat::from_i64(1, work_prec, mode);
257    let mut term = one_f.div_ref_with_mode(&x_f, mode)?;
258
259    // acc = term / (2*0+1) = term / 1 = term
260    let mut acc = term.clone();
261
262    for k in 1..=k_needed {
263        // term /= x²
264        term = term.div_ref_with_mode(&x2_f, mode)?;
265        // scaled_term = term / (2k+1)
266        let denom_f = BigFloat::from_i64((2 * k + 1) as i64, work_prec, mode);
267        let scaled = term.div_ref_with_mode(&denom_f, mode)?;
268        acc = &acc + &scaled;
269    }
270
271    Ok(acc)
272}
273
274/// Compute ln 2 using Hwang's identity at `work_prec` bits.
275fn compute_ln2_at(work_prec: u32) -> OxiNumResult<BigFloat> {
276    let mode = RoundingMode::HalfEven;
277
278    // ln 2 = 14·atanh(1/31) + 10·atanh(1/49) + 6·atanh(1/161)
279    let a31 = atanh_inv_x(31, work_prec)?;
280    let a49 = atanh_inv_x(49, work_prec)?;
281    let a161 = atanh_inv_x(161, work_prec)?;
282
283    let c14 = BigFloat::from_i64(14, work_prec, mode);
284    let c10 = BigFloat::from_i64(10, work_prec, mode);
285    let c6 = BigFloat::from_i64(6, work_prec, mode);
286
287    let t1 = &c14 * &a31;
288    let t2 = &c10 * &a49;
289    let t3 = &c6 * &a161;
290
291    Ok(&(&t1 + &t2) + &t3)
292}
293
294// ---------------------------------------------------------------------------
295// Caching wrappers
296// ---------------------------------------------------------------------------
297
298/// Cache cell for a single constant.
299struct ConstCache {
300    inner: OnceLock<RwLock<Option<BigFloat>>>,
301}
302
303impl ConstCache {
304    const fn new() -> Self {
305        Self {
306            inner: OnceLock::new(),
307        }
308    }
309
310    fn lock(&self) -> &RwLock<Option<BigFloat>> {
311        self.inner.get_or_init(|| RwLock::new(None))
312    }
313
314    /// Return the constant at `prec` bits, using or populating the cache.
315    ///
316    /// `compute` is called with `work_prec = prec + 32` when the cache is
317    /// cold or the cached precision is insufficient.
318    fn get_or_compute<F>(&self, prec: u32, compute: F) -> OxiNumResult<BigFloat>
319    where
320        F: FnOnce(u32) -> OxiNumResult<BigFloat>,
321    {
322        const MODE: RoundingMode = RoundingMode::HalfEven;
323
324        // Fast path: read-lock check.
325        {
326            let guard = self
327                .lock()
328                .read()
329                .map_err(|_| OxiNumError::Precision("ConstCache RwLock poisoned".into()))?;
330            if let Some(ref cached) = *guard {
331                if cached.precision() >= prec + 16 {
332                    return Ok(cached.clone().with_precision(prec, MODE));
333                }
334            }
335        }
336
337        // Slow path: compute at higher precision and write.
338        let work_prec = prec + 32;
339        let result = compute(work_prec)?;
340
341        {
342            let mut guard = self
343                .lock()
344                .write()
345                .map_err(|_| OxiNumError::Precision("ConstCache RwLock poisoned".into()))?;
346            // Double-check: another thread may have updated the cache.
347            if let Some(ref cached) = *guard {
348                if cached.precision() >= prec + 16 {
349                    return Ok(cached.clone().with_precision(prec, MODE));
350                }
351            }
352            *guard = Some(result);
353        }
354
355        // Now re-read from the cache to avoid double-cloning.
356        {
357            let guard = self
358                .lock()
359                .read()
360                .map_err(|_| OxiNumError::Precision("ConstCache RwLock poisoned".into()))?;
361            if let Some(ref cached) = *guard {
362                return Ok(cached.clone().with_precision(prec, MODE));
363            }
364        }
365
366        Err(OxiNumError::Precision(
367            "ConstCache: unexpected empty after write".into(),
368        ))
369    }
370}
371
372static PI_CACHE: ConstCache = ConstCache::new();
373static E_CACHE: ConstCache = ConstCache::new();
374static LN2_CACHE: ConstCache = ConstCache::new();
375
376// ---------------------------------------------------------------------------
377// Public API
378// ---------------------------------------------------------------------------
379
380/// Return π at `prec` bits of precision.
381///
382/// # Errors
383///
384/// Propagates any arithmetic error from the internal Chudnovsky computation
385/// (in practice, only a sqrt-of-negative error which should never occur).
386///
387/// # Examples
388///
389/// ```
390/// use oxinum_float::native::{pi, BigFloat, RoundingMode};
391/// let p = pi(64).expect("pi");
392/// assert!((p.to_f64() - std::f64::consts::PI).abs() < 1e-14);
393/// ```
394pub fn pi(prec: u32) -> OxiNumResult<BigFloat> {
395    PI_CACHE.get_or_compute(prec, compute_pi_at)
396}
397
398/// Return e (Euler's number) at `prec` bits of precision.
399///
400/// # Errors
401///
402/// Propagates any arithmetic error from the internal 1/k! computation.
403///
404/// # Examples
405///
406/// ```
407/// use oxinum_float::native::e_const;
408/// let e = e_const(64).expect("e");
409/// assert!((e.to_f64() - std::f64::consts::E).abs() < 1e-14);
410/// ```
411pub fn e_const(prec: u32) -> OxiNumResult<BigFloat> {
412    E_CACHE.get_or_compute(prec, compute_e_at)
413}
414
415/// Return ln 2 at `prec` bits of precision.
416///
417/// # Errors
418///
419/// Propagates any arithmetic error from the internal atanh series.
420///
421/// # Examples
422///
423/// ```
424/// use oxinum_float::native::ln2;
425/// let l = ln2(64).expect("ln2");
426/// assert!((l.to_f64() - std::f64::consts::LN_2).abs() < 1e-14);
427/// ```
428pub fn ln2(prec: u32) -> OxiNumResult<BigFloat> {
429    LN2_CACHE.get_or_compute(prec, compute_ln2_at)
430}
431
432// ---------------------------------------------------------------------------
433// Internal unit tests
434// ---------------------------------------------------------------------------
435
436#[cfg(test)]
437mod tests {
438    use super::*;
439
440    /// Verify that with 1 Chudnovsky term, the raw sum equals T/Q ≈ 1/(π·12/sqrt(C³))
441    /// ≈ 0.02654 (1/π ≈ 0.3183, times 12/sqrt(C³) ≈ 1.2/5.12e8 = 2.34e-9 — wait,
442    /// recalculate: 12/sqrt(C³) ≈ 12/(5.124e8) ≈ 2.34e-8, so sum term_0 ≈ 1/(π·12/sqrt) —
443    /// just verify the sign structure instead.)
444    #[test]
445    fn chudnovsky_term_zero() {
446        let (p, q, b, a) = Chudnovsky.term(0);
447        assert_eq!(p, BigInt::one());
448        assert_eq!(q, BigInt::one());
449        assert_eq!(b, BigInt::one());
450        assert_eq!(a, BigInt::from(13_591_409i64));
451    }
452
453    #[test]
454    fn chudnovsky_term_one_negative_p() {
455        let (p, _q, _b, _a) = Chudnovsky.term(1);
456        // p(1) = -(6*1-5)(6*1-4)(6*1-3)(6*1-2)(6*1-1)(6*1)
457        //       = -(1)(2)(3)(4)(5)(6) = -720
458        assert_eq!(p, BigInt::from(-720i64));
459    }
460
461    #[test]
462    fn e_series_term() {
463        let (p, q, b, a) = ESeries.term(0);
464        assert_eq!(p, BigInt::one());
465        assert_eq!(q, BigInt::one());
466        assert_eq!(b, BigInt::one());
467        assert_eq!(a, BigInt::one());
468
469        let (p2, q2, _b2, a2) = ESeries.term(5);
470        assert_eq!(p2, BigInt::one());
471        assert_eq!(q2, BigInt::from(5i64));
472        assert_eq!(a2, BigInt::one());
473    }
474
475    #[test]
476    fn pi_f64_matches() {
477        let p = pi(64).expect("pi(64)");
478        assert!((p.to_f64() - std::f64::consts::PI).abs() < 1e-14);
479    }
480
481    #[test]
482    fn e_f64_matches() {
483        let e = e_const(64).expect("e_const(64)");
484        assert!((e.to_f64() - std::f64::consts::E).abs() < 1e-14);
485    }
486
487    #[test]
488    fn ln2_f64_matches() {
489        let l = ln2(64).expect("ln2(64)");
490        assert!((l.to_f64() - std::f64::consts::LN_2).abs() < 1e-14);
491    }
492}