#[cfg(not(target_family = "wasm"))]
fn main() {
use plugmem_core::{
Config, FactId, MemStorage, Memory, RecallQuery, RecallResult, RecallScratch,
};
use plugmem_testgen::{Gen, GenOp, Profile, apply};
fn cosine(a: &[f32], b: &[f32]) -> f32 {
let mut dot = 0.0f32;
let mut na = 0.0f32;
let mut nb = 0.0f32;
for (&x, &y) in a.iter().zip(b) {
dot += x * y;
na += x * x;
nb += y * y;
}
let denom = na.sqrt() * nb.sqrt();
if denom > 0.0 { dot / denom } else { 0.0 }
}
fn ground_truth(
corpus: &[(FactId, Vec<f32>)],
query: &[f32],
self_id: FactId,
k: usize,
) -> Vec<FactId> {
let mut scored: Vec<(f32, FactId)> = corpus
.iter()
.filter(|(id, _)| *id != self_id)
.map(|(id, v)| (cosine(query, v), *id))
.collect();
scored.sort_by(|a, b| b.0.total_cmp(&a.0));
scored.into_iter().take(k).map(|(_, id)| id).collect()
}
const K: usize = 8;
const FILLS: [usize; 3] = [2_000, 20_000, 60_000];
const DIMS: [usize; 2] = [384, 768];
const QUERIES: usize = 100;
println!("# recall@{K} vs exact f32 cosine (ground truth), self-excluded");
println!("dim\tfill\tmode\trecall_mean\trecall_min\tqueries");
for &dim in &DIMS {
for &fill in &FILLS {
let profile = Profile {
dim,
w_revise: 0,
w_forget: 0,
w_link: 0,
..Profile::default()
};
let seed = 0x5EC0_0000_0000_0000 ^ (fill as u64) ^ ((dim as u64) << 40);
let ops = Gen::new(seed, profile).ops(fill);
let mut cfg = Config::default();
cfg.dim = dim;
cfg.w_recency = 0.0;
let mut mem = Memory::new(cfg).unwrap();
let mut store = MemStorage::new();
let mut corpus: Vec<(FactId, Vec<f32>)> = Vec::with_capacity(fill);
for op in &ops {
let outcome = apply(&mut mem, &mut store, op).unwrap();
if let (
GenOp::Remember {
vector: Some(v), ..
},
Some(o),
) = (op, outcome)
{
corpus.push((o.id, v.clone()));
}
}
let now = ops
.iter()
.map(|op| match op {
GenOp::Remember { now, .. }
| GenOp::Revise { now, .. }
| GenOp::Forget { now, .. }
| GenOp::Link { now, .. }
| GenOp::Maintain { now } => *now,
})
.max()
.unwrap_or(0)
+ 1;
mem.maintain(&mut store, now).unwrap();
let mode = if fill >= 24_000 { "hnsw" } else { "flat" };
let step = (corpus.len() / QUERIES).max(1);
let mut out = RecallResult::default();
let mut scratch = RecallScratch::new();
let mut sum = 0.0f64;
let mut min = 1.0f32;
let mut n = 0usize;
for (self_id, qv) in corpus.iter().step_by(step) {
let truth = ground_truth(&corpus, qv, *self_id, K);
if truth.is_empty() {
continue;
}
let q = RecallQuery {
vector: Some(qv),
k: K,
text: None,
..RecallQuery::text(now, "")
};
mem.recall_into(q, &mut scratch, &mut out).unwrap();
let hits = out
.facts
.iter()
.filter(|f| f.id != *self_id && truth.contains(&f.id))
.count();
let recall = hits as f32 / truth.len() as f32;
sum += recall as f64;
min = min.min(recall);
n += 1;
}
let mean = if n > 0 { sum / n as f64 } else { 0.0 };
println!("{dim}\t{fill}\t{mode}\t{mean:.3}\t{min:.3}\t{n}");
}
}
}
#[cfg(target_family = "wasm")]
fn main() {}