use crate::brute;
use crate::{
CellList, KdTree, NeighborList, NeighborPair, PeriodicBox, SpatialBackend, SpatialError,
SpatialSearchOptions,
};
use molframe_core::selection::AtomSelection;
use molframe_core::{CoordinateGeneration, ExecutionContext};
use std::borrow::Cow;
#[derive(Clone, Copy)]
enum PairOrder {
Sorted,
Unsorted,
}
#[derive(Clone, Copy)]
struct PairRequest {
options: SpatialSearchOptions,
order: PairOrder,
}
impl PairRequest {
const fn sorted(options: SpatialSearchOptions) -> Self {
Self {
options,
order: PairOrder::Sorted,
}
}
const fn unsorted(options: SpatialSearchOptions) -> Self {
Self {
options,
order: PairOrder::Unsorted,
}
}
}
#[derive(Clone, Copy)]
struct PairDispatch {
plan: crate::SpatialPlan,
options: SpatialSearchOptions,
order: PairOrder,
}
pub fn pairs_within(
positions: &[[f32; 3]],
left: &AtomSelection,
right: &AtomSelection,
cutoff: f32,
backend: SpatialBackend,
periodic: Option<&PeriodicBox>,
context: &ExecutionContext,
) -> Result<Vec<NeighborPair>, SpatialError> {
pairs_within_with_options(
positions,
left,
right,
cutoff,
SpatialSearchOptions::with_backend(backend),
periodic,
context,
)
}
pub fn pairs_within_unsorted(
positions: &[[f32; 3]],
left: &AtomSelection,
right: &AtomSelection,
cutoff: f32,
backend: SpatialBackend,
periodic: Option<&PeriodicBox>,
context: &ExecutionContext,
) -> Result<Vec<NeighborPair>, SpatialError> {
pairs_within_unsorted_with_options(
positions,
left,
right,
cutoff,
SpatialSearchOptions::with_backend(backend),
periodic,
context,
)
}
pub fn pairs_within_with_options(
positions: &[[f32; 3]],
left: &AtomSelection,
right: &AtomSelection,
cutoff: f32,
options: SpatialSearchOptions,
periodic: Option<&PeriodicBox>,
context: &ExecutionContext,
) -> Result<Vec<NeighborPair>, SpatialError> {
pairs_with_order(
positions,
left,
right,
cutoff,
periodic,
PairRequest::sorted(options),
context,
)
}
pub fn pairs_within_unsorted_with_options(
positions: &[[f32; 3]],
left: &AtomSelection,
right: &AtomSelection,
cutoff: f32,
options: SpatialSearchOptions,
periodic: Option<&PeriodicBox>,
context: &ExecutionContext,
) -> Result<Vec<NeighborPair>, SpatialError> {
pairs_with_order(
positions,
left,
right,
cutoff,
periodic,
PairRequest::unsorted(options),
context,
)
}
fn pairs_with_order(
positions: &[[f32; 3]],
left: &AtomSelection,
right: &AtomSelection,
cutoff: f32,
periodic: Option<&PeriodicBox>,
request: PairRequest,
context: &ExecutionContext,
) -> Result<Vec<NeighborPair>, SpatialError> {
validate_cutoff(cutoff)?;
let indices = super::indices::IndexWorkspace::new(left, right, positions.len(), context)?;
let left_indices = indices.left();
let right_indices = indices.right();
let plan = request
.options
.plan(left_indices.len(), right_indices.len(), cutoff)?;
dispatch_pairs(
positions,
left_indices,
right_indices,
cutoff,
PairDispatch {
plan,
options: request.options,
order: request.order,
},
periodic,
context,
)
}
fn dispatch_pairs(
positions: &[[f32; 3]],
left: &[u32],
right: &[u32],
cutoff: f32,
dispatch: PairDispatch,
periodic: Option<&PeriodicBox>,
context: &ExecutionContext,
) -> Result<Vec<NeighborPair>, SpatialError> {
let same_selection = left == right;
match dispatch.plan.backend {
SpatialBackend::CellList => {
let index = CellList::build_in(
positions,
right,
cutoff,
periodic.copied(),
dispatch.options.cell_grid,
context,
)?;
if same_selection {
let sorted = matches!(dispatch.order, PairOrder::Sorted);
if context.worker_budget() > 1 {
crate::backends::cell::pairs_same_selection_parallel(
&index, left, cutoff, context, sorted,
)
} else if sorted {
crate::backends::cell::pairs_same_selection(&index, left, cutoff)
} else {
crate::backends::cell::pairs_same_selection_unordered(&index, left, cutoff)
}
} else {
index.pairs(left, cutoff)
}
}
SpatialBackend::KdTree => KdTree::build_in(
positions,
right,
periodic.copied(),
dispatch.options.kd_periodic,
context,
)?
.pairs(left, cutoff),
SpatialBackend::NeighborList => neighbor_list_pairs(
positions,
left,
right,
cutoff,
dispatch.plan.neighbor_skin,
dispatch.options,
periodic,
),
SpatialBackend::BruteForce => {
let cutoff_squared = cutoff * cutoff;
if same_selection {
if matches!(dispatch.order, PairOrder::Sorted) {
Ok(brute::pairs_same_selection(
positions,
left,
cutoff_squared,
periodic,
))
} else {
Ok(brute::pairs_same_selection_unordered(
positions,
left,
cutoff_squared,
periodic,
))
}
} else {
Ok(brute::pairs(
positions,
left,
right,
cutoff_squared,
periodic,
))
}
}
SpatialBackend::Auto => Err(SpatialError::InvalidOption(
crate::SpatialOption::UnresolvedBackend,
)),
}
}
fn neighbor_list_pairs(
positions: &[[f32; 3]],
left: &[u32],
right: &[u32],
cutoff: f32,
skin: Option<f32>,
options: SpatialSearchOptions,
periodic: Option<&PeriodicBox>,
) -> Result<Vec<NeighborPair>, SpatialError> {
let Some(skin) = skin else {
return Err(SpatialError::InvalidOption(
crate::SpatialOption::NeighborSkinMinimum,
));
};
let list = NeighborList::build_with_options(
positions,
left,
right,
cutoff,
periodic,
CoordinateGeneration::INITIAL,
crate::NeighborListOptions {
skin,
cell_grid: options.cell_grid,
},
)?;
list.pairs(positions, cutoff, periodic)
}
pub(super) fn checked_indices(
selection: &AtomSelection,
position_count: usize,
) -> Result<Cow<'_, [u32]>, SpatialError> {
if let AtomSelection::Sparse(indices) = selection {
for &atom in indices {
if usize::try_from(atom).map_err(|_| SpatialError::NumericRangeExceeded)?
>= position_count
{
return Err(SpatialError::AtomOutOfBounds(atom));
}
}
return Ok(Cow::Borrowed(indices));
}
if selection.is_empty() {
return Ok(Cow::Borrowed(&[]));
}
let capacity =
usize::try_from(selection.len()).map_err(|_| SpatialError::NumericRangeExceeded)?;
let mut indices = Vec::with_capacity(capacity);
for atom in selection {
let position = usize::try_from(atom).map_err(|_| SpatialError::NumericRangeExceeded)?;
if position >= position_count {
return Err(SpatialError::AtomOutOfBounds(atom));
}
indices.push(atom);
}
Ok(Cow::Owned(indices))
}
pub(super) fn validate_cutoff(cutoff: f32) -> Result<(), SpatialError> {
if cutoff.is_finite() && cutoff >= 0.0 {
Ok(())
} else {
Err(SpatialError::InvalidCutoff)
}
}
#[cfg(test)]
#[path = "search_parallel_tests.rs"]
mod parallel_tests;
#[cfg(test)]
#[path = "search_tests.rs"]
mod tests;