Ctrl K
ComunidadGuías…

cortexm/vsa/hologram_overlay.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""HRR KG overlay — superposed holographic fact store for O(1) lookup.

The Holographic Memory for Knowledge Graphs paper (arXiv:2606.24948,
2026) shows that facts bound as (s, r, v) can be superposed into a
single dense hologram M, and a query (s, r, ?) answered by unbinding
the V slot from M:

    M = Σ_f bind_V(v_f) * (bind_S(s_f) + bind_R(r_f))   # superposition
    v_hat = unbind_V(M) → noisy residual ≈ Σ_f (bind_S(s_f) + bind_R(r_f)) * v_f
    cleanup(v_hat) → snapped to nearest stored value vector

Capacity bound: ~dims^2 / ln(dims) facts before saturation (Clarkson
2023). At d=768 → ~93k facts; at d=16,384 → ~31M facts.

Best used as a per-scope overlay on top of MemoryPalace — single-hop
queries hit the overlay (O(1) + cleanup), multi-hop queries fall back
to TreeIndex search. The overlay trades noise accumulation for lookup
speed; saturation is detected by signal-to-noise ratio on the cleanup
step.

Pure numpy. Reuses VSA.bind/unbind and HopfieldCleanup.recall.

arxiv research: arXiv:2606.24948 (2026 holographic KG); HolE AAAI 2016
(Nickel, Rosasco, Poggio); Plate 1995 (HRR).
"""

from __future__ import annotations

import numpy as np

from cortexm.vsa.cleanup import HopfieldCleanup
from cortexm.vsa.ops import VSA


def _norm(v: np.ndarray) -> np.ndarray:
    n = float(np.linalg.norm(v))
    return v / n if n > 0 else v


class HolographicFactOverlay:
    """Per-scope superposed hologram for O(1) single-hop fact lookup.

    Each (user_id, scope) tuple gets one dense hologram M. Adding a fact
    binds (s, r, v) and adds to M. Querying unbinds the requested role
    and snaps to the nearest stored item via Hopfield cleanup.

    Falls back gracefully when M saturates — caller can switch to
    MemoryPalace.search for the affected scope.
    """

    def __init__(self, vsa: VSA, cleanup: HopfieldCleanup,
                 saturate_threshold: float = 0.55) -> None:
        self.vsa = vsa
        self.cleanup = cleanup
        self.saturate_threshold = saturate_threshold
        self._M: dict[tuple, np.ndarray] = {}     # scope → hologram
        self._counts: dict[tuple, int] = {}        # scope → fact count
        self._saturated: set[tuple] = set()

    def add_fact(self, scope: tuple, s_vec: np.ndarray,
                r_vec: np.ndarray, v_vec: np.ndarray) -> None:
        """Add a fact (s, r, v) into the superposed hologram."""
        # bind each role to its filler
        bound = (self.vsa.bind("S", s_vec)
                 + self.vsa.bind("R", r_vec)
                 + self.vsa.bind("V", v_vec))
        bound = _norm(bound)
        h = self._M.get(scope)
        if h is None:
            self._M[scope] = bound.copy()
            self._counts[scope] = 1
        else:
            self._M[scope] = _norm(h + bound)
            self._counts[scope] = self._counts.get(scope, 0) + 1
        # populate cleanup codebook with each filler
        self.cleanup.add(f"s:{_hash_vec(s_vec)}", s_vec)
        self.cleanup.add(f"r:{_hash_vec(r_vec)}", r_vec)
        self.cleanup.add(f"v:{_hash_vec(v_vec)}", v_vec)

    def query(self, scope: tuple, query_vec: np.ndarray,
              target_role: str = "V",
              fallback_embs: list[tuple[str, np.ndarray]] | None = None
             ) -> tuple[str | None, float]:
        """Query (s, r, ?) — unbind target_role and cleanup the residual.

        Returns (item_key, confidence). Confidence below saturate_threshold
        indicates either saturation or miss; caller should fall back to
        MemoryPalace.search.

        If fallback_embs is provided, those (key, vec) pairs are temporarily
        added to the cleanup codebook before recall — useful for cross-
        scope queries where the answer may be a value vector not yet in
        the codebook.
        """
        M = self._M.get(scope)
        if M is None:
            return None, 0.0
        if scope in self._saturated:
            # signal saturation — caller should fallback to TreeIndex
            return None, 0.0
        # unbind the target role from M to get a noisy residual
        residual = self.vsa.unbind(target_role, M)
        # add fallback embeddings to cleanup if provided
        if fallback_embs:
            for k, v in fallback_embs:
                self.cleanup.add(k, v)
        self.cleanup.build()
        item_key, conf = self.cleanup.recall(residual)
        if conf < self.saturate_threshold:
            # either miss or saturation — mark saturated if we have many
            # facts and conf is consistently low
            if self._counts.get(scope, 0) > 1000:
                self._saturated.add(scope)
            return None, conf
        return item_key, conf

    def stats(self) -> dict:
        return {
            "scopes": len(self._M),
            "total_facts": sum(self._counts.values()),
            "saturated_scopes": len(self._saturated),
            "cleanup": self.cleanup.stats(),
        }

    def reset_scope(self, scope: tuple) -> None:
        """Drop a saturated scope and let it rebuild from new adds."""
        self._M.pop(scope, None)
        self._counts.pop(scope, None)
        self._saturated.discard(scope)


def _hash_vec(v: np.ndarray) -> str:
    """Stable hash for codebook keys."""
    import hashlib
    h = hashlib.blake2b(v.tobytes(), digest_size=8).hexdigest()
    return h[:16]


__all__ = ["HolographicFactOverlay"]