use crate::feffinp::crystal::Potential;
use crate::feffinp::element::z_to_symbol;
use crate::feffinp::{Edge, Error};
#[derive(Debug, Clone, PartialEq)]
pub struct FeffAtom {
pub xyz: [f64; 3],
pub ipot: usize,
pub tag: String,
pub distance: Option<f64>,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct FeffInp {
pub title: Vec<String>,
pub edge: Option<Edge>,
pub potentials: Vec<Potential>,
pub atoms: Vec<FeffAtom>,
}
#[derive(Clone, Copy, PartialEq)]
enum Section {
None,
Potentials,
Atoms,
}
fn is_data_token(t: &str) -> bool {
t.chars()
.next()
.is_some_and(|c| c.is_ascii_digit() || c == '+' || c == '-' || c == '.')
}
impl FeffInp {
pub fn potential(&self, ipot: usize) -> Option<&Potential> {
self.potentials.iter().find(|p| p.ipot == ipot)
}
pub fn parse(text: &str) -> Result<FeffInp, Error> {
let mut out = FeffInp::default();
let mut section = Section::None;
for raw in text.lines() {
let trimmed = raw.trim();
if trimmed.is_empty() || trimmed.starts_with('*') {
continue;
}
let line = trimmed.split('*').next().unwrap_or("").trim();
if line.is_empty() {
continue;
}
let tokens: Vec<&str> = line.split_whitespace().collect();
let first = tokens[0];
if matches!(section, Section::Potentials | Section::Atoms) && is_data_token(first) {
match section {
Section::Potentials => {
if let Some(p) = parse_potential(&tokens) {
out.potentials.push(p);
}
}
Section::Atoms => {
if let Some(a) = parse_atom(&tokens)? {
out.atoms.push(a);
}
}
Section::None => unreachable!(),
}
continue;
}
match first.to_ascii_uppercase().as_str() {
"TITLE" => {
section = Section::None;
let rest = line
.split_once(char::is_whitespace)
.map(|x| x.1)
.unwrap_or("")
.trim();
if !rest.is_empty() {
out.title.push(rest.to_owned());
}
}
"EDGE" => {
section = Section::None;
if let Some(t) = tokens.get(1) {
out.edge = Edge::from_str_ci(t);
}
}
"HOLE" => {
section = Section::None;
if let Some(n) = tokens.get(1).and_then(|t| t.parse::<u32>().ok()) {
out.edge = match n {
1 => Some(Edge::K),
2 => Some(Edge::L1),
3 => Some(Edge::L2),
4 => Some(Edge::L3),
_ => out.edge,
};
}
}
"POTENTIALS" => section = Section::Potentials,
"ATOMS" => section = Section::Atoms,
"END" => section = Section::None,
_ => section = Section::None,
}
}
Ok(out)
}
}
fn parse_potential(tokens: &[&str]) -> Option<Potential> {
let ipot = tokens.first()?.parse::<usize>().ok()?;
let z = tokens.get(1)?.parse::<u32>().ok()?;
let tag = tokens
.get(2)
.map(|s| (*s).to_owned())
.or_else(|| z_to_symbol(z).map(str::to_owned))
.unwrap_or_default();
Some(Potential { ipot, z, tag })
}
fn parse_atom(tokens: &[&str]) -> Result<Option<FeffAtom>, Error> {
if tokens.len() < 4 {
return Ok(None);
}
let parse_f = |i: usize| -> Result<f64, Error> {
tokens[i]
.parse::<f64>()
.map_err(|_| Error::Parse(format!("bad coordinate `{}`", tokens[i])))
};
let x = parse_f(0)?;
let y = parse_f(1)?;
let z = parse_f(2)?;
let ipot = tokens[3]
.parse::<usize>()
.map_err(|_| Error::Parse(format!("bad ipot `{}`", tokens[3])))?;
let tag = tokens.get(4).map(|s| (*s).to_owned()).unwrap_or_default();
let distance = tokens.get(5).and_then(|s| s.parse::<f64>().ok());
Ok(Some(FeffAtom {
xyz: [x, y, z],
ipot,
tag,
distance,
}))
}
#[cfg(test)]
mod tests {
use super::*;
const SAMPLE: &str = "\
* a comment line
TITLE copper test
HOLE 1
EDGE K
S02 1.0
CONTROL 1 1 1 1 1 1
RMAX 5.0
POTENTIALS
* ipot Z tag
0 29 Cu
1 29 Cu
ATOMS
* x y z ipot tag distance
0.00000 0.00000 0.00000 0 Cu1 0.00000
1.80500 1.80500 0.00000 1 Cu 2.55266 * Cu.1
-1.80500 -1.80500 0.00000 1 Cu 2.55266
END
* trailing comment
";
#[test]
fn parses_sample() {
let f = FeffInp::parse(SAMPLE).expect("parse");
assert_eq!(f.title, vec!["copper test".to_owned()]);
assert_eq!(f.edge, Some(Edge::K));
assert_eq!(f.potentials.len(), 2);
assert_eq!(f.potential(0).unwrap().z, 29);
assert_eq!(f.atoms.len(), 3);
assert_eq!(f.atoms[0].ipot, 0);
assert_eq!(f.atoms[0].tag, "Cu1");
assert_eq!(f.atoms[1].distance, Some(2.55266));
assert!((f.atoms[1].xyz[0] - 1.805).abs() < 1e-9);
}
#[test]
fn hole_maps_to_edge_when_no_edge_card() {
let text = " HOLE 3\n ATOMS\n 0 0 0 0 Fe\n END\n";
let f = FeffInp::parse(text).expect("parse");
assert_eq!(f.edge, Some(Edge::L2));
assert_eq!(f.atoms.len(), 1);
assert_eq!(f.atoms[0].distance, None);
}
#[test]
fn unknown_card_ends_section() {
let text = " ATOMS\n 0 0 0 0 Fe\n RPATH 7.0\n 1 0 0 1 O\n END\n";
let f = FeffInp::parse(text).expect("parse");
assert_eq!(f.atoms.len(), 1, "second row is after a card, not in ATOMS");
}
}