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            let hp = wide_half_pi(w);
264            let hp = if v.negative { hp.neg() } else { hp };
265            let raw = hp
266                .round_to_i128(w, SCALE)
267                .expect("D38::asin: result out of range");
268            return Self::from_bits(raw);
269        }
270        // Two-range kernel to keep the 0-ULP contract at |x| → 1.
271        // The half-angle branch uses asin(|x|) = π/2 − 2·asin(√((1−|x|)/2)).
272        let half_w = one_w.halve();
273        let asin_w = if !abs_v.ge_mag(half_w) {
274            // Stable range: |x| ≤ 0.5 → 1 − x² ∈ [0.75, 1], no cancellation.
275            let denom = one_w.sub(v.mul(v, w)).sqrt(w);
276            atan_fixed(v.div(denom, w), w)
277        } else {
278            // |x| > 0.5: recurse on √((1−|x|)/2) ∈ (0, 0.5].
279            let inner = one_w.sub(abs_v).halve();
280            let inner_sqrt = inner.sqrt(w);
281            let inner_denom = one_w.sub(inner_sqrt.mul(inner_sqrt, w)).sqrt(w);
282            let inner_asin = atan_fixed(inner_sqrt.div(inner_denom, w), w);
283            let result_abs = wide_half_pi(w).sub(inner_asin).sub(inner_asin);
284            if v.negative { result_abs.neg() } else { result_abs }
285        };
286        let raw = asin_w
287            .round_to_i128(w, SCALE)
288            .expect("D38::asin: result out of range");
289        Self::from_bits(raw)
290    }
291
292    /// Arccosine of `self`, in radians, in `[0, π]`. Strict:
293    /// `acos(x) = π/2 − asin(x)`, correctly rounded.
294    ///
295    /// # Panics
296    ///
297    /// Panics if `|self| > 1`.
298    #[inline]
299    #[must_use]
300    pub fn acos_strict(self) -> Self {
301        use crate::d_w128_kernels::Fixed;
302        let w = SCALE + crate::log_exp_strict::STRICT_GUARD;
303        let one_w = Fixed { negative: false, mag: Fixed::pow10(w) };
304        let v = to_fixed(self.0);
305        let abs_v = Fixed { negative: false, mag: v.mag };
306        assert!(!(abs_v.ge_mag(one_w) && abs_v != one_w), "D38::acos: argument out of domain [-1, 1]");
307        // Same two-range asin kernel as asin_strict; then π/2 − asin.
308        let half_w = one_w.halve();
309        let asin_w = if abs_v == one_w {
310            let hp = wide_half_pi(w);
311            if v.negative { hp.neg() } else { hp }
312        } else if !abs_v.ge_mag(half_w) {
313            let denom = one_w.sub(v.mul(v, w)).sqrt(w);
314            atan_fixed(v.div(denom, w), w)
315        } else {
316            let inner = one_w.sub(abs_v).halve();
317            let inner_sqrt = inner.sqrt(w);
318            let inner_denom = one_w.sub(inner_sqrt.mul(inner_sqrt, w)).sqrt(w);
319            let inner_asin = atan_fixed(inner_sqrt.div(inner_denom, w), w);
320            let result_abs = wide_half_pi(w).sub(inner_asin).sub(inner_asin);
321            if v.negative { result_abs.neg() } else { result_abs }
322        };
323        let raw = wide_half_pi(w)
324            .sub(asin_w)
325            .round_to_i128(w, SCALE)
326            .expect("D38::acos: result out of range");
327        Self::from_bits(raw)
328    }
329
330    /// Four-quadrant arctangent of `self` (`y`) and `other` (`x`), in
331    /// radians, in `(−π, π]`. Strict: integer-only and correctly
332    /// rounded.
333    #[inline]
334    #[must_use]
335    pub fn atan2_strict(self, other: Self) -> Self {
336        let w = SCALE + crate::log_exp_strict::STRICT_GUARD;
337        let y = to_fixed(self.0);
338        let x = to_fixed(other.0);
339        let result_w = if x.is_zero() {
340            if self.0 > 0 {
341                wide_half_pi(w)
342            } else if self.0 < 0 {
343                wide_half_pi(w).neg()
344            } else {
345                crate::d_w128_kernels::Fixed::ZERO
346            }
347        } else {
348            // Max-branch (same as the wide-tier path): feed atan_fixed
349            // the |smaller|/|larger| ratio so the argument-halving
350            // cascade in atan_fixed doesn't blow up when |y| ≫ |x|.
351            // Identity: atan(t) = sign(t)·π/2 − atan(1/t) for |t| > 1.
352            let abs_y_ge_abs_x = y.ge_mag(x);
353            let base = if !abs_y_ge_abs_x {
354                atan_fixed(y.div(x, w), w)
355            } else {
356                let inv = atan_fixed(x.div(y, w), w);
357                let hp = wide_half_pi(w);
358                // sign(y/x): positive iff y and x have matching sign.
359                let same_sign = y.negative == x.negative;
360                if same_sign { hp.sub(inv) } else { hp.neg().sub(inv) }
361            };
362            if !x.negative {
363                base
364            } else if !y.negative {
365                base.add(wide_pi(w))
366            } else {
367                base.sub(wide_pi(w))
368            }
369        };
370        let raw = result_w
371            .round_to_i128(w, SCALE)
372            .expect("D38::atan2: result out of range");
373        Self::from_bits(raw)
374    }
375
376    /// Hyperbolic sine of `self`. Strict: `sinh(x) = (eˣ − e⁻ˣ)/2`,
377    /// composed in the wide intermediate from the correctly-rounded
378    /// `exp`, so the result is itself correctly rounded.
379    #[inline]
380    #[must_use]
381    pub fn sinh_strict(self) -> Self {
382        let w = SCALE + crate::log_exp_strict::STRICT_GUARD;
383        let v = to_fixed(self.0);
384        let ex = crate::log_exp_strict::exp_fixed(v, w);
385        let enx = crate::log_exp_strict::exp_fixed(v.neg(), w);
386        let raw = ex
387            .sub(enx)
388            .halve()
389            .round_to_i128(w, SCALE)
390            .expect("D38::sinh: result out of range");
391        Self::from_bits(raw)
392    }
393
394    /// Hyperbolic cosine of `self`. Strict: `cosh(x) = (eˣ + e⁻ˣ)/2`,
395    /// correctly rounded.
396    #[inline]
397    #[must_use]
398    pub fn cosh_strict(self) -> Self {
399        let w = SCALE + crate::log_exp_strict::STRICT_GUARD;
400        let v = to_fixed(self.0);
401        let ex = crate::log_exp_strict::exp_fixed(v, w);
402        let enx = crate::log_exp_strict::exp_fixed(v.neg(), w);
403        let raw = ex
404            .add(enx)
405            .halve()
406            .round_to_i128(w, SCALE)
407            .expect("D38::cosh: result out of range");
408        Self::from_bits(raw)
409    }
410
411    /// Hyperbolic tangent of `self`. Strict: `tanh(x) = sinh(x)/cosh(x)`
412    /// with the division in the wide intermediate. `cosh ≥ 1`, so the
413    /// division never traps.
414    #[inline]
415    #[must_use]
416    pub fn tanh_strict(self) -> Self {
417        let w = SCALE + crate::log_exp_strict::STRICT_GUARD;
418        let v = to_fixed(self.0);
419        let ex = crate::log_exp_strict::exp_fixed(v, w);
420        let enx = crate::log_exp_strict::exp_fixed(v.neg(), w);
421        let raw = ex
422            .sub(enx)
423            .div(ex.add(enx), w)
424            .round_to_i128(w, SCALE)
425            .expect("D38::tanh: result out of range");
426        Self::from_bits(raw)
427    }
428
429    /// Inverse hyperbolic sine of `self`. Strict:
430    /// `asinh(x) = sign · ln(|x| + √(x² + 1))`, correctly rounded.
431    /// For `|x| ≥ 1` the radicand is factored as
432    /// `|x|·(1 + √(1 + 1/x²))` to keep `x²` from overflowing the wide
433    /// intermediate.
434    #[inline]
435    #[must_use]
436    pub fn asinh_strict(self) -> Self {
437        use crate::d_w128_kernels::Fixed;
438        if self.0 == 0 {
439            return Self::ZERO;
440        }
441        let w = SCALE + crate::log_exp_strict::STRICT_GUARD;
442        let one_w = Fixed { negative: false, mag: Fixed::pow10(w) };
443        let v = to_fixed(self.0);
444        let ax = Fixed { negative: false, mag: v.mag };
445        let inner = if ax.ge_mag(one_w) {
446            // ln(|x|) + ln(1 + √(1 + 1/x²)).
447            let inv = one_w.div(ax, w);
448            let root = one_w.add(inv.mul(inv, w)).sqrt(w);
449            crate::log_exp_strict::ln_fixed(ax, w).add(crate::log_exp_strict::ln_fixed(one_w.add(root), w))
450        } else {
451            // ln(|x| + √(x² + 1)).
452            let root = ax.mul(ax, w).add(one_w).sqrt(w);
453            crate::log_exp_strict::ln_fixed(ax.add(root), w)
454        };
455        let signed = if self.0 < 0 { inner.neg() } else { inner };
456        let raw = signed
457            .round_to_i128(w, SCALE)
458            .expect("D38::asinh: result out of range");
459        Self::from_bits(raw)
460    }
461
462    /// Inverse hyperbolic cosine of `self`. Strict:
463    /// `acosh(x) = ln(x + √(x² − 1))`, defined for `x ≥ 1`, correctly
464    /// rounded. For `x ≥ 2` the radicand is factored as
465    /// `x·(1 + √(1 − 1/x²))` to keep `x²` in range.
466    ///
467    /// # Panics
468    ///
469    /// Panics if `self < 1`.
470    #[inline]
471    #[must_use]
472    pub fn acosh_strict(self) -> Self {
473        use crate::d_w128_kernels::Fixed;
474        let w = SCALE + crate::log_exp_strict::STRICT_GUARD;
475        let one_w = Fixed { negative: false, mag: Fixed::pow10(w) };
476        let v = to_fixed(self.0);
477        assert!(!v.negative && v.ge_mag(one_w), "D38::acosh: argument must be >= 1");
478        let two_w = one_w.double();
479        let inner = if v.ge_mag(two_w) {
480            // ln(x) + ln(1 + √(1 − 1/x²)).
481            let inv = one_w.div(v, w);
482            let root = one_w.sub(inv.mul(inv, w)).sqrt(w);
483            crate::log_exp_strict::ln_fixed(v, w).add(crate::log_exp_strict::ln_fixed(one_w.add(root), w))
484        } else {
485            // ln(x + √(x² − 1)).
486            let root = v.mul(v, w).sub(one_w).sqrt(w);
487            crate::log_exp_strict::ln_fixed(v.add(root), w)
488        };
489        let raw = inner
490            .round_to_i128(w, SCALE)
491            .expect("D38::acosh: result out of range");
492        Self::from_bits(raw)
493    }
494
495    /// Inverse hyperbolic tangent of `self`. Strict:
496    /// `atanh(x) = ln((1 + x) / (1 − x)) / 2`, defined for `|x| < 1`,
497    /// correctly rounded.
498    ///
499    /// # Panics
500    ///
501    /// Panics if `|self| >= 1`.
502    #[inline]
503    #[must_use]
504    pub fn atanh_strict(self) -> Self {
505        use crate::d_w128_kernels::Fixed;
506        let w = SCALE + crate::log_exp_strict::STRICT_GUARD;
507        let one_w = Fixed { negative: false, mag: Fixed::pow10(w) };
508        let v = to_fixed(self.0);
509        let ax = Fixed { negative: false, mag: v.mag };
510        assert!(!ax.ge_mag(one_w), "D38::atanh: argument out of domain (-1, 1)");
511        // ln((1 + x) / (1 − x)) / 2.
512        let ratio = one_w.add(v).div(one_w.sub(v), w);
513        let raw = crate::log_exp_strict::ln_fixed(ratio, w)
514            .halve()
515            .round_to_i128(w, SCALE)
516            .expect("D38::atanh: result out of range");
517        Self::from_bits(raw)
518    }
519
520    /// Convert radians to degrees: `self · (180 / π)`. Strict: the
521    /// multiply and divide run in the wide intermediate, so the result
522    /// is correctly rounded.
523    #[inline]
524    #[must_use]
525    pub fn to_degrees_strict(self) -> Self {
526        let w = SCALE + crate::log_exp_strict::STRICT_GUARD;
527        let raw = to_fixed(self.0)
528            .mul_u128(180)
529            .div(wide_pi(w), w)
530            .round_to_i128(w, SCALE)
531            .expect("D38::to_degrees: result out of range");
532        Self::from_bits(raw)
533    }
534
535    /// Convert degrees to radians: `self · (π / 180)`. Strict:
536    /// correctly rounded.
537    #[inline]
538    #[must_use]
539    pub fn to_radians_strict(self) -> Self {
540        let w = SCALE + crate::log_exp_strict::STRICT_GUARD;
541        let raw = to_fixed(self.0)
542            .mul(wide_pi(w), w)
543            .div_small(180)
544            .round_to_i128(w, SCALE)
545            .expect("D38::to_radians: result out of range");
546        Self::from_bits(raw)
547    }
548}
549
550
551// ─────────────────────────────────────────────────────────────────────
552// Strict-mode (integer-only) trigonometric, hyperbolic, and angle-
553// conversion methods.
554//
555// These mirror the f64-bridge surface above but are integer-only,
556// `no_std`-compatible, and **correctly rounded** — within 0.5 ULP of
557// the exact result. Every reduction and series step runs in the
558// `d_w128_kernels::Fixed` guard-digit intermediate (the same machinery the
559// log/exp family uses) and the value is rounded once at the end.
560//
561// Composition strategy:
562//
563// - Hyperbolic functions are composed from the strict `exp` / `ln` /
564// `sqrt` already implemented in `log_exp_strict.rs` / `powers_strict.rs`.
565// - `cos` is `sin` phase-shifted by π/2; `tan` is `sin / cos`.
566// - `sin` uses range reduction modulo τ into one π/2 octant followed by
567// a Taylor series.
568// - `atan` uses reciprocal reduction for |x| > 1 plus argument halving,
569// then a Taylor series; `asin` / `acos` / `atan2` are derived from it.
570// ─────────────────────────────────────────────────────────────────────
571
572// Strict-feature dispatchers. When `strict` is enabled (and
573// `fast` is not), the plain trig methods route to the
574// integer-only `*_strict` implementations below.
575
576// ─────────────────────────────────────────────────────────────────────
577// Correctly-rounded strict trigonometric core.
578//
579// Every strict trig method runs on the shared `d_w128_kernels::Fixed`
580// guard-digit intermediate at `SCALE + STRICT_GUARD` working digits,
581// the same machinery the log/exp family uses, and rounds once at the
582// end — so each result is within 0.5 ULP of the exact value.
583// ─────────────────────────────────────────────────────────────────────
584
585/// π at working scale `w`, sourced from the crate-wide 75-digit
586/// `consts::PI_RAW` (Int256 holding `π × 10^75`).
587///
588/// Caller-side precondition: `w <= 75`. The D38 strict trig kernel
589/// runs at `w = SCALE + STRICT_GUARD`, capped at `38 + 30 = 68`, so
590/// the bound is satisfied by every call site in this module. The
591/// debug-assert documents the invariant for any future caller; in
592/// release, `rescale_down(75, w > 75)` would silently produce a
593/// wrong π via the wrapping `from_w − to_w` subtraction.
594fn wide_pi(w: u32) -> crate::d_w128_kernels::Fixed {
595    debug_assert!(w <= 75, "wide_pi: working scale {w} exceeds embedded 75-digit π");
596    // PI_RAW is an Int256, internally [u64; 4]. The D38 Fixed kernel
597    // expects [u128; 2]; repack pairs of u64 limbs into u128.
598    let words = crate::consts::PI_RAW.0;
599    let pi_at_75 = crate::d_w128_kernels::Fixed {
600        negative: false,
601        mag: [
602            (words[0] as u128) | ((words[1] as u128) << 64),
603            (words[2] as u128) | ((words[3] as u128) << 64),
604        ],
605    };
606    if w == 75 {
607        pi_at_75
608    } else {
609        pi_at_75.rescale_down(75, w)
610    }
611}
612
613/// τ = 2π at working scale `w`.
614fn wide_tau(w: u32) -> crate::d_w128_kernels::Fixed {
615    wide_pi(w).double()
616}
617
618/// π/2 at working scale `w`.
619fn wide_half_pi(w: u32) -> crate::d_w128_kernels::Fixed {
620    wide_pi(w).halve()
621}
622
623/// Builds a working-scale `Fixed` from a signed `D38` raw value `r`:
624/// `r · 10^STRICT_GUARD`, carrying the sign.
625fn to_fixed(raw: i128) -> crate::d_w128_kernels::Fixed {
626    use crate::d_w128_kernels::Fixed;
627    let m = Fixed::from_u128_mag(raw.unsigned_abs(), false)
628        .mul_u128(10u128.pow(crate::log_exp_strict::STRICT_GUARD));
629    if raw < 0 {
630        m.neg()
631    } else {
632        m
633    }
634}
635
636/// Taylor series for `sin` on a reduced non-negative argument
637/// `r ∈ [0, π/2]`, evaluated at working scale `w`.
638fn sin_taylor(r: crate::d_w128_kernels::Fixed, w: u32) -> crate::d_w128_kernels::Fixed {
639    let r2 = r.mul(r, w);
640    let mut sum = r;
641    let mut term = r; // term = r^(2k-1)
642    let mut k: u128 = 1;
643    loop {
644        // term_k = term_{k-1} · r² / ((2k)(2k+1)); sign alternates.
645        term = term.mul(r2, w).div_small((2 * k) * (2 * k + 1));
646        if term.is_zero() {
647            break;
648        }
649        if k % 2 == 1 {
650            sum = sum.sub(term);
651        } else {
652            sum = sum.add(term);
653        }
654        k += 1;
655        if k > 200 {
656            break;
657        }
658    }
659    sum
660}
661
662/// Sine of a working-scale value `v_w`, at working scale `w`.
663///
664/// Reduces `v` modulo τ via `q = round(v/τ)`, folds the remainder into
665/// `[0, π/2]` tracking sign and the `π − x` reflection, then evaluates
666/// the Taylor series.
667fn sin_fixed(v_w: crate::d_w128_kernels::Fixed, w: u32) -> crate::d_w128_kernels::Fixed {
668    use crate::d_w128_kernels::Fixed;
669    let tau = wide_tau(w);
670    let pi = wide_pi(w);
671    let half_pi = wide_half_pi(w);
672
673    // r = v - round(v/τ)·τ ∈ [-π, π].
674    let q = v_w.div(tau, w).round_to_nearest_int(w);
675    let q_tau = if q >= 0 {
676        tau.mul_u128(q as u128)
677    } else {
678        tau.mul_u128((-q) as u128).neg()
679    };
680    let r = v_w.sub(q_tau);
681
682    // Fold |r| ∈ [0, π] into [0, π/2] via sin(π − x) = sin(x).
683    let sign = r.negative;
684    let abs_r = Fixed { negative: false, mag: r.mag };
685    let reduced = if abs_r.ge_mag(half_pi) {
686        pi.sub(abs_r)
687    } else {
688        abs_r
689    };
690    let s = sin_taylor(reduced, w);
691    if sign {
692        s.neg()
693    } else {
694        s
695    }
696}
697
698/// Taylor series for `atan` on a reduced non-negative argument
699/// `x ∈ [0, ~1/8]`, evaluated at working scale `w`.
700fn atan_taylor(x: crate::d_w128_kernels::Fixed, w: u32) -> crate::d_w128_kernels::Fixed {
701    let x2 = x.mul(x, w);
702    let mut sum = x;
703    let mut term = x; // term = x^(2k-1)
704    let mut k: u128 = 1;
705    loop {
706        term = term.mul(x2, w);
707        let contrib = term.div_small(2 * k + 1);
708        if contrib.is_zero() {
709            break;
710        }
711        if k % 2 == 1 {
712            sum = sum.sub(contrib);
713        } else {
714            sum = sum.add(contrib);
715        }
716        k += 1;
717        if k > 300 {
718            break;
719        }
720    }
721    sum
722}
723
724/// Arctangent of a working-scale value `v_w`, at working scale `w`,
725/// result in `(−π/2, π/2)`.
726///
727/// Odd-function fold to `x ≥ 0`; reciprocal reduction
728/// `atan(x) = π/2 − atan(1/x)` for `x > 1`; three rounds of argument
729/// halving `atan(x) = 2·atan(x / (1 + √(1+x²)))`; then the series.
730fn atan_fixed(v_w: crate::d_w128_kernels::Fixed, w: u32) -> crate::d_w128_kernels::Fixed {
731    use crate::d_w128_kernels::Fixed;
732    let one_w = Fixed { negative: false, mag: Fixed::pow10(w) };
733    let sign = v_w.negative;
734    let mut x = Fixed { negative: false, mag: v_w.mag };
735    let mut add_half_pi = false;
736    if x.ge_mag(one_w) && x != one_w {
737        x = one_w.div(x, w); // atan(x) = π/2 − atan(1/x)
738        add_half_pi = true;
739    }
740    // Three argument halvings: atan(x) = 2·atan(x / (1 + √(1+x²))).
741    let halvings: u32 = 3;
742    for _ in 0..halvings {
743        let x2 = x.mul(x, w);
744        let denom = one_w.add(one_w.add(x2).sqrt(w));
745        x = x.div(denom, w);
746    }
747    let mut result = atan_taylor(x, w);
748    result = result.shl(halvings);
749    if add_half_pi {
750        result = wide_half_pi(w).sub(result);
751    }
752    if sign {
753        result.neg()
754    } else {
755        result
756    }
757}
758
759#[cfg(test)]
760mod tests {
761    use crate::consts::DecimalConsts;
762    use crate::core_type::D38s12;
763
764    // Tolerance for single-operation results. The f64-bridge build is
765    // one f64 round-trip (≤ 2 LSB); the integer-only `strict` build is
766    // correctly rounded (≤ 0.5 ULP per call) and is held to the same
767    // 2-LSB bound — a couple of LSB for the test's own expected-value
768    // rounding.
769    const TWO_LSB: i128 = 2;
770
771    // Tolerance for results that chain multiple trig calls (e.g.
772    // `sin² + cos²`, `cosh² − sinh²`): each input is within 0.5 ULP, so
773    // the composed quantity stays within a few LSB in both builds.
774    const FOUR_LSB: i128 = 4;
775
776    // Angle-conversion results compared against exact integer targets
777    // (180, 90, 45 degrees). The `pi()` / `quarter_pi()` *input*
778    // constants are themselves rounded to the type's scale, and
779    // `to_degrees` amplifies that input quantization by ~57.3 — so even
780    // a perfectly-rounded conversion lands ~30 LSB off the exact
781    // integer at SCALE = 12. (This bounds the *input*, not the
782    // conversion: `to_degrees` itself is correctly rounded in `strict`.)
783    const ANGLE_TOLERANCE_LSB: i128 = 32;
784
785    fn within_lsb(actual: D38s12, expected: D38s12, lsb: i128) -> bool {
786        let diff = (actual.to_bits() - expected.to_bits()).abs();
787        diff <= lsb
788    }
789
790    // ── Forward trig ──────────────────────────────────────────────────
791
792    /// The strict trig / hyperbolic family is correctly rounded:
793    /// cross-check every method against the f64 bridge at D38<9>,
794    /// where f64 (≈ 15–16 significant digits) is comfortably more
795    /// precise than the type's ULP, so a correctly-rounded integer
796    /// result must agree to within 1 ULP (allow 1 more for the f64
797    /// reference's own rounding).
798    #[cfg(all(feature = "strict", not(feature = "fast")))]
799    #[test]
800    fn strict_trig_family_matches_f64() {
801        use crate::core_type::D38;
802        macro_rules! check {
803            ($name:literal, $raw:expr, $strict:expr, $f64expr:expr) => {{
804                let strict: i128 = $strict;
805                let v = $raw as f64 / 1e9;
806                let reference = ($f64expr(v) * 1e9).round() as i128;
807                assert!(
808                    (strict - reference).abs() <= 2,
809                    concat!($name, "({}) = {}, f64 reference {}"),
810                    $raw,
811                    strict,
812                    reference
813                );
814            }};
815        }
816        // Forward trig — arguments across a few periods, incl. negative.
817        for &raw in &[
818            -7_000_000_000_i128, -1_000_000_000, -100_000_000, 1,
819            500_000_000, 1_000_000_000, 1_570_796_327, 3_000_000_000,
820            6_283_185_307, 12_000_000_000,
821        ] {
822            let x = D38::<9>::from_bits(raw);
823            check!("sin", raw, x.sin_strict().to_bits(), f64::sin);
824            check!("cos", raw, x.cos_strict().to_bits(), f64::cos);
825            check!("atan", raw, x.atan_strict().to_bits(), f64::atan);
826            check!("sinh", raw, x.sinh_strict().to_bits(), f64::sinh);
827            check!("cosh", raw, x.cosh_strict().to_bits(), f64::cosh);
828            check!("tanh", raw, x.tanh_strict().to_bits(), f64::tanh);
829            check!("asinh", raw, x.asinh_strict().to_bits(), f64::asinh);
830        }
831        // asin / acos — domain [-1, 1].
832        for &raw in &[
833            -1_000_000_000_i128, -700_000_000, -100_000_000, 0,
834            250_000_000, 500_000_000, 999_999_999,
835        ] {
836            let x = D38::<9>::from_bits(raw);
837            check!("asin", raw, x.asin_strict().to_bits(), f64::asin);
838            check!("acos", raw, x.acos_strict().to_bits(), f64::acos);
839        }
840        // atanh — domain (-1, 1).
841        for &raw in &[-900_000_000_i128, -300_000_000, 1, 300_000_000, 900_000_000] {
842            let x = D38::<9>::from_bits(raw);
843            check!("atanh", raw, x.atanh_strict().to_bits(), f64::atanh);
844        }
845        // acosh — domain [1, ∞).
846        for &raw in &[1_000_000_000_i128, 1_500_000_000, 3_000_000_000, 50_000_000_000] {
847            let x = D38::<9>::from_bits(raw);
848            check!("acosh", raw, x.acosh_strict().to_bits(), f64::acosh);
849        }
850        // tan — avoid the poles.
851        for &raw in &[-1_000_000_000_i128, 1, 500_000_000, 1_000_000_000, 1_400_000_000] {
852            let x = D38::<9>::from_bits(raw);
853            check!("tan", raw, x.tan_strict().to_bits(), f64::tan);
854        }
855    }
856
857    /// `sin(0) == 0` -- bit-exact via `f64::sin(0.0) == 0.0`.
858    #[test]
859    fn sin_zero_is_zero() {
860        assert_eq!(D38s12::ZERO.sin(), D38s12::ZERO);
861    }
862
863    /// Regression: D38 strict trig at high SCALE drives the working
864    /// scale `w = SCALE + STRICT_GUARD` past the old hard-coded
865    /// 63-digit π constant. With the previous wide_pi the
866    /// `rescale_down(63, w > 63)` call wrapped `from_w - to_w` as u32
867    /// and produced a silently-wrong π. Verify two representative
868    /// SCALEs above the old window (working scale 65 and 67) land
869    /// within 1 LSB of the high-precision canonical sin(1) value.
870    ///
871    /// Reference: sin(1) = 0.8414709848078965066525023216302989996226...
872    /// (OEIS A049469, 40 digits).
873    #[cfg(all(feature = "strict", not(feature = "fast")))]
874    #[test]
875    fn sin_one_correct_past_63_digit_pi_window() {
876        use crate::core_type::D38;
877        // sin(1) × 10^35, rounded half-to-even:
878        // 0.84147098480789650665250232163029899|96226... → ...029900
879        let expected_35: i128 = 84_147_098_480_789_650_665_250_232_163_029_900;
880        // sin(1) × 10^37, rounded:
881        // 0.8414709848078965066525023216302989996|226... → ...02989996
882        let expected_37: i128 =
883            8_414_709_848_078_965_066_525_023_216_302_989_996;
884
885        let got_35 = D38::<35>::ONE.sin_strict().to_bits();
886        assert!(
887            (got_35 - expected_35).abs() <= 1,
888            "sin(1) @ D38<35>: got {got_35}, expected {expected_35}"
889        );
890
891        let got_37 = D38::<37>::ONE.sin_strict().to_bits();
892        assert!(
893            (got_37 - expected_37).abs() <= 1,
894            "sin(1) @ D38<37>: got {got_37}, expected {expected_37}"
895        );
896    }
897
898    /// `cos(0) == 1` -- bit-exact via `f64::cos(0.0) == 1.0`.
899    #[test]
900    fn cos_zero_is_one() {
901        assert_eq!(D38s12::ZERO.cos(), D38s12::ONE);
902    }
903
904    /// `tan(0) == 0` -- bit-exact via `f64::tan(0.0) == 0.0`.
905    #[test]
906    fn tan_zero_is_zero() {
907        assert_eq!(D38s12::ZERO.tan(), D38s12::ZERO);
908    }
909
910    /// Pythagorean identity: `sin^2(x) + cos^2(x) ~= 1` within 4 LSB
911    /// for representative values of `x`. Values are chosen to be well
912    /// away from any well-known mathematical constant.
913    #[test]
914    fn sin_squared_plus_cos_squared_is_one() {
915        for raw in [
916            1_234_567_890_123_i128,  // ~1.234567...
917            -2_345_678_901_234_i128, // ~-2.345678...
918            500_000_000_000_i128,    // 0.5
919            -500_000_000_000_i128,   // -0.5
920            4_567_891_234_567_i128,  // ~4.567891...
921        ] {
922            let x = D38s12::from_bits(raw);
923            let s = x.sin();
924            let c = x.cos();
925            let sum = (s * s) + (c * c);
926            assert!(
927                within_lsb(sum, D38s12::ONE, FOUR_LSB),
928                "sin^2 + cos^2 != 1 for raw={raw}: got bits {} (delta {})",
929                sum.to_bits(),
930                (sum.to_bits() - D38s12::ONE.to_bits()).abs(),
931            );
932        }
933    }
934
935    // ── Inverse trig ──────────────────────────────────────────────────
936
937    /// `asin(0) == 0` -- bit-exact.
938    #[test]
939    fn asin_zero_is_zero() {
940        assert_eq!(D38s12::ZERO.asin(), D38s12::ZERO);
941    }
942
943    /// `acos(1) == 0` -- bit-exact via `f64::acos(1.0) == 0.0`.
944    #[test]
945    fn acos_one_is_zero() {
946        assert_eq!(D38s12::ONE.acos(), D38s12::ZERO);
947    }
948
949    /// `acos(0) ~= pi/2` within 4 LSB.
950    #[test]
951    fn acos_zero_is_half_pi() {
952        let result = D38s12::ZERO.acos();
953        assert!(
954            within_lsb(result, D38s12::half_pi(), FOUR_LSB),
955            "acos(0) bits {}, half_pi bits {}",
956            result.to_bits(),
957            D38s12::half_pi().to_bits(),
958        );
959    }
960
961    /// `atan(0) == 0` -- bit-exact via `f64::atan(0.0) == 0.0`.
962    #[test]
963    fn atan_zero_is_zero() {
964        assert_eq!(D38s12::ZERO.atan(), D38s12::ZERO);
965    }
966
967    /// Round-trip identity: `asin(sin(x)) ~= x` for `x` in
968    /// `[-pi/2, pi/2]`. Values stay within the principal branch.
969    #[test]
970    fn asin_of_sin_round_trip() {
971        for raw in [
972            123_456_789_012_i128,    // ~0.123456...
973            -123_456_789_012_i128,   // ~-0.123456...
974            456_789_012_345_i128,    // ~0.456789...
975            -456_789_012_345_i128,   // ~-0.456789...
976            1_234_567_890_123_i128,  // ~1.234567... (well inside pi/2)
977            -1_234_567_890_123_i128, // ~-1.234567...
978        ] {
979            let x = D38s12::from_bits(raw);
980            let recovered = x.sin().asin();
981            assert!(
982                within_lsb(recovered, x, FOUR_LSB),
983                "asin(sin(x)) != x for raw={raw}: got bits {} (delta {})",
984                recovered.to_bits(),
985                (recovered.to_bits() - x.to_bits()).abs(),
986            );
987        }
988    }
989
990    // ── atan2 ─────────────────────────────────────────────────────────
991
992    /// `atan2(1, 1) ~= pi/4` (first-quadrant 45 degrees).
993    #[test]
994    fn atan2_first_quadrant_diagonal() {
995        let one = D38s12::ONE;
996        let result = one.atan2(one);
997        assert!(
998            within_lsb(result, D38s12::quarter_pi(), TWO_LSB),
999            "atan2(1, 1) bits {}, quarter_pi bits {}",
1000            result.to_bits(),
1001            D38s12::quarter_pi().to_bits(),
1002        );
1003    }
1004
1005    /// `atan2(-1, -1) ~= -3*pi/4` (third-quadrant correctness).
1006    #[test]
1007    fn atan2_third_quadrant_diagonal() {
1008        let neg_one = -D38s12::ONE;
1009        let result = neg_one.atan2(neg_one);
1010        let three = D38s12::from_int(3);
1011        let expected = -(D38s12::quarter_pi() * three);
1012        assert!(
1013            within_lsb(result, expected, TWO_LSB),
1014            "atan2(-1, -1) bits {}, expected -3pi/4 bits {}",
1015            result.to_bits(),
1016            expected.to_bits(),
1017        );
1018    }
1019
1020    /// `atan2(1, -1) ~= 3*pi/4` (second-quadrant correctness).
1021    #[test]
1022    fn atan2_second_quadrant_diagonal() {
1023        let one = D38s12::ONE;
1024        let neg_one = -D38s12::ONE;
1025        let result = one.atan2(neg_one);
1026        let three = D38s12::from_int(3);
1027        let expected = D38s12::quarter_pi() * three;
1028        assert!(
1029            within_lsb(result, expected, TWO_LSB),
1030            "atan2(1, -1) bits {}, expected 3pi/4 bits {}",
1031            result.to_bits(),
1032            expected.to_bits(),
1033        );
1034    }
1035
1036    /// `atan2(-1, 1) ~= -pi/4` (fourth-quadrant correctness).
1037    #[test]
1038    fn atan2_fourth_quadrant_diagonal() {
1039        let one = D38s12::ONE;
1040        let neg_one = -D38s12::ONE;
1041        let result = neg_one.atan2(one);
1042        let expected = -D38s12::quarter_pi();
1043        assert!(
1044            within_lsb(result, expected, TWO_LSB),
1045            "atan2(-1, 1) bits {}, expected -pi/4 bits {}",
1046            result.to_bits(),
1047            expected.to_bits(),
1048        );
1049    }
1050
1051    /// `atan2(0, 1) == 0` (positive x-axis is bit-exact).
1052    #[test]
1053    fn atan2_positive_x_axis_is_zero() {
1054        let zero = D38s12::ZERO;
1055        let one = D38s12::ONE;
1056        assert_eq!(zero.atan2(one), D38s12::ZERO);
1057    }
1058
1059    // ── Hyperbolic ────────────────────────────────────────────────────
1060
1061    /// `sinh(0) == 0` -- bit-exact via `f64::sinh(0.0) == 0.0`.
1062    #[test]
1063    fn sinh_zero_is_zero() {
1064        assert_eq!(D38s12::ZERO.sinh(), D38s12::ZERO);
1065    }
1066
1067    /// `cosh(0) == 1` -- bit-exact via `f64::cosh(0.0) == 1.0`.
1068    #[test]
1069    fn cosh_zero_is_one() {
1070        assert_eq!(D38s12::ZERO.cosh(), D38s12::ONE);
1071    }
1072
1073    /// `tanh(0) == 0` -- bit-exact via `f64::tanh(0.0) == 0.0`.
1074    #[test]
1075    fn tanh_zero_is_zero() {
1076        assert_eq!(D38s12::ZERO.tanh(), D38s12::ZERO);
1077    }
1078
1079    /// `asinh(0) == 0` -- bit-exact.
1080    #[test]
1081    fn asinh_zero_is_zero() {
1082        assert_eq!(D38s12::ZERO.asinh(), D38s12::ZERO);
1083    }
1084
1085    /// `acosh(1) == 0` -- bit-exact via `f64::acosh(1.0) == 0.0`.
1086    #[test]
1087    fn acosh_one_is_zero() {
1088        assert_eq!(D38s12::ONE.acosh(), D38s12::ZERO);
1089    }
1090
1091    /// `atanh(0) == 0` -- bit-exact.
1092    #[test]
1093    fn atanh_zero_is_zero() {
1094        assert_eq!(D38s12::ZERO.atanh(), D38s12::ZERO);
1095    }
1096
1097    /// Identity: `cosh^2(x) - sinh^2(x) == 1` within 4 LSB for
1098    /// representative values of `x`.
1099    #[test]
1100    fn cosh_squared_minus_sinh_squared_is_one() {
1101        if !crate::rounding::DEFAULT_IS_HALF_TO_EVEN { return; }
1102        for raw in [
1103            500_000_000_000_i128,    // 0.5
1104            -500_000_000_000_i128,   // -0.5
1105            1_234_567_890_123_i128,  // ~1.234567
1106            -1_234_567_890_123_i128, // ~-1.234567
1107            2_500_000_000_000_i128,  // 2.5
1108        ] {
1109            let x = D38s12::from_bits(raw);
1110            let ch = x.cosh();
1111            let sh = x.sinh();
1112            let diff = (ch * ch) - (sh * sh);
1113            assert!(
1114                within_lsb(diff, D38s12::ONE, FOUR_LSB),
1115                "cosh^2 - sinh^2 != 1 for raw={raw}: got bits {} (delta {})",
1116                diff.to_bits(),
1117                (diff.to_bits() - D38s12::ONE.to_bits()).abs(),
1118            );
1119        }
1120    }
1121
1122    // ── Angle conversions ─────────────────────────────────────────────
1123
1124    /// `to_degrees(pi) ~= 180` within `ANGLE_TOLERANCE_LSB`. The
1125    /// tolerance is dominated by f64's limited precision on `pi`,
1126    /// amplified by ~57.3 during the degrees conversion.
1127    #[test]
1128    fn to_degrees_pi_is_180() {
1129        if !crate::rounding::DEFAULT_IS_HALF_TO_EVEN { return; }
1130        let pi = D38s12::pi();
1131        let result = pi.to_degrees();
1132        let expected = D38s12::from_int(180);
1133        assert!(
1134            within_lsb(result, expected, ANGLE_TOLERANCE_LSB),
1135            "to_degrees(pi) bits {}, expected 180 bits {} (delta {})",
1136            result.to_bits(),
1137            expected.to_bits(),
1138            (result.to_bits() - expected.to_bits()).abs(),
1139        );
1140    }
1141
1142    /// `to_radians(180) ~= pi` within `ANGLE_TOLERANCE_LSB`.
1143    #[test]
1144    fn to_radians_180_is_pi() {
1145        let one_eighty = D38s12::from_int(180);
1146        let result = one_eighty.to_radians();
1147        let expected = D38s12::pi();
1148        assert!(
1149            within_lsb(result, expected, ANGLE_TOLERANCE_LSB),
1150            "to_radians(180) bits {}, expected pi bits {} (delta {})",
1151            result.to_bits(),
1152            expected.to_bits(),
1153            (result.to_bits() - expected.to_bits()).abs(),
1154        );
1155    }
1156
1157    /// `to_degrees(0) == 0` -- bit-exact (0 * anything == 0).
1158    #[test]
1159    fn to_degrees_zero_is_zero() {
1160        assert_eq!(D38s12::ZERO.to_degrees(), D38s12::ZERO);
1161    }
1162
1163    /// `to_radians(0) == 0` -- bit-exact.
1164    #[test]
1165    fn to_radians_zero_is_zero() {
1166        assert_eq!(D38s12::ZERO.to_radians(), D38s12::ZERO);
1167    }
1168
1169    /// Round-trip: `to_radians(to_degrees(x)) ~= x` within 4 LSB
1170    /// (two f64 round-trips).
1171    #[test]
1172    fn to_radians_to_degrees_round_trip() {
1173        for raw in [
1174            500_000_000_000_i128,    // 0.5
1175            -500_000_000_000_i128,   // -0.5
1176            1_234_567_890_123_i128,  // ~1.234567
1177            -2_345_678_901_234_i128, // ~-2.345678
1178        ] {
1179            let x = D38s12::from_bits(raw);
1180            let recovered = x.to_degrees().to_radians();
1181            assert!(
1182                within_lsb(recovered, x, FOUR_LSB),
1183                "to_radians(to_degrees(x)) != x for raw={raw}: got bits {} (delta {})",
1184                recovered.to_bits(),
1185                (recovered.to_bits() - x.to_bits()).abs(),
1186            );
1187        }
1188    }
1189
1190    /// `to_degrees(half_pi) ~= 90` within `ANGLE_TOLERANCE_LSB`.
1191    #[test]
1192    fn to_degrees_half_pi_is_90() {
1193        if !crate::rounding::DEFAULT_IS_HALF_TO_EVEN { return; }
1194        let result = D38s12::half_pi().to_degrees();
1195        let expected = D38s12::from_int(90);
1196        assert!(
1197            within_lsb(result, expected, ANGLE_TOLERANCE_LSB),
1198            "to_degrees(half_pi) bits {}, expected 90 bits {} (delta {})",
1199            result.to_bits(),
1200            expected.to_bits(),
1201            (result.to_bits() - expected.to_bits()).abs(),
1202        );
1203    }
1204
1205    /// `to_degrees(quarter_pi) ~= 45` within `ANGLE_TOLERANCE_LSB`.
1206    #[test]
1207    fn to_degrees_quarter_pi_is_45() {
1208        let result = D38s12::quarter_pi().to_degrees();
1209        let expected = D38s12::from_int(45);
1210        assert!(
1211            within_lsb(result, expected, ANGLE_TOLERANCE_LSB),
1212            "to_degrees(quarter_pi) bits {}, expected 45 bits {} (delta {})",
1213            result.to_bits(),
1214            expected.to_bits(),
1215            (result.to_bits() - expected.to_bits()).abs(),
1216        );
1217    }
1218
1219    // ── Cross-method consistency ──────────────────────────────────────
1220
1221    /// `tan(x) ~= sin(x) / cos(x)` within 4 LSB for `x` away from
1222    /// odd multiples of `pi/2`.
1223    #[test]
1224    fn tan_matches_sin_over_cos() {
1225        for raw in [
1226            500_000_000_000_i128,    // 0.5
1227            -500_000_000_000_i128,   // -0.5
1228            1_000_000_000_000_i128,  // 1.0 (cos(1.0) ~= 0.54, safe)
1229            -1_000_000_000_000_i128, // -1.0
1230            123_456_789_012_i128,    // ~0.123456
1231        ] {
1232            let x = D38s12::from_bits(raw);
1233            let t = x.tan();
1234            let sc = x.sin() / x.cos();
1235            assert!(
1236                within_lsb(t, sc, FOUR_LSB),
1237                "tan(x) != sin/cos for raw={raw}: tan bits {}, sin/cos bits {}",
1238                t.to_bits(),
1239                sc.to_bits(),
1240            );
1241        }
1242    }
1243
1244    /// `tanh(x) ~= sinh(x) / cosh(x)` within 4 LSB. `cosh` is always
1245    /// positive so there is no divide-by-zero risk.
1246    #[test]
1247    fn tanh_matches_sinh_over_cosh() {
1248        for raw in [
1249            500_000_000_000_i128,    // 0.5
1250            -500_000_000_000_i128,   // -0.5
1251            1_234_567_890_123_i128,  // ~1.234567
1252            -2_345_678_901_234_i128, // ~-2.345678
1253        ] {
1254            let x = D38s12::from_bits(raw);
1255            let t = x.tanh();
1256            let sc = x.sinh() / x.cosh();
1257            assert!(
1258                within_lsb(t, sc, FOUR_LSB),
1259                "tanh(x) != sinh/cosh for raw={raw}: tanh bits {}, sinh/cosh bits {}",
1260                t.to_bits(),
1261                sc.to_bits(),
1262            );
1263        }
1264    }
1265}
1266