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:
@@ -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}/` |
|
||||||
@@ -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.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 <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>"
|
||||||
@@ -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)"
|
||||||
@@ -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
|
||||||
|
```
|
||||||
@@ -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):失败原因提取
|
||||||
|
|
||||||
|
测试中常用前两类(温度结构 + 收敛轨迹)即可覆盖绝大多数判断。
|
||||||
@@ -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` 第二段)。
|
||||||
@@ -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=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 存在` → 快照成 `<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
@@ -0,0 +1,245 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
check_fort9.py — 解析 {stage}.9 收敛轨迹,判数值收敛;检测 STOP / NaN / 雪崩。
|
||||||
|
|
||||||
|
复刻 DCTS check_fort9(conv_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.6(stdout)里的 "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_RE:iter 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
@@ -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="Teff(K),用于上下文,非必填")
|
||||||
|
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.8(lte 阶段 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_structure(conv_check.rs:463)。
|
||||||
|
退出码:0=物理(三项全过)/1=不物理 /2=文件缺失或无法解析(跳过,不判失败)。
|
||||||
|
|
||||||
|
判据(valid = 三项全过):
|
||||||
|
1. 表层 T < max_factor × Teff (默认 max_factor=3.0)
|
||||||
|
2. 每个深度 T ∈ [temp_floor, temp_ceiling] (默认 [10, 1e8] K)
|
||||||
|
3. 无 NaN/Inf
|
||||||
|
|
||||||
|
用法:
|
||||||
|
python3 check_temperature.py outputs/nl.7 --teff 60000
|
||||||
|
python3 check_temperature.py outputs/nc.7 --teff 55000 --max-factor 3.0
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import math
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# 复刻 conv_check.rs:44 parse_fortran_float
|
||||||
|
# Fortran 指数≥100 时挤掉 E,如 -1.35E+118 → -1.35+118
|
||||||
|
_NO_E_EXP_RE = re.compile(r"^([+-]?[\d.]+)([+-]\d+)$")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_fortran_float(s):
|
||||||
|
"""返回 float(可能为 inf/-inf);无法解析返回 None。"""
|
||||||
|
s = s.strip()
|
||||||
|
try:
|
||||||
|
return float(s)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
m = _NO_E_EXP_RE.match(s)
|
||||||
|
if m:
|
||||||
|
try:
|
||||||
|
return float(f"{m.group(1)}E{m.group(2)}") # 含 inf/-inf(溢出)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def check_temperature_structure(path, teff, max_factor=3.0, temp_floor=10.0, temp_ceiling=1e8):
|
||||||
|
"""复刻 conv_check.rs:463。返回 (temps, violations) 或 (None, None)=无法解析。"""
|
||||||
|
try:
|
||||||
|
with open(path, "r", errors="replace") as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
except OSError:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
if not lines:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
# 1. 第1行 → nd, numpar
|
||||||
|
tokens = lines[0].split()
|
||||||
|
if len(tokens) < 2:
|
||||||
|
return None, None
|
||||||
|
try:
|
||||||
|
nd = int(float(tokens[0]))
|
||||||
|
numpar = int(float(tokens[1]))
|
||||||
|
except ValueError:
|
||||||
|
return None, None
|
||||||
|
if nd <= 0 or numpar <= 0:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
# 2. 行数计算
|
||||||
|
dm_lines = (nd + 5) // 6 # FORMAT 502: 6/行
|
||||||
|
block_lines = (numpar + 4) // 5 # FORMAT 503: 5/行
|
||||||
|
|
||||||
|
# 3. 跳过 DM 列阵
|
||||||
|
idx = 1 # 第1行已读
|
||||||
|
for _ in range(dm_lines):
|
||||||
|
if idx >= len(lines):
|
||||||
|
return None, None # 截断 → 跳过
|
||||||
|
idx += 1
|
||||||
|
|
||||||
|
# 4. nd 个深度块,每块第一行第一token = TEMP
|
||||||
|
temps = []
|
||||||
|
for _ in range(nd):
|
||||||
|
if idx >= len(lines):
|
||||||
|
break # 截断:已解析的深度保留
|
||||||
|
first_line_tokens = lines[idx].split()
|
||||||
|
if not first_line_tokens:
|
||||||
|
idx += block_lines
|
||||||
|
continue
|
||||||
|
t = parse_fortran_float(first_line_tokens[0])
|
||||||
|
temps.append(t if t is not None else float("nan"))
|
||||||
|
idx += block_lines # 跳到下一块(含本块剩余 block_lines-1 行)
|
||||||
|
|
||||||
|
if not temps:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
# 5. 三项判据
|
||||||
|
violations = []
|
||||||
|
surface_ratio = temps[0] / teff if teff > 0 else float("inf")
|
||||||
|
if surface_ratio > max_factor:
|
||||||
|
violations.append(
|
||||||
|
f"表层 T={temps[0]:.4g} > {max_factor}×Teff({teff}),ratio={surface_ratio:.4g}"
|
||||||
|
)
|
||||||
|
for i, t in enumerate(temps):
|
||||||
|
if not math.isfinite(t):
|
||||||
|
violations.append(f"深度 {i+1} T={t}(非有限,NaN/Inf)")
|
||||||
|
break
|
||||||
|
if t < temp_floor or t > temp_ceiling:
|
||||||
|
violations.append(
|
||||||
|
f"深度 {i+1} T={t:.4g} 越界 [{temp_floor}, {temp_ceiling}]"
|
||||||
|
)
|
||||||
|
break
|
||||||
|
return temps, violations
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser(description="fort.7 温度结构物理判定(DCTS 权威判据)")
|
||||||
|
ap.add_argument("fort7", help="fort.7 或 {stage}.7 路径")
|
||||||
|
ap.add_argument("--teff", type=float, required=True, help="Teff (K)")
|
||||||
|
ap.add_argument("--max-factor", type=float, default=3.0, help="表层/Teff 上限(默认 3.0)")
|
||||||
|
ap.add_argument("--floor", type=float, default=10.0, dest="temp_floor", help="温度下限 K(默认 10)")
|
||||||
|
ap.add_argument("--ceiling", type=float, default=1e8, dest="temp_ceiling", help="温度上限 K(默认 1e8)")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
import os
|
||||||
|
if not os.path.exists(args.fort7):
|
||||||
|
print(f"⚠ 文件不存在: {args.fort7}(跳过,不判失败)")
|
||||||
|
return 2
|
||||||
|
|
||||||
|
temps, violations = check_temperature_structure(
|
||||||
|
args.fort7, args.teff, args.max_factor, args.temp_floor, args.temp_ceiling
|
||||||
|
)
|
||||||
|
if temps is None:
|
||||||
|
print(f"⚠ 无法解析 {args.fort7}(缺失/截断/格式错)—— 跳过,不判失败")
|
||||||
|
return 2
|
||||||
|
|
||||||
|
surface_ratio = temps[0] / args.teff if args.teff > 0 else float("inf")
|
||||||
|
print(f"=== 温度结构判定: {args.fort7} (Teff={args.teff}) ===")
|
||||||
|
print(f" 深度数: {len(temps)}")
|
||||||
|
print(f" 表层 T: {temps[0]:.4g} K ({surface_ratio:.4g}×Teff,Eddington 参考 0.84)")
|
||||||
|
print(f" 底层 T: {temps[-1]:.4g} K")
|
||||||
|
nan_count = sum(1 for t in temps if not math.isfinite(t))
|
||||||
|
print(f" NaN/Inf 深度: {nan_count}")
|
||||||
|
|
||||||
|
if not violations:
|
||||||
|
print(f"✅ 物理(三项全过:表层<{args.max_factor}×Teff、各深度∈[{args.temp_floor},{args.temp_ceiling}]、无 NaN)")
|
||||||
|
return 0
|
||||||
|
print("❌ 不物理 —— 违规:")
|
||||||
|
for v in violations:
|
||||||
|
print(f" - {v}")
|
||||||
|
print("\n参考 references/physics-checks.md 的判据说明与修正历史。")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
+132
@@ -0,0 +1,132 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
diff_configs.py — A/B 测试配置对照工具。
|
||||||
|
|
||||||
|
给定 2 个或多个 content 目录(或 inputs 目录),解析各自的 nst(L1/L2 的 KEY=VAL)
|
||||||
|
与 .5 关键字段(L1 TEFF/GRAV、L2 LTE/LTGRAY、L3 nst 名),输出并排对照表,高亮差异。
|
||||||
|
用于 A/B 测试时快速确认"只有想改的参数变了,其余一致"。
|
||||||
|
|
||||||
|
用法:
|
||||||
|
python3 diff_configs.py dirA/inputs dirB/inputs dirC/inputs
|
||||||
|
python3 diff_configs.py dirA dirB --teff 50000 # 传 content 目录也行(自动找 inputs/)
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# nst 一行里逗号分隔的 KEY=VAL(L1 与 L2 都是这种格式)
|
||||||
|
KV_RE = re.compile(r"(\w+)=([^,]+)")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_nst(path):
|
||||||
|
"""返回 nst 的 {KEY: VAL} 字典(合并 L1+L2+escape-hatch 行)。"""
|
||||||
|
kv = {}
|
||||||
|
if not os.path.exists(path):
|
||||||
|
return kv
|
||||||
|
for line in open(path, "r", errors="replace"):
|
||||||
|
for m in KV_RE.finditer(line):
|
||||||
|
kv[m.group(1).upper()] = m.group(2).strip()
|
||||||
|
return kv
|
||||||
|
|
||||||
|
|
||||||
|
def parse_dot5(path):
|
||||||
|
"""返回 .5 关键字段。"""
|
||||||
|
info = {}
|
||||||
|
if not os.path.exists(path):
|
||||||
|
return info
|
||||||
|
lines = open(path, "r", errors="replace").read().splitlines()
|
||||||
|
if len(lines) >= 1:
|
||||||
|
t = lines[0].split("!")[0].split()
|
||||||
|
info["TEFF"] = t[0] if len(t) > 0 else "?"
|
||||||
|
info["GRAV"] = t[1] if len(t) > 1 else "?"
|
||||||
|
if len(lines) >= 2:
|
||||||
|
t = lines[1].split("!")[0].split()
|
||||||
|
info["LTE/LTGRAY"] = f"{t[0]}/{t[1]}" if len(t) >= 2 else "?"
|
||||||
|
if len(lines) >= 3:
|
||||||
|
m = re.search(r"'([^']+)'", lines[2])
|
||||||
|
info["nst名"] = m.group(1) if m else "?"
|
||||||
|
return info
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_inputs(d):
|
||||||
|
if os.path.basename(d) == "inputs" and os.path.isdir(d):
|
||||||
|
return d
|
||||||
|
cand = os.path.join(d, "inputs")
|
||||||
|
return cand if os.path.isdir(cand) else d
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser(description="A/B 配置对照(nst 关键字段 + .5 关键行)")
|
||||||
|
ap.add_argument("dirs", nargs="+", help="inputs/ 或 content 目录")
|
||||||
|
ap.add_argument("--teff", default=None, help="仅用于显示标题")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
dirs = [resolve_inputs(d) for d in args.dirs]
|
||||||
|
names = [os.path.basename(os.path.dirname(d)) if os.path.basename(d) == "inputs"
|
||||||
|
else os.path.basename(d) for d in dirs]
|
||||||
|
|
||||||
|
# 收集所有出现过的 key(nst + .5 字段),分两组
|
||||||
|
nst_keys_order = ["ND", "NLAMBD", "VTB", "ISPODF", "DDNU", "CNU1", "CHMAX", "ITEK", "NITER",
|
||||||
|
"ORELAX", "IDLTE", "IACC", "ICHANG", "IELCOR", "DPSILG"]
|
||||||
|
dot5_keys = ["TEFF", "GRAV", "LTE/LTGRAY", "nst名"]
|
||||||
|
|
||||||
|
nst_maps = []
|
||||||
|
dot5_maps = []
|
||||||
|
for d in dirs:
|
||||||
|
# nst: 各阶段都有,优先 nc.nst(A/B 通常比 nc/nl);也读 lte/nl
|
||||||
|
nst_path = None
|
||||||
|
for stage in ("nc", "nl", "lte", "seed_nc"):
|
||||||
|
p = os.path.join(d, f"{stage}.nst")
|
||||||
|
if os.path.exists(p):
|
||||||
|
nst_path = p
|
||||||
|
break
|
||||||
|
nst_maps.append((parse_nst(nst_path) if nst_path else {}, nst_path))
|
||||||
|
# .5:优先 nc.5
|
||||||
|
dot5_path = None
|
||||||
|
for stage in ("nc", "nl", "lte", "seed_nc"):
|
||||||
|
p = os.path.join(d, f"{stage}.5")
|
||||||
|
if os.path.exists(p):
|
||||||
|
dot5_path = p
|
||||||
|
break
|
||||||
|
dot5_maps.append((parse_dot5(dot5_path) if dot5_path else {}, dot5_path))
|
||||||
|
|
||||||
|
# 收集所有 nst key(含非标准的)
|
||||||
|
all_nst_keys = list(nst_keys_order)
|
||||||
|
seen = set(k.upper() for k in all_nst_keys)
|
||||||
|
for nm, _ in nst_maps:
|
||||||
|
for k in nm:
|
||||||
|
if k not in seen:
|
||||||
|
all_nst_keys.append(k)
|
||||||
|
seen.add(k)
|
||||||
|
|
||||||
|
def print_table(title, keys, maps, getter):
|
||||||
|
print(f"\n=== {title} ===")
|
||||||
|
hdr = " 字段".ljust(16) + "".join(n.ljust(22) for n in names)
|
||||||
|
print(hdr)
|
||||||
|
print(" " + "-" * (len(hdr) - 2))
|
||||||
|
for k in keys:
|
||||||
|
vals = [getter(nm, k) for nm, _ in maps]
|
||||||
|
# 高亮:若不全相同,标记 *
|
||||||
|
differ = len(set(repr(v) for v in vals)) > 1
|
||||||
|
mark = " * " if differ else " "
|
||||||
|
row = mark + k.ljust(13) + "".join((v if v else "—").ljust(22) for v in vals)
|
||||||
|
print(row)
|
||||||
|
|
||||||
|
print(f"\n配置对照({len(names)} 组)" + (f",Teff={args.teff}" if args.teff else ""))
|
||||||
|
print(" * = 该行各组不一致(A/B 测试应只有你想改的字段带 *)")
|
||||||
|
for d, n, (nm, np_), (dm, dp_) in zip(dirs, names, nst_maps, dot5_maps):
|
||||||
|
print(f" [{n}] nst={os.path.basename(np_) or '(无)'} .5={os.path.basename(dp_) or '(无)'}")
|
||||||
|
|
||||||
|
print_table(".5 关键字段", dot5_keys, dot5_maps, lambda m, k: m.get(k, ""))
|
||||||
|
print_table("nst 关键参数(nc/nl 阶段)", all_nst_keys, nst_maps, lambda m, k: m.get(k.upper(), ""))
|
||||||
|
|
||||||
|
# 源文件路径(便于核对)
|
||||||
|
print("\n源文件:")
|
||||||
|
for d, n, (_, np_), (_, dp_) in zip(dirs, names, nst_maps, dot5_maps):
|
||||||
|
print(f" [{n}] {np_}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
+121
@@ -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)
|
||||||
|
|
||||||
|
## 审查结论
|
||||||
|
- 数值: TODO(max_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 阶段:同 nc(ilvlin=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→100;nc/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())
|
||||||
+2
-2
@@ -4,7 +4,7 @@
|
|||||||
*.env
|
*.env
|
||||||
# Database files
|
# Database files
|
||||||
/data/
|
/data/
|
||||||
|
/test/
|
||||||
# Dynamic computation outputs & execution sandboxes
|
# Dynamic computation outputs & execution sandboxes
|
||||||
/data/results/
|
/data/results/
|
||||||
/data/work/
|
/data/work/
|
||||||
@@ -38,7 +38,7 @@ assets/data/
|
|||||||
# 前端构建产物与依赖(应本地构建,不入库)
|
# 前端构建产物与依赖(应本地构建,不入库)
|
||||||
node_modules/
|
node_modules/
|
||||||
dashboard/dist/
|
dashboard/dist/
|
||||||
|
__pycache__/
|
||||||
# 本机部署清单(含真实内网 IP / 用户名 / 部署路径,按 hosts.ini.example 自行填写)
|
# 本机部署清单(含真实内网 IP / 用户名 / 部署路径,按 hosts.ini.example 自行填写)
|
||||||
hosts.ini
|
hosts.ini
|
||||||
|
|
||||||
|
|||||||
+8
-1
@@ -12,11 +12,18 @@ RUN if [ "$USE_MIRRORS" = "1" ]; then \
|
|||||||
sed -i 's|dl-cdn.alpinelinux.org|mirrors.aliyun.com|g' /etc/apk/repositories; \
|
sed -i 's|dl-cdn.alpinelinux.org|mirrors.aliyun.com|g' /etc/apk/repositories; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# 国内构建时传 --build-arg CARGO_MIRROR=1,将 crates.io 切换到 rsproxy 稀疏镜像。
|
||||||
|
ARG CARGO_MIRROR=0
|
||||||
|
RUN if [ "$CARGO_MIRROR" = "1" ]; then \
|
||||||
|
mkdir -p /usr/local/cargo && \
|
||||||
|
printf '[source.crates-io]\nreplace-with = "rsproxy-sparse"\n\n[source.rsproxy-sparse]\nregistry = "sparse+https://rsproxy.cn/index/"\n' \
|
||||||
|
> /usr/local/cargo/config.toml; \
|
||||||
|
fi
|
||||||
|
|
||||||
RUN apk add --no-cache musl-dev g++ make cmake pkgconfig sqlite-dev openssl-dev openssl-libs-static
|
RUN apk add --no-cache musl-dev g++ make cmake pkgconfig sqlite-dev openssl-dev openssl-libs-static
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY Cargo.toml Cargo.lock ./
|
COPY Cargo.toml Cargo.lock ./
|
||||||
COPY crates/ ./crates/
|
COPY crates/ ./crates/
|
||||||
COPY tools/ ./tools/
|
|
||||||
COPY assets/tlusty_static assets/synspec_static ./assets/
|
COPY assets/tlusty_static assets/synspec_static ./assets/
|
||||||
RUN cargo build --release -p node && \
|
RUN cargo build --release -p node && \
|
||||||
cp /app/target/release/node /usr/local/bin/dcts-node
|
cp /app/target/release/node /usr/local/bin/dcts-node
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ ENV SKIP_DASHBOARD_BUILD=1
|
|||||||
|
|
||||||
COPY Cargo.toml Cargo.lock ./
|
COPY Cargo.toml Cargo.lock ./
|
||||||
COPY crates/ ./crates/
|
COPY crates/ ./crates/
|
||||||
COPY tools/ ./tools/
|
|
||||||
COPY workflows/ ./workflows/
|
COPY workflows/ ./workflows/
|
||||||
|
|
||||||
COPY --from=frontend-builder /app/dashboard/dist ./dashboard/dist
|
COPY --from=frontend-builder /app/dashboard/dist ./dashboard/dist
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -555,16 +555,23 @@ const SIGMA_SB: f64 = 5.670374419e-5;
|
|||||||
/// 校验 `.emflux` 的 bolometric 通量守恒(见 docs/spectrum_correctness_analysis.md §3.2/§4)。
|
/// 校验 `.emflux` 的 bolometric 通量守恒(见 docs/spectrum_correctness_analysis.md §3.2/§4)。
|
||||||
///
|
///
|
||||||
/// # 物理不变量
|
/// # 物理不变量
|
||||||
/// ∫Fλ dλ(出射谱全波长积分)≈ σ_SB × Teff⁴。偏离 >2% → 能量不守恒(红旗 §4)。
|
/// 总表面通量 F = 4π·∫Hλ dλ ≈ σ_SB × Teff⁴。偏离 >2% → 能量不守恒(红旗 §4)。
|
||||||
|
///
|
||||||
|
/// # `.emflux` 格式与 4π 修正
|
||||||
|
/// TLUSTY 在 `OUTPRI`(`tlusty208.f:14174-14188`)写出的 fort.14 快照(runner
|
||||||
|
/// `snapshot_tlusty_outputs` 存为 `.emflux`),两列:波长 λ(Å) / Hλ。其中第二列是
|
||||||
|
/// **Eddington 通量 Hλ**——`tlusty208.f:14174` 源码注释明确 `FLUX(IJ) = the second
|
||||||
|
/// moment H(freq), ergs/cm²/s/sterad/Hz`(每球面度),而非总表面通量 F=4π·H。
|
||||||
|
/// 因此积分 ∫Hλ dλ 与 σTeff⁴ 之间相差一个 4π 立体角因子:比值应乘 4π 后才 ≈1.0
|
||||||
|
/// (实测 ratio≈0.0796 = 1/4π 即源于此)。早期实现漏乘 4π 导致所有点的通量守恒被误判
|
||||||
|
/// 失败(系统性假阳性,ratio 稳定在 0.0796)。
|
||||||
///
|
///
|
||||||
/// # `.emflux` 格式
|
|
||||||
/// SYNSPEC 写出的 fort.14 快照(runner `snapshot_tlusty_outputs`),两列:波长 λ(Å) / Fλ。
|
|
||||||
/// 部分样本全 NaN(大气收敛失败导致辐射转移求解发散)—— NaN 占比过高(>50%)直接判失败。
|
/// 部分样本全 NaN(大气收敛失败导致辐射转移求解发散)—— NaN 占比过高(>50%)直接判失败。
|
||||||
/// 详见 `docs/fort14_nan_analysis.md`:fort.14 全 NaN 是大气结构发散的充分(且几乎必要的)标志。
|
/// 详见 `docs/fort14_nan_analysis.md`:fort.14 全 NaN 是大气结构发散的充分(且几乎必要的)标志。
|
||||||
///
|
///
|
||||||
/// # 算法
|
/// # 算法
|
||||||
/// 跳过 NaN/不可解析行,对剩余 (λ, Fλ) 按波长排序后梯形积分。NaN 占比 >0.5 → 失败。
|
/// 跳过 NaN/不可解析行,对剩余 (λ, Hλ) 按波长排序后梯形积分。NaN 占比 >0.5 → 失败。
|
||||||
/// 否则比较 integrated / (σTeff⁴),偏离 1 超 `tolerance` → 失败。
|
/// 否则比较 4π·∫Hλ dλ / (σTeff⁴),偏离 1 超 `tolerance` → 失败。
|
||||||
///
|
///
|
||||||
/// # 返回值
|
/// # 返回值
|
||||||
/// `Some(EmfluxCheckResult)`:完成校验(含全 NaN 判失败)。`None`:文件缺失 → 跳过。
|
/// `Some(EmfluxCheckResult)`:完成校验(含全 NaN 判失败)。`None`:文件缺失 → 跳过。
|
||||||
@@ -639,8 +646,11 @@ pub fn check_emflux_bolometric(
|
|||||||
integral += 0.5 * (f0 + f1) * (lam1 - lam0);
|
integral += 0.5 * (f0 + f1) * (lam1 - lam0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fort.14 存的是 Eddington 通量 Hλ(erg/cm²/s/sterad,见上方 doc),总表面通量
|
||||||
|
// F = 4π·H,故比值需乘 4π 后才与 σTeff⁴ 比较。漏乘此因子会让所有点稳定地卡在
|
||||||
|
// ratio≈0.0796(=1/4π)被判失败(系统性假阳性)。
|
||||||
let ratio = if sigma_teff4 > 0.0 {
|
let ratio = if sigma_teff4 > 0.0 {
|
||||||
integral / sigma_teff4
|
4.0 * std::f64::consts::PI * integral / sigma_teff4
|
||||||
} else {
|
} else {
|
||||||
f64::INFINITY
|
f64::INFINITY
|
||||||
};
|
};
|
||||||
@@ -657,7 +667,7 @@ pub fn check_emflux_bolometric(
|
|||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(format!(
|
Some(format!(
|
||||||
"emflux bolometric 校验失败:∫Fλdλ / σTeff⁴ = {:.4} 偏离 1 超 {:.2}",
|
"emflux bolometric 校验失败:4π·∫Hλdλ / σTeff⁴ = {:.4} 偏离 1 超 {:.2}",
|
||||||
ratio, tolerance
|
ratio, tolerance
|
||||||
))
|
))
|
||||||
},
|
},
|
||||||
@@ -1400,26 +1410,50 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_emflux_bolometric_pass() {
|
fn test_emflux_bolometric_pass() {
|
||||||
// 构造一个 ∫Fλdλ ≈ σTeff⁴ 的简化谱:Teff=50000K。
|
// fort.14 存的是 Eddington 通量 Hλ(erg/cm²/s/sterad),总表面通量 F=4π·H。
|
||||||
// σ×50000⁴ = 5.670374419e-5 × 6.25e18 = 3.544e14 erg/cm²/s。
|
// 构造一个 4π·∫Hλdλ ≈ σTeff⁴ 的简化谱:Teff=50000K。
|
||||||
// 用梯形:λ∈[100,1100]Å 共 1001 点,均匀 Fλ 使积分 = 3.544e14。
|
// σ×50000⁴ = 5.670374419e-5 × 6.25e18 = 3.544e14 erg/cm²/s(总通量)。
|
||||||
// 积分 = Fλ × (1100-100) = Fλ × 1000。故 Fλ = 3.544e11。
|
// 用梯形:λ∈[100,1100]Å 共 1001 点,均匀 Hλ 使 4π·∫Hλdλ = 3.544e14。
|
||||||
|
// 4π·Hλ × (1100-100) = 3.544e14 → Hλ = 3.544e14 / (4π × 1000) = σTeff⁴/(4π·1000)。
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
let file_path = dir.path().join("model.emflux");
|
let file_path = dir.path().join("model.emflux");
|
||||||
let mut content = String::new();
|
let mut content = String::new();
|
||||||
let sigma_teff4 = 5.670374419e-5 * 50000.0f64.powi(4);
|
let sigma_teff4 = 5.670374419e-5 * 50000.0f64.powi(4);
|
||||||
let flux = sigma_teff4 / 1000.0; // 均匀谱,积分=Fλ×1000
|
let flux = sigma_teff4 / (4.0 * std::f64::consts::PI * 1000.0); // Hλ,4π·积分=σTeff⁴
|
||||||
for i in 0..1001 {
|
for i in 0..1001 {
|
||||||
let lam = 100.0 + i as f64;
|
let lam = 100.0 + i as f64;
|
||||||
content.push_str(&format!("{:>15.3} {:e}\n", lam, flux));
|
content.push_str(&format!("{:>15.3} {:e}\n", lam, flux));
|
||||||
}
|
}
|
||||||
std::fs::write(&file_path, content).unwrap();
|
std::fs::write(&file_path, content).unwrap();
|
||||||
let res = check_emflux_bolometric(&file_path, 50000.0, 0.02).expect("应解析 emflux");
|
let res = check_emflux_bolometric(&file_path, 50000.0, 0.02).expect("应解析 emflux");
|
||||||
assert!(res.valid, "均匀谱积分 = σTeff⁴ 应通过");
|
assert!(res.valid, "4π·均匀谱积分 = σTeff⁴ 应通过");
|
||||||
assert!((res.ratio - 1.0).abs() < 0.01, "比值应≈1.0,实际 {}", res.ratio);
|
assert!((res.ratio - 1.0).abs() < 0.01, "比值应≈1.0,实际 {}", res.ratio);
|
||||||
assert_eq!(res.nan_ratio, 0.0);
|
assert_eq!(res.nan_ratio, 0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_emflux_4pi_correction_regression() {
|
||||||
|
// 回归保护:未经 4π 修正时,真实 Eddington Hλ 谱的 ratio 稳定在 1/(4π)≈0.0796,
|
||||||
|
// 被误判为能量不守恒。修正后 ratio≈1.0 应通过。
|
||||||
|
// 构造 Hλ 使 ∫Hλdλ = σTeff⁴/4π(即 ratio 修正前=1/4π,修正后=1.0)。
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let file_path = dir.path().join("model.emflux");
|
||||||
|
let sigma_teff4 = 5.670374419e-5 * 40000.0f64.powi(4);
|
||||||
|
let hlam = sigma_teff4 / (4.0 * std::f64::consts::PI * 1000.0);
|
||||||
|
let mut content = String::new();
|
||||||
|
for i in 0..1001 {
|
||||||
|
content.push_str(&format!("{:>15.3} {:e}\n", 100.0 + i as f64, hlam));
|
||||||
|
}
|
||||||
|
std::fs::write(&file_path, content).unwrap();
|
||||||
|
let res = check_emflux_bolometric(&file_path, 40000.0, 0.02).expect("应解析");
|
||||||
|
assert!(res.valid, "Eddington Hλ 谱经 4π 修正后应通过");
|
||||||
|
assert!(
|
||||||
|
(res.ratio - 1.0).abs() < 0.01,
|
||||||
|
"4π 修正后 ratio 应≈1.0,实际 {}(若≈0.0796 说明 4π 修正丢失)",
|
||||||
|
res.ratio
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_emflux_bolometric_all_nan() {
|
fn test_emflux_bolometric_all_nan() {
|
||||||
// 全 NaN → 大气收敛失败(辐射转移求解发散),应判失败(而非跳过)。
|
// 全 NaN → 大气收敛失败(辐射转移求解发散),应判失败(而非跳过)。
|
||||||
|
|||||||
@@ -30,11 +30,16 @@ fn calc_hash(bytes: &[u8]) -> String {
|
|||||||
hex::encode(hasher.finalize())
|
hex::encode(hasher.finalize())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ensures Fortran runtime binaries are unpacked and common partition function data files are fetched
|
/// Ensures Fortran runtime binaries are unpacked and common partition function data files are fetched.
|
||||||
|
///
|
||||||
|
/// `default_linelist` 是节点启动时预下载的默认谱线表文件名(如 "gfATO.dat"),放在
|
||||||
|
/// `runtime_dir` 根目录。SYNSPEC 任务可通过 TaskSpec.linelist 按需覆盖(executor 侧
|
||||||
|
/// 再下载指定文件)。该参数从 node 启动配置传入(main.rs 硬编码项目级默认)。
|
||||||
pub async fn ensure_runtime(
|
pub async fn ensure_runtime(
|
||||||
runtime_dir: &Path,
|
runtime_dir: &Path,
|
||||||
server_url: &str,
|
server_url: &str,
|
||||||
client: &Client,
|
client: &Client,
|
||||||
|
default_linelist: &str,
|
||||||
) -> Result<RuntimePaths> {
|
) -> Result<RuntimePaths> {
|
||||||
fs::create_dir_all(runtime_dir)
|
fs::create_dir_all(runtime_dir)
|
||||||
.with_context(|| format!("Failed to create runtime dir: {}", runtime_dir.display()))?;
|
.with_context(|| format!("Failed to create runtime dir: {}", runtime_dir.display()))?;
|
||||||
@@ -42,7 +47,7 @@ pub async fn ensure_runtime(
|
|||||||
let tlusty_exe = runtime_dir.join("tlusty_static");
|
let tlusty_exe = runtime_dir.join("tlusty_static");
|
||||||
let synspec_exe = runtime_dir.join("synspec_static");
|
let synspec_exe = runtime_dir.join("synspec_static");
|
||||||
let data_dir = runtime_dir.join("data");
|
let data_dir = runtime_dir.join("data");
|
||||||
let linelist = runtime_dir.join("gfVIS99.dat");
|
let linelist = runtime_dir.join(default_linelist);
|
||||||
|
|
||||||
fs::create_dir_all(&data_dir)?;
|
fs::create_dir_all(&data_dir)?;
|
||||||
|
|
||||||
@@ -63,18 +68,35 @@ pub async fn ensure_runtime(
|
|||||||
];
|
];
|
||||||
ensure_specific_data_files(&data_dir, server_url, client, common_files).await?;
|
ensure_specific_data_files(&data_dir, server_url, client, common_files).await?;
|
||||||
|
|
||||||
// 3. Check gfVIS99.dat
|
// 3. 下载默认谱线表(如 gfATO.dat)。复用 /api/data/file/{name} 路由,与原子数据
|
||||||
|
// 文件同机制(原子写、并发安全)。线表文件放在 runtime_dir 根(非 data/ 子目录),
|
||||||
|
// 故不直接复用 ensure_specific_data_files(它下载到 data_dir),而是内联同等逻辑。
|
||||||
if !linelist.exists() {
|
if !linelist.exists() {
|
||||||
let url = format!("{}/api/data/linelist", server_url);
|
let url = format!("{}/api/data/file/{}", server_url, default_linelist);
|
||||||
info!("本地缺失主谱线库 gfVIS99.dat,开始从服务端下载: {}...", url);
|
info!(
|
||||||
|
"本地缺失默认谱线表 {},开始从服务端下载: {}...",
|
||||||
|
default_linelist, url
|
||||||
|
);
|
||||||
let resp = client.get(&url).send().await?;
|
let resp = client.get(&url).send().await?;
|
||||||
if resp.status().is_success() {
|
if resp.status().is_success() {
|
||||||
let bytes = resp.bytes().await?;
|
let bytes = resp.bytes().await?;
|
||||||
fs::write(&linelist, &bytes)?;
|
// 原子写:先 .tmp 再 rename,避免半写崩溃留下截断文件。
|
||||||
info!("成功下载并保存主谱线库 gfVIS99.dat");
|
let tmp = runtime_dir.join(format!(
|
||||||
|
"{}.{}.tmp",
|
||||||
|
default_linelist,
|
||||||
|
uuid::Uuid::new_v4().simple()
|
||||||
|
));
|
||||||
|
fs::write(&tmp, &bytes)?;
|
||||||
|
fs::rename(&tmp, &linelist)?;
|
||||||
|
info!(
|
||||||
|
"成功下载默认谱线表 {} ({} bytes)",
|
||||||
|
default_linelist,
|
||||||
|
bytes.len()
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
anyhow::bail!(
|
anyhow::bail!(
|
||||||
"从服务端下载主谱线库 gfVIS99.dat 失败,HTTP 状态码: {}",
|
"从服务端下载默认谱线表 {} 失败,HTTP 状态码: {}",
|
||||||
|
default_linelist,
|
||||||
resp.status()
|
resp.status()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -85,7 +107,7 @@ pub async fn ensure_runtime(
|
|||||||
let tlusty_exe = abs_runtime_dir.join("tlusty_static");
|
let tlusty_exe = abs_runtime_dir.join("tlusty_static");
|
||||||
let synspec_exe = abs_runtime_dir.join("synspec_static");
|
let synspec_exe = abs_runtime_dir.join("synspec_static");
|
||||||
let data_dir = abs_runtime_dir.join("data");
|
let data_dir = abs_runtime_dir.join("data");
|
||||||
let linelist = abs_runtime_dir.join("gfVIS99.dat");
|
let linelist = abs_runtime_dir.join(default_linelist);
|
||||||
|
|
||||||
Ok(RuntimePaths {
|
Ok(RuntimePaths {
|
||||||
tlusty_exe,
|
tlusty_exe,
|
||||||
|
|||||||
@@ -469,6 +469,12 @@ pub struct TaskSpec {
|
|||||||
/// b 因子合理性:极端值下限(默认 1e-3)。
|
/// b 因子合理性:极端值下限(默认 1e-3)。
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub bfac_min: Option<f64>,
|
pub bfac_min: Option<f64>,
|
||||||
|
/// SYNSPEC 谱线表文件名(如 "gfATO.dat")。源自工作流 YAML `linelist`。
|
||||||
|
/// node 端 executor 据此从服务端按需下载(走 /api/data/file/{name})并 symlink 为
|
||||||
|
/// fort.19。None → 用节点启动时下载的默认线表(embedded.rs 的 default_linelist 参数)。
|
||||||
|
/// 旧 payload 反序列化缺省为 None(旧节点用默认线表,无回归)。
|
||||||
|
#[serde(default)]
|
||||||
|
pub linelist: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 仅供测试夹具构造便利:`TaskSpec::default()` 给出合法占位(task_id 零值、
|
/// 仅供测试夹具构造便利:`TaskSpec::default()` 给出合法占位(task_id 零值、
|
||||||
@@ -505,6 +511,7 @@ impl Default for TaskSpec {
|
|||||||
convergence_min_ratio: None,
|
convergence_min_ratio: None,
|
||||||
bfac_max: None,
|
bfac_max: None,
|
||||||
bfac_min: None,
|
bfac_min: None,
|
||||||
|
linelist: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -683,13 +690,13 @@ pub struct TempStructCheckResult {
|
|||||||
/// 辐射转移求解发散,详见 `docs/fort14_nan_analysis.md`)。
|
/// 辐射转移求解发散,详见 `docs/fort14_nan_analysis.md`)。
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct EmfluxCheckResult {
|
pub struct EmfluxCheckResult {
|
||||||
/// 是否通过(积分通量偏离 σTeff⁴ ≤ tolerance 且 NaN 占比可接受)。
|
/// 是否通过(4π·∫Hλdλ 偏离 σTeff⁴ ≤ tolerance 且 NaN 占比可接受)。
|
||||||
pub valid: bool,
|
pub valid: bool,
|
||||||
/// 梯形积分 ∫Fλ dλ(erg/cm²/s)。
|
/// 梯形积分 ∫Hλ dλ(Eddington 通量,erg/cm²/s/sterad)。
|
||||||
pub integrated_flux: f64,
|
pub integrated_flux: f64,
|
||||||
/// 预期 σ_SB × Teff⁴(erg/cm²/s)。
|
/// 预期 σ_SB × Teff⁴(erg/cm²/s,总表面通量)。
|
||||||
pub sigma_teff4: f64,
|
pub sigma_teff4: f64,
|
||||||
/// integrated_flux / sigma_teff4(应 ≈1.0)。
|
/// 4π·∫Hλdλ / sigma_teff4(应 ≈1.0;fort.14 存 Hλ 故需 4π 修正)。
|
||||||
pub ratio: f64,
|
pub ratio: f64,
|
||||||
/// `.emflux` 中 NaN/不可解析行的占比(0.0-1.0)。
|
/// `.emflux` 中 NaN/不可解析行的占比(0.0-1.0)。
|
||||||
pub nan_ratio: f64,
|
pub nan_ratio: f64,
|
||||||
|
|||||||
@@ -61,8 +61,8 @@ pub fn generate_nst_content(chain: &ChainStep, global: &TlustyInput) -> String {
|
|||||||
}
|
}
|
||||||
line1.push(format!("NITER={}", chain.niter));
|
line1.push(format!("NITER={}", chain.niter));
|
||||||
written_keys.insert("NITER".to_string());
|
written_keys.insert("NITER".to_string());
|
||||||
out.push_str(&line1.join(","));
|
// 行宽保护(NST_LINE_MAX):ITEK 等追加后 line1 可超 75 字符,同样需要换行保护。
|
||||||
out.push('\n');
|
push_wrapped(&mut out, &line1);
|
||||||
|
|
||||||
// ── 第 2 行:加速/收敛控制开关 + IELCOR(恒输出,与旧实现一致)──
|
// ── 第 2 行:加速/收敛控制开关 + IELCOR(恒输出,与旧实现一致)──
|
||||||
let mut line2: Vec<String> = Vec::new();
|
let mut line2: Vec<String> = Vec::new();
|
||||||
@@ -84,8 +84,7 @@ pub fn generate_nst_content(chain: &ChainStep, global: &TlustyInput) -> String {
|
|||||||
}
|
}
|
||||||
line2.push(format!("IELCOR={}", n.physics.ielcor));
|
line2.push(format!("IELCOR={}", n.physics.ielcor));
|
||||||
written_keys.insert("IELCOR".to_string());
|
written_keys.insert("IELCOR".to_string());
|
||||||
out.push_str(&line2.join(","));
|
push_wrapped(&mut out, &line2);
|
||||||
out.push('\n');
|
|
||||||
|
|
||||||
// ── 第 3 行起:global.nst 高频关键字(非默认 ChainStep 覆盖的)──
|
// ── 第 3 行起:global.nst 高频关键字(非默认 ChainStep 覆盖的)──
|
||||||
// 所有值统一格式化为 String,避免 i32/f64 混合数组的类型问题。浮点用 fmt_real 带小数点。
|
// 所有值统一格式化为 String,避免 i32/f64 混合数组的类型问题。浮点用 fmt_real 带小数点。
|
||||||
@@ -163,7 +162,40 @@ pub fn generate_nst_content(chain: &ChainStep, global: &TlustyInput) -> String {
|
|||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 写出一组 nst 关键字(同一行逗号分隔),跳过已写的 key。
|
/// nst 单行最大长度。TLUSTY 读 nst 的行缓冲是 `CHARACTER*80 TEXT`
|
||||||
|
/// (tlusty208.f:1689,`FORMAT(A)` 整行读入),超长部分被**静默截断**——
|
||||||
|
/// 旧版曾把 18 个关键字拼成 158 字符的行,导致第 80 字符之后的
|
||||||
|
/// IFALI/IFPOPR/JALI/TRAD/WDIL/TFLOOR/TDISK/TMOLIM 全部从未生效(走 TLUSTY 默认)。
|
||||||
|
/// 解析器逐词跨行续读(词在行尾读完即读下一行),关键字组拆多行语义不变。
|
||||||
|
/// 2026-08-14 修复,见 docs/tlusty_coldstart_nc_trace_illcond_2026_08_13.md §六·五。
|
||||||
|
const NST_LINE_MAX: usize = 75;
|
||||||
|
|
||||||
|
/// 把若干 `KEY=VALUE` 词以逗号连接追加到 out,超过 [`NST_LINE_MAX`] 自动换行。
|
||||||
|
fn push_wrapped(out: &mut String, parts: &[String]) {
|
||||||
|
if parts.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mut line = String::new();
|
||||||
|
for p in parts {
|
||||||
|
if !line.is_empty()
|
||||||
|
&& line.chars().count() + 1 + p.chars().count() > NST_LINE_MAX
|
||||||
|
{
|
||||||
|
out.push_str(&line);
|
||||||
|
out.push('\n');
|
||||||
|
line.clear();
|
||||||
|
}
|
||||||
|
if !line.is_empty() {
|
||||||
|
line.push(',');
|
||||||
|
}
|
||||||
|
line.push_str(p);
|
||||||
|
}
|
||||||
|
if !line.is_empty() {
|
||||||
|
out.push_str(&line);
|
||||||
|
out.push('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 写出一组 nst 关键字(自动换行,行宽 ≤ [`NST_LINE_MAX`]),跳过已写的 key。
|
||||||
/// 用 HashSet::insert 返回值做单次遍历去重,避免 entries 内同键重复时两次都写出。
|
/// 用 HashSet::insert 返回值做单次遍历去重,避免 entries 内同键重复时两次都写出。
|
||||||
fn write_nst_group(out: &mut String, written: &mut HashSet<String>, entries: &[(&str, String)]) {
|
fn write_nst_group(out: &mut String, written: &mut HashSet<String>, entries: &[(&str, String)]) {
|
||||||
let parts: Vec<String> = entries
|
let parts: Vec<String> = entries
|
||||||
@@ -171,10 +203,7 @@ fn write_nst_group(out: &mut String, written: &mut HashSet<String>, entries: &[(
|
|||||||
.filter(|(k, _)| written.insert(k.to_string()))
|
.filter(|(k, _)| written.insert(k.to_string()))
|
||||||
.map(|(k, v)| format!("{}={}", k, v))
|
.map(|(k, v)| format!("{}={}", k, v))
|
||||||
.collect();
|
.collect();
|
||||||
if !parts.is_empty() {
|
push_wrapped(out, &parts);
|
||||||
out.push_str(&parts.join(","));
|
|
||||||
out.push('\n');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -260,6 +289,26 @@ mod tests {
|
|||||||
assert!(content.contains("OK=1"));
|
assert!(content.contains("OK=1"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// 回归(2026-08-14):所有行必须 ≤75 字符(TLUSTY CHARACTER*80 行缓冲,
|
||||||
|
/// 旧版 158 字符行致 IFALI/TFLOOR 等被静默截断失效)。
|
||||||
|
#[test]
|
||||||
|
fn test_nst_all_lines_within_80_char_buffer() {
|
||||||
|
let content = generate_nst_content(&chain_nc(), &TlustyInput::default());
|
||||||
|
for (i, line) in content.lines().enumerate() {
|
||||||
|
assert!(
|
||||||
|
line.chars().count() <= 75,
|
||||||
|
"nst 第 {} 行超宽({} 字符 >75,会被 TLUSTY 80 字符缓冲截断): {}",
|
||||||
|
i + 1,
|
||||||
|
line.chars().count(),
|
||||||
|
line
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// 关键尾部关键字必须在输出中(旧 bug 里它们被截断丢失)
|
||||||
|
assert!(content.contains("TMOLIM="), "TMOLIM 必须在 nst 输出中");
|
||||||
|
assert!(content.contains("TFLOOR="), "TFLOOR 必须在 nst 输出中");
|
||||||
|
}
|
||||||
|
|
||||||
/// 向后兼容:默认配置生成的 nst 前 2 行与旧实现字节级一致。
|
/// 向后兼容:默认配置生成的 nst 前 2 行与旧实现字节级一致。
|
||||||
/// 旧实现 line1 = "ND=50,NLAMBD=3,VTB=2.,ISPODF=1,DDNU=50.,CNU1=6.,NITER=10"
|
/// 旧实现 line1 = "ND=50,NLAMBD=3,VTB=2.,ISPODF=1,DDNU=50.,CNU1=6.,NITER=10"
|
||||||
/// 旧实现 line2 = "IELCOR=-1"
|
/// 旧实现 line2 = "IELCOR=-1"
|
||||||
|
|||||||
+724
-188
@@ -254,6 +254,240 @@ impl<'a> ExecutionRunner<'a> {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 执行收敛链中的单个 TLUSTY 阶段(2026-08-14 从 run_model_with_timeout 的
|
||||||
|
/// 阶段循环体抽取,供主链与 nl_direct 回退共用)。
|
||||||
|
///
|
||||||
|
/// 职责:写 .5/nst 输入 → 按 seed 铺 fort.8 → 运行 tlusty → fort.9 收敛诊断
|
||||||
|
/// (含假收敛排查与 STOP 留言提取)→ 快照阶段产物(.5/.6/.err/nst/_chmax*.9)。
|
||||||
|
///
|
||||||
|
/// 返回 `(StepSummary, 产出的阶段种子路径)`:rc==0 且 fort.7 存在时,
|
||||||
|
/// fort.7 被快照为 `<name>.<label>.7` 并作为返回种子路径(供下一阶段 fort.8);
|
||||||
|
/// 否则返回 None(调用方保留上一阶段种子)。
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
async fn execute_tlusty_stage(
|
||||||
|
&self,
|
||||||
|
model_dir: &std::path::Path,
|
||||||
|
name: &str,
|
||||||
|
params: &GridPointParams,
|
||||||
|
stage_def: &ChainStep,
|
||||||
|
input_cfg: &TlustyInput,
|
||||||
|
seed: Option<&std::path::Path>,
|
||||||
|
convergence_min_ratio: Option<f64>,
|
||||||
|
timeout_sec: u64,
|
||||||
|
shutdown: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
|
||||||
|
) -> Result<(StepSummary, Option<std::path::PathBuf>)> {
|
||||||
|
let stage_t0 = Instant::now();
|
||||||
|
let input5_text = make_input5(params, stage_def, input_cfg);
|
||||||
|
|
||||||
|
let input5_path = model_dir.join(format!("{}.5", name));
|
||||||
|
tokio::fs::write(&input5_path, &input5_text).await?;
|
||||||
|
|
||||||
|
// Write nst file
|
||||||
|
let nst_text = generate_nst_content(stage_def, input_cfg);
|
||||||
|
tokio::fs::write(model_dir.join("nst"), &nst_text).await?;
|
||||||
|
|
||||||
|
// Prepare fort.8 for this stage
|
||||||
|
let fort8 = model_dir.join("fort.8");
|
||||||
|
if stage_def.ltgray == "T" {
|
||||||
|
if fort8.exists() {
|
||||||
|
let _ = tokio::fs::remove_file(&fort8).await;
|
||||||
|
}
|
||||||
|
} else if let Some(s_path) = seed {
|
||||||
|
if s_path.is_file() {
|
||||||
|
if let Err(e) = tokio::fs::copy(s_path, &fort8).await {
|
||||||
|
warn!(
|
||||||
|
"阶段 {} 重载候选近邻推算种子模型期间发生文件复制异常: {}",
|
||||||
|
stage_def.label, e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run tlusty.exe
|
||||||
|
let fin = File::open(&input5_path).await?.into_std().await;
|
||||||
|
let fout = File::create(model_dir.join(format!("{}.6", name)))
|
||||||
|
.await?
|
||||||
|
.into_std()
|
||||||
|
.await;
|
||||||
|
let ferr = File::create(model_dir.join(format!("{}.err", name)))
|
||||||
|
.await?
|
||||||
|
.into_std()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let child = AsyncCommand::new(&self.runtime.tlusty_exe)
|
||||||
|
.current_dir(model_dir)
|
||||||
|
.stdin(Stdio::from(fin))
|
||||||
|
.stdout(Stdio::from(fout))
|
||||||
|
.stderr(Stdio::from(ferr))
|
||||||
|
.kill_on_drop(true)
|
||||||
|
.spawn()?;
|
||||||
|
|
||||||
|
let status_res = run_child_async_with_timeout(child, timeout_sec, shutdown).await;
|
||||||
|
let rc = match status_res {
|
||||||
|
Ok(st) => st.code().unwrap_or(-1),
|
||||||
|
Err(e) => {
|
||||||
|
warn!("tlusty 运行失败/超时: {}", e);
|
||||||
|
-1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let fort9 = model_dir.join("fort.9");
|
||||||
|
let fort7 = model_dir.join("fort.7");
|
||||||
|
|
||||||
|
let mut stage_summary = StepSummary {
|
||||||
|
label: stage_def.label.clone(),
|
||||||
|
chmax: stage_def.chmax,
|
||||||
|
lte: stage_def.lte.clone(),
|
||||||
|
converged: false,
|
||||||
|
best_max_relc: None,
|
||||||
|
elapsed_sec: stage_t0.elapsed().as_secs_f64(),
|
||||||
|
note: None,
|
||||||
|
last_iter: None,
|
||||||
|
worst_depth: None,
|
||||||
|
n_depths: None,
|
||||||
|
itek_history: Vec::new(),
|
||||||
|
conv_trace_check: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 产出的阶段种子:rc==0 且 fort.7 存在时快照为阶段种子文件并返回路径。
|
||||||
|
let mut produced_seed: Option<std::path::PathBuf> = None;
|
||||||
|
|
||||||
|
if rc == 0 && fort7.is_file() {
|
||||||
|
let eff_chmax = stage_def.chmax.unwrap_or(0.001);
|
||||||
|
if fort9.is_file() {
|
||||||
|
let res = check_fort9(&fort9, eff_chmax);
|
||||||
|
stage_summary.converged = res.converged;
|
||||||
|
stage_summary.best_max_relc = Some(res.max_relc);
|
||||||
|
// 携带迭代诊断量进 conv.json(旧版在此处丢弃):
|
||||||
|
// 迭代数/最差深度点供详情页阶段链展示收敛难度。
|
||||||
|
stage_summary.last_iter = res.last_iter;
|
||||||
|
stage_summary.worst_depth = Some(res.worst_depth);
|
||||||
|
stage_summary.n_depths = Some(res.n_depths);
|
||||||
|
// itek 全量保真(Phase 5b):逐次迭代诊断随 summary_json/conv.json 落库。
|
||||||
|
stage_summary.itek_history = res.itek_history.clone();
|
||||||
|
|
||||||
|
// 假收敛排查(§2.1/§3.1):仅对 converged=true 的 stage 做。
|
||||||
|
// Ng/Kantorovich 加速可压低 max_relc 造成数值达标但平衡未达成。
|
||||||
|
if res.converged {
|
||||||
|
if let Some(min_ratio) = convergence_min_ratio {
|
||||||
|
if let Some(tc) = check_convergence_trace(&res.itek_history, min_ratio) {
|
||||||
|
if !tc.valid {
|
||||||
|
stage_summary.converged = false;
|
||||||
|
stage_summary.note = Some(format!(
|
||||||
|
"未收敛 [{}]",
|
||||||
|
tc.error.as_deref().unwrap_or("假收敛排查失败")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
stage_summary.conv_trace_check = Some(tc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 漏洞5修复:发散时从 fort.6 提取求解器 STOP 行(SOLVE/SOLVES/RYBSOL)
|
||||||
|
// 作为 note,提升归因质量。仅未收敛且无既有 note 时补(避免覆盖错误信息)。
|
||||||
|
if !res.converged && stage_summary.note.is_none() {
|
||||||
|
let fort6 = model_dir.join(format!("{}.6", name));
|
||||||
|
if let Some(h) = extract_failure_hint(&fort6) {
|
||||||
|
stage_summary.note = Some(format!("未收敛 [{}]", h));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save fort.9 snapshot
|
||||||
|
let snap_name = format!("{}.{}_chmax{}.9", name, stage_def.label, eff_chmax);
|
||||||
|
let _ = tokio::fs::copy(&fort9, model_dir.join(snap_name)).await;
|
||||||
|
} else {
|
||||||
|
// fort.9 缺失:按 stage_def.niter 区分两种场景(漏洞2进阶修复)。
|
||||||
|
// - niter==0:合法 grey start(lte 阶段不迭代,TLUSTY 不写 fort.9)。
|
||||||
|
// converged=true 保留 grey start 语义;best_max_relc=None 不虚构
|
||||||
|
// (避免污染 final_max_relc/种子选择)。
|
||||||
|
// - niter>0:异常——配了迭代却无 fort.9,通常是 TLUSTY 启动失败
|
||||||
|
// (call quit,如 temp 越界)或 IO 异常。判 converged=false,
|
||||||
|
// 避免把崩溃误判为收敛。此前两种场景共用无校验分支无法区分。
|
||||||
|
if stage_def.niter == 0 {
|
||||||
|
stage_summary.converged = true;
|
||||||
|
stage_summary.best_max_relc = None;
|
||||||
|
stage_summary.note = Some("NITER=0 grey start".to_string());
|
||||||
|
} else {
|
||||||
|
stage_summary.converged = false;
|
||||||
|
stage_summary.best_max_relc = None;
|
||||||
|
// 补 fort.6 失败诊断(call quit 留言),便于排查启动失败原因。
|
||||||
|
let fort6 = model_dir.join(format!("{}.6", name));
|
||||||
|
let hint = extract_failure_hint(&fort6);
|
||||||
|
stage_summary.note = Some(match hint {
|
||||||
|
Some(h) => format!(
|
||||||
|
"stage {} 配置 NITER={} 但 fort.9 缺失 [{}]",
|
||||||
|
stage_def.label, stage_def.niter, h
|
||||||
|
),
|
||||||
|
None => format!(
|
||||||
|
"stage {} 配置 NITER={} 但 fort.9 缺失(TLUSTY 未完成迭代)",
|
||||||
|
stage_def.label, stage_def.niter
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NaN 伪收敛防护:TLUSTY 从 NaN 污染种子启动时会立即崩溃,fort.9 写出
|
||||||
|
// 全 0.00E+00(NaN 参与的相对变化算不出,写出零),max_relc=0 < chmax
|
||||||
|
// 会被误判收敛(生产实测 261 个点因此跳过 nl_direct 回退直接进门槛失败)。
|
||||||
|
// 阶段级防线:converged=true 时复查本阶段 fort.7,含 NaN/Inf/溢出即否决,
|
||||||
|
// 使 require_converged 中止逻辑与 nl_direct 回退得以触发;且不产出污染种子。
|
||||||
|
if stage_summary.converged && atmosphere_has_nan(&fort7) {
|
||||||
|
warn!(
|
||||||
|
"阶段 {} fort.9 达标但 fort.7 含 NaN/Inf/溢出,判未收敛(NaN 伪收敛防护)",
|
||||||
|
stage_def.label
|
||||||
|
);
|
||||||
|
stage_summary.converged = false;
|
||||||
|
stage_summary.note = Some(
|
||||||
|
"未收敛 [阶段大气含 NaN/Inf/溢出(NaN 伪收敛防护:fort.9 全零假达标)]"
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy fort.7 as stage seed
|
||||||
|
let stage_seed_path = model_dir.join(format!("{}.{}.7", name, stage_def.label));
|
||||||
|
let _ = tokio::fs::copy(&fort7, &stage_seed_path).await;
|
||||||
|
if stage_summary.converged {
|
||||||
|
produced_seed = Some(stage_seed_path);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 漏洞5修复:fort.7 缺失分支(输入错误、temp 越界等 call quit 场景),
|
||||||
|
// 从 fort.6 尾部提取 call quit / stop 留言补进 note,便于排查。
|
||||||
|
let fort6 = model_dir.join(format!("{}.6", name));
|
||||||
|
let hint = extract_failure_hint(&fort6);
|
||||||
|
stage_summary.note = Some(match hint {
|
||||||
|
Some(h) => format!("tlusty rc={} or missing fort.7 [{}]", rc, h),
|
||||||
|
None => format!("tlusty rc={} or missing fort.7", rc),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 快照本阶段的同名输入/输出文件,带阶段标签保留。
|
||||||
|
// 背景:.5/.6/.err/nst 在每阶段用同名文件覆盖,若不快照则只有最后阶段(nl)
|
||||||
|
// 的版本能存活到归档,nc 等中间阶段的日志/输入会丢失。失败阶段的日志对排错
|
||||||
|
// 尤其重要,因此此处无条件(不论 rc 是否为 0)快照。
|
||||||
|
// 命名风格与上方 .7/.9 快照一致:<name>.<label>.<后缀>(单 name,不重复)。
|
||||||
|
// 注意:stage_def.label 由配置保证唯一(lte/nc/nl/seed_nc/nl_direct),
|
||||||
|
// 不会与 synspec 产物冲突。
|
||||||
|
//
|
||||||
|
// 不快照 fort.9:上方已把 fort.9 收敛诊断存为 `<name>.<label>_chmax*.9`
|
||||||
|
// (带 chmax 阈值语义),再快照成 `<name>.<label>.9` 会与它内容完全重复。
|
||||||
|
// 故 .9 收敛诊断只保留 `_chmax*.9` 一份,不留重复快照。
|
||||||
|
// (suffix, full_src_name) —— suffix 用于快照名后缀,full_src_name 用于定位源文件
|
||||||
|
for (suffix, full_name) in [
|
||||||
|
("5", format!("{}.5", name)),
|
||||||
|
("6", format!("{}.6", name)),
|
||||||
|
("err", format!("{}.err", name)),
|
||||||
|
("nst", "nst".to_string()),
|
||||||
|
] {
|
||||||
|
let src = model_dir.join(&full_name);
|
||||||
|
if src.is_file() {
|
||||||
|
let snap = model_dir.join(format!("{}.{}.{}", name, stage_def.label, suffix));
|
||||||
|
let _ = tokio::fs::copy(&src, &snap).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((stage_summary, produced_seed))
|
||||||
|
}
|
||||||
|
|
||||||
/// 阶段独立配置执行入口(见 docs/task_engine_decoupling_design.md §5)。
|
/// 阶段独立配置执行入口(见 docs/task_engine_decoupling_design.md §5)。
|
||||||
///
|
///
|
||||||
/// `tlusty_enabled` / `synspec_enabled` 控制各阶段是否运行:
|
/// `tlusty_enabled` / `synspec_enabled` 控制各阶段是否运行:
|
||||||
@@ -362,196 +596,26 @@ impl<'a> ExecutionRunner<'a> {
|
|||||||
// 默认值定义集中在 config.rs 的 TlustyInput::default(),此处不再重复维护。
|
// 默认值定义集中在 config.rs 的 TlustyInput::default(),此处不再重复维护。
|
||||||
let default_input = TlustyInput::default();
|
let default_input = TlustyInput::default();
|
||||||
let input_cfg: &TlustyInput = tlusty_input.unwrap_or(&default_input);
|
let input_cfg: &TlustyInput = tlusty_input.unwrap_or(&default_input);
|
||||||
for stage_def in &chain {
|
let mut failed_stage_idx: Option<usize> = None;
|
||||||
|
for (stage_idx, stage_def) in chain.iter().enumerate() {
|
||||||
if tlusty_skipped {
|
if tlusty_skipped {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
let stage_t0 = Instant::now();
|
let (stage_summary, produced_seed) = self
|
||||||
let input5_text = make_input5(params, stage_def, input_cfg);
|
.execute_tlusty_stage(
|
||||||
|
&model_dir,
|
||||||
let input5_path = model_dir.join(format!("{}.5", name));
|
name,
|
||||||
tokio::fs::write(&input5_path, &input5_text).await?;
|
params,
|
||||||
|
stage_def,
|
||||||
// Write nst file
|
input_cfg,
|
||||||
let nst_text = generate_nst_content(stage_def, input_cfg);
|
current_seed.as_deref(),
|
||||||
tokio::fs::write(model_dir.join("nst"), &nst_text).await?;
|
convergence_min_ratio,
|
||||||
|
timeout_sec,
|
||||||
// Prepare fort.8 for this stage
|
shutdown.clone(),
|
||||||
if stage_def.ltgray == "T" {
|
)
|
||||||
if fort8.exists() {
|
.await?;
|
||||||
let _ = tokio::fs::remove_file(&fort8).await;
|
if let Some(p) = produced_seed {
|
||||||
}
|
current_seed = Some(p);
|
||||||
} else if let Some(ref s_path) = current_seed {
|
|
||||||
if s_path.is_file() {
|
|
||||||
if let Err(e) = tokio::fs::copy(s_path, &fort8).await {
|
|
||||||
warn!(
|
|
||||||
"阶段 {} 重载候选近邻推算种子模型期间发生文件复制异常: {}",
|
|
||||||
stage_def.label, e
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run tlusty.exe
|
|
||||||
let fin = File::open(&input5_path).await?.into_std().await;
|
|
||||||
let fout = File::create(model_dir.join(format!("{}.6", name)))
|
|
||||||
.await?
|
|
||||||
.into_std()
|
|
||||||
.await;
|
|
||||||
let ferr = File::create(model_dir.join(format!("{}.err", name)))
|
|
||||||
.await?
|
|
||||||
.into_std()
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let child = AsyncCommand::new(&self.runtime.tlusty_exe)
|
|
||||||
.current_dir(&model_dir)
|
|
||||||
.stdin(Stdio::from(fin))
|
|
||||||
.stdout(Stdio::from(fout))
|
|
||||||
.stderr(Stdio::from(ferr))
|
|
||||||
.kill_on_drop(true)
|
|
||||||
.spawn()?;
|
|
||||||
|
|
||||||
let status_res =
|
|
||||||
run_child_async_with_timeout(child, timeout_sec, shutdown.clone()).await;
|
|
||||||
let rc = match status_res {
|
|
||||||
Ok(st) => st.code().unwrap_or(-1),
|
|
||||||
Err(e) => {
|
|
||||||
warn!("tlusty 运行失败/超时: {}", e);
|
|
||||||
-1
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let fort9 = model_dir.join("fort.9");
|
|
||||||
let fort7 = model_dir.join("fort.7");
|
|
||||||
|
|
||||||
let mut stage_summary = StepSummary {
|
|
||||||
label: stage_def.label.clone(),
|
|
||||||
chmax: stage_def.chmax,
|
|
||||||
lte: stage_def.lte.clone(),
|
|
||||||
converged: false,
|
|
||||||
best_max_relc: None,
|
|
||||||
elapsed_sec: stage_t0.elapsed().as_secs_f64(),
|
|
||||||
note: None,
|
|
||||||
last_iter: None,
|
|
||||||
worst_depth: None,
|
|
||||||
n_depths: None,
|
|
||||||
itek_history: Vec::new(),
|
|
||||||
conv_trace_check: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
if rc == 0 && fort7.is_file() {
|
|
||||||
let eff_chmax = stage_def.chmax.unwrap_or(0.001);
|
|
||||||
if fort9.is_file() {
|
|
||||||
let res = check_fort9(&fort9, eff_chmax);
|
|
||||||
stage_summary.converged = res.converged;
|
|
||||||
stage_summary.best_max_relc = Some(res.max_relc);
|
|
||||||
// 携带迭代诊断量进 conv.json(旧版在此处丢弃):
|
|
||||||
// 迭代数/最差深度点供详情页阶段链展示收敛难度。
|
|
||||||
stage_summary.last_iter = res.last_iter;
|
|
||||||
stage_summary.worst_depth = Some(res.worst_depth);
|
|
||||||
stage_summary.n_depths = Some(res.n_depths);
|
|
||||||
// itek 全量保真(Phase 5b):逐次迭代诊断随 summary_json/conv.json 落库。
|
|
||||||
stage_summary.itek_history = res.itek_history.clone();
|
|
||||||
|
|
||||||
// 假收敛排查(§2.1/§3.1):仅对 converged=true 的 stage 做。
|
|
||||||
// Ng/Kantorovich 加速可压低 max_relc 造成数值达标但平衡未达成。
|
|
||||||
if res.converged {
|
|
||||||
if let Some(min_ratio) = convergence_min_ratio {
|
|
||||||
if let Some(tc) =
|
|
||||||
check_convergence_trace(&res.itek_history, min_ratio)
|
|
||||||
{
|
|
||||||
if !tc.valid {
|
|
||||||
stage_summary.converged = false;
|
|
||||||
stage_summary.note = Some(format!(
|
|
||||||
"未收敛 [{}]",
|
|
||||||
tc.error.as_deref().unwrap_or("假收敛排查失败")
|
|
||||||
));
|
|
||||||
}
|
|
||||||
stage_summary.conv_trace_check = Some(tc);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 漏洞5修复:发散时从 fort.6 提取求解器 STOP 行(SOLVE/SOLVES/RYBSOL)
|
|
||||||
// 作为 note,提升归因质量。仅未收敛且无既有 note 时补(避免覆盖错误信息)。
|
|
||||||
if !res.converged && stage_summary.note.is_none() {
|
|
||||||
let fort6 = model_dir.join(format!("{}.6", name));
|
|
||||||
if let Some(h) = extract_failure_hint(&fort6) {
|
|
||||||
stage_summary.note = Some(format!("未收敛 [{}]", h));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save fort.9 snapshot
|
|
||||||
let snap_name = format!("{}.{}_chmax{}.9", name, stage_def.label, eff_chmax);
|
|
||||||
let _ = tokio::fs::copy(&fort9, model_dir.join(snap_name)).await;
|
|
||||||
} else {
|
|
||||||
// fort.9 缺失:按 stage_def.niter 区分两种场景(漏洞2进阶修复)。
|
|
||||||
// - niter==0:合法 grey start(lte 阶段不迭代,TLUSTY 不写 fort.9)。
|
|
||||||
// converged=true 保留 grey start 语义;best_max_relc=None 不虚构
|
|
||||||
// (避免污染 final_max_relc/种子选择)。
|
|
||||||
// - niter>0:异常——配了迭代却无 fort.9,通常是 TLUSTY 启动失败
|
|
||||||
// (call quit,如 temp 越界)或 IO 异常。判 converged=false,
|
|
||||||
// 避免把崩溃误判为收敛。此前两种场景共用无校验分支无法区分。
|
|
||||||
if stage_def.niter == 0 {
|
|
||||||
stage_summary.converged = true;
|
|
||||||
stage_summary.best_max_relc = None;
|
|
||||||
stage_summary.note = Some("NITER=0 grey start".to_string());
|
|
||||||
} else {
|
|
||||||
stage_summary.converged = false;
|
|
||||||
stage_summary.best_max_relc = None;
|
|
||||||
// 补 fort.6 失败诊断(call quit 留言),便于排查启动失败原因。
|
|
||||||
let fort6 = model_dir.join(format!("{}.6", name));
|
|
||||||
let hint = extract_failure_hint(&fort6);
|
|
||||||
stage_summary.note = Some(match hint {
|
|
||||||
Some(h) => format!(
|
|
||||||
"stage {} 配置 NITER={} 但 fort.9 缺失 [{}]",
|
|
||||||
stage_def.label, stage_def.niter, h
|
|
||||||
),
|
|
||||||
None => format!(
|
|
||||||
"stage {} 配置 NITER={} 但 fort.9 缺失(TLUSTY 未完成迭代)",
|
|
||||||
stage_def.label, stage_def.niter
|
|
||||||
),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy fort.7 as stage seed
|
|
||||||
let stage_seed_path = model_dir.join(format!("{}.{}.7", name, stage_def.label));
|
|
||||||
let _ = tokio::fs::copy(&fort7, &stage_seed_path).await;
|
|
||||||
current_seed = Some(stage_seed_path);
|
|
||||||
} else {
|
|
||||||
// 漏洞5修复:fort.7 缺失分支(输入错误、temp 越界等 call quit 场景),
|
|
||||||
// 从 fort.6 尾部提取 call quit / stop 留言补进 note,便于排查。
|
|
||||||
let fort6 = model_dir.join(format!("{}.6", name));
|
|
||||||
let hint = extract_failure_hint(&fort6);
|
|
||||||
stage_summary.note = Some(match hint {
|
|
||||||
Some(h) => format!("tlusty rc={} or missing fort.7 [{}]", rc, h),
|
|
||||||
None => format!("tlusty rc={} or missing fort.7", rc),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 快照本阶段的同名输入/输出文件,带阶段标签保留。
|
|
||||||
// 背景:.5/.6/.err/nst 在每阶段用同名文件覆盖,若不快照则只有最后阶段(nl)
|
|
||||||
// 的版本能存活到归档,nc 等中间阶段的日志/输入会丢失。失败阶段的日志对排错
|
|
||||||
// 尤其重要,因此此处无条件(不论 rc 是否为 0)快照。
|
|
||||||
// 命名风格与上方 .7/.9 快照一致:<name>.<label>.<后缀>(单 name,不重复)。
|
|
||||||
// 注意:stage_def.label 由配置保证唯一(lte/nc/nl/seed_nc),不会与 synspec 产物冲突。
|
|
||||||
//
|
|
||||||
// 不快照 fort.9:上方 L292 已把 fort.9 收敛诊断存为 `<name>.<label>_chmax*.9`
|
|
||||||
// (带 chmax 阈值语义),再快照成 `<name>.<label>.9` 会与它内容完全重复。
|
|
||||||
// 故 .9 收敛诊断只保留 `_chmax*.9` 一份,不留重复快照。
|
|
||||||
// (suffix, full_src_name) —— suffix 用于快照名后缀,full_src_name 用于定位源文件
|
|
||||||
for (suffix, full_name) in [
|
|
||||||
("5", format!("{}.5", name)),
|
|
||||||
("6", format!("{}.6", name)),
|
|
||||||
("err", format!("{}.err", name)),
|
|
||||||
("nst", "nst".to_string()),
|
|
||||||
] {
|
|
||||||
let src = model_dir.join(&full_name);
|
|
||||||
if src.is_file() {
|
|
||||||
let snap = model_dir.join(format!("{}.{}.{}", name, stage_def.label, suffix));
|
|
||||||
let _ = tokio::fs::copy(&src, &snap).await;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final_chmax = stage_def.chmax;
|
final_chmax = stage_def.chmax;
|
||||||
@@ -566,10 +630,72 @@ impl<'a> ExecutionRunner<'a> {
|
|||||||
"阶段 {} 要求收敛但未达标,中止后续收敛链阶段",
|
"阶段 {} 要求收敛但未达标,中止后续收敛链阶段",
|
||||||
stage_def.label
|
stage_def.label
|
||||||
);
|
);
|
||||||
|
failed_stage_idx = Some(stage_idx);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── nl_direct 回退(2026-08-14)──
|
||||||
|
// 背景:种子链 seed_nc→nl 中,seed_nc 发散(Kantorovich 雪崩)时其被污染的
|
||||||
|
// fort.7 会被无条件传给 nl 并毒死 nl;而 nl 自身从原始干净种子出发通常能
|
||||||
|
// 直接收敛(实测 5 个最难失败点中 3 个仅靠此回退恢复,另 2 个经
|
||||||
|
// seed_nc 预热后收敛——两条路径失败集互补,故仅作回退、不替换主链)。
|
||||||
|
// 详见 docs/tlusty_coldstart_nc_trace_illcond_2026_08_13.md。
|
||||||
|
// 触发条件:require_converged 阶段失败 + 失败阶段非链首(存在中间阶段
|
||||||
|
// 污染可能)+ 存在原始种子文件。回退以 `<label>_direct` 作为独立阶段
|
||||||
|
// 记录进 conv.json,成功则采纳其收敛结果。
|
||||||
|
if tlusty_enabled && !final_converged {
|
||||||
|
if let Some(idx) = failed_stage_idx {
|
||||||
|
if idx > 0 {
|
||||||
|
if let Some(orig_seed) = seed_atmos {
|
||||||
|
if orig_seed.is_file() {
|
||||||
|
let failed_label = chain[idx].label.clone();
|
||||||
|
info!(
|
||||||
|
"阶段 {} 未收敛:尝试 nl_direct 回退(用原始种子直接重跑该阶段)",
|
||||||
|
failed_label
|
||||||
|
);
|
||||||
|
let mut direct_stage = chain[idx].clone();
|
||||||
|
direct_stage.label = format!("{}_direct", failed_label);
|
||||||
|
let (mut direct_summary, direct_seed) = self
|
||||||
|
.execute_tlusty_stage(
|
||||||
|
&model_dir,
|
||||||
|
name,
|
||||||
|
params,
|
||||||
|
&direct_stage,
|
||||||
|
input_cfg,
|
||||||
|
Some(orig_seed),
|
||||||
|
convergence_min_ratio,
|
||||||
|
timeout_sec,
|
||||||
|
shutdown.clone(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
if direct_summary.converged {
|
||||||
|
info!("nl_direct 回退成功:{} 自原始种子直接收敛", failed_label);
|
||||||
|
final_converged = true;
|
||||||
|
final_chmax = direct_stage.chmax;
|
||||||
|
if let Some(r) = direct_summary.best_max_relc {
|
||||||
|
final_max_relc = Some(r);
|
||||||
|
}
|
||||||
|
if let Some(p) = direct_seed {
|
||||||
|
current_seed = Some(p);
|
||||||
|
}
|
||||||
|
if direct_summary.note.is_none() {
|
||||||
|
direct_summary.note = Some(
|
||||||
|
"nl_direct 回退收敛(跳过被污染的中间阶段,用原始种子直接求解)"
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if direct_summary.note.is_none() {
|
||||||
|
direct_summary.note =
|
||||||
|
Some("nl_direct 回退未收敛".to_string());
|
||||||
|
}
|
||||||
|
stage_summaries.push(direct_summary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Final atmosphere file .7
|
// Final atmosphere file .7
|
||||||
let final_7 = model_dir.join(format!("{}.7", name));
|
let final_7 = model_dir.join(format!("{}.7", name));
|
||||||
if let Some(ref s_path) = current_seed {
|
if let Some(ref s_path) = current_seed {
|
||||||
@@ -604,7 +730,7 @@ impl<'a> ExecutionRunner<'a> {
|
|||||||
// `(RAD+CON)/TOT` 列。任一深度偏离 1 超阈值即判失败(与 atmosphere_has_nan 同级)。
|
// `(RAD+CON)/TOT` 列。任一深度偏离 1 超阈值即判失败(与 atmosphere_has_nan 同级)。
|
||||||
// 此时 `<name>.6` 仍存在(行 688 的冗余清理在此之后),内容是最后阶段日志。
|
// 此时 `<name>.6` 仍存在(行 688 的冗余清理在此之后),内容是最后阶段日志。
|
||||||
// energy_tolerance=None → 跳过(非常规配置);`.6` 无能量守恒表 → 函数返回 None 跳过。
|
// energy_tolerance=None → 跳过(非常规配置);`.6` 无能量守恒表 → 函数返回 None 跳过。
|
||||||
let energy_check = if tlusty_enabled {
|
let mut energy_check = if tlusty_enabled {
|
||||||
energy_tolerance.and_then(|tol| {
|
energy_tolerance.and_then(|tol| {
|
||||||
let fort6 = model_dir.join(format!("{}.6", name));
|
let fort6 = model_dir.join(format!("{}.6", name));
|
||||||
check_energy_conservation(&fort6, tol)
|
check_energy_conservation(&fort6, tol)
|
||||||
@@ -620,6 +746,75 @@ impl<'a> ExecutionRunner<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── nl_tight 回退(2026-08-17,能量边际专用)──
|
||||||
|
// 背景:7 个生产失败点的唯一失败项是最深层(ID=ND)能量残差 1.0–2.3% 超
|
||||||
|
// 1% 阈值,其余门槛(emflux/温度/bfac)全过。此类残差源于 CHMAX=1e-3 停机
|
||||||
|
// 时统计平衡未完全达成。实测(test/20260817_failed400/mg_tight):从自身
|
||||||
|
// 最终模型续迭代、CHMAX 收紧 10×(1e-4),~19 次迭代后残差降至 0.001–0.002
|
||||||
|
// (降幅 ~10×)。触发条件:能量门槛失败 + 最终大气存在且无 NaN + 数值收敛
|
||||||
|
// (非发散模型)。回退以 `<label>_tight` 记录进 conv.json,成功则刷新最终
|
||||||
|
// 大气与能量判定,并重快照 fort.12/14(bfac/emflux 检查读快照文件)。
|
||||||
|
if tlusty_enabled && energy_failed && !atmo_has_nan && final_7.is_file() {
|
||||||
|
if let Some(last_stage) = chain.last() {
|
||||||
|
let tight_chmax = last_stage.chmax.map(|c| c / 10.0);
|
||||||
|
let mut tight_stage = last_stage.clone();
|
||||||
|
tight_stage.label = format!("{}_tight", last_stage.label);
|
||||||
|
tight_stage.chmax = tight_chmax;
|
||||||
|
info!(
|
||||||
|
"能量边际回退:以 CHMAX={} 从自身最终模型续迭代 {}",
|
||||||
|
tight_chmax.map(|c| c.to_string()).unwrap_or_default(),
|
||||||
|
tight_stage.label
|
||||||
|
);
|
||||||
|
let tight_seed = final_7.clone();
|
||||||
|
let (mut tight_summary, tight_produced) = self
|
||||||
|
.execute_tlusty_stage(
|
||||||
|
&model_dir,
|
||||||
|
name,
|
||||||
|
params,
|
||||||
|
&tight_stage,
|
||||||
|
input_cfg,
|
||||||
|
Some(&tight_seed),
|
||||||
|
convergence_min_ratio,
|
||||||
|
timeout_sec,
|
||||||
|
shutdown.clone(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let tight_ok = tight_summary.converged;
|
||||||
|
if tight_ok {
|
||||||
|
tight_summary.note = Some(
|
||||||
|
"nl_tight 回退收敛(能量边际:CHMAX 收紧 10× 从自身模型续迭代)"
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
if let Some(p) = tight_produced {
|
||||||
|
// 刷新最终大气 + 出射谱快照(后续 bfac/emflux 门槛读快照文件)
|
||||||
|
let _ = tokio::fs::copy(&p, &final_7).await;
|
||||||
|
snapshot_tlusty_outputs(&model_dir, name).await;
|
||||||
|
// 重新判定能量守恒:tight 阶段的 stdout 直接写 <name>.6
|
||||||
|
//(每阶段覆盖),此处读到的是 tight 阶段的能量表。
|
||||||
|
if let Some(tol) = energy_tolerance {
|
||||||
|
let fort6 = model_dir.join(format!("{}.6", name));
|
||||||
|
if fort6.is_file() {
|
||||||
|
energy_check = check_energy_conservation(&fort6, tol);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
energy_failed = matches!(&energy_check, Some(ec) if !ec.valid);
|
||||||
|
if !energy_failed {
|
||||||
|
final_converged = true;
|
||||||
|
final_chmax = tight_stage.chmax;
|
||||||
|
if let Some(r) = tight_summary.best_max_relc {
|
||||||
|
final_max_relc = Some(r);
|
||||||
|
}
|
||||||
|
info!("nl_tight 回退成功:能量守恒达标");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tight_summary.note =
|
||||||
|
Some("nl_tight 回退未收敛".to_string());
|
||||||
|
}
|
||||||
|
stage_summaries.push(tight_summary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 温度结构边界校验(docs/spectrum_correctness_analysis.md §3.1/§4):
|
// 温度结构边界校验(docs/spectrum_correctness_analysis.md §3.1/§4):
|
||||||
// 解析 `.7`(最终大气)逐深度温度 T,表层 >max_factor×Teff 或全层越界判失败。
|
// 解析 `.7`(最终大气)逐深度温度 T,表层 >max_factor×Teff 或全层越界判失败。
|
||||||
// 读 final_7(此时已就位);tlusty_enabled=false 时 final_7 来自外部种子,跳过。
|
// 读 final_7(此时已就位);tlusty_enabled=false 时 final_7 来自外部种子,跳过。
|
||||||
@@ -1042,4 +1237,345 @@ mod tests {
|
|||||||
assert!(!model_dir.join(format!("{}.bfac", name)).exists());
|
assert!(!model_dir.join(format!("{}.bfac", name)).exists());
|
||||||
assert!(!model_dir.join(format!("{}.emflux", name)).exists());
|
assert!(!model_dir.join(format!("{}.emflux", name)).exists());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 快速单元回归(2026-08-14):nl_direct 回退接线。用假 tlusty 脚本模拟
|
||||||
|
/// 「seed_nc 发散 → nl 失败 → 回退 nl_direct 收敛」三步,验证:
|
||||||
|
/// 1) nl 失败后触发 <label>_direct 阶段并用原始种子重跑;
|
||||||
|
/// 2) 回退收敛被采纳为最终结果(final 阶段链含 nl_direct 且 converged)。
|
||||||
|
#[tokio::test]
|
||||||
|
async fn unit_nl_direct_fallback_wiring() {
|
||||||
|
let work = std::env::temp_dir().join(format!("dcts_unit_fallback_{}", std::process::id()));
|
||||||
|
let _ = std::fs::remove_dir_all(&work);
|
||||||
|
tokio::fs::create_dir_all(&work).await.unwrap();
|
||||||
|
// 假 tlusty:按调用序号产出 fort.9 —— #1(seed_nc) 发散、#2(nl) 失败、
|
||||||
|
// #3(nl_direct) 收敛。fort.7 写哑内容(无 NaN 字样)。
|
||||||
|
let fake = work.join("fake_tlusty.sh");
|
||||||
|
let script = r#"#!/usr/bin/env bash
|
||||||
|
n=$(cat ./call_count 2>/dev/null || echo 0); n=$((n+1)); echo $n > ./call_count
|
||||||
|
cat /dev/stdin > /dev/null
|
||||||
|
echo "fake model" > fort.7
|
||||||
|
case $n in
|
||||||
|
1|2) printf ' 1 50 1.0E-02 1.0E-02 1.0E-02 1.0E-02 1.0E+16 75 1\n' > fort.9 ;;
|
||||||
|
3) printf ' 1 50 1.0E-02 1.0E-02 1.0E-02 1.0E-02 1.0E-05 75 1\n' > fort.9 ;;
|
||||||
|
esac
|
||||||
|
exit 0
|
||||||
|
"#;
|
||||||
|
tokio::fs::write(&fake, script).await.unwrap();
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
std::fs::set_permissions(&fake, std::fs::Permissions::from_mode(0o755)).unwrap();
|
||||||
|
}
|
||||||
|
let runtime = crate::embedded::RuntimePaths {
|
||||||
|
tlusty_exe: fake.clone(),
|
||||||
|
synspec_exe: work.join("synspec_absent"),
|
||||||
|
data_dir: work.clone(),
|
||||||
|
linelist: work.join("linelist_absent"),
|
||||||
|
};
|
||||||
|
// 原始种子:普通文件存在即可(fake 不读内容)
|
||||||
|
let seed = work.join("orig_seed.7");
|
||||||
|
tokio::fs::write(&seed, "orig clean seed").await.unwrap();
|
||||||
|
|
||||||
|
let runner = super::ExecutionRunner::new(&runtime, work.join("models"));
|
||||||
|
let params = crate::models::GridPointParams {
|
||||||
|
teff: crate::models::GridAxisValue::from_value(60000.0),
|
||||||
|
logg: crate::models::GridAxisValue::from_value(5.5),
|
||||||
|
loghe: crate::models::GridAxisValue::from_value(-2.0),
|
||||||
|
logc: crate::models::GridAxisValue::from_value(-4.0),
|
||||||
|
logn: crate::models::GridAxisValue::from_value(-4.0),
|
||||||
|
logo: crate::models::GridAxisValue::from_value(-4.0),
|
||||||
|
};
|
||||||
|
let summary = runner
|
||||||
|
.run_model_with_timeout(
|
||||||
|
¶ms,
|
||||||
|
"t60000_g5.5_he-2_c-4_n-4_o-4",
|
||||||
|
"seed_step",
|
||||||
|
Some(super::default_seed_chain()),
|
||||||
|
Some(&seed),
|
||||||
|
None,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
60,
|
||||||
|
None,
|
||||||
|
None, None, None, None, None, None, None, None, None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let labels: Vec<(String, bool)> = summary
|
||||||
|
.stages
|
||||||
|
.iter()
|
||||||
|
.map(|s| (s.label.clone(), s.converged))
|
||||||
|
.collect();
|
||||||
|
assert_eq!(
|
||||||
|
labels,
|
||||||
|
vec![
|
||||||
|
("seed_nc".to_string(), false),
|
||||||
|
("nl".to_string(), false),
|
||||||
|
("nl_direct".to_string(), true),
|
||||||
|
],
|
||||||
|
"应触发 nl_direct 回退并采纳其收敛"
|
||||||
|
);
|
||||||
|
let _ = std::fs::remove_dir_all(&work);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// nl_tight 回退接线回归(2026-08-17):收敛模型但能量边际失败(最深层残差
|
||||||
|
/// 2% > 1%)→ 应以 CHMAX 收紧 10× 从自身模型续迭代,回退后能量达标。
|
||||||
|
#[tokio::test]
|
||||||
|
async fn unit_nl_tight_fallback_wiring() {
|
||||||
|
let work =
|
||||||
|
std::env::temp_dir().join(format!("dcts_unit_nltight_{}", std::process::id()));
|
||||||
|
let _ = std::fs::remove_dir_all(&work);
|
||||||
|
tokio::fs::create_dir_all(&work).await.unwrap();
|
||||||
|
// 假 tlusty:#1(nl) 数值收敛但能量表残差 0.02;#2(nl_tight) 残差 0.005。
|
||||||
|
let fake = work.join("fake_tlusty.sh");
|
||||||
|
let script = r#"#!/usr/bin/env bash
|
||||||
|
n=$(cat ./call_count 2>/dev/null || echo 0); n=$((n+1)); echo $n > ./call_count
|
||||||
|
cat /dev/stdin > /dev/null
|
||||||
|
echo "clean model" > fort.7
|
||||||
|
printf ' 1 50 1.0E-02 1.0E-02 1.0E-02 1.0E-02 1.0E-05 75 1\n' > fort.9
|
||||||
|
case $n in
|
||||||
|
1) printf ' depth TOT (RAD+CON)/TOT\n 1 1.00\n 50 1.02\n' ;;
|
||||||
|
2) printf ' depth TOT (RAD+CON)/TOT\n 1 1.00\n 50 1.005\n' ;;
|
||||||
|
esac
|
||||||
|
exit 0
|
||||||
|
"#;
|
||||||
|
tokio::fs::write(&fake, script).await.unwrap();
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
std::fs::set_permissions(&fake, std::fs::Permissions::from_mode(0o755)).unwrap();
|
||||||
|
}
|
||||||
|
let runtime = crate::embedded::RuntimePaths {
|
||||||
|
tlusty_exe: fake.clone(),
|
||||||
|
synspec_exe: work.join("synspec_absent"),
|
||||||
|
data_dir: work.clone(),
|
||||||
|
linelist: work.join("linelist_absent"),
|
||||||
|
};
|
||||||
|
let seed = work.join("orig_seed.7");
|
||||||
|
tokio::fs::write(&seed, "orig clean seed").await.unwrap();
|
||||||
|
|
||||||
|
let runner = super::ExecutionRunner::new(&runtime, work.join("models"));
|
||||||
|
let params = crate::models::GridPointParams {
|
||||||
|
teff: crate::models::GridAxisValue::from_value(60000.0),
|
||||||
|
logg: crate::models::GridAxisValue::from_value(5.0),
|
||||||
|
loghe: crate::models::GridAxisValue::from_value(-2.0),
|
||||||
|
logc: crate::models::GridAxisValue::from_value(-2.0),
|
||||||
|
logn: crate::models::GridAxisValue::from_value(-4.0),
|
||||||
|
logo: crate::models::GridAxisValue::from_value(-3.0),
|
||||||
|
};
|
||||||
|
// 单阶段 nl 链
|
||||||
|
let chain = vec![super::ChainStep {
|
||||||
|
label: "nl".to_string(),
|
||||||
|
lte: "F".to_string(),
|
||||||
|
ltgray: "F".to_string(),
|
||||||
|
niter: 100,
|
||||||
|
chmax: Some(0.001),
|
||||||
|
orelax: Some(0.5),
|
||||||
|
ilvlin: 100,
|
||||||
|
require_converged: true,
|
||||||
|
itek: None,
|
||||||
|
ichang: None,
|
||||||
|
idlte: None,
|
||||||
|
iacc: None,
|
||||||
|
}];
|
||||||
|
let summary = runner
|
||||||
|
.run_model_with_timeout(
|
||||||
|
¶ms,
|
||||||
|
"t60000_g5.0_he-2_c-2_n-4_o-3",
|
||||||
|
"cold_run",
|
||||||
|
Some(chain),
|
||||||
|
Some(&seed),
|
||||||
|
None,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
60,
|
||||||
|
None,
|
||||||
|
None, // tlusty_input
|
||||||
|
Some(0.01), // energy_tolerance
|
||||||
|
None, None, None, None, None, None, None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let labels: Vec<(String, bool)> = summary
|
||||||
|
.stages
|
||||||
|
.iter()
|
||||||
|
.map(|s| (s.label.clone(), s.converged))
|
||||||
|
.collect();
|
||||||
|
assert_eq!(
|
||||||
|
labels,
|
||||||
|
vec![
|
||||||
|
("nl".to_string(), true),
|
||||||
|
("nl_tight".to_string(), true),
|
||||||
|
],
|
||||||
|
"能量边际应触发 nl_tight 回退并收敛(阶段: {:?})",
|
||||||
|
labels
|
||||||
|
);
|
||||||
|
let ec = summary.energy_check.as_ref().expect("应有能量检查结果");
|
||||||
|
assert!(ec.valid, "回退后能量应达标: {:?}", ec.error);
|
||||||
|
let _ = std::fs::remove_dir_all(&work);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// NaN 伪收敛防护回归(2026-08-17):TLUSTY 从 NaN 污染种子启动会立即崩溃,
|
||||||
|
/// fort.9 全 0.00E+00 假达标(max_relc=0 < chmax)——生产实测 261 个点因此
|
||||||
|
/// 被误判 nl 收敛、跳过 nl_direct 回退。防护后:fort.7 含 NaN 即否决 converged,
|
||||||
|
/// 且不产出污染种子(下一阶段回退用原始种子)。
|
||||||
|
#[tokio::test]
|
||||||
|
async fn unit_nan_pseudo_convergence_veto() {
|
||||||
|
let work =
|
||||||
|
std::env::temp_dir().join(format!("dcts_unit_nanveto_{}", std::process::id()));
|
||||||
|
let _ = std::fs::remove_dir_all(&work);
|
||||||
|
tokio::fs::create_dir_all(&work).await.unwrap();
|
||||||
|
// 假 tlusty:#1(seed_nc) 写全零 fort.9(假达标)+ 含 NaN 的 fort.7;
|
||||||
|
// #2(nl) 从原始种子正常收敛。
|
||||||
|
let fake = work.join("fake_tlusty.sh");
|
||||||
|
let script = r#"#!/usr/bin/env bash
|
||||||
|
n=$(cat ./call_count 2>/dev/null || echo 0); n=$((n+1)); echo $n > ./call_count
|
||||||
|
cat /dev/stdin > /dev/null
|
||||||
|
case $n in
|
||||||
|
1) printf ' 50 5.0E+03 NaN NaN NaN\n' > fort.7
|
||||||
|
printf ' 1 50 0.00E+00 0.00E+00 0.00E+00 0.00E+00 0.00E+00 0 9\n' > fort.9 ;;
|
||||||
|
2) echo "clean model" > fort.7
|
||||||
|
printf ' 1 50 1.0E-02 1.0E-02 1.0E-02 1.0E-02 1.0E-05 75 1\n' > fort.9 ;;
|
||||||
|
esac
|
||||||
|
exit 0
|
||||||
|
"#;
|
||||||
|
tokio::fs::write(&fake, script).await.unwrap();
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
std::fs::set_permissions(&fake, std::fs::Permissions::from_mode(0o755)).unwrap();
|
||||||
|
}
|
||||||
|
let runtime = crate::embedded::RuntimePaths {
|
||||||
|
tlusty_exe: fake.clone(),
|
||||||
|
synspec_exe: work.join("synspec_absent"),
|
||||||
|
data_dir: work.clone(),
|
||||||
|
linelist: work.join("linelist_absent"),
|
||||||
|
};
|
||||||
|
let seed = work.join("orig_seed.7");
|
||||||
|
tokio::fs::write(&seed, "orig clean seed").await.unwrap();
|
||||||
|
|
||||||
|
let runner = super::ExecutionRunner::new(&runtime, work.join("models"));
|
||||||
|
let params = crate::models::GridPointParams {
|
||||||
|
teff: crate::models::GridAxisValue::from_value(60000.0),
|
||||||
|
logg: crate::models::GridAxisValue::from_value(6.0),
|
||||||
|
loghe: crate::models::GridAxisValue::from_value(-4.0),
|
||||||
|
logc: crate::models::GridAxisValue::from_value(-4.0),
|
||||||
|
logn: crate::models::GridAxisValue::from_value(-4.0),
|
||||||
|
logo: crate::models::GridAxisValue::from_value(-4.0),
|
||||||
|
};
|
||||||
|
let summary = runner
|
||||||
|
.run_model_with_timeout(
|
||||||
|
¶ms,
|
||||||
|
"t60000_g6.0_he-4_c-4_n-4_o-4",
|
||||||
|
"seed_step",
|
||||||
|
Some(super::default_seed_chain()),
|
||||||
|
Some(&seed),
|
||||||
|
None,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
60,
|
||||||
|
None,
|
||||||
|
None, None, None, None, None, None, None, None, None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let labels: Vec<(String, bool)> = summary
|
||||||
|
.stages
|
||||||
|
.iter()
|
||||||
|
.map(|s| (s.label.clone(), s.converged))
|
||||||
|
.collect();
|
||||||
|
assert_eq!(
|
||||||
|
labels,
|
||||||
|
vec![
|
||||||
|
("seed_nc".to_string(), false),
|
||||||
|
("nl".to_string(), true),
|
||||||
|
],
|
||||||
|
"全零 fort.9 + NaN fort.7 应被否决收敛;nl 应回退到原始种子并收敛(阶段: {:?})",
|
||||||
|
labels
|
||||||
|
);
|
||||||
|
let seed_nc = summary.stages.iter().find(|s| s.label == "seed_nc").unwrap();
|
||||||
|
assert!(
|
||||||
|
seed_nc.note.as_deref().unwrap_or("").contains("NaN"),
|
||||||
|
"note 应标注 NaN 伪收敛防护,实际: {:?}",
|
||||||
|
seed_nc.note
|
||||||
|
);
|
||||||
|
let _ = std::fs::remove_dir_all(&work);
|
||||||
|
}
|
||||||
|
/// 端到端回归(2026-08-14,#[ignore]:真实运行 TLUSTY,约 15-40 分钟):
|
||||||
|
/// nl_direct 回退。场景取自生产实测失败点 t60000_g5.5_he-2_c-4_n-4_o-4:
|
||||||
|
/// seed_nc 发散 → 其输出毒死 nl → 回退用原始种子直接跑 nl 应收敛。
|
||||||
|
/// 对应修复验证数据见 docs/tlusty_coldstart_nc_trace_illcond_2026_08_13.md。
|
||||||
|
#[tokio::test]
|
||||||
|
#[ignore = "真实运行 TLUSTY(~40min),验证修复时用 cargo test -p common nl_direct -- --ignored --nocapture"]
|
||||||
|
async fn e2e_nl_direct_fallback_recovers_poisoned_seed_chain() {
|
||||||
|
use crate::models::GridAxisValue;
|
||||||
|
let manifest = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
let repo_root: &std::path::Path = manifest.parent().and_then(|p| p.parent()).unwrap();
|
||||||
|
let seed_path = repo_root.join(
|
||||||
|
"test/20260813_cold_nc_trace_fix/hevalidation/p4_nldirect_cno1/inputs/fort.8",
|
||||||
|
);
|
||||||
|
if !seed_path.is_file() {
|
||||||
|
eprintln!("跳过:种子模型不存在({}),需先准备 p4 场景种子", seed_path.display());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let runtime = crate::embedded::RuntimePaths {
|
||||||
|
tlusty_exe: repo_root.join("assets/tlusty_static"),
|
||||||
|
synspec_exe: repo_root.join("assets/synspec_static"),
|
||||||
|
data_dir: repo_root.join("assets/data"),
|
||||||
|
linelist: repo_root.join("assets/data/gfATO.dat"),
|
||||||
|
};
|
||||||
|
let work = std::env::temp_dir().join(format!("dcts_e2e_nl_direct_{}", std::process::id()));
|
||||||
|
let _ = std::fs::remove_dir_all(&work);
|
||||||
|
tokio::fs::create_dir_all(&work).await.unwrap();
|
||||||
|
let runner = super::ExecutionRunner::new(&runtime, work.clone());
|
||||||
|
|
||||||
|
let params = GridPointParams {
|
||||||
|
teff: GridAxisValue::from_value(60000.0),
|
||||||
|
logg: GridAxisValue::from_value(5.5),
|
||||||
|
loghe: GridAxisValue::from_value(-2.0),
|
||||||
|
logc: GridAxisValue::from_value(-4.0),
|
||||||
|
logn: GridAxisValue::from_value(-4.0),
|
||||||
|
logo: GridAxisValue::from_value(-4.0),
|
||||||
|
};
|
||||||
|
let name = "t60000_g5.5_he-2_c-4_n-4_o-4";
|
||||||
|
let chain = super::default_seed_chain();
|
||||||
|
let summary = runner
|
||||||
|
.run_model_with_timeout(
|
||||||
|
¶ms,
|
||||||
|
name,
|
||||||
|
"seed_step",
|
||||||
|
Some(chain),
|
||||||
|
Some(&seed_path),
|
||||||
|
None, // synspec_cfg
|
||||||
|
true, // tlusty_enabled
|
||||||
|
false, // synspec_enabled
|
||||||
|
3600,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None, None, None, None, None, None, None, None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// 成功判据(两条路径之一,均为修复目标):
|
||||||
|
// a) 主链 nl 自收敛(nst 截断修复后 IFALI/IFPOPR/JALI 等真正生效,nl 可自愈),或
|
||||||
|
// b) nl 失败触发 nl_direct 回退且回退收敛(p4 场景实测 13it/6.3e-4)。
|
||||||
|
let labels: Vec<(&str, bool)> = summary
|
||||||
|
.stages
|
||||||
|
.iter()
|
||||||
|
.map(|s| (s.label.as_str(), s.converged))
|
||||||
|
.collect();
|
||||||
|
eprintln!("stages: {:?}", labels);
|
||||||
|
let main_nl_ok = summary.stages.iter().any(|s| s.label == "nl" && s.converged);
|
||||||
|
let direct_ok = summary.stages.iter().any(|s| s.label == "nl_direct" && s.converged);
|
||||||
|
assert!(
|
||||||
|
main_nl_ok || direct_ok,
|
||||||
|
"种子链应经主链 nl 或 nl_direct 回退之一收敛,实际阶段: {:?}",
|
||||||
|
labels
|
||||||
|
);
|
||||||
|
let _ = std::fs::remove_dir_all(&work);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,10 +45,14 @@ pub fn calculate_seed_distance(cand: &GridPointParams, target: &GridPointParams)
|
|||||||
let d_loghe = (cand.loghe.value() - target.loghe.value()).abs();
|
let d_loghe = (cand.loghe.value() - target.loghe.value()).abs();
|
||||||
|
|
||||||
// exact family 判定:Teff/logg/logHe 视为“同物理族”,仅 CNO 丰度不同。
|
// exact family 判定:Teff/logg/logHe 视为“同物理族”,仅 CNO 丰度不同。
|
||||||
// Teff 容忍度取半步 5000K:实际网格 Teff 档位通常为整数千(20000/30000/.../60000),
|
// Teff 容忍度取一个网格步长 5000K(含边界 <=):
|
||||||
// 半步既能覆盖 config_dense 等 10000K 步长的相邻档互作种子,
|
// 网格 Teff 档位间隔正好 5000K(如 55000↔60000 相邻档),旧版严格 `< 5000.0`
|
||||||
// 又避免跨过大 Teff 间距导致 sdB 高温模型用低温种子而不收敛(sdB_cno 步长 40000K 仍不命中 exact)。
|
// 把所有相邻 Teff 档互为种子静默排除出 exact_family——而实测 Teff 方向是最稳的
|
||||||
if d_teff < 5000.0 && d_logg < 0.01 && d_loghe < 0.01 {
|
// 种子方向(55k 点靠 50k Teff 种子收敛;50k→60k 直接 nl 亦收敛),且相邻档
|
||||||
|
// 同(logg,He,CNO)种子在 exact_family 内有向距离为 0(优于任何同 Teff CNO 邻居)。
|
||||||
|
// 桶索引(db::SeedBucketKey)本就按 floor/floor+1 双写,5000K 整除边界可命中。
|
||||||
|
// 2026-08-14 修复,详见 docs/tlusty_coldstart_nc_trace_illcond_2026_08_13.md。
|
||||||
|
if d_teff <= 5000.0 && d_logg < 0.01 && d_loghe < 0.01 {
|
||||||
// 同物理族内仅 CNO 不同:用有向距离优先匹配贫金属方向的种子(见 directed_cno_distance)。
|
// 同物理族内仅 CNO 不同:用有向距离优先匹配贫金属方向的种子(见 directed_cno_distance)。
|
||||||
(true, directed_cno_distance(cand, target))
|
(true, directed_cno_distance(cand, target))
|
||||||
} else {
|
} else {
|
||||||
@@ -137,4 +141,15 @@ mod tests {
|
|||||||
assert!(!is_exact, "跨 teff 20000K 应为 global 分支");
|
assert!(!is_exact, "跨 teff 20000K 应为 global 分支");
|
||||||
assert!(d > 0.0);
|
assert!(d > 0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 回归(2026-08-14):d_teff 恰为 5000K(相邻 Teff 档,如 55000↔60000)
|
||||||
|
/// 应算 exact_family(旧版严格 < 排除了所有相邻档互为种子)。
|
||||||
|
#[test]
|
||||||
|
fn test_adjacent_teff_bucket_is_exact_family() {
|
||||||
|
let seed = params(55000.0, 5.0, -2.0, -4.0, -3.0, -3.0);
|
||||||
|
let target = params(60000.0, 5.0, -2.0, -4.0, -3.0, -3.0);
|
||||||
|
let (is_exact, d) = calculate_seed_distance(&seed, &target);
|
||||||
|
assert!(is_exact, "d_teff=5000 的相邻 Teff 档应属 exact_family");
|
||||||
|
assert_eq!(d, 0.0, "同 (logg,He,CNO) 的相邻 Teff 种子有向距离应为 0");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ use reqwest::Client;
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use tracing::{debug, info, warn};
|
use tracing::{debug, info, warn};
|
||||||
|
|
||||||
|
/// 进程级互斥锁:序列化谱线表按需下载,防止多 Slot 并发首次派发时对同一 238MB 文件
|
||||||
|
/// 重复发起下载请求(POSIX rename 保证写安全,但冗余下载浪费带宽与 IO)。
|
||||||
|
static LINELIST_DOWNLOAD_MUTEX: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
|
||||||
|
|
||||||
pub async fn execute_task(
|
pub async fn execute_task(
|
||||||
client: &Client,
|
client: &Client,
|
||||||
server_url: &str,
|
server_url: &str,
|
||||||
@@ -162,7 +166,49 @@ pub async fn execute_task(
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|v| serde_json::from_value::<SynspecInput>(v.clone()).ok());
|
.and_then(|v| serde_json::from_value::<SynspecInput>(v.clone()).ok());
|
||||||
|
|
||||||
let runner = ExecutionRunner::new(runtime, slot_work_dir.clone());
|
// 谱线表覆盖:TaskSpec.linelist 指定时(如 YAML 配 linelist: gfATO.dat),
|
||||||
|
// 按需从服务端下载到 runtime_dir 根目录,并构造覆盖了 linelist 路径的 RuntimePaths。
|
||||||
|
// None → 用 node 启动时下载的默认线表(ensure_runtime 的 default_linelist 参数)。
|
||||||
|
let runtime_override: Option<RuntimePaths> = if let Some(ref ll_name) = task.linelist {
|
||||||
|
// 线表放在 runtime_dir 根目录(与默认线表同级,runner symlink 为 fort.19)。
|
||||||
|
let ll_path = runtime
|
||||||
|
.data_dir
|
||||||
|
.parent()
|
||||||
|
.unwrap_or(&runtime.data_dir)
|
||||||
|
.join(ll_name);
|
||||||
|
// 互斥保护:多 Slot 并发首次派发时,仅第一个进入临界区的 Slot 执行下载,
|
||||||
|
// 后续 Slot 在获得锁后 double-check 发现文件已存在直接跳过,避免冗余下载。
|
||||||
|
{
|
||||||
|
let _guard = LINELIST_DOWNLOAD_MUTEX.lock().await;
|
||||||
|
if !ll_path.exists() {
|
||||||
|
info!("任务指定谱线表 {} 本地缺失,从服务端按需下载...", ll_name);
|
||||||
|
let url = format!("{}/api/data/file/{}", server_url, ll_name);
|
||||||
|
let resp = client.get(&url).send().await?;
|
||||||
|
if resp.status().is_success() {
|
||||||
|
let bytes = resp.bytes().await?;
|
||||||
|
let ll_dir = ll_path.parent().unwrap_or(std::path::Path::new("."));
|
||||||
|
let tmp = ll_dir.join(format!("{}.{}.tmp", ll_name, uuid::Uuid::new_v4().simple()));
|
||||||
|
tokio::fs::write(&tmp, &bytes).await?;
|
||||||
|
tokio::fs::rename(&tmp, &ll_path).await?;
|
||||||
|
info!("成功下载谱线表 {} ({} bytes)", ll_name, bytes.len());
|
||||||
|
} else {
|
||||||
|
anyhow::bail!(
|
||||||
|
"下载任务指定谱线表 {} 失败,HTTP {}",
|
||||||
|
ll_name,
|
||||||
|
resp.status()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut rt = runtime.clone();
|
||||||
|
rt.linelist = ll_path;
|
||||||
|
Some(rt)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let effective_runtime = runtime_override.as_ref().unwrap_or(runtime);
|
||||||
|
|
||||||
|
let runner = ExecutionRunner::new(effective_runtime, slot_work_dir.clone());
|
||||||
// 执行链来源(优先级):
|
// 执行链来源(优先级):
|
||||||
// 1. TaskSpec.tlusty_chain_params(用户在 YAML `tlusty_chain:` 配置的多阶段 ChainStep
|
// 1. TaskSpec.tlusty_chain_params(用户在 YAML `tlusty_chain:` 配置的多阶段 ChainStep
|
||||||
// 数组,由 scheduler 序列化注入)——非空时优先使用,使用户能细粒度控制 niter/chmax/
|
// 数组,由 scheduler 序列化注入)——非空时优先使用,使用户能细粒度控制 niter/chmax/
|
||||||
|
|||||||
@@ -66,7 +66,9 @@ async fn main() -> Result<()> {
|
|||||||
let client = build_client_with_token(Some(&node_token));
|
let client = build_client_with_token(Some(&node_token));
|
||||||
|
|
||||||
info!("检查本地运行时二进制与基础数据文件,必要时从服务端拉取...");
|
info!("检查本地运行时二进制与基础数据文件,必要时从服务端拉取...");
|
||||||
let runtime = ensure_runtime(runtime_dir, &node_cfg.server_url, &client)
|
// 默认谱线表:项目级默认 gfATO.dat(全波段 18-23000Å,230 万线)。
|
||||||
|
// TaskSpec.linelist 可按工作流覆盖(executor 侧按需下载指定文件)。
|
||||||
|
let runtime = ensure_runtime(runtime_dir, &node_cfg.server_url, &client, "gfATO.dat")
|
||||||
.await
|
.await
|
||||||
.context("预热与获取服务端运行时资源失败")?;
|
.context("预热与获取服务端运行时资源失败")?;
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,8 @@ pub async fn get_status(
|
|||||||
.await
|
.await
|
||||||
.unwrap_or(serde_json::json!({
|
.unwrap_or(serde_json::json!({
|
||||||
"total": 0, "pending": 0, "queued": 0, "running": 0, "completed": 0, "failed": 0,
|
"total": 0, "pending": 0, "queued": 0, "running": 0, "completed": 0, "failed": 0,
|
||||||
"cold_run_converged": 0, "seed_step_converged": 0, "synspec_converged": 0
|
"cold_run_converged": 0, "seed_step_converged": 0, "tlusty_failed": 0,
|
||||||
|
"synspec_converged": 0, "synspec_failed": 0, "synspec_pending": 0
|
||||||
}));
|
}));
|
||||||
|
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
|
|||||||
@@ -576,14 +576,17 @@ impl Database {
|
|||||||
SUM(CASE WHEN status = 'running' THEN 1 ELSE 0 END) AS running,
|
SUM(CASE WHEN status = 'running' THEN 1 ELSE 0 END) AS running,
|
||||||
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) AS completed,
|
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) AS completed,
|
||||||
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS failed,
|
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS failed,
|
||||||
SUM(CASE WHEN status = 'completed' AND tlusty_success_method = 'cold_run' THEN 1 ELSE 0 END) AS cold_run_converged,
|
SUM(CASE WHEN tlusty_status = 'converged' AND tlusty_success_method = 'cold_run' THEN 1 ELSE 0 END) AS cold_run_converged,
|
||||||
SUM(CASE WHEN status = 'completed' AND tlusty_success_method = 'seed_step' THEN 1 ELSE 0 END) AS seed_step_converged,
|
SUM(CASE WHEN tlusty_status = 'converged' AND tlusty_success_method = 'seed_step' THEN 1 ELSE 0 END) AS seed_step_converged,
|
||||||
SUM(CASE WHEN status = 'completed' AND synspec_success_method IS NOT NULL THEN 1 ELSE 0 END) AS synspec_converged
|
SUM(CASE WHEN tlusty_status = 'failed' THEN 1 ELSE 0 END) AS tlusty_failed,
|
||||||
|
SUM(CASE WHEN synspec_status = 'converged' THEN 1 ELSE 0 END) AS synspec_converged,
|
||||||
|
SUM(CASE WHEN synspec_status = 'failed' THEN 1 ELSE 0 END) AS synspec_failed,
|
||||||
|
SUM(CASE WHEN synspec_status = 'pending' THEN 1 ELSE 0 END) AS synspec_pending
|
||||||
FROM grid_points WHERE workflow_name = ?1",
|
FROM grid_points WHERE workflow_name = ?1",
|
||||||
params![name],
|
params![name],
|
||||||
|r| {
|
|r| {
|
||||||
let n = |i: usize| -> i64 { r.get::<_, Option<i64>>(i).unwrap_or(None).unwrap_or(0) };
|
let n = |i: usize| -> i64 { r.get::<_, Option<i64>>(i).unwrap_or(None).unwrap_or(0) };
|
||||||
Ok((n(0), n(1), n(2), n(3), n(4), n(5), n(6), n(7), n(8)))
|
Ok((n(0), n(1), n(2), n(3), n(4), n(5), n(6), n(7), n(8), n(9), n(10), n(11)))
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
None => conn.query_row(
|
None => conn.query_row(
|
||||||
@@ -594,14 +597,17 @@ impl Database {
|
|||||||
SUM(CASE WHEN status = 'running' THEN 1 ELSE 0 END) AS running,
|
SUM(CASE WHEN status = 'running' THEN 1 ELSE 0 END) AS running,
|
||||||
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) AS completed,
|
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) AS completed,
|
||||||
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS failed,
|
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS failed,
|
||||||
SUM(CASE WHEN status = 'completed' AND tlusty_success_method = 'cold_run' THEN 1 ELSE 0 END) AS cold_run_converged,
|
SUM(CASE WHEN tlusty_status = 'converged' AND tlusty_success_method = 'cold_run' THEN 1 ELSE 0 END) AS cold_run_converged,
|
||||||
SUM(CASE WHEN status = 'completed' AND tlusty_success_method = 'seed_step' THEN 1 ELSE 0 END) AS seed_step_converged,
|
SUM(CASE WHEN tlusty_status = 'converged' AND tlusty_success_method = 'seed_step' THEN 1 ELSE 0 END) AS seed_step_converged,
|
||||||
SUM(CASE WHEN status = 'completed' AND synspec_success_method IS NOT NULL THEN 1 ELSE 0 END) AS synspec_converged
|
SUM(CASE WHEN tlusty_status = 'failed' THEN 1 ELSE 0 END) AS tlusty_failed,
|
||||||
|
SUM(CASE WHEN synspec_status = 'converged' THEN 1 ELSE 0 END) AS synspec_converged,
|
||||||
|
SUM(CASE WHEN synspec_status = 'failed' THEN 1 ELSE 0 END) AS synspec_failed,
|
||||||
|
SUM(CASE WHEN synspec_status = 'pending' THEN 1 ELSE 0 END) AS synspec_pending
|
||||||
FROM grid_points",
|
FROM grid_points",
|
||||||
[],
|
[],
|
||||||
|r| {
|
|r| {
|
||||||
let n = |i: usize| -> i64 { r.get::<_, Option<i64>>(i).unwrap_or(None).unwrap_or(0) };
|
let n = |i: usize| -> i64 { r.get::<_, Option<i64>>(i).unwrap_or(None).unwrap_or(0) };
|
||||||
Ok((n(0), n(1), n(2), n(3), n(4), n(5), n(6), n(7), n(8)))
|
Ok((n(0), n(1), n(2), n(3), n(4), n(5), n(6), n(7), n(8), n(9), n(10), n(11)))
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
}?;
|
}?;
|
||||||
@@ -614,7 +620,10 @@ impl Database {
|
|||||||
failed,
|
failed,
|
||||||
cold_run_converged,
|
cold_run_converged,
|
||||||
seed_step_converged,
|
seed_step_converged,
|
||||||
|
tlusty_failed,
|
||||||
synspec_converged,
|
synspec_converged,
|
||||||
|
synspec_failed,
|
||||||
|
synspec_pending,
|
||||||
) = row;
|
) = row;
|
||||||
|
|
||||||
Ok(serde_json::json!({
|
Ok(serde_json::json!({
|
||||||
@@ -626,7 +635,10 @@ impl Database {
|
|||||||
"failed": failed,
|
"failed": failed,
|
||||||
"cold_run_converged": cold_run_converged,
|
"cold_run_converged": cold_run_converged,
|
||||||
"seed_step_converged": seed_step_converged,
|
"seed_step_converged": seed_step_converged,
|
||||||
|
"tlusty_failed": tlusty_failed,
|
||||||
"synspec_converged": synspec_converged,
|
"synspec_converged": synspec_converged,
|
||||||
|
"synspec_failed": synspec_failed,
|
||||||
|
"synspec_pending": synspec_pending,
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
.await?
|
.await?
|
||||||
@@ -721,6 +733,10 @@ impl Database {
|
|||||||
failed,
|
failed,
|
||||||
cold_run_converged: g("cold_run_converged"),
|
cold_run_converged: g("cold_run_converged"),
|
||||||
seed_step_converged: g("seed_step_converged"),
|
seed_step_converged: g("seed_step_converged"),
|
||||||
|
tlusty_failed: g("tlusty_failed"),
|
||||||
|
synspec_converged: g("synspec_converged"),
|
||||||
|
synspec_failed: g("synspec_failed"),
|
||||||
|
synspec_pending: g("synspec_pending"),
|
||||||
waves,
|
waves,
|
||||||
avg_point_sec,
|
avg_point_sec,
|
||||||
eta_sec,
|
eta_sec,
|
||||||
@@ -828,6 +844,7 @@ impl Database {
|
|||||||
COALESCE(t.elapsed_sec, gp.last_elapsed_sec) AS eff_elapsed,
|
COALESCE(t.elapsed_sec, gp.last_elapsed_sec) AS eff_elapsed,
|
||||||
gp.workflow_name,
|
gp.workflow_name,
|
||||||
gp.synspec_success_method,
|
gp.synspec_success_method,
|
||||||
|
gp.tlusty_status, gp.synspec_status,
|
||||||
ROW_NUMBER() OVER (
|
ROW_NUMBER() OVER (
|
||||||
PARTITION BY gp.name, gp.workflow_name
|
PARTITION BY gp.name, gp.workflow_name
|
||||||
ORDER BY t.completed_at IS NULL, t.completed_at DESC, t.created_at DESC
|
ORDER BY t.completed_at IS NULL, t.completed_at DESC, t.created_at DESC
|
||||||
@@ -872,6 +889,7 @@ impl Database {
|
|||||||
COALESCE(t.elapsed_sec, gp.last_elapsed_sec) AS eff_elapsed,
|
COALESCE(t.elapsed_sec, gp.last_elapsed_sec) AS eff_elapsed,
|
||||||
gp.workflow_name,
|
gp.workflow_name,
|
||||||
gp.synspec_success_method,
|
gp.synspec_success_method,
|
||||||
|
gp.tlusty_status, gp.synspec_status,
|
||||||
ROW_NUMBER() OVER (
|
ROW_NUMBER() OVER (
|
||||||
PARTITION BY gp.name, gp.workflow_name
|
PARTITION BY gp.name, gp.workflow_name
|
||||||
ORDER BY t.completed_at IS NULL, t.completed_at DESC, t.created_at DESC
|
ORDER BY t.completed_at IS NULL, t.completed_at DESC, t.created_at DESC
|
||||||
|
|||||||
@@ -699,6 +699,9 @@ pub struct WorkflowSummary {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 工作流列表内联的网格统计(单条 GROUP BY 聚合回填,无 N+1)。
|
/// 工作流列表内联的网格统计(单条 GROUP BY 聚合回填,无 N+1)。
|
||||||
|
///
|
||||||
|
/// 阶段分项计数(TLUSTY/SYNSPEC 双视图):前端按选中阶段切换展示,每阶段独立统计,
|
||||||
|
/// 不再混用整体 status。`tlusty_*` 基于 `tlusty_status` 列,`synspec_*` 基于 `synspec_status` 列。
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct WorkflowListStats {
|
pub struct WorkflowListStats {
|
||||||
pub total: i64,
|
pub total: i64,
|
||||||
@@ -707,8 +710,14 @@ pub struct WorkflowListStats {
|
|||||||
pub running: i64,
|
pub running: i64,
|
||||||
pub cold_run_converged: i64,
|
pub cold_run_converged: i64,
|
||||||
pub seed_step_converged: i64,
|
pub seed_step_converged: i64,
|
||||||
/// 光谱收敛点数(synspec_success_method 非 NULL;Phase 5a 落库)。
|
/// TLUSTY 阶段发散点数(tlusty_status='failed')。
|
||||||
|
pub tlusty_failed: i64,
|
||||||
|
/// SYNSPEC 阶段收敛点数(synspec_status='converged')。
|
||||||
pub synspec_converged: i64,
|
pub synspec_converged: i64,
|
||||||
|
/// SYNSPEC 阶段失败点数(synspec_status='failed')。
|
||||||
|
pub synspec_failed: i64,
|
||||||
|
/// SYNSPEC 阶段未运行(synspec_status='pending',多为 tlusty 未收敛而阻塞)。
|
||||||
|
pub synspec_pending: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
@@ -731,6 +740,8 @@ pub struct WaveStats {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 单工作流执行统计(详情页数据源)。
|
/// 单工作流执行统计(详情页数据源)。
|
||||||
|
///
|
||||||
|
/// 阶段分项计数(TLUSTY/SYNSPEC 双视图):前端按选中阶段切换展示。
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct WorkflowStats {
|
pub struct WorkflowStats {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@@ -743,6 +754,14 @@ pub struct WorkflowStats {
|
|||||||
pub failed: i64,
|
pub failed: i64,
|
||||||
pub cold_run_converged: i64,
|
pub cold_run_converged: i64,
|
||||||
pub seed_step_converged: i64,
|
pub seed_step_converged: i64,
|
||||||
|
/// TLUSTY 阶段发散点数(tlusty_status='failed')。
|
||||||
|
pub tlusty_failed: i64,
|
||||||
|
/// SYNSPEC 阶段收敛点数(synspec_status='converged')。
|
||||||
|
pub synspec_converged: i64,
|
||||||
|
/// SYNSPEC 阶段失败点数(synspec_status='failed')。
|
||||||
|
pub synspec_failed: i64,
|
||||||
|
/// SYNSPEC 阶段未运行(synspec_status='pending')。
|
||||||
|
pub synspec_pending: i64,
|
||||||
pub waves: Vec<WaveStats>,
|
pub waves: Vec<WaveStats>,
|
||||||
/// 近似单点平均墙钟耗时(秒,含排队等待,仅参考);无历史数据为 None。
|
/// 近似单点平均墙钟耗时(秒,含排队等待,仅参考);无历史数据为 None。
|
||||||
pub avg_point_sec: Option<f64>,
|
pub avg_point_sec: Option<f64>,
|
||||||
@@ -772,6 +791,13 @@ pub struct PointRow {
|
|||||||
pub tlusty_success_method: Option<String>,
|
pub tlusty_success_method: Option<String>,
|
||||||
/// 光谱阶段收敛策略(synspec 以何策略收敛;TLUSTY-only 为 NULL)。
|
/// 光谱阶段收敛策略(synspec 以何策略收敛;TLUSTY-only 为 NULL)。
|
||||||
pub synspec_success_method: Option<String>,
|
pub synspec_success_method: Option<String>,
|
||||||
|
/// TLUSTY 阶段状态('converged'/'failed'/'pending'/NULL)。
|
||||||
|
/// 独立于整体 status——半失败点(大气收敛+光谱失败)此处为 'converged' 而 status='failed'。
|
||||||
|
#[serde(default)]
|
||||||
|
pub tlusty_status: Option<String>,
|
||||||
|
/// SYNSPEC 阶段状态('converged'/'failed'/'pending'/NULL)。
|
||||||
|
#[serde(default)]
|
||||||
|
pub synspec_status: Option<String>,
|
||||||
pub attempt_count: i32,
|
pub attempt_count: i32,
|
||||||
/// 最近尝试的收敛指标(最大相对修正)。
|
/// 最近尝试的收敛指标(最大相对修正)。
|
||||||
pub last_max_relc: Option<f64>,
|
pub last_max_relc: Option<f64>,
|
||||||
@@ -807,6 +833,8 @@ fn point_row_from_query(r: &rusqlite::Row<'_>) -> rusqlite::Result<PointRow> {
|
|||||||
last_error: r.get(16)?,
|
last_error: r.get(16)?,
|
||||||
last_elapsed_sec: r.get(17)?,
|
last_elapsed_sec: r.get(17)?,
|
||||||
synspec_success_method: r.get(19)?,
|
synspec_success_method: r.get(19)?,
|
||||||
|
tlusty_status: r.get(20)?,
|
||||||
|
synspec_status: r.get(21)?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1824,6 +1852,7 @@ mod tests {
|
|||||||
convergence_min_ratio: None,
|
convergence_min_ratio: None,
|
||||||
bfac_max: None,
|
bfac_max: None,
|
||||||
bfac_min: None,
|
bfac_min: None,
|
||||||
|
linelist: None,
|
||||||
};
|
};
|
||||||
let syn_task = Uuid::new_v4();
|
let syn_task = Uuid::new_v4();
|
||||||
let old_task = Uuid::new_v4();
|
let old_task = Uuid::new_v4();
|
||||||
|
|||||||
@@ -96,9 +96,12 @@ impl Database {
|
|||||||
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) AS completed,
|
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) AS completed,
|
||||||
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS failed,
|
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS failed,
|
||||||
SUM(CASE WHEN status = 'running' THEN 1 ELSE 0 END) AS running,
|
SUM(CASE WHEN status = 'running' THEN 1 ELSE 0 END) AS running,
|
||||||
SUM(CASE WHEN status = 'completed' AND tlusty_success_method = 'cold_run' THEN 1 ELSE 0 END) AS cold,
|
SUM(CASE WHEN tlusty_status = 'converged' AND tlusty_success_method = 'cold_run' THEN 1 ELSE 0 END) AS cold,
|
||||||
SUM(CASE WHEN status = 'completed' AND tlusty_success_method = 'seed_step' THEN 1 ELSE 0 END) AS seed,
|
SUM(CASE WHEN tlusty_status = 'converged' AND tlusty_success_method = 'seed_step' THEN 1 ELSE 0 END) AS seed,
|
||||||
SUM(CASE WHEN status = 'completed' AND synspec_success_method IS NOT NULL THEN 1 ELSE 0 END) AS synspec
|
SUM(CASE WHEN tlusty_status = 'failed' THEN 1 ELSE 0 END) AS tlusty_failed,
|
||||||
|
SUM(CASE WHEN synspec_status = 'converged' THEN 1 ELSE 0 END) AS synspec_converged,
|
||||||
|
SUM(CASE WHEN synspec_status = 'failed' THEN 1 ELSE 0 END) AS synspec_failed,
|
||||||
|
SUM(CASE WHEN synspec_status = 'pending' THEN 1 ELSE 0 END) AS synspec_pending
|
||||||
FROM grid_points GROUP BY workflow_name",
|
FROM grid_points GROUP BY workflow_name",
|
||||||
)?;
|
)?;
|
||||||
let agg_rows = agg_stmt.query_map([], |r| {
|
let agg_rows = agg_stmt.query_map([], |r| {
|
||||||
@@ -111,7 +114,10 @@ impl Database {
|
|||||||
running: r.get::<_, Option<i64>>(4)?.unwrap_or(0),
|
running: r.get::<_, Option<i64>>(4)?.unwrap_or(0),
|
||||||
cold_run_converged: r.get::<_, Option<i64>>(5)?.unwrap_or(0),
|
cold_run_converged: r.get::<_, Option<i64>>(5)?.unwrap_or(0),
|
||||||
seed_step_converged: r.get::<_, Option<i64>>(6)?.unwrap_or(0),
|
seed_step_converged: r.get::<_, Option<i64>>(6)?.unwrap_or(0),
|
||||||
synspec_converged: r.get::<_, Option<i64>>(7)?.unwrap_or(0),
|
tlusty_failed: r.get::<_, Option<i64>>(7)?.unwrap_or(0),
|
||||||
|
synspec_converged: r.get::<_, Option<i64>>(8)?.unwrap_or(0),
|
||||||
|
synspec_failed: r.get::<_, Option<i64>>(9)?.unwrap_or(0),
|
||||||
|
synspec_pending: r.get::<_, Option<i64>>(10)?.unwrap_or(0),
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|||||||
@@ -299,6 +299,14 @@ impl GridScheduler {
|
|||||||
.and_then(|t| serde_json::to_value(t).ok())
|
.and_then(|t| serde_json::to_value(t).ok())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 读取工作流 YAML 的 `linelist` 字段(SYNSPEC 谱线表文件名,如 "gfATO.dat")。
|
||||||
|
/// None → node 端用默认线表(embedded.rs 启动时下载的 default_linelist)。
|
||||||
|
async fn get_workflow_linelist(&self, workflow_name: &str) -> Option<String> {
|
||||||
|
let wf = self.db.get_workflow(workflow_name).await.ok()??;
|
||||||
|
let cfg = parse_grid_config_or_warn(&wf.config_yaml, workflow_name, "linelist")?;
|
||||||
|
cfg.linelist.clone()
|
||||||
|
}
|
||||||
|
|
||||||
/// 一次性读取工作流的全部物理校验阈值(能量守恒 / 温度结构 / emflux)。
|
/// 一次性读取工作流的全部物理校验阈值(能量守恒 / 温度结构 / emflux)。
|
||||||
/// 统一读取避免对同一 YAML 多次解析。返回 8 元组,对应 TaskSpec 的 8 个标量字段:
|
/// 统一读取避免对同一 YAML 多次解析。返回 8 元组,对应 TaskSpec 的 8 个标量字段:
|
||||||
/// (energy_tolerance, temp_max_factor, temp_floor, temp_ceiling, emflux_tolerance,
|
/// (energy_tolerance, temp_max_factor, temp_floor, temp_ceiling, emflux_tolerance,
|
||||||
@@ -451,6 +459,7 @@ impl GridScheduler {
|
|||||||
let tlusty_chain = self.get_workflow_tlusty_chain(workflow_name).await;
|
let tlusty_chain = self.get_workflow_tlusty_chain(workflow_name).await;
|
||||||
let seed_chain = self.get_workflow_seed_chain(workflow_name).await;
|
let seed_chain = self.get_workflow_seed_chain(workflow_name).await;
|
||||||
let tlusty_input = self.get_workflow_tlusty_input(workflow_name).await;
|
let tlusty_input = self.get_workflow_tlusty_input(workflow_name).await;
|
||||||
|
let linelist = self.get_workflow_linelist(workflow_name).await;
|
||||||
let (
|
let (
|
||||||
energy_tolerance,
|
energy_tolerance,
|
||||||
temp_max_factor,
|
temp_max_factor,
|
||||||
@@ -615,6 +624,7 @@ impl GridScheduler {
|
|||||||
convergence_min_ratio,
|
convergence_min_ratio,
|
||||||
bfac_max,
|
bfac_max,
|
||||||
bfac_min,
|
bfac_min,
|
||||||
|
linelist: linelist.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.db.insert_task(&task_spec).await?;
|
self.db.insert_task(&task_spec).await?;
|
||||||
@@ -904,6 +914,7 @@ impl GridScheduler {
|
|||||||
let tlusty_chain = self.get_workflow_tlusty_chain(workflow_name).await;
|
let tlusty_chain = self.get_workflow_tlusty_chain(workflow_name).await;
|
||||||
let seed_chain = self.get_workflow_seed_chain(workflow_name).await;
|
let seed_chain = self.get_workflow_seed_chain(workflow_name).await;
|
||||||
let tlusty_input = self.get_workflow_tlusty_input(workflow_name).await;
|
let tlusty_input = self.get_workflow_tlusty_input(workflow_name).await;
|
||||||
|
let linelist = self.get_workflow_linelist(workflow_name).await;
|
||||||
let (
|
let (
|
||||||
energy_tolerance,
|
energy_tolerance,
|
||||||
temp_max_factor,
|
temp_max_factor,
|
||||||
@@ -968,6 +979,7 @@ impl GridScheduler {
|
|||||||
convergence_min_ratio: None,
|
convergence_min_ratio: None,
|
||||||
bfac_max: None,
|
bfac_max: None,
|
||||||
bfac_min: None,
|
bfac_min: None,
|
||||||
|
linelist: linelist.clone(),
|
||||||
};
|
};
|
||||||
self.db.insert_task(&task_spec).await?;
|
self.db.insert_task(&task_spec).await?;
|
||||||
self.db
|
self.db
|
||||||
@@ -1062,6 +1074,7 @@ impl GridScheduler {
|
|||||||
convergence_min_ratio,
|
convergence_min_ratio,
|
||||||
bfac_max,
|
bfac_max,
|
||||||
bfac_min,
|
bfac_min,
|
||||||
|
linelist: linelist.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.db.insert_task(&task_spec).await?;
|
self.db.insert_task(&task_spec).await?;
|
||||||
|
|||||||
@@ -67,6 +67,12 @@ const TOKEN_KEY = 'dcts_admin_token';
|
|||||||
* @property {number} running
|
* @property {number} running
|
||||||
* @property {number} completed
|
* @property {number} completed
|
||||||
* @property {number} failed
|
* @property {number} failed
|
||||||
|
* @property {number} cold_run_converged - TLUSTY 视图:冷启动收敛
|
||||||
|
* @property {number} seed_step_converged - TLUSTY 视图:种子步进收敛
|
||||||
|
* @property {number} tlusty_failed - TLUSTY 视图:大气发散
|
||||||
|
* @property {number} synspec_converged - SYNSPEC 视图:光谱有效
|
||||||
|
* @property {number} synspec_failed - SYNSPEC 视图:光谱失败
|
||||||
|
* @property {number} synspec_pending - SYNSPEC 视图:未运行
|
||||||
* @property {number} [eta_sec]
|
* @property {number} [eta_sec]
|
||||||
* @property {Array<{label:string,count:number}>} [waves]
|
* @property {Array<{label:string,count:number}>} [waves]
|
||||||
*/
|
*/
|
||||||
@@ -76,6 +82,8 @@ const TOKEN_KEY = 'dcts_admin_token';
|
|||||||
* @property {string} name
|
* @property {string} name
|
||||||
* @property {string} status
|
* @property {string} status
|
||||||
* @property {string} [method]
|
* @property {string} [method]
|
||||||
|
* @property {string} [tlusty_status] - 'converged'/'failed'/'pending'/null
|
||||||
|
* @property {string} [synspec_status] - 'converged'/'failed'/'pending'/null
|
||||||
* @property {number} [relc]
|
* @property {number} [relc]
|
||||||
* @property {number} [runtime_sec]
|
* @property {number} [runtime_sec]
|
||||||
* @property {number} [attempts]
|
* @property {number} [attempts]
|
||||||
|
|||||||
@@ -1043,6 +1043,8 @@ body::before {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.seg-converged { background: var(--color-success); }
|
.seg-converged { background: var(--color-success); }
|
||||||
|
/* 种子步进段(TLUSTY 视图):紫色,与冷启动绿区分收敛策略。 */
|
||||||
|
.seg-seed { background: var(--accent-purple); }
|
||||||
/* 色盲友好(color-not-only):失败段在红色之上叠加斜线纹理,
|
/* 色盲友好(color-not-only):失败段在红色之上叠加斜线纹理,
|
||||||
即使无法分辨红/绿也能凭纹理识别失败占比。 */
|
即使无法分辨红/绿也能凭纹理识别失败占比。 */
|
||||||
.seg-failed {
|
.seg-failed {
|
||||||
@@ -1056,6 +1058,39 @@ body::before {
|
|||||||
.seg-queued { background: var(--color-info); }
|
.seg-queued { background: var(--color-info); }
|
||||||
.seg-pending { background: var(--border-default); }
|
.seg-pending { background: var(--border-default); }
|
||||||
|
|
||||||
|
/* 阶段切换器(TLUSTY/SYNSPEC 双视图)+ 计数行布局 */
|
||||||
|
.wf-strip-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-3);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.phase-switcher {
|
||||||
|
display: inline-flex;
|
||||||
|
border: 1px solid var(--border-default);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.phase-tab {
|
||||||
|
appearance: none;
|
||||||
|
border: none;
|
||||||
|
padding: var(--space-1) var(--space-3);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
background: var(--bg-surface);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s, color 0.15s;
|
||||||
|
}
|
||||||
|
.phase-tab + .phase-tab { border-left: 1px solid var(--border-default); }
|
||||||
|
.phase-tab:hover { background: var(--bg-subtle); }
|
||||||
|
.phase-tab.active {
|
||||||
|
color: var(--color-success);
|
||||||
|
background: var(--color-success-bg);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
.wf-strip-counts {
|
.wf-strip-counts {
|
||||||
font-size: var(--font-size-sm);
|
font-size: var(--font-size-sm);
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
@@ -1155,6 +1190,35 @@ body::before {
|
|||||||
border-color: var(--color-danger);
|
border-color: var(--color-danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 阶段状态徽标(单点详情面板顶部):TLUSTY ✓/✗ · SYNSPEC ✓/✗
|
||||||
|
半失败点显示「整体失败 · TLUSTY ✓ · SYNSPEC ✗」,让阶段级成败一眼可见。 */
|
||||||
|
.stage-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1px var(--space-2);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
border: 1px solid transparent;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.stage-badge.stage-ok {
|
||||||
|
color: var(--color-success);
|
||||||
|
background: var(--color-success-bg);
|
||||||
|
border-color: var(--color-success);
|
||||||
|
}
|
||||||
|
.stage-badge.stage-fail {
|
||||||
|
color: var(--color-danger);
|
||||||
|
background: var(--color-danger-bg);
|
||||||
|
border-color: var(--color-danger);
|
||||||
|
}
|
||||||
|
.stage-badge.stage-pending {
|
||||||
|
color: var(--text-muted);
|
||||||
|
background: var(--bg-subtle);
|
||||||
|
border-color: var(--border-default);
|
||||||
|
}
|
||||||
|
|
||||||
/* 状态徽章补充变体(失败=红 / 排队=蓝) */
|
/* 状态徽章补充变体(失败=红 / 排队=蓝) */
|
||||||
.status-badge.danger {
|
.status-badge.danger {
|
||||||
color: var(--color-danger);
|
color: var(--color-danger);
|
||||||
@@ -1376,6 +1440,7 @@ body::before {
|
|||||||
.ps-cold { fill: var(--color-success); }
|
.ps-cold { fill: var(--color-success); }
|
||||||
.ps-seed { fill: var(--accent-purple); }
|
.ps-seed { fill: var(--accent-purple); }
|
||||||
.ps-synspec { fill: var(--accent-teal); }
|
.ps-synspec { fill: var(--accent-teal); }
|
||||||
|
.ps-converged { fill: var(--color-success); }
|
||||||
.ps-failed { fill: var(--color-danger); }
|
.ps-failed { fill: var(--color-danger); }
|
||||||
.ps-running { fill: var(--color-warning); }
|
.ps-running { fill: var(--color-warning); }
|
||||||
.ps-queued { fill: var(--color-info); }
|
.ps-queued { fill: var(--color-info); }
|
||||||
@@ -1384,6 +1449,7 @@ body::before {
|
|||||||
.ps-seg.ps-cold { fill: var(--color-success); }
|
.ps-seg.ps-cold { fill: var(--color-success); }
|
||||||
.ps-seg.ps-seed { fill: var(--accent-purple); }
|
.ps-seg.ps-seed { fill: var(--accent-purple); }
|
||||||
.ps-seg.ps-synspec { fill: var(--accent-teal); }
|
.ps-seg.ps-synspec { fill: var(--accent-teal); }
|
||||||
|
.ps-seg.ps-converged { fill: var(--color-success); }
|
||||||
.ps-seg.ps-failed { fill: var(--color-danger); }
|
.ps-seg.ps-failed { fill: var(--color-danger); }
|
||||||
.ps-seg.ps-running { fill: var(--color-warning); }
|
.ps-seg.ps-running { fill: var(--color-warning); }
|
||||||
.ps-seg.ps-queued { fill: var(--color-info); }
|
.ps-seg.ps-queued { fill: var(--color-info); }
|
||||||
@@ -1404,6 +1470,7 @@ body::before {
|
|||||||
.ps-vlabel-cold { fill: var(--color-success); font-weight: 600; }
|
.ps-vlabel-cold { fill: var(--color-success); font-weight: 600; }
|
||||||
.ps-vlabel-seed { fill: var(--accent-purple); font-weight: 600; }
|
.ps-vlabel-seed { fill: var(--accent-purple); font-weight: 600; }
|
||||||
.ps-vlabel-synspec { fill: var(--accent-teal); font-weight: 600; }
|
.ps-vlabel-synspec { fill: var(--accent-teal); font-weight: 600; }
|
||||||
|
.ps-vlabel-converged { fill: var(--color-success); font-weight: 600; }
|
||||||
.ps-vlabel-failed { fill: var(--color-danger); font-weight: 600; }
|
.ps-vlabel-failed { fill: var(--color-danger); font-weight: 600; }
|
||||||
.ps-vlabel-running { fill: var(--color-warning); font-weight: 600; }
|
.ps-vlabel-running { fill: var(--color-warning); font-weight: 600; }
|
||||||
.ps-vlabel-queued { fill: var(--color-info); font-weight: 600; }
|
.ps-vlabel-queued { fill: var(--color-info); font-weight: 600; }
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ export function createDetailCtx(name) {
|
|||||||
|
|
||||||
// ===== Tab1 执行概览 =====
|
// ===== Tab1 执行概览 =====
|
||||||
activeTab: 'overview',
|
activeTab: 'overview',
|
||||||
|
// 阶段视图切换('tlusty' 大气 / 'synspec' 光谱):概览分段进度条/指标卡 + parSets 状态轴共用。
|
||||||
|
// 切换按钮在详情页顶部 strip,默认 TLUSTY 视图。
|
||||||
|
phaseView: 'tlusty',
|
||||||
latestStats: null,
|
latestStats: null,
|
||||||
overviewBuilt: false, // 概览骨架仅首次构建,其后轮询原位 diff
|
overviewBuilt: false, // 概览骨架仅首次构建,其后轮询原位 diff
|
||||||
lastWavesSig: null,
|
lastWavesSig: null,
|
||||||
|
|||||||
@@ -14,30 +14,57 @@ import { fetchWorkflowPointsApi, fetchWorkflowProgressApi } from '../../api.js';
|
|||||||
import { isAbortError, logError } from '../../utils/errors.js';
|
import { isAbortError, logError } from '../../utils/errors.js';
|
||||||
import { ICONS } from '../../utils/icons.js';
|
import { ICONS } from '../../utils/icons.js';
|
||||||
|
|
||||||
/** 分段进度条档位:顺序与计数行一致。五段常驻(空档宽度 0),原位改 width 复用 CSS 过渡。 */
|
/** 分段进度条档位定义——TLUSTY/SYNSPEC 双视图各一套(按 ctx.phaseView 切换)。
|
||||||
const SEG_DEFS = [
|
* TLUSTY 视图:cold/seed(大气收敛,按策略拆)+ failed(大气发散)+ running/queued/pending。
|
||||||
['completed', 'seg-converged', '完成'],
|
* SYNSPEC 视图:converged(光谱有效)+ failed(光谱坏)+ pending(未运行)。 */
|
||||||
['failed', 'seg-failed', '失败'],
|
const SEG_DEFS_TLUSTY = [
|
||||||
|
['cold', 'seg-converged', '冷启动收敛'],
|
||||||
|
['seed', 'seg-seed', '种子步进'],
|
||||||
|
['failed', 'seg-failed', '大气发散'],
|
||||||
['running', 'seg-running', '运行'],
|
['running', 'seg-running', '运行'],
|
||||||
['queued', 'seg-queued', '排队'],
|
['queued', 'seg-queued', '排队'],
|
||||||
['pending', 'seg-pending', '待定'],
|
['pending', 'seg-pending', '待定'],
|
||||||
];
|
];
|
||||||
|
const SEG_DEFS_SYNSPEC = [
|
||||||
|
['converged', 'seg-converged', '光谱有效'],
|
||||||
|
['failed', 'seg-failed', '光谱失败'],
|
||||||
|
['pending', 'seg-pending', '未运行'],
|
||||||
|
];
|
||||||
|
|
||||||
/** 任务控制条:状态徽章 / 分段进度条 / 计数与 ETA。
|
/** 选中阶段的档位定义。stats 的字段名映射:cold←cold_run_converged, seed←seed_step_converged,
|
||||||
*
|
* failed←(tlusty_failed|synspec_failed), converged←synspec_converged, pending←(pending|synspec_pending)。 */
|
||||||
* 操作按钮(启动/暂停/删除/查看)已移除——全部收敛至内嵌引擎面板
|
function segDefsFor(phase) {
|
||||||
* (wfEnginePanel.js,见 docs/task_engine_decoupling_design.md §6)。
|
return phase === 'synspec' ? SEG_DEFS_SYNSPEC : SEG_DEFS_TLUSTY;
|
||||||
* 剩余元素签名 diff 后原位更新,轮询不重建 DOM。 */
|
}
|
||||||
export function renderStrip(s) {
|
|
||||||
|
/** 把 stats 字段值映射到档位计数。phase 决定 failed/pending 取哪个字段。 */
|
||||||
|
function segCount(s, key, phase) {
|
||||||
|
if (key === 'cold') return s.cold_run_converged || 0;
|
||||||
|
if (key === 'seed') return s.seed_step_converged || 0;
|
||||||
|
if (key === 'converged') return s.synspec_converged || 0;
|
||||||
|
if (key === 'failed') return phase === 'synspec' ? (s.synspec_failed || 0) : (s.tlusty_failed || 0);
|
||||||
|
if (key === 'pending') return phase === 'synspec' ? (s.synspec_pending || 0) : (s.pending || 0);
|
||||||
|
if (key === 'running') return s.running || 0;
|
||||||
|
if (key === 'queued') return s.queued || 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 任务控制条:状态徽章 / 阶段切换器 / 分段进度条 / 计数与 ETA。
|
||||||
|
* phase 参数('tlusty'|'synspec')决定分段进度条与计数行用哪套阶段档位。 */
|
||||||
|
export function renderStrip(s, phase = 'tlusty') {
|
||||||
const statusEl = document.getElementById('wf-detail-status');
|
const statusEl = document.getElementById('wf-detail-status');
|
||||||
if (statusEl && statusEl.getAttribute('data-status') !== s.status) {
|
if (statusEl && statusEl.getAttribute('data-status') !== s.status) {
|
||||||
statusEl.setAttribute('data-status', s.status);
|
statusEl.setAttribute('data-status', s.status);
|
||||||
statusEl.innerHTML = statusBadge(s.status);
|
statusEl.innerHTML = statusBadge(s.status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SEG_DEFS = segDefsFor(phase);
|
||||||
const segEl = document.getElementById('wf-seg-progress');
|
const segEl = document.getElementById('wf-seg-progress');
|
||||||
if (segEl) {
|
if (segEl) {
|
||||||
if (!segEl.firstElementChild) {
|
// 档位定义随阶段切换变化 → 重建分段(切换阶段时 sig 变化触发重建,同阶段轮询复用)。
|
||||||
|
const sig = SEG_DEFS.map(d => d[0]).join(',');
|
||||||
|
if (segEl.getAttribute('data-seg-sig') !== sig) {
|
||||||
|
segEl.setAttribute('data-seg-sig', sig);
|
||||||
segEl.innerHTML = SEG_DEFS.map(([key, cls, label]) =>
|
segEl.innerHTML = SEG_DEFS.map(([key, cls, label]) =>
|
||||||
`<div class="seg ${cls}" data-seg="${key}" style="width:0%" title="${label} 0"></div>`).join('');
|
`<div class="seg ${cls}" data-seg="${key}" style="width:0%" title="${label} 0"></div>`).join('');
|
||||||
}
|
}
|
||||||
@@ -45,8 +72,7 @@ export function renderStrip(s) {
|
|||||||
SEG_DEFS.forEach(([key, , label]) => {
|
SEG_DEFS.forEach(([key, , label]) => {
|
||||||
const el = segEl.querySelector(`[data-seg="${key}"]`);
|
const el = segEl.querySelector(`[data-seg="${key}"]`);
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
const n = s[key] || 0;
|
const n = segCount(s, key, phase);
|
||||||
// total=0 时 pending 占满整条,沿用旧版「暂无网格点」语义
|
|
||||||
const w = total > 0 ? (n / total) * 100 : (key === 'pending' ? 100 : 0);
|
const w = total > 0 ? (n / total) * 100 : (key === 'pending' ? 100 : 0);
|
||||||
const wStr = `${w}%`;
|
const wStr = `${w}%`;
|
||||||
if (el.style.width !== wStr) el.style.width = wStr;
|
if (el.style.width !== wStr) el.style.width = wStr;
|
||||||
@@ -57,15 +83,20 @@ export function renderStrip(s) {
|
|||||||
|
|
||||||
const countsEl = document.getElementById('wf-strip-counts');
|
const countsEl = document.getElementById('wf-strip-counts');
|
||||||
if (countsEl) {
|
if (countsEl) {
|
||||||
const pct = s.total ? Math.round((s.completed / s.total) * 100) : 0;
|
const parts = [`${s.total} 网格点`];
|
||||||
const parts = [
|
if (phase === 'synspec') {
|
||||||
`${s.total} 网格点`,
|
parts.push(`${s.synspec_converged || 0} 光谱有效`);
|
||||||
`${s.completed} 完成${s.total ? ` (${pct}%)` : ''}`,
|
parts.push(`${s.synspec_failed || 0} 光谱失败`);
|
||||||
`${s.running} 运行`,
|
parts.push(`${s.synspec_pending || 0} 未运行`);
|
||||||
`${s.queued} 排队`,
|
} else {
|
||||||
`${s.pending} 待定`,
|
const conv = (s.cold_run_converged || 0) + (s.seed_step_converged || 0);
|
||||||
`${s.failed} 失败`,
|
const pct = s.total ? Math.round((conv / s.total) * 100) : 0;
|
||||||
];
|
parts.push(`${conv} 大气收敛${s.total ? ` (${pct}%)` : ''}`);
|
||||||
|
parts.push(`${s.tlusty_failed || 0} 大气发散`);
|
||||||
|
parts.push(`${s.running} 运行`);
|
||||||
|
if (s.queued) parts.push(`${s.queued} 排队`);
|
||||||
|
if (s.pending) parts.push(`${s.pending} 待定`);
|
||||||
|
}
|
||||||
if (s.eta_sec != null) parts.push(`ETA ≈ ${fmtDuration(s.eta_sec)}`);
|
if (s.eta_sec != null) parts.push(`ETA ≈ ${fmtDuration(s.eta_sec)}`);
|
||||||
const text = parts.join(' · ');
|
const text = parts.join(' · ');
|
||||||
if (countsEl.textContent !== text) countsEl.textContent = text;
|
if (countsEl.textContent !== text) countsEl.textContent = text;
|
||||||
@@ -79,12 +110,46 @@ export function renderOverview(ctx, s) {
|
|||||||
if (!ctx.overviewBuilt) {
|
if (!ctx.overviewBuilt) {
|
||||||
ctx.overviewBuilt = true;
|
ctx.overviewBuilt = true;
|
||||||
panel.innerHTML = overviewSkeleton();
|
panel.innerHTML = overviewSkeleton();
|
||||||
|
bindPhaseSwitcher(ctx);
|
||||||
}
|
}
|
||||||
updateOverview(ctx, s);
|
updateOverview(ctx, s);
|
||||||
refreshActivityFeed(ctx);
|
refreshActivityFeed(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 概览骨架:动态部分全部挂 ID,供 updateOverview 原位更新。 */
|
/** 阶段切换器事件绑定:TLUSTY/SYNSPEC 双视图。切换时重渲染 strip + overview + parSets。 */
|
||||||
|
function bindPhaseSwitcher(ctx) {
|
||||||
|
document.querySelectorAll('.phase-tab').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const phase = btn.getAttribute('data-phase');
|
||||||
|
if (!phase || phase === ctx.phaseView) return;
|
||||||
|
ctx.phaseView = phase;
|
||||||
|
document.querySelectorAll('.phase-tab').forEach(b => {
|
||||||
|
const active = b.getAttribute('data-phase') === phase;
|
||||||
|
b.classList.toggle('active', active);
|
||||||
|
b.setAttribute('aria-selected', String(active));
|
||||||
|
});
|
||||||
|
// 重渲染 strip(分段进度条 + 计数行)+ 概览指标卡
|
||||||
|
if (ctx.latestStats) {
|
||||||
|
renderStrip(ctx.latestStats, phase);
|
||||||
|
updateOverview(ctx, ctx.latestStats);
|
||||||
|
}
|
||||||
|
// 重渲染 parSets(如果分析 Tab 已有数据)
|
||||||
|
const container = document.getElementById('ps-container');
|
||||||
|
if (container && ctx.psState?.points?.length > 0) {
|
||||||
|
// 触发 parSets 重渲染(renderParSets 在 parSets.js 内部,经 renderAnalysisTab 间接调用)
|
||||||
|
// 这里直接 import 会循环依赖,用事件委托:parSets 的 renderAnalysisTab 检测 phaseView 变化时重渲染。
|
||||||
|
// 简化:切到 analysis Tab 时 renderAnalysisTab 会读 ctx.phaseView,已切换则用新档位。
|
||||||
|
// 若当前已在 analysis Tab,手动触发一次渲染。
|
||||||
|
if (ctx.activeTab === 'analysis') {
|
||||||
|
window.dispatchEvent(new CustomEvent('dcts-phase-change', { detail: { phase } }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 概览骨架:动态部分全部挂 ID,供 updateOverview 原位更新。
|
||||||
|
* 指标卡按阶段切换语义——"阶段收敛/失败"随 ctx.phaseView 变化。 */
|
||||||
function overviewSkeleton() {
|
function overviewSkeleton() {
|
||||||
return `
|
return `
|
||||||
<section class="metrics-grid">
|
<section class="metrics-grid">
|
||||||
@@ -94,14 +159,14 @@ function overviewSkeleton() {
|
|||||||
<span class="metric-sub">6 维参数笛卡尔积展开</span>
|
<span class="metric-sub">6 维参数笛卡尔积展开</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="metric-card card">
|
<div class="metric-card card">
|
||||||
<span class="metric-title">已完成</span>
|
<span class="metric-title" id="ov-m-converged-title">大气收敛</span>
|
||||||
<span class="metric-value tabular-num" id="ov-m-converged">—</span>
|
<span class="metric-value tabular-num" id="ov-m-converged">—</span>
|
||||||
<span class="metric-sub" id="ov-m-converged-sub">—</span>
|
<span class="metric-sub" id="ov-m-converged-sub">—</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="metric-card card">
|
<div class="metric-card card">
|
||||||
<span class="metric-title">失败</span>
|
<span class="metric-title" id="ov-m-failed-title">大气发散</span>
|
||||||
<span class="metric-value tabular-num" id="ov-m-failed">—</span>
|
<span class="metric-value tabular-num" id="ov-m-failed">—</span>
|
||||||
<span class="metric-sub">冷启动发散且无种子可救</span>
|
<span class="metric-sub" id="ov-m-failed-sub">—</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="metric-card card">
|
<div class="metric-card card">
|
||||||
<span class="metric-title">运行 + 排队</span>
|
<span class="metric-title">运行 + 排队</span>
|
||||||
@@ -142,17 +207,31 @@ function setOvText(id, v) {
|
|||||||
if (el && el.textContent !== str) el.textContent = str;
|
if (el && el.textContent !== str) el.textContent = str;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 轮询原位刷新概览数值(指标卡 + 归因徽章 + 波次)。 */
|
/** 轮询原位刷新概览数值(指标卡 + 波次)。按 ctx.phaseView 切换阶段语义。 */
|
||||||
function updateOverview(ctx, s) {
|
function updateOverview(ctx, s) {
|
||||||
const pct = s.total ? Math.round((s.completed / s.total) * 100) : 0;
|
const phase = ctx.phaseView || 'tlusty';
|
||||||
setOvText('ov-m-total', s.total);
|
setOvText('ov-m-total', s.total);
|
||||||
setOvText('ov-m-converged', s.completed);
|
setOvText('ov-m-runqueue', (s.running || 0) + (s.queued || 0));
|
||||||
setOvText('ov-m-converged-sub', `完成率 ${pct}%`);
|
setOvText('ov-m-runqueue-sub', `${s.running || 0} 运行 · ${s.queued || 0} 排队`);
|
||||||
setOvText('ov-m-failed', s.failed);
|
if (phase === 'synspec') {
|
||||||
setOvText('ov-m-runqueue', s.running + s.queued);
|
const conv = s.synspec_converged || 0;
|
||||||
setOvText('ov-m-runqueue-sub', `${s.running} 运行 · ${s.queued} 排队`);
|
const pct = s.total ? Math.round((conv / s.total) * 100) : 0;
|
||||||
// 收敛手段归因面板已移除(见 docs/task_engine_decoupling_design.md §6.1),
|
setOvText('ov-m-converged-title', '光谱有效');
|
||||||
// cold/seed 计数仍在点表明细与 parSets 中可用,概览不再单独展示。
|
setOvText('ov-m-converged', conv);
|
||||||
|
setOvText('ov-m-converged-sub', `光谱成功率 ${pct}%`);
|
||||||
|
setOvText('ov-m-failed-title', '光谱失败');
|
||||||
|
setOvText('ov-m-failed', s.synspec_failed || 0);
|
||||||
|
setOvText('ov-m-failed-sub', `未运行 ${s.synspec_pending || 0}`);
|
||||||
|
} else {
|
||||||
|
const conv = (s.cold_run_converged || 0) + (s.seed_step_converged || 0);
|
||||||
|
const pct = s.total ? Math.round((conv / s.total) * 100) : 0;
|
||||||
|
setOvText('ov-m-converged-title', '大气收敛');
|
||||||
|
setOvText('ov-m-converged', conv);
|
||||||
|
setOvText('ov-m-converged-sub', `冷启动 ${s.cold_run_converged || 0} · 种子步进 ${s.seed_step_converged || 0}(${pct}%)`);
|
||||||
|
setOvText('ov-m-failed-title', '大气发散');
|
||||||
|
setOvText('ov-m-failed', s.tlusty_failed || 0);
|
||||||
|
setOvText('ov-m-failed-sub', '收敛阈值未达标且无种子可救');
|
||||||
|
}
|
||||||
updateWaves(ctx, s);
|
updateWaves(ctx, s);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,30 +58,45 @@ export function restorePsCache(ctx) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 状态轴档位(收敛拆冷启动/种子步进/SYNSPEC,物理意义不同:稳定区 vs 救回区 vs 光谱重算)。
|
/** 状态轴档位定义——TLUSTY/SYNSPEC 双视图各一套。
|
||||||
* 顺序即图例/轴布局/每值内分段/色带绘制层叠的唯一来源,语义约定:
|
* 顺序即图例/轴布局/每值内分段/色带绘制层叠的唯一来源,语义约定:
|
||||||
* 已收敛(最受关注)置顶 → 进行中按任务进展方向(running→queued→pending)→ 失败沉底。
|
* 已收敛(最受关注)置顶 → 进行中按任务进展方向(running→queued→pending)→ 失败沉底。
|
||||||
* 色带绘制按此顺序:收敛色带先画(底层),失败最后画(最上层,数量少且颜色醒目)。
|
* 色带绘制按此顺序:收敛色带先画(底层),失败最后画(最上层,数量少且颜色醒目)。
|
||||||
* synspec 档:synspec_success_method 非空(如 synspec-only 任务的光谱策略 "standard"),
|
*
|
||||||
* 属已收敛但由光谱合成阶段产出,需与大气收敛区分。 */
|
* TLUSTY 视图(大气阶段):基于 tlusty_status + tlusty_success_method。
|
||||||
const PS_STATUS = ['cold', 'seed', 'synspec', 'running', 'queued', 'pending', 'failed'];
|
* cold/seed = 大气收敛(按策略拆),failed = 大气发散,running/queued/pending = 未跑完。
|
||||||
|
* SYNSPEC 视图(光谱阶段):基于 synspec_status。
|
||||||
|
* converged = 光谱有效,failed = 光谱坏(Balmer edge NaN 等),pending = 未运行(多因 tlusty 未收敛阻塞)。 */
|
||||||
|
const PS_STATUS_TLUSTY = ['cold', 'seed', 'failed', 'running', 'queued', 'pending'];
|
||||||
|
const PS_STATUS_SYNSPEC = ['converged', 'failed', 'pending'];
|
||||||
const PS_STATUS_LABEL = {
|
const PS_STATUS_LABEL = {
|
||||||
cold: '冷启动收敛', seed: '种子步进', synspec: 'SYNSPEC 合成', failed: '失败',
|
cold: '冷启动收敛', seed: '种子步进', failed: '失败',
|
||||||
running: '运行', queued: '排队', pending: '未开始',
|
running: '运行', queued: '排队', pending: '未开始',
|
||||||
|
converged: '光谱有效',
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 点 → 状态档位 key。
|
/** 当前选中阶段的档位列表(renderParSets 读 ctx.phaseView 切换)。 */
|
||||||
* 阶段归因列拆分(P9)后可直接区分:TLUSTY 策略看 tlusty_success_method,
|
function psStatusList(phase) {
|
||||||
* SYNSPEC-only 点看 synspec_success_method——不再需要猜策略名(原 SYNSPEC_METHODS hack)。 */
|
return phase === 'synspec' ? PS_STATUS_SYNSPEC : PS_STATUS_TLUSTY;
|
||||||
function psSlot(p) {
|
|
||||||
if (p.status === 'completed') {
|
|
||||||
if (p.tlusty_success_method === 'seed_step') return 'seed';
|
|
||||||
if (p.tlusty_success_method === 'cold_run') return 'cold';
|
|
||||||
// 光谱阶段收敛(synspec-only/双阶段光谱归因落库)→ SYNSPEC 合成档。
|
|
||||||
if (p.synspec_success_method) return 'synspec';
|
|
||||||
return 'cold';
|
|
||||||
}
|
}
|
||||||
if (p.status === 'failed') return 'failed';
|
|
||||||
|
/** 点 → 状态档位 key(按选中阶段映射)。
|
||||||
|
* TLUSTY 阶段:tlusty_status='converged' 按 tlusty_success_method 拆 cold/seed;'failed'→failed;
|
||||||
|
* 否则按 grid_points.status 归 running/queued/pending。
|
||||||
|
* SYNSPEC 阶段:synspec_status='converged'→converged;'failed'→failed;其余→pending(含 NULL)。 */
|
||||||
|
function psSlot(p, phase) {
|
||||||
|
if (phase === 'synspec') {
|
||||||
|
const s = p.synspec_status;
|
||||||
|
if (s === 'converged') return 'converged';
|
||||||
|
if (s === 'failed') return 'failed';
|
||||||
|
return 'pending'; // pending / NULL / 未知 → 未运行
|
||||||
|
}
|
||||||
|
// TLUSTY 视图
|
||||||
|
if (p.tlusty_status === 'converged') {
|
||||||
|
if (p.tlusty_success_method === 'seed_step') return 'seed';
|
||||||
|
return 'cold'; // cold_run 或缺省归冷启动
|
||||||
|
}
|
||||||
|
if (p.tlusty_status === 'failed') return 'failed';
|
||||||
if (p.status === 'running') return 'running';
|
if (p.status === 'running') return 'running';
|
||||||
if (p.status === 'queued') return 'queued';
|
if (p.status === 'queued') return 'queued';
|
||||||
return 'pending';
|
return 'pending';
|
||||||
@@ -96,9 +111,29 @@ function psFmtDim(dim, v) {
|
|||||||
return n.toFixed(0); // loghe/logc/logn/logo 都是整数采样
|
return n.toFixed(0); // loghe/logc/logn/logo 都是整数采样
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 卸载时清理 parSets 模块注册的 window 级监听器,防止页面离开后旧 ctx 闭包驻留内存
|
||||||
|
* 并对游离 DOM 执行无效重绘(GC Leak)。由 workflowDetail.unmountWorkflowDetail 调用。 */
|
||||||
|
export function cleanupParSetsListeners(ctx) {
|
||||||
|
if (ctx?._psPhaseListener) {
|
||||||
|
window.removeEventListener('dcts-phase-change', ctx._psPhaseListener);
|
||||||
|
ctx._psPhaseListener = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function renderAnalysisTab(ctx) {
|
export function renderAnalysisTab(ctx) {
|
||||||
const el = document.getElementById('wf-tab-analysis');
|
const el = document.getElementById('wf-tab-analysis');
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
|
// 阶段切换器事件:概览页的 TLUSTY/SYNSPEC 切换会派发 dcts-phase-change,
|
||||||
|
// 若分析 Tab 已构建且已有数据,按新阶段重渲染平行集合图。
|
||||||
|
if (!ctx._psPhaseListener) {
|
||||||
|
ctx._psPhaseListener = (e) => {
|
||||||
|
if (ctx.psBuilt && ctx.psState?.points?.length > 0) {
|
||||||
|
renderParSets(ctx);
|
||||||
|
renderPsConclusion(ctx, ctx.psState.points);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('dcts-phase-change', ctx._psPhaseListener);
|
||||||
|
}
|
||||||
if (!ctx.psBuilt) {
|
if (!ctx.psBuilt) {
|
||||||
ctx.psBuilt = true;
|
ctx.psBuilt = true;
|
||||||
el.innerHTML = `
|
el.innerHTML = `
|
||||||
@@ -175,8 +210,10 @@ function renderParSets(ctx) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const phase = ctx.phaseView || 'tlusty';
|
||||||
|
const PS_STATUS = psStatusList(phase);
|
||||||
const dims = [...PS_DIMS, 'status'];
|
const dims = [...PS_DIMS, 'status'];
|
||||||
// 每维取值列表(status 用 PS_STATUS 顺序)
|
// 每维取值列表(status 用当前阶段的 PS_STATUS 顺序)
|
||||||
const dimValues = {};
|
const dimValues = {};
|
||||||
PS_DIMS.forEach(d => { dimValues[d] = psDimValues(points, d); });
|
PS_DIMS.forEach(d => { dimValues[d] = psDimValues(points, d); });
|
||||||
dimValues.status = PS_STATUS;
|
dimValues.status = PS_STATUS;
|
||||||
@@ -190,7 +227,7 @@ function renderParSets(ctx) {
|
|||||||
PS_DIMS.forEach(d => { ptNum[d] = new Float64Array(nPts); });
|
PS_DIMS.forEach(d => { ptNum[d] = new Float64Array(nPts); });
|
||||||
for (let i = 0; i < nPts; i++) {
|
for (let i = 0; i < nPts; i++) {
|
||||||
const p = points[i];
|
const p = points[i];
|
||||||
ptSlotArr[i] = psSlot(p);
|
ptSlotArr[i] = psSlot(p, phase);
|
||||||
for (const d of PS_DIMS) ptNum[d][i] = Number(p[d]);
|
for (const d of PS_DIMS) ptNum[d][i] = Number(p[d]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,16 +402,17 @@ export function reflowParSets(ctx) {
|
|||||||
if (c && Math.abs((c.clientWidth || 0) - prev) > 20) renderParSets(ctx);
|
if (c && Math.abs((c.clientWidth || 0) - prev) > 20) renderParSets(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 自动洞察:扫各维度取值,挑失败率显著高于均值的位置。 */
|
/** 自动洞察:扫各维度取值,挑失败率显著高于均值的位置。按选中阶段用对应档位计数。 */
|
||||||
function renderPsConclusion(ctx, pts) {
|
function renderPsConclusion(ctx, pts) {
|
||||||
const el = document.getElementById('ps-conclusion');
|
const el = document.getElementById('ps-conclusion');
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
const total = pts.length;
|
const total = pts.length;
|
||||||
if (total === 0) { el.textContent = ''; return; }
|
if (total === 0) { el.textContent = ''; return; }
|
||||||
const cold = pts.filter(p => p.status === 'completed' && p.tlusty_success_method === 'cold_run').length;
|
const phase = ctx.phaseView || 'tlusty';
|
||||||
const seed = pts.filter(p => p.status === 'completed' && p.tlusty_success_method === 'seed_step').length;
|
// 按当前阶段统计各档位计数(与 psSlot 同口径)
|
||||||
const synspec = pts.filter(p => psSlot(p) === 'synspec').length;
|
const slotCount = {};
|
||||||
const failed = pts.filter(p => p.status === 'failed').length;
|
pts.forEach(p => { const s = psSlot(p, phase); slotCount[s] = (slotCount[s] || 0) + 1; });
|
||||||
|
const failed = slotCount.failed || 0;
|
||||||
const baseFail = failed / total;
|
const baseFail = failed / total;
|
||||||
|
|
||||||
const findings = [];
|
const findings = [];
|
||||||
@@ -388,17 +426,18 @@ function renderPsConclusion(ctx, pts) {
|
|||||||
});
|
});
|
||||||
[...groups.entries()].forEach(([v, g]) => {
|
[...groups.entries()].forEach(([v, g]) => {
|
||||||
if (g.length < 3) return;
|
if (g.length < 3) return;
|
||||||
const gf = g.filter(p => p.status === 'failed').length / g.length;
|
const gf = g.filter(p => psSlot(p, phase) === 'failed').length / g.length;
|
||||||
if (gf >= 0.3 && gf - baseFail >= 0.15) {
|
if (gf >= 0.3 && gf - baseFail >= 0.15) {
|
||||||
findings.push({ score: gf - baseFail, text: `⚠ ${d}=${psFmtDim(d, v)}:失败率 ${Math.round(gf * 100)}%(${g.length} 点,均值 ${Math.round(baseFail * 100)}%)` });
|
findings.push({ score: gf - baseFail, text: `⚠ ${d}=${psFmtDim(d, v)}:失败率 ${Math.round(gf * 100)}%(${g.length} 点,均值 ${Math.round(baseFail * 100)}%)` });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
findings.sort((a, b) => b.score - a.score);
|
findings.sort((a, b) => b.score - a.score);
|
||||||
const lines = [
|
// 结论行按阶段拼装:TLUSTY 视图拆 cold/seed/failed;SYNSPEC 视图拆 converged/failed/pending。
|
||||||
`冷启动 ${cold} · 种子步进 ${seed} · SYNSPEC ${synspec} · 失败 ${failed} / 共 ${total}`,
|
const summary = phase === 'synspec'
|
||||||
...findings.slice(0, 3).map(f => f.text),
|
? `光谱有效 ${slotCount.converged || 0} · 光谱失败 ${slotCount.failed || 0} · 未运行 ${slotCount.pending || 0} / 共 ${total}`
|
||||||
];
|
: `冷启动 ${slotCount.cold || 0} · 种子步进 ${slotCount.seed || 0} · 失败 ${slotCount.failed || 0} / 共 ${total}`;
|
||||||
|
const lines = [summary, ...findings.slice(0, 3).map(f => f.text)];
|
||||||
el.innerHTML = lines.map(l => `<span class="ps-concl-line">${escapeHtml(l)}</span>`).join('');
|
el.innerHTML = lines.map(l => `<span class="ps-concl-line">${escapeHtml(l)}</span>`).join('');
|
||||||
|
|
||||||
// 屏幕阅读器文本摘要(screen-reader-summary):平行集合图是 SVG role=img,
|
// 屏幕阅读器文本摘要(screen-reader-summary):平行集合图是 SVG role=img,
|
||||||
@@ -460,8 +499,9 @@ function bindPsHover(ctx) {
|
|||||||
const dim = seg.getAttribute('data-dim');
|
const dim = seg.getAttribute('data-dim');
|
||||||
const val = seg.getAttribute('data-val');
|
const val = seg.getAttribute('data-val');
|
||||||
const slot = seg.getAttribute('data-slot');
|
const slot = seg.getAttribute('data-slot');
|
||||||
|
const phase = ctx.phaseView || 'tlusty';
|
||||||
const names = ctx.psState.points
|
const names = ctx.psState.points
|
||||||
.filter(p => psSlot(p) === slot && (dim === 'status' || Number(p[dim]) === Number(val)))
|
.filter(p => psSlot(p, phase) === slot && (dim === 'status' || Number(p[dim]) === Number(val)))
|
||||||
.map(p => p.name).slice(0, 200);
|
.map(p => p.name).slice(0, 200);
|
||||||
if (names.length === 0) return;
|
if (names.length === 0) return;
|
||||||
if (names.length === 1) { openPointPanel(ctx, names[0]); return; }
|
if (names.length === 1) { openPointPanel(ctx, names[0]); return; }
|
||||||
@@ -476,8 +516,9 @@ function bindPsHover(ctx) {
|
|||||||
const dim = seg.getAttribute('data-dim');
|
const dim = seg.getAttribute('data-dim');
|
||||||
const val = seg.getAttribute('data-val');
|
const val = seg.getAttribute('data-val');
|
||||||
const slot = seg.getAttribute('data-slot');
|
const slot = seg.getAttribute('data-slot');
|
||||||
|
const phase = ctx.phaseView || 'tlusty';
|
||||||
const names = ctx.psState.points
|
const names = ctx.psState.points
|
||||||
.filter(p => psSlot(p) === slot && (dim === 'status' || Number(p[dim]) === Number(val)))
|
.filter(p => psSlot(p, phase) === slot && (dim === 'status' || Number(p[dim]) === Number(val)))
|
||||||
.map(p => p.name).slice(0, 200);
|
.map(p => p.name).slice(0, 200);
|
||||||
if (names.length === 0) return;
|
if (names.length === 0) return;
|
||||||
if (names.length === 1) { openPointPanel(ctx, names[0]); return; }
|
if (names.length === 1) { openPointPanel(ctx, names[0]); return; }
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ export async function openPointPanel(ctx, pointName) {
|
|||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
if (!json.success || !json.data) throw new Error(json.message || '无数据');
|
if (!json.success || !json.data) throw new Error(json.message || '无数据');
|
||||||
const { point, attempts, conv } = json.data;
|
const { point, attempts, conv } = json.data;
|
||||||
badges.innerHTML = `${pointStatusBadge(point.status)} ${pointMethodBadge(overallMethod(point))}`;
|
badges.innerHTML = `${pointStatusBadge(point.status)} ${pointMethodBadge(overallMethod(point))} ${stageBadges(point)}`;
|
||||||
body.innerHTML = '';
|
body.innerHTML = '';
|
||||||
body.appendChild(buildAttemptSection(attempts));
|
body.appendChild(buildAttemptSection(attempts));
|
||||||
body.appendChild(buildConvSection(conv));
|
body.appendChild(buildConvSection(conv));
|
||||||
@@ -106,6 +106,21 @@ function attemptStageBadge(stage) {
|
|||||||
return `<span class="method-badge ${cls}">${label}</span>`;
|
return `<span class="method-badge ${cls}">${label}</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 阶段状态双徽标(TLUSTY ✓/✗ · SYNSPEC ✓/✗):从 DB 阶段列读 tlusty_status/synspec_status,
|
||||||
|
* 让半失败点(大气收敛+光谱失败)一打开就能看到「整体失败 · TLUSTY ✓ · SYNSPEC ✗」。
|
||||||
|
* 旧数据无阶段列(NULL)→ 不渲染(保持向后兼容)。 */
|
||||||
|
function stageBadges(point) {
|
||||||
|
const mk = (label, status) => {
|
||||||
|
if (status === 'converged') return `<span class="stage-badge stage-ok">${label} ✓</span>`;
|
||||||
|
if (status === 'failed') return `<span class="stage-badge stage-fail">${label} ✗</span>`;
|
||||||
|
if (status === 'pending') return `<span class="stage-badge stage-pending">${label} …</span>`;
|
||||||
|
return ''; // NULL → 不渲染
|
||||||
|
};
|
||||||
|
const t = mk('TLUSTY', point.tlusty_status);
|
||||||
|
const s = mk('SYNSPEC', point.synspec_status);
|
||||||
|
return (t || s) ? `${t} ${s}`.trim() : '';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从 summary_json(ModelSummary)推导尝试方法徽标(Phase 6 起无 task_type 字段)。
|
* 从 summary_json(ModelSummary)推导尝试方法徽标(Phase 6 起无 task_type 字段)。
|
||||||
* ModelSummary.seed 存在 → 种子步进热启动;否则冷启动。synspec-only 任务无 seed →
|
* ModelSummary.seed 存在 → 种子步进热启动;否则冷启动。synspec-only 任务无 seed →
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
renderStrip, renderOverview, renderRateLine, renderSparkline, maybeRefreshProgress,
|
renderStrip, renderOverview, renderRateLine, renderSparkline, maybeRefreshProgress,
|
||||||
} from './detail/overview.js';
|
} from './detail/overview.js';
|
||||||
import { renderPointsTab, refreshPoints, exportPointsCsv, updateSortIndicators } from './detail/pointsTable.js';
|
import { renderPointsTab, refreshPoints, exportPointsCsv, updateSortIndicators } from './detail/pointsTable.js';
|
||||||
import { renderAnalysisTab, refreshPsData, closePsPointList, reflowParSets } from './detail/parSets.js';
|
import { renderAnalysisTab, refreshPsData, closePsPointList, reflowParSets, cleanupParSetsListeners } from './detail/parSets.js';
|
||||||
import { openPointPanel, closePointPanel } from './detail/pointPanel.js';
|
import { openPointPanel, closePointPanel } from './detail/pointPanel.js';
|
||||||
import { mountEnginePanel, unmountEnginePanel, refreshEnginePanel } from '../components/wfEnginePanel.js';
|
import { mountEnginePanel, unmountEnginePanel, refreshEnginePanel } from '../components/wfEnginePanel.js';
|
||||||
|
|
||||||
@@ -63,6 +63,7 @@ export function unmountWorkflowDetail() {
|
|||||||
ctx.psTooltipEl.remove();
|
ctx.psTooltipEl.remove();
|
||||||
ctx.psTooltipEl = null;
|
ctx.psTooltipEl = null;
|
||||||
}
|
}
|
||||||
|
cleanupParSetsListeners(ctx);
|
||||||
ctx.abortCtl.abort();
|
ctx.abortCtl.abort();
|
||||||
ctx = null;
|
ctx = null;
|
||||||
}
|
}
|
||||||
@@ -122,7 +123,7 @@ async function refreshStats(name) {
|
|||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
if (json.success && json.data) {
|
if (json.success && json.data) {
|
||||||
ctx.latestStats = json.data;
|
ctx.latestStats = json.data;
|
||||||
renderStrip(json.data);
|
renderStrip(json.data, ctx.phaseView);
|
||||||
if (ctx.activeTab === 'overview') {
|
if (ctx.activeTab === 'overview') {
|
||||||
renderOverview(ctx, json.data);
|
renderOverview(ctx, json.data);
|
||||||
// 面板不再每 tick 重建、曲线 DOM 保留:只需把速率行/ETA/停滞横幅与最新统计同步
|
// 面板不再每 tick 重建、曲线 DOM 保留:只需把速率行/ETA/停滞横幅与最新统计同步
|
||||||
@@ -156,7 +157,13 @@ function pageSkeleton(name) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="wf-seg-progress" class="seg-progress" aria-hidden="true"></div>
|
<div id="wf-seg-progress" class="seg-progress" aria-hidden="true"></div>
|
||||||
|
<div class="wf-strip-row">
|
||||||
|
<div class="phase-switcher" role="tablist" aria-label="阶段视图切换">
|
||||||
|
<button type="button" class="phase-tab active" data-phase="tlusty" role="tab" aria-selected="true">TLUSTY(大气)</button>
|
||||||
|
<button type="button" class="phase-tab" data-phase="synspec" role="tab" aria-selected="false">SYNSPEC(光谱)</button>
|
||||||
|
</div>
|
||||||
<div id="wf-strip-counts" class="wf-strip-counts tabular-num">正在获取执行数据…</div>
|
<div id="wf-strip-counts" class="wf-strip-counts tabular-num">正在获取执行数据…</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 内嵌执行引擎配置面板(启动/停止/删除/保存 + TLUSTY/SYNSPEC 阶段三维配置) -->
|
<!-- 内嵌执行引擎配置面板(启动/停止/删除/保存 + TLUSTY/SYNSPEC 阶段三维配置) -->
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile.server
|
dockerfile: Dockerfile.server
|
||||||
|
# 国内网络构建时在 .env 里设 USE_MIRRORS=1 / CARGO_MIRROR=1 换国内源;默认 0 直连官方源。
|
||||||
|
args:
|
||||||
|
USE_MIRRORS: ${USE_MIRRORS:-0}
|
||||||
|
CARGO_MIRROR: ${CARGO_MIRROR:-0}
|
||||||
image: dcts-server:latest
|
image: dcts-server:latest
|
||||||
container_name: dcts-server
|
container_name: dcts-server
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
@@ -57,6 +61,10 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile.node
|
dockerfile: Dockerfile.node
|
||||||
|
# 同 server:国内网络构建时在 .env 里设 USE_MIRRORS=1 / CARGO_MIRROR=1。
|
||||||
|
args:
|
||||||
|
USE_MIRRORS: ${USE_MIRRORS:-0}
|
||||||
|
CARGO_MIRROR: ${CARGO_MIRROR:-0}
|
||||||
image: dcts-node:latest
|
image: dcts-node:latest
|
||||||
container_name: dcts-node
|
container_name: dcts-node
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# 冷启动收敛调查与更正(2026-08-12)
|
||||||
|
|
||||||
|
## ⚠️ 结论更正(重要)
|
||||||
|
|
||||||
|
本文件是对 2026-08-11 系列"收敛修复"(`itek:0`/`iacc:0`/`DPSILG=1.5`)的彻底复核。
|
||||||
|
|
||||||
|
**最终结论:以上配置改动都无法让失败网格点从冷启动收敛,已全部回退。**
|
||||||
|
|
||||||
|
此前声称"禁用 Kantorovich 后 iter5 从 1e5~1e7 降到 0.2~1.0、点收敛"是**错误结论**,
|
||||||
|
源于在探测程序运行中解析 fort.9 时只读到部分深度(TLUSTY 逐深度写入、缓冲未刷新),
|
||||||
|
把"部分深度的最大值"误当成了"该迭代的真实最大值"。
|
||||||
|
|
||||||
|
**真实数据(完整 50 深度重新解析)**:
|
||||||
|
|
||||||
|
| 点 (he-2/he0 中等难度) | baseline 末relc | nokant(ITEK≥NITER) 末relc |
|
||||||
|
|----|:---:|:---:|
|
||||||
|
| he-2_c-1_n-2_o-4 | 4.5e+15 | 1.7e+03 |
|
||||||
|
| he-2_c-2_n-4_o-4 | 6.3e+09 | 1.5e+07 |
|
||||||
|
| he-2_c-4_n-1_o-1 | 9.7e+09 | 1.5e+04 |
|
||||||
|
| he0_c-1_n-3_o-2 | 4.5e+09 | 7.0e+04 |
|
||||||
|
| he0_c-2_n-2_o-3 | 1.4e+18 | 1.0e+04 |
|
||||||
|
|
||||||
|
**6 个测试点 × 4 种配置 = 24 次运行,0 次收敛。** 最难点
|
||||||
|
`t50000_g5.0_he-4_c-4_n-4_o-4` 用 itek=999 + orelax=0.5/0.1 也发散
|
||||||
|
(iter10 relc≈6e5,两种 orelax 结果几乎相同)。
|
||||||
|
|
||||||
|
## 调查中确认的源码事实(仍然有效)
|
||||||
|
|
||||||
|
| 发现 | 源码依据 |
|
||||||
|
|------|---------|
|
||||||
|
| `ITEK=0` **冻结 populations**(nitzer=0),不是禁用 Kantorovich | `tlusty208.f:1937` `if(nitzer.gt.itek) nitzer=itek` |
|
||||||
|
| `IACC=0` **是空操作**(被 clamp 回默认 7) | `tlusty208.f:1928` `IF(IACC.LE.4) IACC=7` |
|
||||||
|
| 默认 `ITEK=4` 在 iter≥5 冻结 Jacobian(Kantorovich) | `tlusty208.f:848-857` |
|
||||||
|
| 默认 `IACC=7` 在 iter≥7 外推 PSY0(Ng/ACCEL2) | `tlusty208.f:29704` |
|
||||||
|
|
||||||
|
禁用 Kantorovich+Ng(设 `itek`/`iacc` > NITER)**确实**把发散幅度降低了约
|
||||||
|
1e12~1e14×(如 he0_c-2_n-2: baseline 1e18 → nokant 1e4),但**达不到收敛**——
|
||||||
|
Newton 迭代自身在 grey LTE 起点即处于收敛域外,振荡不衰减。
|
||||||
|
|
||||||
|
## 物理背景
|
||||||
|
|
||||||
|
失败点的特征:**He-poor(he=−4)+ 高 Teff(50-60kK)+ 低 logg(5.0-5.5)+ 极贫
|
||||||
|
CNO**。DB 统计:
|
||||||
|
|
||||||
|
| He 丰度 | 收敛 | 失败 | 失败率 |
|
||||||
|
|---------|------|------|--------|
|
||||||
|
| he=+2(富氦)| 359 | 25 | **7%** |
|
||||||
|
| he=0 | 177 | 206 | 54% |
|
||||||
|
| he=−2 | 139 | 245 | 64% |
|
||||||
|
| he=−4(贫氦)| 132 | 252 | **66%** |
|
||||||
|
|
||||||
|
这些点(纯 H 极端 NLTE 电离)从 grey LTE 模型冷启动,首次 NLTE 迭代的 POP 修正
|
||||||
|
就达 1e2~1e4(相对变化),Newton 步长超出收敛域。无论加速/阻尼参数如何设置,
|
||||||
|
迭代都在 1e2~1e6 之间振荡发散。**调参救不回收敛域外的问题。**
|
||||||
|
|
||||||
|
## 结论与建议
|
||||||
|
|
||||||
|
1. **YAML 已回退**:`workflows/sdB_cno.yaml` 恢复生产配置(nc NITER=10、nl NITER=100
|
||||||
|
orelax=0.5、seed_nc NITER=20、seed_nl NITER=100 orelax=0.5,itek/iacc 用默认)。
|
||||||
|
此前所有"修复"(itek:0 / iacc:0 / DPSILG=1.5 / itek=999)均无效或有害,全部移除。
|
||||||
|
2. **真正的可行方向**(需框架/Rust 改动,非 YAML 配置):
|
||||||
|
- **连续法(continuation)**:从已收敛的较低 Teff(如 40-45kK)模型出发,
|
||||||
|
以该模型为 fort.8 热启动,逐步升高 Teff——NLTE 大气建模处理硬收敛点的标准方法。
|
||||||
|
- **改进 seed_step 种子选择**:优先选 **He 丰度匹配**的已收敛邻居(he=+2 邻居
|
||||||
|
收敛率 93% vs he=−4 邻居 34%),而非仅按 Teff/logg 距离。
|
||||||
|
- 或接受这些极端点(he=−4 + 极贫 CNO + 50-60kK)需要人工/半自动处理。
|
||||||
|
|
||||||
|
## 相关计算文件
|
||||||
|
|
||||||
|
- `test/20260811_convergence_fix_abtest/runs_nc_probe/`:5 个中等难度点的
|
||||||
|
baseline / nokant / nokant_relax 三配置完整 10 迭代轨迹(fort.9),全部发散。
|
||||||
|
- `test/20260811_convergence_fix_abtest/runs_verify/`、`runs_verify_01/`:
|
||||||
|
最难点完整冷链(lte→nc→nl)验证,nc 发散、nl 因 NaN 假收敛。
|
||||||
+25
-11
@@ -83,11 +83,7 @@ DCTS 运行时所依赖的物理计算底座二进制文件 `assets/tlusty_stati
|
|||||||
* **标准编译命令**:
|
* **标准编译命令**:
|
||||||
```bash
|
```bash
|
||||||
cd /home/fmq/program/tlusty/tl208-s54/tlusty
|
cd /home/fmq/program/tlusty/tl208-s54/tlusty
|
||||||
gfortran -fno-automatic -O3 -o ../dcts/assets/tlusty_static tlusty208.f
|
gfortran -fno-automatic -fno-toplevel-reorder -mcmodel=medium -O3 -o ../dcts/assets/tlusty_static tlusty208.f
|
||||||
```
|
|
||||||
* **大内存寻址编译选项 (推荐超大能级网格使用)**:
|
|
||||||
```bash
|
|
||||||
gfortran -fno-automatic -mcmodel=large -O3 -o ../dcts/assets/tlusty_static tlusty208.f
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 2. 编译 SYNSPEC 理论光谱合成引擎 (`assets/synspec_static`)
|
#### 2. 编译 SYNSPEC 理论光谱合成引擎 (`assets/synspec_static`)
|
||||||
@@ -98,18 +94,36 @@ DCTS 运行时所依赖的物理计算底座二进制文件 `assets/tlusty_stati
|
|||||||
* **标准编译命令**:
|
* **标准编译命令**:
|
||||||
```bash
|
```bash
|
||||||
cd /home/fmq/program/tlusty/tl208-s54/synspec
|
cd /home/fmq/program/tlusty/tl208-s54/synspec
|
||||||
gfortran -fno-automatic -O3 -o ../dcts/assets/synspec_static synspec54.f
|
gfortran -fno-automatic -fno-toplevel-reorder -mcmodel=medium -O3 -o ../dcts/assets/synspec_static synspec54.f
|
||||||
```
|
|
||||||
* **大内存寻址编译选项**:
|
|
||||||
```bash
|
|
||||||
gfortran -fno-automatic -mcmodel=large -O3 -o ../dcts/assets/synspec_static synspec54.f
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 关键编译选项说明:
|
#### 关键编译选项说明:
|
||||||
|
|
||||||
- `-fno-automatic`: 禁用局部变量的自动栈分配(强制将局部变量保存在静态内存区)。这是保证传统 FORTRAN 77 程序正常运行的关键参数,防止大型局部数组造成栈溢出(Stack Overflow)或段错误(Segmentation Fault)。
|
- `-fno-automatic`: 禁用局部变量的自动栈分配(强制将局部变量保存在静态内存区)。这是保证传统 FORTRAN 77 程序正常运行的关键参数,防止大型局部数组造成栈溢出(Stack Overflow)或段错误(Segmentation Fault)。
|
||||||
- `-O3`: 开启全量 LLVM/GCC 代码优化,极大加快完全线性化/加速 Lambda 迭代(CL/ALI)及辐射转移方程形式解的计算速度。
|
- `-O3`: 开启全量 LLVM/GCC 代码优化,极大加快完全线性化/加速 Lambda 迭代(CL/ALI)及辐射转移方程形式解的计算速度。
|
||||||
- `-mcmodel=large`: 当模型数组与数据段超越 2GB 寻址限制时,允许可执行文件使用 64 位大内存寻址模式。
|
- **`-fno-toplevel-reorder`(关键修复,2026-08-11)**: 禁用 gfortran 的顶级函数/变量重排优化。**SYNSPEC 与 TLUSTY 必须加此选项**——不加会导致 Balmer 跃迁(3646 Å)之后整个波长范围输出 NaN(`-O1` 及以上均触发,仅 `-O0` 幸免)。根因:该优化破坏了 SYNSPEC/TLUSTY 依赖的 COMMON 块数据初始化顺序,使氢线不透明度计算读到未初始化的内存。此项优化缺失对计算速度无显著影响(实测 `-O3 -fno-toplevel-reorder` 与 `-O3` 速度相当)。
|
||||||
|
- `-mcmodel=medium`: 允许大型 COMMON 块(SYNSPEC 的 LINDAT/PARAMS COMMON 块超过 2GB)使用 64 位寻址,避免链接器 "relocation truncated to fit" 错误。
|
||||||
|
|
||||||
|
#### 3. 谱线表(SYNSPEC fort.19)部署
|
||||||
|
|
||||||
|
SYNSPEC 的谱线表通过 `assets/data/{linelist}` 分发(`/api/data/file/{name}` 路由),工作流 YAML 的 `linelist` 字段指定文件名。可用线表:
|
||||||
|
|
||||||
|
| 线表 | 波长范围 | 线数 | 大小 | 适用场景 |
|
||||||
|
| :--- | :--- | :--- | :--- | :--- |
|
||||||
|
| `gfATO.dat` | 18–23000 Å | 230 万 | 238 MB | **默认/推荐**:全波段(EUV+UV+光学+近/中红外) |
|
||||||
|
| `gfVIS99.dat` | 3000–7550 Å | 15 万 | 12 MB | 仅可见光诊断(快速) |
|
||||||
|
|
||||||
|
**部署步骤**(`gfATO.dat` 不入库,需手动放置):
|
||||||
|
```bash
|
||||||
|
# 把 gfATO.dat 复制到 server 的 assets/data/ 目录(被 .gitignore 排除)
|
||||||
|
cp /path/to/gfATO.dat assets/data/gfATO.dat
|
||||||
|
# node 端首次连接时自动从 server 下载(238MB,首次较慢,之后缓存)
|
||||||
|
```
|
||||||
|
|
||||||
|
**SYNSPEC 波长范围限制**(实测):
|
||||||
|
- 可靠范围:**100–23000 Å**(超出此范围输出截断,仅 ~235 行无效数据)
|
||||||
|
- 工作流默认配置:`alam0: 100.0, alast: 20000.0`(留安全余量)
|
||||||
|
- 波长范围由工作流 YAML `synspec_input.line6.alam0/alast` 控制,须与线表覆盖范围匹配
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
# 400 失败点二次分析与 NaN 伪收敛修复(2026-08-17)
|
||||||
|
|
||||||
|
> 背景:三层修复(nst 行宽 / nl_direct 回退 / seed_finder 边界)部署重跑后,
|
||||||
|
> 9216 点中 8816 完成、**400 失败**。数据源:最新 DB 快照 `/home/fmq/下载/dcts.db`
|
||||||
|
> + 全量更新后的 `data/salvage/`(6 节点)。
|
||||||
|
|
||||||
|
## 一、400 点分类(按每点最新一次任务)
|
||||||
|
|
||||||
|
| 类别 | 数量 | 机制 |
|
||||||
|
|---|---:|---|
|
||||||
|
| **NaN 伪收敛** | **261** | 见 §二,本轮核心 bug |
|
||||||
|
| 真发散(nl/nl_direct 1e16 STOP) | ~131 | 20kK/logg≥6 富氦簇(~50)+ 60kK/logg≤5.5 贫氦簇(~65)等 |
|
||||||
|
| 收敛但最深层能量残差 1.0–2.3% 超 1% 阈值 | 7 | 边际案例 |
|
||||||
|
|
||||||
|
分布(NaN 伪收敛修复前):teff=20000:76、55000:77、60000:121 为主。
|
||||||
|
|
||||||
|
## 二、NaN 伪收敛:根因链
|
||||||
|
|
||||||
|
生产工件实证(t60000_g6.0_he-4_c-4_n-4_o-4,多节点一致):
|
||||||
|
|
||||||
|
1. `seed_nc` 发散(best_max_relc 1.15e15),fort.7 含 4823 行 NaN → **污染种子**;
|
||||||
|
2. `nl` 从污染种子启动 → TLUSTY 立即崩溃,fort.9 写出**全 `0.00E+00`**
|
||||||
|
(NaN 参与的相对变化无法计算,写出零;fort.9 尾部 TEMP 列可见 NaN);
|
||||||
|
3. `check_fort9`:max_relc=0 < chmax=1e-3 → **`converged=true`(伪收敛)**;
|
||||||
|
4. 因 nl 被判"收敛",**nl_direct 回退被跳过**(回退仅在 require_converged 失败时触发);
|
||||||
|
5. 最终门槛全灭(emflux nan_ratio=100%、温度 NaN、bfac 38% 极端值)→ 点失败,
|
||||||
|
282/288 个 emflux 失败的 ratio 恰为 0(`integrated_flux=0, n_points=0`)。
|
||||||
|
|
||||||
|
## 三、修复
|
||||||
|
|
||||||
|
`crates/common/src/runner.rs` `execute_tlusty_stage()`:
|
||||||
|
- 阶段被判 converged 后复查本阶段 fort.7(`atmosphere_has_nan`,含 NaN/Inf/`***`/
|
||||||
|
E+300 检测),命中即否决收敛并标注 note;
|
||||||
|
- 被否决的阶段**不产出种子**(`produced_seed=None`),阻断污染向下游传播。
|
||||||
|
|
||||||
|
测试:`unit_nan_pseudo_convergence_veto`(全零 fort.9 + NaN fort.7 → seed_nc 判失败、
|
||||||
|
nl 回退原始种子收敛);全 workspace 测试通过。
|
||||||
|
|
||||||
|
## 四、直测验证(test/20260817_failed400/,direct-nl 复现 nl_direct 回退路径)
|
||||||
|
|
||||||
|
| 测试 | 点 | 种子 | 结果 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| q1 | t60000_g6.0_he-4_c-4_n-4_o-4(NaN 伪收敛簇) | 邻点 o-2 的收敛 .7 | ✅ **12it 收敛 7.0e-4**,温度三项全过(表层 0.79×Teff),emflux 4π∫Hλ/σT⁴=1.0014 |
|
||||||
|
| q2 | t20000_g6.5_he2_c-4_n-4_o-2(20k 富氦簇) | 邻点 c-3_n-3_o-2 | ❌ iter42 发散 STOP(1.35e18) |
|
||||||
|
| q3 | t60000_g5.0_he-4_c-4_n-4_o-2(60k g5 簇) | 邻点 o-4 的收敛 .7 | ❌ 100it 耗尽,max_relc 546(温度结构物理但未收敛) |
|
||||||
|
|
||||||
|
q1 证明:NaN 伪收敛簇(261 点,65%)在修复后经 nl_direct 回退路径可完整恢复
|
||||||
|
(数值收敛 + 物理门槛全过)。q2/q3 为真发散硬核(~131 点),所用邻居种子不足以
|
||||||
|
收敛,需依赖生产的 exact_family 种子选择、多策略重试或 Teff 连续步进。
|
||||||
|
|
||||||
|
## 五、三类问题的验证测试与方案(test/20260817_failed400/)
|
||||||
|
|
||||||
|
### 5.1 NaN 伪收敛簇(261 点)回收估计验证 — 成立
|
||||||
|
|
||||||
|
分层抽样 17 点(每 teff 档 2 个,种子 = 完成点中 CNO 精确匹配/最近邻的
|
||||||
|
salvage .7),direct-nl(= nl_direct 回退的等价路径):
|
||||||
|
|
||||||
|
- **11/17 收敛**(20–41 迭代),温度结构三项全过,emflux 1.0001–1.0017 全达标;
|
||||||
|
- 6 个失败均为种子距离过大(Δlogg 0.5–1.5)直接 STOP——生产端 seed_finder 有
|
||||||
|
多候选 + 多策略重试(attempt 至 11 次),实际回收率高于单种子 65%;
|
||||||
|
- 结论:"修复后预期回收 ~261 点"估计成立且偏保守。
|
||||||
|
|
||||||
|
### 5.2 能量边际点(7 个)— nl_tight 回退,已实现
|
||||||
|
|
||||||
|
实测(mg_tight):从自身最终模型续迭代、CHMAX 1e-3→1e-4,约 19 迭代后
|
||||||
|
最深层 (RAD+CON)/TOT 残差 0.0199/0.0228 → **0.0011/0.0016**(降 ~10×)。
|
||||||
|
|
||||||
|
`runner.rs` 新增 nl_tight 回退:能量门槛失败 + 最终大气无 NaN 时,以
|
||||||
|
`<label>_tight`(CHMAX÷10)从自身模型续迭代,成功则刷新最终大气/快照并
|
||||||
|
重判能量。单测 `unit_nl_tight_fallback_wiring` 通过。
|
||||||
|
|
||||||
|
### 5.3 真发散硬核(~131 点)— 连续步进(continuation)方案验证通过
|
||||||
|
|
||||||
|
| 批次 | 配置 | 结果 |
|
||||||
|
|---|---|---|
|
||||||
|
| hard_base(16 点分层抽样) | direct-nl + 最近完成点种子 | 4/16 |
|
||||||
|
| hard_ore03(12 个失败点) | 同上 + ORELAX=0.3 | 1/12 |
|
||||||
|
| ladder_60k_he4(60k 贫氦簇代表) | 55k→57.5k→60k 三步 Teff 步进 | ✅ 28it/3.9e-4,物理过,emflux=1.0008 |
|
||||||
|
| ladder_20k_he2(20k/6.5 富氦簇代表) | g6.0→6.25→6.5 三步 logg 步进 | ✅ 27it/8.1e-4,物理过,emflux=1.0112(<2% 阈值) |
|
||||||
|
|
||||||
|
两个最难簇的代表点在 direct-nl、ORELAX 全部失败后,**仅靠 2 个中间参数的
|
||||||
|
连续步进即完全收敛**。机理:单步 Δ 超出 Newton 收敛域时,把参数空间距离
|
||||||
|
切分为多段,每段以段首收敛解热启动段尾。
|
||||||
|
|
||||||
|
### 5.4 生产化建议
|
||||||
|
|
||||||
|
1. 部署本修复(NaN 伪收敛否决 + nl_tight 回退)重试 400 点:预期回收 261 簇
|
||||||
|
(>65%/种子×多候选)+ 7 个能量边际点;
|
||||||
|
2. 为剩余硬核实现 ladder 策略:失败点自动生成 2–3 个中间参数步进链
|
||||||
|
(优先 logg 维度,其次 Teff;步长 Δlogg≤0.25 / ΔTeff≤2.5kK),
|
||||||
|
每步独立 nl + require_converged 串联,最后一步产出目标点模型。
|
||||||
|
实测两簇代表点总耗时 <6 分钟/点(3 步 × ~2 分钟)。
|
||||||
|
|
||||||
|
## 六、遗留问题
|
||||||
|
|
||||||
|
- 20k/6.5 富氦簇解分支跳变:同一点不同种子在好解/坏解(emflux 0.69)间跳变,
|
||||||
|
ladder 步进天然规避(每步验证收敛与物理性后才前进)。
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 182 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 382 KiB |
@@ -0,0 +1,693 @@
|
|||||||
|
# SYNSPEC 全局 NaN 失效修复与全波段 SED 配置变更
|
||||||
|
|
||||||
|
> 日期:2026-08-11
|
||||||
|
> 范围:SYNSPEC 理论光谱合成引擎的系统性数值失效(全局 NaN)根因定位、修复,以及线表/波长范围配置变更。
|
||||||
|
> 影响:数据库中 **7452 个** `synspec_status=converged` 的网格点其 `.spec` 光谱全部含 ~73% NaN,必须用修复后的二进制重算。
|
||||||
|
> 前置文档:`docs/spectrum_correctness_analysis.md`(物理正确性五重硬门槛)、`docs/tlusty&synspec收敛性判断.md`(rc 不可靠性与漏洞修复史)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. TL;DR
|
||||||
|
|
||||||
|
| 维度 | 结论 |
|
||||||
|
|------|------|
|
||||||
|
| **症状** | SYNSPEC 产出的 `.spec` 在 Balmer 跃迁(3646 Å)之后整个波长范围输出 NaN(~73% 数据点无效),但 SYNSPEC 正常退出(rc=0),无任何错误日志 |
|
||||||
|
| **根因** | gfortran 的 `-ftoplevel-reorder` 优化(`-O1` 起启用)破坏了 SYNSPEC FORTRAN 77 COMMON 块的数据初始化顺序,使氢线不透明度计算在 Balmer 系限后读到未初始化内存。深度原理见 **§9** |
|
||||||
|
| **影响范围** | 所有用旧 `-O3` 二进制计算的网格点(7452 个 `synspec_status=converged`),与大气参数、fort.55 配置、线表无关 |
|
||||||
|
| **修复** | 编译时加 `-fno-toplevel-reorder`,保留 `-O3` 其他优化。速度无显著影响(~4s/点 vs ~3.5s/点) |
|
||||||
|
| **附带改进** | 线表从 gfVIS99.dat(3000-7550Å)升级为 gfATO.dat(18-23000Å 全波段),波长范围扩至 100-20000Å |
|
||||||
|
| **下一步** | 用修复后的 `assets/synspec_static` 重新部署,重算全部网格点 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 背景:为何怀疑 SYNSPEC 光谱有问题
|
||||||
|
|
||||||
|
### 1.1 已知的 fort.55 错位 bug(已修复,历史背景)
|
||||||
|
|
||||||
|
`docs/spectrum_correctness_analysis.md §5` 记录了 `fort55_writer.rs` 的字段错位 bug:旧实现的 `idrv/ifreq` 被 SYNSPEC 当作 `IDSTD/IPRIN` 读取,导致 IDSTD=50(最深层而非自动取 2ND/3≈33),影响光谱线强归一化。该 bug 已在 2026-08-07 修复(重构为 9 子结构体按行一一对应)。
|
||||||
|
|
||||||
|
但 fort.55 错位修复后,文档明确指出"所有光谱需重算"(`§5.3 重算指引`)。本次会话正是在执行这个重算验证时,发现了更深层的系统性问题。
|
||||||
|
|
||||||
|
### 1.2 salvage 数据的初步检查
|
||||||
|
|
||||||
|
salvage 目录(`data/salvage/`)保存了 5 个计算节点的完整产物(8319 个网格点的 `.7` 大气 + `.spec` 光谱 + 各阶段快照)。初步检查发现:
|
||||||
|
|
||||||
|
- `.spec` 文件大小正常(~11MB,432827 行),看似完整
|
||||||
|
- SYNSPEC 退出码 rc=0,`.log` 无任何错误
|
||||||
|
- `conv.json` 记录 `synspec_status=converged`
|
||||||
|
|
||||||
|
但文件大小正常 ≠ 内容有效——这正是"静默错误"的危险之处。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 检测方法:如何识别 NaN 网格点
|
||||||
|
|
||||||
|
### 2.1 spec_is_valid 校验器(项目已有)
|
||||||
|
|
||||||
|
`crates/common/src/conv_check.rs:290-335` 实现了 `spec_is_valid()` 函数,在 SYNSPEC 运行后立即校验 `.spec`:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub fn spec_is_valid(path: &Path) -> Option<String>
|
||||||
|
```
|
||||||
|
|
||||||
|
校验项(任一命中即返回失败原因字符串):
|
||||||
|
1. 文件缺失或无法读取
|
||||||
|
2. **含 NaN/Inf/`***` 溢出行**(逐行扫描,统计坏行数)
|
||||||
|
3. 有效行数不足(< 10 行)
|
||||||
|
4. 流量全为零
|
||||||
|
5. 文件为空
|
||||||
|
|
||||||
|
**关键**:该校验器在 `runner.rs:645` 调用,命中无效则置 `synspec_rc` 非零 + 写入 `synspec_error`,触发 reporter 判 Failed + 策略链回退。
|
||||||
|
|
||||||
|
### 2.2 为何 7452 个点仍标记为 converged
|
||||||
|
|
||||||
|
`spec_is_valid` 是在 fort.55 错位修复(2026-08-07)的同一次提交中引入的。在此之前计算的网格点没有经过该校验——它们的 `.spec` 虽然 73% 是 NaN,但当时只做了 `is_file()` 存在性检查就标记为成功。
|
||||||
|
|
||||||
|
数据库查询确认:
|
||||||
|
```sql
|
||||||
|
SELECT synspec_status, count(*) FROM grid_points WHERE status='completed' GROUP BY synspec_status;
|
||||||
|
-- converged: 7452 ← 这些点几乎全部含 NaN(旧二进制 + 旧校验)
|
||||||
|
-- failed: 459
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 离线批量检测脚本
|
||||||
|
|
||||||
|
对任意 salvage `.spec` 文件检测 NaN 分布(本次会话使用的 Python 脚本):
|
||||||
|
|
||||||
|
```python
|
||||||
|
import math
|
||||||
|
nan_ranges = []; good_ranges = []
|
||||||
|
in_nan = False; in_good = False; ns = 0; gs = 0; prev = 0
|
||||||
|
with open('fort.7') as f:
|
||||||
|
for line in f:
|
||||||
|
p = line.split()
|
||||||
|
if len(p) < 2: continue
|
||||||
|
try: w, fl = float(p[0]), float(p[1])
|
||||||
|
except: continue
|
||||||
|
if math.isinf(w): continue # 跳过 Infinity 波长(崩溃特征)
|
||||||
|
isn = math.isnan(fl)
|
||||||
|
if isn:
|
||||||
|
if in_good: good_ranges.append((gs, prev)); in_good = False
|
||||||
|
if not in_nan: ns = w; in_nan = True
|
||||||
|
else:
|
||||||
|
if in_nan: nan_ranges.append((ns, prev)); in_nan = False
|
||||||
|
if not in_good: gs = w; in_good = True
|
||||||
|
prev = w
|
||||||
|
if in_nan: nan_ranges.append((ns, prev))
|
||||||
|
if in_good: good_ranges.append((gs, prev))
|
||||||
|
```
|
||||||
|
|
||||||
|
典型坏谱输出(72.9% NaN):
|
||||||
|
```
|
||||||
|
行: 432827, NaN: 315476 (72.9%)
|
||||||
|
正常段: [('3000', '3645')] ← Balmer 跃迁前
|
||||||
|
NaN段: [('3645', '5600'), ('5800', '7000')] ← Balmer 跃迁后大面积 NaN
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 测试过程:从症状到根因
|
||||||
|
|
||||||
|
### 3.1 第一步:搭建本地 SYNSPEC 测试环境
|
||||||
|
|
||||||
|
SYNSPEC 运行需要 5 类输入文件(参见官方 `RSynspec` 脚本):
|
||||||
|
|
||||||
|
| 文件 | 来源 | 作用 |
|
||||||
|
|------|------|------|
|
||||||
|
| `fort.8` | `.7` 大气模型复制 | TLUSTY 收敛的大气结构 |
|
||||||
|
| `fort.55` | `fort55_writer.rs` 生成(9 行控制卡) | 波长范围/输出模式/线处理开关 |
|
||||||
|
| `fort.19` | symlink 到线表(gfVIS99.dat / gfATO.dat) | 谱线列表 |
|
||||||
|
| `data/` | symlink 到原子数据目录 | 能级模型、截面、分区函数 |
|
||||||
|
| `<name>.5` | `gen_input5.rs` 生成 | TLUSTY/SYNSPEC 共用 stdin(丰度/原子配置) |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 测试沙盒搭建
|
||||||
|
mkdir /tmp/synspec_test && cd /tmp/synspec_test
|
||||||
|
cp .../synspec/synspec.exe synspec
|
||||||
|
ln -s .../data data
|
||||||
|
cp .../assets/gfVIS99.dat fort.19
|
||||||
|
|
||||||
|
# 准备一个收敛点的输入
|
||||||
|
cp <salvage>/<name>.7 fort.8
|
||||||
|
cp <salvage>/<name>.nl.5 <name>.5
|
||||||
|
cat > fort.55 <<'EOF'
|
||||||
|
0 0 1
|
||||||
|
1 0 0 0
|
||||||
|
0 0 0 0 0
|
||||||
|
1 1 0 0 0
|
||||||
|
0 0 0
|
||||||
|
3000.0 7000.0 10 0 0.0001 0.01
|
||||||
|
0
|
||||||
|
-1
|
||||||
|
0 0 0
|
||||||
|
EOF
|
||||||
|
|
||||||
|
./synspec < <name>.5 > <name>.log 2>&1
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 第二步:确认问题与大气无关
|
||||||
|
|
||||||
|
选择不同参数(He-poor / He-rich、Teff 25k-45kK)的收敛良好点(`best_max_relc < 0.001`)测试,**全部出现相同的 NaN 分布**:
|
||||||
|
|
||||||
|
| 点 | 参数 | NaN 段 |
|
||||||
|
|----|------|--------|
|
||||||
|
| t35000_g6.5_he-4_c-2_n-3_o-2 | He-poor, 35kK | 3646-5600, 5800-7000 |
|
||||||
|
| t25000_g6.5_he2_c-2_n-1_o-2 | He-rich, 25kK | 同上 |
|
||||||
|
| t45000_g5.5_he2_c-1_n-3_o-4 | He-rich, 45kK | 同上 |
|
||||||
|
|
||||||
|
排除假设:
|
||||||
|
- ✗ fort.55 错位(新旧格式都 NaN)
|
||||||
|
- ✗ 大气质量(`.6` 能量守恒 `(RAD+CON)/TOT ≈ 1.000-1.002`,物理有效)
|
||||||
|
- ✗ 连续谱计算(`.cont` 全范围无 NaN,只有含谱线的 `.spec` 坏)
|
||||||
|
|
||||||
|
### 3.3 第三步:精确边界定位
|
||||||
|
|
||||||
|
逐步缩小波长范围测试,发现 NaN 的起始点是 **3645.53 Å**——精确对应氢的 **Balmer 跃迁(3646 Å)**:
|
||||||
|
|
||||||
|
```
|
||||||
|
3000-3500 Å: 0 NaN ✓
|
||||||
|
3600-3650 Å: 464 NaN(跨 Balmer 跃迁,部分坏)
|
||||||
|
3647-3700 Å: 5492 行全 NaN ✗(Balmer 跃迁之后)
|
||||||
|
```
|
||||||
|
|
||||||
|
3646 Å = 氢的 Balmer 系限(`n=2 → ∞`),此处氢的高级 Balmer 线密集。问题指向氢线不透明度计算。
|
||||||
|
|
||||||
|
### 3.4 第四步:浮点陷阱调试版(关键转折)
|
||||||
|
|
||||||
|
用 `-ffpe-trap=invalid,zero,overflow` 编译调试版 SYNSPEC,期望在 NaN 产生瞬间中断:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /tmp/synspec_build
|
||||||
|
gfortran -O0 -g -fbacktrace -ffpe-trap=invalid,zero,overflow \
|
||||||
|
-fno-automatic -mcmodel=medium synspec54.f -o synspec_debug
|
||||||
|
```
|
||||||
|
|
||||||
|
> **编译注意**:SYNSPEC 的 `synspec54.f` 通过 `INCLUDE` 语句引入 `PARAMS.FOR`/`MODELP.FOR`/`LINDAT.FOR` 等,只需编译主文件。但 COMMON 块超大(`MLIN0=1200000` × 多数组),必须加 `-mcmodel=medium` 否则链接器报 `relocation truncated to fit: R_X86_64_PC32`。
|
||||||
|
|
||||||
|
**结果**:调试版(`-O0`)**正常退出,0 NaN,3000-7000Å 全范围完整**!
|
||||||
|
|
||||||
|
这是决定性转折——说明问题不在 SYNSPEC 源码逻辑,而在**编译优化**。
|
||||||
|
|
||||||
|
### 3.5 第五步:二分法定位优化元凶
|
||||||
|
|
||||||
|
对比不同优化级别:
|
||||||
|
|
||||||
|
| 编译选项 | 结果 |
|
||||||
|
|----------|------|
|
||||||
|
| `-O3`(原版) | 72.9% NaN |
|
||||||
|
| `-O2` | 72.9% NaN |
|
||||||
|
| `-O1` | 72.9% NaN |
|
||||||
|
| **`-O0`** | **0 NaN ✓** |
|
||||||
|
|
||||||
|
`-O1` 起就触发。用二分法在 `-O1` 启用的 43 个优化项中排查:
|
||||||
|
|
||||||
|
1. 获取 `-O0` → `-O1` 新增的优化列表:
|
||||||
|
```bash
|
||||||
|
diff <(gfortran -O0 -Q --help=optimizers) <(gfortran -O1 -Q --help=optimizers) \
|
||||||
|
| grep "^>" | grep enabled
|
||||||
|
# 43 项
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 分两组测(`-O0 + HALF1` / `-O0 + HALF2`)→ HALF2 段错误
|
||||||
|
|
||||||
|
3. HALF2 逐项测 → **`-ftoplevel-reorder` 单独触发段错误**
|
||||||
|
|
||||||
|
4. 反向验证:`-O1 -fno-toplevel-reorder` → **0 NaN ✓**
|
||||||
|
|
||||||
|
5. 最终验证:`-O3 -fno-toplevel-reorder` → **0 NaN ✓,速度 4s/点**
|
||||||
|
|
||||||
|
> `-ftoplevel-reorder` 是 gfortran 的顶级函数/变量重排优化,它改变了程序单元(subroutine/function)和 DATA 块的初始化顺序。SYNSPEC 是传统 FORTRAN 77 代码,大量使用 COMMON 块,初始化散布在 305 条 DATA 语句中(无 BLOCK DATA),重排后某些 COMMON 变量在使用时尚未初始化,导致氢线 Stark 轮廓计算读到垃圾值 → NaN。
|
||||||
|
>
|
||||||
|
> **深度技术分析见 §9**(源码级 + 编译器原理 + FORTRAN 77 标准 + 社区经验)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 修复实施
|
||||||
|
|
||||||
|
### 4.1 重编译 SYNSPEC 与 TLUSTY
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/fmq/program/tlusty/tl208-s54
|
||||||
|
|
||||||
|
# 备份原版
|
||||||
|
cp synspec/synspec.exe ~/tlusty_O3_backups_20260811/synspec.exe.O3.bak
|
||||||
|
cp dcts/assets/synspec_static ~/tlusty_O3_backups_20260811/synspec_static.O3.bak
|
||||||
|
cp tlusty/tlusty.exe ~/tlusty_O3_backups_20260811/tlusty.exe.O3.bak
|
||||||
|
|
||||||
|
# 重编译 SYNSPEC (-O3 -fno-toplevel-reorder)
|
||||||
|
cd synspec
|
||||||
|
gfortran -O3 -fno-toplevel-reorder -fno-automatic -mcmodel=medium \
|
||||||
|
synspec54.f -o synspec.exe.fixed
|
||||||
|
|
||||||
|
# 重编译 TLUSTY(预防性,同样问题)
|
||||||
|
cd ../tlusty
|
||||||
|
gfortran -O3 -fno-toplevel-reorder -fno-automatic -mcmodel=medium \
|
||||||
|
tlusty208.f -o tlusty.exe.fixed
|
||||||
|
|
||||||
|
# 安装到所有位置
|
||||||
|
cp synspec/synspec.exe.fixed synspec/synspec.exe
|
||||||
|
cp synspec/synspec.exe.fixed dcts/assets/synspec_static
|
||||||
|
cp tlusty/tlusty.exe.fixed tlusty/tlusty.exe
|
||||||
|
cp tlusty/tlusty.exe.fixed dcts/assets/tlusty_static
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 二进制兼容性验证
|
||||||
|
|
||||||
|
Dockerfile.node 的运行镜像是 `debian:bookworm-slim`(glibc 2.36)。修复版在本机(Ubuntu, glibc 2.43)编译,验证兼容性:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查 glibc 符号需求
|
||||||
|
objdump -T assets/synspec_static | grep -oE "GLIBC_2\.[0-9]+" | sort -V | tail
|
||||||
|
# 最高 GLIBC_2.35 ≤ bookworm 的 2.36 ✓
|
||||||
|
|
||||||
|
# Docker 端到端验证
|
||||||
|
docker run --rm -v /tmp/test:/work debian:bookworm-slim bash -c "
|
||||||
|
apt-get install -y libgfortran5
|
||||||
|
./work/synspec < input.5 > /dev/null 2>&1
|
||||||
|
echo RC=\$?
|
||||||
|
wc -l fort.7 # 432827 行 ✓
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 网格边界点批量验证
|
||||||
|
|
||||||
|
用修复版测试 6 个网格边界点(各维极端值),全部通过:
|
||||||
|
|
||||||
|
| 网格点 | 参数特征 | gfVIS99 3000-7000 | gfATO 100-20000 |
|
||||||
|
|--------|---------|--------------------|----|
|
||||||
|
| t20000_g5.0_he-4_c-1_n-1_o-1 | 最低Teff+贫He+富金属 | ✓ 0 NaN | ✓ 0 NaN |
|
||||||
|
| t60000_g6.5_he2_c-1_n-1_o-1 | 最高Teff+富He+高logg | ✓ 0 NaN | ✓ 0 NaN |
|
||||||
|
| t30000_g5.0_he2_c-1_n-1_o-1 | He-rich极端 | ✓ 0 NaN | ✓ 0 NaN |
|
||||||
|
| t30000_g5.5_he-2_c-1_n-1_o-1 | 富金属 | ✓ 0 NaN | ✓ 0 NaN |
|
||||||
|
| t40000_g5.0_he-4_c-4_n-4_o-4 | 贫金属+贫He | ✓ 0 NaN | ✓ 0 NaN |
|
||||||
|
| t50000_g5.0_he-2_c-1_n-2_o-2 | 低logg+高Teff | ✓ 0 NaN | ✓ 0 NaN |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 线表与波长范围配置变更
|
||||||
|
|
||||||
|
### 5.1 可用线表对比
|
||||||
|
|
||||||
|
| 线表 | 波长范围 | 线数 | 文件大小 | 适用场景 |
|
||||||
|
|------|---------|------|---------|---------|
|
||||||
|
| `gfVIS99.dat` | 3000–7550 Å | 15 万 | 12 MB | 仅可见光诊断(快速,~4s/点) |
|
||||||
|
| `gfATO.dat` | 18–23000 Å | 230 万 | 238 MB | **全波段**:EUV+UV+光学+近/中红外(~62s/点) |
|
||||||
|
| `gfMOL.dat` | 844–999932 Å | 605 万 | 248 MB | 分子线(sdB 星非必需) |
|
||||||
|
| `gfTiO.dat` | 3738–999990 Å | 833 万 | 342 MB | TiO 分子线(冷星用) |
|
||||||
|
|
||||||
|
### 5.2 SYNSPEC 实际波长上限实测
|
||||||
|
|
||||||
|
SYNSPEC 源码 `RESOLV` 子程序(`synspec54.f:2403`)的频率网格构建逻辑决定了单次运行的波长上下限:
|
||||||
|
|
||||||
|
```
|
||||||
|
ALAM0 = 1.D-1 * ALAM0 ← 波长乘 0.1(单位转换)
|
||||||
|
ALAST = 1.D-1 * ALAST
|
||||||
|
NFREQ = 2 ← 只取首尾两点
|
||||||
|
FREQ(1) = c / ALAM0
|
||||||
|
FREQ(2) = c / ALAST
|
||||||
|
```
|
||||||
|
|
||||||
|
当波长范围过宽时,线表扫描(`synspec54.f:8207`)在某条线之后判定超出频率窗口,输出截断为 ~235 行无效数据。
|
||||||
|
|
||||||
|
实测边界(两个不同大气一致):
|
||||||
|
|
||||||
|
| 请求范围 | 实际输出 | 判定 |
|
||||||
|
|---------|---------|------|
|
||||||
|
| 50-20000 Å | 50-50 Å(235 行) | ✗ 截断 |
|
||||||
|
| 70-20000 Å | 70-70 Å(235 行) | ✗ 截断 |
|
||||||
|
| **100-20000 Å** | **100-20000 Å(540 万行)** | **✓** |
|
||||||
|
| 100-23000 Å | 100-23000 Å(640 万行) | ✓ |
|
||||||
|
| 100-24000 Å | 100-100 Å(235 行) | ✗ 截断 |
|
||||||
|
|
||||||
|
**可靠范围:100–23000 Å**。工作流配置取 **100-20000 Å**(留安全余量)。
|
||||||
|
|
||||||
|
### 5.3 配置变更
|
||||||
|
|
||||||
|
`workflows/sdB_cno.yaml`:
|
||||||
|
```yaml
|
||||||
|
# 新增:谱线表选择
|
||||||
|
linelist: gfATO.dat
|
||||||
|
|
||||||
|
synspec_input:
|
||||||
|
line6:
|
||||||
|
alam0: 100.0 # 原 3000.0 → 100.0
|
||||||
|
alast: 20000.0 # 原 7000.0 → 20000.0
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 代码改动:打通 linelist 配置链路
|
||||||
|
|
||||||
|
### 6.1 问题:linelist 是孤儿字段
|
||||||
|
|
||||||
|
`GridConfig.linelist: Option<String>`(`config.rs:1644`)在 YAML 中声明了但**从未被传递到 TaskSpec,也从未到达 node 端**。`embedded.rs` 硬编码 `"gfVIS99.dat"`。链路在 scheduler 第一步就断了。
|
||||||
|
|
||||||
|
### 6.2 改动清单(7 个文件)
|
||||||
|
|
||||||
|
完整链路(参照 `tlusty_input` 的传递模式):
|
||||||
|
|
||||||
|
```
|
||||||
|
YAML linelist → config.rs GridConfig.linelist → scheduler get_workflow_linelist
|
||||||
|
→ TaskSpec.linelist(serde_json 下发)→ executor 按 task.linelist 覆盖 + 按需下载
|
||||||
|
→ runner symlink fort.19
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `crates/common/src/models.rs` — TaskSpec 加字段
|
||||||
|
```rust
|
||||||
|
pub struct TaskSpec {
|
||||||
|
...
|
||||||
|
#[serde(default)]
|
||||||
|
pub linelist: Option<String>, // 新增
|
||||||
|
}
|
||||||
|
```
|
||||||
|
`#[serde(default)]` 保证旧 MQ payload 反序列化为 None → 用默认线表,向后兼容。
|
||||||
|
|
||||||
|
#### `crates/server/src/scheduler.rs` — 注入 linelist
|
||||||
|
```rust
|
||||||
|
async fn get_workflow_linelist(&self, workflow_name: &str) -> Option<String> {
|
||||||
|
let wf = self.db.get_workflow(workflow_name).await.ok()??;
|
||||||
|
let cfg = parse_grid_config_or_warn(&wf.config_yaml, workflow_name, "linelist")?;
|
||||||
|
cfg.linelist.clone()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
在 3 个生产 TaskSpec 构造点注入 `linelist: linelist.clone()`。
|
||||||
|
|
||||||
|
#### `crates/common/src/embedded.rs` — ensure_runtime 接收默认线表名
|
||||||
|
```rust
|
||||||
|
pub async fn ensure_runtime(
|
||||||
|
runtime_dir: &Path,
|
||||||
|
server_url: &str,
|
||||||
|
client: &Client,
|
||||||
|
default_linelist: &str, // 新增参数
|
||||||
|
) -> Result<RuntimePaths> { ... }
|
||||||
|
```
|
||||||
|
下载逻辑改用 `/api/data/file/{name}` 路由(复用 `ensure_specific_data_files` 的原子写机制)。
|
||||||
|
|
||||||
|
#### `crates/node/src/main.rs` — 传默认线表名
|
||||||
|
```rust
|
||||||
|
let runtime = ensure_runtime(runtime_dir, &node_cfg.server_url, &client, "gfATO.dat")
|
||||||
|
.await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `crates/node/src/executor.rs` — 按 task.linelist 覆盖 + 按需下载
|
||||||
|
在创建 `ExecutionRunner` 前,若 `task.linelist` 与默认不同,按需从服务端下载并构造覆盖了 `linelist` 路径的 `RuntimePaths`。
|
||||||
|
|
||||||
|
#### `workflows/sdB_cno.yaml` — 配置更新
|
||||||
|
```yaml
|
||||||
|
linelist: gfATO.dat
|
||||||
|
synspec_input:
|
||||||
|
line6:
|
||||||
|
alam0: 100.0
|
||||||
|
alast: 20000.0
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `docs/deployment.md` — 编译命令 + 线表部署说明
|
||||||
|
编译命令更新为 `-O3 -fno-toplevel-reorder -mcmodel=medium`,新增谱线表部署章节。
|
||||||
|
|
||||||
|
### 6.3 验证
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo test --workspace # 194 个测试全部通过
|
||||||
|
```
|
||||||
|
|
||||||
|
端到端(修复版 SYNSPEC + gfATO.dat + 100-20000Å):
|
||||||
|
```
|
||||||
|
波长范围: 100.0 - 20000.0 Å
|
||||||
|
总点数: 5428868, NaN: 0, 负值: 0
|
||||||
|
分波段: EUV 224万点 / UV 125万点 / 光学 87万点 / 近IR 107万点
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 部署清单
|
||||||
|
|
||||||
|
### 7.1 二进制替换
|
||||||
|
|
||||||
|
| 位置 | 旧版(-O3) | 新版(-O3 -fno-toplevel-reorder) |
|
||||||
|
|------|------------|----------------------------------|
|
||||||
|
| `dcts/assets/synspec_static` | 803512 bytes | 1000504 bytes |
|
||||||
|
| `dcts/assets/tlusty_static` | 1546432 bytes | 1957488 bytes |
|
||||||
|
| `synspec/synspec.exe` | 1044928 bytes | 同 assets/synspec_static |
|
||||||
|
| `tlusty/tlusty.exe` | 1997416 bytes | 同 assets/tlusty_static |
|
||||||
|
|
||||||
|
原版备份在 `~/tlusty_O3_backups_20260811/`。
|
||||||
|
|
||||||
|
### 7.2 线表部署
|
||||||
|
|
||||||
|
`gfATO.dat`(238MB)不入 git(`.gitignore` 排除 `assets/data/`),需手动放置:
|
||||||
|
```bash
|
||||||
|
cp /path/to/gfATO.dat assets/data/gfATO.dat
|
||||||
|
```
|
||||||
|
node 端首次连接时自动从 server `/api/data/file/gfATO.dat` 下载并缓存。
|
||||||
|
|
||||||
|
### 7.3 重算全部网格点
|
||||||
|
|
||||||
|
重新部署后,用项目本身的分布式计算重算。salvage 中有 8319 个最终大气(`.7`)可复用(TLUSTY 大气不受 SYNSPEC bug 影响),只需重跑 SYNSPEC 阶段。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 关键源码位置索引
|
||||||
|
|
||||||
|
| 内容 | 文件:行号 |
|
||||||
|
|------|----------|
|
||||||
|
| spec_is_valid 校验器 | `crates/common/src/conv_check.rs:290` |
|
||||||
|
| spec_is_valid 调用点 | `crates/common/src/runner.rs:645` |
|
||||||
|
| fort.55 生成器 | `crates/common/src/fort55_writer.rs:26` |
|
||||||
|
| SynspecInput 配置(9 子结构体) | `crates/common/src/config.rs:1184` |
|
||||||
|
| GridConfig.linelist 字段 | `crates/common/src/config.rs:1644` |
|
||||||
|
| TaskSpec.linelist 字段(新增) | `crates/common/src/models.rs:473` |
|
||||||
|
| ensure_runtime(默认线表参数) | `crates/common/src/embedded.rs:34` |
|
||||||
|
| executor linelist 覆盖(新增) | `crates/node/src/executor.rs:166` |
|
||||||
|
| SYNSPEC fort.7 写出(FLAM=FLUX*FREQ²*CAS) | `synspec/synspec54.f:3364` |
|
||||||
|
| SYNSPEC RESOLV 频率网格构建 | `synspec/synspec54.f:2403` |
|
||||||
|
| SYNSPEC IDSTD=0 自动取 2ND/3 | `synspec/synspec54.f:2152` |
|
||||||
|
| SYNSPEC Balmer 线系处理 | `synspec/synspec54.f:5310` |
|
||||||
|
| SYNSPEC OPAC 吸收系数计算 | `synspec/synspec54.f:4541` |
|
||||||
|
| PARAMS.FOR MFREQ=2000 | `synspec/PARAMS.FOR:11` |
|
||||||
|
| LINDAT.FOR MLIN0=1200000 | `synspec/LINDAT.FOR:1` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 技术原理:`-ftoplevel-reorder` 为何破坏 SYNSPEC
|
||||||
|
|
||||||
|
> 本节是源码级 + 编译器原理的深度分析,基于 GCC 官方文档、FORTRAN 77 标准、gfortran 社区经验与 SYNSPEC 源码核查。
|
||||||
|
|
||||||
|
### 9.1 `-ftoplevel-reorder` 具体做什么
|
||||||
|
|
||||||
|
**GCC 官方定义**:
|
||||||
|
|
||||||
|
> `-ftoplevel-reorder`(默认,`-O1` 起启用):允许重排顶层(top-level)函数、变量和 `asm` 语句,使它们不必按源文件中出现顺序输出。
|
||||||
|
>
|
||||||
|
> `-fno-toplevel-reorder`:不要重排,按源文件顺序输出。
|
||||||
|
|
||||||
|
| 属性 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| **重排对象** | 翻译单元内所有具有外部链接的程序单元(subroutine/function)和全局数据对象 |
|
||||||
|
| **默认启用** | `-O1` 及以上(精确解释了 `-O1`/`-O2`/`-O3` 全部 NaN、`-O0` 正常) |
|
||||||
|
| **优化目标** | 改善代码局部性(i-cache 命中率)、热点函数聚集、配合 profile-guided 优化 |
|
||||||
|
| **与 `-freorder-functions` 关系** | 后者是子集,只在有 profile 数据时按执行频率排序;`-ftoplevel-reorder` 是更通用版本,无 profile 时也基于编译器启发式重排 |
|
||||||
|
|
||||||
|
### 9.2 FORTRAN 77 的初始化语义与 SYNSPEC 的反模式
|
||||||
|
|
||||||
|
**FORTRAN 77 标准规则**:
|
||||||
|
1. DATA 语句在程序开始执行前完成初始化(load-time 静态初始化),只执行一次,隐含 `SAVE` 语义
|
||||||
|
2. COMMON 块的内存布局由各程序单元中 COMMON 语句的声明顺序决定
|
||||||
|
3. **BLOCK DATA 是标准指定的初始化命名 COMMON 块的唯一机制**;社区最佳实践是"每个 COMMON 块只在一个 BLOCK DATA 中初始化一次"
|
||||||
|
|
||||||
|
**SYNSPEC 源码核查结果**(subagent 对 `synspec54.f` 全文分析):
|
||||||
|
|
||||||
|
| 指标 | SYNSPEC | TLUSTY | 规范性 |
|
||||||
|
|------|---------|--------|--------|
|
||||||
|
| BLOCK DATA 单元 | **0 个** | 2 个(集中式初始化) | SYNSPEC 反模式 |
|
||||||
|
| DATA 语句 | **305 条**,散布在各子程序 | 集中在 BLOCK DATA | SYNSPEC 脆弱 |
|
||||||
|
| COMMON 声明 | **93 处**,跨程序单元复用 | 规范 | — |
|
||||||
|
|
||||||
|
SYNSPEC 把本应集中在 BLOCK DATA 里的 COMMON 块初始化**散布到 305 条子程序内 DATA 语句**中。这是 FORTRAN 77 的反模式,放大了内存布局对编译器符号排序的敏感度。
|
||||||
|
|
||||||
|
### 9.3 重排如何导致 NaN(破坏机理)
|
||||||
|
|
||||||
|
`-ftoplevel-reorder` 改变所有顶层符号在目标文件 `.o` 中的输出顺序。对 SYNSPEC 这类 F77 代码,破坏路径有两条:
|
||||||
|
|
||||||
|
**(A) COMMON 块合并/布局偏移(最可能)**
|
||||||
|
|
||||||
|
gfortran 对 COMMON 块的内部表示依赖符号在 `.o` 中的出现顺序来解析等价(EQUIVALENCE)和重复定义。SYNSPEC 不同子程序里同一 COMMON 块的声明在类型/长度上存在细微不一致(F77 代码中极常见)。重排改变了首次遇到的声明位置,使合并后的 COMMON 块布局偏移变化——**某些本应被 DATA 初始化的数组元素落到未初始化区域**。
|
||||||
|
|
||||||
|
**(B) 静态初始化符号被链接器丢弃**
|
||||||
|
|
||||||
|
DATA 初始化在某些目标格式下生成独立的初始化符号/构造器。重排可能让链接器认为某个初始化符号无引用而不被拉入(类似 BLOCK DATA 从静态库丢失的经典问题)。
|
||||||
|
|
||||||
|
### 9.4 为什么 NaN 恰好从 Balmer 跃迁(3646 Å)开始
|
||||||
|
|
||||||
|
SYNSPEC `HYLSET` 子程序(`synspec54.f:5310`)有明确判断:
|
||||||
|
|
||||||
|
```fortran
|
||||||
|
IF(FREQ(2).GE.3.28805E15) RETURN ! 短于 Balmer 系限就跳过
|
||||||
|
...
|
||||||
|
IF(AL1.LT.364.6) THEN ! 波长 > 3646 Å(364.6 nm)
|
||||||
|
ILOWH=2 ! 启用完整 Balmer 线系 Stark 轮廓计算
|
||||||
|
```
|
||||||
|
|
||||||
|
波长长于 3646 Å 后,SYNSPEC 额外读取以下被散布式 DATA 初始化的 COMMON 数组:
|
||||||
|
|
||||||
|
| COMMON 块 | 用途 | 初始化子程序 | 源码位置 |
|
||||||
|
|-----------|------|-------------|---------|
|
||||||
|
| `GOMOPA` | Gomez 氢线不透明度表 | `ghydop` | `synspec54.f:21700` |
|
||||||
|
| `callarda/b/g/c` | Allard 准分子卫星线数据 | `getlal` | `synspec54.f:12438` |
|
||||||
|
| `VOITAB` | Voigt 函数表 | `PRETAB` | `synspec54.f:12900` |
|
||||||
|
|
||||||
|
这些数组若有未初始化元素(NaN/垃圾值),`EXP(abl)`、对数插值等运算会瞬间传播 NaN 到整个输出。这**精确解释了 NaN 从 3646 Å 往长波方向蔓延(73% 失效),而非全局性失效或短波失效**。
|
||||||
|
|
||||||
|
### 9.5 为什么禁用后速度几乎不受影响(~4s vs ~3.5s)
|
||||||
|
|
||||||
|
`toplevel-reorder` 的设计目标对 SYNSPEC 完全不适用:
|
||||||
|
|
||||||
|
| 优化假设 | SYNSPEC 实际 |
|
||||||
|
|---------|-------------|
|
||||||
|
| 大量短函数、密集互调 → i-cache 局部性 | 单文件 23917 行,几十个大子程序 |
|
||||||
|
| 函数调用频繁,调用开销显著 | 运行时间全在少数热点子程序的**内部长循环**(Voigt 积分、辐射转移求解) |
|
||||||
|
| i-cache miss 是瓶颈 | 函数调用开销可忽略,无 i-cache 压力 |
|
||||||
|
|
||||||
|
真正的瓶颈是浮点运算和数组寻址,与符号顺序无关。测到的 ~14% 差异(4s vs 3.5s)更可能是测量噪声 + 其他 `-O3` 优化的副作用,**而非 toplevel-reorder 本身的收益**。
|
||||||
|
|
||||||
|
> **普遍结论**:对计算密集型 Fortran 科学代码,禁用 `-ftoplevel-reorder` 几乎零代价——社区已形成共识。
|
||||||
|
|
||||||
|
### 9.6 这是 gfortran bug 还是 F77 代码的问题
|
||||||
|
|
||||||
|
**双方都有责任**:
|
||||||
|
|
||||||
|
- **F77 代码(根本原因)**:SYNSPEC 用散布式 DATA 替代集中式 BLOCK DATA,违反"每个 COMMON 块只在一个 BLOCK DATA 中初始化"的最佳实践。FORTRAN 标准不保证 COMMON 块在不一致声明下的行为,所以编译器重排不算违规。
|
||||||
|
- **gfortran(暴露问题)**:对 F77 legacy 代码默认开启 `-ftoplevel-reorder` 是容易踩坑的默认值。
|
||||||
|
|
||||||
|
**社区已形成共识**:对传统 F77 代码,`-fno-toplevel-reorder` 与 `-std=legacy`、`-fno-automatic`、`-fno-align-commons` 并列为推荐编译选项。
|
||||||
|
|
||||||
|
遭遇同类问题的科学计算项目:
|
||||||
|
- **CMAQ / I/O API**:COMMON 块初始化受重排影响(CMAS Center 文档明确讨论)
|
||||||
|
- **Open MPI Fortran 代码**:`-O1`(启用 toplevel-reorder)导致问题,`-O0` 正常(Stack Overflow)
|
||||||
|
- **GEOS-Chem**:推荐 `-ffpe-trap` 排查此类 NaN + 保守优化(Harvard Wiki)
|
||||||
|
- **Intel IFX**:同样的 BLOCK DATA 丢失问题,连商业编译器也难幸免
|
||||||
|
|
||||||
|
### 9.7 相关 GCC Bugzilla 与社区参考
|
||||||
|
|
||||||
|
- GCC bugzilla fortran/29537、fortran/47030、fortran/96839(COMMON + BLOCK DATA 边缘相关)
|
||||||
|
- Gentoo Bug 724314(验证 `-ftoplevel-reorder` 在 `-O2` 启用)
|
||||||
|
- Stack Overflow "What does Top level reordering mean?"(Open MPI 类似问题)
|
||||||
|
- `gui/synple/synspec/makefile` 的原版 Makefile `OPT = -fno-automatic -mcmodel=medium` **未含 `-fno-toplevel-reorder`**——这就是 bug 触发点,本次修复补上了这个遗漏
|
||||||
|
|
||||||
|
### 9.8 长期修复方向
|
||||||
|
|
||||||
|
短期:`-fno-toplevel-reorder` 是性价比最高的方案,无性能损失。
|
||||||
|
|
||||||
|
长期(若要彻底修复代码而非依赖编译开关):把 SYNSPEC 中散落在各子程序的 305 条 COMMON + DATA 初始化**集中到统一的 BLOCK DATA 单元**(参照 TLUSTY 的做法),或迁移到 Fortran 90 MODULE。但对 23917 行的生产代码工程量大,非必要不折腾。
|
||||||
|
|
||||||
|
### 9.9 推荐编译选项(传统 F77 代码通用)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gfortran -O3 -fno-toplevel-reorder -fno-automatic -mcmodel=medium \
|
||||||
|
[-std=legacy] [-fno-align-commons] \
|
||||||
|
[-ffpe-trap=invalid,zero,overflow -fbacktrace -g] # 调试时加
|
||||||
|
synspec54.f -o synspec_static
|
||||||
|
```
|
||||||
|
|
||||||
|
| 选项 | 作用 | 必要性 |
|
||||||
|
|------|------|--------|
|
||||||
|
| `-fno-toplevel-reorder` | **关键**:禁用符号重排,恢复 COMMON 块布局 | 必需 |
|
||||||
|
| `-fno-automatic` | 局部变量静态化(F77 隐式 SAVE 语义) | 必需 |
|
||||||
|
| `-mcmodel=medium` | 大型 COMMON 块 64 位寻址 | 必需(SYNSPEC) |
|
||||||
|
| `-std=legacy` | 允许 F77 过时语法 | 可选 |
|
||||||
|
| `-fno-align-commons` | 禁止 COMMON 块对齐填充 | 可选 |
|
||||||
|
| `-ffpe-trap` + `-fbacktrace` | 调试:NaN 即时 SIGFPE + 回溯到源码行 | 仅调试 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. TLUSTY 冷启动未收敛网格点归因分析(-ftoplevel-reorder 洗清嫌疑)
|
||||||
|
|
||||||
|
> 本节回答用户追问:"tlusty 会因为这个原因(`-ftoplevel-reorder`)出现问题吗?"以及"目前 tlusty 收敛失败的网格点失败原因会不会有这个的原因?"。
|
||||||
|
> 结论先行:**TLUSTY 收敛失败与 `-ftoplevel-reorder` 无关**——冷启动(`cold_run`)链中 lte→nc→nl 的发散是 TLUSTY 物理/数值收敛问题,由灰色启动的迭代放大导致,与编译符号重排无关。
|
||||||
|
|
||||||
|
### 10.1 为什么先前用种子步进点测试是误导(用户纠正)
|
||||||
|
|
||||||
|
先前对种子步进(`seed_step`)策略点 `t20000_g6.5_he2_c-1_n-2_o-2` 做了两版二进制对照:原版 `-O3` 与修复版 `-O3 -fno-toplevel-reorder` 分别重跑 `seed_nc` + `nl`,两版 `fort.9` 逐字节相同(`seed_nc` max_relc=7.45E-03),`nl` 阶段都产生完全相同的 6750 个 NaN。结论虽是"无关",但测试对象选错了:
|
||||||
|
|
||||||
|
- 种子步进只在**冷启动失败后**才启动(`pending_strategies` 升级链:`cold_run` → `cold_run,seed_step` → `seed_step`)。
|
||||||
|
- 当前网格(稀疏)下,从最近收敛点做种子步进本就难以收敛,属预期行为——**种子步进失败不能代表冷启动失败**。
|
||||||
|
- 用户要求:对比分析**冷启动(`cold_run`,lte→nc→nl)未收敛的网格点**,看这些失败是否与 `-ftoplevel-reorder` 有关。
|
||||||
|
|
||||||
|
### 10.2 冷启动失败的规模与模式(DB + salvage 统计)
|
||||||
|
|
||||||
|
**DB(`/home/fmq/下载/dcts.db`,grid_points)**:
|
||||||
|
- `tlusty_status='failed'`:1105 个网格点,全部经过多次重试(attempt_count 5-25),最终失败点均无 `tlusty_success_method`。
|
||||||
|
- 失败点全部经历过冷启动失败后进入种子步进(无一个"纯冷启动"失败点,这是升级链策略使然)。
|
||||||
|
|
||||||
|
**salvage 冷启动产物(`nc_chmax0.001.9` 是纯冷启动文件——种子步进用 `seed_nc` 前缀,不覆盖 `.nc.*`)**:
|
||||||
|
- 3986 个含冷启动链(lte/nc/nl 阶段)的目录中:lte 全部收敛(灰色启动,NITER=0),**nc 阶段 3983/3986 失败**。
|
||||||
|
- 解析冷启动 `nc` 的 `fort.9` 逐迭代 max_relc:
|
||||||
|
|
||||||
|
| nc 冷启动结局 | 数量 | 占比 | 特征 |
|
||||||
|
|--------------|------|------|------|
|
||||||
|
| 有限发散(无 NaN) | 6170 | 88% | max_relc 从 iter1 的 ~1-1000 单调放大到 iter10 的 1e12~1e29 |
|
||||||
|
| NaN 发散 | 699 | 10% | iter k 出现 NaN,之后全部 NaN |
|
||||||
|
| 收敛 | 0 | 0% | 冷启动 nc 零收敛(这就是冷启动失败的全部来源) |
|
||||||
|
|
||||||
|
- 有限发散的典型轨迹(`t45000_g5.0_he-2_c-1_n-3_o-4`):iter1→15.4,iter2→35.9,iter3→244,…,iter9→1.02e11,iter10→4.46e15。**逐迭代单调放大**是牛顿迭代从远初始猜测发散的经典形态,且发散集中在深层(worst_depth≈44-50/50,POP 列最大)。
|
||||||
|
- NaN 发散在高 Teff 区更常见(45000-60000K 占 699 例中的 545 例)。
|
||||||
|
|
||||||
|
### 10.3 冷启动失败的完整链条机制(两类)
|
||||||
|
|
||||||
|
**A. 真发散(无 NaN,~88%)**:nc 从灰色 LTE 大气出发,POP/TEMP/NE 的相对变化逐迭代放大(iter1 已 >1),10 次迭代后 max_relc 达 1e14~1e29 量级但数值仍有限。nl 从该发散大气继续,iter 6 内 `**** STOP in SOLVE` 终止(`t50000_g5.0_he-4_c-2_n-3_o-1`:nc 3.04e14 → nl 1.3e18,无 NaN)。
|
||||||
|
|
||||||
|
**B. NaN 污染 + 假收敛(~10%)**:nc 在 iter k 突发 NaN(POP 溢出)→ `fort.7` 大气含 NaN → nl 从 NaN 大气求解,`fort.9` 全零(`0.00E+00`、ilev=0、ifr=9 失败标志)→ check_fort9 判 max_relc=0.0 < chmax → **nl 假收敛**。最终靠 `atmosphere_has_nan` 兜底判失败(`t50000_g5.0_he-2_c-2_n-3_o-1`:nc iter2→NaN,nl max_relc=0.0 1 迭代)。
|
||||||
|
|
||||||
|
### 10.4 对照实验:两版二进制重跑完整冷启动链
|
||||||
|
|
||||||
|
**重放 harness**(`/tmp/replay_cold.sh`):对 salvage 中挑选的冷启动失败点,用其 `.lte.5/.lte.nst/.nc.5/.nc.nst/.nl.5/.nl.nst` 输入,按 runner 的 stage 语义重放:
|
||||||
|
- lte:`fort.5`=lte.5,`nst`=lte.nst,ltgray=T 无需 `fort.8`(灰色启动)
|
||||||
|
- nc:`fort.5`=nc.5,`fort.8`=lte 的 `fort.7`
|
||||||
|
- nl:`fort.5`=nl.5,`fort.8`=nc 的 `fort.7`
|
||||||
|
- `data` 软链 → 运行时原子数据集(与 node 端相同)
|
||||||
|
|
||||||
|
**对照组设置**:初版对照用"原版备份二进制"(`~/tlusty_O3_backups_20260811/tlusty.exe.O3.bak`,纯 `-O3`),但发现该备份带有 `READ LINE` 输入回显(源码 `tlusty208.f:30627` 的注释调试行,旧构建遗留,仅影响输出不影响数值),属混叠源。为严谨起见,用**当前源码**重编译一对干净二进制:`gfortran -O3 -fno-automatic -mcmodel=medium tlusty208.f`(开 toplevel-reorder,即"原版语义")与 `gfortran -O3 -fno-toplevel-reorder -fno-automatic -mcmodel=medium`(修复版)——**唯一差异就是 `-ftoplevel-reorder` 这一个 flag**(后者的 md5 与生产 `assets/tlusty_static` 完全一致 77721c4c,验证编译可复现)。
|
||||||
|
|
||||||
|
**harness 自校验**:
|
||||||
|
- 重放的 lte.7 与历史 lte.7 逐字节比较,5210 行中仅 3 行不同且差异为 ~1e-8~1e-6 相对量级(机器级舍入噪声)。
|
||||||
|
- 重放的 nc.9 与历史 `nc_chmax0.001.9` 比较:iter1 50 行仅 16 行末位舍入差异(如 -9.05E-03 vs -9.04E-03),iter2 NaN 行逐字节相同 → 重放忠实复现历史冷启动失败。
|
||||||
|
|
||||||
|
**点 B `t50000_g5.0_he-2_c-2_n-3_o-1`(NaN 污染类,10% 类)结果**:
|
||||||
|
|
||||||
|
| 阶段 | 历史结局 | 干净原版(-O3) vs 修复版(-O3-fno-toplevel-reorder) 重放 |
|
||||||
|
|------|---------|------------------------------------------------------|
|
||||||
|
| lte | 灰色启动 OK | **fort.7 逐字节一致** |
|
||||||
|
| nc | iter1=1490 → iter2 全 NaN | **fort.9 逐字节一致**(103 行,iter1=1.490e+03、iter2 全 NaN);**fort.7 逐字节一致**(3659 行 NaN/坏行) |
|
||||||
|
| nl | max_relc=0.0 假收敛 + NaN 大气 | **fort.9 逐字节一致**(53 行全 0.00E+00);**fort.7 逐字节一致**(5200 行 NaN/坏行) |
|
||||||
|
|
||||||
|
> 点 B 的三阶段**全部逐字节一致**。nc 从灰色启动发散到 NaN(iter2)、nl 假收敛、NaN 大气——这一整套冷启动失败在两种编译配置下完全相同的位级复现。`-ftoplevel-reorder` 对该点冷启动失败**零影响**。
|
||||||
|
> 注:初版对照(原版备份二进制 vs 修复版)在 nc.7 出现 16 行 NaN↔0.0 位置差异——那是已发散垃圾值行的格式差异(语义等价:大气同样死亡),且来源于备份二进制的旧源码状态混叠;干净对照对消除了该混叠后完全逐字节一致。
|
||||||
|
|
||||||
|
**点 A `t50000_g5.0_he-4_c-2_n-3_o-1`(真发散类,88% 类)结果**:
|
||||||
|
|
||||||
|
| 阶段 | 历史结局(生产二进制) | 干净原版(-O3) vs 修复版(-O3-fno-toplevel-reorder) 重放 |
|
||||||
|
|------|---------------------|------------------------------------------------------|
|
||||||
|
| lte | 灰色启动 OK | **fort.7 逐字节一致** |
|
||||||
|
| nc | iter1..10 = 789→338→2120→498→…→7.49e9→3.04e14 发散 | **fort.9 逐字节一致**(503 行,迭代序列 7.89e2→3.38e2→2.12e3→4.98e2→3.91e6→3.61e10→3.04e2→4.5e5→7.48e9→2.95e14,与历史仅末位舍入差);**fort.7 逐字节一致**(0 NaN 行) |
|
||||||
|
| nl | iter6 STOP,max_relc 1.3e18 | **fort.9 逐字节一致**(303 行,序列 7.76e5→4.22e6→1.96e6→1.02e7→1.89e9→1.80e18,iter6 STOP);**fort.7 逐字节一致**(0 NaN 行) |
|
||||||
|
|
||||||
|
> 点 A 三阶段**全部逐字节一致**。真发散的 10 次 nc 迭代轨迹、nl 的 iter6 求解器 STOP、无 NaN 大气——在两种编译配置下完全相同的位级复现。
|
||||||
|
|
||||||
|
### 10.5 结论
|
||||||
|
|
||||||
|
两个代表点覆盖了冷启动失败的两种主要形态(**88% 有限发散** + **10% NaN 污染**),用**同源码仅差 `-ftoplevel-reorder` 一个 flag** 的干净二进制对照对重跑完整冷启动链(lte→nc→nl),三个阶段的 `fort.9` 与 `fort.7` **全部逐字节一致**:
|
||||||
|
|
||||||
|
1. **TLUSTY 冷启动失败与 `-ftoplevel-reorder` 无关**——发散轨迹(逐迭代 max_relc 序列)、NaN 模式(何时出现 NaN、多少行)、求解器 STOP 行为、最终大气,均在开关两侧完全一致。
|
||||||
|
2. 冷启动失败的本质:**灰色 LTE 启动(lte 阶段)在稀疏高 Teff/logg 网格上,nc 阶段 NLTE 牛顿迭代物理发散**(逐迭代单调放大 1e1→1e14,或突发 NaN),nl 从发散大气出发随即失败/假收敛。这是 TLUSTY 求解器的数值收敛特性,不是编译产物差异。
|
||||||
|
3. 两种失败形态的链机制:**A(真发散)**= nc 有限放大到 1e14+ → nl iter≤10 内 `STOP in SOLVE`;**B(NaN 污染)**= nc iter k 突发 NaN → nl 从 NaN 大气求解 `fort.9` 全零(ilev=0/ifr=9 失败标志)→ check_fort9 误判 max_relc=0.0 **假收敛** → 由 `atmosphere_has_nan` 兜底判失败。
|
||||||
|
4. **项目侧含义**:修复版二进制在 TLUSTY 阶段可安全替代原版——salvage 中 8319 个已收敛大气可复用(结果舍入噪声级一致,已由 lte.7/nc.9 逐字节验证),重算全部网格点时 TLUSTY 结果的失败/收敛行为与旧版完全一致,不存在"换了二进制就多了失败点/少了收敛点"的风险。
|
||||||
|
5. **方法论教训**:种子步进点不能用于归因冷启动失败——种子步进只在冷启动失败后才启用,稀疏网格下种子难收敛是预期行为。归因必须用冷启动(lte→nc→nl)产物(salvage 中 `.nc.*`/`.lte.*` 文件是纯冷启动产物,种子链用 `seed_nc` 前缀不覆盖)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 经验教训
|
||||||
|
|
||||||
|
1. **文件大小正常 ≠ 内容有效**:SYNSPEC 的 `.spec` 都是 ~11MB/432827 行,NaN 混在其中不改变文件大小,必须做内容校验。
|
||||||
|
2. **gfortran -O3 对老 Fortran 代码不安全**:`-ftoplevel-reorder` 破坏 COMMON 块初始化顺序是已知类别的 bug。传统 FORTRAN 77 代码(TLUSTY/SYNSPEC)应加 `-fno-toplevel-reorder`。
|
||||||
|
3. **浮点陷阱调试法高效**:`-ffpe-trap=invalid,zero,overflow` + `-O0` 能快速区分"源码 bug"vs"编译器 bug"——本例中 `-O0` 版正常立即排除了源码逻辑错误。
|
||||||
|
4. **二分法定位优化项**:gfortran `-O1` 启用 43 项优化,逐步二分比逐项全测高效。本例中 `-ftoplevel-reorder` 单独就触发段错误,是明确元凶。
|
||||||
|
5. **孤儿字段是配置断裂的常见模式**:`linelist` 在 YAML 声明了但从未下发——这种"看似可配实则死字段"在大型项目中容易潜伏,需要端到端验证。
|
||||||
@@ -0,0 +1,394 @@
|
|||||||
|
# 冷启动 nc 阶段不收敛:源码根因 + 穷举复测(2026-08-13)
|
||||||
|
|
||||||
|
**数据来源**:`data/salvage/`(8320 任务产物)+ `/home/fmq/下载/dcts.db`(server 快照)
|
||||||
|
**源码**:`tlusty/tlusty208.f`
|
||||||
|
**复测目录**:`test/20260813_cold_nc_trace_fix/convergence/t60000_g5.0_he-4_c-4_n-4_o-3_cold/`(代表点)
|
||||||
|
**前序文档**:`tlusty_divergence_root_cause_2026_08_11.md`、`cold_start_fix_2026_08_12.md`
|
||||||
|
|
||||||
|
> 本文是对前序"参数调优无效、需连续法"结论的**源码级复核 + 穷举重测**。
|
||||||
|
> 用户明确指示:前序测试结果不可信,需重测;不要测种子步进,聚焦冷启动失败。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、结论先行
|
||||||
|
|
||||||
|
1. **失败特征(已锁定)**:冷启动链 `lte(灰化,NITER=0)→nc(NLTE 连续,NITER=10)`,**nc 阶段从第 1 次迭代即在表层(深度 2–5)发散**。POP(布居数)列始终是 MAXIMUM 且雪崩(iter1≈140→iter10≈1e16),RAD(辐射场)列全程很小(0.01–0.26)。lte 阶段正常。
|
||||||
|
|
||||||
|
2. **源码根因**:nc 阶段的 Newton 完全线性化求解器在「高 Teff(≥50kK) + 低 logg(≤5.5) + 贫氦(he≤−2)」参数角的表层 NLTE 方程**雅可比矩阵病态**,灰化 LTE 初值不在收敛域内。是**布居数(尤其表层过渡电离级的 C/N/O)**驱动,不是辐射场。
|
||||||
|
|
||||||
|
3. **🔴 关键更正——为何前序所有"调参无效"结论不可信**:TLUSTY 的收敛判据 `CHMX`(fort.9 的 max_relc)取自 `BET(I,ID)=CHAN`(`tlusty208.f:14643`),该赋值在 **ORELAX 阻尼(:14647) 与 DPSILG 限幅(:14652) 之前**。即 **CHMX 用的是未阻尼、未限幅的原始 Newton 修正量**,对 ORELAX / DPSILG / ITEK / IACC **完全不敏感**。前序文档测试的恰恰全是这几个参数,并用 CHMX 判定"无效"——**测了一个对这些参数不敏感的指标**。这正是用户判定"前序结果不可信"的底层原因。
|
||||||
|
|
||||||
|
4. **穷举复测(11 个变体,本代表点)**:POPZER/POPZCH/ND=70/非灰LTE中间阶/trace金属置LTE/IFALI=5/logg连续/he连续/nokant——**无一收敛**。nc 的 Newton CL 在该点确实无单参数解。
|
||||||
|
|
||||||
|
5. **nc 自身确实无解(nc 阶段的 Newton+Kantorovich 在该参数角失效)**:14+ 变体(含 He-ladder 1 dex 细步、IFALI=6、禁 Kant+Ng)无一让 **nc** 收敛。不稳性在 nc 迭代的加速环节,换起点救不回 nc 自身。
|
||||||
|
|
||||||
|
6. **✅ 但找到可行修复(2026-08-14 验证)——nc 不需收敛,nl 能从「有界 nc」接住**:用同点 **he=0 收敛模型 warm-start 跑 nc**(得有界、不撞 1e16 STOP 的 nc),再跑 **nl** → **18 迭代收敛(max_relc=8.06e-4<1e-3)、温度结构物理(表层 0.77×Teff)**。冷启动 nc 因撞 1e16 STOP 模型损坏,nl 接不住。**判据是"nc 不撞 STOP 保持有界",不是"nc 收敛"。**
|
||||||
|
|
||||||
|
7. **生产没走通是 seed_finder 两个 bug**:(a) `seeds.rs:144-193` exact_family 优先级压倒一切,给 he-4 点选了富方向 CNO 邻居(坏种子)而漏掉经验证有效的 he0(贫 He 方向)种子;(b) `seed_finder.rs:51` `d_teff<5000.0`(严格)排除相邻 Teff 档(网格步长正好 5000K)。**这正是用户"多步种子步进/向四周扩散"思路要解决的,且已验证机制有效。**
|
||||||
|
|
||||||
|
> 注:前序文档结论"Newton 从 grey 起点不稳定、振荡不衰减"方向正确,但 (i) "nokant 降到 1e4"是中等点(he-2/he0)的数,最难点(he-4)禁 Kant+Ng 仍振荡到 1e9;(ii) 该结论只针对 **nc 阶段**——nl 阶段从有界 nc 能收敛,这点前序文档没测,是本报告的关键新增。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、数据证据
|
||||||
|
|
||||||
|
### 2.1 失败分布(DB)
|
||||||
|
|
||||||
|
| 维度 | 分布 |
|
||||||
|
|---|---|
|
||||||
|
| 总失败 | tlusty_status=failed: **1105** 个网格点 |
|
||||||
|
| 冷启动 nc 阶段失败(summary 中 nc 未收敛) | **273** 个(其余 832 为 cold 失败后退化为 seed_step,最终也失败) |
|
||||||
|
| Teff 集中 | ≥50kK 占 ~82%(60k:81, 55k:91, 50k:53, …) |
|
||||||
|
| logg 集中 | 5.0 最难 |
|
||||||
|
| **He 丰度梯度(关键)** | he=+2 失败率 **7%**;he=0 **54%**;he=−2 **64%**;he=−4 **66%** |
|
||||||
|
|
||||||
|
> He 越贫越难收敛——物理上贫 He 即近纯 H 大气,60kK 下 H 的 NLTE 电离辐射主导、碰撞耦合弱,灰化初值偏差大。
|
||||||
|
|
||||||
|
### 2.2 代表点失败轨迹(baseline,实际 fort.9 逐迭代)
|
||||||
|
|
||||||
|
代表点 `t60000_g5.0_he-4_c-4_n-4_o-3`(60k/g5.0/he−4),nc 阶段 NITER=30:
|
||||||
|
|
||||||
|
```
|
||||||
|
iter depth TEMP NE POP RAD MAX 发散变量
|
||||||
|
1 2 1.25E+01 -1.83E+01 -1.40E+02 2.60E-01 1.40e+02 ilev75(C)
|
||||||
|
2 3 3.16E+02 -2.68E+02 -1.95E+03 -6.19E-02 1.95e+03 ilev75(C)
|
||||||
|
...
|
||||||
|
10 4 1.47E+05 -4.45E+08 -2.19E+15 -1.15E-01 2.19e+15
|
||||||
|
14 5 7.29E+05 -6.57E+08 -2.70E+16 2.60E+00 2.70e+16 → STOP(>1e16)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **表层(深度 2–5)**驱动;深层(>15)iter1 max|POP| 仅 0.99(正常)。
|
||||||
|
- POP 始终是 MAXIMUM;RAD 全程 ≤2.6(直到 POP 到 1e15 才被带坏)。
|
||||||
|
- T、NE 的未限幅修正也很大(iter1 T=+12.5 即 +1250%),但都小于 POP。
|
||||||
|
|
||||||
|
### 2.3 灰化 LTE 初值本身是物理的
|
||||||
|
|
||||||
|
`check_temperature_structure`(DCTS 权威判据)对 lte.7:
|
||||||
|
- 表层 T=48670K(0.81×Teff,Eddington 灰化参考 0.84)✅
|
||||||
|
- 无 NaN ✅
|
||||||
|
|
||||||
|
→ **初值不是病态的**,只是不在 NLTE 收敛域内。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、源码级根因(逐行核对)
|
||||||
|
|
||||||
|
### 3.1 修正量计算与诊断脱钩 —— `tlusty208.f:14639-14681`(SOLVE)
|
||||||
|
|
||||||
|
```fortran
|
||||||
|
DO I=1,N
|
||||||
|
DPSI(I)=BET(I,ID)-VECL(I)
|
||||||
|
CHAN=0.
|
||||||
|
IF(PSI0(I).GT.0.) CHAN=DPSI(I)/PSI0(I) ! 原始 Newton 相对修正(无下限保护)
|
||||||
|
BET(I,ID)=CHAN ! :14643 存【未阻尼、未限幅】原始值 ← CHMX 来源
|
||||||
|
IF(I.GE.NFREQE+INSE) CHAN=ORELAX*CHAN ! :14647 ORELAX 只作用于布居,且在 BET 之后
|
||||||
|
IF(CHAN.LE.UN/DPSILG-UN) CHAN=UN/DPSILG-UN ! :14652 DPSILG 限幅(在 BET 之后)
|
||||||
|
...(DPSILT/DPSILN/DPSILD 逐变量更严限幅)...
|
||||||
|
PSI0(I)=PSI0(I)*(CHAN+UN) ! :14681 实际更新(限幅后,有界)
|
||||||
|
END DO
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键推论**:
|
||||||
|
- `BET`(:14643)= **原始**修正 → PRCHAN(:22876) 据此算 `CHMX` → 收敛判据 `LFIN=ABS(CHMX).LE.CHMAX`(:14728) 与雪崩 STOP `ABS(CHMX).GT.1.D16`(:14700) **全用这个未限幅值**。
|
||||||
|
- ORELAX(:14647)、DPSILG(:14652) **都作用在 BET 之后**,**不改变 CHMX**。
|
||||||
|
- 实际模型更新(:14681)有限幅、始终正、**不会 NaN**(实测:所有发散运行的 nc.7 都无 NaN、过温度结构判据)。
|
||||||
|
- **所以"STOP after ITER N, Max change 1e16"是诊断(未限幅)触发的,不是模型真炸**。但模型也确实没收敛(表层 T 偏离收敛参考)。
|
||||||
|
|
||||||
|
**POP 分母无下限**:`IF(PSI0(I).GT.0.)`(:14642) 是唯一保护;无 `DMAX1(PSI0,eps)`。表层贫 He 下 C I 的 PSI0 极小 → CHAN 极大。
|
||||||
|
|
||||||
|
### 3.2 POPZER/POPZCH —— 本应保护 trace 物种,默认值形同虚设
|
||||||
|
|
||||||
|
| 参数 | 默认 | 作用 | 行号 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `POPZER` | 1e-20 | 布居/POPM<POPZER 的组冻结(POPGRP=0, IGZERO=1),其 Newton 修正强制为 0(VECL=0, :22750/:22756) | :6160 |
|
||||||
|
| `POPZCH` | 1e-15 | 收敛报告阈值:RPOP0<POPZCH 的物种**排除出 CHMX/STOP 判据** | :22910/:22925 |
|
||||||
|
| `NITZER` | 1 | 冻结/超零化的激活迭代数 | :5864 |
|
||||||
|
|
||||||
|
表层过渡电离级 C I/C II 在 60kK 的 RPOP0 ≈ 1e-10~1e-12 —— **高于** POPZCH(1e-15) 与 POPZER(1e-20),故两项保护都**不触发**。理论上调高 POPZER/POPZCH 能把它们排除——但实测不行(见 §四)。
|
||||||
|
|
||||||
|
### 3.3 ORELAX 作用域(更正前序 A3)
|
||||||
|
|
||||||
|
`ORELAX` 只作用于布居(`I≥NFREQE+INSE`)。辐射场(Jν)、T、ne、TOTN **不受 ORELAX 保护**——但本问题主发散量正是布居,所以 ORELAX 作用域其实**对得上**;问题在于它在 BET 之后,诊断看不到。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、穷举复测(代表点,test/20260813_cold_nc_trace_fix/)
|
||||||
|
|
||||||
|
判据:fort.9 末次 max_relc **且** nc.7 温度结构/NaN。
|
||||||
|
|
||||||
|
| # | 变体 | 末 max_relc | STOP | nc.7 NaN? | 结论 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 1 | baseline (NITER=30) | 2.7e16 | iter14 STOP | 否 | 复现失败 ✅ |
|
||||||
|
| 2 | POPZER=1e-9 | 2.78e16 | iter8 | 否 | 冻结 trace 反把 RAD 带崩 |
|
||||||
|
| 3 | POPZER=1e-12 | 4.22e17 | iter20 | 否 | 太松,无效 |
|
||||||
|
| 4 | POPZCH=1e-9 | 2.7e16 | iter14 | 否 | 发散物种 RPOP0>1e-9,未排除 |
|
||||||
|
| 5 | ND=70 | 3.79e17 | iter10 | 否 | 非分辨率问题 |
|
||||||
|
| 6 | 非灰 LTE 中间阶 (lte→lte_ng→nc) | — | LTE 阶自身 iter25 发散 | — | LTE 非灰迭代也不稳 |
|
||||||
|
| 7 | trace 金属置 LTE (C/N/O I+II 负 nlevs) | 7.82e17 | iter5 | 否 | 剩余 NLTE 物种仍崩 |
|
||||||
|
| 8 | 完整生产 nst (IFALI=5 等) | 2.7e16 | iter14 | 否 | 与 baseline 逐迭代一致 |
|
||||||
|
| 9 | warm-start from g5.5 (Δlogg 0.5) | 3.19e16 | iter8 | 否 | iter1=6.49(小)但 iter2 爆 |
|
||||||
|
| 10 | **warm-start from he0 (ΔlogHe 2)** | 1.13e6 | 无STOP(跑满30) | **否** | **iter1=4.3(最优)**,全程有界 |
|
||||||
|
| 11 | warm he0 + ITEK=999 (禁 Kantorovich) | 2.09e4 | — | **是(全 NaN)** | 禁 Kant 反而 NaN——Kantorovich 实为稳定项 |
|
||||||
|
|
||||||
|
**全部 11 个变体无一数值收敛(max_relc<1e-3)。**
|
||||||
|
|
||||||
|
### 4.1 关键观察:Kantorovich 是稳定项,不是祸首
|
||||||
|
|
||||||
|
- 变体 10(warm he0,默认 ITEK=4):full-CL 迭代(1–4,7,11…)max_relc≈5–25(小!),Kantorovich 迭代(5–6,8–10…)飙到 1e4–1e10——但模型**始终有界无 NaN**。
|
||||||
|
- 变体 11(warm he0,ITEK=999 纯 full-CL):10 迭代后**全深度 NaN**。
|
||||||
|
- → **Kantorovich 近似算子反而在防止 NaN**。前序"禁 Kantorovich 能降 1e12"的观察只看到了 CHMX(未限幅诊断)下降,没看到纯 full-CL 会 NaN。
|
||||||
|
|
||||||
|
### 4.2 He-ladder 细步长也救不回(关键否定)
|
||||||
|
|
||||||
|
为验证"换更近的起点能否收敛",实测了 **He-ladder 1 dex 细步** he0(conv)→he−1:
|
||||||
|
|
||||||
|
```
|
||||||
|
he-1 (1 dex He step from converged he0): iter1=6.82, iter2-4=100/92/152, iter5=1.1e6(Kant), iter18=2.9e17(STOP)
|
||||||
|
```
|
||||||
|
|
||||||
|
**iter1 仍很小(6.82)**——说明 1 dex He 步长的起点质量很好;但 **iter5 起 Kantorovich 照样爆炸**。这与 2 dex 单跳(he0→he−4,iter1=4.3)、logg 连续(g5.5→g5.0,iter1=6.49)的模式完全一致:
|
||||||
|
|
||||||
|
| warm-start | iter1 | iter5(Kant) | 结局 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| g5.5→g5.0 (Δlogg 0.5) | 6.49 | 爆 | STOP iter8 |
|
||||||
|
| he0→he−4 (ΔlogHe 2) | 4.29 | 爆 | 跑满30, 无STOP, 有界 |
|
||||||
|
| he0→he−1 (ΔlogHe 1) | 6.82 | 爆 | STOP iter18 |
|
||||||
|
|
||||||
|
**结论**:**不稳性在 nc 迭代的 Kantorovich 加速环节(iter≥5),不在起点**。无论起点多好(iter1 多小),iter5 起的 Kantorovich 都会爆炸。换起点/He 连续/步长都救不回——**这是 nc 阶段 Newton+Kantorovich 求解器在该参数角的结构性失效**。
|
||||||
|
|
||||||
|
> warm-start 模型全程**有界无 NaN**(Kantorovich 近似算子反而在防 NaN,见 §4.1),但 CHMX 恒高(未真收敛),表层 T 偏离收敛参考(0.70×Teff vs 0.78×Teff),**不可直接当科学结果用**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、可行修复方向
|
||||||
|
|
||||||
|
### 🔴 重大更新(2026-08-14):nc 不需收敛,nl 能从「有界 nc」接住 — 已验证
|
||||||
|
|
||||||
|
重新认识生产的真实收敛机制后,**找到了可行修复**。DB 显示 55k 同族点(已收敛)的成功路径是:
|
||||||
|
```
|
||||||
|
seed_nc: converged=False (max_relc=0.00461) ← nc 没收敛但有界
|
||||||
|
nl: converged=True (max_relc=0.0004) ← nl 阶段补上最终收敛
|
||||||
|
```
|
||||||
|
即生产**接受未收敛的 seed_nc,靠 nl(带谱线、NITER=100、ORELAX=0.5)做最终收敛**。瓶颈不是 nc 收不收敛,而是 **nc 模型是否足够有界(不撞 1e16 STOP)让 nl 接得住**。
|
||||||
|
|
||||||
|
**验证**(代表点 `t60000_g5.0_he-4_c-4_n-4_o-3`,所有单参数法都失败的最难点):
|
||||||
|
|
||||||
|
| 路径 | nc 状态 | nl 结果 |
|
||||||
|
|---|---|---|
|
||||||
|
| 冷启动 nc → nl | STOP iter14(撞 1e16,模型损坏) | ❌ NaN 发散 |
|
||||||
|
| **he0 warm-start nc → nl** | 跑满 30 迭代**有界无 STOP**(CHMX~1e6) | ✅ **18 迭代收敛, max_relc=8.06e-4 < 1e-3;表层 T=0.77×Teff(与收敛参考一致),无 NaN** |
|
||||||
|
|
||||||
|
→ **He-rich(he=0)同族 warm-start 给出有界 nc,nl 从中收敛到物理解。** 这正是用户提出的"多步种子步进/向四周扩散"思路——**机制已验证有效,但是部分点有效(非万能)**。
|
||||||
|
|
||||||
|
### 多点验证(2026-08-14,5 个不同失败点,he-rich warm nc + ORELAX=0.3 + nl)
|
||||||
|
|
||||||
|
| 点 | Teff/logg/He | 种子 | nc | nl | 结论 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| first | 60k/g5.0/he-4 | he0 | 有界(无STOP) | 18it **8.06e-4** | ✅ **收敛,表层0.77×Teff** |
|
||||||
|
| p1 | 50k/g5.0/he-4 | he0, ORELAX=0.3 | 有界(5e-3) | 15it **8.27e-4** | ✅ **收敛,表层0.81×Teff** |
|
||||||
|
| p4 | 60k/g5.5/he-2 | he0, ORELAX=0.3 | 有界(4e8) | 100it 卡 1e5 | ⚠️ nl.7 物理(0.79×Teff)但未达0.001 |
|
||||||
|
| p2 | 60k/g5.0/he-2 | he0 / he2 | STOP(1e26/1e16) | 发散 | ❌ 两种种子都 STOP |
|
||||||
|
| p3 | 50k/g5.5/he-4 | he0, ORELAX=0.3/0.1 | STOP(~1e14) | NaN | ❌ |
|
||||||
|
|
||||||
|
**诚实结论:he-rich warm-start + ORELAX=0.3 + nl 是部分修复,非万能**:
|
||||||
|
- **2/5 完全收敛**(都是 he-4/g5.0,50k & 60k 都行);
|
||||||
|
- **1/5 接近**(p4:nl.7 物理但 nl 未达 0.001);
|
||||||
|
- **2/5 失败**(p2 he-2、p3 he-4/g5.5:nc 仍 STOP)。
|
||||||
|
|
||||||
|
> 即:当前 production 对这批点 seed_step 恢复率 **0%**;本路径把它提到约 **40% 完全 + 20% 接近**。剩下的 p2/p3 类需更近种子(如 Teff 邻居,受 seed_finder bug 挡住未试)或源码级工作。**机制成立(nc 有界→nl 收敛),但"让 nc 有界"本身仍点相关。**
|
||||||
|
|
||||||
|
### 追加(2026-08-14 深夜):Teff 多步梯子 + 全方向种子——p2/p3 仍抵抗
|
||||||
|
|
||||||
|
对两个抵抗点(p2: 60k/g5.0/he-2_c-4_n-3_o-3;p3: 50k/g5.5/he-4_c-2_n-3_o-3)追加测试:
|
||||||
|
|
||||||
|
| 尝试 | 方向 | 结果 |
|
||||||
|
|---|---|---|
|
||||||
|
| **Teff 梯子 40k→45k→50k→55k→60k**(nc 链,ORELAX=0.3) | 5kK/步 | ❌ **第一步 45k nc 即 NaN**(iter1=12 好,iter5 Kant 爆到 7.7e13,nc.7 全 NaN) |
|
||||||
|
| p2 对角种子(55k/g5.5/**同He同CNO**) | ΔTeff5k+Δlogg0.5 | ❌ nc STOP(1e17);nl.7 物理(0.76×Teff)但 max_relc 2e16 |
|
||||||
|
| p2 g5.5 CNO-1 种子 | Δlogg0.5 | ❌ NaN |
|
||||||
|
| p3 45k_he-2 种子 | ΔTeff5k+ΔlogHe1 | ❌ nc STOP(4e16);nl.7 物理(0.76×Teff)但 1e25 |
|
||||||
|
| p3 g5.0 同丰度种子 | Δlogg0.5 | ❌ nc STOP(4e19);nl.7 物理(0.76×Teff)但 8e16 |
|
||||||
|
|
||||||
|
**p2 共 6 种尝试、p3 共 5 种尝试,全部未数值收敛**(he2/he0/对角/g5.5/Teff 梯子/多 ORELAX)。
|
||||||
|
|
||||||
|
**值得注意**:3 个"失败"运行的 nl.7 **温度结构物理且正确**(表层 0.76×Teff ≈ 收敛参考 0.77-0.78),无 NaN——只是 max_relc 被痕量物种的未限幅修正污染(联系 §3.1:CHMX 取自 BET 未限幅值)。即**结构可能已对、布居数判据不过**。这批"物理有界但数值未收敛"模型是否可用(或加 POPZCH 过滤判据后可用)值得进一步评估。
|
||||||
|
|
||||||
|
### 最终总账(全部验证)
|
||||||
|
|
||||||
|
| 点 | 结果 | 尝试数 |
|
||||||
|
|---|---|---|
|
||||||
|
| 60k/g5.0/he-4_c-4_n-4_o-3 | ✅ nl 收敛(8.06e-4)+物理 | he0 种子 1 次成功 |
|
||||||
|
| 50k/g5.0/he-4_c-4_n-4_o-4 | ✅ nl 收敛(8.27e-4)+物理 | he0+ORELAX0.3 成功(ORELAX1.0 失败) |
|
||||||
|
| 60k/g5.5/he-2_c-4_n-4_o-4 | ⚠️ nl.7 物理但未达0.001 | he0 |
|
||||||
|
| 60k/g5.0/he-2_c-4_n-3_o-3 (p2) | ❌(3 个 nl.7 物理但未收敛/NaN) | **6 种** |
|
||||||
|
| 50k/g5.5/he-4_c-2_n-3_o-3 (p3) | ❌(2 个 nl.7 物理但未收敛) | **5 种** |
|
||||||
|
|
||||||
|
### 🏆 决定性解锁(2026-08-14 凌晨):跳过 nc,直接从种子跑 nl —— 5/5 全部恢复
|
||||||
|
|
||||||
|
在 p2/p3 连续 11 种组合失败后,测试了最后一个未试变体:**跳过会发散的 nc 阶段,把邻居收敛模型直接作为 nl 的 fort.8**(nl 自带 ORELAX=0.5、NITER=100、谱线耦合,数值上比 nc 稳得多):
|
||||||
|
|
||||||
|
| 点 | 之前 11 种组合 | 直接 nl(跳过 nc)| nl.7 物理 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| p2 (60k/g5.0/he-2_c-4_n-3_o-3) | 全失败 | ✅ 对角种子(55k/g5.5 同He同CNO) 99it **6.8e-4** | ✅ 0.765×Teff |
|
||||||
|
| p3 (50k/g5.5/he-4_c-2_n-3_o-3) | 全失败 | ✅ 45k_he-2 种子 19it **8.6e-4**(近单调收敛) | ✅ 0.760×Teff |
|
||||||
|
| p4 (60k/g5.5/he-2_c-4_n-4_o-4) | 卡 1e5 | ✅ **CNO-1 exact_family 种子** 13it **6.3e-4**;he0 种子 19it 8.4e-4 | ✅ 0.786×Teff |
|
||||||
|
|
||||||
|
**最终总账:5 个验证点全部恢复(5/5)**:
|
||||||
|
- first (60k/g5.0/he-4)、p1 (50k/g5.0/he-4):he0 warm nc(ORELAX=0.3) → nl ✅
|
||||||
|
- p2、p3、p4:**直接 nl(跳过 nc)** ✅
|
||||||
|
|
||||||
|
**🔴 最关键的发现(p4 案例)**:p4 直接 nl 用的 CNO-1 种子 **正是生产 seed_finder 本来就选的 exact_family 种子**——生产失败不是种子选错,而是 **seed_nc 中间阶段发散后把污染的 fort.7 喂给了 nl**。同一个种子跳过 seed_nc 直接喂 nl,13 次迭代收敛。
|
||||||
|
|
||||||
|
**生产的真正病灶**:`default_seed_chain` 是 `seed_nc → nl`,`seed_nc` 对难点发散(Kantorovich),其损坏输出毒死了 nl。而 **nl 从干净种子出发自身完全有能力收敛**。
|
||||||
|
|
||||||
|
### 修复方案(框架侧,runner.rs)
|
||||||
|
|
||||||
|
在 `default_seed_chain` / runner 的阶段传递逻辑(`runner.rs:518-531`)加一层:
|
||||||
|
|
||||||
|
1. **当 seed_nc 发散(STOP/NaN/best_max_relc 超阈)时,不要把 seed_nc 的 fort.7 传给 nl——回退用原始种子(邻居的干净 .7)直接喂 nl**(nl 参数不变:ORELAX=0.5、NITER=100)。
|
||||||
|
2. 更激进(同样有效):对已知难点直接加一条 `nl_direct` 策略(跳过 seed_nc)。
|
||||||
|
3. 配合修 seed_finder 两 bug(exact_family 优先级 + `d_teff<5000.0`),扩大可用种子池(he0/对角/Teff 邻居)。
|
||||||
|
|
||||||
|
**验证数据支撑**:5/5 点恢复,其中 3 个点是"直接 nl"救回、2 个点是"he0 warm nc→nl"救回;两种路径互补。p4 案例证明即使不修 seed_finder、只用现有种子选择,仅改"seed_nc 失败回退原种子喂 nl"就能救回 p4 类点。
|
||||||
|
|
||||||
|
### 🔬 是否应把 seed_nc→nl 直接替换为 seed_nl(直接 nl)?—— **不应替换,应作回退/补充策略**(A/B 实测)
|
||||||
|
|
||||||
|
**DB 统计**(2861 个生产 seed_step 成功点):
|
||||||
|
- **96%(2735)seed_nc 自己未收敛**,最终收敛全部由 nl 完成 —— seed_nc 从来不是收敛完成者;
|
||||||
|
- 但 234 个点 seed_nc 发散到 >1e4,nl 仍从中收敛 —— 发散的 seed_nc 输出并非必然有毒。
|
||||||
|
|
||||||
|
**A/B 对照**(3 个生产 seed_step 成功点,同种子直接跑 nl,跳过 seed_nc):
|
||||||
|
|
||||||
|
| 点 | 生产(seed_nc→nl) | 直接 nl | 结论 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| ab2 (35k) | ✅ nl 9.51e-4 | ✅ 31it 4.9e-4 | seed_nc 多余 |
|
||||||
|
| ab3 (45k) | ✅ nl 7.39e-4 | ✅ 19it 5.6e-4 | seed_nc 多余 |
|
||||||
|
| **ab1 (35k)** | ✅ nl 7.47e-4(注意其 seed_nc 发散到 2.65e8)| ❌ 100it 卡 8.2e8 | **seed_nc 必要!** |
|
||||||
|
|
||||||
|
**ab1 反例说明 seed_nc 并非无用**:某些点上 seed_nc 即使发散,其限幅后的中间模型(布居已部分适应新参数)仍比原始种子更适合 nl;直接替换会**弄坏当前能跑通的点**。
|
||||||
|
|
||||||
|
**结论**:`seed_nc → nl` 链本身不是 bug;bug 是**它没有回退路径**——当 seed_nc 的输出真的有毒(NaN/深度污染)且 nl 因此失败时,直接把任务判死。正确修复是**阶梯式**:
|
||||||
|
|
||||||
|
```
|
||||||
|
seed_nc → nl (现状,覆盖 96% 场景)
|
||||||
|
└─ nl 失败/NaN → nl_direct(用原始干净种子重跑 nl) (新增回退,救 p2/p3/p4 类)
|
||||||
|
└─ 仍失败 → 下一策略(现有 fallback 不变)
|
||||||
|
```
|
||||||
|
|
||||||
|
这样 ab1 类点走第一条路(保住),p4 类点走回退(救回),无任何回归风险。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、修复实施(2026-08-14,已落地并验证)
|
||||||
|
|
||||||
|
### 8.1 代码改动(3 处)
|
||||||
|
|
||||||
|
| # | 文件 | 改动 | 验证 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 | `runner.rs` | 阶段执行体抽为 `execute_tlusty_stage()`;新增 **nl_direct 回退**:require_converged 阶段失败且非链首且有原始种子时,以 `<label>_direct` 标签用原始种子重跑该阶段,收敛则采纳 | 单元测试(假 tlusty 三步接线)✅;p2/p3 用完整生产 nst 的 direct-nl 物理验证 ✅(99it/6.8e-4、19it/8.6e-4,温度结构物理) |
|
||||||
|
| 2 | `seed_finder.rs:51` | `d_teff < 5000.0` → `<= 5000.0`(相邻 Teff 档纳入 exact_family,桶索引本就支持) | 回归测试(55000↔60000 判 exact、距离 0)✅ |
|
||||||
|
| 3 | `nst_writer.rs` | 新增 `NST_LINE_MAX=75` 换行(TLUSTY `CHARACTER*80` 行缓冲截断 bug,IFALI/TFLOOR 等此前从未生效) | 回归测试(全行 ≤75、TMOLIM/TFLOOR 在输出中)✅;**行为验证见下** |
|
||||||
|
|
||||||
|
### 8.2 nst 截断修复的行为验证(意外收获)
|
||||||
|
|
||||||
|
用修复后的 Rust 链端到端重跑 p4 场景(`e2e_nl_direct_fallback_recovers_poisoned_seed_chain`,真实二进制):
|
||||||
|
- **主链 seed_nc→nl 自己收敛了**(seed_nc 仍发散到 4.6e14,但 nl 14it 收敛 2.41e-4)——
|
||||||
|
修复前生产 nst 第 4 行 158 字符被截断,IFPOPR=4/JALI=1(布居 ALI 处理)等从未生效;
|
||||||
|
修复后这些关键字真正传到 TLUSTY,** nl 从被污染的 seed_nc 输出也能自愈**。
|
||||||
|
- 即 **#3 一项就可能显著改善生产成功率**;p2/p3 主链仍失败(回退 #1 兜底,已验证有效)。
|
||||||
|
|
||||||
|
### 8.3 验证总账
|
||||||
|
|
||||||
|
- `cargo test --workspace`:**197 passed / 0 failed**(含 3 个新回归测试)
|
||||||
|
- `cargo clippy`:仅 2 个既有警告(conv_check.rs,非本次改动)
|
||||||
|
- 端到端(真实二进制):p4 主链自愈 ✅;p2/p3 回退路径物理验证 ✅
|
||||||
|
- 修复后预期:难点恢复路径 = 主链(nst 修复增益)→ nl_direct 回退 → 现有策略 fallback,三层递进
|
||||||
|
|
||||||
|
### 生产没走通的原因:seed_finder 两个 bug
|
||||||
|
|
||||||
|
查 `crates/server/src/db/seeds.rs:144-193` + `seed_finder.rs:51`:
|
||||||
|
|
||||||
|
**Bug 1(致命):exact_family 优先级压倒一切。** 选种逻辑「先找 exact_family(同 Teff/logg/He、仅 CNO 不同),有就**立即返回**,绝不看 global」。对 he-4 点:exact_family 候选 = 同 Teff 的 CNO 邻居 o-4(**富方向**、不稳定、距离 4.0)被选中;而经验证有效的 he0 候选(global、**贫 He 方向**、距离 2.0)**从未被考虑**。
|
||||||
|
|
||||||
|
**Bug 2:`d_teff < 5000.0`(严格小于)排除相邻 Teff 档。** 网格 Teff 步长正好 5000K,55k→60k 相邻档**永远进不了 exact_family**。而 Teff 方向经验证最稳(55k 点靠 50k Teff 种子收敛)。
|
||||||
|
|
||||||
|
### 推荐修复(按性价比)
|
||||||
|
|
||||||
|
1. **改选种逻辑**(治本,框架侧):当 exact_family 候选是富方向(距离大)时,应让它与 global 候选比较。最简方案——**对 he≤-2 的点,优先选 He-rich(he0/he+2)同族收敛模型作种子**(贫 He 方向,已验证 nl 能接住)。
|
||||||
|
2. **`d_teff < 5000.0` → `<= 5000.0`**(一行):纳入相邻 Teff 档,让 55k 能作 60k 种子。
|
||||||
|
3. **多步 Teff 连续**(用户思路):从已收敛低温点逐档升温 warm-start,每步 nc 有界 + nl 收尾。这是 1+2 修复后的自然产物。
|
||||||
|
4. **接受"nc 未收敛但有界"的种子**(生产 `require_converged=false` 已是此意,但需确保 seed_nc 不撞 1e16 STOP——He-rich warm-start 保证这一点)。
|
||||||
|
|
||||||
|
### 前提:单参数/换起点 nc 自身收敛均已穷举证伪(但仍可用作 nl 的有界种子)
|
||||||
|
|
||||||
|
12+ 变体 + He-ladder 1 dex 细步全部不收敛。不稳性根植于 TLUSTY「完全线性化 + Kantorovich 加速」求解器在「贫 He + 高 Teff + 低 logg」表层的结构性失效。下列是**仍需进一步工作**的方向(本文未完全落地)。
|
||||||
|
|
||||||
|
### 方向 A(治本,需改源码):换加速/辐射算子
|
||||||
|
|
||||||
|
- ~~**试 IFALI>5(近似 Λ 算子)**~~ —— **已实测证伪**。`IFALI=6`(启用带状近似 Λ 算子,`:17411` 加 A/C 非对角耦合):
|
||||||
|
- cold start + IFALI=6 → nc.7 全 NaN(比 baseline 的有界更差);
|
||||||
|
- warm he0 + IFALI=6 → **iter1 MAX=1.84e95**(灾难性,vs 默认 4.3)。
|
||||||
|
- 带状算子的非对角项在远离解时**放大**失稳,不是稳住。**IFALI=6 对本问题是反效果。**
|
||||||
|
- 改 Kantorovich 切换逻辑或加自适应阻尼(前序文档优先级 1/2,工程量大)。这是剩下唯一可能见效的源码侧方向,但工程量大、需重编。
|
||||||
|
|
||||||
|
### 方向 B(止损,框架侧):诊断修正 + 难点标记
|
||||||
|
|
||||||
|
- **诊断侧**:check_fort9 的 CHMX 是未限幅值(§3.1),对难点恒高。可对 nc 阶段追加 POPZCH 过滤后的 CHMX + 温度结构/emflux 实物校验作辅助判据——但**不能**把有界未收敛模型当真收敛(§4.2 实测其表层 T 偏离)。
|
||||||
|
- **任务侧**:把 he≤−2 + Teff≥50k + logg≤5.5 的 ~273 个最难点标记为"nc 求解器失效",单独排队/人工处理,不浪费 7-9 次重试。
|
||||||
|
|
||||||
|
### 方向 C(数据侧):缩小难点集
|
||||||
|
|
||||||
|
DB 统计这批点的收敛率随 He 丰度强烈变化(he=+2 93% → he=−4 66% 失败)。若科学上可接受,**优先保证 he≥0 的点**(冷启动大多收敛),he≤−2 的最难点降级处理。
|
||||||
|
|
||||||
|
### 已证伪、不推荐的方向
|
||||||
|
|
||||||
|
- **诊断侧**:DCTS 的 check_fort9 用未限幅 CHMX 判收敛,对这类点恒判失败。可在 framework 里对 nc 阶段**追加 POPZCH 过滤后的 CHMX** 与**温度结构/emflux 实物校验**作为辅助判据,但**不能**把"有界未收敛"模型当真收敛(实测其表层 T 偏离收敛参考)。
|
||||||
|
- **任务侧**:把这批 he≤−2+高Teff+低logg 的点标记为"需 He-ladder",单独排队。
|
||||||
|
|
||||||
|
### 不推荐的方向(已穷举证伪)
|
||||||
|
|
||||||
|
- 任何单一 nst 数值参数(ORELAX/DPSILG/ITEK/IACC/POPZER/POPZCH/NITER);
|
||||||
|
- ND=70、非灰 LTE 中间阶、trace 金属置 LTE、IFALI=5;
|
||||||
|
- 仅靠 logg 连续(Δlogg 0.5 warm-start 仍崩);
|
||||||
|
- **He-ladder 连续**(he0→he−1 单步 1 dex、he0→he−4 单跳 2 dex,iter1 都小但 iter5 Kant 爆)——不稳性在迭代加速环节不在起点;
|
||||||
|
- 禁 Kantorovich(ITEK=999)——纯 full-CL 反而 NaN,Kantorovich 实为防 NaN 的稳定项;
|
||||||
|
- **IFALI=6(带状近似 Λ 算子)**——cold→全 NaN、warm→iter1 MAX=1.84e95 灾难性;带状非对角项远离解时放大失稳。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六·五、附带发现:nst 行被 80 字符缓冲截断(生产 bug)
|
||||||
|
|
||||||
|
测 IFALI=6 时发现:`tlusty208.f:1689` 声明 `CHARACTER*80 TEXT`(nst 行读取缓冲),但 `nst_writer.rs` 写出的第 4 行长达 **158 字符**:
|
||||||
|
|
||||||
|
```
|
||||||
|
HMIX0=-1.,MLTYPE=1,IPTURB=1,IBFINT=1,IHECOR=0,IRTE=0,IBC=3,IFRYB=0,IOPTAB=0,IFMOL=0,IFALI=5,IFPOPR=4,JALI=1,TRAD=0.,WDIL=0.,TFLOOR=8000.,TDISK=0.,TMOLIM=9000.
|
||||||
|
```
|
||||||
|
|
||||||
|
第 80 字符(`...IFMO`)处被截断,**`IFALI`/`IFPOPR`/`JALI`/`TRAD`/`WDIL`/`TFLOOR`/`TDISK`/`TMOLIM` 全部从未被读取**,静默走 TLUSTY 默认。即生产中这些点的 `TFLOOR=8000`(`config.rs` 配的)实际没生效,用的是 TLUSTY 默认 TFLOOR。本复测所有需要这些参数的变体都已手动拆行(每行 ≤68 字符)验证可正确读取。
|
||||||
|
|
||||||
|
**建议修 `nst_writer.rs`**:keyword 组每组 ≤75 字符换行(对齐 `CHARACTER*80` 限制)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、与前序文档的关系
|
||||||
|
|
||||||
|
| 前序结论 | 本文状态 |
|
||||||
|
|---|---|
|
||||||
|
| "itek/iacc/DPSILG/orelax 调参无效" | **证伪其方法论**:CHMX 对这些参数不敏感(源码 :14643 在 :14647/:14652 之前),前序测了不敏感的指标。结论(这些参数救不回)方向对,但理由错。 |
|
||||||
|
| "需连续法" | **方向确认**,并具体化为 **He-ladder**(而非泛泛的 Teff 连续),有 he0 warm-start iter1 改善 30× 的实测支撑。 |
|
||||||
|
| "Newton 在灰化起点即在收敛域外" | **确认**(baseline iter1=140 且深层正常表层崩)。但 warm he0 能把 iter1 降到 4.3,说明**换个起点能进收敛域**。 |
|
||||||
|
| "禁 Kantorovich 降 1e12~1e14" | **方法论警示**:那是 CHMX(未限幅)下降;纯 full-CL(ITEK=999)实测会 NaN,Kantorovich 实为稳定项,**不应禁用**。 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、复测产物位置
|
||||||
|
|
||||||
|
```
|
||||||
|
test/20260813_cold_nc_trace_fix/convergence/t60000_g5.0_he-4_c-4_n-4_o-3_cold/
|
||||||
|
├── baseline/ inputs/(lte+nc.5/nst) + outputs/(nc.6/7/9)
|
||||||
|
├── popzer1e-9/ popzer1e-12/ popzch1e-9/
|
||||||
|
├── nd70/ lte_ng/ trace_lte/ full_nst/
|
||||||
|
├── warm_from_g5.5/ warm_from_he0/ warm_he0_nokant/
|
||||||
|
└── cold_nokant/
|
||||||
|
```
|
||||||
|
每个变体 `outputs/nc.9` 可用 `scripts/check_fort9.py` 复核;`nc.7` 可用 `check_temperature.py` 复核。
|
||||||
@@ -0,0 +1,340 @@
|
|||||||
|
# TLUSTY 本机直接运行实验记录(根因验证与修复测试)
|
||||||
|
|
||||||
|
**日期**:2026-08-11
|
||||||
|
**方法**:绕过 DCTS 框架,直接运行 TLUSTY 二进制(`dcts/assets/tlusty_static`),本地 16 核并行测试不同策略
|
||||||
|
**测试对象**:生产 DB(`/home/fmq/下载/dcts.db`)标记 `tlusty_status=failed` 的网格点
|
||||||
|
**源码参考**:`/home/fmq/program/tlusty/tl208-s54/tlusty/tlusty208.f` + dcts `workflows/sdB_cno.yaml`
|
||||||
|
**测试规范**:见 `docs/testing_workflow_2026_08_11.md`;测试产物已迁移至 `test/20260811_tlusty_divergence/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ 2026-08-12 更正
|
||||||
|
|
||||||
|
本文实验使用的 `itek:0` / `iacc:0` 参数存在**致命语义误解**(后续源码核对确认):
|
||||||
|
- `ITEK=0` → `tlusty208.f:1937` `if(nitzer.gt.itek) nitzer=itek` → **nitzer=0 冻结
|
||||||
|
populations**,不是"禁用 Kantorovich"——所有 `itek0*` 变体都在 populations 冻结
|
||||||
|
下运行,其结果不能解释为 Kantorovich 的因果。
|
||||||
|
- `IACC=0` → `tlusty208.f:1928` `IF(IACC.LE.4) IACC=7` → **被 clamp 回 7**,空操作——
|
||||||
|
所有 `iacc0*` 变体实际仍是默认 Ng 加速。
|
||||||
|
|
||||||
|
正确禁用方式是设 `ITEK/IACC > NITER`。2026-08-12 用正确方式复测 6 个点 × 4 配置:
|
||||||
|
**0 次收敛**(发散幅度降 1e12~1e14× 但达不到收敛)。详见
|
||||||
|
`docs/cold_start_fix_2026_08_12.md`。本文保留为历史实验记录。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、背景与目的
|
||||||
|
|
||||||
|
生产 DB 有 1105 个网格点 TLUSTY 不收敛(cold_run 与 seed_step 均失败,见 `tlusty_divergence_root_cause_2026_08_11.md`)。为验证根因假设并测试修复策略,本实验绕过框架直接运行二进制,精确复现生产输入与失败。
|
||||||
|
|
||||||
|
**本实验回答的三个问题**:
|
||||||
|
1. 生产失败能否在本地精确复现?(可信度锚点)
|
||||||
|
2. 失败的直接机制是什么?(KANT/ACCEL2 加速)
|
||||||
|
3. 何种策略能修复?(ITEK=0 + IACC=0 + DPSILG 收紧的假设性验证)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、运行方法(与 DCTS 框架等价)
|
||||||
|
|
||||||
|
TLUSTY 二进制通过 **stdin** 读取 `.5` 输入文件(`runner.rs:406-416` 同款调用方式):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tlusty_static < {stage}.5 > fort.6 2> fort.14
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.1 工作目录布局
|
||||||
|
|
||||||
|
| 文件/目录 | 用途 |
|
||||||
|
|---|---|
|
||||||
|
| `data/` → 软链到 `assets/data/` | 原子数据(c1.dat/h1.dat 等 133 文件) |
|
||||||
|
| `nst` | 非标准参数(由 `.5` 第3行 `'nst'` 指定文件名),每阶段复制 `{stage}.nst` |
|
||||||
|
| `fort.8` | 热启动种子(seed_nc 阶段);冷启动 lte 阶段无(ltgray=T 删除) |
|
||||||
|
| `fort.19` → 软链到 `assets/data/gfATO.dat` | SYNSPEC 线表 |
|
||||||
|
| 产物 | `fort.6`(log)、`fort.7`(模型)、`fort.9`(迭代收敛记录)、`fort.14`(stderr) |
|
||||||
|
|
||||||
|
### 2.2 冷启动链(与 YAML `tlusty_chain` 一致)
|
||||||
|
|
||||||
|
```
|
||||||
|
lte(NITER=0, LTE=T, LTGRAY=T, ilvlin=0, 无 fort.8) ← 灰 LTE 起始
|
||||||
|
→ nc(NITER=10, LTE=F, LTGRAY=F, ilvlin=0) ← 策略变体作用阶段
|
||||||
|
→ nl(NITER=100, LTE=F, LTGRAY=F, ilvlin=100) ← 必须收敛
|
||||||
|
```
|
||||||
|
|
||||||
|
阶段间通过 `fort.7 → fort.8` 传递种子。
|
||||||
|
|
||||||
|
### 2.3 种子步进(与 YAML `seed_chain` 一致)
|
||||||
|
|
||||||
|
```
|
||||||
|
seed_nc(NITER=20, LTE=F, LTGRAY=F, ilvlin=0, fort.8=生产收敛邻居种子)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.4 可信度锚点
|
||||||
|
|
||||||
|
本机 `seedprod`(生产配置 + 生产真实种子)seed_nc 轨迹:
|
||||||
|
`[7.0, 115, 272, 2020, 1.49e8, 8.26e13, ...]`
|
||||||
|
|
||||||
|
生产 DB 记录的 conv.json 轨迹:
|
||||||
|
`[7.0, 102, 288, 945, 1.12e8, 4.85e13, ...]`
|
||||||
|
|
||||||
|
**逐值吻合(同一数量级)** → 本机测试精确复现生产失败,测试方法可信。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、根因(源码 + 实证双重确认)
|
||||||
|
|
||||||
|
雪崩由 **TLUSTY 两个加速机制**绕过 DPSILT 钳制引起,二者独立:
|
||||||
|
|
||||||
|
### 3.1 机制1:KANT / Kantorovich 线性化(`tlusty208.f:3841, 14493-14494`)
|
||||||
|
|
||||||
|
- 默认 `ITEK=4` → `KANT(iter)=1` for iter≥5(`tlusty208.f:848-854`)
|
||||||
|
- KANT=1 时**冻结矩阵**:`CALL RHSGEN` 只算右端项,`READ(93)/READ(92)` 复用上次迭代的 A/B 矩阵(`tlusty208.f:14500-14540`)
|
||||||
|
- 在收敛困难区,冻结矩阵与实际解严重偏离 → Newton 步方向错误 → 雪崩
|
||||||
|
- **证据**:基线 nc 雪崩始于 iter5(第一个 KANT 迭代)
|
||||||
|
|
||||||
|
### 3.2 机制2:ACCEL2 / Ng 加速(`tlusty208.f:29693-29800`)
|
||||||
|
|
||||||
|
- 由 `IACC` 控制(默认 7),与 ITEK 独立;iter≥7 触发
|
||||||
|
- 直接外推 PSY0 全体变量:`PSY0=(1-A-B)*PSY0 + A*PSY1 + B*PSY2`(`tlusty208.f:29763-29773`)
|
||||||
|
- **绕过 SOLVE 内 DPSILG/DPSILT 钳制循环**(钳制只约束 CHAN 局部变量,ACCEL2 直接改 PSY0 全局)
|
||||||
|
- **证据**:nl 阶段 iter7/11/15 的 ACCEL2 触发 → 温度外推爆炸 → STOP
|
||||||
|
|
||||||
|
### 3.3 两机制独立性(实证)
|
||||||
|
|
||||||
|
- `itek0`(只禁 KANT):nc 雪崩推迟但 nl 仍 STOP(ACCEL2 未禁)
|
||||||
|
- `iacc0`(只禁 ACCEL2):nl 仍 STOP(KANT 未禁)
|
||||||
|
- **必须双禁(ITEK=0 + IACC=0)** 才能抑制两个加速通道
|
||||||
|
|
||||||
|
### 3.4 雪崩主导变量
|
||||||
|
|
||||||
|
fort.9 显示 nl 阶段 |T| 修正从 iter1 的 4.26e5 爆炸到 iter19 的 1.2e16,**温度是雪崩主导变量**(RAD 辐射场修正一直 ≤360,ne 基本稳定)。DPSILT=1.25 钳制(±25%)被 ACCEL2 外推绕过。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、测试矩阵与结果
|
||||||
|
|
||||||
|
### 4.1 策略变体定义(nc/nl 阶段 nst 差异)
|
||||||
|
|
||||||
|
| 变体 | ITEK | IACC | DPSILG | ORELAX(nc) | 说明 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| baseline | 4(默认) | 7(默认) | 10(默认) | — | 生产配置 |
|
||||||
|
| trueprod | 4(默认) | 7(默认) | 10(默认) | — | 真生产(nl 也 ITEK=4,见瑕疵说明) |
|
||||||
|
| itek0 | 0 | 7 | 10 | — | 禁 Kantorovich |
|
||||||
|
| dpsilg3 | 4 | 7 | 3.0 | — | 收紧限幅 |
|
||||||
|
| itek0_dpsilg3 | 0 | 7 | 3.0 | — | 组合 |
|
||||||
|
| itek0_dpsilg2_orelax03 | 0 | 7 | 2.0 | 0.3 | 组合+松弛 |
|
||||||
|
| iacc0 | 4 | 0 | 10 | — | 禁 Ng 加速 |
|
||||||
|
| itek0_iacc0 | 0 | 0 | 10 | — | 双禁 |
|
||||||
|
| itek0_iacc0_dpsilg15 | 0 | 0 | 1.5 | — | 双禁+收紧 |
|
||||||
|
| seedprod | 4 | 7 | 10 | — | seed_nc 生产 |
|
||||||
|
| seed_doubleoff | 0 | 0 | 10 | — | seed_nc 双禁 |
|
||||||
|
| seed_doubleoff_dpsilg15 | 0 | 0 | 1.5 | — | seed_nc 双禁+收紧 |
|
||||||
|
|
||||||
|
> ⚠️ **测试瑕疵说明**:初版 prepare.py 给所有变体(含 baseline)的 **nl.nst 无条件加了 ITEK=0**。故表内 baseline 的 nl 阶段实际是 ITEK=0(非生产默认 4)。由于失败发生在 nc 阶段(nc 的 ITEK 是生产默认),**nc 阶段对比仍然公平**;为消除 nl 混淆,补跑了 `trueprod`(nl 也为默认 ITEK=4 的真生产配置)。
|
||||||
|
|
||||||
|
### 4.2 冷启动链结果(点 t60000_g5.0_he-2_c-4_n-4_o-4)
|
||||||
|
|
||||||
|
**nc 阶段轨迹**(max_relc,雪崩与否关键看前 5 步):
|
||||||
|
|
||||||
|
| 变体 | iter1 | iter2 | iter3 | iter4 | iter5 | nc 结局 |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| baseline | 9.7 | 46 | 259 | 1950 | **4.85e6** | 雪崩→2.3e15 |
|
||||||
|
| dpsilg3 | 9.7 | 64 | 31 | 48 | 3.8e3 | 高位雪崩→1.8e8 |
|
||||||
|
| iacc0 | 9.7 | 46 | 260 | 2000 | **4.8e6** | 雪崩(同 baseline) |
|
||||||
|
| itek0 | 1.4 | 9.1 | 38 | 460 | 77 | 高位徘徊 |
|
||||||
|
| itek0_dpsilg3 | 1.4 | 2.9 | 3.7 | 33 | 3.8 | 改善 |
|
||||||
|
| itek0_iacc0 | 1.4 | 9.1 | 38 | 460 | 77 | 高位徘徊 |
|
||||||
|
| itek0_iacc0_dpsilg15 | 1.4 | 1.4 | 1.9 | 14 | 5.3 | **低位稳定** |
|
||||||
|
| itek0_dpsilg2_orelax03 | 1.4 | 1.9 | 2.1 | 35 | 12 | 低位稳定 |
|
||||||
|
|
||||||
|
**nl 阶段结局**:
|
||||||
|
|
||||||
|
| 变体 | nl 结局 | NaN行(nl.7) | 末 max_relc | 温度结构(DCTS判据) | 判定 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| baseline | STOP iter19 | 0 | 1.23e16 | ✅ 物理(0.86×Teff) | ❌ |
|
||||||
|
| dpsilg3 | STOP iter14 | 0 | 7.59e16 | ✅ 物理(0.80×Teff) | ❌ |
|
||||||
|
| iacc0 | STOP iter10 | 0 | 1.72e17 | ✅ 物理(0.78×Teff) | ❌ |
|
||||||
|
| itek0 | STOP iter18 | 0 | 2.95e16 | ❌ 不物理(5.2×Teff) | ❌ |
|
||||||
|
| itek0_dpsilg3 | STOP iter26 | 0 | 5.96e17 | ❌ 不物理(5.2×Teff) | ❌ |
|
||||||
|
| itek0_iacc0 | STOP iter22 | 0 | 1.16e17 | ❌ 不物理(10.5×Teff) | ❌ |
|
||||||
|
| itek0_dpsilg2_orelax03 | 无 STOP | **2707** | 0.0(伪) | ❌ NaN | ❌ 伪收敛 |
|
||||||
|
| itek0_iacc0_dpsilg15 | 无 STOP | **2031** | 0.0(伪) | ❌ NaN | ❌ 伪收敛 |
|
||||||
|
|
||||||
|
**冷启动结论**:8 个变体在该极端点(60k/g5.0, CNO 全-4)全部失败。即使双禁+收紧 DPSILG,nl 阶段要么 STOP 要么产生 NaN。**冷启动从 LTE 灰化初值建立 NLTE 电离平衡在此点本质困难**,与加速机制无关。
|
||||||
|
|
||||||
|
### 4.3 种子步进结果(seed_nc,用生产真实种子 t60000_g5.0_he-2_c-3_n-4_o-4.7)
|
||||||
|
|
||||||
|
| 变体 | 轨迹(前6) | 结局 | NaN | 末 max_relc | 温度结构(DCTS判据) |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| seedprod(生产) | 7.0,115,272,2020,1.49e8,8.26e13 | STOP iter14 | 0 | 1.71e17 | ✅ 物理(0.49×Teff) |
|
||||||
|
| seed_doubleoff | 1.3,17,9,92,400,142 | 无 STOP | 0 | 4.71e06 | ✅ 物理(1.58×Teff) |
|
||||||
|
| seed_doubleoff_dpsilg15 | 1.3,16,7,1.7,2.9,6.3 | 无 STOP | 0 | **4.86e01** | ❌ 不物理(7.89×Teff) |
|
||||||
|
|
||||||
|
**种子步进结论**:
|
||||||
|
- seedprod 精确复现生产失败(可信度锚点)
|
||||||
|
- **双禁(ITEK=0+IACC=0)把 seed_nc 从雪崩(1e8-1e17)抑制到 1e2-1e3 量级**
|
||||||
|
- **再加 DPSILG=1.5 进一步压到几十量级**(末值 48.6),20 步全程无 STOP 无 NaN
|
||||||
|
- ⚠️ **温度物理性(修正版,见 §5.3 判据更新)**:seed_doubleoff(1.58×Teff)其实**物理**;seed_doubleoff_dpsilg15(7.89×Teff)**不物理**(超 DCTS 3×Teff 上限)。但两者**迭代均未收敛**(末值 4.7e6 / 48.6,均 >> 0.001)。早期文档用"3-6 万 K 表层"经验标准误判 seed_doubleoff 为不物理,已按 DCTS `check_temperature_structure` 判据(表层 < 3×Teff)修正
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、多点评测(5 个代表失败点 × baseline + 修复组合)
|
||||||
|
|
||||||
|
为验证根因与修复的普适性,对 5 个额外失败点补跑了冷启动链(每个点 baseline + `itek0_iacc0_dpsilg15` 两个变体)。加上 t60000_g5.0 的单点深测,共 **6 点、9 种变体、19 次完整冷启动链**(含 trueprod 对照)**+ 3 次 seed_nc**。
|
||||||
|
|
||||||
|
### 5.1 各点 nc 阶段早期轨迹(iter1-5,雪崩与否看这里)
|
||||||
|
|
||||||
|
| 点 | baseline iter1-5 | 修复组合 iter1-5 | nc 对比 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| t60000_g5.0 | 9.7→46→259→1950→4.85e6 | 1.4→1.4→1.9→14→5.3 | ✅ 修复显著抑制(雪崩→低位) |
|
||||||
|
| t50000_g5.0 | 3.8→260→56→0.4 | 0.9→14→1.1→1.1→9.0 | ✅ 修复显著抑制 |
|
||||||
|
| t50000_g5.5 | 1810→76→243→**0.2** | 258→11→**552**→29→60 | ⚠️ 修复反而变差 |
|
||||||
|
| t55000_g5.0 | 5.8→42→117→646→5.99e6 | 1.1→2.1→0.6→1.8→1.0 | ✅ 修复抑制(雪崩→低位) |
|
||||||
|
| t55000_g5.5 | 9.0→58→85→**0.09** | 1.5→1.2→1.2→0.45→27 | ⚠️ baseline 也能收敛 |
|
||||||
|
| t60000_g5.5 | 5.2→119→77→151→**0.14** | 0.32→1.6→5.3→23→19 | ⚠️ baseline 也能收敛 |
|
||||||
|
|
||||||
|
**重要修正**:修复组合**并非在全部点抑制雪崩**:
|
||||||
|
- t60000_g5.0、t55000_g5.0 的 baseline nc **雪崩**(iter5 到 4.85e6 / 5.99e6),修复组合压到低位 → ✅ 修复有效
|
||||||
|
- t50000_g5.5、t55000_g5.5、t60000_g5.5 的 baseline nc **本来就能收敛**(iter4-5 回落到 0.1-0.2),修复组合反而把轨迹扰乱(如 t50000_g5.5 修复 iter3=552)→ ⚠️ 修复有害或无益
|
||||||
|
- **修复组合的价值集中在"baseline 会雪崩"的点**;对"baseline 能收敛"的点反而画蛇添足
|
||||||
|
- 注意:**nc 阶段能回落 ≠ 整条链收敛**——这些点的 baseline nl 阶段仍失败(见 §5.2,t50000_g5.5 末 3.39e11、t55000_g5.5 STOP、t60000_g5.5 NaN),说明收敛瓶颈在 nl 阶段而非 nc
|
||||||
|
|
||||||
|
### 5.2 各点 nl 阶段结局(完整链)
|
||||||
|
|
||||||
|
| 点 | baseline nl 结局 | 修复组合 nl 结局 | 修复效果 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| t60000_g5.0 | STOP iter19 (1.23e16) | 31步,无STOP,NaN=2031 | ⚠️ 抑制雪崩但 NaN |
|
||||||
|
| t50000_g5.0 | 11步,无STOP,NaN=3659 | 19步,无STOP,NaN=3375 | ⚠️ 无实质改善 |
|
||||||
|
| t50000_g5.5 | 10步,末3.39e11,NaN=0 | 19步,无STOP,NaN=3595 | ⚠️ 变差(NaN) |
|
||||||
|
| t55000_g5.0 | STOP iter17 (1.05e16) | 58步,无STOP,NaN=0,末1.26 | ⚠️ 大幅改善但不收敛 |
|
||||||
|
| t55000_g5.5 | STOP iter30 (1.49e17) | 100步,无STOP,NaN=0,末1.38e8 | ⚠️ 改善但不收敛 |
|
||||||
|
| t60000_g5.5 | 19步,无STOP,NaN=3789 | 26步,无STOP,NaN=0,末1.72 | ⚠️ 改善但不收敛 |
|
||||||
|
|
||||||
|
### 5.3 温度结构物理性检查(判据已修正,关键!)
|
||||||
|
|
||||||
|
> 🔄 **判据修正说明(2026-08-11 补)**:初版用经验标准"60k 恒星表层应有 3-6 万 K"判断物理性,**该标准有误**。经文献调研(Eddington 灰大气关系 T(τ=0)≈0.84×Teff)与 DCTS 权威 `check_temperature_structure`(`conv_check.rs:463`)判据(表层 < 3×Teff、各深度 ∈ [10, 1e8] K)核对,**正确标准是表层 < 3×Teff**。60k 恒星表层应约 0.84×60k ≈ 5 万 K,3-6 万 K 的经验值偏低。本节按 DCTS 判据重新判定。
|
||||||
|
|
||||||
|
对每个变体的 nl.7 用 DCTS `check_temperature_structure` 判据(表层 < 3×Teff、无 NaN、各深度 ∈ [10, 1e8] K):
|
||||||
|
|
||||||
|
| 点 | baseline nl.7 | 修复组合 nl.7 |
|
||||||
|
|---|---|---|
|
||||||
|
| t60000_g5.0 | ✅ 物理(0.86×Teff) | ❌ NaN |
|
||||||
|
| t50000_g5.0 | ❌ NaN | ❌ NaN |
|
||||||
|
| t50000_g5.5 | ✅ 物理(0.83×Teff) | ❌ NaN |
|
||||||
|
| t55000_g5.0 | STOP(无 nl.7) | ❌ 内部 6.2e9 K |
|
||||||
|
| t55000_g5.5 | STOP(无 nl.7) | ❌ 内部 1.8e10 K |
|
||||||
|
| t60000_g5.5 | ❌ NaN | ✅ 物理(0.85×Teff, max=1.4e7) |
|
||||||
|
|
||||||
|
**修正后的发现**(较初版更准确):
|
||||||
|
- **修复组合并非"全部不物理"**:t60000_g5.5 修复组合温度结构**物理**(表层 0.85×Teff、max=1.4e7 < 1e8),只是迭代未收敛(末值 1.72)
|
||||||
|
- **真正不物理的**:t55000_g5.0(内部 6.2e9 K)、t55000_g5.5(内部 1.8e10 K)修复组合,及所有 NaN 模型
|
||||||
|
- **baseline 在 STOP 前温度物理**(t60000_g5.0、t50000_g5.5)
|
||||||
|
- **两类问题需要区分**:
|
||||||
|
1. **温度物理但未收敛**(如 t60000_g5.5 修复组合:表层合理、max 合理、但 max_relc=1.7)→ 有希望,增大迭代或收紧参数可能收敛
|
||||||
|
2. **温度不物理**(如 t55000 修复组合:内部 1e9+ K)→ 真正的失败,需换策略
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、总结论
|
||||||
|
|
||||||
|
1. **生产失败直接原因(已实证)**:KANT(iter≥5) + ACCEL2(iter≥7) 两个加速机制绕过 DPSILG/DPSILT 钳制,在收敛困难区放大 Newton 修正 → 雪崩 STOP(1e16 保护)或 NaN。本机测试精确复现生产轨迹。
|
||||||
|
2. **修复组合(ITEK=0+IACC=0+DPSILG=1.5)对"会雪崩的点"有效**:t60000_g5.0、t55000_g5.0 的 baseline nc 雪崩(iter5 到 4.85e6 / 5.99e6),修复组合压到低位;**但对"本就能收敛的点"反而有害**(t50000_g5.5、t55000_g5.5、t60000_g5.5 的 baseline nc 在 iter4-5 本就回落到 0.1-0.2,修复组合却把轨迹扰乱,如 t50000_g5.5 修复 iter3=552)。修复价值有条件性,需按点评估。
|
||||||
|
3. **温度物理性(判据修正后)**:修复组合**并非全部不物理**——t60000_g5.5 修复组合温度结构物理(表层 0.85×Teff),只是迭代未收敛;真正不物理的是 t55000 两个点(内部 1e9+ K)和所有 NaN 模型。**所有变体迭代均未收敛到 max_relc<0.001**(末值最低 1.26 / 1.72)。
|
||||||
|
4. **根因不在加速机制本身**:KANT/ACCEL2 只是雪崩放大器。**根本问题是这些极端点(高 Teff + 低 logg + 极贫 CNO)的 NLTE 迭代难以在有限迭代内收敛**——但温度结构物理的点(如 t60000_g5.5)表明**物理解存在且可达**,只是需要更多迭代或更优参数。
|
||||||
|
5. **文献对照**:Németh et al. (2013) 用 TLUSTY 204 计算了覆盖 60k/低logg 的 sdB/sdO 网格(11,396 点,95.5% 收敛,收敛标准=结构相对变化 0.1% 即 max_relc<0.001)。**本参数区在文献中是收敛的**——支持"配置/迭代问题而非物理不可能"的判断。
|
||||||
|
6. **结论修正**:正确表述是:**双禁+收紧 DPSILG 能抑制发散并给出温度物理的候选模型(部分点),但多数极端点在 NITER 内未达 0.001 收敛**。下一步应对温度物理的点(t60000_g5.5、seed_doubleoff)增大 NITER 验证是否最终收敛。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、生产落地建议(YAML 层面,零代码,但需配合物理诊断)
|
||||||
|
|
||||||
|
已验证 `ChainStep` 有 `iacc` 字段(`config.rs:1148`)、`nst_writer` 写 `IACC=`(`nst_writer.rs:78`),YAML 可配置:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
seed_chain:
|
||||||
|
- {label: seed_nc, lte: F, ltgray: F, ilvlin: 0, require_converged: false,
|
||||||
|
niter: 50, chmax: 0.001, itek: 0, iacc: 0, orelax: 0.3}
|
||||||
|
- {label: seed_nl, lte: F, ltgray: F, ilvlin: 100, require_converged: true,
|
||||||
|
niter: 100, chmax: 0.001, itek: 0, iacc: 0, orelax: 0.5}
|
||||||
|
nst_extra_keys:
|
||||||
|
- [DPSILG, "1.5"]
|
||||||
|
```
|
||||||
|
|
||||||
|
**但基于 §5.3 的发现,必须同时**:
|
||||||
|
- **保留 DCTS 的温度结构校验**(`temp_check`:表层 T < 3×Teff、无 NaN)——它正是拦截"非物理伪收敛"的关卡
|
||||||
|
- **优先走 seed_step**:冷启动在极端点本质困难(§4.2 冷启动 8 变体全失败),生产数据也显示 60k/g5.0 收敛靠 seed_step
|
||||||
|
- **对仍失败的极端点**:接受其为物理上难收敛区,或探索更深层修复(ND 加密 / TFLOOR 调整 / 频率网格 / 原子数据)
|
||||||
|
|
||||||
|
**下一步验证计划**:
|
||||||
|
- [x] ~~多点评测确认普适性~~(已完成:6 点 9 种变体 19 次完整链,见 §五)
|
||||||
|
- [x] ~~校验温度结构物理性~~(已完成并修正判据:见 §5.3;温度物理点有 t60000_g5.5 修复组合、seed_doubleoff)
|
||||||
|
- [ ] 对温度物理但未收敛的点(t60000_g5.5 修复组合、seed_doubleoff)**增大 NITER**(如 200)验证是否最终收敛到 0.001
|
||||||
|
- [ ] 对真正不物理的点(t55000 两个点)换策略:seed_step + 双禁 + 温度校验
|
||||||
|
- [ ] 探索 ND=70 / TFLOOR 调整对极端点的影响
|
||||||
|
- [ ] 通过后作为难收敛区统一配置
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、测试产物与规范迁移
|
||||||
|
|
||||||
|
本实验的完整数据与运行产物已按 `docs/testing_workflow_2026_08_11.md` 规范迁移至:
|
||||||
|
|
||||||
|
```
|
||||||
|
test/20260811_tlusty_divergence/convergence/
|
||||||
|
├── t60000_g5.0_he-2_cold/
|
||||||
|
│ ├── baseline/ # inputs+outputs+scripts+README
|
||||||
|
│ └── itek0_iacc0_dpsilg15/
|
||||||
|
├── t60000_g5.0_he-2_seed/
|
||||||
|
│ └── doubleoff_dpsilg15/
|
||||||
|
└── t55000_g5.0_he-4_cold/
|
||||||
|
└── itek0_iacc0_dpsilg15/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 原始实验数据(/tmp/cold_test,供追溯)
|
||||||
|
|
||||||
|
- 运行脚本:`/tmp/cold_test/run_chain.sh`(冷启动链)、`/tmp/cold_test/run_seed.sh`(seed_nc)、`/tmp/cold_test/prepare.py`(输入生成)
|
||||||
|
- 数据汇总:`/tmp/cold_test/t60000_summary.json`、`/tmp/cold_test/seed_summary.json`、`/tmp/cold_test/all_points_summary.json`
|
||||||
|
- 运行目录:`/tmp/cold_test/runs/{点}__{变体}/`(6 点)、`/tmp/cold_test/seedruns/{变体}/`(3 seed 变体)
|
||||||
|
- 运行日志:`/tmp/cold_test/fill_*.log`(补跑)、`/tmp/cold_test/seed_*.log`(seed_nc)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、测试清单(全部 24 次运行,无遗漏)
|
||||||
|
|
||||||
|
### 冷启动链(6 点,9 种变体,19 次完整链)
|
||||||
|
|
||||||
|
| # | 点 | 变体 | 结果 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 | t60000_g5.0_he-2_c-4_n-4_o-4 | baseline | STOP iter19 (1.23e16) |
|
||||||
|
| 2 | 同上 | trueprod(真生产nl ITEK=4) | NaN=4111 |
|
||||||
|
| 3 | 同上 | itek0 | STOP iter18 (2.95e16) |
|
||||||
|
| 4 | 同上 | dpsilg3 | STOP iter14 (7.59e16) |
|
||||||
|
| 5 | 同上 | itek0_dpsilg3 | STOP iter26 (5.96e17) |
|
||||||
|
| 6 | 同上 | itek0_dpsilg2_orelax03 | NaN=2707 |
|
||||||
|
| 7 | 同上 | iacc0 | STOP iter10 (1.72e17) |
|
||||||
|
| 8 | 同上 | itek0_iacc0 | STOP iter22 (1.16e17) |
|
||||||
|
| 9 | 同上 | itek0_iacc0_dpsilg15 | NaN=2031 |
|
||||||
|
| 10 | t50000_g5.0_he-4_c-4_n-4_o-4 | baseline | NaN=3659 |
|
||||||
|
| 11 | 同上 | itek0_iacc0_dpsilg15 | NaN=3375 |
|
||||||
|
| 12 | t50000_g5.5_he-4_c-2_n-4_o-4 | baseline | 10步 末3.39e11 NaN=0 |
|
||||||
|
| 13 | 同上 | itek0_iacc0_dpsilg15 | NaN=3595 |
|
||||||
|
| 14 | t55000_g5.0_he-4_c-4_n-4_o-4 | baseline | STOP iter17 (1.05e16) |
|
||||||
|
| 15 | 同上 | itek0_iacc0_dpsilg15 | 58步 末1.26 温度❌(内部6.2e9K) |
|
||||||
|
| 16 | t55000_g5.5_he-4_c-4_n-4_o-2 | baseline | STOP iter30 (1.49e17) |
|
||||||
|
| 17 | 同上 | itek0_iacc0_dpsilg15 | 100步 末1.38e8 温度❌(内部1.8e10K) |
|
||||||
|
| 18 | t60000_g5.5_he-4_c-4_n-4_o-4 | baseline | NaN=3789 |
|
||||||
|
| 19 | 同上 | itek0_iacc0_dpsilg15 | 26步 末1.72 温度✅(0.85×Teff)未收敛 |
|
||||||
|
|
||||||
|
### seed_nc(1 点,3 变体,用生产真实种子)
|
||||||
|
|
||||||
|
| # | 点 | 变体 | 结果 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 20 | t60000_g5.0_he-2_c-4_n-4_o-4 | seedprod(生产) | STOP iter14 (1.71e17) ← 精确复现生产 |
|
||||||
|
| 21 | 同上 | seed_doubleoff | 20步 末4.71e6 温度✅(1.58×Teff)未收敛 |
|
||||||
|
| 22 | 同上 | seed_doubleoff_dpsilg15 | 20步 末48.6 温度❌(7.89×Teff) |
|
||||||
|
|
||||||
|
### 冒烟测试(方法验证)
|
||||||
|
|
||||||
|
| # | 内容 | 结果 |
|
||||||
|
|---|---|---|
|
||||||
|
| 23 | lte/nc/nl 三阶段调用方式 | 验证通过,fort.7/fort.9 正常生成 |
|
||||||
|
| 24 | 生产轨迹复现 | seedprod 轨迹与生产 DB 一致 |
|
||||||
@@ -0,0 +1,507 @@
|
|||||||
|
# TLUSTY 不收敛根因分析(1105 个网格点)
|
||||||
|
|
||||||
|
**日期**:2026-08-11
|
||||||
|
**数据来源**:`/home/fmq/下载/dcts.db`(server DB 快照)+ `data/salvage/`(8320 次任务尝试的完整产物)
|
||||||
|
**源码版本**:`tlusty208.f`(50010 行)+ dcts 框架 `crates/{common,node,server}`
|
||||||
|
**工作流**:`sdB_cno`(grid 共 9216 点,converged 8102 / failed 1105 / pending 9)
|
||||||
|
|
||||||
|
> 本文不修改任何代码,只做根因定位与修复建议。修复实施见文末「后续行动」。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ 2026-08-12 更新(修复建议已实测证伪,配置改动已回退)
|
||||||
|
|
||||||
|
本文 §3.1 A1/A6 与「后续行动」中推荐的**数值配置类修复**(收紧 DPSILG、禁用
|
||||||
|
Kantorovich/ITEK、禁 Ng/IACC)已经用控制实验实测验证:**均无法让失败点从冷启动
|
||||||
|
收敛**,相关配置改动已全部回退。详见 `docs/cold_start_fix_2026_08_12.md`。
|
||||||
|
|
||||||
|
实测结论(6 个测试点 × 4 种配置 = 24 次运行,**0 次收敛**):
|
||||||
|
- `ITEK=0` → nitzer=0 冻结 populations(`tlusty208.f:1937`),不是禁 Kantorovich;
|
||||||
|
- `IACC=0` → 被 clamp 回 7(`tlusty208.f:1928`),空操作;
|
||||||
|
- 设 `ITEK/IACC > NITER`(真正禁用 Kant+Ng)→ 发散幅度降 1e12~1e14×,但仍不收敛
|
||||||
|
(末 relc 1e3~1e7),Newton 在 grey LTE 起点即处于收敛域外;
|
||||||
|
- `DPSILG=1.5` 有害(截断步长致漂移)。
|
||||||
|
|
||||||
|
**因此本文方向性结论(物理吸引域窄 + 框架无自适应)成立,但可行的修复不在
|
||||||
|
数值配置,而在「连续法(从已收敛较低 Teff 模型逐步过渡)」与「He 匹配种子选择」
|
||||||
|
等框架级改造。** 相关计算文件见 `docs/cold_start_fix_2026_08_12.md`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、结论先行
|
||||||
|
|
||||||
|
1105 个失败网格点全部走完了 `cold_run → seed_step` 的完整退化路径,**两种策略都失败**。这**不是**种子选择问题,**不是**单纯的初值问题,而是两个因素叠加:
|
||||||
|
|
||||||
|
1. **物理侧**——TLUSTY 的 Newton 完全线性化求解器在「高 Teff + 低 logg」参数角落的吸引域过窄,辐射场对布居数极度敏感;
|
||||||
|
2. **工程侧**——dcts 框架缺少发散检测与自适应阻尼反馈,单阶段发散后无回退,只能靠 NITER 自然耗尽再整点重试。
|
||||||
|
|
||||||
|
失败点本身**并非不可解**——同族参数下大量点是收敛的(T=60k/g=5.0 仍有 78/256 收敛),失败的只是收敛盆地最窄的那一批。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、数据证据链
|
||||||
|
|
||||||
|
### 2.1 退化序列(DB `tasks` 历史,1105/1105 全部命中)
|
||||||
|
|
||||||
|
```
|
||||||
|
["cold_run"] → ["cold_run","seed_step"] → ["seed_step"] → ["cold_run","seed_step"] → ["seed_step"] ...
|
||||||
|
```
|
||||||
|
|
||||||
|
每个失败点重试均值 **6.9 次**(`grid_points.attempt_count` 分布:5 次=199,6 次=135,7 次=462,8 次=275,9-13 次=33),在两种策略间反复跳。最新任务全部停在 `failed_stage=tlusty`、`tlusty_strategies=["seed_step"]`。
|
||||||
|
|
||||||
|
### 2.2 失败点的参数分布(物理一致)
|
||||||
|
|
||||||
|
| Teff | 失败数 | | logg | 失败数 | | loghe | 失败数 |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| 60k | 344 | | 5.0 | 587 (53%) | | -4.0 | 345 |
|
||||||
|
| 55k | 330 | | 5.5 | 285 | | -2.0 | 337 |
|
||||||
|
| 50k | 204 | | 6.0 | 118 | | 0.0 | 274 |
|
||||||
|
| 45k | 84 | | 6.5 | 115 | | 2.0 | 149 |
|
||||||
|
| ≤40k | 143 | | | | | | |
|
||||||
|
|
||||||
|
**高 Teff + 低 logg** 高度集中(≥50k 占 878/1105 ≈ 79%;logg=5.0 占 53%)。
|
||||||
|
|
||||||
|
**Teff × logg 收敛率矩阵**(节选,完整矩阵见 §6 附录):
|
||||||
|
|
||||||
|
```
|
||||||
|
g=5.0 g=5.5 g=6.0 g=6.5
|
||||||
|
T=20k 249/256 242/256 238/256 210/256
|
||||||
|
T=40k 233/256 252/256 253/256 256/256
|
||||||
|
T=50k 119/256 224/256 233/256 244/256
|
||||||
|
T=55k 78/256 156/256 227/256 231/256
|
||||||
|
T=60k 78/256 152/256 222/256 228/256
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键**:没有任何格子是 0% 收敛——所以这是"难",不是"不可能"。难度梯度清晰:随 Teff 升高收敛率下降,随 logg 升高收敛率上升。
|
||||||
|
|
||||||
|
### 2.3 失败阶段分布(按 salvage 最后一次尝试)
|
||||||
|
|
||||||
|
| 首失败阶段 | 占比 | 失败特征 |
|
||||||
|
|---|---|---|
|
||||||
|
| `seed_nc`(seed_step 第一阶段) | 1033/1105 (93%) | max_relc 单调雪崩 |
|
||||||
|
| `nc`(cold_run 第二阶段) | 72/1105 | max_relc 单调雪崩 |
|
||||||
|
| `lte` | 0 | —— |
|
||||||
|
|
||||||
|
**两种策略的死亡轨迹几乎相同**——max_relc 在 5 步内从个位数飙到 1e6+。典型例:
|
||||||
|
|
||||||
|
```
|
||||||
|
cold_run nc (T=60k g=5.0):
|
||||||
|
[9.72, 46.5, 259, 1950, 4.85e6, 2.6e10, 2.8e4, 4.04e7, 2.06e11, 2.32e15]
|
||||||
|
|
||||||
|
seed_step seed_nc (T=60k g=5.0, 从收敛邻居热启动):
|
||||||
|
[7.0, 115, 272, 2020, 1.49e8, 8.26e13, 3310, 2.09e7, 7.53e10, 4.71e14, ...]
|
||||||
|
```
|
||||||
|
|
||||||
|
> 注:salvage 中只保留了 282 个点的 cold_run 中间产物(节点在重试时会清理中间态),但 **282/282 全部死在 `nc` 阶段**,且 max_relc 雪崩轨迹与 seed_nc 一致。
|
||||||
|
|
||||||
|
最后一次尝试的失效特征分两档(按 `atmosphere_has_nan` 拆分):
|
||||||
|
|
||||||
|
| 失效特征 | 点数 / 1105 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| `atmosphere_has_nan: true` | **434 (39%)** | 雪崩污染了大气文件;伴随 `temp_check.error: 深度 1 T=NaNK`、`emflux NaN 占比 100%`、`bfac 极端值占比 36%`、`.err: IEEE_INVALID_FLAG IEEE_DIVIDE_BY_ZERO` |
|
||||||
|
| `atmosphere_has_nan: false` | **671 (61%)** | max_relc 雪崩但大气未污染到 NaN;被 `temp_check`(表层 T 超过 3×Teff)、`bfac_check`(极端值占比 >10%)、`emflux_check`(积分通量偏离 σT⁴)等后验校验判废,或 nl 阶段才崩 |
|
||||||
|
|
||||||
|
> ⚠️ **修订说明**:初版误把 "atmosphere_has_nan=1033/1105" 写入报告——1033 实为 "首失败阶段=seed_nc 的点数",与 NaN 计数无关。真实 NaN 仅 434/1105。雪崩到 1e6+ 未必把 `fort.7` 写成 NaN(取决于 TLUSTY 是撞 1e16 STOP 还是跑完 NITER),故多数失败点的大气文件本身可读,是被后验校验(温度结构/b 因子/emflux)挡下的。
|
||||||
|
|
||||||
|
### 2.4 种子选择不是唯一根因(但不能完全排除)
|
||||||
|
|
||||||
|
1105 个失败点的 seed_step 配对 **全部来自 exact_family**(`d_teff<5000 & d_logg<0.01 & d_loghe<0.01`),且 **662 个是贫方向**(seed_finder 设计的稳定方向):
|
||||||
|
|
||||||
|
```
|
||||||
|
seed direction: poor=662, rich=373, same=70
|
||||||
|
d_teff 分布: <5k(exact)=1105, 其余=0
|
||||||
|
d_logg 分布: 0(same)=1105, 其余=0
|
||||||
|
```
|
||||||
|
|
||||||
|
> ⚠️ **校正**:初版标题"种子不是问题(关键反证)"措辞过强。准确结论是**种子选择不是唯一根因,但不能排除它是贡献因素之一**:
|
||||||
|
> 1. **"exact_family" 不等于物理极近**:判定允许 `ΔTeff < 5000K`(半步容忍)。sdB 网格 Teff 步长 10000K 时,55k↔60k 相邻档互作种子的辐射场(Lyman 紫外主导)差异可能很大——"exact_family" 标签覆盖了 Teff 仍可差一整档的事实。
|
||||||
|
> 2. **443 个 rich/same 方向失败**(373 rich + 70 same):seed_finder 自身的回测数据(`seed_finder.rs:14-25`)称富方向成功率仅 3-11%,贫方向 42-54%。这部分失败可能部分源于种子方向不佳。
|
||||||
|
> 3. **种子质量未审**:失败点用的种子虽来自收敛邻居,但该邻居是在多少迭代数、多干净的 max_relc 下收敛的(擦边收敛 vs 干净收敛),本报告未分析。
|
||||||
|
>
|
||||||
|
> 综上,"662 贫方向仍爆"只能说明"贫方向种子不足以保证收敛",**不能**推出"种子方向无关"。种子方向的影响需在 §五 A/B 测试中单独控制变量验证。
|
||||||
|
|
||||||
|
### 2.5 seed_step 在同区域能成功(说明非不可解,但未排除混淆变量)
|
||||||
|
|
||||||
|
高 Teff 危险区里 seed_step 仍有大量成功:
|
||||||
|
|
||||||
|
```
|
||||||
|
T=50k g=5.0: 119 conv (cold=51, seed=68)
|
||||||
|
T=50k g=5.5: 224 conv (cold=87, seed=137)
|
||||||
|
T=55k g=5.0: 78 conv (cold=53, seed=25)
|
||||||
|
T=60k g=5.0: 78 conv (cold=40, seed=38)
|
||||||
|
```
|
||||||
|
|
||||||
|
成功路径的 conv.json 显示 `seed_nc` 即使 `converged=false` 也可用(`note: "accepted as seed (convergence not required)"`),关键是后续 `nl` 能否从不完全收敛的种子收敛到 `max_relc < 1e-4`。失败点中 434/1105 的 `nl` 产出 NaN 大气,其余 671 点被后验校验挡下。
|
||||||
|
|
||||||
|
> ⚠️ **校正**:初版据"同格有成功"推出"非不可解",逻辑成立——同参数区不是数学上不可解。但**未排除混淆变量**:成功的 78 个点(T=60k/g=5.0)与失败的 178 个点用的种子来源/方向可能系统性不同。尤其 cold=40 成功(从 LTE 灰化初值也能收敛一部分)反而暗示**初值/种子的微小差异决定成败**——这更像"吸引域窄 + 初值敏感",而非单纯的"求解器稳定性"。本节只能支撑"非数学不可解",不能支撑"种子/初值无关"。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、源码级根因
|
||||||
|
|
||||||
|
### 3.1 TLUSTY 侧(`tlusty208.f`)
|
||||||
|
|
||||||
|
#### A1. `DPSILG=10.` 的限幅太松 — `tlusty208.f:14652-14653`
|
||||||
|
|
||||||
|
```fortran
|
||||||
|
IF(CHAN.LE.UN/DPSILG-UN) CHAN=UN/DPSILG-UN ! 下限 = 1/10 - 1 = -0.9
|
||||||
|
IF(CHAN.GE.DPSILG-UN) CHAN=DPSILG-UN ! 上限 = 10 - 1 = +9.0
|
||||||
|
```
|
||||||
|
|
||||||
|
默认 `DPSILG=10.`(PVALUE 表 idx 185,`tlusty208.f:1793`),允许单步相对修正达 **10 倍**。
|
||||||
|
|
||||||
|
> ⚠️ **重要校正**:DPSILG 这个宽松限幅(±10×)**主要作用于辐射场(`I ≤ NFREQE`)和布居数块(`I ≥ NFREQE+INSE`)**。紧随其后 `tlusty208.f:14654-14677` 有一组**逐变量更严格的限幅**,按 `INRE/INHE/INPC/INDL` 偏移把对应槽位钳到更窄:
|
||||||
|
> - **温度 T**(`I = NFREQE+INRE`):受 `DPSILT=1.25` 钳制 → 单步上限仅 **±25%**
|
||||||
|
> - **总粒子数 TOTN**(`I = NFREQE+INHE`)、**电子密度 ne**(`I = NFREQE+INPC`):受 `DPSILN=10.` 钳制 → ±10×(与 DPSILG 同级,宽松)
|
||||||
|
> - **Δ(湍流相关,`I = NFREQE+INDL`):受 `DPSILD=1.25` → ±25%**
|
||||||
|
|
||||||
|
所以"高 Teff 下结构量步长放大 10 倍必爆"这一表述**对温度不成立**(温度有 ±25% 独立约束)。真正受 10× 宽松限幅影响的是**辐射场**与**布居数/TOTN/ne**。雪暴主因更可能是辐射场与布居的耦合放大,而非温度跳变。
|
||||||
|
|
||||||
|
#### A2. 首步发散无 STOP 保护 — `tlusty208.f:14700`
|
||||||
|
|
||||||
|
```fortran
|
||||||
|
IF(ITER.NE.1 .AND. ABS(CHMX).GT.1.D16) THEN
|
||||||
|
WRITE(6,610) ITER,CHMX
|
||||||
|
STOP
|
||||||
|
END IF
|
||||||
|
```
|
||||||
|
|
||||||
|
只在 `ITER≥2` 才检查 1e16 雪崩 STOP。**首步(ITER=1)即使 CHMX 巨大也不触发 STOP**——后续迭代在已被污染的解上继续算,直到某步偶然撞上 1e16 才停。
|
||||||
|
|
||||||
|
> ⚠️ **校正**:初版称"第 1 步任意大都继续跑"措辞过强。首步的 CHAN 仍受 A1 限幅钳制(辐射/布居 ±[-0.9,+9],温度 ±25%),**并非"无限幅"**;首步只是**无 STOP**。真正的风险是:限幅允许的 +9×(辐射/布居)在多深度多变量上叠加,几步内放大成 Inf/NaN——这与 §2.3 观测的雪崩量级一致。
|
||||||
|
> 另:同样的 STOP 逻辑在 SOLVES(`tlusty208.f:15047`)和 RYBSOL(`tlusty208.f:47587`)另两条求解路径各有一份。
|
||||||
|
|
||||||
|
#### A3. `ORELAX` 只阻尼布居数,不阻尼辐射场与温度等结构量 — `tlusty208.f:14647`
|
||||||
|
|
||||||
|
```fortran
|
||||||
|
C over-relaxation
|
||||||
|
IF(I.GE.NFREQE+INSE) CHAN=ORELAX*CHAN
|
||||||
|
```
|
||||||
|
|
||||||
|
> ⚠️ **重大校正**:初版把 PSI 向量布局写反了。经核对 `tlusty208.f:3910-3936`(PSY0 各槽位赋值)与 `tlusty208.f:795-797`(INRE/INPC/INSE 偏移量定义:INDL=3, INPC=4, INSE=5),正确布局是:
|
||||||
|
|
||||||
|
```
|
||||||
|
[1 .. NFREQE] = 辐射场(RADEX 平均强度 Jν) → 不被 ORELAX 松弛
|
||||||
|
[NFREQE+INHE] = [NFREQE+1] = 总粒子数 TOTN → 不被 ORELAX 松弛
|
||||||
|
[NFREQE+INRE] = [NFREQE+2] = 温度 T → 不被 ORELAX 松弛
|
||||||
|
[NFREQE+INPC] = [NFREQE+4] = 电子密度 ne → 不被 ORELAX 松弛
|
||||||
|
[NFREQE+INSE] = [NFREQE+5] = 布居数 superlevels 块的起点 → ORELAX 生效(I ≥ NFREQE+INSE)
|
||||||
|
```
|
||||||
|
|
||||||
|
**结论**:`ORELAX` 松弛的是 **布居数(populations)**,**不是**温度/ne/TOTN 等结构量。初版把"布居"与"结构量"的松弛关系完全对调了。
|
||||||
|
|
||||||
|
这对结论的影响:
|
||||||
|
- 框架给 seed_nc 设的 `ORELAX=0.3` **确实会作用到布居数**(初版说"对结构量、对辐射场不生效"是错的——准确说是"对布居生效,对辐射场与温度/ne 不生效")。
|
||||||
|
- **真正欠松弛缺失的是辐射场 Jν**——高 Teff 下辐射场最敏感、最需要欠松弛,而它既不受 ORELAX 保护、又只受 A1 的 ±10× 宽松限幅。这一论断方向不变,但论证链条已修正。
|
||||||
|
|
||||||
|
#### A4. `CHMAX` 只判终值 — `tlusty208.f:14728`
|
||||||
|
|
||||||
|
```fortran
|
||||||
|
LFIN=ABS(CHMX).LE.CHMAX .OR. ITER.GE.NITER
|
||||||
|
```
|
||||||
|
|
||||||
|
`CHMAX=1e-3`(默认,PVALUE idx 77,`tlusty208.f:1767`)是"最后一次迭代最大修正"的终值门槛。**CHMAX 本身对过程无约束**——但它不是唯一的过程机制,见 A5/A6。
|
||||||
|
|
||||||
|
#### A5. ⚠️ 初版遗漏:CHMAXT 驱动的过程反馈 — `tlusty208.f:14711, 22954-22958`
|
||||||
|
|
||||||
|
初版称"过程爆掉只能靠 NITER 自然耗尽"是**夸大**。源码中存在由 `CHMAXT`(默认 0.01,PVALUE idx 80,`tlusty208.f:1767`)驱动的两层过程反馈:
|
||||||
|
|
||||||
|
```fortran
|
||||||
|
C tlusty208.f:14711 — 温度变化大时重置铁线截面
|
||||||
|
if(chmt.gt.chmaxt .and. ispodf.ge.1) LIROST=.TRUE.
|
||||||
|
if(LITEK.and.LIROST) then
|
||||||
|
call iroset
|
||||||
|
LIROST=.FALSE.
|
||||||
|
end if
|
||||||
|
|
||||||
|
C tlusty208.f:22954-22958 — 温度变化小时锁定 Ng 加速节奏
|
||||||
|
if(chmt.lt.chmaxt) then
|
||||||
|
do itl=iter,niter+1
|
||||||
|
nitlam(itl)=nlamt
|
||||||
|
end do
|
||||||
|
end if
|
||||||
|
```
|
||||||
|
|
||||||
|
这是针对**温度变化**(CHMT)的自适应机制。但它**不直接约束辐射场与布居的耦合发散**——而这恰恰是失败点雪暴的主区。所以 A5 的存在削弱了"过程完全无约束"的措辞,但不改变"雪暴主因缺乏针对性干预"的结论。
|
||||||
|
|
||||||
|
#### A6. ⚠️ 初版遗漏:ITEK / Kantorovich 加速切换 — `tlusty208.f:843-856`
|
||||||
|
|
||||||
|
默认 `ITEK=4`(PVALUE idx 130,`tlusty208.f:1779`)。源码:
|
||||||
|
```fortran
|
||||||
|
IF(ITEK.GT.0) THEN
|
||||||
|
DO IKT=ITEK+1,NITER
|
||||||
|
KANT(IKT)=1 ! iter≥5 切到 Kantorovich 加速
|
||||||
|
END DO
|
||||||
|
DO IKT=IACC,18,IACD
|
||||||
|
KANT(IKT)=0 ! 但 Ng 重启点(IACC=7,18,IACD=4) 回到完全线性化
|
||||||
|
END DO
|
||||||
|
```
|
||||||
|
|
||||||
|
即 **iter≥5 切换到 Kantorovich 加速迭代**(更便宜的近似,少算矩阵元)。初版把 ITEK 标为"失败相关性:低"是**无依据的臆断**——雪暴轨迹(如 §2.3 的 `[9.72, 46.5, 259, 1950, 4.85e6, ...]`)第 5 步开始爆,与 Kantorovich 切换点(iter≥5)**时间上重合**。这是一个**尚未排查的嫌疑诱因**:Kantorovich 在发散区可能因近似失真加剧不稳定。**降 DPSILG(修复优先级 3)若救回率不如预期,应优先排查 ITEK。**
|
||||||
|
|
||||||
|
### 3.2 框架侧(`crates/common/src/runner.rs` / `crates/node/src/executor.rs`)
|
||||||
|
|
||||||
|
#### B1. 无链内重试,无自适应阻尼
|
||||||
|
|
||||||
|
`default_seed_chain` / `default_cold_chain`(`runner.rs:20-98`)一旦构造完成参数就是死的。`conv_check::check_fort9`(`conv_check.rs:204-217`)判完发散后,`runner.rs:564-570` 只在 `require_converged=true` 时 `break`,否则继续——**没有任何"发现 max_relc 超过阈值就降低 DPSILG / 加大 ORELAX / 重跑一次"的反馈机制**。
|
||||||
|
|
||||||
|
#### B2. `seed_nc` 的 `require_converged=false` 反而帮了倒忙
|
||||||
|
|
||||||
|
`default_seed_chain` 第一阶段设 `require_converged=false`("接受非收敛种子"),本意是"给 nl 一个起点就行"。`runner.rs:518-521` 在 `rc==0 && fort7.is_file()` 分支**无条件**把本阶段 `fort.7` 拷成下一阶段的 `current_seed`——**stage 间种子传递路径上没有 `atmosphere_has_nan` 检查**。
|
||||||
|
|
||||||
|
> ⚠️ **校正**:初版称"雪崩后仍然把含 NaN 的 fort.7 喂给 nl"措辞过死。是否真的传 NaN 取决于 TLUSTY 的退出方式:
|
||||||
|
> - 若雪崩撞上 1e16 STOP(A2)→ rc≠0 → 走 `runner.rs:522-531` else 分支,**不拷** fort.7;
|
||||||
|
> - 若跑完 NITER 自然结束(rc=0)但 `fort.7` 已含 NaN → **会拷**给 nl,nl 自然也爆。
|
||||||
|
>
|
||||||
|
> §2.3 实测 434/1105 真有 NaN 大气——这部分点确属此路径。其余 671 点是雪崩但大气可读,被后验校验挡下。两档都要修。
|
||||||
|
|
||||||
|
#### B3. `CHMAX=None` 让 TLUSTY 走默认,但没把更保守的 `DPSILG`/`ORELAX` 一起传
|
||||||
|
|
||||||
|
所有默认阶段 `chmax=None`(`runner.rs:20-98`),runner 用 Rust 侧 `eff_chmax=0.001`(`runner.rs:443`)做**后验**判断,但这个值**不传给 TLUSTY**。结果 TLUSTY 用默认 `DPSILG=10.` + `ORELAX=1.0`(冷链)/ `0.3`(种子链,作用于布居)跑,过程约束针对辐射场的几乎为零(见 A3 校正后的作用域)。
|
||||||
|
|
||||||
|
`ChainStep` 结构体(`config.rs:1130-1150`)字段为 `label/lte/ltgray/ilvlin/require_converged/niter/chmax/itek/ichang/idlte/iacc/orelax`,**没有** `dpsilg` 字段。要分阶段注入 DPSILG 需新增字段(走 `nst_writer` line1),或通过 YAML `tlusty_input.nst_extra_keys` 逃生舱(`nst_writer.rs:138-161` 支持,对当前阶段生效)。
|
||||||
|
|
||||||
|
#### B4. 整点重试无参数扰动
|
||||||
|
|
||||||
|
策略 fallback(`scheduler.rs::trigger_strategy_fallback`)只在 `["cold_run"]` 与 `["seed_step"]` 之间切换,重试任务的 `tlusty_chain_params`/`seed_chain_params` 是工作流 YAML 的**逐字克隆**(`scheduler.rs:1065-1066`)——**TLUSTY 数值参数零扰动**。
|
||||||
|
|
||||||
|
> ⚠️ **校正**:初版"每次重跑参数完全一致(除了种子来源)"自相矛盾——种子来源恰恰是初值的一部分。准确表述:**同一策略内的重试是确定性白跑**(相同初值+参数必爆);**跨策略切换时换了初值**(cold_run 无种子 LTE 灰化初值 → seed_step 用近邻收敛种子),但 TLUSTY 数值参数始终不变。所以"重试均值 6.9 次"中只有跨策略切换那几次换了实质初值,同策略内确属白跑。
|
||||||
|
|
||||||
|
### 3.3 未排除的替代根因(诚实声明)
|
||||||
|
|
||||||
|
> 本节是审查后新增。初版把 DPSILG/ORELAX/CHMAX 这些**机制描述**当作根因,但严格地说这是**充分性论证,不是必要性论证**。即便 A1-A6 全为真,也不能排除主要根因在以下未被充分审视的环节。这些替代假说需在 §五 A/B 测试中逐一排除或确认。
|
||||||
|
|
||||||
|
| 替代假说 | 依据 | 排查方法 |
|
||||||
|
|---|---|---|
|
||||||
|
| **TFLOOR=8000K 在高 Teff 下不合理** | `config.rs:815` 默认 8000。Teff=60k 大气表层物理温度 ~30-90k,TFLOOR=8000K 离表层不远;若发散把温度推向 TFLOOR,钳制本身可能破坏 H 电离平衡(8000K 附近 H 电离度突变) | 对失败点试 TFLOOR=30000/40000,看救回率 |
|
||||||
|
| **ND=50 在高 Teff+低 logg 下分辨率不足** | `config.rs:758` 框架覆盖为 50(TLUSTY 默认 70,config 自承)。高 Teff+低 logg 表层温度梯度陡峭,ND=50 可能欠分辨表层 → 数值发散 | 对失败点试 ND=70/100 |
|
||||||
|
| **DDNU=50/CNU1=6 频率网格在高 Teff 不足** | `config.rs:769-776` 框架 DDNU=50(TLUSTY 原始 0.75)、CNU1=6(原始 4.5)。60k 大气 Lyman 极端紫外主导,此"sdB 调优"频率网格可能采样不足 → 辐射场离散误差大 | 对失败点试更密的频率网格 |
|
||||||
|
| **特定离子原子数据 bug** | `config.rs` ions 表用 C III `c3_34+12lev.dat`、N II `n2_32+10lev.dat` 等组合能级文件;某高 Teff 主导离子的能级数据若有误,会导致该离子布居发散 | 对失败点逐离子禁用排查 |
|
||||||
|
| **LTE 阶段未干净通过** | 失败点 salvage 的 `.err` 含 `IEEE_DIVIDE_BY_ZERO`;若某深度 LTE 灰化初值的 ne 近零导致除零,可能**先于** NLTE 雪崩 | 检查失败点的 `.lte.err`/`.lte.6` 是否干净 |
|
||||||
|
| **输入文件渲染 bug** | `gen_input5.rs` 渲染丰度 `10^logx`,logc=-4 等极端值时未验证字节级正确 | 抽查失败点的 `.5` 文件丰度数值 |
|
||||||
|
|
||||||
|
**优先级**:前三个(TFLOOR/ND/DDNU)与"A1-A6 机制"正交,是独立的数值配置假说,应在方案 A 的 A/B 测试中顺带排除。若方案 A(降 DPSILG)救回率远低于 30%,应立即转向这些替代假说。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、修复建议(按性价比排序)
|
||||||
|
|
||||||
|
### 优先级 1:自适应发散检测 + 阻尼回退(框架侧,治本)
|
||||||
|
|
||||||
|
在 `runner.rs` 的 stage 循环里加一层 **"单阶段内 iftek 监控 + 回退重跑"**:
|
||||||
|
|
||||||
|
1. 用 `tokio` 异步跟踪 `fort.9` 增长,每完成 1-2 次迭代解析一次 `max_relc`;
|
||||||
|
2. 若前 3 步 `max_relc` 单调上升且超过阈值(如 1e2),**SIGTERM 当前子进程**;
|
||||||
|
3. 用更保守的参数(`DPSILG` 减半、`ORELAX` 减半)重跑该阶段;
|
||||||
|
4. 最多回退 2-3 次,每次更保守。
|
||||||
|
|
||||||
|
这是最直接的对症方案——雪崩在前 3 步就能看出来,没必要跑到 NITER。
|
||||||
|
|
||||||
|
**实现成本**:中-高。需要在 `ChainStep` 加 `dpsilg`/`adaptive` 字段,runner 加监控+回退逻辑。初版估的 "~300 行 Rust" 偏低——实时跟踪 fort.9 增长(TLUSTY 边写边追加,需处理部分写入)+ SIGTERM 子进程后正确 reap + 清理中间文件 + 回退状态机,工程量更大,预估 400-600 行。
|
||||||
|
|
||||||
|
### 优先级 2:给辐射场与温度/ne 加欠松弛(TLUSTY 源码侧,治本但要改二进制)
|
||||||
|
|
||||||
|
> ⚠️ **校正**:初版给的代码是**替换**原逻辑(`I.GE.` → `I.LT.`),会**移除**原布居松弛——这反而让布居失去保护。正确改法是**新增一行**,且 `ORELAXR` 在源码中本不存在。
|
||||||
|
|
||||||
|
**正确改法**(`tlusty208.f:14647`,SOLVE 与 SOLVES `14996` 两处都要改):
|
||||||
|
```fortran
|
||||||
|
C 现状:只有布居(I≥NFREQE+INSE)被 ORELAX 松弛
|
||||||
|
IF(I.GE.NFREQE+INSE) CHAN=ORELAX*CHAN
|
||||||
|
C 新增:辐射场(I≤NFREQE)与结构量(NFREQE+1..NFREQE+INSE-1,即 T/TOTN/ne/ZD/DENS/DELTA)
|
||||||
|
C 按 ORELAXR 松弛(默认 1.0=不生效,难收敛区配 0.3-0.5)
|
||||||
|
IF(I.LT.NFREQE+INSE) CHAN=ORELAXR*CHAN
|
||||||
|
```
|
||||||
|
|
||||||
|
物理论据(修正后):辐射场 Jν 与温度/ne 在高 Teff 下最敏感、却都不被 ORELAX 松弛(A3 校正后)。给它们加欠松弛是物理上最对症的修复。
|
||||||
|
|
||||||
|
**注意**:
|
||||||
|
1. **不是"改 2 行 + 重编"**。`ORELAXR` 在 `tlusty208.f` 中完全不存在——需扩 VARNAM/PVALUE 两个 DATA 数组各加一项、加 nst 关键字解析、加 PVALUE 默认值(建议 1.D0 不变,仅难收敛阶段通过 nst 覆盖为 0.3-0.5)。工程量约 30-50 行 Fortran + 重编 + 更新 `assets/tlusty_static`。
|
||||||
|
2. **与 NLAMBD(λ-迭代)的交互未验证**:辐射场在每个深度通过 NLAMBD 机制单独更新,给 Jν 加欠松弛可能与该机制冲突,需小规模测试验证。
|
||||||
|
3. 触及二进制,需走完整发布流程。
|
||||||
|
|
||||||
|
### 优先级 3:动态降低 DPSILG(框架侧,立即可做,无侵入)
|
||||||
|
|
||||||
|
在 `ChainStep` 加可选字段 `dpsilg: Option<f64>`,通过 `nst_extra_keys` 注入。给种子链和难收敛区(Teff ≥ 50k & logg ≤ 5.5)设 `DPSILG=3.` 或 `DPSILG=2.`(对应单步上限 ±2 倍 / ±1 倍)。收敛性会显著改善。
|
||||||
|
|
||||||
|
代价:迭代步数变多,但比爆掉重试 7-9 次便宜得多。
|
||||||
|
|
||||||
|
**实现成本**:低。~50 行 Rust + YAML 配置。**可立即上线**。
|
||||||
|
|
||||||
|
### 优先级 4:seed_nc 发散时拒绝喂给 nl(框架侧,止损)
|
||||||
|
|
||||||
|
`runner.rs` 里,当 seed_nc 的 `atmosphere_has_nan` 或 `best_max_relc > 1e3` 时,**不要**把它的 `fort.7` 当作 nl 的种子;要么直接判失败触发下一策略,要么回退用原 `seed_atmos`(邻居的干净种子)重新喂 nl。
|
||||||
|
|
||||||
|
**实现成本**:极低。~20 行 Rust。
|
||||||
|
|
||||||
|
### 优先级 5:首步发散保护(TLUSTY 源码侧)
|
||||||
|
|
||||||
|
`tlusty208.f:14700`(SOLVE)/ `15047`(SOLVES)/ `47587`(RYBSOL)三处把 `ITER.NE.1` 改为允许首步也检查(用更宽松的阈值,如 1e8),防止首步就冲出吸引域:
|
||||||
|
|
||||||
|
```fortran
|
||||||
|
C 改前
|
||||||
|
IF(ITER.NE.1 .AND. ABS(CHMX).GT.1.D16) THEN ... STOP
|
||||||
|
C 改后
|
||||||
|
IF(ITER.EQ.1 .AND. ABS(CHMX).GT.1.D8) THEN ... STOP
|
||||||
|
IF(ITER.GT.1 .AND. ABS(CHMX).GT.1.D16) THEN ... STOP
|
||||||
|
```
|
||||||
|
|
||||||
|
**实现成本**:极低(改 2 行 + 重编)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、验证路径
|
||||||
|
|
||||||
|
用 **30 个失败点**做 A/B 测试(覆盖 60k/55k/50k × g5.0/5.5,每格 5 点):
|
||||||
|
|
||||||
|
| 方案 | 配置 | 预期救回率(假设性估计) |
|
||||||
|
|---|---|---|
|
||||||
|
| 基线 | 当前配置重跑 | 0%(可复现) |
|
||||||
|
| 方案 A | 优先级 3(DPSILG=3)+ 优先级 4(NaN 拒绝) | 30-50% |
|
||||||
|
| 方案 B | 方案 A + 优先级 1(自适应回退) | 70-85% |
|
||||||
|
| 方案 C | 方案 B + 优先级 2/5(改 TLUSTY 源码) | 90%+ |
|
||||||
|
|
||||||
|
> ⚠️ **校正**:初版无标注地把上述救回率当作事实呈现。这些百分比是**无数据支撑的先验猜测**,仅用于排列方案的优先级顺序,不代表实测预期。尤其"方案 C 90%+"与"吸引域窄"的论断存在张力——吸引域窄意味着即便阻尼到位,某些初值仍落吸引域外。**方案 A 的实测救回率是最关键的诊断信号**:若远低于 30%,说明 DPSILG 不是主因,应转向排查 ITEK/Kantorovich(A6)或 §3.3 列出的替代根因。
|
||||||
|
|
||||||
|
**建议**:先实施方案 A 验证假设(DPSILG 是否真是主因),用实测数据说话,再决定是否值得改 TLUSTY 源码走方案 C。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、附录
|
||||||
|
|
||||||
|
### 6.1 完整 Teff × logg 收敛率矩阵
|
||||||
|
|
||||||
|
```
|
||||||
|
logg-> g=5.0 g=5.5 g=6.0 g=6.5
|
||||||
|
T=20k 249/256 242/256 238/256 210/256
|
||||||
|
T=25k 251/256 254/256 256/256 253/256
|
||||||
|
T=30k 252/256 255/256 254/256 256/256
|
||||||
|
T=35k 244/256 252/256 256/256 256/256
|
||||||
|
T=40k 233/256 252/256 253/256 256/256
|
||||||
|
T=45k 207/256 230/256 247/256 254/256
|
||||||
|
T=50k 119/256 224/256 233/256 244/256
|
||||||
|
T=55k 78/256 156/256 227/256 231/256
|
||||||
|
T=60k 78/256 152/256 222/256 228/256
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 TLUSTY 关键变量默认值(PVALUE 表,`tlusty208.f:1750-1804`)
|
||||||
|
|
||||||
|
> 行号为 PVALUE DATA 块内每个值实际所在行(经 VARNAM/PVALUE 平行映射逐项核对,idx 从 1 起)。
|
||||||
|
|
||||||
|
| 变量 | 默认值 | PVALUE 行号 | 含义 | 失败相关性 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `CHMAX` | `1.D-3` | 1767 (idx77) | 收敛门槛(终值) | 低(只判终值) |
|
||||||
|
| `DPSILG` | `10.` | **1793** (idx185) | 单步相对修正上限(辐射场+布居) | **高**(辐射场/布居 10 倍太松) |
|
||||||
|
| `DPSILT` | `1.25` | 1793 (idx186) | **温度**单步上限(±25%) | 低(温度有独立严约束) |
|
||||||
|
| `DPSILN` | `10.` | 1793 (idx187) | TOTN/ne 单步上限 | 中 |
|
||||||
|
| `DPSILD` | `1.25` | 1793 (idx188) | Δ(湍流)单步上限 | 低 |
|
||||||
|
| `CHMAXT` | `0.01` | 1767 (idx80) | 温度变化过程反馈阈值 | 中(A5 机制,未针对辐射场) |
|
||||||
|
| `ORELAX` | `1.D0` | 1779 (idx131) | **布居数**过松弛因子 | 中(框架覆盖为 0.3/0.5,作用于布居) |
|
||||||
|
| `NITER` | `30` | 1763 (idx64) | 最大迭代数 | 低 |
|
||||||
|
| `ITEK` | `4` | 1779 (idx130) | Kantorovich 加速起始 iter | **待排查**(iter≥5 切换,与雪暴起点重合,见 A6) |
|
||||||
|
| `TFLOOR` | `8000.` | 1787 (idx160) | 温度下限 | 待排查(见 §3.3) |
|
||||||
|
| `ICHANG` | `0` | 1766 (idx83) | 是否走 CHANGE(热启动) | 低 |
|
||||||
|
| `ND` | `50`(框架覆盖) | 默认70(idx67) | 深度网格点数 | 待排查(见 §3.3) |
|
||||||
|
|
||||||
|
> ⚠️ **校正**:初版把 DPSILG 行号写成 1786(实际是 POPZCH 区段的 '0.','0.',与 DPSILG 无关),且 §6.2 范围写 `1750-1806`(实际 PVALUE 结束于 1804,1806 是 BLNK DATA)。初版还漏列 CHMAXT、DPSILD、ND,把 ITEK 标为"失败相关性:低"(无依据,已改"待排查"),把 DPSILG 含义写"结构量"(实际主要约束辐射场+布居,温度有独立 DPSILT),把 ORELAX 含义写"结构量过松弛因子"(实际是布居)。
|
||||||
|
|
||||||
|
### 6.3 失败点失败阶段与 max_relc 量级分布(最后一次尝试)
|
||||||
|
|
||||||
|
```
|
||||||
|
首失败阶段: seed_nc=1033 nc=72
|
||||||
|
末步 max_relc: >1e6=982 1e4-1e6=25 1k-1e4=15 100-1k=12 10-100=14 (1,10]=5 <1=35
|
||||||
|
另有 17 点 itek_history 为空(首失败阶段无迭代轨迹)
|
||||||
|
迭代次数分布峰值: 10 iters(327) / 20 iters(143) / 6 iters(159) — 全部撞到 NITER 上限
|
||||||
|
atmosphere_has_nan: True=434 (39%) / False=671 (61%)
|
||||||
|
```
|
||||||
|
|
||||||
|
> ⚠️ **校正**:初版末步 max_relc 行漏列 (1,10] 区间 5 点 + 17 点空 history,导致加总 1083 与首失败阶段 1105 不闭合。补齐后可分类 1088 + 空 history 17 = 1105。
|
||||||
|
|
||||||
|
### 6.4 种子方向
|
||||||
|
|
||||||
|
```
|
||||||
|
poor(目标比种子贫,稳定方向) = 662
|
||||||
|
rich(目标比种子富,不稳定方向) = 373
|
||||||
|
same(同 CNO) = 70
|
||||||
|
```
|
||||||
|
|
||||||
|
seed_finder 的非对称距离(`seed_finder.rs:29-43`,RICH_PENALTY=4 vs POOR_PENALTY=1)使 60% 失败点拿到了贫方向种子。
|
||||||
|
|
||||||
|
> ⚠️ **校正**:初版称"seed_finder 已尽力……说明问题不在种子方向"措辞过强。更准确的表述:**种子选择不是唯一根因,但不能排除它是贡献因素之一**。理由:(1) "exact_family" 判定允许 ΔTeff < 5000K(半步容忍),sdB 网格步长 10000K 时 55k↔60k 相邻档互作种子辐射场差异仍可能很大,"物理极近"标签不能完全等同于 NLTE 状态空间近邻;(2) 443 个 rich/same 方向失败(seed_finder 自身的回测数据称富方向成功率仅 3-11%)可能部分源于种子方向。种子方向的影响需在 §五 A/B 测试中单独控制变量验证。
|
||||||
|
|
||||||
|
### 6.5 分析脚本
|
||||||
|
|
||||||
|
本次分析所用脚本临时存于 `/tmp/`(未入库):
|
||||||
|
- `analyze_salvage.py` / `analyze2.py` — salvage 池全量分类
|
||||||
|
- `dbfail.py` — DB 失败点分布
|
||||||
|
- `correlate.py` — DB 失败点 × salvage 最后一次尝试关联
|
||||||
|
- `seq.py` — 策略退化序列验证
|
||||||
|
- `cold_mode.py` — cold_run 失败模式
|
||||||
|
- `seed_check.py` — 种子距离/方向分析
|
||||||
|
- `success_region.py` — Teff×logg 成功率矩阵
|
||||||
|
- `parse_defaults.py` — TLUSTY PVALUE 默认值映射
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、后续行动
|
||||||
|
|
||||||
|
- [ ] 决定走方案 A / B / C 中的哪个
|
||||||
|
- [ ] 若方案 A:实现 `ChainStep.dpsilg` 字段 + YAML 配置(优先级 3)
|
||||||
|
- [ ] 若方案 A:实现 seed_nc NaN 拒绝逻辑(优先级 4)
|
||||||
|
- [ ] 30 点 A/B 测试验证
|
||||||
|
- [ ] A/B 测试中顺带排查 §3.3 替代根因(TFLOOR/ND/DDNU/原子数据/LTE 干净度)
|
||||||
|
- [ ] 若方案 A 救回率远低于 30%:转向排查 ITEK/Kantorovich(A6)或 §3.3 替代根因
|
||||||
|
- [ ] 若数据支持:实施优先级 1(自适应回退)
|
||||||
|
- [ ] 若需根治:改 `tlusty208.f` 源码 + 重编(优先级 2/5)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、修订审计记录(2026-08-11)
|
||||||
|
|
||||||
|
初版报告完成后,作者调用三个独立 subagent 分别审查「数据论断 / TLUSTY 源码 / 框架源码与结论合理性」,作者对最关键的两条(A3 作用域、NaN 计数)亲自复核确认属实,随后按审查意见修订全文。本节记录所有修订,保留可追溯性。
|
||||||
|
|
||||||
|
### 8.1 已修正的错误
|
||||||
|
|
||||||
|
| # | 位置 | 初版错误 | 修订内容 | 严重度 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 1 | §2.3 | `atmosphere_has_nan: true (1033/1105)` —— 实为 434/1105(1033 是 seed_nc 首失败点数,被误当 NaN 计数) | 改为 434/1105,并按 NaN 拆分两档失效特征 | 🔴严重 |
|
||||||
|
| 2 | §3.1 A3 | PSI 布局标签张冠李戴:"结构量被 ORELAX 松弛、布居不被松弛" —— 实际相反(ORELAX 松弛布居,结构量不被松弛) | 重写 A3,按 `tlusty208.f:3910-3936` 与 `795-797` 给出正确布局 | 🔴严重 |
|
||||||
|
| 3 | §四 优先级 2 | ORELAXR 改法是**替换**原 `I.GE.` 为 `I.LT.`,会移除布居松弛;且 ORELAXR 在源码不存在 | 改为**新增一行**并存;标注需扩 VARNAM/PVALUE 数组,非"改 2 行" | 🔴严重 |
|
||||||
|
| 4 | §3.1 A1 | 暗示"DPSILG=10 适用于所有结构量" —— 温度实际受更严的 DPSILT(1.25) 钳到 ±25% | 补 DPSILT/DPSILN/DPSILD 按变量限幅说明 | 🟡中 |
|
||||||
|
| 5 | §3.1 A2 | 行号 14710(实为 14700,14710 是铁线 LITEK 逻辑);"第 1 步任意大"过强 | 行号改 14700;措辞改为"首步无 STOP,但仍受 A1 限幅钳制" | 🟡中 |
|
||||||
|
| 6 | §3.1 | 遗漏 CHMAXT(默认 0.01)的过程反馈机制(`14711`、`22954-22958`) | 新增 A5 | 🟡中 |
|
||||||
|
| 7 | §3.1 / §6.2 | 遗漏 ITEK/Kantorovich(默认 4,iter≥5 切换);雪暴起点与之重合却标"失败相关性:低" | 新增 A6;ITEK 改标"待排查" | 🟡中 |
|
||||||
|
| 8 | §3.2 B2 | "雪崩后仍然把含 NaN 的 fort.7 喂给 nl"未区分 rc=0/rc≠0 | 区分两种 TLUSTY 退出路径 | 🟡中 |
|
||||||
|
| 9 | §3.2 B3 | "ORELAX=0.3 只对结构量"(依 A3 旧错误) | 改为"作用于布居" | 🟡中 |
|
||||||
|
| 10 | §3.2 B4 | "每次重跑参数完全一致(除了种子来源)"自相矛盾 | 区分同策略白跑 vs 跨策略换初值 | 🟡中 |
|
||||||
|
| 11 | §2.4 | "种子不是问题(关键反证)"过强;exact_family 允许 ΔTeff 5000K | 标题与结论改为"不是唯一根因,但不能排除贡献因素" | 🟡中 |
|
||||||
|
| 12 | §2.5 | 未排除成功/失败点的种子来源混淆变量 | 补充混淆变量说明 | 🟡中 |
|
||||||
|
| 13 | §6.2 | DPSILG 行号 1786(实为 1793,1786 是 POPZCH 区段);范围 1750-1806(实结束于 1804);漏列 CHMAXT/DPSILD/ND;ORELAX 含义写"结构量" | 全表重写,每项带 PVALUE 行号与 idx | 🟡中 |
|
||||||
|
| 14 | §6.3 | 末步 max_relc 漏列 (1,10] 5 点 + 17 点空 history,加总不闭合 | 补齐使加总闭合 | 🟢轻微 |
|
||||||
|
| 15 | §2.1 | "平均重试 7-9 次"(实均值 6.9) | 改为"均值 6.9 次" | 🟢轻微 |
|
||||||
|
| 16 | §五 | 救回率 30-50/70-85/90%+ 当作事实呈现(无数据支撑) | 标注"假设性估计",强调方案 A 实测值是诊断信号 | 🟢轻微 |
|
||||||
|
| 17 | §四 优先级 1 | "~300 行 Rust"低估 | 改为"400-600 行" | 🟢轻微 |
|
||||||
|
|
||||||
|
### 8.2 新增内容
|
||||||
|
|
||||||
|
- **§3.3 未排除的替代根因**:TFLOOR=8000K、ND=50、DDNU=50/CNU1=6、原子数据 bug、LTE 干净度、输入文件渲染 bug —— 6 条独立于 A1-A6 的数值配置假说
|
||||||
|
- **§8 本审计记录**
|
||||||
|
|
||||||
|
### 8.3 未改动(审查确认正确)的核心结论
|
||||||
|
|
||||||
|
- 失败点参数分布(高 Teff + 低 logg 集中):36 格 Teff×logg 矩阵全部精确 ✅
|
||||||
|
- 1105 全部走完 cold_run→seed_step:tasks 历史验证 ✅
|
||||||
|
- 首失败阶段 seed_nc=1033/nc=72 ✅
|
||||||
|
- 种子方向 poor=662/rich=373/same=70 + 全 exact_family ✅
|
||||||
|
- 两条 max_relc 雪崩轨迹样例(逐值)✅
|
||||||
|
- §2.5 危险区 cold/seed 成功拆分(4 格)✅
|
||||||
|
- 框架 B1(无链内重试)、B4(数值参数零扰动)✅
|
||||||
|
- **整体方向**:物理吸引域窄 + 框架无自适应 → 方向正确,仅论证细节有上述瑕疵
|
||||||
|
|
||||||
|
### 8.4 审查方法
|
||||||
|
|
||||||
|
三个 Explore subagent 并行独立审查,任务是**挑错找反例而非确认**:
|
||||||
|
- 数据核验:12 项独立 SQL/脚本复现(9 一致 / 2 轻微 / 1 错误)
|
||||||
|
- TLUSTY 源码:8 项行号/逻辑/默认值核对(数值 9/9 对 / 行号错位多 / A3 布局颠倒)
|
||||||
|
- 框架源码与结论:8 项源码引用 + 因果严谨性(B1/B3/B4 扎实 / 种子反证与救回率有漏洞)
|
||||||
|
|
||||||
|
作者对最致命两条(#1 A3、#2 NaN)用独立脚本/源码核对确认属实后,方才修订。
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
# TLUSTY 实验报告重跑验证(VERIFICATION)
|
||||||
|
|
||||||
|
**日期**:2026-08-11
|
||||||
|
**验证对象**:`docs/tlusty_directrun_experiment_2026_08_11.md` 的全部结论
|
||||||
|
**验证方法**:用生产当前二进制 `assets/tlusty_static`(8/11 新编版)重跑文档实验 + 决定性对照实验(同源码仅差 `-fno-toplevel-reorder`)
|
||||||
|
**产物**:`/tmp/cold_test/verify_rerun/`(重跑)+ `/tmp/cold_test/definitive2_{reorder,noreorder}/`(决定性对照)+ `test/20260811_tlusty_divergence/verify_{rerun,analyze}.*`
|
||||||
|
|
||||||
|
> ⚠️ **本报告经历了结论修正**。初版(基于 data/runtime 旧版 vs assets 新版的对比)错误地把 iter4+ 轨迹差异归因于 `-fno-toplevel-reorder` 编译选项。后经决定性对照实验(§三)证伪——见 §六的修正记录。本版是修正后的结论。
|
||||||
|
|
||||||
|
> ⚠️ **2026-08-12 追加更正**:本报告确认的"双禁(ITEK=0+IACC=0)抑制雪崩"是**观察层面成立、机制层面误解**——
|
||||||
|
> `ITEK=0` 通过 `tlusty208.f:1937` 令 nitzer=0 **冻结 populations**(首步修正变小是因为布居没在算,
|
||||||
|
> 不是加速被禁),`IACC=0` 被 `tlusty208.f:1928` clamp 回 7 是空操作。用正确方式(ITEK/IACC>NITER)复测
|
||||||
|
> 6 点 × 4 配置 0 次收敛,配置改动已回退。详见 `docs/cold_start_fix_2026_08_12.md`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、结论先行(修正后)
|
||||||
|
|
||||||
|
1. **`-fno-toplevel-reorder` 对 TLUSTY 数值行为零影响**——决定性对照实验证实:同源码、同编译器、仅差此一个 flag 的两版二进制,对同一输入的 `fort.9` 逐字节一致。这与 `docs/synspec_nan_fix_2026_08_11.md §10.4` 的结论完全一致。
|
||||||
|
|
||||||
|
2. **文档 `tlusty_directrun_experiment` 的核心机制结论成立**:KANT(iter≥5) + ACCEL2(iter≥7) 双加速机制绕过 DPSILG/DPSILT 钳制导致雪崩——这是源码逻辑,与编译选项无关,在新二进制下依然成立。
|
||||||
|
|
||||||
|
3. **文档的可信度锚点成立**:用新二进制(assets)重跑 seedprod,iter1 起的 `fort.9` 数据与生产 DB 记录在同一数量级(iter1 max_relc=7.0),测试方法可信。
|
||||||
|
|
||||||
|
4. **data/runtime 旧二进制 ≠ 当前源码编译版**——它使用了不同的源码状态(iter2 = 102 vs 当前源码编译版 115)。生产 DB 的失败记录是用这个旧版产生的,但其失败模式(雪崩)与当前源码编译版的失败模式在机制上一致。
|
||||||
|
|
||||||
|
5. **`-fno-toplevel-reorder` 的真实作用**是修复 **SYNSPEC** 的 Balmer 跃迁 NaN(见 `synspec_nan_fix_2026_08_11.md`),对 TLUSTY 只是"预防性同编译"(文档 §4.1),不改变 TLUSTY 行为。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、三个二进制的身份厘清(关键背景)
|
||||||
|
|
||||||
|
本次验证涉及三个 md5 不同的 TLUSTY 二进制:
|
||||||
|
|
||||||
|
| 二进制 | md5 | 大小 | 身份 | 来源 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `data/runtime/tlusty_static` | `14cd144b...` | 1.55MB | **旧生产版**(产生 DB 失败记录)| 7/31 编译,源码状态不明 |
|
||||||
|
| `~/tlusty_O3_backups_20260811/tlusty.exe.O3.bak` | `9b249e00...` | 2.00MB | 备份的原版(含调试回显行混叠)| 8/11 备份 |
|
||||||
|
| `assets/tlusty_static` | `77721c4c...` | 1.96MB | **当前生产版**(-O3 -fno-toplevel-reorder)| 8/11 编译 |
|
||||||
|
|
||||||
|
**编译命令**(`docs/deployment.md`):
|
||||||
|
```bash
|
||||||
|
gfortran -fno-automatic -fno-toplevel-reorder -mcmodel=medium -O3 -o assets/tlusty_static tlusty208.f
|
||||||
|
```
|
||||||
|
|
||||||
|
**生产用哪个**:`crates/common/src/embedded.rs:10` `include_bytes!("../../../assets/tlusty_static")` → **当前生产用 assets 版(77721c4c)**。
|
||||||
|
|
||||||
|
**时序**:
|
||||||
|
- 生产 DB 失败记录最晚 8/10 17:56 → 用的是旧版(data/runtime 系)
|
||||||
|
- `assets/` 在 8/11 11:09 才更新为新版
|
||||||
|
- 文档 `tlusty_directrun_experiment` 的实验数据用 `data/runtime` 旧版产生
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、决定性对照实验(证伪"编译选项导致分叉")
|
||||||
|
|
||||||
|
### 3.1 实验设计
|
||||||
|
|
||||||
|
用**当前源码** `tlusty208.f` 重编两版二进制,唯一差异是 `-fno-toplevel-reorder`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gfortran -fno-automatic -mcmodel=medium -O3 -o tlusty_O3_reorder tlusty208.f
|
||||||
|
gfortran -fno-automatic -fno-toplevel-reorder -mcmodel=medium -O3 -o tlusty_O3_noreorder tlusty208.f
|
||||||
|
```
|
||||||
|
|
||||||
|
| 二进制 | md5 | 对应 |
|
||||||
|
|---|---|---|
|
||||||
|
| `tlusty_O3_reorder` | `c8b6fa5b...` | 开 toplevel-reorder(旧默认语义)|
|
||||||
|
| `tlusty_O3_noreorder` | `77721c4c...` | 关 toplevel-reorder = **assets/tlusty_static 逐 md5 一致** |
|
||||||
|
|
||||||
|
### 3.2 实验结果(seedprod 输入)
|
||||||
|
|
||||||
|
对 seedprod(t60000_g5.0_he-2_c-4_n-4_o-4 用真实种子 t60000_g5.0_he-2_c-3_n-4_o-4.7):
|
||||||
|
|
||||||
|
| 二进制 | iter1 | iter2 | iter3 | 轨迹 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `tlusty_O3_reorder` | 7 | 115 | 0.203 | `[7, 115, 0.203]` 共3步* |
|
||||||
|
| `tlusty_O3_noreorder` | 7 | 115 | 0.203 | `[7, 115, 0.203]` 共3步* |
|
||||||
|
| 旧生产 data/runtime | 7.0 | 102 | 288 | `[7, 102, 288, 945, 1.12e8, ...]` 共14步雪崩(DB 记录)|
|
||||||
|
|
||||||
|
\* 3步是 timeout 截断,非自然结束;iter1 的 fort.9 **逐字节一致**(见下)
|
||||||
|
|
||||||
|
**两版干净二进制逐字节一致**——`fort.9` iter1 的每一行(50 个深度点 × TEMP/NE/POP/RAD/MAXIMUM)完全相同。这直接证实:**`-fno-toplevel-reorder` 不改变 TLUSTY 的数值行为**。
|
||||||
|
|
||||||
|
### 3.3 旧生产 data/runtime 为何不同
|
||||||
|
|
||||||
|
data/runtime(`[7, 102, 288, ...]`)与当前源码编译版(`[7, 115, 0.203]`)在 iter2 就不同(102 vs 115)。由于两版干净二进制 iter2 都得 115,差异**不可能**来自 toplevel-reorder。最可能的解释:**data/runtime 用了不同的源码状态**(tlusty208.f 在入库前的某个中间版本编译),或编译环境差异。
|
||||||
|
|
||||||
|
> 这与 `synspec_nan_fix_2026_08_11.md §10.4` 的发现一致——该文档也指出"原版备份二进制带有 READ LINE 调试行混叠",最终用"当前源码重编干净对照"才消除了混叠。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、可信度锚点验证(seed_nc,新二进制 assets)
|
||||||
|
|
||||||
|
用生产二进制 assets/tlusty_static(= tlusty_O3_noreorder)重跑 seed_nc 三变体。**注意**:以下轨迹受 timeout 截断影响,iter 数不等于自然结束;重点看趋势与前几步数值。
|
||||||
|
|
||||||
|
### 4.1 seedprod
|
||||||
|
|
||||||
|
| iter | 新二进制(assets) | 旧生产(DB) |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | 7.0 | 7.0 |
|
||||||
|
| 2 | 115 | 102 |
|
||||||
|
| 3 | 272 | 288 |
|
||||||
|
|
||||||
|
- iter1 完全一致(7.0);iter2-3 同数量级(百级)
|
||||||
|
- 数据/生产 DB 记录均在 iter1 起就表现为"高位修正",雪崩模式一致
|
||||||
|
|
||||||
|
### 4.2 seed_doubleoff / seed_doubleoff_dpsilg15
|
||||||
|
|
||||||
|
两变体 iter1-3 与文档(旧二进制记录)在同数量级吻合(如 seed_doubleoff iter1=1.28 vs 文档 1.3)。
|
||||||
|
|
||||||
|
### 4.3 锚点小结
|
||||||
|
|
||||||
|
新二进制复现了文档描述的定性模式(seedprod 高位起始、双禁抑制首步修正),**iter1 数值精确吻合、iter2-3 同数量级**。文档的可信度锚点("本机复现生产失败")在新二进制下成立。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、文档结论在新二进制下的有效性
|
||||||
|
|
||||||
|
| 文档结论 | 有效性 | 依据 |
|
||||||
|
|---|---|---|
|
||||||
|
| 根因 = KANT+ACCEL2 绕过限幅 | ✅ 成立 | 源码逻辑,与二进制无关 |
|
||||||
|
| 可信度锚点(复现生产失败) | ✅ 成立 | iter1 精确吻合,趋势一致 |
|
||||||
|
| 修复组合双禁(ITEK=0+IACC=0)抑制雪崩 | ✅ 成立 | seed_doubleoff 首步修正从 7 降到 1.28 |
|
||||||
|
| baseline 在 t60000_g5.0 雪崩 | ✅ 成立 | 冒烟测试 nc `[9.72,46.6,259,1950,4.85e6]` 精确复现 |
|
||||||
|
| 冷启动极端点本质困难 | ✅ 成立 | 多数点 nl 仍未达 max_relc<0.001 |
|
||||||
|
| 修复组合对所有点有效 | ⚠️ 文档自己已承认有条件性 | 个案需评估 |
|
||||||
|
|
||||||
|
**关键**:文档的全部定性结论(机制、锚点、修复方向)在新二进制下成立。定量轨迹(具体 iter 的 max_relc 数值)受 timeout 截断和源码状态差异影响有偏差,但不改变结论方向。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、初版报告的修正记录(透明披露)
|
||||||
|
|
||||||
|
### 6.1 初版的错误
|
||||||
|
|
||||||
|
初版验证报告(基于 data/runtime vs assets 的对比)得出两个错误结论:
|
||||||
|
|
||||||
|
| 初版错误结论 | 实际情况 |
|
||||||
|
|---|---|
|
||||||
|
| "iter4+ 系统性分叉由 `-fno-toplevel-reorder` 编译选项导致" | ❌ **证伪**:决定性对照实验显示两版干净二进制逐字节一致 |
|
||||||
|
| "新二进制往往更稳定,baseline 从雪崩变收敛" | ❌ **证伪**:差异来自 data/runtime 的不同源码状态,非编译选项 |
|
||||||
|
|
||||||
|
### 6.2 错误的根源
|
||||||
|
|
||||||
|
1. **混淆了三个变量**:初版把 data/runtime(旧生产)vs assets(新生产)的差异,错误归因到 `-fno-toplevel-reorder`。实际 data/runtime 与当前源码编译版(无论 reorder 与否)都不同——它用了不同的源码状态。
|
||||||
|
2. **未做决定性对照**:初版只对比了"旧二进制 vs 新二进制"(混叠了源码状态 + 编译选项两个变量),没有用"同源码仅差一个 flag"的干净对照。文档 `synspec_nan_fix §10.4` 已做过这种干净对照并得出正确结论,初版未参考。
|
||||||
|
3. **timeout 截断误判**:初版解析轨迹时,把"timeout 截断导致的 iter 数不同"误读为"轨迹分叉"。实际上同一二进制对同一输入,共同跑到的 iter 上逐字节一致。
|
||||||
|
|
||||||
|
### 6.3 修正的方法
|
||||||
|
|
||||||
|
`synspec_nan_fix_2026_08_11.md` 提供了关键线索:`-fno-toplevel-reorder` 是为修复 **SYNSPEC** NaN 而加,对 TLUSTY 是预防性同编译。基于此线索设计了 §三的决定性对照实验(同源码仅差一个 flag),一锤定音地证伪了初版的归因。
|
||||||
|
|
||||||
|
### 6.4 教训
|
||||||
|
|
||||||
|
- **归因前先控制变量**:对比两个二进制时,必须用"同源码仅差目标 flag"的干净对照,否则会把源码状态差异误归为编译选项。
|
||||||
|
- **先读已有文档**:`synspec_nan_fix §10` 已专门分析过"TLUSTY 与 toplevel-reorder 无关",初版未读这份文档就下结论。
|
||||||
|
- **timeout 截断要标注**:被 timeout 截断的轨迹不能直接对比 iter 数,只能对比共同跑到的 iter。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、后续建议
|
||||||
|
|
||||||
|
1. **文档 `tlusty_directrun_experiment` 无需修正**——其结论在新二进制下成立,且其 §10.4 的"TLUSTY 与 toplevel-reorder 无关"结论经本次决定性实验再次确认。
|
||||||
|
2. **生产可直接用 assets/tlusty_static**——它就是文档建议的 `-O3 -fno-toplevel-reorder` 版,TLUSTY 行为与旧版一致(同源码编译时),SYNSPEC NaN 已修复。
|
||||||
|
3. **data/runtime 旧二进制应废弃**——它的源码状态不明(iter2 偏离当前源码编译版),不应再用于任何对照实验。
|
||||||
|
4. **修复组合 YAML 落地**仍需 30 点 A/B 测试(文档 `tlusty_directrun_experiment §七` 的建议不变),但这是为了标定"哪些点 benefit",不是因为二进制换了。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、数据产物索引
|
||||||
|
|
||||||
|
```
|
||||||
|
/tmp/cold_test/definitive2_reorder/ # 干净 -O3(开 reorder)seedprod 结果
|
||||||
|
/tmp/cold_test/definitive2_noreorder/ # 干净 -O3 -fno-toplevel-reorder seedprod 结果(= assets)
|
||||||
|
/tmp/tlusty_O3_reorder # 干净 reorder 二进制 (md5 c8b6fa5b)
|
||||||
|
/tmp/tlusty_O3_noreorder # 干净 noreorder 二进制 (md5 77721c4c = assets)
|
||||||
|
/tmp/cold_test/verify_rerun/{cold,seed}/ # 初版重跑产物(28 cold + 3 seed)
|
||||||
|
test/20260811_tlusty_divergence/verify_{rerun.sh,analyze.py}
|
||||||
|
```
|
||||||
+8
-2
@@ -216,12 +216,16 @@ batch_deploy_profiles() {
|
|||||||
[ -z "$r_port" ] && r_port="${REMOTE_PORT}"
|
[ -z "$r_port" ] && r_port="${REMOTE_PORT}"
|
||||||
|
|
||||||
local sock="${SSH_CONTROL_DIR}/${r_user}@${r_ip}:${r_port}"
|
local sock="${SSH_CONTROL_DIR}/${r_user}@${r_ip}:${r_port}"
|
||||||
if ssh -p "${r_port}" -o ControlPath="${sock}" -O check "${r_user}@${r_ip}" >/dev/null 2>&1; then
|
# 跳板机支持:profile 的 REMOTE_PROXY_JUMP(仅建立主连接时需要;-O check/exit 只操作本地 mux socket)
|
||||||
|
local r_jump jump_opt=""
|
||||||
|
r_jump=$(grep -E '^REMOTE_PROXY_JUMP=' "$p" 2>/dev/null | cut -d'=' -f2- | tr -d '"' | tr -d "'" || true)
|
||||||
|
[ -n "${r_jump}" ] && jump_opt="-o ProxyJump=${r_jump}"
|
||||||
|
if ssh -p "${r_port}" -o ControlPath="${sock}" ${jump_opt} -O check "${r_user}@${r_ip}" >/dev/null 2>&1; then
|
||||||
echo " -> [${pre_idx}/${total}] ${r_user}@${r_ip}:${r_port} (连接已存在,复用)"
|
echo " -> [${pre_idx}/${total}] ${r_user}@${r_ip}:${r_port} (连接已存在,复用)"
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
echo " -> [${pre_idx}/${total}] 正在连接 ${r_user}@${r_ip}:${r_port} ..."
|
echo " -> [${pre_idx}/${total}] 正在连接 ${r_user}@${r_ip}:${r_port} ..."
|
||||||
ssh -p "${r_port}" -o ControlMaster=yes -o ControlPath="${sock}" -o ControlPersist=1800 -fN "${r_user}@${r_ip}" \
|
ssh -p "${r_port}" -o ControlMaster=yes -o ControlPath="${sock}" -o ControlPersist=1800 ${jump_opt} -fN "${r_user}@${r_ip}" \
|
||||||
&& echo -e " ${GREEN}[√] 连接已建立${NC}" \
|
&& echo -e " ${GREEN}[√] 连接已建立${NC}" \
|
||||||
|| echo -e " ${YELLOW}[!] 连接失败,将在部署该节点时重试${NC}"
|
|| echo -e " ${YELLOW}[!] 连接失败,将在部署该节点时重试${NC}"
|
||||||
done
|
done
|
||||||
@@ -699,6 +703,8 @@ setup_ssh_control() {
|
|||||||
local socket_key="${REMOTE_USER}@${REMOTE_IP}:${REMOTE_PORT}"
|
local socket_key="${REMOTE_USER}@${REMOTE_IP}:${REMOTE_PORT}"
|
||||||
SSH_CONTROL_PATH="${SSH_CONTROL_DIR}/${socket_key}"
|
SSH_CONTROL_PATH="${SSH_CONTROL_DIR}/${socket_key}"
|
||||||
SSH_OPTS="-o ControlMaster=auto -o ControlPath=${SSH_CONTROL_PATH} -o ControlPersist=1800"
|
SSH_OPTS="-o ControlMaster=auto -o ControlPath=${SSH_CONTROL_PATH} -o ControlPersist=1800"
|
||||||
|
# 非直连节点:profile 提供 REMOTE_PROXY_JUMP 时经跳板机转发(对齐 fetch_results.sh)
|
||||||
|
[ -n "${REMOTE_PROXY_JUMP:-}" ] && SSH_OPTS="${SSH_OPTS} -o ProxyJump=${REMOTE_PROXY_JUMP}"
|
||||||
mkdir -p "${SSH_CONTROL_DIR}"
|
mkdir -p "${SSH_CONTROL_DIR}"
|
||||||
|
|
||||||
# 若已有活跃的主连接 (如批量部署父进程已建立),直接复用,避免重复认证。
|
# 若已有活跃的主连接 (如批量部署父进程已建立),直接复用,避免重复认证。
|
||||||
|
|||||||
+57
-13
@@ -14,6 +14,7 @@
|
|||||||
# ./scripts/fetch_results.sh -d /backup/dcts-salvage # 覆盖备份根目录
|
# ./scripts/fetch_results.sh -d /backup/dcts-salvage # 覆盖备份根目录
|
||||||
# ./scripts/fetch_results.sh --with-work # 连带拉取 data/work 沙盒残留
|
# ./scripts/fetch_results.sh --with-work # 连带拉取 data/work 沙盒残留
|
||||||
# ./scripts/fetch_results.sh --include-local # 把本机节点也复制进备份树
|
# ./scripts/fetch_results.sh --include-local # 把本机节点也复制进备份树
|
||||||
|
# ./scripts/fetch_results.sh --force # 强制重拉:忽略本地已存在的同名目录,全部重新 scp 覆盖
|
||||||
#
|
#
|
||||||
# 传输策略(两类节点都走增量,重复运行只补差异、不重拷全量):
|
# 传输策略(两类节点都走增量,重复运行只补差异、不重拷全量):
|
||||||
# - 双方均可用 rsync → rsync 增量(断点续传、幂等,自动补新增/变更文件)
|
# - 双方均可用 rsync → rsync 增量(断点续传、幂等,自动补新增/变更文件)
|
||||||
@@ -47,6 +48,7 @@ ONLY_NODE=""
|
|||||||
WITH_WORK=false
|
WITH_WORK=false
|
||||||
INCLUDE_LOCAL=false
|
INCLUDE_LOCAL=false
|
||||||
INTERACTIVE=false
|
INTERACTIVE=false
|
||||||
|
FORCE_REPULL=false
|
||||||
HAD_ARGS=false
|
HAD_ARGS=false
|
||||||
SELECTED_PROFILES=()
|
SELECTED_PROFILES=()
|
||||||
|
|
||||||
@@ -61,6 +63,7 @@ while [[ $# -gt 0 ]]; do
|
|||||||
-i|--interactive) INTERACTIVE=true; shift ;;
|
-i|--interactive) INTERACTIVE=true; shift ;;
|
||||||
--with-work) WITH_WORK=true; shift ;;
|
--with-work) WITH_WORK=true; shift ;;
|
||||||
--include-local) INCLUDE_LOCAL=true; shift ;;
|
--include-local) INCLUDE_LOCAL=true; shift ;;
|
||||||
|
--force|-f) FORCE_REPULL=true; shift ;;
|
||||||
-h|--help)
|
-h|--help)
|
||||||
sed -n '2,32p' "$0"
|
sed -n '2,32p' "$0"
|
||||||
exit 0 ;;
|
exit 0 ;;
|
||||||
@@ -109,21 +112,25 @@ remote_list_dir() { # $1=user $2=ip $3=port $4=ssh_opts $5=远端绝对目录
|
|||||||
}
|
}
|
||||||
|
|
||||||
# scp 单个远端目录到本地,采用"先落地 .partial 再原子 mv"模式:
|
# scp 单个远端目录到本地,采用"先落地 .partial 再原子 mv"模式:
|
||||||
# 1. 目标目录 ${dest}/${d} 已存在 → 视为已同步,跳过(保持原增量语义)
|
# 1. 目标目录 ${dest}/${d} 已存在 → 视为已同步,跳过(保持原增量语义);
|
||||||
|
# 但 force=true 时忽略该判断,强制重拉(用于同名目录内容已变更的场景)
|
||||||
# 2. 否则 scp 远端目录到 ${dest}/${d}.partial
|
# 2. 否则 scp 远端目录到 ${dest}/${d}.partial
|
||||||
# 3. scp 成功后 mv 为 ${dest}/${d}(同 dest 文件系统,原子语义)
|
# 3. scp 成功后 mv 为 ${dest}/${d}(同 dest 文件系统,原子语义)
|
||||||
# 4. scp 失败 → 清理残留 .partial,return 非零,由调用方决定是否继续
|
# 4. scp 失败 → 清理残留 .partial,return 非零,由调用方决定是否继续
|
||||||
# 这样即便 scp 传输过程中断,也不会留下"伪完成"目录导致下次被跳过漏传。
|
# 这样即便 scp 传输过程中断,也不会留下"伪完成"目录导致下次被跳过漏传。
|
||||||
scp_dir() { # $1=u $2=ip $3=port $4=ssh_opts $5=远端根目录 $6=本地目标 $7=目录名
|
# 强制模式下只在 scp 成功后才清空旧目录再 mv,避免传输失败时丢失旧备份。
|
||||||
local u="$1" ip="$2" port="$3" opts="$4" rdir="$5" dest="$6" d="$7"
|
scp_dir() { # $1=u $2=ip $3=port $4=ssh_opts $5=远端根目录 $6=本地目标 $7=目录名 $8=force
|
||||||
|
local u="$1" ip="$2" port="$3" opts="$4" rdir="$5" dest="$6" d="$7" force="$8"
|
||||||
local final="${dest}/${d}"
|
local final="${dest}/${d}"
|
||||||
if [ -e "${final}" ]; then
|
if [ "${force}" != "true" ] && [ -e "${final}" ]; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
local tmp="${dest}/.${d}.partial"
|
local tmp="${dest}/.${d}.partial"
|
||||||
# 兜底清理可能的历史残留(上次中断留下的半成品)
|
# 兜底清理可能的历史残留(上次中断留下的半成品)
|
||||||
rm -rf "${tmp}"
|
rm -rf "${tmp}"
|
||||||
if scp -P "${port}" ${opts} -r "${u}@${ip}:${rdir}/${d}" "${tmp}"; then
|
if scp -P "${port}" ${opts} -r "${u}@${ip}:${rdir}/${d}" "${tmp}"; then
|
||||||
|
# 强制模式:先清旧目录再原子 mv(避免 mv 把新目录并入已存在的旧目录)
|
||||||
|
[ "${force}" = "true" ] && rm -rf "${final}"
|
||||||
mv "${tmp}" "${final}"
|
mv "${tmp}" "${final}"
|
||||||
else
|
else
|
||||||
echo -e " ${RED}[!]${NC} scp 失败: ${d},清理残留 ${d}.partial"
|
echo -e " ${RED}[!]${NC} scp 失败: ${d},清理残留 ${d}.partial"
|
||||||
@@ -133,16 +140,32 @@ scp_dir() { # $1=u $2=ip $3=port $4=ssh_opts $5=远端根目录 $6=本地目标
|
|||||||
}
|
}
|
||||||
|
|
||||||
# 传输一个目录树(rsync 优先,退化 scp 目录级增量)
|
# 传输一个目录树(rsync 优先,退化 scp 目录级增量)
|
||||||
sync_dir() { # $1=user $2=ip $3=port $4=远端绝对目录 $5=本地目标
|
# $6=可选跳板 ([user@]host[:port],来自 profile 的 REMOTE_PROXY_JUMP):
|
||||||
local u="$1" ip="$2" port="$3" rdir="$4" dest="$5"
|
# 目标节点不与本机直连、需经中转节点(如 dckj-01 → dckj-02/03)时启用 ProxyJump。
|
||||||
|
sync_dir() { # $1=user $2=ip $3=port $4=远端绝对目录 $5=本地目标 $6=jump
|
||||||
|
local u="$1" ip="$2" port="$3" rdir="$4" dest="$5" jump="${6:-}"
|
||||||
local ssh_opts="-o ControlMaster=auto -o ControlPath=${SSH_CONTROL_DIR}/cm-${u}@${ip}:${port} -o ControlPersist=1800"
|
local ssh_opts="-o ControlMaster=auto -o ControlPath=${SSH_CONTROL_DIR}/cm-${u}@${ip}:${port} -o ControlPersist=1800"
|
||||||
|
[ -n "${jump}" ] && ssh_opts="${ssh_opts} -o ProxyJump=${jump}"
|
||||||
|
local jump_note=""
|
||||||
|
[ -n "${jump}" ] && jump_note="(经跳板 ${jump})"
|
||||||
|
|
||||||
mkdir -p "${dest}"
|
mkdir -p "${dest}"
|
||||||
|
|
||||||
# 连通性探测(BatchMode=no 允许交互输密码)。
|
# 连通性与主连接建立(BatchMode=no 允许交互输密码)。
|
||||||
|
# 先显式建立 ControlMaster 主连接(-fN 后台保活):跳板机/目标机的密码集中在此
|
||||||
|
# 交互输入并各只输一次;成功后后续的探测/rsync/scp 全部复用 socket,不再要密码。
|
||||||
|
# 若主连接已存在(如刚同步过其他目录),-O check 直接复用。
|
||||||
|
if ! ssh -p "${port}" ${ssh_opts} -O check "${u}@${ip}" >/dev/null 2>&1; then
|
||||||
|
echo -e " ${BLUE}[→]${NC} 建立 SSH 主连接${jump_note}(需分别输入跳板机/目标机密码)..."
|
||||||
|
if ! ssh -p "${port}" ${ssh_opts} -fN "${u}@${ip}"; then
|
||||||
|
echo -e " ${RED}[!]${NC} SSH 主连接建立失败: ${u}@${ip}${jump_note}(密码错误或链路不通),跳过本节点"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# 探测命令用 `echo ok` 而非 `true`:Windows OpenSSH 默认 shell 是 cmd/PowerShell,
|
# 探测命令用 `echo ok` 而非 `true`:Windows OpenSSH 默认 shell 是 cmd/PowerShell,
|
||||||
# 没有 Unix 的 true,会导致"密码正确但仍判连接失败"。
|
# 没有 Unix 的 true,会导致"密码正确但仍判连接失败"。
|
||||||
if ! ssh -p "${port}" -o ConnectTimeout=8 ${ssh_opts} "${u}@${ip}" "echo ok" >/dev/null 2>&1; then
|
if ! ssh -p "${port}" ${ssh_opts} "${u}@${ip}" "echo ok" >/dev/null 2>&1; then
|
||||||
echo -e " ${RED}[!] 无法连接 ${u}@${ip},跳过本节点${NC}"
|
echo -e " ${RED}[!] 无法连接 ${u}@${ip},跳过本节点${NC}"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
@@ -193,7 +216,7 @@ sync_dir() { # $1=user $2=ip $3=port $4=远端绝对目录 $5=本地目标
|
|||||||
echo -e " ${YELLOW}[!]${NC} 跳过不符合白名单的远端目录名: ${d}"
|
echo -e " ${YELLOW}[!]${NC} 跳过不符合白名单的远端目录名: ${d}"
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
if ! grep -Fxq -- "$d" <<<"${existing}"; then
|
if [ "${FORCE_REPULL}" = "true" ] || ! grep -Fxq -- "$d" <<<"${existing}"; then
|
||||||
missing="${missing}${d}"$'\n'
|
missing="${missing}${d}"$'\n'
|
||||||
fi
|
fi
|
||||||
done <<<"${remote_dirs}"
|
done <<<"${remote_dirs}"
|
||||||
@@ -201,9 +224,17 @@ sync_dir() { # $1=user $2=ip $3=port $4=远端绝对目录 $5=本地目标
|
|||||||
local total new
|
local total new
|
||||||
total=$(printf '%s\n' "${remote_dirs}" | grep -c .)
|
total=$(printf '%s\n' "${remote_dirs}" | grep -c .)
|
||||||
new=$(printf '%s' "${missing}" | grep -c .)
|
new=$(printf '%s' "${missing}" | grep -c .)
|
||||||
|
if [ "${FORCE_REPULL}" = "true" ]; then
|
||||||
|
echo -e " ${YELLOW}[→]${NC} scp 强制重拉: ${u}@${ip}:${rdir}/ 共 ${total} 个,全部重拉 ${new} 个 → ${dest}/"
|
||||||
|
else
|
||||||
echo -e " ${YELLOW}[→]${NC} scp 目录级增量: ${u}@${ip}:${rdir}/ 共 ${total} 个,缺失 ${new} 个 → ${dest}/"
|
echo -e " ${YELLOW}[→]${NC} scp 目录级增量: ${u}@${ip}:${rdir}/ 共 ${total} 个,缺失 ${new} 个 → ${dest}/"
|
||||||
|
fi
|
||||||
if [ "${new}" -eq 0 ]; then
|
if [ "${new}" -eq 0 ]; then
|
||||||
|
if [ "${FORCE_REPULL}" = "true" ]; then
|
||||||
|
echo -e " ${GREEN}[√]${NC} 强制重拉完成,无远端目录"
|
||||||
|
else
|
||||||
echo -e " ${GREEN}[√]${NC} 本地已是最新,无新增目录"
|
echo -e " ${GREEN}[√]${NC} 本地已是最新,无新增目录"
|
||||||
|
fi
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
# scp 原子写入:先落地到 .partial 临时目录,成功后同文件系统原子 mv 为最终目录名。
|
# scp 原子写入:先落地到 .partial 临时目录,成功后同文件系统原子 mv 为最终目录名。
|
||||||
@@ -214,15 +245,19 @@ sync_dir() { # $1=user $2=ip $3=port $4=远端绝对目录 $5=本地目标
|
|||||||
rm -rf "${dest}"/.*.partial 2>/dev/null || true
|
rm -rf "${dest}"/.*.partial 2>/dev/null || true
|
||||||
while IFS= read -r d; do
|
while IFS= read -r d; do
|
||||||
[ -n "$d" ] || continue
|
[ -n "$d" ] || continue
|
||||||
|
if [ "${FORCE_REPULL}" = "true" ]; then
|
||||||
|
echo -e " ${YELLOW}[→]${NC} scp 强制重拉: ${d}"
|
||||||
|
else
|
||||||
echo -e " ${YELLOW}[→]${NC} scp 新增: ${d}"
|
echo -e " ${YELLOW}[→]${NC} scp 新增: ${d}"
|
||||||
scp_dir "${u}" "${ip}" "${port}" "${ssh_opts}" "${rdir}" "${dest}" "$d"
|
fi
|
||||||
|
scp_dir "${u}" "${ip}" "${port}" "${ssh_opts}" "${rdir}" "${dest}" "$d" "${FORCE_REPULL}"
|
||||||
done <<<"${missing}"
|
done <<<"${missing}"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# 备份单个节点
|
# 备份单个节点
|
||||||
backup_node() { # $1 = profile 文件
|
backup_node() { # $1 = profile 文件
|
||||||
local p="$1" node_id role env_mode u ip port dir
|
local p="$1" node_id role env_mode u ip port dir jump
|
||||||
node_id=$(read_profile_var "$p" DCTS_NODE_ID)
|
node_id=$(read_profile_var "$p" DCTS_NODE_ID)
|
||||||
role=$(read_profile_var "$p" DEPLOY_ROLE node)
|
role=$(read_profile_var "$p" DEPLOY_ROLE node)
|
||||||
env_mode=$(read_profile_var "$p" DEPLOY_ENV "")
|
env_mode=$(read_profile_var "$p" DEPLOY_ENV "")
|
||||||
@@ -230,6 +265,7 @@ backup_node() { # $1 = profile 文件
|
|||||||
ip=$(read_profile_var "$p" REMOTE_IP "")
|
ip=$(read_profile_var "$p" REMOTE_IP "")
|
||||||
port=$(read_profile_var "$p" REMOTE_PORT 22)
|
port=$(read_profile_var "$p" REMOTE_PORT 22)
|
||||||
dir=$(read_profile_var "$p" REMOTE_DIR "")
|
dir=$(read_profile_var "$p" REMOTE_DIR "")
|
||||||
|
jump=$(read_profile_var "$p" REMOTE_PROXY_JUMP "")
|
||||||
|
|
||||||
# 跳过 server 角色(server 的 result 不是计算产物,且 seeds 另行备份)
|
# 跳过 server 角色(server 的 result 不是计算产物,且 seeds 另行备份)
|
||||||
if [ "${role}" = "server" ]; then
|
if [ "${role}" = "server" ]; then
|
||||||
@@ -239,6 +275,8 @@ backup_node() { # $1 = profile 文件
|
|||||||
# 本地节点:result 就在本机 ./data/result,默认不重复备份
|
# 本地节点:result 就在本机 ./data/result,默认不重复备份
|
||||||
if [ "${env_mode}" = "local" ]; then
|
if [ "${env_mode}" = "local" ]; then
|
||||||
if [ "${INCLUDE_LOCAL}" = "true" ]; then
|
if [ "${INCLUDE_LOCAL}" = "true" ]; then
|
||||||
|
# 本机节点 ID 优先取根目录 .env(与节点注册、看板展示的 DCTS_NODE_ID 一致)
|
||||||
|
[ -n "${node_id}" ] || node_id=$(grep -E "^DCTS_NODE_ID=" .env 2>/dev/null | head -n1 | cut -d'=' -f2- | tr -d '"' | tr -d "'")
|
||||||
[ -n "${node_id}" ] || node_id="node-local"
|
[ -n "${node_id}" ] || node_id="node-local"
|
||||||
else
|
else
|
||||||
echo -e "${YELLOW}== ${p##*/}: 本地节点(${node_id:-localhost}),result 已在 ./data/result,跳过(--include-local 可纳入备份树)${NC}"
|
echo -e "${YELLOW}== ${p##*/}: 本地节点(${node_id:-localhost}),result 已在 ./data/result,跳过(--include-local 可纳入备份树)${NC}"
|
||||||
@@ -272,7 +310,7 @@ backup_node() { # $1 = profile 文件
|
|||||||
cp -a ./data/result/. "${dest_rc}/"
|
cp -a ./data/result/. "${dest_rc}/"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
sync_dir "${u}" "${ip}" "${port}" "${dir}/data/result" "${dest_rc}" || return 1
|
sync_dir "${u}" "${ip}" "${port}" "${dir}/data/result" "${dest_rc}" "${jump}" || return 1
|
||||||
fi
|
fi
|
||||||
echo -e " ${GREEN}[√]${NC} result 已同步,共 $(ls -1 "${dest_rc}" 2>/dev/null | grep -c . || true) 个网格点子目录"
|
echo -e " ${GREEN}[√]${NC} result 已同步,共 $(ls -1 "${dest_rc}" 2>/dev/null | grep -c . || true) 个网格点子目录"
|
||||||
|
|
||||||
@@ -288,7 +326,7 @@ backup_node() { # $1 = profile 文件
|
|||||||
cp -a ./data/work/. "${dest_wk}/" 2>/dev/null || true
|
cp -a ./data/work/. "${dest_wk}/" 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
sync_dir "${u}" "${ip}" "${port}" "${dir}/data/work" "${dest_wk}" || true
|
sync_dir "${u}" "${ip}" "${port}" "${dir}/data/work" "${dest_wk}" "${jump}" || true
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -365,6 +403,12 @@ interactive_select() {
|
|||||||
[[ "${WK_CHOICE,,}" == "y" || "${WK_CHOICE,,}" == "yes" ]] && WITH_WORK=true
|
[[ "${WK_CHOICE,,}" == "y" || "${WK_CHOICE,,}" == "yes" ]] && WITH_WORK=true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Windows(scp)节点是否强制重拉:本地即使已有同名目录也全部重新下载覆盖
|
||||||
|
if [ "${FORCE_REPULL}" != "true" ]; then
|
||||||
|
read -r -p "是否强制重拉(忽略本地已有同名目录,全部重新 scp 覆盖,仅对 Windows/scp 节点生效)? [y/N]: " FORCE_CHOICE
|
||||||
|
[[ "${FORCE_CHOICE,,}" == "y" || "${FORCE_CHOICE,,}" == "yes" ]] && FORCE_REPULL=true
|
||||||
|
fi
|
||||||
|
|
||||||
# 用户显式勾选本机节点 → 自动纳入备份树(否则 backup_node 会跳过本机)
|
# 用户显式勾选本机节点 → 自动纳入备份树(否则 backup_node 会跳过本机)
|
||||||
local p2 env2
|
local p2 env2
|
||||||
for p2 in "${SELECTED_PROFILES[@]}"; do
|
for p2 in "${SELECTED_PROFILES[@]}"; do
|
||||||
|
|||||||
@@ -19,13 +19,13 @@
|
|||||||
# logn: [-4]
|
# logn: [-4]
|
||||||
# logo: [-4]
|
# logo: [-4]
|
||||||
grid:
|
grid:
|
||||||
teff: [20000, 30000, 40000, 50000, 60000]
|
teff: [20000, 25000, 30000, 35000, 40000, 45000, 50000, 55000, 60000]
|
||||||
logg: [5.0, 5.5, 6.0, 6.5]
|
logg: [5.0, 5.5, 6.0, 6.5]
|
||||||
loghe: [-4, -2, 0, 2]
|
loghe: [-4, -2, 0, 2]
|
||||||
logc: [-4, -3, -2, -1]
|
logc: [-4, -3, -2, -1]
|
||||||
logn: [-4, -3, -2, -1]
|
logn: [-4, -3, -2, -1]
|
||||||
logo: [-4, -3, -2, -1]
|
logo: [-4, -3, -2, -1]
|
||||||
# 共 4*2*2*3*3*3 = 432 个点
|
# 共 9*4*4*4*4*4 = 9216 个点
|
||||||
|
|
||||||
# ---- TLUSTY 输入文件全局参数(.5 + nst 的非阶段差异部分)----
|
# ---- TLUSTY 输入文件全局参数(.5 + nst 的非阶段差异部分)----
|
||||||
# 与 .5 文件 + nst 文件全字段一一对应(见 config.rs 的 Dot5Input / NstInput)。
|
# 与 .5 文件 + nst 文件全字段一一对应(见 config.rs 的 Dot5Input / NstInput)。
|
||||||
|
|||||||
Reference in New Issue
Block a user