物理正确性校验体系(common/conv_check.rs +494 行) - 新增 5 类硬门槛:能量守恒(.6)、温度结构(.7)、emflux 积分校验(.emflux,含全 NaN 判失败)、假收敛排查(itek 轨迹首末比)、b 因子合理性(.bfac) - runner 在 TLUSTY 阶段结束后执行全部校验,任一失败判 final_converged=false - GridConfig 新增 8 个可配阈值,经 scheduler→executor→runner 全链路透传 输入文件配置结构化重构(config.rs +1453 行) - TlustyInput 拆为 dot5/nst 分层结构,字段名严格映射 tlusty208.f READ 语句;SynspecInput 重构为 9 个 Fort55Line 子结构体 - 移除 ChainStep.metals 字段,元素集改由 dot5.atoms/ions 显式声明(gen_input5/nst_writer 同步重写为三源融合 / 分层覆盖) - fort.55 修复行结构 bug:补全分子表行(7→9 行),IDSTD 50→0 错位修正(影响全部光谱线强归一化,需重算 SYNSPEC 阶段) conv 诊断 DB 化与阶段归因修复(server) - 单点详情 conv 面板从磁盘 conv.json 改读 DB grid_points.summary_json;grid_points 新增 summary_json/last_elapsed_sec 两列(旧库幂等 ALTER) - record_task_report 阶段归因列加 CASE 守卫 + clear_synspec 对称处理,修复 synspec-only/TLUSTY-only 重跑污染统计 - 新增 summary_merge.rs 点级增量合并,避免重跑覆盖诊断字段 收敛性 ORELAX 修复与 seed_chain 可配(sdB_cno.yaml + node) - nl 阶段加 orelax=0.5、seed_nc 加 orelax=0.3,阻尼中温区 relc 振荡发散 - seed_chain 块可配,executor 优先采用用户配置而非内置默认链 导入工具下线 - 删除 import_results 客户端工具及 Windows 推送脚本;移除 /admin/import_seed 端点 - 改为服务端临时 migrate_conv 端点(扫 conv.json 增量合并入库,迁移后可删) 文档与分析 - 新增 1305 失败点根因分析、fort.14 全 NaN 物理含义分析两份深度文档 - spectrum_correctness_analysis 两次修订标注已修复项;fetch_results.sh 修 trap RETURN 的 set -u 报错
221 lines
8.7 KiB
Rust
221 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,
|
||
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 保留,避免重算");
|
||
}
|