feat(all): seed_step_stab 稳定化种子链与同族种子轮换、ladder/自适应 Teff/丰度轴延拓三级回退与梯级种子入库复用、热启动首末比豁免、workflow 完成翻转回退链阻塞修复、大气收敛权威统计与 M14 回填

收敛攻坚(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 残点实测矩阵
This commit is contained in:
fmq
2026-09-02 19:37:25 +08:00
parent b058e66722
commit f2700031ce
30 changed files with 3045 additions and 49 deletions
+11
View File
@@ -1147,6 +1147,15 @@ pub struct ChainStep {
pub idlte: Option<i32>,
pub iacc: Option<i32>,
pub orelax: Option<f64>,
/// 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<f64>,
/// POPZER(微布居置零阈值)。设置时同时写 POPZR2/RADZER=同值 + NITZER=1
/// 把可忽略微布居从方程中剔除(官方标准输入恒设 1E-20;难收敛角落用 1E-10)。
#[serde(default)]
pub popzer: Option<f64>,
}
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 能级数据(而非只有终止行)
+44 -2
View File
@@ -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_ladder0.038→6e-4,物理硬门全过)。
let itek: Vec<IterCheck> = (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<IterCheck> = 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
+2
View File
@@ -232,6 +232,8 @@ mod tests {
idlte: None,
iacc: None,
orelax: None,
dpsilg: None,
popzer: None,
}
}
+45
View File
@@ -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<Self> {
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<f64> { caps.get(i)?.as_str().parse::<f64>().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<BfacCheckResult>,
/// ladder 步进链的中间收敛模型(node→server 种子持久化清单)。空 = 未触发 ladder
/// 或无合格中间步。中间模型已过收敛 + NaN 否决,可安全入种子库供相邻失败点复用。
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub ladder_seeds: Vec<LadderSeedInfo>,
pub note: Option<String>,
}
/// 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::*;
+46
View File
@@ -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-18seed_step_stab 策略):
/// dpsilg → DPSILG=<val>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() {
File diff suppressed because it is too large Load Diff
+1
View File
@@ -109,6 +109,7 @@ mod tests {
temp_check: None,
emflux_check: None,
bfac_check: None,
ladder_seeds: Vec::new(),
note: None,
}
}