Temporal binding via STDP — asymmetric connections
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:
-
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.
-
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 someexcite()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:
- Connections become asymmetric under sequential observations. After observing
"A, B, A, B, A, B, ...",C[B-cells, A-cells]is measurably larger thanC[A-cells, B-cells]. - With
stdp_learning_rate = 0, connections stay symmetric — backward compatibility regression test. - 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”. - 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.