1use std::path::{Path, PathBuf};
23use tempfile::TempDir;
24
25use super::*;
26use gchemol::Molecule;
27mod cmd;
31pub struct BlackBoxModel {
35 run_file: PathBuf,
37
38 tpl_file: PathBuf,
40
41 int_file: Option<PathBuf>,
43
44 scr_dir: Option<PathBuf>,
46
47 job_dir: Option<PathBuf>,
49
50 task: Option<Task>,
53
54 temp_dir: Option<TempDir>,
56
57 ncalls: usize,
59}
60struct Task(std::process::Child);
66
67impl Drop for Task {
68 fn drop(&mut self) {
70 info!("Task dropped. Kill external commands in session.");
71 let child = &mut self.0;
72
73 if let Ok(Some(x)) = child.try_wait() {
74 info!("child process exited gracefully with status {x:?}.");
75 } else {
76 if let Err(e) = send_signal_term(child.id()) {
78 error!("Kill child process failure: {:?}", e);
79 }
80 std::thread::sleep(std::time::Duration::from_secs_f64(0.1));
81 if let Ok(None) = child.try_wait() {
84 info!("Child process is still running, one second for clean up ...",);
85 std::thread::sleep(std::time::Duration::from_secs(1));
86 }
87 }
88 }
89}
90
91fn send_signal_term(pid: u32) -> Result<()> {
92 use nix::sys::signal::{kill, Signal};
93
94 let pid = nix::unistd::Pid::from_raw(pid as i32);
95 let signal = Signal::SIGTERM;
96 info!("Inform child process {} to exit by sending signal {:?}.", pid, signal);
97 kill(pid, signal).with_context(|| format!("kill process {:?}", pid))?;
98
99 Ok(())
100}
101mod env {
105 use super::*;
106 use tempfile::{tempdir, tempdir_in};
107
108 fn new_scratch_directory(scr_root: Option<&Path>) -> Result<TempDir> {
110 if let Some(d) = &scr_root {
112 if !d.exists() {
113 std::fs::create_dir_all(d).context("create scratch root dir")?;
114 }
115 }
116 scr_root.map_or_else(
117 || tempdir().context("create temp scratch dir"),
118 |d| tempdir_in(d).with_context(|| format!("create temp scratch dir under {:?}", d)),
119 )
120 }
121
122 impl BlackBoxModel {
123 pub(super) fn prepare_compute_env(&mut self) -> Result<PathBuf> {
125 let run = "run";
126
127 let runfile = if let Some(tdir) = &self.temp_dir {
129 tdir.path().join(run)
130 } else {
131 let tdir = new_scratch_directory(self.scr_dir.as_deref())?;
132 debug!("BBM scratching directory: {:?}", tdir);
133
134 let dest = tdir.path().join(run);
136 let txt = gut::fs::read_file(&self.run_file)?;
137 gut::fs::write_script_file(&dest, &txt)?;
138
139 self.temp_dir = tdir.into();
141 dest.canonicalize()?
142 };
143
144 Ok(runfile)
145 }
146
147 pub(super) fn from_dotenv(dir: &Path) -> Result<Self> {
148 let dir = dir
150 .canonicalize()
151 .with_context(|| format!("invalid template directory: {:?}", dir))?;
152
153 let envfile = envfile::EnvFile::new(dir.join(".env")).unwrap();
155 for (key, value) in &envfile.store {
156 debug!("found env var from {:?}: {}={}", &envfile.path, key, value);
157 }
158
159 let run_file = envfile.get("BBM_RUN_FILE").unwrap_or("submit.sh");
160 let tpl_file = envfile.get("BBM_TPL_FILE").unwrap_or("input.hbs");
161 let int_file_opt = envfile.get("BBM_INT_FILE");
162 let bbm = BlackBoxModel {
163 run_file: dir.join(run_file),
164 tpl_file: dir.join(tpl_file),
165 int_file: int_file_opt.map(|f| dir.join(f)),
166 scr_dir: envfile.get("BBM_SCR_DIR").map(|x| x.into()),
167 job_dir: std::env::current_dir()?.into(),
168 temp_dir: None,
169 task: None,
170 ncalls: 0,
171 };
172 Ok(bbm)
173 }
174
175 }
184
185 #[test]
186 fn test_env() -> Result<()> {
187 let d = new_scratch_directory(Some("/scratch/test".as_ref()))?;
188 assert!(d.path().exists());
189 let d = new_scratch_directory(None)?;
190 assert!(d.path().exists());
191 Ok(())
192 }
193}
194impl BlackBoxModel {
198 fn compute_normal(&mut self, mol: &Molecule) -> Result<Computed> {
199 let txt = self.render_input(&mol)?;
201
202 let output = self.submit_cmd(&txt)?;
204
205 let mp = output
207 .parse()
208 .with_context(|| format!("failed to parse computed results: {:?}", output))?;
209
210 self.ncalls += 1;
211 Ok(mp)
212 }
213
214 fn compute_normal_bunch(&mut self, mols: &[Molecule]) -> Result<Vec<Computed>> {
215 let txt = self.render_input_bunch(mols)?;
217
218 let output = self.submit_cmd(&txt)?;
220
221 let all = Computed::parse_all(&output)?;
223
224 self.ncalls += 1;
225 Ok(all)
226 }
227}
228impl BlackBoxModel {
232 pub fn render_input(&self, mol: &Molecule) -> Result<String> {
234 for (i, a) in mol.atoms() {
236 let p = a.position();
237 if p.iter().any(|x| x.is_nan()) {
238 error!("Invalid position of atom {}: {:?}", i, p);
239 bail!("Molecule has invalid data in positions.");
240 }
241 }
242 let txt = mol.render_with(&self.tpl_file)?;
244
245 Ok(txt)
246 }
247
248 pub fn render_input_bunch(&self, mols: &[Molecule]) -> Result<String> {
250 let mut txt = String::new();
251 for mol in mols.iter() {
252 let part = self.render_input(&mol)?;
253 txt.push_str(&part);
254 }
255
256 Ok(txt)
257 }
258}
259impl BlackBoxModel {
263 pub fn from_dir<P: AsRef<Path>>(dir: P) -> Result<Self> {
265 Self::from_dotenv(dir.as_ref()).context("Initialize BlackBoxModel failure.")
266 }
267
268 pub fn keep_scratch_files(self) {
270 if let Some(tdir) = self.temp_dir {
271 let path = tdir.into_path();
272 println!("Directory for scratch files: {}", path.display());
273 } else {
274 warn!("No temp dir found.");
275 }
276 }
277
278 pub fn number_of_evaluations(&self) -> usize {
280 self.ncalls
281 }
282}
283impl ChemicalModel for BlackBoxModel {
287 fn compute(&mut self, mol: &Molecule) -> Result<Computed> {
288 let mp = self.compute_normal(mol)?;
289
290 debug_assert!({
293 let n = mol.natoms();
294 if let Some(pmol) = mp.get_molecule() {
295 pmol.natoms() == n
296 } else {
297 true
298 }
299 });
300
301 Ok(mp)
302 }
303
304 fn compute_bunch(&mut self, mols: &[Molecule]) -> Result<Vec<Computed>> {
305 let all = self.compute_normal_bunch(mols)?;
306
307 debug_assert_eq!(mols.len(), all.len());
309 Ok(all)
310 }
311}
312#[test]
316fn test_bbm() -> Result<()> {
317 let bbm_vasp = "./tests/files/vasp-sp";
319 let bbm_siesta = "./tests/files/siesta-sp";
320 let vasp = BlackBoxModel::from_dir(bbm_vasp)?;
321 let siesta = BlackBoxModel::from_dir(bbm_siesta)?;
322
323 assert!(vasp.tpl_file.ends_with("input.tera"));
325 assert!(siesta.tpl_file.ends_with("input.hbs"));
327
328 Ok(())
329}
330