Skip to main content

decimal_scaled/types/
trig.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//! All four variants are integer-only, `no_std`-compatible, and
28//! correctly rounded under the selected mode. Without the `strict`
29//! feature, the plain `<fn>` is an f64-bridge instead.
30//!
31//! # Layering
32//!
33//! Every public method on this file is a one-line delegate into
34//! `policy::trig::TrigPolicy`. The correctly-rounded kernels
35//! (`sin_fixed`, `atan_fixed`, `atan2_kernel`, `to_fixed`, `wide_pi`,
36//! `wide_half_pi`, `small_x_linear_threshold`, and every per-method
37//! `*_strict` / `*_with` `Fixed`-shape function for sin / cos / tan /
38//! atan / asin / acos / atan2 / sinh / cosh / tanh / asinh / acosh /
39//! atanh / to_degrees / to_radians) live in
40//! [`crate::algos::trig::fixed_d38`]. This file is a typed-shell
41//! surface; there are zero `crate::algos::*` or
42//! `crate::algos::fixed_d38::*` references in it.
43//!
44//! [`RoundingMode::HalfToEven`]: crate::RoundingMode::HalfToEven
45//!
46//! # `atan2` signature
47//!
48//! `f64::atan2(self, other)` treats `self` as `y` and `other` as `x`.
49//! This module matches that signature exactly so generic numeric code
50//! calling `y.atan2(x)` works with `T = D38`.
51
52use crate::types::widths::D38;
53use crate::types::log_exp::STRICT_GUARD;
54
55impl<const SCALE: u32> D38<SCALE> {
56    // ── Plain dispatchers (strict-feature) ────────────────────────
57
58    #[cfg(all(feature = "strict", not(feature = "fast")))]
59    #[inline]
60    #[must_use]
61    pub fn sin(self) -> Self {
62        self.sin_strict()
63    }
64
65    #[cfg(all(feature = "strict", not(feature = "fast")))]
66    #[inline]
67    #[must_use]
68    pub fn cos(self) -> Self {
69        self.cos_strict()
70    }
71
72    #[cfg(all(feature = "strict", not(feature = "fast")))]
73    #[inline]
74    #[must_use]
75    pub fn tan(self) -> Self {
76        self.tan_strict()
77    }
78
79    #[cfg(all(feature = "strict", not(feature = "fast")))]
80    #[inline]
81    #[must_use]
82    pub fn asin(self) -> Self {
83        self.asin_strict()
84    }
85
86    #[cfg(all(feature = "strict", not(feature = "fast")))]
87    #[inline]
88    #[must_use]
89    pub fn acos(self) -> Self {
90        self.acos_strict()
91    }
92
93    #[cfg(all(feature = "strict", not(feature = "fast")))]
94    #[inline]
95    #[must_use]
96    pub fn atan(self) -> Self {
97        self.atan_strict()
98    }
99
100    #[cfg(all(feature = "strict", not(feature = "fast")))]
101    #[inline]
102    #[must_use]
103    pub fn atan2(self, other: Self) -> Self {
104        self.atan2_strict(other)
105    }
106
107    #[cfg(all(feature = "strict", not(feature = "fast")))]
108    #[inline]
109    #[must_use]
110    pub fn sinh(self) -> Self {
111        self.sinh_strict()
112    }
113
114    #[cfg(all(feature = "strict", not(feature = "fast")))]
115    #[inline]
116    #[must_use]
117    pub fn cosh(self) -> Self {
118        self.cosh_strict()
119    }
120
121    #[cfg(all(feature = "strict", not(feature = "fast")))]
122    #[inline]
123    #[must_use]
124    pub fn tanh(self) -> Self {
125        self.tanh_strict()
126    }
127
128    #[cfg(all(feature = "strict", not(feature = "fast")))]
129    #[inline]
130    #[must_use]
131    pub fn asinh(self) -> Self {
132        self.asinh_strict()
133    }
134
135    #[cfg(all(feature = "strict", not(feature = "fast")))]
136    #[inline]
137    #[must_use]
138    pub fn acosh(self) -> Self {
139        self.acosh_strict()
140    }
141
142    #[cfg(all(feature = "strict", not(feature = "fast")))]
143    #[inline]
144    #[must_use]
145    pub fn atanh(self) -> Self {
146        self.atanh_strict()
147    }
148
149    #[cfg(all(feature = "strict", not(feature = "fast")))]
150    #[inline]
151    #[must_use]
152    pub fn to_degrees(self) -> Self {
153        self.to_degrees_strict()
154    }
155
156    #[cfg(all(feature = "strict", not(feature = "fast")))]
157    #[inline]
158    #[must_use]
159    pub fn to_radians(self) -> Self {
160        self.to_radians_strict()
161    }
162
163    // ── Forward trig (one-line policy delegates) ──────────────────
164
165    /// Sine of `self` (radians). Correctly rounded.
166    #[inline]
167    #[must_use]
168    pub fn sin_strict(self) -> Self {
169        self.sin_strict_with(crate::support::rounding::DEFAULT_ROUNDING_MODE)
170    }
171
172    #[inline]
173    #[must_use]
174    pub fn sin_strict_with(self, mode: crate::support::rounding::RoundingMode) -> Self {
175        <Self as crate::policy::trig::TrigPolicy>::sin_impl(self, mode)
176    }
177
178    #[inline]
179    #[must_use]
180    pub fn sin_approx(self, working_digits: u32) -> Self {
181        self.sin_approx_with(working_digits, crate::support::rounding::DEFAULT_ROUNDING_MODE)
182    }
183
184    #[inline]
185    #[must_use]
186    pub fn sin_approx_with(self, working_digits: u32, mode: crate::support::rounding::RoundingMode) -> Self {
187        if working_digits == STRICT_GUARD {
188            return self.sin_strict_with(mode);
189        }
190        <Self as crate::policy::trig::TrigPolicy>::sin_with_impl(self, working_digits, mode)
191    }
192
193    /// Cosine of `self` (radians). `cos(x) = sin(x + π/2)`.
194    #[inline]
195    #[must_use]
196    pub fn cos_strict(self) -> Self {
197        self.cos_strict_with(crate::support::rounding::DEFAULT_ROUNDING_MODE)
198    }
199
200    #[inline]
201    #[must_use]
202    pub fn cos_strict_with(self, mode: crate::support::rounding::RoundingMode) -> Self {
203        <Self as crate::policy::trig::TrigPolicy>::cos_impl(self, mode)
204    }
205
206    #[inline]
207    #[must_use]
208    pub fn cos_approx(self, working_digits: u32) -> Self {
209        self.cos_approx_with(working_digits, crate::support::rounding::DEFAULT_ROUNDING_MODE)
210    }
211
212    #[inline]
213    #[must_use]
214    pub fn cos_approx_with(self, working_digits: u32, mode: crate::support::rounding::RoundingMode) -> Self {
215        if working_digits == STRICT_GUARD {
216            return self.cos_strict_with(mode);
217        }
218        <Self as crate::policy::trig::TrigPolicy>::cos_with_impl(self, working_digits, mode)
219    }
220
221    /// Tangent. Panics if `cos(self)` is zero.
222    #[inline]
223    #[must_use]
224    pub fn tan_strict(self) -> Self {
225        self.tan_strict_with(crate::support::rounding::DEFAULT_ROUNDING_MODE)
226    }
227
228    #[inline]
229    #[must_use]
230    pub fn tan_strict_with(self, mode: crate::support::rounding::RoundingMode) -> Self {
231        <Self as crate::policy::trig::TrigPolicy>::tan_impl(self, mode)
232    }
233
234    #[inline]
235    #[must_use]
236    pub fn tan_approx(self, working_digits: u32) -> Self {
237        self.tan_approx_with(working_digits, crate::support::rounding::DEFAULT_ROUNDING_MODE)
238    }
239
240    #[inline]
241    #[must_use]
242    pub fn tan_approx_with(self, working_digits: u32, mode: crate::support::rounding::RoundingMode) -> Self {
243        if working_digits == STRICT_GUARD {
244            return self.tan_strict_with(mode);
245        }
246        <Self as crate::policy::trig::TrigPolicy>::tan_with_impl(self, working_digits, mode)
247    }
248
249    /// Arctangent.
250    #[inline]
251    #[must_use]
252    pub fn atan_strict(self) -> Self {
253        self.atan_strict_with(crate::support::rounding::DEFAULT_ROUNDING_MODE)
254    }
255
256    #[inline]
257    #[must_use]
258    pub fn atan_strict_with(self, mode: crate::support::rounding::RoundingMode) -> Self {
259        <Self as crate::policy::trig::TrigPolicy>::atan_impl(self, mode)
260    }
261
262    #[inline]
263    #[must_use]
264    pub fn atan_approx(self, working_digits: u32) -> Self {
265        self.atan_approx_with(working_digits, crate::support::rounding::DEFAULT_ROUNDING_MODE)
266    }
267
268    #[inline]
269    #[must_use]
270    pub fn atan_approx_with(self, working_digits: u32, mode: crate::support::rounding::RoundingMode) -> Self {
271        if working_digits == STRICT_GUARD {
272            return self.atan_strict_with(mode);
273        }
274        <Self as crate::policy::trig::TrigPolicy>::atan_with_impl(self, working_digits, mode)
275    }
276
277    /// Arcsine. Panics if `|self| > 1`.
278    #[inline]
279    #[must_use]
280    pub fn asin_strict(self) -> Self {
281        self.asin_strict_with(crate::support::rounding::DEFAULT_ROUNDING_MODE)
282    }
283
284    #[inline]
285    #[must_use]
286    pub fn asin_strict_with(self, mode: crate::support::rounding::RoundingMode) -> Self {
287        <Self as crate::policy::trig::TrigPolicy>::asin_impl(self, mode)
288    }
289
290    #[inline]
291    #[must_use]
292    pub fn asin_approx(self, working_digits: u32) -> Self {
293        self.asin_approx_with(working_digits, crate::support::rounding::DEFAULT_ROUNDING_MODE)
294    }
295
296    #[inline]
297    #[must_use]
298    pub fn asin_approx_with(self, working_digits: u32, mode: crate::support::rounding::RoundingMode) -> Self {
299        if working_digits == STRICT_GUARD {
300            return self.asin_strict_with(mode);
301        }
302        <Self as crate::policy::trig::TrigPolicy>::asin_with_impl(self, working_digits, mode)
303    }
304
305    /// Arccosine. Panics if `|self| > 1`.
306    #[inline]
307    #[must_use]
308    pub fn acos_strict(self) -> Self {
309        self.acos_strict_with(crate::support::rounding::DEFAULT_ROUNDING_MODE)
310    }
311
312    #[inline]
313    #[must_use]
314    pub fn acos_strict_with(self, mode: crate::support::rounding::RoundingMode) -> Self {
315        <Self as crate::policy::trig::TrigPolicy>::acos_impl(self, mode)
316    }
317
318    #[inline]
319    #[must_use]
320    pub fn acos_approx(self, working_digits: u32) -> Self {
321        self.acos_approx_with(working_digits, crate::support::rounding::DEFAULT_ROUNDING_MODE)
322    }
323
324    #[inline]
325    #[must_use]
326    pub fn acos_approx_with(self, working_digits: u32, mode: crate::support::rounding::RoundingMode) -> Self {
327        if working_digits == STRICT_GUARD {
328            return self.acos_strict_with(mode);
329        }
330        <Self as crate::policy::trig::TrigPolicy>::acos_with_impl(self, working_digits, mode)
331    }
332
333    /// Four-quadrant arctangent of `self` (`y`) and `other` (`x`).
334    #[inline]
335    #[must_use]
336    pub fn atan2_strict(self, other: Self) -> Self {
337        self.atan2_strict_with(other, crate::support::rounding::DEFAULT_ROUNDING_MODE)
338    }
339
340    #[inline]
341    #[must_use]
342    pub fn atan2_strict_with(self, other: Self, mode: crate::support::rounding::RoundingMode) -> Self {
343        <Self as crate::policy::trig::TrigPolicy>::atan2_impl(self, other, mode)
344    }
345
346    #[inline]
347    #[must_use]
348    pub fn atan2_approx(self, other: Self, working_digits: u32) -> Self {
349        self.atan2_approx_with(other, working_digits, crate::support::rounding::DEFAULT_ROUNDING_MODE)
350    }
351
352    #[inline]
353    #[must_use]
354    pub fn atan2_approx_with(self, other: Self, working_digits: u32, mode: crate::support::rounding::RoundingMode) -> Self {
355        if working_digits == STRICT_GUARD {
356            return self.atan2_strict_with(other, mode);
357        }
358        <Self as crate::policy::trig::TrigPolicy>::atan2_with_impl(self, other, working_digits, mode)
359    }
360
361    // ── Hyperbolic family (one-line policy delegates) ─────────────
362
363    /// Hyperbolic sine. Correctly rounded.
364    #[inline]
365    #[must_use]
366    pub fn sinh_strict(self) -> Self {
367        self.sinh_strict_with(crate::support::rounding::DEFAULT_ROUNDING_MODE)
368    }
369
370    #[inline]
371    #[must_use]
372    pub fn sinh_strict_with(self, mode: crate::support::rounding::RoundingMode) -> Self {
373        <Self as crate::policy::trig::TrigPolicy>::sinh_impl(self, mode)
374    }
375
376    #[inline]
377    #[must_use]
378    pub fn sinh_approx(self, working_digits: u32) -> Self {
379        self.sinh_approx_with(working_digits, crate::support::rounding::DEFAULT_ROUNDING_MODE)
380    }
381
382    #[inline]
383    #[must_use]
384    pub fn sinh_approx_with(self, working_digits: u32, mode: crate::support::rounding::RoundingMode) -> Self {
385        if working_digits == STRICT_GUARD {
386            return self.sinh_strict_with(mode);
387        }
388        <Self as crate::policy::trig::TrigPolicy>::sinh_with_impl(self, working_digits, mode)
389    }
390
391    /// Hyperbolic cosine.
392    #[inline]
393    #[must_use]
394    pub fn cosh_strict(self) -> Self {
395        self.cosh_strict_with(crate::support::rounding::DEFAULT_ROUNDING_MODE)
396    }
397
398    #[inline]
399    #[must_use]
400    pub fn cosh_strict_with(self, mode: crate::support::rounding::RoundingMode) -> Self {
401        <Self as crate::policy::trig::TrigPolicy>::cosh_impl(self, mode)
402    }
403
404    #[inline]
405    #[must_use]
406    pub fn cosh_approx(self, working_digits: u32) -> Self {
407        self.cosh_approx_with(working_digits, crate::support::rounding::DEFAULT_ROUNDING_MODE)
408    }
409
410    #[inline]
411    #[must_use]
412    pub fn cosh_approx_with(self, working_digits: u32, mode: crate::support::rounding::RoundingMode) -> Self {
413        if working_digits == STRICT_GUARD {
414            return self.cosh_strict_with(mode);
415        }
416        <Self as crate::policy::trig::TrigPolicy>::cosh_with_impl(self, working_digits, mode)
417    }
418
419    /// Hyperbolic tangent.
420    #[inline]
421    #[must_use]
422    pub fn tanh_strict(self) -> Self {
423        self.tanh_strict_with(crate::support::rounding::DEFAULT_ROUNDING_MODE)
424    }
425
426    #[inline]
427    #[must_use]
428    pub fn tanh_strict_with(self, mode: crate::support::rounding::RoundingMode) -> Self {
429        <Self as crate::policy::trig::TrigPolicy>::tanh_impl(self, mode)
430    }
431
432    #[inline]
433    #[must_use]
434    pub fn tanh_approx(self, working_digits: u32) -> Self {
435        self.tanh_approx_with(working_digits, crate::support::rounding::DEFAULT_ROUNDING_MODE)
436    }
437
438    #[inline]
439    #[must_use]
440    pub fn tanh_approx_with(self, working_digits: u32, mode: crate::support::rounding::RoundingMode) -> Self {
441        if working_digits == STRICT_GUARD {
442            return self.tanh_strict_with(mode);
443        }
444        <Self as crate::policy::trig::TrigPolicy>::tanh_with_impl(self, working_digits, mode)
445    }
446
447    /// Inverse hyperbolic sine. `asinh(x) = sign · ln(|x| + √(x²+1))`.
448    #[inline]
449    #[must_use]
450    pub fn asinh_strict(self) -> Self {
451        self.asinh_strict_with(crate::support::rounding::DEFAULT_ROUNDING_MODE)
452    }
453
454    #[inline]
455    #[must_use]
456    pub fn asinh_strict_with(self, mode: crate::support::rounding::RoundingMode) -> Self {
457        <Self as crate::policy::trig::TrigPolicy>::asinh_impl(self, mode)
458    }
459
460    #[inline]
461    #[must_use]
462    pub fn asinh_approx(self, working_digits: u32) -> Self {
463        self.asinh_approx_with(working_digits, crate::support::rounding::DEFAULT_ROUNDING_MODE)
464    }
465
466    #[inline]
467    #[must_use]
468    pub fn asinh_approx_with(self, working_digits: u32, mode: crate::support::rounding::RoundingMode) -> Self {
469        if working_digits == STRICT_GUARD {
470            return self.asinh_strict_with(mode);
471        }
472        <Self as crate::policy::trig::TrigPolicy>::asinh_with_impl(self, working_digits, mode)
473    }
474
475    /// Inverse hyperbolic cosine. Panics if `self < 1`.
476    #[inline]
477    #[must_use]
478    pub fn acosh_strict(self) -> Self {
479        self.acosh_strict_with(crate::support::rounding::DEFAULT_ROUNDING_MODE)
480    }
481
482    #[inline]
483    #[must_use]
484    pub fn acosh_strict_with(self, mode: crate::support::rounding::RoundingMode) -> Self {
485        <Self as crate::policy::trig::TrigPolicy>::acosh_impl(self, mode)
486    }
487
488    #[inline]
489    #[must_use]
490    pub fn acosh_approx(self, working_digits: u32) -> Self {
491        self.acosh_approx_with(working_digits, crate::support::rounding::DEFAULT_ROUNDING_MODE)
492    }
493
494    #[inline]
495    #[must_use]
496    pub fn acosh_approx_with(self, working_digits: u32, mode: crate::support::rounding::RoundingMode) -> Self {
497        if working_digits == STRICT_GUARD {
498            return self.acosh_strict_with(mode);
499        }
500        <Self as crate::policy::trig::TrigPolicy>::acosh_with_impl(self, working_digits, mode)
501    }
502
503    /// Inverse hyperbolic tangent. Panics if `|self| >= 1`.
504    #[inline]
505    #[must_use]
506    pub fn atanh_strict(self) -> Self {
507        self.atanh_strict_with(crate::support::rounding::DEFAULT_ROUNDING_MODE)
508    }
509
510    #[inline]
511    #[must_use]
512    pub fn atanh_strict_with(self, mode: crate::support::rounding::RoundingMode) -> Self {
513        <Self as crate::policy::trig::TrigPolicy>::atanh_impl(self, mode)
514    }
515
516    #[inline]
517    #[must_use]
518    pub fn atanh_approx(self, working_digits: u32) -> Self {
519        self.atanh_approx_with(working_digits, crate::support::rounding::DEFAULT_ROUNDING_MODE)
520    }
521
522    #[inline]
523    #[must_use]
524    pub fn atanh_approx_with(self, working_digits: u32, mode: crate::support::rounding::RoundingMode) -> Self {
525        if working_digits == STRICT_GUARD {
526            return self.atanh_strict_with(mode);
527        }
528        <Self as crate::policy::trig::TrigPolicy>::atanh_with_impl(self, working_digits, mode)
529    }
530
531    // ── Angle conversions (one-line policy delegates) ─────────────
532
533    /// Convert radians to degrees: `self · (180 / π)`.
534    #[inline]
535    #[must_use]
536    pub fn to_degrees_strict(self) -> Self {
537        self.to_degrees_strict_with(crate::support::rounding::DEFAULT_ROUNDING_MODE)
538    }
539
540    #[inline]
541    #[must_use]
542    pub fn to_degrees_strict_with(self, mode: crate::support::rounding::RoundingMode) -> Self {
543        <Self as crate::policy::trig::TrigPolicy>::to_degrees_impl(self, mode)
544    }
545
546    #[inline]
547    #[must_use]
548    pub fn to_degrees_approx(self, working_digits: u32) -> Self {
549        self.to_degrees_approx_with(working_digits, crate::support::rounding::DEFAULT_ROUNDING_MODE)
550    }
551
552    #[inline]
553    #[must_use]
554    pub fn to_degrees_approx_with(self, working_digits: u32, mode: crate::support::rounding::RoundingMode) -> Self {
555        if working_digits == STRICT_GUARD {
556            return self.to_degrees_strict_with(mode);
557        }
558        <Self as crate::policy::trig::TrigPolicy>::to_degrees_with_impl(self, working_digits, mode)
559    }
560
561    /// Convert degrees to radians: `self · (π / 180)`.
562    #[inline]
563    #[must_use]
564    pub fn to_radians_strict(self) -> Self {
565        self.to_radians_strict_with(crate::support::rounding::DEFAULT_ROUNDING_MODE)
566    }
567
568    #[inline]
569    #[must_use]
570    pub fn to_radians_strict_with(self, mode: crate::support::rounding::RoundingMode) -> Self {
571        <Self as crate::policy::trig::TrigPolicy>::to_radians_impl(self, mode)
572    }
573
574    #[inline]
575    #[must_use]
576    pub fn to_radians_approx(self, working_digits: u32) -> Self {
577        self.to_radians_approx_with(working_digits, crate::support::rounding::DEFAULT_ROUNDING_MODE)
578    }
579
580    #[inline]
581    #[must_use]
582    pub fn to_radians_approx_with(self, working_digits: u32, mode: crate::support::rounding::RoundingMode) -> Self {
583        if working_digits == STRICT_GUARD {
584            return self.to_radians_strict_with(mode);
585        }
586        <Self as crate::policy::trig::TrigPolicy>::to_radians_with_impl(self, working_digits, mode)
587    }
588}
589
590#[cfg(test)]
591mod tests {
592    use crate::types::consts::DecimalConstants;
593    use crate::types::widths::D38s12;
594
595    // Tolerance for single-operation results. The f64-bridge build is
596    // one f64 round-trip (≤ 2 LSB); the integer-only `strict` build is
597    // correctly rounded (≤ 0.5 ULP per call) and is held to the same
598    // 2-LSB bound — a couple of LSB for the test's own expected-value
599    // rounding.
600    const TWO_LSB: i128 = 2;
601
602    // Tolerance for results that chain multiple trig calls.
603    const FOUR_LSB: i128 = 4;
604
605    // Angle conversions amplify the f64 reference's pi quantization;
606    // 32 LSB at SCALE = 12.
607    const ANGLE_TOLERANCE_LSB: i128 = 32;
608
609    fn within_lsb(actual: D38s12, expected: D38s12, lsb: i128) -> bool {
610        let diff = (actual.to_bits() - expected.to_bits()).abs();
611        diff <= lsb
612    }
613
614    // ── Forward trig ──────────────────────────────────────────────────
615
616    /// The strict trig / hyperbolic family is correctly rounded:
617    /// cross-check every method against the f64 bridge at D38<9>.
618    #[cfg(all(feature = "strict", not(feature = "fast")))]
619    #[test]
620    fn strict_trig_family_matches_f64() {
621        use crate::types::widths::D38;
622        macro_rules! check {
623            ($name:literal, $raw:expr, $strict:expr, $f64expr:expr) => {{
624                let strict: i128 = $strict;
625                let v = $raw as f64 / 1e9;
626                let reference = ($f64expr(v) * 1e9).round() as i128;
627                assert!(
628                    (strict - reference).abs() <= 2,
629                    concat!($name, "({}) = {}, f64 reference {}"),
630                    $raw,
631                    strict,
632                    reference
633                );
634            }};
635        }
636        for &raw in &[
637            -7_000_000_000_i128, -1_000_000_000, -100_000_000, 1,
638            500_000_000, 1_000_000_000, 1_570_796_327, 3_000_000_000,
639            6_283_185_307, 12_000_000_000,
640        ] {
641            let x = D38::<9>::from_bits(raw);
642            check!("sin", raw, x.sin_strict().to_bits(), f64::sin);
643            check!("cos", raw, x.cos_strict().to_bits(), f64::cos);
644            check!("atan", raw, x.atan_strict().to_bits(), f64::atan);
645            check!("sinh", raw, x.sinh_strict().to_bits(), f64::sinh);
646            check!("cosh", raw, x.cosh_strict().to_bits(), f64::cosh);
647            check!("tanh", raw, x.tanh_strict().to_bits(), f64::tanh);
648            check!("asinh", raw, x.asinh_strict().to_bits(), f64::asinh);
649        }
650        for &raw in &[
651            -1_000_000_000_i128, -700_000_000, -100_000_000, 0,
652            250_000_000, 500_000_000, 999_999_999,
653        ] {
654            let x = D38::<9>::from_bits(raw);
655            check!("asin", raw, x.asin_strict().to_bits(), f64::asin);
656            check!("acos", raw, x.acos_strict().to_bits(), f64::acos);
657        }
658        for &raw in &[-900_000_000_i128, -300_000_000, 1, 300_000_000, 900_000_000] {
659            let x = D38::<9>::from_bits(raw);
660            check!("atanh", raw, x.atanh_strict().to_bits(), f64::atanh);
661        }
662        for &raw in &[1_000_000_000_i128, 1_500_000_000, 3_000_000_000, 50_000_000_000] {
663            let x = D38::<9>::from_bits(raw);
664            check!("acosh", raw, x.acosh_strict().to_bits(), f64::acosh);
665        }
666        for &raw in &[-1_000_000_000_i128, 1, 500_000_000, 1_000_000_000, 1_400_000_000] {
667            let x = D38::<9>::from_bits(raw);
668            check!("tan", raw, x.tan_strict().to_bits(), f64::tan);
669        }
670    }
671
672    /// `sin(0) == 0`.
673    #[test]
674    fn sin_zero_is_zero() {
675        assert_eq!(D38s12::ZERO.sin(), D38s12::ZERO);
676    }
677
678    /// Regression: D38 strict trig at high SCALE drives the working
679    /// scale `w = SCALE + STRICT_GUARD` past the old hard-coded
680    /// 63-digit π constant.
681    #[cfg(all(feature = "strict", not(feature = "fast")))]
682    #[test]
683    fn sin_one_correct_past_63_digit_pi_window() {
684        use crate::types::widths::D38;
685        let expected_35: i128 = 84_147_098_480_789_650_665_250_232_163_029_900;
686        let expected_37: i128 =
687            8_414_709_848_078_965_066_525_023_216_302_989_996;
688
689        let got_35 = D38::<35>::ONE.sin_strict().to_bits();
690        assert!(
691            (got_35 - expected_35).abs() <= 1,
692            "sin(1) @ D38<35>: got {got_35}, expected {expected_35}"
693        );
694
695        let got_37 = D38::<37>::ONE.sin_strict().to_bits();
696        assert!(
697            (got_37 - expected_37).abs() <= 1,
698            "sin(1) @ D38<37>: got {got_37}, expected {expected_37}"
699        );
700    }
701
702    /// `cos(0) == 1`.
703    #[test]
704    fn cos_zero_is_one() {
705        assert_eq!(D38s12::ZERO.cos(), D38s12::ONE);
706    }
707
708    /// `tan(0) == 0`.
709    #[test]
710    fn tan_zero_is_zero() {
711        assert_eq!(D38s12::ZERO.tan(), D38s12::ZERO);
712    }
713
714    /// Pythagorean identity.
715    #[test]
716    fn sin_squared_plus_cos_squared_is_one() {
717        for raw in [
718            1_234_567_890_123_i128,
719            -2_345_678_901_234_i128,
720            500_000_000_000_i128,
721            -500_000_000_000_i128,
722            4_567_891_234_567_i128,
723        ] {
724            let x = D38s12::from_bits(raw);
725            let s = x.sin();
726            let c = x.cos();
727            let sum = (s * s) + (c * c);
728            assert!(
729                within_lsb(sum, D38s12::ONE, FOUR_LSB),
730                "sin^2 + cos^2 != 1 for raw={raw}: got bits {} (delta {})",
731                sum.to_bits(),
732                (sum.to_bits() - D38s12::ONE.to_bits()).abs(),
733            );
734        }
735    }
736
737    // ── Inverse trig ──────────────────────────────────────────────────
738
739    #[test]
740    fn asin_zero_is_zero() {
741        assert_eq!(D38s12::ZERO.asin(), D38s12::ZERO);
742    }
743
744    #[test]
745    fn acos_one_is_zero() {
746        assert_eq!(D38s12::ONE.acos(), D38s12::ZERO);
747    }
748
749    #[test]
750    fn acos_zero_is_half_pi() {
751        let result = D38s12::ZERO.acos();
752        assert!(
753            within_lsb(result, D38s12::half_pi(), FOUR_LSB),
754            "acos(0) bits {}, half_pi bits {}",
755            result.to_bits(),
756            D38s12::half_pi().to_bits(),
757        );
758    }
759
760    #[test]
761    fn atan_zero_is_zero() {
762        assert_eq!(D38s12::ZERO.atan(), D38s12::ZERO);
763    }
764
765    #[test]
766    fn asin_of_sin_round_trip() {
767        for raw in [
768            123_456_789_012_i128,
769            -123_456_789_012_i128,
770            456_789_012_345_i128,
771            -456_789_012_345_i128,
772            1_234_567_890_123_i128,
773            -1_234_567_890_123_i128,
774        ] {
775            let x = D38s12::from_bits(raw);
776            let recovered = x.sin().asin();
777            assert!(
778                within_lsb(recovered, x, FOUR_LSB),
779                "asin(sin(x)) != x for raw={raw}: got bits {} (delta {})",
780                recovered.to_bits(),
781                (recovered.to_bits() - x.to_bits()).abs(),
782            );
783        }
784    }
785
786    // ── atan2 ─────────────────────────────────────────────────────────
787
788    #[test]
789    fn atan2_first_quadrant_diagonal() {
790        let one = D38s12::ONE;
791        let result = one.atan2(one);
792        assert!(
793            within_lsb(result, D38s12::quarter_pi(), TWO_LSB),
794            "atan2(1, 1) bits {}, quarter_pi bits {}",
795            result.to_bits(),
796            D38s12::quarter_pi().to_bits(),
797        );
798    }
799
800    #[test]
801    fn atan2_third_quadrant_diagonal() {
802        let neg_one = -D38s12::ONE;
803        let result = neg_one.atan2(neg_one);
804        let three = D38s12::from_int(3);
805        let expected = -(D38s12::quarter_pi() * three);
806        assert!(
807            within_lsb(result, expected, TWO_LSB),
808            "atan2(-1, -1) bits {}, expected -3pi/4 bits {}",
809            result.to_bits(),
810            expected.to_bits(),
811        );
812    }
813
814    #[test]
815    fn atan2_second_quadrant_diagonal() {
816        let one = D38s12::ONE;
817        let neg_one = -D38s12::ONE;
818        let result = one.atan2(neg_one);
819        let three = D38s12::from_int(3);
820        let expected = D38s12::quarter_pi() * three;
821        assert!(
822            within_lsb(result, expected, TWO_LSB),
823            "atan2(1, -1) bits {}, expected 3pi/4 bits {}",
824            result.to_bits(),
825            expected.to_bits(),
826        );
827    }
828
829    #[test]
830    fn atan2_fourth_quadrant_diagonal() {
831        let one = D38s12::ONE;
832        let neg_one = -D38s12::ONE;
833        let result = neg_one.atan2(one);
834        let expected = -D38s12::quarter_pi();
835        assert!(
836            within_lsb(result, expected, TWO_LSB),
837            "atan2(-1, 1) bits {}, expected -pi/4 bits {}",
838            result.to_bits(),
839            expected.to_bits(),
840        );
841    }
842
843    #[test]
844    fn atan2_positive_x_axis_is_zero() {
845        let zero = D38s12::ZERO;
846        let one = D38s12::ONE;
847        assert_eq!(zero.atan2(one), D38s12::ZERO);
848    }
849
850    // ── Hyperbolic ────────────────────────────────────────────────────
851
852    #[test]
853    fn sinh_zero_is_zero() {
854        assert_eq!(D38s12::ZERO.sinh(), D38s12::ZERO);
855    }
856
857    #[test]
858    fn cosh_zero_is_one() {
859        assert_eq!(D38s12::ZERO.cosh(), D38s12::ONE);
860    }
861
862    #[test]
863    fn tanh_zero_is_zero() {
864        assert_eq!(D38s12::ZERO.tanh(), D38s12::ZERO);
865    }
866
867    #[test]
868    fn asinh_zero_is_zero() {
869        assert_eq!(D38s12::ZERO.asinh(), D38s12::ZERO);
870    }
871
872    #[test]
873    fn acosh_one_is_zero() {
874        assert_eq!(D38s12::ONE.acosh(), D38s12::ZERO);
875    }
876
877    #[test]
878    fn atanh_zero_is_zero() {
879        assert_eq!(D38s12::ZERO.atanh(), D38s12::ZERO);
880    }
881
882    #[test]
883    fn cosh_squared_minus_sinh_squared_is_one() {
884        if !crate::support::rounding::DEFAULT_IS_HALF_TO_EVEN { return; }
885        for raw in [
886            500_000_000_000_i128,
887            -500_000_000_000_i128,
888            1_234_567_890_123_i128,
889            -1_234_567_890_123_i128,
890            2_500_000_000_000_i128,
891        ] {
892            let x = D38s12::from_bits(raw);
893            let ch = x.cosh();
894            let sh = x.sinh();
895            let diff = (ch * ch) - (sh * sh);
896            assert!(
897                within_lsb(diff, D38s12::ONE, FOUR_LSB),
898                "cosh^2 - sinh^2 != 1 for raw={raw}: got bits {} (delta {})",
899                diff.to_bits(),
900                (diff.to_bits() - D38s12::ONE.to_bits()).abs(),
901            );
902        }
903    }
904
905    // ── Angle conversions ─────────────────────────────────────────────
906
907    #[test]
908    fn to_degrees_pi_is_180() {
909        if !crate::support::rounding::DEFAULT_IS_HALF_TO_EVEN { return; }
910        let pi = D38s12::pi();
911        let result = pi.to_degrees();
912        let expected = D38s12::from_int(180);
913        assert!(
914            within_lsb(result, expected, ANGLE_TOLERANCE_LSB),
915            "to_degrees(pi) bits {}, expected 180 bits {} (delta {})",
916            result.to_bits(),
917            expected.to_bits(),
918            (result.to_bits() - expected.to_bits()).abs(),
919        );
920    }
921
922    #[test]
923    fn to_radians_180_is_pi() {
924        let one_eighty = D38s12::from_int(180);
925        let result = one_eighty.to_radians();
926        let expected = D38s12::pi();
927        assert!(
928            within_lsb(result, expected, ANGLE_TOLERANCE_LSB),
929            "to_radians(180) bits {}, expected pi bits {} (delta {})",
930            result.to_bits(),
931            expected.to_bits(),
932            (result.to_bits() - expected.to_bits()).abs(),
933        );
934    }
935
936    #[test]
937    fn to_degrees_zero_is_zero() {
938        assert_eq!(D38s12::ZERO.to_degrees(), D38s12::ZERO);
939    }
940
941    #[test]
942    fn to_radians_zero_is_zero() {
943        assert_eq!(D38s12::ZERO.to_radians(), D38s12::ZERO);
944    }
945
946    #[test]
947    fn to_radians_to_degrees_round_trip() {
948        for raw in [
949            500_000_000_000_i128,
950            -500_000_000_000_i128,
951            1_234_567_890_123_i128,
952            -2_345_678_901_234_i128,
953        ] {
954            let x = D38s12::from_bits(raw);
955            let recovered = x.to_degrees().to_radians();
956            assert!(
957                within_lsb(recovered, x, FOUR_LSB),
958                "to_radians(to_degrees(x)) != x for raw={raw}: got bits {} (delta {})",
959                recovered.to_bits(),
960                (recovered.to_bits() - x.to_bits()).abs(),
961            );
962        }
963    }
964
965    #[test]
966    fn to_degrees_half_pi_is_90() {
967        if !crate::support::rounding::DEFAULT_IS_HALF_TO_EVEN { return; }
968        let result = D38s12::half_pi().to_degrees();
969        let expected = D38s12::from_int(90);
970        assert!(
971            within_lsb(result, expected, ANGLE_TOLERANCE_LSB),
972            "to_degrees(half_pi) bits {}, expected 90 bits {} (delta {})",
973            result.to_bits(),
974            expected.to_bits(),
975            (result.to_bits() - expected.to_bits()).abs(),
976        );
977    }
978
979    #[test]
980    fn to_degrees_quarter_pi_is_45() {
981        let result = D38s12::quarter_pi().to_degrees();
982        let expected = D38s12::from_int(45);
983        assert!(
984            within_lsb(result, expected, ANGLE_TOLERANCE_LSB),
985            "to_degrees(quarter_pi) bits {}, expected 45 bits {} (delta {})",
986            result.to_bits(),
987            expected.to_bits(),
988            (result.to_bits() - expected.to_bits()).abs(),
989        );
990    }
991
992    // ── Cross-method consistency ──────────────────────────────────────
993
994    #[test]
995    fn tan_matches_sin_over_cos() {
996        for raw in [
997            500_000_000_000_i128,
998            -500_000_000_000_i128,
999            1_000_000_000_000_i128,
1000            -1_000_000_000_000_i128,
1001            123_456_789_012_i128,
1002        ] {
1003            let x = D38s12::from_bits(raw);
1004            let t = x.tan();
1005            let sc = x.sin() / x.cos();
1006            assert!(
1007                within_lsb(t, sc, FOUR_LSB),
1008                "tan(x) != sin/cos for raw={raw}: tan bits {}, sin/cos bits {}",
1009                t.to_bits(),
1010                sc.to_bits(),
1011            );
1012        }
1013    }
1014
1015    #[test]
1016    fn tanh_matches_sinh_over_cosh() {
1017        for raw in [
1018            500_000_000_000_i128,
1019            -500_000_000_000_i128,
1020            1_234_567_890_123_i128,
1021            -2_345_678_901_234_i128,
1022        ] {
1023            let x = D38s12::from_bits(raw);
1024            let t = x.tanh();
1025            let sc = x.sinh() / x.cosh();
1026            assert!(
1027                within_lsb(t, sc, FOUR_LSB),
1028                "tanh(x) != sinh/cosh for raw={raw}: tanh bits {}, sinh/cosh bits {}",
1029                t.to_bits(),
1030                sc.to_bits(),
1031            );
1032        }
1033    }
1034}