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