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,164 @@
# 测试目录结构、命名与文档模板
参考:`docs/testing_workflow_2026_08_11.md`、现有测试 `dcts/test/20260811_tlusty_divergence/``dcts/test/20260812_synspec_fullband/`
## 四级目录结构
每次测试在 `dcts/test/` 下建独立子目录,禁止在 test 根或共享目录直接执行。
```
dcts/test/
├── {test_id}/ # 1层:本次测试唯一目录({日期}_{目的})
│ └── {type}/ # 2层:测试类型
│ └── {grid}/ # 3层:网格/参数区
│ └── {content}/ # 4层:测试内容(测试变量)
│ ├── inputs/ # 输入文件(执行前生成并检查)
│ ├── scripts/ # run.sh(必有)
│ ├── run/ # 执行工作目录(fort.* 在此生成)
│ └── outputs/ # 阶段产物(按阶段重命名)+ RESULT.md
```
`new_test.sh` 会自动建这四级并把对应模板拷成 `scripts/run.sh`
## 命名规范
| 层 | 规则 | 示例 |
| ----------- | ------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- |
| `test_id` | `{日期}_{目的}` | `20260813_tlusty_divergence` |
| `type` | `convergence`(收敛) / `physics`(物理解) / `synspec`(光谱) / `validation`(校验复现) | `convergence` |
| `grid` | `t{teff}_g{logg}_he{loghe}` + 链后缀;可追加 CNO | `t60000_g5.0_he-2_cold``t55000_g5.0_he-4_seed``t50000_g5.0_cno` |
| `content` | 描述测试变量 | `baseline``itek0_iacc0_dpsilg15``niter50``nd70``gfato_100_20000` |
- **链后缀**:冷启动链 `_cold`,种子链 `_seed`
- 脚本统一 `run.sh`;输入文件按链类型命名(见 `references/tlusty-stages.md`)。
## 阶段文件重命名规范(关键)
TLUSTY 每阶段都写同名 `fort.*`,**必须每阶段执行后立即重命名**,否则被下阶段覆盖。
| 阶段产物 | 重命名 | 说明 |
| ------------------------- | -------------- | ----------------------------------------- |
| `fort.6` (stdout) | `{stage}.6` | `lte.6`/`nc.6`/`nl.6`/`seed_nc.6` |
| `fort.7` (模型) | `{stage}.7` | 事后温度结构审查的输入 |
| `fort.9` (迭代收敛记录) | `{stage}.9` | 事后收敛轨迹审查的输入 |
| `fort.14` (stderr) | `{stage}.14` | — |
**种子传递**:下一阶段 `fort.8` = 上一阶段 `fort.7` 的复制(冷启动 lte 阶段删 `fort.8`)。
### 执行后 run/ 目录状态(冷启动链)
```
run/
├── fort.6 fort.7 fort.9 fort.14 # 末阶段(nl)产物
├── lte.6 lte.7 lte.9 lte.14 # lte 阶段(已重命名)
├── nc.6 nc.7 nc.9 nc.14 # nc 阶段
├── nl.6 nl.7 nl.9 nl.14 # nl 阶段
├── data -> assets/data/ # 原子数据软链
└── fort.8 # 种子(若在种子链)
```
## 执行前检查清单(TLUSTY 冷启动链)
执行前对 `inputs/` 逐项确认(`check_inputs.py` 自动做):
- [ ] `lte.5` 第2行 `T T``nc.5`/`nl.5` 第2行 `F F`
- [ ] `.5` 第3行 `'nst'` 与实际 nst 文件名一致(生产默认 `nst`
- [ ] ions 行 `ilvlin`:nc 阶段=0nl 阶段非裸核=100,裸核(nlevs=1)=0
- [ ] nst 第1行含 `NITER=`;第2行 `ORELAX/IACC/ICHANG/IELCOR` 正确;DPSILG 注入值(若有)
- [ ] `data/` 软链指向 `assets/data/`
- [ ] **冷启动链确认无 fort.8**;种子链确认 `fort.8` 存在且无 NaN
建议再加一次冒烟(`timeout 30` 跑 lte),确认 fort.6 回显的 NITER/ITEK/DPSILG 与输入一致。
## 超时与并行
- 不需要超时限制
- 每个 tlusty 进程 ~260MB,按可用内存控制并行度
- 多测试并行时各日志独立(`scripts/run.log` 或外部日志)
## RESULT.md 模板
每个 `{content}/outputs/``RESULT.md`(模板见 `assets/result_template.md`)。核心是**诚实区分数值收敛与物理收敛**:
```markdown
# {test_id} / {type} / {grid} / {content}
## 目的
一句话说清这次测试想验证什么。
## 配置
- 二进制: dcts/assets/tlusty_static(或 synspec_static
- 网格: t{teff}_g{logg}_he{loghe}[_cno]
- 链: 冷启动 lte→nc→nl / 种子 seed_nc→nl / SYNSPEC
- 变体: {ITEK}/{IACC}/{DPSILG}/{ORELAX}/{NITER}(与 baseline 不同的关键参数)
## 输入
- inputs/ 生成方式: 代码生成 / 模板修改(留 diff)
- 关键参数: ...
## 执行
- 命令: bash scripts/run.sh
- 各阶段: lte(rc=0) → nc(rc=0) → nl(rc=?)
- 耗时: ...
## 结果(TLUSTY
| 阶段 | 迭代 | 末 max_relc | STOP | NaN | 温度结构 |
|---|---|---|---|---|---|
| lte | 0 | — | — | 0 | — |
| nc | 10 | 1.4e0 | 无 | 0 | 物理 |
| nl | 19 | 1.2e16 | STOP iter19 | 0 | 物理(停止时) |
## 结果(SYNSPEC,若适用)
| 指标 | 值 |
|---|---|
| 采样点数 | N |
| 波长范围 | ALAM0 ALAST Å |
| NaN/Inf/坏行 | 0 |
| 分段 NaN | EUV/UV/可见/NIR 各段计数 |
## 审查结论
- 数值: ❌/✅(max_relc 是否达标;有无 STOP/雪崩)
- 物理: ❌/✅(温度结构三项:表层<3×Teff、各深度∈[10,1e8]、无 NaN
- 结论: 一句话。区分"数值收敛"与"物理收敛"——只有后者才是真正成功。
## 复现
cd dcts/test/{test_id}/{type}/{grid}/{content}
bash scripts/run.sh
```
## 完整示例(参考现有测试)
```
dcts/test/20260811_tlusty_divergence/
└── convergence/
├── t60000_g5.0_he-2_cold/
│ ├── baseline/ # 生产配置复现
│ │ ├── inputs/ (lte.5 nc.5 nl.5 lte.nst nc.nst nl.nst)
│ │ ├── scripts/ (run.sh)
│ │ ├── run/
│ │ ├── outputs/
│ │ └── README.md
│ └── itek0_iacc0_dpsilg15/ # 修复变体
│ └── ...
├── t60000_g5.0_he-2_seed/
│ └── doubleoff_dpsilg15/ # 种子链变体
│ └── ... (inputs 含 fort.8)
└── t55000_g5.0_he-4_cold/
└── itek0_iacc0_dpsilg15/ # 另一网格点
└── ...
dcts/test/20260812_synspec_fullband/
└── synspec/
└── t50000_g5.0_cno/
└── gfato_100_20000/ # 全波段 SYNSPEC
├── inputs/ (fort.5 fort.55 fort.8 + fort.19/data 软链)
├── scripts/ (run.sh)
├── run/
└── outputs/ (fullband.spec/.cont/.iden + RESULT.md)
```
A/B 测试(baseline vs 变体)放同一 `{grid}` 下两个 `{content}` 目录,便于对照。生成完输入后用 `scripts/diff_configs.py` 确认各组**只有刻意改的参数不同**(带 `*` 的行应只有 ITEK/ORELAX/NITER 等),否则对照无效:
```bash
python3 .agents/skills/tlusty-synspec-test/scripts/diff_configs.py dirA/inputs dirB/inputs dirC/inputs --teff 60000
```
@@ -0,0 +1,114 @@
# 物理校验判据与 fort.7 解析(DCTS 权威)
参考源码:`dcts/crates/common/src/conv_check.rs`。**物理性判定必须以这里的逻辑为准**,不要用经验标准。
## 温度结构判据:`check_temperature_structure`
`conv_check.rs:463`。返回 `TempStructCheckResult``valid = violations.is_empty()`。三项全过才算物理:
1. **表层 T < max_factor × Teff**
- `surface_ratio = temps[0] / teff``temps[0]` = 最浅深度 = 表层)
- `surface_ratio > max_factor`(默认 **3.0**)→ 违规
- `teff <= 0` → ratio = +Inf → 必违规
2. **每个深度 T ∈ [temp_floor, temp_ceiling]**
- 默认 **[10.0, 1.0e8] K**
- 任一深度 T 越界 → 违规
3. **无 NaN/Inf**
- 非有限 T(NaN/Inf)→ 违规(被第 2 条的 `is_finite()` 捕获)
报告**首个**违规深度。
### 物理参考(不是硬判据,仅辅助判断)
- **Eddington 灰大气**`T(τ=0) ≈ 0.84 × Teff`。60kK 模型表层应约 5 万 K。
- **生产基准**:已收敛种子 `t60000_g5.0_he-2_c-3_n-4_o-4.7` 表层约 0.78×Teff。
- **内部温度**:无 1e7+ 异常隆起(如某修复前组合曾出现 6.2e9 K 内部尖峰)。
### ⚠️ 判据修正历史(为何只信 DCTS 三项)
- 旧经验"表层应 0.51.5×Teff":**过严**,曾误判物理模型为不物理(把表层 0.78×Teff 的好模型判死)。
- 旧经验"表层应 36 万 K":对 60kK 模型**偏低**,曾误判。
- **正确做法**:只用"表层 < 3×Teff 且各深度 ∈ [10,1e8] 且无 NaN"三项硬判据,Eddington 0.84 仅作物理直觉参考。
## fort.7 大气模型文件格式与解析
`conv_check.rs:470-502`,对应 TLUSTY 源 `tlusty208.f:14066-14108``OUTPUT` 子程序):
```
第 1 行: ND NUMPAR
ND = 深度点数; NUMPAR = 每深度参数数 = NLEVEL + NUMLT
接下来 ceil(ND/6) 行: DM 列阵(质量深度),每行 6 个值(FORMAT 502
接下来 ND 个深度块: 每块 ceil(NUMPAR/5) 行(FORMAT 503,每行 5 值)
每块【第一行第一个 token】= 该深度的温度 TEMP(ID)
块内其余 = 电子密度、质量密度、NLTE 能级布居/ departure 系数
```
`temps[0]` = 表层(ID=1,最浅);`temps[nd-1]` = 底层(最深)。
### 解析算法(`check_temperature.py` 复刻)
```
1. 读第1行 → nd, numpar = 前两个 token。任一为 0 → 跳过(返回 None,不判失败)
2. dm_lines = (nd + 5) // 6 # 跳过 DM 列阵
block_lines = (numpar + 4) // 5 # 每深度块行数
3. 跳过 dm_lines 行
4. 对 nd 个深度,每个:
取块第一行第一个 tokenparse_fortran_float → temps[id]
跳过 block_lines - 1 行
5. 检查三层判据
```
`parse_fortran_float``conv_check.rs:44`)兼容 Fortran 无-E 记数法:指数 ≥100 时 gfortran 挤掉 E,如 `-1.35E+118` 输出成 `-1.35+118`。解析:先标准 parse;失败则正则 `^([+-]?[\d.]+)([+-]\d+)$` 在尾符号前插 E 再 parse;溢出返回 ±Inf。
> 参考实现见 `conv_check.rs:1339 make_dot7(temps)` —— 构造测试用 fort.7 的辅助函数(写 `ND NUMPAR=3`,一行 DM 占位,每深度一行 `T 1.0E+10 1.0E-15`)。
## 收敛轨迹判据:`check_fort9`
`conv_check.rs:68`。解析 `fort.9`(迭代收敛记录)。
### fort.9 格式
```
RELATIVE CHANGES OF VECTOR PSI
ITER ID TEMP NE POP RAD MAXIMUM ilev ifr
1 50 -8.46E-04 1.53E-03 2.51E-02 -2.20E-03 2.51E-02 74 1
1 49 ...
...
2 50 ...
```
每行 = 一次迭代在一个深度点的相对变化:`ITER(迭代号) ID(深度) TEMP NE POP RAD MAXIMUM(本次迭代本深度的最大相对变化) ilev ifr`
### 判据
- 每次 ITER 的 `max_relc` = 该 ITER 所有深度行 `|MAXIMUM|` 的最大值
- **末次迭代**的 `max_relc` = 整个阶段的结果
- **收敛** = `max_relc < chmax`(默认 **0.001**)且 `is_finite()`
- 单调下降 = 健康;突然涨几个数量级 = 雪崩发散
- `chmax <= 0` 或非有限 = 非法配置(返回错误,max_relc=Inf
### STOP 检测(fort.6
`fort.6`stdout)里的 `**** STOP in SOLVE after ITER N` = 求解器发散中止。`conv_check.rs``SOLVER_STOP_RE` 匹配。这是比"非零 rc"更具体的发散信号。
## NaN / 伪收敛检测
### NaN 匹配模式(与 Rust 一致)
`conv_check.rs:18` `NAN_RE_PATTERN = (?i)(\bnan\b|\binf(?:inity)?\b|\*{3,}|[eE]\+(?:3\d{2}|[4-9]\d{2,}))`
匹配:`nan` / `inf` / `infinity` / `***`(3+星) / 正指数 `E+300`~`E+999`(超高正指数视同数值无效,因发散时布居数变化可达 ±100~±200 量级但温度/流量场溢出用 `***``E+300+`)。
相关函数:
- `atmosphere_has_nan`L246):扫 fort.7 全部数值 token
- `spec_is_valid`L290):扫光谱文件,同正则
### 伪收敛陷阱(必查)
`max_relc` 极低 ≠ 收敛。若模型已 NaN,**在 NaN 上迭代的相对变化为 0**,会伪装成 max_relc≈0 的"收敛"。
**正确流程**(每次事后审查都要做):
1. `check_fort9.py` 看轨迹 + STOP + max_relc
2. `check_temperature.py` 看温度结构三项(含 NaN 检查)
3. **两者都过才算真收敛**;仅 fort.9 看着收敛但 fort.7 有 NaN = 伪收敛
## 其他 DCTS 校验(了解即可)
`conv_check.rs` 还提供(生产链用,测试时按需):
- `check_energy_conservation`L374):能量守恒
- `check_emflux_bolometric`L578):辐射通量
- `check_convergence_trace`L691):收敛轨迹综合
- `check_bfactor`L759):departure 系数(.bfac 文件,同样 ND NUMPAR + DM 头格式)
- `extract_failure_hint`L851):失败原因提取
测试中常用前两类(温度结构 + 收敛轨迹)即可覆盖绝大多数判断。
@@ -0,0 +1,78 @@
# SYNSPEC 输入与执行详解
参考源码:`dcts/crates/common/src/fort55_writer.rs`fort.55 生成)、`config.rs:1184` `SynspecInput`
## SYNSPEC 做什么
给定一个**收敛的 TLUSTY 大气**fort.7/fort.8),SYNSPEC 合成指定波长范围的理论光谱(emergent flux vs wavelength)。输出:连续谱 + 谱线 + 谱线标识。
## 输入文件
| 文件 | 来源 | 说明 |
|---|---|---|
| `fort.5` | SYNSPEC stdin(模型描述) | 与 TLUSTY 的 `.5` 不同;含模型 TEFF/丰度/模式开关。测试里通常从已验证输入复制 |
| `fort.55` | `fort55_writer.rs` 生成 | 控制卡,**恰好 9 行**(有回归测试) |
| `fort.8` | 收敛大气的 `.7` 复制 | 输入大气;TEFF/丰度须与 fort.5 一致 |
| `fort.19` | `assets/data/gfATO.dat` 软链 | 原子线表(全波段,1823000 Å,~238MB278 万条线) |
| `data/` | `assets/data/` 软链 | 原子数据(与 TLUSTY 共用) |
> 可见光子集线表 `assets/gfVIS99.dat`30007000 Å)用于窄段测试;全波段用 `gfATO.dat`。
## fort.55 九行控制卡(逐行)
`fort55_writer.rs:26 generate_fort55_content()`,每行 1-to-1 对应 `synspec54.f` 的一个 `READ(55,*)`
| 行 | 变量 | 默认 | 说明 / 源码行 |
|---|---|---|---|
| 1 | `IMODE IDSTD IPRIN` | `0 0 1` | `synspec54.f:253` |
| 2 | `INMOD INTRPL ICHANG ICHEMC` | `1 0 0 0` | `:254` |
| 3 | `IOPHLI nunalp nunbet nungam nunbal` | — | `:255` 氢线开关 |
| 4 | `IFREQ INLTE ICONTL INLIST IFHE2` | — | `:2146`。**注意 IFREQ 在这行第1列**,不是第1行(历史错位 bug,已加回归测试) |
| 5 | `IHYDPR IHE1PR IHE2PR` | — | `:2148` 氢/氦线详细处理开关 |
| 6 | `ALAM0 ALAST CUTOF0 CUTOFS RELOP SPACE` | `1400.0 1410.0 10 0 0.0001 0.01` | `:2149` **波长范围与采样**。ALAM0=起始 Å,ALAST=结束 Å,RELOP=相对采样步长,SPACE=绝对步长。波长用 `:.1` |
| 7 | `nmlist [iunitm...]` | `0` | `:2208`IMODE∈[-3,1] 时读)。**分子列表行**,必须独占一行,否则会吞掉下一行的 VTB |
| 8 | `VTB` | `-1.0` | `:2251` 微观湍流速度 km/s。**负值**=`-1.0` 表示"不覆盖大气自身 vturb"(来自 TLUSTY 的 2 km/s);正值会触发 `vtb²·1e10` 覆盖 |
| 9 | `NMU0 ANG0 IFLUX` | — | `:2301`IFWIN≤0 时读)角度网格 |
`SynspecInput::validate()``config.rs:1238+`)会拒绝触发不支持的条件 READ 的配置(如 `inmod>0 && intrpl>0` → DM 数组 READ`nmu0<0` → 显式角度数组 READ)。
### 常见波长范围配置(line 6)
- 窄段验证(官方测试):`1400.0 1410.0 ...`
- 可见光:`3000.0 7000.0 ...`
- 全波段:`100.0 20000.0 10 0 0.0001 0.01`SYNSPEC 可靠边界 10023000 Å)
## 输出文件(SYNSPEC 产物)
| fort.* | 复制为 | 内容 |
|---|---|---|
| `fort.7` | `.spec` | 合成光谱(emergent flux vs λ,主产物) |
| `fort.17` | `.cont` | 连续谱 |
| `fort.12` | `.iden` | 谱线标识 |
| `fort.11` | `.lindat` | 谱线数据 |
模板 `assets/run_synspec.sh`:切 `run/` → 复制 fort.5/fort.55/fort.8 + 建软链 → `timeout 1200 synspec_static < fort.5 > synspec.log 2> synspec.err` → 产物复制到 `outputs/{name}.spec/.cont/.iden/.lindat`
## NaN 修复背景(重要)
`synspec_static``-ftoplevel-reorder` 编译,破坏了 SYNSPEC COMMON 块初始化顺序,导致 **Balmer 系限 3646 Å 之后整段 NaN**73% 数据点失效)。修复版用 `-O3 -fno-toplevel-reorder`,全波段 0 NaN。
**验收 SYNSPEC 必查**:分段统计 NaNEUV 100912 / UV 9123646 / 可见 36468200 / NIR 820020000),尤其 36468200 段(旧 bug 重灾区)。详见 `docs/synspec_nan_fix_2026_08_11.md`
## SYNSPEC 校验判据
| 判据 | 工具 | 通过条件 |
|---|---|---|
| 无 NaN/Inf/坏行 | `grep -ciE 'nan\|inf\|\*{3,}' fullband.spec` | = 0 |
| 波长范围正确 | `head -1 / tail -1 fullband.spec` | 首/尾 λ ≈ ALAM0/ALAST |
| 通量物理 | 抽查关键 λ(Lyα 1216、Balmer 3646/6563 | 有限、非全零、形态符合 Teff |
| 谱形 | EUV 峰 → 长波单调下降 | 与 Teff 一致(热星 EUV 占优) |
> 注:深吸收线心可能有少量微负通量(数值残差),不是 NaN/Inf,不影响"0 NaN"结论。绘图时把负通量裁成 NaN 即可。`spec_is_valid``conv_check.rs:290`)只判 NaN/Inf/坏行。
## 官方 Runtest 结构(源码侧参考)
`tests/synspec/{hhe,bstar,kurucz,hybrid,pseudonlt,optab}/``R1` 调用 `RSynspec`
```
$TLUSTY/RSynspec hhe35nl fort.55.con data/gfATO.dat
```
`RSynspec` 做的事:`cp $1.7 fort.8`(输入大气);`ln -s $2 fort.55`(控制卡);`ln -s $3 fort.19`(线表);`synspec.exe < $1.5 > results/$1.log``cp fort.7/17/12 results/$1.spec/.cont/.iden`。Kurucz 测试还演示了不用 RSynspec 直接手动链软链 + 跑二进制的写法(见 `tests/synspec/kurucz/R1` 第二段)。
@@ -0,0 +1,115 @@
# TLUSTY 链阶段与输入文件详解
参考源码:`dcts/crates/common/src/runner.rs`(链定义)、`gen_input5.rs``.5` 生成)、`nst_writer.rs``nst` 生成)、`seed_finder.rs`(种子选择)。
## 两种链
### 冷启动链(cold-start):从零算一个网格点
`runner.rs:20` `default_cold_chain()`,三阶段 `lte → nc → nl`
| 阶段 | 含义 | `.5` 第2行 LTE/LTGRAY | ilvlin | NITER | fort.8 处理 | require_converged |
|---|---|---|---|---|---|---|
| **lte** | LTE 灰大气起步(从灰大气解析解重建) | `T T` | 0 | 0 | **删除**(ltgray=T 强制从灰大气重建) | 否 |
| **nc** | NLTE 连续性方程(无谱线) | `F F` | 0 | 10 | 复制 lte 的 fort.7 → fort.8 | 否 |
| **nl** | 完整 NLTE(含谱线) | `F F` | 100 | 100 | 复制 nc 的 fort.7 → fort.8 | **是**(不收敛则链中断) |
物理直觉:lte 给一个解析的灰大气初值(快、稳但不真实)→ nc 松弛到 NLTE 连续(电子/原子能级 NLTE,但不解谱线辐射转移)→ nl 再加入谱线(最贵、最易发散)。每阶用上一阶收敛模型热启动,是 TLUSTY 控制 NLTE 发散的标准手法。
### 种子步进链(seed-stepping):从已收敛邻居热启动
`runner.rs:67` `default_seed_chain()`,两阶段 `seed_nc → nl`
| 阶段 | NITER | ICHANG | ORELAX | fort.8 | require_converged |
|---|---|---|---|---|---|
| **seed_nc** | 20 | 0 | 0.3 | **邻居收敛模型的 .7 复制成 fort.8** | 否 |
| **nl** | 100 | 0 | 0.5 | 复制 seed_nc 的 fort.7 → fort.8 | 是 |
何时用种子链:网格上邻近点已收敛(如生产 DB 里的模型),新点与它 `|Δteff|<5000, |Δlogg|<0.01, |Δloghe|<0.01` 时(`seed_finder.rs:51` 的 exact-family 判定),热启动比冷启动稳得多。种子距离有方向性:金属贫向种子成功率 42–54%,富向仅 3–11%(`directed_cno_distance` 对富向罚 4×),`MAX_GLOBAL_SEED_DISTANCE=3.0`
## `.5` 主输入文件格式(TLUSTY stdin
`gen_input5.rs:140 make_input5()` 生成。**注意**:DCTS 实际只写一个 `<name>.5` 每阶段覆盖,阶段差异来自 `ChainStep` 而非文件名;测试里我们用 `lte.5`/`nc.5`/`nl.5` 分别保存各阶段版本。
| 行 | 内容 | 说明 |
|---|---|---|
| 1 | `{teff:.1} {logg:.1} ! TEFF, GRAV` | 恒星几何:有效温度、表面重力 |
| 2 | ` {lte} {ltgray} ! LTE, LTGRAY` | **行首有一个空格**(Rust 字面保留,有回归测试);lte 阶段=`T T`nc/nl=`F F` |
| 3 | `'nst' ! name of file containing non-standard flags` | nst 文件名;生产默认字面量 `nst``config.rs:739`),所有阶段同名 |
| 4 | 注释分隔 | — |
| 5 | `* frequencies` | 频率网格段头 |
| 6 | `{nfread} ! NFREAD` | 频率点数,默认 2000`config.rs:742` |
| atoms 块 | 头 `natoms [iabset]`,每行 `mode abn modpf` | 原子种类与丰度(H/He/C/N/O…),丰度 `fmt_abn(logx)=10^logx``{:.4E}` |
| ions 块 | 头 + 每离子 `iat/iz/nlevs/ilast/ilvlin/nonstd typion filei`,终止行 `0 0 0 -1 0 0` | 离子能级 |
| (可选) CHANGE | `ichang<0` 时追加 | 参数修改块 |
### ilvlin 规则(易错点)
`gen_input5.rs:49-53``ilvlin = if ion.nlevs == 1 { 0 } else { chain.ilvlin }`
- **裸核(nlevs==1,即只有一个能级的离子)永远 ilvlin=0**,不论阶段。
- 非裸核:lte/nc 阶段 ilvlin=0nl 阶段 ilvlin=100`runner.rs:24,39,54`)。
- 测试清单:lte/nc 的非裸核=0,nl 的非裸核=100,所有裸核=0。
> **从 nc.5 生成 nl.5**:用 `scripts/stage_transform.py nc.5 -o nl.5 --to nl --diff`,自动按上述规则字节级改写(非裸核 0→100、裸核保持 0、L2 改 F F)。**不要手写 awk**——awk 按空白切分会把 `'data/h1.dat'` 引号路径内的空格拆碎,实测会破坏离子行。
## `nst` 非标准参数文件格式
`nst_writer.rs:36 generate_nst_content()` 生成。三级覆盖优先级:`ChainStep` 阶段字段 > `global.nst` 结构化默认 > `nst_extra_keys` escape hatch(一行一个 `KEY=VALUE`)。
| 行 | 字段 | 说明 |
|---|---|---|
| 1 | `ND=,NLAMBD=,VTB=,ISPODF=,DDNU=,CNU1=`(必有);`CHMAX=`(若配);`ITEK=`(若配);`NITER=`(必有) | 例:`ND=50,NLAMBD=3,VTB=2.,ISPODF=1,DDNU=50.,CNU1=6.,CHMAX=0.001,NITER=10` |
| 2 | `ORELAX=`(若配) `IDLTE=`(若配) `IACC=`(若配) `ICHANG=`(若配) `IELCOR=`(必有,默认 -1 | 例:`IELCOR=-1`;种子链 nl`ICHANG=0,ORELAX=0.5,IELCOR=-1` |
| 3+ | `NMU, ISPLIN, CNU2, NFTAIL, DFTAIL``FRCMAX/FRCMIN/FRLMAX/FRLMIN`(仅非零才写,零=用 TLUSTY 内部默认);`HMIX0, MLTYPE, IPTURB, IBFINT, IHECOR, IRTE, IBC, IFRYB, IOPTAB, IFMOL, IFALI, IFPOPR, JALI, TRAD, WDIL, TFLOOR, TDISK, TMOLIM` | 结构化物理参数 |
| 末 | escape hatch:一行一个 `KEY=VALUE` | 校验:key/value 不含 `,` `\n` `=` 空白 |
### 关键参数含义
- **NITER**:本阶段最大迭代数。lte=0(解析解,不迭代),nc=10nl=100;种子 seed_nc=20。
- **CHMAX**:收敛阈值,默认 0.001。末次迭代 `max_relc < CHMAX` 才算收敛。
- **ITEK / IACC**:迭代控制(ITEK=预条件/加速策略,IACC=累加策略)。生产默认不显式写(用 TLUSTY 内部默认)。
- **ORELAX**:过松弛因子。种子链 seed_nc=0.3、nl=0.5;冷启动链默认不写(TLUSTY 内部决定)。
- **ICHANG**=0 表示禁止 TLUSTY 自动调整深度网格(种子链固定邻居网格用)。
- **IELCOR**:能级修正开关,默认 -1。
- **DPSILG****不在结构化字段里**,通过 escape hatch 注入。默认 TLUSTY DPSILG=10(允许每步 10× 跳变);生产用 `DPSILG=1.5` 钳到 ±1.5×,抑制 NLTE 雪崩(实测末 max_relc 从 4.71e6 降到 48.6)。见 `workflows/sdB_cno.yaml:116-117`
### `fmt_real` 细节(字节兼容)
整数加尾点(`2.0 → "2."`),`-0.0 → "0."`。若用脚本手写 nst 要注意这点,否则与生产字节不一致。
## fort.7 ↔ fort.8 传递(链的核心机制)
`runner.rs` 每阶段循环:
1. **初始 seed**L321-335):删 `fort.8`;若 `seed_atmos`(邻居模型路径)非空则复制成 `fort.8`。冷启动链传 `None` → 不存在 `fort.8`
2. **每阶段 fort.8 准备**L379-393):
- `ltgray=="T"`(仅冷启动 lte):删 `fort.8`(强制灰大气重建)
- 否则若 `current_seed` 有值:复制 `current_seed``fort.8`(热启动)
- `current_seed` 初值 = `seed_atmos`,所以种子链第一阶直接用邻居模型
3. **跑 TLUSTY**:读 `fort.8` 为输入大气,输出新大气到 `fort.7`
4. **fort.7 成为下阶段种子**L518-521):`rc==0 && fort.7 存在` → 快照成 `<name>.<label>.7``current_seed` 更新为该路径。
5. **最终大气**L573-581):`current_seed` 复制成 `<name>.7`(规范最终模型)。
6. **交给 SYNSPEC**L694-702):最终 `<name>.7` 复制成 `fort.8`SYNSPEC 的输入大气单元)。
### test 里的 run.sh 怎么做
模板 `assets/run_cold_chain.sh` 每阶段:
```
[stage=lte] rm -f run/fort.8
cd run && timeout 1200 tlusty_static < stage.5 > fort.6 2> fort.14
cp fort.6 ../{stage}.6 ; cp fort.7 ../{stage}.7 ; cp fort.9 ../{stage}.9 ; cp fort.14 ../{stage}.14
[fort.7 存在] cp fort.7 fort.8 # 下阶段种子
```
## 官方 Runtest 结构(源码侧参考)
`tests/tlusty/{hhe,cwd,disk,bstar,optab}/` 每个目录有 `AAAREADME` + `R1` 脚本 + 参考结果 `results/``R1` 调用 `RTlusty`
```
$TLUSTY/RTlusty hhe35lt # 冷启动第一阶(无种子)
$TLUSTY/RTlusty hhe35nc hhe35lt # nc,种子=hhe35lt.7
$TLUSTY/RTlusty hhe35nl hhe35nc # nl,种子=hhe35nc.7
```
`RTlusty` 做的事(等价于我们的 run.sh 单阶段):建 `data` 软链;若第二参数存在则 `cp $2.7 fort.8`(种子);`tlusty.exe < $1.5 > $1.6``cp fort.7/9/69/14 $1.7/9/69/14`。即官方用 `{model}.{ext}` 命名各模型产物,我们用 `{stage}.{ext}` 等价(一个模型内分阶段)。
**对照方法**:官方 `results/` 存参考输出,测试跑完与 `results/` 比对(数值容差)。我们的事后审查更强:除了"有没有 STOP",还做温度结构物理判定(官方 Runtest 不做)。