1use crate::brute::{canonicalise, finite};
4use crate::{CellGridOptions, NeighborPair, PeriodicBox, SpatialError};
5
6#[path = "cell/budget.rs"]
7mod budget;
8#[path = "cell/geometry.rs"]
9mod geometry;
10#[path = "cell/owned.rs"]
11mod owned;
12use geometry::{finite_bounds, grid_geometry};
13#[path = "cell/periodic.rs"]
14mod periodic_grid;
15use periodic_grid::PeriodicGrid;
16#[path = "cell/unique.rs"]
17mod unique;
18pub(crate) use unique::{
19 for_each_pairs_same_selection_unordered, pairs_same_selection, pairs_same_selection_parallel,
20 pairs_same_selection_unordered, reduce_pairs_same_selection_parallel,
21};
22#[path = "cell/visit.rs"]
23mod visit;
24use visit::grid_for_each_pair;
25
26const NEIGHBOUR_OFFSETS: [[isize; 3]; 27] = [
27 [-1, -1, -1],
28 [-1, -1, 0],
29 [-1, -1, 1],
30 [-1, 0, -1],
31 [-1, 0, 0],
32 [-1, 0, 1],
33 [-1, 1, -1],
34 [-1, 1, 0],
35 [-1, 1, 1],
36 [0, -1, -1],
37 [0, -1, 0],
38 [0, -1, 1],
39 [0, 0, -1],
40 [0, 0, 0],
41 [0, 0, 1],
42 [0, 1, -1],
43 [0, 1, 0],
44 [0, 1, 1],
45 [1, -1, -1],
46 [1, -1, 0],
47 [1, -1, 1],
48 [1, 0, -1],
49 [1, 0, 0],
50 [1, 0, 1],
51 [1, 1, -1],
52 [1, 1, 0],
53 [1, 1, 1],
54];
55
56#[derive(Debug)]
57struct Grid {
58 origin: [f32; 3],
59 edge: f32,
60 dims: [usize; 3],
61 offsets: Vec<usize>,
62 members: Vec<u32>,
63 non_empty_cells: Vec<usize>,
64 largest_cell: usize,
65}
66
67#[derive(Debug)]
68enum CellGrid {
69 Cartesian(Grid),
70 Periodic(PeriodicGrid),
71}
72
73#[derive(Debug)]
75pub struct CellList<'a> {
76 positions: &'a [[f32; 3]],
77 cutoff: f32,
78 periodic: Option<PeriodicBox>,
79 grid: Option<CellGrid>,
80 reservation: Option<molframe_core::MemoryReservation>,
81}
82
83impl<'a> CellList<'a> {
84 pub fn build(
90 positions: &'a [[f32; 3]],
91 targets: &[u32],
92 cutoff: f32,
93 periodic: Option<PeriodicBox>,
94 ) -> Result<Self, SpatialError> {
95 Self::build_with_options(
96 positions,
97 targets,
98 cutoff,
99 periodic,
100 CellGridOptions::default(),
101 )
102 }
103
104 pub fn build_with_options(
110 positions: &'a [[f32; 3]],
111 targets: &[u32],
112 cutoff: f32,
113 periodic: Option<PeriodicBox>,
114 options: CellGridOptions,
115 ) -> Result<Self, SpatialError> {
116 validate_cutoff(cutoff)?;
117 validate_indices(targets, positions.len())?;
118 options.validate()?;
119
120 let grid = match periodic.as_ref() {
121 Some(periodic) => Some(CellGrid::Periodic(PeriodicGrid::build(
122 positions, targets, cutoff, periodic, options,
123 )?)),
124 None => build_grid(positions, targets, cutoff, options)?.map(CellGrid::Cartesian),
125 };
126
127 Ok(Self {
128 positions,
129 cutoff,
130 periodic,
131 grid,
132 reservation: None,
133 })
134 }
135
136 pub fn pairs(&self, query: &[u32], cutoff: f32) -> Result<Vec<NeighborPair>, SpatialError> {
143 let mut found = Vec::with_capacity(query.len());
144 self.for_each_pair(query, cutoff, |pair| found.push(pair))?;
145 canonicalise(&mut found);
146 Ok(found)
147 }
148
149 pub(crate) fn for_each_pair(
151 &self,
152 query: &[u32],
153 cutoff: f32,
154 mut emit: impl FnMut(NeighborPair),
155 ) -> Result<(), SpatialError> {
156 self.for_each_candidate(query, cutoff, |atom, target, squared| {
157 emit(NeighborPair::new(atom, target, squared));
158 })
159 }
160
161 pub fn for_each_candidate(
170 &self,
171 query: &[u32],
172 cutoff: f32,
173 mut emit: impl FnMut(u32, u32, f32),
174 ) -> Result<(), SpatialError> {
175 validate_query_cutoff(cutoff, self.cutoff)?;
176 validate_indices(query, self.positions.len())?;
177
178 match &self.grid {
179 Some(CellGrid::Cartesian(grid)) => {
180 grid_for_each_pair::<false>(self.positions, grid, query, cutoff * cutoff, &mut emit)
181 }
182 Some(CellGrid::Periodic(grid)) => {
183 let Some(periodic) = &self.periodic else {
184 return Err(SpatialError::InvalidCell);
185 };
186 grid.for_each_pair(self.positions, query, cutoff * cutoff, periodic, &mut emit)
187 }
188 None => Ok(()),
189 }
190 }
191
192 pub fn for_each_neighbor(
202 &self,
203 position: [f32; 3],
204 cutoff: f32,
205 mut emit: impl FnMut(u32, f32),
206 ) -> Result<(), SpatialError> {
207 validate_query_cutoff(cutoff, self.cutoff)?;
208 if !finite(position) {
209 return Err(SpatialError::NumericRangeExceeded);
210 }
211 match &self.grid {
212 Some(CellGrid::Cartesian(grid)) => visit::grid_for_each_neighbor(
213 self.positions,
214 grid,
215 position,
216 cutoff * cutoff,
217 &mut emit,
218 ),
219 Some(CellGrid::Periodic(grid)) => {
220 let periodic = self.periodic.as_ref().ok_or(SpatialError::InvalidCell)?;
221 grid.for_each_neighbor(
222 self.positions,
223 position,
224 cutoff * cutoff,
225 periodic,
226 &mut emit,
227 )
228 }
229 None => Ok(()),
230 }
231 }
232}
233
234fn build_grid(
239 positions: &[[f32; 3]],
240 targets: &[u32],
241 cutoff: f32,
242 options: CellGridOptions,
243) -> Result<Option<Grid>, SpatialError> {
244 let Some(bounds) = finite_bounds(positions, targets) else {
245 return Ok(None);
246 };
247 let (edge, dims, count) = if cutoff == 0.0 {
248 (1.0, [1; 3], 1)
249 } else {
250 grid_geometry(bounds.min, bounds.max, cutoff, options)?
251 };
252
253 let mut counts = count_members(positions, targets, bounds.min, edge, dims, count)?;
254 let mut non_empty_cells = Vec::with_capacity(bounds.finite_count.min(count));
255 let mut largest_cell = 0;
256 for (cell, &count) in counts.iter().enumerate() {
257 largest_cell = largest_cell.max(count);
258 if count != 0 {
259 non_empty_cells.push(cell);
260 }
261 }
262 let offsets = prefix_offsets(&counts)?;
263 let cell_count = counts.len();
264 counts.copy_from_slice(&offsets[..cell_count]);
265 let members = fill_members(
266 positions,
267 targets,
268 bounds.min,
269 edge,
270 dims,
271 &mut counts,
272 bounds.finite_count,
273 )?;
274
275 Ok(Some(Grid {
276 origin: bounds.min,
277 edge,
278 dims,
279 offsets,
280 members,
281 non_empty_cells,
282 largest_cell,
283 }))
284}
285
286fn count_members(
290 positions: &[[f32; 3]],
291 targets: &[u32],
292 origin: [f32; 3],
293 edge: f32,
294 dims: [usize; 3],
295 count: usize,
296) -> Result<Vec<usize>, SpatialError> {
297 let mut counts = vec![0usize; count];
298
299 for &target in targets {
300 let Ok(index) = usize::try_from(target) else {
301 return Err(SpatialError::NumericRangeExceeded);
302 };
303 let Some(position) = positions.get(index).copied() else {
304 continue;
305 };
306
307 if !finite(position) {
308 continue;
309 }
310
311 let cell = linear(dims, raw_cell(origin, edge, dims, position)?);
312
313 if let Some(slot) = counts.get_mut(cell) {
314 *slot += 1;
315 }
316 }
317
318 Ok(counts)
319}
320
321fn prefix_offsets(counts: &[usize]) -> Result<Vec<usize>, SpatialError> {
325 let capacity = counts
326 .len()
327 .checked_add(1)
328 .ok_or(SpatialError::NumericRangeExceeded)?;
329 let mut offsets = Vec::with_capacity(capacity);
330 let mut total = 0usize;
331
332 offsets.push(total);
333
334 for &count in counts {
335 total = total
336 .checked_add(count)
337 .ok_or(SpatialError::NumericRangeExceeded)?;
338 offsets.push(total);
339 }
340
341 Ok(offsets)
342}
343
344fn fill_members(
349 positions: &[[f32; 3]],
350 targets: &[u32],
351 origin: [f32; 3],
352 edge: f32,
353 dims: [usize; 3],
354 cursors: &mut [usize],
355 finite_count: usize,
356) -> Result<Vec<u32>, SpatialError> {
357 let mut members = vec![0u32; finite_count];
358
359 for &target in targets {
360 let Ok(index) = usize::try_from(target) else {
361 return Err(SpatialError::NumericRangeExceeded);
362 };
363 let Some(position) = positions.get(index).copied() else {
364 continue;
365 };
366
367 if !finite(position) {
368 continue;
369 }
370
371 let cell = linear(dims, raw_cell(origin, edge, dims, position)?);
372
373 let Some(cursor) = cursors.get_mut(cell) else {
374 continue;
375 };
376
377 let Some(slot) = members.get_mut(*cursor) else {
378 continue;
379 };
380
381 *slot = target;
382 *cursor += 1;
383 }
384
385 Ok(members)
386}
387
388fn raw_cell(
392 origin: [f32; 3],
393 edge: f32,
394 dims: [usize; 3],
395 point: [f32; 3],
396) -> Result<[usize; 3], SpatialError> {
397 let convert = |axis: usize| {
398 let scaled = f64::from((point[axis] - origin[axis]) / edge);
399 if scaled.is_finite() && scaled <= 0.0 {
400 return Ok(0);
401 }
402 let last = dims[axis]
403 .checked_sub(1)
404 .ok_or(SpatialError::NumericRangeExceeded)?;
405 crate::numeric::floor_usize(scaled)
406 .map(|raw| raw.min(last))
407 .ok_or(SpatialError::NumericRangeExceeded)
408 };
409 Ok([convert(0)?, convert(1)?, convert(2)?])
410}
411
412#[inline]
416fn cell_of(grid: &Grid, point: [f32; 3]) -> Result<[usize; 3], SpatialError> {
417 raw_cell(grid.origin, grid.edge, grid.dims, point)
418}
419
420#[inline]
424fn linear(dims: [usize; 3], cell: [usize; 3]) -> usize {
425 (cell[0] * dims[1] + cell[1]) * dims[2] + cell[2]
426}
427
428fn neighbour_cell(dims: [usize; 3], centre: [usize; 3], delta: [isize; 3]) -> Option<usize> {
432 let mut cell = [0usize; 3];
433
434 for axis in 0..3 {
435 let coordinate = centre[axis].checked_add_signed(delta[axis])?;
436
437 if coordinate >= dims[axis] {
438 return None;
439 }
440
441 cell[axis] = coordinate;
442 }
443
444 Some(linear(dims, cell))
445}
446
447fn members_of_cell(grid: &Grid, cell: usize) -> Option<&[u32]> {
451 let &start = grid.offsets.get(cell)?;
452 let next = cell.checked_add(1)?;
453 let &end = grid.offsets.get(next)?;
454
455 grid.members.get(start..end)
456}
457
458fn validate_cutoff(cutoff: f32) -> Result<(), SpatialError> {
462 if cutoff.is_finite() && cutoff >= 0.0 {
463 Ok(())
464 } else {
465 Err(SpatialError::InvalidCutoff)
466 }
467}
468
469fn validate_query_cutoff(cutoff: f32, build_cutoff: f32) -> Result<(), SpatialError> {
473 if cutoff.is_finite() && cutoff >= 0.0 && cutoff <= build_cutoff {
474 Ok(())
475 } else {
476 Err(SpatialError::InvalidCutoff)
477 }
478}
479
480fn validate_indices(atoms: &[u32], position_count: usize) -> Result<(), SpatialError> {
484 for &atom in atoms {
485 let index = usize::try_from(atom).map_err(|_| SpatialError::NumericRangeExceeded)?;
486 if index >= position_count {
487 return Err(SpatialError::AtomOutOfBounds(atom));
488 }
489 }
490
491 Ok(())
492}
493
494#[cfg(test)]
495#[path = "cell_tests.rs"]
496mod tests;