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, OxiNumResult};
30use oxinum_float::native::{BigFloat, RoundingMode};
31
32use super::BigComplex;
33
34/// Working-precision headroom added on top of the requested `prec`.
35const GUARD: u32 = 10;
36
37impl BigComplex {
38    /// The magnitude `|z| = sqrt(a² + b²)` as a real [`BigFloat`] at `prec` bits.
39    ///
40    /// Returns the canonical zero for a zero input (avoiding a `sqrt(0)` round
41    /// trip). The squared magnitude is taken from [`BigComplex::norm_sqr`].
42    ///
43    /// # Errors
44    ///
45    /// Propagates any error from [`BigFloat::sqrt`] (none expected for the
46    /// non-negative `norm_sqr`).
47    ///
48    /// # Examples
49    ///
50    /// ```
51    /// use oxinum_complex::native::{BigComplex, RoundingMode};
52    /// let z = BigComplex::from_f64(3.0, 4.0, 80).expect("finite");
53    /// let m = z.abs(80, RoundingMode::HalfEven).expect("abs");
54    /// assert!((m.to_f64() - 5.0).abs() < 1e-12);
55    /// ```
56    pub fn abs(&self, prec: u32, mode: RoundingMode) -> OxiNumResult<BigFloat> {
57        if self.is_zero() {
58            return Ok(BigFloat::zero(prec));
59        }
60        let guard = prec.saturating_add(GUARD);
61        let nrm = self.norm_sqr().with_precision(guard, mode);
62        Ok(nrm.sqrt(guard, mode)?.with_precision(prec, mode))
63    }
64
65    /// The argument `arg(z) = atan2(b, a)` as a real [`BigFloat`] at `prec`
66    /// bits, the principal value in `(−π, π]`.
67    ///
68    /// # Errors
69    ///
70    /// Propagates any error from [`BigFloat::atan2`].
71    ///
72    /// # Examples
73    ///
74    /// ```
75    /// use oxinum_complex::native::{BigComplex, RoundingMode};
76    /// let i = BigComplex::i(80, RoundingMode::HalfEven);
77    /// let a = i.arg(80, RoundingMode::HalfEven).expect("arg");
78    /// assert!((a.to_f64() - std::f64::consts::FRAC_PI_2).abs() < 1e-12);
79    /// ```
80    pub fn arg(&self, prec: u32, mode: RoundingMode) -> OxiNumResult<BigFloat> {
81        let guard = prec.saturating_add(GUARD);
82        let re = self.re.clone().with_precision(guard, mode);
83        let im = self.im.clone().with_precision(guard, mode);
84        Ok(im.atan2(&re, guard, mode)?.with_precision(prec, mode))
85    }
86
87    /// The complex exponential `exp(z) = eᵃ·(cos b + i·sin b)` at `prec` bits.
88    ///
89    /// # Errors
90    ///
91    /// Propagates errors from [`BigFloat::exp`] (e.g. overflow when `a` is huge)
92    /// and from the real trig routines.
93    ///
94    /// # Examples
95    ///
96    /// ```
97    /// use oxinum_complex::native::{BigComplex, RoundingMode};
98    /// // exp(iπ) ≈ −1 + 0i  (Euler's identity).
99    /// let z = BigComplex::from_f64(0.0, std::f64::consts::PI, 80).expect("finite");
100    /// let e = z.exp(80, RoundingMode::HalfEven).expect("exp");
101    /// assert!((e.re().to_f64() + 1.0).abs() < 1e-12);
102    /// assert!(e.im().to_f64().abs() < 1e-12);
103    /// ```
104    pub fn exp(&self, prec: u32, mode: RoundingMode) -> OxiNumResult<BigComplex> {
105        let guard = prec.saturating_add(GUARD);
106        let a = self.re.clone().with_precision(guard, mode);
107        let b = self.im.clone().with_precision(guard, mode);
108
109        let exp_a = a.exp(guard, mode)?;
110        let cos_b = b.cos(guard, mode)?;
111        let sin_b = b.sin(guard, mode)?;
112
113        let re = (&exp_a * &cos_b).with_precision(prec, mode);
114        let im = (&exp_a * &sin_b).with_precision(prec, mode);
115        Ok(BigComplex { re, im })
116    }
117
118    /// The principal complex logarithm
119    /// `ln(z) = ½·ln(a² + b²) + i·atan2(b, a)` at `prec` bits.
120    ///
121    /// # Errors
122    ///
123    /// - [`OxiNumError::Domain`] if `z` is zero (`ln(0)` is undefined).
124    /// - Propagates errors from [`BigFloat::ln`] / [`BigFloat::atan2`].
125    ///
126    /// # Examples
127    ///
128    /// ```
129    /// use oxinum_complex::native::{BigComplex, RoundingMode};
130    /// // ln(−1) ≈ 0 + iπ.
131    /// let z = BigComplex::from_f64(-1.0, 0.0, 80).expect("finite");
132    /// let l = z.ln(80, RoundingMode::HalfEven).expect("ln");
133    /// assert!(l.re().to_f64().abs() < 1e-12);
134    /// assert!((l.im().to_f64() - std::f64::consts::PI).abs() < 1e-12);
135    /// ```
136    pub fn ln(&self, prec: u32, mode: RoundingMode) -> OxiNumResult<BigComplex> {
137        if self.is_zero() {
138            return Err(OxiNumError::Domain("ln(0) is undefined".into()));
139        }
140        let guard = prec.saturating_add(GUARD);
141        let a = self.re.clone().with_precision(guard, mode);
142        let b = self.im.clone().with_precision(guard, mode);
143
144        // re = ½·ln(a² + b²)
145        let nrm = self.norm_sqr().with_precision(guard, mode);
146        let ln_nrm = nrm.ln(guard, mode)?;
147        let half = BigFloat::from_f64(0.5, guard)?;
148        let re = (&ln_nrm * &half).with_precision(prec, mode);
149
150        // im = atan2(b, a)
151        let im = b.atan2(&a, guard, mode)?.with_precision(prec, mode);
152
153        Ok(BigComplex { re, im })
154    }
155
156    /// The principal square root `sqrt(z)` at `prec` bits.
157    ///
158    /// Uses `re = sqrt((|z| + a)/2)`, `im = sign(b)·sqrt((|z| − a)/2)`, with the
159    /// purely-real input handled by an exact axis split and the radicands
160    /// clamped up to zero before the real `sqrt` to absorb rounding noise. The
161    /// branch chosen has `re ≥ 0` and matches the IEEE-754 / `num-complex`
162    /// principal value.
163    ///
164    /// # Errors
165    ///
166    /// Propagates errors from [`BigFloat::sqrt`] (none expected: radicands are
167    /// clamped non-negative).
168    ///
169    /// # Examples
170    ///
171    /// ```
172    /// use oxinum_complex::native::{BigComplex, RoundingMode};
173    /// // sqrt(2i) = 1 + i.
174    /// let z = BigComplex::from_f64(0.0, 2.0, 80).expect("finite");
175    /// let r = z.sqrt(80, RoundingMode::HalfEven).expect("sqrt");
176    /// assert!((r.re().to_f64() - 1.0).abs() < 1e-12);
177    /// assert!((r.im().to_f64() - 1.0).abs() < 1e-12);
178    /// ```
179    pub fn sqrt(&self, prec: u32, mode: RoundingMode) -> OxiNumResult<BigComplex> {
180        if self.is_zero() {
181            return Ok(BigComplex::zero(prec));
182        }
183        let guard = prec.saturating_add(GUARD);
184
185        // Real-axis fast path: b == 0.
186        if self.im.is_zero() {
187            let a = self.re.clone().with_precision(guard, mode);
188            if !a.is_sign_negative() {
189                // a >= 0: re = √a, im = 0.
190                let re = a.sqrt(guard, mode)?.with_precision(prec, mode);
191                return Ok(BigComplex {
192                    re,
193                    im: BigFloat::zero(prec),
194                });
195            } else {
196                // a < 0: re = 0, im = √(−a).
197                let neg_a = (-&a).with_precision(guard, mode);
198                let im = neg_a.sqrt(guard, mode)?.with_precision(prec, mode);
199                return Ok(BigComplex {
200                    re: BigFloat::zero(prec),
201                    im,
202                });
203            }
204        }
205
206        // General case.
207        let a = self.re.clone().with_precision(guard, mode);
208        let two = BigFloat::from_i64(2, guard, mode);
209
210        // m = |z| at guard precision.
211        let m = {
212            let nrm = self.norm_sqr().with_precision(guard, mode);
213            nrm.sqrt(guard, mode)?
214        };
215
216        let zero = BigFloat::zero(guard);
217
218        // re = sqrt((m + a) / 2), clamping a tiny-negative radicand up to 0.
219        let re_radicand = {
220            let s = &m + &a;
221            let r = s.div_ref_with_mode(&two, mode)?;
222            if r < zero {
223                zero.clone()
224            } else {
225                r
226            }
227        };
228        let re = re_radicand.sqrt(guard, mode)?.with_precision(prec, mode);
229
230        // im_mag = sqrt((m − a) / 2), same clamp.
231        let im_radicand = {
232            let s = &m - &a;
233            let r = s.div_ref_with_mode(&two, mode)?;
234            if r < zero {
235                zero.clone()
236            } else {
237                r
238            }
239        };
240        let im_mag = im_radicand.sqrt(guard, mode)?;
241
242        // Apply sign(b): for b < 0 the imaginary part is negative.
243        let im = if self.im.is_sign_negative() {
244            (-&im_mag).with_precision(prec, mode)
245        } else {
246            im_mag.with_precision(prec, mode)
247        };
248
249        Ok(BigComplex { re, im })
250    }
251
252    /// The complex power `z^w = exp(w · ln z)` at `prec` bits.
253    ///
254    /// The zero base is handled by convention: `0^0 = 1` and `0^w = 0` for any
255    /// other `w` (avoiding `ln(0)`).
256    ///
257    /// # Errors
258    ///
259    /// Propagates errors from [`BigComplex::ln`] / [`BigComplex::exp`].
260    ///
261    /// # Examples
262    ///
263    /// ```
264    /// use oxinum_complex::native::{BigComplex, RoundingMode};
265    /// // i^2 = −1.
266    /// let i = BigComplex::i(80, RoundingMode::HalfEven);
267    /// let two = BigComplex::from_f64(2.0, 0.0, 80).expect("finite");
268    /// let r = i.pow(&two, 80, RoundingMode::HalfEven).expect("pow");
269    /// assert!((r.re().to_f64() + 1.0).abs() < 1e-12);
270    /// assert!(r.im().to_f64().abs() < 1e-12);
271    /// ```
272    pub fn pow(&self, w: &BigComplex, prec: u32, mode: RoundingMode) -> OxiNumResult<BigComplex> {
273        if self.is_zero() {
274            return if w.is_zero() {
275                Ok(BigComplex::one(prec, mode))
276            } else {
277                Ok(BigComplex::zero(prec))
278            };
279        }
280        let guard = prec.saturating_add(GUARD);
281        let ln_z = self.ln(guard, mode)?;
282        let prod = w.mul_core(&ln_z);
283        prod.exp(prec, mode)
284    }
285}
286
287// ---------------------------------------------------------------------------
288// Tests
289// ---------------------------------------------------------------------------
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294
295    const PREC: u32 = 80;
296    const MODE: RoundingMode = RoundingMode::HalfEven;
297
298    fn c(re: f64, im: f64) -> BigComplex {
299        BigComplex::from_f64(re, im, PREC).expect("finite parts")
300    }
301
302    #[test]
303    fn abs_three_four_is_five() {
304        let m = c(3.0, 4.0).abs(PREC, MODE).expect("abs");
305        assert!((m.to_f64() - 5.0).abs() < 1e-9, "|3+4i| = {}", m.to_f64());
306    }
307
308    #[test]
309    fn abs_zero_is_zero() {
310        let m = BigComplex::zero(PREC).abs(PREC, MODE).expect("abs");
311        assert!(m.is_zero());
312    }
313
314    #[test]
315    fn arg_of_i_is_half_pi() {
316        let a = BigComplex::i(PREC, MODE).arg(PREC, MODE).expect("arg");
317        assert!(
318            (a.to_f64() - std::f64::consts::FRAC_PI_2).abs() < 1e-9,
319            "arg(i) = {}",
320            a.to_f64()
321        );
322    }
323
324    #[test]
325    fn exp_i_pi_is_minus_one() {
326        // exp(iπ) ≈ −1.
327        let z = c(0.0, std::f64::consts::PI);
328        let e = z.exp(PREC, MODE).expect("exp");
329        assert!(
330            (e.re().to_f64() + 1.0).abs() < 1e-9,
331            "re = {}",
332            e.re().to_f64()
333        );
334        assert!(e.im().to_f64().abs() < 1e-9, "im = {}", e.im().to_f64());
335    }
336
337    #[test]
338    fn ln_minus_one_is_i_pi() {
339        let l = c(-1.0, 0.0).ln(PREC, MODE).expect("ln");
340        assert!(l.re().to_f64().abs() < 1e-9, "re = {}", l.re().to_f64());
341        assert!(
342            (l.im().to_f64() - std::f64::consts::PI).abs() < 1e-9,
343            "im = {}",
344            l.im().to_f64()
345        );
346    }
347
348    #[test]
349    fn ln_zero_is_domain_error() {
350        let l = BigComplex::zero(PREC).ln(PREC, MODE);
351        assert!(matches!(l, Err(OxiNumError::Domain(_))), "got {l:?}");
352    }
353
354    #[test]
355    fn sqrt_minus_one_is_i() {
356        let r = c(-1.0, 0.0).sqrt(PREC, MODE).expect("sqrt");
357        assert!(r.re().to_f64().abs() < 1e-9, "re = {}", r.re().to_f64());
358        assert!(
359            (r.im().to_f64() - 1.0).abs() < 1e-9,
360            "im = {}",
361            r.im().to_f64()
362        );
363    }
364
365    #[test]
366    fn sqrt_two_i_is_one_plus_i() {
367        let r = c(0.0, 2.0).sqrt(PREC, MODE).expect("sqrt");
368        assert!(
369            (r.re().to_f64() - 1.0).abs() < 1e-9,
370            "re = {}",
371            r.re().to_f64()
372        );
373        assert!(
374            (r.im().to_f64() - 1.0).abs() < 1e-9,
375            "im = {}",
376            r.im().to_f64()
377        );
378    }
379
380    #[test]
381    fn sqrt_positive_real() {
382        // sqrt(4) = 2.
383        let r = c(4.0, 0.0).sqrt(PREC, MODE).expect("sqrt");
384        assert!((r.re().to_f64() - 2.0).abs() < 1e-9);
385        assert!(r.im().to_f64().abs() < 1e-12);
386    }
387
388    #[test]
389    fn sqrt_squared_roundtrip() {
390        // (sqrt(z))² ≈ z for a general z.
391        let z = c(2.0, -3.0);
392        let r = z.sqrt(PREC, MODE).expect("sqrt");
393        let sq = r.mul_core(&r);
394        assert!(
395            (sq.re().to_f64() - 2.0).abs() < 1e-9,
396            "re = {}",
397            sq.re().to_f64()
398        );
399        assert!(
400            (sq.im().to_f64() + 3.0).abs() < 1e-9,
401            "im = {}",
402            sq.im().to_f64()
403        );
404    }
405
406    #[test]
407    fn pow_i_squared_is_minus_one() {
408        let r = BigComplex::i(PREC, MODE)
409            .pow(&c(2.0, 0.0), PREC, MODE)
410            .expect("pow");
411        assert!(
412            (r.re().to_f64() + 1.0).abs() < 1e-9,
413            "re = {}",
414            r.re().to_f64()
415        );
416        assert!(r.im().to_f64().abs() < 1e-9, "im = {}", r.im().to_f64());
417    }
418
419    #[test]
420    fn pow_zero_zero_is_one() {
421        let r = BigComplex::zero(PREC)
422            .pow(&BigComplex::zero(PREC), PREC, MODE)
423            .expect("pow");
424        assert!((r.re().to_f64() - 1.0).abs() < 1e-12);
425        assert!(r.im().to_f64().abs() < 1e-12);
426    }
427
428    #[test]
429    fn pow_zero_base_nonzero_exp_is_zero() {
430        let r = BigComplex::zero(PREC)
431            .pow(&c(2.0, 1.0), PREC, MODE)
432            .expect("pow");
433        assert!(r.is_zero());
434    }
435}