收敛攻坚(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 残点实测矩阵
222 lines
8.7 KiB
Rust
222 lines
8.7 KiB
Rust
//! 验证「历史种子导入的工作流名」与「正式工作流名」的隔离关系。
|
||
//!
|
||
//! 用户意图:离线导入工具把旧计算结果导入,标记为已完成,避免重算。
|
||
//! 关键问题:导入到工作流 A,之后正式启动工作流 B(同名/异名),B 能否看到 A 标记的 converged?
|
||
|
||
use common::config::GridConfig;
|
||
use common::models::{GridPointParams, ModelSummary};
|
||
use mq::sqlite_queue::SqliteTaskQueue;
|
||
use server::{db::Database, scheduler::GridScheduler};
|
||
use std::sync::Arc;
|
||
|
||
fn make_params() -> GridPointParams {
|
||
use common::models::GridAxisValue;
|
||
GridPointParams {
|
||
teff: GridAxisValue::from_value(20000.0),
|
||
logg: GridAxisValue::from_value(5.0),
|
||
loghe: GridAxisValue::from_value(-2.0),
|
||
logc: GridAxisValue::from_value(-4.0),
|
||
logn: GridAxisValue::from_value(-4.0),
|
||
logo: GridAxisValue::from_value(-4.0),
|
||
}
|
||
}
|
||
|
||
/// 构造一个收敛的 ModelSummary(result_valid=true, atmosphere_has_nan=false),
|
||
/// 用 point_name 作权威名。供测试模拟离线导入写入 summary_json。
|
||
fn make_converged_summary(name: &str, params: &GridPointParams) -> ModelSummary {
|
||
ModelSummary {
|
||
name: name.to_string(),
|
||
params: params.clone(),
|
||
stages: Vec::new(),
|
||
result_valid: true,
|
||
final_max_relc: Some(0.001),
|
||
final_chmax: Some(0.001),
|
||
seed: None,
|
||
atmosphere_has_nan: false,
|
||
synspec_rc: None,
|
||
synspec_error: None,
|
||
synspec_sec: None,
|
||
elapsed_sec: 0.0,
|
||
energy_check: None,
|
||
temp_check: None,
|
||
emflux_check: None,
|
||
bfac_check: None,
|
||
ladder_seeds: Vec::new(),
|
||
note: None,
|
||
}
|
||
}
|
||
|
||
/// 测试辅助:模拟离线导入——upsert 点 + 写收敛 summary(等价旧 mark_grid_point_imported)。
|
||
async fn mark_imported(
|
||
db: &Database,
|
||
name: &str,
|
||
workflow: &str,
|
||
params: &GridPointParams,
|
||
method: &str,
|
||
) {
|
||
db.upsert_grid_point_named(name, params, 0, workflow)
|
||
.await
|
||
.unwrap();
|
||
let summary = make_converged_summary(name, params);
|
||
db.upsert_point_summary(name, workflow, &summary, method)
|
||
.await
|
||
.unwrap();
|
||
}
|
||
|
||
/// 构造只含一个网格点(t20000_g5.0_he-2_c-4_n-4_o-4)的 config。
|
||
fn make_grid_cfg() -> GridConfig {
|
||
let yaml = "grid:\n teff: [20000]\n logg: [5.0]\n loghe: [-2]\n logc: [-4]\n logn: [-4]\n logo: [-4]\n";
|
||
GridConfig::from_yaml_str(yaml).unwrap()
|
||
}
|
||
|
||
async fn setup() -> (Database, Arc<GridScheduler>) {
|
||
let tmp = tempfile::tempdir().unwrap();
|
||
let db = Database::new(&tmp.path().join("db.db").to_string_lossy())
|
||
.await
|
||
.unwrap();
|
||
let queue = Arc::new(
|
||
SqliteTaskQueue::new(&tmp.path().join("q.db").to_string_lossy())
|
||
.await
|
||
.unwrap(),
|
||
);
|
||
let sched = Arc::new(GridScheduler::new(db.clone(), queue));
|
||
(db, sched)
|
||
}
|
||
|
||
/// 场景 1(正确用法):导入到工作流 "sdB_cno",再用同名 config initialize_grid。
|
||
/// 期望:initialize_grid 的 ON CONFLICT(workflow_name, name) DO NOTHING 保留 converged 状态。
|
||
#[tokio::test]
|
||
async fn test_same_workflow_name_preserves_converged() {
|
||
let (db, sched) = setup().await;
|
||
let name = "t20000_g5.0_he-2_c-4_n-4_o-4";
|
||
let p = make_params();
|
||
|
||
// 模拟离线导入:upsert + 写收敛 summary,工作流名 = sdB_cno
|
||
mark_imported(&db, name, "sdB_cno", &p, "cold_run").await;
|
||
|
||
// 之后正式启动同名工作流:initialize_grid(sdB_cno)
|
||
sched
|
||
.initialize_grid(&make_grid_cfg(), "sdB_cno")
|
||
.await
|
||
.unwrap();
|
||
|
||
let st = db.get_grid_point_status(name, "sdB_cno").await.unwrap();
|
||
assert_eq!(
|
||
st.unwrap().0,
|
||
"completed",
|
||
"同名工作流:导入的 converged 应被保留,避免重算"
|
||
);
|
||
println!("✓ 场景1(同名):status=converged,旧结果被保留,不会重算");
|
||
}
|
||
|
||
/// 场景 2(错误用法):导入到工作流 "imported",之后正式启动 "sdB_cno"。
|
||
/// 期望:sdB_cno 分区下是新插入的 pending 行,看不到 imported 分区的 converged。
|
||
#[tokio::test]
|
||
async fn test_different_workflow_name_causes_recompute() {
|
||
let (db, sched) = setup().await;
|
||
let name = "t20000_g5.0_he-2_c-4_n-4_o-4";
|
||
let p = make_params();
|
||
|
||
// 模拟离线导入:导入到 "imported" 工作流
|
||
mark_imported(&db, name, "imported", &p, "cold_run").await;
|
||
|
||
// 之后正式启动 "sdB_cno" 工作流
|
||
sched
|
||
.initialize_grid(&make_grid_cfg(), "sdB_cno")
|
||
.await
|
||
.unwrap();
|
||
|
||
// imported 分区:converged(种子库有,但不会被 sdB_cno 调度看到)
|
||
let st_imp = db.get_grid_point_status(name, "imported").await.unwrap();
|
||
assert_eq!(st_imp.unwrap().0, "completed");
|
||
|
||
// sdB_cno 分区:pending(重新算!看不到 imported 的 converged)
|
||
let st_real = db.get_grid_point_status(name, "sdB_cno").await.unwrap();
|
||
assert_eq!(
|
||
st_real.unwrap().0,
|
||
"pending",
|
||
"异名工作流:sdB_cno 看不到 imported 的 converged,会重算"
|
||
);
|
||
println!("✓ 场景2(异名):sdB_cno 分区 status=pending,会重复计算 —— 验证了工作流名必须匹配");
|
||
}
|
||
|
||
/// 场景 3(真实意图验证):旧网格有部分点已算完(导入为 converged),
|
||
/// 新网格比旧网格多了若干点。用同名工作流启动后:
|
||
/// - 旧点保持 converged(不重算)
|
||
/// - 新点是 pending(会被调度计算)
|
||
/// 这正是「同步旧结果避免重算」的核心语义。
|
||
#[tokio::test]
|
||
async fn test_mixed_grid_import_then_init_avoids_recompute() {
|
||
let (db, sched) = setup().await;
|
||
let p_old = make_params(); // t20000_g5.0_...
|
||
|
||
// 模拟离线导入:旧网格里这个点已收敛,导入到 sdB_cno
|
||
mark_imported(&db, "t20000_g5.0_he-2_c-4_n-4_o-4", "sdB_cno", &p_old, "cold_run").await;
|
||
|
||
// 正式启动 sdB_cno,config 比旧网格多了一个新点(t25000)
|
||
let yaml = "grid:\n teff: [20000, 25000]\n logg: [5.0]\n loghe: [-2]\n logc: [-4]\n logn: [-4]\n logo: [-4]\n";
|
||
let cfg = GridConfig::from_yaml_str(yaml).unwrap();
|
||
sched.initialize_grid(&cfg, "sdB_cno").await.unwrap();
|
||
|
||
// 旧点:converged(不重算)
|
||
let st_old = db
|
||
.get_grid_point_status("t20000_g5.0_he-2_c-4_n-4_o-4", "sdB_cno")
|
||
.await
|
||
.unwrap();
|
||
assert_eq!(
|
||
st_old.unwrap().0,
|
||
"completed",
|
||
"旧点应保持 converged 不重算"
|
||
);
|
||
|
||
// 新点:pending(会被调度)
|
||
let st_new = db
|
||
.get_grid_point_status("t25000_g5.0_he-2_c-4_n-4_o-4", "sdB_cno")
|
||
.await
|
||
.unwrap();
|
||
assert_eq!(st_new.unwrap().0, "pending", "新点应为 pending 等待计算");
|
||
|
||
println!("✓ 场景3(混合网格):旧点converged保留 + 新点pending待算 —— 完全符合避免重算的意图");
|
||
}
|
||
|
||
/// 场景 4(精度差异命门):旧 conv.json name=g5(无小数),但配置 logg=5.0 → model_name()=g5.0。
|
||
/// 导入工具必须把 name 重写为 g5.0 入库,否则 initialize_grid 插入的 g5.0 行与导入的 g5 行
|
||
/// 复合唯一键不匹配,导入的 converged 被孤立、g5.0 被重算。
|
||
/// 本测试直接模拟「入库的 grid_points.name = g5.0」(即工具重写后的状态),
|
||
/// 验证 initialize_grid(sdB_cno) 后该行保持 converged(不重算)。
|
||
#[tokio::test]
|
||
async fn test_precision_diff_import_then_init_preserves_converged() {
|
||
let (db, sched) = setup().await;
|
||
// 配置权威名(logg=5.0 → g5.0,保留小数)
|
||
let canonical = "t20000_g5.0_he-2_c-4_n-4_o-4";
|
||
use common::models::GridAxisValue;
|
||
let p = GridPointParams {
|
||
teff: GridAxisValue::from_value(20000.0),
|
||
logg: GridAxisValue::from_value(5.0),
|
||
loghe: GridAxisValue::from_value(-2.0),
|
||
logc: GridAxisValue::from_value(-4.0),
|
||
logn: GridAxisValue::from_value(-4.0),
|
||
logo: GridAxisValue::from_value(-4.0),
|
||
};
|
||
|
||
// 模拟离线导入重写 name 后入库:grid_points.name = canonical(g5.0)
|
||
mark_imported(&db, canonical, "sdB_cno", &p, "cold_run").await;
|
||
|
||
// 启动同名工作流:initialize_grid 用配置 model_name()(=g5.0) 插入
|
||
let yaml = "grid:\n teff: [20000]\n logg: [5.0]\n loghe: [-2]\n logc: [-4]\n logn: [-4]\n logo: [-4]\n";
|
||
let cfg = GridConfig::from_yaml_str(yaml).unwrap();
|
||
sched.initialize_grid(&cfg, "sdB_cno").await.unwrap();
|
||
|
||
// 关键断言:name=g5.0 的行保持 converged(ON CONFLICT DO NOTHING 命中)
|
||
let st = db
|
||
.get_grid_point_status(canonical, "sdB_cno")
|
||
.await
|
||
.unwrap();
|
||
assert_eq!(
|
||
st.unwrap().0,
|
||
"completed",
|
||
"精度一致(g5.0=g5.0)时导入的 converged 必须保留,不重算"
|
||
);
|
||
println!("✓ 场景4(精度差异命门):g5.0 入库 + initialize_grid → converged 保留,避免重算");
|
||
}
|