Skip to main content

phasm_core/
det_math.rs

1// Copyright (c) 2026 Christoph Gaffga
2// SPDX-License-Identifier: GPL-3.0-only
3// https://github.com/cgaffga/phasmcore
4
5//! Deterministic math functions for cross-platform WASM reproducibility.
6//!
7//! Provides `det_sin`, `det_cos`, `det_sincos`, `det_atan2`, `det_hypot`,
8//! `det_exp`, and `det_powi_f64` using only IEEE 754 operations that map
9//! to WASM intrinsics (add, sub, mul, div, floor, sqrt, abs, copysign,
10//! bit-pattern manipulation). No calls to `Math.sin`, `Math.cos`,
11//! `Math.exp`, etc.
12//!
13//! Algorithms and coefficients are classical polynomial approximations of
14//! IEEE 754 transcendentals (Cody-Waite range reduction + minimax
15//! polynomials evaluated by Horner's scheme), guaranteeing < 1 ULP error
16//! for all functions. `det_powi_f64` is exponentiation-by-squaring on
17//! bit-exact IEEE 754 multiply — exact by construction.
18
19use std::f64::consts::{FRAC_PI_2, FRAC_PI_4, PI};
20
21// ──────────────────────────────────────────────────────────────────────────
22// Extended-precision π/2 for Cody-Waite range reduction.
23// PIO2_HI + PIO2_LO = π/2 to ~70 bits of precision.
24// ──────────────────────────────────────────────────────────────────────────
25
26const PIO2_HI: f64 = f64::from_bits(0x3FF921FB54442D18); // 1.5707963267948966
27const PIO2_LO: f64 = f64::from_bits(0x3C91A62633145C07); // 6.123233995736766e-17
28
29// ──────────────────────────────────────────────────────────────────────────
30// Sin kernel coefficients — classical minimax polynomial on [-π/4, π/4].
31// sin(x) ≈ x + x³·(S1 + x²·(S2 + x²·(S3 + x²·(S4 + x²·(S5 + x²·S6)))))
32// for |x| ≤ π/4.
33// ──────────────────────────────────────────────────────────────────────────
34
35const S1: f64 = f64::from_bits(0xBFC5555555555549); // -1.66666666666666324348e-01
36const S2: f64 = f64::from_bits(0x3F8111111110F8A6); //  8.33333333332248946124e-03
37const S3: f64 = f64::from_bits(0xBF2A01A019C161D5); // -1.98412698298579493134e-04
38const S4: f64 = f64::from_bits(0x3EC71DE357B1FE7D); //  2.75573137070700676789e-06
39const S5: f64 = f64::from_bits(0xBE5AE5E68A2B9CEB); // -2.50507602534068634195e-08
40const S6: f64 = f64::from_bits(0x3DE5D93A5ACFD57C); //  1.58969099521155010221e-10
41
42// ──────────────────────────────────────────────────────────────────────────
43// Cos kernel coefficients — classical minimax polynomial on [-π/4, π/4].
44// cos(x) ≈ 1 - x²/2 + x⁴·(C1 + x²·(C2 + …))
45// Uses Kahan trick for precision: w = 1-hz, return w + ((1-w)-hz+r).
46// ──────────────────────────────────────────────────────────────────────────
47
48const C1: f64 = f64::from_bits(0x3FA5555555555549); //  4.16666666666666019037e-02
49const C2: f64 = f64::from_bits(0xBF56C16C16C15177); // -1.38888888888741095749e-03
50const C3: f64 = f64::from_bits(0x3EFA01A019CB1590); //  2.48015872894767294178e-05
51const C4: f64 = f64::from_bits(0xBE927E4F809C52AD); // -2.75573143513906633035e-07
52const C5: f64 = f64::from_bits(0x3E21EE9EBDB4B1C4); //  2.08757232129817482790e-09
53const C6: f64 = f64::from_bits(0xBDA8FAE9BE8838D4); // -1.13596475577881948265e-11
54
55// ──────────────────────────────────────────────────────────────────────────
56// Atan coefficients and constants — classical 11-term polynomial with
57// 5-range argument reduction.
58// ──────────────────────────────────────────────────────────────────────────
59
60const AT: [f64; 11] = [
61    f64::from_bits(0x3FD555555555550D), //  3.33333333333329318027e-01
62    f64::from_bits(0xBFC999999998EBC4), // -1.99999999998764832476e-01
63    f64::from_bits(0x3FC24924920083FF), //  1.42857142725034663711e-01
64    f64::from_bits(0xBFBC71C6FE231671), // -1.11111104054623557880e-01
65    f64::from_bits(0x3FB745CDC54C206E), //  9.09088713343650656196e-02
66    f64::from_bits(0xBFB3B0F2AF749A6D), // -7.69187620504482999495e-02
67    f64::from_bits(0x3FB10D66A0D03D51), //  6.66107313738753120669e-02
68    f64::from_bits(0xBFADDE2D52DEFD9A), // -5.83357013379057348645e-02
69    f64::from_bits(0x3FA97B4B24760DEB), //  4.97687799461593236017e-02
70    f64::from_bits(0xBFA2B4442C6A6C2F), // -3.65315727442169155270e-02
71    f64::from_bits(0x3F90AD3AE322DA11), //  1.62858201153657823623e-02
72];
73
74/// atan reference values: atan(0.5), atan(1.0), atan(1.5), atan(∞).
75/// Using decimal values parsed by the Rust compiler (guaranteed correct f64).
76const ATAN_REF: [f64; 4] = [
77    4.636476090008061e-01,   // atan(0.5)
78    FRAC_PI_4,               // atan(1.0) = π/4
79    9.827_937_232_473_29e-1,   // atan(1.5)
80    FRAC_PI_2,               // atan(∞) = π/2
81];
82
83// ──────────────────────────────────────────────────────────────────────────
84// Exp coefficients and constants — classical exp polynomial with
85// Cody-Waite ln(2) range reduction.
86// exp(x) = 2^k · exp(r), where x = k·ln(2) + r, |r| ≤ 0.5·ln(2).
87// exp(r) ≈ 1 + 2·c / (2 - c), c = r - r²·(P1 + r²·(P2 + r²·(P3 + r²·(P4 + r²·P5))))
88// ──────────────────────────────────────────────────────────────────────────
89
90const EXP_O_THRESHOLD: f64 = f64::from_bits(0x40862E42FEFA39EF); //  7.09782712893383973096e+02
91const EXP_U_THRESHOLD: f64 = f64::from_bits(0xC0874910D52D3051); // -7.45133219101941108420e+02
92const EXP_LN2HI: f64 = f64::from_bits(0x3FE62E42FEE00000);       //  6.93147180369123816490e-01
93const EXP_LN2LO: f64 = f64::from_bits(0x3DEA39EF35793C76);       //  1.90821492927058770002e-10
94const EXP_INVLN2: f64 = f64::from_bits(0x3FF71547652B82FE);      //  1.44269504088896338700e+00
95const EXP_TWOM1000: f64 = f64::from_bits(0x0170000000000000);    //  9.33263618503218878990e-302 = 2^-1000
96
97const EXP_P1: f64 = f64::from_bits(0x3FC555555555553E); //  1.66666666666666019037e-01
98const EXP_P2: f64 = f64::from_bits(0xBF66C16C16BEBD93); // -2.77777777770155933842e-03
99const EXP_P3: f64 = f64::from_bits(0x3F11566AAF25DE2C); //  6.61375632143793436117e-05
100const EXP_P4: f64 = f64::from_bits(0xBEBBBD41C5D26BF1); // -1.65339022054652515390e-07
101const EXP_P5: f64 = f64::from_bits(0x3E66376972BEA4D0); //  4.13813679705723846039e-10
102
103// ──────────────────────────────────────────────────────────────────────────
104// Core polynomial evaluations on reduced range [-π/4, π/4].
105// ──────────────────────────────────────────────────────────────────────────
106
107/// Evaluate sin polynomial for |x| ≤ π/4 (classical sine kernel on the reduced range).
108#[inline]
109fn sin_kern(x: f64) -> f64 {
110    let z = x * x;
111    let v = z * x;
112    let r = S2 + z * (S3 + z * (S4 + z * (S5 + z * S6)));
113    x + v * (S1 + z * r)
114}
115
116/// Evaluate cos polynomial for |x| ≤ π/4 (classical cosine kernel on the reduced range).
117/// cos(x) ≈ 1 - z/2 + z²·(C1 + z·C2 + …) where z = x².
118#[inline]
119fn cos_kern(x: f64) -> f64 {
120    let z = x * x;
121    let r = z * (C1 + z * (C2 + z * (C3 + z * (C4 + z * (C5 + z * C6)))));
122    let hz = 0.5 * z;
123    // Return 1 - (hz - z*r), where z*r = z²·(C1 + z·C2 + …)
124    1.0 - (hz - z * r)
125}
126
127// ──────────────────────────────────────────────────────────────────────────
128// Cody-Waite range reduction: x → r in [-π/4, π/4], quadrant n mod 4.
129// ──────────────────────────────────────────────────────────────────────────
130
131#[inline]
132fn reduce(x: f64) -> (f64, i32) {
133    let n = (x * (2.0 / PI) + 0.5).floor();
134    let r = (x - n * PIO2_HI) - n * PIO2_LO;
135    (r, (n as i64 & 3) as i32)
136}
137
138// ──────────────────────────────────────────────────────────────────────────
139// Public sin / cos / sincos
140// ──────────────────────────────────────────────────────────────────────────
141
142/// Deterministic sine — uses only WASM-intrinsic f64 operations.
143pub fn det_sin(x: f64) -> f64 {
144    if x.is_nan() || x.is_infinite() {
145        return f64::NAN;
146    }
147    let (r, q) = reduce(x);
148    match q {
149        0 =>  sin_kern(r),
150        1 =>  cos_kern(r),
151        2 => -sin_kern(r),
152        3 => -cos_kern(r),
153        _ => unreachable!(),
154    }
155}
156
157/// Deterministic cosine — uses only WASM-intrinsic f64 operations.
158pub fn det_cos(x: f64) -> f64 {
159    if x.is_nan() || x.is_infinite() {
160        return f64::NAN;
161    }
162    let (r, q) = reduce(x);
163    match q {
164        0 =>  cos_kern(r),
165        1 => -sin_kern(r),
166        2 => -cos_kern(r),
167        3 =>  sin_kern(r),
168        _ => unreachable!(),
169    }
170}
171
172/// Deterministic sin and cos computed together (shared range reduction).
173pub fn det_sincos(x: f64) -> (f64, f64) {
174    if x.is_nan() || x.is_infinite() {
175        return (f64::NAN, f64::NAN);
176    }
177    let (r, q) = reduce(x);
178    let s = sin_kern(r);
179    let c = cos_kern(r);
180    match q {
181        0 => ( s,  c),
182        1 => ( c, -s),
183        2 => (-s, -c),
184        3 => (-c,  s),
185        _ => unreachable!(),
186    }
187}
188
189// ──────────────────────────────────────────────────────────────────────────
190// atan / atan2 — classical 5-range argument reduction + polynomial.
191// ──────────────────────────────────────────────────────────────────────────
192
193/// Deterministic atan(x) using 5-range argument reduction + 11-term polynomial.
194fn det_atan(x: f64) -> f64 {
195    if x.is_nan() {
196        return f64::NAN;
197    }
198    let neg = x < 0.0;
199    let mut xa = x.abs();
200
201    // Argument reduction into 5 ranges
202    let id: i32;
203    if xa < 0.4375 {
204        // |x| < 7/16 — use polynomial directly
205        if xa < 1e-29 {
206            return x; // tiny x
207        }
208        id = -1;
209    } else if xa < 1.1875 {
210        if xa < 0.6875 {
211            // 7/16 <= |x| < 11/16 — reduce via atan(0.5)
212            id = 0;
213            xa = (2.0 * xa - 1.0) / (2.0 + xa);
214        } else {
215            // 11/16 <= |x| < 19/16 — reduce via atan(1.0)
216            id = 1;
217            xa = (xa - 1.0) / (xa + 1.0);
218        }
219    } else if xa < 2.4375 {
220        // 19/16 <= |x| < 39/16 — reduce via atan(1.5)
221        id = 2;
222        xa = (xa - 1.5) / (1.0 + 1.5 * xa);
223    } else {
224        // |x| >= 39/16 — reduce via atan(∞) = π/2
225        id = 3;
226        xa = -1.0 / xa;
227    }
228
229    // Polynomial evaluation: split into odd and even parts for accuracy
230    let z = xa * xa;
231    let w = z * z;
232    let s1 = z * (AT[0] + w * (AT[2] + w * (AT[4] + w * (AT[6] + w * (AT[8] + w * AT[10])))));
233    let s2 = w * (AT[1] + w * (AT[3] + w * (AT[5] + w * (AT[7] + w * AT[9]))));
234
235    let result = if id < 0 {
236        xa - xa * (s1 + s2)
237    } else {
238        ATAN_REF[id as usize] + (xa - xa * (s1 + s2))
239    };
240
241    if neg { -result } else { result }
242}
243
244/// Deterministic atan2(y, x) — uses only WASM-intrinsic f64 operations.
245pub fn det_atan2(y: f64, x: f64) -> f64 {
246    if y.is_nan() || x.is_nan() {
247        return f64::NAN;
248    }
249
250    if y == 0.0 {
251        if x > 0.0 || (x == 0.0 && x.is_sign_positive()) {
252            return y; // ±0
253        } else {
254            return if y.is_sign_negative() { -PI } else { PI };
255        }
256    }
257
258    if x == 0.0 {
259        return if y > 0.0 { FRAC_PI_2 } else { -FRAC_PI_2 };
260    }
261
262    if y.is_infinite() {
263        if x.is_infinite() {
264            return if x > 0.0 {
265                if y > 0.0 { FRAC_PI_4 } else { -FRAC_PI_4 }
266            } else if y > 0.0 { 3.0 * FRAC_PI_4 } else { -3.0 * FRAC_PI_4 };
267        }
268        return if y > 0.0 { FRAC_PI_2 } else { -FRAC_PI_2 };
269    }
270
271    if x.is_infinite() {
272        return if x > 0.0 {
273            if y.is_sign_negative() { -0.0 } else { 0.0 }
274        } else if y.is_sign_negative() { -PI } else { PI };
275    }
276
277    // General case
278    let a = det_atan((y / x).abs());
279
280    if x > 0.0 {
281        if y >= 0.0 { a } else { -a }
282    } else if y >= 0.0 { PI - a } else { -(PI - a) }
283}
284
285/// Deterministic hypot(x, y) = sqrt(x² + y²).
286///
287/// sqrt is a WASM intrinsic, so this is deterministic. Uses scaling
288/// to avoid overflow/underflow for extreme values.
289pub fn det_hypot(x: f64, y: f64) -> f64 {
290    let xa = x.abs();
291    let ya = y.abs();
292    let (big, small) = if xa >= ya { (xa, ya) } else { (ya, xa) };
293    if big == 0.0 {
294        return 0.0;
295    }
296    let ratio = small / big;
297    big * (1.0 + ratio * ratio).sqrt()
298}
299
300// ──────────────────────────────────────────────────────────────────────────
301// exp / powi
302// ──────────────────────────────────────────────────────────────────────────
303
304/// Deterministic exp(x) — classical exp polynomial with Cody-Waite ln(2)
305/// range reduction.
306///
307/// Strategy: argument-reduce to `x = k·ln(2) + r` with `|r| ≤ 0.5·ln(2)`,
308/// evaluate `exp(r)` via a 5-term polynomial in `r²`, then scale by `2^k`
309/// using direct exponent-bit manipulation. All operations are pure
310/// IEEE 754 arithmetic + bit cast — bit-exact across iOS / Android /
311/// x86_64 / WASM.
312pub fn det_exp(x: f64) -> f64 {
313    if x.is_nan() {
314        return f64::NAN;
315    }
316    if x.is_infinite() {
317        return if x > 0.0 { f64::INFINITY } else { 0.0 };
318    }
319    if x > EXP_O_THRESHOLD {
320        return f64::INFINITY; // overflow
321    }
322    if x < EXP_U_THRESHOLD {
323        return 0.0; // underflow
324    }
325
326    // High 32 bits of |x| (sign bit cleared); used for branch thresholds
327    // via the standard GET_HIGH_WORD bit-extraction pattern.
328    let hx_abs = (x.abs().to_bits() >> 32) as u32;
329
330    // Argument reduction: x = k·ln(2) + r, |r| ≤ 0.5·ln(2)
331    let (hi, lo, k): (f64, f64, i32) = if hx_abs > 0x3fd62e42 {
332        // |x| > 0.5·ln(2) ≈ 0.347
333        if hx_abs < 0x3FF0A2B2 {
334            // |x| < 1.5·ln(2) ≈ 1.04 — single-step k = ±1
335            if x.is_sign_negative() {
336                (x + EXP_LN2HI, -EXP_LN2LO, -1)
337            } else {
338                (x - EXP_LN2HI, EXP_LN2LO, 1)
339            }
340        } else {
341            // General case: k = round-toward-zero(x · 1/ln(2) + ±0.5)
342            let half = if x.is_sign_negative() { -0.5 } else { 0.5 };
343            let k_int = (EXP_INVLN2 * x + half) as i32;
344            let t = k_int as f64;
345            (x - t * EXP_LN2HI, t * EXP_LN2LO, k_int)
346        }
347    } else if hx_abs < 0x3e300000 {
348        // |x| < 2^-28 — exp(x) ≈ 1 + x
349        return 1.0 + x;
350    } else {
351        // |x| in [2^-28, 0.5·ln(2)] — no reduction needed
352        (x, 0.0, 0)
353    };
354
355    let r = hi - lo;
356    let z = r * r;
357    let c = r - z * (EXP_P1 + z * (EXP_P2 + z * (EXP_P3 + z * (EXP_P4 + z * EXP_P5))));
358
359    let y = if k == 0 {
360        1.0 - ((r * c) / (c - 2.0) - r)
361    } else {
362        1.0 - ((lo - (r * c) / (2.0 - c)) - hi)
363    };
364
365    if k == 0 {
366        return y;
367    }
368
369    // Scale by 2^k via direct exponent-bit add. After reduction, y ∈ ~[0.5, 2],
370    // so its biased exponent is 1022, 1023, or 1024 — adding k stays in the
371    // valid normal range as long as k ≥ -1021. Below that we scale via 2^-1000.
372    if k >= -1021 {
373        let bits = y.to_bits().wrapping_add(((k as i64) << 52) as u64);
374        f64::from_bits(bits)
375    } else {
376        let bits = y.to_bits().wrapping_add((((k + 1000) as i64) << 52) as u64);
377        f64::from_bits(bits) * EXP_TWOM1000
378    }
379}
380
381/// Deterministic integer-exponent power: `base^n` for `n: i32`.
382///
383/// Uses exponentiation-by-squaring with bit-exact IEEE 754 multiply.
384/// For our codec / stego use cases (`n` typically in `[-32, 64]`) the
385/// log₂(|n|) ≤ 6 steps means at most ~12 multiplications, each
386/// deterministic per IEEE 754. Negative `n` inverts the result via a
387/// single deterministic divide.
388///
389/// Behaviour matches `f64::powi` semantically but is bit-exact across
390/// iOS / Android / x86_64 / WASM (whereas `f64::powi` lowers to
391/// `@llvm.powi.f64` which has implementation-defined rounding).
392pub fn det_powi_f64(base: f64, n: i32) -> f64 {
393    if n == 0 {
394        return 1.0;
395    }
396    if base == 1.0 {
397        return 1.0;
398    }
399    let mut e: u64 = (n as i64).unsigned_abs();
400    let mut b = base;
401    let mut result = 1.0;
402    while e > 0 {
403        if e & 1 == 1 {
404            result *= b;
405        }
406        e >>= 1;
407        if e > 0 {
408            b *= b;
409        }
410    }
411    if n < 0 { 1.0 / result } else { result }
412}
413
414// ──────────────────────────────────────────────────────────────────────────
415// Tests
416// ──────────────────────────────────────────────────────────────────────────
417
418#[cfg(test)]
419mod tests {
420    use super::*;
421    use std::f64::consts::{FRAC_PI_3, FRAC_PI_6};
422
423    fn approx_eq(a: f64, b: f64, tol: f64) -> bool {
424        if a.is_nan() && b.is_nan() { return true; }
425        (a - b).abs() <= tol
426    }
427
428    #[test]
429    fn sin_exact_values() {
430        let tol = 1e-15;
431        assert!(approx_eq(det_sin(0.0), 0.0, tol));
432        assert!(approx_eq(det_sin(FRAC_PI_6), 0.5, tol));
433        assert!(approx_eq(det_sin(FRAC_PI_4), std::f64::consts::FRAC_1_SQRT_2, tol));
434        assert!(approx_eq(det_sin(FRAC_PI_3), 3.0_f64.sqrt() / 2.0, tol));
435        assert!(approx_eq(det_sin(FRAC_PI_2), 1.0, tol));
436        assert!(approx_eq(det_sin(PI), 0.0, 1e-15));
437        assert!(approx_eq(det_sin(-FRAC_PI_2), -1.0, tol));
438    }
439
440    #[test]
441    fn cos_exact_values() {
442        let tol = 1e-15;
443        assert!(approx_eq(det_cos(0.0), 1.0, tol));
444        assert!(approx_eq(det_cos(FRAC_PI_6), 3.0_f64.sqrt() / 2.0, tol));
445        assert!(approx_eq(det_cos(FRAC_PI_4), std::f64::consts::FRAC_1_SQRT_2, tol));
446        assert!(approx_eq(det_cos(FRAC_PI_3), 0.5, tol));
447        assert!(approx_eq(det_cos(FRAC_PI_2), 0.0, 1e-15));
448        assert!(approx_eq(det_cos(PI), -1.0, tol));
449    }
450
451    #[test]
452    fn sincos_identity() {
453        // sin²(x) + cos²(x) = 1 for various x
454        for i in 0..200 {
455            let x = (i as f64 - 100.0) * 0.13;
456            let (s, c) = det_sincos(x);
457            let err = (s * s + c * c - 1.0).abs();
458            assert!(err < 1e-14, "sin²+cos²={} at x={x} (err={err})", s * s + c * c);
459        }
460    }
461
462    #[test]
463    fn sincos_matches_separate() {
464        for i in 0..50 {
465            let x = (i as f64 - 25.0) * 0.37;
466            let (s, c) = det_sincos(x);
467            assert_eq!(s, det_sin(x), "sin mismatch at x={x}");
468            assert_eq!(c, det_cos(x), "cos mismatch at x={x}");
469        }
470    }
471
472    #[test]
473    fn sin_large_argument() {
474        let x = 1e6;
475        let s = det_sin(x);
476        let c = det_cos(x);
477        let err = (s * s + c * c - 1.0).abs();
478        assert!(err < 1e-6, "sin²+cos²={} at x={x}", s * s + c * c);
479    }
480
481    #[test]
482    fn sin_special_values() {
483        assert!(det_sin(f64::NAN).is_nan());
484        assert!(det_sin(f64::INFINITY).is_nan());
485        assert!(det_sin(f64::NEG_INFINITY).is_nan());
486    }
487
488    #[test]
489    fn atan2_quadrants() {
490        let eps = 1e-15;
491        assert!(approx_eq(det_atan2(1.0, 1.0), FRAC_PI_4, eps));
492        assert!(approx_eq(det_atan2(1.0, -1.0), 3.0 * FRAC_PI_4, eps));
493        assert!(approx_eq(det_atan2(-1.0, -1.0), -3.0 * FRAC_PI_4, eps));
494        assert!(approx_eq(det_atan2(-1.0, 1.0), -FRAC_PI_4, eps));
495        assert!(approx_eq(det_atan2(0.0, 1.0), 0.0, eps));
496        assert!(approx_eq(det_atan2(1.0, 0.0), FRAC_PI_2, eps));
497        assert!(approx_eq(det_atan2(0.0, -1.0), PI, eps));
498        assert!(approx_eq(det_atan2(-1.0, 0.0), -FRAC_PI_2, eps));
499    }
500
501    #[test]
502    fn atan2_special_values() {
503        assert!(det_atan2(f64::NAN, 1.0).is_nan());
504        assert!(det_atan2(1.0, f64::NAN).is_nan());
505    }
506
507    #[test]
508    fn atan_specific_values() {
509        let eps = 1e-15;
510        assert!(approx_eq(det_atan(0.0), 0.0, eps));
511        assert!(approx_eq(det_atan(1.0), FRAC_PI_4, eps));
512        assert!(approx_eq(det_atan(-1.0), -FRAC_PI_4, eps));
513        // atan(√3) = π/3
514        assert!(approx_eq(det_atan(3.0_f64.sqrt()), FRAC_PI_3, eps));
515        // atan(1/√3) = π/6
516        assert!(approx_eq(det_atan(1.0 / 3.0_f64.sqrt()), FRAC_PI_6, eps));
517    }
518
519    #[test]
520    fn hypot_basic() {
521        assert!(approx_eq(det_hypot(3.0, 4.0), 5.0, 1e-15));
522        assert!(approx_eq(det_hypot(0.0, 0.0), 0.0, 0.0));
523        assert!(approx_eq(det_hypot(1.0, 0.0), 1.0, 0.0));
524        assert!(approx_eq(det_hypot(0.0, 1.0), 1.0, 0.0));
525    }
526
527    #[test]
528    fn hypot_no_overflow() {
529        let big = 1e300;
530        let h = det_hypot(big, big);
531        assert!(h.is_finite());
532        assert!(approx_eq(h, big * 2.0_f64.sqrt(), big * 1e-14));
533    }
534
535    #[test]
536    fn deterministic_across_calls() {
537        for i in 0..100 {
538            let x = (i as f64) * 0.0731 - 3.5;
539            assert_eq!(det_sin(x).to_bits(), det_sin(x).to_bits());
540            assert_eq!(det_cos(x).to_bits(), det_cos(x).to_bits());
541        }
542    }
543
544    #[test]
545    fn matches_std_closely() {
546        for i in 0..200 {
547            let x = (i as f64 - 100.0) * 0.05;
548            let ds = det_sin(x);
549            let ss = x.sin();
550            assert!((ds - ss).abs() < 5e-13,
551                "det_sin({x})={ds} vs std sin={ss}, diff={}", (ds - ss).abs());
552            let dc = det_cos(x);
553            let sc = x.cos();
554            assert!((dc - sc).abs() < 5e-13,
555                "det_cos({x})={dc} vs std cos={sc}, diff={}", (dc - sc).abs());
556        }
557    }
558
559    #[test]
560    fn exp_exact_values() {
561        let tol = 1e-15;
562        assert_eq!(det_exp(0.0).to_bits(), 1.0_f64.to_bits());
563        assert_eq!(det_exp(-0.0).to_bits(), 1.0_f64.to_bits());
564        assert!(approx_eq(det_exp(1.0), std::f64::consts::E, tol));
565        assert!(approx_eq(det_exp(-1.0), 1.0 / std::f64::consts::E, tol));
566        assert!(approx_eq(det_exp(2.0), std::f64::consts::E.powi(2), 1e-14));
567        assert!(approx_eq(det_exp(-3.0), 0.049787068367863944, 1e-15));
568        assert!(approx_eq(det_exp(0.5), 1.6487212707001282, tol));
569    }
570
571    #[test]
572    fn exp_special_values() {
573        assert!(det_exp(f64::NAN).is_nan());
574        assert_eq!(det_exp(f64::INFINITY), f64::INFINITY);
575        assert_eq!(det_exp(f64::NEG_INFINITY), 0.0);
576        // Overflow / underflow saturate.
577        assert_eq!(det_exp(800.0), f64::INFINITY);
578        assert_eq!(det_exp(-800.0), 0.0);
579    }
580
581    #[test]
582    fn exp_tiny_arguments() {
583        // For |x| < 2^-28 the algorithm short-circuits to 1+x.
584        let tiny = 1e-12;
585        let res = det_exp(tiny);
586        assert!((res - (1.0 + tiny)).abs() < 1e-24);
587        let neg_tiny = -1e-12;
588        let res = det_exp(neg_tiny);
589        assert!((res - (1.0 + neg_tiny)).abs() < 1e-24);
590    }
591
592    #[test]
593    fn exp_matches_std_closely() {
594        // Match std f64::exp within a tight tolerance for typical input ranges.
595        for i in 0..200 {
596            let x = (i as f64 - 100.0) * 0.07;
597            let de = det_exp(x);
598            let se = x.exp();
599            let rel = if se != 0.0 { ((de - se) / se).abs() } else { (de - se).abs() };
600            assert!(rel < 5e-15,
601                "det_exp({x})={de} vs std exp={se}, rel={rel}");
602        }
603    }
604
605    #[test]
606    fn exp_deterministic_across_calls() {
607        for i in 0..100 {
608            let x = (i as f64) * 0.0731 - 3.5;
609            assert_eq!(det_exp(x).to_bits(), det_exp(x).to_bits());
610        }
611    }
612
613    #[test]
614    fn powi_zero_exponent() {
615        // x^0 = 1 for any non-NaN x (matches f64::powi).
616        assert_eq!(det_powi_f64(0.0, 0).to_bits(), 1.0_f64.to_bits());
617        assert_eq!(det_powi_f64(1.0, 0).to_bits(), 1.0_f64.to_bits());
618        assert_eq!(det_powi_f64(-3.5, 0).to_bits(), 1.0_f64.to_bits());
619        assert_eq!(det_powi_f64(1e300, 0).to_bits(), 1.0_f64.to_bits());
620    }
621
622    #[test]
623    fn powi_one_exponent() {
624        for &b in &[0.0, 1.0, -1.0, 2.5, -7.3, 1e-10, 1e10] {
625            assert_eq!(det_powi_f64(b, 1).to_bits(), b.to_bits(),
626                "powi({b}, 1) should equal {b}");
627        }
628    }
629
630    #[test]
631    fn powi_basic() {
632        assert_eq!(det_powi_f64(2.0, 10), 1024.0);
633        assert_eq!(det_powi_f64(2.0, 16), 65536.0);
634        assert_eq!(det_powi_f64(0.5, 3), 0.125);
635        assert_eq!(det_powi_f64(0.75, 4), 0.31640625);
636        assert_eq!(det_powi_f64(-1.0, 2), 1.0);
637        assert_eq!(det_powi_f64(-1.0, 3), -1.0);
638        assert_eq!(det_powi_f64(-2.0, 4), 16.0);
639    }
640
641    #[test]
642    fn powi_negative_exponent() {
643        assert_eq!(det_powi_f64(2.0, -1), 0.5);
644        assert_eq!(det_powi_f64(2.0, -10), 1.0 / 1024.0);
645        assert_eq!(det_powi_f64(0.5, -3), 8.0);
646        // Powers of 2 are exact f64 — bit-exact equality.
647        assert_eq!(det_powi_f64(2.0, -8).to_bits(), (1.0_f64 / 256.0).to_bits());
648    }
649
650    #[test]
651    fn powi_matches_std_closely() {
652        // For powers of 2 the result is exactly representable; should be
653        // bit-exact even compared to std.
654        for k in -10..=10 {
655            let d = det_powi_f64(2.0, k);
656            let s = 2.0_f64.powi(k);
657            assert_eq!(d.to_bits(), s.to_bits(),
658                "powi(2.0, {k}): det={d} vs std={s}");
659        }
660        // For non-powers-of-2 we should match within a few ULPs.
661        for &b in &[0.75, 1.5, 0.9, 1.1, 3.7] {
662            for k in 0..=12 {
663                let d = det_powi_f64(b, k);
664                let s = b.powi(k);
665                let rel = ((d - s) / s).abs();
666                assert!(rel < 5e-15,
667                    "powi({b}, {k}): det={d} vs std={s}, rel={rel}");
668            }
669        }
670    }
671
672    #[test]
673    fn powi_deterministic_across_calls() {
674        for k in -20..=20 {
675            for &b in &[0.5, 0.75, 1.5, 2.0, 3.0] {
676                assert_eq!(det_powi_f64(b, k).to_bits(),
677                           det_powi_f64(b, k).to_bits());
678            }
679        }
680    }
681}