Skip to main content

molframe_spatial/backends/
cell.rs

1//! Contiguous fixed-radius cell list.
2
3use super::errors::{validate_cutoff, validate_indices};
4use crate::brute::{canonicalise, finite};
5use crate::{CellGridOptions, NeighborPair, PeriodicBox, SpatialError};
6
7mod budget;
8mod build;
9use build::build_grid;
10mod coords;
11use coords::{cell_of, linear, linear_index, members_of_cell, neighbour_cell, raw_cell};
12mod geometry;
13mod ordered_merge;
14mod owned;
15use geometry::{Layout, choose_layout, finite_bounds};
16mod periodic_grid;
17use periodic_grid::PeriodicGrid;
18mod unique;
19pub(crate) use unique::{
20    for_each_pairs_same_selection_unordered, pairs_same_selection, pairs_same_selection_parallel,
21    pairs_same_selection_unordered, reduce_pairs_same_selection_parallel,
22};
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/// Linear id of a cell. Wide enough for three axes of any single-precision
57/// extent, so a sparse grid never has to coarsen its cells for a far outlier.
58type CellId = u128;
59
60#[derive(Debug)]
61struct Grid {
62    origin: [f32; 3],
63    edge: f32,
64    dims: [usize; 3],
65    offsets: Vec<usize>,
66    members: Vec<u32>,
67    /// Linear ids of occupied cells in ascending order.
68    non_empty_cells: Vec<CellId>,
69    largest_cell: usize,
70    /// When set, `offsets` is indexed by position in `non_empty_cells` rather
71    /// than by linear cell id, so memory is proportional to the atom count.
72    sparse: bool,
73}
74
75#[derive(Debug)]
76enum CellGrid {
77    Cartesian(Grid),
78    Periodic(PeriodicGrid),
79}
80
81/// Fixed-radius grid built in O(n) with one contiguous member array.
82#[derive(Debug)]
83pub struct CellList<'a> {
84    positions: &'a [[f32; 3]],
85    cutoff: f32,
86    periodic: Option<PeriodicBox>,
87    grid: Option<CellGrid>,
88    reservation: Option<molframe_core::MemoryReservation>,
89}
90
91impl<'a> CellList<'a> {
92    /// Builds an index over `targets`.
93    ///
94    /// # Errors
95    ///
96    /// Returns an error for an invalid cutoff or target index.
97    pub fn build(
98        positions: &'a [[f32; 3]],
99        targets: &[u32],
100        cutoff: f32,
101        periodic: Option<PeriodicBox>,
102    ) -> Result<Self, SpatialError> {
103        Self::build_with_options(
104            positions,
105            targets,
106            cutoff,
107            periodic,
108            CellGridOptions::default(),
109        )
110    }
111
112    /// Builds an index under an explicit cell-grid memory policy.
113    ///
114    /// # Errors
115    ///
116    /// Returns an error for invalid options, cutoff or target index.
117    pub fn build_with_options(
118        positions: &'a [[f32; 3]],
119        targets: &[u32],
120        cutoff: f32,
121        periodic: Option<PeriodicBox>,
122        options: CellGridOptions,
123    ) -> Result<Self, SpatialError> {
124        validate_cutoff(cutoff)?;
125        validate_indices(targets, positions.len())?;
126        options.validate()?;
127
128        let grid = match periodic.as_ref() {
129            Some(periodic) => Some(CellGrid::Periodic(PeriodicGrid::build(
130                positions, targets, cutoff, periodic, options,
131            )?)),
132            None => build_grid(positions, targets, cutoff, options)?.map(CellGrid::Cartesian),
133        };
134
135        Ok(Self {
136            positions,
137            cutoff,
138            periodic,
139            grid,
140            reservation: None,
141        })
142    }
143
144    /// Finds pairs for the indexed targets.
145    ///
146    /// # Errors
147    ///
148    /// Returns an error when the requested cutoff exceeds the build cutoff or
149    /// a query index is out of range.
150    pub fn pairs(&self, query: &[u32], cutoff: f32) -> Result<Vec<NeighborPair>, SpatialError> {
151        let mut found = Vec::with_capacity(query.len());
152        self.for_each_pair(query, cutoff, |pair| found.push(pair))?;
153        canonicalise(&mut found);
154        Ok(found)
155    }
156
157    /// Visits pairs for the indexed targets without retaining a result vector.
158    pub(crate) fn for_each_pair(
159        &self,
160        query: &[u32],
161        cutoff: f32,
162        mut emit: impl FnMut(NeighborPair),
163    ) -> Result<(), SpatialError> {
164        self.for_each_candidate(query, cutoff, |atom, target, squared| {
165            emit(NeighborPair::new(atom, target, squared));
166        })
167    }
168
169    /// Visits oriented query-target candidates without retaining pairs.
170    ///
171    /// Self-pairs are omitted. Overlapping query and target sets may emit both
172    /// orientations; callers computing symmetric results must choose ownership.
173    ///
174    /// # Errors
175    ///
176    /// Returns invalid query indices or a cutoff larger than the indexed cutoff.
177    pub fn for_each_candidate(
178        &self,
179        query: &[u32],
180        cutoff: f32,
181        mut emit: impl FnMut(u32, u32, f32),
182    ) -> Result<(), SpatialError> {
183        validate_query_cutoff(cutoff, self.cutoff)?;
184        validate_indices(query, self.positions.len())?;
185
186        match &self.grid {
187            Some(CellGrid::Cartesian(grid)) => {
188                grid_for_each_pair::<false>(self.positions, grid, query, cutoff * cutoff, &mut emit)
189            }
190            Some(CellGrid::Periodic(grid)) => {
191                let Some(periodic) = &self.periodic else {
192                    return Err(SpatialError::InvalidCell);
193                };
194                grid.for_each_pair(self.positions, query, cutoff * cutoff, periodic, &mut emit)
195            }
196            None => Ok(()),
197        }
198    }
199
200    /// Visits indexed atoms within `cutoff` of an arbitrary Cartesian point.
201    ///
202    /// Emits target indices and squared distances in ångström² without
203    /// allocation. Unlike atom queries, a coincident target is included.
204    /// Periodic indices use the same minimum-image metric as pair queries.
205    ///
206    /// # Errors
207    ///
208    /// Returns an error for a nonfinite point or a cutoff exceeding the index.
209    pub fn for_each_neighbor(
210        &self,
211        position: [f32; 3],
212        cutoff: f32,
213        mut emit: impl FnMut(u32, f32),
214    ) -> Result<(), SpatialError> {
215        validate_query_cutoff(cutoff, self.cutoff)?;
216        if !finite(position) {
217            return Err(SpatialError::NumericRangeExceeded);
218        }
219        match &self.grid {
220            Some(CellGrid::Cartesian(grid)) => visit::grid_for_each_neighbor(
221                self.positions,
222                grid,
223                position,
224                cutoff * cutoff,
225                &mut emit,
226            ),
227            Some(CellGrid::Periodic(grid)) => {
228                let periodic = self.periodic.as_ref().ok_or(SpatialError::InvalidCell)?;
229                grid.for_each_neighbor(
230                    self.positions,
231                    position,
232                    cutoff * cutoff,
233                    periodic,
234                    &mut emit,
235                )
236            }
237            None => Ok(()),
238        }
239    }
240}
241
242/// Validates a query cutoff against the cutoff used to construct the grid.
243///
244/// Runtime and auxiliary space are `O(1)`.
245fn validate_query_cutoff(cutoff: f32, build_cutoff: f32) -> Result<(), SpatialError> {
246    if cutoff.is_finite() && cutoff >= 0.0 && cutoff <= build_cutoff {
247        Ok(())
248    } else {
249        Err(SpatialError::InvalidCutoff)
250    }
251}
252
253#[cfg(test)]
254#[path = "cell_tests.rs"]
255mod tests;