Skip to main content

oxinum_float/native/
float_exp.rs

1//! Exponential function for `BigFloat`.
2//!
3//! Implements `BigFloat::exp(x, prec, mode)` using argument reduction followed
4//! by a direct Taylor series evaluation, then k-fold squaring to recover the
5//! result.
6//!
7//! # Algorithm
8//!
9//! 1. **Special cases**: `exp(0) = 1`. Overflow/underflow guarded by f64 range.
10//! 2. **Argument reduction**: Divide x by `2^k` so that `|x/2^k|` is small.
11//!    This is a pure exponent shift — no loss of precision.
12//! 3. **Taylor series**: `e^y = Σ_{n=0}^{N} y^n / n!` computed iteratively
13//!    at `work_prec = prec + guard` bits.
14//! 4. **Squaring back**: `e^x = (e^(x/2^k))^(2^k)` — k squarings at work_prec.
15//! 5. **Round** to requested `prec`.
16
17use oxinum_core::{OxiNumError, OxiNumResult, Sign};
18
19use super::float::{BigFloat, RoundingMode};
20
21impl BigFloat {
22    /// Return `e^self` at `prec` bits using the chosen rounding mode.
23    ///
24    /// # Errors
25    ///
26    /// - [`OxiNumError::Overflow`] if `self` is so large that the result
27    ///   exceeds the representable range (|x| > 745).
28    ///
29    /// # Examples
30    ///
31    /// ```
32    /// use oxinum_float::native::{BigFloat, RoundingMode, e_const};
33    /// let one = BigFloat::from_i64(1, 100, RoundingMode::HalfEven);
34    /// let result = one.exp(100, RoundingMode::HalfEven).expect("exp(1)");
35    /// let e = e_const(100).expect("e_const");
36    /// let diff = (result.to_f64() - e.to_f64()).abs();
37    /// assert!(diff < 1e-14);
38    /// ```
39    pub fn exp(&self, prec: u32, mode: RoundingMode) -> OxiNumResult<BigFloat> {
40        assert!(prec > 0, "BigFloat precision must be > 0");
41
42        // --- IEEE-754 non-finite guards (must be before to_f64() call) ---
43        if self.is_nan() {
44            return Ok(BigFloat::nan(prec));
45        }
46        if self.is_infinite() {
47            return if self.sign == Sign::Negative {
48                Ok(BigFloat::zero(prec)) // exp(-Inf) = +0
49            } else {
50                Ok(BigFloat::infinity(prec)) // exp(+Inf) = +Inf
51            };
52        }
53
54        // --- Special case: exp(0) = 1 ---
55        if self.is_zero() {
56            return Ok(BigFloat::from_i64(1, prec, mode));
57        }
58
59        // --- Overflow / underflow check via f64 approximation ---
60        let x_f64 = self.to_f64();
61        // ln(f64::MAX) ≈ 709.78; add a small margin
62        if x_f64 > 745.0 {
63            return Err(OxiNumError::Overflow(
64                "exp: argument too large (result exceeds BigFloat range)".into(),
65            ));
66        }
67        // For very negative x the result is effectively 0 (subnormal/underflow)
68        if x_f64 < -745.0 {
69            return Ok(BigFloat::zero(prec));
70        }
71
72        // --- Argument reduction ---
73        // Choose k so |x / 2^k| <= 1, with extra reduction for precision.
74        // k = max(0, ceil(log2(|x|+1))) + prec/64 + 4
75        let x_abs = x_f64.abs();
76        let log2_x_abs = if x_abs >= 1.0 {
77            x_abs.log2().ceil() as u64
78        } else {
79            0u64
80        };
81        let guard = 32u32 + (prec / 64 + 4);
82        let work_prec = prec.saturating_add(guard);
83        let k = log2_x_abs + (prec as u64 / 64) + 4;
84
85        // Construct x_reduced = self with exponent decreased by k.
86        // This is exact: value = mantissa * 2^(exponent - k)
87        let x_reduced = if self.is_zero() {
88            BigFloat::zero(work_prec)
89        } else {
90            BigFloat::from_parts(
91                self.sign,
92                self.mantissa.clone(),
93                self.exponent.saturating_sub(k as i64),
94                work_prec,
95                mode,
96            )
97        };
98
99        // --- Taylor series sum ---
100        // e^y = Σ_{n=0}^{N} y^n / n!
101        // After reduction, |y| <= 2^{-(prec/64 + 4)}.
102        // Number of terms: N = max(64, prec / 4 + 16) to ensure full precision.
103        let n_terms = (prec / 4 + 16).max(64) as u64;
104        let mut result = exp_taylor(&x_reduced, n_terms, work_prec, mode)?;
105
106        // --- Square back k times ---
107        // e^x = (e^(x/2^k))^(2^k) = result^(2^k)
108        for _ in 0..k {
109            result = result
110                .mul_ref_with_mode(&result.clone(), mode)
111                .with_precision(work_prec, mode);
112        }
113
114        Ok(result.with_precision(prec, mode))
115    }
116}
117
118/// Compute `e^y` via direct Taylor series at `work_prec` bits.
119///
120/// `e^y = Σ_{n=0}^{n_terms} y^n / n!`
121///
122/// Iterates: `term_n = term_{n-1} * y / n`, starting from `term_0 = 1`.
123fn exp_taylor(
124    y: &BigFloat,
125    n_terms: u64,
126    work_prec: u32,
127    mode: RoundingMode,
128) -> OxiNumResult<BigFloat> {
129    // term = y^0 / 0! = 1
130    let mut term = BigFloat::from_i64(1, work_prec, mode);
131    // result = sum of terms
132    let mut result = term.clone();
133
134    for k in 1..=n_terms {
135        // term_k = term_{k-1} * y / k
136        term = term
137            .mul_ref_with_mode(y, mode)
138            .with_precision(work_prec, mode);
139        let k_float = BigFloat::from_i64(k as i64, work_prec, mode);
140        term = term.div_ref_with_mode(&k_float, mode)?;
141        term = term.with_precision(work_prec, mode);
142
143        // Accumulate
144        result = result.add_ref_with_mode(&term, mode);
145        result = result.with_precision(work_prec, mode);
146
147        // Early exit: term is negligible relative to 2^(-work_prec)
148        // This avoids wasting iterations once convergence is achieved.
149        if term.is_zero() {
150            break;
151        }
152        // Check if term is negligible: compare magnitudes via exponent.
153        // result's value is roughly O(1); term < 2^{-work_prec} when
154        // term.exponent + term.mantissa.bit_length() < result.exponent + result.mantissa.bit_length() - work_prec
155        if !result.is_zero() && !term.is_zero() {
156            let term_top = term
157                .exponent
158                .saturating_add(term.mantissa.bit_length() as i64 - 1);
159            let result_top = result
160                .exponent
161                .saturating_add(result.mantissa.bit_length() as i64 - 1);
162            if term_top < result_top.saturating_sub(work_prec as i64 + 8) {
163                break;
164            }
165        }
166    }
167
168    Ok(result)
169}
170
171// ---------------------------------------------------------------------------
172// Unit tests
173// ---------------------------------------------------------------------------
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178    use crate::native::{e_const, RoundingMode};
179
180    fn mk(n: i64, prec: u32) -> BigFloat {
181        BigFloat::from_i64(n, prec, RoundingMode::HalfEven)
182    }
183
184    #[test]
185    fn exp_zero_is_one() {
186        let x = mk(0, 64);
187        let result = x.exp(64, RoundingMode::HalfEven).expect("exp(0)");
188        let one = mk(1, 64);
189        assert_eq!(result, one, "exp(0) should == 1");
190    }
191
192    #[test]
193    fn exp_one_approx_e() {
194        let x = mk(1, 100);
195        let result = x.exp(100, RoundingMode::HalfEven).expect("exp(1)");
196        let e = e_const(100).expect("e_const(100)");
197        let diff = (result.to_f64() - e.to_f64()).abs();
198        assert!(diff < 1e-14, "exp(1) diff from e: {diff}");
199    }
200
201    #[test]
202    fn exp_overflow() {
203        let x = BigFloat::from_f64(800.0, 64).expect("from_f64");
204        let result = x.exp(64, RoundingMode::HalfEven);
205        assert!(
206            matches!(result, Err(OxiNumError::Overflow(_))),
207            "Expected Overflow, got: {result:?}"
208        );
209    }
210
211    #[test]
212    fn exp_large_negative_returns_zero() {
213        let x = BigFloat::from_f64(-800.0, 64).expect("from_f64");
214        let result = x.exp(64, RoundingMode::HalfEven).expect("exp(-800)");
215        assert!(result.is_zero(), "exp(-800) should be zero");
216    }
217
218    #[test]
219    fn exp_small_value_cross_val() {
220        // exp(0.5) ≈ 1.6487212707
221        let x = BigFloat::from_f64(0.5, 64).expect("0.5");
222        let result = x.exp(64, RoundingMode::HalfEven).expect("exp(0.5)");
223        let expected = 0.5_f64.exp();
224        let diff = (result.to_f64() - expected).abs();
225        assert!(diff < 1e-14, "exp(0.5) diff: {diff}");
226    }
227}