Skip to main content

refeff_io/
codec.rs

1//! Shared FEFF file-format identity and codec contracts.
2//!
3//! FEFF uses the `.bin` suffix for both formatted PAD text (`pot.bin`,
4//! `phase.bin`, `feff.bin`) and byte-oriented payloads (`gg.dat`, `gg.bin`).
5//! The registry makes that distinction explicit for inspection and parity
6//! tools.
7
8use std::path::Path;
9
10use crate::error::{IoError, Result};
11
12/// Stable identifier for a FEFF-compatible file format.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14#[non_exhaustive]
15pub enum FileFormat {
16    /// Root FEFF input.
17    FeffInput,
18    /// Per-potential ATOM diagnostic table.
19    AtomDat,
20    /// Atomic-potential formatted handoff.
21    ApotBin,
22    /// Potential-state PAD text.
23    PotBin,
24    /// Chemical potential and SCF temperature summary.
25    ChemicalDat,
26    /// Phase-shift PAD text.
27    PhaseBin,
28    /// Path-amplitude PAD text.
29    FeffBin,
30    /// FMS metadata PAD text.
31    FmsBin,
32    /// Unformatted Green's-function matrix.
33    GgBin,
34    /// XSPH unformatted complex energy mesh.
35    EmeshBin,
36    /// FF2X unformatted configuration-average accumulator.
37    ChiaBin,
38    /// Per-potential Green's trace.
39    GtrBin,
40    /// Magnetic per-potential Green's trace.
41    HubbardGtrMBin,
42    /// Off-diagonal Hubbard Green's trace.
43    HubbardGtrOffBin,
44    /// Hubbard on-site potential matrix.
45    HubbardVBin,
46    /// Hubbard phase-shift matrix.
47    HubbardAphaseBin,
48    /// Hubbard basis transformation matrices.
49    HubbardTransformationBin,
50    /// RHORRP pair-block Green's-function slice.
51    RhorrpGgSliceBin,
52    /// RHORRP diagonal Green's-function matrices.
53    RhorrpGgDiagBin,
54    /// RHORRP density-grid output.
55    RhorrpDensityBin,
56    /// SFCONV spectral-function cache.
57    SpecfunctDat,
58    /// NRIXS path-decomposition PAD text.
59    FefflBin,
60    /// NRIXS transition cross-section PAD text.
61    XseclBin,
62    /// Absorption spectrum.
63    XmuDat,
64    /// EXAFS spectrum or per-path contribution.
65    ChiDat,
66    /// Cross-section table.
67    XsectDat,
68    /// TDLDA/PMBSE edge table.
69    XsedgeDat,
70    /// Scattering path list.
71    PathsDat,
72    /// GENFMT path list.
73    ListDat,
74    /// BAND result table.
75    BandstructureDat,
76    /// LDOS table family.
77    LdosDat,
78    /// Charge-density table family.
79    RhocDat,
80    /// Magnetic LDOS table family.
81    LmdosDat,
82    /// Magnetic charge-density table family.
83    RhocmDat,
84    /// EELS spectrum.
85    EelsDat,
86    /// EELS mixed dynamic form factor.
87    MdffDat,
88    /// RIXS map or HERFD spectrum.
89    RixsDat,
90    /// Compton profile.
91    ComptonDat,
92    /// Constrained-RPA summary.
93    CrpaDat,
94    /// Optical loss function.
95    LossDat,
96    /// Full-spectrum optical constants.
97    OpconsDat,
98    /// Dynamical-matrix Debye-Waller report.
99    DmdwOut,
100}
101
102/// Physical representation used by a FEFF format.
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104pub enum Representation {
105    /// Human-readable text containing ordinary numeric fields.
106    Text,
107    /// Formatted text containing FEFF PAD-encoded arrays.
108    PadText,
109    /// Compiler-independent unformatted bytes with a typed decoder.
110    Binary,
111}
112
113/// Numeric comparison envelope for a decoded format.
114#[derive(Debug, Clone, Copy, PartialEq)]
115pub struct NumericTolerance {
116    /// Relative tolerance.
117    pub relative: f64,
118    /// Absolute floor near zero.
119    pub absolute: f64,
120}
121
122/// Static metadata for a registered FEFF format.
123#[derive(Debug, Clone, Copy, PartialEq)]
124pub struct FormatDescriptor {
125    /// Stable format identifier.
126    pub format: FileFormat,
127    /// FEFF module that normally produces the file.
128    pub producer: &'static str,
129    /// On-disk representation.
130    pub representation: Representation,
131    /// Default semantic-comparison tolerance.
132    pub tolerance: NumericTolerance,
133}
134
135const STRICT_TEXT: NumericTolerance = NumericTolerance {
136    relative: 1.0e-6,
137    absolute: 1.0e-12,
138};
139const PHASE_TEXT: NumericTolerance = NumericTolerance {
140    relative: 5.0e-5,
141    absolute: 5.0e-8,
142};
143const SPECTRUM_TEXT: NumericTolerance = NumericTolerance {
144    relative: 5.0e-5,
145    absolute: 5.0e-8,
146};
147// The CRPA radial solver has a measured cross-language floor of about
148// 1.9e-5 relative even when Rust consumes FEFF's exact POT handoff.
149const CRPA_TEXT: NumericTolerance = NumericTolerance {
150    relative: 5.0e-5,
151    absolute: 5.0e-8,
152};
153
154/// Codec implemented by typed FEFF input/output models.
155pub trait FeffCodec: Sized {
156    /// Registered format represented by this type.
157    const FORMAT: FileFormat;
158
159    /// Decode a complete file payload.
160    fn decode(path: &Path, bytes: &[u8]) -> Result<Self>;
161
162    /// Read a stream and decode it with a caller-supplied source path.
163    fn read_from(path: &Path, mut source: impl std::io::Read) -> Result<Self> {
164        let mut bytes = Vec::new();
165        source
166            .read_to_end(&mut bytes)
167            .map_err(|source| IoError::Io {
168                path: path.into(),
169                source,
170            })?;
171        Self::decode(path, &bytes).map_err(|source| IoError::Codec {
172            path: path.into(),
173            source: Box::new(source),
174        })
175    }
176    /// Write a canonical payload, preserving destination context on errors.
177    fn write_to(&self, path: &Path, mut destination: impl std::io::Write) -> Result<()> {
178        let bytes = self.encode().map_err(|source| IoError::Codec {
179            path: path.into(),
180            source: Box::new(source),
181        })?;
182        destination.write_all(&bytes).map_err(|source| IoError::Io {
183            path: path.into(),
184            source,
185        })
186    }
187    /// Encode a complete canonical file payload.
188    fn encode(&self) -> Result<Vec<u8>>;
189}
190
191/// Identify a known FEFF format from its basename.
192#[must_use]
193pub fn identify_format(path: impl AsRef<Path>) -> Option<FormatDescriptor> {
194    let name = path.as_ref().file_name()?.to_str()?;
195    let descriptor = match name {
196        "feff.inp" => descriptor(FileFormat::FeffInput, "rdinp", Representation::Text),
197        "apot.bin" => descriptor(FileFormat::ApotBin, "atomic", Representation::PadText),
198        "pot.bin" => descriptor(FileFormat::PotBin, "pot", Representation::PadText),
199        "chemical.dat" => descriptor(FileFormat::ChemicalDat, "pot", Representation::Text),
200        "phase.bin" => descriptor(FileFormat::PhaseBin, "xsph", Representation::PadText),
201        "feff.bin" => descriptor(FileFormat::FeffBin, "genfmt", Representation::PadText),
202        "feffl.bin" => descriptor(FileFormat::FefflBin, "genfmt", Representation::PadText),
203        "fms.bin" | "fmsl.bin" => descriptor(FileFormat::FmsBin, "mkgtr", Representation::PadText),
204        "gg.dat" | "gg.bin" => descriptor(FileFormat::GgBin, "fms", Representation::Binary),
205        "gg_slice.bin" => descriptor(
206            FileFormat::RhorrpGgSliceBin,
207            "rhorrp",
208            Representation::Binary,
209        ),
210        "gg_diag.bin" => descriptor(
211            FileFormat::RhorrpGgDiagBin,
212            "rhorrp",
213            Representation::Binary,
214        ),
215        "emesh.bin" => descriptor(FileFormat::EmeshBin, "xsph", Representation::Binary),
216        "chia.bin" => descriptor(FileFormat::ChiaBin, "ff2x", Representation::Binary),
217        "density.bin" | "valence.bin" => descriptor(
218            FileFormat::RhorrpDensityBin,
219            "rhorrp",
220            Representation::Binary,
221        ),
222        "specfunct.dat" => descriptor(FileFormat::SpecfunctDat, "sfconv", Representation::Binary),
223        "v_hubbard.bin" => descriptor(FileFormat::HubbardVBin, "pot", Representation::Binary),
224        "aphase_hubbard.bin" => {
225            descriptor(FileFormat::HubbardAphaseBin, "xsph", Representation::Binary)
226        }
227        "transformation_hubbard.bin" => descriptor(
228            FileFormat::HubbardTransformationBin,
229            "fms",
230            Representation::Binary,
231        ),
232        "xsecl.bin" => descriptor(FileFormat::XseclBin, "xsph", Representation::PadText),
233        "xmu.dat" | "xmu1.dat" | "xmu2.dat" => {
234            descriptor(FileFormat::XmuDat, "ff2x", Representation::Text)
235        }
236        "chi.dat" => descriptor(FileFormat::ChiDat, "ff2x", Representation::Text),
237        "xsect.dat" => descriptor(FileFormat::XsectDat, "xsph", Representation::Text),
238        "xsedge.dat" => descriptor(FileFormat::XsedgeDat, "xsph", Representation::Text),
239        "paths.dat" => descriptor(FileFormat::PathsDat, "path", Representation::Text),
240        "list.dat" => descriptor(FileFormat::ListDat, "genfmt", Representation::Text),
241        "bandstructure.dat" => {
242            descriptor(FileFormat::BandstructureDat, "band", Representation::Text)
243        }
244        "eels.dat" => descriptor(FileFormat::EelsDat, "eels", Representation::Text),
245        "mdff.dat" => descriptor(FileFormat::MdffDat, "eelsmdff", Representation::Text),
246        "rixsET.dat" | "herfd.dat" | "herfd-sat.dat" => {
247            descriptor(FileFormat::RixsDat, "rixs", Representation::Text)
248        }
249        "compton.dat" => descriptor(FileFormat::ComptonDat, "compton", Representation::Text),
250        "crpa.dat" => descriptor(FileFormat::CrpaDat, "crpa", Representation::Text),
251        "loss.dat" => descriptor(FileFormat::LossDat, "opconsat", Representation::Text),
252        "opcons.dat" => descriptor(FileFormat::OpconsDat, "fullspectrum", Representation::Text),
253        "dmdw.out" => descriptor(FileFormat::DmdwOut, "dmdw", Representation::Text),
254        _ if indexed_name(name, "chip", ".dat") => {
255            descriptor(FileFormat::ChiDat, "ff2x", Representation::Text)
256        }
257        _ if indexed_name(name, "atom", ".dat") => {
258            descriptor(FileFormat::AtomDat, "atomic", Representation::Text)
259        }
260        _ if indexed_name(name, "feff", ".bin") => {
261            descriptor(FileFormat::FeffBin, "genfmt", Representation::PadText)
262        }
263        _ if indexed_name(name, "phase_", ".bin") => {
264            descriptor(FileFormat::PhaseBin, "xsph", Representation::PadText)
265        }
266        _ if indexed_name(name, "gg_", ".bin") => {
267            descriptor(FileFormat::GgBin, "fms", Representation::Binary)
268        }
269        _ if indexed_name(name, "gtr_m", ".bin") => {
270            descriptor(FileFormat::HubbardGtrMBin, "mkgtr", Representation::Binary)
271        }
272        _ if indexed_name(name, "gtr_off", ".bin") => descriptor(
273            FileFormat::HubbardGtrOffBin,
274            "mkgtr",
275            Representation::Binary,
276        ),
277        _ if indexed_name(name, "gtr", ".bin") => {
278            descriptor(FileFormat::GtrBin, "mkgtr", Representation::Binary)
279        }
280        _ if indexed_name(name, "ldos", ".dat") => {
281            descriptor(FileFormat::LdosDat, "ldos", Representation::Text)
282        }
283        _ if indexed_name(name, "rhoc", ".dat") => {
284            descriptor(FileFormat::RhocDat, "ldos", Representation::Text)
285        }
286        _ if indexed_name(name, "lmdos", ".dat") => {
287            descriptor(FileFormat::LmdosDat, "ldos", Representation::Text)
288        }
289        _ if indexed_name(name, "rhocm", ".dat") => {
290            descriptor(FileFormat::RhocmDat, "ldos", Representation::Text)
291        }
292        _ => return None,
293    };
294    Some(descriptor)
295}
296
297const fn descriptor(
298    format: FileFormat,
299    producer: &'static str,
300    representation: Representation,
301) -> FormatDescriptor {
302    FormatDescriptor {
303        format,
304        producer,
305        representation,
306        tolerance: match format {
307            FileFormat::PhaseBin | FileFormat::XsectDat => PHASE_TEXT,
308            FileFormat::XmuDat | FileFormat::ChiDat => SPECTRUM_TEXT,
309            FileFormat::CrpaDat => CRPA_TEXT,
310            _ => STRICT_TEXT,
311        },
312    }
313}
314
315fn indexed_name(name: &str, prefix: &str, suffix: &str) -> bool {
316    name.strip_prefix(prefix)
317        .and_then(|rest| rest.strip_suffix(suffix))
318        .is_some_and(|index| !index.is_empty() && index.bytes().all(|byte| byte.is_ascii_digit()))
319}
320
321fn text<'a>(path: &Path, bytes: &'a [u8]) -> Result<&'a str> {
322    std::str::from_utf8(bytes).map_err(|error| IoError::Parse {
323        path: path.to_path_buf(),
324        line: 0,
325        message: format!("file is not valid UTF-8: {error}"),
326    })
327}
328
329macro_rules! text_codec {
330    ($type:ty, $format:expr, $parse:path, $render:path) => {
331        impl FeffCodec for $type {
332            const FORMAT: FileFormat = $format;
333
334            fn decode(path: &Path, bytes: &[u8]) -> Result<Self> {
335                $parse(text(path, bytes)?)
336            }
337
338            fn encode(&self) -> Result<Vec<u8>> {
339                $render(self).map(String::into_bytes)
340            }
341        }
342    };
343}
344
345macro_rules! binary_codec {
346    ($type:ty, $format:expr, $parse:path, $render:path) => {
347        impl FeffCodec for $type {
348            const FORMAT: FileFormat = $format;
349
350            fn decode(_path: &Path, bytes: &[u8]) -> Result<Self> {
351                $parse(bytes)
352            }
353
354            fn encode(&self) -> Result<Vec<u8>> {
355                $render(self)
356            }
357        }
358    };
359}
360
361text_codec!(
362    crate::ChemicalDatData,
363    FileFormat::ChemicalDat,
364    crate::parse_chemical_dat,
365    crate::chemical_dat_string
366);
367text_codec!(
368    crate::XmuDatData,
369    FileFormat::XmuDat,
370    crate::parse_xmu_dat,
371    crate::xmu_dat_string
372);
373text_codec!(
374    crate::ChiDatData,
375    FileFormat::ChiDat,
376    crate::parse_chi_dat,
377    crate::chi_dat_string
378);
379text_codec!(
380    crate::XsectDatData,
381    FileFormat::XsectDat,
382    crate::parse_xsect_dat,
383    crate::xsect_dat_string
384);
385text_codec!(
386    crate::PotBinData,
387    FileFormat::PotBin,
388    crate::parse_pot_bin,
389    crate::pot_bin_string
390);
391text_codec!(
392    crate::PhaseBinData,
393    FileFormat::PhaseBin,
394    crate::parse_phase_bin,
395    crate::phase_bin_string
396);
397text_codec!(
398    crate::FeffBinData,
399    FileFormat::FeffBin,
400    crate::parse_feff_bin,
401    crate::feff_bin_string
402);
403text_codec!(
404    crate::FmsBinData,
405    FileFormat::FmsBin,
406    crate::parse_fms_bin,
407    crate::fms_bin_string
408);
409binary_codec!(
410    crate::EmeshBinData,
411    FileFormat::EmeshBin,
412    crate::parse_emesh_bin,
413    crate::emesh_bin_bytes
414);
415binary_codec!(
416    crate::ChiaBinData,
417    FileFormat::ChiaBin,
418    crate::parse_chia_bin,
419    crate::chia_bin_bytes
420);
421binary_codec!(
422    crate::GtrBinData,
423    FileFormat::GtrBin,
424    crate::parse_gtr_bin,
425    crate::gtr_bin_bytes
426);
427binary_codec!(
428    crate::GgDatData,
429    FileFormat::GgBin,
430    crate::parse_gg_bin_bytes,
431    crate::gg_bin_bytes
432);
433binary_codec!(
434    crate::SfconvSpecfunctData,
435    FileFormat::SpecfunctDat,
436    crate::parse_specfunct_dat,
437    crate::specfunct_dat_bytes
438);
439
440#[cfg(test)]
441mod tests {
442    use super::*;
443
444    #[test]
445    fn stream_errors_retain_the_actual_path_and_typed_cause() {
446        let path = Path::new("measurements/custom-spectrum.dat");
447        let error = crate::XmuDatData::read_from(path, std::io::Cursor::new(b"1 2\n"))
448            .expect_err("a spectrum row needs six columns");
449        let IoError::Codec {
450            path: actual,
451            source,
452        } = error
453        else {
454            panic!("missing caller's path context");
455        };
456        assert_eq!(actual, path);
457        assert!(matches!(
458            *source,
459            IoError::XmuDatRowWidth {
460                line: 1,
461                actual: 2,
462                expected: 6
463            }
464        ));
465    }
466
467    #[test]
468    fn distinguishes_formatted_and_unformatted_bin_files() {
469        assert_eq!(
470            identify_format("pot.bin").map(|format| format.representation),
471            Some(Representation::PadText)
472        );
473        assert_eq!(
474            identify_format("gg.bin").map(|format| format.representation),
475            Some(Representation::Binary)
476        );
477        assert_eq!(
478            identify_format("specfunct.dat").map(|format| (format.format, format.representation)),
479            Some((FileFormat::SpecfunctDat, Representation::Binary))
480        );
481        assert_eq!(
482            <crate::SfconvSpecfunctData as FeffCodec>::FORMAT,
483            FileFormat::SpecfunctDat
484        );
485        assert_eq!(
486            identify_format("gtr03.bin").map(|format| format.format),
487            Some(FileFormat::GtrBin)
488        );
489        assert_eq!(
490            identify_format("emesh.bin").map(|format| format.format),
491            Some(FileFormat::EmeshBin)
492        );
493        assert_eq!(
494            identify_format("gtr_m03.bin").map(|format| format.format),
495            Some(FileFormat::HubbardGtrMBin)
496        );
497        assert_eq!(
498            identify_format("gtr_off03.bin").map(|format| format.format),
499            Some(FileFormat::HubbardGtrOffBin)
500        );
501        assert_eq!(
502            identify_format("xsecl.bin").map(|format| format.representation),
503            Some(Representation::PadText)
504        );
505        assert_eq!(
506            identify_format("feff09.bin").map(|format| format.format),
507            Some(FileFormat::FeffBin)
508        );
509    }
510
511    #[test]
512    fn self_describing_binary_codecs_roundtrip() -> Result<()> {
513        let emesh = crate::EmeshBinData {
514            point_count_declared: 1,
515            horizontal_count: 1,
516            danes_extension_count: 0,
517            energy_hartree: ndarray::arr1(&[num_complex::Complex64::new(1.0, 0.5)]),
518        };
519        let encoded = <crate::EmeshBinData as FeffCodec>::encode(&emesh)?;
520        let decoded = <crate::EmeshBinData as FeffCodec>::decode(Path::new("emesh.bin"), &encoded)?;
521        assert_eq!(decoded, emesh);
522        Ok(())
523    }
524}