Skip to main content

oxinum_float/native/
float_ln.rs

1//! Natural logarithm for `BigFloat`.
2//!
3//! Implements `BigFloat::ln(x, prec, mode)` using mantissa decomposition
4//! followed by Newton's method on the exponential.
5//!
6//! # Algorithm
7//!
8//! 1. **Special cases**: `ln(0)` → Domain error, `ln(negative)` → Domain
9//!    error, `ln(1)` = 0.
10//! 2. **Mantissa decomposition**: Write `x = m * 2^K` where `m ∈ [0.5, 1)`.
11//!    From the BigFloat representation: `x = mantissa * 2^e` with
12//!    `mantissa.bit_length() == prec`. Set `m` = same mantissa with exponent
13//!    `-prec` (so value = mantissa * 2^(-prec) ∈ [0.5, 1.0)) and `K = e + prec`.
14//!    Then `ln(x) = ln(m) + K * ln(2)`.
15//! 3. **Newton's method for ln(m)**:
16//!    - Seed: `y_0 = ln(m)` from f64.
17//!    - Iteration: `y_{i+1} = y_i + m/exp(y_i) - 1`.
18//!    - Each step doubles correct bits; run `ceil(log2(prec/53)) + 3` iters.
19//!    - Precision schedule doubles each iteration.
20//! 4. **Combine**: `ln(x) = ln(m) + K * ln(2)`, round to `prec`.
21
22use oxinum_core::{OxiNumError, OxiNumResult, Sign};
23
24use super::constants::ln2;
25use super::float::{BigFloat, RoundingMode};
26
27impl BigFloat {
28    /// Return `ln(self)` at `prec` bits using the chosen rounding mode.
29    ///
30    /// # Errors
31    ///
32    /// - [`OxiNumError::Domain`] if `self <= 0`.
33    ///
34    /// # Examples
35    ///
36    /// ```
37    /// use oxinum_float::native::{BigFloat, RoundingMode};
38    /// let one = BigFloat::from_i64(1, 64, RoundingMode::HalfEven);
39    /// let result = one.ln(64, RoundingMode::HalfEven).expect("ln(1)");
40    /// assert!(result.is_zero());
41    /// ```
42    pub fn ln(&self, prec: u32, mode: RoundingMode) -> OxiNumResult<BigFloat> {
43        assert!(prec > 0, "BigFloat precision must be > 0");
44
45        // --- IEEE-754 non-finite guards ---
46        if self.is_nan() {
47            return Ok(BigFloat::nan(prec));
48        }
49        if self.is_infinite() {
50            return if self.sign == Sign::Negative {
51                Ok(BigFloat::nan(prec)) // ln(-Inf) = NaN
52            } else {
53                Ok(BigFloat::infinity(prec)) // ln(+Inf) = +Inf
54            };
55        }
56
57        // --- Special cases ---
58        if self.is_zero() {
59            return Err(OxiNumError::Domain("ln of zero is undefined".into()));
60        }
61        if self.sign == Sign::Negative {
62            return Err(OxiNumError::Domain(
63                "ln of negative number is undefined for real BigFloat".into(),
64            ));
65        }
66
67        // --- Special case: ln(1) = 0 ---
68        let one = BigFloat::from_i64(1, prec, mode);
69        if self == &one {
70            return Ok(BigFloat::zero(prec));
71        }
72
73        // --- Mantissa decomposition ---
74        // x = mantissa * 2^exponent, with mantissa.bit_length() == precision.
75        // Write x = m * 2^K where m = mantissa * 2^(-prec) ∈ [0.5, 1.0)
76        // and K = exponent + prec.
77        let prec_i = prec as i64;
78        let big_k = self.exponent.saturating_add(prec_i);
79
80        // Construct m as a BigFloat with the same mantissa but exponent = -prec.
81        // This gives value = mantissa * 2^(-prec) ∈ [0.5, 1.0).
82        let guard = 32u32 + prec / 16 + 8;
83        let work_prec = prec.saturating_add(guard);
84
85        let m = BigFloat::from_parts(
86            Sign::Positive,
87            self.mantissa.clone(),
88            -prec_i,
89            work_prec,
90            mode,
91        );
92
93        // --- Newton's method for y = ln(m) ---
94        // m is in [0.5, 1.0), so ln(m) is in (-ln(2), 0].
95        // f64 seed for ln(m): use m.to_f64().ln()
96        let m_f64 = m.to_f64();
97        let ln_m_f64 = m_f64.ln(); // valid because m_f64 ∈ (0, 1)
98
99        let ln_m = newton_ln(&m, ln_m_f64, work_prec, mode)?;
100
101        // --- Combine: ln(x) = ln(m) + K * ln(2) ---
102        let k_float = BigFloat::from_i64(big_k, work_prec, mode);
103        let ln2_val = ln2(work_prec)?;
104        let k_times_ln2 = k_float.mul_ref_with_mode(&ln2_val, mode);
105        let result = ln_m.add_ref_with_mode(&k_times_ln2, mode);
106
107        Ok(result.with_precision(prec, mode))
108    }
109}
110
111/// Compute `ln(m)` via Newton's method: `y_{i+1} = y_i + m/exp(y_i) - 1`.
112///
113/// Converges quadratically from the f64 seed `ln_m_f64`.
114///
115/// Each iteration doubles the number of correct bits, starting from ~53 bits.
116/// We run `ceil(log2(work_prec / 53)) + 3` iterations (capped at 30).
117fn newton_ln(
118    m: &BigFloat,
119    ln_m_f64: f64,
120    work_prec: u32,
121    mode: RoundingMode,
122) -> OxiNumResult<BigFloat> {
123    // Seed from f64
124    let mut current_prec: u32 = 64;
125    let mut y = BigFloat::from_f64(ln_m_f64, current_prec)?;
126
127    // Number of Newton iterations needed to reach work_prec from ~53 bits.
128    // Each iteration: bits_correct *= 2
129    // Need: 53 * 2^n >= work_prec => n = ceil(log2(work_prec / 53))
130    let n_iters = if work_prec <= 64 {
131        3u32
132    } else {
133        let ratio = (work_prec as f64) / 53.0;
134        ratio.log2().ceil() as u32 + 3
135    };
136    let n_iters = n_iters.min(60);
137
138    for _ in 0..n_iters {
139        // Double precision for this step (up to work_prec)
140        current_prec = (current_prec.saturating_mul(2)).min(work_prec);
141
142        let m_at_prec = m.clone().with_precision(current_prec, mode);
143        let y_at_prec = y.clone().with_precision(current_prec, mode);
144
145        // Compute exp(y) at current precision
146        let ey = y_at_prec.exp(current_prec, mode)?;
147
148        // correction = m / exp(y)
149        let correction = m_at_prec.div_ref_with_mode(&ey, mode)?;
150
151        // y_{i+1} = y_i + m/exp(y_i) - 1
152        let one = BigFloat::from_i64(1, current_prec, mode);
153        let diff = correction.sub_ref_with_mode(&one, mode);
154        y = y_at_prec
155            .add_ref_with_mode(&diff, mode)
156            .with_precision(current_prec, mode);
157    }
158
159    Ok(y.with_precision(work_prec, mode))
160}
161
162// ---------------------------------------------------------------------------
163// Unit tests
164// ---------------------------------------------------------------------------
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169    use crate::native::{e_const, ln2 as ln2_const, RoundingMode};
170
171    fn mk(n: i64, prec: u32) -> BigFloat {
172        BigFloat::from_i64(n, prec, RoundingMode::HalfEven)
173    }
174
175    fn approx_eq_bits(a: &BigFloat, b: &BigFloat, tol_bits: u32) -> bool {
176        let diff = a.sub_ref(b).abs();
177        // threshold = 2^(-(tol_bits)) constructed as a BigFloat at work precision
178        // Use: diff.exponent + diff.mantissa.bit_length() - 1 < -tol_bits as a proxy
179        // for "diff < 2^(-tol_bits)".
180        // More precisely: compare threshold via from_parts.
181        if diff.is_zero() {
182            return true;
183        }
184        // diff.value approx = diff.mantissa * 2^diff.exponent
185        // diff < 2^(-tol_bits) iff diff.exponent + diff.mantissa.bit_length() - 1 < -(tol_bits as i64)
186        let diff_top_exp = diff
187            .exponent
188            .saturating_add(diff.mantissa.bit_length() as i64 - 1);
189        diff_top_exp < -(tol_bits as i64)
190    }
191
192    #[test]
193    fn ln_one_is_zero() {
194        let x = mk(1, 100);
195        let result = x.ln(100, RoundingMode::HalfEven).expect("ln(1)");
196        assert!(result.is_zero(), "ln(1) should be 0, got: {result:?}");
197    }
198
199    #[test]
200    fn ln_zero_is_domain_error() {
201        let x = mk(0, 64);
202        let result = x.ln(64, RoundingMode::HalfEven);
203        assert!(
204            matches!(result, Err(OxiNumError::Domain(_))),
205            "Expected Domain error, got: {result:?}"
206        );
207    }
208
209    #[test]
210    fn ln_negative_is_domain_error() {
211        let x = mk(-1, 64);
212        let result = x.ln(64, RoundingMode::HalfEven);
213        assert!(
214            matches!(result, Err(OxiNumError::Domain(_))),
215            "Expected Domain error, got: {result:?}"
216        );
217    }
218
219    #[test]
220    fn ln_e_is_one() {
221        let prec = 100u32;
222        let e = e_const(prec).expect("e_const");
223        let result = e.ln(prec, RoundingMode::HalfEven).expect("ln(e)");
224        let one = mk(1, prec);
225        assert!(
226            approx_eq_bits(&result, &one, 85),
227            "ln(e) should ≈ 1, got: {} (diff from 1: {})",
228            result.to_f64(),
229            (result.to_f64() - 1.0).abs()
230        );
231    }
232
233    #[test]
234    fn ln2_matches_constant() {
235        let prec = 100u32;
236        let two = mk(2, prec);
237        let computed = two.ln(prec, RoundingMode::HalfEven).expect("ln(2)");
238        let expected = ln2_const(prec).expect("ln2 constant");
239        assert!(
240            approx_eq_bits(&computed, &expected, 85),
241            "ln(2) should match ln2 constant, diff = {}",
242            (computed.to_f64() - expected.to_f64()).abs()
243        );
244    }
245}