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::expand::apply_cif_symmetry_operation;
use super::parse::tokenize_cif_line;
use super::{
    CifEquivalence, expand_cif_cluster, expand_cif_cluster_with_equivalence, expand_cif_structure,
    expand_cif_structure_with_equivalence, parse_cif,
};

#[test]
fn tokenizes_quoted_cif_values() {
    assert_eq!(
        tokenize_cif_line("_symmetry_space_group_name_H-M   'P 63/m m c' # comment"),
        ["_symmetry_space_group_name_H-M", "P 63/m m c"]
    );
}

#[test]
fn parses_cell_uncertainties_and_atom_sites() -> crate::Result<()> {
    let cif = r#"
data_demo
_cell_length_a 2.9400(0)
_cell_length_b 2.9400(0)
_cell_length_c 12.1100(0)
_cell_angle_alpha 90.0000(0)
_cell_angle_beta 90.0000(0)
_cell_angle_gamma 120.0000(0)
_symmetry_space_group_name_H-M 'P 63/m m c'
_symmetry_Int_Tables_number 194
loop_
_symmetry_equiv_pos_as_xyz
'+x,+y,+z'
'-x,-y,1/2+z'
loop_
_atom_site_type_symbol
_atom_site_label
_atom_site_fract_x
_atom_site_fract_y
_atom_site_fract_z
C C1 0.0 0.0 0.0
Cr Cr1 0.6667 0.3333 0.0833
"#;
    let parsed = parse_cif(cif)?;
    assert_eq!(parsed.data_block.as_deref(), Some("demo"));
    assert_eq!(parsed.cell.a, 2.94);
    assert_eq!(parsed.cell.gamma, 120.0);
    assert_eq!(parsed.space_group_number, Some(194));
    assert_eq!(parsed.space_group_hm.as_deref(), Some("P 63/m m c"));
    assert_eq!(parsed.symmetry_operations.len(), 2);
    assert_eq!(parsed.atom_sites.len(), 2);
    assert_eq!(parsed.atom_sites[1].symbol, "Cr");
    Ok(())
}

#[test]
fn parses_semicolon_cif_text_fields_in_scalars_and_loops() -> crate::Result<()> {
    let cif = r#"
data_text_fields
_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_
_space_group_symop_operation_xyz
;
x,y,z
;
loop_
_atom_site_label
_atom_site_fract_x
_atom_site_fract_y
_atom_site_fract_z
H1 0 0 0
"#;

    let parsed = parse_cif(cif)?;

    assert_eq!(parsed.space_group_hm.as_deref(), Some("P 1"));
    assert_eq!(parsed.symmetry_operations, ["x,y,z"]);
    assert_eq!(parsed.atom_sites.len(), 1);
    Ok(())
}

#[test]
fn parses_loop_headers_and_values_on_shared_lines() -> crate::Result<()> {
    let cif = r#"
data_inline_loop
_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_
_space_group_symop_operation_xyz 'x,y,z'
loop_
_atom_site_label _atom_site_fract_x _atom_site_fract_y _atom_site_fract_z H1 0 0 0
O1 0.5 0.5 0.5
"#;

    let parsed = parse_cif(cif)?;

    assert_eq!(parsed.symmetry_operations, ["x,y,z"]);
    assert_eq!(parsed.atom_sites.len(), 2);
    assert_eq!(parsed.atom_sites[0].label.as_deref(), Some("H1"));
    assert_eq!(parsed.atom_sites[1].symbol, "O");
    Ok(())
}

#[test]
fn parses_cif_data_past_ciftbx_page_boundaries() -> crate::Result<()> {
    let mut cif = String::from(
        r#"
data_page_boundary
_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'
_publ_section_comment
;
"#,
    );
    cif.push_str(&"x".repeat(9000));
    cif.push_str(
        r#"
;
loop_
_atom_site_label _atom_site_fract_x _atom_site_fract_y _atom_site_fract_z
"#,
    );
    for index in 0..180 {
        let x = f64::from(index) / 360.0;
        cif.push_str(&format!("H{index} {x:.6} 0 0\n"));
    }

    let parsed = parse_cif(&cif)?;

    assert!(cif.len() > 8192);
    assert_eq!(parsed.atom_sites.len(), 180);
    assert_eq!(
        parsed
            .atom_sites
            .last()
            .and_then(|site| site.label.as_deref()),
        Some("H179")
    );
    Ok(())
}

#[test]
fn parses_first_cif_data_block_like_ciftbx_blank_data_call() -> crate::Result<()> {
    let cif = r#"
data_first
_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
data_second
_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
O1 0.5 0.5 0.5
"#;

    let parsed = parse_cif(cif)?;

    assert_eq!(parsed.data_block.as_deref(), Some("first"));
    assert_eq!(parsed.cell.a, 4.0);
    assert_eq!(parsed.atom_sites.len(), 1);
    assert_eq!(parsed.atom_sites[0].label.as_deref(), Some("H1"));
    Ok(())
}

#[test]
fn applies_symmetry_operations_to_fractional_coordinates() -> crate::Result<()> {
    let position = [2.0 / 3.0, 1.0 / 3.0, 1.0 / 12.0];
    let transformed = apply_cif_symmetry_operation("-x+y,1/2+z,0.3333-y", position)?;

    assert!((transformed[0] - (2.0 / 3.0)).abs() < 1.0e-12);
    assert!((transformed[1] - (7.0 / 12.0)).abs() < 1.0e-12);
    assert!(transformed[2].abs() < 1.0e-12);
    Ok(())
}

