1use crate::error::QuantEvalError;
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct CompressionBenchmarkConfig {
11 pub dim: usize,
13 pub db_size: usize,
15 pub queries: usize,
17 pub seed: u64,
19 pub top_k: usize,
21 pub iterations: u64,
23}
24
25impl Default for CompressionBenchmarkConfig {
26 fn default() -> Self {
27 Self {
28 dim: 768,
29 db_size: 10_000,
30 queries: 100,
31 seed: 42,
32 top_k: 10,
33 iterations: 100,
34 }
35 }
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct CompressionBenchmarkResult {
41 pub cosine_similarity: CosineSimilarityStats,
43 pub recall_at_k: f32,
45 pub mrr: f32,
47 pub queries: usize,
49 pub db_size: usize,
51 pub top_k: usize,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct CosineSimilarityStats {
58 pub mean: f32,
60 pub median: f32,
62 pub min: f32,
64 pub max: f32,
66 pub std_dev: f32,
68}
69
70#[derive(Debug, Clone)]
72pub struct CompressionBenchmark {
73 config: CompressionBenchmarkConfig,
74}
75
76impl CompressionBenchmark {
77 pub fn new() -> Self {
79 Self {
80 config: CompressionBenchmarkConfig::default(),
81 }
82 }
83
84 pub fn with_config(config: CompressionBenchmarkConfig) -> Self {
86 Self { config }
87 }
88
89 pub fn config(&self) -> &CompressionBenchmarkConfig {
91 &self.config
92 }
93
94 pub fn run(&self) -> Result<CompressionBenchmarkResult, QuantEvalError> {
99 let corpus = self.generate_corpus()?;
101 let queries = self.generate_queries()?;
102
103 let exact_results = self.compute_exact_neighbors(&corpus, &queries)?;
105
106 let compressed_results = self.simulate_compression(&exact_results)?;
109
110 let cosine_stats = self.compute_cosine_similarity(&exact_results, &compressed_results)?;
112 let recall = self.compute_recall_at_k(&exact_results, &compressed_results)?;
113 let mrr = self.compute_mrr(&exact_results, &compressed_results)?;
114
115 Ok(CompressionBenchmarkResult {
116 cosine_similarity: cosine_stats,
117 recall_at_k: recall,
118 mrr,
119 queries: self.config.queries,
120 db_size: self.config.db_size,
121 top_k: self.config.top_k,
122 })
123 }
124
125 fn generate_corpus(&self) -> Result<Vec<Vec<f32>>, QuantEvalError> {
127 use std::collections::hash_map::DefaultHasher;
128 use std::hash::{Hash, Hasher};
129
130 let mut corpus = Vec::with_capacity(self.config.db_size);
131 let mut hasher = DefaultHasher::new();
132 self.config.seed.hash(&mut hasher);
133
134 for i in 0..self.config.db_size {
135 let mut rng = seed_rng(hasher.finish().wrapping_add(i as u64));
136 let vec = generate_random_vector(self.config.dim, &mut rng);
137 corpus.push(vec);
138 }
139
140 Ok(corpus)
141 }
142
143 fn generate_queries(&self) -> Result<Vec<Vec<f32>>, QuantEvalError> {
145 let mut queries = Vec::with_capacity(self.config.queries);
146 let seed = self.config.seed.wrapping_add(0xdeadbeef);
147
148 for i in 0..self.config.queries {
149 let mut rng = seed_rng(seed.wrapping_add(i as u64));
150 let vec = generate_random_vector(self.config.dim, &mut rng);
151 queries.push(vec);
152 }
153
154 Ok(queries)
155 }
156
157 fn compute_exact_neighbors(
159 &self,
160 corpus: &[Vec<f32>],
161 queries: &[Vec<f32>],
162 ) -> Result<Vec<Vec<usize>>, QuantEvalError> {
163 let mut results = Vec::with_capacity(queries.len());
164
165 for query in queries {
166 let mut distances: Vec<(usize, f32)> = corpus
167 .iter()
168 .enumerate()
169 .map(|(idx, vec)| (idx, cosine_similarity(query, vec)))
170 .collect();
171
172 distances.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
174
175 let top_k = distances
177 .into_iter()
178 .take(self.config.top_k)
179 .map(|(idx, _)| idx)
180 .collect();
181
182 results.push(top_k);
183 }
184
185 Ok(results)
186 }
187
188 fn simulate_compression(
193 &self,
194 exact_results: &[Vec<usize>],
195 ) -> Result<Vec<Vec<usize>>, QuantEvalError> {
196 let mut compressed = Vec::with_capacity(exact_results.len());
198
199 for result in exact_results {
200 compressed.push(result.clone());
204 }
205
206 Ok(compressed)
207 }
208
209 fn compute_cosine_similarity(
211 &self,
212 exact: &[Vec<usize>],
213 compressed: &[Vec<usize>],
214 ) -> Result<CosineSimilarityStats, QuantEvalError> {
215 let mut similarities = Vec::new();
217
218 for (exact_result, compressed_result) in exact.iter().zip(compressed.iter()) {
219 let exact_set: std::collections::HashSet<_> = exact_result.iter().cloned().collect();
221 let compressed_set: std::collections::HashSet<_> =
222 compressed_result.iter().cloned().collect();
223
224 let intersection = exact_set.intersection(&compressed_set).count();
225 let union = exact_set.union(&compressed_set).count();
226
227 if union > 0 {
228 let jaccard = intersection as f32 / union as f32;
229 similarities.push((jaccard * 2.0 - 1.0).max(0.0));
232 }
233 }
234
235 if similarities.is_empty() {
236 return Ok(CosineSimilarityStats {
237 mean: 0.0,
238 median: 0.0,
239 min: 0.0,
240 max: 0.0,
241 std_dev: 0.0,
242 });
243 }
244
245 similarities.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
247
248 let n = similarities.len();
249 let mean = similarities.iter().sum::<f32>() / n as f32;
250 let median = if n % 2 == 0 {
251 (similarities[n / 2 - 1] + similarities[n / 2]) / 2.0
252 } else {
253 similarities[n / 2]
254 };
255 let min = similarities[0];
256 let max = similarities[n - 1];
257
258 let variance = similarities.iter().map(|s| (s - mean).powi(2)).sum::<f32>() / n as f32;
260 let std_dev = variance.sqrt();
261
262 Ok(CosineSimilarityStats {
263 mean,
264 median,
265 min,
266 max,
267 std_dev,
268 })
269 }
270
271 fn compute_recall_at_k(
273 &self,
274 exact: &[Vec<usize>],
275 estimated: &[Vec<usize>],
276 ) -> Result<f32, QuantEvalError> {
277 if exact.is_empty() || exact.len() != estimated.len() || self.config.top_k == 0 {
278 return Ok(0.0);
279 }
280
281 let mut hits = 0usize;
282 let mut total = 0usize;
283
284 for (exact_row, estimated_row) in exact.iter().zip(estimated.iter()) {
285 let exact_top = &exact_row[..exact_row.len().min(self.config.top_k)];
286 let estimated_top = &estimated_row[..estimated_row.len().min(self.config.top_k)];
287
288 total += exact_top.len();
289 hits += estimated_top
290 .iter()
291 .filter(|candidate| exact_top.contains(candidate))
292 .count();
293 }
294
295 if total == 0 {
296 Ok(0.0)
297 } else {
298 Ok(hits as f32 / total as f32)
299 }
300 }
301
302 fn compute_mrr(
304 &self,
305 exact: &[Vec<usize>],
306 estimated: &[Vec<usize>],
307 ) -> Result<f32, QuantEvalError> {
308 if exact.is_empty() || exact.len() != estimated.len() {
309 return Ok(0.0);
310 }
311
312 let mut reciprocal_ranks = Vec::new();
313
314 for (exact_row, estimated_row) in exact.iter().zip(estimated.iter()) {
315 let exact_set: std::collections::HashSet<_> = exact_row.iter().cloned().collect();
317
318 for (rank, estimated_idx) in estimated_row.iter().enumerate() {
319 if exact_set.contains(estimated_idx) {
320 reciprocal_ranks.push(1.0 / (rank + 1) as f32);
321 break;
322 }
323 }
324 }
325
326 if reciprocal_ranks.is_empty() {
327 Ok(0.0)
328 } else {
329 Ok(reciprocal_ranks.iter().sum::<f32>() / reciprocal_ranks.len() as f32)
330 }
331 }
332}
333
334impl Default for CompressionBenchmark {
335 fn default() -> Self {
336 Self::new()
337 }
338}
339
340struct SimpleRng(u64);
342
343impl SimpleRng {
344 fn next(&mut self) -> u64 {
345 let x = self.0;
347 let x = x ^ (x << 13);
348 let x = x ^ (x >> 7);
349 let x = x ^ (x << 17);
350 self.0 = x;
351 x
352 }
353
354 fn next_f32(&mut self) -> f32 {
355 (self.next() as f32) / (u64::MAX as f32)
357 }
358}
359
360fn seed_rng(seed: u64) -> SimpleRng {
361 SimpleRng(seed)
362}
363
364fn generate_random_vector(dim: usize, rng: &mut SimpleRng) -> Vec<f32> {
365 let mut vec: Vec<f32> = (0..dim).map(|_| rng.next_f32() * 2.0 - 1.0).collect();
367
368 let magnitude: f32 = vec.iter().map(|x| x * x).sum::<f32>().sqrt();
370 if magnitude > 0.0 {
371 for v in &mut vec {
372 *v /= magnitude;
373 }
374 }
375
376 vec
377}
378
379fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
380 let dot: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
381 let mag_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
382 let mag_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
383
384 if mag_a > 0.0 && mag_b > 0.0 {
385 dot / (mag_a * mag_b)
386 } else {
387 0.0
388 }
389}
390
391#[cfg(test)]
392mod tests {
393 use super::*;
394
395 #[test]
396 fn test_default_config() {
397 let config = CompressionBenchmarkConfig::default();
398 assert_eq!(config.dim, 768);
399 assert_eq!(config.db_size, 10_000);
400 assert_eq!(config.queries, 100);
401 assert_eq!(config.top_k, 10);
402 }
403
404 #[test]
405 fn test_small_benchmark() {
406 let config = CompressionBenchmarkConfig {
407 dim: 64,
408 db_size: 100,
409 queries: 10,
410 seed: 42,
411 top_k: 5,
412 iterations: 10,
413 };
414
415 let benchmark = CompressionBenchmark::with_config(config);
416 let result = benchmark.run().expect("benchmark should succeed");
417
418 assert_eq!(result.queries, 10);
419 assert_eq!(result.db_size, 100);
420 assert_eq!(result.top_k, 5);
421 }
422
423 #[test]
424 fn test_cosine_similarity() {
425 let a = vec![1.0, 0.0, 0.0];
426 let b = vec![1.0, 0.0, 0.0];
427 let c = vec![0.0, 1.0, 0.0];
428
429 assert!((cosine_similarity(&a, &b) - 1.0).abs() < 0.001);
430 assert!((cosine_similarity(&a, &c) - 0.0).abs() < 0.001);
431 }
432
433 #[test]
434 fn test_recall_at_k() {
435 let exact = vec![vec![0, 1, 2, 3, 4], vec![5, 6, 7, 8, 9]];
436 let estimated = vec![vec![0, 1, 2, 10, 11], vec![5, 6, 12, 13, 14]];
437
438 let recall = CompressionBenchmark::default()
439 .compute_recall_at_k(&exact, &estimated)
440 .expect("should compute");
441
442 assert!((recall - 0.5).abs() < 0.001);
444 }
445
446 #[test]
447 fn test_mrr() {
448 let exact = vec![vec![0, 1, 2], vec![5, 6, 7]];
449 let estimated = vec![vec![10, 0, 2], vec![5, 20, 30]];
450
451 let mrr = CompressionBenchmark::default()
452 .compute_mrr(&exact, &estimated)
453 .expect("should compute");
454
455 assert!((mrr - 0.75).abs() < 0.001);
459 }
460}