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::*;

#[test]
fn generates_potentials_for_cif_without_potentials_card() -> anyhow::Result<()> {
    let temp = tempfile::tempdir()?;
    let cif_path = temp.path().join("two-site.cif");
    std::fs::write(
        &cif_path,
        r#"
data_two_site
_cell_length_a 4.0
_cell_length_b 4.0
_cell_length_c 4.0
_cell_angle_alpha 90
_cell_angle_beta 90
_cell_angle_gamma 90
_space_group_IT_number 1
_symmetry_space_group_name_H-M 'P 1'
loop_
_atom_site_label
_atom_site_fract_x
_atom_site_fract_y
_atom_site_fract_z
H 0.0 0.0 0.0
O 0.5 0.5 0.5
"#,
    )?;
    let input_path = temp.path().join("feff.inp");
    std::fs::write(
        &input_path,
        r#"
CIF two-site.cif
TARGET 2
EDGE K
XANES
END
"#,
    )?;

    let input = FeffInput::parse_file(&input_path)?;
    let doc = FeffDocument::from_input(&input)?;

    assert_eq!(doc.potentials.len(), 3);
    assert_eq!(doc.potentials[0].ipot, 0);
    assert_eq!(doc.potentials[0].z, Some(8));
    assert_eq!(doc.potentials[0].tag.as_deref(), Some("O"));
    assert_eq!(doc.potentials[0].xnatph, Some(0.01));
    assert_eq!(doc.potentials[1].ipot, 1);
    assert_eq!(doc.potentials[1].z, Some(1));
    assert_eq!(doc.potentials[1].tag.as_deref(), Some("H"));
    assert_eq!(doc.potentials[1].xnatph, Some(1.0));
    assert_eq!(doc.potentials[2].ipot, 2);
    assert_eq!(doc.potentials[2].z, Some(8));
    assert_eq!(doc.potentials[2].tag.as_deref(), Some("O"));
    assert_eq!(doc.potentials[2].xnatph, Some(1.0));
    Ok(())
}

#[test]
fn cif_equivalence_two_generates_atomic_number_potentials() -> anyhow::Result<()> {
    let temp = tempfile::tempdir()?;
    let cif_path = temp.path().join("three-site.cif");
    std::fs::write(
        &cif_path,
        r#"
data_three_site
_cell_length_a 4.0
_cell_length_b 4.0
_cell_length_c 4.0
_cell_angle_alpha 90
_cell_angle_beta 90
_cell_angle_gamma 90
_space_group_IT_number 1
_symmetry_space_group_name_H-M 'P 1'
loop_
_atom_site_label
_atom_site_fract_x
_atom_site_fract_y
_atom_site_fract_z
H1 0.0 0.0 0.0
H2 0.25 0.0 0.0
O1 0.5 0.5 0.5
"#,
    )?;
    let input_path = temp.path().join("feff.inp");
    std::fs::write(
        &input_path,
        r#"
CIF three-site.cif
TARGET 3
EQUI 2
FMS 4.0
EDGE K
XANES
END
"#,
    )?;

    let input = FeffInput::parse_file(&input_path)?;
    let doc = FeffDocument::from_input(&input)?;

    assert_eq!(doc.cif_equivalence, 2);
    assert_eq!(doc.potentials.len(), 3);
    assert_eq!(doc.potentials[0].ipot, 0);
    assert_eq!(doc.potentials[0].z, Some(8));
    assert_eq!(doc.potentials[0].tag.as_deref(), Some("O"));
    assert_eq!(doc.potentials[1].ipot, 1);
    assert_eq!(doc.potentials[1].z, Some(1));
    assert_eq!(doc.potentials[1].xnatph, Some(2.0));
    assert_eq!(doc.potentials[2].ipot, 2);
    assert_eq!(doc.potentials[2].z, Some(8));
    assert!(doc.atoms.iter().any(|atom| atom.ipot == 1));
    assert!(!doc.atoms.iter().any(|atom| atom.ipot == 3));
    assert!(doc.active_cards.iter().any(|card| card == "EQUIVALENCE"));
    Ok(())
}

