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}