feat(all): 物理正确性五重硬门槛、输入文件结构化与 fort.55 错位修复、conv 诊断 DB 化与阶段归因修复、ORELAX 收敛修复与导入工具下线

物理正确性校验体系(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 报错
This commit is contained in:
fmq
2026-08-09 12:09:48 +08:00
parent d16b3d3cdc
commit 43b82b1ae2
45 changed files with 6059 additions and 3184 deletions
+66 -357
View File
@@ -2,11 +2,39 @@ use axum::{
body::Body,
http::{Request, StatusCode},
};
use common::models::{GridPointParams, ModelSummary};
use mq::sqlite_queue::SqliteTaskQueue;
use server::{api::AppState, db::Database, scheduler::GridScheduler};
use std::sync::Arc;
use tower::ServiceExt; // for oneshot
/// 测试辅助:把一个已 upsert 的点标记为导入收敛(写 summary_json + status=completed)。
/// 等价旧 mark_grid_point_imported。
async fn mark_imported(db: &Database, name: &str, wf: &str, params: &GridPointParams, method: &str) {
let summary = 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,
};
db.upsert_point_summary(name, wf, &summary, method)
.await
.unwrap();
}
#[tokio::test]
async fn test_server_api_flow() {
let temp_dir = tempfile::tempdir().unwrap();
@@ -1256,343 +1284,6 @@ async fn test_cors_same_origin_and_local_policy() {
assert!(res.headers().get("access-control-allow-origin").is_none());
}
/// 构造一个合法的旧版 conv.json (ModelSummary) 文本,用于 import_seed 测试。
/// 关键:name 用源精度真名(`g5.0`),验证服务端逐字符保真落库。
fn make_legacy_conv_json(name: &str, converged: bool) -> String {
let summary = serde_json::json!({
"name": name,
"params": {"teff": 20000.0, "logg": 5.0, "loghe": -2.0, "logc": -4.0, "logn": -4.0, "logo": -4.0},
"stages": [],
"converged": converged,
"final_max_relc": 0.000321,
"final_chmax": null,
"seed": null,
"atmosphere_has_nan": false,
"synspec_rc": 0,
"synspec_error": null,
"synspec_sec": 1.5,
"elapsed_sec": 42.0,
"note": null,
});
serde_json::to_string(&summary).unwrap()
}
/// 构造**旧版 Python run_one.py 形态**的 conv.jsonstage 含 itek_attempts/final 嵌套,
/// 顶层含 elapsed_sec/synspec_*)——验证 import_seed 对真实历史数据的解析兼容性。
fn make_python_legacy_conv_json(name: &str) -> String {
serde_json::json!({
"name": name,
"params": {"teff": 20000.0, "logg": 5.0, "loghe": -2.0, "logc": -4.0, "logn": -4.0, "logo": -4.0},
"stages": [
{"label": "lte", "chmax": null, "lte": "T",
"itek_attempts": [{"itek": null, "rc": 0, "converged": true, "max_relc": 0.0}],
"converged": true,
"final": {"itek": null, "rc": 0, "converged": true, "max_relc": 0.0},
"best_max_relc": 0.0, "elapsed_sec": 2.1},
{"label": "nl", "chmax": null, "lte": "F",
"itek_attempts": [{"itek": null, "rc": 0, "converged": true, "max_relc": 0.000321,
"worst_depth": 1, "last_iter": 17, "n_depths": 50}],
"converged": true,
"final": {"itek": null, "rc": 0, "converged": true, "max_relc": 0.000321,
"worst_depth": 1, "last_iter": 17, "n_depths": 50},
"best_max_relc": 0.000321, "elapsed_sec": 640.0}
],
"converged": true,
"final_max_relc": 0.000321,
"final_chmax": null,
"seed": null,
"atmosphere_has_nan": false,
"synspec_rc": 0,
"synspec_sec": 3.1,
"elapsed_sec": 715.0,
})
.to_string()
}
/// 构造一个 multipart/form-data body,含 report(JSON 文本) + seed_file(二进制)。
fn make_import_multipart(
boundary: &str,
report_json: &str,
seed_bytes: &[u8],
seed_name: &str,
success_method: &str,
) -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(format!("--{}\r\n", boundary).as_bytes());
body.extend_from_slice(b"Content-Disposition: form-data; name=\"report\"\r\n");
body.extend_from_slice(b"Content-Type: application/json\r\n\r\n");
body.extend_from_slice(report_json.as_bytes());
body.extend_from_slice(b"\r\n");
body.extend_from_slice(format!("--{}\r\n", boundary).as_bytes());
body.extend_from_slice(
format!(
"Content-Disposition: form-data; name=\"seed_file\"; filename=\"{}\"\r\n",
seed_name
)
.as_bytes(),
);
body.extend_from_slice(b"Content-Type: application/octet-stream\r\n\r\n");
body.extend_from_slice(seed_bytes);
body.extend_from_slice(b"\r\n");
// 大气收敛途径字段(cold_run/seed_step):模拟 import_results 工具判定后透传的途径。
body.extend_from_slice(format!("--{}\r\n", boundary).as_bytes());
body.extend_from_slice(b"Content-Disposition: form-data; name=\"tlusty_success_method\"\r\n");
body.extend_from_slice(b"Content-Type: text/plain\r\n\r\n");
body.extend_from_slice(success_method.as_bytes());
body.extend_from_slice(b"\r\n");
body.extend_from_slice(format!("--{}--\r\n", boundary).as_bytes());
body
}
#[tokio::test]
async fn test_import_seed_admin_endpoint() {
let temp_dir = tempfile::tempdir().unwrap();
let db_path = temp_dir.path().join("import_db.db");
let queue_db_path = temp_dir.path().join("import_queue.db");
let seeds_dir = temp_dir.path().join("results");
std::fs::create_dir_all(&seeds_dir).unwrap();
let db = Database::new(&db_path.to_string_lossy()).await.unwrap();
let queue = Arc::new(
SqliteTaskQueue::new(&queue_db_path.to_string_lossy())
.await
.unwrap(),
);
let scheduler = Arc::new(GridScheduler::new(db.clone(), queue.clone()));
let state = AppState {
db: db.clone(),
queue,
scheduler,
seeds_dir: seeds_dir.to_string_lossy().to_string(),
rate_limiter: server::api::rate_limit::RateLimiter::new(
5,
std::time::Duration::from_secs(300),
),
admin_token: Some("admin-secret".to_string()),
auth_disabled: false,
admin_sessions: Arc::new(tokio::sync::RwLock::new(std::collections::HashMap::new())),
};
let api_router = axum::Router::new().route(
"/admin/import_seed",
axum::routing::post(server::api::task::import_seed),
);
let auth_layer =
axum::middleware::from_fn_with_state(state.clone(), server::api::auth_middleware);
let app = axum::Router::new()
.nest("/api", api_router.layer(auth_layer))
.with_state(state);
// 1. 无 admin token → 401
let conv = make_legacy_conv_json("t20000_g5.0_he-2_c-4_n-4_o-4", true);
let body_bytes = make_import_multipart(
"boundary1",
&conv,
b"FAKE_ATMOS_7",
"t20000_g5.0_he-2_c-4_n-4_o-4.7",
"cold_run",
);
let res = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri("/api/admin/import_seed?workflow=wf_import")
.header("content-type", "multipart/form-data; boundary=boundary1")
.body(Body::from(body_bytes))
.unwrap(),
)
.await
.unwrap();
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
// 2. admin token + 收敛点 → 200conv.json + .7 落地,grid_points=converged
let body_bytes = make_import_multipart(
"boundary2",
&conv,
b"FAKE_ATMOS_7",
"t20000_g5.0_he-2_c-4_n-4_o-4.7",
"cold_run",
);
let res = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri("/api/admin/import_seed?workflow=wf_import")
.header("authorization", "Bearer admin-secret")
.header("content-type", "multipart/form-data; boundary=boundary2")
.body(Body::from(body_bytes))
.unwrap(),
)
.await
.unwrap();
assert_eq!(res.status(), StatusCode::OK);
// 关键断言:磁盘目录名用旧 conv.json 的源精度真名(g5.0),conv.json 与 .7 均落地。
let point_dir = seeds_dir.join("t20000_g5.0_he-2_c-4_n-4_o-4");
assert!(point_dir.join("conv.json").is_file(), "conv.json 应落地");
assert!(
point_dir.join("t20000_g5.0_he-2_c-4_n-4_o-4.7").is_file(),
".7 种子文件应落地"
);
// grid_points 应被幂等 upsert 且标记为 converged。
let gp = db
.get_grid_point_status("t20000_g5.0_he-2_c-4_n-4_o-4", "wf_import")
.await
.unwrap()
.expect("grid_points 应存在");
assert_eq!(gp.0, "completed", "导入的收敛点应为 converged 状态");
// 3. 幂等:重复导入同名点不应报错,状态仍 converged。
let body_bytes = make_import_multipart(
"boundary3",
&conv,
b"FAKE_ATMOS_7_AGAIN",
"t20000_g5.0_he-2_c-4_n-4_o-4.7",
"cold_run",
);
let res = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri("/api/admin/import_seed?workflow=wf_import")
.header("authorization", "Bearer admin-secret")
.header("content-type", "multipart/form-data; boundary=boundary3")
.body(Body::from(body_bytes))
.unwrap(),
)
.await
.unwrap();
assert_eq!(res.status(), StatusCode::OK);
let gp = db
.get_grid_point_status("t20000_g5.0_he-2_c-4_n-4_o-4", "wf_import")
.await
.unwrap()
.unwrap();
assert_eq!(gp.0, "completed");
// 4. 未收敛点 → 200,但不写 .7、grid_points 维持 pending(未建 converged)。
let conv_fail = make_legacy_conv_json("t20000_g5.0_he-2_c-4_n-4_o-4_fail", false);
let body_bytes =
make_import_multipart("boundary4", &conv_fail, b"WONT_BE_USED", "x.7", "cold_run");
let res = app
.oneshot(
Request::builder()
.method("POST")
.uri("/api/admin/import_seed?workflow=wf_import")
.header("authorization", "Bearer admin-secret")
.header("content-type", "multipart/form-data; boundary=boundary4")
.body(Body::from(body_bytes))
.unwrap(),
)
.await
.unwrap();
assert_eq!(res.status(), StatusCode::OK);
let fail_dir = seeds_dir.join("t20000_g5.0_he-2_c-4_n-4_o-4_fail");
assert!(
fail_dir.join("conv.json").is_file(),
"未收敛点仍应记录 conv.json"
);
assert!(
!fail_dir
.join("t20000_g5.0_he-2_c-4_n-4_o-4_fail.7")
.exists(),
"未收敛点不应写 .7 种子"
);
}
/// 旧版 Python run_one.py conv.json 的完整导入链路(import_results 工具的服务端侧):
/// 嵌套 stagesitek_attempts/final)应被正常解析,收敛标记落地,且旧版的
/// elapsed_sec 经 P3 迁移链路进入 grid_points.last_elapsed_sec(迁移完整性)。
#[tokio::test]
async fn test_import_seed_python_legacy_conv_json() {
let temp_dir = tempfile::tempdir().unwrap();
let db_path = temp_dir.path().join("import_legacy_db.db");
let queue_db_path = temp_dir.path().join("import_legacy_queue.db");
let seeds_dir = temp_dir.path().join("results");
std::fs::create_dir_all(&seeds_dir).unwrap();
let db = Database::new(&db_path.to_string_lossy()).await.unwrap();
let queue = Arc::new(
SqliteTaskQueue::new(&queue_db_path.to_string_lossy())
.await
.unwrap(),
);
let scheduler = Arc::new(GridScheduler::new(db.clone(), queue.clone()));
let state = AppState {
db: db.clone(),
queue,
scheduler,
seeds_dir: seeds_dir.to_string_lossy().to_string(),
rate_limiter: server::api::rate_limit::RateLimiter::new(
5,
std::time::Duration::from_secs(300),
),
admin_token: Some("admin-secret".to_string()),
auth_disabled: false,
admin_sessions: Arc::new(tokio::sync::RwLock::new(std::collections::HashMap::new())),
};
let api_router = axum::Router::new().route(
"/admin/import_seed",
axum::routing::post(server::api::task::import_seed),
);
let auth_layer =
axum::middleware::from_fn_with_state(state.clone(), server::api::auth_middleware);
let app = axum::Router::new()
.nest("/api", api_router.layer(auth_layer))
.with_state(state);
let name = "t20000_g5.0_he-2_c-4_n-4_o-4";
let conv = make_python_legacy_conv_json(name);
let body_bytes = make_import_multipart(
"boundaryL",
&conv,
b"FAKE_ATMOS_7",
&format!("{name}.7"),
"cold_run",
);
let res = app
.oneshot(
Request::builder()
.method("POST")
.uri("/api/admin/import_seed?workflow=wf_legacy")
.header("authorization", "Bearer admin-secret")
.header("content-type", "multipart/form-data; boundary=boundaryL")
.body(Body::from(body_bytes))
.unwrap(),
)
.await
.unwrap();
assert_eq!(
res.status(),
StatusCode::OK,
"旧版嵌套 stages 的 conv.json 应被接受"
);
// grid_pointsconverged + cold_run 手段(旧版 conv.json 无 seed_nc 阶段)+ 旧版 elapsed_sec 已落库
let row = db
.get_workflow_point_row("wf_legacy", name)
.await
.unwrap()
.expect("grid_points 应存在");
assert_eq!(row.status, "completed");
assert_eq!(row.tlusty_success_method.as_deref(), Some("cold_run"));
assert_eq!(
row.last_elapsed_sec,
Some(715.0),
"旧版 conv.json 的单点耗时应完整迁移到 last_elapsed_sec"
);
// conv.json 原文落盘(嵌套诊断数据 itek_attempts/final 不丢失,详情页解析走兼容路径)
assert!(seeds_dir.join(name).join("conv.json").is_file());
assert!(seeds_dir.join(name).join(format!("{name}.7")).is_file());
}
#[tokio::test]
async fn test_node_disable_enable_flow() {
@@ -2145,7 +1836,7 @@ async fn dispatch_and_report(
task_id,
point_name: p.model_name(),
params: p.clone(),
seed_point_name: seed,
seed_point_name: seed.clone(),
timeout_sec: 7200,
workflow_name: Some(wf.to_string()),
wave: 0,
@@ -2157,6 +1848,26 @@ async fn dispatch_and_report(
..Default::default()
};
db.insert_task(&spec).await.unwrap();
// 构造合法 ModelSummary 作为 summary_json(让 record_task_report 能解析合并写入 grid_points)。
let summary = common::models::ModelSummary {
name: p.model_name(),
params: p.clone(),
stages: Vec::new(),
result_valid: converged,
final_max_relc: if converged { Some(0.0005) } else { Some(9.5e5) },
final_chmax: Some(0.001),
seed,
atmosphere_has_nan: false,
synspec_rc: None,
synspec_error: None,
synspec_sec: None,
elapsed_sec: 120.0,
energy_check: None,
temp_check: None,
emflux_check: None,
bfac_check: None,
note: None,
};
let report = common::models::TaskReport {
task_id,
point_name: p.model_name(),
@@ -2176,7 +1887,7 @@ async fn dispatch_and_report(
} else {
Some("nl stage diverged".to_string())
},
summary_json: "{}".to_string(),
summary_json: serde_json::to_string(&summary).unwrap(),
failed_stage: None,
};
db.record_task_report(&report, wf).await.unwrap();
@@ -2292,9 +2003,7 @@ async fn test_wf_stats_endpoint() {
)
.await;
dispatch_and_report(&db, "wf_stats", &p_failed, "cold_run", None, false).await;
db.mark_grid_point_imported(&p_imported.model_name(), "wf_stats", None, "cold_run")
.await
.unwrap();
mark_imported(&db, &p_imported.model_name(), "wf_stats", &p_imported, "cold_run").await;
// 回拨任务创建时间:测试内 insert/report 同秒完成,墙钟差为 0 会被 ETA 估算
// 过滤(avg 必须 > 0);造 120s 的真实感样本,使 avg_point_sec/eta_sec 非空。
@@ -2456,9 +2165,7 @@ async fn seed_obs_fixture(db: &Database, db_path: &std::path::Path, wf: &str) ->
true,
)
.await;
db.mark_grid_point_imported(&p_imported.model_name(), wf, None, "cold_run")
.await
.unwrap();
mark_imported(&db, &p_imported.model_name(), wf, &p_imported, "cold_run").await;
let conn = rusqlite::Connection::open(db_path).unwrap();
conn.execute(
@@ -2709,14 +2416,9 @@ async fn test_point_detail_endpoint() {
let n = seed_obs_fixture(&db, &db_path, "wf_pd").await;
// 给 cold 点写一份合法 conv.jsonrescued 刻意不写,验证 null 降级)
let cold_dir = seeds_dir.join(&n.cold);
std::fs::create_dir_all(&cold_dir).unwrap();
std::fs::write(
cold_dir.join("conv.json"),
make_legacy_conv_json(&n.cold, true),
)
.unwrap();
// conv 诊断面板数据源已改为 grid_points.summary_jsondispatch_and_report 写入),
// 不再需要磁盘 conv.json。rescued 点最终 seed_step 成功 → 也有 summary_json
// 但此处验证 null 降级用 pending 点(未 report,无 summary)。
async fn get_detail(app: &axum::Router, uri: &str) -> (StatusCode, serde_json::Value) {
let res = app
@@ -2757,10 +2459,11 @@ async fn test_point_detail_endpoint() {
assert_eq!(attempts.len(), 1);
assert!(attempts[0]["seed_point_name"].is_null(), "冷启动无种子来源");
assert_eq!(attempts[0]["status"], "completed");
assert_eq!(data["conv"]["result_valid"], true, "conv.json 应被解析");
assert_eq!(data["conv"]["final_max_relc"], 0.000321);
assert_eq!(data["conv"]["result_valid"], true, "summary_json 应被解析");
assert_eq!(data["conv"]["final_max_relc"], 0.0005);
// ---- 3. rescued 点:2 次尝试按时间升序(冷启失败 → 种子步进救回)conv 为 null ----
// ---- 3. rescued 点:2 次尝试按时间升序(冷启失败 → 种子步进救回)----
// 最终 seed_step 成功 → record_task_report 写入 summary_jsonconv 非 null。
let uri = format!("/api/workflows/wf_pd/points/{}", n.rescued);
let (st, data) = get_detail(&app, &uri).await;
assert_eq!(st, StatusCode::OK);
@@ -2775,7 +2478,13 @@ async fn test_point_detail_endpoint() {
attempts[1]["seed_point_name"], n.cold,
"种子来源应为 cold 点"
);
assert!(data["conv"].is_null(), "无 conv.json 应返回 null 而非报错");
assert_eq!(data["conv"]["result_valid"], true, "rescued 最终成功应有 summary");
// ---- 3b. pending 点:从未 report → 无 summary_json → conv 为 null(降级不报错)----
let uri = format!("/api/workflows/wf_pd/points/{}", n.pending);
let (st, data) = get_detail(&app, &uri).await;
assert_eq!(st, StatusCode::OK);
assert!(data["conv"].is_null(), "未结算点无 summary 应返回 null");
// ---- 4. 路径穿越 / 非法字符 → 400 ----
for bad in ["..%2Fevil", "a%2Fb", ".hidden", ".."] {
+51 -30
View File
@@ -1,10 +1,10 @@
//! 验证「历史种子导入的工作流名」与「正式工作流名」的隔离关系。
//!
//! 用户意图:import_results 把旧 Python 计算结果导入,标记为已完成,避免重算。
//! 用户意图:离线导入工具把旧计算结果导入,标记为已完成,避免重算。
//! 关键问题:导入到工作流 A,之后正式启动工作流 B(同名/异名),B 能否看到 A 标记的 converged
use common::config::GridConfig;
use common::models::GridPointParams;
use common::models::{GridPointParams, ModelSummary};
use mq::sqlite_queue::SqliteTaskQueue;
use server::{db::Database, scheduler::GridScheduler};
use std::sync::Arc;
@@ -21,6 +21,47 @@ fn make_params() -> GridPointParams {
}
}
/// 构造一个收敛的 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,
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";
@@ -49,13 +90,8 @@ async fn test_same_workflow_name_preserves_converged() {
let name = "t20000_g5.0_he-2_c-4_n-4_o-4";
let p = make_params();
// 模拟 import_seedupsert + mark_imported,工作流名 = sdB_cno
db.upsert_grid_point_named(name, &p, 0, "sdB_cno")
.await
.unwrap();
db.mark_grid_point_imported(name, "sdB_cno", None, "cold_run")
.await
.unwrap();
// 模拟离线导入:upsert + 写收敛 summary,工作流名 = sdB_cno
mark_imported(&db, name, "sdB_cno", &p, "cold_run").await;
// 之后正式启动同名工作流:initialize_grid(sdB_cno)
sched
@@ -80,13 +116,8 @@ async fn test_different_workflow_name_causes_recompute() {
let name = "t20000_g5.0_he-2_c-4_n-4_o-4";
let p = make_params();
// 模拟 import_seed:导入到 "imported" 工作流
db.upsert_grid_point_named(name, &p, 0, "imported")
.await
.unwrap();
db.mark_grid_point_imported(name, "imported", None, "cold_run")
.await
.unwrap();
// 模拟离线导入:导入到 "imported" 工作流
mark_imported(&db, name, "imported", &p, "cold_run").await;
// 之后正式启动 "sdB_cno" 工作流
sched
@@ -118,13 +149,8 @@ async fn test_mixed_grid_import_then_init_avoids_recompute() {
let (db, sched) = setup().await;
let p_old = make_params(); // t20000_g5.0_...
// 模拟 import_seed:旧网格里这个点已收敛,导入到 sdB_cno
db.upsert_grid_point_named("t20000_g5.0_he-2_c-4_n-4_o-4", &p_old, 0, "sdB_cno")
.await
.unwrap();
db.mark_grid_point_imported("t20000_g5.0_he-2_c-4_n-4_o-4", "sdB_cno", None, "cold_run")
.await
.unwrap();
// 模拟离线导入:旧网格里这个点已收敛,导入到 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";
@@ -172,13 +198,8 @@ async fn test_precision_diff_import_then_init_preserves_converged() {
logo: GridAxisValue::from_value(-4.0),
};
// 模拟 import_results 重写 name 后入库:grid_points.name = canonical(g5.0)
db.upsert_grid_point_named(canonical, &p, 0, "sdB_cno")
.await
.unwrap();
db.mark_grid_point_imported(canonical, "sdB_cno", None, "cold_run")
.await
.unwrap();
// 模拟离线导入重写 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";