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 crate::error::{IoError, Result};

use super::validate::invalid_dym;
use super::*;

#[test]
fn parses_type1_dym_and_builds_mass_weighted_matrix() -> Result<()> {
    let parsed = parse_dym(TYPE1_DYM)?;
    assert_eq!(parsed.dym_type, 1);
    assert_eq!(parsed.atom_count(), 2);
    assert_eq!(parsed.atomic_numbers.to_vec(), vec![29, 8]);
    let positions = parsed.coordinates.cartesian_positions();
    assert_eq!(positions[[1, 0]], 1.0);
    assert_eq!(parsed.force_constants[[0, 1, 0, 0]], -1.0);

    let matrix = parsed.mass_weighted_dynamical_matrix()?;
    assert_eq!(matrix.shape(), &[6, 6]);
    assert_eq!(matrix[[0, 0]], 2.0 / 64.0);
    assert!(matrix[[0, 1]] < 0.0);
    Ok(())
}

#[test]
fn roundtrips_type1_dym_text() -> Result<()> {
    let parsed = parse_dym(TYPE1_DYM)?;
    let rendered = dym_string(&parsed)?;
    assert!(rendered.contains("    1.00000000    0.00000000    0.00000000"));
    assert!(rendered.contains("  2.000000E+00  0.000000E+00  0.000000E+00"));
    let reparsed = parse_dym(&rendered)?;
    assert_eq!(reparsed.dym_type, parsed.dym_type);
    assert_eq!(reparsed.atomic_numbers, parsed.atomic_numbers);
    assert_eq!(reparsed.atomic_masses, parsed.atomic_masses);
    assert_eq!(reparsed.force_constants, parsed.force_constants);
    Ok(())
}

#[test]
fn renders_extended_coordinate_fields_when_needed() -> Result<()> {
    let mut parsed = parse_dym(TYPE1_DYM)?;
    let DymCoordinates::Cartesian(positions) = &mut parsed.coordinates else {
        return Err(invalid_dym("coordinates", "expected Cartesian coordinates"));
    };
    positions[[1, 0]] = 1.000000001;

    let rendered = dym_string(&parsed)?;
    assert!(rendered.contains("      1.0000000010    0.0000000000    0.0000000000"));
    assert_eq!(parse_dym(&rendered)?, parsed);
    Ok(())
}

#[test]
fn parses_type4_reduced_coordinates_and_cell() -> Result<()> {
    let parsed = parse_dym(TYPE4_DYM)?;
    assert_eq!(parsed.dym_type, 4);
    let DymCoordinates::Reduced { reduced, cell } = &parsed.coordinates else {
        return Err(invalid_dym("coordinates", "expected reduced coordinates"));
    };
    assert_eq!(reduced[[1, 0]], 0.5);
    assert_eq!(cell[[0, 0]], 4.0);
    let cartesian = parsed.coordinates.cartesian_positions();
    assert_eq!(cartesian[[1, 0]], 2.0);
    assert_eq!(cartesian[[1, 1]], 0.0);
    Ok(())
}

