Skip to main content

holographic_memory/core/cognition/
patterns.rs

1// Copyright 2024-2026 WritersLogic Contributors
2// SPDX-License-Identifier: Apache-2.0
3
4//! Pattern scanning over the triple store.
5//!
6//! Groups triples by relation, counts co-occurring subject/object atoms,
7//! and surfaces recurring structural regularities.
8
9use fxhash::FxHashMap;
10
11use crate::core::triple_store::TripleStore;
12
13/// A recurring pattern: entities sharing the same relation with similar peers.
14#[derive(Clone, Debug)]
15pub struct RelationPattern {
16    /// The relation this pattern was found on.
17    pub relation: String,
18    /// Subject atoms that appear in this relation.
19    pub subjects: Vec<String>,
20    /// Object atoms that appear in this relation.
21    pub objects: Vec<String>,
22    /// How many triples use this relation.
23    pub frequency: usize,
24}
25
26/// Co-occurrence of two entities appearing together across triples.
27#[derive(Clone, Debug)]
28pub struct CoOccurrence {
29    pub entity_a: String,
30    pub entity_b: String,
31    /// Number of triples where both appear (as subject/object in same relation group).
32    pub count: usize,
33}
34
35/// Finds recurring structural patterns in a triple store using read-only access.
36pub struct PatternScanner;
37
38impl PatternScanner {
39    /// Group all triples by relation and return patterns with frequency >= `min_freq`.
40    pub fn scan_relation_patterns(
41        triple_store: &TripleStore,
42        min_freq: usize,
43    ) -> Vec<RelationPattern> {
44        let snapshot = triple_store.snapshot();
45
46        // Group by relation
47        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            // Deduplicate
63            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    /// Find entities that co-occur as subjects across multiple relations.
83    /// Two subjects co-occur if they share at least `min_shared` relations.
84    pub fn find_co_occurrences(triple_store: &TripleStore, min_shared: usize) -> Vec<CoOccurrence> {
85        let snapshot = triple_store.snapshot();
86
87        // Build entity -> set of relations
88        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        // Deduplicate relation lists
97        for rels in entity_relations.values_mut() {
98            rels.sort();
99            rels.dedup();
100        }
101
102        // Pairwise comparison
103        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
124/// Count shared elements between two sorted slices.
125fn 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        // paris and berlin both have capital_of and located_in
181        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}