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    /// Visits indexed atoms within `cutoff` of an arbitrary Cartesian point.
193    ///
194    /// Emits target indices and squared distances in ångström² without
195    /// allocation. Unlike atom queries, a coincident target is included.
196    /// Periodic indices use the same minimum-image metric as pair queries.
197    ///
198    /// # Errors
199    ///
200    /// Returns an error for a nonfinite point or a cutoff exceeding the index.
201    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
234/// Builds the contiguous grid representation for finite target coordinates.
235///
236/// Building is `O(T + C)` for `T` targets and `C` cells and uses contiguous
237/// count, offset and member arrays.
238fn 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
286/// Computes one count per grid cell.
287///
288/// Runtime is `O(T)` with `O(C)` count storage.
289fn 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
321/// Converts per-cell counts into CSR-style prefix offsets.
322///
323/// Runtime is `O(C)` and output space is `O(C)`.
324fn 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
344/// Fills the contiguous CSR-style member array.
345///
346/// Runtime is `O(T + C)` and output space is exactly one `u32` per finite
347/// target occurrence.
348fn 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
388/// Converts a Cartesian point into clamped cell coordinates.
389///
390/// Runtime and auxiliary space are `O(1)`.
391fn 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/// Converts a Cartesian point into this grid's cell coordinate.
413///
414/// Runtime and auxiliary space are `O(1)`.
415#[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/// Converts three-dimensional cell coordinates to a linear cell index.
421///
422/// Runtime and auxiliary space are `O(1)`.
423#[inline]
424fn linear(dims: [usize; 3], cell: [usize; 3]) -> usize {
425    (cell[0] * dims[1] + cell[1]) * dims[2] + cell[2]
426}
427
428/// Returns a neighbouring linear cell index when it remains in bounds.
429///
430/// Runtime and auxiliary space are `O(1)`.
431fn 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
447/// Returns the contiguous target-member slice belonging to `cell`.
448///
449/// Invalid internal offsets defensively produce an empty slice.
450fn 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
458/// Validates a non-negative finite cutoff.
459///
460/// Runtime and auxiliary space are `O(1)`.
461fn 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
469/// Validates a query cutoff against the cutoff used to construct the grid.
470///
471/// Runtime and auxiliary space are `O(1)`.
472fn 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
480/// Validates atom indices against a coordinate-array length.
481///
482/// Runtime is `O(N)` and no allocation is performed.
483fn 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;