holographic_memory/core/cognition/
refiner.rs1use fxhash::FxHashMap;
16
17use crate::core::atom_memory::AtomMemory;
18use crate::core::entangled::EntangledHVec;
19use crate::core::triple_store::TripleStore;
20
21#[derive(Clone, Debug, Default)]
23pub struct RefinementReport {
24 pub atoms_refined: usize,
26 pub atoms_skipped: usize,
28 pub avg_context_depth: f64,
30}
31
32#[derive(Clone, Debug)]
34pub struct RefinerConfig {
35 pub alpha: f64,
38 pub min_context_relations: usize,
42 pub min_peers_per_relation: usize,
45}
46
47impl Default for RefinerConfig {
48 fn default() -> Self {
49 Self {
50 alpha: 0.15,
51 min_context_relations: 2,
52 min_peers_per_relation: 2,
53 }
54 }
55}
56
57pub struct DistributionalRefiner;
67
68impl DistributionalRefiner {
69 pub fn refine(
75 atom_memory: &AtomMemory,
76 triple_store: &TripleStore,
77 config: &RefinerConfig,
78 ) -> RefinementReport {
79 let snapshot = triple_store.snapshot();
80 if snapshot.is_empty() {
81 return RefinementReport::default();
82 }
83
84 let mut atom_relations: FxHashMap<String, Vec<String>> = FxHashMap::default();
86 for t in &snapshot {
87 atom_relations
88 .entry(t.subject_id.clone())
89 .or_default()
90 .push(t.relation_id.clone());
91 }
92 for rels in atom_relations.values_mut() {
94 rels.sort();
95 rels.dedup();
96 }
97
98 let mut relation_subjects: FxHashMap<String, Vec<String>> = FxHashMap::default();
100 for t in &snapshot {
101 relation_subjects
102 .entry(t.relation_id.clone())
103 .or_default()
104 .push(t.subject_id.clone());
105 }
106 for subjects in relation_subjects.values_mut() {
107 subjects.sort();
108 subjects.dedup();
109 }
110
111 let mut report = RefinementReport::default();
112 let mut total_context_depth: usize = 0;
113
114 for (atom_id, relations) in &atom_relations {
116 if relations.len() < config.min_context_relations {
117 report.atoms_skipped += 1;
118 continue;
119 }
120
121 let original = match atom_memory.get(atom_id) {
122 Some(v) => v,
123 None => continue,
124 };
125
126 let mut peer_index_freq: FxHashMap<u32, f64> = FxHashMap::default();
128 let mut contributing_relations = 0usize;
129
130 for rel in relations {
131 let peers = match relation_subjects.get(rel) {
132 Some(p) => p,
133 None => continue,
134 };
135 let other_peers: Vec<&String> = peers.iter().filter(|p| *p != atom_id).collect();
137 if other_peers.len() < config.min_peers_per_relation {
138 continue;
139 }
140 contributing_relations += 1;
141
142 let weight = 1.0 / other_peers.len() as f64;
144 for peer_id in &other_peers {
145 if let Some(peer_vec) = atom_memory.get(peer_id) {
146 for &idx in peer_vec.indices() {
147 *peer_index_freq.entry(idx).or_insert(0.0) += weight;
148 }
149 }
150 }
151 }
152
153 if contributing_relations == 0 {
154 report.atoms_skipped += 1;
155 continue;
156 }
157
158 total_context_depth += contributing_relations;
159
160 let dim = original.dim;
165 let target_count = (dim / 256).max(1);
166 let ctx_slots = ((target_count as f64) * config.alpha).round() as usize;
167 let orig_slots = target_count.saturating_sub(ctx_slots);
168
169 let orig_set: fxhash::FxHashSet<u32> = original.indices().iter().copied().collect();
171 let mut ctx_scored: Vec<(u32, f64)> = peer_index_freq
172 .iter()
173 .filter(|(idx, _)| !orig_set.contains(idx))
174 .map(|(&idx, &freq)| (idx, freq))
175 .collect();
176 ctx_scored.sort_unstable_by(|a, b| {
177 b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)
178 });
179 ctx_scored.truncate(ctx_slots);
180
181 let mut orig_indices: Vec<u32> = original.indices().to_vec();
184 orig_indices.truncate(orig_slots);
185
186 let mut new_indices: Vec<u32> = orig_indices;
188 new_indices.extend(ctx_scored.iter().map(|(idx, _)| *idx));
189 new_indices.sort_unstable();
190 new_indices.dedup();
191 new_indices.truncate(target_count);
192
193 let refined = EntangledHVec::from_indices(new_indices, dim);
194
195 if original.similarity(&refined) < 0.999 {
197 atom_memory.delete(atom_id);
198 atom_memory.load_atom(atom_id.clone(), refined);
199 report.atoms_refined += 1;
200 } else {
201 report.atoms_skipped += 1;
202 }
203 }
204
205 if report.atoms_refined > 0 {
206 report.avg_context_depth = total_context_depth as f64 / report.atoms_refined as f64;
207 atom_memory.rebuild_indices();
208 }
209
210 report
211 }
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217
218 #[test]
219 fn test_refine_converges_cooccurring_atoms() {
220 let dim = 16384;
221 let atom_mem = AtomMemory::new(dim, 3.0);
222 let triple_store = TripleStore::new();
223
224 atom_mem.get_or_insert("paris");
226 atom_mem.get_or_insert("berlin");
227 atom_mem.get_or_insert("tokyo");
228 atom_mem.get_or_insert("france");
229 atom_mem.get_or_insert("germany");
230 atom_mem.get_or_insert("japan");
231 atom_mem.get_or_insert("europe");
232
233 triple_store.add("paris", "capital_of", "france", "c1");
235 triple_store.add("berlin", "capital_of", "germany", "c2");
236 triple_store.add("tokyo", "capital_of", "japan", "c3");
237 triple_store.add("paris", "located_in", "europe", "c4");
238 triple_store.add("berlin", "located_in", "europe", "c5");
239
240 let paris_before = atom_mem.get("paris").unwrap();
241 let berlin_before = atom_mem.get("berlin").unwrap();
242 let sim_before = paris_before.similarity(&berlin_before);
243
244 let config = RefinerConfig {
246 alpha: 0.3,
247 min_context_relations: 2,
248 min_peers_per_relation: 2,
249 };
250 let report = DistributionalRefiner::refine(&atom_mem, &triple_store, &config);
251
252 assert!(
253 report.atoms_refined > 0,
254 "Should refine at least some atoms"
255 );
256
257 let paris_after = atom_mem.get("paris").unwrap();
258 let berlin_after = atom_mem.get("berlin").unwrap();
259 let sim_after = paris_after.similarity(&berlin_after);
260
261 assert!(
262 sim_after > sim_before,
263 "paris and berlin should become more similar after refinement: before={:.4}, after={:.4}",
264 sim_before,
265 sim_after
266 );
267 }
268
269 #[test]
270 fn test_refine_skips_low_context_atoms() {
271 let dim = 16384;
272 let atom_mem = AtomMemory::new(dim, 3.0);
273 let triple_store = TripleStore::new();
274
275 atom_mem.get_or_insert("lonely");
276 atom_mem.get_or_insert("x");
277 triple_store.add("lonely", "r", "x", "c1");
278
279 let config = RefinerConfig {
280 min_context_relations: 2,
281 ..Default::default()
282 };
283 let report = DistributionalRefiner::refine(&atom_mem, &triple_store, &config);
284 assert_eq!(report.atoms_refined, 0);
285 assert!(report.atoms_skipped > 0);
286 }
287
288 #[test]
289 fn test_refine_empty_store() {
290 let dim = 16384;
291 let atom_mem = AtomMemory::new(dim, 3.0);
292 let triple_store = TripleStore::new();
293 let config = RefinerConfig::default();
294 let report = DistributionalRefiner::refine(&atom_mem, &triple_store, &config);
295 assert_eq!(report.atoms_refined, 0);
296 assert_eq!(report.atoms_skipped, 0);
297 }
298
299 #[test]
300 fn test_refine_preserves_vector_sparsity() {
301 let dim = 16384;
302 let atom_mem = AtomMemory::new(dim, 3.0);
303 let triple_store = TripleStore::new();
304
305 for city in &["a", "b", "c", "d", "e"] {
306 atom_mem.get_or_insert(city);
307 }
308 for country in &["x", "y", "z", "w", "v"] {
309 atom_mem.get_or_insert(country);
310 }
311 atom_mem.get_or_insert("continent");
312
313 for (city, country) in [("a", "x"), ("b", "y"), ("c", "z"), ("d", "w"), ("e", "v")] {
315 triple_store.add(city, "capital_of", country, &format!("c_{}", city));
316 triple_store.add(city, "in", "continent", &format!("l_{}", city));
317 }
318
319 let config = RefinerConfig {
320 alpha: 0.3,
321 min_context_relations: 2,
322 min_peers_per_relation: 2,
323 };
324 let report = DistributionalRefiner::refine(&atom_mem, &triple_store, &config);
325
326 let target = dim / 256;
328 for city in &["a", "b", "c", "d", "e"] {
329 let vec = atom_mem.get(city).unwrap();
330 assert!(
331 vec.indices().len() <= target + 1,
332 "Refined vector for {} has {} indices, expected <= {}",
333 city,
334 vec.indices().len(),
335 target + 1
336 );
337 }
338 assert!(report.atoms_refined >= 3);
339 }
340
341 #[test]
342 fn test_multiple_refinement_rounds() {
343 let dim = 16384;
344 let atom_mem = AtomMemory::new(dim, 3.0);
345 let triple_store = TripleStore::new();
346
347 atom_mem.get_or_insert("a");
348 atom_mem.get_or_insert("b");
349 atom_mem.get_or_insert("c");
350 atom_mem.get_or_insert("x");
351 atom_mem.get_or_insert("y");
352
353 triple_store.add("a", "r1", "x", "c1");
354 triple_store.add("b", "r1", "y", "c2");
355 triple_store.add("c", "r1", "x", "c3");
356 triple_store.add("a", "r2", "y", "c4");
357 triple_store.add("b", "r2", "x", "c5");
358 triple_store.add("c", "r2", "y", "c6");
359
360 let config = RefinerConfig {
361 alpha: 0.2,
362 min_context_relations: 2,
363 min_peers_per_relation: 2,
364 };
365
366 let sim_before = atom_mem
367 .get("a")
368 .unwrap()
369 .similarity(&atom_mem.get("b").unwrap());
370
371 for _ in 0..3 {
373 DistributionalRefiner::refine(&atom_mem, &triple_store, &config);
374 }
375
376 let sim_after = atom_mem
377 .get("a")
378 .unwrap()
379 .similarity(&atom_mem.get("b").unwrap());
380 assert!(
381 sim_after > sim_before,
382 "Multiple rounds should increase similarity: before={:.4}, after={:.4}",
383 sim_before,
384 sim_after
385 );
386 }
387}