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

133 lines
4.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
"""
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())