Skip to main content

decimal_scaled/
trig_strict.rs

1//! Trigonometric, hyperbolic, and angle-conversion methods for [`D38`].
2//!
3//! # Surface
4//!
5//! Fifteen mathematical functions:
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 four-variant matrix
16//!
17//! Each function ships with four entry points so a single name covers
18//! every (precision × rounding) combination the surface needs:
19//!
20//! | Method            | Guard width    | Rounding mode               |
21//! |-------------------|----------------|------------------------------|
22//! | `<fn>_strict`     | crate default  | crate default ([`RoundingMode::HalfToEven`] unless a `rounding-*` feature is set) |
23//! | `<fn>_strict_with`| crate default  | caller-supplied              |
24//! | `<fn>_approx`     | caller-chosen  | crate default               |
25//! | `<fn>_approx_with`| caller-chosen  | caller-supplied              |
26//!
27//! The `_strict` body keeps a const-folded guard width
28//! (`SCALE + STRICT_GUARD`) so LLVM specialises one optimal kernel
29//! per `SCALE`. The `_approx` body takes the guard width at runtime
30//! and is intended for callers who want to trade ULP precision for
31//! latency — pick a smaller `working_digits` than `STRICT_GUARD` to
32//! cut series length, or a larger one to recover headroom. When the
33//! caller passes exactly `STRICT_GUARD` the `_approx_with` body
34//! redirects to `_strict_with` so the const-folded path is never
35//! displaced.
36//!
37//! All four variants are integer-only, `no_std`-compatible, and
38//! correctly rounded under the selected mode. Without the `strict`
39//! feature, the plain `<fn>` is an f64-bridge instead.
40//!
41//! See `docs/strict-mode.md` for the full dual-API + feature-gating
42//! rules and the 0.5 ULP accuracy contract.
43//!
44//! # Precision
45//!
46//! The f64-bridge forms are **Lossy** — the `D38` value round-trips
47//! through `f64`, which introduces up to one LSB of quantisation per
48//! conversion. Every `_strict` / `_strict_with` / `_approx` /
49//! `_approx_with` form is **correctly rounded**: within 0.5 ULP of
50//! the exact result under the selected `RoundingMode`. They evaluate
51//! every reduction and series step in the `d_w128_kernels::Fixed`
52//! guard-digit intermediate and round once at the end.
53//!
54//! [`RoundingMode::HalfToEven`]: crate::RoundingMode::HalfToEven
55//!
56//! # `atan2` signature
57//!
58//! `f64::atan2(self, other)` treats `self` as `y` and `other` as `x`.
59//! This module matches that signature exactly so generic numeric code
60//! calling `y.atan2(x)` works with `T = D38`.
61
62use crate::core_type::D38;
63
64impl<const SCALE: u32> D38<SCALE> {
65#[cfg(all(feature = "strict", not(feature = "fast")))]
66    /// With `strict` this dispatches to [`Self::sin_strict`]; without
67    /// it, the f64-bridge form is used instead.
68    #[inline]
69    #[must_use]
70    pub fn sin(self) -> Self {
71        self.sin_strict()
72    }
73#[cfg(all(feature = "strict", not(feature = "fast")))]
74
75    /// With `strict` this dispatches to [`Self::cos_strict`]; without
76    /// it, the f64-bridge form is used instead.
77    #[inline]
78    #[must_use]
79    pub fn cos(self) -> Self {
80        self.cos_strict()
81    }
82#[cfg(all(feature = "strict", not(feature = "fast")))]
83
84    /// With `strict` this dispatches to [`Self::tan_strict`]; without
85    /// it, the f64-bridge form is used instead.
86    #[inline]
87    #[must_use]
88    pub fn tan(self) -> Self {
89        self.tan_strict()
90    }
91#[cfg(all(feature = "strict", not(feature = "fast")))]
92
93    /// With `strict` this dispatches to [`Self::asin_strict`]; without
94    /// it, the f64-bridge form is used instead.
95    #[inline]
96    #[must_use]
97    pub fn asin(self) -> Self {
98        self.asin_strict()
99    }
100#[cfg(all(feature = "strict", not(feature = "fast")))]
101
102    /// With `strict` this dispatches to [`Self::acos_strict`]; without
103    /// it, the f64-bridge form is used instead.
104    #[inline]
105    #[must_use]
106    pub fn acos(self) -> Self {
107        self.acos_strict()
108    }
109#[cfg(all(feature = "strict", not(feature = "fast")))]
110
111    /// With `strict` this dispatches to [`Self::atan_strict`]; without
112    /// it, the f64-bridge form is used instead.
113    #[inline]
114    #[must_use]
115    pub fn atan(self) -> Self {
116        self.atan_strict()
117    }
118#[cfg(all(feature = "strict", not(feature = "fast")))]
119
120    /// Four-quadrant arctangent of `self` (`y`) and `other` (`x`).
121    /// With `strict` this dispatches to [`Self::atan2_strict`];
122    /// without it, the f64-bridge form is used instead.
123    #[inline]
124    #[must_use]
125    pub fn atan2(self, other: Self) -> Self {
126        self.atan2_strict(other)
127    }
128#[cfg(all(feature = "strict", not(feature = "fast")))]
129
130    /// With `strict` this dispatches to [`Self::sinh_strict`]; without
131    /// it, the f64-bridge form is used instead.
132    #[inline]
133    #[must_use]
134    pub fn sinh(self) -> Self {
135        self.sinh_strict()
136    }
137#[cfg(all(feature = "strict", not(feature = "fast")))]
138
139    /// With `strict` this dispatches to [`Self::cosh_strict`]; without
140    /// it, the f64-bridge form is used instead.
141    #[inline]
142    #[must_use]
143    pub fn cosh(self) -> Self {
144        self.cosh_strict()
145    }
146#[cfg(all(feature = "strict", not(feature = "fast")))]
147
148    /// With `strict` this dispatches to [`Self::tanh_strict`]; without
149    /// it, the f64-bridge form is used instead.
150    #[inline]
151    #[must_use]
152    pub fn tanh(self) -> Self {
153        self.tanh_strict()
154    }
155#[cfg(all(feature = "strict", not(feature = "fast")))]
156
157    /// With `strict` this dispatches to [`Self::asinh_strict`]; without
158    /// it, the f64-bridge form is used instead.
159    #[inline]
160    #[must_use]
161    pub fn asinh(self) -> Self {
162        self.asinh_strict()
163    }
164#[cfg(all(feature = "strict", not(feature = "fast")))]
165
166    /// With `strict` this dispatches to [`Self::acosh_strict`]; without
167    /// it, the f64-bridge form is used instead.
168    #[inline]
169    #[must_use]
170    pub fn acosh(self) -> Self {
171        self.acosh_strict()
172    }
173#[cfg(all(feature = "strict", not(feature = "fast")))]
174
175    /// With `strict` this dispatches to [`Self::atanh_strict`]; without
176    /// it, the f64-bridge form is used instead.
177    #[inline]
178    #[must_use]
179    pub fn atanh(self) -> Self {
180        self.atanh_strict()
181    }
182#[cfg(all(feature = "strict", not(feature = "fast")))]
183
184    /// With `strict` this dispatches to [`Self::to_degrees_strict`]; without
185    /// it, the f64-bridge form is used instead.
186    #[inline]
187    #[must_use]
188    pub fn to_degrees(self) -> Self {
189        self.to_degrees_strict()
190    }
191#[cfg(all(feature = "strict", not(feature = "fast")))]
192
193    /// With `strict` this dispatches to [`Self::to_radians_strict`]; without
194    /// it, the f64-bridge form is used instead.
195    #[inline]
196    #[must_use]
197    pub fn to_radians(self) -> Self {
198        self.to_radians_strict()
199    }
200
201    // Sine of `self` (radians). Strict: integer-only and correctly
202    // rounded — the result is within 0.5 ULP of the exact sine.
203    #[inline]
204    #[must_use]
205    pub fn sin_strict(self) -> Self {
206        self.sin_strict_with(crate::rounding::DEFAULT_ROUNDING_MODE)
207    }
208
209    /// Sine under the supplied rounding mode.
210    #[inline]
211    #[must_use]
212    pub fn sin_strict_with(self, mode: crate::rounding::RoundingMode) -> Self {
213        if self.0 == 0 {
214            return Self::ZERO;
215        }
216        if self.0.abs() <= small_x_linear_threshold::<SCALE>() {
217            return self;
218        }
219        let w = SCALE + crate::log_exp_strict::STRICT_GUARD;
220        let raw = sin_fixed(to_fixed(self.0), w)
221            .round_to_i128_with(w, SCALE, mode)
222            .expect("D38::sin: result out of range");
223        Self::from_bits(raw)
224    }
225
226    /// Sine with caller-chosen guard digits.
227    #[inline]
228    #[must_use]
229    pub fn sin_approx(self, working_digits: u32) -> Self {
230        self.sin_approx_with(working_digits, crate::rounding::DEFAULT_ROUNDING_MODE)
231    }
232
233    /// Sine with caller-chosen guard digits AND rounding mode.
234    #[inline]
235    #[must_use]
236    pub fn sin_approx_with(self, working_digits: u32, mode: crate::rounding::RoundingMode) -> Self {
237        if working_digits == crate::log_exp_strict::STRICT_GUARD {
238            return self.sin_strict_with(mode);
239        }
240        if self.0 == 0 {
241            return Self::ZERO;
242        }
243        if self.0.abs() <= small_x_linear_threshold::<SCALE>() {
244            return self;
245        }
246        let w = SCALE + working_digits;
247        let raw = sin_fixed(to_fixed(self.0), w)
248            .round_to_i128_with(w, SCALE, mode)
249            .expect("D38::sin: result out of range");
250        Self::from_bits(raw)
251    }
252
253    /// Cosine of `self` (radians). Strict: `cos(x) = sin(x + π/2)`,
254    /// correctly rounded.
255    #[inline]
256    #[must_use]
257    pub fn cos_strict(self) -> Self {
258        self.cos_strict_with(crate::rounding::DEFAULT_ROUNDING_MODE)
259    }
260
261    /// Cosine under the supplied rounding mode.
262    #[inline]
263    #[must_use]
264    pub fn cos_strict_with(self, mode: crate::rounding::RoundingMode) -> Self {
265        if self.0 == 0 {
266            return Self::from_bits(10_i128.pow(SCALE));
267        }
268        let w = SCALE + crate::log_exp_strict::STRICT_GUARD;
269        let arg = to_fixed(self.0).add(wide_half_pi(w));
270        let raw = sin_fixed(arg, w)
271            .round_to_i128_with(w, SCALE, mode)
272            .expect("D38::cos: result out of range");
273        Self::from_bits(raw)
274    }
275
276    /// Cosine with caller-chosen guard digits.
277    #[inline]
278    #[must_use]
279    pub fn cos_approx(self, working_digits: u32) -> Self {
280        self.cos_approx_with(working_digits, crate::rounding::DEFAULT_ROUNDING_MODE)
281    }
282
283    /// Cosine with caller-chosen guard digits AND rounding mode.
284    #[inline]
285    #[must_use]
286    pub fn cos_approx_with(self, working_digits: u32, mode: crate::rounding::RoundingMode) -> Self {
287        if working_digits == crate::log_exp_strict::STRICT_GUARD {
288            return self.cos_strict_with(mode);
289        }
290        if self.0 == 0 {
291            return Self::from_bits(10_i128.pow(SCALE));
292        }
293        let w = SCALE + working_digits;
294        let arg = to_fixed(self.0).add(wide_half_pi(w));
295        let raw = sin_fixed(arg, w)
296            .round_to_i128_with(w, SCALE, mode)
297            .expect("D38::cos: result out of range");
298        Self::from_bits(raw)
299    }
300
301    /// Tangent of `self` (radians). Strict: `tan(x) = sin(x) / cos(x)`,
302    /// with the division carried in the wide intermediate so the result
303    /// is correctly rounded.
304    ///
305    /// # Panics
306    ///
307    /// Panics if `cos(self)` is zero (an odd multiple of π/2).
308    #[inline]
309    #[must_use]
310    pub fn tan_strict(self) -> Self {
311        self.tan_strict_with(crate::rounding::DEFAULT_ROUNDING_MODE)
312    }
313
314    /// Tangent under the supplied rounding mode.
315    #[inline]
316    #[must_use]
317    pub fn tan_strict_with(self, mode: crate::rounding::RoundingMode) -> Self {
318        if self.0 == 0 {
319            return Self::ZERO;
320        }
321        if self.0.abs() <= small_x_linear_threshold::<SCALE>() {
322            return self;
323        }
324        let w = SCALE + crate::log_exp_strict::STRICT_GUARD;
325        let v = to_fixed(self.0);
326        let sin_w = sin_fixed(v, w);
327        let cos_w = sin_fixed(v.add(wide_half_pi(w)), w);
328        assert!(!cos_w.is_zero(), "D38::tan: cosine is zero (argument is an odd multiple of pi/2)");
329        let raw = sin_w
330            .div(cos_w, w)
331            .round_to_i128_with(w, SCALE, mode)
332            .expect("D38::tan: result out of range");
333        Self::from_bits(raw)
334    }
335
336    /// Tangent with caller-chosen guard digits.
337    #[inline]
338    #[must_use]
339    pub fn tan_approx(self, working_digits: u32) -> Self {
340        self.tan_approx_with(working_digits, crate::rounding::DEFAULT_ROUNDING_MODE)
341    }
342
343    /// Tangent with caller-chosen guard digits AND rounding mode.
344    #[inline]
345    #[must_use]
346    pub fn tan_approx_with(self, working_digits: u32, mode: crate::rounding::RoundingMode) -> Self {
347        if working_digits == crate::log_exp_strict::STRICT_GUARD {
348            return self.tan_strict_with(mode);
349        }
350        if self.0 == 0 {
351            return Self::ZERO;
352        }
353        if self.0.abs() <= small_x_linear_threshold::<SCALE>() {
354            return self;
355        }
356        let w = SCALE + working_digits;
357        let v = to_fixed(self.0);
358        let sin_w = sin_fixed(v, w);
359        let cos_w = sin_fixed(v.add(wide_half_pi(w)), w);
360        assert!(!cos_w.is_zero(), "D38::tan: cosine is zero (argument is an odd multiple of pi/2)");
361        let raw = sin_w
362            .div(cos_w, w)
363            .round_to_i128_with(w, SCALE, mode)
364            .expect("D38::tan: result out of range");
365        Self::from_bits(raw)
366    }
367
368    /// Arctangent of `self`, in radians, in `(−π/2, π/2)`. Strict:
369    /// integer-only and correctly rounded.
370    #[inline]
371    #[must_use]
372    pub fn atan_strict(self) -> Self {
373        self.atan_strict_with(crate::rounding::DEFAULT_ROUNDING_MODE)
374    }
375
376    /// Arctangent under the supplied rounding mode.
377    #[inline]
378    #[must_use]
379    pub fn atan_strict_with(self, mode: crate::rounding::RoundingMode) -> Self {
380        use crate::consts::DecimalConsts;
381        if self.0 == 0 {
382            return Self::ZERO;
383        }
384        let one_bits: i128 = 10_i128.pow(SCALE);
385        if self.0 == one_bits {
386            return Self::quarter_pi();
387        }
388        if self.0 == -one_bits {
389            return -Self::quarter_pi();
390        }
391        if self.0.abs() <= small_x_linear_threshold::<SCALE>() {
392            return self;
393        }
394        let w = SCALE + crate::log_exp_strict::STRICT_GUARD;
395        let raw = atan_fixed(to_fixed(self.0), w)
396            .round_to_i128_with(w, SCALE, mode)
397            .expect("D38::atan: result out of range");
398        Self::from_bits(raw)
399    }
400
401    /// Arctangent with caller-chosen guard digits.
402    #[inline]
403    #[must_use]
404    pub fn atan_approx(self, working_digits: u32) -> Self {
405        self.atan_approx_with(working_digits, crate::rounding::DEFAULT_ROUNDING_MODE)
406    }
407
408    /// Arctangent with caller-chosen guard digits AND rounding mode.
409    #[inline]
410    #[must_use]
411    pub fn atan_approx_with(self, working_digits: u32, mode: crate::rounding::RoundingMode) -> Self {
412        if working_digits == crate::log_exp_strict::STRICT_GUARD {
413            return self.atan_strict_with(mode);
414        }
415        use crate::consts::DecimalConsts;
416        if self.0 == 0 {
417            return Self::ZERO;
418        }
419        let one_bits: i128 = 10_i128.pow(SCALE);
420        if self.0 == one_bits {
421            return Self::quarter_pi();
422        }
423        if self.0 == -one_bits {
424            return -Self::quarter_pi();
425        }
426        if self.0.abs() <= small_x_linear_threshold::<SCALE>() {
427            return self;
428        }
429        let w = SCALE + working_digits;
430        let raw = atan_fixed(to_fixed(self.0), w)
431            .round_to_i128_with(w, SCALE, mode)
432            .expect("D38::atan: result out of range");
433        Self::from_bits(raw)
434    }
435
436    /// Arcsine of `self`, in radians, in `[−π/2, π/2]`. Strict.
437    ///
438    /// `asin(x) = atan(x / √(1 − x²))`; the endpoints `±1` map directly
439    /// to `±π/2`.
440    ///
441    /// # Panics
442    ///
443    /// Panics if `|self| > 1`.
444    #[inline]
445    #[must_use]
446    pub fn asin_strict(self) -> Self {
447        self.asin_strict_with(crate::rounding::DEFAULT_ROUNDING_MODE)
448    }
449
450    /// Arcsine under the supplied rounding mode.
451    #[inline]
452    #[must_use]
453    pub fn asin_strict_with(self, mode: crate::rounding::RoundingMode) -> Self {
454        if self.0 == 0 {
455            return Self::ZERO;
456        }
457        if self.0.abs() <= small_x_linear_threshold::<SCALE>() {
458            return self;
459        }
460        use crate::d_w128_kernels::Fixed;
461        let w = SCALE + crate::log_exp_strict::STRICT_GUARD;
462        let one_w = Fixed { negative: false, mag: Fixed::pow10(w) };
463        let v = to_fixed(self.0);
464        let abs_v = Fixed { negative: false, mag: v.mag };
465        assert!(!(abs_v.ge_mag(one_w) && abs_v != one_w), "D38::asin: argument out of domain [-1, 1]");
466        if abs_v == one_w {
467            let hp = wide_half_pi(w);
468            let hp = if v.negative { hp.neg() } else { hp };
469            let raw = hp
470                .round_to_i128_with(w, SCALE, mode)
471                .expect("D38::asin: result out of range");
472            return Self::from_bits(raw);
473        }
474        let half_w = one_w.halve();
475        let asin_w = if !abs_v.ge_mag(half_w) {
476            let denom = one_w.sub(v.mul(v, w)).sqrt(w);
477            atan_fixed(v.div(denom, w), w)
478        } else {
479            let inner = one_w.sub(abs_v).halve();
480            let inner_sqrt = inner.sqrt(w);
481            let inner_denom = one_w.sub(inner_sqrt.mul(inner_sqrt, w)).sqrt(w);
482            let inner_asin = atan_fixed(inner_sqrt.div(inner_denom, w), w);
483            let result_abs = wide_half_pi(w).sub(inner_asin).sub(inner_asin);
484            if v.negative { result_abs.neg() } else { result_abs }
485        };
486        let raw = asin_w
487            .round_to_i128_with(w, SCALE, mode)
488            .expect("D38::asin: result out of range");
489        Self::from_bits(raw)
490    }
491
492    /// Arcsine with caller-chosen guard digits.
493    #[inline]
494    #[must_use]
495    pub fn asin_approx(self, working_digits: u32) -> Self {
496        self.asin_approx_with(working_digits, crate::rounding::DEFAULT_ROUNDING_MODE)
497    }
498
499    /// Arcsine with caller-chosen guard digits AND rounding mode.
500    #[inline]
501    #[must_use]
502    pub fn asin_approx_with(self, working_digits: u32, mode: crate::rounding::RoundingMode) -> Self {
503        if working_digits == crate::log_exp_strict::STRICT_GUARD {
504            return self.asin_strict_with(mode);
505        }
506        if self.0 == 0 {
507            return Self::ZERO;
508        }
509        if self.0.abs() <= small_x_linear_threshold::<SCALE>() {
510            return self;
511        }
512        use crate::d_w128_kernels::Fixed;
513        let w = SCALE + working_digits;
514        let one_w = Fixed { negative: false, mag: Fixed::pow10(w) };
515        let v = to_fixed_w(self.0, working_digits);
516        let abs_v = Fixed { negative: false, mag: v.mag };
517        assert!(!(abs_v.ge_mag(one_w) && abs_v != one_w), "D38::asin: argument out of domain [-1, 1]");
518        if abs_v == one_w {
519            let hp = wide_half_pi(w);
520            let hp = if v.negative { hp.neg() } else { hp };
521            let raw = hp
522                .round_to_i128_with(w, SCALE, mode)
523                .expect("D38::asin: result out of range");
524            return Self::from_bits(raw);
525        }
526        let half_w = one_w.halve();
527        let asin_w = if !abs_v.ge_mag(half_w) {
528            let denom = one_w.sub(v.mul(v, w)).sqrt(w);
529            atan_fixed(v.div(denom, w), w)
530        } else {
531            let inner = one_w.sub(abs_v).halve();
532            let inner_sqrt = inner.sqrt(w);
533            let inner_denom = one_w.sub(inner_sqrt.mul(inner_sqrt, w)).sqrt(w);
534            let inner_asin = atan_fixed(inner_sqrt.div(inner_denom, w), w);
535            let result_abs = wide_half_pi(w).sub(inner_asin).sub(inner_asin);
536            if v.negative { result_abs.neg() } else { result_abs }
537        };
538        let raw = asin_w
539            .round_to_i128_with(w, SCALE, mode)
540            .expect("D38::asin: result out of range");
541        Self::from_bits(raw)
542    }
543
544    /// Arccosine of `self`, in radians, in `[0, π]`. Strict:
545    /// `acos(x) = π/2 − asin(x)`, correctly rounded.
546    ///
547    /// # Panics
548    ///
549    /// Panics if `|self| > 1`.
550    #[inline]
551    #[must_use]
552    pub fn acos_strict(self) -> Self {
553        self.acos_strict_with(crate::rounding::DEFAULT_ROUNDING_MODE)
554    }
555
556    /// Arccosine under the supplied rounding mode.
557    #[inline]
558    #[must_use]
559    pub fn acos_strict_with(self, mode: crate::rounding::RoundingMode) -> Self {
560        use crate::consts::DecimalConsts;
561        if self.0 == 0 {
562            return Self::half_pi();
563        }
564        let one_bits: i128 = 10_i128.pow(SCALE);
565        if self.0 == one_bits {
566            return Self::ZERO;
567        }
568        if self.0 == -one_bits {
569            return Self::pi();
570        }
571        use crate::d_w128_kernels::Fixed;
572        let w = SCALE + crate::log_exp_strict::STRICT_GUARD;
573        let one_w = Fixed { negative: false, mag: Fixed::pow10(w) };
574        let v = to_fixed(self.0);
575        let abs_v = Fixed { negative: false, mag: v.mag };
576        assert!(!(abs_v.ge_mag(one_w) && abs_v != one_w), "D38::acos: argument out of domain [-1, 1]");
577        let half_w = one_w.halve();
578        let asin_w = if abs_v == one_w {
579            let hp = wide_half_pi(w);
580            if v.negative { hp.neg() } else { hp }
581        } else if !abs_v.ge_mag(half_w) {
582            let denom = one_w.sub(v.mul(v, w)).sqrt(w);
583            atan_fixed(v.div(denom, w), w)
584        } else {
585            let inner = one_w.sub(abs_v).halve();
586            let inner_sqrt = inner.sqrt(w);
587            let inner_denom = one_w.sub(inner_sqrt.mul(inner_sqrt, w)).sqrt(w);
588            let inner_asin = atan_fixed(inner_sqrt.div(inner_denom, w), w);
589            let result_abs = wide_half_pi(w).sub(inner_asin).sub(inner_asin);
590            if v.negative { result_abs.neg() } else { result_abs }
591        };
592        let raw = wide_half_pi(w)
593            .sub(asin_w)
594            .round_to_i128_with(w, SCALE, mode)
595            .expect("D38::acos: result out of range");
596        Self::from_bits(raw)
597    }
598
599    /// Arccosine with caller-chosen guard digits.
600    #[inline]
601    #[must_use]
602    pub fn acos_approx(self, working_digits: u32) -> Self {
603        self.acos_approx_with(working_digits, crate::rounding::DEFAULT_ROUNDING_MODE)
604    }
605
606    /// Arccosine with caller-chosen guard digits AND rounding mode.
607    #[inline]
608    #[must_use]
609    pub fn acos_approx_with(self, working_digits: u32, mode: crate::rounding::RoundingMode) -> Self {
610        if working_digits == crate::log_exp_strict::STRICT_GUARD {
611            return self.acos_strict_with(mode);
612        }
613        use crate::consts::DecimalConsts;
614        if self.0 == 0 {
615            return Self::half_pi();
616        }
617        let one_bits: i128 = 10_i128.pow(SCALE);
618        if self.0 == one_bits {
619            return Self::ZERO;
620        }
621        if self.0 == -one_bits {
622            return Self::pi();
623        }
624        use crate::d_w128_kernels::Fixed;
625        let w = SCALE + working_digits;
626        let one_w = Fixed { negative: false, mag: Fixed::pow10(w) };
627        let v = to_fixed_w(self.0, working_digits);
628        let abs_v = Fixed { negative: false, mag: v.mag };
629        assert!(!(abs_v.ge_mag(one_w) && abs_v != one_w), "D38::acos: argument out of domain [-1, 1]");
630        let half_w = one_w.halve();
631        let asin_w = if abs_v == one_w {
632            let hp = wide_half_pi(w);
633            if v.negative { hp.neg() } else { hp }
634        } else if !abs_v.ge_mag(half_w) {
635            let denom = one_w.sub(v.mul(v, w)).sqrt(w);
636            atan_fixed(v.div(denom, w), w)
637        } else {
638            let inner = one_w.sub(abs_v).halve();
639            let inner_sqrt = inner.sqrt(w);
640            let inner_denom = one_w.sub(inner_sqrt.mul(inner_sqrt, w)).sqrt(w);
641            let inner_asin = atan_fixed(inner_sqrt.div(inner_denom, w), w);
642            let result_abs = wide_half_pi(w).sub(inner_asin).sub(inner_asin);
643            if v.negative { result_abs.neg() } else { result_abs }
644        };
645        let raw = wide_half_pi(w)
646            .sub(asin_w)
647            .round_to_i128_with(w, SCALE, mode)
648            .expect("D38::acos: result out of range");
649        Self::from_bits(raw)
650    }
651
652    /// Four-quadrant arctangent of `self` (`y`) and `other` (`x`), in
653    /// radians, in `(−π, π]`. Strict: integer-only and correctly
654    /// rounded.
655    #[inline]
656    #[must_use]
657    pub fn atan2_strict(self, other: Self) -> Self {
658        self.atan2_strict_with(other, crate::rounding::DEFAULT_ROUNDING_MODE)
659    }
660
661    /// Four-quadrant arctangent under the supplied rounding mode.
662    #[inline]
663    #[must_use]
664    pub fn atan2_strict_with(self, other: Self, mode: crate::rounding::RoundingMode) -> Self {
665        let w = SCALE + crate::log_exp_strict::STRICT_GUARD;
666        let raw = atan2_kernel(to_fixed(self.0), to_fixed(other.0), self.0, w)
667            .round_to_i128_with(w, SCALE, mode)
668            .expect("D38::atan2: result out of range");
669        Self::from_bits(raw)
670    }
671
672    /// Four-quadrant arctangent with caller-chosen guard digits.
673    #[inline]
674    #[must_use]
675    pub fn atan2_approx(self, other: Self, working_digits: u32) -> Self {
676        self.atan2_approx_with(other, working_digits, crate::rounding::DEFAULT_ROUNDING_MODE)
677    }
678
679    /// Four-quadrant arctangent with caller-chosen guard digits AND rounding mode.
680    #[inline]
681    #[must_use]
682    pub fn atan2_approx_with(self, other: Self, working_digits: u32, mode: crate::rounding::RoundingMode) -> Self {
683        if working_digits == crate::log_exp_strict::STRICT_GUARD {
684            return self.atan2_strict_with(other, mode);
685        }
686        let w = SCALE + working_digits;
687        let raw = atan2_kernel(
688            to_fixed_w(self.0, working_digits),
689            to_fixed_w(other.0, working_digits),
690            self.0,
691            w,
692        )
693            .round_to_i128_with(w, SCALE, mode)
694            .expect("D38::atan2: result out of range");
695        Self::from_bits(raw)
696    }
697
698    /// Hyperbolic sine of `self`. Strict: `sinh(x) = (eˣ − e⁻ˣ)/2`,
699    /// composed in the wide intermediate from the correctly-rounded
700    /// `exp`, so the result is itself correctly rounded.
701    #[inline]
702    #[must_use]
703    pub fn sinh_strict(self) -> Self {
704        self.sinh_strict_with(crate::rounding::DEFAULT_ROUNDING_MODE)
705    }
706
707    /// Hyperbolic sine under the supplied rounding mode.
708    #[inline]
709    #[must_use]
710    pub fn sinh_strict_with(self, mode: crate::rounding::RoundingMode) -> Self {
711        if self.0 == 0 {
712            return Self::ZERO;
713        }
714        if self.0.abs() <= small_x_linear_threshold::<SCALE>() {
715            return self;
716        }
717        let w = SCALE + crate::log_exp_strict::STRICT_GUARD;
718        let v = to_fixed(self.0);
719        let ex = crate::log_exp_strict::exp_fixed(v, w);
720        let enx = crate::log_exp_strict::exp_fixed(v.neg(), w);
721        let raw = ex
722            .sub(enx)
723            .halve()
724            .round_to_i128_with(w, SCALE, mode)
725            .expect("D38::sinh: result out of range");
726        Self::from_bits(raw)
727    }
728
729    /// Hyperbolic sine with caller-chosen guard digits.
730    #[inline]
731    #[must_use]
732    pub fn sinh_approx(self, working_digits: u32) -> Self {
733        self.sinh_approx_with(working_digits, crate::rounding::DEFAULT_ROUNDING_MODE)
734    }
735
736    /// Hyperbolic sine with caller-chosen guard digits AND rounding mode.
737    #[inline]
738    #[must_use]
739    pub fn sinh_approx_with(self, working_digits: u32, mode: crate::rounding::RoundingMode) -> Self {
740        if working_digits == crate::log_exp_strict::STRICT_GUARD {
741            return self.sinh_strict_with(mode);
742        }
743        if self.0 == 0 {
744            return Self::ZERO;
745        }
746        if self.0.abs() <= small_x_linear_threshold::<SCALE>() {
747            return self;
748        }
749        let w = SCALE + working_digits;
750        let v = to_fixed_w(self.0, working_digits);
751        let ex = crate::log_exp_strict::exp_fixed(v, w);
752        let enx = crate::log_exp_strict::exp_fixed(v.neg(), w);
753        let raw = ex
754            .sub(enx)
755            .halve()
756            .round_to_i128_with(w, SCALE, mode)
757            .expect("D38::sinh: result out of range");
758        Self::from_bits(raw)
759    }
760
761    /// Hyperbolic cosine of `self`. Strict: `cosh(x) = (eˣ + e⁻ˣ)/2`,
762    /// correctly rounded.
763    #[inline]
764    #[must_use]
765    pub fn cosh_strict(self) -> Self {
766        self.cosh_strict_with(crate::rounding::DEFAULT_ROUNDING_MODE)
767    }
768
769    /// Hyperbolic cosine under the supplied rounding mode.
770    #[inline]
771    #[must_use]
772    pub fn cosh_strict_with(self, mode: crate::rounding::RoundingMode) -> Self {
773        if self.0 == 0 {
774            return Self::from_bits(10_i128.pow(SCALE));
775        }
776        let w = SCALE + crate::log_exp_strict::STRICT_GUARD;
777        let v = to_fixed(self.0);
778        let ex = crate::log_exp_strict::exp_fixed(v, w);
779        let enx = crate::log_exp_strict::exp_fixed(v.neg(), w);
780        let raw = ex
781            .add(enx)
782            .halve()
783            .round_to_i128_with(w, SCALE, mode)
784            .expect("D38::cosh: result out of range");
785        Self::from_bits(raw)
786    }
787
788    /// Hyperbolic cosine with caller-chosen guard digits.
789    #[inline]
790    #[must_use]
791    pub fn cosh_approx(self, working_digits: u32) -> Self {
792        self.cosh_approx_with(working_digits, crate::rounding::DEFAULT_ROUNDING_MODE)
793    }
794
795    /// Hyperbolic cosine with caller-chosen guard digits AND rounding mode.
796    #[inline]
797    #[must_use]
798    pub fn cosh_approx_with(self, working_digits: u32, mode: crate::rounding::RoundingMode) -> Self {
799        if working_digits == crate::log_exp_strict::STRICT_GUARD {
800            return self.cosh_strict_with(mode);
801        }
802        if self.0 == 0 {
803            return Self::from_bits(10_i128.pow(SCALE));
804        }
805        let w = SCALE + working_digits;
806        let v = to_fixed_w(self.0, working_digits);
807        let ex = crate::log_exp_strict::exp_fixed(v, w);
808        let enx = crate::log_exp_strict::exp_fixed(v.neg(), w);
809        let raw = ex
810            .add(enx)
811            .halve()
812            .round_to_i128_with(w, SCALE, mode)
813            .expect("D38::cosh: result out of range");
814        Self::from_bits(raw)
815    }
816
817    /// Hyperbolic tangent of `self`. Strict: `tanh(x) = sinh(x)/cosh(x)`
818    /// with the division in the wide intermediate. `cosh ≥ 1`, so the
819    /// division never traps.
820    #[inline]
821    #[must_use]
822    pub fn tanh_strict(self) -> Self {
823        self.tanh_strict_with(crate::rounding::DEFAULT_ROUNDING_MODE)
824    }
825
826    /// Hyperbolic tangent under the supplied rounding mode.
827    #[inline]
828    #[must_use]
829    pub fn tanh_strict_with(self, mode: crate::rounding::RoundingMode) -> Self {
830        if self.0 == 0 {
831            return Self::ZERO;
832        }
833        if self.0.abs() <= small_x_linear_threshold::<SCALE>() {
834            return self;
835        }
836        let w = SCALE + crate::log_exp_strict::STRICT_GUARD;
837        let v = to_fixed(self.0);
838        let ex = crate::log_exp_strict::exp_fixed(v, w);
839        let enx = crate::log_exp_strict::exp_fixed(v.neg(), w);
840        let raw = ex
841            .sub(enx)
842            .div(ex.add(enx), w)
843            .round_to_i128_with(w, SCALE, mode)
844            .expect("D38::tanh: result out of range");
845        Self::from_bits(raw)
846    }
847
848    /// Hyperbolic tangent with caller-chosen guard digits.
849    #[inline]
850    #[must_use]
851    pub fn tanh_approx(self, working_digits: u32) -> Self {
852        self.tanh_approx_with(working_digits, crate::rounding::DEFAULT_ROUNDING_MODE)
853    }
854
855    /// Hyperbolic tangent with caller-chosen guard digits AND rounding mode.
856    #[inline]
857    #[must_use]
858    pub fn tanh_approx_with(self, working_digits: u32, mode: crate::rounding::RoundingMode) -> Self {
859        if working_digits == crate::log_exp_strict::STRICT_GUARD {
860            return self.tanh_strict_with(mode);
861        }
862        if self.0 == 0 {
863            return Self::ZERO;
864        }
865        if self.0.abs() <= small_x_linear_threshold::<SCALE>() {
866            return self;
867        }
868        let w = SCALE + working_digits;
869        let v = to_fixed_w(self.0, working_digits);
870        let ex = crate::log_exp_strict::exp_fixed(v, w);
871        let enx = crate::log_exp_strict::exp_fixed(v.neg(), w);
872        let raw = ex
873            .sub(enx)
874            .div(ex.add(enx), w)
875            .round_to_i128_with(w, SCALE, mode)
876            .expect("D38::tanh: result out of range");
877        Self::from_bits(raw)
878    }
879
880    /// Inverse hyperbolic sine of `self`. Strict:
881    /// `asinh(x) = sign · ln(|x| + √(x² + 1))`, correctly rounded.
882    /// For `|x| ≥ 1` the radicand is factored as
883    /// `|x|·(1 + √(1 + 1/x²))` to keep `x²` from overflowing the wide
884    /// intermediate.
885    #[inline]
886    #[must_use]
887    pub fn asinh_strict(self) -> Self {
888        self.asinh_strict_with(crate::rounding::DEFAULT_ROUNDING_MODE)
889    }
890
891    /// Inverse hyperbolic sine under the supplied rounding mode.
892    #[inline]
893    #[must_use]
894    pub fn asinh_strict_with(self, mode: crate::rounding::RoundingMode) -> Self {
895        use crate::d_w128_kernels::Fixed;
896        if self.0 == 0 {
897            return Self::ZERO;
898        }
899        if self.0.abs() <= small_x_linear_threshold::<SCALE>() {
900            return self;
901        }
902        let w = SCALE + crate::log_exp_strict::STRICT_GUARD;
903        let one_w = Fixed { negative: false, mag: Fixed::pow10(w) };
904        let v = to_fixed(self.0);
905        let ax = Fixed { negative: false, mag: v.mag };
906        let inner = if ax.ge_mag(one_w) {
907            let inv = one_w.div(ax, w);
908            let root = one_w.add(inv.mul(inv, w)).sqrt(w);
909            crate::log_exp_strict::ln_fixed(ax, w).add(crate::log_exp_strict::ln_fixed(one_w.add(root), w))
910        } else {
911            let root = ax.mul(ax, w).add(one_w).sqrt(w);
912            crate::log_exp_strict::ln_fixed(ax.add(root), w)
913        };
914        let signed = if self.0 < 0 { inner.neg() } else { inner };
915        let raw = signed
916            .round_to_i128_with(w, SCALE, mode)
917            .expect("D38::asinh: result out of range");
918        Self::from_bits(raw)
919    }
920
921    /// Inverse hyperbolic sine with caller-chosen guard digits.
922    #[inline]
923    #[must_use]
924    pub fn asinh_approx(self, working_digits: u32) -> Self {
925        self.asinh_approx_with(working_digits, crate::rounding::DEFAULT_ROUNDING_MODE)
926    }
927
928    /// Inverse hyperbolic sine with caller-chosen guard digits AND rounding mode.
929    #[inline]
930    #[must_use]
931    pub fn asinh_approx_with(self, working_digits: u32, mode: crate::rounding::RoundingMode) -> Self {
932        if working_digits == crate::log_exp_strict::STRICT_GUARD {
933            return self.asinh_strict_with(mode);
934        }
935        use crate::d_w128_kernels::Fixed;
936        if self.0 == 0 {
937            return Self::ZERO;
938        }
939        if self.0.abs() <= small_x_linear_threshold::<SCALE>() {
940            return self;
941        }
942        let w = SCALE + working_digits;
943        let one_w = Fixed { negative: false, mag: Fixed::pow10(w) };
944        let v = to_fixed_w(self.0, working_digits);
945        let ax = Fixed { negative: false, mag: v.mag };
946        let inner = if ax.ge_mag(one_w) {
947            let inv = one_w.div(ax, w);
948            let root = one_w.add(inv.mul(inv, w)).sqrt(w);
949            crate::log_exp_strict::ln_fixed(ax, w).add(crate::log_exp_strict::ln_fixed(one_w.add(root), w))
950        } else {
951            let root = ax.mul(ax, w).add(one_w).sqrt(w);
952            crate::log_exp_strict::ln_fixed(ax.add(root), w)
953        };
954        let signed = if self.0 < 0 { inner.neg() } else { inner };
955        let raw = signed
956            .round_to_i128_with(w, SCALE, mode)
957            .expect("D38::asinh: result out of range");
958        Self::from_bits(raw)
959    }
960
961    /// Inverse hyperbolic cosine of `self`. Strict:
962    /// `acosh(x) = ln(x + √(x² − 1))`, defined for `x ≥ 1`, correctly
963    /// rounded. For `x ≥ 2` the radicand is factored as
964    /// `x·(1 + √(1 − 1/x²))` to keep `x²` in range.
965    ///
966    /// # Panics
967    ///
968    /// Panics if `self < 1`.
969    #[inline]
970    #[must_use]
971    pub fn acosh_strict(self) -> Self {
972        self.acosh_strict_with(crate::rounding::DEFAULT_ROUNDING_MODE)
973    }
974
975    /// Inverse hyperbolic cosine under the supplied rounding mode.
976    #[inline]
977    #[must_use]
978    pub fn acosh_strict_with(self, mode: crate::rounding::RoundingMode) -> Self {
979        let one_bits: i128 = 10_i128.pow(SCALE);
980        if self.0 == one_bits {
981            return Self::ZERO;
982        }
983        use crate::d_w128_kernels::Fixed;
984        let w = SCALE + crate::log_exp_strict::STRICT_GUARD;
985        let one_w = Fixed { negative: false, mag: Fixed::pow10(w) };
986        let v = to_fixed(self.0);
987        assert!(!v.negative && v.ge_mag(one_w), "D38::acosh: argument must be >= 1");
988        let two_w = one_w.double();
989        let inner = if v.ge_mag(two_w) {
990            let inv = one_w.div(v, w);
991            let root = one_w.sub(inv.mul(inv, w)).sqrt(w);
992            crate::log_exp_strict::ln_fixed(v, w).add(crate::log_exp_strict::ln_fixed(one_w.add(root), w))
993        } else {
994            let root = v.mul(v, w).sub(one_w).sqrt(w);
995            crate::log_exp_strict::ln_fixed(v.add(root), w)
996        };
997        let raw = inner
998            .round_to_i128_with(w, SCALE, mode)
999            .expect("D38::acosh: result out of range");
1000        Self::from_bits(raw)
1001    }
1002
1003    /// Inverse hyperbolic cosine with caller-chosen guard digits.
1004    #[inline]
1005    #[must_use]
1006    pub fn acosh_approx(self, working_digits: u32) -> Self {
1007        self.acosh_approx_with(working_digits, crate::rounding::DEFAULT_ROUNDING_MODE)
1008    }
1009
1010    /// Inverse hyperbolic cosine with caller-chosen guard digits AND rounding mode.
1011    #[inline]
1012    #[must_use]
1013    pub fn acosh_approx_with(self, working_digits: u32, mode: crate::rounding::RoundingMode) -> Self {
1014        if working_digits == crate::log_exp_strict::STRICT_GUARD {
1015            return self.acosh_strict_with(mode);
1016        }
1017        let one_bits: i128 = 10_i128.pow(SCALE);
1018        if self.0 == one_bits {
1019            return Self::ZERO;
1020        }
1021        use crate::d_w128_kernels::Fixed;
1022        let w = SCALE + working_digits;
1023        let one_w = Fixed { negative: false, mag: Fixed::pow10(w) };
1024        let v = to_fixed_w(self.0, working_digits);
1025        assert!(!v.negative && v.ge_mag(one_w), "D38::acosh: argument must be >= 1");
1026        let two_w = one_w.double();
1027        let inner = if v.ge_mag(two_w) {
1028            let inv = one_w.div(v, w);
1029            let root = one_w.sub(inv.mul(inv, w)).sqrt(w);
1030            crate::log_exp_strict::ln_fixed(v, w).add(crate::log_exp_strict::ln_fixed(one_w.add(root), w))
1031        } else {
1032            let root = v.mul(v, w).sub(one_w).sqrt(w);
1033            crate::log_exp_strict::ln_fixed(v.add(root), w)
1034        };
1035        let raw = inner
1036            .round_to_i128_with(w, SCALE, mode)
1037            .expect("D38::acosh: result out of range");
1038        Self::from_bits(raw)
1039    }
1040
1041    /// Inverse hyperbolic tangent of `self`. Strict:
1042    /// `atanh(x) = ln((1 + x) / (1 − x)) / 2`, defined for `|x| < 1`,
1043    /// correctly rounded.
1044    ///
1045    /// # Panics
1046    ///
1047    /// Panics if `|self| >= 1`.
1048    #[inline]
1049    #[must_use]
1050    pub fn atanh_strict(self) -> Self {
1051        self.atanh_strict_with(crate::rounding::DEFAULT_ROUNDING_MODE)
1052    }
1053
1054    /// Inverse hyperbolic tangent under the supplied rounding mode.
1055    #[inline]
1056    #[must_use]
1057    pub fn atanh_strict_with(self, mode: crate::rounding::RoundingMode) -> Self {
1058        if self.0 == 0 {
1059            return Self::ZERO;
1060        }
1061        if self.0.abs() <= small_x_linear_threshold::<SCALE>() {
1062            return self;
1063        }
1064        use crate::d_w128_kernels::Fixed;
1065        let w = SCALE + crate::log_exp_strict::STRICT_GUARD;
1066        let one_w = Fixed { negative: false, mag: Fixed::pow10(w) };
1067        let v = to_fixed(self.0);
1068        let ax = Fixed { negative: false, mag: v.mag };
1069        assert!(!ax.ge_mag(one_w), "D38::atanh: argument out of domain (-1, 1)");
1070        let ratio = one_w.add(v).div(one_w.sub(v), w);
1071        let raw = crate::log_exp_strict::ln_fixed(ratio, w)
1072            .halve()
1073            .round_to_i128_with(w, SCALE, mode)
1074            .expect("D38::atanh: result out of range");
1075        Self::from_bits(raw)
1076    }
1077
1078    /// Inverse hyperbolic tangent with caller-chosen guard digits.
1079    #[inline]
1080    #[must_use]
1081    pub fn atanh_approx(self, working_digits: u32) -> Self {
1082        self.atanh_approx_with(working_digits, crate::rounding::DEFAULT_ROUNDING_MODE)
1083    }
1084
1085    /// Inverse hyperbolic tangent with caller-chosen guard digits AND rounding mode.
1086    #[inline]
1087    #[must_use]
1088    pub fn atanh_approx_with(self, working_digits: u32, mode: crate::rounding::RoundingMode) -> Self {
1089        if working_digits == crate::log_exp_strict::STRICT_GUARD {
1090            return self.atanh_strict_with(mode);
1091        }
1092        if self.0 == 0 {
1093            return Self::ZERO;
1094        }
1095        if self.0.abs() <= small_x_linear_threshold::<SCALE>() {
1096            return self;
1097        }
1098        use crate::d_w128_kernels::Fixed;
1099        let w = SCALE + working_digits;
1100        let one_w = Fixed { negative: false, mag: Fixed::pow10(w) };
1101        let v = to_fixed_w(self.0, working_digits);
1102        let ax = Fixed { negative: false, mag: v.mag };
1103        assert!(!ax.ge_mag(one_w), "D38::atanh: argument out of domain (-1, 1)");
1104        let ratio = one_w.add(v).div(one_w.sub(v), w);
1105        let raw = crate::log_exp_strict::ln_fixed(ratio, w)
1106            .halve()
1107            .round_to_i128_with(w, SCALE, mode)
1108            .expect("D38::atanh: result out of range");
1109        Self::from_bits(raw)
1110    }
1111
1112    /// Convert radians to degrees: `self · (180 / π)`. Strict: the
1113    /// multiply and divide run in the wide intermediate, so the result
1114    /// is correctly rounded.
1115    #[inline]
1116    #[must_use]
1117    pub fn to_degrees_strict(self) -> Self {
1118        self.to_degrees_strict_with(crate::rounding::DEFAULT_ROUNDING_MODE)
1119    }
1120
1121    /// Radians-to-degrees conversion under the supplied rounding mode.
1122    #[inline]
1123    #[must_use]
1124    pub fn to_degrees_strict_with(self, mode: crate::rounding::RoundingMode) -> Self {
1125        if self.0 == 0 {
1126            return Self::ZERO;
1127        }
1128        let w = SCALE + crate::log_exp_strict::STRICT_GUARD;
1129        let raw = to_fixed(self.0)
1130            .mul_u128(180)
1131            .div(wide_pi(w), w)
1132            .round_to_i128_with(w, SCALE, mode)
1133            .expect("D38::to_degrees: result out of range");
1134        Self::from_bits(raw)
1135    }
1136
1137    /// Radians-to-degrees conversion with caller-chosen guard digits.
1138    #[inline]
1139    #[must_use]
1140    pub fn to_degrees_approx(self, working_digits: u32) -> Self {
1141        self.to_degrees_approx_with(working_digits, crate::rounding::DEFAULT_ROUNDING_MODE)
1142    }
1143
1144    /// Radians-to-degrees with caller-chosen guard digits AND rounding mode.
1145    #[inline]
1146    #[must_use]
1147    pub fn to_degrees_approx_with(self, working_digits: u32, mode: crate::rounding::RoundingMode) -> Self {
1148        if working_digits == crate::log_exp_strict::STRICT_GUARD {
1149            return self.to_degrees_strict_with(mode);
1150        }
1151        if self.0 == 0 {
1152            return Self::ZERO;
1153        }
1154        let w = SCALE + working_digits;
1155        let raw = to_fixed_w(self.0, working_digits)
1156            .mul_u128(180)
1157            .div(wide_pi(w), w)
1158            .round_to_i128_with(w, SCALE, mode)
1159            .expect("D38::to_degrees: result out of range");
1160        Self::from_bits(raw)
1161    }
1162
1163    /// Convert degrees to radians: `self · (π / 180)`. Strict:
1164    /// correctly rounded.
1165    #[inline]
1166    #[must_use]
1167    pub fn to_radians_strict(self) -> Self {
1168        self.to_radians_strict_with(crate::rounding::DEFAULT_ROUNDING_MODE)
1169    }
1170
1171    /// Degrees-to-radians conversion under the supplied rounding mode.
1172    #[inline]
1173    #[must_use]
1174    pub fn to_radians_strict_with(self, mode: crate::rounding::RoundingMode) -> Self {
1175        if self.0 == 0 {
1176            return Self::ZERO;
1177        }
1178        let w = SCALE + crate::log_exp_strict::STRICT_GUARD;
1179        let raw = to_fixed(self.0)
1180            .mul(wide_pi(w), w)
1181            .div_small(180)
1182            .round_to_i128_with(w, SCALE, mode)
1183            .expect("D38::to_radians: result out of range");
1184        Self::from_bits(raw)
1185    }
1186
1187    /// Degrees-to-radians conversion with caller-chosen guard digits.
1188    #[inline]
1189    #[must_use]
1190    pub fn to_radians_approx(self, working_digits: u32) -> Self {
1191        self.to_radians_approx_with(working_digits, crate::rounding::DEFAULT_ROUNDING_MODE)
1192    }
1193
1194    /// Degrees-to-radians with caller-chosen guard digits AND rounding mode.
1195    #[inline]
1196    #[must_use]
1197    pub fn to_radians_approx_with(self, working_digits: u32, mode: crate::rounding::RoundingMode) -> Self {
1198        if working_digits == crate::log_exp_strict::STRICT_GUARD {
1199            return self.to_radians_strict_with(mode);
1200        }
1201        if self.0 == 0 {
1202            return Self::ZERO;
1203        }
1204        let w = SCALE + working_digits;
1205        let raw = to_fixed_w(self.0, working_digits)
1206            .mul(wide_pi(w), w)
1207            .div_small(180)
1208            .round_to_i128_with(w, SCALE, mode)
1209            .expect("D38::to_radians: result out of range");
1210        Self::from_bits(raw)
1211    }
1212}
1213
1214
1215// ─────────────────────────────────────────────────────────────────────
1216// Strict-mode (integer-only) trigonometric, hyperbolic, and angle-
1217// conversion methods.
1218//
1219// These mirror the f64-bridge surface above but are integer-only,
1220// `no_std`-compatible, and **correctly rounded** — within 0.5 ULP of
1221// the exact result. Every reduction and series step runs in the
1222// `d_w128_kernels::Fixed` guard-digit intermediate (the same machinery the
1223// log/exp family uses) and the value is rounded once at the end.
1224//
1225// Composition strategy:
1226//
1227// - Hyperbolic functions are composed from the strict `exp` / `ln` /
1228// `sqrt` already implemented in `log_exp_strict.rs` / `powers_strict.rs`.
1229// - `cos` is `sin` phase-shifted by π/2; `tan` is `sin / cos`.
1230// - `sin` uses range reduction modulo τ into one π/2 octant followed by
1231// a Taylor series.
1232// - `atan` uses reciprocal reduction for |x| > 1 plus argument halving,
1233// then a Taylor series; `asin` / `acos` / `atan2` are derived from it.
1234// ─────────────────────────────────────────────────────────────────────
1235
1236// Strict-feature dispatchers. When `strict` is enabled (and
1237// `fast` is not), the plain trig methods route to the
1238// integer-only `*_strict` implementations below.
1239
1240// ─────────────────────────────────────────────────────────────────────
1241// Correctly-rounded strict trigonometric core.
1242//
1243// Every strict trig method runs on the shared `d_w128_kernels::Fixed`
1244// guard-digit intermediate at `SCALE + STRICT_GUARD` working digits,
1245// the same machinery the log/exp family uses, and rounds once at the
1246// end — so each result is within 0.5 ULP of the exact value.
1247// ─────────────────────────────────────────────────────────────────────
1248
1249/// π at working scale `w`, sourced from the crate-wide 75-digit
1250/// `consts::PI_RAW` (Int256 holding `π × 10^75`).
1251///
1252/// Caller-side precondition: `w <= 75`. The D38 strict trig kernel
1253/// runs at `w = SCALE + STRICT_GUARD`, capped at `38 + 30 = 68`, so
1254/// the bound is satisfied by every call site in this module. The
1255/// debug-assert documents the invariant for any future caller; in
1256/// release, `rescale_down(75, w > 75)` would silently produce a
1257/// wrong π via the wrapping `from_w − to_w` subtraction.
1258/// Threshold below which the linear small-x fast paths fire for the
1259/// odd trig functions (`atan`, `sin`, `tan`, `sinh`, `tanh`, `asin`,
1260/// `asinh`, `atanh`).
1261///
1262/// All these functions have a Taylor series `f(x) = x + c·x³ + …`
1263/// where `|c| ≤ 1/3`. For `|x| < (1.5·10⁻ˢᶜᴬᴸᴱ)^(1/3) ≈ 10^(−⌈SCALE/3⌉)`
1264/// the cubic correction is bounded by `0.5·ULP` and `f(x) == x`
1265/// exactly at the storage scale. The threshold returned here is the
1266/// conservative integer `10^(SCALE − ⌈(SCALE+2)/3⌉)` in storage
1267/// units (one decimal digit safety margin from the exact bound).
1268///
1269/// The atan-shaped functions (`c = 1/3`) get the tightest correction;
1270/// the sin-shaped functions (`c = 1/6`) and asin-shaped (`c = 1/6`)
1271/// are slightly less restrictive but using the atan threshold for
1272/// uniformity is safe for all and avoids per-function tuning.
1273#[inline]
1274const fn small_x_linear_threshold<const SCALE: u32>() -> i128 {
1275    let thresh_exp = SCALE.saturating_sub((SCALE + 2) / 3);
1276    10_i128.pow(thresh_exp)
1277}
1278
1279fn wide_pi(w: u32) -> crate::d_w128_kernels::Fixed {
1280    debug_assert!(w <= 75, "wide_pi: working scale {w} exceeds embedded 75-digit π");
1281    // PI_RAW is an Int256, internally [u64; 4]. The D38 Fixed kernel
1282    // expects [u128; 2]; repack pairs of u64 limbs into u128.
1283    let words = crate::consts::PI_RAW.0;
1284    let pi_at_75 = crate::d_w128_kernels::Fixed {
1285        negative: false,
1286        mag: [
1287            (words[0] as u128) | ((words[1] as u128) << 64),
1288            (words[2] as u128) | ((words[3] as u128) << 64),
1289        ],
1290    };
1291    if w == 75 {
1292        pi_at_75
1293    } else {
1294        pi_at_75.rescale_down(75, w)
1295    }
1296}
1297
1298/// τ = 2π at working scale `w`.
1299fn wide_tau(w: u32) -> crate::d_w128_kernels::Fixed {
1300    wide_pi(w).double()
1301}
1302
1303/// π/2 at working scale `w`.
1304fn wide_half_pi(w: u32) -> crate::d_w128_kernels::Fixed {
1305    wide_pi(w).halve()
1306}
1307
1308/// Builds a working-scale `Fixed` from a signed `D38` raw value `r`:
1309/// `r · 10^STRICT_GUARD`, carrying the sign.
1310fn to_fixed(raw: i128) -> crate::d_w128_kernels::Fixed {
1311    to_fixed_w(raw, crate::log_exp_strict::STRICT_GUARD)
1312}
1313
1314/// Builds a working-scale `Fixed` from a signed `D38` raw value `r`:
1315/// `r · 10^working_digits`, carrying the sign. Used by the `_approx`
1316/// variants where the guard width is chosen at runtime.
1317fn to_fixed_w(raw: i128, working_digits: u32) -> crate::d_w128_kernels::Fixed {
1318    use crate::d_w128_kernels::Fixed;
1319    let m = Fixed::from_u128_mag(raw.unsigned_abs(), false)
1320        .mul_u128(10u128.pow(working_digits));
1321    if raw < 0 {
1322        m.neg()
1323    } else {
1324        m
1325    }
1326}
1327
1328/// Shared `atan2` body factored out so the `_strict` and `_approx`
1329/// dispatchers can compose it at their chosen working scale `w`.
1330/// `y_raw` keeps the original sign of the y-argument for the x-zero
1331/// branch where the wide y value would have been signed-zero.
1332fn atan2_kernel(
1333    y: crate::d_w128_kernels::Fixed,
1334    x: crate::d_w128_kernels::Fixed,
1335    y_raw: i128,
1336    w: u32,
1337) -> crate::d_w128_kernels::Fixed {
1338    use crate::d_w128_kernels::Fixed;
1339    if x.is_zero() {
1340        return if y_raw > 0 {
1341            wide_half_pi(w)
1342        } else if y_raw < 0 {
1343            wide_half_pi(w).neg()
1344        } else {
1345            Fixed::ZERO
1346        };
1347    }
1348    // Max-branch: feed atan_fixed the |smaller|/|larger| ratio so the
1349    // argument-halving cascade doesn't blow up when |y| ≫ |x|.
1350    let abs_y_ge_abs_x = y.ge_mag(x);
1351    let base = if !abs_y_ge_abs_x {
1352        atan_fixed(y.div(x, w), w)
1353    } else {
1354        let inv = atan_fixed(x.div(y, w), w);
1355        let hp = wide_half_pi(w);
1356        let same_sign = y.negative == x.negative;
1357        if same_sign { hp.sub(inv) } else { hp.neg().sub(inv) }
1358    };
1359    if !x.negative {
1360        base
1361    } else if !y.negative {
1362        base.add(wide_pi(w))
1363    } else {
1364        base.sub(wide_pi(w))
1365    }
1366}
1367
1368/// Taylor series for `sin` on a reduced non-negative argument
1369/// `r ∈ [0, π/2]`, evaluated at working scale `w`.
1370fn sin_taylor(r: crate::d_w128_kernels::Fixed, w: u32) -> crate::d_w128_kernels::Fixed {
1371    let r2 = r.mul(r, w);
1372    let mut sum = r;
1373    let mut term = r; // term = r^(2k-1)
1374    let mut k: u128 = 1;
1375    loop {
1376        // term_k = term_{k-1} · r² / ((2k)(2k+1)); sign alternates.
1377        term = term.mul(r2, w).div_small((2 * k) * (2 * k + 1));
1378        if term.is_zero() {
1379            break;
1380        }
1381        if k % 2 == 1 {
1382            sum = sum.sub(term);
1383        } else {
1384            sum = sum.add(term);
1385        }
1386        k += 1;
1387        if k > 200 {
1388            break;
1389        }
1390    }
1391    sum
1392}
1393
1394/// Sine of a working-scale value `v_w`, at working scale `w`.
1395///
1396/// Reduces `v` modulo τ via `q = round(v/τ)`, folds the remainder into
1397/// `[0, π/2]` tracking sign and the `π − x` reflection, then evaluates
1398/// the Taylor series.
1399fn sin_fixed(v_w: crate::d_w128_kernels::Fixed, w: u32) -> crate::d_w128_kernels::Fixed {
1400    use crate::d_w128_kernels::Fixed;
1401    let tau = wide_tau(w);
1402    let pi = wide_pi(w);
1403    let half_pi = wide_half_pi(w);
1404
1405    // r = v - round(v/τ)·τ ∈ [-π, π].
1406    let q = v_w.div(tau, w).round_to_nearest_int(w);
1407    let q_tau = if q >= 0 {
1408        tau.mul_u128(q as u128)
1409    } else {
1410        tau.mul_u128((-q) as u128).neg()
1411    };
1412    let r = v_w.sub(q_tau);
1413
1414    // Fold |r| ∈ [0, π] into [0, π/2] via sin(π − x) = sin(x).
1415    let sign = r.negative;
1416    let abs_r = Fixed { negative: false, mag: r.mag };
1417    let reduced = if abs_r.ge_mag(half_pi) {
1418        pi.sub(abs_r)
1419    } else {
1420        abs_r
1421    };
1422    let s = sin_taylor(reduced, w);
1423    if sign {
1424        s.neg()
1425    } else {
1426        s
1427    }
1428}
1429
1430/// Taylor series for `atan` on a reduced non-negative argument
1431/// `x ∈ [0, ~1/8]`, evaluated at working scale `w`.
1432fn atan_taylor(x: crate::d_w128_kernels::Fixed, w: u32) -> crate::d_w128_kernels::Fixed {
1433    let x2 = x.mul(x, w);
1434    let mut sum = x;
1435    let mut term = x; // term = x^(2k-1)
1436    let mut k: u128 = 1;
1437    loop {
1438        term = term.mul(x2, w);
1439        let contrib = term.div_small(2 * k + 1);
1440        if contrib.is_zero() {
1441            break;
1442        }
1443        if k % 2 == 1 {
1444            sum = sum.sub(contrib);
1445        } else {
1446            sum = sum.add(contrib);
1447        }
1448        k += 1;
1449        if k > 300 {
1450            break;
1451        }
1452    }
1453    sum
1454}
1455
1456/// Arctangent of a working-scale value `v_w`, at working scale `w`,
1457/// result in `(−π/2, π/2)`.
1458///
1459/// Odd-function fold to `x ≥ 0`; reciprocal reduction
1460/// `atan(x) = π/2 − atan(1/x)` for `x > 1`; three rounds of argument
1461/// halving `atan(x) = 2·atan(x / (1 + √(1+x²)))`; then the series.
1462fn atan_fixed(v_w: crate::d_w128_kernels::Fixed, w: u32) -> crate::d_w128_kernels::Fixed {
1463    use crate::d_w128_kernels::Fixed;
1464
1465    #[cfg(feature = "perf-trace")]
1466    let _atan_span = ::tracing::info_span!("atan_fixed").entered();
1467
1468    #[cfg(feature = "perf-trace")]
1469    let _setup_span = ::tracing::info_span!("setup").entered();
1470    let one_w = Fixed { negative: false, mag: Fixed::pow10(w) };
1471    let sign = v_w.negative;
1472    let mut x = Fixed { negative: false, mag: v_w.mag };
1473    let mut add_half_pi = false;
1474    if x.ge_mag(one_w) && x != one_w {
1475        x = one_w.div(x, w); // atan(x) = π/2 − atan(1/x)
1476        add_half_pi = true;
1477    }
1478    #[cfg(feature = "perf-trace")]
1479    drop(_setup_span);
1480
1481    // Adaptive argument halvings: atan(x) = 2·atan(x / (1 + √(1+x²))).
1482    // Halve only while |x| > ~0.2 (the Taylor convergence target);
1483    // matches g_math's adaptive-halvings approach. Skips halvings
1484    // entirely when the input is already small (e.g. `atan(0.1)`
1485    // would previously waste 3 halvings to reach 0.0125 — now 0).
1486    // Hard cap at 8 halvings as a safety net against pathological
1487    // edge cases; the post-reciprocal-reduce `x` always falls below
1488    // the threshold long before that.
1489    #[cfg(feature = "perf-trace")]
1490    let _halvings_span = ::tracing::info_span!("halvings").entered();
1491    let halving_threshold = one_w.div_small(5); // 0.2 at scale w
1492    let mut halvings: u32 = 0;
1493    while x.ge_mag(halving_threshold) && halvings < 8 {
1494        let x2 = x.mul(x, w);
1495        let denom = one_w.add(one_w.add(x2).sqrt(w));
1496        x = x.div(denom, w);
1497        halvings += 1;
1498    }
1499    #[cfg(feature = "perf-trace")]
1500    drop(_halvings_span);
1501
1502    #[cfg(feature = "perf-trace")]
1503    let _taylor_span = ::tracing::info_span!("taylor").entered();
1504    let mut result = atan_taylor(x, w);
1505    #[cfg(feature = "perf-trace")]
1506    drop(_taylor_span);
1507
1508    #[cfg(feature = "perf-trace")]
1509    let _reasm_span = ::tracing::info_span!("reassemble").entered();
1510    result = result.shl(halvings);
1511    if add_half_pi {
1512        result = wide_half_pi(w).sub(result);
1513    }
1514    if sign {
1515        result.neg()
1516    } else {
1517        result
1518    }
1519}
1520
1521#[cfg(test)]
1522mod tests {
1523    use crate::consts::DecimalConsts;
1524    use crate::core_type::D38s12;
1525
1526    // Tolerance for single-operation results. The f64-bridge build is
1527    // one f64 round-trip (≤ 2 LSB); the integer-only `strict` build is
1528    // correctly rounded (≤ 0.5 ULP per call) and is held to the same
1529    // 2-LSB bound — a couple of LSB for the test's own expected-value
1530    // rounding.
1531    const TWO_LSB: i128 = 2;
1532
1533    // Tolerance for results that chain multiple trig calls (e.g.
1534    // `sin² + cos²`, `cosh² − sinh²`): each input is within 0.5 ULP, so
1535    // the composed quantity stays within a few LSB in both builds.
1536    const FOUR_LSB: i128 = 4;
1537
1538    // Angle-conversion results compared against exact integer targets
1539    // (180, 90, 45 degrees). The `pi()` / `quarter_pi()` *input*
1540    // constants are themselves rounded to the type's scale, and
1541    // `to_degrees` amplifies that input quantization by ~57.3 — so even
1542    // a perfectly-rounded conversion lands ~30 LSB off the exact
1543    // integer at SCALE = 12. (This bounds the *input*, not the
1544    // conversion: `to_degrees` itself is correctly rounded in `strict`.)
1545    const ANGLE_TOLERANCE_LSB: i128 = 32;
1546
1547    fn within_lsb(actual: D38s12, expected: D38s12, lsb: i128) -> bool {
1548        let diff = (actual.to_bits() - expected.to_bits()).abs();
1549        diff <= lsb
1550    }
1551
1552    // ── Forward trig ──────────────────────────────────────────────────
1553
1554    /// The strict trig / hyperbolic family is correctly rounded:
1555    /// cross-check every method against the f64 bridge at D38<9>,
1556    /// where f64 (≈ 15–16 significant digits) is comfortably more
1557    /// precise than the type's ULP, so a correctly-rounded integer
1558    /// result must agree to within 1 ULP (allow 1 more for the f64
1559    /// reference's own rounding).
1560    #[cfg(all(feature = "strict", not(feature = "fast")))]
1561    #[test]
1562    fn strict_trig_family_matches_f64() {
1563        use crate::core_type::D38;
1564        macro_rules! check {
1565            ($name:literal, $raw:expr, $strict:expr, $f64expr:expr) => {{
1566                let strict: i128 = $strict;
1567                let v = $raw as f64 / 1e9;
1568                let reference = ($f64expr(v) * 1e9).round() as i128;
1569                assert!(
1570                    (strict - reference).abs() <= 2,
1571                    concat!($name, "({}) = {}, f64 reference {}"),
1572                    $raw,
1573                    strict,
1574                    reference
1575                );
1576            }};
1577        }
1578        // Forward trig — arguments across a few periods, incl. negative.
1579        for &raw in &[
1580            -7_000_000_000_i128, -1_000_000_000, -100_000_000, 1,
1581            500_000_000, 1_000_000_000, 1_570_796_327, 3_000_000_000,
1582            6_283_185_307, 12_000_000_000,
1583        ] {
1584            let x = D38::<9>::from_bits(raw);
1585            check!("sin", raw, x.sin_strict().to_bits(), f64::sin);
1586            check!("cos", raw, x.cos_strict().to_bits(), f64::cos);
1587            check!("atan", raw, x.atan_strict().to_bits(), f64::atan);
1588            check!("sinh", raw, x.sinh_strict().to_bits(), f64::sinh);
1589            check!("cosh", raw, x.cosh_strict().to_bits(), f64::cosh);
1590            check!("tanh", raw, x.tanh_strict().to_bits(), f64::tanh);
1591            check!("asinh", raw, x.asinh_strict().to_bits(), f64::asinh);
1592        }
1593        // asin / acos — domain [-1, 1].
1594        for &raw in &[
1595            -1_000_000_000_i128, -700_000_000, -100_000_000, 0,
1596            250_000_000, 500_000_000, 999_999_999,
1597        ] {
1598            let x = D38::<9>::from_bits(raw);
1599            check!("asin", raw, x.asin_strict().to_bits(), f64::asin);
1600            check!("acos", raw, x.acos_strict().to_bits(), f64::acos);
1601        }
1602        // atanh — domain (-1, 1).
1603        for &raw in &[-900_000_000_i128, -300_000_000, 1, 300_000_000, 900_000_000] {
1604            let x = D38::<9>::from_bits(raw);
1605            check!("atanh", raw, x.atanh_strict().to_bits(), f64::atanh);
1606        }
1607        // acosh — domain [1, ∞).
1608        for &raw in &[1_000_000_000_i128, 1_500_000_000, 3_000_000_000, 50_000_000_000] {
1609            let x = D38::<9>::from_bits(raw);
1610            check!("acosh", raw, x.acosh_strict().to_bits(), f64::acosh);
1611        }
1612        // tan — avoid the poles.
1613        for &raw in &[-1_000_000_000_i128, 1, 500_000_000, 1_000_000_000, 1_400_000_000] {
1614            let x = D38::<9>::from_bits(raw);
1615            check!("tan", raw, x.tan_strict().to_bits(), f64::tan);
1616        }
1617    }
1618
1619    /// `sin(0) == 0` -- bit-exact via `f64::sin(0.0) == 0.0`.
1620    #[test]
1621    fn sin_zero_is_zero() {
1622        assert_eq!(D38s12::ZERO.sin(), D38s12::ZERO);
1623    }
1624
1625    /// Regression: D38 strict trig at high SCALE drives the working
1626    /// scale `w = SCALE + STRICT_GUARD` past the old hard-coded
1627    /// 63-digit π constant. With the previous wide_pi the
1628    /// `rescale_down(63, w > 63)` call wrapped `from_w - to_w` as u32
1629    /// and produced a silently-wrong π. Verify two representative
1630    /// SCALEs above the old window (working scale 65 and 67) land
1631    /// within 1 LSB of the high-precision canonical sin(1) value.
1632    ///
1633    /// Reference: sin(1) = 0.8414709848078965066525023216302989996226...
1634    /// (OEIS A049469, 40 digits).
1635    #[cfg(all(feature = "strict", not(feature = "fast")))]
1636    #[test]
1637    fn sin_one_correct_past_63_digit_pi_window() {
1638        use crate::core_type::D38;
1639        // sin(1) × 10^35, rounded half-to-even:
1640        // 0.84147098480789650665250232163029899|96226... → ...029900
1641        let expected_35: i128 = 84_147_098_480_789_650_665_250_232_163_029_900;
1642        // sin(1) × 10^37, rounded:
1643        // 0.8414709848078965066525023216302989996|226... → ...02989996
1644        let expected_37: i128 =
1645            8_414_709_848_078_965_066_525_023_216_302_989_996;
1646
1647        let got_35 = D38::<35>::ONE.sin_strict().to_bits();
1648        assert!(
1649            (got_35 - expected_35).abs() <= 1,
1650            "sin(1) @ D38<35>: got {got_35}, expected {expected_35}"
1651        );
1652
1653        let got_37 = D38::<37>::ONE.sin_strict().to_bits();
1654        assert!(
1655            (got_37 - expected_37).abs() <= 1,
1656            "sin(1) @ D38<37>: got {got_37}, expected {expected_37}"
1657        );
1658    }
1659
1660    /// `cos(0) == 1` -- bit-exact via `f64::cos(0.0) == 1.0`.
1661    #[test]
1662    fn cos_zero_is_one() {
1663        assert_eq!(D38s12::ZERO.cos(), D38s12::ONE);
1664    }
1665
1666    /// `tan(0) == 0` -- bit-exact via `f64::tan(0.0) == 0.0`.
1667    #[test]
1668    fn tan_zero_is_zero() {
1669        assert_eq!(D38s12::ZERO.tan(), D38s12::ZERO);
1670    }
1671
1672    /// Pythagorean identity: `sin^2(x) + cos^2(x) ~= 1` within 4 LSB
1673    /// for representative values of `x`. Values are chosen to be well
1674    /// away from any well-known mathematical constant.
1675    #[test]
1676    fn sin_squared_plus_cos_squared_is_one() {
1677        for raw in [
1678            1_234_567_890_123_i128,  // ~1.234567...
1679            -2_345_678_901_234_i128, // ~-2.345678...
1680            500_000_000_000_i128,    // 0.5
1681            -500_000_000_000_i128,   // -0.5
1682            4_567_891_234_567_i128,  // ~4.567891...
1683        ] {
1684            let x = D38s12::from_bits(raw);
1685            let s = x.sin();
1686            let c = x.cos();
1687            let sum = (s * s) + (c * c);
1688            assert!(
1689                within_lsb(sum, D38s12::ONE, FOUR_LSB),
1690                "sin^2 + cos^2 != 1 for raw={raw}: got bits {} (delta {})",
1691                sum.to_bits(),
1692                (sum.to_bits() - D38s12::ONE.to_bits()).abs(),
1693            );
1694        }
1695    }
1696
1697    // ── Inverse trig ──────────────────────────────────────────────────
1698
1699    /// `asin(0) == 0` -- bit-exact.
1700    #[test]
1701    fn asin_zero_is_zero() {
1702        assert_eq!(D38s12::ZERO.asin(), D38s12::ZERO);
1703    }
1704
1705    /// `acos(1) == 0` -- bit-exact via `f64::acos(1.0) == 0.0`.
1706    #[test]
1707    fn acos_one_is_zero() {
1708        assert_eq!(D38s12::ONE.acos(), D38s12::ZERO);
1709    }
1710
1711    /// `acos(0) ~= pi/2` within 4 LSB.
1712    #[test]
1713    fn acos_zero_is_half_pi() {
1714        let result = D38s12::ZERO.acos();
1715        assert!(
1716            within_lsb(result, D38s12::half_pi(), FOUR_LSB),
1717            "acos(0) bits {}, half_pi bits {}",
1718            result.to_bits(),
1719            D38s12::half_pi().to_bits(),
1720        );
1721    }
1722
1723    /// `atan(0) == 0` -- bit-exact via `f64::atan(0.0) == 0.0`.
1724    #[test]
1725    fn atan_zero_is_zero() {
1726        assert_eq!(D38s12::ZERO.atan(), D38s12::ZERO);
1727    }
1728
1729    /// Round-trip identity: `asin(sin(x)) ~= x` for `x` in
1730    /// `[-pi/2, pi/2]`. Values stay within the principal branch.
1731    #[test]
1732    fn asin_of_sin_round_trip() {
1733        for raw in [
1734            123_456_789_012_i128,    // ~0.123456...
1735            -123_456_789_012_i128,   // ~-0.123456...
1736            456_789_012_345_i128,    // ~0.456789...
1737            -456_789_012_345_i128,   // ~-0.456789...
1738            1_234_567_890_123_i128,  // ~1.234567... (well inside pi/2)
1739            -1_234_567_890_123_i128, // ~-1.234567...
1740        ] {
1741            let x = D38s12::from_bits(raw);
1742            let recovered = x.sin().asin();
1743            assert!(
1744                within_lsb(recovered, x, FOUR_LSB),
1745                "asin(sin(x)) != x for raw={raw}: got bits {} (delta {})",
1746                recovered.to_bits(),
1747                (recovered.to_bits() - x.to_bits()).abs(),
1748            );
1749        }
1750    }
1751
1752    // ── atan2 ─────────────────────────────────────────────────────────
1753
1754    /// `atan2(1, 1) ~= pi/4` (first-quadrant 45 degrees).
1755    #[test]
1756    fn atan2_first_quadrant_diagonal() {
1757        let one = D38s12::ONE;
1758        let result = one.atan2(one);
1759        assert!(
1760            within_lsb(result, D38s12::quarter_pi(), TWO_LSB),
1761            "atan2(1, 1) bits {}, quarter_pi bits {}",
1762            result.to_bits(),
1763            D38s12::quarter_pi().to_bits(),
1764        );
1765    }
1766
1767    /// `atan2(-1, -1) ~= -3*pi/4` (third-quadrant correctness).
1768    #[test]
1769    fn atan2_third_quadrant_diagonal() {
1770        let neg_one = -D38s12::ONE;
1771        let result = neg_one.atan2(neg_one);
1772        let three = D38s12::from_int(3);
1773        let expected = -(D38s12::quarter_pi() * three);
1774        assert!(
1775            within_lsb(result, expected, TWO_LSB),
1776            "atan2(-1, -1) bits {}, expected -3pi/4 bits {}",
1777            result.to_bits(),
1778            expected.to_bits(),
1779        );
1780    }
1781
1782    /// `atan2(1, -1) ~= 3*pi/4` (second-quadrant correctness).
1783    #[test]
1784    fn atan2_second_quadrant_diagonal() {
1785        let one = D38s12::ONE;
1786        let neg_one = -D38s12::ONE;
1787        let result = one.atan2(neg_one);
1788        let three = D38s12::from_int(3);
1789        let expected = D38s12::quarter_pi() * three;
1790        assert!(
1791            within_lsb(result, expected, TWO_LSB),
1792            "atan2(1, -1) bits {}, expected 3pi/4 bits {}",
1793            result.to_bits(),
1794            expected.to_bits(),
1795        );
1796    }
1797
1798    /// `atan2(-1, 1) ~= -pi/4` (fourth-quadrant correctness).
1799    #[test]
1800    fn atan2_fourth_quadrant_diagonal() {
1801        let one = D38s12::ONE;
1802        let neg_one = -D38s12::ONE;
1803        let result = neg_one.atan2(one);
1804        let expected = -D38s12::quarter_pi();
1805        assert!(
1806            within_lsb(result, expected, TWO_LSB),
1807            "atan2(-1, 1) bits {}, expected -pi/4 bits {}",
1808            result.to_bits(),
1809            expected.to_bits(),
1810        );
1811    }
1812
1813    /// `atan2(0, 1) == 0` (positive x-axis is bit-exact).
1814    #[test]
1815    fn atan2_positive_x_axis_is_zero() {
1816        let zero = D38s12::ZERO;
1817        let one = D38s12::ONE;
1818        assert_eq!(zero.atan2(one), D38s12::ZERO);
1819    }
1820
1821    // ── Hyperbolic ────────────────────────────────────────────────────
1822
1823    /// `sinh(0) == 0` -- bit-exact via `f64::sinh(0.0) == 0.0`.
1824    #[test]
1825    fn sinh_zero_is_zero() {
1826        assert_eq!(D38s12::ZERO.sinh(), D38s12::ZERO);
1827    }
1828
1829    /// `cosh(0) == 1` -- bit-exact via `f64::cosh(0.0) == 1.0`.
1830    #[test]
1831    fn cosh_zero_is_one() {
1832        assert_eq!(D38s12::ZERO.cosh(), D38s12::ONE);
1833    }
1834
1835    /// `tanh(0) == 0` -- bit-exact via `f64::tanh(0.0) == 0.0`.
1836    #[test]
1837    fn tanh_zero_is_zero() {
1838        assert_eq!(D38s12::ZERO.tanh(), D38s12::ZERO);
1839    }
1840
1841    /// `asinh(0) == 0` -- bit-exact.
1842    #[test]
1843    fn asinh_zero_is_zero() {
1844        assert_eq!(D38s12::ZERO.asinh(), D38s12::ZERO);
1845    }
1846
1847    /// `acosh(1) == 0` -- bit-exact via `f64::acosh(1.0) == 0.0`.
1848    #[test]
1849    fn acosh_one_is_zero() {
1850        assert_eq!(D38s12::ONE.acosh(), D38s12::ZERO);
1851    }
1852
1853    /// `atanh(0) == 0` -- bit-exact.
1854    #[test]
1855    fn atanh_zero_is_zero() {
1856        assert_eq!(D38s12::ZERO.atanh(), D38s12::ZERO);
1857    }
1858
1859    /// Identity: `cosh^2(x) - sinh^2(x) == 1` within 4 LSB for
1860    /// representative values of `x`.
1861    #[test]
1862    fn cosh_squared_minus_sinh_squared_is_one() {
1863        if !crate::rounding::DEFAULT_IS_HALF_TO_EVEN { return; }
1864        for raw in [
1865            500_000_000_000_i128,    // 0.5
1866            -500_000_000_000_i128,   // -0.5
1867            1_234_567_890_123_i128,  // ~1.234567
1868            -1_234_567_890_123_i128, // ~-1.234567
1869            2_500_000_000_000_i128,  // 2.5
1870        ] {
1871            let x = D38s12::from_bits(raw);
1872            let ch = x.cosh();
1873            let sh = x.sinh();
1874            let diff = (ch * ch) - (sh * sh);
1875            assert!(
1876                within_lsb(diff, D38s12::ONE, FOUR_LSB),
1877                "cosh^2 - sinh^2 != 1 for raw={raw}: got bits {} (delta {})",
1878                diff.to_bits(),
1879                (diff.to_bits() - D38s12::ONE.to_bits()).abs(),
1880            );
1881        }
1882    }
1883
1884    // ── Angle conversions ─────────────────────────────────────────────
1885
1886    /// `to_degrees(pi) ~= 180` within `ANGLE_TOLERANCE_LSB`. The
1887    /// tolerance is dominated by f64's limited precision on `pi`,
1888    /// amplified by ~57.3 during the degrees conversion.
1889    #[test]
1890    fn to_degrees_pi_is_180() {
1891        if !crate::rounding::DEFAULT_IS_HALF_TO_EVEN { return; }
1892        let pi = D38s12::pi();
1893        let result = pi.to_degrees();
1894        let expected = D38s12::from_int(180);
1895        assert!(
1896            within_lsb(result, expected, ANGLE_TOLERANCE_LSB),
1897            "to_degrees(pi) bits {}, expected 180 bits {} (delta {})",
1898            result.to_bits(),
1899            expected.to_bits(),
1900            (result.to_bits() - expected.to_bits()).abs(),
1901        );
1902    }
1903
1904    /// `to_radians(180) ~= pi` within `ANGLE_TOLERANCE_LSB`.
1905    #[test]
1906    fn to_radians_180_is_pi() {
1907        let one_eighty = D38s12::from_int(180);
1908        let result = one_eighty.to_radians();
1909        let expected = D38s12::pi();
1910        assert!(
1911            within_lsb(result, expected, ANGLE_TOLERANCE_LSB),
1912            "to_radians(180) bits {}, expected pi bits {} (delta {})",
1913            result.to_bits(),
1914            expected.to_bits(),
1915            (result.to_bits() - expected.to_bits()).abs(),
1916        );
1917    }
1918
1919    /// `to_degrees(0) == 0` -- bit-exact (0 * anything == 0).
1920    #[test]
1921    fn to_degrees_zero_is_zero() {
1922        assert_eq!(D38s12::ZERO.to_degrees(), D38s12::ZERO);
1923    }
1924
1925    /// `to_radians(0) == 0` -- bit-exact.
1926    #[test]
1927    fn to_radians_zero_is_zero() {
1928        assert_eq!(D38s12::ZERO.to_radians(), D38s12::ZERO);
1929    }
1930
1931    /// Round-trip: `to_radians(to_degrees(x)) ~= x` within 4 LSB
1932    /// (two f64 round-trips).
1933    #[test]
1934    fn to_radians_to_degrees_round_trip() {
1935        for raw in [
1936            500_000_000_000_i128,    // 0.5
1937            -500_000_000_000_i128,   // -0.5
1938            1_234_567_890_123_i128,  // ~1.234567
1939            -2_345_678_901_234_i128, // ~-2.345678
1940        ] {
1941            let x = D38s12::from_bits(raw);
1942            let recovered = x.to_degrees().to_radians();
1943            assert!(
1944                within_lsb(recovered, x, FOUR_LSB),
1945                "to_radians(to_degrees(x)) != x for raw={raw}: got bits {} (delta {})",
1946                recovered.to_bits(),
1947                (recovered.to_bits() - x.to_bits()).abs(),
1948            );
1949        }
1950    }
1951
1952    /// `to_degrees(half_pi) ~= 90` within `ANGLE_TOLERANCE_LSB`.
1953    #[test]
1954    fn to_degrees_half_pi_is_90() {
1955        if !crate::rounding::DEFAULT_IS_HALF_TO_EVEN { return; }
1956        let result = D38s12::half_pi().to_degrees();
1957        let expected = D38s12::from_int(90);
1958        assert!(
1959            within_lsb(result, expected, ANGLE_TOLERANCE_LSB),
1960            "to_degrees(half_pi) bits {}, expected 90 bits {} (delta {})",
1961            result.to_bits(),
1962            expected.to_bits(),
1963            (result.to_bits() - expected.to_bits()).abs(),
1964        );
1965    }
1966
1967    /// `to_degrees(quarter_pi) ~= 45` within `ANGLE_TOLERANCE_LSB`.
1968    #[test]
1969    fn to_degrees_quarter_pi_is_45() {
1970        let result = D38s12::quarter_pi().to_degrees();
1971        let expected = D38s12::from_int(45);
1972        assert!(
1973            within_lsb(result, expected, ANGLE_TOLERANCE_LSB),
1974            "to_degrees(quarter_pi) bits {}, expected 45 bits {} (delta {})",
1975            result.to_bits(),
1976            expected.to_bits(),
1977            (result.to_bits() - expected.to_bits()).abs(),
1978        );
1979    }
1980
1981    // ── Cross-method consistency ──────────────────────────────────────
1982
1983    /// `tan(x) ~= sin(x) / cos(x)` within 4 LSB for `x` away from
1984    /// odd multiples of `pi/2`.
1985    #[test]
1986    fn tan_matches_sin_over_cos() {
1987        for raw in [
1988            500_000_000_000_i128,    // 0.5
1989            -500_000_000_000_i128,   // -0.5
1990            1_000_000_000_000_i128,  // 1.0 (cos(1.0) ~= 0.54, safe)
1991            -1_000_000_000_000_i128, // -1.0
1992            123_456_789_012_i128,    // ~0.123456
1993        ] {
1994            let x = D38s12::from_bits(raw);
1995            let t = x.tan();
1996            let sc = x.sin() / x.cos();
1997            assert!(
1998                within_lsb(t, sc, FOUR_LSB),
1999                "tan(x) != sin/cos for raw={raw}: tan bits {}, sin/cos bits {}",
2000                t.to_bits(),
2001                sc.to_bits(),
2002            );
2003        }
2004    }
2005
2006    /// `tanh(x) ~= sinh(x) / cosh(x)` within 4 LSB. `cosh` is always
2007    /// positive so there is no divide-by-zero risk.
2008    #[test]
2009    fn tanh_matches_sinh_over_cosh() {
2010        for raw in [
2011            500_000_000_000_i128,    // 0.5
2012            -500_000_000_000_i128,   // -0.5
2013            1_234_567_890_123_i128,  // ~1.234567
2014            -2_345_678_901_234_i128, // ~-2.345678
2015        ] {
2016            let x = D38s12::from_bits(raw);
2017            let t = x.tanh();
2018            let sc = x.sinh() / x.cosh();
2019            assert!(
2020                within_lsb(t, sc, FOUR_LSB),
2021                "tanh(x) != sinh/cosh for raw={raw}: tanh bits {}, sinh/cosh bits {}",
2022                t.to_bits(),
2023                sc.to_bits(),
2024            );
2025        }
2026    }
2027}
2028