Virtual Embryo Challenge更新于 10-03 18:47(北京时间) / 每 5 分钟更新

总览 · ← 返回运行 20261003-105537-search-t2-heart-extrap-g24-D

节点 n12

表达kNN引导坐标置换(T2HX-06,node5复刻):机制已实现并实测,贪心2-opt交换49759对细胞-坐标配对使内部cosine-15NN目标下降,但neighborhood_mmd原始值反升(0.11445→0.1152)、d2_shape变差,总分49.85<地板50,按PLAN停止规则默认关闭(VEC_NSWAPS=0),提交输出逐位等于copy_last。

运行?一次完整的自动搜索或 Agent 会话,有自己的锁定配置和证据包。20261003-105537-search-t2-heart-extrap-g24-D
父节点n3
子节点n14
操作?种子:人写的起点;改进:在父节点上改;草稿:从头写;修复:修父节点的报错。改进
状态已打分
分数搜索目标分 50.00(+0.0) · proxy_noscale 50.00(+0.0)
审查未审查
用时?从运行开始到结束(或到现在)的挂钟时间。33 分
程序版本c6ef840565dd4c89f712e278c2965446610983cd (programs.git)

方法说明?节点程序自带的 METHOD.md:这个程序做了什么、为什么。

来自 programs.git c6ef840565:solution/METHOD.md

表达kNN引导坐标置换(T2HX-06,node5复刻):机制已实现并实测,贪心2-opt交换49759对细胞-坐标配对使内部cosine-15NN目标下降,但neighborhood_mmd原始值反升(0.11445→0.1152)、d2_shape变差,总分49.85<地板50,按PLAN停止规则默认关闭(VEC_NSWAPS=0),提交输出逐位等于copy_last。

方法

  1. 基座与父节点3完全相同:anchor=末观测阶段,n 夹到 [min_cells, max_cells],take 按型分层抽样,X 与坐标原样输出。
  2. 机制层(T2HX-06,VEC_NSWAPS>0 时启用):
    • 表达 kNN:完整表达矩阵 cosine,k=15(与评分 neighborhood_mmd 的 15-NN 对齐),另取 16 邻做候选;
    • 空间 kNN:坐标 euclidean,缓存 pool=60 近邻;
    • 候选对:表达 kNN 中不在空间 15-NN 内的 (i,j),去重后按 seed 洗牌;
    • 贪心 2-opt:交换 i、j 坐标,Δ = 受影响细胞集(i、j、两者的空间近邻)各自"与空间 15-NN 表达的 cosine 距离和"的变化(候选池 = 静态 pool + 显式加入换位的 i/j),Δ<0 接受并交换 pool,无交换次数上限(修正 node10 的预算约束);迭代 3 轮,每轮重算空间 kNN。
    • 输出保证:X 逐位不变,坐标点集(多重集)逐位不变(程序内校验),仅细胞-坐标配对改变。
  3. 机制关闭对照(PLAN mechanism_off_control):VEC_NSWAPS=0(当前默认)跳过全部交换循环。

对照与实测(proxy_noscale,3 次查分)

  • 对照 VEC_NSWAPS=0:50.00,四组全 50,输出 = copy_last。✓(管线无副作用)
  • 全量置换(3 轮,49759 次交换,412s,坐标多重集校验通过):49.85
    • neighborhood_mmd 原始 0.11445 → 0.1152(升高,机制方向失败),local_spatial 49.84;
    • d2_shape 0.04891 → 0.04944(变差),occupancy_dice 0.8148 → 0.8145(差 0.0003 < 0.002,按 PLAN 视为噪声),shape_scale 49.57;
    • 表达两组、cell_state 逐位不变(机制只动配对)。
  • PLAN 停止规则:"若 neighborhood_mmd 原始值未降,停止调参、以对照提交" → 触发,默认 VEC_NSWAPS=0。

机制证据与结论(family T2HX-06)

  • 机制确实生效:改变了 49759 对细胞-坐标配对(约 2×细胞数的配对尝试中接受 ~1.6 万次/轮),内部目标(Σ cosine 距离到空间 15-NN)单调下降。
  • 但内部目标与评分指标不同向:把表达相似的细胞搬成空间邻居会"锐化"预测的邻域平均表达分布,而真值 E9.5 的邻域平均分布并不更锐,MMD 反而变大。这解释了 node5(52.74)的 shape_scale 59.99 更可能来自行序改变导致的评分器子抽样差异(PLAN 风险 1),而非可复现的机制收益——本次实现中 occupancy_dice 变化 < 0.002(噪声),d2_shape 变差。
  • node7/9(Dirichlet 能量目标)与本次(cosine-15NN 目标)两种目标函数都实测低于地板:坐标配对置换家族在无真值方向信息的外推榜上没有可靠收益。

验证过 / 没验证

  • 验证过:对照=50.00 逐位等于 copy_last;全量置换 49.85 及四组分解;坐标多重集不变性(程序内 assert 级校验);确定性(固定 seed 的 rng 洗牌,无全局随机态)。
  • 没验证:同型内限制的置换变体(时间不够);其他 k / 目标函数——但停止规则已触发,且两种目标(本节点与 node7/9)都失败,判断该家族不值得继续。
  • 运行时 ~7s(默认对照路径);机制路径 ~8 分钟,仍在 30 分钟限制内。

知识来源

无外部生物知识;15-NN 对齐来自评分规则简报的公开描述(neighborhood_mmd 定义)。

下一步建议

置换家族两次独立实现(Dirichlet、cosine-15NN)均低于地板,建议放弃 T2HX-06;外推榜唯一未试尽的方向是 expression_change 组:父节点已证明速度外推无信号,剩下的是用 prior(Reactome/CollecTRI)构造符号级方向假设并只做秩一致的保守调整,或直接接受 copy_last 保底。

调研员的计划

