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}