1use crate::{
10 ball::Ball,
11 error::{assert_limited_precision, FpError},
12 fbig::FBig,
13 math::{
14 cache::{compute_e, reborrow_cache, ConstCache},
15 FpResult,
16 },
17 repr::{Context, Repr, Word},
18 round::{mode, ErrorBounds, Round, Rounded},
19};
20use core::convert::TryFrom;
21use dashu_base::{AbsOrd, Approximation::Exact, RemEuclid, Sign};
22use dashu_int::IBig;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25enum Quadrant {
26 First,
27 Second,
28 Third,
29 Fourth,
30}
31
32fn signed_zero_normal<R: Round, const B: Word>(
35 ctx: &Context<R>,
36 x: &Repr<B>,
37) -> FpResult<FBig<R, B>> {
38 let zero = if x.is_neg_zero() {
39 Repr::neg_zero()
40 } else {
41 Repr::zero()
42 };
43 Ok(Exact(FBig::<R, B>::new(zero, *ctx)))
44}
45
46impl<R: ErrorBounds> Context<R> {
47 fn compute_work_context_trig<const B: Word>(
52 self,
53 x: &Repr<B>,
54 guard: usize,
55 ) -> Context<mode::HalfEven> {
56 let x_mag = (x.exponent.saturating_add(x.digits_ub() as isize)).max(0) as usize;
58 let extra_guards = guard + x_mag / 10;
59 let work_precision = self
60 .precision
61 .saturating_add(x_mag)
62 .saturating_add(extra_guards);
63 Context::<mode::HalfEven>::new(work_precision)
64 }
65
66 fn reduce_to_quadrant<const B: Word>(
71 self,
72 x: &Repr<B>,
73 guard: usize,
74 mut cache: Option<&mut ConstCache>,
75 ) -> (Context<mode::HalfEven>, Ball<B>, Quadrant) {
76 let work_context = self.compute_work_context_trig(x, guard);
77 let x_ball = Ball::from_rounded(
78 work_context
79 .repr_round(x.clone())
80 .map(|r| FBig::new(r, work_context)),
81 );
82 let x_f = x_ball.mid.clone();
84
85 let pi = work_context.pi::<B>(reborrow_cache(&mut cache)).value();
86 let half_pi = &pi / 2u8;
87 let half_pi_ball = Ball::with_error(half_pi.clone(), IBig::from(8));
90
91 let x_scaled = &x_f / &half_pi;
92 let k_f = x_scaled.round();
93 let k = IBig::try_from(k_f).expect("k_f is an exact integer or signed zero");
96
97 let r_ball = x_ball.sub(&half_pi_ball.scale_int(&k));
99
100 let k_mod_4_big = k.rem_euclid(IBig::from(4));
101 let Ok(k_mod_4_int) = i8::try_from(k_mod_4_big) else {
102 unreachable!("k % 4 is always in [0, 3]");
103 };
104 let quadrant = match k_mod_4_int {
105 0 => Quadrant::First,
106 1 => Quadrant::Second,
107 2 => Quadrant::Third,
108 3 => Quadrant::Fourth,
109 _ => unreachable!(),
110 };
111
112 (work_context, r_ball, quadrant)
113 }
114
115 pub fn sin<const B: Word>(
117 &self,
118 x: &Repr<B>,
119 mut cache: Option<&mut ConstCache>,
120 ) -> FpResult<FBig<R, B>> {
121 if x.is_infinite() {
122 return Err(FpError::InfiniteInput);
123 }
124 assert_limited_precision(self.precision);
125 if x.significand.is_zero() {
126 return signed_zero_normal(self, x);
128 }
129
130 self.ziv(50, |guard| {
134 let (work, r, quadrant) = self.reduce_to_quadrant(x, guard, reborrow_cache(&mut cache));
135 let val = match quadrant {
136 Quadrant::First => work.sin_compute(&r),
137 Quadrant::Second => work.cos_compute(&r),
138 Quadrant::Third => work.sin_compute(&r).neg(),
139 Quadrant::Fourth => work.cos_compute(&r).neg(),
140 };
141 Ok(val.to_value_radius::<R>())
142 })
143 }
144
145 fn sin_compute<const B: Word>(self, x: &Ball<B>) -> Ball<B> {
148 if x.mid.repr().significand.is_zero() {
149 return Ball::exact(x.mid.clone());
150 }
151 let x2 = x.mul(x);
152 let mut sum = x.clone();
153 let mut term = x.clone();
154 let mut k = 1usize;
155 let threshold = sum.mid.ulp_lb();
156 loop {
157 term = term.mul(&x2).div_int((2 * k) * (2 * k + 1));
158 if term.mid.abs_cmp(&threshold).is_le() {
159 break;
160 }
161 if k % 2 == 1 {
162 sum = sum.sub(&term);
163 } else {
164 sum = sum.add(&term);
165 }
166 k += 1;
167 }
168 sum.inflate(&IBig::from(2));
170 sum
171 }
172
173 pub fn cos<const B: Word>(
175 &self,
176 x: &Repr<B>,
177 mut cache: Option<&mut ConstCache>,
178 ) -> FpResult<FBig<R, B>> {
179 if x.is_infinite() {
180 return Err(FpError::InfiniteInput);
181 }
182 assert_limited_precision(self.precision);
183
184 if x.significand.is_zero() {
185 return Ok(FBig::<R, B>::ONE.with_precision(self.precision));
187 }
188
189 self.ziv(50, |guard| {
190 let (work, r, quadrant) = self.reduce_to_quadrant(x, guard, reborrow_cache(&mut cache));
191 let val = match quadrant {
192 Quadrant::First => work.cos_compute(&r),
193 Quadrant::Second => work.sin_compute(&r).neg(),
194 Quadrant::Third => work.cos_compute(&r).neg(),
195 Quadrant::Fourth => work.sin_compute(&r),
196 };
197 Ok(val.to_value_radius::<R>())
198 })
199 }
200
201 fn cos_compute<const B: Word>(self, x: &Ball<B>) -> Ball<B> {
204 if x.mid.repr().significand.is_zero() {
205 return Ball::exact_int(self.precision, IBig::ONE);
206 }
207 let x2 = x.mul(x);
208 let one = Ball::exact_int(self.precision, IBig::ONE);
209 let mut sum = one.clone();
210 let mut term = one.clone();
211 let mut k = 1usize;
212 let threshold = sum.mid.ulp_lb();
213 loop {
214 term = term.mul(&x2).div_int((2 * k) * (2 * k - 1));
215 if term.mid.abs_cmp(&threshold).is_le() {
216 break;
217 }
218 if k % 2 == 1 {
219 sum = sum.sub(&term);
220 } else {
221 sum = sum.add(&term);
222 }
223 k += 1;
224 }
225 sum.inflate(&IBig::from(2));
226 sum
227 }
228
229 pub fn sin_cos<const B: Word>(
233 &self,
234 x: &Repr<B>,
235 mut cache: Option<&mut ConstCache>,
236 ) -> (FpResult<FBig<R, B>>, FpResult<FBig<R, B>>) {
237 if x.is_infinite() {
238 return (Err(FpError::InfiniteInput), Err(FpError::InfiniteInput));
239 }
240 assert_limited_precision(self.precision);
241
242 if x.significand.is_zero() {
243 let s = signed_zero_normal(self, x);
245 let c = Ok(FBig::<R, B>::ONE.with_precision(self.precision));
246 return (s, c);
247 }
248
249 let (s, c) = self.ziv_pair(50, |guard| {
250 let (work, r, quadrant) = self.reduce_to_quadrant(x, guard, reborrow_cache(&mut cache));
251 let (sin_ball, cos_ball) = work.sin_cos_compute(&r);
252 let (s, c) = match quadrant {
253 Quadrant::First => (sin_ball, cos_ball),
254 Quadrant::Second => (cos_ball, sin_ball.neg()),
255 Quadrant::Third => (sin_ball.neg(), cos_ball.neg()),
256 Quadrant::Fourth => (cos_ball.neg(), sin_ball),
257 };
258 Ok((s.to_value_radius::<R>(), c.to_value_radius::<R>()))
259 });
260 (s, c)
261 }
262
263 pub(crate) fn sin_cos_compute<const B: Word>(self, x: &Ball<B>) -> (Ball<B>, Ball<B>) {
266 if x.mid.repr().significand.is_zero() {
267 return (Ball::exact(x.mid.clone()), Ball::exact_int(self.precision, IBig::ONE));
268 }
269 let x2 = x.mul(x);
270 let one = Ball::exact_int(self.precision, IBig::ONE);
271 let mut sin_sum = x.clone();
272 let mut cos_sum = one.clone();
273 let mut sin_term = x.clone();
274 let mut cos_term = one.clone();
275 let mut k = 1usize;
276 let sin_threshold = sin_sum.mid.ulp_lb();
277 let cos_threshold = cos_sum.mid.ulp_lb();
278 loop {
279 cos_term = cos_term.mul(&x2).div_int((2 * k) * (2 * k - 1));
280 sin_term = sin_term.mul(&x2).div_int((2 * k) * (2 * k + 1));
281
282 if sin_term.mid.abs_cmp(&sin_threshold).is_le()
283 && cos_term.mid.abs_cmp(&cos_threshold).is_le()
284 {
285 break;
286 }
287
288 if k % 2 == 1 {
289 cos_sum = cos_sum.sub(&cos_term);
290 sin_sum = sin_sum.sub(&sin_term);
291 } else {
292 cos_sum = cos_sum.add(&cos_term);
293 sin_sum = sin_sum.add(&sin_term);
294 }
295 k += 1;
296 }
297 sin_sum.inflate(&IBig::from(2));
298 cos_sum.inflate(&IBig::from(2));
299 (sin_sum, cos_sum)
300 }
301
302 pub fn tan<const B: Word>(
308 &self,
309 x: &Repr<B>,
310 mut cache: Option<&mut ConstCache>,
311 ) -> FpResult<FBig<R, B>> {
312 if x.is_infinite() {
313 return Err(FpError::InfiniteInput);
314 }
315 assert_limited_precision(self.precision);
316
317 if x.significand.is_zero() {
318 return signed_zero_normal(self, x);
320 }
321
322 self.ziv(50, |guard| {
327 let (work, r, quadrant) = self.reduce_to_quadrant(x, guard, reborrow_cache(&mut cache));
328 let (sin_ball, cos_ball) = work.sin_cos_compute(&r);
329 let (s, c) = match quadrant {
330 Quadrant::First => (sin_ball, cos_ball),
331 Quadrant::Second => (cos_ball, sin_ball.neg()),
332 Quadrant::Third => (sin_ball.neg(), cos_ball.neg()),
333 Quadrant::Fourth => (cos_ball.neg(), sin_ball),
334 };
335 if c.mid.repr().significand.is_zero() {
336 return Ok((FBig::<R, B>::ZERO, FBig::<R, B>::ONE));
339 }
340 Ok(s.div(&c).to_value_radius::<R>())
341 })
342 }
343
344 pub fn asin<const B: Word>(
350 &self,
351 x: &Repr<B>,
352 mut cache: Option<&mut ConstCache>,
353 ) -> FpResult<FBig<R, B>> {
354 if x.is_infinite() {
355 return Err(FpError::InfiniteInput);
356 }
357 assert_limited_precision(self.precision);
358 if x.significand.is_zero() {
359 return signed_zero_normal(self, x);
361 }
362
363 let x_orig = FBig::<R, B>::new(x.clone(), *self);
364 if x_orig.abs_cmp(&FBig::ONE).is_gt() {
366 return Err(FpError::OutOfDomain);
367 }
368
369 self.ziv(50, |guard| {
370 let work = Context::<mode::HalfEven>::new(self.precision + guard);
371 let x_ball = Ball::from_rounded(work.repr_round_ref(x).map(|r| FBig::new(r, work)));
372 Ok(work
373 .asin_ball::<B>(&x_ball, reborrow_cache(&mut cache))
374 .to_value_radius::<R>())
375 })
376 }
377
378 fn asin_ball<const B: Word>(&self, x: &Ball<B>, mut cache: Option<&mut ConstCache>) -> Ball<B> {
381 let one = Ball::exact_int(self.precision, IBig::ONE);
382 let d = one.sub(&x.mul(x)).sqrt();
383 if d.mid.repr().significand.is_zero() {
384 let pi = Context::<mode::HalfEven>::new(self.precision)
386 .pi::<B>(reborrow_cache(&mut cache))
387 .value();
388 let half_pi = Ball::with_error(pi / 2u8, IBig::from(8));
389 if x.mid.repr().sign() == Sign::Negative {
390 half_pi.neg()
391 } else {
392 half_pi
393 }
394 } else {
395 let arg = x.div(&d);
396 self.atan_ball::<B>(&arg, reborrow_cache(&mut cache))
397 }
398 }
399
400 pub fn acos<const B: Word>(
406 &self,
407 x: &Repr<B>,
408 mut cache: Option<&mut ConstCache>,
409 ) -> FpResult<FBig<R, B>> {
410 if x.is_infinite() {
411 return Err(FpError::InfiniteInput);
412 }
413 assert_limited_precision(self.precision);
414
415 let x_orig = FBig::<R, B>::new(x.clone(), *self);
416 let cmp_one = x_orig.abs_cmp(&FBig::ONE);
417 if cmp_one.is_gt() {
418 return Err(FpError::OutOfDomain);
419 }
420 if cmp_one.is_eq() {
421 return Ok(if x.sign() == Sign::Positive {
425 Exact(FBig::<R, B>::new(Repr::zero(), *self))
426 } else {
427 self.pi::<B>(reborrow_cache(&mut cache))
428 });
429 }
430
431 self.ziv(50, |guard| {
432 let work = Context::<mode::HalfEven>::new(self.precision + guard);
433 let x_ball = Ball::from_rounded(work.repr_round_ref(x).map(|r| FBig::new(r, work)));
434 let asin_ball = work.asin_ball::<B>(&x_ball, reborrow_cache(&mut cache));
435 let pi = work.pi::<B>(reborrow_cache(&mut cache)).value();
436 let half_pi = Ball::with_error(pi / 2u8, IBig::from(8));
437 Ok(half_pi.sub(&asin_ball).to_value_radius::<R>())
438 })
439 }
440
441 pub fn atan<const B: Word>(
443 &self,
444 x: &Repr<B>,
445 mut cache: Option<&mut ConstCache>,
446 ) -> FpResult<FBig<R, B>> {
447 if x.is_infinite() {
448 let pi = self.pi::<B>(reborrow_cache(&mut cache)).value();
450 let half_pi: FBig<R, B> = pi / 2;
451 let res: FBig<R, B> = if x.sign() == Sign::Positive {
452 half_pi
453 } else {
454 -half_pi
455 };
456 return Ok(res.with_precision(self.precision));
457 }
458
459 assert_limited_precision(self.precision);
460
461 if x.significand.is_zero() {
462 return signed_zero_normal(self, x);
464 }
465
466 self.ziv(50, |guard| {
467 let work = Context::<mode::HalfEven>::new(self.precision + guard);
468 let x_ball = Ball::from_rounded(work.repr_round_ref(x).map(|r| FBig::new(r, work)));
469 Ok(work
470 .atan_ball::<B>(&x_ball, reborrow_cache(&mut cache))
471 .to_value_radius::<R>())
472 })
473 }
474
475 fn atan_ball<const B: Word>(&self, x: &Ball<B>, mut cache: Option<&mut ConstCache>) -> Ball<B> {
478 let sign = x.mid.repr().sign();
479 let x_abs = if sign == Sign::Negative {
480 x.clone().neg()
481 } else {
482 x.clone()
483 };
484 let one = Ball::exact_int(self.precision, IBig::ONE);
485 let res = if x_abs.mid.abs_cmp(&one.mid).is_ge() {
486 let pi = Context::<mode::HalfEven>::new(self.precision)
487 .pi::<B>(reborrow_cache(&mut cache))
488 .value();
489 let half_pi = Ball::with_error(pi / 2u8, IBig::from(8));
490 let inv_x = one.div(&x_abs);
491 half_pi.sub(&self.atan_compute(&inv_x))
492 } else {
493 self.atan_compute(&x_abs)
494 };
495 if sign == Sign::Negative {
496 res.neg()
497 } else {
498 res
499 }
500 }
501
502 fn atan_compute<const B: Word>(self, x: &Ball<B>) -> Ball<B> {
505 let x2 = x.mul(x);
506 let one = Ball::exact_int(self.precision, IBig::ONE);
507 let one_plus_x2 = one.add(&x2);
508 let mut term = x.div(&one_plus_x2);
509 let mut sum = term.clone();
510 let factor = x2.scale_int(&IBig::from(2)).div(&one_plus_x2);
511 let mut n = 1usize;
512 let threshold = sum.mid.ulp_lb();
513 loop {
514 term = term
515 .mul(&factor)
516 .scale_int(&IBig::from(n))
517 .div_int(2 * n + 1);
518 if term.mid.abs_cmp(&threshold).is_le() {
519 break;
520 }
521 sum = sum.add(&term);
522 n += 1;
523 }
524 sum.inflate(&IBig::from(2));
526 sum
527 }
528
529 pub fn atan2<const B: Word>(
534 &self,
535 y: &Repr<B>,
536 x: &Repr<B>,
537 mut cache: Option<&mut ConstCache>,
538 ) -> FpResult<FBig<R, B>> {
539 if y.is_finite() && x.is_finite() && y.significand.is_zero() && x.significand.is_zero() {
540 return Err(FpError::OutOfDomain);
541 }
542
543 assert_limited_precision(self.precision);
544
545 if y.is_infinite() || x.is_infinite() {
547 let (sy, sx) = (y.sign() == Sign::Positive, x.sign() == Sign::Positive);
548 let pi_val = self.pi::<B>(reborrow_cache(&mut cache)).value();
549 let res: FBig<R, B> = match (y.is_infinite(), x.is_infinite(), sy, sx) {
550 (true, true, true, true) => pi_val.clone() / 4u8,
551 (true, true, true, false) => pi_val.clone() * 3u8 / 4u8,
552 (true, true, false, true) => -(pi_val.clone() / 4u8),
553 (true, true, false, false) => -(pi_val.clone() * 3u8 / 4u8),
554 (true, false, true, _) => pi_val.clone() / 2u8,
555 (true, false, false, _) => -(pi_val.clone() / 2u8),
556 (false, true, _, true) => {
557 if sy {
559 FBig::<R, B>::ZERO
560 } else {
561 FBig::<R, B>::new(Repr::neg_zero(), *self)
562 }
563 }
564 (false, true, true, false) => pi_val.clone(),
565 (false, true, false, false) => -pi_val,
566 _ => unreachable!(),
567 };
568 return Ok(res.with_precision(self.precision));
569 }
570
571 if x.significand.is_zero() {
573 let half_pi = self.pi::<B>(reborrow_cache(&mut cache)).value() / 2u8;
574 let res = if y.sign() == Sign::Positive {
575 half_pi
576 } else {
577 -half_pi
578 };
579 return Ok(res.with_precision(self.precision));
580 }
581
582 self.ziv(50, |guard| {
584 let work = Context::<mode::HalfEven>::new(self.precision + guard);
585 let y_ball = Ball::from_rounded(work.repr_round_ref(y).map(|r| FBig::new(r, work)));
586 let x_ball = Ball::from_rounded(work.repr_round_ref(x).map(|r| FBig::new(r, work)));
587 let ratio = y_ball.div(&x_ball);
588 let atan_val = work.atan_ball::<B>(&ratio, reborrow_cache(&mut cache));
589 let res = if x.sign() == Sign::Positive {
590 atan_val
591 } else {
592 let pi = work.pi::<B>(reborrow_cache(&mut cache)).value();
593 let pi_ball = Ball::with_error(pi, IBig::from(8));
594 if y.sign() == Sign::Positive {
595 atan_val.add(&pi_ball)
596 } else {
597 atan_val.sub(&pi_ball)
598 }
599 };
600 Ok(res.to_value_radius::<R>())
601 })
602 }
603}
604
605impl<R: ErrorBounds, const B: Word> FBig<R, B> {
606 #[inline]
611 pub fn sin(&self) -> Self {
612 self.context.unwrap_fp(self.context.sin(&self.repr, None))
613 }
614
615 #[inline]
620 pub fn cos(&self) -> Self {
621 self.context.unwrap_fp(self.context.cos(&self.repr, None))
622 }
623
624 #[inline]
631 pub fn sin_cos(&self) -> (Self, Self) {
632 let (s, c) = self.context.sin_cos(&self.repr, None);
633 (self.context.unwrap_fp(s), self.context.unwrap_fp(c))
634 }
635
636 #[inline]
643 pub fn tan(&self) -> Self {
644 self.context.unwrap_fp(self.context.tan(&self.repr, None))
645 }
646
647 #[inline]
652 pub fn asin(&self) -> Self {
653 self.context.unwrap_fp(self.context.asin(&self.repr, None))
654 }
655
656 #[inline]
661 pub fn acos(&self) -> Self {
662 self.context.unwrap_fp(self.context.acos(&self.repr, None))
663 }
664
665 #[inline]
667 pub fn atan(&self) -> Self {
668 self.context.unwrap_fp(self.context.atan(&self.repr, None))
669 }
670
671 #[inline]
676 pub fn atan2(&self, x: &Self) -> Self {
677 self.context
678 .unwrap_fp(self.context.atan2(&self.repr, &x.repr, None))
679 }
680}
681
682impl<R: Round> Context<R> {
683 #[must_use]
695 pub fn pi<const B: Word>(&self, cache: Option<&mut ConstCache>) -> Rounded<FBig<R, B>> {
696 if let Some(c) = cache {
697 return c.pi::<B, R>(self.precision);
698 }
699
700 let mut fresh = ConstCache::new();
704 fresh.pi::<B, R>(self.precision)
705 }
706
707 #[must_use]
719 pub fn e<const B: Word>(&self) -> Rounded<FBig<R, B>> {
720 compute_e::<B, R>(self.precision)
721 }
722}
723
724impl<R: Round, const B: Word> FBig<R, B> {
725 #[inline]
727 #[must_use]
728 pub fn pi(precision: usize) -> Self {
729 Context::<R>::new(precision).pi(None).value()
730 }
731
732 #[inline]
735 #[must_use]
736 pub fn e(precision: usize) -> Self {
737 Context::<R>::new(precision).e::<B>().value()
738 }
739}
740
741#[cfg(test)]
742mod tests {
743 use super::*;
744 use crate::round::mode;
745 use crate::DBig;
746 use core::str::FromStr;
747
748 #[test]
749 fn test_atan_infinity_is_preserved() {
750 let ctx = Context::<mode::HalfEven>::new(53);
751 let r = ctx.atan::<2>(&Repr::<2>::infinity(), None).unwrap().value();
753 assert!(r.repr().sign() == Sign::Positive);
754 assert!(r > FBig::<mode::HalfEven>::ONE);
756 }
757
758 #[test]
762 fn test_trig_tiny_negative_no_panic() {
763 let ctx = Context::<mode::HalfAway>::new(30);
764 for &e in &[-1isize, -2, -10, -30] {
765 let x = Repr::<10>::new(IBig::from(-1), e);
767 let s = ctx.sin::<10>(&x, None).unwrap().value();
768 let c = ctx.cos::<10>(&x, None).unwrap().value();
769 let (ss, cc) = ctx.sin_cos::<10>(&x, None);
770 let ss = ss.unwrap().value();
771 let cc = cc.unwrap().value();
772 assert_eq!(s.sign(), Sign::Negative);
774 assert_eq!(c.sign(), Sign::Positive);
775 assert_eq!(ss.sign(), Sign::Negative);
776 assert_eq!(cc.sign(), Sign::Positive);
777 }
778 }
779
780 #[test]
784 fn test_sin_many_digit_rounding_no_panic() {
785 let x = DBig::from_str("-5.525474318981006776603409487767135633516667011547942409467e-3")
786 .unwrap();
787 let ctx = Context::<mode::HalfEven>::new(100);
788 let s = ctx.sin::<10>(x.repr(), None).unwrap().value();
789 assert_eq!(s.sign(), Sign::Negative);
791 }
792
793 #[test]
798 fn test_tan_near_pole_signs_and_no_panic() {
799 let p = 53usize;
800 let ctx = Context::<mode::HalfEven>::new(p);
801 let half_pi = FBig::<mode::HalfEven>::pi(p) / 2u8;
802 let eps = FBig::<mode::HalfEven>::ONE >> 10;
804 let below = ctx
805 .tan::<2>((half_pi.clone() - &eps).repr(), None)
806 .unwrap()
807 .value();
808 let above = ctx
809 .tan::<2>((half_pi.clone() + &eps).repr(), None)
810 .unwrap()
811 .value();
812 assert_eq!(below.sign(), Sign::Positive, "tan just below π/2 is large positive");
813 assert_eq!(above.sign(), Sign::Negative, "tan just above π/2 is large negative");
814 let pi = FBig::<mode::HalfEven>::pi(p);
816 let q = ctx.tan::<2>((pi / 4u8).repr(), None).unwrap().value();
817 assert!(
818 (q.clone() - FBig::ONE).abs_cmp(&(FBig::ONE >> 40)).is_le(),
819 "tan(π/4) ≈ 1, got {q:?}"
820 );
821 }
822}