104 lines
6.7 KiB
Python
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()
|