Files
digital-drosophila/tools/review_feedback_context.py

152 lines
11 KiB
Python
Raw Permalink 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.
"""Read-only reanalysis of sealed event accounting; no simulation/model mutation."""
from datetime import datetime, timezone
import hashlib
import json
from pathlib import Path
import sys
import numpy as np
import pandas as pd
ROOT=Path('/data/digital-drosophila')
Z=ROOT/'experiments/phase4z-pn-feedback-audit'
AC=ROOT/'experiments/phase4ac-lif-semantics'
OUT=ROOT/'registry/feedback-context-review-20260930'
def sha(path):
h=hashlib.sha256()
with path.open('rb') as f:
for b in iter(lambda:f.read(8<<20),b''):h.update(b)
return h.hexdigest()
def save(path,value):
with path.open('x',encoding='utf-8') as f:json.dump(value,f,indent=2,ensure_ascii=False,allow_nan=False);f.write('\n')
def verify_inputs():
selection={Z:['drive/edge-accounting.parquet','replay/results.parquet','drive/late-ISI-and-g.parquet',
'audit/incoming-source-metadata.parquet','audit/watched-metadata.parquet','regression/trials.json'],
AC:['audit/selected-real-edges.csv','motifs/summary.csv','audit/final-status.json']}
rows=[]
for folder,names in selection.items():
manifest=json.loads((folder/'manifest.json').read_text())
indexed={r['path']:r for r in manifest['files']}
for name in names:
p=folder/name;r=indexed[name]
assert p.is_file() and p.stat().st_size==r['bytes'] and sha(p)==r['sha256'],str(p)
rows.append(dict(path=str(p.relative_to(ROOT)),bytes=r['bytes'],sha256=r['sha256'],
sealed_manifest_sha256=sha(folder/'manifest.json')))
return rows
def analyze():
before=verify_inputs()
if OUT.exists():raise RuntimeError('Refusing existing output')
OUT.mkdir()
save(OUT/'protocol.json',dict(timestamp_utc=datetime.now(timezone.utc).isoformat(),
source=str(Path(__file__).resolve().relative_to(ROOT)),source_sha256=sha(Path(__file__)),
analysis='Post-hoc contextualization of preselected motifs using existing sealed accounting; no new simulations.',
scope='All six archived calibration odors, seed 131101, frozen_reference only; not heldout or learning evidence.',
inputs=before,metric='sum of positive arrived synaptic weights, NOT g trajectory attribution or causal responsibility'))
d=pd.read_parquet(Z/'drive/edge-accounting.parquet')
required={'trial','period','edge_id','source_neuron_idx','target_neuron_idx','raw_synapse_count','effective_weight',
'edge_event_count','positive_arrival_sum','negative_arrival_sum','source_role'}
assert required<=set(d)
d=d[d.trial.str.endswith('-frozen_reference')].copy()
expected={f'{i}-131101-frozen_reference' for i in range(6)}
assert set(d.trial)==expected and not d.duplicated(['trial','period','edge_id']).any()
assert set(d.period)=={'input_on','post_input','late'}
assert np.allclose(d.positive_arrival_sum,d.edge_event_count*np.maximum(d.effective_weight,0),rtol=0,atol=0)
assert np.allclose(d.negative_arrival_sum,d.edge_event_count*np.minimum(d.effective_weight,0),rtol=0,atol=0)
edges=pd.read_csv(AC/'audit/selected-real-edges.csv')
for e in edges.itertuples():
records=d[d.edge_id==e.edge_id]
assert len(records)>0
assert records.source_neuron_idx.eq(e.source_neuron_idx).all()
assert records.target_neuron_idx.eq(e.target_neuron_idx).all()
assert records.raw_synapse_count.eq(e.raw_count).all()
metadata=pd.read_parquet(Z/'audit/incoming-source-metadata.parquet').set_index('neuron_idx')
watch=pd.read_parquet(Z/'audit/watched-metadata.parquet').set_index('neuron_idx')
assert metadata.index.is_unique and watch.index.is_unique
nodes=[1197,1381,1511,82312,133666]
scopes={'pair':{1381,1511},'trio':{1197,1381,1511},'five':set(nodes)}
summaries=[];types=[];sources=[]
for (trial,period,target),g in d[d.target_neuron_idx.isin(nodes)].groupby(['trial','period','target_neuron_idx']):
den=float(g.positive_arrival_sum.sum());neg=float(g.negative_arrival_sum.sum())
assert den>0
per=g.groupby('source_neuron_idx',sort=False).positive_arrival_sum.sum().sort_values(ascending=False,kind='stable')
pos=per[per>0];n80=int(np.searchsorted(pos.cumsum().to_numpy(),.8*den,side='left')+1)
alln=float(g.loc[g.source_role.eq('ALLN'),'positive_arrival_sum'].sum())
alpn=float(g.loc[g.source_role.eq('ALPN'),'positive_arrival_sum'].sum())
for scope,subset in scopes.items():
internal=g.source_neuron_idx.isin(subset)&g.target_neuron_idx.isin(subset)
num=float(g.loc[internal,'positive_arrival_sum'].sum())
summaries.append(dict(trial=trial,period=period,target_neuron_idx=int(target),body_id=int(watch.loc[target,'body_id']),
annotation_type=watch.loc[target,'annotation_type'],scope=scope,target_in_scope=bool(target in subset),
positive_arrival_sum=den,negative_arrival_sum=neg,internal_positive_sum=num,
internal_positive_fraction=num/den,outside_positive_fraction=(den-num)/den,
active_positive_source_neurons=int(len(pos)),sources_for_80_percent=n80,
ALLN_positive_fraction=alln/den,ALPN_positive_fraction=alpn/den))
for src,value in per.items():
m=metadata.loc[src]
sources.append(dict(trial=trial,period=period,target_neuron_idx=int(target),source_neuron_idx=int(src),
source_body_id=int(m.body_id),source_type=m.annotation_type,positive_arrival_sum=float(value),positive_fraction=float(value/den)))
gg=g.assign(source_type=g.source_neuron_idx.map(metadata.annotation_type).fillna('UNKNOWN_TYPE'))
for typ,z in gg.groupby('source_type'):
types.append(dict(trial=trial,period=period,target_neuron_idx=int(target),source_type=str(typ),
positive_arrival_sum=float(z.positive_arrival_sum.sum()),positive_fraction=float(z.positive_arrival_sum.sum()/den),
source_neurons=int(z.source_neuron_idx.nunique())))
summary=pd.DataFrame(summaries);summary.to_csv(OUT/'context-fractions.csv',index=False)
pd.DataFrame(types).to_csv(OUT/'source-type-fractions.csv',index=False)
pd.DataFrame(sources).to_csv(OUT/'source-neuron-fractions.csv',index=False)
rep=pd.read_parquet(Z/'replay/results.parquet')
rep=rep[rep.trial.isin(expected)&rep.neuron_idx.isin(nodes)&rep['mode'].isin(['base','omit_ALLN','omit_ALPN'])].copy()
assert len(rep)==6*5*3
rep.to_csv(OUT/'existing-conditional-replay.csv',index=False)
late=summary[(summary.period=='late')&(summary.scope=='five')]
iso=pd.read_csv(AC/'motifs/summary.csv')
isi=pd.read_parquet(Z/'drive/late-ISI-and-g.parquet')
isi=isi[isi.trial.isin(expected)&isi.neuron_idx.isin(nodes)]
comparison=[]
for node,g in late.groupby('target_neuron_idx'):
rr=rep[rep.neuron_idx==node];ii=isi[isi.neuron_idx==node]
row=dict(neuron_idx=int(node),body_id=int(watch.loc[node,'body_id']),type=str(watch.loc[node,'annotation_type']),
internal_positive_fraction_min=float(g.internal_positive_fraction.min()),
internal_positive_fraction_max=float(g.internal_positive_fraction.max()),
sources_for_80_percent_min=int(g.sources_for_80_percent.min()),sources_for_80_percent_max=int(g.sources_for_80_percent.max()),
positive_sources_min=int(g.active_positive_source_neurons.min()),positive_sources_max=int(g.active_positive_source_neurons.max()),
isolated_five_late_spikes=int(iso[(iso.motif=='five')&(iso.neuron_idx==node)].late_spikes.iloc[0]),
whole_brain_late_rate_min=float(ii.late_rate_hz.min()),whole_brain_late_rate_max=float(ii.late_rate_hz.max()))
for mode in ('base','omit_ALLN','omit_ALPN'):
vals=rr.loc[rr['mode']==mode,'late_spikes']
row[mode+'_late_min']=int(vals.min());row[mode+'_late_max']=int(vals.max())
comparison.append(row)
comp=pd.DataFrame(comparison);comp.to_csv(OUT/'summary.csv',index=False)
lines=['# Feedback motif contextualization — read-only review','',
'This is not a new learning stage, intervention or model. Source: six archived calibration odors, one seed 131101, frozen_reference; all cases retained.',
'Late interval 500–1,000 ms. Fractions measure positive arrived weights, not causal importance, g attribution or net excitation after inhibition/APL.', '',
'| Target idx / type | Five-node internal positive input | Positive sources | Sources needed for 80% | Base late spikes | omit ALLN | omit ALPN |',
'|---|---:|---:|---:|---:|---:|---:|']
for r in comp.itertuples():
lines.append(f'| {r.neuron_idx} / {r.type} | {100*r.internal_positive_fraction_min:.2f}–{100*r.internal_positive_fraction_max:.2f}% | {r.positive_sources_min}–{r.positive_sources_max} | {r.sources_for_80_percent_min}–{r.sources_for_80_percent_max} | {r.base_late_min}–{r.base_late_max} | {r.omit_ALLN_late_min}–{r.omit_ALLN_late_max} | {r.omit_ALPN_late_min}–{r.omit_ALPN_late_max} |')
lines+=['', '## Consequence for the next experiment', '',
'The two-cell loop can sustain activity in isolation, but the full-brain watched ALLNs receive most positive input from outside the five-cell motif. Do not target only this pair as the established unique cause.',
'Existing conditional omissions freeze other recorded events: removing ALLN arrivals still leaves PN/other recorded drive. This establishes conditional sufficiency of residual input, NOT that an ALLN knockout in the live brain would fail.',
'Likewise a large input fraction is not a causal fraction; small inputs can matter near threshold. The VA6->KC edge illustrates why fractions alone are not a deletion target ranking.',
'Future separately authorized model tests should first validate operator semantics, then assess distributed local feedback and preservation of stimulus information, before learning. No gain/threshold/eta/mask tuning is licensed by this table.',
'No new simulations, learning, weights or frozen-file changes. No heldout data used. Usable associative learning remains unachieved.','']
with (OUT/'REPORT.md').open('x',encoding='utf-8') as f:f.write('\n'.join(lines))
after=verify_inputs();assert before==after
save(OUT/'verification.json',dict(pass_all=True,inputs_before_after_identical=True,
inputs_checked=len(before),edge_identity_matches=13,frozen_source_CPU_replay_checks='Previously sealed Phase4Z regressions, not rerun here',
new_simulations=0,learning_achieved=False))
records=[dict(path=str(p.relative_to(OUT)),bytes=p.stat().st_size,sha256=sha(p)) for p in sorted(OUT.iterdir()) if p.is_file()]
save(OUT/'manifest.json',dict(files=records))
with (OUT/'checksums.sha256').open('x') as f:
for r in records:f.write(r['sha256']+' '+r['path']+'\n')
print(comp.to_string(index=False));print('REVIEW SEALED; no new model or simulation')
if __name__=='__main__':analyze()