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

104 lines
6.7 KiB
Python

#!/usr/bin/env python3
"""Timing-compatible incoming KC drive paths; not unique causal-spike proofs."""
from __future__ import annotations
import argparse
import json
from collections import defaultdict
from pathlib import Path
import numpy as np
import pandas as pd
from phase4h_audit import write_json
from phase4n import load_config
def main():
ap=argparse.ArgumentParser();ap.add_argument("--config",type=Path,required=True);args=ap.parse_args()
ncfg,mcfg,cfg,_=load_config(args.config);exp=Path(ncfg["experiment_root"])
identity=pd.read_parquet(exp/"edge-identity/frozen-edge-identity.parquet")
bnodes=np.unique(identity.loc[identity.identity_group=="B-specific","source_neuron_idx"].to_numpy(np.int32))
root=Path(cfg["paths"]["connectome_root"])
indptr=np.load(root/"gpu/csr-indptr.int64.npy",mmap_mode="r")
indices=np.load(root/"gpu/csr-indices.int32.npy",mmap_mode="r")
raw=np.load(root/"gpu/csr-weights.int64.npy",mmap_mode="r")
signs=np.load("/data/digital-drosophila/experiments/phase4a-baseline/outputs/derived/synaptic-sign.int8.npy",mmap_mode="r")
meta=pd.read_parquet(root/"neuron-index/unified-metadata.parquet",columns=["neuron_idx","body_id","annotation_class","annotation_type"])
meta=meta.sort_values("neuron_idx").reset_index(drop=True)
if len(signs)!=len(meta) or len(indptr)!=len(meta)+1:raise RuntimeError("frozen graph/schema mismatch")
dan=pd.read_parquet(Path(cfg["paths"]["phase4h_root"])/"mappings/dan-to-mbon-confirmed.parquet",columns=["source_neuron_idx"])
danset=set(map(int,dan.source_neuron_idx.unique()))
edge_ids=np.flatnonzero(np.isin(np.asarray(indices),bnodes)).astype(np.int64)
targets=np.asarray(indices[edge_ids],np.int32)
sources=(np.searchsorted(indptr,edge_ids,side="right")-1).astype(np.int32)
weights=np.asarray(raw[edge_ids],np.float32)*np.asarray(signs[sources],np.float32)*np.float32(cfg["model"]["w_syn_mv_per_anatomical_synapse"])
by_target={int(n):np.flatnonzero(targets==n) for n in bnodes}
classes=meta.annotation_class.fillna("").astype(str).to_numpy()
def source_class(n):
cls=classes[n]
if cls in ("ALPN","olfactory"):return "PN/olfactory"
if cls=="Kenyon_Cell":return "KC recurrent"
if cls=="MBON":return "MBON feedback"
if n in danset:return "DAN"
if "APL" in str(meta.annotation_type.iat[n]):return "APL anatomical"
return "other CNS"
rows=[];pairings=[10,20,50,60,75,100]
dt=float(cfg["model"]["dt_ms"]);delay=int(round(float(cfg["model"]["synaptic_delay_ms"])/dt))
tau=float(cfg["model"]["synaptic_tau_ms"]);window=int(round(5*tau/dt))
for seed in ncfg["train_seeds"]:
for pair in pairings:
file=exp/"whole-brain"/f"spike-events-seed-{seed}-pairings-{pair}.parquet"
events=pd.read_parquet(file)
steps_by_node={int(n):g.step.to_numpy(np.int32) for n,g in events.groupby("neuron_idx")}
for target in bnodes:
times=steps_by_node.get(int(target))
if times is None or not len(times):continue
t=int(times[0])
if t>int(round(float(mcfg["protocol"]["dan_time_ms"])/dt)):continue
cat_totals=defaultdict(float);rank=[]
for position in by_target[int(target)]:
src=int(sources[position]);w=float(weights[position]);
if w<=0:continue
source_steps=steps_by_node.get(src)
if source_steps is None:continue
arrived=source_steps+delay
active=arrived[(arrived<=t)&(arrived>=t-window)]
if not len(active):continue
contribution=float(np.sum(w*np.exp(-(t-active)*dt/tau)))
category=source_class(src);cat_totals[category]+=contribution
latest=int(active[-1])
rank.append((contribution,int(edge_ids[position]),src,latest,category,w))
rank.sort(reverse=True)
top=rank[0] if rank else None
rows.append({"seed":seed,"pairing":pair,"B_specific_KC_neuron_idx":int(target),"B_specific_KC_body_id":int(meta.body_id.iat[target]),
"first_activation_step":t,"first_activation_ms":t*dt,"candidate_predecessor_neuron_idx":None if top is None else top[2],
"candidate_predecessor_body_id":None if top is None else int(meta.body_id.iat[top[2]]),
"actual_edge_id":None if top is None else top[1],"arrival_ms":None if top is None else top[3]*dt,
"candidate_predecessor_population":None if top is None else top[4],
"candidate_predecessor_type":None if top is None else str(meta.annotation_type.iat[top[2]]),
"anatomical_effective_weight_mv":None if top is None else top[5],
"decayed_positive_input_at_spike_mv":None if top is None else top[0],
"positive_input_by_source_class_json":json.dumps(dict(cat_totals),sort_keys=True),
"APL_graded_inhibition_accounted":False,"unique_causal_predecessor_proven":False,
"reason":"Multiple delayed inputs and APL graded inhibition converge; strongest positive timed edge is not a counterfactual causal proof."})
table=pd.DataFrame(rows);table.to_parquet(exp/"whole-brain/B-specific-timed-input-candidates.parquet",index=False)
updates=pd.read_parquet(exp/"local-eligibility/event-level-updates.parquet")
updates=updates[(updates.identity_group=="B-specific")&updates.pairing.isin(pairings)].copy()
linked=updates.merge(table,left_on=["seed","pairing","source_neuron_idx"],
right_on=["seed","pairing","B_specific_KC_neuron_idx"],how="left",validate="many_to_one")
linked.to_parquet(exp/"whole-brain/B-specific-credit-timed-input.parquet",index=False)
distribution=table.candidate_predecessor_population.fillna("none").value_counts().to_dict() if len(table) else {}
write_json(exp/"whole-brain/B-specific-timed-input-summary.json",{"audited_B_specific_KC":int(len(bnodes)),
"rows":int(len(table)),"source_class_of_strongest_timing_compatible_positive_input":distribution,
"linked_B_specific_plastic_update_rows":int(len(linked)),
"linked_updates_with_timed_input":int(linked.actual_edge_id.notna().sum()),
"causal_predecessor_unambiguously_identified":False,
"APL_graded_inhibition_in_source_decomposition":False,
"five_synaptic_tau_lookback_ms":5*tau,"exact_frozen_edge_and_delay_used":True,
"not_interpretable_as_causal_source_percentages":True})
print(json.dumps({"B_nodes":len(bnodes),"rows":len(table),"timed_positive_source_distribution":distribution}),flush=True)
if __name__=="__main__":main()