Skip to main content

holographic_memory/core/cognition/
abstraction.rs

1// Copyright 2024-2026 WritersLogic Contributors
2// SPDX-License-Identifier: Apache-2.0
3
4//! Concept invention via abstraction.
5//!
6//! When N entities share the same relation pattern, bundles their atom
7//! vectors (using `EntangledHVec::bundle`) to create a prototype/category atom.
8
9use fxhash::FxHashMap;
10
11use crate::core::atom_memory::AtomMemory;
12use crate::core::entangled::EntangledHVec;
13use crate::core::triple_store::TripleStore;
14
15/// A discovered abstraction: a set of entities that share a common pattern.
16#[derive(Clone, Debug)]
17pub struct Abstraction {
18    /// Auto-generated name for the concept (e.g., "concept:capital_of:subject").
19    pub name: String,
20    /// The relation pattern shared by these entities.
21    pub relation: String,
22    /// The role (subject or object) that the members fill.
23    pub role: AbstractionRole,
24    /// The member entities.
25    pub members: Vec<String>,
26    /// The bundled prototype vector.
27    pub prototype: EntangledHVec,
28}
29
30/// Which role the abstracted entities fill in the pattern.
31#[derive(Clone, Debug, PartialEq, Eq)]
32pub enum AbstractionRole {
33    Subject,
34    Object,
35}
36
37/// Creates concept atoms by bundling entities that share structural patterns.
38pub struct AbstractionEngine;
39
40impl AbstractionEngine {
41    /// Find groups of entities that share a relation pattern and bundle them.
42    ///
43    /// `min_members`: minimum number of entities sharing a relation to form a concept.
44    /// Returns abstractions for both subject-side and object-side groupings.
45    pub fn discover(
46        triple_store: &TripleStore,
47        atom_memory: &AtomMemory,
48        min_members: usize,
49    ) -> Vec<Abstraction> {
50        let snapshot = triple_store.snapshot();
51
52        // Group subjects by relation, objects by relation
53        let mut subjects_by_rel: FxHashMap<String, Vec<String>> = FxHashMap::default();
54        let mut objects_by_rel: FxHashMap<String, Vec<String>> = FxHashMap::default();
55
56        for t in &snapshot {
57            subjects_by_rel
58                .entry(t.relation_id.clone())
59                .or_default()
60                .push(t.subject_id.clone());
61            objects_by_rel
62                .entry(t.relation_id.clone())
63                .or_default()
64                .push(t.object_id.clone());
65        }
66
67        let mut results = Vec::new();
68
69        // Subject-side abstractions
70        for (relation, mut members) in subjects_by_rel {
71            members.sort();
72            members.dedup();
73            if members.len() < min_members {
74                continue;
75            }
76            if let Some(proto) = Self::bundle_members(&members, atom_memory) {
77                results.push(Abstraction {
78                    name: format!("concept:{}:subject", relation),
79                    relation: relation.clone(),
80                    role: AbstractionRole::Subject,
81                    members,
82                    prototype: proto,
83                });
84            }
85        }
86
87        // Object-side abstractions
88        for (relation, mut members) in objects_by_rel {
89            members.sort();
90            members.dedup();
91            if members.len() < min_members {
92                continue;
93            }
94            if let Some(proto) = Self::bundle_members(&members, atom_memory) {
95                results.push(Abstraction {
96                    name: format!("concept:{}:object", relation),
97                    relation,
98                    role: AbstractionRole::Object,
99                    members,
100                    prototype: proto,
101                });
102            }
103        }
104
105        results
106    }
107
108    /// Build a prototype vector from member atoms.
109    ///
110    /// For large groups (>= 10), uses majority-vote bundling.
111    /// For small groups, uses weighted index frequency counting with top-k
112    /// selection, since majority-vote at high sparsity (64/16384) produces
113    /// empty results when members are independent.
114    fn bundle_members(members: &[String], atom_memory: &AtomMemory) -> Option<EntangledHVec> {
115        let vecs: Vec<EntangledHVec> = members.iter().filter_map(|m| atom_memory.get(m)).collect();
116        if vecs.is_empty() {
117            return None;
118        }
119        let dim = vecs[0].dim;
120        if vecs.len() >= 10 {
121            return Some(EntangledHVec::bundle(&vecs));
122        }
123        // Weighted frequency: count how many vectors contain each index
124        let mut freq: FxHashMap<u32, u32> = FxHashMap::default();
125        for v in &vecs {
126            for &idx in v.indices() {
127                *freq.entry(idx).or_insert(0) += 1;
128            }
129        }
130        let target_count = (dim / 256).max(1);
131        let mut scored: Vec<(u32, u32)> = freq.into_iter().collect();
132        scored.sort_unstable_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
133        scored.truncate(target_count);
134        let mut indices: Vec<u32> = scored.into_iter().map(|(idx, _)| idx).collect();
135        indices.sort_unstable();
136        Some(EntangledHVec::from_indices(indices, dim))
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    #[test]
145    fn test_discover_abstractions() {
146        let dim = 16384;
147        let atom_mem = AtomMemory::new(dim, 3.0);
148        let triple_store = TripleStore::new();
149
150        // Insert atoms
151        atom_mem.get_or_insert("paris");
152        atom_mem.get_or_insert("berlin");
153        atom_mem.get_or_insert("tokyo");
154        atom_mem.get_or_insert("france");
155        atom_mem.get_or_insert("germany");
156        atom_mem.get_or_insert("japan");
157
158        // All three are capitals
159        triple_store.add("paris", "capital_of", "france", "c1");
160        triple_store.add("berlin", "capital_of", "germany", "c2");
161        triple_store.add("tokyo", "capital_of", "japan", "c3");
162
163        let abstractions = AbstractionEngine::discover(&triple_store, &atom_mem, 3);
164        assert!(!abstractions.is_empty());
165
166        // Should have a subject-side concept for capital_of (cities)
167        let cities = abstractions
168            .iter()
169            .find(|a| a.relation == "capital_of" && a.role == AbstractionRole::Subject);
170        assert!(cities.is_some());
171        let cities = cities.unwrap();
172        assert_eq!(cities.members.len(), 3);
173        assert!(
174            !cities.prototype.indices().is_empty(),
175            "Prototype should have indices via frequency counting"
176        );
177    }
178
179    #[test]
180    fn test_discover_below_threshold() {
181        let dim = 16384;
182        let atom_mem = AtomMemory::new(dim, 3.0);
183        let triple_store = TripleStore::new();
184
185        atom_mem.get_or_insert("a");
186        atom_mem.get_or_insert("b");
187        triple_store.add("a", "r", "b", "c1");
188
189        let abstractions = AbstractionEngine::discover(&triple_store, &atom_mem, 5);
190        assert!(abstractions.is_empty());
191    }
192
193    #[test]
194    fn test_discover_empty_store() {
195        let dim = 16384;
196        let atom_mem = AtomMemory::new(dim, 3.0);
197        let triple_store = TripleStore::new();
198
199        let abstractions = AbstractionEngine::discover(&triple_store, &atom_mem, 1);
200        assert!(abstractions.is_empty());
201    }
202}