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, 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
48impl CBig {
49    /// The magnitude `|z| = sqrt(a² + b²)` as a real [`DBig`] at `precision`
50    /// significant digits.
51    ///
52    /// Returns an exact decimal zero for a zero input (avoiding a `sqrt(0)`
53    /// round trip). The squared magnitude is taken from [`CBig::norm_sqr`].
54    ///
55    /// # Errors
56    ///
57    /// Propagates any error from [`oxinum_float::sqrt`] (none expected for the
58    /// non-negative `norm_sqr`; an [`OxiNumError::Precision`] is returned if
59    /// `precision == 0`).
60    pub fn abs(&self, precision: usize) -> OxiNumResult<DBig> {
61        if self.is_zero() {
62            return make_dbig("0.0");
63        }
64        sqrt(&self.norm_sqr(), precision)
65    }
66
67    /// The argument `arg(z) = atan2(b, a)` as a real [`DBig`] at `precision`
68    /// significant digits, the principal value in `(−π, π]`.
69    ///
70    /// # Errors
71    ///
72    /// Propagates any error from [`oxinum_float::atan2`].
73    pub fn arg(&self, precision: usize) -> OxiNumResult<DBig> {
74        atan2(&self.im, &self.re, precision)
75    }
76
77    /// The complex exponential `exp(z) = eᵃ·(cos b + i·sin b)` at `precision`
78    /// significant digits.
79    ///
80    /// # Errors
81    ///
82    /// Propagates errors from [`oxinum_float::exp`], [`oxinum_float::cos`], and
83    /// [`oxinum_float::sin`].
84    pub fn exp(&self, precision: usize) -> OxiNumResult<CBig> {
85        let guard = precision + GUARD;
86        let ea = exp(&self.re, guard)?;
87        let cosb = cos(&self.im, guard)?;
88        let sinb = sin(&self.im, guard)?;
89        Ok(CBig::from_parts(&ea * &cosb, &ea * &sinb))
90    }
91
92    /// The principal complex logarithm
93    /// `ln(z) = ½·ln(a² + b²) + i·atan2(b, a)` at `precision` significant
94    /// digits.
95    ///
96    /// Using `½·ln(norm_sqr)` for the real part avoids an extra `sqrt`.
97    ///
98    /// # Errors
99    ///
100    /// - [`OxiNumError::Domain`] if `z` is zero (`ln(0)` is undefined).
101    /// - Propagates errors from [`oxinum_float::ln`] / [`oxinum_float::atan2`].
102    pub fn ln(&self, precision: usize) -> OxiNumResult<CBig> {
103        if self.is_zero() {
104            return Err(OxiNumError::Domain("ln(0) is undefined".into()));
105        }
106        let guard = precision + GUARD;
107        let ns = self.norm_sqr();
108        let re = &make_dbig("0.5")? * &ln(&ns, guard)?;
109        let im = atan2(&self.im, &self.re, guard)?;
110        Ok(CBig::from_parts(re, im))
111    }
112
113    /// The principal square root `sqrt(z)` at `precision` significant digits.
114    ///
115    /// Uses `re = sqrt((|z| + a)/2)`, `im = sign(b)·sqrt((|z| − a)/2)`, with the
116    /// purely-real input handled by an exact axis split and the radicands
117    /// clamped up to zero before the real `sqrt` to absorb rounding noise. The
118    /// branch chosen has `re ≥ 0` and matches the IEEE-754 / `num-complex`
119    /// principal value (so `sqrt(-1) = +i`).
120    ///
121    /// # Errors
122    ///
123    /// Propagates errors from [`oxinum_float::sqrt`] (none expected: radicands
124    /// are clamped non-negative).
125    pub fn sqrt(&self, precision: usize) -> OxiNumResult<CBig> {
126        let zero = DBig::from(0u32);
127
128        if self.is_zero() {
129            return Ok(CBig::zero());
130        }
131
132        // Real-axis fast path: b == 0.
133        if self.im == zero {
134            if self.re >= zero {
135                // a >= 0: re = √a, im = 0.
136                return Ok(CBig::from_parts(sqrt(&self.re, precision)?, zero));
137            }
138            // a < 0: re = 0, im = √(−a)  (so sqrt(-1) = +i).
139            let neg_a = -&self.re;
140            return Ok(CBig::from_parts(zero, sqrt(&neg_a, precision)?));
141        }
142
143        // General case.
144        let guard = precision + GUARD;
145        let m = sqrt(&self.norm_sqr(), guard)?; // m = |z|
146        let two = make_dbig("2.0")?;
147
148        // re = sqrt((m + a) / 2), clamping a tiny-negative radicand up to 0.
149        let mut r_re = &(&m + &self.re) / &two;
150        if r_re < zero {
151            r_re = DBig::from(0u32);
152        }
153
154        // im_mag = sqrt((m − a) / 2), same clamp.
155        let mut r_im = &(&m - &self.re) / &two;
156        if r_im < zero {
157            r_im = DBig::from(0u32);
158        }
159
160        let re = sqrt(&r_re, precision)?;
161        let im_mag = sqrt(&r_im, precision)?;
162
163        // Apply sign(b): for b < 0 the imaginary part is negative.
164        let im = if self.im < zero { -im_mag } else { im_mag };
165
166        Ok(CBig::from_parts(re, im))
167    }
168
169    /// The complex power `z^w = exp(w · ln z)` at `precision` significant
170    /// digits.
171    ///
172    /// The zero base is handled by convention: `0^0 = 1` and `0^w = 0` for any
173    /// other `w` (avoiding `ln(0)`).
174    ///
175    /// # Errors
176    ///
177    /// Propagates errors from [`CBig::ln`] / [`CBig::exp`].
178    pub fn pow(&self, w: &CBig, precision: usize) -> OxiNumResult<CBig> {
179        if self.is_zero() {
180            return if w.is_zero() {
181                Ok(CBig::one())
182            } else {
183                Ok(CBig::zero())
184            };
185        }
186        let guard = precision + GUARD;
187        let lz = self.ln(guard)?;
188        let prod = w * &lz;
189        prod.exp(precision)
190    }
191}
192
193// ---------------------------------------------------------------------------
194// Tests
195// ---------------------------------------------------------------------------
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200    use oxinum_float::compute_pi;
201
202    /// Build π at the given decimal precision.
203    fn pi(precision: usize) -> DBig {
204        compute_pi(precision)
205    }
206
207    #[test]
208    fn exp_i_pi_is_minus_one() {
209        // exp(iπ) ≈ −1 + 0i  (Euler's identity).
210        let z = CBig::from_parts(DBig::from(0u32), pi(50));
211        let r = z.exp(40).expect("exp");
212        let (re, im) = r.to_f64_parts();
213        assert!((re + 1.0).abs() < 1e-12, "re = {re}");
214        assert!(im.abs() < 1e-12, "im = {im}");
215    }
216
217    #[test]
218    fn ln_minus_one_is_i_pi() {
219        let z = CBig::from_real(make_dbig("-1.0").expect("literal"));
220        let r = z.ln(40).expect("ln");
221        let (re, im) = r.to_f64_parts();
222        assert!(re.abs() < 1e-12, "re = {re}");
223        assert!((im - std::f64::consts::PI).abs() < 1e-12, "im = {im}");
224    }
225
226    #[test]
227    fn ln_zero_is_domain_error() {
228        let r = CBig::zero().ln(40);
229        assert!(matches!(r, Err(OxiNumError::Domain(_))), "got {r:?}");
230    }
231
232    #[test]
233    fn sqrt_minus_one_is_i() {
234        let z = CBig::from_real(make_dbig("-1.0").expect("literal"));
235        let r = z.sqrt(40).expect("sqrt");
236        let (re, im) = r.to_f64_parts();
237        assert!(re.abs() < 1e-12, "re = {re}");
238        assert!((im - 1.0).abs() < 1e-12, "im = {im}");
239    }
240
241    #[test]
242    fn sqrt_two_i_is_one_plus_i() {
243        // sqrt(2i) = 1 + i.
244        let z = CBig::from_parts(DBig::from(0u32), DBig::from(2u32));
245        let r = z.sqrt(40).expect("sqrt");
246        let (re, im) = r.to_f64_parts();
247        assert!((re - 1.0).abs() < 1e-12, "re = {re}");
248        assert!((im - 1.0).abs() < 1e-12, "im = {im}");
249    }
250
251    #[test]
252    fn sqrt_positive_real() {
253        // sqrt(4) = 2.
254        let z = CBig::from_real(DBig::from(4u32));
255        let r = z.sqrt(40).expect("sqrt");
256        let (re, im) = r.to_f64_parts();
257        assert!((re - 2.0).abs() < 1e-12, "re = {re}");
258        assert!(im.abs() < 1e-12, "im = {im}");
259    }
260
261    #[test]
262    fn abs_three_four_is_five() {
263        let z = CBig::from_parts(DBig::from(3u32), DBig::from(4u32));
264        let m = z.abs(40).expect("abs");
265        assert!(m.to_string().starts_with('5'), "|3+4i| = {m}");
266    }
267
268    #[test]
269    fn abs_zero_is_zero() {
270        let m = CBig::zero().abs(40).expect("abs");
271        assert_eq!(m, DBig::from(0u32));
272    }
273
274    #[test]
275    fn arg_of_i_is_half_pi() {
276        let a = CBig::i().arg(40).expect("arg");
277        let v = a.to_f64();
278        assert!(
279            (v.value() - std::f64::consts::FRAC_PI_2).abs() < 1e-12,
280            "arg(i) = {a}"
281        );
282    }
283
284    #[test]
285    fn pow_i_squared_is_minus_one() {
286        // i² = −1.
287        let r = CBig::i()
288            .pow(&CBig::from_real(DBig::from(2u32)), 40)
289            .expect("pow");
290        let (re, im) = r.to_f64_parts();
291        assert!((re + 1.0).abs() < 1e-12, "re = {re}");
292        assert!(im.abs() < 1e-12, "im = {im}");
293    }
294
295    #[test]
296    fn pow_to_one_is_identity() {
297        // z^1 ≈ z on the real axis (arg = 0), where the round trip through
298        // ln/exp is exact and independent of the underlying `atan2`.
299        let z = CBig::from_real(make_dbig("2.0").expect("literal"));
300        let r = z.pow(&CBig::one(), 40).expect("pow");
301        let (re, im) = r.to_f64_parts();
302        assert!((re - 2.0).abs() < 1e-12, "re = {re}");
303        assert!(im.abs() < 1e-12, "im = {im}");
304    }
305
306    #[test]
307    fn pow_zero_zero_is_one() {
308        let r = CBig::zero().pow(&CBig::zero(), 40).expect("pow");
309        assert!(r == CBig::one());
310    }
311
312    #[test]
313    fn pow_zero_base_nonzero_exp_is_zero() {
314        let r = CBig::zero()
315            .pow(&CBig::from_parts(DBig::from(2u32), DBig::from(1u32)), 40)
316            .expect("pow");
317        assert!(r.is_zero());
318    }
319}