名称表达kNN引导坐标置换(修正node7/9/10失败原因的node5复刻)
动机全树唯一超过地板的是node5(52.74,shape_scale 59.99,local_spatial 50.98),机制为表达kNN引导贪心2-opt坐标-细胞配对置换。节点7/9改用坐标15-NN图上Dirichlet能量最小化(不同搜索空间与目标),shape_scale反降至48.33/47.96;节点10加了VEC_SWAP预算上限后退回50.00。父节点3四组均=50.00(地板),本方案针对node5成功而7/9/10失败的具体实现差异做修正复刻。
做法1) 数据加载与copy_last输出同父节点3的run.py(view_io接口,anchor=末观测阶段,n≤max_cells,坐标原样)。2) 表达kNN图:在完整表达矩阵上以cosine距离取k=15近邻(与评分指标neighborhood_mmd的15-NN对齐)。3) 空间kNN图:坐标上euclidean k=15。4) 候选对生成:对每个细胞i,取其表达kNN中不在其空间kNN内的细胞j(表达相似但空间远),构成候选交换对。5) 贪心2-opt:对每个候选对(i,j),计算交换坐标后目标函数变化Δ=Σ(交换后各细胞与其空间15-NN的表达cosine距离之和)−(交换前),Δ<0则接受交换并更新空间邻居索引。6) 迭代2–3轮或至无改善对。不设交换次数上限(修正node10的预算约束问题)。7) 输出:表达矩阵逐位不变,坐标点集(多重集)不变,仅行序(细胞-坐标配对)改变。8) 单输入阶段退路:本方法不依赖第二时间点,单输入即可运行。9) 快速筛选:先跑VEC_NSWAPS=0对照确认=50.00,再跑全量置换,读score_parts中neighborhood_mmd与occupancy_dice原始值。若neighborhood_mmd原始值未降(<0.1126),停止调参、以对照提交。
风险1) shape_scale改善可能部分来自评分器按索引抽样时不同行序导致的子集差异(node7分析已指出),跨种子可能不稳健——Engineer应在第一次查分后对比occupancy_dice原始值,若变化<0.002则视为噪声。2) 贪心搜索若候选对过多(>2M),30分钟内可能跑不完——用numpy向量化计算Δ,限制每轮最多扫描3n个候选对。3) 若表达kNN与目标阶段空间组织不匹配,置换可能轻微恶化neighborhood_mmd——以VEC_NSWAPS=0对照为保底,低于50.00则提交对照。

代码改动?这个节点的程序和父节点程序的逐行差别:绿色是新增,红色是删除。

对比:父节点版本 a0271985a8。改动的文件:solution/METHOD.md +30 −21、solution/run.py +143 −213

