Files
DCTS/docs/PIPELINE.md
T
fmq 43b82b1ae2 feat(all): 物理正确性五重硬门槛、输入文件结构化与 fort.55 错位修复、conv 诊断 DB 化与阶段归因修复、ORELAX 收敛修复与导入工具下线
物理正确性校验体系(common/conv_check.rs +494 行)
- 新增 5 类硬门槛:能量守恒(.6)、温度结构(.7)、emflux 积分校验(.emflux,含全 NaN 判失败)、假收敛排查(itek 轨迹首末比)、b 因子合理性(.bfac)
- runner 在 TLUSTY 阶段结束后执行全部校验,任一失败判 final_converged=false
- GridConfig 新增 8 个可配阈值,经 scheduler→executor→runner 全链路透传

输入文件配置结构化重构(config.rs +1453 行)
- TlustyInput 拆为 dot5/nst 分层结构,字段名严格映射 tlusty208.f READ 语句;SynspecInput 重构为 9 个 Fort55Line 子结构体
- 移除 ChainStep.metals 字段,元素集改由 dot5.atoms/ions 显式声明(gen_input5/nst_writer 同步重写为三源融合 / 分层覆盖)
- fort.55 修复行结构 bug:补全分子表行(7→9 行),IDSTD 50→0 错位修正(影响全部光谱线强归一化,需重算 SYNSPEC 阶段)

conv 诊断 DB 化与阶段归因修复(server)
- 单点详情 conv 面板从磁盘 conv.json 改读 DB grid_points.summary_json;grid_points 新增 summary_json/last_elapsed_sec 两列(旧库幂等 ALTER)
- record_task_report 阶段归因列加 CASE 守卫 + clear_synspec 对称处理,修复 synspec-only/TLUSTY-only 重跑污染统计
- 新增 summary_merge.rs 点级增量合并,避免重跑覆盖诊断字段

收敛性 ORELAX 修复与 seed_chain 可配(sdB_cno.yaml + node)
- nl 阶段加 orelax=0.5、seed_nc 加 orelax=0.3,阻尼中温区 relc 振荡发散
- seed_chain 块可配,executor 优先采用用户配置而非内置默认链

导入工具下线
- 删除 import_results 客户端工具及 Windows 推送脚本;移除 /admin/import_seed 端点
- 改为服务端临时 migrate_conv 端点(扫 conv.json 增量合并入库,迁移后可删)

文档与分析
- 新增 1305 失败点根因分析、fort.14 全 NaN 物理含义分析两份深度文档
- spectrum_correctness_analysis 两次修订标注已修复项;fetch_results.sh 修 trap RETURN 的 set -u 报错
2026-08-09 12:09:48 +08:00

20 KiB
Raw Blame History

CNO 网格完整计算流程

本文档说明完整理论光谱网格的计算流程:每个网格点的计算阶段、每阶段的配置、 配置原理、CPU/并行机制、以及如何统计每个阶段的信息(时间、收敛等)。 当前实现为 Rust 分布式 C/S + MQ 架构(非早期 Python 单机脚本),调度与阶段配置 详见 architecture.md / task_engine_decoupling_design.md


1. 总体架构

workflows/<wf>.yaml (网格点 + 阶段独立配置 + 策略链)
       │
       ▼
server GridScheduler (schedule_pending_tasks)
       │  原子选点(claim_pending_grid_points) → 生成 TaskSpec
       ▼
MQ task_queue (SQLitepending/claimed 状态机)
       │  Worker 轮询 claim 抢占(pull 模型)
       ▼
node executor(每任务独立沙盒 task_<id>/
       │  按 tlusty_strategies[0] 执行收敛链
       ├── ColdRun  : lte → nc → nl → synspec
       ├── SeedStep : seed_nc → nl → synspec(凭 seed_point_name 下载种子 .7 热启动)
       ▼
结果上报(report)→ 白名单归档 result_dir/<name>/ → 清理沙盒
       │  收敛且干净 → .7 入种子库 seeds(供他点热启动)

策略链回退:当前策略失败 → 服务端弹出链首 → 下一顺位(如 seed_step)注入种子重试, 详见 design.md §2task_engine_decoupling_design.md §4.2


2. 每个网格点的计算阶段

每个网格点(一组 Teff/logg/logHe/logC/logN/logO 参数)经过 TLUSTY 大气求解 + SYNSPEC 光谱合成。TLUSTY 阶段按执行策略选用不同收敛链(common::runnerdefault_cold_chain / default_seed_chain)。

冷启动链(适用 20-40K 及大部分易收敛点):

阶段 程序 做什么 NITER 典型耗时
1. LTE 灰大气 tlusty lte=T, ltgray=T 模式,解析求灰色 T(τ) 结构 0 1-3 秒
2. ncNLTE 连续谱) tlusty lte=F, ltgray=F + ilvlin=0,收敛电离平衡(无线跃迁) 10 1-5 分钟
3. nlNLTE 含线) 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)。线性化的收敛半径 有限——当初猜离真解太远时迭代发散。四个阶段逐步缩小初猜与真解的差距:

  • 阶段1LTE 灰大气):LTE + 灰色不透明度假设下解析求温度结构。提供物理 合理的起点,不需要种子(从零开始)。
  • 阶段2nc 连续谱):切换到 NLTE,但 ilvlin=0 不含束缚-束缚线跃迁。 线跃迁是统计平衡方程中最敏感的非线性项;先不加线,只收敛电离平衡(光致电离 +复合),得到稳定的 NLTE 布居数结构。跳过此步直接 grey→含线 NLTE 必发散。
  • 阶段3nl 含线):加入全部线跃迁(ilvlin=100)。从已收敛的 nc 种子起步, 线扰动小,快速收敛(典型 ~15 次迭代)。
  • 阶段4synspec:用 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 下载种子 .7GET /api/seed/<name>),作 fort.8 热启动跑 seed_nc → nl

