Skip to main content

writerslogic_warm/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 crate::core::hash::FastSet;
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 mut patterns = Vec::new();
45        for relation in triple_store.relation_ids() {
46            let mut subjects = FastSet::default();
47            let mut objects = FastSet::default();
48            let mut frequency = 0;
49            triple_store.visit(None, Some(&relation), None, |t| {
50                frequency += 1;
51                subjects.insert(t.subject_id.clone());
52                objects.insert(t.object_id.clone());
53                true
54            });
55            if frequency == 0 || frequency < min_freq {
56                continue;
57            }
58            let mut subjects: Vec<_> = subjects.into_iter().collect();
59            let mut objects: Vec<_> = objects.into_iter().collect();
60            subjects.sort();
61            objects.sort();
62            patterns.push(RelationPattern {
63                relation,
64                subjects,
65                objects,
66                frequency,
67            });
68        }
69
70        patterns.sort_by_key(|p| std::cmp::Reverse(p.frequency));
71        patterns
72    }
73
74    /// Find entities that co-occur as subjects across multiple relations.
75    /// Two subjects co-occur if they share at least `min_shared` relations.
76    pub fn find_co_occurrences(triple_store: &TripleStore, min_shared: usize) -> Vec<CoOccurrence> {
77        let mut entities = Vec::new();
78        for entity in triple_store.subject_ids() {
79            let mut relations = FastSet::default();
80            triple_store.visit(Some(&entity), None, None, |t| {
81                relations.insert(t.relation_id.clone());
82                true
83            });
84            if relations.is_empty() {
85                continue;
86            }
87            let mut relations: Vec<_> = relations.into_iter().collect();
88            relations.sort();
89            entities.push((entity, relations));
90        }
91        let mut results = Vec::new();
92
93        for i in 0..entities.len() {
94            for j in (i + 1)..entities.len() {
95                let shared = count_shared_sorted(&entities[i].1, &entities[j].1);
96                if shared >= min_shared {
97                    results.push(CoOccurrence {
98                        entity_a: entities[i].0.clone(),
99                        entity_b: entities[j].0.clone(),
100                        count: shared,
101                    });
102                }
103            }
104        }
105
106        results.sort_by_key(|r| std::cmp::Reverse(r.count));
107        results
108    }
109}
110
111/// Count shared elements between two sorted slices.
112fn count_shared_sorted(a: &[String], b: &[String]) -> usize {
113    let (mut i, mut j) = (0, 0);
114    let mut count = 0;
115    while i < a.len() && j < b.len() {
116        match a[i].cmp(&b[j]) {
117            std::cmp::Ordering::Less => i += 1,
118            std::cmp::Ordering::Greater => j += 1,
119            std::cmp::Ordering::Equal => {
120                count += 1;
121                i += 1;
122                j += 1;
123            }
124        }
125    }
126    count
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    fn make_store() -> TripleStore {
134        let store = TripleStore::new();
135        store.add("paris", "capital_of", "france", "c1");
136        store.add("berlin", "capital_of", "germany", "c2");
137        store.add("tokyo", "capital_of", "japan", "c3");
138        store.add("paris", "located_in", "europe", "c4");
139        store.add("berlin", "located_in", "europe", "c5");
140        store
141    }
142
143    #[test]
144    fn test_scan_relation_patterns() {
145        let store = make_store();
146        let patterns = PatternScanner::scan_relation_patterns(&store, 2);
147        assert!(!patterns.is_empty());
148        let capital = patterns
149            .iter()
150            .find(|p| p.relation == "capital_of")
151            .unwrap();
152        assert_eq!(capital.frequency, 3);
153        assert_eq!(capital.subjects.len(), 3);
154        assert_eq!(capital.objects.len(), 3);
155    }
156
157    #[test]
158    fn test_scan_below_threshold() {
159        let store = make_store();
160        let patterns = PatternScanner::scan_relation_patterns(&store, 10);
161        assert!(patterns.is_empty());
162    }
163
164    #[test]
165    fn test_co_occurrences() {
166        let store = make_store();
167        // paris and berlin both have capital_of and located_in
168        let co = PatternScanner::find_co_occurrences(&store, 2);
169        assert!(!co.is_empty());
170        let pair = co.iter().find(|c| {
171            (c.entity_a == "paris" && c.entity_b == "berlin")
172                || (c.entity_a == "berlin" && c.entity_b == "paris")
173        });
174        assert!(pair.is_some());
175        assert_eq!(pair.unwrap().count, 2);
176    }
177
178    #[test]
179    fn test_co_occurrences_empty() {
180        let store = TripleStore::new();
181        let co = PatternScanner::find_co_occurrences(&store, 1);
182        assert!(co.is_empty());
183    }
184}