diff --git a/solution/METHOD.md b/solution/METHOD.mdindex 6640572..8546109 100644--- a/solution/METHOD.md+++ b/solution/METHOD.md@@ -1,32 +1,41 @@-T2HX-01 位移层:外推时优先用视图内"晚于末输入、不晚于目标"的外部样本按伪批量相关匹配细胞型做阻尼位移(保坐标);两输入速度回退在代理上验证有害、默认关;无可用方向时逐位等于 copy_last。+表达kNN引导坐标置换(T2HX-06,node5复刻):机制已实现并实测,贪心2-opt交换49759对细胞-坐标配对使内部cosine-15NN目标下降,但neighborhood_mmd原始值反升(0.11445→0.1152)、d2_shape变差,总分49.85<地板50,按PLAN停止规则默认关闭(VEC_NSWAPS=0),提交输出逐位等于copy_last。  ## 方法 -在父节点(copy_last)之上加一个位移层,管线(全部只读 manifest 的相对时间与区间字段,无绝对阶段/尺寸常数,伪装视图安全):+1. 基座与父节点3完全相同:anchor=末观测阶段,n 夹到 [min_cells, max_cells],`take` 按型分层抽样,X 与坐标原样输出。+2. 机制层(T2HX-06,`VEC_NSWAPS>0` 时启用):+   - 表达 kNN:完整表达矩阵 cosine,k=15(与评分 neighborhood_mmd 的 15-NN 对齐),另取 16 邻做候选;+   - 空间 kNN:坐标 euclidean,缓存 pool=60 近邻;+   - 候选对:表达 kNN 中不在空间 15-NN 内的 (i,j),去重后按 seed 洗牌;+   - 贪心 2-opt:交换 i、j 坐标,Δ = 受影响细胞集(i、j、两者的空间近邻)各自"与空间 15-NN 表达的 cosine 距离和"的变化(候选池 = 静态 pool + 显式加入换位的 i/j),Δ<0 接受并交换 pool,无交换次数上限(修正 node10 的预算约束);迭代 3 轮,每轮重算空间 kNN。+   - 输出保证:X 逐位不变,坐标点集(多重集)逐位不变(程序内校验),仅细胞-坐标配对改变。+3. 机制关闭对照(PLAN mechanism_off_control):`VEC_NSWAPS=0`(**当前默认**)跳过全部交换循环。 -1. 细胞选择与坐标和父节点完全相同:anchor=最后观测阶段,n≤max_cells 整份输出,超过则按型分层抽样;坐标一律不动(保 neighborhood_mmd 与结构门)。-2. 位移方向按优先级:-   - (a) **外部前向靶**:`manifest.external` 中存在区间 (e_lo, e_hi) 满足 e_lo > 末输入时间且 e_hi ≤ 目标时间的样本时,加载并对齐到面板(<50 共有基因则放弃;X 若像原始计数则重算 log1p(CP10k));anchor 各细胞型与外部型在共有基因伪批量上算 Pearson r,r≥0.5 才配对;每细胞 shift = α·[β·型级差 + (1−β)·(匹配型内最近邻外部细胞 − 本细胞)],α=0.5、β=1(默认)。-   - (b) 两输入速度回退:型级伪批量差 × α_vel × dt_ratio。**默认 α_vel=0(关)**,理由见下。-   - (c) 都没有 → 输出逐位等于 copy_last。-3. shift 逐基因截断在 ±2·std(基因 in anchor),X+shift 后截 0。异常时 (a) 路径 try/except 回退,绝不崩。+## 对照与实测(proxy_noscale,3 次查分) -## 机制对照与验证(proxy_noscale,5 次查分)+- 对照 VEC_NSWAPS=0:**50.00**,四组全 50,输出 = copy_last。✓(管线无副作用)+- 全量置换(3 轮,49759 次交换,412s,坐标多重集校验通过):**49.85**+  - neighborhood_mmd 原始 0.11445 → **0.1152(升高,机制方向失败)**,local_spatial 49.84;+  - d2_shape 0.04891 → 0.04944(变差),occupancy_dice 0.8148 → 0.8145(差 0.0003 < 0.002,按 PLAN 视为噪声),shape_scale 49.57;+  - 表达两组、cell_state 逐位不变(机制只动配对)。+- PLAN 停止规则:"若 neighborhood_mmd 原始值未降,停止调参、以对照提交" → 触发,默认 VEC_NSWAPS=0。 -- **α=0 对照(mechanism_off_control)**:默认配置在代理上的输出与父节点逐位相同(X、坐标 array_equal 验证),vec-score 实测 **50.00**,四组全 50。✓-- **速度回退 (b) 的方向质量**(这是代理上唯一可测的方向):-  - 型级伪批量速度 α_eff=0.45/0.9/1.5:**49.0 / 48.7 / 48.3**。de_score 原始值 −0.056(低于 chance),de_direction ≈ −0.003~−0.007:E8.25→E8.75 的型级速度与真值 E8.75→E9.5 变化**不相关甚至反相关**,且 DE 两项对 α 近似不变(秩/符号指标),所以任何 α 都拿不到 DE 收益,只赔 variogram(skill 0.36–0.44)。与方法卡"α 网格单调低于地板"一致。-  - 去掉增殖模块(GO BP 名字含 cell cycle/mitotic/DNA replication 等的 44 个面板基因置零,通用基因功能注释,来自视图 prior/go)α_eff=0.45:**49.05**。de_direction 转微正(+0.015,skill 0.504)但 de_score 更差(−0.139),variogram 仍赔 0.8 分。-- 结论:代理上速度外推方向无信号,(b) 默认关(α_vel=0);代理行为 = copy_last = 50.00。-- **外部路径 (a) 冒烟测试**(把末输入时间假装提前以触发):加载 qiu E8.75、27883 基因对齐、33 个 anchor 型中 1 个(心脏谱系)r≥0.5 配对成功、shift 逐型非零——机械上可运行;代理视图内没有真正"晚于末输入"的外部样本,故该路径的科学效果**未能在本地验证**(这正是方法卡指出的外推盲区:本地尺子无法检验方向,且外推榜本地分历史上高估官网分)。-- vec-check 对本 board 返回 "not allowed for this client"(客户端限制,非格式问题);vec-score 正常接收并打分同一文件,格式合规。+## 机制证据与结论(family T2HX-06)++- 机制确实生效:改变了 49759 对细胞-坐标配对(约 2×细胞数的配对尝试中接受 ~1.6 万次/轮),内部目标(Σ cosine 距离到空间 15-NN)单调下降。+- 但内部目标与评分指标不同向:把表达相似的细胞搬成空间邻居会"锐化"预测的邻域平均表达分布,而真值 E9.5 的邻域平均分布并不更锐,MMD 反而变大。这解释了 node5(52.74)的 shape_scale 59.99 更可能来自行序改变导致的评分器子抽样差异(PLAN 风险 1),而非可复现的机制收益——本次实现中 occupancy_dice 变化 < 0.002(噪声),d2_shape 变差。+- node7/9(Dirichlet 能量目标)与本次(cosine-15NN 目标)两种目标函数都实测低于地板:坐标配对置换家族在无真值方向信息的外推榜上没有可靠收益。++## 验证过 / 没验证++- 验证过:对照=50.00 逐位等于 copy_last;全量置换 49.85 及四组分解;坐标多重集不变性(程序内 assert 级校验);确定性(固定 seed 的 rng 洗牌,无全局随机态)。+- 没验证:同型内限制的置换变体(时间不够);其他 k / 目标函数——但停止规则已触发,且两种目标(本节点与 node7/9)都失败,判断该家族不值得继续。+- 运行时 ~7s(默认对照路径);机制路径 ~8 分钟,仍在 30 分钟限制内。  ## 知识来源 -- GO BP 注释(视图 prior/go/gene_sets_bp.gmt):增殖模块(细胞周期/有丝分裂/DNA 复制)是瞬态程序,其早期轨迹不可靠外推——通用基因功能知识,非任何保留阶段测量。-- 细胞型配对用表达相关而非标签名(外部数据标签词汇可能不同)——PLAN T2HX-01 规定。+无外部生物知识;15-NN 对齐来自评分规则简报的公开描述(neighborhood_mmd 定义)。 -## 已知弱点 / 未验证+## 下一步建议 -- (a) 路径在真实 final 视图(可能挂 MOSTA E9.5 等前向外部样本)上的效果无法在本地检验;若方向错会低于地板。终选护栏(proxy_noscale 种子均值需超 copy_last 1 分)在本节点上不会通过,正式提交会回落到基线预测。-- 未做:STRING/通路平滑速度、t 统计加权、逐细胞 OT 位移(方法卡已证 OT 伤邻域)。+置换家族两次独立实现(Dirichlet、cosine-15NN)均低于地板,建议放弃 T2HX-06;外推榜唯一未试尽的方向是 expression_change 组:父节点已证明速度外推无信号,剩下的是用 prior(Reactome/CollecTRI)构造符号级方向假设并只做秩一致的保守调整,或直接接受 copy_last 保底。diff --git a/solution/run.py b/solution/run.pyindex 196e6b2..e610b14 100644--- a/solution/run.py+++ b/solution/run.py@@ -1,22 +1,17 @@ #!/usr/bin/env python3-"""T2 extrapolation: expression-matched pseudobulk displacement on the anchor stage.--Mechanism (family T2HX-01): shift each anchor-stage cell's expression toward a-forward target while keeping coordinates untouched (preserves local spatial-pairing). Target priority, computed only from view data and relative times:--  (a) an external sample whose whole stage interval lies strictly after the-      last input and at/before the target: per-cell type of the anchor is-      matched to external types by pseudobulk Pearson correlation on shared-      panel genes (r >= 0.5); shift blends the matched type-level difference-      (weight beta) with a nearest-neighbour cell-level difference (1 - beta).-  (b) else with >= 2 inputs: per-type pseudobulk velocity between the last two-      inputs, extrapolated over (target - last) time; type-level shift.-  (c) else: copy_last exactly (alpha also forces this when set to 0).--Shifts are per-gene capped at cap * std(gene in anchor), values clipped at 0.-No absolute stage names, times or sizes are hardcoded; only relative time-differences from the manifest are used, so the program is view-independent.+"""T2 extrapolation: expression-kNN-guided greedy 2-opt coordinate permutation.++Mechanism (family T2HX-06, replication of node 5): on the anchor (last observed)+stage output, find pairs of cells that are similar in expression (cosine kNN,+k=15, aligned with the neighborhood_mmd metric's 15-NN definition) but not+already spatial neighbours, and greedily swap their coordinates when the swap+reduces the total cosine distance between every cell's expression and its+spatial 15-NN expressions. The expression matrix is bitwise unchanged and the+coordinate point multiset is unchanged; only the cell-coordinate pairing+changes.++VEC_NSWAPS=0 disables all swaps (mechanism-off control: output bitwise equals+copy_last). No absolute stage names, times or sizes are used; view-independent. """  from __future__ import annotations@@ -24,158 +19,25 @@ from __future__ import annotations import argparse import os import sys+import time  import numpy as np  from src.task2_spatial.sample import take from src.task2_spatial.view_io import (     anchor_entry,-    extrap_step,     load_manifest,     panel_genes,     read_stage,-    target_time,     write_t2, ) -ALPHA = float(os.environ.get("VEC_ALPHA", "0.5"))  # external-forward path-ALPHA_VEL = float(os.environ.get("VEC_ALPHA_VEL", "0.0"))  # two-input velocity fallback (proxy-validated: harmful, kept off)-BETA = float(os.environ.get("VEC_BETA", "1.0"))-CAP = float(os.environ.get("VEC_CAP", "2.0"))-TIME_SCALE = bool(int(os.environ.get("VEC_TS", "1")))-CC_ZERO = bool(int(os.environ.get("VEC_CC", "0")))-MATCH_R = 0.5-MIN_SHARED = 50--_CC_KEYWORDS = ("cell cycle", "mitotic", "meiotic", "dna replication", "chromosome segregation", "cytokinesis")---def _cell_cycle_genes(view: str) -> set[str]:-    """Genes in GO BP terms whose name indicates proliferation programs (generic-    gene-function annotation from the view's prior/; transient modules whose-    early trajectory is not trusted for forward extrapolation)."""-    out: set[str] = set()-    try:-        path = os.path.join(view, "prior", "go", "gene_sets_bp.gmt")-        with open(path) as fh:-            for line in fh:-                parts = line.rstrip("\n").split("\t")-                if len(parts) < 3:-                    continue-                name = parts[1].lower()-                if any(k in name for k in _CC_KEYWORDS):-                    out.update(parts[2:])-    except OSError:-        pass-    return out---def _pb(X: np.ndarray, labels: np.ndarray) -> tuple[list[str], np.ndarray]:-    types = sorted(set(labels.tolist()))-    out = np.zeros((len(types), X.shape[1]), dtype=np.float64)-    for k, t in enumerate(types):-        m = labels == t-        if m.any():-            out[k] = X[m].mean(axis=0)-    return types, out---def _external_forward_entry(manifest: dict, last_time: float, tt: float) -> dict | None:-    for e in manifest.get("external") or []:-        try:-            lo = float(e.get("e_lo", -np.inf))-            hi = float(e.get("e_hi", lo))-        except (TypeError, ValueError):-            continue-        if lo > last_time + 1e-6 and hi <= tt + 1e-6:-            return e-    return None---def _load_external(view: str, entry: dict, genes: list[str]):-    import anndata as ad-    from scipy import sparse--    ad_obj = ad.read_h5ad(os.path.join(view, entry["path"]))-    ext_genes = [str(g) for g in ad_obj.var_names]-    pos = {g: i for i, g in enumerate(ext_genes)}-    shared = [g for g in genes if g in pos]-    if len(shared) < MIN_SHARED:-        return None, None-    cols = np.array([pos[g] for g in shared])-    X = ad_obj.X-    X = X.toarray() if sparse.issparse(X) else np.asarray(X)-    X = np.nan_to_num(np.asarray(X, dtype=np.float64))-    if "log1p" not in ad_obj.uns and X.max() > 30.0:  # looks like raw counts-        tot = X.sum(axis=1, keepdims=True)-        tot[tot <= 0] = 1.0-        X = np.log1p(X / tot * 10000.0)-    X = X[:, cols]-    labels = (-        np.asarray(ad_obj.obs["celltype"]).astype(str)-        if "celltype" in ad_obj.obs-        else np.array(["ext"] * X.shape[0])-    )-    return shared, (X, labels)---def _direction_external(view, manifest, genes, X_a, lab_a, pb_a, last_time, tt):-    entry = _external_forward_entry(manifest, last_time, tt)-    if entry is None:-        return None-    try:-        shared, loaded = _load_external(view, entry, genes)-        if loaded is None:-            return None-        X_e, lab_e = loaded-        gidx = np.array([genes.index(g) for g in shared])-        types_e, pb_e = _pb(X_e, lab_e)-        # match anchor types to external types by pseudobulk Pearson r on shared genes-        A = pb_a[:, gidx]-        B = pb_e-        A = A - A.mean(axis=1, keepdims=True)-        B = B - B.mean(axis=1, keepdims=True)-        na = np.linalg.norm(A, axis=1, keepdims=True)-        nb = np.linalg.norm(B, axis=1, keepdims=True)-        na[na == 0] = 1.0-        nb[nb == 0] = 1.0-        C = (A / na) @ (B / nb).T-        best = C.argmax(axis=1)-        ok = C[np.arange(len(best)), best] >= MATCH_R-        print(f"[diag] external target {entry.get('id')}: matched {ok.sum()}/{len(ok)} anchor types", file=sys.stderr)-        if not ok.any():-            return None-        # type-level difference on full panel (external genes only)-        diff_t = np.zeros_like(pb_a)-        for k in range(len(ok)):-            if ok[k]:-                diff_t[k, gidx] = pb_e[best[k]] - pb_a[k, gidx]-        # cell-level: nearest external neighbour within matched type-        from sklearn.neighbors import NearestNeighbors--        shift = np.zeros_like(X_a)-        d_sub = X_a[:, gidx]-        types_a = sorted(set(lab_a.tolist()))-        for k in range(len(ok)):-            if not ok[k]:-                continue-            m = lab_a == types_a[k]-            if not m.any():-                continue-            tgt_m = lab_e == types_e[best[k]]-            if not tgt_m.any():-                continue-            nn = NearestNeighbors(n_neighbors=1).fit(X_e[tgt_m][:, : len(shared)])-            _, ind = nn.kneighbors(d_sub[m])-            cell_diff = np.zeros((m.sum(), X_a.shape[1]))-            cell_diff[:, gidx] = X_e[tgt_m][ind[:, 0]] - d_sub[m]-            type_diff = np.zeros((m.sum(), X_a.shape[1]))-            type_diff[:, gidx] = diff_t[k, gidx]-            shift[m] = BETA * type_diff + (1.0 - BETA) * cell_diff-        return shift-    except Exception as exc:  # never crash on the optional external path-        print(f"[diag] external path failed: {exc}", file=sys.stderr)-        return None+KNN_K = 15           # spatial neighbourhood size (matches metric definition)+POOL = 60            # cached spatial neighbours per cell for delta evaluation+EXPR_K = 16          # expression kNN incl. self+ROUNDS = int(os.environ.get("VEC_ROUNDS", "3"))+NSWAPS = int(os.environ.get("VEC_NSWAPS", "0"))  # proxy test: permutation harmful (49.85 < 50), default off per PLAN stop-rule+EPS = 1e-9   def main() -> None:@@ -187,8 +49,6 @@ def main() -> None:      manifest = load_manifest(args.data)     genes = panel_genes(args.data, manifest)-    prev, last, dt_ratio = extrap_step(manifest)-    tt = target_time(manifest)     stage = read_stage(args.data, anchor_entry(manifest), genes)     n = int(np.clip(stage.n, manifest["min_cells"], manifest["max_cells"]))     rng = np.random.default_rng(args.seed)@@ -198,60 +58,130 @@ def main() -> None:         rows = np.sort(rng.choice(stage.n, size=n, replace=True))      X = stage.X[rows].toarray().astype(np.float32)-    coords = stage.coords[rows]+    coords = np.ascontiguousarray(stage.coords[rows], dtype=np.float64)[:, :3]+    n_cells = X.shape[0] -    if ALPHA > 0.0 or ALPHA_VEL > 0.0:-        lab = stage.labels[rows]-        types, pb_a = _pb(X.astype(np.float64), lab)-        shift = None-        src = "none"-        if ALPHA > 0.0:-            shift = _direction_external(-                args.data, manifest, genes, X.astype(np.float64), lab, pb_a, float(last["time"]), tt-            )-            if shift is not None:-                shift *= ALPHA-                src = "external"-        if shift is None and prev is not None and ALPHA_VEL > 0.0:-            src = "velocity"-            pstage = read_stage(args.data, prev, genes)-            Xp = pstage.X.toarray().astype(np.float64)-            _, pb_p = _pb(Xp, pstage.labels)-            tmap_p = {t: i for i, t in enumerate(_pb(Xp, pstage.labels)[0])}-            glob = pb_a.mean(axis=0) - Xp.mean(axis=0)-            v = np.tile(glob, (len(types), 1))-            for k, t in enumerate(types):-                if t in tmap_p:-                    v[k] = pb_a[k] - pb_p[tmap_p[t]]-            scale = ALPHA_VEL * (dt_ratio if (TIME_SCALE and dt_ratio) else 1.0)-            per_type = {t: scale * v[k] for k, t in enumerate(types)}-            shift = np.zeros_like(X, dtype=np.float64)-            for t in types:-                m = lab == t-                if m.any():-                    shift[m] = per_type[t]-            del Xp-        if shift is None:-            src = "copy_last(no usable forward direction)"-        if shift is not None:-            if CC_ZERO:-                cc = _cell_cycle_genes(args.data)-                idx = [i for i, g in enumerate(genes) if g in cc]-                if idx:-                    shift[:, idx] = 0.0-                print(f"[diag] cc-zero: {len(idx)} genes zeroed", file=sys.stderr)-            sd = X.astype(np.float64).std(axis=0)-            capv = CAP * np.maximum(sd, 1e-3)-            shift = np.clip(shift, -capv, capv)-            X = np.clip(X.astype(np.float64) + shift, 0.0, None).astype(np.float32)-            norms = np.linalg.norm(shift, axis=1)-            print(f"[diag] src={src} shift norm mean/p50/p95: {norms.mean():.3f} "-                  f"{np.percentile(norms,50):.3f} {np.percentile(norms,95):.3f}", file=sys.stderr)-            gmag = np.abs(shift).mean(axis=0)-            top = np.argsort(-gmag)[:20]-            print("[diag] top shifted genes: " + ", ".join(f"{genes[i]}({gmag[i]:.2f})" for i in top), file=sys.stderr)-        else:-            print(f"[diag] src={src}: no shift applied", file=sys.stderr)+    if NSWAPS > 0 and n_cells > KNN_K + 2:+        t0 = time.time()+        from sklearn.neighbors import NearestNeighbors++        nn_e = NearestNeighbors(n_neighbors=min(EXPR_K, n_cells), metric="cosine").fit(X)+        _, eidx = nn_e.kneighbors(X)++        Xn = X.astype(np.float64)+        nrm = np.linalg.norm(Xn, axis=1, keepdims=True)+        nrm[nrm == 0] = 1.0+        Xn = Xn / nrm+        G = Xn @ Xn.T  # cosine similarity+        np.fill_diagonal(G, -1.0)+        del Xn++        P = coords.copy()+        pool = np.zeros((n_cells, POOL), dtype=np.int64)+        nswaps = 0+        k = min(KNN_K, n_cells - 1)++        for rd in range(ROUNDS):+            nn_s = NearestNeighbors(n_neighbors=min(POOL, n_cells)).fit(P)+            _, sidx = nn_s.kneighbors(P)+            pool[:, : sidx.shape[1]] = sidx+            snbr_set = [set(r[:KNN_K].tolist()) for r in sidx]++            # candidate pairs: expression-similar but not already spatial neighbours+            seen = set()+            cands = []+            for i in range(n_cells):+                row_e = eidx[i]+                row_s = snbr_set[i]+                for jj in row_e:+                    j = int(jj)+                    if j == i or j in row_s:+                        continue+                    key = (i, j) if i < j else (j, i)+                    if key not in seen:+                        seen.add(key)+                        cands.append(key)+            cands = np.array(cands, dtype=np.int64)+            rng.shuffle(cands)+            print(f"[diag] round {rd}: {len(cands)} candidate pairs, setup {time.time()-t0:.1f}s", file=sys.stderr)++            ar_n = np.arange(n_cells, dtype=np.int64)+            acc_this_round = 0+            for pair in cands:+                if nswaps >= NSWAPS:+                    break+                i = int(pair[0])+                j = int(pair[1])+                pi = P[i].copy()+                pj = P[j].copy()+                si = pool[i].copy()+                sj = pool[j].copy()+                A = np.union1d(si, sj)+                A = A[(A != i) & (A != j)]+                m = A.shape[0]++                # ---- cost for i and j themselves (candidates = partner's pool) ----+                def _self_cost(pos, Cp, Gx):+                    Cp = Cp[(Cp != i) & (Cp != j)]+                    d = np.sqrt(((P[Cp] - pos) ** 2).sum(1))+                    s = Gx[Cp]+                    d = np.concatenate([[0.0], d])          # self, distance 0+                    s = np.concatenate([[1.0], s])          # self, similarity 1+                    kk = min(k, d.shape[0])+                    sel = np.argpartition(d, kk - 1)[:kk]+                    return float(kk - s[sel].sum())++                pre_i = _self_cost(pi, si, G[i])+                pre_j = _self_cost(pj, sj, G[j])+                post_i = _self_cost(pj, sj, G[i])+                post_j = _self_cost(pi, si, G[j])++                # ---- cost for affected bystanders A (positions unchanged) ----+                PA = P[A]+                CA = pool[A]                                   # m x POOL+                mask = (CA == i) | (CA == j) | (CA == A[:, None])+                Dp = np.sqrt(((P[CA] - PA[:, None, :]) ** 2).sum(2))+                Dp[mask] = np.inf+                Sp = G[A[:, None], CA]+                Sp[mask] = -1.0+                dAi = np.sqrt(((PA - pi) ** 2).sum(1))+                dAj = np.sqrt(((PA - pj) ** 2).sum(1))+                sAi = G[A, i][:, None]+                sAj = G[A, j][:, None]+                d0 = np.zeros((m, 1))+                s1 = np.ones((m, 1))++                def _bystander(di_col, dj_col):+                    D = np.hstack([Dp, di_col, dj_col, d0])+                    S = np.hstack([Sp, sAi, sAj, s1])+                    kk = min(k, D.shape[1])+                    sel = np.argpartition(D, kk - 1, axis=1)[:, :kk]+                    return float((kk - S[np.arange(m)[:, None], sel].sum(1)).sum())++                pre_A = _bystander(dAi[:, None], dAj[:, None])+                post_A = _bystander(dAj[:, None], dAi[:, None])++                delta = (post_i + post_j + post_A) - (pre_i + pre_j + pre_A)+                if delta < -EPS:+                    P[i] = pj+                    P[j] = pi+                    pool[i] = sj+                    pool[j] = si+                    nswaps += 1+                    acc_this_round += 1++            print(f"[diag] round {rd}: accepted {acc_this_round}, total {nswaps}, {time.time()-t0:.1f}s", file=sys.stderr)+            if acc_this_round == 0:+                break++        coords = np.ascontiguousarray(P, dtype=np.float64)+        src_coords = np.ascontiguousarray(stage.coords[rows], dtype=np.float64)[:, :3]+        for c in range(3):+            if not np.array_equal(np.sort(coords[:, c]), np.sort(src_coords[:, c])):+                print("[diag] WARNING: coordinate multiset changed!", file=sys.stderr)+        print(f"[diag] permutation done: {nswaps} swaps, {time.time()-t0:.1f}s", file=sys.stderr)+    else:+        print("[diag] control (NSWAPS=0): output = copy_last", file=sys.stderr)      write_t2(args.out, X, coords, genes, seed=args.seed) 

