Binary Events and Event-Driven Operations#

This tutorial develops the event side of BrainEvent: create binary event arrays, inspect them, multiply them by data, and process a complete time series without a Python time-step loop.

Creating Binary Events#

Creating Events from Array-Like Inputs#

import brainevent
import brainstate
import jax
import jax.numpy as jnp
import numpy as np

from_list = brainevent.BinaryArray([1, 0, 1, 0])
from_numpy = brainevent.BinaryArray(np.array([True, False, True]))
from_jax = brainevent.BinaryArray(jnp.array([False, True, True]))
print(from_list)
print(from_numpy)
print(from_jax)
BinaryArray(value=[1 0 1 0], dtype=int32)
BinaryArray(value=[ True False  True], dtype=bool)
BinaryArray(value=[False  True  True], dtype=bool)

Representing Simulated Spikes#

brainstate.random.seed(11)
spike_values = brainstate.random.bernoulli(0.25, size=(12,))
spikes = brainevent.BinaryArray(spike_values)
print(spikes)
print("active events:", int(spike_values.sum()))
BinaryArray(value=[False  True False False False  True False False False False False False], dtype=bool)
active events: 2

Inspecting and Transforming Binary Events#

Indexing#

events_2d = brainevent.BinaryArray([[1, 0, 1], [0, 1, 0]])
print("first row:", events_2d[0])
print("last two columns:", events_2d[:, 1:])
first row: [1 0 1]
last two columns: [[0 1]
 [1 0]]

Reductions and Logical Operations#

event_a = brainevent.BinaryArray([1, 0, 1, 0])
event_b = brainevent.BinaryArray([1, 1, 0, 0])
print("events per row:", jnp.sum(events_2d.value, axis=1))
print("A AND B:", jnp.logical_and(event_a.value, event_b.value))
print("A OR B:", jnp.logical_or(event_a.value, event_b.value))
events per row: [2 1]
A AND B: [ True False False False]
A OR B: [ True  True  True False]

Event-Driven Matrix Multiplication#

Binary Events with Dense Data#

pre_spikes = brainevent.BinaryArray([1, 0, 1, 0, 1])
weights = jnp.array([
    [0.5, 0.2, 0.1],
    [0.3, 0.4, 0.2],
    [0.1, 0.5, 0.3],
    [0.2, 0.1, 0.4],
    [0.4, 0.3, 0.5],
])
post_input = jax.block_until_ready(pre_spikes @ weights)
print(post_input)
[1.  1.  0.9]

Correctness and Performance Comparison#

The event-driven result must first match ordinary dense multiplication. Timing is a separate question: warm up compiled work, synchronize every measured result, and report the backend, shapes, event density, and repetition count before interpreting a speed difference.

dense_spikes = jnp.array([1, 0, 1, 0, 1], dtype=weights.dtype)
dense_result = jax.block_until_ready(dense_spikes @ weights)
event_result = jax.block_until_ready(pre_spikes @ weights)
print("results match:", bool(jnp.allclose(event_result, dense_result)))
results match: True

A Small Event-Driven Feedforward Network#

brainstate.random.seed(19)
w1 = brainstate.random.normal(size=(5, 4)) * 0.2
w2 = brainstate.random.normal(size=(4, 2)) * 0.2
hidden_drive = pre_spikes @ w1
hidden_events = brainevent.BinaryArray(hidden_drive > 0.15)
network_output = jax.block_until_ready(hidden_events @ w2)
print("hidden events:", hidden_events)
print("network output:", network_output)
hidden events: BinaryArray(value=[ True False False  True], dtype=bool)
network output: [ 0.28277686 -0.12534817]

Processing Time-Series Events#

BinaryArray accepts a two-dimensional event matrix, so the time axis can be processed in one compiled matrix operation rather than a Python loop.

brainstate.random.seed(23)
spike_trains = brainstate.random.bernoulli(0.1, size=(40, 12))
brainstate.random.seed(29)
readout_weights = brainstate.random.normal(size=(12, 3)) * 0.1
time_series_output = jax.block_until_ready(
    brainevent.BinaryArray(spike_trains) @ readout_weights
)
print("input shape:", spike_trains.shape)
print("output shape:", time_series_output.shape)
input shape: (40, 12)
output shape: (40, 3)

Summary and Next Steps#

BinaryArray represents vector or batched binary events and composes with dense data and JAX synchronization. Continue with Event-Driven Synaptic Plasticity for event-triggered weight updates, or move to Data to choose a connectivity representation.