原理tlusty208.f 源码确认,详见 EXPERIENCE.md):

  • LTGREY=TCALL LTEGR 生成灰大气(忽略 fort.8,冷启动);
  • LTGREY=FCALL 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 大气深度点数 50sdB 标准配置)
NLAMBD lambda 迭代频率点数 3(频率网格细化,用户验证配方)
VTB 微湍流速度 km/s 2.sdB 典型值)
ISPODF 频率网格开关 1(启用细化频率网格)
DDNU 频率间隔因子 50.(频率网格细化参数)
CNU1 频率网格起点 6.(频率网格细化参数)
NITER 最大迭代数 nc=10(实测最优),nl=100seed_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 线)
  • 代码默认波长窗为 14001410 Å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 点推入 MQWorker 只要 active_slots < effective_max_slots 就持续 claim 抢占(pull 模型,天然负载均衡)。
  • 沙盒隔离:每任务独立工作目录 data/work/task_<id>/,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(服务端)记录:

{
  "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 指向邻居 .7stages 里没有 lte,而是 seed_ncltgray=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_secsynspec 也有单独的 synspec_sec。另归档保留各阶段快照: <name>.<label>.5/.6/.err/.nst/.7 与收敛诊断 <name>.<label>_chmax*.9(见 tlusty_result_artifacts.md)。

5.3 统计整个网格的信息

早期 grid_status.json 文件已废弃;当前状态统计由服务端 API 实时查询 DB 提供:

  • GET /api/workflows:各工作流内联进度(total/completed/failed/running…,completed 由 7c 改名自 converged
  • GET /api/workflows/:name/stats:单工作流统计(含 cold_run_converged / seed_step_converged
  • GET /api/workflows/:name/points:逐点列表(可过滤 status/method/wave/q,分页)
  • GET /api/workflows/:name/progress:进度-时间序列曲线
  • 种子步进命中数 = tasksjson_extract(tlusty_strategies,'$[0]')='seed_step'status='completed' 的聚合

前端 Dashboard 直接消费以上端点渲染(首页卡片、详情页概览/点表/平行集合分析图)。

5.4 单个网格点的详细收敛诊断

  • 从节点 result_dir/<模型>/ 读取 <模型>.<label>_chmax*.9(每阶段收敛诊断, 含最大相对变化随迭代下降过程)。
  • 阶段日志 <模型>.<label>.6/.err 查看 tlusty 输出与报错。
  • 光谱 *.spec / 连续谱 *.cont / b 因子 *.bfac / 出射谱 *.emflux 供物理分析。

6. 完整操作步骤

第1步:配置工作流(workflows/.yaml 的 grid 段)

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 个点
tlusty_stage:                            # TLUSTY 阶段独立配置(enabled/policy/strategies
  enabled: true
  policy: skip_converged
  strategies: ["cold_run", "seed_step"]  # 策略链:冷启动失败回退种子步进
synspec_stage:                           # SYNSPEC 阶段独立配置(enabled/policy/strategies
  enabled: true
  policy: skip_converged
  strategies: ["standard"]
synspec_input:                           # SYNSPEC fort.55 控制卡(9 行子结构体,按行配置)
  line1: {imode: 0, idstd: 0, iprin: 1}  # 行1: IMODE/IDSTD(=0 自动 2ND/3)/IPRIN
  line6:                                 # 行6: 波长范围等(其余行走 SynspecInput::default
    alam0: 3000.0                        # 光谱波长起始点 (Å)
    alast: 7000.0                        # 光谱波长终止点 (Å)

第2步:创建并启动工作流(HTTP API / Dashboard

# 创建/保存工作流(config_yaml 上传)
curl -X POST localhost:8090/api/workflows -H "Authorization: Bearer $ADMIN_TOKEN" \
     -F name=sdB_cno -F config_yaml=@workflows/sdB_cno.yaml

# 启动(进入 initializing → running,后台异步建网格并派发)
curl -X POST localhost:8090/api/workflows/sdB_cno/start \
     -H "Authorization: Bearer $ADMIN_TOKEN"

也可直接在 Dashboard「工作流 → 启动配置 → 启动」操作。启动后调度器按 wave(难度) 顺序把点推入 MQ,各节点 pull 抢占计算。

第3步:监控

  • Dashboard:工作流卡片进度条、详情页概览(统计/进度曲线/ETA)、逐点表、平行集合分析图。
  • APIGET /api/workflows/:name/stats / :name/points / :name/progress
  • 服务端日志:RUST_LOG=info ./server

第4步:断点续算 / 增量

  • 工作流默认 policy: skip_converged:启动时跳过已收敛点、重试已失败点。
  • 加密网格:往 grid 各维列表加更多点后保存并重启工作流(自动跳过已完成,只算新点)。
  • 高温区加密建议先冷启动算 He-rich/低金属的"桥头堡"模型,建立种子库再扩散到 难收敛点(见 §7.1)。

单点调试

# 把单个点置回 pending 重跑(或在 Dashboard 详情页查看该点尝试历史/阶段诊断)
curl -X POST localhost:8090/api/workflows/sdB_cno/points \
     -H "Authorization: Bearer $ADMIN_TOKEN" -F point_name=t40000_... 

早期 Python 的 run_grid.py / run_one.py / seed_step.py 脚本与 src/ 目录已废弃。


7. 注意事项

  1. 种子步进回退(核心机制)tlusty_strategies 链含 seed_step 时(默认推荐 ["cold_run", "seed_step"]),冷启动失败的点会自动回退到种子步进链:服务端在全局 种子库找近邻已收敛 .7ltgray=F + ichang=0 热启动重跑。这是高温/He-poor/富金属区的 决定性破局手段(详见 §2.1)。 种子匹配:两级匹配——exact_family(同 Teff/logg/logHe 族,CNO 用有向距离:富方向 重罚 4×、贫方向 1×)与 global 加权距离(d_teff/5000 + d_logg×2 + d_loghe×0.5 + d_cno×0.1 ≤ 3.0),不再是早期简单的绝对跨度阈值(见 seed_finder.rs / design.md §3)。 跨度过大(如 cno-4 直接跳 cno-1,1000× 金属跳跃)即便种子步进也发散 → 这是真实物理 极限,网格如实标注。 种子库建立策略:高温区建议先冷启动算 He-rich 或低金属的"桥头堡"模型,建立种子库 再扩散到难收敛点(如 cno-4 → cno-2 → cno-1 多步跳板)。

  2. 断点续算判定conv.jsonconverged=true 的点会被跳过。假收敛 atmosphere_has_nan=true)的点会被重算。

  3. 失败处理:单点失败(发散/崩溃)不中断整个网格;当前策略失败触发策略链回退, 链耗尽则标记 failed。高温难点的失败大多是真实物理极限(见 §2.1),不强制成功。

  4. 磁盘空间:节点端归档 result_dir 永久保留(无 LRU 淘汰),每个模型约 2-10MB 白名单产物(.spec/.cont/.iden/.7/.bfac/.emflux/各阶段快照/日志)。沙盒 task_* 工作目录结算后清理,节点重启时清理残留。

  5. 并行安全:每任务独立沙盒(data/work/task_<id>/),fort.* 文件不冲突。可安全并行。

  6. nst 文件行长限制(已修复)tlusty 的 nst 解析器有 ~72 字符的行宽限制。 如果参数太多写在一行(如加了 IDLTE/IACC 后 >72 字符),行尾的参数会被 静默截断。nst_writer 把参数分多行写(每行 ≤64c),避免触及该行宽限制。 这是个隐蔽 bug——截断后 tlusty 不报错而是用默认值,导致"看似成功实则参数 没生效"。

  7. fort.84 残留(已修复):tlusty 运行时会在工作目录写 fort.84(nst 参数的 内部表示)。如果下一次 tlusty 运行(不同 NATOMS)读到旧的 fort.84,会报 "Bad integer for item 48" 崩溃。runner 每次运行前删除 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)。 ICRSWHummer & Voels 切换)在 tlusty208 原版中是死代码 SUBROUTINE SWITCH 从未被 CALLCRSW≡1.0),不要依赖它稳定化; 即便源码修复启用后实测对难点也无帮助。详见 EXPERIENCE.md。