物理正确性校验体系(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 报错
12 KiB
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:
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):
3887: IF(LFIN) THEN
3888: CALL OUTPRI
3889: END IF
LFIN 的判定(tlusty208.f:47596):
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):
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):
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 | 混合 |
决定性事实:
- 62/62(100%)的 emflux-NaN 点
converged=false。 converged=true且 emflux 含 NaN 的点 = 0。- 全 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 是大气发散的充分(且几乎必要的)标志。
- 语义需精确化:错误描述从
"辐射转移可能失败"改为 "大气收敛失败(辐射转移求解发散)"。
// 修正后(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. 结论
-
.emflux全 NaN 的物理含义:TLUSTY 大气模型结构(TEMP/POPUL/ELEC)已数值发散到不可用程度,导致最后一次频率循环里 OPACF1→RTEFR1 把整列 FLUX 全部污染为 NaN,OUTPRI 在耗尽 NITER 上限后写入 fort.14。 -
是否等于 TLUSTY 收敛失败?—— 是。源码机制(CHMX 变 NaN 后 STOP 护栏与收敛判定双双失效,全 NaN 只能在大气发散时产生)+ 数据(62/62 全 NaN 点 converged=false,converged=true 点 0 个有 NaN)双重佐证。全 NaN emflux 是大气收敛失败的充分(且几乎必要的)标志。
-
"
.7大气无 NaN(按检测器)且 converged=true,但.emflux全 NaN"是否可能?—— 不可能。162 点中此组合为 0 例。出现".7检测无 NaN + emflux 全 NaN"的 9 例,都是.7实际已发散成巨大有限值/近零温度(检测器盲区),converged 全为 false。 -
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 |