Archive MaleCNS research sources, provenance, bibliography and immutable data inventory
This commit is contained in:
@@ -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))
|
||||
Reference in New Issue
Block a user