Files
DCTS/.agents/skills/tlusty-synspec-test/scripts/check_fort9.py
T
fmq b058e66722 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 篇根因分析/验证文档
2026-08-17 23:55:26 +08:00

246 lines
9.4 KiB
Python
Executable File
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.
#!/usr/bin/env python3
"""
check_fort9.py — 解析 {stage}.9 收敛轨迹,判数值收敛;检测 STOP / NaN / 雪崩。
复刻 DCTS check_fort9conv_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.6stdout)里的 "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_REiter 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())