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