Skip to main content

alice_det_math/
lib.rs

1//! Deterministic `f32` / `f64` transcendentals (cross-platform bit-exact).
2//!
3//! IEEE 754 guarantees that `+`, `-`, `*`, `/` and `sqrt` produce the same
4//! bits on every platform Rust targets (SSE2+, aarch64, wasm32). It does
5//! **not** cover `sin`, `exp`, `ln`, `powf`, `cbrt`, `hypot`, … — those come
6//! from the platform `libm` and differ in the last ulp between macOS, glibc,
7//! MSVC and wasm. A lockstep simulation that calls them diverges across
8//! peers; a signed-distance field evaluated on two machines disagrees about
9//! which side of a surface a point is on.
10//!
11//! Every function here is built from the guaranteed operations only —
12//! integer range reduction (`trunc(t + (t < 0 ? -0.5 : 0.5))`, three
13//! operations), fixed-degree polynomials evaluated in a fixed order, and
14//! bit-level exponent construction — so the result is a pure function of
15//! the input bits on every target. `mul_add` is not used
16//! anywhere: it fuses into one rounding on FMA hardware and stays two
17//! roundings elsewhere, which would break the guarantee.
18//!
19//! The crate is `no_std` (no `alloc` either). `round` and `sqrt`, which
20//! `core` does not provide, are in [`ops`]; with the `std` feature `sqrt` is
21//! the hardware instruction, without it a correctly rounded software one —
22//! both return the same bits.
23//!
24//! With the `simd` feature, the `simd` module provides `wide::f32x8` versions
25//! that are lane-for-lane bit-identical to the scalar functions.
26//!
27//! ```rust
28//! use alice_det_math::{atan2, exp, sin_cos, sin_cos64};
29//!
30//! let (s, c) = sin_cos(1.0_f32);
31//! assert_eq!(s.to_bits(), 0x3f57_6aa5); // the same bits on every platform
32//! assert_eq!(c.to_bits(), 0x3f0a_5140);
33//! assert_eq!(exp(1.0_f32).to_bits(), 0x402d_f854);
34//! let a = atan2(0.0_f32, -1.0); // π, fdlibm special cases
35//! assert_eq!(a, core::f32::consts::PI);
36//!
37//! // f64 over the whole range: 1e22 needs the Payne–Hanek reduction
38//! let (s, c) = sin_cos64(1.0e22);
39//! assert_eq!(s.to_bits(), 0xbfeb_453a_b76b_f397); // -0.8522008497671888
40//! assert_eq!(c.to_bits(), 0x3fe0_be2c_ef01_c8f4); // 0.523214785395139
41//! ```
42//!
43//! # Accuracy
44//!
45//! Measured over dense sweeps (`tests/accuracy.rs`). `f32` functions are
46//! compared with a correctly rounded reference (`f64` libm rounded once to
47//! `f32`), [`sin64`] / [`cos64`] with an independent correctly rounded
48//! reference, the other `f64` functions with the platform `f64` libm — which itself
49//! varies by ≤ 1 ulp between macOS / glibc / MSVC, so those bounds carry that
50//! slack:
51//!
52//! | function | domain measured | max error |
53//! |----------|-----------------|-----------|
54//! | [`sin`] / [`cos`] / [`sin_cos`] | `\|x\| ≤ 100` | ≤ 2 ulp (Cephes single-precision coefficients), including every input within 64 ulp of a zero `k·π/2`; measured max 2 out to `\|x\| ≤ 8192` (`tests/accuracy_trig_zeros.rs`) |
55//! | [`exp`] | `[-87, 88]` | ≤ 2 ulp |
56//! | [`ln`] | `[1e-30, 1e30]` | ≤ 1 ulp (musl `logf` algorithm) |
57//! | [`log2`] / [`log10`] | `[1e-30, 1e30]` | ≤ 1 ulp, measured max 0 ([`log2_64`] / [`log10_64`] rounded once; powers of two are exact) |
58//! | [`cbrt`] | `[1e-30, 1e30]` | ≤ 1 ulp (bit-hack seed + 3 Newton steps) |
59//! | [`hypot`] | finite | ≤ 1 ulp of `sqrt(x² + y²)` |
60//! | [`powf`] | `x ∈ [1e-3, 1e3]`, `\|y\| ≤ 8` | ≤ 1 ulp (evaluated in `f64`, rounded once) |
61//! | [`atan`] / [`atan2`] | `\|x\| ≤ 1e4`, all quadrants | ≤ 1 ulp (fdlibm `s_atanf` / `e_atan2f`, single precision) |
62//! | [`asin`] / [`acos`] | `[-1, 1]` | ≤ 1 ulp (fdlibm in `f64`, rounded once) |
63//! | [`tan`] | `\|x\| ≤ 100` | ≤ 1 ulp (fdlibm `k_sin`/`k_cos` in `f64`) |
64//! | [`tanh`] | finite | ≤ 1 ulp (`exp64` based, `x` below 2^-14) |
65//! | [`sin64`] / [`cos64`] / [`sin_cos64`] | every finite `f64` (Cody–Waite below `2^20·π/2`, Payne–Hanek above) | ≤ 1 ulp of correctly rounded, measured max 1 over 6234 points of an independent 2400-bit reference (musl `sin.c` / `cos.c`) |
66//! | [`tan64`] | every finite `f64` (same reduction as [`sin64`]) | ≤ 1 ulp of correctly rounded, measured max 1 over 6310 points of an independent 2400-bit reference (fdlibm `k_tan.c`) |
67//! | [`atan64`] / [`atan2_64`] | every finite `f64`, all quadrants | ≤ 1 ulp of correctly rounded, measured max 1 over 1758 points of an independent 2400-bit reference covering every branch of the reduction (fdlibm `s_atan.c` / `e_atan2.c`) |
68//! | [`exp64`] / [`ln64`] | `[-745, 710]` / `[1e-300, 1e300]` | ≤ 2 ulp, measured max 1 against an independent 2400-bit reference (fdlibm algorithms) |
69//! | [`log2_64`] / [`log10_64`] | `[1e-300, 1e300]` | ≤ 2 ulp of the platform libm, measured max 2 / 1 (`ln64` converted to the base; powers of two are exact for [`log2_64`]) |
70//! | [`powf64`] | `x ∈ [1e-3, 1e3]`, `\|y\| ≤ 8` | ≤ 16 ulp measured 13 (`exp64(y·ln64 x)` with double-double argument) ⚠️ outside this domain the result can be *wrong*, not just less accurate — see [`powf64`] |
71//! | [`round`] / [`round64`] / [`sqrt`] / [`sqrt64`] | all | exact (bit-identical to `f32::round` / `f32::sqrt`, except that every NaN is the canonical one) |
72//!
73//! The reduction subtracts `k·π/2` in four `f32` pieces, the third formed
74//! exactly with Dekker's two-product (no fused multiply-add, so every target
75//! computes the same bits). The first two products are exact only for
76//! `|k| < 2¹³`, so large arguments to [`sin`] / [`cos`] (`|x| > 2¹³`) are
77//! still deterministic but lose accuracy, exactly as `libm`'s own `sinf` does
78//! without Payne–Hanek. Within the domain the remaining error is the Cephes
79//! polynomial's (≤ 2 ulp). [`sin64`] / [`cos64`] reduce
80//! every finite argument exactly (Payne–Hanek with 2/π to 1584 bits), so
81//! their bound holds up to `f64::MAX`; the reference values are in
82//! `tests/data/sin_cos64_reference.txt`, generated by
83//! `scripts/gen_sin_cos64_reference.py` (`mpmath`, not a libm). Every other
84//! `f64` function whose bound is asserted has its own table under
85//! `tests/data/`, from the matching `scripts/gen_*_reference.py`, so no `f64`
86//! bound depends on a platform `libm`. [`tan64`] is
87//! deliberately not compared with a platform `libm` at all: one disagrees
88//! with the correctly rounded value by ~1.0e5 ulp in the Payne–Hanek range,
89//! so a bound on that difference would depend on the machine running it.
90//!
91//! # Guarantee scope
92//!
93//! Bit-exactness relies on the target honouring IEEE 754 for the basic
94//! operations: `x86_64` (SSE2), `aarch64`, `wasm32` and every other Rust tier-1/2
95//! target qualify. 32-bit x86 built for x87 (`i586`) without SSE2, or any
96//! build with fast-math style flags, is outside the guarantee.
97//! `tests/golden.rs` pins a SHA-256 of every function's output over a fixed
98//! input grid; CI runs it on macOS (ARM / Intel), Linux (x86 / ARM), Windows
99//! and wasm32, and with the SIMD fallback paths forced.
100//!
101//! # Policy for consumers
102//!
103//! List the `f32` / `f64` `libm` methods under `clippy.toml`
104//! `disallowed-methods` and run clippy with `-D warnings`, so a stray
105//! `x.sin()` on a float is a red gate; call these functions instead.
106
107#![cfg_attr(not(feature = "std"), no_std)]
108#![cfg_attr(docsrs, feature(doc_cfg))]
109
110/// Identifies the numeric behaviour of this crate: it changes whenever any
111/// function here would return different bits for the same input, and
112/// otherwise only when the set of functions it covers grows.
113///
114/// ```text
115/// 135c407d4249c1c200e4d1be79a12e848ce5c355fc3a918da965849e697ef000
116/// ```
117///
118/// It is the SHA-256 of the per-function bit pins in `tests/golden.rs` —
119/// each of which is itself a SHA-256 over one function's output on a fixed
120/// input grid — folded in ascending order of function name, each as the
121/// name's length in four big-endian bytes, then the name, then its 32-byte
122/// pin. Because the inputs are the recorded pins and nothing else, the value
123/// is a property of this crate alone: no platform `libm`, no timing, no
124/// environment enters it, and it is the same on every target the pins
125/// reproduce on.
126///
127/// A caller that derives an identifier from a formula plus its parameters can
128/// mix this in, so that the identifier also covers *how the arithmetic was
129/// evaluated*. Two runs that agree on formula, parameters and this value
130/// computed the same bits; if this value differs, they did not, even when
131/// everything else matches.
132///
133/// `tests/golden.rs` recomputes it from the pins and fails if the two
134/// disagree; it also reads every `pub fn` this crate defines out of its own
135/// sources and fails if one has no pin, so the constant cannot drift from the
136/// behaviour it names. The SIMD kernels are covered by parity with the scalar
137/// functions rather than by pins of their own: they are feature-gated, and an
138/// identifier that changed with the feature set would not identify the
139/// arithmetic.
140///
141/// # Re-recording it, in this order
142///
143/// After an intentional change to any function's output, update the
144/// per-function hashes in `tests/golden.rs` **first**, and only then recompute
145/// this value. The fold reads the table as it is written in the source, so
146/// running the re-pin before the table is updated reproduces the old value and
147/// looks as though the identifier failed to notice the change.
148pub const SEMANTICS_ID: [u8; 32] = [
149    0x13, 0x5c, 0x40, 0x7d, 0x42, 0x49, 0xc1, 0xc2, 0x00, 0xe4, 0xd1, 0xbe, 0x79, 0xa1, 0x2e, 0x84,
150    0x8c, 0xe5, 0xc3, 0x55, 0xfc, 0x3a, 0x91, 0x8d, 0xa9, 0x65, 0x84, 0x9e, 0x69, 0x7e, 0xf0, 0x00,
151];
152
153pub mod semantics;
154pub use semantics::{check_semantics, SemanticsCheck, PREVIOUS_SEMANTICS_IDS};
155
156pub mod double;
157pub mod metric;
158pub mod ops;
159#[cfg(feature = "simd")]
160#[cfg_attr(docsrs, doc(cfg(feature = "simd")))]
161pub mod simd;
162pub mod single;
163
164/// The `f32` kernel constants (Cephes / musl), for code generators that must
165/// emit the *same* law elsewhere (a JIT, a shader) and be checked against
166/// these functions bit for bit.
167pub mod consts {
168    pub use crate::single::{
169        COS_P, EXP_HI, EXP_LO, EXP_P, FRAC_2_PI, LG1, LG2, LG3, LG4, LN2_HI, LN2_LO, LOG2E, PIO2_1,
170        PIO2_2, PIO2_3, PIO2_4, SIN_P, SPLIT_MASK, SQRT2_BITS,
171    };
172}
173
174pub use double::{
175    acos64, asin64, atan2_64, atan64, cos64, exp64, ln64, log10_64, log2_64, powf64, sin64,
176    sin_cos64, tan64,
177};
178pub use ops::{round, round64, sqrt, sqrt64};
179pub use single::{
180    acos, asin, atan, atan2, cbrt, cos, exp, hypot, ln, log10, log2, powf, powi, sin, sin_cos, tan,
181    tanh,
182};