物理修复(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 篇根因分析/验证文档
153 lines
5.3 KiB
Python
Executable File
153 lines
5.3 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""
|
||
check_temperature.py — 对 fort.7/{stage}.7 做温度结构物理判定。
|
||
|
||
精确复刻 DCTS 权威判据 check_temperature_structure(conv_check.rs:463)。
|
||
退出码:0=物理(三项全过)/1=不物理 /2=文件缺失或无法解析(跳过,不判失败)。
|
||
|
||
判据(valid = 三项全过):
|
||
1. 表层 T < max_factor × Teff (默认 max_factor=3.0)
|
||
2. 每个深度 T ∈ [temp_floor, temp_ceiling] (默认 [10, 1e8] K)
|
||
3. 无 NaN/Inf
|
||
|
||
用法:
|
||
python3 check_temperature.py outputs/nl.7 --teff 60000
|
||
python3 check_temperature.py outputs/nc.7 --teff 55000 --max-factor 3.0
|
||
"""
|
||
import argparse
|
||
import math
|
||
import re
|
||
import sys
|
||
|
||
# 复刻 conv_check.rs:44 parse_fortran_float
|
||
# Fortran 指数≥100 时挤掉 E,如 -1.35E+118 → -1.35+118
|
||
_NO_E_EXP_RE = re.compile(r"^([+-]?[\d.]+)([+-]\d+)$")
|
||
|
||
|
||
def parse_fortran_float(s):
|
||
"""返回 float(可能为 inf/-inf);无法解析返回 None。"""
|
||
s = s.strip()
|
||
try:
|
||
return float(s)
|
||
except ValueError:
|
||
pass
|
||
m = _NO_E_EXP_RE.match(s)
|
||
if m:
|
||
try:
|
||
return float(f"{m.group(1)}E{m.group(2)}") # 含 inf/-inf(溢出)
|
||
except ValueError:
|
||
pass
|
||
return None
|
||
|
||
|
||
def check_temperature_structure(path, teff, max_factor=3.0, temp_floor=10.0, temp_ceiling=1e8):
|
||
"""复刻 conv_check.rs:463。返回 (temps, violations) 或 (None, None)=无法解析。"""
|
||
try:
|
||
with open(path, "r", errors="replace") as f:
|
||
lines = f.readlines()
|
||
except OSError:
|
||
return None, None
|
||
|
||
if not lines:
|
||
return None, None
|
||
|
||
# 1. 第1行 → nd, numpar
|
||
tokens = lines[0].split()
|
||
if len(tokens) < 2:
|
||
return None, None
|
||
try:
|
||
nd = int(float(tokens[0]))
|
||
numpar = int(float(tokens[1]))
|
||
except ValueError:
|
||
return None, None
|
||
if nd <= 0 or numpar <= 0:
|
||
return None, None
|
||
|
||
# 2. 行数计算
|
||
dm_lines = (nd + 5) // 6 # FORMAT 502: 6/行
|
||
block_lines = (numpar + 4) // 5 # FORMAT 503: 5/行
|
||
|
||
# 3. 跳过 DM 列阵
|
||
idx = 1 # 第1行已读
|
||
for _ in range(dm_lines):
|
||
if idx >= len(lines):
|
||
return None, None # 截断 → 跳过
|
||
idx += 1
|
||
|
||
# 4. nd 个深度块,每块第一行第一token = TEMP
|
||
temps = []
|
||
for _ in range(nd):
|
||
if idx >= len(lines):
|
||
break # 截断:已解析的深度保留
|
||
first_line_tokens = lines[idx].split()
|
||
if not first_line_tokens:
|
||
idx += block_lines
|
||
continue
|
||
t = parse_fortran_float(first_line_tokens[0])
|
||
temps.append(t if t is not None else float("nan"))
|
||
idx += block_lines # 跳到下一块(含本块剩余 block_lines-1 行)
|
||
|
||
if not temps:
|
||
return None, None
|
||
|
||
# 5. 三项判据
|
||
violations = []
|
||
surface_ratio = temps[0] / teff if teff > 0 else float("inf")
|
||
if surface_ratio > max_factor:
|
||
violations.append(
|
||
f"表层 T={temps[0]:.4g} > {max_factor}×Teff({teff}),ratio={surface_ratio:.4g}"
|
||
)
|
||
for i, t in enumerate(temps):
|
||
if not math.isfinite(t):
|
||
violations.append(f"深度 {i+1} T={t}(非有限,NaN/Inf)")
|
||
break
|
||
if t < temp_floor or t > temp_ceiling:
|
||
violations.append(
|
||
f"深度 {i+1} T={t:.4g} 越界 [{temp_floor}, {temp_ceiling}]"
|
||
)
|
||
break
|
||
return temps, violations
|
||
|
||
|
||
def main():
|
||
ap = argparse.ArgumentParser(description="fort.7 温度结构物理判定(DCTS 权威判据)")
|
||
ap.add_argument("fort7", help="fort.7 或 {stage}.7 路径")
|
||
ap.add_argument("--teff", type=float, required=True, help="Teff (K)")
|
||
ap.add_argument("--max-factor", type=float, default=3.0, help="表层/Teff 上限(默认 3.0)")
|
||
ap.add_argument("--floor", type=float, default=10.0, dest="temp_floor", help="温度下限 K(默认 10)")
|
||
ap.add_argument("--ceiling", type=float, default=1e8, dest="temp_ceiling", help="温度上限 K(默认 1e8)")
|
||
args = ap.parse_args()
|
||
|
||
import os
|
||
if not os.path.exists(args.fort7):
|
||
print(f"⚠ 文件不存在: {args.fort7}(跳过,不判失败)")
|
||
return 2
|
||
|
||
temps, violations = check_temperature_structure(
|
||
args.fort7, args.teff, args.max_factor, args.temp_floor, args.temp_ceiling
|
||
)
|
||
if temps is None:
|
||
print(f"⚠ 无法解析 {args.fort7}(缺失/截断/格式错)—— 跳过,不判失败")
|
||
return 2
|
||
|
||
surface_ratio = temps[0] / args.teff if args.teff > 0 else float("inf")
|
||
print(f"=== 温度结构判定: {args.fort7} (Teff={args.teff}) ===")
|
||
print(f" 深度数: {len(temps)}")
|
||
print(f" 表层 T: {temps[0]:.4g} K ({surface_ratio:.4g}×Teff,Eddington 参考 0.84)")
|
||
print(f" 底层 T: {temps[-1]:.4g} K")
|
||
nan_count = sum(1 for t in temps if not math.isfinite(t))
|
||
print(f" NaN/Inf 深度: {nan_count}")
|
||
|
||
if not violations:
|
||
print(f"✅ 物理(三项全过:表层<{args.max_factor}×Teff、各深度∈[{args.temp_floor},{args.temp_ceiling}]、无 NaN)")
|
||
return 0
|
||
print("❌ 不物理 —— 违规:")
|
||
for v in violations:
|
||
print(f" - {v}")
|
||
print("\n参考 references/physics-checks.md 的判据说明与修正历史。")
|
||
return 1
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|