Skip to main content

oxinum_complex/
inverse_trig.rs

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