Skip to main content

molframe_spatial/backends/
cell.rs

1//! Contiguous fixed-radius cell list.
2
3use 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/// Fixed-radius grid built in O(n) with one contiguous member array.
74#[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    /// Builds an index over `targets`.
85    ///
86    /// # Errors
87    ///
88    /// Returns an error for an invalid cutoff or target index.
89    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    /// Builds an index under an explicit cell-grid memory policy.
105    ///
106    /// # Errors
107    ///
108    /// Returns an error for invalid options, cutoff or target index.
109    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    /// Finds pairs for the indexed targets.
137    ///
138    /// # Errors
139    ///
140    /// Returns an error when the requested cutoff exceeds the build cutoff or
141    /// a query index is out of range.
142    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    /// Visits pairs for the indexed targets without retaining a result vector.
150    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    /// Visits oriented query-target candidates without retaining pairs.
162    ///
163    /// Self-pairs are omitted. Overlapping query and target sets may emit both
164    /// orientations; callers computing symmetric results must choose ownership.
165    ///
166    /// # Errors
167    ///
168    /// Returns invalid query indices or a cutoff larger than the indexed cutoff.
169    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
193/// Builds the contiguous grid representation for finite target coordinates.
194///
195/// Building is `O(T + C)` for `T` targets and `C` cells and uses contiguous
196/// count, offset and member arrays.
197fn 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
245/// Computes one count per grid cell.
246///
247/// Runtime is `O(T)` with `O(C)` count storage.
248fn 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
280/// Converts per-cell counts into CSR-style prefix offsets.
281///
282/// Runtime is `O(C)` and output space is `O(C)`.
283fn 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
303/// Fills the contiguous CSR-style member array.
304///
305/// Runtime is `O(T + C)` and output space is exactly one `u32` per finite
306/// target occurrence.
307fn 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
347/// Converts a Cartesian point into clamped cell coordinates.
348///
349/// Runtime and auxiliary space are `O(1)`.
350fn 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/// Converts a Cartesian point into this grid's cell coordinate.
372///
373/// Runtime and auxiliary space are `O(1)`.
374#[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/// Converts three-dimensional cell coordinates to a linear cell index.
380///
381/// Runtime and auxiliary space are `O(1)`.
382#[inline]
383fn linear(dims: [usize; 3], cell: [usize; 3]) -> usize {
384    (cell[0] * dims[1] + cell[1]) * dims[2] + cell[2]
385}
386
387/// Returns a neighbouring linear cell index when it remains in bounds.
388///
389/// Runtime and auxiliary space are `O(1)`.
390fn 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
406/// Returns the contiguous target-member slice belonging to `cell`.
407///
408/// Invalid internal offsets defensively produce an empty slice.
409fn 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
417/// Validates a non-negative finite cutoff.
418///
419/// Runtime and auxiliary space are `O(1)`.
420fn 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
428/// Validates a query cutoff against the cutoff used to construct the grid.
429///
430/// Runtime and auxiliary space are `O(1)`.
431fn 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
439/// Validates atom indices against a coordinate-array length.
440///
441/// Runtime is `O(N)` and no allocation is performed.
442fn 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;