molframe-spatial 0.6.2

Deterministic spatial indices and neighbour search for molframe
Documentation
use super::*;

#[test]
fn orthorhombic_minimum_image_wraps_across_a_boundary() {
    let periodic = PeriodicBox::from_cell(UnitCell {
        lengths: [10.0; 3],
        angles: [90.0; 3],
    });
    let periodic = match periodic {
        Ok(periodic) => periodic,
        Err(error) => panic!("cell failed: {error}"),
    };
    assert!((periodic.distance_squared([0.2, 0.0, 0.0], [9.8, 0.0, 0.0]) - 0.16).abs() < 1e-5);
    assert_eq!(
        periodic
            .minimum_image([0.2, 0.0, 0.0], [9.8, 0.0, 0.0])
            .lattice_shift,
        [1, 0, 0]
    );
}

#[test]
fn shortest_interpolation_crosses_the_periodic_boundary() {
    let periodic = PeriodicBox::from_cell(UnitCell {
        lengths: [10.0; 3],
        angles: [90.0; 3],
    });
    let Ok(periodic) = periodic else {
        panic!("valid cell rejected");
    };
    let middle = periodic.interpolate([9.0, 0.0, 0.0], [1.0, 0.0, 0.0], 0.5);
    assert!(middle.is_some_and(|point| point[0].abs() < 1e-6));
}

#[test]
fn a_skewed_cell_checks_neighbouring_lattice_images() {
    let periodic = PeriodicBox::from_cell(UnitCell {
        lengths: [8.0, 9.0, 10.0],
        angles: [70.0, 80.0, 65.0],
    });
    let periodic = match periodic {
        Ok(periodic) => periodic,
        Err(error) => panic!("cell failed: {error}"),
    };
    let forward = periodic.distance_squared([0.1, 0.2, 0.3], [7.9, 0.2, 0.3]);
    let reverse = periodic.distance_squared([7.9, 0.2, 0.3], [0.1, 0.2, 0.3]);
    assert!((forward - reverse).abs() < f32::EPSILON);
    assert!(forward < 4.0);
}

#[test]
fn a_degenerate_cell_is_refused() {
    assert_eq!(
        PeriodicBox::from_cell(UnitCell {
            lengths: [10.0, 10.0, 0.0],
            angles: [90.0; 3],
        }),
        Err(SpatialError::InvalidCell)
    );
}

#[test]
fn wrapping_uses_fractional_primary_cell_for_a_skewed_box() {
    let periodic = PeriodicBox::from_cell(UnitCell {
        lengths: [8.0, 9.0, 10.0],
        angles: [70.0, 80.0, 65.0],
    })
    .unwrap_or_else(|error| panic!("cell failed: {error}"));
    let position = periodic.cartesian([1.2, -0.25, 2.75]);
    let wrapped = periodic.fractional(periodic.wrap(position));
    assert!(
        wrapped
            .iter()
            .zip([0.2, 0.75, 0.75])
            .all(|(value, expected)| (value - expected).abs() < 1.0e-6)
    );
}

fn exhaustive_min_squared(periodic: &PeriodicBox, fractional: [f64; 3], range: i32) -> f64 {
    let mut best = f64::INFINITY;
    for i in -range..=range {
        for j in -range..=range {
            for k in -range..=range {
                let image = [
                    fractional[0] - f64::from(i),
                    fractional[1] - f64::from(j),
                    fractional[2] - f64::from(k),
                ];
                let cart = multiply(periodic.basis, image);
                best = best.min(squared(cart));
            }
        }
    }
    best
}

fn cell(lengths: [f64; 3], angles: [f64; 3]) -> Option<PeriodicBox> {
    PeriodicBox::from_cell(UnitCell { lengths, angles }).ok()
}

#[test]
fn a_highly_skewed_cell_finds_the_true_minimum_image() {
    let Some(periodic) = cell([10.0, 1.0, 10.0], [90.0, 90.0, 1.0]) else {
        panic!("valid cell rejected");
    };
    let fractional = [0.4, 0.4, 0.0];
    let left = [0.0_f64; 3];
    let right = multiply(periodic.basis, fractional);
    let found = periodic.displacement_f64(left, right);
    let expected = exhaustive_min_squared(&periodic, fractional, 12);
    assert!((squared(found) - expected).abs() < 1e-9, "{found:?}");
    assert!(expected.sqrt() < 0.41);
}

#[test]
fn reported_lattice_shift_reproduces_the_displacement() {
    let Some(periodic) = cell([10.0, 1.0, 10.0], [90.0, 90.0, 1.0]) else {
        panic!("valid cell rejected");
    };
    let right = multiply(periodic.basis, [0.4, 0.4, 0.0]).map(f64_f32);
    let image = periodic.minimum_image([0.0; 3], right);
    let shift = multiply(
        periodic.basis,
        image.lattice_shift.map(|v| i64_f64(v).unwrap_or(0.0)),
    );
    for axis in 0..3 {
        let expected = f64::from(right[axis]) - shift[axis];
        assert!((expected - f64::from(image.displacement[axis])).abs() < 1e-4);
    }
}

mod property {
    use super::*;
    use proptest::prelude::*;

    proptest! {
        #[test]
        fn matches_exhaustive_search_for_random_cells(
            a in 1.0_f64..20.0, b in 1.0_f64..20.0, c in 1.0_f64..20.0,
            alpha in 1.0_f64..179.0, beta in 1.0_f64..179.0, gamma in 1.0_f64..179.0,
            f0 in -3.0_f64..3.0, f1 in -3.0_f64..3.0, f2 in -3.0_f64..3.0,
        ) {
            let Some(periodic) = cell([a, b, c], [alpha, beta, gamma]) else {
                return Ok(());
            };
            let fractional = [f0, f1, f2];
            let right = multiply(periodic.basis, fractional);
            let found = squared(periodic.displacement_f64([0.0; 3], right));
            let expected = exhaustive_min_squared(&periodic, fractional, 10);
            prop_assert!(found <= expected + 1e-9 * (1.0 + expected), "{found} vs {expected}");
        }
    }
}