use crate::spatial::neighbors::linkcell::LinkCell;
use crate::spatial::neighbors::{NbListAlgo, NeighborList, QueryMode};
use crate::spatial::region::simbox::SimBox;
use crate::types::{F, FNx3, FNx3View};
#[derive(Debug, Clone)]
pub struct NeighborQuery {
lc: LinkCell,
points: FNx3,
simbox: SimBox,
cutoff: F,
}
impl NeighborQuery {
pub fn new(simbox: &SimBox, points: FNx3View<'_>, cutoff: F) -> Self {
assert!(cutoff > 0.0, "cutoff must be positive");
assert_eq!(points.ncols(), 3, "points must have shape (N, 3)");
let mut lc = LinkCell::new().cutoff(cutoff);
lc.build_index(points, simbox);
Self {
lc,
points: points.to_owned(),
simbox: simbox.clone(),
cutoff,
}
}
pub fn free(points: FNx3View<'_>, cutoff: F) -> Self {
let bx =
SimBox::free(points, cutoff).expect("degenerate point cloud for free-boundary box");
Self::new(&bx, points, cutoff)
}
pub fn query(&self, query_points: FNx3View<'_>) -> NeighborList {
assert_eq!(
query_points.ncols(),
3,
"query_points must have shape (N, 3)"
);
let n_query = query_points.nrows();
let n_ref = self.points.nrows();
let cutoff_sq = self.cutoff * self.cutoff;
let mut nlist = NeighborList::with_mode(QueryMode::CrossQuery, n_ref, n_query);
for qi in 0..n_query {
let qp = query_points.row(qi);
self.lc
.visit_neighbors_of(qp, &self.simbox, |rj, dist_sq, diff| {
if dist_sq <= cutoff_sq {
nlist.push(qi as u32, rj, dist_sq, diff);
}
});
}
nlist
}
pub fn query_self(&self) -> NeighborList {
let n = self.points.nrows();
let mut lc = LinkCell::new().cutoff(self.cutoff);
lc.build(self.points.view(), &self.simbox);
let raw = lc.query().clone();
NeighborList {
idx_i: raw.idx_i,
idx_j: raw.idx_j,
dist_sq: raw.dist_sq,
diff_flat: raw.diff_flat,
mode: QueryMode::SelfQuery,
num_points: n,
num_query_points: n,
}
}
pub fn simbox(&self) -> &SimBox {
&self.simbox
}
pub fn points(&self) -> FNx3View<'_> {
self.points.view()
}
pub fn cutoff(&self) -> F {
self.cutoff
}
pub fn from_columns(simbox: &SimBox, xs: &[F], ys: &[F], zs: &[F], cutoff: F) -> Self {
assert!(cutoff > 0.0, "cutoff must be positive");
assert!(
xs.len() == ys.len() && ys.len() == zs.len(),
"x/y/z slices must have equal length"
);
let mut lc = LinkCell::new().cutoff(cutoff);
lc.build_index_soa(xs, ys, zs, simbox);
let n = xs.len();
let mut points = FNx3::zeros((n, 3));
for i in 0..n {
points[[i, 0]] = xs[i];
points[[i, 1]] = ys[i];
points[[i, 2]] = zs[i];
}
Self {
lc,
points,
simbox: simbox.clone(),
cutoff,
}
}
pub fn free_columns(xs: &[F], ys: &[F], zs: &[F], cutoff: F) -> Self {
let bx = SimBox::free_columns(xs, ys, zs, cutoff)
.expect("degenerate point cloud for free-boundary box");
Self::from_columns(&bx, xs, ys, zs, cutoff)
}
pub fn query_columns(&self, qx: &[F], qy: &[F], qz: &[F]) -> NeighborList {
assert!(
qx.len() == qy.len() && qy.len() == qz.len(),
"query x/y/z slices must have equal length"
);
let n_query = qx.len();
let n_ref = self.points.nrows();
let cutoff_sq = self.cutoff * self.cutoff;
let mut nlist = NeighborList::with_mode(QueryMode::CrossQuery, n_ref, n_query);
for qi in 0..n_query {
let qp = [qx[qi], qy[qi], qz[qi]];
self.lc
.visit_neighbors_of_pt(qp, &self.simbox, |rj, dist_sq, diff| {
if dist_sq <= cutoff_sq {
nlist.push(qi as u32, rj, dist_sq, diff);
}
});
}
nlist
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::spatial::region::simbox::SimBox;
use ndarray::array;
#[test]
fn self_query_matches_linkcell() {
let bx = SimBox::cube(4.0, array![0.0, 0.0, 0.0], [true, true, true]).expect("invalid box");
let pts = array![[0.1, 0.2, 0.3], [0.3, 0.2, 0.1], [3.9, 3.8, 3.7]];
let nq = NeighborQuery::new(&bx, pts.view(), 0.5);
let nlist = nq.query_self();
assert_eq!(nlist.mode(), QueryMode::SelfQuery);
assert_eq!(nlist.n_pairs(), 1);
assert_eq!(nlist.query_point_indices()[0], 0);
assert_eq!(nlist.point_indices()[0], 1);
assert_eq!(nlist.num_points(), 3);
assert_eq!(nlist.num_query_points(), 3);
}
#[test]
fn cross_query_finds_all_neighbors() {
let bx = SimBox::cube(4.0, array![0.0, 0.0, 0.0], [true, true, true]).expect("invalid box");
let ref_pts = array![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]];
let query_pts = array![[0.5, 0.0, 0.0]];
let nq = NeighborQuery::new(&bx, ref_pts.view(), 0.6);
let nlist = nq.query(query_pts.view());
assert_eq!(nlist.mode(), QueryMode::CrossQuery);
assert_eq!(nlist.num_query_points(), 1);
assert_eq!(nlist.num_points(), 3);
assert_eq!(nlist.n_pairs(), 2);
}
#[test]
fn distances_returns_sqrt() {
let bx = SimBox::cube(4.0, array![0.0, 0.0, 0.0], [true, true, true]).expect("invalid box");
let pts = array![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
let nq = NeighborQuery::new(&bx, pts.view(), 1.5);
let nlist = nq.query_self();
assert_eq!(nlist.n_pairs(), 1);
let dists = nlist.distances();
assert!((dists[0] - 1.0).abs() < 1e-6);
}
#[test]
fn vectors_alias_works() {
let bx = SimBox::cube(4.0, array![0.0, 0.0, 0.0], [true, true, true]).expect("invalid box");
let pts = array![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
let nq = NeighborQuery::new(&bx, pts.view(), 1.5);
let nlist = nq.query_self();
let vecs = nlist.vectors();
assert_eq!(vecs.nrows(), 1);
assert!((vecs[[0, 0]] - 1.0).abs() < 1e-6);
assert!(vecs[[0, 1]].abs() < 1e-6);
assert!(vecs[[0, 2]].abs() < 1e-6);
}
#[test]
fn self_query_pbc_boundary() {
let bx = SimBox::cube(2.0, array![0.0, 0.0, 0.0], [true, true, true]).expect("invalid box");
let pts = array![[0.1, 0.1, 0.1], [1.9, 1.9, 1.9]];
let nq = NeighborQuery::new(&bx, pts.view(), 0.5);
let nlist = nq.query_self();
assert_eq!(nlist.n_pairs(), 1);
}
#[test]
fn cross_query_self_overlap_produces_full_shell() {
let bx = SimBox::cube(4.0, array![0.0, 0.0, 0.0], [true, true, true]).expect("invalid box");
let pts = array![[0.0, 0.0, 0.0], [0.5, 0.0, 0.0]];
let nq = NeighborQuery::new(&bx, pts.view(), 0.6);
let nlist = nq.query(pts.view());
assert_eq!(nlist.n_pairs(), 4);
let self_nlist = nq.query_self();
assert_eq!(self_nlist.n_pairs(), 1);
}
#[test]
fn free_boundary_self_query() {
let pts = array![[0.0 as F, 0.0, 0.0], [0.5, 0.0, 0.0], [10.0, 10.0, 10.0],];
let nq = NeighborQuery::free(pts.view(), 1.0);
let nlist = nq.query_self();
assert_eq!(nlist.n_pairs(), 1);
let dists = nlist.distances();
assert!((dists[0] - 0.5).abs() < 1e-5);
}
#[test]
fn free_boundary_cross_query() {
let ref_pts = array![[0.0 as F, 0.0, 0.0], [1.0, 0.0, 0.0], [5.0, 5.0, 5.0],];
let query_pts = array![[0.3 as F, 0.0, 0.0]];
let nq = NeighborQuery::free(ref_pts.view(), 0.5);
let nlist = nq.query(query_pts.view());
assert_eq!(nlist.n_pairs(), 1);
}
#[test]
fn free_boundary_no_wrap() {
let pts = array![[0.0 as F, 0.0, 0.0], [5.0, 5.0, 5.0],];
let nq = NeighborQuery::free(pts.view(), 1.0);
let nlist = nq.query_self();
assert_eq!(nlist.n_pairs(), 0);
}
fn columns(pts: &ndarray::Array2<F>) -> (Vec<F>, Vec<F>, Vec<F>) {
let n = pts.nrows();
let mut xs = Vec::with_capacity(n);
let mut ys = Vec::with_capacity(n);
let mut zs = Vec::with_capacity(n);
for i in 0..n {
xs.push(pts[[i, 0]]);
ys.push(pts[[i, 1]]);
zs.push(pts[[i, 2]]);
}
(xs, ys, zs)
}
fn assert_bitwise_equal(a: &NeighborList, b: &NeighborList) {
assert_eq!(a.n_pairs(), b.n_pairs(), "n_pairs differ");
let da = a.vectors();
let db = b.vectors();
for k in 0..a.n_pairs() {
assert_eq!(
a.query_point_indices()[k],
b.query_point_indices()[k],
"idx_i"
);
assert_eq!(a.point_indices()[k], b.point_indices()[k], "idx_j");
assert_eq!(a.dist_sq()[k], b.dist_sq()[k], "dist_sq bitwise");
for d in 0..3 {
assert_eq!(da[[k, d]], db[[k, d]], "diff[{}] bitwise", d);
}
}
}
#[test]
fn columns_query_matches_query_bitwise() {
let bx = SimBox::cube(10.0, array![0.0, 0.0, 0.0], [true, true, true]).unwrap();
let refp = array![
[1.0, 1.0, 1.0],
[1.5, 1.0, 1.0],
[9.5, 1.0, 1.0],
[5.0, 5.0, 5.0],
[5.3, 5.0, 5.0],
[2.2, 8.1, 3.3],
];
let qp = array![[1.2, 1.0, 1.0], [5.1, 5.0, 5.0], [9.9, 1.1, 1.0],];
let (rx, ry, rz) = columns(&refp);
let (qx, qy, qz) = columns(&qp);
let nq_a = NeighborQuery::new(&bx, refp.view(), 2.0);
let nl_a = nq_a.query(qp.view());
let nq_s = NeighborQuery::from_columns(&bx, &rx, &ry, &rz, 2.0);
let nl_s = nq_s.query_columns(&qx, &qy, &qz);
assert!(nl_a.n_pairs() > 0, "fixture should produce pairs");
assert_bitwise_equal(&nl_a, &nl_s);
let nq_af = NeighborQuery::free(refp.view(), 2.0);
let nl_af = nq_af.query(qp.view());
let nq_sf = NeighborQuery::free_columns(&rx, &ry, &rz, 2.0);
let nl_sf = nq_sf.query_columns(&qx, &qy, &qz);
assert!(nl_af.n_pairs() > 0, "fixture should produce pairs");
assert_bitwise_equal(&nl_af, &nl_sf);
}
}