1use crate::brute;
4use crate::{
5 CellList, KdTree, NeighborList, NeighborPair, PeriodicBox, SpatialBackend, SpatialError,
6 SpatialSearchOptions,
7};
8use molframe_core::selection::AtomSelection;
9use molframe_core::{CoordinateGeneration, ExecutionContext};
10use std::borrow::Cow;
11
12#[derive(Clone, Copy)]
13enum PairOrder {
14 Sorted,
15 Unsorted,
16}
17
18#[derive(Clone, Copy)]
20struct PairRequest {
21 options: SpatialSearchOptions,
22 order: PairOrder,
23}
24
25impl PairRequest {
26 const fn sorted(options: SpatialSearchOptions) -> Self {
27 Self {
28 options,
29 order: PairOrder::Sorted,
30 }
31 }
32 const fn unsorted(options: SpatialSearchOptions) -> Self {
33 Self {
34 options,
35 order: PairOrder::Unsorted,
36 }
37 }
38}
39
40#[derive(Clone, Copy)]
41struct PairDispatch {
42 plan: crate::SpatialPlan,
43 options: SpatialSearchOptions,
44 order: PairOrder,
45}
46
47pub fn pairs_within(
56 positions: &[[f32; 3]],
57 left: &AtomSelection,
58 right: &AtomSelection,
59 cutoff: f32,
60 backend: SpatialBackend,
61 periodic: Option<&PeriodicBox>,
62 context: &ExecutionContext,
63) -> Result<Vec<NeighborPair>, SpatialError> {
64 pairs_within_with_options(
65 positions,
66 left,
67 right,
68 cutoff,
69 SpatialSearchOptions::with_backend(backend),
70 periodic,
71 context,
72 )
73}
74
75pub fn pairs_within_unsorted(
86 positions: &[[f32; 3]],
87 left: &AtomSelection,
88 right: &AtomSelection,
89 cutoff: f32,
90 backend: SpatialBackend,
91 periodic: Option<&PeriodicBox>,
92 context: &ExecutionContext,
93) -> Result<Vec<NeighborPair>, SpatialError> {
94 pairs_within_unsorted_with_options(
95 positions,
96 left,
97 right,
98 cutoff,
99 SpatialSearchOptions::with_backend(backend),
100 periodic,
101 context,
102 )
103}
104
105pub fn pairs_within_with_options(
111 positions: &[[f32; 3]],
112 left: &AtomSelection,
113 right: &AtomSelection,
114 cutoff: f32,
115 options: SpatialSearchOptions,
116 periodic: Option<&PeriodicBox>,
117 context: &ExecutionContext,
118) -> Result<Vec<NeighborPair>, SpatialError> {
119 pairs_with_order(
120 positions,
121 left,
122 right,
123 cutoff,
124 periodic,
125 PairRequest::sorted(options),
126 context,
127 )
128}
129
130pub fn pairs_within_unsorted_with_options(
136 positions: &[[f32; 3]],
137 left: &AtomSelection,
138 right: &AtomSelection,
139 cutoff: f32,
140 options: SpatialSearchOptions,
141 periodic: Option<&PeriodicBox>,
142 context: &ExecutionContext,
143) -> Result<Vec<NeighborPair>, SpatialError> {
144 pairs_with_order(
145 positions,
146 left,
147 right,
148 cutoff,
149 periodic,
150 PairRequest::unsorted(options),
151 context,
152 )
153}
154
155fn pairs_with_order(
156 positions: &[[f32; 3]],
157 left: &AtomSelection,
158 right: &AtomSelection,
159 cutoff: f32,
160 periodic: Option<&PeriodicBox>,
161 request: PairRequest,
162 context: &ExecutionContext,
163) -> Result<Vec<NeighborPair>, SpatialError> {
164 validate_cutoff(cutoff)?;
165
166 let indices = super::indices::IndexWorkspace::new(left, right, positions.len(), context)?;
167 let left_indices = indices.left();
168 let right_indices = indices.right();
169 let plan = request
170 .options
171 .plan(left_indices.len(), right_indices.len(), cutoff)?;
172
173 dispatch_pairs(
174 positions,
175 left_indices,
176 right_indices,
177 cutoff,
178 PairDispatch {
179 plan,
180 options: request.options,
181 order: request.order,
182 },
183 periodic,
184 context,
185 )
186}
187
188fn dispatch_pairs(
192 positions: &[[f32; 3]],
193 left: &[u32],
194 right: &[u32],
195 cutoff: f32,
196 dispatch: PairDispatch,
197 periodic: Option<&PeriodicBox>,
198 context: &ExecutionContext,
199) -> Result<Vec<NeighborPair>, SpatialError> {
200 let same_selection = left == right;
201
202 match dispatch.plan.backend {
203 SpatialBackend::CellList => {
204 let index = CellList::build_in(
205 positions,
206 right,
207 cutoff,
208 periodic.copied(),
209 dispatch.options.cell_grid,
210 context,
211 )?;
212 if same_selection {
213 let sorted = matches!(dispatch.order, PairOrder::Sorted);
214 if context.worker_budget() > 1 {
215 crate::backends::cell::pairs_same_selection_parallel(
216 &index, left, cutoff, context, sorted,
217 )
218 } else if sorted {
219 crate::backends::cell::pairs_same_selection(&index, left, cutoff)
220 } else {
221 crate::backends::cell::pairs_same_selection_unordered(&index, left, cutoff)
222 }
223 } else {
224 index.pairs(left, cutoff)
225 }
226 }
227 SpatialBackend::KdTree => KdTree::build_in(
228 positions,
229 right,
230 periodic.copied(),
231 dispatch.options.kd_periodic,
232 context,
233 )?
234 .pairs(left, cutoff),
235 SpatialBackend::NeighborList => neighbor_list_pairs(
236 positions,
237 left,
238 right,
239 cutoff,
240 dispatch.plan.neighbor_skin,
241 dispatch.options,
242 periodic,
243 ),
244 SpatialBackend::BruteForce => {
245 let cutoff_squared = cutoff * cutoff;
246 if same_selection {
247 if matches!(dispatch.order, PairOrder::Sorted) {
248 Ok(brute::pairs_same_selection(
249 positions,
250 left,
251 cutoff_squared,
252 periodic,
253 ))
254 } else {
255 Ok(brute::pairs_same_selection_unordered(
256 positions,
257 left,
258 cutoff_squared,
259 periodic,
260 ))
261 }
262 } else {
263 Ok(brute::pairs(
264 positions,
265 left,
266 right,
267 cutoff_squared,
268 periodic,
269 ))
270 }
271 }
272 SpatialBackend::Auto => Err(SpatialError::InvalidOption(
273 crate::SpatialOption::UnresolvedBackend,
274 )),
275 }
276}
277
278fn neighbor_list_pairs(
283 positions: &[[f32; 3]],
284 left: &[u32],
285 right: &[u32],
286 cutoff: f32,
287 skin: Option<f32>,
288 options: SpatialSearchOptions,
289 periodic: Option<&PeriodicBox>,
290) -> Result<Vec<NeighborPair>, SpatialError> {
291 let Some(skin) = skin else {
292 return Err(SpatialError::InvalidOption(
293 crate::SpatialOption::NeighborSkinMinimum,
294 ));
295 };
296 let list = NeighborList::build_with_options(
297 positions,
298 left,
299 right,
300 cutoff,
301 periodic,
302 CoordinateGeneration::INITIAL,
303 crate::NeighborListOptions {
304 skin,
305 cell_grid: options.cell_grid,
306 },
307 )?;
308
309 list.pairs(positions, cutoff, periodic)
310}
311
312pub(super) fn checked_indices(
317 selection: &AtomSelection,
318 position_count: usize,
319) -> Result<Cow<'_, [u32]>, SpatialError> {
320 if let AtomSelection::Sparse(indices) = selection {
321 for &atom in indices {
322 if usize::try_from(atom).map_err(|_| SpatialError::NumericRangeExceeded)?
323 >= position_count
324 {
325 return Err(SpatialError::AtomOutOfBounds(atom));
326 }
327 }
328 return Ok(Cow::Borrowed(indices));
329 }
330 if selection.is_empty() {
331 return Ok(Cow::Borrowed(&[]));
332 }
333 let capacity =
334 usize::try_from(selection.len()).map_err(|_| SpatialError::NumericRangeExceeded)?;
335 let mut indices = Vec::with_capacity(capacity);
336
337 for atom in selection {
338 let position = usize::try_from(atom).map_err(|_| SpatialError::NumericRangeExceeded)?;
339 if position >= position_count {
340 return Err(SpatialError::AtomOutOfBounds(atom));
341 }
342
343 indices.push(atom);
344 }
345
346 Ok(Cow::Owned(indices))
347}
348
349pub(super) fn validate_cutoff(cutoff: f32) -> Result<(), SpatialError> {
353 if cutoff.is_finite() && cutoff >= 0.0 {
354 Ok(())
355 } else {
356 Err(SpatialError::InvalidCutoff)
357 }
358}
359
360#[cfg(test)]
361#[path = "search_tests.rs"]
362mod tests;