142 lines
8.9 KiB
Python
142 lines
8.9 KiB
Python
"""Read-only event diagnosis of sealed Phase 4S. No simulation or selection."""
|
|
from pathlib import Path
|
|
import json
|
|
import hashlib
|
|
import time
|
|
import numpy as np
|
|
import pandas as pd
|
|
|
|
BASE = Path('/data/digital-drosophila')
|
|
SOURCE = BASE/'experiments/phase4s-timing-credit'
|
|
OUT = BASE/'experiments/phase4t-timing-stability/audit'
|
|
MODEL = BASE/'models/male-cns-lif/timing-stability-v1'
|
|
|
|
def sha(p):
|
|
h=hashlib.sha256()
|
|
with p.open('rb') as f:
|
|
for b in iter(lambda:f.read(8<<20),b''):h.update(b)
|
|
return h.hexdigest()
|
|
|
|
def save(p,x):
|
|
p.write_text(json.dumps(x,indent=2,sort_keys=True,allow_nan=False)+'\n')
|
|
|
|
def verify(root):
|
|
records=[]
|
|
for line in (root/'checksums.sha256').read_text().splitlines():
|
|
expected,rel=line.split(' ',1)
|
|
p=root/rel
|
|
assert p.is_file() and sha(p)==expected, str(p)
|
|
records.append({'path':rel,'sha256':expected})
|
|
return records
|
|
|
|
def main():
|
|
OUT.mkdir(parents=True,exist_ok=True)
|
|
assert not (OUT/'summary.json').exists(), 'Refusing overwrite'
|
|
start=time.perf_counter()
|
|
before=verify(SOURCE)
|
|
prereg=json.loads((SOURCE/'protocol/preregistered.json').read_text())
|
|
chosen=json.loads((SOURCE/'calibration/selection.json').read_text())['chosen']
|
|
comp=prereg['config']['target_compartment']
|
|
identity=pd.read_parquet(SOURCE/'protocol/frozen-edge-identity.parquet')
|
|
assert len(identity)==14232
|
|
target=identity[identity.compartment.eq(comp)]
|
|
source=target.groupby('source_neuron_idx').agg(body_id=('source_body_id','first'),edge_count=('edge_id','size'))
|
|
ns=source.index.to_numpy();multiplicity=source.edge_count.to_numpy(np.float32)
|
|
dt=.1;steps=1200;us=round(chosen['US_ms']/dt);w=chosen['window_ms'];tau=chosen['tau_ms']
|
|
rows=[];hist=[];timelines=[];gates=[];repro=[]
|
|
for p in sorted((SOURCE/'events').glob('*.parquet')):
|
|
split,label,seed=p.stem.split('-');seed=int(seed)
|
|
ev=pd.read_parquet(p)
|
|
assert set(ev.columns)=={'step','neuron_idx'}
|
|
assert ev.step.between(0,steps-1).all() and not ev.duplicated().any()
|
|
t=ev[ev.neuron_idx.isin(ns)].sort_values(['step','neuron_idx'])
|
|
ts=t.step.to_numpy(np.int32);nodes=t.neuron_idx.to_numpy(np.int32)
|
|
last=np.full((steps,len(ns)),-1,np.int32)
|
|
lookup={int(n):j for j,n in enumerate(ns)}
|
|
for s,n in zip(ts,nodes):last[s,lookup[int(n)]]=s
|
|
np.maximum.accumulate(last,axis=0,out=last)
|
|
ages=(np.arange(steps)[:,None]-last).astype(np.float32)*np.float32(dt)
|
|
valid=(last>=0)&(ages<=np.float32(w))
|
|
e=np.zeros_like(ages);e[valid]=np.exp(-ages[valid]/np.float32(tau))
|
|
curve=np.sum(e*multiplicity[None,:],axis=1,dtype=np.float64)
|
|
# Independent single-gate oracle, one KC at a time, no prefix array.
|
|
oracle=0.
|
|
for n,m in zip(ns,multiplicity):
|
|
past=ts[(nodes==n)&(ts<=us)]
|
|
if len(past):
|
|
age=np.float32(us-int(past.max()))*np.float32(dt)
|
|
if age<=np.float32(w):oracle+=float(np.float32(np.exp(-age/np.float32(tau)))*m)
|
|
assert oracle==float(curve[us])
|
|
oldpath=SOURCE/('heldout/results.parquet' if split=='held' else 'calibration/grid.parquet')
|
|
old=pd.read_parquet(oldpath)
|
|
old=old[(old.label==label)&(old.seed==seed)&(old.window_ms==w)&(old.US_ms==chosen['US_ms'])].iloc[0]
|
|
assert float(curve[us])==float(old.matched)
|
|
assert float(curve.mean())==float(old.random_expectation)
|
|
recent=ts[(ts>=us-round(w/dt))&(ts<=us)]
|
|
past=ts[ts<=us];future=ts[ts>us]
|
|
if len(recent):reason='credit_present'
|
|
elif not len(ev):reason='no_KC_events'
|
|
elif not len(t):reason='KC_active_but_no_target_compartment_source_events'
|
|
elif not len(past):reason='target_onset_after_US'
|
|
elif not len(future):reason='target_activity_ended_before_gate'
|
|
else:reason='target_activity_gap_at_gate'
|
|
covered=source.loc[np.unique(nodes)].edge_count.sum() if len(nodes) else 0
|
|
gate_nodes=np.unique(nodes[(ts>=us-round(w/dt))&(ts<=us)])
|
|
buckets=np.bincount(ts//50,minlength=24)
|
|
row={'split':split,'label':label,'seed':seed,'source_file':str(p),'events_sha256':sha(p),
|
|
'KC_events':len(ev),'KC_active':int(ev.neuron_idx.nunique()),'target_events':len(t),
|
|
'target_active_KC':int(t.neuron_idx.nunique()),'target_source_KC_total':len(source),
|
|
'target_edge_total':len(target),'edges_covered_any_time':int(covered),
|
|
'edge_coverage_fraction':float(covered/len(target)),'gate_active_KC':len(gate_nodes),
|
|
'gate_covered_edges':int(source.loc[gate_nodes].edge_count.sum()),'gate_events':len(recent),
|
|
'first_target_ms':float(ts.min()*dt) if len(ts) else None,
|
|
'last_target_ms':float(ts.max()*dt) if len(ts) else None,
|
|
'peak_bin_start_ms':int(buckets.argmax())*5 if len(ts) else None,
|
|
'last_before_US_ms':float(past.max()*dt) if len(past) else None,
|
|
'first_after_US_ms':float(future.min()*dt) if len(future) else None,
|
|
'matched_credit':float(curve[us]),'random_expectation':float(curve.mean()),
|
|
'reason':reason}
|
|
rows.append(row);repro.append({'trial':p.stem,'matched_exact':True,'random_exact':True,'independent_oracle_exact':True})
|
|
for begin in range(0,steps,10):
|
|
selected=(ts>=begin)&(ts<begin+10)
|
|
hist.append({'split':split,'label':label,'seed':seed,'start_ms':begin*dt,
|
|
'target_spikes':int(selected.sum()),'target_active_KC':int(np.unique(nodes[selected]).size)})
|
|
for s,n in zip(ts,nodes):timelines.append({'split':split,'label':label,'seed':seed,
|
|
'emission_step':int(s),'arrival_step':int(s)+18,'neuron_idx':int(n),
|
|
'body_id':int(source.loc[n,'body_id']),'target_edge_multiplicity':int(source.loc[n,'edge_count'])})
|
|
# Full time curve is descriptive only. It cannot select a new parameter on old heldout data.
|
|
for s,c in enumerate(curve):gates.append({'split':split,'label':label,'seed':seed,'US_step':s,'credit':float(c)})
|
|
frame=pd.DataFrame(rows)
|
|
frame.to_parquet(OUT/'per-trial-diagnosis.parquet',index=False)
|
|
pd.DataFrame(hist).to_parquet(OUT/'target-activity-1ms.parquet',index=False)
|
|
pd.DataFrame(timelines).to_parquet(OUT/'target-KC-events.parquet',index=False)
|
|
pd.DataFrame(gates).to_parquet(OUT/'descriptive-gate-curves.parquet',index=False)
|
|
zero=frame[frame.matched_credit==0]
|
|
summary={'stage':'P0 event diagnosis','phase':'4T','source_phase':'4S','trials':len(rows),
|
|
'target_compartment':comp,'target_source_KC_total':len(source),'target_edge_total':len(target),
|
|
'zero_credit_trials':[x for x in rows if x['matched_credit']==0],
|
|
'reason_counts':frame.groupby(['split','label','reason']).size().reset_index(name='count').to_dict('records'),
|
|
'all_zero_trials_explained_by_event_timing':bool(len(zero)>0 and zero.reason.isin(['target_onset_after_US','target_activity_ended_before_gate','target_activity_gap_at_gate']).all()),
|
|
'reproduction':repro,'new_simulations':0,'weight_updates':0,'new_candidate':False,
|
|
'time_semantics':'Original gate uses emission times, including same-step KC. Equal +18 steps on KC and US preserves relative ages; no asymmetric arrival correction.',
|
|
'limits':['Descriptive reuse of known seeds, not independent validation.',
|
|
'Gate absence is diagnosed, upstream cause of onset variability is not established.',
|
|
'No new window or US selection on old heldout outcomes.'],
|
|
'implementation_note':'Initial report serialization stopped on pandas NaN for absent pre-US events; serialize original null-bearing records instead. No scientific result changed.',
|
|
'wall_seconds':time.perf_counter()-start}
|
|
after=verify(SOURCE);assert before==after
|
|
save(OUT/'source-integrity.json',{'pass':True,'checked':len(after),'records':after})
|
|
save(OUT/'summary.json',summary)
|
|
lines=['# Phase 4T / P0 — 已封存事件时序诊断','',
|
|
f'共审计 {len(rows)} 条 KC 事件序列,零 credit {len(zero)} 条。没有新脑模拟、权重更新或候选选择。','',
|
|
'| split | odor | seed | KC active | target active | first target ms | peak bin ms | reason |',
|
|
'|---|---|---:|---:|---:|---:|---:|---|']
|
|
for x in rows:lines.append(f"| {x['split']} | {x['label']} | {x['seed']} | {x['KC_active']} | {x['target_active_KC']} | {x['first_target_ms']} | {x['peak_bin_start_ms']} | {x['reason']} |")
|
|
lines+=['','所有 matched/random credit 均精确复现 Phase 4S,并通过独立逐 KC 单点 oracle。',
|
|
'描述性曲线不能用于把旧 heldout 当成新的验证集。下一实验必须先冻结规则,再使用全新种子。',
|
|
'仅定位 gate 落空原因;不能把时序相关性推断为 APL 或某个回路的因果作用。']
|
|
(OUT/'REPORT.md').write_text('\n'.join(lines)+'\n',encoding='utf-8')
|
|
print(json.dumps({k:v for k,v in summary.items() if k!='reproduction'},indent=2),flush=True)
|
|
|
|
if __name__=='__main__':main()
|