Skip to content

kups.core.neighborlist.cell_list

Efficient O(N) neighbor list using spatial hashing with cell lists.

CellListNeighborList

Efficient O(N) neighbor list using spatial hashing with cell lists.

This is the recommended implementation when the cutoff is much smaller than the box size. It divides space into a grid of cells and only checks pairs in neighboring cells, achieving linear scaling with system size.

Honors the cell's per-axis periodic mask: stencil offsets that cross a non-periodic face are routed to an out-of-bounds bin (no key matches), and minimum-image shifts are zero on non-periodic axes. The fully-periodic path is byte-identical to the original (gated at trace time on all(periodic)) so PBC kernels see no overhead.

Complexity: O(N) for well-distributed particles where cutoff << box size. Efficiency improves as cutoff/box ratio decreases.

Attributes:

Name Type Description
avg_candidates Capacity[int]

Capacity for candidate pair storage (from cell list).

avg_edges Capacity[int]

Capacity for final edge array.

cells Capacity[int]

Capacity for cell hash table (grows with box_size³/cutoff³).

avg_image_candidates Capacity[int]

Capacity for image candidate pairs.

Algorithm
  1. Partition space into grid cells of size ~cutoff
  2. Hash each particle to its cell
  3. For each particle, check only neighboring 27 cells (3D)
  4. Filter candidates by actual distance
