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}