CoolFace
Apppublic

whatpull/neuronface

sourceHugging Faceupdated 4mo agoView on Hugging Face
0likes
vectorized.py554 linesDownload Raw Back to modules
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