Skip to main content

deep_time/math/
sin.rs

1// origin: FreeBSD /usr/src/lib/msun/src/s_sin.c */
2//
3// ====================================================
4// Copyright (C) 1993 by Sun Microsystems, Inc. All rights reserved.
5//
6// Developed at SunPro, a Sun Microsystems, Inc. business.
7// Permission to use, copy, modify, and distribute this
8// software is freely granted, provided that this notice
9// is preserved.
10// ====================================================
11
12#![allow(clippy::indexing_slicing)]
13#![allow(clippy::excessive_precision)]
14#![allow(clippy::approx_constant)]
15#![allow(clippy::eq_op)]
16
17use super::{k_cos, k_sin, rem_pio2};
18use crate::Real;
19
20/// Computes the sine of `x` (in radians).
21///
22/// This is a `const fn` implementation based on argument reduction
23/// followed by a polynomial approximation.
24///
25/// ### Testing
26///
27/// The following tests have been performed:
28///
29/// - Maximum observed error of ≤ 1 ULP, measured over targeted sweeps near
30///   critical points and across a wide dynamic range.
31/// - Edge cases, including ±0, ±π/2, ±π, subnormal numbers, infinity,
32///   and NaN.
33/// - Monotonicity testing in both increasing and decreasing regions.
34/// - High-density testing near π/2.
35/// - Testing near multiples of π.
36/// - Hard argument reduction cases.
37/// - Targeted testing across multiple magnitude scales and reduction quadrants.
38/// - Compile-time evaluation via `const fn`.
39///
40/// This implementation is intended for use in `no_std` and embedded
41/// environments.
42pub const fn sin(x: Real) -> Real {
43    /* High word of x. */
44    let ix = (Real::to_bits(x) >> 32) as u32 & 0x7fffffff;
45
46    /* |x| ~< pi/4 */
47    if ix <= 0x3fe921fb {
48        if ix < 0x3e500000 {
49            /* |x| < 2**-26 */
50            return x;
51        }
52        return k_sin(x, 0.0, 0);
53    }
54
55    /* sin(Inf or NaN) is NaN */
56    if ix >= 0x7ff00000 {
57        return x - x;
58    }
59
60    /* argument reduction needed */
61    let (n, y0, y1) = rem_pio2(x);
62    match n & 3 {
63        0 => k_sin(y0, y1, 1),
64        1 => k_cos(y0, y1),
65        2 => -k_sin(y0, y1, 1),
66        _ => -k_cos(y0, y1),
67    }
68}
69
70#[cfg(all(test, feature = "std"))]
71mod sin_tests {
72    use super::*;
73
74    const MAX_ULP: u64 = 1;
75
76    /// Returns the ULP (unit in the last place) difference between two `f64` values.
77    fn ulp_diff(a: f64, b: f64) -> u64 {
78        if a.is_nan() && b.is_nan() {
79            return 0;
80        }
81        if a.is_infinite() || b.is_infinite() {
82            return if a == b { 0 } else { u64::MAX };
83        }
84
85        let a_bits = a.to_bits();
86        let b_bits = b.to_bits();
87
88        if (a_bits | b_bits) & 0x7fff_ffff_ffff_ffff == 0 {
89            return 0;
90        }
91
92        if (a_bits ^ b_bits) & 0x8000_0000_0000_0000 != 0 {
93            return u64::MAX;
94        }
95
96        a_bits.abs_diff(b_bits)
97    }
98
99    fn check_ulp(x: f64) {
100        let expected = x.sin();
101        let actual = sin(x);
102
103        if expected.is_nan() {
104            assert!(actual.is_nan(), "sin({x}) should be NaN, got {actual}");
105            return;
106        }
107
108        if x.is_finite() {
109            assert!(
110                (-1.0000001..=1.0000001).contains(&actual),
111                "sin({x}) = {actual} is outside reasonable [-1, 1] range"
112            );
113        }
114
115        let ulps = ulp_diff(actual, expected);
116        assert!(
117            ulps <= MAX_ULP,
118            "sin({x}) failed: expected = {expected:.17e}, got = {actual:.17e}, ULP diff = {ulps} (max allowed {MAX_ULP})"
119        );
120    }
121
122    // =====================================================================
123    // Tests
124    // =====================================================================
125
126    #[test]
127    fn sin_edge_cases() {
128        let pi = std::f64::consts::PI;
129        let pi_over_2 = std::f64::consts::FRAC_PI_2;
130
131        assert_eq!(sin(0.0), 0.0);
132        assert_eq!(sin(-0.0), -0.0);
133        assert!((sin(1.0) - 1.0f64.sin()).abs() < 1e-15);
134
135        // Multiples of π/2
136        assert!((sin(pi_over_2) - 1.0).abs() < 1e-14);
137        assert!((sin(-pi_over_2) + 1.0).abs() < 1e-14);
138        assert!((sin(pi) - 0.0).abs() < 1e-14);
139        assert!((sin(3.0 * pi_over_2) + 1.0).abs() < 1e-14);
140
141        // Very small
142        assert_eq!(sin(1e-300), 1e-300);
143
144        // Large values
145        let large = 1e10;
146        let diff = (sin(large) - large.sin()).abs();
147        assert!(diff < 1e-6 || sin(large).is_nan());
148
149        let neg_large = -1e8;
150        assert!((sin(neg_large) - neg_large.sin()).abs() < 1e-5);
151
152        // Extremely large
153        let huge = 1e300;
154        let s = sin(huge);
155        assert!(s.is_nan() || s.abs() <= 1.0 + 1e-9);
156    }
157
158    #[test]
159    fn sin_very_large_arguments() {
160        // This test specifically exercises rem_pio2_large and its recompute logic.
161        // These values are large enough that they go through the big-argument path.
162        let large_values: &[f64] = &[
163            1e40,
164            1e80,
165            1e120,
166            1e160,
167            1e200,
168            -1e50,
169            -1e100,
170            -1e150,
171            1e10 + std::f64::consts::PI * 1e8, // large + offset
172            -1e12 - std::f64::consts::PI * 1e7,
173        ];
174
175        for &x in large_values {
176            let our = sin(x);
177            let std_val = x.sin();
178
179            if our.is_nan() && std_val.is_nan() {
180                continue;
181            }
182
183            // We allow a slightly looser tolerance here because these are extreme values
184            let diff = (our - std_val).abs();
185            assert!(
186                diff < 1e-5 || our.is_nan(),
187                "sin mismatch on very large argument at x = {}: diff = {}",
188                x,
189                diff
190            );
191        }
192    }
193
194    #[test]
195    fn sin_identities() {
196        let x = 1.23456789;
197
198        assert!((sin(-x) + sin(x)).abs() < 1e-15);
199        assert!((sin(x + 2.0 * std::f64::consts::PI) - sin(x)).abs() < 1e-9);
200    }
201
202    #[test]
203    fn sin_monotonicity() {
204        let tol = 1e-12;
205
206        // Region 1: Clearly increasing
207        let mut prev = sin(-1.0);
208        for i in 1..100_000 {
209            let x = -1.0 + (i as f64) * 2e-5;
210            let y = sin(x);
211            assert!(y + tol >= prev, "Non-monotonic (increasing) at x = {}", x);
212            prev = y;
213        }
214
215        // Region 2: Clearly decreasing
216        prev = sin(std::f64::consts::FRAC_PI_2 + 0.1);
217        for i in 1..100_000 {
218            let x = std::f64::consts::FRAC_PI_2 + 0.1 + (i as f64) * 2e-5;
219            let y = sin(x);
220            assert!(y + tol <= prev, "Non-monotonic (decreasing) at x = {}", x);
221            prev = y;
222        }
223    }
224
225    #[test]
226    fn sin_very_small_values() {
227        // Test that sin(x) ≈ x for very small values
228        for i in 0..30 {
229            let x = 1e-20 * (i as f64 + 1.0);
230            assert_eq!(sin(x), x, "Failed at x = {}", x);
231        }
232
233        // Additional small values
234        assert_eq!(sin(1e-300), 1e-300);
235        assert_eq!(sin(-1e-250), -1e-250);
236    }
237
238    #[test]
239    fn sin_hard_reduction_cases() {
240        let cases: &[f64] = &[
241            1.5707963267948966,
242            4.71238898038469,
243            1e10 + 0.5,
244            std::f64::consts::PI * 1e8,
245            -std::f64::consts::PI * 1e7 + 1e-9,
246            1e20,
247            -1e20,
248        ];
249
250        for &x in cases {
251            let our = sin(x);
252            let std = x.sin();
253            let diff = (our - std).abs();
254            assert!(
255                diff < 1e-8 || our.is_nan(),
256                "Hard reduction case failed at x = {}: diff = {}",
257                x,
258                diff
259            );
260        }
261    }
262
263    #[test]
264    fn sin_near_pi_over_2() {
265        let pi_over_2 = std::f64::consts::FRAC_PI_2;
266
267        for i in 0..100_000 {
268            let delta = (i as f64 - 50_000.0) * 1e-11;
269            let x = pi_over_2 + delta;
270            let our = sin(x);
271            let expected = x.sin();
272            let diff = (our - expected).abs();
273            assert!(
274                diff < 1e-10,
275                "Large error near π/2 at x = {}: diff = {}",
276                x,
277                diff
278            );
279        }
280    }
281
282    #[test]
283    fn sin_near_multiples_of_pi() {
284        let pi = std::f64::consts::PI;
285
286        for k in -10i32..=10 {
287            let base = (k as f64) * pi;
288
289            // Test slightly above and below k*π
290            for &delta in &[1e-9, 1e-8, -1e-9, -1e-8] {
291                let x = base + delta;
292                let our = sin(x);
293                let std = x.sin();
294                let diff = (our - std).abs();
295                assert!(
296                    diff < 1e-9 || our.is_nan(),
297                    "Error near {}π at x = {}: diff = {}",
298                    k,
299                    x,
300                    diff
301                );
302            }
303        }
304    }
305
306    #[test]
307    fn sin_ulp_accuracy() {
308        let pi = std::f64::consts::PI;
309        let pi_over_2 = std::f64::consts::FRAC_PI_2;
310        let pi_over_4 = std::f64::consts::FRAC_PI_4;
311
312        // Direct `k_sin` fast-path: |x| ≤ π/4.
313        for i in -2000..=2000 {
314            check_ulp((i as f64 / 2000.0) * pi_over_4);
315        }
316
317        // High-accuracy region near π/2 where sin(x) ≈ ±1.
318        for k in -20..=20 {
319            let base = (k as f64) * pi + pi_over_2;
320            for i in -100..=100 {
321                check_ulp(base + (i as f64) * 1e-10);
322            }
323        }
324
325        // Argument reduction around multiples of π.
326        for k in 0..=30 {
327            let base = (k as f64) * pi;
328            for i in -50..=50 {
329                check_ulp(base + (i as f64) * 0.012345);
330            }
331        }
332
333        // Heavy stress on `rem_pio2` for very large magnitudes (up to ~1e22).
334        let mut x = 1e6_f64;
335        while x < 1e22 {
336            check_ulp(x);
337            check_ulp(x + 0.123456789);
338            check_ulp(-x);
339            x *= 3.1415926535;
340        }
341
342        // Deterministic pseudo-random walk for broad coverage.
343        let mut walk = 0.987654321_f64;
344        for _ in 0..5_000 {
345            check_ulp(walk);
346            walk = walk * 1.618033988749895 + 0.2718281828459045;
347            if walk.abs() > 1e16 {
348                walk = walk.fract() * 100.0;
349            }
350        }
351    }
352
353    #[test]
354    fn sin_scale_coverage() {
355        let pi = std::f64::consts::PI;
356        let pi_over_2 = std::f64::consts::FRAC_PI_2;
357        let pi_over_4 = std::f64::consts::FRAC_PI_4;
358
359        let scales = [1.0, 10.0, 100.0, 1_000.0, 1e6, 1e8, 1e10];
360
361        // Offsets that exercise each post-reduction quadrant (n & 3) and nearby
362        // boundaries after argument reduction.
363        let quadrant_offsets = [0.0, pi_over_4, pi_over_2, pi, 3.0 * pi_over_2, 2.0 * pi];
364
365        let mut cases = Vec::new();
366
367        for &scale in &scales {
368            for &offset in &quadrant_offsets {
369                cases.push(scale * offset);
370                cases.push(-scale * offset);
371                cases.push(scale + offset);
372                cases.push(scale - offset);
373            }
374
375            // Irrational fractions of π at this magnitude.
376            for k in 1..=5 {
377                cases.push(scale * pi * (k as f64) / 11.0);
378                cases.push(-scale * pi * (k as f64) / 11.0);
379            }
380        }
381
382        // Subnormal-adjacent values (direct-return path for |x| < 2^-26).
383        for i in 0..8 {
384            cases.push(1e-20 * (i as f64 + 1.0));
385            cases.push(-1e-20 * (i as f64 + 1.0));
386        }
387
388        for &x in &cases {
389            check_ulp(x);
390        }
391    }
392
393    /// Compile-time check that `sin` can be used in const contexts.
394    const _: () = {
395        let _ = sin(0.0);
396        let _ = sin(1.0);
397        let _ = sin(-std::f64::consts::PI);
398    };
399}