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

总览 · ← 返回运行 20261003-153851-search-t1-D-smoke2h

节点 n7

OT-CFM 最小版:两输入视图在共享 30 维 PCA 上用 minibatch 精确 OT 配对训练条件流匹配速度场 v(z,t=1),从最新阶段的输出细胞按阻尼步长 τ=α·r 做 Euler 前推,位移解码回 HVG 并加到细胞自己的表达上;单输入视图退化为均匀抽样照抄。

运行?一次完整的自动搜索或 Agent 会话,有自己的锁定配置和证据包。20261003-153851-search-t1-D-smoke2h
父节点(种子,没有父节点)
子节点—
操作?种子:人写的起点;改进:在父节点上改;草稿:从头写;修复:修父节点的报错。草稿
状态程序报错
分数proxy10 52.75
审查未审查
用时?从运行开始到结束(或到现在)的挂钟时间。24 分
程序版本34b180229963fe2613eff448b3f9dcc92e097ce1 (programs.git)
备注crash on X3: home/spark-longxinyang/vec/runs/test/20261003-153851-search-t1-D-smoke2h/nodes/7/exec_X3/src/solution/run.py", line 171, in main log(f"train done loss {losses[-1]:.4f} cap_norm={cap_…

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

来自 programs.git 34b1802299:solution/METHOD.md

OT-CFM 最小版:两输入视图在共享 30 维 PCA 上用 minibatch 精确 OT 配对训练条件流匹配速度场 v(z,t=1),从最新阶段的输出细胞按阻尼步长 τ=α·r 做 Euler 前推,位移解码回 HVG 并加到细胞自己的表达上;单输入视图退化为均匀抽样照抄。

方法(family_id: ot_cfm)

  • 前处理:view_io.inputs_by_time(manifest) 取全部输入阶段,只用相对时间:dt_in = t_last − t_prev、dt_out = t_target − t_last、r = dt_out/dt_in。不读视图路径 / 名称 / 池大小 / 绝对时间,因此伪装视图(时间整体平移)预测不变。表达按视图契约已是 log1p(CP10k),不再重算。
  • HVG:两阶段 covered_mask 的交集内,按两阶段逐基因方差的均值取前 2,500 个(方差由稀疏矩阵 E[x²]−E[x]² 现场算,不稠密化全矩阵)。
  • 联合 PCA:两阶段各随机取 ≤4,000 细胞,减共同均值后 np.linalg.svd,取前 30 个载荷 W;z 再除以全局标量 s = sqrt(mean(z²))。
  • 训练(OT-CFM,arXiv:2302.00482 / arXiv:2210.02747):batch 256,batch 内用 scipy.optimize.linear_sum_assignment 对平方欧氏代价做精确 OT 配对(等价于 torchcfm 的 ExactOptimalTransportConditionalFlowMatcher 的耦合;本机 torchcfm 版本 API 不同,故直接用 scipy 实现同一配对,确定性);x_t=(1−t)x_0+t x_1+σε(σ=0.05),目标速度 u_t=x_1−x_0;v 为 MLP 31→256→256→256→30,SiLU,Adam lr 1e-3,4,000 步,时间上限 240 s,纯 CPU(EXECUTION.json {"gpu": false})。
  • 前推:t 固定为 1(只用训练区间端点的局部速度,不向 t>1 外推)。τ=α·r,α 默认 0.5,Euler K=8 步;每步位移范数上限 = 训练配对位移范数的 90% 分位(scaled 单位),总位移同样受此上限约束,防过冲。
  • 解码:Δx = (Δz·s)·Wᵀ 只作用于 2,500 个 HVG,加到该细胞自己的表达上(残差与型内共变保留)。非零元:max(0, x+Δx);零元只在 Δx > θ 时开启(θ = 全部正 Δx 的 95% 分位)。非 HVG 基因原样不动。
  • 输出细胞:从最新阶段均匀抽样,n_out = view_io.target_n_cells(manifest, n_last),组成不变。
  • 单输入退路:只有一个输入阶段时无法学速度场,输出 = 最新阶段均匀抽样照抄(生长重采样关闭)。该分支只由 len(inputs) 决定。
  • --ablate:mechanism/velocity0/velocity/zero/copy_last → Δz=0(同流程 copy_last);random → 保留每细胞位移范数、方向在 PCA 内均匀随机;per_type_mean → 每细胞速度换成本型 v 的均值(步长、上限、解码、θ 全同)。

知识来源

  • arXiv:2302.00482(Tong 等,OT-CFM:minibatch 精确 OT 配对 + 条件流匹配);arXiv:2210.02747(Lipman 等,Flow Matching:直线路径与常速目标)。二者均为方法学文献,不含任何保留阶段测量值。
  • 未使用 T1 发育事件表中的任何一行(本 draft 不生成新状态、不命名类型、不用标记程序),也未使用 external/、prior/。未使用 uns.celltype_palette。程序不含任何硬编码的阶段统计量、类型名单、比例或细胞数。

验证过什么

  • vec-check 通过:X3 视图(两输入,E8.75+E9.0 → E9.5)与 proxy10 视图(单输入 E8.5 → E9.5)。
  • X3 视图整流程约 90 s(4,000 步),proxy10 单输入分支约 2 s,内存远低于 28 GB。
  • 训练损失从 0.260 降到 0.171(500 步冒烟 → 4,000 步),说明速度场确实拟合了 OT 配对方向;cap_norm(训练配对位移 90% 分位,scaled)≈3.46,α=0.5 时平均位移 |Δz·s| ≈8.4(log 表达单位,HVG 上),上限基本不触发过冲。
  • 查分(A 半):
    • proxy10,单输入照抄分支:52.97(de_score 12.87 / de_direction 14.67 / mmd_u 15.50 / variogram 9.93;raw mmd_u 0.0406,raw variogram 0.0012)。与节点 5 的 proxy10 52.94 同一水平 —— 单输入分支就是照抄,符合预期,不含新信息。
    • X3,α=0.5、θ=95% 分位开启零元:50.14(de_score raw −0.1857 → 11.14 / de_direction raw −0.0173 → 12.34 / mmd_u raw 0.02667 → 17.66(skill 0.589,高于地板)/ variogram raw 0.001834 → 9.01(低于地板 10))。
  • 结论(诚实):前向 OT-CFM 场在 X3 上确实改善了细胞状态分布(mmd_u 0.02667,skill 0.589 > 0.5,比地板好),方向上 DE 两项在地板附近,variogram 明显被损害(0.001834 vs 地板对应值)。总分 50.14 低于当前最佳节点 5 的 X3 55.32,没有超过本 run 的最好节点。损害 variogram 的嫌疑是解码时对 2,500 个 HVG 做稠密加法并开启零元(nnz 1,282,705 → 1,292,940),破坏了基因间共变结构。

没验证什么 / 待后续节点

  • 因时间预算(本节点会话剩余额度用尽),只查到 α=0.5 主配置和 proxy10 的分;velocity=0 同流程对照、random、per_type_mean 三个对照,以及 α∈{0.25,1.0}、θ∈{90,99, 不开启零元}、多 seed 复跑都未完成查分,所以「机制相对同流程 copy_last 的净收益」尚未由查分证明,只能记为 unclear / 待测。--ablate 接口本身已实现(三个名字均可运行并改变输出)。
  • 最有价值的下一步:(1) 关掉零元开启(θ→∞)并减小 HVG 数 / 只解码位移最大的少数基因,看 variogram 是否回到地板以上 —— 这是本方案当前唯一明确为负的组分;(2) 把 α 与 r 的关系改成向 0 收缩(α≈0.25)并跑 v=0 对照,确认 mmd_u 的收益是真的;(3) 与节点 5 的后向祖先位移机制不同源,若 (1)(2) 后 mmd_u 收益保住而 variogram 不再受损,可与节点 5 的 addnz 解码 / 型内选择组合。

调研员的计划

名称OT-CFM 最小版:PCA 中 minibatch-OT 配对学状态相关速度场,从最新阶段受限步长前推,加回细胞残差
动机ot_cfm 族在本 run 还没有已完成的实现,节点 7 是该族第一个 draft。当前最佳是节点 5(local_ot 系,rank3 54.31)。它在 X3 上的收益几乎全部来自细胞状态:X3 的 mmd_u 从 0.03281 降到 0.02075,cell_state 从 49.21 升到 62.23。但 DE 没有学到:X3 de_score 为 -0.0649,低于地板;X3 de_direction 只有 0.0159。节点 5 的位移来自后向的祖先重心(输出细胞减去它在 E8.5 的祖先均值),而且在 30 个 PCA 模块上放大 3 倍外推,存在过冲风险。组成系(节点 3、4)在 X3 上都在地板附近(48.7)。ot_cfm 的不同之处是学一个平滑、随细胞状态变化的前向速度场 v(x),在最新阶段每个细胞自己的位置上取值,并用受限步长积分。这样检验“局部前向方向”能否比节点 5 的后向位移给出更好的方向(direction / de_recovery),同时保住 cell_state。X3 权重为 2,是主要信号;proxy10 只有单输入,只能照抄(节点 5 在 proxy10 上为 52.94)。
做法0)共用前处理。读视图中全部输入阶段,用 view_io.inputs_by_time 按时间排序。时间只用相对差:dt_in = t_last − t_prev,dt_out = t_target − t_last,r = dt_out/dt_in。伪装视图把所有时间平移 1 天,r 不变,所以预测必须与真实视图完全一致。不读任何尺子名、路径或池子大小。表达按视图契约做 log1p 归一化。在两个输入阶段的并集上选 2,500 个 HVG,在两阶段等量子样本上拟合联合 PCA(30 维,载荷 W)。
1)单输入退路。视图只有一个输入阶段时(proxy10),速度场无法训练,输出为从最新阶段均匀抽样的照抄,与节点 5 的单输入分支相同,生长重采样关闭。这个分支只由输入阶段数决定,与视图身份无关。final(官方 E8.5、E9.5,间隔 1 天,全胚,r=1)和 X3 都走两阶段分支。若某视图两阶段的基因集合不一致,缺失基因用另一阶段该基因的均值补齐,并且不进 HVG。
2)配对与训练。每个阶段随机取 4,000 个细胞(seed 控制),作为 x0(前一阶段)和 x1(最新阶段)的 PCA 坐标,按 PCA 全局标准差统一缩放。用 torchcfm 的 ExactOptimalTransportConditionalFlowMatcher(sigma=0.05),batch 256,在 batch 内做精确 OT 配对。只比较这一种配对方式,不做 SB 变体。速度场 v(x,t) 用 MLP:输入 31 维(30 维 PCA + t),3 层隐藏层,每层 256,SiLU 激活。Adam,学习率 1e-3,训练 4,000 步,设 5 分钟时间上限,在 GPU 或 CPU 上跑。先用 1,000 细胞、500 步做冒烟测试,再上全量。
3)前推,在端点做局部延续。t 固定为 1,即只用训练区间端点处的局部速度,不向训练范围外的 t 外推。从最新阶段的全部输出细胞 z_i 出发,用 Euler 法积分 K=8 步,总时长 τ = α·r。α 初值 0.5,搜索 {0.25, 0.5, 0.75, 1.0}。对每个细胞的总位移范数设上限:不超过 α·r 乘以训练配对位移范数的 90% 分位数,防止过冲。
4)解码,加回残差,并处理稀疏性。Δz_i = z_i(τ) − z_i;Δx_i = Δz_i·Wᵀ,只作用于 HVG,再乘回 PCA 缩放。输出表达 = 该细胞自己的原表达 + Δx_i,因此细胞残差和型内共变都完整保留。非零元直接相加后 clip≥0。零元只有当 Δx_ig > θ 时才开启(取值为 Δx_ig):θ 取全部正 Δx 的 95% 分位数,搜索 {90, 95, 99},也可以不开启。这避免了稠密小正值,同时允许少量基因由关变开。非 HV…
风险(1) 两个边际不能唯一确定动力学:场可能主要学到扩散,或者学到从 E8.5 分布到 E9.5 分布的“平均化”方向。早期信号:输出在 PCA 上的总方差相对输入明显变大,或者 mmd_u 变好而 DE 两项相对 v=0 不变或变差。(2) 过冲:α=1 时 mmd_u 和 variogram 恶化,零值比例明显下降。监控输出零值比例和每基因方差相对输入的变化。(3) 速度退化为每型常向量:用 mechanism_evidence 中的型内 R² 检查;若 R²>0.9,且与“每型均值速度”对照分差在噪声内,就如实写成该机制没有贡献状态依赖性。(4) 与节点 5 机制相似(都用 OT 配对),收益可能重合。若不超过节点 5,如实报告;这只是 draft,不靠组合冲分。(5) 训练不稳或超时:先跑冒烟测试,设训练时间上限,损失曲线要下降并趋于平稳。(6) 零元开启阈值太低会变成稠密加法,损害 variogram。对比 θ 档位时同时看 variogram 原始值。

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

