总览 · ← 返回运行 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_equalTrue)。
代理查分(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):
| 配置 | 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 |
机制被证伪的证据
- occupancy_dice 未改善(PLAN 的核心预期):FPS 各变体 occ 0.810–0.829,与父 0.828 持平或更低;z 分层/jitter 都没把它推高。FPS「最大化空间覆盖→提升 occupancy」的假设在本代理上不成立。
- 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 缓解有限)。
- shape_scale 未达门槛:PLAN risk #4 预设「shape_scale<56 则放弃」。所有 FPS 变体 shape_scale 54.3–55.5 < 56,且低于父 55.93;d2_shape 虽微降但被 local/occ 抵消。
- 父节点建议的替代方向同样失败:对齐加权 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:
调研来源?调研员查到并用到的知识条目和文献检索结果(只列标题和编号)。
用到的知识库条目
| 编号 | 标题 | 出处 |
|---|---|---|
| k007 | Interval staging and held-out-window filtering of external data | notes/official/来件/virtualembryo.ai/rules.md |
| k026 | Canonicalise predicted 3D coordinates before submission | notes/pitfalls/04_scorer_invariance.md |
| k027 | Joint expression-geometry generation with relative geometry | notes/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_id | other |
| 假设是否成立 | 否 |
| 经验 |
|
| mechanism_active | 否 |
| 下一步建议 |
|
对话摘要?每个角色和大模型对话的统计:轮数、工具调用、用时、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 |