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 篇根因分析/验证文档
This commit is contained in:
fmq
2026-08-17 23:55:26 +08:00
parent 43b82b1ae2
commit b058e66722
54 changed files with 5624 additions and 347 deletions
+161
View File
@@ -0,0 +1,161 @@
#!/usr/bin/env python3
"""
check_inputs.py — TLUSTY 测试执行前输入校验(冷启动链 / 种子链)
复刻 docs/testing_workflow_2026_08_11.md §五 与 references/tlusty-stages.md 的"关键不变量"
退出码:0=全过,1=有违规。
用法:
python3 check_inputs.py <inputs_dir> --chain cold --teff 60000
python3 check_inputs.py <inputs_dir> --chain seed --teff 55000
"""
import argparse
import os
import re
import sys
# NaN 模式(与 conv_check.rs:18 NAN_RE_PATTERN 一致)
NAN_RE = re.compile(r"(?i)(\bnan\b|\binf(?:inity)?\b|\*{3,}|[eE]\+(?:3\d{2}|[4-9]\d{2,}))")
def read_lines(path):
with open(path, "r", errors="replace") as f:
return f.read().splitlines()
def check_dot5_l2(path, expect_lte, expect_ltgray):
"""校验 .5 第2行 LTE/LTGRAY 两个标志。行首允许空格。"""
lines = read_lines(path)
if len(lines) < 2:
return f"{path}: 文件不足2行"
l2 = lines[1].split("!")[0].split()
if len(l2) < 2:
return f"{path}: L2 字段不足2个: {lines[1]!r}"
lte, ltgray = l2[0].upper(), l2[1].upper()
if lte != expect_lte or ltgray != expect_ltgray:
return f"{path}: L2 期望 {expect_lte} {expect_ltgray}, 实际 {lte} {ltgray} ({lines[1].strip()!r})"
return None
def check_dot5_nst_name(path):
"""校验 .5 第3行 'nst' 文件名。
约定:生产/测试默认 nst 名都是字面 'nst'run.sh 会在执行时把 {stage}.nst 复制成 nst。
所以 'nst' 不算违规(运行时会有);只有引用了非标准名且该名在 inputs/ 找不到时才报错。"""
lines = read_lines(path)
if len(lines) < 3:
return None
l3 = lines[2]
m = re.search(r"'([^']+)'", l3)
if not m:
return None
nst_name = m.group(1)
if nst_name == "nst":
return None # 标准名,run.sh 运行时把 {stage}.nst → nst,无需 inputs/ 里有
base = os.path.dirname(path)
candidates = [os.path.join(base, nst_name), os.path.join(base, "..", nst_name)]
if not any(os.path.exists(c) for c in candidates):
return f"{path}: L3 指定 nst 文件 {nst_name!r} 未找到(非标准名,run.sh 不会自动改名)"
return None
def check_nst_niter(path):
"""校验 nst 第1行含 NITER=。"""
lines = read_lines(path)
if not lines:
return f"{path}: 空文件"
if "NITER" not in lines[0].upper():
return f"{path}: L1 缺 NITER=: {lines[0][:80]!r}"
return None
def check_file_exists(path, what):
return None if os.path.exists(path) else f"缺输入: {what} ({path})"
def check_no_nan(path, what):
if not os.path.exists(path):
return f"缺输入: {what} ({path})"
n = 0
with open(path, "r", errors="replace") as f:
for line in f:
if NAN_RE.search(line):
n += 1
return None if n == 0 else f"{what}{n} 行 NaN/Inf/坏值 ({path})"
def main():
ap = argparse.ArgumentParser(description="TLUSTY 测试执行前输入校验")
ap.add_argument("inputs_dir", help="inputs/ 目录路径")
ap.add_argument("--chain", choices=["cold", "seed"], required=True)
ap.add_argument("--teff", type=float, default=None, help="TeffK),用于上下文,非必填")
args = ap.parse_args()
d = args.inputs_dir
violations = []
def add(v):
if v:
violations.append(v)
if args.chain == "cold":
# 必须有 lte/nc/nl 的 .5 与 .nst
for f in ["lte.5", "nc.5", "nl.5", "lte.nst", "nc.nst", "nl.nst"]:
add(check_file_exists(os.path.join(d, f), f))
# L2: lte=T T, nc/nl=F F
if os.path.exists(os.path.join(d, "lte.5")):
add(check_dot5_l2(os.path.join(d, "lte.5"), "T", "T"))
for s in ("nc", "nl"):
p = os.path.join(d, f"{s}.5")
if os.path.exists(p):
add(check_dot5_l2(p, "F", "F"))
# nst 名一致
for s in ("lte", "nc", "nl"):
p = os.path.join(d, f"{s}.5")
if os.path.exists(p):
add(check_dot5_nst_name(p))
# nst L1 含 NITER
for s in ("lte", "nc", "nl"):
p = os.path.join(d, f"{s}.nst")
if os.path.exists(p):
add(check_nst_niter(p))
# 冷启动链不应有 fort.8
if os.path.exists(os.path.join(d, "fort.8")):
violations.append("冷启动链不应有 inputs/fort.8lte 阶段 ltgray=T 会删它;若有种子请用 --chain seed")
else: # seed
for f in ["seed_nc.5", "seed_nc.nst", "fort.8"]:
add(check_file_exists(os.path.join(d, f), f))
add(check_no_nan(os.path.join(d, "fort.8"), "fort.8 种子"))
if os.path.exists(os.path.join(d, "seed_nc.5")):
add(check_dot5_l2(os.path.join(d, "seed_nc.5"), "F", "F"))
add(check_dot5_nst_name(os.path.join(d, "seed_nc.5")))
if os.path.exists(os.path.join(d, "seed_nc.nst")):
add(check_nst_niter(os.path.join(d, "seed_nc.nst")))
# data 软链:run.sh / new_test.sh 会在执行时创建,inputs/ 阶段可能还没有。
# 这里只做提示(note),不算硬违规。
notes = []
data_link = None
for cand in [os.path.join(d, "..", "run", "data"), os.path.join(d, "data")]:
if os.path.islink(cand) or os.path.isdir(cand):
data_link = cand
break
if data_link is None:
notes.append("data 软链(run/data)暂不存在——run.sh 执行时会自动创建并指向 assets/data/,可忽略")
# 汇报
print(f"=== 输入校验: {d} (chain={args.chain}" +
(f", teff={args.teff}" if args.teff else "") + ") ===")
for n in notes:
print(f" {n}")
if not violations:
print("✅ 全部通过")
return 0
for v in violations:
print(f"{v}")
print(f"\n{len(violations)} 项违规。请修正后再执行(见 references/tlusty-stages.md)。")
return 1
if __name__ == "__main__":
sys.exit(main())