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 { 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 { 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 { 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>, seed_atmos: Option<&Path>, synspec_cfg: Option<&SynspecConfig>, ) -> Result { 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>, seed_atmos: Option<&Path>, synspec_cfg: Option<&SynspecConfig>, timeout_sec: u64, ) -> Result { 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 = seed_atmos.map(|p| p.to_path_buf()); let mut final_converged = false; let mut final_chmax: Option = None; let mut final_max_relc: Option = 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) } }