Files
DCTS/.agents/skills/tlusty-synspec-test/scripts/check_inputs.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

162 lines
5.9 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_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())