对比:这个提交的上一版(种子程序:相对空仓库)。改动的文件:solution/EXECUTION.json +1 −0、solution/METHOD.md +33 −0、solution/run.py +249 −0

diff --git a/solution/EXECUTION.json b/solution/EXECUTION.jsonnew file mode 100644index 0000000..9d5125c--- /dev/null+++ b/solution/EXECUTION.json@@ -0,0 +1 @@+{"gpu": false}diff --git a/solution/METHOD.md b/solution/METHOD.mdnew file mode 100644index 0000000..4847a57--- /dev/null+++ b/solution/METHOD.md@@ -0,0 +1,33 @@+OT-CFM 最小版:两输入视图在共享 30 维 PCA 上用 minibatch 精确 OT 配对训练条件流匹配速度场 v(z,t=1),从最新阶段的输出细胞按阻尼步长 τ=α·r 做 Euler 前推,位移解码回 HVG 并加到细胞自己的表达上;单输入视图退化为均匀抽样照抄。++## 方法(family_id: ot_cfm)++- 前处理:`view_io.inputs_by_time(manifest)` 取全部输入阶段,只用相对时间:`dt_in = t_last − t_prev`、`dt_out = t_target − t_last`、`r = dt_out/dt_in`。不读视图路径 / 名称 / 池大小 / 绝对时间,因此伪装视图(时间整体平移)预测不变。表达按视图契约已是 log1p(CP10k),不再重算。+- HVG:两阶段 `covered_mask` 的交集内,按两阶段逐基因方差的均值取前 2,500 个(方差由稀疏矩阵 `E[x²]−E[x]²` 现场算,不稠密化全矩阵)。+- 联合 PCA:两阶段各随机取 ≤4,000 细胞,减共同均值后 `np.linalg.svd`,取前 30 个载荷 W;z 再除以全局标量 `s = sqrt(mean(z²))`。+- 训练(OT-CFM,arXiv:2302.00482 / arXiv:2210.02747):batch 256,batch 内用 `scipy.optimize.linear_sum_assignment` 对平方欧氏代价做**精确 OT 配对**(等价于 torchcfm 的 `ExactOptimalTransportConditionalFlowMatcher` 的耦合;本机 torchcfm 版本 API 不同,故直接用 scipy 实现同一配对,确定性);`x_t=(1−t)x_0+t x_1+σε`(σ=0.05),目标速度 `u_t=x_1−x_0`;v 为 MLP 31→256→256→256→30,SiLU,Adam lr 1e-3,4,000 步,时间上限 240 s,纯 CPU(`EXECUTION.json` `{"gpu": false}`)。+- 前推:t 固定为 1(只用训练区间端点的局部速度,不向 t>1 外推)。τ=α·r,α 默认 0.5,Euler K=8 步;每步位移范数上限 = 训练配对位移范数的 90% 分位(scaled 单位),总位移同样受此上限约束,防过冲。+- 解码:`Δx = (Δz·s)·Wᵀ` 只作用于 2,500 个 HVG,加到**该细胞自己**的表达上(残差与型内共变保留)。非零元:`max(0, x+Δx)`;零元只在 `Δx > θ` 时开启(θ = 全部正 Δx 的 95% 分位)。非 HVG 基因原样不动。+- 输出细胞:从最新阶段均匀抽样,`n_out = view_io.target_n_cells(manifest, n_last)`,组成不变。+- 单输入退路:只有一个输入阶段时无法学速度场,输出 = 最新阶段均匀抽样照抄(生长重采样关闭)。该分支只由 `len(inputs)` 决定。+- `--ablate`:`mechanism`/`velocity0`/`velocity`/`zero`/`copy_last` → Δz=0(同流程 copy_last);`random` → 保留每细胞位移范数、方向在 PCA 内均匀随机;`per_type_mean` → 每细胞速度换成本型 v 的均值(步长、上限、解码、θ 全同)。++## 知识来源++- arXiv:2302.00482(Tong 等,OT-CFM:minibatch 精确 OT 配对 + 条件流匹配);arXiv:2210.02747(Lipman 等,Flow Matching:直线路径与常速目标)。二者均为方法学文献,不含任何保留阶段测量值。+- 未使用 T1 发育事件表中的任何一行(本 draft 不生成新状态、不命名类型、不用标记程序),也未使用 `external/`、`prior/`。未使用 `uns.celltype_palette`。程序不含任何硬编码的阶段统计量、类型名单、比例或细胞数。++## 验证过什么++- `vec-check` 通过:X3 视图(两输入,E8.75+E9.0 → E9.5)与 proxy10 视图(单输入 E8.5 → E9.5)。+- X3 视图整流程约 90 s(4,000 步),proxy10 单输入分支约 2 s,内存远低于 28 GB。+- 训练损失从 0.260 降到 0.171(500 步冒烟 → 4,000 步),说明速度场确实拟合了 OT 配对方向;`cap_norm`(训练配对位移 90% 分位,scaled)≈3.46,α=0.5 时平均位移 |Δz·s| ≈8.4(log 表达单位,HVG 上),上限基本不触发过冲。+- 查分(A 半):+  - `proxy10`,单输入照抄分支:**52.97**(de_score 12.87 / de_direction 14.67 / mmd_u 15.50 / variogram 9.93;raw mmd_u 0.0406,raw variogram 0.0012)。与节点 5 的 proxy10 52.94 同一水平 —— 单输入分支就是照抄,符合预期,不含新信息。+  - `X3`,α=0.5、θ=95% 分位开启零元:**50.14**(de_score raw −0.1857 → 11.14 / de_direction raw −0.0173 → 12.34 / mmd_u raw 0.02667 → 17.66(skill 0.589,高于地板)/ variogram raw 0.001834 → 9.01(低于地板 10))。+- 结论(诚实):前向 OT-CFM 场在 X3 上**确实改善了细胞状态分布**(mmd_u 0.02667,skill 0.589 > 0.5,比地板好),方向上 DE 两项在地板附近,`variogram` 明显被损害(0.001834 vs 地板对应值)。总分 50.14 低于当前最佳节点 5 的 X3 55.32,**没有超过本 run 的最好节点**。损害 variogram 的嫌疑是解码时对 2,500 个 HVG 做稠密加法并开启零元(nnz 1,282,705 → 1,292,940),破坏了基因间共变结构。++## 没验证什么 / 待后续节点++- 因时间预算(本节点会话剩余额度用尽),只查到 α=0.5 主配置和 proxy10 的分;`velocity=0` 同流程对照、`random`、`per_type_mean` 三个对照,以及 α∈{0.25,1.0}、θ∈{90,99, 不开启零元}、多 seed 复跑都**未完成查分**,所以「机制相对同流程 copy_last 的净收益」尚未由查分证明,只能记为 unclear / 待测。`--ablate` 接口本身已实现(三个名字均可运行并改变输出)。+- 最有价值的下一步:(1) 关掉零元开启(θ→∞)并减小 HVG 数 / 只解码位移最大的少数基因,看 variogram 是否回到地板以上 —— 这是本方案当前唯一明确为负的组分;(2) 把 α 与 r 的关系改成向 0 收缩(α≈0.25)并跑 v=0 对照,确认 mmd_u 的收益是真的;(3) 与节点 5 的后向祖先位移机制**不同源**,若 (1)(2) 后 mmd_u 收益保住而 variogram 不再受损,可与节点 5 的 addnz 解码 / 型内选择组合。diff --git a/solution/run.py b/solution/run.pynew file mode 100644index 0000000..9e77cf1--- /dev/null+++ b/solution/run.py@@ -0,0 +1,249 @@+"""OT-CFM minimal draft (family ot_cfm).++Two input stages: learn a state-dependent velocity field v(z, t=1) in a shared+30-dim PCA space with minibatch exact-OT-paired conditional flow matching, then+integrate the output cells of the latest stage forward with a damped step+tau = alpha * r (r = dt_out / dt_in), decode the PCA displacement back onto the+HVG block and add it to each cell's own expression (residuals preserved).++One input stage: no velocity can be trained -> uniform-sample copy of the latest+stage (same fallback as the local_ot seeds).++Only relative time differences are used, so the prediction is invariant to a+uniform shift of all stage times.+"""++from __future__ import annotations++import argparse+import time++import numpy as np+from scipy import sparse+from scipy.optimize import linear_sum_assignment++from src.task1_temporal import view_io+++def log(*a):+    print(*a, flush=True)+++def parse_args():+    p = argparse.ArgumentParser()+    p.add_argument("--data", required=True)+    p.add_argument("--out", required=True)+    p.add_argument("--seed", type=int, default=0)+    p.add_argument("--ablate", default=None)+    p.add_argument("--alpha", type=float, default=0.5)+    p.add_argument("--theta-q", type=float, default=0.95, dest="theta_q")+    p.add_argument("--n-hvg", type=int, default=2500)+    p.add_argument("--n-pc", type=int, default=30)+    p.add_argument("--n-train", type=int, default=4000)+    p.add_argument("--steps", type=int, default=4000)+    p.add_argument("--batch", type=int, default=256)+    p.add_argument("--sigma", type=float, default=0.05)+    p.add_argument("--lr", type=float, default=1e-3)+    p.add_argument("--euler-k", type=int, default=8)+    p.add_argument("--time-cap", type=float, default=240.0)+    return p.parse_args()+++def per_gene_var(X: sparse.csr_matrix) -> np.ndarray:+    n = X.shape[0]+    m1 = np.asarray(X.mean(axis=0)).ravel()+    m2 = np.asarray(X.multiply(X).mean(axis=0)).ravel()+    return np.maximum(m2 - m1 * m1, 0.0), m1+++def main():+    args = parse_args()+    t_start = time.time()+    view = args.data+    manifest = view_io.load_manifest(view)+    genes = view_io.panel_genes(view, manifest)+    n_genes = len(genes)+    inputs = view_io.inputs_by_time(manifest)+    last = inputs[-1]+    rng = np.random.default_rng(args.seed)++    a_last = view_io.read_stage(view, last, genes, missing="fill")+    labels_last = np.asarray([str(v) for v in view_io.labels_of(a_last)])+    n_last = a_last.n_obs+    X_last = a_last.X.tocsr()++    n_out = view_io.target_n_cells(manifest, n_last)+    rows_out = view_io.sample_rows(n_last, n_out, rng)+    log(f"inputs={[e['stage'] for e in inputs]} n_last={n_last} n_out={n_out}")++    coords = None+    if manifest.get("needs_coords"):+        coords = rng.uniform(-1.0, 1.0, size=(n_out, 3)).astype(np.float32)++    dX = None  # dense displacement over HVG columns, or None (copy_last)+    hvg_idx = None++    if len(inputs) >= 2:+        prev = inputs[-2]+        dt_in = float(last["time"]) - float(prev["time"])+        dt_out = float(manifest["target"]["time"]) - float(last["time"])+        r = (dt_out / dt_in) if dt_in > 0 else 0.0+        a_prev = view_io.read_stage(view, prev, genes, missing="fill")+        cov = view_io.covered_mask(view, last, genes) & view_io.covered_mask(view, prev, genes)+        X_prev = a_prev.X.tocsr()++        v_last, m_last = per_gene_var(X_last)+        v_prev, m_prev = per_gene_var(X_prev)+        cand = np.flatnonzero(cov)+        score = 0.5 * (v_last[cand] + v_prev[cand])+        keep = cand[np.argsort(-score)[: args.n_hvg]]+        keep = np.sort(keep)+        hvg_idx = keep+        log(f"r={r:.3f} hvg={len(keep)}")++        sub_last = view_io.sample_rows(n_last, min(args.n_train, n_last), rng)+        sub_prev = view_io.sample_rows(a_prev.n_obs, min(args.n_train, a_prev.n_obs), rng)+        A1 = np.asarray(X_last[sub_last][:, keep].todense(), dtype=np.float32)+        A0 = np.asarray(X_prev[sub_prev][:, keep].todense(), dtype=np.float32)+        mu = 0.5 * (A0.mean(0) + A1.mean(0))+        A0 -= mu+        A1 -= mu+        # joint PCA on the two-stage union+        both = np.concatenate([A0, A1], axis=0)+        U, S, Vt = np.linalg.svd(both, full_matrices=False)+        k = min(args.n_pc, Vt.shape[0])+        W = np.ascontiguousarray(Vt[:k].T.astype(np.float32))  # (hvg, k)+        z0 = A0 @ W+        z1 = A1 @ W+        s = float(np.sqrt((z0 ** 2).mean() + (z1 ** 2).mean())) + 1e-8+        del both, A0, A1, U, S, Vt+        z0s = (z0 / s).astype(np.float32)+        z1s = (z1 / s).astype(np.float32)+        log(f"pca k={k} scale={s:.4f} t={time.time()-t_start:.0f}s")++        import torch++        torch.manual_seed(args.seed)+        torch.use_deterministic_algorithms(False)+        dev = torch.device("cpu")+        t0 = torch.from_numpy(z0s).to(dev)+        t1 = torch.from_numpy(z1s).to(dev)+        dim = z0s.shape[1]++        net = torch.nn.Sequential(+            torch.nn.Linear(dim + 1, 256), torch.nn.SiLU(),+            torch.nn.Linear(256, 256), torch.nn.SiLU(),+            torch.nn.Linear(256, 256), torch.nn.SiLU(),+            torch.nn.Linear(256, dim),+        ).to(dev)+        opt = torch.optim.Adam(net.parameters(), lr=args.lr)+        g = torch.Generator(device="cpu").manual_seed(args.seed)+        pair_norms = []+        losses = []+        b = min(args.batch, t0.shape[0], t1.shape[0])+        deadline = t_start + args.time_cap+        for step in range(args.steps):+            if time.time() > deadline:+                log(f"time cap at step {step}")+                break+            i0 = torch.from_numpy(rng.integers(0, t0.shape[0], size=b)).to(dev)+            i1 = torch.from_numpy(rng.integers(0, t1.shape[0], size=b)).to(dev)+            x0 = t0[i0]+            x1 = t1[i1]+            with torch.no_grad():+                cost = torch.cdist(x0, x1).pow(2).cpu().numpy().astype(np.float64)+            _, perm = linear_sum_assignment(cost)+            x1 = x1[torch.from_numpy(perm).to(dev)]+            tt = torch.rand(b, generator=g, device=dev).unsqueeze(1)+            eps = torch.randn(b, dim, generator=g, device=dev)+            xt = (1.0 - tt) * x0 + tt * x1 + args.sigma * eps+            ut = x1 - x0+            vt = net(torch.cat([xt, tt], dim=1))+            loss = ((vt - ut) ** 2).mean()+            opt.zero_grad(set_to_none=True)+            loss.backward()+            opt.step()+            losses.append(float(loss))+            if step % 500 == 0:+                pair_norms.append(np.linalg.norm(ut.detach().numpy(), axis=1))+                log(f"step {step} loss {loss.item():.4f} t={time.time()-t_start:.0f}s")+        cap_norm = float(np.quantile(np.concatenate(pair_norms), 0.90)) if pair_norms else 0.0+        log(f"train done loss {losses[-1]:.4f} cap_norm={cap_norm:.3f} t={time.time()-t_start:.0f}s")++        # ---- integrate the output cells forward from the latest stage ----+        Xo = np.asarray(X_last[rows_out][:, keep].todense(), dtype=np.float32) - mu+        zo = torch.from_numpy((Xo @ W).astype(np.float32) / s).to(dev)+        tau = args.alpha * r+        net.eval()+        ab = (args.ablate or "").lower()++        if ab in ("mechanism", "velocity0", "velocity_0", "zero", "copy_last", "velocity") or tau <= 0:+            dz = torch.zeros_like(zo)+        elif ab == "per_type_mean":+            lab = labels_last[rows_out]+            dz = torch.zeros_like(zo)+            with torch.no_grad():+                v = net(torch.cat([zo, torch.ones(zo.shape[0], 1, device=dev)], 1))+            for name in np.unique(lab):+                sel = torch.from_numpy(np.flatnonzero(lab == name)).to(dev)+                dz[sel] = v[sel].mean(0, keepdim=True).expand(sel.shape[0], -1)+            dz = dz * tau+            nn_ = dz.norm(dim=1, keepdim=True)+            dz = torch.where(nn_ > cap_norm, dz * (cap_norm / nn_.clamp(min=1e-9)), dz)+        else:+            with torch.no_grad():+                if ab == "random":+                    d = torch.randn(zo.shape, generator=g, device=dev)+                    d = d / d.norm(dim=1, keepdim=True).clamp(min=1e-9)+                    ref = net(torch.cat([zo, torch.ones(zo.shape[0], 1, device=dev)], 1))+                    step_len = tau * ref.norm(dim=1, keepdim=True) / max(args.euler_k, 1)+                    step_len = step_len.clamp(max=cap_norm / max(args.euler_k, 1))+                    dz = d * (step_len * args.euler_k)+                else:+                    z = zo.clone()+                    h = tau / max(args.euler_k, 1)+                    dz = torch.zeros_like(z)+                    ones = torch.ones(z.shape[0], 1, device=dev)+                    for _ in range(args.euler_k):+                        vv = net(torch.cat([z, ones], 1))+                        st = vv * h+                        nrm = st.norm(dim=1, keepdim=True)+                        st = torch.where(nrm > cap_norm, st * (cap_norm / nrm.clamp(min=1e-9)), st)+                        dz = dz + st+                        z = z + st+                    nn_ = dz.norm(dim=1, keepdim=True)+                    dz = torch.where(nn_ > cap_norm, dz * (cap_norm / nn_.clamp(min=1e-9)), dz)+        dz_np = dz.detach().numpy().astype(np.float32) * s+        dX = (dz_np @ W.T)  # (n_out, n_hvg)+        log(f"displacement mean |dz|={np.linalg.norm(dz_np, axis=1).mean():.4f} t={time.time()-t_start:.0f}s")+    else:+        log("single input stage: uniform-sample copy of the latest stage")++    X_out = X_last[rows_out]+    if dX is None:+        Xf = X_out.copy()+    else:+        pos = dX[dX > 0]+        theta = float(np.quantile(pos, args.theta_q)) if pos.size else np.inf+        block = np.asarray(X_out[:, hvg_idx].todense(), dtype=np.float32)+        nz = block != 0+        new = np.where(nz, np.maximum(block + dX, 0.0), np.where(dX > theta, dX, 0.0))+        new = np.nan_to_num(new, nan=0.0, posinf=0.0, neginf=0.0).astype(np.float32)+        bm = sparse.csr_matrix(new)+        coo = bm.tocoo()+        hvg_cols = hvg_idx[coo.col]+        other_idx = np.setdiff1d(np.arange(n_genes), hvg_idx)+        Xo = X_out.tocsc()[:, other_idx].tocoo()+        rows_all = np.concatenate([coo.row, Xo.row])+        cols_all = np.concatenate([hvg_cols, other_idx[Xo.col]])+        data_all = np.concatenate([coo.data, Xo.data]).astype(np.float32)+        Xf = sparse.csr_matrix((data_all, (rows_all, cols_all)), shape=(n_out, n_genes))+        Xf.eliminate_zeros()+        log(f"theta={theta:.4f} nnz {X_out.nnz} -> {Xf.nnz}")++    view_io.write_prediction(Xf, genes, args.out, coords=coords, seed=args.seed)+    log(f"wrote {args.out} t={time.time()-t_start:.0f}s")+++if __name__ == "__main__":+    main()

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

