STDP parity: where state lives and how spikes pair#
This page is for users porting NEST spike-timing-dependent plasticity (STDP)
code to brainpy.state. It documents two things the source alone will not
tell you at a glance:
Where the learning state lives — NEST keeps the post-synaptic eligibility trace (and several rule parameters) on the neuron;
brainpy.statemoves them onto the synapse (or a broadcast node) so a rule runs standalone, with no archiving-capable neuron required. Setting a parameter on the wrong object relative to NEST is the single most common porting mistake.How each ``stdp_nn_*`` variant pairs spikes — the three nearest-neighbour schemes differ in subtle, load-bearing ways; this page states each pairing convention exactly and cites the NEST source line that defines it.
Every divergence below is backed by a live-NEST parity test (named per section); this page consolidates what those tests proved.
The online-substrate model in one paragraph#
brainpy.state STDP rules are pure update(state, ctx) kernels evaluated
every time step on a JAX event-driven substrate. The substrate maintains the
pre-synaptic K+ and post-synaptic K- traces (decay-then-add per neuron,
gathered per edge) and the kernel applies potentiation on the post spike and
depression on the pre spike. NEST instead defers the weight integral to the
synapse’s send() (the next pre spike), where its weight_recorder samples.
With no depression between pre spikes the two schemes apply the same operations in
the same order, so the weight coincides at every pre-spike (send) time — which
is exactly where parity is asserted. Keep this online-vs-deferred equivalence in
mind: it is why a few rules carry a small documented band rather than
bit-for-bit agreement (see Documented numerical divergences).
Trace storage: tau_minus is a synapse parameter here, a neuron parameter in NEST#
This is the canonical STDP trace-storage divergence.
In NEST, the post-synaptic trace K- is owned by the postsynaptic
neuron: tau_minus is an archiving_node parameter and the synapse reads
the trace back through get_K_value() at send() time. Setting
tau_minus on the synapse in NEST has no effect:
# NEST: tau_minus is a parameter of the POSTSYNAPTIC NEURON (ArchivingNode).
neuron = nest.Create("iaf_psc_alpha")
nest.SetStatus(neuron, {"tau_minus": 20.0}) # ms — drives the K- trace
nest.CopyModel("stdp_synapse", "syn", {"tau_plus": 20.0})
# the synapse carries tau_plus; the K- time constant lives on the neuron.
In brainpy.state, tau_minus is a synapse-spec attribute. It drives the
substrate’s per-post-neuron K- trace directly, so STDP runs without requiring
the post-synaptic neuron to implement an archiving API:
>>> import brainunit as u
>>> import brainpy.state as bp
>>> s = bp.stdp_synapse(tau_plus=20.0 * u.ms, tau_minus=20.0 * u.ms)
>>> float(u.Quantity(s.tau_minus).to_decimal(u.ms))
20.0
Porting rule. A NEST script that sets tau_minus on the neuron must set the
same value on the brainpy.state synapse spec. A live-NEST parity run sets both
the post neuron’s tau_minus and the synapse’s tau_minus to identical values
so the two K- traces match. The same move applies to every trace-based STDP
rule in the family (stdp_synapse, stdp_synapse_hom, stdp_pl_synapse_hom,
stdp_triplet_synapse and its second post trace tau_minus_triplet, the three
stdp_nn_* variants, stdp_facetshw_synapse_hom, and stdp_dopamine_synapse).
ht_synapse is trace-free and is therefore exempt.
Parameter-location map for the STDP family#
The trace-storage move generalises: wherever NEST precomputes a weight change on
an archiving neuron, brainpy.state relocates the governing parameters onto the
synapse spec (or, for the dopamine modulator, onto the volume_transmitter) so
the rule is self-contained. Parity always sets identical values on both sides.
Parameter(s) |
NEST home |
brainpy.state home |
Why it moves |
|---|---|---|---|
|
Post neuron — |
Synapse spec attribute (drives the substrate’s per-post |
Run STDP without an archiving-capable postsynaptic neuron. |
|
Post neuron — |
Synapse spec attribute (second per-post trace) |
Same as |
|
Post neuron — |
Synapse spec attributes |
Self-containment; mirrors the |
|
Post neuron ( |
Stays on the neuron (read per edge each step) |
The low-pass voltage filters are neuron state; recorded once, reused. |
|
Post neuron — archiving ring buffer (default |
Synapse spec attribute; the online reader uses a one-step lag |
No analog ring buffer online (see Clopath delay_u_bars and online LTP (≤ 5 % band)). |
|
Per-synapse state |
On the |
|
|
Common property, bound to the |
On the |
|
|
Common properties ( |
Synapse spec attributes |
Self-containment. |
Documented numerical divergences#
Three rules do not reproduce NEST bit-for-bit. In each case the direction and ordering of the weight change are exact; only a small magnitude band remains, and each band is asserted by a live-NEST parity test. Pull the numbers from here, not from a fresh measurement.
Clopath delay_u_bars and online LTP (≤ 5 % band)#
- What diverges:
The stored weight after spike-pairing.
- Why:
NEST evaluates LTP/LTD against post voltages held in a ring buffer delayed by
delay_u_bars(default4.0 ms); the online reader has no analog ring buffer and reads the post state with the substrate’s intrinsic one-step lag, so parity setsdelay_u_bars = 0.1 ms. The4.0 msdefault is not reproducible online. The residual is the online-instantaneous read versus NEST’s deferred decayed-history sum; it grows with pairing frequency.- Tolerance asserted:
A documented 5 % band plus exact direction and frequency-ordering (realised ≤ 3.31 %; a pure-LTD train matches to 0.002 %).
- Parity test:
brainpy_state/_nest_validation/clopath_synapse_parity_test.py.
Dopamine online-vs-deferred integration (~0.2 % band)#
- What diverges:
The weight trajectory under dopamine modulation.
- Why:
brainpy.stateintegrates the weight every step against the broadcastn(with a one-step lag), whereas NEST integrates lazily atsend()/trigger_update_weight. Becausenis a clean scalar carrying no analog history, the residual is far tighter than Clopath’s.- Tolerance asserted:
A ~0.2 % band (realised
max|Δw| < 8e-3 pAover LTP / LTD / clamp / window sweeps spanning 50–200 pA), with direction and ordering exact.- Parity test:
brainpy_state/_nest_validation/stdp_dopamine_synapse_parity_test.py.
Nearest-neighbour “phantom pre at 0” (stdp_nn_symm / stdp_nn_restr only)#
- What diverges:
A single facilitation against a virtual pre spike at
t = 0.- Why:
NEST’s first
send()initialisest_lastspike_ = 0, so a post spike that precedes the first real pre facilitates against a phantom pre at the origin. Thebrainpy.statesubstrate seeds its traces and eligibility flags at 0, modelling the physically-correct “no pre has occurred” — so it does not reproduce the phantom pairing.stdp_nn_pre_centered_synapseandstdp_facetshw_synapse_homare immune (their facilitation scales by a trace/flag that starts at 0).- Tolerance asserted:
Not reproduced (documented). Parity sidesteps it with a leading pre spike, or by placing the first pre late enough that the
exp(-(Δ)/tau_plus)phantom term sits below the test’s absolute tolerance.- Parity tests:
brainpy_state/_nest_validation/stdp_nn_symm_synapse_parity_test.pyand…/stdp_nn_restr_synapse_parity_test.py.
Nearest-neighbour pairing conventions#
The pair-based weight maps are identical to stdp_synapse
(potentiation on a post spike, depression on a pre spike):
with \(\hat w = w / W_{\max}\). What differs between the variants is which
spikes contribute to \(K^{+}\) and \(K^{-}\) — the pairing
convention. All three reproduce NEST to machine precision on single spike pairs
(realised abs. error ~1e-13–1e-15); each is named with its single-pair
regression test below.
The substrate supplies nearest-neighbour traces via a per-side
pre_trace_mode / post_trace_mode of 'nearest' (reset-to-1 on the
trace’s own spike, decay otherwise), so the value gathered at a partner spike is
the single nearest preceding pairing, not the all-to-all sum:
>>> import brainpy.state as bp
>>> sym = bp.stdp_nn_symm_synapse()
>>> sym.pre_trace_mode, sym.post_trace_mode
('nearest', 'nearest')
stdp_nn_symm_synapse — symmetric nearest-neighbour#
Each spike pairs only with its nearest partner on the other side: a post
spike facilitates with the nearest preceding pre spike (\(K^{+}\) from the
substrate’s 'nearest' pre trace), and a pre spike depresses with the nearest
preceding post spike (\(K^{-}\) from the 'nearest' post trace). The
kernel is byte-identical to stdp_synapse; the nearest-ness lives entirely
in the trace mode.
NEST source:
models/stdp_nn_symm_synapse.h—send()lines 246–297 (facilitate_at 280; nearestKminusdepression at 286–287).References: Morrison, Aertsen & Diesmann (2007) Neural Comput. 19:1437–1467; Morrison, Diesmann & Gerstner (2008) Biol. Cybern. 98:459–478, fig. 7A.
Divergence: see Nearest-neighbour “phantom pre at 0” (stdp_nn_symm / stdp_nn_restr only).
Parity test:
brainpy_state/_nest_validation/stdp_nn_symm_synapse_parity_test.py.
stdp_nn_restr_synapse — restricted symmetric nearest-neighbour#
The symmetric scheme plus a one-pairing-per-spike restriction: a post spike
facilitates with the nearest preceding pre only if a pre has occurred since the
previous post, and a pre spike depresses with the nearest preceding post only
if a post has occurred since the previous pre. brainpy.state carries this
with two per-edge eligibility flags (pre_avail / post_avail): each spike
makes its own side available and consumes the opposite, reproducing NEST’s
start != finish gate without scanning history.
NEST source:
models/stdp_nn_restr_synapse.h—send()lines 244–307 (facilitation gated onstart != finishat 270–283; nearestKminusdepression at 287–297).References: Morrison, Diesmann & Gerstner (2008) Biol. Cybern. 98:459–478, fig. 7C.
Divergence: see Nearest-neighbour “phantom pre at 0” (stdp_nn_symm / stdp_nn_restr only).
Parity test:
brainpy_state/_nest_validation/stdp_nn_restr_synapse_parity_test.py.
stdp_nn_pre_centered_synapse — presynaptic-centered nearest-neighbour#
Asymmetric. A post spike facilitates with all pre spikes since the last post
(an accumulated \(K^{+}\)), and that accumulator is reset to 0 on every
post spike; a pre spike depresses with the nearest preceding post (substrate
'nearest' \(K^{-}\)). Because the reset is triggered by the post spike,
two edges from one pre reset at different times, so \(K^{+}\) is a per-edge,
in-kernel accumulate-then-reset trace rather than a substrate per-neuron trace —
hence pre_trace_tau is None:
>>> import brainpy.state as bp
>>> pc = bp.stdp_nn_pre_centered_synapse()
>>> pc.pre_trace_tau is None, pc.post_trace_mode
(True, 'nearest')
This variant is immune to Nearest-neighbour “phantom pre at 0” (stdp_nn_symm / stdp_nn_restr only) (its facilitation scales by an accumulator that starts at 0).
NEST source:
models/stdp_nn_pre_centered_synapse.h—send()lines 249–317 (accumulatedKplusfacilitation + reset at 287–294; nearestKminusdepression at 299–302;Kplusdecay/+1 at 304).References: Morrison, Diesmann & Gerstner (2008) Biol. Cybern. 98:459–478, fig. 7B; Izhikevich & Desai (2003) Neural Comput. 15:1511–1523.
Parity test:
brainpy_state/_nest_validation/stdp_nn_pre_centered_synapse_parity_test.py.
stdp_facetshw_synapse_hom — hardware (FACETS) charge accumulation + LUT readout#
A hardware-emulating rule whose weight is quantised to a 4-bit index
(wple = Wmax / 15). Between periodic readouts it accumulates two charges in
the substrate’s 'nearest' mode:
a_causal— the nearest pre before this post, paired with the post (potentiation evidence);a_acausal— the nearest post before this pre, paired with the pre (depression evidence).
A readout (the first pre past each readout_cycle_duration) does
quantise → two eval_function evaluations → LUT lookup → reset → advance clock.
The readout runs before folding its triggering pair, so a readout never sees
its own pair’s charge — an observable ordering that the parity test pins.
NEST keys / scope: the common
tau_minusis exposed astau_minus_stdp;a_thresh_th/a_thresh_tlare per-synapse;no_synapsesandreadout_cycle_durationauto-compute (do not set them). Single-driver scope (E ≤ synapses_per_driver). On-grid weights (e.g.5 * wple) avoid theweight = 1.0footgun, which quantises to index 0 and is zeroed by the first readout.NEST source:
models/stdp_facetshw_synapse_hom.hand…_impl.h(send(): readout, quantisation, LUT update).Parity test:
brainpy_state/_nest_validation/stdp_facetshw_synapse_hom_parity_test.py.
See also#
NEST-Compatible Plasticity Models — the full NEST-compatible plasticity model list.
Validation status — overall NEST-compatibility parity evidence and tolerance categories.
Semantic divergences — the full semantic-divergence catalog.
NEST simulator documentation — the authoritative reference for upstream model semantics.