Skip to main content

oxinum_complex/
transcendental.rs

1//! Transcendental functions for [`crate::CBig`]: `abs`, `arg`, `exp`, `ln`,
2//! `sqrt`, and `pow`.
3//!
4//! Every method works at an internal *guard* precision of `precision + 10`
5//! significant decimal digits and delivers each component at the guard
6//! precision returned by the underlying [`oxinum_float`] free functions. With
7//! `z = a + b·i` (so `a = self.re()`, `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 (so `sqrt(-1) = +i`).
28//!
29//! This is the decimal-backed ([`DBig`]) mirror of the native binary
30//! implementation in [`crate::native`]; the formulas and branch cuts match
31//! exactly.
32
33use core::str::FromStr;
34
35use oxinum_float::{atan2, cos, exp, ln, pow, sin, sqrt};
36
37use crate::{CBig, DBig, OxiNumError, OxiNumResult};
38
39/// Working-precision headroom added on top of the requested `precision`.
40const GUARD: usize = 10;
41
42/// Parse a decimal literal into a [`DBig`], mapping any failure to
43/// [`OxiNumError::Parse`].
44fn make_dbig(s: &str) -> OxiNumResult<DBig> {
45    DBig::from_str(s).map_err(|e| OxiNumError::Parse(format!("{e}").into()))
46}
47
48/// Binary exponentiation for [`CBig`]: computes `base^n` in `O(log n)` multiplications.
49///
50/// `n == 0` is guarded by the caller; this function requires `n >= 1`.
51fn exp_by_squaring_cbig(mut base: CBig, mut n: u32) -> CBig {
52    let mut result = CBig::one();
53    while n > 0 {
54        if n & 1 == 1 {
55            result = &result * &base;
56        }
57        base = &base * &base;
58        n >>= 1;
59    }
60    result
61}
62
63impl CBig {
64    /// The magnitude `|z| = sqrt(a² + b²)` as a real [`DBig`] at `precision`
65    /// significant digits.
66    ///
67    /// Returns an exact decimal zero for a zero input (avoiding a `sqrt(0)`
68    /// round trip). The squared magnitude is taken from [`CBig::norm_sqr`].
69    ///
70    /// # Errors
71    ///
72    /// Propagates any error from [`oxinum_float::sqrt`] (none expected for the
73    /// non-negative `norm_sqr`; an [`OxiNumError::Precision`] is returned if
74    /// `precision == 0`).
75    pub fn abs(&self, precision: usize) -> OxiNumResult<DBig> {
76        if self.is_zero() {
77            return make_dbig("0.0");
78        }
79        sqrt(&self.norm_sqr(), precision)
80    }
81
82    /// The argument `arg(z) = atan2(b, a)` as a real [`DBig`] at `precision`
83    /// significant digits, the principal value in `(−π, π]`.
84    ///
85    /// # Errors
86    ///
87    /// Propagates any error from [`oxinum_float::atan2`].
88    pub fn arg(&self, precision: usize) -> OxiNumResult<DBig> {
89        atan2(&self.im, &self.re, precision)
90    }
91
92    /// The complex exponential `exp(z) = eᵃ·(cos b + i·sin b)` at `precision`
93    /// significant digits.
94    ///
95    /// # Errors
96    ///
97    /// Propagates errors from [`oxinum_float::exp`], [`oxinum_float::cos`], and
98    /// [`oxinum_float::sin`].
99    pub fn exp(&self, precision: usize) -> OxiNumResult<CBig> {
100        let guard = precision + GUARD;
101        let ea = exp(&self.re, guard)?;
102        let cosb = cos(&self.im, guard)?;
103        let sinb = sin(&self.im, guard)?;
104        Ok(CBig::from_parts(&ea * &cosb, &ea * &sinb))
105    }
106
107    /// The principal complex logarithm
108    /// `ln(z) = ½·ln(a² + b²) + i·atan2(b, a)` at `precision` significant
109    /// digits.
110    ///
111    /// Using `½·ln(norm_sqr)` for the real part avoids an extra `sqrt`.
112    ///
113    /// # Errors
114    ///
115    /// - [`OxiNumError::Domain`] if `z` is zero (`ln(0)` is undefined).
116    /// - Propagates errors from [`oxinum_float::ln`] / [`oxinum_float::atan2`].
117    pub fn ln(&self, precision: usize) -> OxiNumResult<CBig> {
118        if self.is_zero() {
119            return Err(OxiNumError::Domain("ln(0) is undefined".into()));
120        }
121        let guard = precision + GUARD;
122        let ns = self.norm_sqr();
123        let re = &make_dbig("0.5")? * &ln(&ns, guard)?;
124        let im = atan2(&self.im, &self.re, guard)?;
125        Ok(CBig::from_parts(re, im))
126    }
127
128    /// The principal square root `sqrt(z)` at `precision` significant digits.
129    ///
130    /// Uses `re = sqrt((|z| + a)/2)`, `im = sign(b)·sqrt((|z| − a)/2)`, with the
131    /// purely-real input handled by an exact axis split and the radicands
132    /// clamped up to zero before the real `sqrt` to absorb rounding noise. The
133    /// branch chosen has `re ≥ 0` and matches the IEEE-754 / `num-complex`
134    /// principal value (so `sqrt(-1) = +i`).
135    ///
136    /// # Errors
137    ///
138    /// Propagates errors from [`oxinum_float::sqrt`] (none expected: radicands
139    /// are clamped non-negative).
140    pub fn sqrt(&self, precision: usize) -> OxiNumResult<CBig> {
141        let zero = DBig::from(0u32);
142
143        if self.is_zero() {
144            return Ok(CBig::zero());
145        }
146
147        // Real-axis fast path: b == 0.
148        if self.im == zero {
149            if self.re >= zero {
150                // a >= 0: re = √a, im = 0.
151                return Ok(CBig::from_parts(sqrt(&self.re, precision)?, zero));
152            }
153            // a < 0: re = 0, im = √(−a)  (so sqrt(-1) = +i).
154            let neg_a = -&self.re;
155            return Ok(CBig::from_parts(zero, sqrt(&neg_a, precision)?));
156        }
157
158        // General case.
159        let guard = precision + GUARD;
160        let m = sqrt(&self.norm_sqr(), guard)?; // m = |z|
161        let two = make_dbig("2.0")?;
162
163        // re = sqrt((m + a) / 2), clamping a tiny-negative radicand up to 0.
164        let mut r_re = &(&m + &self.re) / &two;
165        if r_re < zero {
166            r_re = DBig::from(0u32);
167        }
168
169        // im_mag = sqrt((m − a) / 2), same clamp.
170        let mut r_im = &(&m - &self.re) / &two;
171        if r_im < zero {
172            r_im = DBig::from(0u32);
173        }
174
175        let re = sqrt(&r_re, precision)?;
176        let im_mag = sqrt(&r_im, precision)?;
177
178        // Apply sign(b): for b < 0 the imaginary part is negative.
179        let im = if self.im < zero { -im_mag } else { im_mag };
180
181        Ok(CBig::from_parts(re, im))
182    }
183
184    /// The complex power `z^w = exp(w · ln z)` at `precision` significant
185    /// digits.
186    ///
187    /// The zero base is handled by convention: `0^0 = 1` and `0^w = 0` for any
188    /// other `w` (avoiding `ln(0)`).
189    ///
190    /// # Errors
191    ///
192    /// Propagates errors from [`CBig::ln`] / [`CBig::exp`].
193    pub fn pow(&self, w: &CBig, precision: usize) -> OxiNumResult<CBig> {
194        if self.is_zero() {
195            return if w.is_zero() {
196                Ok(CBig::one())
197            } else {
198                Ok(CBig::zero())
199            };
200        }
201        let guard = precision + GUARD;
202        let lz = self.ln(guard)?;
203        let prod = w * &lz;
204        prod.exp(precision)
205    }
206
207    /// Construct `re + im·i` from polar form `(r, θ)`: `r·cos θ + i·r·sin θ`.
208    ///
209    /// # Errors
210    ///
211    /// Propagates errors from [`oxinum_float::cos`] / [`oxinum_float::sin`].
212    pub fn from_polar(r: &DBig, theta: &DBig, precision: usize) -> OxiNumResult<CBig> {
213        let guard = precision.saturating_add(GUARD);
214        let cos_t = cos(theta, guard)?;
215        let sin_t = sin(theta, guard)?;
216        Ok(CBig::from_parts(r * &cos_t, r * &sin_t))
217    }
218
219    /// Return `(|z|, arg z)` as a `(DBig, DBig)` pair at `precision` digits.
220    ///
221    /// # Errors
222    ///
223    /// Propagates errors from [`CBig::abs`] / [`CBig::arg`].
224    pub fn to_polar(&self, precision: usize) -> OxiNumResult<(DBig, DBig)> {
225        let mag = self.abs(precision)?;
226        let arg = self.arg(precision)?;
227        Ok((mag, arg))
228    }
229
230    /// Integer power `z^n` via binary exponentiation (`O(log |n|)` multiplications).
231    ///
232    /// `n == 0` returns `one()`. `n < 0` computes `(z^|n|)⁻¹` via
233    /// [`CBig::checked_div`], returning [`OxiNumError::DivByZero`] when `z` is zero.
234    ///
235    /// # Errors
236    ///
237    /// [`OxiNumError::DivByZero`] when `n < 0` and `self` is zero.
238    pub fn powi(&self, n: i32, _precision: usize) -> OxiNumResult<CBig> {
239        if n == 0 {
240            return Ok(CBig::one());
241        }
242        let negative = n < 0;
243        let abs_n = n.unsigned_abs();
244        let result = exp_by_squaring_cbig(self.clone(), abs_n);
245        if negative {
246            CBig::one().checked_div(&result)
247        } else {
248            Ok(result)
249        }
250    }
251
252    /// Real-exponent power `z^x = r^x · (cos(x·θ) + i·sin(x·θ))` via polar form.
253    ///
254    /// Avoids the full `exp(w·ln z)` round trip. For `z == 0`: `0^0 = 1`,
255    /// `0^x = 0` for any other `x`.
256    ///
257    /// # Errors
258    ///
259    /// Propagates errors from [`CBig::abs`], [`CBig::arg`], [`oxinum_float::pow`],
260    /// [`oxinum_float::cos`], and [`oxinum_float::sin`].
261    pub fn powf(&self, exp: &DBig, precision: usize) -> OxiNumResult<CBig> {
262        let zero = DBig::from(0u32);
263        if self.is_zero() {
264            return if *exp == zero {
265                Ok(CBig::one())
266            } else {
267                Ok(CBig::zero())
268            };
269        }
270        let guard = precision.saturating_add(GUARD);
271        let r = self.abs(guard)?;
272        let theta = self.arg(guard)?;
273        // r^x via the real `pow` free function
274        let rx = pow(&r, exp, guard)?;
275        // x·θ
276        let x_theta = exp * &theta;
277        let re = &rx * &cos(&x_theta, guard)?;
278        let im = &rx * &sin(&x_theta, guard)?;
279        Ok(CBig::from_parts(re, im))
280    }
281}
282
283// ---------------------------------------------------------------------------
284// Tests
285// ---------------------------------------------------------------------------
286
287#[cfg(test)]
288mod tests {
289    use super::*;
290    use oxinum_float::compute_pi;
291
292    /// Build π at the given decimal precision.
293    fn pi(precision: usize) -> DBig {
294        compute_pi(precision)
295    }
296
297    #[test]
298    fn exp_i_pi_is_minus_one() {
299        // exp(iπ) ≈ −1 + 0i  (Euler's identity).
300        let z = CBig::from_parts(DBig::from(0u32), pi(50));
301        let r = z.exp(40).expect("exp");
302        let (re, im) = r.to_f64_parts();
303        assert!((re + 1.0).abs() < 1e-12, "re = {re}");
304        assert!(im.abs() < 1e-12, "im = {im}");
305    }
306
307    #[test]
308    fn ln_minus_one_is_i_pi() {
309        let z = CBig::from_real(make_dbig("-1.0").expect("literal"));
310        let r = z.ln(40).expect("ln");
311        let (re, im) = r.to_f64_parts();
312        assert!(re.abs() < 1e-12, "re = {re}");
313        assert!((im - std::f64::consts::PI).abs() < 1e-12, "im = {im}");
314    }
315
316    #[test]
317    fn ln_zero_is_domain_error() {
318        let r = CBig::zero().ln(40);
319        assert!(matches!(r, Err(OxiNumError::Domain(_))), "got {r:?}");
320    }
321
322    #[test]
323    fn sqrt_minus_one_is_i() {
324        let z = CBig::from_real(make_dbig("-1.0").expect("literal"));
325        let r = z.sqrt(40).expect("sqrt");
326        let (re, im) = r.to_f64_parts();
327        assert!(re.abs() < 1e-12, "re = {re}");
328        assert!((im - 1.0).abs() < 1e-12, "im = {im}");
329    }
330
331    #[test]
332    fn sqrt_two_i_is_one_plus_i() {
333        // sqrt(2i) = 1 + i.
334        let z = CBig::from_parts(DBig::from(0u32), DBig::from(2u32));
335        let r = z.sqrt(40).expect("sqrt");
336        let (re, im) = r.to_f64_parts();
337        assert!((re - 1.0).abs() < 1e-12, "re = {re}");
338        assert!((im - 1.0).abs() < 1e-12, "im = {im}");
339    }
340
341    #[test]
342    fn sqrt_positive_real() {
343        // sqrt(4) = 2.
344        let z = CBig::from_real(DBig::from(4u32));
345        let r = z.sqrt(40).expect("sqrt");
346        let (re, im) = r.to_f64_parts();
347        assert!((re - 2.0).abs() < 1e-12, "re = {re}");
348        assert!(im.abs() < 1e-12, "im = {im}");
349    }
350
351    #[test]
352    fn abs_three_four_is_five() {
353        let z = CBig::from_parts(DBig::from(3u32), DBig::from(4u32));
354        let m = z.abs(40).expect("abs");
355        assert!(m.to_string().starts_with('5'), "|3+4i| = {m}");
356    }
357
358    #[test]
359    fn abs_zero_is_zero() {
360        let m = CBig::zero().abs(40).expect("abs");
361        assert_eq!(m, DBig::from(0u32));
362    }
363
364    #[test]
365    fn arg_of_i_is_half_pi() {
366        let a = CBig::i().arg(40).expect("arg");
367        let v = a.to_f64();
368        assert!(
369            (v.value() - std::f64::consts::FRAC_PI_2).abs() < 1e-12,
370            "arg(i) = {a}"
371        );
372    }
373
374    #[test]
375    fn pow_i_squared_is_minus_one() {
376        // i² = −1.
377        let r = CBig::i()
378            .pow(&CBig::from_real(DBig::from(2u32)), 40)
379            .expect("pow");
380        let (re, im) = r.to_f64_parts();
381        assert!((re + 1.0).abs() < 1e-12, "re = {re}");
382        assert!(im.abs() < 1e-12, "im = {im}");
383    }
384
385    #[test]
386    fn pow_to_one_is_identity() {
387        // z^1 ≈ z on the real axis (arg = 0), where the round trip through
388        // ln/exp is exact and independent of the underlying `atan2`.
389        let z = CBig::from_real(make_dbig("2.0").expect("literal"));
390        let r = z.pow(&CBig::one(), 40).expect("pow");
391        let (re, im) = r.to_f64_parts();
392        assert!((re - 2.0).abs() < 1e-12, "re = {re}");
393        assert!(im.abs() < 1e-12, "im = {im}");
394    }
395
396    #[test]
397    fn pow_zero_zero_is_one() {
398        let r = CBig::zero().pow(&CBig::zero(), 40).expect("pow");
399        assert!(r == CBig::one());
400    }
401
402    #[test]
403    fn pow_zero_base_nonzero_exp_is_zero() {
404        let r = CBig::zero()
405            .pow(&CBig::from_parts(DBig::from(2u32), DBig::from(1u32)), 40)
406            .expect("pow");
407        assert!(r.is_zero());
408    }
409
410    // ---- Polar helpers --------------------------------------------------------
411
412    #[test]
413    fn from_polar_two_half_pi_is_2i() {
414        // from_polar(2, π/2) ≈ 0 + 2i.
415        let r = DBig::from(2u32);
416        let theta = &pi(50) / &make_dbig("2.0").expect("2.0");
417        let z = CBig::from_polar(&r, &theta, 40).expect("from_polar");
418        let (re, im) = z.to_f64_parts();
419        assert!(re.abs() < 1e-9, "re = {re}");
420        assert!((im - 2.0).abs() < 1e-9, "im = {im}");
421    }
422
423    #[test]
424    fn to_polar_three_four_is_5_atan2_4_3() {
425        // to_polar(3 + 4i) → (5, atan2(4, 3)).
426        // Use from_f64 so parts carry bounded precision (avoids DBig::from(n)
427        // single-digit truncation in norm_sqr).
428        let z = CBig::from_f64(3.0, 4.0).expect("finite");
429        let (mag, arg) = z.to_polar(40).expect("to_polar");
430        assert!((mag.to_f64().value() - 5.0).abs() < 1e-9, "mag = {mag}");
431        let expected_arg = (4.0_f64).atan2(3.0);
432        assert!(
433            (arg.to_f64().value() - expected_arg).abs() < 1e-9,
434            "arg = {arg}"
435        );
436    }
437
438    #[test]
439    fn from_polar_to_polar_roundtrip() {
440        // from_polar(to_polar(z)) ≈ z for z = 2 + 3i.
441        // Use from_f64 so parts carry bounded precision (avoids DBig::from(n)
442        // single-digit truncation in norm_sqr).
443        let z = CBig::from_f64(2.0, 3.0).expect("finite");
444        let (mag, arg) = z.to_polar(50).expect("to_polar");
445        let z2 = CBig::from_polar(&mag, &arg, 40).expect("from_polar");
446        let (re, im) = z2.to_f64_parts();
447        assert!((re - 2.0).abs() < 1e-9, "re = {re}");
448        assert!((im - 3.0).abs() < 1e-9, "im = {im}");
449    }
450
451    // ---- powi ----------------------------------------------------------------
452
453    #[test]
454    fn powi_zero_exponent_is_one() {
455        let z = CBig::from_f64(1.5, 0.7).expect("finite");
456        let r = z.powi(0, 40).expect("powi");
457        assert!(r == CBig::one());
458    }
459
460    #[test]
461    fn powi_one_plus_i_squared() {
462        // (1 + i)^2 = 2i.
463        let z = CBig::from_f64(1.0, 1.0).expect("finite");
464        let r = z.powi(2, 40).expect("powi");
465        let (re, im) = r.to_f64_parts();
466        assert!(re.abs() < 1e-9, "re = {re}");
467        assert!((im - 2.0).abs() < 1e-9, "im = {im}");
468    }
469
470    #[test]
471    fn powi_i_fourth_is_one() {
472        // i^4 = 1.
473        let r = CBig::i().powi(4, 40).expect("powi");
474        let (re, im) = r.to_f64_parts();
475        assert!((re - 1.0).abs() < 1e-9, "re = {re}");
476        assert!(im.abs() < 1e-9, "im = {im}");
477    }
478
479    #[test]
480    fn powi_negative_one_is_reciprocal() {
481        // (2+0i)^(-1) = 0.5.
482        let z = CBig::from_real(DBig::from(2u32));
483        let r = z.powi(-1, 40).expect("powi");
484        let (re, im) = r.to_f64_parts();
485        assert!((re - 0.5).abs() < 1e-9, "re = {re}");
486        assert!(im.abs() < 1e-9, "im = {im}");
487    }
488
489    #[test]
490    fn powf_matches_powi_squared() {
491        // z.powf(2) ≈ z.powi(2) for z = 1.5 + 0.7i.
492        let z = CBig::from_f64(1.5, 0.7).expect("finite");
493        let exp = DBig::from(2u32);
494        let r1 = z.powf(&exp, 40).expect("powf");
495        let r2 = z.powi(2, 40).expect("powi");
496        let (re1, im1) = r1.to_f64_parts();
497        let (re2, im2) = r2.to_f64_parts();
498        assert!((re1 - re2).abs() < 1e-9, "re: {re1} vs {re2}");
499        assert!((im1 - im2).abs() < 1e-9, "im: {im1} vs {im2}");
500    }
501
502    #[test]
503    fn powf_matches_pow_on_real() {
504        // (2+0i).powf(3) ≈ 8+0i.
505        let z = CBig::from_real(DBig::from(2u32));
506        let exp = DBig::from(3u32);
507        let r = z.powf(&exp, 40).expect("powf");
508        let (re, im) = r.to_f64_parts();
509        assert!((re - 8.0).abs() < 1e-9, "re = {re}");
510        assert!(im.abs() < 1e-9, "im = {im}");
511    }
512}