holographic_memory/core/cognition/
patterns.rs1use fxhash::FxHashMap;
10
11use crate::core::triple_store::TripleStore;
12
13#[derive(Clone, Debug)]
15pub struct RelationPattern {
16 pub relation: String,
18 pub subjects: Vec<String>,
20 pub objects: Vec<String>,
22 pub frequency: usize,
24}
25
26#[derive(Clone, Debug)]
28pub struct CoOccurrence {
29 pub entity_a: String,
30 pub entity_b: String,
31 pub count: usize,
33}
34
35pub struct PatternScanner;
37
38impl PatternScanner {
39 pub fn scan_relation_patterns(
41 triple_store: &TripleStore,
42 min_freq: usize,
43 ) -> Vec<RelationPattern> {
44 let snapshot = triple_store.snapshot();
45
46 let mut by_relation: FxHashMap<String, (Vec<String>, Vec<String>)> = FxHashMap::default();
48 for t in &snapshot {
49 let entry = by_relation
50 .entry(t.relation_id.clone())
51 .or_insert_with(|| (Vec::new(), Vec::new()));
52 entry.0.push(t.subject_id.clone());
53 entry.1.push(t.object_id.clone());
54 }
55
56 let mut patterns = Vec::new();
57 for (relation, (subjects, objects)) in by_relation {
58 let freq = subjects.len();
59 if freq < min_freq {
60 continue;
61 }
62 let mut unique_subjects = subjects;
64 unique_subjects.sort();
65 unique_subjects.dedup();
66 let mut unique_objects = objects;
67 unique_objects.sort();
68 unique_objects.dedup();
69
70 patterns.push(RelationPattern {
71 relation,
72 subjects: unique_subjects,
73 objects: unique_objects,
74 frequency: freq,
75 });
76 }
77
78 patterns.sort_by_key(|p| std::cmp::Reverse(p.frequency));
79 patterns
80 }
81
82 pub fn find_co_occurrences(triple_store: &TripleStore, min_shared: usize) -> Vec<CoOccurrence> {
85 let snapshot = triple_store.snapshot();
86
87 let mut entity_relations: FxHashMap<String, Vec<String>> = FxHashMap::default();
89 for t in &snapshot {
90 entity_relations
91 .entry(t.subject_id.clone())
92 .or_default()
93 .push(t.relation_id.clone());
94 }
95
96 for rels in entity_relations.values_mut() {
98 rels.sort();
99 rels.dedup();
100 }
101
102 let entities: Vec<(String, Vec<String>)> = entity_relations.into_iter().collect();
104 let mut results = Vec::new();
105
106 for i in 0..entities.len() {
107 for j in (i + 1)..entities.len() {
108 let shared = count_shared_sorted(&entities[i].1, &entities[j].1);
109 if shared >= min_shared {
110 results.push(CoOccurrence {
111 entity_a: entities[i].0.clone(),
112 entity_b: entities[j].0.clone(),
113 count: shared,
114 });
115 }
116 }
117 }
118
119 results.sort_by_key(|r| std::cmp::Reverse(r.count));
120 results
121 }
122}
123
124fn count_shared_sorted(a: &[String], b: &[String]) -> usize {
126 let (mut i, mut j) = (0, 0);
127 let mut count = 0;
128 while i < a.len() && j < b.len() {
129 match a[i].cmp(&b[j]) {
130 std::cmp::Ordering::Less => i += 1,
131 std::cmp::Ordering::Greater => j += 1,
132 std::cmp::Ordering::Equal => {
133 count += 1;
134 i += 1;
135 j += 1;
136 }
137 }
138 }
139 count
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 fn make_store() -> TripleStore {
147 let store = TripleStore::new();
148 store.add("paris", "capital_of", "france", "c1");
149 store.add("berlin", "capital_of", "germany", "c2");
150 store.add("tokyo", "capital_of", "japan", "c3");
151 store.add("paris", "located_in", "europe", "c4");
152 store.add("berlin", "located_in", "europe", "c5");
153 store
154 }
155
156 #[test]
157 fn test_scan_relation_patterns() {
158 let store = make_store();
159 let patterns = PatternScanner::scan_relation_patterns(&store, 2);
160 assert!(!patterns.is_empty());
161 let capital = patterns
162 .iter()
163 .find(|p| p.relation == "capital_of")
164 .unwrap();
165 assert_eq!(capital.frequency, 3);
166 assert_eq!(capital.subjects.len(), 3);
167 assert_eq!(capital.objects.len(), 3);
168 }
169
170 #[test]
171 fn test_scan_below_threshold() {
172 let store = make_store();
173 let patterns = PatternScanner::scan_relation_patterns(&store, 10);
174 assert!(patterns.is_empty());
175 }
176
177 #[test]
178 fn test_co_occurrences() {
179 let store = make_store();
180 let co = PatternScanner::find_co_occurrences(&store, 2);
182 assert!(!co.is_empty());
183 let pair = co.iter().find(|c| {
184 (c.entity_a == "paris" && c.entity_b == "berlin")
185 || (c.entity_a == "berlin" && c.entity_b == "paris")
186 });
187 assert!(pair.is_some());
188 assert_eq!(pair.unwrap().count, 2);
189 }
190
191 #[test]
192 fn test_co_occurrences_empty() {
193 let store = TripleStore::new();
194 let co = PatternScanner::find_co_occurrences(&store, 1);
195 assert!(co.is_empty());
196 }
197}