Skip to main content

oxinum_complex/native/
inverse_trig.rs

1//! Inverse trigonometric and hyperbolic functions for native [`BigComplex`].
2//!
3//! Mirrors `src/inverse_trig.rs` exactly in formula choice; see that file for
4//! the DLMF / C99 principal-branch derivations. Here each method takes an
5//! explicit `(prec: u32, mode: RoundingMode)` pair following the `BigComplex`
6//! API convention.
7//!
8//! All intermediate computations run at a guard precision of `prec + GUARD`
9//! bits; the final result retains whatever precision the chain of
10//! `ln`/`sqrt`/`mul_i` operations delivered (which is at least `prec` bits).
11
12use oxinum_core::OxiNumResult;
13use oxinum_float::native::{BigFloat, RoundingMode};
14
15use super::BigComplex;
16
17/// Working-precision headroom added on top of the requested `prec`.
18const GUARD: u32 = 10;
19
20/// Multiply `z` by `i`: `i·(a+bi) = −b + a·i`.
21#[inline]
22fn mul_i(z: BigComplex) -> BigComplex {
23    BigComplex {
24        re: -&z.im,
25        im: z.re,
26    }
27}
28
29/// Multiply `z` by `−i`: `−i·(a+bi) = b − a·i`.
30#[inline]
31fn mul_neg_i(z: BigComplex) -> BigComplex {
32    BigComplex {
33        re: z.im,
34        im: -&z.re,
35    }
36}
37
38impl BigComplex {
39    /// The principal value of `arcsin z` at `prec` bits.
40    ///
41    /// Uses the identity `asin z = −i · ln(i·z + √(1 − z²))`.
42    ///
43    /// # Errors
44    ///
45    /// Propagates errors from [`BigComplex::sqrt`] / [`BigComplex::ln`].
46    pub fn asin(&self, prec: u32, mode: RoundingMode) -> OxiNumResult<BigComplex> {
47        if self.is_zero() {
48            return Ok(BigComplex::zero(prec));
49        }
50        let guard = prec.saturating_add(GUARD);
51
52        // i·z
53        let iz = mul_i(self.clone());
54        // z²
55        let z_sq = self.mul_core(self);
56        // 1 − z²
57        let one_c = BigComplex::one(guard, mode);
58        let one_minus_z2 = &one_c - &z_sq;
59        // √(1 − z²)
60        let sqrt_val = one_minus_z2.sqrt(guard, mode)?;
61        // i·z + √(1 − z²)
62        let arg = &iz + &sqrt_val;
63        // ln(...)
64        let ln_val = arg.ln(guard, mode)?;
65        // −i · ln(...)
66        Ok(mul_neg_i(ln_val))
67    }
68
69    /// The principal value of `arccos z` at `prec` bits.
70    ///
71    /// Uses the identity `acos z = −i · ln(z + i·√(1 − z²))`.
72    ///
73    /// # Errors
74    ///
75    /// Propagates errors from [`BigComplex::sqrt`] / [`BigComplex::ln`].
76    pub fn acos(&self, prec: u32, mode: RoundingMode) -> OxiNumResult<BigComplex> {
77        let guard = prec.saturating_add(GUARD);
78
79        // z²
80        let z_sq = self.mul_core(self);
81        // 1 − z²
82        let one_c = BigComplex::one(guard, mode);
83        let one_minus_z2 = &one_c - &z_sq;
84        // √(1 − z²)
85        let sqrt_val = one_minus_z2.sqrt(guard, mode)?;
86        // i·√(1 − z²)
87        let i_sqrt = mul_i(sqrt_val);
88        // z + i·√(1 − z²)
89        let arg = self + &i_sqrt;
90        // ln(...)
91        let ln_val = arg.ln(guard, mode)?;
92        // −i · ln(...)
93        Ok(mul_neg_i(ln_val))
94    }
95
96    /// The principal value of `arctan z` at `prec` bits.
97    ///
98    /// Uses the identity `atan z = (i/2)·[ln(1 − i·z) − ln(1 + i·z)]`.
99    ///
100    /// # Errors
101    ///
102    /// Propagates errors from [`BigComplex::ln`].
103    pub fn atan(&self, prec: u32, mode: RoundingMode) -> OxiNumResult<BigComplex> {
104        if self.is_zero() {
105            return Ok(BigComplex::zero(prec));
106        }
107        let guard = prec.saturating_add(GUARD);
108        let half = BigFloat::from_f64(0.5, guard)?;
109
110        // i·z
111        let iz = mul_i(self.clone());
112        let one_c = BigComplex::one(guard, mode);
113        // ln(1 − i·z)
114        let ln_minus = (&one_c - &iz).ln(guard, mode)?;
115        // ln(1 + i·z)
116        let ln_plus = (&one_c + &iz).ln(guard, mode)?;
117        // diff = ln(1 − i·z) − ln(1 + i·z)
118        let diff = &ln_minus - &ln_plus;
119        // multiply by i/2: first multiply by i, then scale each component by ½
120        let i_diff = mul_i(diff);
121        let re = (&i_diff.re * &half).with_precision(prec, mode);
122        let im = (&i_diff.im * &half).with_precision(prec, mode);
123        Ok(BigComplex { re, im })
124    }
125
126    /// The principal value of `arcsinh z` at `prec` bits.
127    ///
128    /// Uses the identity `asinh z = ln(z + √(z² + 1))`.
129    ///
130    /// # Errors
131    ///
132    /// Propagates errors from [`BigComplex::sqrt`] / [`BigComplex::ln`].
133    pub fn asinh(&self, prec: u32, mode: RoundingMode) -> OxiNumResult<BigComplex> {
134        if self.is_zero() {
135            return Ok(BigComplex::zero(prec));
136        }
137        let guard = prec.saturating_add(GUARD);
138
139        let one_c = BigComplex::one(guard, mode);
140        // z² + 1
141        let z_sq_plus_one = &self.mul_core(self) + &one_c;
142        // √(z² + 1)
143        let sqrt_val = z_sq_plus_one.sqrt(guard, mode)?;
144        // z + √(z² + 1)
145        let arg = self + &sqrt_val;
146        arg.ln(guard, mode)
147    }
148
149    /// The principal value of `arccosh z` at `prec` bits.
150    ///
151    /// Uses `acosh z = ln(z + √(z−1)·√(z+1))` (factored, not `√(z²−1)`) to
152    /// place the branch cut on `(−∞, 1]`, consistent with C99 / `num-complex`.
153    ///
154    /// # Errors
155    ///
156    /// Propagates errors from [`BigComplex::sqrt`] / [`BigComplex::ln`].
157    pub fn acosh(&self, prec: u32, mode: RoundingMode) -> OxiNumResult<BigComplex> {
158        let guard = prec.saturating_add(GUARD);
159
160        let one_c = BigComplex::one(guard, mode);
161        // √(z − 1)
162        let sq1 = (self - &one_c).sqrt(guard, mode)?;
163        // √(z + 1)
164        let sq2 = (self + &one_c).sqrt(guard, mode)?;
165        // z + √(z−1)·√(z+1)
166        let product = sq1.mul_core(&sq2);
167        let arg = self + &product;
168        arg.ln(guard, mode)
169    }
170
171    /// The principal value of `arctanh z` at `prec` bits.
172    ///
173    /// Uses the identity `atanh z = ½·[ln(1 + z) − ln(1 − z)]`.
174    ///
175    /// # Errors
176    ///
177    /// Propagates errors from [`BigComplex::ln`].
178    pub fn atanh(&self, prec: u32, mode: RoundingMode) -> OxiNumResult<BigComplex> {
179        if self.is_zero() {
180            return Ok(BigComplex::zero(prec));
181        }
182        let guard = prec.saturating_add(GUARD);
183        let half = BigFloat::from_f64(0.5, guard)?;
184
185        let one_c = BigComplex::one(guard, mode);
186        // ln(1 + z)
187        let ln_plus = (&one_c + self).ln(guard, mode)?;
188        // ln(1 − z)
189        let ln_minus = (&one_c - self).ln(guard, mode)?;
190        // diff = ln(1+z) − ln(1−z)
191        let diff = &ln_plus - &ln_minus;
192        // ½ · diff
193        let re = (&diff.re * &half).with_precision(prec, mode);
194        let im = (&diff.im * &half).with_precision(prec, mode);
195        Ok(BigComplex { re, im })
196    }
197}
198
199// ---------------------------------------------------------------------------
200// Tests
201// ---------------------------------------------------------------------------
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206
207    const PREC: u32 = 80;
208    const MODE: RoundingMode = RoundingMode::HalfEven;
209    const TOL: f64 = 1e-9;
210
211    fn c(re: f64, im: f64) -> BigComplex {
212        BigComplex::from_f64(re, im, PREC).expect("finite parts")
213    }
214
215    // ---- Zero fast-paths -------------------------------------------------------
216
217    #[test]
218    fn asin_zero_is_zero() {
219        let r = BigComplex::zero(PREC).asin(PREC, MODE).expect("asin");
220        assert!(r.re().to_f64().abs() < TOL, "re = {}", r.re().to_f64());
221        assert!(r.im().to_f64().abs() < TOL, "im = {}", r.im().to_f64());
222    }
223
224    #[test]
225    fn atan_zero_is_zero() {
226        let r = BigComplex::zero(PREC).atan(PREC, MODE).expect("atan");
227        assert!(r.re().to_f64().abs() < TOL, "re = {}", r.re().to_f64());
228        assert!(r.im().to_f64().abs() < TOL, "im = {}", r.im().to_f64());
229    }
230
231    #[test]
232    fn asinh_zero_is_zero() {
233        let r = BigComplex::zero(PREC).asinh(PREC, MODE).expect("asinh");
234        assert!(r.re().to_f64().abs() < TOL, "re = {}", r.re().to_f64());
235        assert!(r.im().to_f64().abs() < TOL, "im = {}", r.im().to_f64());
236    }
237
238    #[test]
239    fn atanh_zero_is_zero() {
240        let r = BigComplex::zero(PREC).atanh(PREC, MODE).expect("atanh");
241        assert!(r.re().to_f64().abs() < TOL, "re = {}", r.re().to_f64());
242        assert!(r.im().to_f64().abs() < TOL, "im = {}", r.im().to_f64());
243    }
244
245    // ---- Known-value checks ---------------------------------------------------
246
247    #[test]
248    fn asin_one_is_half_pi() {
249        // asin(1) = π/2.
250        let r = c(1.0, 0.0).asin(PREC, MODE).expect("asin");
251        let half_pi = std::f64::consts::FRAC_PI_2;
252        assert!(
253            (r.re().to_f64() - half_pi).abs() < TOL,
254            "re = {}, expected π/2 ≈ {half_pi}",
255            r.re().to_f64()
256        );
257        assert!(r.im().to_f64().abs() < TOL, "im = {}", r.im().to_f64());
258    }
259
260    #[test]
261    fn atan_one_is_quarter_pi() {
262        // atan(1) = π/4.
263        let r = c(1.0, 0.0).atan(PREC, MODE).expect("atan");
264        let quarter_pi = std::f64::consts::FRAC_PI_4;
265        assert!(
266            (r.re().to_f64() - quarter_pi).abs() < TOL,
267            "re = {}, expected π/4 ≈ {quarter_pi}",
268            r.re().to_f64()
269        );
270        assert!(r.im().to_f64().abs() < TOL, "im = {}", r.im().to_f64());
271    }
272
273    #[test]
274    fn acosh_one_is_zero() {
275        // acosh(1) = 0.
276        let r = c(1.0, 0.0).acosh(PREC, MODE).expect("acosh");
277        assert!(r.re().to_f64().abs() < TOL, "re = {}", r.re().to_f64());
278        assert!(r.im().to_f64().abs() < TOL, "im = {}", r.im().to_f64());
279    }
280
281    // ---- Round-trip identities ------------------------------------------------
282
283    #[test]
284    fn sin_asin_roundtrip() {
285        // sin(asin(0.3+0.4i)) ≈ 0.3+0.4i.
286        let z = c(0.3, 0.4);
287        let asin_z = z.asin(PREC, MODE).expect("asin");
288        let r = asin_z.sin(PREC, MODE).expect("sin");
289        assert!(
290            (r.re().to_f64() - 0.3).abs() < TOL,
291            "re = {}",
292            r.re().to_f64()
293        );
294        assert!(
295            (r.im().to_f64() - 0.4).abs() < TOL,
296            "im = {}",
297            r.im().to_f64()
298        );
299    }
300
301    #[test]
302    fn cos_acos_roundtrip() {
303        // cos(acos(0.3+0.4i)) ≈ 0.3+0.4i.
304        let z = c(0.3, 0.4);
305        let acos_z = z.acos(PREC, MODE).expect("acos");
306        let r = acos_z.cos(PREC, MODE).expect("cos");
307        assert!(
308            (r.re().to_f64() - 0.3).abs() < TOL,
309            "re = {}",
310            r.re().to_f64()
311        );
312        assert!(
313            (r.im().to_f64() - 0.4).abs() < TOL,
314            "im = {}",
315            r.im().to_f64()
316        );
317    }
318
319    #[test]
320    fn tanh_atanh_roundtrip() {
321        // tanh(atanh(0.2+0.1i)) ≈ 0.2+0.1i.
322        let z = c(0.2, 0.1);
323        let atanh_z = z.atanh(PREC, MODE).expect("atanh");
324        let r = atanh_z.tanh(PREC, MODE).expect("tanh");
325        assert!(
326            (r.re().to_f64() - 0.2).abs() < TOL,
327            "re = {}",
328            r.re().to_f64()
329        );
330        assert!(
331            (r.im().to_f64() - 0.1).abs() < TOL,
332            "im = {}",
333            r.im().to_f64()
334        );
335    }
336
337    #[test]
338    fn sinh_asinh_roundtrip() {
339        // sinh(asinh(0.3+0.4i)) ≈ 0.3+0.4i.
340        let z = c(0.3, 0.4);
341        let asinh_z = z.asinh(PREC, MODE).expect("asinh");
342        let r = asinh_z.sinh(PREC, MODE).expect("sinh");
343        assert!(
344            (r.re().to_f64() - 0.3).abs() < TOL,
345            "re = {}",
346            r.re().to_f64()
347        );
348        assert!(
349            (r.im().to_f64() - 0.4).abs() < TOL,
350            "im = {}",
351            r.im().to_f64()
352        );
353    }
354}