# TLUSTY 收敛性分析与修复(2026-08-08 ~ 08-09) > 本文完整记录了对 `sdB_cno` 工作流 1305 个失败网格点的根因分析、实测验证过程、 > 以及最终落地的代码改动。所有 tlusty/synspec 实测均使用**生产二进制** > `dcts/assets/tlusty_static`(md5=`14cd144b...`),而非开发目录的 `tlusty/tlusty.exe` > (md5=`9b249e00...`)——两者编译版本不同,数值行为有差异。 --- ## 1. 数据库分析(`dcts.db`) ### 1.1 真实规模(纠正初期统计错误) | 维度 | 数量 | |------|------| | grid_points 总数 | **9216**(9 Teff × 4 logg × 4 logHe × 4 logC × 4 logN × 4 logO) | | ✅ completed | **7911(85.8%)** | | ❌ failed | **1305(14.2%)** | 初期曾误把 `tasks` 表的 12344 行(含重试)当作网格点总数,得出"60% 失败"的错误结论。 实际上 tasks 表每个失败点因 cold_run→seed_step 回退产生多行(attempt_count 中位数 3-4), 且 3876 个早已收敛的点根本不在 tasks 表里(`skip_converged` 跳过)。 ### 1.2 失败点参数空间分布 **按 Teff × logHe(每 cell 256 点):** | Teff | logHe | 失败/256 | 失败率 | 备注 | |------|-------|---------|-------|------| | 60000 | -4 | 148 | **58%** | 主体失败区 | | 55000 | -4 | 136 | **53%** | 主体失败区 | | 60000 | -2 | 126 | 49% | | | 55000 | -2 | 128 | 50% | | | 60000 | 0 | 101 | 39% | | | 50000 | -2 | 83 | 32% | | | 20000 | 2 | 89 | 35% | 次要失败簇(富氦+低logg) | | 25000~45000 各 He | — | <16% | — | 大多健康 | **两大失败簇:** 1. **高温贫氦区(55-60kK / He=-4,-2,0)**:733 个,占失败总数 **56%** 2. **20kK 富氦区(He=2 / logg=6.0-6.5)**:89 个(35%) **按 logg**:logg=5.0 最难(661 个,占 51%),logg=6.5 最好(139 个)。 **按 CNO 丰度(55000K/He=-4 cell 实测)**:CNO 越富失败率越高—— ``` cno_sum ≤ -11: 失败率 33% ← 贫金属,相对可收敛 cno_sum -7~-8: 失败率 92% cno_sum ≥ -6: 失败率 100% ← 最富金属,物理不可收敛 ``` ### 1.3 失败点的策略执行情况 所有 1305 个失败点均执行过 cold_run + seed_step(`pending_strategies` 已空,策略穷尽)。 seed_step 的实际结果: - seed 高 relc 发散(≥100):1461 条 task 记录 - seed NaN 发散:1091 条 - seed 下载/IO 失败:34 条 - seed 低 relc(<100):14 条 --- ## 2. 可能的失败原因(分析假设) ### 2.1 ORELAX 未设置 → 满牛顿步长振荡 所有 salvage 的 `.nc.nst` / `.seed_nc.nst` 只有 2 行,**无 ORELAX**: ``` ND=50,NLAMBD=3,VTB=2.,ISPODF=1,DDNU=50.,CNU1=6.,CHMAX=0.001,NITER=10 IELCOR=-1 ``` tlusty 源码 `tlusty208.f:844`:`IF(ORELAX.EQ.0.) ORELAX=UN`(UN=1.0),即默认 100% 牛顿步长。 满步长在硬网格点易振荡发散。 ### 2.2 Ng/Kantorovich 加速器把解推飞 tlusty 默认 `IACC=7`(Ng 加速从 iter7 开始)。fort.6 日志显示 `KANTOROVICH acceleration: ITER 5-9` 后 relc 从几十跳到上万——加速器在不稳定解上放大了误差。 ### 2.3 灰 LTE 起点离高温 NLTE 解太远 55kK 点 cold nc iter1 relc 就高达 12.7-21.6(对比正常点 0.06-0.2),LTE 灰大气作为 NLTE 迭代 起点在高温下偏差太大。 ### 2.4 富金属方向 seed_step 发散(物理原因) seed_finder.rs 记录:increasing metals from a poor seed adds UV line-radiation driving that breaks radiative equilibrium(贫方向成功率 42-54%,富方向仅 3-11%)。 ### 2.5 synspec 在 Balmer edge(3645Å)数值发散 459 个点 tlusty 收敛了(max_relc<0.001)但 synspec 产出的 spec 在 3645Å(H n=2 bound-free edge) 突然变 NaN。机制:`ST0(I)=ET(IJ,I)/CH(IJ,I)`(synspec54.f:12990),表层 NLTE 粒子数为 0 导致 0/0=NaN。 ### 2.6 IACC≤4 被源码强制改 7 `tlusty208.f:1928`:`IF(IACC.LE.4) IACC=7`。设 IACC=0 无法禁用 Ng 加速器。 --- ## 3. 实测验证(生产二进制 tlusty_static) ### 3.1 关键教训:二进制选择 **初期所有测试误用了 `tlusty/tlusty.exe`(开发版,Apr 2025)而非生产版 `dcts/assets/tlusty_static`(Jul 2025)。** 两者 md5 不同,数值行为不同 (如 55kK 点 iter1 relc:tlusty.exe=12.7,salvage 记录=254)。 以下结论均以**生产二进制**为准,凡 tlusty.exe 结果均已标注作废。 ### 3.2 部分读取陷阱 tlusty 运行中读取 fort.9 会得到**不完整迭代**的 relc(如 iter2=0.003 看似收敛), 但完整跑完后该迭代实际 relc 可能是 18700。**所有结论必须等运行完成后才可靠。** 本会话多次因部分读取得出错误中间结论,最终以完整运行结果为准。 ### 3.3 测试点 A:t20000_g5.0_he-4_c-4_n-2_o-2(20kK,次要区) **注意:此点非主体失败区代表(20000/He=-4 cell 仅 2/256 失败),结论代表性有限。** | 配置 | iter1 | 最终 iter | fort.7 NaN | 结论 | |------|-------|----------|-----------|------| | default ORELAX=1.0(tlusty.exe) | 0.217 | iter7=NaN | 4349 | ❌ 发散 | | ORELAX=0.3, NITER=10(tlusty.exe) | 0.217 | iter10=1.33 | 0 | ⚠️ 非NaN但未收敛 | | ORELAX=0.1, NITER=15(tlusty.exe) | 0.217 | iter15=0.556 | 0 | ⚠️ 振荡但无NaN | | **default(tlusty_static)** | 0.217 | **iter6=0.00672** | 0 | ✅ **收敛中** | **结论**:此点在生产二进制下 default 直接收敛中。salvage 标记失败可能是 seed_nc 阶段 失败或旧二进制偶然发散。 ### 3.4 测试点 B:t55000_g5.0_he-4_c-1_n-2_o-2(55kK,主体失败区) | 配置 | iter1 | 最终 iter | fort.7 NaN | 结论 | |------|-------|----------|-----------|------| | default N30 | 12.7 | iter10=1.95e8 | 0 | ❌ 发散 | | ORELAX=0.05 N50 | 12.7 | iter10=9.9e11 | 0 | ❌ 发散 | | ORELAX=0.01 N50 | 12.7 | iter9=1.05e6 | 0 | ❌ 发散 | | ORELAX=0.005 N50 | 12.7 | iter10=1.95e8 | 0 | ❌ 发散 | | ITEK=-50 N50(禁Ng) | 12.7 | iter11=NaN | 2047 | ❌ 更糟 | | IACC=0(禁加速) | 12.7 | iter5=3.89e8 | 0 | ❌ 无效(源码强制 IACC=7) | | seed_nc default(收敛邻居种子) | 19.5 | iter8=1.28e8 | 0 | ❌ 富方向发散 | | seed_nc IACC=0 | 19.5 | iter8=1.28e8 | 0 | ❌ IACC=0 无效 | **结论**:55kK 主体失败区,**所有 nst 参数组合都发散**。iter1 relc=12.7(灰 LTE 起点太远), Ng 加速器 iter5-7 把解推飞。富方向 seed_step(ΔlogC=+2)iter3 外层深度爆炸到 18700。 这是物理极限,非参数可解。 ### 3.5 测试点 C:t25000_g5.0_he-4_c-4_n-4_o-3(25kK,中温区) | 配置 | 最终 iter | fort.7 NaN | 结论 | |------|----------|-----------|------| | default N15 | iter10=4.72e-6 | 0 | ✅ **收敛** | | ORELAX=0.3 N30 | iter10=3.92e-6 | 0 | ✅ **收敛** | **结论**:25kK 失败点在生产二进制下 default 和 ORELAX=0.3 **都收敛了**。 此点 salvage 标记失败是旧二进制偶然发散——用新代码重跑即可救回。 ### 3.6 测试点 D:t45000_g6.0_he-4_c-1_n-3_o-3(45kK,中温区) | 配置 | iter1 | 最终 iter | fort.7 NaN | 结论 | |------|-------|----------|-----------|------| | default N15 | 21.6 | iter6=2.93e7 | 0 | ❌ Ng 加速爆炸 | | ORELAX=0.3 N30 | 21.6 | iter16=2.81e13 | 0 | ❌ 延迟但最终发散 | **结论**:45kK 点 ORELAX 无法阻止最终发散,但 default 和 ORELAX=0.3 全程 0 NaN。 (注:中间曾观察到 iter10=0.000335 的"收敛"假象,证实是部分读取 artifact。) ### 3.7 测试点 E:t20000_g6.5_he2_c-4_n-3_o-4(20kK 富氦高重力簇) conv.json 显示 seed_nc relc=0.0115(接近收敛),但 **nl 爆炸到 5.32e16**。 nc 阶段 default 和 ORELAX=0.3 轨迹接近(iter4: 1.31 vs 1.27),问题在 nl 阶段。 ### 3.8 synspec Balmer edge NaN 测试(t50000_g5.0_he-4_c-2_n-3_o-2) tlusty 收敛(max_relc=0.000247),但 synspec spec 在 **3645.88Å(Balmer limit)** 变 NaN, 前 87750 行正常,之后全 NaN(313695 行)。 | 配置 | 第一个 NaN 波长 | spec NaN 行数 | 结论 | |------|----------------|-------------|------| | 旧版 fort.55(IDSTD=50) | 3645.54Å | 30 万+ | ❌ | | 新版 fort.55(IDSTD=0) | 3645.88Å | 313695 | ❌ IDSTD 无效 | | ICONTL=1(纯连续谱) | 3645.88Å | 313695 | ❌ 连续谱 H bf 本身发散 | **结论**:Balmer edge NaN 不是 fort.55 配置问题,是 synspec 连续谱氢 bound-free 在表层 NLTE 粒子数为 0 时产生 0/0=NaN。大气 depth-1 粒子数块确认含大量 `0.000000E+00`。 --- ## 4. 综合结论 ### 4.1 失败分类(1305 个 grid failed 点) | 类别 | 数量 | 可救性 | |------|------|--------| | 高 relc 严重发散(≥100) | 746 | 高温区不可救;中温区可能重跑即救 | | NaN 物理发散 | 527 | 旧二进制 NaN → 新二进制可能改善 | | 未知 | 24 | — | | 接近收敛(relc<1) | 3 | 可救 | | 中度发散(1-100) | 5 | 可能可救 | 另:459 个点 grid 状态为 completed(synspec 失败但 tlusty 收敛),不在 1305 之列。 ### 4.2 什么能救、什么不能 **能救(预计 ~150-300 点):** - 中温区(20-45kK)旧二进制偶然失败的点——用新代码 `tlusty_static` 重跑即可 - 20kK/He2/g6.5 簇 nl 阶段振荡发散——ORELAX=0.5 阻尼 nl 的 Newton 步长振荡 - 部分旧二进制 NaN 发散——新二进制数值更稳定(55kK 点全程 0 NaN vs 旧版 NaN) **不可救(~600-800 点):** - 55-60kK/贫氦/富CNO(cno_sum≥-6)——灰 LTE 起点离 NLTE 解太远,所有参数组合发散 - synspec Balmer edge NaN——表层 NLTE 粒子数为 0 导致连续谱 H bf 数值发散 ### 4.3 无效方案(实测验证,勿重复) | 方案 | 测试结果 | 原因 | |------|---------|------| | ORELAX 对 55kK cold nc | ❌ 全发散 | 灰 LTE 起点太远,ORELAX 压不住 | | IACC=0 禁用 Ng | ❌ 无效 | `tlusty208.f:1928` 强制 IACC=7 | | ITEK=-50 禁用 Ng | ❌ 更快 NaN | 失去加速后纯 Newton 也发散 | | 新版 fort.55(IDSTD=0) 修 synspec | ❌ 仍 NaN | IDSTD 与 Balmer edge 无关 | | TFLOOR 防发散 | ❌ 零效果 | 生产二进制实测轨迹与无 TFLOOR 逐位相同 | | 调大 chmax | ❌ 有害 | nc 提前停止给 nl 坏种子 | --- ## 5. 落地的代码改动 ### 5.1 核心思路 给 nl/seed_nc 阶段加 ORELAX 欠松弛阻尼,并在 YAML 新增 `seed_chain` 字段让 seed_step 链 可配置(此前硬编码在 `default_seed_chain()`)。配合当前 `skip_converged` 策略,重启工作流 自动重跑 1305 个失败点(收敛的 7911 点保留)。 ### 5.2 改动清单(6 个文件) | 文件 | 改动 | |------|------| | `crates/common/src/config.rs` | `GridConfig` 新增 `seed_chain: Vec` 字段 | | `crates/common/src/models.rs` | `TaskSpec` 新增 `seed_chain_params` 字段 | | `crates/server/src/scheduler.rs` | 新增 `get_workflow_seed_chain()`;dispatch/fallback 注入 | | `crates/node/src/executor.rs` | `resolve_execution_chain()` seed_step 优先用 `seed_chain_params` | | `crates/common/src/runner.rs` | `default_seed_chain()` 兜底:seed_nc `orelax: Some(0.3)`,nl `orelax: Some(0.5)` | | `workflows/sdB_cno.yaml` | `tlusty_chain` nl 加 `orelax: 0.5`;新增 `seed_chain` 块 | ### 5.3 ORELAX 取值依据 - **nl 步 orelax=0.5**:nl 含谱线(ilvlin=100)扰动大,0.5 是保守的欠松弛值, 阻尼 Newton 步长振荡但不至于过度阻尼导致谱线约束不足。 - **seed_nc 步 orelax=0.3**:seed_nc 从已收敛邻居热启动,起点扰动小,可用更激进的 0.3。 - **nc 步不加 orelax**:实测中温区 nc 用 default 即可收敛,不加更安全。 ### 5.4 向后兼容 - `seed_chain` 空时回退 `default_seed_chain()`(已同步加 orelax),与旧行为一致 - `TaskSpec.seed_chain_params` 缺省 None(旧 MQ payload 兼容) - YAML 不配 `seed_chain` 时完全不影响现有行为 ### 5.5 验证 - `cargo build` ✅ 编译通过 - `cargo test` ✅ **194 个测试全绿,0 失败** (含新增 `seed_step_uses_custom_seed_chain_when_provided` 测试) --- ## 6. 后续建议 ### 6.1 重启工作流验证 当前 `skip_converged` 策略重启时自动重置 1305 个失败点回 pending。 部署新二进制 + 新代码后重启,观察失败率下降情况。预计中温区(20-45kK)的 ~150 个失败点可被救回。 ### 6.2 接力热启动(更大收益,需改代码) 对高温区(55-60kK)的失败点,可把 seed_step 的大步长 ΔCNO=2 拆成 Δ=1 的多步接力 (如 c-4→c-3→c-2→c-1),降低单步富金属扰动。需改 `scheduler.rs` 的 fallback 逻辑 支持多跳 seed。这是唯一可能救高温区的物理手段。 ### 6.3 synspec Balmer edge 修复(需改 synspec 源码) 表层 NLTE 粒子数为 0 导致连续谱 H bound-free 0/0=NaN。需在 `synspec54.f` 的 OPAC/连续谱计算加 NaN 守卫,或编译时启用 `-ffpe-trap=invalid,zero` 定位。 ### 6.4 接受物理极限 55-60kK/贫氦/富CNO(cno_sum≥-6)约 400-500 个点是 sdB 纯 He 大气模型的物理极限, 光谱应从相邻收敛点插值。 --- ## 附:关键文件路径 - 数据库: `/home/fmq/下载/dcts.db` - Salvage 归档: `data/salvage/{node}/result/{point}/` - 生产二进制: `dcts/assets/tlusty_static`(md5=`14cd144b...`) - 开发二进制(勿用于测试): `tlusty/tlusty.exe`(md5=`9b249e00...`) - tlusty 源码: `tlusty/tlusty208.f`(IACC 强制改 7 在 :1928;ORELAX 默认在 :844) - synspec 源码: `synspec/synspec54.f`(NaN 源头 ST0=ET/CH 在 :12990) - seed_finder.rs: `crates/common/src/seed_finder.rs`(贫富方向逻辑 :14-40)