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:
+174
-28
@@ -1,5 +1,9 @@
|
||||
use crate::config::{ChainStep, SynspecInput, TlustyInput};
|
||||
use crate::conv_check::{atmosphere_has_nan, check_fort9, extract_failure_hint, spec_is_valid};
|
||||
use crate::conv_check::{
|
||||
atmosphere_has_nan, check_bfactor, check_convergence_trace, check_emflux_bolometric,
|
||||
check_energy_conservation, check_fort9, check_temperature_structure, extract_failure_hint,
|
||||
spec_is_valid,
|
||||
};
|
||||
use crate::embedded::RuntimePaths;
|
||||
use crate::fort55_writer::generate_fort55_content;
|
||||
use crate::gen_input5::make_input5;
|
||||
@@ -24,7 +28,6 @@ pub fn default_cold_chain() -> Vec<ChainStep> {
|
||||
niter: 0,
|
||||
chmax: None,
|
||||
itek: None,
|
||||
metals: Some("cno".to_string()),
|
||||
ichang: None,
|
||||
idlte: None,
|
||||
iacc: None,
|
||||
@@ -39,7 +42,6 @@ pub fn default_cold_chain() -> Vec<ChainStep> {
|
||||
niter: 10,
|
||||
chmax: None,
|
||||
itek: None,
|
||||
metals: Some("cno".to_string()),
|
||||
ichang: None,
|
||||
idlte: None,
|
||||
iacc: None,
|
||||
@@ -54,7 +56,6 @@ pub fn default_cold_chain() -> Vec<ChainStep> {
|
||||
niter: 100,
|
||||
chmax: None,
|
||||
itek: None,
|
||||
metals: Some("cno".to_string()),
|
||||
ichang: None,
|
||||
idlte: None,
|
||||
iacc: None,
|
||||
@@ -74,11 +75,10 @@ pub fn default_seed_chain() -> Vec<ChainStep> {
|
||||
niter: 20,
|
||||
chmax: None,
|
||||
itek: None,
|
||||
metals: Some("cno".to_string()),
|
||||
ichang: Some(0),
|
||||
idlte: None,
|
||||
iacc: None,
|
||||
orelax: None,
|
||||
orelax: Some(0.3),
|
||||
},
|
||||
ChainStep {
|
||||
label: "nl".to_string(),
|
||||
@@ -89,11 +89,10 @@ pub fn default_seed_chain() -> Vec<ChainStep> {
|
||||
niter: 100,
|
||||
chmax: None,
|
||||
itek: None,
|
||||
metals: Some("cno".to_string()),
|
||||
ichang: Some(0),
|
||||
idlte: None,
|
||||
iacc: None,
|
||||
orelax: None,
|
||||
orelax: Some(0.5),
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -222,6 +221,14 @@ impl<'a> ExecutionRunner<'a> {
|
||||
seed_atmos: Option<&Path>,
|
||||
synspec_cfg: Option<&SynspecInput>,
|
||||
tlusty_input: Option<&TlustyInput>,
|
||||
energy_tolerance: Option<f64>,
|
||||
temp_max_factor: Option<f64>,
|
||||
temp_floor: Option<f64>,
|
||||
temp_ceiling: Option<f64>,
|
||||
emflux_tolerance: Option<f64>,
|
||||
convergence_min_ratio: Option<f64>,
|
||||
bfac_max: Option<f64>,
|
||||
bfac_min: Option<f64>,
|
||||
) -> Result<ModelSummary> {
|
||||
self.run_model_with_timeout(
|
||||
params,
|
||||
@@ -235,6 +242,14 @@ impl<'a> ExecutionRunner<'a> {
|
||||
7200,
|
||||
None,
|
||||
tlusty_input,
|
||||
energy_tolerance,
|
||||
temp_max_factor,
|
||||
temp_floor,
|
||||
temp_ceiling,
|
||||
emflux_tolerance,
|
||||
convergence_min_ratio,
|
||||
bfac_max,
|
||||
bfac_min,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -262,6 +277,14 @@ impl<'a> ExecutionRunner<'a> {
|
||||
timeout_sec: u64,
|
||||
shutdown: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
|
||||
tlusty_input: Option<&TlustyInput>,
|
||||
energy_tolerance: Option<f64>,
|
||||
temp_max_factor: Option<f64>,
|
||||
temp_floor: Option<f64>,
|
||||
temp_ceiling: Option<f64>,
|
||||
emflux_tolerance: Option<f64>,
|
||||
convergence_min_ratio: Option<f64>,
|
||||
bfac_max: Option<f64>,
|
||||
bfac_min: Option<f64>,
|
||||
) -> Result<ModelSummary> {
|
||||
// `name` 取自权威的 TaskSpec.point_name(DB 的 grid_points.name 列,源精度正确),
|
||||
// 而非 params.model_name()。原因:服务端把 GridPointParams 存成 6 个 REAL 数值列,
|
||||
@@ -335,26 +358,22 @@ impl<'a> ExecutionRunner<'a> {
|
||||
}
|
||||
|
||||
let tlusty_skipped = !tlusty_enabled;
|
||||
// tlusty_input 为 None(YAML 未配 tlusty_input 块)时用默认值。
|
||||
// 默认值定义集中在 config.rs 的 TlustyInput::default(),此处不再重复维护。
|
||||
let default_input = TlustyInput::default();
|
||||
let input_cfg: &TlustyInput = tlusty_input.unwrap_or(&default_input);
|
||||
for stage_def in &chain {
|
||||
if tlusty_skipped {
|
||||
break;
|
||||
}
|
||||
let stage_t0 = Instant::now();
|
||||
let metals = stage_def.metals.as_deref().unwrap_or("cno");
|
||||
let input5_text = make_input5(
|
||||
params,
|
||||
&stage_def.lte,
|
||||
&stage_def.ltgray,
|
||||
metals,
|
||||
stage_def.ilvlin,
|
||||
tlusty_input,
|
||||
);
|
||||
let input5_text = make_input5(params, stage_def, input_cfg);
|
||||
|
||||
let input5_path = model_dir.join(format!("{}.5", name));
|
||||
tokio::fs::write(&input5_path, &input5_text).await?;
|
||||
|
||||
// Write nst file
|
||||
let nst_text = generate_nst_content(stage_def, tlusty_input);
|
||||
let nst_text = generate_nst_content(stage_def, input_cfg);
|
||||
tokio::fs::write(model_dir.join("nst"), &nst_text).await?;
|
||||
|
||||
// Prepare fort.8 for this stage
|
||||
@@ -417,6 +436,7 @@ impl<'a> ExecutionRunner<'a> {
|
||||
worst_depth: None,
|
||||
n_depths: None,
|
||||
itek_history: Vec::new(),
|
||||
conv_trace_check: None,
|
||||
};
|
||||
|
||||
if rc == 0 && fort7.is_file() {
|
||||
@@ -431,7 +451,26 @@ impl<'a> ExecutionRunner<'a> {
|
||||
stage_summary.worst_depth = Some(res.worst_depth);
|
||||
stage_summary.n_depths = Some(res.n_depths);
|
||||
// itek 全量保真(Phase 5b):逐次迭代诊断随 summary_json/conv.json 落库。
|
||||
stage_summary.itek_history = res.itek_history;
|
||||
stage_summary.itek_history = res.itek_history.clone();
|
||||
|
||||
// 假收敛排查(§2.1/§3.1):仅对 converged=true 的 stage 做。
|
||||
// Ng/Kantorovich 加速可压低 max_relc 造成数值达标但平衡未达成。
|
||||
if res.converged {
|
||||
if let Some(min_ratio) = convergence_min_ratio {
|
||||
if let Some(tc) =
|
||||
check_convergence_trace(&res.itek_history, min_ratio)
|
||||
{
|
||||
if !tc.valid {
|
||||
stage_summary.converged = false;
|
||||
stage_summary.note = Some(format!(
|
||||
"未收敛 [{}]",
|
||||
tc.error.as_deref().unwrap_or("假收敛排查失败")
|
||||
));
|
||||
}
|
||||
stage_summary.conv_trace_check = Some(tc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 漏洞5修复:发散时从 fort.6 提取求解器 STOP 行(SOLVE/SOLVES/RYBSOL)
|
||||
// 作为 note,提升归因质量。仅未收敛且无既有 note 时补(避免覆盖错误信息)。
|
||||
@@ -560,6 +599,87 @@ impl<'a> ExecutionRunner<'a> {
|
||||
final_converged = false;
|
||||
}
|
||||
|
||||
// 能量守恒硬门槛(docs/spectrum_correctness_analysis.md §1.1 第一层 #4):
|
||||
// 解析 `.6`(收敛链最后阶段 stdout,含 LFIN 时 OUTPRI 写出的能量守恒表)的
|
||||
// `(RAD+CON)/TOT` 列。任一深度偏离 1 超阈值即判失败(与 atmosphere_has_nan 同级)。
|
||||
// 此时 `<name>.6` 仍存在(行 688 的冗余清理在此之后),内容是最后阶段日志。
|
||||
// energy_tolerance=None → 跳过(非常规配置);`.6` 无能量守恒表 → 函数返回 None 跳过。
|
||||
let energy_check = if tlusty_enabled {
|
||||
energy_tolerance.and_then(|tol| {
|
||||
let fort6 = model_dir.join(format!("{}.6", name));
|
||||
check_energy_conservation(&fort6, tol)
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let mut energy_failed = false;
|
||||
if let Some(ref ec) = energy_check {
|
||||
if !ec.valid {
|
||||
final_converged = false;
|
||||
energy_failed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 温度结构边界校验(docs/spectrum_correctness_analysis.md §3.1/§4):
|
||||
// 解析 `.7`(最终大气)逐深度温度 T,表层 >max_factor×Teff 或全层越界判失败。
|
||||
// 读 final_7(此时已就位);tlusty_enabled=false 时 final_7 来自外部种子,跳过。
|
||||
let teff = params.teff.value();
|
||||
let temp_check = if tlusty_enabled {
|
||||
temp_max_factor.and_then(|mf| {
|
||||
let floor = temp_floor.unwrap_or(10.0);
|
||||
let ceiling = temp_ceiling.unwrap_or(1.0e8);
|
||||
check_temperature_structure(&final_7, teff, mf, floor, ceiling)
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let mut temp_failed = false;
|
||||
if let Some(ref tc) = temp_check {
|
||||
if !tc.valid {
|
||||
final_converged = false;
|
||||
temp_failed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// emflux bolometric 通量守恒校验(§3.2/§4):
|
||||
// 解析 `.emflux`(snapshot_tlusty_outputs 已快照),梯形积分 ∫Fλdλ 比 σTeff⁴。
|
||||
// 全 NaN(辐射转移失败)也判失败。文件缺失 → 跳过(非常规配置)。
|
||||
let emflux_check = if tlusty_enabled {
|
||||
emflux_tolerance.and_then(|tol| {
|
||||
let emflux_path = model_dir.join(format!("{}.emflux", name));
|
||||
check_emflux_bolometric(&emflux_path, teff, tol)
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let mut emflux_failed = false;
|
||||
if let Some(ref ec) = emflux_check {
|
||||
if !ec.valid {
|
||||
final_converged = false;
|
||||
emflux_failed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// b 因子合理性校验(§3.1/§6 #19):解析 `.bfac`(snapshot_tlusty_outputs 已快照),
|
||||
// 检查 NLTE 偏离因子 b 无极端值(>1e3 或 <1e-3 占比 >10% → 失败)。
|
||||
// 文件缺失/无有效 b 因子 → 跳过(纯 LTE 模型或 grey start)。
|
||||
let bfac_check = if tlusty_enabled {
|
||||
bfac_max.and_then(|mx| {
|
||||
let mn = bfac_min.unwrap_or(1.0e-3);
|
||||
let bfac_path = model_dir.join(format!("{}.bfac", name));
|
||||
check_bfactor(&bfac_path, mx, mn)
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let mut bfac_failed = false;
|
||||
if let Some(ref bc) = bfac_check {
|
||||
if !bc.valid {
|
||||
final_converged = false;
|
||||
bfac_failed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Run synspec if enabled and final .7 atmosphere exists
|
||||
let mut synspec_rc = None;
|
||||
let mut synspec_err = None;
|
||||
@@ -588,15 +708,9 @@ impl<'a> ExecutionRunner<'a> {
|
||||
let _ = tokio::fs::remove_file(&fort55_path).await;
|
||||
let _ = tokio::fs::remove_file(&fort19_path).await;
|
||||
|
||||
let default_cfg = SynspecInput {
|
||||
wstart: 1400.0,
|
||||
wend: 1410.0,
|
||||
imode: 0,
|
||||
idrv: 50,
|
||||
ifreq: 1,
|
||||
rel_cutoff: 0.0001,
|
||||
abs_cutoff: 0.01,
|
||||
};
|
||||
// synspec_cfg 为 None(工作流未配 synspec_input 块)时用默认值。
|
||||
// 默认值定义集中在 config.rs 的 SynspecInput::default(),此处不再重复维护。
|
||||
let default_cfg = SynspecInput::default();
|
||||
let fort55_text = generate_fort55_content(synspec_cfg.unwrap_or(&default_cfg));
|
||||
if let Err(e) = tokio::fs::write(&fort55_path, &fort55_text).await {
|
||||
warn!(
|
||||
@@ -725,6 +839,34 @@ impl<'a> ExecutionRunner<'a> {
|
||||
if atmo_has_nan {
|
||||
notes.push("Invalidated: atmosphere contains NaN/Inf lines".to_string());
|
||||
}
|
||||
if energy_failed {
|
||||
if let Some(ref ec) = energy_check {
|
||||
if let Some(ref err) = ec.error {
|
||||
notes.push(err.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
if temp_failed {
|
||||
if let Some(ref tc) = temp_check {
|
||||
if let Some(ref err) = tc.error {
|
||||
notes.push(err.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
if emflux_failed {
|
||||
if let Some(ref ec) = emflux_check {
|
||||
if let Some(ref err) = ec.error {
|
||||
notes.push(err.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
if bfac_failed {
|
||||
if let Some(ref bc) = bfac_check {
|
||||
if let Some(ref err) = bc.error {
|
||||
notes.push(err.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(ref err) = synspec_err {
|
||||
notes.push(format!("synspec error: {}", err));
|
||||
} else if let Some(rc) = synspec_rc {
|
||||
@@ -752,6 +894,10 @@ impl<'a> ExecutionRunner<'a> {
|
||||
synspec_error: synspec_err,
|
||||
synspec_sec,
|
||||
elapsed_sec,
|
||||
energy_check,
|
||||
temp_check,
|
||||
emflux_check,
|
||||
bfac_check,
|
||||
note,
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user