Skip to main content

dashu_float/math/
trig.rs

1//! Trigonometric functions, built on top of the cached constants π/2 and the real
2//! [`exp`](crate::FBig::exp)/[`ln`](crate::FBig::ln) primitives:
3//!
4//! - Circular: `sin`, `cos`, `tan`, `sin_cos`, and their inverses `asin`, `acos`, `atan`.
5//!
6//! Argument reduction to the first quadrant reuses the cached π so that repeated
7//! calls at increasing precision extend the shared constant state.
8
9use crate::{
10    ball::Ball,
11    error::{assert_limited_precision, FpError},
12    fbig::FBig,
13    math::{
14        cache::{compute_e, reborrow_cache, ConstCache},
15        FpResult,
16    },
17    repr::{Context, Repr, Word},
18    round::{mode, ErrorBounds, Round, Rounded},
19};
20use core::convert::TryFrom;
21use dashu_base::{AbsOrd, Approximation::Exact, RemEuclid, Sign};
22use dashu_int::IBig;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25enum Quadrant {
26    First,
27    Second,
28    Third,
29    Fourth,
30}
31
32/// Build a `Normal` result equal to `±0`, preserving the sign of `x` (used by `sin`/`tan`/`sin_cos`
33/// at zero input, where `sin(-0) = -0` and `tan(-0) = -0`).
34fn signed_zero_normal<R: Round, const B: Word>(
35    ctx: &Context<R>,
36    x: &Repr<B>,
37) -> FpResult<FBig<R, B>> {
38    let zero = if x.is_neg_zero() {
39        Repr::neg_zero()
40    } else {
41        Repr::zero()
42    };
43    Ok(Exact(FBig::<R, B>::new(zero, *ctx)))
44}
45
46impl<R: ErrorBounds> Context<R> {
47    /// Work context for trigonometric functions: enough guard digits to absorb the catastrophic
48    /// cancellation in `x − k·(π/2)` for large `|x|`. `guard` (the Ziv retry's growing margin)
49    /// replaces the fixed base; `x_mag/10` covers cumulative reduction error scaling with `|x|`.
50    /// Always [`mode::HalfEven`] — the Ball arithmetic the trig functions now run on.
51    fn compute_work_context_trig<const B: Word>(
52        self,
53        x: &Repr<B>,
54        guard: usize,
55    ) -> Context<mode::HalfEven> {
56        // x_mag estimates m = floor(log_BASE(|x|))
57        let x_mag = (x.exponent.saturating_add(x.digits_ub() as isize)).max(0) as usize;
58        let extra_guards = guard + x_mag / 10;
59        let work_precision = self
60            .precision
61            .saturating_add(x_mag)
62            .saturating_add(extra_guards);
63        Context::<mode::HalfEven>::new(work_precision)
64    }
65
66    /// Reduces the argument to the first quadrant: `r = x − k·(π/2)` with `r ∈ (−π/4, π/4]`.
67    /// Returns the work context, `r` as a [`Ball`] whose radius already covers the reduction
68    /// error (dominated by `|k|·ulp(π/2)` for huge `|x|` — the cancellation is tracked by the
69    /// Ball subtraction), and the quadrant `k % 4`.
70    fn reduce_to_quadrant<const B: Word>(
71        self,
72        x: &Repr<B>,
73        guard: usize,
74        mut cache: Option<&mut ConstCache>,
75    ) -> (Context<mode::HalfEven>, Ball<B>, Quadrant) {
76        let work_context = self.compute_work_context_trig(x, guard);
77        let x_ball = Ball::from_rounded(
78            work_context
79                .repr_round(x.clone())
80                .map(|r| FBig::new(r, work_context)),
81        );
82        // `x_f` is exactly the ball's midpoint (the same rounded value), so no second rounding.
83        let x_f = x_ball.mid.clone();
84
85        let pi = work_context.pi::<B>(reborrow_cache(&mut cache)).value();
86        let half_pi = &pi / 2u8;
87        // π as a ball: the cached constant is correctly rounded to the work precision; 8 is a
88        // conservative sound radius (as for the ln(2) constant).
89        let half_pi_ball = Ball::with_error(half_pi.clone(), IBig::from(8));
90
91        let x_scaled = &x_f / &half_pi;
92        let k_f = x_scaled.round();
93        // `k_f` is the integer nearest `x_scaled`, so it's exact (or a signed zero for a tiny
94        // argument in (-1, 0), which `IBig::try_from` treats as plain 0).
95        let k = IBig::try_from(k_f).expect("k_f is an exact integer or signed zero");
96
97        // r = x − k·(π/2): the cancellation and π's error (scaled by |k|) are tracked by the Ball.
98        let r_ball = x_ball.sub(&half_pi_ball.scale_int(&k));
99
100        let k_mod_4_big = k.rem_euclid(IBig::from(4));
101        let Ok(k_mod_4_int) = i8::try_from(k_mod_4_big) else {
102            unreachable!("k % 4 is always in [0, 3]");
103        };
104        let quadrant = match k_mod_4_int {
105            0 => Quadrant::First,
106            1 => Quadrant::Second,
107            2 => Quadrant::Third,
108            3 => Quadrant::Fourth,
109            _ => unreachable!(),
110        };
111
112        (work_context, r_ball, quadrant)
113    }
114
115    /// Calculate the sine of the floating point representation.
116    pub fn sin<const B: Word>(
117        &self,
118        x: &Repr<B>,
119        mut cache: Option<&mut ConstCache>,
120    ) -> FpResult<FBig<R, B>> {
121        if x.is_infinite() {
122            return Err(FpError::InfiniteInput);
123        }
124        assert_limited_precision(self.precision);
125        if x.significand.is_zero() {
126            // sin(±0) = ±0
127            return signed_zero_normal(self, x);
128        }
129
130        // Ziv: reduce to the first quadrant (the guard grows per retry, enlarging the work precision
131        // that absorbs the `x − k·(π/2)` cancellation), evaluate the series. The reduction error is
132        // already inside the reduced argument's Ball radius.
133        self.ziv(50, |guard| {
134            let (work, r, quadrant) = self.reduce_to_quadrant(x, guard, reborrow_cache(&mut cache));
135            let val = match quadrant {
136                Quadrant::First => work.sin_compute(&r),
137                Quadrant::Second => work.cos_compute(&r),
138                Quadrant::Third => work.sin_compute(&r).neg(),
139                Quadrant::Fourth => work.cos_compute(&r).neg(),
140            };
141            Ok(val.to_value_radius::<R>())
142        })
143    }
144
145    /// Near-correct sine series `S(x) = x − x³/3! + x⁵/5! − …` on the reduced argument, returning a
146    /// [`Ball`] whose radius is tracked mechanically (each term's rounding plus the truncated tail).
147    fn sin_compute<const B: Word>(self, x: &Ball<B>) -> Ball<B> {
148        if x.mid.repr().significand.is_zero() {
149            return Ball::exact(x.mid.clone());
150        }
151        let x2 = x.mul(x);
152        let mut sum = x.clone();
153        let mut term = x.clone();
154        let mut k = 1usize;
155        let threshold = sum.mid.ulp_lb();
156        loop {
157            term = term.mul(&x2).div_int((2 * k) * (2 * k + 1));
158            if term.mid.abs_cmp(&threshold).is_le() {
159                break;
160            }
161            if k % 2 == 1 {
162                sum = sum.sub(&term);
163            } else {
164                sum = sum.add(&term);
165            }
166            k += 1;
167        }
168        // Omitted tail: the alternating series tail is < the first omitted term < 1 ulp.
169        sum.inflate(&IBig::from(2));
170        sum
171    }
172
173    /// Calculate the cosine of the floating point representation.
174    pub fn cos<const B: Word>(
175        &self,
176        x: &Repr<B>,
177        mut cache: Option<&mut ConstCache>,
178    ) -> FpResult<FBig<R, B>> {
179        if x.is_infinite() {
180            return Err(FpError::InfiniteInput);
181        }
182        assert_limited_precision(self.precision);
183
184        if x.significand.is_zero() {
185            // cos(±0) = 1
186            return Ok(FBig::<R, B>::ONE.with_precision(self.precision));
187        }
188
189        self.ziv(50, |guard| {
190            let (work, r, quadrant) = self.reduce_to_quadrant(x, guard, reborrow_cache(&mut cache));
191            let val = match quadrant {
192                Quadrant::First => work.cos_compute(&r),
193                Quadrant::Second => work.sin_compute(&r).neg(),
194                Quadrant::Third => work.cos_compute(&r).neg(),
195                Quadrant::Fourth => work.sin_compute(&r),
196            };
197            Ok(val.to_value_radius::<R>())
198        })
199    }
200
201    /// Near-correct cosine series `C(x) = 1 − x²/2! + x⁴/4! − …`, returning a [`Ball`] with a
202    /// mechanically tracked radius. (See [`sin_compute`](Self::sin_compute).)
203    fn cos_compute<const B: Word>(self, x: &Ball<B>) -> Ball<B> {
204        if x.mid.repr().significand.is_zero() {
205            return Ball::exact_int(self.precision, IBig::ONE);
206        }
207        let x2 = x.mul(x);
208        let one = Ball::exact_int(self.precision, IBig::ONE);
209        let mut sum = one.clone();
210        let mut term = one.clone();
211        let mut k = 1usize;
212        let threshold = sum.mid.ulp_lb();
213        loop {
214            term = term.mul(&x2).div_int((2 * k) * (2 * k - 1));
215            if term.mid.abs_cmp(&threshold).is_le() {
216                break;
217            }
218            if k % 2 == 1 {
219                sum = sum.sub(&term);
220            } else {
221                sum = sum.add(&term);
222            }
223            k += 1;
224        }
225        sum.inflate(&IBig::from(2));
226        sum
227    }
228
229    /// Calculate both the sine and cosine of the floating point representation.
230    ///
231    /// This is more efficient than calling `sin` and `cos` separately.
232    pub fn sin_cos<const B: Word>(
233        &self,
234        x: &Repr<B>,
235        mut cache: Option<&mut ConstCache>,
236    ) -> (FpResult<FBig<R, B>>, FpResult<FBig<R, B>>) {
237        if x.is_infinite() {
238            return (Err(FpError::InfiniteInput), Err(FpError::InfiniteInput));
239        }
240        assert_limited_precision(self.precision);
241
242        if x.significand.is_zero() {
243            // sin(±0) = ±0, cos(±0) = 1
244            let s = signed_zero_normal(self, x);
245            let c = Ok(FBig::<R, B>::ONE.with_precision(self.precision));
246            return (s, c);
247        }
248
249        let (s, c) = self.ziv_pair(50, |guard| {
250            let (work, r, quadrant) = self.reduce_to_quadrant(x, guard, reborrow_cache(&mut cache));
251            let (sin_ball, cos_ball) = work.sin_cos_compute(&r);
252            let (s, c) = match quadrant {
253                Quadrant::First => (sin_ball, cos_ball),
254                Quadrant::Second => (cos_ball, sin_ball.neg()),
255                Quadrant::Third => (sin_ball.neg(), cos_ball.neg()),
256                Quadrant::Fourth => (cos_ball.neg(), sin_ball),
257            };
258            Ok((s.to_value_radius::<R>(), c.to_value_radius::<R>()))
259        });
260        (s, c)
261    }
262
263    /// Simultaneously evaluate the sine and cosine series, returning both [`Ball`]s with
264    /// mechanically tracked radii.
265    pub(crate) fn sin_cos_compute<const B: Word>(self, x: &Ball<B>) -> (Ball<B>, Ball<B>) {
266        if x.mid.repr().significand.is_zero() {
267            return (Ball::exact(x.mid.clone()), Ball::exact_int(self.precision, IBig::ONE));
268        }
269        let x2 = x.mul(x);
270        let one = Ball::exact_int(self.precision, IBig::ONE);
271        let mut sin_sum = x.clone();
272        let mut cos_sum = one.clone();
273        let mut sin_term = x.clone();
274        let mut cos_term = one.clone();
275        let mut k = 1usize;
276        let sin_threshold = sin_sum.mid.ulp_lb();
277        let cos_threshold = cos_sum.mid.ulp_lb();
278        loop {
279            cos_term = cos_term.mul(&x2).div_int((2 * k) * (2 * k - 1));
280            sin_term = sin_term.mul(&x2).div_int((2 * k) * (2 * k + 1));
281
282            if sin_term.mid.abs_cmp(&sin_threshold).is_le()
283                && cos_term.mid.abs_cmp(&cos_threshold).is_le()
284            {
285                break;
286            }
287
288            if k % 2 == 1 {
289                cos_sum = cos_sum.sub(&cos_term);
290                sin_sum = sin_sum.sub(&sin_term);
291            } else {
292                cos_sum = cos_sum.add(&cos_term);
293                sin_sum = sin_sum.add(&sin_term);
294            }
295            k += 1;
296        }
297        sin_sum.inflate(&IBig::from(2));
298        cos_sum.inflate(&IBig::from(2));
299        (sin_sum, cos_sum)
300    }
301
302    /// Calculate the tangent of the floating point representation.
303    ///
304    /// # Note
305    /// Near odd multiples of π/2 the value grows without bound; dashu's wide exponent range holds
306    /// it as a large finite number rather than saturating to ±∞.
307    pub fn tan<const B: Word>(
308        &self,
309        x: &Repr<B>,
310        mut cache: Option<&mut ConstCache>,
311    ) -> FpResult<FBig<R, B>> {
312        if x.is_infinite() {
313            return Err(FpError::InfiniteInput);
314        }
315        assert_limited_precision(self.precision);
316
317        if x.significand.is_zero() {
318            // tan(±0) = ±0
319            return signed_zero_normal(self, x);
320        }
321
322        // tan = sin/cos, correctly rounded via the Ziv loop; the sin/cos error propagation into the
323        // quotient is tracked by the Ball division. The closure's `significand.is_zero()` guard
324        // below handles the unreachable exact-pole case (cos cancelling to a zero significand) by
325        // forcing a retry.
326        self.ziv(50, |guard| {
327            let (work, r, quadrant) = self.reduce_to_quadrant(x, guard, reborrow_cache(&mut cache));
328            let (sin_ball, cos_ball) = work.sin_cos_compute(&r);
329            let (s, c) = match quadrant {
330                Quadrant::First => (sin_ball, cos_ball),
331                Quadrant::Second => (cos_ball, sin_ball.neg()),
332                Quadrant::Third => (sin_ball.neg(), cos_ball.neg()),
333                Quadrant::Fourth => (cos_ball.neg(), sin_ball),
334            };
335            if c.mid.repr().significand.is_zero() {
336                // cos rounded to a zero significand at this guard (the input sits on a work-
337                // precision pole — unreachable for finite-precision x): force a retry.
338                return Ok((FBig::<R, B>::ZERO, FBig::<R, B>::ONE));
339            }
340            Ok(s.div(&c).to_value_radius::<R>())
341        })
342    }
343
344    /// Calculate the arcsine of the floating point representation.
345    ///
346    /// # Methodology
347    /// Uses the identity: `asin(x) = atan(x / sqrt(1 - x^2))`
348    /// Returns `Err(OutOfDomain)` if `|x| > 1`.
349    pub fn asin<const B: Word>(
350        &self,
351        x: &Repr<B>,
352        mut cache: Option<&mut ConstCache>,
353    ) -> FpResult<FBig<R, B>> {
354        if x.is_infinite() {
355            return Err(FpError::InfiniteInput);
356        }
357        assert_limited_precision(self.precision);
358        if x.significand.is_zero() {
359            // asin(±0) = ±0 (asin is odd), exact.
360            return signed_zero_normal(self, x);
361        }
362
363        let x_orig = FBig::<R, B>::new(x.clone(), *self);
364        // Domain check: |x| must be <= 1
365        if x_orig.abs_cmp(&FBig::ONE).is_gt() {
366            return Err(FpError::OutOfDomain);
367        }
368
369        self.ziv(50, |guard| {
370            let work = Context::<mode::HalfEven>::new(self.precision + guard);
371            let x_ball = Ball::from_rounded(work.repr_round_ref(x).map(|r| FBig::new(r, work)));
372            Ok(work
373                .asin_ball::<B>(&x_ball, reborrow_cache(&mut cache))
374                .to_value_radius::<R>())
375        })
376    }
377
378    /// `asin` of a ball: `atan(x / √(1−x²))`, with the `|x| = 1` endpoint `±π/2` handled directly
379    /// (the composition's `√(1−x²)` denominator would round to zero there).
380    fn asin_ball<const B: Word>(&self, x: &Ball<B>, mut cache: Option<&mut ConstCache>) -> Ball<B> {
381        let one = Ball::exact_int(self.precision, IBig::ONE);
382        let d = one.sub(&x.mul(x)).sqrt();
383        if d.mid.repr().significand.is_zero() {
384            // |x| = 1: asin(±1) = ±π/2 (an exact-ish endpoint; the π radius is folded in).
385            let pi = Context::<mode::HalfEven>::new(self.precision)
386                .pi::<B>(reborrow_cache(&mut cache))
387                .value();
388            let half_pi = Ball::with_error(pi / 2u8, IBig::from(8));
389            if x.mid.repr().sign() == Sign::Negative {
390                half_pi.neg()
391            } else {
392                half_pi
393            }
394        } else {
395            let arg = x.div(&d);
396            self.atan_ball::<B>(&arg, reborrow_cache(&mut cache))
397        }
398    }
399
400    /// Calculate the arccosine of the floating point representation.
401    ///
402    /// # Methodology
403    /// Uses the identity: `acos(x) = pi/2 - asin(x)`.
404    /// Higher precision is used internally to avoid catastrophic cancellation near x ≈ 1.
405    pub fn acos<const B: Word>(
406        &self,
407        x: &Repr<B>,
408        mut cache: Option<&mut ConstCache>,
409    ) -> FpResult<FBig<R, B>> {
410        if x.is_infinite() {
411            return Err(FpError::InfiniteInput);
412        }
413        assert_limited_precision(self.precision);
414
415        let x_orig = FBig::<R, B>::new(x.clone(), *self);
416        let cmp_one = x_orig.abs_cmp(&FBig::ONE);
417        if cmp_one.is_gt() {
418            return Err(FpError::OutOfDomain);
419        }
420        if cmp_one.is_eq() {
421            // |x| = 1: the composition π/2 − asin(±1) cancels onto an exact value. acos(1) = 0 is
422            // the acute case — under directed rounding 0's preimage is one-sided ([0, ulp)), so the
423            // Ziv containment test can never certify it. acos(-1) = π is handled here too.
424            return Ok(if x.sign() == Sign::Positive {
425                Exact(FBig::<R, B>::new(Repr::zero(), *self))
426            } else {
427                self.pi::<B>(reborrow_cache(&mut cache))
428            });
429        }
430
431        self.ziv(50, |guard| {
432            let work = Context::<mode::HalfEven>::new(self.precision + guard);
433            let x_ball = Ball::from_rounded(work.repr_round_ref(x).map(|r| FBig::new(r, work)));
434            let asin_ball = work.asin_ball::<B>(&x_ball, reborrow_cache(&mut cache));
435            let pi = work.pi::<B>(reborrow_cache(&mut cache)).value();
436            let half_pi = Ball::with_error(pi / 2u8, IBig::from(8));
437            Ok(half_pi.sub(&asin_ball).to_value_radius::<R>())
438        })
439    }
440
441    /// Calculate the arctangent of the floating point representation.
442    pub fn atan<const B: Word>(
443        &self,
444        x: &Repr<B>,
445        mut cache: Option<&mut ConstCache>,
446    ) -> FpResult<FBig<R, B>> {
447        if x.is_infinite() {
448            // atan(±inf) = ±π/2 — preserved (a well-defined finite result for an infinite input)
449            let pi = self.pi::<B>(reborrow_cache(&mut cache)).value();
450            let half_pi: FBig<R, B> = pi / 2;
451            let res: FBig<R, B> = if x.sign() == Sign::Positive {
452                half_pi
453            } else {
454                -half_pi
455            };
456            return Ok(res.with_precision(self.precision));
457        }
458
459        assert_limited_precision(self.precision);
460
461        if x.significand.is_zero() {
462            // atan(±0) = ±0
463            return signed_zero_normal(self, x);
464        }
465
466        self.ziv(50, |guard| {
467            let work = Context::<mode::HalfEven>::new(self.precision + guard);
468            let x_ball = Ball::from_rounded(work.repr_round_ref(x).map(|r| FBig::new(r, work)));
469            Ok(work
470                .atan_ball::<B>(&x_ball, reborrow_cache(&mut cache))
471                .to_value_radius::<R>())
472        })
473    }
474
475    /// `atan` of a ball, with the `|x| ≥ 1` branch (`π/2 − atan(1/x)`). Odd: the sign of `x` is
476    /// applied last (the `|x| ≥ 1` branch's `1/x` would otherwise lose it).
477    fn atan_ball<const B: Word>(&self, x: &Ball<B>, mut cache: Option<&mut ConstCache>) -> Ball<B> {
478        let sign = x.mid.repr().sign();
479        let x_abs = if sign == Sign::Negative {
480            x.clone().neg()
481        } else {
482            x.clone()
483        };
484        let one = Ball::exact_int(self.precision, IBig::ONE);
485        let res = if x_abs.mid.abs_cmp(&one.mid).is_ge() {
486            let pi = Context::<mode::HalfEven>::new(self.precision)
487                .pi::<B>(reborrow_cache(&mut cache))
488                .value();
489            let half_pi = Ball::with_error(pi / 2u8, IBig::from(8));
490            let inv_x = one.div(&x_abs);
491            half_pi.sub(&self.atan_compute(&inv_x))
492        } else {
493            self.atan_compute(&x_abs)
494        };
495        if sign == Sign::Negative {
496            res.neg()
497        } else {
498            res
499        }
500    }
501
502    /// Near-correct Euler series for `atan(x)` (`|x| ≤ 1`), returning a [`Ball`] with a
503    /// mechanically tracked radius.
504    fn atan_compute<const B: Word>(self, x: &Ball<B>) -> Ball<B> {
505        let x2 = x.mul(x);
506        let one = Ball::exact_int(self.precision, IBig::ONE);
507        let one_plus_x2 = one.add(&x2);
508        let mut term = x.div(&one_plus_x2);
509        let mut sum = term.clone();
510        let factor = x2.scale_int(&IBig::from(2)).div(&one_plus_x2);
511        let mut n = 1usize;
512        let threshold = sum.mid.ulp_lb();
513        loop {
514            term = term
515                .mul(&factor)
516                .scale_int(&IBig::from(n))
517                .div_int(2 * n + 1);
518            if term.mid.abs_cmp(&threshold).is_le() {
519                break;
520            }
521            sum = sum.add(&term);
522            n += 1;
523        }
524        // Omitted tail: the Euler terms shrink by (2x²/(1+x²))·n/(2n+1) < 1/2, so the tail is < 2 ulps.
525        sum.inflate(&IBig::from(2));
526        sum
527    }
528
529    /// Calculate the arctangent of y / x.
530    ///
531    /// Handles signed infinities according to IEEE 754 standards.
532    /// Returns `Err(OutOfDomain)` if both arguments are zero.
533    pub fn atan2<const B: Word>(
534        &self,
535        y: &Repr<B>,
536        x: &Repr<B>,
537        mut cache: Option<&mut ConstCache>,
538    ) -> FpResult<FBig<R, B>> {
539        if y.is_finite() && x.is_finite() && y.significand.is_zero() && x.significand.is_zero() {
540            return Err(FpError::OutOfDomain);
541        }
542
543        assert_limited_precision(self.precision);
544
545        // Handle Infinities according to IEEE 754 (computed at the target precision).
546        if y.is_infinite() || x.is_infinite() {
547            let (sy, sx) = (y.sign() == Sign::Positive, x.sign() == Sign::Positive);
548            let pi_val = self.pi::<B>(reborrow_cache(&mut cache)).value();
549            let res: FBig<R, B> = match (y.is_infinite(), x.is_infinite(), sy, sx) {
550                (true, true, true, true) => pi_val.clone() / 4u8,
551                (true, true, true, false) => pi_val.clone() * 3u8 / 4u8,
552                (true, true, false, true) => -(pi_val.clone() / 4u8),
553                (true, true, false, false) => -(pi_val.clone() * 3u8 / 4u8),
554                (true, false, true, _) => pi_val.clone() / 2u8,
555                (true, false, false, _) => -(pi_val.clone() / 2u8),
556                (false, true, _, true) => {
557                    // atan2(±finite, +inf) = ±0 (signed zero of y)
558                    if sy {
559                        FBig::<R, B>::ZERO
560                    } else {
561                        FBig::<R, B>::new(Repr::neg_zero(), *self)
562                    }
563                }
564                (false, true, true, false) => pi_val.clone(),
565                (false, true, false, false) => -pi_val,
566                _ => unreachable!(),
567            };
568            return Ok(res.with_precision(self.precision));
569        }
570
571        // x == 0, y finite nonzero: atan2 = ±π/2.
572        if x.significand.is_zero() {
573            let half_pi = self.pi::<B>(reborrow_cache(&mut cache)).value() / 2u8;
574            let res = if y.sign() == Sign::Positive {
575                half_pi
576            } else {
577                -half_pi
578            };
579            return Ok(res.with_precision(self.precision));
580        }
581
582        // x ≠ 0, finite: atan2 = atan(y/x) ± (quadrant π), all as Ball composition.
583        self.ziv(50, |guard| {
584            let work = Context::<mode::HalfEven>::new(self.precision + guard);
585            let y_ball = Ball::from_rounded(work.repr_round_ref(y).map(|r| FBig::new(r, work)));
586            let x_ball = Ball::from_rounded(work.repr_round_ref(x).map(|r| FBig::new(r, work)));
587            let ratio = y_ball.div(&x_ball);
588            let atan_val = work.atan_ball::<B>(&ratio, reborrow_cache(&mut cache));
589            let res = if x.sign() == Sign::Positive {
590                atan_val
591            } else {
592                let pi = work.pi::<B>(reborrow_cache(&mut cache)).value();
593                let pi_ball = Ball::with_error(pi, IBig::from(8));
594                if y.sign() == Sign::Positive {
595                    atan_val.add(&pi_ball)
596                } else {
597                    atan_val.sub(&pi_ball)
598                }
599            };
600            Ok(res.to_value_radius::<R>())
601        })
602    }
603}
604
605impl<R: ErrorBounds, const B: Word> FBig<R, B> {
606    /// Calculate the sine of the floating point number.
607    ///
608    /// # Panics
609    /// Panics if the input is infinite.
610    #[inline]
611    pub fn sin(&self) -> Self {
612        self.context.unwrap_fp(self.context.sin(&self.repr, None))
613    }
614
615    /// Calculate the cosine of the floating point number.
616    ///
617    /// # Panics
618    /// Panics if the input is infinite.
619    #[inline]
620    pub fn cos(&self) -> Self {
621        self.context.unwrap_fp(self.context.cos(&self.repr, None))
622    }
623
624    /// Calculate both the sine and cosine of the floating point number.
625    ///
626    /// This is more efficient than calling `sin` and `cos` separately.
627    ///
628    /// # Panics
629    /// Panics if the input is infinite.
630    #[inline]
631    pub fn sin_cos(&self) -> (Self, Self) {
632        let (s, c) = self.context.sin_cos(&self.repr, None);
633        (self.context.unwrap_fp(s), self.context.unwrap_fp(c))
634    }
635
636    /// Calculate the tangent of the floating point number.
637    ///
638    /// At odd multiples of π/2 the result is an infinity (returned as a value).
639    ///
640    /// # Panics
641    /// Panics if the input is infinite.
642    #[inline]
643    pub fn tan(&self) -> Self {
644        self.context.unwrap_fp(self.context.tan(&self.repr, None))
645    }
646
647    /// Calculate the arcsine of the floating point number.
648    ///
649    /// # Panics
650    /// Panics if the input is infinite or `|self| > 1` (out of domain).
651    #[inline]
652    pub fn asin(&self) -> Self {
653        self.context.unwrap_fp(self.context.asin(&self.repr, None))
654    }
655
656    /// Calculate the arccosine of the floating point number.
657    ///
658    /// # Panics
659    /// Panics if the input is infinite or `|self| > 1` (out of domain).
660    #[inline]
661    pub fn acos(&self) -> Self {
662        self.context.unwrap_fp(self.context.acos(&self.repr, None))
663    }
664
665    /// Calculate the arctangent of the floating point number. `atan(±inf) = ±π/2`.
666    #[inline]
667    pub fn atan(&self) -> Self {
668        self.context.unwrap_fp(self.context.atan(&self.repr, None))
669    }
670
671    /// Calculate the arctangent of `self / x`.
672    ///
673    /// # Panics
674    /// Panics if both arguments are zero.
675    #[inline]
676    pub fn atan2(&self, x: &Self) -> Self {
677        self.context
678            .unwrap_fp(self.context.atan2(&self.repr, &x.repr, None))
679    }
680}
681
682impl<R: Round> Context<R> {
683    /// Calculate π using the Chudnovsky algorithm with binary splitting.
684    ///
685    /// The Chudnovsky algorithm is one of the most efficient methods for
686    /// high-precision π calculation, providing ~14.18 decimal digits per term.
687    ///
688    /// # Methodology
689    /// We use Binary Splitting to evaluate the series. This technique transforms
690    /// the linear-time summation into a recursive tree evaluation. By combining
691    /// terms into large products, it allows the library to leverage fast
692    /// multiplication algorithms (like Toom-3 or FFT) as the numbers grow,
693    /// leading to significant performance gains over simple iterative summation.
694    #[must_use]
695    pub fn pi<const B: Word>(&self, cache: Option<&mut ConstCache>) -> Rounded<FBig<R, B>> {
696        if let Some(c) = cache {
697            return c.pi::<B, R>(self.precision);
698        }
699
700        // No shared cache: compute via a one-shot ConstCache so the Chudnovsky series
701        // and the 426880·√10005·Q/T finalization live in exactly one place (see
702        // ConstCache::pi), instead of being duplicated here.
703        let mut fresh = ConstCache::new();
704        fresh.pi::<B, R>(self.precision)
705    }
706
707    /// Calculate *e* (Euler's number) by binary splitting on `e = Σ 1/k!`.
708    ///
709    /// Unlike [`pi`](Self::pi), this takes no constant cache: *e* depends on no
710    /// other cached constant and is itself reused by no operation, so there is no
711    /// state worth sharing across calls. The factorial series is the optimal
712    /// algorithm for *e* (`O(M(n) log n)`, faster than π) and avoids the
713    /// argument-reduction and `√p`-fold powering that `exp(1)` would pay for.
714    ///
715    /// # Panics
716    ///
717    /// Panics if the context precision is 0.
718    #[must_use]
719    pub fn e<const B: Word>(&self) -> Rounded<FBig<R, B>> {
720        compute_e::<B, R>(self.precision)
721    }
722}
723
724impl<R: Round, const B: Word> FBig<R, B> {
725    /// Calculate π with the given precision and the default rounding mode.
726    #[inline]
727    #[must_use]
728    pub fn pi(precision: usize) -> Self {
729        Context::<R>::new(precision).pi(None).value()
730    }
731
732    /// Calculate *e* (Euler's number) with the given precision and the default
733    /// rounding mode.
734    #[inline]
735    #[must_use]
736    pub fn e(precision: usize) -> Self {
737        Context::<R>::new(precision).e::<B>().value()
738    }
739}
740
741#[cfg(test)]
742mod tests {
743    use super::*;
744    use crate::round::mode;
745    use crate::DBig;
746    use core::str::FromStr;
747
748    #[test]
749    fn test_atan_infinity_is_preserved() {
750        let ctx = Context::<mode::HalfEven>::new(53);
751        // atan(±inf) = ±π/2 — a finite result, preserved (not an error)
752        let r = ctx.atan::<2>(&Repr::<2>::infinity(), None).unwrap().value();
753        assert!(r.repr().sign() == Sign::Positive);
754        // it should be approximately π/2
755        assert!(r > FBig::<mode::HalfEven>::ONE);
756    }
757
758    /// Regression: a tiny *negative* argument used to panic in `reduce_to_quadrant`.
759    /// `round()` of a value in (-1, 0) yields signed zero (exponent sentinel -1),
760    /// which `IBig::try_from` now accepts as plain 0.
761    #[test]
762    fn test_trig_tiny_negative_no_panic() {
763        let ctx = Context::<mode::HalfAway>::new(30);
764        for &e in &[-1isize, -2, -10, -30] {
765            // x = -1 * BASE^e, a tiny negative value
766            let x = Repr::<10>::new(IBig::from(-1), e);
767            let s = ctx.sin::<10>(&x, None).unwrap().value();
768            let c = ctx.cos::<10>(&x, None).unwrap().value();
769            let (ss, cc) = ctx.sin_cos::<10>(&x, None);
770            let ss = ss.unwrap().value();
771            let cc = cc.unwrap().value();
772            // sin is odd, cos is even: sin(x) ≈ x (negative), cos(x) ≈ 1
773            assert_eq!(s.sign(), Sign::Negative);
774            assert_eq!(c.sign(), Sign::Positive);
775            assert_eq!(ss.sign(), Sign::Negative);
776            assert_eq!(cc.sign(), Sign::Positive);
777        }
778    }
779
780    /// Regression: a 49-digit significand at precision 100 used to assertion-fail in `Context::sin`'s
781    /// rounding logic (found during fuzzing). Promoted here from the excluded `fuzz/` crate so it runs
782    /// in CI; rewritten to the current `Context::sin` API.
783    #[test]
784    fn test_sin_many_digit_rounding_no_panic() {
785        let x = DBig::from_str("-5.525474318981006776603409487767135633516667011547942409467e-3")
786            .unwrap();
787        let ctx = Context::<mode::HalfEven>::new(100);
788        let s = ctx.sin::<10>(x.repr(), None).unwrap().value();
789        // sin(x) ≈ x for a small negative x — completing without panicking is the regression guard.
790        assert_eq!(s.sign(), Sign::Negative);
791    }
792
793    /// tan near a pole (π/2) must not panic, and its sign must follow the pole side: just below →
794    /// large positive (→ +∞), just above → large negative (→ −∞). Guards the pole check, which
795    /// tests `cos` with `significand.is_zero()` (not `is_pos_zero`, which would miss `-0`) and
796    /// assigns the infinity sign as `sign(sin)·sign(cos)`.
797    #[test]
798    fn test_tan_near_pole_signs_and_no_panic() {
799        let p = 53usize;
800        let ctx = Context::<mode::HalfEven>::new(p);
801        let half_pi = FBig::<mode::HalfEven>::pi(p) / 2u8;
802        // a clear offset either side of the pole (≈2⁻¹⁰, far larger than half_pi's rounding error)
803        let eps = FBig::<mode::HalfEven>::ONE >> 10;
804        let below = ctx
805            .tan::<2>((half_pi.clone() - &eps).repr(), None)
806            .unwrap()
807            .value();
808        let above = ctx
809            .tan::<2>((half_pi.clone() + &eps).repr(), None)
810            .unwrap()
811            .value();
812        assert_eq!(below.sign(), Sign::Positive, "tan just below π/2 is large positive");
813        assert_eq!(above.sign(), Sign::Negative, "tan just above π/2 is large negative");
814        // sanity: tan(π/4) = 1
815        let pi = FBig::<mode::HalfEven>::pi(p);
816        let q = ctx.tan::<2>((pi / 4u8).repr(), None).unwrap().value();
817        assert!(
818            (q.clone() - FBig::ONE).abs_cmp(&(FBig::ONE >> 40)).is_le(),
819            "tan(π/4) ≈ 1, got {q:?}"
820        );
821    }
822}