用到的知识库条目

编号标题出处
k034Flow matching and Schrödinger bridges offline: torchcfm (OT-CFM, SF2M), metric FM, DSBarXiv:2302.00482 (OT-CFM, Tong et al.); arXiv:2307.03672 ([SF]2M); arXiv:2405.14780 (metric flow matching); arXiv:2106.01357 (DSB)
k031Offline OT toolkit in the sandbox: moscot TemporalProblem, wot OTModel, POT, geomloss10.1038/s41586-024-08453-2 (moscot); 10.1016/j.cell.2019.01.006 (Waddington-OT)
k009Conditional / OT flow matching for population transportarXiv:2210.02747 (Lipman et al., Flow Matching for Generative Modeling)

计划里引用的来源

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

改了什么新建 solution/run.py,实现 OT-CFM 最小版。两输入视图的流程是:取 2,500 个 HVG,做联合 30 维 PCA,batch 内用 scipy linear_sum_assignment 做精确 OT 配对,训练 MLP 速度场(4,000 步,时间上限 240 s,纯 CPU),在 t=1 处用 8 步 Euler 前推,步长受限,然后把位移解码回 HVG,加到细胞自身表达上,零元按 θ=95% 分位开启。单输入视图(proxy10)退化为从最新阶段均匀抽样照抄。另外提供 --ablate 开关(velocity0 / random / per_type_mean)。
各组分数的变化cell_state:proxy10 变坏:mmd_u 0.02981→0.0405,得分 19.55→15.53(−4.02),已降到地板附近(地板 15.00)。原因是 proxy10 走照抄分支,失去了 node 3 的组成调整,与 OT-CFM 机制无关
covariation:proxy10 变坏:variogram 0.001028→0.001233,得分 11.19→9.92(−1.27),略低于地板 10.00。同样只是照抄分支的结果,与机制无关
de_recovery:proxy10 变坏:de_score 0.2679→0.01639,得分 15.11→12.63(−2.48),落到地板 12.50 附近。属于照抄分支的结果
direction:proxy10 变坏:de_direction 0.36→0.1785,得分 17.82→14.67(−3.15)。属于照抄分支的结果。proxy10 照抄合计 52.75,与 Engineer 自报的 52.97、节点 5 的 52.94 处于同一水平,差异在噪声内
榜分:无(X3 崩溃,没有榜分)。表中只有 proxy10 的分解,四项得分合计 52.75;而对照 node 3 的四项合计 63.67,与它表中的总分 53.69 对不上,说明对照的榜分是多把尺子的汇总,两边不可直接相减
失败原因X3(两输入)在隔离重跑中崩溃,报 IndexError: losses[-1] list index out of range。直接原因是训练截止时间 deadline = t_start + time_cap(240 s),从程序启动就开始计时,而不是从训练开始计时。隔离环境是 aarch64 纯 CPU,stderr 里还有 cpuinfo 报错,比 Engineer 本地慢得多:读数据、算 HVG、做 SVD 已经超过 240 s(总耗时 275.4 s),训练循环在第 0 步就因 time cap 退出。于是 losses 和 pair_norms 都为空,取 losses[-1] 越界。即使不崩溃,cap_norm 也会是 0,模型也是未训练的。Engineer 在 METHOD.md 中自报的 X3 50.14(约 90 s 跑完)在本次重跑中没有复现,不能当作证据。
family_idot_cfm
假设是否成立unclear
经验
  1. 给训练设时间上限时,计时起点应该是训练开始,并保证最少训练步数(如 ≥300 步)。不能从程序启动计时:评测环境(aarch64 纯 CPU)的前处理可能比本地慢 3 倍以上,会把训练预算整个吃掉。
  2. 任何依赖循环结果的统计量(losses[-1]、quantile(pair_norms))都要对空列表做保护;循环一步没跑时应报错退出或回退到 v=0,不能静默使用未训练的网络和 cap_norm=0。
  3. Engineer 本地测得的耗时和分数不能代表隔离重跑:本节点本地约 90 s,重跑 275 s 并崩溃。时间敏感的参数要按 3 倍余量设置。
  4. 单输入视图(proxy10)走照抄分支时,四项都会回到地板附近(合计约 52.75–52.97),相对组成系 node 3 会掉分。这个尺子上的变化不能用来评价两输入机制。
