dashu_float/exp.rs
1use core::convert::TryInto;
2
3use crate::{
4 error::{assert_finite, assert_limited_precision, FpError, FpResult},
5 fbig::FBig,
6 math::cache::{reborrow_cache, ConstCache},
7 repr::{Context, Repr, Word},
8 round::Round,
9 utils::ceil_usize,
10};
11use dashu_base::{AbsOrd, Approximation::*, BitTest, DivRemEuclid, EstimatedLog2, Sign};
12use dashu_int::IBig;
13
14impl<R: Round, const B: Word> FBig<R, B> {
15 /// Raise the floating point number to an integer power.
16 ///
17 /// # Examples
18 ///
19 /// ```
20 /// # use dashu_base::ParseError;
21 /// # use dashu_float::DBig;
22 /// # use core::str::FromStr;
23 /// let a = DBig::from_str("-1.234")?;
24 /// assert_eq!(a.powi(10.into()), DBig::from_str("8.188")?);
25 /// # Ok::<(), ParseError>(())
26 /// ```
27 #[inline]
28 pub fn powi(&self, exp: IBig) -> FBig<R, B> {
29 self.context.unwrap_fp(self.context.powi(&self.repr, exp))
30 }
31
32 /// Raise the floating point number to an floating point power.
33 ///
34 /// # Examples
35 ///
36 /// ```
37 /// # use dashu_base::ParseError;
38 /// # use dashu_float::DBig;
39 /// # use core::str::FromStr;
40 /// let x = DBig::from_str("1.23")?;
41 /// let y = DBig::from_str("-4.56")?;
42 /// assert_eq!(x.powf(&y), DBig::from_str("0.389")?);
43 /// # Ok::<(), ParseError>(())
44 /// ```
45 #[inline]
46 pub fn powf(&self, exp: &Self) -> Self {
47 let context = Context::max(self.context, exp.context);
48 context.unwrap_fp(context.powf(&self.repr, &exp.repr, None))
49 }
50
51 /// Calculate the exponential function (`eˣ`) on the floating point number.
52 ///
53 /// # Examples
54 ///
55 /// ```
56 /// # use dashu_base::ParseError;
57 /// # use dashu_float::DBig;
58 /// # use core::str::FromStr;
59 /// let a = DBig::from_str("-1.234")?;
60 /// assert_eq!(a.exp(), DBig::from_str("0.2911")?);
61 /// # Ok::<(), ParseError>(())
62 /// ```
63 #[inline]
64 pub fn exp(&self) -> FBig<R, B> {
65 self.context.unwrap_fp(self.context.exp(&self.repr, None))
66 }
67
68 /// Calculate the exponential minus one function (`eˣ-1`) on the floating point number.
69 ///
70 /// # Examples
71 ///
72 /// ```
73 /// # use dashu_base::ParseError;
74 /// # use dashu_float::DBig;
75 /// # use core::str::FromStr;
76 /// let a = DBig::from_str("-0.1234")?;
77 /// assert_eq!(a.exp_m1(), DBig::from_str("-0.11609")?);
78 /// # Ok::<(), ParseError>(())
79 /// ```
80 #[inline]
81 pub fn exp_m1(&self) -> FBig<R, B> {
82 self.context
83 .unwrap_fp(self.context.exp_m1(&self.repr, None))
84 }
85}
86
87// TODO: give the exact formulation of required guard bits
88
89impl<R: Round> Context<R> {
90 /// Raise the floating point number to an integer power under this context.
91 ///
92 /// # Examples
93 ///
94 /// ```
95 /// # use dashu_base::ParseError;
96 /// # use dashu_float::DBig;
97 /// # use core::str::FromStr;
98 /// use dashu_base::Approximation::*;
99 /// use dashu_float::{Context, round::{mode::HalfAway, Rounding::*}};
100 ///
101 /// let context = Context::<HalfAway>::new(2);
102 /// let a = DBig::from_str("-1.234")?;
103 /// assert_eq!(context.powi(&a.repr(), 10.into()), Ok(Inexact(DBig::from_str("8.2")?, AddOne)));
104 /// # Ok::<(), ParseError>(())
105 /// ```
106 ///
107 /// # Panics
108 ///
109 /// Panics if the precision is unlimited and the exponent is negative. In this case, the exact
110 /// result is likely to have infinite digits.
111 pub fn powi<const B: Word>(&self, base: &Repr<B>, exp: IBig) -> FpResult<FBig<R, B>> {
112 if base.is_infinite() {
113 return Err(FpError::InfiniteInput);
114 }
115
116 let (exp_sign, exp) = exp.into_parts();
117 if exp_sign == Sign::Negative {
118 // if the exponent is negative, then negate the exponent
119 // note that do the inverse at last requires less guard bits
120 assert_limited_precision(self.precision); // TODO: we can allow this if the inverse is exact (only when significand is one?)
121
122 let guard_bits = self.precision.bit_len() * 2; // heuristic
123 let rev_context = Context::<R::Reverse>::new(self.precision + guard_bits);
124 let pow = rev_context.unwrap_fp(rev_context.powi(base, exp.into()));
125 let inv = rev_context.unwrap_fp_repr(rev_context.repr_div(Repr::one(), pow.repr));
126 let repr = self.repr_round(inv);
127 return Ok(repr.map(|v| FBig::new(v, *self)));
128 }
129 if exp.is_zero() {
130 return Ok(Exact(FBig::ONE));
131 } else if exp.is_one() {
132 let repr = self.repr_round_ref(base);
133 return Ok(repr.map(|v| FBig::new(v, *self)));
134 }
135
136 // Guard against exponent overflow for astronomically large results: the result
137 // magnitude has log2 ≈ exp·log2(base); if that exceeds the isize exponent range,
138 // return ±inf (|base| > 1) or 0 (|base| < 1) instead of overflowing mid-computation.
139 let base_log2 = base.log2_est() as f64;
140 let threshold = (isize::MAX as f64) * (B.log2_est() as f64);
141 let exp_f64 = i64::try_from(&exp).ok().map(|e| e as f64);
142 let overflows = match exp_f64 {
143 Some(e) => e * base_log2 > threshold,
144 None => base_log2 != 0.0, // exp doesn't fit i64: overflows unless |base| == 1
145 };
146 if overflows {
147 return if base_log2 > 0.0 {
148 Err(FpError::Overflow(if base.sign() == Sign::Negative {
149 Sign::Negative
150 } else {
151 Sign::Positive
152 }))
153 } else {
154 // |base| < 1 and exponent huge → underflow to signed zero
155 let underflow_sign = if base.sign() == Sign::Negative && exp.bit(0) {
156 Sign::Negative
157 } else {
158 Sign::Positive
159 };
160 Err(FpError::Underflow(underflow_sign))
161 };
162 }
163
164 let work_context = if self.is_limited() {
165 // increase working precision when the exponent is large
166 let guard_digits = exp.bit_len() + self.precision.bit_len(); // heuristic
167 Context::<R>::new(self.precision + guard_digits)
168 } else {
169 Context::<R>::new(0)
170 };
171
172 // binary exponentiation from left to right
173 let mut p = exp.bit_len() - 2;
174 let mut res = work_context.unwrap_fp(work_context.sqr(base));
175 loop {
176 if exp.bit(p) {
177 res = work_context.unwrap_fp(work_context.mul(res.repr(), base));
178 }
179 if p == 0 {
180 break;
181 }
182 p -= 1;
183 res = work_context.unwrap_fp(work_context.sqr(res.repr()));
184 }
185
186 Ok(res.with_precision(self.precision))
187 }
188
189 /// Raise the floating point number to an floating point power under this context.
190 ///
191 /// Note that this method will not rely on [FBig::powi] even if the `exp` is actually an integer.
192 ///
193 /// # Examples
194 ///
195 /// ```
196 /// # use dashu_base::ParseError;
197 /// # use dashu_float::DBig;
198 /// # use core::str::FromStr;
199 /// use dashu_base::Approximation::*;
200 /// use dashu_float::{Context, round::{mode::HalfAway, Rounding::*}};
201 ///
202 /// let context = Context::<HalfAway>::new(2);
203 /// let x = DBig::from_str("1.23")?;
204 /// let y = DBig::from_str("-4.56")?;
205 /// assert_eq!(context.powf(&x.repr(), &y.repr(), None), Ok(Inexact(DBig::from_str("0.39")?, AddOne)));
206 /// # Ok::<(), ParseError>(())
207 /// ```
208 ///
209 /// # Panics
210 ///
211 /// Panics if the precision is unlimited.
212 pub fn powf<const B: Word>(
213 &self,
214 base: &Repr<B>,
215 exp: &Repr<B>,
216 mut cache: Option<&mut ConstCache>,
217 ) -> FpResult<FBig<R, B>> {
218 if base.is_infinite() || exp.is_infinite() {
219 return Err(FpError::InfiniteInput);
220 }
221 assert_limited_precision(self.precision); // TODO: we can allow it if exp is integer
222
223 // shortcuts
224 if exp.is_pos_zero() || exp.is_neg_zero() {
225 // pow(x, ±0) = 1 for any base (IEEE 754 §9.2.1); `-0` is numerically zero, so it
226 // must take the same shortcut as `+0` (otherwise a negative base falls through to
227 // the OutOfDomain path below).
228 return Ok(Exact(FBig::ONE));
229 } else if exp.is_one() {
230 let repr = self.repr_round_ref(base);
231 return Ok(repr.map(|v| FBig::new(v, *self)));
232 } else if base.significand.is_zero() {
233 // With a *float* exponent the result on a zero base is the positive one — this
234 // matches the common float-pow convention (e.g. CPython: `(-0.0) ** y == 0.0`),
235 // which doesn't track the parity of the exponent:
236 // pow(±0, y > 0) = +0, pow(±0, y < 0) = +inf.
237 // For the sign-correct result (e.g. `pow(-0, odd) = -0`), use the integer-exponent
238 // [`powi`](Context::powi). Short-circuiting here also avoids the negative-base path.
239 return Ok(Exact(if exp.sign() == Sign::Negative {
240 FBig::new(Repr::infinity(), *self)
241 } else {
242 FBig::ZERO
243 }));
244 }
245 if base.sign() == Sign::Negative {
246 // TODO: we should allow negative base when exp is an integer
247 return Err(FpError::OutOfDomain);
248 }
249
250 // x^y = exp(y*ln(x)), use a simple rule for guard bits
251 let guard_digits = 10 + ceil_usize(self.precision.log2_est());
252 let work_context = Context::<R>::new(self.precision + guard_digits);
253
254 // ln and exp each consult/extend the shared cache; reborrows are sequential.
255 let ln_val = work_context.unwrap_fp(work_context.ln(base, reborrow_cache(&mut cache)));
256 let mul_val = work_context.unwrap_fp(work_context.mul(ln_val.repr(), exp));
257 let exp_val =
258 work_context.unwrap_fp(work_context.exp(mul_val.repr(), reborrow_cache(&mut cache)));
259 Ok(exp_val.with_precision(self.precision))
260 }
261
262 /// Calculate the exponential function (`eˣ`) on the floating point number under this context.
263 ///
264 /// # Examples
265 ///
266 /// ```
267 /// # use dashu_base::ParseError;
268 /// # use dashu_float::DBig;
269 /// # use core::str::FromStr;
270 /// use dashu_base::Approximation::*;
271 /// use dashu_float::{Context, round::{mode::HalfAway, Rounding::*}};
272 ///
273 /// let context = Context::<HalfAway>::new(2);
274 /// let a = DBig::from_str("-1.234")?;
275 /// assert_eq!(context.exp(&a.repr(), None), Ok(Inexact(DBig::from_str("0.29")?, NoOp)));
276 /// # Ok::<(), ParseError>(())
277 /// ```
278 #[inline]
279 pub fn exp<const B: Word>(
280 &self,
281 x: &Repr<B>,
282 cache: Option<&mut ConstCache>,
283 ) -> FpResult<FBig<R, B>> {
284 if x.is_infinite() {
285 return Ok(Exact(FBig::new(
286 match x.sign() {
287 Sign::Positive => Repr::infinity(),
288 Sign::Negative => Repr::zero(),
289 },
290 *self,
291 )));
292 }
293 self.exp_internal(x, false, cache)
294 }
295
296 /// Calculate the exponential minus one function (`eˣ-1`) on the floating point number under this context.
297 ///
298 /// # Examples
299 ///
300 /// ```
301 /// # use dashu_base::ParseError;
302 /// # use dashu_float::DBig;
303 /// # use core::str::FromStr;
304 /// use dashu_base::Approximation::*;
305 /// use dashu_float::{Context, round::{mode::HalfAway, Rounding::*}};
306 ///
307 /// let context = Context::<HalfAway>::new(2);
308 /// let a = DBig::from_str("-0.1234")?;
309 /// assert_eq!(context.exp_m1(&a.repr(), None), Ok(Inexact(DBig::from_str("-0.12")?, SubOne)));
310 /// # Ok::<(), ParseError>(())
311 /// ```
312 #[inline]
313 pub fn exp_m1<const B: Word>(
314 &self,
315 x: &Repr<B>,
316 cache: Option<&mut ConstCache>,
317 ) -> FpResult<FBig<R, B>> {
318 if x.is_infinite() {
319 return match x.sign() {
320 Sign::Positive => Ok(Exact(FBig::new(Repr::infinity(), *self))),
321 Sign::Negative => Ok(Exact(-FBig::ONE)), // exp_m1(−∞) = −1
322 };
323 }
324 self.exp_internal(x, true, cache)
325 }
326
327 // TODO: change reduction to (x - s log2) / 2ⁿ, so that the final powering is always base 2, and doesn't depends on powi.
328 // the powering exp(r)^(2ⁿ) could be optimized by noticing (1+x)^2 - 1 = x^2 + 2x
329 // consider this change after having a benchmark
330
331 fn exp_internal<const B: Word>(
332 &self,
333 x: &Repr<B>,
334 minus_one: bool,
335 mut cache: Option<&mut ConstCache>,
336 ) -> FpResult<FBig<R, B>> {
337 assert_finite(x);
338 assert_limited_precision(self.precision);
339 let input_sign = x.sign();
340
341 if x.significand.is_zero() {
342 // exp(±0) = 1; exp_m1(±0) = ±0 (IEEE 754 §9.2.1 preserves the sign of zero)
343 return match minus_one {
344 false => Ok(Exact(FBig::ONE)),
345 true => {
346 let zero = if input_sign == Sign::Negative {
347 FBig::new(Repr::neg_zero(), Context::new(0))
348 } else {
349 FBig::ZERO
350 };
351 Ok(Exact(zero))
352 }
353 };
354 }
355
356 // A simple algorithm:
357 // - let r = (x - s logB) / Bⁿ, where s = floor(x / logB), such that r < B⁻ⁿ.
358 // - if the target precision is p digits, then there're only about p/m terms in Tyler series
359 // - finally, exp(x) = Bˢ * exp(r)^(Bⁿ)
360 // - the optimal n is √p as given by MPFR
361
362 // Maclaurin series: exp(r) = 1 + Σ(rⁱ/i!)
363 // There will be about p/log_B(r) summations when calculating the series, to prevent
364 // loss of significance, we need about log_B(p) guard digits.
365 let series_guard_digits = ceil_usize(self.precision.log2_est() / B.log2_est()) + 2;
366
367 // Reduction power: the series value is later raised to Bⁿ, which amplifies its
368 // relative error by a factor of Bⁿ. So the series (and the squarings) must carry
369 // about n extra base-B digits for the result to come out correct to p digits. We
370 // use 2n for safety — this mirrors MPFR's working precision q = precy + 2·K + …
371 // (K ≈ √precy is MPFR's squaring count, the analogue of our n). The log_B(p)
372 // summation/squaring rounding terms are already covered by series_guard_digits.
373 let n = 1usize << (self.precision.bit_len() / 2);
374 let pow_guard_digits = 2 * n;
375 let work_precision;
376
377 // When minus_one is true and |x| < 1/B, the input is fed into the Maclaurin series without scaling
378 let no_scaling = minus_one && x.log2_est() < -B.log2_est();
379 let (s, n, r) = if no_scaling {
380 // if minus_one is true and x is already small (x < 1/B),
381 // then directly evaluate the Maclaurin series without scaling
382 if x.sign() == Sign::Negative {
383 // extra digits are required to prevent cancellation during the summation
384 work_precision = self.precision + 2 * series_guard_digits;
385 } else {
386 work_precision = self.precision + series_guard_digits;
387 }
388 let context = Context::<R>::new(work_precision);
389 (0, 0, FBig::new(context.repr_round_ref(x).value(), context))
390 } else {
391 work_precision = self.precision + series_guard_digits + pow_guard_digits;
392 let context = Context::<R>::new(work_precision);
393 let x = FBig::new(context.repr_round_ref(x).value(), context);
394 let logb = context.ln_base::<B>(reborrow_cache(&mut cache));
395 let (s, r) = x.div_rem_euclid(logb);
396
397 let s: isize = match s.try_into() {
398 Ok(v) => v,
399 Err(_) => {
400 // |floor(x / ln B)| overflows isize — x is astronomically large, so the
401 // result is an infinity (x → +∞) or underflows to the limit (x → −∞).
402 return if input_sign == Sign::Positive {
403 Err(FpError::Overflow(Sign::Positive))
404 } else if minus_one {
405 Ok(Exact(-FBig::ONE)) // exp_m1(−∞) = −1 (finite)
406 } else {
407 Err(FpError::Underflow(Sign::Positive)) // exp(−∞) = +0
408 };
409 }
410 };
411 (s, n, r)
412 };
413
414 let r = r >> n as isize;
415 let mut factorial = IBig::ONE;
416 let mut pow = r.clone();
417 let mut sum = if no_scaling {
418 r.clone()
419 } else {
420 FBig::ONE + &r
421 };
422
423 let mut k = 2;
424 loop {
425 factorial *= k;
426 pow *= &r;
427
428 let increase = &pow / &factorial;
429 if increase.abs_cmp(&sum.sub_ulp()).is_le() {
430 break;
431 }
432 sum += increase;
433 k += 1;
434 }
435
436 if no_scaling {
437 Ok(sum.with_precision(self.precision))
438 } else if minus_one {
439 // Power at the series' working precision (it already carries the 2n guard
440 // digits that the Bⁿ powering amplifies away). The final "−1" can cancel at
441 // most ~1 leading digit here (the |x| < 1/B case is handled by no_scaling),
442 // which the same guard digits comfortably absorb.
443 let pow_ctx = Context::<R>::new(work_precision);
444 let v = pow_ctx.unwrap_fp(pow_ctx.powi(sum.repr(), Repr::<B>::BASE.pow(n).into()));
445 Ok(((v << s) - FBig::ONE).with_precision(self.precision))
446 } else {
447 let pow_ctx = Context::<R>::new(work_precision);
448 let v = pow_ctx.unwrap_fp(pow_ctx.powi(sum.repr(), Repr::<B>::BASE.pow(n).into()));
449 Ok((v << s).with_precision(self.precision))
450 }
451 }
452}
453
454#[cfg(test)]
455mod tests {
456 use super::*;
457 use crate::round::mode;
458
459 #[test]
460 fn test_exp_overflow_is_infinity() {
461 let ctx = Context::<mode::HalfEven>::new(53);
462 // exp(huge) overflows the isize exponent range -> Overflow at Context level.
463 // Need x large enough that floor(x/ln2) > isize::MAX, i.e. x > ~2^62.5.
464 let huge = Repr::new(IBig::from(1) << 63, 0);
465 assert_eq!(ctx.exp::<2>(&huge, None), Err(FpError::Overflow(Sign::Positive)));
466
467 // exp(huge negative) underflows to +0
468 let neg = Repr::new(-(IBig::from(1) << 63), 0);
469 assert_eq!(ctx.exp::<2>(&neg, None), Err(FpError::Underflow(Sign::Positive)));
470
471 // exp_m1(huge negative) -> -1 (a finite value, not an error)
472 let m1 = ctx.exp_m1::<2>(&neg, None).unwrap().value();
473 assert_eq!(m1, -FBig::<mode::HalfEven>::ONE);
474 }
475
476 #[test]
477 fn test_powf_zero_base() {
478 use crate::DBig;
479 // powf with a float exponent returns the *positive* result on a zero base
480 // (matching the common float-pow convention); use powi for the signed result.
481 let ctx = Context::<mode::HalfEven>::new(53);
482 // powf(-0, 2.0) = +0 (NOT -0)
483 let r = ctx
484 .powf::<2>(&Repr::<2>::neg_zero(), &Repr::new(2.into(), 0), None)
485 .unwrap()
486 .value();
487 assert!(r.repr().is_pos_zero(), "expected +0");
488 assert!(!r.repr().is_neg_zero(), "powf(-0, x) should be +0, not -0");
489 // powf(0, -1) = +inf
490 let r = ctx
491 .powf::<2>(&Repr::<2>::zero(), &Repr::new((-1i32).into(), 0), None)
492 .unwrap()
493 .value();
494 assert!(r.repr().is_infinite());
495 assert_eq!(r.repr().sign(), Sign::Positive);
496 // powi(-0, 3) = -0 (the sign-correct, integer-exponent variant)
497 let r = ctx
498 .powi::<2>(&Repr::<2>::neg_zero(), 3.into())
499 .unwrap()
500 .value();
501 assert!(r.repr().is_neg_zero());
502 let _ = DBig::ZERO;
503 }
504}