use crate::common::{hash::hash_value, Mergeable, Sketch, SketchError};
use std::collections::HashMap;
#[derive(Clone, Debug)]
pub struct CpcSketch {
lg_k: u8,
k: u32,
num_coupons: u64,
flavor: Flavor,
surprising_values: HashMap<u32, u8>,
sliding_window: Vec<u8>,
window_offset: u8,
num_surprising_in_window: u32,
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum Flavor {
Empty,
Sparse,
Hybrid,
Pinned,
Sliding,
}
impl CpcSketch {
pub fn new(lg_k: u8) -> Result<Self, SketchError> {
if !(4..=26).contains(&lg_k) {
return Err(SketchError::InvalidParameter {
param: "lg_k".to_string(),
value: lg_k.to_string(),
constraint: "must be between 4 and 26".to_string(),
});
}
let k = 1u32 << lg_k;
Ok(CpcSketch {
lg_k,
k,
num_coupons: 0,
flavor: Flavor::Empty,
surprising_values: HashMap::new(),
sliding_window: Vec::new(),
window_offset: 0,
num_surprising_in_window: 0,
})
}
pub fn lg_k(&self) -> u8 {
self.lg_k
}
pub fn flavor(&self) -> &str {
match self.flavor {
Flavor::Empty => "Empty",
Flavor::Sparse => "Sparse",
Flavor::Hybrid => "Hybrid",
Flavor::Pinned => "Pinned",
Flavor::Sliding => "Sliding",
}
}
pub fn clear(&mut self) {
self.num_coupons = 0;
self.flavor = Flavor::Empty;
self.surprising_values.clear();
self.sliding_window.clear();
self.window_offset = 0;
self.num_surprising_in_window = 0;
}
#[inline]
fn process_hash(&self, hash: u32) -> (u32, u8) {
let slot = hash >> (32 - self.lg_k);
let remaining_bits = hash << self.lg_k;
let rho = if remaining_bits == 0 {
(32 - self.lg_k) + 1
} else {
remaining_bits.leading_zeros() as u8 + 1
};
(slot, rho)
}
fn update_sparse(&mut self, slot: u32, rho: u8) {
self.surprising_values
.entry(slot)
.and_modify(|existing| {
if rho > *existing {
*existing = rho;
}
})
.or_insert(rho);
self.num_coupons += 1;
let sparse_threshold = (3 * self.k) / 32;
if self.surprising_values.len() as u32 > sparse_threshold {
self.transition_to_hybrid();
}
}
fn update_dense(&mut self, slot: u32, rho: u8) {
self.surprising_values
.entry(slot)
.and_modify(|existing| {
if rho > *existing {
*existing = rho;
}
})
.or_insert(rho);
self.num_coupons += 1;
self.check_flavor_transition();
}
fn transition_to_sparse(&mut self) {
self.flavor = Flavor::Sparse;
}
fn transition_to_hybrid(&mut self) {
self.flavor = Flavor::Hybrid;
}
fn check_flavor_transition(&mut self) {
match self.flavor {
Flavor::Hybrid => {
if self.surprising_values.len() as u32 > self.k / 2 {
self.flavor = Flavor::Pinned;
}
}
Flavor::Pinned => {
if self.surprising_values.len() as u32 > (3 * self.k) / 4 {
self.flavor = Flavor::Sliding;
}
}
_ => {}
}
}
fn estimate_sparse(&self) -> f64 {
if self.surprising_values.is_empty() {
return 0.0;
}
let c = self.surprising_values.len() as f64;
let k = self.k as f64;
if c < k / 10.0 {
if c >= k {
c } else {
k * (k / (k - c)).ln()
}
} else {
self.estimate_dense()
}
}
fn estimate_dense(&self) -> f64 {
if self.surprising_values.is_empty() {
return 0.0;
}
let mut registers = vec![0u8; self.k as usize];
for (&slot, &rho) in &self.surprising_values {
registers[slot as usize] = rho;
}
let num_zeros = registers.iter().filter(|&&r| r == 0).count();
if num_zeros > self.k as usize / 2 {
let k = self.k as f64;
return k * (k / num_zeros as f64).ln();
}
let sum: f64 = registers
.iter()
.map(|&rho| {
if rho == 0 {
1.0 } else {
2.0_f64.powi(-(rho as i32))
}
})
.sum();
if sum == 0.0 {
return 0.0;
}
let m = self.k as f64;
let alpha = if self.k >= 128 {
0.7213 / (1.0 + 1.079 / m)
} else {
0.673 };
let raw_estimate = alpha * m * m / sum;
if num_zeros > 0 && raw_estimate < 2.5 * m {
m * (m / num_zeros as f64).ln()
} else {
raw_estimate
}
}
pub fn size_bytes(&self) -> usize {
let base_size = std::mem::size_of::<Self>();
let surprising_values_size = self.surprising_values.len()
* (std::mem::size_of::<u32>() + std::mem::size_of::<u8>() + 24); let window_size = self.sliding_window.len();
base_size + surprising_values_size + window_size
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut bytes = Vec::new();
bytes.push(self.lg_k);
bytes.push(match self.flavor {
Flavor::Empty => 0,
Flavor::Sparse => 1,
Flavor::Hybrid => 2,
Flavor::Pinned => 3,
Flavor::Sliding => 4,
});
bytes.extend_from_slice(&self.num_coupons.to_le_bytes());
bytes.push(self.window_offset);
bytes.extend_from_slice(&(self.surprising_values.len() as u32).to_le_bytes());
for (&slot, &rho) in &self.surprising_values {
bytes.extend_from_slice(&slot.to_le_bytes());
bytes.push(rho);
}
bytes.extend_from_slice(&(self.sliding_window.len() as u32).to_le_bytes());
bytes.extend_from_slice(&self.sliding_window);
bytes
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, SketchError> {
if bytes.len() < 14 {
return Err(SketchError::DeserializationError(
"Buffer too small for CPC sketch".to_string(),
));
}
let lg_k = bytes[0];
let flavor_byte = bytes[1];
let num_coupons = u64::from_le_bytes(bytes[2..10].try_into().unwrap());
let window_offset = bytes[10];
let flavor = match flavor_byte {
0 => Flavor::Empty,
1 => Flavor::Sparse,
2 => Flavor::Hybrid,
3 => Flavor::Pinned,
4 => Flavor::Sliding,
_ => {
return Err(SketchError::DeserializationError(format!(
"Invalid flavor byte: {}",
flavor_byte
)))
}
};
let mut pos = 11;
if bytes.len() < pos + 4 {
return Err(SketchError::DeserializationError(
"Buffer too small for surprising values count".to_string(),
));
}
let num_surprising = u32::from_le_bytes(bytes[pos..pos + 4].try_into().unwrap());
pos += 4;
let mut surprising_values = HashMap::new();
for _ in 0..num_surprising {
if bytes.len() < pos + 5 {
return Err(SketchError::DeserializationError(
"Buffer too small for surprising value".to_string(),
));
}
let slot = u32::from_le_bytes(bytes[pos..pos + 4].try_into().unwrap());
let rho = bytes[pos + 4];
surprising_values.insert(slot, rho);
pos += 5;
}
if bytes.len() < pos + 4 {
return Err(SketchError::DeserializationError(
"Buffer too small for window length".to_string(),
));
}
let window_len = u32::from_le_bytes(bytes[pos..pos + 4].try_into().unwrap()) as usize;
pos += 4;
if bytes.len() < pos + window_len {
return Err(SketchError::DeserializationError(
"Buffer too small for window data".to_string(),
));
}
let sliding_window = bytes[pos..pos + window_len].to_vec();
let k = 1u32 << lg_k;
Ok(CpcSketch {
lg_k,
k,
num_coupons,
flavor,
surprising_values,
sliding_window,
window_offset,
num_surprising_in_window: 0,
})
}
}
impl Sketch for CpcSketch {
type Item = u64;
fn update(&mut self, item: &Self::Item) {
let hash = hash_value(item, 0);
let (slot, rho) = self.process_hash(hash);
match self.flavor {
Flavor::Empty => {
self.transition_to_sparse();
self.update_sparse(slot, rho);
}
Flavor::Sparse => {
self.update_sparse(slot, rho);
}
Flavor::Hybrid | Flavor::Pinned | Flavor::Sliding => {
self.update_dense(slot, rho);
}
}
}
fn estimate(&self) -> f64 {
match self.flavor {
Flavor::Empty => 0.0,
Flavor::Sparse => self.estimate_sparse(),
Flavor::Hybrid | Flavor::Pinned | Flavor::Sliding => self.estimate_dense(),
}
}
fn is_empty(&self) -> bool {
self.num_coupons == 0
}
fn serialize(&self) -> Vec<u8> {
self.to_bytes()
}
fn deserialize(bytes: &[u8]) -> Result<Self, SketchError> {
Self::from_bytes(bytes)
}
}
impl Mergeable for CpcSketch {
fn merge(&mut self, other: &Self) -> Result<(), SketchError> {
if self.lg_k != other.lg_k {
return Err(SketchError::IncompatibleSketches {
reason: format!(
"Cannot merge CPC sketches with different lg_k: {} vs {}",
self.lg_k, other.lg_k
),
});
}
if other.is_empty() {
return Ok(());
}
if self.is_empty() {
*self = other.clone();
return Ok(());
}
for (&slot, &rho) in &other.surprising_values {
self.surprising_values
.entry(slot)
.and_modify(|existing| {
if rho > *existing {
*existing = rho;
}
})
.or_insert(rho);
}
self.num_coupons += other.num_coupons;
if matches!(other.flavor, Flavor::Sliding) {
self.flavor = Flavor::Sliding;
} else if matches!(other.flavor, Flavor::Pinned) && !matches!(self.flavor, Flavor::Sliding)
{
self.flavor = Flavor::Pinned;
} else if matches!(other.flavor, Flavor::Hybrid)
&& matches!(self.flavor, Flavor::Sparse | Flavor::Empty)
{
self.flavor = Flavor::Hybrid;
}
self.check_flavor_transition();
Ok(())
}
}
impl Default for CpcSketch {
fn default() -> Self {
Self::new(11).expect("Default lg_k should be valid")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_valid() {
assert!(CpcSketch::new(4).is_ok());
assert!(CpcSketch::new(11).is_ok());
assert!(CpcSketch::new(26).is_ok());
}
#[test]
fn test_new_invalid() {
assert!(CpcSketch::new(3).is_err());
assert!(CpcSketch::new(27).is_err());
}
#[test]
fn test_process_hash() {
let cpc = CpcSketch::new(10).unwrap();
let hash = 0b11111111110000000000000000000000u32;
let (slot, rho) = cpc.process_hash(hash);
assert_eq!(slot, 1023);
assert_eq!(rho, 23);
}
#[test]
fn test_empty_estimate() {
let cpc = CpcSketch::new(10).unwrap();
assert_eq!(cpc.estimate(), 0.0);
}
#[test]
fn test_single_update() {
let mut cpc = CpcSketch::new(10).unwrap();
cpc.update(&42);
assert!(!cpc.is_empty());
assert_eq!(cpc.flavor(), "Sparse");
}
#[test]
fn test_clear() {
let mut cpc = CpcSketch::new(10).unwrap();
cpc.update(&42);
cpc.clear();
assert!(cpc.is_empty());
assert_eq!(cpc.estimate(), 0.0);
}
}