Files
DCTS/crates/server/tests/wf_migration_isolation.rs
T
fmq f2700031ce 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 残点实测矩阵
2026-09-02 19:37:25 +08:00

222 lines
8.7 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 验证「历史种子导入的工作流名」与「正式工作流名」的隔离关系。
//!
//! 用户意图:离线导入工具把旧计算结果导入,标记为已完成,避免重算。
//! 关键问题:导入到工作流 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),
}
}
/// 构造一个收敛的 ModelSummaryresult_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 的行保持 convergedON 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 保留,避免重算");
}