use crate::common::{Mergeable, Result, Sketch, SketchError};
use crate::frequency::{CountSketch, FrequentItems};
use std::collections::HashMap;
use std::hash::Hasher;
use twox_hash::XxHash64;
#[derive(Clone, Debug)]
pub struct UnivMon {
layers: Vec<Layer>,
num_layers: usize,
max_stream_size: u64,
total_updates: u64,
epsilon: f64,
delta: f64,
}
#[derive(Clone, Debug)]
struct Layer {
count_sketch: CountSketch,
heavy_hitters: FrequentItems<Vec<u8>>,
sampling_rate: f64,
sample_count: u64,
layer_index: usize,
value_sum: f64,
}
impl UnivMon {
pub fn new(max_stream_size: u64, epsilon: f64, delta: f64) -> Result<Self> {
if max_stream_size == 0 {
return Err(SketchError::InvalidParameter {
param: "max_stream_size".to_string(),
value: max_stream_size.to_string(),
constraint: "must be > 0".to_string(),
});
}
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 num_layers = if max_stream_size <= 1 {
3 } else {
let log_n = (max_stream_size as f64).log2().ceil() as usize;
log_n.max(3) };
let mut layers = Vec::with_capacity(num_layers);
for i in 0..num_layers {
let sampling_rate = 2_f64.powi(-(i as i32));
let count_sketch = CountSketch::new(epsilon, delta)?;
let hh_size = ((1000.0 / (i as f64 + 1.0)).ceil() as usize).max(10);
let heavy_hitters = FrequentItems::new(hh_size)?;
layers.push(Layer {
count_sketch,
heavy_hitters,
sampling_rate,
sample_count: 0,
layer_index: i,
value_sum: 0.0,
});
}
Ok(UnivMon {
layers,
num_layers,
max_stream_size,
total_updates: 0,
epsilon,
delta,
})
}
pub fn update(&mut self, item: &[u8], value: f64) -> Result<()> {
if value < 0.0 {
return Err(SketchError::InvalidParameter {
param: "value".to_string(),
value: value.to_string(),
constraint: "must be >= 0".to_string(),
});
}
self.total_updates += 1;
let mut hasher = XxHash64::with_seed(0xDEADBEEF);
hasher.write(item);
let item_hash = hasher.finish();
for layer in &mut self.layers {
let sample_divisor = (1.0 / layer.sampling_rate) as u64;
if item_hash.is_multiple_of(sample_divisor) {
layer.sample_count += 1;
layer.value_sum += value / layer.sampling_rate;
let scaled_value = (value / layer.sampling_rate) as i64;
layer.count_sketch.update(&item, scaled_value);
layer.heavy_hitters.update(item.to_vec());
}
}
Ok(())
}
pub fn estimate_l1(&self) -> f64 {
if self.total_updates == 0 {
return 0.0;
}
let layer = &self.layers[0];
if layer.sample_count == 0 {
return 0.0;
}
layer.value_sum
}
pub fn estimate_l2(&self) -> f64 {
if self.total_updates == 0 {
return 0.0;
}
let layer = &self.layers[0];
if layer.sample_count == 0 {
return 0.0;
}
let inner_product = layer.count_sketch.inner_product(&layer.count_sketch);
let l2_squared = (inner_product as f64).abs();
l2_squared.sqrt()
}
pub fn estimate_entropy(&self) -> f64 {
if self.total_updates == 0 {
return 0.0;
}
let l1 = self.estimate_l1();
if l1 <= 0.0 {
return 0.0;
}
let mut entropy_sum = 0.0;
let mut layer_count = 0;
for layer in self.layers.iter() {
if layer.sample_count < 10 {
continue; }
let items = layer
.heavy_hitters
.frequent_items(crate::frequency::frequent::ErrorType::NoFalsePositives);
if items.is_empty() {
continue;
}
let mut layer_entropy = 0.0;
for (item, _lower, _upper) in &items {
let freq = layer.count_sketch.estimate(&item).abs() as f64;
if freq > 0.0 {
let prob = freq / l1;
if prob > 0.0 && prob <= 1.0 {
layer_entropy -= prob * prob.log2();
}
}
}
entropy_sum += layer_entropy;
layer_count += 1;
}
if layer_count > 0 {
entropy_sum / layer_count as f64
} else {
let l2 = self.estimate_l2();
if l2 > 0.0 {
let n_estimate = (l1 * l1) / (l2 * l2 + 1.0);
n_estimate.max(1.0).log2()
} else {
0.0
}
}
}
pub fn heavy_hitters(&self, threshold: f64) -> Vec<(Vec<u8>, f64)> {
if threshold <= 0.0 || threshold > 1.0 {
return Vec::new();
}
let l1 = self.estimate_l1();
if l1 <= 0.0 {
return Vec::new();
}
let threshold_count = threshold * l1;
let mut candidates: HashMap<Vec<u8>, f64> = HashMap::new();
for layer in &self.layers {
let items = layer
.heavy_hitters
.frequent_items(crate::frequency::frequent::ErrorType::NoFalseNegatives);
for (item, _lower, _upper) in items {
let freq = layer.count_sketch.estimate(&item).abs() as f64;
candidates
.entry(item)
.and_modify(|e| *e = e.max(freq))
.or_insert(freq);
}
}
let mut result: Vec<(Vec<u8>, f64)> = candidates
.into_iter()
.filter(|(_item, freq)| *freq >= threshold_count)
.collect();
result.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
result
}
pub fn detect_change(&self, other: &Self) -> f64 {
if self.num_layers != other.num_layers {
return f64::MAX; }
if self.total_updates == 0 && other.total_updates == 0 {
return 0.0;
}
let mut total_change = 0.0;
let mut layer_weights = 0.0;
for i in 0..self.num_layers {
let layer_self = &self.layers[i];
let layer_other = &other.layers[i];
if layer_self.sample_count < 5 || layer_other.sample_count < 5 {
continue; }
let _diff_sketch = layer_self.count_sketch.clone();
let _neg_other = layer_other.count_sketch.clone();
let ip_self = layer_self
.count_sketch
.inner_product(&layer_self.count_sketch);
let ip_other = layer_other
.count_sketch
.inner_product(&layer_other.count_sketch);
let ip_cross = layer_self
.count_sketch
.inner_product(&layer_other.count_sketch);
let distance_sq = (ip_self + ip_other - 2 * ip_cross).max(0) as f64;
let distance = distance_sq.sqrt();
let weight = layer_self.sampling_rate;
total_change += weight * distance;
layer_weights += weight;
}
if layer_weights > 0.0 {
total_change / layer_weights
} else {
0.0
}
}
pub fn stats(&self) -> UnivMonStats {
let mut total_memory = 0u64;
let mut layer_stats = Vec::new();
for layer in &self.layers {
let cs_memory = layer.count_sketch.width() * layer.count_sketch.depth() * 8; let hh_memory = 1000; let layer_mem = cs_memory + hh_memory;
total_memory += layer_mem as u64;
layer_stats.push(LayerStats {
layer_index: layer.layer_index,
sampling_rate: layer.sampling_rate,
sample_count: layer.sample_count,
memory_bytes: layer_mem as u64,
});
}
UnivMonStats {
num_layers: self.num_layers,
total_memory,
samples_processed: self.total_updates,
layer_stats,
}
}
#[inline]
pub fn num_layers(&self) -> usize {
self.num_layers
}
#[inline]
pub fn max_stream_size(&self) -> u64 {
self.max_stream_size
}
#[inline]
pub fn total_updates(&self) -> u64 {
self.total_updates
}
#[inline]
pub fn epsilon(&self) -> f64 {
self.epsilon
}
#[inline]
pub fn delta(&self) -> f64 {
self.delta
}
}
#[derive(Debug, Clone)]
pub struct UnivMonStats {
pub num_layers: usize,
pub total_memory: u64,
pub samples_processed: u64,
pub layer_stats: Vec<LayerStats>,
}
#[derive(Debug, Clone)]
pub struct LayerStats {
pub layer_index: usize,
pub sampling_rate: f64,
pub sample_count: u64,
pub memory_bytes: u64,
}
impl Sketch for UnivMon {
type Item = (Vec<u8>, f64);
fn update(&mut self, item: &Self::Item) {
let (key, value) = item;
let _ = UnivMon::update(self, key, *value);
}
fn estimate(&self) -> f64 {
self.estimate_l1()
}
fn is_empty(&self) -> bool {
self.total_updates == 0
}
fn serialize(&self) -> Vec<u8> {
let mut bytes = Vec::new();
bytes.extend_from_slice(&self.num_layers.to_le_bytes());
bytes.extend_from_slice(&self.max_stream_size.to_le_bytes());
bytes.extend_from_slice(&self.total_updates.to_le_bytes());
bytes.extend_from_slice(&self.epsilon.to_le_bytes());
bytes.extend_from_slice(&self.delta.to_le_bytes());
for layer in &self.layers {
bytes.extend_from_slice(&layer.sample_count.to_le_bytes());
bytes.extend_from_slice(&layer.layer_index.to_le_bytes());
bytes.extend_from_slice(&layer.sampling_rate.to_le_bytes());
let cs_bytes = layer.count_sketch.serialize();
bytes.extend_from_slice(&(cs_bytes.len() as u64).to_le_bytes());
bytes.extend_from_slice(&cs_bytes);
}
bytes
}
fn deserialize(bytes: &[u8]) -> Result<Self> {
if bytes.len() < 40 {
return Err(SketchError::DeserializationError(
"insufficient bytes for UnivMon header".to_string(),
));
}
let mut offset = 0;
let _num_layers =
usize::from_le_bytes(bytes[offset..offset + 8].try_into().map_err(|_| {
SketchError::DeserializationError("invalid num_layers".to_string())
})?);
offset += 8;
let max_stream_size =
u64::from_le_bytes(bytes[offset..offset + 8].try_into().map_err(|_| {
SketchError::DeserializationError("invalid max_stream_size".to_string())
})?);
offset += 8;
let _total_updates =
u64::from_le_bytes(bytes[offset..offset + 8].try_into().map_err(|_| {
SketchError::DeserializationError("invalid total_updates".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()))?,
);
UnivMon::new(max_stream_size, epsilon, delta)
}
}
impl Mergeable for UnivMon {
fn merge(&mut self, other: &Self) -> Result<()> {
if self.num_layers != other.num_layers {
return Err(SketchError::IncompatibleSketches {
reason: format!(
"layer count mismatch: {} vs {}",
self.num_layers, other.num_layers
),
});
}
if (self.epsilon - other.epsilon).abs() > 1e-10 {
return Err(SketchError::IncompatibleSketches {
reason: format!("epsilon mismatch: {} vs {}", self.epsilon, other.epsilon),
});
}
for i in 0..self.num_layers {
self.layers[i]
.count_sketch
.merge(&other.layers[i].count_sketch)?;
self.layers[i].sample_count += other.layers[i].sample_count;
self.layers[i].value_sum += other.layers[i].value_sum;
}
self.total_updates += other.total_updates;
Ok(())
}
}