Skip to main content

oxinum_float/native/
ln_agm.rs

1//! AGM-based natural logarithm for `BigFloat`.
2//!
3//! Implements `BigFloat::ln_agm(prec, mode)` using the arithmetic-geometric
4//! mean algorithm. This is algorithmically distinct from the Newton-Raphson +
5//! Taylor `ln` in `float_ln.rs`.
6//!
7//! # Algorithm
8//!
9//! ## Core formula (Borwein & Borwein)
10//!
11//! For a sufficiently large argument `s >> 1`:
12//!
13//! ```text
14//! ln(s) = π / (2 · AGM(1, 4/s))
15//! ```
16//!
17//! This follows from the connection between the complete elliptic integral
18//! K(k) and the AGM: `K(k) = π / (2 · AGM(1, √(1−k²)))`, combined with the
19//! limit `K(k) → (1/2) · ln(4/(1−k²))` as `k → 1`. Setting `k² → 1 − (4/s)²`
20//! and taking `s → ∞` yields the identity above.
21//!
22//! ## Argument reduction
23//!
24//! For arbitrary `x > 0`:
25//!
26//! 1. Find `floor(log₂(x)) = x.exponent + x.mantissa.bit_length() - 1`.
27//! 2. Choose a left-shift `K` so that `s = x · 2^K` satisfies
28//!    `floor(log₂(s)) ≥ work_prec/2 + 10`.
29//! 3. Compute `ln(s) = π / (2 · AGM(1, 4/s))` at `work_prec` guard bits.
30//! 4. Recover `ln(x) = ln(s) − K · ln(2)`.
31//!
32//! ## AGM iteration
33//!
34//! Starting from `a₀ = 1`, `b₀ = 4/s`, the arithmetic-geometric mean iteration
35//! is:
36//!
37//! ```text
38//! a_{n+1} = (a_n + b_n) / 2
39//! b_{n+1} = sqrt(a_n · b_n)
40//! ```
41//!
42//! Convergence is quadratic: the number of correct bits doubles each iteration.
43//! Starting with `b₀ ≈ 4/s ≈ 2^(−work_prec/2)`, convergence occurs in
44//! approximately `log₂(work_prec) + 8` iterations.
45
46use oxinum_core::{OxiNumError, OxiNumResult, Sign};
47
48use super::constants::{ln2, pi};
49use super::float::{BigFloat, RoundingMode};
50
51impl BigFloat {
52    /// Return `ln(self)` at `prec` bits using the AGM algorithm.
53    ///
54    /// This is algorithmically distinct from [`BigFloat::ln`], which uses
55    /// Newton-Raphson iteration on the exponential. The AGM method converges
56    /// quadratically from the start (no f64 seed required) and avoids calling
57    /// `exp` internally, making it independent of the Taylor-series path.
58    ///
59    /// # Errors
60    ///
61    /// - [`OxiNumError::Domain`] if `self <= 0` or `prec == 0`.
62    ///
63    /// # Examples
64    ///
65    /// ```
66    /// use oxinum_float::native::{BigFloat, RoundingMode};
67    /// let two = BigFloat::from_i64(2, 64, RoundingMode::HalfEven);
68    /// let result = two.ln_agm(64, RoundingMode::HalfEven).expect("ln_agm(2)");
69    /// assert!((result.to_f64() - std::f64::consts::LN_2).abs() < 1e-14);
70    /// ```
71    pub fn ln_agm(&self, prec: u32, mode: RoundingMode) -> OxiNumResult<BigFloat> {
72        if prec == 0 {
73            return Err(OxiNumError::Domain("ln_agm: precision must be > 0".into()));
74        }
75
76        // --- IEEE-754 non-finite guards (placed after prec==0 check to avoid
77        //     BigFloat::nan(0) panic) ---
78        if self.is_nan() {
79            return Ok(BigFloat::nan(prec));
80        }
81        if self.is_infinite() {
82            return if self.sign == Sign::Negative {
83                Ok(BigFloat::nan(prec)) // ln(-Inf) = NaN
84            } else {
85                Ok(BigFloat::infinity(prec)) // ln(+Inf) = +Inf
86            };
87        }
88
89        // --- Special cases ---
90        if self.is_zero() {
91            return Err(OxiNumError::Domain("ln_agm of zero is undefined".into()));
92        }
93        if self.sign == Sign::Negative {
94            return Err(OxiNumError::Domain(
95                "ln_agm of a negative number is undefined for real BigFloat".into(),
96            ));
97        }
98
99        // --- Special case: ln(1) = 0 ---
100        {
101            let one = BigFloat::from_i64(1, prec, mode);
102            if self == &one {
103                return Ok(BigFloat::zero(prec));
104            }
105        }
106
107        // Guard bits: need enough room for the AGM formula constant error.
108        // prec + 64 is generous; it covers any reasonable input range.
109        let guard = 64u32;
110        let work_prec = prec.saturating_add(guard);
111
112        // --- Argument reduction ---
113        //
114        // floor(log2(self)) = self.exponent + self.mantissa.bit_length() - 1
115        // (The mantissa is normalized: bit_length == self.precision.)
116        let cur_log2: i64 = self
117            .exponent
118            .saturating_add(self.mantissa.bit_length() as i64 - 1);
119
120        // We need floor(log2(s)) >= work_prec/2 + 10 for the AGM formula.
121        // s = self * 2^shift_k  =>  floor(log2(s)) = cur_log2 + shift_k.
122        let target_log2 = (work_prec / 2 + 10) as i64;
123        // shift_k can be negative (i.e., x is already large) — that's fine;
124        // we use saturating arithmetic and allow it to be 0 or negative.
125        let shift_k: i64 = target_log2 - cur_log2;
126
127        // Build s = self * 2^shift_k at work_prec.
128        // Adjust exponent directly: s = BigFloat { same mantissa, exp + shift_k }.
129        let s = {
130            let new_exp = self.exponent.saturating_add(shift_k);
131            BigFloat::from_parts(
132                Sign::Positive,
133                self.mantissa.clone(),
134                new_exp,
135                work_prec,
136                mode,
137            )
138        };
139
140        // --- Compute ln(s) via the AGM formula ---
141        let ln_s = agm_ln_large(&s, work_prec, mode)?;
142
143        // --- Recover ln(x) = ln(s) - shift_k * ln(2) ---
144        let ln_result = if shift_k == 0 {
145            ln_s
146        } else {
147            let ln2_val = ln2(work_prec)?;
148            let shift_f = BigFloat::from_i64(shift_k, work_prec, mode);
149            let correction = shift_f.mul_ref_with_mode(&ln2_val, mode);
150            ln_s.sub_ref_with_mode(&correction, mode)
151        };
152
153        Ok(ln_result.with_precision(prec, mode))
154    }
155}
156
157/// Compute `ln(s)` for a large positive `s` using the AGM formula:
158///
159/// ```text
160/// ln(s) = π / (2 · AGM(1, 4/s))
161/// ```
162///
163/// This is valid when `s >> 1` (specifically when `floor(log2(s)) ≥ work_prec/2`).
164/// The caller must ensure this precondition is met (via argument reduction).
165fn agm_ln_large(s: &BigFloat, work_prec: u32, mode: RoundingMode) -> OxiNumResult<BigFloat> {
166    // a0 = 1
167    let a = BigFloat::from_i64(1, work_prec, mode);
168    // b0 = 4 / s
169    let four = BigFloat::from_i64(4, work_prec, mode);
170    let b = four.div_ref_with_mode(s, mode)?;
171
172    // Compute AGM(a, b)
173    let agm_val = agm_iterate(a, b, work_prec, mode)?;
174
175    // pi / (2 * agm_val)
176    let pi_val = pi(work_prec)?;
177    let two = BigFloat::from_i64(2, work_prec, mode);
178    let two_agm = two.mul_ref_with_mode(&agm_val, mode);
179    let result = pi_val.div_ref_with_mode(&two_agm, mode)?;
180
181    Ok(result)
182}
183
184/// Compute the arithmetic-geometric mean `AGM(a0, b0)`.
185///
186/// The iteration converges quadratically; for `b0 ≈ 4/s` with
187/// `log2(s) ≥ work_prec/2`, the iteration requires at most
188/// `log2(work_prec) + 8` steps.
189///
190/// Convergence test: the value has converged when the top-bit position of
191/// `|a - b|` satisfies `exponent + bit_length - 1 < -(work_prec as i64) + 4`.
192fn agm_iterate(
193    mut a: BigFloat,
194    mut b: BigFloat,
195    work_prec: u32,
196    mode: RoundingMode,
197) -> OxiNumResult<BigFloat> {
198    // Number of iterations: quadratic convergence from ~work_prec/2 correct
199    // bits to work_prec. Iterations needed: ceil(log2(work_prec / (work_prec/2)))
200    // ≈ log2(2) = 1, but we add generous headroom because the initial gap can
201    // be larger. Mirror the existing newton_ln pattern.
202    let max_iters: u32 = if work_prec <= 64 {
203        10
204    } else {
205        let n = (work_prec as f64).log2().ceil() as u32 + 8;
206        n.min(64)
207    };
208
209    // Convergence threshold: |a - b| < 2^(-(work_prec - 4))
210    // Expressed as: top_bit_pos < -((work_prec as i64) - 4)
211    let threshold = -((work_prec as i64) - 4);
212
213    for _ in 0..max_iters {
214        // a_new = (a + b) / 2
215        // Halving is exact: increment exponent by -1 (equivalent to * 0.5).
216        // We compute a + b first, then halve the result.
217        let sum = a.add_ref_with_mode(&b, mode);
218        // Halve: adjust exponent by -1 (exact, no rounding).
219        let a_new = BigFloat::from_parts(
220            sum.sign(),
221            sum.mantissa().clone(),
222            sum.exponent().saturating_sub(1),
223            work_prec,
224            mode,
225        );
226
227        // b_new = sqrt(a * b)
228        let product = a.mul_ref_with_mode(&b, mode);
229        let b_new = product.sqrt(work_prec, mode)?;
230
231        // Convergence check: |a_new - b_new|
232        let diff = a_new.sub_ref_with_mode(&b_new, mode).abs();
233        if diff.is_zero() {
234            return Ok(a_new);
235        }
236        // top_bit_pos = diff.exponent + diff.mantissa.bit_length() - 1
237        let top_bit_pos = diff
238            .exponent()
239            .saturating_add(diff.mantissa().bit_length() as i64 - 1);
240        if top_bit_pos < threshold {
241            return Ok(a_new);
242        }
243
244        a = a_new;
245        b = b_new;
246    }
247
248    // Return best estimate (should have converged within max_iters).
249    Ok(a)
250}
251
252// ---------------------------------------------------------------------------
253// Unit tests
254// ---------------------------------------------------------------------------
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259    use crate::native::RoundingMode;
260
261    fn mk(n: i64, prec: u32) -> BigFloat {
262        BigFloat::from_i64(n, prec, RoundingMode::HalfEven)
263    }
264
265    /// Check |a - b| < 2^{-tol_bits} using exponent-based comparison.
266    fn approx_eq_bits(a: &BigFloat, b: &BigFloat, tol_bits: u32) -> bool {
267        let diff = a.sub_ref_with_mode(b, RoundingMode::HalfEven).abs();
268        if diff.is_zero() {
269            return true;
270        }
271        let top_bit_pos = diff
272            .exponent()
273            .saturating_add(diff.mantissa().bit_length() as i64 - 1);
274        top_bit_pos < -(tol_bits as i64)
275    }
276
277    #[test]
278    fn ln_agm_one_is_zero() {
279        let x = mk(1, 100);
280        let result = x.ln_agm(100, RoundingMode::HalfEven).expect("ln_agm(1)");
281        assert!(result.is_zero(), "ln_agm(1) should be 0, got: {result:?}");
282    }
283
284    #[test]
285    fn ln_agm_zero_is_domain_error() {
286        let x = mk(0, 64);
287        let result = x.ln_agm(64, RoundingMode::HalfEven);
288        assert!(
289            matches!(result, Err(OxiNumError::Domain(_))),
290            "Expected Domain error for ln_agm(0), got: {result:?}"
291        );
292    }
293
294    #[test]
295    fn ln_agm_negative_is_domain_error() {
296        let x = mk(-1, 64);
297        let result = x.ln_agm(64, RoundingMode::HalfEven);
298        assert!(
299            matches!(result, Err(OxiNumError::Domain(_))),
300            "Expected Domain error for ln_agm(-1), got: {result:?}"
301        );
302    }
303
304    #[test]
305    fn ln_agm_prec_zero_is_domain_error() {
306        let x = mk(2, 64);
307        let result = x.ln_agm(0, RoundingMode::HalfEven);
308        assert!(
309            matches!(result, Err(OxiNumError::Domain(_))),
310            "Expected Domain error for prec=0, got: {result:?}"
311        );
312    }
313
314    #[test]
315    fn ln_agm_e_is_approximately_one() {
316        use crate::native::e_const;
317        let prec = 100u32;
318        let e = e_const(prec).expect("e_const");
319        let result = e.ln_agm(60, RoundingMode::HalfEven).expect("ln_agm(e)");
320        let expected = mk(1, 60);
321        assert!(
322            approx_eq_bits(&result, &expected, 45),
323            "ln_agm(e) should be ≈ 1, got: {} (diff from 1: {})",
324            result.to_f64(),
325            (result.to_f64() - 1.0).abs()
326        );
327    }
328
329    #[test]
330    fn ln_agm_matches_newton_ln() {
331        // Cross-validate AGM ln against Newton-Raphson ln for several inputs.
332        let prec = 80u32;
333        let tol = 60u32; // Allow ~20 guard bits of slack
334        let mode = RoundingMode::HalfEven;
335        for n in [2i64, 7, 100, 1000] {
336            let x = BigFloat::from_i64(n, prec + 64, mode);
337            let ln_newton = x.ln(prec, mode).expect("newton ln");
338            let ln_agm = x.ln_agm(prec, mode).expect("agm ln");
339            assert!(
340                approx_eq_bits(&ln_newton, &ln_agm, tol),
341                "ln_agm({n}) vs Newton mismatch: newton={}, agm={}",
342                ln_newton.to_f64(),
343                ln_agm.to_f64()
344            );
345        }
346    }
347
348    #[test]
349    fn ln_agm_small_fraction() {
350        // ln(0.5) = -ln(2)
351        use crate::native::ln2 as ln2_const;
352        let prec = 80u32;
353        let mode = RoundingMode::HalfEven;
354        // 0.5 = 1 * 2^(-1)
355        let half = BigFloat::from_parts(
356            Sign::Positive,
357            oxinum_int::native::BigUint::one(),
358            -1,
359            prec,
360            mode,
361        );
362        let ln_half = half.ln_agm(prec, mode).expect("ln_agm(0.5)");
363        let neg_ln2 = ln2_const(prec).expect("ln2").neg();
364        assert!(
365            approx_eq_bits(&ln_half, &neg_ln2, 60),
366            "ln_agm(0.5) should be -ln(2), got: {}",
367            ln_half.to_f64()
368        );
369    }
370}