feat(all): 物理正确性五重硬门槛、输入文件结构化与 fort.55 错位修复、conv 诊断 DB 化与阶段归因修复、ORELAX 收敛修复与导入工具下线

物理正确性校验体系(common/conv_check.rs +494 行)
- 新增 5 类硬门槛:能量守恒(.6)、温度结构(.7)、emflux 积分校验(.emflux,含全 NaN 判失败)、假收敛排查(itek 轨迹首末比)、b 因子合理性(.bfac)
- runner 在 TLUSTY 阶段结束后执行全部校验,任一失败判 final_converged=false
- GridConfig 新增 8 个可配阈值,经 scheduler→executor→runner 全链路透传

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

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

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

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

文档与分析
- 新增 1305 失败点根因分析、fort.14 全 NaN 物理含义分析两份深度文档
- spectrum_correctness_analysis 两次修订标注已修复项;fetch_results.sh 修 trap RETURN 的 set -u 报错
This commit is contained in:
fmq
2026-08-09 12:09:48 +08:00
parent d16b3d3cdc
commit 43b82b1ae2
45 changed files with 6059 additions and 3184 deletions
+129 -1
View File
@@ -261,7 +261,7 @@ impl GridScheduler {
/// 读取指定工作流的 TLUSTY 物理迭代步进链(`config::GridConfig.tlusty_chain`),
/// 序列化为 JSON Value 供 TaskSpec 携带。节点 executor 反序列化为 `Vec<ChainStep>`
/// 后透传给 runner 的 custom_chain 参数,使用户在 YAML 配置的 niter/chmax/metals
/// 后透传给 runner 的 custom_chain 参数,使用户在 YAML 配置的 niter/chmax
/// 等阶段参数真正生效(此前 executor 硬编码用 default 链,忽略用户配置)。
/// 工作流未配置 tlusty_chain(空数组)→ Noneexecutor 用 default 链兜底)。
async fn get_workflow_tlusty_chain(&self, workflow_name: &str) -> Option<serde_json::Value> {
@@ -273,6 +273,19 @@ impl GridScheduler {
serde_json::to_value(&cfg.tlusty_chain).ok()
}
/// 读取指定工作流的种子热启动链(`config::GridConfig.seed_chain`),序列化为 JSON
/// Value 供 TaskSpec 携带。仅 seed_step 策略下由 executor 读取。
/// 与 `get_workflow_tlusty_chain` 对称。工作流未配置 seed_chain(空数组)→ None
///executor 用 `default_seed_chain()` 兜底)。
async fn get_workflow_seed_chain(&self, workflow_name: &str) -> Option<serde_json::Value> {
let wf = self.db.get_workflow(workflow_name).await.ok()??;
let cfg = parse_grid_config_or_warn(&wf.config_yaml, workflow_name, "seed_chain")?;
if cfg.seed_chain.is_empty() {
return None;
}
serde_json::to_value(&cfg.seed_chain).ok()
}
/// 读取指定工作流的 TLUSTY 输入文件全局参数(`config::GridConfig.tlusty_input`),
/// 序列化为 JSON Value 供 TaskSpec 携带。包含 NFREAD 频率网格、ions 能级表、
/// nst extra_keys 等不随阶段变化的参数。节点 executor 反序列化为 `TlustyInput`
@@ -286,6 +299,38 @@ impl GridScheduler {
.and_then(|t| serde_json::to_value(t).ok())
}
/// 一次性读取工作流的全部物理校验阈值(能量守恒 / 温度结构 / emflux)。
/// 统一读取避免对同一 YAML 多次解析。返回 8 元组,对应 TaskSpec 的 8 个标量字段:
/// (energy_tolerance, temp_max_factor, temp_floor, temp_ceiling, emflux_tolerance,
/// convergence_min_ratio, bfac_max, bfac_min)。
async fn get_workflow_validation_thresholds(
&self,
workflow_name: &str,
) -> Option<(
Option<f64>,
Option<f64>,
Option<f64>,
Option<f64>,
Option<f64>,
Option<f64>,
Option<f64>,
Option<f64>,
)> {
let wf = self.db.get_workflow(workflow_name).await.ok()??;
let cfg = parse_grid_config_or_warn(&wf.config_yaml, workflow_name, "validation_thresholds")?;
Some((
cfg.energy_tolerance,
cfg.temp_max_factor,
cfg.temp_floor,
cfg.temp_ceiling,
cfg.emflux_tolerance,
cfg.convergence_min_ratio,
cfg.bfac_max,
cfg.bfac_min,
))
}
/// 从策略链解析出「首个可派发」的顺位(见 docs/task_engine_decoupling_design.md §4.2)。
///
/// 判定:
@@ -404,7 +449,21 @@ impl GridScheduler {
let (tlusty_cfg, synspec_cfg) = self.get_workflow_stage_configs(workflow_name).await;
let synspec_params = self.get_workflow_synspec_params(workflow_name).await;
let tlusty_chain = self.get_workflow_tlusty_chain(workflow_name).await;
let seed_chain = self.get_workflow_seed_chain(workflow_name).await;
let tlusty_input = self.get_workflow_tlusty_input(workflow_name).await;
let (
energy_tolerance,
temp_max_factor,
temp_floor,
temp_ceiling,
emflux_tolerance,
convergence_min_ratio,
bfac_max,
bfac_min,
) = self
.get_workflow_validation_thresholds(workflow_name)
.await
.unwrap_or((None, None, None, None, None, None, None, None));
// 双阶段全关是退化配置(save_workflow 已拦截,此处兜底防御):无可执行阶段,
// 整工作流跳过派发(修复审查 #5)。
@@ -538,6 +597,7 @@ impl GridScheduler {
synspec_config: synspec_cfg.clone(),
synspec_params: synspec_params.clone(),
tlusty_chain_params: tlusty_chain.clone(),
seed_chain_params: seed_chain.clone(),
tlusty_input_params: tlusty_input.clone(),
// 显式绑定大气来源(设计 §5.2,修复审查 #3):仅 SYNSPEC-onlyTLUSTY 关闭)
// 场景需要外部大气——节点凭 atmosphere_ref(或 point_name 兜底)从本地归档
@@ -547,6 +607,14 @@ impl GridScheduler {
} else {
Some(name.clone())
},
energy_tolerance,
temp_max_factor,
temp_floor,
temp_ceiling,
emflux_tolerance,
convergence_min_ratio,
bfac_max,
bfac_min,
};
self.db.insert_task(&task_spec).await?;
@@ -834,7 +902,21 @@ impl GridScheduler {
let timeout_sec = self.get_workflow_timeout_sec(workflow_name).await;
let synspec_params = self.get_workflow_synspec_params(workflow_name).await;
let tlusty_chain = self.get_workflow_tlusty_chain(workflow_name).await;
let seed_chain = self.get_workflow_seed_chain(workflow_name).await;
let tlusty_input = self.get_workflow_tlusty_input(workflow_name).await;
let (
energy_tolerance,
temp_max_factor,
temp_floor,
temp_ceiling,
emflux_tolerance,
convergence_min_ratio,
bfac_max,
bfac_min,
) = self
.get_workflow_validation_thresholds(workflow_name)
.await
.unwrap_or((None, None, None, None, None, None, None, None));
// SYNSPEC 链回退:重试光谱合成。无邻居种子门控(大气来自目标点自身既有产物,
// 见 docs/task_engine_decoupling_design.md §5)——旧实现把 synspec 失败误归因到
@@ -874,8 +956,18 @@ impl GridScheduler {
synspec_params,
// TLUSTY 已关闭(半失败重试只重跑光谱),不执行 chain/input → None。
tlusty_chain_params: None,
seed_chain_params: None,
tlusty_input_params: None,
atmosphere_ref: Some(name.to_string()),
// 不重算大气 → 不做物理正确性校验。
energy_tolerance: None,
temp_max_factor: None,
temp_floor: None,
temp_ceiling: None,
emflux_tolerance: None,
convergence_min_ratio: None,
bfac_max: None,
bfac_min: None,
};
self.db.insert_task(&task_spec).await?;
self.db
@@ -959,8 +1051,17 @@ impl GridScheduler {
synspec_config: synspec_cfg.clone(),
synspec_params,
tlusty_chain_params: tlusty_chain.clone(),
seed_chain_params: seed_chain.clone(),
tlusty_input_params: tlusty_input.clone(),
atmosphere_ref: None,
energy_tolerance,
temp_max_factor,
temp_floor,
temp_ceiling,
emflux_tolerance,
convergence_min_ratio,
bfac_max,
bfac_min,
};
self.db.insert_task(&task_spec).await?;
@@ -1018,6 +1119,7 @@ mod tests {
logo: vec![(-2.0).into()],
},
tlusty_chain: vec![],
seed_chain: vec![],
tlusty_input: None,
synspec_input: None,
nworkers: 4,
@@ -1030,6 +1132,14 @@ mod tests {
linelist: None,
tlusty_stage: None,
synspec_stage: None,
energy_tolerance: None,
temp_max_factor: None,
temp_floor: None,
temp_ceiling: None,
emflux_tolerance: None,
convergence_min_ratio: None,
bfac_max: None,
bfac_min: None,
};
scheduler.initialize_grid(&cfg, "test_wf").await.unwrap();
@@ -1075,6 +1185,7 @@ mod tests {
logo: vec![(-2.0).into()],
},
tlusty_chain: vec![],
seed_chain: vec![],
tlusty_input: None,
synspec_input: None,
nworkers: 4,
@@ -1087,6 +1198,14 @@ mod tests {
linelist: None,
tlusty_stage: None,
synspec_stage: None,
energy_tolerance: None,
temp_max_factor: None,
temp_floor: None,
temp_ceiling: None,
emflux_tolerance: None,
convergence_min_ratio: None,
bfac_max: None,
bfac_min: None,
};
// wf_a 初始化并推入队列
@@ -1195,6 +1314,7 @@ mod tests {
logo: vec![(-2.0).into()],
},
tlusty_chain: vec![],
seed_chain: vec![],
tlusty_input: None,
synspec_input: None,
nworkers: 4,
@@ -1207,6 +1327,14 @@ mod tests {
linelist: None,
tlusty_stage: None,
synspec_stage: None,
energy_tolerance: None,
temp_max_factor: None,
temp_floor: None,
temp_ceiling: None,
emflux_tolerance: None,
convergence_min_ratio: None,
bfac_max: None,
bfac_min: None,
};
cfg
}