holo-narrative

Ordered memory for events, timelines, and narrative chains.

A single-file library for storing and walking ordered sequences using hyperdimensional vectors. Directed binding preserves order. Symmetric binding does not. Every operation is O(D). No training, no gradients, no external model.

What it does

The store holds two directed traces:

S_next = sum_i bind_dir(event_i, event_{i+1})
S_prev = sum_i bind_dir(event_{i+1}, event_i)

bind_dir is a frequency-domain roll of the second operand in the FFT domain. It is non-commutative: bind_dir(a, b) != bind_dir(b, a). Symmetric binding (bind) is commutative, so bind(a, b) == bind(b, a) and the reverse edge competes with the forward edge. A chain built with symmetric binding walks 0 -> 1 -> 2 -> 3 -> 2 -> 3 -> 2 -> 3 ... and never reaches the end. Directed binding removes this failure.

Given one event, the store returns its successor or predecessor. Chained queries walk forward or backward. Multiple chains share one store without interfering.

Installation

pip install numpy        # required
pip install matplotlib   # optional, for plots

Usage

Python

from holo_narrative import NarrativeMemory

mem = NarrativeMemory(d=4096)
mem.add_chain(["wake", "coffee", "commute", "work", "lunch"])

chain = mem.walk_forward(0, max_steps=10)
print(mem.describe(chain))
# wake -> coffee -> commute -> work -> lunch

nxt = mem.query_next(0)
print(mem.label(nxt))
# coffee

mem.save_json("timeline.json")
mem.plot("timeline.png")

CLI

python holo_narrative.py
python holo_narrative.py --output results/

Runs twelve demonstrations and writes the output to a directory.

Results

All results from a standard run at D=4096 unless noted. The self-test cos(unbind_dir(a, bind_dir(a, b)), b) returns 1.000000 (exact).

1. Chain retrieval β€” 25/25 exact

A 25-event daily routine walks end-to-end without error:

wake_up -> brush_teeth -> make_coffee -> drink_coffee -> shower ->
get_dressed -> pack_bag -> leave_home -> walk_to_station_1 ->
catch_train_1 -> arrive_office -> greet_colleagues -> check_email ->
morning_meeting -> work_on_project -> lunch_break -> afternoon_work ->
wrap_up -> leave_office -> walk_to_station_2 -> catch_train_2 ->
arrive_home -> dinner -> read_book -> go_to_bed

25 events reached out of 25.

2. Bidirectional walk β€” 10/10 both directions

Chain of 10 events:

forward:   a -> b -> c -> d -> e -> f -> g -> h -> i -> j
backward:  j -> i -> h -> g -> f -> e -> d -> c -> b -> a

Both directions reach full length.

3. Multiple storylines in one store

Two 8-event storylines share one store (14 edges total). Each walks its own chain without crossing into the other:

alice chain: alice_wakes -> alice_reads -> alice_cooks ->
             alice_leaves -> alice_meeting -> alice_lunch ->
             alice_returns -> alice_sleeps
bob chain:   bob_wakes -> bob_jogs -> bob_showers -> bob_leaves ->
             bob_workout -> bob_lunch -> bob_returns -> bob_sleeps

Querying the successor of alice's last event returns none (no cross-talk into bob's chain).

4. Fuzzy anchor retrieval

Query with a noisy version of an event vector. The correct anchor is retrieved across a wide noise range:

Noise level Retrieved Similarity
0.1 step_050 0.9975
0.3 step_050 0.9768
0.5 step_050 0.9284
0.8 step_050 0.8040

Walk forward from the retrieved anchor continues cleanly.

5. Timeline queries

Successor and predecessor at interior positions:

Index Label Prev Next
0 e0 none e1
5 e5 e4 e6
10 e10 e9 e11
15 e15 e14 e16
19 e19 e18 none

Boundary events correctly report none for the missing direction.

6. Gap behavior

Removing one edge (c -> d) breaks forward traversal at the gap without breaking other edges:

Query Result
next of b c
next of c none
next of d e
next of e f
prev of e d (backward intact)

The gap is a clean stop, not a partial return.

7. Chain capacity ceiling

Maximum chain length walkable end-to-end at 90%+ accuracy:

D N=50 N=100 N=200 N=500
4096 1.000 1.000 0.160 0.002
8192 1.000 1.000 0.145 0.006

The ceiling is between N=100 and N=200 for both dimensions. Doubling D does not substantially raise the chain ceiling, because each event participates in two edges and per-event interference is roughly doubled compared to flat item storage.

8. Multi-hop exactness

D=8192, chain of 100 events. For four starting positions, walk 20 hops and count exact retrievals:

Start index Exact hops Attempts
0 20 20
25 20 20
50 20 20
75 20 20

All 80 hops across four starts are exact.

9. Interpolation between adjacent events

Query blends two adjacent events: q = alpha * e[5] + (1 - alpha) * e[6]. Unbinding gives a mixture of the successors of the two sources:

alpha Retrieved sim_e6 sim_e7
0.00 e7 0.0041 0.3360
0.25 e7 0.0529 0.3218
0.50 e7 0.2146 0.2046
0.75 e7 0.3276 0.0435
1.00 e4 0.3345 0.0141

The sim_e6 and sim_e7 columns show the mixture cleanly. At alpha=0 the successor is e7 (correct). At alpha=1 the successor should be e6, and sim_e6 = 0.3345 confirms this β€” the readout picks e4 because e6 is excluded from the argmax (see Known issues).

