Skip to main content

oxinum_complex/
trig.rs

1//! Trigonometric and hyperbolic functions for [`crate::CBig`].
2//!
3//! The decimal [`DBig`] foundation (re-exported from [`oxinum_float`]) supplies
4//! the four real building blocks [`sin`](oxinum_float::sin),
5//! [`cos`](oxinum_float::cos), [`sinh`](oxinum_float::sinh), and
6//! [`cosh`](oxinum_float::cosh) as free functions
7//! `(x: &DBig, precision: usize) -> OxiNumResult<DBig>`. From them, with
8//! `z = a + b·i`, the complex identities are assembled component-wise:
9//!
10//! ```text
11//! sin z  = sin a · cosh b + i · cos a · sinh b
12//! cos z  = cos a · cosh b − i · sin a · sinh b
13//! sinh z = sinh a · cos b + i · cosh a · sin b
14//! cosh z = cosh a · cos b + i · sinh a · sin b
15//! tan z  = sin z / cos z          (via checked_div)
16//! tanh z = sinh z / cosh z        (via checked_div)
17//! ```
18//!
19//! Every intermediate real scalar is evaluated at the working precision
20//! `precision + GUARD` so that the final products carry guard digits; the
21//! genuine divisions in `tan`/`tanh` route through the panic-free
22//! [`CBig::checked_div`], which yields
23//! [`OxiNumError::DivByZero`](oxinum_core::OxiNumError::DivByZero) exactly at
24//! the poles where the complex denominator (`cos z`, resp. `cosh z`) collapses
25//! to zero.
26
27use oxinum_float::{cos, cosh, sin, sinh};
28
29use crate::{CBig, OxiNumResult};
30
31/// Working-precision headroom added on top of the requested `precision`.
32const GUARD: usize = 10;
33
34impl CBig {
35    /// The complex sine
36    /// `sin z = sin a · cosh b + i · cos a · sinh b` at `precision` digits.
37    ///
38    /// # Errors
39    ///
40    /// Propagates errors from the real `sin`/`cos`/`sinh`/`cosh` routines.
41    pub fn sin(&self, precision: usize) -> OxiNumResult<CBig> {
42        let guard = precision.saturating_add(GUARD);
43        let a = &self.re;
44        let b = &self.im;
45
46        let sa = sin(a, guard)?;
47        let ca = cos(a, guard)?;
48        let shb = sinh(b, guard)?;
49        let chb = cosh(b, guard)?;
50
51        let re = &sa * &chb;
52        let im = &ca * &shb;
53        Ok(CBig::from_parts(re, im))
54    }
55
56    /// The complex cosine
57    /// `cos z = cos a · cosh b − i · sin a · sinh b` at `precision` digits.
58    ///
59    /// # Errors
60    ///
61    /// Propagates errors from the real `sin`/`cos`/`sinh`/`cosh` routines.
62    pub fn cos(&self, precision: usize) -> OxiNumResult<CBig> {
63        let guard = precision.saturating_add(GUARD);
64        let a = &self.re;
65        let b = &self.im;
66
67        let sa = sin(a, guard)?;
68        let ca = cos(a, guard)?;
69        let shb = sinh(b, guard)?;
70        let chb = cosh(b, guard)?;
71
72        let re = &ca * &chb;
73        // im = −(sin a · sinh b)
74        let im = -(&sa * &shb);
75        Ok(CBig::from_parts(re, im))
76    }
77
78    /// The complex hyperbolic sine
79    /// `sinh z = sinh a · cos b + i · cosh a · sin b` at `precision` digits.
80    ///
81    /// # Errors
82    ///
83    /// Propagates errors from the real `sin`/`cos`/`sinh`/`cosh` routines.
84    pub fn sinh(&self, precision: usize) -> OxiNumResult<CBig> {
85        let guard = precision.saturating_add(GUARD);
86        let a = &self.re;
87        let b = &self.im;
88
89        let sha = sinh(a, guard)?;
90        let cha = cosh(a, guard)?;
91        let sb = sin(b, guard)?;
92        let cb = cos(b, guard)?;
93
94        let re = &sha * &cb;
95        let im = &cha * &sb;
96        Ok(CBig::from_parts(re, im))
97    }
98
99    /// The complex hyperbolic cosine
100    /// `cosh z = cosh a · cos b + i · sinh a · sin b` at `precision` digits.
101    ///
102    /// # Errors
103    ///
104    /// Propagates errors from the real `sin`/`cos`/`sinh`/`cosh` routines.
105    pub fn cosh(&self, precision: usize) -> OxiNumResult<CBig> {
106        let guard = precision.saturating_add(GUARD);
107        let a = &self.re;
108        let b = &self.im;
109
110        let sha = sinh(a, guard)?;
111        let cha = cosh(a, guard)?;
112        let sb = sin(b, guard)?;
113        let cb = cos(b, guard)?;
114
115        let re = &cha * &cb;
116        let im = &sha * &sb;
117        Ok(CBig::from_parts(re, im))
118    }
119
120    /// The complex tangent `tan z = sin z / cos z` at `precision` digits.
121    ///
122    /// # Errors
123    ///
124    /// - [`OxiNumError::DivByZero`](oxinum_core::OxiNumError::DivByZero) at the
125    ///   poles where `cos z = 0`.
126    /// - Propagates errors from the real `sin`/`cos`/`sinh`/`cosh` routines.
127    pub fn tan(&self, precision: usize) -> OxiNumResult<CBig> {
128        self.sin(precision)?.checked_div(&self.cos(precision)?)
129    }
130
131    /// The complex hyperbolic tangent `tanh z = sinh z / cosh z` at
132    /// `precision` digits.
133    ///
134    /// # Errors
135    ///
136    /// - [`OxiNumError::DivByZero`](oxinum_core::OxiNumError::DivByZero) at the
137    ///   poles where `cosh z = 0`.
138    /// - Propagates errors from the real `sin`/`cos`/`sinh`/`cosh` routines.
139    pub fn tanh(&self, precision: usize) -> OxiNumResult<CBig> {
140        self.sinh(precision)?.checked_div(&self.cosh(precision)?)
141    }
142}
143
144// ---------------------------------------------------------------------------
145// Tests
146// ---------------------------------------------------------------------------
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    const PREC: usize = 40;
153
154    /// Build a `CBig` from two `f64`s.
155    fn c(re: f64, im: f64) -> CBig {
156        CBig::from_f64(re, im).expect("finite parts")
157    }
158
159    #[test]
160    fn sin_zero_is_zero() {
161        let s = CBig::zero().sin(PREC).expect("sin");
162        let (re, im) = s.to_f64_parts();
163        assert!(re.abs() < 1e-12, "re = {re}");
164        assert!(im.abs() < 1e-12, "im = {im}");
165    }
166
167    #[test]
168    fn cos_zero_is_one() {
169        let cz = CBig::zero().cos(PREC).expect("cos");
170        let (re, im) = cz.to_f64_parts();
171        assert!((re - 1.0).abs() < 1e-12, "re = {re}");
172        assert!(im.abs() < 1e-12, "im = {im}");
173    }
174
175    #[test]
176    fn sinh_zero_is_zero() {
177        let s = CBig::zero().sinh(PREC).expect("sinh");
178        let (re, im) = s.to_f64_parts();
179        assert!(re.abs() < 1e-12, "re = {re}");
180        assert!(im.abs() < 1e-12, "im = {im}");
181    }
182
183    #[test]
184    fn cosh_zero_is_one() {
185        let cz = CBig::zero().cosh(PREC).expect("cosh");
186        let (re, im) = cz.to_f64_parts();
187        assert!((re - 1.0).abs() < 1e-12, "re = {re}");
188        assert!(im.abs() < 1e-12, "im = {im}");
189    }
190
191    #[test]
192    fn pythagorean_identity_general() {
193        // sin²z + cos²z ≈ 1 at z = 0.5 + 0.3i, using the public Mul/Add ops.
194        let z = c(0.5, 0.3);
195        let s = z.sin(PREC).expect("sin");
196        let co = z.cos(PREC).expect("cos");
197        let sum = &(&s * &s) + &(&co * &co);
198        let (re, im) = sum.to_f64_parts();
199        assert!((re - 1.0).abs() < 1e-9, "re(sum) = {re}");
200        assert!(im.abs() < 1e-9, "im(sum) = {im}");
201    }
202
203    #[test]
204    fn cosh_sq_minus_sinh_sq_is_one() {
205        // cosh²z − sinh²z ≈ 1 at z = 0.4 + 0.7i, using the public ops.
206        let z = c(0.4, 0.7);
207        let ch = z.cosh(PREC).expect("cosh");
208        let sh = z.sinh(PREC).expect("sinh");
209        let diff = &(&ch * &ch) - &(&sh * &sh);
210        let (re, im) = diff.to_f64_parts();
211        assert!((re - 1.0).abs() < 1e-9, "re(diff) = {re}");
212        assert!(im.abs() < 1e-9, "im(diff) = {im}");
213    }
214
215    #[test]
216    fn tan_zero_is_zero() {
217        let t = CBig::zero().tan(PREC).expect("tan");
218        let (re, im) = t.to_f64_parts();
219        assert!(re.abs() < 1e-12, "re = {re}");
220        assert!(im.abs() < 1e-12, "im = {im}");
221    }
222
223    #[test]
224    fn tanh_zero_is_zero() {
225        let t = CBig::zero().tanh(PREC).expect("tanh");
226        let (re, im) = t.to_f64_parts();
227        assert!(re.abs() < 1e-12, "re = {re}");
228        assert!(im.abs() < 1e-12, "im = {im}");
229    }
230
231    #[test]
232    fn tan_matches_known_value() {
233        // (0.5 + 0.3i).tan() ≈ 0.48759231649213874 + 0.3689103968255638i
234        // (reference computed with the standard complex-tan formula).
235        let z = c(0.5, 0.3);
236        let t = z.tan(PREC).expect("tan");
237        let (re, im) = t.to_f64_parts();
238        assert!((re - 0.487_592_316_492_138_74).abs() < 1e-9, "re = {re}");
239        assert!((im - 0.368_910_396_825_563_8).abs() < 1e-9, "im = {im}");
240    }
241
242    #[test]
243    fn tan_is_sin_over_cos() {
244        // tan z is sin z / cos z by construction; confirm the public path.
245        let z = c(0.5, 0.3);
246        let t = z.tan(PREC).expect("tan");
247        let q = z
248            .sin(PREC)
249            .expect("sin")
250            .checked_div(&z.cos(PREC).expect("cos"))
251            .expect("non-zero cos");
252        let (tre, tim) = t.to_f64_parts();
253        let (qre, qim) = q.to_f64_parts();
254        assert!((tre - qre).abs() < 1e-12, "re: {tre} vs {qre}");
255        assert!((tim - qim).abs() < 1e-12, "im: {tim} vs {qim}");
256    }
257}