物理修复(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 篇根因分析/验证文档
246 lines
9.4 KiB
Python
Executable File
246 lines
9.4 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""
|
||
check_fort9.py — 解析 {stage}.9 收敛轨迹,判数值收敛;检测 STOP / NaN / 雪崩。
|
||
|
||
复刻 DCTS check_fort9(conv_check.rs:68)的核心逻辑。
|
||
退出码:0=收敛 /1=未收敛或异常 /2=文件缺失或无有效数据。
|
||
|
||
⚠️ **只对已完成的运行使用本脚本**:TLUSTY 是边算边写 fort.9 的(每次迭代的
|
||
深度行逐行写出),运行中实时读取,最新一拍的 max_relc 读到的是半写状态,数值
|
||
不可信(实测:运行中读到 iter6=0.477、iter9=0.00176 貌似收敛,进程退出后同一
|
||
迭代实为 1060 / 5.07e4)。等 run.sh 完全结束、进程退出后再跑本脚本。
|
||
|
||
判据:
|
||
- 每次 ITER 的 max_relc = 该迭代所有深度行 |MAXIMUM| 的最大值
|
||
- 末次迭代 max_relc < chmax(默认 0.001)且有限 = 收敛
|
||
- 单调下降=健康;骤涨几个数量级=雪崩
|
||
- fort.6(stdout)里的 "STOP in SOLVE after ITER N" = 求解器发散中止
|
||
|
||
用法:
|
||
python3 check_fort9.py outputs/nl.9 --teff 60000
|
||
python3 check_fort9.py outputs/nc.9 --chmax 0.001 --stdout outputs/nc.6
|
||
"""
|
||
import argparse
|
||
import os
|
||
import re
|
||
import sys
|
||
|
||
# 复刻 conv_check.rs:44 parse_fortran_float
|
||
_NO_E_EXP_RE = re.compile(r"^([+-]?[\d.]+)([+-]\d+)$")
|
||
|
||
|
||
def parse_fortran_float(s):
|
||
s = s.strip()
|
||
try:
|
||
return float(s)
|
||
except ValueError:
|
||
pass
|
||
m = _NO_E_EXP_RE.match(s)
|
||
if m:
|
||
try:
|
||
return float(f"{m.group(1)}E{m.group(2)}")
|
||
except ValueError:
|
||
pass
|
||
return None
|
||
|
||
|
||
# 复刻 conv_check.rs:107 FORT9_RE:iter depth 5浮点 2整数
|
||
FORT9_RE = re.compile(
|
||
r"^\s*(\d+)\s+(\d+)\s+([-+\dE.]+)\s+([-+\dE.]+)\s+([-+\dE.]+)\s+([-+\dE.]+)\s+([-+\dE.]+)\s+(\d+)\s+(\d+)\s*$"
|
||
)
|
||
# NaN 模式(conv_check.rs:18)
|
||
NAN_RE = re.compile(r"(?i)(\bnan\b|\binf(?:inity)?\b|\*{3,}|[eE]\+(?:3\d{2}|[4-9]\d{2,}))")
|
||
# STOP 模式(conv_check.rs SOLVER_STOP_RE)
|
||
STOP_RE = re.compile(r"STOP\s+in\s+SOLVE\s+after\s+ITER\s+(\d+)", re.IGNORECASE)
|
||
|
||
|
||
def parse_fort9(path, chmax):
|
||
"""返回 (iters, last_iter, last_max_relc, converged, n_depths_last)。
|
||
iters = [(iter_no, max_relc, n_depths), ...]"""
|
||
iters = []
|
||
cur_iter = None
|
||
cur_max = 0.0
|
||
cur_n = 0
|
||
last_iter = None
|
||
|
||
with open(path, "r", errors="replace") as f:
|
||
for line in f:
|
||
m = FORT9_RE.match(line)
|
||
if not m:
|
||
continue
|
||
try:
|
||
it = int(m.group(1))
|
||
except ValueError:
|
||
continue
|
||
maximum = parse_fortran_float(m.group(7))
|
||
if maximum is None:
|
||
continue
|
||
if cur_iter != it:
|
||
if cur_iter is not None:
|
||
iters.append((cur_iter, cur_max, cur_n))
|
||
cur_iter, cur_max, cur_n = it, 0.0, 0
|
||
cur_n += 1
|
||
am = abs(maximum)
|
||
if am > cur_max:
|
||
cur_max = am
|
||
last_iter = it
|
||
if cur_iter is not None:
|
||
iters.append((cur_iter, cur_max, cur_n))
|
||
|
||
if not iters or last_iter is None:
|
||
return [], None, float("inf"), False, 0
|
||
|
||
last_max_relc = iters[-1][1]
|
||
converged = (last_max_relc < chmax) and (last_max_relc == last_max_relc) and (abs(last_max_relc) != float("inf"))
|
||
return iters, last_iter, last_max_relc, converged, iters[-1][2]
|
||
|
||
|
||
def detect_avalanche(iters):
|
||
"""检测雪崩:相邻迭代 max_relc 涨 >1e3 倍。返回告警列表。"""
|
||
warns = []
|
||
for i in range(1, len(iters)):
|
||
prev = iters[i - 1][1]
|
||
cur = iters[i][1]
|
||
if prev > 0 and cur / prev > 1e3:
|
||
warns.append(f" iter {iters[i-1][0]}→{iters[i][0]}: max_relc {prev:.3g} → {cur:.3g}(涨 {cur/prev:.0g}×,雪崩)")
|
||
return warns
|
||
|
||
|
||
def scan_stop(stdout_path):
|
||
"""扫 fort.6 找 STOP in SOLVE。返回 iter 号或 None。"""
|
||
if not stdout_path or not os.path.exists(stdout_path):
|
||
return None
|
||
last = None
|
||
with open(stdout_path, "r", errors="replace") as f:
|
||
for line in f:
|
||
m = STOP_RE.search(line)
|
||
if m:
|
||
last = int(m.group(1))
|
||
return last
|
||
|
||
|
||
def scan_nan(path):
|
||
if not path or not os.path.exists(path):
|
||
return 0
|
||
n = 0
|
||
with open(path, "r", errors="replace") as f:
|
||
for line in f:
|
||
if NAN_RE.search(line):
|
||
n += 1
|
||
return n
|
||
|
||
|
||
def main():
|
||
ap = argparse.ArgumentParser(description="fort.9 收敛轨迹分析")
|
||
ap.add_argument("fort9", help="{stage}.9 路径")
|
||
ap.add_argument("--teff", type=float, default=None, help="Teff(上下文,非必填)")
|
||
ap.add_argument("--chmax", type=float, default=0.001, help="收敛阈值(默认 0.001)")
|
||
ap.add_argument("--stdout", default=None, help="对应 {stage}.6 路径,用于 STOP 检测(默认自动推断)")
|
||
ap.add_argument("--expected-niter", type=int, default=None,
|
||
help="配置的 NITER(来自 nst)。若末次 iter 远小于此且未收敛,提示疑似被 timeout 截断")
|
||
args = ap.parse_args()
|
||
|
||
if not os.path.exists(args.fort9):
|
||
print(f"⚠ 文件不存在: {args.fort9}")
|
||
return 2
|
||
|
||
# 自动推断 stdout(同目录同名 .6):把末尾 .9 换成 .6
|
||
stdout = args.stdout
|
||
if stdout is None:
|
||
if args.fort9.endswith(".9"):
|
||
cand = args.fort9[:-2] + ".6"
|
||
else:
|
||
cand = args.fort9 + "6"
|
||
stdout = cand if os.path.exists(cand) else None
|
||
|
||
if args.chmax <= 0:
|
||
print(f"❌ 非法 chmax={args.chmax}(须为正有限数)")
|
||
return 1
|
||
|
||
iters, last_iter, last_max_relc, converged, n_depths = parse_fort9(args.fort9, args.chmax)
|
||
if not iters:
|
||
print(f"⚠ {args.fort9} 无有效迭代数据")
|
||
return 2
|
||
|
||
stop_iter = scan_stop(stdout)
|
||
nan9 = scan_nan(args.fort9)
|
||
# 截断检测:① 附近有 .TIMEOUT 标记文件(run.sh 在 rc=124 时写到 outputs/{stage}.TIMEOUT);
|
||
# ② 末次 iter 远小于 --expected-niter 且未收敛(疑似 timeout)。
|
||
import glob
|
||
timeout_marker = None
|
||
d = os.path.dirname(os.path.abspath(args.fort9))
|
||
for pat in (os.path.join(d, "*.TIMEOUT"),
|
||
os.path.join(d, "outputs", "*.TIMEOUT"),
|
||
os.path.join(d, "..", "outputs", "*.TIMEOUT")):
|
||
hits = glob.glob(pat)
|
||
if hits:
|
||
timeout_marker = ",".join(os.path.basename(h) for h in hits)
|
||
break
|
||
likely_truncated = (timeout_marker is not None or
|
||
(args.expected_niter and last_iter < args.expected_niter - 1 and not converged and stop_iter is None))
|
||
|
||
print(f"=== 收敛轨迹: {args.fort9} (chmax={args.chmax}" +
|
||
(f", teff={args.teff}" if args.teff else "") + ") ===")
|
||
print(f" 迭代次数: {len(iters)}(末次 ITER {last_iter})")
|
||
print(f" 末 max_relc: {last_max_relc:.4g} → {'✅ < chmax 收敛' if converged else '❌ ≥ chmax 未收敛'}")
|
||
print(f" 末次深度数: {n_depths}")
|
||
if stop_iter is not None:
|
||
print(f" STOP: ❌ STOP in SOLVE after ITER {stop_iter}(求解器发散中止)")
|
||
else:
|
||
print(f" STOP: 无")
|
||
print(f" fort.9 NaN行: {nan9}")
|
||
if likely_truncated:
|
||
detail = f"(标记文件: {timeout_marker})" if timeout_marker else (
|
||
f"(仅 iter {last_iter}/{args.expected_niter},疑似 timeout 截断)" if args.expected_niter else "")
|
||
print(f" 截断: ⚠ 疑似被 timeout 杀死 {detail}——轨迹不完整,结论需谨慎")
|
||
|
||
# 轨迹(最多显示首/末若干 + 全部若 ≤15)
|
||
print(" 轨迹 (iter: max_relc):")
|
||
show = iters if len(iters) <= 20 else iters[:8] + [None] + iters[-5:]
|
||
for it in show:
|
||
if it is None:
|
||
print(" ...")
|
||
else:
|
||
print(f" iter {it[0]:>3}: {it[1]:.4g} ({it[2]} 深度)")
|
||
|
||
warns = detect_avalanche(iters)
|
||
if warns:
|
||
print(" ⚠ 雪崩告警:")
|
||
for w in warns:
|
||
print(w)
|
||
|
||
# 伪收敛启发式:max_relc 恰为 0 或极小且迭代很少 → 极可能是"在 NaN 模型上迭代"。
|
||
# 真实收敛要多次迭代、max_relc 趋近但极少精确到 0。
|
||
pseudo_warn = False
|
||
if last_max_relc == 0.0 or (len(iters) <= 2 and last_max_relc < 1e-10):
|
||
pseudo_warn = True
|
||
|
||
# 结论
|
||
real_converged = converged and stop_iter is None and nan9 == 0
|
||
print()
|
||
if real_converged:
|
||
print(f"✅ 数值收敛(末 max_relc {last_max_relc:.4g} < {args.chmax},无 STOP,无 NaN)")
|
||
if pseudo_warn:
|
||
print(" ⚠⚠ 伪收敛嫌疑:max_relc≈0 且迭代极少,极可能是『在 NaN 模型上迭代』(NaN 上相对变化=0)。")
|
||
print(" 务必运行 check_temperature.py 查温度结构——若 .7 全 NaN,则此『收敛』无效。")
|
||
else:
|
||
print(" 注意:数值收敛 ≠ 物理收敛。请再用 check_temperature.py 查温度结构(防伪收敛)。")
|
||
return 0
|
||
print("❌ 未数值收敛:" if not real_converged else "")
|
||
reasons = []
|
||
if not converged:
|
||
reasons.append(f"末 max_relc {last_max_relc:.4g} ≥ chmax {args.chmax}")
|
||
if stop_iter is not None:
|
||
reasons.append(f"STOP in SOLVE after ITER {stop_iter}")
|
||
if nan9 > 0:
|
||
reasons.append(f"fort.9 有 {nan9} 行 NaN/坏值")
|
||
if warns:
|
||
reasons.append("迭代轨迹有雪崩")
|
||
for r in reasons:
|
||
print(f" - {r}")
|
||
return 1
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|