use crate::common::{validation, Mergeable, Sketch, SketchError};
use std::hash::{Hash, Hasher};
use twox_hash::XxHash64;
#[derive(Clone, Debug)]
pub struct UltraLogLog {
precision: u8,
registers: Vec<u8>,
}
impl UltraLogLog {
pub fn new(precision: u8) -> Result<Self, SketchError> {
if !(4..=18).contains(&precision) {
return Err(SketchError::InvalidParameter {
param: "precision".to_string(),
value: precision.to_string(),
constraint: "must be between 4 and 18".to_string(),
});
}
let m = 1 << precision; let registers = vec![0u8; m];
Ok(UltraLogLog {
precision,
registers,
})
}
#[inline]
fn register_count(&self) -> usize {
1 << self.precision
}
#[inline]
fn alpha_mm(&self) -> f64 {
let m = self.register_count() as f64;
let alpha = match self.register_count() {
16 => 0.673,
32 => 0.697,
64 => 0.709,
_ => 0.7213 / (1.0 + 1.079 / m),
};
alpha * m * m
}
#[inline]
fn linear_counting(&self, zeros: usize) -> f64 {
let m = self.register_count() as f64;
m * (m / zeros as f64).ln()
}
fn raw_estimate(&self) -> f64 {
let sum: f64 = self
.registers
.iter()
.map(|®| 2.0_f64.powi(-(reg as i32)))
.sum();
self.alpha_mm() / sum
}
fn bias_corrected_estimate(&self, raw: f64) -> f64 {
let m = self.register_count() as f64;
let zeros = self.registers.iter().filter(|&&r| r == 0).count();
if zeros > 0 && raw < 5.0 * m {
return self.linear_counting(zeros);
}
raw
}
pub fn cardinality(&self) -> f64 {
let raw = self.raw_estimate();
self.bias_corrected_estimate(raw)
}
#[inline(always)]
fn hash_item<T: Hash>(&self, item: &T) -> u64 {
let mut hasher = XxHash64::with_seed(0);
item.hash(&mut hasher);
hasher.finish()
}
#[inline(always)]
fn extract_register_and_zeros_64(&self, hash: u64) -> (usize, u8) {
let p = self.precision as u32;
let register_index = (hash >> (64 - p)) as usize;
let remaining_bits = hash << p;
let leading_zeros = if remaining_bits == 0 {
(64 - p + 1) as u8
} else {
(remaining_bits.leading_zeros() + 1) as u8
};
(register_index, leading_zeros)
}
#[inline]
pub fn add<T: Hash>(&mut self, item: &T) {
let hash = self.hash_item(item);
let (register_index, leading_zeros) = self.extract_register_and_zeros_64(hash);
unsafe {
let reg = self.registers.get_unchecked_mut(register_index);
*reg = (*reg).max(leading_zeros);
}
}
}
impl Sketch for UltraLogLog {
type Item = u64;
fn update(&mut self, item: &Self::Item) {
self.add(item);
}
fn estimate(&self) -> f64 {
self.cardinality()
}
fn is_empty(&self) -> bool {
self.registers.iter().all(|&r| r == 0)
}
fn serialize(&self) -> Vec<u8> {
let mut bytes = Vec::with_capacity(1 + self.registers.len());
bytes.push(self.precision);
bytes.extend_from_slice(&self.registers);
bytes
}
fn deserialize(bytes: &[u8]) -> Result<Self, SketchError> {
validation::validate_min_size(bytes.len(), 1)?;
validation::validate_byte_size(bytes.len())?;
let precision = bytes[0];
validation::validate_precision(precision)?;
let expected_len = 1 + (1 << precision);
if bytes.len() != expected_len {
return Err(SketchError::DeserializationError(format!(
"Invalid serialization: expected {} bytes for precision {}, got {}",
expected_len,
precision,
bytes.len()
)));
}
let registers = bytes[1..].to_vec();
Ok(UltraLogLog {
precision,
registers,
})
}
}
impl Mergeable for UltraLogLog {
fn merge(&mut self, other: &Self) -> Result<(), SketchError> {
if self.precision != other.precision {
return Err(SketchError::IncompatibleSketches {
reason: format!(
"precision mismatch: {} vs {}",
self.precision, other.precision
),
});
}
for (i, &other_reg) in other.registers.iter().enumerate() {
self.registers[i] = self.registers[i].max(other_reg);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_register_count() {
let ull = UltraLogLog::new(12).unwrap();
assert_eq!(ull.register_count(), 4096);
let ull = UltraLogLog::new(8).unwrap();
assert_eq!(ull.register_count(), 256);
}
#[test]
fn test_alpha_mm() {
let ull = UltraLogLog::new(12).unwrap();
let alpha_mm = ull.alpha_mm();
assert!(alpha_mm > 10_000_000.0);
}
#[test]
fn test_extract_register_and_zeros() {
let ull = UltraLogLog::new(12).unwrap();
let hash = 0xF00u64 << 52; let (index, zeros) = ull.extract_register_and_zeros_64(hash);
assert_eq!(index, 0xF00);
assert_eq!(zeros, 53);
}
}