Skip to main content

holographic_memory/core/engine/
concepts.rs

1// Copyright 2024-2026 WritersLogic Contributors
2// SPDX-License-Identifier: Apache-2.0
3
4use fxhash::{FxHashMap, FxHashSet};
5
6use super::HmsCore;
7use crate::core::entangled::EntangledHVec;
8use crate::core::types::ConceptCandidate;
9
10impl HmsCore {
11    /// Cluster similar vectors and return concept candidates with centroids and coherence scores.
12    ///
13    /// Uses posting-list candidate generation: only vector pairs sharing at least
14    /// one active index are compared, avoiding O(n²) all-pairs at high sparsity.
15    pub fn synthesize_concepts(&self) -> Vec<ConceptCandidate> {
16        let cfg = &self.config.concepts;
17        let mut all_ids = Vec::new();
18        let mut all_vectors = Vec::new();
19
20        let shards = self.shards.read();
21        shards.for_each_shard(|shard| {
22            let (ids, vectors) = shard.load_all_vectors();
23            all_ids.extend(ids);
24            all_vectors.extend(vectors);
25        });
26
27        if all_ids.len() < cfg.min_cluster_size {
28            return vec![];
29        }
30
31        let n = all_vectors.len();
32        let mut used = vec![false; n];
33        let mut concepts = Vec::new();
34
35        // For large collections, use posting-list candidate generation
36        // to avoid O(n²). For small collections, brute-force is fine.
37        let use_postings = n > 500;
38        let neighbors: Vec<FxHashSet<usize>> = if use_postings {
39            let mut postings: FxHashMap<u32, Vec<usize>> = FxHashMap::default();
40            for (i, vec) in all_vectors.iter().enumerate() {
41                for &idx in vec.indices() {
42                    postings.entry(idx).or_default().push(i);
43                }
44            }
45            let mut nbrs = vec![FxHashSet::default(); n];
46            for list in postings.values() {
47                for (pos, &a) in list.iter().enumerate() {
48                    for &b in &list[pos + 1..] {
49                        nbrs[a].insert(b);
50                        nbrs[b].insert(a);
51                    }
52                }
53            }
54            nbrs
55        } else {
56            Vec::new()
57        };
58
59        for i in 0..n {
60            if used[i] {
61                continue;
62            }
63            let mut cluster = vec![i];
64            if use_postings {
65                for &j in &neighbors[i] {
66                    if !used[j]
67                        && all_vectors[i].similarity(&all_vectors[j]) > cfg.similarity_threshold
68                    {
69                        cluster.push(j);
70                    }
71                }
72            } else {
73                for j in (i + 1)..n {
74                    if !used[j]
75                        && all_vectors[i].similarity(&all_vectors[j]) > cfg.similarity_threshold
76                    {
77                        cluster.push(j);
78                    }
79                }
80            }
81            if cluster.len() >= cfg.min_cluster_size {
82                for &idx in &cluster {
83                    used[idx] = true;
84                }
85                let cluster_vecs: Vec<&EntangledHVec> =
86                    cluster.iter().map(|&idx| &all_vectors[idx]).collect();
87                let centroid = self.bundle(&cluster_vecs);
88                let coherence: f64 = cluster_vecs
89                    .iter()
90                    .map(|v| v.similarity(&centroid))
91                    .sum::<f64>()
92                    / cluster_vecs.len() as f64;
93                let member_ids: Vec<String> =
94                    cluster.iter().map(|&idx| all_ids[idx].clone()).collect();
95
96                concepts.push(ConceptCandidate {
97                    centroid_id: member_ids.first().cloned().unwrap_or_default(),
98                    member_count: cluster.len() as u32,
99                    coherence,
100                    member_ids,
101                    stable: false,
102                });
103            }
104        }
105
106        // Centroid refinement: k-means-style reassignment (up to 3 iterations).
107        // Recompute centroids and reassign vectors to closest centroid.
108        if concepts.len() >= 2 {
109            let cluster_indices: Vec<usize> = (0..n).filter(|&i| used[i]).collect();
110
111            // Build initial assignment: vector index -> concept index
112            let mut assignment: FxHashMap<usize, usize> = FxHashMap::default();
113            for (ci, concept) in concepts.iter().enumerate() {
114                for mid in &concept.member_ids {
115                    if let Some(pos) = all_ids.iter().position(|id| id == mid) {
116                        assignment.insert(pos, ci);
117                    }
118                }
119            }
120
121            let mut centroids: Vec<EntangledHVec> = concepts
122                .iter()
123                .enumerate()
124                .map(|(ci, _)| {
125                    let members: Vec<&EntangledHVec> = cluster_indices
126                        .iter()
127                        .filter(|&&vi| assignment.get(&vi) == Some(&ci))
128                        .map(|&vi| &all_vectors[vi])
129                        .collect();
130                    self.bundle(&members)
131                })
132                .collect();
133
134            let mut stable = true;
135            for _iter in 0..3 {
136                let mut changed = false;
137                for &vi in &cluster_indices {
138                    let mut best_ci = assignment[&vi];
139                    let mut best_sim = all_vectors[vi].similarity(&centroids[best_ci]);
140                    for (ci, centroid) in centroids.iter().enumerate() {
141                        let sim = all_vectors[vi].similarity(centroid);
142                        if sim > best_sim {
143                            best_sim = sim;
144                            best_ci = ci;
145                        }
146                    }
147                    if best_ci != assignment[&vi] {
148                        assignment.insert(vi, best_ci);
149                        changed = true;
150                    }
151                }
152                if !changed {
153                    stable = true;
154                    break;
155                }
156                stable = false;
157                // Recompute centroids after reassignment
158                for (ci, centroid) in centroids.iter_mut().enumerate() {
159                    let members: Vec<&EntangledHVec> = cluster_indices
160                        .iter()
161                        .filter(|&&vi| assignment.get(&vi) == Some(&ci))
162                        .map(|&vi| &all_vectors[vi])
163                        .collect();
164                    if !members.is_empty() {
165                        *centroid = self.bundle(&members);
166                    }
167                }
168            }
169
170            // Rebuild concepts from refined assignments
171            for (ci, concept) in concepts.iter_mut().enumerate() {
172                let members: Vec<usize> = cluster_indices
173                    .iter()
174                    .filter(|&&vi| assignment.get(&vi) == Some(&ci))
175                    .copied()
176                    .collect();
177                if members.is_empty() {
178                    concept.member_count = 0;
179                    concept.member_ids.clear();
180                    concept.coherence = 0.0;
181                    concept.stable = stable;
182                    continue;
183                }
184                let member_vecs: Vec<&EntangledHVec> =
185                    members.iter().map(|&vi| &all_vectors[vi]).collect();
186                let coherence: f64 = member_vecs
187                    .iter()
188                    .map(|v| v.similarity(&centroids[ci]))
189                    .sum::<f64>()
190                    / member_vecs.len() as f64;
191                concept.member_ids = members.iter().map(|&vi| all_ids[vi].clone()).collect();
192                concept.centroid_id = concept.member_ids.first().cloned().unwrap_or_default();
193                concept.member_count = members.len() as u32;
194                concept.coherence = coherence;
195                concept.stable = stable;
196            }
197
198            // Remove empty concepts
199            concepts.retain(|c| c.member_count > 0);
200        } else {
201            // Single or zero concepts: trivially stable
202            for concept in &mut concepts {
203                concept.stable = true;
204            }
205        }
206
207        concepts
208    }
209}