use crate::common::{Mergeable, Sketch, SketchError};
use std::collections::HashMap;
#[derive(Debug, Clone)]
struct Store {
bins: HashMap<i32, u64>, count: u64, min: f64, max: f64, }
impl Store {
fn new() -> Self {
Self {
bins: HashMap::new(),
count: 0,
min: f64::INFINITY,
max: f64::NEG_INFINITY,
}
}
fn add(&mut self, index: i32) {
*self.bins.entry(index).or_insert(0) += 1;
self.count += 1;
}
fn merge(&mut self, other: &Store) {
for (&index, &count) in &other.bins {
*self.bins.entry(index).or_insert(0) += count;
}
self.count += other.count;
self.min = self.min.min(other.min);
self.max = self.max.max(other.max);
}
}
#[derive(Debug, Clone)]
pub struct DDSketch {
alpha: f64, gamma: f64, gamma_ln: f64, offset: f64,
store_positive: Store, store_negative: Store, zero_count: u64, }
impl DDSketch {
pub fn new(relative_accuracy: f64) -> Result<Self, SketchError> {
if relative_accuracy <= 0.0 || relative_accuracy >= 1.0 {
return Err(SketchError::InvalidParameter {
param: "relative_accuracy".to_string(),
value: relative_accuracy.to_string(),
constraint: "must be in (0, 1)".to_string(),
});
}
let gamma = (1.0 + relative_accuracy) / (1.0 - relative_accuracy);
let gamma_ln = gamma.ln();
let offset = 0.0;
Ok(Self {
alpha: relative_accuracy,
gamma,
gamma_ln,
offset,
store_positive: Store::new(),
store_negative: Store::new(),
zero_count: 0,
})
}
pub fn add(&mut self, value: f64) {
if value > 0.0 {
let index = self.key(value);
self.store_positive.add(index);
self.store_positive.min = self.store_positive.min.min(value);
self.store_positive.max = self.store_positive.max.max(value);
} else if value < 0.0 {
let index = self.key(-value);
self.store_negative.add(index);
self.store_negative.min = self.store_negative.min.min(-value);
self.store_negative.max = self.store_negative.max.max(-value);
} else {
self.zero_count += 1;
}
}
fn key(&self, value: f64) -> i32 {
((value.ln() / self.gamma_ln + self.offset).ceil()) as i32
}
fn value(&self, index: i32) -> f64 {
let exponent = (index as f64) - self.offset;
let gamma_k = self.gamma.powf(exponent);
2.0 * gamma_k / (self.gamma + 1.0)
}
pub fn count(&self) -> u64 {
self.store_positive.count + self.zero_count + self.store_negative.count
}
pub fn quantile(&self, q: f64) -> Option<f64> {
if !(0.0..=1.0).contains(&q) {
return None;
}
let count = self.count();
if count == 0 {
return None;
}
let rank = if q == 0.0 {
1
} else {
(q * count as f64).ceil() as u64
};
let mut accumulated = 0u64;
if rank <= self.store_negative.count {
let mut indices: Vec<_> = self.store_negative.bins.keys().copied().collect();
indices.sort_by(|a, b| b.cmp(a));
for index in indices {
accumulated += self.store_negative.bins[&index];
if accumulated >= rank {
return Some(-self.value(index));
}
}
}
accumulated += self.store_negative.count;
if rank <= accumulated + self.zero_count {
return Some(0.0);
}
accumulated += self.zero_count;
let mut indices: Vec<_> = self.store_positive.bins.keys().copied().collect();
indices.sort();
for index in indices {
accumulated += self.store_positive.bins[&index];
if accumulated >= rank {
return Some(self.value(index));
}
}
None
}
pub fn min(&self) -> Option<f64> {
if self.count() == 0 {
return None;
}
let mut result = f64::INFINITY;
if self.store_negative.count > 0 {
result = result.min(-self.store_negative.max);
}
if self.zero_count > 0 {
result = result.min(0.0);
}
if self.store_positive.count > 0 {
result = result.min(self.store_positive.min);
}
Some(result)
}
pub fn max(&self) -> Option<f64> {
if self.count() == 0 {
return None;
}
let mut result = f64::NEG_INFINITY;
if self.store_positive.count > 0 {
result = result.max(self.store_positive.max);
}
if self.zero_count > 0 {
result = result.max(0.0);
}
if self.store_negative.count > 0 {
result = result.max(-self.store_negative.min);
}
Some(result)
}
pub fn alpha(&self) -> f64 {
self.alpha
}
}
impl Sketch for DDSketch {
type Item = f64;
fn update(&mut self, item: &Self::Item) {
self.add(*item);
}
fn estimate(&self) -> f64 {
self.count() as f64
}
fn is_empty(&self) -> bool {
self.count() == 0
}
fn serialize(&self) -> Vec<u8> {
let mut bytes = Vec::new();
bytes.extend_from_slice(&self.alpha.to_le_bytes());
bytes.extend_from_slice(&self.gamma.to_le_bytes());
bytes.extend_from_slice(&self.gamma_ln.to_le_bytes());
bytes.extend_from_slice(&self.offset.to_le_bytes());
bytes.extend_from_slice(&self.zero_count.to_le_bytes());
bytes.extend_from_slice(&self.store_positive.count.to_le_bytes());
bytes.extend_from_slice(&self.store_positive.min.to_le_bytes());
bytes.extend_from_slice(&self.store_positive.max.to_le_bytes());
bytes.extend_from_slice(&(self.store_positive.bins.len() as u64).to_le_bytes());
for (&index, &count) in &self.store_positive.bins {
bytes.extend_from_slice(&index.to_le_bytes());
bytes.extend_from_slice(&count.to_le_bytes());
}
bytes.extend_from_slice(&self.store_negative.count.to_le_bytes());
bytes.extend_from_slice(&self.store_negative.min.to_le_bytes());
bytes.extend_from_slice(&self.store_negative.max.to_le_bytes());
bytes.extend_from_slice(&(self.store_negative.bins.len() as u64).to_le_bytes());
for (&index, &count) in &self.store_negative.bins {
bytes.extend_from_slice(&index.to_le_bytes());
bytes.extend_from_slice(&count.to_le_bytes());
}
bytes
}
fn deserialize(bytes: &[u8]) -> Result<Self, SketchError> {
if bytes.len() < 104 {
return Err(SketchError::DeserializationError(
"Insufficient data for DDSketch header".to_string(),
));
}
let alpha = f64::from_le_bytes(bytes[0..8].try_into().unwrap());
let gamma = f64::from_le_bytes(bytes[8..16].try_into().unwrap());
let gamma_ln = f64::from_le_bytes(bytes[16..24].try_into().unwrap());
let offset = f64::from_le_bytes(bytes[24..32].try_into().unwrap());
let zero_count = u64::from_le_bytes(bytes[32..40].try_into().unwrap());
let mut pos = 40;
let pos_count = u64::from_le_bytes(bytes[pos..pos + 8].try_into().unwrap());
pos += 8;
let pos_min = f64::from_le_bytes(bytes[pos..pos + 8].try_into().unwrap());
pos += 8;
let pos_max = f64::from_le_bytes(bytes[pos..pos + 8].try_into().unwrap());
pos += 8;
let pos_bins_len = u64::from_le_bytes(bytes[pos..pos + 8].try_into().unwrap()) as usize;
pos += 8;
let mut pos_bins = HashMap::new();
for _ in 0..pos_bins_len {
let index = i32::from_le_bytes(bytes[pos..pos + 4].try_into().unwrap());
pos += 4;
let count = u64::from_le_bytes(bytes[pos..pos + 8].try_into().unwrap());
pos += 8;
pos_bins.insert(index, count);
}
let neg_count = u64::from_le_bytes(bytes[pos..pos + 8].try_into().unwrap());
pos += 8;
let neg_min = f64::from_le_bytes(bytes[pos..pos + 8].try_into().unwrap());
pos += 8;
let neg_max = f64::from_le_bytes(bytes[pos..pos + 8].try_into().unwrap());
pos += 8;
let neg_bins_len = u64::from_le_bytes(bytes[pos..pos + 8].try_into().unwrap()) as usize;
pos += 8;
let mut neg_bins = HashMap::new();
for _ in 0..neg_bins_len {
let index = i32::from_le_bytes(bytes[pos..pos + 4].try_into().unwrap());
pos += 4;
let count = u64::from_le_bytes(bytes[pos..pos + 8].try_into().unwrap());
pos += 8;
neg_bins.insert(index, count);
}
Ok(DDSketch {
alpha,
gamma,
gamma_ln,
offset,
store_positive: Store {
bins: pos_bins,
count: pos_count,
min: pos_min,
max: pos_max,
},
store_negative: Store {
bins: neg_bins,
count: neg_count,
min: neg_min,
max: neg_max,
},
zero_count,
})
}
}
impl Mergeable for DDSketch {
fn merge(&mut self, other: &Self) -> Result<(), SketchError> {
if (self.alpha - other.alpha).abs() > 1e-10 {
return Err(SketchError::IncompatibleSketches {
reason: format!(
"Cannot merge sketches with different alpha: {} vs {}",
self.alpha, other.alpha
),
});
}
self.store_positive.merge(&other.store_positive);
self.store_negative.merge(&other.store_negative);
self.zero_count += other.zero_count;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_key_value_inverse() {
let dd = DDSketch::new(0.01).unwrap();
for i in 1..=20 {
let original = 2.0_f64.powi(i);
let index = dd.key(original);
let recovered = dd.value(index);
let relative_error = (recovered - original).abs() / original;
assert!(
relative_error <= 0.02,
"key/value not inverse: {} -> {} -> {}, error: {}",
original,
index,
recovered,
relative_error
);
}
}
#[test]
fn test_gamma_calculation() {
let dd = DDSketch::new(0.01).unwrap();
let expected_gamma = 1.01 / 0.99; assert!((dd.gamma - expected_gamma).abs() < 1e-10);
}
}