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 series for sin(u) and cos(u) at work precision.
246    let sin_u = sin_taylor(&u, work_prec, mode)?;
247    let cos_u = cos_taylor(&u, work_prec, mode)?;
248
249    // Apply quadrant table.
250    let (sin_x, cos_x) = match quadrant {
251        0 => (sin_u, cos_u),
252        1 => (cos_u, sin_u.neg()),
253        2 => (sin_u.neg(), cos_u.neg()),
254        3 => (cos_u.neg(), sin_u),
255        _ => unreachable!("rem_euclid(4) always in 0..=3"),
256    };
257
258    Ok((
259        sin_x.with_precision(prec, mode),
260        cos_x.with_precision(prec, mode),
261    ))
262}
263
264// ---------------------------------------------------------------------------
265// Public BigFloat methods
266// ---------------------------------------------------------------------------
267
268impl BigFloat {
269    /// Return `sin(self)` at `prec` bits using the given rounding mode.
270    ///
271    /// Uses argument reduction mod π/2 and a Taylor series convergent for
272    /// `|u| ≤ π/4`.
273    ///
274    /// # Errors
275    ///
276    /// - [`OxiNumError::Precision`] if `|self|` is too large for the internal
277    ///   argument-reduction step (approximately `|x| > 2^62 · π/2 ≈ 7.2·10^18`).
278    ///
279    /// # Examples
280    ///
281    /// ```
282    /// use oxinum_float::native::{BigFloat, RoundingMode};
283    /// let zero = BigFloat::zero(64);
284    /// let s = zero.sin(64, RoundingMode::HalfEven).expect("sin(0)");
285    /// assert!(s.is_zero());
286    /// ```
287    pub fn sin(&self, prec: u32, mode: RoundingMode) -> OxiNumResult<BigFloat> {
288        // IEEE-754: sin(NaN) = NaN, sin(±Inf) = NaN
289        if self.is_nan() || self.is_infinite() {
290            return Ok(BigFloat::nan(prec));
291        }
292        if self.is_zero() {
293            return Ok(BigFloat::zero(prec));
294        }
295        let (s, _c) = sincos_impl(self, prec, mode)?;
296        Ok(s)
297    }
298
299    /// Return `cos(self)` at `prec` bits using the given rounding mode.
300    ///
301    /// # Errors
302    ///
303    /// Same as [`BigFloat::sin`].
304    ///
305    /// # Examples
306    ///
307    /// ```
308    /// use oxinum_float::native::{BigFloat, RoundingMode};
309    /// let zero = BigFloat::zero(64);
310    /// let c = zero.cos(64, RoundingMode::HalfEven).expect("cos(0)");
311    /// assert_eq!(c.to_f64(), 1.0);
312    /// ```
313    pub fn cos(&self, prec: u32, mode: RoundingMode) -> OxiNumResult<BigFloat> {
314        // IEEE-754: cos(NaN) = NaN, cos(±Inf) = NaN
315        if self.is_nan() || self.is_infinite() {
316            return Ok(BigFloat::nan(prec));
317        }
318        let (_s, c) = sincos_impl(self, prec, mode)?;
319        Ok(c)
320    }
321
322    /// Return `tan(self)` at `prec` bits using the given rounding mode.
323    ///
324    /// # Errors
325    ///
326    /// - [`OxiNumError::Domain`] if `cos(self) = 0` (i.e. `self = π/2 + k·π`).
327    /// - [`OxiNumError::Precision`] for very large `|self|`.
328    ///
329    /// # Examples
330    ///
331    /// ```
332    /// use oxinum_float::native::{BigFloat, RoundingMode};
333    /// let pi_over_4 = {
334    ///     use oxinum_float::native::pi;
335    ///     let p = pi(64).expect("pi");
336    ///     let four = BigFloat::from_i64(4, 64, RoundingMode::HalfEven);
337    ///     p.div_ref(&four).expect("div")
338    /// };
339    /// let t = pi_over_4.tan(64, RoundingMode::HalfEven).expect("tan(π/4)");
340    /// assert!((t.to_f64() - 1.0).abs() < 1e-14);
341    /// ```
342    pub fn tan(&self, prec: u32, mode: RoundingMode) -> OxiNumResult<BigFloat> {
343        // IEEE-754: tan(NaN) = NaN, tan(±Inf) = NaN
344        if self.is_nan() || self.is_infinite() {
345            return Ok(BigFloat::nan(prec));
346        }
347        let (s, c) = sincos_impl(self, prec, mode)?;
348        if c.is_zero() {
349            return Err(OxiNumError::Domain("tan undefined at π/2 + k·π".into()));
350        }
351        let result = s.div_ref_with_mode(&c, mode)?;
352        Ok(result.with_precision(prec, mode))
353    }
354}
355
356// ---------------------------------------------------------------------------
357// Internal tests
358// ---------------------------------------------------------------------------
359
360#[cfg(test)]
361mod tests {
362    use super::*;
363
364    fn mk(n: i64, prec: u32) -> BigFloat {
365        BigFloat::from_i64(n, prec, RoundingMode::HalfEven)
366    }
367
368    #[test]
369    fn sin_zero_is_zero() {
370        let z = mk(0, 64);
371        let s = z.sin(64, RoundingMode::HalfEven).expect("sin(0)");
372        assert!(s.is_zero());
373    }
374
375    #[test]
376    fn cos_zero_is_one() {
377        let z = mk(0, 64);
378        let c = z.cos(64, RoundingMode::HalfEven).expect("cos(0)");
379        assert!((c.to_f64() - 1.0).abs() < 1e-14, "cos(0) = {}", c.to_f64());
380    }
381
382    #[test]
383    fn sin_pi_over_2_is_one() {
384        let prec = 64u32;
385        let p = pi(prec).expect("pi");
386        let two = mk(2, prec);
387        let pi_over_2 = p
388            .div_ref_with_mode(&two, RoundingMode::HalfEven)
389            .expect("pi/2");
390        let s = pi_over_2
391            .sin(prec, RoundingMode::HalfEven)
392            .expect("sin(pi/2)");
393        assert!(
394            (s.to_f64() - 1.0).abs() < 1e-14,
395            "sin(π/2) = {}",
396            s.to_f64()
397        );
398    }
399
400    #[test]
401    fn pythagorean_identity_at_f64_angle() {
402        let prec = 100u32;
403        for x_f64 in [0.3f64, 1.0, 2.7, -1.5, 5.0, -2.9] {
404            let x = BigFloat::from_f64(x_f64, prec).expect("from_f64");
405            let s = x.sin(prec, RoundingMode::HalfEven).expect("sin");
406            let c = x.cos(prec, RoundingMode::HalfEven).expect("cos");
407            let sum = s
408                .mul_ref_with_mode(&s, RoundingMode::HalfEven)
409                .add_ref_with_mode(
410                    &c.mul_ref_with_mode(&c, RoundingMode::HalfEven),
411                    RoundingMode::HalfEven,
412                );
413            let err = (sum.to_f64() - 1.0).abs();
414            assert!(
415                err < 1e-25,
416                "sin²+cos² error = {:.2e} for x = {}",
417                err,
418                x_f64
419            );
420        }
421    }
422}