alice-det-math
Cross-platform bit-exact f32 / f64 transcendentals — sin, cos, exp,
ln, atan2, asin, acos, tan, tanh, powf, cbrt, hypot, … — built
from IEEE 754 basic operations only, so every function is a pure function of
its input bits on x86_64, aarch64, wasm32 and every other IEEE target.
no_std, no alloc, no unsafe.
The platform libm is not: sin(x) differs in the last ulp between macOS,
glibc, MSVC and wasm. A lockstep simulation that calls it diverges across
peers; a signed-distance field evaluated on two machines disagrees about which
side of a surface a point is on. This crate is the transcendental layer of
alice-physics (128-bit deterministic
physics) and alice-sdf (SDF geometry),
extracted so both — and anything else — evaluate the same law.
It is not a faster or more accurate libm: the functions are within 1–2 ulp
of correctly rounded, not correctly rounded, and they keep a fixed operation
order (no mul_add) rather than the fastest one for each target. Use it where two machines must produce the same bits; where
only the value matters, the platform libm is the better choice.
License: Apache-2.0
Contents
- Installation
- Example
- Features
SEMANTICS_ID- Policy for consumers
- Accuracy and guarantee scope
- no_std
- Minimum supported Rust version
- Building and testing
- Related Projects
- License
Installation
Example
use ;
let = sin_cos;
assert_eq!; // the same bits on every platform
assert_eq!;
assert_eq!;
let a = atan2; // π, fdlibm special cases
assert_eq!;
// f64 over the whole range: 1e22 needs the Payne–Hanek reduction
let = sin_cos64;
assert_eq!; // -0.8522008497671888
assert_eq!; // 0.523214785395139
With simd:
use simd;
use f32x8;
let x = new;
let = sin_cos;
for in x.to_array.into_iter.enumerate
Features
- Scalar
f32kernels (Cephessinf/cosf/expf, musllogf) andf64kernels (fdlibmatan/atan2/asin/acos/exp/log), ≤ 1–2 ulp of correctly rounded over the documented domains sin64/cos64/sin_cos64:f64sine and cosine over every finite argument — muslsin.c/cos.cwith the Payne–Hanek reduction for|x| ≥ 2^20·π/2, ≤ 1 ulp of correctly rounded (measured against 6234 values computed withmpmathat 2400 bits, not against a libm);sin_cos64reduces once and is bit-identical to the two separate callstan64:f64tangent over every finite argument — fdlibmk_tan.con
the same reduction, ≤ 1 ulp of correctly rounded (measured against 6310
values from mpmath at 2400 bits). It is not compared with a platform libm
at all: one is ~1.0e5 ulp off in the Payne–Hanek range while another is
within 2 ulp, so a bound on that difference would depend on the machine
log2/log10/log2_64/log10_64: base-2 and base-10
logarithms on the exponent split of fdlibm e_log10.c, which keeps the
mantissa below 1 for a negative exponent so nothing cancels around x = 1.
A power of two returns its exponent exactly, down to the last subnormal;
log2_64 is ≤ 2 ulp and log10_64 ≤ 1 ulp, and the f32 entry points are
the f64 kernels rounded once (measured max 0 ulp from correctly rounded)
- SIMD (
simdfeature):wide::f32x8versions ofsin/cos/sin_cos/exp/ln/round/sqrt, lane-for-lane bit-identical to the scalar functions — same constants, same operation order, nomul_add metric:lerp/clamp/smoothstepwith one canonical operation
order, the three 3D norms, and MetricWeights — a metric built as a
non-negative combination of ‖·‖₁ / ‖·‖₂ / ‖·‖∞, carrying exact closed
forms for its Lipschitz constant and for how far a ball of radius r
reaches in Euclidean space
round/sqrtwithout libm: musl'sx + 2^23 − 2^23rounding and a correctly rounded software square root, so the crate works on bare-metal targets and returns the same bits there- Canonical NaN: a NaN input never propagates through arithmetic (whose payload / sign rules are platform-specific); every function returns the canonical NaN or passes the input bits through unchanged
- Pinned:
tests/golden.rsholds a SHA-256 of every function over a fixed input grid; CI reproduces it on macOS ARM / Intel, Linux x86 / ARM, Windows, wasm32 (wasmtime), with the SSE2-only SIMD fallback, with AVX2 + FMA forced on, and withno_std(softwaresqrt) SEMANTICS_ID: one 32-byte constant that identifies this crate's
numeric behaviour (see below)
SEMANTICS_ID
SEMANTICS_ID is a 32-byte constant that identifies *how this crate evaluates
arithmetic*. It changes whenever any function here would return different bits
for the same input, and otherwise only when the set of functions it covers
grows: it is the SHA-256 of the per-function bit pins in tests/golden.rs,
folded in ascending order of function name, each as the name's length in four
big-endian bytes, then the name, then its 32-byte pin. No platform libm, no
timing and no environment enters it, so it is the same value on every target
the pins reproduce on.
It is for callers that build an identifier out of a formula and its parameters: mixing this in makes the identifier cover the arithmetic as well, so two runs that agree on formula, parameters and this value computed the same bits — and if the value differs, they did not, however well everything else matches.
Three tests keep it honest. One recomputes it from the pins and fails if the
constant has drifted. The second reads every pub fn the crate defines out of
its sources — not only the names re-exported at the crate root — and fails if
any of them has no pin, since a function outside the table could change
behaviour without moving the identifier; it also fails if a module file is
added that the parser does not read. The third covers the SIMD kernels, which
carry no pin of their own because they are feature-gated and an identifier
that changed with the feature set would identify nothing: each one must return
the bits its scalar counterpart returns, lane for lane, so the test fails if
any SIMD function has no parity test.
It moved in 0.4.0, when a corrected atan64 coefficient changed that
function's output bits — which is the intended behaviour: a consumer that
mixes this value into its own identifiers sees the arithmetic change instead
of silently inheriting it. It moved again in 0.5.0 for the other reason: no
function changed, but the 11 metric entry points that had been outside the
table joined it.
Reading an identifier back
Data written by an earlier release carries that release's identifier.
check_semantics(&id) returns Verified for SEMANTICS_ID,
VerifiedPrevious(id) for an entry of PREVIOUS_SEMANTICS_IDS, and
Mismatch otherwise. An entry is listed only when this release returns that
release's bits on every function the release pinned — 0.4.0 is listed, since
0.5.0 changed no output — and a test proves it from that release's pin table.
Readers of stored values use is_reproducible() (accepts both verified
cases); code that compares identifiers as keys uses is_current(). Writers
always stamp SEMANTICS_ID.
Policy for consumers
Add the platform libm methods to clippy.toml disallowed-methods and run
clippy with -D warnings, so a stray x.sin() on a float is a red gate:
= [
{ = "f32::sin", = "platform libm; use alice_det_math::sin" },
{ = "f32::cos", = "platform libm; use alice_det_math::cos" },
{ = "f64::sin", = "platform libm; use alice_det_math::sin64" },
{ = "f64::cos", = "platform libm; use alice_det_math::cos64" },
{ = "f32::exp", = "platform libm; use alice_det_math::exp" },
{ = "f32::ln", = "platform libm; use alice_det_math::ln" },
{ = "f32::atan2", = "platform libm; use alice_det_math::atan2" },
{ = "f32::mul_add", = "fused on FMA targets only; write a * b + c" },
]
Accuracy and guarantee scope
See the crate documentation (cargo doc --open) for the per-function error
table and the exact scope of the bit-exactness guarantee (IEEE 754 basic
operations on the target; x87 without SSE2 and fast-math builds are outside it).
The reference values for every f64 function whose bound is asserted are
committed under tests/data/, so no f64 bound depends on the platform
libm (atan64 is the one exception). They are regenerated with
no_std
The crate is #![no_std] without the default std feature and needs no
alloc. sqrt is then a correctly rounded software square root that returns
the same bits as the hardware instruction; CI builds the library for
thumbv7em-none-eabihf (with and without simd) and runs the golden hashes
with the software path.
Minimum supported Rust version
Rust 1.85 (rust-version in Cargo.toml), checked in CI for the library with
default features, with std,simd, and as a no_std rlib for
thumbv7em-none-eabihf. Development and CI use the toolchain pinned in
rust-toolchain.toml.
Building and testing
Related Projects
Crates that evaluate their laws through this one, so two machines agree bit for
bit on the result. This crate is the joint of the ALICE core: a signed-distance
field (ALICE-SDF) and the bodies
moving through it (ALICE-Physics)
only agree on a surface if they compute sin the same way, and a law verifier
(ALICE-LOL) can only prove something
about a field that is reproducible in the first place.
Keep the version unified across the resolved graph. Two versions of this crate in one dependency tree means two implementations of the same function, and the guarantee is gone. Check with
cargo tree -i alice-det-math.
| Project | Why it needs bit-exact transcendentals | Links |
|---|---|---|
| ALICE-SDF | A signed-distance field evaluated on two machines must agree which side of a surface a point is on | crates.io · docs.rs · GitHub |
| ALICE-Physics | Lockstep / rollback simulation diverges across peers the moment one sin differs by an ulp |
crates.io · docs.rs · GitHub |
| ALICE-LOL | Its SDF law checks run through ALICE-SDF, which evaluates through this crate; from its next release, the research_law module also evaluates multi-variable research laws (exp, ln, sqrt, powers, sin, cos) through the f64 functions here, so a law re-computed on another machine gives the same bits |
crates.io · docs.rs · GitHub |
ALICE-Zip does not depend on this
crate: its generators currently use libm.
License
Apache-2.0 (LICENSE-APACHE, with NOTICE). Releases up to and including
0.4.0 were published as MIT OR Apache-2.0. Coefficients and algorithms are
from Cephes (Stephen L. Moshier), fdlibm (Sun Microsystems) and musl; see the
source for attribution. A redistribution, in source or binary form, must carry
LICENSE-APACHE and NOTICE; they need not appear in a user interface. The
ALICE name is covered by TRADEMARK_NOTICE, not by the licence.