#[must_use]
pub fn fsum(values: impl IntoIterator<Item = f64>) -> f64 {
let mut acc = Partials::default();
for value in values {
acc.add(value);
}
acc.finish()
}
#[derive(Default)]
struct Partials {
partials: Vec<f64>,
special_sum: f64,
inf_sum: f64,
overflow: bool,
}
impl Partials {
fn add(&mut self, value: f64) {
let x = self.absorb(value);
if x == 0.0 {
return;
}
if x.is_finite() {
self.partials.push(x);
return;
}
if value.is_finite() {
self.overflow = true;
} else {
if value.is_infinite() {
self.inf_sum += value;
}
self.special_sum += value;
}
self.partials.clear();
}
fn absorb(&mut self, value: f64) -> f64 {
let mut x = value;
let mut kept = 0;
for j in 0..self.partials.len() {
let mut y = self.partials[j];
if x.abs() < y.abs() {
std::mem::swap(&mut x, &mut y);
}
let hi = x + y;
let yr = hi - x;
let lo = y - yr;
if lo != 0.0 {
self.partials[kept] = lo;
kept += 1;
}
x = hi;
}
self.partials.truncate(kept);
x
}
fn finish(&self) -> f64 {
if self.special_sum != 0.0 {
return if self.inf_sum.is_nan() {
f64::NAN
} else {
self.special_sum
};
}
if self.overflow {
return f64::NAN;
}
round_partials(&self.partials)
}
}
fn round_partials(partials: &[f64]) -> f64 {
let Some(mut n) = partials.len().checked_sub(1) else {
return 0.0;
};
let mut hi = partials[n];
let mut lo = 0.0_f64;
while n > 0 {
let x = hi;
n -= 1;
let y = partials[n];
hi = x + y;
let yr = hi - x;
lo = y - yr;
if lo != 0.0 {
break;
}
}
if n > 0 && same_sign_nonzero(lo, partials[n - 1]) {
let y = lo * 2.0;
let x = hi + y;
let yr = x - hi;
if y == yr {
hi = x;
}
}
hi
}
fn same_sign_nonzero(a: f64, b: f64) -> bool {
(a < 0.0 && b < 0.0) || (a > 0.0 && b > 0.0)
}
#[cfg(test)]
mod tests {
use super::fsum;
fn frozen() -> Vec<(&'static str, Vec<f64>, u64)> {
vec![
("tenth_x10", vec![0.1; 10], 0x3ff0_0000_0000_0000),
(
"cancel",
vec![1e100, 1.0, -1e100, 1e-100, 1e50, -1.0, -1e50],
0x2b2b_ff2e_e48e_0530,
),
("halfway_up", vec![1e-16, 1.0, 1e16], 0x4341_c379_37e0_8001),
(
"p53_a",
vec![2f64.powi(53), -0.5, -2f64.powi(-54)],
0x433f_ffff_ffff_ffff,
),
(
"p53_b",
vec![2f64.powi(53), 1.0, 2f64.powi(-100)],
0x4340_0000_0000_0001,
),
(
"p53_c",
vec![2f64.powi(53) + 10.0, 1.0, 2f64.powi(-100)],
0x4340_0000_0000_0006,
),
(
"p53_d",
vec![2f64.powi(53) - 4.0, 0.5, 2f64.powi(-54)],
0x433f_ffff_ffff_fffd,
),
("empty", vec![], 0),
("neg_zero", vec![-0.0], 0),
(
"thirds",
vec![
1.0 / 3.0,
1.0 / 3.0,
1.0 / 3.0,
2.0 / 3.0,
2.0 / 3.0,
2.0 / 3.0,
],
0x4008_0000_0000_0000,
),
]
}
#[test]
fn exact_sum_matches_frozen_math_fsum() {
for (name, terms, want) in frozen() {
let got = fsum(terms.iter().copied()).to_bits();
assert_eq!(
got,
want,
"fsum({name}) = {:e} ({got:#018x}), math.fsum = {:e} ({want:#018x})",
f64::from_bits(got),
f64::from_bits(want)
);
}
}
#[test]
fn exact_sum_differs_from_a_sequential_sum_where_it_must() {
let seq: f64 = [0.1_f64; 10].iter().fold(0.0, |a, b| a + b);
assert_ne!(seq.to_bits(), fsum([0.1; 10]).to_bits());
assert_eq!(fsum([0.1; 10]).to_bits(), 1.0_f64.to_bits());
let terms = [1e100, 1.0, -1e100, 1e-100, 1e50, -1.0, -1e50];
let mut reversed = terms;
reversed.reverse();
assert_eq!(fsum(terms).to_bits(), fsum(reversed).to_bits());
assert_eq!(fsum(terms), 1e-100);
}
#[test]
fn exact_sum_non_finite_follows_math_fsum() {
assert_eq!(fsum([1.0, f64::INFINITY]), f64::INFINITY);
assert_eq!(fsum([f64::NEG_INFINITY, 2.0]), f64::NEG_INFINITY);
assert!(fsum([f64::INFINITY, f64::NEG_INFINITY]).is_nan());
assert!(fsum([1.0, f64::NAN]).is_nan());
assert!(fsum([f64::MAX, f64::MAX]).is_nan());
}
}