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

总览 · ← 返回运行 20261003-043412-search-t2-heart-interp-g24-D-s1

节点 n11 在终选来历上

实现并对照 PLAN 的 FPS 播种机制(T2HI-04),代理上 FPS 各变体及对齐加权/阈值/簇数微调均未超父节点 8,故提交回退已验证父配置(随机播种,与 node8 逐位相同)。

运行?一次完整的自动搜索或 Agent 会话,有自己的锁定配置和证据包。20261003-043412-search-t2-heart-interp-g24-D-s1
父节点n8
子节点n13、n16
操作?种子:人写的起点;改进:在父节点上改;草稿:从头写;修复:修父节点的报错。改进
状态已打分
分数搜索目标分 63.52(+0.0) · proxy 63.52(+0.0)
审查通过 1 越界读取:未发现问题——候选代码(run.py/cluster_sample.py/matched_align.py)无任何直接文件 I/O、网络或绝对路径,数据读取全部经 view_io 的 load_manifest/read_stage/interp_bracket(run.py:79-89)走视图清单;imports 仅限 src.task2_spatial 提供的库,未触及 src/common/evaluation 或目标阶段文件。; 2 硬编码目标统计量:未发现问题——代码中数值常量仅为方法超参(k=25、n_clusters=10、corr_thresh=0.3、min_…
用时?从运行开始到结束(或到现在)的挂钟时间。18 分
程序版本d4367f5fa0ea88326be98612e7fb33e0a517785d (programs.git)

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

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

实现并对照 PLAN 的 FPS 播种机制(T2HI-04),代理上 FPS 各变体及对齐加权/阈值/簇数微调均未超父节点 8,故提交回退已验证父配置(随机播种,与 node8 逐位相同)。

方法(提交配置 = 父节点 8)

  • mix 路径不变:括号两阶段 matched-align 到同一帧、log 线性 RMS 缩放(scale_damp=1)、_limits 定细胞数、round(t*n) 从后侧取、每类型配额算术不变;类型内用 k-NN 贪心区域生长抽空间连续簇(k=25、n_clusters=10、随机播种)。
  • matched-align 不变:跨类型 pseudobulk 表达 Pearson 相关贪心配对(r≥0.3、每侧≥30 细胞、Kabsch 权重 min(两侧细胞数),z 保持切片轴、符号由匹配质心 z 相关定),<3 对回退 exact-name 再回退 PCA。
  • 确定性:只用视图数据 + np.random.default_rng(seed);只用时间差 t,不用绝对时间、不看视图路径/名称/manifest 键序。提交默认配置与父节点 8 seed 0 逐位相同(np.array_equal 本地验证 .X 与 obsm 均相等)。

本节点实现的机制(FPS 播种)与对照结果

PLAN 机制已在 solution/cluster_sample.py 完整实现,通过 T2_SEED_MODE 开关切换:

  • seed_mode="fps":每型用 farthest-point sampling 选 n_clusters 个种子(首个取型质心最近细胞,之后每轮取离已选种子集合最大最小距离的细胞),可选 z 分层(_z_layers 把离散 z 值按最大间隙聚成 2–4 层,按各层细胞比例分配种子配额、每层≥1,min_layer<3 回退不分层),可选 σ=jitter_frac·RMS 高斯抖动后 snap 到最近细胞,簇大小上限 max_size=ceil(1.5·q/n_clusters)(超出配额由下一簇吸收)。
  • seed_mode="random"(提交默认):回退父节点 rng.integers 随机播种,输出与 node 8 逐位相同(mechanism_off_control 已验证 np.array_equal True)。

代理查分(proxy,E8.25_late+E9.5→E8.75,t=0.4,seed 0,A 半;父 node 8 seed0=62.94,shape 55.93,local 65.67,occ 0.828,d2_shape 0.0291):

配置boardshape_scalelocal_spatialoccupancy_diced2_shape
父 node8 = random/count(提交)62.9455.9365.670.8280.0291
FPS + z分层 + jitter0.0261.9355.5161.940.8290.0286
FPS + z分层, 无 jitter62.1055.1762.440.8210.0281
FPS 纯(无 z、无 jitter)61.9954.3362.210.8100.0286
对齐加权 count·r62.8555.2164.770.8330.0304
对齐加权 count·r²62.7354.7764.760.8320.0317
corr_thresh 0.2 / 0.462.9455.3565.020.8290.0291
n_clusters 16 / 2462.07 / 61.5654.5 / 52.263.6 / 63.10.826 / 0.8300.031 / 0.042

机制被证伪的证据

  1. occupancy_dice 未改善(PLAN 的核心预期):FPS 各变体 occ 0.810–0.829,与父 0.828 持平或更低;z 分层/jitter 都没把它推高。FPS「最大化空间覆盖→提升 occupancy」的假设在本代理上不成立。
  2. local_spatial 显著下降:FPS 把 local_spatial 从 65.67 打到 61.9–62.4(−3.2~−3.7,远超 T2 约 1 分噪声),neighborhood_mmd 从 0.0518 升到 0.058。原因:FPS 种子落在型云的最远点(外围/边缘),区域生长出的簇处在邻域稀疏的边缘,破坏了 5-NN 邻域结构——这是 FPS 播种的固有副作用,不是参数问题(jitter 缓解有限)。
  3. shape_scale 未达门槛:PLAN risk #4 预设「shape_scale<56 则放弃」。所有 FPS 变体 shape_scale 54.3–55.5 < 56,且低于父 55.93;d2_shape 虽微降但被 local/occ 抵消。
  4. 父节点建议的替代方向同样失败:对齐加权 count·r / r² 提升 occ 但恶化 d2_shape(0.030/0.032 vs 0.029)净负;corr_thresh 0.2/0.4 因贪心已选满 22 对而逐位等于父;n_clusters 16/24 单调恶化 d2_shape。

结论与判断

  • PLAN 指定的 FPS 播种机制已实现、已对照、被代理数据证伪(不改善 occupancy_dice,反而显著伤 local_spatial),按 PLAN 预登记的 risk #4 判据放弃,提交默认切回 control(random)。没有把别的方法冒充成 FPS。
  • 提交配置与父节点 8 逐位相同,是本节点在代理上能达到的非回退最优;父节点在 shape_scale 上已处局部最优,瓶颈是 scale_log_ratio=0.4669 固定(预测点云 RMS≈346 vs 真实目标≈217 的非单调收缩,log 线性从两端 354/335 够不到),而真实目标 RMS 属保留阶段的「尺寸」信息、不可用,故代理上无法合法改善。

未验证 / 风险

  • final 括号无法本地验证:FPS 在 final(31 共有类型、RMS 单调 354→217、t=0.5)上 local_spatial 是否仍下降未测;但代理已明确 FPS 伤邻域,方向一致地不看好,故不提交。
  • 未试:真正能改善 scale_log_ratio 的合法手段(需要不依赖保留尺寸的机制,如用 prior 通路/谱系推断收缩方向),本节点时间/额度内未展开。
  • 知识来源:无外部生物知识;只用视图内表达、坐标、标签与时间差。

调研员的计划

名称FPS-seeded cluster sampling for occupancy_dice
动机shape_scale 55.93 仍是四组最低(次低 expression_change 64.90)。ANALYSIS 指出 occupancy_dice 0.80–0.83 且种子敏感,主因是随机播种导致簇间空洞。父节点 8 的 matched-align 已把 d2_shape 从 0.0339 降到 0.0291,剩余 shape 瓶颈在覆盖率。节点 6 证明加权/剔除/z-均衡均无效,节点 5 的 OT 位移也失败;唯一未试的结构改动是播种策略。
做法在 cluster_sample.py 的 grow_clusters 前替换种子选取:(1) 对每型对齐后坐标做 farthest-point sampling (FPS) 选 n_clusters 个种子——第一个种子取该型质心最近细胞,之后每轮取离已选种子集合最大最小距离的细胞;(2) 若该型跨 ≥2 个 z 层(z 按分位数分层),按各层细胞比例分配种子配额,保证每层 ≥1 种子(层内仍用 FPS);(3) 加簇大小上限 max_size=ceil(1.5*quota/n_clusters),生长到上限即停,剩余配额由下一簇吸收。其余逻辑(k=25 区域生长、表达/坐标取真实细胞、配额算术、matched-align、RMS 缩放)全部不变。参数:seed_mode=fps(新),n_clusters=10,k=25,z_layers=auto(按 unique z 值聚类为 2-4 层)。vec-score 筛选:先跑 seed0 看 shape_scale 是否 ≥56.5 且 local_spatial ≥65;再跑 3 种子均值确认 ≥63.5。单输入阶段退路:无括号时直接分层复制,不走簇抽样,与父节点一致。
风险1) FPS 过于确定性,失去随机多样性导致 local_spatial 微降——缓解:FPS 选种子后对种子加 σ=0.02*RMS 高斯抖动再生长;Engineer 应检查 local_spatial 不低于 65。2) z 分层在薄片数据上某些层细胞太少(<n_clusters),需回退到不分层——代码里加 if min_layer_count < 3: skip z-stratify。3) 30 分钟时限紧——改动集中在 cluster_sample.py 的 ~30 行种子选取函数,run.py 只加环境变量开关,风险可控。4) 若 occupancy_dice 不是 shape_scale 主要扣分项(而是其他子指标),改善可能 <1 分——Engineer 用 seed0 快速判断,若 shape_scale <56 则放弃提交。

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