#[test]
fn expands_hexagonal_cif_like_feff_importcif() -> crate::Result<()> {
    let cif = parse_cif(
        r#"
data_Cr2GeC
_cell_length_a 2.9400(0)
_cell_length_b 2.9400(0)
_cell_length_c 12.1100(0)
_cell_angle_alpha 90.0000(0)
_cell_angle_beta 90.0000(0)
_cell_angle_gamma 120.0000(0)
_symmetry_space_group_name_H-M 'P 63/m m c'
_symmetry_Int_Tables_number 194
loop_
_symmetry_equiv_pos_as_xyz
'+x,+y,+z'
'-y,+x-y,+z'
'-x+y,-x,+z'
'-x,-y,1/2+z'
'+y,-x+y,1/2+z'
'+x-y,+x,1/2+z'
'-y,-x,+z'
'-x+y,+y,+z'
'+x,+x-y,+z'
'+y,+x,1/2+z'
'+x-y,-y,1/2+z'
'-x,-x+y,1/2+z'
'-x,-y,-z'
'+y,-x+y,-z'
'+x-y,+x,-z'
'+x,+y,1/2-z'
'-y,+x-y,1/2-z'
'-x+y,-x,1/2-z'
'+y,+x,-z'
'+x-y,-y,-z'
'-x,-x+y,-z'
'-y,-x,1/2-z'
'-x+y,+y,1/2-z'
'+x,+x-y,1/2-z'
loop_
_atom_site_type_symbol
_atom_site_label
_atom_site_fract_x
_atom_site_fract_y
_atom_site_fract_z
C C 0.0000 0.0000 0.0000
Cr Cr 0.6667 0.3333 0.0833
Ge Ge 0.6667 0.3333 0.7500
"#,
    )?;
    let structure = expand_cif_structure(&cif, 3)?;

    assert_eq!(structure.lattice_name, "H");
    assert_eq!(structure.space_group_hm, "P63/mmc");
    assert_eq!(structure.space_group, 194);
    assert_eq!(structure.absorber, 7);
    assert_eq!(structure.potentials, [1, 1, 2, 2, 2, 2, 3, 3]);
    assert_eq!(structure.labels, ["Ge", "C", "Cr", "Ge"]);
    assert_eq!(structure.positions.len(), 8);
    assert!((structure.positions[0][0] + 0.57735).abs() < 1.0e-5);
    assert!((structure.positions[0][2] - 1.02976).abs() < 1.0e-5);

    let cluster = expand_cif_cluster(&cif, 3, 6.0)?;
    assert!(cluster.atoms.len() > structure.positions.len());
    assert_eq!(cluster.atoms[0].potential, 0);
    assert_eq!(cluster.potentials[0].label, "Ge");
    Ok(())
}

#[test]
fn cif_equivalence_two_collapses_potentials_by_atomic_number() -> crate::Result<()> {
    let cif = parse_cif(
        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 structure = expand_cif_structure_with_equivalence(&cif, 3, CifEquivalence::AtomicNumber)?;
    assert_eq!(structure.absorber_atomic_number, 8);
    assert_eq!(structure.absorber_label, "O");
    assert_eq!(structure.potentials, [1, 1, 2]);
    assert_eq!(structure.site_atomic_numbers, [1, 8]);
    assert_eq!(structure.site_labels, ["H", "O"]);
    assert_eq!(structure.site_multiplicities, [2, 1]);
    assert_eq!(structure.labels, ["O", "H", "O"]);

    let cluster = expand_cif_cluster_with_equivalence(&cif, 3, 4.0, CifEquivalence::AtomicNumber)?;
    assert_eq!(cluster.potentials.len(), 3);
    assert_eq!(cluster.potentials[0].atomic_number, 8);
    assert_eq!(cluster.potentials[1].atomic_number, 1);
    assert_eq!(cluster.potentials[1].multiplicity, 2);
    assert_eq!(cluster.potentials[2].atomic_number, 8);
    assert!(cluster.atoms.iter().any(|atom| atom.potential == 1));
    assert!(!cluster.atoms.iter().any(|atom| atom.potential == 3));
    Ok(())
}

#[test]
fn cif_equivalence_four_uses_feff_potential_limit() -> crate::Result<()> {
    let small = parse_cif(
        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 small_structure =
        expand_cif_structure_with_equivalence(&small, 3, CifEquivalence::AutomaticLimit)?;
    assert_eq!(small_structure.potentials, [1, 2, 3]);

    let mut large_text = 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;
        large_text.push_str(&format!("{symbol}{index} {x:.6} 0.0 0.0\n"));
    }
    let large = parse_cif(&large_text)?;
    let large_structure =
        expand_cif_structure_with_equivalence(&large, 2, CifEquivalence::AutomaticLimit)?;

    assert_eq!(large_structure.site_atomic_numbers.as_slice(), &[1, 8]);
    assert_eq!(large_structure.site_multiplicities.as_slice(), &[16, 16]);
    assert!(
        large_structure
            .potentials
            .iter()
            .all(|ipot| matches!(*ipot, 1 | 2))
    );
    Ok(())
}