use std::ffi::OsStr;
use std::fmt;
use std::path::{Path, PathBuf};
use std::process::Command;
pub const MODULES: [&str; 6] = [
"feff8l_rdinp",
"feff8l_pot",
"feff8l_xsph",
"feff8l_pathfinder",
"feff8l_genfmt",
"feff8l_ff2x",
];
pub const BIN_DIR_ENV: &str = "FEFF8L_DIR";
#[derive(Debug, Clone, Default)]
pub struct Feff8l {
bin_dir: Option<PathBuf>,
}
#[derive(Debug, Clone)]
pub struct RunOutput {
pub workdir: PathBuf,
pub dat_files: Vec<PathBuf>,
}
#[derive(Debug)]
pub enum FeffError {
ExeNotFound {
module: String,
searched: Vec<PathBuf>,
},
NoFeffInp(PathBuf),
Module {
module: String,
code: Option<i32>,
stderr: String,
},
NoOutput(PathBuf),
#[cfg(feature = "feff10")]
Backend {
backend: &'static str,
message: String,
},
Io {
action: String,
source: std::io::Error,
},
}
impl fmt::Display for FeffError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FeffError::ExeNotFound { module, searched } => {
write!(f, "FEFF8L executable `{module}` not found; looked in")?;
for p in searched {
write!(f, " {}", p.display())?;
}
Ok(())
}
FeffError::NoFeffInp(p) => write!(f, "no feff.inp at {}", p.display()),
FeffError::Module {
module,
code,
stderr,
} => {
let code = code.map_or_else(|| "signal".to_string(), |c| c.to_string());
write!(f, "module `{module}` exited {code}: {}", stderr.trim())
}
FeffError::NoOutput(p) => {
write!(f, "pipeline produced no feffNNNN.dat in {}", p.display())
}
#[cfg(feature = "feff10")]
FeffError::Backend { backend, message } => {
write!(f, "{backend} pipeline failed: {message}")
}
FeffError::Io { action, source } => write!(f, "{action}: {source}"),
}
}
}
impl std::error::Error for FeffError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
FeffError::Io { source, .. } => Some(source),
_ => None,
}
}
}
impl Feff8l {
pub fn new() -> Self {
Self::default()
}
pub fn with_bin_dir(dir: impl Into<PathBuf>) -> Self {
Self {
bin_dir: Some(dir.into()),
}
}
fn command_for(&self, module: &str) -> Result<Command, FeffError> {
let mut dirs: Vec<PathBuf> = Vec::new();
if let Some(d) = &self.bin_dir {
dirs.push(d.clone());
} else if let Some(d) = std::env::var_os(BIN_DIR_ENV) {
dirs.push(PathBuf::from(d));
}
if dirs.is_empty() {
return Ok(Command::new(module));
}
for d in &dirs {
let p = d.join(module);
if p.is_file() {
return Ok(Command::new(p));
}
}
Err(FeffError::ExeNotFound {
module: module.to_string(),
searched: dirs.iter().map(|d| d.join(module)).collect(),
})
}
pub fn run_in(&self, workdir: &Path) -> Result<RunOutput, FeffError> {
let inp = workdir.join("feff.inp");
if !inp.is_file() {
return Err(FeffError::NoFeffInp(inp));
}
for module in MODULES {
let mut cmd = self.command_for(module)?;
cmd.current_dir(workdir);
let output = cmd.output().map_err(|e| FeffError::Io {
action: format!("spawn {module}"),
source: e,
})?;
if !output.status.success() {
return Err(FeffError::Module {
module: module.to_string(),
code: output.status.code(),
stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
});
}
}
let dat_files = collect_dat_files(workdir)?;
Ok(RunOutput {
workdir: workdir.to_path_buf(),
dat_files,
})
}
pub fn run(&self, feff_inp: &str, workdir: &Path) -> Result<RunOutput, FeffError> {
std::fs::create_dir_all(workdir).map_err(|e| FeffError::Io {
action: format!("create {}", workdir.display()),
source: e,
})?;
std::fs::write(workdir.join("feff.inp"), feff_inp).map_err(|e| FeffError::Io {
action: "write feff.inp".to_string(),
source: e,
})?;
self.run_in(workdir)
}
}
#[cfg(feature = "feff10")]
#[derive(Debug, Clone, Default)]
pub struct Feff10 {
_priv: (),
}
#[cfg(feature = "feff10")]
impl Feff10 {
pub fn new() -> Self {
Self::default()
}
pub fn run_in(&self, workdir: &Path) -> Result<RunOutput, FeffError> {
let inp = workdir.join("feff.inp");
if !inp.is_file() {
return Err(FeffError::NoFeffInp(inp));
}
let content = std::fs::read_to_string(&inp).map_err(|e| FeffError::Io {
action: format!("read {}", inp.display()),
source: e,
})?;
feff10::run_str(&content, workdir).map_err(|e| FeffError::Backend {
backend: "feff10",
message: e.to_string(),
})?;
let dat_files = collect_dat_files(workdir)?;
Ok(RunOutput {
workdir: workdir.to_path_buf(),
dat_files,
})
}
pub fn run(&self, feff_inp: &str, workdir: &Path) -> Result<RunOutput, FeffError> {
std::fs::create_dir_all(workdir).map_err(|e| FeffError::Io {
action: format!("create {}", workdir.display()),
source: e,
})?;
std::fs::write(workdir.join("feff.inp"), feff_inp).map_err(|e| FeffError::Io {
action: "write feff.inp".to_string(),
source: e,
})?;
self.run_in(workdir)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Backend {
#[cfg_attr(not(feature = "feff10"), default)]
Feff8l,
#[cfg(feature = "feff10")]
#[default]
Feff10,
}
impl Backend {
pub fn run(self, feff_inp: &str, workdir: &Path) -> Result<RunOutput, FeffError> {
match self {
Backend::Feff8l => Feff8l::new().run(feff_inp, workdir),
#[cfg(feature = "feff10")]
Backend::Feff10 => Feff10::new().run(feff_inp, workdir),
}
}
}
fn collect_dat_files(workdir: &Path) -> Result<Vec<PathBuf>, FeffError> {
let mut dat_files: Vec<PathBuf> = std::fs::read_dir(workdir)
.map_err(|e| FeffError::Io {
action: format!("read_dir {}", workdir.display()),
source: e,
})?
.filter_map(|e| e.ok().map(|e| e.path()))
.filter(|p| {
p.file_name()
.and_then(OsStr::to_str)
.is_some_and(is_feff_dat)
})
.collect();
dat_files.sort();
if dat_files.is_empty() {
return Err(FeffError::NoOutput(workdir.to_path_buf()));
}
Ok(dat_files)
}
fn is_feff_dat(name: &str) -> bool {
match name
.strip_prefix("feff")
.and_then(|s| s.strip_suffix(".dat"))
{
Some(mid) => !mid.is_empty() && mid.bytes().all(|b| b.is_ascii_digit()),
None => false,
}
}
#[cfg(test)]
mod tests {
use super::is_feff_dat;
#[test]
fn feff_dat_name_matching() {
assert!(is_feff_dat("feff0001.dat"));
assert!(is_feff_dat("feff1.dat"));
assert!(is_feff_dat("feff9999.dat"));
assert!(!is_feff_dat("feff.dat")); assert!(!is_feff_dat("feffNNNN.dat")); assert!(!is_feff_dat("chi.dat"));
assert!(!is_feff_dat("feff0001.txt"));
assert!(!is_feff_dat("xfeff0001.dat")); }
}