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}