Skip to content

What "holographic" means here

In a hologram every part of the plate carries a degraded copy of the whole image. A hypervector memory works the same way: many items are superposed into one high-dimensional vector, no single coordinate stores any one item, and damaging the vector degrades every item a little instead of erasing a few. That is the sense in which HMS is holographic. It is a property of the vector-symbolic layer, and this page says exactly which parts of the engine have it.

The vectors

An EntangledHVec is a sparse binary vector of D dimensions. By default about D/256 bits are active (DEFAULT_RHO_DENOM = 256 in src/core/entangled.rs), stored as a sorted list of active indices. At D = 16,384 that is 64 indices, 256 bytes, against 64 KiB for a dense f32 vector. Vectors for atoms (names, relations, words) are generated deterministically from a hash seed, so the same atom has the same vector in every process and no codebook has to be stored.

The three operations

Operation On sparse index sets What it is for
Bind (bind) symmetric difference (XOR) of the active sets Tie a role to a filler. It is its own inverse: binding the result with the role recovers a noisy copy of the filler.
Bundle (bundle) per-index count, keep indices above a threshold Superpose many items into one vector that stays similar to each of them.
Permute (permute) cyclic shift of every index Encode order. bind_ordered(a, b) differs from bind_ordered(b, a).

Similarity is Jaccard overlap of the active sets. Bind keeps the result dissimilar to both inputs; bundle keeps it similar to every input, which is the superposition.

bundle_bloom is a union rather than a thresholded count (a Bloom filter over the items). It is what the capacity sweep in Capacity limits measures.

Where the algebra is used

  • Meaning memory (MeaningConfig::enabled): a triple (subject, relation, object) is stored as one composite vector, with the roles separated by cyclic shifts and combined by bind. A query binds the known parts to isolate the unknown one, and Hopfield cleanup (sparse softmax attention over the stored atoms) snaps the noisy residue back to the nearest stored atom. multi_hop chains this over several relations.
  • Sequences and analogies: positional binding by permutation, and relation mapping between domains.
  • Resonator network: factors a bound product back into its parts by iterating cleanup.
  • Sparse vector index: memorize_vector / query_vector store sparse codes of dense embeddings. It is an experimental path and not a competitive nearest-neighbour index; see the public benchmarks for the measured recall.

Where it is not used

The document pipeline does not rank with hypervectors. A document is split into chunks; each chunk keeps its stemmed term counts, its text, and optionally the dense embeddings you supply. A query is scored by BM25 over the terms and by exact cosine over the embeddings, then the two lists are fused (a min-max normalised blend, or reciprocal rank fusion). Each chunk also carries a sparse hypervector from the text encoder, serialised with the chunk, but search does not read it. To check this against the code: src/core/documents/search.rs has no use of chunk.vector.

So search_documents is a conventional hybrid retriever with a metadata filter, byte offsets and atomic replace. Choose it for that, not for holographic behaviour.

Graceful degradation, measured

Corrupting a stored vector should degrade retrieval smoothly. The committed sweep benchmark_scaling_results.json holds a codebook of 100 stored vectors, replaces 0 to 90 percent (steps of 10) of a probe's active indices with random ones, and checks whether the original vector is the nearest neighbour (Jaccard) or the Hopfield cleanup output. Both were correct for all 30 probes at every corruption level tested, at each of the nine dimension and density settings in the file. That is one codebook size (100), 30 probes, and uniform random corruption of a single stored vector. It says nothing about a codebook of 10,000, about a vector that is itself a superposition, or about adversarial noise; for superpositions see Capacity limits.

Try it

examples/quickstart/rust/src/bin/holographic.rs binds a role to a filler, superposes 100 items into one vector, and damages the vector step by step. CI runs it and diffs the output with the table below.

// Copyright 2024-2026 WritersLogic Contributors
// SPDX-License-Identifier: Apache-2.0

//! The "holographic" part in a few lines: bind a role to a filler, superpose many items in ONE
//! vector, test membership, and damage the vector to watch the signal degrade gradually.
//! Run: `cargo run --release --manifest-path examples/quickstart/rust/Cargo.toml --bin holographic`

use holographic_memory::EntangledHVec;

const DIM: usize = 16_384;
const ITEMS: u64 = 100;

fn atom(seed: u64) -> EntangledHVec {
    EntangledHVec::new_deterministic(DIM, seed)
}

/// Share of `item`'s active bits that are set in `memory` (1.0 = fully present).
fn presence(memory: &EntangledHVec, item: &EntangledHVec) -> f64 {
    let hits = item
        .indices()
        .iter()
        .filter(|bit| memory.indices().binary_search(bit).is_ok())
        .count();
    hits as f64 / item.indices().len() as f64
}

fn main() {
    // Bind is its own inverse: unbinding a role from role (x) filler returns the filler.
    let (role, filler) = (atom(1), atom(2));
    let bound = role.bind(&filler);
    assert_eq!(role.bind(&bound).indices(), filler.indices());
    println!("bind(role, filler) unbound with role recovers the filler exactly");

    // Superpose {ITEMS} atoms into ONE fixed-size vector (a union bundle).
    let members: Vec<EntangledHVec> = (100..100 + ITEMS).map(atom).collect();
    let strangers: Vec<EntangledHVec> = (10_000..10_000 + ITEMS).map(atom).collect();
    let memory = EntangledHVec::bundle_bloom(&members);
    println!(
        "{ITEMS} items in one vector of {} active bits out of {DIM}\n",
        memory.indices().len()
    );

    // Damage: keep only a share of the bundle's bits. No single bit held any one item, so every
    // member degrades together and gradually; strangers stay near the noise floor.
    println!("bits kept  members (mean presence)  strangers (mean presence)");
    let bits = memory.indices().to_vec();
    for keep_percent in [100usize, 75, 50, 25, 10] {
        let kept: Vec<u32> = bits
            .iter()
            .copied()
            .enumerate()
            .filter(|(i, _)| (i * 7919) % 100 < keep_percent)
            .map(|(_, bit)| bit)
            .collect();
        let damaged = EntangledHVec::from_indices(kept, DIM);
        let mean = |set: &[EntangledHVec]| {
            set.iter().map(|v| presence(&damaged, v)).sum::<f64>() / set.len() as f64
        };
        println!(
            "{keep_percent:>8}%  {:>22.2}  {:>25.2}",
            mean(&members),
            mean(&strangers)
        );
    }
}
bind(role, filler) unbound with role recovers the filler exactly
100 items in one vector of 5299 active bits out of 16384

bits kept  members (mean presence)  strangers (mean presence)
     100%                    1.00                       0.32
      75%                    0.75                       0.24
      50%                    0.50                       0.16
      25%                    0.25                       0.08
      10%                    0.10                       0.03

Read the table for what it shows and no more. Members of the bundle keep a presence equal to the share of bits kept (1.00 down to 0.10), so degradation is gradual and proportional, with no cliff. Strangers stay at a fixed fraction of that, so members remain separable from non-members at every damage level tested (100 items, D = 16,384, a fixed deterministic bit-dropping pattern). It does not show recall of structured records from a bundle; for that, see the meaning memory and the capacity page.