ADR 0008 · Accepted

Temporal binding via STDP — asymmetric connections

May 24, 2026

Decision 0008: Temporal binding via STDP — asymmetric connections

Date: 2026-05-24 Status: Accepted Builds on: ADRs 0001–0007

Context

The v0.6 experiments script ran four scenarios; one of them (“arithmetic — the original test from the architecture spec”) cleanly surfaced a structural limit. After training on sequences like "1 + 1 = 2", "1 + 2 = 3", the system queried with "1 + 1 =" produced an output distribution that reflected the buffer’s recent activity (=, +, 1 dominate) rather than what should come next (any digit, ideally 2).

The cause is architectural: connections under ADR 0003 are symmetric. C[i,j] = C[j,i]. They encode “these cells fire together” but not “this cell fires before that cell.” The mechanism cannot represent order.

This ADR introduces ordering. Connections become asymmetric and update via a STDP-style rule (spike-timing-dependent plasticity): if cell A fires just before cell B, the connection C[B,A] (cells whose firing predicts B’s firing) strengthens; the reverse direction strengthens less or even weakens. Over many observed token transitions, C[B,A] accumulates a directional bias toward “A typically precedes B.”

This does not by itself solve arithmetic — the system still has no mechanism for combining two operands into one answer. What it gives is the substrate for “the system has learned that a digit usually follows an =” or “a + usually follows a digit.” That’s enough to make output distributions during dreaming much more sequence-aware than they were in v0.6.

Decision

Track previous-tick activations

Add internal state _previous_activations to the system. After every tick (both observe() and excite()), the post-tick activation vector is saved as _previous_activations for use as the “pre-synaptic” snapshot on the next tick.

On a brand new system with no prior tick, _previous_activations is None and STDP is a no-op.

Asymmetric STDP update

On each tick, the connection update is split into two terms:

  1. Symmetric Hebbian component (the ADR 0003 rule, unchanged): cells that both activate above the mean on the current tick strengthen connections to each other in both directions.

  2. Asymmetric STDP component (new): cells that activated above the mean on the previous tick and cells that activate above the mean on the current tick strengthen the connection from pre to post.

Mathematically:

centered_post = post_activation - mean(post_activation)
centered_pre  = pre_activation  - mean(pre_activation)

# Existing symmetric Hebbian — unchanged
sym_update = connection_learning_rate · outer(centered_post, centered_post)

# New asymmetric STDP — pre influences post
asym_update = stdp_learning_rate · outer(centered_post, centered_pre)

C += sym_update + asym_update

In matrix indexing, C[i,j] represents the strength of cell j’s influence on cell i. After many transitions where j fires before i, C[i,j] grows; the reverse C[j,i] does not get the same update unless i also fires before j.

What “pre” means across observation and excitation

The STDP rule uses the activation vector from the previous tick as the pre-synaptic snapshot. That previous tick may have been:

  • A previous observe() call — sequence-encoding between externally provided tokens. E.g., observing "1" then "+" produces an STDP update where the "1"-cell activations are pre and the "+"-cell activations are post. Over many "1 +" observations, C["+", "1"] accumulates.
  • A previous excite() call — replay-induced sequence reinforcement. As the system dreams through past observations in buffer order, similar transitions get reinforced.
  • A previous observe() followed by some excite() ticks — STDP still applies but the “pre” was a replay rather than an observation. This is fine; the directional binding reflects whatever the system actually went through.

There’s no special case for which type of tick provided the “pre” — the rule is uniform.

Propagation during excitation now has direction

With asymmetric C, the propagation step in excite() (propagated = C @ activations) now flows asymmetrically:

propagated[i] = sum_j C[i,j] · activation[j]

activation[j] weighted by C[i,j] means: cell i gets excited proportional to how often j fires before i. After many "1 +" observations, when “1”-cells fire (say in response to observing or replaying "1"), the propagation step partially activates ”+“-cells next tick — even before observing "+".

This is the substrate for the system anticipating what comes next.

Defaults — STDP on by default

Unlike normalization and decay, STDP is enabled by default with a small stdp_learning_rate = 0.005 (half of connection_learning_rate = 0.01). The argument for on-by-default:

  • The mechanism adds new representational capacity (direction) without removing anything. Symmetric Hebbian still happens.
  • Existing tests don’t check connection symmetry, so they should still pass.
  • The clear architectural problem (no sequence binding at all in v0.6) outweighs the smaller risk of perturbing existing dynamics.

To disable, set stdp_learning_rate = 0.0.


What this fixes and what it doesn’t

Fixes

  • The directional poverty of the connection matrix. The system can now represent “A precedes B” as a distinct fact from “A and B fire together.”
  • Sequence anticipation during excitation: a cell that fired just before another tends to propagate activation to it on subsequent ticks.
  • Dreaming becomes more sequence-aware. Buffer order matters, not just buffer content.

Doesn’t fix (still open)

  • Arithmetic-as-such. “1 + 1 = ?” requires combining two operands into one answer. STDP doesn’t do that — it gives directional priors but no variable binding. After STDP, the system will tend to output a digit (because digits follow =), but it won’t know which digit. Closing that gap is the job of ADR 0009 (predictive cells with context).
  • Long-range dependencies. STDP looks one tick back. To learn “the token two positions back matters,” we’d need either multiple STDP layers (cells with longer receptive fields) or explicit context-buffer cells (which is what ADR 0009 introduces).
  • The variance-compression problem from ADRs 0005/0006. Adding STDP doesn’t change the activation dynamics; lateral inhibition and connection magnitudes still depend on activation variance, which normalization/decay still compress.

Stability considerations

Connection magnitudes can grow faster

The previous update had one component per tick. Now there are two (symmetric + asymmetric), so the matrix accumulates roughly twice as fast in the worst case. Existing connection_max clamping handles this, but the effective time constant for connection saturation is shorter. Default values still leave a comfortable margin.

Asymmetry creates new instabilities under propagation

With symmetric C, propagation (C @ A) has bounded spectral radius if connections stay bounded. With asymmetric C, the spectrum can be complex — but the magnitude of the operation is still bounded by connection_max · n_cells. The activation cap (2 · (baseline + saturation)) from ADR 0003 continues to prevent runaway.

Diagonal still forced to zero

Self-connections are still forced to zero after every update. A cell never causally precedes itself.


Behavioral predictions

These claims have tests in tests/test_stdp.py:

  1. Connections become asymmetric under sequential observations. After observing "A, B, A, B, A, B, ...", C[B-cells, A-cells] is measurably larger than C[A-cells, B-cells].
  2. With stdp_learning_rate = 0, connections stay symmetric — backward compatibility regression test.
  3. Propagation during excitation now reflects directional structure. If cells for “A” are active and C[B, A] is high, the next excitation tick activates cells for “B”.
  4. All prior 60 tests still pass with default STDP enabled.

Open questions deferred

  • Multi-tick STDP. Currently we only look back one tick. Real STDP has a window — connections strengthen for any pre/post pair within ~20 ms of each other, weighted by timing. A future ADR may extend to a window of K ticks.
  • STDP weight depression. Real STDP also weakens when post precedes pre. Currently we only do potentiation (strengthen pre→post); we don’t actively weaken post→pre. The symmetric component partially balances this, but a true LTD term may be useful.
  • STDP for the asking protocol. The “asking” lateral-inhibition mechanism (ADR 0003) is currently temporally instantaneous — cells inhibit peers within the same tick. A future ADR may extend inhibition to have temporal structure too.
← All decisions