1use crate::error::QuantEvalError;
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct SemanticMemoryConfig {
12 pub dim: usize,
14 pub index_size: usize,
16 pub num_queries: usize,
18 pub top_k: usize,
20 pub seed: u64,
22}
23
24impl Default for SemanticMemoryConfig {
25 fn default() -> Self {
26 Self {
27 dim: 768,
28 index_size: 10_000,
29 num_queries: 100,
30 top_k: 10,
31 seed: 42,
32 }
33 }
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct SemanticMemoryResult {
39 pub raw_quality: SearchQualityScore,
41 pub compressed_quality: SearchQualityScore,
43 pub degradation_ratio: f32,
45 pub queries: usize,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct SearchQualityScore {
52 pub precision_at_k: f32,
54 pub recall_at_k: f32,
56 pub ndcg_at_k: f32,
58 pub map: f32,
60}
61
62impl Default for SearchQualityScore {
63 fn default() -> Self {
64 Self {
65 precision_at_k: 0.0,
66 recall_at_k: 0.0,
67 ndcg_at_k: 0.0,
68 map: 0.0,
69 }
70 }
71}
72
73#[derive(Debug, Clone)]
75pub struct SemanticMemoryBenchmark {
76 config: SemanticMemoryConfig,
77}
78
79impl SemanticMemoryBenchmark {
80 pub fn new() -> Self {
82 Self {
83 config: SemanticMemoryConfig::default(),
84 }
85 }
86
87 pub fn with_config(config: SemanticMemoryConfig) -> Self {
89 Self { config }
90 }
91
92 pub fn config(&self) -> &SemanticMemoryConfig {
94 &self.config
95 }
96
97 pub fn run(&self) -> Result<SemanticMemoryResult, QuantEvalError> {
99 let index = self.generate_index()?;
101 let queries = self.generate_queries()?;
102 let relevance = self.generate_relevance_judgments(&index, &queries)?;
103
104 let raw_results = self.raw_search(&index, &queries)?;
106 let raw_quality = self.compute_quality(&raw_results, &relevance)?;
107
108 let compressed_results = self.compressed_search(&index, &queries)?;
110 let compressed_quality = self.compute_quality(&compressed_results, &relevance)?;
111
112 let degradation_ratio = if raw_quality.ndcg_at_k > 0.0 {
114 compressed_quality.ndcg_at_k / raw_quality.ndcg_at_k
115 } else {
116 0.0
117 };
118
119 Ok(SemanticMemoryResult {
120 raw_quality,
121 compressed_quality,
122 degradation_ratio,
123 queries: self.config.num_queries,
124 })
125 }
126
127 fn generate_index(&self) -> Result<Vec<Vec<f32>>, QuantEvalError> {
129 let mut rng = seed_rng(self.config.seed);
130 let mut index = Vec::with_capacity(self.config.index_size);
131
132 for i in 0..self.config.index_size {
133 let vec = generate_random_vector(self.config.dim, &mut rng);
134 index.push(vec);
135 rng = seed_rng(self.config.seed.wrapping_add(i as u64 * 0x9e3779b9));
137 }
138
139 Ok(index)
140 }
141
142 fn generate_queries(&self) -> Result<Vec<Vec<f32>>, QuantEvalError> {
144 let mut queries = Vec::with_capacity(self.config.num_queries);
145 let base_seed = self.config.seed.wrapping_add(0xdeadbeef);
146
147 for i in 0..self.config.num_queries {
148 let mut rng = seed_rng(base_seed.wrapping_add(i as u64));
149 let vec = generate_random_vector(self.config.dim, &mut rng);
150 queries.push(vec);
151 }
152
153 Ok(queries)
154 }
155
156 fn generate_relevance_judgments(
158 &self,
159 index: &[Vec<f32>],
160 queries: &[Vec<f32>],
161 ) -> Result<Vec<Vec<(usize, f32)>>, QuantEvalError> {
162 let mut judgments = Vec::with_capacity(queries.len());
163
164 for query in queries {
165 let mut scores: Vec<(usize, f32)> = index
166 .iter()
167 .enumerate()
168 .map(|(idx, vec)| (idx, cosine_similarity(query, vec)))
169 .collect();
170
171 scores.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
172
173 let relevant: Vec<(usize, f32)> = scores.into_iter().take(self.config.top_k).collect();
175
176 judgments.push(relevant);
177 }
178
179 Ok(judgments)
180 }
181
182 fn raw_search(
184 &self,
185 index: &[Vec<f32>],
186 queries: &[Vec<f32>],
187 ) -> Result<Vec<Vec<usize>>, QuantEvalError> {
188 let mut results = Vec::with_capacity(queries.len());
189
190 for query in queries {
191 let mut distances: Vec<(usize, f32)> = index
192 .iter()
193 .enumerate()
194 .map(|(idx, vec)| (idx, cosine_similarity(query, vec)))
195 .collect();
196
197 distances.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
198
199 let top_k: Vec<usize> = distances
200 .into_iter()
201 .take(self.config.top_k)
202 .map(|(idx, _)| idx)
203 .collect();
204
205 results.push(top_k);
206 }
207
208 Ok(results)
209 }
210
211 fn compressed_search(
216 &self,
217 index: &[Vec<f32>],
218 queries: &[Vec<f32>],
219 ) -> Result<Vec<Vec<usize>>, QuantEvalError> {
220 self.raw_search(index, queries)
223 }
224
225 fn compute_quality(
227 &self,
228 results: &[Vec<usize>],
229 relevance: &[Vec<(usize, f32)>],
230 ) -> Result<SearchQualityScore, QuantEvalError> {
231 if results.is_empty() || results.len() != relevance.len() {
232 return Ok(SearchQualityScore::default());
233 }
234
235 let k = self.config.top_k;
236 let mut precision_sum = 0.0f32;
237 let mut recall_sum = 0.0f32;
238 let mut ndcg_sum = 0.0f32;
239 let mut ap_sum = 0.0f32;
240
241 for (result, rel) in results.iter().zip(relevance.iter()) {
242 let result_set: std::collections::HashSet<_> = result.iter().take(k).cloned().collect();
243 let rel_set: std::collections::HashMap<_, _> =
244 rel.iter().take(k).map(|(i, s)| (i, *s)).collect();
245
246 let relevant_retrieved = result_set
248 .iter()
249 .filter(|idx| rel_set.contains_key(*idx))
250 .count();
251 let precision = if k > 0 {
252 relevant_retrieved as f32 / k as f32
253 } else {
254 0.0
255 };
256 precision_sum += precision;
257
258 let total_relevant = rel_set.len();
260 let recall = if total_relevant > 0 {
261 relevant_retrieved as f32 / total_relevant as f32
262 } else {
263 0.0
264 };
265 recall_sum += recall;
266
267 let mut dcg = 0.0f32;
269 for (i, idx) in result.iter().enumerate().take(k) {
270 let relevance_score = rel_set.get(idx).copied().unwrap_or(0.0);
271 dcg += relevance_score / (i + 1) as f32;
272 }
273
274 let mut idcg = 0.0f32;
275 let mut sorted_rel: Vec<f32> = rel.iter().map(|(_, s)| *s).collect();
276 sorted_rel.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
277 for (i, score) in sorted_rel.iter().enumerate().take(k) {
278 idcg += score / (i + 1) as f32;
279 }
280
281 let ndcg = if idcg > 0.0 { dcg / idcg } else { 0.0 };
282 ndcg_sum += ndcg;
283
284 let mut ap = 0.0f32;
286 let mut relevant_count = 0usize;
287 for (i, idx) in result.iter().enumerate().take(k) {
288 if rel_set.contains_key(idx) {
289 relevant_count += 1;
290 ap += relevant_count as f32 / (i + 1) as f32;
291 }
292 }
293 if relevant_count > 0 {
294 ap /= relevant_count as f32;
295 }
296 ap_sum += ap;
297 }
298
299 let n = results.len() as f32;
300 Ok(SearchQualityScore {
301 precision_at_k: precision_sum / n,
302 recall_at_k: recall_sum / n,
303 ndcg_at_k: ndcg_sum / n,
304 map: ap_sum / n,
305 })
306 }
307}
308
309impl Default for SemanticMemoryBenchmark {
310 fn default() -> Self {
311 Self::new()
312 }
313}
314
315struct SimpleRng(u64);
317
318impl SimpleRng {
319 fn next(&mut self) -> u64 {
320 let x = self.0;
321 let x = x ^ (x << 13);
322 let x = x ^ (x >> 7);
323 let x = x ^ (x << 17);
324 self.0 = x;
325 x
326 }
327
328 fn next_f32(&mut self) -> f32 {
329 (self.next() as f32) / (u64::MAX as f32)
330 }
331}
332
333fn seed_rng(seed: u64) -> SimpleRng {
334 SimpleRng(seed)
335}
336
337fn generate_random_vector(dim: usize, rng: &mut SimpleRng) -> Vec<f32> {
338 let mut vec: Vec<f32> = (0..dim).map(|_| rng.next_f32() * 2.0 - 1.0).collect();
339 let magnitude: f32 = vec.iter().map(|x| x * x).sum::<f32>().sqrt();
340 if magnitude > 0.0 {
341 for v in &mut vec {
342 *v /= magnitude;
343 }
344 }
345 vec
346}
347
348fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
349 let dot: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
350 let mag_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
351 let mag_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
352 if mag_a > 0.0 && mag_b > 0.0 {
353 dot / (mag_a * mag_b)
354 } else {
355 0.0
356 }
357}
358
359#[cfg(test)]
360mod tests {
361 use super::*;
362
363 #[test]
364 fn test_default_config() {
365 let config = SemanticMemoryConfig::default();
366 assert_eq!(config.dim, 768);
367 assert_eq!(config.index_size, 10_000);
368 assert_eq!(config.num_queries, 100);
369 assert_eq!(config.top_k, 10);
370 }
371
372 #[test]
373 fn test_small_benchmark() {
374 let config = SemanticMemoryConfig {
375 dim: 32,
376 index_size: 100,
377 num_queries: 5,
378 top_k: 5,
379 seed: 42,
380 };
381
382 let benchmark = SemanticMemoryBenchmark::with_config(config);
383 let result = benchmark.run().expect("benchmark should succeed");
384
385 assert_eq!(result.queries, 5);
386 assert!(result.raw_quality.ndcg_at_k > 0.9);
388 assert!(result.degradation_ratio > 0.9);
390 }
391
392 #[test]
393 fn test_cosine_similarity() {
394 let a = vec![1.0, 0.0, 0.0];
395 let b = vec![1.0, 0.0, 0.0];
396 assert!((cosine_similarity(&a, &b) - 1.0).abs() < 0.001);
397 }
398}