Files
digital-drosophila/models/male-cns-lif/csus-audit-v1/phase4v_analyze.py
T

94 lines
6.6 KiB
Python

"""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))