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 let mut backend = dispatch.plan.backend;
202 let switched = backend == SpatialBackend::CellList
205 && dispatch.plan.requested_backend == SpatialBackend::Auto
206 && super::crowding::is_crowded(positions, right, cutoff);
207 if switched {
208 backend = SpatialBackend::KdTree;
209 }
210 let cell_pairs = || {
211 let index = CellList::build_in(
212 positions,
213 right,
214 cutoff,
215 periodic.copied(),
216 dispatch.options.cell_grid,
217 context,
218 )?;
219 if same_selection {
220 let sorted = matches!(dispatch.order, PairOrder::Sorted);
221 if context.worker_budget() > 1 {
222 crate::backends::cell::pairs_same_selection_parallel(
223 &index, left, cutoff, context, sorted,
224 )
225 } else if sorted {
226 crate::backends::cell::pairs_same_selection(&index, left, cutoff)
227 } else {
228 crate::backends::cell::pairs_same_selection_unordered(&index, left, cutoff)
229 }
230 } else {
231 index.pairs(left, cutoff)
232 }
233 };
234
235 match backend {
236 SpatialBackend::CellList => cell_pairs(),
237 SpatialBackend::KdTree => {
238 let found = KdTree::build_in(
239 positions,
240 right,
241 periodic.copied(),
242 dispatch.options.kd_periodic,
243 context,
244 )
245 .and_then(|tree| {
246 tree.pairs_in(
247 left,
248 cutoff,
249 same_selection,
250 matches!(dispatch.order, PairOrder::Sorted),
251 context,
252 )
253 });
254 match found {
257 Err(SpatialError::PeriodicImageLimitExceeded { .. }) if switched => cell_pairs(),
258 other => other,
259 }
260 }
261 SpatialBackend::NeighborList => neighbor_list_pairs(
262 positions,
263 left,
264 right,
265 cutoff,
266 dispatch.plan.neighbor_skin,
267 dispatch.options,
268 periodic,
269 ),
270 SpatialBackend::BruteForce => {
271 let cutoff_squared = cutoff * cutoff;
272 if same_selection {
273 if matches!(dispatch.order, PairOrder::Sorted) {
274 Ok(brute::pairs_same_selection(
275 positions,
276 left,
277 cutoff_squared,
278 periodic,
279 ))
280 } else {
281 Ok(brute::pairs_same_selection_unordered(
282 positions,
283 left,
284 cutoff_squared,
285 periodic,
286 ))
287 }
288 } else {
289 Ok(brute::pairs(
290 positions,
291 left,
292 right,
293 cutoff_squared,
294 periodic,
295 ))
296 }
297 }
298 SpatialBackend::Auto => Err(SpatialError::InvalidOption(
299 crate::SpatialOption::UnresolvedBackend,
300 )),
301 }
302}
303
304fn neighbor_list_pairs(
309 positions: &[[f32; 3]],
310 left: &[u32],
311 right: &[u32],
312 cutoff: f32,
313 skin: Option<f32>,
314 options: SpatialSearchOptions,
315 periodic: Option<&PeriodicBox>,
316) -> Result<Vec<NeighborPair>, SpatialError> {
317 let Some(skin) = skin else {
318 return Err(SpatialError::InvalidOption(
319 crate::SpatialOption::NeighborSkinMinimum,
320 ));
321 };
322 let list = NeighborList::build_with_options(
323 positions,
324 left,
325 right,
326 cutoff,
327 periodic,
328 CoordinateGeneration::INITIAL,
329 crate::NeighborListOptions {
330 skin,
331 cell_grid: options.cell_grid,
332 },
333 )?;
334
335 list.pairs(positions, cutoff, periodic)
336}
337
338pub(super) fn checked_indices(
343 selection: &AtomSelection,
344 position_count: usize,
345) -> Result<Cow<'_, [u32]>, SpatialError> {
346 if let AtomSelection::Sparse(indices) = selection {
347 for &atom in indices {
348 if usize::try_from(atom).map_err(|_| SpatialError::NumericRangeExceeded)?
349 >= position_count
350 {
351 return Err(SpatialError::AtomOutOfBounds(atom));
352 }
353 }
354 return Ok(Cow::Borrowed(indices));
355 }
356 if selection.is_empty() {
357 return Ok(Cow::Borrowed(&[]));
358 }
359 let capacity =
360 usize::try_from(selection.len()).map_err(|_| SpatialError::NumericRangeExceeded)?;
361 let mut indices = Vec::with_capacity(capacity);
362
363 for atom in selection {
364 let position = usize::try_from(atom).map_err(|_| SpatialError::NumericRangeExceeded)?;
365 if position >= position_count {
366 return Err(SpatialError::AtomOutOfBounds(atom));
367 }
368
369 indices.push(atom);
370 }
371
372 Ok(Cow::Owned(indices))
373}
374
375pub(super) fn validate_cutoff(cutoff: f32) -> Result<(), SpatialError> {
379 if cutoff.is_finite() && cutoff >= 0.0 {
380 Ok(())
381 } else {
382 Err(SpatialError::InvalidCutoff)
383 }
384}
385
386#[cfg(test)]
387#[path = "search_parallel_tests.rs"]
388mod parallel_tests;
389#[cfg(test)]
390#[path = "search_tests.rs"]
391mod tests;