物理正确性校验体系(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 报错
239 lines
12 KiB
Markdown
239 lines
12 KiB
Markdown
# 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/62(100%)的 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.0:4514/4514 行全 NaN;t60000_g6.5:4420/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/62(100%)的 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)开始全是 NaN(5200/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 全部污染为 NaN,OUTPRI 在耗尽 NITER 上限后写入 fort.14。
|
||
|
||
2. **是否等于 TLUSTY 收敛失败?—— 是**。源码机制(CHMX 变 NaN 后 STOP 护栏与收敛判定双双失效,全 NaN 只能在大气发散时产生)+ 数据(62/62 全 NaN 点 converged=false,converged=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 |
|