1use std::path::Path;
9
10use crate::error::{IoError, Result};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14#[non_exhaustive]
15pub enum FileFormat {
16 FeffInput,
18 AtomDat,
20 ApotBin,
22 PotBin,
24 ChemicalDat,
26 PhaseBin,
28 FeffBin,
30 FmsBin,
32 GgBin,
34 EmeshBin,
36 ChiaBin,
38 GtrBin,
40 HubbardGtrMBin,
42 HubbardGtrOffBin,
44 HubbardVBin,
46 HubbardAphaseBin,
48 HubbardTransformationBin,
50 RhorrpGgSliceBin,
52 RhorrpGgDiagBin,
54 RhorrpDensityBin,
56 SpecfunctDat,
58 FefflBin,
60 XseclBin,
62 XmuDat,
64 ChiDat,
66 XsectDat,
68 XsedgeDat,
70 PathsDat,
72 ListDat,
74 BandstructureDat,
76 LdosDat,
78 RhocDat,
80 LmdosDat,
82 RhocmDat,
84 EelsDat,
86 MdffDat,
88 RixsDat,
90 ComptonDat,
92 CrpaDat,
94 LossDat,
96 OpconsDat,
98 DmdwOut,
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104pub enum Representation {
105 Text,
107 PadText,
109 Binary,
111}
112
113#[derive(Debug, Clone, Copy, PartialEq)]
115pub struct NumericTolerance {
116 pub relative: f64,
118 pub absolute: f64,
120}
121
122#[derive(Debug, Clone, Copy, PartialEq)]
124pub struct FormatDescriptor {
125 pub format: FileFormat,
127 pub producer: &'static str,
129 pub representation: Representation,
131 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};
147const CRPA_TEXT: NumericTolerance = NumericTolerance {
150 relative: 5.0e-5,
151 absolute: 5.0e-8,
152};
153
154pub trait FeffCodec: Sized {
156 const FORMAT: FileFormat;
158
159 fn decode(path: &Path, bytes: &[u8]) -> Result<Self>;
161
162 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 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 fn encode(&self) -> Result<Vec<u8>>;
189}
190
191#[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}