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。
+121 -37
View File
@@ -46,7 +46,7 @@
### 1.3 限流机制与错误模型 (Rate Limiting & Unified Error)
- **API 限流 (Rate Limiting)**:服务端对敏感接口(如 `/login`、`/node/register`)应用了基于 Governor / 漏桶算法的请求限流器。超出速率上限时返回 `429 Too Many Requests`。
- **API 限流 (Rate Limiting)**:服务端对敏感接口(如 `/login`、`/node/register`)应用了**滑动窗口**sliding window)请求限流器(`rate_limit.rs`。超出速率上限时返回 `429 Too Many Requests`。
- **统一错误格式 (AppError)**:所有 RESTful API 的错误响应均格式化为标准 JSON:
```json
{
@@ -103,23 +103,31 @@
### 2.2 任务规格 (`TaskSpec`)
服务端派发给 Worker 节点的单个计算任务定义。
- **Rust 定义** ([models.rs](file:///home/fmq/program/tlusty/tl208-s54/dcts/crates/common/src/models.rs#L98-L106)):
- **Rust 定义** ([models.rs](file:///home/fmq/program/tlusty/tl208-s54/dcts/crates/common/src/models.rs#L389-L425)):
```rust
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskSpec {
pub task_id: Uuid,
pub point_name: String,
pub params: GridPointParams,
pub task_type: TaskType, // ColdRun | SeedStep
pub seed_point_name: Option<String>,// 步进种子点名称(如适用)
pub task_type: TaskType, // ColdRun | SeedStep(兼容字段 = strategies[0]
pub seed_point_name: Option<String>, // 步进种子点名称(如适用)
pub timeout_sec: u64,
pub workflow_name: Option<String>, // 多工作流分区键
pub wave: i32, // 难度波次
pub tlusty_config: EngineStageConfig, // TLUSTY 阶段配置(enabled/policy/strategies
pub synspec_config: EngineStageConfig, // SYNSPEC 阶段配置
pub synspec_params: Option<serde_json::Value>, // SYNSPEC 数值参数(波长范围等)
pub atmosphere_ref: Option<String>, // 显式大气来源点(仅 SYNSPEC-only 场景)
}
```
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum TaskType {
ColdRun,
SeedStep,
`EngineStageConfig`(阶段独立配置,见 `task_engine_decoupling_design.md §3`):
```rust
pub struct EngineStageConfig {
pub enabled: bool,
pub policy: String, // skip_converged | force_recompute | skip_failed
pub strategies: Vec<String>, // 策略链:["cold_run","seed_step"],失败回退弹首项
}
```
@@ -127,6 +135,12 @@
```typescript
export type TaskType = 'cold_run' | 'seed_step';
export interface EngineStageConfig {
enabled: boolean;
policy: string;
strategies: string[];
}
export interface TaskSpec {
task_id: string;
point_name: string;
@@ -134,6 +148,12 @@
task_type: TaskType;
seed_point_name?: string | null;
timeout_sec: number;
workflow_name?: string | null;
wave: number;
tlusty_config: EngineStageConfig;
synspec_config: EngineStageConfig;
synspec_params?: Record<string, unknown> | null;
atmosphere_ref?: string | null;
}
```
@@ -142,7 +162,7 @@
### 2.3 任务上报报告 (`TaskReport`)
Worker 节点向服务端上报的任务计算结果。
- **Rust 定义** ([models.rs](file:///home/fmq/program/tlusty/tl208-s54/dcts/crates/common/src/models.rs#L126-L140)):
- **Rust 定义** ([models.rs](file:///home/fmq/program/tlusty/tl208-s54/dcts/crates/common/src/models.rs#L496-L513)):
```rust
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskReport {
@@ -158,6 +178,7 @@ Worker 节点向服务端上报的任务计算结果。
pub elapsed_sec: f64,
pub error_message: Option<String>,
pub summary_json: String,
pub failed_stage: Option<String>, // "tlusty" | "synspec":失败阶段归因,决定弹哪条策略链
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
@@ -187,6 +208,7 @@ Worker 节点向服务端上报的任务计算结果。
elapsed_sec: number;
error_message?: string | null;
summary_json: string;
failed_stage?: 'tlusty' | 'synspec' | null;
}
```
@@ -237,7 +259,7 @@ Worker 节点向服务端上报的任务计算结果。
node_id: string;
max_slots: number;
active_slots: number;
status: 'online' | 'offline';
status: 'online' | 'offline' | 'pending_approval' | 'disabled' | 'rejected';
cpu_usage: number;
memory_usage: number;
last_heartbeat: string;
@@ -281,9 +303,19 @@ Worker 节点向服务端上报的任务计算结果。
export interface WorkflowSummary {
name: string;
description?: string | null;
status: 'idle' | 'running' | 'paused' | 'completed';
status: 'idle' | 'initializing' | 'running' | 'paused' | 'completed';
created_at: string;
updated_at: string;
stats?: WorkflowListStats | null; // 内联网格点聚合计数(未启动为 null,见 §8.11)
}
export interface WorkflowListStats {
total: number;
converged: number;
failed: number;
running: number;
cold_run_converged: number;
seed_step_converged: number;
}
export interface WorkflowItem {
@@ -328,9 +360,12 @@ Worker 节点向服务端上报的任务计算结果。
{
"status": "pending_approval",
"message": "节点注册申请已成功提交!请在管理 Dashboard 控制台上点击【同意接入】授权该节点",
"node_token": null
"node_token": null,
"registration_secret": "<一次性凭据>"
}
```
> `registration_secret`:节点注册时下发的一次性凭据,节点须在后续
> `/api/node/check_status` 时回传,才能取走待发 token(防止仅知 node_id 的攻击者抢先冒领)。
- `200 OK` (已授权节点带专属 token 刷新配置):
```json
{
@@ -353,15 +388,16 @@ Worker 节点向服务端上报的任务计算结果。
### 3.2 节点心跳保活 (`POST /api/node/heartbeat`)
- **处理函数**: [`heartbeat_node`](file:///home/fmq/program/tlusty/tl208-s54/dcts/crates/server/src/api/node.rs#L17-L25)
- **处理函数**: [`heartbeat_node`](file:///home/fmq/program/tlusty/tl208-s54/dcts/crates/server/src/api/node.rs#L167-L190)
- **函数签名**:
```rust
pub async fn heartbeat_node(
State(state): State<AppState>,
Extension(auth_node): Extension<AuthenticatedNode>,
Json(req): Json<NodeHeartbeatRequest>,
) -> impl IntoResponse
```
- **鉴权**: 是 (若配置 Token)
- **鉴权**: 是Node 角色必填;`req.node_id` 必须与鉴权身份一致,否则 403
- **请求 Header**: `Content-Type: application/json`
- **请求 Body**:
```json
@@ -376,9 +412,12 @@ Worker 节点向服务端上报的任务计算结果。
- `200 OK` (成功):
```json
{
"status": "ok"
"status": "ok",
"admin_max_slots": 4
}
```
> `admin_max_slots`:管理员强制的并发槽位配额(`null` = 无限制,沿用物理 `max_slots`)。
> 节点据此动态调整本地 `effective_max_slots`(见 `dynamic_cpu_slots_design.md`)。
- `200 OK` (失败):
```json
{
@@ -415,12 +454,14 @@ Worker 节点向服务端上报的任务计算结果。
"node_id": "node-worker-01",
"status": "online",
"token_status": "active",
"token_issued_at": "2026-07-28T12:00:00Z"
"token_issued_at": "2026-07-28T12:00:00Z",
"admin_max_slots": 4
}
]
}
```
> `token_status` 取值:`active`(有效)/ `none`(无凭据记录)。token 失效靠重发覆盖 hash 实现,不存在「已吊销」中间态。
> `admin_max_slots`:管理员强制并发槽位配额(`null` = 无限制),Dashboard 据此渲染节点配额状态。
#### 2. 同意节点接入申请 (`POST /api/admin/nodes/:node_id/approve`)
- **权限**: Admin 角色
@@ -459,6 +500,22 @@ Worker 节点向服务端上报的任务计算结果。
-H "Authorization: Bearer <ADMIN_TOKEN>"
```
#### 7. 调整节点并发配额 (`POST /api/admin/nodes/:node_id/quota`)
- **权限**: Admin 角色
- **说明**: 动态调整节点并发槽位配额上限(不重启 Worker 进程,经下一次心跳响应下发生效)。传 `null` 解除限制,恢复物理 `max_slots`。详见 `dynamic_cpu_slots_design.md`。
- **请求 Body**:
```json
{ "admin_max_slots": 4 }
```
- **响应**:
- `200 OK`: `{"success": true, "message": "节点配额已更新"}`
- **curl 示例**:
```bash
curl -X POST http://localhost:8090/api/admin/nodes/node-worker-01/quota \
-H "Authorization: Bearer <ADMIN_TOKEN>" -H "Content-Type: application/json" \
-d '{"admin_max_slots": 4}'
```
---
## 4. 任务调度与结果上报 API (Task Processing)
@@ -470,9 +527,12 @@ Worker 节点向服务端上报的任务计算结果。
- **处理函数**: [`claim_task`](file:///home/fmq/program/tlusty/tl208-s54/dcts/crates/server/src/api/task.rs#L15-L25)
- **函数签名**:
```rust
pub async fn claim_task(State(state): State<AppState>) -> impl IntoResponse
pub async fn claim_task(
State(state): State<AppState>,
Extension(auth_node): Extension<AuthenticatedNode>,
) -> impl IntoResponse
```
- **鉴权**: 是 (若配置 Token)
- **鉴权**: 是(Node 角色必填;服务端据注入身份校验节点是否被停用)
- **请求 Body**: 无
- **响应 Schema**:
- `200 OK` (有可计算任务):
@@ -533,21 +593,23 @@ Worker 节点向服务端上报的任务计算结果。
```rust
pub async fn report_task(
State(state): State<AppState>,
Extension(auth_node): Extension<AuthenticatedNode>,
mut multipart: Multipart,
) -> impl IntoResponse
```
- **鉴权**: 是 (若配置 Token)
- **鉴权**: 是(Node 角色必填;服务端据注入身份做任务归属校验 `verify_task_claim`,防跨节点伪造)
- **请求格式**: `multipart/form-data`
- Part `report`: JSON 字符串 (映射为 `TaskReport`)
- Part `seed_file` *(可选)*: 二进制数据 (收敛网格点的 `.7` 大气结构种子文件)
- **响应 Schema**:
- `200 OK` (成功):
- `200 OK` (成功 / 幂等重放):
```json
{
"status": "ok",
"message": "上报成功"
}
```
> **幂等重放**:已结算任务(终态守卫已吸收)的重复/迟到上报仍返回 200,并补写种子,不重复结算。
- `400 Bad Request` (缺少 `report` 字段):
```json
{
@@ -562,7 +624,10 @@ Worker 节点向服务端上报的任务计算结果。
"message": "无法解析 params 或 summary_json"
}
```
- **说明**: 当 `converged == true` 且 `atmosphere_has_nan == false` 且包含 `seed_file` 时,服务端会将种子保存至 `seeds_dir/<point_name>/<point_name>.7` 并记入 `seeds` 表。若冷启动任务失败,服务端会自动唤醒 `GridScheduler` 触发针对该网格点的步进回退算法 (Seed-step Fallback)
- `409 Conflict` (任务归属校验失败): 任务被其它节点重新领用、或队列中已无该 task_id(已被结算清理)。此时返回冲突,不写库
- **说明**:
- 当 `converged == true` 且 `atmosphere_has_nan == false` 且包含 `seed_file` 时,服务端会将种子原子写入 `seeds_dir/<point_name>/<point_name>.7` 并记入 `seeds` 表(源精度 `point_name`)。
- 失败上报(未收敛 / 失败 / 超时)且 `state_changed == true` 时,服务端才触发**策略链回退**(`trigger_strategy_fallback`,按 `failed_stage` 弹 TLUSTY 或 SYNSPEC 策略链);被终态守卫吸收的重复失败(`state_changed=false`)不再触发,杜绝重复派发涡旋(2026-08-02 修复)。
- **curl 示例**:
```bash
curl -X POST http://localhost:8090/api/task/report \
@@ -591,6 +656,7 @@ Worker 节点向服务端上报的任务计算结果。
- **请求格式**: `multipart/form-data`
- Part `report`: 旧版 `conv.json` 的**原文 JSON**(映射为 `ModelSummary`,服务端解析出 `name` / `params` / `converged` / `final_max_relc`
- Part `seed_file`: 二进制数据(`.7` 大气种子文件;收敛点必传)
- Part `success_method` *(可选)*: 文本 `cold_run` / `seed_step`。由 `tools/import_results` 依据旧 `conv.json` 的 stages 是否含 `seed_nc` 判定后设置,决定导入点最终归因(缺省按 seed_step 统计)
- **Query 参数**: `workflow`(可选,默认 `imported`):目标工作流名,种子导入到该工作流的 `grid_points`。
- **命名保真**: `point_name` 取旧 `conv.json` 的 `name` 字段(源精度真名,如 `t20000_g5.0_...`),**逐字符**落库(磁盘目录、`grid_points.name`、`seeds.point_name`),与旧版 Python `gen_input5.model_name` 完全一致。
- **幂等**: `ON CONFLICT DO NOTHING` upsert `grid_points`、`conv.json` 与 `.7` 原子覆盖写,可重复运行。
@@ -742,15 +808,19 @@ Worker 节点向服务端上报的任务计算结果。
"grid_stats": {
"total": 512,
"pending": 210,
"queued": 60,
"running": 32,
"converged": 260,
"failed": 10
"failed": 10,
"cold_run_converged": 200,
"seed_step_converged": 60
}
}
```
> `grid_stats` 为**全部工作流的合计**(跨工作流全局聚合)。多工作流并发运行时,此处展示所有
> 工作流 grid_points 的汇总进度;如需查看单个工作流的进度,可读取该工作流各自的 grid_points 统计
> `Database::get_grid_summary_stats(Some(workflow_name))`)。
> 工作流 grid_points 的汇总进度;`queued` 与 `pending` 分开计数(多工作流分区新语义),
> `cold_run_converged` / `seed_step_converged` 为收敛手段归因;如需查看单个工作流的进度,
> 可读取该工作流各自的 grid_points 统计(`Database::get_grid_summary_stats(Some(workflow_name))`)。
- **curl 示例**:
```bash
curl -X GET http://localhost:8090/api/status \
@@ -881,7 +951,6 @@ Worker 节点向服务端上报的任务计算结果。
"success": true,
"message": "成功获取工作流详情",
"data": {
"id": 1,
"name": "sdB_cno",
"description": "sdB CNO 6D Stellar Atmosphere Grid",
"config_yaml": "...",
@@ -944,13 +1013,13 @@ Worker 节点向服务端上报的任务计算结果。
AxumPath(name): AxumPath<String>,
) -> impl IntoResponse
```
- **说明**: 校验工作流,解析 YAML 中定义的所有 6 维网格坐标点,通过 `GridScheduler::initialize_grid` 展开网格点并计算保序难度 Wave,写入 SQLite 任务队列并开启节点调度
- **说明**: 以原子 CAS`transition_workflow_to_initializing`)抢占启动权,随后**异步 spawn** 网格初始化:解析 YAML 的 6 维网格点、按 policy 决定点状态(跳过收敛/打回失败)、展开并计算保序难度 Wave、把 pending 点推入 MQ 开启节点调度。接口先返回 200,后台推进为 `running`
- **响应 Schema**:
- `200 OK` (成功启动):
- `200 OK` (成功启动,后台初始化中):
```json
{
"success": true,
"message": "工作流 'sdB_cno' 已成功启动并安排计算任务",
"message": "工作流 'sdB_cno' 已进入后台异步建立与挂载流程",
"data": null
}
```
@@ -962,6 +1031,14 @@ Worker 节点向服务端上报的任务计算结果。
"data": null
}
```
- `409 Conflict` (并发启动,初始化抢占失败):
```json
{
"success": false,
"message": "工作流 'sdB_cno' 初始化抢占挂起异常,请稍后重试",
"data": null
}
```
- `404 Not Found`:
```json
{
@@ -1028,7 +1105,6 @@ Worker 节点向服务端上报的任务计算结果。
"failed": 3,
"cold_run_converged": 220,
"seed_step_converged": 41,
"imported_converged": 0,
"waves": [
{ "wave": 0, "total": 108, "converged": 108, "failed": 0 }
],
@@ -1037,6 +1113,8 @@ Worker 节点向服务端上报的任务计算结果。
}
}
```
> 收敛手段归因仅 `cold_run_converged` / `seed_step_converged` 两字段;**无独立
> `imported_converged`**——历史导入点统一按 seed_step 途径计入(`success_method` 语义见 §8.8)。
> `avg_point_sec` = `AVG(COALESCE(tasks.elapsed_sec, created_at→completed_at 时间戳差))`——
> 优先用 Worker 回报的精确墙钟(不含排队等待),历史无 `elapsed_sec` 的行回退时间戳差近似;
> `eta_sec` = `avg_point_sec × (total - converged - failed) ÷ 在线节点总槽位`(并发感知;
@@ -1048,14 +1126,14 @@ Worker 节点向服务端上报的任务计算结果。
- **处理函数**: `get_workflow_points``crates/server/src/api/workflow.rs`
- **权限**: Admin
- **用途**: 详情页"网格点明细"表与"收敛性分析"热力图、概览"最近动态"流的数据源。
- **用途**: 详情页"网格点明细"表与"收敛性分析"Parallel Sets 平行集合图)、概览"最近动态"流的数据源。
每行附带**最近一次尝试**信息(`tasks` 关联子查询取最新行,从未派发过的点 `last_*` 为 null)。
- **查询参数**(全部可选,枚举值白名单校验,非法 → `400`:
| 参数 | 取值 | 默认 |
| :--- | :--- | :--- |
| `status` | `pending`/`queued`/`running`/`converged`/`failed` | 不过滤 |
| `method` | `cold_run`/`seed_step`/`imported` | 不过滤 |
| `method` | `cold_run`/`seed_step`(白名单**不含** `imported`,传入即 `400` | 不过滤 |
| `wave` | 整数波次 | 不过滤 |
| `q` | 点名子串(LIKE 通配符已转义) | 不过滤 |
| `sort` | `wave`/`teff`/`max_relc`/`attempts`/`last_completed_at` | `wave` |
@@ -1167,13 +1245,17 @@ Worker 节点向服务端上报的任务计算结果。
"running": 8, "converged": 261, "failed": 3 }
],
"rate_per_hour": 12.5,
"now": "2026-07-31T09:00:00Z",
"done_rate_per_hour": 11.0,
"rate_span_hours": 23.5,
"stalled_minutes": 0.0
}
}
```
> `series` 超 300 条自动降采样(首末点保留);`rate_per_hour` = 窗口首末
> converged 增量 ÷ 时长(快照不足 2 条为 null);`stalled_minutes` = 终态数
> converged+failed)最后一次增长至窗口末端的分钟数(前端 >10 分钟触发停滞预警)。
> converged 增量 ÷ 时长(快照不足 2 条为 null);`now` = 服务端当前 UTC 时刻(前端锚定曲线右缘);
> `done_rate_per_hour` = 终态完成速率(用于 ETA);`rate_span_hours` = 速率统计实际时间跨度;
> `stalled_minutes` = 终态数(converged+failed)最后一次增长至窗口末端的分钟数(前端 >10 分钟触发停滞预警)。
---
@@ -1213,11 +1295,13 @@ Worker 节点向服务端上报的任务计算结果。
| HTTP 状态码 | 触发场景说明 | 响应格式 | 核心原因与解决建议 |
| :--- | :--- | :--- | :--- |
| **`200 OK`** | 请求正常处理 | JSON / Binary Stream | 操作成功执行。 |
| **`400 Bad Request`** | 参数校验失败、缺失关键字段YAML 格式错误 | `application/json` / Plain Text | 检查请求 JSON 结构,验证 YAML 配置语法是否正确。 |
| **`400 Bad Request`** | 参数校验失败、缺失关键字段YAML 格式错误或枚举白名单拒绝 | `application/json` / Plain Text | 检查请求 JSON 结构,验证 YAML 配置语法,确认 `method`/`sort`/`order` 等枚举在合法取值内。 |
| **`401 Unauthorized`** | 鉴权失败或缺失 Authorization Header | `application/json` | 确认环境变量配置,并在 Request Header 中包含正确的 Token。 |
| **`403 Forbidden`** | 身份校验失败(心跳 `node_id` 与鉴权身份不匹配、跨点上报拒绝、角色权限不足) | `application/json` | 确认节点 token 与上报身份一致,检查 RBAC 角色。 |
| **`404 Not Found`** | 资源、种子文件或工作流不存在 | `application/json` | 校验请求 URL 中的资源文件名或工作流 `name` 是否拼写无误。 |
| **`429 Too Many Requests`** | API 请求超出速率限制(限流生效) | `application/json` | 降低请求频率或配置漏桶/令牌桶容量参数。 |
| **`409 Conflict`** | 状态冲突:节点停用/启用状态不支持、start 初始化抢占失败、report 任务归属校验失败 | `application/json` | 检查目标状态是否合法,或稍后重试。 |
| **`429 Too Many Requests`** | API 请求超出速率限制(滑动窗口限流生效) | `application/json` | 降低请求频率;限流为滑动窗口实现(`rate_limit.rs`),非漏桶。 |
| **`500 Internal Server Error`** | 服务端数据库错误、I/O 打开失败或队列异常 | `application/json` | 检查服务端日志以进一步厘清 SQLite 锁冲突、磁盘空间或资源路径问题。 |
---
*文档生成于 2026-07-27 | DCTS Server 0.1.0*
*文档生成于 2026-07-272026-08-04 更新以对齐阶段独立配置 / 多工作流隔离 / 节点配额 / 失败阶段归因。*
+4 -3
View File
@@ -45,7 +45,7 @@ flowchart TB
### 2.1 Master 服务端 (`server` & Web `dashboard`)
- **工作流与多任务隔离**:支持多工作流并发隔离运行(`grid_points``task_queue` 增加 `workflow_name` 复合唯一索引),调度与重置操作严格隔离在单工作流作用域内。支持旧版 SQLite 数据库启动时无缝平滑迁移。
- **安全中间件与 RBAC**:基于角色访问控制 (Admin / Node / Public) 划分 API 权限,内嵌 Bearer Token 校验、Governor / 漏桶算法限流中间件与 CORS 跨域控制,防御侧信道攻击与暴力破解。
- **安全中间件与 RBAC**:基于角色访问控制 (Admin / Node / Public) 划分 API 权限,内嵌 Bearer Token 校验、**滑动窗口(sliding window限流中间件**与 CORS 跨域控制,防御侧信道攻击与暴力破解。
- **节点凭据生命周期管理**:支持 Worker 节点注册申请、管理员审批授权、Token 重新颁发(旧 token 失效)与节点停用/启用全生命周期管理。
- **任务调度与分配**:调度器将 pending 网格点推入 `mq` 队列;Worker 以 **pull 抢占式** 领用任务(详见 [§5 任务调度模型](#5-任务调度模型-task-scheduling))。
- **状态维护与心跳监测**:后台离线检测线程定期标记超时未心跳的节点为 `offline`,并能将僵挂在超时节点上的任务自动回收到队列中(Requeue)。
@@ -96,7 +96,7 @@ stateDiagram-v2
1. **分布式无状态 Worker & 上报阶梯退避**:Worker 节点不保存持久运行控制状态,异常宕机不会损坏主数据集。计算结果向 Master 上报时,具备多达 8 次指数阶梯容灾回退(最大间隔 60 秒,覆盖超 2 分钟断断连长窗),稳健保障长时间高密物理算单不受瞬时组网闪断或服务端短时上线切换干预。
2. **零文件扫描与连接并发缩流**:底层任务分批取配、排队清洗与邻接优化(Seed-Stepping)全链线依赖常驻内存的 SQlite 主从精算并调优收敛连接池配置(主库=8,队列=4 减免本地排他写冲突并发挂断);去除了历史残存的高损及同步阻塞磁盘遍历 API,在确保零卡死响应的前提下提升查询搜索效力。
3. **任务超时与流控平稳保护 (Stale & Requeue)**:服务端后台定期向已超时死挂的 `Running` (默认 >1800 秒)作业予以强退回转为 `Pending`;此外当操作维护员发起暂停或终止工作流行为时,将仅平滑洗退待调 `Queued` 项,悉心保育在途已投的 Worker 数值演算完整出计算归表,防假命题竞合。
4. **多级退避与种子隔离**:若某点冷启动发散,自动隔离失败现场,依靠数据库记录寻找欧氏空间距离最匹配的热启动合拢 `.7` 气象序列;即便遇到底层强硬大步发散也不产生干扰并留存物理运行根系以便溯源
4. **策略链回退与种子匹配**:若某点当前策略失败,服务端弹出策略链首项(`trigger_strategy_fallback`),下一顺位为 `seed_step` 时在全局种子库中按**有向 CNO 距离**(富方向重罚 4×、贫方向 1×,非对称)匹配最近邻收敛 `.7` 大气作为热启动种子;无种子则保持 pending 待种子出现后自愈。失败现场经**白名单归档**(`result_dir`)保留科学产物,沙盒则在任务结算后清理(详见 `tlusty_result_artifacts.md` / `design.md`
---
@@ -125,12 +125,13 @@ LIMIT 1
```rust
let active = self.active_slots.load(Ordering::Acquire);
if (active as usize) < self.config.max_slots {
if (active as usize) < self.effective_max_slots.load(Ordering::Acquire) {
match self.claim_task().await { /* 抢任务 */ }
} else {
sleep(Duration::from_secs(2)).await; // 满载:不 claim,等槽位释放
}
```
> `effective_max_slots` = `min(admin_max_slots, physical_max_slots)`,管理员配额经心跳响应下发、动态生效(见 `dynamic_cpu_slots_design.md`)。
由此分两种工况:
+10 -7
View File
@@ -25,14 +25,16 @@ cargo build
```
dcts/
├── Cargo.toml # Workspace 根配置
├── config.yaml # 示例网格配置文件
├── crates/
│ ├── common/ # [Library] 物理计算引擎、子进程管理、收敛检测与模板
│ ├── common/ # [Library] 物理计算引擎、子进程管理、收敛检测、种子匹配与归档白名单
│ ├── server/ # [Binary] Axum HTTP REST 服务端与 Scheduler
│ ├── node/ # [Binary] Worker 节点 Daemon 与任务抢占回路
│ └── mq/ # [Library] SQLite 事务型分布式任务队列
├── dashboard/ # [Web] ESM 模块化运维看板(Vite
├── workflows/sdB_cno.yaml # 示例网格工作流配置
├── tools/
│ └── import_results/ # [Binary] 历史计算结果导入工具(旧版网格结果 → 标记已完成 + 迁移产物树
│ └── import_results/ # [Binary] 历史计算结果导入工具(旧版网格结果 → 种子/收敛入库
├── scripts/ # 结果拉取等运维脚本
└── docs/ # 分主题架构与规格技术文档
```
@@ -64,7 +66,8 @@ cargo clippy --workspace --all-targets -- -D warnings
## 4. 提交 Code Review 规范
1. **分支策略**:从 `main` 切出特性分支,推荐命名如 `feature/seed-optimizer``fix/stale-node-leak`
2. **Commit Message 规范**格式推荐使用 `module: succinct explanation`,如:
- `common: add tolerance factor check for seed finder`
- `server: fix sqlite connection pool leak under heavy load`
3. **保持文档更新**:若修改了 API 接口定义或数据库 Schema,请同步更新 `docs/api_reference.md` `docs/database.md`
2. **Commit Message 规范**遵循 Conventional Commits,格式为 `<type>(<scope>): <description>`,如:
- `feat(common): 有向 CNO 距离种子匹配`
- `fix(server): 修复调度竞态与僵尸任务自愈`
- `type``feat` / `fix` / `refactor` / `docs` / `test` / `chore` / `perf` / `ci``scope` 常用 `common` / `server` / `node` / `mq` / `dashboard` / `all`
3. **保持文档更新**:若修改了 API 接口定义或数据库 Schema,请同步更新 `docs/api.md``docs/database.md`,以及物理/调度设计文档(`docs/design.md``docs/task_engine_decoupling_design.md`)。
+109 -9
View File
@@ -10,8 +10,11 @@
erDiagram
WORKFLOWS ||--o{ GRID_POINTS : contains
GRID_POINTS ||--o{ TASKS : logs
WORKFLOWS ||--o{ WORKFLOW_PROGRESS_SNAPSHOTS : samples
NODES ||--o{ TASK_QUEUE : executes
NODES ||--o| NODE_CREDENTIALS : authenticates
SEEDS }o--o{ GRID_POINTS : hot-starts
subgraph PrimaryDB ["主数据库 (dcts.db)"]
WORKFLOWS {
string name PK
@@ -22,7 +25,8 @@ erDiagram
datetime updated_at
}
GRID_POINTS {
string name PK
integer id PK
string name
string workflow_name FK
double teff
double logg
@@ -35,16 +39,31 @@ erDiagram
string status
integer attempt_count
string success_method
double last_elapsed_sec
}
TASKS {
string task_id PK
string point_name FK
string point_name
string node_id
string task_type
string seed_point_name
string status
double max_relc
boolean atmosphere_has_nan
integer retry_count
datetime created_at
datetime started_at
datetime completed_at
string error_message
string workflow_name
boolean tlusty_enabled
string tlusty_policy
text tlusty_strategies
boolean synspec_enabled
string synspec_policy
text synspec_strategies
string atmosphere_ref
double elapsed_sec
}
NODES {
string node_id PK
@@ -54,8 +73,40 @@ erDiagram
double cpu_usage
double memory_usage
datetime last_heartbeat
string registration_secret
integer admin_max_slots
}
}
NODE_CREDENTIALS {
string node_id PK
string token_hash
datetime issued_at
integer revoked
string raw_token_pending
}
SEEDS {
integer id PK
string point_name UK
double teff
double logg
double loghe
double logc
double logn
double logo
string file_path
boolean is_clean
}
WORKFLOW_PROGRESS_SNAPSHOTS {
integer id PK
string workflow_name
datetime ts
integer total
integer pending
integer queued
integer running
integer converged
integer failed
}
end
subgraph QueueDB ["队列库 (dcts_queue.db / mq)"]
TASK_QUEUE {
@@ -91,28 +142,76 @@ erDiagram
`name` 共同构成复合唯一约束 `UNIQUE(workflow_name, name)`
- `teff`, `logg`, `loghe`, `logc`, `logn`, `logo` (`REAL NOT NULL`)6 维物理参数。
- `cno_sum` (`REAL NOT NULL`)CNO 丰度之和(调度排序用)。
- `wave` (`INTEGER`):按 cno_sum 分组的批次波次(调度优先级用)。
- `wave` (`INTEGER NOT NULL DEFAULT 0`):按 cno_sum 分组的批次波次(调度优先级用)。
- `status` (`VARCHAR(32)`)`pending` / `queued` / `running` / `converged` / `failed`
- `attempt_count` (`INTEGER`):失败重试计数(仅观测用)。
- `success_method` (`VARCHAR(32)`):收敛时的成功手段 (`cold_run` 冷启动成功 / `seed_step` 种子步进成功)。
- `success_method` (`VARCHAR(32)`):收敛时的成功手段 (`cold_run` 冷启动成功 / `seed_step` 种子步进成功 / `imported` 历史导入)。
- `last_elapsed_sec` (`REAL`):最近一次尝试的墙钟耗时(详情页 ETA 估算用,兼容旧数据回退)。
> **多工作流分区(per-workflow partitioning**`grid_points` 与 `task_queue` 均按 `workflow_name` 隔离。
> 调度、状态更新、stale 重投、`stop_workflow` 重置都限定在单个工作流内,互不影响。
> 历史旧库(无 `workflow_name` 列)在启动时自动迁移:表重建为复合唯一结构,旧行 `workflow_name`
> 标记为 `__legacy__`,不干扰新工作流查询。
### 2.3 `nodes` (计算节点心跳与状态表)
### 2.3 `tasks` (任务尝试记录表)
每次派发/尝试生成一行,记录任务的阶段配置快照与执行结果(详情页/归因/回退弹栈的数据源)。
- `task_id` (`VARCHAR(128) PRIMARY KEY`):任务 IDUUID)。
- `point_name` (`TEXT NOT NULL`):网格点权威名(源精度,非 params 重推)。
- `node_id` (`TEXT`):执行节点 ID。
- `task_type` (`VARCHAR(32)`):兼容字段,等于 `tlusty_strategies[0]``cold_run` / `seed_step`),供旧节点识别。
- `seed_point_name` (`TEXT`):种子步进时注入的近邻种子点(`GET /api/seed/<name>` 下载依据)。
- `status` (`VARCHAR(32)`)`pending` / `claimed` / `running` / `completed` / `failed` / `timeout`
- `max_relc` (`REAL`):最终最大相对变化(收敛判据量)。
- `atmosphere_has_nan` (`BOOLEAN`):最终大气是否含 >10% NaN 行(无效化标记)。
- `retry_count` (`INTEGER`):重试计数。
- `created_at` / `started_at` / `completed_at` (`DATETIME`):创建 / 开始 / 完成时间。
- `error_message` (`TEXT`):失败原因(含 synspec 错误/超时等)。
- `workflow_name` (`TEXT`):所属工作流(多工作流分区键)。
- **阶段独立配置快照(派发时落库)**:`tlusty_enabled` (`BOOLEAN`)、`tlusty_policy` (`TEXT`)、
`tlusty_strategies` (`JSON`)、`synspec_enabled` (`BOOLEAN`)、`synspec_policy` (`TEXT`)、
`synspec_strategies` (`JSON`)、`atmosphere_ref` (`TEXT`,显式大气来源点)。
回退时弹 `*_strategies` 链首(见 `task_engine_decoupling_design.md §4.2`),policy/策略链取派发时快照。
- `elapsed_sec` (`REAL`):任务墙钟耗时(详情页 ETA 估算优先用此值)。
### 2.4 `nodes` (计算节点心跳与状态表)
- `node_id` (`TEXT PRIMARY KEY`):节点唯一标识(未指定 `DCTS_NODE_ID` 时自动生成 `node-<uuid>`)。
- `max_slots` (`INTEGER NOT NULL`):节点声明的最大并发计算槽位数(即 `DCTS_MAX_SLOTS`,领用上限)。
- `active_slots` (`INTEGER NOT NULL DEFAULT 0`):当前正在执行的任务数,随 claim 自增、report 完结自减。
- `status` (`VARCHAR(32) NOT NULL DEFAULT 'online'`)`pending_approval`(注册待审批)/ `online`(在线)/ `offline`(心跳超时离线)/ `disabled`(管理员手动停用,保持心跳但不再分发任务)/ `rejected`
- `cpu_usage` / `memory_usage` (`REAL NOT NULL DEFAULT 0.0`):心跳上报的 CPU/内存使用率(仅观测展示用,**不参与任务分发决策**)。
- `last_heartbeat` (`DATETIME NOT NULL`):最后一次心跳上报时间,后台线程据此判定 `online → offline`
- `registration_secret` (`TEXT`):节点注册时下发的一次性凭据,用于 `/node/check_status` 取走专属 token 的二次鉴权。
- `admin_max_slots` (`INTEGER`,可空):管理员强制并发槽位配额(`NULL` = 无限制,沿用物理 `max_slots`),经心跳响应下发给节点动态生效。
### 2.4 `task_queue` (分布式任务队列表 - `mq`)
### 2.5 `node_credentials` (节点 L2 鉴权凭据表)
- `node_id` (`TEXT PRIMARY KEY`):关联 `nodes.node_id`
- `token_hash` (`TEXT NOT NULL`):节点专属 token 的 SHA-256 哈希(**不存明文**)。
- `issued_at` (`DATETIME NOT NULL`):颁发时间。
- `revoked` (`INTEGER NOT NULL DEFAULT 0`):吊销标记(重新颁发 token 时旧行吊销)。
- `raw_token_pending` (`TEXT`):暂存待确认的明文 token(颁发流程过渡用,确认后清除)。
### 2.6 `seeds` (种子缓存池表)
全局共享的已收敛大气 `.7` 索引(跨工作流复用,种子步进热启动数据源)。
- `id` (`INTEGER PRIMARY KEY AUTOINCREMENT`)。
- `point_name` (`TEXT UNIQUE NOT NULL`):种子点权威名(源精度,与磁盘文件名一致)。
- `teff`, `logg`, `loghe`, `logc`, `logn`, `logo` (`REAL NOT NULL`):6 维物理参数(种子匹配距离计算用)。
- `file_path` (`TEXT NOT NULL`)`.7` 大气文件在 `seeds_dir` 的路径。
- `is_clean` (`BOOLEAN NOT NULL DEFAULT 1`):大气是否干净(无 NaN)——仅干净种子可被匹配(`reload_seed_cache` 只加载 `is_clean=1`)。
> 运行期维护内存 `seed_cache` + `seed_index`exact_family 桶索引),`find_best_seed_from_db`
> 先查桶索引(O(1)~O(小)),未命中退化为全量 global 扫描(见 `seed_finder.rs` / `design.md §3`)。
### 2.7 `workflow_progress_snapshots` (工作流进度时间序列表)
后台定期(每分钟)为每个 running 工作流记录进度计数,供详情页进度曲线与 ETA 估算。
- `id` (`INTEGER PRIMARY KEY AUTOINCREMENT`)。
- `workflow_name` (`TEXT NOT NULL`)。
- `ts` (`DATETIME NOT NULL DEFAULT (datetime('now'))`):采样时间。
- `total` / `pending` / `queued` / `running` / `converged` / `failed` (`INTEGER NOT NULL`):该时刻各状态计数。
### 2.8 `task_queue` (分布式任务队列表 - `mq`)
驱动分布式抢占与超时重试的核心表,使用 SQLite `WAL` 模式确保高吞吐并发安全(详见 [`sqlite_queue.rs`](file:///home/fmq/program/tlusty/tl208-s54/dcts/crates/mq/src/sqlite_queue.rs#L65))。
- `task_id` (`VARCHAR(128) PRIMARY KEY`):任务 IDUUID)。
- `payload` (`TEXT NOT NULL`):序列化的 [`TaskSpec`](file:///home/fmq/program/tlusty/tl208-s54/dcts/crates/common/src/models.rs)(含 point_name / params / task_type / seed_point_name / workflow_name 等)。
- `payload` (`TEXT NOT NULL`):序列化的 [`TaskSpec`](file:///home/fmq/program/tlusty/tl208-s54/dcts/crates/common/src/models.rs)(含 point_name / params / task_type / seed_point_name / workflow_name / tlusty_config / synspec_config 等)。
- `status` (`TEXT NOT NULL`)`pending`(就绪待领用)/ `claimed`(已被某 node 领用,计算中)。
- `created_at` (`DATETIME NOT NULL`):推入队列时间(同 `wave` 内 FIFO 排序键)。
- `claimed_at` (`DATETIME`):被领用的时间戳(`requeue_stale_tasks` 据此判定超时回投)。
@@ -129,3 +228,4 @@ erDiagram
1. **WAL (Write-Ahead Logging) 模式**SQLite 连接自动启用 `PRAGMA journal_mode=WAL;``PRAGMA busy_timeout=15000;`,解决多线程/多进程读写锁竞争。
2. **连接池机制**:借助 `r2d2` + `r2d2_sqlite` 维护异步连接池,防止高并发下数据库句柄冲突。
3. **原子 Claim 事务**:任务抢占在单个 `IMMEDIATE` 事务内完成——先 `SELECT ... WHERE status='pending' ORDER BY wave ASC, created_at ASC LIMIT 1` 取队首,再 `UPDATE SET status='claimed', claimed_by_node_id=?`,提交后返回。事务保证同一任务不会被两个 node 同时领用;遇到 `SQLITE_BUSY/LOCKED` 最多退避重试 5 次(详见 [`pop_task`](file:///home/fmq/program/tlusty/tl208-s54/dcts/crates/mq/src/sqlite_queue.rs#L145))。
4. **凭据零明文存储**`node_credentials.token_hash` 只存 SHA-256 哈希;节点注册的一次性 `registration_secret` 也仅在审批前短期有效,避免数据库泄漏直接泄露有效 token。
+4 -4
View File
@@ -129,8 +129,8 @@ DCTS 运行时所依赖的物理计算底座二进制文件 `assets/tlusty_stati
| `DCTS_ADMIN_TOKEN` | *空* | 管理员控制台与敏感 API 鉴权令牌 |
| `DCTS_AUTH_TOKEN` | *空* | 旧版全局令牌(兼容回退为 Admin 凭据,建议迁移到 `DCTS_ADMIN_TOKEN` |
| `DCTS_AUTH_DISABLE` | `false` | 应急开发参数:设置为`1` 或 `true` 时跳过鉴权(仅本地调试,切勿生产) |
| `DCTS_STALE_SEC` | `1800` | 任务运行超时重新放回队列的时间上限(秒) |
| `DCTS_NODE_STALE_SEC` | `120` | 判定 Worker 节点离线的心跳超时时间(秒) |
| `DCTS_STALE_SEC` | `21600` | 任务运行超时重新放回队列的时间上限(秒)。默认 6 小时,约为单任务默认超时(7200s)的 3 倍缓冲,避免接近 timeout 的任务在上报前被重投 |
| `DCTS_NODE_STALE_SEC` | `60` | 判定 Worker 节点离线的心跳超时时间(秒) |
### 3.2 Worker 节点环境变量表 (`node`)
@@ -141,12 +141,12 @@ DCTS 运行时所依赖的物理计算底座二进制文件 `assets/tlusty_stati
| `DCTS_MAX_SLOTS` | `4` | 本地 Worker 节点的并发计算 Slot 槽位数 |
| `DCTS_RUNTIME_DIR` | `data/runtime` | 本地 TLUSTY/SYNSPEC 可执行程序及物理谱线数据存放目录 |
| `DCTS_WORK_DIR` | `data/work` | 本地计算沙盒工作目录 |
| `DCTS_RESULT_DIR` | `data/result` | node 端**完整计算结果归档**目录(光谱/连续谱/各阶段大气快照等全部产物)。超过 200 个网格点子目录时按 LRU 删最旧。旧名 `DCTS_ARCHIVE_DIR` 向后兼容 |
| `DCTS_RESULT_DIR` | `data/result` | node 端**完整计算结果归档**目录(光谱/连续谱/各阶段大气快照等全部产物)。**永久保留,无 LRU 淘汰**2026-08-02 撤销旧 MAX_RESULT_MODELS=200 上限,避免科学产物被淘汰丢失)。旧名 `DCTS_ARCHIVE_DIR` 向后兼容 |
| `DCTS_HEARTBEAT_SEC` | `15` | 向服务端发送心跳报告的时间间隔(秒) |
> **两目录分工**(重要,避免混淆):
> - **`data/seeds/`**server 端,`DCTS_SEEDS_DIR`):**种子库**。只存最小集 `conv.json` + `<name>.7`,供 `download_seed` 端点给远程 node 热启动下载。**永不清理**(种子是 SeedStep 必需资源,删除会导致已收敛点重算)。
> - **`data/result/`**node 端,`DCTS_RESULT_DIR`):**完整计算结果归档**。存全部科学产物(`.spec/.cont/.iden/.7/各阶段快照/日志`),供本地留档/排错。LRU 上限 200
> - **`data/result/`**node 端,`DCTS_RESULT_DIR`):**完整计算结果归档**。存全部科学产物(`.spec/.cont/.iden/.7/.bfac/.emflux/各阶段快照/日志`),供本地留档/排错。**永久保留,无 LRU 淘汰**2026-08-02 撤销 LRU 上限)
>
> 多机部署下两者物理分离:seeds 在 server 机、result 在各 node 机。单机部署下都挂在 `./data` 下。
+80 -35
View File
@@ -1,6 +1,9 @@
# DCTS 物理计算与流程设计 (Physics Pipeline Design)
> 本文档详细阐述 DCTS 核心物理计算引擎的设计原理、4 阶段 TLUSTY/SYNSPEC 计算链,以及冷启动与种子步进(Seed Stepping)发散回退机制。
> 本文档阐述 DCTS 核心物理计算引擎的设计原理、TLUSTY/SYNSPEC 计算链(冷启动 / 种子步进),
> 以及基于策略链(Strategy Chain)的回退机制与种子匹配算法。
> 阶段独立配置(`enabled` / `policy` / `strategies`)的完整设计见
> [`task_engine_decoupling_design.md`](./task_engine_decoupling_design.md)。
---
@@ -8,7 +11,9 @@
TLUSTY 通过**完全线性化 (Complete Linearization)** 方法求解恒星大气结构的 NLTE(非局部热力学平衡)统计平衡方程与辐射转移方程。由于线性化的收敛半径有限,若初始猜想(初猜)偏离真解较远,迭代极易发生数值发散。
DCTS 单点计算分为 **4 个渐进物理阶段**
DCTS 单点计算分为 **TLUSTY 大气结构求解 + SYNSPEC 光谱合成**两个独立阶段(各自有 `enabled` 开关)。TLUSTY 阶段内再按执行策略选用不同收敛链
**冷启动链 (ColdRun`default_cold_chain`)**
```mermaid
flowchart LR
@@ -17,52 +22,92 @@ flowchart LR
Stage3 --> Stage4["4. 合成光谱\n(synspec)\n输出 .spec / .cont"]
```
**种子步进链 (SeedStep`default_seed_chain`)**:直接以近邻已收敛大气为热启动起点,**跳过 lte 灰大气阶段**:
```mermaid
flowchart LR
Seed1["1. NLTE 连续谱热启动\n(seed_nc / tlusty)\n以种子 .7 作 fort.8 初猜"] --> Seed2["2. NLTE 完整谱线\n(nl / tlusty)\n精化收敛"]
Seed2 --> Seed3["3. 合成光谱\n(synspec)"]
```
### 各阶段物理配置与功能明细
| 阶段 | 执行程序 | 物理含义 | 关键参数 | 典型迭代/耗时 |
阶段配置见 `common::runner::default_cold_chain` / `default_seed_chain``StageConfig`):
| 阶段 | 执行程序 | 物理含义 | 关键参数 | 迭代上限 |
| :--- | :--- | :--- | :--- | :--- |
| **1. LTE 灰大气 (lte)** | `tlusty.exe` | 解析灰色不透明度求解温度结构,无需种子,提供合理起点。 | `T T` 模式 | 0 次迭代 / 1-3 秒 |
| **2. NLTE 连续谱 (nc)** | `tlusty.exe` | 切换到 NLTE,忽略束缚-束缚线跃迁(`ilvlin=0`),收敛基础电离平衡。 | `F F`, `ilvlin=0` | 10 次迭代 / 1-5 分钟 |
| **3. NLTE 完整谱线 (nl)** | `tlusty.exe` | 引入全部线跃迁(`ilvlin=100`),求解包含非平衡辐射场的大气结构。 | `F F`, `ilvlin=100` | 15-30 次迭代 / 5-20 分钟 |
| **4. 合成光谱 (synspec)** | `synspec.exe` | 基于阶段 3 收敛大气结构(`.7` 文件),计算高分辨率合成光谱。 | `INPOP=35` | 3-10 |
| **1. LTE 灰大气 (lte)** | `tlusty.exe` | 解析灰色不透明度求解温度结构,无需种子,提供合理起点。 | `lte=T, ltgray=T, niter=0, ilvlin=0` | 0 次(生成初始结构) |
| **2. NLTE 连续谱 (nc)** | `tlusty.exe` | 切换到 NLTE,忽略束缚-束缚线跃迁(`ilvlin=0`),收敛基础电离平衡。 | `lte=F, ltgray=F, ilvlin=0, niter=10` | 10 次 |
| **3. NLTE 完整谱线 (nl)** | `tlusty.exe` | 引入全部线跃迁(`ilvlin=100`),求解包含非平衡辐射场的大气结构**要求收敛**`require_converged=true`)。 | `lte=F, ltgray=F, ilvlin=100, niter=100` | 100 次 |
| **3'. 种子热启动 (seed_nc)** | `tlusty.exe` | 以近邻收敛大气 `.7` `fort.8` 初猜做 NLTE 连续谱收敛(跳过低温度灰大气阶段)。 | `lte=F, ltgray=F, ilvlin=0, niter=20, ichang=0` | 20 |
| **4. 合成光谱 (synspec)** | `synspec.exe` | 基于收敛大气结构(`.7`),计算高分辨率合成光谱。 | 波长窗/展宽/截断由 `synspec:` 数值参数配置 | — |
> 阶段标签由 workflow 配置保证唯一(`lte`/`nc`/`nl`/`seed_nc`),各阶段输入卡/日志/大气/收敛诊断
> 以 `<name>.<label>.<后缀>` 快照落盘归档(见 [`tlusty_result_artifacts.md`](./tlusty_result_artifacts.md))。
---
## 2. 冷启动链 vs 种子步进链 (Seed-Stepping Fallback)
## 2. 冷启动链 vs 种子步进链(策略链机制)
在极端高有效温度(如 $T_{\text{eff}} \ge 50,000\text{ K}$、极低氦丰度或强金属线空白区,从 LTE 灰大气直接启动的**冷启动链 (DEFAULT_CHAIN)** 容易发散。
针对这一问题,DCTS 引入了**动态种子步进机制 (Seed Stepping Chain)**
在极端高有效温度、极低氦丰度或强金属线空白区,从 LTE 灰大气直接启动的冷启动链容易发散。DCTS 通过 **策略链 (Strategy Chain)** 机制处理:工作流可配置 `tlusty_strategies`(如 `["cold_run", "seed_step"]`),调度器按顺序派发,失败时弹出链首、以剩余链回退重试(见 `task_engine_decoupling_design.md §4.2`
```mermaid
flowchart TD
Start([开始计算指定网格点]) --> ExecCold[执行冷启动链 lte -> nc -> nl]
ExecCold --> CheckCold{nl 阶段物理收敛?}
CheckCold -- "是 (Success)" --> RunSyn1[运行 Synspec 生成光谱] --> Save1[保存结果 & 汇报成功]
CheckCold -- "否 (Diverging)" --> IsolCold[清理并隔离冷启动失败现场]
IsolCold --> FindSeed[在种子库搜索最近邻收敛 .7 种子]
FindSeed --> HasSeed{找到合规邻居种子?}
HasSeed -- "" --> ExecSeed[启动种子步进链 seed_nc -> nl\nLTGRAY=F 热启动]
ExecSeed --> CheckSeed{nl 阶段物理收敛?}
CheckSeed -- "是 (Success)" --> RunSyn2[运行 Synspec 生成光谱] --> Save2[标记 seed_step_used=true & 汇报成功]
CheckSeed -- "否 (Failed)" --> MarkFail[标记任务彻底失败]
HasSeed -- "否" --> MarkFail
Start([开始计算指定网格点]) --> Resolve[调度器解析策略链\nresolve_dispatchable_chain]
Resolve --> Exec[执行链首策略]
Exec --> Check{nl 阶段物理收敛?}
Check -- "是 (Success)" --> RunSyn[运行 Synspec 生成光谱] --> Save[结果入库 & 汇报成功]
Check -- "否 (失败)" --> Pop[服务端弹出链首策略\ntrigger_strategy_fallback]
Pop --> Rest{剩余链非空?}
Rest -- "是,下一顺位 seed_step" --> FindSeed[查全局种子池找近邻收敛 .7 种子]
FindSeed --> HasSeed{找到合规种子?}
HasSeed -- "是" --> Inject[注入 seed_point_name\n重置 pending 再派发] --> Exec
HasSeed -- "" --> Hold[保持 pending 待种子出现后自愈\nseed_step 顺位暂存链尾)]
Rest -- "否(链耗尽)" --> Fail[标记任务彻底失败]
```
### 种子匹配策略 (Seed Finding Algorithm)
要点:
`common::seed_finder` 模块彻底摆脱了早期对文件系统进行同步阻塞遍历式查档的高耗延迟做法,改为经由 Master 服务端 SQLite 内存快照索表直接执行多级筛选,并使用标准化欧氏距离(Euclidean Distance)查找最佳热起邻格起点:
1. **派发门控**:链首为 `seed_step` 时必须能查到近邻种子才可派发;无种子则暂存该顺位到链尾、继续解析下一项(保证终止性,重复 seed_step 不陷入死循环)。初始派发与失败回退共用 `resolve_dispatchable_chain`
2. **种子注入**`seed_step` 派发时服务端在 `tasks` 行写入 `seed_point_name`,节点凭此下载种子大气(`GET /api/seed/<name>`),TLUSTY 以种子 `.7` 作为 `fort.8` 热启动输入,跳过灰大气阶段直接跑 `seed_nc -> nl`
3. **回退语义**:仅 `failed` 状态且状态实际迁移时触发(2026-08-02 修复,杜绝重复失败报告触发多余 seed_step);回退保留派发时的策略链快照,不修改原 `policy`
$$d(p_1, p_2) = \sqrt{ \sum_{i} w_i \left( \frac{x_{1,i} - x_{2,i}}{\sigma_i} \right)^2 }$$
---
优先匹配有效温度 $T_{\text{eff}}$ 和表面重力 $\log g$ 变化最小的已收敛 `.7` 大气结构作为 `fort.8` 热启动输入,跳过容易发散的灰大气阶段。
## 3. 种子匹配算法 (Seed Finding)
> [!NOTE]
> **物理现场溯源说明**:为了服务于严谨的天文理论算理复盘,Node 端的沙盒演算文件夹(`data/work/task_{uuid}`)及其中所产生成的全部迭代物理日志与 Fortran 临时数表不会触发自动入侵清除,为发生极端大气参数无解突断时的推导验证提供了长期完整的痕迹。
>
> **严谨声明校验**:在 `GridConfig` 的加载引擎中引入了非合规键位阻断(Denying Unknown Fields),有效杜绝了用户在定义参数和扩展选项(如 `niter`, `itek_fallback`, `template`, `fort55`, `linelist`)由于错拼被静默忽略而导致不可预测迭代的行为。
种子池是**全局共享的物理资源池**`seeds` 表 + 内存缓存 + 索引,跨工作流共享)。收敛且大气干净(无 NaN)的结果上报时,`.7` 大气原子写入 `seeds_dir/<name>/<name>.7` 并入库。
### 3.1 exact_family 桶索引
`(teff/5000, logg×100, loghe×100)` 量化成 `SeedBucketKey`,插入时写入 floor/floor+1 的 8 个笛卡尔积桶,查询时查 8 桶 → O(1)~O(小) 缩候选集,桶内再精算距离(量化只缩候选、不影响正确性)。
### 3.2 有向 CNO 距离(非对称,核心创新)
exact_family 判定(同物理族,仅 CNO 不同):`Δteff<5000 && Δlogg<0.01 && Δloghe<0.01`。候选按**有向 CNO 距离**排序:
```
delta = target cand(对 logc / logn / logo 各分量)
delta > 0(目标更富,坏方向)→ 惩罚 4.0 × delta
delta < 0(目标更贫,好方向)→ 惩罚 1.0 × |delta|
```
**数据标定依据**:对历史 1191 个真实 seed_step 配对的成败统计——贫金属方向(种子更富、目标往贫走)成功率 42–54%,富金属方向仅 3–11%(`he=-4` 时 54% vs 3%,差 18 倍)。物理解释:从高金属收敛解**减少**金属是稳定微扰;反过来**增加**金属时,新增紫外谱线辐射驱动会破坏已建立的辐射平衡 → 发散出现 NaN。
### 3.3 全局距离(跨 teff
exact_family 未命中时退化到全量扫描,候选需 `d ≤ 3.0`
```
d = Δteff/5000 + Δlogg×2 + Δloghe×0.5 + Δcno×0.1
```
权重物理意义:Teff 主导黑体势(÷5000 归一);logg 破坏静力学压强平衡最烈(×2);loghe 次之(×0.5);CNO 影响紫外谱线辐射驱动,方向性已由有向距离表达(×0.1)。
---
## 4. 节点端执行与现场处理
1. **种子获取**seed_step 任务凭 `seed_point_name` 下载种子 `.7`,落 `.seed_cache/`(LRU 上限 8),再复制私有副本进 slot 沙盒作为 `fort.8`
2. **沙盒生命周期**:任务算完 → 上报成功(或失败,尽力而为)→ **白名单归档**`result_dir/<name>/`,永久保留)→ **清理沙盒**;节点重启时也会**清理残留的 `task_*` 沙盒**2026-08-02 修复:避免强杀遗留工作目录写满磁盘)。归档白名单与 TLUSTY fort 单元语义详见 [`tlusty_result_artifacts.md`](./tlusty_result_artifacts.md)。
3. **节点无感知**:节点只执行 `strategies[0]` + 注入的 `seed_point_name`,对回退过程完全无感知,调度与计算解耦。
+76
View File
@@ -0,0 +1,76 @@
# 动态调整计算节点 CPU 核数(并发槽位)设计方案
> **状态:已实现**`effective_max_slots` + 心跳响应下发配额 + `/api/admin/nodes/:node_id/quota` +
> 节点详情管理弹窗)。下文为设计原文,保留作实现参照;实际实现以 `crates/node/src/worker.rs`、
> `crates/server/src/api/admin.rs`、`dashboard/src/components/nodesTable.js` 为准。
## 1. 需求背景
目前 DCTS 的计算节点 (Worker) 在启动时通过本地配置确定最大计算槽位 (`max_slots`),并在心跳和注册时上报给服务端。如果在不重启 Worker 进程的情况下,因为设备资源被其他用户借用等原因,需要临时减少该节点分发的任务数(即缩减 CPU 核数/并发槽位)。
## 2. 现有架构分析
- **Pull 模式**:任务的领用是由 Worker 主动发起 `POST /api/task/claim` 的。Worker 内部根据自身的 `max_slots` 控制并发领用的数量。
- **状态同步**Worker 定期发送 `POST /api/node/heartbeat` 汇报当前的 `active_slots`(正在运行的任务数)和资源负载。
因为是 Pull 模式,如果在服务端强行拒绝 `claim`(例如当节点的活跃任务超过管理员设定的配额时返回空),会导致 Worker 不断进行无效的轮询(Busy Loop),浪费网络和日志资源。因此,最优解是**让 Worker 能够感知到服务端下发的配额上限,并动态调整其本地的并发控制器**。
## 3. 设计方案:基于心跳响应的配额下发机制 (Heartbeat-based Quota Sync)
### 3.1 数据库与服务端改造
1. **DB 结构更新**
在服务端的 `nodes` 表中新增一个可空字段 `admin_max_slots` (INTEGER, Nullable),用于保存管理员强制指定的并发上限。
2. **新增管理 API**
- 接口:`POST /api/admin/nodes/:node_id/quota`
- 权限:Admin 角色
- 请求体:`{"admin_max_slots": 4}`(传 `null` 表示解除限制,恢复节点的物理槽位上限)
3. **心跳响应扩展**
修改 `POST /api/node/heartbeat` 的响应结构,将 `admin_max_slots` 的值透传给 Worker
```json
{
"status": "ok",
"admin_max_slots": 4 // 如果无限制,则为 null
}
```
*注意*:还需在 `crates/common/src/models.rs` 中为 `NodeInfo` 增加 `pub admin_max_slots: Option<i32>` 字段,确保 `/api/admin/nodes` 返回配额数据供 Dashboard 渲染。
### 3.2 Worker 节点改造
1. **解析心跳响应**
目前 Worker 的心跳线程(`crates/node/src/worker.rs`)忽略了响应体。需要在 `crates/common/src/models.rs` 中定义 `NodeHeartbeatResponse` 结构体,并在 Worker 中反序列化,提取 `admin_max_slots`。
2. **状态共享**
Worker 并非使用 `Semaphore` 控制并发,而是依赖 `Arc<AtomicI32>` (`active_slots`) 计数。我们需要在 `NodeWorker` 的状态中新增一个 `effective_max_slots: Arc<AtomicUsize>`,以便心跳线程和领用线程共享配额信息。
3. **动态调整并发控制逻辑**
- 心跳线程在收到心跳响应后,计算生效配额并更新:
```rust
let target_slots = match admin_max_slots {
Some(admin_limit) => std::cmp::min(admin_limit as usize, physical_max_slots),
None => physical_max_slots
};
self.effective_max_slots.store(target_slots, Ordering::Release);
```
- 任务领用(Claim)主循环的条件相应修改为:
```rust
let active = self.active_slots.load(Ordering::Acquire);
if (active as usize) < self.effective_max_slots.load(Ordering::Acquire) {
// ... 领用任务 ...
} else {
tokio::time::sleep(Duration::from_secs(2)).await;
}
```
- **平滑降级**:如果缩减时当前正在运行的槽位大于 `target_slots`,上述 `if` 判断会直接失败,Worker 自然进入 `sleep` 分支待命。无需强行中断物理计算任务,等现有任务结束后,数量便会自动回落到新配额以下,实现安全平滑降级(即便配额设为 0,也会安全地暂停接收新任务)。
### 3.3 前端控制台 (Dashboard) 改造
1. **保持表格原状**
外部节点列表的大表维持原状(“CPU 槽位”列仍然显示 `占用 / 最大`,不作修改,保持界面整洁)。
2. **重构操作列交互(收敛至统一弹窗)**:
- 将“管理操作”列中原有的“启用/停用”、“重发 Token”等多个离散按钮,统一替换为一个**三个点 (More Options) 的 SVG 图标按钮**。
- 点击该按钮后,利用系统中现有的弹窗样式 (如 `.modal-card`) 弹出一个**节点详情与管理 Modal**。
3. **节点详情管理弹窗内容**
- **基础信息展示**:显示节点的详细状态、心跳时间、资源使用率等。
- **槽位占用情况展示**:如果没有配额限制,显示格式为 `16/16`(占用/最大);如果有配额限制,显示格式为 `12/12/16`(占用/配额/最大),其中“最大”代表节点 `.env` 中 `DCTS_MAX_SLOTS` 设置的运行使用上限。
- **修改配额 (CPU 核数)**:提供表单供输入新的配额数字,调用 `/api/admin/nodes/:node_id/quota` 保存;或提供“清除配额”恢复运行使用的最大核数上限。
- **节点调度控制**:提供“暂停(停用)/启动(启用)节点”的操作按钮。
- **安全管理**:提供“重新颁发 Token”的操作按钮。
## 4. 方案优势
1. **平滑降级**:不中断正在运行的 TLUSTY 任务,实现优雅缩容。
2. **网络高效**:利用现有的高频心跳链路顺带下发配置,不增加额外的 RPC 接口,且避免了服务端阻断 claim 带来的无效轮询。
3. **职责清晰**Server 依然是状态中心,Worker 仍旧掌控并发调度,逻辑解耦。
+112
View File
@@ -0,0 +1,112 @@
# Runbook:僵尸任务涡旋修复上线(2026-08-02)
## 背景
2026-08-01 晚重启工作流后发生重复派发涡旋(单点最高被算 101 次、wave3-9 积压饿死 6+ 小时)与收敛点被迟到失败报告翻黑(converged 净减少)。根因与修复见本次代码变更(三条不变量:每点至多一个活任务;converged 吸收态;stop/重启不产生僵尸)。
**当前生产状态(14:24 实时)**:工作流已暂停、槽位已排空、converged 4621 / failed 1139 / pending 3456。本 runbook 从"部署新版"开始。
## 第 1 步:部署新版服务端
```bash
cd /home/fmq/program/tlusty/tl208-s54/dcts # 本机(代码所在)
./scripts/deploy.sh -e remote -b compose -r server
```
部署期间工作流保持 paused,无影响。部署完成后确认容器正常:
```bash
ssh fmq@100.66.1.2 'cd /vol1/1000/program/dcts && docker compose ps server'
curl -s "http://100.66.1.2:8090/api/status" -H "Authorization: Bearer <admin_token>" | head -c 300
```
## 第 2 步:存量清理(维护窗口,约 2 分钟)
在 100.66.1.2 上执行。建议停容器避免 WAL 锁争用(节点心跳会短暂失败并自动恢复):
```bash
ssh fmq@100.66.1.2
cd /vol1/1000/program/dcts
docker compose stop server
# 确认队列库路径(compose 默认为 data/dcts_queue.db
ls -la data/dcts.db data/dcts_queue.db
# 备份
cp data/dcts.db data/dcts.db.bak.$(date +%s)
cp data/dcts_queue.db data/dcts_queue.db.bak.$(date +%s)
# 清理 + 修复(先审计后执行)
sqlite3 data/dcts.db <<'SQL'
ATTACH 'file:data/dcts_queue.db?mode=ro' AS q;
-- 审计 1:将删除的僵尸行数(预期数千条;= 无队列凭证的 pending 行)
SELECT COUNT(*) AS zombies FROM main.tasks
WHERE status='pending' AND workflow_name='sdB_cno'
AND task_id NOT IN (SELECT task_id FROM q.task_queue);
-- 审计 2:被翻黑的收敛点数(success_method 非空的 failed 点)
SELECT COUNT(*) AS flipped FROM grid_points
WHERE workflow_name='sdB_cno' AND status='failed' AND success_method IS NOT NULL;
-- 审计 3:从未真正获得种子救援的 failed 点数
SELECT COUNT(*) AS never_rescued FROM grid_points
WHERE workflow_name='sdB_cno' AND status='failed' AND success_method IS NULL
AND NOT EXISTS (SELECT 1 FROM tasks t
WHERE t.point_name=grid_points.name
AND t.workflow_name='sdB_cno'
AND t.task_type='seed_step');
-- 执行 1:清僵尸(claimed 在途行自动保留,NOT IN 子查询不过滤 status
DELETE FROM main.tasks
WHERE status='pending' AND workflow_name='sdB_cno'
AND task_id NOT IN (SELECT task_id FROM q.task_queue);
-- 执行 2:修复被翻黑的收敛点
UPDATE grid_points SET status='converged'
WHERE workflow_name='sdB_cno' AND status='failed' AND success_method IS NOT NULL;
-- 执行 3(可选):复活从未获救援的 failed 点,走正常 cold_run → 种子回退路径
UPDATE grid_points SET status='pending'
WHERE workflow_name='sdB_cno' AND status='failed' AND success_method IS NULL
AND NOT EXISTS (SELECT 1 FROM tasks t
WHERE t.point_name=grid_points.name
AND t.workflow_name=grid_points.workflow_name
AND t.task_type='seed_step');
SQL
docker compose up -d server
```
> 注意:执行 3 会把约百个点打回 pending,重启后它们会先跑一轮 cold_run 再触发种子回退——这是预期行为。
## 第 3 步:启动工作流
```bash
curl -s -X POST "http://100.66.1.2:8090/api/workflows/sdB_cno/start" \
-H "Authorization: Bearer <admin_token>"
```
启动时 `initialize_grid` 卫生逻辑会再清一遍残留(与第 2 步不冲突),随后 3456 个 pending 点按 wave 升序正常派发。
## 第 4 步:验证(启动后 1 小时内)
```bash
# 1. 无重复派发:最近 1 小时每个点应至多 1 个任务(预期空结果)
ssh fmq@100.66.1.2 "sqlite3 /vol1/1000/program/dcts/data/dcts.db \
\"SELECT point_name, COUNT(*) n FROM tasks WHERE created_at > datetime('now','-1 hour') GROUP BY point_name HAVING n > 1;\""
# 2. converged 单调不减、queued 以 ~100/h 下降(每 10 分钟看一次)
watch -n 600 "curl -s http://100.66.1.2:8090/api/status -H 'Authorization: Bearer <admin_token>' | head -c 200"
# 3. 历史涡旋点不再产生新任务
ssh fmq@100.66.1.2 "sqlite3 /vol1/1000/program/dcts/data/dcts.db \
\"SELECT COUNT(*) FROM tasks WHERE point_name='t55000_g5.0_he2_c-4_n-3_o-4' AND created_at > datetime('now','-1 hour');\""
# 4. 观察项(非承诺):快照 running 计数是否恢复 >0。若节点满载但 running 恒 0,
# 属独立显示问题,查 server 日志中 claim 的 "跳过 running 标记" info 行定位。
```
## 回滚预案
若新版出现异常:`docker compose stop server` → 恢复备份(`cp data/dcts.db.bak.<ts> data/dcts.db`,队列库同理)→ 部署旧镜像 → up。清理 SQL 的逆操作不需要(僵尸行清除与翻黑修复都是单向正确方向)。
+282
View File
@@ -0,0 +1,282 @@
# 收敛恒星光谱的物理正确性保证调研
> 调研日期:2026-08-04
> 范围:TLUSTY 208 + SYNSPEC 54 计算的恒星大气与光谱的**物理正确性**保证方法。
> 前置文档:`docs/task_failure_detection_analysis.md`(程序"是否崩溃/收敛"的判定,是本调研的下层)。
> 核心命题:**程序正常退出(rc=0)且 max_relc < chmax ≠ 光谱在物理上正确**。本文调研介于两者之间的静默错误来源与验证方法。
> 方法:源码级静态分析 + 天体物理方法学综述 + 三方独立复核。
---
## 0. TL;DR
当前 pipeline 对光谱正确性的保证**基本停留在"数值收敛"层面**,完全没有做物理不变量校验。三方调研发现:
1. **最高 ROI 缺口**:TLUSTY 已经无条件地把**能量守恒诊断**(`TOTAL SURFACE FLUX`、逐深度 `RAD/TOT``CON/TOT``(RAD+CON)/TOT`)写进已归档的 `.6` 文件,**却从未被任何 Rust 代码解析**。这是 Hubeny 官方推荐的 1% 判据,零成本可得,是物理正确性的第一道硬门槛。
2. **系统性的配方风险**`fort55_writer.rs` 写的字段 `idrv/ifreq` 与 SYNSPEC 实际读取的 `IDSTD/IPRIN` 错位(已核实),导致所有光谱的标准深度参考点与输出详尽度偏离预期。
3. **网格级错误传播**:通过 `converged && !nan` 门控上传的种子大气(`.7`),会把"伪收敛但物理未平衡"的点扩散给整个物理族——这是网格计算特有的静默放大机制,现有检查无法识别。
4. **完整的方法学分层**(§6):硬门槛(线上每点)→ 质量监控(抽样)→ 深度核查(线下基准比对),覆盖辐射平衡、光谱自洽、网格一致性、外部基准四个维度。
---
## 1. 当前已产出但未利用的物理诊断
> 这是后续做正确性校验的基础——很多信息 TLUSTY/SYNSPEC 已经产出,只是我们没解析。
### 1.1 能量守恒诊断(最关键,零成本可得)
TLUSTY 在 `LFIN`(最终迭代)时由 `OUTPRI` 子程序无条件写出,位于 `.6` 文件(unit 6,已归档):
```
TOTAL SURFACE FLUX 6.77712339D+12 ← 实际积分通量 TOTF
ID MASS TAUROSS TEMP NE DENS P_gas LOG(G_rad) RAD/TOT CON/TOT (RAD+CON)/TOT
1 ... 8.913E-01 0.000E+00 8.91264E-01
```
- `FLTT = SIG4P * TEFF**4`(预期通量 = σTeff⁴/4π,`SIG4P``BASICS.FOR:51`
- `RAD/TOT` = 辐射通量 / σTeff⁴
- `(RAD+CON)/TOT` = 总通量(辐射+对流)/ σTeff⁴ —— **此列应≈1.0**
- 实测样本(发散模型 max_relc≈1e21):`(RAD+CON)/TOT ≈ 0.891`,即 **11% 能量守恒违反**;收敛良好模型应≈1.0
**关键事实**`.6` 已在归档白名单(`STAGE_SNAPSHOT_SUFFIXES` 含 "6"),但 `crates/` 下无任何代码解析它。**数据已就位,只差一个 `.6` 解析器写入 conv.json**。
### 1.2 辐射平衡逐深度残差(被覆盖丢失)
TLUSTY `RECHCK` 子程序(`tlusty208.f:49854`)在 LFIN 时无条件调用,逐深度计算 `re=(吸收−发射)/发射` 写入 unit 17fort.17)。但 unit 17 随后被 SYNSPEC `OUTPRI` 覆盖写连续谱(`synspec54.f:3371`)——**成功路径下 RECHCK 输出永久丢失**,仅 SYNSPEC 失败时 `.cont` 里残留的才是 RECHCK 表(这也是 `.cont` 后缀二义性的来源)。
修复方向:仿照 `.bfac`/`.emflux` 的快照做法,在 RECHCK 后/SYNSPEC 前快照 fort.17。
### 1.3 其他未启用 / 未解析的诊断
| 诊断 | 子程序 | unit→文件 | 启用条件 | 现状 |
|------|--------|----------|---------|------|
| 能量守恒总表 | `OUTPRI` | 6→`.6` | 无条件 | **已产出,未解析**(最高 ROI |
| 辐射平衡逐深度 | `RECHCK` | 17→`.cont` | 无条件 | **被 SYNSPEC 覆盖丢失** |
| 统计平衡残差 | `CHCKSE` | 16→fort.16 | `ICHCKP≠0`nst 未设) | **未启用** |
| 冷却/加热率 | `COOLRT` | 86/87/88 | `ICOOLP≠0`nst 未设) | **未启用** |
| 出射辐射场谱 | `OUTPRI` | 13→fort.13 | 无条件 | **白名单排除**result_filter.rs:179 |
| TLUSTY 出射谱 | `OUTPRI` | 14→`.emflux` | 无条件 | **已快照归档,未解析**(可做 bolometric 积分) |
| b 因子(NLTE 偏离) | `OUTPRI` | 12→`.bfac` | 无条件 | **已快照归档,未解析** |
| SYNSPEC 谱线统计 | START/OUTPRI | 6→`.log` | 无条件 | **已归档,未解析**(含 `LINES-TOTAL`、波长范围、等效宽度) |
### 1.4 conv.json 的现状
`ModelSummary` / `StageSummary` / `ConvCheckResult``models.rs`)记录的字段**全部是数值收敛指标**max_relc, last_iter, worst_depth, synspec_rc 等),**没有任何物理正确性字段**(能量守恒、辐射平衡、统计平衡残差)。
---
## 2. 静默错误的物理来源
> "程序正常退出但光谱物理错误"的四层来源,按严重度排序。每层给出源码证据与可检测性。
### 2.1 第一层:TLUSTY 大气结构错误(光谱错误的根因)
| 错误来源 | 物理后果 | 源码证据 | 当前是否捕获 | 严重度 |
|---------|---------|---------|------------|--------|
| **NITER 截断被当收敛** | 大气未达统计平衡,布居数错误 | `tlusty208.f:14728` `LFIN=ABS(CHMX).LE.CHMAX.OR.ITER.GE.NITER`OR 短路:CHMX 再大也会因 `ITER≥NITER` 判 LFIN | 部分(`conv_check.rs` 重判 max_relc<chmax,但若截断时"刚好"<chmax 则通过) | 极高 |
| **ACCEL2/Ng 加速伪收敛** | 加速外推后下次 CHMX 人为变小,提前判收敛但平衡未达成 | `tlusty208.f:29693-29797`,无加速后发散回退检查 | 未捕获(max_relc 变小与真收敛难区分) | 高 |
| **模型截断但物理不充分** | 关键 H/He 跃迁被省略,NLTE 种群错误但程序正常 | `gen_input5.rs` 固定 H=9lev、HeI=14、HeII=14;远低于 MLEVEL=1134 上限 | 未捕获(需物理判断) | 高 |
| **ND=50 深度网格偏稀** | 大气结构分辨率不足,谱合成输入质量差 | `nst_writer.rs:6` ND=50`BASICS.FOR:9` MDEPTH=100 | 未捕获(需对比 ND=99 | 中-高 |
### 2.2 第二层:SYNSPEC 谱合成错误
| 错误来源 | 物理后果 | 源码证据 | 当前是否捕获 | 严重度 |
|---------|---------|---------|------------|--------|
| **★fort.55 字段错位** | 标准深度取最深层(IDSTD=50=ND 而非自动 2ND/3≈33)、输出详尽度偏离、Lyman 线翼开关被误设 | `fort55_writer.rs:5``imode,idrv,ifreq`SYNSPEC `:253``IMODE,IDSTD,IPRIN` —— **已核实错位** | 未捕获 | **高(系统性)** |
| **NLTE 静默降级为 LTE** | 匹配不上的 NLTE 线被 set to LTE,线强度错误但程序继续 | `synspec54.f:9114-9176`,仅写 fort.11 计数 | 未捕获(DCTS 不解析 fort.11 | 高 |
| **IBFAC=0 默认全程按 LTE 读种群** | fort.8 不含/不全含 NLTE 种群时种群保持 LTE 不替换 | `synspec54.f:1157` IBFAC=0 默认;`:11220` NUMP>IP 才替换 | 未捕获 | 高 |
| **波长网格步长固定且窗口窄** | 窄线被漏掉,谱"正常但缺线" | `fort55_writer.rs:12` SPACE=0.01;默认窗口仅 1400-1410Å(`runner.rs:522` | 未捕获 | 中-高 |
| **线表缺失(只链 fort.19** | 缺关键谱线,有计数但无"应有线却缺失"校验 | `runner.rs:540` symlink 单一线表;`:9157` NLIN0 计数 | 未捕获 | 高 |
### 2.3 第三层:数据传递与种子继承(网格级错误放大)
| 错误来源 | 物理后果 | 源码证据 | 当前是否捕获 | 严重度 |
|---------|---------|---------|------------|--------|
| **★种子继承的错误传播** | 伪收敛点的 `.7` 通过 `converged && !nan` 门控上传为种子,被相邻 SeedStep 点热启动,**错误沿网格拓扑传播**;`directed_cno_distance` 倾向选贫方向,一个坏种子可污染整个 Teff/logg 族 | `executor.rs:193` 种子门控;`seed_finder.rs` 最近邻选种 | **未捕获**(坏种子本身"通过"了所有检查) | **极高** |
### 2.4 第四层:编译/数值精度
| 错误来源 | 物理后果 | 源码证据 | 当前是否捕获 | 严重度 |
|---------|---------|---------|------------|--------|
| **生产编译无 -fcheck/-ffpe-trap** | 数组越界、IEEE 异常 → rc=0 + 静默写越界内存,产出无 NaN 标记但物理错误的大气 | `deployment.md:86,90` `gfortran -fno-automatic -O3` | 未捕获(详见 task_failure_detection_analysis.md §4 漏洞 3 | 高 |
> 详见 `task_failure_detection_analysis.md` 的 gfortran 运行时错误退出码实测表。
---
## 3. 天体物理验证方法学
> 每个方法标注:验证的物理不变量、操作步骤、容差判据、所需数据、实现难度、线上/线下。
### 3.1 大气模型层面(TLUSTY 产出)
| 方法 | 验证的物理不变量 | 容差/判据 | 所需数据 | 难度 | 时机 |
|------|----------------|----------|---------|------|------|
| **通量守恒** | 每深度 πF(rad+conv)=σTeff⁴ | `(RAD+CON)/TOT` 偏离 1 ≤1%(Hubeny 官方判据);任一深度 >5% 直接判坏 | `.6` 末表 | 低 | **线上** |
| **max_relc 收敛门槛** | 状态向量相对修正已可忽略 | < chmax(默认 10⁻³,对 sdB/早型星合理;精密线分析可收紧到 10⁻⁴) | `*_chmax*.9` | 低(已实现) | **线上** |
| **假收敛排查** | max_relc 单调下降 ≥3 个量级 | 初始/最终 max_relc ≥10³ | fort.9 全序列 | 中 | 抽样 |
| **NaN/Inf 否决(收紧)** | 所有深度点温度/密度为有限物理值 | **任意 1 行命中即否决**(当前 10% 阈值过松) | `.7` | 低 | **线上** |
| **温度结构边界** | T(τ) 落在物理合理范围 | 表层 T ≲ 2-3×Teffidstd 处 T≈(2/3)Teff;总体 10¹-10⁸ K | `.7` | 中 | 抽样 |
| **统计平衡残差(NLTE** | 跃迁平衡 R_ij·n_i=R_ji·n_j、粒子数守恒 | b 因子无 NaN/Inf;关键能级 b∈10⁻³-10³ | `.bfac` | 中 | 抽样 |
**对 TLUSTY 的已知坑**:(a) fort.9 缺失分支无条件判收敛 + 虚构 `best_max_relc=0.0`(见 task_failure_detection_analysis.md 漏洞 2);(b) Kantorovich 加速的伪收敛风险;(c) 表层(worst_depth=1)最易出 NaN。
### 3.2 光谱合成层面(SYNSPEC 产出)
| 方法 | 验证的物理不变量 | 容差/判据 | 所需数据 | 难度 | 时机 |
|------|----------------|----------|---------|------|------|
| **NaN/负值/零行否决(最关键)** | 归一化谱∈[0,1],绝对流量≥0,无 NaN/Inf | **0 容忍**(任意一个即判坏) | `.spec``.cont` | 低 | **线上** |
| **Rayleigh-Jeans 尾 Planck 检验** | 长波端连续谱 → Bλ(Teff) | 斜率与 Bλ(Teff) 一致;偏离 >20% 异常 | `.cont` + Teff | 中 | 抽样 |
| **总积分流量能量守恒** | ∫Fλ dλ ≈ σTeff⁴(bolometric | 偏离 <1-2% | `.emflux`(全频率范围) | 中 | 抽样 |
| **连续谱自洽(TLUSTY vs SYNSPEC** | 同波长点 `.emflux``.cont` 水平一致 | 连续谱区差异 <1-2% | `.emflux` + `.cont` | 低 | 抽样 |
| **Balmer 线基准** | Hα/Hβ EW 随 Teff/logg 已知行为 | 线心深度∈(0,1)sdB 的 Hβ EW 约 5-15Å;高 Teff(>30kK) 不应异常强 | 光学 `.spec` | 中 | 抽样 |
| **He 线电离序列** | He II/He I 强度比随 Teff 单调 | 比值突变 = NLTE 电离平衡错 | 光学 `.spec` | 中 | 抽样 |
**归一化谱 vs 绝对流量谱**:归一化谱只验线形/线强,不能验能量守恒;绝对流量谱(`.emflux``.cont`)才能验通量守恒与 Planck。两者必须分别处理。
### 3.3 网格层面(内部一致性)
| 方法 | 验证的物理不变量 | 容差/判据 | 所需数据 | 难度 | 时机 |
|------|----------------|----------|---------|------|------|
| **相邻点平滑性(一阶差分)** | 光谱随参数连续变化 | 连续谱单步 Δ<10-15%;线 EW<30% | 全网格 `.cont`/EW | 中 | 线下 |
| **二阶差分 / 离群点检测** | 光谱无孤立异常 | r>4σ 或 \|残差\|>20% 标 outlier | 全网格光谱 | 中 | 线下 |
| **沿等参数线单调性** | Balmer/He 线随 Teff/logg 已知单调趋势 | 出现锯齿/反转 = 可疑 | 全网格光谱序列 | 低 | 线下 |
| **收敛质量地图** | max_relc 在网格上无孤立坏区 | 孤立高 max_relc 点(邻居都收敛)= 可疑 | 全网格 conv.json | 低 | 线下 |
**已知坑**:网格在 80kK+He-poor+富金属区有真实物理极限(CNO 高价离子主导不透明度,线性化无法阻尼)——这片成片失败是物理真实的,不应误判为 bug;但**孤立单点失败**(邻居都好)才需要复查。
### 3.4 外部基准比对(ground truth
| 基准 | 验证什么 | 容差/判据 | 数据来源 | 难度 | 时机 |
|------|---------|----------|---------|------|------|
| **Lanz & Hubeny 2003/2007 网格** | 与社区标准 TLUSTY/SYNSPEC 网格逐点一致 | 连续谱 <1-2%,线轮廓 <5% | tlusty.oca.euOSTAR2002/BSTAR2006,免费) | 中 | 建库时 |
| **NLTE-OBGRID** | Hubeny 更新版网格(更完整 H 原子) | 同上 | MAST archive.stsci.edu/hlsp/nlte-obgrid | 中 | 建库时 |
| **PHOENIX / CASTELLI / ATLAS9** | 与独立代码(不同物理近似)合理一致 | 光学几个百分点;UV(400nm)可达 10-20% | STScI | 中-高 | 建库时 |
| **CALSPEC 实测标准星** | 终极 ground truth | Bohlin+2020TLUSTY 白矮星网格 1500Å-30μm 一致到 **1%** | MASTGD153/GD71/G191B2B | 高 | 深度核查 |
| **Gaia FGK 基准星** | 非光谱学基本参数(干涉/角直径)校验 | gold standard | blancocuaresma.com/s/benchmarkstars | 高 | 深度核查 |
**实测比对必须考虑的系统误差**:消光(RV=3.1)、视向速度、instrumental broadening(需 ROTINS 卷积)、距离/标度因子。
---
## 4. 物理红旗清单(一票否决)
以下任一出现即光谱不可信,必须标记复查:
| 红旗 | 物理含义 | 检测方法 |
|------|---------|---------|
| 连续谱出现负值或 NaN | 辐射转移数值失败 | `.spec`/`.cont` 任意点 <0 或 NaN |
| 归一化谱线心深度 >1(流量 <0) | 线吸收过度/数值错 | 归一化谱任意点 <0 或 >1.05 |
| `.spec` 全零或行数异常少 | SYNSPEC 中途 quit 留脏数据 | 行数 <预期×0.9 或全零 |
| 总积分流量偏离 σTeff⁴ >2% | 能量不守恒 | 积分 `.emflux` |
| 通量守恒表末列偏离 1 >1%(任一深度) | 辐射平衡未达 | 读 `.6` 末表 |
| 表层温度 >>Teff(如 >3×Teff | T(τ) 结构非物理 | 读 `.7` 表层 T |
| 热星(Teff>30kKBalmer 线异常强 | NLTE 电离算错 | 测 Hα/Hβ EW 比对基准 |
| 连续谱斜率与 Teff 不符 | 温度结构错 | RJ 尾比对 Bλ(Teff) |
| 沿 Teff 序列 Balmer/He 线非单调 | 局部点 NLTE 错 | 网格级单调性检查 |
| 孤立单点与四邻插值偏离 >20% | 该点异常 | 离群点检测 |
---
## 5. fort.55 字段错位问题(已核实的系统性 bug)
这是一个影响**所有光谱**的系统性问题,值得单独记录。
### 5.1 事实
- `fort55_writer.rs:5` 生成的第一行:`imode, idrv, ifreq`(值如 `0, 50, 1`
- SYNSPEC `synspec54.f:253` 实际读取:`IMODE, IDSTD, IPRIN`
-**`idrv=50` 被当作 `IDSTD=50``ifreq=1` 被当作 `IPRIN=1`**
### 5.2 影响
- **IDSTD=50**=ND,最深层):SYNSPEC 的标准深度(`synspec54.f:2155`)本应自动取 `2*ND/3≈33`,现被强制取最深层。标准深度是谱线多普勒宽度、Stark 加宽的参考层,取错层会让线翼轮廓参考错误深度。
- **IPRIN=1**SYNSPEC 的输出详尽度开关,影响 `.log` 里打印的诊断量多少。
- **第 3 行 `IOPHLI=0`**`fort55_writer.rs:7` 第一个 0):`synspec54.f:255` 读取,IOPHLI 控制 Lyman 线翼处理,0 可能关闭相关处理。
### 5.3 处置
这是配方层面的 bug,需要单独修复(修正 `fort55_writer.rs` 的字段语义,使 IDSTD=0 自动、IPRIN/IPRIND 按需设置),并重算受影响的光谱。本调研仅记录,不含修复实现。
---
## 6. 分层验证策略建议
> 按"必须做 / 建议做 / 可选做"分层,标注每项的现状与所需投入。
### 第一层:硬门槛(线上,每个点必跑,0 容忍,失败即标记重算)
| # | 校验项 | 现状 | 所需投入 | 依据 |
|---|--------|------|---------|------|
| 1 | SYNSPEC `.spec`/`.cont` 内容校验(NaN/Inf/负值/全零/行数) | **未做**(漏洞 1 | 低 | task_failure_detection_analysis.md P0 |
| 2 | 大气 NaN 否决收紧到 0 行容忍 | 10% 阈值过松(漏洞 4 | 低 | conv_check.rs:212 |
| 3 | max_relc < chmax + is_finite + fort.9 真实存在 | 已实现,但 fort.9 缺失分支有虚构值(漏洞 2 | 低 | runner.rs:417 |
| 4 | **通量守恒 `(RAD+CON)/TOT` 偏离 1 ≤1%**(从 `.6` 提取) | **未做(数据已就位)** | 低(写一个 .6 解析器) | §1.1,Hubeny 官方判据 |
### 第二层:质量监控(线上抽样 + 线下定期)
| # | 校验项 | 现状 | 所需投入 |
|---|--------|------|---------|
| 5 | 总积分流量 vs σTeff⁴(`.emflux`,偏离 <2%) | 未做,数据已归档 | 中(bolometric 积分) |
| 6 | Rayleigh-Jeans 尾 Planck 斜率检验 | 未做 | 中 |
| 7 | TLUSTY `.emflux` 与 SYNSPEC `.cont` 连续谱自洽 | 未做 | 低 |
| 8 | 假收敛排查(max_relc 下降 ≥3 个量级) | 未做 | 中 |
| 9 | 收敛质量热图(网格级 max_relc 分布) | 未做 | 低(数据在 conv.json |
| 10 | 温度结构边界(表层 T、idstd 处 T) | 未做 | 中 |
### 第三层:深度核查(线下,建库时 + 每次配方变更时)
| # | 校验项 | 所需投入 |
|---|--------|---------|
| 11 | 与 Lanz & Hubeny 2003/2007 / NLTE-OBGRID 重叠点逐波长比对 | 中 |
| 12 | 与 PHOENIX/CASTELLI 独立代码比对 | 中-高 |
| 13 | 与 CALSPEC/SDSS 实测标准星比对 | 高 |
| 14 | Balmer/He 线序列单调性与基准 | 中 |
| 15 | 网格二阶差分离群点检测 | 中 |
| 16 | 统计平衡残差 / b 因子合理性(`.bfac` | 中 |
### 配方变更触发规则
任何以下变更必须重跑第三层深度核查:CHMAX/NITER/ND 改动、原子模型/线表(fort.19)更新、TLUSTY/SYNSPEC 重编译(尤其加 `-fcheck`/`-ffpe-trap`)、丰度网格范围调整、**fort.55 字段修复(§5**。
---
## 7. 综合判断
**当前 pipeline 在"光谱物理正确性校验"上基本没做** —— 只做了数值收敛性(max_relc)和最低限度的完整性(`.7` 无 NaN、synspec 退出码),完全没有能量守恒、辐射平衡、统计平衡这三项核心物理不变量校验。
**投入产出比排序**
1. **P0:解析 `.6` 的能量守恒诊断**(§1.1)—— 数据已就位,Hubeny 官方 1% 判据,是物理正确性的第一道硬门槛。同时修 `.spec` 内容校验(§6 第一层)。
2. **P1fort.55 字段错位修复**(§5)—— 影响所有光谱的系统性 bug,需修正后重算。
3. **P1:网格级种子传播校验**(§2.3)—— 这是网格计算特有的静默放大机制,可通过"同一物理族下游点的 max_relc/通量守恒分布异常"间接检测。
4. **P2:质量监控层**(§6 第二层)—— bolometric 积分、Planck 检验、连续谱自洽。
5. **P3:建库时基准比对**(§6 第三层)—— 与 Lanz & Hubeny 网格逐点比对是最直接的同代码 ground truth。
---
## 8. 参考资料
**官方文档**
- Hubeny 2017, "A Brief Introductory Guide to TLUSTY and SYNSPEC"arXiv:1706.01859)—— 通量守恒 1% 判据、假收敛、ND=50 B 星例、改丰度警告
- TLUSTY User's Guide IIIarXiv:1706.01937)—— fort.9 收敛日志格式、unit 6 辐射平衡表
- TLUSTY User's Guide IV, Hubeny 2021arXiv:2104.02829)—— CHMAX=10⁻⁴ 高精度例
**社区基准网格(ground truth**
- OSTAR2002 / BSTAR2006tlusty.oca.eu)—— Lanz & Hubeny 2003/2007 社区标准网格
- NLTE-OBGRIDMAST archive.stsci.edu/hlsp/nlte-obgrid)—— Hubeny 更新版
- WD-GRIDMAST archive.stsci.edu/hlsp/wd-grid)—— Bohlin+2020 白矮星 NLTE 网格,1500Å-30μm 对 CALSPEC 1% 一致
**交叉代码比对**
- Witzke+2021, MPS-ATLASA&A)—— 三代码流量差异:~400nm 最大,其他波段几个百分点
- CASTELLI/Kurucz ATLAS9STScI
**本仓库内相关文档**
- `docs/task_failure_detection_analysis.md` —— rc 不可靠性、fort.9/NaN 阈值漏洞、gfortran 退出码实测(第一层硬门槛的依据)
- `docs/tlusty_result_artifacts.md` —— fort 单元语义、`.emflux`/`.bfac` 快照、TLUSTY fort.14 与 SYNSPEC `.spec` 关系
- `docs/PIPELINE.md` —— 阶段链、nst 配方(ND=50、默认 CHMAX=0.001)、物理极限区
**源码证据**
- 能量守恒诊断:`tlusty208.f:14179-14191,14271-14273`OUTPRI);`:49854`RECHCK);实测 `hotsd/tlusty (副本)/hhe35nl.6:334-339`
- fort.55 错位:`fort55_writer.rs:5` vs `synspec54.f:253`(已核实)
- 种子传播:`executor.rs:193``seed_finder.rs`
+221
View File
@@ -0,0 +1,221 @@
# DCTS 任务计算引擎解耦与阶段独立配置架构设计方案
## 1. 背景与现状分析 (Background & Current Bottlenecks)
### 1.1 现状与概念降维问题
在 DCTS 早期版本中,任务类型通过单级平铺枚举 `TaskType`(如 `ColdRun`, `SeedStep`)定义。它将 TLUSTY 阶段的“初始猜想策略”、SYNSPEC 阶段的执行与否,以及全局任务的生命周期强行绑定在一起,导致概念降维与严重耦合。
### 1.2 组合爆炸风险
随着扩展能力(如 `Interpolation`, `LineListScan`)的加入,平铺枚举会导致 $N \times M$ 的组合爆炸,引发代码中大量的硬编码匹配分支。
### 1.3 `runner.rs` 与 `executor.rs` 的硬编码耦合问题
现存节点端代码将执行顺序与文件依赖硬编码,例如必须检查 `TaskType::SeedStep` 才能下载大气文件,无法灵活应对只运行光谱合成的场景。
---
## 2. 独立阶段配置架构设计 (Stage-Based Independent Configuration)
为了实现极高的灵活性并彻底避免组合爆炸,摒弃全局单一的 `TaskType` 枚举绑定,将 TLUSTY 和 SYNSPEC 视为平级的**独立阶段 (Stage)**。
对每一个计算阶段,均提供正交的三个配置维度:
1. **启用开关 (Enabled)**:布尔值,决定当前任务流是否要执行该阶段。
2. **执行策略 (Execution Policy)**:启动工作流时对历史终态点的处理逻辑(跳过收敛/重试失败、强制重算、跳过收敛及失败)。注意:策略只决定**启动时**对终态点的处理;失败后的策略链回退由策略链(§4.2)独立驱动,不受策略门控(2026-08-04 语义修正)。
3. **计算策略 (Compute Strategy)**:该阶段所采用的具体科学计算方法。
### 2.1 独立阶段正交模型
**阶段一:TLUSTY 大气结构求解**
- **Enabled**: `true` / `false`
- **Policy**: `SkipConverged` (默认:跳过收敛·重试失败) / `ForceRecompute` (全量重算) / `SkipFailed` (跳过收敛及失败)
- **Strategy**: `ColdRun` (冷启动) / `SeedStep` (种子推演)(系统已预留扩展其他策略如网格内插的接口)
**阶段二:SYNSPEC 光谱合成**
- **Enabled**: `true` / `false`
- **Policy**: `SkipConverged` / `ForceRecompute` / `SkipFailed`
- **Strategy**: `Standard` (标准)(系统已预留扩展如多分辨率展宽、线表扫描等策略接口)
### 2.2 典型场景映射
此设计允许前端灵活组装出任意复杂度的科研计算流:
- **场景 A(新网格生成)**TLUSTY [启用, 增量, ["cold_run", "seed_step"]] + SYNSPEC [启用, 增量, ["standard"]]
- **场景 B(仅更新光谱)**TLUSTY [关闭] + SYNSPEC [启用, 强制重算, ["standard"]]
- **场景 C(强制更新大气)**:TLUSTY [启用, 强制重算, ["seed_step"]] + SYNSPEC [关闭]
---
## 3. 数据模型与阶段配置结构设计 (Domain Model)
在底层核心结构 (`TaskSpec`) 中,我们将原有的单级平铺类型彻底废弃,转而采用**嵌套式单阶段配置模型 (`EngineStageConfig`)**(注:为避免与 `common::config::StageConfig` 迭代步进参数同名冲突,阶段配置统命名为 `EngineStageConfig`)。
- **`EngineStageConfig` 结构说明**
每一个阶段(无论是 TLUSTY 还是 SYNSPEC)都会拥有一个独立的 `EngineStageConfig` 对象,包含以下三个核心字段:
1. `enabled`: 布尔值。是否在当前计算流中启用该阶段。
2. `policy`: 枚举。决定**启动工作流时**对历史终态点的处理,可选值为:
- `SkipConverged`: 跳过已收敛、重试已失败(启动时把失败点打回 pending 重试,收敛点保留)。
- `ForceRecompute`: 强制重算(启动时收敛 + 失败全部打回 pending,无视历史状态与产物)。
- `SkipFailed`: 跳过收敛及失败(启动时收敛和失败点都保留,只算从未计算过的点)。
- 失败后的策略链回退不受策略门控,只由 `strategies` 链(回退优先级排序)驱动。
3. `strategies`: 策略链队列。存储该阶段的计算策略组合,例如 `[ColdRun, SeedStep]`
- **`TaskSpec` 根模型重构**
原有的 `task_type` 被标记为向后兼容保留字段,新增了:
- `tlusty_config`: 对应 TLUSTY 阶段的 `EngineStageConfig` 实例。
- `synspec_config`: 对应 SYNSPEC 阶段的 `EngineStageConfig` 实例。
- 保留旧版 `task_type` 作为废弃兼容字段,主要用于 Serde 反序列化历史遗留任务或在途 MQ 消息,并在 `normalize_compat()` 方法中自动映射到新的嵌套结构。
---
## 4. 数据库与流转设计 (Database & Scheduler Lifecycle)
### 4.1 数据库结构升级
任务表 (`tasks`) 需要打平阶段配置,彻底移除阶段绑定关系。
- **新增阶段控制字段**
- 为 TLUSTY 新增 `tlusty_enabled` (布尔)、`tlusty_policy` (文本)、`tlusty_strategies` (JSON 数组,默认 `["cold_run"]`)。
- 为 SYNSPEC 新增 `synspec_enabled` (布尔)、`synspec_policy` (文本)、`synspec_strategies` (JSON 数组,默认 `["standard"]`)。
- 新增 `atmosphere_ref` 用于显式关联大气网格点。
- **移除冗余旧字段**
- 在数据库层面移除旧的 `task_type` 列(或保留为 NULL 兼容列)。注:原数据库并无 `pipeline_scope` 列,无需清理。
### 4.2 Strategy Chain 自动链式回退机制 (Scheduler-Level)
原本硬编码在 `config.yaml` 中的 `seed_step_fallback` 逻辑,在本次重构中**升级为更为通用的“策略链 (Strategy Chain)”机制 (`tlusty_strategies`)**。用户可以选择一个或多个策略并排序,形成执行队列。
它的工作流如下:
1. **节点执行当前策略**:Node 接收到任务后,总是读取并执行队列中的第一个策略(例如 `tlusty_strategies[0]``ColdRun`)。
2. **失败上报**:当该策略执行失败时,Node 向 Server 报告任务失败 (`Failed`)。
3. **调度介入弹出队列**:Server 接收到失败报告后,检查 `tasks` 表中的 `tlusty_strategies` 数组。
- 若数组中只有一个元素,说明策略链耗尽,任务彻底失败。
- 若数组中还有后续元素(如 `[ColdRun, SeedStep]`),Server 会将失败的 `ColdRun` 从队列中弹出。
4. **生成新任务指令**Server 将该网格点重置为 `Pending` 状态,并为其生成**下一顺位策略**的任务指令:
- `tlusty_strategies: ["seed_step"]` (剩下的策略链)
- **不修改**原有的 `tlusty_policy` 字段(保持用户的初始配置,避免状态污染)。策略链回退只由策略链驱动(2026-08-04 语义修正:移除策略门控);若用户不想重试失败点,用单策略链(如 `["cold_run"]`+ `SkipFailed` 表达。
- `seed_point_name: 'xxx'` (如果是 SeedStep,Server 在此时负责在全局网格中寻找已收敛邻居点并注入)
5. **节点无感知执行**:Node 再次拉取到任务时,继续单纯地执行当前队列头部的 `SeedStep` 策略即可。Node 在准备运行新策略前,会自动清理该网格点上一轮策略留下的故障标记;节点对“回退过程”完全无感知,实现了全局调度与局部计算的完美解耦。
注:`synspec_strategies` 的自动弹栈与回退机制与 TLUSTY 保持完全一致(若配置了多策略链)。
---
## 5. 节点 Runner 与沙箱执行逻辑 (Node Executor)
在 DCTS 架构中,Node 采用**单任务独立沙箱 (`data/work/task_<task_id>`)** 执行流,并在完成后清理沙箱 (`cleanup_slot_work_dir`)。Server 端的 Scheduler 在生成 `TaskSpec` 时已依据网格数据库状态计算好当前任务指令,Node 仅需依指令执行:
1. **TLUSTY 阶段沙箱执行**
- 首先判断 `tlusty_config.enabled` 开关。若为 `false` 则跳过 TLUSTY 阶段。
- 若开启,Node 读取 `tlusty_config.strategies` 数组的第一项策略。
- 若策略为 `SeedStep`Node 凭 `seed_point_name` 自动向 Server API 异步下载种子 `.7` 大气文件并放入沙箱工作目录,随后调用 `ExecutionRunner` 启动 TLUSTY 迭代计算。
2. **SYNSPEC 阶段沙箱执行**
- 独立判断 `synspec_config.enabled` 开关。若为 `false` 则跳过。
- 若开启,Node 校验当前沙箱或关联的大气产物是否存在:若 TLUSTY 阶段刚完成则直接复用沙箱内 `.7` 文件;若为单独运行 SYNSPEC 场景(TLUSTY 关闭),Node先查看本地result目录是否有.7文件并复制到沙箱,如果没有再 凭 `atmosphere_ref`(或 `point_name`)自动向 Server/存储拉取目标 `.7` 文件放入沙箱。
- 读取 `synspec_config.strategies` 的第一项策略(如 `Standard`),调用 SYNSPEC 二进制进程合成光谱,并按照白名单将结果归档至result。
这种沙箱无状态机制完美切合节点端的生命周期,既保证了节点的干脆利落,又实现了与服务端全局调度的彻底解耦。
---
## 6. 前端 Dashboard 设计 (UI Design)
针对恒星大气网格工作流的主页 UI 进行大幅精简与重构。
### 6.1 移除冗余组件
- **移除卡片下方操作按钮**:彻底删除每个工作流卡片下方的“YAML配置”、“启动”、“暂停”等老旧按钮,工作流的启动配置将收敛至全新的内嵌配置面板。
- **移除收敛手段归因图表**:删除工作流首页中缺乏实际参考价值的“收敛手段归因”模块,为全新的任务调度引擎配置面板腾出空间。
### 6.2 工作流内嵌启动面板 (Embedded Task Configuration Panel)
不再使用弹窗 (Modal) 形式,而是直接将任务阶段独立配置面板**内嵌**在工作流详情页或首页的卡片内部,给予用户最直观且最大化的自由配置能力。
**2026-08 补充**:面板**默认折叠成一行**(标题 + 状态 + 操作按钮:YAML 配置 / 导出 / 暂停 / 删除 / 保存),点「启动配置 ▾」展开——折叠态下主按钮即「启动配置」(展开核对 TLUSTY/SYNSPEC 设置后再点真正的「启动」;工作流运行中该按钮退化为「配置 ▾」,且「启动」仅在展开态出现、避免误触发)。展开后 TLUSTY/SYNSPEC 两张阶段卡**左右并排**(宽屏两列,窄屏自动退化为单列),高度比纵向叠放减半。折叠状态跨轮询保持,展开中的编辑不因折叠丢失。
UI 布局草图如下:
```
+-------------------------------------------------------------------------+
| Workflow Execution Engine: o_star_grid_v1 |
+-------------------------------------------------------------------------+
| |
| [x] TLUSTY Atmosphere Computing Stage |
| Policy: [ Skip Converged (Incremental) v ] |
| Strategy Chain (Ordered by fallback priority): |
| 1. [ Cold Run v ] (X) |
| 2. [ Seed Step v ] (X) |
| + Add Fallback Strategy |
| |
| --------------------------------------------------------------------- |
| |
| [x] SYNSPEC Spectrum Synthesis Stage |
| Policy: [ Force Recompute v ] |
| Strategy Chain: |
| 1. [ Standard v ] (X) |
| |
| [ Save Config ] [ Start Grid ] |
+-------------------------------------------------------------------------+
```
前端 Payload 发送格式:
```json
{
"tlusty_config": {
"enabled": true,
"policy": "skip_converged",
"strategies": ["cold_run", "seed_step"]
},
"synspec_config": {
"enabled": true,
"policy": "force_recompute",
"strategies": ["standard"]
}
}
```
---
## 7. 兼容与部署限制(2026-08 审查补充)
### 7.1 滚动升级约束:阶段开关需全集群新节点
`task_type` 兼容字段只向后兼容 TLUSTY 策略(`cold_run` / `seed_step`)——旧节点仍能据此
区分冷启动与种子步进。但 **`enabled` 阶段开关无法传达给旧节点**
- TLUSTY-only 工作流(`synspec_stage.enabled: false`)在旧节点上仍会执行光谱合成;
- SYNSPEC-only 工作流(`tlusty.enabled: false`)在旧节点上被当作普通 cold_run+synspec 任务,
语义退化(需自带种子/大气才能成功)。
因此启用**阶段开关**(非默认双开)的工作流,须确保集群内全部节点已升级到支持阶段配置的
版本,否则节点行为与工作流意图不符。纯默认配置(双阶段启用)不受影响。
### 7.2 policy 决策取派发时快照(不随 YAML 漂移)
策略链回退(§4.2)的**策略链**与回退任务继承的 policy 均取**派发时落库的 DB 快照**
`tasks` 表的 `*_policy` / `*_strategies` 列,见 `FallbackSnapshot`),与 §4.2「不修改原有
policy,保持用户初始配置」语义一致。2026-08-04 起策略不再门控回退(回退只由策略链驱动),
但 policy 仍随重试任务落库,供审计与下次启动时的策略重置逻辑消费。数值参数
`synspec: {wstart/wend/...}``timeout_sec`)仍取当前 YAML(用户编辑意图优先),形成
「旧链 / 旧 policy + 新数值参数」的混合口径——这是有意取舍:策略链与 policy 是任务级
回退语义记录必须用派发时的,运行参数允许跟随最新配置。
### 7.3 完整 YAML 编辑器(数值参数编辑通道)已恢复
内嵌面板(§6.2)只编辑阶段三维配置(enabled / policy / strategies);`synspec:` 数值参数块
(波长范围、展宽、截断等)不在面板编辑范围。**2026-08 补充**:引擎面板新增「YAML 配置」按钮,
恢复原 `yamlEditor.js` 的完整 YAML 查看/编辑弹窗(语法高亮 + 行号 + 脏状态守卫 + 运行中锁定 +
服务端校验错误内联横幅),数值参数等完整配置均可在此查看与修改后直接保存(PUT
/api/workflows/:name)。该弹窗与内嵌面板的保存互相独立、以各自编辑内容为准——二者维度正交:
面板管执行语义(阶段开关/策略),弹窗管完整配置(含数值参数)。
+360
View File
@@ -0,0 +1,360 @@
# 任务失败判定逻辑调研:Tlusty&Synspec收敛性判断LUSTY / SYNSPEC 退出码可靠性与兜底机制
> 调研日期:2026-08-04
> 范围:`crates/common/src/runner.rs`、`crates/node/src/reporter.rs`、`crates/common/src/conv_check.rs`
> 的任务成败判定逻辑,对照 TLUSTY 208 / SYNSPEC 54 源码(`tlusty/tlusty208.f`、`synspec/synspec54.f`
> 与 gfortran 运行时退出码行为。
> 方法:源码级静态分析 + gfortran 实测(退出码、运行时错误)+ 三方独立复核。
> 本文仅记录调研结论,不含修复实现。
---
## 0. TL;DR
调度系统通过子进程退出码 `rc` 判定 TLUSTY / SYNSPEC 成败,但 **gfortran 下裸 `STOP` 默认返回 rc=0**,而两个 Fortran 程序的几乎所有错误路径用的都是裸 `STOP``call quit`(内部亦为裸 `stop`)。因此 **rc 本身不可靠**
当前判定能"基本不出错",靠的是 rc 之外的兜底:
- **TLUSTY**`rc==0 && fort.7 存在` 才进入 `check_fort9`max_relc < chmax+ `atmosphere_has_nan` 双重否决。这三重条件对"显式发散 STOP"和"硬崩溃(信号)"覆盖可靠。
- **SYNSPEC****仅靠 `synspec_rc==0`,下游零内容校验**,是当前系统最大的科学正确性风险。
复核(含 gfortran 运行时错误实测)发现两类此前未点出的风险:
1. **gfortran 运行时错误默认返回 rc=0 且静默**(实数 IEEE 异常、数组越界),生产编译未启用 `-fcheck`/`-ffpe-trap`
2. **fort.9 缺失分支无条件判收敛**,且写入虚构的 `best_max_relc=0.0`,会污染种子选择并向相邻网格点扩散。
修复优先级收敛为:SYNSPEC `.spec` 内容校验 > 收紧 `atmosphere_has_nan` 阈值 > 修正 fort.9 缺失分支 > 补齐 SOLVES/RYBSOL 发散路径归因。
---
## 1. 现状:失败判定的四层结构
node 端任务失败判定从底向上分四层:
```
execute_task (executor.rs) ← 前置 IO 失败 → 直接返回 Err
└─ run_model_with_timeout ← 计算失败 → 产出 ModelSummary(converged 字段)
└─ derive_report_status ← 半失败判定 → Completed / Failed
└─ infer_failed_stage ← 失败归因 → tlusty / synspec
```
### 第一层:`execute_task`executor.rs:11)—— 前置 IO 失败
直接返回 `Err`、进不到计算:
| 失败情形 | 处理 | 行号 |
| -------------------------------------- | ----------------------------------- | ------------------- |
| 原子数据文件拉取失败 | `warn` 但**继续**(非致命) | executor.rs:46-50 |
| 种子大气下载失败(HTTP 非 2xx / 网络) | `bail!` 返回 Err | executor.rs:114-137 |
| 沙盒目录创建失败 | `?` 返回 Err | executor.rs:57 |
返回 Err 时,worker.rs:456 转为 `String`reporter 走 `Err(e)` 分支(reporter.rs:78-89):
`status=Failed, converged=false, error_message=e`
### 第二层:`run_model_with_timeout`runner.rs:232)—— 计算过程收敛判定
核心,`final_converged` 在此累积,分三块:
**① TLUSTY 阶段链收敛判定(runner.rs:402-469**
每个阶段执行后,只有 `rc==0 && fort.7 存在` 才进入收敛检查,否则该阶段 `converged=false`
```rust
// runner.rs:402
if rc == 0 && fort7.is_file() {
if fort9.is_file() {
let res = check_fort9(&fort9, eff_chmax);
stage_summary.converged = res.converged; // max_relc < chmax
} else {
// NITER=0 grey start without fort.9 ← 见 §4 漏洞 2
stage_summary.converged = true;
stage_summary.best_max_relc = Some(0.0);
}
} else {
stage_summary.note = Some(format!("tlusty rc={} or missing fort.7", rc));
}
```
`check_fort9` 的收敛判据(conv_check.rs:163):
```
converged = max_relc.is_finite() && max_relc < chmax
```
其中 `max_relc` 是 fort.9 **最后一次迭代**中所有深度点里**最大的相对修正绝对值**。关键防御:
- `chmax ≤ 0 或非有限` → 直接判失败并报错(conv_check.rs:58,防误配)
- fort.9 无有效迭代数据 → `max_relc=Infinity`,判失败(conv_check.rs:127
- 发散值(NaN/Inf/无-E 记数法 `-1.35+118`)→ `is_finite()=false` → 判失败(conv_check.rs:160
- fort.9 缺失但 rc=0 → 视为"NITER=0 grey start"**判收敛**runner.rs:419)—— 见 §4 漏洞 2
阶段间联动:若某阶段 `require_converged=true` 且未收敛 → `break`,中止后续所有阶段(runner.rs:463)。
`final_converged` 取最后执行阶段的值。
**② 大气 NaN 强制否决(runner.rs:488-491**
```rust
let atmo_has_nan = atmosphere_has_nan(&final_7);
if atmo_has_nan { final_converged = false; }
```
`atmosphere_has_nan`conv_check.rs:190)检测大气文件里 `NaN`/`Inf`/`********`Fortran 字段溢出),
**超 10% 行命中**即判无效。此步会推翻前面收敛结论 —— 见 §4 漏洞 3 的阈值盲区。
**③ SYNSPEC 阶段(runner.rs:498-603**
SYNSPEC 不直接改 `final_converged`,记录到 `synspec_rc` / `synspec_error`
- `fort.8`(大气输入)复制失败 → 记 `synspec_error`,跳过(runner.rs:508
- `fort.55`(控制卡)写入失败 → 记 `synspec_error`,跳过(runner.rs:531
- synspec 进程 rc≠0 或超时(上限 `min(600, timeout_sec)`)→ 记 `synspec_rc`runner.rs:568-575
**仅 SYNSPEC 场景**(tlusty 关闭)的收敛重判(runner.rs:611-616):
`final_converged = (synspec_rc == 0)`
### 第三层:`derive_report_status`reporter.rs:38)—— 半失败 = 失败
```rust
let synspec_failed = synspec_error.is_some() || matches!(synspec_rc, Some(rc) if rc != 0);
if s.converged && !synspec_failed { TaskStatus::Completed } else { TaskStatus::Failed }
```
旧逻辑 `if converged { Completed }` 只看大气收敛,TLUSTY 收敛但 SYNSPEC 失败时 `converged` 仍为 true
→ 误报成功 → 服务端吸收为终态。新逻辑:**大气好 + 光谱坏 = 整体失败**,让服务端弹 synspec 链重试。
`converged` 字段仍按大气真实状态上报(为 true 时服务端仍存 .7 作种子)。
### 第四层:`infer_failed_stage`reporter.rs:15)—— 失败归因
| 条件 | 归因 |
| ---------------------------------------- | -------------------- |
| `converged=false` + tlusty 启用 | `tlusty`(含级联) |
| `converged=false` + tlusty 关闭 | `synspec` |
| `converged=true` 但 synspec 出错/rc≠0 | `synspec` |
| 全部成功 | `None` |
---
## 2. `rc==0` 的真实含义与不可靠性
### 2.1 gfortran 退出码实测(实证)
`/tmp` 用最小 Fortran 程序实测(gfortran 15.2.0`-fno-automatic -O3` 等价于生产编译环境):
| Fortran 语句 / 错误类型 | 触发方式 | 退出码 rc | runner 能否抓住 |
| ---------------------------------- | ------------- | --------------------------- | ----------------------- |
| 裸`STOP` | `stop` | **0** | ❌ 靠 fort.9 兜 |
| `STOP 'msg'` | `stop 'x'` | **0** | ❌ 靠 fort.9 兜 |
| `STOP 1`(带数字) | `stop 1` | 1 | ✅(TLUSTY 未用此形式) |
| 自然`END` | 主程序结束 | 0 | — |
| 实数除零`1.0/0.0` | IEEE 默认 | **0** | ❌ 产 Inf,无陷阱 |
| `0.0/0.0` | IEEE 默认 | **0** | ❌ 产 NaN |
| 实数溢出`1e30*1e30` | IEEE 默认 | **0** | ❌ 产 Inf |
| `sqrt(-1)` / `log(0)` | IEEE 默认 | **0** | ❌ 产 NaN/-Inf |
| **整数除零** `1/0` | SIGFPE | **136**128+8 | ✅ |
| **数组越界(默认)** | 无`-fcheck` | **0,静默写越界内存** | ❌ |
| 数组越界(`-fcheck=bounds`) | 启用检查 | 2 | ✅ |
| READ 遇 EOF(无`end=` 守卫) | 运行时 | **2** | ✅ |
| READ 类型不匹配(无`err=` 守卫) | 运行时 | **2** | ✅ |
| 栈溢出 / 段错误 | SIGSEGV | **139**128+11 | ✅ |
> 生产编译命令(deployment.md:86,90)为 `gfortran -fno-automatic -O3`**未启用** `-fcheck=bounds` 与
> `-ffpe-trap=invalid,zero,overflow`。因此表中所有 rc=0 的静默失败在生产二进制中真实存在。
### 2.2 核心矛盾
**Fortran 的 `STOP` 语句默认返回 0**,而 TLUSTY/SYNSPEC 几乎所有错误处理用的都是裸 `STOP``call quit`(内部裸 `stop`)。导致**进程异常终止的退出码 = 正常完成的退出码 = 0**。
- TLUSTY `subroutine quit`tlusty208.f:29946-29955)末行裸 `stop`
- SYNSPEC `subroutine quit`synspec54.f:12014-12021)末行裸 `stop`
- 两者全文 80+ 处 `STOP`/`call quit`,错误路径几乎全走裸 `STOP`
---
## 3. 源码级失败路径分析
### 3.1 TLUSTY:三重兜底对"显式发散"覆盖可靠
**主循环结构(tlusty208.f:1-62):**
```fortran
10 ITER=ITER+1
CALL RESOLV CALL OUTPUT(:3807/:3828) fort.7
IF(LFIN) GO TO 20 LFIN
IF(IACC.GT.0) CALL ACCEL2
IF(IFRYB.EQ.0) THEN
IF(NN.GT.MSMX) THEN
CALL SOLVE STOP(:14703)
ELSE
CALL SOLVES STOP(:15050)
END IF
ELSE
CALL RYBSOL STOP(:47585-47590)
END IF
GO TO 10
20 STOP STOP rc=0
END
```
**关键事实:**
- **fort.7 每次迭代都写**`CALL OUTPUT`tlusty208.f:3807, 3828)在 RESOLV 内,每次迭代执行。
- **发散 STOP 在三处平行求解器内**(不止 SOLVE):
- `SOLVE` :14701-14706`CHMX > 1.D16` → 裸 STOP
- `SOLVES` :15047-15051(同逻辑)
- `RYBSOL`/RYBCHN :47585-47590(同逻辑,`IFRYB≠0` 时走此路)
三处发散检查都在 `PRCHAN`/RYBCHN 写完 fort.9 **之后**,故发散 STOP 时 fort.7 与 fort.9 均已落盘。
- **达到 NITER 未收敛**`LFIN=ABS(CHMX).LE.CHMAX.OR.ITER.GE.NITER`(:14728),走主程序正常 ENDrc=0fort.9 含未收敛的 max_relc。
**TLUSTY 失败场景对照:**
| 场景 | Fortran 处理 | rc | 写 fort.7 | 写 fort.9 | runner 判定 | 是否合适 |
| --------------------- | --------------------------------- | ----------- | --------- | --------- | ---------------------------------------- | -------- |
| 正常收敛 | 主程序 END | 0 | ✅ | ✅ | check_fort9 → converged | ✅ |
| 达到 NITER 未收敛 | `LFIN`END | 0 | ✅ | ✅ | check_fort9 → max_relc≥chmax → 未收敛 | ✅ |
| 数值发散 CHMX>1e16 | SOLVE/SOLVES/RYBSOL 内裸 STOP | **0** | ✅ | ✅ | check_fort9 → 发散值未收敛 | ✅ |
| 输入错误(call quit | 裸 stop | 0 | ❌ | ❌ | rc=0 但 fort.7 缺失 → 失败 | ✅ |
| temp 越界 | `stop 'partf; temp...'`(:44624) | **0** | ❌ | ❌ | fort.7 缺失 → 失败 | ✅ |
**结论**rc 虽不可靠,但 `rc==0 + fort.7 存在 + check_fort9 + atmosphere_has_nan` 四重条件对"显式 STOP 类发散"和"硬崩溃(信号)"覆盖可靠 —— 这是一个**巧合但有效**的设计。
### 3.2 SYNSPEC:仅靠 rc,基本裸奔
**主程序结构(synspec54.f:1-174):** 主循环 `:10` 标签到 `:150 IF(IBLANK.LT.NBLANK) GO TO 10`
`CALL OUTPRI`:143)写谱输出(unit 7 谱 / 17 连续谱 / 16 部分等宽 / 6 诊断)且**在循环内、多次追加写**(无 REWIND)。
主程序以自然 `END`(:174)结束,正常路径 rc=0。
**call quit / STOP 相对 OUTPRI 的时序分类(复核产出):**
| 归类 | 数量 | 代表行号 | 风险 |
| ------------------------------------------------------------------ | -------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------ |
| **OUTPRI 之前**(START 阶段输入校验) | 多数 | :726, :808, :937, :1062, :1856, :2229, :12168, :12224, :12252, :12323, :12324, :16745, :19455, :19601 | 低 —— 谱未生成,靠"文件缺失"兜得住 |
| **OUTPRI 之后 / 之间**(主循环内 RESOLV/RESOLW/OPAC 调用链) | **6 处** | **:4203, :8208, :9935, :18863, :18865, :19891** | 高 —— 脏数据风险 |
| 死代码 / 坏码 | 3 处 | :17414`stop23`(坏码)、:17851 CHCKAB(调用被注释)、:18383 MOLSET(从未调用) | 无 |
**脏数据风险的精确触发条件**:仅在 **NBLANK>1 或分子/集合重处理**synspec54.f:152-162)导致前序迭代已写 fort.7 之后,后续迭代在 INISET/OPAC/RESOLW/SETRAY 等处中途 `call quit` → 残缺谱已落盘 + rc=0 → 被误判成功。**单集合单次运行(NBLANK=1)所有 quit 都在首次 OUTPRI 之前,无脏数据**。
**SYNSPEC 失败场景对照:**
| 场景 | Fortran 处理 | rc | runner 判定 | 是否合适 |
| ---------------------------------------- | ------------------ | ----------- | --------------------------------------------------------------- | --------------- |
| 正常完成 | 主程序 END:174 | 0 | synspec_rc=0 → 成功 | ✅ |
| 输入数据错误(OUTPRI 前 quit | 裸 stop | **0** | 谱未生成 → .spec 缺失(runner.rs:579 is_file 不成立)→ 未产谱 | ⚠️ 靠文件缺失 |
| 计算中途错误(OUTPRI 后 quit,多轮场景) | 裸 stop | **0** | synspec_rc=0 →**误判成功** | ❌ 严重 |
---
## 4. 复核发现的漏洞(按严重度排序)
> 以下为源码级静态分析与 gfortran 实测、经三方独立复核确认的风险。
### 漏洞 1(最严重):SYNSPEC 产出的 `.spec` 全链路零内容校验
runner 对 SYNSPEC 产出(runner.rs:578-599**只有 `is_file()` 存在性检查 + rename/copy**,无任何内容校验:
```rust
if model_dir.join("fort.7").is_file() {
let _ = tokio::fs::rename(..., ... ".spec").await; // 不查内容
}
```
下游全部不拦截:
- `executor.rs:191-215`:只读 `.7`(种子),从不碰 `.spec`
- `result_filter.rs:64-110`:纯文件名后缀白名单,`spec` 在白名单内(result_filter.rs:28),0 字节 `.spec` 与干净的 50MB `.spec` **同等归档**
- `derive_report_status`reporter.rs:38-45):只看 `synspec_rc``synspec_error`,不看产物。
完整穿透链:SYNSPEC `call quit`rc=0+ 写出含 NaN/Inf/0 字节的 `.spec``is_file()` 成立 → rename 成 `.spec`
`final_converged = rc==0`runner.rs:612)→ `synspec_failed=false`reporter.rs:39)→ `Completed`
脏谱归档、服务端吸收为终态、不重试。**没有任何一道防线检查 `.spec` 内容。**
### 漏洞 2(严重):fort.9 缺失分支无条件判收敛 + 虚构 best_max_relc=0.0
runner.rs:417-421,当 `rc==0 && fort.7 存在` 但 fort.9 **不存在**时:
```rust
} else {
// NITER=0 grey start without fort.9
stage_summary.converged = true; // 无条件判收敛
stage_summary.best_max_relc = Some(0.0); // 虚构"完美收敛"值
stage_summary.note = Some("NITER=0 grey start".to_string());
}
```
- 注释"NITER=0 grey start"是**断言而非检查**,代码未校验本次 stage 的 `niter` 配置是否真为 0。
- `best_max_relc = Some(0.0)` 进入 `final_max_relc`runner.rs:458-459)→ 上报(reporter.rs:73)。若服务端用 max_relc 选种子,会把误判模型当"完美收敛"优先选作下一轮种子,**污染向相邻网格点扩散**。
- 合法 grey start`lte`/`nc` 阶段,`require_converged=false`runner.rs:23/38)与致命崩溃共用同一段无校验代码,无法区分。
**触发条件**rc==0gfortran 裸 STOP 或运行时静默错误)+ fort.7 已写 + fort.9 缺失。严重度比漏洞 1 低,因为多数发散情况 fort.9 会写出含发散值的行,反而触发 `check_fort9``is_finite()=false` 判不收敛。
### 漏洞 3(严重):gfortran 运行时错误默认 rc=0 且静默
见 §2.1 实测表。在生产编译(无 `-fcheck`/`-ffpe-trap`)下:
- **实数 IEEE 异常**(除零、溢出、NaN 产生)→ rc=0,无陷阱,静默产出 NaN/Inf。
- **数组越界**(无 `-fcheck=bounds`)→ rc=0,**静默写越界内存**,可能产出物理上错误但无 NaN 标记的大气。
这两类若发生在 RESOLV 写完 fort.7tlusty208.f:3828)之后、SOLVE 写 fort.9 之前 → fort.7 在、fort.9 不在 → 命中漏洞 2 → 误判收敛。`atmosphere_has_nan` 能否兜住取决于越界是否恰好产生 NaN/Inf 字面值;若只是静默改写相邻内存,此防线无效。
### 漏洞 4(中):`atmosphere_has_nan` 的 10% 阈值对物理产物过松
conv_check.rs:212`(bad_lines as f64) > (total_lines as f64 * 0.1)`,严格大于。
典型 `.7` 文件 60-100 个深度行,阈值 6-10 行:
| 场景 | bad/total | 判定 | 物理正确性 |
| --------------------------- | --------- | ----------------------- | -------------- |
| 全 NaN(彻底发散) | 100% | true → 否决 | 正确 |
| 6/100 行 NaN(表层发散) | 6% | **false → 放过** | **错误** |
| 5/60 行 NaN(典型深度网格) | 8.3% | **false → 放过** | **错误** |
| 单行 NaN | 1% | **false → 放过** | **错误** |
物理上**任意一个深度点的温度/密度是 NaN,整个大气就不可用**(积分会传播 NaN)。10% 是统计学语义,与大气可用性的布尔语义不匹配。表层(深度浅、温度高)最易数值发散,往往只有最外几层出 NaN —— 正好落在盲区。
补充盲区:regex `(?i)(\bnan\b|\binf(?:inity)?\b|\*{3,})`conv_check.rs:199)只匹配字面 `NaN`/`Inf`/`***`;若 Fortran 把发散值写成接近 f64 上限但未溢出的普通科学记数法(如 `1.0E+308`),不命中任何模式。
### 漏洞 5(低):`infer_failed_stage` / 日志归因对 RYBSOL 路径不完整
发散 STOP 在 SOLVE / SOLVES / RYBSOL 三处(§3.1),但失败 note 统一记 `tlusty rc=0 or missing fort.7`runner.rs:429),丢失"为何未收敛"的线索。对走 RYBSOL`IFRYB≠0`)的发散,归因信息不完整。
### 漏洞 6(低):仅 SYNSPEC 场景 final_converged 重判只看 rc
runner.rs:611-616TLUSTY 关闭的纯复算场景,`final_converged = (synspec_rc == 0)`,与漏洞 1 同源,场景更窄。
---
## 5. 被排除的伪风险
**超时 / shutdown 后半成品 fort.7 进 check_fort9 误判收敛 —— 不存在。**
`run_child_async_with_timeout` 在超时/shutdown 时返回 `Err`runner.rs:151, 156)→ 上层 match `rc = -1`
runner.rs:380-383 / 568-574)。于是 `rc==0 && fort.7 存在` 前置门不成立 → 进 `else` 分支 →
`converged` 保持 false → `check_fort9` 根本不调用。SYNSPEC 超时同理 → `synspec_rc=-1` → 判失败。
**超时路径是可靠的。**
---
## 6. 修复优先级(仅建议,不含实现)
| 优先级 | 漏洞 | 建议 | 改动范围 |
| ------ | ------ | --------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- |
| P0 | 漏洞 1 | SYNSPEC`.spec` 内容校验:NaN/Inf/`***` + 行数下限 + 非全零,命中则置 `synspec_rc` 为非零或设 `synspec_error` | runner.rsSYNSPEC 产出后,runner.rs:585 附近) |
| P1 | 漏洞 4 | 收紧物理产物 NaN 阈值:产物准入用 0 行容忍(任意一行 NaN 即否决),而非 10% | conv_check.rs:212 或新增严格版函数 |
| P1 | 漏洞 2 | runner.rs:417-421:至少把`best_max_relc``Some(0.0)` 改为 `None`(消除污染扩散);理想是校验 stage 的 `niter` 配置才走该分支 | runner.rs:417-421 |
| P2 | 漏洞 5 | 补齐 SOLVES / RYBSOL 发散路径的归因,失败 note 区分求解器类型 | runner.rs:429 |
| P3 | 漏洞 3 | 生产编译加`-fcheck=bounds -ffpe-trap=invalid,zero,overflow`,让数组越界与 IEEE 异常返回 rc≠0(性能换正确性) | deployment.md 编译命令 |
---
## 7. 参考资料
- 源码:
- `tlusty/tlusty208.f`(主程序 :1-62;发散 STOP :14703/:15050/:47585quit :29946-29955
PRCHAN :22876-22944RYBCHN :47445-47598RESOLV :3724-3903OUTPUT :14055 起)
- `synspec/synspec54.f`(主程序 :1-174OUTPRI :3343-3458quit :12014-12021
脏数据风险点 :4203/:8208/:9935/:18863/:18865/:19891
- 判定逻辑:
- `crates/common/src/runner.rs`run_model_with_timeout :232-682;漏洞 1/2/5/6 所在)
- `crates/node/src/reporter.rs`derive_report_status :38-45infer_failed_stage :15-27
- `crates/common/src/conv_check.rs`check_fort9 :54-175atmosphere_has_nan :190-213
- 编译配置:`docs/deployment.md:86,90`(确认生产编译无 `-fcheck`/`-ffpe-trap`
- 相关设计:`docs/task_engine_decoupling_design.md`(阶段独立配置、半失败判定语义)
+195
View File
@@ -0,0 +1,195 @@
# TLUSTY 结果产物分析:Node 归档白名单与 fort 单元快照
> 本文档记录对 TLUSTY/SYNSPEC 源码的单元号(fort.N)语义分析,以及据此确定的
> Node 端结果归档(`result_dir`)白名单与 TLUSTY b 因子/出射谱快照的设计依据。
> 源码路径:TLUSTY 位于 `/home/fmq/program/SpectraRust/tlusty/``tlusty208.f`),
> SYNSPEC 位于 `/home/fmq/program/SpectraRust/synspec/``synspec54.f`)。
> 归档逻辑实现于 `crates/node/src/executor.rs`、`crates/common/src/runner.rs`、
> `crates/common/src/result_filter.rs`。
## 1. 背景与问题
Node 每个任务算完后,把沙盒 `work_dir/task_<id>/<name>/` 内的产物拷贝到持久归档
`result_dir/<name>/`。归档采用**白名单**策略(`is_result_worthy`),只保留有语义价值
的产物,丢弃 Tlusty/Synspec 运行时的中间工作单元(旧版 catch-all 会把它们一并搬进
归档,每个模型浪费约 2MB / 4.8MB)。
需要回答的核心问题:
1. 当前归档到底保留了哪些文件?
2. TLUSTY 的 `fort.12`b 因子)、`fort.14`(出射谱)、`fort.69`(计时日志)是否有
保存价值?—— 需以 TLUSTY 源码为准。
3. TLUSTY 出射谱(fort.14)与 SYNSPEC 输出的 `.spec` 有什么区别,是否冗余?
## 2. Node 结果归档机制(现状)
- **归档路径**`result_dir/<name>/``result_dir` 由 node 配置指定,默认为 `data/result/`)。
- **触发时机**:上报成功后(上报失败也归档,用于排错),随后清理沙盒。
- **原子写入**:先拷为 `.result.tmp.<uuid>` 再 rename,防止半截文件。
- **无 LRU 上限**:所有已算网格点产物永久保留(2026-08-02 撤销此前 200 目录上限)。
- **白名单判定**`common::result_filter::is_result_worthy(fname, model_name)`
### 2.1 保留的文件(白名单)
`model_name` 为网格点权威名,如 `t20000_g5.0_he-2_c-4_n-4_o-4`
**裸名保留**(不以 model_name 为前缀,有独立语义):
| 文件 | 语义 |
|------|------|
| `conv.json` | 收敛摘要 |
| `fort.8` | SYNSPEC 输入大气 |
| `fort.55` | SYNSPEC 控制卡 |
**科学核心** `<name>.<后缀>`
| 后缀 | 来源 | 语义 |
|------|------|------|
| `.7` | TLUSTY/SYNSPEC | 最终大气 |
| `.spec` | SYNSPEC fort.7 | 合成光谱(高分辨率) |
| `.cont` | SYNSPEC fort.17 | 连续谱 |
| `.iden` | SYNSPEC fort.12 | 谱线证认表 |
| `.log` | SYNSPEC | SYNSPEC 日志 |
| `.bfac` | TLUSTY fort.12 快照 | 最终模型 b 因子(非 LTE 偏离因子) |
| `.emflux` | TLUSTY fort.14 快照 | 最终模型出射谱(波长 Å + Fλ) |
**阶段快照** `<name>.<label>.<后缀>`label ∈ `lte`/`nc`/`nl`/`seed_nc`):
- `.5`(输入卡)、`.6`(输出日志)、`.err`(错误日志)、`.nst`(控制卡)、`.7`(该阶段大气)
**收敛诊断** `<name>.<label>_chmax*.9`**唯一保留的 .9 形态**(裸 `<name>.<label>.9`
`_chmax*.9` 内容重复,runner 源头已停止写出)。
### 2.2 丢弃的内容
- 所有 Tlusty 中间工作单元:`fort.1/2/3/10/11/13/14/18/22/42/44/50/57/69/82/95`
- `fort.84`NATOMS 崩溃缓存)、`.tmp` 残留
- 符号链接(`data``fort.19` 等共享 runtime 资源)、子目录
- 不以 `<name>.` 开头且不在裸名列表的文件、未知后缀/未知阶段标签
- `fort.12` / `fort.17`:被 runner 消费(见 §4.1)后删除
## 3. TLUSTY fort 单元语义(源码分析)
TLUSTY 每次运行到达最终迭代(`LFIN=.TRUE.`)时,经 `OUTPRI`tlusty208.f:14156,调用点
:3889)一次性写出多个单元。各单元语义与行号:
| 单元 | 内容 | 源码位置 | 判定 |
|------|------|----------|------|
| `fort.12` | **b 因子(非 LTE 偏离因子表)**:头 2I5 + 每深度 TEMP/ELEC/DENS/BFAC,格式 701/702/703 | :14344 | 科学价值高,需快照 |
| `fort.13` | 出射辐射场(FREQ, FLUX=H(ν), FH=Eddington 因子),格式 602 | :14185 | 与 fort.14 同源,未快照 |
| `fort.14` | **出射谱(波长 Å + Fλ)**,格式 614 `F15.3,1pe15.3` | :14188 | 科学价值高,需快照 |
| `fort.17` | 每深度布居数 POPUL(格式 503 | :14124 | SYNSPEC 覆盖后转存 `.cont` |
| `fort.20` | b 因子 BFAC(另一分支) | :14119 | 未保留 |
| `fort.22` | 绝对 b 因子 BFAB | :14346 | 未保留 |
| `fort.69` | **迭代计时日志**`TIMING` 子程序写 IP/MOD/TIME/DT/ROUTFORMAL SOLUTION / LINEARIZATION),格式 600 | :29937 | 纯性能诊断,丢弃 |
> 注意:`OUTPRI` 仅在最终迭代调用,故每个单元内保存的是**最后一次迭代**的最终值。
> 收敛链上最后一次 TLUSTY 运行即最终模型。
## 4. SYNSPEC 对 unit 12/14 的覆盖与丢失风险
### 4.1 单元号复用导致覆盖
TLUSTY 与 SYNSPEC **同目录串行执行**,且两个程序复用相同的单元号:
| 单元 | TLUSTY 写 | SYNSPEC 写 |
|------|-----------|-----------|
| 12 | b 因子 | 谱线证认表(synspec54.f:9719,格式 603 |
| 14 | 出射谱 | 谱线数据(synspec54.f:9852,格式 601 |
由于 TLUSTY 先跑、SYNSPEC 后跑,SYNSPEC 会把 `fort.12`/`fort.14` 覆盖。现有 runner
流程:
1. TLUSTY 写 b 因子 + 出射谱 → **被 SYNSPEC 覆盖**
2. SYNSPEC 写 `fort.7`(谱)、`fort.17`(连续谱)、`fort.12`(谱线证认表)
3. runner 将 `fort.7``.spec``fort.17``.cont``fort.12``.iden`,随后删除 `fort.12`/`fort.17`
**结论**:归档里的 `.iden` 保存的是 SYNSPEC 的谱线证认表(覆盖后的 fort.12),
**TLUSTY 的 b 因子与出射谱在覆盖前无任何快照,会被静默丢失** —— 这是真实的数据损失点。
### 4.2 快照决策
- `fort.69`:纯计时日志,**不保存**(无科学内容)。
- `fort.12`b 因子)与 `fort.14`(出射谱):**需快照**。
## 5. 实现:快照 b 因子 + 出射谱
### 5.1 快照命名与白名单
| 源文件 | 快照名 | 白名单后缀 |
|--------|--------|-----------|
| `fort.12` | `<name>.bfac` | `bfac`(加入 `SCIENCE_SUFFIXES` |
| `fort.14` | `<name>.emflux` | `emflux`(加入 `SCIENCE_SUFFIXES` |
### 5.2 插入点
`runner::run_model_with_timeout`:**收敛链循环结束后、SYNSPEC 启动前**调用
`snapshot_tlusty_outputs(&model_dir, name)`。此时 fort.12/14 是最后一次 TLUSTY 运行
(最终模型)的产物,且尚未被 SYNSPEC 覆盖。仅 SYNSPEC 场景(tlusty 未运行)下
fort.12/14 不存在,函数按文件存在性静默跳过。
### 5.3 变更文件
- `crates/common/src/runner.rs`:新增 `snapshot_tlusty_outputs()` + 调用 + 2 个测试
- `crates/common/src/result_filter.rs``SCIENCE_SUFFIXES``bfac`/`emflux` + 文档 + 测试
- `crates/node/src/executor.rs`:归档文档、`test_save_result_artifacts` 同步 + 修正
`fort.13` 注释(原误标为"NLTE 跃迁频率网格",实为出射辐射场 FREQ/FLUX/FH
## 6. TLUSTY 出射谱(fort.14vs SYNSPEC `.spec`
两者**转换公式完全相同、物理量一致**,差异在网格密度、线不透明度处理、覆盖范围与用途。
### 6.1 相同点
- 公式一字不差:`Fλ = H(ν) × ν² / c`
- TLUSTY:14188):`FLAM=FLUX(IJP)*FREQ(IJP)*FREQ(IJP)/2.997925E18`
- SYNSPEC:3365):`FLAM=FLUX(IJ)*FREQ(IJ)*FREQ(IJ)*CAS``CAS=1./2.997925D18`
- `FLUX` 都是表面出射 Eddington 通量 H(ν)erg/cm²/s/sterad/HzOUTPRI 注释明确
"precisely the second moment H(freq) at the surface")。
- 同一波长点的连续谱水平应当一致。
### 6.2 差异点
| 维度 | TLUSTY fort.14 | SYNSPEC `.spec` |
|------|---------------|-----------------|
| 网格密度 | 模型自身频率网格(NFREQ 个点,数百~数千,非均匀) | 以每条谱线为中心的细网格(`FR0=FREQ0(IL0)`SPACE/CUTOFF/DOPSTD 采样线轮廓,密度高几个量级) |
| 线不透明度 | 模型求解时的粗/近似线处理(不透明度采样/ODF/超能级),只体现最强线 | 完整线表(fort.19)逐细频点重解辐射转移,得到分辨开的真实线轮廓 |
| 波长覆盖 | 整个模型宽带频率范围 | 仅 fort.55 指定窗口(dcts 默认 14001410 Å) |
| FLUX 来源 | 模型迭代过程的自洽辐射场解(顺带用于通量守恒检查 TOTF) | 用已收敛大气在细网格重新解 RT |
| 用途/下游 | 模型副带品,快速看谱/校验 | 科学产品,注释明说 "serves as input to ROTINS"(转动/仪器展宽卷积),用于与观测对比 |
### 6.3 实际含义
两者是**同一物理量(表面 Fλ)在不同分辨率和线处理下的版本**,非两种东西。叠画时
连续谱重合,`.spec` 显示被分辨开的精细谱线轮廓,fort.14 中弱线基本不可见。归档同时
保留二者不构成冗余:`.emflux` 代表模型自身辐射场(宽带、粗,可独立校验),`.spec`
代表最终高分辨率合成谱(窄窗、细,与观测对比的产品)。
## 7. 完整归档文件清单(快照实施后)
```
result_dir/<name>/
├── conv.json
├── fort.8 # SYNSPEC 输入大气(裸名保留)
├── fort.55 # SYNSPEC 控制卡(裸名保留)
├── <name>.7 # 最终大气
├── <name>.spec # SYNSPEC 合成光谱
├── <name>.cont # SYNSPEC 连续谱
├── <name>.iden # SYNSPEC 谱线证认表
├── <name>.log # SYNSPEC 日志
├── <name>.bfac # TLUSTY 最终 b 因子(快照 fort.12
├── <name>.emflux # TLUSTY 最终出射谱(快照 fort.14
├── <name>.<label>.5 # 阶段输入卡快照
├── <name>.<label>.6 # 阶段输出日志快照
├── <name>.<label>.err # 阶段错误日志快照
├── <name>.<label>.nst # 阶段控制卡快照
├── <name>.<label>.7 # 阶段大气快照
└── <name>.<label>_chmax*.9 # 阶段收敛诊断(唯一保留的 .9)
```
## 8. 后续可选优化
- `fort.13`(出射辐射场,含 Eddington 因子 FH)未快照;如需保留,在
`snapshot_tlusty_outputs` 中加 `("fort.13", "emrad")` 并在白名单加 `emrad` 后缀即可。
- `fort.20`b 因子)、`fort.22`(绝对 b 因子)同属 b 因子家族;如需保留可在 TLUSTY
阶段结束后一并快照,但 `fort.12` 已覆盖相对 b 因子,绝对 b 因子可由 POPUL/POPLTE 重算。
+5 -5
View File
@@ -9,12 +9,12 @@
### 现象 1`nl` 阶段出现 `DIVD ... BIG` 或 `NITER` 达到上限发散
- **原因**:初猜大气与当前网格点的真实 NLTE 大气物理状态相差过大,线性化半径无法收敛。
- **排查步骤**
1. 查看节点运行目录中的 `fort.6``conv.json`
1. 查看归档的 `conv.json`(节点端在 `DCTS_RESULT_DIR/<model_name>/conv.json`,服务端在 `DCTS_SEEDS_DIR/<model_name>/conv.json`
```bash
cat results/<model_name>/conv.json
cat data/seeds/<model_name>/conv.json
```
2. 检查 `coldfail` 现场保存:冷启动失败后,系统会自动保存过程数据到 `<model>.coldfail/`
3. **解决方案**:确保 Master 的 `results/` 目录下存有相近 $T_{\text{eff}}$ / $\log g$ 的已收敛 `.7` 大气文件。系统将在下次重试时自动触发 **Seed-Stepping** 种子步进算法
2. 失败任务的各阶段输入/日志/收敛诊断(`<name>.<label>.5/.6/.err/.nst/.7`、`_chmax*.9`)经白名单归档到节点 `result_dir`,供排错;沙盒 `task_*` 在结算后清理
3. **解决方案**:确保服务端 `DCTS_SEEDS_DIR`(默认 `data/seeds`下存有相近 $T_{\text{eff}}$ / $\log g$ 的已收敛 `.7` 种子。若 `tlusty_strategies` 含 `seed_step`,调度器会在失败回退时自动注入近邻种子重试(**Seed-Stepping** 种子步进
### 现象 2`lte` 阶段报错或瞬间终止
- **原因**:输入的物理参数超出了灰色大气基本假设或基础原子数据(原子能级/光致电离截面)损坏缺失。
@@ -43,7 +43,7 @@
### 现象 3:网络抖动或后端滚动重配下提示 `向服务端上报任务 ... 结果失败`
- **原因**:在较长时效(如1-2小时)的运算完结回传一瞬间,Server 恰遇热更重启或遭遇防火墙短暂会话剔除。
- **容灾机制**:Worker 内置了超强的 8 轮指数级自适应退避长跳上报防护(跨度可自 1s 到 60s 顺次延迟,支撑 2 分钟以上的长时断裂耐受窗口);若由于连天硬件故障真正超出了总重试界限,亦可在本地非清理型数据栈(`data/work/task_{uuid}`)的目录直接调出本套算法终极收敛物并执行手工打标还原
- **容灾机制**:Worker 内置了超强的 8 轮指数级自适应退避长跳上报防护(跨度可自 1s 到 60s 顺次延迟,支撑 2 分钟以上的长时断裂耐受窗口);上报失败时产物仍会**尽力归档**到本地 `DCTS_RESULT_DIR/<model_name>/`(白名单保留科学产物),沙盒随后清理。若真正超出了总重试界限,可从 `result_dir/<model_name>/` 直接调出最终收敛物(`.7`/`.spec` 等)手工补录
---
+19
View File
@@ -7,6 +7,25 @@
> 原则:**数据已经在库里大半,缺口主要是 HTTP 暴露 + 少量持久化 + 全新前端 UI。**
> 零新增前端依赖(不引入图表库),沿用现有 ESM + 原生 DOM + 设计令牌风格。
> ## ⚠️ 实现状态(2026-08 更新,与当前 dashboard 实现的差异)
>
> 本文为**设计文档**,部分设计已被后续实现与 `task_engine_decoupling_design.md` 的
> 引擎面板设计覆盖,按现状对齐如下:
>
> - **已实现**:详情页 4 个 Tab(概览 overview / 逐点表 pointsTable / 平行集合分析
> parSets / 点详情面板 pointPanel)、进度曲线与 ETA、停滞检测横幅、CSV 导出、
> `workflow_progress_snapshots` 表、YAML 编辑器(yamlEditor,双模式/语法高亮/运行中
> 锁定/脏状态守卫)、引擎配置面板(wfEnginePanelTLUSTY/SYNSPEC 双阶段卡、enabled/
> policy/strategies 编辑、折叠跨轮询保持)。
> - **已变更(设计被覆盖)**:首页卡片操作按钮与详情页顶部控制条按钮**已全部移除**,
> 操作收敛至详情页内嵌引擎面板(见 `task_engine_decoupling_design.md §6`);Tab 3 由
> **2D 热力矩阵改为 Parallel Sets 平行集合图**(§2.2 Tab 3 描述过时);Tab 1 方法归因
> 行已移除(cold/seed 计数在点表与 parSets 中可用)。
> - **未实现**:§2.2/§2.4 的「重试失败点」按钮与 `POST .../points/retry` 端点
> (前后端均未实现)。
> - **方法归因**`imported` 不再作为独立收敛途径(导入点按 seed_step 统计),
> `success_method` 过滤白名单仅 `cold_run` / `seed_step`。
---
## 1. 现状盘点(带代码证据)