调研来源?调研员查到并用到的知识条目和文献检索结果(只列标题和编号)。

用到的知识库条目

编号标题出处
k007Interval staging and held-out-window filtering of external datanotes/official/来件/virtualembryo.ai/rules.md
k016Degenerate-solution checks for population predictionsnotes/handover/02_知识学习路线.md
k024World-model evaluation dimensions for state-transition predictorsnotes/competition/07_biomedical_world_models.md

分析结果?分析员写的 ANALYSIS.json:改了什么、各组分数怎么变、假设是否成立、经验和下一步建议。

改了什么把父节点3的 T2HX-01 位移层整体删掉,改为实现 T2HX-06:表达 cosine-15NN 引导的贪心 2-opt 细胞-坐标配对置换(X 与坐标点集不变,仅改行序配对)。本地实测全量置换(49759 次交换)得 49.85、neighborhood_mmd 原始值 0.11445→0.1152 上升,触发 PLAN 停止规则,故默认 VEC_NSWAPS=0 关闭机制,提交输出逐位等于 copy_last——变化量表八项原始值与父节点完全相同(de_score/de_direction 0、mmd_u 0.05832、variogram 0.05806、d2_shape 0.04911、occupancy_dice 0.8066、scale_log_ratio -0.4334、neighborhood_mmd 0.1126),榜分 50.00 = 地板。
各组分数的变化cell_state:噪声内(严格为 0):mmd_u 0.05832、variogram 0.05806,skill 均 0.500、各 6.25 分,与父节点相同。
expression_change:噪声内(严格为 0):de_score 与 de_direction 原始值 0、skill 0.500、各 6.25 分,与父节点逐位相同。
local_spatial:噪声内(严格为 0):neighborhood_mmd 原始值 0.1126、12.50 分,与父节点相同。机制开启态本地实测反而恶化(0.11445→0.1152,组 49.84),故被关闭。
shape_scale:噪声内(严格为 0):d2_shape 0.04911、occupancy_dice 0.8066、scale_log_ratio -0.4334,各 4.17 分,与父节点相同。Engineer 报告的机制开启态 shape_scale 49.57(d2_shape 0.04891→0.04944 变差、occupancy_dice 变化 0.0003)未进入提交。
family_idT2HX-06
假设是否成立否
经验
  1. 在无真值方向信息的外推榜上,坐标-细胞配对置换家族(T2HX-06)两次独立目标函数均低于地板:Dirichlet 能量最小化(node7/9,shape_scale 48.33/47.96)与 cosine-15NN 距离最小化(本节点,总分 49.85、neighborhood_mmd 0.11445→0.1152),说明该家族应放弃。
  2. 把表达相似的细胞搬成空间邻居会'锐化'预测的邻域平均表达分布,而真值阶段的邻域平均分布并不更锐,所以 neighborhood_mmd 上升——降低 neighborhood_mmd 不等于最大化表达-空间一致性,内部目标函数与评分指标可以反向。
  3. 只改行序(不改表达值与坐标点集)得到的形状分不可信:node5 的 52.74(shape_scale 59.99)在本次复刻中 occupancy_dice 变化仅 0.0003、d2_shape 变差,符合'评分器按索引子抽样时行序不同导致子集差异'的解释,属抽样运气而非机制收益。
  4. 先跑关闭机制对照(VEC_NSWAPS=0)并验证输出逐位等于 copy_last,是有效的保底设计:机制实测有害时可直接以对照提交,本节点因此保住 50.00 而非交出 49.85。
  5. 写'原始值未降即停止调参'这类可判定的停止规则很有用:它在一次查分后就切断了继续调 k/轮数的诱惑,避免把预算花在已失败的方向上。
  6. 就地修改从视图读出的 coords 需要显式 copy(本节点修了一个坐标视图复制 bug),否则机制路径会污染基线输出,使对照失去意义。
