use crate::core::entangled::EntangledHVec;
pub const DEFAULT_Z_THRESHOLD: f64 = 3.0;
pub struct CountingBloomMemory {
dim: usize,
counts: Vec<u32>,
active_insertions: u64,
item_count: usize,
}
impl CountingBloomMemory {
pub fn new(dim: usize) -> Self {
Self {
dim,
counts: vec![0; dim],
active_insertions: 0,
item_count: 0,
}
}
pub fn insert(&mut self, item: &EntangledHVec) {
for &i in item.indices() {
if let Some(c) = self.counts.get_mut(i as usize) {
*c = c.saturating_add(1);
self.active_insertions += 1;
}
}
self.item_count += 1;
}
pub fn insert_batch(&mut self, items: &[EntangledHVec]) {
for item in items {
self.insert(item);
}
}
pub fn membership_score(&self, item: &EntangledHVec) -> f64 {
let ix = item.indices();
if ix.is_empty() {
return 0.0;
}
let lambda = self.active_insertions as f64 / self.dim.max(1) as f64;
let excess: f64 = ix
.iter()
.map(|&i| self.counts.get(i as usize).copied().unwrap_or(0) as f64 - lambda)
.sum();
let std = (ix.len() as f64 * lambda).sqrt();
if std <= f64::EPSILON {
return if excess > 0.0 { f64::INFINITY } else { 0.0 };
}
excess / std
}
pub fn contains(&self, item: &EntangledHVec, z_threshold: f64) -> bool {
self.membership_score(item) >= z_threshold
}
pub fn query_candidates(
&self,
candidates: &[EntangledHVec],
z_threshold: f64,
) -> Vec<(usize, f64)> {
candidates
.iter()
.enumerate()
.map(|(i, item)| (i, self.membership_score(item)))
.filter(|&(_, z)| z >= z_threshold)
.collect()
}
pub fn item_count(&self) -> usize {
self.item_count
}
pub fn dim(&self) -> usize {
self.dim
}
pub fn mean_count(&self) -> f64 {
self.active_insertions as f64 / self.dim.max(1) as f64
}
}
#[cfg(test)]
mod tests {
use super::*;
fn store(dim: usize, n: u64) -> CountingBloomMemory {
let mut mem = CountingBloomMemory::new(dim);
let items: Vec<EntangledHVec> = (0..n)
.map(|i| EntangledHVec::new_deterministic(dim, i * 100))
.collect();
mem.insert_batch(&items);
mem
}
fn mean(v: &[f64]) -> f64 {
v.iter().sum::<f64>() / v.len() as f64
}
#[test]
fn single_member_scores_high_nonmember_near_zero() {
let dim = 16384;
let mut mem = CountingBloomMemory::new(dim);
let item = EntangledHVec::new_deterministic(dim, 42);
mem.insert(&item);
let member = mem.membership_score(&item);
assert!(member > 20.0, "lone member must stand out, got {member:.2}");
let nms: Vec<f64> = (0..50)
.map(|j| mem.membership_score(&EntangledHVec::new_deterministic(dim, 900_000 + j)))
.collect();
assert!(
mean(&nms).abs() < 1.0,
"non-member mean should sit near the null"
);
let nm_max = nms.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
assert!(
member > 5.0 * nm_max.max(1.0),
"member must dwarf non-members"
);
}
#[test]
fn empty_query_is_zero() {
let mem = store(16384, 100);
let empty = EntangledHVec::from_indices(Vec::new(), 16384);
assert_eq!(mem.membership_score(&empty), 0.0);
}
#[test]
fn discriminates_at_1000_items() {
let dim = 16384;
let mem = store(dim, 1000);
let members: Vec<f64> = (0..1000)
.map(|i| mem.membership_score(&EntangledHVec::new_deterministic(dim, i * 100)))
.collect();
let nonmembers: Vec<f64> = (0..200)
.map(|j| mem.membership_score(&EntangledHVec::new_deterministic(dim, 5_000_000 + j)))
.collect();
let m = mean(&members);
let nm = mean(&nonmembers);
assert!(m > 3.0, "member mean z should clear 3, got {m:.2}");
assert!(
nm.abs() < 1.0,
"non-member mean z should sit near 0, got {nm:.2}"
);
let fpr = nonmembers.iter().filter(|&&z| z >= 3.0).count() as f64 / nonmembers.len() as f64;
assert!(
fpr < 0.05,
"non-member FPR@z>3 should be small, got {fpr:.3}"
);
}
#[test]
fn still_discriminates_where_binary_is_dead() {
let dim = 16384;
let mem = store(dim, 2000);
let members: Vec<f64> = (0..400)
.map(|i| mem.membership_score(&EntangledHVec::new_deterministic(dim, i * 100)))
.collect();
let nonmembers: Vec<f64> = (0..400)
.map(|j| mem.membership_score(&EntangledHVec::new_deterministic(dim, 6_000_000 + j)))
.collect();
assert!(
mean(&members) - mean(&nonmembers) > 2.0,
"counting must still separate at 2000 items: members {:.2} vs non {:.2}",
mean(&members),
mean(&nonmembers)
);
}
}