mechanism_activeunclear
下一步建议
  1. [全部组,先修崩溃] 把 deadline 改成训练开始后计时,上限约 150 s,至少训练 500 步,并给 losses / pair_norms 加空列表保护。同时降低前处理和训练开销:n_train 2000、steps 2000、torch.set_num_threads 设为可用核数。然后在 X3 上重跑 α=0.5 主配置。
  2. [cell_state / 机制判定] 修好后,在 X3 上用同一 seed 跑 --ablate velocity0、random、per_type_mean 三个对照,并加跑 seed 1、2,逐项记录 mmd_u 原始值和得分。只有完整场同时优于 velocity0 和 random 超过 2 分,才算机制生效。
  3. [covariation] 把零元开启关掉(θ_q=1.0,即 θ=∞),只解码 |Δx| 最大的约 300–500 个 HVG,同时试 α=0.25,看 variogram 能否回到地板 10 分以上。Engineer 自报 variogram 0.001834 / 9.01,尚未验证。

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

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

分析员

角色?调研员写计划、工程师改代码、分析员解读分数、审查员检查作弊。分析员 vec-relay-claude-cli/claude-opus-5-5
调用次数1 次
轮数?大模型一共回复了几轮(每轮可以调用多个工具)。1
工具调用?大模型调用读文件、执行命令等工具的次数,按工具名统计。共 0 次:—
用时不到 1 分
token 数?输入 / 输出(含思考)的 token 数,来自接口返回的用量。输入 2 · 输出 2,921 · 思考 0
任务(第一行)分析节点 n7 的打分结果,写 ANALYSIS.json
最后的回答(摘录)
 node 3 的四项合计 63.67,与它表中的总分 53.69 对不上,说明对照的榜分是多把尺子的汇总,两边不可直接相减",
  "cell_state": "proxy10 变坏:mmd_u 0.02981→0.0405,得分 19.55→15.53(−4.02),已降到地板附近(地板 15.00)。原因是 proxy10 走照抄分支,失去了 node 3 的组成调整,与 OT-CFM 机制无关",
  "covariation": "proxy10 变坏:variogram 0.001028→0.001233,得分 11.19→9.92(−1.27),略低于地板 10.00。同样只是照抄分支的结果,与机制无关",
  "de_recovery": "proxy10 变坏:de_score 0.2679→0.01639,得分 15.11→12.63(−2.48),落到地板 12.50 附近。属于照抄分支的结果",
  "direction": "proxy10 变坏:de_direction 0.36→0.1785,得分 17.82→14.67(−3.15)。属于照抄分支的结果。proxy10 照抄合计 52.75,与 Engineer 自报的 52.97、节点 5 的 52.94 处于同一水平,差异在噪声内"
 },
 "hypothesis_supported": "unclear",
 "failure_reason": "X3(两输入)在隔离重跑中崩溃,报 IndexError: losses[-1] list index out of range。直接原因是训练截止时间 deadline = t_start + time_cap(240 s),从程序启动就开始计时,而不是从训练开始计时。隔离环境是 aarch64 纯 CPU,stderr 里还有 cpuinfo 报错,比 Engineer 本地慢得多:读数据、算 HVG、做 SVD 已经超过 240 s(总耗时 275.4 s),训练循环在第 0 步就因 time cap 退出。于是 losses 和 pair_norms 都为空,取 losses[-1] 越界。即使不崩溃,cap_norm 也会是 0,模型也是未训练的。Engineer 在 METHOD.md 中自报的 X3 50.14(约 90 s 跑完)在本次重跑中没有复现,不能当作证据。",
 "lessons": [
  "给训练设时间上限时,计时起点应该是训练开始,并保证最少训练步数(如 ≥300 步)。不能从程序启动计时:评测环境(aarch64 纯 CPU)的前处理可能比本地慢 3 倍以上,会把训练预算整个吃掉。",
  "任何依赖循环结果的统计量(losses[-1]、quantile(pair_norms))都要对空列表做保护;循环一步没跑时应报错退出或回退到 v=0,不能静默使用未训练的网络和 cap_norm=0。",
  "Engineer 本地测得的耗时和分数不能代表隔离重跑:本节点本地约 90 s,重跑 275 s 并崩溃。时间敏感的参数要按 3 倍余量设置。",
  "单输入视图(proxy10)走照抄分支时,四项都会回到地板附近(合计约 52.75–52.97),相对组成系 node 3 会掉分。这个尺子上的变化不能用来评价两输入机制。"
 ],
 "next_suggestions": [
  "[全部组,先修崩溃] 把 deadline 改成训练开始后计时,上限约 150 s,至少训练 500 步,并给 losses / pair_norms 加空列表保护。同时降低前处理和训练开销:n_train 2000、steps 2000、torch.set_num_threads 设为可用核数。然后在 X3 上重跑 α=0.5 主配置。",
  "[cell_state / 机制判定] 修好后,在 X3 上用同一 seed 跑 --ablate velocity0、random、per_type_mean 三个对照,并加跑 seed 1、2,逐项记录 mmd_u 原始值和得分。只有完整场同时优于 velocity0 和 random 超过 2 分,才算机制生效。",
  "[covariation] 把零元开启关掉(θ_q=1.0,即 θ=∞),只解码 |Δx| 最大的约 300–500 个 HVG,同时试 α=0.25,看 variogram 能否回到地板 10 分以上。Engineer 自报 variogram 0.001834 / 9.01,尚未验证。"
 ],
 "family_id": "ot_cfm",
 "mechanism_active": "unclear"
}
原始记录位置?完整对话记录在 Spark 上的文件路径;看板不复制原始内容。/home/spark-longxinyang/vec/runs/test/20261003-153851-search-t1-D-smoke2h/nodes/7/analyst.jsonl 5 KB

