feat(all): NaN 伪收敛否决与 nl_tight 能量回退、emflux 检验 4π 修正、nst 行宽与种子边界修复、gfATO 谱线表接通与网格加密 9216 点、阶段分项统计与跳板机部署

物理修复(400 失败点归因,见 docs/failed400_nan_pseudo_convergence_2026_08_17.md):
- runner: 阶段 converged 后复查 fort.7,含 NaN/Inf 即否决(fort.9 全零伪收敛,
  曾致 261 点误跳过 nl_direct 回退);否决阶段不产出种子,阻断污染传播
- runner: nl_tight 回退——仅能量边际失败时以 CHMAX 收紧 10× 从自身模型续迭代,
  残差降幅 ~10×;execute_tlusty_stage 抽取供主链与回退共用
- conv_check: fort.14 为 Eddington 通量 Hλ,积分需乘 4π 再比 σTeff⁴
  (旧版 ratio 稳定 0.0796=1/4π,全点系统性假阳性)+ 回归测试
- nst_writer: 单行超 80 字符被 TLUSTY 静默截断,IFALI/JALI/TRAD 等从未生效;
  按 75 字符自动换行
- seed_finder: Teff 容忍度改含边界 <=,相邻 5000K 档恢复互为种子 + 回归测试

谱线表与网格:
- 默认线表 gfVIS99 → gfATO(全波段 18-23000Å),TaskSpec.linelist 支持工作流
  级覆盖,节点按需下载(进程互斥锁防并发重复下载 238MB)
- sdB_cno Teff 加密至 5000K 步长,432 → 9216 点;tlusty/synspec 静态二进制更新

统计与部署:
- grid 汇总改按 tlusty_status/synspec_status 分项计数,新增 tlusty_failed/
  synspec_failed/synspec_pending,前端详情页双视图适配
