TLUSTY/cno_grid/PIPELINE.md
2026-07-23 19:06:45 +08:00

462 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# CNO 网格完整计算流程
> 本文档说明完整理论光谱网格的计算流程:每个网格点的计算阶段、每阶段的配置、
> 配置原理、CPU/并行机制、以及如何统计每个阶段的信息(时间、收敛等)。
---
## 1. 总体架构
```
config.yaml (网格点 + 收敛链配置)
run_grid.py ── 生成 6 维笛卡尔积参数点
│ 断点续算(跳过已成功) / 种子复用(最近邻) /
│ 失败隔离 / 冷启动失败→种子步进回退
├── worker 1 ── run_one.py ── 点 A
├── worker 2 ── run_one.py ── 点 B 每个 worker 独立工作目录
├── ... 互不干扰,并行(默认16核)
└── worker N ── run_one.py ── 点 X
冷启动链(lte→nc→nl) + synspec
│ 冷启动发散且有干净邻居种子?
▼ → 移失败结果到 <model>.coldfail/
种子步进链(seed_nc→nl) + synspec 改用 LTGRAY=F 热启动重试
results/<模型名>/
conv.json ← 阶段信息(收敛/迭代/时间/seed_step_used)
*.spec/.cont ← 光谱
*.7 ← 各阶段大气
```
---
## 2. 每个网格点的计算阶段
每个网格点(一组 Teff/logg/logHe/logC/logN/logO 参数)经过 **4 个阶段**
**默认走"冷启动链"**DEFAULT_CHAIN适用 20-40K 及大部分易收敛点):
| 阶段 | 程序 | 做什么 | NITER | 典型耗时 |
|------|------|--------|-------|---------|
| 1. LTE 灰大气 | tlusty | `T T` 模式,解析求灰色 T(τ) 结构 | 0 | 1-3 秒 |
| 2. ncNLTE 连续谱)| tlusty | `F F` + `ilvlin=0`,收敛电离平衡(无线跃迁)| **10** | 1-5 分钟 |
| 3. nlNLTE 含线)| tlusty | `F F` + `ilvlin=100`,加全部谱线跃迁(要求收敛)| 100 | 5-20 分钟 |
| 4. synspec | synspec | 用 nl 大气合成可观测光谱 | — | 3-10 秒 |
**阶段间依赖**1→2→3→4 严格顺序。每阶段用上一阶段的 `.7` 大气作种子fort.8)。
> 冷启动链在高温/He-poor/富金属区会发散,此时 run_grid 自动改走下文 §2.1
> 的**种子步进链**seed_nc→nl跳过 LTE grey 直接热启动)。
### 为什么是这 4 个阶段(原理)
Tlusty 的 NLTE 求解用**迭代线性化**complete linearization。线性化的收敛半径
有限——当初猜离真解太远时迭代发散。四个阶段逐步缩小初猜与真解的差距:
- **阶段1LTE 灰大气)**LTE + 灰色不透明度假设下解析求温度结构。提供物理
合理的起点,不需要种子(从零开始)。
- **阶段2nc 连续谱)**:切换到 NLTE`ilvlin=0` 不含束缚-束缚线跃迁。
线跃迁是统计平衡方程中最敏感的非线性项;先不加线,只收敛电离平衡(光致电离
+复合),得到稳定的 NLTE 布居数结构。**跳过此步直接 grey→含线 NLTE 必发散。**
- **阶段3nl 含线)**加入全部线跃迁ilvlin=100。从已收敛的 nc 种子起步,
线扰动小,快速收敛(典型 ~15 次迭代)。
- **阶段4synspec**:用 nl 阶段收敛的大气模型,计算指定波长范围的合成光谱。
### 2.1 种子步进链seed_step—— 高温区破局NEW
当冷启动链发散时典型60000-80000K + He-poor + 富金属run_grid 自动
切换到**种子步进链**`seed_step.SEED_STEP_CHAIN`),跳过 LTE grey 冷启动,
直接用邻居已收敛的 `.7` 热启动:
| 阶段 | 做什么 | 关键标志 | NITER |
|------|--------|---------|-------|
| seed_nc | 从种子热启动 NLTE 连续谱 | `LTGRAY=F`(读 fort.8) + `ICHANG=0` | 80 |
| nl | 含谱线完整 NLTE要求收敛| `ilvlin=100` | 100 |
**触发流程**`run_grid.py` 的 `_worker`
1. 先跑冷启动链DEFAULT_CHAIN
2.`converged=false``seed_step_fallback=true` 且找到了干净邻居种子:
- 把失败结果整体移到 `results/<model>.coldfail/`(避免污染种子库);
-`SEED_STEP_CHAIN``seed=<邻居>.7`)重算;
- conv.json 里记 `seed_step_used=true`、`coldfail_backup=<路径>`。
**原理**`tlusty208.f` 源码确认,详见 EXPERIENCE.md §5Y
- `LTGREY=T``CALL LTEGR` 生成灰大气(**忽略 fort.8**,冷启动);
- `LTGREY=F``CALL INPMOD` 读 fort.8 作初猜(**热启动**)。
- `ICHANG=0`:种子与目标模型原子完全一致(同为 H/He/CNO 设置),只改
Teff/logg/丰度,不需要重映射布居数。
**实测突破**80K + He-poor + logCNO=-4冷启动必败点种子步进 126s 收敛
(冷启动 970s 发散到 NaN。详见 EXPERIENCE.md §5Y 验证表。
> **物理极限(如实标注)**80000K + He-poor + logCNO=-1 即使种子步进也发散
> CNO 高价离子主导不透明度,金属 ×10 跳跃线性化无法阻尼)。网格如实标
> `converged=false`,不强制成功——这些极端参数组合观测上本就罕见。
>
> **ICRSW 是死代码**:源码中 `SUBROUTINE SWITCH` 定义但从未被 CALL
> 默认 CRSW≡1.0 无阻尼效果。不要依赖 ICRSW 稳定化(见 §7.8)。
---
## 3. 每阶段的配置信息与原理
### 3.1 `.5` 文件(每阶段一份,三阶段相同 NATOMS/ions只改 3 处)
```
第1行: TEFF GRAV (三阶段相同:目标参数)
第2行: LTE LTGRAY 阶段1=T T阶段2/3=F F种子步进链全 F
第3行: nst 文件名 (固定写 'nst',内容每阶段由 write_nst 生成)
第4行: NFREAD =2000 → 展开约 75443 个频率点;不要用 50
第5行: NATOMS =8: H,He,空×3,C,N,O
第6+行: atoms (mode abn modpf) C/N/O 的 mode=2 显式NLTE, abn=10^logX
ions段: iat iz nlevs ilast ilvlin nonstd typion filei
阶段1/2/seed_nc: ilvlin=0; 阶段3/nl: ilvlin=100 ← 关键区别)
```
**为什么 NATOMS/ions 三阶段必须相同**:每阶段的 `.7` 大气记录了每个能级的
布居数。种子与目标的能级结构必须一一对应,否则读取时索引错位 → NaN。
### 3.2 nst 文件(非标准参数,每阶段不同)
**阶段1LTE 灰大气)—— 保持干净,不加稳定化参数**
```
ND=50,VTB=2.,NITER=0
```
- `NITER=0`:灰大气不迭代,只做一次形式解。
**阶段2nc和阶段3nl—— 频率细化(用户验证配方,不设 CHMAX/ITEK**
```
ND=50,NLAMBD=3,VTB=2.,ISPODF=1,DDNU=50.,CNU1=6.,NITER=<阶段>
IELCOR=-1
```
- nc: NITER=10, nl: NITER=100
- **不设 CHMAX**(用默认 0.001,强迫 nc 真正收敛)
- **不设 ITEK**(用默认 4
每个参数的作用与原理:
| 参数 | nc值 | nl值 | 作用 | 为什么这样设 |
|------|------|------|------|-------------|
| `ND` | 50 | 50 | 大气深度点数 | sdB 标准配置 |
| `NLAMBD` | 3 | 3 | lambda 迭代频率点数 | 频率网格细化(用户验证配方) |
| `VTB` | 2. | 2. | 微湍流速度 km/s | sdB 典型值 |
| `ISPODF` | 1 | 1 | 频率网格开关 | 启用细化频率网格 |
| `DDNU` | 50. | 50. | 频率间隔因子 | 频率网格细化参数 |
| `CNU1` | 6. | 6. | 频率网格起点 | 频率网格细化参数 |
| `NITER` | **10** | 100 | 最大迭代数 | nc 给 10 次足够实测最优nl 给 100 次 |
| `IELCOR` | -1 | -1 | 电子密度修正 | 关闭 |
> **关键:不设 CHMAX用默认 0.001)、不设 ITEK用默认 4、不设 IDLTE/ORELAX。**
> 之前版本设了 CHMAX=0.1 导致 nc 没真正收敛,是大部分失败的根本原因。
> 详见 EXPERIENCE.md §4 的错误记录。
>
> **nc 的 NITER=10 是实测最优**GUIDE.md NITER 扫描结论):
> - nc纯连续谱缺少谱线约束外层温度永不真正收敛只在外层漂移
> - NITER=10 vs NITER=50 的最终光谱差异仅 6e-6完全等价
> - nl含谱线会自修正到正确解无论 nc 给什么初值;
> - NITER=10 总耗时 ~12 分钟35000K CNONITER=50 浪费 2.2× 时间。
### 3.3 synspec 配置fort.55.lin + 谱线表)
```
fort.55.lin 第6行: WLMIN WLMAX WLSTEP ... CUTOFF ...
谱线表 fort.19: data/gfVIS99.dat (含 C 1412 / N 2396 / O 1885 条线)
```
- 当前用 3000-7000Å光学波段覆盖 C II 4267、C III 4647 等)。
- 大气来自 nl 阶段的 `.7`(复制为 fort.8)。
---
## 4. CPU 与并行机制
### 4.1 每个网格点只用一个 CPU 核
**是的。** tlusty.exe 和 synspec.exe 是 Fortran 编译的单线程程序,每个实例只用
1 个 CPU 核。网格点的并行不是靠程序内部的多线程,而是靠**同时启动多个程序实例**。
### 4.2 如何做到并行
`run_grid.py` 用 Python 的 `multiprocessing.Pool``run_grid.py` 的 `_worker`
```python
with Pool(nworkers) as pool:
for res in pool.imap_unordered(_worker, worker_args):
...
```
- `nworkers`config.yaml当前=**16**):同时运行的 worker 进程数。
- 每个 worker 是一个独立的 Python 子进程,调用 `run_one.py` 跑一个网格点
(在独立的工作目录里,互不干扰)。
- `imap_unordered`:哪个点先完成就先回收,立即分配下一个点(动态负载均衡)。
- 16 核机器跑 16 个 worker = 16 个 tlusty 实例同时跑 = 满载利用。
(按机器核数调整;每个 tlusty 运行是单线程的,调大 nworkers 即可吃更多核。)
**关键:每个 worker 用独立工作目录**`results/<模型名>/`),避免 fort.* 文件
冲突。这是并行安全的基础。
### 4.3 吞吐量估算
| 模型类型 | 单点耗时 | 16核并行吞吐 | 收敛性 |
|---------|---------|-------------|--------|
| 20000-40000K标准 sdB 区)| ~12-25 分钟 | ~48-80 点/小时 | 冷启动全区间可靠 |
| 60000K + He-rich/低金属 | ~10-15 分钟 | ~64-96 点/小时 | 冷启动或一步种子步进 |
| 80000K + He-rich/低金属 | ~3-5 分钟(种子步进)| 约同上 | 冷启动失败→种子步进成功 |
| 80000K + He-poor + logCNO=-1 | — | — | **真实物理极限,发散**(如实标注) |
当前 config.yaml 共 432 点4×2×2×3×3*3中等参数区冷启动为主
高温区走种子步进回退,整体约需数小时到一天。
---
## 5. 如何统计每阶段信息
### 5.1 当前已记录的信息conv.json
每个网格点完成后,`results/<模型名>/conv.json` 记录:
```json
{
"name": "t40000_g6.0_he0_c-1_n-1_o-1",
"params": {"teff":40000, "logg":6.0, "loghe":0, "logc":-1, "logn":-1, "logo":-1},
"converged": true,
"final_max_relc": 0.0069,
"atmosphere_has_nan": false,
"synspec_rc": 0,
"elapsed_sec": 715.0, 总耗时(所有阶段+synspec之和
"seed": null, 冷启动为 null走种子步进时为邻居 .7 路径
"seed_step_used": false, true=种子步进链跑成功的(含 coldfail_backup 路径)
"stages": [
{
"label": "lte",
"converged": true,
"final": {"itek":null, "rc":0, "max_relc":0.0,
"note":"NITER=0 grey start (no iterations)"}
},
{
"label": "nc",
"converged": false, nc 不要求收敛作种子即可NITER=10
"final": {"itek":null, "rc":0, "max_relc":0.957,
"worst_depth":1, "last_iter":10, "n_depths":50}
},
{
"label": "nl",
"converged": true,
"final": {"itek":null, "rc":0, "max_relc":0.0069,
"worst_depth":1, "last_iter":17, "n_depths":50}
}
]
}
```
> **走种子步进链时**`seed` 指向邻居 `.7``seed_step_used=true`
> `coldfail_backup` 指向 `<model>.coldfail/``stages` 里没有 `lte`,而是
> `seed_nc`LTGRAY=F 热启动NITER=80→ `nl`。
每阶段记录:`converged`(是否收敛)、`max_relc`(最大相对变化)、
`worst_depth`(最差深度点)、`last_iter`(迭代次数)、`n_depths`(深度点数)、
`elapsed_sec`(本阶段耗时)。
### 5.2 每阶段时间记录(已实现)
`run_one.py` 现在在每个阶段的循环开始/结束处计时conv.json 里每个 stage 有
`elapsed_sec`synspec 也有单独的 `synspec_sec`
```json
"stages": [
{"label":"lte", "elapsed_sec": 2.1, "converged":true, ...},
{"label":"nc", "elapsed_sec": 62.4, "converged":false, ...},
{"label":"nl", "elapsed_sec": 7.8, "converged":true, ...}
],
"synspec_sec": 3.1,
"elapsed_sec": 75.4
```
统计所有模型的阶段时间分布:
```bash
python3 -c "
import json,glob
for f in sorted(glob.glob('results/*/conv.json')):
j=json.load(open(f))
times = {s['label']:s.get('elapsed_sec',0) for s in j['stages']}
print('%-30s lte=%5.0fs nc=%5.0fs nl=%5.0fs syn=%4.0fs total=%5.0fs' % (
j['name'], times.get('lte',0), times.get('nc',0), times.get('nl',0),
j.get('synspec_sec',0), j['elapsed_sec']))
"
```
### 5.3 统计整个网格的信息
`run_grid.py` 完成后写 `results/grid_status.json`
```json
{
"total": 432,
"elapsed_sec": 36000,
"counts": {"converged": 400, "unfinished": 18, "error": 3, "skipped": 11},
"seed_step_retries": 47,
"models": [
{"name":"t20000_...", "status":"converged", "max_relc":0.0065,
"seed_step_used": false},
{"name":"t80000_...", "status":"converged", "max_relc":0.00091,
"seed_step_used": true},
...
]
}
```
汇总统计命令:
```bash
# 成功率 + 种子步进命中数
python3 -c "import json; j=json.load(open('results/grid_status.json')); print(j['counts'], 'seed_step_retries=', j['seed_step_retries'])"
# 所有收敛模型的 max_relc 分布(标注是否走了种子步进)
python3 -c "
import json,glob
for f in sorted(glob.glob('results/*/conv.json')):
j=json.load(open(f))
if j['converged']:
tag='SEED' if j.get('seed_step_used') else 'cold'
print(j['name'], tag, j['final_max_relc'], str(j['elapsed_sec'])+'s')
"
# 失败/未收敛的模型
python3 -c "
import json,glob
for f in sorted(glob.glob('results/*/conv.json')):
j=json.load(open(f))
if not j['converged']:
print(j['name'], 'FAILED', j.get('note',''))
"
```
### 5.4 单个网格点的详细收敛诊断
```bash
# 看某阶段的迭代收敛趋势fort.9
python3 src/check_conv.py results/<模型>/<模型>.nl.9 --chmax 0.01
# 画光谱(标出 CNO 诊断线位置)
python3 src/plot_spec.py results/<模型>
```
---
## 6. 完整操作步骤
### 第1步配置网格密度config.yaml 的 grid 段)
```yaml
grid:
teff: [20000, 30000, 40000, 60000] # 各维采样点列表
logg: [5.0, 6.0]
loghe: [-2, 0]
logc: [-4, -2, -1] # 亚太阳范围(已修正;旧版 -1..1 会发散)
logn: [-4, -2, -1]
logo: [-4, -2, -1]
# 共 4*2*2*3*3*3 = 432 个点
```
### 第2步设置环境变量
```bash
export TLUSTY=/home/dckj/program/tlusty/tl208-s54
```
### 第3步预览dry-run
```bash
cd $TLUSTY/cno_grid
python3 src/run_grid.py config.yaml --dry-run
# 输出grid: 432 points total, N already done, M to compute
```
### 第4步启动批量计算后台并行
```bash
nohup python3 src/run_grid.py config.yaml > results/grid_run.log 2>&1 &
# 冷启动失败的点会自动尝试种子步进回退seed_step_fallback: true
# 想关闭回退:在 config.yaml 设 seed_step_fallback: false。
```
### 第5步监控
```bash
tail -f results/grid_run.log # 实时进度
grep seed_step results/grid_run.log # 看哪些点走了种子步进
cat results/grid_status.json # 汇总(完成后才有)
```
### 第6步断点续算中断后恢复自动跳过已成功的
```bash
python3 src/run_grid.py config.yaml # 重跑同一命令即可
```
### 第7步检查结果 + 画图
```bash
# 成功率 + 种子步进命中数
python3 -c "import json;j=json.load(open('results/grid_status.json'));print(j['counts'],'seed_step=',j['seed_step_retries'])"
# 画某个模型光谱
python3 src/plot_spec.py results/<模型名>
```
### 单点调试(不走批量)
```bash
# 冷启动单点(适用 20-40K 大部分点)
python3 src/run_one.py --teff 40000 --logg 6.0 --loghe 0 --logc -1 --logn -1 --logo -1
# 种子步进单点(高温 He-poor 等冷启动失败点)
python3 src/seed_step.py --teff 80000 --logg 6.5 --loghe -4 \
--logc -4 --logn -4 --logo -4 \
--seed results/<seed-model>/<seed-model>.7
```
### 第8步加密网格Phase 2
在 config.yaml 的各维列表里加更多点,重跑 `run_grid.py`(自动跳过已完成的,
只算新点)。高温区加密时建议先冷启动算 He-rich/低金属的"桥头堡"模型,
建立种子库再扩散到难收敛点(见 §7.1)。
---
## 7. 注意事项
1. **种子步进回退(核心机制)**`seed_step_fallback: true`(默认开)时,
冷启动失败的点会自动改走种子步进链(`seed_step.SEED_STEP_CHAIN`
把失败结果备份到 `<model>.coldfail/`,用 `find_seed` 找最近邻已收敛的 `.7`
作种子,`LTGRAY=F + ICHANG=0` 热启动重跑。这是高温/He-poor/富金属区的
决定性破局手段(详见 §2.1)。
**种子跨度限制**种子与目标的参数差不能太大logg ≤0.5/步,
Teff ≤5000K/步logCNO 一步 ≤100×。跨度过大如 cno-4 直接跳 cno-1
1000× 金属跳跃)即便种子步进也发散 → 这是真实物理极限,网格如实标注。
**种子库建立策略**:高温区建议先冷启动算 He-rich 或低金属的"桥头堡"
模型,建立种子库再扩散到难收敛点(如 cno-4 → cno-2 → cno-1 多步跳板)。
2. **断点续算判定**`conv.json` 里 `converged=true` 的点会被跳过。假收敛
atmosphere_has_nan=true的点会被重算。
3. **失败隔离**:单点失败(发散/崩溃)不中断整个网格,记入 grid_status.json
的 error/unfinished 列表。高温难点的失败大多是真实物理极限(见 §2.1
不强制成功;如确需重试,可用 `seed_step.py` 手动从更近的种子起跳。
4. **磁盘空间**:每个模型约 50-100MB含中间文件走种子步进的点还会留
`<model>.coldfail/` 备份。432 点约需 20-40GB。如不够可定期清理中间文件
(保留 .spec/.cont/.7/conv.json
5. **并行安全**:每个 worker 用独立工作目录results/<模型名>/fort.* 文件
不冲突。可安全并行。
6. **nst 文件行长限制(已修复)**tlusty 的 nst 解析器有 ~72 字符的行宽限制。
如果参数太多写在一行(如加了 IDLTE/IACC 后 >72 字符),行尾的参数会被
静默截断。`write_nst()` 现在把参数分两行写line1≤64c, line2 余下参数)。
这是个隐蔽 bug——截断后 tlusty 不报错而是用默认值,导致"看似成功实则参数
没生效"。
7. **fort.84 残留(已修复)**tlusty 运行时会在工作目录写 fort.84nst 参数的
内部表示)。如果下一次 tlusty 运行(不同 NATOMS读到旧的 fort.84,会报
"Bad integer for item 48" 崩溃。`run_tlusty()` 现在每次运行前删除 fort.84。
8. **收敛可靠性(如实)**:用正确配方(无 CHMAX/ITEK, NFREAD=2000, nc NITER=10
20000-40000K 全区间冷启动可靠收敛60000-80000K + He-rich/低金属用
种子步进可解决;**80000K + He-poor + logCNO=-1 是真实物理极限**
CNO 高价离子主导不透明度,金属 ×10 跳跃线性化无法阻尼),网格如实标记
未收敛。之前版本的"8/8 边界全部成功"不准确(基于错误的 CHMAX=0.1)。
`ICRSW`Hummer & Voels 切换)在 tlusty208 **原版中是死代码**
SUBROUTINE SWITCH 从未被 CALLCRSW≡1.0),不要依赖它稳定化;
即便源码修复启用后实测对难点也无帮助。详见 EXPERIENCE.md §5Y。