feat(all): NaN 伪收敛否决与 nl_tight 能量回退、emflux 检验 4π 修正、nst 行宽与种子边界修复、gfATO 谱线表接通与网格加密 9216 点、阶段分项统计与跳板机部署
物理修复(400 失败点归因,见 docs/failed400_nan_pseudo_convergence_2026_08_17.md): - runner: 阶段 converged 后复查 fort.7,含 NaN/Inf 即否决(fort.9 全零伪收敛, 曾致 261 点误跳过 nl_direct 回退);否决阶段不产出种子,阻断污染传播 - runner: nl_tight 回退——仅能量边际失败时以 CHMAX 收紧 10× 从自身模型续迭代, 残差降幅 ~10×;execute_tlusty_stage 抽取供主链与回退共用 - conv_check: fort.14 为 Eddington 通量 Hλ,积分需乘 4π 再比 σTeff⁴ (旧版 ratio 稳定 0.0796=1/4π,全点系统性假阳性)+ 回归测试 - nst_writer: 单行超 80 字符被 TLUSTY 静默截断,IFALI/JALI/TRAD 等从未生效; 按 75 字符自动换行 - seed_finder: Teff 容忍度改含边界 <=,相邻 5000K 档恢复互为种子 + 回归测试 谱线表与网格: - 默认线表 gfVIS99 → gfATO(全波段 18-23000Å),TaskSpec.linelist 支持工作流 级覆盖,节点按需下载(进程互斥锁防并发重复下载 238MB) - sdB_cno Teff 加密至 5000K 步长,432 → 9216 点;tlusty/synspec 静态二进制更新 统计与部署: - grid 汇总改按 tlusty_status/synspec_status 分项计数,新增 tlusty_failed/ synspec_failed/synspec_pending,前端详情页双视图适配 - deploy/fetch_results 支持跳板机 ProxyJump 与 SSH 主连接复用,fetch 新增 --force; - Docker 构建支持 CARGO_MIRROR/USE_MIRRORS 国内镜像参数;移除 tools/ 拷贝 - 新增 tlusty-synspec-test skill 与 6 篇根因分析/验证文档
This commit is contained in:
+724
-188
@@ -254,6 +254,240 @@ impl<'a> ExecutionRunner<'a> {
|
||||
.await
|
||||
}
|
||||
|
||||
/// 执行收敛链中的单个 TLUSTY 阶段(2026-08-14 从 run_model_with_timeout 的
|
||||
/// 阶段循环体抽取,供主链与 nl_direct 回退共用)。
|
||||
///
|
||||
/// 职责:写 .5/nst 输入 → 按 seed 铺 fort.8 → 运行 tlusty → fort.9 收敛诊断
|
||||
/// (含假收敛排查与 STOP 留言提取)→ 快照阶段产物(.5/.6/.err/nst/_chmax*.9)。
|
||||
///
|
||||
/// 返回 `(StepSummary, 产出的阶段种子路径)`:rc==0 且 fort.7 存在时,
|
||||
/// fort.7 被快照为 `<name>.<label>.7` 并作为返回种子路径(供下一阶段 fort.8);
|
||||
/// 否则返回 None(调用方保留上一阶段种子)。
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn execute_tlusty_stage(
|
||||
&self,
|
||||
model_dir: &std::path::Path,
|
||||
name: &str,
|
||||
params: &GridPointParams,
|
||||
stage_def: &ChainStep,
|
||||
input_cfg: &TlustyInput,
|
||||
seed: Option<&std::path::Path>,
|
||||
convergence_min_ratio: Option<f64>,
|
||||
timeout_sec: u64,
|
||||
shutdown: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
|
||||
) -> Result<(StepSummary, Option<std::path::PathBuf>)> {
|
||||
let stage_t0 = Instant::now();
|
||||
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, input_cfg);
|
||||
tokio::fs::write(model_dir.join("nst"), &nst_text).await?;
|
||||
|
||||
// Prepare fort.8 for this stage
|
||||
let fort8 = model_dir.join("fort.8");
|
||||
if stage_def.ltgray == "T" {
|
||||
if fort8.exists() {
|
||||
let _ = tokio::fs::remove_file(&fort8).await;
|
||||
}
|
||||
} else if let Some(s_path) = seed {
|
||||
if s_path.is_file() {
|
||||
if let Err(e) = tokio::fs::copy(s_path, &fort8).await {
|
||||
warn!(
|
||||
"阶段 {} 重载候选近邻推算种子模型期间发生文件复制异常: {}",
|
||||
stage_def.label, e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run tlusty.exe
|
||||
let fin = File::open(&input5_path).await?.into_std().await;
|
||||
let fout = File::create(model_dir.join(format!("{}.6", name)))
|
||||
.await?
|
||||
.into_std()
|
||||
.await;
|
||||
let ferr = File::create(model_dir.join(format!("{}.err", name)))
|
||||
.await?
|
||||
.into_std()
|
||||
.await;
|
||||
|
||||
let child = AsyncCommand::new(&self.runtime.tlusty_exe)
|
||||
.current_dir(model_dir)
|
||||
.stdin(Stdio::from(fin))
|
||||
.stdout(Stdio::from(fout))
|
||||
.stderr(Stdio::from(ferr))
|
||||
.kill_on_drop(true)
|
||||
.spawn()?;
|
||||
|
||||
let status_res = run_child_async_with_timeout(child, timeout_sec, shutdown).await;
|
||||
let rc = match status_res {
|
||||
Ok(st) => st.code().unwrap_or(-1),
|
||||
Err(e) => {
|
||||
warn!("tlusty 运行失败/超时: {}", e);
|
||||
-1
|
||||
}
|
||||
};
|
||||
|
||||
let fort9 = model_dir.join("fort.9");
|
||||
let fort7 = model_dir.join("fort.7");
|
||||
|
||||
let mut stage_summary = StepSummary {
|
||||
label: stage_def.label.clone(),
|
||||
chmax: stage_def.chmax,
|
||||
lte: stage_def.lte.clone(),
|
||||
converged: false,
|
||||
best_max_relc: None,
|
||||
elapsed_sec: stage_t0.elapsed().as_secs_f64(),
|
||||
note: None,
|
||||
last_iter: None,
|
||||
worst_depth: None,
|
||||
n_depths: None,
|
||||
itek_history: Vec::new(),
|
||||
conv_trace_check: None,
|
||||
};
|
||||
|
||||
// 产出的阶段种子:rc==0 且 fort.7 存在时快照为阶段种子文件并返回路径。
|
||||
let mut produced_seed: Option<std::path::PathBuf> = None;
|
||||
|
||||
if rc == 0 && fort7.is_file() {
|
||||
let eff_chmax = stage_def.chmax.unwrap_or(0.001);
|
||||
if fort9.is_file() {
|
||||
let res = check_fort9(&fort9, eff_chmax);
|
||||
stage_summary.converged = res.converged;
|
||||
stage_summary.best_max_relc = Some(res.max_relc);
|
||||
// 携带迭代诊断量进 conv.json(旧版在此处丢弃):
|
||||
// 迭代数/最差深度点供详情页阶段链展示收敛难度。
|
||||
stage_summary.last_iter = res.last_iter;
|
||||
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.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 时补(避免覆盖错误信息)。
|
||||
if !res.converged && stage_summary.note.is_none() {
|
||||
let fort6 = model_dir.join(format!("{}.6", name));
|
||||
if let Some(h) = extract_failure_hint(&fort6) {
|
||||
stage_summary.note = Some(format!("未收敛 [{}]", h));
|
||||
}
|
||||
}
|
||||
|
||||
// Save fort.9 snapshot
|
||||
let snap_name = format!("{}.{}_chmax{}.9", name, stage_def.label, eff_chmax);
|
||||
let _ = tokio::fs::copy(&fort9, model_dir.join(snap_name)).await;
|
||||
} else {
|
||||
// fort.9 缺失:按 stage_def.niter 区分两种场景(漏洞2进阶修复)。
|
||||
// - niter==0:合法 grey start(lte 阶段不迭代,TLUSTY 不写 fort.9)。
|
||||
// converged=true 保留 grey start 语义;best_max_relc=None 不虚构
|
||||
// (避免污染 final_max_relc/种子选择)。
|
||||
// - niter>0:异常——配了迭代却无 fort.9,通常是 TLUSTY 启动失败
|
||||
// (call quit,如 temp 越界)或 IO 异常。判 converged=false,
|
||||
// 避免把崩溃误判为收敛。此前两种场景共用无校验分支无法区分。
|
||||
if stage_def.niter == 0 {
|
||||
stage_summary.converged = true;
|
||||
stage_summary.best_max_relc = None;
|
||||
stage_summary.note = Some("NITER=0 grey start".to_string());
|
||||
} else {
|
||||
stage_summary.converged = false;
|
||||
stage_summary.best_max_relc = None;
|
||||
// 补 fort.6 失败诊断(call quit 留言),便于排查启动失败原因。
|
||||
let fort6 = model_dir.join(format!("{}.6", name));
|
||||
let hint = extract_failure_hint(&fort6);
|
||||
stage_summary.note = Some(match hint {
|
||||
Some(h) => format!(
|
||||
"stage {} 配置 NITER={} 但 fort.9 缺失 [{}]",
|
||||
stage_def.label, stage_def.niter, h
|
||||
),
|
||||
None => format!(
|
||||
"stage {} 配置 NITER={} 但 fort.9 缺失(TLUSTY 未完成迭代)",
|
||||
stage_def.label, stage_def.niter
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// NaN 伪收敛防护:TLUSTY 从 NaN 污染种子启动时会立即崩溃,fort.9 写出
|
||||
// 全 0.00E+00(NaN 参与的相对变化算不出,写出零),max_relc=0 < chmax
|
||||
// 会被误判收敛(生产实测 261 个点因此跳过 nl_direct 回退直接进门槛失败)。
|
||||
// 阶段级防线:converged=true 时复查本阶段 fort.7,含 NaN/Inf/溢出即否决,
|
||||
// 使 require_converged 中止逻辑与 nl_direct 回退得以触发;且不产出污染种子。
|
||||
if stage_summary.converged && atmosphere_has_nan(&fort7) {
|
||||
warn!(
|
||||
"阶段 {} fort.9 达标但 fort.7 含 NaN/Inf/溢出,判未收敛(NaN 伪收敛防护)",
|
||||
stage_def.label
|
||||
);
|
||||
stage_summary.converged = false;
|
||||
stage_summary.note = Some(
|
||||
"未收敛 [阶段大气含 NaN/Inf/溢出(NaN 伪收敛防护:fort.9 全零假达标)]"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
// Copy fort.7 as stage seed
|
||||
let stage_seed_path = model_dir.join(format!("{}.{}.7", name, stage_def.label));
|
||||
let _ = tokio::fs::copy(&fort7, &stage_seed_path).await;
|
||||
if stage_summary.converged {
|
||||
produced_seed = Some(stage_seed_path);
|
||||
}
|
||||
} else {
|
||||
// 漏洞5修复:fort.7 缺失分支(输入错误、temp 越界等 call quit 场景),
|
||||
// 从 fort.6 尾部提取 call quit / stop 留言补进 note,便于排查。
|
||||
let fort6 = model_dir.join(format!("{}.6", name));
|
||||
let hint = extract_failure_hint(&fort6);
|
||||
stage_summary.note = Some(match hint {
|
||||
Some(h) => format!("tlusty rc={} or missing fort.7 [{}]", rc, h),
|
||||
None => format!("tlusty rc={} or missing fort.7", rc),
|
||||
});
|
||||
}
|
||||
|
||||
// 快照本阶段的同名输入/输出文件,带阶段标签保留。
|
||||
// 背景:.5/.6/.err/nst 在每阶段用同名文件覆盖,若不快照则只有最后阶段(nl)
|
||||
// 的版本能存活到归档,nc 等中间阶段的日志/输入会丢失。失败阶段的日志对排错
|
||||
// 尤其重要,因此此处无条件(不论 rc 是否为 0)快照。
|
||||
// 命名风格与上方 .7/.9 快照一致:<name>.<label>.<后缀>(单 name,不重复)。
|
||||
// 注意:stage_def.label 由配置保证唯一(lte/nc/nl/seed_nc/nl_direct),
|
||||
// 不会与 synspec 产物冲突。
|
||||
//
|
||||
// 不快照 fort.9:上方已把 fort.9 收敛诊断存为 `<name>.<label>_chmax*.9`
|
||||
// (带 chmax 阈值语义),再快照成 `<name>.<label>.9` 会与它内容完全重复。
|
||||
// 故 .9 收敛诊断只保留 `_chmax*.9` 一份,不留重复快照。
|
||||
// (suffix, full_src_name) —— suffix 用于快照名后缀,full_src_name 用于定位源文件
|
||||
for (suffix, full_name) in [
|
||||
("5", format!("{}.5", name)),
|
||||
("6", format!("{}.6", name)),
|
||||
("err", format!("{}.err", name)),
|
||||
("nst", "nst".to_string()),
|
||||
] {
|
||||
let src = model_dir.join(&full_name);
|
||||
if src.is_file() {
|
||||
let snap = model_dir.join(format!("{}.{}.{}", name, stage_def.label, suffix));
|
||||
let _ = tokio::fs::copy(&src, &snap).await;
|
||||
}
|
||||
}
|
||||
|
||||
Ok((stage_summary, produced_seed))
|
||||
}
|
||||
|
||||
/// 阶段独立配置执行入口(见 docs/task_engine_decoupling_design.md §5)。
|
||||
///
|
||||
/// `tlusty_enabled` / `synspec_enabled` 控制各阶段是否运行:
|
||||
@@ -362,196 +596,26 @@ impl<'a> ExecutionRunner<'a> {
|
||||
// 默认值定义集中在 config.rs 的 TlustyInput::default(),此处不再重复维护。
|
||||
let default_input = TlustyInput::default();
|
||||
let input_cfg: &TlustyInput = tlusty_input.unwrap_or(&default_input);
|
||||
for stage_def in &chain {
|
||||
let mut failed_stage_idx: Option<usize> = None;
|
||||
for (stage_idx, stage_def) in chain.iter().enumerate() {
|
||||
if tlusty_skipped {
|
||||
break;
|
||||
}
|
||||
let stage_t0 = Instant::now();
|
||||
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, input_cfg);
|
||||
tokio::fs::write(model_dir.join("nst"), &nst_text).await?;
|
||||
|
||||
// Prepare fort.8 for this stage
|
||||
if stage_def.ltgray == "T" {
|
||||
if fort8.exists() {
|
||||
let _ = tokio::fs::remove_file(&fort8).await;
|
||||
}
|
||||
} else if let Some(ref s_path) = current_seed {
|
||||
if s_path.is_file() {
|
||||
if let Err(e) = tokio::fs::copy(s_path, &fort8).await {
|
||||
warn!(
|
||||
"阶段 {} 重载候选近邻推算种子模型期间发生文件复制异常: {}",
|
||||
stage_def.label, e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run tlusty.exe
|
||||
let fin = File::open(&input5_path).await?.into_std().await;
|
||||
let fout = File::create(model_dir.join(format!("{}.6", name)))
|
||||
.await?
|
||||
.into_std()
|
||||
.await;
|
||||
let ferr = File::create(model_dir.join(format!("{}.err", name)))
|
||||
.await?
|
||||
.into_std()
|
||||
.await;
|
||||
|
||||
let child = AsyncCommand::new(&self.runtime.tlusty_exe)
|
||||
.current_dir(&model_dir)
|
||||
.stdin(Stdio::from(fin))
|
||||
.stdout(Stdio::from(fout))
|
||||
.stderr(Stdio::from(ferr))
|
||||
.kill_on_drop(true)
|
||||
.spawn()?;
|
||||
|
||||
let status_res =
|
||||
run_child_async_with_timeout(child, timeout_sec, shutdown.clone()).await;
|
||||
let rc = match status_res {
|
||||
Ok(st) => st.code().unwrap_or(-1),
|
||||
Err(e) => {
|
||||
warn!("tlusty 运行失败/超时: {}", e);
|
||||
-1
|
||||
}
|
||||
};
|
||||
|
||||
let fort9 = model_dir.join("fort.9");
|
||||
let fort7 = model_dir.join("fort.7");
|
||||
|
||||
let mut stage_summary = StepSummary {
|
||||
label: stage_def.label.clone(),
|
||||
chmax: stage_def.chmax,
|
||||
lte: stage_def.lte.clone(),
|
||||
converged: false,
|
||||
best_max_relc: None,
|
||||
elapsed_sec: stage_t0.elapsed().as_secs_f64(),
|
||||
note: None,
|
||||
last_iter: None,
|
||||
worst_depth: None,
|
||||
n_depths: None,
|
||||
itek_history: Vec::new(),
|
||||
conv_trace_check: None,
|
||||
};
|
||||
|
||||
if rc == 0 && fort7.is_file() {
|
||||
let eff_chmax = stage_def.chmax.unwrap_or(0.001);
|
||||
if fort9.is_file() {
|
||||
let res = check_fort9(&fort9, eff_chmax);
|
||||
stage_summary.converged = res.converged;
|
||||
stage_summary.best_max_relc = Some(res.max_relc);
|
||||
// 携带迭代诊断量进 conv.json(旧版在此处丢弃):
|
||||
// 迭代数/最差深度点供详情页阶段链展示收敛难度。
|
||||
stage_summary.last_iter = res.last_iter;
|
||||
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.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 时补(避免覆盖错误信息)。
|
||||
if !res.converged && stage_summary.note.is_none() {
|
||||
let fort6 = model_dir.join(format!("{}.6", name));
|
||||
if let Some(h) = extract_failure_hint(&fort6) {
|
||||
stage_summary.note = Some(format!("未收敛 [{}]", h));
|
||||
}
|
||||
}
|
||||
|
||||
// Save fort.9 snapshot
|
||||
let snap_name = format!("{}.{}_chmax{}.9", name, stage_def.label, eff_chmax);
|
||||
let _ = tokio::fs::copy(&fort9, model_dir.join(snap_name)).await;
|
||||
} else {
|
||||
// fort.9 缺失:按 stage_def.niter 区分两种场景(漏洞2进阶修复)。
|
||||
// - niter==0:合法 grey start(lte 阶段不迭代,TLUSTY 不写 fort.9)。
|
||||
// converged=true 保留 grey start 语义;best_max_relc=None 不虚构
|
||||
// (避免污染 final_max_relc/种子选择)。
|
||||
// - niter>0:异常——配了迭代却无 fort.9,通常是 TLUSTY 启动失败
|
||||
// (call quit,如 temp 越界)或 IO 异常。判 converged=false,
|
||||
// 避免把崩溃误判为收敛。此前两种场景共用无校验分支无法区分。
|
||||
if stage_def.niter == 0 {
|
||||
stage_summary.converged = true;
|
||||
stage_summary.best_max_relc = None;
|
||||
stage_summary.note = Some("NITER=0 grey start".to_string());
|
||||
} else {
|
||||
stage_summary.converged = false;
|
||||
stage_summary.best_max_relc = None;
|
||||
// 补 fort.6 失败诊断(call quit 留言),便于排查启动失败原因。
|
||||
let fort6 = model_dir.join(format!("{}.6", name));
|
||||
let hint = extract_failure_hint(&fort6);
|
||||
stage_summary.note = Some(match hint {
|
||||
Some(h) => format!(
|
||||
"stage {} 配置 NITER={} 但 fort.9 缺失 [{}]",
|
||||
stage_def.label, stage_def.niter, h
|
||||
),
|
||||
None => format!(
|
||||
"stage {} 配置 NITER={} 但 fort.9 缺失(TLUSTY 未完成迭代)",
|
||||
stage_def.label, stage_def.niter
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Copy fort.7 as stage seed
|
||||
let stage_seed_path = model_dir.join(format!("{}.{}.7", name, stage_def.label));
|
||||
let _ = tokio::fs::copy(&fort7, &stage_seed_path).await;
|
||||
current_seed = Some(stage_seed_path);
|
||||
} else {
|
||||
// 漏洞5修复:fort.7 缺失分支(输入错误、temp 越界等 call quit 场景),
|
||||
// 从 fort.6 尾部提取 call quit / stop 留言补进 note,便于排查。
|
||||
let fort6 = model_dir.join(format!("{}.6", name));
|
||||
let hint = extract_failure_hint(&fort6);
|
||||
stage_summary.note = Some(match hint {
|
||||
Some(h) => format!("tlusty rc={} or missing fort.7 [{}]", rc, h),
|
||||
None => format!("tlusty rc={} or missing fort.7", rc),
|
||||
});
|
||||
}
|
||||
|
||||
// 快照本阶段的同名输入/输出文件,带阶段标签保留。
|
||||
// 背景:.5/.6/.err/nst 在每阶段用同名文件覆盖,若不快照则只有最后阶段(nl)
|
||||
// 的版本能存活到归档,nc 等中间阶段的日志/输入会丢失。失败阶段的日志对排错
|
||||
// 尤其重要,因此此处无条件(不论 rc 是否为 0)快照。
|
||||
// 命名风格与上方 .7/.9 快照一致:<name>.<label>.<后缀>(单 name,不重复)。
|
||||
// 注意:stage_def.label 由配置保证唯一(lte/nc/nl/seed_nc),不会与 synspec 产物冲突。
|
||||
//
|
||||
// 不快照 fort.9:上方 L292 已把 fort.9 收敛诊断存为 `<name>.<label>_chmax*.9`
|
||||
// (带 chmax 阈值语义),再快照成 `<name>.<label>.9` 会与它内容完全重复。
|
||||
// 故 .9 收敛诊断只保留 `_chmax*.9` 一份,不留重复快照。
|
||||
// (suffix, full_src_name) —— suffix 用于快照名后缀,full_src_name 用于定位源文件
|
||||
for (suffix, full_name) in [
|
||||
("5", format!("{}.5", name)),
|
||||
("6", format!("{}.6", name)),
|
||||
("err", format!("{}.err", name)),
|
||||
("nst", "nst".to_string()),
|
||||
] {
|
||||
let src = model_dir.join(&full_name);
|
||||
if src.is_file() {
|
||||
let snap = model_dir.join(format!("{}.{}.{}", name, stage_def.label, suffix));
|
||||
let _ = tokio::fs::copy(&src, &snap).await;
|
||||
}
|
||||
let (stage_summary, produced_seed) = self
|
||||
.execute_tlusty_stage(
|
||||
&model_dir,
|
||||
name,
|
||||
params,
|
||||
stage_def,
|
||||
input_cfg,
|
||||
current_seed.as_deref(),
|
||||
convergence_min_ratio,
|
||||
timeout_sec,
|
||||
shutdown.clone(),
|
||||
)
|
||||
.await?;
|
||||
if let Some(p) = produced_seed {
|
||||
current_seed = Some(p);
|
||||
}
|
||||
|
||||
final_chmax = stage_def.chmax;
|
||||
@@ -566,10 +630,72 @@ impl<'a> ExecutionRunner<'a> {
|
||||
"阶段 {} 要求收敛但未达标,中止后续收敛链阶段",
|
||||
stage_def.label
|
||||
);
|
||||
failed_stage_idx = Some(stage_idx);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ── nl_direct 回退(2026-08-14)──
|
||||
// 背景:种子链 seed_nc→nl 中,seed_nc 发散(Kantorovich 雪崩)时其被污染的
|
||||
// fort.7 会被无条件传给 nl 并毒死 nl;而 nl 自身从原始干净种子出发通常能
|
||||
// 直接收敛(实测 5 个最难失败点中 3 个仅靠此回退恢复,另 2 个经
|
||||
// seed_nc 预热后收敛——两条路径失败集互补,故仅作回退、不替换主链)。
|
||||
// 详见 docs/tlusty_coldstart_nc_trace_illcond_2026_08_13.md。
|
||||
// 触发条件:require_converged 阶段失败 + 失败阶段非链首(存在中间阶段
|
||||
// 污染可能)+ 存在原始种子文件。回退以 `<label>_direct` 作为独立阶段
|
||||
// 记录进 conv.json,成功则采纳其收敛结果。
|
||||
if tlusty_enabled && !final_converged {
|
||||
if let Some(idx) = failed_stage_idx {
|
||||
if idx > 0 {
|
||||
if let Some(orig_seed) = seed_atmos {
|
||||
if orig_seed.is_file() {
|
||||
let failed_label = chain[idx].label.clone();
|
||||
info!(
|
||||
"阶段 {} 未收敛:尝试 nl_direct 回退(用原始种子直接重跑该阶段)",
|
||||
failed_label
|
||||
);
|
||||
let mut direct_stage = chain[idx].clone();
|
||||
direct_stage.label = format!("{}_direct", failed_label);
|
||||
let (mut direct_summary, direct_seed) = self
|
||||
.execute_tlusty_stage(
|
||||
&model_dir,
|
||||
name,
|
||||
params,
|
||||
&direct_stage,
|
||||
input_cfg,
|
||||
Some(orig_seed),
|
||||
convergence_min_ratio,
|
||||
timeout_sec,
|
||||
shutdown.clone(),
|
||||
)
|
||||
.await?;
|
||||
if direct_summary.converged {
|
||||
info!("nl_direct 回退成功:{} 自原始种子直接收敛", failed_label);
|
||||
final_converged = true;
|
||||
final_chmax = direct_stage.chmax;
|
||||
if let Some(r) = direct_summary.best_max_relc {
|
||||
final_max_relc = Some(r);
|
||||
}
|
||||
if let Some(p) = direct_seed {
|
||||
current_seed = Some(p);
|
||||
}
|
||||
if direct_summary.note.is_none() {
|
||||
direct_summary.note = Some(
|
||||
"nl_direct 回退收敛(跳过被污染的中间阶段,用原始种子直接求解)"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
} else if direct_summary.note.is_none() {
|
||||
direct_summary.note =
|
||||
Some("nl_direct 回退未收敛".to_string());
|
||||
}
|
||||
stage_summaries.push(direct_summary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Final atmosphere file .7
|
||||
let final_7 = model_dir.join(format!("{}.7", name));
|
||||
if let Some(ref s_path) = current_seed {
|
||||
@@ -604,7 +730,7 @@ impl<'a> ExecutionRunner<'a> {
|
||||
// `(RAD+CON)/TOT` 列。任一深度偏离 1 超阈值即判失败(与 atmosphere_has_nan 同级)。
|
||||
// 此时 `<name>.6` 仍存在(行 688 的冗余清理在此之后),内容是最后阶段日志。
|
||||
// energy_tolerance=None → 跳过(非常规配置);`.6` 无能量守恒表 → 函数返回 None 跳过。
|
||||
let energy_check = if tlusty_enabled {
|
||||
let mut energy_check = if tlusty_enabled {
|
||||
energy_tolerance.and_then(|tol| {
|
||||
let fort6 = model_dir.join(format!("{}.6", name));
|
||||
check_energy_conservation(&fort6, tol)
|
||||
@@ -620,6 +746,75 @@ impl<'a> ExecutionRunner<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
// ── nl_tight 回退(2026-08-17,能量边际专用)──
|
||||
// 背景:7 个生产失败点的唯一失败项是最深层(ID=ND)能量残差 1.0–2.3% 超
|
||||
// 1% 阈值,其余门槛(emflux/温度/bfac)全过。此类残差源于 CHMAX=1e-3 停机
|
||||
// 时统计平衡未完全达成。实测(test/20260817_failed400/mg_tight):从自身
|
||||
// 最终模型续迭代、CHMAX 收紧 10×(1e-4),~19 次迭代后残差降至 0.001–0.002
|
||||
// (降幅 ~10×)。触发条件:能量门槛失败 + 最终大气存在且无 NaN + 数值收敛
|
||||
// (非发散模型)。回退以 `<label>_tight` 记录进 conv.json,成功则刷新最终
|
||||
// 大气与能量判定,并重快照 fort.12/14(bfac/emflux 检查读快照文件)。
|
||||
if tlusty_enabled && energy_failed && !atmo_has_nan && final_7.is_file() {
|
||||
if let Some(last_stage) = chain.last() {
|
||||
let tight_chmax = last_stage.chmax.map(|c| c / 10.0);
|
||||
let mut tight_stage = last_stage.clone();
|
||||
tight_stage.label = format!("{}_tight", last_stage.label);
|
||||
tight_stage.chmax = tight_chmax;
|
||||
info!(
|
||||
"能量边际回退:以 CHMAX={} 从自身最终模型续迭代 {}",
|
||||
tight_chmax.map(|c| c.to_string()).unwrap_or_default(),
|
||||
tight_stage.label
|
||||
);
|
||||
let tight_seed = final_7.clone();
|
||||
let (mut tight_summary, tight_produced) = self
|
||||
.execute_tlusty_stage(
|
||||
&model_dir,
|
||||
name,
|
||||
params,
|
||||
&tight_stage,
|
||||
input_cfg,
|
||||
Some(&tight_seed),
|
||||
convergence_min_ratio,
|
||||
timeout_sec,
|
||||
shutdown.clone(),
|
||||
)
|
||||
.await?;
|
||||
let tight_ok = tight_summary.converged;
|
||||
if tight_ok {
|
||||
tight_summary.note = Some(
|
||||
"nl_tight 回退收敛(能量边际:CHMAX 收紧 10× 从自身模型续迭代)"
|
||||
.to_string(),
|
||||
);
|
||||
if let Some(p) = tight_produced {
|
||||
// 刷新最终大气 + 出射谱快照(后续 bfac/emflux 门槛读快照文件)
|
||||
let _ = tokio::fs::copy(&p, &final_7).await;
|
||||
snapshot_tlusty_outputs(&model_dir, name).await;
|
||||
// 重新判定能量守恒:tight 阶段的 stdout 直接写 <name>.6
|
||||
//(每阶段覆盖),此处读到的是 tight 阶段的能量表。
|
||||
if let Some(tol) = energy_tolerance {
|
||||
let fort6 = model_dir.join(format!("{}.6", name));
|
||||
if fort6.is_file() {
|
||||
energy_check = check_energy_conservation(&fort6, tol);
|
||||
}
|
||||
}
|
||||
energy_failed = matches!(&energy_check, Some(ec) if !ec.valid);
|
||||
if !energy_failed {
|
||||
final_converged = true;
|
||||
final_chmax = tight_stage.chmax;
|
||||
if let Some(r) = tight_summary.best_max_relc {
|
||||
final_max_relc = Some(r);
|
||||
}
|
||||
info!("nl_tight 回退成功:能量守恒达标");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tight_summary.note =
|
||||
Some("nl_tight 回退未收敛".to_string());
|
||||
}
|
||||
stage_summaries.push(tight_summary);
|
||||
}
|
||||
}
|
||||
|
||||
// 温度结构边界校验(docs/spectrum_correctness_analysis.md §3.1/§4):
|
||||
// 解析 `.7`(最终大气)逐深度温度 T,表层 >max_factor×Teff 或全层越界判失败。
|
||||
// 读 final_7(此时已就位);tlusty_enabled=false 时 final_7 来自外部种子,跳过。
|
||||
@@ -1042,4 +1237,345 @@ mod tests {
|
||||
assert!(!model_dir.join(format!("{}.bfac", name)).exists());
|
||||
assert!(!model_dir.join(format!("{}.emflux", name)).exists());
|
||||
}
|
||||
|
||||
/// 快速单元回归(2026-08-14):nl_direct 回退接线。用假 tlusty 脚本模拟
|
||||
/// 「seed_nc 发散 → nl 失败 → 回退 nl_direct 收敛」三步,验证:
|
||||
/// 1) nl 失败后触发 <label>_direct 阶段并用原始种子重跑;
|
||||
/// 2) 回退收敛被采纳为最终结果(final 阶段链含 nl_direct 且 converged)。
|
||||
#[tokio::test]
|
||||
async fn unit_nl_direct_fallback_wiring() {
|
||||
let work = std::env::temp_dir().join(format!("dcts_unit_fallback_{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&work);
|
||||
tokio::fs::create_dir_all(&work).await.unwrap();
|
||||
// 假 tlusty:按调用序号产出 fort.9 —— #1(seed_nc) 发散、#2(nl) 失败、
|
||||
// #3(nl_direct) 收敛。fort.7 写哑内容(无 NaN 字样)。
|
||||
let fake = work.join("fake_tlusty.sh");
|
||||
let script = r#"#!/usr/bin/env bash
|
||||
n=$(cat ./call_count 2>/dev/null || echo 0); n=$((n+1)); echo $n > ./call_count
|
||||
cat /dev/stdin > /dev/null
|
||||
echo "fake model" > fort.7
|
||||
case $n in
|
||||
1|2) printf ' 1 50 1.0E-02 1.0E-02 1.0E-02 1.0E-02 1.0E+16 75 1\n' > fort.9 ;;
|
||||
3) printf ' 1 50 1.0E-02 1.0E-02 1.0E-02 1.0E-02 1.0E-05 75 1\n' > fort.9 ;;
|
||||
esac
|
||||
exit 0
|
||||
"#;
|
||||
tokio::fs::write(&fake, script).await.unwrap();
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
std::fs::set_permissions(&fake, std::fs::Permissions::from_mode(0o755)).unwrap();
|
||||
}
|
||||
let runtime = crate::embedded::RuntimePaths {
|
||||
tlusty_exe: fake.clone(),
|
||||
synspec_exe: work.join("synspec_absent"),
|
||||
data_dir: work.clone(),
|
||||
linelist: work.join("linelist_absent"),
|
||||
};
|
||||
// 原始种子:普通文件存在即可(fake 不读内容)
|
||||
let seed = work.join("orig_seed.7");
|
||||
tokio::fs::write(&seed, "orig clean seed").await.unwrap();
|
||||
|
||||
let runner = super::ExecutionRunner::new(&runtime, work.join("models"));
|
||||
let params = crate::models::GridPointParams {
|
||||
teff: crate::models::GridAxisValue::from_value(60000.0),
|
||||
logg: crate::models::GridAxisValue::from_value(5.5),
|
||||
loghe: crate::models::GridAxisValue::from_value(-2.0),
|
||||
logc: crate::models::GridAxisValue::from_value(-4.0),
|
||||
logn: crate::models::GridAxisValue::from_value(-4.0),
|
||||
logo: crate::models::GridAxisValue::from_value(-4.0),
|
||||
};
|
||||
let summary = runner
|
||||
.run_model_with_timeout(
|
||||
¶ms,
|
||||
"t60000_g5.5_he-2_c-4_n-4_o-4",
|
||||
"seed_step",
|
||||
Some(super::default_seed_chain()),
|
||||
Some(&seed),
|
||||
None,
|
||||
true,
|
||||
false,
|
||||
60,
|
||||
None,
|
||||
None, None, None, None, None, None, None, None, None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let labels: Vec<(String, bool)> = summary
|
||||
.stages
|
||||
.iter()
|
||||
.map(|s| (s.label.clone(), s.converged))
|
||||
.collect();
|
||||
assert_eq!(
|
||||
labels,
|
||||
vec![
|
||||
("seed_nc".to_string(), false),
|
||||
("nl".to_string(), false),
|
||||
("nl_direct".to_string(), true),
|
||||
],
|
||||
"应触发 nl_direct 回退并采纳其收敛"
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&work);
|
||||
}
|
||||
|
||||
/// nl_tight 回退接线回归(2026-08-17):收敛模型但能量边际失败(最深层残差
|
||||
/// 2% > 1%)→ 应以 CHMAX 收紧 10× 从自身模型续迭代,回退后能量达标。
|
||||
#[tokio::test]
|
||||
async fn unit_nl_tight_fallback_wiring() {
|
||||
let work =
|
||||
std::env::temp_dir().join(format!("dcts_unit_nltight_{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&work);
|
||||
tokio::fs::create_dir_all(&work).await.unwrap();
|
||||
// 假 tlusty:#1(nl) 数值收敛但能量表残差 0.02;#2(nl_tight) 残差 0.005。
|
||||
let fake = work.join("fake_tlusty.sh");
|
||||
let script = r#"#!/usr/bin/env bash
|
||||
n=$(cat ./call_count 2>/dev/null || echo 0); n=$((n+1)); echo $n > ./call_count
|
||||
cat /dev/stdin > /dev/null
|
||||
echo "clean model" > fort.7
|
||||
printf ' 1 50 1.0E-02 1.0E-02 1.0E-02 1.0E-02 1.0E-05 75 1\n' > fort.9
|
||||
case $n in
|
||||
1) printf ' depth TOT (RAD+CON)/TOT\n 1 1.00\n 50 1.02\n' ;;
|
||||
2) printf ' depth TOT (RAD+CON)/TOT\n 1 1.00\n 50 1.005\n' ;;
|
||||
esac
|
||||
exit 0
|
||||
"#;
|
||||
tokio::fs::write(&fake, script).await.unwrap();
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
std::fs::set_permissions(&fake, std::fs::Permissions::from_mode(0o755)).unwrap();
|
||||
}
|
||||
let runtime = crate::embedded::RuntimePaths {
|
||||
tlusty_exe: fake.clone(),
|
||||
synspec_exe: work.join("synspec_absent"),
|
||||
data_dir: work.clone(),
|
||||
linelist: work.join("linelist_absent"),
|
||||
};
|
||||
let seed = work.join("orig_seed.7");
|
||||
tokio::fs::write(&seed, "orig clean seed").await.unwrap();
|
||||
|
||||
let runner = super::ExecutionRunner::new(&runtime, work.join("models"));
|
||||
let params = crate::models::GridPointParams {
|
||||
teff: crate::models::GridAxisValue::from_value(60000.0),
|
||||
logg: crate::models::GridAxisValue::from_value(5.0),
|
||||
loghe: crate::models::GridAxisValue::from_value(-2.0),
|
||||
logc: crate::models::GridAxisValue::from_value(-2.0),
|
||||
logn: crate::models::GridAxisValue::from_value(-4.0),
|
||||
logo: crate::models::GridAxisValue::from_value(-3.0),
|
||||
};
|
||||
// 单阶段 nl 链
|
||||
let chain = vec![super::ChainStep {
|
||||
label: "nl".to_string(),
|
||||
lte: "F".to_string(),
|
||||
ltgray: "F".to_string(),
|
||||
niter: 100,
|
||||
chmax: Some(0.001),
|
||||
orelax: Some(0.5),
|
||||
ilvlin: 100,
|
||||
require_converged: true,
|
||||
itek: None,
|
||||
ichang: None,
|
||||
idlte: None,
|
||||
iacc: None,
|
||||
}];
|
||||
let summary = runner
|
||||
.run_model_with_timeout(
|
||||
¶ms,
|
||||
"t60000_g5.0_he-2_c-2_n-4_o-3",
|
||||
"cold_run",
|
||||
Some(chain),
|
||||
Some(&seed),
|
||||
None,
|
||||
true,
|
||||
false,
|
||||
60,
|
||||
None,
|
||||
None, // tlusty_input
|
||||
Some(0.01), // energy_tolerance
|
||||
None, None, None, None, None, None, None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let labels: Vec<(String, bool)> = summary
|
||||
.stages
|
||||
.iter()
|
||||
.map(|s| (s.label.clone(), s.converged))
|
||||
.collect();
|
||||
assert_eq!(
|
||||
labels,
|
||||
vec![
|
||||
("nl".to_string(), true),
|
||||
("nl_tight".to_string(), true),
|
||||
],
|
||||
"能量边际应触发 nl_tight 回退并收敛(阶段: {:?})",
|
||||
labels
|
||||
);
|
||||
let ec = summary.energy_check.as_ref().expect("应有能量检查结果");
|
||||
assert!(ec.valid, "回退后能量应达标: {:?}", ec.error);
|
||||
let _ = std::fs::remove_dir_all(&work);
|
||||
}
|
||||
|
||||
/// NaN 伪收敛防护回归(2026-08-17):TLUSTY 从 NaN 污染种子启动会立即崩溃,
|
||||
/// fort.9 全 0.00E+00 假达标(max_relc=0 < chmax)——生产实测 261 个点因此
|
||||
/// 被误判 nl 收敛、跳过 nl_direct 回退。防护后:fort.7 含 NaN 即否决 converged,
|
||||
/// 且不产出污染种子(下一阶段回退用原始种子)。
|
||||
#[tokio::test]
|
||||
async fn unit_nan_pseudo_convergence_veto() {
|
||||
let work =
|
||||
std::env::temp_dir().join(format!("dcts_unit_nanveto_{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&work);
|
||||
tokio::fs::create_dir_all(&work).await.unwrap();
|
||||
// 假 tlusty:#1(seed_nc) 写全零 fort.9(假达标)+ 含 NaN 的 fort.7;
|
||||
// #2(nl) 从原始种子正常收敛。
|
||||
let fake = work.join("fake_tlusty.sh");
|
||||
let script = r#"#!/usr/bin/env bash
|
||||
n=$(cat ./call_count 2>/dev/null || echo 0); n=$((n+1)); echo $n > ./call_count
|
||||
cat /dev/stdin > /dev/null
|
||||
case $n in
|
||||
1) printf ' 50 5.0E+03 NaN NaN NaN\n' > fort.7
|
||||
printf ' 1 50 0.00E+00 0.00E+00 0.00E+00 0.00E+00 0.00E+00 0 9\n' > fort.9 ;;
|
||||
2) echo "clean model" > fort.7
|
||||
printf ' 1 50 1.0E-02 1.0E-02 1.0E-02 1.0E-02 1.0E-05 75 1\n' > fort.9 ;;
|
||||
esac
|
||||
exit 0
|
||||
"#;
|
||||
tokio::fs::write(&fake, script).await.unwrap();
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
std::fs::set_permissions(&fake, std::fs::Permissions::from_mode(0o755)).unwrap();
|
||||
}
|
||||
let runtime = crate::embedded::RuntimePaths {
|
||||
tlusty_exe: fake.clone(),
|
||||
synspec_exe: work.join("synspec_absent"),
|
||||
data_dir: work.clone(),
|
||||
linelist: work.join("linelist_absent"),
|
||||
};
|
||||
let seed = work.join("orig_seed.7");
|
||||
tokio::fs::write(&seed, "orig clean seed").await.unwrap();
|
||||
|
||||
let runner = super::ExecutionRunner::new(&runtime, work.join("models"));
|
||||
let params = crate::models::GridPointParams {
|
||||
teff: crate::models::GridAxisValue::from_value(60000.0),
|
||||
logg: crate::models::GridAxisValue::from_value(6.0),
|
||||
loghe: crate::models::GridAxisValue::from_value(-4.0),
|
||||
logc: crate::models::GridAxisValue::from_value(-4.0),
|
||||
logn: crate::models::GridAxisValue::from_value(-4.0),
|
||||
logo: crate::models::GridAxisValue::from_value(-4.0),
|
||||
};
|
||||
let summary = runner
|
||||
.run_model_with_timeout(
|
||||
¶ms,
|
||||
"t60000_g6.0_he-4_c-4_n-4_o-4",
|
||||
"seed_step",
|
||||
Some(super::default_seed_chain()),
|
||||
Some(&seed),
|
||||
None,
|
||||
true,
|
||||
false,
|
||||
60,
|
||||
None,
|
||||
None, None, None, None, None, None, None, None, None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let labels: Vec<(String, bool)> = summary
|
||||
.stages
|
||||
.iter()
|
||||
.map(|s| (s.label.clone(), s.converged))
|
||||
.collect();
|
||||
assert_eq!(
|
||||
labels,
|
||||
vec![
|
||||
("seed_nc".to_string(), false),
|
||||
("nl".to_string(), true),
|
||||
],
|
||||
"全零 fort.9 + NaN fort.7 应被否决收敛;nl 应回退到原始种子并收敛(阶段: {:?})",
|
||||
labels
|
||||
);
|
||||
let seed_nc = summary.stages.iter().find(|s| s.label == "seed_nc").unwrap();
|
||||
assert!(
|
||||
seed_nc.note.as_deref().unwrap_or("").contains("NaN"),
|
||||
"note 应标注 NaN 伪收敛防护,实际: {:?}",
|
||||
seed_nc.note
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&work);
|
||||
}
|
||||
/// 端到端回归(2026-08-14,#[ignore]:真实运行 TLUSTY,约 15-40 分钟):
|
||||
/// nl_direct 回退。场景取自生产实测失败点 t60000_g5.5_he-2_c-4_n-4_o-4:
|
||||
/// seed_nc 发散 → 其输出毒死 nl → 回退用原始种子直接跑 nl 应收敛。
|
||||
/// 对应修复验证数据见 docs/tlusty_coldstart_nc_trace_illcond_2026_08_13.md。
|
||||
#[tokio::test]
|
||||
#[ignore = "真实运行 TLUSTY(~40min),验证修复时用 cargo test -p common nl_direct -- --ignored --nocapture"]
|
||||
async fn e2e_nl_direct_fallback_recovers_poisoned_seed_chain() {
|
||||
use crate::models::GridAxisValue;
|
||||
let manifest = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
let repo_root: &std::path::Path = manifest.parent().and_then(|p| p.parent()).unwrap();
|
||||
let seed_path = repo_root.join(
|
||||
"test/20260813_cold_nc_trace_fix/hevalidation/p4_nldirect_cno1/inputs/fort.8",
|
||||
);
|
||||
if !seed_path.is_file() {
|
||||
eprintln!("跳过:种子模型不存在({}),需先准备 p4 场景种子", seed_path.display());
|
||||
return;
|
||||
}
|
||||
let runtime = crate::embedded::RuntimePaths {
|
||||
tlusty_exe: repo_root.join("assets/tlusty_static"),
|
||||
synspec_exe: repo_root.join("assets/synspec_static"),
|
||||
data_dir: repo_root.join("assets/data"),
|
||||
linelist: repo_root.join("assets/data/gfATO.dat"),
|
||||
};
|
||||
let work = std::env::temp_dir().join(format!("dcts_e2e_nl_direct_{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&work);
|
||||
tokio::fs::create_dir_all(&work).await.unwrap();
|
||||
let runner = super::ExecutionRunner::new(&runtime, work.clone());
|
||||
|
||||
let params = GridPointParams {
|
||||
teff: GridAxisValue::from_value(60000.0),
|
||||
logg: GridAxisValue::from_value(5.5),
|
||||
loghe: GridAxisValue::from_value(-2.0),
|
||||
logc: GridAxisValue::from_value(-4.0),
|
||||
logn: GridAxisValue::from_value(-4.0),
|
||||
logo: GridAxisValue::from_value(-4.0),
|
||||
};
|
||||
let name = "t60000_g5.5_he-2_c-4_n-4_o-4";
|
||||
let chain = super::default_seed_chain();
|
||||
let summary = runner
|
||||
.run_model_with_timeout(
|
||||
¶ms,
|
||||
name,
|
||||
"seed_step",
|
||||
Some(chain),
|
||||
Some(&seed_path),
|
||||
None, // synspec_cfg
|
||||
true, // tlusty_enabled
|
||||
false, // synspec_enabled
|
||||
3600,
|
||||
None,
|
||||
None,
|
||||
None, None, None, None, None, None, None, None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// 成功判据(两条路径之一,均为修复目标):
|
||||
// a) 主链 nl 自收敛(nst 截断修复后 IFALI/IFPOPR/JALI 等真正生效,nl 可自愈),或
|
||||
// b) nl 失败触发 nl_direct 回退且回退收敛(p4 场景实测 13it/6.3e-4)。
|
||||
let labels: Vec<(&str, bool)> = summary
|
||||
.stages
|
||||
.iter()
|
||||
.map(|s| (s.label.as_str(), s.converged))
|
||||
.collect();
|
||||
eprintln!("stages: {:?}", labels);
|
||||
let main_nl_ok = summary.stages.iter().any(|s| s.label == "nl" && s.converged);
|
||||
let direct_ok = summary.stages.iter().any(|s| s.label == "nl_direct" && s.converged);
|
||||
assert!(
|
||||
main_nl_ok || direct_ok,
|
||||
"种子链应经主链 nl 或 nl_direct 回退之一收敛,实际阶段: {:?}",
|
||||
labels
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&work);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user