Files
digital-drosophila/models/male-cns-lif/dpm-hypothesis-v1/phase4y_describe.py
T

107 lines
9.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Descriptive paired effects only; no condition selection or parameter fitting."""
from pathlib import Path
import json
import pandas as pd
import numpy as np
import yaml
from phase4r import save
cfg=yaml.safe_load(Path(__file__).with_name('phase4y.yaml').read_text());exp=Path(cfg['experiment_root'])
assert not (exp/'checksums.sha256').exists(),'Sealed output is immutable'
w=pd.read_parquet(exp/'comparisons/watched-KC.parquet');t=pd.read_parquet(exp/'comparisons/trials.parquet')
p=pd.read_parquet(exp/'comparisons/populations.parquet');x=pd.read_parquet(Path(cfg['prior_root'])/'replay/results.parquet')
inputrows=[]
for _,r in t.iterrows():
obs=pd.read_parquet(exp/'observations'/f'{r.odor_index}-{r.seed}-{r.condition}.parquet').iloc[-1]
inputrows.append({'odor_index':int(r.odor_index),'seed':int(r.seed),'condition':r.condition,
'requested':int(obs.PN_requested_cumulative),'accepted_new_spikes':int(obs.PN_accepted_cumulative),
'preexisting_spikes':int(obs.PN_preexisting_cumulative),'refractory_blocked':int(obs.PN_blocked_cumulative)})
inp=pd.DataFrame(inputrows);assert (inp.groupby(['odor_index','seed']).requested.nunique()==1).all()
assert (inp.requested==inp.accepted_new_spikes+inp.preexisting_spikes+inp.refractory_blocked).all()
inp.to_parquet(exp/'comparisons/input-request-vs-acceptance.parquet',index=False)
save(exp/'audit/input-matching.json',{'scheduled_requests_matched_all_pairs':True,'partition_exact':True,
'pairs_with_changed_accepted_count':int((inp.groupby(['odor_index','seed']).accepted_new_spikes.nunique()>1).sum()),
'interpretation':'identical exogenous requests, not guaranteed identical accepted PN spikes; acceptance depends on endogenous refractory/spike state through the unchanged force interface'})
# Reporting-only schema correction. Preserve original tables and the registered run source.
# These exact superclass values are used by the frozen Phase4C propagation summaries.
meta_path=Path('/data/digital-drosophila/datasets/male-cns-v1.0/processed/neuron-only/neuron-index/unified-metadata.parquet')
sup=pd.read_parquet(meta_path,columns=['neuron_idx','annotation_superclass']).sort_values('neuron_idx')
definitions={'descending':['descending_neuron','descending_neuron_tbc'],'motor':['vnc_motor','cb_motor']}
corrected=[]
for _,r in t.iterrows():
name=f'{r.odor_index}-{r.seed}-{r.condition}'
with np.load(exp/'states'/f'{name}.npz') as z:
for pop,values in definitions.items():
idx=np.flatnonzero(sup.annotation_superclass.isin(values).to_numpy());assert len(idx)>0
c=z['counts'][idx];l=z['late_counts'][idx]
corrected.append({'odor_index':int(r.odor_index),'seed':int(r.seed),'condition':r.condition,'population':pop,
'neurons':len(idx),'total_spikes':int(c.astype(np.int64).sum()),'active':int(np.count_nonzero(c)),
'late_spikes':int(l.astype(np.int64).sum()),'late_active':int(np.count_nonzero(l))})
p=pd.concat([p[~p.population.isin(definitions)],pd.DataFrame(corrected)],ignore_index=True)
p.to_parquet(exp/'comparisons/populations-corrected.parquet',index=False)
save(exp/'audit/population-schema-correction.json',{'scope':'report aggregation only, no dynamics or registered conditions changed',
'issue':'initial literal superclass descending/motor matches zero rows; those zero-population rows are invalid readouts, not evidence of silence',
'definitions_from':'frozen phase4c.py summary definitions, verified against actual metadata',
'corrected_values':definitions,'corrected_counts':{k:int(sup.annotation_superclass.isin(v).sum()) for k,v in definitions.items()},
'source':str(meta_path),'original_tables_preserved':True,'recomputed_from':'saved complete per-neuron total and late counts','simulation_rerun':False})
(exp/'READ-ME-FIRST.md').write_text(
'# Phase 4Y evidence routing\n\n'
'Use audit/MECHANISM.md and comparisons/populations-corrected.parquet for population readouts.\n\n'
'The original comparisons/populations.parquet descending/motor rows used invalid literal superclass names and matched zero neurons. '
'Those rows are preserved for audit but must not be interpreted as no activity. Corrected rows use the frozen Phase4C definitions '
'(descending_neuron/descending_neuron_tbc; vnc_motor/cb_motor) and were recomputed from saved complete counts, without rerunning dynamics. '
'See audit/population-schema-correction.json. Other original population rows are unchanged.\n\n'
'All three hypotheses are reported. None is promoted to a learned or biological baseline.\n',encoding='utf-8')
effects=[];isolated=[]
for (oi,seed,node),g in w.groupby(['odor_index','seed','neuron_idx']):
baseline=int(g.loc[g.condition.eq('frozen_reference'),'late_spikes'].iloc[0])
for _,r in g.iterrows():
effects.append({'odor_index':int(oi),'seed':int(seed),'neuron_idx':int(node),'condition':r.condition,
'baseline_late_spikes':baseline,'condition_late_spikes':int(r.late_spikes),'paired_change':int(r.late_spikes)-baseline})
if seed==cfg['old_replay_seed']:
name=f'{oi}-{seed}-offset';z=x[(x.trial==name)&(x.neuron_idx==node)]
ib=int(z.loc[z['mode'].eq('base'),'late_spikes'].iloc[0]);io=int(z.loc[z['mode'].eq('omit_DPM_annotation'),'late_spikes'].iloc[0])
off=int(g.loc[g.condition.eq('DPM_fast_current_off'),'late_spikes'].iloc[0]);assert ib==baseline
isolated.append({'odor_index':int(oi),'neuron_idx':int(node),'baseline_late_spikes':baseline,
'isolated_DPM_off_late_spikes':io,'whole_brain_DPM_off_late_spikes':off,
'whole_minus_isolated':off-io,'interpretation':'different feedback assumptions; not a numerical regression failure'})
pd.DataFrame(effects).to_parquet(exp/'comparisons/watched-paired-effects.parquet',index=False)
pd.DataFrame(isolated).to_parquet(exp/'comparisons/isolated-vs-whole-brain.parquet',index=False)
new=[];loss=[]
metadata=pd.read_parquet(Path(cfg['prior_root'])/'roles/neuron-evidence-axes.parquet').set_index('neuron_idx')
for (oi,seed),group in t.groupby(['odor_index','seed']):
def late_set(condition):
e=pd.read_parquet(exp/'events'/f'{oi}-{seed}-{condition}.parquet');return set(map(int,e.loc[e.step>=5000,'neuron_idx']))
ref=late_set('frozen_reference')
for condition in ('DPM_fast_current_off','DPM_fast_current_reversed'):
s=late_set(condition)
for label,nodes in [('new',s-ref),('lost',ref-s)]:
for node in sorted(nodes):
row={'odor_index':int(oi),'seed':int(seed),'condition':condition,'change':label,'neuron_idx':node,
'body_id':int(metadata.loc[node,'body_id']),'annotation_type':metadata.loc[node,'annotation_type']}
(new if label=='new' else loss).append(row)
pd.DataFrame(new+loss,columns=['odor_index','seed','condition','change','neuron_idx','body_id','annotation_type']).to_parquet(exp/'comparisons/late-KC-membership-changes.parquet',index=False)
ep=pd.DataFrame(effects);summary=[]
for (node,condition),g in ep.groupby(['neuron_idx','condition']):
summary.append({'neuron_idx':int(node),'condition':condition,'n':len(g),'late_min':int(g.condition_late_spikes.min()),
'late_max':int(g.condition_late_spikes.max()),'change_min':int(g.paired_change.min()),'change_max':int(g.paired_change.max()),
'decreases':int((g.paired_change<0).sum()),'increases':int((g.paired_change>0).sum()),'unchanged':int((g.paired_change==0).sum())})
save(exp/'audit/descriptive-effects.json',{'scope':'post-run descriptive summaries; no promotion or new gate',
'watched':summary,'new_late_KC_memberships':len(new),'lost_late_KC_memberships':len(loss)})
lines=['# Phase 4Y — paired whole-brain effects','',
'All branches are diagnostic hypotheses; none is selected as a better biological model.','',
'| KC | condition | late spike range | paired change range | decreases / unchanged / increases |','|---:|---|---:|---:|---:|']
for r in summary:lines.append(f"| {r['neuron_idx']} | {r['condition']} | {r['late_min']}–{r['late_max']} | {r['change_min']}–{r['change_max']} | {r['decreases']} / {r['unchanged']} / {r['increases']} |")
lines+=['','| KC | isolated DPM-off range | whole-brain DPM-off range (same old seed) |','|---:|---:|---:|']
for node,g in pd.DataFrame(isolated).groupby('neuron_idx'):
lines.append(f'| {node} | {g.isolated_DPM_off_late_spikes.min()}–{g.isolated_DPM_off_late_spikes.max()} | {g.whole_brain_DPM_off_late_spikes.min()}–{g.whole_brain_DPM_off_late_spikes.max()} |')
lines+=['','| population | condition | mean late spikes | mean late active neurons |','|---|---|---:|---:|']
for (pop,condition),g in p.groupby(['population','condition']):
lines.append(f'| {pop} | {condition} | {g.late_spikes.mean():.2f} | {g.late_active.mean():.2f} |')
lines+=['','Whole-brain feedback is allowed to respond; isolated replay holds the other recorded arrivals fixed. A difference between them is the target of the experiment, not grounds for choosing one result.',
'Input matching is at the scheduled request level. Accepted new PN spikes can differ through feedback-dependent refractory state; see input-request-vs-acceptance.parquet. Do not claim identical delivered PN spike trains.',
'Descending/motor rows here use populations-corrected.parquet, reconstructed from saved full counts and verified actual superclass names. The original zero-population rows are retained but invalid; see audit/population-schema-correction.json.',
'No learning, no reward events, no parameter fitting, no promotion; smaller late activity is not itself success. Biological signs and long-term stability remain unproven.']
(exp/'audit/MECHANISM.md').write_text('\n'.join(lines)+'\n',encoding='utf-8')
print('\n'.join(lines[:20]))