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}