holo-curiosity
A memory that proposes questions it wants answered.
Given a store of items with optional role structure, the engine proposes queries whose answers would most reduce the store's own uncertainty. Five mechanisms generate candidate questions. Each is scored by expected information gain and returned with a per-mechanism diversity constraint.
The engine is closed-loop. It proposes questions, the caller answers some of them, the store updates, and the next proposal reflects the new state.
What it does
from holo_curiosity import CuriosityMemory, CuriosityEngine
mem = CuriosityMemory(d=2048)
mem.observe("france", capital="paris", continent="europe")
mem.observe("germany", capital="berlin")
mem.observe("japan", capital="tokyo", continent="asia")
mem.observe("italy", capital="rome", continent="europe")
mem.observe("spain", weight=0.2, capital="madrid")
engine = CuriosityEngine(mem)
for q in engine.propose(n=6):
print(q["query"], q["kind"], q["expected_info_gain"])
Output:
what relates to germany? sparse 0.900
what relates to spain? sparse 0.900
what relates to japan? sparse 0.891
tell me more about spain low_weight 0.800
what is germany's continent? slot_gap 0.540
what is spain's continent? slot_gap 0.540
The five mechanisms
| Mechanism | Question | Trigger |
|---|---|---|
low_weight |
"tell me more about X" | X has weight far below the max |
sparse |
"what relates to X?" | X has no close neighbors |
slot_gap |
"what is X's R?" | R appears on other items but not on X |
reverse_pair |
"what does B say about A?" | A mentions B, B doesn't mention A |
interpolation |
"what is between X and Y?" | X and Y are highly similar |
Each mechanism returns a list of candidates with an expected information gain. Candidates from all mechanisms are merged, sorted by gain, and truncated with a per-mechanism cap (default 3) so that no single mechanism dominates the proposal list.
Installation
pip install numpy
No other dependencies. Single file, approximately 600 lines.
Usage
CLI
python holo_curiosity.py
python holo_curiosity.py --output results/
Runs six demonstrations and writes a JSON state file.
Python
from holo_curiosity import CuriosityMemory, CuriosityEngine
# Storage
mem = CuriosityMemory(d=2048)
mem.observe("france", weight=1.0, capital="paris", continent="europe")
mem.observe("germany", weight=1.0, capital="berlin")
mem.observe("spain", weight=0.2, capital="madrid")
# Query the store directly
mem.query("france") # ~1.0
mem.similarity("france", "germany")
mem.nearest("france", k=3)
# Propose questions
engine = CuriosityEngine(mem, mechanism_cap=3)
proposals = engine.propose(n=6)
for p in proposals:
print(p["query"], p["kind"], p["expected_info_gain"])
# Closed loop: answer a proposal, re-propose
for p in proposals:
if p["kind"] == "slot_gap":
label, role = p["target"]
mem.observe(label, weight=0.3, **{role: "placeholder"})
break
new_proposals = engine.propose(n=6)
Results
All results at D=2048.
Self-test
| Check | Result |
|---|---|
| bind/unbind identity | PASS |
| observe + query (france sim=0.964) | PASS |
| slot-gap detects germany | PASS |
Basic proposals
Store of five countries with partial field coverage. Proposal ranking
with mechanism_cap=3:
| Rank | Gain | Kind | Query |
|---|---|---|---|
| 1 | 0.900 | sparse | what relates to germany? |
| 2 | 0.900 | sparse | what relates to spain? |
| 3 | 0.891 | sparse | what relates to japan? |
| 4 | 0.800 | low_weight | tell me more about spain |
| 5 | 0.540 | slot_gap | what is germany's continent? |
| 6 | 0.540 | slot_gap | what is spain's continent? |
The engine correctly identifies that germany and spain lack continents (while france, japan, italy have them). It also identifies spain as under-weighted. And it flags three items as isolated.
Each mechanism in isolation
low_weight. Items stored with weights 1.0, 0.9, 0.2, 0.05. Only
the two weak items generate proposals. Gain is 1 β weight/max.
sparse. Eight items with no close neighbors. All eight produce
proposals at gain ~0.89. Sparsity is 1 β mean nn similarity.
slot_gap. Four items with capital set; continent set on the
first only. Two items are missing continent, one is missing both
continent and currency. Only the missing slots with 2+ coverage
generate proposals. Gain scales with how many other items cover the
slot.
reverse_pair. Five items with pairwise mentions. Proposes the reverse of every one-sided mention: "what does paris say about alice?", "what does france say about paris?", "what does bob say about alice?", etc. Gain is 0.4 + 0.4 Γ similarity.
interpolation. With four scattered items, no pair is above the similarity threshold of 0.15, so no proposals. The mechanism is conservative by design.
Diversity constraint
Same store, two cap values:
cap=2 produces a mix of mechanisms:
low_weight: 2
sparse: 2
slot_gap: 2
cap=6 produces only the top mechanism:
low_weight: 6
The cap ensures the top-N proposals represent multiple kinds of uncertainty rather than many instances of the same kind.
Closed loop
Round 1 proposals include slot_gap: what is germany's continent?
at rank 4. After answering with mem.observe("germany", continent="europe"), the slot is filled. Round 2 proposals include
reverse_pair: what does rome say about italy? at rank 4 instead.
The engine adapts to what it has learned. The slot-gap question disappears once filled; new questions surface from other mechanisms.
Store growth under guidance
Six rounds of propose β answer β re-propose on a five-item store.
The top proposal was always a sparse query ("what relates to
germany?"), which the demo loop does not answer (it only answers
slot_gap and low_weight). The store stays at 5 items.
This is a limitation of the demonstration loop, not the engine. A full loop would handle all five mechanism kinds. The engine itself proposed the correct questions; the loop just didn't act on all of them.
Proposal statistics
Same store, mechanism_cap=100, top 50 proposals:
| Kind | Count | Mean gain |
|---|---|---|
| low_weight | 5 | 0.900 |
| reverse_pair | 10 | 0.400 |
| slot_gap | 19 | 0.587 |
| sparse | 16 | 0.888 |
With no diversity constraint, sparse and low_weight dominate because their gains are highest. Slot_gap proposals have moderate gain. Reverse_pair proposals cluster near 0.4 because similarity is often low. Interpolation is absent because no pair meets the threshold.
API reference
CuriosityMemory
CuriosityMemory(d=2048)
observe(label, weight=1.0, **fields)β store an item with optional role fields. The composite vector (name + role-bound field values) goes into the trace. Fields accumulate: callingobservetwice on the same label merges the fields.query(label) -> floatβ raw projection of the composite against the trace.similarity(a, b) -> floatβ cosine between two composite items.nearest(label, k=5, exclude_self=True) -> [(label, sim)]stats() -> dict
CuriosityEngine
CuriosityEngine(
memory,
mechanism_cap=3,
max_weight_ratio=0.5,
min_sparsity=0.5,
min_interp_similarity=0.15,
)
propose(n=5) -> list[dict]β top-n proposals, ranked by gain with a per-mechanism cap.
Each proposal is:
{
"query": str, # human-readable question
"kind": str, # mechanism name
"target": str or tuple, # the label(s) involved
"expected_info_gain": float, # 0.0 to 1.0
"explanation": str, # why this was proposed
# mechanism-specific extras:
"weight_ratio": float, # low_weight only
"sparsity": float, # sparse only
"coverage": int, # slot_gap only
"similarity": float, # reverse_pair, interpolation
}
Design notes
Composite items
An item is stored as norm(name + Ξ£ bind(role, value)). The name
vector identifies the label; the field bindings make the item's
structure retrievable. The composite is what lives in the trace.
Field values share the name-vector namespace with labels. The string "paris" as a capital and the label "paris" both resolve to the same underlying name vector. This means reverse-pair detection works for any pair of strings that appear as either a label or a value.
Why diversity constraint
Without a cap, the top-N proposals are often all from the same
mechanism. sparse and low_weight both produce gain values near
0.9, so they crowd out everything else. The cap ensures the user sees
a mix. The default cap of 3 means at most 3 candidates from any one
mechanism.
Why information gain, not entropy
The engine does not use Shannon entropy over the value space. The substrate does not maintain a distribution over values; it maintains a set of stored observations. The gain score is a heuristic on the store's own geometry: how far below the max is this item's weight, how sparse is this item's neighborhood, how many other items cover this field. The score is not a probability; it is a ranking signal.
Limitations
The demonstration loop does not handle all mechanisms. DEMO 5
shows the store staying at 5 items because the loop only answers
slot_gap and low_weight queries. Sparse, reverse_pair, and
interpolation queries require the caller to supply new items or
relationships, which the demo does not do.
Interpolation rarely fires. With min_interp_similarity=0.15
and composite items built from random vectors, most pairs are below
threshold. In practice, interpolation would trigger only for items
that happen to share field values or that are otherwise correlated
in the codebook. The threshold is configurable.
No contradiction detection between proposals. If two questions
imply different answers for the same slot, the engine does not
report this. The slot_gap mechanism proposes missing slots but
does not check whether the answer would conflict with anything.
The gain formulas are heuristic. Each mechanism has a hand-tuned formula. There is no principled derivation of the scores. A different designer would choose different formulas. The ranking is stable within each mechanism but the relative ordering across mechanisms is a design choice.
No "already asked" tracking. The engine does not remember which
proposals the caller has already answered. Running propose(n) twice
on the same state returns the same list. A closed loop that answers
proposals will see different lists because the state has changed,
but the engine itself does not track proposal history.
No mechanism for proposing new roles. The slot_gap mechanism
finds missing values for existing roles. It cannot propose that a
new role should be introduced (e.g., "does france have a
population field?").
Similarity has no semantic meaning. Two items are "similar" if their composite vectors have high cosine similarity. With random name vectors, this similarity is dominated by shared field values and by coincidence. Two unrelated items that happen to share a field value will look more similar than they semantically are.
Citation
@misc{holo-curiosity2026,
title = {holo-curiosity: A memory that proposes questions it
wants answered},
author = {zeechimp},
year = {2026},
note = {Five-mechanism question generator with information-gain
ranking and per-mechanism diversity constraint.}
}
References
- Plate, T. A. "Holographic Reduced Representations." IEEE Transactions on Neural Networks 6:3 (1995), 623β641.
- Kanerva, P. "Hyperdimensional Computing." Cognitive Computation 1:2 (2009), 139β159.
- Schmidhuber, J. "Formal Theory of Creativity, Fun, and Intrinsic Motivation." IEEE Transactions on Autonomous Mental Development 2:3 (2010), 230β247.
- Pathak, D., Agrawal, P., Efros, A. A., Darrell, T. "Curiosity-driven Exploration by Self-supervised Prediction." ICML 2017.
License
Apache 2.0