use std::path::{Path, PathBuf};
use tempfile::TempDir;
use super::*;
use gchemol::Molecule;
mod cmd;
pub struct BlackBoxModel {
run_file: PathBuf,
tpl_file: PathBuf,
int_file: Option<PathBuf>,
scr_dir: Option<PathBuf>,
job_dir: Option<PathBuf>,
task: Option<Task>,
temp_dir: Option<TempDir>,
ncalls: usize,
}
struct Task(std::process::Child);
impl Drop for Task {
fn drop(&mut self) {
info!("Task dropped. Kill external commands in session.");
let child = &mut self.0;
if let Ok(Some(x)) = child.try_wait() {
info!("child process exited gracefully with status {x:?}.");
} else {
if let Err(e) = send_signal_term(child.id()) {
error!("Kill child process failure: {:?}", e);
}
std::thread::sleep(std::time::Duration::from_secs_f64(0.1));
if let Ok(None) = child.try_wait() {
info!("Child process is still running, one second for clean up ...",);
std::thread::sleep(std::time::Duration::from_secs(1));
}
}
}
}
fn send_signal_term(pid: u32) -> Result<()> {
use nix::sys::signal::{kill, Signal};
let pid = nix::unistd::Pid::from_raw(pid as i32);
let signal = Signal::SIGTERM;
info!("Inform child process {} to exit by sending signal {:?}.", pid, signal);
kill(pid, signal).with_context(|| format!("kill process {:?}", pid))?;
Ok(())
}
mod env {
use super::*;
use tempfile::{tempdir, tempdir_in};
fn new_scratch_directory(scr_root: Option<&Path>) -> Result<TempDir> {
if let Some(d) = &scr_root {
if !d.exists() {
std::fs::create_dir_all(d).context("create scratch root dir")?;
}
}
scr_root.map_or_else(
|| tempdir().context("create temp scratch dir"),
|d| tempdir_in(d).with_context(|| format!("create temp scratch dir under {:?}", d)),
)
}
impl BlackBoxModel {
pub(super) fn prepare_compute_env(&mut self) -> Result<PathBuf> {
let run = "run";
let runfile = if let Some(tdir) = &self.temp_dir {
tdir.path().join(run)
} else {
let tdir = new_scratch_directory(self.scr_dir.as_deref())?;
debug!("BBM scratching directory: {:?}", tdir);
let dest = tdir.path().join(run);
let txt = gut::fs::read_file(&self.run_file)?;
gut::fs::write_script_file(&dest, &txt)?;
self.temp_dir = tdir.into();
dest.canonicalize()?
};
Ok(runfile)
}
pub(super) fn from_dotenv(dir: &Path) -> Result<Self> {
let dir = dir
.canonicalize()
.with_context(|| format!("invalid template directory: {:?}", dir))?;
let envfile = envfile::EnvFile::new(dir.join(".env")).unwrap();
for (key, value) in &envfile.store {
debug!("found env var from {:?}: {}={}", &envfile.path, key, value);
}
let run_file = envfile.get("BBM_RUN_FILE").unwrap_or("submit.sh");
let tpl_file = envfile.get("BBM_TPL_FILE").unwrap_or("input.hbs");
let int_file_opt = envfile.get("BBM_INT_FILE");
let bbm = BlackBoxModel {
run_file: dir.join(run_file),
tpl_file: dir.join(tpl_file),
int_file: int_file_opt.map(|f| dir.join(f)),
scr_dir: envfile.get("BBM_SCR_DIR").map(|x| x.into()),
job_dir: std::env::current_dir()?.into(),
temp_dir: None,
task: None,
ncalls: 0,
};
Ok(bbm)
}
}
#[test]
fn test_env() -> Result<()> {
let d = new_scratch_directory(Some("/scratch/test".as_ref()))?;
assert!(d.path().exists());
let d = new_scratch_directory(None)?;
assert!(d.path().exists());
Ok(())
}
}
impl BlackBoxModel {
fn compute_normal(&mut self, mol: &Molecule) -> Result<Computed> {
let txt = self.render_input(&mol)?;
let output = self.submit_cmd(&txt)?;
let mp = output
.parse()
.with_context(|| format!("failed to parse computed results: {:?}", output))?;
self.ncalls += 1;
Ok(mp)
}
fn compute_normal_bunch(&mut self, mols: &[Molecule]) -> Result<Vec<Computed>> {
let txt = self.render_input_bunch(mols)?;
let output = self.submit_cmd(&txt)?;
let all = Computed::parse_all(&output)?;
self.ncalls += 1;
Ok(all)
}
}
impl BlackBoxModel {
pub fn render_input(&self, mol: &Molecule) -> Result<String> {
for (i, a) in mol.atoms() {
let p = a.position();
if p.iter().any(|x| x.is_nan()) {
error!("Invalid position of atom {}: {:?}", i, p);
bail!("Molecule has invalid data in positions.");
}
}
let txt = mol.render_with(&self.tpl_file)?;
Ok(txt)
}
pub fn render_input_bunch(&self, mols: &[Molecule]) -> Result<String> {
let mut txt = String::new();
for mol in mols.iter() {
let part = self.render_input(&mol)?;
txt.push_str(&part);
}
Ok(txt)
}
}
impl BlackBoxModel {
pub fn from_dir<P: AsRef<Path>>(dir: P) -> Result<Self> {
Self::from_dotenv(dir.as_ref()).context("Initialize BlackBoxModel failure.")
}
pub fn keep_scratch_files(self) {
if let Some(tdir) = self.temp_dir {
let path = tdir.into_path();
println!("Directory for scratch files: {}", path.display());
} else {
warn!("No temp dir found.");
}
}
pub fn number_of_evaluations(&self) -> usize {
self.ncalls
}
}
impl ChemicalModel for BlackBoxModel {
fn compute(&mut self, mol: &Molecule) -> Result<Computed> {
let mp = self.compute_normal(mol)?;
debug_assert!({
let n = mol.natoms();
if let Some(pmol) = mp.get_molecule() {
pmol.natoms() == n
} else {
true
}
});
Ok(mp)
}
fn compute_bunch(&mut self, mols: &[Molecule]) -> Result<Vec<Computed>> {
let all = self.compute_normal_bunch(mols)?;
debug_assert_eq!(mols.len(), all.len());
Ok(all)
}
}
#[test]
fn test_bbm() -> Result<()> {
let bbm_vasp = "./tests/files/vasp-sp";
let bbm_siesta = "./tests/files/siesta-sp";
let vasp = BlackBoxModel::from_dir(bbm_vasp)?;
let siesta = BlackBoxModel::from_dir(bbm_siesta)?;
assert!(vasp.tpl_file.ends_with("input.tera"));
assert!(siesta.tpl_file.ends_with("input.hbs"));
Ok(())
}