Skip to main content

decimal_scaled/
trig_strict.rs

1//! Trigonometric, hyperbolic, and angle-conversion methods for [`D38`].
2//!
3//! # Methods
4//!
5//! Fifteen methods:
6//!
7//! - **Forward trig (radians input):** [`D38::sin`] / [`D38::cos`] /
8//! [`D38::tan`].
9//! - **Inverse trig (returns radians):** [`D38::asin`] / [`D38::acos`]
10//! / [`D38::atan`] / [`D38::atan2`].
11//! - **Hyperbolic:** [`D38::sinh`] / [`D38::cosh`] / [`D38::tanh`] /
12//! [`D38::asinh`] / [`D38::acosh`] / [`D38::atanh`].
13//! - **Angle conversions:** [`D38::to_degrees`] / [`D38::to_radians`].
14//!
15//! # The `*_strict` dual API
16//!
17//! Each method has two implementations:
18//!
19//! - An integer-only `<method>_strict` form — always compiled (unless
20//! the `fast` feature is set), `no_std`-compatible, and
21//! platform-deterministic. `sin`/`cos`/`tan` range-reduce and
22//! evaluate a Taylor series; `atan`/`asin`/`acos`/`atan2` derive from
23//! a reciprocal-reduced Taylor `atan`; the hyperbolic family composes
24//! the strict `exp` / `ln` / `sqrt`.
25//! - An f64-bridge form — converts to `f64`, calls the platform
26//! intrinsic, converts back. Gated on `std`.
27//!
28//! The plain `<method>` is a dispatcher: with the `strict` feature it
29//! calls `<method>_strict`; otherwise it is the f64 bridge. See
30//! `docs/strict-mode.md` for the full dual-API and feature-gating
31//! rules and the 0.5 ULP accuracy contract.
32//!
33//! # Precision
34//!
35//! The f64-bridge forms are **Lossy** — the `D38` value round-trips
36//! through `f64`, which introduces up to one LSB of quantisation per
37//! conversion. The `*_strict` forms are **correctly rounded**: within
38//! 0.5 ULP of the exact result (IEEE-754 round-to-nearest). They
39//! evaluate every reduction and series step in the `d_w128_kernels::Fixed`
40//! guard-digit intermediate and round once at the end.
41//!
42//! # `atan2` signature
43//!
44//! `f64::atan2(self, other)` treats `self` as `y` and `other` as `x`.
45//! This module matches that signature exactly so generic numeric code
46//! calling `y.atan2(x)` works with `T = D38`.
47
48use crate::core_type::D38;
49
50impl<const SCALE: u32> D38<SCALE> {
51#[cfg(all(feature = "strict", not(feature = "fast")))]
52    /// With `strict` this dispatches to [`Self::sin_strict`]; without
53    /// it, the f64-bridge form is used instead.
54    #[inline]
55    #[must_use]
56    pub fn sin(self) -> Self {
57        self.sin_strict()
58    }
59#[cfg(all(feature = "strict", not(feature = "fast")))]
60
61    /// With `strict` this dispatches to [`Self::cos_strict`]; without
62    /// it, the f64-bridge form is used instead.
63    #[inline]
64    #[must_use]
65    pub fn cos(self) -> Self {
66        self.cos_strict()
67    }
68#[cfg(all(feature = "strict", not(feature = "fast")))]
69
70    /// With `strict` this dispatches to [`Self::tan_strict`]; without
71    /// it, the f64-bridge form is used instead.
72    #[inline]
73    #[must_use]
74    pub fn tan(self) -> Self {
75        self.tan_strict()
76    }
77#[cfg(all(feature = "strict", not(feature = "fast")))]
78
79    /// With `strict` this dispatches to [`Self::asin_strict`]; without
80    /// it, the f64-bridge form is used instead.
81    #[inline]
82    #[must_use]
83    pub fn asin(self) -> Self {
84        self.asin_strict()
85    }
86#[cfg(all(feature = "strict", not(feature = "fast")))]
87
88    /// With `strict` this dispatches to [`Self::acos_strict`]; without
89    /// it, the f64-bridge form is used instead.
90    #[inline]
91    #[must_use]
92    pub fn acos(self) -> Self {
93        self.acos_strict()
94    }
95#[cfg(all(feature = "strict", not(feature = "fast")))]
96
97    /// With `strict` this dispatches to [`Self::atan_strict`]; without
98    /// it, the f64-bridge form is used instead.
99    #[inline]
100    #[must_use]
101    pub fn atan(self) -> Self {
102        self.atan_strict()
103    }
104#[cfg(all(feature = "strict", not(feature = "fast")))]
105
106    /// Four-quadrant arctangent of `self` (`y`) and `other` (`x`).
107    /// With `strict` this dispatches to [`Self::atan2_strict`];
108    /// without it, the f64-bridge form is used instead.
109    #[inline]
110    #[must_use]
111    pub fn atan2(self, other: Self) -> Self {
112        self.atan2_strict(other)
113    }
114#[cfg(all(feature = "strict", not(feature = "fast")))]
115
116    /// With `strict` this dispatches to [`Self::sinh_strict`]; without
117    /// it, the f64-bridge form is used instead.
118    #[inline]
119    #[must_use]
120    pub fn sinh(self) -> Self {
121        self.sinh_strict()
122    }
123#[cfg(all(feature = "strict", not(feature = "fast")))]
124
125    /// With `strict` this dispatches to [`Self::cosh_strict`]; without
126    /// it, the f64-bridge form is used instead.
127    #[inline]
128    #[must_use]
129    pub fn cosh(self) -> Self {
130        self.cosh_strict()
131    }
132#[cfg(all(feature = "strict", not(feature = "fast")))]
133
134    /// With `strict` this dispatches to [`Self::tanh_strict`]; without
135    /// it, the f64-bridge form is used instead.
136    #[inline]
137    #[must_use]
138    pub fn tanh(self) -> Self {
139        self.tanh_strict()
140    }
141#[cfg(all(feature = "strict", not(feature = "fast")))]
142
143    /// With `strict` this dispatches to [`Self::asinh_strict`]; without
144    /// it, the f64-bridge form is used instead.
145    #[inline]
146    #[must_use]
147    pub fn asinh(self) -> Self {
148        self.asinh_strict()
149    }
150#[cfg(all(feature = "strict", not(feature = "fast")))]
151
152    /// With `strict` this dispatches to [`Self::acosh_strict`]; without
153    /// it, the f64-bridge form is used instead.
154    #[inline]
155    #[must_use]
156    pub fn acosh(self) -> Self {
157        self.acosh_strict()
158    }
159#[cfg(all(feature = "strict", not(feature = "fast")))]
160
161    /// With `strict` this dispatches to [`Self::atanh_strict`]; without
162    /// it, the f64-bridge form is used instead.
163    #[inline]
164    #[must_use]
165    pub fn atanh(self) -> Self {
166        self.atanh_strict()
167    }
168#[cfg(all(feature = "strict", not(feature = "fast")))]
169
170    /// With `strict` this dispatches to [`Self::to_degrees_strict`]; without
171    /// it, the f64-bridge form is used instead.
172    #[inline]
173    #[must_use]
174    pub fn to_degrees(self) -> Self {
175        self.to_degrees_strict()
176    }
177#[cfg(all(feature = "strict", not(feature = "fast")))]
178
179    /// With `strict` this dispatches to [`Self::to_radians_strict`]; without
180    /// it, the f64-bridge form is used instead.
181    #[inline]
182    #[must_use]
183    pub fn to_radians(self) -> Self {
184        self.to_radians_strict()
185    }
186    /// Sine of `self` (radians). Strict: integer-only and correctly
187    /// rounded — the result is within 0.5 ULP of the exact sine.
188    #[inline]
189    #[must_use]
190    pub fn sin_strict(self) -> Self {
191        let w = SCALE + crate::log_exp_strict::STRICT_GUARD;
192        let raw = sin_fixed(to_fixed(self.0), w)
193            .round_to_i128(w, SCALE)
194            .expect("D38::sin: result out of range");
195        Self::from_bits(raw)
196    }
197
198    /// Cosine of `self` (radians). Strict: `cos(x) = sin(x + π/2)`,
199    /// correctly rounded.
200    #[inline]
201    #[must_use]
202    pub fn cos_strict(self) -> Self {
203        let w = SCALE + crate::log_exp_strict::STRICT_GUARD;
204        let arg = to_fixed(self.0).add(wide_half_pi(w));
205        let raw = sin_fixed(arg, w)
206            .round_to_i128(w, SCALE)
207            .expect("D38::cos: result out of range");
208        Self::from_bits(raw)
209    }
210
211    /// Tangent of `self` (radians). Strict: `tan(x) = sin(x) / cos(x)`,
212    /// with the division carried in the wide intermediate so the result
213    /// is correctly rounded.
214    ///
215    /// # Panics
216    ///
217    /// Panics if `cos(self)` is zero (an odd multiple of π/2).
218    #[inline]
219    #[must_use]
220    pub fn tan_strict(self) -> Self {
221        let w = SCALE + crate::log_exp_strict::STRICT_GUARD;
222        let v = to_fixed(self.0);
223        let sin_w = sin_fixed(v, w);
224        let cos_w = sin_fixed(v.add(wide_half_pi(w)), w);
225        assert!(!cos_w.is_zero(), "D38::tan: cosine is zero (argument is an odd multiple of pi/2)");
226        let raw = sin_w
227            .div(cos_w, w)
228            .round_to_i128(w, SCALE)
229            .expect("D38::tan: result out of range");
230        Self::from_bits(raw)
231    }
232
233    /// Arctangent of `self`, in radians, in `(−π/2, π/2)`. Strict:
234    /// integer-only and correctly rounded.
235    #[inline]
236    #[must_use]
237    pub fn atan_strict(self) -> Self {
238        let w = SCALE + crate::log_exp_strict::STRICT_GUARD;
239        let raw = atan_fixed(to_fixed(self.0), w)
240            .round_to_i128(w, SCALE)
241            .expect("D38::atan: result out of range");
242        Self::from_bits(raw)
243    }
244
245    /// Arcsine of `self`, in radians, in `[−π/2, π/2]`. Strict.
246    ///
247    /// `asin(x) = atan(x / √(1 − x²))`; the endpoints `±1` map directly
248    /// to `±π/2`.
249    ///
250    /// # Panics
251    ///
252    /// Panics if `|self| > 1`.
253    #[inline]
254    #[must_use]
255    pub fn asin_strict(self) -> Self {
256        use crate::d_w128_kernels::Fixed;
257        let w = SCALE + crate::log_exp_strict::STRICT_GUARD;
258        let one_w = Fixed { negative: false, mag: Fixed::pow10(w) };
259        let v = to_fixed(self.0);
260        let abs_v = Fixed { negative: false, mag: v.mag };
261        assert!(!(abs_v.ge_mag(one_w) && abs_v != one_w), "D38::asin: argument out of domain [-1, 1]");
262        if abs_v == one_w {
263            // asin(±1) = ±π/2.
264            let hp = wide_half_pi(w);
265            let hp = if v.negative { hp.neg() } else { hp };
266            let raw = hp
267                .round_to_i128(w, SCALE)
268                .expect("D38::asin: result out of range");
269            return Self::from_bits(raw);
270        }
271        // √(1 − x²); x² ≤ 1 so 1 − x² ∈ [0, 1].
272        let denom = one_w.sub(v.mul(v, w)).sqrt(w);
273        let raw = atan_fixed(v.div(denom, w), w)
274            .round_to_i128(w, SCALE)
275            .expect("D38::asin: result out of range");
276        Self::from_bits(raw)
277    }
278
279    /// Arccosine of `self`, in radians, in `[0, π]`. Strict:
280    /// `acos(x) = π/2 − asin(x)`, correctly rounded.
281    ///
282    /// # Panics
283    ///
284    /// Panics if `|self| > 1`.
285    #[inline]
286    #[must_use]
287    pub fn acos_strict(self) -> Self {
288        use crate::d_w128_kernels::Fixed;
289        let w = SCALE + crate::log_exp_strict::STRICT_GUARD;
290        let one_w = Fixed { negative: false, mag: Fixed::pow10(w) };
291        let v = to_fixed(self.0);
292        let abs_v = Fixed { negative: false, mag: v.mag };
293        assert!(!(abs_v.ge_mag(one_w) && abs_v != one_w), "D38::acos: argument out of domain [-1, 1]");
294        // asin(v) in the wide intermediate, then π/2 − asin(v).
295        let asin_w = if abs_v == one_w {
296            let hp = wide_half_pi(w);
297            if v.negative {
298                hp.neg()
299            } else {
300                hp
301            }
302        } else {
303            let denom = one_w.sub(v.mul(v, w)).sqrt(w);
304            atan_fixed(v.div(denom, w), w)
305        };
306        let raw = wide_half_pi(w)
307            .sub(asin_w)
308            .round_to_i128(w, SCALE)
309            .expect("D38::acos: result out of range");
310        Self::from_bits(raw)
311    }
312
313    /// Four-quadrant arctangent of `self` (`y`) and `other` (`x`), in
314    /// radians, in `(−π, π]`. Strict: integer-only and correctly
315    /// rounded.
316    #[inline]
317    #[must_use]
318    pub fn atan2_strict(self, other: Self) -> Self {
319        let w = SCALE + crate::log_exp_strict::STRICT_GUARD;
320        let y = to_fixed(self.0);
321        let x = to_fixed(other.0);
322        let result_w = if x.is_zero() {
323            if self.0 > 0 {
324                wide_half_pi(w)
325            } else if self.0 < 0 {
326                wide_half_pi(w).neg()
327            } else {
328                crate::d_w128_kernels::Fixed::ZERO
329            }
330        } else {
331            let base = atan_fixed(y.div(x, w), w);
332            if !x.negative {
333                base
334            } else if !y.negative {
335                base.add(wide_pi(w))
336            } else {
337                base.sub(wide_pi(w))
338            }
339        };
340        let raw = result_w
341            .round_to_i128(w, SCALE)
342            .expect("D38::atan2: result out of range");
343        Self::from_bits(raw)
344    }
345
346    /// Hyperbolic sine of `self`. Strict: `sinh(x) = (eˣ − e⁻ˣ)/2`,
347    /// composed in the wide intermediate from the correctly-rounded
348    /// `exp`, so the result is itself correctly rounded.
349    #[inline]
350    #[must_use]
351    pub fn sinh_strict(self) -> Self {
352        let w = SCALE + crate::log_exp_strict::STRICT_GUARD;
353        let v = to_fixed(self.0);
354        let ex = crate::log_exp_strict::exp_fixed(v, w);
355        let enx = crate::log_exp_strict::exp_fixed(v.neg(), w);
356        let raw = ex
357            .sub(enx)
358            .halve()
359            .round_to_i128(w, SCALE)
360            .expect("D38::sinh: result out of range");
361        Self::from_bits(raw)
362    }
363
364    /// Hyperbolic cosine of `self`. Strict: `cosh(x) = (eˣ + e⁻ˣ)/2`,
365    /// correctly rounded.
366    #[inline]
367    #[must_use]
368    pub fn cosh_strict(self) -> Self {
369        let w = SCALE + crate::log_exp_strict::STRICT_GUARD;
370        let v = to_fixed(self.0);
371        let ex = crate::log_exp_strict::exp_fixed(v, w);
372        let enx = crate::log_exp_strict::exp_fixed(v.neg(), w);
373        let raw = ex
374            .add(enx)
375            .halve()
376            .round_to_i128(w, SCALE)
377            .expect("D38::cosh: result out of range");
378        Self::from_bits(raw)
379    }
380
381    /// Hyperbolic tangent of `self`. Strict: `tanh(x) = sinh(x)/cosh(x)`
382    /// with the division in the wide intermediate. `cosh ≥ 1`, so the
383    /// division never traps.
384    #[inline]
385    #[must_use]
386    pub fn tanh_strict(self) -> Self {
387        let w = SCALE + crate::log_exp_strict::STRICT_GUARD;
388        let v = to_fixed(self.0);
389        let ex = crate::log_exp_strict::exp_fixed(v, w);
390        let enx = crate::log_exp_strict::exp_fixed(v.neg(), w);
391        let raw = ex
392            .sub(enx)
393            .div(ex.add(enx), w)
394            .round_to_i128(w, SCALE)
395            .expect("D38::tanh: result out of range");
396        Self::from_bits(raw)
397    }
398
399    /// Inverse hyperbolic sine of `self`. Strict:
400    /// `asinh(x) = sign · ln(|x| + √(x² + 1))`, correctly rounded.
401    /// For `|x| ≥ 1` the radicand is factored as
402    /// `|x|·(1 + √(1 + 1/x²))` to keep `x²` from overflowing the wide
403    /// intermediate.
404    #[inline]
405    #[must_use]
406    pub fn asinh_strict(self) -> Self {
407        use crate::d_w128_kernels::Fixed;
408        if self.0 == 0 {
409            return Self::ZERO;
410        }
411        let w = SCALE + crate::log_exp_strict::STRICT_GUARD;
412        let one_w = Fixed { negative: false, mag: Fixed::pow10(w) };
413        let v = to_fixed(self.0);
414        let ax = Fixed { negative: false, mag: v.mag };
415        let inner = if ax.ge_mag(one_w) {
416            // ln(|x|) + ln(1 + √(1 + 1/x²)).
417            let inv = one_w.div(ax, w);
418            let root = one_w.add(inv.mul(inv, w)).sqrt(w);
419            crate::log_exp_strict::ln_fixed(ax, w).add(crate::log_exp_strict::ln_fixed(one_w.add(root), w))
420        } else {
421            // ln(|x| + √(x² + 1)).
422            let root = ax.mul(ax, w).add(one_w).sqrt(w);
423            crate::log_exp_strict::ln_fixed(ax.add(root), w)
424        };
425        let signed = if self.0 < 0 { inner.neg() } else { inner };
426        let raw = signed
427            .round_to_i128(w, SCALE)
428            .expect("D38::asinh: result out of range");
429        Self::from_bits(raw)
430    }
431
432    /// Inverse hyperbolic cosine of `self`. Strict:
433    /// `acosh(x) = ln(x + √(x² − 1))`, defined for `x ≥ 1`, correctly
434    /// rounded. For `x ≥ 2` the radicand is factored as
435    /// `x·(1 + √(1 − 1/x²))` to keep `x²` in range.
436    ///
437    /// # Panics
438    ///
439    /// Panics if `self < 1`.
440    #[inline]
441    #[must_use]
442    pub fn acosh_strict(self) -> Self {
443        use crate::d_w128_kernels::Fixed;
444        let w = SCALE + crate::log_exp_strict::STRICT_GUARD;
445        let one_w = Fixed { negative: false, mag: Fixed::pow10(w) };
446        let v = to_fixed(self.0);
447        assert!(!v.negative && v.ge_mag(one_w), "D38::acosh: argument must be >= 1");
448        let two_w = one_w.double();
449        let inner = if v.ge_mag(two_w) {
450            // ln(x) + ln(1 + √(1 − 1/x²)).
451            let inv = one_w.div(v, w);
452            let root = one_w.sub(inv.mul(inv, w)).sqrt(w);
453            crate::log_exp_strict::ln_fixed(v, w).add(crate::log_exp_strict::ln_fixed(one_w.add(root), w))
454        } else {
455            // ln(x + √(x² − 1)).
456            let root = v.mul(v, w).sub(one_w).sqrt(w);
457            crate::log_exp_strict::ln_fixed(v.add(root), w)
458        };
459        let raw = inner
460            .round_to_i128(w, SCALE)
461            .expect("D38::acosh: result out of range");
462        Self::from_bits(raw)
463    }
464
465    /// Inverse hyperbolic tangent of `self`. Strict:
466    /// `atanh(x) = ln((1 + x) / (1 − x)) / 2`, defined for `|x| < 1`,
467    /// correctly rounded.
468    ///
469    /// # Panics
470    ///
471    /// Panics if `|self| >= 1`.
472    #[inline]
473    #[must_use]
474    pub fn atanh_strict(self) -> Self {
475        use crate::d_w128_kernels::Fixed;
476        let w = SCALE + crate::log_exp_strict::STRICT_GUARD;
477        let one_w = Fixed { negative: false, mag: Fixed::pow10(w) };
478        let v = to_fixed(self.0);
479        let ax = Fixed { negative: false, mag: v.mag };
480        assert!(!ax.ge_mag(one_w), "D38::atanh: argument out of domain (-1, 1)");
481        // ln((1 + x) / (1 − x)) / 2.
482        let ratio = one_w.add(v).div(one_w.sub(v), w);
483        let raw = crate::log_exp_strict::ln_fixed(ratio, w)
484            .halve()
485            .round_to_i128(w, SCALE)
486            .expect("D38::atanh: result out of range");
487        Self::from_bits(raw)
488    }
489
490    /// Convert radians to degrees: `self · (180 / π)`. Strict: the
491    /// multiply and divide run in the wide intermediate, so the result
492    /// is correctly rounded.
493    #[inline]
494    #[must_use]
495    pub fn to_degrees_strict(self) -> Self {
496        let w = SCALE + crate::log_exp_strict::STRICT_GUARD;
497        let raw = to_fixed(self.0)
498            .mul_u128(180)
499            .div(wide_pi(w), w)
500            .round_to_i128(w, SCALE)
501            .expect("D38::to_degrees: result out of range");
502        Self::from_bits(raw)
503    }
504
505    /// Convert degrees to radians: `self · (π / 180)`. Strict:
506    /// correctly rounded.
507    #[inline]
508    #[must_use]
509    pub fn to_radians_strict(self) -> Self {
510        let w = SCALE + crate::log_exp_strict::STRICT_GUARD;
511        let raw = to_fixed(self.0)
512            .mul(wide_pi(w), w)
513            .div_small(180)
514            .round_to_i128(w, SCALE)
515            .expect("D38::to_radians: result out of range");
516        Self::from_bits(raw)
517    }
518}
519
520
521// ─────────────────────────────────────────────────────────────────────
522// Strict-mode (integer-only) trigonometric, hyperbolic, and angle-
523// conversion methods.
524//
525// These mirror the f64-bridge surface above but are integer-only,
526// `no_std`-compatible, and **correctly rounded** — within 0.5 ULP of
527// the exact result. Every reduction and series step runs in the
528// `d_w128_kernels::Fixed` guard-digit intermediate (the same machinery the
529// log/exp family uses) and the value is rounded once at the end.
530//
531// Composition strategy:
532//
533// - Hyperbolic functions are composed from the strict `exp` / `ln` /
534// `sqrt` already implemented in `log_exp_strict.rs` / `powers_strict.rs`.
535// - `cos` is `sin` phase-shifted by π/2; `tan` is `sin / cos`.
536// - `sin` uses range reduction modulo τ into one π/2 octant followed by
537// a Taylor series.
538// - `atan` uses reciprocal reduction for |x| > 1 plus argument halving,
539// then a Taylor series; `asin` / `acos` / `atan2` are derived from it.
540// ─────────────────────────────────────────────────────────────────────
541
542// Strict-feature dispatchers. When `strict` is enabled (and
543// `fast` is not), the plain trig methods route to the
544// integer-only `*_strict` implementations below.
545
546// ─────────────────────────────────────────────────────────────────────
547// Correctly-rounded strict trigonometric core.
548//
549// Every strict trig method runs on the shared `d_w128_kernels::Fixed`
550// guard-digit intermediate at `SCALE + STRICT_GUARD` working digits,
551// the same machinery the log/exp family uses, and rounds once at the
552// end — so each result is within 0.5 ULP of the exact value.
553// ─────────────────────────────────────────────────────────────────────
554
555/// π at working scale `w` (`w <= 63`), from a 63-digit embedded value.
556fn wide_pi(w: u32) -> crate::d_w128_kernels::Fixed {
557    // π = 3.141592653589793238462643383279502884197169399375105820974944 592
558    crate::d_w128_kernels::Fixed::from_decimal_split(
559        31_415_926_535_897_932_384_626_433_832_795_u128,
560        2_884_197_169_399_375_105_820_974_944_592_u128,
561    )
562    .rescale_down(63, w)
563}
564
565/// τ = 2π at working scale `w`.
566fn wide_tau(w: u32) -> crate::d_w128_kernels::Fixed {
567    wide_pi(w).double()
568}
569
570/// π/2 at working scale `w`.
571fn wide_half_pi(w: u32) -> crate::d_w128_kernels::Fixed {
572    wide_pi(w).halve()
573}
574
575/// Builds a working-scale `Fixed` from a signed `D38` raw value `r`:
576/// `r · 10^STRICT_GUARD`, carrying the sign.
577fn to_fixed(raw: i128) -> crate::d_w128_kernels::Fixed {
578    use crate::d_w128_kernels::Fixed;
579    let m = Fixed::from_u128_mag(raw.unsigned_abs(), false)
580        .mul_u128(10u128.pow(crate::log_exp_strict::STRICT_GUARD));
581    if raw < 0 {
582        m.neg()
583    } else {
584        m
585    }
586}
587
588/// Taylor series for `sin` on a reduced non-negative argument
589/// `r ∈ [0, π/2]`, evaluated at working scale `w`.
590fn sin_taylor(r: crate::d_w128_kernels::Fixed, w: u32) -> crate::d_w128_kernels::Fixed {
591    let r2 = r.mul(r, w);
592    let mut sum = r;
593    let mut term = r; // term = r^(2k-1)
594    let mut k: u128 = 1;
595    loop {
596        // term_k = term_{k-1} · r² / ((2k)(2k+1)); sign alternates.
597        term = term.mul(r2, w).div_small((2 * k) * (2 * k + 1));
598        if term.is_zero() {
599            break;
600        }
601        if k % 2 == 1 {
602            sum = sum.sub(term);
603        } else {
604            sum = sum.add(term);
605        }
606        k += 1;
607        if k > 200 {
608            break;
609        }
610    }
611    sum
612}
613
614/// Sine of a working-scale value `v_w`, at working scale `w`.
615///
616/// Reduces `v` modulo τ via `q = round(v/τ)`, folds the remainder into
617/// `[0, π/2]` tracking sign and the `π − x` reflection, then evaluates
618/// the Taylor series.
619fn sin_fixed(v_w: crate::d_w128_kernels::Fixed, w: u32) -> crate::d_w128_kernels::Fixed {
620    use crate::d_w128_kernels::Fixed;
621    let tau = wide_tau(w);
622    let pi = wide_pi(w);
623    let half_pi = wide_half_pi(w);
624
625    // r = v - round(v/τ)·τ ∈ [-π, π].
626    let q = v_w.div(tau, w).round_to_nearest_int(w);
627    let q_tau = if q >= 0 {
628        tau.mul_u128(q as u128)
629    } else {
630        tau.mul_u128((-q) as u128).neg()
631    };
632    let r = v_w.sub(q_tau);
633
634    // Fold |r| ∈ [0, π] into [0, π/2] via sin(π − x) = sin(x).
635    let sign = r.negative;
636    let abs_r = Fixed { negative: false, mag: r.mag };
637    let reduced = if abs_r.ge_mag(half_pi) {
638        pi.sub(abs_r)
639    } else {
640        abs_r
641    };
642    let s = sin_taylor(reduced, w);
643    if sign {
644        s.neg()
645    } else {
646        s
647    }
648}
649
650/// Taylor series for `atan` on a reduced non-negative argument
651/// `x ∈ [0, ~1/8]`, evaluated at working scale `w`.
652fn atan_taylor(x: crate::d_w128_kernels::Fixed, w: u32) -> crate::d_w128_kernels::Fixed {
653    let x2 = x.mul(x, w);
654    let mut sum = x;
655    let mut term = x; // term = x^(2k-1)
656    let mut k: u128 = 1;
657    loop {
658        term = term.mul(x2, w);
659        let contrib = term.div_small(2 * k + 1);
660        if contrib.is_zero() {
661            break;
662        }
663        if k % 2 == 1 {
664            sum = sum.sub(contrib);
665        } else {
666            sum = sum.add(contrib);
667        }
668        k += 1;
669        if k > 300 {
670            break;
671        }
672    }
673    sum
674}
675
676/// Arctangent of a working-scale value `v_w`, at working scale `w`,
677/// result in `(−π/2, π/2)`.
678///
679/// Odd-function fold to `x ≥ 0`; reciprocal reduction
680/// `atan(x) = π/2 − atan(1/x)` for `x > 1`; three rounds of argument
681/// halving `atan(x) = 2·atan(x / (1 + √(1+x²)))`; then the series.
682fn atan_fixed(v_w: crate::d_w128_kernels::Fixed, w: u32) -> crate::d_w128_kernels::Fixed {
683    use crate::d_w128_kernels::Fixed;
684    let one_w = Fixed { negative: false, mag: Fixed::pow10(w) };
685    let sign = v_w.negative;
686    let mut x = Fixed { negative: false, mag: v_w.mag };
687    let mut add_half_pi = false;
688    if x.ge_mag(one_w) && x != one_w {
689        x = one_w.div(x, w); // atan(x) = π/2 − atan(1/x)
690        add_half_pi = true;
691    }
692    // Three argument halvings: atan(x) = 2·atan(x / (1 + √(1+x²))).
693    let halvings: u32 = 3;
694    for _ in 0..halvings {
695        let x2 = x.mul(x, w);
696        let denom = one_w.add(one_w.add(x2).sqrt(w));
697        x = x.div(denom, w);
698    }
699    let mut result = atan_taylor(x, w);
700    result = result.shl(halvings);
701    if add_half_pi {
702        result = wide_half_pi(w).sub(result);
703    }
704    if sign {
705        result.neg()
706    } else {
707        result
708    }
709}
710
711#[cfg(test)]
712mod tests {
713    use crate::consts::DecimalConsts;
714    use crate::core_type::D38s12;
715
716    // Tolerance for single-operation results. The f64-bridge build is
717    // one f64 round-trip (≤ 2 LSB); the integer-only `strict` build is
718    // correctly rounded (≤ 0.5 ULP per call) and is held to the same
719    // 2-LSB bound — a couple of LSB for the test's own expected-value
720    // rounding.
721    const TWO_LSB: i128 = 2;
722
723    // Tolerance for results that chain multiple trig calls (e.g.
724    // `sin² + cos²`, `cosh² − sinh²`): each input is within 0.5 ULP, so
725    // the composed quantity stays within a few LSB in both builds.
726    const FOUR_LSB: i128 = 4;
727
728    // Angle-conversion results compared against exact integer targets
729    // (180, 90, 45 degrees). The `pi()` / `quarter_pi()` *input*
730    // constants are themselves rounded to the type's scale, and
731    // `to_degrees` amplifies that input quantization by ~57.3 — so even
732    // a perfectly-rounded conversion lands ~30 LSB off the exact
733    // integer at SCALE = 12. (This bounds the *input*, not the
734    // conversion: `to_degrees` itself is correctly rounded in `strict`.)
735    const ANGLE_TOLERANCE_LSB: i128 = 32;
736
737    fn within_lsb(actual: D38s12, expected: D38s12, lsb: i128) -> bool {
738        let diff = (actual.to_bits() - expected.to_bits()).abs();
739        diff <= lsb
740    }
741
742    // ── Forward trig ──────────────────────────────────────────────────
743
744    /// The strict trig / hyperbolic family is correctly rounded:
745    /// cross-check every method against the f64 bridge at D38<9>,
746    /// where f64 (≈ 15–16 significant digits) is comfortably more
747    /// precise than the type's ULP, so a correctly-rounded integer
748    /// result must agree to within 1 ULP (allow 1 more for the f64
749    /// reference's own rounding).
750    #[cfg(all(feature = "strict", not(feature = "fast")))]
751    #[test]
752    fn strict_trig_family_matches_f64() {
753        use crate::core_type::D38;
754        macro_rules! check {
755            ($name:literal, $raw:expr, $strict:expr, $f64expr:expr) => {{
756                let strict: i128 = $strict;
757                let v = $raw as f64 / 1e9;
758                let reference = ($f64expr(v) * 1e9).round() as i128;
759                assert!(
760                    (strict - reference).abs() <= 2,
761                    concat!($name, "({}) = {}, f64 reference {}"),
762                    $raw,
763                    strict,
764                    reference
765                );
766            }};
767        }
768        // Forward trig — arguments across a few periods, incl. negative.
769        for &raw in &[
770            -7_000_000_000_i128, -1_000_000_000, -100_000_000, 1,
771            500_000_000, 1_000_000_000, 1_570_796_327, 3_000_000_000,
772            6_283_185_307, 12_000_000_000,
773        ] {
774            let x = D38::<9>::from_bits(raw);
775            check!("sin", raw, x.sin_strict().to_bits(), f64::sin);
776            check!("cos", raw, x.cos_strict().to_bits(), f64::cos);
777            check!("atan", raw, x.atan_strict().to_bits(), f64::atan);
778            check!("sinh", raw, x.sinh_strict().to_bits(), f64::sinh);
779            check!("cosh", raw, x.cosh_strict().to_bits(), f64::cosh);
780            check!("tanh", raw, x.tanh_strict().to_bits(), f64::tanh);
781            check!("asinh", raw, x.asinh_strict().to_bits(), f64::asinh);
782        }
783        // asin / acos — domain [-1, 1].
784        for &raw in &[
785            -1_000_000_000_i128, -700_000_000, -100_000_000, 0,
786            250_000_000, 500_000_000, 999_999_999,
787        ] {
788            let x = D38::<9>::from_bits(raw);
789            check!("asin", raw, x.asin_strict().to_bits(), f64::asin);
790            check!("acos", raw, x.acos_strict().to_bits(), f64::acos);
791        }
792        // atanh — domain (-1, 1).
793        for &raw in &[-900_000_000_i128, -300_000_000, 1, 300_000_000, 900_000_000] {
794            let x = D38::<9>::from_bits(raw);
795            check!("atanh", raw, x.atanh_strict().to_bits(), f64::atanh);
796        }
797        // acosh — domain [1, ∞).
798        for &raw in &[1_000_000_000_i128, 1_500_000_000, 3_000_000_000, 50_000_000_000] {
799            let x = D38::<9>::from_bits(raw);
800            check!("acosh", raw, x.acosh_strict().to_bits(), f64::acosh);
801        }
802        // tan — avoid the poles.
803        for &raw in &[-1_000_000_000_i128, 1, 500_000_000, 1_000_000_000, 1_400_000_000] {
804            let x = D38::<9>::from_bits(raw);
805            check!("tan", raw, x.tan_strict().to_bits(), f64::tan);
806        }
807    }
808
809    /// `sin(0) == 0` -- bit-exact via `f64::sin(0.0) == 0.0`.
810    #[test]
811    fn sin_zero_is_zero() {
812        assert_eq!(D38s12::ZERO.sin(), D38s12::ZERO);
813    }
814
815    /// `cos(0) == 1` -- bit-exact via `f64::cos(0.0) == 1.0`.
816    #[test]
817    fn cos_zero_is_one() {
818        assert_eq!(D38s12::ZERO.cos(), D38s12::ONE);
819    }
820
821    /// `tan(0) == 0` -- bit-exact via `f64::tan(0.0) == 0.0`.
822    #[test]
823    fn tan_zero_is_zero() {
824        assert_eq!(D38s12::ZERO.tan(), D38s12::ZERO);
825    }
826
827    /// Pythagorean identity: `sin^2(x) + cos^2(x) ~= 1` within 4 LSB
828    /// for representative values of `x`. Values are chosen to be well
829    /// away from any well-known mathematical constant.
830    #[test]
831    fn sin_squared_plus_cos_squared_is_one() {
832        for raw in [
833            1_234_567_890_123_i128,  // ~1.234567...
834            -2_345_678_901_234_i128, // ~-2.345678...
835            500_000_000_000_i128,    // 0.5
836            -500_000_000_000_i128,   // -0.5
837            4_567_891_234_567_i128,  // ~4.567891...
838        ] {
839            let x = D38s12::from_bits(raw);
840            let s = x.sin();
841            let c = x.cos();
842            let sum = (s * s) + (c * c);
843            assert!(
844                within_lsb(sum, D38s12::ONE, FOUR_LSB),
845                "sin^2 + cos^2 != 1 for raw={raw}: got bits {} (delta {})",
846                sum.to_bits(),
847                (sum.to_bits() - D38s12::ONE.to_bits()).abs(),
848            );
849        }
850    }
851
852    // ── Inverse trig ──────────────────────────────────────────────────
853
854    /// `asin(0) == 0` -- bit-exact.
855    #[test]
856    fn asin_zero_is_zero() {
857        assert_eq!(D38s12::ZERO.asin(), D38s12::ZERO);
858    }
859
860    /// `acos(1) == 0` -- bit-exact via `f64::acos(1.0) == 0.0`.
861    #[test]
862    fn acos_one_is_zero() {
863        assert_eq!(D38s12::ONE.acos(), D38s12::ZERO);
864    }
865
866    /// `acos(0) ~= pi/2` within 4 LSB.
867    #[test]
868    fn acos_zero_is_half_pi() {
869        let result = D38s12::ZERO.acos();
870        assert!(
871            within_lsb(result, D38s12::half_pi(), FOUR_LSB),
872            "acos(0) bits {}, half_pi bits {}",
873            result.to_bits(),
874            D38s12::half_pi().to_bits(),
875        );
876    }
877
878    /// `atan(0) == 0` -- bit-exact via `f64::atan(0.0) == 0.0`.
879    #[test]
880    fn atan_zero_is_zero() {
881        assert_eq!(D38s12::ZERO.atan(), D38s12::ZERO);
882    }
883
884    /// Round-trip identity: `asin(sin(x)) ~= x` for `x` in
885    /// `[-pi/2, pi/2]`. Values stay within the principal branch.
886    #[test]
887    fn asin_of_sin_round_trip() {
888        for raw in [
889            123_456_789_012_i128,    // ~0.123456...
890            -123_456_789_012_i128,   // ~-0.123456...
891            456_789_012_345_i128,    // ~0.456789...
892            -456_789_012_345_i128,   // ~-0.456789...
893            1_234_567_890_123_i128,  // ~1.234567... (well inside pi/2)
894            -1_234_567_890_123_i128, // ~-1.234567...
895        ] {
896            let x = D38s12::from_bits(raw);
897            let recovered = x.sin().asin();
898            assert!(
899                within_lsb(recovered, x, FOUR_LSB),
900                "asin(sin(x)) != x for raw={raw}: got bits {} (delta {})",
901                recovered.to_bits(),
902                (recovered.to_bits() - x.to_bits()).abs(),
903            );
904        }
905    }
906
907    // ── atan2 ─────────────────────────────────────────────────────────
908
909    /// `atan2(1, 1) ~= pi/4` (first-quadrant 45 degrees).
910    #[test]
911    fn atan2_first_quadrant_diagonal() {
912        let one = D38s12::ONE;
913        let result = one.atan2(one);
914        assert!(
915            within_lsb(result, D38s12::quarter_pi(), TWO_LSB),
916            "atan2(1, 1) bits {}, quarter_pi bits {}",
917            result.to_bits(),
918            D38s12::quarter_pi().to_bits(),
919        );
920    }
921
922    /// `atan2(-1, -1) ~= -3*pi/4` (third-quadrant correctness).
923    #[test]
924    fn atan2_third_quadrant_diagonal() {
925        let neg_one = -D38s12::ONE;
926        let result = neg_one.atan2(neg_one);
927        let three = D38s12::from_int(3);
928        let expected = -(D38s12::quarter_pi() * three);
929        assert!(
930            within_lsb(result, expected, TWO_LSB),
931            "atan2(-1, -1) bits {}, expected -3pi/4 bits {}",
932            result.to_bits(),
933            expected.to_bits(),
934        );
935    }
936
937    /// `atan2(1, -1) ~= 3*pi/4` (second-quadrant correctness).
938    #[test]
939    fn atan2_second_quadrant_diagonal() {
940        let one = D38s12::ONE;
941        let neg_one = -D38s12::ONE;
942        let result = one.atan2(neg_one);
943        let three = D38s12::from_int(3);
944        let expected = D38s12::quarter_pi() * three;
945        assert!(
946            within_lsb(result, expected, TWO_LSB),
947            "atan2(1, -1) bits {}, expected 3pi/4 bits {}",
948            result.to_bits(),
949            expected.to_bits(),
950        );
951    }
952
953    /// `atan2(-1, 1) ~= -pi/4` (fourth-quadrant correctness).
954    #[test]
955    fn atan2_fourth_quadrant_diagonal() {
956        let one = D38s12::ONE;
957        let neg_one = -D38s12::ONE;
958        let result = neg_one.atan2(one);
959        let expected = -D38s12::quarter_pi();
960        assert!(
961            within_lsb(result, expected, TWO_LSB),
962            "atan2(-1, 1) bits {}, expected -pi/4 bits {}",
963            result.to_bits(),
964            expected.to_bits(),
965        );
966    }
967
968    /// `atan2(0, 1) == 0` (positive x-axis is bit-exact).
969    #[test]
970    fn atan2_positive_x_axis_is_zero() {
971        let zero = D38s12::ZERO;
972        let one = D38s12::ONE;
973        assert_eq!(zero.atan2(one), D38s12::ZERO);
974    }
975
976    // ── Hyperbolic ────────────────────────────────────────────────────
977
978    /// `sinh(0) == 0` -- bit-exact via `f64::sinh(0.0) == 0.0`.
979    #[test]
980    fn sinh_zero_is_zero() {
981        assert_eq!(D38s12::ZERO.sinh(), D38s12::ZERO);
982    }
983
984    /// `cosh(0) == 1` -- bit-exact via `f64::cosh(0.0) == 1.0`.
985    #[test]
986    fn cosh_zero_is_one() {
987        assert_eq!(D38s12::ZERO.cosh(), D38s12::ONE);
988    }
989
990    /// `tanh(0) == 0` -- bit-exact via `f64::tanh(0.0) == 0.0`.
991    #[test]
992    fn tanh_zero_is_zero() {
993        assert_eq!(D38s12::ZERO.tanh(), D38s12::ZERO);
994    }
995
996    /// `asinh(0) == 0` -- bit-exact.
997    #[test]
998    fn asinh_zero_is_zero() {
999        assert_eq!(D38s12::ZERO.asinh(), D38s12::ZERO);
1000    }
1001
1002    /// `acosh(1) == 0` -- bit-exact via `f64::acosh(1.0) == 0.0`.
1003    #[test]
1004    fn acosh_one_is_zero() {
1005        assert_eq!(D38s12::ONE.acosh(), D38s12::ZERO);
1006    }
1007
1008    /// `atanh(0) == 0` -- bit-exact.
1009    #[test]
1010    fn atanh_zero_is_zero() {
1011        assert_eq!(D38s12::ZERO.atanh(), D38s12::ZERO);
1012    }
1013
1014    /// Identity: `cosh^2(x) - sinh^2(x) == 1` within 4 LSB for
1015    /// representative values of `x`.
1016    #[test]
1017    fn cosh_squared_minus_sinh_squared_is_one() {
1018        if !crate::rounding::DEFAULT_IS_HALF_TO_EVEN { return; }
1019        for raw in [
1020            500_000_000_000_i128,    // 0.5
1021            -500_000_000_000_i128,   // -0.5
1022            1_234_567_890_123_i128,  // ~1.234567
1023            -1_234_567_890_123_i128, // ~-1.234567
1024            2_500_000_000_000_i128,  // 2.5
1025        ] {
1026            let x = D38s12::from_bits(raw);
1027            let ch = x.cosh();
1028            let sh = x.sinh();
1029            let diff = (ch * ch) - (sh * sh);
1030            assert!(
1031                within_lsb(diff, D38s12::ONE, FOUR_LSB),
1032                "cosh^2 - sinh^2 != 1 for raw={raw}: got bits {} (delta {})",
1033                diff.to_bits(),
1034                (diff.to_bits() - D38s12::ONE.to_bits()).abs(),
1035            );
1036        }
1037    }
1038
1039    // ── Angle conversions ─────────────────────────────────────────────
1040
1041    /// `to_degrees(pi) ~= 180` within `ANGLE_TOLERANCE_LSB`. The
1042    /// tolerance is dominated by f64's limited precision on `pi`,
1043    /// amplified by ~57.3 during the degrees conversion.
1044    #[test]
1045    fn to_degrees_pi_is_180() {
1046        if !crate::rounding::DEFAULT_IS_HALF_TO_EVEN { return; }
1047        let pi = D38s12::pi();
1048        let result = pi.to_degrees();
1049        let expected = D38s12::from_int(180);
1050        assert!(
1051            within_lsb(result, expected, ANGLE_TOLERANCE_LSB),
1052            "to_degrees(pi) bits {}, expected 180 bits {} (delta {})",
1053            result.to_bits(),
1054            expected.to_bits(),
1055            (result.to_bits() - expected.to_bits()).abs(),
1056        );
1057    }
1058
1059    /// `to_radians(180) ~= pi` within `ANGLE_TOLERANCE_LSB`.
1060    #[test]
1061    fn to_radians_180_is_pi() {
1062        let one_eighty = D38s12::from_int(180);
1063        let result = one_eighty.to_radians();
1064        let expected = D38s12::pi();
1065        assert!(
1066            within_lsb(result, expected, ANGLE_TOLERANCE_LSB),
1067            "to_radians(180) bits {}, expected pi bits {} (delta {})",
1068            result.to_bits(),
1069            expected.to_bits(),
1070            (result.to_bits() - expected.to_bits()).abs(),
1071        );
1072    }
1073
1074    /// `to_degrees(0) == 0` -- bit-exact (0 * anything == 0).
1075    #[test]
1076    fn to_degrees_zero_is_zero() {
1077        assert_eq!(D38s12::ZERO.to_degrees(), D38s12::ZERO);
1078    }
1079
1080    /// `to_radians(0) == 0` -- bit-exact.
1081    #[test]
1082    fn to_radians_zero_is_zero() {
1083        assert_eq!(D38s12::ZERO.to_radians(), D38s12::ZERO);
1084    }
1085
1086    /// Round-trip: `to_radians(to_degrees(x)) ~= x` within 4 LSB
1087    /// (two f64 round-trips).
1088    #[test]
1089    fn to_radians_to_degrees_round_trip() {
1090        for raw in [
1091            500_000_000_000_i128,    // 0.5
1092            -500_000_000_000_i128,   // -0.5
1093            1_234_567_890_123_i128,  // ~1.234567
1094            -2_345_678_901_234_i128, // ~-2.345678
1095        ] {
1096            let x = D38s12::from_bits(raw);
1097            let recovered = x.to_degrees().to_radians();
1098            assert!(
1099                within_lsb(recovered, x, FOUR_LSB),
1100                "to_radians(to_degrees(x)) != x for raw={raw}: got bits {} (delta {})",
1101                recovered.to_bits(),
1102                (recovered.to_bits() - x.to_bits()).abs(),
1103            );
1104        }
1105    }
1106
1107    /// `to_degrees(half_pi) ~= 90` within `ANGLE_TOLERANCE_LSB`.
1108    #[test]
1109    fn to_degrees_half_pi_is_90() {
1110        if !crate::rounding::DEFAULT_IS_HALF_TO_EVEN { return; }
1111        let result = D38s12::half_pi().to_degrees();
1112        let expected = D38s12::from_int(90);
1113        assert!(
1114            within_lsb(result, expected, ANGLE_TOLERANCE_LSB),
1115            "to_degrees(half_pi) bits {}, expected 90 bits {} (delta {})",
1116            result.to_bits(),
1117            expected.to_bits(),
1118            (result.to_bits() - expected.to_bits()).abs(),
1119        );
1120    }
1121
1122    /// `to_degrees(quarter_pi) ~= 45` within `ANGLE_TOLERANCE_LSB`.
1123    #[test]
1124    fn to_degrees_quarter_pi_is_45() {
1125        let result = D38s12::quarter_pi().to_degrees();
1126        let expected = D38s12::from_int(45);
1127        assert!(
1128            within_lsb(result, expected, ANGLE_TOLERANCE_LSB),
1129            "to_degrees(quarter_pi) bits {}, expected 45 bits {} (delta {})",
1130            result.to_bits(),
1131            expected.to_bits(),
1132            (result.to_bits() - expected.to_bits()).abs(),
1133        );
1134    }
1135
1136    // ── Cross-method consistency ──────────────────────────────────────
1137
1138    /// `tan(x) ~= sin(x) / cos(x)` within 4 LSB for `x` away from
1139    /// odd multiples of `pi/2`.
1140    #[test]
1141    fn tan_matches_sin_over_cos() {
1142        for raw in [
1143            500_000_000_000_i128,    // 0.5
1144            -500_000_000_000_i128,   // -0.5
1145            1_000_000_000_000_i128,  // 1.0 (cos(1.0) ~= 0.54, safe)
1146            -1_000_000_000_000_i128, // -1.0
1147            123_456_789_012_i128,    // ~0.123456
1148        ] {
1149            let x = D38s12::from_bits(raw);
1150            let t = x.tan();
1151            let sc = x.sin() / x.cos();
1152            assert!(
1153                within_lsb(t, sc, FOUR_LSB),
1154                "tan(x) != sin/cos for raw={raw}: tan bits {}, sin/cos bits {}",
1155                t.to_bits(),
1156                sc.to_bits(),
1157            );
1158        }
1159    }
1160
1161    /// `tanh(x) ~= sinh(x) / cosh(x)` within 4 LSB. `cosh` is always
1162    /// positive so there is no divide-by-zero risk.
1163    #[test]
1164    fn tanh_matches_sinh_over_cosh() {
1165        for raw in [
1166            500_000_000_000_i128,    // 0.5
1167            -500_000_000_000_i128,   // -0.5
1168            1_234_567_890_123_i128,  // ~1.234567
1169            -2_345_678_901_234_i128, // ~-2.345678
1170        ] {
1171            let x = D38s12::from_bits(raw);
1172            let t = x.tanh();
1173            let sc = x.sinh() / x.cosh();
1174            assert!(
1175                within_lsb(t, sc, FOUR_LSB),
1176                "tanh(x) != sinh/cosh for raw={raw}: tanh bits {}, sinh/cosh bits {}",
1177                t.to_bits(),
1178                sc.to_bits(),
1179            );
1180        }
1181    }
1182}
1183