工程师

角色工程师 alibaba-token-plan-cn/qwen3.8-max
调用次数1 次(失败 1 次)
轮数24
工具调用共 28 次:bash 25、write 2、edit 1
用时18 分
token 数输入 54,710 · 输出 9,763 · 思考 6,875
任务(第一行)按计划修改程序:OT-CFM 最小版:PCA 中 minibatch-OT 配对学状态相关速度场,从最新阶段受限步长前推,加回细胞残差
报错exit 143:
最后的回答(摘录)—
原始记录位置/home/spark-longxinyang/vec/runs/test/20261003-153851-search-t1-D-smoke2h/nodes/7/engineer.jsonl 169 KB
/home/spark-longxinyang/vec/runs/test/20261003-153851-search-t1-D-smoke2h/nodes/7/engineer.stderr

调研员

角色调研员 vec-relay-claude-cli/claude-opus-5-5
调用次数1 次
轮数1
工具调用共 0 次:—
用时1 分
token 数输入 2 · 输出 5,398 · 思考 0
任务(第一行)为节点 n7(草稿,父节点 nNone)写改进计划 PLAN.json,思路 ot_cfm
最后的回答(摘录)
始值。总分提升小于 2 分时,以多 seed 的配对差为准。",
 "expected_groups": ["cell_state", "direction", "de_recovery"],
 "risks": "(1) 两个边际不能唯一确定动力学:场可能主要学到扩散,或者学到从 E8.5 分布到 E9.5 分布的“平均化”方向。早期信号:输出在 PCA 上的总方差相对输入明显变大,或者 mmd_u 变好而 DE 两项相对 v=0 不变或变差。(2) 过冲:α=1 时 mmd_u 和 variogram 恶化,零值比例明显下降。监控输出零值比例和每基因方差相对输入的变化。(3) 速度退化为每型常向量:用 mechanism_evidence 中的型内 R² 检查;若 R²>0.9,且与“每型均值速度”对照分差在噪声内,就如实写成该机制没有贡献状态依赖性。(4) 与节点 5 机制相似(都用 OT 配对),收益可能重合。若不超过节点 5,如实报告;这只是 draft,不靠组合冲分。(5) 训练不稳或超时:先跑冒烟测试,设训练时间上限,损失曲线要下降并趋于平稳。(6) 零元开启阈值太低会变成稠密加法,损害 variogram。对比 θ 档位时同时看 variogram 原始值。",
 "family_id": "ot_cfm",
 "mechanism": "在共享 30 维 PCA 中,用 minibatch 精确 OT 配对的条件流匹配,学一个随细胞状态变化的速度场 v(x, t=1)。从最新阶段的每个真实细胞出发,按步长 α·r 受限积分,把 PCA 位移解码回 HVG,加到该细胞自己的表达上,残差得以保留。",
 "vs_constant_shift": "每型常数位移给同一类型的所有细胞同一个向量;全局组成重加权只改各型比例,不改表达。本方法的位移是 v 在每个细胞自身坐标上的取值,经多步积分后随状态弯曲,同一类型内不同细胞有不同的方向和幅度;组成完全不变(均匀抽样)。它与节点 5 也不同:节点 5 用后向的祖先重心位移,并且整体放大 3 倍;本方法用前向的平滑场,带步长上限,并在训练端点 t=1 做局部延续。",
 "mechanism_evidence": "报告以下几项。(a) 速度的型内离散程度:按源细胞标签分组,计算 v 的“型均值解释方差比” R² = 1 − Σ型内方差/总方差,以及每个细胞的 v 与本型均值速度的余弦相似度分布(中位数和 10% 分位)。(b) 改变了哪些细胞:位移范数在各型中的分布,变化最大的 20 个 HVG 及其符号(只作描述,不用于调参)。(c) 扩散检查:输出相对输入在 PCA 上的总方差比;输出与 v=0 输出之间的平均配对距离,以及它们各自到最新输入阶段的能量距离。(d) 四组分相对 v=0 的变化,按 score_parts 分开报告 mmd_u、variogram、de_score、de_direction 的原始值和 points。(e) 训练损失曲线;在一个留出的配对子集上,学到的 v 与配对方向 x1−x0 的余弦,应明显大于 0。",
 "mechanism_off_control": "用同一程序和同一 seed 加 --ablate 开关,跑三个对照。(1) velocity=0:输出应与同流程 copy_last 逐字节相同;若主配置的输出也与它相同,说明机制没有运行。(2) random:每个细胞的位移换成范数相同、方向在 PCA 中均匀随机的向量,用同一解码和同一零元阈值,检验收益是否只来自变宽或扩散;预期 mmd_u 和 variogram 不变或变差,DE 两项落回噪声。(3) per-type-mean:每个细胞的速度换成本型 v 的均值,即每型常数位移,步长相同,检验状态依赖性的贡献;预期 cell_state 和 covariation 不如完整场。只有完整场在 X3 上同时优于 (1) 和 (2),并且分差超过噪声(多 seed 确认),才算机制有效;若与 (3) 只差噪声内,就写成“方向有效但状态依赖性未体现”。",
 "sources": ["arXiv:2302.00482 — Tong 等 OT-CFM:minibatch 精确 OT 配对 + 条件流匹配损失,torchcfm 的 ExactOptimalTransportConditionalFlowMatcher(经知识条目 k034)", "arXiv:2210.02747 — Lipman 等 Flow Matching:直线条件路径 x_t=(1−t)x0+t·x1、目标速度 x1−x0;只有两个时间点时场识别较弱,需要对外推步长做阻尼(经知识条目 k009)"]}
```
原始记录位置/home/spark-longxinyang/vec/runs/test/20261003-153851-search-t1-D-smoke2h/nodes/7/researcher.jsonl 9 KB