Files
digital-drosophila/models/male-cns-lif/pn-feedback-audit-v1/phase4z.py
T

284 lines
22 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Event-resolved read-only diagnosis of frozen trials and existing 4Y hypotheses."""
from pathlib import Path
import argparse,json,time
import numpy as np
import pandas as pd
import torch,yaml
import phase4p,phase4p_finalize,phase4s,phase4v,phase4w,phase4y
from phase4p_revalidate import DeterministicAPLSim,door_inputs,make_drive
from phase4r import save,digest,arrhash
from phase4h_audit import verify_checksum
from transmitter_roles_v1 import roles
def checks(cfg):
old=yaml.safe_load((Path(cfg['prior_model'])/'phase4y.yaml').read_text())
out=phase4y.checks(old)
for key in ('prior_model','prior_root','event_experiment'):
p=Path(cfg[key]);out['Z_input_'+key]=verify_checksum(p,p/'checksums.sha256')
assert all(x['passed'] for x in out.values());return out
class Observer(phase4w.Observer):
def __init__(self,rt,watch,requests,targets):
super().__init__(rt,watch)
self.requests=requests;self.targets=np.asarray(targets,np.int32);self.request_rows=[]
def force_step(self,*a):
step=self.rt.step_index;ii=np.flatnonzero(self.requests[step])
if len(ii):
tg=a[4][torch.as_tensor(ii,device=self.rt.device)]
x=torch.stack((a[0][tg],self.rt.g[tg],a[1][tg].float(),a[2][tg].float())).cpu().numpy()
self.ext.force_step(*a)
if len(ii):
after=a[2][tg].cpu().numpy()
for j,k in enumerate(ii):
node=int(self.targets[k]);status='preexisting' if x[3,j] else ('blocked' if x[2,j] else 'accepted')
self.request_rows.append({'step':step,'neuron_idx':node,'status':status,
'pre_force_v':float(x[0,j]),'pre_force_g':float(x[1,j]),'pre_force_ref':int(x[2,j]),
'preexisting_spike':bool(x[3,j]),'actual_output_spike':bool(after[j])})
assert bool(after[j])==(status!='blocked')
if status=='accepted':self.forced.append((step,node))
def role_codes(meta):
x=roles(meta);out=x.role_group.to_numpy(object).copy()
for cls,code in [('ALPN','ALPN'),('ALLN','ALLN'),('Kenyon_Cell','KC'),('MBON','MBON'),('DAN','DAN')]:
out[meta.annotation_class.eq(cls).to_numpy()]=code
out[meta.annotation_type.eq('DPM').to_numpy()]='DPM'
out[meta.annotation_type.eq('APL').to_numpy()]='APL'
return out
def replay(arrival,overlay,requests,rt):
f=np.float32;v=f(rt.rest);g=f(0);ref=0;events=[];states=[];accepted=[]
for step,a in enumerate(arrival):
g=f(f(g*rt.gd)+a);ref=max(ref-1,0)
v=f(v+f(rt.alpha*f(g-f(v-rt.rest)))) if ref==0 else f(rt.resetv)
fire=ref==0 and v>=rt.threshold
if fire:v=f(rt.resetv);ref=rt.ref_steps
states.append((v,g,float(fire)))
if requests[step] and not fire and ref==0:
v=f(rt.resetv);ref=rt.ref_steps;fire=True;accepted.append(step)
if fire:events.append(step)
g=f(np.float64(g)+overlay[step])
return np.asarray(events,np.int32),np.asarray(states,np.float32),v,g,np.asarray(accepted,np.int32)
def summarize_drive(cfg,exp,name,sim,obs,result,edges,codes,request_frame):
rt=sim.rt;state=np.asarray(obs.states);T=len(state);ev=obs.events()
assert np.array_equal(np.bincount(ev.neuron_idx,minlength=sim.n),result['counts'])
arrivals=ev.merge(edges,left_on='neuron_idx',right_on='source_neuron_idx').drop(columns='neuron_idx')
arrivals=arrivals.rename(columns={'step':'emission_step'})
arrivals['arrival_step']=arrivals.emission_step+rt.delay_steps
arrivals=arrivals[arrivals.arrival_step<T].sort_values(['target_neuron_idx','arrival_step','edge_id'],kind='stable')
# Compact event-derived edge accounting retains exact edge IDs; whole events permit full reconstruction.
summaries=[];replays=[];reg=[]
for wi,node in enumerate(obs.watch):
a=arrivals[arrivals.target_neuron_idx==node]
grouped={int(t):g for t,g in a.groupby('arrival_step',sort=False)}
ar=np.zeros(T,np.float32)
for t,g in grouped.items():ar[t]=np.cumsum(g.effective_weight.to_numpy(np.float32),dtype=np.float32)[-1]
assert np.array_equal(ar,state[:,3,wi]),f'arrival mismatch {node}'
after=np.r_[state[1:,1,wi],result['final_g'][node]]
overlay=after.astype(np.float64)-state[:,5,wi].astype(np.float64)
if codes[node]!='KC':assert not np.any(overlay),'unexpected non-KC overlay'
req=np.zeros(T,bool);q=request_frame[request_frame.neuron_idx==node];req[q.step.to_numpy(np.int32)]=True
base=replay(ar,overlay,req,rt)
expected=ev[ev.neuron_idx==node].step.to_numpy(np.int32)
eq={'queue_exact':True,'events_exact':np.array_equal(base[0],expected),
'post_state_exact':np.array_equal(base[1],state[:,4:7,wi]),
'final_v_exact':base[2]==result['final_v'][node],'final_g_exact':base[3]==result['final_g'][node],
'accepted_exact':np.array_equal(base[4],q[q.status=='accepted'].step.to_numpy(np.int32))}
assert all(eq.values()),(name,int(node),eq)
reg.append({'trial':name,'neuron_idx':int(node),**{k:bool(v) for k,v in eq.items()}})
# Predeclared population omissions plus each actual incoming ALPN edge to the target KC.
modes=[('base',None)]+[('omit_'+c,a.source_role.eq(c).to_numpy()) for c in cfg['isolated_omissions']]
if node==cfg['target_KC']:
modes += [('omit_edge_'+str(e),a.edge_id.eq(e).to_numpy()) for e in edges.loc[(edges.target_neuron_idx==node)&(edges.source_role=='ALPN'),'edge_id']]
for mode,omit in modes:
aa=ar
if omit is not None:
aa=np.zeros(T,np.float32)
for t,g in a.loc[~omit].groupby('arrival_step',sort=False):
aa[int(t)]=np.cumsum(g.effective_weight.to_numpy(np.float32),dtype=np.float32)[-1]
rr=base if omit is None else replay(aa,overlay,req,rt)
replays.append({'trial':name,'neuron_idx':int(node),'mode':mode,'spikes':len(rr[0]),
'late_spikes':int(np.count_nonzero(rr[0]>=round(cfg['late_start_ms']/rt.dt))),
'accepted_requests':len(rr[4]),'max_abs_v_diff':float(np.abs(rr[1][:,0].astype(float)-base[1][:,0]).max())})
for period,lo,hi in [('input_on',0,1200),('post_input',1200,T),('late',5000,T)]:
aa=a[(a.arrival_step>=lo)&(a.arrival_step<hi)]
for eid,g in aa.groupby('edge_id',sort=False):
e=g.iloc[0];weight=float(e.effective_weight)
summaries.append({'trial':name,'period':period,'target_neuron_idx':int(node),'edge_id':int(eid),
'source_neuron_idx':int(e.source_neuron_idx),'source_role':str(e.source_role),
'raw_synapse_count':int(e.raw_synapse_count),'effective_weight':weight,'edge_event_count':len(g),
'signed_arrival_sum':len(g)*weight,'positive_arrival_sum':len(g)*max(0.,weight),
'negative_arrival_sum':len(g)*min(0.,weight)})
return ev,reg,replays,summaries
def request_comparison(exp,oi,seed,a,b):
z=a.merge(b,on=['step','neuron_idx'],suffixes=('_reference','_off'),validate='one_to_one',how='outer',indicator=True)
assert z['_merge'].eq('both').all();z=z.drop(columns='_merge')
z['status_changed']=z.status_reference!=z.status_off
z['state_changed']=(z.pre_force_v_reference!=z.pre_force_v_off)|(z.pre_force_g_reference!=z.pre_force_g_off)|(z.pre_force_ref_reference!=z.pre_force_ref_off)
z.to_parquet(exp/'input-audit'/f'{oi}-{seed}-paired-requests.parquet',index=False)
diff=z[z.status_changed];st=z[z.state_changed]
row={'odor_index':oi,'seed':seed,'scheduled_requests':len(z),'status_changed_requests':len(diff),
'first_status_difference_ms':float(diff.step.min()/10) if len(diff) else None,
'first_requested_state_difference_ms':float(st.step.min()/10) if len(st) else None,
'delivered_output_differences':int((z.actual_output_spike_reference!=z.actual_output_spike_off).sum()),
'neurons_with_status_difference':int(diff.neuron_idx.nunique()),'requests_exact':True}
for cond,q in [('reference',a),('off',b)]:
for status in ('accepted','preexisting','blocked'):row[cond+'_'+status]=int(q.status.eq(status).sum())
return row
def run(cfg,exp,model):
for d in ('protocol','audit','events','states','drive','replay','ancestry','input-audit','regression','performance'):(exp/d).mkdir(parents=True,exist_ok=True)
assert not (exp/'protocol/preregistered.json').exists(),'Refusing overwrite'
save(exp/'audit/preflight.json',checks(cfg))
vc=yaml.safe_load((Path(cfg['input_model'])/'phase4v.yaml').read_text())
assert vc['short_duration_ms']==120 and vc['observation_duration_ms']==1000
save(exp/'protocol/preregistered.json',{'config':cfg,'source_hashes':{p.name:digest(p) for p in model.iterdir() if p.is_file()},
'before_simulation':True,'selection_is_exploratory':True,'input_request_clamp_not_attempted':True})
pc=yaml.safe_load((Path(cfg['runtime_root'])/'phase4p.yaml').read_text())
rt=phase4p.CanonicalRuntime(pc,Path(cfg['runtime_root']),exp);sim=DeterministicAPLSim(rt);sim.install(None)
assert sim.n==166700 and len(rt.raw_np)==25582938 and rt.dt==0.1 and rt.delay_steps==18
codes=role_codes(sim.meta);original_install=sim.install;basehash=arrhash(sim.base.cpu().numpy());weights0=rt.weights.cpu().numpy().copy()
target=cfg['target_KC'];incoming=np.flatnonzero((rt.indices_np==target)&(codes[sim.sources]=='ALPN'))
pn=np.unique(sim.sources[incoming]);watch=np.unique(np.r_[target,pn,cfg['local_loop_nodes']]).astype(np.int32)
assert codes[target]=='KC' and all(codes[cfg['local_loop_nodes']]=='ALLN')
sim.meta.iloc[watch].to_parquet(exp/'audit/watched-metadata.parquet',index=False)
save(exp/'protocol/watched.json',{'target_KC':target,'all_incoming_ALPNs':pn.tolist(),'ALLN_from_prior_event_path':cfg['local_loop_nodes'],'all_watched':watch.tolist()})
dpm=np.flatnonzero(sim.meta.annotation_type.eq('DPM'));de=np.flatnonzero(np.isin(sim.sources,dpm));dg=torch.as_tensor(de,device=sim.device)
assert len(dpm)==2 and len(de)==4934
factor=1.
def install(E):
assert E is None;original_install(None);rt.weights[dg]=sim.base[dg]*float(factor)
sim.install=install
ids=np.flatnonzero(np.isin(rt.indices_np,watch))
edge_base=pd.DataFrame({'edge_id':ids,'source_neuron_idx':sim.sources[ids],'target_neuron_idx':rt.indices_np[ids],
'raw_synapse_count':rt.raw_np[ids],'source_role':codes[sim.sources[ids]]})
edge_base.assign(frozen_effective_weight=weights0[ids]).to_parquet(exp/'audit/incoming-edges.parquet',index=False)
sim.meta.iloc[np.unique(sim.sources[ids])].to_parquet(exp/'audit/incoming-source-metadata.parquet',index=False)
csc=(np.argsort(rt.indices_np,kind='stable'),np.r_[0,np.cumsum(np.bincount(rt.indices_np,minlength=sim.n))])
oldcode=phase4w.classify(sim.meta)
oc,panel,vectors,selection,mapping=door_inputs();panel=panel[panel.split=='calibration'].reset_index(drop=True)
targetkc=np.unique(sim.identity.loc[sim.identity.compartment.eq(vc['target_compartment']),'source_neuron_idx'].to_numpy(np.int32))
regs=[];replays=[];drives=[];paths=[];pathsum=[];requests=[];trials=[];inputhash=[]
start=time.perf_counter();torch.cuda.reset_peak_memory_stats()
for oi in cfg['odor_indices']:
seed=cfg['seed'];odor=panel.iloc[oi];targets,rates=make_drive(oc,panel,vectors,selection,mapping,odor.odor_id,'physiological')
forced=phase4v.forcing(sim,targets,rates,vc['seed_namespace']+oi*vc['odor_seed_offset']+seed,vc,'offset')
ff=forced.cpu().numpy();inputhash.append({'odor_index':oi,'forcing':arrhash(ff),'targets_rates':arrhash(targets,rates)})
reqpair={}
for spec in cfg['conditions']:
factor=spec['factor'];condition=spec['name'];name=f'{oi}-{seed}-{condition}'
obs=Observer(rt,watch,ff,targets);obs.attach()
try:r=phase4v.observed(sim,targets,forced,targetkc,vc)
finally:obs.detach()
old=np.load(Path(cfg['prior_root'])/'states'/f'{name}.npz')
eq={k:bool(np.array_equal(old[k],r[k])) for k in ('counts','final_v','final_g','trace')}
oldev=pd.read_parquet(Path(cfg['prior_root'])/'events'/f'{name}.parquet')
eq['KC_events']=arrhash(oldev.step.to_numpy(),oldev.neuron_idx.to_numpy())==arrhash(r['kc_event_step'],r['kc_event_node'])
eq['finite']=r['finite'];assert all(eq.values()),(name,eq)
regs.append({'trial':name,'prior_Y_exact':eq})
rq=pd.DataFrame(obs.request_rows);rq.to_parquet(exp/'input-audit'/f'{name}-requests.parquet',index=False);reqpair[condition]=rq
last=r['samples'][-1]
assert len(rq)==int(ff.sum())==last['PN_requested_cumulative']
for status,field in [('accepted','PN_accepted_cumulative'),('preexisting','PN_preexisting_cumulative'),('blocked','PN_blocked_cumulative')]:
assert int(rq.status.eq(status).sum())==last[field]
weights=rt.weights.cpu().numpy().copy();expected=weights0.copy();expected[de]*=np.float32(factor);assert np.array_equal(weights,expected)
edges=edge_base.assign(effective_weight=weights[ids])
ev,reg,rep,drive=summarize_drive(cfg,exp,name,sim,obs,r,edges,codes,rq)
regs[-1]['CPU_shadow_replay']=reg;replays+=rep;drives+=drive
ev.to_parquet(exp/'events'/f'{name}-whole-brain.parquet',index=False)
np.savez_compressed(exp/'states'/f'{name}-watched.npz',state=np.asarray(obs.states),watch=watch)
if condition=='frozen_reference':
oldfull=pd.read_parquet(Path(cfg['event_experiment'])/'events'/f'{oi}-{seed}-offset-whole-brain.parquet')
regs[-1]['W_full_events_exact']=bool(ev.equals(oldfull));assert regs[-1]['W_full_events_exact']
ancestry_cfg={'watch_neuron_idx':[target],'late_start_ms':cfg['late_start_ms'],'ancestry':cfg['ancestry']}
pr,ps=phase4w.ancestry(ancestry_cfg,sim,ev,obs.forced,csc,weights,oldcode,name)
# Correct orthogonal role labels; historical W category field never used as biological DAN identity.
for row in pr:
row['source_category']=str(codes[row['source_neuron_idx']]);row['target_category']=str(codes[row['target_neuron_idx']])
for item in ps:
selected=[p for p in pr if p['endpoint']==item['endpoint']]
item['categories']=sorted({p['source_category'] for p in selected})
item['repeated_neuron_ids']=len({p['source_neuron_idx'] for p in selected})<len(selected)
paths+=pr;pathsum+=ps
trials.append({'trial':name,'odor_index':oi,'seed':seed,'condition':condition,'whole_spikes':int(r['counts'].astype(np.int64).sum()),
'KC82312_late_spikes':int(((ev.neuron_idx==target)&(ev.step>=5000)).sum()),'finite':r['finite'],'wall_seconds':r['wall_seconds']})
if oi==cfg['repeat_odor_index']:
again=Observer(rt,watch,ff,targets);again.attach()
try:rr=phase4v.observed(sim,targets,forced,targetkc,vc)
finally:again.detach()
repeat={'events':ev.equals(again.events()),'states':np.array_equal(obs.states,again.states),'requests':rq.equals(pd.DataFrame(again.request_rows))}
assert all(repeat.values());regs[-1]['repeat_exact']=repeat
save(exp/'regression/trials.json',regs)
print(json.dumps({'trial_complete':name,'watched_neurons':len(watch),'incoming_edges':len(ids),'CPU_checks':len(reg)}),flush=True)
requests.append(request_comparison(exp,oi,seed,reqpair['frozen_reference'],reqpair['DPM_fast_current_off']))
sim.install=original_install;original_install(None)
assert arrhash(sim.base.cpu().numpy())==basehash and np.array_equal(rt.weights.cpu().numpy(),weights0)
save(exp/'regression/restore.json',{'base_unchanged':True,'APL_overlay_restored':True,'new_weight_hypotheses':False})
assert phase4s.evidence_regression(cfg,exp,rt,sim)
for folder,name,rows in [('audit','trials',trials),('replay','results',replays),('drive','edge-accounting',drives),('ancestry','event-supported-edges',paths),('input-audit','paired-summary',requests)]:
pd.DataFrame(rows).to_parquet(exp/folder/f'{name}.parquet',index=False)
save(exp/'ancestry/summary.json',pathsum);save(exp/'protocol/input-hashes.json',inputhash)
save(exp/'audit/results.json',{'phase':'4Z','status':'COMPLETE-EVENT-RESOLVED-PN-AUDIT','trials':len(trials),'exact_repeat_trials':2,
'Y_reproductions':len(trials),'W_full_event_reproductions':6,'CPU_shadow_replay_checks':sum(len(x['CPU_shadow_replay']) for x in regs),
'reference_pass':True,'all_finite':True,'input_pairs':len(requests),'pairs_with_status_difference':sum(x['status_changed_requests']>0 for x in requests),
'full_brain_loop_necessity_proven':False,'biological_sign_proven':False,'weight_training':False,'candidate':False,'frozen_baseline_changed':False})
save(exp/'performance/summary.json',{'wall_seconds_excluding_build':time.perf_counter()-start,'peak_VRAM_bytes':torch.cuda.max_memory_allocated(),
'GPU':torch.cuda.get_device_name(0),'diagnostic_overhead_included':True})
def describe(cfg,exp):
d=pd.read_parquet(exp/'drive/edge-accounting.parquet');rep=pd.read_parquet(exp/'replay/results.parquet')
q=pd.read_parquet(exp/'input-audit/paired-summary.parquet');meta=pd.read_parquet(exp/'audit/incoming-source-metadata.parquet')
late=d[d.period=='late'].copy();den=late.groupby(['trial','target_neuron_idx']).positive_arrival_sum.transform('sum')
late['positive_fraction']=np.where(den>0,late.positive_arrival_sum/den,np.nan)
late=late.merge(meta[['neuron_idx','body_id','annotation_type','annotation_class','neurotransmitter','nt_ground_truth']],left_on='source_neuron_idx',right_on='neuron_idx',how='left',validate='many_to_one').drop(columns='neuron_idx')
late.to_parquet(exp/'drive/late-input-fractions.parquet',index=False)
lines=['# Phase 4Z — PN feedback and input acceptance audit','',
'Diagnostic only. No training, no new sign hypothesis, no candidate. The two branches reproduce sealed Phase4Y reference and DPM-off conditions.',
'Target and three ALLNs were selected from prior data; this is mechanism exploration, not heldout validation.',
'All source fractions are positive arrival-weight accounting, not causal percentages or voltage attribution.','',
'## Late positive inputs to KC 82312','', '| condition | source idx | body_id | type | mean fraction |','|---|---:|---:|---|---:|']
z=late[late.target_neuron_idx==cfg['target_KC']].copy();z['condition']=z.trial.str.split('-',n=2).str[-1]
for (cond,idx,body,typ),g in z.groupby(['condition','source_neuron_idx','body_id','annotation_type'],dropna=False):
if g.positive_arrival_sum.sum()>0:lines.append(f'| {cond} | {idx} | {body} | {typ} | {g.positive_fraction.mean():.6f} |')
lines+=['','## Conditional single-cell replay','',
'Other recorded arrivals and measured APL remain fixed; original force requests and refractory logic retained. This does not test whole-brain loop necessity.',
'| neuron_idx | condition | mode | late spike range |','|---:|---|---|---:|']
rep['condition']=rep.trial.str.split('-',n=2).str[-1]
for (node,cond,mode),g in rep.groupby(['neuron_idx','condition','mode']):
if mode=='base' or node==cfg['target_KC'] or (node in cfg['local_loop_nodes'] and mode=='omit_ALLN'):
lines.append(f'| {node} | {cond} | {mode} | {g.late_spikes.min()}–{g.late_spikes.max()} |')
lines+=['','## Input acceptance','', '| odor | requests | accepted reference | accepted off | status differences | first difference ms |','|---:|---:|---:|---:|---:|---:|']
for r in q.itertuples():lines.append(f'| {r.odor_index} | {r.scheduled_requests} | {r.reference_accepted} | {r.off_accepted} | {r.status_changed_requests} | {r.first_status_difference_ms} |')
lines+=['','Identical requests do not establish matched delivered inputs. Refusal and preexisting spikes depend on endogenous state. No forcing clamp or altered refractory rule was introduced.',
'Ancestry edges require actual emission +18 steps <= downstream spike; greedy ranking is not a unique or sufficient causal path. Repeated neuron IDs indicate a time-supported recurrent motif, not proof it is the only sustaining loop.',
'Frozen LIF/APL, raw graph/counts, eligibility and masks unchanged. All detailed state/event checks are in regression/.']
(exp/'REPORT.md').write_text('\n'.join(lines)+'\n',encoding='utf-8')
def seal(cfg,exp,model):
assert not (exp/'checksums.sha256').exists(),'Already sealed'
describe(cfg,exp)
now=checks(cfg);hist=phase4p_finalize.integrity();pre=json.loads((exp/'protocol/preregistered.json').read_text())
ok=hist['all_static_checks_passed'] and now==json.loads((exp/'audit/preflight.json').read_text()) and all(digest(model/n)==h for n,h in pre['source_hashes'].items())
save(exp/'audit/frozen-integrity.json',{'pass':ok,'history':hist,'inputs':now});assert ok
r=json.loads((exp/'audit/results.json').read_text());r['integrity_pass']=True;save(exp/'audit/results.json',r)
for root in (model,exp):
rows=[{'path':p.relative_to(root).as_posix(),'sha256':digest(p),'bytes':p.stat().st_size} for p in sorted(root.rglob('*')) if p.is_file() and p.name not in ('manifest.json','checksums.sha256')]
save(root/'manifest.json',{'files':rows});rows.append({'path':'manifest.json','sha256':digest(root/'manifest.json')})
(root/'checksums.sha256').write_text(''.join(x['sha256']+' '+x['path']+'\n' for x in rows))
check=verify_checksum(root,root/'checksums.sha256');assert check['passed'];print(json.dumps(check),flush=True)
if __name__=='__main__':
p=argparse.ArgumentParser();p.add_argument('task',choices=['run','seal']);p.add_argument('--config',required=True);a=p.parse_args()
cfg=yaml.safe_load(Path(a.config).read_text());(run if a.task=='run' else seal)(cfg,Path(cfg['experiment_root']),Path(cfg['model_root']))