mechanism_active否
下一步建议
  1. 不要再在 local_spatial / shape_scale 上做坐标或配对层面的重排(含同型内置换、其他 k、其他目标函数):两种目标已实测低于地板,收益上限被结构门限制而下行风险大。
  2. 针对 expression_change 组:用 prior(Reactome / CollecTRI)构造符号级方向假设,只做与假设秩一致的保守调整(不改幅度),α 网格 {0.25, 0.5},逐基因截断在 ±1·std(gene in anchor)。
  3. 外推榜本地尺子历史上高估官网分(心脏外推 本地 54.2 → 官网 49.6),任何本地领先 <1 分(T2 噪声量级)都不算收益:新方向需本地多种子均值超 copy_last ≥1 分才提交,否则一律回落 copy_last。
  4. 若 expression_change 也无信号,直接把预算转向检查 scale_log_ratio(本地原始 -0.4334,是唯一看绝对尺寸的项):确认输出坐标单位与官方一致、RMS 半径是否可通过整体等比缩放靠近真值,这不依赖方向信息且对 neighborhood_mmd 的相对邻域结构影响小(需验证缩放是否改变 15-NN 组成)。

对话摘要?每个角色和大模型对话的统计:轮数、工具调用、用时、token 数和最后的回答摘录;原始记录只给路径。

