dashu_float/math/cache.rs
1//! Opt-in cache of mathematical constants, enabling progressive refinement.
2
3use core::fmt;
4
5use dashu_base::{BitTest, EstimatedLog2, Sign, UnsignedAbs};
6use dashu_int::{IBig, UBig};
7
8use crate::error::assert_limited_precision;
9use crate::fbig::FBig;
10use crate::repr::{Context, Repr, Word};
11use crate::round::{Round, Rounded};
12use crate::utils::ceil_usize;
13
14/// Binary-splitting tree state — exact integers, losslessly extensible.
15///
16/// Represents `binary_split(start, num_terms)` as the universal triple
17/// `(P, Q, T)`, where `start` is 0 for π and 1 for `L(n)` (whose `k=0` term
18/// `1/n` is pulled out). These are pure integers: independent of base and
19/// rounding mode. To extend to `new_terms > num_terms`, compute the right half
20/// over the new range and merge with the universal `T' = T_l·Q_r + P_l·T_r`.
21#[derive(Clone)]
22pub(crate) struct CachedState {
23 pub p: UBig,
24 pub q: UBig,
25 pub t: IBig,
26 pub num_terms: usize,
27}
28
29/// An opt-in cache of mathematical constants.
30///
31/// Holds exact binary-splitting tree state so that repeated calls at increasing
32/// precision *extend* prior work instead of recomputing from scratch. For
33/// example, computing π at 100 digits and then at 1000 digits only pays for the
34/// extra ~900 digits of work.
35///
36/// The cache is **base-free**: a single [`ConstCache`] serves any base. The base
37/// and rounding mode are specified on each method call (e.g.
38/// `cache.pi::<10, HalfAway>(100)` for 100 decimal digits).
39///
40/// `ConstCache` is a plain struct of big integers, so it is `Send + Sync`. The
41/// methods take `&mut self` (they extend the cached state on a miss), so a caller
42/// either owns one directly, or — to share it across many values and operations —
43/// wraps it in `Rc<RefCell<ConstCache>>` as the
44/// [`CachedFBig`](crate::CachedFBig) type does. To share one cache across
45/// threads, wrap a `ConstCache` (or a `CachedFBig`) in `Arc<Mutex<..>>`.
46///
47/// # Examples
48///
49/// ```
50/// use dashu_float::ConstCache;
51/// use dashu_float::round::mode::HalfAway;
52///
53/// let mut cache = ConstCache::new();
54/// // first call computes from scratch
55/// let _pi_100 = cache.pi::<10, HalfAway>(100).value();
56/// // second call at higher precision extends the cached state
57/// let pi_1000 = cache.pi::<10, HalfAway>(1000).value();
58/// assert!(pi_1000.to_string().starts_with("3.141592653589793"));
59/// ```
60pub struct ConstCache {
61 pi: Option<CachedState>,
62 /// `L(6)`, `L(9)`, `L(99)` — the sub-series used by ln2 / ln10.
63 iacoth_6: Option<CachedState>,
64 iacoth_9: Option<CachedState>,
65 iacoth_99: Option<CachedState>,
66 /// Base-free integer `floor(sqrt(10005) · 2^sqrt_10005_bits)`, reused by π.
67 /// Unlike the series slots this holds a plain value (not a `(P,Q,T)` triple) and
68 /// is extended by a fresh Karatsuba `UBig::sqrt` — Newton refinement would be no
69 /// faster, since `UBig::sqrt` is already O(M(n)).
70 sqrt_10005: Option<UBig>,
71 sqrt_10005_bits: usize,
72}
73
74impl ConstCache {
75 /// Create an empty cache.
76 pub const fn new() -> Self {
77 Self {
78 pi: None,
79 iacoth_6: None,
80 iacoth_9: None,
81 iacoth_99: None,
82 sqrt_10005: None,
83 sqrt_10005_bits: 0,
84 }
85 }
86
87 /// `floor(sqrt(10005) · 2^bits)` as a base-free integer, cached and extended on
88 /// demand. Used by [`pi`](Self::pi). Computed via Karatsuba `UBig::sqrt` (O(M(n))).
89 /// Returns the value together with the number of bits it actually corresponds to
90 /// (which may be larger than requested, when a higher-precision value is reused).
91 fn sqrt_10005(&mut self, bits: usize) -> (UBig, usize) {
92 if bits > self.sqrt_10005_bits {
93 let n = UBig::from(10005u32) << (2 * bits);
94 self.sqrt_10005 = Some(dashu_base::SquareRoot::sqrt(&n));
95 self.sqrt_10005_bits = bits;
96 }
97 (self.sqrt_10005.as_ref().unwrap().clone(), self.sqrt_10005_bits)
98 }
99
100 /// π at `precision` base-`B` digits, rounded per `R`. Extends any prior π
101 /// state cached in `self`.
102 ///
103 /// # Panics
104 ///
105 /// Panics if `precision` is 0.
106 #[must_use]
107 pub fn pi<const B: Word, R: Round>(&mut self, precision: usize) -> Rounded<FBig<R, B>> {
108 assert_limited_precision(precision);
109
110 let bits = bits_for_precision::<B>(precision);
111 let num_terms = (bits * 100 / 4708) + 1;
112
113 let (_p, q, t) = extend_or_compute(&mut self.pi, 0, num_terms, chudnovsky_bs);
114
115 // Finalize: π = 426880·√10005·Q / T (identical to Context::pi)
116 let guard_bits = num_terms.bit_len() + 32;
117 let work_bits = bits + guard_bits;
118 let work_precision = precision_for_bits::<B>(work_bits);
119 let work = Context::<R>::new(work_precision);
120
121 // Finalize: π = 426880·√10005·Q / T. With √10005 ≈ isqrt_val·2^(-isqrt_bits)
122 // from the base-free cached isqrt, this folds into a single integer ratio
123 // π = (426880 · isqrt_val · Q) / (T · 2^isqrt_bits),
124 // avoiding any cross-base conversion of √10005 (convert_int is the fast path,
125 // the same one used for Q and T).
126 let (isqrt_val, isqrt_bits) = self.sqrt_10005(work_bits);
127 let num = IBig::from(426_880) * IBig::from(isqrt_val) * IBig::from(q);
128 let den = t << isqrt_bits;
129 let num_f = work.convert_int::<B>(num).value();
130 let den_f = work.convert_int::<B>(den).value();
131 let pi = num_f / den_f;
132 pi.with_precision(precision)
133 }
134
135 /// `L(n) = acoth(n)` at `precision` base-`B` digits, extending its cached
136 /// series state. Only `n ∈ {6, 9, 99}` are cached (the sub-series of ln2 / ln10).
137 fn iacoth<const N: u32, const B: Word, R: Round>(&mut self, precision: usize) -> FBig<R, B> {
138 // terms until r_k < B^{-p}: (2k+1)·log_B(n) > p. The count is generously
139 // over-provisioned (extra terms only add precision), so a plain (truncating)
140 // cast suffices in place of a ceiling.
141 let log_b_n = N.log2_est() / B.log2_est();
142 let required_terms = (precision as f32 / (2.0 * log_b_n)) as usize + 10;
143
144 let slot = match N {
145 6 => &mut self.iacoth_6,
146 9 => &mut self.iacoth_9,
147 99 => &mut self.iacoth_99,
148 _ => unreachable!("iacoth only caches n ∈ {{6, 9, 99}}"),
149 };
150 let (_p, q, t) = extend_or_compute(slot, 1, required_terms, |a, b| iacoth_bs(N, a, b));
151
152 // L(n) = (Q + T) / (n·Q)
153 let guard = ceil_usize(precision.log2_est() / B.log2_est()) + 2;
154 let work = Context::<R>::new(precision + guard);
155 let num = work.convert_int::<B>(q.as_ibig() + &t).value();
156 let denom = work.convert_int::<B>(IBig::from(N) * &q).value();
157 num / denom
158 }
159
160 /// ln(2) at `precision` base-`B` digits, reusing the cached `L(6)` and
161 /// `L(99)` sub-series.
162 ///
163 /// # Panics
164 ///
165 /// Panics if `precision` is 0.
166 #[must_use]
167 pub fn ln2<const B: Word, R: Round>(&mut self, precision: usize) -> FBig<R, B> {
168 // log(2) = 4·L(6) + 2·L(99) (Gourdon & Sebah, "Log 2")
169 let work = precision + combine_guard::<B>(precision);
170 let l6 = self.iacoth::<6, B, R>(work);
171 let l99 = self.iacoth::<99, B, R>(work);
172 (4u8 * l6 + 2u8 * l99).with_precision(precision).value()
173 }
174
175 /// ln(10) at `precision` base-`B` digits, reusing the cached `L(6)`, `L(99)`,
176 /// and `L(9)` sub-series.
177 ///
178 /// # Panics
179 ///
180 /// Panics if `precision` is 0.
181 #[must_use]
182 pub fn ln10<const B: Word, R: Round>(&mut self, precision: usize) -> FBig<R, B> {
183 // log(10) = 3·log(2) + 2·L(9) = 3·(4·L(6) + 2·L(99)) + 2·L(9)
184 // = 12·L(6) + 6·L(99) + 2·L(9)
185 // Flattening avoids the intermediate rounding of ln2 inside the product.
186 let work = precision + combine_guard::<B>(precision);
187 let l6 = self.iacoth::<6, B, R>(work);
188 let l99 = self.iacoth::<99, B, R>(work);
189 let l9 = self.iacoth::<9, B, R>(work);
190 (12u8 * l6 + 6u8 * l99 + 2u8 * l9)
191 .with_precision(precision)
192 .value()
193 }
194
195 /// ln(B) at `precision` base-`B` digits, reusing the cached ln2 / ln10 where
196 /// possible.
197 ///
198 /// # Panics
199 ///
200 /// Panics if `precision` is 0.
201 #[must_use]
202 pub fn ln_base<const B: Word, R: Round>(&mut self, precision: usize) -> FBig<R, B> {
203 match B {
204 2 => self.ln2::<B, R>(precision),
205 10 => self.ln10::<B, R>(precision),
206 b if b.is_power_of_two() => {
207 // ln(2^k) = k·ln(2); evaluate ln2 at elevated precision so the
208 // k·ln2 product survives the final round.
209 let work = precision + combine_guard::<B>(precision);
210 let bits = b.trailing_zeros() as usize;
211 (bits * self.ln2::<B, R>(work))
212 .with_precision(precision)
213 .value()
214 }
215 _ => {
216 // generic base: no cached L(n) sub-series applies, so compute
217 // ln(B) directly through Context::ln on the base literal.
218 let ctx = Context::<R>::new(precision);
219 ctx.unwrap_fp(ctx.ln::<B>(
220 &Repr::new(Repr::<B>::BASE.into(), 0),
221 // no cache for the generic base (its L(n) isn't cached)
222 None,
223 ))
224 }
225 }
226 }
227
228 /// Sum of `num_terms` across all populated cache slots.
229 #[inline]
230 pub fn total_terms(&self) -> usize {
231 let sum = |s: &Option<CachedState>| s.as_ref().map_or(0, |s| s.num_terms);
232 sum(&self.pi) + sum(&self.iacoth_6) + sum(&self.iacoth_9) + sum(&self.iacoth_99)
233 }
234
235 /// Sum of word counts across all cached big integers (P, Q, T, and the cached
236 /// `√10005` isqrt).
237 ///
238 /// This reflects the underlying storage words used by the cached state.
239 #[inline]
240 pub fn total_words(&self) -> usize {
241 let slot_words = |s: &Option<CachedState>| {
242 s.as_ref().map_or(0, |s| {
243 s.p.as_words().len() + s.q.as_words().len() + s.t.as_sign_words().1.len()
244 })
245 };
246 slot_words(&self.pi)
247 + slot_words(&self.iacoth_6)
248 + slot_words(&self.iacoth_9)
249 + slot_words(&self.iacoth_99)
250 + self.sqrt_10005.as_ref().map_or(0, |s| s.as_words().len())
251 }
252
253 /// Clear all cached constant state, freeing the underlying memory.
254 ///
255 /// After calling `clear()`, the next constant computation will start from scratch
256 /// rather than extending the prior cached state.
257 #[inline]
258 pub fn clear(&mut self) {
259 self.pi = None;
260 self.iacoth_6 = None;
261 self.iacoth_9 = None;
262 self.iacoth_99 = None;
263 self.sqrt_10005 = None;
264 self.sqrt_10005_bits = 0;
265 }
266}
267
268impl Default for ConstCache {
269 #[inline]
270 fn default() -> Self {
271 Self::new()
272 }
273}
274
275/// Ensure `slot` holds state for at least `target` terms, then return `(P, Q, T)`
276/// covering `target` terms (or more, when an existing higher-precision state
277/// already covers `target` — finalize then rounds down to the requested precision).
278///
279/// `range_bs(a, b)` computes the leaf-merged state over `[a, b)` and must handle
280/// `a == b` by returning the identity `(1, 1, 0)`.
281fn extend_or_compute<F>(
282 slot: &mut Option<CachedState>,
283 start: usize,
284 target: usize,
285 range_bs: F,
286) -> (UBig, UBig, IBig)
287where
288 F: Fn(usize, usize) -> (UBig, UBig, IBig),
289{
290 match slot {
291 // Already have >= target terms: reuse (extra terms only add precision).
292 Some(s) if s.num_terms >= target => (s.p.clone(), s.q.clone(), s.t.clone()),
293 // Have fewer terms: extend the right half [num_terms, target) and merge.
294 Some(s) => {
295 let (pr, qr, tr) = range_bs(s.num_terms, target);
296 let (p, q, t) = merge(&s.p, &s.q, &s.t, &pr, &qr, &tr);
297 *slot = Some(CachedState {
298 p: p.clone(),
299 q: q.clone(),
300 t: t.clone(),
301 num_terms: target,
302 });
303 (p, q, t)
304 }
305 // Cold: compute from `start`.
306 None => {
307 let (p, q, t) = range_bs(start, target);
308 *slot = Some(CachedState {
309 p: p.clone(),
310 q: q.clone(),
311 t: t.clone(),
312 num_terms: target,
313 });
314 (p, q, t)
315 }
316 }
317}
318
319/// Reborrow an `Option<&mut ConstCache>` so it can be threaded into several
320/// sequential sub-calls. `as_deref_mut` is the natural reborrow here; clippy's
321/// `needless_option_as_deref` flags it (the deref target equals the referent),
322/// so the lint is allowed at this single centralized spot.
323#[inline]
324#[allow(clippy::needless_option_as_deref)]
325pub(crate) fn reborrow_cache<'a>(
326 cache: &'a mut Option<&mut ConstCache>,
327) -> Option<&'a mut ConstCache> {
328 cache.as_deref_mut()
329}
330
331/// Number of bits needed to represent `precision` base-`B` digits exactly.
332///
333/// For power-of-two bases this is exact; for arbitrary bases it uses the upper
334/// bound from [`EstimatedLog2`], which is far tighter than `ilog2(B) + 1`.
335fn bits_for_precision<const B: Word>(precision: usize) -> usize {
336 if B.is_power_of_two() {
337 precision.saturating_mul(B.ilog2() as usize)
338 } else {
339 // ub ≥ log2(B) with error ≤ 2/256. Multiply in f64 so the product
340 // is exact for precision up to 2^53. +1 guards float rounding.
341 let ub = B.log2_bounds().1;
342 ceil_usize(precision as f32 * ub) + 1
343 }
344}
345
346/// Convert a work-precision expressed in bits back to base-`B` digits.
347///
348/// For base 2 the identity holds; for power-of-two bases it uses ceiling
349/// division; for arbitrary bases it inverts the lower bound from
350/// [`EstimatedLog2`] to get a tight ceiling.
351fn precision_for_bits<const B: Word>(bits: usize) -> usize {
352 if B.is_power_of_two() {
353 let log2 = B.ilog2() as usize;
354 (bits + log2 - 1) / log2
355 } else {
356 // lb ≤ log2(B), so 1/lb ≥ 1/log2(B). +1 guards float rounding.
357 let lb = B.log2_bounds().0;
358 ceil_usize(bits as f32 / lb) + 1
359 }
360}
361
362/// Guard digits added when combining sub-series, large enough that the linear
363/// combination and its final round to `precision` are unaffected by summation
364/// rounding (a few digits cover the constant multipliers and term count).
365fn combine_guard<const B: Word>(precision: usize) -> usize {
366 ceil_usize(precision.log2_est() / B.log2_est()) + 4
367}
368
369/// Universal binary-splitting merge:
370/// `combine((P_l,Q_l,T_l), (P_r,Q_r,T_r)) = (P_l·P_r, Q_l·Q_r, T_l·Q_r + P_l·T_r)`.
371///
372/// This operation is associative, so the `(P, Q, T)` for a range is independent of
373/// how the recursion splits it — which is exactly what lets a cached partial tree
374/// state be extended by merging in a freshly computed right half.
375pub(crate) fn merge(
376 pl: &UBig,
377 ql: &UBig,
378 tl: &IBig,
379 pr: &UBig,
380 qr: &UBig,
381 tr: &IBig,
382) -> (UBig, UBig, IBig) {
383 let p = pl * pr;
384 let q = ql * qr;
385 // re-interpret the magnitudes as signed without cloning the big integers
386 let t = qr.as_ibig() * tl + pl.as_ibig() * tr;
387 (p, q, t)
388}
389
390/// Binary splitting implementation for the Chudnovsky series.
391/// Returns `(P, Q, T)` for the range `[a, b)`. An empty range `[a, a)` yields the
392/// identity `(1, 1, 0)`, so callers may safely merge a cached left state with a
393/// right half that starts exactly where the left one ended.
394pub(crate) fn chudnovsky_bs(a: usize, b: usize) -> (UBig, UBig, IBig) {
395 if a >= b {
396 return (UBig::ONE, UBig::ONE, IBig::ZERO);
397 }
398 if b - a == 1 {
399 const COEFF1: IBig = IBig::from_parts_const(Sign::Positive, 13591409);
400 const COEFF2: IBig = IBig::from_parts_const(Sign::Positive, 545140134);
401
402 // Base case: calculate single term
403 if a == 0 {
404 return (UBig::ONE, UBig::ONE, COEFF1);
405 }
406
407 let k = a as u64;
408 let p = UBig::from(6 * k - 5) * (2 * k - 1) * (6 * k - 1);
409 let q = UBig::from(k).pow(3) * 10_939_058_860_032_000u64;
410 let t_val = COEFF1 + COEFF2 * k;
411 let t_abs = &p * t_val.unsigned_abs();
412 let t = IBig::from(t_abs) * Sign::from(a % 2 == 1);
413 return (p, q, t);
414 }
415
416 // Recursive step
417 let mid = (a + b) / 2;
418 let (p_l, q_l, t_l) = chudnovsky_bs(a, mid);
419 let (p_r, q_r, t_r) = chudnovsky_bs(mid, b);
420
421 // T = T_L * Q_R + T_R * P_L (the universal merge)
422 merge(&p_l, &q_l, &t_l, &p_r, &q_r, &t_r)
423}
424
425/// Binary splitting for `L(n) = acoth(n) = Σ_{k≥0} 1/(n^{2k+1}(2k+1))` over `[1, b)`.
426///
427/// Term ratio (k≥1): `r_k/r_{k-1} = p_k/q_k` with `p_k = 2k-1`, `q_k = (2k+1)·n²`.
428/// The `k=0` term `r_0 = 1/n` is pulled out; over `[1, b)` the tree state satisfies
429/// `T/Q = n · Σ_{k=1}^{b-1} 1/((2k+1)·n^{2k+1})`, hence `L(n) = (Q + T)/(n·Q)`.
430///
431/// Using the ratio form (rather than independent `1/q_k` terms) keeps
432/// `Q = Π(2k+1)·n²` at O(p) digits: each leaf multiplies only small integers
433/// (with `n²` folded in), no `n.pow(2k+1)` per leaf.
434pub(crate) fn iacoth_bs(n: u32, a: usize, b: usize) -> (UBig, UBig, IBig) {
435 debug_assert!(a >= 1, "iacoth_bs leaf index must be >= 1");
436 if a >= b {
437 return (UBig::ONE, UBig::ONE, IBig::ZERO); // identity
438 }
439 // Precomputed initial block [1, 1+K): skip its K leaves on every fresh
440 // computation. Because the merge is associative, the constant triple is
441 // identical to the recursively computed state regardless of split order.
442 // It only applies at the series start (a == 1); recursive/extend calls have
443 // a >= 1 + K and never reach this branch.
444 if a == 1 {
445 if let Some((k, p0, q0, t0)) = iacoth_initial_block(n) {
446 // the precomputed block covers [1, 1+k); use it when [a, b) reaches
447 // past its end (b > k)
448 if b > k {
449 let (pr, qr, tr) = iacoth_bs(n, 1 + k, b);
450 return merge(&p0, &q0, &t0, &pr, &qr, &tr);
451 }
452 }
453 }
454 if b - a == 1 {
455 // leaf at k = a (a >= 1): (p_a, q_a, p_a), p_a = 2a-1, q_a = (2a+1)·n²
456 let pa = UBig::from(2 * a - 1);
457 let n2 = UBig::from(n).pow(2);
458 let qa = UBig::from(2 * a + 1) * n2;
459 let ta = IBig::from(pa.clone());
460 return (pa, qa, ta);
461 }
462 let mid = (a + b) / 2;
463 let (pl, ql, tl) = iacoth_bs(n, a, mid);
464 let (pr, qr, tr) = iacoth_bs(n, mid, b);
465 merge(&pl, &ql, &tl, &pr, &qr, &tr) // universal merge
466}
467
468/// Precomputed binary-splitting state for `L(n) = acoth(n)` over the first `K`
469/// terms `[1, 1+K)`, stored as `(K, P, Q, T)`. `K` is chosen (per `n`) so that
470/// `P`, `Q` and `|T|` each fit in a `u32`. Since `DoubleWord` is `u32`/`u64`/`u128`
471/// for `Word = u16`/`u32`/`u64`, a `u32`-sized literal is accepted by
472/// [`UBig::from_dword`] / [`IBig::from_parts_const`] on **every** configuration —
473/// so this single set of constants is portable without needing to detect the
474/// `Word` width (which is internal to `dashu-int`). The constants also use the
475/// inline small-integer representation, so instantiating them never allocates.
476///
477/// Only the sub-series that back ln2 / ln10 (`n ∈ {6, 9, 99}`) are precomputed;
478/// π cannot use this trick because its 2-term `T` already overflows a `u32`.
479fn iacoth_initial_block(n: u32) -> Option<(usize, UBig, UBig, IBig)> {
480 match n {
481 // L(6) over [1, 5): 4 leaves.
482 6 => Some(IACOTH_6_INITIAL),
483 // L(9) over [1, 4): 3 leaves.
484 9 => Some(IACOTH_9_INITIAL),
485 // L(99) over [1, 3): 2 leaves.
486 99 => Some(IACOTH_99_INITIAL),
487 _ => None,
488 }
489}
490
491/// `(K, P, Q, T)` for `L(6)` over `[1, 5)` (4 leaves).
492const IACOTH_6_INITIAL: (usize, UBig, UBig, IBig) = (
493 4,
494 UBig::from_word(105),
495 UBig::from_dword(1587237120),
496 IBig::from_parts_const(Sign::Positive, 14946549),
497);
498/// `(K, P, Q, T)` for `L(9)` over `[1, 4)` (3 leaves).
499const IACOTH_9_INITIAL: (usize, UBig, UBig, IBig) = (
500 3,
501 UBig::from_word(15),
502 UBig::from_dword(55801305),
503 IBig::from_parts_const(Sign::Positive, 231351),
504);
505/// `(K, P, Q, T)` for `L(99)` over `[1, 3)` (2 leaves).
506const IACOTH_99_INITIAL: (usize, UBig, UBig, IBig) = (
507 2,
508 UBig::from_word(3),
509 UBig::from_dword(1440894015),
510 IBig::from_parts_const(Sign::Positive, 49008),
511);
512
513impl fmt::Debug for ConstCache {
514 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
515 f.debug_struct("ConstCache")
516 .field("pi", &DebugSlot(&self.pi))
517 .field("iacoth_6", &DebugSlot(&self.iacoth_6))
518 .field("iacoth_9", &DebugSlot(&self.iacoth_9))
519 .field("iacoth_99", &DebugSlot(&self.iacoth_99))
520 .finish()
521 }
522}
523
524/// Newtype so we can implement `Debug` for `&Option<CachedState>` via the
525/// big-integer `Debug` formatters.
526struct DebugSlot<'a>(&'a Option<CachedState>);
527
528impl fmt::Debug for DebugSlot<'_> {
529 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
530 match self.0 {
531 Some(s) => f
532 .debug_struct("CachedState")
533 .field("num_terms", &s.num_terms)
534 .field("p", &s.p)
535 .field("q", &s.q)
536 .field("t", &s.t)
537 .finish(),
538 None => f.write_str("None"),
539 }
540 }
541}
542
543#[cfg(test)]
544mod tests {
545 use super::*;
546 use crate::round::mode;
547 use alloc::format;
548
549 /// Independently (left-fold) merge the first `k` leaves of `L(n)` and check
550 /// that the result matches the precomputed constant triple. Guards against
551 /// transcription errors in the `IACOTH_*_INITIAL` literals; correctness of the
552 /// finalized `L(n)` values is covered by `log::tests`.
553 #[test]
554 fn test_iacoth_initial_blocks() {
555 fn check(n: u32, expected: &(usize, UBig, UBig, IBig)) {
556 let k = expected.0;
557 // independently (left-fold) merge the first k leaves
558 let mut acc = (UBig::ONE, UBig::ONE, IBig::ZERO);
559 for kk in 1..=k {
560 let pa = UBig::from(2 * kk as u64 - 1);
561 let qa = UBig::from(2 * kk as u64 + 1) * UBig::from(n).pow(2);
562 let ta = IBig::from(pa.clone());
563 let (p, q, t) = merge(&acc.0, &acc.1, &acc.2, &pa, &qa, &ta);
564 acc = (p, q, t);
565 }
566 assert_eq!(acc.0, expected.1, "P mismatch for n={n}");
567 assert_eq!(acc.1, expected.2, "Q mismatch for n={n}");
568 assert_eq!(acc.2, expected.3, "T mismatch for n={n}");
569 // the seed branch must reproduce the same state as the full recursion
570 assert_eq!(iacoth_bs(n, 1, 1 + k), (acc.0, acc.1, acc.2));
571 }
572 check(6, &IACOTH_6_INITIAL);
573 check(9, &IACOTH_9_INITIAL);
574 check(99, &IACOTH_99_INITIAL);
575 }
576
577 #[test]
578 fn test_pi_matches_context() {
579 // Cache miss must reproduce Context::pi exactly.
580 for &precision in &[10usize, 50, 100] {
581 let mut cache = ConstCache::new();
582 let cached = cache.pi::<10, mode::HalfEven>(precision).value();
583 let direct = Context::<mode::HalfEven>::new(precision)
584 .pi::<10>(None)
585 .value();
586 assert_eq!(cached, direct, "pi mismatch at precision {precision}");
587 }
588 }
589
590 #[test]
591 fn test_pi_lower_precision_reuses() {
592 // Compute at high precision, then a lower-precision request must round
593 // down from the cached state and still be correct.
594 let mut cache = ConstCache::new();
595 let _pi_high = cache.pi::<10, mode::HalfEven>(200).value();
596 // the slot now holds >=200 terms; a 50-digit request reuses it
597 let pi_50 = cache.pi::<10, mode::HalfEven>(50).value();
598 let direct = Context::<mode::HalfEven>::new(50).pi::<10>(None).value();
599 assert_eq!(pi_50, direct);
600 }
601
602 #[test]
603 fn test_pi_extension_matches_scratch() {
604 // Extending 100 -> 1000 must be bit-identical to a from-scratch 1000-digit compute.
605 let mut cache = ConstCache::new();
606 let _pi_100 = cache.pi::<10, mode::HalfAway>(100).value();
607 let pi_1000_extended = cache.pi::<10, mode::HalfAway>(1000).value();
608
609 let direct = Context::<mode::HalfAway>::new(1000).pi::<10>(None).value();
610 assert_eq!(pi_1000_extended, direct);
611 }
612
613 #[test]
614 fn test_iacoth_matches_context() {
615 let mut cache = ConstCache::new();
616 // ln2 / ln10 via cache must match ln(2)/ln(10) computed independently
617 // through Context::ln (a different, atanh-based algorithm) at several precisions.
618 for &precision in &[20usize, 45, 80] {
619 let cached_ln2 = cache
620 .ln2::<10, mode::Zero>(precision)
621 .with_precision(precision)
622 .value();
623 let ln2_ctx = Context::<mode::Zero>::new(precision);
624 let direct_ln2 = ln2_ctx.unwrap_fp(ln2_ctx.ln::<10>(&Repr::new(2.into(), 0), None));
625 assert_eq!(cached_ln2, direct_ln2, "ln2 mismatch at precision {precision}");
626
627 let cached_ln10 = cache
628 .ln10::<10, mode::Zero>(precision)
629 .with_precision(precision)
630 .value();
631 let ln10_ctx = Context::<mode::Zero>::new(precision);
632 let direct_ln10 = ln10_ctx.unwrap_fp(ln10_ctx.ln::<10>(&Repr::new(10.into(), 0), None));
633 assert_eq!(cached_ln10, direct_ln10, "ln10 mismatch at precision {precision}");
634 }
635 }
636
637 #[test]
638 fn test_iacoth_extension_matches_scratch() {
639 // Extend ln2 from low to high precision; result must match from-scratch.
640 let mut cache = ConstCache::new();
641 let _ln2_low = cache.ln2::<10, mode::HalfAway>(20);
642 let ln2_high = cache.ln2::<10, mode::HalfAway>(120);
643
644 let mut fresh = ConstCache::new();
645 let direct = fresh.ln2::<10, mode::HalfAway>(120);
646 assert_eq!(ln2_high, direct);
647 }
648
649 #[test]
650 fn test_ln_base() {
651 // binary base: ln(base) == ln(2)
652 let mut cache = ConstCache::new();
653 let ln_base = cache.ln_base::<2, mode::HalfAway>(50);
654 let ln2 = cache.ln2::<2, mode::HalfAway>(50);
655 assert_eq!(ln_base, ln2);
656
657 // power-of-two base: ln(8) = 3·ln(2)
658 let ln8 = cache.ln_base::<8, mode::HalfAway>(50);
659 let expected = 3u8 * cache.ln2::<8, mode::HalfAway>(50);
660 assert_eq!(ln8.with_precision(50).value(), expected.with_precision(50).value());
661 }
662
663 #[test]
664 fn test_debug_shows_bigint_head_tail() {
665 let mut cache = ConstCache::new();
666 let _pi = cache.pi::<10, mode::HalfAway>(100); // populate the cache (value unused)
667 let s = format!("{:?}", cache);
668 assert!(s.contains("pi"));
669 assert!(s.contains("num_terms"));
670 // UBig/IBig Debug prints head..tail, so the output stays compact
671 assert!(s.contains(".."), "Debug output should use head..tail truncation");
672 assert!(s.len() < 512);
673 }
674
675 #[test]
676 fn test_sqrt_10005_cached_and_counted() {
677 // Computing π caches the base-free √10005 isqrt; total_words counts it, and
678 // clear() frees it.
679 let mut cache = ConstCache::new();
680 assert_eq!(cache.total_terms(), 0);
681 assert_eq!(cache.total_words(), 0);
682
683 let _pi = cache.pi::<10, mode::HalfAway>(200); // fills the cache (value unused)
684 // the isqrt is now cached (total_terms stays series-only; words include isqrt)
685 assert!(cache.total_words() > 0);
686
687 cache.clear();
688 assert_eq!(cache.total_terms(), 0);
689 assert_eq!(cache.total_words(), 0);
690
691 // after clear, π recomputes from scratch and still matches the direct value
692 let direct = Context::<mode::HalfAway>::new(50).pi::<10>(None).value();
693 let after_clear = cache.pi::<10, mode::HalfAway>(50).value();
694 assert_eq!(after_clear, direct);
695 }
696
697 #[test]
698 fn test_sqrt_10005_reuse_higher_precision() {
699 // A high-precision π call caches a high-bit isqrt; a later lower-precision
700 // call must reuse it (no recompute) and still be correct.
701 let mut cache = ConstCache::new();
702 let _high = cache.pi::<2, mode::HalfEven>(1000);
703 let words_after_high = cache.total_words();
704
705 let low = cache.pi::<2, mode::HalfEven>(100).value();
706 // word count unchanged ⇒ isqrt (and series) were reused, not recomputed
707 assert_eq!(cache.total_words(), words_after_high);
708
709 let direct = Context::<mode::HalfEven>::new(100).pi::<2>(None).value();
710 assert_eq!(low, direct);
711 }
712}