Spike-timing-dependent plasticity
When an input spikes shortly before its neuron does, it probably helped, and STDP strengthens the synapse. When it spikes shortly after, it didn't, and STDP weakens it. The closer the two spikes, the larger the change. sparx.dynamics.PairSTDP is NEST's stdp_synapse: each neuron keeps a trace of its recent spikes, and each spike reads the other side's trace.
Each point runs the rule on one pair of spikes at that lag, starting from half the maximum weight. With additive bounds every change has the same size wherever the weight is, which pushes weights toward 0 or the maximum. With multiplicative bounds a strong synapse grows less and a weak one shrinks less, which keeps them in between.
Finding a pattern in noise
Masquelier, Guyonneau and Thorpe (2008) showed what this rule can do on its own. 400 afferents fire at random, at 30 Hz plus 10 Hz of noise. Now and then, for 50 ms, the first 200 replay the same frozen spike pattern, shaded below, at the same rate, so no afferent's rate gives it away. One neuron listens to all of them through STDP, with nothing telling it the pattern exists.
At first the neuron fires anywhere. Inputs that happened to come before its spikes grow, and since the pattern always repeats the same spikes in the same order, its afferents grow more often than the others. Within about a minute of simulated time the neuron stops firing outside the pattern and fires inside most of them. The weights of the pattern's afferents (violet) split into strong and weak, and the others fall.
This is a smaller version of their experiment, with sparx's LeakyIntegrateAndFire and PairSTDP in place of their neuron and rule. At their depression of alpha = 0.85 it fell silent before finding the pattern, so this one uses 0.55.
The code
import jax
from sparx.dynamics import Delta, LeakyIntegrateAndFire, PairSTDP, Receptorfrom sparx.graph import FixedProbability, Network, PoissonInput, Population, Projection, simulate
stdp = PairSTDP(tau_plus=16.8, tau_minus=33.7, lambda_=0.005, alpha=0.55, mu_plus=0.0, mu_minus=0.0, w_max=1.0) # NEST's stdp_synapse, additiveneuron = LeakyIntegrateAndFire(tau_m=10.0, e_l=0.0, v_th=20.0, v_reset=0.0, t_ref=1.0)network = Network( populations=(Population("in", 400, LeakyIntegrateAndFire(), {"ampa": Receptor(Delta())}), Population("out", 1, neuron, {"ampa": Receptor(Delta())})), projections=(Projection("in", "out", FixedProbability(1.0), weight=0.475, delay=1.0, receptor="ampa", plasticity=stdp),), inputs=(PoissonInput("in", rate=40.0, weight=30.0, receptor="ampa"),), dt=1.0,)result = simulate(network, network.init(jax.random.key(0)), duration=2000.0, key=jax.random.key(1))pre, post, weight, delay = network.connections(result.variables)["in->out:ampa"]print(weight.min(), weight.max()) # the weights as STDP left themChecked
The browser's PairSTDP runs on recorded spikes beside sparx's in site/test/learning.test.ts: twelve synapses for 3,000 steps, additive and multiplicative, agree to 2e-15. Its neuron is the LeakyIntegrateAndFire that Neurons checks.
sparx's pair, triplet and dopamine-modulated STDP match NEST's synapses: every transmitted weight within 1e-10 relative (fidelity ledger).