Archive MaleCNS research sources, provenance, bibliography and immutable data inventory

This commit is contained in:
Digital Drosophila Archive
2026-09-29 03:38:32 +08:00
commit 3981628ff8
682 changed files with 161475 additions and 0 deletions
@@ -0,0 +1,27 @@
{
"file_count": 4,
"files": [
{
"bytes": 268,
"path": "STAGE1_AUDIT_GATE.json",
"sha256": "b029bdfabd7b707f4101103a1e90ec5e95178a27398baf200b6f03d1272646fa"
},
{
"bytes": 380,
"path": "VERSION.json",
"sha256": "d289c642786e251e62de42ae8db096ff2288ab886db5d59b03bfb766985ddd95"
},
{
"bytes": 29381,
"path": "phase4h.py",
"sha256": "4c310b059ec954e7bc21d71482f1cb6a62b0d979ab25ab4a50d6a4689a5b9524"
},
{
"bytes": 4912,
"path": "phase4h.yaml",
"sha256": "5cd3aa12e0aa249004911c7396cab0c80b256b21f8ad21969fea897db7ba2793"
}
],
"root": "/data/digital-drosophila/models/male-cns-lif/dopamine-plasticity-v1",
"total_bytes": 34941
}
@@ -0,0 +1,6 @@
{
"audit_report": "/data/digital-drosophila/experiments/phase4h-dopamine-plasticity/audit/stage1-audit-report.json",
"audit_report_sha256": "96f0fc28d3c9072e2c907f1910dfb22e1ee00a1601ba84c9bc8083ac1adb75c2",
"stage2_authorized": true,
"status": "PASS-AUDIT"
}
@@ -0,0 +1,9 @@
{
"base_model": "MaleCNS-LIF-reference-v1",
"candidate_name": null,
"plasticity_overlay_only": true,
"runtime": "MaleCNS-LIF-runtime-v1",
"stage1_gate_sha256": "b029bdfabd7b707f4101103a1e90ec5e95178a27398baf200b6f03d1272646fa",
"status": "FAIL-NO-DOPAMINE-PLASTICITY-CANDIDATE",
"summary_sha256": "7b01ff77c8918ef0db18ab78e09af8cf364038ca134ef799ce24182d5e03dd47"
}
@@ -0,0 +1,5 @@
677bd6d3656e019da0145f4f3d91e98ceb14234eda70ea56f413a33d68b8a368 MANIFEST.json
b029bdfabd7b707f4101103a1e90ec5e95178a27398baf200b6f03d1272646fa STAGE1_AUDIT_GATE.json
d289c642786e251e62de42ae8db096ff2288ab886db5d59b03bfb766985ddd95 VERSION.json
4c310b059ec954e7bc21d71482f1cb6a62b0d979ab25ab4a50d6a4689a5b9524 phase4h.py
5cd3aa12e0aa249004911c7396cab0c80b256b21f8ad21969fea897db7ba2793 phase4h.yaml
@@ -0,0 +1,468 @@
#!/usr/bin/env python3
"""Phase 4H stage 2: conservative dopamine-gated KC-to-MBON plasticity overlay."""
from __future__ import annotations
import argparse
import gc
import hashlib
import json
import math
import shutil
import sys
import time
from collections import Counter
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
import numpy as np
import pandas as pd
import pyarrow.parquet as pq
import torch
import yaml
from brain_runtime import BrainRuntime, InputBatch, InputEvent
from phase4h_audit import run_frozen_audit, sha256, tree_manifest, write_json
def vector_counts(nodes: np.ndarray, members: np.ndarray, n: int) -> np.ndarray:
lookup = np.full(n, -1, dtype=np.int32)
lookup[members] = np.arange(len(members), dtype=np.int32)
local = lookup[nodes]
local = local[local >= 0]
return np.bincount(local, minlength=len(members)).astype(np.int32)
def cosine_distance(a: np.ndarray, b: np.ndarray) -> float:
af = a.astype(np.float64); bf = b.astype(np.float64)
denom = float(np.linalg.norm(af) * np.linalg.norm(bf))
return 0.0 if denom == 0 else float(1.0 - np.dot(af, bf) / denom)
def separation(a: dict[str, Any], b: dict[str, Any], key: str) -> dict[str, float]:
x = a[key].astype(np.float64); y = b[key].astype(np.float64)
return {
"euclidean": float(np.linalg.norm(x - y)),
"cosine_distance": cosine_distance(x, y),
"l1": float(np.abs(x - y).sum()),
}
def make_batch(targets: np.ndarray, rate_hz: float, duration_ms: float, name: str,
dan_targets: np.ndarray | None = None, dan_start_ms: float = 80.0,
dan_duration_ms: float = 20.0, dan_rate_hz: float = 100.0) -> tuple[InputBatch, float]:
events = [InputEvent(
target_neuron_idx=targets.astype(np.int64), stimulation_mode="spike_injection",
value_kind="poisson_rate_hz", value=float(rate_hz), start_ms=0.0,
duration_ms=float(duration_ms), source_id="phase4h-engineering-olfactory-CS",
channel_id=name, metadata={"interpretation": "engineering olfactory CS interface"},
)]
total_ms = duration_ms
if dan_targets is not None and len(dan_targets):
events.append(InputEvent(
target_neuron_idx=dan_targets.astype(np.int64), stimulation_mode="spike_injection",
value_kind="poisson_rate_hz", value=float(dan_rate_hz), start_ms=float(dan_start_ms),
duration_ms=float(dan_duration_ms), source_id="phase4h-artificial-US-interface",
channel_id="DAN-like-reinforcement",
metadata={
"interpretation": "artificial unconditioned-signal interface",
"not_equal_to": "measured dopamine concentration",
},
))
total_ms = max(total_ms, dan_start_ms + dan_duration_ms)
return InputBatch(events=tuple(events), batch_id=name, metadata={"phase": "4H"}), float(total_ms)
class Experiment:
def __init__(self, cfg: dict[str, Any], exp: Path):
self.cfg = cfg; self.exp = exp
self.rt = BrainRuntime(Path(__file__).with_name("phase4h.yaml"), exp / "compiler-reports/extension-build")
self.base_weights = self.rt.weights.detach().clone()
self.n = self.rt.n
self.metadata = self.rt.table.to_pandas().sort_values("neuron_idx").reset_index(drop=True)
self.classes = self.metadata["annotation_class"].fillna("").astype(str).to_numpy()
self.superclass = self.metadata["annotation_superclass"].fillna("").astype(str).to_numpy()
self.types = self.metadata["annotation_type"].fillna("").astype(str).to_numpy()
self.nt = self.metadata["nt_consensus_nt"].fillna("unclear").astype(str).str.lower().to_numpy()
self.kc = np.flatnonzero(self.classes == "Kenyon_Cell").astype(np.int32)
self.mbon = np.flatnonzero(self.classes == "MBON").astype(np.int32)
self.desc = self.rt.output_populations["descending"]
self.motor = self.rt.output_populations["motor_related"]
self.central = np.flatnonzero(np.char.startswith(self.superclass.astype(str), "cb_")).astype(np.int32)
downstream = pq.read_table(exp / "mappings/mbon-to-downstream-actual-edges.parquet",
columns=["target_neuron_idx"])["target_neuron_idx"].to_numpy()
self.downstream = np.unique(downstream).astype(np.int32)
self.candidates = pq.read_table(exp / "plastic-mask/plastic-edge-candidates.parquet").to_pandas()
self.edge_ids = self.candidates["edge_id"].to_numpy(dtype=np.int64)
self.base_candidate = self.base_weights[torch.as_tensor(self.edge_ids, device=self.rt.device)].cpu().numpy()
self.sign_policy = cfg["sign_policy"]["neurotransmitter_sign"]
olfactory = np.flatnonzero(self.rt.population_masks["olfactory"]).astype(np.int32)
take = int(cfg["conditioning"]["cs_targets_per_identity"])
if len(olfactory) < 2 * take:
take = len(olfactory) // 2
if take == 0:
raise RuntimeError("olfactory population is too small for two disjoint CS identities")
olfactory.sort(); self.cs_a = olfactory[:take]; self.cs_b = olfactory[take:2 * take]
if np.intersect1d(self.cs_a, self.cs_b).size:
raise RuntimeError("CS identities overlap")
self.dan_targets = {}
for label in ("appetitive", "aversive"):
dtype = str(cfg["conditioning"][f"{label}_dan_type_required"])
comp = str(cfg["conditioning"][f"{label}_compartment_verbatim"])
rows = self.candidates[self.candidates["target_compartment_verbatim"] == comp]
confirmed = pq.read_table(exp / "mappings/dan-to-mbon-confirmed.parquet").to_pandas()
sources = confirmed[(confirmed["source_annotation_type"] == dtype)
& (confirmed["source_compartment_verbatim"] == comp)]["source_neuron_idx"].unique()
if rows.empty or len(sources) == 0:
raise RuntimeError(f"required exact DAN/compartment mapping absent: {label} {dtype} {comp}")
self.dan_targets[label] = np.sort(sources.astype(np.int32))
def install_delta(self, delta: np.ndarray) -> None:
if delta.shape != (len(self.edge_ids),):
raise ValueError("plastic delta shape mismatch")
self.rt.weights.copy_(self.base_weights)
ids = torch.as_tensor(self.edge_ids, dtype=torch.int64, device=self.rt.device)
values = torch.as_tensor(self.base_candidate + delta, dtype=torch.float32, device=self.rt.device)
self.rt.weights[ids] = values
def evaluate(self, cs_name: str, seed: int, delta: np.ndarray) -> dict[str, Any]:
self.install_delta(delta); self.rt.reset(seed)
targets = self.cs_a if cs_name == "CS-A" else self.cs_b
batch, duration = make_batch(targets, self.cfg["conditioning"]["cs_rate_hz"],
self.cfg["conditioning"]["cs_duration_ms"], f"eval-{cs_name}")
t0 = time.perf_counter(); self.rt.step(batch, duration); wall = time.perf_counter() - t0
events = self.rt.read_spikes(); nodes = events["neuron_idx"].astype(np.int32)
origins = events["origin"]
vectors = {
"kc": vector_counts(nodes, self.kc, self.n),
"mbon": vector_counts(nodes, self.mbon, self.n),
"downstream": vector_counts(nodes, self.downstream, self.n),
"central": vector_counts(nodes, self.central, self.n),
"descending": vector_counts(nodes, self.desc, self.n),
"motor": vector_counts(nodes, self.motor, self.n),
}
mbon_nt = self.nt[self.mbon]
positive_nt = {key for key, value in self.sign_policy.items() if int(value) > 0}
negative_nt = {key for key, value in self.sign_policy.items() if int(value) < 0}
mvec = vectors["mbon"]
pos = int(mvec[np.isin(mbon_nt, list(positive_nt))].sum())
neg = int(mvec[np.isin(mbon_nt, list(negative_nt))].sum())
finite = bool(torch.isfinite(self.rt.v).all().item() and torch.isfinite(self.rt.g).all().item())
active = int(np.unique(nodes).size)
return {
"seed": int(seed), "CS": cs_name, "wall_seconds": wall,
"total_spikes": int(len(nodes)), "network_spikes": int(np.count_nonzero(origins == "network_spike")),
"active_neurons": active, "active_fraction": float(active / self.n),
"finite": finite, "runaway": bool(active > 0.5 * self.n),
"population_spikes": {key: int(value.sum()) for key, value in vectors.items()},
"mbon_balance": {
"excitatory_transmitter_spikes": pos, "inhibitory_transmitter_spikes": neg,
"signed_transmitter_balance": pos - neg,
"interpretation": "transmitter-stratified MBON activity; not an assigned approach/avoidance valence",
},
**{f"_{key}_vector": value for key, value in vectors.items()},
}
def update(self, cs_name: str, seed: int, delta: np.ndarray, compartment: str | None,
dan_label: str | None, delay_ms: float, enabled: bool, d_value: float) -> tuple[np.ndarray, dict[str, Any]]:
self.install_delta(delta); self.rt.reset(seed)
targets = self.cs_a if cs_name == "CS-A" else self.cs_b
dan = self.dan_targets[dan_label] if dan_label else None
dan_start = self.cfg["conditioning"]["cs_duration_ms"] + delay_ms if delay_ms > 0 else 80.0
batch, duration = make_batch(targets, self.cfg["conditioning"]["cs_rate_hz"],
self.cfg["conditioning"]["cs_duration_ms"], f"train-{cs_name}",
dan_targets=dan, dan_start_ms=dan_start)
self.rt.step(batch, duration)
nodes = self.rt.read_spikes()["neuron_idx"].astype(np.int32)
kc_counts = np.bincount(nodes[np.isin(nodes, self.kc)], minlength=self.n)
pre = self.candidates["source_neuron_idx"].to_numpy(dtype=np.int32)
eligible = np.minimum(kc_counts[pre].astype(np.float32), 1.0)
if delay_ms > 0:
eligible *= math.exp(-delay_ms / float(self.cfg["plasticity"]["eligibility_tau_ms"]))
comp_mask = (self.candidates["target_compartment_verbatim"].astype(str).to_numpy() == str(compartment)) if compartment else np.zeros(len(delta), bool)
update = np.zeros_like(delta)
if enabled and d_value != 0.0 and compartment is not None:
polarity = float(self.cfg["plasticity"]["default_confirmed_compartment_polarity"])
update[comp_mask] = polarity * float(self.cfg["plasticity"]["eta_effective_mv"]) * float(d_value) * eligible[comp_mask]
out = delta + update
lower = -float(self.cfg["plasticity"]["max_fractional_depression_per_edge"]) * self.base_candidate
out = np.maximum(out, lower).astype(np.float32)
if np.any(self.base_candidate + out < -1e-7):
raise RuntimeError("plastic overlay made an excitatory KC edge negative")
record = {
"CS": cs_name, "seed": int(seed), "compartment_verbatim": compartment,
"DAN_label": dan_label, "DAN_neuron_count": 0 if dan is None else int(len(dan)),
"D_value": float(d_value), "D_interpretation": "artificial US interface scalar, not dopamine concentration",
"delay_ms": float(delay_ms), "plasticity_enabled": bool(enabled),
"eligible_candidate_edges": int(np.count_nonzero(eligible * comp_mask)),
"updated_edges_this_trial": int(np.count_nonzero(update)),
"delta_l1_effective_mv": float(np.abs(out).sum()),
"delta_min_effective_mv": float(out.min(initial=0.0)), "delta_max_effective_mv": float(out.max(initial=0.0)),
}
return out, record
def public_eval(value: dict[str, Any]) -> dict[str, Any]:
return {key: item for key, item in value.items() if not key.startswith("_")}
def eval_pair(experiment: Experiment, seed: int, delta: np.ndarray) -> dict[str, Any]:
a = experiment.evaluate("CS-A", seed, delta)
b = experiment.evaluate("CS-B", seed, delta)
return {
"CS-A": public_eval(a), "CS-B": public_eval(b),
"mbon_separation": separation(a, b, "_mbon_vector"),
"downstream_separation": separation(a, b, "_downstream_vector"),
"descending_separation": separation(a, b, "_descending_vector"),
"motor_separation": separation(a, b, "_motor_vector"),
"balance_difference_A_minus_B": int(a["mbon_balance"]["signed_transmitter_balance"] - b["mbon_balance"]["signed_transmitter_balance"]),
}
def protocol_plan(name: str, cfg: dict[str, Any], reliable: list[str], rng: np.random.Generator) -> list[dict[str, Any]]:
app = str(cfg["conditioning"]["appetitive_compartment_verbatim"])
av = str(cfg["conditioning"]["aversive_compartment_verbatim"])
trials = int(cfg["plasticity"]["training_trials"])
base = {"delay_ms": 0.0, "enabled": True, "d_value": float(cfg["plasticity"]["d_magnitude"])}
if name == "appetitive-conditioning":
return [base | {"CS": "CS-A", "compartment": app, "dan_label": "appetitive"} for _ in range(trials)]
if name == "aversive-conditioning":
return [base | {"CS": "CS-A", "compartment": av, "dan_label": "aversive"} for _ in range(trials)]
if name == "delayed-reward":
return [base | {"CS": "CS-A", "compartment": app, "dan_label": "appetitive",
"delay_ms": float(cfg["conditioning"]["delayed_reward_ms"])} for _ in range(trials)]
if name == "retention":
return [base | {"CS": "CS-A", "compartment": app, "dan_label": "appetitive"} for _ in range(trials)]
if name == "d-zero-control":
return [base | {"CS": "CS-A", "compartment": app, "dan_label": None, "d_value": 0.0} for _ in range(trials)]
if name == "frozen-brain-control":
return [base | {"CS": "CS-A", "compartment": app, "dan_label": "appetitive", "enabled": False} for _ in range(trials)]
if name == "random-reward-control":
plan = []
for _ in range(trials):
comp = str(rng.choice(reliable))
label = "appetitive" if comp == app else ("aversive" if comp == av else None)
plan.append(base | {"CS": str(rng.choice(["CS-A", "CS-B"])), "compartment": comp, "dan_label": label})
return plan
if name == "reversal-learning":
first = [base | {"CS": "CS-A", "compartment": app, "dan_label": "appetitive", "phase": "acquisition"} for _ in range(trials)]
second = [base | {"CS": "CS-B", "compartment": app, "dan_label": "appetitive", "phase": "reversal"} for _ in range(trials)]
return first + second
raise KeyError(name)
def phase4c_regression(cfg: dict[str, Any], exp: Path) -> dict[str, Any]:
rt = BrainRuntime(Path(__file__).with_name("phase4h.yaml"), exp / "compiler-reports/extension-build")
specs = [
("auditory-10hz", "auditory", 10.0, 42001),
("auditory-100hz", "auditory", 100.0, 42001),
("olfactory-50hz", "olfactory", 50.0, 42003),
("mechanosensory-50hz", "mechanosensory", 50.0, 42003),
("gustatory-50hz", "gustatory", 50.0, 42003),
]
results = {}
root = Path(cfg["paths"]["phase4c_root"]) / "regression/reference"
for name, population, rate, seed in specs:
targets = np.flatnonzero(rt.population_masks[population]).astype(np.int64)
batch, duration = make_batch(targets, rate, 1000.0, f"phase4c-regression-{name}")
rt.reset(seed); rt.step(batch, duration)
events = rt.read_spikes(); steps = events["step"].astype(np.int32); nodes = events["neuron_idx"].astype(np.int32)
ref = pq.read_table(root / name / "spike-events.parquet")
ref_steps = ref["step"].to_numpy().astype(np.int32); ref_nodes = ref["neuron_idx"].to_numpy().astype(np.int32)
counts = rt.spike_counts.cpu().numpy().astype(np.int32)
ref_counts = np.load(root / name / "per-neuron-spike-counts.int32.npy")
v = rt.v.cpu().numpy(); g = rt.g.cpu().numpy()
ref_v = np.load(root / name / "final-voltage.float32.npy"); ref_g = np.load(root / name / "final-g.float32.npy")
results[name] = {
"spike_events_exact": bool(np.array_equal(steps, ref_steps) and np.array_equal(nodes, ref_nodes)),
"per_neuron_counts_exact": bool(np.array_equal(counts, ref_counts)),
"max_abs_voltage_error": float(np.max(np.abs(v - ref_v))),
"max_abs_g_error": float(np.max(np.abs(g - ref_g))),
"event_count": int(len(nodes)),
}
results["all_passed"] = bool(all(v["spike_events_exact"] and v["per_neuron_counts_exact"]
and v["max_abs_voltage_error"] <= 0.00390625
and v["max_abs_g_error"] <= 0.00390625
for k, v in results.items() if k != "all_passed"))
write_json(exp / "regression/phase4c-reference-replay.json", results)
del rt; gc.collect(); torch.cuda.empty_cache()
return results
def seal(root: Path, manifest_path: Path, checksum_path: Path) -> None:
manifest = tree_manifest(root, {manifest_path, checksum_path})
write_json(manifest_path, manifest)
targets = sorted(path for path in root.rglob("*") if path.is_file() and path != checksum_path)
checksum_path.write_text("".join(f"{sha256(path)} {path.relative_to(root)}\n" for path in targets), encoding="utf-8")
def main() -> None:
ap = argparse.ArgumentParser(); ap.add_argument("--config", type=Path, required=True); args = ap.parse_args()
cfg = yaml.safe_load(args.config.read_text(encoding="utf-8"))
exp = Path(cfg["paths"]["experiment_root"]); learned = Path(cfg["paths"]["learned_root"]); model = Path(cfg["paths"]["model_root"])
for sub in ("conditioning", "controls", "plasticity", "metrics", "regression", "audit", "manifests", "compiler-reports/extension-build"):
(exp / sub).mkdir(parents=True, exist_ok=True)
for sub in ("plastic-delta", "training-metadata"):
(learned / sub).mkdir(parents=True, exist_ok=True)
model.mkdir(parents=True, exist_ok=True)
gate_path = model / "STAGE1_AUDIT_GATE.json"
if not gate_path.is_file():
raise RuntimeError("stage-1 audit gate missing")
gate = json.loads(gate_path.read_text(encoding="utf-8"))
if gate.get("status") != "PASS-AUDIT" or not gate.get("stage2_authorized"):
raise RuntimeError(f"stage-1 audit did not authorize conditioning: {gate}")
report_path = Path(gate["audit_report"])
if sha256(report_path) != gate["audit_report_sha256"]:
raise RuntimeError("stage-1 audit report changed after gate creation")
stage1 = json.loads(report_path.read_text(encoding="utf-8"))
reliable = stage1["reliable_mb_compartments"]["verbatim_values"]
frozen_before = run_frozen_audit(cfg)
write_json(exp / "audit/frozen-checksums-before-stage2.json", frozen_before)
shutil.copy2(args.config, exp / "audit/phase4h-stage2-config.yaml")
shutil.copy2(Path(__file__).resolve(), exp / "audit/phase4h.py")
experiment = Experiment(cfg, exp)
write_json(exp / "audit/conditioning-interface.json", {
"CS_A_neuron_idx": experiment.cs_a.tolist(), "CS_B_neuron_idx": experiment.cs_b.tolist(),
"CS_populations_are_disjoint": True, "CS_identity": "engineering split of sorted olfactory annotation_class population",
"DAN_targets": {key: value.tolist() for key, value in experiment.dan_targets.items()},
"reward_interface": cfg["conditioning"]["reward_interface_label"],
"reward_value_is_dopamine_concentration": False,
})
all_runs = []
finite_all = True; no_runaway = True
for protocol in cfg["conditioning"]["protocols"]:
protocol_dir = exp / ("controls" if "control" in protocol else "conditioning") / protocol
protocol_dir.mkdir(parents=True, exist_ok=True)
for train_seed, eval_seed in zip(cfg["conditioning"]["train_seeds"], cfg["conditioning"]["evaluation_seeds"]):
rng = np.random.default_rng(int(train_seed))
delta = np.zeros(len(experiment.edge_ids), dtype=np.float32)
pre = eval_pair(experiment, int(eval_seed), delta)
plan = protocol_plan(protocol, cfg, reliable, rng)
updates = []; trajectory = []
acquisition_snapshot = None
for trial_idx, item in enumerate(plan):
delta, update = experiment.update(item["CS"], int(train_seed) + trial_idx, delta,
item["compartment"], item["dan_label"], item["delay_ms"],
item["enabled"], item["d_value"])
update["trial"] = trial_idx; update["phase"] = item.get("phase", "acquisition")
updates.append(update)
pair = eval_pair(experiment, int(eval_seed), delta)
trajectory.append({"trial": trial_idx, "phase": update["phase"],
"mbon_separation": pair["mbon_separation"],
"downstream_separation": pair["downstream_separation"],
"balance_difference_A_minus_B": pair["balance_difference_A_minus_B"]})
if protocol == "reversal-learning" and update["phase"] == "acquisition":
acquisition_snapshot = pair
post = eval_pair(experiment, int(eval_seed), delta)
retention = []
if protocol == "retention":
for nominal_ms, extra_seed in zip((0, 1000, 10000), cfg["conditioning"]["evaluation_seeds"][:3]):
pair = eval_pair(experiment, int(extra_seed), delta)
retention.append({"nominal_retention_interval_ms": nominal_ms,
"state_policy": "fresh neural state; persistent plastic_delta; no idle dynamics approximation",
"mbon_separation": pair["mbon_separation"], "downstream_separation": pair["downstream_separation"]})
run = {
"protocol": protocol, "train_seed": int(train_seed), "evaluation_seed": int(eval_seed),
"pre": pre, "post": post, "updates": updates, "trajectory": trajectory,
"retention": retention,
"mbon_euclidean_gain": post["mbon_separation"]["euclidean"] - pre["mbon_separation"]["euclidean"],
"downstream_euclidean_gain": post["downstream_separation"]["euclidean"] - pre["downstream_separation"]["euclidean"],
"reversal_balance_sign_changed": (
False if acquisition_snapshot is None else
bool(np.sign(acquisition_snapshot["balance_difference_A_minus_B"]) != np.sign(post["balance_difference_A_minus_B"]))
),
"changed_edges": int(np.count_nonzero(delta)), "delta_l1_effective_mv": float(np.abs(delta).sum()),
"base_CSR_modified": False,
}
run_path = protocol_dir / f"train-{train_seed}-eval-{eval_seed}.json"
write_json(run_path, run)
np.savez_compressed(learned / "plastic-delta" / f"{protocol}-seed-{train_seed}.npz",
edge_id=experiment.edge_ids, plastic_delta_effective_mv=delta)
finite_all = finite_all and all(x["finite"] for x in (post["CS-A"], post["CS-B"]))
no_runaway = no_runaway and not any(x["runaway"] for x in (post["CS-A"], post["CS-B"]))
all_runs.append(run)
del experiment; gc.collect(); torch.cuda.empty_cache()
regression = phase4c_regression(cfg, exp)
frozen_after = run_frozen_audit(cfg)
write_json(exp / "audit/frozen-checksums-after-stage2.json", frozen_after)
by_protocol = {}
for protocol in cfg["conditioning"]["protocols"]:
rows = [row for row in all_runs if row["protocol"] == protocol]
by_protocol[protocol] = {
"seeds": len(rows),
"mbon_gain_mean": float(np.mean([row["mbon_euclidean_gain"] for row in rows])),
"mbon_gain_positive_seeds": int(sum(row["mbon_euclidean_gain"] > 0 for row in rows)),
"downstream_gain_mean": float(np.mean([row["downstream_euclidean_gain"] for row in rows])),
"downstream_gain_positive_seeds": int(sum(row["downstream_euclidean_gain"] > 0 for row in rows)),
"changed_edges_mean": float(np.mean([row["changed_edges"] for row in rows])),
"delta_l1_effective_mv_mean": float(np.mean([row["delta_l1_effective_mv"] for row in rows])),
"reversal_signature_seeds": int(sum(row["reversal_balance_sign_changed"] for row in rows)),
}
majority = int(cfg["success_gate"]["require_minimum_seed_majority"])
app = by_protocol["appetitive-conditioning"]; av = by_protocol["aversive-conditioning"]
real_mbon = app["mbon_gain_positive_seeds"] >= majority and av["mbon_gain_positive_seeds"] >= majority
real_downstream = app["downstream_gain_positive_seeds"] >= majority and av["downstream_gain_positive_seeds"] >= majority
reversal = by_protocol["reversal-learning"]["reversal_signature_seeds"] >= majority
retention_rows = [row for row in all_runs if row["protocol"] == "retention"]
retention = sum(bool(row["retention"]) and row["retention"][-1]["mbon_separation"]["euclidean"] > row["pre"]["mbon_separation"]["euclidean"] for row in retention_rows) >= majority
control_ceiling = max(by_protocol[name]["mbon_gain_mean"] for name in ("d-zero-control", "random-reward-control", "frozen-brain-control"))
condition_floor = min(app["mbon_gain_mean"], av["mbon_gain_mean"])
controls_distinct = condition_floor > control_ceiling
passed = all([frozen_before["all_passed"], frozen_after["all_passed"], regression["all_passed"],
finite_all, no_runaway, real_mbon, real_downstream, reversal, retention, controls_distinct])
status = "MaleCNS-DopaminePlasticity-v1-candidate" if passed else "FAIL-NO-DOPAMINE-PLASTICITY-CANDIDATE"
summary = {
"status": status, "candidate_generated": passed, "stage1_status": stage1["status"],
"mapping": {
"KC": stage1["population_counts"]["KC"], "MBON": stage1["population_counts"]["MBON"],
"PAM": stage1["population_counts"]["PAM"], "PPL1": stage1["population_counts"]["PPL1"],
"other_DAN": stage1["population_counts"]["other_DAN"],
"reliable_compartments": reliable, "plastic_candidate_edges": stage1["plastic_edge_candidate_mask"]["edges"],
},
"protocols": by_protocol,
"gate": {
"all_frozen_checksums_pass": frozen_before["all_passed"] and frozen_after["all_passed"],
"phase4c_reference_regression_pass": regression["all_passed"],
"finite": finite_all, "no_runaway": no_runaway,
"appetitive_and_aversive_MBON_separation_majority": real_mbon,
"appetitive_and_aversive_downstream_separation_majority": real_downstream,
"reversal_signature_majority": reversal, "retention_signature_majority": retention,
"controls_distinct": controls_distinct,
"weight_change_alone_used_as_success": False,
},
"base_brain": {"modified_on_disk": False, "runtime_base_restored_between_runs": True,
"equation": cfg["plasticity"]["overlay_equation"]},
"reward_interface": {"artificial_unconditioned_signal": True, "dopamine_concentration_claimed": False},
"limitations": [
"CS-A and CS-B are reproducible engineering subsets of the annotated olfactory population, not identified odor receptor channels.",
"Compartment membership is based on exact verbatim annotation_instance tokens and direct neuron-level CSR edges; synapse-level ROI is unavailable.",
"MBON balance is transmitter-stratified and is not relabeled as approach/avoidance without a reliable functional annotation.",
"Retention uses a fresh neural state with persistent plastic_delta and does not simulate a long idle biological interval.",
],
}
write_json(exp / "audit/stage2-summary.json", summary)
write_json(model / "VERSION.json", {
"status": status, "candidate_name": status if passed else None,
"base_model": cfg["names"]["reference"], "runtime": cfg["names"]["runtime"],
"plasticity_overlay_only": True, "stage1_gate_sha256": sha256(gate_path),
"summary_sha256": sha256(exp / "audit/stage2-summary.json"),
})
shutil.copy2(args.config, model / "phase4h.yaml")
shutil.copy2(Path(__file__).resolve(), model / "phase4h.py")
write_json(learned / "training-metadata/summary.json", {"status": status, "protocols": by_protocol, "candidate": passed})
print(json.dumps(summary, indent=2, sort_keys=True))
sys.stdout.flush()
# Flush redirected stdout before hashing so a log inside the experiment tree
# cannot change after its checksum has been recorded.
seal(model, model / "MANIFEST.json", model / "checksums.sha256")
seal(learned, learned / "MANIFEST.json", learned / "checksums.sha256")
seal(exp, exp / "manifests/manifest.json", exp / "manifests/checksums.sha256")
if __name__ == "__main__":
main()
@@ -0,0 +1,134 @@
phase: 4H
names:
reference: MaleCNS-LIF-reference-v1
runtime: MaleCNS-LIF-runtime-v1
io_api: MaleCNS-BrainIO-v1
activity_stream: MaleCNS-ActivityStream-v1
plasticity_candidate: MaleCNS-DopaminePlasticity-v1-candidate
execution:
mode: two-stage
stage1_only_entrypoint: phase4h_audit.py
require_stage1_pass_before_conditioning: true
paths:
connectome_root: /data/digital-drosophila/datasets/male-cns-v1.0/processed/neuron-only
model_root: /data/digital-drosophila/models/male-cns-lif/dopamine-plasticity-v1
learned_root: /data/digital-drosophila/models/learned/phase4h
experiment_root: /data/digital-drosophila/experiments/phase4h-dopamine-plasticity
reference_root: /data/digital-drosophila/models/male-cns-lif/reference-v1
runtime_root: /data/digital-drosophila/models/male-cns-lif/runtime-v1
io_root: /data/digital-drosophila/models/male-cns-lif/io-v1
phase4c_root: /data/digital-drosophila/experiments/phase4c-runtime
phase4d_root: /data/digital-drosophila/experiments/phase4d-io
phase4e_root: /data/digital-drosophila/experiments/phase4e-visual-calibration
phase4f_root: /data/digital-drosophila/experiments/phase4f-retinotopic-vision
phase4f_model_root: /data/digital-drosophila/models/male-cns-lif/visual-encoder-v1
phase4g_root: /data/digital-drosophila/experiments/phase4g-frozen-interface
phase4g_model_root: /data/digital-drosophila/models/male-cns-lif/frozen-interface-v1
phase4g_learned_root: /data/digital-drosophila/models/learned/phase4g
expected_graph:
neurons: 166700
directed_edges: 25582938
synaptic_contact_weight_sum: 124177617
model:
dt_ms: 0.1
membrane_tau_ms: 20.0
synaptic_tau_ms: 5.0
refractory_ms: 2.2
resting_potential_mv: -52.0
reset_potential_mv: -52.0
threshold_mv: -45.0
synaptic_delay_ms: 1.8
w_syn_mv_per_anatomical_synapse: 0.275
sign_policy:
neurotransmitter_sign: {acetylcholine: 1, gaba: -1, glutamate: -1, dopamine: 1, octopamine: 1, serotonin: 1, histamine: -1}
unknown_sign: 0
unclear_sign: 0
readout:
bins_ms: [1, 5, 10, 20, 50, 100]
audit:
exact_class_values:
KC: Kenyon_Cell
MBON: MBON
DAN: DAN
pam_type_prefix: PAM
ppl1_type_prefix: PPL1
compartment_source_field: annotation_instance
compartment_parser: single_parenthesized_substring_verbatim
require_exact_compartment_string_match: true
candidate_requires_actual_kc_mbon_edge: true
candidate_requires_actual_dan_mbon_edge: true
candidate_requires_exact_dan_mbon_compartment_match: true
do_not_infer_missing_compartment_from_type: true
plasticity:
base_connectome_mode: read_only
overlay_equation: effective_weight = base_weight + plastic_delta
rule: delta_w = polarity_compartment * eta * D_compartment(t) * eligibility_ij(t)
first_version_rule_family: compartment_specific_dopamine_gated_anti_hebbian_depression
default_confirmed_compartment_polarity: -1
unknown_compartment_policy: exclude_from_plastic_mask
pam_ppl1_cluster_valence_policy: prohibited
reward_interface_interpretation: artificial_unconditioned_signal_not_dopamine_concentration
polarity_provenance:
- https://pubmed.ncbi.nlm.nih.gov/26637800/
- https://pubmed.ncbi.nlm.nih.gov/25535793/
- https://www.nature.com/articles/s41586-024-07819-w
eligibility_tau_ms: 1000.0
eta_effective_mv: 0.00275
min_effective_weight_mv: 0.0
max_fractional_depression_per_edge: 0.25
training_trials: 3
d_magnitude: 1.0
conditioning:
cs_population: olfactory
cs_rate_hz: 50.0
cs_duration_ms: 100.0
cs_targets_per_identity: 64
cs_identity_rule: sorted_neuron_idx_disjoint_halves
appetitive_compartment_verbatim: "y5B'2a"
appetitive_dan_type_required: PAM15
aversive_compartment_verbatim: "y2a'1"
aversive_dan_type_required: PPL103
delayed_reward_ms: 50.0
train_seeds: [48001, 48002, 48003, 48004, 48005]
evaluation_seeds: [48101, 48102, 48103, 48104, 48105]
protocols:
- appetitive-conditioning
- aversive-conditioning
- reversal-learning
- delayed-reward
- retention
- d-zero-control
- random-reward-control
- frozen-brain-control
reward_interface_label: artificial_unconditioned_signal_interface
success_gate:
require_all_frozen_checksums_pass: true
require_phase4c_reference_spike_events_exact: true
require_no_nan_inf_or_runaway: true
require_minimum_seed_majority: 3
require_appetitive_and_aversive_mbon_separation_gain: true
require_downstream_separation_gain: true
require_reversal_signature: true
require_retention_signature: true
controls_must_not_equal_conditioning_effect: true
weight_change_alone_is_never_sufficient: true
device:
required_name_substring: RTX 4090
launcher_visible_gpu_uuid: GPU-79f3a85d-adce-7833-7003-6488140ca596
prohibitions:
modify_phase1_to_phase4g: false
conditioning_during_stage1: false
weight_update_during_stage1: false
infer_unknown_mapping: false
train_base_brain: false