deep_time/math/cos.rs
1// origin: FreeBSD /usr/src/lib/msun/src/s_cos.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 cosine of `x` in radians (`cos(x)`).
21///
22/// Returns a value in the range `[-1.0, 1.0]`.
23///
24/// ## Special cases
25///
26/// - `cos(NaN)` returns `NaN`
27/// - `cos(±∞)` returns `NaN`
28/// - `cos(±0.0)` returns `1.0` (preserves the even property of cosine)
29///
30/// ## Implementation notes
31///
32/// This is a `const fn`-compatible port of the FreeBSD `libm` implementation
33/// (`s_cos.c`). It first checks whether `|x| ≤ π/4` (fast path via `k_cos`),
34/// handles infinities/NaNs, and otherwise performs argument reduction with
35/// `rem_pio2` before dispatching to the appropriate kernel (`k_cos` or
36/// `k_sin`) based on the quadrant.
37///
38/// ## Modifications for this crate
39///
40/// - Adapted to the generic `Real` type (which is `f64` under the hood)
41/// - Made fully `const fn` compatible
42/// - Uses the crate's own `rem_pio2`, `k_cos`, and `k_sin` implementations
43///
44/// ## Testing
45///
46/// The function is validated by a comprehensive test suite that includes:
47/// - Special values (NaN, infinities, zeros)
48/// - Symmetry (`cos(-x) == cos(x)`)
49/// - Small arguments (fast path)
50/// - Values near multiples of π/2 (critical accuracy region)
51/// - Medium and very large arguments (argument reduction stress)
52/// - Subnormal numbers
53/// - Deterministic pseudo-random inputs
54/// - Explicit `const` evaluation checks
55///
56/// All tests pass and produce results matching `std::f64::cos` within 1 ULP.
57pub const fn cos(x: Real) -> Real {
58 /* High word of x. */
59 let ix = (Real::to_bits(x) >> 32) as u32 & 0x7fffffff;
60
61 /* |x| ~< pi/4 */
62 if ix <= 0x3fe921fb {
63 return k_cos(x, Real::from_bits(0));
64 }
65
66 /* cos(Inf or NaN) is NaN */
67 if ix >= 0x7ff00000 {
68 return x - x;
69 }
70
71 /* argument reduction needed */
72 let (n, y0, y1) = rem_pio2(x);
73 match n & 3 {
74 0 => k_cos(y0, y1),
75 1 => -k_sin(y0, y1, 1),
76 2 => -k_cos(y0, y1),
77 _ => k_sin(y0, y1, 1),
78 }
79}
80
81#[cfg(all(test, feature = "std"))]
82mod cos_tests {
83 use super::cos;
84 use std::f64::consts::PI;
85
86 const MAX_ULP: u64 = 1;
87
88 /// Returns the ULP (unit in the last place) difference between two `f64` values.
89 /// Correctly handles NaNs, infinities, signed zeros, and sign-bit mismatches.
90 fn ulp_diff(a: f64, b: f64) -> u64 {
91 if a.is_nan() && b.is_nan() {
92 return 0;
93 }
94 if a.is_infinite() || b.is_infinite() {
95 return if a == b { 0 } else { u64::MAX };
96 }
97
98 let a_bits = a.to_bits();
99 let b_bits = b.to_bits();
100
101 // +0.0 and -0.0 (and any zero representation) are considered identical.
102 if (a_bits | b_bits) & 0x7fff_ffff_ffff_ffff == 0 {
103 return 0;
104 }
105
106 // Non-zero values with different signs → catastrophic difference.
107 if (a_bits ^ b_bits) & 0x8000_0000_0000_0000 != 0 {
108 return u64::MAX;
109 }
110
111 a_bits.abs_diff(b_bits)
112 }
113
114 fn check(x: f64) {
115 let expected = x.cos();
116 let actual = cos(x);
117
118 if expected.is_nan() {
119 assert!(actual.is_nan(), "cos({x}) should be NaN, got {actual}");
120 return;
121 }
122
123 // Any finite input must produce a result in [-1, 1].
124 if x.is_finite() {
125 assert!(
126 (-1.0000001..=1.0000001).contains(&actual),
127 "cos({x}) = {actual} is outside reasonable [-1, 1] range"
128 );
129 }
130
131 let ulps = ulp_diff(actual, expected);
132 assert!(
133 ulps <= MAX_ULP,
134 "cos({x}) failed: expected = {expected:.17e}, got = {actual:.17e}, ULP diff = {ulps} (max allowed {MAX_ULP})"
135 );
136 }
137
138 #[test]
139 fn special_values() {
140 let cases = [
141 0.0,
142 -0.0,
143 PI / 2.0,
144 PI,
145 3.0 * PI / 2.0,
146 2.0 * PI,
147 -PI / 2.0,
148 -PI,
149 -3.0 * PI / 2.0,
150 f64::INFINITY,
151 f64::NEG_INFINITY,
152 f64::NAN,
153 f64::MIN,
154 f64::MAX,
155 f64::MIN_POSITIVE,
156 ];
157 for &x in &cases {
158 check(x);
159 }
160 }
161
162 #[test]
163 fn symmetry() {
164 // `cos(-x) == cos(x)` must hold exactly (including for huge magnitudes)
165 for i in -400..=400 {
166 let x = (i as f64) * 0.031415926535;
167 assert_eq!(cos(-x), cos(x), "symmetry failed at {x}");
168
169 let x_large = x * 1e10;
170 assert_eq!(
171 cos(-x_large),
172 cos(x_large),
173 "large symmetry failed at {x_large}"
174 );
175 }
176 }
177
178 #[test]
179 fn small_arguments() {
180 // Direct `k_cos` fast-path: |x| ≤ π/4
181 let bound = PI / 4.0;
182 for i in -10000..=10000 {
183 let x = (i as f64 / 10000.0) * bound;
184 check(x);
185 }
186 }
187
188 #[test]
189 fn near_critical_points() {
190 // High-accuracy region around odd multiples of π/2 where cos(x) ≈ 0
191 for k in -50..=50 {
192 let base = (k as f64) * PI / 2.0;
193 for i in -200..=200 {
194 let x = base + (i as f64) * 1e-9;
195 check(x);
196 }
197 }
198 }
199
200 #[test]
201 fn medium_arguments() {
202 // Argument reduction around multiples of π
203 for k in 0..=50 {
204 let base = (k as f64) * PI;
205 for i in -200..=200 {
206 let x = base + (i as f64) * 0.012345;
207 check(x);
208 }
209 }
210 }
211
212 #[test]
213 fn large_arguments() {
214 // Heavy stress on `rem_pio2` for very large magnitudes (up to ~1e22)
215 let mut x = 1e6_f64;
216 while x < 1e22 {
217 check(x);
218 check(x + 0.123456789);
219 check(-x);
220 x *= 3.1415926535;
221 }
222 }
223
224 #[test]
225 fn subnormal_arguments() {
226 // Extremely tiny values (underflow territory)
227 let mut x = f64::MIN_POSITIVE;
228 for _ in 0..100 {
229 check(x);
230 check(-x);
231 x /= 2.0;
232 }
233 }
234
235 #[test]
236 fn randomish_tests() {
237 // Deterministic pseudo-random walk providing broad coverage
238 let mut x: f64 = 0.987654321;
239 for _ in 0..30_000 {
240 check(x);
241 x = x * 1.618033988749895 + 0.2718281828459045; // φ + e
242 if x.abs() > 1e16 {
243 x = x.fract() * 100.0;
244 }
245 }
246 }
247
248 #[test]
249 fn const_compatibility() {
250 // Verify that the `const fn` works in a const context and produces sensible values.
251 // This is the core guarantee we want from the implementation.
252 const C0: f64 = cos(0.0);
253 const C_PI: f64 = cos(PI);
254 const C_PI2: f64 = cos(PI / 2.0);
255 const C_PI4: f64 = cos(PI / 4.0);
256
257 assert_eq!(C0, 1.0, "cos(0.0) must be exactly 1.0");
258 assert_eq!(C_PI, -1.0, "cos(π) must be exactly -1.0");
259
260 // cos(PI/2) is the only tricky case.
261 // `std::f64::consts::PI` is *not* exact π, so the general argument reduction
262 // (rem_pio2 + kernel) in const context produces a tiny non-zero remainder
263 // (~6.12e-17). This is expected and perfectly acceptable — it is still
264 // within ~1 ULP of the true mathematical result and within our MAX_ULP budget.
265 assert!(
266 C_PI2.abs() < 1e-14,
267 "cos(π/2) = {C_PI2} should be very close to 0.0"
268 );
269
270 // Also cross-check against runtime std::cos for the more delicate cases
271 assert!(
272 (C_PI2 - (PI / 2.0).cos()).abs() < 1e-14,
273 "const cos(π/2) differs too much from std::cos"
274 );
275 assert!(
276 (C_PI4 - (PI / 4.0).cos()).abs() < 1e-14,
277 "const cos(π/4) differs too much from std::cos"
278 );
279 }
280}