use arrow::array::{
Array, Float32Array, Float64Array, Int8Array, Int16Array, Int32Array, Int64Array, UInt8Array,
UInt16Array, UInt32Array, UInt64Array,
};
use super::dataset_state::downcast;
use crate::{KernelKind, ProofFrameError, SumBounds};
#[derive(Debug, Clone, Copy)]
pub(super) enum Total {
Integer(i128),
Float(Neumaier),
}
impl Total {
pub(super) const fn integer() -> Self {
Self::Integer(0)
}
pub(super) const fn float() -> Self {
Self::Float(Neumaier::new())
}
pub(super) fn add_integer(&mut self, value: i128) -> Result<(), ProofFrameError> {
match self {
Self::Integer(total) => {
*total = total.checked_add(value).ok_or_else(|| {
ProofFrameError::CorruptData(
"Column total left the 128-bit integer range".into(),
)
})?;
Ok(())
}
Self::Float(total) => {
total.add(value as f64);
Ok(())
}
}
}
pub(super) fn add_float(&mut self, value: f64) {
match self {
Self::Integer(total) => *total = total.saturating_add(value as i128),
Self::Float(total) => total.add(value),
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub(super) struct Neumaier {
sum: f64,
compensation: f64,
}
impl Neumaier {
pub(super) const fn new() -> Self {
Self {
sum: 0.0,
compensation: 0.0,
}
}
pub(super) fn add(&mut self, value: f64) {
let total = self.sum + value;
self.compensation += if self.sum.abs() >= value.abs() {
(self.sum - total) + value
} else {
(value - total) + self.sum
};
self.sum = total;
}
pub(super) fn value(self) -> f64 {
self.sum + self.compensation
}
}
pub(super) fn describe(total: &Total) -> String {
match total {
Total::Integer(value) => value.to_string(),
Total::Float(value) => value.value().to_string(),
}
}
pub(super) fn accumulate(
total: &mut Total,
kernel: &KernelKind,
array: &dyn Array,
) -> Result<(), ProofFrameError> {
macro_rules! integers {
($variant:pat, $array:ty) => {
if matches!(kernel, $variant) {
let values = downcast::<$array>(array);
for row in 0..values.len() {
if !values.is_null(row) {
total.add_integer(i128::from(values.value(row)))?;
}
}
return Ok(());
}
};
}
integers!(KernelKind::I8, Int8Array);
integers!(KernelKind::I16, Int16Array);
integers!(KernelKind::I32, Int32Array);
integers!(KernelKind::I64, Int64Array);
integers!(KernelKind::U8, UInt8Array);
integers!(KernelKind::U16, UInt16Array);
integers!(KernelKind::U32, UInt32Array);
integers!(KernelKind::U64, UInt64Array);
match kernel {
KernelKind::F32 => {
let values = downcast::<Float32Array>(array);
for row in 0..values.len() {
if !values.is_null(row) {
total.add_float(f64::from(values.value(row)));
}
}
Ok(())
}
KernelKind::F64 => {
let values = downcast::<Float64Array>(array);
for row in 0..values.len() {
if !values.is_null(row) {
total.add_float(values.value(row));
}
}
Ok(())
}
_ => Err(ProofFrameError::UnsupportedType(format!(
"total for {}",
array.data_type()
))),
}
}
pub(super) fn outside(total: &Total, bounds: &SumBounds) -> Option<String> {
match (total, bounds) {
(&Total::Integer(value), &SumBounds::Integer { min, max }) => match (min, max) {
(Some(min), _) if value < min => Some(format!("below the minimum {min}")),
(_, Some(max)) if value > max => Some(format!("above the maximum {max}")),
_ => None,
},
(&Total::Float(value), &SumBounds::Float { min, max }) => {
let value = value.value();
match (min, max) {
(Some(min), _) if value < min => Some(format!("below the minimum {min}")),
(_, Some(max)) if value > max => Some(format!("above the maximum {max}")),
_ => None,
}
}
_ => Some("not comparable with its declared bounds".to_string()),
}
}
#[derive(Debug, Clone, Copy, Default)]
pub(super) struct Moments {
count: u64,
mean: f64,
sum_squares: f64,
}
impl Moments {
pub(super) const fn new() -> Self {
Self {
count: 0,
mean: 0.0,
sum_squares: 0.0,
}
}
pub(super) fn add(&mut self, value: f64) {
self.count += 1;
let delta = value - self.mean;
self.mean += delta / self.count as f64;
self.sum_squares += delta * (value - self.mean);
}
pub(super) const fn count(&self) -> u64 {
self.count
}
pub(super) const fn mean(&self) -> Option<f64> {
if self.count == 0 {
None
} else {
Some(self.mean)
}
}
pub(super) fn std_dev(&self) -> Option<f64> {
if self.count == 0 {
return None;
}
Some((self.sum_squares / self.count as f64).sqrt())
}
}
pub(super) fn observe(
moments: &mut Moments,
kernel: &KernelKind,
array: &dyn Array,
) -> Result<(), ProofFrameError> {
macro_rules! numbers {
($variant:pat, $array:ty) => {
if matches!(kernel, $variant) {
let values = downcast::<$array>(array);
for row in 0..values.len() {
if !values.is_null(row) {
moments.add(values.value(row) as f64);
}
}
return Ok(());
}
};
}
numbers!(KernelKind::I8, Int8Array);
numbers!(KernelKind::I16, Int16Array);
numbers!(KernelKind::I32, Int32Array);
numbers!(KernelKind::I64, Int64Array);
numbers!(KernelKind::U8, UInt8Array);
numbers!(KernelKind::U16, UInt16Array);
numbers!(KernelKind::U32, UInt32Array);
numbers!(KernelKind::U64, UInt64Array);
numbers!(KernelKind::F32, Float32Array);
numbers!(KernelKind::F64, Float64Array);
Err(ProofFrameError::UnsupportedType(format!(
"statistic for {}",
array.data_type()
)))
}
#[derive(Debug)]
pub(super) struct Frequencies {
counts: std::collections::HashMap<Vec<u8>, u64>,
total: u64,
budget: u64,
used: u64,
}
impl Frequencies {
pub(super) fn new(budget: u64) -> Self {
Self {
counts: std::collections::HashMap::new(),
total: 0,
budget,
used: 0,
}
}
pub(super) fn add(&mut self, key: &[u8]) -> Result<(), ProofFrameError> {
self.total += 1;
if let Some(count) = self.counts.get_mut(key) {
*count += 1;
return Ok(());
}
let cost = (key.len() + std::mem::size_of::<u64>() + 48) as u64;
self.used = self.used.saturating_add(cost);
if self.used > self.budget {
return Err(ProofFrameError::ResourceLimit {
resource: "dominant_value_map",
requested: cost,
used: self.used,
limit: self.budget,
});
}
self.counts.insert(key.to_vec(), 1);
Ok(())
}
pub(super) fn dominant(&self) -> Option<(f64, u64)> {
if self.total == 0 {
return None;
}
let top = self.counts.values().copied().max()?;
Some((top as f64 / self.total as f64, top))
}
}
pub(super) fn as_float(total: &Total) -> f64 {
match total {
Total::Integer(value) => *value as f64,
Total::Float(value) => value.value(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn compensation_keeps_small_values_that_plain_addition_drops() {
let mut plain = 1.0e16_f64;
let mut compensated = Neumaier::new();
compensated.add(1.0e16);
for _ in 0..1_000 {
plain += 1.0;
compensated.add(1.0);
}
assert_eq!(plain, 1.0e16, "plain addition loses every one of them");
assert_eq!(compensated.value(), 1.0e16 + 1_000.0);
}
#[test]
fn an_integer_total_past_i128_is_an_error_rather_than_a_wrapped_number() {
let mut total = Total::Integer(i128::MAX - 1);
assert!(total.add_integer(1).is_ok());
assert!(total.add_integer(1).is_err());
}
}