对比:父节点版本 dcafc254da。改动的文件:solution/METHOD.md +34 −26、solution/cluster_sample.py +124 −11、solution/matched_align.py +8 −3、solution/run.py +12 −2

diff --git a/solution/METHOD.md b/solution/METHOD.mdindex cb8ca2e..bb1413b 100644--- a/solution/METHOD.md+++ b/solution/METHOD.md@@ -1,37 +1,45 @@-在父节点簇抽样(k=25,n_clusters=10)上,把 Procrustes 对齐的地标从 exact-name 共有类型换成跨类型表达相关贪心匹配对(Pearson r≥0.3、每侧≥30 细胞),组成/表达/配额逻辑不变。+实现并对照 PLAN 的 FPS 播种机制(T2HI-04),代理上 FPS 各变体及对齐加权/阈值/簇数微调均未超父节点 8,故提交回退已验证父配置(随机播种,与 node8 逐位相同)。 -## 方法+## 方法(提交配置 = 父节点 8) -- 与父节点 7 相同的 mix 路径:括号两阶段对齐到同一帧、log 线性 RMS 缩放(scale_damp=1)、`_limits` 定细胞数、`round(t*n)` 从后侧取、每类型配额算术不变;类型内用 k-NN 贪心区域生长抽空间连续簇(k=25, n_clusters=10)。-- 本节点机制(`MATCHED_ALIGN=True`,`solution/matched_align.py`):对齐地标不再要求类型同名。对两侧各自 ≥30 细胞的每个类型算 pseudobulk(类型均值)表达,两两 Pearson 相关,从高到低贪心配对(每个类型只用一次,r<0.3 停止),用匹配对的质心做 xy Kabsch(权重 min(两侧细胞数),允许反射),z 保持切片轴、符号由匹配质心 z 相关定。<3 对时回退 exact-name 共有质心,再回退 PCA。-- 动机:代理括号两侧注释体系不同(E8.25_late 33 型 vs E9.5 心脏 22 型,exact-name 只有 5 对且其中同名类型未必同源——贪心把 A 的 Peri 配到 B 的 ST、A 的 JCF 配到 B 的 Peri,pPHM↔aPHM 交叉,说明两侧命名确有错位);表达相关匹配给出 22 对更可信的地标。final 括号两侧同一官方词汇(31 个共有类型),同名的同源类型相关通常最高,贪心会自然保留它们,机制退化为"同名为主 + 少数跨名补充",方向一致、低风险(PLAN risk 3)。-- 参数开关:`T2_MATCHED_ALIGN=0` / `T2_CLUSTER_SAMPLING=0` / `T2_MATCH_MODE` / `T2_CORR_THRESH` / `T2_N_CLUSTERS` 环境变量仅用于对照实验,提交默认 greedy、r=0.3、nc=10、两个机制都打开。-- 确定性:只用视图数据 + `np.random.default_rng(seed)`;seed 0 重跑逐位相同(本地验证)。只用时间差 t,不用绝对时间、不看视图路径/名称。+- mix 路径不变:括号两阶段 matched-align 到同一帧、log 线性 RMS 缩放(scale_damp=1)、`_limits` 定细胞数、`round(t*n)` 从后侧取、每类型配额算术不变;类型内用 k-NN 贪心区域生长抽空间连续簇(k=25、n_clusters=10、**随机播种**)。+- matched-align 不变:跨类型 pseudobulk 表达 Pearson 相关贪心配对(r≥0.3、每侧≥30 细胞、Kabsch 权重 **min(两侧细胞数)**,z 保持切片轴、符号由匹配质心 z 相关定),<3 对回退 exact-name 再回退 PCA。+- 确定性:只用视图数据 + `np.random.default_rng(seed)`;只用时间差 t,不用绝对时间、不看视图路径/名称/manifest 键序。提交默认配置与父节点 8 seed 0 逐位相同(`np.array_equal` 本地验证 .X 与 obsm 均相等)。 -## 机制生效证据(proxy,E8.25_late+E9.5→E8.75,t=0.4,seed 0,A 半)+## 本节点实现的机制(FPS 播种)与对照结果 -- 匹配对数:exact-name 5 → greedy 22 对(全部 r≥0.389,最高 0.863),Kabsch 旋转改变、z 符号翻转为 -1(父 +1),输出与父节点不逐位相同(已验证)。-- 关闭对照(`T2_MATCHED_ALIGN=0`,其余不变):输出与父节点 7 逐位相同(本地 np.array_equal 验证)。-- 四组分(对照 = 父 node 7 seed 0):shape_scale 53.5→55.35(+1.85,d2_shape 0.0339→0.0291),local_spatial 64.61→65.02(持平微升,邻域机制未破坏),expression_change 64.6→64.6、cell_state 66.8→66.8(同一批真实细胞、组成不变,不动,符合预期)。-- 平均 5-NN 结构保持(neighborhood_mmd 0.0518 vs 父 0.0530)。+PLAN 机制已在 `solution/cluster_sample.py` 完整实现,通过 `T2_SEED_MODE` 开关切换: -## 查分(vec-score A 半,proxy;父 node 7 三种子 A 半均值 62.11)+- `seed_mode="fps"`:每型用 farthest-point sampling 选 n_clusters 个种子(首个取型质心最近细胞,之后每轮取离已选种子集合最大最小距离的细胞),可选 z 分层(`_z_layers` 把离散 z 值按最大间隙聚成 2–4 层,按各层细胞比例分配种子配额、每层≥1,min_layer<3 回退不分层),可选 σ=jitter_frac·RMS 高斯抖动后 snap 到最近细胞,簇大小上限 max_size=ceil(1.5·q/n_clusters)(超出配额由下一簇吸收)。+- `seed_mode="random"`(**提交默认**):回退父节点 `rng.integers` 随机播种,输出与 node 8 逐位相同(mechanism_off_control 已验证 `np.array_equal` True)。 -| 配置 | seed0 | seed1 | seed2 | 3种子均值 |-|---|---:|---:|---:|---:|-| greedy r0.3, nc=10(提交) | **62.94** | 61.74 | 62.27 | **62.32** |-| greedy r0.5, nc=10 | 62.94 | | | |-| mutual-best r0.3, nc=10 | 62.79 | 61.41 | 62.15 | 62.12 |-| greedy r0.3, nc=12 | 63.15 | 61.60 | 61.05 | 61.93 |-| greedy r0.3, nc=14 | 61.72 | | | |-| mutual r0.3, nc=12 | 63.11 | | | |-| matched-align 单独(关簇抽样) | 59.07 | | | =父 mix 对照 59.07 |+代理查分(proxy,E8.25_late+E9.5→E8.75,t=0.4,seed 0,A 半;父 node 8 seed0=62.94,shape 55.93,local 65.67,occ 0.828,d2_shape 0.0291): -- seed 0 满足 PLAN 门槛(shape 55.35≥53.5、local 65.02≥64、shape 提升 +1.85>1);nc=12 第二轮在 seed 0 更高但 3 种子均值更低,不取。-- 如实说明:3 种子均值 62.32 只比父 62.11 高 +0.2,在 T2 约 1 分噪声内;波动主要来自 occupancy_dice 对簇落点的种子敏感性(0.80–0.83),matched-align 本身在代理上无系统性害处(单独关簇抽样时与父 mix 同分 59.07)。按 PLAN risk 3 提交:机制在 final(31 个共有类型、注释一致)上更可能生效,代理只 5 对同名地标、信号弱。+| 配置 | board | shape_scale | local_spatial | occupancy_dice | d2_shape |+|---|---:|---:|---:|---:|---:|+| 父 node8 = random/count(提交) | **62.94** | **55.93** | **65.67** | 0.828 | 0.0291 |+| FPS + z分层 + jitter0.02 | 61.93 | 55.51 | 61.94 | 0.829 | 0.0286 |+| FPS + z分层, 无 jitter | 62.10 | 55.17 | 62.44 | 0.821 | 0.0281 |+| FPS 纯(无 z、无 jitter) | 61.99 | 54.33 | 62.21 | 0.810 | 0.0286 |+| 对齐加权 count·r | 62.85 | 55.21 | 64.77 | 0.833 | 0.0304 |+| 对齐加权 count·r² | 62.73 | 54.77 | 64.76 | 0.832 | 0.0317 |+| corr_thresh 0.2 / 0.4 | 62.94 | 55.35 | 65.02 | 0.829 | 0.0291 |+| n_clusters 16 / 24 | 62.07 / 61.56 | 54.5 / 52.2 | 63.6 / 63.1 | 0.826 / 0.830 | 0.031 / 0.042 |++## 机制被证伪的证据++1. **occupancy_dice 未改善**(PLAN 的核心预期):FPS 各变体 occ 0.810–0.829,与父 0.828 持平或更低;z 分层/jitter 都没把它推高。FPS「最大化空间覆盖→提升 occupancy」的假设在本代理上不成立。+2. **local_spatial 显著下降**:FPS 把 local_spatial 从 65.67 打到 61.9–62.4(−3.2~−3.7,远超 T2 约 1 分噪声),neighborhood_mmd 从 0.0518 升到 0.058。原因:FPS 种子落在型云的最远点(外围/边缘),区域生长出的簇处在邻域稀疏的边缘,破坏了 5-NN 邻域结构——这是 FPS 播种的固有副作用,不是参数问题(jitter 缓解有限)。+3. **shape_scale 未达门槛**:PLAN risk #4 预设「shape_scale<56 则放弃」。所有 FPS 变体 shape_scale 54.3–55.5 < 56,且低于父 55.93;d2_shape 虽微降但被 local/occ 抵消。+4. 父节点建议的替代方向同样失败:对齐加权 count·r / r² 提升 occ 但恶化 d2_shape(0.030/0.032 vs 0.029)净负;corr_thresh 0.2/0.4 因贪心已选满 22 对而逐位等于父;n_clusters 16/24 单调恶化 d2_shape。++## 结论与判断++- PLAN 指定的 FPS 播种机制**已实现、已对照、被代理数据证伪**(不改善 occupancy_dice,反而显著伤 local_spatial),按 PLAN 预登记的 risk #4 判据放弃,提交默认切回 control(random)。没有把别的方法冒充成 FPS。+- 提交配置与父节点 8 逐位相同,是本节点在代理上能达到的非回退最优;父节点在 shape_scale 上已处局部最优,瓶颈是 **scale_log_ratio=0.4669 固定**(预测点云 RMS≈346 vs 真实目标≈217 的非单调收缩,log 线性从两端 354/335 够不到),而真实目标 RMS 属保留阶段的「尺寸」信息、不可用,故代理上无法合法改善。  ## 未验证 / 风险 -- final 括号无法本地验证;若 final 上同名地标本就可靠,greedy 跨名配对可能引入个别错配地标(缓解:同名同源类型相关通常最高,贪心优先选中;权重 min(count) 抑制小类型影响)。-- z 符号在代理上翻转为 -1(父为 +1),shape 反而升;final 上两侧切片方向若一致,匹配质心 z 相关会给出正确符号(机制与父相同的判据,只是地标更准)。+- final 括号无法本地验证:FPS 在 final(31 共有类型、RMS 单调 354→217、t=0.5)上 local_spatial 是否仍下降未测;但代理已明确 FPS 伤邻域,方向一致地不看好,故不提交。+- 未试:真正能改善 scale_log_ratio 的合法手段(需要不依赖保留尺寸的机制,如用 prior 通路/谱系推断收缩方向),本节点时间/额度内未展开。 - 知识来源:无外部生物知识;只用视图内表达、坐标、标签与时间差。diff --git a/solution/cluster_sample.py b/solution/cluster_sample.pyindex 270d1de..1080127 100644--- a/solution/cluster_sample.py+++ b/solution/cluster_sample.py@@ -40,8 +40,69 @@ def type_alloc(labels: np.ndarray, n: int):     return types, counts, alloc  +def _z_layers(z: np.ndarray, max_layers: int = 4) -> np.ndarray | None:+    """Group discrete z slice values into 2..max_layers contiguous layers.++    Unique z values are sorted and split at the largest relative gaps so that+    physically separated slice groups become distinct layers. Returns an+    integer layer id per cell, or None when z carries no usable stratification.+    """+    uz = np.unique(z)+    if uz.size < 2:+        return None+    if uz.size <= max_layers:+        pos = {float(v): i for i, v in enumerate(uz)}+        return np.array([pos[float(v)] for v in z], dtype=int)+    gaps = np.diff(uz.astype(np.float64))+    n_split = min(max_layers - 1, int(gaps.size) - 1)+    if n_split < 1:+        return None+    cuts = np.sort(np.argsort(-gaps)[:n_split])+    layer = np.zeros(z.size, dtype=int)+    for c_i, c in enumerate(cuts):+        layer[z > uz[c]] = c_i + 1+    return layer+++def _fps_seeds(coords: np.ndarray, n_seeds: int, idx: np.ndarray | None = None,+               jitter_sigma: float = 0.0, rng: np.random.Generator | None = None,+               tree: "cKDTree | None" = None) -> list[int]:+    """Farthest-point sampling seeds over rows ``idx`` of ``coords``.++    First seed: cell closest to the subset centroid. Each next seed: the cell+    maximising the minimum distance to already-chosen seeds. Optional Gaussian+    jitter (sigma) snaps each seed to its nearest cell to retain some+    stochastic diversity.+    """+    if idx is None:+        idx = np.arange(coords.shape[0])+    pts = coords[idx]+    n_seeds = int(min(max(n_seeds, 1), idx.size))+    centroid = pts.mean(axis=0)+    d2 = ((pts - centroid) ** 2).sum(axis=1)+    first = int(np.argmin(d2))+    seeds = [first]+    mind = ((pts - pts[first]) ** 2).sum(axis=1)+    while len(seeds) < n_seeds:+        nxt = int(np.argmax(mind))+        if mind[nxt] <= 0.0:+            break+        seeds.append(nxt)+        mind = np.minimum(mind, ((pts - pts[nxt]) ** 2).sum(axis=1))+    out = [int(idx[s]) for s in seeds]+    if jitter_sigma > 0.0 and rng is not None and tree is not None:+        jittered = []+        for s in out:+            pos = coords[s] + rng.normal(0.0, jitter_sigma, size=coords.shape[1])+            jittered.append(int(tree.query(pos)[1]))+        out = jittered+    return out++ def grow_clusters(coords: np.ndarray, q: int, rng: np.random.Generator,-                  k: int = 15, n_clusters: int = 5) -> np.ndarray:+                  k: int = 15, n_clusters: int = 5,+                  seed_mode: str = "random", jitter_frac: float = 0.02,+                  max_layers: int = 4) -> np.ndarray:     """Select q of the m points as ~n_clusters contiguous region-growing clusters."""     m = int(coords.shape[0])     if q >= m:@@ -51,15 +112,60 @@ def grow_clusters(coords: np.ndarray, q: int, rng: np.random.Generator,     kk = int(min(max(k, 1), m - 1))     nbrs = tree.query(coords, k=kk + 1)[1][:, 1:]     cluster_target = max(1, int(np.ceil(q / max(1, int(n_clusters)))))+    fps_seeds: list[int] = []+    max_size = cluster_target+    if seed_mode == "fps":+        max_size = max(cluster_target, int(np.ceil(1.5 * q / max(1, int(n_clusters)))))+        n_seeds = int(min(max(1, int(n_clusters)), q, m))+        rms = float(np.sqrt((coords ** 2).sum(axis=1).mean()))+        sigma = float(jitter_frac) * rms+        layer = _z_layers(coords[:, 2], max_layers=max_layers) if max_layers >= 2 else None+        if layer is not None:+            counts = np.bincount(layer)+            nl = int(counts.size)+            if nl >= 2 and nl <= n_seeds and int(counts.min()) >= 3:+                raw = counts / counts.sum() * n_seeds+                quota = np.maximum(np.floor(raw).astype(int), 1)+                over = int(quota.sum() - n_seeds)+                while over > 0:+                    j = int(np.argmax(np.where(quota > 1, quota, 0)))+                    if quota[j] <= 1:+                        break+                    quota[j] -= 1+                    over -= 1+                under = n_seeds - int(quota.sum())+                if under > 0:+                    order_l = np.argsort(-counts)+                    for j in order_l[:under]:+                        quota[int(j)] += 1+                for li in range(nl):+                    idx_l = np.flatnonzero(layer == li)+                    fps_seeds.extend(_fps_seeds(coords, int(quota[li]), idx_l,+                                                sigma, rng, tree))+        if not fps_seeds:+            fps_seeds = _fps_seeds(coords, n_seeds, None, sigma, rng, tree)     selected = np.zeros(m, dtype=bool)     order: list[int] = []     n_sel = 0+    si = 0+    carry = 0     while n_sel < q:-        avail = np.flatnonzero(~selected)-        seed = int(avail[int(rng.integers(len(avail)))])+        if fps_seeds and si < len(fps_seeds):+            seed = int(fps_seeds[si])+            si += 1+            if selected[seed]:+                carry = min(carry + cluster_target, max_size)+                continue+        else:+            avail = np.flatnonzero(~selected)+            if avail.size == 0:+                break+            seed = int(avail[int(rng.integers(len(avail)))])         heap = [(0.0, seed)]         size = 0-        while heap and size < cluster_target and n_sel < q:+        limit = cluster_target + carry if seed_mode == "fps" else cluster_target+        limit = int(min(limit, max_size))+        while heap and size < limit and n_sel < q:             d, c = heapq.heappop(heap)             if selected[c]:                 continue@@ -73,12 +179,14 @@ def grow_clusters(coords: np.ndarray, q: int, rng: np.random.Generator,                 if not selected[nb]:                     diff = pc - coords[nb]                     heapq.heappush(heap, (float(np.dot(diff, diff)), nb))+        carry = 0 if n_sel >= q else max(0, limit - size)     return np.asarray(order[:q], dtype=int)   def cluster_take(labels: np.ndarray, coords: np.ndarray, n: int,                  rng: np.random.Generator, k: int = 15, n_clusters: int = 5,-                 cluster_frac: float = 1.0) -> np.ndarray:+                 cluster_frac: float = 1.0, seed_mode: str = "random",+                 jitter_frac: float = 0.02, max_layers: int = 4) -> np.ndarray:     labels = np.asarray(labels).astype(str)     n = int(min(max(n, 1), len(labels)))     if n >= len(labels):@@ -90,7 +198,9 @@ def cluster_take(labels: np.ndarray, coords: np.ndarray, n: int,             continue         idx = np.flatnonzero(labels == lab)         q_cl = int(np.clip(round(int(kk) * float(cluster_frac)), 0, int(kk)))-        sel = grow_clusters(coords[idx], q_cl, rng, k=k, n_clusters=n_clusters)+        sel = grow_clusters(coords[idx], q_cl, rng, k=k, n_clusters=n_clusters,+                            seed_mode=seed_mode, jitter_frac=jitter_frac,+                            max_layers=max_layers)         chosen = idx[sel]         if chosen.size < int(kk):             rest = np.setdiff1d(idx, chosen, assume_unique=False)@@ -107,14 +217,17 @@ def cluster_take(labels: np.ndarray, coords: np.ndarray, n: int,  def cluster_mix_indices(labels_a, labels_b, coords_a, coords_b, t: float, n: int,                         rng: np.random.Generator, k: int = 15, n_clusters: int = 5,-                        cluster_frac: float = 1.0):+                        cluster_frac: float = 1.0, seed_mode: str = "random",+                        jitter_frac: float = 0.02, max_layers: int = 4):     """Drop-in replacement for sample.mix_indices using cluster sampling."""     n_b = int(round(float(t) * n))     n_b = int(np.clip(n_b, 0, n))     n_a = n - n_b+    kw = dict(k=k, n_clusters=n_clusters, cluster_frac=cluster_frac,+              seed_mode=seed_mode, jitter_frac=jitter_frac, max_layers=max_layers)     if n_a == 0:-        return np.array([], dtype=int), cluster_take(labels_b, coords_b, n_b, rng, k, n_clusters, cluster_frac)+        return np.array([], dtype=int), cluster_take(labels_b, coords_b, n_b, rng, **kw)     if n_b == 0:-        return cluster_take(labels_a, coords_a, n_a, rng, k, n_clusters, cluster_frac), np.array([], dtype=int)-    return (cluster_take(labels_a, coords_a, n_a, rng, k, n_clusters, cluster_frac),-            cluster_take(labels_b, coords_b, n_b, rng, k, n_clusters, cluster_frac))+        return cluster_take(labels_a, coords_a, n_a, rng, **kw), np.array([], dtype=int)+    return (cluster_take(labels_a, coords_a, n_a, rng, **kw),+            cluster_take(labels_b, coords_b, n_b, rng, **kw))diff --git a/solution/matched_align.py b/solution/matched_align.pyindex ba8c1e2..a2ff238 100644--- a/solution/matched_align.py+++ b/solution/matched_align.py@@ -41,7 +41,7 @@ def _type_stats(X, labels, coords, min_count: int) -> dict:   def matched_centroids(a, la, b, lb, Xa, Xb, min_count: int = 30, corr_thresh: float = 0.3,-                      match_mode: str = "greedy"):+                      match_mode: str = "greedy", weight_mode: str = "count"):     """Cross-type landmark pairs: greedy best expression correlation (r >= thresh).      Type names across stages come from different annotations, so exact-name@@ -89,6 +89,10 @@ def matched_centroids(a, la, b, lb, Xa, Xb, min_count: int = 30, corr_thresh: fl         ca = np.stack([sa[p[0]][2] for p in pairs])         cb = np.stack([sb[p[1]][2] for p in pairs])         w = np.array([min(sa[p[0]][1], sb[p[1]][1]) for p in pairs], dtype=np.float64)+        if weight_mode == "count_r":+            w = w * np.array([max(p[2], 0.0) for p in pairs], dtype=np.float64)+        elif weight_mode == "count_r2":+            w = w * np.array([max(p[2], 0.0) ** 2 for p in pairs], dtype=np.float64)         keys = [f"{p[0]}~{p[1]}" for p in pairs]         return ca, cb, w, keys, [p[2] for p in pairs], True, pairs     ca, cb, w, keys = shared_centroids(a, la, b, lb, min_count)@@ -97,7 +101,7 @@ def matched_centroids(a, la, b, lb, Xa, Xb, min_count: int = 30, corr_thresh: fl  def align_pair_matched(coords_a, coords_b, labels_a, labels_b, Xa, Xb,                        min_count: int = 30, corr_thresh: float = 0.3,-                       match_mode: str = "greedy"):+                       match_mode: str = "greedy", weight_mode: str = "count"):     """Procrustes (z held) on expression-matched shared-type centroids.      Mirrors ``frame.align_pair(method="procrustes")``; only the landmark set@@ -113,7 +117,8 @@ def align_pair_matched(coords_a, coords_b, labels_a, labels_b, Xa, Xb,         "z_axis_b": int(np.argmin(slice_counts(coords_b))),     }     ca, cb, w, keys, corrs, matched, _pairs = matched_centroids(-        a, labels_a, b, labels_b, Xa, Xb, min_count, corr_thresh, match_mode)+        a, labels_a, b, labels_b, Xa, Xb, min_count, corr_thresh, match_mode,+        weight_mode)     info["match_corrs"] = corrs     info["match_keys"] = keys     info["n_matched_types"] = int(len(keys)) if matched else 0diff --git a/solution/run.py b/solution/run.pyindex dbcdd50..32eed81 100644--- a/solution/run.py+++ b/solution/run.py@@ -43,8 +43,10 @@ from matched_align import align_pair_matched CLUSTER_SAMPLING = True MATCHED_ALIGN = True PARAMS = {"align": "procrustes", "scale_damp": 1.0}-CLUSTER_PARAMS = {"k": 25, "n_clusters": 10}-MATCH_PARAMS = {"min_count": 30, "corr_thresh": 0.3, "match_mode": "greedy"}+CLUSTER_PARAMS = {"k": 25, "n_clusters": 10, "seed_mode": "random",+                  "jitter_frac": 0.02, "max_layers": 4}+MATCH_PARAMS = {"min_count": 30, "corr_thresh": 0.3, "match_mode": "greedy",+                "weight_mode": "count"}  import os if os.environ.get("T2_CLUSTER_SAMPLING") == "0":@@ -53,10 +55,18 @@ if os.environ.get("T2_MATCHED_ALIGN") == "0":     MATCHED_ALIGN = False if os.environ.get("T2_MATCH_MODE"):     MATCH_PARAMS["match_mode"] = os.environ["T2_MATCH_MODE"]+if os.environ.get("T2_WEIGHT_MODE"):+    MATCH_PARAMS["weight_mode"] = os.environ["T2_WEIGHT_MODE"] if os.environ.get("T2_CORR_THRESH"):     MATCH_PARAMS["corr_thresh"] = float(os.environ["T2_CORR_THRESH"]) if os.environ.get("T2_N_CLUSTERS"):     CLUSTER_PARAMS["n_clusters"] = int(os.environ["T2_N_CLUSTERS"])+if os.environ.get("T2_SEED_MODE"):+    CLUSTER_PARAMS["seed_mode"] = os.environ["T2_SEED_MODE"]+if os.environ.get("T2_JITTER_FRAC"):+    CLUSTER_PARAMS["jitter_frac"] = float(os.environ["T2_JITTER_FRAC"])+if os.environ.get("T2_MAX_LAYERS"):+    CLUSTER_PARAMS["max_layers"] = int(os.environ["T2_MAX_LAYERS"])   def main() -> None:

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

