diff --git a/.agents/skills/tlusty-synspec-test/SKILL.md b/.agents/skills/tlusty-synspec-test/SKILL.md new file mode 100644 index 0000000..670b181 --- /dev/null +++ b/.agents/skills/tlusty-synspec-test/SKILL.md @@ -0,0 +1,212 @@ +--- +name: tlusty-synspec-test +description: 规范直接运行 TLUSTY / SYNSPEC 二进制做物理验证测试的全流程——目录脚手架、输入文件生成与校验、冷启动链(lte→nc→nl)/种子步进链(seed_nc→nl)/SYNSPEC 执行、以及基于 DCTS 权威判据的事后物理审查。只要用户要在 DCTS 框架之外直接跑 tlusty_static / synspec_static 做收敛性、发散复现、物理解、光谱合成、fort.7/fort.9 分析、冷启动/种子链、网格点验证等测试,或在 dcts/test/ 下建测试目录,就必须用本 skill——即使用户没说"测试流程"四个字。 +--- + +# TLUSTY / SYNSPEC 物理测试流程 + +本 skill 把 `docs/testing_workflow_2026_08_11.md` 的规范、源码侧 `tests/{tlusty,synspec}/` 的官方 Runtest 结构、以及 DCTS `crates/common/src/` 的输入生成器与物理校验逻辑,统一成一套可执行流程。 + +## 核心原则(违反这些 = 测试无效) + +1. **测试必须建在 `dcts/test/` 下**(已被 `.gitignore` 排除,不入库)。禁止在仓库根、`data/runtime/`、共享目录直接跑二进制。 +2. **二进制优先用 `dcts/assets/tlusty_static` 与 `dcts/assets/synspec_static`**——它们比 `data/runtime/` 下的旧版新,且是生产链实际使用的版本。`data/runtime/` 的旧版仅用于历史对照。 +3. **物理性判定必须用 DCTS 权威判据** `check_temperature_structure`(见 §事后审查),不要用"表层应 0.5–1.5×Teff"等经验标准——后者曾误判物理模型为不物理。 +4. **执行前必须检查输入**(见 §执行前检查)。一次输入错误会浪费一次 20 分钟级的计算。 +5. **执行后必须回头审查**,不能只看"有没有 STOP"。有 STOP 但模型含 NaN = 非物理;无 STOP 但温度结构不物理 = 仍未完成。 + +## 第一步:判断测试类型(决策树) + +先问用户(或从任务推断)属于哪类,再去读对应的 reference: + +| 用户在做什么 | 测试类型 | 链类型 | 读哪个 reference | +|---|---|---|---| +| 从零算一个网格点的大气(LTE 灰化起步 → NLTE 连续 → NLTE 谱线) | `convergence`/`physics` | **冷启动链** lte→nc→nl | `references/tlusty-stages.md` | +| 从一个**已收敛邻居模型**热启动算新网格点 | `convergence` | **种子步进链** seed_nc→nl | `references/tlusty-stages.md` | +| 复现生产的发散/失败,对比生产 DB 轨迹 | `validation` | 冷启动或种子链(看生产记录) | `references/tlusty-stages.md` + `physics-checks.md` | +| 给定一个收敛大气(fort.7),合成光谱 | `synspec` | SYNSPEC 单步 | `references/synspec-inputs.md` | + +> 区分冷启动 vs 种子链的关键:冷启动 lte 阶段 `ltgray=T`,会**删除 fort.8**从灰大气重建;种子链第一阶 `seed_nc` 把**邻居的 fort.7 复制成 fort.8** 热启动。如果输入里有 `fort.8`(邻居种子)→ 种子链;如果没有、从 lte 开始 → 冷启动链。 + +## 第二步:建目录(用脚手架脚本) + +四级目录结构(详见 `references/directory-convention.md`): + +``` +dcts/test/{test_id}/{type}/{grid}/{content}/ + ├── inputs/ # 执行前生成并检查的全部输入 + ├── scripts/ # run.sh(必有) + ├── run/ # 执行工作目录,fort.* 在此生成 + └── outputs/ # 阶段产物(按阶段重命名)+ RESULT.md +``` + +**直接用脚手架,不要手敲 mkdir**: + +```bash +bash .agents/skills/tlusty-synspec-test/scripts/new_test.sh \ + 20260813_my_purpose convergence t60000_g5.0_he-2_cold baseline +``` + +`new_test.sh` 会创建四级目录、按链类型拷贝对应 `assets/run_*.sh` 模板为 `scripts/run.sh`、放一个 README 骨架。它会询问链类型(cold/seed/synspec)来选模板。 + +### 命名规范 + +- `test_id` = `{日期}_{目的}`,如 `20260813_tlusty_divergence` +- `type` ∈ `convergence | physics | synspec | validation` +- `grid` = `t{teff}_g{logg}_he{loghe}`,冷启动链加 `_cold`、种子链加 `_seed`(如 `t60000_g5.0_he-2_cold`、`t55000_g5.0_he-4_seed`) +- `content` = 描述测试变量,如 `baseline` / `itek0_iacc0_dpsilg15` / `niter50` / `nd70` + +## 第三步:生成输入文件 + +**优先级**:有 DCTS 代码生成器就用代码生成(保证与生产一致) > 无生成器则从已知正确模板 sed/python 改(必须留 diff 或注释) > 绝不凭空手写。 + +### 各链需要的输入(速查) + +| 链 | inputs/ 文件 | +|---|---| +| 冷启动链 | `lte.5` `nc.5` `nl.5` `lte.nst` `nc.nst` `nl.nst`(**无 fort.8**) | +| 种子链 | `seed_nc.5` `seed_nc.nst` `fort.8`(邻居收敛模型的 `.7`);若跑到 nl 还需 `nl.5` `nl.nst` | +| SYNSPEC | `fort.5` `fort.55` `fort.8`(收敛大气)+ `fort.19`/`data` 软链 | + +各文件的逐字段含义、各阶段差异、ilvlin/DPSILG/ORELAX/NITER 默认值——见 `references/tlusty-stages.md`(TLUSTY)与 `references/synspec-inputs.md`(SYNSPEC fort.55 九行)。 + +**关键不变量**(违反即输入错误,先记这几条): +- 冷启动链 `.5` 第 2 行:`lte.5`=`T T`,`nc.5`/`nl.5`=`F F` +- ions 行 `ilvlin`:lte/nc 阶段=0,nl 阶段=100,**裸核(nlevs=1)永远=0** +- `.5` 第 3 行的 `'nst'` 文件名必须与实际 nst 文件名一致(生产默认就是字面量 `nst`) +- nst 第 1 行含 `NITER=`;第 2 行可含 `ORELAX=/IACC=/ICHANG=/IELCOR=`;DPSILG 通过 escape hatch 注入(默认无,生产常用 `DPSILG=1.5` 抑制雪崩) +- `data/` 软链必须指向 `assets/data/`(133 个原子数据文件) + +### 阶段变换与 A/B 对照 + +**nc.5 → nl.5**(阶段变换,ilvlin 0→100)用 `scripts/stage_transform.py`,它字节级安全地改写离子行的 ilvlin 列(裸核保持 0)并按需改第 2 行 LTE/LTGRAY。**不要手写 awk 做这事**——awk 按空格切分会把 `'data/h1.dat'` 这类带空格引号路径拆碎,破坏输入(实测踩坑)。 + +```bash +python3 .agents/skills/tlusty-synspec-test/scripts/stage_transform.py inputs/nc.5 -o inputs/nl.5 --to nl --diff +``` + +**A/B 测试**:同 `{grid}` 下放多个 `{content}` 目录(baseline / 变体),生成后必须用 `scripts/diff_configs.py` 确认各组**只有你刻意改的参数不同**(带 `*` 的行应只有 ITEK/ORELAX/NITER 等),其余一致——这是对照有效的前提。 + +```bash +python3 .agents/skills/tlusty-synspec-test/scripts/diff_configs.py dirA/inputs dirB/inputs --teff 60000 +``` + +## 第四步:执行前检查(必做,用脚本) + +跑计算前,用校验脚本确认输入无误: + +```bash +# TLUSTY 链:校验 .5 第2行/ions、nst 关键参数、软链 +python3 .agents/skills/tlusty-synspec-test/scripts/check_inputs.py \ + path/to/{content}/inputs --chain cold --teff 60000 +``` + +`check_inputs.py` 会检查上面"关键不变量"那几条。**建议再做一次冒烟**(`timeout 30` 跑 lte 阶段),确认二进制能启动、fort.6 里回显的 NITER/ITEK/DPSILG 与输入一致,再全量跑。 + +## 第五步:执行(用 run.sh 模板) + +每个测试目录的 `scripts/run.sh` 已由 `new_test.sh` 从 `assets/` 拷好。直接: + +```bash +cd dcts/test/{test_id}/{type}/{grid}/{content} +bash scripts/run.sh # 可选参数:每阶段超时秒数(默认 1200) +``` + +模板 run.sh 已内置规范行为(`assets/run_cold_chain.sh` 等): +1. 切到 `run/`,复制 `inputs/*` 进去,建 `data` 软链 +2. **逐阶段执行**,每阶段 `timeout 1200 tlusty_static < stage.5 > fort.6 2> fort.14` +3. **每阶段产物立即按阶段重命名**:`fort.6→{stage}.6`、`fort.7→{stage}.7`、`fort.9→{stage}.9`、`fort.14→{stage}.14`(避免被下阶段覆盖) +4. **阶段间种子传递**:下一阶段 `fort.8` = 上一阶段 `fort.7` 的复制;冷启动链 lte 阶段删 `fort.8` +5. 归档到 `outputs/` + +> **为什么按阶段重命名是关键**:TLUSTY 每次都写同名 `fort.7`/`fort.9`,不重命名的话 lte 的产物会被 nc 覆盖,事后无法分阶段审查。官方 `RTlusty` 脚本用 `{model}.{ext}` 命名(如 `hhe35lt.7`)解决同样问题;我们用 `{stage}.7` 等价。 + +## 第六步:事后审查(必做,用脚本 + 判据) + +执行完,用脚本做数值与物理审查,**不要只看 rc=0**。 + +> ⚠️ **审查时机**:**必须等进程完全退出后再审查**(`bash scripts/run.sh` 完成、或 `pgrep -f tlusty_static` 为空)。TLUSTY 边算边写 fort.9——运行中实时读,最新一拍的迭代行是半写状态,max_relc 不可信(实测踩坑:运行中读到 iter6=0.477 貌似收敛,进程退出后同一迭代实为 1060)。run.sh 里每个阶段是串行的(一个阶段跑完才进下一阶段),所以等 run.sh 结束即可放心审查。 + +```bash +# 1. 收敛轨迹:解析 {stage}.9 的 max_relc 演化、检测 STOP/NaN。 +# --expected-niter N 传 nst 里配的 NITER:若末次 iter 远小于 N 且未收敛, +# 会提示"疑似 timeout 截断"(轨迹不完整,结论需谨慎)。rc=124 时的 +# outputs/{stage}.TIMEOUT 标记也会被自动发现。 +python3 .agents/skills/tlusty-synspec-test/scripts/check_fort9.py outputs/nl.9 --teff 60000 --expected-niter 100 + +# 2. 温度结构物理性(DCTS 权威判据,复刻 conv_check.rs:463) +python3 .agents/skills/tlusty-synspec-test/scripts/check_temperature.py outputs/nl.7 --teff 60000 +``` + +### 收敛判据(数值) +- `{stage}.9` 每次 ITER 跨所有深度的 `max_relc` = `|MAXIMUM|` 列最大值 +- **收敛** = 末次迭代 `max_relc < chmax`(默认 `0.001`)且有限 +- 单调下降 = 好;雪崩(突然涨几个数量级)= 发散 +- `{stage}.6` 里的 `**** STOP in SOLVE after ITER N` = 求解器发散中止 + +### 物理判据(DCTS 权威,必须用这套) +`check_temperature_structure`(`conv_check.rs:463`)三项全过才算物理: +1. **表层 T < 3 × Teff**(`surface_ratio = T[0]/Teff`,> 3 判不物理) +2. **每个深度 T ∈ [10, 1e8] K** +3. **无 NaN/Inf** + +物理参考(不是硬判据):Eddington 灰大气 `T(τ=0) ≈ 0.84×Teff`(60kK 模型表层应约 5 万 K);已收敛生产种子 `t60000_g5.0_he-2_c-3_n-4_o-4.7` 表层约 0.78×Teff 可作基准。 + +> ⚠️ **判据修正历史**:旧经验"表层 0.5–1.5×Teff"过严(误判物理模型为不物理);"表层 3–6 万 K"对 60kK 偏低(误判)。**只信 DCTS 三项判据**。详见 `references/physics-checks.md`。 + +### 伪收敛陷阱 +`max_relc` 极低 ≠ 收敛。若模型已 NaN,在 NaN 上迭代相对变化为 0,会伪装成收敛。务必同时跑温度结构检查 + NaN 检查。NaN 匹配模式(与 Rust 一致):`nan`/`inf`/`infinity`/`***`(3+星)/正指数 `E+300`+。 + +### 测试不算完成的情况(需继续) +- 有 STOP 但模型含 NaN(非物理) +- 无 STOP 但温度结构不物理 +- 无 STOP 且 `max_relc` 未达 0.001 + +## 第七步:记录结论(用 RESULT 模板) + +在 `outputs/` 写 `RESULT.md`(模板见 `assets/result_template.md`),诚实区分: +- **数值收敛**(max_relc 达标)vs **物理收敛**(温度结构三项全过)——只有后者才是真正成功 +- 记录:测试配置、各阶段迭代/末 max_relc/STOP/NaN/温度结构表、审查结论 + +完整的 README/RESULT 字段、阶段产物重命名表、目录示例——见 `references/directory-convention.md`。 + +--- + +## 何时读哪个 reference + +| 想知道 | 读 | +|---|---| +| `.5` 每行含义、各阶段(lte/nc/nl)参数差异、ilvlin/DPSILG/ORELAX/NITER 默认、冷启动 vs 种子链、fort.7↔fort.8 传递 | `references/tlusty-stages.md` | +| fort.55 九行控制卡逐行、fort.5/fort.8/fort.19、SYNSPEC 产物(.spec/.cont/.iden)、NaN 修复背景 | `references/synspec-inputs.md` | +| `check_temperature_structure` 完整判据、fort.7 解析算法(`(nd+5)/6` 行 DM、每块 `(numpar+4)/5` 行)、NaN/伪收敛/STOP、判据修正史 | `references/physics-checks.md` | +| 四级目录结构、命名规范、阶段文件重命名表、README/RESULT 模板 | `references/directory-convention.md` | + +## 脚本速查 + +| 脚本 | 作用 | +|---|---| +| `scripts/new_test.sh` | 脚手架:建四级目录 + 拷 run.sh 模板 + README 骨架 | +| `scripts/check_inputs.py` | 执行前校验 `.5`/nst/软链(TLUSTY 链) | +| `scripts/check_fort9.py` | 解析 `{stage}.9` 收敛轨迹、max_relc 演化、STOP/NaN;`--expected-niter N` 检测 timeout 截断 | +| `scripts/check_temperature.py` | 复刻 `check_temperature_structure`,对 `{stage}.7` 做温度结构物理判定 | +| `scripts/stage_transform.py` | 冷启动链 `.5` 阶段变换:`nc.5 → nl.5`(非裸核 ilvlin 0→100,字节级安全,避免手写 awk 踩坑) | +| `scripts/diff_configs.py` | A/B 测试配置对照:并排显示多组 nst/.5 参数,`*` 标出不一致项(应只有你刻意改的字段带 `*`) | + +**rc=124(timeout)感知**:三个 `run_*.sh` 模板在阶段被 `timeout` 杀死时会写 `outputs/{stage}.TIMEOUT` 标记并打印告警;`check_fort9.py` 会自动发现该标记(或用 `--expected-niter` 推断截断),在结论里提示"轨迹不完整,结论需谨慎"。**不要把 timeout 截断的轨迹当成完整发散/收敛结论**。 + +所有脚本都自定位 skill 目录,可从任意测试目录调用,传相对路径即可。 + +## 固定路径速查 + +| 项 | 路径 | +|---|---| +| 测试根 | `dcts/test/` | +| TLUSTY 二进制 | `dcts/assets/tlusty_static` | +| SYNSPEC 二进制 | `dcts/assets/synspec_static` | +| 原子数据 | `dcts/assets/data/`(133 文件,运行时 `data` 软链指此) | +| 全波段线表 | `dcts/assets/data/gfATO.dat`(SYNSPEC `fort.19` 软链) | +| 物理判据源码 | `dcts/crates/common/src/conv_check.rs`(`check_temperature_structure` @ L463、`check_fort9` @ L68) | +| 输入生成器源码 | `dcts/crates/common/src/{gen_input5,nst_writer,fort55_writer}.rs` | +| 链定义源码 | `dcts/crates/common/src/runner.rs`(`default_cold_chain` @ L20、`default_seed_chain` @ L67) | +| 流程规范文档 | `dcts/docs/testing_workflow_2026_08_11.md` | +| 源码侧参考测试 | `tests/tlusty/{hhe,cwd,disk,bstar,optab}/`、`tests/synspec/{hhe,bstar,kurucz,hybrid,pseudonlt,optab}/` | diff --git a/.agents/skills/tlusty-synspec-test/assets/result_template.md b/.agents/skills/tlusty-synspec-test/assets/result_template.md new file mode 100644 index 0000000..88b63fa --- /dev/null +++ b/.agents/skills/tlusty-synspec-test/assets/result_template.md @@ -0,0 +1,65 @@ +# {test_id} / {type} / {grid} / {content} + +## 目的 +一句话说清这次测试想验证什么。例:复现生产 t60000 冷启动 nl 阶段发散,对比 baseline 与 itek0/iacc0/dpsilg15 修复组合。 + +## 配置 +- 二进制: `dcts/assets/tlusty_static`(或 `synspec_static`) +- 网格: `t{teff}_g{logg}_he{loghe}`(如 `t60000_g5.0_he-2`,Teff=60000K, logg=5.0, He/H=1e-2) +- 链: 冷启动 `lte→nc→nl` / 种子 `seed_nc→nl` / SYNSPEC +- 变体: 与 baseline 不同的关键参数(`ITEK`/`IACC`/`DPSILG`/`ORELAX`/`NITER`) + +## 输入 +- inputs/ 生成方式: 代码生成(`gen_input5.rs`/`nst_writer.rs`)/ 模板修改(留 diff)/ 复制已验证输入 +- 关键参数: 例 nc NITER=10, nl NITER=100, DPSILG=1.5 + +## 执行 +- 命令: `bash scripts/run.sh` +- 各阶段: lte(rc=0) → nc(rc=0) → nl(rc=?) +- 耗时: 例 lte 8s + nc 312s + nl 421s + +## 结果(TLUSTY) + +| 阶段 | 迭代 | 末 max_relc | STOP | NaN | 温度结构 | +|---|---|---|---|---|---| +| lte | 0 | — | — | 0 | — | +| nc | 10 | 1.4e0 | 无 | 0 | 物理 | +| nl | 19 | 1.2e16 | STOP iter19 | 0 | 物理(停止时) | + +> max_relc 来自 `check_fort9.py`;温度结构来自 `check_temperature.py`(DCTS 权威判据)。 + +## 结果(SYNSPEC,若适用) + +| 指标 | 值 | +|---|---| +| 采样点数 | N | +| 波长范围 | ALAM0 – ALAST Å | +| NaN/Inf/坏行 | 0 | +| 分段 NaN (EUV/UV/可见/NIR) | 各段计数 | + +## 审查结论 +- **数值**: ❌/✅(max_relc 是否 < chmax=0.001;有无 STOP/雪崩) +- **物理**: ❌/✅(温度结构三项:表层 < 3×Teff、各深度 ∈ [10,1e8]K、无 NaN) +- **结论**: 一句话。**区分数值收敛与物理收敛**——只有后者才是真正成功。 + +例: +- 数值: ❌ nc 雪崩(9.7→2.3e15), nl STOP(1.23e16) +- 物理: ✅ 停止时温度结构物理(表层 0.85×Teff,无 NaN) +- 结论: 精确复现生产失败;修复组合(itek0/iacc0/dpsilg15)使 nl max_relc 降至 48.6 但仍 STOP,未达数值收敛。 + +## 复现 +```bash +cd dcts/test/{test_id}/{type}/{grid}/{content} +bash scripts/run.sh +# 事后审查(TLUSTY): +python3 ../../../../../../.agents/skills/tlusty-synspec-test/scripts/check_fort9.py outputs/nl.9 --teff 60000 +python3 ../../../../../../.agents/skills/tlusty-synspec-test/scripts/check_temperature.py outputs/nl.7 --teff 60000 +``` + +## 产物清单(outputs/) +| 文件 | 内容 | +|---|---| +| `lte.6/.7/.9/.14` | lte 阶段产物 | +| `nc.6/.7/.9/.14` | nc 阶段产物 | +| `nl.6/.7/.9/.14` | nl 阶段产物(最终模型 nl.7) | +| `{name}.spec/.cont/.iden` | SYNSPEC 光谱产物(若适用) | diff --git a/.agents/skills/tlusty-synspec-test/assets/run_cold_chain.sh b/.agents/skills/tlusty-synspec-test/assets/run_cold_chain.sh new file mode 100755 index 0000000..40e8ab5 --- /dev/null +++ b/.agents/skills/tlusty-synspec-test/assets/run_cold_chain.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +# ============================================================ +# 冷启动链执行模板 (lte → nc → nl) — 由 new_test.sh 拷为 scripts/run.sh +# 规范: docs/testing_workflow_2026_08_11.md §四;references/tlusty-stages.md +# 用法: bash scripts/run.sh [timeout_per_stage] +# ============================================================ +set -u +TEST_DIR="$(cd "$(dirname "$0")/.." && pwd)" +TIMEOUT="${1:-1200}" # 每阶段超时(秒) +BIN="/home/fmq/program/tlusty/tl208-s54/dcts/assets/tlusty_static" +RUNTIME_DATA="/home/fmq/program/tlusty/tl208-s54/dcts/assets/data" + +cd "$TEST_DIR" || { echo "无法进入 $TEST_DIR"; exit 1; } + +# ---------- 执行前检查 ---------- +echo "=== [检查] 输入文件 ===" +for f in lte.5 nc.5 nl.5 lte.nst nc.nst nl.nst; do + [ -f "inputs/$f" ] || { echo "❌ 缺输入: inputs/$f"; exit 1; } +done +echo " lte.5 L2: $(sed -n '2p' inputs/lte.5) (应含 T T)" +echo " nc.5 L2: $(sed -n '2p' inputs/nc.5) (应含 F F)" +echo " nl.5 L2: $(sed -n '2p' inputs/nl.5) (应含 F F)" +for s in lte nc nl; do echo " $s.nst L1: $(head -1 inputs/$s.nst)"; done +# 冷启动链不应有 fort.8 +[ -f "inputs/fort.8" ] && { echo "❌ 冷启动链不应有 inputs/fort.8(lte 阶段 ltgray=T 会删它)"; exit 1; } +mkdir -p run outputs +ln -sfn "$RUNTIME_DATA" run/data +echo " data -> $(readlink run/data)" + +# ---------- 准备 run/ ---------- +echo "=== [准备] run/ 工作目录 ===" +cp inputs/*.5 inputs/*.nst run/ 2>/dev/null +rm -f run/fort.6 run/fort.7 run/fort.8 run/fort.9 run/fort.14 + +# ---------- 阶段执行(每阶段产物按阶段重命名) ---------- +run_stage() { + local stage="$1" + echo "=== [执行] $stage (timeout ${TIMEOUT}s) ===" + cp "run/$stage.nst" run/nst 2>/dev/null || { echo "❌ 无 run/$stage.nst"; exit 1; } + # 冷启动 lte: 删 fort.8 (ltgray=T 强制灰大气重建);nc/nl: 保留上阶段 fort.7→fort.8 + if [ "$stage" = "lte" ]; then rm -f run/fort.8; fi + cd run + timeout "$TIMEOUT" "$BIN" < "$stage.5" > fort.6 2> fort.14 + local rc=$? + cd .. + # rc=124 = timeout 杀死(GNU timeout)。结果可能未完成/被截断,写标记供事后审查注意。 + if [ "$rc" -eq 124 ]; then + echo " ⚠⚠ $stage TIMEOUT (rc=124, 超过 ${TIMEOUT}s 被 kill —— 迭代可能未完成,产物被截断)" + touch "$TEST_DIR/outputs/${stage}.TIMEOUT" + fi + cp run/fort.6 "$stage.6" 2>/dev/null + [ -f run/fort.7 ] && cp run/fort.7 "$stage.7" + [ -f run/fort.9 ] && cp run/fort.9 "$stage.9" + cp run/fort.14 "$stage.14" 2>/dev/null + [ -f run/fort.7 ] && cp run/fort.7 run/fort.8 # 下阶段种子 + echo " $stage rc=$rc" +} + +echo "[$(date +%H:%M:%S)] $TEST_DIR 开始" +run_stage lte +run_stage nc +run_stage nl +echo "[$(date +%H:%M:%S)] $TEST_DIR 完成" + +# ---------- 归档 ---------- +echo "=== [归档] -> outputs/ ===" +for s in lte nc nl; do + [ -f "$s.6" ] && cp "$s.6" "$s.7" "$s.9" "$s.14" outputs/ 2>/dev/null +done +echo "outputs/: $(ls outputs/ | wc -l) 个文件" +echo "=== 完成。请做事后审查(references/physics-checks.md):" +echo " python3 ../../../../../../.agents/skills/tlusty-synspec-test/scripts/check_fort9.py outputs/nl.9" +echo " python3 ../../../../../../.agents/skills/tlusty-synspec-test/scripts/check_temperature.py outputs/nl.7 --teff " diff --git a/.agents/skills/tlusty-synspec-test/assets/run_seed_chain.sh b/.agents/skills/tlusty-synspec-test/assets/run_seed_chain.sh new file mode 100755 index 0000000..9491ed8 --- /dev/null +++ b/.agents/skills/tlusty-synspec-test/assets/run_seed_chain.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +# ============================================================ +# 种子步进链执行模板 (seed_nc [→ nl]) — 由 new_test.sh 拷为 scripts/run.sh +# 规范: docs/testing_workflow_2026_08_11.md §四;references/tlusty-stages.md +# 输入: seed_nc.5 seed_nc.nst fort.8(邻居收敛模型);若跑到 nl 还需 nl.5 nl.nst +# 用法: bash scripts/run.sh [timeout_per_stage] +# ============================================================ +set -u +TEST_DIR="$(cd "$(dirname "$0")/.." && pwd)" +TIMEOUT="${1:-1800}" # 种子链 seed_nc 给宽点 +BIN="/home/fmq/program/tlusty/tl208-s54/dcts/assets/tlusty_static" +RUNTIME_DATA="/home/fmq/program/tlusty/tl208-s54/dcts/assets/data" + +cd "$TEST_DIR" || { echo "无法进入 $TEST_DIR"; exit 1; } + +# ---------- 执行前检查 ---------- +echo "=== [检查] 输入文件 ===" +for f in seed_nc.5 seed_nc.nst fort.8; do + [ -f "inputs/$f" ] || { echo "❌ 缺输入: inputs/$f"; exit 1; } +done +echo " seed_nc.5 L2: $(sed -n '2p' inputs/seed_nc.5) (应含 F F)" +echo " seed_nc.nst L1: $(head -1 inputs/seed_nc.nst) (应含 NITER=, ICHANG=0, ORELAX=0.3)" +echo " fort.8 NaN行: $(grep -c -iE 'nan|inf|\*{3,}' inputs/fort.8 || true) (应为 0)" +mkdir -p run outputs +ln -sfn "$RUNTIME_DATA" run/data +echo " data -> $(readlink run/data)" + +# ---------- 准备 run/ ---------- +cp inputs/seed_nc.5 inputs/seed_nc.nst inputs/fort.8 run/ 2>/dev/null +[ -f inputs/nl.5 ] && cp inputs/nl.5 inputs/nl.nst run/ 2>/dev/null +rm -f run/fort.6 run/fort.7 run/fort.8.run run/fort.9 run/fort.14 +# seed_nc 阶段种子 = 邻居模型 (inputs/fort.8 已经拷进 run/) + +run_stage() { + local stage="$1" + echo "=== [执行] $stage (timeout ${TIMEOUT}s) ===" + cp "run/$stage.nst" run/nst 2>/dev/null || { echo "❌ 无 run/$stage.nst"; exit 1; } + # seed_nc: run/fort.8 已是邻居种子;nl: 上阶段 fort.7→fort.8 + if [ "$stage" = "nl" ]; then + [ -f run/seed_nc.7 ] && cp run/seed_nc.7 run/fort.8 || { echo "⚠ 无 seed_nc.7 作 nl 种子"; } + fi + cd run + timeout "$TIMEOUT" "$BIN" < "$stage.5" > fort.6 2> fort.14 + local rc=$? + cd .. + # rc=124 = timeout 杀死。种子链 seed_nc 本就慢,更要注意截断。 + if [ "$rc" -eq 124 ]; then + echo " ⚠⚠ $stage TIMEOUT (rc=124, 超过 ${TIMEOUT}s 被 kill —— 迭代可能未完成,产物被截断)" + touch "$TEST_DIR/outputs/${stage}.TIMEOUT" + fi + cp run/fort.6 "$stage.6" 2>/dev/null + [ -f run/fort.7 ] && cp run/fort.7 "$stage.7" + [ -f run/fort.9 ] && cp run/fort.9 "$stage.9" + cp run/fort.14 "$stage.14" 2>/dev/null + echo " $stage rc=$rc" +} + +echo "[$(date +%H:%M:%S)] $TEST_DIR 开始" +run_stage seed_nc +# nl 仅在 nl.5 存在时执行 +if [ -f inputs/nl.5 ]; then + [ -f run/seed_nc.7 ] && cp run/seed_nc.7 run/fort.8 + run_stage nl +fi +echo "[$(date +%H:%M:%S)] $TEST_DIR 完成" + +# ---------- 归档 ---------- +echo "=== [归档] -> outputs/ ===" +for s in seed_nc nl; do + [ -f "$s.6" ] && cp "$s.6" "$s.7" "$s.9" "$s.14" outputs/ 2>/dev/null +done +echo "outputs/: $(ls outputs/ | wc -l) 个文件" +echo "=== 完成。请做事后审查(references/physics-checks.md):" +echo " python3 ../../../../../../.agents/skills/tlusty-synspec-test/scripts/check_fort9.py outputs/seed_nc.9" +echo " python3 ../../../../../../.agents/skills/tlusty-synspec-test/scripts/check_temperature.py outputs/seed_nc.7 --teff " diff --git a/.agents/skills/tlusty-synspec-test/assets/run_synspec.sh b/.agents/skills/tlusty-synspec-test/assets/run_synspec.sh new file mode 100755 index 0000000..196a1ff --- /dev/null +++ b/.agents/skills/tlusty-synspec-test/assets/run_synspec.sh @@ -0,0 +1,90 @@ +#!/bin/bash +# ============================================================ +# SYNSPEC 执行模板 — 由 new_test.sh 拷为 scripts/run.sh +# 规范: docs/testing_workflow_2026_08_11.md §四;references/synspec-inputs.md +# 输入: fort.5(stdin) fort.55(控制卡) fort.8(收敛大气) + fort.19/data 软链 +# 用法: bash scripts/run.sh [timeout_sec] +# 产物: outputs/{name}.spec/.cont/.iden/.lindat + synspec.log +# ============================================================ +set -uo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +TEST_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +INPUTS="$TEST_DIR/inputs"; RUN="$TEST_DIR/run"; OUTPUTS="$TEST_DIR/outputs" + +SYNSPEC="/home/fmq/program/tlusty/tl208-s54/dcts/assets/synspec_static" +RUNTIME_DATA="/home/fmq/program/tlusty/tl208-s54/dcts/assets/data" +TIMEOUT_SEC="${1:-1200}" +OUT_NAME="${OUT_NAME:-spectrum}" # 产物名前缀,可环境变量覆盖 + +# --- 1. 切 run/,清理上轮 --- +mkdir -p "$RUN" "$OUTPUTS" +cd "$RUN" +rm -f fort.* *.spec *.cont *.iden *.lindat *.log *.err + +# --- 2. 复制输入 --- +[ -f "$INPUTS/fort.5" ] || { echo "❌ 缺 inputs/fort.5"; exit 1; } +[ -f "$INPUTS/fort.55" ] || { echo "❌ 缺 inputs/fort.55"; exit 1; } +[ -f "$INPUTS/fort.8" ] || { echo "❌ 缺 inputs/fort.8 (收敛大气)"; exit 1; } +cp "$INPUTS/fort.5" fort.5 +cp "$INPUTS/fort.55" fort.55 +cp "$INPUTS/fort.8" fort.8 +# fort.19 / data: 软链(优先用 inputs 里已有的,否则指 assets) +if [ -e "$INPUTS/fort.19" ]; then + ln -sfn "$(readlink -f "$INPUTS/fort.19")" fort.19 +else + ln -sfn "$RUNTIME_DATA/gfATO.dat" fort.19 # 全波段默认 +fi +ln -sfn "$RUNTIME_DATA" data + +# --- 3. 执行前自检 --- +echo "=== 执行前自检 ===" +echo "fort.5 第1行 TEFF/GRAV: $(sed -n '1p' fort.5)" +echo "fort.55 第6行 波长范围: $(sed -n '6p' fort.55)" +echo "fort.8 行数: $(wc -l < fort.8)" +echo "fort.8 NaN/坏行: $(grep -ciE 'nan|inf|\*{3,}' fort.8 || true)" +echo "fort.19 -> $(readlink fort.19)" +echo "data -> $(readlink data)" +echo "" + +# --- 4. 执行 SYNSPEC --- +echo "=== 执行 SYNSPEC (timeout ${TIMEOUT_SEC}s) ===" +T0=$(date +%s) +set +e +timeout "$TIMEOUT_SEC" "$SYNSPEC" < fort.5 > synspec.log 2> synspec.err +RC=$? +set -e +T1=$(date +%s) +echo "rc=$RC, elapsed=$((T1-T0))s" +# rc=124 = timeout 杀死。SYNSPEC 大线表全波段易超时,产物可能不完整。 +if [ "$RC" -eq 124 ]; then + echo "⚠⚠ SYNSPEC TIMEOUT (rc=124, 超过 ${TIMEOUT_SEC}s 被 kill —— spec 可能不完整/被截断)" + touch "$OUTPUTS/SYNSPEC.TIMEOUT" +fi + +# --- 5. 产物重命名 --- +# SYNSPEC: fort.7=spectrum, fort.17=continuum, fort.12=line IDs, fort.11=line data +[ -f fort.7 ] && cp fort.7 "$OUTPUTS/${OUT_NAME}.spec" +[ -f fort.17 ] && cp fort.17 "$OUTPUTS/${OUT_NAME}.cont" +[ -f fort.12 ] && cp fort.12 "$OUTPUTS/${OUT_NAME}.iden" +[ -f fort.11 ] && cp fort.11 "$OUTPUTS/${OUT_NAME}.lindat" +cp synspec.log "$OUTPUTS/synspec.log" 2>/dev/null || true +cp synspec.err "$OUTPUTS/synspec.err" 2>/dev/null || true + +# --- 6. 立即数值审查 --- +echo "" +echo "=== 数值审查 ===" +SPEC="$OUTPUTS/${OUT_NAME}.spec" +if [ -f "$SPEC" ]; then + ROWS=$(wc -l < "$SPEC") + FIRST=$(head -1 "$SPEC" | awk '{print $1}') + LAST=$(tail -1 "$SPEC" | awk '{print $1}') + NAN=$(grep -ciE 'nan|inf|\*{3,}' "$SPEC" || true) + echo "spec 行数: $ROWS" + echo "波长范围: $FIRST - $LAST Å" + echo "NaN/Inf/坏行: $NAN $([ "$NAN" = 0 ] && echo '✅' || echo '❌')" +else + echo "!! 未生成 ${OUT_NAME}.spec(SYNSPEC 失败,rc=$RC)" +fi +echo "" +echo "synspec.log 尾 15 行:" +tail -15 "$OUTPUTS/synspec.log" 2>/dev/null || echo "(无 log)" diff --git a/.agents/skills/tlusty-synspec-test/references/directory-convention.md b/.agents/skills/tlusty-synspec-test/references/directory-convention.md new file mode 100644 index 0000000..f54c0b4 --- /dev/null +++ b/.agents/skills/tlusty-synspec-test/references/directory-convention.md @@ -0,0 +1,164 @@ +# 测试目录结构、命名与文档模板 + +参考:`docs/testing_workflow_2026_08_11.md`、现有测试 `dcts/test/20260811_tlusty_divergence/`、`dcts/test/20260812_synspec_fullband/`。 + +## 四级目录结构 + +每次测试在 `dcts/test/` 下建独立子目录,禁止在 test 根或共享目录直接执行。 + +``` +dcts/test/ +├── {test_id}/ # 1层:本次测试唯一目录({日期}_{目的}) +│ └── {type}/ # 2层:测试类型 +│ └── {grid}/ # 3层:网格/参数区 +│ └── {content}/ # 4层:测试内容(测试变量) +│ ├── inputs/ # 输入文件(执行前生成并检查) +│ ├── scripts/ # run.sh(必有) +│ ├── run/ # 执行工作目录(fort.* 在此生成) +│ └── outputs/ # 阶段产物(按阶段重命名)+ RESULT.md +``` + +`new_test.sh` 会自动建这四级并把对应模板拷成 `scripts/run.sh`。 + +## 命名规范 + +| 层 | 规则 | 示例 | +| ----------- | ------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | +| `test_id` | `{日期}_{目的}` | `20260813_tlusty_divergence` | +| `type` | `convergence`(收敛) / `physics`(物理解) / `synspec`(光谱) / `validation`(校验复现) | `convergence` | +| `grid` | `t{teff}_g{logg}_he{loghe}` + 链后缀;可追加 CNO | `t60000_g5.0_he-2_cold`、`t55000_g5.0_he-4_seed`、`t50000_g5.0_cno` | +| `content` | 描述测试变量 | `baseline`、`itek0_iacc0_dpsilg15`、`niter50`、`nd70`、`gfato_100_20000` | + +- **链后缀**:冷启动链 `_cold`,种子链 `_seed`。 +- 脚本统一 `run.sh`;输入文件按链类型命名(见 `references/tlusty-stages.md`)。 + +## 阶段文件重命名规范(关键) + +TLUSTY 每阶段都写同名 `fort.*`,**必须每阶段执行后立即重命名**,否则被下阶段覆盖。 + +| 阶段产物 | 重命名 | 说明 | +| ------------------------- | -------------- | ----------------------------------------- | +| `fort.6` (stdout) | `{stage}.6` | `lte.6`/`nc.6`/`nl.6`/`seed_nc.6` | +| `fort.7` (模型) | `{stage}.7` | 事后温度结构审查的输入 | +| `fort.9` (迭代收敛记录) | `{stage}.9` | 事后收敛轨迹审查的输入 | +| `fort.14` (stderr) | `{stage}.14` | — | + +**种子传递**:下一阶段 `fort.8` = 上一阶段 `fort.7` 的复制(冷启动 lte 阶段删 `fort.8`)。 + +### 执行后 run/ 目录状态(冷启动链) + +``` +run/ +├── fort.6 fort.7 fort.9 fort.14 # 末阶段(nl)产物 +├── lte.6 lte.7 lte.9 lte.14 # lte 阶段(已重命名) +├── nc.6 nc.7 nc.9 nc.14 # nc 阶段 +├── nl.6 nl.7 nl.9 nl.14 # nl 阶段 +├── data -> assets/data/ # 原子数据软链 +└── fort.8 # 种子(若在种子链) +``` + +## 执行前检查清单(TLUSTY 冷启动链) + +执行前对 `inputs/` 逐项确认(`check_inputs.py` 自动做): + +- [ ] `lte.5` 第2行 `T T`;`nc.5`/`nl.5` 第2行 `F F` +- [ ] `.5` 第3行 `'nst'` 与实际 nst 文件名一致(生产默认 `nst`) +- [ ] ions 行 `ilvlin`:nc 阶段=0,nl 阶段非裸核=100,裸核(nlevs=1)=0 +- [ ] nst 第1行含 `NITER=`;第2行 `ORELAX/IACC/ICHANG/IELCOR` 正确;DPSILG 注入值(若有) +- [ ] `data/` 软链指向 `assets/data/` +- [ ] **冷启动链确认无 fort.8**;种子链确认 `fort.8` 存在且无 NaN + +建议再加一次冒烟(`timeout 30` 跑 lte),确认 fort.6 回显的 NITER/ITEK/DPSILG 与输入一致。 + +## 超时与并行 + +- 不需要超时限制 +- 每个 tlusty 进程 ~260MB,按可用内存控制并行度 +- 多测试并行时各日志独立(`scripts/run.log` 或外部日志) + +## RESULT.md 模板 + +每个 `{content}/outputs/` 写 `RESULT.md`(模板见 `assets/result_template.md`)。核心是**诚实区分数值收敛与物理收敛**: + +```markdown +# {test_id} / {type} / {grid} / {content} + +## 目的 +一句话说清这次测试想验证什么。 + +## 配置 +- 二进制: dcts/assets/tlusty_static(或 synspec_static) +- 网格: t{teff}_g{logg}_he{loghe}[_cno] +- 链: 冷启动 lte→nc→nl / 种子 seed_nc→nl / SYNSPEC +- 变体: {ITEK}/{IACC}/{DPSILG}/{ORELAX}/{NITER}(与 baseline 不同的关键参数) + +## 输入 +- inputs/ 生成方式: 代码生成 / 模板修改(留 diff) +- 关键参数: ... + +## 执行 +- 命令: bash scripts/run.sh +- 各阶段: lte(rc=0) → nc(rc=0) → nl(rc=?) +- 耗时: ... + +## 结果(TLUSTY) +| 阶段 | 迭代 | 末 max_relc | STOP | NaN | 温度结构 | +|---|---|---|---|---|---| +| lte | 0 | — | — | 0 | — | +| nc | 10 | 1.4e0 | 无 | 0 | 物理 | +| nl | 19 | 1.2e16 | STOP iter19 | 0 | 物理(停止时) | + +## 结果(SYNSPEC,若适用) +| 指标 | 值 | +|---|---| +| 采样点数 | N | +| 波长范围 | ALAM0 – ALAST Å | +| NaN/Inf/坏行 | 0 | +| 分段 NaN | EUV/UV/可见/NIR 各段计数 | + +## 审查结论 +- 数值: ❌/✅(max_relc 是否达标;有无 STOP/雪崩) +- 物理: ❌/✅(温度结构三项:表层<3×Teff、各深度∈[10,1e8]、无 NaN) +- 结论: 一句话。区分"数值收敛"与"物理收敛"——只有后者才是真正成功。 + +## 复现 +cd dcts/test/{test_id}/{type}/{grid}/{content} +bash scripts/run.sh +``` + +## 完整示例(参考现有测试) + +``` +dcts/test/20260811_tlusty_divergence/ +└── convergence/ + ├── t60000_g5.0_he-2_cold/ + │ ├── baseline/ # 生产配置复现 + │ │ ├── inputs/ (lte.5 nc.5 nl.5 lte.nst nc.nst nl.nst) + │ │ ├── scripts/ (run.sh) + │ │ ├── run/ + │ │ ├── outputs/ + │ │ └── README.md + │ └── itek0_iacc0_dpsilg15/ # 修复变体 + │ └── ... + ├── t60000_g5.0_he-2_seed/ + │ └── doubleoff_dpsilg15/ # 种子链变体 + │ └── ... (inputs 含 fort.8) + └── t55000_g5.0_he-4_cold/ + └── itek0_iacc0_dpsilg15/ # 另一网格点 + └── ... + +dcts/test/20260812_synspec_fullband/ +└── synspec/ + └── t50000_g5.0_cno/ + └── gfato_100_20000/ # 全波段 SYNSPEC + ├── inputs/ (fort.5 fort.55 fort.8 + fort.19/data 软链) + ├── scripts/ (run.sh) + ├── run/ + └── outputs/ (fullband.spec/.cont/.iden + RESULT.md) +``` + +A/B 测试(baseline vs 变体)放同一 `{grid}` 下两个 `{content}` 目录,便于对照。生成完输入后用 `scripts/diff_configs.py` 确认各组**只有刻意改的参数不同**(带 `*` 的行应只有 ITEK/ORELAX/NITER 等),否则对照无效: + +```bash +python3 .agents/skills/tlusty-synspec-test/scripts/diff_configs.py dirA/inputs dirB/inputs dirC/inputs --teff 60000 +``` diff --git a/.agents/skills/tlusty-synspec-test/references/physics-checks.md b/.agents/skills/tlusty-synspec-test/references/physics-checks.md new file mode 100644 index 0000000..e324db5 --- /dev/null +++ b/.agents/skills/tlusty-synspec-test/references/physics-checks.md @@ -0,0 +1,114 @@ +# 物理校验判据与 fort.7 解析(DCTS 权威) + +参考源码:`dcts/crates/common/src/conv_check.rs`。**物理性判定必须以这里的逻辑为准**,不要用经验标准。 + +## 温度结构判据:`check_temperature_structure` + +`conv_check.rs:463`。返回 `TempStructCheckResult`,`valid = violations.is_empty()`。三项全过才算物理: + +1. **表层 T < max_factor × Teff** + - `surface_ratio = temps[0] / teff`(`temps[0]` = 最浅深度 = 表层) + - `surface_ratio > max_factor`(默认 **3.0**)→ 违规 + - `teff <= 0` → ratio = +Inf → 必违规 +2. **每个深度 T ∈ [temp_floor, temp_ceiling]** + - 默认 **[10.0, 1.0e8] K** + - 任一深度 T 越界 → 违规 +3. **无 NaN/Inf** + - 非有限 T(NaN/Inf)→ 违规(被第 2 条的 `is_finite()` 捕获) + +报告**首个**违规深度。 + +### 物理参考(不是硬判据,仅辅助判断) +- **Eddington 灰大气**:`T(τ=0) ≈ 0.84 × Teff`。60kK 模型表层应约 5 万 K。 +- **生产基准**:已收敛种子 `t60000_g5.0_he-2_c-3_n-4_o-4.7` 表层约 0.78×Teff。 +- **内部温度**:无 1e7+ 异常隆起(如某修复前组合曾出现 6.2e9 K 内部尖峰)。 + +### ⚠️ 判据修正历史(为何只信 DCTS 三项) +- 旧经验"表层应 0.5–1.5×Teff":**过严**,曾误判物理模型为不物理(把表层 0.78×Teff 的好模型判死)。 +- 旧经验"表层应 3–6 万 K":对 60kK 模型**偏低**,曾误判。 +- **正确做法**:只用"表层 < 3×Teff 且各深度 ∈ [10,1e8] 且无 NaN"三项硬判据,Eddington 0.84 仅作物理直觉参考。 + +## fort.7 大气模型文件格式与解析 + +`conv_check.rs:470-502`,对应 TLUSTY 源 `tlusty208.f:14066-14108`(`OUTPUT` 子程序): + +``` +第 1 行: ND NUMPAR + ND = 深度点数; NUMPAR = 每深度参数数 = NLEVEL + NUMLT +接下来 ceil(ND/6) 行: DM 列阵(质量深度),每行 6 个值(FORMAT 502) +接下来 ND 个深度块: 每块 ceil(NUMPAR/5) 行(FORMAT 503,每行 5 值) + 每块【第一行第一个 token】= 该深度的温度 TEMP(ID) + 块内其余 = 电子密度、质量密度、NLTE 能级布居/ departure 系数 +``` + +`temps[0]` = 表层(ID=1,最浅);`temps[nd-1]` = 底层(最深)。 + +### 解析算法(`check_temperature.py` 复刻) +``` +1. 读第1行 → nd, numpar = 前两个 token。任一为 0 → 跳过(返回 None,不判失败) +2. dm_lines = (nd + 5) // 6 # 跳过 DM 列阵 + block_lines = (numpar + 4) // 5 # 每深度块行数 +3. 跳过 dm_lines 行 +4. 对 nd 个深度,每个: + 取块第一行第一个 token,parse_fortran_float → temps[id] + 跳过 block_lines - 1 行 +5. 检查三层判据 +``` + +`parse_fortran_float`(`conv_check.rs:44`)兼容 Fortran 无-E 记数法:指数 ≥100 时 gfortran 挤掉 E,如 `-1.35E+118` 输出成 `-1.35+118`。解析:先标准 parse;失败则正则 `^([+-]?[\d.]+)([+-]\d+)$` 在尾符号前插 E 再 parse;溢出返回 ±Inf。 + +> 参考实现见 `conv_check.rs:1339 make_dot7(temps)` —— 构造测试用 fort.7 的辅助函数(写 `ND NUMPAR=3`,一行 DM 占位,每深度一行 `T 1.0E+10 1.0E-15`)。 + +## 收敛轨迹判据:`check_fort9` + +`conv_check.rs:68`。解析 `fort.9`(迭代收敛记录)。 + +### fort.9 格式 +``` + RELATIVE CHANGES OF VECTOR PSI + ITER ID TEMP NE POP RAD MAXIMUM ilev ifr + 1 50 -8.46E-04 1.53E-03 2.51E-02 -2.20E-03 2.51E-02 74 1 + 1 49 ... + ... + 2 50 ... +``` +每行 = 一次迭代在一个深度点的相对变化:`ITER(迭代号) ID(深度) TEMP NE POP RAD MAXIMUM(本次迭代本深度的最大相对变化) ilev ifr`。 + +### 判据 +- 每次 ITER 的 `max_relc` = 该 ITER 所有深度行 `|MAXIMUM|` 的最大值 +- **末次迭代**的 `max_relc` = 整个阶段的结果 +- **收敛** = `max_relc < chmax`(默认 **0.001**)且 `is_finite()` +- 单调下降 = 健康;突然涨几个数量级 = 雪崩发散 +- `chmax <= 0` 或非有限 = 非法配置(返回错误,max_relc=Inf) + +### STOP 检测(fort.6) +`fort.6`(stdout)里的 `**** STOP in SOLVE after ITER N` = 求解器发散中止。`conv_check.rs` 用 `SOLVER_STOP_RE` 匹配。这是比"非零 rc"更具体的发散信号。 + +## NaN / 伪收敛检测 + +### NaN 匹配模式(与 Rust 一致) +`conv_check.rs:18` `NAN_RE_PATTERN = (?i)(\bnan\b|\binf(?:inity)?\b|\*{3,}|[eE]\+(?:3\d{2}|[4-9]\d{2,}))` + +匹配:`nan` / `inf` / `infinity` / `***`(3+星) / 正指数 `E+300`~`E+999`(超高正指数视同数值无效,因发散时布居数变化可达 ±100~±200 量级但温度/流量场溢出用 `***` 或 `E+300+`)。 + +相关函数: +- `atmosphere_has_nan`(L246):扫 fort.7 全部数值 token +- `spec_is_valid`(L290):扫光谱文件,同正则 + +### 伪收敛陷阱(必查) +`max_relc` 极低 ≠ 收敛。若模型已 NaN,**在 NaN 上迭代的相对变化为 0**,会伪装成 max_relc≈0 的"收敛"。 + +**正确流程**(每次事后审查都要做): +1. `check_fort9.py` 看轨迹 + STOP + max_relc +2. `check_temperature.py` 看温度结构三项(含 NaN 检查) +3. **两者都过才算真收敛**;仅 fort.9 看着收敛但 fort.7 有 NaN = 伪收敛 + +## 其他 DCTS 校验(了解即可) +`conv_check.rs` 还提供(生产链用,测试时按需): +- `check_energy_conservation`(L374):能量守恒 +- `check_emflux_bolometric`(L578):辐射通量 +- `check_convergence_trace`(L691):收敛轨迹综合 +- `check_bfactor`(L759):departure 系数(.bfac 文件,同样 ND NUMPAR + DM 头格式) +- `extract_failure_hint`(L851):失败原因提取 + +测试中常用前两类(温度结构 + 收敛轨迹)即可覆盖绝大多数判断。 diff --git a/.agents/skills/tlusty-synspec-test/references/synspec-inputs.md b/.agents/skills/tlusty-synspec-test/references/synspec-inputs.md new file mode 100644 index 0000000..6343f3b --- /dev/null +++ b/.agents/skills/tlusty-synspec-test/references/synspec-inputs.md @@ -0,0 +1,78 @@ +# SYNSPEC 输入与执行详解 + +参考源码:`dcts/crates/common/src/fort55_writer.rs`(fort.55 生成)、`config.rs:1184` `SynspecInput`。 + +## SYNSPEC 做什么 + +给定一个**收敛的 TLUSTY 大气**(fort.7/fort.8),SYNSPEC 合成指定波长范围的理论光谱(emergent flux vs wavelength)。输出:连续谱 + 谱线 + 谱线标识。 + +## 输入文件 + +| 文件 | 来源 | 说明 | +|---|---|---| +| `fort.5` | SYNSPEC stdin(模型描述) | 与 TLUSTY 的 `.5` 不同;含模型 TEFF/丰度/模式开关。测试里通常从已验证输入复制 | +| `fort.55` | `fort55_writer.rs` 生成 | 控制卡,**恰好 9 行**(有回归测试) | +| `fort.8` | 收敛大气的 `.7` 复制 | 输入大气;TEFF/丰度须与 fort.5 一致 | +| `fort.19` | `assets/data/gfATO.dat` 软链 | 原子线表(全波段,18–23000 Å,~238MB,278 万条线) | +| `data/` | `assets/data/` 软链 | 原子数据(与 TLUSTY 共用) | + +> 可见光子集线表 `assets/gfVIS99.dat`(3000–7000 Å)用于窄段测试;全波段用 `gfATO.dat`。 + +## fort.55 九行控制卡(逐行) + +`fort55_writer.rs:26 generate_fort55_content()`,每行 1-to-1 对应 `synspec54.f` 的一个 `READ(55,*)`: + +| 行 | 变量 | 默认 | 说明 / 源码行 | +|---|---|---|---| +| 1 | `IMODE IDSTD IPRIN` | `0 0 1` | `synspec54.f:253` | +| 2 | `INMOD INTRPL ICHANG ICHEMC` | `1 0 0 0` | `:254` | +| 3 | `IOPHLI nunalp nunbet nungam nunbal` | — | `:255` 氢线开关 | +| 4 | `IFREQ INLTE ICONTL INLIST IFHE2` | — | `:2146`。**注意 IFREQ 在这行第1列**,不是第1行(历史错位 bug,已加回归测试) | +| 5 | `IHYDPR IHE1PR IHE2PR` | — | `:2148` 氢/氦线详细处理开关 | +| 6 | `ALAM0 ALAST CUTOF0 CUTOFS RELOP SPACE` | `1400.0 1410.0 10 0 0.0001 0.01` | `:2149` **波长范围与采样**。ALAM0=起始 Å,ALAST=结束 Å,RELOP=相对采样步长,SPACE=绝对步长。波长用 `:.1` | +| 7 | `nmlist [iunitm...]` | `0` | `:2208`(IMODE∈[-3,1] 时读)。**分子列表行**,必须独占一行,否则会吞掉下一行的 VTB | +| 8 | `VTB` | `-1.0` | `:2251` 微观湍流速度 km/s。**负值**=`-1.0` 表示"不覆盖大气自身 vturb"(来自 TLUSTY 的 2 km/s);正值会触发 `vtb²·1e10` 覆盖 | +| 9 | `NMU0 ANG0 IFLUX` | — | `:2301`(IFWIN≤0 时读)角度网格 | + +`SynspecInput::validate()`(`config.rs:1238+`)会拒绝触发不支持的条件 READ 的配置(如 `inmod>0 && intrpl>0` → DM 数组 READ;`nmu0<0` → 显式角度数组 READ)。 + +### 常见波长范围配置(line 6) +- 窄段验证(官方测试):`1400.0 1410.0 ...` +- 可见光:`3000.0 7000.0 ...` +- 全波段:`100.0 20000.0 10 0 0.0001 0.01`(SYNSPEC 可靠边界 100–23000 Å) + +## 输出文件(SYNSPEC 产物) + +| fort.* | 复制为 | 内容 | +|---|---|---| +| `fort.7` | `.spec` | 合成光谱(emergent flux vs λ,主产物) | +| `fort.17` | `.cont` | 连续谱 | +| `fort.12` | `.iden` | 谱线标识 | +| `fort.11` | `.lindat` | 谱线数据 | + +模板 `assets/run_synspec.sh`:切 `run/` → 复制 fort.5/fort.55/fort.8 + 建软链 → `timeout 1200 synspec_static < fort.5 > synspec.log 2> synspec.err` → 产物复制到 `outputs/{name}.spec/.cont/.iden/.lindat`。 + +## NaN 修复背景(重要) + +旧 `synspec_static` 用 `-ftoplevel-reorder` 编译,破坏了 SYNSPEC COMMON 块初始化顺序,导致 **Balmer 系限 3646 Å 之后整段 NaN**(73% 数据点失效)。修复版用 `-O3 -fno-toplevel-reorder`,全波段 0 NaN。 + +**验收 SYNSPEC 必查**:分段统计 NaN(EUV 100–912 / UV 912–3646 / 可见 3646–8200 / NIR 8200–20000),尤其 3646–8200 段(旧 bug 重灾区)。详见 `docs/synspec_nan_fix_2026_08_11.md`。 + +## SYNSPEC 校验判据 + +| 判据 | 工具 | 通过条件 | +|---|---|---| +| 无 NaN/Inf/坏行 | `grep -ciE 'nan\|inf\|\*{3,}' fullband.spec` | = 0 | +| 波长范围正确 | `head -1 / tail -1 fullband.spec` | 首/尾 λ ≈ ALAM0/ALAST | +| 通量物理 | 抽查关键 λ(Lyα 1216、Balmer 3646/6563) | 有限、非全零、形态符合 Teff | +| 谱形 | EUV 峰 → 长波单调下降 | 与 Teff 一致(热星 EUV 占优) | + +> 注:深吸收线心可能有少量微负通量(数值残差),不是 NaN/Inf,不影响"0 NaN"结论。绘图时把负通量裁成 NaN 即可。`spec_is_valid`(`conv_check.rs:290`)只判 NaN/Inf/坏行。 + +## 官方 Runtest 结构(源码侧参考) + +`tests/synspec/{hhe,bstar,kurucz,hybrid,pseudonlt,optab}/`,`R1` 调用 `RSynspec`: +``` +$TLUSTY/RSynspec hhe35nl fort.55.con data/gfATO.dat +``` +`RSynspec` 做的事:`cp $1.7 fort.8`(输入大气);`ln -s $2 fort.55`(控制卡);`ln -s $3 fort.19`(线表);`synspec.exe < $1.5 > results/$1.log`;`cp fort.7/17/12 results/$1.spec/.cont/.iden`。Kurucz 测试还演示了不用 RSynspec 直接手动链软链 + 跑二进制的写法(见 `tests/synspec/kurucz/R1` 第二段)。 diff --git a/.agents/skills/tlusty-synspec-test/references/tlusty-stages.md b/.agents/skills/tlusty-synspec-test/references/tlusty-stages.md new file mode 100644 index 0000000..f571b46 --- /dev/null +++ b/.agents/skills/tlusty-synspec-test/references/tlusty-stages.md @@ -0,0 +1,115 @@ +# TLUSTY 链阶段与输入文件详解 + +参考源码:`dcts/crates/common/src/runner.rs`(链定义)、`gen_input5.rs`(`.5` 生成)、`nst_writer.rs`(`nst` 生成)、`seed_finder.rs`(种子选择)。 + +## 两种链 + +### 冷启动链(cold-start):从零算一个网格点 + +`runner.rs:20` `default_cold_chain()`,三阶段 `lte → nc → nl`: + +| 阶段 | 含义 | `.5` 第2行 LTE/LTGRAY | ilvlin | NITER | fort.8 处理 | require_converged | +|---|---|---|---|---|---|---| +| **lte** | LTE 灰大气起步(从灰大气解析解重建) | `T T` | 0 | 0 | **删除**(ltgray=T 强制从灰大气重建) | 否 | +| **nc** | NLTE 连续性方程(无谱线) | `F F` | 0 | 10 | 复制 lte 的 fort.7 → fort.8 | 否 | +| **nl** | 完整 NLTE(含谱线) | `F F` | 100 | 100 | 复制 nc 的 fort.7 → fort.8 | **是**(不收敛则链中断) | + +物理直觉:lte 给一个解析的灰大气初值(快、稳但不真实)→ nc 松弛到 NLTE 连续(电子/原子能级 NLTE,但不解谱线辐射转移)→ nl 再加入谱线(最贵、最易发散)。每阶用上一阶收敛模型热启动,是 TLUSTY 控制 NLTE 发散的标准手法。 + +### 种子步进链(seed-stepping):从已收敛邻居热启动 + +`runner.rs:67` `default_seed_chain()`,两阶段 `seed_nc → nl`: + +| 阶段 | NITER | ICHANG | ORELAX | fort.8 | require_converged | +|---|---|---|---|---|---| +| **seed_nc** | 20 | 0 | 0.3 | **邻居收敛模型的 .7 复制成 fort.8** | 否 | +| **nl** | 100 | 0 | 0.5 | 复制 seed_nc 的 fort.7 → fort.8 | 是 | + +何时用种子链:网格上邻近点已收敛(如生产 DB 里的模型),新点与它 `|Δteff|<5000, |Δlogg|<0.01, |Δloghe|<0.01` 时(`seed_finder.rs:51` 的 exact-family 判定),热启动比冷启动稳得多。种子距离有方向性:金属贫向种子成功率 42–54%,富向仅 3–11%(`directed_cno_distance` 对富向罚 4×),`MAX_GLOBAL_SEED_DISTANCE=3.0`。 + +## `.5` 主输入文件格式(TLUSTY stdin) + +由 `gen_input5.rs:140 make_input5()` 生成。**注意**:DCTS 实际只写一个 `.5` 每阶段覆盖,阶段差异来自 `ChainStep` 而非文件名;测试里我们用 `lte.5`/`nc.5`/`nl.5` 分别保存各阶段版本。 + +| 行 | 内容 | 说明 | +|---|---|---| +| 1 | `{teff:.1} {logg:.1} ! TEFF, GRAV` | 恒星几何:有效温度、表面重力 | +| 2 | ` {lte} {ltgray} ! LTE, LTGRAY` | **行首有一个空格**(Rust 字面保留,有回归测试);lte 阶段=`T T`,nc/nl=`F F` | +| 3 | `'nst' ! name of file containing non-standard flags` | nst 文件名;生产默认字面量 `nst`(`config.rs:739`),所有阶段同名 | +| 4 | 注释分隔 | — | +| 5 | `* frequencies` | 频率网格段头 | +| 6 | `{nfread} ! NFREAD` | 频率点数,默认 2000(`config.rs:742`) | +| atoms 块 | 头 `natoms [iabset]`,每行 `mode abn modpf` | 原子种类与丰度(H/He/C/N/O…),丰度 `fmt_abn(logx)=10^logx`,`{:.4E}` | +| ions 块 | 头 + 每离子 `iat/iz/nlevs/ilast/ilvlin/nonstd typion filei`,终止行 `0 0 0 -1 0 0` | 离子能级 | +| (可选) CHANGE | `ichang<0` 时追加 | 参数修改块 | + +### ilvlin 规则(易错点) + +`gen_input5.rs:49-53`:`ilvlin = if ion.nlevs == 1 { 0 } else { chain.ilvlin }`。 + +- **裸核(nlevs==1,即只有一个能级的离子)永远 ilvlin=0**,不论阶段。 +- 非裸核:lte/nc 阶段 ilvlin=0,nl 阶段 ilvlin=100(`runner.rs:24,39,54`)。 +- 测试清单:lte/nc 的非裸核=0,nl 的非裸核=100,所有裸核=0。 + +> **从 nc.5 生成 nl.5**:用 `scripts/stage_transform.py nc.5 -o nl.5 --to nl --diff`,自动按上述规则字节级改写(非裸核 0→100、裸核保持 0、L2 改 F F)。**不要手写 awk**——awk 按空白切分会把 `'data/h1.dat'` 引号路径内的空格拆碎,实测会破坏离子行。 + +## `nst` 非标准参数文件格式 + +由 `nst_writer.rs:36 generate_nst_content()` 生成。三级覆盖优先级:`ChainStep` 阶段字段 > `global.nst` 结构化默认 > `nst_extra_keys` escape hatch(一行一个 `KEY=VALUE`)。 + +| 行 | 字段 | 说明 | +|---|---|---| +| 1 | `ND=,NLAMBD=,VTB=,ISPODF=,DDNU=,CNU1=`(必有);`CHMAX=`(若配);`ITEK=`(若配);`NITER=`(必有) | 例:`ND=50,NLAMBD=3,VTB=2.,ISPODF=1,DDNU=50.,CNU1=6.,CHMAX=0.001,NITER=10` | +| 2 | `ORELAX=`(若配) `IDLTE=`(若配) `IACC=`(若配) `ICHANG=`(若配) `IELCOR=`(必有,默认 -1) | 例:`IELCOR=-1`;种子链 nl:`ICHANG=0,ORELAX=0.5,IELCOR=-1` | +| 3+ | `NMU, ISPLIN, CNU2, NFTAIL, DFTAIL`;`FRCMAX/FRCMIN/FRLMAX/FRLMIN`(仅非零才写,零=用 TLUSTY 内部默认);`HMIX0, MLTYPE, IPTURB, IBFINT, IHECOR, IRTE, IBC, IFRYB, IOPTAB, IFMOL, IFALI, IFPOPR, JALI, TRAD, WDIL, TFLOOR, TDISK, TMOLIM` | 结构化物理参数 | +| 末 | escape hatch:一行一个 `KEY=VALUE` | 校验:key/value 不含 `,` `\n` `=` 空白 | + +### 关键参数含义 + +- **NITER**:本阶段最大迭代数。lte=0(解析解,不迭代),nc=10,nl=100;种子 seed_nc=20。 +- **CHMAX**:收敛阈值,默认 0.001。末次迭代 `max_relc < CHMAX` 才算收敛。 +- **ITEK / IACC**:迭代控制(ITEK=预条件/加速策略,IACC=累加策略)。生产默认不显式写(用 TLUSTY 内部默认)。 +- **ORELAX**:过松弛因子。种子链 seed_nc=0.3、nl=0.5;冷启动链默认不写(TLUSTY 内部决定)。 +- **ICHANG**:=0 表示禁止 TLUSTY 自动调整深度网格(种子链固定邻居网格用)。 +- **IELCOR**:能级修正开关,默认 -1。 +- **DPSILG**:**不在结构化字段里**,通过 escape hatch 注入。默认 TLUSTY DPSILG=10(允许每步 10× 跳变);生产用 `DPSILG=1.5` 钳到 ±1.5×,抑制 NLTE 雪崩(实测末 max_relc 从 4.71e6 降到 48.6)。见 `workflows/sdB_cno.yaml:116-117`。 + +### `fmt_real` 细节(字节兼容) +整数加尾点(`2.0 → "2."`),`-0.0 → "0."`。若用脚本手写 nst 要注意这点,否则与生产字节不一致。 + +## fort.7 ↔ fort.8 传递(链的核心机制) + +`runner.rs` 每阶段循环: + +1. **初始 seed**(L321-335):删 `fort.8`;若 `seed_atmos`(邻居模型路径)非空则复制成 `fort.8`。冷启动链传 `None` → 不存在 `fort.8`。 +2. **每阶段 fort.8 准备**(L379-393): + - `ltgray=="T"`(仅冷启动 lte):删 `fort.8`(强制灰大气重建) + - 否则若 `current_seed` 有值:复制 `current_seed` → `fort.8`(热启动) + - `current_seed` 初值 = `seed_atmos`,所以种子链第一阶直接用邻居模型 +3. **跑 TLUSTY**:读 `fort.8` 为输入大气,输出新大气到 `fort.7`。 +4. **fort.7 成为下阶段种子**(L518-521):`rc==0 && fort.7 存在` → 快照成 `.