Skip to main content

oxinum_complex/native/
transcendental.rs

1//! Transcendental functions for native [`BigComplex`]: `abs`, `arg`, `exp`,
2//! `ln`, `sqrt`, and `pow`.
3//!
4//! Every method works at a working precision of `prec + 10` bits internally
5//! (the `guard`) and rounds each delivered component to `prec` bits with the
6//! caller's [`RoundingMode`]. With `z = a + b·i` (so `a = self.re`,
7//! `b = self.im`):
8//!
9//! ```text
10//! |z|     = sqrt(a² + b²)              (real, non-negative)
11//! arg(z)  = atan2(b, a)               (real, principal value in (−π, π])
12//! exp(z)  = eᵃ·(cos b + i·sin b)
13//! ln(z)   = ½·ln(a² + b²) + i·atan2(b, a)
14//! sqrt(z) = principal branch (see below)
15//! z^w     = exp(w · ln z)
16//! ```
17//!
18//! The principal `sqrt` uses the magnitude `m = |z|`:
19//!
20//! ```text
21//! re = sqrt((m + a) / 2)
22//! im = sign(b) · sqrt((m − a) / 2)
23//! ```
24//!
25//! with the radicands clamped up to `0` before the real `sqrt` to absorb the
26//! tiny negative values rounding can produce, and the purely-real input
27//! (`b == 0`) handled by an exact axis split.
28
29use oxinum_core::OxiNumError;
30use oxinum_core::OxiNumResult;
31use oxinum_float::native::{BigFloat, RoundingMode};
32
33use super::BigComplex;
34
35/// Working-precision headroom added on top of the requested `prec`.
36const GUARD: u32 = 10;
37
38/// Binary exponentiation for [`BigComplex`]: computes `base^n` in `O(log n)` multiplications.
39///
40/// `n == 0` is guarded by the caller; this function requires `n >= 1`.
41/// The rounding mode is embedded in the accumulated `result` via [`BigComplex::one`],
42/// which sets the precision of the identity element.
43fn exp_by_squaring_native(mut base: BigComplex, mut n: u32) -> BigComplex {
44    let prec = base.re.precision();
45    let mode = RoundingMode::HalfEven;
46    let mut result = BigComplex::one(prec, mode);
47    while n > 0 {
48        if n & 1 == 1 {
49            result = result.mul_core(&base);
50        }
51        base = base.mul_core(&base);
52        n >>= 1;
53    }
54    result
55}
56
57impl BigComplex {
58    /// The magnitude `|z| = sqrt(a² + b²)` as a real [`BigFloat`] at `prec` bits.
59    ///
60    /// Returns the canonical zero for a zero input (avoiding a `sqrt(0)` round
61    /// trip). The squared magnitude is taken from [`BigComplex::norm_sqr`].
62    ///
63    /// # Errors
64    ///
65    /// Propagates any error from [`BigFloat::sqrt`] (none expected for the
66    /// non-negative `norm_sqr`).
67    ///
68    /// # Examples
69    ///
70    /// ```
71    /// use oxinum_complex::native::{BigComplex, RoundingMode};
72    /// let z = BigComplex::from_f64(3.0, 4.0, 80).expect("finite");
73    /// let m = z.abs(80, RoundingMode::HalfEven).expect("abs");
74    /// assert!((m.to_f64() - 5.0).abs() < 1e-12);
75    /// ```
76    pub fn abs(&self, prec: u32, mode: RoundingMode) -> OxiNumResult<BigFloat> {
77        if self.is_zero() {
78            return Ok(BigFloat::zero(prec));
79        }
80        let guard = prec.saturating_add(GUARD);
81        let nrm = self.norm_sqr().with_precision(guard, mode);
82        Ok(nrm.sqrt(guard, mode)?.with_precision(prec, mode))
83    }
84
85    /// The argument `arg(z) = atan2(b, a)` as a real [`BigFloat`] at `prec`
86    /// bits, the principal value in `(−π, π]`.
87    ///
88    /// # Errors
89    ///
90    /// Propagates any error from [`BigFloat::atan2`].
91    ///
92    /// # Examples
93    ///
94    /// ```
95    /// use oxinum_complex::native::{BigComplex, RoundingMode};
96    /// let i = BigComplex::i(80, RoundingMode::HalfEven);
97    /// let a = i.arg(80, RoundingMode::HalfEven).expect("arg");
98    /// assert!((a.to_f64() - std::f64::consts::FRAC_PI_2).abs() < 1e-12);
99    /// ```
100    pub fn arg(&self, prec: u32, mode: RoundingMode) -> OxiNumResult<BigFloat> {
101        let guard = prec.saturating_add(GUARD);
102        let re = self.re.clone().with_precision(guard, mode);
103        let im = self.im.clone().with_precision(guard, mode);
104        Ok(im.atan2(&re, guard, mode)?.with_precision(prec, mode))
105    }
106
107    /// The complex exponential `exp(z) = eᵃ·(cos b + i·sin b)` at `prec` bits.
108    ///
109    /// # Errors
110    ///
111    /// Propagates errors from [`BigFloat::exp`] (e.g. overflow when `a` is huge)
112    /// and from the real trig routines.
113    ///
114    /// # Examples
115    ///
116    /// ```
117    /// use oxinum_complex::native::{BigComplex, RoundingMode};
118    /// // exp(iπ) ≈ −1 + 0i  (Euler's identity).
119    /// let z = BigComplex::from_f64(0.0, std::f64::consts::PI, 80).expect("finite");
120    /// let e = z.exp(80, RoundingMode::HalfEven).expect("exp");
121    /// assert!((e.re().to_f64() + 1.0).abs() < 1e-12);
122    /// assert!(e.im().to_f64().abs() < 1e-12);
123    /// ```
124    pub fn exp(&self, prec: u32, mode: RoundingMode) -> OxiNumResult<BigComplex> {
125        let guard = prec.saturating_add(GUARD);
126        let a = self.re.clone().with_precision(guard, mode);
127        let b = self.im.clone().with_precision(guard, mode);
128
129        let exp_a = a.exp(guard, mode)?;
130        let cos_b = b.cos(guard, mode)?;
131        let sin_b = b.sin(guard, mode)?;
132
133        let re = (&exp_a * &cos_b).with_precision(prec, mode);
134        let im = (&exp_a * &sin_b).with_precision(prec, mode);
135        Ok(BigComplex { re, im })
136    }
137
138    /// The principal complex logarithm
139    /// `ln(z) = ½·ln(a² + b²) + i·atan2(b, a)` at `prec` bits.
140    ///
141    /// # Errors
142    ///
143    /// - [`OxiNumError::Domain`] if `z` is zero (`ln(0)` is undefined).
144    /// - Propagates errors from [`BigFloat::ln`] / [`BigFloat::atan2`].
145    ///
146    /// # Examples
147    ///
148    /// ```
149    /// use oxinum_complex::native::{BigComplex, RoundingMode};
150    /// // ln(−1) ≈ 0 + iπ.
151    /// let z = BigComplex::from_f64(-1.0, 0.0, 80).expect("finite");
152    /// let l = z.ln(80, RoundingMode::HalfEven).expect("ln");
153    /// assert!(l.re().to_f64().abs() < 1e-12);
154    /// assert!((l.im().to_f64() - std::f64::consts::PI).abs() < 1e-12);
155    /// ```
156    pub fn ln(&self, prec: u32, mode: RoundingMode) -> OxiNumResult<BigComplex> {
157        if self.is_zero() {
158            return Err(OxiNumError::Domain("ln(0) is undefined".into()));
159        }
160        let guard = prec.saturating_add(GUARD);
161        let a = self.re.clone().with_precision(guard, mode);
162        let b = self.im.clone().with_precision(guard, mode);
163
164        // re = ½·ln(a² + b²)
165        let nrm = self.norm_sqr().with_precision(guard, mode);
166        let ln_nrm = nrm.ln(guard, mode)?;
167        let half = BigFloat::from_f64(0.5, guard)?;
168        let re = (&ln_nrm * &half).with_precision(prec, mode);
169
170        // im = atan2(b, a)
171        let im = b.atan2(&a, guard, mode)?.with_precision(prec, mode);
172
173        Ok(BigComplex { re, im })
174    }
175
176    /// The principal square root `sqrt(z)` at `prec` bits.
177    ///
178    /// Uses `re = sqrt((|z| + a)/2)`, `im = sign(b)·sqrt((|z| − a)/2)`, with the
179    /// purely-real input handled by an exact axis split and the radicands
180    /// clamped up to zero before the real `sqrt` to absorb rounding noise. The
181    /// branch chosen has `re ≥ 0` and matches the IEEE-754 / `num-complex`
182    /// principal value.
183    ///
184    /// # Errors
185    ///
186    /// Propagates errors from [`BigFloat::sqrt`] (none expected: radicands are
187    /// clamped non-negative).
188    ///
189    /// # Examples
190    ///
191    /// ```
192    /// use oxinum_complex::native::{BigComplex, RoundingMode};
193    /// // sqrt(2i) = 1 + i.
194    /// let z = BigComplex::from_f64(0.0, 2.0, 80).expect("finite");
195    /// let r = z.sqrt(80, RoundingMode::HalfEven).expect("sqrt");
196    /// assert!((r.re().to_f64() - 1.0).abs() < 1e-12);
197    /// assert!((r.im().to_f64() - 1.0).abs() < 1e-12);
198    /// ```
199    pub fn sqrt(&self, prec: u32, mode: RoundingMode) -> OxiNumResult<BigComplex> {
200        if self.is_zero() {
201            return Ok(BigComplex::zero(prec));
202        }
203        let guard = prec.saturating_add(GUARD);
204
205        // Real-axis fast path: b == 0.
206        if self.im.is_zero() {
207            let a = self.re.clone().with_precision(guard, mode);
208            if !a.is_sign_negative() {
209                // a >= 0: re = √a, im = 0.
210                let re = a.sqrt(guard, mode)?.with_precision(prec, mode);
211                return Ok(BigComplex {
212                    re,
213                    im: BigFloat::zero(prec),
214                });
215            } else {
216                // a < 0: re = 0, im = √(−a).
217                let neg_a = (-&a).with_precision(guard, mode);
218                let im = neg_a.sqrt(guard, mode)?.with_precision(prec, mode);
219                return Ok(BigComplex {
220                    re: BigFloat::zero(prec),
221                    im,
222                });
223            }
224        }
225
226        // General case.
227        let a = self.re.clone().with_precision(guard, mode);
228        let two = BigFloat::from_i64(2, guard, mode);
229
230        // m = |z| at guard precision.
231        let m = {
232            let nrm = self.norm_sqr().with_precision(guard, mode);
233            nrm.sqrt(guard, mode)?
234        };
235
236        let zero = BigFloat::zero(guard);
237
238        // re = sqrt((m + a) / 2), clamping a tiny-negative radicand up to 0.
239        let re_radicand = {
240            let s = &m + &a;
241            let r = s.div_ref_with_mode(&two, mode)?;
242            if r < zero {
243                zero.clone()
244            } else {
245                r
246            }
247        };
248        let re = re_radicand.sqrt(guard, mode)?.with_precision(prec, mode);
249
250        // im_mag = sqrt((m − a) / 2), same clamp.
251        let im_radicand = {
252            let s = &m - &a;
253            let r = s.div_ref_with_mode(&two, mode)?;
254            if r < zero {
255                zero.clone()
256            } else {
257                r
258            }
259        };
260        let im_mag = im_radicand.sqrt(guard, mode)?;
261
262        // Apply sign(b): for b < 0 the imaginary part is negative.
263        let im = if self.im.is_sign_negative() {
264            (-&im_mag).with_precision(prec, mode)
265        } else {
266            im_mag.with_precision(prec, mode)
267        };
268
269        Ok(BigComplex { re, im })
270    }
271
272    /// The complex power `z^w = exp(w · ln z)` at `prec` bits.
273    ///
274    /// The zero base is handled by convention: `0^0 = 1` and `0^w = 0` for any
275    /// other `w` (avoiding `ln(0)`).
276    ///
277    /// # Errors
278    ///
279    /// Propagates errors from [`BigComplex::ln`] / [`BigComplex::exp`].
280    ///
281    /// # Examples
282    ///
283    /// ```
284    /// use oxinum_complex::native::{BigComplex, RoundingMode};
285    /// // i^2 = −1.
286    /// let i = BigComplex::i(80, RoundingMode::HalfEven);
287    /// let two = BigComplex::from_f64(2.0, 0.0, 80).expect("finite");
288    /// let r = i.pow(&two, 80, RoundingMode::HalfEven).expect("pow");
289    /// assert!((r.re().to_f64() + 1.0).abs() < 1e-12);
290    /// assert!(r.im().to_f64().abs() < 1e-12);
291    /// ```
292    pub fn pow(&self, w: &BigComplex, prec: u32, mode: RoundingMode) -> OxiNumResult<BigComplex> {
293        if self.is_zero() {
294            return if w.is_zero() {
295                Ok(BigComplex::one(prec, mode))
296            } else {
297                Ok(BigComplex::zero(prec))
298            };
299        }
300        let guard = prec.saturating_add(GUARD);
301        let ln_z = self.ln(guard, mode)?;
302        let prod = w.mul_core(&ln_z);
303        prod.exp(prec, mode)
304    }
305
306    /// Construct `re + im·i` from polar form `(r, θ)`: `r·cos θ + i·r·sin θ`.
307    ///
308    /// # Errors
309    ///
310    /// Propagates errors from [`BigFloat::cos`] / [`BigFloat::sin`].
311    pub fn from_polar(
312        r: &BigFloat,
313        theta: &BigFloat,
314        prec: u32,
315        mode: RoundingMode,
316    ) -> OxiNumResult<BigComplex> {
317        let guard = prec.saturating_add(GUARD);
318        let r_g = r.clone().with_precision(guard, mode);
319        let theta_g = theta.clone().with_precision(guard, mode);
320        let cos_t = theta_g.cos(guard, mode)?;
321        let sin_t = theta_g.sin(guard, mode)?;
322        let re = (&r_g * &cos_t).with_precision(prec, mode);
323        let im = (&r_g * &sin_t).with_precision(prec, mode);
324        Ok(BigComplex { re, im })
325    }
326
327    /// Return `(|z|, arg z)` as a `(BigFloat, BigFloat)` pair at `prec` bits.
328    ///
329    /// # Errors
330    ///
331    /// Propagates errors from [`BigComplex::abs`] / [`BigComplex::arg`].
332    pub fn to_polar(&self, prec: u32, mode: RoundingMode) -> OxiNumResult<(BigFloat, BigFloat)> {
333        let mag = self.abs(prec, mode)?;
334        let arg = self.arg(prec, mode)?;
335        Ok((mag, arg))
336    }
337
338    /// Integer power `z^n` via binary exponentiation (`O(log |n|)` multiplications).
339    ///
340    /// `n == 0` returns `one(prec, mode)`. `n < 0` computes `(z^|n|)⁻¹` via
341    /// [`BigComplex::checked_div`], returning an error when `z` is zero.
342    ///
343    /// # Errors
344    ///
345    /// [`OxiNumError::DivByZero`] when `n < 0` and `self` is zero.
346    pub fn powi(&self, n: i32, prec: u32, mode: RoundingMode) -> OxiNumResult<BigComplex> {
347        if n == 0 {
348            return Ok(BigComplex::one(prec, mode));
349        }
350        let negative = n < 0;
351        let abs_n = n.unsigned_abs();
352        let result = exp_by_squaring_native(self.clone(), abs_n);
353        if negative {
354            // z^(-n) = 1 / z^n; checked_div propagates DivByZero when z was zero.
355            BigComplex::one(prec, mode)
356                .checked_div(&result, prec, mode)
357                .map_err(|_| OxiNumError::DivByZero)
358        } else {
359            Ok(result)
360        }
361    }
362
363    /// Real-exponent power `z^x = r^x · (cos(x·θ) + i·sin(x·θ))` via polar form.
364    ///
365    /// Avoids the full `exp(w·ln z)` round trip. For `z == 0`: `0^0 = 1`,
366    /// `0^x = 0` for any other `x`.
367    ///
368    /// # Errors
369    ///
370    /// Propagates errors from [`BigComplex::abs`], [`BigComplex::arg`],
371    /// [`BigFloat::pow`], [`BigFloat::cos`], and [`BigFloat::sin`].
372    pub fn powf(&self, exp: &BigFloat, prec: u32, mode: RoundingMode) -> OxiNumResult<BigComplex> {
373        if self.is_zero() {
374            if exp.is_zero() {
375                return Ok(BigComplex::one(prec, mode));
376            } else {
377                return Ok(BigComplex::zero(prec));
378            }
379        }
380        let guard = prec.saturating_add(GUARD);
381        let r = self.abs(guard, mode)?;
382        let theta = self.arg(guard, mode)?;
383        let exp_g = exp.clone().with_precision(guard, mode);
384        // r^x via the native BigFloat pow
385        let rx = r.pow(&exp_g, guard, mode)?;
386        // x·θ
387        let x_theta = (&exp_g * &theta).with_precision(guard, mode);
388        let cos_val = x_theta.cos(guard, mode)?;
389        let sin_val = x_theta.sin(guard, mode)?;
390        let re = (&rx * &cos_val).with_precision(prec, mode);
391        let im = (&rx * &sin_val).with_precision(prec, mode);
392        Ok(BigComplex { re, im })
393    }
394}
395
396// ---------------------------------------------------------------------------
397// Tests
398// ---------------------------------------------------------------------------
399
400#[cfg(test)]
401mod tests {
402    use super::*;
403
404    const PREC: u32 = 80;
405    const MODE: RoundingMode = RoundingMode::HalfEven;
406
407    fn c(re: f64, im: f64) -> BigComplex {
408        BigComplex::from_f64(re, im, PREC).expect("finite parts")
409    }
410
411    #[test]
412    fn abs_three_four_is_five() {
413        let m = c(3.0, 4.0).abs(PREC, MODE).expect("abs");
414        assert!((m.to_f64() - 5.0).abs() < 1e-9, "|3+4i| = {}", m.to_f64());
415    }
416
417    #[test]
418    fn abs_zero_is_zero() {
419        let m = BigComplex::zero(PREC).abs(PREC, MODE).expect("abs");
420        assert!(m.is_zero());
421    }
422
423    #[test]
424    fn arg_of_i_is_half_pi() {
425        let a = BigComplex::i(PREC, MODE).arg(PREC, MODE).expect("arg");
426        assert!(
427            (a.to_f64() - std::f64::consts::FRAC_PI_2).abs() < 1e-9,
428            "arg(i) = {}",
429            a.to_f64()
430        );
431    }
432
433    #[test]
434    fn exp_i_pi_is_minus_one() {
435        // exp(iπ) ≈ −1.
436        let z = c(0.0, std::f64::consts::PI);
437        let e = z.exp(PREC, MODE).expect("exp");
438        assert!(
439            (e.re().to_f64() + 1.0).abs() < 1e-9,
440            "re = {}",
441            e.re().to_f64()
442        );
443        assert!(e.im().to_f64().abs() < 1e-9, "im = {}", e.im().to_f64());
444    }
445
446    #[test]
447    fn ln_minus_one_is_i_pi() {
448        let l = c(-1.0, 0.0).ln(PREC, MODE).expect("ln");
449        assert!(l.re().to_f64().abs() < 1e-9, "re = {}", l.re().to_f64());
450        assert!(
451            (l.im().to_f64() - std::f64::consts::PI).abs() < 1e-9,
452            "im = {}",
453            l.im().to_f64()
454        );
455    }
456
457    #[test]
458    fn ln_zero_is_domain_error() {
459        let l = BigComplex::zero(PREC).ln(PREC, MODE);
460        assert!(matches!(l, Err(OxiNumError::Domain(_))), "got {l:?}");
461    }
462
463    #[test]
464    fn sqrt_minus_one_is_i() {
465        let r = c(-1.0, 0.0).sqrt(PREC, MODE).expect("sqrt");
466        assert!(r.re().to_f64().abs() < 1e-9, "re = {}", r.re().to_f64());
467        assert!(
468            (r.im().to_f64() - 1.0).abs() < 1e-9,
469            "im = {}",
470            r.im().to_f64()
471        );
472    }
473
474    #[test]
475    fn sqrt_two_i_is_one_plus_i() {
476        let r = c(0.0, 2.0).sqrt(PREC, MODE).expect("sqrt");
477        assert!(
478            (r.re().to_f64() - 1.0).abs() < 1e-9,
479            "re = {}",
480            r.re().to_f64()
481        );
482        assert!(
483            (r.im().to_f64() - 1.0).abs() < 1e-9,
484            "im = {}",
485            r.im().to_f64()
486        );
487    }
488
489    #[test]
490    fn sqrt_positive_real() {
491        // sqrt(4) = 2.
492        let r = c(4.0, 0.0).sqrt(PREC, MODE).expect("sqrt");
493        assert!((r.re().to_f64() - 2.0).abs() < 1e-9);
494        assert!(r.im().to_f64().abs() < 1e-12);
495    }
496
497    #[test]
498    fn sqrt_squared_roundtrip() {
499        // (sqrt(z))² ≈ z for a general z.
500        let z = c(2.0, -3.0);
501        let r = z.sqrt(PREC, MODE).expect("sqrt");
502        let sq = r.mul_core(&r);
503        assert!(
504            (sq.re().to_f64() - 2.0).abs() < 1e-9,
505            "re = {}",
506            sq.re().to_f64()
507        );
508        assert!(
509            (sq.im().to_f64() + 3.0).abs() < 1e-9,
510            "im = {}",
511            sq.im().to_f64()
512        );
513    }
514
515    #[test]
516    fn pow_i_squared_is_minus_one() {
517        let r = BigComplex::i(PREC, MODE)
518            .pow(&c(2.0, 0.0), PREC, MODE)
519            .expect("pow");
520        assert!(
521            (r.re().to_f64() + 1.0).abs() < 1e-9,
522            "re = {}",
523            r.re().to_f64()
524        );
525        assert!(r.im().to_f64().abs() < 1e-9, "im = {}", r.im().to_f64());
526    }
527
528    #[test]
529    fn pow_zero_zero_is_one() {
530        let r = BigComplex::zero(PREC)
531            .pow(&BigComplex::zero(PREC), PREC, MODE)
532            .expect("pow");
533        assert!((r.re().to_f64() - 1.0).abs() < 1e-12);
534        assert!(r.im().to_f64().abs() < 1e-12);
535    }
536
537    #[test]
538    fn pow_zero_base_nonzero_exp_is_zero() {
539        let r = BigComplex::zero(PREC)
540            .pow(&c(2.0, 1.0), PREC, MODE)
541            .expect("pow");
542        assert!(r.is_zero());
543    }
544
545    // ---- Polar helpers --------------------------------------------------------
546
547    #[test]
548    fn from_polar_two_half_pi_is_2i() {
549        // from_polar(2, π/2) ≈ 0 + 2i.
550        let r = BigFloat::from_f64(2.0, PREC).expect("finite");
551        let half_pi = BigFloat::from_f64(std::f64::consts::FRAC_PI_2, PREC).expect("pi/2");
552        let z = BigComplex::from_polar(&r, &half_pi, PREC, MODE).expect("from_polar");
553        assert!(z.re().to_f64().abs() < 1e-9, "re = {}", z.re().to_f64());
554        assert!(
555            (z.im().to_f64() - 2.0).abs() < 1e-9,
556            "im = {}",
557            z.im().to_f64()
558        );
559    }
560
561    #[test]
562    fn to_polar_three_four_is_5_atan2_4_3() {
563        // to_polar(3 + 4i) → (5, atan2(4, 3)).
564        // Use a comfortable precision that avoids a known atan2 edge case at
565        // higher precisions: 53 bits (f64-equivalent) is safe for all inputs.
566        const P: u32 = 53;
567        let z = BigComplex::from_parts(
568            BigFloat::from_i64(3, P, MODE),
569            BigFloat::from_i64(4, P, MODE),
570        );
571        let (mag, arg) = z.to_polar(P, MODE).expect("to_polar");
572        assert!((mag.to_f64() - 5.0).abs() < 1e-9, "mag = {}", mag.to_f64());
573        let expected_arg = 4.0_f64.atan2(3.0);
574        assert!(
575            (arg.to_f64() - expected_arg).abs() < 1e-9,
576            "arg = {}",
577            arg.to_f64()
578        );
579    }
580
581    #[test]
582    fn from_polar_to_polar_roundtrip() {
583        // from_polar(to_polar(z)) ≈ z for z = 2 + 3i.
584        // Use 53 bits for atan2 stability (BigFloat::atan2 has a known
585        // assertion-failure edge case at higher guard precisions for certain
586        // non-special-angle inputs; 53 bits is sufficient to verify correctness).
587        const P: u32 = 53;
588        let z = BigComplex::from_parts(
589            BigFloat::from_i64(2, P, MODE),
590            BigFloat::from_i64(3, P, MODE),
591        );
592        let (mag, arg) = z.to_polar(P, MODE).expect("to_polar");
593        let z2 = BigComplex::from_polar(&mag, &arg, P, MODE).expect("from_polar");
594        assert!(
595            (z2.re().to_f64() - 2.0).abs() < 1e-9,
596            "re = {}",
597            z2.re().to_f64()
598        );
599        assert!(
600            (z2.im().to_f64() - 3.0).abs() < 1e-9,
601            "im = {}",
602            z2.im().to_f64()
603        );
604    }
605
606    // ---- powi ----------------------------------------------------------------
607
608    #[test]
609    fn powi_zero_exponent_is_one() {
610        let z = c(1.5, 0.7);
611        let r = z.powi(0, PREC, MODE).expect("powi");
612        assert!(
613            (r.re().to_f64() - 1.0).abs() < 1e-12,
614            "re = {}",
615            r.re().to_f64()
616        );
617        assert!(r.im().to_f64().abs() < 1e-12, "im = {}", r.im().to_f64());
618    }
619
620    #[test]
621    fn powi_one_plus_i_squared() {
622        // (1 + i)^2 = 2i.
623        let z = c(1.0, 1.0);
624        let r = z.powi(2, PREC, MODE).expect("powi");
625        assert!(r.re().to_f64().abs() < 1e-9, "re = {}", r.re().to_f64());
626        assert!(
627            (r.im().to_f64() - 2.0).abs() < 1e-9,
628            "im = {}",
629            r.im().to_f64()
630        );
631    }
632
633    #[test]
634    fn powi_i_fourth_is_one() {
635        // i^4 = 1.
636        let r = BigComplex::i(PREC, MODE).powi(4, PREC, MODE).expect("powi");
637        assert!(
638            (r.re().to_f64() - 1.0).abs() < 1e-9,
639            "re = {}",
640            r.re().to_f64()
641        );
642        assert!(r.im().to_f64().abs() < 1e-9, "im = {}", r.im().to_f64());
643    }
644
645    #[test]
646    fn powi_negative_one_is_reciprocal() {
647        // (2+0i)^(-1) = 0.5.
648        let z = c(2.0, 0.0);
649        let r = z.powi(-1, PREC, MODE).expect("powi");
650        assert!(
651            (r.re().to_f64() - 0.5).abs() < 1e-9,
652            "re = {}",
653            r.re().to_f64()
654        );
655        assert!(r.im().to_f64().abs() < 1e-9, "im = {}", r.im().to_f64());
656    }
657
658    #[test]
659    fn powf_matches_powi_squared() {
660        // z.powf(2) ≈ z.powi(2) for z = 1.5 + 0.7i.
661        let z = c(1.5, 0.7);
662        let exp = BigFloat::from_f64(2.0, PREC).expect("finite");
663        let r1 = z.powf(&exp, PREC, MODE).expect("powf");
664        let r2 = z.powi(2, PREC, MODE).expect("powi");
665        assert!(
666            (r1.re().to_f64() - r2.re().to_f64()).abs() < 1e-9,
667            "re: {} vs {}",
668            r1.re().to_f64(),
669            r2.re().to_f64()
670        );
671        assert!(
672            (r1.im().to_f64() - r2.im().to_f64()).abs() < 1e-9,
673            "im: {} vs {}",
674            r1.im().to_f64(),
675            r2.im().to_f64()
676        );
677    }
678
679    #[test]
680    fn powf_matches_pow_on_real() {
681        // (2+0i).powf(3) ≈ 8+0i.
682        let z = c(2.0, 0.0);
683        let exp = BigFloat::from_f64(3.0, PREC).expect("finite");
684        let r = z.powf(&exp, PREC, MODE).expect("powf");
685        assert!(
686            (r.re().to_f64() - 8.0).abs() < 1e-9,
687            "re = {}",
688            r.re().to_f64()
689        );
690        assert!(r.im().to_f64().abs() < 1e-9, "im = {}", r.im().to_f64());
691    }
692}