物理修复(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 篇根因分析/验证文档
133 lines
4.9 KiB
Python
Executable File
133 lines
4.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""
|
||
diff_configs.py — A/B 测试配置对照工具。
|
||
|
||
给定 2 个或多个 content 目录(或 inputs 目录),解析各自的 nst(L1/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.nst(A/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())
|