Event-Driven Operators#

In a spiking neural network only a small fraction of neurons fire on any given step. A dense matrix multiply ignores this: it pays for every connection whether or not the presynaptic neuron spiked. Event-driven operators exploit the sparsity of spike trains — their cost scales with the number of active inputs, not the total number of neurons — while producing exactly the same result as the dense computation.

BrainState provides several, covering the connectivity patterns used in large-scale SNNs:

  • EventLinear — event-driven dense connectivity;

  • EventFixedProb — sparse random connectivity with a fixed connection probability;

  • FixedNumConn / EventFixedNumConn — a fixed number of connections per neuron.

import jax.numpy as jnp

import brainstate
import brainstate.nn as nn
import braintools

brainstate.random.seed(0)
brainstate.__version__
An NVIDIA GPU may be present on this machine, but a CUDA-enabled jaxlib is not installed. Falling back to cpu.
'0.4.0'

Sparse spike vectors#

A layer’s input is a spike vector: mostly zeros, with a one at each neuron that fired. We build a population of 200 neurons with roughly 10% activity.

n_pre, n_post = 200, 100
spikes = (brainstate.random.rand(n_pre) < 0.1).astype(float)
print('active inputs:', int(spikes.sum()), 'of', n_pre)
active inputs: 25 of 200

EventLinear: event-driven dense connectivity#

EventLinear holds a full weight matrix, exactly like nn.Linear, but performs the matrix-vector product event-driven: it accumulates only the columns selected by spiking inputs. The result is identical to the dense product — only the cost differs.

weight = brainstate.random.randn(n_pre, n_post) * 0.1
event_linear = nn.EventLinear(n_pre, n_post, weight=weight)

event_out = event_linear(spikes)
dense_out = spikes @ weight

print('output shape:', event_out.shape)
print('matches dense matmul:', bool(jnp.allclose(event_out, dense_out, atol=1e-5)))
output shape: (100,)
matches dense matmul: True

Spikes are naturally boolean, and EventLinear accepts boolean input directly — the most efficient form, since a spike is simply present or absent.

bool_out = event_linear(spikes.astype(bool))
print('boolean spikes give the same result:', bool(jnp.allclose(bool_out, dense_out, atol=1e-5)))
boolean spikes give the same result: True

EventFixedProb: sparse random connectivity#

Cortical-scale models are not densely connected — each neuron contacts a small random subset of targets. EventFixedProb represents this directly: conn_num is the connection probability, and the operator never materialises the full dense matrix, so memory scales with the number of actual synapses.

sparse_syn = nn.EventFixedProb(
    n_pre, n_post,
    conn_num=0.2,        # each post neuron receives ~20% of pre neurons
    conn_weight=0.5,
)

sparse_out = sparse_syn(spikes)
print('sparse output shape:', sparse_out.shape)
sparse output shape: (100,)

FixedNumConn: a fixed number of connections#

When biological fan-in must be controlled exactly, FixedNumConn wires each neuron to a fixed number of partners rather than a probability. EventFixedNumConn is its event-driven form for spiking input.

fixed_syn = nn.FixedNumConn(
    n_pre, n_post,
    conn_num=10,         # exactly 10 connections per neuron
    conn_weight=0.5,
)

fixed_out = fixed_syn(spikes)
print('fixed-fan-in output shape:', fixed_out.shape)
fixed-fan-in output shape: (100,)

When to use event-driven operators#

Reach for these operators whenever a connection’s input is a spike train:

  • Sparse activity — if only a few percent of neurons fire per step, event-driven evaluation avoids the wasted work of a dense multiply.

  • Sparse connectivityEventFixedProb and EventFixedNumConn store only real synapses, so large networks fit in memory.

  • Drop-in correctness — the numerical result equals the dense equivalent, so you can prototype with nn.Linear and switch to EventLinear for scale without changing the model’s behaviour.

See also#