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
+132
View File
@@ -0,0 +1,132 @@
#!/usr/bin/env python3
"""
diff_configs.py — A/B 测试配置对照工具。
给定 2 个或多个 content 目录(或 inputs 目录),解析各自的 nstL1/L2 的 KEY=VAL
与 .5 关键字段(L1 TEFF/GRAV、L2 LTE/LTGRAY、L3 nst 名),输出并排对照表,高亮差异。
用于 A/B 测试时快速确认"只有想改的参数变了,其余一致"
用法:
python3 diff_configs.py dirA/inputs dirB/inputs dirC/inputs
python3 diff_configs.py dirA dirB --teff 50000 # 传 content 目录也行(自动找 inputs/
"""
import argparse
import os
import re
import sys
# nst 一行里逗号分隔的 KEY=VAL(L1 与 L2 都是这种格式)
KV_RE = re.compile(r"(\w+)=([^,]+)")
def parse_nst(path):
"""返回 nst 的 {KEY: VAL} 字典(合并 L1+L2+escape-hatch 行)。"""
kv = {}
if not os.path.exists(path):
return kv
for line in open(path, "r", errors="replace"):
for m in KV_RE.finditer(line):
kv[m.group(1).upper()] = m.group(2).strip()
return kv
def parse_dot5(path):
"""返回 .5 关键字段。"""
info = {}
if not os.path.exists(path):
return info
lines = open(path, "r", errors="replace").read().splitlines()
if len(lines) >= 1:
t = lines[0].split("!")[0].split()
info["TEFF"] = t[0] if len(t) > 0 else "?"
info["GRAV"] = t[1] if len(t) > 1 else "?"
if len(lines) >= 2:
t = lines[1].split("!")[0].split()
info["LTE/LTGRAY"] = f"{t[0]}/{t[1]}" if len(t) >= 2 else "?"
if len(lines) >= 3:
m = re.search(r"'([^']+)'", lines[2])
info["nst名"] = m.group(1) if m else "?"
return info
def resolve_inputs(d):
if os.path.basename(d) == "inputs" and os.path.isdir(d):
return d
cand = os.path.join(d, "inputs")
return cand if os.path.isdir(cand) else d
def main():
ap = argparse.ArgumentParser(description="A/B 配置对照(nst 关键字段 + .5 关键行)")
ap.add_argument("dirs", nargs="+", help="inputs/ 或 content 目录")
ap.add_argument("--teff", default=None, help="仅用于显示标题")
args = ap.parse_args()
dirs = [resolve_inputs(d) for d in args.dirs]
names = [os.path.basename(os.path.dirname(d)) if os.path.basename(d) == "inputs"
else os.path.basename(d) for d in dirs]
# 收集所有出现过的 key(nst + .5 字段),分两组
nst_keys_order = ["ND", "NLAMBD", "VTB", "ISPODF", "DDNU", "CNU1", "CHMAX", "ITEK", "NITER",
"ORELAX", "IDLTE", "IACC", "ICHANG", "IELCOR", "DPSILG"]
dot5_keys = ["TEFF", "GRAV", "LTE/LTGRAY", "nst名"]
nst_maps = []
dot5_maps = []
for d in dirs:
# nst: 各阶段都有,优先 nc.nstA/B 通常比 nc/nl);也读 lte/nl
nst_path = None
for stage in ("nc", "nl", "lte", "seed_nc"):
p = os.path.join(d, f"{stage}.nst")
if os.path.exists(p):
nst_path = p
break
nst_maps.append((parse_nst(nst_path) if nst_path else {}, nst_path))
# .5:优先 nc.5
dot5_path = None
for stage in ("nc", "nl", "lte", "seed_nc"):
p = os.path.join(d, f"{stage}.5")
if os.path.exists(p):
dot5_path = p
break
dot5_maps.append((parse_dot5(dot5_path) if dot5_path else {}, dot5_path))
# 收集所有 nst key(含非标准的)
all_nst_keys = list(nst_keys_order)
seen = set(k.upper() for k in all_nst_keys)
for nm, _ in nst_maps:
for k in nm:
if k not in seen:
all_nst_keys.append(k)
seen.add(k)
def print_table(title, keys, maps, getter):
print(f"\n=== {title} ===")
hdr = " 字段".ljust(16) + "".join(n.ljust(22) for n in names)
print(hdr)
print(" " + "-" * (len(hdr) - 2))
for k in keys:
vals = [getter(nm, k) for nm, _ in maps]
# 高亮:若不全相同,标记 *
differ = len(set(repr(v) for v in vals)) > 1
mark = " * " if differ else " "
row = mark + k.ljust(13) + "".join((v if v else "").ljust(22) for v in vals)
print(row)
print(f"\n配置对照({len(names)} 组)" + (f"Teff={args.teff}" if args.teff else ""))
print(" * = 该行各组不一致(A/B 测试应只有你想改的字段带 *)")
for d, n, (nm, np_), (dm, dp_) in zip(dirs, names, nst_maps, dot5_maps):
print(f" [{n}] nst={os.path.basename(np_) or '(无)'} .5={os.path.basename(dp_) or '(无)'}")
print_table(".5 关键字段", dot5_keys, dot5_maps, lambda m, k: m.get(k, ""))
print_table("nst 关键参数(nc/nl 阶段)", all_nst_keys, nst_maps, lambda m, k: m.get(k.upper(), ""))
# 源文件路径(便于核对)
print("\n源文件:")
for d, n, (_, np_), (_, dp_) in zip(dirs, names, nst_maps, dot5_maps):
print(f" [{n}] {np_}")
return 0
if __name__ == "__main__":
sys.exit(main())