feat(all): 任务引擎双阶段解耦、僵尸涡旋修复、动态 CPU 配额与前端详情页重构

将 TLUSTY/SYNSPEC 拆为各自独立的 enabled/policy/strategies 阶段,
以策略链自动弹栈取代单级 seed_step 布尔回退;定向修复 2026-08-02
僵尸任务涡旋事故;新增节点并发配额热调;前端详情页从 1412 行巨型
视图拆为薄控制器 + detail 子模块,并补齐工具层与单测。

引擎与调度(task_engine_decoupling_design.md)
- models.rs: 新增 StagePolicy / EngineStageConfig / TaskSpec 阶段字段、
  normalize_compat() 校正旧版在途消息策略链、failed_stage 归因
- scheduler.rs: resolve_dispatchable_chain 派发门控、
  trigger_strategy_fallback 按 failed_stage 精确弹栈;启动期
  force_recompute/skip_converged(默认)/skip_failed 三策略
- db.rs: tasks 表 +7 列持久化阶段配置;终态守卫
  (mark_grid_point_running 仅 pending/queued→running;
  record_task_report 拒绝迟到失败翻黑 converged);策略弹栈快照

僵尸涡旋修复(runbook-20260802-zombie-vortex-fix.md)
- 全链路跨库活性交叉校验:派发/claim/孤儿回收/回退统一查 MQ 队列活性,
  活则放行、死则清僵尸,结构性消除"每点重复派发"
- stop/重启卫生:清队列同步 delete_tasks_by_ids,杜绝遗留 pending 行
- report_task: 幂等吸收 + 409 区分迟到冗余结果,仅 state_changed 时回退
- MQ: NULL workflow_name 回填 __legacy__、requeue 后迟到上报被 403 竞态修复

动态 CPU 配额(dynamic_cpu_slots_design.md)
- admin.rs: POST /admin/nodes/:id/quota(Option<Option<i32>> 区分
  缺字段/显式 null);nodes 表 +admin_max_slots
- worker.rs: effective_max_slots = min(admin, physical),心跳下发原子生效

科学产物保全(tlusty_result_artifacts.md)
- runner.rs: SYNSPEC 启动前快照 fort.12/fort.14 → .bfac/.emflux 防覆盖
- 半失败点(大气收敛+光谱失败)改判 Failed 并写入 note;仅 SYNSPEC
  场景不再恒判失败;撤销归档 LRU 200 上限改为永久保留
- executor.rs: 透传 synspec_params 数值参数(此前固定 None)

前端(dashboard/)
- workflowDetail.js 1412→328 行,拆出 views/detail/{ctx,overview,
  pointsTable,parSets,pointPanel}.js,AbortController 治理监听/请求生命周期
- 删除 wfActions.js,新增 wfEnginePanel.js(双阶段三维配置编辑面板)
- 新增 utils/{errors,format,icons,polling,yamlStage}.js 纯函数模块
- 路由级动态 import 代码分割;节点配额三点菜单 + Modal 管理
- 首次引入 node:test 单测(format/polling/yamlStage/psCache,644 行)
- 系统性补齐 a11y:skip-link、ARIA、Tab 键盘漫游、toast 关闭、退出动画

文档与工具
- 新增 6 篇设计/调研:引擎解耦、动态配额、涡旋 runbook、
  光谱正确性分析、收敛判断、产物归档
