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`, the ×π variants `sin_pi`, `cos_pi`, `tan_pi`,
5//! `sin_cos_pi`, and the inverses `asin`, `acos`, `atan`.
6//!
7//! Argument reduction to the first quadrant reuses the cached π so that repeated
8//! calls at increasing precision extend the shared constant state. The ×π variants reduce
9//! exactly in integer arithmetic instead (see [`Context::sin_pi`]), so they need no
10//! magnitude-dependent guard digits and resolve quarter-integer arguments exactly.
11
12use crate::{
13 ball::{ulps, Ball},
14 cmp::repr_cmp_same_base,
15 error::{assert_limited_precision, FpError},
16 fbig::FBig,
17 mag::Mag,
18 math::{
19 cache::{compute_e, reborrow_cache, ConstCache},
20 FpResult,
21 },
22 repr::{Context, Repr, Word},
23 round::{mode, ErrorBounds, Round, Rounded},
24 utils::{digit_len, log2_u_bs_lb, shl_digits},
25};
26use core::convert::TryFrom;
27use dashu_base::{
28 Abs, AbsOrd, Approximation::Exact, DivRem, EstimatedLog2, RemEuclid, Sign, UnsignedAbs,
29};
30use dashu_int::{fast_div::ConstDivisor, IBig, UBig};
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33enum Quadrant {
34 First,
35 Second,
36 Third,
37 Fourth,
38}
39
40/// Build a `Normal` result equal to `±0`, preserving the sign of `x` (used by `sin`/`tan`/`sin_cos`
41/// at zero input, where `sin(-0) = -0` and `tan(-0) = -0`).
42fn signed_zero_normal<R: Round, const B: Word>(
43 ctx: &Context<R>,
44 x: &Repr<B>,
45) -> FpResult<FBig<R, B>> {
46 let zero = if x.is_neg_zero() {
47 Repr::neg_zero()
48 } else {
49 Repr::zero()
50 };
51 Ok(Exact(FBig::<R, B>::new(zero, *ctx)))
52}
53
54/// `B^e mod M` through the integer crate's prepared-divisor ring — the residue of a radix
55/// power without ever materializing the power (e can be astronomically large, e.g. an
56/// exponent near the `isize` range). All intermediates stay below `M`, which the callers
57/// keep small (`2k·u ≤ 24u`), so the single/double-word fast paths of [`ConstDivisor`]
58/// apply.
59fn digits_powmod<const B: Word>(e: usize, modulus: &IBig) -> IBig {
60 let ring = ConstDivisor::new(modulus.unsigned_abs());
61 let base = ring.reduce(crate::utils::base_as_ibig::<B>().unsigned_abs());
62 IBig::from(base.pow(&UBig::from(e)).residue())
63}
64
65/// Classify the ×u exact cases: `Some(j)` with `j = (k·x/u) mod 2k` (an euclidean modulus on
66/// the signed value, so negative arguments classify directly) when `k·x/u` is an integer,
67/// `None` otherwise. The two keys the ×u family uses:
68/// - `k = 12` (sine/cosine): `j ≡ 0 (mod 3)` means the angle is a multiple of π/2 — the
69/// quarter table keyed by `(4x/u) mod 8 = (j/3) mod 8`; `j` odd (`j mod 12 ∈ {1, 5}` /
70/// `{7, 11}`) means ±π/6 modulo π — sine is `±1/2`; `j` even with `j mod 12 ∈ {2, 10}` /
71/// `{4, 8}` means ±π/3 modulo π — cosine is `±1/2`.
72/// - `k = 8` (tangent): `j mod 8` is the eighth-of-turn index — `0`/`4` map to `±0`, `2`/`6`
73/// to the poles, `1`/`5` to `+1`, `3`/`7` to `−1`.
74///
75/// Powers of B are only materialized when bounded by the input's own size; astronomical
76/// exponents are reduced by modular exponentiation.
77fn unit_residue<const B: Word>(x: &Repr<B>, u: usize, k: usize) -> Option<i8> {
78 debug_assert!(u > 0, "the callers reject u = 0");
79 let u_ibig = IBig::from(u);
80 let k_ibig = IBig::from(k);
81 let modulus = IBig::from(2 * k) * &u_ibig; // 2k·u
82 let m = &x.significand;
83 if m.is_zero() {
84 // ±0 — the infinities are screened out by the callers before this.
85 return Some(0);
86 }
87 let e = x.exponent;
88 if e >= 0 {
89 // j = (k·m·B^e / u) mod 2k = (k·m·B^e mod 2ku) / u — all modular, so the
90 // astronomically large B^e is reduced by binary exponentiation instead of being
91 // materialized.
92 let km = &k_ibig * m;
93 let n = IBig::from(km.rem_euclid(modulus.clone()))
94 * digits_powmod::<B>(e.unsigned_abs(), &modulus);
95 let n = IBig::from(n.rem_euclid(modulus));
96 if &n % &u_ibig != IBig::ZERO {
97 return None;
98 }
99 let j = n / &u_ibig; // in [0, 2k)
100 Some(i8::try_from(j).expect("j < 2k ≤ 24 fits in i8"))
101 } else {
102 // x = m/B^s: k·x/u = k·m/(u·B^s) is an integer iff u·B^s divides k·m — possible only
103 // when u·B^s ≤ |k·m|, so astronomical exponents are rejected by log2 bounds before any
104 // power of B is materialized (the materialized power stays within the input's size).
105 let s = e.unsigned_abs();
106 let km = &k_ibig * m;
107 let (_, ub_km) = km.log2_bounds();
108 let (u_lb, _) = u.log2_bounds();
109 let (b_lb, _) = B.log2_bounds();
110 if ub_km < log2_u_bs_lb(u_lb, b_lb, s) {
111 // u·B^s > |k·m| (bound certified below the true value): cannot divide.
112 return None;
113 }
114 let d = &u_ibig * shl_digits::<B>(&IBig::ONE, s);
115 let (v, rem) = km.div_rem(d);
116 if !rem.is_zero() {
117 return None;
118 }
119 let j = v.rem_euclid(IBig::from(2 * k));
120 Some(i8::try_from(j).expect("j mod 2k ≤ 23 fits in i8"))
121 }
122}
123
124/// The quadrant of a half-integer index k — the `k = round(x/(π/2))` of the radian reduction
125/// and the `k = round(4|x|/u)` of the ×u reduction alike (both subtract k·π/2 from the angle).
126fn quadrant_of(k: &IBig) -> Quadrant {
127 let r = k.rem_euclid(IBig::from(4));
128 match i8::try_from(r).expect("k mod 4 fits in i8") {
129 0 => Quadrant::First,
130 1 => Quadrant::Second,
131 2 => Quadrant::Third,
132 3 => Quadrant::Fourth,
133 _ => unreachable!(),
134 }
135}
136
137/// The exact ×u argument reduction (on `|x|`): first `|x| mod u` (exact, `∈ [0, u)`), then
138/// `k = round(4·|x|/u)` with ties rounded up and `r = |x| − k·u/4 ∈ [−u/8, u/8]` as an exact
139/// rational, plus the quadrant `k mod 4`. Unlike the radian
140/// [`Context::reduce_to_quadrant`], the reduction is exact integer arithmetic on the
141/// significand, so it holds for arbitrarily large `|x|` — the working precision needs no
142/// guard scaling with the magnitude.
143enum UnitReduced<const B: Word> {
144 /// The `k = 0` case (`|x| mod u < u/8`, quadrant `First`): `r` itself, kept as a raw
145 /// [`Repr`] because the power of B behind its exponent can be astronomically large
146 /// (e.g. `3·2^−10⁹`) and must not be materialized — the argument ball is formed by a
147 /// product instead of a rational split.
148 Small(Repr<B>),
149 /// `r = num/den` as an exact rational — never a `Repr`, since an odd base cannot
150 /// represent the fractions exactly. `den` is bounded by the input's own digit count,
151 /// because the astronomically-scaled cases were taken by the `k = 0` fast path.
152 Split {
153 quadrant: Quadrant,
154 num: IBig,
155 den: IBig,
156 },
157}
158
159fn reduce_unit_argument<const B: Word>(x: &Repr<B>, u: usize) -> UnitReduced<B> {
160 debug_assert!(!x.significand.is_zero());
161 debug_assert!(u > 0);
162
163 // Step 1: r0 = |x| mod u ∈ [0, u), exact. (A zero r0 means x is a multiple of u — a
164 // quarter case, resolved by the exact-case table before the reduction.)
165 let u_ibig = IBig::from(u);
166 let m = x.significand.clone().abs();
167 let e = x.exponent;
168 // log2 lower bounds of u and of the base B, for the `u·B^s` magnitude comparisons below.
169 let (u_lb, _) = u.log2_bounds();
170 let (b_lb, _) = B.log2_bounds();
171 let (sig0, e0) = if e >= 0 {
172 // Integer argument: m·B^e mod u by modular exponentiation — B^e is never materialized.
173 let r = IBig::from(m.rem_euclid(u_ibig.clone()))
174 * digits_powmod::<B>(e.unsigned_abs(), &u_ibig);
175 (IBig::from(r.rem_euclid(u_ibig.clone())), 0isize)
176 } else {
177 // |x| = m/B^s < u ⟺ m < u·B^s (checked by bounds, no materialization): r0 = |x| itself.
178 let s = e.unsigned_abs();
179 let (_, ub_m) = m.log2_bounds();
180 if ub_m < log2_u_bs_lb(u_lb, b_lb, s) {
181 (m, e)
182 } else {
183 // |x| ≥ u, so u·B^s ≤ m: the power is bounded by the input's own size.
184 let d = &u_ibig * shl_digits::<B>(&IBig::ONE, s);
185 (IBig::from(m.rem_euclid(d)), e)
186 }
187 };
188 debug_assert!(!sig0.is_zero(), "multiples of u are table-resolved");
189 let s0 = e0.unsigned_abs(); // e0 ≤ 0
190
191 // Step 2 — k = 0 fast path: 8·sig0 < u·B^(s0) (guaranteed by the bounds), so 4r0/u < 1/2
192 // rounds to k = 0 and no power of B is materialized — crucially so, since here it can be
193 // astronomically large. The bound is the conservative [`log2_u_bs_lb`]: overshooting here
194 // would classify a `k ≥ 1` argument as `Small` (wrong quadrant — a wrong result), while
195 // undershooting only falls through to the exact general split.
196 let (_, ub8m) = (IBig::from(8) * &sig0).log2_bounds();
197 if ub8m < log2_u_bs_lb(u_lb, b_lb, s0) {
198 return UnitReduced::Small(Repr::new(sig0, e0));
199 }
200
201 // Not tiny: within the bounds' slack sig0 ≥ u·B^(s0)/8, so the power stays within the
202 // input's own digit count. k = round-half-up of 4r0/u = ⌊(8·sig0 + u·B^(s0)) /
203 // (2·u·B^(s0))⌋ (truncating division of non-negatives is a floor); the split
204 // r = (4·sig0 − k·u·B^(s0)) / (4·B^(s0)) is exact integer arithmetic — the cancellation a
205 // float subtraction would suffer is free here.
206 let b_pow = shl_digits::<B>(&IBig::ONE, s0);
207 let u_bs = &u_ibig * &b_pow; // u·B^(s0)
208 let k = (IBig::from(8) * &sig0 + &u_bs) / (IBig::from(2) * &u_bs);
209 let num = IBig::from(4) * sig0 - &k * &u_bs;
210 let den = IBig::from(4) * b_pow;
211 UnitReduced::Split {
212 quadrant: quadrant_of(&k),
213 num,
214 den,
215 }
216}
217
218/// The ×π argument as a ball on a raw (unreduced) value: `π·x`, with π's conservative radius
219/// and x's rounding to the work precision both tracked by the ball arithmetic. Used by the
220/// hyperbolic ×π functions (which have no periodic argument reduction at all).
221pub(crate) fn pi_scaled_ball<const B: Word>(
222 work: &Context<mode::HalfEven>,
223 x: &Repr<B>,
224 mut cache: Option<&mut ConstCache>,
225) -> Result<Ball<B>, FpError> {
226 let wp = work.precision;
227 let pi = work.pi::<B>(reborrow_cache(&mut cache)).value();
228 // π as a ball: the cached constant is correctly rounded to the work precision; 8 is a
229 // conservative sound radius (as for the ln(2) constant).
230 let rad = ulps::<B>(&pi.repr, wp, 8);
231 let pi_ball = Ball::with_error(pi.into_repr(), rad);
232 let x_ball = Ball::from_rounded(work.repr_round_ref(x), wp);
233 pi_ball.mul(&x_ball, wp)
234}
235
236/// The reduced ×u argument as a ball: `t = (2π/u)·r` with `|t| ≤ π/4`, plus the quadrant. The
237/// constant `2π/u` carries π's conservative radius (as in `Context::reduce_to_quadrant`) and
238/// divides by the *exact* integer u (held at its own digit width — a work precision narrower
239/// than u would round the divisor); the rational `r` is rounded to the work precision, with
240/// every rounding tracked by the ball arithmetic.
241fn unit_argument_ball<const B: Word>(
242 work: &Context<mode::HalfEven>,
243 reduced: &UnitReduced<B>,
244 u: usize,
245 mut cache: Option<&mut ConstCache>,
246) -> Result<(Ball<B>, Quadrant), FpError> {
247 let wp = work.precision;
248 let pi = work.pi::<B>(reborrow_cache(&mut cache)).value();
249 // π as a ball: the cached constant is correctly rounded to the work precision; 8 is a
250 // conservative sound radius (as for the ln(2) constant).
251 let rad = ulps::<B>(&pi.repr, wp, 8);
252 let pi_ball = Ball::with_error(pi.into_repr(), rad);
253 let two_pi = pi_ball.scale_int(&IBig::from(2), wp)?;
254 let u_digits = digit_len::<B>(&IBig::from(u));
255 let u_ball = Ball::exact_int(IBig::from(u), u_digits.max(wp));
256 let two_pi_over_u = two_pi.div(&u_ball, wp)?;
257 Ok(match reduced {
258 UnitReduced::Small(r) => {
259 let r_ball = Ball::from_rounded(work.repr_round_ref(r), wp);
260 (two_pi_over_u.mul(&r_ball, wp)?, Quadrant::First)
261 }
262 UnitReduced::Split { quadrant, num, den } => {
263 // num and den are rounded to the work precision before the ball division — for a
264 // long input significand this caps the intermediate width instead of carrying the
265 // input's full digit count through the division.
266 let num_ball = Ball::from_rounded(work.repr_round_ref(&Repr::new(num.clone(), 0)), wp);
267 let den_ball = Ball::from_rounded(work.repr_round_ref(&Repr::new(den.clone(), 0)), wp);
268 (two_pi_over_u.mul(&num_ball, wp)?.div(&den_ball, wp)?, *quadrant)
269 }
270 })
271}
272
273/// `v·u/(2π)` of a ball — the inverse ×u family's final scaling. 2π carries the usual
274/// conservative π radius; the integer scaling is exact.
275fn scale_unit<const B: Word>(
276 work: &Context<mode::HalfEven>,
277 v: &Ball<B>,
278 u: usize,
279 mut cache: Option<&mut ConstCache>,
280) -> Result<Ball<B>, FpError> {
281 let wp = work.precision;
282 let pi = work.pi::<B>(reborrow_cache(&mut cache)).value();
283 // π as a ball: the cached constant is correctly rounded to the work precision; 8 is a
284 // conservative sound radius (as for the ln(2) constant).
285 let rad = ulps::<B>(&pi.repr, wp, 8);
286 let two_pi = Ball::with_error(pi.into_repr(), rad).scale_int(&IBig::from(2), wp)?;
287 v.scale_int(&IBig::from(u), wp)?.div(&two_pi, wp)
288}
289
290/// The exact rational `num/den` rounded under the caller's mode — the exact-value rows of the
291/// ×u tables (exact whenever den's prime factors divide the base, e.g. `u/4` in base 2 or 10;
292/// correctly rounded otherwise).
293fn exact_ratio<R: Round, const B: Word>(
294 ctx: &Context<R>,
295 num: IBig,
296 den: usize,
297) -> FpResult<FBig<R, B>> {
298 ctx.div(&Repr::new(num, 0), &Repr::from(den))
299}
300
301impl<R: ErrorBounds> Context<R> {
302 /// Work context for trigonometric functions: enough guard digits to absorb the catastrophic
303 /// cancellation in `x − k·(π/2)` for large `|x|`. `guard` (the Ziv retry's growing margin)
304 /// replaces the fixed base; `x_mag/10` covers cumulative reduction error scaling with `|x|`.
305 /// Always [`mode::HalfEven`] — the Ball arithmetic the trig functions now run on.
306 fn compute_work_context_trig<const B: Word>(
307 self,
308 x: &Repr<B>,
309 guard: usize,
310 ) -> Context<mode::HalfEven> {
311 // x_mag estimates m = floor(log_BASE(|x|))
312 let x_mag = (x.exponent.saturating_add(x.digits_ub() as isize)).max(0) as usize;
313 let extra_guards = guard + x_mag / 10;
314 let work_precision = self
315 .precision
316 .saturating_add(x_mag)
317 .saturating_add(extra_guards);
318 Context::<mode::HalfEven>::new(work_precision)
319 }
320
321 /// Reduces the argument to the first quadrant: `r = x − k·(π/2)` with `r ∈ (−π/4, π/4]`.
322 /// Returns the work context, `r` as a [`Ball`] whose radius already covers the reduction
323 /// error (dominated by `|k|·ulp(π/2)` for huge `|x|` — the cancellation is tracked by the
324 /// Ball subtraction), and the quadrant `k % 4`.
325 fn reduce_to_quadrant<const B: Word>(
326 self,
327 x: &Repr<B>,
328 guard: usize,
329 mut cache: Option<&mut ConstCache>,
330 ) -> Result<(Context<mode::HalfEven>, Ball<B>, Quadrant), FpError> {
331 let work_context = self.compute_work_context_trig(x, guard);
332 let work_precision = work_context.precision;
333 let x_ball = Ball::from_rounded(work_context.repr_round(x.clone()), work_precision);
334 // `x_f` is exactly the ball's midpoint (the same rounded value), so no second rounding.
335 // `div_rem_euclid`-style quotient extraction lives on FBig, so wrap the bare mids
336 // (zero-cost).
337 let x_f = FBig::new(x_ball.mid.clone(), work_context);
338
339 let pi = work_context.pi::<B>(reborrow_cache(&mut cache)).value();
340 let half_pi = &pi / 2u8;
341 // π as a ball: the cached constant is correctly rounded to the work precision; 8 is a
342 // conservative sound radius (as for the ln(2) constant).
343 let rad = ulps::<B>(&half_pi.repr, work_precision, 8);
344 let half_pi_ball = Ball::with_error(half_pi.clone().into_repr(), rad);
345
346 let x_scaled = &x_f / &half_pi;
347 let k_f = x_scaled.round();
348 // `k_f` is the integer nearest `x_scaled`, so it's exact (or a signed zero for a tiny
349 // argument in (-1, 0), which `IBig::try_from` treats as plain 0).
350 let k = IBig::try_from(k_f).expect("k_f is an exact integer or signed zero");
351
352 // r = x − k·(π/2): the cancellation and π's error (scaled by |k|) are tracked by the Ball.
353 let scaled = half_pi_ball.scale_int(&k, work_precision)?;
354 let r_ball = x_ball.sub(&scaled, work_precision)?;
355
356 Ok((work_context, r_ball, quadrant_of(&k)))
357 }
358
359 /// Calculate the sine of the floating point representation.
360 pub fn sin<const B: Word>(
361 &self,
362 x: &Repr<B>,
363 mut cache: Option<&mut ConstCache>,
364 ) -> FpResult<FBig<R, B>> {
365 if x.is_infinite() {
366 return Err(FpError::InfiniteInput);
367 }
368 assert_limited_precision(self.precision);
369 if x.significand.is_zero() {
370 // sin(±0) = ±0
371 return signed_zero_normal(self, x);
372 }
373
374 // Ziv: reduce to the first quadrant (the guard grows per retry, enlarging the work precision
375 // that absorbs the `x − k·(π/2)` cancellation), evaluate the series. The reduction error is
376 // already inside the reduced argument's Ball radius.
377 self.ziv(50, |guard| {
378 let (work, r, quadrant) =
379 self.reduce_to_quadrant(x, guard, reborrow_cache(&mut cache))?;
380 let val = match quadrant {
381 Quadrant::First => work.sin_compute(&r)?,
382 Quadrant::Second => work.cos_compute(&r)?,
383 Quadrant::Third => -work.sin_compute(&r)?,
384 Quadrant::Fourth => -work.cos_compute(&r)?,
385 };
386 Ok(val.to_value_radius::<R>(&Context::<R>::new(work.precision)))
387 })
388 }
389
390 /// Near-correct sine series `S(x) = x − x³/3! + x⁵/5! − …` on the reduced argument, returning a
391 /// [`Ball`] whose radius is tracked mechanically (each term's rounding plus the truncated tail).
392 fn sin_compute<const B: Word>(self, x: &Ball<B>) -> Result<Ball<B>, FpError> {
393 if x.mid.significand.is_zero() {
394 return Ok(Ball::exact(x.mid.clone()));
395 }
396 let wp = self.precision;
397 let x2 = x.mul(x, wp)?;
398 let mut sum = x.clone();
399 let mut term = x.clone();
400 let mut k = 1usize;
401 loop {
402 term = term.mul(&x2, wp)?.div_int((2 * k) * (2 * k + 1), wp)?;
403 if term.mid_le_ulp_lb(&sum, wp) {
404 break;
405 }
406 if k % 2 == 1 {
407 sum = sum.sub(&term, wp)?;
408 } else {
409 sum = sum.add(&term, wp)?;
410 }
411 k += 1;
412 }
413 // Omitted tail: the alternating series tail is < the first omitted term < 1 ulp.
414 // A zero sum means the argument (and every term) is exactly zero — the tail is 0.
415 if !sum.mid.significand().is_zero() {
416 sum.add_error(ulps::<B>(&sum.mid, wp, 2));
417 }
418 Ok(sum)
419 }
420
421 /// Calculate the cosine of the floating point representation.
422 pub fn cos<const B: Word>(
423 &self,
424 x: &Repr<B>,
425 mut cache: Option<&mut ConstCache>,
426 ) -> FpResult<FBig<R, B>> {
427 if x.is_infinite() {
428 return Err(FpError::InfiniteInput);
429 }
430 assert_limited_precision(self.precision);
431
432 if x.significand.is_zero() {
433 // cos(±0) = 1
434 return Ok(FBig::<R, B>::ONE.with_precision(self.precision));
435 }
436
437 self.ziv(50, |guard| {
438 let (work, r, quadrant) =
439 self.reduce_to_quadrant(x, guard, reborrow_cache(&mut cache))?;
440 let val = match quadrant {
441 Quadrant::First => work.cos_compute(&r)?,
442 Quadrant::Second => -work.sin_compute(&r)?,
443 Quadrant::Third => -work.cos_compute(&r)?,
444 Quadrant::Fourth => work.sin_compute(&r)?,
445 };
446 Ok(val.to_value_radius::<R>(&Context::<R>::new(work.precision)))
447 })
448 }
449
450 /// Near-correct cosine series `C(x) = 1 − x²/2! + x⁴/4! − …`, returning a [`Ball`] with a
451 /// mechanically tracked radius. (See [`sin_compute`](Self::sin_compute).)
452 fn cos_compute<const B: Word>(self, x: &Ball<B>) -> Result<Ball<B>, FpError> {
453 let wp = self.precision;
454 if x.mid.significand.is_zero() {
455 return Ok(Ball::exact_int(IBig::ONE, wp));
456 }
457 let x2 = x.mul(x, wp)?;
458 let one = Ball::exact_int(IBig::ONE, wp);
459 let mut sum = one.clone();
460 let mut term = one.clone();
461 let mut k = 1usize;
462 loop {
463 term = term.mul(&x2, wp)?.div_int((2 * k) * (2 * k - 1), wp)?;
464 if term.mid_le_ulp_lb(&sum, wp) {
465 break;
466 }
467 if k % 2 == 1 {
468 sum = sum.sub(&term, wp)?;
469 } else {
470 sum = sum.add(&term, wp)?;
471 }
472 k += 1;
473 }
474 sum.add_error(ulps::<B>(&sum.mid, wp, 2));
475 Ok(sum)
476 }
477
478 /// Calculate both the sine and cosine of the floating point representation.
479 ///
480 /// This is more efficient than calling `sin` and `cos` separately.
481 pub fn sin_cos<const B: Word>(
482 &self,
483 x: &Repr<B>,
484 mut cache: Option<&mut ConstCache>,
485 ) -> (FpResult<FBig<R, B>>, FpResult<FBig<R, B>>) {
486 if x.is_infinite() {
487 return (Err(FpError::InfiniteInput), Err(FpError::InfiniteInput));
488 }
489 assert_limited_precision(self.precision);
490
491 if x.significand.is_zero() {
492 // sin(±0) = ±0, cos(±0) = 1
493 let s = signed_zero_normal(self, x);
494 let c = Ok(FBig::<R, B>::ONE.with_precision(self.precision));
495 return (s, c);
496 }
497
498 let (s, c) = self.ziv_pair(50, |guard| {
499 let (work, r, quadrant) =
500 self.reduce_to_quadrant(x, guard, reborrow_cache(&mut cache))?;
501 let (sin_ball, cos_ball) = work.sin_cos_compute(&r)?;
502 let (s, c) = match quadrant {
503 Quadrant::First => (sin_ball, cos_ball),
504 Quadrant::Second => (cos_ball, -sin_ball),
505 Quadrant::Third => (-sin_ball, -cos_ball),
506 Quadrant::Fourth => (-cos_ball, sin_ball),
507 };
508 let ctx = Context::<R>::new(work.precision);
509 Ok((s.to_value_radius::<R>(&ctx), c.to_value_radius::<R>(&ctx)))
510 });
511 (s, c)
512 }
513
514 /// Simultaneously evaluate the sine and cosine series, returning both [`Ball`]s with
515 /// mechanically tracked radii.
516 pub(crate) fn sin_cos_compute<const B: Word>(
517 self,
518 x: &Ball<B>,
519 ) -> Result<(Ball<B>, Ball<B>), FpError> {
520 let wp = self.precision;
521 if x.mid.significand.is_zero() {
522 return Ok((Ball::exact(x.mid.clone()), Ball::exact_int(IBig::ONE, wp)));
523 }
524 let x2 = x.mul(x, wp)?;
525 let one = Ball::exact_int(IBig::ONE, wp);
526 let mut sin_sum = x.clone();
527 let mut cos_sum = one.clone();
528 let mut sin_term = x.clone();
529 let mut cos_term = one.clone();
530 let mut k = 1usize;
531 loop {
532 cos_term = cos_term.mul(&x2, wp)?.div_int((2 * k) * (2 * k - 1), wp)?;
533 sin_term = sin_term.mul(&x2, wp)?.div_int((2 * k) * (2 * k + 1), wp)?;
534
535 if sin_term.mid_le_ulp_lb(&sin_sum, wp) && cos_term.mid_le_ulp_lb(&cos_sum, wp) {
536 break;
537 }
538
539 if k % 2 == 1 {
540 cos_sum = cos_sum.sub(&cos_term, wp)?;
541 sin_sum = sin_sum.sub(&sin_term, wp)?;
542 } else {
543 cos_sum = cos_sum.add(&cos_term, wp)?;
544 sin_sum = sin_sum.add(&sin_term, wp)?;
545 }
546 k += 1;
547 }
548 if !sin_sum.mid.significand().is_zero() {
549 sin_sum.add_error(ulps::<B>(&sin_sum.mid, wp, 2));
550 }
551 cos_sum.add_error(ulps::<B>(&cos_sum.mid, wp, 2));
552 Ok((sin_sum, cos_sum))
553 }
554
555 /// Calculate the tangent of the floating point representation.
556 ///
557 /// # Note
558 /// Near odd multiples of π/2 the value grows without bound; dashu's wide exponent range holds
559 /// it as a large finite number rather than saturating to ±∞.
560 pub fn tan<const B: Word>(
561 &self,
562 x: &Repr<B>,
563 mut cache: Option<&mut ConstCache>,
564 ) -> FpResult<FBig<R, B>> {
565 if x.is_infinite() {
566 return Err(FpError::InfiniteInput);
567 }
568 assert_limited_precision(self.precision);
569
570 if x.significand.is_zero() {
571 // tan(±0) = ±0
572 return signed_zero_normal(self, x);
573 }
574
575 // tan = sin/cos, correctly rounded via the Ziv loop; the sin/cos error propagation into the
576 // quotient is tracked by the Ball division. The closure's `significand.is_zero()` guard
577 // below handles the unreachable exact-pole case (cos cancelling to a zero significand) by
578 // forcing a retry.
579 self.ziv(50, |guard| {
580 let (work, r, quadrant) =
581 self.reduce_to_quadrant(x, guard, reborrow_cache(&mut cache))?;
582 let (sin_ball, cos_ball) = work.sin_cos_compute(&r)?;
583 let (s, c) = match quadrant {
584 Quadrant::First => (sin_ball, cos_ball),
585 Quadrant::Second => (cos_ball, -sin_ball),
586 Quadrant::Third => (-sin_ball, -cos_ball),
587 Quadrant::Fourth => (-cos_ball, sin_ball),
588 };
589 if c.mid.significand.is_zero() {
590 // cos rounded to a zero significand at this guard (the input sits on a work-
591 // precision pole — unreachable for finite-precision x): force a retry.
592 return Ok((FBig::<R, B>::ZERO, FBig::<R, B>::ONE));
593 }
594 Ok(s.div(&c, work.precision)?
595 .to_value_radius::<R>(&Context::<R>::new(work.precision)))
596 })
597 }
598
599 /// Calculate the sine of `2π·x/u` — the ×u family with `u = 2` (i.e. `sin(x·π)`).
600 ///
601 /// See [`sin_unit`](Self::sin_unit) for the semantics; this is a thin wrapper.
602 pub fn sin_pi<const B: Word>(
603 &self,
604 x: &Repr<B>,
605 cache: Option<&mut ConstCache>,
606 ) -> FpResult<FBig<R, B>> {
607 self.sin_unit(x, 2, cache)
608 }
609
610 /// Calculate the sine of `2π·x/u` (the angle x is measured in units of the full turn
611 /// divided by `u`; e.g. `u = 360` gives degrees).
612 ///
613 /// # Methodology
614 /// The ×u variant has rational special points: the argument reduces *exactly* mod u in
615 /// integer arithmetic, so arguments where `12x/u` is an integer resolve to exact values
616 /// (`0`, `±1`, `±1/2`) and — unlike [`sin`](Self::sin) — the accuracy does not degrade
617 /// with the magnitude of x. The exact cases are resolved before the Ziv loop, whose
618 /// containment test cannot certify exact values under directed rounding; the general path
619 /// evaluates the sine/cosine series on the exact rational remainder
620 /// `r = |x| − k·u/4 ∈ [−u/8, u/8]` scaled by `2π/u`.
621 ///
622 /// Returns `Err(OutOfDomain)` if `u = 0`.
623 pub fn sin_unit<const B: Word>(
624 &self,
625 x: &Repr<B>,
626 u: usize,
627 mut cache: Option<&mut ConstCache>,
628 ) -> FpResult<FBig<R, B>> {
629 if u == 0 {
630 return Err(FpError::OutOfDomain);
631 }
632 if x.is_infinite() {
633 return Err(FpError::InfiniteInput);
634 }
635 assert_limited_precision(self.precision);
636
637 // Exact cases (±0 lands in the quarter table's q = 0 row). Resolved outside the Ziv
638 // loop: its containment test cannot certify an exact 0/±1/±1/2 under directed rounding.
639 if let Some(j) = unit_residue(x, u, 12) {
640 if j % 3 == 0 {
641 // quarter table, keyed by q = (4x/u) mod 8 = (j/3) mod 8 — sin(q·π/2)
642 let q = j / 3;
643 return match q {
644 0 | 2 | 4 | 6 => {
645 Ok(Exact(FBig::<R, B>::new(Repr::zero_with_sign(x.sign()), *self)))
646 }
647 1 | 5 => Ok(FBig::<R, B>::ONE.with_precision(self.precision)),
648 3 | 7 => Ok(FBig::<R, B>::NEG_ONE.with_precision(self.precision)),
649 _ => unreachable!("q = 4x/u mod 8 covers all rows"),
650 };
651 } else if j % 2 == 1 {
652 // sine sixths: j mod 12 ∈ {1, 5} → +1/2, {7, 11} → −1/2
653 let positive = j % 12 == 1 || j % 12 == 5;
654 return exact_ratio(self, if positive { IBig::ONE } else { -IBig::ONE }, 2);
655 }
656 // j even with 3 ∤ j: the cosine sixths — sine is ±√3/2 there, not exact.
657 }
658
659 // sin is odd: reduce and evaluate on |x|, apply the sign at the end.
660 let reduced = reduce_unit_argument(x, u);
661 let negative = x.sign() == Sign::Negative;
662 self.ziv(50, |guard| {
663 let work = Context::<mode::HalfEven>::new(self.precision + guard);
664 let (t, quadrant) = unit_argument_ball(&work, &reduced, u, reborrow_cache(&mut cache))?;
665 let val = match quadrant {
666 Quadrant::First => work.sin_compute(&t)?,
667 Quadrant::Second => work.cos_compute(&t)?,
668 Quadrant::Third => -work.sin_compute(&t)?,
669 Quadrant::Fourth => -work.cos_compute(&t)?,
670 };
671 let val = if negative { -val } else { val };
672 Ok(val.to_value_radius::<R>(&Context::<R>::new(work.precision)))
673 })
674 }
675
676 /// Calculate the cosine of `2π·x/u` — the ×u family with `u = 2` (i.e. `cos(x·π)`).
677 ///
678 /// See [`cos_unit`](Self::cos_unit) for the semantics; this is a thin wrapper.
679 pub fn cos_pi<const B: Word>(
680 &self,
681 x: &Repr<B>,
682 cache: Option<&mut ConstCache>,
683 ) -> FpResult<FBig<R, B>> {
684 self.cos_unit(x, 2, cache)
685 }
686
687 /// Calculate the cosine of `2π·x/u` (see [`sin_unit`](Self::sin_unit) for the ×u
688 /// semantics).
689 ///
690 /// # Methodology
691 /// Exact whenever `12x/u` is an integer (integers map to `±1`, odd multiples of `u/4` to
692 /// `+0`, the cosine sixths to `±1/2`); the general path is [`sin_unit`](Self::sin_unit)'s
693 /// with the cos quadrant mapping. cos is even, so the result never depends on the sign
694 /// of x.
695 ///
696 /// Returns `Err(OutOfDomain)` if `u = 0`.
697 pub fn cos_unit<const B: Word>(
698 &self,
699 x: &Repr<B>,
700 u: usize,
701 mut cache: Option<&mut ConstCache>,
702 ) -> FpResult<FBig<R, B>> {
703 if u == 0 {
704 return Err(FpError::OutOfDomain);
705 }
706 if x.is_infinite() {
707 return Err(FpError::InfiniteInput);
708 }
709 assert_limited_precision(self.precision);
710
711 // Exact cases — outside the Ziv loop, whose containment test cannot certify exact
712 // values under directed rounding.
713 if let Some(j) = unit_residue(x, u, 12) {
714 if j % 3 == 0 {
715 // quarter table, keyed by q = (4x/u) mod 8 — cos(q·π/2)
716 let q = j / 3;
717 return match q {
718 0 | 4 => Ok(FBig::<R, B>::ONE.with_precision(self.precision)),
719 2 | 6 => Ok(FBig::<R, B>::NEG_ONE.with_precision(self.precision)),
720 1 | 3 | 5 | 7 => Ok(Exact(FBig::<R, B>::new(Repr::zero(), *self))),
721 _ => unreachable!("q = 4x/u mod 8 covers all rows"),
722 };
723 } else if j % 2 == 0 {
724 // cosine sixths: 6x/u mod 6 ∈ {1, 5} → +1/2, {2, 4} → −1/2; from
725 // j = 12x/u mod 24: j mod 12 ∈ {2, 10} → +1/2, {4, 8} → −1/2.
726 let m = j % 12;
727 let positive = m == 2 || m == 10;
728 return exact_ratio(self, if positive { IBig::ONE } else { -IBig::ONE }, 2);
729 }
730 // j odd (the sine sixths): cosine is ±√3/2 there, not exact.
731 }
732
733 let reduced = reduce_unit_argument(x, u);
734 self.ziv(50, |guard| {
735 let work = Context::<mode::HalfEven>::new(self.precision + guard);
736 let (t, quadrant) = unit_argument_ball(&work, &reduced, u, reborrow_cache(&mut cache))?;
737 let val = match quadrant {
738 Quadrant::First => work.cos_compute(&t)?,
739 Quadrant::Second => -work.sin_compute(&t)?,
740 Quadrant::Third => -work.cos_compute(&t)?,
741 Quadrant::Fourth => work.sin_compute(&t)?,
742 };
743 Ok(val.to_value_radius::<R>(&Context::<R>::new(work.precision)))
744 })
745 }
746
747 /// Calculate both the sine and cosine of `2π·x/u` — the ×u family with `u = 2`.
748 ///
749 /// See [`sin_cos_unit`](Self::sin_cos_unit); this is a thin wrapper.
750 pub fn sin_cos_pi<const B: Word>(
751 &self,
752 x: &Repr<B>,
753 cache: Option<&mut ConstCache>,
754 ) -> (FpResult<FBig<R, B>>, FpResult<FBig<R, B>>) {
755 self.sin_cos_unit(x, 2, cache)
756 }
757
758 /// Calculate both the sine and cosine of `2π·x/u`.
759 ///
760 /// This is more efficient than calling [`sin_unit`](Self::sin_unit) and
761 /// [`cos_unit`](Self::cos_unit) separately — except at the sixth-type exact points, where
762 /// exactly one of the two results is exact (`±1/2` vs `±√3/2`) and the other is evaluated
763 /// on its own.
764 ///
765 /// Returns `Err(OutOfDomain)` in both slots if `u = 0`.
766 pub fn sin_cos_unit<const B: Word>(
767 &self,
768 x: &Repr<B>,
769 u: usize,
770 mut cache: Option<&mut ConstCache>,
771 ) -> (FpResult<FBig<R, B>>, FpResult<FBig<R, B>>) {
772 if u == 0 {
773 return (Err(FpError::OutOfDomain), Err(FpError::OutOfDomain));
774 }
775 if x.is_infinite() {
776 return (Err(FpError::InfiniteInput), Err(FpError::InfiniteInput));
777 }
778 assert_limited_precision(self.precision);
779
780 // Exact cases — outside the Ziv loop, whose containment test cannot certify exact
781 // values under directed rounding.
782 if let Some(j) = unit_residue(x, u, 12) {
783 if j % 3 == 0 {
784 // both exact: the two quarter tables, keyed by q = (4x/u) mod 8
785 let q = j / 3;
786 let s = match q {
787 0 | 2 | 4 | 6 => {
788 Ok(Exact(FBig::<R, B>::new(Repr::zero_with_sign(x.sign()), *self)))
789 }
790 1 | 5 => Ok(FBig::<R, B>::ONE.with_precision(self.precision)),
791 3 | 7 => Ok(FBig::<R, B>::NEG_ONE.with_precision(self.precision)),
792 _ => unreachable!("q = 4x/u mod 8 covers all rows"),
793 };
794 let c = match q {
795 0 | 4 => Ok(FBig::<R, B>::ONE.with_precision(self.precision)),
796 2 | 6 => Ok(FBig::<R, B>::NEG_ONE.with_precision(self.precision)),
797 1 | 3 | 5 | 7 => Ok(Exact(FBig::<R, B>::new(Repr::zero(), *self))),
798 _ => unreachable!("q = 4x/u mod 8 covers all rows"),
799 };
800 return (s, c);
801 } else {
802 // sixth-type: one result is the exact ±1/2, the other (±√3/2) is evaluated on
803 // its own (its own Ziv loop — the re-classification inside the callee lands on
804 // the general path).
805 if j % 2 == 1 {
806 let positive = j % 12 == 1 || j % 12 == 5;
807 let s = exact_ratio(self, if positive { IBig::ONE } else { -IBig::ONE }, 2);
808 let c = self.cos_unit(x, u, reborrow_cache(&mut cache));
809 return (s, c);
810 }
811 let m = j % 12;
812 let positive = m == 2 || m == 10;
813 let c = exact_ratio(self, if positive { IBig::ONE } else { -IBig::ONE }, 2);
814 let s = self.sin_unit(x, u, reborrow_cache(&mut cache));
815 return (s, c);
816 }
817 }
818
819 // sin is odd (cos even): reduce and evaluate on |x|, apply the sign to sin at the end.
820 let reduced = reduce_unit_argument(x, u);
821 let negative = x.sign() == Sign::Negative;
822 let (s, c) = self.ziv_pair(50, |guard| {
823 let work = Context::<mode::HalfEven>::new(self.precision + guard);
824 let (t, quadrant) = unit_argument_ball(&work, &reduced, u, reborrow_cache(&mut cache))?;
825 let (sin_ball, cos_ball) = work.sin_cos_compute(&t)?;
826 let (s, c) = match quadrant {
827 Quadrant::First => (sin_ball, cos_ball),
828 Quadrant::Second => (cos_ball, -sin_ball),
829 Quadrant::Third => (-sin_ball, -cos_ball),
830 Quadrant::Fourth => (-cos_ball, sin_ball),
831 };
832 let s = if negative { -s } else { s };
833 let ctx = Context::<R>::new(work.precision);
834 Ok((s.to_value_radius::<R>(&ctx), c.to_value_radius::<R>(&ctx)))
835 });
836 (s, c)
837 }
838
839 /// Calculate the tangent of `2π·x/u` — the ×u family with `u = 2` (i.e. `tan(x·π)`).
840 ///
841 /// See [`tan_unit`](Self::tan_unit) for the semantics; this is a thin wrapper.
842 pub fn tan_pi<const B: Word>(
843 &self,
844 x: &Repr<B>,
845 cache: Option<&mut ConstCache>,
846 ) -> FpResult<FBig<R, B>> {
847 self.tan_unit(x, 2, cache)
848 }
849
850 /// Calculate the tangent of `2π·x/u` (see [`sin_unit`](Self::sin_unit) for the ×u
851 /// semantics).
852 ///
853 /// # Methodology
854 /// Every quarter-type argument (u a multiple of `u/4`) is exact: integers map to `±0`,
855 /// quarter-integers to `±1`. The odd multiples of `u/4` are the poles: tan approaches
856 /// `+∞` from one side and `−∞` from the other, so the sign of an infinity is unknown
857 /// there — like `0/0`, the case is reported as [`FpError::Indeterminate`] instead of
858 /// guessing a sign. Otherwise [`sin_unit`](Self::sin_unit)/[`cos_unit`](Self::cos_unit)
859 /// share one series evaluation; near a pole the wide exponent range holds a large finite
860 /// value, as for [`tan`](Self::tan).
861 ///
862 /// Returns `Err(OutOfDomain)` if `u = 0`.
863 pub fn tan_unit<const B: Word>(
864 &self,
865 x: &Repr<B>,
866 u: usize,
867 mut cache: Option<&mut ConstCache>,
868 ) -> FpResult<FBig<R, B>> {
869 if u == 0 {
870 return Err(FpError::OutOfDomain);
871 }
872 if x.is_infinite() {
873 return Err(FpError::InfiniteInput);
874 }
875 assert_limited_precision(self.precision);
876
877 // tan hits an exact value at every eighth-of-turn argument (`8x/u` an integer) — the
878 // whole table resolves before the Ziv loop (its containment test cannot certify exact
879 // 0/±1 under directed rounding). The poles (odd multiples of u/4, m ≡ 2 or 6 mod 8) are
880 // indeterminate: the two one-sided limits are +∞ and −∞, so no signed infinity can be
881 // certified — unlike 1/0, whose sign is well-defined.
882 if let Some(j) = unit_residue(x, u, 8) {
883 let m = j % 8; // the eighth-of-turn index (mod its period 8)
884 return match m {
885 0 | 4 => Ok(Exact(FBig::<R, B>::new(Repr::zero_with_sign(x.sign()), *self))),
886 1 | 5 => Ok(FBig::<R, B>::ONE.with_precision(self.precision)),
887 2 | 6 => Err(FpError::Indeterminate),
888 3 | 7 => Ok(FBig::<R, B>::NEG_ONE.with_precision(self.precision)),
889 _ => unreachable!("m = 8x/u mod 8 covers all rows"),
890 };
891 }
892
893 // tan is odd: reduce and evaluate on |x|, apply the sign at the end.
894 let reduced = reduce_unit_argument(x, u);
895 let negative = x.sign() == Sign::Negative;
896 self.ziv(50, |guard| {
897 let work = Context::<mode::HalfEven>::new(self.precision + guard);
898 let (t, quadrant) = unit_argument_ball(&work, &reduced, u, reborrow_cache(&mut cache))?;
899 let (sin_ball, cos_ball) = work.sin_cos_compute(&t)?;
900 let (s, c) = match quadrant {
901 Quadrant::First => (sin_ball, cos_ball),
902 Quadrant::Second => (cos_ball, -sin_ball),
903 Quadrant::Third => (-sin_ball, -cos_ball),
904 Quadrant::Fourth => (-cos_ball, sin_ball),
905 };
906 if c.mid.significand.is_zero() {
907 // cos rounded to a zero significand at this guard — unreachable here (the
908 // exact poles were resolved by the table, and |t| ≤ π/4 keeps cos ≥ √2/2);
909 // force a retry to stay on the safe side.
910 return Ok((FBig::<R, B>::ZERO, FBig::<R, B>::ONE));
911 }
912 let val = s.div(&c, work.precision)?;
913 let val = if negative { -val } else { val };
914 Ok(val.to_value_radius::<R>(&Context::<R>::new(work.precision)))
915 })
916 }
917
918 /// Calculate `asin(x)·u/(2π)` — the arc sine expressed in units of the full turn divided
919 /// by `u` (e.g. `u = 360` gives degrees). The inverse of [`sin_unit`](Self::sin_unit).
920 ///
921 /// # Methodology
922 /// The exact-value rows resolve outside the Ziv loop (directed rounding cannot certify
923 /// them there): `asin(±0) = ±0`, `asin(±1) = ±u/4`, and `asin(±1/2) = ±u/12`. The general
924 /// path scales the [`Ball`](crate) composition of the radian [`asin`](Self::asin) by
925 /// `u/(2π)`.
926 ///
927 /// `u = 0` is the limit `u·asin(x)/(2π) → ±0` (the signed zero, matching the function's
928 /// oddness, in-domain inputs only); returns `Err(OutOfDomain)` if `|x| > 1` — the domain
929 /// error outranks the `u = 0` limit (`asin_unit(2, 0)` is an error, not `+0`).
930 pub fn asin_unit<const B: Word>(
931 &self,
932 x: &Repr<B>,
933 u: usize,
934 mut cache: Option<&mut ConstCache>,
935 ) -> FpResult<FBig<R, B>> {
936 if x.is_infinite() {
937 return Err(FpError::InfiniteInput);
938 }
939 assert_limited_precision(self.precision);
940 if x.significand.is_zero() {
941 // asin(±0) = ±0 (asin is odd), exact.
942 return signed_zero_normal(self, x);
943 }
944
945 // Domain check |x| ≤ 1 and the `±u/4` row, compared *exactly* (no precision
946 // argument) — `±1` is a vertical tangent of `asin`, and the ×u inverse family's exact
947 // rows are exactly that, not "rounded onto".
948 if repr_cmp_same_base::<B, true>(x, &Repr::<B>::one(), None).is_gt() {
949 return Err(FpError::OutOfDomain);
950 }
951 if u == 0 {
952 // the u → 0 limit: the signed zero (asin is odd)
953 return signed_zero_normal(self, x);
954 }
955 // exact rows (±u/4, ±u/12), correctly rounded for any base by the exact ratio
956 if repr_cmp_same_base::<B, true>(x, &Repr::<B>::one(), None).is_eq() {
957 let num = if x.sign() == Sign::Negative {
958 -(IBig::from(u))
959 } else {
960 IBig::from(u)
961 };
962 return exact_ratio(self, num, 4);
963 }
964 // The `±1/2` row keeps the *rounded* comparison: the derivative is bounded there
965 // (`2/√3`), so an argument a sub-ulp below `1/2` moves the result by at most an ulp — and
966 // `1/2` is not exactly representable in an odd base, so there is no exact form to compare.
967 let x_orig = FBig::<R, B>::new(x.clone(), *self);
968 if x_orig.abs_cmp(&(FBig::<R, B>::ONE / 2u8)).is_eq() {
969 let num = if x.sign() == Sign::Negative {
970 -(IBig::from(u))
971 } else {
972 IBig::from(u)
973 };
974 return exact_ratio(self, num, 12);
975 }
976
977 self.ziv(50, |guard| {
978 let work = Context::<mode::HalfEven>::new(self.precision + guard);
979 let x_ball = Ball::from_rounded(work.repr_round_ref(x), work.precision);
980 let a = work.asin_ball::<B>(&x_ball, reborrow_cache(&mut cache))?;
981 Ok(scale_unit(&work, &a, u, reborrow_cache(&mut cache))?
982 .to_value_radius::<R>(&Context::<R>::new(work.precision)))
983 })
984 }
985
986 /// Calculate `acos(x)·u/(2π)` — the arc cosine in units of the full turn divided by `u`.
987 /// The inverse of [`cos_unit`](Self::cos_unit).
988 ///
989 /// # Methodology
990 /// Uses the identity `acos(x) = π/2 − asin(x)` scaled by `u/(2π)`, i.e. `u/4 −
991 /// asin_unit(x)`. Exact rows (outside the Ziv loop): `acos(+1) = 0`, `acos(−1) = u/2`,
992 /// `acos(±0) = u/4`, `acos(±1/2) = u/6` / `u/3`.
993 ///
994 /// `u = 0` is the limit `u·acos(x)/(2π) → +0` (acos ≥ 0, in-domain inputs only); returns
995 /// `Err(OutOfDomain)` if `|x| > 1` — the domain error outranks the `u = 0` limit.
996 pub fn acos_unit<const B: Word>(
997 &self,
998 x: &Repr<B>,
999 u: usize,
1000 mut cache: Option<&mut ConstCache>,
1001 ) -> FpResult<FBig<R, B>> {
1002 if x.is_infinite() {
1003 return Err(FpError::InfiniteInput);
1004 }
1005 assert_limited_precision(self.precision);
1006
1007 // Domain |x| ≤ 1 and the `u/2` row, compared *exactly* (no precision argument) — see
1008 // `asin_unit` for why the exact form matters here.
1009 let cmp_one = repr_cmp_same_base::<B, true>(x, &Repr::<B>::one(), None);
1010 if cmp_one.is_gt() {
1011 return Err(FpError::OutOfDomain);
1012 }
1013 if u == 0 {
1014 // the u → 0 limit: +0 (acos ≥ 0)
1015 return Ok(Exact(FBig::<R, B>::new(Repr::zero(), *self)));
1016 }
1017 // exact rows — see asin_unit for why they stay outside the Ziv loop
1018 if cmp_one.is_eq() {
1019 return if x.sign() == Sign::Positive {
1020 Ok(Exact(FBig::<R, B>::new(Repr::zero(), *self)))
1021 } else {
1022 exact_ratio(self, IBig::from(u), 2)
1023 };
1024 }
1025 if x.significand.is_zero() {
1026 return exact_ratio(self, IBig::from(u), 4);
1027 }
1028 // The `u/6` row keeps the rounded comparison, as in `asin_unit` (bounded derivative).
1029 if FBig::<R, B>::new(x.clone(), *self)
1030 .abs_cmp(&(FBig::<R, B>::ONE / 2u8))
1031 .is_eq()
1032 {
1033 return exact_ratio(
1034 self,
1035 if x.sign() == Sign::Positive {
1036 IBig::from(u)
1037 } else {
1038 IBig::from(2) * IBig::from(u)
1039 },
1040 6,
1041 );
1042 }
1043
1044 // acos_unit(x) = u/4 − asin_unit(x): the composition avoids the catastrophic
1045 // cancellation `π/2 − asin(x)` would suffer near x = 1 once scaled.
1046 self.ziv(50, |guard| {
1047 let work = Context::<mode::HalfEven>::new(self.precision + guard);
1048 let x_ball = Ball::from_rounded(work.repr_round_ref(x), work.precision);
1049 let a = work.asin_ball::<B>(&x_ball, reborrow_cache(&mut cache))?;
1050 let quarter = scale_unit(&work, &a, u, reborrow_cache(&mut cache))?;
1051 // u/4 as an exact ball (held at u's own digit width — see unit_argument_ball)
1052 let u_digits = digit_len::<B>(&IBig::from(u)).max(work.precision);
1053 let wider = u_digits.max(2);
1054 let u_over_4 = Ball::exact_int(IBig::from(u), u_digits)
1055 .div(&Ball::exact_int(IBig::from(4), wider), wider)?;
1056 Ok(u_over_4
1057 .sub(&quarter, wider)?
1058 .to_value_radius::<R>(&Context::<R>::new(work.precision)))
1059 })
1060 }
1061
1062 /// Calculate `atan(x)·u/(2π)` — the arc tangent in units of the full turn divided by `u`.
1063 /// The inverse of [`tan_unit`](Self::tan_unit).
1064 ///
1065 /// # Methodology
1066 /// Exact rows (outside the Ziv loop): `atan(±0) = ±0`, `atan(±1) = ±u/8`,
1067 /// `atan(±inf) = ±u/4`. The general path scales the radian [`atan`](Self::atan)
1068 /// composition by `u/(2π)`.
1069 ///
1070 /// `u = 0` is the limit `u·atan(x)/(2π) → ±0` (the signed zero, matching the oddness).
1071 pub fn atan_unit<const B: Word>(
1072 &self,
1073 x: &Repr<B>,
1074 u: usize,
1075 mut cache: Option<&mut ConstCache>,
1076 ) -> FpResult<FBig<R, B>> {
1077 if x.is_infinite() {
1078 // atan(±inf) = ±u/4 — the scaled form of ±π/2
1079 if u == 0 {
1080 return signed_zero_normal(self, x);
1081 }
1082 let num = if x.sign() == Sign::Negative {
1083 -(IBig::from(u))
1084 } else {
1085 IBig::from(u)
1086 };
1087 return exact_ratio(self, num, 4);
1088 }
1089 assert_limited_precision(self.precision);
1090 if u == 0 {
1091 return signed_zero_normal(self, x);
1092 }
1093 if x.significand.is_zero() {
1094 // atan(±0) = ±0
1095 return signed_zero_normal(self, x);
1096 }
1097 // atan(±1) = ±u/8 — outside the Ziv loop. Compared *exactly*, like the other `±1` rows;
1098 // `atan` has no pole there, so a rounded `1 ± ulp` argument would only be a fraction of an
1099 // ulp out, but the exact form makes the whole family's `±1` rows mean "the argument *is*
1100 // the endpoint".
1101 if repr_cmp_same_base::<B, true>(x, &Repr::<B>::one(), None).is_eq() {
1102 let num = if x.sign() == Sign::Negative {
1103 -(IBig::from(u))
1104 } else {
1105 IBig::from(u)
1106 };
1107 return exact_ratio(self, num, 8);
1108 }
1109
1110 self.ziv(50, |guard| {
1111 let work = Context::<mode::HalfEven>::new(self.precision + guard);
1112 let x_ball = Ball::from_rounded(work.repr_round_ref(x), work.precision);
1113 let a = work.atan_ball::<B>(&x_ball, reborrow_cache(&mut cache))?;
1114 Ok(scale_unit(&work, &a, u, reborrow_cache(&mut cache))?
1115 .to_value_radius::<R>(&Context::<R>::new(work.precision)))
1116 })
1117 }
1118
1119 /// Calculate `atan2(y, x)·u/(2π)` — the four-quadrant arc tangent in units of the full
1120 /// turn divided by `u`. The inverse of [`tan_unit`](Self::tan_unit) with quadrant
1121 /// disambiguation.
1122 ///
1123 /// # Methodology
1124 /// The axis/infinity special values follow the same C99 signed-zero model as
1125 /// [`atan2`](Self::atan2), with the π-multiples mapped to their `u`-fractions (`π/4 →
1126 /// u/8`, `π/2 → u/4`, `3π/4 → 3u/8`, `π → u/2`). The general path scales the radian
1127 /// atan2 by `u/(2π)`.
1128 ///
1129 /// `u = 0` is the limit `u·atan2(y, x)/(2π) → ±0` (the sign of the angle). Returns
1130 /// `Err(OutOfDomain)` if both arguments are zero.
1131 pub fn atan2_unit<const B: Word>(
1132 &self,
1133 y: &Repr<B>,
1134 x: &Repr<B>,
1135 u: usize,
1136 mut cache: Option<&mut ConstCache>,
1137 ) -> FpResult<FBig<R, B>> {
1138 if y.is_finite() && x.is_finite() && y.significand.is_zero() && x.significand.is_zero() {
1139 return Err(FpError::OutOfDomain);
1140 }
1141 if u == 0 {
1142 // the u → 0 limit: the signed zero carrying the angle's sign (atan2's sign is y's)
1143 return Ok(Exact(FBig::<R, B>::new(Repr::zero_with_sign(y.sign()), *self)));
1144 }
1145
1146 assert_limited_precision(self.precision);
1147
1148 // The `k·π/8` angles as exact `k·u/8` ratios (shared by the infinity table and the
1149 // finite axis/diagonal rows below).
1150 let u8th = |k: i8| -> FpResult<FBig<R, B>> {
1151 let num = IBig::from(u) * IBig::from(k.unsigned_abs());
1152 exact_ratio::<R, B>(self, if k < 0 { -num } else { num }, 8)
1153 };
1154
1155 // Infinities, per the C99 model — the π-multiples map to u-fractions.
1156 if y.is_infinite() || x.is_infinite() {
1157 let (sy, sx) = (y.sign() == Sign::Positive, x.sign() == Sign::Positive);
1158 return match (y.is_infinite(), x.is_infinite(), sy, sx) {
1159 (true, true, true, true) => u8th(1), // atan2(+inf, +inf) = π/4
1160 (true, true, true, false) => u8th(3), // 3π/4
1161 (true, true, false, true) => u8th(-1), // −π/4
1162 (true, true, false, false) => u8th(-3), // −3π/4
1163 (true, false, true, _) => u8th(2), // atan2(+inf, finite) = π/2
1164 (true, false, false, _) => u8th(-2), // −π/2
1165 (false, true, _, true) => {
1166 // atan2(±finite, +inf) = ±0 (signed zero of y)
1167 Ok(Exact(FBig::<R, B>::new(Repr::zero_with_sign(y.sign()), *self)))
1168 }
1169 (false, true, true, false) => u8th(4), // atan2(+y, −inf) = π
1170 (false, true, false, false) => u8th(-4), // −π
1171 _ => unreachable!(),
1172 };
1173 }
1174
1175 // The finite axis/diagonal rows, where atan2(y, x) is an exact multiple of π/4 (so the
1176 // scaled result `k·u/8` is exactly representable). Like the other exact rows they must
1177 // stay outside the Ziv loop: an exactly-representable result has a one-sided rounding
1178 // preimage under the directed modes, which no ball interval can certify.
1179 if y.significand.is_zero() {
1180 // y = ±0, x ≠ 0 (both-zero was screened): atan2(±0, +x) = ±0 (C99 signed zero),
1181 // atan2(±0, −x) = ±π → the u/2 row with the sign of y.
1182 return if x.sign() == Sign::Positive {
1183 Ok(Exact(FBig::<R, B>::new(Repr::zero_with_sign(y.sign()), *self)))
1184 } else {
1185 u8th(if y.sign() == Sign::Positive { 4 } else { -4 })
1186 };
1187 }
1188 if x.significand.is_zero() {
1189 // x = 0, y nonzero: atan2 = ±π/2 → ±u/4.
1190 return u8th(if y.sign() == Sign::Positive { 2 } else { -2 });
1191 }
1192 // |y| == |x| (finite, nonzero): the diagonals ±π/4, ±3π/4.
1193 let y_abs =
1194 FBig::<R, B>::new(Repr::new(y.significand.clone().abs(), y.exponent), Context::new(0));
1195 let x_abs =
1196 FBig::<R, B>::new(Repr::new(x.significand.clone().abs(), x.exponent), Context::new(0));
1197 if y_abs.abs_cmp(&x_abs).is_eq() {
1198 let same_half = (y.sign() == Sign::Positive) == (x.sign() == Sign::Positive);
1199 return u8th(match (y.sign() == Sign::Positive, same_half) {
1200 (true, true) => 1, // (+y, +x): π/4
1201 (true, false) => 3, // (+y, −x): 3π/4
1202 (false, true) => -1, // (−y, +x): −π/4
1203 (false, false) => -3,
1204 });
1205 }
1206
1207 // x ≠ 0, finite: atan2(y/x) ± (quadrant π), scaled by u/(2π). The inner atan2 is
1208 // itself correctly rounded at the working precision; the scaling's rounding is
1209 // tracked by the Ball.
1210 self.ziv(50, |guard| {
1211 let work = Context::<mode::HalfEven>::new(self.precision + guard);
1212 let a = work.atan2::<B>(y, x, reborrow_cache(&mut cache))?;
1213 let a_ball = Ball::from_rounded(a.map(FBig::into_repr), work.precision);
1214 Ok(scale_unit(&work, &a_ball, u, reborrow_cache(&mut cache))?
1215 .to_value_radius::<R>(&Context::<R>::new(work.precision)))
1216 })
1217 }
1218
1219 /// Calculate the arcsine of the floating point representation.
1220 ///
1221 /// # Methodology
1222 /// Uses the identity: `asin(x) = atan(x / sqrt(1 - x^2))`
1223 /// Returns `Err(OutOfDomain)` if `|x| > 1`.
1224 pub fn asin<const B: Word>(
1225 &self,
1226 x: &Repr<B>,
1227 mut cache: Option<&mut ConstCache>,
1228 ) -> FpResult<FBig<R, B>> {
1229 if x.is_infinite() {
1230 return Err(FpError::InfiniteInput);
1231 }
1232 assert_limited_precision(self.precision);
1233 if x.significand.is_zero() {
1234 // asin(±0) = ±0 (asin is odd), exact.
1235 return signed_zero_normal(self, x);
1236 }
1237
1238 // Domain check: |x| must be <= 1, compared *exactly* (no precision argument) — `±1` is
1239 // a vertical tangent of `asin`, so an argument that merely rounds onto it
1240 // (`1 − 10⁻¹⁵⁵` at 100 digits) is not the endpoint.
1241 if repr_cmp_same_base::<B, true>(x, &Repr::<B>::one(), None).is_gt() {
1242 return Err(FpError::OutOfDomain);
1243 }
1244
1245 self.ziv(50, |guard| {
1246 let work = Context::<mode::HalfEven>::new(self.precision + guard);
1247 let x_ball = Ball::from_rounded(work.repr_round_ref(x), work.precision);
1248 Ok(work
1249 .asin_ball::<B>(&x_ball, reborrow_cache(&mut cache))?
1250 .to_value_radius::<R>(&Context::<R>::new(work.precision)))
1251 })
1252 }
1253
1254 /// `asin` of a ball: `atan(x / √(1−x²))`, with the `|x| = 1` endpoint `±π/2` handled directly
1255 /// (the composition's `√(1−x²)` denominator would round to zero there).
1256 fn asin_ball<const B: Word>(
1257 &self,
1258 x: &Ball<B>,
1259 mut cache: Option<&mut ConstCache>,
1260 ) -> Result<Ball<B>, FpError> {
1261 let wp = self.precision;
1262 let one = Ball::exact_int(IBig::ONE, wp);
1263 let d = one.sub(&x.mul(x, wp)?, wp)?.sqrt(wp)?;
1264 if d.mid.significand.is_zero() {
1265 // |x| = 1: asin(±1) = ±π/2 (an exact-ish endpoint; the π radius is folded in).
1266 let pi = Context::<mode::HalfEven>::new(wp)
1267 .pi::<B>(reborrow_cache(&mut cache))
1268 .value();
1269 let half_pi = &pi / 2u8;
1270 // `d = √(1 − x²)` collapsed onto zero — which is the endpoint *only* when the
1271 // argument is exactly ±1. Otherwise the input merely rounded onto ±1 at this work
1272 // precision, and the true `√(2δ)` is many ulps away (the derivative at the endpoint
1273 // is infinite): reporting π/2 with a few ulps of radius would certify a value the
1274 // true result is nowhere near. An infinite radius instead forces a Ziv retry at a
1275 // higher guard, where the argument (and so the true difference) is representable and
1276 // the general path below computes it.
1277 let rad = if d.rad.is_zero() {
1278 ulps::<B>(&half_pi.repr, wp, 8)
1279 } else {
1280 Mag::INFINITY
1281 };
1282 let half_pi = Ball::with_error(half_pi.into_repr(), rad);
1283 Ok(if x.mid.sign() == Sign::Negative {
1284 -half_pi
1285 } else {
1286 half_pi
1287 })
1288 } else {
1289 let arg = x.div(&d, wp)?;
1290 self.atan_ball::<B>(&arg, reborrow_cache(&mut cache))
1291 }
1292 }
1293
1294 /// Calculate the arccosine of the floating point representation.
1295 ///
1296 /// # Methodology
1297 /// Uses the identity: `acos(x) = pi/2 - asin(x)`.
1298 /// Higher precision is used internally to avoid catastrophic cancellation near x ≈ 1.
1299 pub fn acos<const B: Word>(
1300 &self,
1301 x: &Repr<B>,
1302 mut cache: Option<&mut ConstCache>,
1303 ) -> FpResult<FBig<R, B>> {
1304 if x.is_infinite() {
1305 return Err(FpError::InfiniteInput);
1306 }
1307 assert_limited_precision(self.precision);
1308
1309 // Domain |x| ≤ 1 and the `±1` rows, compared *exactly* (no precision argument):
1310 // `acos(1 − δ)` is `√(2δ)`, which the endpoint's `0` misses entirely once `δ` is far
1311 // above the result's ulp.
1312 let cmp_one = repr_cmp_same_base::<B, true>(x, &Repr::<B>::one(), None);
1313 if cmp_one.is_gt() {
1314 return Err(FpError::OutOfDomain);
1315 }
1316 if cmp_one.is_eq() {
1317 // |x| = 1: the composition π/2 − asin(±1) cancels onto an exact value. acos(1) = 0 is
1318 // the acute case — under directed rounding 0's preimage is one-sided ([0, ulp)), so the
1319 // Ziv containment test can never certify it. acos(-1) = π is handled here too.
1320 return Ok(if x.sign() == Sign::Positive {
1321 Exact(FBig::<R, B>::new(Repr::zero(), *self))
1322 } else {
1323 self.pi::<B>(reborrow_cache(&mut cache))
1324 });
1325 }
1326
1327 self.ziv(50, |guard| {
1328 let work = Context::<mode::HalfEven>::new(self.precision + guard);
1329 let x_ball = Ball::from_rounded(work.repr_round_ref(x), work.precision);
1330 let asin_ball = work.asin_ball::<B>(&x_ball, reborrow_cache(&mut cache))?;
1331 let pi = work.pi::<B>(reborrow_cache(&mut cache)).value();
1332 let half_pi = &pi / 2u8;
1333 let rad = ulps::<B>(&half_pi.repr, work.precision, 8);
1334 let half_pi = Ball::with_error(half_pi.into_repr(), rad);
1335 Ok(half_pi
1336 .sub(&asin_ball, work.precision)?
1337 .to_value_radius::<R>(&Context::<R>::new(work.precision)))
1338 })
1339 }
1340
1341 /// Calculate the arctangent of the floating point representation.
1342 pub fn atan<const B: Word>(
1343 &self,
1344 x: &Repr<B>,
1345 mut cache: Option<&mut ConstCache>,
1346 ) -> FpResult<FBig<R, B>> {
1347 if x.is_infinite() {
1348 // atan(±inf) = ±π/2 — preserved (a well-defined finite result for an infinite input)
1349 let pi = self.pi::<B>(reborrow_cache(&mut cache)).value();
1350 let half_pi: FBig<R, B> = pi / 2;
1351 let res: FBig<R, B> = if x.sign() == Sign::Positive {
1352 half_pi
1353 } else {
1354 -half_pi
1355 };
1356 return Ok(res.with_precision(self.precision));
1357 }
1358
1359 assert_limited_precision(self.precision);
1360
1361 if x.significand.is_zero() {
1362 // atan(±0) = ±0
1363 return signed_zero_normal(self, x);
1364 }
1365
1366 self.ziv(50, |guard| {
1367 let work = Context::<mode::HalfEven>::new(self.precision + guard);
1368 let x_ball = Ball::from_rounded(work.repr_round_ref(x), work.precision);
1369 Ok(work
1370 .atan_ball::<B>(&x_ball, reborrow_cache(&mut cache))?
1371 .to_value_radius::<R>(&Context::<R>::new(work.precision)))
1372 })
1373 }
1374
1375 /// `atan` of a ball, with the `|x| ≥ 1` branch (`π/2 − atan(1/x)`). Odd: the sign of `x` is
1376 /// applied last (the `|x| ≥ 1` branch's `1/x` would otherwise lose it).
1377 fn atan_ball<const B: Word>(
1378 &self,
1379 x: &Ball<B>,
1380 mut cache: Option<&mut ConstCache>,
1381 ) -> Result<Ball<B>, FpError> {
1382 let wp = self.precision;
1383 let sign = x.mid.sign();
1384 let x_abs = if sign == Sign::Negative {
1385 -x.clone()
1386 } else {
1387 x.clone()
1388 };
1389 let one = Ball::exact_int(IBig::ONE, wp);
1390 let abs_ge_one = repr_cmp_same_base::<B, true>(&x_abs.mid, &one.mid, None).is_ge();
1391 let res = if abs_ge_one {
1392 let pi = Context::<mode::HalfEven>::new(wp)
1393 .pi::<B>(reborrow_cache(&mut cache))
1394 .value();
1395 let half_pi = &pi / 2u8;
1396 let rad = ulps::<B>(&half_pi.repr, wp, 8);
1397 let half_pi = Ball::with_error(half_pi.into_repr(), rad);
1398 let inv_x = one.div(&x_abs, wp)?;
1399 half_pi.sub(&self.atan_compute(&inv_x)?, wp)
1400 } else {
1401 self.atan_compute(&x_abs)
1402 };
1403 Ok(if sign == Sign::Negative { -res? } else { res? })
1404 }
1405
1406 /// Near-correct Euler series for `atan(x)` (`|x| ≤ 1`), returning a [`Ball`] with a
1407 /// mechanically tracked radius.
1408 fn atan_compute<const B: Word>(self, x: &Ball<B>) -> Result<Ball<B>, FpError> {
1409 let wp = self.precision;
1410 let x2 = x.mul(x, wp)?;
1411 let one = Ball::exact_int(IBig::ONE, wp);
1412 let one_plus_x2 = one.add(&x2, wp)?;
1413 let mut term = x.div(&one_plus_x2, wp)?;
1414 let mut sum = term.clone();
1415 let factor = x2.scale_int(&IBig::from(2), wp)?.div(&one_plus_x2, wp)?;
1416 let mut n = 1usize;
1417 loop {
1418 term = term
1419 .mul(&factor, wp)?
1420 .scale_int(&IBig::from(n), wp)?
1421 .div_int(2 * n + 1, wp)?;
1422 if term.mid_le_ulp_lb(&sum, wp) {
1423 break;
1424 }
1425 sum = sum.add(&term, wp)?;
1426 n += 1;
1427 }
1428 // Omitted tail: the Euler terms shrink by (2x²/(1+x²))·n/(2n+1) < 1/2, so the tail is < 2 ulps.
1429 // A zero sum means x (and every term) is exactly zero — the tail is 0 (atan2(±0, ·)).
1430 if !sum.mid.significand().is_zero() {
1431 sum.add_error(ulps::<B>(&sum.mid, wp, 2));
1432 }
1433 Ok(sum)
1434 }
1435
1436 /// Calculate the arctangent of y / x.
1437 ///
1438 /// Handles signed infinities according to IEEE 754 standards.
1439 /// Returns `Err(OutOfDomain)` if both arguments are zero.
1440 pub fn atan2<const B: Word>(
1441 &self,
1442 y: &Repr<B>,
1443 x: &Repr<B>,
1444 mut cache: Option<&mut ConstCache>,
1445 ) -> FpResult<FBig<R, B>> {
1446 if y.is_finite() && x.is_finite() && y.significand.is_zero() && x.significand.is_zero() {
1447 return Err(FpError::OutOfDomain);
1448 }
1449
1450 assert_limited_precision(self.precision);
1451
1452 // Handle Infinities according to IEEE 754 (computed at the target precision).
1453 if y.is_infinite() || x.is_infinite() {
1454 let (sy, sx) = (y.sign() == Sign::Positive, x.sign() == Sign::Positive);
1455 let pi_val = self.pi::<B>(reborrow_cache(&mut cache)).value();
1456 let res: FBig<R, B> = match (y.is_infinite(), x.is_infinite(), sy, sx) {
1457 (true, true, true, true) => pi_val.clone() / 4u8,
1458 (true, true, true, false) => pi_val.clone() * 3u8 / 4u8,
1459 (true, true, false, true) => -(pi_val.clone() / 4u8),
1460 (true, true, false, false) => -(pi_val.clone() * 3u8 / 4u8),
1461 (true, false, true, _) => pi_val.clone() / 2u8,
1462 (true, false, false, _) => -(pi_val.clone() / 2u8),
1463 (false, true, _, true) => {
1464 // atan2(±finite, +inf) = ±0 (signed zero of y)
1465 if sy {
1466 FBig::<R, B>::ZERO
1467 } else {
1468 FBig::<R, B>::new(Repr::neg_zero(), *self)
1469 }
1470 }
1471 (false, true, true, false) => pi_val.clone(),
1472 (false, true, false, false) => -pi_val,
1473 _ => unreachable!(),
1474 };
1475 return Ok(res.with_precision(self.precision));
1476 }
1477
1478 // x == 0, y finite nonzero: atan2 = ±π/2.
1479 if x.significand.is_zero() {
1480 let half_pi = self.pi::<B>(reborrow_cache(&mut cache)).value() / 2u8;
1481 let res = if y.sign() == Sign::Positive {
1482 half_pi
1483 } else {
1484 -half_pi
1485 };
1486 return Ok(res.with_precision(self.precision));
1487 }
1488
1489 // x ≠ 0, finite: atan2 = atan(y/x) ± (quadrant π), all as Ball composition.
1490 self.ziv(50, |guard| {
1491 let work = Context::<mode::HalfEven>::new(self.precision + guard);
1492 let y_ball = Ball::from_rounded(work.repr_round_ref(y), work.precision);
1493 let x_ball = Ball::from_rounded(work.repr_round_ref(x), work.precision);
1494 let ratio = y_ball.div(&x_ball, work.precision)?;
1495 let atan_val = work.atan_ball::<B>(&ratio, reborrow_cache(&mut cache))?;
1496 let res = if x.sign() == Sign::Positive {
1497 atan_val
1498 } else {
1499 let pi = work.pi::<B>(reborrow_cache(&mut cache)).value();
1500 let rad = ulps::<B>(&pi.repr, work.precision, 8);
1501 let pi_ball = Ball::with_error(pi.into_repr(), rad);
1502 if y.sign() == Sign::Positive {
1503 atan_val.add(&pi_ball, work.precision)?
1504 } else {
1505 atan_val.sub(&pi_ball, work.precision)?
1506 }
1507 };
1508 Ok(res.to_value_radius::<R>(&Context::<R>::new(work.precision)))
1509 })
1510 }
1511}
1512
1513impl<R: ErrorBounds, const B: Word> FBig<R, B> {
1514 /// Calculate the sine of the floating point number.
1515 ///
1516 /// # Panics
1517 /// Panics if the input is infinite.
1518 #[inline]
1519 pub fn sin(&self) -> Self {
1520 self.context.unwrap_fp(self.context.sin(&self.repr, None))
1521 }
1522
1523 /// Calculate the cosine of the floating point number.
1524 ///
1525 /// # Panics
1526 /// Panics if the input is infinite.
1527 #[inline]
1528 pub fn cos(&self) -> Self {
1529 self.context.unwrap_fp(self.context.cos(&self.repr, None))
1530 }
1531
1532 /// Calculate both the sine and cosine of the floating point number.
1533 ///
1534 /// This is more efficient than calling `sin` and `cos` separately.
1535 ///
1536 /// # Panics
1537 /// Panics if the input is infinite.
1538 #[inline]
1539 pub fn sin_cos(&self) -> (Self, Self) {
1540 let (s, c) = self.context.sin_cos(&self.repr, None);
1541 (self.context.unwrap_fp(s), self.context.unwrap_fp(c))
1542 }
1543
1544 /// Calculate the tangent of the floating point number.
1545 ///
1546 /// At odd multiples of π/2 the result is an infinity (returned as a value).
1547 ///
1548 /// # Panics
1549 /// Panics if the input is infinite.
1550 #[inline]
1551 pub fn tan(&self) -> Self {
1552 self.context.unwrap_fp(self.context.tan(&self.repr, None))
1553 }
1554
1555 /// Calculate the sine of the floating point number multiplied by π, i.e. `sin(self·π)`.
1556 ///
1557 /// Unlike [`sin`](Self::sin), the ×π variant resolves rational fractions of π exactly:
1558 /// integers map to `±0` (with the sign of `self`) and half-integers to `±1`. The argument
1559 /// reduces exactly in integer arithmetic, so the accuracy is independent of the magnitude
1560 /// of `self`.
1561 ///
1562 /// # Examples
1563 /// ```
1564 /// # use core::str::FromStr;
1565 /// # use dashu_float::DBig;
1566 /// assert_eq!(DBig::from_str("0.5")?.sin_pi(), DBig::from(1));
1567 /// // sin(π·10^100) = 0 *exactly*: huge integer arguments lose nothing
1568 /// assert_eq!(DBig::from_str("1e100")?.sin_pi(), DBig::ZERO);
1569 /// # Ok::<(), dashu_base::ParseError>(())
1570 /// ```
1571 ///
1572 /// # Panics
1573 /// Panics if the input is infinite.
1574 #[inline]
1575 pub fn sin_pi(&self) -> Self {
1576 self.context
1577 .unwrap_fp(self.context.sin_pi(&self.repr, None))
1578 }
1579
1580 /// Calculate the cosine of the floating point number multiplied by π, i.e. `cos(self·π)`.
1581 ///
1582 /// Unlike [`cos`](Self::cos), the ×π variant resolves rational fractions of π exactly:
1583 /// integers map to `±1` and odd half-integers to `+0`. The argument reduces exactly in
1584 /// integer arithmetic, so the accuracy is independent of the magnitude of `self`.
1585 ///
1586 /// # Examples
1587 /// ```
1588 /// # use core::str::FromStr;
1589 /// # use dashu_float::DBig;
1590 /// assert_eq!(DBig::from_str("0.5")?.cos_pi(), DBig::ZERO);
1591 /// assert_eq!(DBig::from_str("3")?.cos_pi(), DBig::from(-1));
1592 /// # Ok::<(), dashu_base::ParseError>(())
1593 /// ```
1594 ///
1595 /// # Panics
1596 /// Panics if the input is infinite.
1597 #[inline]
1598 pub fn cos_pi(&self) -> Self {
1599 self.context
1600 .unwrap_fp(self.context.cos_pi(&self.repr, None))
1601 }
1602
1603 /// Calculate both the sine and cosine of the floating point number multiplied by π.
1604 ///
1605 /// This is more efficient than calling `sin_pi` and `cos_pi` separately. At half-integer
1606 /// arguments both results are exact.
1607 ///
1608 /// # Examples
1609 /// ```
1610 /// # use core::str::FromStr;
1611 /// # use dashu_float::DBig;
1612 /// let (s, c) = DBig::from_str("0.25")?.sin_cos_pi();
1613 /// // sin(π/4) = cos(π/4) = √2/2
1614 /// assert!(s == c);
1615 /// # Ok::<(), dashu_base::ParseError>(())
1616 /// ```
1617 ///
1618 /// # Panics
1619 /// Panics if the input is infinite.
1620 #[inline]
1621 pub fn sin_cos_pi(&self) -> (Self, Self) {
1622 let (s, c) = self.context.sin_cos_pi(&self.repr, None);
1623 (self.context.unwrap_fp(s), self.context.unwrap_fp(c))
1624 }
1625
1626 /// Calculate the tangent of the floating point number multiplied by π, i.e. `tan(self·π)`.
1627 ///
1628 /// Every quarter-integer argument is exact: integers map to `±0` (with the sign of the
1629 /// input, keeping `tan_pi` odd — matching MPFR) and quarter-integers to `±1`. At odd
1630 /// half-integers — the poles — the one-sided limits are `+∞` and `−∞`, so no
1631 /// signed infinity can be certified: the case is indeterminate (like `0/0`) and reported
1632 /// as an error at the context layer. The argument reduces exactly in integer arithmetic,
1633 /// so the accuracy is independent of the magnitude of `self`.
1634 ///
1635 /// # Examples
1636 /// ```
1637 /// # use core::str::FromStr;
1638 /// # use dashu_float::DBig;
1639 /// assert_eq!(DBig::from_str("0.25")?.tan_pi(), DBig::from(1));
1640 /// assert_eq!(DBig::from_str("1.75")?.tan_pi(), DBig::from(-1));
1641 /// # Ok::<(), dashu_base::ParseError>(())
1642 /// ```
1643 ///
1644 /// # Panics
1645 /// Panics if the input is infinite or an odd multiple of `1/2` (a pole, where the sign
1646 /// of the infinity is indeterminate).
1647 #[inline]
1648 pub fn tan_pi(&self) -> Self {
1649 self.context
1650 .unwrap_fp(self.context.tan_pi(&self.repr, None))
1651 }
1652
1653 /// Calculate the sine of `self·2π/u` — the angle `self` measured in units of the full
1654 /// turn divided by `u` (e.g. `u = 360` gives degrees).
1655 ///
1656 /// Unlike [`sin`](Self::sin), the ×u variant has rational special points: the argument
1657 /// reduces *exactly* mod u in integer arithmetic, so the accuracy is independent of the
1658 /// magnitude of `self`, and arguments where `12·self/u` is an integer resolve exactly
1659 /// (e.g. `sin_unit(90, 360)` is exactly `1`).
1660 ///
1661 /// # Examples
1662 /// ```
1663 /// # use core::str::FromStr;
1664 /// # use dashu_float::DBig;
1665 /// let deg = DBig::from_str("90")?;
1666 /// assert_eq!(deg.sin_unit(360), DBig::from(1));
1667 /// let thirty = DBig::from_str("30")?;
1668 /// assert_eq!(thirty.sin_unit(360), DBig::from_str("0.5")?);
1669 /// # Ok::<(), dashu_base::ParseError>(())
1670 /// ```
1671 ///
1672 /// # Panics
1673 /// Panics if the input is infinite or `u` is zero.
1674 #[inline]
1675 pub fn sin_unit(&self, u: usize) -> Self {
1676 self.context
1677 .unwrap_fp(self.context.sin_unit(&self.repr, u, None))
1678 }
1679
1680 /// Calculate the cosine of `self·2π/u` (see [`sin_unit`](Self::sin_unit) for the ×u
1681 /// semantics).
1682 ///
1683 /// # Examples
1684 /// ```
1685 /// # use core::str::FromStr;
1686 /// # use dashu_float::DBig;
1687 /// let deg = DBig::from_str("60")?;
1688 /// assert_eq!(deg.cos_unit(360), DBig::from_str("0.5")?);
1689 /// # Ok::<(), dashu_base::ParseError>(())
1690 /// ```
1691 ///
1692 /// # Panics
1693 /// Panics if the input is infinite or `u` is zero.
1694 #[inline]
1695 pub fn cos_unit(&self, u: usize) -> Self {
1696 self.context
1697 .unwrap_fp(self.context.cos_unit(&self.repr, u, None))
1698 }
1699
1700 /// Calculate both the sine and cosine of `self·2π/u` (see [`sin_unit`](Self::sin_unit)).
1701 ///
1702 /// This is more efficient than calling [`sin_unit`](Self::sin_unit) and
1703 /// [`cos_unit`](Self::cos_unit) separately, except at the sixth-type exact points where
1704 /// exactly one of the two results is exact.
1705 ///
1706 /// # Examples
1707 /// ```
1708 /// # use core::str::FromStr;
1709 /// # use dashu_float::DBig;
1710 /// let deg = DBig::from_str("30")?;
1711 /// let (s, c) = deg.sin_cos_unit(360);
1712 /// assert_eq!(s, DBig::from_str("0.5")?);
1713 /// assert_eq!(c, DBig::from_str("0.86602540378443864676")?.with_precision(deg.precision()).value());
1714 /// # Ok::<(), dashu_base::ParseError>(())
1715 /// ```
1716 ///
1717 /// # Panics
1718 /// Panics if the input is infinite or `u` is zero.
1719 #[inline]
1720 pub fn sin_cos_unit(&self, u: usize) -> (Self, Self) {
1721 let (s, c) = self.context.sin_cos_unit(&self.repr, u, None);
1722 (self.context.unwrap_fp(s), self.context.unwrap_fp(c))
1723 }
1724
1725 /// Calculate the tangent of `self·2π/u` (see [`sin_unit`](Self::sin_unit) for the ×u
1726 /// semantics).
1727 ///
1728 /// Every eighth-of-turn argument (`8·self/u` an integer) is exact; at the odd multiples
1729 /// of `u/4` — the poles — the one-sided limits are `+∞` and `−∞`: the case is
1730 /// indeterminate and reported as an error at the context layer.
1731 ///
1732 /// # Examples
1733 /// ```
1734 /// # use core::str::FromStr;
1735 /// # use dashu_float::DBig;
1736 /// let deg = DBig::from_str("45")?;
1737 /// assert_eq!(deg.tan_unit(360), DBig::from(1));
1738 /// # Ok::<(), dashu_base::ParseError>(())
1739 /// ```
1740 ///
1741 /// # Panics
1742 /// Panics if the input is infinite, `u` is zero, or `self` is an odd multiple of `u/4`
1743 /// (a pole).
1744 #[inline]
1745 pub fn tan_unit(&self, u: usize) -> Self {
1746 self.context
1747 .unwrap_fp(self.context.tan_unit(&self.repr, u, None))
1748 }
1749
1750 /// Calculate `asin(self)·u/(2π)` — the arc sine in units of the full turn divided by `u`
1751 /// (the inverse of [`sin_unit`](Self::sin_unit)).
1752 ///
1753 /// # Examples
1754 /// ```
1755 /// # use core::str::FromStr;
1756 /// # use dashu_float::DBig;
1757 /// let half = DBig::from_str("0.5")?;
1758 /// assert_eq!(half.asin_unit(360), DBig::from(30)); // asin(1/2) = 30°
1759 /// # Ok::<(), dashu_base::ParseError>(())
1760 /// ```
1761 ///
1762 /// # Panics
1763 /// Panics if the input is infinite or `|self| > 1` (out of domain).
1764 #[inline]
1765 pub fn asin_unit(&self, u: usize) -> Self {
1766 self.context
1767 .unwrap_fp(self.context.asin_unit(&self.repr, u, None))
1768 }
1769
1770 /// Calculate `acos(self)·u/(2π)` — the arc cosine in units of the full turn divided by
1771 /// `u` (the inverse of [`cos_unit`](Self::cos_unit)).
1772 ///
1773 /// # Examples
1774 /// ```
1775 /// # use core::str::FromStr;
1776 /// # use dashu_float::DBig;
1777 /// let half = DBig::from_str("0.5")?;
1778 /// assert_eq!(half.acos_unit(360), DBig::from(60)); // acos(1/2) = 60°
1779 /// # Ok::<(), dashu_base::ParseError>(())
1780 /// ```
1781 ///
1782 /// # Panics
1783 /// Panics if the input is infinite or `|self| > 1` (out of domain).
1784 #[inline]
1785 pub fn acos_unit(&self, u: usize) -> Self {
1786 self.context
1787 .unwrap_fp(self.context.acos_unit(&self.repr, u, None))
1788 }
1789
1790 /// Calculate `atan(self)·u/(2π)` — the arc tangent in units of the full turn divided by
1791 /// `u` (the inverse of [`tan_unit`](Self::tan_unit)). `atan_unit(±inf) = ±u/4`.
1792 ///
1793 /// # Examples
1794 /// ```
1795 /// # use core::str::FromStr;
1796 /// # use dashu_float::DBig;
1797 /// let one = DBig::from_str("1.0")?; // two digits: the result 45 needs precision ≥ 2
1798 /// assert_eq!(one.atan_unit(360), DBig::from(45)); // atan(1) = 45°
1799 /// # Ok::<(), dashu_base::ParseError>(())
1800 /// ```
1801 #[inline]
1802 pub fn atan_unit(&self, u: usize) -> Self {
1803 self.context
1804 .unwrap_fp(self.context.atan_unit(&self.repr, u, None))
1805 }
1806
1807 /// Calculate `atan2(self, x)·u/(2π)` — the four-quadrant arc tangent in units of the
1808 /// full turn divided by `u`. Follows the same C99 signed-zero model as
1809 /// [`atan2`](Self::atan2). The receiver is the `y`-coordinate, the first argument the
1810 /// `x`-coordinate: `(1).atan2_unit(&x, 360)`.
1811 ///
1812 /// # Examples
1813 /// ```
1814 /// # use core::str::FromStr;
1815 /// # use dashu_float::DBig;
1816 /// // atan2(1, 1) = 45° — the receiver is y, the argument x
1817 /// let y = DBig::from_str("1.0")?; // two digits: the result 45 needs precision ≥ 2
1818 /// let x = DBig::from_str("1.0")?;
1819 /// assert_eq!(y.atan2_unit(&x, 360), DBig::from(45));
1820 /// # Ok::<(), dashu_base::ParseError>(())
1821 /// ```
1822 ///
1823 /// # Panics
1824 /// Panics if both arguments are zero.
1825 #[inline]
1826 pub fn atan2_unit(&self, x: &Self, u: usize) -> Self {
1827 self.context
1828 .unwrap_fp(self.context.atan2_unit(&self.repr, &x.repr, u, None))
1829 }
1830
1831 /// Calculate the arcsine of the floating point number.
1832 ///
1833 /// # Panics
1834 /// Panics if the input is infinite or `|self| > 1` (out of domain).
1835 #[inline]
1836 pub fn asin(&self) -> Self {
1837 self.context.unwrap_fp(self.context.asin(&self.repr, None))
1838 }
1839
1840 /// Calculate the arccosine of the floating point number.
1841 ///
1842 /// # Panics
1843 /// Panics if the input is infinite or `|self| > 1` (out of domain).
1844 #[inline]
1845 pub fn acos(&self) -> Self {
1846 self.context.unwrap_fp(self.context.acos(&self.repr, None))
1847 }
1848
1849 /// Calculate the arctangent of the floating point number. `atan(±inf) = ±π/2`.
1850 #[inline]
1851 pub fn atan(&self) -> Self {
1852 self.context.unwrap_fp(self.context.atan(&self.repr, None))
1853 }
1854
1855 /// Calculate the arctangent of `self / x`.
1856 ///
1857 /// # Panics
1858 /// Panics if both arguments are zero.
1859 #[inline]
1860 pub fn atan2(&self, x: &Self) -> Self {
1861 self.context
1862 .unwrap_fp(self.context.atan2(&self.repr, &x.repr, None))
1863 }
1864}
1865
1866impl<R: Round> Context<R> {
1867 /// Calculate π using the Chudnovsky algorithm with binary splitting.
1868 ///
1869 /// The Chudnovsky algorithm is one of the most efficient methods for
1870 /// high-precision π calculation, providing ~14.18 decimal digits per term.
1871 ///
1872 /// # Methodology
1873 /// We use Binary Splitting to evaluate the series. This technique transforms
1874 /// the linear-time summation into a recursive tree evaluation. By combining
1875 /// terms into large products, it allows the library to leverage fast
1876 /// multiplication algorithms (like Toom-3 or FFT) as the numbers grow,
1877 /// leading to significant performance gains over simple iterative summation.
1878 #[must_use]
1879 pub fn pi<const B: Word>(&self, cache: Option<&mut ConstCache>) -> Rounded<FBig<R, B>> {
1880 if let Some(c) = cache {
1881 return c.pi::<B, R>(self.precision);
1882 }
1883
1884 // No shared cache: compute via a one-shot ConstCache so the Chudnovsky series
1885 // and the 426880·√10005·Q/T finalization live in exactly one place (see
1886 // ConstCache::pi), instead of being duplicated here.
1887 let mut fresh = ConstCache::new();
1888 fresh.pi::<B, R>(self.precision)
1889 }
1890
1891 /// Calculate *e* (Euler's number) by binary splitting on `e = Σ 1/k!`.
1892 ///
1893 /// Unlike [`pi`](Self::pi), this takes no constant cache: *e* depends on no
1894 /// other cached constant and is itself reused by no operation, so there is no
1895 /// state worth sharing across calls. The factorial series is the optimal
1896 /// algorithm for *e* (`O(M(n) log n)`, faster than π) and avoids the
1897 /// argument-reduction and `√p`-fold powering that `exp(1)` would pay for.
1898 ///
1899 /// # Panics
1900 ///
1901 /// Panics if the context precision is 0.
1902 #[must_use]
1903 pub fn e<const B: Word>(&self) -> Rounded<FBig<R, B>> {
1904 compute_e::<B, R>(self.precision)
1905 }
1906}
1907
1908impl<R: Round, const B: Word> FBig<R, B> {
1909 /// Calculate π with the given precision and the default rounding mode.
1910 #[inline]
1911 #[must_use]
1912 pub fn pi(precision: usize) -> Self {
1913 Context::<R>::new(precision).pi(None).value()
1914 }
1915
1916 /// Calculate *e* (Euler's number) with the given precision and the default
1917 /// rounding mode.
1918 #[inline]
1919 #[must_use]
1920 pub fn e(precision: usize) -> Self {
1921 Context::<R>::new(precision).e::<B>().value()
1922 }
1923}
1924
1925#[cfg(test)]
1926mod tests {
1927 use super::*;
1928 use crate::round::mode;
1929 use crate::DBig;
1930 use alloc::format;
1931 use core::str::FromStr;
1932
1933 #[test]
1934 fn test_atan_infinity_is_preserved() {
1935 let ctx = Context::<mode::HalfEven>::new(53);
1936 // atan(±inf) = ±π/2 — a finite result, preserved (not an error)
1937 let r = ctx.atan::<2>(&Repr::<2>::infinity(), None).unwrap().value();
1938 assert!(r.repr().sign() == Sign::Positive);
1939 // it should be approximately π/2
1940 assert!(r > FBig::<mode::HalfEven>::ONE);
1941 }
1942
1943 /// Regression: a tiny *negative* argument used to panic in `reduce_to_quadrant`.
1944 /// `round()` of a value in (-1, 0) yields signed zero (exponent sentinel -1),
1945 /// which `IBig::try_from` now accepts as plain 0.
1946 #[test]
1947 fn test_trig_tiny_negative_no_panic() {
1948 let ctx = Context::<mode::HalfAway>::new(30);
1949 for &e in &[-1isize, -2, -10, -30] {
1950 // x = -1 * BASE^e, a tiny negative value
1951 let x = Repr::<10>::new(IBig::from(-1), e);
1952 let s = ctx.sin::<10>(&x, None).unwrap().value();
1953 let c = ctx.cos::<10>(&x, None).unwrap().value();
1954 let (ss, cc) = ctx.sin_cos::<10>(&x, None);
1955 let ss = ss.unwrap().value();
1956 let cc = cc.unwrap().value();
1957 // sin is odd, cos is even: sin(x) ≈ x (negative), cos(x) ≈ 1
1958 assert_eq!(s.sign(), Sign::Negative);
1959 assert_eq!(c.sign(), Sign::Positive);
1960 assert_eq!(ss.sign(), Sign::Negative);
1961 assert_eq!(cc.sign(), Sign::Positive);
1962 }
1963 }
1964
1965 /// Regression: a 49-digit significand at precision 100 used to assertion-fail in `Context::sin`'s
1966 /// rounding logic (found during fuzzing). Promoted here from the excluded `fuzz/` crate so it runs
1967 /// in CI; rewritten to the current `Context::sin` API.
1968 #[test]
1969 fn test_sin_many_digit_rounding_no_panic() {
1970 let x = DBig::from_str("-5.525474318981006776603409487767135633516667011547942409467e-3")
1971 .unwrap();
1972 let ctx = Context::<mode::HalfEven>::new(100);
1973 let s = ctx.sin::<10>(x.repr(), None).unwrap().value();
1974 // sin(x) ≈ x for a small negative x — completing without panicking is the regression guard.
1975 assert_eq!(s.sign(), Sign::Negative);
1976 }
1977
1978 /// tan near a pole (π/2) must not panic, and its sign must follow the pole side: just below →
1979 /// large positive (→ +∞), just above → large negative (→ −∞). Guards the pole check, which
1980 /// tests `cos` with `significand.is_zero()` (not `is_pos_zero`, which would miss `-0`) and
1981 /// assigns the infinity sign as `sign(sin)·sign(cos)`.
1982 #[test]
1983 fn test_tan_near_pole_signs_and_no_panic() {
1984 let p = 53usize;
1985 let ctx = Context::<mode::HalfEven>::new(p);
1986 let half_pi = FBig::<mode::HalfEven>::pi(p) / 2u8;
1987 // a clear offset either side of the pole (≈2⁻¹⁰, far larger than half_pi's rounding error)
1988 let eps = FBig::<mode::HalfEven>::ONE >> 10;
1989 let below = ctx
1990 .tan::<2>((half_pi.clone() - &eps).repr(), None)
1991 .unwrap()
1992 .value();
1993 let above = ctx
1994 .tan::<2>((half_pi.clone() + &eps).repr(), None)
1995 .unwrap()
1996 .value();
1997 assert_eq!(below.sign(), Sign::Positive, "tan just below π/2 is large positive");
1998 assert_eq!(above.sign(), Sign::Negative, "tan just above π/2 is large negative");
1999 // sanity: tan(π/4) = 1
2000 let pi = FBig::<mode::HalfEven>::pi(p);
2001 let q = ctx.tan::<2>((pi / 4u8).repr(), None).unwrap().value();
2002 assert!(
2003 (q.clone() - FBig::ONE).abs_cmp(&(FBig::ONE >> 40)).is_le(),
2004 "tan(π/4) ≈ 1, got {q:?}"
2005 );
2006 }
2007
2008 /// Re-round a high-precision `HalfEven` oracle to precision `p` under the mode under test.
2009 /// The +60 guard bits put the oracle far from any rounding boundary, so the re-round is
2010 /// the correctly rounded value for the fixed inputs used here.
2011 fn reround<R: Round, const B: Word>(hi: &FBig<mode::HalfEven, B>, p: usize) -> FBig<R, B> {
2012 let ctx = Context::<R>::new(p);
2013 FBig::new(ctx.repr_round_ref(hi.repr()).value(), ctx)
2014 }
2015
2016 /// Assert the value is a zero and report its sign; asserts non-zero-ness by construction.
2017 fn zero_sign<R: Round, const B: Word>(v: &FBig<R, B>) -> bool {
2018 assert!(v.repr().significand.is_zero());
2019 v.repr().is_neg_zero()
2020 }
2021
2022 /// Assert the value is ±1 and return it as a sign-carrying integer.
2023 fn plus_minus_one<R: Round, const B: Word>(v: &FBig<R, B>) -> IBig {
2024 let i = IBig::try_from(v.clone()).unwrap();
2025 assert!(i == IBig::from(1) || i == IBig::from(-1));
2026 i
2027 }
2028
2029 /// The ×π exact-case table: quarter-integer arguments resolve exactly at every precision
2030 /// and rounding mode (they bypass the Ziv loop, whose containment test cannot certify an
2031 /// exact 0/±1 under directed rounding).
2032 #[test]
2033 fn test_pi_family_exact_cases() {
2034 fn check<R: ErrorBounds>(mode_ctx: &Context<R>, p: usize) {
2035 // sin_pi: integers → ±0 with the sign of x; odd half-integers → ±1
2036 for (input, neg_zero, expect_one) in [
2037 ("0", Some(false), None),
2038 ("1", Some(false), None),
2039 ("-1", Some(true), None),
2040 ("2", Some(false), None),
2041 ("-3", Some(true), None),
2042 ("0.5", None, Some(true)),
2043 ("-0.5", None, Some(false)),
2044 ("1.5", None, Some(false)),
2045 ("2.5", None, Some(true)),
2046 ] {
2047 let x = DBig::from_str(input).unwrap();
2048 let s = mode_ctx.sin_pi::<10>(x.repr(), None).unwrap().value();
2049 match (neg_zero, expect_one) {
2050 (Some(expected), _) => {
2051 assert_eq!(zero_sign(&s), expected, "sin_pi({input}) sign of zero, p={p}")
2052 }
2053 (_, Some(positive)) => assert_eq!(
2054 plus_minus_one(&s),
2055 if positive {
2056 IBig::from(1)
2057 } else {
2058 IBig::from(-1)
2059 },
2060 "sin_pi({input}) magnitude, p={p}"
2061 ),
2062 _ => panic!("test table row misconfigured"),
2063 }
2064 }
2065
2066 // cos_pi: integers → ±1 by parity; odd half-integers → +0
2067 for (input, neg_one, is_zero) in [
2068 ("0", false, false),
2069 ("1", true, false),
2070 ("2", false, false),
2071 ("3", true, false),
2072 ("0.5", false, true),
2073 ("1.5", false, true),
2074 ("-0.5", false, true),
2075 ("2.5", false, true),
2076 ] {
2077 let x = DBig::from_str(input).unwrap();
2078 let c = mode_ctx.cos_pi::<10>(x.repr(), None).unwrap().value();
2079 if is_zero {
2080 assert!(!zero_sign(&c), "cos_pi({input}) is +0, p={p}");
2081 } else {
2082 let expect = if neg_one {
2083 IBig::from(-1)
2084 } else {
2085 IBig::from(1)
2086 };
2087 assert_eq!(plus_minus_one(&c), expect, "cos_pi({input}), p={p}");
2088 }
2089 }
2090
2091 // tan_pi: every integer zero carries the sign of x (tan is odd, so
2092 // `tan_pi(−n) = −tan_pi(n)` must hold on the zeros too, matching MPFR);
2093 // quarter-integers → ±1; odd half-integers are indeterminate poles
2094 for (input, neg_zero, expect_one) in [
2095 ("0", Some(false), None),
2096 ("1", Some(false), None),
2097 ("-1", Some(true), None),
2098 ("2", Some(false), None),
2099 ("-3", Some(true), None),
2100 ("0.25", None, Some(true)),
2101 ("0.75", None, Some(false)),
2102 ("1.25", None, Some(true)),
2103 ("1.75", None, Some(false)),
2104 ("-0.25", None, Some(false)),
2105 ("-0.75", None, Some(true)),
2106 ] {
2107 let x = DBig::from_str(input).unwrap();
2108 let t = mode_ctx.tan_pi::<10>(x.repr(), None).unwrap().value();
2109 match (neg_zero, expect_one) {
2110 (Some(expected), _) => {
2111 assert_eq!(zero_sign(&t), expected, "tan_pi({input}), p={p}")
2112 }
2113 (_, Some(positive)) => assert_eq!(
2114 plus_minus_one(&t),
2115 if positive {
2116 IBig::from(1)
2117 } else {
2118 IBig::from(-1)
2119 },
2120 "tan_pi({input}), p={p}"
2121 ),
2122 _ => panic!("test table row misconfigured"),
2123 }
2124 }
2125 for input in ["0.5", "1.5", "-0.5", "2.5"] {
2126 let x = DBig::from_str(input).unwrap();
2127 assert!(
2128 matches!(mode_ctx.tan_pi::<10>(x.repr(), None), Err(FpError::Indeterminate)),
2129 "tan_pi({input}) is an indeterminate pole, p={p}"
2130 );
2131 }
2132
2133 // sin_cos_pi agrees with the two tables at half-integers
2134 let x = DBig::from_str("-0.5").unwrap();
2135 let (s, c) = mode_ctx.sin_cos_pi::<10>(x.repr(), None);
2136 assert_eq!(plus_minus_one(&s.unwrap().value()), IBig::from(-1));
2137 assert!(!zero_sign(&c.unwrap().value()));
2138
2139 // the string parser collapses "-0" to +0, so probe the −0 sentinel directly
2140 let s = mode_ctx
2141 .sin_pi::<10>(&Repr::neg_zero(), None)
2142 .unwrap()
2143 .value();
2144 assert!(zero_sign(&s), "sin_pi(-0) is -0");
2145 let t = mode_ctx
2146 .tan_pi::<10>(&Repr::neg_zero(), None)
2147 .unwrap()
2148 .value();
2149 assert!(zero_sign(&t), "tan_pi(-0) is -0");
2150 let (s, _) = mode_ctx.sin_cos_pi::<10>(&Repr::neg_zero(), None);
2151 assert!(zero_sign(&s.unwrap().value()), "sin_cos_pi(-0).0 is -0");
2152 }
2153
2154 for &p in &[20usize, 50, 100, 500] {
2155 check(&Context::<mode::HalfEven>::new(p), p);
2156 check(&Context::<mode::Down>::new(p), p);
2157 check(&Context::<mode::Up>::new(p), p);
2158 }
2159 }
2160
2161 /// The ×π general path vs the high-precision oracle (p + 60 under `HalfEven`, re-rounded
2162 /// under the mode under test), across the four canonical significand widths. The inputs
2163 /// cover every reduction branch: tiny arguments (k = 0 product path), the rational split,
2164 /// the quarter-integer ties (odd `n`, so `sin_pi`/`cos_pi` still take the general path),
2165 /// large integer parts, and negative arguments.
2166 #[test]
2167 fn test_pi_family_oracle() {
2168 let inputs = [
2169 "0.1",
2170 "0.2",
2171 "0.3",
2172 "0.4",
2173 "0.6",
2174 "0.7",
2175 "0.8",
2176 "0.9",
2177 "1.1",
2178 "1.2",
2179 "2.3",
2180 "2.6",
2181 "-0.3",
2182 "-1.2",
2183 "-2.6",
2184 "10.1",
2185 "123.456",
2186 "12345678901234567890.125",
2187 ];
2188 for &p in &[20usize, 50, 100, 500] {
2189 for input in inputs {
2190 let x = DBig::from_str(input).unwrap();
2191 let hi_ctx = Context::<mode::HalfEven>::new(p + 60);
2192 let hi_s = hi_ctx.sin_pi::<10>(x.repr(), None).unwrap().value();
2193 let hi_c = hi_ctx.cos_pi::<10>(x.repr(), None).unwrap().value();
2194 let hi_t = hi_ctx.tan_pi::<10>(x.repr(), None).unwrap().value();
2195
2196 // sin under HalfEven
2197 let got = Context::<mode::HalfEven>::new(p)
2198 .sin_pi::<10>(x.repr(), None)
2199 .unwrap()
2200 .value();
2201 assert_eq!(
2202 got,
2203 reround::<mode::HalfEven, 10>(&hi_s, p),
2204 "sin_pi({input}) at p={p}"
2205 );
2206 // cos under Down
2207 let got = Context::<mode::Down>::new(p)
2208 .cos_pi::<10>(x.repr(), None)
2209 .unwrap()
2210 .value();
2211 assert_eq!(
2212 got,
2213 reround::<mode::Down, 10>(&hi_c, p),
2214 "cos_pi({input}) at p={p} under Down"
2215 );
2216 // tan under Up
2217 let got = Context::<mode::Up>::new(p)
2218 .tan_pi::<10>(x.repr(), None)
2219 .unwrap()
2220 .value();
2221 assert_eq!(
2222 got,
2223 reround::<mode::Up, 10>(&hi_t, p),
2224 "tan_pi({input}) at p={p} under Up"
2225 );
2226 // sin_cos under HalfEven agrees with the separate evaluations
2227 let (s, c) = Context::<mode::HalfEven>::new(p).sin_cos_pi::<10>(x.repr(), None);
2228 assert_eq!(
2229 s.unwrap().value(),
2230 reround::<mode::HalfEven, 10>(&hi_s, p),
2231 "sin_cos_pi sin({input}) at p={p}"
2232 );
2233 assert_eq!(
2234 c.unwrap().value(),
2235 reround::<mode::HalfEven, 10>(&hi_c, p),
2236 "sin_cos_pi cos({input}) at p={p}"
2237 );
2238 }
2239 }
2240 }
2241
2242 /// The exact reduction mod 2 holds for arbitrarily large arguments: a huge integer part
2243 /// plus a `2^-10` residue must give exactly the same result as the bare residue (the
2244 /// radian `sin` would need ~1000 extra guard digits here), and huge integers hit the
2245 /// exact-case table through the no-materialization path.
2246 #[test]
2247 fn test_sin_pi_huge_exact_reduction() {
2248 for &p in &[20usize, 50, 100, 500] {
2249 let ctx = Context::<mode::HalfEven>::new(p);
2250
2251 // x = ((2^60 + 1)·2^950 + 1)·2^-10 ≡ 2^-10 (mod 2)
2252 let sig = (((IBig::from(1) << 60usize) + 1) << 950usize) + 1;
2253 let x = Repr::<2>::new(sig, -10);
2254 let residue = Repr::<2>::new(IBig::from(1), -10);
2255 assert_eq!(
2256 ctx.sin_pi::<2>(&x, None).unwrap().value(),
2257 ctx.sin_pi::<2>(&residue, None).unwrap().value(),
2258 "sin_pi(huge) == sin_pi(2^-10) at p={p}"
2259 );
2260 assert_eq!(
2261 ctx.cos_pi::<2>(&x, None).unwrap().value(),
2262 ctx.cos_pi::<2>(&residue, None).unwrap().value(),
2263 "cos_pi(huge) == cos_pi(2^-10) at p={p}"
2264 );
2265
2266 // an even integer at a huge exponent: quarter_class must not materialize 2^940
2267 let huge_int = Repr::<2>::new((IBig::from(1) << 60usize) + 1, 940);
2268 assert_eq!(
2269 ctx.sin_pi::<2>(&huge_int, None).unwrap().value(),
2270 FBig::<mode::HalfEven>::ZERO
2271 );
2272 assert_eq!(
2273 ctx.cos_pi::<2>(&huge_int, None).unwrap().value(),
2274 FBig::<mode::HalfEven>::ONE
2275 );
2276 // ...and an odd one at a small exponent: cos_pi(2^60+1) = -1 (odd integer)
2277 assert_eq!(
2278 ctx.cos_pi::<2>(&Repr::<2>::new((IBig::from(1) << 60usize) + 1, 0), None)
2279 .unwrap()
2280 .value(),
2281 FBig::<mode::HalfEven>::NEG_ONE
2282 );
2283 }
2284 }
2285
2286 /// An astronomically-scaled tiny argument (`|x| < 1/4` with s ~ 10⁹) must take the
2287 /// k = 0 fast path — materializing `B^s` here would allocate ~125 MB — and still round
2288 /// correctly against the oracle.
2289 // The 10⁹-scale exponent needs the 64-bit `isize` range; on 32-bit targets the underflow
2290 // guard fires first (its range is ~4000× smaller), so the test is 64-bit-only.
2291 #[cfg(target_pointer_width = "64")]
2292 #[test]
2293 fn test_sin_pi_tiny_no_materialization() {
2294 let x = Repr::<2>::new(IBig::from(3), -1_000_000_000);
2295 for &p in &[20usize, 50, 100, 500] {
2296 let ctx = Context::<mode::HalfEven>::new(p);
2297 let s = ctx.sin_pi::<2>(&x, None).unwrap().value();
2298 assert_eq!(s.sign(), Sign::Positive);
2299
2300 let hi = Context::<mode::HalfEven>::new(p + 60)
2301 .sin_pi::<2>(&x, None)
2302 .unwrap()
2303 .value();
2304 assert_eq!(s, reround::<mode::HalfEven, 2>(&hi, p));
2305 }
2306 }
2307
2308 /// `tan_pi` just off a pole: large positive below, large negative above (the poles
2309 /// themselves are `Indeterminate`).
2310 #[test]
2311 fn test_tan_pi_near_pole_signs() {
2312 for &p in &[20usize, 50, 100, 500] {
2313 let ctx = Context::<mode::HalfEven>::new(p);
2314 let eps = FBig::<mode::HalfEven>::ONE >> 10;
2315 // both operands must carry the context precision: at precision 1 the subtraction
2316 // 0.5 − 2^-10 rounds back onto the exact pole
2317 let half: FBig<mode::HalfEven> = FBig::ONE.with_precision(p).value() / 2u8;
2318 let eps: FBig<mode::HalfEven> = eps.with_precision(p).value();
2319 let below = ctx
2320 .tan_pi::<2>((half.clone() - &eps).repr(), None)
2321 .unwrap()
2322 .value();
2323 let above = ctx
2324 .tan_pi::<2>((half.clone() + &eps).repr(), None)
2325 .unwrap()
2326 .value();
2327 assert_eq!(below.sign(), Sign::Positive, "tan_pi just below 1/2 at p={p}");
2328 assert_eq!(above.sign(), Sign::Negative, "tan_pi just above 1/2 at p={p}");
2329 }
2330 }
2331
2332 /// The ×u forward family's exact-value tables, in degrees (u = 360 — quarters, sixths and
2333 /// the tangent eighths all appear) and at u ∈ {1, 4, 12}, across the canonical precision
2334 /// sweep and rounding modes.
2335 #[test]
2336 fn test_unit_family_exact_tables() {
2337 fn check<R: ErrorBounds>(mode_ctx: &Context<R>) {
2338 let x = |v: &str| DBig::from_str(v).unwrap();
2339
2340 // degrees: quarters
2341 assert_eq!(
2342 mode_ctx
2343 .sin_unit::<10>(x("90").repr(), 360, None)
2344 .unwrap()
2345 .value(),
2346 DBig::from(1)
2347 );
2348 assert_eq!(
2349 mode_ctx
2350 .sin_unit::<10>(x("270").repr(), 360, None)
2351 .unwrap()
2352 .value(),
2353 -DBig::from(1)
2354 );
2355 let zero = mode_ctx
2356 .sin_unit::<10>(x("180").repr(), 360, None)
2357 .unwrap()
2358 .value();
2359 assert!(zero.repr().significand.is_zero() && !zero.repr().is_neg_zero());
2360 assert_eq!(
2361 mode_ctx
2362 .cos_unit::<10>(x("90").repr(), 360, None)
2363 .unwrap()
2364 .value(),
2365 DBig::ZERO
2366 );
2367 assert_eq!(
2368 mode_ctx
2369 .cos_unit::<10>(x("180").repr(), 360, None)
2370 .unwrap()
2371 .value(),
2372 -DBig::from(1)
2373 );
2374 // degrees: sixths
2375 assert_eq!(
2376 mode_ctx
2377 .sin_unit::<10>(x("30").repr(), 360, None)
2378 .unwrap()
2379 .value(),
2380 DBig::from_str("0.5").unwrap()
2381 );
2382 assert_eq!(
2383 mode_ctx
2384 .sin_unit::<10>(x("210").repr(), 360, None)
2385 .unwrap()
2386 .value(),
2387 DBig::from_str("-0.5").unwrap()
2388 );
2389 assert_eq!(
2390 mode_ctx
2391 .cos_unit::<10>(x("60").repr(), 360, None)
2392 .unwrap()
2393 .value(),
2394 DBig::from_str("0.5").unwrap()
2395 );
2396 assert_eq!(
2397 mode_ctx
2398 .cos_unit::<10>(x("120").repr(), 360, None)
2399 .unwrap()
2400 .value(),
2401 DBig::from_str("-0.5").unwrap()
2402 );
2403 // degrees: tangent eighths and poles
2404 assert_eq!(
2405 mode_ctx
2406 .tan_unit::<10>(x("45").repr(), 360, None)
2407 .unwrap()
2408 .value(),
2409 DBig::from(1)
2410 );
2411 assert_eq!(
2412 mode_ctx
2413 .tan_unit::<10>(x("135").repr(), 360, None)
2414 .unwrap()
2415 .value(),
2416 -DBig::from(1)
2417 );
2418 let t180 = mode_ctx
2419 .tan_unit::<10>(x("180").repr(), 360, None)
2420 .unwrap()
2421 .value();
2422 assert!(t180.repr().is_pos_zero(), "tan_unit(180°) is +0 (sign of x)");
2423 let t_180 = mode_ctx
2424 .tan_unit::<10>(x("-180").repr(), 360, None)
2425 .unwrap()
2426 .value();
2427 assert!(t_180.repr().is_neg_zero(), "tan_unit(−180°) is −0 (sign of x)");
2428 for pole in ["90", "270"] {
2429 assert!(matches!(
2430 mode_ctx.tan_unit::<10>(x(pole).repr(), 360, None),
2431 Err(FpError::Indeterminate)
2432 ));
2433 }
2434 // u = 1: sin_unit(x, 1) = sin(2πx), same table shape in u/4 = 0.25 steps
2435 assert_eq!(
2436 mode_ctx
2437 .sin_unit::<10>(x("0.25").repr(), 1, None)
2438 .unwrap()
2439 .value(),
2440 DBig::from(1)
2441 );
2442 assert_eq!(
2443 mode_ctx
2444 .sin_unit::<10>(x("0.50").repr(), 1, None)
2445 .unwrap()
2446 .value(),
2447 DBig::ZERO
2448 );
2449 // u = 12: x = 1 = u/12 (sine sixth), x = 2 = u/6 (cosine sixth), x = 3 = u/4
2450 assert_eq!(
2451 mode_ctx
2452 .sin_unit::<10>(x("1").repr(), 12, None)
2453 .unwrap()
2454 .value(),
2455 DBig::from_str("0.5").unwrap()
2456 );
2457 assert_eq!(
2458 mode_ctx
2459 .cos_unit::<10>(x("2").repr(), 12, None)
2460 .unwrap()
2461 .value(),
2462 DBig::from_str("0.5").unwrap() // cos(π/3) = +1/2
2463 );
2464 assert_eq!(
2465 mode_ctx
2466 .cos_unit::<10>(x("3").repr(), 12, None)
2467 .unwrap()
2468 .value(),
2469 DBig::ZERO
2470 );
2471 // sin_cos_unit agrees with the separate evaluations, including at the
2472 // sixth-type points (one exact, one general)
2473 for deg in ["30", "45", "60", "90", "100", "210"] {
2474 let xd = x(deg);
2475 let (s, c) = mode_ctx.sin_cos_unit::<10>(xd.repr(), 360, None);
2476 assert_eq!(
2477 s.unwrap().value(),
2478 mode_ctx
2479 .sin_unit::<10>(xd.repr(), 360, None)
2480 .unwrap()
2481 .value(),
2482 "sin_cos_unit sin {deg}°"
2483 );
2484 assert_eq!(
2485 c.unwrap().value(),
2486 mode_ctx
2487 .cos_unit::<10>(xd.repr(), 360, None)
2488 .unwrap()
2489 .value(),
2490 "sin_cos_unit cos {deg}°"
2491 );
2492 }
2493 // u = 0 is out of domain for the forward family
2494 let r1 = mode_ctx.sin_unit::<10>(x("1").repr(), 0, None);
2495 let r2 = mode_ctx.cos_unit::<10>(x("1").repr(), 0, None);
2496 let r3 = mode_ctx.tan_unit::<10>(x("1").repr(), 0, None);
2497 let (r4s, r4c) = mode_ctx.sin_cos_unit::<10>(x("1").repr(), 0, None);
2498 for (i, r) in [r1, r2, r3, r4s, r4c].into_iter().enumerate() {
2499 assert!(
2500 matches!(r.map(|_| ()), Err(FpError::OutOfDomain)),
2501 "u = 0 is out of domain (slot {i})"
2502 );
2503 }
2504 }
2505
2506 for &p in &[20usize, 50, 100, 500] {
2507 check(&Context::<mode::HalfEven>::new(p));
2508 check(&Context::<mode::Down>::new(p));
2509 check(&Context::<mode::Up>::new(p));
2510 }
2511 }
2512
2513 /// The ×π family is exactly the ×u family at u = 2: every value agrees.
2514 #[test]
2515 fn test_pi_family_equals_unit_family() {
2516 for input in [
2517 "0.1", "0.25", "0.3", "0.5", "1.2", "2.5", "-0.75", "10.1", "1e50",
2518 ] {
2519 let x = DBig::from_str(input).unwrap();
2520 for &p in &[20usize, 50, 100, 500] {
2521 let ctx = Context::<mode::HalfEven>::new(p);
2522 assert_eq!(
2523 ctx.sin_pi::<10>(x.repr(), None).unwrap().value(),
2524 ctx.sin_unit::<10>(x.repr(), 2, None).unwrap().value(),
2525 "sin_pi == sin_unit(2), x={input}"
2526 );
2527 assert_eq!(
2528 ctx.cos_pi::<10>(x.repr(), None).unwrap().value(),
2529 ctx.cos_unit::<10>(x.repr(), 2, None).unwrap().value()
2530 );
2531 let t_pi = ctx.tan_pi::<10>(x.repr(), None);
2532 let t_u = ctx.tan_unit::<10>(x.repr(), 2, None);
2533 assert!(
2534 t_pi.is_err() == t_u.is_err()
2535 && t_pi.map(|v| v.value()) == t_u.map(|v| v.value())
2536 );
2537 let (s_pi, c_pi) = ctx.sin_cos_pi::<10>(x.repr(), None);
2538 let (s_u, c_u) = ctx.sin_cos_unit::<10>(x.repr(), 2, None);
2539 assert_eq!(s_pi.unwrap().value(), s_u.unwrap().value());
2540 assert_eq!(c_pi.unwrap().value(), c_u.unwrap().value());
2541 }
2542 }
2543 }
2544
2545 /// The inverse ×u family: exact rows (in degrees), the u = 0 limits, and the general path
2546 /// against the high-precision oracle.
2547 #[test]
2548 fn test_inverse_unit_family() {
2549 fn check<R: ErrorBounds>(mode_ctx: &Context<R>) {
2550 let x = |v: &str| DBig::from_str(v).unwrap();
2551 let u = 360usize;
2552
2553 // exact rows
2554 assert_eq!(
2555 mode_ctx
2556 .asin_unit::<10>(x("1").repr(), u, None)
2557 .unwrap()
2558 .value(),
2559 DBig::from_str("90").unwrap()
2560 );
2561 assert_eq!(
2562 mode_ctx
2563 .asin_unit::<10>(x("-1").repr(), u, None)
2564 .unwrap()
2565 .value(),
2566 DBig::from_str("-90").unwrap()
2567 );
2568 assert_eq!(
2569 mode_ctx
2570 .asin_unit::<10>(x("0.5").repr(), u, None)
2571 .unwrap()
2572 .value(),
2573 DBig::from_str("30").unwrap()
2574 );
2575 assert_eq!(
2576 mode_ctx
2577 .acos_unit::<10>(x("1").repr(), u, None)
2578 .unwrap()
2579 .value(),
2580 DBig::ZERO
2581 );
2582 assert_eq!(
2583 mode_ctx
2584 .acos_unit::<10>(x("0").repr(), u, None)
2585 .unwrap()
2586 .value(),
2587 DBig::from_str("90").unwrap()
2588 );
2589 assert_eq!(
2590 mode_ctx
2591 .acos_unit::<10>(x("-0.5").repr(), u, None)
2592 .unwrap()
2593 .value(),
2594 DBig::from_str("120").unwrap()
2595 );
2596 assert_eq!(
2597 mode_ctx
2598 .atan_unit::<10>(x("1.0").repr(), u, None)
2599 .unwrap()
2600 .value(),
2601 DBig::from_str("45").unwrap()
2602 );
2603 assert_eq!(
2604 mode_ctx
2605 .atan_unit::<10>(&Repr::infinity(), u, None)
2606 .unwrap()
2607 .value(),
2608 DBig::from_str("90").unwrap()
2609 );
2610
2611 // atan2_unit's C99 table, in u-fractions
2612 let quarter = |a: &str, b: &str| {
2613 mode_ctx
2614 .atan2_unit::<10>(x(a).repr(), x(b).repr(), u, None)
2615 .unwrap()
2616 .value()
2617 };
2618 assert_eq!(quarter("1.0", "1.0"), DBig::from_str("45").unwrap());
2619 assert_eq!(quarter("1.0", "-1.0"), DBig::from_str("135").unwrap());
2620 assert_eq!(quarter("0", "1"), DBig::ZERO);
2621 assert_eq!(quarter("1", "0"), DBig::from_str("90").unwrap());
2622 assert_eq!(quarter("0", "-1"), DBig::from_str("180").unwrap());
2623 assert_eq!(
2624 mode_ctx
2625 .atan2_unit::<10>(&Repr::infinity(), &Repr::infinity(), u, None)
2626 .unwrap()
2627 .value(),
2628 DBig::from_str("45").unwrap()
2629 );
2630 assert_eq!(
2631 mode_ctx
2632 .atan2_unit::<10>(&Repr::infinity(), &Repr::neg_infinity(), u, None)
2633 .unwrap()
2634 .value(),
2635 DBig::from_str("135").unwrap()
2636 );
2637 assert!(matches!(
2638 mode_ctx.atan2_unit::<10>(x("0").repr(), x("0").repr(), u, None),
2639 Err(FpError::OutOfDomain)
2640 ));
2641
2642 // u = 0: the signed-zero limits
2643 assert!(mode_ctx
2644 .asin_unit::<10>(x("0.5").repr(), 0, None)
2645 .unwrap()
2646 .value()
2647 .repr()
2648 .significand
2649 .is_zero());
2650 assert!(mode_ctx
2651 .acos_unit::<10>(x("0.5").repr(), 0, None)
2652 .unwrap()
2653 .value()
2654 .repr()
2655 .is_pos_zero());
2656
2657 // domain errors
2658 assert!(matches!(
2659 mode_ctx.asin_unit::<10>(x("1.5").repr(), u, None),
2660 Err(FpError::OutOfDomain)
2661 ));
2662 assert!(matches!(
2663 mode_ctx.acos_unit::<10>(x("-1.5").repr(), u, None),
2664 Err(FpError::OutOfDomain)
2665 ));
2666 // ... and the domain error outranks the u = 0 limit (the limit of a function
2667 // that is undefined at this x for every u > 0 is not +0)
2668 assert!(matches!(
2669 mode_ctx.asin_unit::<10>(x("1.5").repr(), 0, None),
2670 Err(FpError::OutOfDomain)
2671 ));
2672 assert!(matches!(
2673 mode_ctx.acos_unit::<10>(x("-1.5").repr(), 0, None),
2674 Err(FpError::OutOfDomain)
2675 ));
2676 }
2677
2678 for &p in &[20usize, 50, 100, 500] {
2679 check(&Context::<mode::HalfEven>::new(p));
2680 check(&Context::<mode::Down>::new(p));
2681
2682 // general path vs the oracle, under two modes
2683 for input in ["0.1", "0.3", "0.7", "-0.2", "0.9"] {
2684 let x = DBig::from_str(input).unwrap();
2685 for &u in &[2usize, 7, 360] {
2686 let hi = Context::<mode::HalfEven>::new(p + 60);
2687 for op in 0..3 {
2688 match op {
2689 0 => {
2690 let expect = reround::<mode::HalfEven, 10>(
2691 &hi.asin_unit::<10>(x.repr(), u, None).unwrap().value(),
2692 p,
2693 );
2694 assert_eq!(
2695 Context::<mode::HalfEven>::new(p)
2696 .asin_unit::<10>(x.repr(), u, None)
2697 .unwrap()
2698 .value(),
2699 expect,
2700 "asin_unit({input}, {u}) at p={p}"
2701 );
2702 }
2703 1 => {
2704 let expect = reround::<mode::Down, 10>(
2705 &hi.acos_unit::<10>(x.repr(), u, None).unwrap().value(),
2706 p,
2707 );
2708 assert_eq!(
2709 Context::<mode::Down>::new(p)
2710 .acos_unit::<10>(x.repr(), u, None)
2711 .unwrap()
2712 .value(),
2713 expect,
2714 "acos_unit({input}, {u}) at p={p} under Down"
2715 );
2716 }
2717 _ => {
2718 let expect = reround::<mode::HalfEven, 10>(
2719 &hi.atan_unit::<10>(x.repr(), u, None).unwrap().value(),
2720 p,
2721 );
2722 assert_eq!(
2723 Context::<mode::HalfEven>::new(p)
2724 .atan_unit::<10>(x.repr(), u, None)
2725 .unwrap()
2726 .value(),
2727 expect,
2728 "atan_unit({input}, {u}) at p={p}"
2729 );
2730 }
2731 }
2732 }
2733 }
2734 }
2735
2736 // atan2_unit general path vs the oracle
2737 for (ya, xa) in [("1.2", "3.4"), ("-2.5", "0.7"), ("0.3", "-4.0")] {
2738 let y = DBig::from_str(ya).unwrap();
2739 let x = DBig::from_str(xa).unwrap();
2740 let hi = Context::<mode::HalfEven>::new(p + 60);
2741 let expect = reround::<mode::HalfEven, 10>(
2742 &hi.atan2_unit::<10>(y.repr(), x.repr(), 360, None)
2743 .unwrap()
2744 .value(),
2745 p,
2746 );
2747 assert_eq!(
2748 Context::<mode::HalfEven>::new(p)
2749 .atan2_unit::<10>(y.repr(), x.repr(), 360, None)
2750 .unwrap()
2751 .value(),
2752 expect,
2753 "atan2_unit({ya}, {xa}, 360) at p={p}"
2754 );
2755 }
2756 }
2757
2758 // roundtrip: asin_unit(sin_unit(x)) ≈ x on the degrees scale, for x in the principal
2759 // range [−90°, 90°] (beyond it asin folds into the range)
2760 type HF = FBig<mode::HalfEven, 10>;
2761 let ctx = Context::<mode::HalfEven>::new(50);
2762 let tol = HF::from_parts(IBig::from(1), -40);
2763 for deg in ["10", "30", "45", "89.9", "90", "-42.7"] {
2764 let x = HF::from_str(deg).unwrap().with_precision(50).value();
2765 let s = ctx.sin_unit::<10>(x.repr(), 360, None).unwrap().value();
2766 let back = ctx.asin_unit::<10>(s.repr(), 360, None).unwrap().value();
2767 assert!(
2768 (x.clone() - back).abs().abs_cmp(&tol).is_le(),
2769 "asin_unit(sin_unit({deg}°)) roundtrip"
2770 );
2771 }
2772 }
2773
2774 /// The ×u general path vs the high-precision oracle (p + 60 under `HalfEven`, re-rounded
2775 /// under the mode under test), across u values with no shared factors (7, 11), the
2776 /// composite 360, and the tight u = 1.
2777 #[test]
2778 fn test_unit_family_oracle() {
2779 let inputs = [
2780 "0.1", "0.2", "0.3", "0.4", "0.6", "0.7", "0.9", "1.1", "2.3", "-0.3", "-1.2", "10.1",
2781 "123.456", "1e20",
2782 ];
2783 for &p in &[20usize, 50, 100, 500] {
2784 for input in inputs {
2785 let x = DBig::from_str(input).unwrap();
2786 for &u in &[1usize, 7, 11, 360] {
2787 let hi_ctx = Context::<mode::HalfEven>::new(p + 60);
2788 let hi_s = hi_ctx.sin_unit::<10>(x.repr(), u, None).unwrap().value();
2789 let hi_c = hi_ctx.cos_unit::<10>(x.repr(), u, None).unwrap().value();
2790 let hi_t = match hi_ctx.tan_unit::<10>(x.repr(), u, None) {
2791 Ok(v) => v.value(),
2792 Err(_) => continue, // pole (pinpointed by the exact-table test)
2793 };
2794
2795 let got = Context::<mode::HalfEven>::new(p)
2796 .sin_unit::<10>(x.repr(), u, None)
2797 .unwrap()
2798 .value();
2799 assert_eq!(
2800 got,
2801 reround::<mode::HalfEven, 10>(&hi_s, p),
2802 "sin_unit({input}, {u}) at p={p}"
2803 );
2804 let got = Context::<mode::Down>::new(p)
2805 .cos_unit::<10>(x.repr(), u, None)
2806 .unwrap()
2807 .value();
2808 assert_eq!(
2809 got,
2810 reround::<mode::Down, 10>(&hi_c, p),
2811 "cos_unit({input}, {u}) at p={p} under Down"
2812 );
2813 let got = Context::<mode::Up>::new(p)
2814 .tan_unit::<10>(x.repr(), u, None)
2815 .unwrap()
2816 .value();
2817 assert_eq!(
2818 got,
2819 reround::<mode::Up, 10>(&hi_t, p),
2820 "tan_unit({input}, {u}) at p={p} under Up"
2821 );
2822 }
2823 }
2824 }
2825 }
2826
2827 /// Direct unit checks of the integer classification and reduction kernels, including an
2828 /// odd base (where the exact points beyond the integers are sparse, and the split's
2829 /// `r = num/den` is not representable as a base-B float).
2830 #[test]
2831 fn test_unit_class_and_reduction_kernels() {
2832 // classification j = (k·x/u) mod 2k, base 10. For u = 2 the k = 12 residue is always a
2833 // multiple of 3 (12x/2 = 6x — a half-integer), so the u = 2 rows only exercise the
2834 // quarter table; the degrees rows hit the sixths and the tangent eighths.
2835 let uc = |m: i64, e: isize, u: usize, k: usize| {
2836 unit_residue(&Repr::<10>::new(IBig::from(m), e), u, k)
2837 };
2838 // u = 2, k = 12: j = 6x mod 24
2839 assert_eq!(uc(5, -1, 2, 12), Some(3)); // 0.5 → 3 (q = 1: sin +1)
2840 assert_eq!(uc(25, -2, 2, 12), None); // 1.5 ∉ ℤ (sin(π/4) = √2/2, general)
2841 assert_eq!(uc(75, -2, 2, 12), None); // 4.5 ∉ ℤ
2842 assert_eq!(uc(-25, -2, 2, 12), None);
2843 assert_eq!(uc(3, 0, 2, 12), Some(18)); // 18 (q = 6: sin ±0)
2844 assert_eq!(uc(8, 0, 2, 12), Some(0)); // 48 ≡ 0
2845 assert_eq!(uc(1, 1, 2, 12), Some(12)); // 60 ≡ 12 (q = 4)
2846 assert_eq!(uc(1, -1, 2, 12), None); // 0.6 ∉ ℤ
2847 assert_eq!(uc(1, -1000, 2, 12), None); // huge s: rejected by bounds, no materialization
2848 // u = 2, k = 8: j = 4x mod 16
2849 assert_eq!(uc(5, -1, 2, 8), Some(2)); // 0.5 → 2 (m = 2: the tan pole)
2850 assert_eq!(uc(25, -2, 2, 8), Some(1)); // 0.25 → 1 (tan(π/4) = 1)
2851 assert_eq!(uc(75, -2, 2, 8), Some(3)); // 0.75 → 3 (tan(3π/4) = −1)
2852 assert_eq!(uc(15, -1, 2, 8), Some(6)); // 1.5 → 6 (m = 6: the tan pole)
2853 // degrees, k = 12: j = x/30 mod 24
2854 assert_eq!(uc(30, 0, 360, 12), Some(1)); // sin 30° = 1/2
2855 assert_eq!(uc(150, 0, 360, 12), Some(5)); // sin 150° = 1/2
2856 assert_eq!(uc(210, 0, 360, 12), Some(7)); // sin 210° = −1/2
2857 assert_eq!(uc(60, 0, 360, 12), Some(2)); // cos 60° = 1/2
2858 assert_eq!(uc(120, 0, 360, 12), Some(4)); // cos 120° = −1/2
2859 assert_eq!(uc(90, 0, 360, 12), Some(3)); // sin 90° = 1 (quarter row)
2860 assert_eq!(uc(45, 0, 360, 12), None); // sin 45° = √2/2 (general path)
2861 assert_eq!(uc(455, -1, 360, 12), None); // 45.5° — general path
2862 // degrees, k = 8: j = x/45 mod 16
2863 assert_eq!(uc(45, 0, 360, 8), Some(1)); // tan 45° = 1
2864 assert_eq!(uc(135, 0, 360, 8), Some(3)); // tan 135° = −1
2865 assert_eq!(uc(90, 0, 360, 8), Some(2)); // tan 90° — the pole
2866 assert_eq!(uc(180, 0, 360, 8), Some(4)); // tan 180° → −0 row
2867 // huge integer exponent reduced by modular exponentiation (10^30 never materialized):
2868 // 12·10^30/360 = 10^29/3 ∉ ℤ; 12·10^30/8 = 15·10^29 ∈ ℤ with 15·10^29 ≡ 0 (mod 24).
2869 assert_eq!(uc(1, 30, 360, 12), None);
2870 assert_eq!(uc(1, 30, 8, 12), Some(0));
2871
2872 // classification, base 3 (odd): k·x/u ∈ ℤ for integer x whenever u | k·m
2873 let uc3 = |m: i64, e: isize, u: usize, k: usize| {
2874 unit_residue(&Repr::<3>::new(IBig::from(m), e), u, k)
2875 };
2876 assert_eq!(uc3(7, 0, 2, 12), Some(18)); // 42 ≡ 18 (mod 24)
2877 assert_eq!(uc3(6, 0, 2, 12), Some(12)); // 36 ≡ 12 (mod 24)
2878 // 1/3 is exact in base 3: 12·(1/3)/2 = 2 ∈ ℤ — a sixth row invisible in base 2/10
2879 assert_eq!(uc3(1, -1, 2, 12), Some(2));
2880 assert_eq!(uc3(1, -1, 2, 8), None); // 8/(3·2) = 4/3 ∉ ℤ
2881
2882 // exact reduction split, base 10, u = 2: r = |x| − k/2 as an exact rational
2883 match reduce_unit_argument(&Repr::<10>::new(IBig::from(3), -1), 2) {
2884 // 0.3: k = 1, r = -0.2 = -8/40
2885 UnitReduced::Split { quadrant, num, den } => {
2886 assert_eq!(num, IBig::from(-8));
2887 assert_eq!(den, IBig::from(40));
2888 assert_eq!(quadrant, Quadrant::Second);
2889 }
2890 _ => panic!("0.3 must take the rational split"),
2891 }
2892 match reduce_unit_argument(&Repr::<10>::new(IBig::from(26), -1), 2) {
2893 // 2.6: k = 5, r = 0.1 = 4/40
2894 UnitReduced::Split { quadrant, num, den } => {
2895 assert_eq!(num, IBig::from(4));
2896 assert_eq!(den, IBig::from(40));
2897 assert_eq!(quadrant, Quadrant::Second); // k = 5 ≡ 1 (mod 4)
2898 }
2899 _ => panic!("2.6 must take the rational split"),
2900 }
2901 // the tie 0.25 (u = 2): k = 1 (half-up), r = -1/4 = -50/200
2902 match reduce_unit_argument(&Repr::<10>::new(IBig::from(25), -2), 2) {
2903 UnitReduced::Split { num, den, .. } => {
2904 assert_eq!(num, IBig::from(-100));
2905 assert_eq!(den, IBig::from(400));
2906 }
2907 _ => panic!("0.25 must take the rational split"),
2908 }
2909 // u = 8, integer argument on the general path: x = 5 ≡ 5 (mod 8), k = 3,
2910 // r = 5 − 3·8/4 = −1 = −4/4
2911 match reduce_unit_argument(&Repr::<10>::new(IBig::from(5), 0), 8) {
2912 UnitReduced::Split { quadrant, num, den } => {
2913 assert_eq!(num, IBig::from(-4));
2914 assert_eq!(den, IBig::from(4));
2915 assert_eq!(quadrant, Quadrant::Fourth); // k = 3
2916 }
2917 _ => panic!("sin_unit(5, 8) must take the rational split"),
2918 }
2919 // u = 360, a huge integer multiple of u reduces exactly: x = 10^6 degrees,
2920 // 10^6 mod 360 = 280, k = round(4·280/360) = 3, r = 280 − 270 = 10 = 40/4
2921 match reduce_unit_argument(&Repr::<10>::new(IBig::from(1_000_000), 0), 360) {
2922 UnitReduced::Split { quadrant, num, den } => {
2923 assert_eq!(num, IBig::from(40));
2924 assert_eq!(den, IBig::from(4));
2925 assert_eq!(quadrant, Quadrant::Fourth); // k = 3
2926 }
2927 _ => panic!("10^6 degrees must take the rational split"),
2928 }
2929 // tiny: the k = 0 product path, no denominator materialized
2930 assert!(matches!(
2931 reduce_unit_argument(&Repr::<10>::new(IBig::from(1), -1), 2),
2932 UnitReduced::Small(_)
2933 ));
2934 assert!(matches!(
2935 reduce_unit_argument(&Repr::<2>::new(IBig::from(3), -1_000_000_000), 2),
2936 UnitReduced::Small(_)
2937 ));
2938 // negative input reduces on |x|
2939 match reduce_unit_argument(&Repr::<10>::new(IBig::from(-3), -1), 2) {
2940 UnitReduced::Split { num, den, .. } => {
2941 assert_eq!(num, IBig::from(-8));
2942 assert_eq!(den, IBig::from(40));
2943 }
2944 _ => panic!("-0.3 must take the rational split"),
2945 }
2946 }
2947
2948 /// An argument that merely *rounds* onto `±1` is not the endpoint: the inverse functions have
2949 /// a vertical tangent there, so the true result sits `√(2δ)` away — many ulps of the result.
2950 ///
2951 /// Regression: `asin_ball`'s endpoint branch keyed on `√(1−x²)` *collapsing* onto zero, and
2952 /// `Ball::sqrt` returned that zero with the radius dropped — so `asin(1 − 10⁻¹⁵⁵)` at 100
2953 /// digits returned exactly `π/2`, a result ~10⁹ ulps off, certified by the Ziv loop because
2954 /// the endpoint branch reports only a few ulps of radius. (The `acos`/×u rows restated here
2955 /// were saved by the loop's zero-candidate guard, which refuses to certify an exact zero
2956 /// against a nonzero radius; they are asserted for the same reason.)
2957 #[test]
2958 fn test_inverse_endpoints_compare_exactly() {
2959 // `1 − 10^-n` with `n = 1.5p + 5`: beyond the initial guard (50), so the argument rounds
2960 // onto exactly `1` at the first work precision for p ≥ 100 — the case that used to snap —
2961 // yet `√(2·10^-n)` is far above a result ulp. At p = 20/50 the general path is taken
2962 // anyway; those rows are plain correctness checks.
2963 for &p in &[20usize, 50, 100, 500] {
2964 let n = 3 * p / 2 + 5;
2965 let x = DBig::from_str(&format!("0.{}", "9".repeat(n))).unwrap();
2966 let ctx = Context::<mode::HalfEven>::new(p);
2967 let hi = Context::<mode::HalfEven>::new(p + 60);
2968 let reround = |v: FBig<mode::HalfEven, 10>| v.with_precision(p).value();
2969
2970 let asin = ctx.asin::<10>(x.repr(), None).unwrap().value();
2971 let asin_hi = reround(hi.asin::<10>(x.repr(), None).unwrap().value());
2972 assert_eq!(asin, asin_hi, "asin(1 - 10^-{n}) at p={p}");
2973 let one = Repr::<10>::one();
2974 assert_ne!(
2975 asin,
2976 ctx.asin::<10>(&one, None).unwrap().value(),
2977 "asin(1 - 10^-{n}) is not the endpoint"
2978 );
2979
2980 let acos = ctx.acos::<10>(x.repr(), None).unwrap().value();
2981 let acos_hi = reround(hi.acos::<10>(x.repr(), None).unwrap().value());
2982 assert_eq!(acos, acos_hi, "acos(1 − 10^-{n}) at p={p}");
2983 assert!(!acos.repr().significand().is_zero(), "acos is not an exact 0");
2984
2985 // the ×u inverses route through the same kernels
2986 let (asin_u, asin_u_hi) = (
2987 ctx.asin_unit::<10>(x.repr(), 360, None).unwrap().value(),
2988 reround(hi.asin_unit::<10>(x.repr(), 360, None).unwrap().value()),
2989 );
2990 assert_eq!(asin_u, asin_u_hi, "asin_unit(1 − 10^-{n}, 360) at p={p}");
2991 assert_ne!(
2992 asin_u,
2993 ctx.asin_unit::<10>(&one, 360, None).unwrap().value(),
2994 "asin_unit(1 − 10^-{n}) is not the u/4 endpoint"
2995 );
2996 let (acos_u, acos_u_hi) = (
2997 ctx.acos_unit::<10>(x.repr(), 360, None).unwrap().value(),
2998 reround(hi.acos_unit::<10>(x.repr(), 360, None).unwrap().value()),
2999 );
3000 assert_eq!(acos_u, acos_u_hi, "acos_unit(1 − 10^-{n}, 360) at p={p}");
3001 assert!(!acos_u.repr().significand().is_zero(), "acos_unit is not an exact 0");
3002 }
3003 }
3004}