只给统计和最后回答的摘录;完整对话请到原始记录位置里列出的文件看。

分析员

角色?调研员写计划、工程师改代码、分析员解读分数、审查员检查作弊。分析员 alibaba-token-plan-cn/qwen3.8-max
调用次数1 次
轮数?大模型一共回复了几轮(每轮可以调用多个工具)。4
工具调用?大模型调用读文件、执行命令等工具的次数,按工具名统计。共 3 次:bash 2、write 1
用时不到 1 分
token 数?输入 / 输出(含思考)的 token 数,来自接口返回的用量。输入 18,054 · 输出 1,506 · 思考 992
任务(第一行)分析节点 n12 的打分结果,写 ANALYSIS.json
最后的回答(摘录)
ANALYSIS.json written
原始记录位置?完整对话记录在 Spark 上的文件路径;看板不复制原始内容。/home/spark-longxinyang/vec/runs/formal/20261003-105537-search-t2-heart-extrap-g24-D/nodes/12/analyst.jsonl 15 KB
/home/spark-longxinyang/vec/runs/formal/20261003-105537-search-t2-heart-extrap-g24-D/nodes/12/analyst.stderr

工程师

角色工程师 alibaba-token-plan-cn/qwen3.8-max
调用次数1 次
轮数17
工具调用共 19 次:bash 12、write 3、edit 3、read 1
用时25 分
token 数输入 53,618 · 输出 8,877 · 思考 11,619
任务(第一行)按计划修改程序:表达kNN引导坐标置换(修正node7/9/10失败原因的node5复刻)
最后的回答(摘录)
**做了什么**:按 PLAN 实现 T2HX-06 表达kNN引导坐标置换(node5 复刻,修正 node7/9/10 的失败点:cosine-15NN 目标、无交换预算上限、Δ评估显式处理换位细胞),并修复了一个坐标视图复制 bug。

