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
+212
View File
@@ -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.51.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 阶段=0nl 阶段=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.51.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=124timeout)感知**:三个 `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}/` |
@@ -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 光谱产物(若适用) |
@@ -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.8lte 阶段 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 <Teff>"
@@ -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 <Teff>"
+90
View File
@@ -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}.specSYNSPEC 失败,rc=$RC"
fi
echo ""
echo "synspec.log 尾 15 行:"
tail -15 "$OUTPUTS/synspec.log" 2>/dev/null || echo "(无 log)"
@@ -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 阶段=0nl 阶段非裸核=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
```
@@ -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.51.5×Teff":**过严**,曾误判物理模型为不物理(把表层 0.78×Teff 的好模型判死)。
- 旧经验"表层应 36 万 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 个深度,每个:
取块第一行第一个 tokenparse_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):失败原因提取
测试中常用前两类(温度结构 + 收敛轨迹)即可覆盖绝大多数判断。
@@ -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` 软链 | 原子线表(全波段,1823000 Å,~238MB278 万条线) |
| `data/` | `assets/data/` 软链 | 原子数据(与 TLUSTY 共用) |
> 可见光子集线表 `assets/gfVIS99.dat`30007000 Å)用于窄段测试;全波段用 `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 可靠边界 10023000 Å)
## 输出文件(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 必查**:分段统计 NaNEUV 100912 / UV 9123646 / 可见 36468200 / NIR 820020000),尤其 36468200 段(旧 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` 第二段)。
@@ -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 实际只写一个 `<name>.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=0nl 阶段 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=10nl=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 存在` → 快照成 `<name>.<label>.7``current_seed` 更新为该路径。
5. **最终大气**L573-581):`current_seed` 复制成 `<name>.7`(规范最终模型)。
6. **交给 SYNSPEC**L694-702):最终 `<name>.7` 复制成 `fort.8`SYNSPEC 的输入大气单元)。
### test 里的 run.sh 怎么做
模板 `assets/run_cold_chain.sh` 每阶段:
```
[stage=lte] rm -f run/fort.8
cd run && timeout 1200 tlusty_static < stage.5 > fort.6 2> fort.14
cp fort.6 ../{stage}.6 ; cp fort.7 ../{stage}.7 ; cp fort.9 ../{stage}.9 ; cp fort.14 ../{stage}.14
[fort.7 存在] cp fort.7 fort.8 # 下阶段种子
```
## 官方 Runtest 结构(源码侧参考)
`tests/tlusty/{hhe,cwd,disk,bstar,optab}/` 每个目录有 `AAAREADME` + `R1` 脚本 + 参考结果 `results/``R1` 调用 `RTlusty`
```
$TLUSTY/RTlusty hhe35lt # 冷启动第一阶(无种子)
$TLUSTY/RTlusty hhe35nc hhe35lt # nc,种子=hhe35lt.7
$TLUSTY/RTlusty hhe35nl hhe35nc # nl,种子=hhe35nc.7
```
`RTlusty` 做的事(等价于我们的 run.sh 单阶段):建 `data` 软链;若第二参数存在则 `cp $2.7 fort.8`(种子);`tlusty.exe < $1.5 > $1.6``cp fort.7/9/69/14 $1.7/9/69/14`。即官方用 `{model}.{ext}` 命名各模型产物,我们用 `{stage}.{ext}` 等价(一个模型内分阶段)。
**对照方法**:官方 `results/` 存参考输出,测试跑完与 `results/` 比对(数值容差)。我们的事后审查更强:除了"有没有 STOP",还做温度结构物理判定(官方 Runtest 不做)。
+245
View File
@@ -0,0 +1,245 @@
#!/usr/bin/env python3
"""
check_fort9.py — 解析 {stage}.9 收敛轨迹,判数值收敛;检测 STOP / NaN / 雪崩。
复刻 DCTS check_fort9conv_check.rs:68)的核心逻辑。
退出码:0=收敛 /1=未收敛或异常 /2=文件缺失或无有效数据。
⚠️ **只对已完成的运行使用本脚本**:TLUSTY 是边算边写 fort.9 的(每次迭代的
深度行逐行写出),运行中实时读取,最新一拍的 max_relc 读到的是半写状态,数值
不可信(实测:运行中读到 iter6=0.477、iter9=0.00176 貌似收敛,进程退出后同一
迭代实为 1060 / 5.07e4)。等 run.sh 完全结束、进程退出后再跑本脚本。
判据:
- 每次 ITER 的 max_relc = 该迭代所有深度行 |MAXIMUM| 的最大值
- 末次迭代 max_relc < chmax(默认 0.001)且有限 = 收敛
- 单调下降=健康;骤涨几个数量级=雪崩
- fort.6stdout)里的 "STOP in SOLVE after ITER N" = 求解器发散中止
用法:
python3 check_fort9.py outputs/nl.9 --teff 60000
python3 check_fort9.py outputs/nc.9 --chmax 0.001 --stdout outputs/nc.6
"""
import argparse
import os
import re
import sys
# 复刻 conv_check.rs:44 parse_fortran_float
_NO_E_EXP_RE = re.compile(r"^([+-]?[\d.]+)([+-]\d+)$")
def parse_fortran_float(s):
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)}")
except ValueError:
pass
return None
# 复刻 conv_check.rs:107 FORT9_REiter depth 5浮点 2整数
FORT9_RE = re.compile(
r"^\s*(\d+)\s+(\d+)\s+([-+\dE.]+)\s+([-+\dE.]+)\s+([-+\dE.]+)\s+([-+\dE.]+)\s+([-+\dE.]+)\s+(\d+)\s+(\d+)\s*$"
)
# NaN 模式(conv_check.rs:18
NAN_RE = re.compile(r"(?i)(\bnan\b|\binf(?:inity)?\b|\*{3,}|[eE]\+(?:3\d{2}|[4-9]\d{2,}))")
# STOP 模式(conv_check.rs SOLVER_STOP_RE
STOP_RE = re.compile(r"STOP\s+in\s+SOLVE\s+after\s+ITER\s+(\d+)", re.IGNORECASE)
def parse_fort9(path, chmax):
"""返回 (iters, last_iter, last_max_relc, converged, n_depths_last)。
iters = [(iter_no, max_relc, n_depths), ...]"""
iters = []
cur_iter = None
cur_max = 0.0
cur_n = 0
last_iter = None
with open(path, "r", errors="replace") as f:
for line in f:
m = FORT9_RE.match(line)
if not m:
continue
try:
it = int(m.group(1))
except ValueError:
continue
maximum = parse_fortran_float(m.group(7))
if maximum is None:
continue
if cur_iter != it:
if cur_iter is not None:
iters.append((cur_iter, cur_max, cur_n))
cur_iter, cur_max, cur_n = it, 0.0, 0
cur_n += 1
am = abs(maximum)
if am > cur_max:
cur_max = am
last_iter = it
if cur_iter is not None:
iters.append((cur_iter, cur_max, cur_n))
if not iters or last_iter is None:
return [], None, float("inf"), False, 0
last_max_relc = iters[-1][1]
converged = (last_max_relc < chmax) and (last_max_relc == last_max_relc) and (abs(last_max_relc) != float("inf"))
return iters, last_iter, last_max_relc, converged, iters[-1][2]
def detect_avalanche(iters):
"""检测雪崩:相邻迭代 max_relc 涨 >1e3 倍。返回告警列表。"""
warns = []
for i in range(1, len(iters)):
prev = iters[i - 1][1]
cur = iters[i][1]
if prev > 0 and cur / prev > 1e3:
warns.append(f" iter {iters[i-1][0]}{iters[i][0]}: max_relc {prev:.3g}{cur:.3g}(涨 {cur/prev:.0g}×,雪崩)")
return warns
def scan_stop(stdout_path):
"""扫 fort.6 找 STOP in SOLVE。返回 iter 号或 None。"""
if not stdout_path or not os.path.exists(stdout_path):
return None
last = None
with open(stdout_path, "r", errors="replace") as f:
for line in f:
m = STOP_RE.search(line)
if m:
last = int(m.group(1))
return last
def scan_nan(path):
if not path or not os.path.exists(path):
return 0
n = 0
with open(path, "r", errors="replace") as f:
for line in f:
if NAN_RE.search(line):
n += 1
return n
def main():
ap = argparse.ArgumentParser(description="fort.9 收敛轨迹分析")
ap.add_argument("fort9", help="{stage}.9 路径")
ap.add_argument("--teff", type=float, default=None, help="Teff(上下文,非必填)")
ap.add_argument("--chmax", type=float, default=0.001, help="收敛阈值(默认 0.001")
ap.add_argument("--stdout", default=None, help="对应 {stage}.6 路径,用于 STOP 检测(默认自动推断)")
ap.add_argument("--expected-niter", type=int, default=None,
help="配置的 NITER(来自 nst)。若末次 iter 远小于此且未收敛,提示疑似被 timeout 截断")
args = ap.parse_args()
if not os.path.exists(args.fort9):
print(f"⚠ 文件不存在: {args.fort9}")
return 2
# 自动推断 stdout(同目录同名 .6):把末尾 .9 换成 .6
stdout = args.stdout
if stdout is None:
if args.fort9.endswith(".9"):
cand = args.fort9[:-2] + ".6"
else:
cand = args.fort9 + "6"
stdout = cand if os.path.exists(cand) else None
if args.chmax <= 0:
print(f"❌ 非法 chmax={args.chmax}(须为正有限数)")
return 1
iters, last_iter, last_max_relc, converged, n_depths = parse_fort9(args.fort9, args.chmax)
if not iters:
print(f"{args.fort9} 无有效迭代数据")
return 2
stop_iter = scan_stop(stdout)
nan9 = scan_nan(args.fort9)
# 截断检测:① 附近有 .TIMEOUT 标记文件(run.sh 在 rc=124 时写到 outputs/{stage}.TIMEOUT);
# ② 末次 iter 远小于 --expected-niter 且未收敛(疑似 timeout)。
import glob
timeout_marker = None
d = os.path.dirname(os.path.abspath(args.fort9))
for pat in (os.path.join(d, "*.TIMEOUT"),
os.path.join(d, "outputs", "*.TIMEOUT"),
os.path.join(d, "..", "outputs", "*.TIMEOUT")):
hits = glob.glob(pat)
if hits:
timeout_marker = ",".join(os.path.basename(h) for h in hits)
break
likely_truncated = (timeout_marker is not None or
(args.expected_niter and last_iter < args.expected_niter - 1 and not converged and stop_iter is None))
print(f"=== 收敛轨迹: {args.fort9} (chmax={args.chmax}" +
(f", teff={args.teff}" if args.teff else "") + ") ===")
print(f" 迭代次数: {len(iters)}(末次 ITER {last_iter}")
print(f" 末 max_relc: {last_max_relc:.4g}{'✅ < chmax 收敛' if converged else '❌ ≥ chmax 未收敛'}")
print(f" 末次深度数: {n_depths}")
if stop_iter is not None:
print(f" STOP: ❌ STOP in SOLVE after ITER {stop_iter}(求解器发散中止)")
else:
print(f" STOP: 无")
print(f" fort.9 NaN行: {nan9}")
if likely_truncated:
detail = f"(标记文件: {timeout_marker}" if timeout_marker else (
f"(仅 iter {last_iter}/{args.expected_niter},疑似 timeout 截断)" if args.expected_niter else "")
print(f" 截断: ⚠ 疑似被 timeout 杀死 {detail}——轨迹不完整,结论需谨慎")
# 轨迹(最多显示首/末若干 + 全部若 ≤15)
print(" 轨迹 (iter: max_relc):")
show = iters if len(iters) <= 20 else iters[:8] + [None] + iters[-5:]
for it in show:
if it is None:
print(" ...")
else:
print(f" iter {it[0]:>3}: {it[1]:.4g} ({it[2]} 深度)")
warns = detect_avalanche(iters)
if warns:
print(" ⚠ 雪崩告警:")
for w in warns:
print(w)
# 伪收敛启发式:max_relc 恰为 0 或极小且迭代很少 → 极可能是"在 NaN 模型上迭代"。
# 真实收敛要多次迭代、max_relc 趋近但极少精确到 0。
pseudo_warn = False
if last_max_relc == 0.0 or (len(iters) <= 2 and last_max_relc < 1e-10):
pseudo_warn = True
# 结论
real_converged = converged and stop_iter is None and nan9 == 0
print()
if real_converged:
print(f"✅ 数值收敛(末 max_relc {last_max_relc:.4g} < {args.chmax},无 STOP,无 NaN")
if pseudo_warn:
print(" ⚠⚠ 伪收敛嫌疑:max_relc≈0 且迭代极少,极可能是『在 NaN 模型上迭代』(NaN 上相对变化=0)。")
print(" 务必运行 check_temperature.py 查温度结构——若 .7 全 NaN,则此『收敛』无效。")
else:
print(" 注意:数值收敛 ≠ 物理收敛。请再用 check_temperature.py 查温度结构(防伪收敛)。")
return 0
print("❌ 未数值收敛:" if not real_converged else "")
reasons = []
if not converged:
reasons.append(f"末 max_relc {last_max_relc:.4g} ≥ chmax {args.chmax}")
if stop_iter is not None:
reasons.append(f"STOP in SOLVE after ITER {stop_iter}")
if nan9 > 0:
reasons.append(f"fort.9 有 {nan9} 行 NaN/坏值")
if warns:
reasons.append("迭代轨迹有雪崩")
for r in reasons:
print(f" - {r}")
return 1
if __name__ == "__main__":
sys.exit(main())
+161
View File
@@ -0,0 +1,161 @@
#!/usr/bin/env python3
"""
check_inputs.py — TLUSTY 测试执行前输入校验(冷启动链 / 种子链)
复刻 docs/testing_workflow_2026_08_11.md §五 与 references/tlusty-stages.md 的"关键不变量"
退出码:0=全过,1=有违规。
用法:
python3 check_inputs.py <inputs_dir> --chain cold --teff 60000
python3 check_inputs.py <inputs_dir> --chain seed --teff 55000
"""
import argparse
import os
import re
import sys
# NaN 模式(与 conv_check.rs:18 NAN_RE_PATTERN 一致)
NAN_RE = re.compile(r"(?i)(\bnan\b|\binf(?:inity)?\b|\*{3,}|[eE]\+(?:3\d{2}|[4-9]\d{2,}))")
def read_lines(path):
with open(path, "r", errors="replace") as f:
return f.read().splitlines()
def check_dot5_l2(path, expect_lte, expect_ltgray):
"""校验 .5 第2行 LTE/LTGRAY 两个标志。行首允许空格。"""
lines = read_lines(path)
if len(lines) < 2:
return f"{path}: 文件不足2行"
l2 = lines[1].split("!")[0].split()
if len(l2) < 2:
return f"{path}: L2 字段不足2个: {lines[1]!r}"
lte, ltgray = l2[0].upper(), l2[1].upper()
if lte != expect_lte or ltgray != expect_ltgray:
return f"{path}: L2 期望 {expect_lte} {expect_ltgray}, 实际 {lte} {ltgray} ({lines[1].strip()!r})"
return None
def check_dot5_nst_name(path):
"""校验 .5 第3行 'nst' 文件名。
约定:生产/测试默认 nst 名都是字面 'nst'run.sh 会在执行时把 {stage}.nst 复制成 nst。
所以 'nst' 不算违规(运行时会有);只有引用了非标准名且该名在 inputs/ 找不到时才报错。"""
lines = read_lines(path)
if len(lines) < 3:
return None
l3 = lines[2]
m = re.search(r"'([^']+)'", l3)
if not m:
return None
nst_name = m.group(1)
if nst_name == "nst":
return None # 标准名,run.sh 运行时把 {stage}.nst → nst,无需 inputs/ 里有
base = os.path.dirname(path)
candidates = [os.path.join(base, nst_name), os.path.join(base, "..", nst_name)]
if not any(os.path.exists(c) for c in candidates):
return f"{path}: L3 指定 nst 文件 {nst_name!r} 未找到(非标准名,run.sh 不会自动改名)"
return None
def check_nst_niter(path):
"""校验 nst 第1行含 NITER=。"""
lines = read_lines(path)
if not lines:
return f"{path}: 空文件"
if "NITER" not in lines[0].upper():
return f"{path}: L1 缺 NITER=: {lines[0][:80]!r}"
return None
def check_file_exists(path, what):
return None if os.path.exists(path) else f"缺输入: {what} ({path})"
def check_no_nan(path, what):
if not os.path.exists(path):
return f"缺输入: {what} ({path})"
n = 0
with open(path, "r", errors="replace") as f:
for line in f:
if NAN_RE.search(line):
n += 1
return None if n == 0 else f"{what}{n} 行 NaN/Inf/坏值 ({path})"
def main():
ap = argparse.ArgumentParser(description="TLUSTY 测试执行前输入校验")
ap.add_argument("inputs_dir", help="inputs/ 目录路径")
ap.add_argument("--chain", choices=["cold", "seed"], required=True)
ap.add_argument("--teff", type=float, default=None, help="TeffK),用于上下文,非必填")
args = ap.parse_args()
d = args.inputs_dir
violations = []
def add(v):
if v:
violations.append(v)
if args.chain == "cold":
# 必须有 lte/nc/nl 的 .5 与 .nst
for f in ["lte.5", "nc.5", "nl.5", "lte.nst", "nc.nst", "nl.nst"]:
add(check_file_exists(os.path.join(d, f), f))
# L2: lte=T T, nc/nl=F F
if os.path.exists(os.path.join(d, "lte.5")):
add(check_dot5_l2(os.path.join(d, "lte.5"), "T", "T"))
for s in ("nc", "nl"):
p = os.path.join(d, f"{s}.5")
if os.path.exists(p):
add(check_dot5_l2(p, "F", "F"))
# nst 名一致
for s in ("lte", "nc", "nl"):
p = os.path.join(d, f"{s}.5")
if os.path.exists(p):
add(check_dot5_nst_name(p))
# nst L1 含 NITER
for s in ("lte", "nc", "nl"):
p = os.path.join(d, f"{s}.nst")
if os.path.exists(p):
add(check_nst_niter(p))
# 冷启动链不应有 fort.8
if os.path.exists(os.path.join(d, "fort.8")):
violations.append("冷启动链不应有 inputs/fort.8lte 阶段 ltgray=T 会删它;若有种子请用 --chain seed")
else: # seed
for f in ["seed_nc.5", "seed_nc.nst", "fort.8"]:
add(check_file_exists(os.path.join(d, f), f))
add(check_no_nan(os.path.join(d, "fort.8"), "fort.8 种子"))
if os.path.exists(os.path.join(d, "seed_nc.5")):
add(check_dot5_l2(os.path.join(d, "seed_nc.5"), "F", "F"))
add(check_dot5_nst_name(os.path.join(d, "seed_nc.5")))
if os.path.exists(os.path.join(d, "seed_nc.nst")):
add(check_nst_niter(os.path.join(d, "seed_nc.nst")))
# data 软链:run.sh / new_test.sh 会在执行时创建,inputs/ 阶段可能还没有。
# 这里只做提示(note),不算硬违规。
notes = []
data_link = None
for cand in [os.path.join(d, "..", "run", "data"), os.path.join(d, "data")]:
if os.path.islink(cand) or os.path.isdir(cand):
data_link = cand
break
if data_link is None:
notes.append("data 软链(run/data)暂不存在——run.sh 执行时会自动创建并指向 assets/data/,可忽略")
# 汇报
print(f"=== 输入校验: {d} (chain={args.chain}" +
(f", teff={args.teff}" if args.teff else "") + ") ===")
for n in notes:
print(f" {n}")
if not violations:
print("✅ 全部通过")
return 0
for v in violations:
print(f"{v}")
print(f"\n{len(violations)} 项违规。请修正后再执行(见 references/tlusty-stages.md)。")
return 1
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,152 @@
#!/usr/bin/env python3
"""
check_temperature.py — 对 fort.7/{stage}.7 做温度结构物理判定。
精确复刻 DCTS 权威判据 check_temperature_structureconv_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}×TeffEddington 参考 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())
+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())
+121
View File
@@ -0,0 +1,121 @@
#!/usr/bin/env bash
# ============================================================
# new_test.sh — 脚手架:在 dcts/test/ 下创建规范的四级测试目录
#
# 用法:
# bash new_test.sh <test_id> <type> <grid> <content> [--chain cold|seed|synspec]
#
# 示例:
# bash new_test.sh 20260813_my_div conv t60000_g5.0_he-2_cold baseline --chain cold
# bash new_test.sh 20260813_seed conv t55000_g5.0_he-2_seed doubleoff --chain seed
# bash new_test.sh 20260813_spec synspec t50000_g5.0_cno gfato_100_20000 --chain synspec
#
# 创建:
# dcts/test/<test_id>/<type>/<grid>/<content>/{inputs,scripts,run,outputs}
# scripts/run.sh <- 从 assets/run_{chain}_chain.sh 拷贝
# README.md <- 骨架
# ============================================================
set -euo pipefail
# --- 自定位 skill 目录(脚本在 scripts/ 下,skill 根是父目录)---
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SKILL_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
ASSETS="$SKILL_DIR/assets"
# --- 找 dcts 根(含 assets/tlusty_static 的目录)---
# 优先用环境变量;否则从 skill 目录向上找第一个含 assets/tlusty_static 的祖先
DCTS_ROOT="${DCTS_ROOT:-}"
if [ -z "$DCTS_ROOT" ]; then
cur="$SCRIPT_DIR"
while [ "$cur" != "/" ]; do
if [ -f "$cur/assets/tlusty_static" ]; then DCTS_ROOT="$cur"; break; fi
cur="$(dirname "$cur")"
done
fi
TEST_ROOT="$DCTS_ROOT/test"
if [ -z "$DCTS_ROOT" ] || [ ! -f "$DCTS_ROOT/assets/tlusty_static" ]; then
echo "❌ 未找到 dcts 根(含 assets/tlusty_static 的目录)。用 DCTS_ROOT=... 环境变量指定。" >&2
exit 1
fi
# --- 解析参数 ---
if [ $# -lt 4 ]; then
sed -n '3,20p' "$0" | sed 's/^# \{0,1\}//' >&2
exit 1
fi
TEST_ID="$1"; TYPE="$2"; GRID="$3"; CONTENT="$4"; shift 4
CHAIN="cold"
while [ $# -gt 0 ]; do
case "$1" in
--chain) CHAIN="$2"; shift 2;;
*) echo "未知参数: $1" >&2; exit 1;;
esac
done
case "$CHAIN" in
cold) RUN_TEMPLATE="$ASSETS/run_cold_chain.sh"; STAGES="lte → nc → nl";;
seed) RUN_TEMPLATE="$ASSETS/run_seed_chain.sh"; STAGES="seed_nc → nl";;
synspec) RUN_TEMPLATE="$ASSETS/run_synspec.sh"; STAGES="SYNSPEC";;
*) echo "❌ --chain 必须是 cold|seed|synspec,收到: $CHAIN" >&2; exit 1;;
esac
# --- 创建四级目录 ---
TARGET="$TEST_ROOT/$TEST_ID/$TYPE/$GRID/$CONTENT"
if [ -e "$TARGET" ]; then
echo "❌ 目录已存在: $TARGET" >&2
exit 1
fi
mkdir -p "$TARGET/inputs" "$TARGET/scripts" "$TARGET/run" "$TARGET/outputs"
# --- 拷 run.sh 模板 ---
cp "$RUN_TEMPLATE" "$TARGET/scripts/run.sh"
chmod +x "$TARGET/scripts/run.sh"
# --- 建 data 软链到 run/TLUSTY/SYNSPEC 都要)---
ln -sfn "$DCTS_ROOT/assets/data" "$TARGET/run/data"
# --- README 骨架 ---
cat > "$TARGET/README.md" <<EOF
# $TEST_ID / $TYPE / $GRID / $CONTENT
## 目的
TODO: 一句话说清这次测试想验证什么。
## 配置
- 二进制: $DCTS_ROOT/assets/$([ "$CHAIN" = synspec ] && echo synspec || echo tlusty)_static
- 网格: $GRID
- 链: $STAGES
- 变体: TODO(与 baseline 不同的关键参数,如 ITEK/IACC/DPSILG/ORELAX/NITER
## 输入
TODO: inputs/ 生成方式(代码生成 / 模板修改,留 diff);关键参数。
- [ ] 见 references/directory-convention.md 的执行前检查清单
## 执行
- 命令: cd "$TARGET" && bash scripts/run.sh
- 各阶段: TODO
## 结果
TODO: 见 outputs/RESULT.md(用 assets/result_template.md
## 审查结论
- 数值: TODOmax_relc 是否达标;STOP/雪崩?)
- 物理: TODO(温度结构三项:表层<3×Teff、各深度∈[10,1e8]、无 NaN
- 结论: TODO
EOF
echo "✅ 已创建测试目录: $TARGET"
echo " 链类型: $CHAIN ($STAGES)"
echo ""
echo "下一步:"
echo " 1. 把输入文件放进 $TARGET/inputs/"
echo " 2. 执行前校验(TLUSTY 链):"
echo " python3 $SKILL_DIR/scripts/check_inputs.py $TARGET/inputs --chain $CHAIN --teff <Teff>"
echo " 3. 跑: cd $TARGET && bash scripts/run.sh"
echo " 4. 事后审查:"
if [ "$CHAIN" != "synspec" ]; then
echo " python3 $SKILL_DIR/scripts/check_fort9.py $TARGET/outputs/nl.9 --teff <Teff>"
echo " python3 $SKILL_DIR/scripts/check_temperature.py $TARGET/outputs/nl.7 --teff <Teff>"
else
echo " grep -ciE 'nan|inf|\\*{3,}' $TARGET/outputs/*.spec # 应为 0"
fi
@@ -0,0 +1,120 @@
#!/usr/bin/env python3
"""
stage_transform.py — 在冷启动链各阶段的 .5 输入文件之间做"阶段变换"
主要解决 nc.5 → nl.5 的 ilvlin 改写(这是手写最容易踩坑的一步:awk 会吞掉引号
里的空格、朴素正则会吞掉换行)。本脚本逐行**字节级**保留原格式,只改 ion 行的
ilvlin 列。
规则(见 references/tlusty-stages.md "ilvlin 规则":
- nc 阶段 (ilvlin=0):所有离子 ilvlin=0
- nl 阶段 (ilvlin=100):非裸核 (nlevs>1) → 100;裸核 (nlevs==1) 永远 0
- lte 阶段:同 ncilvlin=0),但 .5 第2行 LTE/LTGRAY = T T(本工具也支持改 L2)
ion 行识别:行首空白 + 6 个整数字段 (iat iz nlevs ilast ilvlin nonstd) + 字符串。
第 3 个字段 = nlevs,第 5 个 = ilvlin。
用法:
# nc.5 → nl.5(非裸核 ilvlin 0→100
python3 stage_transform.py nc.5 -o nl.5 --to nl
# nl.5 → nc.5(所有 ilvlin→0+ 第2行改 F F
python3 stage_transform.py nl.5 -o nc.5 --to nc
# 仅打印 diff,不写文件
python3 stage_transform.py nc.5 --to nl --diff
"""
import argparse
import re
import sys
# ion data 行:行首空白 + iat iz nlevs ilast ilvlin nonstd + 后续字符串
# 用分组保留各组间空白,只重写 ilvlin(第5个整数),字节级安全。
ION_RE = re.compile(
r"^(?P<lead>\s+)(?P<iat>\d+)(?P<s1>\s+)(?P<iz>\d+)(?P<s2>\s+)"
r"(?P<nlevs>\d+)(?P<s3>\s+)(?P<ilast>\d+)(?P<s4>\s+)"
r"(?P<ilvlin>\d+)(?P<rest>\s+\d+\s+.*)$"
)
# .5 第2行 LTE/LTGRAY(行首允许空格)
L2_RE = re.compile(r"^(\s*)(\S+)(\s+)(\S+)(\s*.*)$")
def transform_ions(text, target_ilvlin_for_nonbare):
"""改写 ion 行的 ilvlin。target_ilvlin_for_nonbare: nl=100, nc/lte=0。返回新文本。"""
out = []
changed = 0
bare_kept = 0
for line in text.splitlines(keepends=True):
m = ION_RE.match(line.rstrip("\n"))
if m:
nlevs = int(m.group("nlevs"))
if nlevs == 1:
new_ilvlin = "0" # 裸核永远 0
bare_kept += 1
else:
new_ilvlin = str(target_ilvlin_for_nonbare)
if m.group("ilvlin") != new_ilvlin:
changed += 1
rebuilt = (m.group("lead") + m.group("iat") + m.group("s1") + m.group("iz") +
m.group("s2") + m.group("nlevs") + m.group("s3") + m.group("ilast") +
m.group("s4") + new_ilvlin + m.group("rest") + "\n")
out.append(rebuilt)
else:
out.append(line)
return "".join(out), changed, bare_kept
def transform_l2(text, lte_flag, ltgray_flag):
"""改写 .5 第2行的 LTE/LTGRAY 两个标志。"""
lines = text.splitlines(keepends=True)
if len(lines) < 2:
return text, False
m = L2_RE.match(lines[1].rstrip("\n"))
if not m or not m.group(2).upper() in ("T", "F"):
return text, False
rebuilt = (m.group(1) + lte_flag + m.group(3) + ltgray_flag + m.group(5) + "\n")
if rebuilt != lines[1]:
lines[1] = rebuilt
return "".join(lines), True
return text, False
def main():
ap = argparse.ArgumentParser(description="冷启动链 .5 阶段变换(ilvlin / L2")
ap.add_argument("input", help="输入 .5 文件(如 nc.5")
ap.add_argument("-o", "--output", help="输出文件(省略则只打印摘要/diff")
ap.add_argument("--to", choices=["nl", "nc", "lte"], required=True,
help="目标阶段:nl=非裸核 ilvlin→100nc/lte=所有 ilvlin→0")
ap.add_argument("--diff", action="store_true", help="打印改动行的 diff(需系统带 diff)")
args = ap.parse_args()
with open(args.input, "r", errors="replace") as f:
text = f.read()
target = 100 if args.to == "nl" else 0
new_text, ion_changed, bare = transform_ions(text, target)
l2_changed = False
if args.to == "lte":
new_text, l2_changed = transform_l2(new_text, "T", "T")
elif args.to in ("nc", "nl"):
new_text, l2_changed = transform_l2(new_text, "F", "F")
print(f"=== stage_transform: {args.input} → 阶段 {args.to} ===")
print(f" ion 行 ilvlin 改写: {ion_changed} 行(裸核 nlevs==1 保持 0: {bare} 行)")
print(f" 第2行 LTE/LTGRAY 改写: {'' if l2_changed else '否(已是目标值)'}")
if args.diff:
import difflib
for line in difflib.unified_diff(text.splitlines(), new_text.splitlines(),
fromfile=args.input, tofile=f"({args.to})", lineterm=""):
print(" " + line)
if args.output:
with open(args.output, "w") as f:
f.write(new_text)
print(f"✅ 已写出 {args.output}")
else:
print("(未指定 -o,仅打印摘要。加 --diff 看改动详情。)")
return 0
if __name__ == "__main__":
sys.exit(main())