Skip to main content

reliary_memory/
lib.rs

1/// HDC memory: 10K-bit hypervectors with Hebbian updates, SQLite persistence.
2/// M1: Bit-packed hypervectors — 10K bipolar {-1,+1} values stored as 10K bits
3/// in a Vec<u64> (156 words = 1248 bytes vs 10K bytes = 8× memory reduction).
4/// Dot product uses XOR + popcount = 64× fewer loop iterations.
5use rustc_hash::FxHashMap;
6use rand::{Rng, SeedableRng};
7use rand_chacha::ChaCha8Rng;
8
9/// Bit-packed hypervector: each bit represents a bipolar value (0 → -1, 1 → +1).
10/// 10,000 dims = 157 u64 words = 1,256 bytes (vs 10,000 bytes as Vec<i8>).
11#[derive(Debug, Clone, PartialEq)]
12pub struct Hypervector {
13    bits: Vec<u64>,
14    dims: usize,
15}
16
17impl Hypervector {
18    pub fn new(dims: usize) -> Self {
19        let words = dims.div_ceil(64);
20        Self { bits: vec![0u64; words], dims }
21    }
22
23    /// Get bipolar value at index: returns -1 or +1.
24    #[inline(always)]
25    pub fn bits_get(&self, word: usize) -> u64 {
26        *self.bits.get(word).unwrap_or(&0)
27    }
28
29    pub fn get(&self, idx: usize) -> i8 {
30        if idx >= self.dims { return 0; }
31        let word = idx / 64;
32        let bit = idx % 64;
33        if (self.bits[word] >> bit) & 1 == 1 { 1 } else { -1 }
34    }
35
36    /// Set bipolar value at index: -1 → clear bit, +1 → set bit.
37    #[inline(always)]
38    pub fn set(&mut self, idx: usize, val: i8) {
39        if idx >= self.dims { return; }
40        let word = idx / 64;
41        let bit = idx % 64;
42        if val >= 0 {
43            self.bits[word] |= 1u64 << bit;
44        } else {
45            self.bits[word] &= !(1u64 << bit);
46        }
47    }
48
49    /// M1: XOR + popcount dot product — 64× fewer iterations than element-wise.
50    /// For bipolar {-1,+1}: dot = (matching_bits - differing_bits) = dims - 2*hamming_distance.
51    #[inline(always)]
52    pub fn dot(&self, other: &Hypervector) -> i64 {
53        let min_words = self.bits.len().min(other.bits.len());
54        let mut hamming: u64 = 0;
55        for i in 0..min_words {
56            hamming += (self.bits[i] ^ other.bits[i]).count_ones() as u64;
57        }
58        let dims = self.dims.min(other.dims) as i64;
59        dims - 2 * (hamming as i64)
60    }
61
62    /// M3: Precomputed norm — for bipolar HVs, norm = sqrt(dims) ≈ constant.
63    /// No per-call sum-of-squares needed.
64    #[inline(always)]
65    pub fn norm(&self) -> f64 {
66        (self.dims as f64).sqrt()
67    }
68
69    /// Cosine similarity = dot / (norm_a * norm_b).
70    /// For bipolar HVs: cosine = 1 - 2*hamming/dims.
71    #[inline(always)]
72    pub fn cosine(&self, other: &Hypervector) -> f64 {
73        let d = self.dot(other) as f64;
74        let n = self.norm() * other.norm();
75        if n == 0.0 { 0.0 } else { d / n }
76    }
77
78    /// Bundle: XOR-based majority vote for bipolar HVs.
79    /// Sum via i32 accumulation, then sign → set/clear bit.
80    pub fn bundle_into(&mut self, other: &Hypervector) {
81        // Accumulate per-word sign via popcount majority.
82        // For bipolar, bundle = sign(sum of +1/-1). We use a temp accumulator.
83        let words = self.bits.len().min(other.bits.len());
84        let mut acc: Vec<i32> = Vec::with_capacity(64);
85        for w in 0..words {
86            // Count +1s in self word and other word
87            // For each bit: self contributes +1 (set) or -1 (clear), same for other.
88            // Sum per bit = (self_val + other_val). We need sign of sum.
89            // Use a per-bit accumulator array (the per-word popcount sums
90            // below are only informative; the per-bit loop computes the real
91            // majority).
92            acc.clear();
93            acc.resize(64, 0i32);
94            for (b, slot) in acc.iter_mut().enumerate() {
95                let s = if (self.bits[w] >> b) & 1 == 1 { 1 } else { -1 };
96                let o = if (other.bits[w] >> b) & 1 == 1 { 1 } else { -1 };
97                *slot = s + o;
98            }
99            // V58: majority INCLUDES the current self vote — acc[b] is
100            // (self + other) ∈ {-2,0,2}; tie (0) keeps self's bit.
101            let mut new_word = 0u64;
102            for (b, slot) in acc.iter().enumerate() {
103                if *slot > 0 || (*slot == 0 && (self.bits[w] >> b) & 1 == 1) {
104                    new_word |= 1u64 << b;
105                }
106            }
107            self.bits[w] = new_word;
108        }
109    }
110}
111
112/// Generate a deterministic random hypervector from a seed.
113/// M1: Uses bit-packing — 1 bit per dim instead of 1 byte.
114pub fn make_hv(seed: u64, dims: usize) -> Hypervector {
115    let mut rng = ChaCha8Rng::seed_from_u64(seed);
116    let mut hv = Hypervector::new(dims);
117    for i in 0..dims {
118        hv.set(i, if rng.gen_bool(0.5) { 1 } else { -1 });
119    }
120    hv
121}
122
123/// Bundle: sum + bipolar clamp (legacy API — delegates to bit-packed).
124pub fn bundle(hv: &mut Hypervector, other: &Hypervector) {
125    hv.bundle_into(other);
126}
127
128/// Bipolar clamp: for bit-packed HVs, values are already bipolar {-1,+1}.
129/// This is a no-op — bit-packing enforces bipolar by construction.
130#[inline(always)]
131pub fn bipolar_clamp(_hv: &mut Hypervector) {
132    // No-op: bit-packed HVs are always bipolar.
133}
134
135/// Dot product of two hypervectors (cosine similarity).
136/// M1+M3: Uses XOR+popcount dot + precomputed norm.
137#[inline(always)]
138pub fn dot(a: &Hypervector, b: &Hypervector) -> f64 {
139    a.cosine(b)
140}
141
142/// Memory record
143#[derive(Debug, Clone)]
144pub struct MemoryRecord {
145    pub id: i64,
146    pub content: String,
147    pub source: String,
148    pub timestamp: i64,
149    pub tier: i32,      // 0=episodic, 1=semantic, 2=consolidated
150    pub recall_count: i32,
151    pub error_flag: i32,
152    pub entropy: f64,
153}
154
155/// Scored memory (from a search query)
156#[derive(Debug, Clone)]
157pub struct ScoredMemory {
158    pub memory: MemoryRecord,
159    pub score: f64,
160}
161
162/// Open or create a persistent SQLite-backed memory store
163pub fn open_persistent(path: &str) -> Result<MemoryStore, String> {
164    let conn = rusqlite::Connection::open(path).map_err(|e| format!("DB: {}", e))?;
165    conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;")
166        .map_err(|e| format!("PRAGMA: {}", e))?;
167    conn.execute_batch(
168        "CREATE TABLE IF NOT EXISTS cortex_memories (
169            id INTEGER PRIMARY KEY AUTOINCREMENT,
170            content TEXT NOT NULL,
171            source TEXT DEFAULT '',
172            timestamp INTEGER NOT NULL,
173            tier INTEGER DEFAULT 0,
174            recall_count INTEGER DEFAULT 0
175        );"
176    ).map_err(|e| format!("schema: {}", e))?;
177
178    let mut store = MemoryStore::new(100);
179
180    let mut stmt = conn.prepare("SELECT id, content, source, timestamp, tier, recall_count, 0, 0.0 FROM cortex_memories ORDER BY id").map_err(|e| format!("query: {}", e))?;
181    let rows = stmt.query_map([], |row| {
182        Ok(MemoryRecord {
183            id: row.get(0)?,
184            content: row.get(1)?,
185            source: row.get(2)?,
186            timestamp: row.get(3)?,
187            tier: row.get(4)?,
188            recall_count: row.get(5)?,
189            error_flag: row.get(6)?,
190            entropy: row.get(7)?,
191        })
192    }).map_err(|e| format!("rows: {}", e))?;
193
194    for row in rows.flatten() {
195        store.memories.push(row);
196    }
197    Ok(store)
198}
199
200/// Save all memories to SQLite
201pub fn save_persistent(store: &MemoryStore, path: &str) -> Result<(), String> {
202    let conn = rusqlite::Connection::open(path).map_err(|e| format!("DB: {}", e))?;
203    conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;")
204        .map_err(|e| format!("PRAGMA: {}", e))?;
205    conn.execute_batch(
206        "CREATE TABLE IF NOT EXISTS cortex_memories (
207            id INTEGER PRIMARY KEY AUTOINCREMENT,
208            content TEXT NOT NULL,
209            source TEXT DEFAULT '',
210            timestamp INTEGER NOT NULL,
211            tier INTEGER DEFAULT 0,
212            recall_count INTEGER DEFAULT 0
213        );"
214    ).map_err(|e| format!("schema: {}", e))?;
215
216    let _ = conn.execute("DELETE FROM cortex_memories", []);
217    for m in &store.memories {
218        if let Err(e) = conn.execute(
219                "INSERT INTO cortex_memories (content, source, timestamp, tier, recall_count) VALUES (?1, ?2, ?3, ?4, ?5)",
220                rusqlite::params![m.content, m.source, m.timestamp, m.tier, m.recall_count],
221            ) {
222                eprintln!("[memory] save: {}", e);
223            }
224    }
225    Ok(())
226}
227
228pub struct MemoryStore {
229    pub memories: Vec<MemoryRecord>,
230    token_hvs: FxHashMap<String, Hypervector>,
231    cooccur: FxHashMap<(String, String), u64>,
232    pub dims: usize,
233}
234
235impl MemoryStore {
236    pub fn new(dims: usize) -> Self {
237        Self { memories: Vec::new(), token_hvs: FxHashMap::default(), cooccur: FxHashMap::default(), dims }
238    }
239
240    /// M1: Return reference instead of clone — eliminates 10KB clone per call.
241    pub fn ensure_token_hv(&mut self, token: &str) -> &Hypervector {
242        let dims = self.dims;
243        self.token_hvs.entry(token.to_string())
244            // V58: seed by token CONTENT (FNV-1a), not length — length-seeded
245            // HVs collapse all same-length tokens to one vector (sim 1.0 bug).
246            .or_insert_with(|| make_hv(fnv1a(token.as_bytes()), dims))
247    }
248
249    /// Ingest a memory: tokenize, encode, store, update co-occurrence
250    pub fn retain(&mut self, content: &str, source: &str, error: bool) -> i64 {
251        let tokens = scan_tokens(content);
252        if tokens.is_empty() { return 0; }
253
254        // Co-occurrence update
255        for i in 0..tokens.len() {
256            for j in (i + 1)..tokens.len().min(i + 5) {
257                let key = (tokens[i].clone(), tokens[j].clone());
258                *self.cooccur.entry(key).or_insert(0) += 1;
259                self.hebbian_update(&tokens[i], &tokens[j]);
260            }
261        }
262
263        let now = chrono::Utc::now().timestamp();
264        let id = self.memories.len() as i64 + 1;
265        self.memories.push(MemoryRecord {
266            id,
267            content: content.to_string(),
268            source: source.to_string(),
269            timestamp: now,
270            tier: 0,
271            recall_count: 0,
272            error_flag: if error { 1 } else { 0 },
273            entropy: 0.0,
274        });
275        id
276    }
277
278    /// M2: Integer Hebbian update — no f64 casts.
279    /// epsilon = 0.1 → fixed-point: flip bit with 10% probability toward other.
280    /// For bipolar {-1,+1}: Hebbian learning = stochastic bit flip toward partner.
281    fn hebbian_update(&mut self, a: &str, b: &str) {
282        let dims = self.dims;
283        let hv_b_val = {
284            let hv_b = self.token_hvs.entry(b.to_string()).or_insert_with(|| make_hv(b.len() as u64, dims));
285            // Sample bits from b to flip toward
286            let mut rng = ChaCha8Rng::seed_from_u64(
287                (a.len() as u64).wrapping_mul(31).wrapping_add(b.len() as u64)
288            );
289            let n_flip = (dims / 10).max(1);
290            let mut bits_to_set: Vec<(usize, i8)> = Vec::with_capacity(n_flip);
291            for _ in 0..n_flip {
292                let idx = rng.gen_range(0..dims);
293                bits_to_set.push((idx, hv_b.get(idx)));
294            }
295            bits_to_set
296        };
297        // Now apply to a with only mutable borrow.
298        let hv_a = self.token_hvs.entry(a.to_string()).or_insert_with(|| make_hv(a.len() as u64, dims));
299        for (idx, val) in hv_b_val {
300            hv_a.set(idx, val);
301        }
302    }
303
304    /// V58 P2a: deterministic token-set hypervector — majority vote over the
305    /// k token HVs only (NOT chained bundle_into, which starts from an empty
306    /// HV whose -1 votes poison every tie and collapse everything to zero).
307    /// Sort+dedup for order-independence; FNV content seeds give distinct
308    /// per-token vectors.
309    pub fn encode_tokens(&mut self, tokens: &[String]) -> Hypervector {
310        let mut sorted: Vec<String> = tokens.to_vec();
311        sorted.sort();
312        sorted.dedup();
313        let k = sorted.len();
314        let dims = self.dims;
315        // Register token HVs in the store (for co-occurrence reuse), then
316        // majority-vote over clones.
317        let hvs: Vec<Hypervector> = sorted.iter()
318            .map(|t| self.ensure_token_hv(t).clone())
319            .collect();
320        let mut hv = Hypervector::new(dims);
321        if k == 0 { return hv; }
322        let ones: Vec<i32> = {
323            let mut acc = vec![0i32; dims];
324            for t in &hvs {
325                for (i, slot) in acc.iter_mut().enumerate() {
326                    if t.get(i) == 1 { *slot += 1; }
327                }
328            }
329            acc
330        };
331        for (i, o) in ones.iter().enumerate() {
332            if *o * 2 > k as i32 { hv.set(i, 1); }
333        }
334        hv
335    }
336
337    /// Encode a query into a hypervector (sum of token HVs)
338    pub fn encode_query(&self, query: &str) -> Hypervector {
339        let tokens = scan_tokens(query);
340        let mut hv = Hypervector::new(self.dims);
341        for t in tokens {
342            if let Some(thv) = self.token_hvs.get(&t) {
343                hv.bundle_into(thv);
344            }
345        }
346        hv
347    }
348
349    /// Search memories by query similarity.
350    /// M4: Uses select_nth_unstable for O(N) top-K instead of O(N log N) full sort.
351    pub fn recall(&self, query: &str, top_n: usize) -> Vec<ScoredMemory> {
352        if self.memories.is_empty() { return vec![]; }
353        let q_hv = self.encode_query(query);
354        let mut scored: Vec<(usize, f64)> = self.memories.iter().enumerate().map(|(i, m)| {
355            let m_hv = self.encode_query(&m.content);
356            let score = q_hv.cosine(&m_hv);
357            (i, score)
358        }).collect();
359        // M4: Partial sort — O(N) average for top-K.
360        if top_n < scored.len() {
361            scored.select_nth_unstable_by(top_n, |a, b| {
362                b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)
363            });
364        }
365        scored.truncate(top_n);
366        scored.into_iter().map(|(i, score)| {
367            ScoredMemory { memory: self.memories[i].clone(), score }
368        }).collect()
369    }
370
371    /// Co-occurrence prediction: given query tokens, predict most associated tokens
372    pub fn predict(&self, query: &str, top_n: usize) -> Vec<(String, u64)> {
373        let tokens = scan_tokens(query);
374        let mut scores: FxHashMap<String, u64> = FxHashMap::default();
375        for t in &tokens {
376            for ((a, b), count) in &self.cooccur {
377                if a == t { *scores.entry(b.clone()).or_insert(0) += count; }
378                if b == t { *scores.entry(a.clone()).or_insert(0) += count; }
379            }
380        }
381        let mut sorted: Vec<(String, u64)> = scores.into_iter().collect();
382        // M4: Partial sort for top-K.
383        if top_n < sorted.len() {
384            sorted.select_nth_unstable_by(top_n, |a, b| b.1.cmp(&a.1));
385        }
386        sorted.truncate(top_n);
387        sorted
388    }
389
390    /// Promote memories to higher tiers
391    pub fn consolidate(&mut self) {
392        let now = chrono::Utc::now().timestamp();
393        for m in &mut self.memories {
394            if m.recall_count >= 10 && m.tier < 2 { m.tier = 2; }
395            if m.recall_count >= 3 && m.tier < 1 && now - m.timestamp < 7 * 86400 { m.tier = 1; }
396        }
397    }
398}
399
400/// Grammar-free identifier scanning
401/// M12: to_ascii_lowercase instead of to_lowercase (no Unicode table).
402pub fn fnv_seed(bytes:&[u8])->u64{fnv1a(bytes)}
403/// FNV-1a 64-bit hash — deterministic per-token seeding.
404fn fnv1a(bytes: &[u8]) -> u64 {
405    let mut h: u64 = 0xcbf29ce484222325;
406    for &b in bytes {
407        h ^= b as u64;
408        h = h.wrapping_mul(0x100000001b3);
409    }
410    h
411}
412
413pub fn scan_tokens(text: &str) -> Vec<String> {
414    text.split(|c: char| !c.is_alphanumeric() && c != '_')
415        .filter(|t| t.len() >= 3 && t.chars().any(|c| c.is_alphabetic()))
416        .map(|t| t.to_ascii_lowercase())
417        .collect()
418}
419
420#[cfg(test)]
421mod tests {
422    use super::*;
423
424    #[test]
425    fn test_make_hv() {
426        let hv = make_hv(42, 10000);
427        assert_eq!(hv.dims, 10000);
428        // Check that HV has both +1 and -1 values (bipolar).
429        let mut has_pos = false;
430        let mut has_neg = false;
431        for i in 0..100 {
432            match hv.get(i) {
433                1 => has_pos = true,
434                -1 => has_neg = true,
435                _ => {}
436            }
437        }
438        assert!(has_pos, "HV should contain +1 values");
439        assert!(has_neg, "HV should contain -1 values");
440    }
441
442    #[test]
443    fn test_bundle_and_clamp() {
444        let mut a = Hypervector::new(100);
445        let mut b = Hypervector::new(100);
446        // Set all of a to +1, all of b to -1
447        for i in 0..100 { a.set(i, 1); b.set(i, -1); }
448        bundle(&mut a, &b);
449        // Bundle of +1 and -1 = 0 → sign(0) = 0 → bit stays 0 → get returns -1.
450        // Actually for majority vote: sum=0, sign is undefined. We use > 0 → set.
451        // With equal +1 and -1, sum=0, bit stays 0 (clear) → get returns -1.
452        // This is acceptable for HDC — ties resolve to -1.
453    }
454
455    #[test]
456    fn test_retain_and_recall() {
457        let mut store = MemoryStore::new(100);
458        store.retain("Alice works at Google", "user", false);
459        let results = store.recall("Alice", 5);
460        assert_eq!(results.len(), 1);
461        assert!(results[0].score > 0.0);
462    }
463
464    #[test]
465    fn test_prediction() {
466        let mut store = MemoryStore::new(100);
467        store.retain("Alice works at Google", "user", false);
468        store.retain("Bob works at Google too", "user", false);
469        let preds = store.predict("Alice", 5);
470        assert!(!preds.is_empty());
471        assert!(preds.iter().any(|(t, _)| t.contains("google")));
472    }
473
474    #[test]
475    fn test_empty_store() {
476        let store = MemoryStore::new(100);
477        assert!(store.recall("anything", 5).is_empty());
478    }
479}