Event-Driven Synaptic Plasticity#

This tutorial shows how spike events select sparse synapses for weight updates. It implements a minimal pair-based STDP example; it does not claim that this rule captures the full biological diversity of synaptic plasticity.

Contents#

  1. From Spike Events to Synaptic Updates

  2. Implement a Minimal STDP Rule

  3. Visualize the Learning Window

  4. Update CSR Weights from Pre- and Postsynaptic Events

  5. Apply Event-Driven Updates in a Network

  6. Summary and Next Steps

From Spike Events to Synaptic Updates#

Hebb’s Rule#

Hebbian ideas motivate activity-dependent weight changes, but a concrete implementation requires an explicit update rule, state variables, bounds, and a timing convention.

Spike-Timing-Dependent Plasticity#

Pair-based STDP uses decaying pre- and postsynaptic traces as summaries of recent events. A spike on one side triggers an update determined by the trace on the other side.

The Update Rule#

For an existing synapse from presynaptic neuron \(i\) to postsynaptic neuron \(j\), a pre-triggered update adds a scaled postsynaptic trace; a post-triggered update adds a scaled presynaptic trace. Signs determine potentiation or depression, and clipping enforces weight bounds.

BrainEvent Update Operations#

update_csr_on_binary_pre traverses outgoing CSR entries selected by presynaptic events. update_csr_on_binary_post uses a CSC index view plus a permutation back to CSR data order to traverse incoming entries selected by postsynaptic events.

import brainevent
import jax
import jax.numpy as jnp
import matplotlib.pyplot as plt
import numpy as np

Implement a Minimal STDP Rule#

tau_pre = 20.0
tau_post = 20.0
a_plus = 0.01
a_minus = 0.012
delta_t = jnp.linspace(-50.0, 50.0, 401)
learning_window = jnp.where(
    delta_t > 0,
    a_plus * jnp.exp(-delta_t / tau_post),
    -a_minus * jnp.exp(delta_t / tau_pre),
)

Visualize the Learning Window#

fig, ax = plt.subplots(figsize=(7, 3))
ax.plot(np.asarray(delta_t), np.asarray(learning_window))
ax.axhline(0.0, color="black", linewidth=0.8)
ax.axvline(0.0, color="black", linewidth=0.8)
ax.set(xlabel="relative event time", ylabel="weight change", title="Illustrative pair-based STDP window")
plt.tight_layout()
plt.show()
../../_images/762cc5f741aa7b134268e21e95cdbb379d3544b91cfeee81697cf70fd1e854b9.png

Update CSR Weights from Pre- and Postsynaptic Events#

dense_weights = jnp.array([
    [0.20, 0.00, 0.40],
    [0.00, 0.30, 0.00],
], dtype=jnp.float32)
csr = brainevent.CSR.fromdense(dense_weights)

pre_spike = jnp.array([True, False])
post_trace = jnp.array([0.01, 0.02, 0.03])
after_pre = brainevent.update_csr_on_binary_pre(
    csr.data, csr.indices, csr.indptr, pre_spike, post_trace,
    0.0, 1.0, shape=csr.shape,
)

csc_indptr, csc_indices, weight_indices = brainevent.csr_to_csc_index(
    csr.indptr, csr.indices, shape=csr.shape
)
post_spike = jnp.array([False, True, True])
pre_trace = jnp.array([-0.01, -0.02])
after_post = brainevent.update_csr_on_binary_post(
    after_pre, csc_indices, csc_indptr, weight_indices, pre_trace, post_spike,
    0.0, 1.0, shape=csr.shape,
)
after_post = jax.block_until_ready(after_post)
print("initial CSR data:", csr.data)
print("after pre-triggered update:", after_pre)
print("after post-triggered update:", after_post)
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
File /opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/site-packages/brainevent/_op/main.py:569, in XLACustomKernel._register_fallback_lowering.<locals>.fallback_kernel_fn(*args, **kwargs)
    568 try:
--> 569     kernel = entry.kernel_generator(**kwargs)
    570 except Exception as exc:

File /opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/site-packages/brainevent/_csr/plasticity_binary.py:180, in _csr_on_pre_numba_kernel_generator(spike_info, **kwargs)
    176 def _csr_on_pre_numba_kernel_generator(
    177     spike_info: jax.ShapeDtypeStruct,
    178     **kwargs
    179 ):
--> 180     import numba
    182     if spike_info.dtype == jnp.bool_:

ModuleNotFoundError: No module named 'numba'

The above exception was the direct cause of the following exception:

JaxStackTraceBeforeTransformation         Traceback (most recent call last)
File /opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/site-packages/brainevent/_csr/plasticity_binary.py:168, in update_csr_on_binary_pre()
    166 post_trace = u.Quantity(post_trace).to(wunit).mantissa
    167 weight = u.maybe_decimal(
--> 168     csr_on_pre_prim_call(
    169         weight, indices, indptr, pre_spike, post_trace, shape=shape, backend=backend
    170     )[0] * wunit
    171 )
    172 weight = u.math.clip(weight, w_min, w_max)

