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}