"""
memory.py — Memory subsystem for AETHER.
Two complementary stores:
1. SparseDistributedMemory (Kanerva 1988):
- N "hard locations" sampled uniformly in the HD space.
- Write/read activate the k nearest locations (top-k by Hamming distance).
- Counters accumulate ±1 per write; read returns majority vote.
- O(N * dim) per op, no GPU. Sparse and noise-robust.
2. AssociativeMemory:
- Symbol table: name -> HDVector (deterministic from text seed).
- Episodic store: list of (vector, payload) for explicit recall.
- Concept KB: stores bindings like bind(subject, predicate) -> object.
Both learn in O(1) per item — true one-shot, no epochs.
"""
from __future__ import annotations
import numpy as np
from typing import List, Tuple, Dict, Any, Optional
from dataclasses import dataclass, field
from .hd import HDVector, DIM, _sign
class SparseDistributedMemory:
"""
Kanerva's Sparse Distributed Memory.
Hard locations are random bipolar vectors. A read/write addresses the
k closest locations (by Hamming distance, or equivalently by inner
product since bipolar). Writes accumulate ±1 counters; reads return
the sign of the summed counters across activated locations.
"""
def __init__(self, dim: int = DIM, n_locations: int = 5000, k: int = 15):
self.dim = dim
self.n_locations = n_locations
self.k = k
# Hard locations — random bipolar vectors
rng = np.random.default_rng(42)
self.locations = rng.choice([-1, 1], size=(n_locations, dim)).astype(np.int8)
# Per-location per-bit counters (int8 to bound memory)
self.counters = np.zeros((n_locations, dim), dtype=np.int8)
self.write_count = np.zeros(n_locations, dtype=np.int32)
def _activate(self, address: np.ndarray) -> np.ndarray:
"""Return indices of the k closest hard locations to `address`."""
# For bipolar, dot product = (dim - 2*hamming). Higher dot = closer.
dots = self.locations @ address.astype(np.int16)
k = min(self.k, self.n_locations)
if k >= self.n_locations:
return np.arange(self.n_locations)
# Top-k by dot product
return np.argpartition(-dots, k - 1)[:k]
def write(self, address: HDVector, data: HDVector) -> None:
"""Write `data` at `address`. Activates k nearest hard locations."""
active = self._activate(address.data)
# Each activated location: counter += data (with int8 saturation)
# We use int16 accumulation to avoid overflow, then saturate to int8
upd = self.counters[active].astype(np.int16) + data.data.astype(np.int16)
self.counters[active] = np.clip(upd, -127, 127).astype(np.int8)
self.write_count[active] += 1
def read(self, address: HDVector) -> Optional[HDVector]:
"""Read from `address`. Returns None if no writes happened nearby."""
active = self._activate(address.data)
total_writes = self.write_count[active].sum()
if total_writes == 0:
return None
# CRITICAL: cast to int32 BEFORE sum — int8 sum overflows for k>15
summed = self.counters[active].astype(np.int32).sum(axis=0)
if np.all(summed == 0):
return None
return HDVector(data=_sign(summed), dim=self.dim)
def stats(self) -> Dict[str, Any]:
return {
"n_locations": self.n_locations,
"dim": self.dim,
"k": self.k,
"total_writes": int(self.write_count.sum()),
"active_locations": int((self.write_count > 0).sum()),
}
@dataclass
class _Episode:
vector: HDVector
payload: str
metadata: Dict[str, Any] = field(default_factory=dict)
class AssociativeMemory:
"""
Symbol-grounded associative memory.
- vocab: name -> HDVector (deterministic from text seed for stability)
- episodes: list of (vector, text) for retrieval
- bindings: stored as (subject, predicate) -> object HD vector
This is the symbolic KB layer.
"""
def __init__(self, dim: int = DIM):
self.dim = dim
self.vocab: Dict[str, HDVector] = {}
self.episodes: List[_Episode] = []
# KB of triples: store as bind(bind(s_vec, p_vec), o_vec) in an SDM
self.kb_store = SparseDistributedMemory(dim=dim, n_locations=5000, k=15)
# Also keep an explicit triple list for inspection / debugging
self.triples: List[Tuple[str, str, str]] = []
# ----- vocabulary -----
def get_symbol(self, name: str) -> HDVector:
"""Get or create a stable HD vector for a symbol."""
name = name.lower().strip()
if name not in self.vocab:
self.vocab[name] = HDVector.from_text_seed(name, self.dim)
return self.vocab[name]
def has_symbol(self, name: str) -> bool:
return name.lower().strip() in self.vocab
# ----- episodic storage (raw text + vector) -----
def add_episode(self, text: str, vector: HDVector, metadata: Optional[Dict] = None) -> None:
self.episodes.append(_Episode(vector=vector, payload=text, metadata=metadata or {}))
def retrieve_similar(self, query: HDVector, top_k: int = 3) -> List[Tuple[str, float]]:
"""Find the top-k most similar stored episodes."""
if not self.episodes:
return []
sims = [(ep.payload, query.similarity(ep.vector)) for ep in self.episodes]
sims.sort(key=lambda x: -x[1])
return sims[:top_k]
# ----- semantic KB (subject, predicate, object) -----
def learn_triple(self, subject: str, predicate: str, obj: str) -> None:
"""Store a triple in BOTH directions so we can query either way.
Convention (bind is commutative for bipolar):
- bind(s, p) -> o : answers 'what is S?' / 'where is S?'
- bind(p, o) -> s : answers 'what is the P of O?'
Example: 'Paris is the capital of France'
s=Paris, p=capital_of, o=France
- bind(Paris, capital_of) -> France (for 'Paris is the capital of what?')
- bind(capital_of, France) -> Paris (for 'What is the capital of France?')
"""
s = self.get_symbol(subject)
p = self.get_symbol(predicate)
o = self.get_symbol(obj)
# Subject-predicate -> object (supports 'what is S?' queries)
self.kb_store.write(s.bind(p), o)
# Predicate-object -> subject (supports 'what is the P of O?' queries)
self.kb_store.write(p.bind(o), s)
self.triples.append((subject.lower(), predicate.lower(), obj.lower()))
def query_triple(self, subject: str, predicate: str) -> Optional[Tuple[str, float]]:
"""Query (subject, predicate) -> ?object. Returns (best_match, similarity).
Structural tokens (, , , , single-char punctuation) are
excluded — they should never be returned as factual answers.
"""
s = self.get_symbol(subject)
p = self.get_symbol(predicate)
addr = s.bind(p)
retrieved = self.kb_store.read(addr)
if retrieved is None:
return None
# Tokens that should never be returned as a factual answer
STRUCTURAL = {"", "", "", "", "?", ".", ",", "!", ":", ";", "|"}
# Match against known symbols
best_name, best_sim = None, -1.0
for name, vec in self.vocab.items():
if name in STRUCTURAL:
continue
sim = retrieved.similarity(vec)
if sim > best_sim:
best_sim, best_name = sim, name
if best_name is None or best_sim < 0.10:
return None
return best_name, best_sim
def list_triples(self) -> List[Tuple[str, str, str]]:
return list(self.triples)
# ----- persistence -----
def stats(self) -> Dict[str, Any]:
return {
"vocab_size": len(self.vocab),
"episodes": len(self.episodes),
"triples": len(self.triples),
"kb_store": self.kb_store.stats(),
}