1use crate::{
10 error::{assert_limited_precision, FpError},
11 fbig::FBig,
12 math::{
13 cache::{compute_e, reborrow_cache, ConstCache},
14 FpResult,
15 },
16 repr::{Context, Repr, Word},
17 round::{ErrorBounds, Round, Rounded},
18};
19use core::convert::TryFrom;
20use dashu_base::{Abs, AbsOrd, Approximation::Exact, RemEuclid, Sign, UnsignedAbs};
21use dashu_int::IBig;
22
23pub(crate) type Rad<R, const B: Word> = (FBig<R, B>, FBig<R, B>);
25
26pub(crate) fn series_radius<R: Round, const B: Word>(
32 value: &FBig<R, B>,
33 terms: usize,
34) -> FBig<R, B> {
35 value.ulp() * (4 * terms + 12)
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39enum Quadrant {
40 First,
41 Second,
42 Third,
43 Fourth,
44}
45
46fn signed_zero_normal<R: Round, const B: Word>(
49 ctx: &Context<R>,
50 x: &Repr<B>,
51) -> FpResult<FBig<R, B>> {
52 let zero = if x.is_neg_zero() {
53 Repr::neg_zero()
54 } else {
55 Repr::zero()
56 };
57 Ok(Exact(FBig::<R, B>::new(zero, *ctx)))
58}
59
60impl<R: ErrorBounds> Context<R> {
61 fn compute_work_context_trig<const B: Word>(self, x: &Repr<B>, guard: usize) -> Self {
65 let x_mag = (x.exponent.saturating_add(x.digits_ub() as isize)).max(0) as usize;
67 let extra_guards = guard + x_mag / 10;
68 let work_precision = self
69 .precision
70 .saturating_add(x_mag)
71 .saturating_add(extra_guards);
72 Self::new(work_precision)
73 }
74
75 fn reduce_to_quadrant<const B: Word>(
80 self,
81 x: &Repr<B>,
82 guard: usize,
83 mut cache: Option<&mut ConstCache>,
84 ) -> (Self, FBig<R, B>, Quadrant, FBig<R, B>) {
85 let work_context = self.compute_work_context_trig(x, guard);
86 let x_f = FBig::<R, B>::new(work_context.repr_round(x.clone()).value(), work_context);
87
88 let pi = work_context.pi::<B>(reborrow_cache(&mut cache)).value();
89 let half_pi = &pi / 2u8;
90 let x_scaled: FBig<R, B> = &x_f / &half_pi;
91 let k_f = x_scaled.round();
92 let r = k_f.fma(&half_pi, &x_f, Sign::Negative);
99 let k = IBig::try_from(k_f).expect("k_f is an exact integer or signed zero");
102
103 let half_pi_ulp = half_pi.ulp();
108 let r_ulp = r.ulp();
109 let k_abs = k.clone().unsigned_abs();
110 let reduction_err = half_pi_ulp * k_abs + r_ulp * 4;
111
112 let k_mod_4_big = k.rem_euclid(IBig::from(4));
113 let Ok(k_mod_4_int) = i8::try_from(k_mod_4_big) else {
114 unreachable!("k % 4 is always in [0, 3]");
115 };
116 let quadrant = match k_mod_4_int {
117 0 => Quadrant::First,
118 1 => Quadrant::Second,
119 2 => Quadrant::Third,
120 3 => Quadrant::Fourth,
121 _ => unreachable!(),
122 };
123
124 (work_context, r, quadrant, reduction_err)
125 }
126
127 pub fn sin<const B: Word>(
129 &self,
130 x: &Repr<B>,
131 mut cache: Option<&mut ConstCache>,
132 ) -> FpResult<FBig<R, B>> {
133 if x.is_infinite() {
134 return Err(FpError::InfiniteInput);
135 }
136 assert_limited_precision(self.precision);
137 if x.significand.is_zero() {
138 return signed_zero_normal(self, x);
140 }
141
142 Ok(self.ziv(50, |guard| {
146 let (work, r, quadrant, reduction_err) =
147 self.reduce_to_quadrant(x, guard, reborrow_cache(&mut cache));
148 let (val, series_radius) = match quadrant {
149 Quadrant::First => work.sin_compute(&r),
150 Quadrant::Second => work.cos_compute(&r),
151 Quadrant::Third => {
152 let (v, e) = work.sin_compute(&r);
153 (-v, e)
154 }
155 Quadrant::Fourth => {
156 let (v, e) = work.cos_compute(&r);
157 (-v, e)
158 }
159 };
160 (val, series_radius + reduction_err)
161 }))
162 }
163
164 fn sin_compute<const B: Word>(self, x: &FBig<R, B>) -> (FBig<R, B>, FBig<R, B>) {
168 if x.repr.significand.is_zero() {
169 return (FBig::ZERO, FBig::ZERO);
170 }
171 let x2 = x.sqr();
172 let mut sum = x.clone();
173 let mut term = x.clone();
174 let mut k = 1usize;
175 let threshold = sum.ulp_lb();
176 loop {
177 term *= &x2;
178 term /= (2 * k) * (2 * k + 1);
179 if term.abs_cmp(&threshold).is_le() {
180 break;
181 }
182 if k % 2 == 1 {
183 sum -= &term;
184 } else {
185 sum += &term;
186 }
187 k += 1;
188 }
189 let radius = series_radius(&sum, k);
190 (sum, radius)
191 }
192
193 pub fn cos<const B: Word>(
195 &self,
196 x: &Repr<B>,
197 mut cache: Option<&mut ConstCache>,
198 ) -> FpResult<FBig<R, B>> {
199 if x.is_infinite() {
200 return Err(FpError::InfiniteInput);
201 }
202 assert_limited_precision(self.precision);
203
204 if x.significand.is_zero() {
205 return Ok(FBig::<R, B>::ONE.with_precision(self.precision));
207 }
208
209 Ok(self.ziv(50, |guard| {
210 let (work, r, quadrant, reduction_err) =
211 self.reduce_to_quadrant(x, guard, reborrow_cache(&mut cache));
212 let (val, series_radius) = match quadrant {
213 Quadrant::First => work.cos_compute(&r),
214 Quadrant::Second => {
215 let (v, e) = work.sin_compute(&r);
216 (-v, e)
217 }
218 Quadrant::Third => {
219 let (v, e) = work.cos_compute(&r);
220 (-v, e)
221 }
222 Quadrant::Fourth => work.sin_compute(&r),
223 };
224 (val, series_radius + reduction_err)
225 }))
226 }
227
228 fn cos_compute<const B: Word>(self, x: &FBig<R, B>) -> (FBig<R, B>, FBig<R, B>) {
231 if x.repr.significand.is_zero() {
232 return (FBig::ONE.with_precision(self.precision).value(), FBig::ZERO);
233 }
234 let x2 = x.sqr();
235 let mut sum = FBig::<R, B>::ONE.with_precision(self.precision).value();
236 let mut term = sum.clone();
237 let mut k = 1usize;
238 let threshold = sum.ulp_lb();
239 loop {
240 term *= &x2;
241 term /= (2 * k) * (2 * k - 1);
242 if term.abs_cmp(&threshold).is_le() {
243 break;
244 }
245 if k % 2 == 1 {
246 sum -= &term;
247 } else {
248 sum += &term;
249 }
250 k += 1;
251 }
252 let radius = series_radius(&sum, k);
253 (sum, radius)
254 }
255
256 pub fn sin_cos<const B: Word>(
260 &self,
261 x: &Repr<B>,
262 mut cache: Option<&mut ConstCache>,
263 ) -> (FpResult<FBig<R, B>>, FpResult<FBig<R, B>>) {
264 if x.is_infinite() {
265 return (Err(FpError::InfiniteInput), Err(FpError::InfiniteInput));
266 }
267 assert_limited_precision(self.precision);
268
269 if x.significand.is_zero() {
270 let s = signed_zero_normal(self, x);
272 let c = Ok(FBig::<R, B>::ONE.with_precision(self.precision));
273 return (s, c);
274 }
275
276 let (s, c) = self.ziv_pair(50, |guard| {
277 let (work, r, quadrant, reduction_err) =
278 self.reduce_to_quadrant(x, guard, reborrow_cache(&mut cache));
279 let ((sin_r, sin_e), (cos_r, cos_e)) = work.sin_cos_compute(&r);
280 let (s, c) = match quadrant {
281 Quadrant::First => (sin_r, cos_r),
282 Quadrant::Second => (cos_r, -sin_r),
283 Quadrant::Third => (-sin_r, -cos_r),
284 Quadrant::Fourth => (-cos_r, sin_r),
285 };
286 ((s, sin_e + reduction_err.clone()), (c, cos_e + reduction_err))
287 });
288 (Ok(s), Ok(c))
289 }
290
291 pub(crate) fn sin_cos_compute<const B: Word>(self, x: &FBig<R, B>) -> (Rad<R, B>, Rad<R, B>) {
293 if x.repr.significand.is_zero() {
294 return (
295 (FBig::ZERO, FBig::ZERO),
296 (FBig::ONE.with_precision(self.precision).value(), FBig::ZERO),
297 );
298 }
299 let x2 = x.sqr();
300 let mut sin_sum = x.clone();
301 let mut cos_sum = FBig::<R, B>::ONE.with_precision(self.precision).value();
302 let mut sin_term = x.clone();
303 let mut cos_term = cos_sum.clone();
304 let mut k = 1usize;
305 let sin_threshold = sin_sum.ulp_lb();
306 let cos_threshold = cos_sum.ulp_lb();
307 loop {
308 cos_term *= &x2;
309 cos_term /= (2 * k) * (2 * k - 1);
310 sin_term *= &x2;
311 sin_term /= (2 * k) * (2 * k + 1);
312
313 if sin_term.abs_cmp(&sin_threshold).is_le() && cos_term.abs_cmp(&cos_threshold).is_le()
314 {
315 break;
316 }
317
318 if k % 2 == 1 {
319 cos_sum -= &cos_term;
320 sin_sum -= &sin_term;
321 } else {
322 cos_sum += &cos_term;
323 sin_sum += &sin_term;
324 }
325 k += 1;
326 }
327 (
328 (sin_sum.clone(), series_radius(&sin_sum, k)),
329 (cos_sum.clone(), series_radius(&cos_sum, k)),
330 )
331 }
332
333 pub fn tan<const B: Word>(
339 &self,
340 x: &Repr<B>,
341 mut cache: Option<&mut ConstCache>,
342 ) -> FpResult<FBig<R, B>> {
343 if x.is_infinite() {
344 return Err(FpError::InfiniteInput);
345 }
346 assert_limited_precision(self.precision);
347
348 if x.significand.is_zero() {
349 return signed_zero_normal(self, x);
351 }
352
353 Ok(self.ziv(50, |guard| {
361 let (work, r, quadrant, reduction_err) =
362 self.reduce_to_quadrant(x, guard, reborrow_cache(&mut cache));
363 let ((sin_r, sin_e), (cos_r, cos_e)) = work.sin_cos_compute(&r);
364 let (s, c) = match quadrant {
365 Quadrant::First => (sin_r, cos_r),
366 Quadrant::Second => (cos_r, -sin_r),
367 Quadrant::Third => (-sin_r, -cos_r),
368 Quadrant::Fourth => (-cos_r, sin_r),
369 };
370 if c.repr.significand.is_zero() {
371 return (FBig::ZERO, FBig::ONE);
375 }
376 let result = work.div(&s.repr, &c.repr).unwrap().value();
377 let e_s = sin_e + reduction_err.clone();
381 let e_c = cos_e + reduction_err;
382 let radius = (e_s + result.clone().abs() * e_c) / c.clone().abs() + result.ulp() * 8;
383 (result, radius)
384 }))
385 }
386
387 pub fn asin<const B: Word>(
393 &self,
394 x: &Repr<B>,
395 mut cache: Option<&mut ConstCache>,
396 ) -> FpResult<FBig<R, B>> {
397 if x.is_infinite() {
398 return Err(FpError::InfiniteInput);
399 }
400 assert_limited_precision(self.precision);
401 if x.significand.is_zero() {
402 return signed_zero_normal(self, x);
406 }
407
408 let x_orig = FBig::<R, B>::new(x.clone(), *self);
409 if x_orig.abs_cmp(&FBig::ONE).is_gt() {
411 return Err(FpError::OutOfDomain);
412 }
413
414 Ok(self.ziv(50, |guard| {
415 let work = Context::<R>::new(self.precision + guard);
416 let x_f = FBig::<R, B>::new(work.repr_round_ref(x).value(), work);
417 let one = FBig::<R, B>::ONE.with_precision(work.precision).value();
418 let d = work
419 .sqrt(&(one.clone() - x_f.clone().sqr()).repr)
420 .unwrap()
421 .value();
422 if d.repr.is_pos_zero() || d.repr.is_neg_zero() {
423 let pi = work.pi::<B>(reborrow_cache(&mut cache)).value();
425 let half_pi = pi / 2u8;
426 let res = if x_f.sign() == Sign::Positive {
427 half_pi
428 } else {
429 -half_pi
430 };
431 let radius = res.ulp() * 4;
432 return (res, radius);
433 }
434 let arg = &x_f / &d;
438 let res = work
439 .atan(&arg.repr, reborrow_cache(&mut cache))
440 .unwrap()
441 .value();
442 let radius = res.ulp() * 16;
443 (res, radius)
444 }))
445 }
446
447 pub fn acos<const B: Word>(
453 &self,
454 x: &Repr<B>,
455 mut cache: Option<&mut ConstCache>,
456 ) -> FpResult<FBig<R, B>> {
457 if x.is_infinite() {
458 return Err(FpError::InfiniteInput);
459 }
460 assert_limited_precision(self.precision);
461
462 let x_orig = FBig::<R, B>::new(x.clone(), *self);
463 let cmp_one = x_orig.abs_cmp(&FBig::ONE);
464 if cmp_one.is_gt() {
465 return Err(FpError::OutOfDomain);
466 }
467 if cmp_one.is_eq() {
468 return Ok(if x.sign() == Sign::Positive {
473 Exact(FBig::<R, B>::new(Repr::zero(), *self))
474 } else {
475 self.pi::<B>(reborrow_cache(&mut cache))
476 });
477 }
478
479 Ok(self.ziv(50, |guard| {
480 let work = Context::<R>::new(self.precision + guard);
481 let asin_x = work.asin(x, reborrow_cache(&mut cache)).unwrap().value();
485 let pi = work.pi::<B>(reborrow_cache(&mut cache)).value();
486 let res = (pi / 2u8) - &asin_x;
487 let radius = asin_x.ulp().clone().with_precision(0).value() * 2
488 + res.ulp().clone().with_precision(0).value() * 4;
489 (res, radius)
490 }))
491 }
492
493 pub fn atan<const B: Word>(
495 &self,
496 x: &Repr<B>,
497 mut cache: Option<&mut ConstCache>,
498 ) -> FpResult<FBig<R, B>> {
499 if x.is_infinite() {
500 let pi = self.pi::<B>(reborrow_cache(&mut cache)).value();
502 let half_pi: FBig<R, B> = pi / 2;
503 let res: FBig<R, B> = if x.sign() == Sign::Positive {
504 half_pi
505 } else {
506 -half_pi
507 };
508 return Ok(res.with_precision(self.precision));
509 }
510
511 assert_limited_precision(self.precision);
512
513 if x.significand.is_zero() {
514 return signed_zero_normal(self, x);
516 }
517
518 Ok(self.ziv(50, |guard| {
519 let work = Context::<R>::new(self.precision + guard);
520 let x_f = FBig::<R, B>::new(work.repr_round_ref(x).value(), work);
521 let sign = x_f.sign();
522 let x_abs = x_f.abs();
523 let one = FBig::<R, B>::ONE.with_precision(work.precision).value();
524 let (res, radius) = if x_abs >= one {
525 let pi = work.pi::<B>(reborrow_cache(&mut cache)).value();
527 let inv_x = &one / &x_abs;
528 let (atan_val, atan_radius) = work.atan_compute(&inv_x);
529 let res = (pi / 2u8) - atan_val;
530 let radius = atan_radius + res.ulp() * 4;
531 (res, radius)
532 } else {
533 work.atan_compute(&x_abs)
534 };
535 let res = if sign == Sign::Negative { -res } else { res };
536 (res, radius)
537 }))
538 }
539
540 fn atan_compute<const B: Word>(self, x: &FBig<R, B>) -> (FBig<R, B>, FBig<R, B>) {
543 let x2 = x.sqr();
545 let one_plus_x2 = FBig::ONE + &x2;
546 let mut term = x / &one_plus_x2;
547 let mut sum = term.clone();
548 let factor = (2 * &x2) / one_plus_x2;
549 let mut n = 1usize;
550 let threshold = sum.ulp_lb();
551 loop {
552 term *= &factor;
553 term *= n;
554 term /= 2 * n + 1;
555 if term.abs_cmp(&threshold).is_le() {
556 break;
557 }
558 sum += &term;
559 n += 1;
560 }
561 let radius = series_radius(&sum, n);
562 (sum, radius)
563 }
564
565 pub fn atan2<const B: Word>(
570 &self,
571 y: &Repr<B>,
572 x: &Repr<B>,
573 mut cache: Option<&mut ConstCache>,
574 ) -> FpResult<FBig<R, B>> {
575 if y.is_finite() && x.is_finite() && y.significand.is_zero() && x.significand.is_zero() {
576 return Err(FpError::OutOfDomain);
577 }
578
579 assert_limited_precision(self.precision);
580
581 if y.is_infinite() || x.is_infinite() {
583 let (sy, sx) = (y.sign() == Sign::Positive, x.sign() == Sign::Positive);
584 let pi_val = self.pi::<B>(reborrow_cache(&mut cache)).value();
585 let res: FBig<R, B> = match (y.is_infinite(), x.is_infinite(), sy, sx) {
586 (true, true, true, true) => pi_val.clone() / 4u8,
587 (true, true, true, false) => pi_val.clone() * 3u8 / 4u8,
588 (true, true, false, true) => -(pi_val.clone() / 4u8),
589 (true, true, false, false) => -(pi_val.clone() * 3u8 / 4u8),
590 (true, false, true, _) => pi_val.clone() / 2u8,
591 (true, false, false, _) => -(pi_val.clone() / 2u8),
592 (false, true, _, true) => {
593 if sy {
595 FBig::<R, B>::ZERO
596 } else {
597 FBig::<R, B>::new(Repr::neg_zero(), *self)
598 }
599 }
600 (false, true, true, false) => pi_val.clone(),
601 (false, true, false, false) => -pi_val,
602 _ => unreachable!(),
603 };
604 return Ok(res.with_precision(self.precision));
605 }
606
607 if x.significand.is_zero() {
609 let half_pi = self.pi::<B>(reborrow_cache(&mut cache)).value() / 2u8;
610 let res = if y.sign() == Sign::Positive {
611 half_pi
612 } else {
613 -half_pi
614 };
615 return Ok(res.with_precision(self.precision));
616 }
617
618 Ok(self.ziv(50, |guard| {
621 let work = Context::<R>::new(self.precision + guard);
622 let y_f = FBig::<R, B>::new(work.repr_round_ref(y).value(), work);
623 let x_f = FBig::<R, B>::new(work.repr_round_ref(x).value(), work);
624 let ratio = &y_f / &x_f;
625 let atan_val = work
626 .atan(&ratio.repr, reborrow_cache(&mut cache))
627 .unwrap()
628 .value();
629 let (res, radius) = if x.sign() == Sign::Positive {
630 (atan_val.clone(), atan_val.ulp() * 6)
631 } else {
632 let pi = work.pi::<B>(reborrow_cache(&mut cache)).value();
633 let r = if y_f.sign() == Sign::Positive {
634 &atan_val + &pi
635 } else {
636 &atan_val - &pi
637 };
638 let radius = atan_val.ulp() * 2 + r.ulp() * 6;
639 (r, radius)
640 };
641 (res, radius)
642 }))
643 }
644}
645
646impl<R: ErrorBounds, const B: Word> FBig<R, B> {
647 #[inline]
652 pub fn sin(&self) -> Self {
653 self.context.unwrap_fp(self.context.sin(&self.repr, None))
654 }
655
656 #[inline]
661 pub fn cos(&self) -> Self {
662 self.context.unwrap_fp(self.context.cos(&self.repr, None))
663 }
664
665 #[inline]
672 pub fn sin_cos(&self) -> (Self, Self) {
673 let (s, c) = self.context.sin_cos(&self.repr, None);
674 (self.context.unwrap_fp(s), self.context.unwrap_fp(c))
675 }
676
677 #[inline]
684 pub fn tan(&self) -> Self {
685 self.context.unwrap_fp(self.context.tan(&self.repr, None))
686 }
687
688 #[inline]
693 pub fn asin(&self) -> Self {
694 self.context.unwrap_fp(self.context.asin(&self.repr, None))
695 }
696
697 #[inline]
702 pub fn acos(&self) -> Self {
703 self.context.unwrap_fp(self.context.acos(&self.repr, None))
704 }
705
706 #[inline]
708 pub fn atan(&self) -> Self {
709 self.context.unwrap_fp(self.context.atan(&self.repr, None))
710 }
711
712 #[inline]
717 pub fn atan2(&self, x: &Self) -> Self {
718 self.context
719 .unwrap_fp(self.context.atan2(&self.repr, &x.repr, None))
720 }
721}
722
723impl<R: Round> Context<R> {
724 #[must_use]
736 pub fn pi<const B: Word>(&self, cache: Option<&mut ConstCache>) -> Rounded<FBig<R, B>> {
737 if let Some(c) = cache {
738 return c.pi::<B, R>(self.precision);
739 }
740
741 let mut fresh = ConstCache::new();
745 fresh.pi::<B, R>(self.precision)
746 }
747
748 #[must_use]
760 pub fn e<const B: Word>(&self) -> Rounded<FBig<R, B>> {
761 compute_e::<B, R>(self.precision)
762 }
763}
764
765impl<R: Round, const B: Word> FBig<R, B> {
766 #[inline]
768 #[must_use]
769 pub fn pi(precision: usize) -> Self {
770 Context::<R>::new(precision).pi(None).value()
771 }
772
773 #[inline]
776 #[must_use]
777 pub fn e(precision: usize) -> Self {
778 Context::<R>::new(precision).e::<B>().value()
779 }
780}
781
782#[cfg(test)]
783mod tests {
784 use super::*;
785 use crate::round::mode;
786 use crate::DBig;
787 use core::str::FromStr;
788
789 #[test]
790 fn test_atan_infinity_is_preserved() {
791 let ctx = Context::<mode::HalfEven>::new(53);
792 let r = ctx.atan::<2>(&Repr::<2>::infinity(), None).unwrap().value();
794 assert!(r.repr().sign() == Sign::Positive);
795 assert!(r > FBig::<mode::HalfEven>::ONE);
797 }
798
799 #[test]
803 fn test_trig_tiny_negative_no_panic() {
804 let ctx = Context::<mode::HalfAway>::new(30);
805 for &e in &[-1isize, -2, -10, -30] {
806 let x = Repr::<10>::new(IBig::from(-1), e);
808 let s = ctx.sin::<10>(&x, None).unwrap().value();
809 let c = ctx.cos::<10>(&x, None).unwrap().value();
810 let (ss, cc) = ctx.sin_cos::<10>(&x, None);
811 let ss = ss.unwrap().value();
812 let cc = cc.unwrap().value();
813 assert_eq!(s.sign(), Sign::Negative);
815 assert_eq!(c.sign(), Sign::Positive);
816 assert_eq!(ss.sign(), Sign::Negative);
817 assert_eq!(cc.sign(), Sign::Positive);
818 }
819 }
820
821 #[test]
825 fn test_sin_many_digit_rounding_no_panic() {
826 let x = DBig::from_str("-5.525474318981006776603409487767135633516667011547942409467e-3")
827 .unwrap();
828 let ctx = Context::<mode::HalfEven>::new(100);
829 let s = ctx.sin::<10>(x.repr(), None).unwrap().value();
830 assert_eq!(s.sign(), Sign::Negative);
832 }
833
834 #[test]
839 fn test_tan_near_pole_signs_and_no_panic() {
840 let p = 53usize;
841 let ctx = Context::<mode::HalfEven>::new(p);
842 let half_pi = FBig::<mode::HalfEven>::pi(p) / 2u8;
843 let eps = FBig::<mode::HalfEven>::ONE >> 10;
845 let below = ctx
846 .tan::<2>((half_pi.clone() - &eps).repr(), None)
847 .unwrap()
848 .value();
849 let above = ctx
850 .tan::<2>((half_pi.clone() + &eps).repr(), None)
851 .unwrap()
852 .value();
853 assert_eq!(below.sign(), Sign::Positive, "tan just below π/2 is large positive");
854 assert_eq!(above.sign(), Sign::Negative, "tan just above π/2 is large negative");
855 let pi = FBig::<mode::HalfEven>::pi(p);
857 let q = ctx.tan::<2>((pi / 4u8).repr(), None).unwrap().value();
858 assert!(
859 (q.clone() - FBig::ONE).abs_cmp(&(FBig::ONE >> 40)).is_le(),
860 "tan(π/4) ≈ 1, got {q:?}"
861 );
862 }
863}