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}