File /opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/site-packages/brainevent/_csr/plasticity_binary.py:427, in csr_on_pre_prim_call()
    424 assert weight.shape[0] == indices.shape[0], (
    425     f'weight shape {weight.shape}, indices shape {indices.shape}, indptr shape {indptr.shape} do not match.'
    426 )
--> 427 return update_csr_on_binary_pre_p(
    428     weight, indices, indptr, pre_spike, post_trace,
    429     outs=[jax.ShapeDtypeStruct(weight.shape, weight.dtype)],
    430     shape=shape,
    431     weight_info=jax.ShapeDtypeStruct(weight.shape, weight.dtype),
    432     indices_info=jax.ShapeDtypeStruct(indices.shape, indices.dtype),
    433     indptr_info=jax.ShapeDtypeStruct(indptr.shape, indptr.dtype),
    434     spike_info=jax.ShapeDtypeStruct(pre_spike.shape, pre_spike.dtype),
    435     trace_info=jax.ShapeDtypeStruct(post_trace.shape, post_trace.dtype),
    436     backend=backend,
    437 )

File /opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/site-packages/brainevent/_op/main.py:338, in __call__()
    337 flat_outs, tree_def = abstract_arguments(outs)
--> 338 r = self.primitive.bind(*ins, **kwargs, outs=tuple(flat_outs))
    339 if len(r) != len(flat_outs):

JaxStackTraceBeforeTransformation: brainevent.KernelCompilationError: Backend 'numba' failed to construct a kernel for primitive 'binary_csr_plast' on platform 'cpu': ModuleNotFoundError: No module named 'numba'. Available backend(s) for platform 'cpu': ['numba', 'jax_raw']. Switch with backend='<name>' on this call, or binary_csr_plast.set_default('cpu', '<name>') / brainevent.set_backend('cpu', '<name>') to change the default.

The preceding stack trace is the source of the JAX operation that, once transformed by JAX, triggered the following exception.

--------------------

The above exception was the direct cause of the following exception:

KernelCompilationError                    Traceback (most recent call last)
Cell In[4], line 9
      5 csr = brainevent.CSR.fromdense(dense_weights)
      6 
      7 pre_spike = jnp.array([True, False])
      8 post_trace = jnp.array([0.01, 0.02, 0.03])
----> 9 after_pre = brainevent.update_csr_on_binary_pre(
     10     csr.data, csr.indices, csr.indptr, pre_spike, post_trace,
     11     0.0, 1.0, shape=csr.shape,
     12 )

File /opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/site-packages/brainevent/_misc.py:1770, in NameScope.__call__(self, *args, **kwargs)
   1768 backend = kwargs.pop('backend', None)
   1769 jit_fn = self._get_jit_fn(backend)
-> 1770 return jit_fn(*args, **kwargs)

    [... skipping hidden 18 frame]

File /opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/site-packages/brainevent/_op/main.py:571, in XLACustomKernel._register_fallback_lowering.<locals>.fallback_kernel_fn(*args, **kwargs)
    569     kernel = entry.kernel_generator(**kwargs)
    570 except Exception as exc:
--> 571     raise KernelCompilationError(
    572         f"Backend '{backend_to_use}' failed to construct a kernel "
    573         f"for primitive '{self.name}' on platform '{platform}': "
    574         f"{type(exc).__name__}: {exc}." + _construction_error_suffix()
    575     ) from exc
    577 try:
    578     return kernel(*args)

KernelCompilationError: Backend 'numba' failed to construct a kernel for primitive 'binary_csr_plast' on platform 'cpu': ModuleNotFoundError: No module named 'numba'. Available backend(s) for platform 'cpu': ['numba', 'jax_raw']. Switch with backend='<name>' on this call, or binary_csr_plast.set_default('cpu', '<name>') / brainevent.set_backend('cpu', '<name>') to change the default.

Apply Event-Driven Updates in a Network#

The sparsity pattern is unchanged; only its stored weights change. The updated data can therefore be placed back into the same CSR structure and used immediately by an event-driven forward pass.

updated_csr = csr.with_data(after_post)
input_events = brainevent.BinaryArray(jnp.array([True, False]))
output = jax.block_until_ready(input_events @ updated_csr)
print("updated dense weights:\n", updated_csr.todense())
print("network output:", output)

Summary and Next Steps#

BrainEvent separates the event trigger from the sparse connectivity data: pre- and postsynaptic events select which stored CSR weights receive trace-based updates. For storage details, continue with CSR and CSC Sparse Matrices; for task-oriented operator selection, see Apply event-driven synaptic plasticity.