libm_ext/
trig.rs

1//! Trigonometric functions
2//!
3//! The implementation is based on the [Julia Standard Library](https://github.com/JuliaLang/julia/blob/master/base/special/trig.jl).
4//!
5//! - [`sinpi`] / [`sinpif`]
6//! - [`cospi`] / [`cospif`]
7//! - [`sincospi`] / [`sincospif`]
8//!
9#![allow(clippy::approx_constant)]
10
11use crate::utils::evalpoly;
12use hexf::hexf64;
13
14const MAXINTFLOAT64: u64 = 1 << f64::MANTISSA_DIGITS;
15const MAXINTFLOAT32: u64 = 1 << f32::MANTISSA_DIGITS;
16
17/// Uses minimax polynomial of $\sin(\pi x)/x$ for $x \in [0, 0.25]$ to approximate $\sin(\pi x)$.
18#[inline]
19fn sinpi_kernel(x: f64) -> f64 {
20    // coefficients for minimax polynormal of sin(pi*x)/x
21    // c0 = c0_hi + c0_lo
22    let c0_hi = hexf64!("0x1.921fb54442d18p+1"); // 3.141592653589793
23    let c0_lo = hexf64!("0x1.1a5f14401a52p-53"); // 1.2245907532226012e-16
24    let c2 = hexf64!("-0x1.4abbce625be01p2"); // -5.167712780049897
25    let c4 = hexf64!("0x1.466bc67753d55p1"); // 2.550164039864891
26    let c6 = hexf64!("-0x1.32d2ccde1222cp-1"); // -0.5992645283777782
27    let c8 = hexf64!("0x1.50782aae65ef4p-4"); // 8.214584991798884e-2
28    let c10 = hexf64!("-0x1.e2fa787f268fep-8"); // -7.369665544638913e-3
29    let c12 = hexf64!("0x1.e06afd55415bp-12"); // 4.5816223912739217e-4
30    let c14 = hexf64!("0x1.add9d45ae8195p-17"); // 1.2810554997078747e-5
31
32    let x_square = x * x;
33    let x_bisquare = x_square * x_square;
34
35    // c4 + c6*x^2 + ... + c14*x^10
36    let mut r = evalpoly(&[c4, c6, c8, c10, c12, c14], x_square);
37    // c0_lo + c4*x^4 + c6*x^6 + ... + c14*x^14
38    r = x_bisquare.mul_add(r, c0_lo);
39    // c0_lo + c2*x^2 + c4*x^4 + ... + c14*x^14
40    r = c2.mul_add(x_square, r);
41    // c0_hi*x + co_lo*x + c2*x^3 + c4*x^5 + ... + c14*x^15
42    c0_hi.mul_add(x, x * r)
43}
44
45#[inline]
46fn sinpif_kernel(x: f32) -> f32 {
47    let c0 = hexf64!("0x1.921fb6p1"); // 3.1415927410125732
48    let c2 = hexf64!("-0x1.4abc1cp2"); // -5.167731285095215
49    let c4 = hexf64!("0x1.468e6cp1"); // 2.5512213706970215
50    let c6 = hexf64!("-0x1.3e497cp-1"); // -0.6216543912887573
51    let c8 = hexf64!("0x1.eb5482p-3"); // 0.23990727961063385
52
53    let x_f64 = x as f64;
54
55    let res_f64 = x_f64 * evalpoly(&[c0, c2, c4, c6, c8], x_f64 * x_f64);
56    res_f64 as f32
57}
58
59/// Uses minimax polynomial of $\cos(\pi x)$ for $x \in [0, 0.25]$.
60///
61/// # Double-Double Compensation Technique
62///
63/// To achieve nearly 1 ULP accuracy, we use double-double arithmetic for the
64/// leading terms where cancellation error is most significant.
65///
66/// The polynomial approximation is:
67/// $$\cos(\pi x) \approx c_0 + c_2 x^2 + c_4 x^4 + \cdots + c_{14} x^{14}$$
68///
69/// where $c_0 = 1$ and $c_2 \approx -\pi^2/2 \approx -4.9348$.
70///
71/// ## Why Double-Double?
72///
73/// When computing $1 + c_2 x^2$ for small $x$, the result is close to 1, causing
74/// significant cancellation. A single f64 cannot represent $c_2$ exactly, so we
75/// split it: $c_2 = c_{2,hi} - c_{2,lo}$ where $c_{2,hi}$ is the nearest f64 and
76/// $c_{2,lo}$ is the small correction term.
77///
78/// ## Algorithm
79///
80/// 1. Compute `a_x_square_hi = c2_hi * x²` (primary term)
81/// 2. Compute `a_x_square_lo = c2_lo * x² + rounding_error(c2_hi * x²)`
82///    using FMA to capture the rounding error: `(-c2_hi) * x² + a_x_square_hi`
83/// 3. Compute `w = c0 + a_x_square_hi` (may lose precision)
84/// 4. Recover lost precision: `(c0 - w) + a_x_square_hi` gives the rounding error
85/// 5. Final result compensates: `w + (higher_terms + rounding_error - c2_lo*x²)`
86///
87/// This technique is based on Dekker's TwoSum and TwoProduct algorithms.
88#[inline]
89fn cospi_kernel(x: f64) -> f64 {
90    // coefficients for minimax polynomial of cos(pi*x)
91    let c0 = 1.0;
92    // c2 = c2_hi - c2_lo
93    let c2_hi = hexf64!("-0x1.3bd3cc9be45dep2"); // -4.934802200544679
94    let c2_lo = hexf64!("0x1.219c35926754dp-52"); // 2.5119679985578543e-16
95    let c4 = hexf64!("0x1.03c1f081b5a67p2"); // 4.058712126416686
96    let c6 = hexf64!("-0x1.55d3c7e3c325bp0"); // -1.3352627688465393
97    let c8 = hexf64!("0x1.e1f5067b90b37p-3"); // 0.23533062996474438
98    let c10 = hexf64!("-0x1.a6d1e7294bffap-6"); // -2.5806880706284098e-2
99    let c12 = hexf64!("0x1.f9c89ca1d5187p-10"); // 1.9294114685709256e-3
100    let c14 = hexf64!("-0x1.b167302e37198p-14"); // -1.0333134625590266e-4
101
102    let x_square = x * x;
103
104    // Higher-order terms: c4*x^4 + c6*x^6 + ... + c14*x^14
105    let r = x_square * evalpoly(&[c4, c6, c8, c10, c12, c14], x_square);
106
107    // Double-double multiplication: c2 * x² = (c2_hi - c2_lo) * x²
108    // Step 1: Primary product
109    let a_x_square_hi = c2_hi * x_square;
110    // Step 2: Capture rounding error of c2_hi * x² using FMA, plus c2_lo * x²
111    //         = c2_lo * x² + (c2_hi * x² - a_x_square_hi)  [rounding error]
112    let a_x_square_lo = c2_lo.mul_add(x_square, (-c2_hi).mul_add(x_square, a_x_square_hi));
113
114    // Double-double addition: c0 + c2_hi * x²
115    let w = c0 + a_x_square_hi;
116
117    // Compensated summation:
118    // - (c0 - w) + a_x_square_hi: recovers rounding error from c0 + a_x_square_hi
119    // - Subtract a_x_square_lo: because c2 = c2_hi - c2_lo, we need to subtract c2_lo * x²
120    w + x_square.mul_add(r, ((c0 - w) + a_x_square_hi) - a_x_square_lo)
121}
122
123#[inline]
124fn cospif_kernel(x: f32) -> f32 {
125    let c0 = 1.0;
126    let c2 = hexf64!("-0x1.3bd3ccp2"); // -4.934802055358887
127    let c4 = hexf64!("0x1.03c1a6p2"); // 4.058694362640381
128    let c6 = hexf64!("-0x1.55a3b4p0"); // -1.334529161453247
129    let c8 = hexf64!("0x1.c85d38p-3"); // 0.222834050655365
130    let c10 = hexf64!("0x1.97cb1p-5"); // 0.04977944493293762
131
132    let x_f64 = x as f64;
133
134    let res_f64 = evalpoly(&[c0, c2, c4, c6, c8, c10], x_f64 * x_f64);
135    res_f64 as f32
136}
137
138/// Compute $\sin(\pi x)$ more accurately than `sin(pi*x)`, especially for large `x` (f64).
139///
140/// # Notes
141///
142/// If `x` is infinite or NAN, return NAN.
143pub fn sinpi(x: f64) -> f64 {
144    let x_abs = x.abs();
145    if x_abs.is_infinite() || x_abs.is_nan() {
146        return f64::NAN;
147    }
148    // If x is too large, return 0.0
149    if x_abs >= MAXINTFLOAT64 as f64 {
150        return 0.0f64.copysign(x);
151    }
152
153    // reduce x to interval [0, 0.5]
154    let n = (2.0 * x_abs).round();
155    let rx = (-0.5f64).mul_add(n, x_abs);
156    let n = n as i64 & 3;
157    let res = match n {
158        0 => sinpi_kernel(rx),
159        1 => cospi_kernel(rx),
160        2 => 0.0 - sinpi_kernel(rx),
161        _ => 0.0 - cospi_kernel(rx),
162    };
163    if x.is_sign_negative() { -res } else { res }
164}
165
166/// Compute $\sin(\pi x)$ more accurately than `sin(pi*x)`, especially for large `x` (f32).
167///
168/// # Notes
169///
170/// If `x` is infinite or NAN, return NAN.
171pub fn sinpif(x: f32) -> f32 {
172    let x_abs = x.abs();
173    if x_abs.is_infinite() || x_abs.is_nan() {
174        return f32::NAN;
175    }
176    // If x is too large, return 0.0
177    if x_abs >= MAXINTFLOAT32 as f32 {
178        return 0.0f32.copysign(x);
179    }
180
181    // reduce x to interval [0, 0.5]
182    let n = (2.0 * x_abs).round();
183    let rx = (-0.5f32).mul_add(n, x_abs);
184    let n = n as i64 & 3;
185    let res = match n {
186        0 => sinpif_kernel(rx),
187        1 => cospif_kernel(rx),
188        2 => 0.0 - sinpif_kernel(rx),
189        _ => 0.0 - cospif_kernel(rx),
190    };
191    if x.is_sign_negative() { -res } else { res }
192}
193
194/// Compute $\cos(\pi x)$ more accurately than `cos(pi*x)`, especially for large `x` (f64).
195///
196/// # Notes
197///
198/// If `x` is infinite or NAN, return NAN.
199pub fn cospi(x: f64) -> f64 {
200    let x_abs = x.abs();
201    if x_abs.is_infinite() || x_abs.is_nan() {
202        return f64::NAN;
203    }
204    // If x is too large, return 1.0
205    if x_abs >= MAXINTFLOAT64 as f64 {
206        return 1.0;
207    }
208
209    // reduce x to interval [0, 0.5]
210    let n = (2.0 * x_abs).round();
211    let rx = (-0.5f64).mul_add(n, x_abs);
212    let n = n as i64 & 3;
213    match n {
214        0 => cospi_kernel(rx),
215        1 => 0.0 - sinpi_kernel(rx),
216        2 => 0.0 - cospi_kernel(rx),
217        _ => sinpi_kernel(rx),
218    }
219}
220
221/// Compute $\cos(\pi x)$ more accurately than `cos(pi*x)`, especially for large `x` (f32).
222///
223/// # Notes
224///
225/// If `x` is infinite or NAN, return NAN.
226pub fn cospif(x: f32) -> f32 {
227    let x_abs = x.abs();
228    if x_abs.is_infinite() || x_abs.is_nan() {
229        return f32::NAN;
230    }
231    // If x is too large, return 1.0
232    if x_abs >= MAXINTFLOAT32 as f32 {
233        return 1.0;
234    }
235
236    // reduce x to interval [0, 0.5]
237    let n = (2.0 * x_abs).round();
238    let rx = (-0.5f32).mul_add(n, x_abs);
239    let n = n as i64 & 3;
240    match n {
241        0 => cospif_kernel(rx),
242        1 => 0.0 - sinpif_kernel(rx),
243        2 => 0.0 - cospif_kernel(rx),
244        _ => sinpif_kernel(rx),
245    }
246}
247
248/// Simultaneously compute [`sinpi`] and [`cospi`] (f64).
249///
250/// # Notes
251///
252/// If `x` is infinite or NAN, return (NAN, NAN).
253pub fn sincospi(x: f64) -> (f64, f64) {
254    let x_abs = x.abs();
255    if x_abs.is_infinite() || x_abs.is_nan() {
256        return (f64::NAN, f64::NAN);
257    }
258    // If x is too large, return 0.0
259    if x_abs >= MAXINTFLOAT64 as f64 {
260        return (0.0f64.copysign(x), 1.0);
261    }
262
263    // reduce x to interval [0, 0.5]
264    let n = (2.0 * x_abs).round();
265    let rx = (-0.5f64).mul_add(n, x_abs);
266    let n = n as i64 & 3;
267    let mut si = sinpi_kernel(rx);
268    let mut co = cospi_kernel(rx);
269    (si, co) = match n {
270        0 => (si, co),
271        1 => (co, 0.0 - si),
272        2 => (0.0 - si, 0.0 - co),
273        _ => (0.0 - co, si),
274    };
275    si = if x.is_sign_negative() { -si } else { si };
276    (si, co)
277}
278
279/// Simultaneously compute [`sinpif`] and [`cospif`] (f32).
280///
281/// # Notes
282///
283/// If `x` is infinite or NAN, return (NAN, NAN).
284pub fn sincospif(x: f32) -> (f32, f32) {
285    let x_abs = x.abs();
286    if x_abs.is_infinite() || x_abs.is_nan() {
287        return (f32::NAN, f32::NAN);
288    }
289    // If x is too large, return 0.0
290    if x_abs >= MAXINTFLOAT32 as f32 {
291        return (0.0f32.copysign(x), 1.0f32);
292    }
293
294    // reduce x to interval [0, 0.5]
295    let n = (2.0 * x_abs).round();
296    let rx = (-0.5f32).mul_add(n, x_abs);
297    let n = n as i64 & 3;
298    let mut si = sinpif_kernel(rx);
299    let mut co = cospif_kernel(rx);
300    (si, co) = match n {
301        0 => (si, co),
302        1 => (co, 0.0 - si),
303        2 => (0.0 - si, 0.0 - co),
304        _ => (0.0 - co, si),
305    };
306    si = if x.is_sign_negative() { -si } else { si };
307    (si, co)
308}
309
310#[cfg(test)]
311mod tests {
312    use super::*;
313    use crate::utils::assert_approx_eq;
314
315    const EPSILON_F64: f64 = 1e-15;
316    const EPSILON_F32: f32 = 1e-6;
317
318    #[test]
319    fn test_maxintfloat64() {
320        assert_eq!(9007199254740992, MAXINTFLOAT64);
321    }
322
323    #[test]
324    fn test_maxintfloat32() {
325        assert_eq!(16777216, MAXINTFLOAT32);
326    }
327
328    #[test]
329    fn test_sinpi_special_values() {
330        // sin(pi * n) = 0 for integer n
331        for i in -10..=10 {
332            let x = i as f64;
333            assert_eq!(sinpi(x), 0.0, "sinpi({}) should be 0.0", x);
334        }
335
336        // sin(pi * (n + 0.5)) = (-1)^n
337        assert_approx_eq!(sinpi(0.5), 1.0, EPSILON_F64);
338        assert_approx_eq!(sinpi(1.5), -1.0, EPSILON_F64);
339        assert_approx_eq!(sinpi(-0.5), -1.0, EPSILON_F64);
340    }
341
342    #[test]
343    fn test_cospi_special_values() {
344        // cos(pi * n) = (-1)^n
345        assert_approx_eq!(cospi(0.0), 1.0, EPSILON_F64);
346        assert_approx_eq!(cospi(1.0), -1.0, EPSILON_F64);
347        assert_approx_eq!(cospi(2.0), 1.0, EPSILON_F64);
348        assert_approx_eq!(cospi(-1.0), -1.0, EPSILON_F64);
349
350        // cos(pi * (n + 0.5)) = 0
351        assert_approx_eq!(cospi(0.5), 0.0, EPSILON_F64);
352        assert_approx_eq!(cospi(1.5), 0.0, EPSILON_F64);
353        assert_approx_eq!(cospi(-0.5), 0.0, EPSILON_F64);
354    }
355
356    #[test]
357    fn test_sincospi_consistency() {
358        let values = [-0.1, 0.2, 0.33, 0.5, 10.7, 1000.123];
359        for &x in &values {
360            let (s, c) = sincospi(x);
361            let s_single = sinpi(x);
362            let c_single = cospi(x);
363            assert_approx_eq!(s, s_single, EPSILON_F64);
364            assert_approx_eq!(c, c_single, EPSILON_F64);
365        }
366    }
367
368    #[test]
369    fn test_pythagorean_identity() {
370        // sin^2 + cos^2 = 1
371        let values = [0.123, 0.456, 1.789, -2.345, 100.0];
372        for &x in &values {
373            let s = sinpi(x);
374            let c = cospi(x);
375            assert_approx_eq!(s * s + c * c, 1.0, EPSILON_F64);
376        }
377    }
378
379    #[test]
380    fn test_nan_inf() {
381        assert!(sinpi(f64::NAN).is_nan());
382        assert!(sinpi(f64::INFINITY).is_nan());
383        assert!(sinpi(f64::NEG_INFINITY).is_nan());
384
385        assert!(cospi(f64::NAN).is_nan());
386        assert!(cospi(f64::INFINITY).is_nan());
387    }
388
389    // f32 tests
390    #[test]
391    fn test_sinpif_special_values() {
392        assert_approx_eq!(sinpif(0.0), 0.0, EPSILON_F32);
393        assert_approx_eq!(sinpif(0.5), 1.0, EPSILON_F32);
394        assert_approx_eq!(sinpif(1.0), 0.0, EPSILON_F32);
395    }
396
397    #[test]
398    fn test_cospif_special_values() {
399        assert_approx_eq!(cospif(0.0), 1.0, EPSILON_F32);
400        assert_approx_eq!(cospif(0.5), 0.0, EPSILON_F32);
401        assert_approx_eq!(cospif(1.0), -1.0, EPSILON_F32);
402    }
403}