When to use
  • When cutoff/box_size << 1 (cutoff much smaller than box)
  • Typically cutoff/box < 0.3 for good efficiency
  • On non-periodic axes positions must lie inside [0, L) in real coordinates (the caller's invariant; out-of-range positions are silently routed to the OOB bin)
Example
# Example: 10 Å cutoff in 50 Å box → cutoff/box = 0.2 -- Good for CellList
nl = CellListNeighborList.new(state, lens(lambda s: s.nl_params))

# Or, if the state implements IsNeighborListState:
nl = CellListNeighborList.from_state(state)

edges = nl(particles, None, systems, cutoffs, None)
Source code in src/kups/core/neighborlist/cell_list.py
@dataclass
class CellListNeighborList:
    """Efficient O(N) neighbor list using spatial hashing with cell lists.

    This is the recommended implementation when the cutoff is much smaller than
    the box size. It divides space into a grid of cells and only checks pairs in
    neighboring cells, achieving linear scaling with system size.

    Honors the cell's per-axis ``periodic`` mask: stencil offsets that cross a
    non-periodic face are routed to an out-of-bounds bin (no key matches), and
    minimum-image shifts are zero on non-periodic axes. The fully-periodic path
    is byte-identical to the original (gated at trace time on ``all(periodic)``)
    so PBC kernels see no overhead.

    Complexity: O(N) for well-distributed particles where cutoff << box size.
    Efficiency improves as cutoff/box ratio decreases.

    Attributes:
        avg_candidates: Capacity for candidate pair storage (from cell list).
        avg_edges: Capacity for final edge array.
        cells: Capacity for cell hash table (grows with box_size³/cutoff³).
        avg_image_candidates: Capacity for image candidate pairs.

    Algorithm:
        1. Partition space into grid cells of size ~cutoff
        2. Hash each particle to its cell
        3. For each particle, check only neighboring 27 cells (3D)
        4. Filter candidates by actual distance

    When to use:
        - When cutoff/box_size << 1 (cutoff much smaller than box)
        - Typically cutoff/box < 0.3 for good efficiency
        - On non-periodic axes positions must lie inside ``[0, L)`` in real
          coordinates (the caller's invariant; out-of-range positions are
          silently routed to the OOB bin)

    Example:
        ```python
        # Example: 10 Å cutoff in 50 Å box → cutoff/box = 0.2 -- Good for CellList
        nl = CellListNeighborList.new(state, lens(lambda s: s.nl_params))

        # Or, if the state implements IsNeighborListState:
        nl = CellListNeighborList.from_state(state)

        edges = nl(particles, None, systems, cutoffs, None)
        ```
    """

    avg_candidates: Capacity[int]
    avg_edges: Capacity[int]
    cells: Capacity[int]
    avg_image_candidates: Capacity[int]

    @classmethod
    def new[S](cls, state: S, lens: Lens[S, IsCellListParams]) -> CellListNeighborList:
        params = lens.get(state)
        return CellListNeighborList(
            avg_candidates=LensCapacity(
                params.avg_candidates, lens.focus(lambda x: x.avg_candidates)
            ),
            avg_edges=LensCapacity(params.avg_edges, lens.focus(lambda x: x.avg_edges)),
            avg_image_candidates=LensCapacity(
                params.avg_image_candidates,
                lens.focus(lambda x: x.avg_image_candidates),
            ),
            cells=LensCapacity(params.cells, lens.focus(lambda x: x.cells), base=1),
        )

    @classmethod
    def from_state(
        cls, state: IsNeighborListState[IsCellListParams]
    ) -> CellListNeighborList:
        return cls.new(state, lens(lambda s: s.neighborlist_params))

    @jit
    def __call__(
        self,
        lh: Table[ParticleId, NeighborListPoints],
        rh: Table[ParticleId, NeighborListPoints] | None,
        systems: Table[SystemId, NeighborListSystems],
        cutoffs: Table[SystemId, Array],
        rh_index_remap: Index[ParticleId] | None = None,
    ) -> Edges[Literal[2]]:
        rh_size = rh.size if rh is not None else lh.size
        cutoffs = Table.broadcast_to(cutoffs, systems)
        pipeline = Pipeline[Literal[2]](
            selector=CellListSelector(
                cutoffs=cutoffs,
                max_cells=self.cells,
                max_candidates=self.avg_candidates.multiply(rh_size),
                max_image_candidates=self.avg_image_candidates.multiply(rh_size),
            ),
            masks=(
                InBoundsMask(),
                InclusionMatchMask(),
                RemapDedupMask(),
                DistanceCutoffMask(cutoffs=cutoffs),
                ExclusionMask(),
            ),
            compactor=ReduceCompactor(avg_edges=self.avg_edges.multiply(rh_size)),
        )
        return pipeline(lh, rh, systems, rh_index_remap)

CellListSelector

Selector for the cell-list algorithm.

Calls the raw spatial-hash candidate emission, then replicates per image multiplicity when max(cutoff/perp) > 0.5.

Source code in src/kups/core/neighborlist/cell_list.py
@dataclass
class CellListSelector:
    """Selector for the cell-list algorithm.

    Calls the raw spatial-hash candidate emission, then replicates per image
    multiplicity when ``max(cutoff/perp) > 0.5``.
    """

    cutoffs: Table[SystemId, Array]
    max_cells: Capacity[int]
    max_candidates: Capacity[int]
    max_image_candidates: Capacity[int]

    def __call__(self, ctx: PipelineContext) -> CandidateBatch[Literal[2]]:
        candidates = _cell_list_subselect(
            ctx.lh,
            ctx.rh,
            ctx.systems,
            cutoffs=self.cutoffs.data,
            max_num_cells=self.max_cells,
            max_num_candidates=self.max_candidates,
        )
        return replicate_for_images(
            candidates,
            ctx.lh,
            ctx.rh,
            ctx.systems,
            self.cutoffs,
            self.max_image_candidates,
        )

IsCellListParams

Bases: Protocol

Protocol for parameters required by CellListNeighborList.

Source code in src/kups/core/neighborlist/cell_list.py
class IsCellListParams(Protocol):
    """Protocol for parameters required by ``CellListNeighborList``."""

    @property
    def avg_candidates(self) -> int: ...
    @property
    def avg_edges(self) -> int: ...
    @property
    def cells(self) -> int: ...
    @property
    def avg_image_candidates(self) -> int: ...