Skip to main content

oxinum_float/native/
trig.rs

1//! Trigonometric functions for native `BigFloat`: sin, cos, tan.
2//!
3//! # Algorithms
4//!
5//! ## Argument reduction (quadrant)
6//!
7//! Given `x`, compute `q = round(x / (π/2))` and `u = x - q * (π/2)`.
8//! The remainder `u ∈ [−π/4, π/4]` satisfies `|u| ≤ π/4 ≈ 0.785`.
9//!
10//! For large `|x|` where `|q|` would overflow `i64`, the quotient is computed
11//! via `BigFloat` arithmetic: we extract the integer `q` from the BigFloat
12//! representation by shifting the mantissa using the stored exponent.
13//!
14//! The quadrant `q mod 4` maps to the final signs and sin/cos exchange:
15//!
16//! | quadrant | sin(x)   | cos(x)   |
17//! |----------|----------|----------|
18//! | 0        | +sin(u)  | +cos(u)  |
19//! | 1        | +cos(u)  | −sin(u)  |
20//! | 2        | −sin(u)  | −cos(u)  |
21//! | 3        | −cos(u)  | +sin(u)  |
22//!
23//! ## Taylor series for `|u| ≤ π/4`
24//!
25//! `sin(u) = u − u³/3! + u⁵/5! − …` with `n_terms = prec/2 + 10`.
26//! `cos(u) = 1 − u²/2! + u⁴/4! − …` with `n_terms = prec/2 + 10`.
27//!
28//! These term counts ensure convergence because for `|u| ≤ 1` the ratio
29//! `|u^(2k+1)| / (2k+1)!` decays faster than `(π/4)^(2k+1) / (2k+1)!`.
30//! At 2k+1 ≈ prec/2 terms, this is well below `2^{-prec}`.
31
32use oxinum_core::{OxiNumError, OxiNumResult};
33
34use super::constants::pi;
35use super::float::{BigFloat, RoundingMode};
36
37// ---------------------------------------------------------------------------
38// Taylor series helpers
39// ---------------------------------------------------------------------------
40
41/// Compute `sin(u)` via Taylor series for `|u| ≤ π/4`.
42///
43/// `sin(u) = u − u³/3! + u⁵/5! − …`
44///
45/// Iterative form: `term_0 = u`; `term_k = −term_{k−1} * u² / ((2k)(2k+1))`.
46fn sin_taylor(u: &BigFloat, prec: u32, mode: RoundingMode) -> OxiNumResult<BigFloat> {
47    if u.is_zero() {
48        return Ok(BigFloat::zero(prec));
49    }
50
51    // u² at work precision.
52    let u_sq = u.mul_ref_with_mode(u, mode).with_precision(prec, mode);
53
54    // term = u; result = u.
55    let mut term = u.clone().with_precision(prec, mode);
56    let mut result = term.clone();
57
58    // n_terms is a safe upper bound for convergence at |u| ≤ π/4.
59    let n_terms: u64 = (prec as u64) / 2 + 10;
60
61    for k in 1..=n_terms {
62        // term *= u² / ((2k)(2k+1))
63        // Denominator: (2k)(2k+1) fits in i64 for all practical k.
64        let denom_val = (2 * k) * (2 * k + 1);
65        // Guard against enormous k turning denom > i64::MAX (unreachable in
66        // practice: prec ≤ 2^32 so n_terms ≤ 2^31+10, denom ≤ 4*(2^31)^2 > i64,
67        // but also at that scale |u|^(2k+1)/(2k+1)! is exactly 0 in float).
68        // Use saturating to avoid panic; if it saturates the term is negligible.
69        let denom_i64 = denom_val.min(i64::MAX as u64) as i64;
70        let denom_f = BigFloat::from_i64(denom_i64, prec, mode);
71        // term = -(term * u_sq) / denom
72        let numer = term
73            .mul_ref_with_mode(&u_sq, mode)
74            .with_precision(prec, mode);
75        term = numer
76            .div_ref_with_mode(&denom_f, mode)
77            .map_err(|e| OxiNumError::Precision(format!("sin_taylor denom zero: {e}").into()))?;
78        term = term.neg().with_precision(prec, mode);
79        result = result
80            .add_ref_with_mode(&term, mode)
81            .with_precision(prec, mode);
82    }
83
84    Ok(result.with_precision(prec, mode))
85}
86
87/// Compute `cos(u)` via Taylor series for `|u| ≤ π/4`.
88///
89/// `cos(u) = 1 − u²/2! + u⁴/4! − …`
90///
91/// Iterative form: `term_0 = 1`; `term_k = −term_{k−1} * u² / ((2k−1)(2k))`.
92fn cos_taylor(u: &BigFloat, prec: u32, mode: RoundingMode) -> OxiNumResult<BigFloat> {
93    // u² at work precision.
94    let u_sq = u.mul_ref_with_mode(u, mode).with_precision(prec, mode);
95
96    // term = 1; result = 1.
97    let one = BigFloat::from_i64(1, prec, mode);
98    let mut term = one.clone();
99    let mut result = one;
100
101    let n_terms: u64 = (prec as u64) / 2 + 10;
102
103    for k in 1..=n_terms {
104        let denom_val = (2 * k - 1) * (2 * k);
105        let denom_i64 = denom_val.min(i64::MAX as u64) as i64;
106        let denom_f = BigFloat::from_i64(denom_i64, prec, mode);
107        let numer = term
108            .mul_ref_with_mode(&u_sq, mode)
109            .with_precision(prec, mode);
110        term = numer
111            .div_ref_with_mode(&denom_f, mode)
112            .map_err(|e| OxiNumError::Precision(format!("cos_taylor denom zero: {e}").into()))?;
113        term = term.neg().with_precision(prec, mode);
114        result = result
115            .add_ref_with_mode(&term, mode)
116            .with_precision(prec, mode);
117    }
118
119    Ok(result.with_precision(prec, mode))
120}
121
122// ---------------------------------------------------------------------------
123// Argument reduction: extract integer quotient from BigFloat
124// ---------------------------------------------------------------------------
125
126/// Extract the integer part (floor toward zero) of a `BigFloat`.
127///
128/// Returns `None` if the value is too large to represent in `i64` (exponent
129/// plus bit-length would overflow). Returns `0` for sub-unit values.
130fn bigfloat_to_i64_round(x: &BigFloat) -> Option<i64> {
131    // The value is (-1)^s * mantissa * 2^e.
132    // The integer part is non-zero only when e + bit_length > 0 (i.e. the
133    // value has magnitude >= 1).
134    if x.is_zero() {
135        return Some(0);
136    }
137    let e = x.exponent();
138    let bits = x.mantissa().bit_length() as i64;
139    // Effective position of the top bit in the integer: e + bits - 1.
140    let top_pos = e.saturating_add(bits - 1);
141    if top_pos < 0 {
142        // |value| < 1, integer part is 0.
143        return Some(0);
144    }
145    if top_pos >= 63 {
146        // Too large to represent in i64.
147        return None;
148    }
149    // The integer part has at most `top_pos + 1` bits which is < 63.
150    // integer_part_biguint = mantissa >> (-e) when e < 0, or mantissa << e when e >= 0.
151    // We operate in BigUint space so the shift is exact regardless of mantissa size.
152    let int_biguint = if e >= 0 {
153        // Left-shift: value = mantissa * 2^e. The integer part is the full value.
154        // top_pos < 63 guarantees this fits in i64.
155        x.mantissa().shl_bits(e as u64)
156    } else {
157        // Right-shift: integer part = mantissa >> (-e).
158        let shift = (-e) as u64;
159        x.mantissa().shr_bits(shift)
160    };
161    let int_mag = int_biguint.to_u64()?;
162    // Apply sign.
163    if x.signum() < 0 {
164        // Negative: int_mag fits in i64 because top_pos < 63.
165        Some(-(int_mag as i64))
166    } else {
167        Some(int_mag as i64)
168    }
169}
170
171/// Compute `q = round(x / (π/2))` as an `i64` at work precision.
172///
173/// Returns `None` if `|q|` is too large to represent in `i64` (i.e. `|x| ≥
174/// 2^62 * π/2 ≈ 7e18`). Callers should propagate a `Precision` error in that
175/// case.
176fn round_div_pi_over_2(
177    x: &BigFloat,
178    pi_over_2: &BigFloat,
179    work_prec: u32,
180    mode: RoundingMode,
181) -> Option<i64> {
182    let ratio = x
183        .clone()
184        .with_precision(work_prec, mode)
185        .div_ref_with_mode(pi_over_2, mode)
186        .ok()?;
187
188    // Add ±0.5 and take the floor to implement rounding.
189    let half = BigFloat::from_f64(0.5, work_prec).ok()?;
190    let shifted = if ratio.signum() >= 0 {
191        ratio.add_ref_with_mode(&half, mode)
192    } else {
193        ratio.sub_ref_with_mode(&half, mode)
194    };
195    // Extract integer part.
196    bigfloat_to_i64_round(&shifted)
197}
198
199// ---------------------------------------------------------------------------
200// Core sin/cos implementation
201// ---------------------------------------------------------------------------
202
203/// Compute `(sin(x), cos(x))` at `prec` bits via argument reduction + Taylor.
204pub(crate) fn sincos_impl(
205    x: &BigFloat,
206    prec: u32,
207    mode: RoundingMode,
208) -> OxiNumResult<(BigFloat, BigFloat)> {
209    // Guard bits: extra bits to absorb cancellation during argument reduction.
210    // The reduction computes u = x − q*(π/2). For large |x| we need extra
211    // precision to get the low bits of u right. We use |exponent| + 32 as
212    // a conservative guard.
213    let exp_guard = if x.exponent() > 0 {
214        (x.exponent() as u32).min(256)
215    } else {
216        0u32
217    };
218    let work_prec = prec.saturating_add(exp_guard).saturating_add(32);
219
220    // π/2 at work precision.
221    let pi_val = pi(work_prec)?;
222    let two = BigFloat::from_i64(2, work_prec, mode);
223    let pi_over_2 = pi_val.div_ref_with_mode(&two, mode)?;
224
225    // Compute q = round(x / (π/2)).
226    let q = match round_div_pi_over_2(x, &pi_over_2, work_prec, mode) {
227        Some(v) => v,
228        None => {
229            return Err(OxiNumError::Precision(
230                "sin/cos: argument magnitude too large (|x| > 2^62 * π/2); \
231                 use argument reduction before calling"
232                    .into(),
233            ));
234        }
235    };
236    let quadrant = q.rem_euclid(4) as u32;
237
238    // u = x − q * (π/2).
239    let q_bf = BigFloat::from_i64(q, work_prec, mode);
240    let u = x
241        .clone()
242        .with_precision(work_prec, mode)
243        .sub_ref_with_mode(&q_bf.mul_ref_with_mode(&pi_over_2, mode), mode);
244
245    // Taylor / binary-splitting series for sin(u) and cos(u) at work precision.
246    // Above BS_THRESHOLD_BITS, the binary-splitting path replaces both Taylor
247    // calls and is strictly faster at high precision.
248    let (sin_u, cos_u) = if prec >= super::bs_transcendental::BS_THRESHOLD_BITS {
249        super::bs_transcendental::sincos_bs(&u, work_prec, mode)?
250    } else {
251        let s = sin_taylor(&u, work_prec, mode)?;
252        let c = cos_taylor(&u, work_prec, mode)?;
253        (s, c)
254    };
255
256    // Apply quadrant table.
257    let (sin_x, cos_x) = match quadrant {
258        0 => (sin_u, cos_u),
259        1 => (cos_u, sin_u.neg()),
260        2 => (sin_u.neg(), cos_u.neg()),
261        3 => (cos_u.neg(), sin_u),
262        _ => unreachable!("rem_euclid(4) always in 0..=3"),
263    };
264
265    Ok((
266        sin_x.with_precision(prec, mode),
267        cos_x.with_precision(prec, mode),
268    ))
269}
270
271// ---------------------------------------------------------------------------
272// Public BigFloat methods
273// ---------------------------------------------------------------------------
274
275impl BigFloat {
276    /// Return `sin(self)` at `prec` bits using the given rounding mode.
277    ///
278    /// Uses argument reduction mod π/2 and a Taylor series convergent for
279    /// `|u| ≤ π/4`.
280    ///
281    /// # Errors
282    ///
283    /// - [`OxiNumError::Precision`] if `|self|` is too large for the internal
284    ///   argument-reduction step (approximately `|x| > 2^62 · π/2 ≈ 7.2·10^18`).
285    ///
286    /// # Examples
287    ///
288    /// ```
289    /// use oxinum_float::native::{BigFloat, RoundingMode};
290    /// let zero = BigFloat::zero(64);
291    /// let s = zero.sin(64, RoundingMode::HalfEven).expect("sin(0)");
292    /// assert!(s.is_zero());
293    /// ```
294    pub fn sin(&self, prec: u32, mode: RoundingMode) -> OxiNumResult<BigFloat> {
295        // IEEE-754: sin(NaN) = NaN, sin(±Inf) = NaN
296        if self.is_nan() || self.is_infinite() {
297            return Ok(BigFloat::nan(prec));
298        }
299        if self.is_zero() {
300            return Ok(BigFloat::zero(prec));
301        }
302        let (s, _c) = sincos_impl(self, prec, mode)?;
303        Ok(s)
304    }
305
306    /// Return `cos(self)` at `prec` bits using the given rounding mode.
307    ///
308    /// # Errors
309    ///
310    /// Same as [`BigFloat::sin`].
311    ///
312    /// # Examples
313    ///
314    /// ```
315    /// use oxinum_float::native::{BigFloat, RoundingMode};
316    /// let zero = BigFloat::zero(64);
317    /// let c = zero.cos(64, RoundingMode::HalfEven).expect("cos(0)");
318    /// assert_eq!(c.to_f64(), 1.0);
319    /// ```
320    pub fn cos(&self, prec: u32, mode: RoundingMode) -> OxiNumResult<BigFloat> {
321        // IEEE-754: cos(NaN) = NaN, cos(±Inf) = NaN
322        if self.is_nan() || self.is_infinite() {
323            return Ok(BigFloat::nan(prec));
324        }
325        let (_s, c) = sincos_impl(self, prec, mode)?;
326        Ok(c)
327    }
328
329    /// Return `tan(self)` at `prec` bits using the given rounding mode.
330    ///
331    /// # Errors
332    ///
333    /// - [`OxiNumError::Domain`] if `cos(self) = 0` (i.e. `self = π/2 + k·π`).
334    /// - [`OxiNumError::Precision`] for very large `|self|`.
335    ///
336    /// # Examples
337    ///
338    /// ```
339    /// use oxinum_float::native::{BigFloat, RoundingMode};
340    /// let pi_over_4 = {
341    ///     use oxinum_float::native::pi;
342    ///     let p = pi(64).expect("pi");
343    ///     let four = BigFloat::from_i64(4, 64, RoundingMode::HalfEven);
344    ///     p.div_ref(&four).expect("div")
345    /// };
346    /// let t = pi_over_4.tan(64, RoundingMode::HalfEven).expect("tan(π/4)");
347    /// assert!((t.to_f64() - 1.0).abs() < 1e-14);
348    /// ```
349    pub fn tan(&self, prec: u32, mode: RoundingMode) -> OxiNumResult<BigFloat> {
350        // IEEE-754: tan(NaN) = NaN, tan(±Inf) = NaN
351        if self.is_nan() || self.is_infinite() {
352            return Ok(BigFloat::nan(prec));
353        }
354        let (s, c) = sincos_impl(self, prec, mode)?;
355        if c.is_zero() {
356            return Err(OxiNumError::Domain("tan undefined at π/2 + k·π".into()));
357        }
358        let result = s.div_ref_with_mode(&c, mode)?;
359        Ok(result.with_precision(prec, mode))
360    }
361}
362
363// ---------------------------------------------------------------------------
364// Internal tests
365// ---------------------------------------------------------------------------
366
367#[cfg(test)]
368mod tests {
369    use super::*;
370
371    fn mk(n: i64, prec: u32) -> BigFloat {
372        BigFloat::from_i64(n, prec, RoundingMode::HalfEven)
373    }
374
375    #[test]
376    fn sin_zero_is_zero() {
377        let z = mk(0, 64);
378        let s = z.sin(64, RoundingMode::HalfEven).expect("sin(0)");
379        assert!(s.is_zero());
380    }
381
382    #[test]
383    fn cos_zero_is_one() {
384        let z = mk(0, 64);
385        let c = z.cos(64, RoundingMode::HalfEven).expect("cos(0)");
386        assert!((c.to_f64() - 1.0).abs() < 1e-14, "cos(0) = {}", c.to_f64());
387    }
388
389    #[test]
390    fn sin_pi_over_2_is_one() {
391        let prec = 64u32;
392        let p = pi(prec).expect("pi");
393        let two = mk(2, prec);
394        let pi_over_2 = p
395            .div_ref_with_mode(&two, RoundingMode::HalfEven)
396            .expect("pi/2");
397        let s = pi_over_2
398            .sin(prec, RoundingMode::HalfEven)
399            .expect("sin(pi/2)");
400        assert!(
401            (s.to_f64() - 1.0).abs() < 1e-14,
402            "sin(π/2) = {}",
403            s.to_f64()
404        );
405    }
406
407    #[test]
408    fn pythagorean_identity_at_f64_angle() {
409        let prec = 100u32;
410        for x_f64 in [0.3f64, 1.0, 2.7, -1.5, 5.0, -2.9] {
411            let x = BigFloat::from_f64(x_f64, prec).expect("from_f64");
412            let s = x.sin(prec, RoundingMode::HalfEven).expect("sin");
413            let c = x.cos(prec, RoundingMode::HalfEven).expect("cos");
414            let sum = s
415                .mul_ref_with_mode(&s, RoundingMode::HalfEven)
416                .add_ref_with_mode(
417                    &c.mul_ref_with_mode(&c, RoundingMode::HalfEven),
418                    RoundingMode::HalfEven,
419                );
420            let err = (sum.to_f64() - 1.0).abs();
421            assert!(
422                err < 1e-25,
423                "sin²+cos² error = {:.2e} for x = {}",
424                err,
425                x_f64
426            );
427        }
428    }
429}