use crate::common::{Mergeable, Sketch, SketchError};
use std::hash::{Hash, Hasher};
use twox_hash::XxHash64;
#[derive(Clone, Debug)]
pub struct CountSketch {
width: usize,
mask: usize,
depth: usize,
table: Vec<i64>,
epsilon: f64,
delta: f64,
}
impl CountSketch {
pub fn new(epsilon: f64, delta: f64) -> Result<Self, SketchError> {
if epsilon <= 0.0 || epsilon >= 1.0 {
return Err(SketchError::InvalidParameter {
param: "epsilon".to_string(),
value: epsilon.to_string(),
constraint: "must be in (0, 1)".to_string(),
});
}
if delta <= 0.0 || delta >= 1.0 {
return Err(SketchError::InvalidParameter {
param: "delta".to_string(),
value: delta.to_string(),
constraint: "must be in (0, 1)".to_string(),
});
}
let width_min = (3.0 / (epsilon * epsilon)).ceil() as usize;
let width = width_min.next_power_of_two();
let mask = width - 1;
let depth_computed = (1.0 / delta).ln().ceil() as usize;
let depth = depth_computed.max(3);
let table = vec![0i64; depth * width];
Ok(CountSketch {
width,
mask,
depth,
table,
epsilon,
delta,
})
}
#[inline]
pub fn update<T: Hash>(&mut self, item: &T, delta: i64) {
let mut pos_hasher = XxHash64::with_seed(0);
item.hash(&mut pos_hasher);
let mut sign_hasher = XxHash64::with_seed(0x9E3779B97F4A7C15);
item.hash(&mut sign_hasher);
let width = self.width;
let mask = self.mask;
let depth = self.depth;
for row_idx in 0..depth {
let pos_hash = pos_hasher.finish();
let col_idx = (pos_hash as usize) & mask;
let sign_hash = sign_hasher.finish();
let sign: i64 = if (sign_hash & 1) == 0 { 1 } else { -1 };
let idx = row_idx * width + col_idx;
unsafe {
*self.table.get_unchecked_mut(idx) += sign * delta;
}
pos_hasher.write(&[0x7B]);
sign_hasher.write(&[0x5A]);
}
}
#[inline]
pub fn estimate<T: Hash>(&self, item: &T) -> i64 {
let mut pos_hasher = XxHash64::with_seed(0);
item.hash(&mut pos_hasher);
let mut sign_hasher = XxHash64::with_seed(0x9E3779B97F4A7C15);
item.hash(&mut sign_hasher);
let width = self.width;
let mask = self.mask;
let depth = self.depth;
let mut estimates = Vec::with_capacity(depth);
for row_idx in 0..depth {
let pos_hash = pos_hasher.finish();
let col_idx = (pos_hash as usize) & mask;
let sign_hash = sign_hasher.finish();
let sign: i64 = if (sign_hash & 1) == 0 { 1 } else { -1 };
let idx = row_idx * width + col_idx;
let counter = unsafe { *self.table.get_unchecked(idx) };
estimates.push(sign * counter);
pos_hasher.write(&[0x7B]);
sign_hasher.write(&[0x5A]);
}
Self::median(&mut estimates)
}
pub fn inner_product(&self, other: &Self) -> i64 {
assert_eq!(self.width, other.width, "Width mismatch");
assert_eq!(self.depth, other.depth, "Depth mismatch");
let width = self.width;
let depth = self.depth;
let mut row_products = Vec::with_capacity(depth);
for row_idx in 0..depth {
let row_start = row_idx * width;
let mut row_sum: i64 = 0;
for col_idx in 0..width {
let idx = row_start + col_idx;
unsafe {
row_sum += self.table.get_unchecked(idx) * other.table.get_unchecked(idx);
}
}
row_products.push(row_sum);
}
Self::median(&mut row_products)
}
#[inline]
fn median(values: &mut [i64]) -> i64 {
let len = values.len();
if len == 0 {
return 0;
}
if len == 1 {
return values[0];
}
values.sort_unstable();
if len % 2 == 1 {
values[len / 2]
} else {
(values[len / 2 - 1] + values[len / 2]) / 2
}
}
#[inline]
pub fn width(&self) -> usize {
self.width
}
#[inline]
pub fn depth(&self) -> usize {
self.depth
}
#[inline]
pub fn epsilon(&self) -> f64 {
self.epsilon
}
#[inline]
pub fn delta(&self) -> f64 {
self.delta
}
}
impl Sketch for CountSketch {
type Item = i64;
fn update(&mut self, item: &Self::Item) {
CountSketch::update(self, item, 1);
}
fn estimate(&self) -> f64 {
0.0
}
fn is_empty(&self) -> bool {
self.table.iter().all(|&count| count == 0)
}
fn serialize(&self) -> Vec<u8> {
let mut bytes = Vec::new();
bytes.extend_from_slice(&self.width.to_le_bytes());
bytes.extend_from_slice(&self.depth.to_le_bytes());
bytes.extend_from_slice(&self.epsilon.to_le_bytes());
bytes.extend_from_slice(&self.delta.to_le_bytes());
for &count in &self.table {
bytes.extend_from_slice(&count.to_le_bytes());
}
bytes
}
fn deserialize(bytes: &[u8]) -> Result<Self, SketchError> {
if bytes.len() < 32 {
return Err(SketchError::DeserializationError(
"insufficient bytes for header".to_string(),
));
}
let mut offset = 0;
let width = usize::from_le_bytes(
bytes[offset..offset + 8]
.try_into()
.map_err(|_| SketchError::DeserializationError("invalid width".to_string()))?,
);
offset += 8;
let depth = usize::from_le_bytes(
bytes[offset..offset + 8]
.try_into()
.map_err(|_| SketchError::DeserializationError("invalid depth".to_string()))?,
);
offset += 8;
let epsilon = f64::from_le_bytes(
bytes[offset..offset + 8]
.try_into()
.map_err(|_| SketchError::DeserializationError("invalid epsilon".to_string()))?,
);
offset += 8;
let delta = f64::from_le_bytes(
bytes[offset..offset + 8]
.try_into()
.map_err(|_| SketchError::DeserializationError("invalid delta".to_string()))?,
);
offset += 8;
let mask = width - 1;
let expected_table_size = depth * width * 8;
if bytes.len() < offset + expected_table_size {
return Err(SketchError::DeserializationError(
"insufficient bytes for table".to_string(),
));
}
let mut table = Vec::with_capacity(depth * width);
for _ in 0..(depth * width) {
let count = i64::from_le_bytes(
bytes[offset..offset + 8]
.try_into()
.map_err(|_| SketchError::DeserializationError("invalid count".to_string()))?,
);
table.push(count);
offset += 8;
}
Ok(CountSketch {
width,
mask,
depth,
table,
epsilon,
delta,
})
}
}
impl Mergeable for CountSketch {
fn merge(&mut self, other: &Self) -> Result<(), SketchError> {
if self.width != other.width {
return Err(SketchError::IncompatibleSketches {
reason: format!(
"width mismatch: {} vs {} (different epsilon parameters)",
self.width, other.width
),
});
}
if self.depth != other.depth {
return Err(SketchError::IncompatibleSketches {
reason: format!(
"depth mismatch: {} vs {} (different delta parameters)",
self.depth, other.depth
),
});
}
for (a, &b) in self.table.iter_mut().zip(other.table.iter()) {
*a = a.saturating_add(b);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
#[test]
fn test_positive_counts() {
let mut cs = CountSketch::new(0.1, 0.01).unwrap();
cs.update(&"apple", 5);
cs.update(&"banana", 3);
cs.update(&"cherry", 1);
let apple_est = cs.estimate(&"apple");
let banana_est = cs.estimate(&"banana");
let cherry_est = cs.estimate(&"cherry");
assert!(
(apple_est - 5).abs() <= 2,
"apple estimate {} too far from 5",
apple_est
);
assert!(
(banana_est - 3).abs() <= 2,
"banana estimate {} too far from 3",
banana_est
);
assert!(
(cherry_est - 1).abs() <= 2,
"cherry estimate {} too far from 1",
cherry_est
);
}
#[test]
fn test_negative_counts() {
let mut cs = CountSketch::new(0.1, 0.01).unwrap();
cs.update(&"item", 10);
cs.update(&"item", -7);
let estimate = cs.estimate(&"item");
assert!(
(estimate - 3).abs() <= 2,
"estimate {} should be close to 3",
estimate
);
let mut cs2 = CountSketch::new(0.1, 0.01).unwrap();
cs2.update(&"negative", -5);
let neg_est = cs2.estimate(&"negative");
assert!(
(neg_est - (-5)).abs() <= 2,
"negative estimate {} should be close to -5",
neg_est
);
}
#[test]
fn test_unbiased_property() {
const NUM_TRIALS: usize = 100;
const TRUE_COUNT: i64 = 50;
let mut total_estimate: i64 = 0;
for seed in 0..NUM_TRIALS {
let mut cs = CountSketch::new(0.1, 0.01).unwrap();
let item = format!("item_{}", seed);
cs.update(&item, TRUE_COUNT);
total_estimate += cs.estimate(&item);
}
let avg_estimate = total_estimate as f64 / NUM_TRIALS as f64;
let relative_error = ((avg_estimate - TRUE_COUNT as f64) / TRUE_COUNT as f64).abs();
assert!(
relative_error < 0.1,
"Average estimate {} deviates too much from true count {} (relative error: {:.2}%)",
avg_estimate,
TRUE_COUNT,
relative_error * 100.0
);
}
#[test]
fn test_l2_error_bound() {
let epsilon = 0.1;
let mut cs = CountSketch::new(epsilon, 0.01).unwrap();
let items_and_counts: Vec<(&str, i64)> =
vec![("a", 100), ("b", 50), ("c", 30), ("d", 20), ("e", 10)];
let l2_norm_sq: i64 = items_and_counts
.iter()
.map(|(_, count)| count * count)
.sum();
let l2_norm = (l2_norm_sq as f64).sqrt();
for (item, count) in &items_and_counts {
cs.update(item, *count);
}
let mut errors_within_bound = 0;
let total_items = items_and_counts.len();
for (item, true_count) in &items_and_counts {
let estimate = cs.estimate(item);
let error = (estimate - true_count).abs() as f64;
let bound = epsilon * l2_norm;
if error <= bound * 2.0 {
errors_within_bound += 1;
}
}
assert!(
errors_within_bound >= total_items - 1,
"Only {} of {} items within error bound",
errors_within_bound,
total_items
);
}
#[test]
fn test_median_aggregation() {
assert_eq!(CountSketch::median(&mut [1, 2, 3]), 2);
assert_eq!(CountSketch::median(&mut [1, 2, 3, 4]), 2); assert_eq!(CountSketch::median(&mut [5, 1, 9, 3, 7]), 5);
assert_eq!(CountSketch::median(&mut [-10, 0, 10]), 0);
assert_eq!(CountSketch::median(&mut [-5, -3, -1]), -3);
assert_eq!(CountSketch::median(&mut []), 0);
assert_eq!(CountSketch::median(&mut [42]), 42);
}
#[test]
fn test_vs_count_min_on_zipf() {
use crate::frequency::CountMinSketch;
let mut items: Vec<(String, i64)> = Vec::new();
items.push(("heavy1".to_string(), 1000));
items.push(("heavy2".to_string(), 500));
items.push(("heavy3".to_string(), 250));
for i in 0..100 {
items.push((format!("light_{}", i), 1));
}
let mut cs = CountSketch::new(0.1, 0.01).unwrap();
let mut cms = CountMinSketch::new(0.1, 0.01).unwrap();
for (item, count) in &items {
cs.update(item, *count);
for _ in 0..*count {
cms.update(item);
}
}
let cs_heavy1 = cs.estimate(&"heavy1");
let cms_heavy1 = cms.estimate(&"heavy1") as i64;
assert!(
(cs_heavy1 - 1000).abs() <= 200,
"Count Sketch estimate {} too far from 1000",
cs_heavy1
);
assert!(
cms_heavy1 >= 1000,
"Count-Min should not underestimate: {}",
cms_heavy1
);
let mut cs_light_errors: Vec<i64> = Vec::new();
let mut cms_light_errors: Vec<i64> = Vec::new();
for i in 0..10 {
let item = format!("light_{}", i);
let cs_est = cs.estimate(&item);
let cms_est = cms.estimate(&item) as i64;
cs_light_errors.push((cs_est - 1).abs());
cms_light_errors.push((cms_est - 1).abs());
}
let cs_avg_error: f64 = cs_light_errors.iter().sum::<i64>() as f64 / 10.0;
let cms_avg_error: f64 = cms_light_errors.iter().sum::<i64>() as f64 / 10.0;
assert!(
cs_avg_error <= cms_avg_error * 3.0 + 5.0,
"CS avg error {} much worse than CMS avg error {}",
cs_avg_error,
cms_avg_error
);
}
#[test]
fn test_inner_product() {
let mut cs1 = CountSketch::new(0.1, 0.01).unwrap();
let mut cs2 = CountSketch::new(0.1, 0.01).unwrap();
cs1.update(&"a", 3);
cs1.update(&"b", 2);
cs2.update(&"a", 4);
cs2.update(&"b", 5);
let inner = cs1.inner_product(&cs2);
assert!(
(inner - 22).abs() <= 10,
"Inner product {} should be close to 22",
inner
);
}
#[test]
fn test_inner_product_orthogonal() {
let mut cs1 = CountSketch::new(0.1, 0.01).unwrap();
let mut cs2 = CountSketch::new(0.1, 0.01).unwrap();
cs1.update(&"only_in_1", 100);
cs2.update(&"only_in_2", 100);
let inner = cs1.inner_product(&cs2);
assert!(
inner.abs() <= 50,
"Inner product of orthogonal vectors {} should be close to 0",
inner
);
}
#[test]
fn test_merge_additive() {
let mut cs1 = CountSketch::new(0.1, 0.01).unwrap();
let mut cs2 = CountSketch::new(0.1, 0.01).unwrap();
cs1.update(&"shared_item", 30);
cs1.update(&"only_in_1", 10);
cs2.update(&"shared_item", 20);
cs2.update(&"only_in_2", 15);
cs1.merge(&cs2).unwrap();
let shared_est = cs1.estimate(&"shared_item");
let only1_est = cs1.estimate(&"only_in_1");
let only2_est = cs1.estimate(&"only_in_2");
assert!(
(shared_est - 50).abs() <= 10,
"Merged estimate {} should be close to 50",
shared_est
);
assert!(
(only1_est - 10).abs() <= 5,
"only_in_1 estimate {} should be close to 10",
only1_est
);
assert!(
(only2_est - 15).abs() <= 5,
"only_in_2 estimate {} should be close to 15",
only2_est
);
}
#[test]
fn test_merge_incompatible() {
let mut cs1 = CountSketch::new(0.1, 0.01).unwrap();
let cs2 = CountSketch::new(0.01, 0.01).unwrap();
let result = cs1.merge(&cs2);
assert!(
result.is_err(),
"Merge should fail for incompatible sketches"
);
}
#[test]
fn test_basic_construction() {
let cs = CountSketch::new(0.1, 0.01).unwrap();
assert!(cs.width() > 0);
assert!(cs.depth() >= 3); assert!(cs.width().is_power_of_two());
}
#[test]
fn test_dimension_calculation() {
let cs = CountSketch::new(0.1, 0.01).unwrap();
assert_eq!(cs.width(), 512);
assert_eq!(cs.depth(), 5);
let cs2 = CountSketch::new(0.1, 0.5).unwrap();
assert!(cs2.depth() >= 3);
}
#[test]
fn test_invalid_parameters() {
assert!(CountSketch::new(0.0, 0.01).is_err());
assert!(CountSketch::new(1.0, 0.01).is_err());
assert!(CountSketch::new(-0.1, 0.01).is_err());
assert!(CountSketch::new(0.1, 0.0).is_err());
assert!(CountSketch::new(0.1, 1.0).is_err());
assert!(CountSketch::new(0.1, -0.1).is_err());
}
#[test]
fn test_serialization_roundtrip() {
let mut cs = CountSketch::new(0.1, 0.01).unwrap();
cs.update(&"test_item", 42);
cs.update(&"another", -10);
let bytes = cs.serialize();
let cs_restored = CountSketch::deserialize(&bytes).unwrap();
assert_eq!(cs.width(), cs_restored.width());
assert_eq!(cs.depth(), cs_restored.depth());
assert_eq!(
cs.estimate(&"test_item"),
cs_restored.estimate(&"test_item")
);
assert_eq!(cs.estimate(&"another"), cs_restored.estimate(&"another"));
}
#[test]
fn test_is_empty() {
let cs = CountSketch::new(0.1, 0.01).unwrap();
assert!(cs.is_empty());
let mut cs2 = CountSketch::new(0.1, 0.01).unwrap();
cs2.update(&"item", 1);
assert!(!cs2.is_empty());
}
#[test]
fn test_zero_delta_update() {
let mut cs = CountSketch::new(0.1, 0.01).unwrap();
cs.update(&"item", 10);
cs.update(&"item", 0);
let estimate = cs.estimate(&"item");
assert!(
(estimate - 10).abs() <= 2,
"estimate {} should be close to 10",
estimate
);
}
#[test]
fn test_many_items() {
let mut cs = CountSketch::new(0.05, 0.01).unwrap();
let mut true_counts: HashMap<String, i64> = HashMap::new();
for i in 0..1000 {
let item = format!("item_{}", i);
let count = (i % 10 + 1) as i64;
cs.update(&item, count);
true_counts.insert(item, count);
}
let mut within_bound = 0;
for i in (0..1000).step_by(100) {
let item = format!("item_{}", i);
let true_count = true_counts[&item];
let estimate = cs.estimate(&item);
if (estimate - true_count).abs() <= 10 {
within_bound += 1;
}
}
assert!(
within_bound >= 6,
"Only {} of 10 sampled items within error bound",
within_bound
);
}
}