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
193fn build_grid(
198 positions: &[[f32; 3]],
199 targets: &[u32],
200 cutoff: f32,
201 options: CellGridOptions,
202) -> Result<Option<Grid>, SpatialError> {
203 let Some(bounds) = finite_bounds(positions, targets) else {
204 return Ok(None);
205 };
206 let (edge, dims, count) = if cutoff == 0.0 {
207 (1.0, [1; 3], 1)
208 } else {
209 grid_geometry(bounds.min, bounds.max, cutoff, options)?
210 };
211
212 let mut counts = count_members(positions, targets, bounds.min, edge, dims, count)?;
213 let mut non_empty_cells = Vec::with_capacity(bounds.finite_count.min(count));
214 let mut largest_cell = 0;
215 for (cell, &count) in counts.iter().enumerate() {
216 largest_cell = largest_cell.max(count);
217 if count != 0 {
218 non_empty_cells.push(cell);
219 }
220 }
221 let offsets = prefix_offsets(&counts)?;
222 let cell_count = counts.len();
223 counts.copy_from_slice(&offsets[..cell_count]);
224 let members = fill_members(
225 positions,
226 targets,
227 bounds.min,
228 edge,
229 dims,
230 &mut counts,
231 bounds.finite_count,
232 )?;
233
234 Ok(Some(Grid {
235 origin: bounds.min,
236 edge,
237 dims,
238 offsets,
239 members,
240 non_empty_cells,
241 largest_cell,
242 }))
243}
244
245fn count_members(
249 positions: &[[f32; 3]],
250 targets: &[u32],
251 origin: [f32; 3],
252 edge: f32,
253 dims: [usize; 3],
254 count: usize,
255) -> Result<Vec<usize>, SpatialError> {
256 let mut counts = vec![0usize; count];
257
258 for &target in targets {
259 let Ok(index) = usize::try_from(target) else {
260 return Err(SpatialError::NumericRangeExceeded);
261 };
262 let Some(position) = positions.get(index).copied() else {
263 continue;
264 };
265
266 if !finite(position) {
267 continue;
268 }
269
270 let cell = linear(dims, raw_cell(origin, edge, dims, position)?);
271
272 if let Some(slot) = counts.get_mut(cell) {
273 *slot += 1;
274 }
275 }
276
277 Ok(counts)
278}
279
280fn prefix_offsets(counts: &[usize]) -> Result<Vec<usize>, SpatialError> {
284 let capacity = counts
285 .len()
286 .checked_add(1)
287 .ok_or(SpatialError::NumericRangeExceeded)?;
288 let mut offsets = Vec::with_capacity(capacity);
289 let mut total = 0usize;
290
291 offsets.push(total);
292
293 for &count in counts {
294 total = total
295 .checked_add(count)
296 .ok_or(SpatialError::NumericRangeExceeded)?;
297 offsets.push(total);
298 }
299
300 Ok(offsets)
301}
302
303fn fill_members(
308 positions: &[[f32; 3]],
309 targets: &[u32],
310 origin: [f32; 3],
311 edge: f32,
312 dims: [usize; 3],
313 cursors: &mut [usize],
314 finite_count: usize,
315) -> Result<Vec<u32>, SpatialError> {
316 let mut members = vec![0u32; finite_count];
317
318 for &target in targets {
319 let Ok(index) = usize::try_from(target) else {
320 return Err(SpatialError::NumericRangeExceeded);
321 };
322 let Some(position) = positions.get(index).copied() else {
323 continue;
324 };
325
326 if !finite(position) {
327 continue;
328 }
329
330 let cell = linear(dims, raw_cell(origin, edge, dims, position)?);
331
332 let Some(cursor) = cursors.get_mut(cell) else {
333 continue;
334 };
335
336 let Some(slot) = members.get_mut(*cursor) else {
337 continue;
338 };
339
340 *slot = target;
341 *cursor += 1;
342 }
343
344 Ok(members)
345}
346
347fn raw_cell(
351 origin: [f32; 3],
352 edge: f32,
353 dims: [usize; 3],
354 point: [f32; 3],
355) -> Result<[usize; 3], SpatialError> {
356 let convert = |axis: usize| {
357 let scaled = f64::from((point[axis] - origin[axis]) / edge);
358 if scaled.is_finite() && scaled <= 0.0 {
359 return Ok(0);
360 }
361 let last = dims[axis]
362 .checked_sub(1)
363 .ok_or(SpatialError::NumericRangeExceeded)?;
364 crate::numeric::floor_usize(scaled)
365 .map(|raw| raw.min(last))
366 .ok_or(SpatialError::NumericRangeExceeded)
367 };
368 Ok([convert(0)?, convert(1)?, convert(2)?])
369}
370
371#[inline]
375fn cell_of(grid: &Grid, point: [f32; 3]) -> Result<[usize; 3], SpatialError> {
376 raw_cell(grid.origin, grid.edge, grid.dims, point)
377}
378
379#[inline]
383fn linear(dims: [usize; 3], cell: [usize; 3]) -> usize {
384 (cell[0] * dims[1] + cell[1]) * dims[2] + cell[2]
385}
386
387fn neighbour_cell(dims: [usize; 3], centre: [usize; 3], delta: [isize; 3]) -> Option<usize> {
391 let mut cell = [0usize; 3];
392
393 for axis in 0..3 {
394 let coordinate = centre[axis].checked_add_signed(delta[axis])?;
395
396 if coordinate >= dims[axis] {
397 return None;
398 }
399
400 cell[axis] = coordinate;
401 }
402
403 Some(linear(dims, cell))
404}
405
406fn members_of_cell(grid: &Grid, cell: usize) -> Option<&[u32]> {
410 let &start = grid.offsets.get(cell)?;
411 let next = cell.checked_add(1)?;
412 let &end = grid.offsets.get(next)?;
413
414 grid.members.get(start..end)
415}
416
417fn validate_cutoff(cutoff: f32) -> Result<(), SpatialError> {
421 if cutoff.is_finite() && cutoff >= 0.0 {
422 Ok(())
423 } else {
424 Err(SpatialError::InvalidCutoff)
425 }
426}
427
428fn validate_query_cutoff(cutoff: f32, build_cutoff: f32) -> Result<(), SpatialError> {
432 if cutoff.is_finite() && cutoff >= 0.0 && cutoff <= build_cutoff {
433 Ok(())
434 } else {
435 Err(SpatialError::InvalidCutoff)
436 }
437}
438
439fn validate_indices(atoms: &[u32], position_count: usize) -> Result<(), SpatialError> {
443 for &atom in atoms {
444 let index = usize::try_from(atom).map_err(|_| SpatialError::NumericRangeExceeded)?;
445 if index >= position_count {
446 return Err(SpatialError::AtomOutOfBounds(atom));
447 }
448 }
449
450 Ok(())
451}
452
453#[cfg(test)]
454#[path = "cell_tests.rs"]
455mod tests;