**查分结果**(3/20 次):对照 VEC_NSWAPS=0 = **50.00**(逐位等于 copy_last,已验证);全量置换(49759 次交换)= **49.85**,neighborhood_mmd 原始值反升(0.11445→0.1152),d2_shape 变差,occupancy_dice 变化 <0.002(噪声)。PLAN 停止规则触发,默认关闭机制,提交输出 = copy_last = 50.00。

**下一步建议**:置换家族两种独立目标(Dirichlet 于 node7/9、cosine-15NN 于本节点)均低于地板,node5 的 52.74 大概率是行序子抽样运气,建议放弃 T2HX-06;剩余未试尽方向是用 prior(Reactome/CollecTRI)做符号级保守表达调整攻 expression_change 组。
原始记录位置/home/spark-longxinyang/vec/runs/formal/20261003-105537-search-t2-heart-extrap-g24-D/nodes/12/engineer.jsonl 103 KB
/home/spark-longxinyang/vec/runs/formal/20261003-105537-search-t2-heart-extrap-g24-D/nodes/12/engineer.stderr

调研员

角色调研员 alibaba-token-plan-cn/qwen3.8-max
调用次数1 次
轮数1
工具调用共 0 次:—
用时6 分
token 数输入 15,839 · 输出 9,446 · 思考 8,192
任务(第一行)为节点 n12(改进,父节点 n3)写改进计划 PLAN.json
最后的回答(摘录)
different objective (Dirichlet energy on coordinate-NN graph); node 10 failed by adding artificial swap budget constraints. I'll propose a faithful replication with corrections.

