HDC Symbolic Query Demo — "Capital of France?"¶

SC-NeuroCore v3.10 — Hyper-Dimensional Computing (HDC/VSA) Kernel

This notebook demonstrates how SC-NeuroCore's SIMD-accelerated BitStreamTensor enables symbolic reasoning via Hyper-Dimensional Computing on 10,000-bit binary vectors.

Key operations:

  • Bind (* / XOR) — associates two concepts (self-inverse)
  • Bundle (+ / majority vote) — combines multiple items into a set
  • Permute (cyclic rotation) — encodes position/sequence
  • Similarity (1 − normalised Hamming distance) — measures relatedness

© 1998–2026 Miroslav Šotek. All rights reserved.
License: GNU AFFERO GENERAL PUBLIC LICENSE v3 | Commercial Licensing Available
Contact: www.anulum.li   protoscience@anulum.li

In [1]:
from sc_neurocore.hdc import HDCEncoder, AssociativeMemory
import numpy as np

DIM = 10_000  # 10,000-bit hypervectors
enc = HDCEncoder(dim=DIM, seed=0)

def sim(a, b):
    """Normalised Hamming similarity between two binary hypervectors."""
    return float(np.mean(a == b))

print(f"SC-NeuroCore HDC Symbolic Query Demo (D={DIM})")
SC-NeuroCore HDC Symbolic Query Demo (D=10000)

Step 1: Create Atomic Symbols¶

Each concept (country, capital, role) gets a random 10,000-bit vector. Random high-dimensional vectors are quasi-orthogonal: pairwise similarity ≈ 0.50.

In [2]:
# Role vectors
role_country = enc.generate_random_vector()
role_capital = enc.generate_random_vector()

# Country atoms
france  = enc.generate_random_vector()
germany = enc.generate_random_vector()
japan   = enc.generate_random_vector()

# Capital atoms
paris  = enc.generate_random_vector()
berlin = enc.generate_random_vector()
tokyo  = enc.generate_random_vector()

atoms = {
    "France": france, "Germany": germany, "Japan": japan,
    "Paris": paris, "Berlin": berlin, "Tokyo": tokyo,
}

print("Pairwise similarity (should be ~0.50):")
print(f"  France vs Germany: {sim(france, germany):.3f}")
print(f"  Paris vs Berlin:   {sim(paris, berlin):.3f}")
Pairwise similarity (should be ~0.50):
  France vs Germany: 0.493
  Paris vs Berlin:   0.496

Step 2: Encode Records¶

Each country–capital pair is encoded as:

$$\text{record} = (\text{role\_country} \oplus \text{country}) + (\text{role\_capital} \oplus \text{capital})$$

where $\oplus$ is XOR-bind and $+$ is majority-vote bundle.

In [3]:
rec_france  = enc.bundle([enc.bind(role_country, france),  enc.bind(role_capital, paris)])
rec_germany = enc.bundle([enc.bind(role_country, germany), enc.bind(role_capital, berlin)])
rec_japan   = enc.bundle([enc.bind(role_country, japan),   enc.bind(role_capital, tokyo)])

# Bundle all records into memory
memory = enc.bundle([rec_france, rec_germany, rec_japan])
print("Records encoded and bundled into memory.")
Records encoded and bundled into memory.

Step 3: Query — "Which countries are in memory?"¶

Unbind the capital role from memory to reveal country associations:

In [4]:
# Unbind the "capital" role from the whole memory -> a superposition of all capitals.
probe = enc.bind(memory, role_capital)

print("Which capitals are stored? (unbind role_capital from memory)")
for name, atom in [("Paris", paris), ("Berlin", berlin), ("Tokyo", tokyo),
                   ("France", france), ("Germany", germany)]:
    tag = "  <- capital" if name in ("Paris", "Berlin", "Tokyo") else ""
    print(f"  sim(probe, {name:>8s}) = {sim(probe, atom):.4f}{tag}")
Which capitals are stored? (unbind role_capital from memory)
  sim(probe,    Paris) = 0.5925  <- capital
  sim(probe,   Berlin) = 0.5875  <- capital
  sim(probe,    Tokyo) = 0.5935  <- capital
  sim(probe,   France) = 0.4856
  sim(probe,  Germany) = 0.4950

Step 4: Query — "Capital of France?"¶

Two-step unbinding:

  1. Unbind France from memory: hat = memory * france
  2. Unbind the capital role: answer = hat * role_capital

The result should be most similar to Paris.

In [5]:
# "Capital of France?" -> unbind the capital role from the France record, then clean up.
answer = enc.bind(rec_france, role_capital)

am = AssociativeMemory()
for name, atom in atoms.items():
    am.store(name, atom)

print("Query: 'Capital of France?' (unbind role_capital from the France record)")
best_name, best_sim = "", -1.0
for name, atom in atoms.items():
    s = sim(answer, atom)
    if s > best_sim:
        best_sim, best_name = s, name
    print(f"  sim(answer, {name:>8s}) = {s:.4f}")

print(f"\nItem-memory cleanup -> {am.query(answer)}")
print(f"Best match: {best_name} (similarity {best_sim:.4f})")
Query: 'Capital of France?' (unbind role_capital from the France record)
  sim(answer,   France) = 0.4905
  sim(answer,  Germany) = 0.4951
  sim(answer,    Japan) = 0.5052
  sim(answer,    Paris) = 0.7428
  sim(answer,   Berlin) = 0.5010
  sim(answer,    Tokyo) = 0.4960

Item-memory cleanup -> Paris
Best match: Paris (similarity 0.7428)

Step 5: Verify Bind-Inverse Property¶

XOR is self-inverse: $(a \oplus b) \oplus b = a$. This is the mathematical foundation that makes unbinding work.

In [6]:
a = enc.generate_random_vector()
b = enc.generate_random_vector()
recovered = enc.bind(enc.bind(a, b), b)
print(f"Bind-inverse: sim(a, (a*b)*b) = {sim(recovered, a):.4f} (should be ~1.0)")
Bind-inverse: sim(a, (a*b)*b) = 1.0000 (should be ~1.0)

Step 6: Permute for Sequence Encoding¶

Cyclic rotation produces quasi-orthogonal vectors, enabling position encoding:

$$\text{sim}(v, \pi^k(v)) \approx 0.50 \quad \text{for } k \neq 0$$

In [7]:
v  = enc.generate_random_vector()
p1 = enc.permute(v, 1)
p2 = enc.permute(v, 2)
print("Permute orthogonality:")
print(f"  sim(v, permute(v,1)) = {sim(v, p1):.4f}")
print(f"  sim(v, permute(v,2)) = {sim(v, p2):.4f}")
Permute orthogonality:
  sim(v, permute(v,1)) = 0.4940
  sim(v, permute(v,2)) = 0.5028