90 lines
6.8 KiB
Python
90 lines
6.8 KiB
Python
"""Post-hoc interpretation only; no further trials, selection or parameter changes."""
|
||
from pathlib import Path
|
||
import sys
|
||
import numpy as np
|
||
import pandas as pd
|
||
|
||
ROOT = Path('/data/digital-drosophila'); MODEL = ROOT/'models/male-cns-lif/terminal-scope-diagnostic-v1'
|
||
EXP = ROOT/'experiments/phase4ab-terminal-scope'
|
||
sys.path.insert(0, str(MODEL))
|
||
import diagnostic
|
||
cfg, _, _, _, save, digest, _, _ = diagnostic.load_context()
|
||
assert (EXP/'audit/run-complete.json').exists()
|
||
m = pd.read_parquet(ROOT/cfg['canonical']/'neuron-index/unified-metadata.parquet').sort_values('neuron_idx').reset_index(drop=True)
|
||
trials = pd.read_csv(EXP/'audit/trials.csv'); watched = pd.read_csv(EXP/'audit/watched-counts.csv')
|
||
remaining, comparisons = [], []
|
||
for oi in cfg['odor_indices']:
|
||
names = {c: f'{oi}-{cfg["seed"]}-{c}' for c in cfg['conditions']}
|
||
narrow = pd.read_parquet(EXP/'events'/f'{names["calyx_PN_KC_contact_block"]}.parquet')
|
||
allkc = pd.read_parquet(EXP/'events'/f'{names["all_PN_KC_block"]}.parquet')
|
||
a = np.load(EXP/'states'/f'{names["calyx_PN_KC_contact_block"]}.npz')
|
||
b = np.load(EXP/'states'/f'{names["all_PN_KC_block"]}.npz')
|
||
comparisons.append({'odor_index': oi, 'terminal_vs_allKC_events_exact': narrow.equals(allkc),
|
||
'counts_exact': bool(np.array_equal(a['counts'], b['counts'])),
|
||
'max_V_difference': float(np.abs(a['final_v'].astype(float)-b['final_v']).max()),
|
||
'max_g_difference': float(np.abs(a['final_g'].astype(float)-b['final_g']).max())})
|
||
kc = np.flatnonzero(m.annotation_class.eq('Kenyon_Cell'))
|
||
late = narrow[(narrow.step >= 5000) & narrow.neuron_idx.isin(kc)]
|
||
for node, group in late.groupby('neuron_idx'):
|
||
row = m.iloc[node]
|
||
remaining.append({'odor_index': oi, 'neuron_idx': int(node), 'body_id': int(row.body_id),
|
||
'type': str(row.annotation_type), 'late_spikes': len(group),
|
||
'first_late_spike_ms': float(group.step.min()/10), 'last_spike_ms': float(group.step.max()/10),
|
||
'source_identity_not_inferred_from_this_count': True})
|
||
pd.DataFrame(remaining).to_csv(EXP/'audit/remaining-late-KCs.csv', index=False)
|
||
pd.DataFrame(comparisons).to_csv(EXP/'audit/narrow-vs-allKC.csv', index=False)
|
||
|
||
rows = []
|
||
for name, g in trials.groupby('condition', sort=False):
|
||
row = {'condition': name}
|
||
for key in ['whole_spikes', 'KC_active', 'KC_spikes', 'KC_late_spikes', 'KC82312_late_spikes', 'VA6_late_spikes', 'wall_seconds']:
|
||
row[key+'_min'] = float(g[key].min()); row[key+'_max'] = float(g[key].max())
|
||
rows.append(row)
|
||
pd.DataFrame(rows).to_csv(EXP/'audit/condition-ranges.csv', index=False)
|
||
ca = trials[trials.condition == 'calyx_PN_KC_contact_block']
|
||
wide = trials[trials.condition == 'whole_PN_output_block']
|
||
local = watched[(watched.population == 'ALLN') & watched.trial.str.endswith('calyx_PN_KC_contact_block')]
|
||
result = {'posthoc_summary_not_new_experiment': True,
|
||
'all_six_CA_block_targetKC_late_zero': bool(ca.KC82312_late_spikes.eq(0).all()),
|
||
'all_six_CA_block_VA6_late_persists': bool(ca.VA6_late_spikes.gt(0).all()),
|
||
'all_six_broad_block_KC_and_VA6_late_zero': bool((wide.KC_late_spikes.eq(0) & wide.VA6_late_spikes.eq(0)).all()),
|
||
'CA_block_active_KC_range': [int(ca.KC_active.min()), int(ca.KC_active.max())],
|
||
'CA_block_ALLN_watched_late_rate_range_hz': [float(local.late_rate_hz.min()), float(local.late_rate_hz.max())],
|
||
'CA_vs_allKC_spike_sequence_exact_for_all_odors': bool(all(x['terminal_vs_allKC_events_exact'] for x in comparisons)),
|
||
'not_a_physiological_APL_gain_estimate': True, 'not_a_memory_or_separability_success': True,
|
||
'new_candidate': False, 'source_sha256': digest(Path(__file__))}
|
||
save(EXP/'audit/mechanism-interpretation.json', result)
|
||
|
||
def span(frame, key):
|
||
lo, hi = frame[key].min(), frame[key].max()
|
||
return str(int(lo)) if lo == hi else f'{int(lo)}–{int(hi)}'
|
||
ref = trials[trials.condition == 'frozen_reference']
|
||
lines = ['# Phase 4AB 机制解读', '',
|
||
'正式状态:**COMPLETE-SCOPE-COUNTERFACTUAL / NO-DYNAMICS-CANDIDATE**。', '',
|
||
'这一轮区分了“PN→KC 传入位置”与“维持 PN 放电的上游反馈”。没有修复或批准新的 APL 生理模型,也没有训练。', '',
|
||
'| 诊断范围 | 活跃 KC(6 个旧气味) | 目标 KC 晚期 spikes | VA6 PN 晚期 spikes |',
|
||
'|---|---:|---:|---:|']
|
||
labels = {'frozen_reference': '原模型', 'calyx_PN_KC_contact_block': '仅明确 calyx 接触',
|
||
'all_PN_KC_block': '同批 PN 的全部 KC 输出', 'whole_PN_output_block': '同批 PN 的全部快速输出'}
|
||
for name in cfg['conditions']:
|
||
g = trials[trials.condition == name]
|
||
lines.append(f'| {labels[name]} | {span(g,"KC_active")} | {span(g,"KC82312_late_spikes")} | {span(g,"VA6_late_spikes")} |')
|
||
lines += ['', '晚期窗口是 500–1,000 ms。每个气味沿用 seed 131101;总计 24 组诊断、4 次精确重复,不是 5-seed 学习验证。', '',
|
||
'## 能说明什么', '',
|
||
f'仅 calyx 接触阻断后,目标 KC82312 的晚期 spikes 范围为 {span(ca,"KC82312_late_spikes")},VA6 仍为 {span(ca,"VA6_late_spikes")}。',
|
||
f'同时 KC 总活跃数降为 {span(ca,"KC_active")},原模型为 {span(ref,"KC_active")}。不能把这种接近静默的结果包装成编码或记忆改善。',
|
||
'即使两个局部阻断条件的事件序列相同,其电压/g 仍可能不同;逐气味比较见 narrow-vs-allKC.csv。',
|
||
'宽范围 PN 快速输出阻断影响反馈路径,不等价于局部 calyx 调制;它也没有关闭 PN 胞体或 frozen graded APL 的 PN 输入。', '',
|
||
'## 仍不能说明什么', '',
|
||
'完全阻断只是工程极端对照,不是 APL 的实测释放抑制比例。没有估计 receptor、bouton、局部扩散范围或时间常数。',
|
||
'未使用 held-out 气味,未检验学习、保持、反转或动作输出。不得选取这次“让活动更少”的操作作为正式脑模型。',
|
||
'剩余 late KC 的 ID/type 已单独保存;其来源不能仅凭残余计数猜测。', '',
|
||
'## 下一步边界', '',
|
||
'先继续 upstream feedback / compartment-specific physiology 的证据约束,区分 calyx 信号传递与触角叶持续放电,再确定可检验的局部动力学假设。',
|
||
'不能靠增加整体抑制、调 eta/threshold、扩大 plastic mask 或让 KC 全静默宣称修复。当前不进入学习、游戏或视觉。', '',
|
||
'相关原始研究:[Prisco et al., 2021](https://doi.org/10.7554/eLife.74172)、[Amin et al., 2020](https://doi.org/10.7554/eLife.56954)。它们支持研究局部 APL 作用,不直接验证本实验完全阻断幅度。', '',
|
||
'顶层验收仍为 5/10(50%);本轮推进的是机制定位与可重复诊断能力。最终完整性状态见 regression/final-integrity.json。']
|
||
with (EXP/'INTERPRETATION.zh.md').open('x', encoding='utf-8') as out:
|
||
out.write('\n'.join(lines)+'\n')
|
||
print(result)
|