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 / binary-splitting series sum ---
100        // e^y = Σ_{n=0}^{N} y^n / n!
101        // After reduction, |y| <= 2^{-(prec/64 + 4)}.
102        // Above BS_THRESHOLD_BITS bits, use the binary-splitting engine which
103        // achieves O(M(N) log N) rather than the O(N²) iterative path.
104        // Number of iterative terms: N = max(64, prec / 4 + 16).
105        let n_terms = (prec / 4 + 16).max(64) as u64;
106        let mut result = if prec >= super::bs_transcendental::BS_THRESHOLD_BITS {
107            super::bs_transcendental::exp_bs(&x_reduced, work_prec, mode)?
108        } else {
109            exp_taylor(&x_reduced, n_terms, work_prec, mode)?
110        };
111
112        // --- Square back k times ---
113        // e^x = (e^(x/2^k))^(2^k) = result^(2^k)
114        for _ in 0..k {
115            result = result
116                .mul_ref_with_mode(&result.clone(), mode)
117                .with_precision(work_prec, mode);
118        }
119
120        Ok(result.with_precision(prec, mode))
121    }
122}
123
124/// Compute `e^y` via direct Taylor series at `work_prec` bits.
125///
126/// `e^y = Σ_{n=0}^{n_terms} y^n / n!`
127///
128/// Iterates: `term_n = term_{n-1} * y / n`, starting from `term_0 = 1`.
129fn exp_taylor(
130    y: &BigFloat,
131    n_terms: u64,
132    work_prec: u32,
133    mode: RoundingMode,
134) -> OxiNumResult<BigFloat> {
135    // term = y^0 / 0! = 1
136    let mut term = BigFloat::from_i64(1, work_prec, mode);
137    // result = sum of terms
138    let mut result = term.clone();
139
140    for k in 1..=n_terms {
141        // term_k = term_{k-1} * y / k
142        term = term
143            .mul_ref_with_mode(y, mode)
144            .with_precision(work_prec, mode);
145        let k_float = BigFloat::from_i64(k as i64, work_prec, mode);
146        term = term.div_ref_with_mode(&k_float, mode)?;
147        term = term.with_precision(work_prec, mode);
148
149        // Accumulate
150        result = result.add_ref_with_mode(&term, mode);
151        result = result.with_precision(work_prec, mode);
152
153        // Early exit: term is negligible relative to 2^(-work_prec)
154        // This avoids wasting iterations once convergence is achieved.
155        if term.is_zero() {
156            break;
157        }
158        // Check if term is negligible: compare magnitudes via exponent.
159        // result's value is roughly O(1); term < 2^{-work_prec} when
160        // term.exponent + term.mantissa.bit_length() < result.exponent + result.mantissa.bit_length() - work_prec
161        if !result.is_zero() && !term.is_zero() {
162            let term_top = term
163                .exponent
164                .saturating_add(term.mantissa.bit_length() as i64 - 1);
165            let result_top = result
166                .exponent
167                .saturating_add(result.mantissa.bit_length() as i64 - 1);
168            if term_top < result_top.saturating_sub(work_prec as i64 + 8) {
169                break;
170            }
171        }
172    }
173
174    Ok(result)
175}
176
177// ---------------------------------------------------------------------------
178// Unit tests
179// ---------------------------------------------------------------------------
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184    use crate::native::{e_const, RoundingMode};
185
186    fn mk(n: i64, prec: u32) -> BigFloat {
187        BigFloat::from_i64(n, prec, RoundingMode::HalfEven)
188    }
189
190    #[test]
191    fn exp_zero_is_one() {
192        let x = mk(0, 64);
193        let result = x.exp(64, RoundingMode::HalfEven).expect("exp(0)");
194        let one = mk(1, 64);
195        assert_eq!(result, one, "exp(0) should == 1");
196    }
197
198    #[test]
199    fn exp_one_approx_e() {
200        let x = mk(1, 100);
201        let result = x.exp(100, RoundingMode::HalfEven).expect("exp(1)");
202        let e = e_const(100).expect("e_const(100)");
203        let diff = (result.to_f64() - e.to_f64()).abs();
204        assert!(diff < 1e-14, "exp(1) diff from e: {diff}");
205    }
206
207    #[test]
208    fn exp_overflow() {
209        let x = BigFloat::from_f64(800.0, 64).expect("from_f64");
210        let result = x.exp(64, RoundingMode::HalfEven);
211        assert!(
212            matches!(result, Err(OxiNumError::Overflow(_))),
213            "Expected Overflow, got: {result:?}"
214        );
215    }
216
217    #[test]
218    fn exp_large_negative_returns_zero() {
219        let x = BigFloat::from_f64(-800.0, 64).expect("from_f64");
220        let result = x.exp(64, RoundingMode::HalfEven).expect("exp(-800)");
221        assert!(result.is_zero(), "exp(-800) should be zero");
222    }
223
224    #[test]
225    fn exp_small_value_cross_val() {
226        // exp(0.5) ≈ 1.6487212707
227        let x = BigFloat::from_f64(0.5, 64).expect("0.5");
228        let result = x.exp(64, RoundingMode::HalfEven).expect("exp(0.5)");
229        let expected = 0.5_f64.exp();
230        let diff = (result.to_f64() - expected).abs();
231        assert!(diff < 1e-14, "exp(0.5) diff: {diff}");
232    }
233}