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
+212
View File
@@ -0,0 +1,212 @@
---
name: tlusty-synspec-test
description: 规范直接运行 TLUSTY / SYNSPEC 二进制做物理验证测试的全流程——目录脚手架、输入文件生成与校验、冷启动链(lte→nc→nl)/种子步进链(seed_nc→nl)/SYNSPEC 执行、以及基于 DCTS 权威判据的事后物理审查。只要用户要在 DCTS 框架之外直接跑 tlusty_static / synspec_static 做收敛性、发散复现、物理解、光谱合成、fort.7/fort.9 分析、冷启动/种子链、网格点验证等测试,或在 dcts/test/ 下建测试目录,就必须用本 skill——即使用户没说"测试流程"四个字。
---
# TLUSTY / SYNSPEC 物理测试流程
本 skill 把 `docs/testing_workflow_2026_08_11.md` 的规范、源码侧 `tests/{tlusty,synspec}/` 的官方 Runtest 结构、以及 DCTS `crates/common/src/` 的输入生成器与物理校验逻辑,统一成一套可执行流程。
## 核心原则(违反这些 = 测试无效)
1. **测试必须建在 `dcts/test/` 下**(已被 `.gitignore` 排除,不入库)。禁止在仓库根、`data/runtime/`、共享目录直接跑二进制。
2. **二进制优先用 `dcts/assets/tlusty_static` 与 `dcts/assets/synspec_static`**——它们比 `data/runtime/` 下的旧版新,且是生产链实际使用的版本。`data/runtime/` 的旧版仅用于历史对照。
3. **物理性判定必须用 DCTS 权威判据** `check_temperature_structure`(见 §事后审查),不要用"表层应 0.51.5×Teff"等经验标准——后者曾误判物理模型为不物理。
4. **执行前必须检查输入**(见 §执行前检查)。一次输入错误会浪费一次 20 分钟级的计算。
5. **执行后必须回头审查**,不能只看"有没有 STOP"。有 STOP 但模型含 NaN = 非物理;无 STOP 但温度结构不物理 = 仍未完成。
## 第一步:判断测试类型(决策树)
先问用户(或从任务推断)属于哪类,再去读对应的 reference:
| 用户在做什么 | 测试类型 | 链类型 | 读哪个 reference |
|---|---|---|---|
| 从零算一个网格点的大气(LTE 灰化起步 → NLTE 连续 → NLTE 谱线) | `convergence`/`physics` | **冷启动链** lte→nc→nl | `references/tlusty-stages.md` |
| 从一个**已收敛邻居模型**热启动算新网格点 | `convergence` | **种子步进链** seed_nc→nl | `references/tlusty-stages.md` |
| 复现生产的发散/失败,对比生产 DB 轨迹 | `validation` | 冷启动或种子链(看生产记录) | `references/tlusty-stages.md` + `physics-checks.md` |
| 给定一个收敛大气(fort.7),合成光谱 | `synspec` | SYNSPEC 单步 | `references/synspec-inputs.md` |
> 区分冷启动 vs 种子链的关键:冷启动 lte 阶段 `ltgray=T`,会**删除 fort.8**从灰大气重建;种子链第一阶 `seed_nc` 把**邻居的 fort.7 复制成 fort.8** 热启动。如果输入里有 `fort.8`(邻居种子)→ 种子链;如果没有、从 lte 开始 → 冷启动链。
## 第二步:建目录(用脚手架脚本)
四级目录结构(详见 `references/directory-convention.md`):
```
dcts/test/{test_id}/{type}/{grid}/{content}/
├── inputs/ # 执行前生成并检查的全部输入
├── scripts/ # run.sh(必有)
├── run/ # 执行工作目录,fort.* 在此生成
└── outputs/ # 阶段产物(按阶段重命名)+ RESULT.md
```
**直接用脚手架,不要手敲 mkdir**
```bash
bash .agents/skills/tlusty-synspec-test/scripts/new_test.sh \
20260813_my_purpose convergence t60000_g5.0_he-2_cold baseline
```
`new_test.sh` 会创建四级目录、按链类型拷贝对应 `assets/run_*.sh` 模板为 `scripts/run.sh`、放一个 README 骨架。它会询问链类型(cold/seed/synspec)来选模板。
### 命名规范
- `test_id` = `{日期}_{目的}`,如 `20260813_tlusty_divergence`
- `type``convergence | physics | synspec | validation`
- `grid` = `t{teff}_g{logg}_he{loghe}`,冷启动链加 `_cold`、种子链加 `_seed`(如 `t60000_g5.0_he-2_cold``t55000_g5.0_he-4_seed`
- `content` = 描述测试变量,如 `baseline` / `itek0_iacc0_dpsilg15` / `niter50` / `nd70`
## 第三步:生成输入文件
**优先级**:有 DCTS 代码生成器就用代码生成(保证与生产一致) > 无生成器则从已知正确模板 sed/python 改(必须留 diff 或注释) > 绝不凭空手写。
### 各链需要的输入(速查)
| 链 | inputs/ 文件 |
|---|---|
| 冷启动链 | `lte.5` `nc.5` `nl.5` `lte.nst` `nc.nst` `nl.nst`**无 fort.8** |
| 种子链 | `seed_nc.5` `seed_nc.nst` `fort.8`(邻居收敛模型的 `.7`);若跑到 nl 还需 `nl.5` `nl.nst` |
| SYNSPEC | `fort.5` `fort.55` `fort.8`(收敛大气)+ `fort.19`/`data` 软链 |
各文件的逐字段含义、各阶段差异、ilvlin/DPSILG/ORELAX/NITER 默认值——见 `references/tlusty-stages.md`TLUSTY)与 `references/synspec-inputs.md`SYNSPEC fort.55 九行)。
**关键不变量**(违反即输入错误,先记这几条):
- 冷启动链 `.5` 第 2 行:`lte.5`=`T T``nc.5`/`nl.5`=`F F`
- ions 行 `ilvlin`lte/nc 阶段=0nl 阶段=100**裸核(nlevs=1)永远=0**
- `.5` 第 3 行的 `'nst'` 文件名必须与实际 nst 文件名一致(生产默认就是字面量 `nst`
- nst 第 1 行含 `NITER=`;第 2 行可含 `ORELAX=/IACC=/ICHANG=/IELCOR=`DPSILG 通过 escape hatch 注入(默认无,生产常用 `DPSILG=1.5` 抑制雪崩)
- `data/` 软链必须指向 `assets/data/`133 个原子数据文件)
### 阶段变换与 A/B 对照
**nc.5 → nl.5**(阶段变换,ilvlin 0→100)用 `scripts/stage_transform.py`,它字节级安全地改写离子行的 ilvlin 列(裸核保持 0)并按需改第 2 行 LTE/LTGRAY。**不要手写 awk 做这事**——awk 按空格切分会把 `'data/h1.dat'` 这类带空格引号路径拆碎,破坏输入(实测踩坑)。
```bash
python3 .agents/skills/tlusty-synspec-test/scripts/stage_transform.py inputs/nc.5 -o inputs/nl.5 --to nl --diff
```
**A/B 测试**:同 `{grid}` 下放多个 `{content}` 目录(baseline / 变体),生成后必须用 `scripts/diff_configs.py` 确认各组**只有你刻意改的参数不同**(带 `*` 的行应只有 ITEK/ORELAX/NITER 等),其余一致——这是对照有效的前提。
```bash
python3 .agents/skills/tlusty-synspec-test/scripts/diff_configs.py dirA/inputs dirB/inputs --teff 60000
```
## 第四步:执行前检查(必做,用脚本)
跑计算前,用校验脚本确认输入无误:
```bash
# TLUSTY 链:校验 .5 第2行/ions、nst 关键参数、软链
python3 .agents/skills/tlusty-synspec-test/scripts/check_inputs.py \
path/to/{content}/inputs --chain cold --teff 60000
```
`check_inputs.py` 会检查上面"关键不变量"那几条。**建议再做一次冒烟**(`timeout 30` 跑 lte 阶段),确认二进制能启动、fort.6 里回显的 NITER/ITEK/DPSILG 与输入一致,再全量跑。
## 第五步:执行(用 run.sh 模板)
每个测试目录的 `scripts/run.sh` 已由 `new_test.sh``assets/` 拷好。直接:
```bash
cd dcts/test/{test_id}/{type}/{grid}/{content}
bash scripts/run.sh # 可选参数:每阶段超时秒数(默认 1200)
```
模板 run.sh 已内置规范行为(`assets/run_cold_chain.sh` 等):
1. 切到 `run/`,复制 `inputs/*` 进去,建 `data` 软链
2. **逐阶段执行**,每阶段 `timeout 1200 tlusty_static < stage.5 > fort.6 2> fort.14`
3. **每阶段产物立即按阶段重命名**`fort.6→{stage}.6``fort.7→{stage}.7``fort.9→{stage}.9``fort.14→{stage}.14`(避免被下阶段覆盖)
4. **阶段间种子传递**:下一阶段 `fort.8` = 上一阶段 `fort.7` 的复制;冷启动链 lte 阶段删 `fort.8`
5. 归档到 `outputs/`
> **为什么按阶段重命名是关键**:TLUSTY 每次都写同名 `fort.7`/`fort.9`,不重命名的话 lte 的产物会被 nc 覆盖,事后无法分阶段审查。官方 `RTlusty` 脚本用 `{model}.{ext}` 命名(如 `hhe35lt.7`)解决同样问题;我们用 `{stage}.7` 等价。
## 第六步:事后审查(必做,用脚本 + 判据)
执行完,用脚本做数值与物理审查,**不要只看 rc=0**。
> ⚠️ **审查时机**:**必须等进程完全退出后再审查**`bash scripts/run.sh` 完成、或 `pgrep -f tlusty_static` 为空)。TLUSTY 边算边写 fort.9——运行中实时读,最新一拍的迭代行是半写状态,max_relc 不可信(实测踩坑:运行中读到 iter6=0.477 貌似收敛,进程退出后同一迭代实为 1060)。run.sh 里每个阶段是串行的(一个阶段跑完才进下一阶段),所以等 run.sh 结束即可放心审查。
```bash
# 1. 收敛轨迹:解析 {stage}.9 的 max_relc 演化、检测 STOP/NaN。
# --expected-niter N 传 nst 里配的 NITER:若末次 iter 远小于 N 且未收敛,
# 会提示"疑似 timeout 截断"(轨迹不完整,结论需谨慎)。rc=124 时的
# outputs/{stage}.TIMEOUT 标记也会被自动发现。
python3 .agents/skills/tlusty-synspec-test/scripts/check_fort9.py outputs/nl.9 --teff 60000 --expected-niter 100
# 2. 温度结构物理性(DCTS 权威判据,复刻 conv_check.rs:463
python3 .agents/skills/tlusty-synspec-test/scripts/check_temperature.py outputs/nl.7 --teff 60000
```
### 收敛判据(数值)
- `{stage}.9` 每次 ITER 跨所有深度的 `max_relc` = `|MAXIMUM|` 列最大值
- **收敛** = 末次迭代 `max_relc < chmax`(默认 `0.001`)且有限
- 单调下降 = 好;雪崩(突然涨几个数量级)= 发散
- `{stage}.6` 里的 `**** STOP in SOLVE after ITER N` = 求解器发散中止
### 物理判据(DCTS 权威,必须用这套)
`check_temperature_structure``conv_check.rs:463`)三项全过才算物理:
1. **表层 T < 3 × Teff**`surface_ratio = T[0]/Teff`> 3 判不物理)
2. **每个深度 T ∈ [10, 1e8] K**
3. **无 NaN/Inf**
物理参考(不是硬判据):Eddington 灰大气 `T(τ=0) ≈ 0.84×Teff`(60kK 模型表层应约 5 万 K);已收敛生产种子 `t60000_g5.0_he-2_c-3_n-4_o-4.7` 表层约 0.78×Teff 可作基准。
> ⚠️ **判据修正历史**:旧经验"表层 0.51.5×Teff"过严(误判物理模型为不物理);"表层 3–6 万 K"对 60kK 偏低(误判)。**只信 DCTS 三项判据**。详见 `references/physics-checks.md`。
### 伪收敛陷阱
`max_relc` 极低 ≠ 收敛。若模型已 NaN,在 NaN 上迭代相对变化为 0,会伪装成收敛。务必同时跑温度结构检查 + NaN 检查。NaN 匹配模式(与 Rust 一致):`nan`/`inf`/`infinity`/`***`(3+星)/正指数 `E+300`+。
### 测试不算完成的情况(需继续)
- 有 STOP 但模型含 NaN(非物理)
- 无 STOP 但温度结构不物理
- 无 STOP 且 `max_relc` 未达 0.001
## 第七步:记录结论(用 RESULT 模板)
`outputs/``RESULT.md`(模板见 `assets/result_template.md`),诚实区分:
- **数值收敛**max_relc 达标)vs **物理收敛**(温度结构三项全过)——只有后者才是真正成功
- 记录:测试配置、各阶段迭代/末 max_relc/STOP/NaN/温度结构表、审查结论
完整的 README/RESULT 字段、阶段产物重命名表、目录示例——见 `references/directory-convention.md`
---
## 何时读哪个 reference
| 想知道 | 读 |
|---|---|
| `.5` 每行含义、各阶段(lte/nc/nl)参数差异、ilvlin/DPSILG/ORELAX/NITER 默认、冷启动 vs 种子链、fort.7↔fort.8 传递 | `references/tlusty-stages.md` |
| fort.55 九行控制卡逐行、fort.5/fort.8/fort.19、SYNSPEC 产物(.spec/.cont/.iden)、NaN 修复背景 | `references/synspec-inputs.md` |
| `check_temperature_structure` 完整判据、fort.7 解析算法(`(nd+5)/6` 行 DM、每块 `(numpar+4)/5` 行)、NaN/伪收敛/STOP、判据修正史 | `references/physics-checks.md` |
| 四级目录结构、命名规范、阶段文件重命名表、README/RESULT 模板 | `references/directory-convention.md` |
## 脚本速查
| 脚本 | 作用 |
|---|---|
| `scripts/new_test.sh` | 脚手架:建四级目录 + 拷 run.sh 模板 + README 骨架 |
| `scripts/check_inputs.py` | 执行前校验 `.5`/nst/软链(TLUSTY 链) |
| `scripts/check_fort9.py` | 解析 `{stage}.9` 收敛轨迹、max_relc 演化、STOP/NaN`--expected-niter N` 检测 timeout 截断 |
| `scripts/check_temperature.py` | 复刻 `check_temperature_structure`,对 `{stage}.7` 做温度结构物理判定 |
| `scripts/stage_transform.py` | 冷启动链 `.5` 阶段变换:`nc.5 → nl.5`(非裸核 ilvlin 0→100,字节级安全,避免手写 awk 踩坑) |
| `scripts/diff_configs.py` | A/B 测试配置对照:并排显示多组 nst/.5 参数,`*` 标出不一致项(应只有你刻意改的字段带 `*` |
**rc=124timeout)感知**:三个 `run_*.sh` 模板在阶段被 `timeout` 杀死时会写 `outputs/{stage}.TIMEOUT` 标记并打印告警;`check_fort9.py` 会自动发现该标记(或用 `--expected-niter` 推断截断),在结论里提示"轨迹不完整,结论需谨慎"。**不要把 timeout 截断的轨迹当成完整发散/收敛结论**。
所有脚本都自定位 skill 目录,可从任意测试目录调用,传相对路径即可。
## 固定路径速查
| 项 | 路径 |
|---|---|
| 测试根 | `dcts/test/` |
| TLUSTY 二进制 | `dcts/assets/tlusty_static` |
| SYNSPEC 二进制 | `dcts/assets/synspec_static` |
| 原子数据 | `dcts/assets/data/`133 文件,运行时 `data` 软链指此) |
| 全波段线表 | `dcts/assets/data/gfATO.dat`SYNSPEC `fort.19` 软链) |
| 物理判据源码 | `dcts/crates/common/src/conv_check.rs``check_temperature_structure` @ L463、`check_fort9` @ L68 |
| 输入生成器源码 | `dcts/crates/common/src/{gen_input5,nst_writer,fort55_writer}.rs` |
| 链定义源码 | `dcts/crates/common/src/runner.rs``default_cold_chain` @ L20、`default_seed_chain` @ L67 |
| 流程规范文档 | `dcts/docs/testing_workflow_2026_08_11.md` |
| 源码侧参考测试 | `tests/tlusty/{hhe,cwd,disk,bstar,optab}/``tests/synspec/{hhe,bstar,kurucz,hybrid,pseudonlt,optab}/` |