use std::path::{Path, PathBuf};
use std::str::FromStr;
use crate::{IoError, Result};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PathsInput {
pub control: PathsControl,
pub criteria: PathsCriteria,
pub ica: i32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PathsControl {
pub mpath: i32,
pub ms: i32,
pub nncrit: i32,
pub nlegxx: i32,
pub ipr4: i32,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PathsCriteria {
pub critpw: f64,
pub pcritk: f64,
pub pcrith: f64,
pub rmax: f64,
pub rfms2: f64,
}
impl PathsInput {
pub fn parse_str(source: impl Into<PathBuf>, text: &str) -> Result<Self> {
let mut parser = PathsInputParser::new(source.into(), text);
parser.parse()
}
}
pub fn paths_input_string(input: &PathsInput) -> Result<String> {
validate_paths_input(input)?;
let mut out = String::new();
out.push_str("mpath, ms, nncrit, nlegxx, ipr4\n");
push_i4_row(
&mut out,
[
input.control.mpath,
input.control.ms,
input.control.nncrit,
input.control.nlegxx,
input.control.ipr4,
],
);
out.push_str("critpw, pcritk, pcrith, rmax, rfms2\n");
out.push_str(&format!(
"{:13.5}{:13.5}{:13.5}{:13.5}{:13.5}\n",
input.criteria.critpw,
input.criteria.pcritk,
input.criteria.pcrith,
input.criteria.rmax,
input.criteria.rfms2
));
out.push_str("ica\n");
push_i4_row(&mut out, [input.ica]);
Ok(out)
}
fn validate_paths_input(input: &PathsInput) -> Result<()> {
validate_finite("critpw", input.criteria.critpw)?;
validate_finite("pcritk", input.criteria.pcritk)?;
validate_finite("pcrith", input.criteria.pcrith)?;
validate_finite("rmax", input.criteria.rmax)?;
validate_finite("rfms2", input.criteria.rfms2)
}
fn validate_finite(field: &'static str, value: f64) -> Result<()> {
if value.is_finite() {
Ok(())
} else {
Err(IoError::Parse {
path: "paths.inp".into(),
line: 0,
message: format!("{field} must be finite"),
})
}
}
fn push_i4_row(out: &mut String, values: impl IntoIterator<Item = i32>) {
for value in values {
out.push_str(&format!("{value:4}"));
}
out.push('\n');
}
struct PathsInputParser<'a> {
source: PathBuf,
lines: std::iter::Enumerate<std::str::Lines<'a>>,
}
impl<'a> PathsInputParser<'a> {
fn new(source: PathBuf, text: &'a str) -> Self {
Self {
source,
lines: text.lines().enumerate(),
}
}
fn parse(&mut self) -> Result<PathsInput> {
self.expect_header("mpath, ms, nncrit, nlegxx, ipr4")?;
let control_values = self.parse_values::<i32>(5, "PATHS control line")?;
let control = PathsControl {
mpath: control_values[0],
ms: control_values[1],
nncrit: control_values[2],
nlegxx: control_values[3],
ipr4: control_values[4],
};
self.expect_header("critpw, pcritk, pcrith, rmax, rfms2")?;
let criteria_values = self.parse_values::<f64>(5, "PATHS criteria line")?;
let criteria = PathsCriteria {
critpw: criteria_values[0],
pcritk: criteria_values[1],
pcrith: criteria_values[2],
rmax: criteria_values[3],
rfms2: criteria_values[4],
};
self.expect_header("ica")?;
let ica = self.parse_values::<i32>(1, "PATHS ica line")?[0];
Ok(PathsInput {
control,
criteria,
ica,
})
}
fn expect_header(&mut self, expected: &str) -> Result<()> {
let (line_number, line) = self.next_line(expected)?;
if line.trim() == expected {
Ok(())
} else {
Err(self.parse_error(
line_number,
format!("expected header {expected:?}, found {line:?}"),
))
}
}
fn parse_values<T>(&mut self, count: usize, description: &str) -> Result<Vec<T>>
where
T: FromStr,
{
let (line_number, line) = self.next_line(description)?;
let fields: Vec<&str> = line.split_whitespace().collect();
if fields.len() < count {
return Err(self.parse_error(
line_number,
format!("{description} requires {count} fields"),
));
}
fields
.iter()
.take(count)
.map(|field| parse_field(&self.source, line_number, field))
.collect()
}
fn next_line(&mut self, description: &str) -> Result<(usize, &'a str)> {
self.lines
.next()
.map(|(index, line)| (index + 1, line))
.ok_or_else(|| self.parse_error(0, format!("expected {description}")))
}
fn parse_error(&self, line: usize, message: impl Into<String>) -> IoError {
IoError::Parse {
path: self.source.clone(),
line,
message: message.into(),
}
}
}
fn parse_field<T>(source: &Path, line: usize, field: &str) -> Result<T>
where
T: FromStr,
{
field.parse::<T>().map_err(|_| IoError::Parse {
path: source.to_path_buf(),
line,
message: format!("invalid numeric field {field:?}"),
})
}
#[cfg(test)]
mod tests {
use crate::{FeffDocument, FeffInput, rdinp};
use super::{PathsInput, paths_input_string};
#[test]
fn parses_generated_paths_input() -> crate::Result<()> {
let input = FeffInput::parse_str(
"feff.inp",
r#"
CONTROL 1 1 1 1 1 1
PRINT 0 0 0 2 0 0
CRITERIA 3.1 4.2
PCRITERIA 0.5 0.6
RPATH 5.5
NLEG 6
FMS 4.0
POTENTIALS
0 29 Cu
1 29 Cu
ATOMS
0.0 0.0 0.0 0 Cu0
1.0 0.0 0.0 1 Cu1
END
"#,
)?;
let document = FeffDocument::from_input(&input)?;
let text = rdinp::paths_inp_string(&document)?;
let paths = PathsInput::parse_str("paths.inp", &text)?;
assert_eq!(paths.control.mpath, 1);
assert_eq!(paths.control.ms, 1);
assert_eq!(paths.control.nncrit, 0);
assert_eq!(paths.control.nlegxx, 6);
assert_eq!(paths.control.ipr4, 2);
assert_eq!(paths.criteria.critpw, 4.2);
assert_eq!(paths.criteria.pcritk, 0.5);
assert_eq!(paths.criteria.pcrith, 0.6);
assert_eq!(paths.criteria.rmax, 5.5);
assert_eq!(paths.criteria.rfms2, 4.0);
assert_eq!(paths.ica, -1);
Ok(())
}
#[test]
fn renders_generated_paths_input() -> crate::Result<()> {
let input = FeffInput::parse_str(
"feff.inp",
r#"
CONTROL 1 1 1 1 1 1
PRINT 0 0 0 2 0 0
CRITERIA 3.1 4.2
PCRITERIA 0.5 0.6
RPATH 5.5
NLEG 6
FMS 4.0
POTENTIALS
0 29 Cu
1 29 Cu
ATOMS
0.0 0.0 0.0 0 Cu0
1.0 0.0 0.0 1 Cu1
END
"#,
)?;
let document = FeffDocument::from_input(&input)?;
let expected = rdinp::paths_inp_string(&document)?;
let paths = PathsInput::parse_str("paths.inp", &expected)?;
let rendered = paths_input_string(&paths)?;
let reparsed = PathsInput::parse_str("paths.inp", &rendered)?;
assert_eq!(rendered, expected);
assert_eq!(reparsed, paths);
Ok(())
}
#[test]
fn rejects_invalid_paths_rendering() {
let input = PathsInput {
control: super::PathsControl {
mpath: 1,
ms: 1,
nncrit: 0,
nlegxx: 7,
ipr4: 0,
},
criteria: super::PathsCriteria {
critpw: f64::NAN,
pcritk: 0.0,
pcrith: 0.0,
rmax: 5.5,
rfms2: -1.0,
},
ica: -1,
};
assert!(paths_input_string(&input).is_err());
}
}