From f2700031cee6b932a218e8617692068fae77a04c Mon Sep 17 00:00:00 2001 From: Asfmq <2696428814@qq.com> Date: Wed, 2 Sep 2026 19:37:25 +0800 Subject: [PATCH] =?UTF-8?q?feat(all):=20seed=5Fstep=5Fstab=20=E7=A8=B3?= =?UTF-8?q?=E5=AE=9A=E5=8C=96=E7=A7=8D=E5=AD=90=E9=93=BE=E4=B8=8E=E5=90=8C?= =?UTF-8?q?=E6=97=8F=E7=A7=8D=E5=AD=90=E8=BD=AE=E6=8D=A2=E3=80=81ladder/?= =?UTF-8?q?=E8=87=AA=E9=80=82=E5=BA=94=20Teff/=E4=B8=B0=E5=BA=A6=E8=BD=B4?= =?UTF-8?q?=E5=BB=B6=E6=8B=93=E4=B8=89=E7=BA=A7=E5=9B=9E=E9=80=80=E4=B8=8E?= =?UTF-8?q?=E6=A2=AF=E7=BA=A7=E7=A7=8D=E5=AD=90=E5=85=A5=E5=BA=93=E5=A4=8D?= =?UTF-8?q?=E7=94=A8=E3=80=81=E7=83=AD=E5=90=AF=E5=8A=A8=E9=A6=96=E6=9C=AB?= =?UTF-8?q?=E6=AF=94=E8=B1=81=E5=85=8D=E3=80=81workflow=20=E5=AE=8C?= =?UTF-8?q?=E6=88=90=E7=BF=BB=E8=BD=AC=E5=9B=9E=E9=80=80=E9=93=BE=E9=98=BB?= =?UTF-8?q?=E5=A1=9E=E4=BF=AE=E5=A4=8D=E3=80=81=E5=A4=A7=E6=B0=94=E6=94=B6?= =?UTF-8?q?=E6=95=9B=E6=9D=83=E5=A8=81=E7=BB=9F=E8=AE=A1=E4=B8=8E=20M14=20?= =?UTF-8?q?=E5=9B=9E=E5=A1=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 收敛攻坚(81/400 失败点根因与实测,见 docs/failed81_cno_seed_popzer_dpsilg_2026_08_18.md): - runner: 新增 seed_step_stab 策略链——同物理族(同 Teff/logg/logHe)CNO 邻居种子 + DPSILG=3.0 λ 算子欠松弛 + POPZER=1E-10 微布居置零(联动 POPZR2/RADZER 同值 + NITZER=1),针对 20kK He 富大气 He I/II 电离前沿布居极限环 - runner: 三级链内延拓回退(主链与 nl_direct 全败后自动触发): · 固定 ladder 步进——plan_ladder_steps 按归一化间隔选轴,Δlogg≤0.25/ΔTeff≤2.5kK,≤4 步 · 自适应 Teff 延拓——步长 1250K 起、成功 ×1.5 恢复、失败二分至 25K 折叠墙,≤48 步 · C/N 丰度轴延拓——高温域(Teff>30kK)专用,严格同族种子沿 C(优先)/N 轴 0.2 dex 起步 延拓阶段 NITER 下限提至 300(慢收敛 waypoint 迭代饥饿误判修复) - runner: 稳定化多档回退(DPSILG/POPZER 三档互补,联合回收 41/65); 域门控 Teff≤30kK——高温高金属域实测旋钮致散(17 拍爆至 4e16),域外跳过 - 梯级种子持久化: 收敛中间模型登记 ladder_seeds 随上报落 server seeds 表 (任务失败也上传,合成名 _ladder 与真实网格点零冲突),簇内相邻失败点自动复用 调度与执行: - scheduler: 策略解析新增 seed_step_stab 臂——find_exact_family_seed_from_db 严格 同物理族判定(不做 global 退化,防 ladder 中间种子 ΔTeff≤5000K 误命中), 排除本点历史已用种子实现重试轮换;链在 stab 耗尽时轮换未试过同族邻居重派 - executor: seed_step_stab 补种子下载(漏列曾致 78 任务假失败,seed_nc 无 fort.8 崩溃); Teff>30kK 域外自动降级普通种子链 收敛判据: - conv_check: 热启动豁免——首拍 max_relc<1(种子已近解)时首末比 1e3 判据数学上 不可达,豁免后交五重物理硬门槛裁决(修复 nl_ladder 0.038→6e-4 物理全过被误杀); 冷启动仍受判据门控 workflow 生命周期: - workflows/tasks: 完成 flip 增加「未消费回退链」阻塞子句——failed 点策略链未耗尽 或链尾 seed_step_stab 尚有未试过同族种子时不得置 completed(修复最后活跃点 cold 失败上报抢先 flip、still_running 守卫拦截后续策略永不派发);按 failed_stage 归因 (synspec 失败行只看 synspec 链,防 stale 审计副本永久卡死)+ json_valid 脏行防护 统计与前端: - 统计新增权威口径 tlusty_converged(不按策略拆)与 seed_step_stab_converged 分项, 前端详情页色带/概览卡消费权威总数并新增稳定化青色段(修复 stab 收敛点漏计, 生产 9137/9216 差额);M14 迁移回填历史 completed 点的 tlusty_status 文档: - 新增 failed81 POPZER/DPSILG 制胜配方根因分析、Windows 节点经跳板 RDP 运维手册; failed400 增补 ladder 生产化实现与第二轮 121 残点实测矩阵 --- crates/common/src/config.rs | 11 + crates/common/src/conv_check.rs | 46 +- crates/common/src/gen_input5.rs | 2 + crates/common/src/models.rs | 45 + crates/common/src/nst_writer.rs | 46 + crates/common/src/runner.rs | 906 +++++++++++++++++- crates/common/src/summary_merge.rs | 1 + crates/node/src/executor.rs | 91 +- crates/node/src/reporter.rs | 27 +- crates/node/src/worker.rs | 2 +- crates/server/src/api/task.rs | 54 ++ crates/server/src/db/grid.rs | 17 +- crates/server/src/db/mod.rs | 481 ++++++++++ crates/server/src/db/seeds.rs | 56 ++ crates/server/src/db/tasks.rs | 40 +- crates/server/src/db/workflows.rs | 94 +- crates/server/src/migrations.rs | 76 ++ crates/server/src/scheduler.rs | 121 ++- crates/server/tests/api_tests.rs | 2 + crates/server/tests/wf_migration_isolation.rs | 1 + dashboard/src/api.js | 2 + dashboard/src/style.css | 5 + dashboard/src/views/detail/overview.js | 15 +- dashboard/src/views/detail/parSets.js | 5 +- ...ed400_nan_pseudo_convergence_2026_08_17.md | 74 +- ...led81_cno_seed_popzer_dpsilg_2026_08_18.md | 472 +++++++++ docs/remote_desktop_via_jump.md | 113 +++ docs/sdB_cno 光谱计算小结_2026_08_25.md | 279 ++++++ docs/troubleshooting.md | 4 + workflows/sdB_cno.yaml | 6 +- 30 files changed, 3045 insertions(+), 49 deletions(-) create mode 100644 docs/failed81_cno_seed_popzer_dpsilg_2026_08_18.md create mode 100644 docs/remote_desktop_via_jump.md create mode 100644 docs/sdB_cno 光谱计算小结_2026_08_25.md diff --git a/crates/common/src/config.rs b/crates/common/src/config.rs index 76bc7e1..2f19016 100644 --- a/crates/common/src/config.rs +++ b/crates/common/src/config.rs @@ -1147,6 +1147,15 @@ pub struct ChainStep { pub idlte: Option, pub iacc: Option, pub orelax: Option, + /// DPSILG(λ 算子欠松弛上限,默认不生效)。难收敛角落(如 20kK He 富大气的 + /// He I/II 电离前沿布居极限环)用 3.0 抑制——2026-08-18 实测,见 + /// docs/failed81_cno_seed_popzer_dpsilg_2026_08_18.md。 + #[serde(default)] + pub dpsilg: Option, + /// POPZER(微布居置零阈值)。设置时同时写 POPZR2/RADZER=同值 + NITZER=1, + /// 把可忽略微布居从方程中剔除(官方标准输入恒设 1E-20;难收敛角落用 1E-10)。 + #[serde(default)] + pub popzer: Option, } fn default_false_str() -> String { @@ -1991,6 +2000,8 @@ mod tests { idlte: None, iacc: None, orelax: None, + dpsilg: None, + popzer: None, }; let input5 = make_input5(&grid, &chain, &yaml_loaded); // 兜底后应含完整 ions 能级数据(而非只有终止行) diff --git a/crates/common/src/conv_check.rs b/crates/common/src/conv_check.rs index cc5c7e0..ebd38a9 100644 --- a/crates/common/src/conv_check.rs +++ b/crates/common/src/conv_check.rs @@ -719,7 +719,14 @@ pub fn check_convergence_trace( .windows(2) .all(|w| w[0] >= w[1] * 0.5); // 容忍 Ng 加速的局部反弹(×2 内) - let valid = ratio >= min_ratio; + // 热启动豁免(2026-08-18 修复):首拍 max_relc < 1 说明种子已接近收敛解, + // 首末比在此情形下数学上不可能达到 1e3 量级(首拍被钳在低位),判据失效。 + // 误杀实例:t55000_g5.0_he-4_c-3_n-4_o-1 的 nl_ladder 阶段(首 0.038→末 6e-4, + // 四项物理硬门全过)被否决为未收敛。真伪收敛的最终裁决交给五重物理硬门槛。 + const HOT_START_FIRST_MAX_RELC: f64 = 1.0; + let hot_start_exempt = first < HOT_START_FIRST_MAX_RELC; + + let valid = ratio >= min_ratio || hot_start_exempt; Some(ConvergenceTraceCheckResult { valid, first_max_relc: first, @@ -729,7 +736,13 @@ pub fn check_convergence_trace( n_iters: itek.len(), min_ratio, error: if valid { - None + if hot_start_exempt && ratio < min_ratio { + Some(format!( + "热启动豁免:首拍 max_relc {first:.1e} < {HOT_START_FIRST_MAX_RELC},首末比判据不适用" + )) + } else { + None + } } else { Some(format!( "假收敛排查失败:首末 max_relc 比 {:.1e} < {:.0e}(可能 Ng 加速伪收敛)", @@ -1509,6 +1522,35 @@ mod tests { assert!(res.error.is_some()); } + #[test] + fn test_convergence_trace_hot_start_exempt() { + // 热启动豁免(2026-08-18):首拍 < 1(种子已接近解)时首末比数学上达不到 1e3, + // 旧判据误杀(实例 t55000_g5.0_he-4_c-3_n-4_o-1 nl_ladder:0.038→6e-4,物理硬门全过)。 + let itek: Vec = (0..7) + .map(|i| IterCheck { + iter: i + 1, + max_relc: 0.038 / (1.0 + i as f64), + n_depths: 50, + }) + .collect(); + let res = check_convergence_trace(&itek, 1000.0).expect("≥3拍应判定"); + assert!(res.valid, "热启动首拍 <1 应回退到物理硬门槛裁决,不应判假收敛"); + assert!(res.ratio < 1000.0); + assert!(res.error.is_some(), "豁免时应带豁免说明"); + } + + #[test] + fn test_convergence_trace_cold_start_still_gated() { + // 冷启动(首拍 >= 1)不受豁免影响:比值不足仍判假收敛嫌疑。 + let itek: Vec = vec![ + IterCheck { iter: 1, max_relc: 5.0, n_depths: 50 }, + IterCheck { iter: 2, max_relc: 2.0, n_depths: 50 }, + IterCheck { iter: 3, max_relc: 0.05, n_depths: 50 }, + ]; + let res = check_convergence_trace(&itek, 1000.0).expect("≥3拍应判定"); + assert!(!res.valid, "冷启动首拍 ≥1 且比值 <1000 仍应失败"); + } + #[test] fn test_convergence_trace_too_short() { // 2拍:无法判定 → None diff --git a/crates/common/src/gen_input5.rs b/crates/common/src/gen_input5.rs index 669172f..3b40df5 100644 --- a/crates/common/src/gen_input5.rs +++ b/crates/common/src/gen_input5.rs @@ -232,6 +232,8 @@ mod tests { idlte: None, iacc: None, orelax: None, + dpsilg: None, + popzer: None, } } diff --git a/crates/common/src/models.rs b/crates/common/src/models.rs index bbb6803..ab6c440 100644 --- a/crates/common/src/models.rs +++ b/crates/common/src/models.rs @@ -223,6 +223,27 @@ impl GridPointParams { ) } + /// 从规范网格点名解析参数:`t60000_g5.5_he-2_c-4_n-4_o-4` → GridPointParams。 + /// 接受可选的 `_ladder` 等后缀(ladder 合成种子名)。解析失败返回 None。 + /// 用途:node 端从 seed_point_name 取种子参数(ladder 步进规划)、 + /// server 端从上传文件名取中间种子参数。 + pub fn parse_point_name(name: &str) -> Option { + let re = regex::Regex::new( + r"^t(\d+(?:\.\d+)?)_g(-?\d+(?:\.\d+)?)_he(-?\d+(?:\.\d+)?)_c(-?\d+(?:\.\d+)?)_n(-?\d+(?:\.\d+)?)_o(-?\d+(?:\.\d+)?)(?:_.*)?$", + ) + .ok()?; + let caps = re.captures(name)?; + let v = |i: usize| -> Option { caps.get(i)?.as_str().parse::().ok() }; + Some(GridPointParams { + teff: GridAxisValue::from_value(v(1)?), + logg: GridAxisValue::from_value(v(2)?), + loghe: GridAxisValue::from_value(v(3)?), + logc: GridAxisValue::from_value(v(4)?), + logn: GridAxisValue::from_value(v(5)?), + logo: GridAxisValue::from_value(v(6)?), + }) + } + /// CNO 对数丰度之和 (`logc + logn + logo`)。 /// /// 注:此数值专门用于网格调度中的 Wave 难度分级与保序分组(对数和越小代表重元素丰度越低, @@ -832,9 +853,33 @@ pub struct ModelSummary { /// `.bfac` b 因子合理性校验结果。None = 未做校验。 #[serde(default)] pub bfac_check: Option, + /// ladder 步进链的中间收敛模型(node→server 种子持久化清单)。空 = 未触发 ladder + /// 或无合格中间步。中间模型已过收敛 + NaN 否决,可安全入种子库供相邻失败点复用。 + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub ladder_seeds: Vec, pub note: Option, } +/// ladder 步进链中间种子信息。 +/// +/// `point_name` 是合成种子名(中间参数 model_name + `_ladder` 后缀),与任何真实 +/// 网格点名不冲突;server 端按其中的参数落 seeds 表(bucket 查找按 teff/logg/loghe +/// 数值列匹配,天然覆盖中间参数点)。 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LadderSeedInfo { + /// 产生该模型的阶段标签(如 `ladder_g6.25`)。 + pub label: String, + /// 合成种子名(server 端 seeds.point_name / 磁盘文件名,不含 .7 后缀)。 + pub point_name: String, + /// 中间步大气参数(teff/logg 为中间值,丰度同目标点)。 + pub teff: f64, + pub logg: f64, + pub loghe: f64, + pub logc: f64, + pub logn: f64, + pub logo: f64, +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/common/src/nst_writer.rs b/crates/common/src/nst_writer.rs index 5ad0cb3..eadf2ac 100644 --- a/crates/common/src/nst_writer.rs +++ b/crates/common/src/nst_writer.rs @@ -82,6 +82,21 @@ pub fn generate_nst_content(chain: &ChainStep, global: &TlustyInput) -> String { line2.push(format!("ICHANG={}", ichang)); written_keys.insert("ICHANG".to_string()); } + if let Some(dpsilg) = chain.dpsilg { + line2.push(format!("DPSILG={}", dpsilg)); + written_keys.insert("DPSILG".to_string()); + } + if let Some(popzer) = chain.popzer { + // POPZER 联动组:官方标准输入恒 POPZER=POPZR2=RADZER + NITZER=1 + // (tests/tlusty/*/.6 回显),缺一则置零机制不完整。 + line2.push(format!("POPZER={:.E}", popzer)); + line2.push(format!("POPZR2={:.E}", popzer)); + line2.push(format!("RADZER={:.E}", popzer)); + line2.push("NITZER=1".to_string()); + for k in ["POPZER", "POPZR2", "RADZER", "NITZER"] { + written_keys.insert(k.to_string()); + } + } line2.push(format!("IELCOR={}", n.physics.ielcor)); written_keys.insert("IELCOR".to_string()); push_wrapped(&mut out, &line2); @@ -224,6 +239,8 @@ mod tests { idlte: None, iacc: None, orelax: None, + dpsilg: None, + popzer: None, } } @@ -331,6 +348,35 @@ mod tests { assert!(!content.contains("FRLMIN"), "FRLMIN=0 不应写出"); } + /// DPSILG/POPZER 稳定化旋钮(2026-08-18,seed_step_stab 策略): + /// dpsilg → DPSILG=;popzer → POPZER/POPZR2/RADZER 三键同值 + NITZER=1 + /// (官方标准输入联动组),且行宽仍受 75 字符保护(自动换行)。 + #[test] + fn test_nst_dpsilg_popzer_stabilization() { + let mut chain = chain_nc(); + chain.dpsilg = Some(3.0); + chain.popzer = Some(1e-10); + let content = generate_nst_content(&chain, &TlustyInput::default()); + assert!(content.contains("DPSILG=3"), "DPSILG 应写出: {}", content); + assert!(content.contains("POPZER=1E-10"), "POPZER 应以 Fortran E 格式写出"); + assert!(content.contains("POPZR2=1E-10")); + assert!(content.contains("RADZER=1E-10")); + assert!(content.contains("NITZER=1"), "POPZER 联动 NITZER=1"); + for (i, line) in content.lines().enumerate() { + assert!( + line.chars().count() <= 75, + "nst 第 {} 行超宽({} 字符): {}", + i + 1, + line.chars().count(), + line + ); + } + // 默认(None)不写出,保持字节级兼容 + let plain = generate_nst_content(&chain_nc(), &TlustyInput::default()); + assert!(!plain.contains("DPSILG")); + assert!(!plain.contains("POPZER")); + } + /// fmt_real 边界:-0.0 归一为 "0."(审查 Nit-1)。 #[test] fn test_fmt_real_negative_zero() { diff --git a/crates/common/src/runner.rs b/crates/common/src/runner.rs index 8cb6d38..f3d0982 100644 --- a/crates/common/src/runner.rs +++ b/crates/common/src/runner.rs @@ -17,6 +17,15 @@ use tokio::fs::File; use tokio::process::Command as AsyncCommand; use tracing::{info, warn}; +/// 阶梯/延拓阶段(Teff 自适应延拓、固定梯、丰度轴延拓)的迭代下限。 +/// +/// 这些阶段克隆自 nl 步(NITER=100),而慢收敛 waypoint 常在 chmax=0.001 门槛前 +/// 耗尽迭代——生产实证(2026-08-21,t60000_g5.0_he-4_c-4_n-4_o-1):N 轴延拓 +/// 在 n=-3.53 处 best=0.002/0.005 卡满 100 迭代且仍在单调下降(迭代饥饿而非 +/// 发散),二分触底 0.025 dex 后被误判为轴折叠。TLUSTY 收敛即停,本上限只给 +/// 慢阶段 3× 余量,不影响快阶段;真发散步会提前 STOP in SOLVE,也不受损。 +const LADDER_STAGE_NITER: i32 = 300; + pub fn default_cold_chain() -> Vec { vec![ ChainStep { @@ -32,6 +41,8 @@ pub fn default_cold_chain() -> Vec { idlte: None, iacc: None, orelax: None, + dpsilg: None, + popzer: None, }, ChainStep { label: "nc".to_string(), @@ -46,6 +57,8 @@ pub fn default_cold_chain() -> Vec { idlte: None, iacc: None, orelax: None, + dpsilg: None, + popzer: None, }, ChainStep { label: "nl".to_string(), @@ -60,6 +73,8 @@ pub fn default_cold_chain() -> Vec { idlte: None, iacc: None, orelax: None, + dpsilg: None, + popzer: None, }, ] } @@ -79,6 +94,8 @@ pub fn default_seed_chain() -> Vec { idlte: None, iacc: None, orelax: Some(0.3), + dpsilg: None, + popzer: None, }, ChainStep { label: "nl".to_string(), @@ -93,23 +110,92 @@ pub fn default_seed_chain() -> Vec { idlte: None, iacc: None, orelax: Some(0.5), + dpsilg: None, + popzer: None, }, ] } +/// 稳定化种子步进链(策略 `seed_step_stab`)。 +/// +/// 面向难收敛角落(2026-08-18 实测 20kK He 富大气 He I/II 电离前沿布居极限环): +/// 同物理族(同 Teff/logg/logHe)不同 CNO 的已收敛邻居种子 + POPZER 微布居置零 + +/// DPSILG λ 算子欠松弛。代表点 t20000_g6.5_he2_c-4_n-3_o-4 其余配置组合全部发散, +/// 本配方收敛且五重物理硬门全过(emflux 1.0013、首末比 4.5e5)。 +pub fn default_seed_stab_chain() -> Vec { + default_seed_chain() + .into_iter() + .map(|mut s| { + s.dpsilg = Some(3.0); + s.popzer = Some(1e-10); + s + }) + .collect() +} + /// 按当前策略选默认执行链(`custom_chain` 为 None/空时的兜底)。 /// -/// Phase 6(P8)起取代废弃的 task_type 匹配:`"seed_step"` → 种子热启动链,其余策略 -/// (`cold_run` 等)→ 冷启动链。executor 现优先使用 TaskSpec.tlusty_chain_params +/// Phase 6(P8)起取代废弃的 task_type 匹配:`"seed_step"` → 种子热启动链, +/// `"seed_step_stab"` → 稳定化种子链(POPZER+DPSILG),其余策略(`cold_run` 等) +/// → 冷启动链。executor 现优先使用 TaskSpec.tlusty_chain_params /// (用户 YAML `tlusty_chain:` 配置),None/空才回退本函数的默认链。 pub fn default_chain_for_strategy(current_strategy: &str) -> Vec { if current_strategy == "seed_step" { default_seed_chain() + } else if current_strategy == "seed_step_stab" { + default_seed_stab_chain() } else { default_cold_chain() } } +/// ladder 步进规划(纯函数):给定种子参数与目标参数,产出中间步坐标。 +/// +/// 规则(2026-08-17 实测验证:test/20260817_failed400/ladder_*): +/// - 只沿与种子差值更大的轴步进(归一化:Δlogg/0.25 vs Δteff/2500); +/// - 步长上限 Δlogg=0.25、ΔTeff=2500K,中间步数 ≤4; +/// - 间隔已在单步收敛域内(Δlogg≤0.25 且 ΔTeff≤2500)→ 返回空(无需 ladder, +/// 该场景本就不该触发); +/// - 均匀切分:n = ceil(归一化间隔),每步走 gap/n。 +/// +/// 返回 (teff, logg, label) 列表,label 如 `ladder_g6.25` / `ladder_t57.5k`。 +/// 目标步不在其中(由调用方用原 ChainStep 跑目标参数)。 +pub fn plan_ladder_steps( + from_teff: f64, + from_logg: f64, + to_teff: f64, + to_logg: f64, +) -> Vec<(f64, f64, String)> { + let d_logg = (to_logg - from_logg).abs(); + let d_teff = (to_teff - from_teff).abs(); + // 归一化到"单步上限"的单位数 + let n_logg = (d_logg / 0.25).ceil() as usize; + let n_teff = (d_teff / 2500.0).ceil() as usize; + // 双轴都在单步收敛域内(含恰好等于上限)→ 无需 ladder(direct nl 即可覆盖) + if n_logg <= 1 && n_teff <= 1 { + return Vec::new(); + } + // 轴选择:需要更多步的轴(间隔更远 = 收敛域外的主因) + let use_logg = n_logg >= n_teff; + let n = if use_logg { n_logg } else { n_teff }.clamp(1, 4); + (1..=n) + .map(|k| { + let f = k as f64 / n as f64; + let (t, g) = if use_logg { + (to_teff, from_logg + f * (to_logg - from_logg)) + } else { + (from_teff + f * (to_teff - from_teff), to_logg) + }; + let label = if use_logg { + format!("ladder_g{}", (g * 100.0).round() / 100.0) + } else { + format!("ladder_t{}k", (t / 1000.0 * 10.0).round() / 10.0) + }; + (t, g, label) + }) + .collect() +} + /// 运行子进程,带超时与优雅退出(shutdown)感知。 /// /// 三种终止路径: @@ -219,6 +305,7 @@ impl<'a> ExecutionRunner<'a> { current_strategy: &str, custom_chain: Option>, seed_atmos: Option<&Path>, + seed_params: Option<&GridPointParams>, synspec_cfg: Option<&SynspecInput>, tlusty_input: Option<&TlustyInput>, energy_tolerance: Option, @@ -236,6 +323,7 @@ impl<'a> ExecutionRunner<'a> { current_strategy, custom_chain, seed_atmos, + seed_params, synspec_cfg, true, true, @@ -505,6 +593,7 @@ impl<'a> ExecutionRunner<'a> { current_strategy: &str, custom_chain: Option>, seed_atmos: Option<&Path>, + seed_params: Option<&crate::models::GridPointParams>, synspec_cfg: Option<&SynspecInput>, tlusty_enabled: bool, synspec_enabled: bool, @@ -581,6 +670,8 @@ impl<'a> ExecutionRunner<'a> { let mut final_converged = false; let mut final_chmax: Option = None; let mut final_max_relc: Option = None; + // ladder 中间梯级(收敛 + 无 NaN 双过才登记),随 ModelSummary 上报持久化 + let mut ladder_seeds: Vec = Vec::new(); // 阶段独立配置(见 docs/task_engine_decoupling_design.md §5): // TLUSTY 关闭时跳过整个 chain 循环——current_seed 直接作为 final_7 来源, @@ -696,6 +787,482 @@ impl<'a> ExecutionRunner<'a> { } } + // ── ladder 步进回退(2026-08-17,真发散硬核专用)── + // 背景:~131 个生产失败点从任何网格邻居种子直接求解都在 Newton 收敛域外 + // (首 6-13 迭代即 1e16 STOP,ORELAX 无效)。实测(ladder_60k_he4 / + // ladder_20k_he2):把种子→目标的参数间隔切成 ≤4 段(Δlogg≤0.25 或 + // ΔTeff≤2.5kK)逐步热启动,两个最难簇代表点均完全收敛(物理三项过 + + // emflux 达标)。触发条件:此前全部失败 + 有原始种子 + 知道种子参数 + // (ladder 规划需要种子与目标的差值)。 + // 每个中间步收敛即登记进 ladder_seeds(node→server 持久化为可复用梯级, + // 簇内相邻失败点可共享),链在中途断掉已完成的梯级仍有价值。 + if tlusty_enabled && !final_converged { + if let (Some(orig_seed), Some(sp)) = (seed_atmos, seed_params) { + if orig_seed.is_file() { + if let Some(last_stage) = chain.last() { + let steps = plan_ladder_steps( + sp.teff.value(), + sp.logg.value(), + params.teff.value(), + params.logg.value(), + ); + // 2026-08-21 自适应 Teff 延拓(60k/g5.0 攻坚,10/13 点唯一 + // 制胜路径,docs/failed81_...md §十一):Teff 轴为主且间隔 + // 超一档时,固定 ≤2500K 均分档在 58–59.5k 折叠墙前全灭 + // (实测一步 2500K 必发散、二分到 25–150K 才能穿过)。 + // 改为自适应步长控制:初始 1250K,成功 ×1.5(上限 1250K) + // 恢复、失败二分(下限 25K),最多 48 步。logg 轴与短间隔 + // 维持原固定档(logg 轴未经自适应验证)。 + let d_teff_gap = + (params.teff.value() - sp.teff.value()).abs(); + let teff_axis_adaptive = d_teff_gap > 2500.0 && { + let n_logg = ((params.logg.value() - sp.logg.value()).abs() + / 0.25) + .ceil() as usize; + let n_teff = (d_teff_gap / 2500.0).ceil() as usize; + n_teff > n_logg + }; + if teff_axis_adaptive { + info!( + "ladder 回退(自适应 Teff 延拓):种子 t{}/g{} → 目标 t{}/g{},步长 1250K 起、失败二分至 25K", + sp.teff.value(), + sp.logg.value(), + params.teff.value(), + params.logg.value() + ); + let mut ladder_seed_now: Option = + Some(orig_seed.to_path_buf()); + let teff_target = params.teff.value(); + let mut t_now = sp.teff.value(); + let mut d_t = 1250.0_f64; + let mut steps_done = 0usize; + const MAX_WALK_STAGES: usize = 48; + const MIN_DT: f64 = 25.0; + while (teff_target - t_now).abs() > 1.0 + && steps_done < MAX_WALK_STAGES + { + let t_next = if teff_target > t_now { + (t_now + d_t).min(teff_target) + } else { + (t_now - d_t).max(teff_target) + }; + let label = format!( + "ladder_t{:.2}k", + (t_next / 1000.0 * 100.0).round() / 100.0 + ); + let step_params = crate::models::GridPointParams { + teff: crate::models::GridAxisValue::from_value(t_next), + logg: params.logg.clone(), + loghe: params.loghe.clone(), + logc: params.logc.clone(), + logn: params.logn.clone(), + logo: params.logo.clone(), + }; + let mut step_def = last_stage.clone(); + step_def.label = label.clone(); + step_def.require_converged = true; + step_def.niter = step_def.niter.max(LADDER_STAGE_NITER); + let (step_summary, step_produced) = self + .execute_tlusty_stage( + &model_dir, + name, + &step_params, + &step_def, + input_cfg, + ladder_seed_now.as_deref(), + convergence_min_ratio, + timeout_sec, + shutdown.clone(), + ) + .await?; + steps_done += 1; + let ok = step_summary.converged; + if ok { + if let Some(p) = step_produced { + ladder_seeds.push(crate::models::LadderSeedInfo { + label: label.clone(), + point_name: format!( + "{}_ladder", + step_params.model_name() + ), + teff: t_next, + logg: params.logg.value(), + loghe: params.loghe.value(), + logc: params.logc.value(), + logn: params.logn.value(), + logo: params.logo.value(), + }); + ladder_seed_now = Some(p); + } + t_now = t_next; + d_t = (d_t * 1.5).min(1250.0); + } else { + // 失败步产物不传递(沿用上一收敛态),步长二分; + // 低于下限视为折叠墙,中止链。 + d_t /= 2.0; + if d_t < MIN_DT { + warn!( + "自适应 Teff 延拓在 {} 处遇到折叠墙(步长 <{MIN_DT}K 仍失败)", + label + ); + stage_summaries.push(step_summary); + break; + } + } + stage_summaries.push(step_summary); + } + // 延拓末步通常已 clamp 到目标参数(step_def 即 chain.last() + // 的 nl 定义,与目标一致)→ 直接采纳,不再跑冗余的 nl_ladder + // 同参数重跑(审查修复 2026-08-21)。仅当中途断链 + // (步数上限/折叠墙)时保持未收敛,交给后续回退顺位。 + if !final_converged + && (teff_target - t_now).abs() <= 1.0 + && stage_summaries.last().map(|s| s.converged).unwrap_or(false) + && ladder_seed_now.is_some() + && ladder_seed_now.as_deref() != Some(orig_seed) + { + final_converged = true; + final_chmax = last_stage.chmax; + if let Some(s) = stage_summaries.last() { + if let Some(r) = s.best_max_relc { + final_max_relc = Some(r); + } + if s.label == "nl_ladder" { + // 不可能:本分支不跑 nl_ladder;占位防误标 + } + } + current_seed = ladder_seed_now.clone(); + info!("自适应 Teff 延拓成功:末步已达目标并收敛"); + } + } else if !steps.is_empty() { + info!( + "ladder 回退:{} 步中间参数步进(种子 t{}/g{} → 目标 t{}/g{})", + steps.len(), + sp.teff.value(), + sp.logg.value(), + params.teff.value(), + params.logg.value() + ); + let mut ladder_seed_now: Option = + Some(orig_seed.to_path_buf()); + for (t, g, label) in steps { + let step_params = crate::models::GridPointParams { + teff: crate::models::GridAxisValue::from_value(t), + logg: crate::models::GridAxisValue::from_value(g), + loghe: params.loghe.clone(), + logc: params.logc.clone(), + logn: params.logn.clone(), + logo: params.logo.clone(), + }; + let mut step_def = last_stage.clone(); + step_def.label = label.clone(); + step_def.require_converged = true; + step_def.niter = step_def.niter.max(LADDER_STAGE_NITER); + let (step_summary, step_produced) = self + .execute_tlusty_stage( + &model_dir, + name, + &step_params, + &step_def, + input_cfg, + ladder_seed_now.as_deref(), + convergence_min_ratio, + timeout_sec, + shutdown.clone(), + ) + .await?; + let ok = step_summary.converged; + if let Some(p) = step_produced { + // 收敛 + NaN 否决双过(execute_tlusty_stage 保证 + // converged ⇒ fort.7 干净)→ 登记为可复用梯级 + if ok { + ladder_seeds.push(crate::models::LadderSeedInfo { + label: label.clone(), + point_name: format!( + "{}_ladder", + step_params.model_name() + ), + teff: t, + logg: g, + loghe: params.loghe.value(), + logc: params.logc.value(), + logn: params.logn.value(), + logo: params.logo.value(), + }); + } + ladder_seed_now = Some(p); + } + stage_summaries.push(step_summary); + if !ok { + warn!("ladder 步进在 {} 处未收敛,中止步进链", label); + break; + } + } + // 全部中间步通过 → 用目标参数跑最终步 + if !final_converged + && stage_summaries.last().map(|s| s.converged).unwrap_or(false) + && ladder_seed_now.is_some() + && ladder_seed_now.as_deref() != Some(orig_seed) + { + let mut final_step = last_stage.clone(); + final_step.label = "nl_ladder".to_string(); + final_step.require_converged = true; + // 与中间梯级同口径(LADDER_STAGE_NITER):末步是从最后 + // 梯级到目标的逼近,越近目标越慢收敛——恰恰是最可能 + // 迭代饥饿的一步(2026-08-22 审查补充)。 + final_step.niter = final_step.niter.max(LADDER_STAGE_NITER); + let (mut final_summary, final_produced) = self + .execute_tlusty_stage( + &model_dir, + name, + params, + &final_step, + input_cfg, + ladder_seed_now.as_deref(), + convergence_min_ratio, + timeout_sec, + shutdown.clone(), + ) + .await?; + if final_summary.converged { + final_summary.note = Some( + "nl_ladder 回退收敛(连续步进链最终步)".to_string(), + ); + final_converged = true; + final_chmax = final_step.chmax; + if let Some(r) = final_summary.best_max_relc { + final_max_relc = Some(r); + } + if let Some(p) = final_produced { + current_seed = Some(p); + } + info!("nl_ladder 回退成功:目标点经步进链收敛"); + } else if final_summary.note.is_none() { + final_summary.note = + Some("nl_ladder 回退未收敛".to_string()); + } + stage_summaries.push(final_summary); + } + } + } + } + } + } + + // ── 稳定化多档回退(2026-08-18/19)── + // 背景:20kK He 富大气布居极限环对 (DPSILG, POPZER) 组合逐点敏感,实测 + // (test/20260818_failed81/full20k* 全 65 点批量)三档互补,联合回收 41/65: + // 一档 DPSILG=3.0/POPZER=1E-10(17 点,seed_step_stab 策略链自带) + // 二档 DPSILG=2.0/POPZER=3E-11(+14 点) + // 三档 DPSILG=2.0/POPZER=1E-11(+10 点,含全部 g5.5) + // 收敛点全部通过温度结构物理判据。此回退在全部常规路径失败后逐档自原始 + // 种子重跑最后的 require_converged 阶段,任一档收敛即采纳。 + // 详见 docs/failed81_cno_seed_popzer_dpsilg_2026_08_18.md。 + const STAB_TIERS: [(f64, f64); 3] = [(2.0, 3e-11), (2.0, 1e-11), (2.5, 1e-11)]; + // 2026-08-21 域门控(60k/g5.0 攻坚实测,docs/failed81_...md §十一): + // DPSILG/POPZER 族旋钮仅在低温 He 富域(Teff≤30kK)有效;60k 高金属域 + // 实测自复现收敛种子加档后 17 拍爆到 4e16(致散而非稳定)。域外跳过 + // 全部档位,避免白跑与二次伤害。 + if tlusty_enabled && !final_converged && params.teff.value() <= 30000.0 { + if let Some(idx) = failed_stage_idx { + if let Some(orig_seed) = seed_atmos { + if orig_seed.is_file() { + for (tier_no, (dg, pz)) in STAB_TIERS.iter().enumerate() { + let mut tier_stage = chain[idx].clone(); + // 链自带一档参数时跳过同档重跑 + if tier_stage.dpsilg == Some(*dg) + && tier_stage.popzer == Some(*pz) + { + continue; + } + tier_stage.label = format!("nl_stab{}", tier_no + 2); + tier_stage.dpsilg = Some(*dg); + tier_stage.popzer = Some(*pz); + info!( + "尝试 {} 回退:DPSILG={}/POPZER={:.0E} 自原始种子重跑 {}", + tier_stage.label, + dg, + pz, + chain[idx].label + ); + let (mut tier_summary, tier_seed) = self + .execute_tlusty_stage( + &model_dir, + name, + params, + &tier_stage, + input_cfg, + Some(orig_seed), + convergence_min_ratio, + timeout_sec, + shutdown.clone(), + ) + .await?; + if tier_summary.converged { + info!("{} 回退成功:稳定化参数收敛", tier_stage.label); + final_converged = true; + final_chmax = tier_stage.chmax; + if let Some(r) = tier_summary.best_max_relc { + final_max_relc = Some(r); + } + if let Some(p) = tier_seed { + current_seed = Some(p); + } + if tier_summary.note.is_none() { + tier_summary.note = Some(format!( + "{} 回退收敛(DPSILG={}/POPZER={:.0E} 稳定化)", + tier_stage.label, dg, pz + )); + } + stage_summaries.push(tier_summary); + break; + } else if tier_summary.note.is_none() { + tier_summary.note = + Some(format!("{} 回退未收敛", tier_stage.label)); + } + stage_summaries.push(tier_summary); + } + } + } + } + } + + // ── 丰度轴延拓回退(2026-08-21,60k/g5.0 折叠角攻坚)── + // 背景:60k/g5.0 高金属角(o-1 × 高 C+N)三点上,Teff 轴(自适应二分至 25K)、 + // logg 轴、O 丰度轴全部在目标前折叠,但 **C 或 N 丰度轴延拓全部走通** + // (test/20260820_residual27/p60/*.ax_*.log,3/3 点物理收敛)——静力解存在, + // 只是可达方向在 C/N。配方:同 (Teff,logg,logHe) 的 CNO 邻居收敛种子 + // (seed_step_stab 的 exact_family 种子),沿 C 轴(3/3 验证,优先)或 + // N 轴自适应小步爬向目标丰度:初始 0.2 dex,成功 ×1.4(上限 0.25)、 + // 失败二分(下限 0.025 dex)、每轴最多 40 步;失败步产物不传递。 + // 与稳定化域门控互补:本回退仅高温域(Teff>30kK)启用(低温域由 + // seed_step_stab 稳定化档覆盖,见上方域门控注释)。 + if tlusty_enabled && !final_converged && params.teff.value() > 30000.0 { + if let (Some(orig_seed), Some(sp), Some(last_stage)) = + (seed_atmos, seed_params, chain.last()) + { + // 仅严格同物理族种子(同 Teff/logg/logHe、仅 CNO 不同)——延拓前提 + // 是结构与背景大气已处于目标 T/g/He 的解分支附近。 + let same_family = (sp.teff.value() - params.teff.value()).abs() < 1e-9 + && (sp.logg.value() - params.logg.value()).abs() < 1e-9 + && (sp.loghe.value() - params.loghe.value()).abs() < 1e-9; + let cno_differs = (sp.logc.value() - params.logc.value()).abs() > 1e-9 + || (sp.logn.value() - params.logn.value()).abs() > 1e-9 + || (sp.logo.value() - params.logo.value()).abs() > 1e-9; + if orig_seed.is_file() && same_family && cno_differs { + // 轴顺序:C 优先(3/3 验证),N 兜底(he-2 验证) + for (axis_name, axis_from, axis_to) in [ + ("c", sp.logc.value(), params.logc.value()), + ("n", sp.logn.value(), params.logn.value()), + ] { + if (axis_to - axis_from).abs() < 1e-9 || final_converged { + continue; + } + info!( + "丰度轴延拓回退:沿 {axis_name} 轴 {axis_from} → {axis_to}(种子 {} → 目标 {})", + sp.model_name(), + params.model_name() + ); + let mut seed_now: Option = Some(orig_seed.to_path_buf()); + let mut v_now = axis_from; + let mut d_v = 0.2_f64; + let mut steps_done = 0usize; + const MAX_AX_STAGES: usize = 40; + const MIN_DV: f64 = 0.025; + while (axis_to - v_now).abs() > 1e-6 && steps_done < MAX_AX_STAGES { + let v_next = if axis_to > v_now { + (v_now + d_v).min(axis_to) + } else { + (v_now - d_v).max(axis_to) + }; + let label = format!("axlad_{}{:.3}", axis_name, v_next); + let step_params = crate::models::GridPointParams { + teff: params.teff.clone(), + logg: params.logg.clone(), + loghe: params.loghe.clone(), + logc: if axis_name == "c" { + crate::models::GridAxisValue::from_value(v_next) + } else { + params.logc.clone() + }, + logn: if axis_name == "n" { + crate::models::GridAxisValue::from_value(v_next) + } else { + params.logn.clone() + }, + logo: params.logo.clone(), + }; + let mut step_def = last_stage.clone(); + step_def.label = label.clone(); + step_def.require_converged = true; + step_def.niter = step_def.niter.max(LADDER_STAGE_NITER); + let (step_summary, step_produced) = self + .execute_tlusty_stage( + &model_dir, + name, + &step_params, + &step_def, + input_cfg, + seed_now.as_deref(), + convergence_min_ratio, + timeout_sec, + shutdown.clone(), + ) + .await?; + steps_done += 1; + if step_summary.converged { + if let Some(p) = step_produced { + ladder_seeds.push(crate::models::LadderSeedInfo { + label: label.clone(), + point_name: format!("{}_ladder", step_params.model_name()), + teff: step_params.teff.value(), + logg: step_params.logg.value(), + loghe: step_params.loghe.value(), + logc: step_params.logc.value(), + logn: step_params.logn.value(), + logo: step_params.logo.value(), + }); + seed_now = Some(p); + } + v_now = v_next; + d_v = (d_v * 1.4).min(0.25); + } else { + d_v /= 2.0; + if d_v < MIN_DV { + warn!( + "丰度轴 {axis_name} 延拓在 {v_next} 处折叠(步长 <{MIN_DV} dex 仍失败)" + ); + stage_summaries.push(step_summary); + break; + } + } + stage_summaries.push(step_summary); + } + // 走到目标丰度且末步收敛 → 采纳(末步参数即目标) + if !final_converged + && (axis_to - v_now).abs() <= 1e-6 + && stage_summaries.last().map(|s| s.converged).unwrap_or(false) + && seed_now.is_some() + && seed_now.as_deref() != Some(orig_seed) + { + final_converged = true; + final_chmax = last_stage.chmax; + if let Some(s) = stage_summaries.last() { + if let Some(r) = s.best_max_relc { + final_max_relc = Some(r); + } + } + current_seed = seed_now.clone(); + info!("丰度轴 {axis_name} 延拓成功:末步已达目标丰度并收敛"); + } + if final_converged { + break; + } + } + } + } + } + // Final atmosphere file .7 let final_7 = model_dir.join(format!("{}.7", name)); if let Some(ref s_path) = current_seed { @@ -1093,6 +1660,7 @@ impl<'a> ExecutionRunner<'a> { temp_check, emflux_check, bfac_check, + ladder_seeds, note, }; @@ -1111,6 +1679,25 @@ mod tests { /// Phase 6(P8):执行链按当前策略派生——seed_step 走种子热启动链,其余走冷启动链。 #[test] + /// seed_step_stab 策略链:种子链形状 + 全步带 POPZER=1e-10/DPSILG=3.0 稳定化旋钮。 + #[test] + fn test_default_seed_stab_chain() { + let chain = super::default_seed_stab_chain(); + assert_eq!( + chain.iter().map(|s| s.label.as_str()).collect::>(), + vec!["seed_nc", "nl"], + "stab 链应保持种子链形状" + ); + for step in &chain { + assert_eq!(step.dpsilg, Some(3.0), "步 {} 应带 DPSILG=3.0", step.label); + assert_eq!(step.popzer, Some(1e-10), "步 {} 应带 POPZER=1e-10", step.label); + } + // 策略映射 + let via_strategy = super::default_chain_for_strategy("seed_step_stab"); + assert_eq!(via_strategy.len(), chain.len()); + assert!(via_strategy.iter().all(|s| s.popzer == Some(1e-10))); + } + fn test_default_chain_for_strategy() { let labels = |chain: Vec| -> Vec { chain.into_iter().map(|s| s.label).collect() @@ -1293,6 +1880,7 @@ exit 0 Some(super::default_seed_chain()), Some(&seed), None, + None, true, false, 60, @@ -1372,6 +1960,8 @@ exit 0 niter: 100, chmax: Some(0.001), orelax: Some(0.5), + dpsilg: None, + popzer: None, ilvlin: 100, require_converged: true, itek: None, @@ -1386,6 +1976,7 @@ exit 0 "cold_run", Some(chain), Some(&seed), + None, // seed_params None, true, false, @@ -1473,6 +2064,7 @@ exit 0 Some(super::default_seed_chain()), Some(&seed), None, + None, true, false, 60, @@ -1549,6 +2141,7 @@ exit 0 "seed_step", Some(chain), Some(&seed_path), + None, // seed_params None, // synspec_cfg true, // tlusty_enabled false, // synspec_enabled @@ -1578,4 +2171,313 @@ exit 0 ); let _ = std::fs::remove_dir_all(&work); } + + /// plan_ladder_steps 步进规划(纯函数)回归: + /// - logg 间隔 0.5(> 单步上限 0.25)→ logg 轴 2 步,每步 0.25; + /// - teff 间隔 5000(= 2×2500)→ teff 轴 2 步,每步 2500; + /// - 双轴都在单步域内 → 空(不该触发 ladder)。 + #[test] + fn test_plan_ladder_steps() { + // logg 轴主导:Δlogg=0.5、Δteff=0 + let s = super::plan_ladder_steps(60000.0, 5.5, 60000.0, 6.0); + assert_eq!(s.len(), 2); + assert!((s[0].1 - 5.75).abs() < 1e-9, "{:?}", s); + assert!((s[1].1 - 6.0).abs() < 1e-9); + assert!(s[0].2.starts_with("ladder_g")); + // teff 轴主导:Δteff=5000、Δlogg=0.2 + let s = super::plan_ladder_steps(55000.0, 5.0, 60000.0, 5.2); + assert_eq!(s.len(), 2); + assert!((s[0].0 - 57500.0).abs() < 1e-9, "{:?}", s); + assert!((s[0].1 - 5.2).abs() < 1e-9); + // 单步域内 → 空 + assert!(super::plan_ladder_steps(60000.0, 5.0, 57500.0, 5.1).is_empty()); + assert!(super::plan_ladder_steps(60000.0, 5.0, 60000.0, 5.25).is_empty()); + } + + /// ladder 回退接线回归(2026-08-17):种子链 + nl_direct 全失败 + 种子参数 + /// Δlogg=0.5 → 触发 2 步 logg 步进 + 目标步 nl_ladder;中间步登记 ladder_seeds。 + #[tokio::test] + async fn unit_ladder_fallback_wiring() { + let work = + std::env::temp_dir().join(format!("dcts_unit_ladder_{}", std::process::id())); + let _ = std::fs::remove_dir_all(&work); + tokio::fs::create_dir_all(&work).await.unwrap(); + // 假 tlusty:#1(seed_nc)/#2(nl)/#3(nl_direct) 发散;#4/#5(中间步)/#6(nl_ladder) + // 收敛。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 " 50 1.0E+03 2.0E+03" > fort.7 +case $n in + 1|2|3) printf ' 1 50 1.0E-02 1.0E-02 1.0E-02 1.0E-02 1.0E+16 75 1\n' > fort.9 ;; + *) 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), + }; + // 种子参数 Δlogg=0.5(同 teff/丰度)→ 2 步 logg 步进 + let seed_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(-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), + Some(&seed_params), + 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(), false), + ("ladder_g5.75".to_string(), true), + ("ladder_g6".to_string(), true), + ("nl_ladder".to_string(), true), + ], + "应触发 ladder 步进链并经 nl_ladder 收敛(阶段: {:?})", + labels + ); + assert_eq!(summary.ladder_seeds.len(), 2, "两个中间步都应登记梯级"); + assert_eq!( + summary.ladder_seeds[0].point_name, + "t60000_g5.75_he-4_c-4_n-4_o-4_ladder" + ); + assert!((summary.ladder_seeds[0].logg - 5.75).abs() < 1e-9); + assert!((summary.ladder_seeds[1].logg - 6.0).abs() < 1e-9); + assert!(summary.result_valid); + let _ = std::fs::remove_dir_all(&work); + } + + /// nl_stab2 回退接线(2026-08-18):种子链 seed_nc/nl/nl_direct 全发散、 + /// 无种子参数(ladder 不触发)→ nl_stab2 以二档稳定化参数自原始种子收敛。 + #[tokio::test] + async fn unit_nl_stab2_fallback_wiring() { + let work = std::env::temp_dir().join(format!("dcts_unit_stab2_{}", std::process::id())); + let _ = std::fs::remove_dir_all(&work); + tokio::fs::create_dir_all(&work).await.unwrap(); + // 假 tlusty:#1(seed_nc)/#2(nl)/#3(nl_direct) 发散;#4(nl_stab2) 收敛。 + 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 " 50 1.0E+03 2.0E+03" > fort.7 +case $n in + 1|2|3) printf ' 1 50 1.0E-02 1.0E-02 1.0E-02 1.0E-02 1.0E+16 75 1\n' > fort.9 ;; + *) 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(20000.0), + logg: crate::models::GridAxisValue::from_value(6.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(-4.0), + }; + let summary = runner + .run_model_with_timeout( + ¶ms, + "t20000_g6.0_he2_c-2_n-4_o-4", + "seed_step", + Some(super::default_seed_chain()), + Some(&seed), + None, // 种子参数未知 → ladder 回退不触发,直达 nl_stab2 + 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(), false), + ("nl_stab2".to_string(), true), + ], + "全链失败后应触发 nl_stab2 二档回退(阶段: {:?})", + labels + ); + assert!(summary.result_valid); + let _ = std::fs::remove_dir_all(&work); + } + + /// 丰度轴延拓接线回归(2026-08-21,60k 折叠角攻坚):高温域(Teff>30kK) + /// 下常规链全失败(stab 档因域门控跳过)→ 从同族 CNO 邻居种子沿 C 轴 + /// 自适应延拓,第 4 次调用收敛。种子参数与目标同 T/g/He、仅 C 不同。 + #[tokio::test] + async fn unit_axlad_c_axis_fallback_wiring() { + let work = std::env::temp_dir().join(format!("dcts_unit_axlad_{}", std::process::id())); + let _ = std::fs::remove_dir_all(&work); + tokio::fs::create_dir_all(&work).await.unwrap(); + // 假 tlusty:#1(seed_nc)/#2(nl)/#3(nl_direct) 发散;#4 起收敛。 + 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 " 50 1.0E+03 2.0E+03" > fort.7 +case $n in + 1|2|3) printf ' 1 50 1.0E-02 1.0E-02 1.0E-02 1.0E-02 1.0E+16 75 1\n' > fort.9 ;; + *) 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(); + // 种子参数:同 T/g/He、仅 logC 与目标不同 → C 轴延拓触发 + let seed_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(-4.0), + logc: crate::models::GridAxisValue::from_value(-1.0), + logn: crate::models::GridAxisValue::from_value(-4.0), + logo: crate::models::GridAxisValue::from_value(-1.0), + }; + + 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(-4.0), + logc: crate::models::GridAxisValue::from_value(-4.0), + logn: crate::models::GridAxisValue::from_value(-4.0), + logo: crate::models::GridAxisValue::from_value(-1.0), + }; + let summary = runner + .run_model_with_timeout( + ¶ms, + "t60000_g5.0_he-4_c-4_n-4_o-1", + "seed_step", + Some(super::default_seed_chain()), + Some(&seed), + Some(&seed_params), + None, + true, + false, + 120, + None, + None, None, None, None, None, None, None, None, None, + ) + .await + .unwrap(); + + let labels: Vec = summary.stages.iter().map(|s| s.label.clone()).collect(); + assert!( + labels.iter().any(|l| l.starts_with("axlad_c")), + "高温域全链失败后应触发 C 轴丰度延拓(阶段: {:?})", + labels + ); + assert!(summary.result_valid, "延拓收敛后结果应有效"); + assert!(summary + .stages + .last() + .map(|s| s.converged) + .unwrap_or(false)); + let _ = std::fs::remove_dir_all(&work); + } + + /// parse_point_name 回归:规范名与 _ladder 后缀名均可解析,乱名返回 None。 + #[test] + fn test_parse_point_name() { + let p = crate::models::GridPointParams::parse_point_name("t20000_g5.0_he-2_c-1_n-3_o-4"); + assert!(p.is_some()); + let p = p.unwrap(); + assert_eq!(p.teff.value(), 20000.0); + assert_eq!(p.logg.value(), 5.0); + assert_eq!(p.loghe.value(), -2.0); + // _ladder 合成名(ladder 持久化路径) + let p = crate::models::GridPointParams::parse_point_name( + "t60000_g5.75_he-4_c-4_n-4_o-4_ladder", + ); + assert!(p.is_some()); + assert!((p.unwrap().logg - 5.75).abs() < 1e-9); + assert!(crate::models::GridPointParams::parse_point_name("garbage").is_none()); + assert!(crate::models::GridPointParams::parse_point_name("").is_none()); + } } diff --git a/crates/common/src/summary_merge.rs b/crates/common/src/summary_merge.rs index 800dd73..07e7800 100644 --- a/crates/common/src/summary_merge.rs +++ b/crates/common/src/summary_merge.rs @@ -109,6 +109,7 @@ mod tests { temp_check: None, emflux_check: None, bfac_check: None, + ladder_seeds: Vec::new(), note: None, } } diff --git a/crates/node/src/executor.rs b/crates/node/src/executor.rs index 0be0bde..e0ff4ba 100644 --- a/crates/node/src/executor.rs +++ b/crates/node/src/executor.rs @@ -20,7 +20,7 @@ pub async fn execute_task( result_dir: &Path, task: &TaskSpec, shutdown: Option>, -) -> Result<(ModelSummary, Option>)> { +) -> Result<(ModelSummary, Option>, Vec<(String, Vec)>)> { info!( "开始执行计算任务 {} (网格点: {})", task.task_id, task.point_name @@ -68,7 +68,11 @@ pub async fn execute_task( // 全新 slot 内不可能已有目标大气;重试任务的 slot 亦全新(task_id 唯一)。 let tlusty_enabled = task.tlusty_config.enabled; let current_strategy = task.tlusty_config.current_strategy("cold_run"); - let needs_seed_download = (tlusty_enabled && current_strategy == "seed_step") + // seed_step_stab(2026-08-18 稳定化种子步进)同样是种子热启动链,必须下载种子; + // 2026-08-19 生产事故:漏列该策略导致种子未下载,seed_nc 无 fort.8 直接 + // EOF 崩溃(rc=2),78 个任务全部假失败。 + let needs_seed_download = (tlusty_enabled + && matches!(current_strategy, "seed_step" | "seed_step_stab")) || (!tlusty_enabled && task.synspec_config.enabled); if needs_seed_download { @@ -223,6 +227,7 @@ pub async fn execute_task( &task.tlusty_chain_params, &task.seed_chain_params, &task.task_id.to_string(), + task.params.teff.value(), ); // TLUSTY 输入文件全局参数(NFREAD/ions 表/nst extra_keys 等)。 // None → runner 用代码内硬编码默认(向后兼容)。 @@ -239,6 +244,12 @@ pub async fn execute_task( } } }); + // 种子参数(ladder 步进规划需要种子与目标的参数差):从权威 seed_point_name + // 解析。解析失败(命名不符)→ None → ladder 回退不触发,安全降级。 + let seed_params = task + .seed_point_name + .as_deref() + .and_then(common::models::GridPointParams::parse_point_name); let summary = runner .run_model_with_timeout( &task.params, @@ -248,6 +259,7 @@ pub async fn execute_task( current_strategy, Some(chain), seed_atmos_path.as_deref(), + seed_params.as_ref(), synspec_cfg.as_ref(), // 阶段独立配置开关(见 docs/task_engine_decoupling_design.md §5)。 task.tlusty_config.enabled, @@ -303,7 +315,30 @@ pub async fn execute_task( slot_work_dir.display() ); - Ok((summary, seed_bytes)) + // ladder 中间梯级种子字节:即便最终失败也上报(簇内相邻失败点可复用已收敛梯级)。 + // 阶段快照命名 .