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:
@@ -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 能级数据(而非只有终止行)
|
||||
|
||||
@@ -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<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
|
||||
|
||||
@@ -232,6 +232,8 @@ mod tests {
|
||||
idlte: None,
|
||||
iacc: None,
|
||||
orelax: None,
|
||||
dpsilg: None,
|
||||
popzer: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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=<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() {
|
||||
|
||||
+904
-2
File diff suppressed because it is too large
Load Diff
@@ -109,6 +109,7 @@ mod tests {
|
||||
temp_check: None,
|
||||
emflux_check: None,
|
||||
bfac_check: None,
|
||||
ladder_seeds: Vec::new(),
|
||||
note: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ pub async fn execute_task(
|
||||
result_dir: &Path,
|
||||
task: &TaskSpec,
|
||||
shutdown: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
|
||||
) -> Result<(ModelSummary, Option<Vec<u8>>)> {
|
||||
) -> Result<(ModelSummary, Option<Vec<u8>>, Vec<(String, Vec<u8>)>)> {
|
||||
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 中间梯级种子字节:即便最终失败也上报(簇内相邻失败点可复用已收敛梯级)。
|
||||
// 阶段快照命名 <name>.<label>.7(execute_tlusty_stage 写入 model_dir)。
|
||||
let mut ladder_seed_files: Vec<(String, Vec<u8>)> = Vec::new();
|
||||
if !summary.ladder_seeds.is_empty() {
|
||||
let model_sub_dir = slot_work_dir.join(&summary.name);
|
||||
for info in &summary.ladder_seeds {
|
||||
let path = model_sub_dir.join(format!("{}.{}.7", summary.name, info.label));
|
||||
if path.is_file() {
|
||||
if let Ok(bytes) = tokio::fs::read(&path).await {
|
||||
info!(
|
||||
"读取 ladder 中间种子 {}({},{} 字节)",
|
||||
info.point_name,
|
||||
path.display(),
|
||||
bytes.len()
|
||||
);
|
||||
ladder_seed_files.push((info.point_name.clone(), bytes));
|
||||
}
|
||||
} else {
|
||||
warn!("ladder 种子快照缺失: {}", path.display());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok((summary, seed_bytes, ladder_seed_files))
|
||||
}
|
||||
|
||||
/// 清理任务在 Node 端的沙盒目录
|
||||
@@ -518,6 +553,7 @@ fn resolve_execution_chain(
|
||||
tlusty_chain_params: &Option<serde_json::Value>,
|
||||
seed_chain_params: &Option<serde_json::Value>,
|
||||
task_id: &str,
|
||||
teff: f64,
|
||||
) -> Vec<ChainStep> {
|
||||
if current_strategy == "seed_step" {
|
||||
seed_chain_params
|
||||
@@ -534,6 +570,27 @@ fn resolve_execution_chain(
|
||||
})
|
||||
.filter(|c| !c.is_empty())
|
||||
.unwrap_or_else(common::runner::default_seed_chain)
|
||||
} else if current_strategy == "seed_step_stab" {
|
||||
// 稳定化种子步进:POPZER+DPSILG 稳定化参数即本策略的实质——用户自定义链
|
||||
// (tlusty_chain/seed_chain)不带这些旋钮,热启动会重演发散,故不采纳、
|
||||
// 恒用 default_seed_stab_chain。
|
||||
// 2026-08-21 域门控(60k 攻坚实测,docs/failed81_...md §十一):DPSILG/POPZER
|
||||
// 族旋钮仅在低温 He 富域(Teff≤30kK)有效;高温高金属域(60k/g5.0 o-1 角)
|
||||
// 实测自复现收敛种子加档后 17 拍爆到 4e16——致散。域外改用普通种子链,
|
||||
// 交给 runner 的自适应 Teff 延拓处理。
|
||||
if teff <= 30000.0 {
|
||||
common::runner::default_seed_stab_chain()
|
||||
} else {
|
||||
info!(
|
||||
"任务 {}:Teff={}>30kK 域外,seed_step_stab 降级为普通种子链(稳定化旋钮高温致散)",
|
||||
task_id, teff
|
||||
);
|
||||
seed_chain_params
|
||||
.as_ref()
|
||||
.and_then(|v| serde_json::from_value::<Vec<ChainStep>>(v.clone()).ok())
|
||||
.filter(|c| !c.is_empty())
|
||||
.unwrap_or_else(common::runner::default_seed_chain)
|
||||
}
|
||||
} else {
|
||||
tlusty_chain_params
|
||||
.as_ref()
|
||||
@@ -585,6 +642,7 @@ mod tests {
|
||||
temp_check: None,
|
||||
emflux_check: None,
|
||||
bfac_check: None,
|
||||
ladder_seeds: Vec::new(),
|
||||
note: None,
|
||||
}
|
||||
}
|
||||
@@ -604,7 +662,7 @@ mod tests {
|
||||
]);
|
||||
|
||||
// seed_chain_params=None → 走 default_seed_chain;tlusty_chain_params 被忽略。
|
||||
let chain = resolve_execution_chain("seed_step", &Some(custom_cold), &None, "t1");
|
||||
let chain = resolve_execution_chain("seed_step", &Some(custom_cold), &None, "t1", 20000.0);
|
||||
assert_eq!(labels(&chain), vec!["seed_nc", "nl"]);
|
||||
// 首步必须是非灰 LTE(ltgray=F),否则会删 fort.8 丢弃种子。
|
||||
assert_eq!(chain[0].ltgray, "F");
|
||||
@@ -618,13 +676,32 @@ mod tests {
|
||||
{"label": "nl", "lte": "F", "ltgray": "F", "ilvlin": 100, "niter": 100, "orelax": 0.5},
|
||||
]);
|
||||
|
||||
let chain = resolve_execution_chain("seed_step", &None, &Some(custom_seed), "t1b");
|
||||
let chain = resolve_execution_chain("seed_step", &None, &Some(custom_seed), "t1b", 20000.0);
|
||||
assert_eq!(labels(&chain), vec!["seed_nc", "nl"]);
|
||||
// 用户配置的 orelax 生效。
|
||||
assert_eq!(chain[0].orelax, Some(0.3));
|
||||
assert_eq!(chain[1].orelax, Some(0.5));
|
||||
}
|
||||
|
||||
/// seed_step_stab 温度域门控(2026-08-21,60k 攻坚):
|
||||
/// Teff≤30kK 用稳定化链(全步带 DPSILG/POPZER);
|
||||
/// 高温域(60k/g5.0 高金属角实测旋钮致散)降级为普通种子链。
|
||||
#[test]
|
||||
fn seed_step_stab_teff_domain_gating() {
|
||||
let cold = resolve_execution_chain("seed_step_stab", &None, &None, "hot1", 20000.0);
|
||||
assert!(
|
||||
cold.iter().all(|s| s.dpsilg.is_some() && s.popzer.is_some()),
|
||||
"低温域 seed_step_stab 应带稳定化参数"
|
||||
);
|
||||
|
||||
let hot = resolve_execution_chain("seed_step_stab", &None, &None, "hot2", 60000.0);
|
||||
assert!(
|
||||
hot.iter().all(|s| s.dpsilg.is_none() && s.popzer.is_none()),
|
||||
"高温域 seed_step_stab 应降级为普通种子链(不带稳定化旋钮)"
|
||||
);
|
||||
assert_eq!(labels(&hot), vec!["seed_nc", "nl"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cold_run_uses_custom_chain_when_provided() {
|
||||
let custom = serde_json::json!([
|
||||
@@ -633,13 +710,13 @@ mod tests {
|
||||
{"label": "nl", "lte": "F", "ltgray": "F", "ilvlin": 100, "niter": 100},
|
||||
]);
|
||||
|
||||
let chain = resolve_execution_chain("cold_run", &Some(custom), &None, "t2");
|
||||
let chain = resolve_execution_chain("cold_run", &Some(custom), &None, "t2", 20000.0);
|
||||
assert_eq!(labels(&chain), vec!["lte", "nc", "nl"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cold_run_falls_back_to_default_when_no_custom_chain() {
|
||||
let chain = resolve_execution_chain("cold_run", &None, &None, "t3");
|
||||
let chain = resolve_execution_chain("cold_run", &None, &None, "t3", 20000.0);
|
||||
assert_eq!(labels(&chain), vec!["lte", "nc", "nl"]);
|
||||
}
|
||||
|
||||
|
||||
@@ -50,12 +50,22 @@ fn derive_report_status(s: &ModelSummary) -> TaskStatus {
|
||||
}
|
||||
}
|
||||
|
||||
/// executor 返回值:(summary, 最终种子字节, ladder 中间梯级种子字节列表)。
|
||||
pub type ExecOutcome = Result<
|
||||
(
|
||||
ModelSummary,
|
||||
Option<Vec<u8>>,
|
||||
Vec<(String, Vec<u8>)>,
|
||||
),
|
||||
String,
|
||||
>;
|
||||
|
||||
pub async fn report_result(
|
||||
client: &Client,
|
||||
server_url: &str,
|
||||
node_id: &str,
|
||||
task: &TaskSpec,
|
||||
exec_res: Result<(ModelSummary, Option<Vec<u8>>), String>,
|
||||
exec_res: ExecOutcome,
|
||||
) -> Result<()> {
|
||||
let report_url = format!("{}/api/task/report", server_url);
|
||||
|
||||
@@ -69,8 +79,9 @@ pub async fn report_result(
|
||||
summary_json,
|
||||
seed_bytes,
|
||||
failed_stage,
|
||||
ladder_seed_files,
|
||||
) = match exec_res {
|
||||
Ok((s, s_bytes)) => (
|
||||
Ok((s, s_bytes, ladder_files)) => (
|
||||
derive_report_status(&s),
|
||||
s.result_valid,
|
||||
s.final_max_relc,
|
||||
@@ -80,6 +91,7 @@ pub async fn report_result(
|
||||
serde_json::to_string(&s).unwrap_or_default(),
|
||||
s_bytes,
|
||||
infer_failed_stage(task, &s),
|
||||
ladder_files,
|
||||
),
|
||||
Err(e) => (
|
||||
TaskStatus::Failed,
|
||||
@@ -91,6 +103,7 @@ pub async fn report_result(
|
||||
serde_json::json!({"error": e}).to_string(),
|
||||
None,
|
||||
None,
|
||||
Vec::new(),
|
||||
),
|
||||
};
|
||||
|
||||
@@ -128,6 +141,15 @@ pub async fn report_result(
|
||||
}
|
||||
}
|
||||
|
||||
// ladder 中间梯级种子:即便最终任务失败也上传(executor 只收集收敛+无 NaN
|
||||
// 的中间模型),server 端落 seeds 表供相邻失败点复用。
|
||||
for (seed_name, bytes) in &ladder_seed_files {
|
||||
let part = Part::bytes(bytes.clone())
|
||||
.file_name(format!("{}.7", seed_name))
|
||||
.mime_str("application/octet-stream")?;
|
||||
form = form.part("ladder_seed", part);
|
||||
}
|
||||
|
||||
match client.post(&report_url).multipart(form).send().await {
|
||||
Ok(resp) if resp.status().is_success() => {
|
||||
info!(
|
||||
@@ -239,6 +261,7 @@ mod tests {
|
||||
temp_check: None,
|
||||
emflux_check: None,
|
||||
bfac_check: None,
|
||||
ladder_seeds: Vec::new(),
|
||||
note: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -542,7 +542,7 @@ impl NodeWorker {
|
||||
// 严重失败(runner 抛 Err)时 summary_opt=None,此时回退到 task.point_name
|
||||
// (二者均源自 params.model_name())作为归档目录名,确保失败任务的
|
||||
// 排错日志也能落盘而非随沙盒删除丢失。
|
||||
let summary_opt = res.as_ref().ok().map(|(s, _)| s.clone());
|
||||
let summary_opt = res.as_ref().ok().map(|(s, _, _)| s.clone());
|
||||
let result_name = summary_opt
|
||||
.as_ref()
|
||||
.map(|s| s.name.clone())
|
||||
|
||||
@@ -81,6 +81,8 @@ pub async fn report_task(
|
||||
) -> Result<impl IntoResponse, crate::api::AppError> {
|
||||
let mut report_json: Option<TaskReport> = None;
|
||||
let mut seed_file_data: Option<Vec<u8>> = None;
|
||||
// ladder 中间梯级种子(可重复字段):文件名(去 .7)即合成种子名,参数从名称解析。
|
||||
let mut ladder_seed_files: Vec<(String, Vec<u8>)> = Vec::new();
|
||||
let mut multipart_error = false;
|
||||
|
||||
// 遍历全部 multipart 字段。旧实现 `while let Ok(Some(field))` 在首个字段读取错误时
|
||||
@@ -114,6 +116,24 @@ pub async fn report_task(
|
||||
multipart_error = true;
|
||||
}
|
||||
}
|
||||
} else if field_name == "ladder_seed" {
|
||||
// 文件名即合成种子名(<model_name>_ladder.7)
|
||||
let file_name = field
|
||||
.file_name()
|
||||
.unwrap_or("")
|
||||
.trim_end_matches(".7")
|
||||
.to_string();
|
||||
match field.bytes().await {
|
||||
Ok(bytes) => {
|
||||
if !file_name.is_empty() {
|
||||
ladder_seed_files.push((file_name, bytes.to_vec()));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("读取 ladder_seed 字段失败: {}", e);
|
||||
multipart_error = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None) => break,
|
||||
@@ -329,6 +349,40 @@ pub async fn report_task(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ladder 中间梯级种子持久化(2026-08-17):node 端仅上报「收敛 + 无 NaN」的
|
||||
// 中间模型;此处原子落盘 + 按 _ladder 合成名入 seeds 表(ON CONFLICT 仅刷新
|
||||
// file_path,绝不触碰真实网格点种子行)。名称解析失败 → 丢弃并告警(防脏名)。
|
||||
for (seed_name, bytes) in &ladder_seed_files {
|
||||
let Some(lp) = common::models::GridPointParams::parse_point_name(seed_name) else {
|
||||
warn!("ladder 种子名 {} 无法解析参数,丢弃", seed_name);
|
||||
continue;
|
||||
};
|
||||
let ladder_dir = Path::new(&state.seeds_dir).join(seed_name);
|
||||
if fs::create_dir_all(&ladder_dir).await.is_err() {
|
||||
warn!("ladder 种子目录创建失败: {}", ladder_dir.display());
|
||||
continue;
|
||||
}
|
||||
let tmp = ladder_dir.join(format!("{}.7.{}.tmp", seed_name, uuid::Uuid::new_v4().simple()));
|
||||
let path = ladder_dir.join(format!("{}.7", seed_name));
|
||||
if fs::write(&tmp, bytes).await.is_ok() && fs::rename(&tmp, &path).await.is_ok() {
|
||||
if let Err(e) = state
|
||||
.db
|
||||
.insert_seed_named(seed_name, &lp, &path.to_string_lossy())
|
||||
.await
|
||||
{
|
||||
warn!("ladder 种子 {} 入库失败: {}", seed_name, e);
|
||||
} else {
|
||||
info!(
|
||||
"ladder 中间种子入库: {} (t{} g{} he{})",
|
||||
seed_name,
|
||||
lp.teff.value(),
|
||||
lp.logg.value(),
|
||||
lp.loghe.value()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if state_changed
|
||||
|
||||
@@ -576,8 +576,10 @@ impl Database {
|
||||
SUM(CASE WHEN status = 'running' THEN 1 ELSE 0 END) AS running,
|
||||
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) AS completed,
|
||||
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS failed,
|
||||
SUM(CASE WHEN tlusty_status = 'converged' THEN 1 ELSE 0 END) AS tlusty_converged,
|
||||
SUM(CASE WHEN tlusty_status = 'converged' AND tlusty_success_method = 'cold_run' THEN 1 ELSE 0 END) AS cold_run_converged,
|
||||
SUM(CASE WHEN tlusty_status = 'converged' AND tlusty_success_method = 'seed_step' THEN 1 ELSE 0 END) AS seed_step_converged,
|
||||
SUM(CASE WHEN tlusty_status = 'converged' AND tlusty_success_method = 'seed_step_stab' THEN 1 ELSE 0 END) AS seed_step_stab_converged,
|
||||
SUM(CASE WHEN tlusty_status = 'failed' THEN 1 ELSE 0 END) AS tlusty_failed,
|
||||
SUM(CASE WHEN synspec_status = 'converged' THEN 1 ELSE 0 END) AS synspec_converged,
|
||||
SUM(CASE WHEN synspec_status = 'failed' THEN 1 ELSE 0 END) AS synspec_failed,
|
||||
@@ -586,7 +588,7 @@ impl Database {
|
||||
params![name],
|
||||
|r| {
|
||||
let n = |i: usize| -> i64 { r.get::<_, Option<i64>>(i).unwrap_or(None).unwrap_or(0) };
|
||||
Ok((n(0), n(1), n(2), n(3), n(4), n(5), n(6), n(7), n(8), n(9), n(10), n(11)))
|
||||
Ok((n(0), n(1), n(2), n(3), n(4), n(5), n(6), n(7), n(8), n(9), n(10), n(11), n(12), n(13)))
|
||||
},
|
||||
),
|
||||
None => conn.query_row(
|
||||
@@ -597,8 +599,10 @@ impl Database {
|
||||
SUM(CASE WHEN status = 'running' THEN 1 ELSE 0 END) AS running,
|
||||
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) AS completed,
|
||||
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS failed,
|
||||
SUM(CASE WHEN tlusty_status = 'converged' THEN 1 ELSE 0 END) AS tlusty_converged,
|
||||
SUM(CASE WHEN tlusty_status = 'converged' AND tlusty_success_method = 'cold_run' THEN 1 ELSE 0 END) AS cold_run_converged,
|
||||
SUM(CASE WHEN tlusty_status = 'converged' AND tlusty_success_method = 'seed_step' THEN 1 ELSE 0 END) AS seed_step_converged,
|
||||
SUM(CASE WHEN tlusty_status = 'converged' AND tlusty_success_method = 'seed_step_stab' THEN 1 ELSE 0 END) AS seed_step_stab_converged,
|
||||
SUM(CASE WHEN tlusty_status = 'failed' THEN 1 ELSE 0 END) AS tlusty_failed,
|
||||
SUM(CASE WHEN synspec_status = 'converged' THEN 1 ELSE 0 END) AS synspec_converged,
|
||||
SUM(CASE WHEN synspec_status = 'failed' THEN 1 ELSE 0 END) AS synspec_failed,
|
||||
@@ -607,7 +611,7 @@ impl Database {
|
||||
[],
|
||||
|r| {
|
||||
let n = |i: usize| -> i64 { r.get::<_, Option<i64>>(i).unwrap_or(None).unwrap_or(0) };
|
||||
Ok((n(0), n(1), n(2), n(3), n(4), n(5), n(6), n(7), n(8), n(9), n(10), n(11)))
|
||||
Ok((n(0), n(1), n(2), n(3), n(4), n(5), n(6), n(7), n(8), n(9), n(10), n(11), n(12), n(13)))
|
||||
},
|
||||
),
|
||||
}?;
|
||||
@@ -618,8 +622,10 @@ impl Database {
|
||||
running,
|
||||
completed,
|
||||
failed,
|
||||
tlusty_converged,
|
||||
cold_run_converged,
|
||||
seed_step_converged,
|
||||
seed_step_stab_converged,
|
||||
tlusty_failed,
|
||||
synspec_converged,
|
||||
synspec_failed,
|
||||
@@ -633,8 +639,13 @@ impl Database {
|
||||
"running": running,
|
||||
"completed": completed,
|
||||
"failed": failed,
|
||||
// tlusty_converged(2026-08-25):大气收敛的权威总数(不按 method 拆分)。
|
||||
// 前端「N 大气收敛」此前用 cold+seed 之和——seed_step_stab(2026-08-18 引入)
|
||||
// 收敛的 70 点被漏计(生产 9137/9216 差额主因)。
|
||||
"tlusty_converged": tlusty_converged,
|
||||
"cold_run_converged": cold_run_converged,
|
||||
"seed_step_converged": seed_step_converged,
|
||||
"seed_step_stab_converged": seed_step_stab_converged,
|
||||
"tlusty_failed": tlusty_failed,
|
||||
"synspec_converged": synspec_converged,
|
||||
"synspec_failed": synspec_failed,
|
||||
@@ -733,6 +744,8 @@ impl Database {
|
||||
failed,
|
||||
cold_run_converged: g("cold_run_converged"),
|
||||
seed_step_converged: g("seed_step_converged"),
|
||||
tlusty_converged: g("tlusty_converged"),
|
||||
seed_step_stab_converged: g("seed_step_stab_converged"),
|
||||
tlusty_failed: g("tlusty_failed"),
|
||||
synspec_converged: g("synspec_converged"),
|
||||
synspec_failed: g("synspec_failed"),
|
||||
|
||||
@@ -710,6 +710,12 @@ pub struct WorkflowListStats {
|
||||
pub running: i64,
|
||||
pub cold_run_converged: i64,
|
||||
pub seed_step_converged: i64,
|
||||
/// TLUSTY 阶段收敛总数(tlusty_status='converged')——权威口径,语义同
|
||||
/// WorkflowStats.tlusty_converged(2026-08-25 补,见彼处注释)。
|
||||
#[serde(default)]
|
||||
pub tlusty_converged: i64,
|
||||
#[serde(default)]
|
||||
pub seed_step_stab_converged: i64,
|
||||
/// TLUSTY 阶段发散点数(tlusty_status='failed')。
|
||||
pub tlusty_failed: i64,
|
||||
/// SYNSPEC 阶段收敛点数(synspec_status='converged')。
|
||||
@@ -754,6 +760,15 @@ pub struct WorkflowStats {
|
||||
pub failed: i64,
|
||||
pub cold_run_converged: i64,
|
||||
pub seed_step_converged: i64,
|
||||
/// TLUSTY 阶段收敛总数(tlusty_status='converged',不按 method 拆分)——权威口径。
|
||||
/// 前端「N 大气收敛」应消费此字段;cold/seed/stab 为策略细分。2026-08-25 补:
|
||||
/// 此前前端用 cold+seed 之和,seed_step_stab(2026-08-18 引入)收敛点被漏计
|
||||
/// (生产 9137/9216 差额主因)。
|
||||
#[serde(default)]
|
||||
pub tlusty_converged: i64,
|
||||
/// TLUSTY 阶段以 seed_step_stab 策略收敛的点数(稳定化/waypoint 种子链)。
|
||||
#[serde(default)]
|
||||
pub seed_step_stab_converged: i64,
|
||||
/// TLUSTY 阶段发散点数(tlusty_status='failed')。
|
||||
pub tlusty_failed: i64,
|
||||
/// SYNSPEC 阶段收敛点数(synspec_status='converged')。
|
||||
@@ -1199,6 +1214,7 @@ mod tests {
|
||||
temp_check: None,
|
||||
emflux_check: None,
|
||||
bfac_check: None,
|
||||
ladder_seeds: Vec::new(),
|
||||
note: None,
|
||||
};
|
||||
db.upsert_point_summary(&summary.name, "wf_a", &summary, "seed_step")
|
||||
@@ -1215,12 +1231,16 @@ mod tests {
|
||||
assert_eq!(all["completed"], 1);
|
||||
assert_eq!(all["cold_run_converged"], 0);
|
||||
assert_eq!(all["seed_step_converged"], 1);
|
||||
// tlusty_converged(2026-08-25):不按 method 拆分的权威总数。
|
||||
assert_eq!(all["tlusty_converged"], 1);
|
||||
assert_eq!(all["seed_step_stab_converged"], 0);
|
||||
// 单工作流 wf_a:1 pending + 1 seed_step 收敛
|
||||
let a = db.get_grid_summary_stats(Some("wf_a")).await.unwrap();
|
||||
assert_eq!(a["total"], 2);
|
||||
assert_eq!(a["pending"], 1);
|
||||
assert_eq!(a["queued"], 0);
|
||||
assert_eq!(a["seed_step_converged"], 1);
|
||||
assert_eq!(a["tlusty_converged"], 1);
|
||||
// 不存在的工作流:0
|
||||
let none = db
|
||||
.get_grid_summary_stats(Some("nonexistent"))
|
||||
@@ -1264,6 +1284,120 @@ mod tests {
|
||||
assert_eq!(m.unwrap().name, exact.model_name());
|
||||
}
|
||||
|
||||
/// find_exact_family_seed_from_db 严格同物理族判定(2026-08-19 生产修复回归):
|
||||
/// ladder 中间种子(如 teff=22500)即便 CNO 距离为 0,也因 Teff 不同被排除;
|
||||
/// 同 Teff/logg/logHe 的 CNO 邻居正常命中;排除目标自身名。
|
||||
#[tokio::test]
|
||||
async fn test_find_exact_family_seed_strict_same_family() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let db = Database::new(&temp_dir.path().join("seed_stab_db.db").to_string_lossy())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let target = GridPointParams {
|
||||
teff: 20000.0.into(),
|
||||
logg: 6.5.into(),
|
||||
loghe: 2.0.into(),
|
||||
logc: (-4.0).into(),
|
||||
logn: (-3.0).into(),
|
||||
logo: (-4.0).into(),
|
||||
};
|
||||
// ladder 中间种子:同 logg/logHe、CNO 完全一致,但 teff=22500 —— 必须排除。
|
||||
let ladder = GridPointParams {
|
||||
teff: 22500.0.into(),
|
||||
logg: 6.5.into(),
|
||||
loghe: 2.0.into(),
|
||||
logc: (-4.0).into(),
|
||||
logn: (-3.0).into(),
|
||||
logo: (-4.0).into(),
|
||||
};
|
||||
// 合法 CNO 邻居:同 Teff/logg/logHe,Δo=1。
|
||||
let cno_neighbor = GridPointParams {
|
||||
teff: 20000.0.into(),
|
||||
logg: 6.5.into(),
|
||||
loghe: 2.0.into(),
|
||||
logc: (-4.0).into(),
|
||||
logn: (-3.0).into(),
|
||||
logo: (-3.0).into(),
|
||||
};
|
||||
db.insert_seed(&ladder, "/tmp/ladder.7").await.unwrap();
|
||||
db.insert_seed(&cno_neighbor, "/tmp/neighbor.7").await.unwrap();
|
||||
|
||||
let m = db
|
||||
.find_exact_family_seed_from_db(&target, &[target.model_name()])
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(m.is_some(), "应命中 CNO 邻居");
|
||||
assert_eq!(
|
||||
m.unwrap().name,
|
||||
cno_neighbor.model_name(),
|
||||
"不得选 teff 不同的 ladder 中间种子(即便其 CNO 距离为 0)"
|
||||
);
|
||||
|
||||
// 无严格同族候选时返回 None(不退化到 global)。
|
||||
let lonely = GridPointParams {
|
||||
teff: 30000.0.into(),
|
||||
logg: 5.0.into(),
|
||||
loghe: (-2.0).into(),
|
||||
logc: (-2.0).into(),
|
||||
logn: (-2.0).into(),
|
||||
logo: (-2.0).into(),
|
||||
};
|
||||
let none = db
|
||||
.find_exact_family_seed_from_db(&lonely, &[lonely.model_name()])
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(none.is_none());
|
||||
}
|
||||
|
||||
|
||||
/// 种子轮换回归(2026-08-20 修复):find_exact_family_seed_from_db 排除列表
|
||||
/// 应使调用方在重试间轮换到下一个未试过的同族 CNO 邻居;全部排除后返回 None。
|
||||
#[tokio::test]
|
||||
async fn test_find_exact_family_seed_rotation() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let db = Database::new(&temp_dir.path().join("seed_rot_db.db").to_string_lossy())
|
||||
.await
|
||||
.unwrap();
|
||||
let base = |logc: f64, logn: f64, logo: f64| GridPointParams {
|
||||
teff: 20000.0.into(),
|
||||
logg: 6.0.into(),
|
||||
loghe: 2.0.into(),
|
||||
logc: logc.into(),
|
||||
logn: logn.into(),
|
||||
logo: logo.into(),
|
||||
};
|
||||
let target = base(-3.0, -3.0, -1.0);
|
||||
let n1 = base(-3.0, -2.0, -1.0);
|
||||
let n2 = base(-3.0, -3.0, -2.0);
|
||||
db.insert_seed(&n1, "/tmp/rot_n1.7").await.unwrap();
|
||||
db.insert_seed(&n2, "/tmp/rot_n2.7").await.unwrap();
|
||||
|
||||
let self_name = target.model_name();
|
||||
// 首次:选距离最近的未排除邻居。
|
||||
let first = db
|
||||
.find_exact_family_seed_from_db(&target, &[self_name.clone()])
|
||||
.await
|
||||
.unwrap()
|
||||
.expect("应命中 CNO 邻居");
|
||||
// 排除首个后:轮换到另一个邻居。
|
||||
let second = db
|
||||
.find_exact_family_seed_from_db(&target, &[self_name, first.name.clone()])
|
||||
.await
|
||||
.unwrap()
|
||||
.expect("排除首个邻居后应轮换到第二个邻居");
|
||||
assert_ne!(first.name, second.name);
|
||||
// 全部排除后:None(终态 failed,不死循环)。
|
||||
let none = db
|
||||
.find_exact_family_seed_from_db(
|
||||
&target,
|
||||
&[target.model_name(), first.name, second.name],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(none.is_none(), "邻居全部试过后应返回 None");
|
||||
}
|
||||
|
||||
/// "种子回退仅一次"守卫语义(2026-08-02 涡旋事故定稿):has_seed_step_attempt
|
||||
/// 统计**一切** seed_step 行(含 pending)。pending 行在新架构下只有两种来源:
|
||||
/// (a) 真在途(排队/已领用)——计数它正是对在途回退的去重,挡住救援途中迟到失败
|
||||
@@ -3265,6 +3399,347 @@ mod tests {
|
||||
assert_eq!(item.status, "completed");
|
||||
}
|
||||
|
||||
/// 完成 flip 的「未消费回退链」阻塞回归(2026-08-22 修复):
|
||||
/// 最后一个活跃点失败时,其最新 failed 行策略链弹掉失败首项后仍有顺位
|
||||
/// (长度 >1)→ workflow 不得置 completed(否则 trigger_strategy_fallback
|
||||
/// 的 still_running 守卫拦截,链上 seed_step/seed_step_stab 永不派发;
|
||||
/// 生产实证 sdB_cno/t60000_g5.0_he-4_c-4_n-4_o-1,8-22 04:00:50)。
|
||||
/// 链耗尽(长度 1)后放行翻转。
|
||||
#[tokio::test]
|
||||
async fn test_workflow_flip_blocked_by_unconsumed_fallback_chain() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let db = Database::new(
|
||||
&temp_dir
|
||||
.path()
|
||||
.join("flip_block_db.db")
|
||||
.to_string_lossy(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let params = GridPointParams {
|
||||
teff: 60000.0.into(),
|
||||
logg: 5.0.into(),
|
||||
loghe: (-4.0).into(),
|
||||
logc: (-4.0).into(),
|
||||
logn: (-4.0).into(),
|
||||
logo: (-1.0).into(),
|
||||
};
|
||||
let name = params.model_name();
|
||||
db.upsert_workflow("wf_fb2", None, "config", "idle")
|
||||
.await
|
||||
.unwrap();
|
||||
db.update_workflow_status("wf_fb2", "running").await.unwrap();
|
||||
db.upsert_grid_point(¶ms, 0, "wf_fb2").await.unwrap();
|
||||
// 点终态 failed、无 pending/queued/running——旧条件已满足 flip。
|
||||
db.update_grid_status(&name, GridPointStatus::Failed, "wf_fb2")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// 最新 failed 行携带 3 顺位链(cold_run 刚失败,尚余 seed_step/seed_step_stab)。
|
||||
let t1 = uuid::Uuid::new_v4();
|
||||
db.insert_task(&common::models::TaskSpec {
|
||||
task_id: t1,
|
||||
point_name: name.clone(),
|
||||
params: params.clone(),
|
||||
timeout_sec: 7200,
|
||||
workflow_name: Some("wf_fb2".to_string()),
|
||||
wave: 0,
|
||||
tlusty_config: PhaseConfig {
|
||||
strategies: vec![
|
||||
"cold_run".to_string(),
|
||||
"seed_step".to_string(),
|
||||
"seed_step_stab".to_string(),
|
||||
],
|
||||
..PhaseConfig::default_tlusty()
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
{
|
||||
let pool = db.pool.clone();
|
||||
let tid = t1.to_string();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let conn = pool.get().unwrap();
|
||||
conn.execute(
|
||||
"UPDATE tasks SET status = 'failed', completed_at = datetime('now') \
|
||||
WHERE task_id = ?1",
|
||||
params![tid],
|
||||
)
|
||||
.unwrap();
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
db.sync_all_running_workflows_completion().await.unwrap();
|
||||
let wf = db.get_workflow("wf_fb2").await.unwrap().unwrap();
|
||||
assert_eq!(
|
||||
wf.status, "running",
|
||||
"存在未消费回退链的 failed 点时,workflow 不应翻转 completed"
|
||||
);
|
||||
|
||||
// 回退推进到链尾(更新的 failed 行仅剩单顺位)→ 阻塞解除,翻转放行。
|
||||
let t2 = uuid::Uuid::new_v4();
|
||||
db.insert_task(&common::models::TaskSpec {
|
||||
task_id: t2,
|
||||
point_name: name.clone(),
|
||||
params: params.clone(),
|
||||
timeout_sec: 7200,
|
||||
workflow_name: Some("wf_fb2".to_string()),
|
||||
wave: 0,
|
||||
tlusty_config: PhaseConfig {
|
||||
strategies: vec!["seed_step_stab".to_string()],
|
||||
..PhaseConfig::default_tlusty()
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
{
|
||||
let pool = db.pool.clone();
|
||||
let tid = t2.to_string();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let conn = pool.get().unwrap();
|
||||
conn.execute(
|
||||
"UPDATE tasks SET status = 'failed', completed_at = datetime('now'), \
|
||||
created_at = datetime('now', '+1 hour') WHERE task_id = ?1",
|
||||
params![tid],
|
||||
)
|
||||
.unwrap();
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
db.sync_all_running_workflows_completion().await.unwrap();
|
||||
let wf2 = db.get_workflow("wf_fb2").await.unwrap().unwrap();
|
||||
assert_eq!(
|
||||
wf2.status, "completed",
|
||||
"链耗尽(长度 1)后应恢复完成翻转"
|
||||
);
|
||||
}
|
||||
|
||||
/// 完成 flip 的「种子轮换待执行」阻塞回归(2026-08-22 审查补充):
|
||||
/// 链尾 [seed_step_stab] 失败 + 存在未试过的同物理族干净种子 → 不翻转
|
||||
/// (否则 rotation 臂被 still_running 守卫吞掉,waypoint 只有一次机会);
|
||||
/// 同族种子全部用过后放行翻转。
|
||||
#[tokio::test]
|
||||
async fn test_workflow_flip_blocked_by_seed_rotation_pending() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let db = Database::new(
|
||||
&temp_dir
|
||||
.path()
|
||||
.join("flip_rot_db.db")
|
||||
.to_string_lossy(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let params = GridPointParams {
|
||||
teff: 60000.0.into(),
|
||||
logg: 5.0.into(),
|
||||
loghe: (-4.0).into(),
|
||||
logc: (-4.0).into(),
|
||||
logn: (-4.0).into(),
|
||||
logo: (-1.0).into(),
|
||||
};
|
||||
let name = params.model_name();
|
||||
db.upsert_workflow("wf_rot", None, "config", "idle")
|
||||
.await
|
||||
.unwrap();
|
||||
db.update_workflow_status("wf_rot", "running").await.unwrap();
|
||||
db.upsert_grid_point(¶ms, 0, "wf_rot").await.unwrap();
|
||||
db.update_grid_status(&name, GridPointStatus::Failed, "wf_rot")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// 链尾 [seed_step_stab] 失败行。
|
||||
let t1 = uuid::Uuid::new_v4();
|
||||
db.insert_task(&common::models::TaskSpec {
|
||||
task_id: t1,
|
||||
point_name: name.clone(),
|
||||
params: params.clone(),
|
||||
timeout_sec: 7200,
|
||||
workflow_name: Some("wf_rot".to_string()),
|
||||
wave: 0,
|
||||
tlusty_config: PhaseConfig {
|
||||
strategies: vec!["seed_step_stab".to_string()],
|
||||
..PhaseConfig::default_tlusty()
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
{
|
||||
let pool = db.pool.clone();
|
||||
let tid = t1.to_string();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let conn = pool.get().unwrap();
|
||||
conn.execute(
|
||||
"UPDATE tasks SET status = 'failed', completed_at = datetime('now') \
|
||||
WHERE task_id = ?1",
|
||||
params![tid],
|
||||
)
|
||||
.unwrap();
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// 无同族种子 → 无可轮换 → 放行翻转。
|
||||
db.sync_all_running_workflows_completion().await.unwrap();
|
||||
assert_eq!(
|
||||
db.get_workflow("wf_rot").await.unwrap().unwrap().status,
|
||||
"completed",
|
||||
"无同族候选种子时链尾失败应放行翻转"
|
||||
);
|
||||
|
||||
// 重启场景:workflow 回 running,注入一个未用过的同族干净种子。
|
||||
db.update_workflow_status("wf_rot", "running").await.unwrap();
|
||||
let neighbor = GridPointParams {
|
||||
logn: (-3.489798).into(),
|
||||
..params.clone()
|
||||
};
|
||||
db.insert_seed_named(
|
||||
"t60000_g5_he-4_c-4_n-3.489798_o-1_ladder",
|
||||
&neighbor,
|
||||
"/tmp/seed.7",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.sync_all_running_workflows_completion().await.unwrap();
|
||||
assert_eq!(
|
||||
db.get_workflow("wf_rot").await.unwrap().unwrap().status,
|
||||
"running",
|
||||
"存在未试过的同族种子时,链尾 seed_step_stab 失败应阻塞翻转(rotation 待执行)"
|
||||
);
|
||||
|
||||
// 轮换消费该种子(任务行携带 seed_point_name)→ 候选耗尽 → 放行。
|
||||
let t2 = uuid::Uuid::new_v4();
|
||||
db.insert_task(&common::models::TaskSpec {
|
||||
task_id: t2,
|
||||
point_name: name.clone(),
|
||||
params: params.clone(),
|
||||
seed_point_name: Some("t60000_g5_he-4_c-4_n-3.489798_o-1_ladder".to_string()),
|
||||
timeout_sec: 7200,
|
||||
workflow_name: Some("wf_rot".to_string()),
|
||||
wave: 0,
|
||||
tlusty_config: PhaseConfig {
|
||||
strategies: vec!["seed_step_stab".to_string()],
|
||||
..PhaseConfig::default_tlusty()
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
{
|
||||
let pool = db.pool.clone();
|
||||
let tid = t2.to_string();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let conn = pool.get().unwrap();
|
||||
conn.execute(
|
||||
"UPDATE tasks SET status = 'failed', completed_at = datetime('now'), \
|
||||
created_at = datetime('now', '+1 hour') WHERE task_id = ?1",
|
||||
params![tid],
|
||||
)
|
||||
.unwrap();
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
db.sync_all_running_workflows_completion().await.unwrap();
|
||||
assert_eq!(
|
||||
db.get_workflow("wf_rot").await.unwrap().unwrap().status,
|
||||
"completed",
|
||||
"同族种子全部用过(无新候选)后应放行翻转"
|
||||
);
|
||||
}
|
||||
|
||||
/// 完成 flip 的阶段归因回归(2026-08-22 审查补充):synspec 失败行的
|
||||
/// tlusty_strategies 是完整审计副本(scheduler.rs synspec 回退刻意保留),
|
||||
/// 阻塞判定必须按 failed_stage='synspec' 只看 synspec 链——否则 synspec
|
||||
/// 工作流会因 stale 审计副本永久卡 running。
|
||||
#[tokio::test]
|
||||
async fn test_workflow_flip_stage_attribution_ignores_tlusty_audit_copy() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let db = Database::new(
|
||||
&temp_dir
|
||||
.path()
|
||||
.join("flip_syn_db.db")
|
||||
.to_string_lossy(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let params = GridPointParams {
|
||||
teff: 30000.0.into(),
|
||||
logg: 5.0.into(),
|
||||
loghe: (-2.0).into(),
|
||||
logc: (-2.0).into(),
|
||||
logn: (-2.0).into(),
|
||||
logo: (-2.0).into(),
|
||||
};
|
||||
let name = params.model_name();
|
||||
db.upsert_workflow("wf_syn2", None, "config", "idle")
|
||||
.await
|
||||
.unwrap();
|
||||
db.update_workflow_status("wf_syn2", "running").await.unwrap();
|
||||
db.upsert_grid_point(¶ms, 0, "wf_syn2").await.unwrap();
|
||||
db.update_grid_status(&name, GridPointStatus::Failed, "wf_syn2")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// synspec 失败行:synspec 链已耗尽(长度 1),但 tlusty 链是完整审计
|
||||
// 副本(长度 3)——阶段归因下不应阻塞。
|
||||
let t1 = uuid::Uuid::new_v4();
|
||||
db.insert_task(&common::models::TaskSpec {
|
||||
task_id: t1,
|
||||
point_name: name.clone(),
|
||||
params: params.clone(),
|
||||
timeout_sec: 7200,
|
||||
workflow_name: Some("wf_syn2".to_string()),
|
||||
wave: 0,
|
||||
tlusty_config: PhaseConfig {
|
||||
strategies: vec![
|
||||
"cold_run".to_string(),
|
||||
"seed_step".to_string(),
|
||||
"seed_step_stab".to_string(),
|
||||
],
|
||||
..PhaseConfig::default_tlusty()
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
{
|
||||
let pool = db.pool.clone();
|
||||
let tid = t1.to_string();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let conn = pool.get().unwrap();
|
||||
conn.execute(
|
||||
"UPDATE tasks SET status = 'failed', failed_stage = 'synspec', \
|
||||
synspec_strategies = '[\"standard\"]', completed_at = datetime('now') \
|
||||
WHERE task_id = ?1",
|
||||
params![tid],
|
||||
)
|
||||
.unwrap();
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
db.sync_all_running_workflows_completion().await.unwrap();
|
||||
assert_eq!(
|
||||
db.get_workflow("wf_syn2").await.unwrap().unwrap().status,
|
||||
"completed",
|
||||
"synspec 链已耗尽的失败行,其 tlusty 审计副本不应阻塞完成翻转"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_take_pending_node_token_atomic() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
@@ -3695,6 +4170,7 @@ mod tests {
|
||||
temp_check: None,
|
||||
emflux_check: None,
|
||||
bfac_check: None,
|
||||
ladder_seeds: Vec::new(),
|
||||
note: None,
|
||||
};
|
||||
let report1 = TaskReport {
|
||||
@@ -3761,6 +4237,7 @@ mod tests {
|
||||
temp_check: None,
|
||||
emflux_check: None,
|
||||
bfac_check: None,
|
||||
ladder_seeds: Vec::new(),
|
||||
note: None,
|
||||
};
|
||||
let report2 = TaskReport {
|
||||
@@ -3927,6 +4404,7 @@ mod tests {
|
||||
temp_check: None,
|
||||
emflux_check: None,
|
||||
bfac_check: None,
|
||||
ladder_seeds: Vec::new(),
|
||||
note: None,
|
||||
};
|
||||
let report1 = TaskReport {
|
||||
@@ -3984,6 +4462,7 @@ mod tests {
|
||||
temp_check: None,
|
||||
emflux_check: None,
|
||||
bfac_check: None,
|
||||
ladder_seeds: Vec::new(),
|
||||
note: Some("synspec 失败".into()),
|
||||
};
|
||||
let report2 = TaskReport {
|
||||
@@ -4108,6 +4587,7 @@ mod tests {
|
||||
temp_check: None,
|
||||
emflux_check: None,
|
||||
bfac_check: None,
|
||||
ladder_seeds: Vec::new(),
|
||||
note: None,
|
||||
};
|
||||
let report1 = TaskReport {
|
||||
@@ -4183,6 +4663,7 @@ mod tests {
|
||||
temp_check: None,
|
||||
emflux_check: None,
|
||||
bfac_check: None,
|
||||
ladder_seeds: Vec::new(),
|
||||
note: None,
|
||||
};
|
||||
let report2 = TaskReport {
|
||||
|
||||
@@ -196,4 +196,60 @@ impl Database {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// 仅在 exact_family(同 Teff/logg/logHe、仅 CNO 不同)内找种子,供
|
||||
/// `seed_step_stab` 稳定化策略使用(2026-08-18,docs/failed81_cno_seed_popzer_dpsilg_2026_08_18.md)。
|
||||
///
|
||||
/// 与 `find_best_seed_from_db` 的区别:不做 global 退化——稳定化配方的验证前提
|
||||
/// 是种子与目标仅差 CNO 丰度(微扰),跨 Teff/logg 的种子不在其适用域内。
|
||||
/// `exclude` 排除种子名列表(至少含目标点自身名,防止误把自己旧产物当种子)。
|
||||
/// 2026-08-20 种子轮换修复:稳定化配方对种子**逐点敏感**——同 CNO 距离的不同
|
||||
/// 邻居(换 N 还是换 O)收敛性不同。调用方传入本点历史任务已用过的种子名,
|
||||
/// 使每次 seed_step_stab 重试轮换到下一个未试过的同族邻居,而非重复同一组合。
|
||||
pub async fn find_exact_family_seed_from_db(
|
||||
&self,
|
||||
target: &GridPointParams,
|
||||
exclude: &[String],
|
||||
) -> Result<Option<common::seed_finder::SeedMatch>> {
|
||||
let exact_candidates: Vec<SeedCacheItem> = {
|
||||
let idx_lock = self.seed_index.read().await;
|
||||
let keys = SeedBucketKey::from_params(target);
|
||||
let mut out = Vec::new();
|
||||
for key in keys {
|
||||
if let Some(bucket) = idx_lock.get(&key) {
|
||||
out.extend(bucket.iter().cloned());
|
||||
}
|
||||
}
|
||||
out
|
||||
};
|
||||
|
||||
let mut best: Option<(String, std::path::PathBuf, f64)> = None;
|
||||
for item in &exact_candidates {
|
||||
if exclude.iter().any(|ex| &item.point_name == ex) {
|
||||
continue;
|
||||
}
|
||||
// 严格同物理族(2026-08-19 生产修复):必须 Teff/logg/logHe 逐值相同、
|
||||
// 仅 CNO 不同。不能用 calculate_seed_distance 的 is_exact——其判定
|
||||
// 容忍 ΔTeff≤5000K(一个网格档),会把 ladder 中间种子(如
|
||||
// t22500_g6.5_...)当成同族并因 CNO 距离 0 排最前,而稳定化配方
|
||||
// (POPZER+DPSILG)的验证前提是种子与目标仅差丰度微扰、同温同重力。
|
||||
let strict_same_family = (item.params.teff.value() - target.teff.value()).abs()
|
||||
< 1e-9
|
||||
&& (item.params.logg.value() - target.logg.value()).abs() < 1e-9
|
||||
&& (item.params.loghe.value() - target.loghe.value()).abs() < 1e-9;
|
||||
if !strict_same_family {
|
||||
continue;
|
||||
}
|
||||
let (_, d) = common::seed_finder::calculate_seed_distance(&item.params, target);
|
||||
if best.is_none() || d < best.as_ref().unwrap().2 {
|
||||
let path = std::path::PathBuf::from(&item.file_path);
|
||||
best = Some((item.point_name.clone(), path, d));
|
||||
}
|
||||
}
|
||||
Ok(best.map(|(name, path, distance)| common::seed_finder::SeedMatch {
|
||||
name,
|
||||
path,
|
||||
distance,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,6 +292,33 @@ impl Database {
|
||||
/// `strategy` 传 Some("seed_step")/Some("cold_run") 时按当前策略过滤(Phase 6 起派生:
|
||||
/// 比较 `json_extract(tlusty_strategies, '$[0]')`,不再依赖已删除的 task_type 列),
|
||||
/// None 不过滤。生产调用仅传 None,Some 分支供测试断言用。
|
||||
/// 本点历史任务已用过的全部种子名(DISTINCT、非 NULL)。
|
||||
/// 2026-08-20 种子轮换修复:`seed_step_stab` 重试时排除这些种子,
|
||||
/// 使 `find_exact_family_seed_from_db` 轮换到未试过的同族 CNO 邻居。
|
||||
pub async fn list_used_seed_names(
|
||||
&self,
|
||||
point_name: &str,
|
||||
workflow_name: &str,
|
||||
) -> Result<Vec<String>> {
|
||||
let pool = self.pool.clone();
|
||||
let point = point_name.to_string();
|
||||
let wf = workflow_name.to_string();
|
||||
tokio::task::spawn_blocking(move || -> Result<Vec<String>> {
|
||||
let conn = pool.get().map_err(|e| anyhow::anyhow!("DB Pool Error: {}", e))?;
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT DISTINCT seed_point_name FROM tasks
|
||||
WHERE point_name = ?1 AND workflow_name = ?2 AND seed_point_name IS NOT NULL",
|
||||
)?;
|
||||
let rows = stmt.query_map(params![point, wf], |r| r.get::<_, String>(0))?;
|
||||
let mut out = Vec::new();
|
||||
for r in rows {
|
||||
out.push(r?);
|
||||
}
|
||||
Ok(out)
|
||||
})
|
||||
.await?
|
||||
}
|
||||
|
||||
pub async fn has_pending_tasks_for_point(
|
||||
&self,
|
||||
point_name: &str,
|
||||
@@ -621,8 +648,14 @@ impl Database {
|
||||
// 审查修复 #S1:workflow 完成 flip 并入同一事务(原实现在 tx.commit() 后单独 UPDATE,
|
||||
// 崩溃窗口期 task 已终态但 workflow 卡 running;且 `let _ =` 丢弃 I/O 错误)。
|
||||
// 现在事务内更新,与 task/grid_points 结算原子提交,错误正常传播。
|
||||
//
|
||||
// 2026-08-22 修复:「无 pending/queued/running」之外还须无「未消费回退链」的
|
||||
// failed 点(UNCONSUMED_FALLBACK_BLOCKER_SQL)——否则最后一个活跃点 cold_run
|
||||
// 失败的这份上报会先把 workflow 置 completed,紧随其后的
|
||||
// trigger_strategy_fallback 被 still_running 守卫拦截,链上后续策略永不派发。
|
||||
tx.execute(
|
||||
"UPDATE workflows
|
||||
&format!(
|
||||
"UPDATE workflows
|
||||
SET status = 'completed', updated_at = datetime('now')
|
||||
WHERE name = ?1
|
||||
AND status = 'running'
|
||||
@@ -631,7 +664,10 @@ impl Database {
|
||||
SELECT 1 FROM grid_points
|
||||
WHERE workflow_name = workflows.name
|
||||
AND status IN ('pending', 'queued', 'running')
|
||||
)",
|
||||
)
|
||||
{}",
|
||||
super::workflows::UNCONSUMED_FALLBACK_BLOCKER_SQL
|
||||
),
|
||||
params![wf],
|
||||
)?;
|
||||
|
||||
|
||||
@@ -2,6 +2,78 @@
|
||||
//! `impl Database` 的 工作流 域方法。共享基础设施(Database struct、连接管理、类型、helper)见父模块 `super`(crate::db)。
|
||||
use super::*;
|
||||
|
||||
/// 工作流完成 flip 的「未消费回退」阻塞子句(两条 flip SQL 共用:
|
||||
/// 本文件后台对账 + tasks.rs `record_task_report` 事务内 flip)。
|
||||
///
|
||||
/// 2026-08-22 修复:最后一批活跃点失败时,flip 只看「无 pending/queued/running」,
|
||||
/// 而 failed 点的失败上报**先于**策略链回退被处理——workflow 被提前置 completed,
|
||||
/// `trigger_strategy_fallback` 的 still_running 守卫随即拦截,cold_run 之后的
|
||||
/// seed_step/seed_step_stab 永不派发(生产实证:sdB_cno 仅剩
|
||||
/// t60000_g5.0_he-4_c-4_n-4_o-1 时 cold 失败即终局)。
|
||||
///
|
||||
/// 阻塞语义(对每个 failed 点取最新 failed/timeout 任务行,行选择口径镜像
|
||||
/// `pop_stage_strategy_for_fallback`:ORDER BY created_at DESC, rowid DESC):
|
||||
///
|
||||
/// 1. **未消费策略链**:按该行 failed_stage 归因(镜像 pop 的 stage→列映射,
|
||||
/// NULL 归因 tlusty)取对应策略链,json 长度 >1——弹掉刚失败首项后仍有
|
||||
/// 顺位。链随每次失败上报严格变短,终会耗尽,不会永久阻塞。
|
||||
/// 2. **种子轮换待执行**(2026-08-22 审查补充):链尾 [`seed_step_stab`] 失败
|
||||
/// 且仍存在未试过的同物理族干净种子(teff/logg/loghe 逐值相等、is_clean=1、
|
||||
/// 非自身、不在本点历史 seed_point_name 集合内)——镜像 scheduler.rs
|
||||
/// rotation 臂(trigger_strategy_fallback 内)的派发条件,否则链尾失败时
|
||||
/// flip 仍会先于 rotation 吞掉换种重试。轮换每次消耗一个种子,used 集合
|
||||
/// 单调增长,终会耗尽同族种子,不会永久阻塞。
|
||||
///
|
||||
/// JSON 防护:json_array_length/json_extract 对非法 JSON **抛错**而非返回 NULL
|
||||
/// (SQLite 3.45 实测)——若不加防护,一条脏行会让 record_task_report 整个
|
||||
/// 结算事务失败。列 NOT NULL DEFAULT 且唯一写入方是 serde_json,正常不可达;
|
||||
/// json_valid 守卫下脏行按长度 0 处理(保守放行翻转)。
|
||||
pub(crate) const UNCONSUMED_FALLBACK_BLOCKER_SQL: &str = "AND NOT EXISTS (
|
||||
SELECT 1 FROM grid_points gp
|
||||
JOIN tasks t
|
||||
ON t.point_name = gp.name
|
||||
AND t.workflow_name = gp.workflow_name
|
||||
AND t.status IN ('failed', 'timeout')
|
||||
AND t.rowid = (
|
||||
SELECT t2.rowid FROM tasks t2
|
||||
WHERE t2.point_name = gp.name
|
||||
AND t2.workflow_name = gp.workflow_name
|
||||
AND t2.status IN ('failed', 'timeout')
|
||||
ORDER BY t2.created_at DESC, t2.rowid DESC LIMIT 1
|
||||
)
|
||||
WHERE gp.workflow_name = workflows.name
|
||||
AND gp.status = 'failed'
|
||||
AND (
|
||||
CASE WHEN t.failed_stage = 'synspec'
|
||||
THEN CASE WHEN json_valid(t.synspec_strategies)
|
||||
THEN json_array_length(t.synspec_strategies)
|
||||
ELSE 0 END
|
||||
ELSE CASE WHEN json_valid(t.tlusty_strategies)
|
||||
THEN json_array_length(t.tlusty_strategies)
|
||||
ELSE 0 END
|
||||
END > 1
|
||||
OR (
|
||||
json_valid(t.tlusty_strategies)
|
||||
AND json_array_length(t.tlusty_strategies) = 1
|
||||
AND json_extract(t.tlusty_strategies, '$[0]') = 'seed_step_stab'
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM seeds s
|
||||
WHERE s.is_clean = 1
|
||||
AND s.point_name != gp.name
|
||||
AND s.teff = gp.teff
|
||||
AND s.logg = gp.logg
|
||||
AND s.loghe = gp.loghe
|
||||
AND s.point_name NOT IN (
|
||||
SELECT t3.seed_point_name FROM tasks t3
|
||||
WHERE t3.point_name = gp.name
|
||||
AND t3.workflow_name = gp.workflow_name
|
||||
AND t3.seed_point_name IS NOT NULL
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)";
|
||||
|
||||
impl Database {
|
||||
pub async fn upsert_workflow(
|
||||
&self,
|
||||
@@ -47,8 +119,10 @@ impl Database {
|
||||
// L6 修复:原 `let _ =` 丢弃 r2d2/rusqlite 错误——若该后台对账 UPDATE 失败,
|
||||
// 工作流可持续卡在 running 无任何提示。现记录错误(仍返回 Ok 不中断主流程,
|
||||
// 因为每份上报内的 workflow-completion flip 才是主路径,见 record_task_report)。
|
||||
// 「未消费回退链」子句见 UNCONSUMED_FALLBACK_BLOCKER_SQL(2026-08-22 修复)。
|
||||
if let Err(e) = conn.execute(
|
||||
"UPDATE workflows
|
||||
&format!(
|
||||
"UPDATE workflows
|
||||
SET status = 'completed', updated_at = datetime('now')
|
||||
WHERE status = 'running'
|
||||
AND EXISTS (SELECT 1 FROM grid_points WHERE workflow_name = workflows.name)
|
||||
@@ -56,7 +130,10 @@ impl Database {
|
||||
SELECT 1 FROM grid_points
|
||||
WHERE workflow_name = workflows.name
|
||||
AND status IN ('pending', 'queued', 'running')
|
||||
)",
|
||||
)
|
||||
{}",
|
||||
UNCONSUMED_FALLBACK_BLOCKER_SQL
|
||||
),
|
||||
[],
|
||||
) {
|
||||
tracing::error!("后台对账:同步 running 工作流完成态失败: {}", e);
|
||||
@@ -90,6 +167,7 @@ impl Database {
|
||||
}
|
||||
|
||||
// 一次 GROUP BY 聚合全部工作流的网格计数并回填(不做逐工作流查询,无 N+1)。
|
||||
// tlusty 总收敛与 stab 分项为 2026-08-25 补(口径见 WorkflowStats.tlusty_converged)。
|
||||
let mut agg_stmt = conn.prepare(
|
||||
"SELECT workflow_name,
|
||||
COUNT(*) AS total,
|
||||
@@ -98,6 +176,8 @@ impl Database {
|
||||
SUM(CASE WHEN status = 'running' THEN 1 ELSE 0 END) AS running,
|
||||
SUM(CASE WHEN tlusty_status = 'converged' AND tlusty_success_method = 'cold_run' THEN 1 ELSE 0 END) AS cold,
|
||||
SUM(CASE WHEN tlusty_status = 'converged' AND tlusty_success_method = 'seed_step' THEN 1 ELSE 0 END) AS seed,
|
||||
SUM(CASE WHEN tlusty_status = 'converged' THEN 1 ELSE 0 END) AS tlusty_converged,
|
||||
SUM(CASE WHEN tlusty_status = 'converged' AND tlusty_success_method = 'seed_step_stab' THEN 1 ELSE 0 END) AS stab,
|
||||
SUM(CASE WHEN tlusty_status = 'failed' THEN 1 ELSE 0 END) AS tlusty_failed,
|
||||
SUM(CASE WHEN synspec_status = 'converged' THEN 1 ELSE 0 END) AS synspec_converged,
|
||||
SUM(CASE WHEN synspec_status = 'failed' THEN 1 ELSE 0 END) AS synspec_failed,
|
||||
@@ -114,10 +194,12 @@ impl Database {
|
||||
running: r.get::<_, Option<i64>>(4)?.unwrap_or(0),
|
||||
cold_run_converged: r.get::<_, Option<i64>>(5)?.unwrap_or(0),
|
||||
seed_step_converged: r.get::<_, Option<i64>>(6)?.unwrap_or(0),
|
||||
tlusty_failed: r.get::<_, Option<i64>>(7)?.unwrap_or(0),
|
||||
synspec_converged: r.get::<_, Option<i64>>(8)?.unwrap_or(0),
|
||||
synspec_failed: r.get::<_, Option<i64>>(9)?.unwrap_or(0),
|
||||
synspec_pending: r.get::<_, Option<i64>>(10)?.unwrap_or(0),
|
||||
tlusty_converged: r.get::<_, Option<i64>>(7)?.unwrap_or(0),
|
||||
seed_step_stab_converged: r.get::<_, Option<i64>>(8)?.unwrap_or(0),
|
||||
tlusty_failed: r.get::<_, Option<i64>>(9)?.unwrap_or(0),
|
||||
synspec_converged: r.get::<_, Option<i64>>(10)?.unwrap_or(0),
|
||||
synspec_failed: r.get::<_, Option<i64>>(11)?.unwrap_or(0),
|
||||
synspec_pending: r.get::<_, Option<i64>>(12)?.unwrap_or(0),
|
||||
},
|
||||
))
|
||||
})?;
|
||||
|
||||
@@ -176,6 +176,32 @@ pub const MIGRATIONS: &[Migration] = &[
|
||||
detect: |c| Ok(!has_column(c, "grid_points", "success_method")?),
|
||||
up: &["ALTER TABLE grid_points DROP COLUMN success_method"],
|
||||
},
|
||||
// M14(2026-08-25,大气收敛统计补漏):Phase 5b 阶段列(tlusty_status)上线前完成的历史
|
||||
// 点该列为 NULL——但 tlusty_success_method 仅在整管线成功(tlusty_enabled=1 且收敛)时
|
||||
// 写入(record_task_report 成功分支),故 completed + method 非空 ⟹ 大气必已收敛。
|
||||
// 回填 'converged',否则 stats 的 CASE WHEN tlusty_status='converged' 漏计
|
||||
// (生产实证:sdB_cno 9216 点中 9 个此形态,前端 9137/9216 差额的一部分)。
|
||||
Migration {
|
||||
version: 14,
|
||||
name: "backfill-tlusty-status-for-completed",
|
||||
detect: |c| {
|
||||
let n: i64 = c.query_row(
|
||||
"SELECT COUNT(*) FROM grid_points
|
||||
WHERE status = 'completed'
|
||||
AND COALESCE(tlusty_success_method, '') != ''
|
||||
AND COALESCE(tlusty_status, '') = ''",
|
||||
[],
|
||||
|r| r.get(0),
|
||||
)?;
|
||||
Ok(n == 0)
|
||||
},
|
||||
up: &[
|
||||
"UPDATE grid_points SET tlusty_status = 'converged' \
|
||||
WHERE status = 'completed' \
|
||||
AND COALESCE(tlusty_success_method, '') != '' \
|
||||
AND COALESCE(tlusty_status, '') = ''",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
/// 当前 schema 版本(`PRAGMA user_version`)。
|
||||
@@ -562,4 +588,54 @@ mod tests {
|
||||
);
|
||||
assert_eq!(current_version(&conn).unwrap(), 13);
|
||||
}
|
||||
|
||||
/// M14(2026-08-25):completed + tlusty_success_method 非空 + tlusty_status 空
|
||||
/// 的历史点回填 'converged'(method 仅在整管线成功时写入);其它形态不动。
|
||||
#[test]
|
||||
fn m14_backfills_tlusty_status_for_legacy_completed() {
|
||||
let mut conn = mem_conn();
|
||||
conn.execute_batch(
|
||||
"CREATE TABLE grid_points (
|
||||
name TEXT NOT NULL,
|
||||
workflow_name TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
tlusty_success_method TEXT,
|
||||
synspec_success_method TEXT,
|
||||
tlusty_status TEXT,
|
||||
synspec_status TEXT,
|
||||
pending_strategies TEXT
|
||||
);
|
||||
INSERT INTO grid_points (name, workflow_name, status, tlusty_success_method, tlusty_status) VALUES
|
||||
('legacy_ok', 'wf', 'completed', 'cold_run', NULL),
|
||||
('legacy_seed', 'wf', 'completed', 'seed_step', ''),
|
||||
('already_failed', 'wf', 'failed', 'cold_run', 'failed'),
|
||||
('already_conv', 'wf', 'completed', 'cold_run', 'converged'),
|
||||
('no_method', 'wf', 'completed', NULL, NULL);",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// 仅跑 M14(全量 MIGRATIONS 含 M1 的 tasks 迁移,本夹具只建 grid_points)。
|
||||
let m14 = MIGRATIONS.iter().find(|m| m.version == 14).unwrap();
|
||||
apply_migrations_with(&mut conn, &[*m14]).unwrap();
|
||||
|
||||
let read = |c: &Connection, name: &str| -> Option<String> {
|
||||
c.query_row(
|
||||
"SELECT tlusty_status FROM grid_points WHERE name = ?1",
|
||||
rusqlite::params![name],
|
||||
|r| r.get(0),
|
||||
)
|
||||
.unwrap()
|
||||
};
|
||||
assert_eq!(read(&conn, "legacy_ok").as_deref(), Some("converged"));
|
||||
assert_eq!(read(&conn, "legacy_seed").as_deref(), Some("converged"));
|
||||
// failed 点 / 已有状态 / 无 method 的点不受影响。
|
||||
assert_eq!(read(&conn, "already_failed").as_deref(), Some("failed"));
|
||||
assert_eq!(read(&conn, "already_conv").as_deref(), Some("converged"));
|
||||
assert_eq!(read(&conn, "no_method"), None);
|
||||
assert_eq!(current_version(&conn).unwrap(), 14);
|
||||
|
||||
// 幂等:再跑一遍 detect 命中(无待回填行)→ 仅推进版本,不改数据。
|
||||
apply_migrations_with(&mut conn, &[*m14]).unwrap();
|
||||
assert_eq!(read(&conn, "no_method"), None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -360,6 +360,7 @@ impl GridScheduler {
|
||||
/// 「弹出再压回」保持原序而陷入死循环。
|
||||
async fn resolve_dispatchable_chain(
|
||||
db: &Database,
|
||||
workflow_name: &str,
|
||||
params: &GridPointParams,
|
||||
mut chain: Vec<String>,
|
||||
) -> Result<(Vec<String>, Option<String>)> {
|
||||
@@ -394,6 +395,36 @@ impl GridScheduler {
|
||||
}
|
||||
}
|
||||
}
|
||||
// 稳定化种子步进(2026-08-18,docs/failed81_cno_seed_popzer_dpsilg_2026_08_18.md):
|
||||
// 仅在 exact_family(同 Teff/logg/logHe、仅 CNO 不同)内找种子——稳定化配方
|
||||
// (POPZER+DPSILG)的验证前提是种子与目标仅差丰度微扰,跨 Teff/logg 种子
|
||||
// 不在适用域。排除目标点自身名(防止把自己旧产物当种子)。
|
||||
// 2026-08-20 种子轮换:配方对种子逐点敏感(同距离换 N/换 O 邻居收敛性不同),
|
||||
// 排除本点历史任务已用过的种子,重试时轮换到下一个未试过的同族邻居。
|
||||
Some("seed_step_stab") => {
|
||||
let mut exclude = vec![params.model_name()];
|
||||
exclude.extend(db.list_used_seed_names(¶ms.model_name(), workflow_name).await?);
|
||||
let found_seed = db
|
||||
.find_exact_family_seed_from_db(params, &exclude)
|
||||
.await?;
|
||||
match found_seed {
|
||||
Some(seed) => {
|
||||
if !skipped.is_empty() {
|
||||
chain.extend(skipped);
|
||||
}
|
||||
return Ok((chain, Some(seed.name.clone())));
|
||||
}
|
||||
None if chain.len() > 1 => {
|
||||
let head = chain.remove(0);
|
||||
skipped.push(head);
|
||||
info!("策略解析:seed_step_stab 无同物理族 CNO 邻居种子,暂存顺位");
|
||||
}
|
||||
None => {
|
||||
chain.clear();
|
||||
return Ok((chain, None));
|
||||
}
|
||||
}
|
||||
}
|
||||
// 空链或非 seed_step 顺位:可直接派发。先前跳过的 seed_step 追加到链尾,
|
||||
// 保留为后续回退顺位(空链由调用方判空处理)。
|
||||
_ => {
|
||||
@@ -561,7 +592,7 @@ impl GridScheduler {
|
||||
};
|
||||
let (dispatch_chain, seed_point_name) = if tlusty_cfg.enabled {
|
||||
let (chain, seed) =
|
||||
Self::resolve_dispatchable_chain(&self.db, ¶ms, base_chain).await?;
|
||||
Self::resolve_dispatchable_chain(&self.db, workflow_name, ¶ms, base_chain).await?;
|
||||
if chain.is_empty() {
|
||||
// enabled 阶段无可派发顺位(基础链全为无种子的 seed_step)→ 打回 pending,
|
||||
// 待近邻种子出现后由下轮调度自愈派发(不是终态,不能被卡死在 queued)。
|
||||
@@ -896,13 +927,6 @@ impl GridScheduler {
|
||||
popped,
|
||||
policy: row_policy,
|
||||
} = snap;
|
||||
if rest_strategies.is_empty() {
|
||||
info!(
|
||||
"策略回退:网格点 {} 的 {} 策略链已耗尽(弹出最后项 {}),保持 failed 终态",
|
||||
name, stage_label, popped
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// 2026-08-04 语义修正:策略链回退**只由策略链驱动**(弹出失败首项、派发下一顺位,
|
||||
// 链耗尽保持 failed),不再受执行策略门控——策略只决定启动工作流时对历史终态点的
|
||||
@@ -929,6 +953,83 @@ impl GridScheduler {
|
||||
.await
|
||||
.unwrap_or((None, None, None, None, None, None, None, None));
|
||||
|
||||
// 2026-08-20 种子轮换:seed_step_stab 常为链上最后一项,失败弹出后链空。
|
||||
// 稳定化配方对种子逐点敏感(同 CNO 距离、换不同元素方向的邻居收敛性不同,
|
||||
// 见 docs/failed81_cno_seed_popzer_dpsilg_2026_08_18.md §九)——首个选中
|
||||
// 的邻居失败不代表配方无效。排除本点历史已用种子后若还有未试过的同族
|
||||
// 邻居,则以 seed_step_stab 单顺位重链重派。轮换次数天然受同族邻居数约束:
|
||||
// 全部试过后此处不再命中,落入下方 failed 终态,无死循环。
|
||||
if rest_strategies.is_empty() && failed_stage != "synspec" && popped == "seed_step_stab" {
|
||||
let mut exclude = vec![params.model_name()];
|
||||
exclude.extend(self.db.list_used_seed_names(name, workflow_name).await?);
|
||||
if let Some(fresh_seed) = self
|
||||
.db
|
||||
.find_exact_family_seed_from_db(params, &exclude)
|
||||
.await?
|
||||
{
|
||||
info!(
|
||||
"策略回退:工作流 {} 网格点 {} seed_step_stab 失败但存在未试过的同族邻居种子 {},轮换种子重派(已试过 {:?})",
|
||||
workflow_name, name, fresh_seed.name, &exclude[1..]
|
||||
);
|
||||
// 轮换任务显式注入新种子(绕过 resolve 的种子查找,避免其再次
|
||||
// 命中同一邻居)。
|
||||
let mut stab_cfg = tlusty_cfg.clone();
|
||||
stab_cfg.strategies = vec!["seed_step_stab".to_string()];
|
||||
stab_cfg.policy = row_policy.clone();
|
||||
let task_spec = TaskSpec {
|
||||
task_id: Uuid::new_v4(),
|
||||
point_name: name.to_string(),
|
||||
params: params.clone(),
|
||||
seed_point_name: Some(fresh_seed.name),
|
||||
timeout_sec,
|
||||
workflow_name: Some(workflow_name.to_string()),
|
||||
wave: 0,
|
||||
tlusty_config: stab_cfg,
|
||||
synspec_config: synspec_cfg.clone(),
|
||||
synspec_params,
|
||||
tlusty_chain_params: tlusty_chain.clone(),
|
||||
seed_chain_params: seed_chain.clone(),
|
||||
tlusty_input_params: tlusty_input.clone(),
|
||||
atmosphere_ref: None,
|
||||
energy_tolerance,
|
||||
temp_max_factor,
|
||||
temp_floor,
|
||||
temp_ceiling,
|
||||
emflux_tolerance,
|
||||
convergence_min_ratio,
|
||||
bfac_max,
|
||||
bfac_min,
|
||||
linelist: linelist.clone(),
|
||||
};
|
||||
self.db.insert_task(&task_spec).await?;
|
||||
self.db
|
||||
.update_grid_status(name, common::models::GridPointStatus::Queued, workflow_name)
|
||||
.await?;
|
||||
if let Err(e) = self.queue.push_task(&task_spec).await {
|
||||
let _ = self
|
||||
.db
|
||||
.update_grid_status(
|
||||
name,
|
||||
common::models::GridPointStatus::Pending,
|
||||
workflow_name,
|
||||
)
|
||||
.await;
|
||||
let _ = self.queue.remove_task(&task_spec.task_id.to_string()).await;
|
||||
let _ = self.db.delete_task(&task_spec.task_id).await;
|
||||
return Err(e);
|
||||
}
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
|
||||
if rest_strategies.is_empty() {
|
||||
info!(
|
||||
"策略回退:网格点 {} 的 {} 策略链已耗尽(弹出最后项 {}),保持 failed 终态",
|
||||
name, stage_label, popped
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// SYNSPEC 链回退:重试光谱合成。无邻居种子门控(大气来自目标点自身既有产物,
|
||||
// 见 docs/task_engine_decoupling_design.md §5)——旧实现把 synspec 失败误归因到
|
||||
// TLUSTY 链并做无意义的种子搜索(修复审查 #2)。
|
||||
@@ -1011,7 +1112,7 @@ impl GridScheduler {
|
||||
// pending_rest:resolve 前的基础剩余链(含全部 seed_step 顺位),H1 空链分支据此记录标记。
|
||||
let pending_rest = rest_strategies.clone();
|
||||
let (rest_strategies, seed_point_name) =
|
||||
Self::resolve_dispatchable_chain(&self.db, params, rest_strategies).await?;
|
||||
Self::resolve_dispatchable_chain(&self.db, workflow_name, params, rest_strategies).await?;
|
||||
if rest_strategies.is_empty() {
|
||||
// H1 修复:resolve_dispatchable_chain 仅在「剩余顺位全为无近邻种子的 seed_step」
|
||||
// 时返回空(调用方已在上方 line 790 排除了真耗尽)。此时**不能**保持 failed 终态:
|
||||
@@ -2909,6 +3010,7 @@ tlusty_stage:\n enabled: true\n policy: skip_converged\n strategies: [seed_st
|
||||
// 无种子 → 重复 seed_step 链应判空(不派发、不死循环)。
|
||||
let (chain, seed) = GridScheduler::resolve_dispatchable_chain(
|
||||
&db,
|
||||
"wf",
|
||||
¶ms,
|
||||
vec!["seed_step".to_string(), "seed_step".to_string()],
|
||||
)
|
||||
@@ -2921,6 +3023,7 @@ tlusty_stage:\n enabled: true\n policy: skip_converged\n strategies: [seed_st
|
||||
// 两个无种子 seed_step 保留为回退顺位且链首可派发。
|
||||
let (chain2, seed2) = GridScheduler::resolve_dispatchable_chain(
|
||||
&db,
|
||||
"wf",
|
||||
¶ms,
|
||||
vec![
|
||||
"seed_step".to_string(),
|
||||
|
||||
@@ -28,6 +28,7 @@ async fn mark_imported(db: &Database, name: &str, wf: &str, params: &GridPointPa
|
||||
temp_check: None,
|
||||
emflux_check: None,
|
||||
bfac_check: None,
|
||||
ladder_seeds: Vec::new(),
|
||||
note: None,
|
||||
};
|
||||
db.upsert_point_summary(name, wf, &summary, method)
|
||||
@@ -1866,6 +1867,7 @@ async fn dispatch_and_report(
|
||||
temp_check: None,
|
||||
emflux_check: None,
|
||||
bfac_check: None,
|
||||
ladder_seeds: Vec::new(),
|
||||
note: None,
|
||||
};
|
||||
let report = common::models::TaskReport {
|
||||
|
||||
@@ -41,6 +41,7 @@ fn make_converged_summary(name: &str, params: &GridPointParams) -> ModelSummary
|
||||
temp_check: None,
|
||||
emflux_check: None,
|
||||
bfac_check: None,
|
||||
ladder_seeds: Vec::new(),
|
||||
note: None,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user