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
}
}
#[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);
}
}