rexafs 0.2.3

Rust-powered X-ray absorption spectroscopy analysis and EXAFS fitting
Documentation
use nalgebra::DVector;
use rexafs::prelude::*;

fn fixture_path(name: &str) -> String {
    format!("{}/tests/testfiles/{name}", env!("CARGO_MANIFEST_DIR"))
}

fn load_two_column(path: &str) -> (DVector<f64>, DVector<f64>) {
    let content = std::fs::read_to_string(path).expect("failed to read fixture");
    let mut x = Vec::new();
    let mut y = Vec::new();

    for line in content.lines() {
        let line = line.trim();
        if line.is_empty() || line.starts_with('#') {
            continue;
        }
        let mut parts = line.split_whitespace();
        let xv: f64 = parts.next().expect("missing x").parse().expect("invalid x");
        let yv: f64 = parts.next().expect("missing y").parse().expect("invalid y");
        x.push(xv);
        y.push(yv);
    }

    (DVector::from_vec(x), DVector::from_vec(y))
}

fn assert_chi_matches_larch(
    k: &DVector<f64>,
    actual: &DVector<f64>,
    expected: &DVector<f64>,
    epsilon: f64,
) {
    assert_eq!(
        actual.len(),
        expected.len(),
        "model/expected length mismatch"
    );
    assert_eq!(k.len(), expected.len(), "k/model length mismatch");
    for i in 0..k.len() {
        if k[i] < 2.0 {
            continue;
        }
        let diff = (actual[i] - expected[i]).abs();
        assert!(
            diff <= epsilon,
            "k={} index={} |actual-expected|={} > {}",
            k[i],
            i,
            diff,
            epsilon
        );
    }
}

fn larch_truth_variables() -> FitVariables {
    let mut vars = FitVariables::new();
    vars.insert("amp", FitVariable::new(0.92, false));
    vars.insert("de0", FitVariable::new(1.4, false));
    vars.insert("sig2", FitVariable::new(0.0031, false));
    vars.insert("dr", FitVariable::new(0.011, false));
    vars.insert("amp2", FitVariable::new(0.35, false));
    vars.insert("dr2", FitVariable::new(0.0025, false));
    vars
}

#[test]
fn example_path_builder_matches_xraylarch_path2chi() {
    let base_path = feffpath(&fixture_path("feffcu01.dat"), FeffFlavor::Feff85L)
        .unwrap()
        .set_s02("amp")
        .set_e0("de0")
        .set_deltar("dr");
    let path = base_path.clone().set_sigma2("sig2");

    let (k, chi_expected) = load_two_column(&fixture_path("feff_path_chi_larch_ref.txt"));
    let chi = path2chi(&path, &larch_truth_variables(), &k).unwrap();
    assert_chi_matches_larch(&k, &chi, &chi_expected, 3.0e-3);
}

#[test]
fn example_multi_path_model_matches_xraylarch_ff2chi() {
    let path1 = feffpath(&fixture_path("feffcu01.dat"), FeffFlavor::Feff85L)
        .unwrap()
        .set_s02("amp")
        .set_e0("de0")
        .set_sigma2("sig2")
        .set_deltar("dr");
    let path2 = feffpath(&fixture_path("feff0002.dat"), FeffFlavor::Feff85L)
        .unwrap()
        .set_s02("amp2")
        .set_e0("de0")
        .set_sigma2("sig2")
        .set_deltar("dr2");

    let (k, chi_expected) = load_two_column(&fixture_path("feff_ff2chi_larch_ref.txt"));
    let out = ff2chi(&[path1, path2], &larch_truth_variables(), &k).unwrap();
    assert_chi_matches_larch(&k, &out.chi, &chi_expected, 4.0e-3);
}

#[test]
fn example_single_dataset_fit_matches_xraylarch_target_curve() {
    let path = feffpath(&fixture_path("feffcu01.dat"), FeffFlavor::Feff85L)
        .unwrap()
        .set_s02("amp")
        .set_e0("de0")
        .set_sigma2("sig2")
        .set_deltar("dr");
    let (k, chi_expected) = load_two_column(&fixture_path("feff_fit_target_larch.txt"));

    let result = FeffFit::new()
        .data(&k, &chi_expected)
        .add_path(path)
        .set_inits([("amp", 0.95), ("de0", 0.0), ("sig2", 0.002), ("dr", 0.0)])
        .set_bounds("sig2", 0.0, 0.02)
        .krange(2.0, 14.0)
        .rrange(1.0, 3.0)
        .fit()
        .unwrap();

    assert_eq!(result.datasets.len(), 1);
    assert_chi_matches_larch(&k, &result.model_chi, &chi_expected, 6.0e-3);
}

