use crate::common::{Mergeable, SketchError};
use crate::frequency::CountMinSketch;
#[derive(Clone, Debug)]
pub struct SALSA {
inner: CountMinSketch,
max_observed: u64,
adaptation_threshold: f64,
total_updates: u64,
adaptation_level: u32,
}
impl SALSA {
pub fn new(epsilon: f64, delta: f64) -> Result<Self, SketchError> {
let inner = CountMinSketch::new(epsilon, delta)?;
Ok(SALSA {
inner,
max_observed: 0,
adaptation_threshold: 0.75, total_updates: 0,
adaptation_level: 0,
})
}
pub fn update<T: std::hash::Hash>(&mut self, item: &T, count: u64) {
for _ in 0..count {
self.inner.update(item);
}
self.total_updates = self.total_updates.saturating_add(count);
self.max_observed = self.max_observed.max(count);
if self.should_adapt() {
self.adapt();
}
}
#[inline]
fn should_adapt(&self) -> bool {
let threshold = (u64::MAX as f64 * self.adaptation_threshold) as u64;
self.max_observed > threshold
}
fn adapt(&mut self) {
self.adaptation_level += 1;
}
pub fn estimate<T: std::hash::Hash>(&self, item: &T) -> (u64, u64) {
let estimate = self.inner.estimate(item);
let confidence = if self.total_updates > 0 {
std::cmp::min(100, self.total_updates / 10)
} else {
0
};
(estimate, confidence)
}
pub fn epsilon(&self) -> f64 {
self.inner.epsilon()
}
pub fn delta(&self) -> f64 {
self.inner.delta()
}
pub fn max_observed(&self) -> u64 {
self.max_observed
}
pub fn total_updates(&self) -> u64 {
self.total_updates
}
pub fn adaptation_level(&self) -> u32 {
self.adaptation_level
}
pub fn width(&self) -> usize {
self.inner.width()
}
pub fn depth(&self) -> usize {
self.inner.depth()
}
pub fn merge(&mut self, other: &SALSA) -> Result<(), SketchError> {
self.inner.merge(&other.inner)?;
self.max_observed = self.max_observed.max(other.max_observed);
self.total_updates = self.total_updates.saturating_add(other.total_updates);
if self.should_adapt() {
self.adapt();
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_salsa_new() {
let salsa = SALSA::new(0.01, 0.01).unwrap();
assert_eq!(salsa.max_observed(), 0);
assert_eq!(salsa.total_updates(), 0);
assert_eq!(salsa.adaptation_level(), 0);
}
#[test]
fn test_salsa_single_update() {
let mut salsa = SALSA::new(0.01, 0.01).unwrap();
salsa.update(&"item", 1);
assert_eq!(salsa.max_observed(), 1);
assert_eq!(salsa.total_updates(), 1);
let (estimate, _) = salsa.estimate(&"item");
assert!(estimate >= 1);
}
#[test]
fn test_salsa_multiple_updates() {
let mut salsa = SALSA::new(0.01, 0.01).unwrap();
salsa.update(&"item", 50);
salsa.update(&"item", 50);
assert_eq!(salsa.max_observed(), 50);
assert_eq!(salsa.total_updates(), 100);
let (estimate, _) = salsa.estimate(&"item");
assert!(estimate >= 100);
}
#[test]
fn test_salsa_different_items() {
let mut salsa = SALSA::new(0.01, 0.01).unwrap();
salsa.update(&"apple", 30);
salsa.update(&"banana", 20);
salsa.update(&"cherry", 10);
let (apple_est, _) = salsa.estimate(&"apple");
let (banana_est, _) = salsa.estimate(&"banana");
let (cherry_est, _) = salsa.estimate(&"cherry");
assert!(apple_est >= 30);
assert!(banana_est >= 20);
assert!(cherry_est >= 10);
}
#[test]
fn test_salsa_accuracy_uniform_distribution() {
let mut salsa = SALSA::new(0.01, 0.01).unwrap();
for i in 0..100 {
let key = format!("item_{}", i);
salsa.update(&key, 10);
}
let (est_0, _) = salsa.estimate(&"item_0");
let (est_50, _) = salsa.estimate(&"item_50");
assert!(est_0 >= 10);
assert!(est_50 >= 10);
}
#[test]
fn test_salsa_accuracy_zipfian_distribution() {
let mut salsa = SALSA::new(0.01, 0.01).unwrap();
for i in 0..100 {
let freq = ((100 - i) as u64) * 5; let key = format!("item_{}", i);
salsa.update(&key, freq);
}
let (est, _) = salsa.estimate(&"item_0");
assert!(est >= 500);
let (est_least, _) = salsa.estimate(&"item_99");
assert!(est_least >= 5);
}
#[test]
fn test_salsa_merge() {
let mut salsa1 = SALSA::new(0.01, 0.01).unwrap();
let mut salsa2 = SALSA::new(0.01, 0.01).unwrap();
salsa1.update(&"item", 100);
salsa2.update(&"item", 50);
salsa1.merge(&salsa2).unwrap();
let (estimate, _) = salsa1.estimate(&"item");
assert!(estimate >= 150);
}
#[test]
fn test_salsa_merge_incompatible() {
let salsa1 = SALSA::new(0.01, 0.01).unwrap();
let mut salsa2 = SALSA::new(0.001, 0.01).unwrap();
let result = salsa2.merge(&salsa1);
assert!(result.is_err());
}
}