用到的知识库条目

编号标题出处
k007Interval staging and held-out-window filtering of external datanotes/official/来件/virtualembryo.ai/rules.md
k026Canonicalise predicted 3D coordinates before submissionnotes/pitfalls/04_scorer_invariance.md
k027Joint expression-geometry generation with relative geometrynotes/competition/03_solution_landscape.md

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

改了什么在 cluster_sample.py 完整实现了 PLAN 的 FPS 播种(farthest-point sampling + z 分层配额 + jitter + max_size 上限,T2_SEED_MODE 开关),并在 matched_align.py 加了 weight_mode(count_r/count_r2);但代理查分证伪 FPS 后,提交默认切回 random 播种,提交输出与父节点 8 逐位相同(榜分四组均 +0.00 印证)。
各组分数的变化cell_state:不变,67.58 → 67.58(+0.00),提交配置与父逐位相同
expression_change:不变,64.90 → 64.90(+0.00),提交配置与父逐位相同
local_spatial:不变,65.67 → 65.67(+0.00);代理上 FPS 把 local_spatial 打到 61.9–62.4(-3.2~-3.7,远超噪声),是被证伪的主因之一
shape_scale:不变,55.93 → 55.93(+0.00);FPS 各变体在代理上 shape_scale 54.3–55.5 均低于父,未达 PLAN 预设的 56 门槛,故未提交
family_idother
假设是否成立否
经验
  1. 在该 T2 心脏插值代理上,用 FPS 替代随机播种选簇种子:occupancy_dice 不升(0.810–0.829 vs 父 0.828),local_spatial 反降 3+ 分(neighborhood_mmd 0.0518→0.058),因为 FPS 种子落在型云最远点/边缘,区域生长出的簇处于 5-NN 稀疏区——最远点覆盖类播种对邻域结构指标有固有伤害,jitter(σ=0.02·RMS) 缓解有限。
  2. occupancy_dice(0.80–0.83 区间)对播种策略不敏感:随机、FPS、z 分层、对齐加权 count·r/r²(occ 可到 0.833)都只在 ±0.02 内波动,说明它不是靠改采样位置能撬动的瓶颈,d2_shape 已饱和在 ~0.0291。
  3. 预登记放弃判据(risk #4: shape_scale<56 则弃)+ 机制关闭对照(T2_SEED_MODE=random 与父逐位相同)让本节点在证伪后安全回退,榜分零损失——'先代理筛选、不达标就提交 control' 的流程值得沿用,但代价是整个节点对榜分零信息增益。
  4. shape_scale 的真实瓶颈是 scale_log_ratio=0.4669:预测点云 RMS≈346 vs 真实目标≈217(此阶段心脏非单调收缩),log 线性插值从两端 RMS 354/335 够不到;采样/对齐类改动均无法触及该项,真实目标 RMS 属保留信息不可用。
  5. corr_thresh 0.2/0.4 输出逐位等于父(贪心在 r≥0.3 前已选满 22 对)——对已饱和的贪心匹配调阈值是无效实验,先检查匹配是否已选满再调参。
mechanism_active否
下一步建议
  1. 针对 shape_scale:放弃采样/播种/对齐权重方向(节点 5/6/11 已三次证伪),改攻 scale_log_ratio——用视图内可得信号(如 prior 通路活性、谱系组成随 t 的变化)推断收缩方向,对坐标做各向异性缩放(如沿 z 或径向压缩 RMS 至两端 log 线性值以下),先在代理 seed0 验证 shape_scale 是否 >56.5 再提交。
  2. 针对 local_spatial(65.67)与 cell_state(67.58):这两组是当前较高项且对采样结构敏感,若尝试全局缩放类改动,必须用 T2 开关做关闭对照并确认 local_spatial 不低于 65,避免重蹈 FPS 伤邻域的覆辙。
  3. 针对流程:下一个节点若代理筛选再次全部 ≤ 父,不要第三次提交与父逐位相同的回退(零信息),应把剩余额度用于在代理上快速扫描至少一个结构性不同的机制(如坐标域变换而非细胞选取)。

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

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

分析员

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

工程师

角色工程师 alibaba-token-plan-cn/qwen3.8-max
调用次数1 次
轮数40
工具调用共 42 次:bash 21、edit 16、read 4、write 1
用时15 分
token 数输入 73,268 · 输出 13,287 · 思考 19,421
任务(第一行)按计划修改程序:FPS-seeded cluster sampling for occupancy_dice
最后的回答(摘录)
完成。提交已定稿并通过 `vec-check`。

**做了什么**:按 PLAN 在 `cluster_sample.py` 完整实现了 FPS 播种机制(farthest-point sampling + z 分层配额 + jitter + max_size 上限),并用 `T2_SEED_MODE` 开关做机制对照(`random` 时输出与父节点 8 逐位相同,`np.array_equal` 验证 True)。FPS 证伪后,又试了父节点建议的对齐加权(count·r/r²)、corr_thresh(0.2/0.4)、n_clusters(16/24)等替代方向。

**查分结果**(proxy A 半,seed0;父 node8=62.94,shape 55.93,local 65.67,occ 0.828):
- FPS 各变体 61.9–62.1,shape_scale 54.3–55.5(均 <56),local_spatial 掉到 61.9–62.4,occupancy_dice 未改善(0.81–0.83)——**FPS 核心假设被证伪**:种子落在型云外围,簇处于邻域稀疏边缘,伤 local_spatial。
- 对齐加权/阈值/簇数微调全部 ≤ 父(62.7–62.94,噪声内),d2_shape 已饱和在 0.0291。
- 无一变体超过父节点,按 PLAN risk #4 判据放弃 FPS,提交默认切回 control(random+count,与 node8 逐位相同)。METHOD.md 如实记录,未把别的方法冒充成 FPS。

**下一步最值得试**:shape_scale 的真正瓶颈是 scale_log_ratio=0.4669 固定——预测点云 RMS≈346 而真实目标≈217(心脏此阶段非单调收缩),log 线性从两端 354/335 够不到;但真实目标 RMS 属保留阶段「尺寸」信息不可用。值得探索的是**不依赖保留尺寸**的合法收缩机制(如用 prior 通路/谱系知识推断 compaction 方向对坐标做各向异性缩放),这是唯一能实质抬升 shape_scale 的杠杆,代理上可测。
原始记录位置/home/spark-longxinyang/vec/runs/formal/20261003-043412-search-t2-heart-interp-g24-D-s1/nodes/11/engineer.jsonl 218 KB
/home/spark-longxinyang/vec/runs/formal/20261003-043412-search-t2-heart-interp-g24-D-s1/nodes/11/engineer.stderr

调研员

角色调研员 alibaba-token-plan-cn/qwen3.8-max
调用次数1 次
轮数1
工具调用共 0 次:—
用时1 分
token 数输入 11,803 · 输出 1,077 · 思考 2,071
任务(第一行)为节点 n11(改进,父节点 n8)写改进计划 PLAN.json
最后的回答(摘录)
lusters or k.

```json
{
  "name": "FPS-seeded cluster sampling for occupancy_dice",
  "motivation": "shape_scale 55.93 仍是四组最低(次低 expression_change 64.90)。ANALYSIS 指出 occupancy_dice 0.80–0.83 且种子敏感,主因是随机播种导致簇间空洞。父节点 8 的 matched-align 已把 d2_shape 从 0.0339 降到 0.0291,剩余 shape 瓶颈在覆盖率。节点 6 证明加权/剔除/z-均衡均无效,节点 5 的 OT 位移也失败;唯一未试的结构改动是播种策略。",
  "approach": "在 cluster_sample.py 的 grow_clusters 前替换种子选取:(1) 对每型对齐后坐标做 farthest-point sampling (FPS) 选 n_clusters 个种子——第一个种子取该型质心最近细胞,之后每轮取离已选种子集合最大最小距离的细胞;(2) 若该型跨 ≥2 个 z 层(z 按分位数分层),按各层细胞比例分配种子配额,保证每层 ≥1 种子(层内仍用 FPS);(3) 加簇大小上限 max_size=ceil(1.5*quota/n_clusters),生长到上限即停,剩余配额由下一簇吸收。其余逻辑(k=25 区域生长、表达/坐标取真实细胞、配额算术、matched-align、RMS 缩放)全部不变。参数:seed_mode=fps(新),n_clusters=10,k=25,z_layers=auto(按 unique z 值聚类为 2-4 层)。vec-score 筛选:先跑 seed0 看 shape_scale 是否 ≥56.5 且 local_spatial ≥65;再跑 3 种子均值确认 ≥63.5。单输入阶段退路:无括号时直接分层复制,不走簇抽样,与父节点一致。",
  "expected_groups": ["shape_scale"],
  "risks": "1) FPS 过于确定性,失去随机多样性导致 local_spatial 微降——缓解:FPS 选种子后对种子加 σ=0.02*RMS 高斯抖动再生长;Engineer 应检查 local_spatial 不低于 65。2) z 分层在薄片数据上某些层细胞太少(<n_clusters),需回退到不分层——代码里加 if min_layer_count < 3: skip z-stratify。3) 30 分钟时限紧——改动集中在 cluster_sample.py 的 ~30 行种子选取函数,run.py 只加环境变量开关,风险可控。4) 若 occupancy_dice 不是 shape_scale 主要扣分项(而是其他子指标),改善可能 <1 分——Engineer 用 seed0 快速判断,若 shape_scale <56 则放弃提交。",
  "family_id": "T2HI-04",
  "mechanism": "用 farthest-point sampling + z-层配额约束替代随机播种,使各型簇种子在三维空间最大化覆盖,减少簇间空洞,提升输出点云对目标形状的占据率(occupancy_dice)。",
  "vs_constant_shift": "常数位移只平移整体坐标,不改变哪些细胞被选中或它们的空间分布;本机制改变的是每型内被采样细胞的选取位置(从随机改为最大覆盖),直接影响输出点云的空间覆盖模式,是采样策略的结构性变化。",
  "mechanism_evidence": "Engineer 应记录:(1) 每型 FPS 种子的空间离散度(种子间最小距离 / 型内直径之比,应 >0.3)对比随机种子;(2) 输出点云的 occupancy_dice 代理值(用 3D 直方图 10×10×10 bin 的 dice 系数)对比父节点;(3) 四组分各自变化——预期 shape_scale 上升、local_spatial 持平、cell_state/expression_change 不变;(4) 若 z 分层生效,报告各层种子数。",
  "mechanism_off_control": "环境变量 T2_SEED_MODE=random 时回退到父节点的 np.random.default_rng(seed).choice 随机播种,输出应与父节点 8 逐位相同(np.array_equal 验证)。关闭后若输出不变则机制未运行。",
  "sources": []
}
```
原始记录位置/home/spark-longxinyang/vec/runs/formal/20261003-043412-search-t2-heart-interp-g24-D-s1/nodes/11/researcher.jsonl 5 KB
/home/spark-longxinyang/vec/runs/formal/20261003-043412-search-t2-heart-interp-g24-D-s1/nodes/11/researcher.stderr

审查员

角色审查员 alibaba-token-plan-cn/qwen3.8-max
调用次数1 次
轮数9
工具调用共 13 次:read 9、bash 3、write 1
用时2 分
token 数输入 27,213 · 输出 1,745 · 思考 6,017
任务(第一行)审查节点 n11 的程序是否越界读取、写死目标、钻评分器漏洞
最后的回答(摘录)
REVIEW.json written
原始记录位置/home/spark-longxinyang/vec/runs/formal/20261003-043412-search-t2-heart-interp-g24-D-s1/nodes/11/reviewer.jsonl 120 KB
/home/spark-longxinyang/vec/runs/formal/20261003-043412-search-t2-heart-interp-g24-D-s1/nodes/11/reviewer.stderr