const F64_BIN_COUNT: usize = 2048;
#[derive(Clone)]
pub struct BinnedAccumulatorF64 {
bins: [f64; F64_BIN_COUNT],
comp: [f64; F64_BIN_COUNT],
counts: [u32; F64_BIN_COUNT],
pos_inf_count: u32,
neg_inf_count: u32,
has_nan: bool,
total_count: u64,
}
impl BinnedAccumulatorF64 {
#[inline]
pub fn new() -> Self {
BinnedAccumulatorF64 {
bins: [0.0; F64_BIN_COUNT],
comp: [0.0; F64_BIN_COUNT],
counts: [0; F64_BIN_COUNT],
pos_inf_count: 0,
neg_inf_count: 0,
has_nan: false,
total_count: 0,
}
}
#[inline]
pub fn add(&mut self, value: f64) {
self.total_count += 1;
if value.is_nan() {
self.has_nan = true;
return;
}
if value.is_infinite() {
if value > 0.0 {
self.pos_inf_count += 1;
} else {
self.neg_inf_count += 1;
}
return;
}
let bits = value.to_bits();
let biased_exp = ((bits >> 52) & 0x7FF) as usize;
self.bins[biased_exp] += value;
self.counts[biased_exp] += 1;
}
#[inline]
pub fn add_slice(&mut self, values: &[f64]) {
for &v in values {
self.add(v);
}
}
#[inline]
pub fn merge(&mut self, other: &BinnedAccumulatorF64) {
for i in 0..F64_BIN_COUNT {
let a = self.bins[i];
let b = other.bins[i];
let s = a + b;
let v = s - a;
let e = (a - (s - v)) + (b - v);
self.bins[i] = s;
self.comp[i] += e + other.comp[i];
self.counts[i] += other.counts[i];
}
self.pos_inf_count += other.pos_inf_count;
self.neg_inf_count += other.neg_inf_count;
self.has_nan = self.has_nan || other.has_nan;
self.total_count += other.total_count;
}
#[inline]
pub fn finalize(&self) -> f64 {
if self.has_nan {
return f64::NAN;
}
if self.pos_inf_count > 0 && self.neg_inf_count > 0 {
return f64::NAN; }
if self.pos_inf_count > 0 {
return f64::INFINITY;
}
if self.neg_inf_count > 0 {
return f64::NEG_INFINITY;
}
let mut sum = 0.0f64;
let mut kahan_comp = 0.0f64;
for i in 0..F64_BIN_COUNT {
if self.counts[i] == 0 {
continue;
}
let val = self.bins[i] + self.comp[i];
let y = val - kahan_comp;
let t = sum + y;
kahan_comp = (t - sum) - y;
sum = t;
}
sum
}
#[inline]
pub fn count(&self) -> u64 {
self.total_count
}
#[inline]
pub fn has_nan(&self) -> bool {
self.has_nan
}
}
impl Default for BinnedAccumulatorF64 {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for BinnedAccumulatorF64 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let active_bins = self.counts.iter().filter(|&&c| c > 0).count();
f.debug_struct("BinnedAccumulatorF64")
.field("total_count", &self.total_count)
.field("active_bins", &active_bins)
.field("has_nan", &self.has_nan)
.field("pos_inf_count", &self.pos_inf_count)
.field("neg_inf_count", &self.neg_inf_count)
.finish()
}
}
const F32_BIN_COUNT: usize = 256;
#[derive(Clone)]
pub struct BinnedAccumulatorF32 {
bins: [f64; F32_BIN_COUNT],
comp: [f64; F32_BIN_COUNT],
counts: [u32; F32_BIN_COUNT],
pos_inf_count: u32,
neg_inf_count: u32,
has_nan: bool,
total_count: u64,
}
impl BinnedAccumulatorF32 {
#[inline]
pub fn new() -> Self {
BinnedAccumulatorF32 {
bins: [0.0; F32_BIN_COUNT],
comp: [0.0; F32_BIN_COUNT],
counts: [0; F32_BIN_COUNT],
pos_inf_count: 0,
neg_inf_count: 0,
has_nan: false,
total_count: 0,
}
}
#[inline]
pub fn add(&mut self, value: f32) {
self.total_count += 1;
if value.is_nan() {
self.has_nan = true;
return;
}
if value.is_infinite() {
if value > 0.0 {
self.pos_inf_count += 1;
} else {
self.neg_inf_count += 1;
}
return;
}
let bits = value.to_bits();
let biased_exp = ((bits >> 23) & 0xFF) as usize;
self.bins[biased_exp] += value as f64;
self.counts[biased_exp] += 1;
}
#[inline]
pub fn add_slice(&mut self, values: &[f32]) {
for &v in values {
self.add(v);
}
}
#[inline]
pub fn merge(&mut self, other: &BinnedAccumulatorF32) {
for i in 0..F32_BIN_COUNT {
let a = self.bins[i];
let b = other.bins[i];
let s = a + b;
let v = s - a;
let e = (a - (s - v)) + (b - v);
self.bins[i] = s;
self.comp[i] += e + other.comp[i];
self.counts[i] += other.counts[i];
}
self.pos_inf_count += other.pos_inf_count;
self.neg_inf_count += other.neg_inf_count;
self.has_nan = self.has_nan || other.has_nan;
self.total_count += other.total_count;
}
#[inline]
pub fn finalize(&self) -> f32 {
if self.has_nan {
return f32::NAN;
}
if self.pos_inf_count > 0 && self.neg_inf_count > 0 {
return f32::NAN;
}
if self.pos_inf_count > 0 {
return f32::INFINITY;
}
if self.neg_inf_count > 0 {
return f32::NEG_INFINITY;
}
let mut sum = 0.0f64;
let mut kahan_comp = 0.0f64;
for i in 0..F32_BIN_COUNT {
if self.counts[i] == 0 {
continue;
}
let val = self.bins[i] + self.comp[i];
let y = val - kahan_comp;
let t = sum + y;
kahan_comp = (t - sum) - y;
sum = t;
}
sum as f32
}
#[inline]
pub fn count(&self) -> u64 {
self.total_count
}
}
impl Default for BinnedAccumulatorF32 {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for BinnedAccumulatorF32 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let active_bins = self.counts.iter().filter(|&&c| c > 0).count();
f.debug_struct("BinnedAccumulatorF32")
.field("total_count", &self.total_count)
.field("active_bins", &active_bins)
.field("has_nan", &self.has_nan)
.finish()
}
}
#[inline]
pub fn binned_sum_f64(values: &[f64]) -> f64 {
let mut acc = BinnedAccumulatorF64::new();
acc.add_slice(values);
acc.finalize()
}
#[inline]
pub fn binned_sum_f32(values: &[f32]) -> f32 {
let mut acc = BinnedAccumulatorF32::new();
acc.add_slice(values);
acc.finalize()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty_accumulator() {
let acc = BinnedAccumulatorF64::new();
assert_eq!(acc.finalize(), 0.0);
assert_eq!(acc.count(), 0);
}
#[test]
fn test_single_value() {
let mut acc = BinnedAccumulatorF64::new();
acc.add(42.0);
assert_eq!(acc.finalize(), 42.0);
}
#[test]
fn test_simple_sum() {
let mut acc = BinnedAccumulatorF64::new();
for i in 1..=10 {
acc.add(i as f64);
}
assert_eq!(acc.finalize(), 55.0);
}
#[test]
fn test_order_invariance() {
let values = vec![1e16, 1.0, -1e16, 0.5, -0.5, 1e-16, -1e-16];
let mut acc1 = BinnedAccumulatorF64::new();
acc1.add_slice(&values);
let rev: Vec<f64> = values.iter().rev().copied().collect();
let mut acc2 = BinnedAccumulatorF64::new();
acc2.add_slice(&rev);
assert_eq!(acc1.finalize().to_bits(), acc2.finalize().to_bits());
}
#[test]
fn test_merge_commutativity() {
let a = vec![1.0, 2.0, 3.0];
let b = vec![4.0, 5.0, 6.0];
let mut acc_a = BinnedAccumulatorF64::new();
acc_a.add_slice(&a);
let mut acc_b = BinnedAccumulatorF64::new();
acc_b.add_slice(&b);
let mut merged_ab = acc_a.clone();
merged_ab.merge(&acc_b);
let mut merged_ba = acc_b.clone();
merged_ba.merge(&acc_a);
assert_eq!(merged_ab.finalize().to_bits(), merged_ba.finalize().to_bits());
assert_eq!(merged_ab.finalize(), 21.0);
}
#[test]
fn test_nan_propagation() {
let mut acc = BinnedAccumulatorF64::new();
acc.add(1.0);
acc.add(f64::NAN);
acc.add(2.0);
assert!(acc.finalize().is_nan());
assert!(acc.has_nan());
}
#[test]
fn test_inf_handling() {
let mut acc = BinnedAccumulatorF64::new();
acc.add(f64::INFINITY);
assert_eq!(acc.finalize(), f64::INFINITY);
let mut acc2 = BinnedAccumulatorF64::new();
acc2.add(f64::NEG_INFINITY);
assert_eq!(acc2.finalize(), f64::NEG_INFINITY);
let mut acc3 = BinnedAccumulatorF64::new();
acc3.add(f64::INFINITY);
acc3.add(f64::NEG_INFINITY);
assert!(acc3.finalize().is_nan());
}
#[test]
fn test_subnormal_preservation() {
let subnormal = 5e-324_f64; let mut acc = BinnedAccumulatorF64::new();
acc.add(subnormal);
assert_eq!(acc.finalize(), subnormal);
}
#[test]
fn test_signed_zero() {
let mut acc = BinnedAccumulatorF64::new();
acc.add(0.0);
acc.add(-0.0);
let result = acc.finalize();
assert_eq!(result, 0.0);
}
#[test]
fn test_catastrophic_cancellation() {
let mut acc = BinnedAccumulatorF64::new();
acc.add(1e16);
acc.add(1.0);
acc.add(-1e16);
let result = acc.finalize();
assert_eq!(result, 1.0);
}
#[test]
fn test_many_small_values() {
let mut acc = BinnedAccumulatorF64::new();
for _ in 0..10000 {
acc.add(0.0001);
}
let result = acc.finalize();
assert!((result - 1.0).abs() < 1e-10);
}
#[test]
fn test_f32_accumulator() {
let mut acc = BinnedAccumulatorF32::new();
for i in 1..=100 {
acc.add(i as f32);
}
assert_eq!(acc.finalize(), 5050.0);
}
#[test]
fn test_f32_order_invariance() {
let values: Vec<f32> = (1..=1000).map(|i| i as f32 * 0.001).collect();
let reversed: Vec<f32> = values.iter().rev().copied().collect();
let r1 = binned_sum_f32(&values);
let r2 = binned_sum_f32(&reversed);
assert_eq!(r1.to_bits(), r2.to_bits());
}
#[test]
fn test_convenience_function() {
let values: Vec<f64> = (1..=100).map(|i| i as f64).collect();
assert_eq!(binned_sum_f64(&values), 5050.0);
}
#[test]
fn test_merge_associativity() {
let a = vec![1.0, 2.0];
let b = vec![3.0, 4.0];
let c = vec![5.0, 6.0];
let mut acc_a = BinnedAccumulatorF64::new();
acc_a.add_slice(&a);
let mut acc_b = BinnedAccumulatorF64::new();
acc_b.add_slice(&b);
let mut acc_c = BinnedAccumulatorF64::new();
acc_c.add_slice(&c);
let mut ab = acc_a.clone();
ab.merge(&acc_b);
let mut abc1 = ab;
abc1.merge(&acc_c);
let mut bc = acc_b.clone();
bc.merge(&acc_c);
let mut abc2 = acc_a.clone();
abc2.merge(&bc);
assert_eq!(abc1.finalize().to_bits(), abc2.finalize().to_bits());
assert_eq!(abc1.finalize(), 21.0);
}
#[test]
fn test_deterministic_across_chunk_sizes() {
let values: Vec<f64> = (0..1000).map(|i| (i as f64 * 0.7) - 350.0).collect();
let r1 = binned_sum_f64(&values);
let mut final_acc = BinnedAccumulatorF64::new();
for chunk in values.chunks(7) {
let mut chunk_acc = BinnedAccumulatorF64::new();
chunk_acc.add_slice(chunk);
final_acc.merge(&chunk_acc);
}
let r2 = final_acc.finalize();
let mut final_acc2 = BinnedAccumulatorF64::new();
for chunk in values.chunks(13) {
let mut chunk_acc = BinnedAccumulatorF64::new();
chunk_acc.add_slice(chunk);
final_acc2.merge(&chunk_acc);
}
let r3 = final_acc2.finalize();
let ulp_12 = (r1.to_bits() as i64 - r2.to_bits() as i64).unsigned_abs();
let ulp_13 = (r1.to_bits() as i64 - r3.to_bits() as i64).unsigned_abs();
let ulp_23 = (r2.to_bits() as i64 - r3.to_bits() as i64).unsigned_abs();
assert!(ulp_12 < 1000,
"Single vs chunk-7 differ by {ulp_12} ULPs: {r1} vs {r2}");
assert!(ulp_13 < 1000,
"Single vs chunk-13 differ by {ulp_13} ULPs: {r1} vs {r3}");
assert!(ulp_23 < 1000,
"Chunk-7 vs chunk-13 differ by {ulp_23} ULPs: {r2} vs {r3}");
}
}