Skip to main content

dashu_cmplx/
exp.rs

1//! Complex exponential and powers.
2//!
3//! * [`Context::exp`] / [`CBig::exp`]: `exp(x+iy) = e^x·(cos y + i sin y)`.
4//! * [`Context::powi`] / [`CBig::powi`]: integer exponent via repeated squaring (branch-cut-free,
5//!   cheaper than `exp(n·log z)`).
6//! * [`Context::powf`] / [`CBig::powf`]: `exp(w·log z)` on the principal branch.
7//!
8//! Mirroring `dashu-float`, the power family lives alongside `exp` in a single module.
9
10use crate::cbig::CBig;
11use crate::repr::{combine_parts, exact, reborrow_cache, riemann, CfpResult, Context};
12use dashu_base::Approximation::*;
13use dashu_base::{BitTest, Sign};
14use dashu_float::round::Round;
15use dashu_float::{ConstCache, FBig, FpError};
16use dashu_int::{IBig, Word};
17
18/// Guard digits (base-B) for `exp`. Composes a real `exp`, a `sin_cos`, and two products.
19const EXP_GUARD: usize = 14;
20
21/// Guard digits (base-B) for `powf`. Composes `log`, a complex product, and `exp` — the
22/// cancellation-prone path, so a larger guard than the bare arithmetic ops.
23const POWF_GUARD: usize = 22;
24
25impl<R: Round> Context<R> {
26    /// Complex exponential under this context (context layer). Reuses `dashu-float`'s `exp` and
27    /// `sin_cos`; the cache is threaded into both (the convenience layer passes `None`).
28    ///
29    /// Special values: `exp(0) = 1`; `exp(+inf + i·finite) = +∞` (Riemann point);
30    /// `exp(-inf + i·finite) = 0`; an infinite imaginary part makes the trig undefined
31    /// (`Indeterminate`).
32    pub fn exp<const B: Word>(
33        &self,
34        z: &CBig<R, B>,
35        mut cache: Option<&mut ConstCache>,
36    ) -> CfpResult<R, B> {
37        if z.is_zero() {
38            return Ok(exact(FBig::ONE, FBig::ZERO));
39        }
40        if z.is_infinite() {
41            if z.im().is_infinite() {
42                return Err(FpError::Indeterminate); // cos/sin(±inf) undefined
43            }
44            return if z.re().sign() == Sign::Positive {
45                Ok(riemann(*self))
46            } else {
47                Ok(exact(FBig::ZERO, FBig::ZERO))
48            };
49        }
50
51        let gctx = self.guard(EXP_GUARD);
52        let p = self.precision();
53        let ex = gctx.exp(z.re(), reborrow_cache(&mut cache))?.value();
54        let (sin_y, cos_y) = gctx.sin_cos(z.im(), reborrow_cache(&mut cache));
55        let cos_y = cos_y?.value();
56        let sin_y = sin_y?.value();
57        let re = gctx.mul(ex.repr(), cos_y.repr())?.value().with_precision(p);
58        let im = gctx.mul(ex.repr(), sin_y.repr())?.value().with_precision(p);
59        Ok(combine_parts(re, im))
60    }
61
62    /// Raise a complex number to an integer power under this context (context layer), via repeated
63    /// squaring (branch-cut-free, cheaper than `exp(n·log z)`). No cache.
64    ///
65    /// `powi(z, 0) = 1`; a negative exponent computes `powi(z, |n|)` then inverts.
66    pub fn powi<const B: Word>(&self, z: &CBig<R, B>, exp: IBig) -> CfpResult<R, B> {
67        let (sign, n) = exp.into_parts();
68        if n.is_zero() {
69            return Ok(Exact(CBig::ONE));
70        }
71        let negative = sign == Sign::Negative;
72        let bitlen = n.bit_len();
73        // left-to-right binary exponentiation, starting from the leading set bit
74        let mut acc = z.clone();
75        for i in (0..bitlen - 1).rev() {
76            acc = self.sqr(&acc)?.value();
77            if n.bit(i) {
78                acc = self.mul(&acc, z)?.value();
79            }
80        }
81        // The intermediate rounding flags are folded away (the value is near-correctly rounded);
82        // for a negative exponent the final `inv` carries its own flags.
83        if negative {
84            self.inv(&acc)
85        } else {
86            Ok(Exact(acc))
87        }
88    }
89
90    /// Raise `base` to a complex power under this context (context layer): `exp(w·log base)` on the
91    /// principal branch, evaluated at `p + POWF_GUARD` and re-rounded. `powf(0, 0) = 1` (matching
92    /// `FBig::powf`).
93    ///
94    /// Unlike `exp`, this drives whole-[`CBig`] operations (`log`/`mul`/`exp`), so it builds a
95    /// complex working [`Context`] at guard precision directly rather than the float
96    /// `Context::guard` (which yields a `FloatCtxt` for per-part math).
97    pub fn powf<const B: Word>(
98        &self,
99        base: &CBig<R, B>,
100        w: &CBig<R, B>,
101        mut cache: Option<&mut ConstCache>,
102    ) -> CfpResult<R, B> {
103        if w.is_zero() {
104            return Ok(Exact(CBig::ONE)); // powf(z, 0) = 1, incl. powf(0, 0)
105        }
106        let gctx = Context::new(self.precision() + POWF_GUARD);
107        let log_z = gctx.log(base, reborrow_cache(&mut cache))?.value();
108        let wlogz = gctx.mul(w, &log_z)?.value();
109        let hi = gctx.exp(&wlogz, reborrow_cache(&mut cache))?.value();
110        let p = self.precision();
111        let (re, im) = hi.into_parts();
112        Ok(combine_parts(re.with_precision(p), im.with_precision(p)))
113    }
114}
115
116impl<R: Round, const B: Word> CBig<R, B> {
117    /// Complex exponential `e^z` (convenience layer).
118    ///
119    /// # Panics
120    ///
121    /// Panics if the precision is unlimited or on an indeterminate special value.
122    #[inline]
123    pub fn exp(&self) -> Self {
124        self.context().unwrap_cfp(self.context().exp(self, None))
125    }
126
127    /// Integer power (convenience layer).
128    ///
129    /// # Panics
130    ///
131    /// Panics on an indeterminate / out-of-domain result (e.g. `0⁻¹`).
132    #[inline]
133    pub fn powi(&self, exp: IBig) -> Self {
134        self.context().unwrap_cfp(self.context().powi(self, exp))
135    }
136
137    /// Complex power `self^w` (convenience layer).
138    ///
139    /// `powf(z, 0) = 1` (including `powf(0, 0) = 1`), matching `FBig::powf` and the real `0⁰ = 1`
140    /// convention.
141    #[inline]
142    pub fn powf(&self, w: &Self) -> Self {
143        self.context()
144            .unwrap_cfp(self.context().powf(self, w, None))
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151    use dashu_float::round::mode;
152
153    type C = CBig<mode::HalfAway, 10>;
154    type F = FBig<mode::HalfAway, 10>;
155
156    fn c(re: i32, im: i32) -> C {
157        let mk = |v: i32| -> F { F::from(v).with_precision(53).value() };
158        CBig::from_parts(mk(re), mk(im))
159    }
160
161    #[test]
162    fn exp_zero_is_one() {
163        assert!(C::ZERO.exp() == C::ONE);
164    }
165
166    #[test]
167    fn exp_one_is_e() {
168        // exp(1+0i) = e ≈ 2.71828…; check 2 < e < 3 via the real part
169        let e = C::ONE.exp();
170        let (re, _im) = e.into_parts();
171        assert!(re > F::from(2));
172        assert!(re < F::from(3));
173    }
174
175    #[test]
176    fn exp_pi_i_is_neg_one() {
177        use dashu_base::{Abs, AbsOrd};
178        // exp(iπ) = -1 + i·0; use a π literal precise enough that sin(π_approx) ≈ 0
179        let pi = F::from_parts(31415926535897932i64.into(), -16)
180            .with_precision(60)
181            .value();
182        let z = CBig::from_parts(F::ZERO, pi);
183        let (re, im) = z.exp().into_parts();
184        let re_err = (re + F::ONE).abs();
185        let tol = F::from_parts(1.into(), -12);
186        assert!(re_err.abs_cmp(&tol).is_le());
187        assert!(im.abs_cmp(&tol).is_le());
188    }
189
190    #[test]
191    fn exp_pos_infinity_is_riemann() {
192        let inf = CBig::from(F::INFINITY);
193        let r = inf.exp();
194        assert!(r.re().is_infinite());
195        assert!(r.im().is_pos_zero());
196    }
197
198    #[test]
199    fn powi_zero_is_one() {
200        assert!(c(3, 4).powi(0.into()) == C::ONE);
201    }
202
203    #[test]
204    fn powi_one_is_self() {
205        let z = c(3, 4);
206        assert!(z.powi(1.into()) == z);
207    }
208
209    #[test]
210    fn powi_two_is_sqr() {
211        let z = c(1, 2);
212        assert!(z.powi(2.into()) == z.sqr());
213    }
214
215    #[test]
216    fn powi_negative_is_inv() {
217        // z^(-1) = inv(z); z · z^(-1) = 1
218        let z = c(3, 4);
219        let r = z.powi((-1).into());
220        let one = &z * &r;
221        assert!(one == C::ONE);
222    }
223
224    #[test]
225    fn powf_zero_exponent_is_one() {
226        // powf(z, 0) = 1, including powf(0, 0)
227        assert!(c(3, 4).powf(&C::ZERO) == C::ONE);
228        assert!(C::ZERO.powf(&C::ZERO) == C::ONE);
229    }
230
231    #[test]
232    fn powf_one_exponent_is_self() {
233        let z = c(2, 1);
234        assert!(z.powf(&C::ONE) == z);
235    }
236}