Archive MaleCNS research sources, provenance, bibliography and immutable data inventory

This commit is contained in:
Digital Drosophila Archive
2026-09-29 03:38:32 +08:00
commit 3981628ff8
682 changed files with 161475 additions and 0 deletions
@@ -0,0 +1,6 @@
eebb61a1b97fb3de90746d23cc3037d1290a97d505ecff7fc784948e68dfa88e phase4t.py
98a16acd2c7ff035134844b0aabd2b4b3b56ab732c91881b0f9b97bf88fc4a46 phase4t.yaml
7c8ab087dfcae9b0871d1f63a124937e15125c6d5d3540156ef5b7c5b4f34a02 phase4t_audit.py
0352394bdf6400b7eadde3ed1ffded07e5598c2c0df915264c5c16df720ca096 timing_stability_diagnostics.py
3fb7077bbcb820f7faef737988ee22c8f0d369bc38252df902c7619cb1469fa9 timing_stability_report.py
4751b2ddf7fa2e223c3d49792f22be586487a49a53129e637a6a97774dcfc5c3 manifest.json
@@ -0,0 +1,29 @@
{
"files": [
{
"bytes": 12119,
"path": "phase4t.py",
"sha256": "eebb61a1b97fb3de90746d23cc3037d1290a97d505ecff7fc784948e68dfa88e"
},
{
"bytes": 2044,
"path": "phase4t.yaml",
"sha256": "98a16acd2c7ff035134844b0aabd2b4b3b56ab732c91881b0f9b97bf88fc4a46"
},
{
"bytes": 9078,
"path": "phase4t_audit.py",
"sha256": "7c8ab087dfcae9b0871d1f63a124937e15125c6d5d3540156ef5b7c5b4f34a02"
},
{
"bytes": 2273,
"path": "timing_stability_diagnostics.py",
"sha256": "0352394bdf6400b7eadde3ed1ffded07e5598c2c0df915264c5c16df720ca096"
},
{
"bytes": 4930,
"path": "timing_stability_report.py",
"sha256": "3fb7077bbcb820f7faef737988ee22c8f0d369bc38252df902c7619cb1469fa9"
}
]
}
@@ -0,0 +1,160 @@
"""Preregistered independent fixed-US stability test. Frozen brain, no learning."""
from pathlib import Path
import argparse
import json
import time
import numpy as np
import pandas as pd
import torch
import yaml
import phase4p
import phase4p_finalize
import phase4s
from phase4p_revalidate import DeterministicAPLSim,door_inputs,make_drive
from phase4r import save,digest,arrhash,curves
from phase4q import last_spike_trace,v2_trace,edge_elig
from phase4h_audit import verify_checksum
def checks(cfg):
sc=yaml.safe_load((Path(cfg['source_model'])/'phase4s.yaml').read_text())
out=phase4s.checks(sc)
for key in ('source_root','source_model'):
root=Path(cfg[key]);out[key]=verify_checksum(root,root/'checksums.sha256')
assert all(x['passed'] for x in out.values())
return out
def gate(rows,cfg,w,u):
f=pd.DataFrame(rows);f=f[(f.window_ms==w)&(f.US_ms==u)];out=[]
for odor,g in f.groupby('odor_id',sort=True):
den=float(g.random_expectation.mean());rat=float(g.matched.mean()/den) if den else None
q=float(np.quantile(g.ratio,0.2));n=int((g.ratio>=cfg['timing_gate']['ratio']).sum())
out.append({'odor_id':odor,'n':len(g),'qualifying':n,'ratio_of_means':rat,'q20_ratio':q,
'zero_credit':int((g.matched==0).sum()),'mean_credit':float(g.matched.mean()),
'pass':bool(den>0 and rat>=cfg['timing_gate']['mean_ratio_each_odor'] and n/len(g)>=cfg['timing_gate']['qualifying_fraction_each_odor'])})
return {'window_ms':int(w),'US_ms':int(u),'tau_ms':float(w/3),'odors':out,
'score':min(x['q20_ratio'] for x in out),'pass':bool(out and all(x['pass'] for x in out))}
def controls(w,u,dt,root):
lookup=np.arange(3,dtype=np.int32);us=round(u/dt);tau=w/3
def calc(ss,nn,gate=us):return last_spike_trace(np.array(ss,np.int32),np.array(nn,np.int32),lookup,gate,w,tau,dt)[0]
recent=calc([us-1],[0]);future=calc([us+1],[0]);old=calc([0],[0],round((w+10)/dt))
unrelated=edge_elig(recent,np.array([0,1,2]),np.zeros(3,bool))
d0=edge_elig(recent,np.array([0,1,2]),np.ones(3,bool),teaching=False)
base=calc([us-1,us],[0,1]);shift=last_spike_trace(np.array([us+17,us+18]),np.array([0,1]),lookup,us+18,w,tau,dt)[0]
path=root/'trace-checkpoint.npz';np.savez(path,trace=base)
tests={'recent_positive':bool(recent[0]>0),'inactive_zero':bool(recent[1]==0),
'future_zero':bool(not future.any()),'distant_zero':bool(not old.any()),
'unrelated_zero':bool(not unrelated.any()),'D0_zero':bool(not d0.any()),
'same_step_one':bool(base[1]==1),'both_arrivals_shift_exact':bool(np.array_equal(base,shift)),
'disk_checkpoint_exact':bool(np.array_equal(base,np.load(path)['trace'])),
'repeated_algebra_exact':all(np.array_equal(base,calc([us-1,us],[0,1])) for _ in range(10))}
save(root/'unit-tests.json',{'scope':'Algebraic controls; not physiological simulations','tests':tests,'pass':all(tests.values())})
assert all(tests.values())
def run(cfg,exp,model):
for d in ('protocol','calibration','validation','events','regression','performance'):(exp/d).mkdir(parents=True,exist_ok=True)
assert not (exp/'protocol/preregistered.json').exists(),'Refusing overwrite'
audit=json.loads((exp/'audit/summary.json').read_text());assert audit['all_zero_trials_explained_by_event_timing']
pre=checks(cfg);save(exp/'protocol/preflight.json',pre)
ocfg,panel,vectors,selection,mapping=door_inputs()
cal=panel[panel.split=='calibration'].iloc[:2]
formal={selection['ODOR_A']['odor_id'],selection['ODOR_B']['odor_id']}
novel=panel[(panel.split=='heldout')&~panel.odor_id.isin(formal)]
assert len(cal)==2 and len(novel)==4 and not set(cal.odor_id)&set(novel.odor_id)
odors=pd.concat([cal,novel]).reset_index(drop=True)
seedsets=[{int(s)+i*cfg['odor_seed_offset'] for i in range(len(odors)) for s in cfg[k]} for k in ('calibration_seeds','validation_seeds')]
assert seedsets[0].isdisjoint(seedsets[1])
prereg={'config':cfg,'odors':odors[['odor_id','odor_name','split']].to_dict('records'),
'created_before_new_simulations':True,'sources':{p.name:digest(p) for p in model.glob('phase4t*') if p.is_file()},
'historical_4S_used_only_for_hypothesis':True,'new_heldout_not_used_for_selection':True}
save(exp/'protocol/preregistered.json',prereg)
pcfg=yaml.safe_load((Path(cfg['runtime_root'])/'phase4p.yaml').read_text())
rt=phase4p.CanonicalRuntime(pcfg,Path(cfg['runtime_root']),exp);sim=DeterministicAPLSim(rt)
lookup=np.full(sim.n,-1,np.int32);lookup[sim.kc]=np.arange(len(sim.kc))
identity=sim.identity;assert len(identity)==14232
target=identity.compartment.eq(cfg['target_compartment']).to_numpy()
loc=lookup[identity.source_neuron_idx.to_numpy()]
multi=np.bincount(loc[target],minlength=len(sim.kc)).astype(np.float32)
duration=cfg['duration_ms'];nsteps=round(duration/sim.dt);dan=sim.dan_targets(cfg['matched_DAN'],cfg['target_compartment'])
start=time.perf_counter();torch.cuda.reset_peak_memory_stats();records=[];live=[];repeats=[]
def trial(oi,seed,split,ws,us):
odor=odors.iloc[oi];t,rates=make_drive(ocfg,panel,vectors,selection,mapping,odor.odor_id,'physiological')
actual=seed+oi*cfg['odor_seed_offset']
r=sim.run(t,rates,actual,duration,collect_events=True);assert r['finite']
phase4s.persist_run(exp,f'{split}-{oi}-{seed}',r)
c=curves(r,lookup,multi,ws,sim.dt,nsteps)
for wi,w in enumerate(ws):
den=float(c[wi].mean())
for u in us:
m=float(c[wi][round(u/sim.dt)])
records.append({'split':split,'odor_id':odor.odor_id,'odor_index':oi,'seed':seed,'actual_seed':actual,
'window_ms':w,'US_ms':u,'matched':m,'random_expectation':den,'ratio':m/den if den else 0.})
return r,t,rates,actual
for oi in range(len(cal)):
for seed in cfg['calibration_seeds']:trial(oi,seed,'calibration',cfg['windows_ms'],cfg['candidate_US_ms'])
print(json.dumps({'stage':'calibration','odor_index':oi,'seeds':len(cfg['calibration_seeds'])}),flush=True)
grid=[gate(records,cfg,w,u) for w in cfg['windows_ms'] for u in cfg['candidate_US_ms']]
good=[x for x in grid if x['pass']]
chosen=sorted(good,key=lambda x:(-x['score'],x['window_ms'],x['US_ms']))[0] if good else None
pd.DataFrame(records).to_parquet(exp/'calibration/grid.parquet',index=False)
save(exp/'calibration/selection.json',{'grid':grid,'chosen':chosen,'selected_before_validation':True})
print(json.dumps({'selected':chosen}),flush=True)
summary={'phase':'4T','P0_diagnosis_pass':True,'calibration':chosen,'seed_validation':None,'odor_validation':None,
'timing_gate_pass':False,'credit_specificity_pass':False,'formal_training_run':False,'memory_claim':False}
if chosen:
w=chosen['window_ms'];u=chosen['US_ms'];us=round(u/sim.dt);controls(w,u,sim.dt,exp/'regression')
for oi in range(len(odors)):
split='seed_validation' if oi<len(cal) else 'odor_validation'
for si,seed in enumerate(cfg['validation_seeds']):
r,t,rates,actual=trial(oi,seed,split,[w],[u])
if si<cfg['live_US_checks']['first_seeds_per_validation_odor']:
for pulse in (us,int(np.random.default_rng(actual+cfg['live_US_checks']['random_seed_offset']).integers(nsteps))):
forced=sim.run(t,rates,actual,duration,dan_targets=dan,dan_pulses=[pulse*sim.dt],collect_events=True);assert forced['finite']
phase4s.persist_run(exp,f'live-{oi}-{seed}-{pulse}',forced)
a,_=last_spike_trace(r['kc_event_step'],r['kc_event_node'],lookup,pulse,w,w/3,sim.dt)
b,_=last_spike_trace(forced['kc_event_step'],forced['kc_event_node'],lookup,pulse,w,w/3,sim.dt)
v=v2_trace(forced['kc_event_step'],forced['kc_event_node'],lookup,pulse,sim.dt)
live.append({'odor_index':oi,'seed':seed,'US_step':pulse,'trace_exact':bool(np.array_equal(a,b)),
'v2_trace_exact':bool(np.array_equal(v,forced['trace']))})
if si==0:
rr=sim.run(t,rates,actual,duration,collect_events=True);assert rr['finite']
repeats.append({'odor_index':oi,'seed':seed,'events_exact':arrhash(rr['kc_event_step'],rr['kc_event_node'])==arrhash(r['kc_event_step'],r['kc_event_node']),
'states_exact':arrhash(rr['final_v'],rr['final_g'])==arrhash(r['final_v'],r['final_g'])})
print(json.dumps({'stage':split,'odor_index':oi,'seeds':len(cfg['validation_seeds'])}),flush=True)
rows=[r for r in records if r['split']!='calibration'];pd.DataFrame(rows).to_parquet(exp/'validation/results.parquet',index=False)
for split in ('seed_validation','odor_validation'):summary[split]=gate([r for r in rows if r['split']==split],cfg,w,u)
save(exp/'regression/live-US-checks.json',live);save(exp/'regression/repeat-checks.json',repeats)
summary['live_controls_pass']=all(x['trace_exact'] and x['v2_trace_exact'] for x in live)
summary['repeats_pass']=all(x['events_exact'] and x['states_exact'] for x in repeats)
summary['timing_gate_pass']=summary['seed_validation']['pass'] and summary['odor_validation']['pass'] and summary['live_controls_pass'] and summary['repeats_pass']
summary['frozen_reference_pass']=phase4s.evidence_regression(cfg,exp,rt,sim)
summary['timing_gate_pass'] &= summary['frozen_reference_pass']
summary['status']='PASS-INDEPENDENT-TIMING-ONLY' if summary['timing_gate_pass'] else 'FAIL-TIMING-STABILITY-OR-GENERALIZATION'
summary['next_gate']='Independent A/B local-credit specificity; no memory claim' if summary['timing_gate_pass'] else 'Stop downstream learning; report failed timing/odor gates without retuning'
save(exp/'validation/summary.json',summary)
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),'GPU_utilization':'not sampled','weight_updates':0})
print(json.dumps(summary,indent=2),flush=True)
def seal(cfg,exp,model):
before=json.loads((exp/'protocol/preflight.json').read_text());after=checks(cfg)
old=phase4p_finalize.integrity();p=json.loads((exp/'protocol/preregistered.json').read_text())
same=all(digest(model/n)==h for n,h in p['sources'].items())
integrity={'old':old,'current':after,'before_after_equal':before==after,'source_hashes_unchanged':same,
'pass':old['all_static_checks_passed'] and before==after and same}
save(exp/'audit/frozen-integrity.json',integrity);assert integrity['pass']
summary=json.loads((exp/'validation/summary.json').read_text());summary['integrity_pass']=True
if summary['timing_gate_pass']:
save(model/'MaleCNS-CSUS-Timing-Stability-v1-candidate.json',{'scope':'Independent seed and timing-heldout odor validation only; not local credit specificity or memory',
'selection':summary['calibration'],'validation':summary})
save(exp/'validation/summary.json',summary)
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 ('checksums.sha256','manifest.json')]
save(root/'manifest.json',{'files':rows});rows.append({'path':'manifest.json','sha256':digest(root/'manifest.json')})
(root/'checksums.sha256').write_text(''.join(r['sha256']+' '+r['path']+'\n' for r in rows))
result=verify_checksum(root,root/'checksums.sha256');assert result['passed'];print(json.dumps(result),flush=True)
if __name__=='__main__':
p=argparse.ArgumentParser();p.add_argument('task',choices=['run','seal']);p.add_argument('--config',required=True);args=p.parse_args()
cfg=yaml.safe_load(Path(args.config).read_text());(run if args.task=='run' else seal)(cfg,Path(cfg['experiment_root']),Path(cfg['model_root']))
@@ -0,0 +1,39 @@
phase: 4T
experiment_root: /data/digital-drosophila/experiments/phase4t-timing-stability
model_root: /data/digital-drosophila/models/male-cns-lif/timing-stability-v1
source_root: /data/digital-drosophila/experiments/phase4s-timing-credit
source_model: /data/digital-drosophila/models/male-cns-lif/timing-credit-v1
runtime_root: /data/digital-drosophila/models/male-cns-lif/deterministic-runtime-v2
phase4p_root: /data/digital-drosophila/experiments/phase4p-deterministic-runtime
duration_ms: 120
target_compartment: "y5B'2a"
matched_DAN: PAM15
candidate_US_ms: [10, 15, 20, 25, 30, 40, 60, 90]
windows_ms: [5, 10, 20, 50, 100, 250]
tau_rule: window/3
calibration_odors: first two records in frozen panel where split=calibration; same identities as 4S
odor_generalization: all frozen panel heldout odors excluding formal ODOR_A and ODOR_B; never used for selection
calibration_seeds: [86101,86102,86103,86104,86105,86106,86107,86108,86109,86110,86111,86112,86113,86114,86115,86116,86117,86118,86119,86120]
validation_seeds: [87101,87102,87103,87104,87105,87106,87107,87108,87109,87110,87111,87112,87113,87114,87115,87116,87117,87118,87119,87120]
odor_seed_offset: 1000000
timing_gate:
ratio: 3.0
qualifying_fraction_each_odor: 0.8
mean_ratio_each_odor: 3.0
selection: maximize minimum across calibration odors of 20th percentile per-seed matched/random ratio; ties choose shorter window then earlier US
random: exact expectation of one US uniformly sampled from 1200 discrete timesteps; identical KC stream
no_zero_denominator_pass: true
live_US_checks:
first_seeds_per_validation_odor: 2
random_seed_offset: 9700000
require_noUS_vs_forcedUS_credit_exact: true
require_v2_CPU_vs_runtime_trace_exact: true
scope:
formal_AB_used: false
weight_updates: false
fixed_US_only: true
response_triggered_US: false
no_odor_specific_US: true
all_old_heldout_data_now_exploratory: true
heldout_odors_not_used_for_timing_selection_but_previously_tested_in_4L: true
candidate: timing-only, not credit-specificity or memory
@@ -0,0 +1,141 @@
"""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()
@@ -0,0 +1,36 @@
"""Descriptive validation failures only. No new timing selection or simulation."""
from pathlib import Path
import json
import numpy as np
import pandas as pd
root=Path('/data/digital-drosophila/experiments/phase4t-timing-stability')
source=Path('/data/digital-drosophila/experiments/phase4s-timing-credit')
prereg=json.loads((root/'protocol/preregistered.json').read_text())
gate=json.loads((root/'validation/summary.json').read_text())
identity=pd.read_parquet(source/'protocol/frozen-edge-identity.parquet')
mask=identity[identity.compartment.eq(prereg['config']['target_compartment'])]
multi=mask.groupby('source_neuron_idx').size()
us=round(gate['calibration']['US_ms']/.1);window=round(gate['calibration']['window_ms']/.1)
rows=[]
for record in pd.read_parquet(root/'validation/results.parquet').to_dict('records'):
tag=f"{record['split']}-{record['odor_index']}-{record['seed']}"
e=pd.read_parquet(root/'events'/f'{tag}.parquet')
t=e[e.neuron_idx.isin(multi.index)]
s=t.step.to_numpy();recent=t[t.step.between(us-window,us)]
past=s[s<=us];after=s[s>us]
rows.append({**record,'odor_name':prereg['odors'][record['odor_index']]['odor_name'],
'meets_ratio':bool(record['ratio']>=prereg['config']['timing_gate']['ratio']),
'target_active_KC':int(t.neuron_idx.nunique()),'target_spikes':len(t),
'first_target_ms':float(s.min()*.1) if len(s) else None,
'last_before_US_ms':float(past.max()*.1) if len(past) else None,
'first_after_US_ms':float(after.min()*.1) if len(after) else None,
'gate_active_KC':int(recent.neuron_idx.nunique()),'gate_spikes':len(recent),
'fraction_target_events_after_US':float((s>us).mean()) if len(s) else None})
pd.DataFrame(rows).to_parquet(root/'validation/descriptive-event-diagnosis.parquet',index=False)
summary={'scope':'Post-validation descriptive only, no selection/retest',
'low_ratio_trials':[x for x in rows if not x['meets_ratio']],
'limitations':['Ratio failure may reflect small numerator and/or denominator changes.',
'No APL/circuit causation inferred; no new candidate timing selected.']}
(root/'validation/descriptive-event-diagnosis.json').write_text(json.dumps(summary,indent=2,allow_nan=False)+'\n')
print(json.dumps(summary,indent=2))
@@ -0,0 +1,49 @@
"""Present preregistered results without modifying any scientific choice."""
from pathlib import Path
import json
root=Path('/data/digital-drosophila/experiments/phase4t-timing-stability')
a=json.loads((root/'audit/summary.json').read_text())
r=json.loads((root/'validation/summary.json').read_text())
p=json.loads((root/'protocol/preregistered.json').read_text())
names={x['odor_id']:x['odor_name'] for x in p['odors']}
lines=['# Phase 4T — 时序稳定性诊断与独立验证','',f"正式实验状态:`{r['status']}`",'',
'## P0:旧事件诊断','',
'20 条 Phase 4S 序列均精确复现原 matched/random credit。3 个零 credit 均为目标 KC 首次活动晚于 US,不是全程无目标活动。','',
'| seed | 首次目标 KC 放电 ms | 全程目标 active KC | 全程覆盖目标边数 |',
'|---|---:|---:|---:|']
for x in a['zero_credit_trials']:
lines.append(f"| {x['seed']} | {x['first_target_ms']:.1f} | {x['target_active_KC']} | {x['edges_covered_any_time']} |")
lines+=['','原 US=20 ms;3 个失败种子都没有 US 前目标 KC 放电。提前奖励与首次目标活动错位解释零 credit,但没有确立上游时序波动的生物学原因。','',
'## P1:新预注册实验','',
'只改候选时序选择准则,不改 frozen brain、输入强度、DAN 强度、学习幅度或 14,232-edge mask。依旧测试原有 8 个 US 时点与 6 个窗口。',
'选择目标:校准气味中最差的第 20 百分位 matched/random ratio 最大;相同分数才选更短窗口和更早 US。每气味 20 seeds,至少 16/20 ratio≥3 且均值比≥3。',
'验证分为同气味新 seeds 和未参与本轮 timing selection 的 4 个气味。后者此前出现在 Phase 4L,不是从未研究过的全新数据。']
if r['calibration']:
c=r['calibration'];lines+=['',f"冻结选择:US={c['US_ms']} ms,window={c['window_ms']} ms,tau={c['tau_ms']:.6f} ms。"]
for key,title in [('calibration','校准'),('seed_validation','同气味独立 seeds'),('odor_validation','跨气味验证')]:
g=r[key]
if g:
lines+=['',f'### {title}','','| 气味 | 达标 seeds | matched/random 均值比 | q20 比值 | 零 credit | 通过 |',
'|---|---:|---:|---:|---:|---|']
for x in g['odors']:
lines.append(f"| {names[x['odor_id']]} | {x['qualifying']}/{x['n']} | {x['ratio_of_means']:.4f} | {x['q20_ratio']:.4f} | {x['zero_credit']} | {x['pass']} |")
lines+=['','## 门槛与后续','',f"Timing gate:{r['timing_gate_pass']}。",f"五组 frozen reference:{r['frozen_reference_pass']}。",
f"Live-US 对照:{r.get('live_controls_pass','未运行')};整脑重复:{r.get('repeats_pass','未运行')}。",
f"下一门槛:{r['next_gate']}。",'','本轮没有训练权重、没有 formal A/B credit 累积、没有游戏或动作 decoder。',
'时间 credit 优于随机,不等于刺激特异记忆;只在独立 gate 通过后才能继续分组 plasticity 和 isolated/whole-brain 功能传递。',
'单元测试中 D=0、unrelated-compartment、far-timing 为代数控制,不冒充生理模拟。US/KC 同步平移 18 步验证相对时间,未改动 delay。',
'历史 integrity 最终结果见 audit/frozen-integrity.json。本报告在封存前生成;全部产物以 checksums.sha256 为准。']
diag=json.loads((root/'validation/descriptive-event-diagnosis.json').read_text())
lines+=['','## 验证失败的事后事件描述(不用于重新选参数)','',
'| 气味 | seed | ratio | 首次目标活动 ms | gate active KC | US 后事件比例 |',
'|---|---:|---:|---:|---:|---:|']
for x in diag['low_ratio_trials']:
lines.append(f"| {x['odor_name']} | {x['seed']} | {x['ratio']:.4f} | {x['first_target_ms']:.1f} | {x['gate_active_KC']} | {x['fraction_target_events_after_US']:.2%} |")
perf=json.loads((root/'performance/summary.json').read_text())
lines+=['','ethyl 3-hydroxyhexanoate 的 7 个未达标种子,在 US 前 gate 仅有 2–12 个目标 KC 激活,84.3%–97.5% 的目标放电在 US 后发生。固定时点的跨气味覆盖仍不足;不能据此反向授予 future credit。',
'另一气味有 1 个零 credit 种子首次目标活动在 29.0 ms。总体通过门槛不表示逐试次零失败。',
'',f"运行耗时(不含编译):{perf['wall_seconds_excluding_build']:.2f} s;PyTorch peak allocated VRAM:{perf['peak_VRAM_bytes']/2**20:.2f} MiB。GPU utilization 未采样。",'',
'额外异常记录:底层冻结加载器出现 NumPy 只读数组转张量提示;此轮未改其源码,完整回归与输入 checksum 用于验证未改写。审计初次 JSON 导出因缺失时刻的 NaN 停止,已修复为 null;无模型运行结果变更。']
(root/'REPORT.md').write_text('\n'.join(lines)+'\n',encoding='utf-8')
print('\n'.join(lines))