#[test]
fn parses_type2_unique_atom_metadata() -> Result<()> {
    let (_, type1_body) = TYPE1_DYM
        .split_once('\n')
        .ok_or_else(|| invalid_dym("type", "test fixture missing type header"))?;
    let type2_text = String::from("    2\n")
        + type1_body
        + "\
    2    2
   29    1
    1  1.000000E+00  0.000000E+00  0.000000E+00  0.000000E+00
    8    1
    2  2.000000E+00  1.000000E+00  0.000000E+00  0.000000E+00
";
    let parsed = parse_dym(&type2_text)?;
    assert_eq!(parsed.dym_type, 2);
    let metadata = parsed
        .type2_metadata
        .as_ref()
        .ok_or_else(|| invalid_dym("type 2 metadata", "missing test metadata"))?;
    assert_eq!(metadata.cell_atom_count, 2);
    assert_eq!(metadata.unique_atoms.len(), 2);
    assert_eq!(metadata.unique_atoms[0].atom_type, 29);
    assert_eq!(
        metadata.unique_atoms[0].center_atom_indices.to_vec(),
        vec![0]
    );
    assert_eq!(metadata.unique_atoms[0].weights.to_vec(), vec![1.0]);
    assert_eq!(metadata.unique_atoms[1].atom_type, 8);
    assert_eq!(
        metadata.unique_atoms[1].center_atom_indices.to_vec(),
        vec![1]
    );
    assert_eq!(metadata.unique_atoms[1].coordinates[[0, 0]], 1.0);

    let rendered = dym_string(&parsed)?;
    let reparsed = parse_dym(&rendered)?;
    assert_eq!(reparsed, parsed);
    Ok(())
}

#[test]
fn parses_type3_dipole_derivatives_for_ir_runs() -> Result<()> {
    let parsed = parse_dym(TYPE3_DYM)?;
    assert_eq!(parsed.dym_type, 3);
    let dipoles = parsed
        .dipole_derivatives
        .as_ref()
        .ok_or_else(|| invalid_dym("dipole derivatives", "missing test dipoles"))?;
    assert_eq!(dipoles.shape(), &[2, 3, 3]);
    assert_eq!(dipoles[[0, 1, 1]], 0.5);
    assert_eq!(dipoles[[1, 2, 2]], 1.8);

    let rendered = dym_string(&parsed)?;
    let reparsed = parse_dym(&rendered)?;
    assert_eq!(reparsed, parsed);
    Ok(())
}

#[test]
fn fills_missing_atomic_metadata_like_feff() -> Result<()> {
    let parsed = parse_dym(TYPE1_MISSING_ATOMIC_METADATA_DYM)?;
    assert_eq!(parsed.atomic_numbers.to_vec(), vec![29, 8]);
    assert!((parsed.atomic_masses[1] - 15.999).abs() < 1.0e-6);
    Ok(())
}

#[test]
fn rejects_bad_dym_inputs() -> Result<()> {
    assert!(matches!(
        parse_dym("1\n"),
        Err(IoError::DymMissing {
            field: "atom count"
        })
    ));
    assert!(matches!(
        parse_dym(TYPE1_BAD_PAIR_DYM),
        Err(IoError::InvalidDym {
            field: "force-constant i atom",
            ..
        })
    ));
    let mut bad_mass = parse_dym(TYPE1_DYM)?;
    bad_mass.atomic_masses[0] = 0.0;
    assert!(matches!(
        dym_string(&bad_mass),
        Err(IoError::InvalidDym {
            field: "atomic mass",
            ..
        })
    ));
    Ok(())
}

const TYPE1_DYM: &str = "\
    1
    2
   29
    8
   64.000000
   16.000000
    0.00000000    0.00000000    0.00000000
    1.00000000    0.00000000    0.00000000
    1    1
  2.000000E+00  0.000000E+00  0.000000E+00
  0.000000E+00  2.000000E+00  0.000000E+00
  0.000000E+00  0.000000E+00  2.000000E+00
    1    2
 -1.000000E+00  0.000000E+00  0.000000E+00
  0.000000E+00 -1.000000E+00  0.000000E+00
  0.000000E+00  0.000000E+00 -1.000000E+00
    2    1
 -1.000000E+00  0.000000E+00  0.000000E+00
  0.000000E+00 -1.000000E+00  0.000000E+00
  0.000000E+00  0.000000E+00 -1.000000E+00
    2    2
  2.000000E+00  0.000000E+00  0.000000E+00
  0.000000E+00  2.000000E+00  0.000000E+00
  0.000000E+00  0.000000E+00  2.000000E+00
";

