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 5,000,000 random
30///   samples 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/// - Statistical random testing across multiple magnitude scales.
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    // =====================================================================
75    // Test Helper: Simple PRNG (only used in tests)
76    // =====================================================================
77
78    #[allow(dead_code)]
79    struct Rng(u64);
80
81    #[allow(dead_code)]
82    impl Rng {
83        fn new(seed: u64) -> Self {
84            Self(seed)
85        }
86
87        #[inline]
88        fn next_u64(&mut self) -> u64 {
89            self.0 = self.0.wrapping_add(0x9E3779B97F4A7C15);
90            let mut z = self.0;
91            z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
92            z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
93            z ^ (z >> 31)
94        }
95
96        /// Returns a value roughly in `[-scale, scale]`
97        fn next_f64(&mut self, scale: f64) -> f64 {
98            let bits = self.next_u64();
99            let mantissa = bits & 0x000f_ffff_ffff_ffff;
100            let f = f64::from_bits(0x3ff0_0000_0000_0000 | mantissa);
101            (f - 1.0) * 2.0 * scale - scale
102        }
103    }
104
105    // =====================================================================
106    // Utility: ULP difference
107    // =====================================================================
108
109    #[allow(dead_code)]
110    fn ulp_diff(a: f64, b: f64) -> u64 {
111        if a.is_nan() && b.is_nan() {
112            return 0;
113        }
114        if a.is_infinite() && b.is_infinite() && a.signum() == b.signum() {
115            return 0;
116        }
117        a.to_bits().abs_diff(b.to_bits())
118    }
119
120    // =====================================================================
121    // Tests
122    // =====================================================================
123
124    #[test]
125    fn sin_edge_cases() {
126        let pi = std::f64::consts::PI;
127        let pi_over_2 = std::f64::consts::FRAC_PI_2;
128
129        assert_eq!(sin(0.0), 0.0);
130        assert_eq!(sin(-0.0), -0.0);
131        assert!((sin(1.0) - 1.0f64.sin()).abs() < 1e-15);
132
133        // Multiples of π/2
134        assert!((sin(pi_over_2) - 1.0).abs() < 1e-14);
135        assert!((sin(-pi_over_2) + 1.0).abs() < 1e-14);
136        assert!((sin(pi) - 0.0).abs() < 1e-14);
137        assert!((sin(3.0 * pi_over_2) + 1.0).abs() < 1e-14);
138
139        // Very small
140        assert_eq!(sin(1e-300), 1e-300);
141
142        // Large values
143        let large = 1e10;
144        let diff = (sin(large) - large.sin()).abs();
145        assert!(diff < 1e-6 || sin(large).is_nan());
146
147        let neg_large = -1e8;
148        assert!((sin(neg_large) - neg_large.sin()).abs() < 1e-5);
149
150        // Extremely large
151        let huge = 1e300;
152        let s = sin(huge);
153        assert!(s.is_nan() || s.abs() <= 1.0 + 1e-9);
154    }
155
156    #[test]
157    fn sin_very_large_arguments() {
158        // This test specifically exercises rem_pio2_large and its recompute logic.
159        // These values are large enough that they go through the big-argument path.
160        let large_values: &[f64] = &[
161            1e40,
162            1e80,
163            1e120,
164            1e160,
165            1e200,
166            -1e50,
167            -1e100,
168            -1e150,
169            1e10 + std::f64::consts::PI * 1e8, // large + offset
170            -1e12 - std::f64::consts::PI * 1e7,
171        ];
172
173        for &x in large_values {
174            let our = sin(x);
175            let std_val = x.sin();
176
177            if our.is_nan() && std_val.is_nan() {
178                continue;
179            }
180
181            // We allow a slightly looser tolerance here because these are extreme values
182            let diff = (our - std_val).abs();
183            assert!(
184                diff < 1e-5 || our.is_nan(),
185                "sin mismatch on very large argument at x = {}: diff = {}",
186                x,
187                diff
188            );
189        }
190    }
191
192    #[test]
193    fn sin_identities() {
194        let x = 1.23456789;
195
196        assert!((sin(-x) + sin(x)).abs() < 1e-15);
197        assert!((sin(x + 2.0 * std::f64::consts::PI) - sin(x)).abs() < 1e-9);
198    }
199
200    #[test]
201    fn sin_monotonicity() {
202        let tol = 1e-12;
203
204        // Region 1: Clearly increasing
205        let mut prev = sin(-1.0);
206        for i in 1..100_000 {
207            let x = -1.0 + (i as f64) * 2e-5;
208            let y = sin(x);
209            assert!(y + tol >= prev, "Non-monotonic (increasing) at x = {}", x);
210            prev = y;
211        }
212
213        // Region 2: Clearly decreasing
214        prev = sin(std::f64::consts::FRAC_PI_2 + 0.1);
215        for i in 1..100_000 {
216            let x = std::f64::consts::FRAC_PI_2 + 0.1 + (i as f64) * 2e-5;
217            let y = sin(x);
218            assert!(y + tol <= prev, "Non-monotonic (decreasing) at x = {}", x);
219            prev = y;
220        }
221    }
222
223    #[test]
224    fn sin_very_small_values() {
225        // Test that sin(x) ≈ x for very small values
226        for i in 0..30 {
227            let x = 1e-20 * (i as f64 + 1.0);
228            assert_eq!(sin(x), x, "Failed at x = {}", x);
229        }
230
231        // Additional small values
232        assert_eq!(sin(1e-300), 1e-300);
233        assert_eq!(sin(-1e-250), -1e-250);
234    }
235
236    #[test]
237    fn sin_hard_reduction_cases() {
238        let cases: &[f64] = &[
239            1.5707963267948966,
240            4.71238898038469,
241            1e10 + 0.5,
242            std::f64::consts::PI * 1e8,
243            -std::f64::consts::PI * 1e7 + 1e-9,
244            1e20,
245            -1e20,
246        ];
247
248        for &x in cases {
249            let our = sin(x);
250            let std = x.sin();
251            let diff = (our - std).abs();
252            assert!(
253                diff < 1e-8 || our.is_nan(),
254                "Hard reduction case failed at x = {}: diff = {}",
255                x,
256                diff
257            );
258        }
259    }
260
261    #[test]
262    fn sin_near_pi_over_2() {
263        let pi_over_2 = std::f64::consts::FRAC_PI_2;
264
265        for i in 0..100_000 {
266            let delta = (i as f64 - 50_000.0) * 1e-11;
267            let x = pi_over_2 + delta;
268            let our = sin(x);
269            let expected = x.sin();
270            let diff = (our - expected).abs();
271            assert!(
272                diff < 1e-10,
273                "Large error near π/2 at x = {}: diff = {}",
274                x,
275                diff
276            );
277        }
278    }
279
280    #[test]
281    fn sin_near_multiples_of_pi() {
282        let pi = std::f64::consts::PI;
283
284        for k in -10i32..=10 {
285            let base = (k as f64) * pi;
286
287            // Test slightly above and below k*π
288            for &delta in &[1e-9, 1e-8, -1e-9, -1e-8] {
289                let x = base + delta;
290                let our = sin(x);
291                let std = x.sin();
292                let diff = (our - std).abs();
293                assert!(
294                    diff < 1e-9 || our.is_nan(),
295                    "Error near {}π at x = {}: diff = {}",
296                    k,
297                    x,
298                    diff
299                );
300            }
301        }
302    }
303
304    #[test]
305    fn sin_max_ulp_error() {
306        let mut max_ulp: u64 = 0;
307        let mut worst_x = 0.0f64;
308        let mut rng = Rng::new(0xdead_beef_cafe_babe);
309
310        for _ in 0..5_000_000 {
311            let scale = if rng.next_u64() & 1 == 0 { 1e6 } else { 1e12 };
312            let x = rng.next_f64(scale);
313            let our = sin(x);
314            let std = x.sin();
315
316            if our.is_nan() || std.is_nan() {
317                continue;
318            }
319
320            let ulp = ulp_diff(our, std);
321            if ulp > max_ulp {
322                max_ulp = ulp;
323                worst_x = x;
324            }
325        }
326
327        // println!("Maximum observed ULP error: {} at x ≈ {}", max_ulp, worst_x);
328        assert!(
329            max_ulp <= 1,
330            "sin() maximum error too high: {} ULPs at x = {}",
331            max_ulp,
332            worst_x
333        );
334    }
335
336    #[test]
337    fn sin_random_many() {
338        let mut rng = Rng::new(0x1234_5678_9abc_def0);
339        let scales = [1.0, 10.0, 100.0, 1_000.0, 1e6, 1e8, 1e10];
340
341        for &scale in &scales {
342            for _ in 0..150_000 {
343                let x = rng.next_f64(scale);
344                let our = sin(x);
345                let std_val = x.sin();
346
347                if our.is_nan() && std_val.is_nan() {
348                    continue;
349                }
350
351                // Adaptive tolerance
352                let diff = if x.abs() < 1e-8 {
353                    (our - std_val).abs()
354                } else {
355                    (our - std_val).abs() / x.abs().max(1.0)
356                };
357
358                let tol = if x.abs() < 1e-8 { 1e-14 } else { 5e-12 };
359
360                assert!(
361                    diff < tol,
362                    "sin mismatch at x = {}: our={}, std={}, diff={}",
363                    x,
364                    our,
365                    std_val,
366                    diff
367                );
368            }
369        }
370    }
371
372    /// Compile-time check that `sin` can be used in const contexts.
373    const _: () = {
374        let _ = sin(0.0);
375        let _ = sin(1.0);
376        let _ = sin(-std::f64::consts::PI);
377    };
378}