#[test]
fn example_clone_template_fits_xraylarch_reference_curves() {
    let path1 = feffpath(&fixture_path("feffcu01.dat"), FeffFlavor::Feff85L)
        .unwrap()
        .set_s02("amp")
        .set_e0("de0")
        .set_sigma2("sig2")
        .set_deltar("dr");
    let path2 = feffpath(&fixture_path("feff0002.dat"), FeffFlavor::Feff85L)
        .unwrap()
        .set_s02("amp2")
        .set_e0("de0")
        .set_sigma2("sig2")
        .set_deltar("dr2");

    let base = FeffFit::new()
        .params([
            Param::new("amp", 0.95),
            Param::new("de0", 0.0),
            Param::new("sig2", 0.002).bounds(0.0, 0.02),
            Param::new("dr", 0.0),
            Param::new("amp2", 0.2),
            Param::new("dr2", 0.0),
        ])
        .krange(2.0, 14.0)
        .rrange(1.0, 3.0);

    let (k1, chi1_expected) = load_two_column(&fixture_path("feff_path_chi_larch_ref.txt"));
    let (k2, chi2_expected) = load_two_column(&fixture_path("feff_ff2chi_larch_ref.txt"));

    let r1 = base
        .clone()
        .data(&k1, &chi1_expected)
        .add_path(path1.clone())
        .fit()
        .unwrap();
    let r2 = base
        .clone()
        .data(&k2, &chi2_expected)
        .add_path(path1)
        .add_path(path2)
        .fit()
        .unwrap();

    assert_chi_matches_larch(&k1, &r1.model_chi, &chi1_expected, 7.0e-3);
    assert_chi_matches_larch(&k2, &r2.model_chi, &chi2_expected, 7.0e-3);
}

#[test]
fn example_multi_dataset_global_fit_matches_xraylarch_references() {
    let ds1_path = feffpath(&fixture_path("feffcu01.dat"), FeffFlavor::Feff85L)
        .unwrap()
        .set_s02("amp")
        .set_e0("de0")
        .set_sigma2("sig2")
        .set_deltar("dr");
    let ds2_path1 = feffpath(&fixture_path("feffcu01.dat"), FeffFlavor::Feff85L)
        .unwrap()
        .set_s02("amp")
        .set_e0("de0")
        .set_sigma2("sig2")
        .set_deltar("dr");
    let ds2_path2 = feffpath(&fixture_path("feff0002.dat"), FeffFlavor::Feff85L)
        .unwrap()
        .set_s02("amp2")
        .set_e0("de0")
        .set_sigma2("sig2")
        .set_deltar("dr2");

    let (k1, chi1_expected) = load_two_column(&fixture_path("feff_path_chi_larch_ref.txt"));
    let (k2, chi2_expected) = load_two_column(&fixture_path("feff_ff2chi_larch_ref.txt"));

    let ds1 = FeffFitDataset::new()
        .data(&k1, &chi1_expected)
        .add_path(ds1_path)
        .krange(2.0, 14.0)
        .rrange(1.0, 3.0);
    let ds2 = FeffFitDataset::new()
        .data(&k2, &chi2_expected)
        .add_path(ds2_path1)
        .add_path(ds2_path2)
        .krange(2.0, 14.0)
        .rrange(1.0, 3.0);

    let result = FeffFit::new()
        .add_dataset(ds1)
        .add_dataset(ds2)
        .set_inits([
            ("amp", 0.95),
            ("de0", 0.0),
            ("sig2", 0.002),
            ("dr", 0.0),
            ("amp2", 0.2),
            ("dr2", 0.0),
        ])
        .set_bounds("sig2", 0.0, 0.02)
        .fit()
        .unwrap();

    assert_eq!(result.datasets.len(), 2);
    assert_chi_matches_larch(&k1, &result.datasets[0].model_chi, &chi1_expected, 8.0e-3);
    assert_chi_matches_larch(&k2, &result.datasets[1].model_chi, &chi2_expected, 8.0e-3);
}