summaryrefslogtreecommitdiff
path: root/sdil
diff options
context:
space:
mode:
authorYurenHao0426 <Blackhao0426@gmail.com>2026-07-22 19:55:05 -0500
committerYurenHao0426 <Blackhao0426@gmail.com>2026-07-22 19:55:05 -0500
commit661496c172a53d09c59951c642f1a468abb7f07e (patch)
tree8555fcd5fa14bb619f4be843a96c3ae22e173020 /sdil
parent0008f2cd96c06bea98f43560867d93967c597711 (diff)
mechanism: separate BCI role from performance velocity
Diffstat (limited to 'sdil')
-rw-r--r--sdil/bci.py38
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.