- PIPELINE/design/api/database 等协同重写为分布式 C/S 架构口径
- scripts/fetch_results.sh 跨节点产物备份;import_results 按 cno 升序导入
- workflows/sdB_cno.yaml: 新增 tlusty/synspec_stage 配置块,修正 wstart 笔误
This commit is contained in:
fmq
2026-08-04 23:40:52 +08:00
parent c8fd24b120
commit cd370d88e7
74 changed files with 13386 additions and 3033 deletions
+167 -247
View File
@@ -2,56 +2,56 @@
> 本文档说明完整理论光谱网格的计算流程:每个网格点的计算阶段、每阶段的配置、
> 配置原理、CPU/并行机制、以及如何统计每个阶段的信息(时间、收敛等)。
> 当前实现为 **Rust 分布式 C/S + MQ 架构**(非早期 Python 单机脚本),调度与阶段配置
> 详见 `architecture.md` / `task_engine_decoupling_design.md`。
---
## 1. 总体架构
```
config.yaml (网格点 + 收敛链配置)
workflows/<wf>.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 ← 各阶段大气
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 §2``task_engine_decoupling_design.md §4.2`
---
## 2. 每个网格点的计算阶段
每个网格点(一组 Teff/logg/logHe/logC/logN/logO 参数)经过 **4 个阶段**
每个网格点(一组 Teff/logg/logHe/logC/logN/logO 参数)经过 **TLUSTY 大气求解 + SYNSPEC
光谱合成**。TLUSTY 阶段按执行策略选用不同收敛链(`common::runner``default_cold_chain` /
`default_seed_chain`)。
**默认走"冷启动链"**DEFAULT_CHAIN适用 20-40K 及大部分易收敛点):
**冷启动链**(适用 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 分钟 |
| 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/富金属区会发散,此时 run_grid 自动改走下文 §2.1
> 的**种子步进链**seed_nc→nl,跳过 LTE grey 直接热启动)。
> 冷启动链在高温/He-poor/富金属区会发散,此时按策略链回退走 **种子步进链**
> seed_nc→nl,跳过 LTE grey 直接热启动)。
### 为什么是这 4 个阶段(原理)
@@ -67,33 +67,31 @@ Tlusty 的 NLTE 求解用**迭代线性化**complete linearization)。线
线扰动小,快速收敛(典型 ~15 次迭代)。
- **阶段4synspec)**:用 nl 阶段收敛的大气模型,计算指定波长范围的合成光谱。
### 2.1 种子步进链(seed_step)—— 高温区破局NEW
### 2.1 种子步进链(seed_step)—— 高温区破局
当冷启动链发散时(典型:60000-80000K + He-poor + 富金属),run_grid 自动
切换到**种子步进链**`seed_step.SEED_STEP_CHAIN`),跳过 LTE grey 冷启动,
直接用邻居已收敛的 `.7` 热启动:
当冷启动链发散时(典型:60000-80000K + He-poor + 富金属),策略链回退切换到
**种子步进链**`default_seed_chain`),跳过 LTE grey 冷启动,直接用邻居已收敛的
`.7` 热启动:
| 阶段 | 做什么 | 关键标志 | NITER |
|------|--------|---------|-------|
| seed_nc | 从种子热启动 NLTE 连续谱 | `LTGRAY=F`(读 fort.8) + `ICHANG=0` | 80 |
| seed_nc | 从种子热启动 NLTE 连续谱 | `ltgray=F`(读 fort.8) + `ichang=0` | 20 |
| 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=<路径>`
**触发流程**服务端策略链机制,非节点端自行判断):
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/<name>`),作 fort.8 热启动跑
`seed_nc → nl`
**原理**`tlusty208.f` 源码确认,详见 EXPERIENCE.md §5Y):
**原理**`tlusty208.f` 源码确认,详见 EXPERIENCE.md):
- `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`,不强制成功——这些极端参数组合观测上本就罕见。
@@ -110,7 +108,7 @@ Tlusty 的 NLTE 求解用**迭代线性化**complete linearization)。线
```
第1行: TEFF GRAV (三阶段相同:目标参数)
第2行: LTE LTGRAY (阶段1=T T,阶段2/3=F F;种子步进链全 F)
第3行: nst 文件名 (固定写 'nst',内容每阶段由 write_nst 生成)
第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
@@ -121,35 +119,32 @@ ions段: iat iz nlevs ilast ilvlin nonstd typion filei
**为什么 NATOMS/ions 三阶段必须相同**:每阶段的 `.7` 大气记录了每个能级的
布居数。种子与目标的能级结构必须一一对应,否则读取时索引错位 → NaN。
### 3.2 nst 文件(非标准参数,每阶段不同)
### 3.2 nst 文件(非标准参数,每阶段统一结构,NITER 不同)
**阶段1(LTE 灰大气)—— 保持干净,不加稳定化参数**
`nst_writer::generate_nst_content` 对**所有阶段(含 lte)统一生成两行**
```
ND=50,VTB=2.,NITER=0
ND=50,NLAMBD=3,VTB=2.,ISPODF=1,DDNU=50.,CNU1=6.,[CHMAX=..][,ITEK=..],NITER=<阶段>
[ORELAX=..][,IDLTE=..][,IACC=..][,ICHANG=..],IELCOR=-1
```
- `NITER=0`灰大气不迭代,只做一次形式解
**阶段2(nc)和阶段3(nl)—— 频率细化(用户验证配方,不设 CHMAX/ITEK)**
```
ND=50,NLAMBD=3,VTB=2.,ISPODF=1,DDNU=50.,CNU1=6.,NITER=<阶段>
IELCOR=-1
```
- nc: NITER=10, nl: NITER=100
- lte: `NITER=0`灰大气不迭代,只做一次形式解
- nc: `NITER=10`, nl: `NITER=100`, seed_nc: `NITER=20`
- **不设 CHMAX**(用默认 0.001,强迫 nc 真正收敛)
- **不设 ITEK**(用默认 4
- 尾部统一 `IELCOR=-1`(电子密度修正关闭)
- 参数分行写(line1 ≤64c, line2 余下),避免 tlusty nst 解析器 ~72 字符行宽截断
每个参数的作用与原理:
| 参数 | 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 | 电子密度修正 | 关闭 |
| 参数 | 作用 | 为什么这样设 |
|------|------|-------------|
| `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 没真正收敛,是大部分失败的根本原因。
@@ -161,13 +156,14 @@ IELCOR=-1
> - nl(含谱线)会自修正到正确解,无论 nc 给什么初值;
> - NITER=10 总耗时 ~12 分钟(35000K CNO),NITER=50 浪费 2.2× 时间。
### 3.3 synspec 配置(fort.55.lin + 谱线表)
### 3.3 synspec 配置(fort.55 + 谱线表)
```
fort.55.lin 第6行: WLMIN WLMAX WLSTEP ... CUTOFF ...
谱线表 fort.19: data/gfVIS99.dat (含 C 1412 / N 2396 / O 1885 条线)
fort.55 控制卡: 波长窗/展宽/截断(由 workflow YAML 的 synspec: 块或代码默认配置)
谱线表 fort.19: data 下 gf 谱线数据(含 C/N/O 线)
```
- 当前用 3000-7000Å(光学波段,覆盖 C II 4267、C III 4647 等)。
- 代码默认波长窗为 **14001410 Å**`SynspecConfig` 默认),实际使用通常配置到
目标波段(如 3000-7000Å 光学波段,覆盖 C II 4267、C III 4647 等)。
- 大气来自 nl 阶段的 `.7`(复制为 fort.8)。
---
@@ -177,47 +173,38 @@ fort.55.lin 第6行: WLMIN WLMAX WLSTEP ... CUTOFF ...
### 4.1 每个网格点只用一个 CPU 核
**是的。** tlusty.exe 和 synspec.exe 是 Fortran 编译的单线程程序,每个实例只用
1 个 CPU 核。网格点的并行不是靠程序内部的多线程,而是靠**同时启动多个程序实例**
1 个 CPU 核。网格点的并行不是靠程序内部的多线程,而是靠**同时启动多个程序实例**
(分布在多台计算节点上)。
### 4.2 如何做到并行
### 4.2 如何做到并行(分布式 Pull 模型)
`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):
...
```
- **节点**:每个物理设备跑一个 `node` 进程,`DCTS_MAX_SLOTS` 控制该节点并发槽位数
(管理员可经心跳配额动态下调)。
- **队列**:服务端把 pending 点推入 MQWorker 只要 `active_slots < effective_max_slots`
就持续 `claim` 抢占(pull 模型,天然负载均衡)。
- **沙盒隔离**:每任务独立工作目录 `data/work/task_<id>/`fort.* 文件互不冲突,
这是并行安全的基础。
- 早期 Python 单机 `multiprocessing.Pool` 架构已废弃。
- `nworkers`config.yaml,当前=**16**):同时运行的 worker 进程数。
- 每个 worker 是一个独立的 Python 子进程,调用 `run_one.py` 跑一个网格点
(在独立的工作目录里,互不干扰)。
- `imap_unordered`:哪个点先完成就先回收,立即分配下一个点(动态负载均衡)。
- 16 核机器跑 16 个 worker = 16 个 tlusty 实例同时跑 = 满载利用。
(按机器核数调整;每个 tlusty 运行是单线程的,调大 nworkers 即可吃更多核。)
### 4.3 吞吐量估算(参考)
**关键:每个 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 分钟(种子步进)| 同上 | 冷启动失败→种子步进成功 |
| 20000-40000K(标准 sdB 区)| ~12-25 分钟 | 随节点槽位总数线性扩展 | 冷启动全区间可靠 |
| 60000K + He-rich/低金属 | ~10-15 分钟 | 同上 | 冷启动或一步种子步进 |
| 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` 记录:
每个网格点完成后,`result_dir/<模型名>/conv.json`(节点归档)与
`seeds_dir/<模型名>/conv.json`(服务端)记录:
```json
{
@@ -229,33 +216,36 @@ with Pool(nworkers) as pool:
"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)"}
"max_relc": 0.0,
"note": "NITER=0 grey start",
"elapsed_sec": 2.1
},
{
"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}
"max_relc": 0.957,
"worst_depth": 1, "last_iter": 10, "n_depths": 50,
"elapsed_sec": 62.4
},
{
"label": "nl",
"converged": true,
"final": {"itek":null, "rc":0, "max_relc":0.0069,
"worst_depth":1, "last_iter":17, "n_depths":50}
"max_relc": 0.0069,
"worst_depth": 1, "last_iter": 17, "n_depths": 50,
"elapsed_sec": 7.8
}
]
}
```
> **走种子步进链时**`seed` 指向邻居 `.7``seed_step_used=true`
> `coldfail_backup` 指向 `<model>.coldfail/``stages` 里没有 `lte`,而是
> `seed_nc`LTGRAY=F 热启动,NITER=80)→ `nl`。
> **走种子步进链时**`seed` 指向邻居 `.7``stages` 里没有 `lte`而是
> `seed_nc``ltgray=F` 热启动,NITER=20)→ `nl`。
> 注意:`conv.json` 不含 `seed_step_used` / `coldfail_backup` 布尔字段(旧版字段已移除);
> 收敛手段归因由服务端 `tasks` 表的 `task_type`cold_run / seed_step)聚合统计。
每阶段记录:`converged`(是否收敛)、`max_relc`(最大相对变化)、
`worst_depth`(最差深度点)、`last_iter`(迭代次数)、`n_depths`(深度点数)、
@@ -263,90 +253,35 @@ with Pool(nworkers) as pool:
### 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']))
"
```
runner 在每个阶段的循环开始/结束处计时,conv.json 里每个 stage 有
`elapsed_sec`synspec 也有单独的 `synspec_sec`。另归档保留各阶段快照
`<name>.<label>.5/.6/.err/.nst/.7` 与收敛诊断 `<name>.<label>_chmax*.9`(见
`tlusty_result_artifacts.md`)。
### 5.3 统计整个网格的信息
`run_grid.py` 完成后写 `results/grid_status.json`
早期 `grid_status.json` 文件已废弃;当前状态统计由服务端 API 实时查询 DB 提供
```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},
...
]
}
```
- `GET /api/workflows`:各工作流内联进度(total/converged/failed/running…)
- `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`:进度-时间序列曲线
- 种子步进命中数 = `tasks``task_type='seed_step'``status='completed'` 的聚合
汇总统计命令:
```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',''))
"
```
前端 Dashboard 直接消费以上端点渲染(首页卡片、详情页概览/点表/平行集合分析图)。
### 5.4 单个网格点的详细收敛诊断
```bash
# 看某阶段的迭代收敛趋势(fort.9)
python3 src/check_conv.py results/<模型>/<模型>.nl.9 --chmax 0.01
# 画光谱(标出 CNO 诊断线位置)
python3 src/plot_spec.py results/<模型>
```
- 从节点 `result_dir/<模型>/` 读取 `<模型>.<label>_chmax*.9`(每阶段收敛诊断,
含最大相对变化随迭代下降过程)。
- 阶段日志 `<模型>.<label>.6/.err` 查看 tlusty 输出与报错。
- 光谱 `*.spec` / 连续谱 `*.cont` / b 因子 `*.bfac` / 出射谱 `*.emflux` 供物理分析。
---
## 6. 完整操作步骤
### 第1步:配置网格密度(config.yaml 的 grid 段)
### 第1步:配置工作流(workflows/<wf>.yaml 的 grid 段)
```yaml
grid:
teff: [20000, 30000, 40000, 60000] # 各维采样点列表
@@ -356,100 +291,85 @@ grid:
logn: [-4, -2, -1]
logo: [-4, -2, -1]
# 共 4*2*2*3*3*3 = 432 个点
tlusty:
enabled: true
policy: skip_converged
strategies: ["cold_run", "seed_step"] # 策略链:冷启动失败回退种子步进
synspec:
enabled: true
wstart: 3000
wend: 7000
```
### 第2步:设置环境变量
### 第2步:创建并启动工作流(HTTP API / Dashboard
```bash
export TLUSTY=/home/dckj/program/tlusty/tl208-s54
```
# 创建/保存工作流(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
### 第3步:预览(dry-run
# 启动(进入 initializing → running,后台异步建网格并派发)
curl -X POST localhost:8090/api/workflows/sdB_cno/start \
-H "Authorization: Bearer $ADMIN_TOKEN"
```
也可直接在 Dashboard「工作流 → 启动配置 → 启动」操作。启动后调度器按 wave(难度)
顺序把点推入 MQ,各节点 pull 抢占计算。
### 第3步:监控
- Dashboard:工作流卡片进度条、详情页概览(统计/进度曲线/ETA)、逐点表、平行集合分析图。
- API`GET /api/workflows/:name/stats` / `:name/points` / `:name/progress`
- 服务端日志:`RUST_LOG=info ./server`
### 第4步:断点续算 / 增量
- 工作流默认 `policy: skip_converged`:启动时跳过已收敛点、重试已失败点。
- 加密网格:往 `grid` 各维列表加更多点后保存并重启工作流(自动跳过已完成,只算新点)。
- 高温区加密建议先冷启动算 He-rich/低金属的"桥头堡"模型,建立种子库再扩散到
难收敛点(见 §7.1)。
### 单点调试
```bash
cd $TLUSTY/cno_grid
python3 src/run_grid.py config.yaml --dry-run
# 输出:grid: 432 points total, N already done, M to compute
# 把单个点置回 pending 重跑(或在 Dashboard 详情页查看该点尝试历史/阶段诊断)
curl -X POST localhost:8090/api/workflows/sdB_cno/points \
-H "Authorization: Bearer $ADMIN_TOKEN" -F point_name=t40000_...
```
### 第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)。
> 早期 Python 的 `run_grid.py` / `run_one.py` / `seed_step.py` 脚本与 `src/` 目录已废弃。
---
## 7. 注意事项
1. **种子步进回退(核心机制)**`seed_step_fallback: true`(默认开)时,
冷启动失败的点会自动改走种子步进链`seed_step.SEED_STEP_CHAIN`):
把失败结果备份到 `<model>.coldfail/`,用 `find_seed` 找最近邻已收敛的 `.7`
作种子,`LTGRAY=F + ICHANG=0` 热启动重跑。这是高温/He-poor/富金属区的
1. **种子步进回退(核心机制)**`tlusty_strategies` 链含 `seed_step` 时(默认推荐
`["cold_run", "seed_step"]`),冷启动失败的点会自动回退到种子步进链:服务端在全局
种子库找近邻已收敛 `.7``ltgray=F + ichang=0` 热启动重跑。这是高温/He-poor/富金属区的
决定性破局手段(详见 §2.1)。
**种子跨度限制**种子与目标的参数差不能太大(logg ≤0.5/步,
Teff5000K/步;logCNO 一步 ≤100×)。跨度过大(如 cno-4 直接跳 cno-1
1000× 金属跳跃)即便种子步进也发散 → 这是真实物理极限,网格如实标注
**种子库建立策略**:高温区建议先冷启动算 He-rich 或低金属的"桥头堡"
模型,建立种子库再扩散到难收敛点(如 cno-4 → cno-2 → cno-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.json``converged=true` 的点会被跳过。假收敛
atmosphere_has_nan=true)的点会被重算。
`atmosphere_has_nan=true`)的点会被重算。
3. **失败隔离**:单点失败(发散/崩溃)不中断整个网格,记入 grid_status.json
的 error/unfinished 列表。高温难点的失败大多是真实物理极限(见 §2.1),
不强制成功;如确需重试,可用 `seed_step.py` 手动从更近的种子起跳。
3. **失败处理**:单点失败(发散/崩溃)不中断整个网格;当前策略失败触发策略链回退,
链耗尽则标记 failed。高温难点的失败大多是真实物理极限(见 §2.1),不强制成功。
4. **磁盘空间**每个模型约 50-100MB(含中间文件,走种子步进的点还会留
`<model>.coldfail/` 备份)。432 点约需 20-40GB。如不够可定期清理中间文件
(保留 .spec/.cont/.7/conv.json
4. **磁盘空间**节点端归档 `result_dir` 永久保留(无 LRU 淘汰),每个模型约
2-10MB 白名单产物(`.spec/.cont/.iden/.7/.bfac/.emflux/各阶段快照/日志`)。沙盒
`task_*` 工作目录结算后清理,节点重启时清理残留
5. **并行安全**:每个 worker 用独立工作目录(results/<模型名>/),fort.* 文件
不冲突。可安全并行。
5. **并行安全**:每任务独立沙盒(`data/work/task_<id>/`),fort.* 文件不冲突。可安全并行。
6. **nst 文件行长限制(已修复)**tlusty 的 nst 解析器有 ~72 字符的行宽限制。
如果参数太多写在一行(如加了 IDLTE/IACC 后 >72 字符),行尾的参数会被
静默截断。`write_nst()` 现在把参数分两行写(line1≤64c, line2 余下参数)。
静默截断。`nst_writer` 把参数分两行写(line1≤64c, line2 余下参数)。
这是个隐蔽 bug——截断后 tlusty 不报错而是用默认值,导致"看似成功实则参数
没生效"。
7. **fort.84 残留(已修复)**:tlusty 运行时会在工作目录写 fort.84(nst 参数的
内部表示)。如果下一次 tlusty 运行(不同 NATOMS)读到旧的 fort.84,会报
"Bad integer for item 48" 崩溃。`run_tlusty()` 现在每次运行前删除 fort.84。
"Bad integer for item 48" 崩溃。runner 每次运行前删除 fort.84。
8. **收敛可靠性(如实)**:用正确配方(无 CHMAX/ITEK, NFREAD=2000, nc NITER=10
后,20000-40000K 全区间冷启动可靠收敛;60000-80000K + He-rich/低金属用
@@ -458,4 +378,4 @@ python3 src/seed_step.py --teff 80000 --logg 6.5 --loghe -4 \
未收敛。之前版本的"8/8 边界全部成功"不准确(基于错误的 CHMAX=0.1)。
`ICRSW`Hummer & Voels 切换)在 tlusty208 **原版中是死代码**
SUBROUTINE SWITCH 从未被 CALLCRSW≡1.0),不要依赖它稳定化;
即便源码修复启用后实测对难点也无帮助。详见 EXPERIENCE.md §5Y
即便源码修复启用后实测对难点也无帮助。详见 EXPERIENCE.md。