whatpull/neuronface
0
1"""Session 37 Phase 7: Vectorized network scaffold (numpy 기반).2 3본 module 영역 NeuralNetwork (modules/network.py) 일부 alternative path 일부.4단일 neuron 영역 (modules/neuron.py) 영역 비트 단위 정확 일부 보장 영역 (Phase 7 본격 영역5정확도 검증 영역 turn 영역). 영역 1000+ 뉴런 일부 throughput 대부분 영역.6 7설계 결정 (Phase 7 본격, Session 37 영역):8- numpy float32 array 영역 (v / last_spike_time / pre_trace / post_trace 영역).9- synapse 영역 = 영역 array (pre_idx, post_idx, weight, delay).10- integrate + fire 영역 vectorized loop (영역 step 대부분 ufunc).11- STDP 영역 본 scaffold 영역 미포함 (Phase 7 후속 turn 영역).12- 영역 path (NeuralNetwork) 대부분 — anchor 영역.13 14Benchmark: VectorizedNetwork.benchmark(n_neurons, n_synapses, n_steps) 영역15ms / step 측정. 본격 vectorization 대부분 vs 영역 NeuralNetwork 일부.16"""17 18from __future__ import annotations19 20import time21from dataclasses import dataclass, field22from typing import Optional23 24import numpy as np25 26 27@dataclass28class VectorizedNetwork:29 """Phase 7 scaffold: numpy 기반 LIF 영역.30 31 대부분 fixed-size 일부 build 대부분 — 영역 동적 add 영역 032 (회로 build 후 immutable). 본격 영역 dynamic add 영역 future turn 영역.33 34 Fields:35 n_neurons: 일부.36 v: shape (n_neurons,) — 대부분 (mV).37 v_rest / v_threshold / v_reset: shape (n_neurons,) — 영역 LIF 영역.38 tau_m: shape (n_neurons,) — 일부 (ms).39 refractory: shape (n_neurons,) — 일부 (ms).40 last_spike_time: shape (n_neurons,) — 대부분 (ms, 영역 -inf).41 pre_idx / post_idx: shape (n_synapses,) — 대부분 영역.42 syn_weight: shape (n_synapses,) — 일부.43 syn_delay: shape (n_synapses,) — 일부 (ms).44 t: 대부분 (ms).45 dt: 영역 (ms).46 """47 48 n_neurons: int49 v: np.ndarray = field(default_factory=lambda: np.zeros(0, dtype=np.float32))50 v_rest: np.ndarray = field(default_factory=lambda: np.zeros(0, dtype=np.float32))51 v_threshold: np.ndarray = field(default_factory=lambda: np.zeros(0, dtype=np.float32))52 v_reset: np.ndarray = field(default_factory=lambda: np.zeros(0, dtype=np.float32))53 tau_m: np.ndarray = field(default_factory=lambda: np.zeros(0, dtype=np.float32))54 refractory: np.ndarray = field(default_factory=lambda: np.zeros(0, dtype=np.float32))55 syn_frozen: np.ndarray = field(default_factory=lambda: np.zeros(0, dtype=bool))56 # last_spike_time은 float64 — float32일 경우 0.6 저장 시 0.6000000238로 라운딩되어57 # refractory boundary check (t_exact - last_spike < refractory)에서 1-step off-by-one 발생.58 last_spike_time: np.ndarray = field(default_factory=lambda: np.zeros(0, dtype=np.float64))59 pre_idx: np.ndarray = field(default_factory=lambda: np.zeros(0, dtype=np.int32))60 post_idx: np.ndarray = field(default_factory=lambda: np.zeros(0, dtype=np.int32))61 syn_weight: np.ndarray = field(default_factory=lambda: np.zeros(0, dtype=np.float32))62 syn_delay: np.ndarray = field(default_factory=lambda: np.zeros(0, dtype=np.float32))63 syn_psp_duration_ms: np.ndarray = field(default_factory=lambda: np.zeros(0, dtype=np.float32))64 pending_inputs: np.ndarray = field(default_factory=lambda: np.zeros(0, dtype=np.float32))65 t: float = 0.066 dt: float = 0.167 # Session 37 Phase 7 sustained PSP: post 대부분 대부분 일부.68 # ring buffer 일부 — shape (max_psp_steps, n_neurons), 영역 step 대부분 영역.69 _psp_buffer: Optional[np.ndarray] = None70 _psp_max_steps: int = 071 _psp_offset: int = 0 # 영역 step 대부분 일부.72 # Phase 7 STDP vectorization (Session 37): pre/post traces + last_trace_time.73 pre_trace: np.ndarray = field(default_factory=lambda: np.zeros(0, dtype=np.float32))74 post_trace: np.ndarray = field(default_factory=lambda: np.zeros(0, dtype=np.float32))75 last_trace_time: np.ndarray = field(default_factory=lambda: np.zeros(0, dtype=np.float64))76 stdp_enabled: bool = False77 stdp_mode: str = "pair" # "pair" | "triplet"78 stdp_gain: float = 1.0 # Phase 5 dopamine modulation.79 # Triplet STDP traces (Pfister-Gerstner 2006).80 r1: np.ndarray = field(default_factory=lambda: np.zeros(0, dtype=np.float32))81 r2: np.ndarray = field(default_factory=lambda: np.zeros(0, dtype=np.float32))82 o1: np.ndarray = field(default_factory=lambda: np.zeros(0, dtype=np.float32))83 o2: np.ndarray = field(default_factory=lambda: np.zeros(0, dtype=np.float32))84 # Phase 1 D140 NMDA dendritic plateau (per-neuron opt-in).85 nmda_enabled_arr: np.ndarray = field(default_factory=lambda: np.zeros(0, dtype=bool))86 nmda_threshold_arr: np.ndarray = field(default_factory=lambda: np.zeros(0, dtype=np.float32))87 nmda_gain_arr: np.ndarray = field(default_factory=lambda: np.zeros(0, dtype=np.float32))88 89 @classmethod90 def from_neural_network(cls, neural_net) -> "VectorizedNetwork":91 """기존 NeuralNetwork (modules/network.py) 영역 VectorizedNetwork 일부.92 93 영역 + synapse 대부분. 영역 LIF 영역 (V_th / V_rest / tau_m 등) 일부.94 STDP / NMDA / dynamic add / sustained PSP 일부 본 변환 일부 0 (Phase 7 본격 영역 후속 turn).95 """96 names = list(neural_net.neurons.keys())97 n = len(names)98 idx_of = {nm: i for i, nm in enumerate(names)}99 net = cls(n_neurons=n)100 net.v = np.array([neural_net.neurons[nm].v for nm in names], dtype=np.float32)101 net.v_rest = np.array([neural_net.neurons[nm].v_rest for nm in names], dtype=np.float32)102 net.v_threshold = np.array([neural_net.neurons[nm].v_threshold for nm in names], dtype=np.float32)103 net.v_reset = np.array([neural_net.neurons[nm].v_reset for nm in names], dtype=np.float32)104 net.tau_m = np.array([neural_net.neurons[nm].tau_m for nm in names], dtype=np.float32)105 net.refractory = np.array([neural_net.neurons[nm].refractory for nm in names], dtype=np.float32)106 net.last_spike_time = np.full(n, -np.inf, dtype=np.float64)107 synapses = neural_net.synapses108 net.pre_idx = np.array([idx_of[s.pre.name] for s in synapses], dtype=np.int32)109 net.post_idx = np.array([idx_of[s.post.name] for s in synapses], dtype=np.int32)110 net.syn_weight = np.array([s.weight for s in synapses], dtype=np.float32)111 net.syn_delay = np.array([s.delay for s in synapses], dtype=np.float32)112 net.syn_frozen = np.array([getattr(s, "frozen", False) for s in synapses], dtype=bool)113 net.syn_psp_duration_ms = np.array(114 [getattr(s, "psp_duration_ms", 2.0) for s in synapses], dtype=np.float32,115 )116 net.pending_inputs = np.zeros(n, dtype=np.float32)117 net.dt = neural_net.dt118 net.t = neural_net.t119 net._names = names # 일부 (외부 일부).120 # Sustained PSP ring buffer 영역 — max_psp_steps = max(syn_psp_duration / dt).121 if synapses:122 max_psp_steps = max(1, int(np.ceil(net.syn_psp_duration_ms.max() / net.dt)))123 else:124 max_psp_steps = 1125 # 영역 delay 대부분 (대부분 영역 도달).126 max_delay_steps = max(1, int(np.ceil(net.syn_delay.max() / net.dt))) if synapses else 1127 net._psp_max_steps = max_psp_steps + max_delay_steps + 1128 net._psp_buffer = np.zeros((net._psp_max_steps, n), dtype=np.float32)129 net._psp_offset = 0130 # STDP traces 영역.131 net.pre_trace = np.zeros(n, dtype=np.float32)132 net.post_trace = np.zeros(n, dtype=np.float32)133 net.last_trace_time = np.full(n, -np.inf, dtype=np.float64)134 net.stdp_enabled = neural_net.stdp_enabled135 net.stdp_mode = getattr(neural_net, "stdp_mode", "pair")136 net.stdp_gain = 1.0 + getattr(neural_net, "dopamine", 0.0)137 # Triplet traces.138 net.r1 = np.zeros(n, dtype=np.float32)139 net.r2 = np.zeros(n, dtype=np.float32)140 net.o1 = np.zeros(n, dtype=np.float32)141 net.o2 = np.zeros(n, dtype=np.float32)142 # NMDA per-neuron parameters (D140).143 net.nmda_enabled_arr = np.array(144 [getattr(neural_net.neurons[nm], "nmda_enabled", False) for nm in names], dtype=bool,145 )146 net.nmda_threshold_arr = np.array(147 [getattr(neural_net.neurons[nm], "nmda_threshold", -60.0) for nm in names], dtype=np.float32,148 )149 net.nmda_gain_arr = np.array(150 [getattr(neural_net.neurons[nm], "nmda_gain", 2.0) for nm in names], dtype=np.float32,151 )152 return net153 154 def add_neuron(155 self,156 name: str,157 v_rest: float = -70.0,158 v_threshold: float = -55.0,159 v_reset: float = -75.0,160 tau_m: float = 15.0,161 refractory: float = 2.0,162 nmda_enabled: bool = False,163 nmda_threshold: float = -60.0,164 nmda_gain: float = 2.0,165 ) -> int:166 """Phase 7 dynamic add: 신규 뉴런 추가 후 인덱스 반환.167 168 모든 numpy 배열에 1 칸씩 append. 기존 ring buffer는 컬럼 1개 추가하여 확장.169 """170 if not hasattr(self, "_names"):171 self._names = []172 if name in self._names:173 raise ValueError(f"neuron '{name}' already exists")174 idx = len(self._names)175 self._names.append(name)176 self.n_neurons = idx + 1177 178 def _grow(arr: np.ndarray, fill, dtype) -> np.ndarray:179 new = np.empty(arr.size + 1, dtype=dtype)180 new[:arr.size] = arr181 new[arr.size] = fill182 return new183 184 self.v = _grow(self.v, v_rest, np.float32)185 self.v_rest = _grow(self.v_rest, v_rest, np.float32)186 self.v_threshold = _grow(self.v_threshold, v_threshold, np.float32)187 self.v_reset = _grow(self.v_reset, v_reset, np.float32)188 self.tau_m = _grow(self.tau_m, tau_m, np.float32)189 self.refractory = _grow(self.refractory, refractory, np.float32)190 self.last_spike_time = _grow(self.last_spike_time, -np.inf, np.float64) # float64 for precision191 self.pending_inputs = _grow(self.pending_inputs, 0.0, np.float32)192 self.pre_trace = _grow(self.pre_trace, 0.0, np.float32)193 self.post_trace = _grow(self.post_trace, 0.0, np.float32)194 self.last_trace_time = _grow(self.last_trace_time, -np.inf, np.float64) # float64 for precision195 self.r1 = _grow(self.r1, 0.0, np.float32)196 self.r2 = _grow(self.r2, 0.0, np.float32)197 self.o1 = _grow(self.o1, 0.0, np.float32)198 self.o2 = _grow(self.o2, 0.0, np.float32)199 self.nmda_enabled_arr = _grow(self.nmda_enabled_arr, nmda_enabled, bool)200 self.nmda_threshold_arr = _grow(self.nmda_threshold_arr, nmda_threshold, np.float32)201 self.nmda_gain_arr = _grow(self.nmda_gain_arr, nmda_gain, np.float32)202 # Ring buffer: 컬럼 1개 추가.203 if self._psp_buffer is not None:204 new_buf = np.zeros((self._psp_max_steps, self.n_neurons), dtype=np.float32)205 new_buf[:, : self.n_neurons - 1] = self._psp_buffer206 self._psp_buffer = new_buf207 return idx208 209 def add_synapse(210 self, pre_name_or_idx, post_name_or_idx,211 weight: float, delay: float = 1.0, psp_duration_ms: float = 2.0,212 ) -> int:213 """Phase 7 dynamic add: 신규 시냅스 추가 후 인덱스 반환."""214 def _resolve(x):215 if isinstance(x, str):216 return self._names.index(x)217 return int(x)218 pre_i = _resolve(pre_name_or_idx)219 post_i = _resolve(post_name_or_idx)220 self.pre_idx = np.append(self.pre_idx, np.int32(pre_i))221 self.post_idx = np.append(self.post_idx, np.int32(post_i))222 self.syn_weight = np.append(self.syn_weight, np.float32(weight))223 self.syn_delay = np.append(self.syn_delay, np.float32(delay))224 self.syn_psp_duration_ms = np.append(self.syn_psp_duration_ms, np.float32(psp_duration_ms))225 # 새 시냅스 PSP duration이 기존 max를 넘으면 ring buffer 확장.226 new_psp_steps = max(1, int(np.ceil(psp_duration_ms / self.dt)))227 new_delay_steps = max(1, int(np.ceil(delay / self.dt)))228 needed = new_psp_steps + new_delay_steps + 1229 if self._psp_buffer is not None and needed > self._psp_max_steps:230 old_buf = self._psp_buffer231 old_max = self._psp_max_steps232 self._psp_max_steps = needed233 new_buf = np.zeros((needed, self.n_neurons), dtype=np.float32)234 # Copy with offset re-alignment (시작점부터 ordering 유지 위해 offset 0 재시작).235 for i in range(old_max):236 new_buf[i] = old_buf[(self._psp_offset + i) % old_max]237 self._psp_buffer = new_buf238 self._psp_offset = 0239 return int(self.pre_idx.size - 1)240 241 def inject(self, neuron_name_or_idx, weight: float, duration_ms: float = 0.0):242 """외부 자극 — 영역 NeuralNetwork.inject 일부 (sustained PSP 영역 dt 영역 누적)."""243 idx = neuron_name_or_idx244 if isinstance(idx, str):245 if not hasattr(self, "_names"):246 raise ValueError("inject by name requires from_neural_network() (no _names map)")247 idx = self._names.index(idx)248 if duration_ms <= 0.0:249 self.pending_inputs[idx] += weight250 else:251 steps = max(1, int(duration_ms / self.dt))252 # 영역 step 영역 누적 — 영역 step 영역 weight 추가.253 # 영역 step 대부분 대부분 영역.254 if not hasattr(self, "_scheduled_inputs"):255 self._scheduled_inputs = []256 for k in range(steps):257 self._scheduled_inputs.append((idx, weight, self.t + k * self.dt))258 259 def step(self) -> np.ndarray:260 """One simulation step — vectorized integrate + fire + sustained PSP propagation.261 262 Sustained PSP semantics (Phase 7 본격, NeuralNetwork 정합):263 - 영역 spike 일부 syn_psp_duration_ms / dt 만큼 영역 step 대부분 영역.264 - 영역 step 일부 _psp_buffer ring 대부분 영역.265 Returns spike mask (bool array, True 대부분 step 영역 fire).266 """267 # Deliver scheduled inputs (inject) whose arrival <= current t.268 # NN과 정합: t = start + step * dt (fresh multiplication, no cumulative drift).269 if not hasattr(self, "_step_count"):270 self._step_count = 0271 self._t_start = self.t272 # 정확한 t (float drift 없음): t_exact = start + step * dt.273 t_exact = self._t_start + self._step_count * self.dt274 if hasattr(self, "_scheduled_inputs") and self._scheduled_inputs:275 remaining = []276 for idx, weight, arrival in self._scheduled_inputs:277 if arrival <= t_exact:278 self.pending_inputs[idx] += weight279 else:280 remaining.append((idx, weight, arrival))281 self._scheduled_inputs = remaining282 283 # Apply current step's PSP buffer slot to pending_inputs, then clear that slot.284 if self._psp_buffer is not None:285 slot = self._psp_offset % self._psp_max_steps286 self.pending_inputs += self._psp_buffer[slot]287 self._psp_buffer[slot] = 0.0288 289 # Refractory mask (NN과 동일 검사: t - last_spike < refractory).290 # last_spike_time이 float64이므로 float32 라운딩 문제 없음.291 in_refractory = (t_exact - self.last_spike_time) < self.refractory292 # Phase 1 D140 NMDA dendritic plateau (vectorized, opt-in per-neuron).293 # nmda_enabled_arr=True && v > nmda_threshold_arr → excitatory current × nmda_gain_arr.294 current = self.pending_inputs.copy()295 if self.nmda_enabled_arr.size > 0 and self.nmda_enabled_arr.any():296 nmda_active = self.nmda_enabled_arr & (self.v > self.nmda_threshold_arr) & (current > 0)297 if nmda_active.any():298 current = np.where(nmda_active, current * self.nmda_gain_arr, current)299 dv = (-(self.v - self.v_rest) + current) / self.tau_m * self.dt300 self.v = np.where(in_refractory, self.v_reset, self.v + dv)301 # Phase 7 bit-equivalence fix (Session 37):302 # NeuralNetwork.integrate는 refractory 중에 pending_inputs를 보존(early return) 후303 # 추후 refractory 종료 시 누적 입력을 한 번에 처리. 벡터화도 동일 동작 위해304 # refractory 중인 뉴런의 pending_inputs는 zero-out 안 함.305 self.pending_inputs = np.where(in_refractory, self.pending_inputs, 0.0).astype(np.float32)306 spike_mask = self.v >= self.v_threshold307 self.v = np.where(spike_mask, self.v_reset, self.v)308 # Record fire time using t_exact (no cumulative float drift).309 self.last_spike_time = np.where(spike_mask, t_exact, self.last_spike_time)310 311 # Propagate spikes — sustained PSP via ring buffer.312 if self.pre_idx.size > 0 and spike_mask.any():313 spiking_synapses = spike_mask[self.pre_idx]314 if spiking_synapses.any():315 if self._psp_buffer is not None:316 # 영역 spiking synapse 영역 delay + duration 대부분 영역.317 syn_post = self.post_idx[spiking_synapses]318 syn_w = self.syn_weight[spiking_synapses]319 syn_dur = self.syn_psp_duration_ms[spiking_synapses]320 syn_dly = self.syn_delay[spiking_synapses]321 # 영역 spiking synapse 영역 — 일부 vectorize 영역.322 for i in range(len(syn_post)):323 delay_steps = max(1, int(syn_dly[i] / self.dt))324 dur_steps = max(1, int(syn_dur[i] / self.dt))325 post = syn_post[i]326 w = syn_w[i]327 for k in range(dur_steps):328 slot_idx = (self._psp_offset + delay_steps + k) % self._psp_max_steps329 self._psp_buffer[slot_idx, post] += w330 else:331 # Fallback (no ring buffer 영역) — 1-step propagation.332 np.add.at(333 self.pending_inputs,334 self.post_idx[spiking_synapses],335 self.syn_weight[spiking_synapses],336 )337 338 # Phase 7 STDP vectorization. Pair (default) | Triplet (Pfister-Gerstner 2006).339 if self.stdp_enabled and spike_mask.any():340 if self.stdp_mode == "triplet":341 self._apply_triplet_stdp_vectorized(spike_mask, t_exact=t_exact)342 else:343 self._apply_stdp_vectorized(spike_mask, t_exact=t_exact)344 345 self._psp_offset += 1346 self._step_count += 1347 # Sync self.t to t_exact + dt (so external observers see precise time).348 self.t = self._t_start + self._step_count * self.dt349 return spike_mask350 351 def _apply_stdp_vectorized(self, spike_mask: np.ndarray, t_exact: Optional[float] = None) -> None:352 t_now = t_exact if t_exact is not None else self.t353 """Vectorized pair-based STDP (modules/neuron.py 영역 정합).354 355 영역 step 대부분:356 1. 일부 traces 갱신 (decay + +1).357 2. 일부 incoming synapse (post=spiking) 영역 LTP: Δw = +A_PLUS × gain × x_pre × (W_MAX - w).358 3. 일부 outgoing synapse (pre=spiking) 영역 LTD: Δw = -A_MINUS × gain × y_post × (w - W_MIN).359 영역 inhibitory synapse (weight ≤ 0) 대부분.360 """361 from .neuron import (362 STDP_A_PLUS, STDP_A_MINUS,363 STDP_TAU_PLUS_MS, STDP_TAU_MINUS_MS,364 STDP_W_MAX, STDP_W_MIN,365 )366 367 # Update traces for spiking neurons: trace_new = trace_old × exp(-dt_lag/tau) + 1.368 spiking_idx = np.where(spike_mask)[0]369 for idx in spiking_idx:370 last = self.last_trace_time[idx]371 if last == -np.inf:372 self.pre_trace[idx] = 1.0373 self.post_trace[idx] = 1.0374 else:375 dt_lag = t_now - last376 self.pre_trace[idx] = self.pre_trace[idx] * np.exp(-dt_lag / STDP_TAU_PLUS_MS) + 1.0377 self.post_trace[idx] = self.post_trace[idx] * np.exp(-dt_lag / STDP_TAU_MINUS_MS) + 1.0378 self.last_trace_time[idx] = t_now379 380 # LTP: incoming synapses where post is spiking.381 post_spiking = spike_mask[self.post_idx]382 excitatory = self.syn_weight > 0.0383 # pre 영역 trace_time 대부분 (영역 -inf).384 pre_trace_time = self.last_trace_time[self.pre_idx]385 valid = post_spiking & excitatory & np.isfinite(pre_trace_time) & ~self.syn_frozen386 if valid.any():387 dt_lag = t_now - pre_trace_time[valid]388 x_pre = self.pre_trace[self.pre_idx[valid]] * np.exp(-dt_lag / STDP_TAU_PLUS_MS)389 w = self.syn_weight[valid]390 delta = STDP_A_PLUS * self.stdp_gain * x_pre * (STDP_W_MAX - w)391 self.syn_weight[np.where(valid)[0]] = np.clip(w + delta, STDP_W_MIN, STDP_W_MAX)392 393 # LTD: outgoing synapses where pre is spiking.394 pre_spiking = spike_mask[self.pre_idx]395 post_trace_time = self.last_trace_time[self.post_idx]396 valid = pre_spiking & excitatory & np.isfinite(post_trace_time) & ~self.syn_frozen397 if valid.any():398 dt_lag = t_now - post_trace_time[valid]399 y_post = self.post_trace[self.post_idx[valid]] * np.exp(-dt_lag / STDP_TAU_MINUS_MS)400 w = self.syn_weight[valid]401 delta = STDP_A_MINUS * self.stdp_gain * y_post * (w - STDP_W_MIN)402 self.syn_weight[np.where(valid)[0]] = np.clip(w - delta, STDP_W_MIN, STDP_W_MAX)403 404 def _apply_triplet_stdp_vectorized(self, spike_mask: np.ndarray, t_exact: Optional[float] = None) -> None:405 t_now = t_exact if t_exact is not None else self.t406 """Vectorized triplet STDP (Pfister-Gerstner 2006, modules/neuron.py 정합).407 408 Post-fire (self=post): LTP via incoming. Δw = +(A2_PLUS × r1_pre + A3_PLUS × r1_pre × o2_self_pre_eps).409 Pre-fire (self=pre): LTD via outgoing. Δw = -(A2_MINUS × o1_post + A3_MINUS × o1_post × r2_self_pre_eps).410 Inhibitory (weight ≤ 0) skip. Soft-bound clip [W_MIN, W_MAX].411 """412 from .neuron import (413 TRIPLET_TAU_PLUS_MS, TRIPLET_TAU_X_MS,414 TRIPLET_TAU_MINUS_MS, TRIPLET_TAU_Y_MS,415 TRIPLET_A_2_PLUS, TRIPLET_A_3_PLUS,416 TRIPLET_A_2_MINUS, TRIPLET_A_3_MINUS,417 STDP_W_MIN, STDP_W_MAX,418 )419 420 # Update triplet traces (decay × exp + 1) for spiking neurons.421 spiking_idx = np.where(spike_mask)[0]422 for idx in spiking_idx:423 last = self.last_trace_time[idx]424 if last == -np.inf:425 self.r1[idx] = 1.0426 self.r2[idx] = 1.0427 self.o1[idx] = 1.0428 self.o2[idx] = 1.0429 else:430 dt_lag = t_now - last431 self.r1[idx] = self.r1[idx] * np.exp(-dt_lag / TRIPLET_TAU_PLUS_MS) + 1.0432 self.r2[idx] = self.r2[idx] * np.exp(-dt_lag / TRIPLET_TAU_X_MS) + 1.0433 self.o1[idx] = self.o1[idx] * np.exp(-dt_lag / TRIPLET_TAU_MINUS_MS) + 1.0434 self.o2[idx] = self.o2[idx] * np.exp(-dt_lag / TRIPLET_TAU_Y_MS) + 1.0435 self.last_trace_time[idx] = t_now436 437 excitatory = self.syn_weight > 0.0438 439 # LTP (post-fire): incoming syn where post is spiking.440 # delta = +A2_PLUS × r1_pre(t) + A3_PLUS × r1_pre(t) × o2_post(t-eps).441 # o2_post(t-eps) = self.o2[post] - 1 (post fired this step, trace already updated).442 post_spiking = spike_mask[self.post_idx]443 pre_trace_time = self.last_trace_time[self.pre_idx]444 valid = post_spiking & excitatory & np.isfinite(pre_trace_time) & ~self.syn_frozen445 if valid.any():446 dt_lag = t_now - pre_trace_time[valid]447 r1_pre = self.r1[self.pre_idx[valid]] * np.exp(-dt_lag / TRIPLET_TAU_PLUS_MS)448 o2_post_eps = self.o2[self.post_idx[valid]] - 1.0449 w = self.syn_weight[valid]450 delta = (TRIPLET_A_2_PLUS * r1_pre + TRIPLET_A_3_PLUS * r1_pre * o2_post_eps) * self.stdp_gain451 self.syn_weight[np.where(valid)[0]] = np.clip(w + delta * (STDP_W_MAX - w), STDP_W_MIN, STDP_W_MAX)452 453 # LTD (pre-fire): outgoing syn where pre is spiking.454 # delta = -A2_MINUS × o1_post(t) - A3_MINUS × o1_post(t) × r2_pre(t-eps).455 # r2_pre_eps = self.r2[pre] - 1 (pre fired this step).456 pre_spiking = spike_mask[self.pre_idx]457 post_trace_time = self.last_trace_time[self.post_idx]458 valid = pre_spiking & excitatory & np.isfinite(post_trace_time) & ~self.syn_frozen459 if valid.any():460 dt_lag = t_now - post_trace_time[valid]461 o1_post = self.o1[self.post_idx[valid]] * np.exp(-dt_lag / TRIPLET_TAU_MINUS_MS)462 r2_pre_eps = self.r2[self.pre_idx[valid]] - 1.0463 w = self.syn_weight[valid]464 delta = (TRIPLET_A_2_MINUS * o1_post + TRIPLET_A_3_MINUS * o1_post * r2_pre_eps) * self.stdp_gain465 self.syn_weight[np.where(valid)[0]] = np.clip(w - delta * (w - STDP_W_MIN), STDP_W_MIN, STDP_W_MAX)466 467 @classmethod468 def random(469 cls,470 n_neurons: int,471 n_synapses: int,472 v_rest: float = -70.0,473 v_threshold: float = -55.0,474 v_reset: float = -75.0,475 tau_m: float = 15.0,476 refractory: float = 2.0,477 weight_mean: float = 8.0,478 weight_std: float = 1.0,479 delay_mean: float = 1.0,480 seed: int = 42,481 dt: float = 0.1,482 ) -> "VectorizedNetwork":483 """Random network — benchmark / sanity 영역."""484 rng = np.random.default_rng(seed)485 net = cls(n_neurons=n_neurons)486 net.v = np.full(n_neurons, v_rest, dtype=np.float32)487 net.v_rest = np.full(n_neurons, v_rest, dtype=np.float32)488 net.v_threshold = np.full(n_neurons, v_threshold, dtype=np.float32)489 net.v_reset = np.full(n_neurons, v_reset, dtype=np.float32)490 net.tau_m = np.full(n_neurons, tau_m, dtype=np.float32)491 net.refractory = np.full(n_neurons, refractory, dtype=np.float32)492 net.last_spike_time = np.full(n_neurons, -np.inf, dtype=np.float64)493 net.pre_idx = rng.integers(0, n_neurons, size=n_synapses).astype(np.int32)494 net.post_idx = rng.integers(0, n_neurons, size=n_synapses).astype(np.int32)495 net.syn_weight = rng.normal(weight_mean, weight_std, size=n_synapses).astype(np.float32)496 net.syn_delay = np.full(n_synapses, delay_mean, dtype=np.float32)497 net.pending_inputs = np.zeros(n_neurons, dtype=np.float32)498 net.dt = dt499 return net500 501 def run(self, duration_ms: float) -> int:502 """duration_ms 영역 step 영역 — 총 spike 일부.503 504 실행 후 self.per_neuron_spike_count (shape: n_neurons) 에505 각 뉴런의 spike 횟수를 누적 저장 — inject_feature16 vectorized path506 의 firing_rate 계산에 사용 (Fix 3).507 """508 steps = int(duration_ms / self.dt)509 total_spikes = 0510 self.per_neuron_spike_count = np.zeros(self.n_neurons, dtype=np.int32)511 for _ in range(steps):512 mask = self.step()513 self.per_neuron_spike_count += mask.astype(np.int32)514 total_spikes += int(mask.sum())515 return total_spikes516 517 def firing_rates(self, names_subset=None):518 """대부분 일부 spike count 일부 (Hz, 영역 self.t 영역).519 520 영역 SpikeMonitor 영역 — spike count 대부분. 영역 후속 turn 영역.521 영역 last_spike_time 일부 (대부분 일부).522 """523 # Placeholder — Phase 7 본격 일부 SpikeMonitor 통합 영역.524 return {}525 526 @classmethod527 def benchmark(528 cls,529 n_neurons: int = 1000,530 n_synapses: int = 5000,531 n_steps: int = 500,532 dt: float = 0.1,533 ) -> dict:534 """Benchmark vectorized run — ms/step 영역.535 536 Phase 7 본격 영역 baseline 영역. 영역 NeuralNetwork (modules/network.py) 영역537 대부분 영역 (대부분 일부).538 """539 net = cls.random(n_neurons=n_neurons, n_synapses=n_synapses, dt=dt)540 # Warm-up: 영역 step (numpy first-call overhead 영역).541 net.step()542 start = time.perf_counter()543 for _ in range(n_steps):544 net.step()545 elapsed = time.perf_counter() - start546 return {547 "n_neurons": n_neurons,548 "n_synapses": n_synapses,549 "n_steps": n_steps,550 "elapsed_sec": round(elapsed, 4),551 "ms_per_step": round(elapsed * 1000 / n_steps, 4),552 "throughput_neuron_steps_per_sec": int(n_neurons * n_steps / elapsed) if elapsed > 0 else 0,553 }554 