diff options
| author | YurenHao0426 <Blackhao0426@gmail.com> | 2026-07-22 19:55:05 -0500 |
|---|---|---|
| committer | YurenHao0426 <Blackhao0426@gmail.com> | 2026-07-22 19:55:05 -0500 |
| commit | 661496c172a53d09c59951c642f1a468abb7f07e (patch) | |
| tree | 8555fcd5fa14bb619f4be843a96c3ae22e173020 /sdil | |
| parent | 0008f2cd96c06bea98f43560867d93967c597711 (diff) | |
mechanism: separate BCI role from performance velocity
Diffstat (limited to 'sdil')
| -rw-r--r-- | sdil/bci.py | 38 |
1 files changed, 34 insertions, 4 deletions
diff --git a/sdil/bci.py b/sdil/bci.py index c8408e3..2978e5a 100644 --- a/sdil/bci.py +++ b/sdil/bci.py @@ -37,10 +37,11 @@ class BCIConfig: @property def feedback_dim(self): - return 1 if self.feedback == "error" else 2 + return 1 if self.feedback in ("error", "performance_velocity") else 2 def validate(self): - if self.feedback not in ("error", "error_velocity"): + if self.feedback not in ( + "error", "error_velocity", "performance_velocity"): raise ValueError(f"unknown BCI feedback: {self.feedback}") if self.n_plus < 1 or self.n_minus < 1 or self.n_background < 1: raise ValueError("all BCI populations must be nonempty") @@ -141,6 +142,8 @@ class BCISDIL: return error.unsqueeze(1) improvement_velocity = (torch.zeros_like(error) if previous_abs_error is None else previous_abs_error - error.abs()) + if self.cfg.feedback == "performance_velocity": + return improvement_velocity.unsqueeze(1) return torch.stack((error, improvement_velocity), dim=1) def apical_components(self, soma, feedback): @@ -162,6 +165,20 @@ class BCISDIL: directional_derivative = (loss_plus - loss_minus) / (2.0 * sigma) return -directional_derivative.unsqueeze(1) * perturbations + def causal_role_targets(self, soma, perturbations): + """Estimate each cell's signed causal effect on the BCI cursor. + + Unlike ``causal_targets``, this target contains no instantaneous error + magnitude. A scalar antithetic cursor difference is tagged by the + locally available perturbation at each cell; its expectation is the + experimenter-unknown causal role vector. + """ + sigma = self.cfg.perturb_sigma + plus = (soma + sigma * perturbations) @ self.role + minus = (soma - sigma * perturbations) @ self.role + directional_derivative = (plus - minus) / (2.0 * sigma) + return directional_derivative.unsqueeze(1) * perturbations + def exact_causal_direction(self, soma, target): """Analytic diagnostic only; never used by a learning update.""" error = target - soma @ self.role @@ -185,6 +202,15 @@ class BCISDIL: def update_vectorizer(self, feedback, innovation, causal_target, active): if not bool(active.any()): return + if self.cfg.feedback == "performance_velocity": + # Role identification and performance modulation are deliberately + # separated. The former is an amortized node-perturbation estimate + # of dz/dh_i; the latter enters apical activity only through the + # within-episode performance innovation in ``feedback``. + target = causal_target[active].mean(0) + self.A[:, 0] += self.cfg.vectorizer_eta * ( + target - self.A[:, 0]) + return c = feedback[active] calibration_error = causal_target[active] - innovation[active] self.A += self.cfg.vectorizer_eta * ( @@ -228,8 +254,12 @@ class BCISDIL: and global_step % self.cfg.perturb_every == 0) causal_target = None if did_perturb: - causal_target = self.causal_targets( - base_soma, self.cfg.target, perturbations) + if self.cfg.feedback == "performance_velocity": + causal_target = self.causal_role_targets( + base_soma, perturbations) + else: + causal_target = self.causal_targets( + base_soma, self.cfg.target, perturbations) # All updates see the same pre-update state. The order below only # mutates parameters after every required local quantity is stored. |
