#[derive(Debug, Clone, Copy, PartialEq)]
pub struct NumericAccumulator {
count: u64,
shift: f64,
shift_low: f64,
running_mean: f64,
m2: f64,
m2_scale: M2Scale,
sum: f64,
compensation: f64,
scaled: bool,
min: f64,
max: f64,
}
impl Default for NumericAccumulator {
fn default() -> Self {
Self::new()
}
}
const OVERFLOW_SCALE: f64 = 1.0 / 18_446_744_073_709_551_616.0;
const UNDERFLOW_SCALE: f64 = f64::from_bits((1023 + 1000) << 52);
const LOST_SPREAD_PER_VALUE: f64 = f64::from_bits(1 << 40);
const UNDERFLOW_HALF: f64 = f64::from_bits((1023 + 500) << 52);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum M2Scale {
Raised,
Unit,
Reduced,
}
impl M2Scale {
fn factor(self) -> f64 {
match self {
Self::Raised => UNDERFLOW_SCALE,
Self::Unit => 1.0,
Self::Reduced => OVERFLOW_SCALE,
}
}
fn root(self) -> f64 {
match self {
Self::Raised => UNDERFLOW_HALF,
Self::Unit => 1.0,
Self::Reduced => 1.0 / 4_294_967_296.0,
}
}
fn next(self) -> Option<Self> {
match self {
Self::Raised => Some(Self::Unit),
Self::Unit => Some(Self::Reduced),
Self::Reduced => None,
}
}
fn convert(self, value: f64, to: Self) -> f64 {
if self == to {
value
} else {
value / self.factor() * to.factor()
}
}
}
const EXACT_F64_INTEGER: u128 = 1 << 53;
#[inline]
fn two_sum(a: f64, b: f64) -> (f64, f64) {
let sum = a + b;
let b_virtual = sum - a;
let err = (a - (sum - b_virtual)) + (b - b_virtual);
(sum, err)
}
impl NumericAccumulator {
pub fn new() -> Self {
Self {
count: 0,
shift: 0.0,
shift_low: 0.0,
running_mean: 0.0,
m2: 0.0,
m2_scale: M2Scale::Raised,
sum: 0.0,
compensation: 0.0,
scaled: false,
min: f64::INFINITY,
max: f64::NEG_INFINITY,
}
}
#[inline]
pub fn update(&mut self, value: f64) {
debug_assert!(value.is_finite());
self.push(value, 0.0);
}
#[inline]
pub fn update_integer(&mut self, value: i64) {
self.update_wide_integer(i128::from(value));
}
#[inline]
pub fn update_unsigned(&mut self, value: u64) {
self.update_wide_integer(i128::from(value));
}
#[inline]
pub fn update_number(&mut self, number: ParsedNumber) {
match number {
ParsedNumber::Float(value) => self.update(value),
ParsedNumber::Integer(value) => self.update_wide_integer(value),
}
}
fn update_wide_integer(&mut self, value: i128) {
if value.unsigned_abs() <= EXACT_F64_INTEGER {
self.update(value as f64);
return;
}
let high = value as f64;
let low = (value - high as i128) as f64;
self.push(high, low);
}
#[inline]
fn push(&mut self, high: f64, low: f64) {
self.count += 1;
if self.count == 1 {
self.shift = high;
self.shift_low = low;
}
let shifted = (high - self.shift) - (self.shift_low - low);
let delta = shifted - self.running_mean;
self.running_mean += delta / self.count as f64;
let deviation = shifted - self.running_mean;
self.add_to_m2(0.0, M2Scale::Raised, |scale| match scale {
M2Scale::Raised => (delta * UNDERFLOW_HALF) * (deviation * UNDERFLOW_HALF),
M2Scale::Unit => delta * deviation,
M2Scale::Reduced => (delta * OVERFLOW_SCALE) * deviation,
});
self.add_to_sum(high, low, false);
self.min = self.min.min(high);
self.max = self.max.max(high);
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn from_lane_state(
count: u64,
shift: f64,
running_mean: f64,
m2: f64,
sum: f64,
compensation: f64,
min: f64,
max: f64,
) -> Self {
let raised = m2 * UNDERFLOW_SCALE;
let (m2, m2_scale) = if raised.is_finite() {
(raised, M2Scale::Raised)
} else {
(m2, M2Scale::Unit)
};
Self {
count,
shift,
shift_low: 0.0,
running_mean,
m2,
m2_scale,
sum,
compensation,
scaled: false,
min,
max,
}
}
#[inline]
fn add_to_sum(&mut self, sum: f64, compensation: f64, scaled: bool) {
if scaled && !self.scaled {
self.rescale_sum();
}
let unit = if self.scaled && !scaled {
OVERFLOW_SCALE
} else {
1.0
};
let (total, err) = two_sum(self.sum, sum * unit);
if total.is_finite() {
self.sum = total;
self.compensation += err + compensation * unit;
return;
}
self.rescale_sum();
let (total, err) = two_sum(self.sum, sum * OVERFLOW_SCALE);
self.sum = total;
self.compensation += err + compensation * OVERFLOW_SCALE;
}
#[inline]
fn add_to_m2(&mut self, m2: f64, scale: M2Scale, increment: impl Fn(M2Scale) -> f64) {
let mut target = self.m2_scale.max(scale);
loop {
let total = self.m2_scale.convert(self.m2, target)
+ (scale.convert(m2, target) + increment(target));
match target.next() {
Some(next) if !total.is_finite() => target = next,
_ => {
self.m2 = total;
self.m2_scale = target;
return;
}
}
}
}
fn m2_over(&self, denominator: f64) -> f64 {
self.m2 / denominator / self.m2_scale.factor()
}
fn std_dev_over(&self, denominator: f64) -> f64 {
let variance = self.m2_over(denominator);
if !variance.is_finite() {
return variance.sqrt();
}
if self.spread_lost() {
return f64::NAN;
}
(self.m2 / denominator).max(0.0).sqrt() / self.m2_scale.root()
}
fn reported_variance(&self, variance: f64) -> f64 {
if variance.is_nan() {
variance
} else if (variance <= 0.0 && self.m2 > 0.0) || self.spread_lost() {
f64::NAN
} else {
variance.max(0.0)
}
}
fn spread_lost(&self) -> bool {
self.min < self.max && self.m2 < self.count as f64 * LOST_SPREAD_PER_VALUE
}
fn rescale_sum(&mut self) {
debug_assert!(!self.scaled, "the running sum is rescaled at most once");
self.sum *= OVERFLOW_SCALE;
self.compensation *= OVERFLOW_SCALE;
self.scaled = true;
}
pub fn merge(&mut self, other: &Self) {
if other.count == 0 {
return;
}
if self.count == 0 {
*self = *other;
return;
}
let (a, b) = (self.count as f64, other.count as f64);
let combined = a + b;
let shift_gap = (other.shift - self.shift) - (self.shift_low - other.shift_low);
let delta = shift_gap + (other.running_mean - self.running_mean);
self.running_mean += delta * (b / combined);
let weight = a * b / combined;
self.add_to_m2(other.m2, other.m2_scale, |scale| match scale {
M2Scale::Raised => (delta * UNDERFLOW_HALF) * (delta * UNDERFLOW_HALF) * weight,
M2Scale::Unit => delta * delta * weight,
M2Scale::Reduced => (delta * OVERFLOW_SCALE) * delta * weight,
});
self.count += other.count;
self.add_to_sum(other.sum, other.compensation, other.scaled);
self.min = self.min.min(other.min);
self.max = self.max.max(other.max);
}
#[inline]
pub fn count(&self) -> u64 {
self.count
}
#[inline]
pub fn min(&self) -> Option<f64> {
(self.count > 0).then_some(self.min)
}
#[inline]
pub fn max(&self) -> Option<f64> {
(self.count > 0).then_some(self.max)
}
pub fn mean(&self) -> f64 {
if self.count == 0 {
return 0.0;
}
let count = self.count as f64;
if !self.scaled {
let total = self.sum + self.compensation;
if total.is_finite() {
return total / count;
}
}
let unit = if self.scaled { 1.0 } else { OVERFLOW_SCALE };
(self.sum * unit + self.compensation * unit) / count / OVERFLOW_SCALE
}
pub fn sample_variance(&self) -> f64 {
if self.count < 2 {
return 0.0;
}
self.reported_variance(self.m2_over((self.count - 1) as f64))
}
pub fn sample_std_dev(&self) -> f64 {
if self.count < 2 {
return 0.0;
}
self.std_dev_over((self.count - 1) as f64)
}
pub fn population_variance(&self) -> f64 {
if self.count < 2 {
return 0.0;
}
self.reported_variance(self.m2_over(self.count as f64))
}
pub fn population_std_dev(&self) -> f64 {
if self.count < 2 {
return 0.0;
}
self.std_dev_over(self.count as f64)
}
}
impl FromIterator<f64> for NumericAccumulator {
fn from_iter<I: IntoIterator<Item = f64>>(values: I) -> Self {
let mut accumulator = Self::new();
for value in values {
accumulator.update(value);
}
accumulator
}
}
impl FromIterator<ParsedNumber> for NumericAccumulator {
fn from_iter<I: IntoIterator<Item = ParsedNumber>>(values: I) -> Self {
let mut accumulator = Self::new();
for value in values {
accumulator.update_number(value);
}
accumulator
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub enum ParsedNumber {
Float(f64),
Integer(i128),
}
impl ParsedNumber {
pub fn parse(text: &str) -> Option<Self> {
let text = text.trim();
let value = text.parse::<f64>().ok().filter(|value| value.is_finite())?;
if value.abs() >= EXACT_F64_INTEGER as f64
&& let Ok(integer) = text.parse::<i128>()
&& (i128::from(i64::MIN)..=i128::from(u64::MAX)).contains(&integer)
{
return Some(Self::Integer(integer));
}
Some(Self::Float(value))
}
pub fn to_f64(self) -> f64 {
match self {
Self::Float(value) => value,
Self::Integer(value) => value as f64,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn accumulate(values: &[f64]) -> NumericAccumulator {
values.iter().copied().collect()
}
#[test]
fn empty_accumulator_reports_no_values() {
let accumulator = NumericAccumulator::new();
assert_eq!(accumulator.count(), 0);
assert_eq!(accumulator.min(), None);
assert_eq!(accumulator.max(), None);
assert_eq!(accumulator.mean(), 0.0);
assert_eq!(accumulator.sample_variance(), 0.0);
}
#[test]
fn reports_textbook_statistics() {
let accumulator = accumulate(&[2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0]);
assert_eq!(accumulator.count(), 8);
assert_eq!(accumulator.min(), Some(2.0));
assert_eq!(accumulator.max(), Some(9.0));
assert_eq!(accumulator.mean(), 5.0);
assert!((accumulator.sample_variance() - 32.0 / 7.0).abs() < 1e-12);
assert!((accumulator.population_variance() - 4.0).abs() < 1e-12);
}
#[test]
fn integers_within_2_53_accumulate_as_their_f64() {
let values = [
-(1_i64 << 53),
-7,
0,
1,
42,
1_000_000_007,
(1_i64 << 53) - 1,
1 << 53,
];
let mut integers = NumericAccumulator::new();
for value in values {
integers.update_integer(value);
}
let floats = accumulate(&values.map(|value| value as f64));
assert_eq!(integers, floats);
}
#[test]
fn integers_beyond_2_53_keep_their_spread_through_a_merge() {
let values: [i64; 4] = [
1_759_700_000_123_456_789,
1_759_700_000_123_456_791,
1_759_700_000_123_456_795,
1_759_700_000_123_456_800,
];
let (mut left, mut right, mut whole) = (
NumericAccumulator::new(),
NumericAccumulator::new(),
NumericAccumulator::new(),
);
for (index, value) in values.into_iter().enumerate() {
whole.update_integer(value);
if index < 2 { &mut left } else { &mut right }.update_integer(value);
}
left.merge(&right);
for accumulator in [whole, left] {
assert!((accumulator.sample_variance() - 283.0 / 12.0).abs() < 1e-12);
assert_eq!(
accumulator.max(),
Some(1_759_700_000_123_456_800_i64 as f64)
);
}
let mut unsigned = NumericAccumulator::new();
for value in [u64::MAX - 15, u64::MAX - 10, u64::MAX - 5, u64::MAX] {
unsigned.update_unsigned(value);
}
assert!((unsigned.sample_variance() - 125.0 / 3.0).abs() < 1e-12);
}
#[test]
fn parsed_numbers_keep_only_wide_64_bit_integer_literals_exact() {
assert_eq!(
ParsedNumber::parse(" +9007199254740993 "),
Some(ParsedNumber::Integer(9_007_199_254_740_993))
);
assert_eq!(
ParsedNumber::parse("18446744073709551615"),
Some(ParsedNumber::Integer(i128::from(u64::MAX)))
);
assert_eq!(
ParsedNumber::parse("-9223372036854775808"),
Some(ParsedNumber::Integer(i128::from(i64::MIN)))
);
assert_eq!(ParsedNumber::parse("42"), Some(ParsedNumber::Float(42.0)));
assert_eq!(ParsedNumber::parse("1e20"), Some(ParsedNumber::Float(1e20)));
assert_eq!(
ParsedNumber::parse("9007199254740993.0"),
Some(ParsedNumber::Float(9_007_199_254_740_992.0))
);
assert_eq!(
ParsedNumber::parse("18446744073709551616"),
Some(ParsedNumber::Float(18_446_744_073_709_551_616.0))
);
for text in ["", "N/A", "inf", "NaN", "1e400"] {
assert_eq!(ParsedNumber::parse(text), None, "{text:?}");
}
}
#[test]
fn variance_is_translation_invariant() {
for offset in [0.0, 1e6, 1e8, 1e9, 1e12] {
let accumulator = accumulate(&[offset, offset + 1.0, offset + 2.0, offset + 3.0]);
assert!(
(accumulator.sample_variance() - 5.0 / 3.0).abs() < 1e-9,
"offset {offset} reported variance {}",
accumulator.sample_variance()
);
}
}
#[test]
fn mean_survives_cancellation_in_any_order() {
for values in [
[1e16, 1.0, -1e16],
[1e16, -1e16, 1.0],
[-1e16, 1e16, 1.0],
[1.0, 1e16, -1e16],
] {
let accumulator = accumulate(&values);
assert!(
(accumulator.mean() - 1.0 / 3.0).abs() < 1e-12,
"{values:?} reported mean {}",
accumulator.mean()
);
}
}
#[test]
fn mean_stays_finite_when_the_sum_overflows() {
let accumulator = accumulate(&[1e308, 1e308]);
assert_eq!(accumulator.mean(), 1e308);
assert_eq!(accumulator.sample_variance(), 0.0);
}
#[test]
fn mean_survives_an_overflowing_sum_that_cancels() {
for values in [
[1e308, 1e308, -1e308, -1e308],
[-1e308, -1e308, 1e308, 1e308],
[1e308, 1e308, -1e308, -1e308].map(|value| value * 1.7),
] {
let accumulator = accumulate(&values);
assert_eq!(accumulator.mean(), 0.0, "{values:?}");
assert!(!accumulator.sample_variance().is_finite(), "{values:?}");
}
let accumulator = accumulate(&[1e308, 1e308, -1e308, -1e308, 3.0]);
assert_eq!(accumulator.mean(), 3.0 / 5.0);
}
#[test]
fn mean_survives_a_compensation_that_overflows_the_total() {
let small = 9e291;
let accumulator = accumulate(&[f64::MAX, small, small]);
assert_eq!(accumulator.sum, f64::MAX);
assert!(!accumulator.scaled);
let expected = f64::MAX / 3.0 + small * 2.0 / 3.0;
assert!(
(accumulator.mean() - expected).abs() <= expected * 1e-15,
"reported mean {}",
accumulator.mean()
);
}
#[test]
fn merge_rescales_a_sum_that_overflows_across_parts() {
let values = [1e308, 1e308, -1e308, -1e308, 3.0];
for split in 1..values.len() {
let (left, right) = values.split_at(split);
let mut merged = accumulate(left);
merged.merge(&accumulate(right));
assert_eq!(merged.mean(), 3.0 / 5.0, "split {split}");
let mut reversed = accumulate(right);
reversed.merge(&accumulate(left));
assert_eq!(reversed.mean(), 3.0 / 5.0, "reversed split {split}");
}
}
#[test]
fn overflowing_spread_is_not_reported_as_zero() {
let accumulator = accumulate(&[1e308, -1e308, 5.0, 0.5, 5.0, 7.0]);
assert!(!accumulator.sample_variance().is_finite());
assert!(!accumulator.sample_std_dev().is_finite());
assert!(!accumulator.population_variance().is_finite());
}
#[test]
fn variance_fits_where_its_sum_of_squares_does_not() {
let values: Vec<f64> = (0..104)
.map(|i| if (i / 4) % 2 == 0 { 1e154 } else { -1e154 })
.collect();
let exact = 1.009_708_737_864_077_7e308;
let single = accumulate(&values);
let mut merged = accumulate(&values[..37]);
merged.merge(&accumulate(&values[37..]));
let simd = crate::acceleration::simd::compute_stats_simd(&values);
for (path, accumulator) in [("single", single), ("merged", merged), ("simd", simd)] {
let variance = accumulator.sample_variance();
assert!(
(variance - exact).abs() <= exact * 1e-14,
"{path}: variance {variance}, exact {exact}"
);
assert!(accumulator.population_variance().is_finite(), "{path}");
}
let wider: Vec<f64> = values.iter().map(|value| value * 10.0).collect();
assert!(!accumulate(&wider).sample_variance().is_finite());
}
fn power_of_two(exponent: i32) -> f64 {
f64::from_bits(((1023 + exponent) as u64) << 52)
}
#[test]
fn a_tiny_spread_keeps_its_standard_deviation() {
let pattern = [1.0, 2.0, 4.0, 9.0];
let cycled: Vec<f64> = (0..104).map(|i| pattern[i % 4]).collect();
let blocked: Vec<f64> = (0..104).map(|i| pattern[(i / 4) % 4]).collect();
let paths = |values: &[f64]| {
let mut merged = accumulate(&values[..37]);
merged.merge(&accumulate(&values[37..]));
[
("single", accumulate(values)),
("merged", merged),
(
"simd",
crate::acceleration::simd::compute_stats_simd(values),
),
]
};
let mut wrong = Vec::new();
for (shape, unit) in [("cycled", &cycled), ("blocked", &blocked)] {
let expected: Vec<f64> = paths(unit)
.iter()
.map(|(_, accumulator)| accumulator.sample_std_dev())
.collect();
for exponent in [-530, -665, -997] {
let scale = power_of_two(exponent);
let values: Vec<f64> = unit.iter().map(|value| value * scale).collect();
for ((path, accumulator), unit_std_dev) in paths(&values).iter().zip(&expected) {
let std_dev = accumulator.sample_std_dev();
let scaled = unit_std_dev * scale;
let tolerance = if *path == "simd" { 1e-14 * scaled } else { 0.0 };
if (std_dev - scaled).abs() > tolerance {
wrong.push(format!("{shape} 2^{exponent} {path}: std_dev {std_dev:e}"));
}
let variance = accumulator.sample_variance();
let underflowed = exponent <= -665;
if variance.is_finite() == underflowed || variance == 0.0 {
wrong.push(format!(
"{shape} 2^{exponent} {path}: variance {variance:e}"
));
}
}
}
}
assert!(wrong.is_empty(), "{wrong:#?}");
}
#[test]
fn a_spread_lost_to_subnormal_rounding_is_not_reported() {
let pattern = [1.0, 2.0, 4.0, 9.0];
let mut wrong = Vec::new();
for exponent in [-1040, -1074] {
let unit = f64::from_bits(1 << (1074 + exponent));
let cycled: Vec<f64> = (0..104).map(|i| pattern[i % 4] * unit).collect();
let mut merged = accumulate(&cycled[..37]);
merged.merge(&accumulate(&cycled[37..]));
let simd = crate::acceleration::simd::compute_stats_simd(&cycled);
for (path, accumulator) in [
("single", accumulate(&cycled)),
("merged", merged),
("simd", simd),
] {
let (variance, std_dev) =
(accumulator.sample_variance(), accumulator.sample_std_dev());
if variance.is_finite() || std_dev.is_finite() {
wrong.push(format!(
"2^{exponent} {path}: variance {variance:e}, std_dev {std_dev:e}"
));
}
}
let constant = accumulate(&[unit; 8]);
assert_eq!(
(constant.sample_variance(), constant.sample_std_dev()),
(0.0, 0.0)
);
}
assert!(wrong.is_empty(), "{wrong:#?}");
}
#[test]
fn a_spread_that_fits_reports_the_same_bits_at_every_scale() {
let values = [0.1, 0.2, 0.30000000000000004, 1e3, 7.5, -2.25];
let accumulator = accumulate(&values);
let mean = values.iter().sum::<f64>() / values.len() as f64;
let squares: f64 = values.iter().map(|value| (value - mean).powi(2)).sum();
let variance = accumulator.sample_variance();
assert!((variance - squares / 5.0).abs() <= 1e-12 * variance);
assert_eq!(accumulator.sample_std_dev(), variance.sqrt());
}
fn exact_integer_variance(values: &[i64]) -> f64 {
let n = values.len() as i128;
let sum: i128 = values.iter().map(|&v| v as i128).sum();
let squares: i128 = values.iter().map(|&v| (v as i128) * (v as i128)).sum();
(n * squares - sum * sum) as f64 / (n * (n - 1)) as f64
}
#[test]
fn variance_on_a_large_offset_matches_the_exact_value() {
let spread: Vec<i64> = (0..20_000).map(|i: i64| (i * 7919) % 2001 - 1000).collect();
let expected = exact_integer_variance(&spread);
for offset in [1e9, 1e12, 1e14] {
let values: Vec<f64> = spread.iter().map(|&k| offset + k as f64).collect();
let single = accumulate(&values);
let mut merged = accumulate(&values[..7_001]);
merged.merge(&accumulate(&values[7_001..]));
for (path, accumulator) in [("single", single), ("merged", merged)] {
let variance = accumulator.sample_variance();
assert!(
(variance - expected).abs() <= expected * 1e-13,
"{path} at offset {offset}: variance {variance}, exact {expected}"
);
}
}
}
#[test]
fn an_overflowing_pair_is_not_reported_as_zero_variance() {
let big = 0.9 * f64::MAX;
for values in [[big, -big], [-big, big]] {
let accumulator = accumulate(&values);
assert!(!accumulator.sample_variance().is_finite(), "{values:?}");
assert!(!accumulator.population_variance().is_finite(), "{values:?}");
}
}
#[test]
fn merge_matches_a_single_pass() {
let values: Vec<f64> = (0..97).map(|i| 1e9 + (i % 7) as f64).collect();
let single = accumulate(&values);
for split in [1, 13, 48, 96] {
let mut merged = accumulate(&values[..split]);
merged.merge(&accumulate(&values[split..]));
assert_eq!(merged.count(), single.count());
assert_eq!(merged.min(), single.min());
assert_eq!(merged.max(), single.max());
assert!(
(merged.mean() - single.mean()).abs() <= single.mean().abs() * 1e-12,
"split {split} mean {} vs {}",
merged.mean(),
single.mean()
);
assert!(
(merged.sample_variance() - single.sample_variance()).abs()
<= single.sample_variance() * 1e-13,
"split {split} variance {} vs {}",
merged.sample_variance(),
single.sample_variance()
);
}
}
#[test]
fn merging_an_empty_accumulator_changes_nothing() {
let single = accumulate(&[1.0, 2.0, 3.0]);
let mut with_empty = single;
with_empty.merge(&NumericAccumulator::new());
assert_eq!(with_empty, single);
let mut from_empty = NumericAccumulator::new();
from_empty.merge(&single);
assert_eq!(from_empty, single);
}
}