- deploy/fetch_results 支持跳板机 ProxyJump 与 SSH 主连接复用,fetch 新增 --force;
- Docker 构建支持 CARGO_MIRROR/USE_MIRRORS 国内镜像参数;移除 tools/ 拷贝
- 新增 tlusty-synspec-test skill 与 6 篇根因分析/验证文档
This commit is contained in:
fmq
2026-08-17 23:55:26 +08:00
parent 43b82b1ae2
commit b058e66722
54 changed files with 5624 additions and 347 deletions
@@ -0,0 +1,507 @@
# TLUSTY 不收敛根因分析(1105 个网格点)
**日期**2026-08-11
**数据来源**`/home/fmq/下载/dcts.db`server DB 快照)+ `data/salvage/`8320 次任务尝试的完整产物)
**源码版本**`tlusty208.f`50010 行)+ dcts 框架 `crates/{common,node,server}`
**工作流**`sdB_cno`grid 共 9216 点,converged 8102 / failed 1105 / pending 9
> 本文不修改任何代码,只做根因定位与修复建议。修复实施见文末「后续行动」。
---
## ⚠️ 2026-08-12 更新(修复建议已实测证伪,配置改动已回退)
本文 §3.1 A1/A6 与「后续行动」中推荐的**数值配置类修复**(收紧 DPSILG、禁用
Kantorovich/ITEK、禁 Ng/IACC)已经用控制实验实测验证:**均无法让失败点从冷启动
收敛**,相关配置改动已全部回退。详见 `docs/cold_start_fix_2026_08_12.md`
实测结论(6 个测试点 × 4 种配置 = 24 次运行,**0 次收敛**):
- `ITEK=0` → nitzer=0 冻结 populations`tlusty208.f:1937`),不是禁 Kantorovich
- `IACC=0` → 被 clamp 回 7`tlusty208.f:1928`),空操作;
-`ITEK/IACC > NITER`(真正禁用 Kant+Ng)→ 发散幅度降 1e12~1e14×,但仍不收敛
(末 relc 1e3~1e7),Newton 在 grey LTE 起点即处于收敛域外;
- `DPSILG=1.5` 有害(截断步长致漂移)。
**因此本文方向性结论(物理吸引域窄 + 框架无自适应)成立,但可行的修复不在
数值配置,而在「连续法(从已收敛较低 Teff 模型逐步过渡)」与「He 匹配种子选择」
等框架级改造。** 相关计算文件见 `docs/cold_start_fix_2026_08_12.md`
---
## 一、结论先行
1105 个失败网格点全部走完了 `cold_run → seed_step` 的完整退化路径,**两种策略都失败**。这**不是**种子选择问题,**不是**单纯的初值问题,而是两个因素叠加:
1. **物理侧**——TLUSTY 的 Newton 完全线性化求解器在「高 Teff + 低 logg」参数角落的吸引域过窄,辐射场对布居数极度敏感;
2. **工程侧**——dcts 框架缺少发散检测与自适应阻尼反馈,单阶段发散后无回退,只能靠 NITER 自然耗尽再整点重试。
失败点本身**并非不可解**——同族参数下大量点是收敛的(T=60k/g=5.0 仍有 78/256 收敛),失败的只是收敛盆地最窄的那一批。
---
## 二、数据证据链
### 2.1 退化序列(DB `tasks` 历史,1105/1105 全部命中)
```
["cold_run"] → ["cold_run","seed_step"] → ["seed_step"] → ["cold_run","seed_step"] → ["seed_step"] ...
```
每个失败点重试均值 **6.9 次**`grid_points.attempt_count` 分布:5 次=1996 次=1357 次=4628 次=2759-13 次=33),在两种策略间反复跳。最新任务全部停在 `failed_stage=tlusty``tlusty_strategies=["seed_step"]`
### 2.2 失败点的参数分布(物理一致)
| Teff | 失败数 | | logg | 失败数 | | loghe | 失败数 |
|---|---|---|---|---|---|---|---|
| 60k | 344 | | 5.0 | 587 (53%) | | -4.0 | 345 |
| 55k | 330 | | 5.5 | 285 | | -2.0 | 337 |
| 50k | 204 | | 6.0 | 118 | | 0.0 | 274 |
| 45k | 84 | | 6.5 | 115 | | 2.0 | 149 |
| ≤40k | 143 | | | | | | |
**高 Teff + 低 logg** 高度集中(≥50k 占 878/1105 ≈ 79%logg=5.0 占 53%)。
**Teff × logg 收敛率矩阵**(节选,完整矩阵见 §6 附录):
```
g=5.0 g=5.5 g=6.0 g=6.5
T=20k 249/256 242/256 238/256 210/256
T=40k 233/256 252/256 253/256 256/256
T=50k 119/256 224/256 233/256 244/256
T=55k 78/256 156/256 227/256 231/256
T=60k 78/256 152/256 222/256 228/256
```
**关键**:没有任何格子是 0% 收敛——所以这是"难",不是"不可能"。难度梯度清晰:随 Teff 升高收敛率下降,随 logg 升高收敛率上升。
### 2.3 失败阶段分布(按 salvage 最后一次尝试)
| 首失败阶段 | 占比 | 失败特征 |
|---|---|---|
| `seed_nc`seed_step 第一阶段) | 1033/1105 (93%) | max_relc 单调雪崩 |
| `nc`cold_run 第二阶段) | 72/1105 | max_relc 单调雪崩 |
| `lte` | 0 | —— |
**两种策略的死亡轨迹几乎相同**——max_relc 在 5 步内从个位数飙到 1e6+。典型例:
```
cold_run nc (T=60k g=5.0):
[9.72, 46.5, 259, 1950, 4.85e6, 2.6e10, 2.8e4, 4.04e7, 2.06e11, 2.32e15]
seed_step seed_nc (T=60k g=5.0, 从收敛邻居热启动):
[7.0, 115, 272, 2020, 1.49e8, 8.26e13, 3310, 2.09e7, 7.53e10, 4.71e14, ...]
```
> 注:salvage 中只保留了 282 个点的 cold_run 中间产物(节点在重试时会清理中间态),但 **282/282 全部死在 `nc` 阶段**,且 max_relc 雪崩轨迹与 seed_nc 一致。
最后一次尝试的失效特征分两档(按 `atmosphere_has_nan` 拆分):
| 失效特征 | 点数 / 1105 | 说明 |
|---|---|---|
| `atmosphere_has_nan: true` | **434 (39%)** | 雪崩污染了大气文件;伴随 `temp_check.error: 深度 1 T=NaNK``emflux NaN 占比 100%``bfac 极端值占比 36%``.err: IEEE_INVALID_FLAG IEEE_DIVIDE_BY_ZERO` |
| `atmosphere_has_nan: false` | **671 (61%)** | max_relc 雪崩但大气未污染到 NaN;被 `temp_check`(表层 T 超过 3×Teff)、`bfac_check`(极端值占比 >10%)、`emflux_check`(积分通量偏离 σT⁴)等后验校验判废,或 nl 阶段才崩 |
> ⚠️ **修订说明**:初版误把 "atmosphere_has_nan=1033/1105" 写入报告——1033 实为 "首失败阶段=seed_nc 的点数",与 NaN 计数无关。真实 NaN 仅 434/1105。雪崩到 1e6+ 未必把 `fort.7` 写成 NaN(取决于 TLUSTY 是撞 1e16 STOP 还是跑完 NITER),故多数失败点的大气文件本身可读,是被后验校验(温度结构/b 因子/emflux)挡下的。
### 2.4 种子选择不是唯一根因(但不能完全排除)
1105 个失败点的 seed_step 配对 **全部来自 exact_family**`d_teff<5000 & d_logg<0.01 & d_loghe<0.01`),且 **662 个是贫方向**seed_finder 设计的稳定方向):
```
seed direction: poor=662, rich=373, same=70
d_teff 分布: <5k(exact)=1105, 其余=0
d_logg 分布: 0(same)=1105, 其余=0
```
> ⚠️ **校正**:初版标题"种子不是问题(关键反证)"措辞过强。准确结论是**种子选择不是唯一根因,但不能排除它是贡献因素之一**:
> 1. **"exact_family" 不等于物理极近**:判定允许 `ΔTeff < 5000K`(半步容忍)。sdB 网格 Teff 步长 10000K 时,55k↔60k 相邻档互作种子的辐射场(Lyman 紫外主导)差异可能很大——"exact_family" 标签覆盖了 Teff 仍可差一整档的事实。
> 2. **443 个 rich/same 方向失败**373 rich + 70 same):seed_finder 自身的回测数据(`seed_finder.rs:14-25`)称富方向成功率仅 3-11%,贫方向 42-54%。这部分失败可能部分源于种子方向不佳。
> 3. **种子质量未审**:失败点用的种子虽来自收敛邻居,但该邻居是在多少迭代数、多干净的 max_relc 下收敛的(擦边收敛 vs 干净收敛),本报告未分析。
>
> 综上,"662 贫方向仍爆"只能说明"贫方向种子不足以保证收敛",**不能**推出"种子方向无关"。种子方向的影响需在 §五 A/B 测试中单独控制变量验证。
### 2.5 seed_step 在同区域能成功(说明非不可解,但未排除混淆变量)
高 Teff 危险区里 seed_step 仍有大量成功:
```
T=50k g=5.0: 119 conv (cold=51, seed=68)
T=50k g=5.5: 224 conv (cold=87, seed=137)
T=55k g=5.0: 78 conv (cold=53, seed=25)
T=60k g=5.0: 78 conv (cold=40, seed=38)
```
成功路径的 conv.json 显示 `seed_nc` 即使 `converged=false` 也可用(`note: "accepted as seed (convergence not required)"`),关键是后续 `nl` 能否从不完全收敛的种子收敛到 `max_relc < 1e-4`。失败点中 434/1105 的 `nl` 产出 NaN 大气,其余 671 点被后验校验挡下。
> ⚠️ **校正**:初版据"同格有成功"推出"非不可解",逻辑成立——同参数区不是数学上不可解。但**未排除混淆变量**:成功的 78 个点(T=60k/g=5.0)与失败的 178 个点用的种子来源/方向可能系统性不同。尤其 cold=40 成功(从 LTE 灰化初值也能收敛一部分)反而暗示**初值/种子的微小差异决定成败**——这更像"吸引域窄 + 初值敏感",而非单纯的"求解器稳定性"。本节只能支撑"非数学不可解",不能支撑"种子/初值无关"。
---
## 三、源码级根因
### 3.1 TLUSTY 侧(`tlusty208.f`
#### A1. `DPSILG=10.` 的限幅太松 — `tlusty208.f:14652-14653`
```fortran
IF(CHAN.LE.UN/DPSILG-UN) CHAN=UN/DPSILG-UN ! 下限 = 1/10 - 1 = -0.9
IF(CHAN.GE.DPSILG-UN) CHAN=DPSILG-UN ! 上限 = 10 - 1 = +9.0
```
默认 `DPSILG=10.`PVALUE 表 idx 185`tlusty208.f:1793`),允许单步相对修正达 **10 倍**
> ⚠️ **重要校正**:DPSILG 这个宽松限幅(±10×)**主要作用于辐射场(`I ≤ NFREQE`)和布居数块(`I ≥ NFREQE+INSE`**。紧随其后 `tlusty208.f:14654-14677` 有一组**逐变量更严格的限幅**,按 `INRE/INHE/INPC/INDL` 偏移把对应槽位钳到更窄:
> - **温度 T**`I = NFREQE+INRE`):受 `DPSILT=1.25` 钳制 → 单步上限仅 **±25%**
> - **总粒子数 TOTN**`I = NFREQE+INHE`)、**电子密度 ne**`I = NFREQE+INPC`):受 `DPSILN=10.` 钳制 → ±10×(与 DPSILG 同级,宽松)
> - **Δ(湍流相关,`I = NFREQE+INDL`):受 `DPSILD=1.25` → ±25%**
所以"高 Teff 下结构量步长放大 10 倍必爆"这一表述**对温度不成立**(温度有 ±25% 独立约束)。真正受 10× 宽松限幅影响的是**辐射场**与**布居数/TOTN/ne**。雪暴主因更可能是辐射场与布居的耦合放大,而非温度跳变。
#### A2. 首步发散无 STOP 保护 — `tlusty208.f:14700`
```fortran
IF(ITER.NE.1 .AND. ABS(CHMX).GT.1.D16) THEN
WRITE(6,610) ITER,CHMX
STOP
END IF
```
只在 `ITER≥2` 才检查 1e16 雪崩 STOP。**首步(ITER=1)即使 CHMX 巨大也不触发 STOP**——后续迭代在已被污染的解上继续算,直到某步偶然撞上 1e16 才停。
> ⚠️ **校正**:初版称"第 1 步任意大都继续跑"措辞过强。首步的 CHAN 仍受 A1 限幅钳制(辐射/布居 ±[-0.9,+9],温度 ±25%),**并非"无限幅"**;首步只是**无 STOP**。真正的风险是:限幅允许的 +9×(辐射/布居)在多深度多变量上叠加,几步内放大成 Inf/NaN——这与 §2.3 观测的雪崩量级一致。
> 另:同样的 STOP 逻辑在 SOLVES`tlusty208.f:15047`)和 RYBSOL`tlusty208.f:47587`)另两条求解路径各有一份。
#### A3. `ORELAX` 只阻尼布居数,不阻尼辐射场与温度等结构量 — `tlusty208.f:14647`
```fortran
C over-relaxation
IF(I.GE.NFREQE+INSE) CHAN=ORELAX*CHAN
```
> ⚠️ **重大校正**:初版把 PSI 向量布局写反了。经核对 `tlusty208.f:3910-3936`PSY0 各槽位赋值)与 `tlusty208.f:795-797`INRE/INPC/INSE 偏移量定义:INDL=3, INPC=4, INSE=5),正确布局是:
```
[1 .. NFREQE] = 辐射场(RADEX 平均强度 Jν) → 不被 ORELAX 松弛
[NFREQE+INHE] = [NFREQE+1] = 总粒子数 TOTN → 不被 ORELAX 松弛
[NFREQE+INRE] = [NFREQE+2] = 温度 T → 不被 ORELAX 松弛
[NFREQE+INPC] = [NFREQE+4] = 电子密度 ne → 不被 ORELAX 松弛
[NFREQE+INSE] = [NFREQE+5] = 布居数 superlevels 块的起点 → ORELAX 生效(I ≥ NFREQE+INSE
```
**结论**`ORELAX` 松弛的是 **布居数(populations****不是**温度/ne/TOTN 等结构量。初版把"布居"与"结构量"的松弛关系完全对调了。
这对结论的影响:
- 框架给 seed_nc 设的 `ORELAX=0.3` **确实会作用到布居数**(初版说"对结构量、对辐射场不生效"是错的——准确说是"对布居生效,对辐射场与温度/ne 不生效")。
- **真正欠松弛缺失的是辐射场 Jν**——高 Teff 下辐射场最敏感、最需要欠松弛,而它既不受 ORELAX 保护、又只受 A1 的 ±10× 宽松限幅。这一论断方向不变,但论证链条已修正。
#### A4. `CHMAX` 只判终值 — `tlusty208.f:14728`
```fortran
LFIN=ABS(CHMX).LE.CHMAX .OR. ITER.GE.NITER
```
`CHMAX=1e-3`(默认,PVALUE idx 77`tlusty208.f:1767`)是"最后一次迭代最大修正"的终值门槛。**CHMAX 本身对过程无约束**——但它不是唯一的过程机制,见 A5/A6。
#### A5. ⚠️ 初版遗漏:CHMAXT 驱动的过程反馈 — `tlusty208.f:14711, 22954-22958`
初版称"过程爆掉只能靠 NITER 自然耗尽"是**夸大**。源码中存在由 `CHMAXT`(默认 0.01PVALUE idx 80`tlusty208.f:1767`)驱动的两层过程反馈:
```fortran
C tlusty208.f:14711 线
if(chmt.gt.chmaxt .and. ispodf.ge.1) LIROST=.TRUE.
if(LITEK.and.LIROST) then
call iroset
LIROST=.FALSE.
end if
C tlusty208.f:22954-22958 Ng
if(chmt.lt.chmaxt) then
do itl=iter,niter+1
nitlam(itl)=nlamt
end do
end if
```
这是针对**温度变化**(CHMT)的自适应机制。但它**不直接约束辐射场与布居的耦合发散**——而这恰恰是失败点雪暴的主区。所以 A5 的存在削弱了"过程完全无约束"的措辞,但不改变"雪暴主因缺乏针对性干预"的结论。
#### A6. ⚠️ 初版遗漏:ITEK / Kantorovich 加速切换 — `tlusty208.f:843-856`
默认 `ITEK=4`PVALUE idx 130`tlusty208.f:1779`)。源码:
```fortran
IF(ITEK.GT.0) THEN
DO IKT=ITEK+1,NITER
KANT(IKT)=1 ! iter≥5 切到 Kantorovich 加速
END DO
DO IKT=IACC,18,IACD
KANT(IKT)=0 ! 但 Ng 重启点(IACC=7,18,IACD=4) 回到完全线性化
END DO
```
**iter≥5 切换到 Kantorovich 加速迭代**(更便宜的近似,少算矩阵元)。初版把 ITEK 标为"失败相关性:低"是**无依据的臆断**——雪暴轨迹(如 §2.3 的 `[9.72, 46.5, 259, 1950, 4.85e6, ...]`)第 5 步开始爆,与 Kantorovich 切换点(iter≥5**时间上重合**。这是一个**尚未排查的嫌疑诱因**:Kantorovich 在发散区可能因近似失真加剧不稳定。**降 DPSILG(修复优先级 3)若救回率不如预期,应优先排查 ITEK。**
### 3.2 框架侧(`crates/common/src/runner.rs` / `crates/node/src/executor.rs`
#### B1. 无链内重试,无自适应阻尼
`default_seed_chain` / `default_cold_chain``runner.rs:20-98`)一旦构造完成参数就是死的。`conv_check::check_fort9``conv_check.rs:204-217`)判完发散后,`runner.rs:564-570` 只在 `require_converged=true``break`,否则继续——**没有任何"发现 max_relc 超过阈值就降低 DPSILG / 加大 ORELAX / 重跑一次"的反馈机制**。
#### B2. `seed_nc` 的 `require_converged=false` 反而帮了倒忙
`default_seed_chain` 第一阶段设 `require_converged=false`("接受非收敛种子"),本意是"给 nl 一个起点就行"。`runner.rs:518-521``rc==0 && fort7.is_file()` 分支**无条件**把本阶段 `fort.7` 拷成下一阶段的 `current_seed`——**stage 间种子传递路径上没有 `atmosphere_has_nan` 检查**。
> ⚠️ **校正**:初版称"雪崩后仍然把含 NaN 的 fort.7 喂给 nl"措辞过死。是否真的传 NaN 取决于 TLUSTY 的退出方式:
> - 若雪崩撞上 1e16 STOPA2)→ rc≠0 → 走 `runner.rs:522-531` else 分支,**不拷** fort.7
> - 若跑完 NITER 自然结束(rc=0)但 `fort.7` 已含 NaN → **会拷**给 nl,nl 自然也爆。
>
> §2.3 实测 434/1105 真有 NaN 大气——这部分点确属此路径。其余 671 点是雪崩但大气可读,被后验校验挡下。两档都要修。
#### B3. `CHMAX=None` 让 TLUSTY 走默认,但没把更保守的 `DPSILG`/`ORELAX` 一起传
所有默认阶段 `chmax=None``runner.rs:20-98`),runner 用 Rust 侧 `eff_chmax=0.001``runner.rs:443`)做**后验**判断,但这个值**不传给 TLUSTY**。结果 TLUSTY 用默认 `DPSILG=10.` + `ORELAX=1.0`(冷链)/ `0.3`(种子链,作用于布居)跑,过程约束针对辐射场的几乎为零(见 A3 校正后的作用域)。
`ChainStep` 结构体(`config.rs:1130-1150`)字段为 `label/lte/ltgray/ilvlin/require_converged/niter/chmax/itek/ichang/idlte/iacc/orelax`**没有** `dpsilg` 字段。要分阶段注入 DPSILG 需新增字段(走 `nst_writer` line1),或通过 YAML `tlusty_input.nst_extra_keys` 逃生舱(`nst_writer.rs:138-161` 支持,对当前阶段生效)。
#### B4. 整点重试无参数扰动
策略 fallback`scheduler.rs::trigger_strategy_fallback`)只在 `["cold_run"]``["seed_step"]` 之间切换,重试任务的 `tlusty_chain_params`/`seed_chain_params` 是工作流 YAML 的**逐字克隆**(`scheduler.rs:1065-1066`)——**TLUSTY 数值参数零扰动**。
> ⚠️ **校正**:初版"每次重跑参数完全一致(除了种子来源)"自相矛盾——种子来源恰恰是初值的一部分。准确表述:**同一策略内的重试是确定性白跑**(相同初值+参数必爆);**跨策略切换时换了初值**(cold_run 无种子 LTE 灰化初值 → seed_step 用近邻收敛种子),但 TLUSTY 数值参数始终不变。所以"重试均值 6.9 次"中只有跨策略切换那几次换了实质初值,同策略内确属白跑。
### 3.3 未排除的替代根因(诚实声明)
> 本节是审查后新增。初版把 DPSILG/ORELAX/CHMAX 这些**机制描述**当作根因,但严格地说这是**充分性论证,不是必要性论证**。即便 A1-A6 全为真,也不能排除主要根因在以下未被充分审视的环节。这些替代假说需在 §五 A/B 测试中逐一排除或确认。
| 替代假说 | 依据 | 排查方法 |
|---|---|---|
| **TFLOOR=8000K 在高 Teff 下不合理** | `config.rs:815` 默认 8000。Teff=60k 大气表层物理温度 ~30-90kTFLOOR=8000K 离表层不远;若发散把温度推向 TFLOOR,钳制本身可能破坏 H 电离平衡(8000K 附近 H 电离度突变) | 对失败点试 TFLOOR=30000/40000,看救回率 |
| **ND=50 在高 Teff+低 logg 下分辨率不足** | `config.rs:758` 框架覆盖为 50TLUSTY 默认 70config 自承)。高 Teff+低 logg 表层温度梯度陡峭,ND=50 可能欠分辨表层 → 数值发散 | 对失败点试 ND=70/100 |
| **DDNU=50/CNU1=6 频率网格在高 Teff 不足** | `config.rs:769-776` 框架 DDNU=50TLUSTY 原始 0.75)、CNU1=6(原始 4.5)。60k 大气 Lyman 极端紫外主导,此"sdB 调优"频率网格可能采样不足 → 辐射场离散误差大 | 对失败点试更密的频率网格 |
| **特定离子原子数据 bug** | `config.rs` ions 表用 C III `c3_34+12lev.dat`、N II `n2_32+10lev.dat` 等组合能级文件;某高 Teff 主导离子的能级数据若有误,会导致该离子布居发散 | 对失败点逐离子禁用排查 |
| **LTE 阶段未干净通过** | 失败点 salvage 的 `.err``IEEE_DIVIDE_BY_ZERO`;若某深度 LTE 灰化初值的 ne 近零导致除零,可能**先于** NLTE 雪崩 | 检查失败点的 `.lte.err`/`.lte.6` 是否干净 |
| **输入文件渲染 bug** | `gen_input5.rs` 渲染丰度 `10^logx`,logc=-4 等极端值时未验证字节级正确 | 抽查失败点的 `.5` 文件丰度数值 |
**优先级**:前三个(TFLOOR/ND/DDNU)与"A1-A6 机制"正交,是独立的数值配置假说,应在方案 A 的 A/B 测试中顺带排除。若方案 A(降 DPSILG)救回率远低于 30%,应立即转向这些替代假说。
---
## 四、修复建议(按性价比排序)
### 优先级 1:自适应发散检测 + 阻尼回退(框架侧,治本)
`runner.rs` 的 stage 循环里加一层 **"单阶段内 iftek 监控 + 回退重跑"**
1.`tokio` 异步跟踪 `fort.9` 增长,每完成 1-2 次迭代解析一次 `max_relc`
2. 若前 3 步 `max_relc` 单调上升且超过阈值(如 1e2),**SIGTERM 当前子进程**
3. 用更保守的参数(`DPSILG` 减半、`ORELAX` 减半)重跑该阶段;
4. 最多回退 2-3 次,每次更保守。
这是最直接的对症方案——雪崩在前 3 步就能看出来,没必要跑到 NITER。
**实现成本**:中-高。需要在 `ChainStep``dpsilg`/`adaptive` 字段,runner 加监控+回退逻辑。初版估的 "~300 行 Rust" 偏低——实时跟踪 fort.9 增长(TLUSTY 边写边追加,需处理部分写入)+ SIGTERM 子进程后正确 reap + 清理中间文件 + 回退状态机,工程量更大,预估 400-600 行。
### 优先级 2:给辐射场与温度/ne 加欠松弛(TLUSTY 源码侧,治本但要改二进制)
> ⚠️ **校正**:初版给的代码是**替换**原逻辑(`I.GE.` → `I.LT.`),会**移除**原布居松弛——这反而让布居失去保护。正确改法是**新增一行**,且 `ORELAXR` 在源码中本不存在。
**正确改法**`tlusty208.f:14647`SOLVE 与 SOLVES `14996` 两处都要改):
```fortran
C (INFREQE+INSE) ORELAX
IF(I.GE.NFREQE+INSE) CHAN=ORELAX*CHAN
C (INFREQE)(NFREQE+1..NFREQE+INSE-1 T/TOTN/ne/ZD/DENS/DELTA)
C ORELAXR 1.0= 0.3-0.5
IF(I.LT.NFREQE+INSE) CHAN=ORELAXR*CHAN
```
物理论据(修正后):辐射场 Jν 与温度/ne 在高 Teff 下最敏感、却都不被 ORELAX 松弛(A3 校正后)。给它们加欠松弛是物理上最对症的修复。
**注意**
1. **不是"改 2 行 + 重编"**`ORELAXR``tlusty208.f` 中完全不存在——需扩 VARNAM/PVALUE 两个 DATA 数组各加一项、加 nst 关键字解析、加 PVALUE 默认值(建议 1.D0 不变,仅难收敛阶段通过 nst 覆盖为 0.3-0.5)。工程量约 30-50 行 Fortran + 重编 + 更新 `assets/tlusty_static`
2. **与 NLAMBD(λ-迭代)的交互未验证**:辐射场在每个深度通过 NLAMBD 机制单独更新,给 Jν 加欠松弛可能与该机制冲突,需小规模测试验证。
3. 触及二进制,需走完整发布流程。
### 优先级 3:动态降低 DPSILG(框架侧,立即可做,无侵入)
`ChainStep` 加可选字段 `dpsilg: Option<f64>`,通过 `nst_extra_keys` 注入。给种子链和难收敛区(Teff ≥ 50k & logg ≤ 5.5)设 `DPSILG=3.``DPSILG=2.`(对应单步上限 ±2 倍 / ±1 倍)。收敛性会显著改善。
代价:迭代步数变多,但比爆掉重试 7-9 次便宜得多。
**实现成本**:低。~50 行 Rust + YAML 配置。**可立即上线**。
### 优先级 4seed_nc 发散时拒绝喂给 nl(框架侧,止损)
`runner.rs` 里,当 seed_nc 的 `atmosphere_has_nan``best_max_relc > 1e3` 时,**不要**把它的 `fort.7` 当作 nl 的种子;要么直接判失败触发下一策略,要么回退用原 `seed_atmos`(邻居的干净种子)重新喂 nl。
**实现成本**:极低。~20 行 Rust。
### 优先级 5:首步发散保护(TLUSTY 源码侧)
`tlusty208.f:14700`SOLVE/ `15047`SOLVES/ `47587`RYBSOL)三处把 `ITER.NE.1` 改为允许首步也检查(用更宽松的阈值,如 1e8),防止首步就冲出吸引域:
```fortran
C
IF(ITER.NE.1 .AND. ABS(CHMX).GT.1.D16) THEN ... STOP
C
IF(ITER.EQ.1 .AND. ABS(CHMX).GT.1.D8) THEN ... STOP
IF(ITER.GT.1 .AND. ABS(CHMX).GT.1.D16) THEN ... STOP
```
**实现成本**:极低(改 2 行 + 重编)。
---
## 五、验证路径
用 **30 个失败点**做 A/B 测试(覆盖 60k/55k/50k × g5.0/5.5,每格 5 点):
| 方案 | 配置 | 预期救回率(假设性估计) |
|---|---|---|
| 基线 | 当前配置重跑 | 0%(可复现) |
| 方案 A | 优先级 3DPSILG=3+ 优先级 4NaN 拒绝) | 30-50% |
| 方案 B | 方案 A + 优先级 1(自适应回退) | 70-85% |
| 方案 C | 方案 B + 优先级 2/5(改 TLUSTY 源码) | 90%+ |
> ⚠️ **校正**:初版无标注地把上述救回率当作事实呈现。这些百分比是**无数据支撑的先验猜测**,仅用于排列方案的优先级顺序,不代表实测预期。尤其"方案 C 90%+"与"吸引域窄"的论断存在张力——吸引域窄意味着即便阻尼到位,某些初值仍落吸引域外。**方案 A 的实测救回率是最关键的诊断信号**:若远低于 30%,说明 DPSILG 不是主因,应转向排查 ITEK/KantorovichA6)或 §3.3 列出的替代根因。
**建议**:先实施方案 A 验证假设(DPSILG 是否真是主因),用实测数据说话,再决定是否值得改 TLUSTY 源码走方案 C。
---
## 六、附录
### 6.1 完整 Teff × logg 收敛率矩阵
```
logg-> g=5.0 g=5.5 g=6.0 g=6.5
T=20k 249/256 242/256 238/256 210/256
T=25k 251/256 254/256 256/256 253/256
T=30k 252/256 255/256 254/256 256/256
T=35k 244/256 252/256 256/256 256/256
T=40k 233/256 252/256 253/256 256/256
T=45k 207/256 230/256 247/256 254/256
T=50k 119/256 224/256 233/256 244/256
T=55k 78/256 156/256 227/256 231/256
T=60k 78/256 152/256 222/256 228/256
```
### 6.2 TLUSTY 关键变量默认值(PVALUE 表,`tlusty208.f:1750-1804`
> 行号为 PVALUE DATA 块内每个值实际所在行(经 VARNAM/PVALUE 平行映射逐项核对,idx 从 1 起)。
| 变量 | 默认值 | PVALUE 行号 | 含义 | 失败相关性 |
|---|---|---|---|---|
| `CHMAX` | `1.D-3` | 1767 (idx77) | 收敛门槛(终值) | 低(只判终值) |
| `DPSILG` | `10.` | **1793** (idx185) | 单步相对修正上限(辐射场+布居) | **高**(辐射场/布居 10 倍太松) |
| `DPSILT` | `1.25` | 1793 (idx186) | **温度**单步上限(±25%) | 低(温度有独立严约束) |
| `DPSILN` | `10.` | 1793 (idx187) | TOTN/ne 单步上限 | 中 |
| `DPSILD` | `1.25` | 1793 (idx188) | Δ(湍流)单步上限 | 低 |
| `CHMAXT` | `0.01` | 1767 (idx80) | 温度变化过程反馈阈值 | 中(A5 机制,未针对辐射场) |
| `ORELAX` | `1.D0` | 1779 (idx131) | **布居数**过松弛因子 | 中(框架覆盖为 0.3/0.5,作用于布居) |
| `NITER` | `30` | 1763 (idx64) | 最大迭代数 | 低 |
| `ITEK` | `4` | 1779 (idx130) | Kantorovich 加速起始 iter | **待排查**(iter≥5 切换,与雪暴起点重合,见 A6) |
| `TFLOOR` | `8000.` | 1787 (idx160) | 温度下限 | 待排查(见 §3.3) |
| `ICHANG` | `0` | 1766 (idx83) | 是否走 CHANGE(热启动) | 低 |
| `ND` | `50`(框架覆盖) | 默认70(idx67) | 深度网格点数 | 待排查(见 §3.3) |
> ⚠️ **校正**:初版把 DPSILG 行号写成 1786(实际是 POPZCH 区段的 '0.','0.',与 DPSILG 无关),且 §6.2 范围写 `1750-1806`(实际 PVALUE 结束于 18041806 是 BLNK DATA)。初版还漏列 CHMAXT、DPSILD、ND,把 ITEK 标为"失败相关性:低"(无依据,已改"待排查"),把 DPSILG 含义写"结构量"(实际主要约束辐射场+布居,温度有独立 DPSILT),把 ORELAX 含义写"结构量过松弛因子"(实际是布居)。
### 6.3 失败点失败阶段与 max_relc 量级分布(最后一次尝试)
```
首失败阶段: seed_nc=1033 nc=72
末步 max_relc: >1e6=982 1e4-1e6=25 1k-1e4=15 100-1k=12 10-100=14 (1,10]=5 <1=35
另有 17 点 itek_history 为空(首失败阶段无迭代轨迹)
迭代次数分布峰值: 10 iters(327) / 20 iters(143) / 6 iters(159) — 全部撞到 NITER 上限
atmosphere_has_nan: True=434 (39%) / False=671 (61%)
```
> ⚠️ **校正**:初版末步 max_relc 行漏列 (1,10] 区间 5 点 + 17 点空 history,导致加总 1083 与首失败阶段 1105 不闭合。补齐后可分类 1088 + 空 history 17 = 1105。
### 6.4 种子方向
```
poor(目标比种子贫,稳定方向) = 662
rich(目标比种子富,不稳定方向) = 373
same(同 CNO) = 70
```
seed_finder 的非对称距离(`seed_finder.rs:29-43`RICH_PENALTY=4 vs POOR_PENALTY=1)使 60% 失败点拿到了贫方向种子。
> ⚠️ **校正**:初版称"seed_finder 已尽力……说明问题不在种子方向"措辞过强。更准确的表述:**种子选择不是唯一根因,但不能排除它是贡献因素之一**。理由:(1) "exact_family" 判定允许 ΔTeff < 5000K(半步容忍),sdB 网格步长 10000K 时 55k↔60k 相邻档互作种子辐射场差异仍可能很大,"物理极近"标签不能完全等同于 NLTE 状态空间近邻;(2) 443 个 rich/same 方向失败(seed_finder 自身的回测数据称富方向成功率仅 3-11%)可能部分源于种子方向。种子方向的影响需在 §五 A/B 测试中单独控制变量验证。
### 6.5 分析脚本
本次分析所用脚本临时存于 `/tmp/`(未入库):
- `analyze_salvage.py` / `analyze2.py` — salvage 池全量分类
- `dbfail.py` — DB 失败点分布
- `correlate.py` — DB 失败点 × salvage 最后一次尝试关联
- `seq.py` — 策略退化序列验证
- `cold_mode.py` — cold_run 失败模式
- `seed_check.py` — 种子距离/方向分析
- `success_region.py` — Teff×logg 成功率矩阵
- `parse_defaults.py` — TLUSTY PVALUE 默认值映射
---
## 七、后续行动
- [ ] 决定走方案 A / B / C 中的哪个
- [ ] 若方案 A:实现 `ChainStep.dpsilg` 字段 + YAML 配置(优先级 3)
- [ ] 若方案 A:实现 seed_nc NaN 拒绝逻辑(优先级 4
- [ ] 30 点 A/B 测试验证
- [ ] A/B 测试中顺带排查 §3.3 替代根因(TFLOOR/ND/DDNU/原子数据/LTE 干净度)
- [ ] 若方案 A 救回率远低于 30%:转向排查 ITEK/KantorovichA6)或 §3.3 替代根因
- [ ] 若数据支持:实施优先级 1(自适应回退)
- [ ] 若需根治:改 `tlusty208.f` 源码 + 重编(优先级 2/5
---
## 八、修订审计记录(2026-08-11
初版报告完成后,作者调用三个独立 subagent 分别审查「数据论断 / TLUSTY 源码 / 框架源码与结论合理性」,作者对最关键的两条(A3 作用域、NaN 计数)亲自复核确认属实,随后按审查意见修订全文。本节记录所有修订,保留可追溯性。
### 8.1 已修正的错误
| # | 位置 | 初版错误 | 修订内容 | 严重度 |
|---|---|---|---|---|
| 1 | §2.3 | `atmosphere_has_nan: true (1033/1105)` —— 实为 434/11051033 是 seed_nc 首失败点数,被误当 NaN 计数) | 改为 434/1105,并按 NaN 拆分两档失效特征 | 🔴严重 |
| 2 | §3.1 A3 | PSI 布局标签张冠李戴:"结构量被 ORELAX 松弛、布居不被松弛" —— 实际相反(ORELAX 松弛布居,结构量不被松弛) | 重写 A3,按 `tlusty208.f:3910-3936``795-797` 给出正确布局 | 🔴严重 |
| 3 | §四 优先级 2 | ORELAXR 改法是**替换**原 `I.GE.``I.LT.`,会移除布居松弛;且 ORELAXR 在源码不存在 | 改为**新增一行**并存;标注需扩 VARNAM/PVALUE 数组,非"改 2 行" | 🔴严重 |
| 4 | §3.1 A1 | 暗示"DPSILG=10 适用于所有结构量" —— 温度实际受更严的 DPSILT(1.25) 钳到 ±25% | 补 DPSILT/DPSILN/DPSILD 按变量限幅说明 | 🟡中 |
| 5 | §3.1 A2 | 行号 14710(实为 1470014710 是铁线 LITEK 逻辑);"第 1 步任意大"过强 | 行号改 14700;措辞改为"首步无 STOP,但仍受 A1 限幅钳制" | 🟡中 |
| 6 | §3.1 | 遗漏 CHMAXT(默认 0.01)的过程反馈机制(`14711``22954-22958` | 新增 A5 | 🟡中 |
| 7 | §3.1 / §6.2 | 遗漏 ITEK/Kantorovich(默认 4iter≥5 切换);雪暴起点与之重合却标"失败相关性:低" | 新增 A6ITEK 改标"待排查" | 🟡中 |
| 8 | §3.2 B2 | "雪崩后仍然把含 NaN 的 fort.7 喂给 nl"未区分 rc=0/rc≠0 | 区分两种 TLUSTY 退出路径 | 🟡中 |
| 9 | §3.2 B3 | "ORELAX=0.3 只对结构量"(依 A3 旧错误) | 改为"作用于布居" | 🟡中 |
| 10 | §3.2 B4 | "每次重跑参数完全一致(除了种子来源)"自相矛盾 | 区分同策略白跑 vs 跨策略换初值 | 🟡中 |
| 11 | §2.4 | "种子不是问题(关键反证)"过强;exact_family 允许 ΔTeff 5000K | 标题与结论改为"不是唯一根因,但不能排除贡献因素" | 🟡中 |
| 12 | §2.5 | 未排除成功/失败点的种子来源混淆变量 | 补充混淆变量说明 | 🟡中 |
| 13 | §6.2 | DPSILG 行号 1786(实为 17931786 是 POPZCH 区段);范围 1750-1806(实结束于 1804);漏列 CHMAXT/DPSILD/NDORELAX 含义写"结构量" | 全表重写,每项带 PVALUE 行号与 idx | 🟡中 |
| 14 | §6.3 | 末步 max_relc 漏列 (1,10] 5 点 + 17 点空 history,加总不闭合 | 补齐使加总闭合 | 🟢轻微 |
| 15 | §2.1 | "平均重试 7-9 次"(实均值 6.9 | 改为"均值 6.9 次" | 🟢轻微 |
| 16 | §五 | 救回率 30-50/70-85/90%+ 当作事实呈现(无数据支撑) | 标注"假设性估计",强调方案 A 实测值是诊断信号 | 🟢轻微 |
| 17 | §四 优先级 1 | "~300 行 Rust"低估 | 改为"400-600 行" | 🟢轻微 |
### 8.2 新增内容
- **§3.3 未排除的替代根因**TFLOOR=8000K、ND=50、DDNU=50/CNU1=6、原子数据 bug、LTE 干净度、输入文件渲染 bug —— 6 条独立于 A1-A6 的数值配置假说
- **§8 本审计记录**
### 8.3 未改动(审查确认正确)的核心结论
- 失败点参数分布(高 Teff + 低 logg 集中):36 格 Teff×logg 矩阵全部精确 ✅
- 1105 全部走完 cold_run→seed_steptasks 历史验证 ✅
- 首失败阶段 seed_nc=1033/nc=72 ✅
- 种子方向 poor=662/rich=373/same=70 + 全 exact_family ✅
- 两条 max_relc 雪崩轨迹样例(逐值)✅
- §2.5 危险区 cold/seed 成功拆分(4 格)✅
- 框架 B1(无链内重试)、B4(数值参数零扰动)✅
- **整体方向**:物理吸引域窄 + 框架无自适应 → 方向正确,仅论证细节有上述瑕疵
### 8.4 审查方法
三个 Explore subagent 并行独立审查,任务是**挑错找反例而非确认**:
- 数据核验:12 项独立 SQL/脚本复现(9 一致 / 2 轻微 / 1 错误)
- TLUSTY 源码:8 项行号/逻辑/默认值核对(数值 9/9 对 / 行号错位多 / A3 布局颠倒)
- 框架源码与结论:8 项源码引用 + 因果严谨性(B1/B3/B4 扎实 / 种子反证与救回率有漏洞)
作者对最致命两条(#1 A3、#2 NaN)用独立脚本/源码核对确认属实后,方才修订。