refeff-io 0.2.0

FEFF file-format readers and writers (feff.inp, .dat/.bin handoffs, PAD encoding) for the refeff FEFF10 port
Documentation
use super::common::parse_error_value;
use super::*;
use crate::error::Result;

#[test]
fn parses_empty_dmdw_out() -> Result<()> {
    let parsed = parse_dmdw_out("")?;
    assert_eq!(parsed.header, None);
    assert!(!parsed.mass_enhancement_header);
    assert_eq!(parsed.section_count(), 0);
    assert_eq!(dmdw_out_string(&parsed)?, "");
    Ok(())
}

#[test]
fn parses_type2_mass_enhancement_header() -> Result<()> {
    let parsed = parse_dmdw_out(DMDW_OUT_SELF_ENERGY)?;
    let header = parsed
        .header
        .as_ref()
        .ok_or_else(|| parse_error_value(0, "test fixture should contain a dmdw.out header"))?;

    assert_eq!(header.lanczos_recursion_order, 6);
    assert_eq!(header.temperature, DmdwOutTemperature::Single(450.0));
    assert_eq!(header.dynamical_matrix_file, "feff.dym");
    assert!(parsed.mass_enhancement_header);
    assert_eq!(parsed.section_count(), 0);

    let rendered = dmdw_out_string(&parsed)?;
    assert_eq!(rendered, DMDW_OUT_SELF_ENERGY);
    assert_eq!(parse_dmdw_out(&rendered)?, parsed);
    Ok(())
}

#[test]
fn parses_path_dmdw_out() -> Result<()> {
    let parsed = parse_dmdw_out(DMDW_OUT)?;
    let header = parsed
        .header
        .as_ref()
        .ok_or_else(|| parse_error_value(0, "test fixture should contain a dmdw.out header"))?;
    assert_eq!(header.lanczos_recursion_order, 6);
    assert_eq!(header.temperature, DmdwOutTemperature::Single(450.0));
    assert_eq!(header.dynamical_matrix_file, "feff.dym");
    assert_eq!(parsed.section_count(), 1);

    let section = &parsed.sections[0];
    assert_eq!(section.subject, DmdwOutSubject::PathIndices(vec![1, 2]));
    assert_eq!(section.pdos_poles.len(), 6);
    assert_eq!(section.pdos_poles[0].frequency_thz, 2.860);
    assert_eq!(section.pdos_poles[0].weight, 0.039_469_598);
    assert_eq!(
        section.einstein,
        Some(DmdwOutEinstein {
            frequency_thz: 5.784,
            temperature_kelvin: 277.60,
            effective_force_constant_n_per_m: 69.6914,
        })
    );
    assert_eq!(section.moments.len(), 5);
    assert_eq!(section.moments[2].order, 0);
    assert_eq!(section.moments[2].frequency_thz, None);
    assert_eq!(section.reduced_mass_amu, Some(31.773));
    assert_eq!(section.path_length_angstrom, Some(2.5323));
    assert_eq!(section.sigma2_1e_minus_3_angstrom2, Some(11.8576));

    let rendered = dmdw_out_string(&parsed)?;
    assert_eq!(rendered, DMDW_OUT);
    assert_eq!(parse_dmdw_out(&rendered)?, parsed);
    Ok(())
}

#[test]
fn parses_multi_temperature_and_atom_variants() -> Result<()> {
    let parsed = parse_dmdw_out(DMDW_OUT_VARIANTS)?;
    assert_eq!(
        parsed.header.as_ref().map(|header| &header.temperature),
        Some(&DmdwOutTemperature::ListedBelow)
    );
    assert_eq!(parsed.section_count(), 3);

    let path_section = &parsed.sections[0];
    assert_eq!(path_section.sigma2_by_temperature.len(), 2);
    assert_eq!(
        path_section.sigma2_by_temperature[1].temperature_kelvin,
        300.0
    );
    assert_eq!(path_section.sigma2_by_temperature[1].value, 2.5);

    let atom_section = &parsed.sections[1];
    assert_eq!(
        atom_section.subject,
        DmdwOutSubject::AtomIndex {
            indices: vec![3],
            direction: Some("x".to_owned()),
        }
    );
    assert_eq!(atom_section.u2_by_temperature.len(), 2);
    assert!(atom_section.projected_dos_component_computed);

    let total_section = &parsed.sections[2];
    assert_eq!(total_section.subject, DmdwOutSubject::TotalPdos);
    assert_eq!(total_section.vibrational_free_energy_ev, Some(-0.125));

    let rendered = dmdw_out_string(&parsed)?;
    assert_eq!(rendered, DMDW_OUT_VARIANTS);
    assert_eq!(parse_dmdw_out(&rendered)?, parsed);
    Ok(())
}

#[test]
fn parses_total_vfe_section() -> Result<()> {
    let parsed = parse_dmdw_out(DMDW_OUT_TOTAL_VFE)?;

    assert_eq!(parsed.section_count(), 1);
    let section = &parsed.sections[0];
    assert_eq!(section.subject, DmdwOutSubject::TotalVfe);
    assert_eq!(section.vibrational_free_energy_by_temperature.len(), 2);
    assert_eq!(
        section.vibrational_free_energy_by_temperature[1].temperature_kelvin,
        300.0
    );
    assert_eq!(
        section.vibrational_free_energy_by_temperature[1].value,
        -0.125
    );
    assert_eq!(parse_dmdw_out(&dmdw_out_string(&parsed)?)?, parsed);
    Ok(())
}

