# CNO 网格完整计算流程 > 本文档说明完整理论光谱网格的计算流程:每个网格点的计算阶段、每阶段的配置、 > 配置原理、CPU/并行机制、以及如何统计每个阶段的信息(时间、收敛等)。 > 当前实现为 **Rust 分布式 C/S + MQ 架构**(非早期 Python 单机脚本),调度与阶段配置 > 详见 `architecture.md` / `task_engine_decoupling_design.md`。 --- ## 1. 总体架构 ``` workflows/.yaml (网格点 + 阶段独立配置 + 策略链) │ ▼ server GridScheduler (schedule_pending_tasks) │ 原子选点(claim_pending_grid_points) → 生成 TaskSpec ▼ MQ task_queue (SQLite,pending/claimed 状态机) │ Worker 轮询 claim 抢占(pull 模型) ▼ node executor(每任务独立沙盒 task_/) │ 按 tlusty_strategies[0] 执行收敛链 ├── ColdRun : lte → nc → nl → synspec ├── SeedStep : seed_nc → nl → synspec(凭 seed_point_name 下载种子 .7 热启动) ▼ 结果上报(report)→ 白名单归档 result_dir// → 清理沙盒 │ 收敛且干净 → .7 入种子库 seeds(供他点热启动) ``` 策略链回退:当前策略失败 → 服务端弹出链首 → 下一顺位(如 `seed_step`)注入种子重试, 详见 `design.md §2` 与 `task_engine_decoupling_design.md §4.2`。 --- ## 2. 每个网格点的计算阶段 每个网格点(一组 Teff/logg/logHe/logC/logN/logO 参数)经过 **TLUSTY 大气求解 + SYNSPEC 光谱合成**。TLUSTY 阶段按执行策略选用不同收敛链(`common::runner` 的 `default_cold_chain` / `default_seed_chain`)。 **冷启动链**(适用 20-40K 及大部分易收敛点): | 阶段 | 程序 | 做什么 | NITER | 典型耗时 | |------|------|--------|-------|---------| | 1. LTE 灰大气 | tlusty | `lte=T, ltgray=T` 模式,解析求灰色 T(τ) 结构 | 0 | 1-3 秒 | | 2. nc(NLTE 连续谱)| tlusty | `lte=F, ltgray=F` + `ilvlin=0`,收敛电离平衡(无线跃迁)| **10** | 1-5 分钟 | | 3. nl(NLTE 含线)| tlusty | `lte=F, ltgray=F` + `ilvlin=100`,加全部谱线跃迁(要求收敛)| 100 | 5-20 分钟 | | 4. synspec | synspec | 用 nl 大气合成可观测光谱 | — | 3-10 秒 | **阶段间依赖**:1→2→3→4 严格顺序。每阶段用上一阶段的 `.7` 大气作种子(fort.8)。 > 冷启动链在高温/He-poor/富金属区会发散,此时按策略链回退走 **种子步进链** > (seed_nc→nl,跳过 LTE grey 直接热启动)。 ### 为什么是这 4 个阶段(原理) Tlusty 的 NLTE 求解用**迭代线性化**(complete linearization)。线性化的收敛半径 有限——当初猜离真解太远时迭代发散。四个阶段逐步缩小初猜与真解的差距: - **阶段1(LTE 灰大气)**:LTE + 灰色不透明度假设下解析求温度结构。提供物理 合理的起点,不需要种子(从零开始)。 - **阶段2(nc 连续谱)**:切换到 NLTE,但 `ilvlin=0` 不含束缚-束缚线跃迁。 线跃迁是统计平衡方程中最敏感的非线性项;先不加线,只收敛电离平衡(光致电离 +复合),得到稳定的 NLTE 布居数结构。**跳过此步直接 grey→含线 NLTE 必发散。** - **阶段3(nl 含线)**:加入全部线跃迁(ilvlin=100)。从已收敛的 nc 种子起步, 线扰动小,快速收敛(典型 ~15 次迭代)。 - **阶段4(synspec)**:用 nl 阶段收敛的大气模型,计算指定波长范围的合成光谱。 ### 2.1 种子步进链(seed_step)—— 高温区破局 当冷启动链发散时(典型:60000-80000K + He-poor + 富金属),策略链回退切换到 **种子步进链**(`default_seed_chain`),跳过 LTE grey 冷启动,直接用邻居已收敛的 `.7` 热启动: | 阶段 | 做什么 | 关键标志 | NITER | |------|--------|---------|-------| | seed_nc | 从种子热启动 NLTE 连续谱 | `ltgray=F`(读 fort.8) + `ichang=0` | 20 | | nl | 含谱线完整 NLTE(要求收敛)| `ilvlin=100` | 100 | **触发流程**(服务端策略链机制,非节点端自行判断): 1. 初始派发链首策略(如 `cold_run`);失败上报(`failed_stage="tlusty"`)后, 服务端 `trigger_strategy_fallback` 弹出链首,重置点 pending,再派发下一顺位。 2. 若下一顺位为 `seed_step`:服务端在全局种子库按**有向 CNO 距离**找最近邻收敛种子, 注入 `seed_point_name` 后派发;无种子则暂存顺位待种子出现。 3. 节点凭 `seed_point_name` 下载种子 `.7`(`GET /api/seed/`),作 fort.8 热启动跑 `seed_nc → nl`。 **原理**(`tlusty208.f` 源码确认,详见 EXPERIENCE.md): - `LTGREY=T` → `CALL LTEGR` 生成灰大气(**忽略 fort.8**,冷启动); - `LTGREY=F` → `CALL INPMOD` 读 fort.8 作初猜(**热启动**)。 - `ICHANG=0`:种子与目标模型原子完全一致(同为 H/He/CNO 设置),只改 Teff/logg/丰度,不需要重映射布居数。 > **物理极限(如实标注)**: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',内容每阶段由 nst_writer 生成) 第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 文件(非标准参数,每阶段统一结构,NITER 不同) `nst_writer::generate_nst_content` 对**所有阶段(含 lte)统一生成 4+ 行**:前 2 行与旧实现字节级一致(阶段参数 + `IELCOR=-1`),从第 3 行起显式写出 `global.nst` 的 NLTE 高频关键字(值等于 TLUSTY 内置默认,物理等价): ``` ND=50,NLAMBD=3,VTB=2.,ISPODF=1,DDNU=50.,CNU1=6.,[CHMAX=..][,ITEK=..],NITER=<阶段> [ORELAX=..][,IDLTE=..][,IACC=..][,ICHANG=..],IELCOR=-1 NMU=3,ISPLIN=0,CNU2=3.,NFTAIL=21,DFTAIL=0.25 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. ``` - lte: `NITER=0`(灰大气不迭代,只做一次形式解) - nc: `NITER=10`, nl: `NITER=100`, seed_nc: `NITER=20` - **不设 CHMAX**(用默认 0.001,强迫 nc 真正收敛) - **不设 ITEK**(用默认 4) - 尾部统一 `IELCOR=-1`(电子密度修正关闭) - 频率截断 `FRCMAX/MIN/FRLMAX/MIN` 仅当显式配非零值才写出(0=TLUSTY 内置默认,不覆盖) - 参数分行写(每行 ≤64c),避免 tlusty nst 解析器 ~72 字符行宽截断 每个参数的作用与原理: | 参数 | 作用 | 为什么这样设 | |------|------|-------------| | `ND` | 大气深度点数 | 50(sdB 标准配置) | | `NLAMBD` | lambda 迭代频率点数 | 3(频率网格细化,用户验证配方) | | `VTB` | 微湍流速度 km/s | 2.(sdB 典型值) | | `ISPODF` | 频率网格开关 | 1(启用细化频率网格) | | `DDNU` | 频率间隔因子 | 50.(频率网格细化参数) | | `CNU1` | 频率网格起点 | 6.(频率网格细化参数) | | `NITER` | 最大迭代数 | nc=10(实测最优),nl=100,seed_nc=20 | | `IELCOR` | 电子密度修正 | -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 CNO),NITER=50 浪费 2.2× 时间。 ### 3.3 synspec 配置(fort.55 + 谱线表) ``` fort.55 控制卡: 波长窗/展宽/截断(由 workflow YAML 的 `synspec_input:` 块或代码默认配置) 谱线表 fort.19: data 下 gf 谱线数据(含 C/N/O 线) ``` - 代码默认波长窗为 **1400–1410 Å**(`SynspecInput` 默认),实际使用通常配置到 目标波段(如 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 如何做到并行(分布式 Pull 模型) 并行由 **多计算节点 + 每节点多槽位** 构成: - **节点**:每个物理设备跑一个 `node` 进程,`DCTS_MAX_SLOTS` 控制该节点并发槽位数 (管理员可经心跳配额动态下调)。 - **队列**:服务端把 pending 点推入 MQ,Worker 只要 `active_slots < effective_max_slots` 就持续 `claim` 抢占(pull 模型,天然负载均衡)。 - **沙盒隔离**:每任务独立工作目录 `data/work/task_/`,fort.* 文件互不冲突, 这是并行安全的基础。 - 早期 Python 单机 `multiprocessing.Pool` 架构已废弃。 ### 4.3 吞吐量估算(参考) | 模型类型 | 单点耗时 | 参考吞吐(若干节点) | 收敛性 | |---------|---------|-------------|--------| | 20000-40000K(标准 sdB 区)| ~12-25 分钟 | 随节点槽位总数线性扩展 | 冷启动全区间可靠 | | 60000K + He-rich/低金属 | ~10-15 分钟 | 同上 | 冷启动或一步种子步进 | | 80000K + He-rich/低金属 | ~3-5 分钟(种子步进)| 同上 | 冷启动失败→种子步进成功 | | 80000K + He-poor + logCNO=-1 | — | — | **真实物理极限,发散**(如实标注) | --- ## 5. 如何统计每阶段信息 ### 5.1 当前已记录的信息(conv.json) 每个网格点完成后,`result_dir/<模型名>/conv.json`(节点归档)与 `seeds_dir/<模型名>/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 路径 "stages": [ { "label": "lte", "converged": true, "max_relc": 0.0, "note": "NITER=0 grey start", "elapsed_sec": 2.1 }, { "label": "nc", "converged": false, ← nc 不要求收敛,作种子即可(NITER=10) "max_relc": 0.957, "worst_depth": 1, "last_iter": 10, "n_depths": 50, "elapsed_sec": 62.4 }, { "label": "nl", "converged": true, "max_relc": 0.0069, "worst_depth": 1, "last_iter": 17, "n_depths": 50, "elapsed_sec": 7.8 } ] } ``` > **走种子步进链时**:`seed` 指向邻居 `.7`,`stages` 里没有 `lte`,而是 > `seed_nc`(`ltgray=F` 热启动,NITER=20)→ `nl`。 > 注意:`conv.json` 不含 `seed_step_used` / `coldfail_backup` 布尔字段(旧版字段已移除); > 收敛手段归因由服务端 `tasks` 表的 `tlusty_strategies[0]`(cold_run / seed_step)聚合统计 > (Phase 6 起 `task_type` 列已删除,策略链首项即当前执行策略的权威快照)。 每阶段记录:`converged`(是否收敛)、`max_relc`(最大相对变化)、 `worst_depth`(最差深度点)、`last_iter`(迭代次数)、`n_depths`(深度点数)、 `elapsed_sec`(本阶段耗时)。 ### 5.2 每阶段时间记录(已实现) runner 在每个阶段的循环开始/结束处计时,conv.json 里每个 stage 有 `elapsed_sec`,synspec 也有单独的 `synspec_sec`。另归档保留各阶段快照: `.