Skip to main content

holographic_memory/core/cognition/
loop.rs

1// Copyright 2024-2026 WritersLogic Contributors
2// SPDX-License-Identifier: Apache-2.0
3
4//! Background cognition loop.
5//!
6//! Runs pattern scanning, abstraction, gap detection, hypothesis generation,
7//! and analogy detection on a configurable interval. Uses READ-ONLY locks on
8//! meaning memory stores. Discovered insights are collected in a separate Vec.
9
10use parking_lot::RwLock;
11use std::sync::atomic::{AtomicBool, Ordering};
12use std::sync::Arc;
13use std::time::Duration;
14
15use crate::core::atom_memory::AtomMemory;
16use crate::core::cognition::abstraction::{Abstraction, AbstractionEngine};
17use crate::core::cognition::analogy::{Analogy, AnalogyDetector};
18use crate::core::cognition::gaps::{GapDetector, KnowledgeGap};
19use crate::core::cognition::hypothesis::{Hypothesis, HypothesisEngine};
20use crate::core::cognition::patterns::{PatternScanner, RelationPattern};
21use crate::core::triple_store::TripleStore;
22
23/// A single insight discovered by the cognition loop.
24#[derive(Clone, Debug)]
25pub enum Insight {
26    Pattern(RelationPattern),
27    Abstraction(Abstraction),
28    Gap(KnowledgeGap),
29    Hypothesis(Hypothesis),
30    Analogy(Analogy),
31}
32
33/// Configuration for the cognition loop.
34#[derive(Clone, Debug)]
35pub struct CognitionConfig {
36    /// Interval between cognition cycles.
37    pub interval: Duration,
38    /// Minimum frequency for a relation pattern to be reported.
39    pub min_pattern_freq: usize,
40    /// Minimum members to form an abstraction.
41    pub min_abstraction_members: usize,
42    /// Minimum shared relations to consider entities as peers (gap detection).
43    pub min_shared_relations: usize,
44    /// Minimum fraction of peers that must have a relation for it to be a gap.
45    pub min_peer_coverage: f64,
46    /// Hopfield beta for hypothesis cleanup.
47    pub hypothesis_beta: f64,
48    /// Minimum confidence for a hypothesis to be reported.
49    pub min_hypothesis_confidence: f64,
50    /// Minimum shared relations for analogy detection.
51    pub min_analogy_relations: usize,
52}
53
54impl Default for CognitionConfig {
55    fn default() -> Self {
56        Self {
57            interval: Duration::from_secs(60),
58            min_pattern_freq: 3,
59            min_abstraction_members: 3,
60            min_shared_relations: 2,
61            min_peer_coverage: 0.5,
62            hypothesis_beta: 24.0,
63            min_hypothesis_confidence: 0.3,
64            min_analogy_relations: 2,
65        }
66    }
67}
68
69/// Shared state for the cognition loop: insights and run flag.
70pub struct CognitionState {
71    insights: RwLock<Vec<Insight>>,
72    running: AtomicBool,
73    stop: AtomicBool,
74    cycle_count: std::sync::atomic::AtomicU64,
75}
76
77impl CognitionState {
78    fn new() -> Self {
79        Self {
80            insights: RwLock::new(Vec::new()),
81            running: AtomicBool::new(false),
82            stop: AtomicBool::new(false),
83            cycle_count: std::sync::atomic::AtomicU64::new(0),
84        }
85    }
86
87    pub fn is_running(&self) -> bool {
88        self.running.load(Ordering::SeqCst)
89    }
90
91    pub fn cycle_count(&self) -> u64 {
92        self.cycle_count.load(Ordering::SeqCst)
93    }
94
95    pub fn take_insights(&self) -> Vec<Insight> {
96        std::mem::take(&mut *self.insights.write())
97    }
98
99    pub fn insight_count(&self) -> usize {
100        self.insights.read().len()
101    }
102}
103
104/// The cognition loop. Spawns a background thread that periodically
105/// scans the triple store for patterns, gaps, and analogies.
106pub struct CognitionLoop {
107    state: Arc<CognitionState>,
108    handle: Option<std::thread::JoinHandle<()>>,
109}
110
111impl CognitionLoop {
112    /// Start the cognition loop in a background thread.
113    ///
114    /// The loop acquires read-only references to the atom memory and triple store.
115    /// It does NOT hold locks across cycles; it snapshots and releases.
116    pub fn start(
117        atom_memory: Arc<AtomMemory>,
118        triple_store: Arc<TripleStore>,
119        config: CognitionConfig,
120    ) -> Self {
121        let state = Arc::new(CognitionState::new());
122        let thread_state = Arc::clone(&state);
123
124        state.running.store(true, Ordering::SeqCst);
125
126        let handle = std::thread::Builder::new()
127            .name("hms-cognition".to_string())
128            .spawn(move || {
129                cognition_thread(&thread_state, &atom_memory, &triple_store, &config);
130            })
131            .expect("failed to spawn cognition thread");
132
133        Self {
134            state,
135            handle: Some(handle),
136        }
137    }
138
139    /// Request the loop to stop and wait for the thread to finish.
140    pub fn stop(&mut self) {
141        self.state.stop.store(true, Ordering::SeqCst);
142        if let Some(handle) = self.handle.take() {
143            let _ = handle.join();
144        }
145        self.state.running.store(false, Ordering::SeqCst);
146    }
147
148    /// Access the shared state (insights, cycle count, etc.).
149    pub fn state(&self) -> &Arc<CognitionState> {
150        &self.state
151    }
152
153    /// Run a single cognition cycle synchronously (for testing).
154    pub fn run_once(
155        atom_memory: &AtomMemory,
156        triple_store: &TripleStore,
157        config: &CognitionConfig,
158    ) -> Vec<Insight> {
159        run_cycle(atom_memory, triple_store, config)
160    }
161}
162
163impl Drop for CognitionLoop {
164    fn drop(&mut self) {
165        self.stop();
166    }
167}
168
169fn cognition_thread(
170    state: &CognitionState,
171    atom_memory: &AtomMemory,
172    triple_store: &TripleStore,
173    config: &CognitionConfig,
174) {
175    while !state.stop.load(Ordering::SeqCst) {
176        let new_insights = run_cycle(atom_memory, triple_store, config);
177
178        if !new_insights.is_empty() {
179            state.insights.write().extend(new_insights);
180        }
181
182        state.cycle_count.fetch_add(1, Ordering::SeqCst);
183
184        // Sleep in small increments so we can respond to stop quickly
185        let mut remaining = config.interval;
186        let tick = Duration::from_millis(100);
187        while remaining > Duration::ZERO && !state.stop.load(Ordering::SeqCst) {
188            let sleep_time = remaining.min(tick);
189            std::thread::sleep(sleep_time);
190            remaining = remaining.saturating_sub(sleep_time);
191        }
192    }
193
194    state.running.store(false, Ordering::SeqCst);
195}
196
197fn run_cycle(
198    atom_memory: &AtomMemory,
199    triple_store: &TripleStore,
200    config: &CognitionConfig,
201) -> Vec<Insight> {
202    let mut insights = Vec::new();
203
204    // 1. Pattern scanning
205    let patterns = PatternScanner::scan_relation_patterns(triple_store, config.min_pattern_freq);
206    for p in patterns {
207        insights.push(Insight::Pattern(p));
208    }
209
210    // 2. Abstraction
211    let abstractions =
212        AbstractionEngine::discover(triple_store, atom_memory, config.min_abstraction_members);
213    for a in abstractions {
214        insights.push(Insight::Abstraction(a));
215    }
216
217    // 3. Gap detection
218    let gaps = GapDetector::detect(
219        triple_store,
220        config.min_shared_relations,
221        config.min_peer_coverage,
222    );
223
224    // 4. Hypothesis generation (from gaps)
225    let hypotheses = HypothesisEngine::propose(
226        &gaps,
227        triple_store,
228        atom_memory,
229        config.hypothesis_beta,
230        config.min_hypothesis_confidence,
231    );
232    for h in hypotheses {
233        insights.push(Insight::Hypothesis(h));
234    }
235
236    // Push gaps after hypotheses so hypotheses reference valid gaps
237    for g in gaps {
238        insights.push(Insight::Gap(g));
239    }
240
241    // 5. Analogy detection
242    let analogies = AnalogyDetector::detect(triple_store, config.min_analogy_relations);
243    for a in analogies {
244        insights.push(Insight::Analogy(a));
245    }
246
247    insights
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253
254    fn make_test_data() -> (AtomMemory, TripleStore) {
255        let dim = 16384;
256        let atom_mem = AtomMemory::new(dim, 3.0);
257        let triple_store = TripleStore::new();
258
259        for city in &["paris", "berlin", "tokyo", "london", "madrid"] {
260            atom_mem.get_or_insert(city);
261        }
262        for country in &["france", "germany", "japan", "uk", "spain", "europe"] {
263            atom_mem.get_or_insert(country);
264        }
265
266        triple_store.add("paris", "capital_of", "france", "c1");
267        triple_store.add("berlin", "capital_of", "germany", "c2");
268        triple_store.add("tokyo", "capital_of", "japan", "c3");
269        triple_store.add("london", "capital_of", "uk", "c4");
270        triple_store.add("madrid", "capital_of", "spain", "c5");
271        triple_store.add("paris", "located_in", "europe", "c6");
272        triple_store.add("berlin", "located_in", "europe", "c7");
273        triple_store.add("london", "located_in", "europe", "c8");
274        triple_store.add("madrid", "located_in", "europe", "c9");
275        // tokyo missing located_in
276
277        (atom_mem, triple_store)
278    }
279
280    #[test]
281    fn test_run_once() {
282        let (atom_mem, triple_store) = make_test_data();
283        let config = CognitionConfig::default();
284        let insights = CognitionLoop::run_once(&atom_mem, &triple_store, &config);
285        assert!(!insights.is_empty());
286
287        // Should find patterns
288        assert!(insights.iter().any(|i| matches!(i, Insight::Pattern(_))));
289    }
290
291    #[test]
292    fn test_run_once_finds_gaps() {
293        let (atom_mem, triple_store) = make_test_data();
294        let config = CognitionConfig {
295            min_shared_relations: 1,
296            min_peer_coverage: 0.5,
297            ..Default::default()
298        };
299        let insights = CognitionLoop::run_once(&atom_mem, &triple_store, &config);
300
301        let has_gap = insights.iter().any(|i| {
302            if let Insight::Gap(g) = i {
303                g.entity == "tokyo" && g.missing_relation == "located_in"
304            } else {
305                false
306            }
307        });
308        assert!(has_gap, "Should detect tokyo missing located_in");
309    }
310
311    #[test]
312    fn test_background_loop_start_stop() {
313        let (atom_mem, triple_store) = make_test_data();
314        let config = CognitionConfig {
315            interval: Duration::from_millis(50),
316            ..Default::default()
317        };
318
319        let mut cognition =
320            CognitionLoop::start(Arc::new(atom_mem), Arc::new(triple_store), config);
321
322        assert!(cognition.state().is_running());
323
324        // Let it run a couple cycles
325        std::thread::sleep(Duration::from_millis(200));
326
327        let cycles = cognition.state().cycle_count();
328        assert!(
329            cycles >= 1,
330            "Should have completed at least 1 cycle, got {}",
331            cycles
332        );
333
334        cognition.stop();
335        assert!(!cognition.state().is_running());
336    }
337
338    #[test]
339    fn test_take_insights() {
340        let (atom_mem, triple_store) = make_test_data();
341        let config = CognitionConfig {
342            interval: Duration::from_millis(50),
343            ..Default::default()
344        };
345
346        let mut cognition =
347            CognitionLoop::start(Arc::new(atom_mem), Arc::new(triple_store), config);
348
349        std::thread::sleep(Duration::from_millis(200));
350
351        let insights = cognition.state().take_insights();
352        assert!(!insights.is_empty());
353
354        // After take, count should be 0 (or only new ones from ongoing cycle)
355        // Stop first to prevent race
356        cognition.stop();
357        let remaining = cognition.state().take_insights();
358        // This is fine - just checking take works
359        let _ = remaining;
360    }
361
362    #[test]
363    fn test_empty_store_no_panic() {
364        let atom_mem = AtomMemory::new(16384, 3.0);
365        let triple_store = TripleStore::new();
366        let config = CognitionConfig::default();
367        let insights = CognitionLoop::run_once(&atom_mem, &triple_store, &config);
368        assert!(insights.is_empty());
369    }
370}