const TYPE4_DYM: &str = "\
    4
    2
   29
    8
   64.000000
   16.000000
    0.00000000    0.00000000    0.00000000
    0.50000000    0.00000000    0.00000000
    1    1
  2.000000E+00  0.000000E+00  0.000000E+00
  0.000000E+00  2.000000E+00  0.000000E+00
  0.000000E+00  0.000000E+00  2.000000E+00
    1    2
 -1.000000E+00  0.000000E+00  0.000000E+00
  0.000000E+00 -1.000000E+00  0.000000E+00
  0.000000E+00  0.000000E+00 -1.000000E+00
    2    1
 -1.000000E+00  0.000000E+00  0.000000E+00
  0.000000E+00 -1.000000E+00  0.000000E+00
  0.000000E+00  0.000000E+00 -1.000000E+00
    2    2
  2.000000E+00  0.000000E+00  0.000000E+00
  0.000000E+00  2.000000E+00  0.000000E+00
  0.000000E+00  0.000000E+00  2.000000E+00

    4.00000000    0.00000000    0.00000000
    0.00000000    5.00000000    0.00000000
    0.00000000    0.00000000    6.00000000
";

const TYPE3_DYM: &str = "\
    3
    2
   29
    8
   64.000000
   16.000000
    0.00000000    0.00000000    0.00000000
    1.00000000    0.00000000    0.00000000
    1    1
  2.000000E+00  0.000000E+00  0.000000E+00
  0.000000E+00  2.000000E+00  0.000000E+00
  0.000000E+00  0.000000E+00  2.000000E+00
    1    2
 -1.000000E+00  0.000000E+00  0.000000E+00
  0.000000E+00 -1.000000E+00  0.000000E+00
  0.000000E+00  0.000000E+00 -1.000000E+00
    2    1
 -1.000000E+00  0.000000E+00  0.000000E+00
  0.000000E+00 -1.000000E+00  0.000000E+00
  0.000000E+00  0.000000E+00 -1.000000E+00
    2    2
  2.000000E+00  0.000000E+00  0.000000E+00
  0.000000E+00  2.000000E+00  0.000000E+00
  0.000000E+00  0.000000E+00  2.000000E+00

  1.000000E-01  2.000000E-01  3.000000E-01
  4.000000E-01  5.000000E-01  6.000000E-01
  7.000000E-01  8.000000E-01  9.000000E-01
  1.000000E+00  1.100000E+00  1.200000E+00
  1.300000E+00  1.400000E+00  1.500000E+00
  1.600000E+00  1.700000E+00  1.800000E+00
";

const TYPE1_BAD_PAIR_DYM: &str = "\
    1
    1
   29
   64.000000
    0.00000000    0.00000000    0.00000000
    2    1
  2.000000E+00  0.000000E+00  0.000000E+00
  0.000000E+00  2.000000E+00  0.000000E+00
  0.000000E+00  0.000000E+00  2.000000E+00
";

const TYPE1_MISSING_ATOMIC_METADATA_DYM: &str = "\
    1
    2
    0
    8
   63.546000
    0.000000
    0.00000000    0.00000000    0.00000000
    1.00000000    0.00000000    0.00000000
    1    1
  2.000000E+00  0.000000E+00  0.000000E+00
  0.000000E+00  2.000000E+00  0.000000E+00
  0.000000E+00  0.000000E+00  2.000000E+00
    1    2
 -1.000000E+00  0.000000E+00  0.000000E+00
  0.000000E+00 -1.000000E+00  0.000000E+00
  0.000000E+00  0.000000E+00 -1.000000E+00
    2    1
 -1.000000E+00  0.000000E+00  0.000000E+00
  0.000000E+00 -1.000000E+00  0.000000E+00
  0.000000E+00  0.000000E+00 -1.000000E+00
    2    2
  2.000000E+00  0.000000E+00  0.000000E+00
  0.000000E+00  2.000000E+00  0.000000E+00
  0.000000E+00  0.000000E+00  2.000000E+00
";