Files
DCTS/docs/fort14_nan_analysis.md
T
fmq 43b82b1ae2 feat(all): 物理正确性五重硬门槛、输入文件结构化与 fort.55 错位修复、conv 诊断 DB 化与阶段归因修复、ORELAX 收敛修复与导入工具下线
物理正确性校验体系(common/conv_check.rs +494 行)
- 新增 5 类硬门槛:能量守恒(.6)、温度结构(.7)、emflux 积分校验(.emflux,含全 NaN 判失败)、假收敛排查(itek 轨迹首末比)、b 因子合理性(.bfac)
- runner 在 TLUSTY 阶段结束后执行全部校验,任一失败判 final_converged=false
- GridConfig 新增 8 个可配阈值,经 scheduler→executor→runner 全链路透传

输入文件配置结构化重构(config.rs +1453 行)
- TlustyInput 拆为 dot5/nst 分层结构,字段名严格映射 tlusty208.f READ 语句;SynspecInput 重构为 9 个 Fort55Line 子结构体
- 移除 ChainStep.metals 字段,元素集改由 dot5.atoms/ions 显式声明(gen_input5/nst_writer 同步重写为三源融合 / 分层覆盖)
- fort.55 修复行结构 bug:补全分子表行(7→9 行),IDSTD 50→0 错位修正(影响全部光谱线强归一化,需重算 SYNSPEC 阶段)

conv 诊断 DB 化与阶段归因修复(server)
- 单点详情 conv 面板从磁盘 conv.json 改读 DB grid_points.summary_json;grid_points 新增 summary_json/last_elapsed_sec 两列(旧库幂等 ALTER)
- record_task_report 阶段归因列加 CASE 守卫 + clear_synspec 对称处理,修复 synspec-only/TLUSTY-only 重跑污染统计
- 新增 summary_merge.rs 点级增量合并,避免重跑覆盖诊断字段

收敛性 ORELAX 修复与 seed_chain 可配(sdB_cno.yaml + node)
- nl 阶段加 orelax=0.5、seed_nc 加 orelax=0.3,阻尼中温区 relc 振荡发散
- seed_chain 块可配,executor 优先采用用户配置而非内置默认链

导入工具下线
- 删除 import_results 客户端工具及 Windows 推送脚本;移除 /admin/import_seed 端点
- 改为服务端临时 migrate_conv 端点(扫 conv.json 增量合并入库,迁移后可删)

文档与分析
- 新增 1305 失败点根因分析、fort.14 全 NaN 物理含义分析两份深度文档
- spectrum_correctness_analysis 两次修订标注已修复项;fetch_results.sh 修 trap RETURN 的 set -u 报错
2026-08-09 12:09:48 +08:00

