use std::path::Path;
use ndarray::{Array1, ArrayView1};
use num_complex::Complex64;
use refeff_core::{FullSpectrumHamakerInput, full_spectrum_hamaker_transform};
use crate::error::{IoError, Result};
use crate::format::write_fortran_zero_scaled_exp;
const HAMAKER_DAT_PATH: &str = "hamaker.dat";
const HAMAKER_DAT_ROW_WIDTH: usize = 3;
#[derive(Debug, Clone, PartialEq)]
pub struct HamakerDatData {
pub header_lines: Vec<String>,
pub omega: Array1<f64>,
pub imaginary_axis_epsilon: Array1<Complex64>,
}
impl HamakerDatData {
#[must_use]
pub fn point_count(&self) -> usize {
self.omega.len()
}
}
pub fn hamaker_dat_from_fullspectrum_epsilon(
header_lines: Vec<String>,
omega: ArrayView1<'_, f64>,
epsilon_minus_one: ArrayView1<'_, Complex64>,
) -> Result<HamakerDatData> {
let transform = full_spectrum_hamaker_transform(FullSpectrumHamakerInput {
omega,
epsilon: epsilon_minus_one,
})
.map_err(|source| invalid_hamaker_dat_error("hamaker_transform", source.to_string()))?;
let data = HamakerDatData {
header_lines,
omega: omega.to_owned(),
imaginary_axis_epsilon: transform.mapv(|value| Complex64::new(value, 0.0)),
};
validate_hamaker_dat(&data)?;
Ok(data)
}
pub fn hamaker_dat_string(data: &HamakerDatData) -> Result<String> {
validate_hamaker_dat(data)?;
let mut out = String::new();
for line in &data.header_lines {
out.push_str(line);
out.push('\n');
}
for (omega, epsilon) in data.omega.iter().zip(data.imaginary_axis_epsilon.iter()) {
write_hamaker_row(&mut out, [*omega, epsilon.re, epsilon.im])?;
}
Ok(out)
}
pub fn parse_hamaker_dat(text: &str) -> Result<HamakerDatData> {
let mut header_lines = Vec::new();
let mut omega = Vec::new();
let mut imaginary_axis_epsilon = Vec::new();
for (index, raw) in text.lines().enumerate() {
let line_number = index + 1;
let line = raw.trim_end();
let tokens = line.split_whitespace().collect::<Vec<_>>();
if tokens.first().is_some_and(|token| is_numeric_token(token)) {
if tokens.len() != HAMAKER_DAT_ROW_WIDTH {
return parse_error(
line_number,
format!(
"hamaker.dat row has {} token(s), expected {HAMAKER_DAT_ROW_WIDTH}",
tokens.len()
),
);
}
omega.push(parse_f64(line_number, "omega", tokens[0])?);
imaginary_axis_epsilon.push(Complex64::new(
parse_f64(line_number, "imaginary-axis epsilon real", tokens[1])?,
parse_f64(line_number, "imaginary-axis epsilon imaginary", tokens[2])?,
));
} else if !line.trim().is_empty() {
header_lines.push(line.to_string());
}
}
let data = HamakerDatData {
header_lines,
omega: Array1::from_vec(omega),
imaginary_axis_epsilon: Array1::from_vec(imaginary_axis_epsilon),
};
validate_hamaker_dat(&data)?;
Ok(data)
}
pub fn write_hamaker_dat(path: impl AsRef<Path>, data: &HamakerDatData) -> Result<()> {
let path = path.as_ref();
std::fs::write(path, hamaker_dat_string(data)?).map_err(|source| IoError::io(path, source))
}
pub fn read_hamaker_dat(path: impl AsRef<Path>) -> Result<HamakerDatData> {
let path = path.as_ref();
let text = std::fs::read_to_string(path).map_err(|source| IoError::io(path, source))?;
parse_hamaker_dat(&text)
}
fn write_hamaker_row(out: &mut String, fields: [f64; HAMAKER_DAT_ROW_WIDTH]) -> Result<()> {
for value in fields {
write_fortran_zero_scaled_exp(out, value, 20, 10)?;
}
out.push('\n');
Ok(())
}
fn validate_hamaker_dat(data: &HamakerDatData) -> Result<()> {
let point_count = data.point_count();
if point_count == 0 {
return invalid_hamaker_dat("rows", "at least one Hamaker row is required");
}
validate_len(
"imaginary_axis_epsilon",
data.imaginary_axis_epsilon.len(),
point_count,
)?;
for (row, omega) in data.omega.iter().enumerate() {
validate_positive("omega", *omega, row + 1)?;
}
for (row, epsilon) in data.imaginary_axis_epsilon.iter().enumerate() {
validate_finite("imaginary-axis epsilon real", epsilon.re, row + 1)?;
validate_finite("imaginary-axis epsilon imaginary", epsilon.im, row + 1)?;
}
Ok(())
}
fn validate_len(field: &'static str, actual: usize, expected: usize) -> Result<()> {
if actual == expected {
Ok(())
} else {
invalid_hamaker_dat(field, format!("got {actual} value(s), expected {expected}"))
}
}
fn validate_positive(field: &'static str, value: f64, row: usize) -> Result<()> {
if !value.is_finite() {
invalid_hamaker_dat(field, format!("row {row} value must be finite"))
} else if value <= 0.0 {
invalid_hamaker_dat(field, format!("row {row} value must be positive"))
} else {
Ok(())
}
}
fn validate_finite(field: &'static str, value: f64, row: usize) -> Result<()> {
if value.is_finite() {
Ok(())
} else {
invalid_hamaker_dat(field, format!("row {row} value must be finite"))
}
}
fn parse_f64(line: usize, field: &'static str, token: &str) -> Result<f64> {
token
.replace(['D', 'd'], "E")
.parse::<f64>()
.map_err(|_| parse_error_value(line, format!("invalid {field} value {token:?}")))
}
fn invalid_hamaker_dat<T>(field: &'static str, message: impl Into<String>) -> Result<T> {
Err(invalid_hamaker_dat_error(field, message))
}
fn invalid_hamaker_dat_error(field: &'static str, message: impl Into<String>) -> IoError {
IoError::Parse {
path: HAMAKER_DAT_PATH.into(),
line: 0,
message: format!("{field}: {}", message.into()),
}
}
fn parse_error<T>(line: usize, message: impl Into<String>) -> Result<T> {
Err(parse_error_value(line, message))
}
fn parse_error_value(line: usize, message: impl Into<String>) -> IoError {
IoError::Parse {
path: HAMAKER_DAT_PATH.into(),
line,
message: message.into(),
}
}
fn is_numeric_token(token: &str) -> bool {
token.replace(['D', 'd'], "E").parse::<f64>().is_ok()
}
#[cfg(test)]
mod tests {
use ndarray::array;
use super::*;
#[test]
fn parses_feff_fullspectrum_hamaker_dat() -> Result<()> {
let parsed = parse_hamaker_dat(HAMAKER_DAT)?;
assert_eq!(parsed.point_count(), 5);
assert_eq!(parsed.omega[0], 1.0);
assert_eq!(
parsed.imaginary_axis_epsilon[2],
Complex64::new(0.223_104_490_2, 0.0)
);
Ok(())
}
#[test]
fn roundtrips_feff_fullspectrum_hamaker_dat() -> Result<()> {
let parsed = parse_hamaker_dat(HAMAKER_DAT)?;
let rendered = hamaker_dat_string(&parsed)?;
assert_eq!(rendered, HAMAKER_DAT);
assert_eq!(parse_hamaker_dat(&rendered)?, parsed);
Ok(())
}
#[test]
fn derives_hamaker_dat_from_fullspectrum_epsilon() -> Result<()> {
let omega = array![1.0, 2.0, 4.0, 7.0, 11.0];
let epsilon = array![
Complex64::new(0.1, 0.2),
Complex64::new(0.3, 0.5),
Complex64::new(0.2, 0.25),
Complex64::new(0.4, 0.4),
Complex64::new(0.1, 0.15),
];
let rendered = hamaker_dat_string(&hamaker_dat_from_fullspectrum_epsilon(
Vec::new(),
omega.view(),
epsilon.view(),
)?)?;
assert_eq!(rendered, HAMAKER_DAT);
Ok(())
}
#[test]
fn rejects_bad_hamaker_dat_inputs() {
assert!(parse_hamaker_dat("").is_err());
assert!(parse_hamaker_dat("# only header\n").is_err());
assert!(parse_hamaker_dat("1 2\n").is_err());
assert!(parse_hamaker_dat("1 2 3 4\n").is_err());
assert!(parse_hamaker_dat("0 1 0\n").is_err());
assert!(parse_hamaker_dat("1 NaN 0\n").is_err());
let bad = HamakerDatData {
header_lines: Vec::new(),
omega: array![1.0, 2.0],
imaginary_axis_epsilon: array![Complex64::new(1.0, 0.0)],
};
assert!(hamaker_dat_string(&bad).is_err());
}
const HAMAKER_DAT: &str = " 0.1000000000E+01 0.3546469825E+00 0.0000000000E+00\n 0.2000000000E+01 0.3546469825E+00 0.0000000000E+00\n 0.4000000000E+01 0.2231044902E+00 0.0000000000E+00\n 0.7000000000E+01 0.1268866601E+00 0.0000000000E+00\n 0.1100000000E+02 0.1268866601E+00 0.0000000000E+00\n";
}