#[test]
fn allows_total_pdos_to_collect_more_poles_than_lanczos_order() -> Result<()> {
    let parsed = parse_dmdw_out(DMDW_OUT_TOTAL_PDOS_AGGREGATE)?;

    assert_eq!(parsed.section_count(), 1);
    assert_eq!(parsed.sections[0].subject, DmdwOutSubject::TotalPdos);
    assert_eq!(parsed.sections[0].pdos_poles.len(), 3);
    assert_eq!(parse_dmdw_out(&dmdw_out_string(&parsed)?)?, parsed);
    Ok(())
}

#[test]
fn accepts_fortran_d_exponents() -> Result<()> {
    let parsed = parse_dmdw_out(DMDW_OUT.replace("450.00", "4.5D+02").as_str())?;
    assert_eq!(
        parsed.header.as_ref().map(|header| &header.temperature),
        Some(&DmdwOutTemperature::Single(450.0))
    );
    Ok(())
}

#[test]
fn rejects_bad_dmdw_out_inputs() {
    assert!(parse_dmdw_out("# Temperature: 300.00\n").is_err());
    assert!(
        parse_dmdw_out(
            "# Lanczos recursion order: 0\n# Temperature: 300.00\n# Dynamical matrix file: feff.dym\n"
        )
        .is_err()
    );
    assert!(
        parse_dmdw_out(
            "# Lanczos recursion order: 1\n# Temperature: 300.00\n# Dynamical matrix file: feff.dym\nPath Indices: 0\n"
        )
        .is_err()
    );
    assert!(
        parse_dmdw_out(
            "# Lanczos recursion order: 1\n# Temperature: 300.00\n# Dynamical matrix file: feff.dym\nPath Indices: 1\nPDOS Poles:\nFreq. (THz) Weight\nNaN 1\n"
        )
        .is_err()
    );
    assert!(
        parse_dmdw_out(
            "# Lanczos recursion order: 2\n# Temperature: 300.00\n# Dynamical matrix file: feff.dym\nPath Indices: 1\nPDOS Poles:\nFreq. (THz) Weight\n1 1\n"
        )
        .is_err()
    );
}

const DMDW_OUT: &str = concat!(
    r#"# Lanczos recursion order:    6
# Temperature:  450.00
# Dynamical matrix file: feff.dym

--------------------------------------------------------------
 Path Indices:    1   2
 PDOS Poles:
     Freq. (THz)    Weight
        2.860       0.039469598
        3.854       0.182890396
        4.940       0.220041663
        6.026       0.159715119
        6.812       0.284980130
        7.306       0.112876736

"#,
    " PDOS Einstein freq (single pole), associated temp and eff. force constant: \n",
    r#" Freq (THz)   Temp (K)   Eff. FC (N/m)
   5.784       277.60      69.6914

 pDOS n Moments, associated Einstein freqs, temps and eff. force constants:
  n     Mom (THz^n)   Freq (THz)     Temp (K)    Eff. FC (N/m)
 -2       0.03881       5.07607       243.60      53.6688
 -1       0.18959       5.27461       253.13      57.9492
  0       0.99997     ---------     --------
  1       5.63317       5.63317       270.34      66.0957
  2      33.45823       5.78431       277.59      69.6899

 Path Red. Mass (AMU):   31.773000
 Path Length (Ang), s^2 (1e-3 Ang^2):  2.5323  11.8576
--------------------------------------------------------------
"#
);

const DMDW_OUT_SELF_ENERGY: &str = concat!(
    "# Lanczos recursion order:    6\n",
    "# Temperature:  450.00\n",
    "# Dynamical matrix file: feff.dym\n",
    "Mass Enchancement Factor  \n",
);

const DMDW_OUT_VARIANTS: &str = r#"# Lanczos recursion order:    2
# Temperature: (See list Below)
# Dynamical matrix file: feff.dym

--------------------------------------------------------------
 Path Indices:    1   2
 PDOS Poles:
     Freq. (THz)    Weight
        2.000       0.500000000
        3.000       0.500000000

 Path Red. Mass (AMU):   31.773000
 Path Length (Ang):  2.5323
 Temp (K)   s^2 (1e-3 Ang^2)
  100.00       1.2500
  300.00       2.5000

--------------------------------------------------------------
 Atom Index:    3
--------- Direction x ---------------------------
 PDOS Poles:
     Freq. (THz)    Weight
        2.000       0.500000000
        3.000       0.500000000

 Temp (K)   u^2 (1e-3 Ang^2)
  100.00       0.500
  300.00       0.750
 Projected DOS component computed.

--------------------------------------------------------------
 Total PDOS results:
 PDOS Poles:
     Freq. (THz)    Weight
        2.000       0.500000000
        3.000       0.500000000

 VFE (eV):       -0.125000
--------------------------------------------------------------
"#;

const DMDW_OUT_TOTAL_VFE: &str = "\
# Lanczos recursion order:    2
# Temperature: (See list Below)
# Dynamical matrix file: feff.dym

--------------------------------------------------------------
 Total VFE for the paths requested:
 Temp (K)        VFE (eV)
  100.00     -0.050000
  300.00     -0.125000
--------------------------------------------------------------
";

const DMDW_OUT_TOTAL_PDOS_AGGREGATE: &str = "\
# Lanczos recursion order:    1
# Temperature:  300.00
# Dynamical matrix file: feff.dym

--------------------------------------------------------------
 Total PDOS results:
 PDOS Poles:
     Freq. (THz)    Weight
        1.000       0.250000000
        2.000       0.500000000
        3.000       0.250000000

 Projected DOS component computed.

--------------------------------------------------------------
";