use std::fmt::Write as _;
use std::path::Path;
use ndarray::{Array1, ArrayView2};
use refeff_core::{XSPH_AXAFS_COLUMN_COUNT, XsphAxafs};
use crate::error::{IoError, Result};
use crate::format::write_fortran_exp;
const AXAFS_ROW_WIDTH: usize = XSPH_AXAFS_COLUMN_COUNT;
#[derive(Debug, Clone, PartialEq)]
pub struct AxafsDatData {
pub header_lines: Vec<String>,
pub energy_ev: Array1<f64>,
pub edge_relative_energy_ev: Array1<f64>,
pub wave_number_inverse_angstrom: Array1<f64>,
pub atomic_absorption: Array1<f64>,
pub atomic_background: Array1<f64>,
pub chi_atomic: Array1<f64>,
}
impl AxafsDatData {
#[must_use]
pub fn point_count(&self) -> usize {
self.energy_ev.len()
}
}
pub fn axafs_dat_from_xsph_axafs(axafs: &XsphAxafs) -> Result<AxafsDatData> {
axafs_dat_from_rows(axafs.rows.view())
}
pub fn axafs_dat_from_rows(rows: ArrayView2<'_, f64>) -> Result<AxafsDatData> {
if rows.ncols() != AXAFS_ROW_WIDTH {
return Err(IoError::AxafsDatShape {
field: "rows",
actual: rows.ncols(),
expected: AXAFS_ROW_WIDTH,
});
}
let data = AxafsDatData {
header_lines: default_axafs_header_lines(),
energy_ev: rows.column(0).to_owned(),
edge_relative_energy_ev: rows.column(1).to_owned(),
wave_number_inverse_angstrom: rows.column(2).to_owned(),
atomic_absorption: rows.column(3).to_owned(),
atomic_background: rows.column(4).to_owned(),
chi_atomic: rows.column(5).to_owned(),
};
validate_axafs_dat(&data)?;
Ok(data)
}
pub fn axafs_dat_string(data: &AxafsDatData) -> Result<String> {
validate_axafs_dat(data)?;
let mut out = String::new();
for line in &data.header_lines {
writeln!(out, "{line}")?;
}
for (((((energy, relative), wave_number), absorption), background), chi) in data
.energy_ev
.iter()
.zip(data.edge_relative_energy_ev.iter())
.zip(data.wave_number_inverse_angstrom.iter())
.zip(data.atomic_absorption.iter())
.zip(data.atomic_background.iter())
.zip(data.chi_atomic.iter())
{
write!(out, " {:11.3}{:11.3}{:8.3}", energy, relative, wave_number)?;
write_fortran_exp(&mut out, *absorption, 13, 5)?;
write_fortran_exp(&mut out, *background, 13, 5)?;
write_fortran_exp(&mut out, *chi, 13, 5)?;
out.push('\n');
}
Ok(out)
}
pub fn parse_axafs_dat(text: &str) -> Result<AxafsDatData> {
let mut header_lines = Vec::new();
let mut energy_ev = Vec::new();
let mut edge_relative_energy_ev = Vec::new();
let mut wave_number_inverse_angstrom = Vec::new();
let mut atomic_absorption = Vec::new();
let mut atomic_background = Vec::new();
let mut chi_atomic = Vec::new();
for (index, raw) in text.lines().enumerate() {
let line = raw.trim();
if line.is_empty() {
continue;
}
if line.starts_with('#') {
header_lines.push(raw.trim_end().to_string());
continue;
}
let line_number = index + 1;
let tokens = line.split_whitespace().collect::<Vec<_>>();
if tokens.len() != AXAFS_ROW_WIDTH {
return Err(IoError::AxafsDatRowWidth {
line: line_number,
actual: tokens.len(),
expected: AXAFS_ROW_WIDTH,
});
}
energy_ev.push(parse_f64(line_number, "energy", tokens[0])?);
edge_relative_energy_ev.push(parse_f64(line_number, "edge_relative_energy", tokens[1])?);
wave_number_inverse_angstrom.push(parse_f64(line_number, "wave_number", tokens[2])?);
atomic_absorption.push(parse_f64(line_number, "atomic_absorption", tokens[3])?);
atomic_background.push(parse_f64(line_number, "atomic_background", tokens[4])?);
chi_atomic.push(parse_f64(line_number, "chi_atomic", tokens[5])?);
}
let data = AxafsDatData {
header_lines,
energy_ev: Array1::from_vec(energy_ev),
edge_relative_energy_ev: Array1::from_vec(edge_relative_energy_ev),
wave_number_inverse_angstrom: Array1::from_vec(wave_number_inverse_angstrom),
atomic_absorption: Array1::from_vec(atomic_absorption),
atomic_background: Array1::from_vec(atomic_background),
chi_atomic: Array1::from_vec(chi_atomic),
};
validate_axafs_dat(&data)?;
Ok(data)
}
pub fn read_axafs_dat(path: impl AsRef<Path>) -> Result<AxafsDatData> {
let path = path.as_ref();
let text = std::fs::read_to_string(path).map_err(|source| IoError::io(path, source))?;
parse_axafs_dat(&text)
}
pub fn write_axafs_dat(path: impl AsRef<Path>, data: &AxafsDatData) -> Result<()> {
let path = path.as_ref();
std::fs::write(path, axafs_dat_string(data)?).map_err(|source| IoError::io(path, source))
}
fn validate_axafs_dat(data: &AxafsDatData) -> Result<()> {
let point_count = data.energy_ev.len();
if point_count == 0 {
return Err(IoError::InvalidAxafsDat {
field: "rows",
message: "at least one AXAFS row is required".to_string(),
});
}
validate_len(
"edge_relative_energy",
data.edge_relative_energy_ev.len(),
point_count,
)?;
validate_len(
"wave_number",
data.wave_number_inverse_angstrom.len(),
point_count,
)?;
validate_len(
"atomic_absorption",
data.atomic_absorption.len(),
point_count,
)?;
validate_len(
"atomic_background",
data.atomic_background.len(),
point_count,
)?;
validate_len("chi_atomic", data.chi_atomic.len(), point_count)?;
for (row, (((((energy, relative), wave_number), absorption), background), chi)) in data
.energy_ev
.iter()
.zip(data.edge_relative_energy_ev.iter())
.zip(data.wave_number_inverse_angstrom.iter())
.zip(data.atomic_absorption.iter())
.zip(data.atomic_background.iter())
.zip(data.chi_atomic.iter())
.enumerate()
{
validate_finite("energy", row, *energy)?;
validate_finite("edge_relative_energy", row, *relative)?;
validate_finite("wave_number", row, *wave_number)?;
validate_finite("atomic_absorption", row, *absorption)?;
validate_finite("atomic_background", row, *background)?;
validate_finite("chi_atomic", row, *chi)?;
}
Ok(())
}
fn validate_len(field: &'static str, actual: usize, expected: usize) -> Result<()> {
if actual == expected {
Ok(())
} else {
Err(IoError::AxafsDatShape {
field,
actual,
expected,
})
}
}
fn validate_finite(field: &'static str, row: usize, value: f64) -> Result<()> {
if value.is_finite() {
Ok(())
} else {
Err(IoError::InvalidAxafsDat {
field,
message: format!("row {} is not finite", row + 1),
})
}
}
fn parse_f64(line: usize, field: &'static str, token: &str) -> Result<f64> {
token
.replace(['D', 'd'], "E")
.parse::<f64>()
.map_err(|_| IoError::AxafsDatParse {
field,
line,
token: token.to_string(),
})
}
fn default_axafs_header_lines() -> Vec<String> {
vec![
" # File contains AXAFS. See manual for details.".to_string(),
" #--------------------------------------------------------------".to_string(),
" # e, e(wrt edge), k, mu_at=(1+chi_at)*mu0_at, mu0_at, chi_at @#".to_string(),
]
}
#[cfg(test)]
mod tests {
use super::{
AxafsDatData, axafs_dat_from_rows, axafs_dat_from_xsph_axafs, axafs_dat_string,
parse_axafs_dat,
};
use ndarray::{Array1, arr2};
use refeff_core::XsphAxafs;
#[test]
fn parses_feff_axafs_reference_shape() -> crate::Result<()> {
let data = parse_axafs_dat(AXAFS_DAT)?;
assert_eq!(data.point_count(), 2);
assert_eq!(data.header_lines.len(), 3);
assert_eq!(data.energy_ev[0], 8979.0);
assert_eq!(data.edge_relative_energy_ev[1], 1.5);
assert_eq!(data.wave_number_inverse_angstrom[1], 0.627);
assert_eq!(data.atomic_absorption[0], 1.23456);
assert_eq!(data.atomic_background[1], 1.11111);
assert_eq!(data.chi_atomic[1], -0.045);
Ok(())
}
#[test]
fn roundtrips_axafs_text() -> crate::Result<()> {
let data = parse_axafs_dat(AXAFS_DAT)?;
let rendered = axafs_dat_string(&data)?;
let reparsed = parse_axafs_dat(&rendered)?;
assert_eq!(reparsed, data);
Ok(())
}
#[test]
fn builds_axafs_dat_from_core_rows() -> crate::Result<()> {
let rows = arr2(&[
[8979.0, 0.0, 0.0, 1.25, 1.1, 0.136_363_636],
[8980.5, 1.5, 0.627, 1.4, 1.2, 0.166_666_667],
]);
let data = axafs_dat_from_rows(rows.view())?;
let axafs = XsphAxafs {
rows,
coefficients: [1.0, 2.0, 3.0],
normalization: 4.0,
};
assert_eq!(axafs_dat_from_xsph_axafs(&axafs)?, data);
Ok(())
}
#[test]
fn rejects_bad_axafs_inputs() {
assert!(parse_axafs_dat("# header only\n").is_err());
assert!(parse_axafs_dat("1 2 3 4 5\n").is_err());
assert!(parse_axafs_dat("1 2 3 bad 5 6\n").is_err());
assert!(parse_axafs_dat("1 2 3 4 NaN 6\n").is_err());
assert!(axafs_dat_string(&empty_axafs()).is_err());
assert!(axafs_dat_from_rows(arr2(&[[1.0, 2.0, 3.0]]).view()).is_err());
}
fn empty_axafs() -> AxafsDatData {
AxafsDatData {
header_lines: Vec::new(),
energy_ev: Array1::from_vec(Vec::new()),
edge_relative_energy_ev: Array1::from_vec(Vec::new()),
wave_number_inverse_angstrom: Array1::from_vec(Vec::new()),
atomic_absorption: Array1::from_vec(Vec::new()),
atomic_background: Array1::from_vec(Vec::new()),
chi_atomic: Array1::from_vec(Vec::new()),
}
}
const AXAFS_DAT: &str = "\
# File contains AXAFS. See manual for details.
#--------------------------------------------------------------
# e, e(wrt edge), k, mu_at=(1+chi_at)*mu0_at, mu0_at, chi_at @#
8979.000 0.000 0.000 1.23456E+00 1.00000E+00 2.34560E-01
8980.500 1.500 0.627 1.06111D+00 1.11111D+00 -4.50000D-02
";
}