239 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# fort.14`.emflux`)全 NaN 现象分析
> 调研日期:2026-08-08
> 范围:TLUSTY 208 输出文件 fort.14(出射谱 λ-Fλ,runner 快照为 `.emflux`)中出现全 NaN 的物理与数值含义。
> 核心问题:**`.emflux` 全 NaN 意味着什么?是否代表 TLUSTY 大气模型收敛失败?**
---
## 0. TL;DR
**`.emflux` 全文件 NaN = TLUSTY 大气模型结构(TEMP/POPUL/ELEC)数值发散到不可用程度的可靠标志。** 源码机制与真实数据双重佐证:
- **源码层面**fort.14 的流量 `FLUX` 由辐射转移求解器(RTEFR1)基于大气结构(TEMP/POPUL)计算。大气发散 → 不透明度计算产生 Inf/0 → 源函数 Inf/Inf=NaN → 整列 FLUX 全 NaN → OUTPRI 在耗尽 NITER 上限后写进 fort.14。全 NaN 只能由大气发散引起,**不存在"大气收敛良好但辐射转移单独失败"的路径**。
- **数据层面**162 个网格点中,**62/62100%)的 emflux-NaN 点 `converged=false`**`converged=true` 且 emflux 含 NaN 的点 = **0**。没有任何局部 NaN 样本。
**对校验设计的结论**emflux bolometric 校验把"全 NaN 判失败"是**正确的**,语义应表述为"**大气收敛失败(辐射转移求解发散)**",而非仅"辐射转移失败"。它与 `atmosphere_has_nan` 同级、互为冗余,且能捕获后者的检测盲区。
---
## 1. fort.14 的写出机制
### 1.1 写出位置与格式
fort.14 由 `SUBROUTINE OUTPRI``tlusty208.f:14156`)写出,唯一活跃写出语句在 **`tlusty208.f:14188`**
```fortran
14181 DO IJ=1,NFREQ
14182 IJP=IJ
14183 IF(ispodf.eq.0) IJP=JIK(IJ)
14184 IF(IJX(IJP).NE.-1) THEN
14185 WRITE(13,602) FREQ(IJP),FLUX(IJP),FH(IJP)
14186 TOTF=TOTF+FLUX(IJP)*W(IJP)
14187 FLAM=FLUX(IJP)*FREQ(IJP)*FREQ(IJP)/2.997925E18
14188 write(14,614) 2.997925e18/freq(ijp),slam
14189 END IF
14190 END DO
```
- **FORMAT** `614 FORMAT(F15.3,1pe15.3)``tlusty208.f:14406`)—— 第一列波长 λ(Å),第二列 Fλ。
- **写出变量**`FLUX(IJP)`COMMON/SURFAC/`MODELQ.FOR:135`),不是局部量。`FLAM` 是从 Fν 到 Fλ 的换算(`Fλ = Fν · ν² / c`)。
- **关键**`FLUX` 是直接从 COMMON 块读已存的值,OUTPRI 本身**不重新求解辐射转移**。
### 1.2 写出时机
OUTPRI 仅在 `IF(LFIN)` 块内被调用(`tlusty208.f:3887-3889`):
```fortran
3887: IF(LFIN) THEN
3888: CALL OUTPRI
3889: END IF
```
`LFIN` 的判定(`tlusty208.f:47596`):
```fortran
47596: LFIN=ABS(CHMX).LE.CHMAX.OR.ITER.GE.NITER
```
即**最后一次迭代**(收敛 `CHMX≤CHMAX` 或耗尽迭代上限 `ITER≥NITER`)才写 fort.14。这意味着:fort.14 反映的是**最终大气状态**下的辐射转移解。
---
## 2. NaN 的产生链(源码追踪)
### 2.1 FLUX 的计算路径
`FLUX(IJ)` 的赋值有三处(都在辐射转移求解子程序中):
| 位置 | 子程序 | 语句 |
|------|--------|------|
| `tlusty208.f:31800` | `RTEFR1`Feautrier 形式解) | `FLUX(IJ)=AHout*half` |
| `tlusty208.f:32225` | `RTEDF2` | `FLUX(IJ)=AH` |
| `tlusty208.f:32848` | `RTEDF1` | `FLUX(IJ)=FH(IJ)*RAD1(1)-half*hextrd(ij)` |
这些在每次迭代的频率循环中被调用(如 `tlusty208.f:4269-4271`):
```fortran
4269: DO IJ=1,NFREQ
4270: CALL OPACF1(IJ) 不透明度计算
4271: CALL RTEFR1(IJ) 辐射转移求解
```
### 2.2 NaN 的具体产生点
**第一步:不透明度计算 OPACF1`tlusty208.f:4800`)依赖大气结构**
| 源码行 | 语句 | 发散时 |
|--------|------|--------|
| `tlusty208.f:4857` / `:45375` | `HKT1(ID)=HK/T`T=TEMP(ID) | TEMP→0 时 HKT1→Inf |
| `tlusty208.f:4858` | `XKF(ID)=EXP(-HKT1(ID)*FR)` | HKT1·FR 极大时下溢到 0 |
| `tlusty208.f:31444` / `:33455` | `ABTRA(ITR,ID)=POPUL(II,ID)` | POPUL 含 NaN → ABTRA 含 NaN |
| `tlusty208.f:31446` | `EMTRA(ITR,ID)=POPUL(JJ,ID)*ANE*SBF(II)*...` | POPUL 极大值 × 其他 → Inf |
**第二步:辐射转移 RTEFR1`tlusty208.f:32265`)源函数计算**
| 源码行 | 语句 | 发散时 |
|--------|------|--------|
| `tlusty208.f:32352` | `ST0(ID)=EMIS1(ID)/AB0(ID)` | **两个无穷大相除 = NaN**(典型源头) |
| `tlusty208.f:32361` | `SS0(ID)=-SCAT1(ID)/AB0(ID)` | 同上 |
NaN 一旦进入 `RAD1`(辐射转移矩阵解),就通过 `AH`/`AHout` 传播到 `FLUX(IJ)`,然后波及整列所有频率点。
### 2.3 为什么发散后不会提前 STOP
TLUSTY 有一个发散护栏(`tlusty208.f:47587-47591`):
```fortran
47587: IF(ITER.NE.1 .AND. ABS(CHMX).GT.1.D16) THEN
47588: WRITE(6,610) ITER,CHMX
47590: STOP
47591: END IF
```
**CHMX 是温度相对变化的最大值**`tlusty208.f:47468` `CHT=CHANGT(ID)/TEMP(ID)`),当 TEMP 含 NaN 时 CHMX 也变 NaN。而 **NaN 与任何数比较都返回 `.FALSE.`**
- `ABS(NaN).GT.1.D16` 为假 → **STOP 不触发**(护栏对 NaN 失效)。
- `ABS(NaN).LE.CHMAX` 为假 → `LFIN` 只能靠 `ITER.GE.NITER`(耗尽上限)成立。
因此程序**一路跑到 NITER,然后带着含 NaN 的 TEMP/POPUL 走完最后一次频率循环,把整列 FLUX 全写成 NaN,最后 OUTPRI 把全 NaN 的 FLUX 倒进 fort.14**。
---
## 3. 两种 NaN 场景的区分
fort.14 的 NaN 理论上有两种可能来源:
| 场景 | 描述 | 实际是否存在 |
|------|------|-------------|
| **A. 大气模型发散** | TEMP/POPUL 含 NaN → FLUX 全 NaN | ✅ 存在(62 例) |
| **B. 大气收敛良好,局部辐射转移溢出** | 某些波长点不透明度极值 → 局部 NaN | ❌ 数据中不存在 |
**源码判定**:场景 B 在机制上极不可能。FLUX 全 NaN 要求所有频率点的源函数都变为 NaN,这只能由大气结构整体发散引起。局部 NaN(少数频率点)需要大气结构有效但个别频率点溢出——但 TLUSTY 的频率循环对每个频率点用同一套 TEMP/POPUL,大气有效时不会让某个频率点单独产生 NaN(除非该频率点恰好触发了吸收系数的精确除零,但这在实测数据中从未出现)。
**数据判定**:在 162 个有 `.emflux` 的网格点中,**所有 62 个 emflux-NaN 点都是 100% 全 NaN**(如 t55000_g5.04514/4514 行全 NaNt60000_g6.54420/4420 行全 NaN)。**没有任何一个点是局部/特定波长 NaN**。这直接排除了场景 B。
---
## 4. 真实数据验证
### 4.1 全量统计(162 个网格点)
| 情形 | 数量 | conv.json converged |
|------|------|---------------------|
| `.emflux` 全 NaN + `.7` 含 NaN | **53** | 全部 false |
| `.emflux` 全 NaN + `.7` 检测无 NaN | **9** | 全部 false |
| `.emflux` 正常 + `.7` 含 NaN | **0** | — |
| 两者都正常(0 NaN | **100** | 混合 |
**决定性事实**
1. **62/62100%)的 emflux-NaN 点 `converged=false`**
2. `converged=true` 且 emflux 含 NaN 的点 = **0**
3. 全 NaN 点都是 100% 整文件 NaN,无局部 NaN。
### 4.2 典型全 NaN 点
`t55000_g5.0_he-2_c-1_n-4_o-4`
- `.7` 从第 11 行(depth 1 的 TEMP/ELEC/DENS/POPUL)开始全是 NaN5200/5210 行 NaN)。
- `conv.json``converged:false``atmosphere_has_nan:true``best_max_relc:39100`(seed_nc 第 3 次迭代发散)。
- `.nl.err``IEEE_INVALID_FLAG IEEE_UNDERFLOW_FLAG IEEE_DENORMAL` 浮点异常。
### 4.3 检测器盲区:9 个"`.7` 检测无 NaN 但 emflux 全 NaN"的点
这 9 个点最关键——它们决定了 emflux 校验是否有独立价值:
- `.7` 不含字面 NaN/Inf`atmosphere_has_nan` 检测器通过),但实际已发散成**巨大但有限的非物理值**:
- TEMP ≈ 1.015e-15(趋近 0
- POPUL 达 5e12(极大)
- `final_max_relc` 在 1e16 ~ 1e27(灾难性发散)
- 这些值不是字面 NaN,但在 OPACF1 里 `HK/TEMP`→巨大、POPUL 乘积→Inf、`EMIS1/AB0`→Inf/Inf=NaN。
- `synspec_error``"spec 含 NaN/Inf/溢出行 (共 303554 行)"` —— 发散大气喂给 SYNSPEC 后整谱也是 NaN/溢出。
- **原因**`atmosphere_has_nan``conv_check.rs:18`)的正则 `(?i)(\bnan\b|\binf(?:inity)?\b|\*{3,}|[eE]\+(?:3\d{2}|[4-9]\d{2,}))` 只匹配 `nan/inf/***/E+300+`,**抓不到"巨大但有限的发散值"**(如 5e12 POPUL、1e-15 TEMP)。
**这 9 个点靠 `converged=false`max_relc 极大)+ emflux 全 NaN + synspec NaN 三重否决才被正确标记。emflux 全 NaN 校验在这类点上恰好补位了 `atmosphere_has_nan` 的盲区。**
---
## 5. 对校验设计的影响
### 5.1 emflux bolometric 校验的语义修正
现有 `check_emflux_bolometric``conv_check.rs`)把"全 NaN(占比>50%"判失败。根据本调研:
- **判定正确**:全 NaN 是大气发散的充分(且几乎必要的)标志。
- **语义需精确化**:错误描述从 ~~"辐射转移可能失败"~~ 改为 **"大气收敛失败(辐射转移求解发散)"**。
```rust
// 修正后(conv_check.rs
error: Some(format!(
"emflux NaN/不可解析占比 {:.0}% 过高(>{:.0}%),大气收敛失败(辐射转移求解发散)",
nan_ratio * 100.0, 50.0
)),
```
### 5.2 与 `atmosphere_has_nan` 的关系
emflux 全 NaN 校验与 `atmosphere_has_nan` **同级、互为冗余**
| 校验 | 覆盖的发散形态 | 盲区 |
|------|---------------|------|
| `atmosphere_has_nan` | `.7` 含字面 NaN/Inf/`***`/E+300+ | 巨大但有限的发散值(如 5e12 POPUL、1e-15 TEMP |
| `check_emflux_bolometric`(NaN 分支) | 大气发散导致的辐射转移全 NaN | 需 TLUSTY 启用且 fort.14 存在 |
| `check_fort9`max_relc | max_relc 极大(收敛失败) | 无法区分真收敛/假收敛 |
三者交叉覆盖,任一独立即可拦截大部分发散点。emflux 全 NaN 校验的独特价值在于捕获那 9 个 `atmosphere_has_nan` 盲区点。
### 5.3 不影响的部分
- **局部 NaN(如 `.spec` 中 Balmer 线区域的 NaN**:这是 SYNSPEC 处理 Balmer 线 Stark 加宽时的正常行为(与 `tests/synspec/hhe/results/hhe35nc.spec` 一致),不是收敛失败。emflux 校验不涉及 `.spec`,且阈值设在 50%(容忍局部 NaN,只拦全 NaN)。
---
## 6. 结论
1. **`.emflux` 全 NaN 的物理含义**TLUSTY 大气模型结构(TEMP/POPUL/ELEC)已数值发散到不可用程度,导致最后一次频率循环里 OPACF1→RTEFR1 把整列 FLUX 全部污染为 NaNOUTPRI 在耗尽 NITER 上限后写入 fort.14。
2. **是否等于 TLUSTY 收敛失败?—— 是**。源码机制(CHMX 变 NaN 后 STOP 护栏与收敛判定双双失效,全 NaN 只能在大气发散时产生)+ 数据(62/62 全 NaN 点 converged=falseconverged=true 点 0 个有 NaN)双重佐证。**全 NaN emflux 是大气收敛失败的充分(且几乎必要的)标志。**
3. **"`.7` 大气无 NaN(按检测器)且 converged=true,但 `.emflux` 全 NaN"是否可能?—— 不可能**。162 点中此组合为 0 例。出现"`.7` 检测无 NaN + emflux 全 NaN"的 9 例,都是 `.7` 实际已发散成巨大有限值/近零温度(检测器盲区),converged 全为 false。
4. **emflux 全 NaN 校验的独立价值**:能捕获 `atmosphere_has_nan` 正则盲区(巨大有限发散值),起到交叉验证补位作用。
---
## 附录:关键源码行号
均在 `/home/fmq/program/tlusty/tl208-s54/tlusty/tlusty208.f`
| 功能 | 行号 |
|------|------|
| fort.14 写出(OUTPRI | 14188 |
| FORMAT 614 | 14406 |
| FLUX 来源 COMMON | `MODELQ.FOR:135` |
| FLUX 赋值(辐射转移解) | 31800, 32225, 32848 |
| NaN 产生(不透明度 HK/TEMP | 4857, 45375 |
| NaN 产生(POPUL 布居) | 31444, 31446, 33455 |
| NaN 传播(源函数 EMIS1/AB0 | 32352, 32361 |
| 发散 STOP 护栏(对 NaN 失效) | 47587-47591 |
| LFIN 收敛判定 | 47596 |
| OUTPRI 调用时机(LFIN | 3887-3889 |