Files
digital-drosophila/models/male-cns-lif/credit-assignment-v1/phase4n_finish.py
T

255 lines
17 KiB
Python

#!/usr/bin/env python3
"""Phase 4N checkpoint-specific whole-brain/credit audit and conservative sealing."""
from __future__ import annotations
import argparse
import gc
import json
import resource
import shutil
import subprocess
import time
from pathlib import Path
import numpy as np
import pandas as pd
import torch
from phase4h_audit import sha256, verify_checksum, write_json
from phase4i import Validator, phase4c_regression
from phase4j import effect
from phase4k import dataset_checks, seal
from phase4l import OdorSimulator, pn_drive
from phase4m import frozen_checks, frozen_hashes, load_odor_inputs
from phase4n import GROUPS, load_config
def state(exp, seed, pair):
if pair == 0:
return np.ones(14232, np.float32)
with np.load(exp / "acquisition-trajectory" / f"E-seed-{seed}-pairings-{pair}.npz") as f:
return f["E_bounded"].copy()
def curves(ncfg, exp, csi, replay, identity):
csi = csi[csi.mechanism == "bounded"]
peak = json.loads((exp / "specificity-curve/peak.json").read_text())["pairings"]
onset = None
if peak is not None:
p = csi[csi.pairings == peak]
baseline_csi = p.CSI.mean()
baseline_other = max(p.shared.mean(), p["B-specific"].mean())
for pair in ncfg["checkpoints"]:
if pair <= peak:
continue
q = csi[csi.pairings == pair]
other = max(q.shared.mean(), q["B-specific"].mean())
if q.CSI.mean() <= .8 * baseline_csi and other >= 1.1 * baseline_other and baseline_csi > 0 and baseline_other > 0:
onset = pair
break
write_json(exp / "specificity-curve/generalization-onset.json", {"status": "GENERALIZATION-ONSET-IDENTIFIED" if onset is not None else "UNIDENTIFIED",
"pairings": onset, "rule": ncfg["generalization_onset_rule"]})
groups = pd.read_parquet(exp / "acquisition-trajectory/group-plasticity.parquet")
groups = groups[groups.mechanism == "bounded"]
# Dependency-free vector plot; values remain in the accompanying Parquet files.
parts = ['<svg xmlns="http://www.w3.org/2000/svg" width="900" height="640" viewBox="0 0 900 640">',
'<rect width="900" height="640" fill="white"/>', '<text x="55" y="30" font-size="17">Phase 4N acquisition and specificity</text>']
def panel(series, y0, height, title):
allv = np.concatenate([np.asarray(v, np.float64) for _, v, _ in series]); lo, hi = float(np.min(allv)), float(np.max(allv))
if hi <= lo: hi = lo + 1.0
parts.append(f'<text x="55" y="{y0-10}" font-size="13">{title}</text>')
parts.append(f'<path d="M60 {y0} V{y0+height} H850" fill="none" stroke="black"/>')
for label, vals, color in series:
xs = np.asarray(ncfg["checkpoints"], np.float64)
ys = np.asarray(vals, np.float64)
coords = ' '.join(f'{60+790*x/100:.1f},{y0+height-20-(height-35)*(y-lo)/(hi-lo):.1f}' for x,y in zip(xs,ys))
parts.append(f'<polyline points="{coords}" fill="none" stroke="{color}" stroke-width="2"/>')
parts.append(f'<text x="{625}" y="{y0+18+17*len([s for s in series if s[0] < label])}" font-size="11" fill="{color}">{label}</text>')
for p,color in ((peak,"green"),(onset,"red")):
if p is not None:
x = 60 + 790*p/100
parts.append(f'<path d="M{x:.1f} {y0} V{y0+height}" stroke="{color}" stroke-dasharray="4,4"/>')
first = []
for name,color in zip(GROUPS,("#1261a0","#d4871e","#b72230","#777777")):
g = groups[groups.group == name].groupby("pairings").weighted_transmission_reduction.mean().reindex(ncfg["checkpoints"]).fillna(0.0)
first.append((name,g.to_numpy(),color))
panel(first, 80, 210, "Mean anatomical-weighted E reduction")
panel([("A-B spike L1",replay.mean_spike_L1_separation.to_numpy(),"#2059a6"),
("A-B latency ms",replay.mean_latency_separation_ms.to_numpy(),"#b72230")], 370, 210, "Functional MBON effect difference")
parts.append('</svg>')
(exp / "specificity-curve/trajectory.svg").write_text('\n'.join(parts), encoding="utf-8")
return peak, onset
def shared_decomposition(ncfg, exp, identity, sim, v, peak):
if peak is None:
write_json(exp / "shared-edge/summary.json", {"status": "NOT-RUN-NO-SPECIFICITY-PEAK"})
return None
from phase4m import ReplayEngine, replay_effect
comp = identity.compartment.astype(str).to_numpy() == str(ncfg["matched_compartment_verbatim"])
targets = np.unique(identity.loc[comp, "target_neuron_idx"].to_numpy(np.int32))
engine = ReplayEngine(sim, targets)
w = identity.anatomical_weight_mv.to_numpy(np.float32)
groups = identity.identity_group.astype(str).to_numpy()
rows = []
from phase4n import load_config
_, mcfg, _, _ = load_config(Path(__file__).with_name("phase4n.yaml"))
for seed, es in zip(ncfg["train_seeds"], ncfg["evaluation_seeds"]):
E = state(exp, seed, peak)
for odor in ("A", "B"):
spikes = pd.read_parquet(Path(mcfg["experiment_root"]) / "isolated-replay" / f"KC-spikes-ODOR_{odor}-seed-{es}.parquet")
baseline = engine.run(spikes, np.zeros(len(E), np.float32), float(mcfg["protocol"]["duration_ms"]))
for label, included in (("A-only", ("A-specific",)), ("A+shared", ("A-specific", "shared")),
("A+shared+B", ("A-specific", "shared", "B-specific")), ("all-updated", GROUPS)):
delta = np.where(np.isin(groups, included), w * (E - 1), 0).astype(np.float32)
post = engine.run(spikes, delta, float(mcfg["protocol"]["duration_ms"]))
metrics = replay_effect(baseline, post, sim.dt)
rows.append({"train_seed": seed, "eval_seed": es, "pairings": peak, "odor": odor, "decomposition": label,
**{k: val for k, val in metrics.items() if k != "isolated_input_scope"}})
pd.DataFrame(rows).to_parquet(exp / "shared-edge/isolated-decomposition.parquet", index=False)
return pd.DataFrame(rows)
def wholebrain(ncfg, mcfg, cfg, exp, identity, sim, v, peak):
wanted = sorted(set([10, 20, 50, 75, 100] + ([peak] if peak is not None else [])))
panel, vectors, selection, mapping = load_odor_inputs(cfg)
gloms = selection["glomeruli_order"]
matched = float(np.median(panel.mapped_response_sum))
pn = {}
for label in ("A", "B"):
oid = selection[f"ODOR_{label}"]["odor_id"]
pn[label] = pn_drive(oid, vectors[oid], mapping, gloms, str(mcfg["protocol"]["input_mode"]), matched, cfg)[:2]
dan = np.unique(v.confirmed.source_neuron_idx.to_numpy(np.int32))
group_sources = {name: np.unique(identity.loc[identity.identity_group == name, "source_neuron_idx"].to_numpy(np.int32)) for name in GROUPS}
w = identity.anatomical_weight_mv.to_numpy(np.float32)
rows, calls = [], []
for seed, es in zip(ncfg["train_seeds"], ncfg["evaluation_seeds"]):
baseline = {label: sim.run_rates(*pn[label], es, float(mcfg["protocol"]["duration_ms"]), collect_kc_spikes=True) for label in pn}
for pair in wanted:
delta = w * (state(exp, seed, pair) - 1)
for label in ("A", "B"):
post = sim.run_rates(*pn[label], es, float(mcfg["protocol"]["duration_ms"]), delta=delta, collect_kc_spikes=True)
calls.append(post["wall_seconds"])
eff = effect(baseline[label], post)
row = {"train_seed": seed, "eval_seed": es, "pairings": pair, "odor": label,
"MBON_spike_L1": eff["MBON_spike_l1"], "MBON_g_max_abs_mv": eff["MBON_g_max_abs"],
"MBON_v_max_abs_mv": eff["MBON_v_max_abs"], "MBON_latency_delta_ms": eff["MBON_first_latency_delta_ms"],
"downstream_spike_L1": eff["downstream_spike_l1"], "KC_spike_L1": int(np.abs(post["kc_counts"]-baseline[label]["kc_counts"]).sum()),
"endogenous_DAN_spikes": int(post["counts"][dan].sum()), "baseline_endogenous_DAN_spikes": int(baseline[label]["counts"][dan].sum()),
"finite": bool(post["finite"]), "peak_vram_bytes": int(post["peak_vram_bytes"]), "wall_seconds": post["wall_seconds"]}
for group, nodes in group_sources.items():
row[f"{group}_KC_spikes"] = int(post["counts"][nodes].sum())
row[f"{group}_KC_newly_active_vs_E1"] = int(np.count_nonzero((post["counts"][nodes] > 0) & (baseline[label]["counts"][nodes] == 0)))
row["MBON_spikes"] = int(post["mbon_counts"].sum())
row["other_feedback_spikes"] = int(post["downstream_counts"].sum())
rows.append(row)
frame = pd.DataFrame(rows)
frame.to_parquet(exp / "whole-brain/checkpoint-responses.parquet", index=False)
write_json(exp / "performance/whole-brain.json", {"calls": len(calls), "mean_wall_seconds": float(np.mean(calls)),
"sum_wall_seconds": float(np.sum(calls)), "max_peak_vram_bytes": int(frame.peak_vram_bytes.max())})
return frame
def audit_updates(ncfg, mcfg, exp, identity, history, csi, peak):
group = identity.set_index("edge_id").identity_group
hist = history.copy()
hist["identity_group"] = group.loc[hist.edge_id.to_numpy(np.int64)].to_numpy()
hist["delta_E_bounded"] = hist.E_bounded_after - hist.E_bounded_before
hist["DAN_source"] = "external-US-associated"
hist["matched_compartment"] = True
hist["endogenous_DAN_used_as_gate"] = False
hist["local_KC_trace_nonzero"] = hist.eligibility > 0
hist.to_parquet(exp / "local-eligibility/phase4m-updates-classified.parquet", index=False)
first = hist[(hist.identity_group == "B-specific") & (hist.delta_E_bounded < -1e-7)].groupby("train_seed").pairing.min().reset_index(name="first_B_specific_update_pairing")
first.to_parquet(exp / "b-contamination/first-B-updates.parquet", index=False)
causes = {"direct_A_driven_KC_activity": None, "recurrent_KC_reactivation": None, "MBON_DAN_feedback": None,
"olfactory_feedback": None, "long_eligibility_trace": None, "endogenous_DAN_teaching_interpretation": 0.0,
"unresolved_runtime_KC_recruitment_or_trace": 1.0}
write_json(exp / "b-contamination/provenance.json", {"classification": causes,
"reason": "Frozen Phase 4M update history records trace at forced matched-US but no event-level causal predecessor or KC spike times. Assigning a recurrent/feedback percentage would be invented.",
"earliest_B_update_by_seed": first.to_dict("records"), "all_updates_gated_by_external_matched_US": True})
write_json(exp / "dan-source/phase4m-gate-audit.json", {"external_US_gates": int(history[["train_seed", "pairing"]].drop_duplicates().shape[0]),
"endogenous_DAN_as_teaching_gate": 0, "policy": "Phase 4M implementation opens plasticity once at imposed 90 ms US, not at endogenous DAN spike times",
"observed_endogenous_DAN_activity_is_not_equivalent_to_dopamine_concentration": True})
score = csi[csi.mechanism == "bounded"].groupby("pairings").CSI.mean()
write_json(exp / "stopping-analysis/summary.json", {"status": "POST-HOC-ONLY-NO-ADAPTIVE-POLICY",
"peak_CSI_checkpoint": int(score.idxmax()), "peak_CSI": float(score.max()), "functional_peak_checkpoint": peak,
"online_validated_criterion": False, "heldout_training_validation_required": True})
def controls(exp, identity, ncfg, mcfg):
rows = []
for seed in ncfg["train_seeds"]:
for pair in ncfg["checkpoints"]:
E = state(exp, seed, pair)
rows += [{"seed": seed, "pairings": pair, "condition": "A+matched-US", "E_changed": int(np.count_nonzero(E < 1 - 1e-7)), "status": "RUN-RECONSTRUCTED-FROZEN-PHASE4M"},
{"seed": seed, "pairings": pair, "condition": "A+D0", "E_changed": 0, "status": "EXACT-ZERO-GATE-IDENTITY"},
{"seed": seed, "pairings": pair, "condition": "frozen-E1", "E_changed": 0, "status": "EXACT-FROZEN-IDENTITY"},
{"seed": seed, "pairings": pair, "condition": "external-US-only-gate", "E_changed": int(np.count_nonzero(E < 1 - 1e-7)), "status": "IDENTICAL-TO-FROZEN-PHASE4M-RULE"}]
pd.DataFrame(rows).to_parquet(exp / "controls/trajectory-status.parquet", index=False)
report = {"A_matched_US": "RUN-PHASE4M-TRAJECTORY-RECONSTRUCTED", "A_D0": "ANALYTIC-EXACT-IDENTITY", "frozen_E1": "ANALYTIC-EXACT-IDENTITY",
"external_US_only": "RUN-IDENTICAL-TO-PHASE4M", "B_matched_US": "NOT-RUN", "random_US_timing": "NOT-RUN",
"unrelated_compartment_US": "NOT-RUN", "all_DAN_gate": "NOT-RUN", "full_control_matrix_pass": False}
write_json(exp / "controls/summary.json", report)
return report
def integrity(ncfg, mcfg, cfg, base_path, exp, frozen_before):
gc.collect(); torch.cuda.empty_cache()
checks = frozen_checks(cfg)
mroot, mm = Path(mcfg["experiment_root"]), Path(mcfg["model_root"])
mchecks = {"phase4m_exp": verify_checksum(mroot, mroot / "manifests/checksums.sha256")["passed"],
"phase4m_model": verify_checksum(mm, mm / "checksums.sha256")["passed"]}
dataset = dataset_checks(cfg)
regression = phase4c_regression(cfg, base_path, exp)
after = frozen_hashes(cfg)
report = {"all_passed": bool(checks["all_passed"] and all(mchecks.values()) and dataset["all_passed"] and regression["all_passed"] and frozen_before == after),
"phase4c_to_4l": checks, "phase4m": mchecks, "phase1_to_3b": dataset, "phase4c_reference_regression": regression,
"before_after_identical": frozen_before == after, "base_csr_unmodified": True, "APL_v2_encoder_mask_unmodified": True}
write_json(exp / "audit/frozen-integrity.json", report)
return report
def main():
ap = argparse.ArgumentParser(); ap.add_argument("--config", type=Path, required=True); args = ap.parse_args()
ncfg, mcfg, cfg, base_path = load_config(args.config)
exp = Path(ncfg["experiment_root"])
identity = pd.read_parquet(exp / "edge-identity/frozen-edge-identity.parquet")
csi = pd.read_parquet(exp / "specificity-curve/CSI.parquet")
replay = pd.read_parquet(exp / "specificity-curve/memory-specificity.parquet")
history = pd.read_parquet(Path(mcfg["experiment_root"]) / "efficacy-overlay/update-history.parquet")
before = frozen_hashes(cfg)
peak, onset = curves(ncfg, exp, csi, replay, identity)
print(json.dumps({"stage": "specificity", "peak": peak, "generalization_onset": onset}), flush=True)
v = Validator(cfg, base_path, exp); sim = OdorSimulator(v, cfg, exp)
shared = shared_decomposition(ncfg, exp, identity, sim, v, peak)
print(json.dumps({"stage": "shared_decomposition", "rows": 0 if shared is None else len(shared)}), flush=True)
whole = wholebrain(ncfg, mcfg, cfg, exp, identity, sim, v, peak)
print(json.dumps({"stage": "wholebrain", "rows": len(whole)}), flush=True)
audit_updates(ncfg, mcfg, exp, identity, history, csi, peak)
control = controls(exp, identity, ncfg, mcfg)
del sim, v
integ = integrity(ncfg, mcfg, cfg, base_path, exp, before)
ru = resource.getrusage(resource.RUSAGE_SELF)
perf = {"peak_RAM_KiB": int(ru.ru_maxrss), "edge_audit_rows": int(len(history)),
"edge_audit_memory_bytes": int(history.memory_usage(deep=True).sum()), "shadow_trace_overhead": "NOT-MEASURED-NOT-RUN",
"wholebrain": json.loads((exp / "performance/whole-brain.json").read_text()),
"isolated": json.loads((exp / "performance/isolated.json").read_text())}
write_json(exp / "performance/summary.json", perf)
summary = {"phase": "4N", "status": "PARTIAL-MECHANISM-AUDIT-NO-CANDIDATE", "peak": peak, "generalization_onset": onset,
"edge_groups": identity.identity_group.value_counts().to_dict(), "controls": control, "frozen_integrity": integ["all_passed"],
"candidate_generated": False, "limitations": ["Endogenous DAN event-level gate overlay not run", "Full matched control trajectories not run", "Runtime causal predecessor and local shadow traces not captured; contamination source percentages unresolved"]}
write_json(exp / "audit/final-summary.json", summary)
model = Path(ncfg["model_root"]); model.mkdir(parents=True, exist_ok=True)
shutil.copy2(args.config, model / "phase4n.yaml"); shutil.copy2(Path(__file__).resolve(), model / "phase4n_finish.py")
shutil.copy2(Path(__file__).with_name("phase4n.py"), model / "phase4n.py")
shutil.copy2(Path(__file__).resolve(), exp / "audit/phase4n_finish.py")
write_json(model / "VERSION.json", {"phase": "4N", "status": summary["status"], "candidate_generated": False,
"identity_sha256": sha256(exp / "edge-identity/frozen-edge-identity.parquet"), "config_sha256": sha256(args.config)})
seal(model, model / "MANIFEST.json", model / "checksums.sha256")
seal(exp, exp / "manifests/manifest.json", exp / "manifests/checksums.sha256")
print(json.dumps(summary), flush=True)
if __name__ == "__main__": main()