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,5 @@
771016bac7aa91a376d69f33fa065cd080c9d488258d666eaef119a5a59f60cb phase4v-literature.md
9007a0b7b01760d05a0d29d01e8551452d8029dee507992f4f06b1aa74885e53 phase4v.py
f35a45efb55f7702eb85adedd4d6ac284735c39470f0cb8b515f501927a7539e phase4v.yaml
7151ae11391a3e6e9dd011cc7667cfc7dcccffc7e0943fd87d4603b1405f2a99 phase4v_analyze.py
d8de7192d56bc719efbfb6a6ea5048eb7e36a4de64e690db41fac7e4e7bddde7 manifest.json
@@ -0,0 +1,24 @@
{
"files": [
{
"bytes": 2761,
"path": "phase4v-literature.md",
"sha256": "771016bac7aa91a376d69f33fa065cd080c9d488258d666eaef119a5a59f60cb"
},
{
"bytes": 16601,
"path": "phase4v.py",
"sha256": "9007a0b7b01760d05a0d29d01e8551452d8029dee507992f4f06b1aa74885e53"
},
{
"bytes": 1727,
"path": "phase4v.yaml",
"sha256": "f35a45efb55f7702eb85adedd4d6ac284735c39470f0cb8b515f501927a7539e"
},
{
"bytes": 6801,
"path": "phase4v_analyze.py",
"sha256": "7151ae11391a3e6e9dd011cc7667cfc7dcccffc7e0943fd87d4603b1405f2a99"
}
]
}
@@ -0,0 +1,27 @@
# CS/US 语义与时间尺度依据
核对:2026-09-29。延续 Phase 4U 的 Google Research / Google DeepMind 研究边界:连接组、外部 RL 身体控制与脑内关联学习分别验收。本轮不引入 RL。
## 原始实验,不直接移植到模型
- [Handler et al., Cell 2019](https://pmc.ncbi.nlm.nih.gov/articles/PMC9012144/),doi:10.1016/j.cell.2019.05.040:公开方法描述的一套闭环实验用 10 s ACV 气味,最后 1 s 进行 PAM 光刺激;反向配对先进行 1 s DAN 刺激,随后隔 1 s 呈现 10 s 气味。训练结束到测试开始为 40 s。这是特定制备/通路的实验,不是所有 compartment 的统一参数。
- [Dylla et al., Frontiers in Neural Circuits 2017](https://www.frontiersin.org/journals/neural-circuits/articles/10.3389/fncir.2017.00042/full),doi:10.3389/fncir.2017.00042:一套 trace-conditioning 实验采用 10 s CS,CS/US 起点间隔 15 s(配对)或 90 s(非配对),US 为四次 1.5 s 电刺激;试次间隔 210 s。刺激持续时间、起点间隔、刺激之间的空隙与试次间隔是不同变量。
以上为简短方法归纳,不转载正文。不将电压、光照强度、DAN spike count 与 dopamine concentration 等同;不把秒级实验任意压缩成毫秒并称为生理复现。
## 当前模型的差别
1. CS:DoOR 静态归一化响应映射为 PN Bernoulli 放电请求,整个 120 ms 试次保持同一 rate,没有气味输送/受体时序模型。
2. 初态:每试次 V=rest,g/ref/delay queue=0,APL state=0;没有经过真实试次间隔的连续状态。
3. US:指定 DAN 一步放电请求。refractory 或当步已有 spike 时请求不会额外产生 spike。
4. 教学 gate:由外部计划时刻取 KC trace;未以实际 DAN 接受数、dopamine release、receptor kinetics 或 second messenger 定义。
5. Gate 和神经传输区分:KC/DAN spikes 的网络效应保留 18 步延迟,外部教学 gate 使用 emission 时刻。两者共同平移不改变相对间隔,但并不构成生化模型。
## 本轮检验
- 先复现短试次,确认仅添加观测没有改变动力学。
- 观察 1 s 持续同率输入,以及 120 ms 输入后关闭的对照。不把增加持续时间当作改善 learning 的调参。
- 使用完全相同前 120 ms 输入,防止不同随机矩阵形状造成前缀改变。
- 100 ms 无输入预热是否仅平移响应?若初态是吸收静息态,空等不等于生理稳态预热。
- 分开记录 scheduled US、实际新增 DAN spikes、已有自然 spikes、refractory 拒绝、外部 teaching gate。
- 无权重更新、无 US 扫描、无 APL ablation、无新学习候选。失败边界只能约束当前模型,不能替真实果蝇作不可能性结论。
@@ -0,0 +1,227 @@
"""Read-only CS/US semantics diagnostics; no learning or parameter selection."""
from pathlib import Path
import argparse
import json
import math
import time
import numpy as np
import pandas as pd
import torch
import yaml
import phase4p
import phase4p_finalize
import phase4s
import phase4u
from phase4p_revalidate import DeterministicAPLSim, door_inputs, make_drive
from phase4r import save, digest, arrhash
from phase4h_audit import verify_checksum
def checks(cfg):
old = yaml.safe_load((Path(cfg['prior_model'])/'phase4u.yaml').read_text())
out = phase4u.checks(old)
for key in ('prior_root', 'prior_model'):
root = Path(cfg[key]); out['U_'+key] = verify_checksum(root, root/'checksums.sha256')
assert all(x['passed'] for x in out.values())
return out
def forcing(sim, targets, rates, seed, cfg, mode):
block = round(cfg['forcing_block_ms']/sim.dt)
steps = round((cfg['observation_duration_ms'] if mode in ('sustained','offset') else cfg['short_duration_ms'])/sim.dt)
probs = torch.as_tensor(np.asarray(rates, np.float32)*sim.dt/1000., device=sim.device)
parts = []
for b in range(math.ceil(steps/block)):
gen = torch.Generator(device=sim.device); gen.manual_seed(int(seed+b*cfg['forcing_block_seed_stride']))
parts.append((torch.rand((block,len(targets)),generator=gen,device=sim.device)<probs[None,:]).to(torch.uint8))
out = torch.cat(parts)[:steps]
if mode == 'offset': out[block:] = 0
if mode == 'warmup':
out = torch.cat((torch.zeros((round(cfg['warmup_ms']/sim.dt),len(targets)),dtype=torch.uint8,device=sim.device),out))
return out
def observed(sim, targets, forced, target_kc, cfg, dan=None, pulse=None):
"""Statement ordering and all state mutations mirror frozen DeterministicAPLSim.run."""
rt=sim.rt; sim.install(None); rt.reset(); steps=len(forced)
tg=torch.as_tensor(targets,dtype=torch.int64,device=sim.device)
dan=np.empty(0,np.int32) if dan is None else dan
dg=torch.as_tensor(dan,dtype=torch.int64,device=sim.device)
df=torch.zeros((steps,len(dan)),dtype=torch.uint8,device=sim.device)
ds=set() if pulse is None else {round(pulse/sim.dt)}
for s in ds: df[s]=1
apl=torch.zeros(len(sim.apl),dtype=torch.float32,device=sim.device)
trace=torch.zeros(len(sim.kc),dtype=torch.float32,device=sim.device)
first=torch.full((sim.n,),-1,dtype=torch.int32,device=sim.device)
ad=np.float32(math.exp(-sim.dt/20.)); td=np.float32(math.exp(-sim.dt/100.))
trace_at=[]; evs=[]; evn=[]; samples=[]; us=[]; prefix=None
stride=round(cfg['sampling_ms']/sim.dt); short=round(cfg['short_duration_ms']/sim.dt)
requests=0; accepted=0; existing=0; blocked=0
target_gpu=torch.as_tensor(target_kc,dtype=torch.int64,device=sim.device)
torch.cuda.synchronize(); start=time.perf_counter()
for step in range(steps):
rt.ext.state_step(rt.v,rt.g,rt.ref,rt.queue[step%rt.qlen],rt.spike_u8,rt.counts,
float(rt.gd),float(rt.alpha),float(rt.rest),float(rt.resetv),float(rt.threshold),rt.ref_steps)
pre=rt.spike_u8[tg].clone(); pre_ref=rt.ref[tg].clone()
rt.ext.force_step(rt.v,rt.ref,rt.spike_u8,rt.counts,tg,forced[step],float(rt.resetv),rt.ref_steps)
req=forced[step].bool(); requests+=int(req.sum())
accepted+=int((req & ~pre.bool() & (pre_ref==0)).sum())
existing+=int((req & pre.bool()).sum()); blocked+=int((req & ~pre.bool() & (pre_ref!=0)).sum())
if step in ds: before=rt.spike_u8[dg].clone(); refs=rt.ref[dg].clone()
if len(dan): rt.ext.force_step(rt.v,rt.ref,rt.spike_u8,rt.counts,dg,df[step],float(rt.resetv),rt.ref_steps)
if step in ds:
after=rt.spike_u8[dg]
for j,node in enumerate(dan):
us.append({'step':step,'time_ms':step*sim.dt,'neuron_idx':int(node),'preexisting_spike':bool(before[j]),
'pre_force_refractory':int(refs[j]),'accepted_new_spike':bool(after[j] and not before[j]),
'actual_output_spike':bool(after[j]),'teaching_gate_sampled':True,'first_edge_arrival_ms':(step+rt.delay_steps)*sim.dt})
sources=torch.nonzero(rt.spike_u8,as_tuple=False).flatten()
if sources.numel():
missing=first[sources]<0; first[sources[missing]]=step
rt.canonical_fanout(sources,rt.queue[(step+rt.delay_steps)%rt.qlen])
kc_spike=rt.spike_u8[sim.kc_gpu]
trace.mul_(float(td)).add_(kc_spike.to(torch.float32)).clamp_max_(1.)
if step in ds: trace_at.append(trace.clone())
active=torch.nonzero(kc_spike,as_tuple=False).flatten()
if active.numel():
node=sim.kc_gpu[active].cpu().numpy().astype(np.int32)
evn.append(node); evs.append(np.full(len(node),step,np.int32))
apl.mul_(float(ad)).add_(torch.mv(sim.input_gpu,rt.spike_u8.to(torch.float32)))
inhibition=torch.matmul(apl,sim.output_gpu)*np.float32(.01)
rt.g[sim.kc_gpu]=rt.g[sim.kc_gpu]-inhibition
rt.step_index+=1
if step==short-1:
prefix={k:x.cpu().numpy().copy() for k,x in [('counts',rt.counts),('final_v',rt.v),('final_g',rt.g)]}
if (step+1)%stride==0:
samples.append({'step':step,'time_ms':(step+1)*sim.dt,'PN_requested_cumulative':requests,
'PN_accepted_cumulative':accepted,'PN_preexisting_cumulative':existing,'PN_blocked_cumulative':blocked,
'KC_spikes_cumulative':int(rt.counts[sim.kc_gpu].sum()),'target_KC_spikes_cumulative':int(rt.counts[target_gpu].sum()),
'KC_active_cumulative':int((rt.counts[sim.kc_gpu]>0).sum()),'KC_g_mean':float(rt.g[sim.kc_gpu].mean()),
'KC_V_mean':float(rt.v[sim.kc_gpu].mean()),'KC_threshold_margin_min':float((rt.threshold-rt.v[sim.kc_gpu]).min()),
'APL_state_0':float(apl[0]),'APL_state_1':float(apl[1]),'APL_inhibition_mean':float(inhibition.mean()),
'finite':bool(torch.isfinite(rt.v).all() and torch.isfinite(rt.g).all())})
torch.cuda.synchronize()
counts=rt.counts.cpu().numpy().copy()
return {'counts':counts,'kc_counts':counts[sim.kc], 'first':first.cpu().numpy().copy(),
'final_v':rt.v.cpu().numpy().copy(),'final_g':rt.g.cpu().numpy().copy(),
'trace':np.zeros(len(sim.kc),np.float32) if not trace_at else trace_at[-1].cpu().numpy().copy(),
'kc_event_step':np.concatenate(evs) if evs else np.empty(0,np.int32),
'kc_event_node':np.concatenate(evn) if evn else np.empty(0,np.int32),
'prefix':prefix,'samples':samples,'US':us,'wall_seconds':time.perf_counter()-start,
'finite':all(x['finite'] for x in samples)}
def exact(a,b,keys):
return {k:bool(np.array_equal(a[k],b[k])) for k in keys}
def run(cfg,exp,model):
for name in ('protocol','audit','events','states','observations','regression','performance'): (exp/name).mkdir(parents=True,exist_ok=True)
assert not (exp/'protocol/preregistered.json').exists(),'Refusing overwrite'
save(exp/'audit/preflight.json',checks(cfg))
oc,panel,vectors,selection,mapping=door_inputs(); panel=panel[panel.split=='calibration'].reset_index(drop=True)
assert len(panel)==6
save(exp/'protocol/preregistered.json',{'config':cfg,'panel':panel.to_dict('records'),
'sources':{p.name:digest(p) for p in model.iterdir() if p.is_file()},'before_simulation':True,'no_parameter_selection':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)
assert len(sim.identity)==14232
target=np.unique(sim.identity.loc[sim.identity.compartment.eq(cfg['target_compartment']),'source_neuron_idx'].to_numpy(np.int32))
dan=sim.dan_targets(cfg['matched_DAN'],cfg['target_compartment'])
save(exp/'audit/semantics.json',{'CS':'frozen physiological DoOR-to-PN rates; Bernoulli force request per 0.1ms step; refractory may refuse',
'US':'one scheduled force request per confirmed PAM15 neuron; engineering teaching trace sampled at scheduled emission step regardless acceptance',
'dopamine_concentration_measured':False,'initial_state':'all V at rest; g/ref/queue/APL/trace zero',
'delay_steps':rt.delay_steps,'delay_ms':rt.delay_steps*sim.dt,'DAN_ids':dan.tolist(),'target_KC_ids':target.tolist(),
'long_input':'120ms RNG blocks with explicit seed stride; first block identical, later sequence not claimed to equal legacy long run',
'time_scaling_to_biological_seconds':'unknown; 1000ms is observation, not conditioning replication'})
keys=['counts','first','final_v','final_g','trace','kc_event_step','kc_event_node']; regress=[]; summaries=[]; usrows=[]
start=time.perf_counter(); torch.cuda.reset_peak_memory_stats()
def persist(name,r,oi,seed,mode):
assert r['finite'],'Nonfinite state'
phase4s.persist_run(exp,name,r)
np.savez_compressed(exp/'states'/f'{name}.npz',**{k:r[k] for k in ('counts','final_v','final_g','trace')})
pd.DataFrame(r['samples']).to_parquet(exp/'observations'/f'{name}.parquet',index=False)
steps=r['kc_event_step']; nodes=r['kc_event_node']; t=steps[np.isin(nodes,target)]*sim.dt
row={'odor_index':oi,'odor_name':panel.iloc[oi].odor_name,'seed':seed,'condition':mode,
'KC_active':int(np.count_nonzero(r['kc_counts'])),'KC_events':len(steps),'target_events':len(t),
'first_target_ms':float(t.min()) if len(t) else None,'median_target_ms':float(np.median(t)) if len(t) else None,
'target_events_after120ms':int(np.count_nonzero(t>=120)), 'KC_events_after120ms':int(np.count_nonzero(steps*sim.dt>=120)),
'last_KC_event_ms':float(steps.max()*sim.dt) if len(steps) else None,'wall_seconds':r['wall_seconds'],'finite':r['finite']}
summaries.append(row)
for oi,odor in panel.iterrows():
targets,rates=make_drive(oc,panel,vectors,selection,mapping,odor.odor_id,'physiological')
for si,seed in enumerate(cfg['seeds']):
actual=cfg['seed_namespace']+oi*cfg['odor_seed_offset']+seed
legacy=sim.run(targets,rates,actual,cfg['short_duration_ms'],collect_events=True)
f=forcing(sim,targets,rates,actual,cfg,'short')
short=observed(sim,targets,f,target,cfg)
eq=exact(legacy,short,keys); regress.append({'odor':oi,'seed':seed,'test':'short-exact','fields':eq})
save(exp/'regression/instrumentation.json',regress); assert all(eq.values()),'Instrumentation diverges'
persist(f'{oi}-{seed}-short',short,oi,seed,'short')
for mode in ('sustained','offset'):
ff=forcing(sim,targets,rates,actual,cfg,mode); assert torch.equal(ff[:len(f)],f)
r=observed(sim,targets,ff,target,cfg)
eq=exact(legacy,r['prefix'],['counts','final_v','final_g'])
mask=r['kc_event_step']<len(f)
eq['events']=arrhash(r['kc_event_step'][mask],r['kc_event_node'][mask])==arrhash(legacy['kc_event_step'],legacy['kc_event_node'])
regress.append({'odor':oi,'seed':seed,'test':mode+'-prefix','fields':eq}); assert all(eq.values())
persist(f'{oi}-{seed}-{mode}',r,oi,seed,mode)
if si==0:
warm=observed(sim,targets,forcing(sim,targets,rates,actual,cfg,'warmup'),target,cfg)
eq=exact(legacy,warm,['counts','final_v','final_g','trace','kc_event_node'])
eq['shifted_events']=np.array_equal(warm['kc_event_step']-round(cfg['warmup_ms']/sim.dt),legacy['kc_event_step'])
regress.append({'odor':oi,'seed':seed,'test':'quiet-warmup','fields':eq})
persist(f'{oi}-{seed}-warmup',warm,oi,seed,'warmup')
for pulse in cfg['US_audit_ms']:
lr=sim.run(targets,rates,actual,cfg['short_duration_ms'],dan_targets=dan,dan_pulses=[pulse],collect_events=True)
r=observed(sim,targets,f,target,cfg,dan,pulse); eq=exact(lr,r,keys)
regress.append({'odor':oi,'seed':seed,'test':f'US{pulse}-exact','fields':eq}); assert all(eq.values())
usrows.extend([dict(x,odor_index=oi,seed=seed,US_ms=pulse) for x in r['US']])
persist(f'{oi}-{seed}-US{pulse}',r,oi,seed,f'US{pulse}')
save(exp/'regression/instrumentation.json',regress)
print(json.dumps({'completed_odor':oi,'seed':seed,'trials':len(summaries)}),flush=True)
pd.DataFrame(summaries).to_parquet(exp/'audit/trials.parquet',index=False)
pd.DataFrame(usrows).to_parquet(exp/'audit/US-acceptance.parquet',index=False)
reference=phase4s.evidence_regression(cfg,exp,rt,sim)
result={'phase':'4V','status':'COMPLETE-DIAGNOSTIC-ONLY','reference_pass':reference,
'exact_instrumentation_pass':all(all(x['fields'].values()) for x in regress),'diagnostic_trials':len(summaries),
'weight_updates':0,'candidate_created':False,'reserved_test_odors_consumed':0,
'DAN_requests':len(usrows),'DAN_accepted_new':sum(x['accepted_new_spike'] for x in usrows),
'DAN_preexisting':sum(x['preexisting_spike'] for x in usrows),
'DAN_refused':sum(not x['actual_output_spike'] for x in usrows)}
assert result['exact_instrumentation_pass'] and reference
save(exp/'audit/results.json',result)
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),
'measurement':'instrumented diagnostics include synchronous observation overhead; not runtime speed benchmark'})
print(json.dumps(result),flush=True)
def seal(cfg,exp,model):
now=checks(cfg); history=phase4p_finalize.integrity(); pre=json.loads((exp/'protocol/preregistered.json').read_text())
passed=history['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['sources'].items())
save(exp/'audit/frozen-integrity.json',{'pass':passed,'history':history,'current':now}); assert passed
r=json.loads((exp/'audit/results.json').read_text()); r['integrity_pass']=True; save(exp/'audit/results.json',r)
df=pd.read_parquet(exp/'audit/trials.parquet'); lines=['# Phase 4V — CS/US semantics and timescale audit','',
'Status: COMPLETE-DIAGNOSTIC-ONLY. No training, selection, new timing policy or learned candidate.',
'Frozen instrumentation and five reference trials: exact PASS. Frozen checksums: PASS.',
'Only six old calibration odors were used; six reserved test odors remain unconsumed.','',
'| Odor | Condition | mean KC events | mean target events | mean target events >=120ms |',
'|---|---|---:|---:|---:|']
for (odor,mode),g in df.groupby(['odor_name','condition'],sort=False):
lines.append(f"| {odor} | {mode} | {g.KC_events.mean():.2f} | {g.target_events.mean():.2f} | {g.target_events_after120ms.mean():.2f} |")
lines += ['',f"DAN requests: {r['DAN_requests']}; accepted new spikes: {r['DAN_accepted_new']}; preexisting: {r['DAN_preexisting']}; refused: {r['DAN_refused']}.",
'US is a force request, not measured dopamine. Teaching trace is sampled at the scheduled emission step; synaptic arrivals are 1.8ms later.',
'Observation does not establish biological time scaling or associative memory. Literature boundaries are in phase4v-literature.md.',
'No game, decoder, RL, visual input, gain/threshold changes, eligibility changes, or plastic-mask changes.']
(exp/'REPORT.md').write_text('\n'.join(lines)+'\n',encoding='utf-8')
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']))
@@ -0,0 +1,40 @@
phase: 4V
experiment_root: /data/digital-drosophila/experiments/phase4v-csus-semantics
model_root: /data/digital-drosophila/models/male-cns-lif/csus-audit-v1
runtime_root: /data/digital-drosophila/models/male-cns-lif/deterministic-runtime-v2
phase4p_root: /data/digital-drosophila/experiments/phase4p-deterministic-runtime
prior_root: /data/digital-drosophila/experiments/phase4u-odor-generalization
prior_model: /data/digital-drosophila/models/male-cns-lif/odor-timing-generalization-v1
target_compartment: "y5B'2a"
matched_DAN: PAM15
odors: all six original calibration odors only; do not consume six reserved Phase4U test odors
seeds: [131101,131102,131103]
seed_namespace: 80000000
odor_seed_offset: 1000000
short_duration_ms: 120
observation_duration_ms: 1000
warmup_ms: 100
US_audit_ms: [25,90]
forcing_block_ms: 120
forcing_block_seed_stride: 1000003
sampling_ms: 1
activity_bin_ms: 10
conditions:
- short frozen replay for every seed
- sustained same-rate PN input to 1000ms
- same first120ms then PN input off until1000ms
- silent100ms warmup followed by same120ms input, first seed each odor
- scheduled US25 or90ms force-acceptance audit, first seed each odor
checks:
mandatory_before_long_trials: short instrumented run vs frozen original counts/events/finalV/finalG/trace exact
long_trial_prefix: first120ms inputs and complete neuron counts/V/g match original
no_runtime_source_modification: true
frozen_reference: five original deterministic trials
scope:
diagnostic_only: true
learning: false
parameter_selection: false
new_timing_candidate: false
new_US_policy: false
unknown_biological_time_scaling: true
long_input_is_observation_not_biological_conditioning_replication: true
@@ -0,0 +1,93 @@
"""Descriptive audit only; no parameter ranking, fitting or candidate selection."""
from pathlib import Path
import json
import numpy as np
import pandas as pd
import yaml
from phase4r import save
cfg=yaml.safe_load(Path(__file__).with_name('phase4v.yaml').read_text())
exp=Path(cfg['experiment_root']); prior=Path(cfg['prior_root'])
assert not (exp/'checksums.sha256').exists(),'Already sealed'
target=set(json.loads((exp/'audit/semantics.json').read_text())['target_KC_ids'])
old=[]
for p in sorted((prior/'events').glob('calibration-*.parquet')):
x=pd.read_parquet(p); t=x.loc[x.neuron_idx.isin(target),'step'].to_numpy()*.1
old.append({'trial':p.stem,'events':len(t),'first_ms':float(t.min()) if len(t) else None,
'fraction_after25ms':float((t>25).mean()) if len(t) else None,
'fraction_after60ms':float((t>60).mean()) if len(t) else None,
'fraction_after90ms':float((t>90).mean()) if len(t) else None})
assert len(old)==120
pd.DataFrame(old).to_parquet(exp/'audit/prior120-event-distribution.parquet',index=False)
trials=pd.read_parquet(exp/'audit/trials.parquet'); details=[]; bins=[]
for _,r in trials.iterrows():
name=f'{r.odor_index}-{r.seed}-{r.condition}'
x=pd.read_parquet(exp/'events'/f'{name}.parquet')
obs=pd.read_parquet(exp/'observations'/f'{name}.parquet')
assert (obs.PN_requested_cumulative == obs.PN_accepted_cumulative+obs.PN_preexisting_cumulative+obs.PN_blocked_cumulative).all()
if r.condition=='offset':
assert obs.loc[obs.time_ms>=cfg['short_duration_ms'],'PN_requested_cumulative'].nunique()==1
t=x.step.to_numpy()*.1; tt=x.loc[x.neuron_idx.isin(target),'step'].to_numpy()*.1
for start in np.arange(0,float(obs.time_ms.max()),cfg['activity_bin_ms']):
end=start+cfg['activity_bin_ms']; mask=(t>=start)&(t<end)
bins.append({'odor_index':int(r.odor_index),'seed':int(r.seed),'condition':r.condition,'start_ms':float(start),
'KC_events':int(mask.sum()),'KC_active':int(x.loc[mask,'neuron_idx'].nunique()),
'target_events':int(((tt>=start)&(tt<end)).sum())})
with np.load(exp/'states'/f'{name}.npz') as state:
count=state['counts']; total=int(count.astype(np.int64).sum()); active=int(np.count_nonzero(count))
row=r.to_dict(); row.update({'whole_brain_spikes':total,'whole_brain_active':active,
'KC_events_120_200ms':int(((t>=120)&(t<200)).sum()),
'KC_events_after200ms':int((t>=200).sum()),'KC_events_after500ms':int((t>=500).sum()),
'PN_requests':int(obs.iloc[-1].PN_requested_cumulative),'PN_new_spikes':int(obs.iloc[-1].PN_accepted_cumulative),
'PN_refractory_blocked':int(obs.iloc[-1].PN_blocked_cumulative),
'late_KC_g_mean':float(obs[obs.time_ms>=max(obs.time_ms)-100].KC_g_mean.mean()),
'late_APL_inhibition_mean':float(obs[obs.time_ms>=max(obs.time_ms)-100].APL_inhibition_mean.mean())})
details.append(row)
pd.DataFrame(bins).to_parquet(exp/'audit/activity-10ms-bins.parquet',index=False)
df=pd.DataFrame(details); df.to_parquet(exp/'audit/detailed-trials.parquet',index=False)
rows=[]
for (odor,condition),g in df.groupby(['odor_name','condition'],sort=False):
row={'odor':odor,'condition':condition,'n':len(g)}
for k in ('KC_events','target_events','KC_events_after120ms','KC_events_after200ms','KC_events_after500ms','whole_brain_spikes','whole_brain_active','PN_requests','PN_new_spikes','PN_refractory_blocked','late_KC_g_mean','late_APL_inhibition_mean'):
row[k+'_mean']=float(g[k].mean())
rows.append(row)
save(exp/'audit/descriptive-summary.json',{'groups':rows,'analysis':'post-run descriptive summaries, not hypothesis selection or tuning'})
us=pd.read_parquet(exp/'audit/US-acceptance.parquet')
us['scheduled_arrival_slot_ms']=us.first_edge_arrival_ms
us['output_spike_arrival_ms']=us.first_edge_arrival_ms.where(us.actual_output_spike)
us.to_parquet(exp/'audit/US-arrival-semantics.parquet',index=False)
save(exp/'audit/field-semantics.json',{
'PN_request_accounting_partition_exact':True,
'offset_no_external_requests_after120ms':True,
'US_original_first_edge_arrival_ms':'scheduled emission step + frozen delay; not evidence of arrival if force rejected and no actual spike',
'US_output_spike_arrival_ms':'nominal edge delivery time only for actual output spikes; not dopamine release/concentration',
'samples_time_ms':'state after step; (step+1)*dt',
'events_time_ms':'runtime spike convention; step*dt',
'analysis_source_role':'descriptive audit added after simulation preregistration; cannot select parameters or candidate'})
late=[]
for _,r in trials[trials.condition=='offset'].iterrows():
e=pd.read_parquet(exp/'events'/f'{r.odor_index}-{r.seed}-offset.parquet')
late.append((int(r.odor_index),set(map(int,e.loc[e.step>=5000,'neuron_idx']))))
pairs={'within_odor':[],'between_odor':[]}
for i,(oi,a) in enumerate(late):
for oj,b in late[i+1:]:
pairs['within_odor' if oi==oj else 'between_odor'].append(len(a&b)/len(a|b) if a|b else 1.)
save(exp/'audit/offset-late-KC-overlap.json',{
'scope':'posthoc descriptive >=500ms KC activity; no memory/learning gate',
'trials':len(late),'late_active_KC_counts':[len(x[1]) for x in late],
'common_KC_ids':sorted(set.intersection(*(x[1] for x in late))),
'pairwise_Jaccard':{k:{'pairs':len(v),'mean':float(np.mean(v)),'min':float(min(v)),'max':float(max(v))} for k,v in pairs.items()}})
lines=['# Mechanism observations (descriptive, no training)','',
'| Odor | Condition | KC events | target events | KC events >=200ms | late mean KC g |',
'|---|---|---:|---:|---:|---:|']
for r in rows:
if r['condition'] in ('short','sustained','offset'):
lines.append(f"| {r['odor']} | {r['condition']} | {r['KC_events_mean']:.2f} | {r['target_events_mean']:.2f} | {r['KC_events_after200ms_mean']:.2f} | {r['late_KC_g_mean_mean']:.6f} |")
lines += ['', 'The longer trials are diagnostics of the unchanged brain at unchanged input rates. Differences are not evidence of learning.',
'Counter observations add synchronization overhead and cannot be used as runtime speed benchmarks.',
'No baseline-activity input is added. A silent warmup is not a physiological ongoing-state initialization.',
'Turning external PN forcing off does not silence PN neurons or recurrent network activity. Persistent activity is not itself retained associative memory.',
'A scheduled teaching gate is an engineering signal, not measured dopamine release or receptor kinetics.',
'No new US policy or seconds-to-milliseconds conversion is selected from these observations.']
(exp/'audit/MECHANISM.md').write_text('\n'.join(lines)+'\n',encoding='utf-8')
print(json.dumps({'old_trials':len(old),'diagnostic_trials':len(df),'groups':rows},indent=2))