#[test]
fn rejects_bare_cif_equivalence_like_feff() -> anyhow::Result<()> {
    let input = FeffInput::parse_str(
        "feff.inp",
        r#"
EQUIVALENCE
END
"#,
    )?;

    let error = FeffDocument::from_input(&input).expect_err("bare EQUIVALENCE should fail");
    assert!(
        error
            .to_string()
            .contains("EQUIVALENCE requires a selector")
    );
    Ok(())
}

#[test]
fn cif_equivalence_four_collapses_when_potential_limit_is_exceeded() -> anyhow::Result<()> {
    let temp = tempfile::tempdir()?;
    let cif_path = temp.path().join("many-site.cif");
    let mut cif = String::from(
        r#"
data_many_sites
_cell_length_a 8.0
_cell_length_b 8.0
_cell_length_c 8.0
_cell_angle_alpha 90
_cell_angle_beta 90
_cell_angle_gamma 90
_space_group_IT_number 1
_symmetry_space_group_name_H-M 'P 1'
loop_
_atom_site_label
_atom_site_fract_x
_atom_site_fract_y
_atom_site_fract_z
"#,
    );
    for index in 0..32 {
        let symbol = if index % 2 == 0 { "H" } else { "O" };
        let x = index as f64 / 64.0;
        cif.push_str(&format!("{symbol}{index} {x:.6} 0.0 0.0\n"));
    }
    std::fs::write(&cif_path, cif)?;

    let input_path = temp.path().join("feff.inp");
    std::fs::write(
        &input_path,
        r#"
CIF many-site.cif
TARGET 2
EQUIVALENCE 4
FMS 4.0
EDGE K
XANES
END
"#,
    )?;

    let input = FeffInput::parse_file(&input_path)?;
    let doc = FeffDocument::from_input(&input)?;

    assert_eq!(doc.cif_equivalence, 4);
    assert_eq!(doc.potentials.len(), 3);
    assert_eq!(doc.potentials[0].z, Some(8));
    assert_eq!(doc.potentials[1].z, Some(1));
    assert_eq!(doc.potentials[1].xnatph, Some(16.0));
    assert_eq!(doc.potentials[2].z, Some(8));
    assert_eq!(doc.potentials[2].xnatph, Some(16.0));
    assert!(doc.atoms.iter().any(|atom| atom.ipot == 1));
    assert!(!doc.atoms.iter().any(|atom| atom.ipot == 3));
    Ok(())
}

#[test]
fn generates_atoms_for_cif_without_atoms_card() -> anyhow::Result<()> {
    let temp = tempfile::tempdir()?;
    let cif_path = temp.path().join("two-site.cif");
    std::fs::write(
        &cif_path,
        r#"
data_two_site
_cell_length_a 4.0
_cell_length_b 4.0
_cell_length_c 4.0
_cell_angle_alpha 90
_cell_angle_beta 90
_cell_angle_gamma 90
_space_group_IT_number 1
_symmetry_space_group_name_H-M 'P 1'
loop_
_atom_site_label
_atom_site_fract_x
_atom_site_fract_y
_atom_site_fract_z
H 0.0 0.0 0.0
O 0.5 0.5 0.5
"#,
    )?;
    let input_path = temp.path().join("feff.inp");
    std::fs::write(
        &input_path,
        r#"
CIF two-site.cif
TARGET 2
FMS 4.0
RMULTIPLIER 2.0
EDGE K
XANES
END
"#,
    )?;

    let input = FeffInput::parse_file(&input_path)?;
    let doc = FeffDocument::from_input(&input)?;

    assert!(!doc.atoms.is_empty());
    assert_eq!(doc.atoms[0].ipot, 0);
    assert_eq!(
        (
            doc.atoms[0].x.round() as i32,
            doc.atoms[0].y.round() as i32,
            doc.atoms[0].z.round() as i32,
        ),
        (0, 0, 0)
    );
    assert!(
        doc.atoms
            .iter()
            .any(|atom| atom.ipot == 1 && (atom.x.abs() - 4.0).abs() < 1.0e-9)
    );
    Ok(())
}