use std::path::Path;
use ndarray::Array1;
use num_complex::Complex64;
use refeff_core::{FullSpectrumDrudeInput, FullSpectrumDrudeTerm, full_spectrum_drude_term};
use crate::error::{IoError, Result};
use crate::format::write_fortran_zero_scaled_exp;
const DRUDE_DAT_PATH: &str = "drude.dat";
const DRUDE_DAT_ROW_WIDTH: usize = 3;
#[derive(Debug, Clone, PartialEq)]
pub struct DrudeDatData {
pub gamma_ev: f64,
pub plasma_frequency_ev: f64,
pub omega: Array1<f64>,
pub epsilon: Array1<Complex64>,
}
impl DrudeDatData {
#[must_use]
pub fn point_count(&self) -> usize {
self.omega.len()
}
}
impl From<FullSpectrumDrudeTerm> for DrudeDatData {
fn from(term: FullSpectrumDrudeTerm) -> Self {
Self {
gamma_ev: term.gamma_ev,
plasma_frequency_ev: term.plasma_frequency_ev,
omega: term.omega,
epsilon: term.epsilon,
}
}
}
pub fn drude_dat_from_grid(
omega: ndarray::ArrayView1<'_, f64>,
lifetime_seconds: f64,
number_density: f64,
) -> Result<DrudeDatData> {
full_spectrum_drude_term(FullSpectrumDrudeInput {
omega,
lifetime_seconds,
number_density,
})
.map(DrudeDatData::from)
.map_err(|error| invalid_drude_dat_error("drude_term", error.to_string()))
}
pub fn drude_dat_string(data: &DrudeDatData) -> Result<String> {
validate_drude_dat(data)?;
let mut out = String::new();
out.push_str("# gam (eV):");
write_fortran_zero_scaled_exp(&mut out, data.gamma_ev, 20, 10)?;
out.push('\n');
out.push_str("# wp (eV):");
write_fortran_zero_scaled_exp(&mut out, data.plasma_frequency_ev, 20, 10)?;
out.push('\n');
for (omega, epsilon) in data.omega.iter().zip(data.epsilon.iter()) {
write_drude_row(&mut out, [*omega, epsilon.re, epsilon.im])?;
}
Ok(out)
}
pub fn parse_drude_dat(text: &str) -> Result<DrudeDatData> {
let mut gamma_ev = None;
let mut plasma_frequency_ev = None;
let mut omega = Vec::new();
let mut 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 let Some(value) = parse_header_value(line, "# gam (eV):", line_number)? {
gamma_ev = Some(value);
} else if let Some(value) = parse_header_value(line, "# wp (eV):", line_number)? {
plasma_frequency_ev = Some(value);
} else if tokens.first().is_some_and(|token| is_numeric_token(token)) {
if tokens.len() != DRUDE_DAT_ROW_WIDTH {
return parse_error(
line_number,
format!(
"drude.dat row has {} token(s), expected {DRUDE_DAT_ROW_WIDTH}",
tokens.len()
),
);
}
omega.push(parse_f64(line_number, "omega", tokens[0])?);
epsilon.push(Complex64::new(
parse_f64(line_number, "epsilon real", tokens[1])?,
parse_f64(line_number, "epsilon imaginary", tokens[2])?,
));
} else if !line.trim().is_empty() && !line.trim_start().starts_with('#') {
return parse_error(line_number, format!("unexpected drude.dat line {line:?}"));
}
}
let data = DrudeDatData {
gamma_ev: gamma_ev.ok_or_else(|| missing_drude_dat("gamma_ev"))?,
plasma_frequency_ev: plasma_frequency_ev
.ok_or_else(|| missing_drude_dat("plasma_frequency_ev"))?,
omega: Array1::from_vec(omega),
epsilon: Array1::from_vec(epsilon),
};
validate_drude_dat(&data)?;
Ok(data)
}
pub fn write_drude_dat(path: impl AsRef<Path>, data: &DrudeDatData) -> Result<()> {
let path = path.as_ref();
std::fs::write(path, drude_dat_string(data)?).map_err(|source| IoError::io(path, source))
}
pub fn read_drude_dat(path: impl AsRef<Path>) -> Result<DrudeDatData> {
let path = path.as_ref();
let text = std::fs::read_to_string(path).map_err(|source| IoError::io(path, source))?;
parse_drude_dat(&text)
}
fn write_drude_row(out: &mut String, fields: [f64; DRUDE_DAT_ROW_WIDTH]) -> Result<()> {
for value in fields {
write_fortran_zero_scaled_exp(out, value, 20, 10)?;
}
out.push('\n');
Ok(())
}
fn validate_drude_dat(data: &DrudeDatData) -> Result<()> {
validate_positive("gamma_ev", data.gamma_ev, 0)?;
validate_nonnegative("plasma_frequency_ev", data.plasma_frequency_ev, 0)?;
let point_count = data.point_count();
if point_count == 0 {
return invalid_drude_dat("rows", "at least one Drude row is required");
}
validate_len("epsilon", data.epsilon.len(), point_count)?;
for (row, omega) in data.omega.iter().enumerate() {
validate_positive("omega", *omega, row + 1)?;
}
for (row, epsilon) in data.epsilon.iter().enumerate() {
validate_finite("epsilon real", epsilon.re, row + 1)?;
validate_finite("epsilon imaginary", epsilon.im, row + 1)?;
}
Ok(())
}
fn validate_len(field: &'static str, actual: usize, expected: usize) -> Result<()> {
if actual == expected {
Ok(())
} else {
invalid_drude_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_drude_dat(field, format!("row {row} value must be finite"))
} else if value <= 0.0 {
invalid_drude_dat(field, format!("row {row} value must be positive"))
} else {
Ok(())
}
}
fn validate_nonnegative(field: &'static str, value: f64, row: usize) -> Result<()> {
if !value.is_finite() {
invalid_drude_dat(field, format!("row {row} value must be finite"))
} else if value < 0.0 {
invalid_drude_dat(field, format!("row {row} value must be nonnegative"))
} else {
Ok(())
}
}
fn validate_finite(field: &'static str, value: f64, row: usize) -> Result<()> {
if value.is_finite() {
Ok(())
} else {
invalid_drude_dat(field, format!("row {row} value must be finite"))
}
}
fn parse_header_value(line: &str, prefix: &'static str, line_number: usize) -> Result<Option<f64>> {
if !line.starts_with(prefix) {
return Ok(None);
}
let value = line[prefix.len()..].trim();
if value.is_empty() {
return parse_error(line_number, format!("missing {prefix} value"));
}
parse_f64(line_number, prefix, value).map(Some)
}
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 missing_drude_dat(field: &'static str) -> IoError {
invalid_drude_dat_error(field, "missing required header")
}
fn invalid_drude_dat<T>(field: &'static str, message: impl Into<String>) -> Result<T> {
Err(invalid_drude_dat_error(field, message))
}
fn invalid_drude_dat_error(field: &'static str, message: impl Into<String>) -> IoError {
IoError::Parse {
path: DRUDE_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: DRUDE_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 num_complex::Complex64;
use super::*;
#[test]
fn parses_feff_fullspectrum_drude_dat() -> Result<()> {
let parsed = parse_drude_dat(DRUDE_DAT)?;
assert_eq!(parsed.point_count(), 3);
assert_eq!(parsed.gamma_ev, 0.658);
assert_eq!(parsed.plasma_frequency_ev, 26.417_175_80);
assert_eq!(parsed.omega[0], 0.1);
assert_eq!(
parsed.epsilon[1],
Complex64::new(-23.222_477_02, 2.807_718_847)
);
Ok(())
}
#[test]
fn roundtrips_feff_fullspectrum_drude_dat() -> Result<()> {
let parsed = parse_drude_dat(DRUDE_DAT)?;
let rendered = drude_dat_string(&parsed)?;
assert_eq!(rendered, DRUDE_DAT);
assert_eq!(parse_drude_dat(&rendered)?, parsed);
Ok(())
}
#[test]
fn derives_drude_dat_from_grid() -> Result<()> {
let omega = array![0.1, 0.2, 0.5];
let rendered = drude_dat_string(&drude_dat_from_grid(omega.view(), 1.0e-15, 0.075)?)?;
assert_eq!(rendered, DRUDE_DAT);
Ok(())
}
#[test]
fn roundtrips_feff_zero_density_drude_term() -> Result<()> {
let omega = array![0.1, 0.2];
let data = drude_dat_from_grid(omega.view(), 1.0e-15, 0.0)?;
let rendered = drude_dat_string(&data)?;
assert_eq!(data.plasma_frequency_ev, 0.0);
assert_eq!(parse_drude_dat(&rendered)?, data);
Ok(())
}
#[test]
fn rejects_bad_drude_dat_inputs() {
assert!(parse_drude_dat("").is_err());
assert!(parse_drude_dat("# gam (eV): 1\n").is_err());
assert!(parse_drude_dat("# gam (eV): 1\n# wp (eV): 2\n").is_err());
assert!(parse_drude_dat("# gam (eV): 1\n# wp (eV): 2\n1 2\n").is_err());
assert!(parse_drude_dat("# gam (eV): 1\n# wp (eV): 2\n1 2 3 4\n").is_err());
assert!(parse_drude_dat("# gam (eV): 1\n# wp (eV): 2\n0 2 3\n").is_err());
let bad = DrudeDatData {
gamma_ev: 1.0,
plasma_frequency_ev: 2.0,
omega: array![1.0, 2.0],
epsilon: array![Complex64::new(1.0, 0.0)],
};
assert!(drude_dat_string(&bad).is_err());
}
const DRUDE_DAT: &str = "# gam (eV): 0.6580000000E+00\n# wp (eV): 0.2641717580E+02\n 0.1000000000E+00 -0.8904132874E+02 0.2153112406E+02\n 0.2000000000E+00 -0.2322247702E+02 0.2807718847E+01\n 0.5000000000E+00 -0.3761114345E+01 0.1818953529E+00\n";
}