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

总览 · ← 返回运行 20261003-004922-search-t2-heart-interp-g24

节点 n8

mix 空间分层抽样(T2HI-04):每共有型 xy k-means 分 bin,bin 内按 (1−t,t) 抽两阶段细胞;local_spatial 未提升(3 种子均值 58.99 vs 父 59.36),机制已生效但方向无效。

运行?一次完整的自动搜索或 Agent 会话,有自己的锁定配置和证据包。20261003-004922-search-t2-heart-interp-g24
父节点n2
子节点n9
操作?种子:人写的起点;改进:在父节点上改;草稿:从头写;修复:修父节点的报错。改进
状态已打分
分数搜索目标分 59.80(+0.3) · proxy 59.80(+0.3) · 3 次复测均分 59.04
审查通过 1 越界读取:未发现问题——run.py 只读 --data 指向的视图内数据(load_manifest/read_stage/panel_genes,run.py:47-57),无绝对路径、..、/mnt、联网或读取打分器代码。; 2 硬编码目标统计量:未发现问题——spatial_mix.py 仅有算法超参(MIN_TYPE_CELLS=20、cells_per_bin、ndim),细胞类型配额与空间 bin 全部由输入 labels/coords 现场计算(_alloc_by_type、_kmeans)。; 3 钻评分器漏洞:未发现问题——改动是型内空间分层抽样(spatial_mi…
用时?从运行开始到结束(或到现在)的挂钟时间。12 分
程序版本01d7f77244b11451f9690b808dc4f31eb89087dc (programs.git)

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

来自 programs.git 01d7f77244:solution/METHOD.md

mix 空间分层抽样(T2HI-04):每共有型 xy k-means 分 bin,bin 内按 (1−t,t) 抽两阶段细胞;local_spatial 未提升(3 种子均值 58.99 vs 父 59.36),机制已生效但方向无效。

方法

在父节点 2(mix,align=procrustes,scale_damp=1)之上,只改抽样一步:

  • 对每个两阶段共有的细胞型,把该型全部细胞(两阶段合并,对齐+缩放到目标 RMS 后的坐标)在 xy 上做 k-means,k = max(4, n_type // CELLS_PER_BIN),CELLS_PER_BIN=100(提交默认)。
  • bin 内按该型的 (1−t, t) 配额、以 bin 内各阶段细胞数为权重比例分配(floor + 最大余数 + 容量约束),无放回抽样;表达和坐标随细胞走。
  • 非共有型(只出现在一个阶段)与 <20 细胞的型退回父节点的随机抽样;非共有型无法做跨阶段空间配比,这是本机制的天然覆盖上限。
  • 单输入阶段(b is None)退路与父节点完全一致。
  • 实现:solution/spatial_mix.py(spatial_mix_indices 复制 transport.stratified_choice 的型级配额逻辑,保证每型总数与父节点相同,只改型内选哪些细胞);run.py 里 SPATIAL_STRATIFY=0 为关闭机制对照(直接调父节点的 methods.interpolate,输出与父节点逐位一致,已验证代码路径相同)。

机制生效证据(proxy,E8.25_late+E9.5→E8.75,t=0.4)

  • proxy 上共有型只有 5 个(NCC/Peri/V-CM/aPHM/pPHM),占输出 17616 细胞中的约 3553(20%)。
  • 源阶段标签的空间自相关:Moran's I 从 0.302(父随机抽样)降到 0.287–0.288(bin100–400);kNN(k=10) 同源边比例 0.665 → 0.659(随机混合理论值 t²+(1−t)²=0.52,剩余超出部分来自非共有型的纯簇,机制无法触及)。
  • 四组分(bin100, seed0):expression_change 64.07(+0.2)、cell_state 66.17(−0.5)、shape_scale 53.32(−0.05)、local_spatial 53.60(−0.4),vs 父 63.9/66.7/53.4/54.0。

查分记录(A 半,vec-score)

配置seed0seed1seed2均值
父节点 mix59.50--59.36(rank3)
bin20058.87---
bin40059.17---
bin100(提交默认)59.2958.7458.9358.99
bin100 + 3D 分箱59.01---

结论与已验证/未验证

  • 已验证:机制按 PLAN 实现并生效(上面 Moran's I / kNN 证据);bin∈{100,200,400}、2D/3D 分箱都在父节点 ±0.6 分内,无一提升 local_spatial;关闭开关与父节点同路径。
  • 结论(负面):局部均匀化两阶段混合比不改善 local_spatial,反而 shape_scale 略降(occupancy_dice 0.84→0.81–0.83)。提示真实 E8.75 里"源阶段身份"与空间位置本就相关(发育梯度),强行局部均匀化偏离真实结构;且 proxy 上共有型仅覆盖 20% 细胞,机制杠杆太小。
  • 未验证:final 括号(E8.25+E8.75→E8.5,31 个共有型,覆盖率更高)上机制可能表现不同,本节点无法测。
  • 生物学知识来源:仅方法卡与 PLAN 中的坐标系约定(z 为离散切片轴、保 z 的 procrustes),未引入新的先验。

下一步建议

放弃"抽样空间均匀化"方向;local_spatial 的瓶颈更可能在非共有型的空间占位(occupancy/variogram),值得试的是对非共有型按目标阶段的空间密度重加权抽样,或直接改坐标生成(如类型级形状插值)而非选细胞。

调研员的计划

名称mix 空间分层抽样保邻域(spatial-stratified patch sampling)
动机父节点 2 四组分中 local_spatial 54.03、shape_scale 53.37 最弱。当前 mix 按型随机抽样混合两阶段细胞,同一型内空间上随机散布,导致输出云中局部邻域由不同阶段、不同空间位置的细胞拼凑,破坏邻域一致性。节点 5(T2HI-02 质心位移)已证明刚体位移对 local_spatial 无效(-0.58,噪声内);节点 3(NN 匹配+坐标位移)生成失败。改变抽样策略(而非后处理位移)是尚未尝试的结构方向。
做法在 mix 流程的 stratified sampling 步骤中,将每型内的随机抽样替换为空间分层抽样:
1. 对齐+缩放后,对每个共有细胞型,用该型所有细胞(两阶段合并)的 xy 坐标做 k-means(k = max(4, n_type // 200)),得到空间 bin。
2. 在每个 bin 内,按 (1-t, t) 比例分别从 stage_a、stage_b 抽样(四舍五入),保证每个空间区域都有正确比例的两阶段细胞。若某 bin 内某阶段细胞不足,从最近邻 bin 补。
3. 表达和坐标随细胞一起走,与父节点相同。
4. 非共有型(仅出现在一个阶段的型)保持原逻辑随机抽样。
5. 单输入阶段退路(b is None):无 bracket,直接走父节点的 fallback 路径,空间分层不适用,行为不变。
6. 关键参数:k_per_type 初始 200 细胞/bin;搜索范围 [100, 400]。用 vec-score 跑 1 次确认方向,若 local_spatial 提升 >1 分再跑 2 次确认。
7. 先小样本(取前 3000 细胞)验证抽样逻辑正确、无空 bin,再全量。
风险1) 空间 bin 太小导致某些 bin 内细胞数不足,抽样退化;Engineer 应打印每 bin 实际抽样数,若 >10% bin 不足则增大 bin 尺寸。2) 分层后表达分布可能微变,影响 expression_change;但每型总数不变,预期影响 <0.5 分。3) 若 local_spatial 的评分主要取决于全局形状而非邻域,则改善有限;用第 1 次查分即可判断。4) k-means 在极小型(<20 细胞)上不稳定,这些型退回随机抽样。

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

对比:父节点版本 33a8f2ab38。改动的文件:solution/METHOD.md +38 −0、solution/run.py +25 −10、solution/spatial_mix.py +197 −0

diff --git a/solution/METHOD.md b/solution/METHOD.mdnew file mode 100644index 0000000..2fdb97d--- /dev/null+++ b/solution/METHOD.md@@ -0,0 +1,38 @@+# mix 空间分层抽样(T2HI-04):每共有型 xy k-means 分 bin,bin 内按 (1−t,t) 抽两阶段细胞;local_spatial 未提升(3 种子均值 58.99 vs 父 59.36),机制已生效但方向无效。++## 方法++在父节点 2(mix,align=procrustes,scale_damp=1)之上,只改抽样一步:++- 对每个**两阶段共有**的细胞型,把该型全部细胞(两阶段合并,对齐+缩放到目标 RMS 后的坐标)在 xy 上做 k-means,k = max(4, n_type // CELLS_PER_BIN),CELLS_PER_BIN=100(提交默认)。+- bin 内按该型的 (1−t, t) 配额、以 bin 内各阶段细胞数为权重比例分配(floor + 最大余数 + 容量约束),无放回抽样;表达和坐标随细胞走。+- 非共有型(只出现在一个阶段)与 <20 细胞的型退回父节点的随机抽样;非共有型无法做跨阶段空间配比,这是本机制的天然覆盖上限。+- 单输入阶段(b is None)退路与父节点完全一致。+- 实现:`solution/spatial_mix.py`(`spatial_mix_indices` 复制 `transport.stratified_choice` 的型级配额逻辑,保证每型总数与父节点相同,只改型内选哪些细胞);`run.py` 里 `SPATIAL_STRATIFY=0` 为关闭机制对照(直接调父节点的 `methods.interpolate`,输出与父节点逐位一致,已验证代码路径相同)。++## 机制生效证据(proxy,E8.25_late+E9.5→E8.75,t=0.4)++- proxy 上共有型只有 5 个(NCC/Peri/V-CM/aPHM/pPHM),占输出 17616 细胞中的约 3553(20%)。+- 源阶段标签的空间自相关:Moran's I 从 0.302(父随机抽样)降到 0.287–0.288(bin100–400);kNN(k=10) 同源边比例 0.665 → 0.659(随机混合理论值 t²+(1−t)²=0.52,剩余超出部分来自非共有型的纯簇,机制无法触及)。+- 四组分(bin100, seed0):expression_change 64.07(+0.2)、cell_state 66.17(−0.5)、shape_scale 53.32(−0.05)、local_spatial 53.60(−0.4),vs 父 63.9/66.7/53.4/54.0。++## 查分记录(A 半,vec-score)++| 配置 | seed0 | seed1 | seed2 | 均值 |+|---|---:|---:|---:|---:|+| 父节点 mix | 59.50 | - | - | 59.36(rank3) |+| bin200 | 58.87 | - | - | - |+| bin400 | 59.17 | - | - | - |+| bin100(提交默认) | 59.29 | 58.74 | 58.93 | 58.99 |+| bin100 + 3D 分箱 | 59.01 | - | - | - |++## 结论与已验证/未验证++- **已验证**:机制按 PLAN 实现并生效(上面 Moran's I / kNN 证据);bin∈{100,200,400}、2D/3D 分箱都在父节点 ±0.6 分内,无一提升 local_spatial;关闭开关与父节点同路径。+- **结论(负面)**:局部均匀化两阶段混合比不改善 local_spatial,反而 shape_scale 略降(occupancy_dice 0.84→0.81–0.83)。提示真实 E8.75 里"源阶段身份"与空间位置本就相关(发育梯度),强行局部均匀化偏离真实结构;且 proxy 上共有型仅覆盖 20% 细胞,机制杠杆太小。+- **未验证**:final 括号(E8.25+E8.75→E8.5,31 个共有型,覆盖率更高)上机制可能表现不同,本节点无法测。+- 生物学知识来源:仅方法卡与 PLAN 中的坐标系约定(z 为离散切片轴、保 z 的 procrustes),未引入新的先验。++## 下一步建议++放弃"抽样空间均匀化"方向;local_spatial 的瓶颈更可能在非共有型的空间占位(occupancy/variogram),值得试的是对非共有型按目标阶段的空间密度重加权抽样,或直接改坐标生成(如类型级形状插值)而非选细胞。diff --git a/solution/run.py b/solution/run.pyindex 8c61767..7bdd6a7 100644--- a/solution/run.py+++ b/solution/run.py@@ -1,12 +1,16 @@ #!/usr/bin/env python3-"""mix (T2 interpolation): real cells from both bracketing inputs, drawn (1−t, t).--Brackets the target with the nearest inputs before and after it, puts both in-one frame (``ALIGN``), rescales both clouds to the log-linear RMS-exp(log r_a + SCALE_DAMP·t·Δlog r), and draws cells stratified by type:-round(t·n) from the later stage, the rest from the earlier one. Expression and-coordinates travel together. n is log-linear in t, clipped to the board range.-Parameters are the T2 card's choice for this board (selected_params.json).+"""mix + spatial-stratified per-type sampling (T2HI-04).++Same as the parent mix (align=procrustes, scale_damp=1, cells drawn+(1-t, t) stratified by type, expression and coordinates travel together),+except that within each cell type present in both brackets the draw is+spatially stratified: pooled xy of the type is k-means-binned+(CELLS_PER_BIN cells per bin) and every bin contributes stage-a / stage-b+cells in the type's (1-t, t) proportion, so each spatial region of the+output holds the right stage mixture (keeps local neighbourhoods+consistent). Types in one bracket only, or with < 20 cells, keep the+parent's random draw. Set SPATIAL_STRATIFY=0 to fall back to the exact+parent behaviour (mechanism-off control). If the target is not bracketed, falls back to the latest input before it. """ @@ -14,15 +18,23 @@ from __future__ import annotations  import argparse import json+import os import sys  import numpy as np +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))+ from src.task2_spatial.methods import interpolate from src.task2_spatial.sample import take from src.task2_spatial.view_io import board_params, interp_bracket, load_manifest, panel_genes, read_stage, write_t2 +from spatial_mix import interpolate_spatial+ PARAMS = {"align": "procrustes", "scale_damp": 1.0}+CELLS_PER_BIN = int(os.environ.get("CELLS_PER_BIN", "100"))+BIN_NDIM = int(os.environ.get("BIN_NDIM", "2"))+SPATIAL_STRATIFY = os.environ.get("SPATIAL_STRATIFY", "1") != "0"   def main() -> None:@@ -44,8 +56,11 @@ def main() -> None:     stage_a = read_stage(args.data, a, genes)     stage_b = read_stage(args.data, b, genes)     params = board_params(manifest, "mix", PARAMS, args.seed)-    expr, coords, info = interpolate(stage_a, stage_b, t, params)-    keep = {k: info.get(k) for k in ("t", "n", "rms_a", "rms_b", "out_rms", "n_shared_types", "z_dot", "z_flipped", "align")}+    if SPATIAL_STRATIFY:+        expr, coords, info = interpolate_spatial(stage_a, stage_b, t, params, cells_per_bin=CELLS_PER_BIN, ndim=BIN_NDIM)+    else:+        expr, coords, info = interpolate(stage_a, stage_b, t, params)+    keep = {k: info.get(k) for k in ("t", "n", "rms_a", "rms_b", "out_rms", "n_shared_types", "z_dot", "z_flipped", "align", "n_from_a", "n_from_b")}     print(json.dumps({"bracket": [a["stage"], b["stage"]], **keep}, default=float), file=sys.stderr)     write_t2(args.out, expr, coords, genes, seed=args.seed) diff --git a/solution/spatial_mix.py b/solution/spatial_mix.pynew file mode 100644index 0000000..c4778ed--- /dev/null+++ b/solution/spatial_mix.py@@ -0,0 +1,197 @@+"""mix with spatial-stratified per-type sampling (T2HI-04).++Same pipeline as src.task2_spatial.methods.interpolate for+coord_mode="mix", expr_mode="carry", except that within each cell type+present in BOTH bracketing stages the random draw is replaced by a+spatially stratified draw: pooled xy coordinates of the type are binned+with k-means, and each bin contributes cells from stage a / stage b in+the same proportions as the type-level (1-t, t) allocation, so every+spatial region of the output cloud holds the right stage mixture.+Types seen in only one stage (or too small) keep the parent's random draw.+"""++from __future__ import annotations++import numpy as np++from src.task2_spatial.frame import align_pair, log_interp, rms_radius, scale_to_rms+from src.task2_spatial.methods import _jitter, _limits, _pack_expr+from src.task2_spatial.transport import as_dense++MIN_TYPE_CELLS = 20+++def _alloc_by_type(labels: np.ndarray, n: int):+    """Per-type counts identical to transport.stratified_choice's allocation."""+    labels = np.asarray(labels).astype(str)+    n = int(min(max(n, 1), len(labels)))+    if n >= len(labels):+        return {lab: int(c) for lab, c in zip(*np.unique(labels, return_counts=True))}+    types, counts = np.unique(labels, return_counts=True)+    raw = counts / counts.sum() * n+    alloc = np.floor(raw).astype(int)+    rem = int(n - alloc.sum())+    order = np.argsort(-(raw - alloc))+    for i in range(rem):+        alloc[order[i % len(order)]] += 1+    alloc = np.minimum(alloc, counts)+    deficit = int(n - alloc.sum())+    if deficit > 0:+        spare = counts - alloc+        for i in np.argsort(-spare):+            take = int(min(deficit, spare[i]))+            alloc[i] += take+            deficit -= take+            if deficit == 0:+                break+    return {lab: int(k) for lab, k in zip(types, alloc)}+++def _spread(k: int, weights: np.ndarray, caps: np.ndarray) -> np.ndarray:+    """Split k over bins proportional to weights, capped at caps."""+    out = np.zeros(len(weights), dtype=int)+    total = float(weights.sum())+    if k <= 0 or total <= 0:+        return out+    k = int(min(k, int(caps.sum())))+    raw = weights / total * k+    base = np.minimum(np.floor(raw).astype(int), caps)+    out += base+    rem = k - int(out.sum())+    if rem > 0:+        frac = raw - np.floor(raw)+        spare = caps - out+        order = np.argsort(-frac)+        i = 0+        guard = 0+        while rem > 0 and guard < 10 * len(order) + 10:+            j = order[i % len(order)]+            if spare[j] > 0:+                out[j] += 1+                spare[j] -= 1+                rem -= 1+            i += 1+            guard += 1+    return out+++def _kmeans(xy: np.ndarray, k: int, rng: np.random.Generator, iters: int = 25) -> np.ndarray:+    n = xy.shape[0]+    k = int(min(max(k, 1), n))+    if k <= 1:+        return np.zeros(n, dtype=int)+    cent = xy[rng.choice(n, k, replace=False)].astype(np.float64)+    labels = np.zeros(n, dtype=int)+    for _ in range(iters):+        d = ((xy[:, None, :] - cent[None, :, :]) ** 2).sum(-1) if n * k <= 4_000_000 else _chunked_d2(xy, cent)+        new = np.argmin(d, axis=1)+        if np.array_equal(new, labels):+            labels = new+            break+        labels = new+        for j in range(k):+            m = labels == j+            if m.any():+                cent[j] = xy[m].mean(0)+    return labels+++def _chunked_d2(xy: np.ndarray, cent: np.ndarray, chunk: int = 4096) -> np.ndarray:+    out = np.empty((xy.shape[0], cent.shape[0]), dtype=np.float64)+    for s in range(0, xy.shape[0], chunk):+        blk = xy[s:s + chunk]+        out[s:s + chunk] = ((blk[:, None, :] - cent[None, :, :]) ** 2).sum(-1)+    return out+++def spatial_mix_indices(labels_a, labels_b, coords_a, coords_b, t: float, n: int,+                        rng: np.random.Generator, cells_per_bin: int = 200, ndim: int = 2):+    """Like sample.mix_indices, but per shared type draw spatially stratified.++    coords_a / coords_b are the aligned (already rescaled) coordinates used+    for the output cloud; only their xy columns drive the binning.+    Returns (ia, ib, bin_labels) where bin_labels is an int array aligned to+    the concatenation of picked a-rows then b-rows (for diagnostics), or None.+    """+    n_b = int(np.clip(int(round(float(t) * n)), 0, n))+    n_a = n - n_b+    labels_a = np.asarray(labels_a).astype(str)+    labels_b = np.asarray(labels_b).astype(str)+    alloc_a = _alloc_by_type(labels_a, n_a) if n_a > 0 else {}+    alloc_b = _alloc_by_type(labels_b, n_b) if n_b > 0 else {}+    idx_a_by = {lab: np.flatnonzero(labels_a == lab) for lab in alloc_a}+    idx_b_by = {lab: np.flatnonzero(labels_b == lab) for lab in alloc_b}++    picks_a, picks_b = [], []+    shared = sorted(set(alloc_a) & set(alloc_b))+    for lab in sorted(set(alloc_a) | set(alloc_b)):+        ia_t = idx_a_by.get(lab, np.array([], dtype=int))+        ib_t = idx_b_by.get(lab, np.array([], dtype=int))+        k_a = int(min(alloc_a.get(lab, 0), ia_t.size))+        k_b = int(min(alloc_b.get(lab, 0), ib_t.size))+        pooled = ia_t.size + ib_t.size+        stratify = (lab in shared and pooled >= MIN_TYPE_CELLS+                    and k_a + k_b < pooled and cells_per_bin > 0)+        if not stratify:+            if k_a > 0:+                picks_a.append(ia_t if k_a == ia_t.size else rng.choice(ia_t, k_a, replace=False))+            if k_b > 0:+                picks_b.append(ib_t if k_b == ib_t.size else rng.choice(ib_t, k_b, replace=False))+            continue+        xy = np.vstack([coords_a[ia_t, :ndim], coords_b[ib_t, :ndim]]).astype(np.float64)+        k = max(4, pooled // max(int(cells_per_bin), 1))+        bins = _kmeans(xy, k, rng)+        ba = bins[:ia_t.size]+        bb = bins[ia_t.size:]+        wa = np.bincount(ba, minlength=k).astype(np.float64)+        wb = np.bincount(bb, minlength=k).astype(np.float64)+        sa = _spread(k_a, wa, wa.astype(int))+        sb = _spread(k_b, wb, wb.astype(int))+        for j in range(k):+            if sa[j] > 0:+                pool = ia_t[ba == j]+                picks_a.append(pool if sa[j] == pool.size else rng.choice(pool, int(sa[j]), replace=False))+            if sb[j] > 0:+                pool = ib_t[bb == j]+                picks_b.append(pool if sb[j] == pool.size else rng.choice(pool, int(sb[j]), replace=False))+    ia = np.concatenate(picks_a) if picks_a else np.array([], dtype=int)+    ib = np.concatenate(picks_b) if picks_b else np.array([], dtype=int)+    return ia, ib+++def interpolate_spatial(stage_a, stage_b, t: float, params: dict, cells_per_bin: int = 200, ndim: int = 2):+    t = float(t)+    damp = float(params.get("scale_damp", 1.0))+    align = str(params.get("align", "procrustes"))+    rng = np.random.default_rng(int(params.get("seed", 0)))++    aligned_a, aligned_b, info = align_pair(stage_a.coords, stage_b.coords, stage_a.labels, stage_b.labels, align)+    rms_a = rms_radius(stage_a.coords)+    rms_b = rms_radius(stage_b.coords)+    target_rms = log_interp(rms_a, rms_b, t, damp)+    ca = scale_to_rms(aligned_a, target_rms)+    cb = scale_to_rms(aligned_b, target_rms)+    n = _limits(params, stage_a.n, stage_b.n, t, "interp")++    ia, ib = spatial_mix_indices(stage_a.labels, stage_b.labels, ca, cb, t, n, rng, cells_per_bin, ndim)++    expr_parts, coord_parts = [], []+    if ia.size:+        expr_parts.append(as_dense(stage_a.X, ia))+        coord_parts.append(np.asarray(ca[ia], dtype=np.float64))+    if ib.size:+        expr_parts.append(as_dense(stage_b.X, ib))+        coord_parts.append(np.asarray(cb[ib], dtype=np.float64))++    expr = _pack_expr(expr_parts)+    coords = _jitter(np.vstack(coord_parts), rng)+    coords = scale_to_rms(coords, target_rms)+    info.update(+        t=t, n=int(expr.shape[0]), rms_a=rms_a, rms_b=rms_b,+        target_rms=target_rms, out_rms=rms_radius(coords),+        coord="mix", expr="carry", blend="convex", scale_damp=damp,+        nearer="b" if t >= 0.5 else "a",+        n_from_a=int(ia.size), n_from_b=int(ib.size),+        spatial_stratify=True, cells_per_bin=cells_per_bin,+    )+    return expr, coords.astype(np.float32), info

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

用到的知识库条目

编号标题出处
k027Joint expression-geometry generation with relative geometrynotes/competition/03_solution_landscape.md
k003Fused Gromov-Wasserstein mapping for spatial snapshots10.1038/s41586-024-08453-2
k007Interval staging and held-out-window filtering of external datanotes/official/来件/virtualembryo.ai/rules.md

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

改了什么在父节点 2 的 mix 流程中,将两阶段共有细胞型的型内随机抽样替换为空间分层抽样(新增 solution/spatial_mix.py):每型合并两阶段 xy 坐标做 k-means 分 bin(CELLS_PER_BIN=100),bin 内按 (1-t,t) 配比无放回抽取,非共有型和 <20 细胞的小型退回随机抽样;run.py 提供 SPATIAL_STRATIFY=0 关闭对照(与父节点逐位一致,Engineer 已验证)。
各组分数的变化cell_state:噪声内:+0.25(66.70→66.95)
expression_change:噪声内:+0.47(63.90→64.37,T2 噪声约 1 分)
local_spatial:噪声内:+0.10(54.03→54.13),PLAN 预期的 >1 分提升未出现
shape_scale:噪声内:+0.39(53.37→53.76)
family_idT2HI-04
假设是否成立否
经验
  1. 在 proxy bracket(E8.25_late+E9.5→E8.75)上共有型仅覆盖约 20% 输出细胞时,只作用于共有型的抽样策略杠杆太小,难以移动 local_spatial 组分。
  2. 对两阶段细胞做空间均匀化混合(bin 内强制 (1-t,t) 配比)没有提升 local_spatial(+0.10,噪声内):真实发育数据里源阶段身份与空间位置本就相关(Moran's I 0.30),强行局部均匀化偏离真实结构;bin∈{100,200,400} 和 2D/3D 分箱全在父节点 ±0.6 分内,说明该方向对参数不敏感、可判死。
  3. 机制生效不等于组分提升:本节点 Moran's I 0.302→0.287、kNN 同源边 0.665→0.659 证明分层抽样确实改变了输出结构,但 local_spatial 评分不响应这种'局部混合比均匀化',提示 local_spatial 瓶颈更可能在空间占位(occupancy/variogram)而非阶段标签的邻域一致性。
  4. 带环境变量关闭开关(SPATIAL_STRATIFY=0 走父节点原路径)的对照实现方式廉价且可靠,值得沿用。
mechanism_active是
下一步建议
  1. 针对 local_spatial/shape_scale:放弃抽样空间均匀化,转向非共有型的空间占位——按目标阶段的空间密度对非共有型细胞重加权抽样,或在 mix 前对非共有型做坐标 warp,使输出云的区域占用(occupancy_dice)更贴近插值目标。
  2. 针对 local_spatial:尝试类型级坐标形状插值(对每型质心/协方差在 a、b 间按 t 插值后重排细胞坐标),即改坐标生成而非改选细胞;节点 5 已证刚体位移无效,需要型级非刚性变换。
  3. 若再验证抽样类机制,先在 final bracket(E8.25+E8.75→E8.5,31 个共有型、覆盖率更高)上估算共有型细胞占比,占比 <40% 时不要投入实现。

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

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

分析员

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

工程师

角色工程师 alibaba-token-plan-cn/qwen3.8-max
调用次数1 次
轮数25
工具调用共 29 次:bash 22、write 3、edit 3、read 1
用时9 分
token 数输入 51,603 · 输出 10,305 · 思考 7,721
任务(第一行)按计划修改程序:mix 空间分层抽样保邻域(spatial-stratified patch sampling)
最后的回答(摘录)
All verified. Final state: `solution/run.py` (mechanism on by default, bin=100) + `solution/spatial_mix.py` + `METHOD.md`.

**总结**

- **做了什么**:按 PLAN(T2HI-04)在父节点 mix 的型内随机抽样上实现空间分层抽样:共有型 xy k-means 分 bin(bin∈{100,200,400}、2D/3D 都试了),bin 内按 (1−t,t) 配比抽两阶段细胞;`SPATIAL_STRATIFY=0` 关闭对照与父节点同路径(逐位一致已验证)。机制确实生效:源阶段标签 Moran's I 0.302→0.287,kNN 同源边 0.665→0.659(理论 0.52)。
- **查分**:bin100 最优但 3 种子均值 58.99 vs 父 rank3 59.36,local_spatial 未提升(53.4–53.6 vs 54.0),shape_scale 略降。方向判负:proxy 上共有型只覆盖 20% 细胞,且真实 E8.75 的源阶段身份本就与空间位置相关,强行局部均匀化反而偏离。
- **下一步最值得试**:放弃抽样均匀化;local_spatial 瓶颈更可能在非共有型的空间占位(occupancy_dice/variogram),可试对非共有型按空间密度重加权抽样,或类型级坐标形状插值(改坐标生成而非选细胞)。
原始记录位置/home/spark-longxinyang/vec/runs/formal/20261003-004922-search-t2-heart-interp-g24/nodes/8/engineer.jsonl 125 KB
/home/spark-longxinyang/vec/runs/formal/20261003-004922-search-t2-heart-interp-g24/nodes/8/engineer.stderr

调研员

角色调研员 alibaba-token-plan-cn/qwen3.8-max
调用次数1 次
轮数1
工具调用共 0 次:—
用时1 分
token 数输入 6,912 · 输出 1,000 · 思考 3,891
任务(第一行)为节点 n8(改进,父节点 n2)写改进计划 PLAN.json
最后的回答(摘录)
```json
{
  "name": "mix 空间分层抽样保邻域(spatial-stratified patch sampling)",
  "motivation": "父节点 2 四组分中 local_spatial 54.03、shape_scale 53.37 最弱。当前 mix 按型随机抽样混合两阶段细胞,同一型内空间上随机散布,导致输出云中局部邻域由不同阶段、不同空间位置的细胞拼凑,破坏邻域一致性。节点 5(T2HI-02 质心位移)已证明刚体位移对 local_spatial 无效(-0.58,噪声内);节点 3(NN 匹配+坐标位移)生成失败。改变抽样策略(而非后处理位移)是尚未尝试的结构方向。",
  "approach": "在 mix 流程的 stratified sampling 步骤中,将每型内的随机抽样替换为空间分层抽样:\n1. 对齐+缩放后,对每个共有细胞型,用该型所有细胞(两阶段合并)的 xy 坐标做 k-means(k = max(4, n_type // 200)),得到空间 bin。\n2. 在每个 bin 内,按 (1-t, t) 比例分别从 stage_a、stage_b 抽样(四舍五入),保证每个空间区域都有正确比例的两阶段细胞。若某 bin 内某阶段细胞不足,从最近邻 bin 补。\n3. 表达和坐标随细胞一起走,与父节点相同。\n4. 非共有型(仅出现在一个阶段的型)保持原逻辑随机抽样。\n5. 单输入阶段退路(b is None):无 bracket,直接走父节点的 fallback 路径,空间分层不适用,行为不变。\n6. 关键参数:k_per_type 初始 200 细胞/bin;搜索范围 [100, 400]。用 vec-score 跑 1 次确认方向,若 local_spatial 提升 >1 分再跑 2 次确认。\n7. 先小样本(取前 3000 细胞)验证抽样逻辑正确、无空 bin,再全量。",
  "expected_groups": ["local_spatial", "shape_scale"],
  "risks": "1) 空间 bin 太小导致某些 bin 内细胞数不足,抽样退化;Engineer 应打印每 bin 实际抽样数,若 >10% bin 不足则增大 bin 尺寸。2) 分层后表达分布可能微变,影响 expression_change;但每型总数不变,预期影响 <0.5 分。3) 若 local_spatial 的评分主要取决于全局形状而非邻域,则改善有限;用第 1 次查分即可判断。4) k-means 在极小型(<20 细胞)上不稳定,这些型退回随机抽样。",
  "family_id": "T2HI-04",
  "mechanism": "将 mix 的型内随机抽样改为空间分层抽样:每型按坐标 k-means 分 bin,每 bin 内按 (1-t, t) 比例从两阶段取样,保证输出云在每个空间区域都保留正确的两阶段混合比,从而维持局部邻域的空间一致性。",
  "vs_constant_shift": "不施加任何位移或全局变换;不改变表达值。改变的是哪些细胞被选入输出云——从型内随机改为空间分层,使局部邻域由空间相近的同阶段细胞组成,而非随机拼凑。这是抽样策略的结构变化,不是对已有云的坐标平移或缩放。",
  "mechanism_evidence": "1) 计算输出云中每个细胞的源阶段标签(a/b),再算该标签在输出坐标上的 Moran's I(空间自相关);随机抽样下 I≈0,空间分层下每 bin 混合比一致,I 应更接近 0 或略负(无大团簇)。2) 计算输出云的 kNN 图中同源边比例,与理论值 t²+(1-t)² 比较。3) 四组分:预期 local_spatial 提升 >1 分,其余组分变化 <1 分。",
  "mechanism_off_control": "在 run.py 中设 SPATIAL_STRATIFY=False,退回父节点的型内随机抽样(同 seed),输出应与父节点逐位一致。对比两次查分:若 local_spatial 差异 <0.5 分,说明分层抽样未生效。"
}
```
原始记录位置/home/spark-longxinyang/vec/runs/formal/20261003-004922-search-t2-heart-interp-g24/nodes/8/researcher.jsonl 4 KB
/home/spark-longxinyang/vec/runs/formal/20261003-004922-search-t2-heart-interp-g24/nodes/8/researcher.stderr

审查员

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