use std::collections::HashMap;
pub(crate) struct Contact {
pub i: usize,
pub j: usize,
pub delta: [f64; 3],
pub dist2: f64,
}
pub(crate) fn contacts(positions: &[[f64; 3]], cutoff: f64) -> Vec<Contact> {
if cutoff <= 0.0 || cutoff.is_nan() {
return brute_force(positions, cutoff);
}
let grid = Grid::new(positions, cutoff);
let cutoff2 = cutoff * cutoff;
let mut out = Vec::new();
for i in 0..positions.len() {
grid.for_each_candidate(positions[i], |j| {
if j > i {
out.extend(contact_within(positions, i, j, cutoff2));
}
});
}
out
}
pub(crate) fn disconnected_atoms(n_atoms: usize, contacts: &[Contact]) -> Vec<usize> {
let mut connected = vec![false; n_atoms];
for c in contacts {
connected[c.i] = true;
connected[c.j] = true;
}
(0..n_atoms).filter(|&a| !connected[a]).collect()
}
fn displacement(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[b[0] - a[0], b[1] - a[1], b[2] - a[2]]
}
fn contact_within(positions: &[[f64; 3]], i: usize, j: usize, cutoff2: f64) -> Option<Contact> {
let delta = displacement(positions[i], positions[j]);
let dist2 = delta[0] * delta[0] + delta[1] * delta[1] + delta[2] * delta[2];
(dist2 <= cutoff2).then_some(Contact { i, j, delta, dist2 })
}
struct Grid {
cells: HashMap<[i64; 3], Vec<usize>>,
origin: [f64; 3],
cutoff: f64,
}
impl Grid {
fn new(positions: &[[f64; 3]], cutoff: f64) -> Self {
let mut origin = [f64::INFINITY; 3];
for p in positions {
for axis in 0..3 {
origin[axis] = origin[axis].min(p[axis]);
}
}
let mut grid = Self {
cells: HashMap::new(),
origin,
cutoff,
};
for (atom, &p) in positions.iter().enumerate() {
grid.cells.entry(grid.cell_of(p)).or_default().push(atom);
}
grid
}
fn cell_of(&self, p: [f64; 3]) -> [i64; 3] {
std::array::from_fn(|axis| ((p[axis] - self.origin[axis]) / self.cutoff).floor() as i64)
}
fn for_each_candidate(&self, p: [f64; 3], mut visit: impl FnMut(usize)) {
let [cx, cy, cz] = self.cell_of(p);
for dx in -1..=1 {
for dy in -1..=1 {
for dz in -1..=1 {
if let Some(atoms) = self.cells.get(&[cx + dx, cy + dy, cz + dz]) {
atoms.iter().copied().for_each(&mut visit);
}
}
}
}
}
}
fn brute_force(positions: &[[f64; 3]], cutoff: f64) -> Vec<Contact> {
let cutoff2 = cutoff * cutoff;
let mut out = Vec::new();
for i in 0..positions.len() {
for j in (i + 1)..positions.len() {
out.extend(contact_within(positions, i, j, cutoff2));
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn finds_only_pairs_within_cutoff() {
let pos = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [3.0, 0.0, 0.0]];
let c = contacts(&pos, 1.5);
assert_eq!(c.len(), 1);
assert_eq!((c[0].i, c[0].j), (0, 1));
assert_eq!(c[0].dist2, 1.0);
}
#[test]
fn cutoff_is_inclusive() {
let pos = [[0.0, 0.0, 0.0], [2.0, 0.0, 0.0]];
assert_eq!(contacts(&pos, 2.0).len(), 1);
}
#[test]
fn cell_list_matches_brute_force() {
let positions: Vec<[f64; 3]> = (0..400)
.map(|i| {
let f = i as f64;
[
(f * 12.9898).sin() * 20.0,
(f * 78.233).sin() * 20.0,
(f * 37.719).sin() * 20.0,
]
})
.collect();
for &cutoff in &[1.0, 3.5, 8.0] {
let mut fast: Vec<(usize, usize)> = contacts(&positions, cutoff)
.iter()
.map(|c| (c.i, c.j))
.collect();
let mut slow: Vec<(usize, usize)> = brute_force(&positions, cutoff)
.iter()
.map(|c| (c.i, c.j))
.collect();
fast.sort_unstable();
slow.sort_unstable();
assert_eq!(fast, slow, "cutoff {cutoff}");
}
}
#[test]
fn isolated_atom_is_disconnected() {
let pos = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [50.0, 0.0, 0.0]];
let c = contacts(&pos, 1.5);
assert_eq!(disconnected_atoms(3, &c), vec![2]);
}
#[test]
fn fully_connected_has_no_disconnected_atoms() {
let pos = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
let c = contacts(&pos, 1.5);
assert!(disconnected_atoms(3, &c).is_empty());
}
}