```json
{
  "name": "表达kNN引导坐标置换(修正node7/9/10失败原因的node5复刻)",
  "motivation": "全树唯一超过地板的是node5(52.74,shape_scale 59.99,local_spatial 50.98),机制为表达kNN引导贪心2-opt坐标-细胞配对置换。节点7/9改用坐标15-NN图上Dirichlet能量最小化(不同搜索空间与目标),shape_scale反降至48.33/47.96;节点10加了VEC_SWAP预算上限后退回50.00。父节点3四组均=50.00(地板),本方案针对node5成功而7/9/10失败的具体实现差异做修正复刻。",
  "approach": "1) 数据加载与copy_last输出同父节点3的run.py(view_io接口,anchor=末观测阶段,n≤max_cells,坐标原样)。2) 表达kNN图:在完整表达矩阵上以cosine距离取k=15近邻(与评分指标neighborhood_mmd的15-NN对齐)。3) 空间kNN图:坐标上euclidean k=15。4) 候选对生成:对每个细胞i,取其表达kNN中不在其空间kNN内的细胞j(表达相似但空间远),构成候选交换对。5) 贪心2-opt:对每个候选对(i,j),计算交换坐标后目标函数变化Δ=Σ(交换后各细胞与其空间15-NN的表达cosine距离之和)−(交换前),Δ<0则接受交换并更新空间邻居索引。6) 迭代2–3轮或至无改善对。不设交换次数上限(修正node10的预算约束问题)。7) 输出:表达矩阵逐位不变,坐标点集(多重集)不变,仅行序(细胞-坐标配对)改变。8) 单输入阶段退路:本方法不依赖第二时间点,单输入即可运行。9) 快速筛选:先跑VEC_NSWAPS=0对照确认=50.00,再跑全量置换,读score_parts中neighborhood_mmd与occupancy_dice原始值。若neighborhood_mmd原始值未降(<0.1126),停止调参、以对照提交。",
  "expected_groups": ["local_spatial", "shape_scale"],
  "risks": "1) shape_scale改善可能部分来自评分器按索引抽样时不同行序导致的子集差异(node7分析已指出),跨种子可能不稳健——Engineer应在第一次查分后对比occupancy_dice原始值,若变化<0.002则视为噪声。2) 贪心搜索若候选对过多(>2M),30分钟内可能跑不完——用numpy向量化计算Δ,限制每轮最多扫描3n个候选对。3) 若表达kNN与目标阶段空间组织不匹配,置换可能轻微恶化neighborhood_mmd——以VEC_NSWAPS=0对照为保底,低于50.00则提交对照。",
  "family_id": "T2HX-06",
  "mechanism": "在表达空间找相似但空间上远的细胞对,贪心交换其坐标使表达相似细胞成为空间邻居,从而改善表达-位置配对(降低neighborhood_mmd),只重排配对、不改表达矩阵与坐标点集。",
  "vs_constant_shift": "常数位移对每个细胞型加同一个表达向量,不改变细胞间相对关系也不动坐标;本方法不改表达值,只重排细胞与坐标的配对,直接优化表达-空间一致性,是组合/配对层面的操作而非表达幅度平移。",
  "mechanism_evidence": "1) neighborhood_mmd原始值从0.1126下降(目标≤0.110);2) 实际交换细胞数>0且交换后表达kNN与空间kNN的重叠率上升(诊断输出);3) score_parts中local_spatial组得分>12.50;4) 若shape_scale也改善,读occupancy_dice原始值变化方向。",
  "mechanism_off_control": "设环境变量VEC_NSWAPS=0,跳过全部交换循环,输出应与copy_last逐位相同(array_equal验证),vec-score应返回50.00、四组均50。若对照≠50.00说明管线有副作用,需先修复。",
  "sources": []
}
```
原始记录位置/home/spark-longxinyang/vec/runs/formal/20261003-105537-search-t2-heart-extrap-g24-D/nodes/12/researcher.jsonl 38 KB
/home/spark-longxinyang/vec/runs/formal/20261003-105537-search-t2-heart-extrap-g24-D/nodes/12/researcher.stderr