DCTS/crates/common/src/runner.rs

418 lines
15 KiB
Rust

use crate::config::{StageConfig, SynspecConfig};
use crate::conv_check::{atmosphere_has_nan, check_fort9};
use crate::embedded::RuntimePaths;
use crate::fort55_writer::generate_fort55_content;
use crate::gen_input5::make_input5;
use crate::models::{GridPointParams, ModelSummary, StageSummary, TaskType};
use crate::nst_writer::generate_nst_content;
use anyhow::Result;
use tokio::fs::File;
use std::path::{Path, PathBuf};
use std::process::Stdio;
use tokio::process::Command as AsyncCommand;
use std::time::Instant;
use tracing::{info, warn};
pub fn default_cold_chain() -> Vec<StageConfig> {
vec![
StageConfig {
label: "lte".to_string(),
lte: "T".to_string(),
ltgray: "T".to_string(),
ilvlin: 0,
require_converged: false,
niter: 0,
chmax: None,
itek: None,
metals: Some("cno".to_string()),
ichang: None,
idlte: None,
iacc: None,
orelax: None,
},
StageConfig {
label: "nc".to_string(),
lte: "F".to_string(),
ltgray: "F".to_string(),
ilvlin: 0,
require_converged: false,
niter: 10,
chmax: None,
itek: None,
metals: Some("cno".to_string()),
ichang: None,
idlte: None,
iacc: None,
orelax: None,
},
StageConfig {
label: "nl".to_string(),
lte: "F".to_string(),
ltgray: "F".to_string(),
ilvlin: 100,
require_converged: true,
niter: 100,
chmax: None,
itek: None,
metals: Some("cno".to_string()),
ichang: None,
idlte: None,
iacc: None,
orelax: None,
},
]
}
pub fn default_seed_chain() -> Vec<StageConfig> {
vec![
StageConfig {
label: "seed_nc".to_string(),
lte: "F".to_string(),
ltgray: "F".to_string(),
ilvlin: 0,
require_converged: false,
niter: 20,
chmax: None,
itek: None,
metals: Some("cno".to_string()),
ichang: Some(0),
idlte: None,
iacc: None,
orelax: None,
},
StageConfig {
label: "nl".to_string(),
lte: "F".to_string(),
ltgray: "F".to_string(),
ilvlin: 100,
require_converged: true,
niter: 100,
chmax: None,
itek: None,
metals: Some("cno".to_string()),
ichang: Some(0),
idlte: None,
iacc: None,
orelax: None,
},
]
}
async fn run_child_async_with_timeout(
mut child: tokio::process::Child,
timeout_sec: u64,
) -> Result<std::process::ExitStatus> {
match tokio::time::timeout(tokio::time::Duration::from_secs(timeout_sec), child.wait()).await {
Ok(res) => Ok(res?),
Err(_) => {
let _ = child.start_kill();
let _ = child.wait().await;
anyhow::bail!("进程计算超时 (上限: {} 秒)", timeout_sec);
}
}
}
pub struct ExecutionRunner<'a> {
pub runtime: &'a RuntimePaths,
pub work_dir: PathBuf,
}
impl<'a> ExecutionRunner<'a> {
pub fn new(runtime: &'a RuntimePaths, work_dir: PathBuf) -> Self {
Self { runtime, work_dir }
}
pub async fn run_model(
&self,
params: &GridPointParams,
task_type: TaskType,
custom_chain: Option<Vec<StageConfig>>,
seed_atmos: Option<&Path>,
synspec_cfg: Option<&SynspecConfig>,
) -> Result<ModelSummary> {
self.run_model_with_timeout(params, task_type, custom_chain, seed_atmos, synspec_cfg, 7200).await
}
pub async fn run_model_with_timeout(
&self,
params: &GridPointParams,
task_type: TaskType,
custom_chain: Option<Vec<StageConfig>>,
seed_atmos: Option<&Path>,
synspec_cfg: Option<&SynspecConfig>,
timeout_sec: u64,
) -> Result<ModelSummary> {
let name = params.model_name();
let model_dir = self.work_dir.join(&name);
tokio::fs::create_dir_all(&model_dir).await?;
info!("开始物理计算网格模型 {} (类型: {:?})", name, task_type);
let t0 = Instant::now();
// 1. Data directory symlink setup
let link_data = model_dir.join("data");
if tokio::fs::symlink_metadata(&link_data).await.is_ok() || link_data.exists() {
let _ = tokio::fs::remove_file(&link_data).await;
}
#[cfg(unix)]
{
let abs_data_dir = tokio::fs::canonicalize(&self.runtime.data_dir).await.unwrap_or_else(|_| self.runtime.data_dir.clone());
if let Err(e) = std::os::unix::fs::symlink(&abs_data_dir, &link_data) {
warn!("构建 data 数据集软链时发生提示性告警: {}", e);
}
}
// 2. Initial fort.8 seed setup
let fort8 = model_dir.join("fort.8");
if fort8.exists() {
let _ = tokio::fs::remove_file(&fort8).await;
}
if let Some(seed_path) = seed_atmos {
if seed_path.is_file() {
if let Err(e) = tokio::fs::copy(seed_path, &fort8).await {
warn!("向工作沙盒引导填载首期收敛模型种子 fort.8 发生复制错误: {}", e);
}
}
}
// Clean fort.84 residue to prevent NATOMS Fortran crash
let fort84 = model_dir.join("fort.84");
if fort84.exists() {
let _ = tokio::fs::remove_file(&fort84).await;
}
let chain = custom_chain.unwrap_or_else(|| match task_type {
TaskType::ColdRun => default_cold_chain(),
TaskType::SeedStep => default_seed_chain(),
});
let mut stage_summaries = Vec::new();
let mut current_seed: Option<PathBuf> = seed_atmos.map(|p| p.to_path_buf());
let mut final_converged = false;
let mut final_chmax: Option<f64> = None;
let mut final_max_relc: Option<f64> = None;
for stage_def in &chain {
let stage_t0 = Instant::now();
let metals = stage_def.metals.as_deref().unwrap_or("cno");
let input5_text = make_input5(
params,
&stage_def.lte,
&stage_def.ltgray,
metals,
stage_def.ilvlin,
);
let input5_path = model_dir.join(format!("{}.5", name));
tokio::fs::write(&input5_path, &input5_text).await?;
// Write nst file
let nst_text = generate_nst_content(stage_def);
tokio::fs::write(model_dir.join("nst"), &nst_text).await?;
// Prepare fort.8 for this stage
if stage_def.ltgray == "T" {
if fort8.exists() {
let _ = tokio::fs::remove_file(&fort8).await;
}
} else if let Some(ref s_path) = current_seed {
if s_path.is_file() {
if let Err(e) = tokio::fs::copy(s_path, &fort8).await {
warn!("阶段 {} 重载候选近邻推算种子模型期间发生文件复制异常: {}", stage_def.label, e);
}
}
}
// Run tlusty.exe
let fin = File::open(&input5_path).await?.into_std().await;
let fout = File::create(model_dir.join(format!("{}.6", name))).await?.into_std().await;
let ferr = File::create(model_dir.join(format!("{}.err", name))).await?.into_std().await;
let child = AsyncCommand::new(&self.runtime.tlusty_exe)
.current_dir(&model_dir)
.stdin(Stdio::from(fin))
.stdout(Stdio::from(fout))
.stderr(Stdio::from(ferr))
.kill_on_drop(true)
.spawn()?;
let status_res = run_child_async_with_timeout(child, timeout_sec).await;
let rc = match status_res {
Ok(st) => st.code().unwrap_or(-1),
Err(e) => {
warn!("tlusty 运行失败/超时: {}", e);
-1
}
};
let fort9 = model_dir.join("fort.9");
let fort7 = model_dir.join("fort.7");
let mut stage_summary = StageSummary {
label: stage_def.label.clone(),
chmax: stage_def.chmax,
lte: stage_def.lte.clone(),
converged: false,
best_max_relc: None,
elapsed_sec: stage_t0.elapsed().as_secs_f64(),
note: None,
};
if rc == 0 && fort7.is_file() {
let eff_chmax = stage_def.chmax.unwrap_or(0.001);
if fort9.is_file() {
let res = check_fort9(&fort9, eff_chmax);
stage_summary.converged = res.converged;
stage_summary.best_max_relc = Some(res.max_relc);
// Save fort.9 snapshot
let snap_name = format!("{}.{}_chmax{}.9", name, stage_def.label, eff_chmax);
let _ = tokio::fs::copy(&fort9, model_dir.join(snap_name)).await;
} else {
// NITER=0 grey start without fort.9
stage_summary.converged = true;
stage_summary.best_max_relc = Some(0.0);
stage_summary.note = Some("NITER=0 grey start".to_string());
}
// Copy fort.7 as stage seed
let stage_seed_path = model_dir.join(format!("{}.{}.7", name, stage_def.label));
let _ = tokio::fs::copy(&fort7, &stage_seed_path).await;
current_seed = Some(stage_seed_path);
} else {
stage_summary.note = Some(format!("tlusty rc={} or missing fort.7", rc));
}
final_chmax = stage_def.chmax;
final_converged = stage_summary.converged;
if let Some(r) = stage_summary.best_max_relc {
final_max_relc = Some(r);
}
stage_summaries.push(stage_summary);
if !final_converged && stage_def.require_converged {
warn!("阶段 {} 要求收敛但未达标,中止后续收敛链阶段", stage_def.label);
break;
}
}
// Final atmosphere file .7
let final_7 = model_dir.join(format!("{}.7", name));
if let Some(ref s_path) = current_seed {
if s_path.is_file() {
let _ = tokio::fs::copy(s_path, &final_7).await;
}
} else if model_dir.join("fort.7").is_file() {
let _ = tokio::fs::copy(model_dir.join("fort.7"), &final_7).await;
}
let atmo_has_nan = atmosphere_has_nan(&final_7);
if atmo_has_nan {
final_converged = false;
}
// Run synspec if final .7 atmosphere exists
let mut synspec_rc = None;
let mut synspec_err = None;
let mut synspec_sec = None;
if final_7.is_file() {
let syn_t0 = Instant::now();
let _ = tokio::fs::copy(&final_7, model_dir.join("fort.8")).await;
let _ = tokio::fs::remove_file(model_dir.join("fort.7")).await;
// Fort.55 parameter generation or symlink
let fort55_path = model_dir.join("fort.55");
let fort19_path = model_dir.join("fort.19");
let _ = tokio::fs::remove_file(&fort55_path).await;
let _ = tokio::fs::remove_file(&fort19_path).await;
let default_cfg = SynspecConfig {
wstart: 1400.0,
wend: 1410.0,
imode: 0,
idrv: 50,
ifreq: 1,
rel_cutoff: 0.0001,
abs_cutoff: 0.01,
};
let fort55_text = generate_fort55_content(synspec_cfg.unwrap_or(&default_cfg));
let _ = tokio::fs::write(&fort55_path, &fort55_text).await;
#[cfg(unix)]
{
let abs_linelist = tokio::fs::canonicalize(&self.runtime.linelist).await.unwrap_or_else(|_| self.runtime.linelist.clone());
let _ = std::os::unix::fs::symlink(&abs_linelist, &fort19_path);
}
let input5_path = model_dir.join(format!("{}.5", name));
if input5_path.is_file() {
let fin = File::open(&input5_path).await?.into_std().await;
let fout = File::create(model_dir.join(format!("{}.log", name))).await?.into_std().await;
let child = AsyncCommand::new(&self.runtime.synspec_exe)
.current_dir(&model_dir)
.stdin(Stdio::from(fin))
.stdout(Stdio::from(fout))
.stderr(Stdio::null())
.kill_on_drop(true)
.spawn()?;
let status_res = run_child_async_with_timeout(child, timeout_sec).await;
let rc = match status_res {
Ok(st) => st.code().unwrap_or(-1),
Err(e) => {
warn!("synspec 运行失败/超时: {}", e);
-1
}
};
synspec_rc = Some(rc);
synspec_sec = Some(syn_t0.elapsed().as_secs_f64());
// Copy/move outputs: fort.7 (Synspec spectrum) -> .spec, fort.17 -> .cont, fort.12 -> .iden
if model_dir.join("fort.7").is_file() {
let _ = tokio::fs::rename(model_dir.join("fort.7"), model_dir.join(format!("{}.spec", name))).await;
}
if model_dir.join("fort.17").is_file() {
let _ = tokio::fs::copy(model_dir.join("fort.17"), model_dir.join(format!("{}.cont", name))).await;
}
if model_dir.join("fort.12").is_file() {
let _ = tokio::fs::copy(model_dir.join("fort.12"), model_dir.join(format!("{}.iden", name))).await;
}
}
} else {
synspec_err = Some("No atmosphere .7 produced".to_string());
}
let elapsed_sec = t0.elapsed().as_secs_f64();
let summary = ModelSummary {
name,
params: params.clone(),
stages: stage_summaries,
converged: final_converged,
final_max_relc,
final_chmax,
seed: seed_atmos.map(|p| p.to_string_lossy().to_string()),
atmosphere_has_nan: atmo_has_nan,
synspec_rc,
synspec_error: synspec_err,
synspec_sec,
elapsed_sec,
note: if atmo_has_nan {
Some("Invalidated: atmosphere contains >10% NaN lines".to_string())
} else {
None
},
};
// Write conv.json
let json_text = serde_json::to_string_pretty(&summary)?;
tokio::fs::write(model_dir.join("conv.json"), json_text).await?;
Ok(summary)
}
}