feffit 0.1.0

Pure-Rust EXAFS toolkit — data reduction (pre-edge/normalize/AUTOBK), Fourier transforms, FEFF path fitting (feffit), and feff.inp build/run; a port of larch.xafs
//! `groups2matrix` (the LCF/PCA regridding step) vs `larch`
//! `larch.math.lincombo_fitting.groups2matrix` with `interp_kind='cubic'`.
//!
//! Reference generated by `scripts/ref_groups2matrix.py` on three standards
//! sampled on three *different* energy grids, so the cubic interpolation of the
//! non-reference curves is genuinely exercised (a linear resample would diverge
//! by ~1e-3 here). The reference grid's own row is an untouched slice; the other
//! two rows are larch's cubic `interp` onto that grid. The inputs are read from
//! the same file, so the Rust port is fed bit-identical arrays.

use std::collections::HashMap;
use std::path::PathBuf;

use feffit::xasproc::groups2matrix;

fn load_ref() -> HashMap<String, Vec<f64>> {
    let path: PathBuf = [
        env!("CARGO_MANIFEST_DIR"),
        "tests",
        "data",
        "ref_groups2matrix.txt",
    ]
    .iter()
    .collect();
    let text = std::fs::read_to_string(&path).expect("read ref_groups2matrix.txt");
    let mut map = HashMap::new();
    for line in text.lines() {
        let line = line.trim();
        if line.is_empty() || line.starts_with('#') {
            continue;
        }
        let mut it = line.split_whitespace();
        let key = it.next().unwrap().to_owned();
        let vals: Vec<f64> = it.map(|t| t.parse::<f64>().unwrap()).collect();
        map.insert(key, vals);
    }
    map
}

fn assert_close(got: &[f64], want: &[f64], tol: f64, label: &str) {
    assert_eq!(
        got.len(),
        want.len(),
        "{label}: length {} != {}",
        got.len(),
        want.len()
    );
    for (i, (g, w)) in got.iter().zip(want).enumerate() {
        assert!(
            (g - w).abs() <= tol,
            "{label}[{i}]: got {g}, want {w}, |Δ|={:e} > {tol:e}",
            (g - w).abs()
        );
    }
}

#[test]
fn groups2matrix_matches_larch() {
    let r = load_ref();
    let xmin = r["xmin"][0];
    let xmax = r["xmax"][0];
    let curves: Vec<(&[f64], &[f64])> = vec![
        (&r["eA"], &r["nA"]),
        (&r["eB"], &r["nB"]),
        (&r["eC"], &r["nC"]),
    ];
    let (grid, rows) = groups2matrix(&curves, xmin, xmax).expect("groups2matrix");

    // The reference grid (curves[0].x sliced to range) must match exactly.
    assert_close(&grid, &r["grid"], 1e-9, "grid");
    // Reference row is an untouched slice of nA → bit-exact.
    assert_close(&rows[0], &r["row0"], 1e-12, "row0");
    // Cubic-interpolated standards: FITPACK splrep(s=0) vs scipy interp1d(cubic);
    // both are the not-a-knot cubic spline, agreeing to round-off in-range.
    assert_close(&rows[1], &r["row1"], 1e-9, "row1");
    assert_close(&rows[2], &r["row2"], 1e-9, "row2");
}