Skip to main content

mathverse_core/
float.rs

1//! Floating-point utilities beyond the standard library surface:
2//! accurate `expm1`/`log1p`, exact summation, `isclose`, and the classic
3//! `frexp`/`ldexp`/`modf`/`nextafter` decomposition functions.
4//!
5//! `expm1`/`log1p` use hardware-accelerated `std` methods on standard builds
6//! and fall back to `libm` under `no_std` (when the `libm` feature is on).
7
8/// `e^x - 1`, accurate for small `x` (no catastrophic cancellation).
9///
10/// ```
11/// use mathverse_core::float::expm1;
12/// assert!((expm1(1e-10) - 1e-10).abs() < 1e-20);
13/// assert!((expm1(1.0) - (core::f64::consts::E - 1.0)).abs() < 1e-15);
14/// ```
15#[cfg(feature = "std")]
16#[inline]
17pub fn expm1(x: f64) -> f64 {
18    x.exp_m1()
19}
20
21/// `e^x - 1`, accurate for small `x` (libm fallback for `no_std`).
22#[cfg(all(not(feature = "std"), feature = "libm"))]
23#[inline]
24pub fn expm1(x: f64) -> f64 {
25    libm::expm1(x)
26}
27
28/// `ln(1 + x)`, accurate for small `x`.
29///
30/// ```
31/// use mathverse_core::float::log1p;
32/// assert!((log1p(1e-12) - 1e-12).abs() < 1e-24);
33/// assert!((log1p(core::f64::consts::E - 1.0) - 1.0).abs() < 1e-15);
34/// ```
35#[cfg(feature = "std")]
36#[inline]
37pub fn log1p(x: f64) -> f64 {
38    x.ln_1p()
39}
40
41/// `ln(1 + x)`, accurate for small `x` (libm fallback for `no_std`).
42#[cfg(all(not(feature = "std"), feature = "libm"))]
43#[inline]
44pub fn log1p(x: f64) -> f64 {
45    libm::log1p(x)
46}
47
48/// Exactly rounded sum of a slice (Neumaier compensated summation).
49///
50/// Returns the correctly-rounded sum even when naive summation would lose
51/// precision to cancellation.
52///
53/// ```
54/// use mathverse_core::float::fsum;
55/// let xs = [1e16, 1.0, -1e16];
56/// assert_eq!(fsum(&xs), 1.0); // naive sum gives 0.0
57/// ```
58pub fn fsum(xs: &[f64]) -> f64 {
59    let mut sum = 0.0;
60    let mut correction = 0.0;
61    for &x in xs {
62        let t = sum + x;
63        if sum.abs() >= x.abs() {
64            correction += (sum - t) + x;
65        } else {
66            correction += (x - t) + sum;
67        }
68        sum = t;
69    }
70    sum + correction
71}
72
73/// Approximate equality with relative and absolute tolerances.
74///
75/// `|a - b| <= abs_tol + rel_tol * max(|a|, |b|)`; NaNs never compare close.
76///
77/// ```
78/// use mathverse_core::float::isclose;
79/// assert!(isclose(0.1 + 0.2, 0.3, 1e-9, 1e-12));
80/// assert!(!isclose(1.0, 2.0, 1e-9, 1e-12));
81/// ```
82#[inline]
83pub fn isclose(a: f64, b: f64, rel_tol: f64, abs_tol: f64) -> bool {
84    (a - b).abs() <= abs_tol + rel_tol * b.abs().max(a.abs())
85}
86
87/// Split `x` into a mantissa `m` in `[0.5, 1)` (sign preserved) and an
88/// integer exponent `e` such that `x == m * 2^e`.
89///
90/// Zero, infinities and NaN return `(x, 0)`.
91///
92/// ```
93/// use mathverse_core::float::frexp;
94/// let (m, e) = frexp(6.0);
95/// assert_eq!(m, 0.75);
96/// assert_eq!(e, 3);
97/// ```
98pub fn frexp(x: f64) -> (f64, i32) {
99    if x == 0.0 || x.is_nan() || x.is_infinite() {
100        return (x, 0);
101    }
102    let bits = x.to_bits();
103    let exponent = ((bits >> 52) & 0x7FF) as i32;
104    if exponent == 0 {
105        // Subnormal: normalize by scaling up, then adjust the exponent.
106        let (m, e) = frexp(x * 2_f64.powi(52));
107        return (m, e - 52);
108    }
109    let mantissa = (bits & !(0x7FFu64 << 52)) | (1022u64 << 52);
110    (f64::from_bits(mantissa), exponent - 1022)
111}
112
113/// Construct `x * 2^e`, correctly handling subnormals and overflow.
114///
115/// ```
116/// use mathverse_core::float::{frexp, ldexp};
117/// let x = 123.456;
118/// let (m, e) = frexp(x);
119/// assert!((ldexp(m, e) - x).abs() < 1e-12);
120/// assert!(ldexp(1.0, 2000).is_infinite());
121/// ```
122pub fn ldexp(x: f64, e: i32) -> f64 {
123    if x == 0.0 || x.is_nan() || x.is_infinite() {
124        return x;
125    }
126    let mut bits = x.to_bits();
127    let exponent = ((bits >> 52) & 0x7FF) as i32;
128    if exponent == 0 {
129        // Subnormal input: normalize first, then recurse with adjusted exponent.
130        return ldexp(x * 2_f64.powi(52), e - 52);
131    }
132    let new_exp = exponent + e;
133    if new_exp <= 0 {
134        // Underflow to subnormal: multiply by 2^new_exp through repeated
135        // halving (at most 1074 iterations).
136        let mut r = x;
137        let mut k = e;
138        while k < 0 {
139            r *= 0.5;
140            k += 1;
141        }
142        return r;
143    }
144    if new_exp >= 0x7FF {
145        return if bits >> 63 == 1 { f64::NEG_INFINITY } else { f64::INFINITY };
146    }
147    bits = (bits & !(0x7FFu64 << 52)) | ((new_exp as u64) << 52);
148    f64::from_bits(bits)
149}
150
151/// Split `x` into fractional and integral parts, both with `x`'s sign.
152///
153/// Returns `(fractional, integral)`.
154///
155/// ```
156/// use mathverse_core::float::modf;
157/// let (frac, int) = modf(2.7);
158/// assert!((frac - 0.7).abs() < 1e-12);
159/// assert_eq!(int, 2.0);
160/// let (frac, int) = modf(-2.7);
161/// assert!((frac + 0.7).abs() < 1e-12);
162/// assert_eq!(int, -2.0);
163/// ```
164pub fn modf(x: f64) -> (f64, f64) {
165    if x.is_nan() || x.is_infinite() {
166        return (x, 0.0);
167    }
168    let integral = x.trunc();
169    (x - integral, integral)
170}
171
172/// Next representable `f64` stepping from `x` toward `y`.
173///
174/// Returns `y` when `x == y`; `+0.0`/`-0.0` are handled per IEEE-754.
175///
176/// ```
177/// use mathverse_core::float::nextafter;
178/// assert!(nextafter(1.0, 2.0) > 1.0);
179/// assert!(nextafter(1.0, 0.0) < 1.0);
180/// assert_eq!(nextafter(0.0, 1.0), f64::from_bits(1));
181/// assert_eq!(nextafter(1.0, 1.0), 1.0);
182/// ```
183pub fn nextafter(x: f64, y: f64) -> f64 {
184    if x.is_nan() || y.is_nan() {
185        return f64::NAN;
186    }
187    if x == y {
188        return y;
189    }
190    if x == 0.0 {
191        let mut r = f64::from_bits(1);
192        if y < 0.0 {
193            r = -r;
194        }
195        return r;
196    }
197    let mut bits = x.to_bits();
198    if (x < y) == (x > 0.0) {
199        bits += 1;
200    } else {
201        bits -= 1;
202    }
203    f64::from_bits(bits)
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209
210    #[test]
211    fn expm1_log1p_small() {
212        assert!((expm1(1e-10) - 1e-10).abs() < 1e-20);
213        assert!((log1p(1e-10) - 1e-10).abs() < 1e-20);
214        assert!((expm1(0.0) - 0.0).abs() < 1e-20);
215        assert!((log1p(0.0) - 0.0).abs() < 1e-20);
216    }
217
218    #[test]
219    fn fsum_exactness() {
220        assert_eq!(fsum(&[1e16, 1.0, -1e16]), 1.0);
221        assert_eq!(fsum(&[0.1; 10]), 1.0);
222    }
223
224    #[test]
225    fn isclose_behavior() {
226        assert!(isclose(0.1 + 0.2, 0.3, 1e-9, 1e-12));
227        assert!(!isclose(1.0, 2.0, 1e-9, 1e-12));
228        assert!(!isclose(f64::NAN, f64::NAN, 1.0, 1.0));
229        // Subnormal numbers: use very small atol
230        assert!(isclose(1e-300, 1e-300, 1e-9, 1e-320));
231    }
232
233    #[test]
234    fn frexp_ldexp_roundtrip() {
235        for &x in &[0.5, 1.0, 2.0, 6.0, 123.456, 1e-300, -7.5] {
236            let (m, e) = frexp(x);
237            assert!(m.abs() >= 0.5 && m.abs() < 1.0 || x == 0.0);
238            assert!((ldexp(m, e) - x).abs() < x.abs() * 1e-12 || x == 0.0);
239        }
240        assert_eq!(frexp(0.0), (0.0, 0));
241        assert_eq!(frexp(0.75), (0.75, 0));
242        assert_eq!(frexp(6.0), (0.75, 3));
243        assert!(ldexp(1.0, 2000).is_infinite());
244        assert_eq!(ldexp(1.0, -2000), 0.0);
245    }
246
247    #[test]
248    fn modf_split() {
249        assert_eq!(modf(2.7).1, 2.0);
250        assert_eq!(modf(-2.7).1, -2.0);
251        assert_eq!(modf(0.0).1, 0.0);
252        assert!(modf(f64::INFINITY).1 == 0.0);
253    }
254
255    #[test]
256    fn nextafter_stepping() {
257        assert!(nextafter(1.0, 2.0) > 1.0);
258        assert!(nextafter(1.0, 0.0) < 1.0);
259        assert_eq!(nextafter(0.0, 1.0), f64::from_bits(1));
260        assert_eq!(nextafter(1.0, 1.0), 1.0);
261        assert!(nextafter(f64::MAX, f64::INFINITY).is_infinite());
262    }
263}