Compare commits
2
Commits
43b82b1ae2
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2700031ce | ||
|
|
b058e66722 |
@@ -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.
@@ -1147,6 +1147,15 @@ pub struct ChainStep {
|
|||||||
pub idlte: Option<i32>,
|
pub idlte: Option<i32>,
|
||||||
pub iacc: Option<i32>,
|
pub iacc: Option<i32>,
|
||||||
pub orelax: Option<f64>,
|
pub orelax: Option<f64>,
|
||||||
|
/// DPSILG(λ 算子欠松弛上限,默认不生效)。难收敛角落(如 20kK He 富大气的
|
||||||
|
/// He I/II 电离前沿布居极限环)用 3.0 抑制——2026-08-18 实测,见
|
||||||
|
/// docs/failed81_cno_seed_popzer_dpsilg_2026_08_18.md。
|
||||||
|
#[serde(default)]
|
||||||
|
pub dpsilg: Option<f64>,
|
||||||
|
/// POPZER(微布居置零阈值)。设置时同时写 POPZR2/RADZER=同值 + NITZER=1,
|
||||||
|
/// 把可忽略微布居从方程中剔除(官方标准输入恒设 1E-20;难收敛角落用 1E-10)。
|
||||||
|
#[serde(default)]
|
||||||
|
pub popzer: Option<f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_false_str() -> String {
|
fn default_false_str() -> String {
|
||||||
@@ -1991,6 +2000,8 @@ mod tests {
|
|||||||
idlte: None,
|
idlte: None,
|
||||||
iacc: None,
|
iacc: None,
|
||||||
orelax: None,
|
orelax: None,
|
||||||
|
dpsilg: None,
|
||||||
|
popzer: None,
|
||||||
};
|
};
|
||||||
let input5 = make_input5(&grid, &chain, &yaml_loaded);
|
let input5 = make_input5(&grid, &chain, &yaml_loaded);
|
||||||
// 兜底后应含完整 ions 能级数据(而非只有终止行)
|
// 兜底后应含完整 ions 能级数据(而非只有终止行)
|
||||||
|
|||||||
@@ -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
|
||||||
))
|
))
|
||||||
},
|
},
|
||||||
@@ -709,7 +719,14 @@ pub fn check_convergence_trace(
|
|||||||
.windows(2)
|
.windows(2)
|
||||||
.all(|w| w[0] >= w[1] * 0.5); // 容忍 Ng 加速的局部反弹(×2 内)
|
.all(|w| w[0] >= w[1] * 0.5); // 容忍 Ng 加速的局部反弹(×2 内)
|
||||||
|
|
||||||
let valid = ratio >= min_ratio;
|
// 热启动豁免(2026-08-18 修复):首拍 max_relc < 1 说明种子已接近收敛解,
|
||||||
|
// 首末比在此情形下数学上不可能达到 1e3 量级(首拍被钳在低位),判据失效。
|
||||||
|
// 误杀实例:t55000_g5.0_he-4_c-3_n-4_o-1 的 nl_ladder 阶段(首 0.038→末 6e-4,
|
||||||
|
// 四项物理硬门全过)被否决为未收敛。真伪收敛的最终裁决交给五重物理硬门槛。
|
||||||
|
const HOT_START_FIRST_MAX_RELC: f64 = 1.0;
|
||||||
|
let hot_start_exempt = first < HOT_START_FIRST_MAX_RELC;
|
||||||
|
|
||||||
|
let valid = ratio >= min_ratio || hot_start_exempt;
|
||||||
Some(ConvergenceTraceCheckResult {
|
Some(ConvergenceTraceCheckResult {
|
||||||
valid,
|
valid,
|
||||||
first_max_relc: first,
|
first_max_relc: first,
|
||||||
@@ -719,7 +736,13 @@ pub fn check_convergence_trace(
|
|||||||
n_iters: itek.len(),
|
n_iters: itek.len(),
|
||||||
min_ratio,
|
min_ratio,
|
||||||
error: if valid {
|
error: if valid {
|
||||||
None
|
if hot_start_exempt && ratio < min_ratio {
|
||||||
|
Some(format!(
|
||||||
|
"热启动豁免:首拍 max_relc {first:.1e} < {HOT_START_FIRST_MAX_RELC},首末比判据不适用"
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Some(format!(
|
Some(format!(
|
||||||
"假收敛排查失败:首末 max_relc 比 {:.1e} < {:.0e}(可能 Ng 加速伪收敛)",
|
"假收敛排查失败:首末 max_relc 比 {:.1e} < {:.0e}(可能 Ng 加速伪收敛)",
|
||||||
@@ -1400,26 +1423,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 → 大气收敛失败(辐射转移求解发散),应判失败(而非跳过)。
|
||||||
@@ -1475,6 +1522,35 @@ mod tests {
|
|||||||
assert!(res.error.is_some());
|
assert!(res.error.is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_convergence_trace_hot_start_exempt() {
|
||||||
|
// 热启动豁免(2026-08-18):首拍 < 1(种子已接近解)时首末比数学上达不到 1e3,
|
||||||
|
// 旧判据误杀(实例 t55000_g5.0_he-4_c-3_n-4_o-1 nl_ladder:0.038→6e-4,物理硬门全过)。
|
||||||
|
let itek: Vec<IterCheck> = (0..7)
|
||||||
|
.map(|i| IterCheck {
|
||||||
|
iter: i + 1,
|
||||||
|
max_relc: 0.038 / (1.0 + i as f64),
|
||||||
|
n_depths: 50,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let res = check_convergence_trace(&itek, 1000.0).expect("≥3拍应判定");
|
||||||
|
assert!(res.valid, "热启动首拍 <1 应回退到物理硬门槛裁决,不应判假收敛");
|
||||||
|
assert!(res.ratio < 1000.0);
|
||||||
|
assert!(res.error.is_some(), "豁免时应带豁免说明");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_convergence_trace_cold_start_still_gated() {
|
||||||
|
// 冷启动(首拍 >= 1)不受豁免影响:比值不足仍判假收敛嫌疑。
|
||||||
|
let itek: Vec<IterCheck> = vec![
|
||||||
|
IterCheck { iter: 1, max_relc: 5.0, n_depths: 50 },
|
||||||
|
IterCheck { iter: 2, max_relc: 2.0, n_depths: 50 },
|
||||||
|
IterCheck { iter: 3, max_relc: 0.05, n_depths: 50 },
|
||||||
|
];
|
||||||
|
let res = check_convergence_trace(&itek, 1000.0).expect("≥3拍应判定");
|
||||||
|
assert!(!res.valid, "冷启动首拍 ≥1 且比值 <1000 仍应失败");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_convergence_trace_too_short() {
|
fn test_convergence_trace_too_short() {
|
||||||
// 2拍:无法判定 → None
|
// 2拍:无法判定 → None
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -232,6 +232,8 @@ mod tests {
|
|||||||
idlte: None,
|
idlte: None,
|
||||||
iacc: None,
|
iacc: None,
|
||||||
orelax: None,
|
orelax: None,
|
||||||
|
dpsilg: None,
|
||||||
|
popzer: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -223,6 +223,27 @@ impl GridPointParams {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 从规范网格点名解析参数:`t60000_g5.5_he-2_c-4_n-4_o-4` → GridPointParams。
|
||||||
|
/// 接受可选的 `_ladder` 等后缀(ladder 合成种子名)。解析失败返回 None。
|
||||||
|
/// 用途:node 端从 seed_point_name 取种子参数(ladder 步进规划)、
|
||||||
|
/// server 端从上传文件名取中间种子参数。
|
||||||
|
pub fn parse_point_name(name: &str) -> Option<Self> {
|
||||||
|
let re = regex::Regex::new(
|
||||||
|
r"^t(\d+(?:\.\d+)?)_g(-?\d+(?:\.\d+)?)_he(-?\d+(?:\.\d+)?)_c(-?\d+(?:\.\d+)?)_n(-?\d+(?:\.\d+)?)_o(-?\d+(?:\.\d+)?)(?:_.*)?$",
|
||||||
|
)
|
||||||
|
.ok()?;
|
||||||
|
let caps = re.captures(name)?;
|
||||||
|
let v = |i: usize| -> Option<f64> { caps.get(i)?.as_str().parse::<f64>().ok() };
|
||||||
|
Some(GridPointParams {
|
||||||
|
teff: GridAxisValue::from_value(v(1)?),
|
||||||
|
logg: GridAxisValue::from_value(v(2)?),
|
||||||
|
loghe: GridAxisValue::from_value(v(3)?),
|
||||||
|
logc: GridAxisValue::from_value(v(4)?),
|
||||||
|
logn: GridAxisValue::from_value(v(5)?),
|
||||||
|
logo: GridAxisValue::from_value(v(6)?),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// CNO 对数丰度之和 (`logc + logn + logo`)。
|
/// CNO 对数丰度之和 (`logc + logn + logo`)。
|
||||||
///
|
///
|
||||||
/// 注:此数值专门用于网格调度中的 Wave 难度分级与保序分组(对数和越小代表重元素丰度越低,
|
/// 注:此数值专门用于网格调度中的 Wave 难度分级与保序分组(对数和越小代表重元素丰度越低,
|
||||||
@@ -469,6 +490,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 +532,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 +711,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,
|
||||||
@@ -825,9 +853,33 @@ pub struct ModelSummary {
|
|||||||
/// `.bfac` b 因子合理性校验结果。None = 未做校验。
|
/// `.bfac` b 因子合理性校验结果。None = 未做校验。
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub bfac_check: Option<BfacCheckResult>,
|
pub bfac_check: Option<BfacCheckResult>,
|
||||||
|
/// ladder 步进链的中间收敛模型(node→server 种子持久化清单)。空 = 未触发 ladder
|
||||||
|
/// 或无合格中间步。中间模型已过收敛 + NaN 否决,可安全入种子库供相邻失败点复用。
|
||||||
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
|
pub ladder_seeds: Vec<LadderSeedInfo>,
|
||||||
pub note: Option<String>,
|
pub note: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// ladder 步进链中间种子信息。
|
||||||
|
///
|
||||||
|
/// `point_name` 是合成种子名(中间参数 model_name + `_ladder` 后缀),与任何真实
|
||||||
|
/// 网格点名不冲突;server 端按其中的参数落 seeds 表(bucket 查找按 teff/logg/loghe
|
||||||
|
/// 数值列匹配,天然覆盖中间参数点)。
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct LadderSeedInfo {
|
||||||
|
/// 产生该模型的阶段标签(如 `ladder_g6.25`)。
|
||||||
|
pub label: String,
|
||||||
|
/// 合成种子名(server 端 seeds.point_name / 磁盘文件名,不含 .7 后缀)。
|
||||||
|
pub point_name: String,
|
||||||
|
/// 中间步大气参数(teff/logg 为中间值,丰度同目标点)。
|
||||||
|
pub teff: f64,
|
||||||
|
pub logg: f64,
|
||||||
|
pub loghe: f64,
|
||||||
|
pub logc: f64,
|
||||||
|
pub logn: f64,
|
||||||
|
pub logo: f64,
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -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();
|
||||||
@@ -82,10 +82,24 @@ pub fn generate_nst_content(chain: &ChainStep, global: &TlustyInput) -> String {
|
|||||||
line2.push(format!("ICHANG={}", ichang));
|
line2.push(format!("ICHANG={}", ichang));
|
||||||
written_keys.insert("ICHANG".to_string());
|
written_keys.insert("ICHANG".to_string());
|
||||||
}
|
}
|
||||||
|
if let Some(dpsilg) = chain.dpsilg {
|
||||||
|
line2.push(format!("DPSILG={}", dpsilg));
|
||||||
|
written_keys.insert("DPSILG".to_string());
|
||||||
|
}
|
||||||
|
if let Some(popzer) = chain.popzer {
|
||||||
|
// POPZER 联动组:官方标准输入恒 POPZER=POPZR2=RADZER + NITZER=1
|
||||||
|
// (tests/tlusty/*/.6 回显),缺一则置零机制不完整。
|
||||||
|
line2.push(format!("POPZER={:.E}", popzer));
|
||||||
|
line2.push(format!("POPZR2={:.E}", popzer));
|
||||||
|
line2.push(format!("RADZER={:.E}", popzer));
|
||||||
|
line2.push("NITZER=1".to_string());
|
||||||
|
for k in ["POPZER", "POPZR2", "RADZER", "NITZER"] {
|
||||||
|
written_keys.insert(k.to_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 +177,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 +218,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)]
|
||||||
@@ -195,6 +239,8 @@ mod tests {
|
|||||||
idlte: None,
|
idlte: None,
|
||||||
iacc: None,
|
iacc: None,
|
||||||
orelax: None,
|
orelax: None,
|
||||||
|
dpsilg: None,
|
||||||
|
popzer: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,6 +306,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"
|
||||||
@@ -282,6 +348,35 @@ mod tests {
|
|||||||
assert!(!content.contains("FRLMIN"), "FRLMIN=0 不应写出");
|
assert!(!content.contains("FRLMIN"), "FRLMIN=0 不应写出");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// DPSILG/POPZER 稳定化旋钮(2026-08-18,seed_step_stab 策略):
|
||||||
|
/// dpsilg → DPSILG=<val>;popzer → POPZER/POPZR2/RADZER 三键同值 + NITZER=1
|
||||||
|
/// (官方标准输入联动组),且行宽仍受 75 字符保护(自动换行)。
|
||||||
|
#[test]
|
||||||
|
fn test_nst_dpsilg_popzer_stabilization() {
|
||||||
|
let mut chain = chain_nc();
|
||||||
|
chain.dpsilg = Some(3.0);
|
||||||
|
chain.popzer = Some(1e-10);
|
||||||
|
let content = generate_nst_content(&chain, &TlustyInput::default());
|
||||||
|
assert!(content.contains("DPSILG=3"), "DPSILG 应写出: {}", content);
|
||||||
|
assert!(content.contains("POPZER=1E-10"), "POPZER 应以 Fortran E 格式写出");
|
||||||
|
assert!(content.contains("POPZR2=1E-10"));
|
||||||
|
assert!(content.contains("RADZER=1E-10"));
|
||||||
|
assert!(content.contains("NITZER=1"), "POPZER 联动 NITZER=1");
|
||||||
|
for (i, line) in content.lines().enumerate() {
|
||||||
|
assert!(
|
||||||
|
line.chars().count() <= 75,
|
||||||
|
"nst 第 {} 行超宽({} 字符): {}",
|
||||||
|
i + 1,
|
||||||
|
line.chars().count(),
|
||||||
|
line
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// 默认(None)不写出,保持字节级兼容
|
||||||
|
let plain = generate_nst_content(&chain_nc(), &TlustyInput::default());
|
||||||
|
assert!(!plain.contains("DPSILG"));
|
||||||
|
assert!(!plain.contains("POPZER"));
|
||||||
|
}
|
||||||
|
|
||||||
/// fmt_real 边界:-0.0 归一为 "0."(审查 Nit-1)。
|
/// fmt_real 边界:-0.0 归一为 "0."(审查 Nit-1)。
|
||||||
#[test]
|
#[test]
|
||||||
fn test_fmt_real_negative_zero() {
|
fn test_fmt_real_negative_zero() {
|
||||||
|
|||||||
+1628
-190
File diff suppressed because it is too large
Load Diff
@@ -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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ mod tests {
|
|||||||
temp_check: None,
|
temp_check: None,
|
||||||
emflux_check: None,
|
emflux_check: None,
|
||||||
bfac_check: None,
|
bfac_check: None,
|
||||||
|
ladder_seeds: Vec::new(),
|
||||||
note: None,
|
note: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+131
-8
@@ -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,
|
||||||
@@ -16,7 +20,7 @@ pub async fn execute_task(
|
|||||||
result_dir: &Path,
|
result_dir: &Path,
|
||||||
task: &TaskSpec,
|
task: &TaskSpec,
|
||||||
shutdown: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
|
shutdown: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
|
||||||
) -> Result<(ModelSummary, Option<Vec<u8>>)> {
|
) -> Result<(ModelSummary, Option<Vec<u8>>, Vec<(String, Vec<u8>)>)> {
|
||||||
info!(
|
info!(
|
||||||
"开始执行计算任务 {} (网格点: {})",
|
"开始执行计算任务 {} (网格点: {})",
|
||||||
task.task_id, task.point_name
|
task.task_id, task.point_name
|
||||||
@@ -64,7 +68,11 @@ pub async fn execute_task(
|
|||||||
// 全新 slot 内不可能已有目标大气;重试任务的 slot 亦全新(task_id 唯一)。
|
// 全新 slot 内不可能已有目标大气;重试任务的 slot 亦全新(task_id 唯一)。
|
||||||
let tlusty_enabled = task.tlusty_config.enabled;
|
let tlusty_enabled = task.tlusty_config.enabled;
|
||||||
let current_strategy = task.tlusty_config.current_strategy("cold_run");
|
let current_strategy = task.tlusty_config.current_strategy("cold_run");
|
||||||
let needs_seed_download = (tlusty_enabled && current_strategy == "seed_step")
|
// seed_step_stab(2026-08-18 稳定化种子步进)同样是种子热启动链,必须下载种子;
|
||||||
|
// 2026-08-19 生产事故:漏列该策略导致种子未下载,seed_nc 无 fort.8 直接
|
||||||
|
// EOF 崩溃(rc=2),78 个任务全部假失败。
|
||||||
|
let needs_seed_download = (tlusty_enabled
|
||||||
|
&& matches!(current_strategy, "seed_step" | "seed_step_stab"))
|
||||||
|| (!tlusty_enabled && task.synspec_config.enabled);
|
|| (!tlusty_enabled && task.synspec_config.enabled);
|
||||||
|
|
||||||
if needs_seed_download {
|
if needs_seed_download {
|
||||||
@@ -162,7 +170,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/
|
||||||
@@ -177,6 +227,7 @@ pub async fn execute_task(
|
|||||||
&task.tlusty_chain_params,
|
&task.tlusty_chain_params,
|
||||||
&task.seed_chain_params,
|
&task.seed_chain_params,
|
||||||
&task.task_id.to_string(),
|
&task.task_id.to_string(),
|
||||||
|
task.params.teff.value(),
|
||||||
);
|
);
|
||||||
// TLUSTY 输入文件全局参数(NFREAD/ions 表/nst extra_keys 等)。
|
// TLUSTY 输入文件全局参数(NFREAD/ions 表/nst extra_keys 等)。
|
||||||
// None → runner 用代码内硬编码默认(向后兼容)。
|
// None → runner 用代码内硬编码默认(向后兼容)。
|
||||||
@@ -193,6 +244,12 @@ pub async fn execute_task(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
// 种子参数(ladder 步进规划需要种子与目标的参数差):从权威 seed_point_name
|
||||||
|
// 解析。解析失败(命名不符)→ None → ladder 回退不触发,安全降级。
|
||||||
|
let seed_params = task
|
||||||
|
.seed_point_name
|
||||||
|
.as_deref()
|
||||||
|
.and_then(common::models::GridPointParams::parse_point_name);
|
||||||
let summary = runner
|
let summary = runner
|
||||||
.run_model_with_timeout(
|
.run_model_with_timeout(
|
||||||
&task.params,
|
&task.params,
|
||||||
@@ -202,6 +259,7 @@ pub async fn execute_task(
|
|||||||
current_strategy,
|
current_strategy,
|
||||||
Some(chain),
|
Some(chain),
|
||||||
seed_atmos_path.as_deref(),
|
seed_atmos_path.as_deref(),
|
||||||
|
seed_params.as_ref(),
|
||||||
synspec_cfg.as_ref(),
|
synspec_cfg.as_ref(),
|
||||||
// 阶段独立配置开关(见 docs/task_engine_decoupling_design.md §5)。
|
// 阶段独立配置开关(见 docs/task_engine_decoupling_design.md §5)。
|
||||||
task.tlusty_config.enabled,
|
task.tlusty_config.enabled,
|
||||||
@@ -257,7 +315,30 @@ pub async fn execute_task(
|
|||||||
slot_work_dir.display()
|
slot_work_dir.display()
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok((summary, seed_bytes))
|
// ladder 中间梯级种子字节:即便最终失败也上报(簇内相邻失败点可复用已收敛梯级)。
|
||||||
|
// 阶段快照命名 <name>.<label>.7(execute_tlusty_stage 写入 model_dir)。
|
||||||
|
let mut ladder_seed_files: Vec<(String, Vec<u8>)> = Vec::new();
|
||||||
|
if !summary.ladder_seeds.is_empty() {
|
||||||
|
let model_sub_dir = slot_work_dir.join(&summary.name);
|
||||||
|
for info in &summary.ladder_seeds {
|
||||||
|
let path = model_sub_dir.join(format!("{}.{}.7", summary.name, info.label));
|
||||||
|
if path.is_file() {
|
||||||
|
if let Ok(bytes) = tokio::fs::read(&path).await {
|
||||||
|
info!(
|
||||||
|
"读取 ladder 中间种子 {}({},{} 字节)",
|
||||||
|
info.point_name,
|
||||||
|
path.display(),
|
||||||
|
bytes.len()
|
||||||
|
);
|
||||||
|
ladder_seed_files.push((info.point_name.clone(), bytes));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warn!("ladder 种子快照缺失: {}", path.display());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((summary, seed_bytes, ladder_seed_files))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 清理任务在 Node 端的沙盒目录
|
/// 清理任务在 Node 端的沙盒目录
|
||||||
@@ -472,6 +553,7 @@ fn resolve_execution_chain(
|
|||||||
tlusty_chain_params: &Option<serde_json::Value>,
|
tlusty_chain_params: &Option<serde_json::Value>,
|
||||||
seed_chain_params: &Option<serde_json::Value>,
|
seed_chain_params: &Option<serde_json::Value>,
|
||||||
task_id: &str,
|
task_id: &str,
|
||||||
|
teff: f64,
|
||||||
) -> Vec<ChainStep> {
|
) -> Vec<ChainStep> {
|
||||||
if current_strategy == "seed_step" {
|
if current_strategy == "seed_step" {
|
||||||
seed_chain_params
|
seed_chain_params
|
||||||
@@ -488,6 +570,27 @@ fn resolve_execution_chain(
|
|||||||
})
|
})
|
||||||
.filter(|c| !c.is_empty())
|
.filter(|c| !c.is_empty())
|
||||||
.unwrap_or_else(common::runner::default_seed_chain)
|
.unwrap_or_else(common::runner::default_seed_chain)
|
||||||
|
} else if current_strategy == "seed_step_stab" {
|
||||||
|
// 稳定化种子步进:POPZER+DPSILG 稳定化参数即本策略的实质——用户自定义链
|
||||||
|
// (tlusty_chain/seed_chain)不带这些旋钮,热启动会重演发散,故不采纳、
|
||||||
|
// 恒用 default_seed_stab_chain。
|
||||||
|
// 2026-08-21 域门控(60k 攻坚实测,docs/failed81_...md §十一):DPSILG/POPZER
|
||||||
|
// 族旋钮仅在低温 He 富域(Teff≤30kK)有效;高温高金属域(60k/g5.0 o-1 角)
|
||||||
|
// 实测自复现收敛种子加档后 17 拍爆到 4e16——致散。域外改用普通种子链,
|
||||||
|
// 交给 runner 的自适应 Teff 延拓处理。
|
||||||
|
if teff <= 30000.0 {
|
||||||
|
common::runner::default_seed_stab_chain()
|
||||||
|
} else {
|
||||||
|
info!(
|
||||||
|
"任务 {}:Teff={}>30kK 域外,seed_step_stab 降级为普通种子链(稳定化旋钮高温致散)",
|
||||||
|
task_id, teff
|
||||||
|
);
|
||||||
|
seed_chain_params
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|v| serde_json::from_value::<Vec<ChainStep>>(v.clone()).ok())
|
||||||
|
.filter(|c| !c.is_empty())
|
||||||
|
.unwrap_or_else(common::runner::default_seed_chain)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
tlusty_chain_params
|
tlusty_chain_params
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -539,6 +642,7 @@ mod tests {
|
|||||||
temp_check: None,
|
temp_check: None,
|
||||||
emflux_check: None,
|
emflux_check: None,
|
||||||
bfac_check: None,
|
bfac_check: None,
|
||||||
|
ladder_seeds: Vec::new(),
|
||||||
note: None,
|
note: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -558,7 +662,7 @@ mod tests {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// seed_chain_params=None → 走 default_seed_chain;tlusty_chain_params 被忽略。
|
// seed_chain_params=None → 走 default_seed_chain;tlusty_chain_params 被忽略。
|
||||||
let chain = resolve_execution_chain("seed_step", &Some(custom_cold), &None, "t1");
|
let chain = resolve_execution_chain("seed_step", &Some(custom_cold), &None, "t1", 20000.0);
|
||||||
assert_eq!(labels(&chain), vec!["seed_nc", "nl"]);
|
assert_eq!(labels(&chain), vec!["seed_nc", "nl"]);
|
||||||
// 首步必须是非灰 LTE(ltgray=F),否则会删 fort.8 丢弃种子。
|
// 首步必须是非灰 LTE(ltgray=F),否则会删 fort.8 丢弃种子。
|
||||||
assert_eq!(chain[0].ltgray, "F");
|
assert_eq!(chain[0].ltgray, "F");
|
||||||
@@ -572,13 +676,32 @@ mod tests {
|
|||||||
{"label": "nl", "lte": "F", "ltgray": "F", "ilvlin": 100, "niter": 100, "orelax": 0.5},
|
{"label": "nl", "lte": "F", "ltgray": "F", "ilvlin": 100, "niter": 100, "orelax": 0.5},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let chain = resolve_execution_chain("seed_step", &None, &Some(custom_seed), "t1b");
|
let chain = resolve_execution_chain("seed_step", &None, &Some(custom_seed), "t1b", 20000.0);
|
||||||
assert_eq!(labels(&chain), vec!["seed_nc", "nl"]);
|
assert_eq!(labels(&chain), vec!["seed_nc", "nl"]);
|
||||||
// 用户配置的 orelax 生效。
|
// 用户配置的 orelax 生效。
|
||||||
assert_eq!(chain[0].orelax, Some(0.3));
|
assert_eq!(chain[0].orelax, Some(0.3));
|
||||||
assert_eq!(chain[1].orelax, Some(0.5));
|
assert_eq!(chain[1].orelax, Some(0.5));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// seed_step_stab 温度域门控(2026-08-21,60k 攻坚):
|
||||||
|
/// Teff≤30kK 用稳定化链(全步带 DPSILG/POPZER);
|
||||||
|
/// 高温域(60k/g5.0 高金属角实测旋钮致散)降级为普通种子链。
|
||||||
|
#[test]
|
||||||
|
fn seed_step_stab_teff_domain_gating() {
|
||||||
|
let cold = resolve_execution_chain("seed_step_stab", &None, &None, "hot1", 20000.0);
|
||||||
|
assert!(
|
||||||
|
cold.iter().all(|s| s.dpsilg.is_some() && s.popzer.is_some()),
|
||||||
|
"低温域 seed_step_stab 应带稳定化参数"
|
||||||
|
);
|
||||||
|
|
||||||
|
let hot = resolve_execution_chain("seed_step_stab", &None, &None, "hot2", 60000.0);
|
||||||
|
assert!(
|
||||||
|
hot.iter().all(|s| s.dpsilg.is_none() && s.popzer.is_none()),
|
||||||
|
"高温域 seed_step_stab 应降级为普通种子链(不带稳定化旋钮)"
|
||||||
|
);
|
||||||
|
assert_eq!(labels(&hot), vec!["seed_nc", "nl"]);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cold_run_uses_custom_chain_when_provided() {
|
fn cold_run_uses_custom_chain_when_provided() {
|
||||||
let custom = serde_json::json!([
|
let custom = serde_json::json!([
|
||||||
@@ -587,13 +710,13 @@ mod tests {
|
|||||||
{"label": "nl", "lte": "F", "ltgray": "F", "ilvlin": 100, "niter": 100},
|
{"label": "nl", "lte": "F", "ltgray": "F", "ilvlin": 100, "niter": 100},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let chain = resolve_execution_chain("cold_run", &Some(custom), &None, "t2");
|
let chain = resolve_execution_chain("cold_run", &Some(custom), &None, "t2", 20000.0);
|
||||||
assert_eq!(labels(&chain), vec!["lte", "nc", "nl"]);
|
assert_eq!(labels(&chain), vec!["lte", "nc", "nl"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cold_run_falls_back_to_default_when_no_custom_chain() {
|
fn cold_run_falls_back_to_default_when_no_custom_chain() {
|
||||||
let chain = resolve_execution_chain("cold_run", &None, &None, "t3");
|
let chain = resolve_execution_chain("cold_run", &None, &None, "t3", 20000.0);
|
||||||
assert_eq!(labels(&chain), vec!["lte", "nc", "nl"]);
|
assert_eq!(labels(&chain), vec!["lte", "nc", "nl"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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("预热与获取服务端运行时资源失败")?;
|
||||||
|
|
||||||
|
|||||||
@@ -50,12 +50,22 @@ fn derive_report_status(s: &ModelSummary) -> TaskStatus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// executor 返回值:(summary, 最终种子字节, ladder 中间梯级种子字节列表)。
|
||||||
|
pub type ExecOutcome = Result<
|
||||||
|
(
|
||||||
|
ModelSummary,
|
||||||
|
Option<Vec<u8>>,
|
||||||
|
Vec<(String, Vec<u8>)>,
|
||||||
|
),
|
||||||
|
String,
|
||||||
|
>;
|
||||||
|
|
||||||
pub async fn report_result(
|
pub async fn report_result(
|
||||||
client: &Client,
|
client: &Client,
|
||||||
server_url: &str,
|
server_url: &str,
|
||||||
node_id: &str,
|
node_id: &str,
|
||||||
task: &TaskSpec,
|
task: &TaskSpec,
|
||||||
exec_res: Result<(ModelSummary, Option<Vec<u8>>), String>,
|
exec_res: ExecOutcome,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let report_url = format!("{}/api/task/report", server_url);
|
let report_url = format!("{}/api/task/report", server_url);
|
||||||
|
|
||||||
@@ -69,8 +79,9 @@ pub async fn report_result(
|
|||||||
summary_json,
|
summary_json,
|
||||||
seed_bytes,
|
seed_bytes,
|
||||||
failed_stage,
|
failed_stage,
|
||||||
|
ladder_seed_files,
|
||||||
) = match exec_res {
|
) = match exec_res {
|
||||||
Ok((s, s_bytes)) => (
|
Ok((s, s_bytes, ladder_files)) => (
|
||||||
derive_report_status(&s),
|
derive_report_status(&s),
|
||||||
s.result_valid,
|
s.result_valid,
|
||||||
s.final_max_relc,
|
s.final_max_relc,
|
||||||
@@ -80,6 +91,7 @@ pub async fn report_result(
|
|||||||
serde_json::to_string(&s).unwrap_or_default(),
|
serde_json::to_string(&s).unwrap_or_default(),
|
||||||
s_bytes,
|
s_bytes,
|
||||||
infer_failed_stage(task, &s),
|
infer_failed_stage(task, &s),
|
||||||
|
ladder_files,
|
||||||
),
|
),
|
||||||
Err(e) => (
|
Err(e) => (
|
||||||
TaskStatus::Failed,
|
TaskStatus::Failed,
|
||||||
@@ -91,6 +103,7 @@ pub async fn report_result(
|
|||||||
serde_json::json!({"error": e}).to_string(),
|
serde_json::json!({"error": e}).to_string(),
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
Vec::new(),
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -128,6 +141,15 @@ pub async fn report_result(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ladder 中间梯级种子:即便最终任务失败也上传(executor 只收集收敛+无 NaN
|
||||||
|
// 的中间模型),server 端落 seeds 表供相邻失败点复用。
|
||||||
|
for (seed_name, bytes) in &ladder_seed_files {
|
||||||
|
let part = Part::bytes(bytes.clone())
|
||||||
|
.file_name(format!("{}.7", seed_name))
|
||||||
|
.mime_str("application/octet-stream")?;
|
||||||
|
form = form.part("ladder_seed", part);
|
||||||
|
}
|
||||||
|
|
||||||
match client.post(&report_url).multipart(form).send().await {
|
match client.post(&report_url).multipart(form).send().await {
|
||||||
Ok(resp) if resp.status().is_success() => {
|
Ok(resp) if resp.status().is_success() => {
|
||||||
info!(
|
info!(
|
||||||
@@ -239,6 +261,7 @@ mod tests {
|
|||||||
temp_check: None,
|
temp_check: None,
|
||||||
emflux_check: None,
|
emflux_check: None,
|
||||||
bfac_check: None,
|
bfac_check: None,
|
||||||
|
ladder_seeds: Vec::new(),
|
||||||
note: None,
|
note: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -542,7 +542,7 @@ impl NodeWorker {
|
|||||||
// 严重失败(runner 抛 Err)时 summary_opt=None,此时回退到 task.point_name
|
// 严重失败(runner 抛 Err)时 summary_opt=None,此时回退到 task.point_name
|
||||||
// (二者均源自 params.model_name())作为归档目录名,确保失败任务的
|
// (二者均源自 params.model_name())作为归档目录名,确保失败任务的
|
||||||
// 排错日志也能落盘而非随沙盒删除丢失。
|
// 排错日志也能落盘而非随沙盒删除丢失。
|
||||||
let summary_opt = res.as_ref().ok().map(|(s, _)| s.clone());
|
let summary_opt = res.as_ref().ok().map(|(s, _, _)| s.clone());
|
||||||
let result_name = summary_opt
|
let result_name = summary_opt
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|s| s.name.clone())
|
.map(|s| s.name.clone())
|
||||||
|
|||||||
@@ -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!({
|
||||||
|
|||||||
@@ -81,6 +81,8 @@ pub async fn report_task(
|
|||||||
) -> Result<impl IntoResponse, crate::api::AppError> {
|
) -> Result<impl IntoResponse, crate::api::AppError> {
|
||||||
let mut report_json: Option<TaskReport> = None;
|
let mut report_json: Option<TaskReport> = None;
|
||||||
let mut seed_file_data: Option<Vec<u8>> = None;
|
let mut seed_file_data: Option<Vec<u8>> = None;
|
||||||
|
// ladder 中间梯级种子(可重复字段):文件名(去 .7)即合成种子名,参数从名称解析。
|
||||||
|
let mut ladder_seed_files: Vec<(String, Vec<u8>)> = Vec::new();
|
||||||
let mut multipart_error = false;
|
let mut multipart_error = false;
|
||||||
|
|
||||||
// 遍历全部 multipart 字段。旧实现 `while let Ok(Some(field))` 在首个字段读取错误时
|
// 遍历全部 multipart 字段。旧实现 `while let Ok(Some(field))` 在首个字段读取错误时
|
||||||
@@ -114,6 +116,24 @@ pub async fn report_task(
|
|||||||
multipart_error = true;
|
multipart_error = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if field_name == "ladder_seed" {
|
||||||
|
// 文件名即合成种子名(<model_name>_ladder.7)
|
||||||
|
let file_name = field
|
||||||
|
.file_name()
|
||||||
|
.unwrap_or("")
|
||||||
|
.trim_end_matches(".7")
|
||||||
|
.to_string();
|
||||||
|
match field.bytes().await {
|
||||||
|
Ok(bytes) => {
|
||||||
|
if !file_name.is_empty() {
|
||||||
|
ladder_seed_files.push((file_name, bytes.to_vec()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!("读取 ladder_seed 字段失败: {}", e);
|
||||||
|
multipart_error = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(None) => break,
|
Ok(None) => break,
|
||||||
@@ -329,6 +349,40 @@ pub async fn report_task(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ladder 中间梯级种子持久化(2026-08-17):node 端仅上报「收敛 + 无 NaN」的
|
||||||
|
// 中间模型;此处原子落盘 + 按 _ladder 合成名入 seeds 表(ON CONFLICT 仅刷新
|
||||||
|
// file_path,绝不触碰真实网格点种子行)。名称解析失败 → 丢弃并告警(防脏名)。
|
||||||
|
for (seed_name, bytes) in &ladder_seed_files {
|
||||||
|
let Some(lp) = common::models::GridPointParams::parse_point_name(seed_name) else {
|
||||||
|
warn!("ladder 种子名 {} 无法解析参数,丢弃", seed_name);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let ladder_dir = Path::new(&state.seeds_dir).join(seed_name);
|
||||||
|
if fs::create_dir_all(&ladder_dir).await.is_err() {
|
||||||
|
warn!("ladder 种子目录创建失败: {}", ladder_dir.display());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let tmp = ladder_dir.join(format!("{}.7.{}.tmp", seed_name, uuid::Uuid::new_v4().simple()));
|
||||||
|
let path = ladder_dir.join(format!("{}.7", seed_name));
|
||||||
|
if fs::write(&tmp, bytes).await.is_ok() && fs::rename(&tmp, &path).await.is_ok() {
|
||||||
|
if let Err(e) = state
|
||||||
|
.db
|
||||||
|
.insert_seed_named(seed_name, &lp, &path.to_string_lossy())
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
warn!("ladder 种子 {} 入库失败: {}", seed_name, e);
|
||||||
|
} else {
|
||||||
|
info!(
|
||||||
|
"ladder 中间种子入库: {} (t{} g{} he{})",
|
||||||
|
seed_name,
|
||||||
|
lp.teff.value(),
|
||||||
|
lp.logg.value(),
|
||||||
|
lp.loghe.value()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if state_changed
|
if state_changed
|
||||||
|
|||||||
@@ -576,14 +576,19 @@ 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' THEN 1 ELSE 0 END) AS tlusty_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 = 'cold_run' THEN 1 ELSE 0 END) AS cold_run_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 = 'converged' 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_stab' THEN 1 ELSE 0 END) AS seed_step_stab_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), n(12), n(13)))
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
None => conn.query_row(
|
None => conn.query_row(
|
||||||
@@ -594,14 +599,19 @@ 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' THEN 1 ELSE 0 END) AS tlusty_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 = 'cold_run' THEN 1 ELSE 0 END) AS cold_run_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 = 'converged' 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_stab' THEN 1 ELSE 0 END) AS seed_step_stab_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), n(12), n(13)))
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
}?;
|
}?;
|
||||||
@@ -612,9 +622,14 @@ impl Database {
|
|||||||
running,
|
running,
|
||||||
completed,
|
completed,
|
||||||
failed,
|
failed,
|
||||||
|
tlusty_converged,
|
||||||
cold_run_converged,
|
cold_run_converged,
|
||||||
seed_step_converged,
|
seed_step_converged,
|
||||||
|
seed_step_stab_converged,
|
||||||
|
tlusty_failed,
|
||||||
synspec_converged,
|
synspec_converged,
|
||||||
|
synspec_failed,
|
||||||
|
synspec_pending,
|
||||||
) = row;
|
) = row;
|
||||||
|
|
||||||
Ok(serde_json::json!({
|
Ok(serde_json::json!({
|
||||||
@@ -624,9 +639,17 @@ impl Database {
|
|||||||
"running": running,
|
"running": running,
|
||||||
"completed": completed,
|
"completed": completed,
|
||||||
"failed": failed,
|
"failed": failed,
|
||||||
|
// tlusty_converged(2026-08-25):大气收敛的权威总数(不按 method 拆分)。
|
||||||
|
// 前端「N 大气收敛」此前用 cold+seed 之和——seed_step_stab(2026-08-18 引入)
|
||||||
|
// 收敛的 70 点被漏计(生产 9137/9216 差额主因)。
|
||||||
|
"tlusty_converged": tlusty_converged,
|
||||||
"cold_run_converged": cold_run_converged,
|
"cold_run_converged": cold_run_converged,
|
||||||
"seed_step_converged": seed_step_converged,
|
"seed_step_converged": seed_step_converged,
|
||||||
|
"seed_step_stab_converged": seed_step_stab_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 +744,12 @@ 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_converged: g("tlusty_converged"),
|
||||||
|
seed_step_stab_converged: g("seed_step_stab_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 +857,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 +902,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
|
||||||
|
|||||||
+511
-1
@@ -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,20 @@ 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='converged')——权威口径,语义同
|
||||||
|
/// WorkflowStats.tlusty_converged(2026-08-25 补,见彼处注释)。
|
||||||
|
#[serde(default)]
|
||||||
|
pub tlusty_converged: i64,
|
||||||
|
#[serde(default)]
|
||||||
|
pub seed_step_stab_converged: i64,
|
||||||
|
/// 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 +746,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 +760,23 @@ 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='converged',不按 method 拆分)——权威口径。
|
||||||
|
/// 前端「N 大气收敛」应消费此字段;cold/seed/stab 为策略细分。2026-08-25 补:
|
||||||
|
/// 此前前端用 cold+seed 之和,seed_step_stab(2026-08-18 引入)收敛点被漏计
|
||||||
|
/// (生产 9137/9216 差额主因)。
|
||||||
|
#[serde(default)]
|
||||||
|
pub tlusty_converged: i64,
|
||||||
|
/// TLUSTY 阶段以 seed_step_stab 策略收敛的点数(稳定化/waypoint 种子链)。
|
||||||
|
#[serde(default)]
|
||||||
|
pub seed_step_stab_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 +806,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 +848,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)?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1171,6 +1214,7 @@ mod tests {
|
|||||||
temp_check: None,
|
temp_check: None,
|
||||||
emflux_check: None,
|
emflux_check: None,
|
||||||
bfac_check: None,
|
bfac_check: None,
|
||||||
|
ladder_seeds: Vec::new(),
|
||||||
note: None,
|
note: None,
|
||||||
};
|
};
|
||||||
db.upsert_point_summary(&summary.name, "wf_a", &summary, "seed_step")
|
db.upsert_point_summary(&summary.name, "wf_a", &summary, "seed_step")
|
||||||
@@ -1187,12 +1231,16 @@ mod tests {
|
|||||||
assert_eq!(all["completed"], 1);
|
assert_eq!(all["completed"], 1);
|
||||||
assert_eq!(all["cold_run_converged"], 0);
|
assert_eq!(all["cold_run_converged"], 0);
|
||||||
assert_eq!(all["seed_step_converged"], 1);
|
assert_eq!(all["seed_step_converged"], 1);
|
||||||
|
// tlusty_converged(2026-08-25):不按 method 拆分的权威总数。
|
||||||
|
assert_eq!(all["tlusty_converged"], 1);
|
||||||
|
assert_eq!(all["seed_step_stab_converged"], 0);
|
||||||
// 单工作流 wf_a:1 pending + 1 seed_step 收敛
|
// 单工作流 wf_a:1 pending + 1 seed_step 收敛
|
||||||
let a = db.get_grid_summary_stats(Some("wf_a")).await.unwrap();
|
let a = db.get_grid_summary_stats(Some("wf_a")).await.unwrap();
|
||||||
assert_eq!(a["total"], 2);
|
assert_eq!(a["total"], 2);
|
||||||
assert_eq!(a["pending"], 1);
|
assert_eq!(a["pending"], 1);
|
||||||
assert_eq!(a["queued"], 0);
|
assert_eq!(a["queued"], 0);
|
||||||
assert_eq!(a["seed_step_converged"], 1);
|
assert_eq!(a["seed_step_converged"], 1);
|
||||||
|
assert_eq!(a["tlusty_converged"], 1);
|
||||||
// 不存在的工作流:0
|
// 不存在的工作流:0
|
||||||
let none = db
|
let none = db
|
||||||
.get_grid_summary_stats(Some("nonexistent"))
|
.get_grid_summary_stats(Some("nonexistent"))
|
||||||
@@ -1236,6 +1284,120 @@ mod tests {
|
|||||||
assert_eq!(m.unwrap().name, exact.model_name());
|
assert_eq!(m.unwrap().name, exact.model_name());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// find_exact_family_seed_from_db 严格同物理族判定(2026-08-19 生产修复回归):
|
||||||
|
/// ladder 中间种子(如 teff=22500)即便 CNO 距离为 0,也因 Teff 不同被排除;
|
||||||
|
/// 同 Teff/logg/logHe 的 CNO 邻居正常命中;排除目标自身名。
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_find_exact_family_seed_strict_same_family() {
|
||||||
|
let temp_dir = tempfile::tempdir().unwrap();
|
||||||
|
let db = Database::new(&temp_dir.path().join("seed_stab_db.db").to_string_lossy())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let target = GridPointParams {
|
||||||
|
teff: 20000.0.into(),
|
||||||
|
logg: 6.5.into(),
|
||||||
|
loghe: 2.0.into(),
|
||||||
|
logc: (-4.0).into(),
|
||||||
|
logn: (-3.0).into(),
|
||||||
|
logo: (-4.0).into(),
|
||||||
|
};
|
||||||
|
// ladder 中间种子:同 logg/logHe、CNO 完全一致,但 teff=22500 —— 必须排除。
|
||||||
|
let ladder = GridPointParams {
|
||||||
|
teff: 22500.0.into(),
|
||||||
|
logg: 6.5.into(),
|
||||||
|
loghe: 2.0.into(),
|
||||||
|
logc: (-4.0).into(),
|
||||||
|
logn: (-3.0).into(),
|
||||||
|
logo: (-4.0).into(),
|
||||||
|
};
|
||||||
|
// 合法 CNO 邻居:同 Teff/logg/logHe,Δo=1。
|
||||||
|
let cno_neighbor = GridPointParams {
|
||||||
|
teff: 20000.0.into(),
|
||||||
|
logg: 6.5.into(),
|
||||||
|
loghe: 2.0.into(),
|
||||||
|
logc: (-4.0).into(),
|
||||||
|
logn: (-3.0).into(),
|
||||||
|
logo: (-3.0).into(),
|
||||||
|
};
|
||||||
|
db.insert_seed(&ladder, "/tmp/ladder.7").await.unwrap();
|
||||||
|
db.insert_seed(&cno_neighbor, "/tmp/neighbor.7").await.unwrap();
|
||||||
|
|
||||||
|
let m = db
|
||||||
|
.find_exact_family_seed_from_db(&target, &[target.model_name()])
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(m.is_some(), "应命中 CNO 邻居");
|
||||||
|
assert_eq!(
|
||||||
|
m.unwrap().name,
|
||||||
|
cno_neighbor.model_name(),
|
||||||
|
"不得选 teff 不同的 ladder 中间种子(即便其 CNO 距离为 0)"
|
||||||
|
);
|
||||||
|
|
||||||
|
// 无严格同族候选时返回 None(不退化到 global)。
|
||||||
|
let lonely = GridPointParams {
|
||||||
|
teff: 30000.0.into(),
|
||||||
|
logg: 5.0.into(),
|
||||||
|
loghe: (-2.0).into(),
|
||||||
|
logc: (-2.0).into(),
|
||||||
|
logn: (-2.0).into(),
|
||||||
|
logo: (-2.0).into(),
|
||||||
|
};
|
||||||
|
let none = db
|
||||||
|
.find_exact_family_seed_from_db(&lonely, &[lonely.model_name()])
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(none.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// 种子轮换回归(2026-08-20 修复):find_exact_family_seed_from_db 排除列表
|
||||||
|
/// 应使调用方在重试间轮换到下一个未试过的同族 CNO 邻居;全部排除后返回 None。
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_find_exact_family_seed_rotation() {
|
||||||
|
let temp_dir = tempfile::tempdir().unwrap();
|
||||||
|
let db = Database::new(&temp_dir.path().join("seed_rot_db.db").to_string_lossy())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let base = |logc: f64, logn: f64, logo: f64| GridPointParams {
|
||||||
|
teff: 20000.0.into(),
|
||||||
|
logg: 6.0.into(),
|
||||||
|
loghe: 2.0.into(),
|
||||||
|
logc: logc.into(),
|
||||||
|
logn: logn.into(),
|
||||||
|
logo: logo.into(),
|
||||||
|
};
|
||||||
|
let target = base(-3.0, -3.0, -1.0);
|
||||||
|
let n1 = base(-3.0, -2.0, -1.0);
|
||||||
|
let n2 = base(-3.0, -3.0, -2.0);
|
||||||
|
db.insert_seed(&n1, "/tmp/rot_n1.7").await.unwrap();
|
||||||
|
db.insert_seed(&n2, "/tmp/rot_n2.7").await.unwrap();
|
||||||
|
|
||||||
|
let self_name = target.model_name();
|
||||||
|
// 首次:选距离最近的未排除邻居。
|
||||||
|
let first = db
|
||||||
|
.find_exact_family_seed_from_db(&target, &[self_name.clone()])
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.expect("应命中 CNO 邻居");
|
||||||
|
// 排除首个后:轮换到另一个邻居。
|
||||||
|
let second = db
|
||||||
|
.find_exact_family_seed_from_db(&target, &[self_name, first.name.clone()])
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.expect("排除首个邻居后应轮换到第二个邻居");
|
||||||
|
assert_ne!(first.name, second.name);
|
||||||
|
// 全部排除后:None(终态 failed,不死循环)。
|
||||||
|
let none = db
|
||||||
|
.find_exact_family_seed_from_db(
|
||||||
|
&target,
|
||||||
|
&[target.model_name(), first.name, second.name],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(none.is_none(), "邻居全部试过后应返回 None");
|
||||||
|
}
|
||||||
|
|
||||||
/// "种子回退仅一次"守卫语义(2026-08-02 涡旋事故定稿):has_seed_step_attempt
|
/// "种子回退仅一次"守卫语义(2026-08-02 涡旋事故定稿):has_seed_step_attempt
|
||||||
/// 统计**一切** seed_step 行(含 pending)。pending 行在新架构下只有两种来源:
|
/// 统计**一切** seed_step 行(含 pending)。pending 行在新架构下只有两种来源:
|
||||||
/// (a) 真在途(排队/已领用)——计数它正是对在途回退的去重,挡住救援途中迟到失败
|
/// (a) 真在途(排队/已领用)——计数它正是对在途回退的去重,挡住救援途中迟到失败
|
||||||
@@ -1824,6 +1986,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();
|
||||||
@@ -3236,6 +3399,347 @@ mod tests {
|
|||||||
assert_eq!(item.status, "completed");
|
assert_eq!(item.status, "completed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 完成 flip 的「未消费回退链」阻塞回归(2026-08-22 修复):
|
||||||
|
/// 最后一个活跃点失败时,其最新 failed 行策略链弹掉失败首项后仍有顺位
|
||||||
|
/// (长度 >1)→ workflow 不得置 completed(否则 trigger_strategy_fallback
|
||||||
|
/// 的 still_running 守卫拦截,链上 seed_step/seed_step_stab 永不派发;
|
||||||
|
/// 生产实证 sdB_cno/t60000_g5.0_he-4_c-4_n-4_o-1,8-22 04:00:50)。
|
||||||
|
/// 链耗尽(长度 1)后放行翻转。
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_workflow_flip_blocked_by_unconsumed_fallback_chain() {
|
||||||
|
let temp_dir = tempfile::tempdir().unwrap();
|
||||||
|
let db = Database::new(
|
||||||
|
&temp_dir
|
||||||
|
.path()
|
||||||
|
.join("flip_block_db.db")
|
||||||
|
.to_string_lossy(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let params = GridPointParams {
|
||||||
|
teff: 60000.0.into(),
|
||||||
|
logg: 5.0.into(),
|
||||||
|
loghe: (-4.0).into(),
|
||||||
|
logc: (-4.0).into(),
|
||||||
|
logn: (-4.0).into(),
|
||||||
|
logo: (-1.0).into(),
|
||||||
|
};
|
||||||
|
let name = params.model_name();
|
||||||
|
db.upsert_workflow("wf_fb2", None, "config", "idle")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
db.update_workflow_status("wf_fb2", "running").await.unwrap();
|
||||||
|
db.upsert_grid_point(¶ms, 0, "wf_fb2").await.unwrap();
|
||||||
|
// 点终态 failed、无 pending/queued/running——旧条件已满足 flip。
|
||||||
|
db.update_grid_status(&name, GridPointStatus::Failed, "wf_fb2")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// 最新 failed 行携带 3 顺位链(cold_run 刚失败,尚余 seed_step/seed_step_stab)。
|
||||||
|
let t1 = uuid::Uuid::new_v4();
|
||||||
|
db.insert_task(&common::models::TaskSpec {
|
||||||
|
task_id: t1,
|
||||||
|
point_name: name.clone(),
|
||||||
|
params: params.clone(),
|
||||||
|
timeout_sec: 7200,
|
||||||
|
workflow_name: Some("wf_fb2".to_string()),
|
||||||
|
wave: 0,
|
||||||
|
tlusty_config: PhaseConfig {
|
||||||
|
strategies: vec![
|
||||||
|
"cold_run".to_string(),
|
||||||
|
"seed_step".to_string(),
|
||||||
|
"seed_step_stab".to_string(),
|
||||||
|
],
|
||||||
|
..PhaseConfig::default_tlusty()
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
{
|
||||||
|
let pool = db.pool.clone();
|
||||||
|
let tid = t1.to_string();
|
||||||
|
tokio::task::spawn_blocking(move || {
|
||||||
|
let conn = pool.get().unwrap();
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE tasks SET status = 'failed', completed_at = datetime('now') \
|
||||||
|
WHERE task_id = ?1",
|
||||||
|
params![tid],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
db.sync_all_running_workflows_completion().await.unwrap();
|
||||||
|
let wf = db.get_workflow("wf_fb2").await.unwrap().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
wf.status, "running",
|
||||||
|
"存在未消费回退链的 failed 点时,workflow 不应翻转 completed"
|
||||||
|
);
|
||||||
|
|
||||||
|
// 回退推进到链尾(更新的 failed 行仅剩单顺位)→ 阻塞解除,翻转放行。
|
||||||
|
let t2 = uuid::Uuid::new_v4();
|
||||||
|
db.insert_task(&common::models::TaskSpec {
|
||||||
|
task_id: t2,
|
||||||
|
point_name: name.clone(),
|
||||||
|
params: params.clone(),
|
||||||
|
timeout_sec: 7200,
|
||||||
|
workflow_name: Some("wf_fb2".to_string()),
|
||||||
|
wave: 0,
|
||||||
|
tlusty_config: PhaseConfig {
|
||||||
|
strategies: vec!["seed_step_stab".to_string()],
|
||||||
|
..PhaseConfig::default_tlusty()
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
{
|
||||||
|
let pool = db.pool.clone();
|
||||||
|
let tid = t2.to_string();
|
||||||
|
tokio::task::spawn_blocking(move || {
|
||||||
|
let conn = pool.get().unwrap();
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE tasks SET status = 'failed', completed_at = datetime('now'), \
|
||||||
|
created_at = datetime('now', '+1 hour') WHERE task_id = ?1",
|
||||||
|
params![tid],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
db.sync_all_running_workflows_completion().await.unwrap();
|
||||||
|
let wf2 = db.get_workflow("wf_fb2").await.unwrap().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
wf2.status, "completed",
|
||||||
|
"链耗尽(长度 1)后应恢复完成翻转"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 完成 flip 的「种子轮换待执行」阻塞回归(2026-08-22 审查补充):
|
||||||
|
/// 链尾 [seed_step_stab] 失败 + 存在未试过的同物理族干净种子 → 不翻转
|
||||||
|
/// (否则 rotation 臂被 still_running 守卫吞掉,waypoint 只有一次机会);
|
||||||
|
/// 同族种子全部用过后放行翻转。
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_workflow_flip_blocked_by_seed_rotation_pending() {
|
||||||
|
let temp_dir = tempfile::tempdir().unwrap();
|
||||||
|
let db = Database::new(
|
||||||
|
&temp_dir
|
||||||
|
.path()
|
||||||
|
.join("flip_rot_db.db")
|
||||||
|
.to_string_lossy(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let params = GridPointParams {
|
||||||
|
teff: 60000.0.into(),
|
||||||
|
logg: 5.0.into(),
|
||||||
|
loghe: (-4.0).into(),
|
||||||
|
logc: (-4.0).into(),
|
||||||
|
logn: (-4.0).into(),
|
||||||
|
logo: (-1.0).into(),
|
||||||
|
};
|
||||||
|
let name = params.model_name();
|
||||||
|
db.upsert_workflow("wf_rot", None, "config", "idle")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
db.update_workflow_status("wf_rot", "running").await.unwrap();
|
||||||
|
db.upsert_grid_point(¶ms, 0, "wf_rot").await.unwrap();
|
||||||
|
db.update_grid_status(&name, GridPointStatus::Failed, "wf_rot")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// 链尾 [seed_step_stab] 失败行。
|
||||||
|
let t1 = uuid::Uuid::new_v4();
|
||||||
|
db.insert_task(&common::models::TaskSpec {
|
||||||
|
task_id: t1,
|
||||||
|
point_name: name.clone(),
|
||||||
|
params: params.clone(),
|
||||||
|
timeout_sec: 7200,
|
||||||
|
workflow_name: Some("wf_rot".to_string()),
|
||||||
|
wave: 0,
|
||||||
|
tlusty_config: PhaseConfig {
|
||||||
|
strategies: vec!["seed_step_stab".to_string()],
|
||||||
|
..PhaseConfig::default_tlusty()
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
{
|
||||||
|
let pool = db.pool.clone();
|
||||||
|
let tid = t1.to_string();
|
||||||
|
tokio::task::spawn_blocking(move || {
|
||||||
|
let conn = pool.get().unwrap();
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE tasks SET status = 'failed', completed_at = datetime('now') \
|
||||||
|
WHERE task_id = ?1",
|
||||||
|
params![tid],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 无同族种子 → 无可轮换 → 放行翻转。
|
||||||
|
db.sync_all_running_workflows_completion().await.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
db.get_workflow("wf_rot").await.unwrap().unwrap().status,
|
||||||
|
"completed",
|
||||||
|
"无同族候选种子时链尾失败应放行翻转"
|
||||||
|
);
|
||||||
|
|
||||||
|
// 重启场景:workflow 回 running,注入一个未用过的同族干净种子。
|
||||||
|
db.update_workflow_status("wf_rot", "running").await.unwrap();
|
||||||
|
let neighbor = GridPointParams {
|
||||||
|
logn: (-3.489798).into(),
|
||||||
|
..params.clone()
|
||||||
|
};
|
||||||
|
db.insert_seed_named(
|
||||||
|
"t60000_g5_he-4_c-4_n-3.489798_o-1_ladder",
|
||||||
|
&neighbor,
|
||||||
|
"/tmp/seed.7",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
db.sync_all_running_workflows_completion().await.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
db.get_workflow("wf_rot").await.unwrap().unwrap().status,
|
||||||
|
"running",
|
||||||
|
"存在未试过的同族种子时,链尾 seed_step_stab 失败应阻塞翻转(rotation 待执行)"
|
||||||
|
);
|
||||||
|
|
||||||
|
// 轮换消费该种子(任务行携带 seed_point_name)→ 候选耗尽 → 放行。
|
||||||
|
let t2 = uuid::Uuid::new_v4();
|
||||||
|
db.insert_task(&common::models::TaskSpec {
|
||||||
|
task_id: t2,
|
||||||
|
point_name: name.clone(),
|
||||||
|
params: params.clone(),
|
||||||
|
seed_point_name: Some("t60000_g5_he-4_c-4_n-3.489798_o-1_ladder".to_string()),
|
||||||
|
timeout_sec: 7200,
|
||||||
|
workflow_name: Some("wf_rot".to_string()),
|
||||||
|
wave: 0,
|
||||||
|
tlusty_config: PhaseConfig {
|
||||||
|
strategies: vec!["seed_step_stab".to_string()],
|
||||||
|
..PhaseConfig::default_tlusty()
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
{
|
||||||
|
let pool = db.pool.clone();
|
||||||
|
let tid = t2.to_string();
|
||||||
|
tokio::task::spawn_blocking(move || {
|
||||||
|
let conn = pool.get().unwrap();
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE tasks SET status = 'failed', completed_at = datetime('now'), \
|
||||||
|
created_at = datetime('now', '+1 hour') WHERE task_id = ?1",
|
||||||
|
params![tid],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
db.sync_all_running_workflows_completion().await.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
db.get_workflow("wf_rot").await.unwrap().unwrap().status,
|
||||||
|
"completed",
|
||||||
|
"同族种子全部用过(无新候选)后应放行翻转"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 完成 flip 的阶段归因回归(2026-08-22 审查补充):synspec 失败行的
|
||||||
|
/// tlusty_strategies 是完整审计副本(scheduler.rs synspec 回退刻意保留),
|
||||||
|
/// 阻塞判定必须按 failed_stage='synspec' 只看 synspec 链——否则 synspec
|
||||||
|
/// 工作流会因 stale 审计副本永久卡 running。
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_workflow_flip_stage_attribution_ignores_tlusty_audit_copy() {
|
||||||
|
let temp_dir = tempfile::tempdir().unwrap();
|
||||||
|
let db = Database::new(
|
||||||
|
&temp_dir
|
||||||
|
.path()
|
||||||
|
.join("flip_syn_db.db")
|
||||||
|
.to_string_lossy(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let params = GridPointParams {
|
||||||
|
teff: 30000.0.into(),
|
||||||
|
logg: 5.0.into(),
|
||||||
|
loghe: (-2.0).into(),
|
||||||
|
logc: (-2.0).into(),
|
||||||
|
logn: (-2.0).into(),
|
||||||
|
logo: (-2.0).into(),
|
||||||
|
};
|
||||||
|
let name = params.model_name();
|
||||||
|
db.upsert_workflow("wf_syn2", None, "config", "idle")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
db.update_workflow_status("wf_syn2", "running").await.unwrap();
|
||||||
|
db.upsert_grid_point(¶ms, 0, "wf_syn2").await.unwrap();
|
||||||
|
db.update_grid_status(&name, GridPointStatus::Failed, "wf_syn2")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// synspec 失败行:synspec 链已耗尽(长度 1),但 tlusty 链是完整审计
|
||||||
|
// 副本(长度 3)——阶段归因下不应阻塞。
|
||||||
|
let t1 = uuid::Uuid::new_v4();
|
||||||
|
db.insert_task(&common::models::TaskSpec {
|
||||||
|
task_id: t1,
|
||||||
|
point_name: name.clone(),
|
||||||
|
params: params.clone(),
|
||||||
|
timeout_sec: 7200,
|
||||||
|
workflow_name: Some("wf_syn2".to_string()),
|
||||||
|
wave: 0,
|
||||||
|
tlusty_config: PhaseConfig {
|
||||||
|
strategies: vec![
|
||||||
|
"cold_run".to_string(),
|
||||||
|
"seed_step".to_string(),
|
||||||
|
"seed_step_stab".to_string(),
|
||||||
|
],
|
||||||
|
..PhaseConfig::default_tlusty()
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
{
|
||||||
|
let pool = db.pool.clone();
|
||||||
|
let tid = t1.to_string();
|
||||||
|
tokio::task::spawn_blocking(move || {
|
||||||
|
let conn = pool.get().unwrap();
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE tasks SET status = 'failed', failed_stage = 'synspec', \
|
||||||
|
synspec_strategies = '[\"standard\"]', completed_at = datetime('now') \
|
||||||
|
WHERE task_id = ?1",
|
||||||
|
params![tid],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
db.sync_all_running_workflows_completion().await.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
db.get_workflow("wf_syn2").await.unwrap().unwrap().status,
|
||||||
|
"completed",
|
||||||
|
"synspec 链已耗尽的失败行,其 tlusty 审计副本不应阻塞完成翻转"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_take_pending_node_token_atomic() {
|
async fn test_take_pending_node_token_atomic() {
|
||||||
let temp_dir = tempfile::tempdir().unwrap();
|
let temp_dir = tempfile::tempdir().unwrap();
|
||||||
@@ -3666,6 +4170,7 @@ mod tests {
|
|||||||
temp_check: None,
|
temp_check: None,
|
||||||
emflux_check: None,
|
emflux_check: None,
|
||||||
bfac_check: None,
|
bfac_check: None,
|
||||||
|
ladder_seeds: Vec::new(),
|
||||||
note: None,
|
note: None,
|
||||||
};
|
};
|
||||||
let report1 = TaskReport {
|
let report1 = TaskReport {
|
||||||
@@ -3732,6 +4237,7 @@ mod tests {
|
|||||||
temp_check: None,
|
temp_check: None,
|
||||||
emflux_check: None,
|
emflux_check: None,
|
||||||
bfac_check: None,
|
bfac_check: None,
|
||||||
|
ladder_seeds: Vec::new(),
|
||||||
note: None,
|
note: None,
|
||||||
};
|
};
|
||||||
let report2 = TaskReport {
|
let report2 = TaskReport {
|
||||||
@@ -3898,6 +4404,7 @@ mod tests {
|
|||||||
temp_check: None,
|
temp_check: None,
|
||||||
emflux_check: None,
|
emflux_check: None,
|
||||||
bfac_check: None,
|
bfac_check: None,
|
||||||
|
ladder_seeds: Vec::new(),
|
||||||
note: None,
|
note: None,
|
||||||
};
|
};
|
||||||
let report1 = TaskReport {
|
let report1 = TaskReport {
|
||||||
@@ -3955,6 +4462,7 @@ mod tests {
|
|||||||
temp_check: None,
|
temp_check: None,
|
||||||
emflux_check: None,
|
emflux_check: None,
|
||||||
bfac_check: None,
|
bfac_check: None,
|
||||||
|
ladder_seeds: Vec::new(),
|
||||||
note: Some("synspec 失败".into()),
|
note: Some("synspec 失败".into()),
|
||||||
};
|
};
|
||||||
let report2 = TaskReport {
|
let report2 = TaskReport {
|
||||||
@@ -4079,6 +4587,7 @@ mod tests {
|
|||||||
temp_check: None,
|
temp_check: None,
|
||||||
emflux_check: None,
|
emflux_check: None,
|
||||||
bfac_check: None,
|
bfac_check: None,
|
||||||
|
ladder_seeds: Vec::new(),
|
||||||
note: None,
|
note: None,
|
||||||
};
|
};
|
||||||
let report1 = TaskReport {
|
let report1 = TaskReport {
|
||||||
@@ -4154,6 +4663,7 @@ mod tests {
|
|||||||
temp_check: None,
|
temp_check: None,
|
||||||
emflux_check: None,
|
emflux_check: None,
|
||||||
bfac_check: None,
|
bfac_check: None,
|
||||||
|
ladder_seeds: Vec::new(),
|
||||||
note: None,
|
note: None,
|
||||||
};
|
};
|
||||||
let report2 = TaskReport {
|
let report2 = TaskReport {
|
||||||
|
|||||||
@@ -196,4 +196,60 @@ impl Database {
|
|||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 仅在 exact_family(同 Teff/logg/logHe、仅 CNO 不同)内找种子,供
|
||||||
|
/// `seed_step_stab` 稳定化策略使用(2026-08-18,docs/failed81_cno_seed_popzer_dpsilg_2026_08_18.md)。
|
||||||
|
///
|
||||||
|
/// 与 `find_best_seed_from_db` 的区别:不做 global 退化——稳定化配方的验证前提
|
||||||
|
/// 是种子与目标仅差 CNO 丰度(微扰),跨 Teff/logg 的种子不在其适用域内。
|
||||||
|
/// `exclude` 排除种子名列表(至少含目标点自身名,防止误把自己旧产物当种子)。
|
||||||
|
/// 2026-08-20 种子轮换修复:稳定化配方对种子**逐点敏感**——同 CNO 距离的不同
|
||||||
|
/// 邻居(换 N 还是换 O)收敛性不同。调用方传入本点历史任务已用过的种子名,
|
||||||
|
/// 使每次 seed_step_stab 重试轮换到下一个未试过的同族邻居,而非重复同一组合。
|
||||||
|
pub async fn find_exact_family_seed_from_db(
|
||||||
|
&self,
|
||||||
|
target: &GridPointParams,
|
||||||
|
exclude: &[String],
|
||||||
|
) -> Result<Option<common::seed_finder::SeedMatch>> {
|
||||||
|
let exact_candidates: Vec<SeedCacheItem> = {
|
||||||
|
let idx_lock = self.seed_index.read().await;
|
||||||
|
let keys = SeedBucketKey::from_params(target);
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for key in keys {
|
||||||
|
if let Some(bucket) = idx_lock.get(&key) {
|
||||||
|
out.extend(bucket.iter().cloned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut best: Option<(String, std::path::PathBuf, f64)> = None;
|
||||||
|
for item in &exact_candidates {
|
||||||
|
if exclude.iter().any(|ex| &item.point_name == ex) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 严格同物理族(2026-08-19 生产修复):必须 Teff/logg/logHe 逐值相同、
|
||||||
|
// 仅 CNO 不同。不能用 calculate_seed_distance 的 is_exact——其判定
|
||||||
|
// 容忍 ΔTeff≤5000K(一个网格档),会把 ladder 中间种子(如
|
||||||
|
// t22500_g6.5_...)当成同族并因 CNO 距离 0 排最前,而稳定化配方
|
||||||
|
// (POPZER+DPSILG)的验证前提是种子与目标仅差丰度微扰、同温同重力。
|
||||||
|
let strict_same_family = (item.params.teff.value() - target.teff.value()).abs()
|
||||||
|
< 1e-9
|
||||||
|
&& (item.params.logg.value() - target.logg.value()).abs() < 1e-9
|
||||||
|
&& (item.params.loghe.value() - target.loghe.value()).abs() < 1e-9;
|
||||||
|
if !strict_same_family {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let (_, d) = common::seed_finder::calculate_seed_distance(&item.params, target);
|
||||||
|
if best.is_none() || d < best.as_ref().unwrap().2 {
|
||||||
|
let path = std::path::PathBuf::from(&item.file_path);
|
||||||
|
best = Some((item.point_name.clone(), path, d));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(best.map(|(name, path, distance)| common::seed_finder::SeedMatch {
|
||||||
|
name,
|
||||||
|
path,
|
||||||
|
distance,
|
||||||
|
}))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -292,6 +292,33 @@ impl Database {
|
|||||||
/// `strategy` 传 Some("seed_step")/Some("cold_run") 时按当前策略过滤(Phase 6 起派生:
|
/// `strategy` 传 Some("seed_step")/Some("cold_run") 时按当前策略过滤(Phase 6 起派生:
|
||||||
/// 比较 `json_extract(tlusty_strategies, '$[0]')`,不再依赖已删除的 task_type 列),
|
/// 比较 `json_extract(tlusty_strategies, '$[0]')`,不再依赖已删除的 task_type 列),
|
||||||
/// None 不过滤。生产调用仅传 None,Some 分支供测试断言用。
|
/// None 不过滤。生产调用仅传 None,Some 分支供测试断言用。
|
||||||
|
/// 本点历史任务已用过的全部种子名(DISTINCT、非 NULL)。
|
||||||
|
/// 2026-08-20 种子轮换修复:`seed_step_stab` 重试时排除这些种子,
|
||||||
|
/// 使 `find_exact_family_seed_from_db` 轮换到未试过的同族 CNO 邻居。
|
||||||
|
pub async fn list_used_seed_names(
|
||||||
|
&self,
|
||||||
|
point_name: &str,
|
||||||
|
workflow_name: &str,
|
||||||
|
) -> Result<Vec<String>> {
|
||||||
|
let pool = self.pool.clone();
|
||||||
|
let point = point_name.to_string();
|
||||||
|
let wf = workflow_name.to_string();
|
||||||
|
tokio::task::spawn_blocking(move || -> Result<Vec<String>> {
|
||||||
|
let conn = pool.get().map_err(|e| anyhow::anyhow!("DB Pool Error: {}", e))?;
|
||||||
|
let mut stmt = conn.prepare(
|
||||||
|
"SELECT DISTINCT seed_point_name FROM tasks
|
||||||
|
WHERE point_name = ?1 AND workflow_name = ?2 AND seed_point_name IS NOT NULL",
|
||||||
|
)?;
|
||||||
|
let rows = stmt.query_map(params![point, wf], |r| r.get::<_, String>(0))?;
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for r in rows {
|
||||||
|
out.push(r?);
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
})
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn has_pending_tasks_for_point(
|
pub async fn has_pending_tasks_for_point(
|
||||||
&self,
|
&self,
|
||||||
point_name: &str,
|
point_name: &str,
|
||||||
@@ -621,8 +648,14 @@ impl Database {
|
|||||||
// 审查修复 #S1:workflow 完成 flip 并入同一事务(原实现在 tx.commit() 后单独 UPDATE,
|
// 审查修复 #S1:workflow 完成 flip 并入同一事务(原实现在 tx.commit() 后单独 UPDATE,
|
||||||
// 崩溃窗口期 task 已终态但 workflow 卡 running;且 `let _ =` 丢弃 I/O 错误)。
|
// 崩溃窗口期 task 已终态但 workflow 卡 running;且 `let _ =` 丢弃 I/O 错误)。
|
||||||
// 现在事务内更新,与 task/grid_points 结算原子提交,错误正常传播。
|
// 现在事务内更新,与 task/grid_points 结算原子提交,错误正常传播。
|
||||||
|
//
|
||||||
|
// 2026-08-22 修复:「无 pending/queued/running」之外还须无「未消费回退链」的
|
||||||
|
// failed 点(UNCONSUMED_FALLBACK_BLOCKER_SQL)——否则最后一个活跃点 cold_run
|
||||||
|
// 失败的这份上报会先把 workflow 置 completed,紧随其后的
|
||||||
|
// trigger_strategy_fallback 被 still_running 守卫拦截,链上后续策略永不派发。
|
||||||
tx.execute(
|
tx.execute(
|
||||||
"UPDATE workflows
|
&format!(
|
||||||
|
"UPDATE workflows
|
||||||
SET status = 'completed', updated_at = datetime('now')
|
SET status = 'completed', updated_at = datetime('now')
|
||||||
WHERE name = ?1
|
WHERE name = ?1
|
||||||
AND status = 'running'
|
AND status = 'running'
|
||||||
@@ -631,7 +664,10 @@ impl Database {
|
|||||||
SELECT 1 FROM grid_points
|
SELECT 1 FROM grid_points
|
||||||
WHERE workflow_name = workflows.name
|
WHERE workflow_name = workflows.name
|
||||||
AND status IN ('pending', 'queued', 'running')
|
AND status IN ('pending', 'queued', 'running')
|
||||||
)",
|
)
|
||||||
|
{}",
|
||||||
|
super::workflows::UNCONSUMED_FALLBACK_BLOCKER_SQL
|
||||||
|
),
|
||||||
params![wf],
|
params![wf],
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,78 @@
|
|||||||
//! `impl Database` 的 工作流 域方法。共享基础设施(Database struct、连接管理、类型、helper)见父模块 `super`(crate::db)。
|
//! `impl Database` 的 工作流 域方法。共享基础设施(Database struct、连接管理、类型、helper)见父模块 `super`(crate::db)。
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
/// 工作流完成 flip 的「未消费回退」阻塞子句(两条 flip SQL 共用:
|
||||||
|
/// 本文件后台对账 + tasks.rs `record_task_report` 事务内 flip)。
|
||||||
|
///
|
||||||
|
/// 2026-08-22 修复:最后一批活跃点失败时,flip 只看「无 pending/queued/running」,
|
||||||
|
/// 而 failed 点的失败上报**先于**策略链回退被处理——workflow 被提前置 completed,
|
||||||
|
/// `trigger_strategy_fallback` 的 still_running 守卫随即拦截,cold_run 之后的
|
||||||
|
/// seed_step/seed_step_stab 永不派发(生产实证:sdB_cno 仅剩
|
||||||
|
/// t60000_g5.0_he-4_c-4_n-4_o-1 时 cold 失败即终局)。
|
||||||
|
///
|
||||||
|
/// 阻塞语义(对每个 failed 点取最新 failed/timeout 任务行,行选择口径镜像
|
||||||
|
/// `pop_stage_strategy_for_fallback`:ORDER BY created_at DESC, rowid DESC):
|
||||||
|
///
|
||||||
|
/// 1. **未消费策略链**:按该行 failed_stage 归因(镜像 pop 的 stage→列映射,
|
||||||
|
/// NULL 归因 tlusty)取对应策略链,json 长度 >1——弹掉刚失败首项后仍有
|
||||||
|
/// 顺位。链随每次失败上报严格变短,终会耗尽,不会永久阻塞。
|
||||||
|
/// 2. **种子轮换待执行**(2026-08-22 审查补充):链尾 [`seed_step_stab`] 失败
|
||||||
|
/// 且仍存在未试过的同物理族干净种子(teff/logg/loghe 逐值相等、is_clean=1、
|
||||||
|
/// 非自身、不在本点历史 seed_point_name 集合内)——镜像 scheduler.rs
|
||||||
|
/// rotation 臂(trigger_strategy_fallback 内)的派发条件,否则链尾失败时
|
||||||
|
/// flip 仍会先于 rotation 吞掉换种重试。轮换每次消耗一个种子,used 集合
|
||||||
|
/// 单调增长,终会耗尽同族种子,不会永久阻塞。
|
||||||
|
///
|
||||||
|
/// JSON 防护:json_array_length/json_extract 对非法 JSON **抛错**而非返回 NULL
|
||||||
|
/// (SQLite 3.45 实测)——若不加防护,一条脏行会让 record_task_report 整个
|
||||||
|
/// 结算事务失败。列 NOT NULL DEFAULT 且唯一写入方是 serde_json,正常不可达;
|
||||||
|
/// json_valid 守卫下脏行按长度 0 处理(保守放行翻转)。
|
||||||
|
pub(crate) const UNCONSUMED_FALLBACK_BLOCKER_SQL: &str = "AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM grid_points gp
|
||||||
|
JOIN tasks t
|
||||||
|
ON t.point_name = gp.name
|
||||||
|
AND t.workflow_name = gp.workflow_name
|
||||||
|
AND t.status IN ('failed', 'timeout')
|
||||||
|
AND t.rowid = (
|
||||||
|
SELECT t2.rowid FROM tasks t2
|
||||||
|
WHERE t2.point_name = gp.name
|
||||||
|
AND t2.workflow_name = gp.workflow_name
|
||||||
|
AND t2.status IN ('failed', 'timeout')
|
||||||
|
ORDER BY t2.created_at DESC, t2.rowid DESC LIMIT 1
|
||||||
|
)
|
||||||
|
WHERE gp.workflow_name = workflows.name
|
||||||
|
AND gp.status = 'failed'
|
||||||
|
AND (
|
||||||
|
CASE WHEN t.failed_stage = 'synspec'
|
||||||
|
THEN CASE WHEN json_valid(t.synspec_strategies)
|
||||||
|
THEN json_array_length(t.synspec_strategies)
|
||||||
|
ELSE 0 END
|
||||||
|
ELSE CASE WHEN json_valid(t.tlusty_strategies)
|
||||||
|
THEN json_array_length(t.tlusty_strategies)
|
||||||
|
ELSE 0 END
|
||||||
|
END > 1
|
||||||
|
OR (
|
||||||
|
json_valid(t.tlusty_strategies)
|
||||||
|
AND json_array_length(t.tlusty_strategies) = 1
|
||||||
|
AND json_extract(t.tlusty_strategies, '$[0]') = 'seed_step_stab'
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1 FROM seeds s
|
||||||
|
WHERE s.is_clean = 1
|
||||||
|
AND s.point_name != gp.name
|
||||||
|
AND s.teff = gp.teff
|
||||||
|
AND s.logg = gp.logg
|
||||||
|
AND s.loghe = gp.loghe
|
||||||
|
AND s.point_name NOT IN (
|
||||||
|
SELECT t3.seed_point_name FROM tasks t3
|
||||||
|
WHERE t3.point_name = gp.name
|
||||||
|
AND t3.workflow_name = gp.workflow_name
|
||||||
|
AND t3.seed_point_name IS NOT NULL
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)";
|
||||||
|
|
||||||
impl Database {
|
impl Database {
|
||||||
pub async fn upsert_workflow(
|
pub async fn upsert_workflow(
|
||||||
&self,
|
&self,
|
||||||
@@ -47,8 +119,10 @@ impl Database {
|
|||||||
// L6 修复:原 `let _ =` 丢弃 r2d2/rusqlite 错误——若该后台对账 UPDATE 失败,
|
// L6 修复:原 `let _ =` 丢弃 r2d2/rusqlite 错误——若该后台对账 UPDATE 失败,
|
||||||
// 工作流可持续卡在 running 无任何提示。现记录错误(仍返回 Ok 不中断主流程,
|
// 工作流可持续卡在 running 无任何提示。现记录错误(仍返回 Ok 不中断主流程,
|
||||||
// 因为每份上报内的 workflow-completion flip 才是主路径,见 record_task_report)。
|
// 因为每份上报内的 workflow-completion flip 才是主路径,见 record_task_report)。
|
||||||
|
// 「未消费回退链」子句见 UNCONSUMED_FALLBACK_BLOCKER_SQL(2026-08-22 修复)。
|
||||||
if let Err(e) = conn.execute(
|
if let Err(e) = conn.execute(
|
||||||
"UPDATE workflows
|
&format!(
|
||||||
|
"UPDATE workflows
|
||||||
SET status = 'completed', updated_at = datetime('now')
|
SET status = 'completed', updated_at = datetime('now')
|
||||||
WHERE status = 'running'
|
WHERE status = 'running'
|
||||||
AND EXISTS (SELECT 1 FROM grid_points WHERE workflow_name = workflows.name)
|
AND EXISTS (SELECT 1 FROM grid_points WHERE workflow_name = workflows.name)
|
||||||
@@ -56,7 +130,10 @@ impl Database {
|
|||||||
SELECT 1 FROM grid_points
|
SELECT 1 FROM grid_points
|
||||||
WHERE workflow_name = workflows.name
|
WHERE workflow_name = workflows.name
|
||||||
AND status IN ('pending', 'queued', 'running')
|
AND status IN ('pending', 'queued', 'running')
|
||||||
)",
|
)
|
||||||
|
{}",
|
||||||
|
UNCONSUMED_FALLBACK_BLOCKER_SQL
|
||||||
|
),
|
||||||
[],
|
[],
|
||||||
) {
|
) {
|
||||||
tracing::error!("后台对账:同步 running 工作流完成态失败: {}", e);
|
tracing::error!("后台对账:同步 running 工作流完成态失败: {}", e);
|
||||||
@@ -90,15 +167,21 @@ impl Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 一次 GROUP BY 聚合全部工作流的网格计数并回填(不做逐工作流查询,无 N+1)。
|
// 一次 GROUP BY 聚合全部工作流的网格计数并回填(不做逐工作流查询,无 N+1)。
|
||||||
|
// tlusty 总收敛与 stab 分项为 2026-08-25 补(口径见 WorkflowStats.tlusty_converged)。
|
||||||
let mut agg_stmt = conn.prepare(
|
let mut agg_stmt = conn.prepare(
|
||||||
"SELECT workflow_name,
|
"SELECT workflow_name,
|
||||||
COUNT(*) AS total,
|
COUNT(*) AS total,
|
||||||
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 = 'converged' THEN 1 ELSE 0 END) AS tlusty_converged,
|
||||||
|
SUM(CASE WHEN tlusty_status = 'converged' AND tlusty_success_method = 'seed_step_stab' THEN 1 ELSE 0 END) AS stab,
|
||||||
|
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 +194,12 @@ 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_converged: r.get::<_, Option<i64>>(7)?.unwrap_or(0),
|
||||||
|
seed_step_stab_converged: r.get::<_, Option<i64>>(8)?.unwrap_or(0),
|
||||||
|
tlusty_failed: r.get::<_, Option<i64>>(9)?.unwrap_or(0),
|
||||||
|
synspec_converged: r.get::<_, Option<i64>>(10)?.unwrap_or(0),
|
||||||
|
synspec_failed: r.get::<_, Option<i64>>(11)?.unwrap_or(0),
|
||||||
|
synspec_pending: r.get::<_, Option<i64>>(12)?.unwrap_or(0),
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|||||||
@@ -176,6 +176,32 @@ pub const MIGRATIONS: &[Migration] = &[
|
|||||||
detect: |c| Ok(!has_column(c, "grid_points", "success_method")?),
|
detect: |c| Ok(!has_column(c, "grid_points", "success_method")?),
|
||||||
up: &["ALTER TABLE grid_points DROP COLUMN success_method"],
|
up: &["ALTER TABLE grid_points DROP COLUMN success_method"],
|
||||||
},
|
},
|
||||||
|
// M14(2026-08-25,大气收敛统计补漏):Phase 5b 阶段列(tlusty_status)上线前完成的历史
|
||||||
|
// 点该列为 NULL——但 tlusty_success_method 仅在整管线成功(tlusty_enabled=1 且收敛)时
|
||||||
|
// 写入(record_task_report 成功分支),故 completed + method 非空 ⟹ 大气必已收敛。
|
||||||
|
// 回填 'converged',否则 stats 的 CASE WHEN tlusty_status='converged' 漏计
|
||||||
|
// (生产实证:sdB_cno 9216 点中 9 个此形态,前端 9137/9216 差额的一部分)。
|
||||||
|
Migration {
|
||||||
|
version: 14,
|
||||||
|
name: "backfill-tlusty-status-for-completed",
|
||||||
|
detect: |c| {
|
||||||
|
let n: i64 = c.query_row(
|
||||||
|
"SELECT COUNT(*) FROM grid_points
|
||||||
|
WHERE status = 'completed'
|
||||||
|
AND COALESCE(tlusty_success_method, '') != ''
|
||||||
|
AND COALESCE(tlusty_status, '') = ''",
|
||||||
|
[],
|
||||||
|
|r| r.get(0),
|
||||||
|
)?;
|
||||||
|
Ok(n == 0)
|
||||||
|
},
|
||||||
|
up: &[
|
||||||
|
"UPDATE grid_points SET tlusty_status = 'converged' \
|
||||||
|
WHERE status = 'completed' \
|
||||||
|
AND COALESCE(tlusty_success_method, '') != '' \
|
||||||
|
AND COALESCE(tlusty_status, '') = ''",
|
||||||
|
],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
/// 当前 schema 版本(`PRAGMA user_version`)。
|
/// 当前 schema 版本(`PRAGMA user_version`)。
|
||||||
@@ -562,4 +588,54 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(current_version(&conn).unwrap(), 13);
|
assert_eq!(current_version(&conn).unwrap(), 13);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// M14(2026-08-25):completed + tlusty_success_method 非空 + tlusty_status 空
|
||||||
|
/// 的历史点回填 'converged'(method 仅在整管线成功时写入);其它形态不动。
|
||||||
|
#[test]
|
||||||
|
fn m14_backfills_tlusty_status_for_legacy_completed() {
|
||||||
|
let mut conn = mem_conn();
|
||||||
|
conn.execute_batch(
|
||||||
|
"CREATE TABLE grid_points (
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
workflow_name TEXT NOT NULL,
|
||||||
|
status TEXT NOT NULL DEFAULT 'pending',
|
||||||
|
tlusty_success_method TEXT,
|
||||||
|
synspec_success_method TEXT,
|
||||||
|
tlusty_status TEXT,
|
||||||
|
synspec_status TEXT,
|
||||||
|
pending_strategies TEXT
|
||||||
|
);
|
||||||
|
INSERT INTO grid_points (name, workflow_name, status, tlusty_success_method, tlusty_status) VALUES
|
||||||
|
('legacy_ok', 'wf', 'completed', 'cold_run', NULL),
|
||||||
|
('legacy_seed', 'wf', 'completed', 'seed_step', ''),
|
||||||
|
('already_failed', 'wf', 'failed', 'cold_run', 'failed'),
|
||||||
|
('already_conv', 'wf', 'completed', 'cold_run', 'converged'),
|
||||||
|
('no_method', 'wf', 'completed', NULL, NULL);",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// 仅跑 M14(全量 MIGRATIONS 含 M1 的 tasks 迁移,本夹具只建 grid_points)。
|
||||||
|
let m14 = MIGRATIONS.iter().find(|m| m.version == 14).unwrap();
|
||||||
|
apply_migrations_with(&mut conn, &[*m14]).unwrap();
|
||||||
|
|
||||||
|
let read = |c: &Connection, name: &str| -> Option<String> {
|
||||||
|
c.query_row(
|
||||||
|
"SELECT tlusty_status FROM grid_points WHERE name = ?1",
|
||||||
|
rusqlite::params![name],
|
||||||
|
|r| r.get(0),
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
};
|
||||||
|
assert_eq!(read(&conn, "legacy_ok").as_deref(), Some("converged"));
|
||||||
|
assert_eq!(read(&conn, "legacy_seed").as_deref(), Some("converged"));
|
||||||
|
// failed 点 / 已有状态 / 无 method 的点不受影响。
|
||||||
|
assert_eq!(read(&conn, "already_failed").as_deref(), Some("failed"));
|
||||||
|
assert_eq!(read(&conn, "already_conv").as_deref(), Some("converged"));
|
||||||
|
assert_eq!(read(&conn, "no_method"), None);
|
||||||
|
assert_eq!(current_version(&conn).unwrap(), 14);
|
||||||
|
|
||||||
|
// 幂等:再跑一遍 detect 命中(无待回填行)→ 仅推进版本,不改数据。
|
||||||
|
apply_migrations_with(&mut conn, &[*m14]).unwrap();
|
||||||
|
assert_eq!(read(&conn, "no_method"), None);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -352,6 +360,7 @@ impl GridScheduler {
|
|||||||
/// 「弹出再压回」保持原序而陷入死循环。
|
/// 「弹出再压回」保持原序而陷入死循环。
|
||||||
async fn resolve_dispatchable_chain(
|
async fn resolve_dispatchable_chain(
|
||||||
db: &Database,
|
db: &Database,
|
||||||
|
workflow_name: &str,
|
||||||
params: &GridPointParams,
|
params: &GridPointParams,
|
||||||
mut chain: Vec<String>,
|
mut chain: Vec<String>,
|
||||||
) -> Result<(Vec<String>, Option<String>)> {
|
) -> Result<(Vec<String>, Option<String>)> {
|
||||||
@@ -386,6 +395,36 @@ impl GridScheduler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 稳定化种子步进(2026-08-18,docs/failed81_cno_seed_popzer_dpsilg_2026_08_18.md):
|
||||||
|
// 仅在 exact_family(同 Teff/logg/logHe、仅 CNO 不同)内找种子——稳定化配方
|
||||||
|
// (POPZER+DPSILG)的验证前提是种子与目标仅差丰度微扰,跨 Teff/logg 种子
|
||||||
|
// 不在适用域。排除目标点自身名(防止把自己旧产物当种子)。
|
||||||
|
// 2026-08-20 种子轮换:配方对种子逐点敏感(同距离换 N/换 O 邻居收敛性不同),
|
||||||
|
// 排除本点历史任务已用过的种子,重试时轮换到下一个未试过的同族邻居。
|
||||||
|
Some("seed_step_stab") => {
|
||||||
|
let mut exclude = vec![params.model_name()];
|
||||||
|
exclude.extend(db.list_used_seed_names(¶ms.model_name(), workflow_name).await?);
|
||||||
|
let found_seed = db
|
||||||
|
.find_exact_family_seed_from_db(params, &exclude)
|
||||||
|
.await?;
|
||||||
|
match found_seed {
|
||||||
|
Some(seed) => {
|
||||||
|
if !skipped.is_empty() {
|
||||||
|
chain.extend(skipped);
|
||||||
|
}
|
||||||
|
return Ok((chain, Some(seed.name.clone())));
|
||||||
|
}
|
||||||
|
None if chain.len() > 1 => {
|
||||||
|
let head = chain.remove(0);
|
||||||
|
skipped.push(head);
|
||||||
|
info!("策略解析:seed_step_stab 无同物理族 CNO 邻居种子,暂存顺位");
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
chain.clear();
|
||||||
|
return Ok((chain, None));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// 空链或非 seed_step 顺位:可直接派发。先前跳过的 seed_step 追加到链尾,
|
// 空链或非 seed_step 顺位:可直接派发。先前跳过的 seed_step 追加到链尾,
|
||||||
// 保留为后续回退顺位(空链由调用方判空处理)。
|
// 保留为后续回退顺位(空链由调用方判空处理)。
|
||||||
_ => {
|
_ => {
|
||||||
@@ -451,6 +490,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,
|
||||||
@@ -552,7 +592,7 @@ impl GridScheduler {
|
|||||||
};
|
};
|
||||||
let (dispatch_chain, seed_point_name) = if tlusty_cfg.enabled {
|
let (dispatch_chain, seed_point_name) = if tlusty_cfg.enabled {
|
||||||
let (chain, seed) =
|
let (chain, seed) =
|
||||||
Self::resolve_dispatchable_chain(&self.db, ¶ms, base_chain).await?;
|
Self::resolve_dispatchable_chain(&self.db, workflow_name, ¶ms, base_chain).await?;
|
||||||
if chain.is_empty() {
|
if chain.is_empty() {
|
||||||
// enabled 阶段无可派发顺位(基础链全为无种子的 seed_step)→ 打回 pending,
|
// enabled 阶段无可派发顺位(基础链全为无种子的 seed_step)→ 打回 pending,
|
||||||
// 待近邻种子出现后由下轮调度自愈派发(不是终态,不能被卡死在 queued)。
|
// 待近邻种子出现后由下轮调度自愈派发(不是终态,不能被卡死在 queued)。
|
||||||
@@ -615,6 +655,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?;
|
||||||
@@ -886,13 +927,6 @@ impl GridScheduler {
|
|||||||
popped,
|
popped,
|
||||||
policy: row_policy,
|
policy: row_policy,
|
||||||
} = snap;
|
} = snap;
|
||||||
if rest_strategies.is_empty() {
|
|
||||||
info!(
|
|
||||||
"策略回退:网格点 {} 的 {} 策略链已耗尽(弹出最后项 {}),保持 failed 终态",
|
|
||||||
name, stage_label, popped
|
|
||||||
);
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2026-08-04 语义修正:策略链回退**只由策略链驱动**(弹出失败首项、派发下一顺位,
|
// 2026-08-04 语义修正:策略链回退**只由策略链驱动**(弹出失败首项、派发下一顺位,
|
||||||
// 链耗尽保持 failed),不再受执行策略门控——策略只决定启动工作流时对历史终态点的
|
// 链耗尽保持 failed),不再受执行策略门控——策略只决定启动工作流时对历史终态点的
|
||||||
@@ -904,6 +938,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,
|
||||||
@@ -918,6 +953,83 @@ impl GridScheduler {
|
|||||||
.await
|
.await
|
||||||
.unwrap_or((None, None, None, None, None, None, None, None));
|
.unwrap_or((None, None, None, None, None, None, None, None));
|
||||||
|
|
||||||
|
// 2026-08-20 种子轮换:seed_step_stab 常为链上最后一项,失败弹出后链空。
|
||||||
|
// 稳定化配方对种子逐点敏感(同 CNO 距离、换不同元素方向的邻居收敛性不同,
|
||||||
|
// 见 docs/failed81_cno_seed_popzer_dpsilg_2026_08_18.md §九)——首个选中
|
||||||
|
// 的邻居失败不代表配方无效。排除本点历史已用种子后若还有未试过的同族
|
||||||
|
// 邻居,则以 seed_step_stab 单顺位重链重派。轮换次数天然受同族邻居数约束:
|
||||||
|
// 全部试过后此处不再命中,落入下方 failed 终态,无死循环。
|
||||||
|
if rest_strategies.is_empty() && failed_stage != "synspec" && popped == "seed_step_stab" {
|
||||||
|
let mut exclude = vec![params.model_name()];
|
||||||
|
exclude.extend(self.db.list_used_seed_names(name, workflow_name).await?);
|
||||||
|
if let Some(fresh_seed) = self
|
||||||
|
.db
|
||||||
|
.find_exact_family_seed_from_db(params, &exclude)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
info!(
|
||||||
|
"策略回退:工作流 {} 网格点 {} seed_step_stab 失败但存在未试过的同族邻居种子 {},轮换种子重派(已试过 {:?})",
|
||||||
|
workflow_name, name, fresh_seed.name, &exclude[1..]
|
||||||
|
);
|
||||||
|
// 轮换任务显式注入新种子(绕过 resolve 的种子查找,避免其再次
|
||||||
|
// 命中同一邻居)。
|
||||||
|
let mut stab_cfg = tlusty_cfg.clone();
|
||||||
|
stab_cfg.strategies = vec!["seed_step_stab".to_string()];
|
||||||
|
stab_cfg.policy = row_policy.clone();
|
||||||
|
let task_spec = TaskSpec {
|
||||||
|
task_id: Uuid::new_v4(),
|
||||||
|
point_name: name.to_string(),
|
||||||
|
params: params.clone(),
|
||||||
|
seed_point_name: Some(fresh_seed.name),
|
||||||
|
timeout_sec,
|
||||||
|
workflow_name: Some(workflow_name.to_string()),
|
||||||
|
wave: 0,
|
||||||
|
tlusty_config: stab_cfg,
|
||||||
|
synspec_config: synspec_cfg.clone(),
|
||||||
|
synspec_params,
|
||||||
|
tlusty_chain_params: tlusty_chain.clone(),
|
||||||
|
seed_chain_params: seed_chain.clone(),
|
||||||
|
tlusty_input_params: tlusty_input.clone(),
|
||||||
|
atmosphere_ref: None,
|
||||||
|
energy_tolerance,
|
||||||
|
temp_max_factor,
|
||||||
|
temp_floor,
|
||||||
|
temp_ceiling,
|
||||||
|
emflux_tolerance,
|
||||||
|
convergence_min_ratio,
|
||||||
|
bfac_max,
|
||||||
|
bfac_min,
|
||||||
|
linelist: linelist.clone(),
|
||||||
|
};
|
||||||
|
self.db.insert_task(&task_spec).await?;
|
||||||
|
self.db
|
||||||
|
.update_grid_status(name, common::models::GridPointStatus::Queued, workflow_name)
|
||||||
|
.await?;
|
||||||
|
if let Err(e) = self.queue.push_task(&task_spec).await {
|
||||||
|
let _ = self
|
||||||
|
.db
|
||||||
|
.update_grid_status(
|
||||||
|
name,
|
||||||
|
common::models::GridPointStatus::Pending,
|
||||||
|
workflow_name,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let _ = self.queue.remove_task(&task_spec.task_id.to_string()).await;
|
||||||
|
let _ = self.db.delete_task(&task_spec.task_id).await;
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if rest_strategies.is_empty() {
|
||||||
|
info!(
|
||||||
|
"策略回退:网格点 {} 的 {} 策略链已耗尽(弹出最后项 {}),保持 failed 终态",
|
||||||
|
name, stage_label, popped
|
||||||
|
);
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
// SYNSPEC 链回退:重试光谱合成。无邻居种子门控(大气来自目标点自身既有产物,
|
// SYNSPEC 链回退:重试光谱合成。无邻居种子门控(大气来自目标点自身既有产物,
|
||||||
// 见 docs/task_engine_decoupling_design.md §5)——旧实现把 synspec 失败误归因到
|
// 见 docs/task_engine_decoupling_design.md §5)——旧实现把 synspec 失败误归因到
|
||||||
// TLUSTY 链并做无意义的种子搜索(修复审查 #2)。
|
// TLUSTY 链并做无意义的种子搜索(修复审查 #2)。
|
||||||
@@ -968,6 +1080,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
|
||||||
@@ -999,7 +1112,7 @@ impl GridScheduler {
|
|||||||
// pending_rest:resolve 前的基础剩余链(含全部 seed_step 顺位),H1 空链分支据此记录标记。
|
// pending_rest:resolve 前的基础剩余链(含全部 seed_step 顺位),H1 空链分支据此记录标记。
|
||||||
let pending_rest = rest_strategies.clone();
|
let pending_rest = rest_strategies.clone();
|
||||||
let (rest_strategies, seed_point_name) =
|
let (rest_strategies, seed_point_name) =
|
||||||
Self::resolve_dispatchable_chain(&self.db, params, rest_strategies).await?;
|
Self::resolve_dispatchable_chain(&self.db, workflow_name, params, rest_strategies).await?;
|
||||||
if rest_strategies.is_empty() {
|
if rest_strategies.is_empty() {
|
||||||
// H1 修复:resolve_dispatchable_chain 仅在「剩余顺位全为无近邻种子的 seed_step」
|
// H1 修复:resolve_dispatchable_chain 仅在「剩余顺位全为无近邻种子的 seed_step」
|
||||||
// 时返回空(调用方已在上方 line 790 排除了真耗尽)。此时**不能**保持 failed 终态:
|
// 时返回空(调用方已在上方 line 790 排除了真耗尽)。此时**不能**保持 failed 终态:
|
||||||
@@ -1062,6 +1175,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?;
|
||||||
@@ -2896,6 +3010,7 @@ tlusty_stage:\n enabled: true\n policy: skip_converged\n strategies: [seed_st
|
|||||||
// 无种子 → 重复 seed_step 链应判空(不派发、不死循环)。
|
// 无种子 → 重复 seed_step 链应判空(不派发、不死循环)。
|
||||||
let (chain, seed) = GridScheduler::resolve_dispatchable_chain(
|
let (chain, seed) = GridScheduler::resolve_dispatchable_chain(
|
||||||
&db,
|
&db,
|
||||||
|
"wf",
|
||||||
¶ms,
|
¶ms,
|
||||||
vec!["seed_step".to_string(), "seed_step".to_string()],
|
vec!["seed_step".to_string(), "seed_step".to_string()],
|
||||||
)
|
)
|
||||||
@@ -2908,6 +3023,7 @@ tlusty_stage:\n enabled: true\n policy: skip_converged\n strategies: [seed_st
|
|||||||
// 两个无种子 seed_step 保留为回退顺位且链首可派发。
|
// 两个无种子 seed_step 保留为回退顺位且链首可派发。
|
||||||
let (chain2, seed2) = GridScheduler::resolve_dispatchable_chain(
|
let (chain2, seed2) = GridScheduler::resolve_dispatchable_chain(
|
||||||
&db,
|
&db,
|
||||||
|
"wf",
|
||||||
¶ms,
|
¶ms,
|
||||||
vec![
|
vec![
|
||||||
"seed_step".to_string(),
|
"seed_step".to_string(),
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ async fn mark_imported(db: &Database, name: &str, wf: &str, params: &GridPointPa
|
|||||||
temp_check: None,
|
temp_check: None,
|
||||||
emflux_check: None,
|
emflux_check: None,
|
||||||
bfac_check: None,
|
bfac_check: None,
|
||||||
|
ladder_seeds: Vec::new(),
|
||||||
note: None,
|
note: None,
|
||||||
};
|
};
|
||||||
db.upsert_point_summary(name, wf, &summary, method)
|
db.upsert_point_summary(name, wf, &summary, method)
|
||||||
@@ -1866,6 +1867,7 @@ async fn dispatch_and_report(
|
|||||||
temp_check: None,
|
temp_check: None,
|
||||||
emflux_check: None,
|
emflux_check: None,
|
||||||
bfac_check: None,
|
bfac_check: None,
|
||||||
|
ladder_seeds: Vec::new(),
|
||||||
note: None,
|
note: None,
|
||||||
};
|
};
|
||||||
let report = common::models::TaskReport {
|
let report = common::models::TaskReport {
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ fn make_converged_summary(name: &str, params: &GridPointParams) -> ModelSummary
|
|||||||
temp_check: None,
|
temp_check: None,
|
||||||
emflux_check: None,
|
emflux_check: None,
|
||||||
bfac_check: None,
|
bfac_check: None,
|
||||||
|
ladder_seeds: Vec::new(),
|
||||||
note: None,
|
note: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,14 @@ 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_converged] - TLUSTY 视图:大气收敛总数(权威口径,不按策略拆;2026-08-25 补)
|
||||||
|
* @property {number} [seed_step_stab_converged] - TLUSTY 视图:稳定化种子收敛(2026-08-25 补)
|
||||||
|
* @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 +84,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,10 @@ body::before {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.seg-converged { background: var(--color-success); }
|
.seg-converged { background: var(--color-success); }
|
||||||
|
/* 种子步进段(TLUSTY 视图):紫色,与冷启动绿区分收敛策略。 */
|
||||||
|
.seg-seed { background: var(--accent-purple); }
|
||||||
|
/* 稳定化/waypoint 种子段(TLUSTY 视图,2026-08-25 补):青色,与普通种子步进区分。 */
|
||||||
|
.seg-stab { background: var(--accent-teal); }
|
||||||
/* 色盲友好(color-not-only):失败段在红色之上叠加斜线纹理,
|
/* 色盲友好(color-not-only):失败段在红色之上叠加斜线纹理,
|
||||||
即使无法分辨红/绿也能凭纹理识别失败占比。 */
|
即使无法分辨红/绿也能凭纹理识别失败占比。 */
|
||||||
.seg-failed {
|
.seg-failed {
|
||||||
@@ -1056,6 +1060,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 +1192,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);
|
||||||
@@ -1375,7 +1441,9 @@ body::before {
|
|||||||
/* 状态配色(与 method-badge/status-badge 配色族一致) */
|
/* 状态配色(与 method-badge/status-badge 配色族一致) */
|
||||||
.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-stab { fill: var(--accent-teal); }
|
||||||
.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); }
|
||||||
@@ -1383,7 +1451,9 @@ body::before {
|
|||||||
/* 色带默认 fill-opacity 已定,色块需不透明 → 用 stroke 不可,故色块状态类同时设 fill */
|
/* 色带默认 fill-opacity 已定,色块需不透明 → 用 stroke 不可,故色块状态类同时设 fill */
|
||||||
.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-stab { fill: var(--accent-teal); }
|
||||||
.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); }
|
||||||
@@ -1403,7 +1473,9 @@ 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-stab { fill: var(--accent-teal); 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,59 @@ 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', '种子步进'],
|
||||||
|
['stab', 'seg-stab', '稳定化种子'],
|
||||||
|
['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 === 'stab') return s.seed_step_stab_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 +74,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 +85,24 @@ 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} 待定`,
|
// tlusty_converged(2026-08-25):权威总数,不按 method 拆——此前 cold+seed 之和
|
||||||
`${s.failed} 失败`,
|
// 漏计 seed_step_stab 收敛点(生产 9137/9216)。旧 server 无该字段时回退三桶之和。
|
||||||
];
|
const conv = (s.tlusty_converged != null)
|
||||||
|
? s.tlusty_converged
|
||||||
|
: (s.cold_run_converged || 0) + (s.seed_step_converged || 0) + (s.seed_step_stab_converged || 0);
|
||||||
|
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 +116,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 +165,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 +213,34 @@ 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 {
|
||||||
|
// tlusty_converged 为权威总数(2026-08-25 补,见 renderStrip 同名注释),旧 server 回退三桶和。
|
||||||
|
const conv = (s.tlusty_converged != null)
|
||||||
|
? s.tlusty_converged
|
||||||
|
: (s.cold_run_converged || 0) + (s.seed_step_converged || 0) + (s.seed_step_stab_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} · 稳定化 ${s.seed_step_stab_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,46 @@ 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', 'stab', '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: '种子步进', stab: '稳定化种子', 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';
|
/** 点 → 状态档位 key(按选中阶段映射)。
|
||||||
if (p.tlusty_success_method === 'cold_run') return 'cold';
|
* TLUSTY 阶段:tlusty_status='converged' 按 tlusty_success_method 拆 cold/seed;'failed'→failed;
|
||||||
// 光谱阶段收敛(synspec-only/双阶段光谱归因落库)→ SYNSPEC 合成档。
|
* 否则按 grid_points.status 归 running/queued/pending。
|
||||||
if (p.synspec_success_method) return 'synspec';
|
* SYNSPEC 阶段:synspec_status='converged'→converged;'failed'→failed;其余→pending(含 NULL)。 */
|
||||||
return 'cold';
|
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 / 未知 → 未运行
|
||||||
}
|
}
|
||||||
if (p.status === 'failed') return 'failed';
|
// TLUSTY 视图
|
||||||
|
if (p.tlusty_status === 'converged') {
|
||||||
|
if (p.tlusty_success_method === 'seed_step') return 'seed';
|
||||||
|
if (p.tlusty_success_method === 'seed_step_stab') return 'stab';
|
||||||
|
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 +112,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 +211,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 +228,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 +403,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 +427,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 +500,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 +517,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 id="wf-strip-counts" class="wf-strip-counts tabular-num">正在获取执行数据…</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>
|
||||||
</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,151 @@
|
|||||||
|
# 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 生产化:ladder 策略已实现(2026-08-17)
|
||||||
|
|
||||||
|
三层回退架构(runner 内自动触发,零配置/调度器改动):
|
||||||
|
|
||||||
|
```
|
||||||
|
主链(nst 修复后) → nl_direct(污染种子跳过) → nl_ladder(连续步进) → nl_tight(能量边际)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **规划**:`plan_ladder_steps(seed, target)`——归一化间隔更大的轴,
|
||||||
|
步长 Δlogg≤0.25 / ΔTeff≤2.5kK,≤4 个中间步;双轴均在单步域内不触发。
|
||||||
|
种子参数由 executor 从权威 `seed_point_name` 解析(`parse_point_name`)。
|
||||||
|
- **执行**:每中间步 clone 终阶段 ChainStep(require_converged),前步收敛解作
|
||||||
|
后步 fort.8;链尾 `nl_ladder` 用目标参数收尾。任一步发散即中止。
|
||||||
|
- **持久化**:中间步「收敛 + fort.7 无 NaN」即登记 `ModelSummary.ladder_seeds` →
|
||||||
|
node executor 读快照字节随 TaskReport 上报(**任务失败也上传**)→ server
|
||||||
|
`POST /api/task/report` 落 `seeds_dir/<name>_ladder/` + seeds 表(ON CONFLICT
|
||||||
|
仅刷新 file_path,`_ladder` 后缀名与真实网格点零冲突;bucket 查找按数值列
|
||||||
|
匹配天然覆盖中间参数点)。簇内相邻失败点自动复用已收敛梯级。
|
||||||
|
- **重复网格点**:未来工作流若含与中间种子同参数的网格点,照常计算(种子≠结果),
|
||||||
|
seed_step 命中同参数种子后 ~1–2 迭代收敛——重验五重门槛,成本可忽略。
|
||||||
|
- 测试:`test_plan_ladder_steps`(规划纯函数)、`unit_ladder_fallback_wiring`
|
||||||
|
(假 tlusty 六调用接线:seed_nc/nl/nl_direct 失败→ladder_g5.75/ladder_g6/
|
||||||
|
nl_ladder 收敛 + ladder_seeds 登记)、`test_parse_point_name`。
|
||||||
|
物理有效性由手工 ladder 实测证明(§5.3,与生产同一 ChainStep 配置与种子)。
|
||||||
|
workspace 全测试 10 套件通过。
|
||||||
|
|
||||||
|
部署:server + node 重建重部署后,对 400 失败点发起重试即可(ladder 自动生效)。
|
||||||
|
|
||||||
|
## 七、第二轮重跑后 121 残点分析与 ladder 实测(2026-08-18)
|
||||||
|
|
||||||
|
### 7.1 121 残点分类(最新 DB,每点最新任务)
|
||||||
|
|
||||||
|
| 类别 | 数量 | 归因 |
|
||||||
|
|---|---:|---|
|
||||||
|
| NaN 伪收敛(否决未生效) | **32** | **全部来自 node-fmq-ubuntu-01 旧二进制**(其他节点同期已有 nl_tight 记录证明新二进制)——重新部署该节点即解决,随重试进入 nl_direct/正常链路 |
|
||||||
|
| 真发散(20kK/logg≥6 富氦簇) | ~62 | nl/nl_direct 均 1e16 STOP |
|
||||||
|
| 真发散(60kK/logg5.0 最贫 CNO 簇) | ~19 | 同上,且对一切种子/轴免疫 |
|
||||||
|
| 能量边际残留 | 2 | nl_tight 已触发(其一 tight 数值收敛但模型含 NaN 被否决) |
|
||||||
|
|
||||||
|
### 7.2 ladder 实测矩阵(12 点分层抽样 + 变体,test/20260817_failed400/)
|
||||||
|
|
||||||
|
| 变体 | 配置 | 结果 |
|
||||||
|
|---|---|---|
|
||||||
|
| ladder121 | 生产语义(nl 步进,Δlogg 0.25 / ΔTeff 2.5k) | **1/12** |
|
||||||
|
| ladder121_teff | 换 Teff 轴(同 CNO ±5kK rung) | +2/7 |
|
||||||
|
| ladder121_nc | 中间步 seed_nc 式(ilvlin=0 + ORELAX=0.3 + NITER=20) | 0/9(发散转"卡住") |
|
||||||
|
| ladder121_nc100 | 同上 + NITER=100 + 2× 细化(0.125/1.25k) | 0/9(单步可达 1e-4@34it,但链条后段单深度 POP 振荡 ~0.2 或 NITER 耗尽;每步最长达 1h+) |
|
||||||
|
| he_seed | 60k 簇换 he0 种子直接 nl | 1/4(t55000 完整收敛 83it/3.7e-4/emflux 1.0008) |
|
||||||
|
| ladder20k_final | 20k 簇 nc+细化+7200s/步 | 0/3(step3 处 iter100 单深度 0.199 振荡) |
|
||||||
|
|
||||||
|
### 7.3 结论
|
||||||
|
|
||||||
|
1. **部署项收益最大**:重新部署 ubuntu-01 → 直接回收 32 点(确定性)。
|
||||||
|
2. **ladder 对本轮残点增益有限**:全部变体合计 ~4-6/87。已实现的 ladder 保留
|
||||||
|
(自动触发、零额外成本、对上一轮 5 个验证点有效),但不应期望它清掉这批残点。
|
||||||
|
3. **残点本质**:20kK 富氦高重力簇在阻尼下不雪崩但呈**单深度 POP 极限环**
|
||||||
|
(iter 100 时个别深度 |ΔPOP|≈0.2 不衰减);60kK 贫 CNO 低重力簇从任何
|
||||||
|
CNO/He/logg/Teff 种子均在前几迭代雪崩。两者均已超出"步进/种子"工程层
|
||||||
|
可解范围,属 TLUSTY Newton 求解器在这些极端参数组合下的固有收敛域缺失。
|
||||||
|
4. **可行后续**(求解器层,需另行立项):针对极限环深度的逐深度阻尼/POPZER
|
||||||
|
策略调优;或物理上接受这 ~85 点为"不可收敛角"并在工作流中显式标记。
|
||||||
|
|
||||||
|
### 7.4 测试工件
|
||||||
|
|
||||||
|
`test/20260817_failed400/`:`ladder121/`、`ladder121_teff/`、`ladder121_nc/`、
|
||||||
|
`ladder121_nc100/`、`ladder20k_final/`、`he_seed/`、`batch_ladder.py`(支持
|
||||||
|
`--nc-int`/`--fine=N` 变体的通用 ladder 驱动)。
|
||||||
@@ -0,0 +1,472 @@
|
|||||||
|
# 81 点未收敛根因分析与稳定化种子步进修复(2026-08-18)
|
||||||
|
|
||||||
|
数据库快照:`/home/fmq/下载/dcts.db`(9135 completed / 81 failed)。
|
||||||
|
测试目录:`dcts/test/20260818_failed81/`。
|
||||||
|
|
||||||
|
## 一、81 点分类
|
||||||
|
|
||||||
|
| 群 | 点数 | 特征 |
|
||||||
|
|---|---|---|
|
||||||
|
| 20kK / he2(logg 5.5/6.0/6.5) | 66 | 全链(seed_nc→nl→nl_direct→Teff 轴 ladder)发散;ladder 中间步(22.5kK)收敛但最终步 20kK 发散 |
|
||||||
|
| 55–60kK / g5.0(he-4 / he-2) | 15 | 同样全链发散;57.5k 中间步部分收敛但 60k 最终步发散 |
|
||||||
|
| 假收敛否决误杀 | 2(含在上面) | nl_ladder 实际收敛且物理硬门全过,被"首末 max_relc 比 < 1e3"否决 |
|
||||||
|
|
||||||
|
## 二、逐项排除的假设(20k 群代表点 t20000_g6.5_he2_c-4_n-3_o-4)
|
||||||
|
|
||||||
|
基线复现:用生产的 22.5k ladder 种子热启动 20k 最终步,轨迹与生产逐拍一致
|
||||||
|
(iter1=18.2 → KANTOROVICH 加速介入 → iter10 雪崩 1.13e19,STOP@10)。
|
||||||
|
|
||||||
|
| 变体 | 结果 | 结论 |
|
||||||
|
|---|---|---|
|
||||||
|
| ORELAX 0.5→0.3→0.2 | 雪崩轨迹几乎不变 | 欠松弛无效 |
|
||||||
|
| IACC=0(试图禁加速) | 与基线**逐字节相同** | 源码 `IF(IACC.LE.4) IACC=7`(tlusty208.f:1928)——IACC=0 被重置,无效 |
|
||||||
|
| IACC=100(真禁加速) | STOP@7,更快爆 | 加速不是根因 |
|
||||||
|
| logg 轴 ladder(g6.0→6.25→6.5) | 6.25 收敛,6.5 雪崩 | 换轴无效 |
|
||||||
|
| 更细 logg 步(Δ0.125) | 极限环 0.22 | 该区存在真实电离前沿不稳定性 |
|
||||||
|
| CNO 邻居种子(同 T/g) | 雪崩 1.7e4 | 光换种子不够 |
|
||||||
|
| POPZER=1E-20(官方默认值) | 雪崩不变 | 太保守 |
|
||||||
|
| POPZER=1E-10 | **雪崩消失**,极限环 0.41 | 微布居置零是关键稳定器 |
|
||||||
|
| POPZER=1E-8 | 雪崩 | 过度置零破坏解 |
|
||||||
|
| **CNO 邻居种子 + POPZER=1E-10 + DPSILG=3.0** | **收敛 2e-4** ✅ | 制胜配方 |
|
||||||
|
|
||||||
|
制胜模型全过五重物理硬门:温度结构三项 ✅、emflux=1.0013 ✅、
|
||||||
|
首末 max_relc 比 4.5e5(不触发假收敛排查)✅。
|
||||||
|
|
||||||
|
## 三、物理根因
|
||||||
|
|
||||||
|
- 20k 群:fort.9 列语义(ITER ID TEMP NE **POP** RAD MAXIMUM ilev ifr)显示
|
||||||
|
雪崩在**布居列**,振荡能级为碳(ilev 38=C I 基态、103=C III)与氦
|
||||||
|
(ilev 20=He I,深度 4–9 外层 |ΔPOP|≈0.2 极限环)。He 富(He/H=100)大气
|
||||||
|
在 20kK 的 He I/II 电离前沿 + 微量碳(logC=-4)的电离平衡处于牛顿迭代
|
||||||
|
临界点:微布居在深度间振荡放大成雪崩。POPZER 把可忽略微布居剔除出方程
|
||||||
|
(官方标准输入恒设此机制,我们生产 nst 此前完全未设)、DPSILG 限制 λ 算子
|
||||||
|
单步变化幅度,两者组合把迭代拉回收敛域。
|
||||||
|
- 60k/g5.0 群(15 点):同 CNO 的 g5.5 收敛模型、仅 Δ0.25 logg 的步进也在
|
||||||
|
3 拍内雪崩(3.3e23)——目标解在牛顿域之外,与第三轮结论一致。工程层
|
||||||
|
(种子/步进/阻尼/置零)全部无效,属求解器级问题或应接受为不收敛角落。
|
||||||
|
|
||||||
|
## 四、假收敛否决误杀(2 点)
|
||||||
|
|
||||||
|
`t55000_g5.0_he-4_c-3_n-4_o-1`:nl_ladder 末次 max_relc=6.03e-4 < chmax,
|
||||||
|
能量/温度/emflux/b 因子四项校验全过,却被"首末比 63 < 1000"硬否决。
|
||||||
|
热启动时种子已接近解,首拍 max_relc 被钳在低位,首末比**数学上不可能**达到
|
||||||
|
1e3 量级——判据对热启动系统性误判。
|
||||||
|
|
||||||
|
修复(conv_check.rs `check_convergence_trace`):首拍 max_relc < 1.0 时豁免
|
||||||
|
首末比判据(valid=true + 豁免说明),真伪收敛交由五重物理硬门槛裁决。
|
||||||
|
|
||||||
|
## 五、生产集成:`seed_step_stab` 策略
|
||||||
|
|
||||||
|
- `ChainStep` 新增 `dpsilg` / `popzer` 字段(nst_writer 写出 DPSILG 及
|
||||||
|
POPZER/POPZR2/RADZER 同值联动组 + NITZER=1,行宽自动保护 ≤75 字符)。
|
||||||
|
- `runner::default_seed_stab_chain()`:种子链(seed_nc→nl)全步带
|
||||||
|
DPSILG=3.0 + POPZER=1E-10。
|
||||||
|
- 调度:`seed_step_stab` 顺位仅在 **exact_family**(同 Teff/logg/logHe、仅 CNO
|
||||||
|
不同)内找种子(新 db API `find_exact_family_seed_from_db`,排除自身名),
|
||||||
|
排除跨 Teff/logg 种子(不在配方适用域)。
|
||||||
|
- executor:该策略恒用稳定化链(用户自定义链不带旋钮,不采纳)。
|
||||||
|
- 工作流 YAML:`strategies: [cold_run, seed_step, seed_step_stab]`。
|
||||||
|
|
||||||
|
## 六、验证
|
||||||
|
|
||||||
|
- 单测:热启动豁免 ×2、nst 稳定化旋钮写出、stab 链形状/旋钮,全绿
|
||||||
|
(10 个测试套件全过,clippy 仅 2 个预存在 div_ceil 警告)。
|
||||||
|
- 全 20k 群 65/66 点批量实测(test/20260818_failed81/full20k*,最近同物理族
|
||||||
|
干净种子):稳定化配方对 **(DPSILG, POPZER) 组合逐点敏感**,四档互补——
|
||||||
|
- 一档 DPSILG=3.0/POPZER=1E-10:17 点(全 g6.5)
|
||||||
|
- 二档 DPSILG=2.0/POPZER=3E-11:+14 点(g6.0/g6.5)
|
||||||
|
- 三档 DPSILG=2.0/POPZER=1E-11:+10 点(含全部 4 个 g5.5)
|
||||||
|
- 四档 DPSILG=2.5/POPZER=1E-11:+1 点
|
||||||
|
- 联合覆盖 **42/65**,全部通过温度结构物理判据;剩余 23 点对四档均不收敛
|
||||||
|
(需逐点参数搜索或求解器级工作,后续迭代)
|
||||||
|
生产接线:`seed_step_stab` 策略链携带一档;runner 内稳定化多档回退
|
||||||
|
`nl_stab2/3/4`(全链失败后逐档自原始种子重跑,任一档收敛即采纳)。
|
||||||
|
- 60k/g5.0 群 15 点:本轮再次确认工程层不可解,保持"求解器级/接受不收敛"结论。
|
||||||
|
|
||||||
|
## 七、部署要求
|
||||||
|
|
||||||
|
重新部署 **server + node**(双方都有改动:scheduler/db 与 runner/nst_writer)。
|
||||||
|
重跑策略链后预期:20k 群 42 点由 seed_step_stab(一档)+ nl_stab2/3/4(二至
|
||||||
|
四档)回收;2 个假收敛误杀点修复后重试即收敛;其余(20k 群 ~23 点 +
|
||||||
|
60k/g5.0 群 15 点)预计仍失败(如实归因)——20k 残余点需逐点参数搜索
|
||||||
|
(后续迭代),60k/g5.0 群属求解器级/接受不收敛。
|
||||||
|
|
||||||
|
## 八、2026-08-19 部署后 79 点复盘(两个接线 bug)
|
||||||
|
|
||||||
|
重新部署后失败数 81→79(仅回收 2 个假收敛误杀点;42 个验证点全部未回收)。
|
||||||
|
DB 239 个新任务分析定位两个 bug:
|
||||||
|
|
||||||
|
1. **Bug A(主力,78 个任务 rc=2)**:executor 的 `needs_seed_download` 只认
|
||||||
|
`seed_step` 策略,不认 `seed_step_stab` → 种子未下载,seed_nc 阶段无 fort.8,
|
||||||
|
TLUSTY 读种子直接 EOF 崩溃(`tlusty rc=2 or missing fort.7`,0.2s 内失败)。
|
||||||
|
修复:`matches!(current_strategy, "seed_step" | "seed_step_stab")`。
|
||||||
|
2. **Bug B(种子选错)**:`find_exact_family_seed_from_db` 复用了
|
||||||
|
`calculate_seed_distance` 的 is_exact 判定,其容忍 ΔTeff≤5000K——把 ladder
|
||||||
|
中间种子(如 t22500_g6.5_...,CNO 距离 0)当成同族且排最前。稳定化配方的
|
||||||
|
验证前提是同温同重力仅差丰度,必须严格 Teff/logg/logHe 逐值相同。
|
||||||
|
修复:改为严格判定 + 回归测试 `test_find_exact_family_seed_strict_same_family`。
|
||||||
|
|
||||||
|
60k/g5.0 群(15 点)与 20k 残余 23 点的失败为真实物理失败(如期),不受这两个
|
||||||
|
bug 影响。修复后需再次重新部署 server + node。
|
||||||
|
|
||||||
|
## 九、2026-08-20 部署后 44 点复盘(种子逐点敏感 → 种子轮换修复)
|
||||||
|
|
||||||
|
Bug A/B 修复部署后失败 79→44(回收 35 点,链路全部按设计执行:
|
||||||
|
seed_nc→nl→nl_direct→nl_stab2/3/4,种子均为严格同族 CNO 邻居,无 rc=2)。
|
||||||
|
|
||||||
|
### 归因
|
||||||
|
|
||||||
|
44 点分布:20k/he2 群 31 点(g5.5×4 / g6.0×8 / g6.5×19)+ 55–60k/g5.0 群 13 点。
|
||||||
|
|
||||||
|
与 08-18 直接测试的四档矩阵结果交叉比对(union 收敛 45/65,物理收敛判据
|
||||||
|
末拍 max_relc<1e-3 且 fort.7 无 NaN):
|
||||||
|
|
||||||
|
- **17 个 20k 点在测试中收敛、生产却失败**。逐一比对种子:测试制胜种子与
|
||||||
|
生产选中种子**全部不同**(同为 CNO 距离 1–2 的邻居,但换的元素方向不同,
|
||||||
|
如测试用 ΔO=1 而生产用 ΔN=1)。配方不仅对 (DPSILG,POPZER) 逐点敏感,
|
||||||
|
**对种子本身也逐点敏感**。
|
||||||
|
- 其中 11 点的制胜组合就是 tier1(DPSILG=3.0/POPZER=1E-10)+ 特定距离 1
|
||||||
|
邻居——即生产首轮就会用到的档位,只差换对种子。
|
||||||
|
- 其余 14 个 20k 点在测试四档下也全部失败(真残余,需逐点参数搜索或接受)。
|
||||||
|
- 13 个 60k/g5.0 点维持求解器级失败的既有结论;但它们在 seeds 表中有同族
|
||||||
|
候选(40–295 个),轮换机制会一并尝试,或有少量意外回收。
|
||||||
|
|
||||||
|
### 根因(工程层)
|
||||||
|
|
||||||
|
生产每次重试 `find_exact_family_seed_from_db` 按 CNO 距离排序**稳定选中同一
|
||||||
|
个种子**,重试只是重复同一组合(种子×四档)白跑;且 `seed_step_stab` 是链上
|
||||||
|
最后一项,失败弹出后链空 → 直接终态 failed,从未轮换过种子。
|
||||||
|
|
||||||
|
### 修复:种子轮换
|
||||||
|
|
||||||
|
1. `find_exact_family_seed_from_db` 的 `exclude: Option<&str>` 改为
|
||||||
|
`&[String]` 排除列表。
|
||||||
|
2. 新增 `db.list_used_seed_names(point, workflow)`:本点历史任务用过的全部
|
||||||
|
种子名(DISTINCT)。
|
||||||
|
3. 调度两处接入排除:
|
||||||
|
- `resolve_dispatchable_chain` 的 seed_step_stab 分支注入已用种子 →
|
||||||
|
每次重派自然轮换;
|
||||||
|
- `trigger_strategy_fallback`:seed_step_stab 失败且链耗尽时,若还有
|
||||||
|
未试过的同族邻居则以单顺位 `["seed_step_stab"]` 重链重派(wave=0 优先);
|
||||||
|
邻居全部试过后落入 failed 终态——轮换次数受同族邻居数(45–60 个)天然
|
||||||
|
约束,无死循环风险。实际预期在前几次轮换(距离 1 邻居圈)内命中制胜组合。
|
||||||
|
4. 回归测试:`test_find_exact_family_seed_rotation`(轮换到第二个邻居、
|
||||||
|
全排除后 None)。全套 10 个测试套件通过。
|
||||||
|
|
||||||
|
### 部署与预期
|
||||||
|
|
||||||
|
重新部署 server(node 无改动)+ 重启工作流(skip_converged)。预期 20k 群
|
||||||
|
再回收 ~17 点(测试已验证的组合),残余约 14(20k 真残余)+ 0–13(60k)。
|
||||||
|
|
||||||
|
## 十、2026-08-20 残余 27 点直接测试:20k 全可回收、60k 求解器级定论(60k 结论后被 §十三 翻案)
|
||||||
|
|
||||||
|
针对 §九 后仍失败的 14 个 20k 真残余点与 13 个 60k/g5.0 点,批次
|
||||||
|
`dcts/test/20260820_residual27/`(249 个单阶段热启动运行,判据 = 末拍
|
||||||
|
max_relc<1e-3 + fort.7 无 NaN + check_temperature 三项):
|
||||||
|
|
||||||
|
1. **20k:14/14 全部物理收敛**。制胜组合均为 t1/t2(生产链内建档位)×
|
||||||
|
特定 CNO 邻居种子(距离 2-3,非最近邻);g6.5 存在「种子星」
|
||||||
|
`t20000_g6.5_he2_c-2_n-3_o-2`(对 9 点有效)。§九 的种子轮换按距离
|
||||||
|
升序枚举即可命中,**无需新增生产代码**——部署轮换修复后 20k 群 31 个
|
||||||
|
失败点预期全部回收。
|
||||||
|
2. **60k/g5.0:13/13 仍不可收敛,定论为求解器级**。在近邻种子基础上
|
||||||
|
穷尽五族输入层旋钮——tier1 配方、DPSILT/DPSILN/DPSILD 热分量钳制、
|
||||||
|
ORELAX=0.3/0.2 强松弛、极端钳制(DPSILG=2/DPSILT=1.2/DPSILN=1.2)——
|
||||||
|
全部在 iter 4-10 内 `**** STOP in SOLVE`(牛顿矩阵奇异、线性解失败,
|
||||||
|
阻尼类旋钮作用于更新步、到不了 SOLVE 内部)。与 §一/§三「Newton
|
||||||
|
排除域」结论一致:除非修改 TLUSTY 求解器本体,建议接受这 13 点为
|
||||||
|
不收敛角落。
|
||||||
|
|
||||||
|
## 十一、2026-08-20 60k/g5.0 群 13 点攻坚:10 点物理收敛,3 点折叠排除("折叠排除"已被 §十三 翻案为 13/13)
|
||||||
|
|
||||||
|
继 §十 后针对 60k 群的专项测试(`test/20260820_residual27/`,累计 500+
|
||||||
|
次运行)。过程中修正两个方法论错误:① fort.9 每拍 max_relc 应取该拍
|
||||||
|
**全深度最大值**(非末行);② salvage 种子库存在跨尝试工件混叠,种子
|
||||||
|
必须配对同阶段 `.9` 末拍收敛(真验证池 259 个)。另发现 **DPSILG/POPZER
|
||||||
|
稳定化旋钮在高 CNO 60k 上致散**(自复现实验证实),§五/§九 配方仅在
|
||||||
|
20k He-rich 域有效。
|
||||||
|
|
||||||
|
**制胜配方(10/13 点物理收敛,三项判据全过)**:同 CNO 跨 Teff(Δ5k
|
||||||
|
冷端)真收敛种子 → **纯档自适应 Teff 延拓**(plain nst,步长 1250K 起、
|
||||||
|
失败二分、最小 25K、成功×1.5 恢复)。本地无跨温种子时级联(50k→55k
|
||||||
|
收敛后再 55k→60k)。
|
||||||
|
|
||||||
|
**折叠排除 3 点**(he-2 c-4_n-4、he-4 c-4_n-3、he-4 c-4_n-4):Teff
|
||||||
|
(二分至 25K)、logg(5.125 以下全爆)、O 丰度(二分至 0.025 dex)三条
|
||||||
|
独立延拓轴全部在目标前折叠,IACC=100/ORELAX=0.3/NITER=500 亦无效——
|
||||||
|
判定为静力解不存在的物理角落(60k/g5.0 最大金属组合,辐射加速度逼近
|
||||||
|
引力),建议接受为不收敛终态。
|
||||||
|
|
||||||
|
**对 15-80k 网格扩展的工程要求**:① runner 需新增自适应 Teff 延拓
|
||||||
|
(替换固定 2500K 阶梯);② 种子搜索需纳入同 CNO 跨 Teff 方向(冷端
|
||||||
|
优先);③ 稳定化旋钮按温度域门控(仅 20k He-rich);④ 设计「折叠排除」
|
||||||
|
终态标记避免扩展网格时对此类角落无限重试。
|
||||||
|
|
||||||
|
## 十二、2026-08-21 生产代码落地(§十一 工程要求实现)
|
||||||
|
|
||||||
|
1. **自适应 Teff 延拓**(`runner.rs` ladder 回退块):Teff 轴为主且间隔
|
||||||
|
>2500K 时,替换固定 ≤2500K 均分档为自适应步长控制——初始 1250K、
|
||||||
|
成功 ×1.5(上限 1250K)、失败二分(下限 25K)、最多 48 步;失败步
|
||||||
|
产物不传递(沿用上一收敛态),步长低于 25K 判折叠墙中止。收敛梯级
|
||||||
|
照旧登记 ladder_seeds 供簇内共享;终点由 nl_ladder 收尾。logg 轴与
|
||||||
|
短间隔维持原固定档(自适应仅 Teff 轴经 60k 验证)。
|
||||||
|
2. **稳定化旋钮温度域门控**(两处):
|
||||||
|
- `executor.rs resolve_execution_chain`:Teff>30kK 时 seed_step_stab
|
||||||
|
降级为普通种子链(不再恒用 default_seed_stab_chain);
|
||||||
|
- `runner.rs` STAB_TIERS 多档回退:Teff>30kK 跳过全部档位。
|
||||||
|
依据:60k/g5.0 高金属域实测 DPSILG/POPZER 族旋钮致散(§十一)。
|
||||||
|
新增回归测试 `seed_step_stab_teff_domain_gating`。
|
||||||
|
3. **同 CNO 跨温种子方向**:无需改动——`calculate_seed_distance` 的
|
||||||
|
exact_family(2026-08-14 修复)已含 ΔTeff≤5000K 且同 CNO 距离为 0
|
||||||
|
(最优排序),55k→60k / 50k→55k 制胜种子方向天然可用。
|
||||||
|
4. **折叠排除终态标记**:暂缓(需新增 GridPointStatus 变体并波及前端/
|
||||||
|
调度语义,属产品决策);当前 3 个折叠点保持 failed 终态,§十一 已
|
||||||
|
记录三轴证据。
|
||||||
|
|
||||||
|
全套 210 测试通过;clippy 仅余 2 个既有 div_ceil 警告。需重新部署
|
||||||
|
server + node(两 crate 均有改动)。
|
||||||
|
|
||||||
|
## 十三、2026-08-21 独立审查 + C/N 丰度轴翻案:13/13 全部可收敛
|
||||||
|
|
||||||
|
### 独立审查(subagent 全量复核)
|
||||||
|
- **证实**:fort.9 判据(源码级:PRCHAN 恒 9 列、MAXIMUM=第 7 字段、CHM
|
||||||
|
取全深度最大、LFIN=|CHMX|≤CHMAX);10 个收敛点零假阳性(末拍全深度
|
||||||
|
最大 4.0e-4~9.9e-4、fort.7 干净、温度结构三项过、种子链 md5 连续);
|
||||||
|
稳定化致散对照实验公平(selfrepro3 vs smoke 仅 nst 旋钮行不同);
|
||||||
|
§十二 生产代码正确。
|
||||||
|
- **证伪两处失败侧证据**:① tlad3 的 64 个 stab 步是 nst 行超长解析崩溃
|
||||||
|
(非物理发散)——出现在脚本修复前的日志里;② glad logg 轴链被失败态
|
||||||
|
污染(失败步 fort.7 被当种子传递)+ stage.5 logg 截断(5.125→"5.12")
|
||||||
|
——logg 轴"二分耗尽"证据无效。两点均不改变当时总判决,但提示
|
||||||
|
he-2_c-4_n-4 的折叠证据实际只剩 Teff+O 两轴。
|
||||||
|
- 审查修复已入代码:自适应延拓 label 精度 0.01k(防重名);到达目标后
|
||||||
|
直接采纳(去冗余 nl_ladder 重跑)。
|
||||||
|
|
||||||
|
### C/N 丰度轴延拓:折叠结论翻案
|
||||||
|
针对 3 个"折叠点"补测此前未走通的 C/N 丰度轴(同 T/g/He 的 CNO 邻居
|
||||||
|
真收敛种子,0.2 dex 起二分至 0.025 dex):
|
||||||
|
|
||||||
|
| 点 | C 轴 | N 轴 |
|
||||||
|
|---|---|---|
|
||||||
|
| he-2 c-4_n-4_o-1 | ✅ 13 步 | ✅ 9 步 |
|
||||||
|
| he-4 c-4_n-3_o-1 | ✅ 13 步 | 爬升至 -2.41(冗余证据) |
|
||||||
|
| he-4 c-4_n-4_o-1 | ✅ 13 步 | 爬升中(冗余证据) |
|
||||||
|
|
||||||
|
三点 C 轴全部物理收敛(三项判据过)→ **"静力解不存在"结论撤销**:
|
||||||
|
解存在,可达方向在 C/N 丰度轴(Teff/logg/O 三轴确实折叠,但非充分
|
||||||
|
证据)。**13/13 全部可收敛**。
|
||||||
|
|
||||||
|
### 生产落地(runner 丰度轴延拓回退)
|
||||||
|
`runner.rs` 新增丰度轴延拓回退块(与域门控互补,仅 Teff>30kK 启用):
|
||||||
|
严格同族(同 T/g/He 仅 CNO 不同)种子 → C 轴优先、N 轴兜底,0.2 dex
|
||||||
|
起、成功 ×1.4(上限 0.25)、失败二分(下限 0.025)、每轴 40 步、
|
||||||
|
失败步不传递;收敛梯级登记 ladder_seeds。接线单测
|
||||||
|
`unit_axlad_c_axis_fallback_wiring` 通过;全套 211 测试通过。
|
||||||
|
|
||||||
|
生产路径闭环:seed_step_stab(高温降级普通链)注入 exact-family CNO
|
||||||
|
邻居种子 → seed_nc/nl 失败 → Teff 延拓(种子同温无步)→ stab 跳过
|
||||||
|
(域门控)→ **丰度轴延拓收敛**。§九 种子轮换在失败时自动换下一邻居。
|
||||||
|
|
||||||
|
## 十四、2026-08-22 最后 1 点收编:axlad 迭代饥饿修复(LADDER_STAGE_NITER)+ server 部署不一致发现
|
||||||
|
|
||||||
|
§十二/§十三 代码部署后 44 点中 43 点恢复,唯一残余
|
||||||
|
`t60000_g5.0_he-4_c-4_n-4_o-1`(60k/g5.0/氢主导、高氧 o-1 角,24 次尝试)。
|
||||||
|
详见 `test/20260822_last1/RESULT.md`。
|
||||||
|
|
||||||
|
### 根因 A:延拓阶段迭代饥饿(物理层,已修复)
|
||||||
|
|
||||||
|
8-21 12:11 生产任务(种子 n-2)axlad_n 已从 -2 收敛到 -3.490,随后
|
||||||
|
-3.568/-3.529 两步 best=0.002/0.005 **卡满 100 迭代且仍在单调下降**——不是
|
||||||
|
发散,是 NITER=100 迭代饥饿;二分触底 0.025 dex 后被误判"轴折叠"。
|
||||||
|
|
||||||
|
离线复现(NITER=300,种子=生产 n-3 收敛大气):n=-3.52373 于第 **103**
|
||||||
|
迭代收敛(恰好超旧上限 3 次);全程 5 个 waypoint 需 >100 迭代
|
||||||
|
(103/111/111/127/147),16 步走通 -3.0→-4.0,目标点 4.1e-04、三项温度
|
||||||
|
检查过、无 NaN。**9216/9216 全网格物理可解。**
|
||||||
|
|
||||||
|
修复:`runner.rs` 新增 `LADDER_STAGE_NITER=300`,自适应 Teff 延拓/固定梯/
|
||||||
|
丰度轴延拓三站点统一 `niter.max(300)`(TLUSTY 收敛即停,快阶段与真发散
|
||||||
|
步均不受损)。全套工作区测试通过。
|
||||||
|
|
||||||
|
### 根因 B:server 部署二进制与工作区不一致(部署层,需重建)
|
||||||
|
|
||||||
|
12:11 任务给 n-4 挂了 8-19 已用过的 n-2 种子——违反当前代码的
|
||||||
|
`list_used_seed_names` 排除;且按 directed_cno_distance 排除后应选
|
||||||
|
c-2_n-4_o-1。与"无排除 + 种子表序"旧行为吻合 → **部署的 server 二进制
|
||||||
|
构建自中间状态(至少缺 resolve 路径种子轮换排除)**。node 端为新
|
||||||
|
(axlad 已运行)。需以当前工作区重建重部署两端。
|
||||||
|
|
||||||
|
### 有利条件与预期恢复
|
||||||
|
|
||||||
|
- n-3 点 8-21 12:05 任务已用同款 axlad 从 -2 走到 -3.000 收敛(24 步),
|
||||||
|
种子已注册;
|
||||||
|
- n-4 失败任务已注册 waypoint 种子最深至 `..._n-3.489798_o-1_ladder`
|
||||||
|
(距目标 0.51 dex、从未用过)——重部署后首个 seed_step_stab 派发自然
|
||||||
|
选中它,axlad_n 以 300 迭代走完剩余 0.51 dex 即收敛。
|
||||||
|
|
||||||
|
## 十五、2026-08-22 二次重试仍失败:完成翻转 × 回退触发顺序竞态(flip 阻塞修复)
|
||||||
|
|
||||||
|
§十四 修复部署后(8-22 03:46)重试仍失败,且**只派发了 1 个 cold_run 任务
|
||||||
|
(14 分钟发散)后整链静默终止**——无 seed_step/seed_step_stab 后续任务。
|
||||||
|
|
||||||
|
### 根因(server 顺序竞态,非物理)
|
||||||
|
|
||||||
|
失败上报处理链(api/task.rs → record_task_report 事务 → trigger_strategy_fallback):
|
||||||
|
|
||||||
|
1. 事务内:任务行 failed + 网格点 failed + **workflow 完成 flip**(条件仅
|
||||||
|
「无 pending/queued/running 点」);
|
||||||
|
2. 事务后:trigger_strategy_fallback 派发下一策略——但其 still_running
|
||||||
|
守卫发现 workflow 已是 completed → **直接跳过**。
|
||||||
|
|
||||||
|
该点为最后一个活跃点(9215 completed + 1 failed),失败上报的同一事务内
|
||||||
|
flip 条件即满足(workflows.updated_at=04:00:50 = 上报时刻,实证)→
|
||||||
|
cold_run 之后的策略链永不派发。此前从未暴露:只有当失败点不是最后一个
|
||||||
|
活跃点时,flip 条件不满足、回退正常(8-21 的 09:45→10:04→12:11 三连回退
|
||||||
|
正是因为当时还有其它点在跑)。
|
||||||
|
|
||||||
|
### 修复:完成 flip 增加「未消费回退链」阻塞子句
|
||||||
|
|
||||||
|
`db/workflows.rs` 新增共享子句 `UNCONSUMED_FALLBACK_BLOCKER_SQL`,两条
|
||||||
|
flip SQL(record_task_report 事务内 + 后台 30s 对账)统一追加:
|
||||||
|
|
||||||
|
> 存在 failed 点、其最新 failed/timeout 任务行(镜像 pop 的
|
||||||
|
> ORDER BY created_at DESC, rowid DESC 口径)的 tlusty 或 synspec 策略链
|
||||||
|
> json_array_length > 1(弹掉刚失败首项后仍有顺位)→ 不翻转 completed。
|
||||||
|
|
||||||
|
- 链随每次失败上报严格变短,终会耗尽(长度 1)→ 不永久阻塞完成;
|
||||||
|
- JSON 异常 → json_array_length=NULL(NULL>1 不为真)→ 保守放行;
|
||||||
|
- 修复后顺序:失败上报 → flip 被阻塞 → 回退正常派发 → 点回 queued →
|
||||||
|
链耗尽后 flip 恢复。
|
||||||
|
|
||||||
|
回归测试 `test_workflow_flip_blocked_by_unconsumed_fallback_chain`
|
||||||
|
(3 顺位链阻塞 → 单顺位链放行)通过;全套 212 测试通过。
|
||||||
|
|
||||||
|
### 部署要求
|
||||||
|
|
||||||
|
本轮仅改 server(db/tasks.rs、db/workflows.rs、db/mod.rs 测试);
|
||||||
|
node 无需重新部署。**server 需用当前工作区再次重建重部署**,然后按既有
|
||||||
|
流程重试 failed 点。预期链路:cold 失败(flip 不再提前杀链)→ seed_step
|
||||||
|
(55k ladder 种子,失败)→ seed_step_stab 排除已用种子后选中
|
||||||
|
`..._n-3.489798_o-1_ladder`(0.51 dex)→ axlad_n 300 迭代走完 → 收敛。
|
||||||
|
|
||||||
|
### 审查驱动补强(2026-08-22 subagent 复审,4 项修复)
|
||||||
|
|
||||||
|
复审确认主路径有效(SQL 口径与 pop 严格镜像、快照实证同一行、恢复链路
|
||||||
|
seed_step→waypoint 逐步核实、无永久卡 running、全测通过),另发现 4 项:
|
||||||
|
|
||||||
|
1. **【高】链尾 rotation 仍会被吞**(同构 bug 后退一环):最后一个活跃点以
|
||||||
|
`[seed_step_stab]`(长度 1)失败时,blocker 放行 flip → rotation 臂
|
||||||
|
(换种子重派,§九机制)被 still_running 守卫拦截 → waypoint 只有
|
||||||
|
一次机会。修复:blocker 增加「种子轮换待执行」子句——链尾
|
||||||
|
seed_step_stab 失败且存在未试过的同物理族干净种子(teff/logg/loghe
|
||||||
|
逐值相等、is_clean=1、不在本点已用种子集)→ 阻塞翻转。快照实测该点
|
||||||
|
有 **96 个**未用同族候选(waypoint 阶梯 -3.4898/-3.4337/…),轮换
|
||||||
|
每次消耗一个种子、单调收敛,不会永久阻塞。
|
||||||
|
2. **【中】synspec 双链 OR 永久卡死隐患**:synspec 回退任务行刻意保留完整
|
||||||
|
tlusty 审计链(scheduler.rs synspec 臂 clone),len≥2 恒真 → synspec
|
||||||
|
启用的工作流 flip 永久阻塞。修复:按行 failed_stage 阶段归因(镜像
|
||||||
|
pop 的 stage→列映射,NULL→tlusty)只看对应链。
|
||||||
|
3. **【低】固定梯末步 nl_ladder 漏提升**:中间梯级 300、最难的目标逼近末步
|
||||||
|
反而 100(越近目标越慢收敛)。修复:`final_step.niter.max(300)`。
|
||||||
|
4. **【低】json 防护**:json_array_length/json_extract 对非法 JSON 抛
|
||||||
|
`malformed JSON` 错误而非返回 NULL——一条脏行会让 record_task_report
|
||||||
|
整个结算事务失败。修复:json_valid 守卫(脏行按长度 0 放行)+ 注释更正。
|
||||||
|
|
||||||
|
回归测试新增 `test_workflow_flip_blocked_by_seed_rotation_pending`
|
||||||
|
(无候选放行 → 注入候选阻塞 → 消耗候选后放行)与
|
||||||
|
`test_workflow_flip_stage_attribution_ignores_tlusty_audit_copy`
|
||||||
|
(synspec 耗尽行 + tlusty 审计副本 len3 不阻塞);全套 **214 测试**通过。
|
||||||
|
|
||||||
|
**运维附注**(审查 #5/#6):
|
||||||
|
- NITER=300 拉长任务墙钟(55k→60k 重走 + axlad 可能 >6h),而 stale 重投
|
||||||
|
阈值 21600s(claimed_at 不随心跳刷新)——攻坚期间建议调大
|
||||||
|
`DCTS_STALE_SEC`(如 43200),避免任务被重投双跑。
|
||||||
|
- seed_step 臂将在 35 个 d=0.0 并列种子中选中最早插入的 55k 真实点
|
||||||
|
(exact 族内距离不含 Teff 项),节点需重走 55→57.7k 折叠墙(预期失败、
|
||||||
|
耗时数小时)后才轮到 seed_step_stab→waypoint;属效率问题非正确性,
|
||||||
|
后续可考虑并列时按 Teff 邻近决胜(暂不动 seed_finder 语义)。
|
||||||
|
|
||||||
|
## 十六、2026-08-25 全网格收官核验 + 前端「9137/9216 大气收敛」显示口径修复
|
||||||
|
|
||||||
|
### 收敛核验(快照 dcts.db)
|
||||||
|
|
||||||
|
- **9216/9216 网格点 status=completed,tlusty 收敛 9207 + 9 个历史空状态点
|
||||||
|
(M14 回填后 9216)——全网格物理收敛达成。**
|
||||||
|
- 末点 t60000_g5.0_he-4_c-4_n-4_o-1 制胜链(8-24):55k 种子任务重走折叠墙
|
||||||
|
失败(23417s)→ waypoint -3.4898 种子任务失败(axlad 爬至 -3.825 后
|
||||||
|
中断)→ **rotation 换 -3.6248 waypoint 种子** → axlad 以 **107/107/175
|
||||||
|
迭代**三连收敛(-3.725/-3.865/-4.000,final 9.46e-04)——三个 waypoint
|
||||||
|
全部超过旧 NITER=100 上限,LADDER_STAGE_NITER=300 与 rotation/flip 修复
|
||||||
|
均为必要条件;五重物理门全过(energy 7.7e-4 / temp / emflux 1.00056 /
|
||||||
|
bfac 0.0077 / result_valid)。
|
||||||
|
|
||||||
|
### 「9137 大气收敛」差额解剖
|
||||||
|
|
||||||
|
前端口径 `大气收敛 = cold_run_converged + seed_step_converged`(按
|
||||||
|
tlusty_success_method 分类),漏掉两类:
|
||||||
|
|
||||||
|
| 差额 | 数量 | 成因 |
|
||||||
|
|---|---|---|
|
||||||
|
| seed_step_stab 收敛点 | 70 | 统计口径写于 2026-08-18 引入 stab 策略之前,未跟进 |
|
||||||
|
| tlusty_status 空的历史点 | 9 | Phase 5b 阶段列上线前完成的点,列值为 NULL |
|
||||||
|
|
||||||
|
(9207 − 70 − 9 = 9137,与前端显示精确吻合。)
|
||||||
|
|
||||||
|
### 修复(server 端 + 前端,均随 server 二进制打包)
|
||||||
|
|
||||||
|
- **后端统计**:`get_grid_summary_stats`(grid.rs 双分支)与
|
||||||
|
`list_workflows` 聚合新增 `tlusty_converged`(不按 method 拆的权威总数)
|
||||||
|
与 `seed_step_stab_converged`;WorkflowStats/WorkflowListStats 同步
|
||||||
|
(serde default 兼容旧 JSON)。
|
||||||
|
- **前端**:详情页计数行与指标卡改用 `tlusty_converged`(旧 server 回退
|
||||||
|
三桶和);TLUSTY 分段进度条与 parSets 新增「稳定化种子」档(teal 色,
|
||||||
|
psSlot 按 method 三分)。
|
||||||
|
- **M14 迁移**:`completed + method 非空 + tlusty_status 空` → 回填
|
||||||
|
'converged'(method 仅在整管线成功时写入,回填安全;detect 幂等)。
|
||||||
|
快照实测回填 9 行,修复后前端显示 **9216 大气收敛 (100%)**
|
||||||
|
(冷启动 5573 · 种子 3573 · 稳定化 70)。
|
||||||
|
- 回归:M14 迁移测试 + stats 断言扩展;全套 **215 测试**过、clippy 0 错。
|
||||||
|
|
||||||
|
### 光谱侧现状(非本轮问题,仅记录)
|
||||||
|
|
||||||
|
工作流配置 `synspec_stage.enabled: false`(本攻坚期 TLUSTY-only)。快照
|
||||||
|
synspec_status:7452 converged / 459 failed / 1305 空——459 个 failed 均
|
||||||
|
为 8-7 之前旧时代残留(无成功方法标记,synspec_success_method 全空),
|
||||||
|
1305 个空多为 TLUSTY-only 重跑后 clear_synspec 置空。若后续需要光谱全覆盖,
|
||||||
|
需另开一轮(重新启用 synspec_stage + skip_converged)。
|
||||||
|
|
||||||
|
### 附录(2026-08-26):9 个 tlusty_status 空历史点的逐点审查
|
||||||
|
|
||||||
|
**是否真收敛——三重独立证据,结论:是。**
|
||||||
|
|
||||||
|
1. **任务链**:9 点各有且仅有一个 completed 完整管线任务(tlusty_enabled=1、
|
||||||
|
synspec_enabled=1、cold_run/seed_step),max_relc 0.000114~0.000933 全部
|
||||||
|
< 1e-3,elapsed 1500~7500s 为真实完整计算;
|
||||||
|
2. **种子库**:9/9 在 seeds 表注册 is_clean=1 且服务端 `.7` 在档——种子注册
|
||||||
|
仅发生在「收敛 + 大气无 NaN」的成功上报/导入路径;
|
||||||
|
3. **功能验证(最硬)**:6/9 的大气被后续任务用作种子,5/9 有制胜案例——
|
||||||
|
如 `t55000_g5.0_he0_c-4_n-2_o-4` 的大气 8-15 作为种子把
|
||||||
|
`t60000_g5.0_he0_c-4_n-2_o-4` 带到收敛(9.77e-04);
|
||||||
|
`t45000_g5.0_he-4_c-4_n-4_o-2` 的大气 8-18 带收敛
|
||||||
|
`t50000_g5.0_he-4_c-4_n-4_o-2`(2.77e-04)。发散/垃圾大气做种子只会
|
||||||
|
立即爆炸,不可能制胜。
|
||||||
|
(注:Aug-1 时代产物未进 salvage、tasks.summary_json 早于 M1 列,故无法
|
||||||
|
做配对 .9 复核;以上为可得的最强证据。)
|
||||||
|
|
||||||
|
**为什么恰好 9 个空(机制清楚)**
|
||||||
|
|
||||||
|
- 9 点的 TLUSTY 制胜上报全部发生在 **8-1~8-2**(重复派发混沌期,每点恰好
|
||||||
|
一次成功、前后皆失败重复派发;completed 终态被守卫保护未被翻黑);
|
||||||
|
- `tlusty_status` 阶段列 **8-6(d16b3d3/M8)才存在**,M8 只 ADD COLUMN
|
||||||
|
不回填;此后 9 点再无任何任务(attempt_count 冻结),无上报可补写;
|
||||||
|
- 同期(8-2 前制胜)606 点中 597 个的列已被一次**中期批量修复**补齐(该
|
||||||
|
修复同时写了全队列的 synspec_success_method='standard'——git 中无此
|
||||||
|
语句,应为当时手工修复 SQL),但它漏掉了这 9 个(8/9 的最后任务行是
|
||||||
|
failed,疑被其筛选条件排除;3 个例外说明条件并非单纯按最后任务状态)。
|
||||||
|
精确 SQL 已不可考,不影响收敛性判定。
|
||||||
|
|
||||||
|
**补全**:M14 迁移(status='completed' AND method 非空 AND tlusty_status
|
||||||
|
空 → 'converged')即为此而设;method 的三个写入方(成功上报/mark_grid_
|
||||||
|
point_imported/upsert_point_summary)全部以收敛为前提,判定安全。快照实测
|
||||||
|
恰回填 9 行。若需绝对 belt-and-braces,可对这 9 点单独 force 重跑(各
|
||||||
|
~30min),让列经由真实计算路径写入。
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 182 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 382 KiB |
@@ -0,0 +1,113 @@
|
|||||||
|
# Windows 节点远程桌面连接手册(经 Linux 跳板中转)
|
||||||
|
|
||||||
|
> 2026-08-20 实战总结。背景:两台 Windows 计算节点(node-fmq-dckj-02/03)与本机不直连,
|
||||||
|
> 必须经 Linux 跳板 node-fmq-dckj-01(dckj@100.66.1.5,Tailscale 网段)中转。
|
||||||
|
> 本机无节点小宝直连 Windows 的能力,所有 Windows 运维(尤其 Docker Desktop 登录自启)
|
||||||
|
> 都走本文的 SSH 隧道 + RDP 方案。
|
||||||
|
|
||||||
|
## 1. 网络拓扑与凭据
|
||||||
|
|
||||||
|
| 节点 | 地址 | 登录 | 密码 | 可达路径 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 跳板 dckj-01 | 100.66.1.5 | dckj | dckjzndx | 本机直连 |
|
||||||
|
| node-fmq-dckj-02 | 192.168.6.102 | fmq | 1235 | 仅经跳板 |
|
||||||
|
| node-fmq-dckj-03 | 192.168.6.103 | Administrator | dev#2dckj@zndx | 仅经跳板 |
|
||||||
|
|
||||||
|
- Windows 侧 SSH(22)和 RDP(3389)均开放,但只对跳板所在的内网段可达。
|
||||||
|
- Windows 上运行**节点小宝**(内网穿透工具,服务名 `NodeBabyLinkService`,
|
||||||
|
`StartMode=Auto` 开机自启),负责与 dckj-01 之间的 P2P 链路——
|
||||||
|
跳板能通 SSH 本身就说明节点小宝是活的。
|
||||||
|
- ⚠️ 密码写在文档里仅图方便,机器若更换使用人请先改密;长期方案建议配 SSH 公钥。
|
||||||
|
|
||||||
|
## 2. 核心方案:SSH 隧道转发 RDP(无需在跳板装任何桌面软件)
|
||||||
|
|
||||||
|
原理:本机 `ssh -N -L` 建立经跳板的加密隧道,把远端 3389 映射到本机端口,
|
||||||
|
再用本机任意 RDP 客户端(Remmina / xfreerdp / GNOME Connections)连 localhost。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 隧道一:本机 13389 → dckj-03 (192.168.6.103) RDP
|
||||||
|
sshpass -p 'dev#2dckj@zndx' ssh -N \
|
||||||
|
-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
|
||||||
|
-o ServerAliveInterval=30 -o ServerAliveCountMax=6 \
|
||||||
|
-o ProxyCommand="sshpass -p dckjzndx ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -W %h:%p dckj@100.66.1.5" \
|
||||||
|
-L 13389:192.168.6.103:3389 Administrator@192.168.6.103 &
|
||||||
|
|
||||||
|
# 隧道二:本机 13390 → dckj-02 (192.168.6.102) RDP
|
||||||
|
sshpass -p 1235 ssh -N \
|
||||||
|
-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
|
||||||
|
-o ServerAliveInterval=30 -o ServerAliveCountMax=6 \
|
||||||
|
-o ProxyCommand="sshpass -p dckjzndx ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -W %h:%p dckj@100.66.1.5" \
|
||||||
|
-L 13390:192.168.6.102:3389 fmq@192.168.6.102 &
|
||||||
|
```
|
||||||
|
|
||||||
|
然后 RDP 客户端连接:
|
||||||
|
|
||||||
|
- dckj-03 → `localhost:13389`(用户 Administrator)
|
||||||
|
- dckj-02 → `localhost:13390`(用户 fmq)
|
||||||
|
|
||||||
|
要点:
|
||||||
|
- `ProxyCommand` 里层 `sshpass` 付**跳板机**密码,外层 `sshpass` 付**目标机**密码,
|
||||||
|
两级密码各管一段,这是双密码跳板场景的关键结构。
|
||||||
|
- `ServerAliveCountMax=6`(默认 3):节点小宝 P2P 链路偶发抖动,放宽容忍避免隧道被误杀。
|
||||||
|
- 验证隧道:`timeout 3 bash -c '</dev/tcp/127.0.0.1/13389' && echo OK`。
|
||||||
|
- 隧道断了(症状:RDP 卡死、后台 ssh 报 "Timeout, server not responding")直接重跑上面命令即可。
|
||||||
|
|
||||||
|
## 3. 经跳板执行远程命令(非交互,运维日常)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rwin() { # rwin <win-02|win-03> "命令"
|
||||||
|
case "$1" in
|
||||||
|
win-02) h=fmq@192.168.6.102; p=1235 ;;
|
||||||
|
win-03) h=Administrator@192.168.6.103; p='dev#2dckj@zndx' ;;
|
||||||
|
esac
|
||||||
|
sshpass -p "$p" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
|
||||||
|
-o ProxyCommand="sshpass -p dckjzndx ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -W %h:%p dckj@100.66.1.5" \
|
||||||
|
"$h" "$2"
|
||||||
|
}
|
||||||
|
# 用例:rwin win-03 "docker ps"
|
||||||
|
```
|
||||||
|
|
||||||
|
`scripts/fetch_results.sh` / `scripts/deploy.sh` 已原生支持跳板:profile 里加
|
||||||
|
`REMOTE_PROXY_JUMP=dckj@100.66.1.5` 即自动 `-o ProxyJump`(win-01/win-02 两个 profile 已配好)。
|
||||||
|
|
||||||
|
## 4. 重要经验:Windows 节点重启后的恢复顺序
|
||||||
|
|
||||||
|
**Docker Desktop 是 GUI 程序,必须有所属用户的交互桌面会话才能启动引擎。**
|
||||||
|
从 SSH 远程 `Start-Process 'Docker Desktop.exe'` 无效——进程落在无桌面的 SSH 会话里,
|
||||||
|
GUI 初始化不了,引擎 named pipe(`dockerDesktopLinuxEngine`)不会创建,
|
||||||
|
`docker ps` 报 "unable to start" / "cannot find the file specified"。
|
||||||
|
|
||||||
|
正确的恢复链:
|
||||||
|
|
||||||
|
1. 确认节点小宝活着(能 SSH 进去即活着;服务 `NodeBabyLinkService` Auto 自启,一般不用管)。
|
||||||
|
2. 建隧道、RDP 登录对应账户的桌面(**必须是与 Docker 数据同账户的桌面**,
|
||||||
|
dckj-03 的 Docker 挂在 Administrator 下,登录别的用户如 gaoyuqi 没用)。
|
||||||
|
3. 登录后 Docker Desktop 自动启动(两台均已开 AutoStart),任务栏鲸鱼图标静止即就绪。
|
||||||
|
4. `dcts-node` 容器配了 restart 策略,引擎就绪后**自动恢复运行**,无需手动 compose up。
|
||||||
|
5. 验证:`rwin win-03 "docker ps"` 看到 `dcts-node Up`,再看看板心跳上线。
|
||||||
|
|
||||||
|
诊断命令速查(经 `rwin` 执行):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# 开机时间(判断是否重启过)
|
||||||
|
(Get-CimInstance Win32_OperatingSystem).LastBootUpTime
|
||||||
|
# 节点小宝服务状态
|
||||||
|
Get-CimInstance Win32_Service | Where-Object {$_.Name -match 'NodeBaby'} | Format-List Name,State,StartMode
|
||||||
|
# Docker 引擎是否就绪
|
||||||
|
docker ps
|
||||||
|
# 当前桌面会话归属(判断该登录哪个账户)
|
||||||
|
quser
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 本次事故时间线(2026-08-20)
|
||||||
|
|
||||||
|
- 两台 Windows 于 14:52 左右重启;节点小宝 Auto 自启正常(开机 16 秒内拉起),
|
||||||
|
网络链路一直通。
|
||||||
|
- Docker Desktop 虽配了 AutoStart,但因 **Administrator/fmq 未登录桌面**(Docker Desktop
|
||||||
|
依赖登录会话),引擎未启动,节点掉线。
|
||||||
|
- 远程 Start-Process 反复失败 → 查日志发现 backend 未被拉起 → `quser` 发现桌面会话
|
||||||
|
属于第三方用户且已断开 → 定位根因。
|
||||||
|
- 经 SSH 隧道 RDP 登录桌面后,Docker 引擎启动、`dcts-node` 容器自动恢复。
|
||||||
|
|
||||||
|
治本方向(未做,待定):给两台 Windows 配置 Administrator/fmq 自动登录
|
||||||
|
(注册表 `AutoAdminLogon`,密码明文存储需评估安全),彻底摆脱重启后必须人工 RDP。
|
||||||
@@ -0,0 +1,279 @@
|
|||||||
|
# sdB_cno 光谱计算小结
|
||||||
|
|
||||||
|
**日期**:2026-08-25
|
||||||
|
**数据来源**:分布式计算框架的任务记录数据库快照(共 21384 条任务执行记录)、2026-07-30 分布式框架上线前单机执行期经导入工具入库的成果(无任务行记录),及 `data/salvage/` 中的计算产物;单机执行期的起点与过程依据 tl208-s54 仓库 git 提交记录(首提交 2026-07-15)与 `cno_grid/EXPERIENCE.md` 调试记录。求解器为 TLUSTY v208(Hubeny & Lanz),光谱合成为 SYNSPEC。全部 9216 个网格点的大气模型均已收敛并通过物理有效性检验。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 摘要
|
||||||
|
|
||||||
|
本文总结 sdB_cno 模型大气网格 9216 个网格点的收敛方法分布、各收敛策略的构造与物理依据,以及策略体系随计算进程的演化。60.5% 的网格点由分级松弛冷启动收敛(LTE 初猜逐步放开至全非局域热动平衡),38.8% 由近邻收敛模型热启动收敛,0.8% 依赖稳定化参数(布居变化阻尼与微布居置零)在低温富氦区收敛;另有三类链内参数延拓(Teff 延拓、稳定化多档、C/N 丰度轴延拓)处理常规路径失效的困难网格点。各策略在参数空间中的分布与恒星大气物理预期一致:有效温度越高、表面重力越低,能级布居偏离局域热动平衡越大,冷启动初猜越远离真实解,热启动的必要性随之上升。全部收敛结果均通过统一的物理有效性检验(波长积分流量守恒、能量守恒、温度结构合理性、迭代收敛判据、数值有效性)。计算成本方面,9216 点制胜任务合计 5791 核时(单点中位数约 24–36 分钟),计入失败尝试后的全周期投入 14038 核时(约 585 核日)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、网格定义、求解器与收敛判据
|
||||||
|
|
||||||
|
### 1.1 网格
|
||||||
|
|
||||||
|
网格面向热亚矮星 B 型(sdB,极端水平支)恒星 CNO 丰度响应研究,共 9216 个模型:Teff ∈ {20, 25, 30, 35, 40, 45, 50, 55, 60} kK(9 值)× log g ∈ {5.0, 5.5, 6.0, 6.5} × log He ∈ {−4, −2, 0, +2} × log C ∈ {−4, −3, −2, −1} × log N ∈ {−4, −3, −2, −1} × log O ∈ {−4, −3, −2, −1}(9 × 4⁵)。模型命名约定 `t{Teff/kK}_g{logg}_he{logHe}_c{logC}_n{logN}_o{logO}`。
|
||||||
|
|
||||||
|
### 1.2 求解器与初猜问题
|
||||||
|
|
||||||
|
TLUSTY 求解统计平衡方程(能级布居速率方程)、辐射转移与能量方程的强非线性耦合系统,核心算法为完全线性化(complete linearization;Auer & Mihalas 1969,TLUSTY 实现:Hubeny 1988),并辅以 Kantorovich 型雅可比更新与 Ng 加速。此类牛顿型迭代对初猜高度敏感:仅当初猜位于真解的吸引域内时迭代才收敛。本文的核心问题即——**对每个网格点,如何为其构造一个落在吸引域内的初猜,以及当初猜难以获得时如何延拓逼近**(方法学文献见文末参考文献,按主题与本文小节对应)。
|
||||||
|
|
||||||
|
### 1.3 收敛判据与物理有效性检验
|
||||||
|
|
||||||
|
"收敛"采用 TLUSTY 自身判据:相邻迭代间全部深度上各求解量(温度结构、能级布居)的最大相对变化 CHMAX < 1×10⁻³(收敛史记录于 fort.9 文件),且末次迭代全深度均满足。收敛仅表明迭代达到不动点,**不保证是物理解**,故所有收敛结果另需通过五项物理有效性检验:
|
||||||
|
|
||||||
|
1. 温度结构合理性检验(温度—深度分布单调性与量级);
|
||||||
|
2. 迭代收敛判据(fort.9 末次迭代全深度最大相对变化 < 1×10⁻³);
|
||||||
|
3. 数值有效性(全部输出无 NaN/Inf);
|
||||||
|
4. 波长积分流量守恒(bolometric flux:∫F_λ dλ ≈ σT_eff⁴,实测比值如 1.0013;该检验曾存在遗漏 4π 立体角因子的程序缺陷,2026-08-17 修正);
|
||||||
|
5. 能量守恒检验。
|
||||||
|
|
||||||
|
收敛策略只负责把迭代带入不动点;结果是否为物理上可接受的大气模型,由上述检验统一裁决。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、收敛策略体系总览
|
||||||
|
|
||||||
|
策略体系分两级:**策略链**(`[冷启动, 热启动, 稳定化热启动]`,由调度端逐项回退,热启动失败后按距离度量轮换初猜模型),以及每条策略链内部的**链内延拓回退**(Teff 自适应延拓、固定阶梯、稳定化多档、丰度轴延拓)。
|
||||||
|
|
||||||
|
| 收敛方法 | 点数 | 占比 | 物理内涵 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `cold_run`(冷启动) | 5573 | 60.5% | 从灰大气 LTE 解出发,分级松弛至全非 LTE |
|
||||||
|
| `seed_step`(热启动) | 3573 | 38.8% | 以同物理族近邻的收敛模型为初猜 |
|
||||||
|
| `seed_step_stab`(稳定化热启动) | 70 | 0.8% | 热启动链叠加布居稳定化参数,用于低温富氦区 |
|
||||||
|
|
||||||
|
(含 9 个 2026-08-01/02 完成的早期网格点,其状态字段晚于完成时间引入而为空值,收敛性已由任务记录与初猜复用记录独立验证,详见技术附注。)
|
||||||
|
|
||||||
|
链内延拓在最终成功任务中的使用量:稳定化多档 47 点、Teff 自适应延拓 12 点、C/N 丰度轴延拓 4 点、固定阶梯末步 3 点——使用频率低,但均为最困难网格点的决定性路径。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、各策略的构造与物理依据
|
||||||
|
|
||||||
|
### 3.1 冷启动链:LTE → 连续谱非 LTE → 全非 LTE
|
||||||
|
|
||||||
|
| 阶段 | 主要设置 | 迭代上限 | 物理含义 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `lte` | LTE 解(灰大气初猜) | — | 布居仅由局域温度、密度决定的零级近似 |
|
||||||
|
| `nc` | 关闭谱线 | 10 | 连续谱不透明度自洽的非 LTE 预解 |
|
||||||
|
| `nl` | 全谱线(ILVLIN=100),强制收敛 | 100 | 全谱线统计平衡 + 辐射转移耦合终解 |
|
||||||
|
|
||||||
|
**依据**:LTE 解虽然在高温低密度大气中偏离真实解很远,但提供了一个"拓扑正确"的起点;先在连续谱层面引入非 LTE,再放开全部谱线,使非线性逐级进入迭代。这是 Hubeny/Lanz 分级松弛的经典构造。适用域为参数平缓、LTE 初猜仍在吸引域内的区域:实测 Teff ≤ 35 kK 区间冷启动成功率 71–77%。
|
||||||
|
|
||||||
|
### 3.2 热启动链:近邻收敛模型初猜 + 参数延拓
|
||||||
|
|
||||||
|
| 阶段 | 主要设置 | 迭代上限 | 物理含义 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `seed_nc` | 关闭谱线,松弛因子 0.3 | 20 | 以近邻模型为初猜的连续谱预适应 |
|
||||||
|
| `nl` | 全谱线,松弛因子 0.5,强制收敛 | 100 | 终解(欠松弛抑制丰度突变处的过冲) |
|
||||||
|
|
||||||
|
**依据**:大气结构随 (Teff, log g, 丰度) 连续变化,即静力解构成参数空间中的连续解分支;一个已收敛的近邻模型(本文取严格同物理族:Teff/log g/log He 相同、仅 CNO 丰度不同)比 LTE 初猜距离目标解近若干数量级。松弛因子 0.3/0.5 表示仅施加部分牛顿修正(欠松弛),以适应丰度改变引起的陡峭响应。
|
||||||
|
|
||||||
|
**初猜选择度量**:按 CNO 距离取最近邻,但区分方向——目标相对初猜"减丰"方向权重 ×1、"增丰"方向 ×4(增丰使金属线不透明度上升,非线性更强);初猜失败后排除已用模型逐次轮换。
|
||||||
|
|
||||||
|
**参数延拓**:实测单步收敛域约为 ΔTeff ≤ 2500 K 且 Δlog g ≤ 0.25 dex,当初猜与目标参数差超出该域时:
|
||||||
|
|
||||||
|
- **Teff 自适应延拓**:从初猜 Teff 向目标逐步推进,步长 1250 K(与网格间距一致),失败步二分(下限 25 K,触底判定该方向解分支折叠,中止),最多 48 步;中间收敛模型自动登记为可用初猜。
|
||||||
|
- **固定阶梯**:沿差值更大的轴均匀切分(中间步 ≤ 4),末步强制收敛判据。
|
||||||
|
|
||||||
|
**依据**:解分支沿参数曲面连续延伸,但一步跨越过大将离开吸引域;小步延拓(continuation method)即逐点跟踪解分支,步长自适应是伪弧长延拓的简化实现。
|
||||||
|
|
||||||
|
### 3.3 稳定化热启动链:低温富氦区的布居极限环
|
||||||
|
|
||||||
|
热启动链各步叠加两个 TLUSTY 稳定化参数:**DPSILG**(限制能级布居单次迭代的最大相对变化,即布居阻尼)与 **POPZER**(布居置零阈值,低于该值的布居置零,消除深壳层痕量能级的数值噪声)。此外设三档链内回退(DPSILG, POPZER = (2.0, 3×10⁻¹¹), (2.0, 1×10⁻¹¹), (2.5, 1×10⁻¹¹)),在常规路径全部失败后自原始初猜逐档重试。
|
||||||
|
|
||||||
|
**依据**:20 kK 富氦大气的病灶是 He I/He II 电离前沿处能级布居的**极限环振荡**——电离平衡陡变区对温度扰动的响应使迭代映射出现周期性数值不稳定,而静力解本身存在。布居阻尼抑制振荡增幅,微布居置零消除噪声源。批量实测(65 个困难点)三档互补,联合回收 41 点,全部通过温度结构检验。
|
||||||
|
|
||||||
|
**域边界**:该族参数仅在 Teff ≤ 30 kK 有效;高温区实测对本已可收敛的迭代加档反而致散(17 次迭代后相对变化发散至 4×10¹⁶),故按温度域门控启用。
|
||||||
|
|
||||||
|
### 3.4 丰度轴延拓:高温高金属区的路径切换
|
||||||
|
|
||||||
|
Teff 60 kK、log g 5.0 的高金属角,实测 Teff 轴、log g 轴、O 丰度轴延拓全部在目标前折叠,而 **C 或 N 丰度轴延拓全部走通**——静力解存在,只是可达方向在 C/N。配方:严格同物理族初猜沿 C 轴(优先,3 个测试点全部验证)或 N 轴向目标丰度延拓:初始步长 0.2 dex,成功 ×1.4(上限 0.25 dex),失败二分(下限 0.025 dex),每轴 ≤ 40 步,失败步产物不作为后续初猜。仅 Teff > 30 kK 域启用(低温区由稳定化档覆盖)。
|
||||||
|
|
||||||
|
**依据**:CNO 丰度直接改变金属线不透明度与辐射加速,结构响应是非线性的,但解分支沿不同参数方向的连通性不同——延拓方向本身是自由度,"更换延拓路径"与"调整延拓步长"同属延拓法的基本手段。
|
||||||
|
|
||||||
|
### 3.5 延拓阶段迭代上限(100 → 300)
|
||||||
|
|
||||||
|
全部延拓/阶梯阶段的迭代上限由 100 提升至 300。生产实证(t60000_g5.0_he-4_c-4_n-4_o-1,N 轴延拓在 log N = −3.53 处):迭代残差 0.002–0.005 在 100 次迭代上限处仍单调下降——表明迭代仍在正常收敛进程中被上限过早截断,并非发散;此前该现象被误判为解分支折叠。TLUSTY 达到收敛即停止,提高上限只对慢收敛阶段给出余量,不影响快阶段,也不影响真发散阶段的提前中止。最终网格点即经此修复后收敛:丰度轴延拓三段分别以 107/107/175 次迭代收敛(全部超过旧上限 100),终解最大相对变化 9.46×10⁻⁴。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、收敛方法在参数空间中的分布及其物理解释
|
||||||
|
|
||||||
|
### 4.1 有效温度(每档 1024 点)
|
||||||
|
|
||||||
|
| Teff | 冷启动 | 热启动 | 稳定化 | 冷启动占比 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 20 kK | 725 | 233 | 66 | 70.8% |
|
||||||
|
| 25 kK | 793 | 231 | 0 | 77.4% |
|
||||||
|
| 30 kK | 760 | 264 | 0 | 74.2% |
|
||||||
|
| 35 kK | 756 | 268 | 0 | 73.8% |
|
||||||
|
| 40 kK | 587 | 437 | 0 | 57.3% |
|
||||||
|
| 45 kK | 525 | 499 | 0 | 51.3% |
|
||||||
|
| 50 kK | 558 | 466 | 0 | 54.5% |
|
||||||
|
| 55 kK | 457 | 567 | 0 | 44.6% |
|
||||||
|
| 60 kK | 412 | 608 | 4 | 40.2% |
|
||||||
|
|
||||||
|
**解释**:Teff ≤ 35 kK 冷启动主导(约 74%),≥ 40 kK 热启动主导。有效温度升高时辐射场增强而电子碰撞速率相对下降,能级布居的偏离系数(departure coefficients)增大,LTE 初猜(偏离系数恒为 1)远离真实解,牛顿迭代起点落在吸引域之外;而已收敛的近邻模型始终位于同一解分支附近,是更可靠的内插起点。
|
||||||
|
|
||||||
|
### 4.2 表面重力(每档 2304 点)
|
||||||
|
|
||||||
|
| log g | 热启动占比 | 稳定化点数 |
|
||||||
|
|---|---|---|
|
||||||
|
| 5.0 | 55.1% | 4 |
|
||||||
|
| 5.5 | 47.1% | 4 |
|
||||||
|
| 6.0 | 31.3% | 18 |
|
||||||
|
| 6.5 | 21.6% | 44 |
|
||||||
|
|
||||||
|
**解释**:低重力对应低密度大气,电子碰撞激发/电离速率下降,布居由辐射场主导,与 LTE 偏离更大,冷启动更难。稳定化点数则向高重力集中——布居极限环需要足够密集的 He 电离前沿区。
|
||||||
|
|
||||||
|
### 4.3 氦丰度(每档 2304 点)
|
||||||
|
|
||||||
|
| log He | 冷启动占比 | 稳定化点数 |
|
||||||
|
|---|---|---|
|
||||||
|
| −4.0 | 57.2% | 3 |
|
||||||
|
| −2.0 | 54.7% | 1 |
|
||||||
|
| 0.0 | 62.4% | 0 |
|
||||||
|
| +2.0 | 67.6% | 66 |
|
||||||
|
|
||||||
|
**解释**:氦是主要电子施主,氦丰度升高使电子密度上升、碰撞耦合增强,布居更接近 LTE 行为,冷启动反而更容易。唯一的例外正是 20 kK 富氦角——66 个稳定化点集中于 (Teff = 20 kK, log He = +2, log g = 5.5–6.5),物理上"应当容易"的参数组合恰是 He I/He II 电离前沿极限环的发生区,需要最强的数值稳定化。
|
||||||
|
|
||||||
|
### 4.4 CNO 丰度轴(各轴 4 值 × 2304 点)
|
||||||
|
|
||||||
|
C 轴冷启动占比 58.7–61.8%,N 轴 57.3–64.1%,O 轴 60.4–61.1%——分布近乎平坦(±3%)。在本网格的 CNO 丰度范围内(各跨 3 dex),金属丰度不是收敛难度的主控因素(相对 Teff/log g/He 而言);丰度轴延拓仅在高温高金属角的个别点成为必经路径。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、初猜模型复用网络
|
||||||
|
|
||||||
|
热启动是一个自我强化的复用体系:全部计算中共执行 9203 次以其他点收敛模型为初猜的任务,**2727 个网格点(占 29.6%)至少一次被用作初猜供体**。供体分布高度集中于 55–60 kK、log g 5.0–5.5 区域(热启动最主要的需求区),形成"先收敛的骨干模型带动邻近区域"的传播结构。供体使用频次前列:
|
||||||
|
|
||||||
|
| 供体模型 | 被用作初猜次数 |
|
||||||
|
|---|---|
|
||||||
|
| t55000_g5.0_he-2_c-3_n-4_o-4 | 222 |
|
||||||
|
| t50000_g5.0_he-2_c-1_n-2_o-2 | 153 |
|
||||||
|
| t60000_g5.0_he-4_c-3_n-2_o-4 | 121 |
|
||||||
|
| t55000_g5.0_he-4_c-3_n-2_o-2 | 105 |
|
||||||
|
| t60000_g5.0_he-2_c-1_n-2_o-2 | 101 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、收敛所需计算时间
|
||||||
|
|
||||||
|
每个网格点记录其最终成功任务(制胜任务)的实际执行时长(单机执行期导入的成果为其单机运行记录,量级与分布式执行一致:4335 个此类点平均 27.2 分钟、跨度 9.5–153.1 分钟)。全部 9216 点的制胜任务耗时按方法统计:
|
||||||
|
|
||||||
|
| 方法 | 点数 | 中位数 | P75 | P90 | 平均 | 最长 |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| 冷启动 | 5573 | 24.3 min | 29.1 min | 47.7 min | 31.7 min | 141.5 min |
|
||||||
|
| 热启动 | 3573 | 36.2 min | 49.2 min | 94.9 min | 47.1 min | 153.1 min |
|
||||||
|
| 稳定化热启动 | 70 | 29.9 min | 47.5 min | 63.8 min | 36.4 min | 123.2 min |
|
||||||
|
|
||||||
|
冷启动任务的耗时中位数反而最短:冷启动得以成功的点本就是参数平缓区域,迭代次数少;热启动更长(中位数 36 分钟、P90 达 95 分钟)——需要热启动的点收敛更慢,且参数延拓(Teff 延拓最多 48 步、丰度轴延拓最多 40 步)作为链内回退全部计入单任务时长,慢收敛长尾即由此而来。按 Teff 分层的中位数在 22–38 分钟间无系统趋势,P90 的层间差异(34–99 分钟)主要反映各层困难点比例与所在批次节点的硬件差异。稳定化任务时长介于两者之间——稳定化旋钮本身不显著增加单步成本,其耗时结构需按两层重试口径分别核算:
|
||||||
|
|
||||||
|
- **任务内链内重试已全部计入制胜任务时长**:多档稳定化(自原始初猜逐档重跑最后收敛阶段)与延拓中间模型的逐段计算均发生在同一任务内。70 个稳定化点的制胜任务中 47 个含多档重试、4 个含延拓阶段;典型如某一制胜任务内 seed_nc → nl → nl_direct → nl_stab2(未收敛)→ nl_stab3(收敛)五段合计约 30 分钟,全部计入该点时长(与表中位数一致)。
|
||||||
|
- **跨任务重试不计入制胜时长,仅计入全周期总账**:初猜轮换与策略回退产生的每次独立任务各有自己的执行时长。70 个稳定化点的制胜任务合计仅 42.4 核时,但其全部 1588 次任务执行合计 926 核时(平均每点 22.7 次任务、13.2 核时)——全周期成本约为制胜成本的 22 倍,是全网格中重试强度最高的群体(作为对比,全网格平均为 2.4 倍)。
|
||||||
|
|
||||||
|
**总量核算(核时)**:
|
||||||
|
|
||||||
|
- 制胜任务合计 **5791 小时**(约 241 核日),单点平均 37.7 分钟;
|
||||||
|
- 计入全部失败尝试后,任务执行总量 **14038 小时**(约 585 核日),为制胜时长的 2.4 倍——即平均每个网格点的全周期投入约 1.5 核时,其中约 40% 消耗于最终未被采纳的尝试;743 个任务接近 7200 秒超时上限;
|
||||||
|
- 光谱合成(SYNSPEC)单点约 4 秒(单机期实测 3.6 秒),相对大气计算可忽略;本网格当前阶段的分布式任务仅含大气计算。
|
||||||
|
|
||||||
|
时长均为节点实际执行的墙钟时间,不同批次节点硬件不同,层间横向比较包含硬件异质性。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、策略体系的演化:基于 21384 次任务记录
|
||||||
|
|
||||||
|
本节回答"每一层策略是哪类失败引入的"。计算分两段:**单机执行期**(2026-07-15 起,07-29 止)以单机流程完成三千余点收敛后,转入**分布式阶段**(07-28 框架首提交、07-30 首批任务入库),共执行 21384 个任务(平均每点 2.3 次),其中收敛 6283、失败/超时 15101,任务级成功率 29.4%——策略体系是随失败类型逐步建构的。按时间聚合为十个阶段:
|
||||||
|
|
||||||
|
| 阶段 | 日期 | 任务数(收敛/失败) | 失败特征 | 诊断与变更 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| ⓪ 单机执行期 | 07-15→07-29(git:单机管线 07-15/23/27 三次提交;分布式框架 07-28 首提交) | 约 4300 点收敛(无分布式任务记录) | nc 阶段发散;CNO 谱线全 NaN;宽松判据伪收敛(见 7.2) | 单机管线(16 核并行、断点续算、失败隔离)确立三步法收敛链与"金属谱线自洽进入大气模型"方案(纯 H+He 大气无法产生 CNO 谱线),并实现最近邻初猜复用与冷启动失败→热启动回退——此即策略链体系的原型;定型求解器配方(收敛判据取默认 1×10⁻³、nc 步 NITER=10、He 模型原子 14 能级、NFREAD=2000)。完成约 4300 点收敛(数据库中无分布式完成记录的收敛点共 4335 个:冷启动 2898、热启动 1437,其中 3876 个从未被分布式系统重算);成果经导入工具写入数据库,构成分布式初期的初猜库——07-30/31 首批 1252 个热启动任务中 1249 个的初猜供体属此类无任务记录的点;07-30 分布式框架上线接管剩余计算 |
|
||||||
|
| ① 分布式初期 | 07-30→08-04 | 8894(4978/3916) | 大气输出 >10% NaN 行 1775 例 | 高温低重力区冷启动大量发散;热启动(最近邻初猜复用)继承自单机管线并自始使用,08-04 将其正式化为策略链体系 |
|
||||||
|
| ② 物理检验强化期 | 08-06→08-10 | 7905(0/7905) | NaN/Inf 大气 2707 例;流量比 ≈0.08(后证实为检验缺陷);光谱整表 NaN | 五项物理检验与输入文件结构化上线,输入谱线数据错位与检验程序缺陷集中暴露,当周零收敛 |
|
||||||
|
| ③ 根因分析与方案证伪 | 08-11→08-14 | 0(停机分析) | — | 根因定位为吸引域狭窄且缺乏自适应初猜机制;数值参数调整方案经 6 点 × 4 配置 = 24 次对照实验全部证伪(见 7.2),方向转向初猜质量 |
|
||||||
|
| ④ 热启动批量复算 | 08-15→08-16 | 3162(905/2257) | 非收敛发散 | 以热启动规模化重算存量失败点 |
|
||||||
|
| ⑤ 参数延拓引入 | 08-17 | 547(279/268) | 初猜与目标参数差超出单步收敛域 | 引入固定阶梯与 Teff 自适应延拓(实测确定单步收敛域);修正流量检验 4π 因子缺陷;增设 NaN 伪收敛否决 |
|
||||||
|
| ⑥ 低温富氦区稳定化 | 08-18→08-19 | 682(77/605) | 20 kK 富氦区布居极限环 | 引入稳定化热启动策略与三档 DPSILG/POPZER 回退(65 困难点联合回收 41) |
|
||||||
|
| ⑦ 初猜轮换机制 | 08-20 | 0(离线开发) | 同一初猜反复失败 | 调度端排除已用初猜,按方向加权距离轮换最近未用近邻 |
|
||||||
|
| ⑧ 高温高金属区丰度延拓 | 08-21 | 189(43/146) | 60 kK/log g 5.0 角多轴折叠 | 引入 C/N 丰度轴延拓;稳定化参数按温度域门控;延拓迭代上限 100→300 |
|
||||||
|
| ⑨ 收尾 | 08-22→08-24 | 5(1/4) | 完成状态翻转与回退派发的竞态 | 最终点 t60000_g5.0_he-4_c-4_n-4_o-1 经初猜轮换(N 丰度 −3.62 中间模型)+ 丰度轴延拓收敛(107/107/175 次迭代);竞态由数据库层约束修复 |
|
||||||
|
|
||||||
|
策略链形式的演化轨迹(任务记录中各链形式的首现日期):
|
||||||
|
|
||||||
|
```
|
||||||
|
07-15 单机管线(原型):冷启动链 + 冷启动失败→最近邻热启动回退
|
||||||
|
07-30 [cold_run] ← 分布式初期纯冷启动(早期记录归档回填)
|
||||||
|
08-04 [cold_run, seed_step] ← 策略链体系上线,热启动入链
|
||||||
|
08-19 [cold_run, seed_step, seed_step_stab] ← 稳定化补入链尾
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.1 分布注释
|
||||||
|
|
||||||
|
网格点的重试计数字段为分布式阶段后期引入;计零的 3876 个点即单机执行期导入、从未被分布式系统重算的成果(其中热启动完成 1056 个),故重试分布统计仅对分布式阶段有效。重试 ≥ 16 次的 114 个点几乎全部落在稳定化策略覆盖的两个困难区域;单点最深重试 103 次,属调度早期配置不当的遗留记录。
|
||||||
|
|
||||||
|
### 7.2 五次被证伪的方案及其方法论价值
|
||||||
|
|
||||||
|
前两次证伪发生在单机执行期(记录于 `cno_grid/EXPERIENCE.md`):
|
||||||
|
|
||||||
|
1. **宽松收敛判据导致伪收敛**(单机期):早期以 CHMAX = 0.1 运行,曾据以得出"全部边界点收敛"的结论;改回默认判据 1×10⁻³ 复测后该结论被推翻。教训:收敛结论的有效性完全取决于判据的严格性——判据取值须与求解器默认值一致,任何放宽都需单独论证并复测。
|
||||||
|
2. **丰度输入约定误读**(单机期):TLUSTY 的 abn 输入字段语义为 0 = 太阳丰度、负值 = 太阳丰度的倍数、正值 = 绝对数密度比 N(X)/N(H);早期将 logC = 0 等取值直接换算写入,实为 C/H = 1.0(约 4000 倍太阳丰度),nc 阶段因此普遍发散,且一度被误归因于高温区物理。教训:非标准输入约定必须对照求解器手册逐字段核实。
|
||||||
|
3. **数值阻尼与加速参数无法弥补初猜缺陷**(08-12 证伪):调整 Kantorovich/ Ng 加速与布居阻尼(ITEK/IACC/DPSILG)的 24 次对照实验 0 次收敛——其中 ITEK=0 实为冻结布居、IACC=0 被求解器内部钳制为空操作;真正禁用加速后发散幅度下降 10¹²–10¹⁴ 倍但仍不收敛(末次残差 10³–10⁷)。结论:牛顿型迭代起点在吸引域之外时,松弛与加速参数均无效;**初猜质量优先于数值参数**。附带教训:此前"禁用加速后残差降至 0.2"的结论源于收敛史文件(fort.9)的部分读取假象——TLUSTY 逐深度写入、缓冲未刷新时只读到部分深度的最大值。
|
||||||
|
4. **检验程序自身须经检验**(08-06 引入,08-17 修正):流量守恒检验遗漏 4π 立体角因子,将正确的波长积分流量(∫F_λdλ/σT_eff⁴ ≈ 0.08)判为失败约千例。新检验上线引发的失败潮中,须先审查检验自身再归因于物理。
|
||||||
|
5. **迭代上限耗尽不等于发散**(08-21 修正):残差在上限处仍单调下降,说明迭代仍在收敛进程中被上限截断,并非解分支折叠;延拓迭代上限提升至 300 后,最终点三段延拓以超过旧上限的迭代数(107/107/175)收敛,构成直接反证。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、结论
|
||||||
|
|
||||||
|
sdB_cno 网格 9216 个模型大气的收敛路径构成为:60.5% 分级松弛冷启动(低 Teff、高 log g、富 He 的平缓参数域),38.8% 近邻收敛模型热启动(高 Teff、低 log g 的强非 LTE 域,29.6% 的网格点被复用为初猜供体,形成自强化传播网络),0.8% 稳定化热启动(20 kK 富氦角 He I/He II 电离前沿布居极限环);叠加三类链内延拓(Teff 自适应延拓、稳定化多档、C/N 丰度轴延拓)与延拓迭代上限 300,处理"静力解存在但延拓路径曲折"的极端网格点。策略分布与恒星大气物理一致:非 LTE 偏离程度(由 Teff、log g、电子施主丰度控制)决定冷启动初猜的适用边界。全部结果通过统一的五项物理有效性检验——策略决定如何到达收敛域,物理解由检验裁决。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 参考文献(按主题,与本文小节对应)
|
||||||
|
|
||||||
|
**求解器与控制参数**
|
||||||
|
|
||||||
|
1. Auer, L. H., & Mihalas, D. 1969, ApJ, 158, 641 —— 完全线性化方法原始文献(§1.2、§3.1 的方法出处)。
|
||||||
|
2. Hubeny, I. 1988, Comput. Phys. Commun., 52, 103 —— TLUSTY 程序原始文献。
|
||||||
|
3. Hubeny, I., & Lanz, T. 2017, arXiv:1706.01859(用户指南卷 I:简明指南)、arXiv:1706.01935(卷 II:参考手册——迭代控制参数的权威定义所在;同系列另有卷 III 操作手册)。
|
||||||
|
4. Mihalas, D. 1978, Stellar Atmospheres, 2nd ed.(San Francisco: Freeman);Hubeny, I., & Mihalas, D. 2014, Theory of Stellar Atmospheres(Princeton Univ. Press)——教科书;开放获取 NLTE 入门综述:Kubát, J. 2015, in Determination of Atmospheric Parameters of B-, A-, F- and G-Type Stars, ed. E. Niemczura et al.(Springer),arXiv:1406.3553("Basics of the NLTE physics")。
|
||||||
|
|
||||||
|
**迭代加速与统计平衡求解**
|
||||||
|
|
||||||
|
5. Ng, K.-C. 1974, J. Chem. Phys., 61, 2680 —— Ng 加速方法原始文献(本文§7.2 证伪实验涉及的 IACC/Ng 加速)。
|
||||||
|
6. Olson, G. L., Auer, L. H., & Buchler, J.-R. 1986, JQSRT, 35, 431 ——对角 Λ 算子近似,加速 Λ 迭代(ALI)。
|
||||||
|
7. Rybicki, G. B., & Hummer, D. G. 1991, A&A, 245, 171 —— 多能级 ALI 与预条件统计平衡方程。
|
||||||
|
8. Hubeny, I., Lanz, T., & Jeffery, C. S. 1994, A&A, 282, 151 —— 混合完全线性化/ALI 处理金属线覆盖(TLUSTY 现代架构的基础)。
|
||||||
|
|
||||||
|
**网格延拓与初猜实践**
|
||||||
|
|
||||||
|
9. Lanz, T., & Hubeny, I. 2003, ApJS, 146, 417 —— OSTAR2002 网格论文;以邻近已收敛模型为初猜逐点推进网格的标准实践(§3.2 的直接先例)。
|
||||||
|
10. Lanz, T., & Hubeny, I. 2007, ApJS, 169, 83 —— BSTAR2006 网格(同上)。
|
||||||
|
11. Keller, H. B. 1977, in Applications of Bifurcation Theory, ed. P. H. Rabinowitz(New York: Academic Press), 359 —— 伪弧长延拓,参数延拓法与折叠点处理的经典文献(§3.2/§3.4 的方法学基础)。
|
||||||
|
12. Allgower, E. L., & Georg, K. 2003, Introduction to Numerical Continuation Methods(SIAM, Classics in Applied Mathematics 45;1990 年 Springer 初版书名为 Numerical Continuation Methods: An Introduction)—— 延拓法教科书(可选)。
|
||||||
|
|
||||||
|
**热亚矮星(sdB/sdO)应用**
|
||||||
|
|
||||||
|
13. Heber, U. 2016, PASP, 128, 082001 —— 热亚矮星综述(§1.1 科学背景)。
|
||||||
|
14. Németh, P. 2012, MNRAS, 427, 2180 —— TLUSTY/SYNSPEC 分析热亚矮星样本。
|
||||||
|
15. Németh, P. 2014, ASP Conf. Ser., 481, 95 —— O/B 型亚矮星合成光谱网格。
|
||||||
|
16. Pacheco, T. A., et al. 2021, ApJS, 256, 41 —— 亚矮星非 LTE 线覆盖大气模型与光谱网格(H/He 与金属丰度维度,与本文网格同类);更新版 arXiv:2307.08362。
|
||||||
|
|
||||||
|
**关于原创性的说明**:§3.3 的稳定化档位配方(DPSILG/POPZER 逐档组合)、布居极限环的判别-处置流程、§3.2 的方向加权初猜距离与轮换调度,属本项目工程实现,文献中未见面世;上述文献支撑的是其方法学基础——完全线性化对初猜的敏感性、参数延拓、迭代加速、以及网格逐点热启动的通行实践。参数语义的权威定义见 Hubeny & Lanz (2017) 卷 II。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 技术附注(工程细节,与物理结论无关)
|
||||||
|
|
||||||
|
- **数据来源**:server 端 SQLite 快照;`grid_points` 表记录逐点最终状态与方法、制胜任务时长(`last_elapsed_sec`),`tasks` 表记录每次任务执行(时间戳、耗时、初猜供体、失败阶段、错误摘要、策略链)。
|
||||||
|
- **单机执行期版本记录**:单机代码仓库为 tl208-s54(git 首提交 2026-07-15;cno_grid 单机管线于 07-15/23/27 三次提交,run_grid.py 含笛卡尔积网格、断点续算、最近邻初猜复用、冷启动失败→热启动回退、失败隔离、16 核并行);调试史与配方复盘见 `cno_grid/EXPERIENCE.md`(如实记录 8 项已纠正的错误)。分布式框架 dcts 为该仓库内嵌的独立 git 仓库,首提交 2026-07-28。
|
||||||
|
- **单机执行期成果的数据库痕迹**:无分布式完成记录的收敛点共 4335 个(冷启动 2898、热启动 1437):3876 个无任何任务行且重试计数为零(从未被分布式系统碰过);另 459 个曾被分布式阶段重试(重试计数 > 0)但未产生完成记录,保留单机期结果。07-30/31 首批热启动任务 1252 个中 1249 个的初猜供体属于此类点,即分布式初期的初猜库直接继承自单机期成果。
|
||||||
|
- **9 个空状态历史点**:完成于 2026-08-01/02(全管线、末次迭代最大相对变化 1.14×10⁻⁴–9.33×10⁻⁴),早于阶段状态列引入时间(08-06,加列不回填),此后无任务再执行;数据库迁移 M14 将按"已完成 + 方法已记录"条件回填为收敛,快照实测恰为 9 行。9 点均登记为可用初猜供体,其中 6 点被后续任务实际使用。
|
||||||
|
- **光谱合成**:本工作流 SYNSPEC 阶段当前关闭;历史记录中 459 个光谱失败与 1305 个空值属早期 TLUSTY-only 重算批,不在本文收敛判定范围。
|
||||||
|
- **策略链字段**:08-04 策略体系上线前的任务记录(含 07-31 批 1252 个热启动任务)归档时统一回填为 `["cold_run"]`,此后记录携带真实链形式。
|
||||||
|
- **代码版本**:阶段⑥⑨(稳定化、初猜轮换、丰度轴延拓、迭代上限、竞态修复)的实现位于版本控制工作树,最后提交停留在 2026-08-17(b058e66)。
|
||||||
@@ -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}
|
||||||
|
```
|
||||||
@@ -45,6 +45,10 @@
|
|||||||
- **原因**:在较长时效(如1-2小时)的运算完结回传一瞬间,Server 恰遇热更重启或遭遇防火墙短暂会话剔除。
|
- **原因**:在较长时效(如1-2小时)的运算完结回传一瞬间,Server 恰遇热更重启或遭遇防火墙短暂会话剔除。
|
||||||
- **容灾机制**:Worker 内置了超强的 8 轮指数级自适应退避长跳上报防护(跨度可自 1s 到 60s 顺次延迟,支撑 2 分钟以上的长时断裂耐受窗口);上报失败时产物仍会**尽力归档**到本地 `DCTS_RESULT_DIR/<model_name>/`(白名单保留科学产物),沙盒随后清理。若真正超出了总重试界限,可从 `result_dir/<model_name>/` 直接调出最终收敛物(`.7`/`.spec` 等)手工补录。
|
- **容灾机制**:Worker 内置了超强的 8 轮指数级自适应退避长跳上报防护(跨度可自 1s 到 60s 顺次延迟,支撑 2 分钟以上的长时断裂耐受窗口);上报失败时产物仍会**尽力归档**到本地 `DCTS_RESULT_DIR/<model_name>/`(白名单保留科学产物),沙盒随后清理。若真正超出了总重试界限,可从 `result_dir/<model_name>/` 直接调出最终收敛物(`.7`/`.spec` 等)手工补录。
|
||||||
|
|
||||||
|
### 现象 4:Windows 节点重启后掉线(Docker Desktop 未自启)
|
||||||
|
- **原因**:Docker Desktop 是 GUI 程序,必须有所属用户的**交互桌面会话**才能启动引擎;重启后无人登录桌面时,即使配了 AutoStart 引擎也不会起来(节点小宝网络链路本身 Auto 自启,不受影响)。
|
||||||
|
- **解决办法**:经跳板 SSH 隧道转发 RDP 登录对应账户桌面,Docker 引擎随登录自启、`dcts-node` 容器靠 restart 策略自动恢复。完整拓扑、凭据、隧道命令与诊断速查见 **[remote_desktop_via_jump.md](remote_desktop_via_jump.md)**。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. 日志与现场诊断
|
## 3. 日志与现场诊断
|
||||||
|
|||||||
+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}"
|
||||||
|
|
||||||
# 若已有活跃的主连接 (如批量部署父进程已建立),直接复用,避免重复认证。
|
# 若已有活跃的主连接 (如批量部署父进程已建立),直接复用,避免重复认证。
|
||||||
|
|||||||
+60
-16
@@ -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 .)
|
||||||
echo -e " ${YELLOW}[→]${NC} scp 目录级增量: ${u}@${ip}:${rdir}/ 共 ${total} 个,缺失 ${new} 个 → ${dest}/"
|
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}/"
|
||||||
|
fi
|
||||||
if [ "${new}" -eq 0 ]; then
|
if [ "${new}" -eq 0 ]; then
|
||||||
echo -e " ${GREEN}[√]${NC} 本地已是最新,无新增目录"
|
if [ "${FORCE_REPULL}" = "true" ]; then
|
||||||
|
echo -e " ${GREEN}[√]${NC} 强制重拉完成,无远端目录"
|
||||||
|
else
|
||||||
|
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
|
||||||
echo -e " ${YELLOW}[→]${NC} scp 新增: ${d}"
|
if [ "${FORCE_REPULL}" = "true" ]; then
|
||||||
scp_dir "${u}" "${ip}" "${port}" "${ssh_opts}" "${rdir}" "${dest}" "$d"
|
echo -e " ${YELLOW}[→]${NC} scp 强制重拉: ${d}"
|
||||||
|
else
|
||||||
|
echo -e " ${YELLOW}[→]${NC} scp 新增: ${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)。
|
||||||
@@ -184,7 +184,11 @@ synspec_input:
|
|||||||
tlusty_stage:
|
tlusty_stage:
|
||||||
enabled: true
|
enabled: true
|
||||||
policy: skip_converged
|
policy: skip_converged
|
||||||
strategies: [cold_run, seed_step]
|
# seed_step_stab:稳定化种子步进(2026-08-18)——同物理族(同 Teff/logg/logHe)
|
||||||
|
# 不同 CNO 邻居种子 + POPZER=1E-10 微布居置零 + DPSILG=3.0 λ 算子欠松弛。
|
||||||
|
# 针对 20kK He 富大气 He I/II 电离前沿布居极限环(seed_step 全部发散的难收敛角落),
|
||||||
|
# 实测代表点收敛且五重物理硬门全过,见 docs/failed81_cno_seed_popzer_dpsilg_2026_08_18.md。
|
||||||
|
strategies: [cold_run, seed_step, seed_step_stab]
|
||||||
|
|
||||||
# ---- 种子步进回退(旧字段,保留兼容;新配置见下方 tlusty_stage 块)----
|
# ---- 种子步进回退(旧字段,保留兼容;新配置见下方 tlusty_stage 块)----
|
||||||
# 冷启动失败时,自动用已收敛的邻居模型作种子,用 LTGRAY=F 热启动重试。
|
# 冷启动失败时,自动用已收敛的邻居模型作种子,用 LTGRAY=F 热启动重试。
|
||||||
|
|||||||
Reference in New Issue
Block a user