use std::path::Path;
use crate::error::{IoError, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum FileFormat {
FeffInput,
AtomDat,
ApotBin,
PotBin,
ChemicalDat,
PhaseBin,
FeffBin,
FmsBin,
GgBin,
EmeshBin,
ChiaBin,
GtrBin,
HubbardGtrMBin,
HubbardGtrOffBin,
HubbardVBin,
HubbardAphaseBin,
HubbardTransformationBin,
RhorrpGgSliceBin,
RhorrpGgDiagBin,
RhorrpDensityBin,
SpecfunctDat,
FefflBin,
XseclBin,
XmuDat,
ChiDat,
XsectDat,
XsedgeDat,
PathsDat,
ListDat,
BandstructureDat,
LdosDat,
RhocDat,
LmdosDat,
RhocmDat,
EelsDat,
MdffDat,
RixsDat,
ComptonDat,
CrpaDat,
LossDat,
OpconsDat,
DmdwOut,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Representation {
Text,
PadText,
Binary,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct NumericTolerance {
pub relative: f64,
pub absolute: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct FormatDescriptor {
pub format: FileFormat,
pub producer: &'static str,
pub representation: Representation,
pub tolerance: NumericTolerance,
}
const STRICT_TEXT: NumericTolerance = NumericTolerance {
relative: 1.0e-6,
absolute: 1.0e-12,
};
const PHASE_TEXT: NumericTolerance = NumericTolerance {
relative: 5.0e-5,
absolute: 5.0e-8,
};
const SPECTRUM_TEXT: NumericTolerance = NumericTolerance {
relative: 5.0e-5,
absolute: 5.0e-8,
};
const CRPA_TEXT: NumericTolerance = NumericTolerance {
relative: 5.0e-5,
absolute: 5.0e-8,
};
pub trait FeffCodec: Sized {
const FORMAT: FileFormat;
fn decode(path: &Path, bytes: &[u8]) -> Result<Self>;
fn read_from(path: &Path, mut source: impl std::io::Read) -> Result<Self> {
let mut bytes = Vec::new();
source
.read_to_end(&mut bytes)
.map_err(|source| IoError::Io {
path: path.into(),
source,
})?;
Self::decode(path, &bytes).map_err(|source| IoError::Codec {
path: path.into(),
source: Box::new(source),
})
}
fn write_to(&self, path: &Path, mut destination: impl std::io::Write) -> Result<()> {
let bytes = self.encode().map_err(|source| IoError::Codec {
path: path.into(),
source: Box::new(source),
})?;
destination.write_all(&bytes).map_err(|source| IoError::Io {
path: path.into(),
source,
})
}
fn encode(&self) -> Result<Vec<u8>>;
}
#[must_use]
pub fn identify_format(path: impl AsRef<Path>) -> Option<FormatDescriptor> {
let name = path.as_ref().file_name()?.to_str()?;
let descriptor = match name {
"feff.inp" => descriptor(FileFormat::FeffInput, "rdinp", Representation::Text),
"apot.bin" => descriptor(FileFormat::ApotBin, "atomic", Representation::PadText),
"pot.bin" => descriptor(FileFormat::PotBin, "pot", Representation::PadText),
"chemical.dat" => descriptor(FileFormat::ChemicalDat, "pot", Representation::Text),
"phase.bin" => descriptor(FileFormat::PhaseBin, "xsph", Representation::PadText),
"feff.bin" => descriptor(FileFormat::FeffBin, "genfmt", Representation::PadText),
"feffl.bin" => descriptor(FileFormat::FefflBin, "genfmt", Representation::PadText),
"fms.bin" | "fmsl.bin" => descriptor(FileFormat::FmsBin, "mkgtr", Representation::PadText),
"gg.dat" | "gg.bin" => descriptor(FileFormat::GgBin, "fms", Representation::Binary),
"gg_slice.bin" => descriptor(
FileFormat::RhorrpGgSliceBin,
"rhorrp",
Representation::Binary,
),
"gg_diag.bin" => descriptor(
FileFormat::RhorrpGgDiagBin,
"rhorrp",
Representation::Binary,
),
"emesh.bin" => descriptor(FileFormat::EmeshBin, "xsph", Representation::Binary),
"chia.bin" => descriptor(FileFormat::ChiaBin, "ff2x", Representation::Binary),
"density.bin" | "valence.bin" => descriptor(
FileFormat::RhorrpDensityBin,
"rhorrp",
Representation::Binary,
),
"specfunct.dat" => descriptor(FileFormat::SpecfunctDat, "sfconv", Representation::Binary),
"v_hubbard.bin" => descriptor(FileFormat::HubbardVBin, "pot", Representation::Binary),
"aphase_hubbard.bin" => {
descriptor(FileFormat::HubbardAphaseBin, "xsph", Representation::Binary)
}
"transformation_hubbard.bin" => descriptor(
FileFormat::HubbardTransformationBin,
"fms",
Representation::Binary,
),
"xsecl.bin" => descriptor(FileFormat::XseclBin, "xsph", Representation::PadText),
"xmu.dat" | "xmu1.dat" | "xmu2.dat" => {
descriptor(FileFormat::XmuDat, "ff2x", Representation::Text)
}
"chi.dat" => descriptor(FileFormat::ChiDat, "ff2x", Representation::Text),
"xsect.dat" => descriptor(FileFormat::XsectDat, "xsph", Representation::Text),
"xsedge.dat" => descriptor(FileFormat::XsedgeDat, "xsph", Representation::Text),
"paths.dat" => descriptor(FileFormat::PathsDat, "path", Representation::Text),
"list.dat" => descriptor(FileFormat::ListDat, "genfmt", Representation::Text),
"bandstructure.dat" => {
descriptor(FileFormat::BandstructureDat, "band", Representation::Text)
}
"eels.dat" => descriptor(FileFormat::EelsDat, "eels", Representation::Text),
"mdff.dat" => descriptor(FileFormat::MdffDat, "eelsmdff", Representation::Text),
"rixsET.dat" | "herfd.dat" | "herfd-sat.dat" => {
descriptor(FileFormat::RixsDat, "rixs", Representation::Text)
}
"compton.dat" => descriptor(FileFormat::ComptonDat, "compton", Representation::Text),
"crpa.dat" => descriptor(FileFormat::CrpaDat, "crpa", Representation::Text),
"loss.dat" => descriptor(FileFormat::LossDat, "opconsat", Representation::Text),
"opcons.dat" => descriptor(FileFormat::OpconsDat, "fullspectrum", Representation::Text),
"dmdw.out" => descriptor(FileFormat::DmdwOut, "dmdw", Representation::Text),
_ if indexed_name(name, "chip", ".dat") => {
descriptor(FileFormat::ChiDat, "ff2x", Representation::Text)
}
_ if indexed_name(name, "atom", ".dat") => {
descriptor(FileFormat::AtomDat, "atomic", Representation::Text)
}
_ if indexed_name(name, "feff", ".bin") => {
descriptor(FileFormat::FeffBin, "genfmt", Representation::PadText)
}
_ if indexed_name(name, "phase_", ".bin") => {
descriptor(FileFormat::PhaseBin, "xsph", Representation::PadText)
}
_ if indexed_name(name, "gg_", ".bin") => {
descriptor(FileFormat::GgBin, "fms", Representation::Binary)
}
_ if indexed_name(name, "gtr_m", ".bin") => {
descriptor(FileFormat::HubbardGtrMBin, "mkgtr", Representation::Binary)
}
_ if indexed_name(name, "gtr_off", ".bin") => descriptor(
FileFormat::HubbardGtrOffBin,
"mkgtr",
Representation::Binary,
),
_ if indexed_name(name, "gtr", ".bin") => {
descriptor(FileFormat::GtrBin, "mkgtr", Representation::Binary)
}
_ if indexed_name(name, "ldos", ".dat") => {
descriptor(FileFormat::LdosDat, "ldos", Representation::Text)
}
_ if indexed_name(name, "rhoc", ".dat") => {
descriptor(FileFormat::RhocDat, "ldos", Representation::Text)
}
_ if indexed_name(name, "lmdos", ".dat") => {
descriptor(FileFormat::LmdosDat, "ldos", Representation::Text)
}
_ if indexed_name(name, "rhocm", ".dat") => {
descriptor(FileFormat::RhocmDat, "ldos", Representation::Text)
}
_ => return None,
};
Some(descriptor)
}
const fn descriptor(
format: FileFormat,
producer: &'static str,
representation: Representation,
) -> FormatDescriptor {
FormatDescriptor {
format,
producer,
representation,
tolerance: match format {
FileFormat::PhaseBin | FileFormat::XsectDat => PHASE_TEXT,
FileFormat::XmuDat | FileFormat::ChiDat => SPECTRUM_TEXT,
FileFormat::CrpaDat => CRPA_TEXT,
_ => STRICT_TEXT,
},
}
}
fn indexed_name(name: &str, prefix: &str, suffix: &str) -> bool {
name.strip_prefix(prefix)
.and_then(|rest| rest.strip_suffix(suffix))
.is_some_and(|index| !index.is_empty() && index.bytes().all(|byte| byte.is_ascii_digit()))
}
fn text<'a>(path: &Path, bytes: &'a [u8]) -> Result<&'a str> {
std::str::from_utf8(bytes).map_err(|error| IoError::Parse {
path: path.to_path_buf(),
line: 0,
message: format!("file is not valid UTF-8: {error}"),
})
}
macro_rules! text_codec {
($type:ty, $format:expr, $parse:path, $render:path) => {
impl FeffCodec for $type {
const FORMAT: FileFormat = $format;
fn decode(path: &Path, bytes: &[u8]) -> Result<Self> {
$parse(text(path, bytes)?)
}
fn encode(&self) -> Result<Vec<u8>> {
$render(self).map(String::into_bytes)
}
}
};
}
macro_rules! binary_codec {
($type:ty, $format:expr, $parse:path, $render:path) => {
impl FeffCodec for $type {
const FORMAT: FileFormat = $format;
fn decode(_path: &Path, bytes: &[u8]) -> Result<Self> {
$parse(bytes)
}
fn encode(&self) -> Result<Vec<u8>> {
$render(self)
}
}
};
}
text_codec!(
crate::ChemicalDatData,
FileFormat::ChemicalDat,
crate::parse_chemical_dat,
crate::chemical_dat_string
);
text_codec!(
crate::XmuDatData,
FileFormat::XmuDat,
crate::parse_xmu_dat,
crate::xmu_dat_string
);
text_codec!(
crate::ChiDatData,
FileFormat::ChiDat,
crate::parse_chi_dat,
crate::chi_dat_string
);
text_codec!(
crate::XsectDatData,
FileFormat::XsectDat,
crate::parse_xsect_dat,
crate::xsect_dat_string
);
text_codec!(
crate::PotBinData,
FileFormat::PotBin,
crate::parse_pot_bin,
crate::pot_bin_string
);
text_codec!(
crate::PhaseBinData,
FileFormat::PhaseBin,
crate::parse_phase_bin,
crate::phase_bin_string
);
text_codec!(
crate::FeffBinData,
FileFormat::FeffBin,
crate::parse_feff_bin,
crate::feff_bin_string
);
text_codec!(
crate::FmsBinData,
FileFormat::FmsBin,
crate::parse_fms_bin,
crate::fms_bin_string
);
binary_codec!(
crate::EmeshBinData,
FileFormat::EmeshBin,
crate::parse_emesh_bin,
crate::emesh_bin_bytes
);
binary_codec!(
crate::ChiaBinData,
FileFormat::ChiaBin,
crate::parse_chia_bin,
crate::chia_bin_bytes
);
binary_codec!(
crate::GtrBinData,
FileFormat::GtrBin,
crate::parse_gtr_bin,
crate::gtr_bin_bytes
);
binary_codec!(
crate::GgDatData,
FileFormat::GgBin,
crate::parse_gg_bin_bytes,
crate::gg_bin_bytes
);
binary_codec!(
crate::SfconvSpecfunctData,
FileFormat::SpecfunctDat,
crate::parse_specfunct_dat,
crate::specfunct_dat_bytes
);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn stream_errors_retain_the_actual_path_and_typed_cause() {
let path = Path::new("measurements/custom-spectrum.dat");
let error = crate::XmuDatData::read_from(path, std::io::Cursor::new(b"1 2\n"))
.expect_err("a spectrum row needs six columns");
let IoError::Codec {
path: actual,
source,
} = error
else {
panic!("missing caller's path context");
};
assert_eq!(actual, path);
assert!(matches!(
*source,
IoError::XmuDatRowWidth {
line: 1,
actual: 2,
expected: 6
}
));
}
#[test]
fn distinguishes_formatted_and_unformatted_bin_files() {
assert_eq!(
identify_format("pot.bin").map(|format| format.representation),
Some(Representation::PadText)
);
assert_eq!(
identify_format("gg.bin").map(|format| format.representation),
Some(Representation::Binary)
);
assert_eq!(
identify_format("specfunct.dat").map(|format| (format.format, format.representation)),
Some((FileFormat::SpecfunctDat, Representation::Binary))
);
assert_eq!(
<crate::SfconvSpecfunctData as FeffCodec>::FORMAT,
FileFormat::SpecfunctDat
);
assert_eq!(
identify_format("gtr03.bin").map(|format| format.format),
Some(FileFormat::GtrBin)
);
assert_eq!(
identify_format("emesh.bin").map(|format| format.format),
Some(FileFormat::EmeshBin)
);
assert_eq!(
identify_format("gtr_m03.bin").map(|format| format.format),
Some(FileFormat::HubbardGtrMBin)
);
assert_eq!(
identify_format("gtr_off03.bin").map(|format| format.format),
Some(FileFormat::HubbardGtrOffBin)
);
assert_eq!(
identify_format("xsecl.bin").map(|format| format.representation),
Some(Representation::PadText)
);
assert_eq!(
identify_format("feff09.bin").map(|format| format.format),
Some(FileFormat::FeffBin)
);
}
#[test]
fn self_describing_binary_codecs_roundtrip() -> Result<()> {
let emesh = crate::EmeshBinData {
point_count_declared: 1,
horizontal_count: 1,
danes_extension_count: 0,
energy_hartree: ndarray::arr1(&[num_complex::Complex64::new(1.0, 0.5)]),
};
let encoded = <crate::EmeshBinData as FeffCodec>::encode(&emesh)?;
let decoded = <crate::EmeshBinData as FeffCodec>::decode(Path::new("emesh.bin"), &encoded)?;
assert_eq!(decoded, emesh);
Ok(())
}
}