FlyBrain-Lab / src /compute /cpu_reference.py
timfromhcs's picture
FlyBrain v4.1.0 Space build (REAL_SUBGRAPH, CPU-only, honest backend)
3d46076 verified
Raw
History Blame Contribute Delete
5.2 kB
"""
Deterministic CPU reference implementation of classical Leaky Integrate-and-Fire (LIF).
UNIT CONTRACT (architecture C): the implementation works in NORMALIZED simulation
units by default (decay=0.85, threshold=1.0, v_reset=0.0, v_rest=0.0, t_ref=2).
Physical millivolt semantics are obtained by passing explicit parameters, with the
reference mapping V_norm = (V_mV + 70.0) / 20.0, i.e. rest -70mV -> 0.0,
threshold -50mV -> 1.0, reset -70mV -> 0.0. The Vulkan shader implements the
identical parametrized equations; CPU/GPU parity is validated by
scripts + tests, not assumed.
"""
import numpy as np
from typing import Tuple, Optional
def cpu_lif_step(
row_offsets: np.ndarray,
col_indices: np.ndarray,
weights: np.ndarray,
prev_spikes: np.ndarray,
external_inputs: np.ndarray,
potentials_in: np.ndarray,
refractory_in: Optional[np.ndarray] = None,
decay: float = 0.85,
threshold: float = 1.0,
v_reset: float = 0.0,
v_rest: float = 0.0,
t_ref: int = 2
) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
"""
Deterministic CPU reference implementation of classical Leaky Integrate-and-Fire (LIF) dynamics.
Implements:
- Synaptic current summation: I_syn_i = sum_j W_ij * S_j(t)
- Absolute refractory period handling (clamping to v_reset, spike suppression)
- Leaky subthreshold integration: V_cand = v_rest + (V_old - v_rest)*decay + I_syn + I_ext
- Action potential firing: S_i = 1.0, V_i = v_reset, R_i = t_ref when V_cand >= threshold
- Hard reset and hyperpolarization floor
Returns:
(potentials_out, spikes_out, refractory_out)
"""
N = len(potentials_in)
if refractory_in is None:
refractory_in = np.zeros(N, dtype=np.int32)
potentials_out = np.zeros(N, dtype=np.float32)
spikes_out = np.zeros(N, dtype=np.float32)
refractory_out = np.zeros(N, dtype=np.int32)
for i in range(N):
ref_count = int(refractory_in[i])
if ref_count > 0:
# In absolute refractory period
refractory_out[i] = ref_count - 1
potentials_out[i] = v_reset
spikes_out[i] = 0.0
continue
start_idx = row_offsets[i]
end_idx = row_offsets[i + 1]
synaptic_sum = 0.0
for k in range(start_idx, end_idx):
pre_idx = col_indices[k]
synaptic_sum += float(weights[k]) * float(prev_spikes[pre_idx])
v_old = float(potentials_in[i])
v_cand = v_rest + (v_old - v_rest) * decay + synaptic_sum + float(external_inputs[i])
if v_cand >= threshold:
spikes_out[i] = 1.0
potentials_out[i] = v_reset
refractory_out[i] = t_ref
else:
spikes_out[i] = 0.0
potentials_out[i] = max(v_cand, v_reset - 1.0)
refractory_out[i] = 0
return potentials_out, spikes_out, refractory_out
def cpu_brain_step(
row_offsets: np.ndarray,
col_indices: np.ndarray,
weights: np.ndarray,
prev_activations: np.ndarray,
external_inputs: np.ndarray,
potentials_in: np.ndarray,
refractory_in: Optional[np.ndarray] = None,
decay: float = 0.85,
threshold: float = 1.0,
v_reset: float = 0.0,
v_rest: float = 0.0,
t_ref: int = 2
) -> Tuple[np.ndarray, np.ndarray]:
"""
Backwards-compatible wrapper returning (potentials_out, spikes_out).
"""
pot, spk, _ = cpu_lif_step(
row_offsets=row_offsets,
col_indices=col_indices,
weights=weights,
prev_spikes=prev_activations,
external_inputs=external_inputs,
potentials_in=potentials_in,
refractory_in=refractory_in,
decay=decay,
threshold=threshold,
v_reset=v_reset,
v_rest=v_rest,
t_ref=t_ref
)
return pot, spk
def cpu_plasticity_step(
col_indices: np.ndarray,
weights: np.ndarray,
pre_activations: np.ndarray,
post_activations: np.ndarray,
row_offsets: np.ndarray,
learning_rate: float = 0.05,
reward: float = 1.0,
weight_decay: float = 0.01,
min_weight: float = 0.01,
max_weight: float = 1.0
) -> np.ndarray:
"""
Deterministic CPU reference implementation of the documented three-factor
reward-modulated Hebbian rule. Matches shaders/plasticity.comp exactly:
delta_w = lr * reward * (a_pre * a_post - decay * w)
where a_post is the postsynaptic (CSR row owner) activation of each synapse.
"""
M = len(weights)
new_weights = np.copy(weights)
pre_act = np.asarray(pre_activations, dtype=np.float64)
post_act = np.asarray(post_activations, dtype=np.float64)
# CSR row (postsynaptic neuron) owning each synapse k.
post_idx = np.searchsorted(np.asarray(row_offsets), np.arange(M), side="right") - 1
for k in range(M):
pre_idx = int(col_indices[k])
a_pre = float(pre_act[pre_idx])
a_post = float(post_act[int(post_idx[k])])
w = float(weights[k])
delta_w = learning_rate * reward * (a_pre * a_post - weight_decay * w)
new_w = float(np.clip(w + delta_w, min_weight, max_weight))
new_weights[k] = new_w
return new_weights