Skip to main content

holographic_memory/core/cognition/
hypothesis.rs

1// Copyright 2024-2026 WritersLogic Contributors
2// SPDX-License-Identifier: Apache-2.0
3
4//! Hypothesis generation for knowledge gaps.
5//!
6//! Given a detected gap (entity missing a relation), examines peer data
7//! to propose the most likely filler. Uses Hopfield cleanup to map
8//! the bundled peer-filler vector to the nearest stored atom.
9
10use crate::core::atom_memory::AtomMemory;
11use crate::core::cognition::gaps::KnowledgeGap;
12use crate::core::entangled::EntangledHVec;
13use crate::core::triple_store::TripleStore;
14
15/// A proposed hypothesis to fill a knowledge gap.
16#[derive(Clone, Debug)]
17pub struct Hypothesis {
18    /// The entity with the gap.
19    pub entity: String,
20    /// The missing relation.
21    pub relation: String,
22    /// The proposed filler entity.
23    pub proposed_filler: String,
24    /// Confidence score from Hopfield cleanup (0.0 - 1.0).
25    pub confidence: f64,
26    /// How many peers contributed evidence.
27    pub evidence_count: usize,
28    /// Whether this hypothesis has been confirmed by a user.
29    pub confirmed: bool,
30}
31
32/// Proposes gap fillers based on peer data and Hopfield cleanup.
33pub struct HypothesisEngine;
34
35impl HypothesisEngine {
36    /// Generate hypotheses for a set of knowledge gaps.
37    ///
38    /// For each gap, looks at what objects peers have for the missing relation,
39    /// bundles those filler vectors, and runs Hopfield cleanup to find the
40    /// nearest clean atom.
41    pub fn propose(
42        gaps: &[KnowledgeGap],
43        triple_store: &TripleStore,
44        atom_memory: &AtomMemory,
45        beta: f64,
46        min_confidence: f64,
47    ) -> Vec<Hypothesis> {
48        let mut hypotheses = Vec::new();
49
50        for gap in gaps {
51            if let Some(hyp) = Self::propose_one(gap, triple_store, atom_memory, beta) {
52                if hyp.confidence >= min_confidence {
53                    hypotheses.push(hyp);
54                }
55            }
56        }
57
58        hypotheses.sort_by(|a, b| {
59            b.confidence
60                .partial_cmp(&a.confidence)
61                .unwrap_or(std::cmp::Ordering::Equal)
62        });
63        hypotheses
64    }
65
66    fn propose_one(
67        gap: &KnowledgeGap,
68        triple_store: &TripleStore,
69        atom_memory: &AtomMemory,
70        beta: f64,
71    ) -> Option<Hypothesis> {
72        // Collect what peers have as objects for this relation
73        let mut filler_vecs: Vec<EntangledHVec> = Vec::new();
74        let mut evidence_count = 0;
75
76        for peer in &gap.peers_with_relation {
77            let triples = triple_store.query(Some(peer), Some(&gap.missing_relation), None);
78            for t in &triples {
79                if let Some(vec) = atom_memory.get(&t.object_id) {
80                    filler_vecs.push(vec);
81                    evidence_count += 1;
82                }
83            }
84        }
85
86        if filler_vecs.is_empty() {
87            return None;
88        }
89
90        // Bundle peer fillers into a prototype.
91        // For small N at high sparsity, majority-vote may produce empty results.
92        // Fall back to the first filler if bundling yields nothing.
93        let bundled = EntangledHVec::bundle(&filler_vecs);
94        let probe = if bundled.indices().is_empty() {
95            filler_vecs[0].clone()
96        } else {
97            bundled
98        };
99
100        // Hopfield cleanup to find nearest stored atom
101        let result = atom_memory.cleanup(&probe, beta, 64, 3);
102        if !result.found {
103            return None;
104        }
105
106        Some(Hypothesis {
107            entity: gap.entity.clone(),
108            relation: gap.missing_relation.clone(),
109            proposed_filler: result.id,
110            confidence: result.confidence,
111            evidence_count,
112            confirmed: false,
113        })
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120    use crate::core::cognition::gaps::GapDetector;
121
122    #[test]
123    fn test_propose_hypothesis() {
124        let dim = 16384;
125        let atom_mem = AtomMemory::new(dim, 3.0);
126        let triple_store = TripleStore::new();
127
128        // Setup: cities in continents
129        atom_mem.get_or_insert("paris");
130        atom_mem.get_or_insert("berlin");
131        atom_mem.get_or_insert("tokyo");
132        atom_mem.get_or_insert("europe");
133        atom_mem.get_or_insert("asia");
134        atom_mem.get_or_insert("france");
135        atom_mem.get_or_insert("germany");
136        atom_mem.get_or_insert("japan");
137
138        triple_store.add("paris", "capital_of", "france", "c1");
139        triple_store.add("paris", "located_in", "europe", "c2");
140        triple_store.add("berlin", "capital_of", "germany", "c3");
141        triple_store.add("berlin", "located_in", "europe", "c4");
142        triple_store.add("tokyo", "capital_of", "japan", "c5");
143        // Tokyo is missing located_in
144
145        let gaps = GapDetector::detect(&triple_store, 1, 0.5);
146        let tokyo_gaps: Vec<_> = gaps
147            .iter()
148            .filter(|g| g.entity == "tokyo" && g.missing_relation == "located_in")
149            .cloned()
150            .collect();
151
152        if !tokyo_gaps.is_empty() {
153            let hypotheses =
154                HypothesisEngine::propose(&tokyo_gaps, &triple_store, &atom_mem, 24.0, 0.0);
155            // Peers (paris, berlin) both have located_in -> europe
156            // Bundle of [europe, europe] = europe, cleanup should find europe
157            if !hypotheses.is_empty() {
158                assert_eq!(hypotheses[0].entity, "tokyo");
159                assert_eq!(hypotheses[0].relation, "located_in");
160                assert_eq!(hypotheses[0].proposed_filler, "europe");
161                assert!(hypotheses[0].evidence_count >= 1);
162            }
163        }
164    }
165
166    #[test]
167    fn test_propose_empty_gaps() {
168        let dim = 16384;
169        let atom_mem = AtomMemory::new(dim, 3.0);
170        let triple_store = TripleStore::new();
171        let hypotheses = HypothesisEngine::propose(&[], &triple_store, &atom_mem, 24.0, 0.0);
172        assert!(hypotheses.is_empty());
173    }
174
175    #[test]
176    fn test_propose_min_confidence_filter() {
177        let dim = 16384;
178        let atom_mem = AtomMemory::new(dim, 3.0);
179        let triple_store = TripleStore::new();
180        // With impossibly high confidence threshold, nothing passes
181        let gaps = vec![KnowledgeGap {
182            entity: "x".to_string(),
183            missing_relation: "r".to_string(),
184            peers_with_relation: vec![],
185            peer_coverage: 1.0,
186        }];
187        let hypotheses = HypothesisEngine::propose(&gaps, &triple_store, &atom_mem, 24.0, 0.99);
188        assert!(hypotheses.is_empty());
189    }
190}