10. Narrative QA

One-hop, one-hop backward, and three-hop queries on a 12-event story:

Q: After 'alice_reads_letter', what happened?
A: alice_calls_bob

Q: Before 'they_discover_secret', what happened?
A: they_open_door

Q: What happened after 'they_discuss_clues' (3 steps)?
A: they_visit_house -> they_find_key -> they_open_door

11. Causal DAG with branching

A 9-node DAG with 10 directed edges. Nodes with multiple successors return all branches via successors():

successors of 'power_outage':
    elevators_stop        sim=0.3098
    traffic_lights_fail   sim=0.3023

successors of 'delayed_meetings':
    missed_connections    sim=0.3224

walk_forward picks one branch per step (greedy). Use successors() for DAG traversal with choice.

12. Alternating storylines

Two storylines with interleaved timestamps stay separate:

step alice bob
0 a_wake b_wake
1 a_breakfast b_run
2 a_walk b_shower
3 a_meeting b_commute
4 a_lunch b_lunch
5 a_work b_gym
6 a_home b_home
7 a_sleep b_sleep

Querying next of a_meeting returns a_lunch, not b_lunch. The two chains are stored in the same trace and remain separate.

API reference

NarrativeMemory

NarrativeMemory(d=4096, seed=0)

Event management

  • add_event(label) -> idx β€” add a single event.
  • add_chain(labels) -> [idx] β€” add a chain of events and link consecutive ones.
  • link(a, b) β€” add a directed edge between two existing events.
  • link_many([idx]) β€” link a list of indices as a chain.

Retrieval

  • query_next(idx) -> idx | None β€” successor of an event.
  • query_prev(idx) -> idx | None β€” predecessor of an event.
  • walk_forward(idx, max_steps=20) -> [idx] β€” walk forward until a gap or repeat.
  • walk_backward(idx, max_steps=20) -> [idx] β€” walk backward.
  • successors(idx, k=4, threshold=0.05) -> [(idx, sim)] β€” all successors above threshold, for DAGs.

Display

  • label(idx) -> str
  • describe(chain) -> str
  • stats() -> dict

Export

  • save_json(path) β€” events, edges, metadata.
  • plot(path) β€” timeline diagram (requires matplotlib).

Design notes

Why directed binding

Symmetric binding bind(a, b) = ifft(fft(a) * fft(b)) is commutative. For a chain stored as sum_i bind(e_i, e_{i+1}), the reverse edge bind(e_{i+1}, e_i) is the same object. Unbinding with e_{i+1} returns e_i, and the walk enters a 2-cycle.

Directed binding bind_dir(a, b) = ifft(fft(a) * roll(fft(b), 1)) breaks the symmetry. The reverse interference becomes noise rather than a structured competing signal. The self-test at the top of the demo verifies this: cos(unbind_dir(a, bind_dir(a, b)), b) = 1.000000.

Why two traces

Forward and backward walks need separate traces because directed binding is not self-inverse in the way symmetric binding is. S_next handles forward traversal; S_prev handles backward. The cost of both is the same as storing one, and the two directions are independent (removing a forward edge does not affect backward retrieval).

Why no learned codebook

Events are assigned random phase-only vectors. No training, no gradients, no embeddings. The substrate carries order through the binding operation, not through the vectors themselves. This makes the tool reproducible, inspectable, and dependency-free beyond NumPy.

Limitations

Chain capacity is D-limited. Around N=100 for D=4096, roughly the same for D=8192. Doubling D does not substantially raise it because each event participates in two edges. For longer sequences, split into multiple stores or use sparse anchoring.

Greedy walk on a DAG picks one branch. walk_forward returns the highest-similarity successor at each step. For explicit branching use successors() and traverse manually.

No deletion of individual events. Removing an event requires subtracting its two edges and re-linking the neighbors. A helper method for this is not yet implemented.

No cyclic chains. A chain where the last event links back to the first would walk into an infinite loop under walk_forward. The visited set prevents the loop, but the chain terminates early.

Known issues

EXP 9 (interpolation) excludes the correct answer at alpha=1.

The demo excludes indices 5 and 6 (the two source events) from the argmax, on the assumption that they are the query's own components and should not appear as answers. But unbind_dir returns the successor of the query. At alpha=1 (pure e5) the correct answer is e6 β€” which is one of the excluded indices. The sim_e6 = 0.3345 column shows the correct answer is present in the raw similarity vector; the argmax simply cannot select it.

To see the correct interpolation behavior, remove the source-event exclusion from the demo. The library itself does not exclude anything; the issue is only in the demonstration code.

EXP 7 shows D=8192 slightly worse than D=4096 at N=200.

Values 0.145 and 0.160 respectively. Both are deep in the degraded regime and the difference is within seed variation. It is not an inversion of the capacity law.

Citation

@misc{holo-narrative2026,
  title  = {holo-narrative: Ordered memory for events, timelines,
            and narrative chains},
  author = {zeechimp},
  year   = {2026},
  note   = {Single-file library for ordered retrieval from a
            holographic substrate.}
}

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.
  • Gayler, R. W. "Vector Symbolic Architectures Answer Jackendoff's Challenges." ICCS/ASCS (2003).

License

Apache 2.0

Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support