Spaces:
Sleeping
Sleeping
| """ | |
| 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 | |