Files

120 lines
7.7 KiB
Python

#!/usr/bin/env python3
"""Exact-order replay of all frozen Phase 4N KC events through Phase 4M eligibility."""
from __future__ import annotations
import hashlib
import json
import math
from pathlib import Path
import numpy as np
import pandas as pd
import pyarrow.parquet as pq
import yaml
ROOT=Path('/data/digital-drosophila')
EXP=ROOT/'experiments/phase4o-runtime-credit'
MODEL=ROOT/'models/male-cns-lif'
def save(p,x):
p.parent.mkdir(parents=True,exist_ok=True)
p.write_text(json.dumps(x,indent=2,sort_keys=True,default=str)+'\n')
def h(x): return hashlib.sha256(x.tobytes()).hexdigest()
def main():
ncfg=yaml.safe_load((MODEL/'credit-assignment-v1/phase4n.yaml').read_text())
mcfg=yaml.safe_load((MODEL/'kcmbon-efficacy-v1/phase4m.yaml').read_text())
lcfg=yaml.safe_load(Path(mcfg['frozen_phase4l_config']).read_text())
cfg=lcfg
identity=pd.read_parquet(ROOT/'experiments/phase4n-credit-assignment/edge-identity/frozen-edge-identity.parquet')
historical=pd.read_parquet(Path(mcfg['experiment_root'])/'efficacy-overlay/update-history.parquet')
kc_events=pd.read_parquet(ROOT/'experiments/phase4n-credit-assignment/local-eligibility/KC-spike-history.parquet')
graph=Path(cfg['paths']['connectome_root'])
meta=pq.read_table(graph/'neuron-index/unified-metadata.parquet',columns=['neuron_idx','annotation_class']).to_pandas()
kc=meta.loc[meta.annotation_class=='Kenyon_Cell','neuron_idx'].to_numpy(np.int32)
kc.sort()
lookup=np.full(len(meta),-1,np.int32);lookup[kc]=np.arange(len(kc),dtype=np.int32)
src=identity.source_neuron_idx.to_numpy(np.int32);local=lookup[src]
if (local<0).any(): raise RuntimeError('non-KC edge source')
comp=identity.compartment.astype(str).to_numpy()==str(ncfg['matched_compartment_verbatim'])
edgeid=identity.edge_id.to_numpy(np.int64)
id_to_local=pd.Series(np.arange(len(identity),dtype=np.int32),index=edgeid)
events={(int(seed),int(pair)):group[['step','neuron_idx']].to_numpy(np.int32)
for (seed,pair),group in kc_events.groupby(['seed','pairing'])}
histories={(int(seed),int(pair)):group for (seed,pair),group in historical.groupby(['train_seed','pairing'])}
dt=float(cfg['model']['dt_ms']);tau=float(cfg['eligibility_v2']['kc_trace_tau_ms'])
decay=np.float32(math.exp(-dt/tau));step_gate=int(round(float(mcfg['protocol']['dan_time_ms'])/dt))
k=np.float32(cfg['eligibility_v2']['eta_effective_mv']/cfg['model']['w_syn_mv_per_anatomical_synapse'])
bound=np.float32(1.0-cfg['eligibility_v2']['max_fractional_depression'])
results=[];first_signatures={};mismatch_examples=[]
for repeat in range(5):
for seed in ncfg['train_seeds']:
E=np.ones(len(identity),np.float32)
for pairing in range(1,101):
arr=events.get((seed,pairing),np.empty((0,2),np.int32))
# Phase4N emitted events in simulation-time, then neuron_idx order.
if len(arr) and (np.any(arr[:,0]<0) or np.any(arr[:,0]>=int(round(mcfg['protocol']['duration_ms']/dt)))): raise RuntimeError('invalid event timestamp')
if len(arr)>1 and (np.any(arr[1:,0]<arr[:-1,0]) or np.any((arr[1:,0]==arr[:-1,0])&(arr[1:,1]<=arr[:-1,1]))): raise RuntimeError('event tie/duplicate/order ambiguity')
relevant=arr[arr[:,0]<=step_gate]
if len(relevant) and (lookup[relevant[:,1]]<0).any(): raise RuntimeError('non-KC event')
by_step={int(step):lookup[relevant[relevant[:,0]==step,1]] for step in np.unique(relevant[:,0])}
trace=np.zeros(len(kc),np.float32)
for step in range(step_gate+1):
trace*=decay
firing=by_step.get(step)
if firing is not None: trace[firing]+=np.float32(1)
np.minimum(trace,np.float32(1),out=trace)
rec=histories[(seed,pairing)]
rid=id_to_local.loc[rec.edge_id.to_numpy(np.int64)].to_numpy(np.int32)
observed=trace[local[rid]]
expected=rec.eligibility.to_numpy(np.float32)
err=np.abs(observed-expected)
actual=np.flatnonzero(comp&(trace[local]>0))
missing=np.setdiff1d(rid,actual,assume_unique=True)
extra=np.setdiff1d(actual,rid,assume_unique=True)
predicted=np.maximum(bound,E[rid]*(np.float32(1)-k*observed))
frozen_after=rec.E_bounded_after.to_numpy(np.float32)
update_error=float(np.max(np.abs(predicted-frozen_after))) if len(predicted) else 0.0
trace_error=float(err.max()) if len(err) else 0.0
signature=h(np.concatenate((arr.flatten(),observed.view(np.int32),predicted.view(np.int32))))
key=(seed,pairing)
first_signatures.setdefault(key,signature)
row={'repeat':repeat,'seed':seed,'pairing':pairing,'event_count':len(arr),'event_sha256':h(arr),
'gate_step':step_gate,'eligibility_sha256':h(observed),'update_sha256':h(predicted),
'repeat_exact':signature==first_signatures[key],
'frozen_trace_max_abs_error':trace_error,'frozen_update_max_abs_error':update_error,
'missing_frozen_active_edges':len(missing),'extra_active_edges':len(extra),
'exact_to_phase4m':trace_error<=2e-5 and update_error<=2e-5 and len(missing)==0 and len(extra)==0}
if not row['exact_to_phase4m'] and len(mismatch_examples)<30:
j=int(np.argmax(err)) if len(err) else None
row['example']={'edge_id':int(rec.edge_id.iloc[j]) if j is not None else None,
'KC_neuron_idx':int(src[rid[j]]) if j is not None else None,
'timestamp_ms':float(mcfg['protocol']['dan_time_ms']),
'event_trace':float(observed[j]) if j is not None else None,
'frozen_trace':float(expected[j]) if j is not None else None,
'frozen_delta_E':float(frozen_after[j]-rec.E_bounded_before.iloc[j]) if j is not None else None,
'missing_edge_ids':edgeid[missing[:20]].tolist(),'extra_edge_ids':edgeid[extra[:20]].tolist()}
mismatch_examples.append(row.copy())
results.append(row)
# Each trial is run against the sealed Phase4M before/after trajectory.
# A divergent diagnostic event trace never feeds the next trial.
E[rid]=frozen_after
print(json.dumps({'repeat':repeat,'seed':seed,'total_so_far':len(results),
'exact_to_phase4m_so_far':sum(r['exact_to_phase4m'] for r in results)}),flush=True)
save(EXP/'eligibility-replay/event-reproducibility-matrix.json',results)
save(EXP/'eligibility-replay/event-reproducibility-matrix.json',results)
save(EXP/'eligibility-replay/event-replay-summary.json',{'complete_repeats':5,'events_per_repeat':500,
'same_event_and_update_sequence_5_of_5':all(r['repeat_exact'] for r in results),
'all_exact_to_frozen_phase4m':all(r['exact_to_phase4m'] for r in results),
'frozen_exact_count_by_repeat':{str(rep):sum(r['exact_to_phase4m'] for r in results if r['repeat']==rep) for rep in range(5)},
'mismatch_examples':mismatch_examples,
'event_order_contract_observed':'simulation_step ascending, neuron_idx ascending, no duplicate KC/step; trace updated after spike; external gate sampled after step 900 trace update',
'limitation':'Phase4M did not seal its training KC event sequence. Phase4N sealed the later event replay; exact repeat of those events does not prove the original GPU trajectory.'})
if __name__=='__main__':main()